From 22391cae137166346f1660732ecc0a2541b48267 Mon Sep 17 00:00:00 2001 From: Adam Date: Wed, 23 Jul 2025 19:35:12 -0700 Subject: [PATCH] Improve event loop --- Cargo.lock | 25 +-- Cargo.toml | 4 + desktop/Cargo.toml | 34 ++-- desktop/src/app.rs | 176 ++++++++---------- desktop/src/cef.rs | 35 ++-- desktop/src/cef/context.rs | 4 +- .../cef/internal/browser_process_handler.rs | 6 - desktop/src/cef/internal/render_handler.rs | 16 +- desktop/src/main.rs | 139 +++----------- desktop/src/render.rs | 89 +++++---- package.json | 1 + 11 files changed, 203 insertions(+), 326 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2d33ed756..b3a656247 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "Inflector" @@ -632,7 +632,7 @@ checksum = "fe6d2e5af09e8c8ad56c969f2157a3d4238cebc7c55f0a517728c38f7b200f81" dependencies = [ "serde", "termcolor", - "unicode-width 0.1.14", + "unicode-width", ] [[package]] @@ -691,7 +691,7 @@ dependencies = [ "encode_unicode", "libc", "once_cell", - "unicode-width 0.2.1", + "unicode-width", "windows-sys 0.60.2", ] @@ -1796,12 +1796,9 @@ dependencies = [ name = "graphite-desktop" version = "0.1.0" dependencies = [ - "base64 0.22.1", - "bytemuck", "cef", - "graphite-editor", + "futures", "include_dir", - "pollster", "thiserror 2.0.12", "tracing", "tracing-subscriber", @@ -2332,7 +2329,7 @@ checksum = "70a646d946d06bedbbc4cac4c218acf4bbf2d87757a784857025f4d447e4e1cd" dependencies = [ "console", "portable-atomic", - "unicode-width 0.2.1", + "unicode-width", "unit-prefix", "web-time", ] @@ -3599,12 +3596,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "pollster" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22686f4785f02a4fcc856d3b3bb19bf6c8160d103f7a99cc258bddd0251dc7f2" - [[package]] name = "portable-atomic" version = "1.11.1" @@ -5289,12 +5280,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" -[[package]] -name = "unicode-width" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" - [[package]] name = "unicode-width" version = "0.2.1" diff --git a/Cargo.toml b/Cargo.toml index cf2a1ec0f..68a179c85 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -154,6 +154,10 @@ tinyvec = { version = "1", features = ["std"] } criterion = { version = "0.5", features = ["html_reports"] } iai-callgrind = { version = "0.12.3" } ndarray = "0.16.1" +cef = "138.5.0" +include_dir = "0.7.4" +tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } +tracing = "0.1.41" [profile.dev] opt-level = 1 diff --git a/desktop/Cargo.toml b/desktop/Cargo.toml index fb4b2899a..95a26926c 100644 --- a/desktop/Cargo.toml +++ b/desktop/Cargo.toml @@ -8,25 +8,23 @@ repository = "" edition = "2024" rust-version = "1.87" -[features] -default = ["gpu"] -gpu = ["graphite-editor/gpu"] +# [features] +# default = ["gpu"] +# gpu = ["graphite-editor/gpu"] [dependencies] -# Local dependencies -graphite-editor = { path = "../editor", features = [ - "gpu", - "ron", - "vello", - "decouple-execution", -] } +# # 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 } +tracing-subscriber = { workspace = true } +tracing = { workspace = true } diff --git a/desktop/src/app.rs b/desktop/src/app.rs index fb447aa92..adf28d928 100644 --- a/desktop/src/app.rs +++ b/desktop/src/app.rs @@ -1,8 +1,8 @@ -use crate::CustomEvent; -use crate::WindowState; -use crate::WindowStateHandle; +use crate::WindowSizeHandle; +use crate::WinitEvent; +use crate::cef::WindowSize; +use crate::render::FrameBuffer; use crate::render::GraphicsState; -use std::sync::Arc; use std::time::Duration; use std::time::Instant; use winit::application::ApplicationHandler; @@ -10,79 +10,90 @@ use winit::event::StartCause; use winit::event::WindowEvent; use winit::event_loop::ActiveEventLoop; use winit::event_loop::ControlFlow; -use winit::window::Window; +use winit::event_loop::EventLoopProxy; use winit::window::WindowId; use crate::cef; pub(crate) struct WinitApp { - pub(crate) window_state: WindowStateHandle, + pub(crate) event_loop_proxy: EventLoopProxy, + // + pub(crate) shared_render_data: WindowSizeHandle, + pub(crate) graphics_state: Option, pub(crate) cef_context: cef::Context, - pub(crate) window: Option>, - cef_schedule: Option, + // Cached frame buffer from CEF, used to check if mouse is on a transparent pixel + pub(crate) frame_buffer: Option, } impl WinitApp { - pub(crate) fn new(window_state: WindowStateHandle, cef_context: cef::Context) -> Self { + pub(crate) fn new(elp: EventLoopProxy, shared_render_data: WindowSizeHandle, cef_context: cef::Context) -> Self { Self { - window_state, + event_loop_proxy: elp, + shared_render_data, cef_context, - window: None, - cef_schedule: Some(Instant::now()), + graphics_state: None, + frame_buffer: None, } } } -impl ApplicationHandler for WinitApp { - fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) { - let timeout = Instant::now() + Duration::from_millis(10); - let wait_until = timeout.min(self.cef_schedule.unwrap_or(timeout)); - event_loop.set_control_flow(ControlFlow::WaitUntil(wait_until)); - } - - fn new_events(&mut self, _event_loop: &ActiveEventLoop, _cause: StartCause) { - if let Some(schedule) = self.cef_schedule - && schedule < Instant::now() - { - self.cef_schedule = None; - self.cef_context.work(); +impl ApplicationHandler for WinitApp { + // Runs on every event, but when resume time is reached (100x per second) it does the CEF work and queues a new timer. + fn new_events(&mut self, event_loop: &ActiveEventLoop, cause: StartCause) { + match cause { + // When the event loop starts running, queue the timer. + StartCause::Init => { + event_loop.set_control_flow(ControlFlow::WaitUntil(Instant::now() + Duration::from_millis(10))); + } + // When the timer expires, run the CEF work event and queue a new timer. + StartCause::ResumeTimeReached { .. } => { + self.cef_context.work(); + event_loop.set_control_flow(ControlFlow::WaitUntil(Instant::now() + Duration::from_millis(10))); + } + _ => {} } } fn resumed(&mut self, event_loop: &ActiveEventLoop) { - self.window_state - .with(|s| { - if let WindowState { width: Some(w), height: Some(h), .. } = s { - 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())); - - self.window = Some(window.clone()); - s.graphics_state = Some(graphics_state); - - tracing::info!("Winit window created and ready"); - } - }) - .unwrap(); + self.graphics_state = Some(futures::executor::block_on(GraphicsState::new(event_loop))); } - fn user_event(&mut self, _: &ActiveEventLoop, event: CustomEvent) { + fn user_event(&mut self, _: &ActiveEventLoop, event: WinitEvent) { match event { - CustomEvent::UiUpdate => { - if let Some(window) = &self.window { - window.request_redraw(); - } - } - CustomEvent::ScheduleBrowserWork(instant) => { - self.cef_schedule = Some(instant); + WinitEvent::TryLoopCefWorkWhenResizing { window_size } => { + let Some(frame_buffer) = &self.frame_buffer else { + return; + }; + if window_size.width != frame_buffer.width() || window_size.height != frame_buffer.height() { + let _ = self.event_loop_proxy.send_event(WinitEvent::TryLoopCefWorkWhenResizing { window_size }); + self.cef_context.work(); + }; } + WinitEvent::UIUpdate { frame_buffer } => { + let Some(graphics_state) = &mut self.graphics_state else { + println!("Graphics state must be initialized in UIUpdate"); + return; + }; + graphics_state.update_ui_texture(&frame_buffer); + graphics_state.window.request_redraw(); + self.frame_buffer = Some(frame_buffer); + } // WinitEvent::ViewportResized { + // top_left + // } => { + // let Some(graphics_state) = &mut self.graphics_state else { + // println!("Graphics state must be initialized in load_frame_buffer"); + // return Err("Graphics state must be initialized".to_string()); + // }; + // graphics_state._viewport_top_left = top_left; + // } + // , + // WinitEvent::ViewportUpdate { texture } => { + // let Some(graphics_state) = &mut self.graphics_state else { + // println!("Graphics state must be initialized in load_frame_buffer"); + // return Err("Graphics state must be initialized".to_string()); + // }; + // graphics_state.viewport_texture = Some(texture.texture); + // } } } @@ -95,55 +106,20 @@ impl ApplicationHandler for WinitApp { event_loop.exit(); } WindowEvent::Resized(physical_size) => { - self.window_state - .with(|s| { - let width = physical_size.width as usize; - let height = physical_size.height as usize; - s.width = Some(width); - s.height = Some(height); - if let Some(graphics_state) = &mut s.graphics_state { - graphics_state.resize(width, height); - } - }) - .unwrap(); + // The WaitUntil control flow for the timed event loop will not run when the window is being resized, so CEF needs to be manually worked + let window_size = WindowSize::new(physical_size.width, physical_size.height); + let _ = self.shared_render_data.with(|shared_render_data| { + *shared_render_data = Some(window_size.clone()); + }); self.cef_context.notify_of_resize(); + let _ = self.event_loop_proxy.send_event(WinitEvent::TryLoopCefWorkWhenResizing { window_size }); } - WindowEvent::RedrawRequested => { - self.cef_context.work(); - - self.window_state - .with(|s| { - if let WindowState { - width: Some(width), - height: Some(height), - graphics_state: Some(graphics_state), - ui_frame_buffer: ui_fb, - .. - } = s - { - if let Some(fb) = &*ui_fb { - graphics_state.update_texture(fb); - if fb.width() != *width && fb.height() != *height { - graphics_state.resize(*width, *height); - } - } else if let Some(window) = &self.window { - window.request_redraw(); - } - - match graphics_state.render() { - Ok(_) => {} - Err(wgpu::SurfaceError::Lost) => { - graphics_state.resize(*width, *height); - } - Err(wgpu::SurfaceError::OutOfMemory) => { - event_loop.exit(); - } - Err(e) => tracing::error!("{:?}", e), - } - } - }) - .unwrap(); + let Some(graphics_state) = &mut self.graphics_state else { + println!("Graphics state must be initialized before RedrawRequested"); + return; + }; + let _ = graphics_state.render(); } _ => {} } diff --git a/desktop/src/cef.rs b/desktop/src/cef.rs index eaa217035..eed63a79b 100644 --- a/desktop/src/cef.rs +++ b/desktop/src/cef.rs @@ -1,29 +1,42 @@ -use crate::FrameBuffer; -use std::time::Instant; - mod context; mod input; mod internal; mod scheme_handler; +use std::sync::{Arc, Mutex, MutexGuard, PoisonError}; + pub(crate) use context::{Context, InitError, Initialized, Setup, SetupError}; pub(crate) trait CefEventHandler: Clone { fn window_size(&self) -> WindowSize; - fn draw(&self, frame_buffer: FrameBuffer) -> bool; - /// Scheudule the main event loop to run the cef event loop after the timeout - /// [`_cef_browser_process_handler_t::on_schedule_message_pump_work`] for more documentation. - fn schedule_cef_message_loop_work(&self, scheduled_time: Instant); + fn on_paint(&self, buffer: *const u8, width: u32, height: u32); } -#[derive(Clone)] +#[derive(Clone, Debug, PartialEq)] pub(crate) struct WindowSize { - pub(crate) width: usize, - pub(crate) height: usize, + pub(crate) width: u32, + pub(crate) height: u32, } impl WindowSize { - pub(crate) fn new(width: usize, height: usize) -> Self { + pub(crate) fn new(width: u32, height: u32) -> Self { Self { width, height } } } + +// Shared between the CEF render handler and the Winit app +#[derive(Clone, Default)] +pub(crate) struct WindowSizeHandle { + inner: Arc>>, +} + +impl WindowSizeHandle { + pub fn with

(&self, p: P) -> Result<(), PoisonError>>> + where + P: FnOnce(&mut Option), + { + let mut guard = self.inner.lock()?; + p(&mut guard); + Ok(()) + } +} diff --git a/desktop/src/cef/context.rs b/desktop/src/cef/context.rs index a19172e18..2b39fca15 100644 --- a/desktop/src/cef/context.rs +++ b/desktop/src/cef/context.rs @@ -117,9 +117,7 @@ impl Context { } pub(crate) fn notify_of_resize(&self) { - if let Some(browser) = &self.browser { - browser.host().unwrap().was_resized(); - } + self.browser.as_ref().expect("browser must be initialized").host().unwrap().was_resized(); } } diff --git a/desktop/src/cef/internal/browser_process_handler.rs b/desktop/src/cef/internal/browser_process_handler.rs index 553540ba3..b82a44a4b 100644 --- a/desktop/src/cef/internal/browser_process_handler.rs +++ b/desktop/src/cef/internal/browser_process_handler.rs @@ -1,5 +1,3 @@ -use std::time::{Duration, Instant}; - use cef::rc::{Rc, RcImpl}; use cef::sys::{_cef_browser_process_handler_t, cef_base_ref_counted_t, cef_browser_process_handler_t}; use cef::{CefString, ImplBrowserProcessHandler, SchemeHandlerFactory, WrapBrowserProcessHandler}; @@ -28,10 +26,6 @@ impl ImplBrowserProcessHandler for BrowserProcessHandlerImpl fn get_raw(&self) -> *mut _cef_browser_process_handler_t { self.object.cast() } - - fn on_schedule_message_pump_work(&self, delay_ms: i64) { - self.event_handler.schedule_cef_message_loop_work(Instant::now() + Duration::from_millis(delay_ms as u64)); - } } impl Clone for BrowserProcessHandlerImpl { diff --git a/desktop/src/cef/internal/render_handler.rs b/desktop/src/cef/internal/render_handler.rs index cc4394daf..069622397 100644 --- a/desktop/src/cef/internal/render_handler.rs +++ b/desktop/src/cef/internal/render_handler.rs @@ -1,8 +1,7 @@ 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, WrapRenderHandler}; +use cef::{Browser, ImplRenderHandler, PaintElementType, Rect, WrapRenderHandler}; -use crate::FrameBuffer; use crate::cef::CefEventHandler; pub(crate) struct RenderHandlerImpl { @@ -32,7 +31,7 @@ impl ImplRenderHandler for RenderHandlerImpl { fn on_paint( &self, - browser: Option<&mut Browser>, + _browser: Option<&mut Browser>, _type_: PaintElementType, _dirty_rect_count: usize, _dirty_rects: Option<&Rect>, @@ -40,16 +39,7 @@ impl ImplRenderHandler for RenderHandlerImpl { width: ::std::os::raw::c_int, height: ::std::os::raw::c_int, ) { - let buffer_size = (width * height * 4) as usize; - let buffer_slice = unsafe { std::slice::from_raw_parts(buffer, buffer_size) }; - let frame_buffer = FrameBuffer::new(buffer_slice.to_vec(), width as usize, height as usize).expect("Failed to create frame buffer"); - - let draw_successful = self.event_handler.draw(frame_buffer); - if !draw_successful { - if let Some(browser) = browser { - browser.host().unwrap().was_resized(); - } - } + self.event_handler.on_paint(buffer, width as u32, height as u32); } fn get_raw(&self) -> *mut _cef_render_handler_t { diff --git a/desktop/src/main.rs b/desktop/src/main.rs index 824f469d0..8292f2260 100644 --- a/desktop/src/main.rs +++ b/desktop/src/main.rs @@ -1,7 +1,5 @@ use std::fmt::Debug; use std::process::exit; -use std::sync::{Arc, Mutex, MutexGuard, PoisonError}; -use std::time::Instant; use tracing_subscriber::EnvFilter; use winit::event_loop::{EventLoop, EventLoopProxy}; @@ -10,127 +8,57 @@ mod cef; use cef::Setup; mod render; -use render::{FrameBuffer, GraphicsState}; +use render::FrameBuffer; mod app; use app::WinitApp; -#[derive(Debug)] -pub(crate) enum CustomEvent { - UiUpdate, - ScheduleBrowserWork(Instant), -} +use crate::cef::{WindowSize, WindowSizeHandle}; #[derive(Debug)] -pub(crate) struct WindowState { - width: Option, - height: Option, - ui_frame_buffer: Option, - _viewport_frame_buffer: Option, - graphics_state: Option, - event_loop_proxy: Option>, -} - -impl WindowState { - fn new() -> Self { - Self { - width: None, - height: None, - ui_frame_buffer: None, - _viewport_frame_buffer: None, - graphics_state: None, - event_loop_proxy: None, - } - } - - fn handle(self) -> WindowStateHandle { - WindowStateHandle { inner: Arc::new(Mutex::new(self)) } - } -} - -pub(crate) struct WindowStateHandle { - inner: Arc>, -} - -impl WindowStateHandle { - fn with<'a, P>(&self, p: P) -> Result<(), PoisonError>> - where - P: FnOnce(&mut WindowState), - { - match self.inner.lock() { - Ok(mut guard) => { - p(&mut guard); - Ok(()) - } - Err(_) => todo!("not error handling yet"), - } - } -} - -impl Clone for WindowStateHandle { - fn clone(&self) -> Self { - Self { inner: self.inner.clone() } - } +pub(crate) enum WinitEvent { + // Constantly run CEF when resizing until the cef ui overlay matches the current window size + // This is because the ResumeTimeReached event loop does not run when the window is being resized + TryLoopCefWorkWhenResizing { window_size: WindowSize }, + // Called from the on_paint callback in OffscreenRenderHandler, and if the buffer is different than the previous buffer size + UIUpdate { frame_buffer: FrameBuffer }, + // Called from the javascript binding to onResize for the canvas + // ViewportResized { top_left: (u32, u32) }, + // // Called from the editor if the render node is evaluated and returns an UpdateViewport message + // ViewportUpdate { texture: wgpu::TextureView }, } #[derive(Clone)] struct CefHandler { - window_state: WindowStateHandle, + event_loop_proxy: EventLoopProxy, + window_state: WindowSizeHandle, } impl CefHandler { - fn new(window_state: WindowStateHandle) -> Self { - Self { window_state } + fn new(event_loop_proxy: EventLoopProxy, window_state: WindowSizeHandle) -> Self { + Self { event_loop_proxy, window_state } } } impl cef::CefEventHandler for CefHandler { fn window_size(&self) -> cef::WindowSize { - let mut w = 1; - let mut h = 1; - + let mut window_size = cef::WindowSize::new(1, 1); self.window_state .with(|s| { - if let WindowState { - width: Some(width), - height: Some(height), - .. - } = s - { - w = *width; - h = *height; + if let Some(s) = s { + window_size = cef::WindowSize::new(s.width as u32, s.height as u32); } }) .unwrap(); - - cef::WindowSize::new(w, h) + window_size } - fn draw(&self, frame_buffer: FrameBuffer) -> bool { - 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); - } - if frame_buffer.width() != s.width.unwrap_or(1) || frame_buffer.height() != s.height.unwrap_or(1) { - correct_size = false; - } else { - s.ui_frame_buffer = Some(frame_buffer); - } - }) - .unwrap(); + fn on_paint(&self, buffer: *const u8, width: u32, height: u32) { + let buffer_size = (width * height * 4) as usize; + let buffer_slice = unsafe { std::slice::from_raw_parts(buffer, buffer_size) }; + let frame_buffer = FrameBuffer::new(buffer_slice.to_vec(), width, height).expect("Failed to create frame buffer"); - correct_size - } - - fn schedule_cef_message_loop_work(&self, scheduled_time: std::time::Instant) { - self.window_state - .with(|s| { - let Some(event_loop_proxy) = &mut s.event_loop_proxy else { return }; - let _ = event_loop_proxy.send_event(CustomEvent::ScheduleBrowserWork(scheduled_time)); - }) - .unwrap(); + let _ = self.event_loop_proxy.send_event(WinitEvent::UIUpdate { frame_buffer }); } } @@ -146,20 +74,11 @@ fn main() { } }; - let window_state = WindowState::new().handle(); + let shared_window_data = WindowSizeHandle::default(); - window_state - .with(|s| { - s.width = Some(1200); - s.height = Some(800); - }) - .unwrap(); + let event_loop = EventLoop::::with_user_event().build().unwrap(); - let event_loop = EventLoop::::with_user_event().build().unwrap(); - - window_state.with(|s| s.event_loop_proxy = Some(event_loop.create_proxy())).unwrap(); - - let cef_context = match cef_context.init(CefHandler::new(window_state.clone())) { + let cef_context = match cef_context.init(CefHandler::new(event_loop.create_proxy(), shared_window_data.clone())) { Ok(c) => c, Err(cef::InitError::InitializationFailed) => { tracing::error!("Cef initialization failed"); @@ -169,7 +88,7 @@ fn main() { tracing::info!("Cef initialized successfully"); - let mut winit_app = WinitApp::new(window_state, cef_context); + let mut winit_app = WinitApp::new(event_loop.create_proxy(), shared_window_data, cef_context); event_loop.run_app(&mut winit_app).unwrap(); } diff --git a/desktop/src/render.rs b/desktop/src/render.rs index e6f07b46b..5263930ff 100644 --- a/desktop/src/render.rs +++ b/desktop/src/render.rs @@ -1,16 +1,17 @@ use std::sync::Arc; use thiserror::Error; -use winit::window::Window; +use winit::{event_loop::ActiveEventLoop, window::Window}; pub(crate) struct FrameBuffer { buffer: Vec, - width: usize, - height: usize, + width: u32, + height: u32, } + impl std::fmt::Debug for FrameBuffer { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("WindowState") + f.debug_struct("FrameBuffer") .field("width", &self.width) .field("height", &self.height) .field("len", &self.buffer.len()) @@ -21,11 +22,11 @@ impl std::fmt::Debug for 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 }, + InvalidSize { buffer_size: usize, expected_size: usize, width: u32, height: u32 }, } impl FrameBuffer { - pub(crate) fn new(buffer: Vec, width: usize, height: usize) -> Result { + pub(crate) fn new(buffer: Vec, width: u32, height: u32) -> Result { let fb = Self { buffer, width, height }; fb.validate_size()?; Ok(fb) @@ -35,19 +36,19 @@ impl FrameBuffer { &self.buffer } - pub(crate) fn width(&self) -> usize { + pub(crate) fn width(&self) -> u32 { self.width } - pub(crate) fn height(&self) -> usize { + pub(crate) fn height(&self) -> u32 { self.height } fn validate_size(&self) -> Result<(), FrameBufferError> { - if self.buffer.len() != self.width * self.height * 4 { + if self.buffer.len() != (self.width * self.height * 4) as usize { Err(FrameBufferError::InvalidSize { buffer_size: self.buffer.len(), - expected_size: self.width * self.height * 4, + expected_size: (self.width * self.height * 4) as usize, width: self.width, height: self.height, }) @@ -59,18 +60,35 @@ impl FrameBuffer { #[derive(Debug)] pub(crate) struct GraphicsState { + pub window: Arc, surface: wgpu::Surface<'static>, + config: wgpu::SurfaceConfiguration, device: wgpu::Device, queue: wgpu::Queue, - config: wgpu::SurfaceConfiguration, - texture: Option, - bind_group: Option, render_pipeline: wgpu::RenderPipeline, sampler: wgpu::Sampler, + + // Cached texture for UI rendering + ui_texture: Option, + ui_bind_group: Option, + // Cached texture for node graph output + // viewport_texture: Option, + // // Returned from CEF js event callback + // viewport_top_left: (u32, u32), } impl GraphicsState { - pub(crate) async fn new(window: Arc) -> Self { + pub(crate) async fn new(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 { @@ -78,7 +96,7 @@ impl GraphicsState { ..Default::default() }); - let surface = instance.create_surface(window).unwrap(); + let surface = instance.create_surface(window.clone()).unwrap(); let adapter = instance .request_adapter(&wgpu::RequestAdapterOptions { @@ -196,49 +214,30 @@ impl GraphicsState { cache: None, }); - let mut graphics_state = Self { + let graphics_state = Self { + window, surface, device, queue, config, - texture: None, - bind_group: None, + ui_texture: None, + ui_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) { + pub(crate) fn update_ui_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; - if width > 0 && height > 0 && (self.config.width != width || self.config.height != height) { - self.config.width = width; + // Resize the surface if the dimensions changed + if self.config.width != width || self.config.height != height { self.config.height = height; + self.config.width = width; self.surface.configure(&self.device, &self.config); } @@ -294,8 +293,8 @@ impl GraphicsState { label: Some("texture_bind_group"), }); - self.texture = Some(texture); - self.bind_group = Some(bind_group); + self.ui_texture = Some(texture); + self.ui_bind_group = Some(bind_group); } pub(crate) fn render(&mut self) -> Result<(), wgpu::SurfaceError> { @@ -321,7 +320,7 @@ impl GraphicsState { }); render_pass.set_pipeline(&self.render_pipeline); - if let Some(bind_group) = &self.bind_group { + if let Some(bind_group) = &self.ui_bind_group { render_pass.set_bind_group(0, bind_group, &[]); render_pass.draw(0..6, 0..1); // Draw 3 vertices for fullscreen triangle } else { diff --git a/package.json b/package.json index 552865928..ce7498371 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "scripts": { "---------- DEV SERVER ----------": "", "start": "cd frontend && npm start", + "start-desktop": "cd frontend && npm run build && cargo run -p graphite-desktop", "profiling": "cd frontend && npm run profiling", "production": "cd frontend && npm run production", "---------- BUILDS ----------": "",