Skip to content

Commit cd6f37f

Browse files
committed
Work on fixing rendering for wasm+vello
1 parent 6e66c79 commit cd6f37f

File tree

7 files changed

+229
-40
lines changed

7 files changed

+229
-40
lines changed

desktop/src/window.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use winit::window::{Window as WinitWindow, WindowAttributes};
44

55
use crate::consts::APP_NAME;
66
use crate::event::AppEventScheduler;
7+
#[cfg(target_os = "macos")]
78
use crate::window::mac::NativeWindowImpl;
89
use crate::wrapper::messages::MenuItem;
910

@@ -37,7 +38,7 @@ pub(crate) struct Window {
3738

3839
impl Window {
3940
pub(crate) fn init() {
40-
NativeWindowImpl::init();
41+
native::NativeWindowImpl::init();
4142
}
4243

4344
pub(crate) fn new(event_loop: &dyn ActiveEventLoop, app_event_scheduler: AppEventScheduler) -> Self {

editor/src/node_graph_executor.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -421,8 +421,8 @@ impl NodeGraphExecutor {
421421
let matrix = format_transform_matrix(frame.transform);
422422
let transform = if matrix.is_empty() { String::new() } else { format!(" transform=\"{matrix}\"") };
423423
let svg = format!(
424-
r#"<svg><foreignObject width="{}" height="{}"{transform}><div data-canvas-placeholder="{}"></div></foreignObject></svg>"#,
425-
frame.resolution.x, frame.resolution.y, frame.surface_id.0
424+
r#"<svg><foreignObject width="{}" height="{}"{transform}><div data-canvas-placeholder="{}" data-physical-width="{}" data-physical-height="{}"></div></foreignObject></svg>"#,
425+
frame.resolution.x, frame.resolution.y, frame.surface_id.0, frame.physical_resolution.x, frame.physical_resolution.y
426426
);
427427
self.last_svg_canvas = Some(frame);
428428
responses.add(FrontendMessage::UpdateDocumentArtwork { svg });

editor/src/node_graph_executor/runtime.rs

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@ pub struct NodeRuntime {
5555
/// The current renders of the thumbnails for layer nodes.
5656
thumbnail_renders: HashMap<NodeId, Vec<SvgSegment>>,
5757
vector_modify: HashMap<NodeId, Vector>,
58+
59+
/// Cached surface for WASM viewport rendering (reused across frames)
60+
#[cfg(all(target_family = "wasm", feature = "gpu"))]
61+
wasm_viewport_surface: Option<wgpu_executor::WgpuSurface>,
5862
}
5963

6064
/// Messages passed from the editor thread to the node runtime thread.
@@ -131,6 +135,8 @@ impl NodeRuntime {
131135
thumbnail_renders: Default::default(),
132136
vector_modify: Default::default(),
133137
inspect_state: None,
138+
#[cfg(all(target_family = "wasm", feature = "gpu"))]
139+
wasm_viewport_surface: None,
134140
}
135141
}
136142

@@ -259,6 +265,83 @@ impl NodeRuntime {
259265
None,
260266
)
261267
}
268+
#[cfg(all(target_family = "wasm", feature = "gpu"))]
269+
Ok(TaggedValue::RenderOutput(RenderOutput {
270+
data: RenderOutputType::Texture(image_texture),
271+
metadata,
272+
})) if !render_config.for_export => {
273+
// On WASM, for viewport rendering, blit the texture to a surface and return a CanvasFrame
274+
let app_io = self.editor_api.application_io.as_ref().unwrap();
275+
let executor = app_io.gpu_executor().expect("GPU executor should be available when we receive a texture");
276+
277+
// Get or create the cached surface
278+
if self.wasm_viewport_surface.is_none() {
279+
let surface_handle = app_io.create_window();
280+
let wasm_surface = executor
281+
.create_surface(graphene_std::wasm_application_io::WasmSurfaceHandle {
282+
surface: surface_handle.surface.clone(),
283+
window_id: surface_handle.window_id,
284+
})
285+
.expect("Failed to create surface");
286+
self.wasm_viewport_surface = Some(Arc::new(wasm_surface));
287+
}
288+
289+
let surface = self.wasm_viewport_surface.as_ref().unwrap();
290+
291+
// Use logical resolution for CSS sizing, physical resolution for the actual surface/texture
292+
let logical_resolution = render_config.viewport.resolution;
293+
let physical_resolution = (logical_resolution.as_dvec2() * render_config.scale).as_uvec2();
294+
295+
// Blit the texture to the surface
296+
let mut encoder = executor.context.device.create_command_encoder(&vello::wgpu::CommandEncoderDescriptor {
297+
label: Some("Texture to Surface Blit"),
298+
});
299+
300+
// Configure the surface at physical resolution (for HiDPI displays)
301+
let surface_inner = &surface.surface.inner;
302+
let surface_caps = surface_inner.get_capabilities(&executor.context.adapter);
303+
surface_inner.configure(
304+
&executor.context.device,
305+
&vello::wgpu::SurfaceConfiguration {
306+
usage: vello::wgpu::TextureUsages::RENDER_ATTACHMENT | vello::wgpu::TextureUsages::COPY_DST,
307+
format: vello::wgpu::TextureFormat::Rgba8Unorm,
308+
width: physical_resolution.x,
309+
height: physical_resolution.y,
310+
present_mode: surface_caps.present_modes[0],
311+
alpha_mode: vello::wgpu::CompositeAlphaMode::Opaque,
312+
view_formats: vec![],
313+
desired_maximum_frame_latency: 2,
314+
},
315+
);
316+
317+
let surface_texture = surface_inner.get_current_texture().expect("Failed to get surface texture");
318+
319+
// Blit the rendered texture to the surface
320+
surface.surface.blitter.copy(
321+
&executor.context.device,
322+
&mut encoder,
323+
&image_texture.texture.create_view(&vello::wgpu::TextureViewDescriptor::default()),
324+
&surface_texture.texture.create_view(&vello::wgpu::TextureViewDescriptor::default()),
325+
);
326+
327+
executor.context.queue.submit([encoder.finish()]);
328+
surface_texture.present();
329+
330+
let frame = graphene_std::application_io::SurfaceFrame {
331+
surface_id: surface.window_id,
332+
resolution: logical_resolution,
333+
physical_resolution,
334+
transform: glam::DAffine2::IDENTITY,
335+
};
336+
337+
(
338+
Ok(TaggedValue::RenderOutput(RenderOutput {
339+
data: RenderOutputType::CanvasFrame(frame),
340+
metadata,
341+
})),
342+
None,
343+
)
344+
}
262345
Ok(TaggedValue::RenderOutput(RenderOutput {
263346
data: RenderOutputType::Texture(texture),
264347
metadata,

frontend/src/components/panels/Document.svelte

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -203,17 +203,28 @@
203203
// eslint-disable-next-line @typescript-eslint/no-explicit-any
204204
let canvas = (window as any).imageCanvases[canvasName];
205205
206-
if (canvasName !== "0" && canvas.parentElement) {
207-
var newCanvas = window.document.createElement("canvas");
208-
var context = newCanvas.getContext("2d");
206+
// Get logical dimensions from foreignObject parent (set by backend)
207+
const foreignObject = placeholder.parentElement;
208+
if (!foreignObject) return;
209+
const logicalWidth = parseInt(foreignObject.getAttribute("width") || "0");
210+
const logicalHeight = parseInt(foreignObject.getAttribute("height") || "0");
209211
210-
newCanvas.width = canvas.width;
211-
newCanvas.height = canvas.height;
212+
// if (canvasName !== "0" && canvas.parentElement) {
213+
// console.log("test");
214+
// var newCanvas = window.document.createElement("canvas");
215+
// var context = newCanvas.getContext("2d");
212216
213-
context?.drawImage(canvas, 0, 0);
217+
// newCanvas.width = canvas.width;
218+
// newCanvas.height = canvas.height;
214219
215-
canvas = newCanvas;
216-
}
220+
// context?.drawImage(canvas, 0, 0);
221+
222+
// canvas = newCanvas;
223+
// }
224+
225+
// Set CSS size to logical resolution (for correct display size)
226+
canvas.style.width = `${logicalWidth}px`;
227+
canvas.style.height = `${logicalHeight}px`;
217228
218229
placeholder.replaceWith(canvas);
219230
});

node-graph/libraries/application-io/src/lib.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@ impl std::fmt::Display for SurfaceId {
2323
#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
2424
pub struct SurfaceFrame {
2525
pub surface_id: SurfaceId,
26+
/// Logical resolution in CSS pixels (used for foreignObject dimensions)
2627
pub resolution: UVec2,
28+
/// Physical resolution in device pixels (used for actual canvas/texture dimensions)
29+
pub physical_resolution: UVec2,
2730
pub transform: DAffine2,
2831
}
2932

@@ -101,10 +104,12 @@ impl Size for ImageTexture {
101104

102105
impl<S: Size> From<SurfaceHandleFrame<S>> for SurfaceFrame {
103106
fn from(x: SurfaceHandleFrame<S>) -> Self {
107+
let size = x.surface_handle.surface.size();
104108
Self {
105109
surface_id: x.surface_handle.window_id,
106110
transform: x.transform,
107-
resolution: x.surface_handle.surface.size(),
111+
resolution: size,
112+
physical_resolution: size,
108113
}
109114
}
110115
}

node-graph/nodes/gstd/src/render_node.rs

Lines changed: 6 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use core_types::{Color, Context, Ctx, ExtractFootprint, OwnedContextImpl, WasmNo
55
use graph_craft::document::value::RenderOutput;
66
pub use graph_craft::document::value::RenderOutputType;
77
pub use graph_craft::wasm_application_io::*;
8-
use graphene_application_io::{ApplicationIo, ExportFormat, ImageTexture, RenderConfig, SurfaceFrame};
8+
use graphene_application_io::{ApplicationIo, ExportFormat, ImageTexture, RenderConfig};
99
use graphic_types::Artboard;
1010
use graphic_types::Graphic;
1111
use graphic_types::Vector;
@@ -124,7 +124,6 @@ async fn render<'a: 'n>(
124124
ctx: impl Ctx + ExtractFootprint + ExtractVarArgs,
125125
editor_api: &'a WasmEditorApi,
126126
data: RenderIntermediate,
127-
_surface_handle: impl Node<Context<'static>, Output = Option<wgpu_executor::WgpuSurface>>,
128127
) -> RenderOutput {
129128
let footprint = ctx.footprint();
130129
let render_params = ctx
@@ -171,14 +170,8 @@ async fn render<'a: 'n>(
171170
};
172171
let (child, context) = Arc::as_ref(vello_data);
173172

174-
let surface_handle = if cfg!(all(feature = "vello", target_family = "wasm")) {
175-
_surface_handle.eval(None).await
176-
} else {
177-
None
178-
};
179-
180-
// When rendering to a surface, we do not want to apply the scale
181-
let scale = if surface_handle.is_none() { render_params.scale } else { 1. };
173+
// Always apply scale when rendering to texture
174+
let scale = render_params.scale;
182175

183176
let scale_transform = glam::DAffine2::from_scale(glam::DVec2::splat(scale));
184177
let footprint_transform = scale_transform * footprint.transform;
@@ -204,25 +197,10 @@ async fn render<'a: 'n>(
204197
background = Color::WHITE;
205198
}
206199

207-
if let Some(surface_handle) = surface_handle {
208-
exec.render_vello_scene(&scene, &surface_handle, resolution, context, background)
209-
.await
210-
.expect("Failed to render Vello scene");
211-
212-
let frame = SurfaceFrame {
213-
surface_id: surface_handle.window_id,
214-
// TODO: Find a cleaner way to get the unscaled resolution here.
215-
// This is done because the surface frame (canvas) is in logical pixels, not physical pixels.
216-
resolution,
217-
transform: glam::DAffine2::IDENTITY,
218-
};
200+
// Always render to texture (unified path for both WASM and desktop)
201+
let texture = exec.render_vello_scene_to_texture(&scene, resolution, context, background).await.expect("Failed to render Vello scene");
219202

220-
RenderOutputType::CanvasFrame(frame)
221-
} else {
222-
let texture = exec.render_vello_scene_to_texture(&scene, resolution, context, background).await.expect("Failed to render Vello scene");
223-
224-
RenderOutputType::Texture(ImageTexture { texture })
225-
}
203+
RenderOutputType::Texture(ImageTexture { texture })
226204
}
227205
_ => unreachable!("Render node did not receive its requested data type"),
228206
};
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
use crate::WgpuExecutor;
2+
use core_types::Color;
3+
use core_types::bounds::{BoundingBox, RenderBoundingBox};
4+
use core_types::ops::Convert;
5+
use core_types::table::Table;
6+
use core_types::transform::Footprint;
7+
use glam::{DAffine2, DVec2, UVec2};
8+
use graphic_types::raster_types::{GPU, Raster};
9+
use graphic_types::vector_types::GradientStops;
10+
use graphic_types::{Artboard, Graphic, Vector};
11+
use rendering::{Render, RenderOutputType, RenderParams};
12+
use wgpu::{CommandEncoderDescriptor, TextureFormat, TextureViewDescriptor};
13+
14+
macro_rules! impl_convert {
15+
($ty:ty) => {
16+
/// Converts Table<T> to Table<Raster<GPU>> by rendering each item to Vello scene and then to texture
17+
impl<'i> Convert<Table<Raster<GPU>>, &'i WgpuExecutor> for Table<$ty> {
18+
async fn convert(self, footprint: Footprint, executor: &'i WgpuExecutor) -> Table<Raster<GPU>> {
19+
// Create render parameters for Vello rendering
20+
let render_params = RenderParams {
21+
render_mode: graphic_types::vector_types::vector::style::RenderMode::Normal,
22+
hide_artboards: false,
23+
for_export: false,
24+
render_output_type: RenderOutputType::Vello,
25+
footprint,
26+
..Default::default()
27+
};
28+
29+
let vector = &self;
30+
let bounding_box = vector.bounding_box(DAffine2::IDENTITY, true);
31+
// TODO: Add cases for infinite bounding boxes
32+
let RenderBoundingBox::Rectangle(rect) = bounding_box else {
33+
panic!("did not find valid bounding box")
34+
};
35+
36+
// Create a Vello scene for this vector
37+
let mut scene = vello::Scene::new();
38+
let mut context = crate::RenderContext::default();
39+
40+
let viewport_bounds = footprint.viewport_bounds_in_local_space();
41+
42+
let image_bounds = core_types::math::bbox::AxisAlignedBbox { start: rect[0], end: rect[1] };
43+
let intersection = viewport_bounds.intersect(&image_bounds);
44+
45+
let size = intersection.size();
46+
47+
let offset = (intersection.start - image_bounds.start).max(DVec2::ZERO) + image_bounds.start;
48+
49+
// If the image would not be visible, return an empty image
50+
if size.x <= 0. || size.y <= 0. {
51+
return Table::new();
52+
}
53+
54+
let scale = footprint.scale();
55+
let width = (size.x * scale.x) as u32;
56+
let height = (size.y * scale.y) as u32;
57+
58+
// Render the scene to a GPU texture
59+
let resolution = UVec2::new(width, height);
60+
let background = core_types::Color::TRANSPARENT;
61+
62+
let render_transform = DAffine2::from_scale(scale) * DAffine2::from_translation(-offset);
63+
// Render the vector to the Vello scene with the row's transform
64+
vector.render_to_vello(&mut scene, render_transform, &mut context, &render_params);
65+
66+
// Use async rendering to get the texture
67+
let texture = executor
68+
.render_vello_scene_to_texture(&scene, resolution, &context, background)
69+
.await
70+
.expect("Failed to render Vello scene to texture");
71+
72+
let device = &executor.context.device;
73+
let queue = &executor.context.queue;
74+
let mut encoder = device.create_command_encoder(&CommandEncoderDescriptor::default());
75+
let blitter = wgpu::util::TextureBlitter::new(device, TextureFormat::Rgba8UnormSrgb);
76+
let view = texture.create_view(&TextureViewDescriptor::default());
77+
let new_texture = device.create_texture(&wgpu::wgt::TextureDescriptor {
78+
label: None,
79+
size: wgpu::Extent3d {
80+
width: texture.width(),
81+
height: texture.height(),
82+
depth_or_array_layers: 1,
83+
},
84+
mip_level_count: 1,
85+
sample_count: 1,
86+
dimension: wgpu::TextureDimension::D2,
87+
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::RENDER_ATTACHMENT,
88+
format: TextureFormat::Rgba8UnormSrgb,
89+
view_formats: &[],
90+
});
91+
let new_view = new_texture.create_view(&TextureViewDescriptor::default());
92+
93+
blitter.copy(device, &mut encoder, &view, &new_view);
94+
encoder.on_submitted_work_done(move || texture.destroy());
95+
let command_buffer = encoder.finish();
96+
queue.submit([command_buffer]);
97+
98+
let mut table = Table::new_from_element(Raster::new_gpu(new_texture));
99+
*(table.get_mut(0).as_mut().unwrap().transform) = DAffine2::from_translation(offset) * DAffine2::from_scale(size);
100+
// texture.destroy();
101+
table
102+
}
103+
}
104+
};
105+
}
106+
107+
impl_convert!(Vector);
108+
impl_convert!(Graphic);
109+
impl_convert!(Artboard);
110+
impl_convert!(GradientStops);
111+
impl_convert!(Color);

0 commit comments

Comments
 (0)