Render artwork at correct resolution when using vello on wasm (#3416)

* Work on fixing rendering for wasm+vello

* Render vello canvas in wasm at the correct resolution

* Cleanup unused surface rendering code

* Remove vector to raster conversion

* Remove desktop changes

* Revert window.rs changes

* Don't round logical coordinates

* Fix desktop compilation + don't round logical coordinates for svg rendering

* Further cleanup

* Compute logical size from acutal physical sizes
This commit is contained in:
Dennis Kobert
2025-11-24 15:23:27 +01:00
committed by GitHub
parent 6e66c79392
commit a932eaedcf
13 changed files with 197 additions and 216 deletions

View File

@@ -882,70 +882,6 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
properties: None,
},
#[cfg(feature = "gpu")]
DocumentNodeDefinition {
identifier: "Create GPU Surface",
category: "Debug: GPU",
node_template: NodeTemplate {
document_node: DocumentNode {
implementation: DocumentNodeImplementation::Network(NodeNetwork {
exports: vec![NodeInput::node(NodeId(1), 0)],
nodes: [
DocumentNode {
inputs: vec![NodeInput::scope("editor-api")],
implementation: DocumentNodeImplementation::ProtoNode(wgpu_executor::create_gpu_surface::IDENTIFIER),
..Default::default()
},
DocumentNode {
inputs: vec![NodeInput::node(NodeId(0), 0)],
implementation: DocumentNodeImplementation::ProtoNode(memo::memo::IDENTIFIER),
..Default::default()
},
]
.into_iter()
.enumerate()
.map(|(id, node)| (NodeId(id as u64), node))
.collect(),
..Default::default()
}),
..Default::default()
},
persistent_node_metadata: DocumentNodePersistentMetadata {
output_names: vec!["GPU Surface".to_string()],
network_metadata: Some(NodeNetworkMetadata {
persistent_metadata: NodeNetworkPersistentMetadata {
node_metadata: [
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
display_name: "Create GPU Surface".to_string(),
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(0, 0)),
..Default::default()
},
..Default::default()
},
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
display_name: "Cache".to_string(),
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(7, 0)),
..Default::default()
},
..Default::default()
},
]
.into_iter()
.enumerate()
.map(|(id, node)| (NodeId(id as u64), node))
.collect(),
..Default::default()
},
..Default::default()
}),
..Default::default()
},
},
description: Cow::Borrowed("TODO"),
properties: None,
},
#[cfg(feature = "gpu")]
DocumentNodeDefinition {
identifier: "Upload Texture",
category: "Debug: GPU",

View File

@@ -21,6 +21,7 @@ use crate::messages::prelude::*;
use crate::messages::tool::common_functionality::graph_modification_utils;
use crate::messages::tool::common_functionality::utility_functions::make_path_editable_is_allowed;
use crate::messages::tool::utility_types::{HintData, HintGroup, ToolType};
use crate::messages::viewport::ToPhysical;
use crate::node_graph_executor::{ExportConfig, NodeGraphExecutor};
use derivative::*;
use glam::{DAffine2, DVec2};
@@ -364,12 +365,13 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
let node_to_inspect = self.node_to_inspect();
let scale = viewport.scale();
let resolution = viewport.size().into_dvec2().round().as_uvec2();
// Use exact physical dimensions from browser (via ResizeObserver's devicePixelContentBoxSize)
let physical_resolution = viewport.size().to_physical().into_dvec2().round().as_uvec2();
if let Ok(message) = self.executor.submit_node_graph_evaluation(
self.documents.get_mut(document_id).expect("Tried to render non-existent document"),
*document_id,
resolution,
physical_resolution,
scale,
timing_information,
node_to_inspect,
@@ -970,11 +972,12 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
};
let scale = viewport.scale();
let resolution = viewport.size().into_dvec2().round().as_uvec2();
// Use exact physical dimensions from browser (via ResizeObserver's devicePixelContentBoxSize)
let physical_resolution = viewport.size().to_physical().into_dvec2().round().as_uvec2();
let result = self
.executor
.submit_node_graph_evaluation(document, document_id, resolution, scale, timing_information, node_to_inspect, ignore_hash);
.submit_node_graph_evaluation(document, document_id, physical_resolution, scale, timing_information, node_to_inspect, ignore_hash);
match result {
Err(description) => {

View File

@@ -421,8 +421,8 @@ impl NodeGraphExecutor {
let matrix = format_transform_matrix(frame.transform);
let transform = if matrix.is_empty() { String::new() } else { format!(" transform=\"{matrix}\"") };
let svg = format!(
r#"<svg><foreignObject width="{}" height="{}"{transform}><div data-canvas-placeholder="{}"></div></foreignObject></svg>"#,
frame.resolution.x, frame.resolution.y, frame.surface_id.0
r#"<svg><foreignObject width="{}" height="{}"{transform}><div data-canvas-placeholder="{}" data-is-viewport="true"></div></foreignObject></svg>"#,
frame.resolution.x, frame.resolution.y, frame.surface_id.0,
);
self.last_svg_canvas = Some(frame);
responses.add(FrontendMessage::UpdateDocumentArtwork { svg });

View File

@@ -55,6 +55,10 @@ pub struct NodeRuntime {
/// The current renders of the thumbnails for layer nodes.
thumbnail_renders: HashMap<NodeId, Vec<SvgSegment>>,
vector_modify: HashMap<NodeId, Vector>,
/// Cached surface for WASM viewport rendering (reused across frames)
#[cfg(all(target_family = "wasm", feature = "gpu"))]
wasm_viewport_surface: Option<wgpu_executor::WgpuSurface>,
}
/// Messages passed from the editor thread to the node runtime thread.
@@ -131,6 +135,8 @@ impl NodeRuntime {
thumbnail_renders: Default::default(),
vector_modify: Default::default(),
inspect_state: None,
#[cfg(all(target_family = "wasm", feature = "gpu"))]
wasm_viewport_surface: None,
}
}
@@ -259,6 +265,82 @@ impl NodeRuntime {
None,
)
}
#[cfg(all(target_family = "wasm", feature = "gpu"))]
Ok(TaggedValue::RenderOutput(RenderOutput {
data: RenderOutputType::Texture(image_texture),
metadata,
})) if !render_config.for_export => {
// On WASM, for viewport rendering, blit the texture to a surface and return a CanvasFrame
let app_io = self.editor_api.application_io.as_ref().unwrap();
let executor = app_io.gpu_executor().expect("GPU executor should be available when we receive a texture");
// Get or create the cached surface
if self.wasm_viewport_surface.is_none() {
let surface_handle = app_io.create_window();
let wasm_surface = executor
.create_surface(graphene_std::wasm_application_io::WasmSurfaceHandle {
surface: surface_handle.surface.clone(),
window_id: surface_handle.window_id,
})
.expect("Failed to create surface");
self.wasm_viewport_surface = Some(Arc::new(wasm_surface));
}
let surface = self.wasm_viewport_surface.as_ref().unwrap();
// Use logical resolution for CSS sizing, physical resolution for the actual surface/texture
let physical_resolution = render_config.viewport.resolution;
let logical_resolution = physical_resolution.as_dvec2() / render_config.scale;
// Blit the texture to the surface
let mut encoder = executor.context.device.create_command_encoder(&vello::wgpu::CommandEncoderDescriptor {
label: Some("Texture to Surface Blit"),
});
// Configure the surface at physical resolution (for HiDPI displays)
let surface_inner = &surface.surface.inner;
let surface_caps = surface_inner.get_capabilities(&executor.context.adapter);
surface_inner.configure(
&executor.context.device,
&vello::wgpu::SurfaceConfiguration {
usage: vello::wgpu::TextureUsages::RENDER_ATTACHMENT | vello::wgpu::TextureUsages::COPY_DST,
format: vello::wgpu::TextureFormat::Rgba8Unorm,
width: physical_resolution.x,
height: physical_resolution.y,
present_mode: surface_caps.present_modes[0],
alpha_mode: vello::wgpu::CompositeAlphaMode::Opaque,
view_formats: vec![],
desired_maximum_frame_latency: 2,
},
);
let surface_texture = surface_inner.get_current_texture().expect("Failed to get surface texture");
// Blit the rendered texture to the surface
surface.surface.blitter.copy(
&executor.context.device,
&mut encoder,
&image_texture.texture.create_view(&vello::wgpu::TextureViewDescriptor::default()),
&surface_texture.texture.create_view(&vello::wgpu::TextureViewDescriptor::default()),
);
executor.context.queue.submit([encoder.finish()]);
surface_texture.present();
let frame = graphene_std::application_io::SurfaceFrame {
surface_id: surface.window_id,
resolution: logical_resolution,
transform: glam::DAffine2::IDENTITY,
};
(
Ok(TaggedValue::RenderOutput(RenderOutput {
data: RenderOutputType::CanvasFrame(frame),
metadata,
})),
None,
)
}
Ok(TaggedValue::RenderOutput(RenderOutput {
data: RenderOutputType::Texture(texture),
metadata,