Remove surface and window from ApplicationIo (#3941)

* Remove surface and window from ApplicationIo

* Seperate Wasm and Native ApplicationIo

* Fix warnings

* Fix tests

* Remove redundant PlatformApplicationIo::new_offscreen

* Fixup

* Remove unused From implementaitions for ApplicationIo
This commit is contained in:
Timon 2026-04-09 22:12:53 +02:00 committed by GitHub
parent b100892bfa
commit 661e8bc569
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
36 changed files with 716 additions and 773 deletions

19
Cargo.lock generated
View File

@ -1942,6 +1942,23 @@ dependencies = [
"wgpu", "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]] [[package]]
name = "graphene-cli" name = "graphene-cli"
version = "0.1.0" version = "0.1.0"
@ -1990,6 +2007,7 @@ dependencies = [
"glam", "glam",
"graph-craft", "graph-craft",
"graphene-application-io", "graphene-application-io",
"graphene-canvas-utils",
"graphene-core", "graphene-core",
"graphic-nodes", "graphic-nodes",
"graphic-types", "graphic-types",
@ -6601,7 +6619,6 @@ dependencies = [
"vello", "vello",
"web-sys", "web-sys",
"wgpu", "wgpu",
"winit",
] ]
[[package]] [[package]]

View File

@ -89,6 +89,8 @@ interpreted-executor = { path = "node-graph/interpreted-executor" }
node-macro = { path = "node-graph/node-macro" } node-macro = { path = "node-graph/node-macro" }
wgpu-executor = { path = "node-graph/libraries/wgpu-executor" } wgpu-executor = { path = "node-graph/libraries/wgpu-executor" }
graphite-proc-macros = { path = "proc-macros" } graphite-proc-macros = { path = "proc-macros" }
graphite-editor = { path = "editor" }
graphene-canvas-utils = { path = "node-graph/libraries/canvas-utils" }
# Workspace dependencies # Workspace dependencies
rustc-hash = "2.0" rustc-hash = "2.0"

View File

@ -13,9 +13,7 @@ gpu = ["graphite-editor/gpu", "graphene-std/shader-nodes"]
[dependencies] [dependencies]
# Local dependencies # Local dependencies
graphite-editor = { path = "../../editor", features = [ graphite-editor = { workspace = true }
"gpu",
] }
graphene-std = { workspace = true } graphene-std = { workspace = true }
graph-craft = { workspace = true } graph-craft = { workspace = true }
wgpu-executor = { workspace = true } wgpu-executor = { workspace = true }

View File

@ -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::application::{Editor, Environment, Host, Platform};
use graphite_editor::messages::prelude::{FrontendMessage, Message}; use graphite_editor::messages::prelude::{FrontendMessage, Message};
use message_dispatcher::DesktopWrapperMessageDispatcher; use message_dispatcher::DesktopWrapperMessageDispatcher;
@ -38,7 +38,7 @@ impl DesktopWrapper {
} }
pub fn init(&self, wgpu_context: WgpuContext) { 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)); 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 { pub async fn execute_node_graph() -> NodeGraphExecutionResult {
let result = graphite_editor::node_graph_executor::run_node_graph().await; let result = graphite_editor::node_graph_executor::run_node_graph().await;
match result { match result {
(true, texture) => NodeGraphExecutionResult::HasRun(texture.map(|t| t.texture)), (true, texture) => NodeGraphExecutionResult::HasRun(texture.map(Into::into)),
(false, _) => NodeGraphExecutionResult::NotRun, (false, _) => NodeGraphExecutionResult::NotRun,
} }
} }

View File

@ -11,9 +11,9 @@ repository = "https://github.com/GraphiteEditor/Graphite"
license = "Apache-2.0" license = "Apache-2.0"
[features] [features]
default = ["wasm", "gpu"] default = ["gpu"]
wasm = ["wasm-bindgen", "graphene-std/wasm"] wasm = ["graphene-std/wasm", "interpreted-executor/wasm", "dep:wasm-bindgen"]
gpu = ["interpreted-executor/gpu", "wgpu-executor"] gpu = ["interpreted-executor/gpu", "dep:wgpu-executor"]
[dependencies] [dependencies]
# Local dependencies # Local dependencies

View File

@ -131,7 +131,7 @@ pub enum FrontendMessage {
TriggerOpen, TriggerOpen,
TriggerImport, TriggerImport,
TriggerSavePreferences { TriggerSavePreferences {
#[tsify(type = "unknown")] #[cfg_attr(feature = "wasm", tsify(type = "unknown"))]
preferences: PreferencesMessageHandler, preferences: PreferencesMessageHandler,
}, },
TriggerSaveWorkspaceLayout { TriggerSaveWorkspaceLayout {

View File

@ -947,12 +947,12 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
nodes: [ nodes: [
DocumentNode { DocumentNode {
inputs: vec![NodeInput::value(TaggedValue::None, false), NodeInput::scope("editor-api"), NodeInput::import(concrete!(String), 1)], inputs: vec![NodeInput::value(TaggedValue::None, false), NodeInput::scope("editor-api"), NodeInput::import(concrete!(String), 1)],
implementation: DocumentNodeImplementation::ProtoNode(wasm_application_io::load_resource::IDENTIFIER), implementation: DocumentNodeImplementation::ProtoNode(platform_application_io::load_resource::IDENTIFIER),
..Default::default() ..Default::default()
}, },
DocumentNode { DocumentNode {
inputs: vec![NodeInput::node(NodeId(0), 0)], inputs: vec![NodeInput::node(NodeId(0), 0)],
implementation: DocumentNodeImplementation::ProtoNode(wasm_application_io::decode_image::IDENTIFIER), implementation: DocumentNodeImplementation::ProtoNode(platform_application_io::decode_image::IDENTIFIER),
..Default::default() ..Default::default()
}, },
] ]
@ -1010,8 +1010,8 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
exports: vec![NodeInput::node(NodeId(2), 0)], exports: vec![NodeInput::node(NodeId(2), 0)],
nodes: [ nodes: [
DocumentNode { DocumentNode {
inputs: vec![NodeInput::scope("editor-api")], inputs: vec![],
implementation: DocumentNodeImplementation::ProtoNode(wasm_application_io::create_surface::IDENTIFIER), implementation: DocumentNodeImplementation::ProtoNode(platform_application_io::create_canvas::IDENTIFIER),
skip_deduplication: true, skip_deduplication: true,
..Default::default() ..Default::default()
}, },
@ -1022,7 +1022,7 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
}, },
DocumentNode { DocumentNode {
inputs: vec![NodeInput::import(generic!(T), 0), NodeInput::import(concrete!(Footprint), 1), NodeInput::node(NodeId(1), 0)], inputs: vec![NodeInput::import(generic!(T), 0), NodeInput::import(concrete!(Footprint), 1), NodeInput::node(NodeId(1), 0)],
implementation: DocumentNodeImplementation::ProtoNode(wasm_application_io::rasterize::IDENTIFIER), implementation: DocumentNodeImplementation::ProtoNode(platform_application_io::rasterize::IDENTIFIER),
..Default::default() ..Default::default()
}, },
] ]

View File

@ -11,8 +11,10 @@ use graphene_std::vector::misc::ManipulatorPointId;
use graphene_std::vector::{PointId, SegmentId, Vector}; use graphene_std::vector::{PointId, SegmentId, Vector};
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::{LazyLock, Mutex}; use std::sync::{LazyLock, Mutex};
#[cfg(target_family = "wasm")]
use wasm_bindgen::JsCast; use wasm_bindgen::JsCast;
#[cfg(target_family = "wasm")]
pub fn overlay_canvas_element() -> Option<web_sys::HtmlCanvasElement> { pub fn overlay_canvas_element() -> Option<web_sys::HtmlCanvasElement> {
let window = web_sys::window()?; let window = web_sys::window()?;
let document = window.document()?; let document = window.document()?;
@ -20,6 +22,7 @@ pub fn overlay_canvas_element() -> Option<web_sys::HtmlCanvasElement> {
canvas.dyn_into::<web_sys::HtmlCanvasElement>().ok() canvas.dyn_into::<web_sys::HtmlCanvasElement>().ok()
} }
#[cfg(target_family = "wasm")]
pub fn overlay_canvas_context() -> web_sys::CanvasRenderingContext2d { pub fn overlay_canvas_context() -> web_sys::CanvasRenderingContext2d {
let create_context = || { let create_context = || {
let context = overlay_canvas_element()?.get_context("2d").ok().flatten()?; let context = overlay_canvas_element()?.get_context("2d").ok().flatten()?;

View File

@ -4,7 +4,7 @@ use crate::messages::portfolio::document::utility_types::wires::GraphWireStyle;
use crate::messages::preferences::SelectionMode; use crate::messages::preferences::SelectionMode;
use crate::messages::prelude::*; use crate::messages::prelude::*;
use crate::messages::tool::utility_types::ToolType; use crate::messages::tool::utility_types::ToolType;
use graph_craft::wasm_application_io::EditorPreferences; use graph_craft::application_io::EditorPreferences;
#[derive(ExtractField)] #[derive(ExtractField)]
pub struct PreferencesMessageContext<'a> { pub struct PreferencesMessageContext<'a> {
@ -51,7 +51,7 @@ impl PreferencesMessageHandler {
} }
pub fn supports_wgpu(&self) -> bool { 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()
} }
} }

View File

@ -1,17 +1,16 @@
use crate::messages::frontend::utility_types::{ExportBounds, FileType}; use crate::messages::frontend::utility_types::{ExportBounds, FileType};
use crate::messages::prelude::*; use crate::messages::prelude::*;
use glam::{DAffine2, DVec2, UVec2}; 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::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeInput};
use graph_craft::proto::GraphErrors; use graph_craft::proto::GraphErrors;
use graph_craft::wasm_application_io::EditorPreferences;
use graphene_std::application_io::{NodeGraphUpdateMessage, RenderConfig, TimingInformation}; use graphene_std::application_io::{NodeGraphUpdateMessage, RenderConfig, TimingInformation};
use graphene_std::raster::{CPU, Raster}; 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::text::FontCache;
use graphene_std::transform::Footprint; use graphene_std::transform::Footprint;
use graphene_std::vector::Vector; use graphene_std::vector::Vector;
use graphene_std::wasm_application_io::RenderOutputType;
use interpreted_executor::dynamic_executor::ResolvedDocumentNodeTypesDelta; use interpreted_executor::dynamic_executor::ResolvedDocumentNodeTypesDelta;
mod runtime_io; mod runtime_io;
@ -380,6 +379,7 @@ impl NodeGraphExecutor {
let (data, width, height) = raster.to_flat_u8(); let (data, width, height) = raster.to_flat_u8();
responses.add(EyedropperToolMessage::PreviewImage { data, width, height }); responses.add(EyedropperToolMessage::PreviewImage { data, width, height });
} }
NodeGraphUpdate::NodeGraphUpdateMessage(_) => {}
} }
} }
@ -397,12 +397,11 @@ impl NodeGraphExecutor {
responses.add(FrontendMessage::UpdateImageData { image_data }); responses.add(FrontendMessage::UpdateImageData { image_data });
responses.add(FrontendMessage::UpdateDocumentArtwork { svg }); responses.add(FrontendMessage::UpdateDocumentArtwork { svg });
} }
RenderOutputType::CanvasFrame(frame) => { #[cfg(target_family = "wasm")]
let matrix = format_transform_matrix(frame.transform); RenderOutputType::CanvasFrame { canvas_id, resolution } => {
let transform = if matrix.is_empty() { String::new() } else { format!(" transform=\"{matrix}\"") };
let svg = format!( let svg = format!(
r#"<svg><foreignObject width="{}" height="{}"{transform}><div data-canvas-placeholder="{}" data-is-viewport="true"></div></foreignObject></svg>"#, r#"<svg><foreignObject width="{}" height="{}"><div data-canvas-placeholder="{}" data-is-viewport="true"></div></foreignObject></svg>"#,
frame.resolution.x, frame.resolution.y, frame.surface_id.0, resolution.x, resolution.y, canvas_id,
); );
responses.add(FrontendMessage::UpdateDocumentArtwork { svg }); responses.add(FrontendMessage::UpdateDocumentArtwork { svg });
} }

View File

@ -1,25 +1,25 @@
use super::*; use super::*;
use crate::messages::frontend::utility_types::{ExportBounds, FileType}; use crate::messages::frontend::utility_types::{ExportBounds, FileType};
use glam::{DAffine2, UVec2}; 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::document::{NodeId, NodeNetwork};
use graph_craft::graphene_compiler::Compiler; use graph_craft::graphene_compiler::Compiler;
use graph_craft::proto::GraphErrors; use graph_craft::proto::GraphErrors;
use graph_craft::wasm_application_io::EditorPreferences;
use graph_craft::{ProtoNodeIdentifier, concrete}; use graph_craft::{ProtoNodeIdentifier, concrete};
use graphene_std::application_io::{ApplicationIo, ExportFormat, ImageTexture, NodeGraphUpdateMessage, NodeGraphUpdateSender, RenderConfig}; use graphene_std::application_io::{ApplicationIo, ExportFormat, ImageTexture, NodeGraphUpdateMessage, NodeGraphUpdateSender, RenderConfig};
use graphene_std::bounds::RenderBoundingBox; use graphene_std::bounds::RenderBoundingBox;
use graphene_std::memo::IORecord; use graphene_std::memo::IORecord;
use graphene_std::ops::Convert; 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::raster_types::Raster;
use graphene_std::renderer::{Render, RenderParams, SvgRender}; use graphene_std::renderer::{Render, RenderParams, RenderSvgSegmentList, SvgRender, SvgSegment};
use graphene_std::renderer::{RenderSvgSegmentList, SvgSegment};
use graphene_std::table::{Table, TableRow}; use graphene_std::table::{Table, TableRow};
use graphene_std::text::FontCache; use graphene_std::text::FontCache;
use graphene_std::transform::RenderQuality; use graphene_std::transform::RenderQuality;
use graphene_std::vector::Vector; use graphene_std::vector::Vector;
use graphene_std::vector::style::RenderMode; use graphene_std::vector::style::RenderMode;
use graphene_std::wasm_application_io::{RenderOutputType, WasmApplicationIo, WasmEditorApi};
use graphene_std::{Artboard, Context, Graphic}; use graphene_std::{Artboard, Context, Graphic};
use interpreted_executor::dynamic_executor::{DynamicExecutor, IntrospectError, ResolvedDocumentNodeTypesDelta}; use interpreted_executor::dynamic_executor::{DynamicExecutor, IntrospectError, ResolvedDocumentNodeTypesDelta};
use interpreted_executor::util::wrap_network_in_scope; use interpreted_executor::util::wrap_network_in_scope;
@ -28,7 +28,7 @@ use std::sync::Arc;
use std::sync::mpsc::{Receiver, Sender}; use std::sync::mpsc::{Receiver, Sender};
/// Persistent data between graph executions. It's updated via message passing from the editor thread with [`GraphRuntimeRequest`]`. /// 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. /// 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 { pub struct NodeRuntime {
#[cfg(test)] #[cfg(test)]
@ -41,7 +41,7 @@ pub struct NodeRuntime {
old_graph: Option<NodeNetwork>, old_graph: Option<NodeNetwork>,
update_thumbnails: bool, update_thumbnails: bool,
editor_api: Arc<WasmEditorApi>, editor_api: Arc<PlatformEditorApi>,
node_graph_errors: GraphErrors, node_graph_errors: GraphErrors,
monitor_nodes: Vec<Vec<NodeId>>, monitor_nodes: Vec<Vec<NodeId>>,
@ -57,10 +57,10 @@ pub struct NodeRuntime {
vector_modify: HashMap<NodeId, Vector>, vector_modify: HashMap<NodeId, Vector>,
/// Cached surface for Wasm viewport rendering (reused across frames) /// Cached surface for Wasm viewport rendering (reused across frames)
#[cfg(all(target_family = "wasm", feature = "gpu"))] #[cfg(all(target_family = "wasm", feature = "gpu", feature = "wasm"))]
wasm_viewport_surface: Option<wgpu_executor::WgpuSurface>, 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. /// 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<ImageTexture>, current_viewport_texture: Option<ImageTexture>,
} }
@ -128,7 +128,7 @@ impl NodeRuntime {
old_graph: None, old_graph: None,
update_thumbnails: true, update_thumbnails: true,
editor_api: WasmEditorApi { editor_api: PlatformEditorApi {
font_cache: FontCache::default(), font_cache: FontCache::default(),
editor_preferences: Box::new(EditorPreferences::default()), editor_preferences: Box::new(EditorPreferences::default()),
node_graph_message_sender: Box::new(InternalNodeGraphUpdateSender(sender)), node_graph_message_sender: Box::new(InternalNodeGraphUpdateSender(sender)),
@ -146,7 +146,7 @@ impl NodeRuntime {
vector_modify: Default::default(), vector_modify: Default::default(),
inspect_state: None, inspect_state: None,
#[cfg(all(target_family = "wasm", feature = "gpu"))] #[cfg(all(target_family = "wasm", feature = "gpu"))]
wasm_viewport_surface: None, wasm_canvas_cache: CanvasSurfaceHandle::new(),
#[cfg(all(target_family = "wasm", feature = "gpu"))] #[cfg(all(target_family = "wasm", feature = "gpu"))]
current_viewport_texture: None, current_viewport_texture: None,
} }
@ -154,11 +154,11 @@ impl NodeRuntime {
pub async fn run(&mut self) -> Option<ImageTexture> { pub async fn run(&mut self) -> Option<ImageTexture> {
if self.editor_api.application_io.is_none() { if self.editor_api.application_io.is_none() {
self.editor_api = WasmEditorApi { self.editor_api = PlatformEditorApi {
#[cfg(all(not(test), target_family = "wasm"))] #[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")))] #[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(), font_cache: self.editor_api.font_cache.clone(),
node_graph_message_sender: Box::new(self.sender.clone()), node_graph_message_sender: Box::new(self.sender.clone()),
editor_preferences: Box::new(self.editor_preferences.clone()), editor_preferences: Box::new(self.editor_preferences.clone()),
@ -208,7 +208,7 @@ impl NodeRuntime {
for request in requests { for request in requests {
match request { match request {
GraphRuntimeRequest::FontCacheUpdate(font_cache) => { GraphRuntimeRequest::FontCacheUpdate(font_cache) => {
self.editor_api = WasmEditorApi { self.editor_api = PlatformEditorApi {
font_cache, font_cache,
application_io: self.editor_api.application_io.clone(), application_io: self.editor_api.application_io.clone(),
node_graph_message_sender: Box::new(self.sender.clone()), node_graph_message_sender: Box::new(self.sender.clone()),
@ -222,7 +222,7 @@ impl NodeRuntime {
} }
GraphRuntimeRequest::EditorPreferencesUpdate(preferences) => { GraphRuntimeRequest::EditorPreferencesUpdate(preferences) => {
self.editor_preferences = preferences.clone(); self.editor_preferences = preferences.clone();
self.editor_api = WasmEditorApi { self.editor_api = PlatformEditorApi {
font_cache: self.editor_api.font_cache.clone(), font_cache: self.editor_api.font_cache.clone(),
application_io: self.editor_api.application_io.clone(), application_io: self.editor_api.application_io.clone(),
node_graph_message_sender: Box::new(self.sender.clone()), node_graph_message_sender: Box::new(self.sender.clone()),
@ -280,7 +280,7 @@ impl NodeRuntime {
.gpu_executor() .gpu_executor()
.expect("GPU executor should be available when we receive a texture"); .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(); let (data, width, height) = raster_cpu.to_flat_u8();
@ -304,7 +304,7 @@ impl NodeRuntime {
.gpu_executor() .gpu_executor()
.expect("GPU executor should be available when we receive a texture"); .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); self.sender.send_eyedropper_preview(raster_cpu);
continue; continue;
@ -318,83 +318,20 @@ impl NodeRuntime {
data: RenderOutputType::Texture(image_texture), data: RenderOutputType::Texture(image_texture),
metadata, metadata,
})) if !render_config.for_export => { })) 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 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"); let executor = app_io.gpu_executor().expect("GPU executor should be available when we receive a texture");
// Get or create the cached surface self.wasm_canvas_cache.present(&image_texture, executor);
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,
};
let logical_resolution = render_config.viewport.resolution.as_dvec2() / render_config.scale;
( (
Ok(TaggedValue::RenderOutput(RenderOutput { Ok(TaggedValue::RenderOutput(RenderOutput {
data: RenderOutputType::CanvasFrame(frame), data: RenderOutputType::CanvasFrame {
canvas_id: self.wasm_canvas_cache.id(),
resolution: logical_resolution,
},
metadata, metadata,
})), })),
None, None,
@ -592,10 +529,10 @@ pub async fn replace_node_runtime(runtime: NodeRuntime) -> Option<NodeRuntime> {
let mut node_runtime = NODE_RUNTIME.lock(); let mut node_runtime = NODE_RUNTIME.lock();
node_runtime.replace(runtime) 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(); let mut node_runtime = NODE_RUNTIME.lock();
if let Some(node_runtime) = &mut *node_runtime { 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(), font_cache: node_runtime.editor_api.font_cache.clone(),
application_io: Some(application_io.into()), application_io: Some(application_io.into()),
node_graph_message_sender: Box::new(node_runtime.sender.clone()), node_graph_message_sender: Box::new(node_runtime.sender.clone()),

View File

@ -21,9 +21,7 @@ crate-type = ["cdylib", "rlib"]
[dependencies] [dependencies]
# Local dependencies # Local dependencies
editor = { path = "../../editor", package = "graphite-editor", features = [ editor = { path = "../../editor", package = "graphite-editor", features = ["gpu", "wasm"] }
"gpu",
] }
graphene-std = { workspace = true } graphene-std = { workspace = true }
# Workspace dependencies # Workspace dependencies

View File

@ -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<PlatformApplicationIo>;
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<bool> {
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;
}

View File

@ -0,0 +1,113 @@
use dyn_any::StaticType;
use graphene_application_io::{ApplicationError, ApplicationIo, 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<WgpuExecutor>,
pub resources: HashMap<String, Arc<[u8]>>,
}
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<str>) -> Result<ResourceFuture, ApplicationError> {
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;
}

View File

@ -0,0 +1,105 @@
use dyn_any::StaticType;
use graphene_application_io::{ApplicationError, ApplicationIo, 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<WgpuExecutor>,
pub resources: HashMap<String, Arc<[u8]>>,
}
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::<js_sys::Function>()?;
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<str>) -> Result<ResourceFuture, ApplicationError> {
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;
}

View File

@ -1,6 +1,6 @@
use super::DocumentNode; use super::DocumentNode;
use crate::application_io::PlatformEditorApi;
use crate::proto::{Any as DAny, FutureAny}; use crate::proto::{Any as DAny, FutureAny};
use crate::wasm_application_io::WasmEditorApi;
use brush_nodes::brush_cache::BrushCache; use brush_nodes::brush_cache::BrushCache;
use brush_nodes::brush_stroke::BrushStroke; use brush_nodes::brush_stroke::BrushStroke;
use core_types::table::Table; use core_types::table::Table;
@ -10,7 +10,6 @@ use dyn_any::DynAny;
pub use dyn_any::StaticType; pub use dyn_any::StaticType;
use glam::{Affine2, Vec2}; use glam::{Affine2, Vec2};
pub use glam::{DAffine2, DVec2, IVec2, UVec2}; pub use glam::{DAffine2, DVec2, IVec2, UVec2};
use graphene_application_io::{ImageTexture, SurfaceFrame};
use graphic_types::Artboard; use graphic_types::Artboard;
use graphic_types::Graphic; use graphic_types::Graphic;
use graphic_types::Vector; use graphic_types::Vector;
@ -40,9 +39,8 @@ macro_rules! tagged_value {
None, None,
$( $(#[$meta] ) *$identifier( $ty ), )* $( $(#[$meta] ) *$identifier( $ty ), )*
RenderOutput(RenderOutput), RenderOutput(RenderOutput),
SurfaceFrame(SurfaceFrame),
#[serde(skip)] #[serde(skip)]
EditorApi(Arc<WasmEditorApi>) EditorApi(Arc<PlatformEditorApi>)
} }
// We must manually implement hashing because some values are floats and so do not reproducibly hash (see FakeHash below) // 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::None => {}
$( Self::$identifier(x) => {x.hash(state)}),* $( Self::$identifier(x) => {x.hash(state)}),*
Self::RenderOutput(x) => x.hash(state), Self::RenderOutput(x) => x.hash(state),
Self::SurfaceFrame(x) => x.hash(state),
Self::EditorApi(x) => x.hash(state), Self::EditorApi(x) => x.hash(state),
} }
} }
@ -66,7 +63,6 @@ macro_rules! tagged_value {
Self::None => Box::new(()), Self::None => Box::new(()),
$( Self::$identifier(x) => Box::new(x), )* $( Self::$identifier(x) => Box::new(x), )*
Self::RenderOutput(x) => Box::new(x), Self::RenderOutput(x) => Box::new(x),
Self::SurfaceFrame(x) => Box::new(x),
Self::EditorApi(x) => Box::new(x), Self::EditorApi(x) => Box::new(x),
} }
} }
@ -76,7 +72,6 @@ macro_rules! tagged_value {
Self::None => Arc::new(()), Self::None => Arc::new(()),
$( Self::$identifier(x) => Arc::new(x), )* $( Self::$identifier(x) => Arc::new(x), )*
Self::RenderOutput(x) => Arc::new(x), Self::RenderOutput(x) => Arc::new(x),
Self::SurfaceFrame(x) => Arc::new(x),
Self::EditorApi(x) => Arc::new(x), Self::EditorApi(x) => Arc::new(x),
} }
} }
@ -86,8 +81,7 @@ macro_rules! tagged_value {
Self::None => concrete!(()), Self::None => concrete!(()),
$( Self::$identifier(_) => concrete!($ty), )* $( Self::$identifier(_) => concrete!($ty), )*
Self::RenderOutput(_) => concrete!(RenderOutput), Self::RenderOutput(_) => concrete!(RenderOutput),
Self::SurfaceFrame(_) => concrete!(SurfaceFrame), Self::EditorApi(_) => concrete!(&PlatformEditorApi)
Self::EditorApi(_) => concrete!(&WasmEditorApi)
} }
} }
/// Attempts to downcast the dynamic type to a tagged value /// 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::<()>() => Ok(TaggedValue::None),
$( x if x == TypeId::of::<$ty>() => Ok(TaggedValue::$identifier(*downcast(input).unwrap())), )* $( x if x == TypeId::of::<$ty>() => Ok(TaggedValue::$identifier(*downcast(input).unwrap())), )*
x if x == TypeId::of::<RenderOutput>() => Ok(TaggedValue::RenderOutput(*downcast(input).unwrap())), x if x == TypeId::of::<RenderOutput>() => Ok(TaggedValue::RenderOutput(*downcast(input).unwrap())),
x if x == TypeId::of::<SurfaceFrame>() => Ok(TaggedValue::SurfaceFrame(*downcast(input).unwrap())),
_ => Err(format!("Cannot convert {:?} to TaggedValue", DynAny::type_name(input.as_ref()))), _ => 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::<()>() => 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::<$ty>() => Ok(TaggedValue::$identifier(<$ty as Clone>::clone(input.downcast_ref().unwrap()))), )*
x if x == TypeId::of::<RenderOutput>() => Ok(TaggedValue::RenderOutput(RenderOutput::clone(input.downcast_ref().unwrap()))), x if x == TypeId::of::<RenderOutput>() => Ok(TaggedValue::RenderOutput(RenderOutput::clone(input.downcast_ref().unwrap()))),
x if x == TypeId::of::<SurfaceFrame>() => 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()` /// 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::None => "()".to_string(),
$( Self::$identifier(x) => format!("{:?}", x), )* $( Self::$identifier(x) => format!("{:?}", x), )*
Self::RenderOutput(_) => "RenderOutput".to_string(), Self::RenderOutput(_) => "RenderOutput".to_string(),
Self::SurfaceFrame(_) => "SurfaceFrame".to_string(), Self::EditorApi(_) => "PlatformEditorApi".to_string(),
Self::EditorApi(_) => "WasmEditorApi".to_string(),
} }
} }
} }
@ -482,11 +472,10 @@ pub struct RenderOutput {
pub metadata: RenderMetadata, 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 { pub enum RenderOutputType {
CanvasFrame(SurfaceFrame),
#[serde(skip)] #[serde(skip)]
Texture(ImageTexture), Texture(graphene_application_io::ImageTexture),
#[serde(skip)] #[serde(skip)]
Buffer { Buffer {
data: Vec<u8>, data: Vec<u8>,
@ -497,8 +486,36 @@ pub enum RenderOutputType {
svg: String, svg: String,
image_data: Vec<(u64, Image<Color>)>, image_data: Vec<(u64, Image<Color>)>,
}, },
#[cfg(target_family = "wasm")]
CanvasFrame {
canvas_id: u64,
resolution: DVec2,
},
} }
impl Hash for RenderOutputType {
fn hash<H: std::hash::Hasher>(&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 { impl Hash for RenderOutput {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) { fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.data.hash(state) self.data.hash(state)

View File

@ -5,9 +5,9 @@ extern crate core_types;
pub use core_types::{ProtoNodeIdentifier, Type, TypeDescriptor, concrete, generic}; pub use core_types::{ProtoNodeIdentifier, Type, TypeDescriptor, concrete, generic};
pub mod application_io;
pub mod document; pub mod document;
pub mod graphene_compiler; pub mod graphene_compiler;
pub mod proto; pub mod proto;
#[cfg(feature = "loading")] #[cfg(feature = "loading")]
pub mod util; pub mod util;
pub mod wasm_application_io;

View File

@ -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<HtmlCanvasElement>,
#[cfg(not(target_family = "wasm"))]
window: SurfaceHandle<Arc<dyn winit::window::Window>>,
}
#[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<WgpuExecutor>,
windows: Vec<WindowWrapper>,
pub resources: HashMap<String, Arc<[u8]>>,
}
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<bool> {
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::<js_sys::Function>()?;
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<WasmApplicationIo>;
impl ApplicationIo for WasmApplicationIo {
#[cfg(target_family = "wasm")]
type Surface = HtmlCanvasElement;
#[cfg(not(target_family = "wasm"))]
type Surface = Arc<dyn winit::window::Window>;
#[cfg(feature = "wgpu")]
type Executor = WgpuExecutor;
#[cfg(not(feature = "wgpu"))]
type Executor = ();
#[cfg(target_family = "wasm")]
fn create_window(&self) -> SurfaceHandle<Self::Surface> {
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::<HtmlCanvasElement>()?;
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<Self::Surface> {
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<str>) -> Result<ResourceFuture, ApplicationError> {
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<SurfaceHandle<Self::Surface>> {
self.windows.first().map(|wrapper| wrapper.window.clone())
}
}
#[cfg(feature = "wgpu")]
pub type WasmSurfaceHandle = SurfaceHandle<wgpu_executor::Window>;
#[cfg(feature = "wgpu")]
pub type WasmSurfaceHandleFrame = graphene_application_io::SurfaceHandleFrame<wgpu_executor::Window>;
#[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;
}

View File

@ -3,14 +3,14 @@ mod export;
use clap::{Args, Parser, Subcommand}; use clap::{Args, Parser, Subcommand};
use fern::colors::{Color, ColoredLevelConfig}; use fern::colors::{Color, ColoredLevelConfig};
use futures::executor::block_on; use futures::executor::block_on;
use graph_craft::application_io::EditorPreferences;
use graph_craft::document::*; use graph_craft::document::*;
use graph_craft::graphene_compiler::Compiler; use graph_craft::graphene_compiler::Compiler;
use graph_craft::proto::ProtoNetwork; use graph_craft::proto::ProtoNetwork;
use graph_craft::util::load_network; 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::{ApplicationIo, NodeGraphUpdateMessage, NodeGraphUpdateSender};
use graphene_std::application_io::{PlatformEditorApi, WasmApplicationIo};
use graphene_std::text::FontCache; use graphene_std::text::FontCache;
use graphene_std::wasm_application_io::{WasmApplicationIo, WasmEditorApi};
use interpreted_executor::dynamic_executor::DynamicExecutor; use interpreted_executor::dynamic_executor::DynamicExecutor;
use interpreted_executor::util::wrap_network_in_scope; use interpreted_executor::util::wrap_network_in_scope;
use std::error::Error; use std::error::Error;
@ -121,7 +121,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
let document_string = std::fs::read_to_string(document_path).expect("Failed to read document"); let document_string = std::fs::read_to_string(document_path).expect("Failed to read document");
log::info!("Creating GPU context"); 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 { 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"))); 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<dyn Error>> {
let preferences = EditorPreferences { let preferences = EditorPreferences {
max_render_region_size: EditorPreferences::default().max_render_region_size, 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(), font_cache: FontCache::default(),
application_io: Some(application_io_for_api), application_io: Some(application_io_for_api),
node_graph_message_sender: Box::new(UpdateLogger {}), 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<WasmEditorApi>) -> Result<ProtoNetwork, Box<dyn Error>> { fn compile_graph(document_string: String, editor_api: Arc<PlatformEditorApi>) -> Result<ProtoNetwork, Box<dyn Error>> {
let mut network = load_network(&document_string); let mut network = load_network(&document_string);
fix_nodes(&mut network); fix_nodes(&mut network);

View File

@ -8,6 +8,7 @@ authors.workspace = true
[features] [features]
default = [] default = []
gpu = ["graphene-std/gpu", "graphene-std/wgpu"] gpu = ["graphene-std/gpu", "graphene-std/wgpu"]
wasm = ["graphene-std/wasm"]
[dependencies] [dependencies]
# Local dependencies # Local dependencies

View File

@ -1,13 +1,16 @@
use dyn_any::StaticType; use dyn_any::StaticType;
use glam::{DAffine2, DVec2, IVec2}; use glam::{DAffine2, DVec2, IVec2};
use graph_craft::application_io::PlatformEditorApi;
use graph_craft::document::DocumentNode; use graph_craft::document::DocumentNode;
use graph_craft::document::value::RenderOutput; use graph_craft::document::value::RenderOutput;
use graph_craft::proto::{NodeConstructor, TypeErasedBox}; use graph_craft::proto::{NodeConstructor, TypeErasedBox};
use graphene_std::any::DynAnyNode; 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_cache::BrushCache;
use graphene_std::brush::brush_stroke::BrushStroke; use graphene_std::brush::brush_stroke::BrushStroke;
use graphene_std::gradient::GradientStops; use graphene_std::gradient::GradientStops;
#[cfg(target_family = "wasm")]
use graphene_std::platform_application_io::canvas_utils::CanvasHandle;
#[cfg(feature = "gpu")] #[cfg(feature = "gpu")]
use graphene_std::raster::GPU; use graphene_std::raster::GPU;
use graphene_std::raster::color::Color; use graphene_std::raster::color::Color;
@ -18,17 +21,11 @@ use graphene_std::table::Table;
use graphene_std::transform::Footprint; use graphene_std::transform::Footprint;
use graphene_std::uuid::NodeId; use graphene_std::uuid::NodeId;
use graphene_std::vector::Vector; 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 graphene_std::{Artboard, Context, Graphic, NodeIO, NodeIOTypes, ProtoNodeIdentifier, concrete, fn_type_fut, future};
use node_registry_macros::{async_node, convert_node, into_node}; use node_registry_macros::{async_node, convert_node, into_node};
use std::collections::HashMap; use std::collections::HashMap;
#[cfg(feature = "gpu")] #[cfg(feature = "gpu")]
use std::sync::Arc;
#[cfg(feature = "gpu")]
use wgpu_executor::WgpuExecutor; use wgpu_executor::WgpuExecutor;
use wgpu_executor::{WgpuSurface, WindowHandle};
// TODO: turn into hashmap // TODO: turn into hashmap
fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeConstructor>> { fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeConstructor>> {
@ -47,7 +44,7 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
convert_node!(from: Table<Raster<GPU>>, to: Table<Graphic>), convert_node!(from: Table<Raster<GPU>>, to: Table<Graphic>),
// into_node!(from: Table<Raster<CPU>>, to: Table<Raster<SRGBA8>>), // into_node!(from: Table<Raster<CPU>>, to: Table<Raster<SRGBA8>>),
#[cfg(feature = "gpu")] #[cfg(feature = "gpu")]
into_node!(from: &WasmEditorApi, to: &WgpuExecutor), into_node!(from: &PlatformEditorApi, to: &WgpuExecutor),
convert_node!(from: DVec2, to: DVec2), convert_node!(from: DVec2, to: DVec2),
convert_node!(from: String, to: String), convert_node!(from: String, to: String),
convert_node!(from: bool, to: String), convert_node!(from: bool, to: String),
@ -138,14 +135,11 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::InterpolationDistribution]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::InterpolationDistribution]),
// Context nullification // Context nullification
#[cfg(feature = "gpu")] #[cfg(feature = "gpu")]
async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => &WasmEditorApi, Context => graphene_std::ContextFeatures]), async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => &PlatformEditorApi, Context => graphene_std::ContextFeatures]),
#[cfg(feature = "gpu")]
async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => Arc<WasmSurfaceHandle>, 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 => 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 => RenderOutput, Context => graphene_std::ContextFeatures]),
async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => WgpuSurface, Context => graphene_std::ContextFeatures]), #[cfg(target_family = "wasm")]
async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => Option<WgpuSurface>, Context => graphene_std::ContextFeatures]), async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => CanvasHandle, Context => graphene_std::ContextFeatures]),
async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => WindowHandle, Context => graphene_std::ContextFeatures]),
// ========== // ==========
// MEMO NODES // MEMO NODES
// ========== // ==========
@ -163,11 +157,8 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Vec<f64>]), async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Vec<f64>]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Vec<f32>]), async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Vec<f32>]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Vec<String>]), async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Vec<String>]),
#[cfg(feature = "gpu")] #[cfg(target_family = "wasm")]
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Arc<WasmSurfaceHandle>]), async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => CanvasHandle]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => WindowHandle]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Option<WgpuSurface>]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => SurfaceFrame]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => f64]), 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 => f32]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => u32]), async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => u32]),
@ -177,9 +168,7 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => DAffine2]), async_node!(graphene_core::memo::MemoNode<_, _>, 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 => Footprint]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => RenderOutput]), async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => RenderOutput]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => &WasmEditorApi]), 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 => WgpuSurface]),
#[cfg(feature = "gpu")] #[cfg(feature = "gpu")]
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Table<Raster<GPU>>]), async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Table<Raster<GPU>>]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Option<f64>]), async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Option<f64>]),

View File

@ -1,16 +1,16 @@
use graph_craft::ProtoNodeIdentifier; use graph_craft::ProtoNodeIdentifier;
use graph_craft::application_io::PlatformEditorApi;
use graph_craft::concrete; use graph_craft::concrete;
use graph_craft::document::value::TaggedValue; use graph_craft::document::value::TaggedValue;
use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeInput, NodeNetwork}; use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeInput, NodeNetwork};
use graph_craft::generic; use graph_craft::generic;
use graph_craft::wasm_application_io::WasmEditorApi;
use graphene_std::Context; use graphene_std::Context;
use graphene_std::ContextFeatures; use graphene_std::ContextFeatures;
use graphene_std::uuid::NodeId; use graphene_std::uuid::NodeId;
use std::sync::Arc; use std::sync::Arc;
use wgpu_executor::WgpuExecutor; use wgpu_executor::WgpuExecutor;
pub fn wrap_network_in_scope(mut network: NodeNetwork, editor_api: Arc<WasmEditorApi>) -> NodeNetwork { pub fn wrap_network_in_scope(mut network: NodeNetwork, editor_api: Arc<PlatformEditorApi>) -> NodeNetwork {
network.generate_node_paths(&[]); network.generate_node_paths(&[]);
let inner_network = DocumentNode { let inner_network = DocumentNode {
@ -102,7 +102,7 @@ pub fn wrap_network_in_scope(mut network: NodeNetwork, editor_api: Arc<WasmEdito
..Default::default() ..Default::default()
}, },
]; ];
let mut scope_injections = vec![("editor-api".to_string(), (NodeId(2), concrete!(&WasmEditorApi)))]; let mut scope_injections = vec![("editor-api".to_string(), (NodeId(2), concrete!(&PlatformEditorApi)))];
if cfg!(feature = "gpu") { if cfg!(feature = "gpu") {
nodes.push(DocumentNode { nodes.push(DocumentNode {

View File

@ -1,6 +1,6 @@
use core_types::transform::Footprint; use core_types::transform::Footprint;
use dyn_any::{DynAny, StaticType, StaticTypeSized}; use dyn_any::{DynAny, StaticType, StaticTypeSized};
use glam::{DAffine2, DVec2, UVec2}; use glam::DVec2;
use std::fmt::Debug; use std::fmt::Debug;
use std::future::Future; use std::future::Future;
use std::hash::{Hash, Hasher}; use std::hash::{Hash, Hasher};
@ -11,145 +11,36 @@ use std::time::Duration;
use text_nodes::FontCache; use text_nodes::FontCache;
use vector_types::vector::style::RenderMode; use vector_types::vector::style::RenderMode;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub struct SurfaceId(pub u64);
impl std::fmt::Display for SurfaceId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> 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<H: Hasher>(&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<wgpu::Texture>,
#[cfg(not(feature = "wgpu"))]
pub texture: (),
}
impl<'a> serde::Deserialize<'a> for ImageTexture {
fn deserialize<D>(_: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'a>,
{
unimplemented!("attempted to serialize a texture")
}
}
impl Hash for ImageTexture {
#[cfg(feature = "wgpu")]
fn hash<H: Hasher>(&self, state: &mut H) {
self.texture.hash(state);
}
#[cfg(not(feature = "wgpu"))]
fn hash<H: Hasher>(&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")] #[cfg(feature = "wgpu")]
impl Size for ImageTexture { #[derive(Debug, Clone, Hash, PartialEq, Eq, DynAny)]
fn size(&self) -> UVec2 { pub struct ImageTexture(Arc<wgpu::Texture>);
UVec2::new(self.texture.width(), self.texture.height()) #[cfg(feature = "wgpu")]
impl AsRef<wgpu::Texture> for ImageTexture {
fn as_ref(&self) -> &wgpu::Texture {
&self.0
} }
} }
#[cfg(feature = "wgpu")]
impl<S: Size> From<SurfaceHandleFrame<S>> for SurfaceFrame { impl From<wgpu::Texture> for ImageTexture {
fn from(x: SurfaceHandleFrame<S>) -> Self { fn from(texture: wgpu::Texture) -> Self {
let size = x.surface_handle.surface.size(); Self(Arc::new(texture))
Self {
surface_id: x.surface_handle.window_id,
transform: x.transform,
resolution: size.into(),
}
} }
} }
#[cfg(feature = "wgpu")]
#[derive(Clone, Debug, PartialEq, Eq)] impl From<Arc<wgpu::Texture>> for ImageTexture {
pub struct SurfaceHandle<Surface> { fn from(texture: Arc<wgpu::Texture>) -> Self {
pub window_id: SurfaceId, Self(texture)
pub surface: Surface,
}
// #[cfg(target_family = "wasm")]
// unsafe impl<T: dyn_any::WasmNotSend> Send for SurfaceHandle<T> {}
// #[cfg(target_family = "wasm")]
// unsafe impl<T: dyn_any::WasmNotSync> Sync for SurfaceHandle<T> {}
impl<S: Size> Size for SurfaceHandle<S> {
fn size(&self) -> UVec2 {
self.surface.size()
} }
} }
#[cfg(feature = "wgpu")]
unsafe impl<T: 'static> StaticType for SurfaceHandle<T> { impl From<ImageTexture> for Arc<wgpu::Texture> {
type Static = SurfaceHandle<T>; fn from(image_texture: ImageTexture) -> Self {
} image_texture.0
#[derive(Clone, Debug, PartialEq)]
pub struct SurfaceHandleFrame<Surface> {
pub surface_handle: Arc<SurfaceHandle<Surface>>,
pub transform: DAffine2,
}
unsafe impl<T: 'static> StaticType for SurfaceHandleFrame<T> {
type Static = SurfaceHandleFrame<T>;
}
#[cfg(feature = "wasm")]
pub type WasmSurfaceHandle = SurfaceHandle<web_sys::HtmlCanvasElement>;
#[cfg(feature = "wasm")]
pub type WasmSurfaceHandleFrame = SurfaceHandleFrame<web_sys::HtmlCanvasElement>;
// 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")] #[cfg(target_family = "wasm")]
pub type ResourceFuture = Pin<Box<dyn Future<Output = Result<Arc<[u8]>, ApplicationError>>>>; pub type ResourceFuture = Pin<Box<dyn Future<Output = Result<Arc<[u8]>, ApplicationError>>>>;
@ -157,11 +48,7 @@ pub type ResourceFuture = Pin<Box<dyn Future<Output = Result<Arc<[u8]>, Applicat
pub type ResourceFuture = Pin<Box<dyn Future<Output = Result<Arc<[u8]>, ApplicationError>> + Send>>; pub type ResourceFuture = Pin<Box<dyn Future<Output = Result<Arc<[u8]>, ApplicationError>> + Send>>;
pub trait ApplicationIo { pub trait ApplicationIo {
type Surface;
type Executor; type Executor;
fn window(&self) -> Option<SurfaceHandle<Self::Surface>>;
fn create_window(&self) -> SurfaceHandle<Self::Surface>;
fn destroy_window(&self, surface_id: SurfaceId);
fn gpu_executor(&self) -> Option<&Self::Executor> { fn gpu_executor(&self) -> Option<&Self::Executor> {
None None
} }
@ -169,21 +56,8 @@ pub trait ApplicationIo {
} }
impl<T: ApplicationIo> ApplicationIo for &T { impl<T: ApplicationIo> ApplicationIo for &T {
type Surface = T::Surface;
type Executor = T::Executor; type Executor = T::Executor;
fn window(&self) -> Option<SurfaceHandle<Self::Surface>> {
(**self).window()
}
fn create_window(&self) -> SurfaceHandle<T::Surface> {
(**self).create_window()
}
fn destroy_window(&self, surface_id: SurfaceId) {
(**self).destroy_window(surface_id)
}
fn gpu_executor(&self) -> Option<&T::Executor> { fn gpu_executor(&self) -> Option<&T::Executor> {
(**self).gpu_executor() (**self).gpu_executor()
} }
@ -260,12 +134,12 @@ impl GetEditorPreferences for DummyPreferences {
} }
pub struct EditorApi<Io> { pub struct EditorApi<Io> {
/// 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, 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). /// 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<Arc<Io>>, pub application_io: Option<Arc<Io>>,
pub node_graph_message_sender: Box<dyn NodeGraphUpdateSender + Send + Sync>, pub node_graph_message_sender: Box<dyn NodeGraphUpdateSender + Send + Sync>,
/// Editor preferences made available to the graph through the [`WasmEditorApi`]. /// Editor preferences made available to the graph through the `PlatformEditorApi`.
pub editor_preferences: Box<dyn GetEditorPreferences + Send + Sync>, pub editor_preferences: Box<dyn GetEditorPreferences + Send + Sync>,
} }

View File

@ -0,0 +1,28 @@
[package]
name = "graphene-canvas-utils"
version = "0.1.0"
edition = "2024"
description = "graphene canvas utilities"
authors = ["Graphite Authors <contact@graphite.art>"]
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 }

View File

@ -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::*;

View File

@ -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<Arc<CanvasImpl>>);
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<Arc<wgpu::Surface<'static>>>);
#[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::<HtmlCanvasElement>().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::<CanvasRenderingContext2d>()
.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 {}

View File

@ -20,6 +20,5 @@ anyhow = { workspace = true }
wgpu = { workspace = true } wgpu = { workspace = true }
futures = { workspace = true } futures = { workspace = true }
web-sys = { workspace = true } web-sys = { workspace = true }
winit = { workspace = true }
vello = { workspace = true } vello = { workspace = true }
bytemuck = { workspace = true } bytemuck = { workspace = true }

View File

@ -7,13 +7,10 @@ use crate::resample::Resampler;
use crate::shader_runtime::ShaderRuntime; use crate::shader_runtime::ShaderRuntime;
use anyhow::Result; use anyhow::Result;
use core_types::Color; use core_types::Color;
use dyn_any::StaticType;
use futures::lock::Mutex; use futures::lock::Mutex;
use glam::UVec2; use glam::UVec2;
use graphene_application_io::{ApplicationIo, EditorApi, SurfaceHandle, SurfaceId}; use graphene_application_io::{ApplicationIo, EditorApi};
use std::sync::Arc;
use vello::{AaConfig, AaSupport, RenderParams, Renderer, RendererOptions, Scene}; use vello::{AaConfig, AaSupport, RenderParams, Renderer, RendererOptions, Scene};
use wgpu::util::TextureBlitter;
use wgpu::{Origin3d, TextureAspect}; use wgpu::{Origin3d, TextureAspect};
pub use context::Context as WgpuContext; pub use context::Context as WgpuContext;
@ -42,15 +39,6 @@ impl<'a, T: ApplicationIo<Executor = WgpuExecutor>> From<&'a EditorApi<T>> for &
} }
} }
pub type WgpuSurface = Arc<SurfaceHandle<Surface>>;
pub type WgpuWindow = Arc<SurfaceHandle<WindowHandle>>;
pub struct Surface {
pub inner: wgpu::Surface<'static>,
pub target_texture: Mutex<Option<TargetTexture>>,
pub blitter: TextureBlitter,
}
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct TargetTexture { pub struct TargetTexture {
texture: wgpu::Texture, 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<dyn winit::window::Window>;
unsafe impl StaticType for Surface {
type Static = Surface;
}
const VELLO_SURFACE_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm; const VELLO_SURFACE_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
impl WgpuExecutor { impl WgpuExecutor {
@ -160,29 +139,6 @@ impl WgpuExecutor {
pub fn resample_texture(&self, source: &wgpu::Texture, target_size: UVec2, transform: &glam::DAffine2) -> wgpu::Texture { 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) self.resampler.resample(&self.context, source, target_size, transform)
} }
#[cfg(target_family = "wasm")]
pub fn create_surface(&self, canvas: graphene_application_io::WasmSurfaceHandle) -> Result<SurfaceHandle<Surface>> {
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<Window>) -> Result<SurfaceHandle<Surface>> {
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<SurfaceHandle<Surface>> {
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 { impl WgpuExecutor {
@ -213,5 +169,3 @@ impl WgpuExecutor {
}) })
} }
} }
pub type WindowHandle = Arc<SurfaceHandle<Window>>;

View File

@ -1271,7 +1271,7 @@ mod tests {
fn test_async_node() { fn test_async_node() {
let attr = quote!(category("IO")); let attr = quote!(category("IO"));
let input = quote!( let input = quote!(
async fn load_image(api: &WasmEditorApi, #[expose] path: String) -> Table<Raster<CPU>> { async fn load_image(api: &PlatformEditorApi, #[expose] path: String) -> Table<Raster<CPU>> {
// Implementation details... // Implementation details...
} }
); );
@ -1296,7 +1296,7 @@ mod tests {
where_clause: None, where_clause: None,
input: Input { input: Input {
pat_ident: pat_ident("api"), pat_ident: pat_ident("api"),
ty: parse_quote!(&WasmEditorApi), ty: parse_quote!(&PlatformEditorApi),
implementations: Punctuated::new(), implementations: Punctuated::new(),
context_features: vec![], context_features: vec![],
}, },

View File

@ -7,9 +7,9 @@ authors = ["Graphite Authors <contact@graphite.art>"]
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
[features] [features]
default = ["wasm", "wgpu"] default = ["wgpu"]
gpu = [] 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 = [
"wasm-bindgen", "wasm-bindgen",
"wasm-bindgen-futures", "wasm-bindgen-futures",
@ -24,6 +24,7 @@ wasm = [
"vector-nodes/wasm", "vector-nodes/wasm",
"graphene-core/wasm", "graphene-core/wasm",
"graph-craft/wasm", "graph-craft/wasm",
"dep:graphene-canvas-utils"
] ]
image-compare = [] image-compare = []
vello = ["gpu"] vello = ["gpu"]
@ -61,6 +62,9 @@ image = { workspace = true }
base64 = { workspace = true } base64 = { workspace = true }
wgpu = { workspace = true } wgpu = { workspace = true }
# Optional local dependencies
graphene-canvas-utils = { workspace = true, optional = true }
# Optional workspace dependencies # Optional workspace dependencies
wasm-bindgen = { workspace = true, optional = true } wasm-bindgen = { workspace = true, optional = true }
wasm-bindgen-futures = { workspace = true, optional = true } wasm-bindgen-futures = { workspace = true, optional = true }

View File

@ -1,10 +1,9 @@
pub mod any; pub mod any;
pub mod pixel_preview; pub mod pixel_preview;
pub mod platform_application_io;
pub mod render_cache; pub mod render_cache;
pub mod render_node; pub mod render_node;
pub mod text; pub mod text;
#[cfg(feature = "wasm")]
pub mod wasm_application_io;
pub use blending_nodes; pub use blending_nodes;
pub use brush_nodes as brush; pub use brush_nodes as brush;
pub use core_types::*; pub use core_types::*;

View File

@ -2,16 +2,16 @@ use crate::render_node::RenderOutputType;
use core_types::transform::{Footprint, Transform}; use core_types::transform::{Footprint, Transform};
use core_types::{CloneVarArgs, Context, Ctx, ExtractAll, OwnedContextImpl}; use core_types::{CloneVarArgs, Context, Ctx, ExtractAll, OwnedContextImpl};
use glam::{DAffine2, DVec2, UVec2}; use glam::{DAffine2, DVec2, UVec2};
use graph_craft::application_io::PlatformEditorApi;
use graph_craft::document::value::RenderOutput; use graph_craft::document::value::RenderOutput;
use graph_craft::wasm_application_io::WasmEditorApi; use graphene_application_io::ApplicationIo;
use graphene_application_io::{ApplicationIo, ImageTexture};
use rendering::{RenderOutputType as RenderOutputTypeRequest, RenderParams}; use rendering::{RenderOutputType as RenderOutputTypeRequest, RenderParams};
use vector_types::vector::style::RenderMode; use vector_types::vector::style::RenderMode;
#[node_macro::node(category(""))] #[node_macro::node(category(""))]
pub async fn pixel_preview<'a: 'n>( pub async fn pixel_preview<'a: 'n>(
ctx: impl Ctx + ExtractAll + CloneVarArgs + Sync, ctx: impl Ctx + ExtractAll + CloneVarArgs + Sync,
editor_api: &'a WasmEditorApi, editor_api: &'a PlatformEditorApi,
data: impl Node<Context<'static>, Output = RenderOutput> + Send + Sync, data: impl Node<Context<'static>, Output = RenderOutput> + Send + Sync,
) -> RenderOutput { ) -> RenderOutput {
let Some(render_params) = ctx.vararg(0).ok().and_then(|v| v.downcast_ref::<RenderParams>()).cloned() else { let Some(render_params) = ctx.vararg(0).ok().and_then(|v| v.downcast_ref::<RenderParams>()).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 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 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 result
.metadata .metadata

View File

@ -1,6 +1,8 @@
#[cfg(target_family = "wasm")] #[cfg(target_family = "wasm")]
use base64::Engine; use base64::Engine;
#[cfg(target_family = "wasm")] #[cfg(target_family = "wasm")]
use canvas_utils::{Canvas, CanvasHandle};
#[cfg(target_family = "wasm")]
use core_types::WasmNotSend; use core_types::WasmNotSend;
#[cfg(target_family = "wasm")] #[cfg(target_family = "wasm")]
use core_types::math::bbox::Bbox; use core_types::math::bbox::Bbox;
@ -8,10 +10,12 @@ use core_types::table::Table;
#[cfg(target_family = "wasm")] #[cfg(target_family = "wasm")]
use core_types::transform::Footprint; use core_types::transform::Footprint;
use core_types::{Color, Ctx}; use core_types::{Color, Ctx};
pub use graph_craft::application_io::*;
pub use graph_craft::document::value::RenderOutputType; pub use graph_craft::document::value::RenderOutputType;
pub use graph_craft::wasm_application_io::*;
use graphene_application_io::ApplicationIo; use graphene_application_io::ApplicationIo;
#[cfg(target_family = "wasm")] #[cfg(target_family = "wasm")]
pub use graphene_canvas_utils as canvas_utils;
#[cfg(target_family = "wasm")]
use graphic_types::Graphic; use graphic_types::Graphic;
#[cfg(target_family = "wasm")] #[cfg(target_family = "wasm")]
use graphic_types::Vector; use graphic_types::Vector;
@ -22,17 +26,6 @@ use graphic_types::vector_types::gradient::GradientStops;
#[cfg(target_family = "wasm")] #[cfg(target_family = "wasm")]
use rendering::{Render, RenderParams, RenderSvgSegmentList, SvgRender}; use rendering::{Render, RenderParams, RenderSvgSegmentList, SvgRender};
use std::sync::Arc; 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<WasmSurfaceHandle> {
Arc::new(editor.application_io.as_ref().unwrap().create_window())
}
fn parse_headers(headers: &str) -> reqwest::header::HeaderMap { fn parse_headers(headers: &str) -> reqwest::header::HeaderMap {
use reqwest::header::{HeaderMap, HeaderName, HeaderValue}; use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
@ -132,7 +125,7 @@ fn image_to_bytes(_: impl Ctx, image: Table<Raster<CPU>>) -> Vec<u8> {
/// Loads binary from URLs and local asset paths. Returns a transparent placeholder if the resource fails to load, allowing rendering to continue. /// 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"))] #[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 { let Some(api) = editor_resources.application_io.as_ref() else {
return Arc::from(include_bytes!("../../../graph-craft/src/null.png").to_vec()); 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<Raster<CPU>> {
Table::new_from_element(Raster::new_cpu(image)) 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*. /// Renders a view of the input graphic within an area defined by the *Footprint*.
#[cfg(target_family = "wasm")] #[cfg(target_family = "wasm")]
#[node_macro::node(category(""))] #[node_macro::node(category(""))]
@ -182,7 +181,7 @@ async fn rasterize<T: WasmNotSend + 'n>(
)] )]
mut data: Table<T>, mut data: Table<T>,
footprint: Footprint, footprint: Footprint,
surface_handle: Arc<graphene_application_io::SurfaceHandle<HtmlCanvasElement>>, mut canvas: CanvasHandle,
) -> Table<Raster<CPU>> ) -> Table<Raster<CPU>>
where where
Table<T>: Render, Table<T>: Render,
@ -211,11 +210,8 @@ where
render.format_svg(glam::DVec2::ZERO, size); render.format_svg(glam::DVec2::ZERO, size);
let svg_string = render.svg.to_svg_string(); let svg_string = render.svg.to_svg_string();
let canvas = &surface_handle.surface; canvas.set_resolution(resolution);
canvas.set_width(resolution.x); let context = canvas.context();
canvas.set_height(resolution.y);
let context = canvas.get_context("2d").unwrap().unwrap().dyn_into::<CanvasRenderingContext2d>().unwrap();
let preamble = "data:image/svg+xml;base64,"; let preamble = "data:image/svg+xml;base64,";
let mut base64_string = String::with_capacity(preamble.len() + svg_string.len() * 4); let mut base64_string = String::with_capacity(preamble.len() + svg_string.len() * 4);

View File

@ -4,9 +4,9 @@ use core_types::math::bbox::AxisAlignedBbox;
use core_types::transform::{Footprint, RenderQuality, Transform}; use core_types::transform::{Footprint, RenderQuality, Transform};
use core_types::{CloneVarArgs, Context, Ctx, ExtractAll, ExtractAnimationTime, ExtractPointerPosition, ExtractRealTime, OwnedContextImpl}; use core_types::{CloneVarArgs, Context, Ctx, ExtractAll, ExtractAnimationTime, ExtractPointerPosition, ExtractRealTime, OwnedContextImpl};
use glam::{DAffine2, DVec2, IVec2, UVec2}; use glam::{DAffine2, DVec2, IVec2, UVec2};
use graph_craft::application_io::PlatformEditorApi;
use graph_craft::document::value::RenderOutput; use graph_craft::document::value::RenderOutput;
use graph_craft::wasm_application_io::WasmEditorApi; use graphene_application_io::ApplicationIo;
use graphene_application_io::{ApplicationIo, ImageTexture};
use rendering::{RenderOutputType as RenderOutputTypeRequest, RenderParams}; use rendering::{RenderOutputType as RenderOutputTypeRequest, RenderParams};
use std::collections::HashSet; use std::collections::HashSet;
use std::hash::Hash; use std::hash::Hash;
@ -374,7 +374,7 @@ fn flood_fill(start: &TileCoord, tile_set: &HashSet<TileCoord>, visited: &mut Ha
#[node_macro::node(category(""))] #[node_macro::node(category(""))]
pub async fn render_output_cache<'a: 'n>( pub async fn render_output_cache<'a: 'n>(
ctx: impl Ctx + ExtractAll + CloneVarArgs + ExtractRealTime + ExtractAnimationTime + ExtractPointerPosition + Sync, ctx: impl Ctx + ExtractAll + CloneVarArgs + ExtractRealTime + ExtractAnimationTime + ExtractPointerPosition + Sync,
editor_api: &'a WasmEditorApi, editor_api: &'a PlatformEditorApi,
data: impl Node<Context<'static>, Output = RenderOutput> + Send + Sync, data: impl Node<Context<'static>, Output = RenderOutput> + Send + Sync,
#[data] tile_cache: TileCache, #[data] tile_cache: TileCache,
) -> RenderOutput { ) -> 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); let combined_metadata = composite_cached_regions(&all_regions, output_texture.as_ref(), &device_origin_offset, &footprint.transform, exec);
RenderOutput { RenderOutput {
data: RenderOutputType::Texture(ImageTexture { texture: output_texture }), data: RenderOutputType::Texture(output_texture.into()),
metadata: combined_metadata, metadata: combined_metadata,
} }
} }
@ -506,7 +506,7 @@ where
let memory_size = (region_pixel_size.x * region_pixel_size.y) as usize * BYTES_PER_PIXEL; let memory_size = (region_pixel_size.x * region_pixel_size.y) as usize * BYTES_PER_PIXEL;
CachedRegion { CachedRegion {
texture: rendered_texture.texture.as_ref().clone(), texture: rendered_texture.as_ref().clone(),
texture_size: region_pixel_size, texture_size: region_pixel_size,
tiles: region.tiles.clone(), tiles: region.tiles.clone(),
metadata: result.metadata, metadata: result.metadata,
@ -558,7 +558,7 @@ fn composite_cached_regions(
aspect: wgpu::TextureAspect::All, aspect: wgpu::TextureAspect::All,
}, },
wgpu::TexelCopyTextureInfo { wgpu::TexelCopyTextureInfo {
texture: &output_texture, texture: output_texture,
mip_level: 0, mip_level: 0,
origin: wgpu::Origin3d { x: dst_x, y: dst_y, z: 0 }, origin: wgpu::Origin3d { x: dst_x, y: dst_y, z: 0 },
aspect: wgpu::TextureAspect::All, aspect: wgpu::TextureAspect::All,

View File

@ -2,10 +2,10 @@ use core_types::table::Table;
use core_types::transform::{Footprint, Transform}; use core_types::transform::{Footprint, Transform};
use core_types::{CloneVarArgs, ExtractAll, ExtractVarArgs}; use core_types::{CloneVarArgs, ExtractAll, ExtractVarArgs};
use core_types::{Color, Context, Ctx, ExtractFootprint, OwnedContextImpl, WasmNotSend}; use core_types::{Color, Context, Ctx, ExtractFootprint, OwnedContextImpl, WasmNotSend};
pub use graph_craft::application_io::*;
use graph_craft::document::value::RenderOutput; use graph_craft::document::value::RenderOutput;
pub use graph_craft::document::value::RenderOutputType; pub use graph_craft::document::value::RenderOutputType;
pub use graph_craft::wasm_application_io::*; use graphene_application_io::{ApplicationIo, ExportFormat, RenderConfig};
use graphene_application_io::{ApplicationIo, ExportFormat, ImageTexture, RenderConfig};
use graphic_types::raster_types::Image; use graphic_types::raster_types::Image;
use graphic_types::raster_types::{CPU, Raster}; use graphic_types::raster_types::{CPU, Raster};
use graphic_types::{Artboard, Graphic, Vector}; use graphic_types::{Artboard, Graphic, Vector};
@ -124,7 +124,7 @@ async fn create_context<'a: 'n>(
} }
#[node_macro::node(category(""))] #[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 footprint = ctx.footprint();
let render_params = ctx let render_params = ctx
.vararg(0) .vararg(0)
@ -202,7 +202,7 @@ async fn render<'a: 'n>(ctx: impl Ctx + ExtractFootprint + ExtractVarArgs, edito
.expect("Failed to render Vello scene"), .expect("Failed to render Vello scene"),
); );
RenderOutputType::Texture(ImageTexture { texture }) RenderOutputType::Texture(texture.into())
} }
_ => unreachable!("Render node did not receive its requested data type"), _ => unreachable!("Render node did not receive its requested data type"),
}; };

View File

@ -1,5 +1,5 @@
use core_types::{Ctx, table::Table}; use core_types::{Ctx, table::Table};
use graph_craft::wasm_application_io::WasmEditorApi; use graph_craft::application_io::PlatformEditorApi;
use graphic_types::Vector; use graphic_types::Vector;
pub use text_nodes::*; pub use text_nodes::*;
@ -9,7 +9,7 @@ fn text<'i: 'n>(
_: impl Ctx, _: impl Ctx,
/// The Graphite editor's source for global font resources. /// The Graphite editor's source for global font resources.
#[scope("editor-api")] #[scope("editor-api")]
editor_resources: &'i WasmEditorApi, editor_resources: &'i PlatformEditorApi,
/// The text content to be drawn. /// The text content to be drawn.
#[widget(ParsedWidgetOverride::Custom = "text_area")] #[widget(ParsedWidgetOverride::Custom = "text_area")]
#[default("Lorem ipsum")] #[default("Lorem ipsum")]