From c2e66f3f73651886acda2ab3e63f9e56cdb0fe0f Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Thu, 31 Jul 2025 09:55:25 +0000 Subject: [PATCH 1/9] Desktop: Execute editor and node graph natively --- Cargo.lock | 6 + desktop/Cargo.toml | 23 +-- desktop/src/app.rs | 132 +++++++++++++++++- desktop/src/cef.rs | 12 +- .../cef/internal/render_process_handler.rs | 9 +- desktop/src/cef/ipc.rs | 2 +- desktop/src/main.rs | 6 +- desktop/src/render.rs | 41 +----- .../portfolio/portfolio_message_handler.rs | 1 + frontend/package.json | 2 + frontend/src/editor.ts | 6 +- frontend/wasm/Cargo.toml | 2 + frontend/wasm/src/editor_api.rs | 36 ++--- frontend/wasm/src/lib.rs | 2 + frontend/wasm/src/native_communcation.rs | 37 +++++ package.json | 2 +- 16 files changed, 241 insertions(+), 78 deletions(-) create mode 100644 frontend/wasm/src/native_communcation.rs diff --git a/Cargo.lock b/Cargo.lock index e7e6f13868..961b67c1ab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1838,11 +1838,16 @@ dependencies = [ "cef", "dirs", "futures", + "glam", + "graph-craft", + "graphite-editor", "include_dir", + "ron", "thiserror 2.0.12", "tracing", "tracing-subscriber", "wgpu", + "wgpu-executor", "winit", ] @@ -1904,6 +1909,7 @@ dependencies = [ "js-sys", "log", "math-parser", + "ron", "serde", "serde-wasm-bindgen", "wasm-bindgen", diff --git a/desktop/Cargo.toml b/desktop/Cargo.toml index 30723b7848..3d330aaedf 100644 --- a/desktop/Cargo.toml +++ b/desktop/Cargo.toml @@ -9,17 +9,20 @@ edition = "2024" rust-version = "1.87" [features] -# default = ["gpu"] -# gpu = ["graphite-editor/gpu"] +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", +] } +graph-craft = { workspace = true } +wgpu-executor = { workspace = true } + wgpu = { workspace = true } winit = { workspace = true, features = ["serde"] } thiserror = { workspace = true } @@ -29,4 +32,6 @@ include_dir = { workspace = true } tracing-subscriber = { workspace = true } tracing = { workspace = true } dirs = { workspace = true } +ron = { workspace = true} bytemuck = { workspace = true } +glam = { workspace = true } diff --git a/desktop/src/app.rs b/desktop/src/app.rs index 8a904662db..5115957f74 100644 --- a/desktop/src/app.rs +++ b/desktop/src/app.rs @@ -2,6 +2,9 @@ use crate::CustomEvent; use crate::WindowSize; use crate::render::GraphicsState; use crate::render::WgpuContext; +use graph_craft::wasm_application_io::WasmApplicationIo; +use graphite_editor::application::Editor; +use graphite_editor::messages::prelude::*; use std::sync::Arc; use std::sync::mpsc::Sender; use std::time::Duration; @@ -21,11 +24,13 @@ pub(crate) struct WinitApp { 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 _ui_frame_buffer: Option, window_size_sender: Sender, _viewport_frame_buffer: Option, graphics_state: Option, wgpu_context: WgpuContext, + pub(crate) editor: Editor, } impl WinitApp { @@ -39,8 +44,25 @@ impl WinitApp { graphics_state: None, window_size_sender, wgpu_context, + editor: Editor::new(), } } + + fn dispatch_message(&mut self, message: Message) { + let responses = self.editor.handle_message(message); + self.send_messages_to_editor(responses); + } + + fn send_messages_to_editor(&mut self, responses: Vec) { + if responses.is_empty() { + return; + } + let Ok(message) = ron::to_string(&responses) else { + tracing::error!("Failed to serialize Messages"); + return; + }; + self.cef_context.send_web_message(message.as_bytes()); + } } impl ApplicationHandler for WinitApp { @@ -49,16 +71,41 @@ impl ApplicationHandler for WinitApp { let timeout = Instant::now() + Duration::from_millis(10); let wait_until = timeout.min(self.cef_schedule.unwrap_or(timeout)); self.cef_context.work(); + + let (_has_run, texture) = futures::executor::block_on(graphite_editor::node_graph_executor::run_node_graph()); + if _has_run { + let mut responses = VecDeque::new(); + let err = self.editor.poll_node_graph_evaluation(&mut responses); + if let Err(e) = err { + tracing::error!("Error poling node graph: {}", e); + } + let frontend_messages = responses + .into_iter() + .flat_map(|response| if let Message::Frontend(frontend) = response { Some(frontend) } else { None }) + .collect(); + self.send_messages_to_editor(frontend_messages); + } + if let Some(texture) = texture + && let Some(graphics_state) = &mut self.graphics_state + { + graphics_state.bind_viewport_texture(texture.texture.as_ref()); + } + event_loop.set_control_flow(ControlFlow::WaitUntil(wait_until)); } - fn new_events(&mut self, _event_loop: &ActiveEventLoop, _cause: StartCause) { + 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(); } + if let StartCause::ResumeTimeReached { .. } = cause { + if let Some(window) = &self.window { + window.request_redraw(); + } + } } fn resumed(&mut self, event_loop: &ActiveEventLoop) { @@ -71,12 +118,67 @@ impl ApplicationHandler for WinitApp { ) .unwrap(), ); - let graphics_state = GraphicsState::new(window.clone(), self.wgpu_context.clone()); + let mut graphics_state = GraphicsState::new(window.clone(), self.wgpu_context.clone()); + + let mut test_data = vec![0u8; 800 * 600 * 4]; + + for y in 0..600 { + for x in 0..800 { + let idx = (y * 800 + x) * 4; + test_data[idx + 1] = (x * 255 / 800) as u8; // Blue + test_data[idx + 2] = (y * 255 / 600) as u8; // Green + test_data[idx] = 255; // Red + test_data[idx + 3] = 255; // Alpha + } + } + + let texture = self.wgpu_context.device.create_texture(&wgpu::TextureDescriptor { + label: Some("Viewport Texture"), + size: wgpu::Extent3d { + width: 800, + height: 600, + 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.wgpu_context.queue.write_texture( + wgpu::TexelCopyTextureInfo { + texture: &texture, + mip_level: 0, + origin: wgpu::Origin3d::ZERO, + aspect: wgpu::TextureAspect::All, + }, + test_data.as_slice(), + wgpu::TexelCopyBufferLayout { + offset: 0, + bytes_per_row: Some(4 * 800), + rows_per_image: Some(600), + }, + wgpu::Extent3d { + width: 800, + height: 600, + depth_or_array_layers: 1, + }, + ); + + graphics_state.bind_viewport_texture(&texture); self.window = Some(window); self.graphics_state = Some(graphics_state); tracing::info!("Winit window created and ready"); + + graphite_editor::application::set_uuid_seed(42); + + let application_io = WasmApplicationIo::new_with_context(self.wgpu_context.clone()); + + futures::executor::block_on(graphite_editor::node_graph_executor::replace_application_io(application_io)); } fn user_event(&mut self, _: &ActiveEventLoop, event: CustomEvent) { @@ -97,6 +199,32 @@ impl ApplicationHandler for WinitApp { self.cef_schedule = Some(instant); } } + CustomEvent::MessageReceived { message } => { + if let Message::InputPreprocessor(ipp_message) = &message { + if let Some(window) = &self.window { + window.request_redraw(); + } + if let InputPreprocessorMessage::CurrentTime { .. } | InputPreprocessorMessage::PointerMove { .. } = &ipp_message { + } else { + // println!("got ipp message: {:?}", &ipp_message.to_discriminant()); + } + } + if let Message::InputPreprocessor(InputPreprocessorMessage::BoundsOfViewports { bounds_of_viewports }) = &message { + if let Some(graphic_state) = &mut self.graphics_state { + let window_size = self.window.as_ref().unwrap().inner_size(); + let window_size = glam::Vec2::new(window_size.width as f32, window_size.height as f32); + let top_left = bounds_of_viewports[0].top_left.as_vec2() / window_size; + let bottom_right = bounds_of_viewports[0].bottom_right.as_vec2() / window_size; + let offset = top_left.to_array(); + let scale = (bottom_right - top_left).recip(); + graphic_state.set_viewport_offset(offset); + graphic_state.set_viewport_scale(scale.to_array()); + } else { + panic!("graphics state not intialized, viewport offset might be lost"); + } + } + self.dispatch_message(message); + } } } diff --git a/desktop/src/cef.rs b/desktop/src/cef.rs index 8096ba9e1f..7d3e68e328 100644 --- a/desktop/src/cef.rs +++ b/desktop/src/cef.rs @@ -120,5 +120,15 @@ impl CefEventHandler for CefHandler { let _ = self.event_loop_proxy.send_event(CustomEvent::ScheduleBrowserWork(scheduled_time)); } - fn receive_web_message(&self, message: &[u8]) {} + fn receive_web_message(&self, message: &[u8]) { + let str = std::str::from_utf8(message).unwrap(); + match ron::from_str(str) { + Ok(message) => { + let _ = self.event_loop_proxy.send_event(CustomEvent::MessageReceived { message }); + } + Err(e) => { + tracing::error!("Failed to deserialize message {:?}", e) + } + } + } } diff --git a/desktop/src/cef/internal/render_process_handler.rs b/desktop/src/cef/internal/render_process_handler.rs index 2d125ae897..62917a8b87 100644 --- a/desktop/src/cef/internal/render_process_handler.rs +++ b/desktop/src/cef/internal/render_process_handler.rs @@ -1,9 +1,6 @@ use cef::rc::{ConvertReturnValue, Rc, RcImpl}; use cef::sys::{_cef_render_process_handler_t, cef_base_ref_counted_t, cef_render_process_handler_t, cef_v8_propertyattribute_t, cef_v8_value_create_array_buffer_with_copy}; -use cef::{ - CefString, ImplFrame, ImplRenderProcessHandler, ImplV8Context, ImplV8Value, V8Handler, V8Propertyattribute, V8Value, WrapRenderProcessHandler, v8_context_get_entered_context, - v8_value_create_function, -}; +use cef::{CefString, ImplFrame, ImplRenderProcessHandler, ImplV8Context, ImplV8Value, V8Handler, V8Propertyattribute, V8Value, WrapRenderProcessHandler, v8_value_create_function}; use crate::cef::ipc::{MessageType, UnpackMessage, UnpackedMessage}; @@ -61,7 +58,9 @@ impl ImplRenderProcessHandler for RenderProcessHandlerImpl { cef_v8_propertyattribute_t::V8_PROPERTY_ATTRIBUTE_READONLY.wrap_result(), ); - frame.execute_java_script(Some(&CefString::from(function_call.as_str())), None, 0); + if global.value_bykey(Some(&CefString::from(function_name))).is_some() { + frame.execute_java_script(Some(&CefString::from(function_call.as_str())), None, 0); + } if context.exit() == 0 { tracing::error!("Failed to exit V8 context"); diff --git a/desktop/src/cef/ipc.rs b/desktop/src/cef/ipc.rs index 2c37c2c3af..69b344a464 100644 --- a/desktop/src/cef/ipc.rs +++ b/desktop/src/cef/ipc.rs @@ -1,4 +1,4 @@ -use cef::{CefString, Frame, ImplBinaryValue, ImplBrowser, ImplFrame, ImplListValue, ImplProcessMessage, ImplV8Context, ProcessId, V8Context, rc::ConvertParam, sys::cef_process_id_t}; +use cef::{CefString, Frame, ImplBinaryValue, ImplBrowser, ImplFrame, ImplListValue, ImplProcessMessage, ImplV8Context, ProcessId, V8Context, sys::cef_process_id_t}; use super::{Context, Initialized}; diff --git a/desktop/src/main.rs b/desktop/src/main.rs index edaafcd272..e843c35754 100644 --- a/desktop/src/main.rs +++ b/desktop/src/main.rs @@ -2,6 +2,7 @@ use std::fmt::Debug; use std::process::exit; use std::time::Instant; +use graphite_editor::messages::prelude::Message; use tracing_subscriber::EnvFilter; use winit::event_loop::EventLoop; @@ -20,6 +21,7 @@ mod dirs; pub(crate) enum CustomEvent { UiUpdate(wgpu::Texture), ScheduleBrowserWork(Instant), + MessageReceived { message: Message }, } fn main() { @@ -38,7 +40,7 @@ fn main() { let (window_size_sender, window_size_receiver) = std::sync::mpsc::channel(); - let wgpu_context = futures::executor::block_on(WgpuContext::new()); + let wgpu_context = futures::executor::block_on(WgpuContext::new()).unwrap(); let cef_context = match cef_context.init(cef::CefHandler::new(window_size_receiver, event_loop.create_proxy(), wgpu_context.clone())) { Ok(c) => c, Err(cef::InitError::AlreadyRunning) => { @@ -51,6 +53,8 @@ fn main() { } }; + tracing::info!("Cef initialized successfully"); + let mut winit_app = WinitApp::new(cef_context, window_size_sender, wgpu_context); event_loop.run_app(&mut winit_app).unwrap(); diff --git a/desktop/src/render.rs b/desktop/src/render.rs index 5aaf422ee1..73f54e8d81 100644 --- a/desktop/src/render.rs +++ b/desktop/src/render.rs @@ -56,46 +56,7 @@ pub(crate) enum FrameBufferError { InvalidSize { buffer_size: usize, expected_size: usize, width: usize, height: usize }, } -#[derive(Debug, Clone)] -pub(crate) struct WgpuContext { - pub(crate) device: wgpu::Device, - pub(crate) queue: wgpu::Queue, - adapter: wgpu::Adapter, - instance: wgpu::Instance, -} - -impl WgpuContext { - pub(crate) async fn new() -> Self { - let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor { - backends: wgpu::Backends::PRIMARY, - ..Default::default() - }); - - let adapter = instance - .request_adapter(&wgpu::RequestAdapterOptions { - power_preference: wgpu::PowerPreference::default(), - compatible_surface: None, - force_fallback_adapter: false, - }) - .await - .unwrap(); - - let required_limits = adapter.limits(); - - let (device, queue) = adapter - .request_device(&wgpu::DeviceDescriptor { - label: None, - required_features: wgpu::Features::PUSH_CONSTANTS, - required_limits, - memory_hints: Default::default(), - trace: wgpu::Trace::Off, - }) - .await - .unwrap(); - - Self { device, queue, adapter, instance } - } -} +pub use wgpu_executor::Context as WgpuContext; #[derive(Debug)] pub(crate) struct GraphicsState { diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index 96028179ed..0ac2321f57 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -883,6 +883,7 @@ impl MessageHandler> for Portfolio responses.add(FrontendMessage::UpdateOpenDocumentsList { open_documents }); } PortfolioMessage::UpdateVelloPreference => { + responses.add(FrontendMessage::UpdateViewportHolePunch { active: preferences.use_vello }); responses.add(NodeGraphMessage::RunDocumentGraph); self.persistent_data.use_vello = preferences.use_vello; } diff --git a/frontend/package.json b/frontend/package.json index 07fc0460b0..40d36d4c88 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,6 +12,7 @@ "production": "npm run setup && npm run wasm:build-production && concurrently -k -n \"VITE,RUST\" \"vite\" \"npm run wasm:watch-production\"", "---------- BUILDS ----------": "", "build-dev": "npm run wasm:build-dev && vite build", + "build-native": "npm run native:build-dev && vite build", "build-profiling": "npm run wasm:build-profiling && vite build", "build": "npm run wasm:build-production && vite build", "---------- UTILITIES ----------": "", @@ -19,6 +20,7 @@ "lint-fix": "eslint . --fix && tsc --noEmit", "---------- INTERNAL ----------": "", "setup": "node package-installer.js", + "native:build-dev": "wasm-pack build ./wasm --dev --target=web --features native", "wasm:build-dev": "wasm-pack build ./wasm --dev --target=web", "wasm:build-profiling": "wasm-pack build ./wasm --profiling --target=web", "wasm:build-production": "wasm-pack build ./wasm --release --target=web", diff --git a/frontend/src/editor.ts b/frontend/src/editor.ts index 7dd6a98951..7c5cb359fa 100644 --- a/frontend/src/editor.ts +++ b/frontend/src/editor.ts @@ -1,7 +1,7 @@ // import { panicProxy } from "@graphite/utility-functions/panic-proxy"; import { type JsMessageType } from "@graphite/messages"; import { createSubscriptionRouter, type SubscriptionRouter } from "@graphite/subscription-router"; -import init, { setRandomSeed, wasmMemory, EditorHandle } from "@graphite-frontend/wasm/pkg/graphite_wasm.js"; +import init, { setRandomSeed, wasmMemory, EditorHandle, receiveNativeMessage } from "@graphite-frontend/wasm/pkg/graphite_wasm.js"; export type Editor = { raw: WebAssembly.Memory; @@ -24,6 +24,10 @@ export async function initWasm() { if (name.startsWith("__node_registry")) f(); } + if (receiveNativeMessage) { + (window as any).receiveNativeMessage = receiveNativeMessage; + } + wasmImport = await wasmMemory(); // eslint-disable-next-line @typescript-eslint/no-explicit-any (window as any).imageCanvases = {}; diff --git a/frontend/wasm/Cargo.toml b/frontend/wasm/Cargo.toml index b335c5efbf..90b8ebe6ba 100644 --- a/frontend/wasm/Cargo.toml +++ b/frontend/wasm/Cargo.toml @@ -13,6 +13,7 @@ license = "Apache-2.0" [features] default = ["gpu"] gpu = ["editor/gpu"] +native = [] [lib] crate-type = ["cdylib", "rlib"] @@ -37,6 +38,7 @@ wasm-bindgen-futures = { workspace = true } math-parser = { workspace = true } wgpu = { workspace = true } web-sys = { workspace = true } +ron = { workspace = true } [package.metadata.wasm-pack.profile.dev] wasm-opt = false diff --git a/frontend/wasm/src/editor_api.rs b/frontend/wasm/src/editor_api.rs index a4ded1c024..21dc1176fa 100644 --- a/frontend/wasm/src/editor_api.rs +++ b/frontend/wasm/src/editor_api.rs @@ -154,6 +154,7 @@ impl EditorHandle { } // Sends a message to the dispatcher in the Editor Backend + #[cfg(not(feature = "native"))] fn dispatch>(&self, message: T) { // Process no further messages after a crash to avoid spamming the console if EDITOR_HAS_CRASHED.load(Ordering::SeqCst) { @@ -169,6 +170,16 @@ impl EditorHandle { } } + #[cfg(feature = "native")] + fn dispatch>(&self, message: T) { + let message: Message = message.into(); + let Ok(serialized_message) = ron::to_string(&message) else { + log::error!("Failed to serialize message"); + return; + }; + crate::native_communcation::send_message_to_cef(serialized_message) + } + // Sends a FrontendMessage to JavaScript fn send_frontend_message_to_js(&self, mut message: FrontendMessage) { if let FrontendMessage::UpdateImageData { ref image_data } = message { @@ -228,22 +239,15 @@ impl EditorHandle { wasm_bindgen_futures::spawn_local(poll_node_graph_evaluation()); if !EDITOR_HAS_CRASHED.load(Ordering::SeqCst) { - editor_and_handle(|editor, handle| { - for message in editor.handle_message(InputPreprocessorMessage::CurrentTime { + editor_and_handle(|_, handle| { + handle.dispatch(InputPreprocessorMessage::CurrentTime { timestamp: js_sys::Date::now() as u64, - }) { - handle.send_frontend_message_to_js(message); - } - - for message in editor.handle_message(AnimationMessage::IncrementFrameCounter) { - handle.send_frontend_message_to_js(message); - } + }); + handle.dispatch(AnimationMessage::IncrementFrameCounter); // Used by auto-panning, but this could possibly be refactored in the future, see: // - for message in editor.handle_message(BroadcastMessage::TriggerEvent(BroadcastEvent::AnimationFrame)) { - handle.send_frontend_message_to_js(message); - } + handle.dispatch(BroadcastMessage::TriggerEvent(BroadcastEvent::AnimationFrame)); }); } @@ -910,7 +914,7 @@ fn editor(callback: impl FnOnce(&mut editor::application::Editor) -> } /// Provides access to the `Editor` and its `EditorHandle` by calling the given closure with them as arguments. -pub(crate) fn editor_and_handle(mut callback: impl FnMut(&mut Editor, &mut EditorHandle)) { +pub(crate) fn editor_and_handle(callback: impl FnOnce(&mut Editor, &mut EditorHandle)) { EDITOR_HANDLE.with(|editor_handle| { editor(|editor| { let mut guard = editor_handle.try_lock(); @@ -964,9 +968,7 @@ fn auto_save_all_documents() { return; } - editor_and_handle(|editor, handle| { - for message in editor.handle_message(PortfolioMessage::AutoSaveAllDocuments) { - handle.send_frontend_message_to_js(message); - } + editor_and_handle(|_, handle| { + handle.dispatch(PortfolioMessage::AutoSaveAllDocuments); }); } diff --git a/frontend/wasm/src/lib.rs b/frontend/wasm/src/lib.rs index 730fb4215c..d27a4dcdd0 100644 --- a/frontend/wasm/src/lib.rs +++ b/frontend/wasm/src/lib.rs @@ -6,6 +6,8 @@ extern crate log; pub mod editor_api; pub mod helpers; +#[cfg(feature = "native")] +pub mod native_communcation; use editor::messages::prelude::*; use std::panic; diff --git a/frontend/wasm/src/native_communcation.rs b/frontend/wasm/src/native_communcation.rs new file mode 100644 index 0000000000..9f3a08bafb --- /dev/null +++ b/frontend/wasm/src/native_communcation.rs @@ -0,0 +1,37 @@ +use editor::{application::Editor, messages::prelude::FrontendMessage}; +use js_sys::{ArrayBuffer, Uint8Array}; +use wasm_bindgen::prelude::*; + +use crate::editor_api::{self, EditorHandle}; + +#[wasm_bindgen(js_name = "receiveNativeMessage")] +pub fn receive_native_message(buffer: ArrayBuffer) { + let buffer = Uint8Array::new(buffer.as_ref()).to_vec(); + match ron::from_str::>(str::from_utf8(buffer.as_slice()).unwrap()) { + Ok(messages) => { + // log::debug!("Received messages: {:?}", messages); + + let callback = move |_: &mut Editor, handle: &mut EditorHandle| { + for message in messages { + handle.send_frontend_message_to_js_rust_proxy(message); + } + }; + editor_api::editor_and_handle(callback); + } + Err(e) => log::error!("Failed to deserialize frontend messages: {e:?}"), + } +} + +pub fn send_message_to_cef(message: String) { + let global = js_sys::global(); + + // Get the function by name + let func = js_sys::Reflect::get(&global, &JsValue::from_str("sendNativeMessage")).expect("Function not found"); + + let func = func.dyn_into::().expect("Not a function"); + let array = Uint8Array::from(message.as_bytes()); + let buffer = array.buffer(); + + // Call it with argument + func.call1(&JsValue::NULL, &JsValue::from(buffer)).expect("Function call failed"); +} diff --git a/package.json b/package.json index 7197418fb0..9bdfdae2e2 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "scripts": { "---------- DEV SERVER ----------": "", "start": "cd frontend && npm start", - "start-desktop": "cd frontend && npm run build-dev && cargo run -p graphite-desktop", + "start-desktop": "cd frontend && npm run build-native && cargo run -p graphite-desktop", "profiling": "cd frontend && npm run profiling", "production": "cd frontend && npm run production", "---------- BUILDS ----------": "", From c59b5be91cdfe4479546e8adb9b18d5353685341 Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Thu, 31 Jul 2025 09:55:25 +0000 Subject: [PATCH 2/9] Remove decouple execution feature --- desktop/Cargo.toml | 1 - editor/Cargo.toml | 1 - editor/src/node_graph_executor.rs | 2 -- editor/src/node_graph_executor/runtime.rs | 10 ---------- 4 files changed, 14 deletions(-) diff --git a/desktop/Cargo.toml b/desktop/Cargo.toml index 3d330aaedf..97f6bca65a 100644 --- a/desktop/Cargo.toml +++ b/desktop/Cargo.toml @@ -18,7 +18,6 @@ graphite-editor = { path = "../editor", features = [ "gpu", "ron", "vello", - "decouple-execution", ] } graph-craft = { workspace = true } wgpu-executor = { workspace = true } diff --git a/editor/Cargo.toml b/editor/Cargo.toml index 2d4a12d7cc..6871b98312 100644 --- a/editor/Cargo.toml +++ b/editor/Cargo.toml @@ -14,7 +14,6 @@ license = "Apache-2.0" default = ["wasm"] wasm = ["wasm-bindgen", "graphene-std/wasm", "wasm-bindgen-futures"] gpu = ["interpreted-executor/gpu", "wgpu-executor"] -decouple-execution = [] resvg = ["graphene-std/resvg"] vello = ["graphene-std/vello", "resvg"] ron = ["dep:ron"] diff --git a/editor/src/node_graph_executor.rs b/editor/src/node_graph_executor.rs index 0b4ceb38b7..aa439960dd 100644 --- a/editor/src/node_graph_executor.rs +++ b/editor/src/node_graph_executor.rs @@ -29,7 +29,6 @@ pub struct ExecutionRequest { render_config: RenderConfig, } -#[cfg_attr(feature = "decouple-execution", derive(serde::Serialize, serde::Deserialize))] pub struct ExecutionResponse { execution_id: u64, result: Result, @@ -46,7 +45,6 @@ pub struct CompilationResponse { node_graph_errors: GraphErrors, } -#[cfg_attr(feature = "decouple-execution", derive(serde::Serialize, serde::Deserialize))] pub enum NodeGraphUpdate { ExecutionResponse(ExecutionResponse), CompilationResponse(CompilationResponse), diff --git a/editor/src/node_graph_executor/runtime.rs b/editor/src/node_graph_executor/runtime.rs index b4362ce833..efc9bffe38 100644 --- a/editor/src/node_graph_executor/runtime.rs +++ b/editor/src/node_graph_executor/runtime.rs @@ -427,22 +427,14 @@ struct InspectState { } /// The resulting value from the temporary inspected during execution #[derive(Clone, Debug, Default)] -#[cfg_attr(feature = "decouple-execution", derive(serde::Serialize, serde::Deserialize))] pub struct InspectResult { - #[cfg(not(feature = "decouple-execution"))] introspected_data: Option>, - #[cfg(feature = "decouple-execution")] - introspected_data: Option, pub inspect_node: NodeId, } impl InspectResult { pub fn take_data(&mut self) -> Option> { - #[cfg(not(feature = "decouple-execution"))] return self.introspected_data.clone(); - - #[cfg(feature = "decouple-execution")] - return self.introspected_data.take().map(|value| value.to_any()); } } @@ -487,8 +479,6 @@ impl InspectState { fn access(&self, executor: &DynamicExecutor) -> Option { let introspected_data = executor.introspect(&[self.monitor_node]).inspect_err(|e| warn!("Failed to introspect monitor node {e}")).ok(); // TODO: Consider displaying the error instead of ignoring it - #[cfg(feature = "decouple-execution")] - let introspected_data = introspected_data.as_ref().and_then(|data| TaggedValue::try_from_std_any_ref(data).ok()); Some(InspectResult { inspect_node: self.inspect_node, From a73adfc9177b0cbf7165dc261f6cf81fdd4c98bb Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Thu, 31 Jul 2025 09:55:25 +0000 Subject: [PATCH 3/9] Disable feature gate for native communication functions --- frontend/src/editor.ts | 5 +---- frontend/wasm/src/lib.rs | 1 - 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/frontend/src/editor.ts b/frontend/src/editor.ts index 7c5cb359fa..ab9fd7505c 100644 --- a/frontend/src/editor.ts +++ b/frontend/src/editor.ts @@ -24,13 +24,10 @@ export async function initWasm() { if (name.startsWith("__node_registry")) f(); } - if (receiveNativeMessage) { - (window as any).receiveNativeMessage = receiveNativeMessage; - } - wasmImport = await wasmMemory(); // eslint-disable-next-line @typescript-eslint/no-explicit-any (window as any).imageCanvases = {}; + (window as any).receiveNativeMessage = receiveNativeMessage; // Provide a random starter seed which must occur after initializing the WASM module, since WASM can't generate its own random numbers const randomSeedFloat = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER); diff --git a/frontend/wasm/src/lib.rs b/frontend/wasm/src/lib.rs index d27a4dcdd0..01415fc29f 100644 --- a/frontend/wasm/src/lib.rs +++ b/frontend/wasm/src/lib.rs @@ -6,7 +6,6 @@ extern crate log; pub mod editor_api; pub mod helpers; -#[cfg(feature = "native")] pub mod native_communcation; use editor::messages::prelude::*; From c1723783cd6cb2e2048ea05db47318ff169b1470 Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Thu, 31 Jul 2025 09:55:25 +0000 Subject: [PATCH 4/9] Avoid ininite message loop on an infinite canvas --- .../new_document_dialog_message_handler.rs | 7 +++---- editor/src/messages/portfolio/portfolio_message_handler.rs | 6 +++--- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/editor/src/messages/dialog/new_document_dialog/new_document_dialog_message_handler.rs b/editor/src/messages/dialog/new_document_dialog/new_document_dialog_message_handler.rs index 23fb98dea6..3b29475cc9 100644 --- a/editor/src/messages/dialog/new_document_dialog/new_document_dialog_message_handler.rs +++ b/editor/src/messages/dialog/new_document_dialog/new_document_dialog_message_handler.rs @@ -34,11 +34,10 @@ impl MessageHandler for NewDocumentDialogMessageHa .into(), ], }); + responses.add(DeferMessage::AfterNavigationReady { + messages: vec![DocumentMessage::ZoomCanvasToFitAll.into(), DocumentMessage::DeselectAllLayers.into()], + }); } - - responses.add(DeferMessage::AfterNavigationReady { - messages: vec![DocumentMessage::ZoomCanvasToFitAll.into(), DocumentMessage::DeselectAllLayers.into()], - }); } } diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index 0ac2321f57..37eabd96de 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -702,12 +702,12 @@ impl MessageHandler> for Portfolio if create_document { // Wait for the document to be rendered so the click targets can be calculated in order to determine the artboard size that will encompass the pasted image - responses.add(DeferMessage::AfterNavigationReady { - messages: vec![DocumentMessage::ZoomCanvasToFitAll.into()], - }); responses.add(DeferMessage::AfterGraphRun { messages: vec![DocumentMessage::WrapContentInArtboard { place_artboard_at_origin: true }.into()], }); + responses.add(DeferMessage::AfterNavigationReady { + messages: vec![DocumentMessage::ZoomCanvasToFitAll.into()], + }); } } PortfolioMessage::PasteSvg { From ec5ce863022b1228bbca690540276ebbbad48f55 Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Thu, 31 Jul 2025 09:55:25 +0000 Subject: [PATCH 5/9] Add any lint exception --- frontend/src/editor.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/editor.ts b/frontend/src/editor.ts index ab9fd7505c..0eacb4f76d 100644 --- a/frontend/src/editor.ts +++ b/frontend/src/editor.ts @@ -27,6 +27,7 @@ export async function initWasm() { wasmImport = await wasmMemory(); // eslint-disable-next-line @typescript-eslint/no-explicit-any (window as any).imageCanvases = {}; + // eslint-disable-next-line @typescript-eslint/no-explicit-any (window as any).receiveNativeMessage = receiveNativeMessage; // Provide a random starter seed which must occur after initializing the WASM module, since WASM can't generate its own random numbers From 55ea797736447ac4d8a283d40b8baca8899221ef Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Thu, 31 Jul 2025 09:55:25 +0000 Subject: [PATCH 6/9] Build evaluation loop --- desktop/src/app.rs | 35 ++++++++++++++++++----------------- desktop/src/main.rs | 18 +++++++++++++++++- 2 files changed, 35 insertions(+), 18 deletions(-) diff --git a/desktop/src/app.rs b/desktop/src/app.rs index 5115957f74..5707252e22 100644 --- a/desktop/src/app.rs +++ b/desktop/src/app.rs @@ -73,23 +73,6 @@ impl ApplicationHandler for WinitApp { self.cef_context.work(); let (_has_run, texture) = futures::executor::block_on(graphite_editor::node_graph_executor::run_node_graph()); - if _has_run { - let mut responses = VecDeque::new(); - let err = self.editor.poll_node_graph_evaluation(&mut responses); - if let Err(e) = err { - tracing::error!("Error poling node graph: {}", e); - } - let frontend_messages = responses - .into_iter() - .flat_map(|response| if let Message::Frontend(frontend) = response { Some(frontend) } else { None }) - .collect(); - self.send_messages_to_editor(frontend_messages); - } - if let Some(texture) = texture - && let Some(graphics_state) = &mut self.graphics_state - { - graphics_state.bind_viewport_texture(texture.texture.as_ref()); - } event_loop.set_control_flow(ControlFlow::WaitUntil(wait_until)); } @@ -225,6 +208,24 @@ impl ApplicationHandler for WinitApp { } self.dispatch_message(message); } + CustomEvent::NodeGraphRan { texture } => { + if let Some(texture) = texture + && let Some(graphics_state) = &mut self.graphics_state + { + graphics_state.bind_viewport_texture(texture.texture.as_ref()); + } + let mut responses = VecDeque::new(); + let err = self.editor.poll_node_graph_evaluation(&mut responses); + if let Err(e) = err { + if e != "No active document" { + tracing::error!("Error poling node graph: {}", e); + } + } + + for message in responses { + self.dispatch_message(message); + } + } } } diff --git a/desktop/src/main.rs b/desktop/src/main.rs index e843c35754..e1c4fa7d5f 100644 --- a/desktop/src/main.rs +++ b/desktop/src/main.rs @@ -1,6 +1,6 @@ -use std::fmt::Debug; use std::process::exit; use std::time::Instant; +use std::{fmt::Debug, time::Duration}; use graphite_editor::messages::prelude::Message; use tracing_subscriber::EnvFilter; @@ -22,6 +22,7 @@ pub(crate) enum CustomEvent { UiUpdate(wgpu::Texture), ScheduleBrowserWork(Instant), MessageReceived { message: Message }, + NodeGraphRan { texture: Option }, } fn main() { @@ -55,6 +56,21 @@ fn main() { tracing::info!("Cef initialized successfully"); + let rendering_loop_proxy = event_loop.create_proxy(); + let target_fps = 60; + std::thread::spawn(move || { + loop { + let last_render = Instant::now(); + let (has_run, texture) = futures::executor::block_on(graphite_editor::node_graph_executor::run_node_graph()); + if has_run { + rendering_loop_proxy.send_event(CustomEvent::NodeGraphRan { texture }); + } + let frame_time = Duration::from_secs_f32((target_fps as f32).recip()); + let sleep = last_render + frame_time - Instant::now(); + std::thread::sleep(sleep); + } + }); + let mut winit_app = WinitApp::new(cef_context, window_size_sender, wgpu_context); event_loop.run_app(&mut winit_app).unwrap(); From f2b79ee0243725cfae2b9eeb8bcbb27b47c8faf1 Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Thu, 31 Jul 2025 09:55:25 +0000 Subject: [PATCH 7/9] Fix texture passing message --- desktop/src/app.rs | 4 +--- desktop/src/main.rs | 6 ++++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/desktop/src/app.rs b/desktop/src/app.rs index 5707252e22..bd5c878d2c 100644 --- a/desktop/src/app.rs +++ b/desktop/src/app.rs @@ -72,8 +72,6 @@ impl ApplicationHandler for WinitApp { let wait_until = timeout.min(self.cef_schedule.unwrap_or(timeout)); self.cef_context.work(); - let (_has_run, texture) = futures::executor::block_on(graphite_editor::node_graph_executor::run_node_graph()); - event_loop.set_control_flow(ControlFlow::WaitUntil(wait_until)); } @@ -212,7 +210,7 @@ impl ApplicationHandler for WinitApp { if let Some(texture) = texture && let Some(graphics_state) = &mut self.graphics_state { - graphics_state.bind_viewport_texture(texture.texture.as_ref()); + graphics_state.bind_viewport_texture(&texture); } let mut responses = VecDeque::new(); let err = self.editor.poll_node_graph_evaluation(&mut responses); diff --git a/desktop/src/main.rs b/desktop/src/main.rs index e1c4fa7d5f..fcaab73d9c 100644 --- a/desktop/src/main.rs +++ b/desktop/src/main.rs @@ -22,7 +22,7 @@ pub(crate) enum CustomEvent { UiUpdate(wgpu::Texture), ScheduleBrowserWork(Instant), MessageReceived { message: Message }, - NodeGraphRan { texture: Option }, + NodeGraphRan { texture: Option }, } fn main() { @@ -63,7 +63,9 @@ fn main() { let last_render = Instant::now(); let (has_run, texture) = futures::executor::block_on(graphite_editor::node_graph_executor::run_node_graph()); if has_run { - rendering_loop_proxy.send_event(CustomEvent::NodeGraphRan { texture }); + let _ = rendering_loop_proxy.send_event(CustomEvent::NodeGraphRan { + texture: texture.map(|t| (*t.texture).clone()), + }); } let frame_time = Duration::from_secs_f32((target_fps as f32).recip()); let sleep = last_render + frame_time - Instant::now(); From 607f46d527fda7195f31e8950999640b55c1829a Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Thu, 31 Jul 2025 09:55:25 +0000 Subject: [PATCH 8/9] Cleanup --- desktop/src/app.rs | 58 +--------------------------------------- editor/src/dispatcher.rs | 7 +++-- 2 files changed, 6 insertions(+), 59 deletions(-) diff --git a/desktop/src/app.rs b/desktop/src/app.rs index bd5c878d2c..e34436be49 100644 --- a/desktop/src/app.rs +++ b/desktop/src/app.rs @@ -24,10 +24,7 @@ pub(crate) struct WinitApp { 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 - _ui_frame_buffer: Option, window_size_sender: Sender, - _viewport_frame_buffer: Option, graphics_state: Option, wgpu_context: WgpuContext, pub(crate) editor: Editor, @@ -39,8 +36,6 @@ impl WinitApp { cef_context, window: None, cef_schedule: Some(Instant::now()), - _viewport_frame_buffer: None, - _ui_frame_buffer: None, graphics_state: None, window_size_sender, wgpu_context, @@ -99,64 +94,13 @@ impl ApplicationHandler for WinitApp { ) .unwrap(), ); - let mut graphics_state = GraphicsState::new(window.clone(), self.wgpu_context.clone()); - - let mut test_data = vec![0u8; 800 * 600 * 4]; - - for y in 0..600 { - for x in 0..800 { - let idx = (y * 800 + x) * 4; - test_data[idx + 1] = (x * 255 / 800) as u8; // Blue - test_data[idx + 2] = (y * 255 / 600) as u8; // Green - test_data[idx] = 255; // Red - test_data[idx + 3] = 255; // Alpha - } - } - - let texture = self.wgpu_context.device.create_texture(&wgpu::TextureDescriptor { - label: Some("Viewport Texture"), - size: wgpu::Extent3d { - width: 800, - height: 600, - 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.wgpu_context.queue.write_texture( - wgpu::TexelCopyTextureInfo { - texture: &texture, - mip_level: 0, - origin: wgpu::Origin3d::ZERO, - aspect: wgpu::TextureAspect::All, - }, - test_data.as_slice(), - wgpu::TexelCopyBufferLayout { - offset: 0, - bytes_per_row: Some(4 * 800), - rows_per_image: Some(600), - }, - wgpu::Extent3d { - width: 800, - height: 600, - depth_or_array_layers: 1, - }, - ); - - graphics_state.bind_viewport_texture(&texture); + let graphics_state = GraphicsState::new(window.clone(), self.wgpu_context.clone()); self.window = Some(window); self.graphics_state = Some(graphics_state); tracing::info!("Winit window created and ready"); - graphite_editor::application::set_uuid_seed(42); - let application_io = WasmApplicationIo::new_with_context(self.wgpu_context.clone()); futures::executor::block_on(graphite_editor::node_graph_executor::replace_application_io(application_io)); diff --git a/editor/src/dispatcher.rs b/editor/src/dispatcher.rs index 4be3e53621..e311c05f9e 100644 --- a/editor/src/dispatcher.rs +++ b/editor/src/dispatcher.rs @@ -206,11 +206,14 @@ impl Dispatcher { self.message_handlers.preferences_message_handler.process_message(message, &mut queue, ()); } Message::Tool(message) => { - let document_id = self.message_handlers.portfolio_message_handler.active_document_id().unwrap(); - let Some(document) = self.message_handlers.portfolio_message_handler.documents.get_mut(&document_id) else { + let Some(document_id) = self.message_handlers.portfolio_message_handler.active_document_id() else { warn!("Called ToolMessage without an active document.\nGot {message:?}"); return; }; + let Some(document) = self.message_handlers.portfolio_message_handler.documents.get_mut(&document_id) else { + warn!("Called ToolMessage with an invalid active document.\nGot {message:?}"); + return; + }; let context = ToolMessageContext { document_id, From 44bf0c24f798a964aecca1e656d1d8b5dbaebd22 Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Thu, 31 Jul 2025 10:16:04 +0000 Subject: [PATCH 9/9] More cleanup --- desktop/src/app.rs | 4 ---- frontend/wasm/src/native_communcation.rs | 2 -- 2 files changed, 6 deletions(-) diff --git a/desktop/src/app.rs b/desktop/src/app.rs index e34436be49..ae1fdc2979 100644 --- a/desktop/src/app.rs +++ b/desktop/src/app.rs @@ -129,10 +129,6 @@ impl ApplicationHandler for WinitApp { if let Some(window) = &self.window { window.request_redraw(); } - if let InputPreprocessorMessage::CurrentTime { .. } | InputPreprocessorMessage::PointerMove { .. } = &ipp_message { - } else { - // println!("got ipp message: {:?}", &ipp_message.to_discriminant()); - } } if let Message::InputPreprocessor(InputPreprocessorMessage::BoundsOfViewports { bounds_of_viewports }) = &message { if let Some(graphic_state) = &mut self.graphics_state { diff --git a/frontend/wasm/src/native_communcation.rs b/frontend/wasm/src/native_communcation.rs index 9f3a08bafb..bdf90b9524 100644 --- a/frontend/wasm/src/native_communcation.rs +++ b/frontend/wasm/src/native_communcation.rs @@ -9,8 +9,6 @@ pub fn receive_native_message(buffer: ArrayBuffer) { let buffer = Uint8Array::new(buffer.as_ref()).to_vec(); match ron::from_str::>(str::from_utf8(buffer.as_slice()).unwrap()) { Ok(messages) => { - // log::debug!("Received messages: {:?}", messages); - let callback = move |_: &mut Editor, handle: &mut EditorHandle| { for message in messages { handle.send_frontend_message_to_js_rust_proxy(message);