adam changes

This commit is contained in:
Adam
2025-07-22 14:59:00 -07:00
parent 809a00979a
commit 18029731ff
37 changed files with 2356 additions and 3915 deletions
+4 -19
View File
@@ -8,25 +8,10 @@ repository = ""
edition = "2021"
rust-version = "1.79"
[features]
default = ["gpu"]
gpu = ["graphite-editor/gpu"]
[dependencies]
# Local dependencies
graphite-editor = { path = "../editor", features = [
"gpu",
"ron",
"vello",
"decouple-execution",
] }
wgpu = { workspace = true }
winit = { workspace = true, features = ["serde"] }
base64.workspace = true
thiserror.workspace = true
pollster = "0.3"
cef = "138.5.0"
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
tracing = "0.1.41"
bytemuck = { version = "1.23.1", features = ["derive"] }
include_dir = "0.7.4"
thiserror = { workspace = true }
futures = { workspace = true }
cef = { workspace = true }
include_dir = { workspace = true }
+2 -1
View File
@@ -1,7 +1,8 @@
use std::fs::metadata;
fn main() {
let frontend_dir = format!("{}/../frontend/dist", env!("CARGO_MANIFEST_DIR"));
let frontend_dir = format!("{}/../frontend-native/dist", env!("CARGO_MANIFEST_DIR"));
println!("frontend_dir: {}", frontend_dir);
metadata(&frontend_dir).expect("Failed to find frontend directory. Please build the frontend first.");
metadata(format!("{}/index.html", &frontend_dir)).expect("Failed to find index.html in frontend directory.");
+8 -15
View File
@@ -4,10 +4,14 @@ use cef::{browser_host_create_browser_sync, initialize, BrowserSettings, Diction
use thiserror::Error;
use winit::event::WindowEvent;
use crate::cef::internal::OffscreenRenderHandler;
use crate::render::FrameBufferHandle;
use crate::WindowStateHandle;
use super::input::{handle_window_event, InputState};
use super::EventHandler;
use super::internal::{AppImpl, ClientImpl, NonBrowserAppImpl, RenderHandlerImpl};
use super::internal::{AppImpl, ClientImpl, OffscreenApp, RenderHandlerImpl};
pub(crate) struct Setup {}
pub(crate) struct Initialized {}
@@ -37,7 +41,7 @@ impl Context<Setup> {
let is_browser_process = cmd.has_switch(Some(&switch)) != 1;
if !is_browser_process {
let process_type = CefString::from(&cmd.switch_value(Some(&switch)));
let mut app = NonBrowserAppImpl::new();
let mut app = OffscreenApp::new();
let ret = execute_process(Some(args.as_main_args()), Some(&mut app), std::ptr::null_mut());
if ret >= 0 {
return Err(SetupError::SubprocessFailed(process_type.to_string()));
@@ -53,19 +57,8 @@ impl Context<Setup> {
})
}
pub(crate) fn init(self, event_handler: impl EventHandler) -> Result<Context<Initialized>, InitError> {
let mut settings = Settings::default();
settings.windowless_rendering_enabled = 1;
settings.multi_threaded_message_loop = 0;
let mut cef_app = AppImpl::new(event_handler.clone());
let res = initialize(Some(self.args.as_main_args()), Some(&settings), Some(&mut cef_app), std::ptr::null_mut());
if res != 1 {
return Err(InitError::InitializationFailed);
}
let render_handler = RenderHandlerImpl::new(event_handler.clone());
pub(crate) fn init(self, frame_buffer: FrameBufferHandle) -> Result<Context<Initialized>, InitError> {
let render_handler = OffscreenRenderHandler::new(frame_buffer);
let mut client = ClientImpl::new(render_handler);
let url = CefString::from("graphite://frontend/");
+1 -1
View File
@@ -23,7 +23,7 @@ pub(crate) fn handle_window_event(context: &mut Context<Initialized>, event: &Wi
context.input_state.update_mouse_position(position);
let mouse_event: MouseEvent = (&context.input_state).into();
browser.host().unwrap().send_mouse_move_event(Some(&mouse_event), 0);
// browser.host().unwrap().send_mouse_move_event(Some(&mouse_event), 0);
}
}
WindowEvent::MouseInput { state, button, .. } => {
+14 -10
View File
@@ -1,31 +1,35 @@
use cef::rc::{Rc, RcImpl};
use cef::sys::{_cef_app_t, cef_base_ref_counted_t};
use cef::{App, BrowserProcessHandler, ImplApp, SchemeRegistrar, WrapApp};
use cef::{App, BrowserProcessHandler, Frame, ImplApp, SchemeRegistrar, WrapApp};
use crate::cef::scheme_handler::GraphiteSchemeHandlerFactory;
use crate::cef::EventHandler;
use crate::render::{FrameBuffer, FrameBufferHandle};
use super::browser_process_handler::BrowserProcessHandlerImpl;
use super::browser_process_handler::OffscreenBrowserProcessHandler;
pub(crate) struct AppImpl<H: EventHandler> {
object: *mut RcImpl<_cef_app_t, Self>,
event_handler: H,
struct OffscreenApp {
object: *mut RcImpl<cef_dll_sys::_cef_app_t, Self>,
frame_buffer: Arc<Mutex<FrameBuffer>>,
}
impl<H: EventHandler> AppImpl<H> {
pub(crate) fn new(event_handler: H) -> App {
impl OffscreenApp {
fn new(frame_buffer: Arc<Mutex<FrameBuffer>>) -> App {
App::new(Self {
object: std::ptr::null_mut(),
event_handler,
frame_buffer,
})
}
}
impl<H: EventHandler> ImplApp for AppImpl<H> {
impl ImplApp for OffscreenApp {
fn browser_process_handler(&self) -> Option<BrowserProcessHandler> {
Some(BrowserProcessHandlerImpl::new(self.event_handler.clone()))
println!("browser_process_handler");
Some(OffscreenBrowserProcessHandler::new(self.frame_buffer.clone()))
}
fn on_register_custom_schemes(&self, registrar: Option<&mut SchemeRegistrar>) {
println!("on_register_custom_schemes");
GraphiteSchemeHandlerFactory::register_schemes(registrar);
}
@@ -3,22 +3,17 @@ use cef::sys::{_cef_browser_process_handler_t, cef_base_ref_counted_t, cef_brows
use cef::{BrowserProcessHandler, CefString, ImplBrowserProcessHandler, SchemeHandlerFactory, WrapBrowserProcessHandler};
use crate::cef::scheme_handler::GraphiteSchemeHandlerFactory;
use crate::cef::EventHandler;
pub(crate) struct BrowserProcessHandlerImpl<H: EventHandler> {
pub(crate) struct OffscreenBrowserProcessHandler {
object: *mut RcImpl<cef_browser_process_handler_t, Self>,
event_handler: H,
}
impl<H: EventHandler> BrowserProcessHandlerImpl<H> {
pub(crate) fn new(event_handler: H) -> BrowserProcessHandler {
BrowserProcessHandler::new(Self {
object: std::ptr::null_mut(),
event_handler,
})
impl OffscreenBrowserProcessHandler {
pub(crate) fn new() -> BrowserProcessHandler {
BrowserProcessHandler::new(Self { object: std::ptr::null_mut() })
}
}
impl<H: EventHandler> ImplBrowserProcessHandler for BrowserProcessHandlerImpl<H> {
impl ImplBrowserProcessHandler for OffscreenBrowserProcessHandler {
fn on_context_initialized(&self) {
cef::register_scheme_handler_factory(Some(&CefString::from("graphite")), None, Some(&mut SchemeHandlerFactory::new(GraphiteSchemeHandlerFactory::new())));
}
@@ -28,19 +23,16 @@ impl<H: EventHandler> ImplBrowserProcessHandler for BrowserProcessHandlerImpl<H>
}
}
impl<H: EventHandler> Clone for BrowserProcessHandlerImpl<H> {
impl Clone for OffscreenBrowserProcessHandler {
fn clone(&self) -> Self {
unsafe {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
}
Self {
object: self.object,
event_handler: self.event_handler.clone(),
}
Self { object: self.object }
}
}
impl<H: EventHandler> Rc for BrowserProcessHandlerImpl<H> {
impl Rc for OffscreenBrowserProcessHandler {
fn as_base(&self) -> &cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
@@ -48,7 +40,7 @@ impl<H: EventHandler> Rc for BrowserProcessHandlerImpl<H> {
}
}
}
impl<H: EventHandler> WrapBrowserProcessHandler for BrowserProcessHandlerImpl<H> {
impl WrapBrowserProcessHandler for OffscreenBrowserProcessHandler {
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_browser_process_handler_t, Self>) {
self.object = object;
}
+1
View File
@@ -2,6 +2,7 @@ use cef::rc::{Rc, RcImpl};
use cef::sys::{_cef_client_t, cef_base_ref_counted_t};
use cef::{Client, ImplClient, RenderHandler, WrapClient};
pub(crate) struct ClientImpl {
object: *mut RcImpl<_cef_client_t, Self>,
render_handler: RenderHandler,
+3 -5
View File
@@ -1,10 +1,8 @@
mod app;
mod browser_process_handler;
mod client;
mod non_browser_app;
mod offscreen_app;
mod render_handler;
pub(crate) use app::AppImpl;
pub(crate) use client::ClientImpl;
pub(crate) use non_browser_app::NonBrowserAppImpl;
pub(crate) use render_handler::RenderHandlerImpl;
pub(crate) use offscreen_app::OffscreenApp;
pub(crate) use render_handler::OffscreenRenderHandler;
@@ -1,19 +1,25 @@
use cef::rc::{Rc, RcImpl};
use cef::sys::{_cef_app_t, cef_base_ref_counted_t};
use cef::{App, ImplApp, SchemeRegistrar, WrapApp};
use cef::{App, BrowserProcessHandler, ImplApp, SchemeRegistrar, WrapApp};
use crate::cef::internal::browser_process_handler::OffscreenBrowserProcessHandler;
use crate::cef::scheme_handler::GraphiteSchemeHandlerFactory;
use crate::render::FrameBufferHandle;
pub(crate) struct NonBrowserAppImpl {
pub(crate) struct OffscreenApp {
object: *mut RcImpl<_cef_app_t, Self>,
}
impl NonBrowserAppImpl {
impl OffscreenApp {
pub(crate) fn new() -> App {
App::new(Self { object: std::ptr::null_mut() })
}
}
impl ImplApp for NonBrowserAppImpl {
impl ImplApp for OffscreenApp {
fn browser_process_handler(&self) -> Option<BrowserProcessHandler> {
Some(OffscreenBrowserProcessHandler::new())
}
fn on_register_custom_schemes(&self, registrar: Option<&mut SchemeRegistrar>) {
GraphiteSchemeHandlerFactory::register_schemes(registrar);
}
@@ -23,7 +29,7 @@ impl ImplApp for NonBrowserAppImpl {
}
}
impl Clone for NonBrowserAppImpl {
impl Clone for OffscreenApp {
fn clone(&self) -> Self {
unsafe {
let rc_impl = &mut *self.object;
@@ -32,7 +38,7 @@ impl Clone for NonBrowserAppImpl {
Self { object: self.object }
}
}
impl Rc for NonBrowserAppImpl {
impl Rc for OffscreenApp {
fn as_base(&self) -> &cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
@@ -40,7 +46,7 @@ impl Rc for NonBrowserAppImpl {
}
}
}
impl WrapApp for NonBrowserAppImpl {
impl WrapApp for OffscreenApp {
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_app_t, Self>) {
self.object = object;
}
+25 -29
View File
@@ -1,31 +1,30 @@
use cef::rc::{Rc, RcImpl};
use cef::sys::{_cef_render_handler_t, cef_base_ref_counted_t};
use cef::{Browser, ImplBrowser, ImplBrowserHost, ImplRenderHandler, PaintElementType, Rect, RenderHandler, WrapRenderHandler};
use cef::{Browser, ImplRenderHandler, PaintElementType, Rect, RenderHandler, WrapRenderHandler};
use crate::cef::EventHandler;
use crate::render::FrameBufferHandle;
pub(crate) struct RenderHandlerImpl<H: EventHandler> {
// CEF render handler for offscreen rendering
pub struct OffscreenRenderHandler {
object: *mut RcImpl<_cef_render_handler_t, Self>,
event_handler: H,
frame_buffer: FrameBufferHandle,
}
impl<H: EventHandler> RenderHandlerImpl<H> {
pub(crate) fn new(event_handler: H) -> RenderHandler {
impl OffscreenRenderHandler {
pub(crate) fn new(frame_buffer: FrameBufferHandle) -> RenderHandler {
RenderHandler::new(Self {
object: std::ptr::null_mut(),
event_handler,
frame_buffer,
})
}
}
impl<H: EventHandler> ImplRenderHandler for RenderHandlerImpl<H> {
impl ImplRenderHandler for OffscreenRenderHandler {
fn view_rect(&self, _browser: Option<&mut Browser>, rect: Option<&mut Rect>) {
let frame_buffer = self.frame_buffer.inner.lock().unwrap();
let width = frame_buffer.width() as i32;
let height = frame_buffer.height() as i32;
if let Some(rect) = rect {
let view = self.event_handler.view();
*rect = Rect {
x: 0,
y: 0,
width: view.width as i32,
height: view.height as i32,
};
*rect = Rect { x: 0, y: 0, width, height };
}
}
@@ -41,12 +40,7 @@ impl<H: EventHandler> ImplRenderHandler for RenderHandlerImpl<H> {
) {
let buffer_size = (width * height * 4) as usize;
let buffer_slice = unsafe { std::slice::from_raw_parts(buffer, buffer_size) };
let draw_successful = self.event_handler.draw(buffer_slice.to_vec(), width as usize, height as usize);
if !draw_successful {
if let Some(browser) = browser {
browser.host().unwrap().was_resized();
}
}
self.frame_buffer.inner.lock().unwrap().add_buffer(buffer_slice, width, height);
}
fn get_raw(&self) -> *mut _cef_render_handler_t {
@@ -54,7 +48,13 @@ impl<H: EventHandler> ImplRenderHandler for RenderHandlerImpl<H> {
}
}
impl<H: EventHandler> Clone for RenderHandlerImpl<H> {
impl WrapRenderHandler for OffscreenRenderHandler {
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_render_handler_t, Self>) {
self.object = object;
}
}
impl Clone for OffscreenRenderHandler {
fn clone(&self) -> Self {
unsafe {
let rc_impl = &mut *self.object;
@@ -62,11 +62,12 @@ impl<H: EventHandler> Clone for RenderHandlerImpl<H> {
}
Self {
object: self.object,
event_handler: self.event_handler.clone(),
frame_buffer: self.frame_buffer.clone(),
}
}
}
impl<H: EventHandler> Rc for RenderHandlerImpl<H> {
impl Rc for OffscreenRenderHandler {
fn as_base(&self) -> &cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
@@ -74,8 +75,3 @@ impl<H: EventHandler> Rc for RenderHandlerImpl<H> {
}
}
}
impl<H: EventHandler> WrapRenderHandler for RenderHandlerImpl<H> {
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_render_handler_t, Self>) {
self.object = object;
}
}
-17
View File
@@ -6,21 +6,4 @@ mod context;
mod internal;
pub(crate) trait EventHandler: Clone {
fn view(&self) -> View;
fn draw(&self, buffer: Vec<u8>, width: usize, height: usize) -> bool;
}
#[derive(Clone)]
pub(crate) struct View {
pub(crate) width: usize,
pub(crate) height: usize,
}
impl View {
pub(crate) fn new(width: usize, height: usize) -> Self {
Self { width, height }
}
}
pub(crate) use context::{Context, InitError, Initialized, Setup, SetupError};
+2 -2
View File
@@ -61,7 +61,7 @@ impl ImplSchemeHandlerFactory for GraphiteSchemeHandlerFactory {
}
}
static FRONTEND: Dir = include_dir!("$CARGO_MANIFEST_DIR/../frontend/dist");
static FRONTEND: Dir = include_dir!("$CARGO_MANIFEST_DIR/../frontend-native/dist");
struct GraphiteFrontendResourceHandler<'a> {
object: *mut RcImpl<_cef_resource_handler_t, Self>,
@@ -74,7 +74,7 @@ impl<'a> GraphiteFrontendResourceHandler<'a> {
let data = if let Some(file) = file {
Some(RefCell::new(file.contents().iter()))
} else {
println!("Failed to find asset at path: {}", path);
println!("Failed to find asset at path: {}/{}", FRONTEND.path().to_str().unwrap(), path);
None
};
let mimetype = if let Some(file) = file {
+40 -253
View File
@@ -1,203 +1,42 @@
use std::fmt::Debug;
use std::process::exit;
use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
use std::thread;
use std::time::Duration;
use winit::application::ApplicationHandler;
use winit::event::*;
use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop, EventLoopProxy};
use winit::window::{Window, WindowId};
use winit::window::WindowId;
mod cef;
use cef::Setup;
mod winit_app;
mod render;
use render::{FrameBuffer, GraphicsState};
pub(crate) enum CustomEvent {
UiUpdate,
Resized,
DoBrowserWork,
}
use crate::render::FrameBufferHandle;
use crate::winit_app::WinitApp;
pub(crate) struct WindowState {
width: Option<usize>,
height: Option<usize>,
ui_fb: Option<FrameBuffer>,
preview_fb: Option<FrameBuffer>,
graphics_state: Option<GraphicsState>,
event_loop_proxy: Option<EventLoopProxy<CustomEvent>>,
}
impl WindowState {
fn new() -> Self {
Self {
width: None,
height: None,
ui_fb: None,
preview_fb: None,
graphics_state: None,
event_loop_proxy: None,
}
}
fn handle(self) -> WindowStateHandle {
WindowStateHandle { inner: Arc::new(Mutex::new(self)) }
}
}
impl Debug for WindowState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("WindowState")
.field("width", &self.width.is_some())
.field("height", &self.height.is_some())
.field("ui_fb", &self.ui_fb.is_some())
.field("preview_fb", &self.preview_fb.is_some())
.field("graphics_state", &self.graphics_state.is_some())
.finish()
}
}
pub(crate) struct WindowStateHandle {
inner: Arc<Mutex<WindowState>>,
}
impl WindowStateHandle {
fn with<'a, P>(&self, p: P) -> Result<(), PoisonError<MutexGuard<'a, WindowState>>>
where
P: FnOnce(&mut WindowState),
{
match self.inner.lock() {
Ok(mut guard) => Ok(p(&mut guard)),
Err(_) => todo!("not error handling yet"),
}
}
}
impl Clone for WindowStateHandle {
fn clone(&self) -> Self {
Self { inner: self.inner.clone() }
}
}
#[derive(Clone)]
struct CefEventHandler {
window_state: WindowStateHandle,
}
impl CefEventHandler {
fn new(window_state: WindowStateHandle) -> Self {
Self { window_state }
}
}
impl cef::EventHandler for CefEventHandler {
fn view(&self) -> cef::View {
let mut w = 1;
let mut h = 1;
self.window_state
.with(|s| match s {
WindowState {
width: Some(width),
height: Some(height),
..
} => {
w = *width;
h = *height;
}
_ => {}
})
.unwrap();
cef::View::new(w, h)
}
fn draw(&self, buffer: Vec<u8>, width: usize, height: usize) -> bool {
let fb = FrameBuffer::new(buffer, width, height)
.map_err(|e| {
panic!("Failed to create FrameBuffer: {}", e);
})
.unwrap();
let mut correct_size = true;
self.window_state
.with(|s| {
if let Some(event_loop_proxy) = &s.event_loop_proxy {
let _ = event_loop_proxy.send_event(CustomEvent::UiUpdate);
let _ = event_loop_proxy.send_event(CustomEvent::DoBrowserWork);
}
if width != s.width.unwrap_or(1) || height != s.height.unwrap_or(1) {
correct_size = false;
} else {
s.ui_fb = Some(fb);
}
})
.unwrap();
correct_size
}
}
struct WinitApp {
window_state: WindowStateHandle,
cef_context: cef::Context<cef::Initialized>,
window: Option<Arc<Window>>,
}
impl WinitApp {
fn new(window_state: WindowStateHandle, cef_context: cef::Context<cef::Initialized>) -> Self {
Self {
window_state,
cef_context,
window: None,
}
}
}
impl ApplicationHandler<CustomEvent> for WinitApp {
impl ApplicationHandler for WinitApp {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
self.window_state
.with(|s| match s {
WindowState { width: Some(w), height: Some(h), .. } => {
let window = Arc::new(
event_loop
.create_window(
Window::default_attributes()
.with_title("CEF Offscreen Rendering")
.with_inner_size(winit::dpi::LogicalSize::new(*w as u32, *h as u32)),
)
.unwrap(),
);
let graphics_state = pollster::block_on(GraphicsState::new(window.clone()));
println!("resumed");
let graphics_state = futures::executor::block_on(GraphicsState::init(event_loop));
let width = graphics_state.window.inner_size().width;
let height = graphics_state.window.inner_size().height;
self.resize(width, height);
// Initialize with a test pattern so we always have something to render
let initial_data = vec![34u8; (width * height * 4) as usize]; // Gray texture #22222222
self.frame_buffer.inner.lock().unwrap().add_buffer(&initial_data, width, height);
self.window = Some(window.clone());
s.graphics_state = Some(graphics_state);
let _ = thread::spawn(move || loop {
thread::sleep(Duration::from_millis(100));
window.request_redraw();
});
println!("Winit window created and ready");
}
_ => {}
})
.unwrap();
self.graphics_state = Some(graphics_state);
}
fn user_event(&mut self, _: &ActiveEventLoop, event: CustomEvent) {
match event {
CustomEvent::DoBrowserWork => {
self.cef_context.work();
}
CustomEvent::UiUpdate | CustomEvent::Resized => {
if let Some(window) = &self.window {
window.request_redraw();
}
}
}
fn new_events(&mut self, event_loop: &ActiveEventLoop, cause: StartCause) {
// Try load the frame buffer into the ui texture if it changes
self.try_load_frame_buffer();
// Update the viewport texture if the canvas element changes
self.try_load_viewport();
}
fn window_event(&mut self, event_loop: &ActiveEventLoop, _window_id: WindowId, event: WindowEvent) {
@@ -209,94 +48,39 @@ impl ApplicationHandler<CustomEvent> for WinitApp {
event_loop.exit();
}
WindowEvent::Resized(physical_size) => {
self.window_state
.with(|s| {
s.width = Some(physical_size.width as usize);
s.height = Some(physical_size.height as usize);
if let Some(elp) = &s.event_loop_proxy {
let _ = elp.send_event(CustomEvent::Resized);
}
if let Some(event_loop_proxy) = &s.event_loop_proxy {
let _ = event_loop_proxy.send_event(CustomEvent::DoBrowserWork);
}
})
.unwrap();
self.resize(physical_size.width, physical_size.height);
}
WindowEvent::RedrawRequested => {
self.cef_context.work();
self.window_state
.with(|s| match s {
WindowState {
width: Some(width),
height: Some(height),
graphics_state: Some(graphics_state),
ui_fb,
..
} => {
if let Some(fb) = &*ui_fb {
graphics_state.update_texture(fb);
if fb.width() != *width && fb.height() != *height {
graphics_state.resize(*width, *height);
}
} else {
graphics_state.resize(*width, *height);
}
match graphics_state.render() {
Ok(_) => {}
Err(wgpu::SurfaceError::Lost) => {
graphics_state.resize(*width, *height);
}
Err(wgpu::SurfaceError::OutOfMemory) => {
event_loop.exit();
}
Err(e) => eprintln!("{:?}", e),
}
}
_ => {}
})
.unwrap();
match self.render() {
Ok(_) => {}
Err(wgpu::SurfaceError::OutOfMemory) => {
event_loop.exit();
}
Err(e) => eprintln!("{:?}", e),
}
}
_ => {}
}
self.window_state
.with(|s| {
if let Some(event_loop_proxy) = &s.event_loop_proxy {
let _ = event_loop_proxy.send_event(CustomEvent::DoBrowserWork);
}
})
.unwrap();
}
}
fn main() {
let args: Vec<String> = std::env::args().collect();
let cef_context = match cef::Context::<Setup>::new() {
Ok(c) => c,
Err(cef::SetupError::Subprocess) => exit(0),
Err(cef::SetupError::SubprocessFailed(t)) => {
println!("Subprocess of type {t} failed");
println!("Subprocess of type {t} failed. args: {:?}", args);
exit(1);
}
};
let window_state = WindowState::new().handle();
let frame_buffer = FrameBufferHandle::new();
window_state
.with(|s| {
s.width = Some(1200);
s.height = Some(800);
})
.unwrap();
let event_loop = EventLoop::<CustomEvent>::with_user_event().build().unwrap();
event_loop.set_control_flow(ControlFlow::Wait);
window_state.with(|s| s.event_loop_proxy = Some(event_loop.create_proxy())).unwrap();
let cef_context = match cef_context.init(CefEventHandler::new(window_state.clone())) {
let cef_context = match cef_context.init(frame_buffer.clone()) {
Ok(c) => c,
Err(cef::InitError::InitializationFailed) => {
println!("Cef initialization failed");
@@ -306,8 +90,11 @@ fn main() {
println!("Cef initialized successfully");
let mut winit_app = WinitApp::new(window_state, cef_context);
let mut winit_app = WinitApp::new(cef_context, frame_buffer);
// Start winit event loop
let event_loop = EventLoop::new().unwrap();
event_loop.set_control_flow(ControlFlow::Poll);
event_loop.run_app(&mut winit_app).unwrap();
winit_app.cef_context.shutdown();
@@ -1,36 +0,0 @@
struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
@location(0) tex_coords: vec2<f32>,
}
@vertex
fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput {
var out: VertexOutput;
let pos = array(
// 1st triangle
vec2f( -1.0, -1.0), // center
vec2f( 1.0, -1.0), // right, center
vec2f( -1.0, 1.0), // center, top
// 2nd triangle
vec2f( -1.0, 1.0), // center, top
vec2f( 1.0, -1.0), // right, center
vec2f( 1.0, 1.0), // right, top
);
let xy = pos[vertex_index];
out.clip_position = vec4f(xy , 0.0, 1.0);
let coords = (xy / 2. + 0.5);
out.tex_coords = vec2f(coords.x, 1. - coords.y);
return out;
}
@group(0) @binding(0)
var t_diffuse: texture_2d<f32>;
@group(0) @binding(1)
var s_diffuse: sampler;
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
return textureSample(t_diffuse, s_diffuse, in.tex_coords);
}
+199 -177
View File
@@ -1,74 +1,163 @@
use std::sync::Arc;
use std::sync::{Arc, Mutex};
use cef::Frame;
use thiserror::Error;
use winit::window::Window;
use winit::{event_loop::ActiveEventLoop, window::Window};
pub(crate) struct FrameBuffer {
buffer: Vec<u8>,
width: usize,
height: usize,
#[derive(Clone)]
pub struct FrameBufferHandle {
pub inner: Arc<Mutex<FrameBuffer>>,
}
#[derive(Error, Debug)]
pub(crate) enum FrameBufferError {
#[error("Invalid buffer size {buffer_size}, expected {expected_size} for width {width} multiplied with height {height} multiplied by 4 channels")]
InvalidSize { buffer_size: usize, expected_size: usize, width: usize, height: usize },
}
impl FrameBuffer {
pub(crate) fn new(buffer: Vec<u8>, width: usize, height: usize) -> Result<Self, FrameBufferError> {
let fb = Self { buffer, width, height };
fb.validate_size()?;
Ok(fb)
}
pub(crate) fn buffer(&self) -> &[u8] {
&self.buffer
}
pub(crate) fn width(&self) -> usize {
self.width
}
pub(crate) fn height(&self) -> usize {
self.height
}
fn validate_size(&self) -> Result<(), FrameBufferError> {
if self.buffer.len() != self.width * self.height * 4 {
Err(FrameBufferError::InvalidSize {
buffer_size: self.buffer.len(),
expected_size: self.width * self.height * 4,
width: self.width,
height: self.height,
})
} else {
Ok(())
impl FrameBufferHandle {
pub fn new() -> Self {
Self {
inner: Arc::new(Mutex::new(FrameBuffer::new())),
}
}
}
#[derive(Debug)]
pub struct FrameBuffer {
// The buffer is only valid after the CEF on_paint, and before it is loaded into the texture
buffer_has_new_data: bool,
buffer: Vec<u8>,
width: u32,
height: u32,
viewport_resized: bool,
viewport_top_left_x: u32,
viewport_top_left_y: u32,
viewport_width: u32,
viewport_height: u32,
}
#[derive(Error, Debug)]
pub(crate) enum FrameBufferError {
#[error("Invalid buffer size. Expected {expected_width}x{expected_height} recieved {received_width}x{received_height}")]
InvalidSize {
expected_width: usize,
expected_height: usize,
received_width: usize,
received_height: usize,
},
#[error("Buffer dimensions are correct, but the allocated vec length : {vec_length} does not match the buffer length: {buffer_length}")]
InvalidBufferSize { vec_length: usize, buffer_length: usize },
}
impl FrameBuffer {
//Initialize the frame buffer to 4k, but set width and height to 0 as it should be initialized when
pub fn new() -> Self {
Self {
buffer: Vec::with_capacity(3840 * 2160 * 4),
buffer_has_new_data: false,
width: 0,
height: 0,
viewport_resized: false,
viewport_top_left_x: 0,
viewport_top_left_y: 0,
viewport_height: 0,
viewport_width: 0,
}
}
// Always keep the frame buffer in sync with the window size
pub fn resize(&mut self, width: u32, height: u32) -> Result<Self, FrameBufferError> {
let new_size = width * height * 4;
if self.buffer.len() < new_size {
self.buffer.resize(new_size, 0);
} else {
self.buffer.truncate(new_size);
}
}
pub fn add_buffer(&mut self, buffer_slice: &[u8], width: u32, height: u32) -> Result<(), FrameBufferError> {
if width != self.width || height != self.height {
Err(FrameBufferError::InvalidSize {
expected_width: self.width,
expected_height: self.height,
received_width: width,
received_height: height,
})
} else if buffer_slice.len() != self.buffer.len() {
Err(FrameBufferError::InvalidBufferSize {
vec_length: self.buffer.len(),
buffer_length: buffer_slice.len(),
})
} else {
self.buffer.copy_from_slice(buffer_slice);
self.buffer_has_new_data = true;
Ok(())
}
}
pub(crate) fn take_buffer(&mut self) -> Option<(&[u8], u32, u32)> {
if buffer_has_new_data {
Some((&self.buffer, self.width, self.height));
self.buffer_has_new_data = false;
} else {
None
}
}
pub fn add_viewport_size(&mut self, viewport_top_left_x: u32, viewport_top_left_y: u32, viewport_width: u32, viewport_height: u32) {
if self.viewport_top_left_x != viewport_top_left_x || self.viewport_top_left_y != viewport_top_left_y || self.viewport_width != viewport_width || self.viewport_height != viewport_height {
self.viewport_resized = true;
}
self.viewport_top_left_x = viewport_top_left_x;
self.viewport_top_left_y = viewport_top_left_y;
self.viewport_width = viewport_width;
self.viewport_height = viewport_height;
}
pub fn get_viewport_size(&mut self) -> Option<(u32, u32, u32, u32)> {
if self.viewport_resized {
self.viewport_resized = false;
Some((self.viewport_top_left_x, self.viewport_top_left_y, self.viewport_width, self.viewport_height))
} else {
None
}
}
pub(crate) fn width(&self) -> u32 {
self.width
}
pub(crate) fn height(&self) -> u32 {
self.height
}
}
pub(crate) struct GraphicsState {
surface: wgpu::Surface<'static>,
device: wgpu::Device,
queue: wgpu::Queue,
config: wgpu::SurfaceConfiguration,
texture: Option<wgpu::Texture>,
bind_group: Option<wgpu::BindGroup>,
render_pipeline: wgpu::RenderPipeline,
sampler: wgpu::Sampler,
pub window: Arc<Window>,
pub surface: wgpu::Surface<'static>,
pub device: wgpu::Device,
pub queue: wgpu::Queue,
pub config: wgpu::SurfaceConfiguration,
pub render_pipeline: wgpu::RenderPipeline,
pub sampler: wgpu::Sampler,
}
impl GraphicsState {
pub(crate) async fn new(window: Arc<Window>) -> Self {
pub(crate) async fn init(event_loop: &ActiveEventLoop) -> Self {
let window = Arc::new(
event_loop
.create_window(
Window::default_attributes()
.with_title("CEF Offscreen Rendering Test")
.with_inner_size(winit::dpi::LogicalSize::new(800, 600)),
)
.unwrap(),
);
let size = window.inner_size();
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
backends: wgpu::Backends::PRIMARY,
..Default::default()
});
let surface = instance.create_surface(window).unwrap();
let surface = instance.create_surface(window.clone()).unwrap();
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions {
@@ -106,10 +195,61 @@ impl GraphicsState {
desired_maximum_frame_latency: 2,
};
surface.configure(&device, &config);
// Create shader module
let shader = device.create_shader_module(wgpu::include_wgsl!("fullscreen_texture.wgsl"));
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("Shader"),
source: wgpu::ShaderSource::Wgsl(
r#"
struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
@location(0) tex_coords: vec2<f32>,
}
@vertex
fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput {
var out: VertexOutput;
let pos = array(
// 1st triangle
vec2f( -1.0, -1.0), // center
vec2f( 1.0, -1.0), // right, center
vec2f( -1.0, 1.0), // center, top
// 2nd triangle
vec2f( -1.0, 1.0), // center, top
vec2f( 1.0, -1.0), // right, center
vec2f( 1.0, 1.0), // right, top
);
let xy = pos[vertex_index];
out.clip_position = vec4f(xy , 0.0, 1.0);
let coords = (xy/ 2. + 0.5);
out.tex_coords = vec2f(coords.x, 1. - coords.y);
// // Generate a fullscreen triangle
// let x = f32(i32(vertex_index) - 1);
// let y = f32(i32(vertex_index & 1u) * 2 - 1);
// out.clip_position = vec4<f32>(x, y, 0.0, 1.0);
// out.tex_coords = vec2<f32>((x + 1.0) * 0.5, (1.0 - y) * 0.5);
return out;
}
@group(0) @binding(0)
var t_diffuse: texture_2d<f32>;
@group(0) @binding(1)
var s_diffuse: sampler;
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
// Test: use texture coordinates as colors to debug
// return vec4<f32>(in.tex_coords.x, in.tex_coords.y, 0.0, 1.0);
// Uncomment this line to use CEF texture:
return textureSample(t_diffuse, s_diffuse, in.tex_coords);
}
"#
.into(),
),
});
// Create sampler
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
@@ -188,141 +328,23 @@ impl GraphicsState {
cache: None,
});
let mut graphics_state = Self {
Self {
window,
surface,
device,
queue,
config,
texture: None,
bind_group: None,
render_pipeline,
sampler,
};
// Initialize with a test pattern so we always have something to render
let width = 800;
let height = 600;
let initial_data = vec![34u8; width * height * 4]; // Gray texture #222222FF
let fb = FrameBuffer::new(initial_data, width, height)
.map_err(|e| {
panic!("Failed to create initial FrameBuffer: {}", e);
})
.unwrap();
graphics_state.update_texture(&fb);
graphics_state
}
pub(crate) fn resize(&mut self, width: usize, height: usize) {
if width > 0 && height > 0 && (self.config.width != width as u32 || self.config.height != height as u32) {
self.config.width = width as u32;
self.config.height = height as u32;
self.surface.configure(&self.device, &self.config);
}
}
pub(crate) fn update_texture(&mut self, frame_buffer: &FrameBuffer) {
let data = frame_buffer.buffer();
let width = frame_buffer.width() as u32;
let height = frame_buffer.height() as u32;
// Creates the cached ui texture, reconfigures the surface
pub(crate) fn resize_surface(&mut self, width: u32, height: u32) {
if width > 0 && height > 0 && (self.config.width != width || self.config.height != height) {
self.config.width = width;
self.config.height = height;
self.surface.configure(&self.device, &self.config);
}
let texture = self.device.create_texture(&wgpu::TextureDescriptor {
label: Some("CEF Texture"),
size: wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Bgra8UnormSrgb,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
self.queue.write_texture(
wgpu::ImageCopyTexture {
texture: &texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
data,
wgpu::ImageDataLayout {
offset: 0,
bytes_per_row: Some(4 * width),
rows_per_image: Some(height),
},
wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
);
let texture_view = texture.create_view(&wgpu::TextureViewDescriptor::default());
let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
layout: &self.render_pipeline.get_bind_group_layout(0),
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&texture_view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&self.sampler),
},
],
label: Some("texture_bind_group"),
});
self.texture = Some(texture);
self.bind_group = Some(bind_group);
}
pub(crate) fn render(&mut self) -> Result<(), wgpu::SurfaceError> {
let output = self.surface.get_current_texture()?;
let view = output.texture.create_view(&wgpu::TextureViewDescriptor::default());
let mut encoder = self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("Render Encoder") });
{
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("Render Pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color { r: 0.01, g: 0.01, b: 0.01, a: 1.0 }),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
occlusion_query_set: None,
timestamp_writes: None,
});
render_pass.set_pipeline(&self.render_pipeline);
if let Some(bind_group) = &self.bind_group {
render_pass.set_bind_group(0, bind_group, &[]);
render_pass.draw(0..6, 0..1); // Draw 3 vertices for fullscreen triangle
} else {
println!("No bind group available - showing clear color only");
}
}
self.queue.submit(std::iter::once(encoder.finish()));
output.present();
Ok(())
}
}
+218
View File
@@ -0,0 +1,218 @@
use std::process::exit;
use winit::application::ApplicationHandler;
use winit::event::*;
use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop, EventLoopProxy};
use winit::window::WindowId;
use crate::cef::{Context, Initialized};
use crate::render::{FrameBuffer, FrameBufferHandle, GraphicsState};
pub struct WinitApp {
pub cef_context: Context<Initialized>,
// Persistent initalized state when the window is created
pub graphics_state: Option<GraphicsState>,
// Shared between winit and cef, and stores the state for the window size and ui frame buffer.
// Automatically kept in sync with the width/height in graphics state surface config
pub frame_buffer: FrameBufferHandle,
// Cached node graph output texture. And its position relative to the full ui
pub viewport_top_left: u32,
pub viewport_top_right: u32,
pub viewport_texture: Option<wgpu::Texture>,
pub viewport_bind_group: Option<wgpu::BindGroup>,
// Cached UI texture and bindgroup for the CEF overlay
pub ui_texture: Option<wgpu::Texture>,
pub ui_bind_group: Option<wgpu::BindGroup>,
}
impl WinitApp {
pub fn new(cef_context: Context<Initialized>, frame_buffer: FrameBufferHandle) -> Self {
Self {
cef_context,
graphics_state: None,
frame_buffer,
viewport_top_left: 0,
viewport_top_right: 0,
viewport_texture: None,
viewport_bind_group: None,
ui_texture: None,
ui_bind_group: None,
}
}
// The single entrypoint for window resizing. It updates the frame buffer, surface config, cached UI overlay texture, and clears the viewport texture
pub fn resize(&mut self, width: u32, height: u32) {
if let Some(graphics_state) = &mut self.graphics_state {
// Updates the surface config
graphics_state.resize_surface(width, height);
// Creates the cached ui texture, reconfigures the surface, and updates the frame buffer
self.ui_texture = Some(graphics_state.device.create_texture(&wgpu::TextureDescriptor {
label: Some("CEF Texture"),
size: wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Bgra8UnormSrgb,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
}));
self.ui_bind_group = Some(graphics_state.device.create_bind_group(&wgpu::BindGroupDescriptor {
layout: &graphics_state.render_pipeline.get_bind_group_layout(0),
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(self.ui_texture.as_ref().unwrap()),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&graphics_state.sampler),
},
],
label: Some("texture_bind_group"),
}));
// Invalidate the viewport texture, since we need to wait until cef calls on paint so we can get the new viewport size, which always changes when the window changes
self.viewport_bind_group = None;
self.viewport_texture = None;
}
// Keep the frame buffer in sync
self.frame_buffer.lock().unwrap().resize(width, height);
if let Some(browser) = &self.cef_context.browser {
browser.host().unwrap().was_resized();
}
}
// Composites the cached ui overlay texture onto the cached node graph texture. This should be called when the DOM or node graph gets evaluated.
pub fn render(&mut self) -> Result<(), wgpu::SurfaceError> {
let Some(graphics_state) = &mut self.graphics_state else {
println!("Graphics state not initialized in render function");
return Ok(());
};
let output = graphics_state.surface.get_current_texture()?;
let view = output.texture.create_view(&wgpu::TextureViewDescriptor::default());
let mut encoder = graphics_state.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("Render Encoder") });
{
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("Render Pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color { r: 0.1, g: 0.2, b: 0.3, a: 1.0 }),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
occlusion_query_set: None,
timestamp_writes: None,
});
render_pass.set_pipeline(&graphics_state.render_pipeline);
if let Some(node_graph_bind_group) = &self.viewport_bind_group {
render_pass.set_bind_group(0, node_graph_bind_group, &[]);
render_pass.draw(0..6, 0..1);
}
if let Some(ui_bind_group) = &self.ui_bind_group {
render_pass.set_bind_group(0, ui_bind_group, &[]);
render_pass.draw(0..6, 0..1);
} else {
println!("No bind group available for ui overlay");
}
}
graphics_state.queue.submit(std::iter::once(encoder.finish()));
output.present();
Ok(())
}
// Loads the framebuffer data from CEF into the texture if it changed
pub fn try_load_frame_buffer(&mut self) -> Result<(), String> {
// Load the data from the shared frame buffer to the texture
if let Some((new_buffer, width, height)) = self.frame_buffer.inner.lock().unwrap().take_buffer() {
let Some(cached_ui_texture) = &self.ui_texture else {
return Err("UI texture must be initialzed before loading framebuffer data".to_string());
};
let Some(graphics_state) = &mut self.graphics_state else {
return Err("graphics state must exist before loading framebuffer data".to_string());
};
graphics_state.queue.write_texture(
wgpu::ImageCopyTexture {
texture: &cached_ui_texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
new_buffer,
wgpu::ImageDataLayout {
offset: 0,
bytes_per_row: Some(4 * width),
rows_per_image: Some(height),
},
wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
);
}
Ok(())
}
// Load the viewport texture and keep its position in sync with the browser viewport
pub fn try_load_viewport(&mut self) -> Result<(), String> {
let Some(graphics_state) = &mut self.graphics_state else {
println!("Graphics state not initialized in try_load_viewport");
return Ok(());
};
// Only runs if the viewport changes, in which case the cached texture should be recreated
if let Some((top_left, top_right, width, height)) = self.frame_buffer.inner.lock().unwrap().get_viewport_size() {
self.viewport_top_left = top_left;
self.viewport_top_right = top_right;
self.ui_texture = Some(graphics_state.device.create_texture(&wgpu::TextureDescriptor {
label: Some("Vello Texture"),
size: wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
// Based on Vello requirements https://github.com/linebender/vello/blob/daf940230a24cbb123a458b6de95721af47aef98/vello/src/lib.rs#L460C36-L460C46
format: wgpu::TextureFormat::Rgba8Unorm,
usage: wgpu::TextureUsages::STORAGE_BINDING,
view_formats: &[],
}));
self.ui_bind_group = Some(graphics_state.device.create_bind_group(&wgpu::BindGroupDescriptor {
layout: &graphics_state.render_pipeline.get_bind_group_layout(0),
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(self.ui_texture.as_ref().unwrap()),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&graphics_state.sampler),
},
],
label: Some("texture_bind_group"),
}));
}
}
}