mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-19 02:48:12 +08:00
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:
@@ -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",
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user