diff --git a/Cargo.lock b/Cargo.lock index 4aae673311..fd992f59d1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2191,6 +2191,23 @@ dependencies = [ "wgpu", ] +[[package]] +name = "graphene-canvas-utils" +version = "0.1.0" +dependencies = [ + "core-types", + "dyn-any", + "glam", + "graphene-application-io", + "log", + "serde", + "text-nodes", + "vector-types", + "web-sys", + "wgpu", + "wgpu-executor", +] + [[package]] name = "graphene-cli" version = "0.1.0" @@ -2239,6 +2256,7 @@ dependencies = [ "glam", "graph-craft", "graphene-application-io", + "graphene-canvas-utils", "graphene-core", "graphic-nodes", "graphic-types", @@ -6932,7 +6950,6 @@ dependencies = [ "vello", "web-sys", "wgpu", - "winit", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 468b88ea79..aa29f12eb8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -89,6 +89,8 @@ interpreted-executor = { path = "node-graph/interpreted-executor" } node-macro = { path = "node-graph/node-macro" } wgpu-executor = { path = "node-graph/libraries/wgpu-executor" } graphite-proc-macros = { path = "proc-macros" } +graphite-editor = { path = "editor" } +graphene-canvas-utils = { path = "node-graph/libraries/canvas-utils" } # Workspace dependencies rustc-hash = "2.0" diff --git a/desktop/wrapper/Cargo.toml b/desktop/wrapper/Cargo.toml index de8b0e2398..102782dcca 100644 --- a/desktop/wrapper/Cargo.toml +++ b/desktop/wrapper/Cargo.toml @@ -13,9 +13,7 @@ gpu = ["graphite-editor/gpu", "graphene-std/shader-nodes"] [dependencies] # Local dependencies -graphite-editor = { path = "../../editor", features = [ - "gpu", -] } +graphite-editor = { workspace = true } graphene-std = { workspace = true } graph-craft = { workspace = true } wgpu-executor = { workspace = true } diff --git a/desktop/wrapper/src/lib.rs b/desktop/wrapper/src/lib.rs index 343d924a86..4415ba69e7 100644 --- a/desktop/wrapper/src/lib.rs +++ b/desktop/wrapper/src/lib.rs @@ -1,4 +1,4 @@ -use graph_craft::wasm_application_io::WasmApplicationIo; +use graph_craft::application_io::PlatformApplicationIo; use graphite_editor::application::{Editor, Environment, Host, Platform}; use graphite_editor::messages::prelude::{FrontendMessage, Message}; use message_dispatcher::DesktopWrapperMessageDispatcher; @@ -38,7 +38,7 @@ impl DesktopWrapper { } pub fn init(&self, wgpu_context: WgpuContext) { - let application_io = WasmApplicationIo::new_with_context(wgpu_context); + let application_io = PlatformApplicationIo::new_with_context(wgpu_context); futures::executor::block_on(graphite_editor::node_graph_executor::replace_application_io(application_io)); } @@ -51,7 +51,7 @@ impl DesktopWrapper { pub async fn execute_node_graph() -> NodeGraphExecutionResult { let result = graphite_editor::node_graph_executor::run_node_graph().await; match result { - (true, texture) => NodeGraphExecutionResult::HasRun(texture.map(|t| t.texture)), + (true, texture) => NodeGraphExecutionResult::HasRun(texture.map(Into::into)), (false, _) => NodeGraphExecutionResult::NotRun, } } diff --git a/editor/Cargo.toml b/editor/Cargo.toml index 4719d5430a..c7ee2c3ebb 100644 --- a/editor/Cargo.toml +++ b/editor/Cargo.toml @@ -11,9 +11,9 @@ repository = "https://github.com/GraphiteEditor/Graphite" license = "Apache-2.0" [features] -default = ["wasm", "gpu"] -wasm = ["wasm-bindgen", "graphene-std/wasm"] -gpu = ["interpreted-executor/gpu", "wgpu-executor"] +default = ["gpu"] +wasm = ["graphene-std/wasm", "interpreted-executor/wasm", "dep:wasm-bindgen"] +gpu = ["interpreted-executor/gpu", "dep:wgpu-executor"] [dependencies] # Local dependencies diff --git a/editor/src/messages/frontend/frontend_message.rs b/editor/src/messages/frontend/frontend_message.rs index dffd9e9c92..6a97650add 100644 --- a/editor/src/messages/frontend/frontend_message.rs +++ b/editor/src/messages/frontend/frontend_message.rs @@ -130,7 +130,7 @@ pub enum FrontendMessage { TriggerOpen, TriggerImport, TriggerSavePreferences { - #[tsify(type = "unknown")] + #[cfg_attr(feature = "wasm", tsify(type = "unknown"))] preferences: PreferencesMessageHandler, }, TriggerSaveActiveDocument { diff --git a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs index d4e4ec83a6..1591e5fea4 100644 --- a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs +++ b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs @@ -947,12 +947,12 @@ fn document_node_definitions() -> HashMap HashMap HashMap Option { let window = web_sys::window()?; let document = window.document()?; @@ -20,6 +22,7 @@ pub fn overlay_canvas_element() -> Option { canvas.dyn_into::().ok() } +#[cfg(target_family = "wasm")] pub fn overlay_canvas_context() -> web_sys::CanvasRenderingContext2d { let create_context = || { let context = overlay_canvas_element()?.get_context("2d").ok().flatten()?; diff --git a/editor/src/messages/preferences/preferences_message_handler.rs b/editor/src/messages/preferences/preferences_message_handler.rs index 270b0f21bd..591f9cf3a3 100644 --- a/editor/src/messages/preferences/preferences_message_handler.rs +++ b/editor/src/messages/preferences/preferences_message_handler.rs @@ -4,7 +4,7 @@ use crate::messages::portfolio::document::utility_types::wires::GraphWireStyle; use crate::messages::preferences::SelectionMode; use crate::messages::prelude::*; use crate::messages::tool::utility_types::ToolType; -use graph_craft::wasm_application_io::EditorPreferences; +use graph_craft::application_io::EditorPreferences; #[derive(ExtractField)] pub struct PreferencesMessageContext<'a> { @@ -51,7 +51,7 @@ impl PreferencesMessageHandler { } pub fn supports_wgpu(&self) -> bool { - graph_craft::wasm_application_io::wgpu_available().unwrap_or_default() + graph_craft::application_io::wgpu_available().unwrap_or_default() } } diff --git a/editor/src/node_graph_executor.rs b/editor/src/node_graph_executor.rs index 33c2b9b3d9..7f3b2ad9ca 100644 --- a/editor/src/node_graph_executor.rs +++ b/editor/src/node_graph_executor.rs @@ -1,17 +1,16 @@ use crate::messages::frontend::utility_types::{ExportBounds, FileType}; use crate::messages::prelude::*; use glam::{DAffine2, DVec2, UVec2}; -use graph_craft::document::value::{RenderOutput, TaggedValue}; +use graph_craft::application_io::EditorPreferences; +use graph_craft::document::value::{RenderOutput, RenderOutputType, TaggedValue}; use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeInput}; use graph_craft::proto::GraphErrors; -use graph_craft::wasm_application_io::EditorPreferences; use graphene_std::application_io::{NodeGraphUpdateMessage, RenderConfig, TimingInformation}; use graphene_std::raster::{CPU, Raster}; -use graphene_std::renderer::{RenderMetadata, format_transform_matrix}; +use graphene_std::renderer::RenderMetadata; use graphene_std::text::FontCache; use graphene_std::transform::Footprint; use graphene_std::vector::Vector; -use graphene_std::wasm_application_io::RenderOutputType; use interpreted_executor::dynamic_executor::ResolvedDocumentNodeTypesDelta; mod runtime_io; @@ -380,6 +379,7 @@ impl NodeGraphExecutor { let (data, width, height) = raster.to_flat_u8(); responses.add(EyedropperToolMessage::PreviewImage { data, width, height }); } + NodeGraphUpdate::NodeGraphUpdateMessage(_) => {} } } @@ -397,12 +397,11 @@ impl NodeGraphExecutor { responses.add(FrontendMessage::UpdateImageData { image_data }); responses.add(FrontendMessage::UpdateDocumentArtwork { svg }); } - RenderOutputType::CanvasFrame(frame) => { - let matrix = format_transform_matrix(frame.transform); - let transform = if matrix.is_empty() { String::new() } else { format!(" transform=\"{matrix}\"") }; + #[cfg(target_family = "wasm")] + RenderOutputType::CanvasFrame { canvas_id, resolution } => { let svg = format!( - r#"
"#, - frame.resolution.x, frame.resolution.y, frame.surface_id.0, + r#"
"#, + resolution.x, resolution.y, canvas_id, ); responses.add(FrontendMessage::UpdateDocumentArtwork { svg }); } diff --git a/editor/src/node_graph_executor/runtime.rs b/editor/src/node_graph_executor/runtime.rs index 26102cda07..dfd79e8e42 100644 --- a/editor/src/node_graph_executor/runtime.rs +++ b/editor/src/node_graph_executor/runtime.rs @@ -1,25 +1,25 @@ use super::*; use crate::messages::frontend::utility_types::{ExportBounds, FileType}; use glam::{DAffine2, UVec2}; -use graph_craft::document::value::TaggedValue; +use graph_craft::application_io::{PlatformApplicationIo, PlatformEditorApi}; +use graph_craft::document::value::{RenderOutput, RenderOutputType, TaggedValue}; use graph_craft::document::{NodeId, NodeNetwork}; use graph_craft::graphene_compiler::Compiler; use graph_craft::proto::GraphErrors; -use graph_craft::wasm_application_io::EditorPreferences; use graph_craft::{ProtoNodeIdentifier, concrete}; use graphene_std::application_io::{ApplicationIo, ExportFormat, ImageTexture, NodeGraphUpdateMessage, NodeGraphUpdateSender, RenderConfig}; use graphene_std::bounds::RenderBoundingBox; use graphene_std::memo::IORecord; use graphene_std::ops::Convert; +#[cfg(all(target_family = "wasm", feature = "gpu", feature = "wasm"))] +use graphene_std::platform_application_io::canvas_utils::{Canvas, CanvasSurface, CanvasSurfaceHandle}; use graphene_std::raster_types::Raster; -use graphene_std::renderer::{Render, RenderParams, SvgRender}; -use graphene_std::renderer::{RenderSvgSegmentList, SvgSegment}; +use graphene_std::renderer::{Render, RenderParams, RenderSvgSegmentList, SvgRender, SvgSegment}; use graphene_std::table::{Table, TableRow}; use graphene_std::text::FontCache; use graphene_std::transform::RenderQuality; use graphene_std::vector::Vector; use graphene_std::vector::style::RenderMode; -use graphene_std::wasm_application_io::{RenderOutputType, WasmApplicationIo, WasmEditorApi}; use graphene_std::{Artboard, Context, Graphic}; use interpreted_executor::dynamic_executor::{DynamicExecutor, IntrospectError, ResolvedDocumentNodeTypesDelta}; use interpreted_executor::util::wrap_network_in_scope; @@ -28,7 +28,7 @@ use std::sync::Arc; use std::sync::mpsc::{Receiver, Sender}; /// Persistent data between graph executions. It's updated via message passing from the editor thread with [`GraphRuntimeRequest`]`. -/// Some of these fields are put into a [`WasmEditorApi`] which is passed to the final compiled graph network upon each execution. +/// Some of these fields are put into a [`PlatformEditorApi`] which is passed to the final compiled graph network upon each execution. /// Once the implementation is finished, this will live in a separate thread. Right now it's part of the main JS thread, but its own separate JS stack frame independent from the editor. pub struct NodeRuntime { #[cfg(test)] @@ -41,7 +41,7 @@ pub struct NodeRuntime { old_graph: Option, update_thumbnails: bool, - editor_api: Arc, + editor_api: Arc, node_graph_errors: GraphErrors, monitor_nodes: Vec>, @@ -57,10 +57,10 @@ pub struct NodeRuntime { vector_modify: HashMap, /// Cached surface for Wasm viewport rendering (reused across frames) - #[cfg(all(target_family = "wasm", feature = "gpu"))] - wasm_viewport_surface: Option, + #[cfg(all(target_family = "wasm", feature = "gpu", feature = "wasm"))] + wasm_canvas_cache: CanvasSurfaceHandle, /// Currently displayed texture, the runtime keeps a reference to it to avoid the texture getting destroyed while it is still in use. - #[cfg(all(target_family = "wasm", feature = "gpu"))] + #[cfg(all(target_family = "wasm", feature = "gpu", feature = "wasm"))] current_viewport_texture: Option, } @@ -128,7 +128,7 @@ impl NodeRuntime { old_graph: None, update_thumbnails: true, - editor_api: WasmEditorApi { + editor_api: PlatformEditorApi { font_cache: FontCache::default(), editor_preferences: Box::new(EditorPreferences::default()), node_graph_message_sender: Box::new(InternalNodeGraphUpdateSender(sender)), @@ -146,7 +146,7 @@ impl NodeRuntime { vector_modify: Default::default(), inspect_state: None, #[cfg(all(target_family = "wasm", feature = "gpu"))] - wasm_viewport_surface: None, + wasm_canvas_cache: CanvasSurfaceHandle::new(), #[cfg(all(target_family = "wasm", feature = "gpu"))] current_viewport_texture: None, } @@ -154,11 +154,11 @@ impl NodeRuntime { pub async fn run(&mut self) -> Option { if self.editor_api.application_io.is_none() { - self.editor_api = WasmEditorApi { + self.editor_api = PlatformEditorApi { #[cfg(all(not(test), target_family = "wasm"))] - application_io: Some(WasmApplicationIo::new().await.into()), + application_io: Some(PlatformApplicationIo::new().await.into()), #[cfg(any(test, not(target_family = "wasm")))] - application_io: Some(WasmApplicationIo::new_offscreen().await.into()), + application_io: Some(PlatformApplicationIo::new().await.into()), font_cache: self.editor_api.font_cache.clone(), node_graph_message_sender: Box::new(self.sender.clone()), editor_preferences: Box::new(self.editor_preferences.clone()), @@ -208,7 +208,7 @@ impl NodeRuntime { for request in requests { match request { GraphRuntimeRequest::FontCacheUpdate(font_cache) => { - self.editor_api = WasmEditorApi { + self.editor_api = PlatformEditorApi { font_cache, application_io: self.editor_api.application_io.clone(), node_graph_message_sender: Box::new(self.sender.clone()), @@ -222,7 +222,7 @@ impl NodeRuntime { } GraphRuntimeRequest::EditorPreferencesUpdate(preferences) => { self.editor_preferences = preferences.clone(); - self.editor_api = WasmEditorApi { + self.editor_api = PlatformEditorApi { font_cache: self.editor_api.font_cache.clone(), application_io: self.editor_api.application_io.clone(), node_graph_message_sender: Box::new(self.sender.clone()), @@ -280,7 +280,7 @@ impl NodeRuntime { .gpu_executor() .expect("GPU executor should be available when we receive a texture"); - let raster_cpu = Raster::new_gpu(image_texture.texture.as_ref().clone()).convert(Footprint::BOUNDLESS, executor).await; + let raster_cpu = Raster::new_gpu(image_texture.as_ref().clone()).convert(Footprint::BOUNDLESS, executor).await; let (data, width, height) = raster_cpu.to_flat_u8(); @@ -304,7 +304,7 @@ impl NodeRuntime { .gpu_executor() .expect("GPU executor should be available when we receive a texture"); - let raster_cpu = Raster::new_gpu(image_texture.texture.as_ref().clone()).convert(Footprint::BOUNDLESS, executor).await; + let raster_cpu = Raster::new_gpu(image_texture.as_ref().clone()).convert(Footprint::BOUNDLESS, executor).await; self.sender.send_eyedropper_preview(raster_cpu); continue; @@ -318,83 +318,20 @@ impl NodeRuntime { data: RenderOutputType::Texture(image_texture), metadata, })) if !render_config.for_export => { - // On Wasm, for viewport rendering, blit the texture to a surface and return a CanvasFrame + self.current_viewport_texture = Some(image_texture.clone()); + let app_io = self.editor_api.application_io.as_ref().unwrap(); let executor = app_io.gpu_executor().expect("GPU executor should be available when we receive a texture"); - // Get or create the cached surface - if self.wasm_viewport_surface.is_none() { - let surface_handle = app_io.create_window(); - let wasm_surface = executor - .create_surface(graphene_std::wasm_application_io::WasmSurfaceHandle { - surface: surface_handle.surface.clone(), - window_id: surface_handle.window_id, - }) - .expect("Failed to create surface"); - self.wasm_viewport_surface = Some(Arc::new(wasm_surface)); - } - - let surface = self.wasm_viewport_surface.as_ref().unwrap(); - - // Use logical resolution for CSS sizing, physical resolution for the actual surface/texture - let physical_resolution = render_config.viewport.resolution; - let logical_resolution = physical_resolution.as_dvec2() / render_config.scale; - - // Blit the texture to the surface - let mut encoder = executor.context.device.create_command_encoder(&vello::wgpu::CommandEncoderDescriptor { - label: Some("Texture to Surface Blit"), - }); - - // Configure the surface at physical resolution (for HiDPI displays) - let surface_inner = &surface.surface.inner; - let surface_caps = surface_inner.get_capabilities(&executor.context.adapter); - surface_inner.configure( - &executor.context.device, - &vello::wgpu::SurfaceConfiguration { - usage: vello::wgpu::TextureUsages::RENDER_ATTACHMENT | vello::wgpu::TextureUsages::COPY_DST, - format: vello::wgpu::TextureFormat::Rgba8Unorm, - width: physical_resolution.x, - height: physical_resolution.y, - present_mode: surface_caps.present_modes[0], - alpha_mode: vello::wgpu::CompositeAlphaMode::PreMultiplied, - view_formats: vec![], - desired_maximum_frame_latency: 2, - }, - ); - - let surface_texture = surface_inner.get_current_texture().expect("Failed to get surface texture"); - self.current_viewport_texture = Some(image_texture.clone()); - - encoder.copy_texture_to_texture( - vello::wgpu::TexelCopyTextureInfoBase { - texture: image_texture.texture.as_ref(), - mip_level: 0, - origin: Default::default(), - aspect: Default::default(), - }, - vello::wgpu::TexelCopyTextureInfoBase { - texture: &surface_texture.texture, - mip_level: 0, - origin: Default::default(), - aspect: Default::default(), - }, - image_texture.texture.size(), - ); - - executor.context.queue.submit([encoder.finish()]); - surface_texture.present(); - - // TODO: Figure out if we can explicityl destroy the wgpu texture here to reduce the allocation pressure. We might also be able to use a texture allocation pool - - let frame = graphene_std::application_io::SurfaceFrame { - surface_id: surface.window_id, - resolution: logical_resolution, - transform: glam::DAffine2::IDENTITY, - }; + self.wasm_canvas_cache.present(&image_texture, executor); + let logical_resolution = render_config.viewport.resolution.as_dvec2() / render_config.scale; ( Ok(TaggedValue::RenderOutput(RenderOutput { - data: RenderOutputType::CanvasFrame(frame), + data: RenderOutputType::CanvasFrame { + canvas_id: self.wasm_canvas_cache.id(), + resolution: logical_resolution, + }, metadata, })), None, @@ -592,10 +529,10 @@ pub async fn replace_node_runtime(runtime: NodeRuntime) -> Option { let mut node_runtime = NODE_RUNTIME.lock(); node_runtime.replace(runtime) } -pub async fn replace_application_io(application_io: WasmApplicationIo) { +pub async fn replace_application_io(application_io: PlatformApplicationIo) { let mut node_runtime = NODE_RUNTIME.lock(); if let Some(node_runtime) = &mut *node_runtime { - node_runtime.editor_api = WasmEditorApi { + node_runtime.editor_api = PlatformEditorApi { font_cache: node_runtime.editor_api.font_cache.clone(), application_io: Some(application_io.into()), node_graph_message_sender: Box::new(node_runtime.sender.clone()), diff --git a/frontend/wrapper/Cargo.toml b/frontend/wrapper/Cargo.toml index 3b5db1d24a..d0c9945bd2 100644 --- a/frontend/wrapper/Cargo.toml +++ b/frontend/wrapper/Cargo.toml @@ -21,9 +21,7 @@ crate-type = ["cdylib", "rlib"] [dependencies] # Local dependencies -editor = { path = "../../editor", package = "graphite-editor", features = [ - "gpu", -] } +editor = { path = "../../editor", package = "graphite-editor", features = ["gpu", "wasm"] } graphene-std = { workspace = true } # Workspace dependencies diff --git a/node-graph/graph-craft/src/application_io.rs b/node-graph/graph-craft/src/application_io.rs new file mode 100644 index 0000000000..b1ecf42669 --- /dev/null +++ b/node-graph/graph-craft/src/application_io.rs @@ -0,0 +1,55 @@ +use dyn_any::StaticType; + +#[cfg(not(target_family = "wasm"))] +mod native; +#[cfg(target_family = "wasm")] +mod wasm; + +#[cfg(not(target_family = "wasm"))] +pub type PlatformApplicationIo = native::NativeApplicationIo; +#[cfg(target_family = "wasm")] +pub type PlatformApplicationIo = wasm::WasmApplicationIo; + +pub type PlatformEditorApi = graphene_application_io::EditorApi; + +static WGPU_AVAILABLE: std::sync::atomic::AtomicI8 = std::sync::atomic::AtomicI8::new(-1); + +/// Returns: +/// - `None` if the availability of WGPU has not been determined yet +/// - `Some(true)` if WGPU is available +/// - `Some(false)` if WGPU is not available +pub fn wgpu_available() -> Option { + match WGPU_AVAILABLE.load(std::sync::atomic::Ordering::SeqCst) { + -1 => None, + 0 => Some(false), + _ => Some(true), + } +} + +pub(crate) fn set_wgpu_available(available: bool) { + WGPU_AVAILABLE.store(available as i8, std::sync::atomic::Ordering::SeqCst); +} + +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[derive(Clone, Debug, PartialEq, Hash, serde::Serialize, serde::Deserialize)] +pub struct EditorPreferences { + /// Maximum render region size in pixels along one dimension of the square area. + pub max_render_region_size: u32, +} + +impl graphene_application_io::GetEditorPreferences for EditorPreferences { + fn max_render_region_area(&self) -> u32 { + let size = self.max_render_region_size.min(u32::MAX.isqrt()); + size.pow(2) + } +} + +impl Default for EditorPreferences { + fn default() -> Self { + Self { max_render_region_size: 1280 } + } +} + +unsafe impl StaticType for EditorPreferences { + type Static = EditorPreferences; +} diff --git a/node-graph/graph-craft/src/application_io/native.rs b/node-graph/graph-craft/src/application_io/native.rs new file mode 100644 index 0000000000..c795824f19 --- /dev/null +++ b/node-graph/graph-craft/src/application_io/native.rs @@ -0,0 +1,125 @@ +use dyn_any::StaticType; +use graphene_application_io::{ApplicationError, ApplicationIo, EditorApi, ResourceFuture}; +use std::collections::HashMap; +use std::sync::Arc; +#[cfg(feature = "tokio")] +use tokio::io::AsyncReadExt; +#[cfg(target_family = "wasm")] +use wasm_bindgen::JsCast; +#[cfg(feature = "wgpu")] +use wgpu_executor::WgpuExecutor; + +#[derive(Debug, Default)] +pub struct NativeApplicationIo { + #[cfg(feature = "wgpu")] + pub(crate) gpu_executor: Option, + pub resources: HashMap>, +} + +impl NativeApplicationIo { + pub async fn new() -> Self { + #[cfg(feature = "wgpu")] + let executor = WgpuExecutor::new().await; + + #[cfg(not(feature = "wgpu"))] + let wgpu_available = false; + #[cfg(feature = "wgpu")] + let wgpu_available = executor.is_some(); + super::set_wgpu_available(wgpu_available); + + let mut io = Self { + #[cfg(feature = "wgpu")] + gpu_executor: executor, + resources: HashMap::new(), + }; + io.resources.insert("null".to_string(), Arc::from(include_bytes!("../null.png").to_vec())); + + io + } + + #[cfg(feature = "wgpu")] + pub fn new_with_context(context: wgpu_executor::WgpuContext) -> Self { + #[cfg(feature = "wgpu")] + let executor = WgpuExecutor::with_context(context); + + #[cfg(not(feature = "wgpu"))] + let wgpu_available = false; + #[cfg(feature = "wgpu")] + let wgpu_available = executor.is_some(); + super::set_wgpu_available(wgpu_available); + + let mut io = Self { + gpu_executor: executor, + resources: HashMap::new(), + }; + + io.resources.insert("null".to_string(), Arc::from(include_bytes!("../null.png").to_vec())); + + io + } +} + +impl ApplicationIo for NativeApplicationIo { + #[cfg(feature = "wgpu")] + type Executor = WgpuExecutor; + #[cfg(not(feature = "wgpu"))] + type Executor = (); + + #[cfg(feature = "wgpu")] + fn gpu_executor(&self) -> Option<&Self::Executor> { + self.gpu_executor.as_ref() + } + + fn load_resource(&self, url: impl AsRef) -> Result { + let url = url::Url::parse(url.as_ref()).map_err(|_| ApplicationError::InvalidUrl)?; + log::trace!("Loading resource: {url:?}"); + match url.scheme() { + #[cfg(feature = "tokio")] + "file" => { + let path = url.to_file_path().map_err(|_| ApplicationError::NotFound)?; + let path = path.to_str().ok_or(ApplicationError::NotFound)?; + let path = path.to_owned(); + Ok(Box::pin(async move { + let file = tokio::fs::File::open(path).await.map_err(|_| ApplicationError::NotFound)?; + let mut reader = tokio::io::BufReader::new(file); + let mut data = Vec::new(); + reader.read_to_end(&mut data).await.map_err(|_| ApplicationError::NotFound)?; + Ok(Arc::from(data)) + }) as ResourceFuture) + } + "http" | "https" => { + let url = url.to_string(); + Ok(Box::pin(async move { + let client = reqwest::Client::new(); + let response = client.get(url).send().await.map_err(|_| ApplicationError::NotFound)?; + let data = response.bytes().await.map_err(|_| ApplicationError::NotFound)?; + Ok(Arc::from(data.to_vec())) + }) as ResourceFuture) + } + "graphite" => { + let path = url.path(); + let path = path.to_owned(); + log::trace!("Loading local resource: {path}"); + let data = self.resources.get(&path).ok_or(ApplicationError::NotFound)?.clone(); + Ok(Box::pin(async move { Ok(data.clone()) }) as ResourceFuture) + } + _ => Err(ApplicationError::NotFound), + } + } +} + +unsafe impl StaticType for NativeApplicationIo { + type Static = NativeApplicationIo; +} + +impl<'a> From<&'a EditorApi> for &'a NativeApplicationIo { + fn from(editor_api: &'a EditorApi) -> Self { + editor_api.application_io.as_ref().unwrap() + } +} +#[cfg(feature = "wgpu")] +impl<'a> From<&'a NativeApplicationIo> for &'a WgpuExecutor { + fn from(app_io: &'a NativeApplicationIo) -> Self { + app_io.gpu_executor.as_ref().unwrap() + } +} diff --git a/node-graph/graph-craft/src/application_io/wasm.rs b/node-graph/graph-craft/src/application_io/wasm.rs new file mode 100644 index 0000000000..2d752acd82 --- /dev/null +++ b/node-graph/graph-craft/src/application_io/wasm.rs @@ -0,0 +1,117 @@ +use dyn_any::StaticType; +use graphene_application_io::{ApplicationError, ApplicationIo, EditorApi, ResourceFuture}; +use std::collections::HashMap; +use std::sync::Arc; +#[cfg(feature = "tokio")] +use tokio::io::AsyncReadExt; +#[cfg(target_family = "wasm")] +use wasm_bindgen::JsCast; +#[cfg(feature = "wgpu")] +use wgpu_executor::WgpuExecutor; + +#[derive(Debug, Default)] +pub struct WasmApplicationIo { + #[cfg(feature = "wgpu")] + pub(crate) gpu_executor: Option, + pub resources: HashMap>, +} + +impl WasmApplicationIo { + pub async fn new() -> Self { + #[cfg(feature = "wgpu")] + let executor = if let Some(gpu) = web_sys::window().map(|w| w.navigator().gpu()) { + let request_adapter = || { + let request_adapter = js_sys::Reflect::get(&gpu, &wasm_bindgen::JsValue::from_str("requestAdapter")).ok()?; + let function = request_adapter.dyn_ref::()?; + function.call0(&gpu).ok() + }; + let result = request_adapter(); + match result { + None => None, + Some(_) => WgpuExecutor::new().await, + } + } else { + None + }; + + #[cfg(not(feature = "wgpu"))] + let wgpu_available = false; + #[cfg(feature = "wgpu")] + let wgpu_available = executor.is_some(); + super::set_wgpu_available(wgpu_available); + + let mut io = Self { + #[cfg(feature = "wgpu")] + gpu_executor: executor, + resources: HashMap::new(), + }; + io.resources.insert("null".to_string(), Arc::from(include_bytes!("../null.png").to_vec())); + + io + } +} + +impl ApplicationIo for WasmApplicationIo { + #[cfg(feature = "wgpu")] + type Executor = WgpuExecutor; + #[cfg(not(feature = "wgpu"))] + type Executor = (); + + #[cfg(feature = "wgpu")] + fn gpu_executor(&self) -> Option<&Self::Executor> { + self.gpu_executor.as_ref() + } + + fn load_resource(&self, url: impl AsRef) -> Result { + let url = url::Url::parse(url.as_ref()).map_err(|_| ApplicationError::InvalidUrl)?; + log::trace!("Loading resource: {url:?}"); + match url.scheme() { + #[cfg(feature = "tokio")] + "file" => { + let path = url.to_file_path().map_err(|_| ApplicationError::NotFound)?; + let path = path.to_str().ok_or(ApplicationError::NotFound)?; + let path = path.to_owned(); + Ok(Box::pin(async move { + let file = tokio::fs::File::open(path).await.map_err(|_| ApplicationError::NotFound)?; + let mut reader = tokio::io::BufReader::new(file); + let mut data = Vec::new(); + reader.read_to_end(&mut data).await.map_err(|_| ApplicationError::NotFound)?; + Ok(Arc::from(data)) + }) as ResourceFuture) + } + "http" | "https" => { + let url = url.to_string(); + Ok(Box::pin(async move { + let client = reqwest::Client::new(); + let response = client.get(url).send().await.map_err(|_| ApplicationError::NotFound)?; + let data = response.bytes().await.map_err(|_| ApplicationError::NotFound)?; + Ok(Arc::from(data.to_vec())) + }) as ResourceFuture) + } + "graphite" => { + let path = url.path(); + let path = path.to_owned(); + log::trace!("Loading local resource: {path}"); + let data = self.resources.get(&path).ok_or(ApplicationError::NotFound)?.clone(); + Ok(Box::pin(async move { Ok(data.clone()) }) as ResourceFuture) + } + _ => Err(ApplicationError::NotFound), + } + } +} + +unsafe impl StaticType for WasmApplicationIo { + type Static = WasmApplicationIo; +} + +impl<'a> From<&'a EditorApi> for &'a WasmApplicationIo { + fn from(editor_api: &'a EditorApi) -> Self { + editor_api.application_io.as_ref().unwrap() + } +} +#[cfg(feature = "wgpu")] +impl<'a> From<&'a WasmApplicationIo> for &'a WgpuExecutor { + fn from(app_io: &'a WasmApplicationIo) -> Self { + app_io.gpu_executor.as_ref().unwrap() + } +} diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index d38b556354..511710a597 100644 --- a/node-graph/graph-craft/src/document/value.rs +++ b/node-graph/graph-craft/src/document/value.rs @@ -1,6 +1,6 @@ use super::DocumentNode; +use crate::application_io::PlatformEditorApi; use crate::proto::{Any as DAny, FutureAny}; -use crate::wasm_application_io::WasmEditorApi; use brush_nodes::brush_cache::BrushCache; use brush_nodes::brush_stroke::BrushStroke; use core_types::table::Table; @@ -10,7 +10,6 @@ use dyn_any::DynAny; pub use dyn_any::StaticType; use glam::{Affine2, Vec2}; pub use glam::{DAffine2, DVec2, IVec2, UVec2}; -use graphene_application_io::{ImageTexture, SurfaceFrame}; use graphic_types::Artboard; use graphic_types::Graphic; use graphic_types::Vector; @@ -40,9 +39,8 @@ macro_rules! tagged_value { None, $( $(#[$meta] ) *$identifier( $ty ), )* RenderOutput(RenderOutput), - SurfaceFrame(SurfaceFrame), #[serde(skip)] - EditorApi(Arc) + EditorApi(Arc) } // We must manually implement hashing because some values are floats and so do not reproducibly hash (see FakeHash below) @@ -54,7 +52,6 @@ macro_rules! tagged_value { Self::None => {} $( Self::$identifier(x) => {x.hash(state)}),* Self::RenderOutput(x) => x.hash(state), - Self::SurfaceFrame(x) => x.hash(state), Self::EditorApi(x) => x.hash(state), } } @@ -66,7 +63,6 @@ macro_rules! tagged_value { Self::None => Box::new(()), $( Self::$identifier(x) => Box::new(x), )* Self::RenderOutput(x) => Box::new(x), - Self::SurfaceFrame(x) => Box::new(x), Self::EditorApi(x) => Box::new(x), } } @@ -76,7 +72,6 @@ macro_rules! tagged_value { Self::None => Arc::new(()), $( Self::$identifier(x) => Arc::new(x), )* Self::RenderOutput(x) => Arc::new(x), - Self::SurfaceFrame(x) => Arc::new(x), Self::EditorApi(x) => Arc::new(x), } } @@ -86,8 +81,7 @@ macro_rules! tagged_value { Self::None => concrete!(()), $( Self::$identifier(_) => concrete!($ty), )* Self::RenderOutput(_) => concrete!(RenderOutput), - Self::SurfaceFrame(_) => concrete!(SurfaceFrame), - Self::EditorApi(_) => concrete!(&WasmEditorApi) + Self::EditorApi(_) => concrete!(&PlatformEditorApi) } } /// Attempts to downcast the dynamic type to a tagged value @@ -99,8 +93,6 @@ macro_rules! tagged_value { x if x == TypeId::of::<()>() => Ok(TaggedValue::None), $( x if x == TypeId::of::<$ty>() => Ok(TaggedValue::$identifier(*downcast(input).unwrap())), )* x if x == TypeId::of::() => Ok(TaggedValue::RenderOutput(*downcast(input).unwrap())), - x if x == TypeId::of::() => Ok(TaggedValue::SurfaceFrame(*downcast(input).unwrap())), - _ => Err(format!("Cannot convert {:?} to TaggedValue", DynAny::type_name(input.as_ref()))), } @@ -113,8 +105,7 @@ macro_rules! tagged_value { x if x == TypeId::of::<()>() => Ok(TaggedValue::None), $( x if x == TypeId::of::<$ty>() => Ok(TaggedValue::$identifier(<$ty as Clone>::clone(input.downcast_ref().unwrap()))), )* x if x == TypeId::of::() => Ok(TaggedValue::RenderOutput(RenderOutput::clone(input.downcast_ref().unwrap()))), - x if x == TypeId::of::() => Ok(TaggedValue::SurfaceFrame(SurfaceFrame::clone(input.downcast_ref().unwrap()))), - _ => Err(format!("Cannot convert {:?} to TaggedValue",std::any::type_name_of_val(input))), + _ => Err(format!("Cannot convert {:?} to TaggedValue", std::any::type_name_of_val(input))), } } /// Returns a TaggedValue from the type, where that value is its type's `Default::default()` @@ -148,8 +139,7 @@ macro_rules! tagged_value { Self::None => "()".to_string(), $( Self::$identifier(x) => format!("{:?}", x), )* Self::RenderOutput(_) => "RenderOutput".to_string(), - Self::SurfaceFrame(_) => "SurfaceFrame".to_string(), - Self::EditorApi(_) => "WasmEditorApi".to_string(), + Self::EditorApi(_) => "PlatformEditorApi".to_string(), } } } @@ -482,11 +472,10 @@ pub struct RenderOutput { pub metadata: RenderMetadata, } -#[derive(Debug, Clone, Hash, PartialEq, dyn_any::DynAny, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, PartialEq, dyn_any::DynAny, serde::Serialize, serde::Deserialize)] pub enum RenderOutputType { - CanvasFrame(SurfaceFrame), #[serde(skip)] - Texture(ImageTexture), + Texture(graphene_application_io::ImageTexture), #[serde(skip)] Buffer { data: Vec, @@ -497,8 +486,36 @@ pub enum RenderOutputType { svg: String, image_data: Vec<(u64, Image)>, }, + #[cfg(target_family = "wasm")] + CanvasFrame { + canvas_id: u64, + resolution: DVec2, + }, } +impl Hash for RenderOutputType { + fn hash(&self, state: &mut H) { + match self { + Self::Texture(texture) => { + texture.hash(state); + } + Self::Buffer { data, width, height } => { + data.hash(state); + width.hash(state); + height.hash(state); + } + Self::Svg { svg, image_data } => { + svg.hash(state); + image_data.hash(state); + } + #[cfg(target_family = "wasm")] + Self::CanvasFrame { canvas_id, resolution } => { + canvas_id.hash(state); + resolution.to_array().iter().for_each(|x| x.to_bits().hash(state)); + } + } + } +} impl Hash for RenderOutput { fn hash(&self, state: &mut H) { self.data.hash(state) diff --git a/node-graph/graph-craft/src/lib.rs b/node-graph/graph-craft/src/lib.rs index d8d2fe6e07..763eeba7bd 100644 --- a/node-graph/graph-craft/src/lib.rs +++ b/node-graph/graph-craft/src/lib.rs @@ -5,9 +5,9 @@ extern crate core_types; pub use core_types::{ProtoNodeIdentifier, Type, TypeDescriptor, concrete, generic}; +pub mod application_io; pub mod document; pub mod graphene_compiler; pub mod proto; #[cfg(feature = "loading")] pub mod util; -pub mod wasm_application_io; diff --git a/node-graph/graph-craft/src/wasm_application_io.rs b/node-graph/graph-craft/src/wasm_application_io.rs deleted file mode 100644 index 311bf532e1..0000000000 --- a/node-graph/graph-craft/src/wasm_application_io.rs +++ /dev/null @@ -1,361 +0,0 @@ -use dyn_any::StaticType; -use graphene_application_io::{ApplicationError, ApplicationIo, ResourceFuture, SurfaceHandle, SurfaceId}; -#[cfg(target_family = "wasm")] -use js_sys::{Object, Reflect}; -use std::collections::HashMap; -use std::hash::Hash; -use std::sync::Arc; -#[cfg(target_family = "wasm")] -use std::sync::atomic::AtomicU64; -use std::sync::atomic::Ordering; -#[cfg(feature = "tokio")] -use tokio::io::AsyncReadExt; -#[cfg(target_family = "wasm")] -use wasm_bindgen::JsCast; -#[cfg(target_family = "wasm")] -use wasm_bindgen::JsValue; -#[cfg(target_family = "wasm")] -use web_sys::HtmlCanvasElement; -#[cfg(target_family = "wasm")] -use web_sys::window; -#[cfg(feature = "wgpu")] -use wgpu_executor::WgpuExecutor; - -#[derive(Debug)] -struct WindowWrapper { - #[cfg(target_family = "wasm")] - window: SurfaceHandle, - #[cfg(not(target_family = "wasm"))] - window: SurfaceHandle>, -} - -#[cfg(target_family = "wasm")] -impl Drop for WindowWrapper { - fn drop(&mut self) { - let window = window().expect("should have a window in this context"); - let window = Object::from(window); - - let image_canvases_key = JsValue::from_str("imageCanvases"); - - let wrapper = || { - if let Ok(canvases) = Reflect::get(&window, &image_canvases_key) { - // Convert key and value to JsValue - let js_key = JsValue::from_str(self.window.window_id.to_string().as_str()); - - // Use Reflect API to set property - Reflect::delete_property(&canvases.into(), &js_key)?; - } - Ok::<_, JsValue>(()) - }; - - wrapper().expect("should be able to set canvas in global scope") - } -} - -#[cfg(target_family = "wasm")] -unsafe impl Sync for WindowWrapper {} -#[cfg(target_family = "wasm")] -unsafe impl Send for WindowWrapper {} - -#[derive(Debug, Default)] -pub struct WasmApplicationIo { - #[cfg(target_family = "wasm")] - ids: AtomicU64, - #[cfg(feature = "wgpu")] - pub(crate) gpu_executor: Option, - windows: Vec, - pub resources: HashMap>, -} - -static WGPU_AVAILABLE: std::sync::atomic::AtomicI8 = std::sync::atomic::AtomicI8::new(-1); - -/// Returns: -/// - `None` if the availability of WGPU has not been determined yet -/// - `Some(true)` if WGPU is available -/// - `Some(false)` if WGPU is not available -pub fn wgpu_available() -> Option { - match WGPU_AVAILABLE.load(Ordering::SeqCst) { - -1 => None, - 0 => Some(false), - _ => Some(true), - } -} - -impl WasmApplicationIo { - pub async fn new() -> Self { - #[cfg(all(feature = "wgpu", target_family = "wasm"))] - let executor = if let Some(gpu) = web_sys::window().map(|w| w.navigator().gpu()) { - let request_adapter = || { - let request_adapter = js_sys::Reflect::get(&gpu, &wasm_bindgen::JsValue::from_str("requestAdapter")).ok()?; - let function = request_adapter.dyn_ref::()?; - Some(function.call0(&gpu).ok()) - }; - let result = request_adapter(); - match result { - None => None, - Some(_) => WgpuExecutor::new().await, - } - } else { - None - }; - - #[cfg(all(feature = "wgpu", not(target_family = "wasm")))] - let executor = WgpuExecutor::new().await; - - #[cfg(not(feature = "wgpu"))] - let wgpu_available = false; - #[cfg(feature = "wgpu")] - let wgpu_available = executor.is_some(); - WGPU_AVAILABLE.store(wgpu_available as i8, Ordering::SeqCst); - - let mut io = Self { - #[cfg(target_family = "wasm")] - ids: AtomicU64::new(0), - #[cfg(feature = "wgpu")] - gpu_executor: executor, - windows: Vec::new(), - resources: HashMap::new(), - }; - let window = io.create_window(); - io.windows.push(WindowWrapper { window }); - io.resources.insert("null".to_string(), Arc::from(include_bytes!("null.png").to_vec())); - - io - } - - pub async fn new_offscreen() -> Self { - #[cfg(feature = "wgpu")] - let executor = WgpuExecutor::new().await; - - #[cfg(not(feature = "wgpu"))] - let wgpu_available = false; - #[cfg(feature = "wgpu")] - let wgpu_available = executor.is_some(); - WGPU_AVAILABLE.store(wgpu_available as i8, Ordering::SeqCst); - - let mut io = Self { - #[cfg(target_family = "wasm")] - ids: AtomicU64::new(0), - #[cfg(feature = "wgpu")] - gpu_executor: executor, - windows: Vec::new(), - resources: HashMap::new(), - }; - - io.resources.insert("null".to_string(), Arc::from(include_bytes!("null.png").to_vec())); - - io - } - #[cfg(all(not(target_family = "wasm"), feature = "wgpu"))] - pub fn new_with_context(context: wgpu_executor::WgpuContext) -> Self { - #[cfg(feature = "wgpu")] - let executor = WgpuExecutor::with_context(context); - - #[cfg(not(feature = "wgpu"))] - let wgpu_available = false; - #[cfg(feature = "wgpu")] - let wgpu_available = executor.is_some(); - WGPU_AVAILABLE.store(wgpu_available as i8, Ordering::SeqCst); - - let mut io = Self { - gpu_executor: executor, - windows: Vec::new(), - resources: HashMap::new(), - }; - - io.resources.insert("null".to_string(), Arc::from(include_bytes!("null.png").to_vec())); - - io - } -} - -unsafe impl StaticType for WasmApplicationIo { - type Static = WasmApplicationIo; -} - -impl<'a> From<&'a WasmEditorApi> for &'a WasmApplicationIo { - fn from(editor_api: &'a WasmEditorApi) -> Self { - editor_api.application_io.as_ref().unwrap() - } -} -#[cfg(feature = "wgpu")] -impl<'a> From<&'a WasmApplicationIo> for &'a WgpuExecutor { - fn from(app_io: &'a WasmApplicationIo) -> Self { - app_io.gpu_executor.as_ref().unwrap() - } -} - -pub type WasmEditorApi = graphene_application_io::EditorApi; - -impl ApplicationIo for WasmApplicationIo { - #[cfg(target_family = "wasm")] - type Surface = HtmlCanvasElement; - #[cfg(not(target_family = "wasm"))] - type Surface = Arc; - #[cfg(feature = "wgpu")] - type Executor = WgpuExecutor; - #[cfg(not(feature = "wgpu"))] - type Executor = (); - - #[cfg(target_family = "wasm")] - fn create_window(&self) -> SurfaceHandle { - let wrapper = || { - let document = window().expect("should have a window in this context").document().expect("window should have a document"); - - let canvas: HtmlCanvasElement = document.create_element("canvas")?.dyn_into::()?; - let id = self.ids.fetch_add(1, Ordering::SeqCst); - // store the canvas in the global scope so it doesn't get garbage collected - let window = window().expect("should have a window in this context"); - let window = Object::from(window); - - let image_canvases_key = JsValue::from_str("imageCanvases"); - - let mut canvases = Reflect::get(&window, &image_canvases_key); - if canvases.is_err() { - Reflect::set(&JsValue::from(web_sys::window().unwrap()), &image_canvases_key, &Object::new()).unwrap(); - canvases = Reflect::get(&window, &image_canvases_key); - } - - // Convert key and value to JsValue - let js_key = JsValue::from_str(id.to_string().as_str()); - let js_value = JsValue::from(canvas.clone()); - - let canvases = Object::from(canvases.unwrap()); - - // Use Reflect API to set property - Reflect::set(&canvases, &js_key, &js_value)?; - Ok::<_, JsValue>(SurfaceHandle { - window_id: SurfaceId(id), - surface: canvas, - }) - }; - - wrapper().expect("should be able to set canvas in global scope") - } - #[cfg(not(target_family = "wasm"))] - fn create_window(&self) -> SurfaceHandle { - todo!("winit api changed, calling create_window on EventLoop is deprecated"); - - // log::trace!("Spawning window"); - - // #[cfg(all(not(test), target_os = "linux", feature = "wayland"))] - // use winit::platform::wayland::EventLoopBuilderExtWayland; - - // #[cfg(all(not(test), target_os = "linux", feature = "wayland"))] - // let event_loop = winit::event_loop::EventLoopBuilder::new().with_any_thread(true).build().unwrap(); - // #[cfg(not(all(not(test), target_os = "linux", feature = "wayland")))] - // let event_loop = winit::event_loop::EventLoop::new().unwrap(); - - // let window = event_loop - // .create_window( - // winit::window::WindowAttributes::default() - // .with_title("Graphite") - // .with_inner_size(winit::dpi::PhysicalSize::new(800, 600)), - // ) - // .unwrap(); - - // SurfaceHandle { - // window_id: SurfaceId(window.id().into()), - // surface: Arc::new(window), - // } - } - - #[cfg(target_family = "wasm")] - fn destroy_window(&self, surface_id: SurfaceId) { - let window = window().expect("should have a window in this context"); - let window = Object::from(window); - - let image_canvases_key = JsValue::from_str("imageCanvases"); - - let wrapper = || { - if let Ok(canvases) = Reflect::get(&window, &image_canvases_key) { - // Convert key and value to JsValue - let js_key = JsValue::from_str(surface_id.0.to_string().as_str()); - - // Use Reflect API to set property - Reflect::delete_property(&canvases.into(), &js_key)?; - } - Ok::<_, JsValue>(()) - }; - - wrapper().expect("should be able to set canvas in global scope") - } - - #[cfg(not(target_family = "wasm"))] - fn destroy_window(&self, _surface_id: SurfaceId) {} - - #[cfg(feature = "wgpu")] - fn gpu_executor(&self) -> Option<&Self::Executor> { - self.gpu_executor.as_ref() - } - - fn load_resource(&self, url: impl AsRef) -> Result { - let url = url::Url::parse(url.as_ref()).map_err(|_| ApplicationError::InvalidUrl)?; - log::trace!("Loading resource: {url:?}"); - match url.scheme() { - #[cfg(feature = "tokio")] - "file" => { - let path = url.to_file_path().map_err(|_| ApplicationError::NotFound)?; - let path = path.to_str().ok_or(ApplicationError::NotFound)?; - let path = path.to_owned(); - Ok(Box::pin(async move { - let file = tokio::fs::File::open(path).await.map_err(|_| ApplicationError::NotFound)?; - let mut reader = tokio::io::BufReader::new(file); - let mut data = Vec::new(); - reader.read_to_end(&mut data).await.map_err(|_| ApplicationError::NotFound)?; - Ok(Arc::from(data)) - }) as ResourceFuture) - } - "http" | "https" => { - let url = url.to_string(); - Ok(Box::pin(async move { - let client = reqwest::Client::new(); - let response = client.get(url).send().await.map_err(|_| ApplicationError::NotFound)?; - let data = response.bytes().await.map_err(|_| ApplicationError::NotFound)?; - Ok(Arc::from(data.to_vec())) - }) as ResourceFuture) - } - "graphite" => { - let path = url.path(); - let path = path.to_owned(); - log::trace!("Loading local resource: {path}"); - let data = self.resources.get(&path).ok_or(ApplicationError::NotFound)?.clone(); - Ok(Box::pin(async move { Ok(data.clone()) }) as ResourceFuture) - } - _ => Err(ApplicationError::NotFound), - } - } - - fn window(&self) -> Option> { - self.windows.first().map(|wrapper| wrapper.window.clone()) - } -} - -#[cfg(feature = "wgpu")] -pub type WasmSurfaceHandle = SurfaceHandle; -#[cfg(feature = "wgpu")] -pub type WasmSurfaceHandleFrame = graphene_application_io::SurfaceHandleFrame; - -#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] -#[derive(Clone, Debug, PartialEq, Hash, serde::Serialize, serde::Deserialize)] -pub struct EditorPreferences { - /// Maximum render region size in pixels along one dimension of the square area. - pub max_render_region_size: u32, -} - -impl graphene_application_io::GetEditorPreferences for EditorPreferences { - fn max_render_region_area(&self) -> u32 { - let size = self.max_render_region_size.min(u32::MAX.isqrt()); - size.pow(2) - } -} - -impl Default for EditorPreferences { - fn default() -> Self { - Self { max_render_region_size: 1280 } - } -} - -unsafe impl StaticType for EditorPreferences { - type Static = EditorPreferences; -} diff --git a/node-graph/graphene-cli/src/main.rs b/node-graph/graphene-cli/src/main.rs index 00cddd2bf0..5cf12ac04d 100644 --- a/node-graph/graphene-cli/src/main.rs +++ b/node-graph/graphene-cli/src/main.rs @@ -3,14 +3,14 @@ mod export; use clap::{Args, Parser, Subcommand}; use fern::colors::{Color, ColoredLevelConfig}; use futures::executor::block_on; +use graph_craft::application_io::EditorPreferences; use graph_craft::document::*; use graph_craft::graphene_compiler::Compiler; use graph_craft::proto::ProtoNetwork; use graph_craft::util::load_network; -use graph_craft::wasm_application_io::EditorPreferences; use graphene_std::application_io::{ApplicationIo, NodeGraphUpdateMessage, NodeGraphUpdateSender}; +use graphene_std::application_io::{PlatformEditorApi, WasmApplicationIo}; use graphene_std::text::FontCache; -use graphene_std::wasm_application_io::{WasmApplicationIo, WasmEditorApi}; use interpreted_executor::dynamic_executor::DynamicExecutor; use interpreted_executor::util::wrap_network_in_scope; use std::error::Error; @@ -121,7 +121,7 @@ async fn main() -> Result<(), Box> { let document_string = std::fs::read_to_string(document_path).expect("Failed to read document"); log::info!("Creating GPU context"); - let mut application_io = block_on(WasmApplicationIo::new_offscreen()); + let mut application_io = block_on(WasmApplicationIo::new()); if let Command::Export { image: Some(ref image_path), .. } = app.command { application_io.resources.insert("null".to_string(), Arc::from(std::fs::read(image_path).expect("Failed to read image"))); @@ -140,7 +140,7 @@ async fn main() -> Result<(), Box> { let preferences = EditorPreferences { max_render_region_size: EditorPreferences::default().max_render_region_size, }; - let editor_api = Arc::new(WasmEditorApi { + let editor_api = Arc::new(PlatformEditorApi { font_cache: FontCache::default(), application_io: Some(application_io_for_api), node_graph_message_sender: Box::new(UpdateLogger {}), @@ -247,7 +247,7 @@ fn fix_nodes(network: &mut NodeNetwork) { } } } -fn compile_graph(document_string: String, editor_api: Arc) -> Result> { +fn compile_graph(document_string: String, editor_api: Arc) -> Result> { let mut network = load_network(&document_string); fix_nodes(&mut network); diff --git a/node-graph/interpreted-executor/Cargo.toml b/node-graph/interpreted-executor/Cargo.toml index 1013f48862..38178e33c8 100644 --- a/node-graph/interpreted-executor/Cargo.toml +++ b/node-graph/interpreted-executor/Cargo.toml @@ -8,6 +8,7 @@ authors.workspace = true [features] default = [] gpu = ["graphene-std/gpu", "graphene-std/wgpu"] +wasm = ["graphene-std/wasm"] [dependencies] # Local dependencies diff --git a/node-graph/interpreted-executor/src/node_registry.rs b/node-graph/interpreted-executor/src/node_registry.rs index f8cc25f7c7..38512bece9 100644 --- a/node-graph/interpreted-executor/src/node_registry.rs +++ b/node-graph/interpreted-executor/src/node_registry.rs @@ -1,13 +1,16 @@ use dyn_any::StaticType; use glam::{DAffine2, DVec2, IVec2}; +use graph_craft::application_io::PlatformEditorApi; use graph_craft::document::DocumentNode; use graph_craft::document::value::RenderOutput; use graph_craft::proto::{NodeConstructor, TypeErasedBox}; use graphene_std::any::DynAnyNode; -use graphene_std::application_io::{ImageTexture, SurfaceFrame}; +use graphene_std::application_io::ImageTexture; use graphene_std::brush::brush_cache::BrushCache; use graphene_std::brush::brush_stroke::BrushStroke; use graphene_std::gradient::GradientStops; +#[cfg(target_family = "wasm")] +use graphene_std::platform_application_io::canvas_utils::CanvasHandle; #[cfg(feature = "gpu")] use graphene_std::raster::GPU; use graphene_std::raster::color::Color; @@ -18,17 +21,11 @@ use graphene_std::table::Table; use graphene_std::transform::Footprint; use graphene_std::uuid::NodeId; use graphene_std::vector::Vector; -use graphene_std::wasm_application_io::WasmEditorApi; -#[cfg(feature = "gpu")] -use graphene_std::wasm_application_io::WasmSurfaceHandle; use graphene_std::{Artboard, Context, Graphic, NodeIO, NodeIOTypes, ProtoNodeIdentifier, concrete, fn_type_fut, future}; use node_registry_macros::{async_node, convert_node, into_node}; use std::collections::HashMap; #[cfg(feature = "gpu")] -use std::sync::Arc; -#[cfg(feature = "gpu")] use wgpu_executor::WgpuExecutor; -use wgpu_executor::{WgpuSurface, WindowHandle}; // TODO: turn into hashmap fn node_registry() -> HashMap> { @@ -47,7 +44,7 @@ fn node_registry() -> HashMap>, to: Table), // into_node!(from: Table>, to: Table>), #[cfg(feature = "gpu")] - into_node!(from: &WasmEditorApi, to: &WgpuExecutor), + into_node!(from: &PlatformEditorApi, to: &WgpuExecutor), convert_node!(from: DVec2, to: DVec2), convert_node!(from: String, to: String), convert_node!(from: bool, to: String), @@ -138,14 +135,11 @@ fn node_registry() -> HashMap, input: Context, fn_params: [Context => graphene_std::vector::misc::InterpolationDistribution]), // Context nullification #[cfg(feature = "gpu")] - async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => &WasmEditorApi, Context => graphene_std::ContextFeatures]), - #[cfg(feature = "gpu")] - async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => Arc, Context => graphene_std::ContextFeatures]), + async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => &PlatformEditorApi, Context => graphene_std::ContextFeatures]), async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => RenderIntermediate, Context => graphene_std::ContextFeatures]), async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => RenderOutput, Context => graphene_std::ContextFeatures]), - async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => WgpuSurface, Context => graphene_std::ContextFeatures]), - async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => Option, Context => graphene_std::ContextFeatures]), - async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => WindowHandle, Context => graphene_std::ContextFeatures]), + #[cfg(target_family = "wasm")] + async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => CanvasHandle, Context => graphene_std::ContextFeatures]), // ========== // MEMO NODES // ========== @@ -163,11 +157,8 @@ fn node_registry() -> HashMap, input: Context, fn_params: [Context => Vec]), async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Vec]), async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Vec]), - #[cfg(feature = "gpu")] - async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Arc]), - async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => WindowHandle]), - async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Option]), - async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => SurfaceFrame]), + #[cfg(target_family = "wasm")] + async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => CanvasHandle]), async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => f64]), async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => f32]), async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => u32]), @@ -177,9 +168,7 @@ fn node_registry() -> HashMap, input: Context, fn_params: [Context => DAffine2]), async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Footprint]), async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => RenderOutput]), - async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => &WasmEditorApi]), - #[cfg(feature = "gpu")] - async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => WgpuSurface]), + async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => &PlatformEditorApi]), #[cfg(feature = "gpu")] async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Table>]), async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Option]), diff --git a/node-graph/interpreted-executor/src/util.rs b/node-graph/interpreted-executor/src/util.rs index 036db17b2d..ab3b8b4a58 100644 --- a/node-graph/interpreted-executor/src/util.rs +++ b/node-graph/interpreted-executor/src/util.rs @@ -1,16 +1,16 @@ use graph_craft::ProtoNodeIdentifier; +use graph_craft::application_io::PlatformEditorApi; use graph_craft::concrete; use graph_craft::document::value::TaggedValue; use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeInput, NodeNetwork}; use graph_craft::generic; -use graph_craft::wasm_application_io::WasmEditorApi; use graphene_std::Context; use graphene_std::ContextFeatures; use graphene_std::uuid::NodeId; use std::sync::Arc; use wgpu_executor::WgpuExecutor; -pub fn wrap_network_in_scope(mut network: NodeNetwork, editor_api: Arc) -> NodeNetwork { +pub fn wrap_network_in_scope(mut network: NodeNetwork, editor_api: Arc) -> NodeNetwork { network.generate_node_paths(&[]); let inner_network = DocumentNode { @@ -102,7 +102,7 @@ pub fn wrap_network_in_scope(mut network: NodeNetwork, editor_api: Arc) -> std::fmt::Result { - f.write_fmt(format_args!("{}", self.0)) - } -} - -#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)] -pub struct SurfaceFrame { - pub surface_id: SurfaceId, - /// Logical resolution in CSS pixels (used for foreignObject dimensions) - pub resolution: DVec2, - pub transform: DAffine2, -} - -impl Hash for SurfaceFrame { - fn hash(&self, state: &mut H) { - self.surface_id.hash(state); - self.transform.to_cols_array().iter().for_each(|x| x.to_bits().hash(state)); - } -} - -unsafe impl StaticType for SurfaceFrame { - type Static = SurfaceFrame; -} - -pub trait Size { - fn size(&self) -> UVec2; -} - -#[cfg(target_family = "wasm")] -impl Size for web_sys::HtmlCanvasElement { - fn size(&self) -> UVec2 { - UVec2::new(self.width(), self.height()) - } -} - -#[derive(Debug, Clone, DynAny)] -pub struct ImageTexture { - #[cfg(feature = "wgpu")] - pub texture: Arc, - #[cfg(not(feature = "wgpu"))] - pub texture: (), -} - -impl<'a> serde::Deserialize<'a> for ImageTexture { - fn deserialize(_: D) -> Result - where - D: serde::Deserializer<'a>, - { - unimplemented!("attempted to serialize a texture") - } -} - -impl Hash for ImageTexture { - #[cfg(feature = "wgpu")] - fn hash(&self, state: &mut H) { - self.texture.hash(state); - } - #[cfg(not(feature = "wgpu"))] - fn hash(&self, _state: &mut H) {} -} - -impl PartialEq for ImageTexture { - fn eq(&self, other: &Self) -> bool { - #[cfg(feature = "wgpu")] - { - self.texture == other.texture - } - #[cfg(not(feature = "wgpu"))] - { - self.texture == other.texture - } +#[cfg(feature = "wgpu")] +#[derive(Debug, Clone, Hash, PartialEq, Eq, DynAny)] +pub struct ImageTexture(Arc); +#[cfg(feature = "wgpu")] +impl AsRef for ImageTexture { + fn as_ref(&self) -> &wgpu::Texture { + &self.0 } } - #[cfg(feature = "wgpu")] -impl Size for ImageTexture { - fn size(&self) -> UVec2 { - UVec2::new(self.texture.width(), self.texture.height()) +impl From for ImageTexture { + fn from(texture: wgpu::Texture) -> Self { + Self(Arc::new(texture)) } } - -impl From> for SurfaceFrame { - fn from(x: SurfaceHandleFrame) -> Self { - let size = x.surface_handle.surface.size(); - Self { - surface_id: x.surface_handle.window_id, - transform: x.transform, - resolution: size.into(), - } +#[cfg(feature = "wgpu")] +impl From> for ImageTexture { + fn from(texture: Arc) -> Self { + Self(texture) } } - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct SurfaceHandle { - pub window_id: SurfaceId, - pub surface: Surface, -} - -// #[cfg(target_family = "wasm")] -// unsafe impl Send for SurfaceHandle {} -// #[cfg(target_family = "wasm")] -// unsafe impl Sync for SurfaceHandle {} - -impl Size for SurfaceHandle { - fn size(&self) -> UVec2 { - self.surface.size() +#[cfg(feature = "wgpu")] +impl From for Arc { + fn from(image_texture: ImageTexture) -> Self { + image_texture.0 } } - -unsafe impl StaticType for SurfaceHandle { - type Static = SurfaceHandle; -} - -#[derive(Clone, Debug, PartialEq)] -pub struct SurfaceHandleFrame { - pub surface_handle: Arc>, - pub transform: DAffine2, -} - -unsafe impl StaticType for SurfaceHandleFrame { - type Static = SurfaceHandleFrame; -} - -#[cfg(feature = "wasm")] -pub type WasmSurfaceHandle = SurfaceHandle; -#[cfg(feature = "wasm")] -pub type WasmSurfaceHandleFrame = SurfaceHandleFrame; - -// TODO: think about how to automatically clean up memory -/* -impl<'a, Surface> Drop for SurfaceHandle<'a, Surface> { - fn drop(&mut self) { - self.application_io.destroy_surface(self.surface_id) - } -}*/ +#[cfg(not(feature = "wgpu"))] +#[derive(Debug, Clone, Hash, PartialEq, Eq, DynAny)] +pub struct ImageTexture; #[cfg(target_family = "wasm")] pub type ResourceFuture = Pin, ApplicationError>>>>; @@ -157,11 +48,7 @@ pub type ResourceFuture = Pin, Applicat pub type ResourceFuture = Pin, ApplicationError>> + Send>>; pub trait ApplicationIo { - type Surface; type Executor; - fn window(&self) -> Option>; - fn create_window(&self) -> SurfaceHandle; - fn destroy_window(&self, surface_id: SurfaceId); fn gpu_executor(&self) -> Option<&Self::Executor> { None } @@ -169,21 +56,8 @@ pub trait ApplicationIo { } impl ApplicationIo for &T { - type Surface = T::Surface; type Executor = T::Executor; - fn window(&self) -> Option> { - (**self).window() - } - - fn create_window(&self) -> SurfaceHandle { - (**self).create_window() - } - - fn destroy_window(&self, surface_id: SurfaceId) { - (**self).destroy_window(surface_id) - } - fn gpu_executor(&self) -> Option<&T::Executor> { (**self).gpu_executor() } @@ -260,12 +134,12 @@ impl GetEditorPreferences for DummyPreferences { } pub struct EditorApi { - /// Font data (for rendering text) made available to the graph through the [`WasmEditorApi`]. + /// Font data (for rendering text) made available to the graph through the `PlatformEditorApi`. pub font_cache: FontCache, /// Gives access to APIs like a rendering surface (native window handle or HTML5 canvas) and WGPU (which becomes WebGPU on web). pub application_io: Option>, pub node_graph_message_sender: Box, - /// Editor preferences made available to the graph through the [`WasmEditorApi`]. + /// Editor preferences made available to the graph through the `PlatformEditorApi`. pub editor_preferences: Box, } diff --git a/node-graph/libraries/canvas-utils/Cargo.toml b/node-graph/libraries/canvas-utils/Cargo.toml new file mode 100644 index 0000000000..0656c493e5 --- /dev/null +++ b/node-graph/libraries/canvas-utils/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "graphene-canvas-utils" +version = "0.1.0" +edition = "2024" +description = "graphene canvas utilities" +authors = ["Graphite Authors "] +license = "MIT OR Apache-2.0" + +[features] +wgpu = ["dep:wgpu", "dep:wgpu-executor"] + +[dependencies] +# Local dependencies +dyn-any = { workspace = true } +core-types = { workspace = true } +vector-types = { workspace = true } +text-nodes = { workspace = true } +graphene-application-io = { workspace = true } + +# Workspace dependencies +web-sys = { workspace = true } +glam = { workspace = true } +serde = { workspace = true } +log = { workspace = true } + +# Optional workspace dependencies +wgpu = { workspace = true, optional = true } +wgpu-executor = { workspace = true, optional = true } diff --git a/node-graph/libraries/canvas-utils/src/lib.rs b/node-graph/libraries/canvas-utils/src/lib.rs new file mode 100644 index 0000000000..3e5a985e7f --- /dev/null +++ b/node-graph/libraries/canvas-utils/src/lib.rs @@ -0,0 +1,8 @@ +//! A collection of utilities for working with HTML canvases. +//! This library is designed to be used in a WebAssembly context. +//! It doesn't expose any functionality when compiled for non-WebAssembly targets + +#[cfg(target_family = "wasm")] +mod wasm; +#[cfg(target_family = "wasm")] +pub use wasm::*; diff --git a/node-graph/libraries/canvas-utils/src/wasm.rs b/node-graph/libraries/canvas-utils/src/wasm.rs new file mode 100644 index 0000000000..9ec9154d8e --- /dev/null +++ b/node-graph/libraries/canvas-utils/src/wasm.rs @@ -0,0 +1,208 @@ +use dyn_any::DynAny; +#[cfg(feature = "wgpu")] +use graphene_application_io::ImageTexture; +use std::sync::Arc; +use std::sync::atomic::{AtomicU64, Ordering}; +use web_sys::js_sys::{Object, Reflect}; +use web_sys::wasm_bindgen::{JsCast, JsValue}; +use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement, window}; +#[cfg(feature = "wgpu")] +use wgpu_executor::WgpuExecutor; + +const CANVASES_OBJECT_KEY: &str = "imageCanvases"; + +pub type CanvasId = u64; + +static CANVAS_IDS: AtomicU64 = AtomicU64::new(0); + +pub trait Canvas { + fn id(&mut self) -> CanvasId; + fn context(&mut self) -> CanvasRenderingContext2d; + fn set_resolution(&mut self, resolution: glam::UVec2); +} + +#[cfg(feature = "wgpu")] +pub trait CanvasSurface: Canvas { + fn present(&mut self, image_texture: &ImageTexture, executor: &WgpuExecutor); +} + +#[derive(Clone, DynAny)] +pub struct CanvasHandle(Option>); +impl CanvasHandle { + pub fn new() -> Self { + Self(None) + } + fn get(&mut self) -> &CanvasImpl { + if self.0.is_none() { + self.0 = Some(Arc::new(CanvasImpl::new())); + } + self.0.as_ref().unwrap() + } +} +impl Canvas for CanvasHandle { + fn id(&mut self) -> CanvasId { + self.get().canvas_id + } + fn context(&mut self) -> CanvasRenderingContext2d { + self.get().context() + } + fn set_resolution(&mut self, resolution: glam::UVec2) { + self.get().set_resolution(resolution); + } +} + +#[cfg(feature = "wgpu")] +pub struct CanvasSurfaceHandle(CanvasHandle, Option>>); +#[cfg(feature = "wgpu")] +impl CanvasSurfaceHandle { + pub fn new() -> Self { + Self(CanvasHandle::new(), None) + } + fn surface(&mut self, executor: &WgpuExecutor) -> &wgpu::Surface<'_> { + if self.1.is_none() { + let canvas = self.0.get().canvas.clone(); + let surface = executor + .context + .instance + .create_surface(wgpu::SurfaceTarget::Canvas(canvas)) + .expect("Failed to create surface from canvas"); + self.1 = Some(Arc::new(surface)); + } + self.1.as_ref().unwrap() + } +} +#[cfg(feature = "wgpu")] +impl Canvas for CanvasSurfaceHandle { + fn id(&mut self) -> CanvasId { + self.0.id() + } + fn context(&mut self) -> CanvasRenderingContext2d { + self.0.context() + } + fn set_resolution(&mut self, resolution: glam::UVec2) { + self.0.set_resolution(resolution); + } +} +#[cfg(feature = "wgpu")] +impl CanvasSurface for CanvasSurfaceHandle { + fn present(&mut self, image_texture: &ImageTexture, executor: &WgpuExecutor) { + let source_texture: &wgpu::Texture = image_texture.as_ref(); + + let surface = self.surface(executor); + + // Blit the texture to the surface + let mut encoder = executor.context.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("Texture to Surface Blit"), + }); + + let size = source_texture.size(); + + // Configure the surface at physical resolution (for HiDPI displays) + let surface_caps = surface.get_capabilities(&executor.context.adapter); + surface.configure( + &executor.context.device, + &wgpu::SurfaceConfiguration { + usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_DST, + format: wgpu::TextureFormat::Rgba8Unorm, + width: size.width, + height: size.height, + present_mode: surface_caps.present_modes[0], + alpha_mode: wgpu::CompositeAlphaMode::PreMultiplied, + view_formats: vec![], + desired_maximum_frame_latency: 2, + }, + ); + + let surface_texture = surface.get_current_texture().expect("Failed to get surface texture"); + + encoder.copy_texture_to_texture( + wgpu::TexelCopyTextureInfoBase { + texture: source_texture, + mip_level: 0, + origin: Default::default(), + aspect: Default::default(), + }, + wgpu::TexelCopyTextureInfoBase { + texture: &surface_texture.texture, + mip_level: 0, + origin: Default::default(), + aspect: Default::default(), + }, + source_texture.size(), + ); + + executor.context.queue.submit([encoder.finish()]); + surface_texture.present(); + } +} + +/// A wgpu surface backed by an HTML canvas element. +/// Holds a reference to the canvas to prevent garbage collection. +pub struct CanvasImpl { + canvas_id: u64, + canvas: HtmlCanvasElement, +} + +impl CanvasImpl { + fn new() -> Self { + let document = window().expect("should have a window in this context").document().expect("window should have a document"); + + let canvas: HtmlCanvasElement = document.create_element("canvas").unwrap().dyn_into::().unwrap(); + let canvas_id = CANVAS_IDS.fetch_add(1, Ordering::SeqCst); + + // Store the canvas in the global scope so it doesn't get garbage collected + let window = window().expect("should have a window in this context"); + let window_obj = Object::from(window); + + let image_canvases_key = JsValue::from_str(CANVASES_OBJECT_KEY); + + let mut canvases = Reflect::get(&window_obj, &image_canvases_key); + if canvases.is_err() || canvases.as_ref().map_or(false, |v| v.is_undefined() || v.is_null()) { + Reflect::set(&window_obj.clone(), &image_canvases_key, &Object::new()).unwrap(); + canvases = Reflect::get(&window_obj, &image_canvases_key); + } + + // Convert key and value to JsValue + let js_key = JsValue::from_str(canvas_id.to_string().as_str()); + let js_value = JsValue::from(canvas.clone()); + + let canvases = Object::from(canvases.unwrap()); + + // Use Reflect API to set property + Reflect::set(&canvases, &js_key, &js_value).unwrap(); + + Self { canvas_id, canvas } + } + fn context(&self) -> CanvasRenderingContext2d { + self.canvas + .get_context("2d") + .expect("Failed to get 2D context from canvas") + .unwrap() + .dyn_into::() + .expect("Failed to cast context to CanvasRenderingContext2d") + } + fn set_resolution(&self, resolution: glam::UVec2) { + self.canvas.set_width(resolution.x); + self.canvas.set_height(resolution.y); + } +} + +impl Drop for CanvasImpl { + fn drop(&mut self) { + let canvas_id = self.canvas_id; + let window = window().expect("should have a window in this context"); + let window_obj = Object::from(window); + + let image_canvases_key = JsValue::from_str(CANVASES_OBJECT_KEY); + + if let Ok(canvases) = Reflect::get(&window_obj, &image_canvases_key) { + let canvases = Object::from(canvases); + let js_key = JsValue::from_str(canvas_id.to_string().as_str()); + Reflect::delete_property(&canvases, &js_key).unwrap(); + } + } +} + +// SAFETY: WASM is single-threaded, so Send/Sync are safe +unsafe impl Send for CanvasImpl {} +unsafe impl Sync for CanvasImpl {} diff --git a/node-graph/libraries/wgpu-executor/Cargo.toml b/node-graph/libraries/wgpu-executor/Cargo.toml index 23ae4fd5d3..8565d8682d 100644 --- a/node-graph/libraries/wgpu-executor/Cargo.toml +++ b/node-graph/libraries/wgpu-executor/Cargo.toml @@ -20,6 +20,5 @@ anyhow = { workspace = true } wgpu = { workspace = true } futures = { workspace = true } web-sys = { workspace = true } -winit = { workspace = true } vello = { workspace = true } bytemuck = { workspace = true } diff --git a/node-graph/libraries/wgpu-executor/src/lib.rs b/node-graph/libraries/wgpu-executor/src/lib.rs index 011da195e4..7c5c355524 100644 --- a/node-graph/libraries/wgpu-executor/src/lib.rs +++ b/node-graph/libraries/wgpu-executor/src/lib.rs @@ -7,13 +7,10 @@ use crate::resample::Resampler; use crate::shader_runtime::ShaderRuntime; use anyhow::Result; use core_types::Color; -use dyn_any::StaticType; use futures::lock::Mutex; use glam::UVec2; -use graphene_application_io::{ApplicationIo, EditorApi, SurfaceHandle, SurfaceId}; -use std::sync::Arc; +use graphene_application_io::{ApplicationIo, EditorApi}; use vello::{AaConfig, AaSupport, RenderParams, Renderer, RendererOptions, Scene}; -use wgpu::util::TextureBlitter; use wgpu::{Origin3d, TextureAspect}; pub use context::Context as WgpuContext; @@ -42,15 +39,6 @@ impl<'a, T: ApplicationIo> From<&'a EditorApi> for & } } -pub type WgpuSurface = Arc>; -pub type WgpuWindow = Arc>; - -pub struct Surface { - pub inner: wgpu::Surface<'static>, - pub target_texture: Mutex>, - pub blitter: TextureBlitter, -} - #[derive(Clone, Debug)] pub struct TargetTexture { texture: wgpu::Texture, @@ -103,15 +91,6 @@ impl TargetTexture { } } -#[cfg(target_family = "wasm")] -pub type Window = web_sys::HtmlCanvasElement; -#[cfg(not(target_family = "wasm"))] -pub type Window = Arc; - -unsafe impl StaticType for Surface { - type Static = Surface; -} - const VELLO_SURFACE_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm; impl WgpuExecutor { @@ -160,29 +139,6 @@ impl WgpuExecutor { pub fn resample_texture(&self, source: &wgpu::Texture, target_size: UVec2, transform: &glam::DAffine2) -> wgpu::Texture { self.resampler.resample(&self.context, source, target_size, transform) } - - #[cfg(target_family = "wasm")] - pub fn create_surface(&self, canvas: graphene_application_io::WasmSurfaceHandle) -> Result> { - let surface = self.context.instance.create_surface(wgpu::SurfaceTarget::Canvas(canvas.surface))?; - self.create_surface_inner(surface, canvas.window_id) - } - #[cfg(not(target_family = "wasm"))] - pub fn create_surface(&self, window: SurfaceHandle) -> Result> { - let surface = self.context.instance.create_surface(wgpu::SurfaceTarget::Window(Box::new(window.surface)))?; - self.create_surface_inner(surface, window.window_id) - } - - pub fn create_surface_inner(&self, surface: wgpu::Surface<'static>, window_id: SurfaceId) -> Result> { - let blitter = TextureBlitter::new(&self.context.device, VELLO_SURFACE_FORMAT); - Ok(SurfaceHandle { - window_id, - surface: Surface { - inner: surface, - target_texture: Mutex::new(None), - blitter, - }, - }) - } } impl WgpuExecutor { @@ -213,5 +169,3 @@ impl WgpuExecutor { }) } } - -pub type WindowHandle = Arc>; diff --git a/node-graph/node-macro/src/parsing.rs b/node-graph/node-macro/src/parsing.rs index f8be7b910e..e612ca575a 100644 --- a/node-graph/node-macro/src/parsing.rs +++ b/node-graph/node-macro/src/parsing.rs @@ -1271,7 +1271,7 @@ mod tests { fn test_async_node() { let attr = quote!(category("IO")); let input = quote!( - async fn load_image(api: &WasmEditorApi, #[expose] path: String) -> Table> { + async fn load_image(api: &PlatformEditorApi, #[expose] path: String) -> Table> { // Implementation details... } ); @@ -1296,7 +1296,7 @@ mod tests { where_clause: None, input: Input { pat_ident: pat_ident("api"), - ty: parse_quote!(&WasmEditorApi), + ty: parse_quote!(&PlatformEditorApi), implementations: Punctuated::new(), context_features: vec![], }, diff --git a/node-graph/nodes/gstd/Cargo.toml b/node-graph/nodes/gstd/Cargo.toml index ba1d2d62f9..e28b78251f 100644 --- a/node-graph/nodes/gstd/Cargo.toml +++ b/node-graph/nodes/gstd/Cargo.toml @@ -7,9 +7,9 @@ authors = ["Graphite Authors "] license = "MIT OR Apache-2.0" [features] -default = ["wasm", "wgpu"] +default = ["wgpu"] gpu = [] -wgpu = ["gpu", "graph-craft/wgpu", "graphene-application-io/wgpu"] +wgpu = ["gpu", "graph-craft/wgpu", "graphene-application-io/wgpu", "graphene-canvas-utils?/wgpu"] wasm = [ "wasm-bindgen", "wasm-bindgen-futures", @@ -24,6 +24,7 @@ wasm = [ "vector-nodes/wasm", "graphene-core/wasm", "graph-craft/wasm", + "dep:graphene-canvas-utils" ] image-compare = [] vello = ["gpu"] @@ -61,6 +62,9 @@ image = { workspace = true } base64 = { workspace = true } wgpu = { workspace = true } +# Optional local dependencies +graphene-canvas-utils = { workspace = true, optional = true } + # Optional workspace dependencies wasm-bindgen = { workspace = true, optional = true } wasm-bindgen-futures = { workspace = true, optional = true } diff --git a/node-graph/nodes/gstd/src/lib.rs b/node-graph/nodes/gstd/src/lib.rs index bb6a083ca0..f7d36139d6 100644 --- a/node-graph/nodes/gstd/src/lib.rs +++ b/node-graph/nodes/gstd/src/lib.rs @@ -1,10 +1,9 @@ pub mod any; pub mod pixel_preview; +pub mod platform_application_io; pub mod render_cache; pub mod render_node; pub mod text; -#[cfg(feature = "wasm")] -pub mod wasm_application_io; pub use blending_nodes; pub use brush_nodes as brush; pub use core_types::*; diff --git a/node-graph/nodes/gstd/src/pixel_preview.rs b/node-graph/nodes/gstd/src/pixel_preview.rs index 5a56417ec9..266ff7de93 100644 --- a/node-graph/nodes/gstd/src/pixel_preview.rs +++ b/node-graph/nodes/gstd/src/pixel_preview.rs @@ -2,16 +2,16 @@ use crate::render_node::RenderOutputType; use core_types::transform::{Footprint, Transform}; use core_types::{CloneVarArgs, Context, Ctx, ExtractAll, OwnedContextImpl}; use glam::{DAffine2, DVec2, UVec2}; +use graph_craft::application_io::PlatformEditorApi; use graph_craft::document::value::RenderOutput; -use graph_craft::wasm_application_io::WasmEditorApi; -use graphene_application_io::{ApplicationIo, ImageTexture}; +use graphene_application_io::ApplicationIo; use rendering::{RenderOutputType as RenderOutputTypeRequest, RenderParams}; use vector_types::vector::style::RenderMode; #[node_macro::node(category(""))] pub async fn pixel_preview<'a: 'n>( ctx: impl Ctx + ExtractAll + CloneVarArgs + Sync, - editor_api: &'a WasmEditorApi, + editor_api: &'a PlatformEditorApi, data: impl Node, Output = RenderOutput> + Send + Sync, ) -> RenderOutput { let Some(render_params) = ctx.vararg(0).ok().and_then(|v| v.downcast_ref::()).cloned() else { @@ -59,9 +59,9 @@ pub async fn pixel_preview<'a: 'n>( let transform = DAffine2::from_translation(-upstream_min) * footprint.transform.inverse() * DAffine2::from_scale(logical_resolution); let exec = editor_api.application_io.as_ref().unwrap().gpu_executor().unwrap(); - let resampled = exec.resample_texture(&source_texture.texture, physical_resolution, &transform); + let resampled = exec.resample_texture(source_texture.as_ref(), physical_resolution, &transform); - result.data = RenderOutputType::Texture(ImageTexture { texture: resampled.into() }); + result.data = RenderOutputType::Texture(resampled.into()); result .metadata diff --git a/node-graph/nodes/gstd/src/wasm_application_io.rs b/node-graph/nodes/gstd/src/platform_application_io.rs similarity index 90% rename from node-graph/nodes/gstd/src/wasm_application_io.rs rename to node-graph/nodes/gstd/src/platform_application_io.rs index 805436a326..c72828a9d7 100644 --- a/node-graph/nodes/gstd/src/wasm_application_io.rs +++ b/node-graph/nodes/gstd/src/platform_application_io.rs @@ -1,6 +1,8 @@ #[cfg(target_family = "wasm")] use base64::Engine; #[cfg(target_family = "wasm")] +use canvas_utils::{Canvas, CanvasHandle}; +#[cfg(target_family = "wasm")] use core_types::WasmNotSend; #[cfg(target_family = "wasm")] use core_types::math::bbox::Bbox; @@ -8,10 +10,12 @@ use core_types::table::Table; #[cfg(target_family = "wasm")] use core_types::transform::Footprint; use core_types::{Color, Ctx}; +pub use graph_craft::application_io::*; pub use graph_craft::document::value::RenderOutputType; -pub use graph_craft::wasm_application_io::*; use graphene_application_io::ApplicationIo; #[cfg(target_family = "wasm")] +pub use graphene_canvas_utils as canvas_utils; +#[cfg(target_family = "wasm")] use graphic_types::Graphic; #[cfg(target_family = "wasm")] use graphic_types::Vector; @@ -22,17 +26,6 @@ use graphic_types::vector_types::gradient::GradientStops; #[cfg(target_family = "wasm")] use rendering::{Render, RenderParams, RenderSvgSegmentList, SvgRender}; use std::sync::Arc; -#[cfg(target_family = "wasm")] -use wasm_bindgen::JsCast; -#[cfg(target_family = "wasm")] -use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement}; - -/// Allocates GPU memory and a rendering context for vector-to-raster conversion. -#[cfg(feature = "wgpu")] -#[node_macro::node(category(""))] -async fn create_surface<'a: 'n>(_: impl Ctx, editor: &'a WasmEditorApi) -> Arc { - Arc::new(editor.application_io.as_ref().unwrap().create_window()) -} fn parse_headers(headers: &str) -> reqwest::header::HeaderMap { use reqwest::header::{HeaderMap, HeaderName, HeaderValue}; @@ -132,7 +125,7 @@ fn image_to_bytes(_: impl Ctx, image: Table>) -> Vec { /// Loads binary from URLs and local asset paths. Returns a transparent placeholder if the resource fails to load, allowing rendering to continue. #[node_macro::node(category("Web Request"))] -async fn load_resource<'a: 'n>(_: impl Ctx, _primary: (), #[scope("editor-api")] editor_resources: &'a WasmEditorApi, #[name("URL")] url: String) -> Arc<[u8]> { +async fn load_resource<'a: 'n>(_: impl Ctx, _primary: (), #[scope("editor-api")] editor_resources: &'a PlatformEditorApi, #[name("URL")] url: String) -> Arc<[u8]> { let Some(api) = editor_resources.application_io.as_ref() else { return Arc::from(include_bytes!("../../../graph-craft/src/null.png").to_vec()); }; @@ -168,6 +161,12 @@ fn decode_image(_: impl Ctx, data: Arc<[u8]>) -> Table> { Table::new_from_element(Raster::new_cpu(image)) } +#[cfg(target_family = "wasm")] +#[node_macro::node(category(""))] +async fn create_canvas(_: impl Ctx) -> CanvasHandle { + CanvasHandle::new() +} + /// Renders a view of the input graphic within an area defined by the *Footprint*. #[cfg(target_family = "wasm")] #[node_macro::node(category(""))] @@ -182,7 +181,7 @@ async fn rasterize( )] mut data: Table, footprint: Footprint, - surface_handle: Arc>, + mut canvas: CanvasHandle, ) -> Table> where Table: Render, @@ -211,11 +210,8 @@ where render.format_svg(glam::DVec2::ZERO, size); let svg_string = render.svg.to_svg_string(); - let canvas = &surface_handle.surface; - canvas.set_width(resolution.x); - canvas.set_height(resolution.y); - - let context = canvas.get_context("2d").unwrap().unwrap().dyn_into::().unwrap(); + canvas.set_resolution(resolution); + let context = canvas.context(); let preamble = "data:image/svg+xml;base64,"; let mut base64_string = String::with_capacity(preamble.len() + svg_string.len() * 4); diff --git a/node-graph/nodes/gstd/src/render_cache.rs b/node-graph/nodes/gstd/src/render_cache.rs index 72ba23ca83..434c2c8ebb 100644 --- a/node-graph/nodes/gstd/src/render_cache.rs +++ b/node-graph/nodes/gstd/src/render_cache.rs @@ -4,9 +4,9 @@ use core_types::math::bbox::AxisAlignedBbox; use core_types::transform::{Footprint, RenderQuality, Transform}; use core_types::{CloneVarArgs, Context, Ctx, ExtractAll, ExtractAnimationTime, ExtractPointerPosition, ExtractRealTime, OwnedContextImpl}; use glam::{DAffine2, DVec2, IVec2, UVec2}; +use graph_craft::application_io::PlatformEditorApi; use graph_craft::document::value::RenderOutput; -use graph_craft::wasm_application_io::WasmEditorApi; -use graphene_application_io::{ApplicationIo, ImageTexture}; +use graphene_application_io::ApplicationIo; use rendering::{RenderOutputType as RenderOutputTypeRequest, RenderParams}; use std::collections::HashSet; use std::hash::Hash; @@ -374,7 +374,7 @@ fn flood_fill(start: &TileCoord, tile_set: &HashSet, visited: &mut Ha #[node_macro::node(category(""))] pub async fn render_output_cache<'a: 'n>( ctx: impl Ctx + ExtractAll + CloneVarArgs + ExtractRealTime + ExtractAnimationTime + ExtractPointerPosition + Sync, - editor_api: &'a WasmEditorApi, + editor_api: &'a PlatformEditorApi, data: impl Node, Output = RenderOutput> + Send + Sync, #[data] tile_cache: TileCache, ) -> RenderOutput { @@ -460,7 +460,7 @@ pub async fn render_output_cache<'a: 'n>( let combined_metadata = composite_cached_regions(&all_regions, output_texture.as_ref(), &device_origin_offset, &footprint.transform, exec); RenderOutput { - data: RenderOutputType::Texture(ImageTexture { texture: output_texture }), + data: RenderOutputType::Texture(output_texture.into()), metadata: combined_metadata, } } @@ -506,7 +506,7 @@ where let memory_size = (region_pixel_size.x * region_pixel_size.y) as usize * BYTES_PER_PIXEL; CachedRegion { - texture: rendered_texture.texture.as_ref().clone(), + texture: rendered_texture.as_ref().clone(), texture_size: region_pixel_size, tiles: region.tiles.clone(), metadata: result.metadata, @@ -558,7 +558,7 @@ fn composite_cached_regions( aspect: wgpu::TextureAspect::All, }, wgpu::TexelCopyTextureInfo { - texture: &output_texture, + texture: output_texture, mip_level: 0, origin: wgpu::Origin3d { x: dst_x, y: dst_y, z: 0 }, aspect: wgpu::TextureAspect::All, diff --git a/node-graph/nodes/gstd/src/render_node.rs b/node-graph/nodes/gstd/src/render_node.rs index d60eb10bcf..5fa07663fc 100644 --- a/node-graph/nodes/gstd/src/render_node.rs +++ b/node-graph/nodes/gstd/src/render_node.rs @@ -2,10 +2,10 @@ use core_types::table::Table; use core_types::transform::{Footprint, Transform}; use core_types::{CloneVarArgs, ExtractAll, ExtractVarArgs}; use core_types::{Color, Context, Ctx, ExtractFootprint, OwnedContextImpl, WasmNotSend}; +pub use graph_craft::application_io::*; use graph_craft::document::value::RenderOutput; pub use graph_craft::document::value::RenderOutputType; -pub use graph_craft::wasm_application_io::*; -use graphene_application_io::{ApplicationIo, ExportFormat, ImageTexture, RenderConfig}; +use graphene_application_io::{ApplicationIo, ExportFormat, RenderConfig}; use graphic_types::raster_types::Image; use graphic_types::raster_types::{CPU, Raster}; use graphic_types::{Artboard, Graphic, Vector}; @@ -124,7 +124,7 @@ async fn create_context<'a: 'n>( } #[node_macro::node(category(""))] -async fn render<'a: 'n>(ctx: impl Ctx + ExtractFootprint + ExtractVarArgs, editor_api: &'a WasmEditorApi, data: RenderIntermediate) -> RenderOutput { +async fn render<'a: 'n>(ctx: impl Ctx + ExtractFootprint + ExtractVarArgs, editor_api: &'a PlatformEditorApi, data: RenderIntermediate) -> RenderOutput { let footprint = ctx.footprint(); let render_params = ctx .vararg(0) @@ -202,7 +202,7 @@ async fn render<'a: 'n>(ctx: impl Ctx + ExtractFootprint + ExtractVarArgs, edito .expect("Failed to render Vello scene"), ); - RenderOutputType::Texture(ImageTexture { texture }) + RenderOutputType::Texture(texture.into()) } _ => unreachable!("Render node did not receive its requested data type"), }; diff --git a/node-graph/nodes/gstd/src/text.rs b/node-graph/nodes/gstd/src/text.rs index 4cb280ba30..436993a999 100644 --- a/node-graph/nodes/gstd/src/text.rs +++ b/node-graph/nodes/gstd/src/text.rs @@ -1,5 +1,5 @@ use core_types::{Ctx, table::Table}; -use graph_craft::wasm_application_io::WasmEditorApi; +use graph_craft::application_io::PlatformEditorApi; use graphic_types::Vector; pub use text_nodes::*; @@ -9,7 +9,7 @@ fn text<'i: 'n>( _: impl Ctx, /// The Graphite editor's source for global font resources. #[scope("editor-api")] - editor_resources: &'i WasmEditorApi, + editor_resources: &'i PlatformEditorApi, /// The text content to be drawn. #[widget(ParsedWidgetOverride::Custom = "text_area")] #[default("Lorem ipsum")]