From cd6f37f65cc6f20adfc3ba74447f3173d80f8a8d Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Sun, 23 Nov 2025 18:14:14 +0100 Subject: [PATCH 01/11] Work on fixing rendering for wasm+vello --- desktop/src/window.rs | 3 +- editor/src/node_graph_executor.rs | 4 +- editor/src/node_graph_executor/runtime.rs | 83 +++++++++++++ .../src/components/panels/Document.svelte | 27 +++-- .../libraries/application-io/src/lib.rs | 7 +- node-graph/nodes/gstd/src/render_node.rs | 34 +----- .../wgpu-executor/src/vector_to_raster.rs | 111 ++++++++++++++++++ 7 files changed, 229 insertions(+), 40 deletions(-) create mode 100644 node-graph/wgpu-executor/src/vector_to_raster.rs diff --git a/desktop/src/window.rs b/desktop/src/window.rs index 6a6b2dce71..7a250e6b18 100644 --- a/desktop/src/window.rs +++ b/desktop/src/window.rs @@ -4,6 +4,7 @@ use winit::window::{Window as WinitWindow, WindowAttributes}; use crate::consts::APP_NAME; use crate::event::AppEventScheduler; +#[cfg(target_os = "macos")] use crate::window::mac::NativeWindowImpl; use crate::wrapper::messages::MenuItem; @@ -37,7 +38,7 @@ pub(crate) struct Window { impl Window { pub(crate) fn init() { - NativeWindowImpl::init(); + native::NativeWindowImpl::init(); } pub(crate) fn new(event_loop: &dyn ActiveEventLoop, app_event_scheduler: AppEventScheduler) -> Self { diff --git a/editor/src/node_graph_executor.rs b/editor/src/node_graph_executor.rs index bc2be65f44..02adee8876 100644 --- a/editor/src/node_graph_executor.rs +++ b/editor/src/node_graph_executor.rs @@ -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#"
"#, - frame.resolution.x, frame.resolution.y, frame.surface_id.0 + r#"
"#, + frame.resolution.x, frame.resolution.y, frame.surface_id.0, frame.physical_resolution.x, frame.physical_resolution.y ); self.last_svg_canvas = Some(frame); responses.add(FrontendMessage::UpdateDocumentArtwork { svg }); diff --git a/editor/src/node_graph_executor/runtime.rs b/editor/src/node_graph_executor/runtime.rs index 8f306dc46f..c27fb3d51a 100644 --- a/editor/src/node_graph_executor/runtime.rs +++ b/editor/src/node_graph_executor/runtime.rs @@ -55,6 +55,10 @@ pub struct NodeRuntime { /// The current renders of the thumbnails for layer nodes. thumbnail_renders: HashMap>, vector_modify: HashMap, + + /// Cached surface for WASM viewport rendering (reused across frames) + #[cfg(all(target_family = "wasm", feature = "gpu"))] + wasm_viewport_surface: Option, } /// 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,83 @@ 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 logical_resolution = render_config.viewport.resolution; + let physical_resolution = (logical_resolution.as_dvec2() * render_config.scale).as_uvec2(); + + // 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, + physical_resolution, + transform: glam::DAffine2::IDENTITY, + }; + + ( + Ok(TaggedValue::RenderOutput(RenderOutput { + data: RenderOutputType::CanvasFrame(frame), + metadata, + })), + None, + ) + } Ok(TaggedValue::RenderOutput(RenderOutput { data: RenderOutputType::Texture(texture), metadata, diff --git a/frontend/src/components/panels/Document.svelte b/frontend/src/components/panels/Document.svelte index 3f2a96ff65..8034435643 100644 --- a/frontend/src/components/panels/Document.svelte +++ b/frontend/src/components/panels/Document.svelte @@ -203,17 +203,28 @@ // eslint-disable-next-line @typescript-eslint/no-explicit-any let canvas = (window as any).imageCanvases[canvasName]; - if (canvasName !== "0" && canvas.parentElement) { - var newCanvas = window.document.createElement("canvas"); - var context = newCanvas.getContext("2d"); + // Get logical dimensions from foreignObject parent (set by backend) + const foreignObject = placeholder.parentElement; + if (!foreignObject) return; + const logicalWidth = parseInt(foreignObject.getAttribute("width") || "0"); + const logicalHeight = parseInt(foreignObject.getAttribute("height") || "0"); - newCanvas.width = canvas.width; - newCanvas.height = canvas.height; + // if (canvasName !== "0" && canvas.parentElement) { + // console.log("test"); + // var newCanvas = window.document.createElement("canvas"); + // var context = newCanvas.getContext("2d"); - context?.drawImage(canvas, 0, 0); + // newCanvas.width = canvas.width; + // newCanvas.height = canvas.height; - canvas = newCanvas; - } + // context?.drawImage(canvas, 0, 0); + + // canvas = newCanvas; + // } + + // Set CSS size to logical resolution (for correct display size) + canvas.style.width = `${logicalWidth}px`; + canvas.style.height = `${logicalHeight}px`; placeholder.replaceWith(canvas); }); diff --git a/node-graph/libraries/application-io/src/lib.rs b/node-graph/libraries/application-io/src/lib.rs index 79d9ed1015..9b54544061 100644 --- a/node-graph/libraries/application-io/src/lib.rs +++ b/node-graph/libraries/application-io/src/lib.rs @@ -23,7 +23,10 @@ impl std::fmt::Display for SurfaceId { #[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: UVec2, + /// Physical resolution in device pixels (used for actual canvas/texture dimensions) + pub physical_resolution: UVec2, pub transform: DAffine2, } @@ -101,10 +104,12 @@ impl Size for ImageTexture { impl From> for SurfaceFrame { fn from(x: SurfaceHandleFrame) -> Self { + let size = x.surface_handle.surface.size(); Self { surface_id: x.surface_handle.window_id, transform: x.transform, - resolution: x.surface_handle.surface.size(), + resolution: size, + physical_resolution: size, } } } diff --git a/node-graph/nodes/gstd/src/render_node.rs b/node-graph/nodes/gstd/src/render_node.rs index c5d8bc9bea..43e5d03e46 100644 --- a/node-graph/nodes/gstd/src/render_node.rs +++ b/node-graph/nodes/gstd/src/render_node.rs @@ -5,7 +5,7 @@ use core_types::{Color, Context, Ctx, ExtractFootprint, OwnedContextImpl, WasmNo use graph_craft::document::value::RenderOutput; pub use graph_craft::document::value::RenderOutputType; pub use graph_craft::wasm_application_io::*; -use graphene_application_io::{ApplicationIo, ExportFormat, ImageTexture, RenderConfig, SurfaceFrame}; +use graphene_application_io::{ApplicationIo, ExportFormat, ImageTexture, RenderConfig}; use graphic_types::Artboard; use graphic_types::Graphic; use graphic_types::Vector; @@ -124,7 +124,6 @@ async fn render<'a: 'n>( ctx: impl Ctx + ExtractFootprint + ExtractVarArgs, editor_api: &'a WasmEditorApi, data: RenderIntermediate, - _surface_handle: impl Node, Output = Option>, ) -> RenderOutput { let footprint = ctx.footprint(); let render_params = ctx @@ -171,14 +170,8 @@ async fn render<'a: 'n>( }; let (child, context) = Arc::as_ref(vello_data); - let surface_handle = if cfg!(all(feature = "vello", target_family = "wasm")) { - _surface_handle.eval(None).await - } else { - None - }; - - // When rendering to a surface, we do not want to apply the scale - let scale = if surface_handle.is_none() { render_params.scale } else { 1. }; + // Always apply scale when rendering to texture + let scale = render_params.scale; let scale_transform = glam::DAffine2::from_scale(glam::DVec2::splat(scale)); let footprint_transform = scale_transform * footprint.transform; @@ -204,25 +197,10 @@ async fn render<'a: 'n>( background = Color::WHITE; } - if let Some(surface_handle) = surface_handle { - exec.render_vello_scene(&scene, &surface_handle, resolution, context, background) - .await - .expect("Failed to render Vello scene"); - - let frame = SurfaceFrame { - surface_id: surface_handle.window_id, - // TODO: Find a cleaner way to get the unscaled resolution here. - // This is done because the surface frame (canvas) is in logical pixels, not physical pixels. - resolution, - transform: glam::DAffine2::IDENTITY, - }; + // Always render to texture (unified path for both WASM and desktop) + let texture = exec.render_vello_scene_to_texture(&scene, resolution, context, background).await.expect("Failed to render Vello scene"); - RenderOutputType::CanvasFrame(frame) - } else { - let texture = exec.render_vello_scene_to_texture(&scene, resolution, context, background).await.expect("Failed to render Vello scene"); - - RenderOutputType::Texture(ImageTexture { texture }) - } + RenderOutputType::Texture(ImageTexture { texture }) } _ => unreachable!("Render node did not receive its requested data type"), }; diff --git a/node-graph/wgpu-executor/src/vector_to_raster.rs b/node-graph/wgpu-executor/src/vector_to_raster.rs new file mode 100644 index 0000000000..6e53a362d5 --- /dev/null +++ b/node-graph/wgpu-executor/src/vector_to_raster.rs @@ -0,0 +1,111 @@ +use crate::WgpuExecutor; +use core_types::Color; +use core_types::bounds::{BoundingBox, RenderBoundingBox}; +use core_types::ops::Convert; +use core_types::table::Table; +use core_types::transform::Footprint; +use glam::{DAffine2, DVec2, UVec2}; +use graphic_types::raster_types::{GPU, Raster}; +use graphic_types::vector_types::GradientStops; +use graphic_types::{Artboard, Graphic, Vector}; +use rendering::{Render, RenderOutputType, RenderParams}; +use wgpu::{CommandEncoderDescriptor, TextureFormat, TextureViewDescriptor}; + +macro_rules! impl_convert { + ($ty:ty) => { + /// Converts Table to Table> by rendering each item to Vello scene and then to texture + impl<'i> Convert>, &'i WgpuExecutor> for Table<$ty> { + async fn convert(self, footprint: Footprint, executor: &'i WgpuExecutor) -> Table> { + // Create render parameters for Vello rendering + let render_params = RenderParams { + render_mode: graphic_types::vector_types::vector::style::RenderMode::Normal, + hide_artboards: false, + for_export: false, + render_output_type: RenderOutputType::Vello, + footprint, + ..Default::default() + }; + + let vector = &self; + let bounding_box = vector.bounding_box(DAffine2::IDENTITY, true); + // TODO: Add cases for infinite bounding boxes + let RenderBoundingBox::Rectangle(rect) = bounding_box else { + panic!("did not find valid bounding box") + }; + + // Create a Vello scene for this vector + let mut scene = vello::Scene::new(); + let mut context = crate::RenderContext::default(); + + let viewport_bounds = footprint.viewport_bounds_in_local_space(); + + let image_bounds = core_types::math::bbox::AxisAlignedBbox { start: rect[0], end: rect[1] }; + let intersection = viewport_bounds.intersect(&image_bounds); + + let size = intersection.size(); + + let offset = (intersection.start - image_bounds.start).max(DVec2::ZERO) + image_bounds.start; + + // If the image would not be visible, return an empty image + if size.x <= 0. || size.y <= 0. { + return Table::new(); + } + + let scale = footprint.scale(); + let width = (size.x * scale.x) as u32; + let height = (size.y * scale.y) as u32; + + // Render the scene to a GPU texture + let resolution = UVec2::new(width, height); + let background = core_types::Color::TRANSPARENT; + + let render_transform = DAffine2::from_scale(scale) * DAffine2::from_translation(-offset); + // Render the vector to the Vello scene with the row's transform + vector.render_to_vello(&mut scene, render_transform, &mut context, &render_params); + + // Use async rendering to get the texture + let texture = executor + .render_vello_scene_to_texture(&scene, resolution, &context, background) + .await + .expect("Failed to render Vello scene to texture"); + + let device = &executor.context.device; + let queue = &executor.context.queue; + let mut encoder = device.create_command_encoder(&CommandEncoderDescriptor::default()); + let blitter = wgpu::util::TextureBlitter::new(device, TextureFormat::Rgba8UnormSrgb); + let view = texture.create_view(&TextureViewDescriptor::default()); + let new_texture = device.create_texture(&wgpu::wgt::TextureDescriptor { + label: None, + size: wgpu::Extent3d { + width: texture.width(), + height: texture.height(), + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::RENDER_ATTACHMENT, + format: TextureFormat::Rgba8UnormSrgb, + view_formats: &[], + }); + let new_view = new_texture.create_view(&TextureViewDescriptor::default()); + + blitter.copy(device, &mut encoder, &view, &new_view); + encoder.on_submitted_work_done(move || texture.destroy()); + let command_buffer = encoder.finish(); + queue.submit([command_buffer]); + + let mut table = Table::new_from_element(Raster::new_gpu(new_texture)); + *(table.get_mut(0).as_mut().unwrap().transform) = DAffine2::from_translation(offset) * DAffine2::from_scale(size); + // texture.destroy(); + table + } + } + }; +} + +impl_convert!(Vector); +impl_convert!(Graphic); +impl_convert!(Artboard); +impl_convert!(GradientStops); +impl_convert!(Color); From c40cee56c043cc98bfcb160088826283c3547108 Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Mon, 24 Nov 2025 12:09:18 +0100 Subject: [PATCH 02/11] Render vello canvas in wasm at the correct resolution --- .../portfolio/portfolio_message_handler.rs | 15 ++++-- .../src/messages/viewport/viewport_message.rs | 10 +++- .../viewport/viewport_message_handler.rs | 28 +++++++++- editor/src/node_graph_executor.rs | 12 +++-- editor/src/node_graph_executor/runtime.rs | 2 +- .../src/components/panels/Document.svelte | 39 ++++++++------ frontend/src/io-managers/input.ts | 4 -- frontend/src/utility-functions/viewports.ts | 54 +++++++++++++++++-- frontend/wasm/src/editor_api.rs | 12 ++++- .../libraries/application-io/src/lib.rs | 2 + 10 files changed, 142 insertions(+), 36 deletions(-) diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index a1364c03bc..b50e69d44d 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -364,12 +364,16 @@ impl MessageHandler> for Portfolio let node_to_inspect = self.node_to_inspect(); let scale = viewport.scale(); - let resolution = viewport.size().into_dvec2().round().as_uvec2(); + // Use logical dimensions for viewport resolution (foreignObject sizing) + let logical_resolution = viewport.size().into_dvec2().ceil().as_uvec2(); + // Use exact physical dimensions from browser (via ResizeObserver's devicePixelContentBoxSize) + let physical_resolution = viewport.physical_size_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, + logical_resolution, + physical_resolution, scale, timing_information, node_to_inspect, @@ -970,11 +974,14 @@ impl MessageHandler> for Portfolio }; let scale = viewport.scale(); - let resolution = viewport.size().into_dvec2().round().as_uvec2(); + // Use logical dimensions for viewport resolution (foreignObject sizing) + let logical_resolution = viewport.size().into_dvec2().ceil().as_uvec2(); + // Use exact physical dimensions from browser (via ResizeObserver's devicePixelContentBoxSize) + let physical_resolution = viewport.physical_size_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, logical_resolution, physical_resolution, scale, timing_information, node_to_inspect, ignore_hash); match result { Err(description) => { diff --git a/editor/src/messages/viewport/viewport_message.rs b/editor/src/messages/viewport/viewport_message.rs index 06c5af74c2..ba7e896180 100644 --- a/editor/src/messages/viewport/viewport_message.rs +++ b/editor/src/messages/viewport/viewport_message.rs @@ -3,6 +3,14 @@ use crate::messages::prelude::*; #[impl_message(Message, Viewport)] #[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)] pub enum ViewportMessage { - Update { x: f64, y: f64, width: f64, height: f64, scale: f64 }, + Update { + x: f64, + y: f64, + width: f64, + height: f64, + scale: f64, + physical_width: f64, + physical_height: f64, + }, RepropagateUpdate, } diff --git a/editor/src/messages/viewport/viewport_message_handler.rs b/editor/src/messages/viewport/viewport_message_handler.rs index 628343bd07..6998be13c1 100644 --- a/editor/src/messages/viewport/viewport_message_handler.rs +++ b/editor/src/messages/viewport/viewport_message_handler.rs @@ -6,6 +6,8 @@ use crate::messages::tool::tool_messages::tool_prelude::DVec2; #[derive(Debug, PartialEq, Clone, Copy, serde::Serialize, serde::Deserialize, specta::Type, ExtractField)] pub struct ViewportMessageHandler { bounds: Bounds, + // Physical bounds in device pixels (exact pixel dimensions from browser) + physical_bounds: Bounds, // Ratio of logical pixels to physical pixels scale: f64, } @@ -16,6 +18,10 @@ impl Default for ViewportMessageHandler { offset: Point { x: 0.0, y: 0.0 }, size: Point { x: 0.0, y: 0.0 }, }, + physical_bounds: Bounds { + offset: Point { x: 0.0, y: 0.0 }, + size: Point { x: 0.0, y: 0.0 }, + }, scale: 1.0, } } @@ -25,7 +31,15 @@ impl Default for ViewportMessageHandler { impl MessageHandler for ViewportMessageHandler { fn process_message(&mut self, message: ViewportMessage, responses: &mut VecDeque, _: ()) { match message { - ViewportMessage::Update { x, y, width, height, scale } => { + ViewportMessage::Update { + x, + y, + width, + height, + scale, + physical_width, + physical_height, + } => { assert_ne!(scale, 0.0, "Viewport scale cannot be zero"); self.scale = scale; @@ -33,6 +47,13 @@ impl MessageHandler for ViewportMessageHandler { offset: Point { x, y }, size: Point { x: width, y: height }, }; + self.physical_bounds = Bounds { + offset: Point { x, y }, + size: Point { + x: physical_width, + y: physical_height, + }, + }; responses.add(NodeGraphMessage::UpdateNodeGraphWidth); } ViewportMessage::RepropagateUpdate => {} @@ -81,6 +102,11 @@ impl ViewportMessageHandler { self.bounds.size().into_scaled(self.scale) } + pub fn physical_size_uvec2(&self) -> glam::UVec2 { + let size = self.physical_bounds.size(); + glam::UVec2::new(size.x.ceil() as u32, size.y.ceil() as u32) + } + #[expect(private_bounds)] pub fn logical>(&self, point: T) -> LogicalPoint { point.into().convert_to_logical(self.scale) diff --git a/editor/src/node_graph_executor.rs b/editor/src/node_graph_executor.rs index 02adee8876..634478e3d2 100644 --- a/editor/src/node_graph_executor.rs +++ b/editor/src/node_graph_executor.rs @@ -138,6 +138,7 @@ impl NodeGraphExecutor { document: &mut DocumentMessageHandler, document_id: DocumentId, viewport_resolution: UVec2, + physical_viewport_resolution: UVec2, viewport_scale: f64, time: TimingInformation, ) -> Result { @@ -148,6 +149,7 @@ impl NodeGraphExecutor { }; let render_config = RenderConfig { viewport, + physical_viewport_resolution, scale: viewport_scale, time, export_format: graphene_std::application_io::ExportFormat::Raster, @@ -171,13 +173,14 @@ impl NodeGraphExecutor { document: &mut DocumentMessageHandler, document_id: DocumentId, viewport_resolution: UVec2, + physical_viewport_resolution: UVec2, viewport_scale: f64, time: TimingInformation, node_to_inspect: Option, ignore_hash: bool, ) -> Result { self.update_node_graph(document, node_to_inspect, ignore_hash)?; - self.submit_current_node_graph_evaluation(document, document_id, viewport_resolution, viewport_scale, time) + self.submit_current_node_graph_evaluation(document, document_id, viewport_resolution, physical_viewport_resolution, viewport_scale, time) } /// Evaluates a node graph for export @@ -206,6 +209,8 @@ impl NodeGraphExecutor { transform, ..Default::default() }, + // For export, logical and physical are the same (no HiDPI scaling) + physical_viewport_resolution: resolution, scale: export_config.scale_factor, time: Default::default(), export_format, @@ -420,9 +425,10 @@ impl NodeGraphExecutor { } let matrix = format_transform_matrix(frame.transform); let transform = if matrix.is_empty() { String::new() } else { format!(" transform=\"{matrix}\"") }; + // Mark viewport canvas with data attribute so it won't be cloned for repeated instances let svg = format!( - r#"
"#, - frame.resolution.x, frame.resolution.y, frame.surface_id.0, frame.physical_resolution.x, frame.physical_resolution.y + r#"
"#, + frame.resolution.x, frame.resolution.y, frame.surface_id.0 ); self.last_svg_canvas = Some(frame); responses.add(FrontendMessage::UpdateDocumentArtwork { svg }); diff --git a/editor/src/node_graph_executor/runtime.rs b/editor/src/node_graph_executor/runtime.rs index c27fb3d51a..2803d48b40 100644 --- a/editor/src/node_graph_executor/runtime.rs +++ b/editor/src/node_graph_executor/runtime.rs @@ -290,7 +290,7 @@ impl NodeRuntime { // Use logical resolution for CSS sizing, physical resolution for the actual surface/texture let logical_resolution = render_config.viewport.resolution; - let physical_resolution = (logical_resolution.as_dvec2() * render_config.scale).as_uvec2(); + let physical_resolution = render_config.physical_viewport_resolution; // Blit the texture to the surface let mut encoder = executor.context.device.create_command_encoder(&vello::wgpu::CommandEncoderDescriptor { diff --git a/frontend/src/components/panels/Document.svelte b/frontend/src/components/panels/Document.svelte index 8034435643..01d275d5b1 100644 --- a/frontend/src/components/panels/Document.svelte +++ b/frontend/src/components/panels/Document.svelte @@ -1,5 +1,5 @@ e.preventDefault()} on:drop={dropFile}> diff --git a/frontend/src/io-managers/input.ts b/frontend/src/io-managers/input.ts index eaf79bb285..5054bb4525 100644 --- a/frontend/src/io-managers/input.ts +++ b/frontend/src/io-managers/input.ts @@ -10,7 +10,6 @@ import { makeKeyboardModifiersBitfield, textInputCleanup, getLocalizedScanCode } import { operatingSystem } from "@graphite/utility-functions/platform"; import { extractPixelData } from "@graphite/utility-functions/rasterization"; import { stripIndents } from "@graphite/utility-functions/strip-indents"; -import { updateBoundsOfViewports } from "@graphite/utility-functions/viewports"; const BUTTON_LEFT = 0; const BUTTON_MIDDLE = 1; @@ -43,7 +42,6 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli // eslint-disable-next-line @typescript-eslint/no-explicit-any const listeners: { target: EventListenerTarget; eventName: EventName; action: (event: any) => void; options?: AddEventListenerOptions }[] = [ - { target: window, eventName: "resize", action: () => updateBoundsOfViewports(editor) }, { target: window, eventName: "beforeunload", action: (e: BeforeUnloadEvent) => onBeforeUnload(e) }, { target: window, eventName: "keyup", action: (e: KeyboardEvent) => onKeyUp(e) }, { target: window, eventName: "keydown", action: (e: KeyboardEvent) => onKeyDown(e) }, @@ -529,8 +527,6 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli // Bind the event listeners bindListeners(); - // Resize on creation - updateBoundsOfViewports(editor); // Return the destructor return unbindListeners; diff --git a/frontend/src/utility-functions/viewports.ts b/frontend/src/utility-functions/viewports.ts index 8fe6a32311..37bc4002b9 100644 --- a/frontend/src/utility-functions/viewports.ts +++ b/frontend/src/utility-functions/viewports.ts @@ -1,12 +1,56 @@ import { type Editor } from "@graphite/editor"; -export function updateBoundsOfViewports(editor: Editor) { - const viewports = Array.from(window.document.querySelectorAll("[data-viewport-container]")); +let resizeObserver: ResizeObserver | undefined; + +export function setupViewportResizeObserver(editor: Editor) { + // Clean up existing observer if any + if (resizeObserver) { + resizeObserver.disconnect(); + } + const viewports = Array.from(window.document.querySelectorAll("[data-viewport-container]")); if (viewports.length <= 0) return; - const bounds = viewports[0].getBoundingClientRect(); - const scale = window.devicePixelRatio || 1; + const viewport = viewports[0] as HTMLElement; + + resizeObserver = new ResizeObserver((entries) => { + for (const entry of entries) { + const devicePixelRatio = window.devicePixelRatio || 1; + + // Get exact device pixel dimensions from the browser + // Use devicePixelContentBoxSize for pixel-perfect rendering with fallback for Safari + let physicalWidth: number; + let physicalHeight: number; + + if (entry.devicePixelContentBoxSize && entry.devicePixelContentBoxSize.length > 0) { + // Modern browsers (Chrome, Firefox): get exact device pixels from the browser + physicalWidth = entry.devicePixelContentBoxSize[0].inlineSize; + physicalHeight = entry.devicePixelContentBoxSize[0].blockSize; + } else { + // Fallback for Safari: calculate from contentBoxSize and devicePixelRatio + physicalWidth = entry.contentBoxSize[0].inlineSize * devicePixelRatio; + physicalHeight = entry.contentBoxSize[0].blockSize * devicePixelRatio; + } + + // Get logical dimensions from contentBoxSize (these may be fractional pixels) + const logicalWidth = entry.contentBoxSize[0].inlineSize; + const logicalHeight = entry.contentBoxSize[0].blockSize; + + // Get viewport position + const bounds = entry.target.getBoundingClientRect(); + + // Send both logical and physical dimensions to the backend + // Logical dimensions are used for CSS/SVG sizing, physical for GPU textures + editor.handle.updateViewport(bounds.x, bounds.y, logicalWidth, logicalHeight, devicePixelRatio, physicalWidth, physicalHeight); + } + }); + + resizeObserver.observe(viewport); +} - editor.handle.updateViewport(bounds.x, bounds.y, bounds.width, bounds.height, scale); +export function cleanupViewportResizeObserver() { + if (resizeObserver) { + resizeObserver.disconnect(); + resizeObserver = undefined; + } } diff --git a/frontend/wasm/src/editor_api.rs b/frontend/wasm/src/editor_api.rs index afa96ee726..913be893d4 100644 --- a/frontend/wasm/src/editor_api.rs +++ b/frontend/wasm/src/editor_api.rs @@ -518,8 +518,16 @@ impl EditorHandle { /// Send new viewport info to the backend #[wasm_bindgen(js_name = updateViewport)] - pub fn update_viewport(&self, x: f64, y: f64, width: f64, height: f64, scale: f64) { - let message = ViewportMessage::Update { x, y, width, height, scale }; + pub fn update_viewport(&self, x: f64, y: f64, width: f64, height: f64, scale: f64, physical_width: f64, physical_height: f64) { + let message = ViewportMessage::Update { + x, + y, + width, + height, + scale, + physical_width, + physical_height, + }; self.dispatch(message); } diff --git a/node-graph/libraries/application-io/src/lib.rs b/node-graph/libraries/application-io/src/lib.rs index 9b54544061..328e61efa3 100644 --- a/node-graph/libraries/application-io/src/lib.rs +++ b/node-graph/libraries/application-io/src/lib.rs @@ -239,6 +239,8 @@ pub struct TimingInformation { #[derive(Debug, Default, Clone, Copy, PartialEq, DynAny, serde::Serialize, serde::Deserialize)] pub struct RenderConfig { pub viewport: Footprint, + /// Physical viewport resolution in device pixels (from ResizeObserver's devicePixelContentBoxSize) + pub physical_viewport_resolution: UVec2, pub scale: f64, pub export_format: ExportFormat, pub time: TimingInformation, From c68026d2b10300744dee4e0488a2346d52f0545d Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Mon, 24 Nov 2025 12:40:55 +0100 Subject: [PATCH 03/11] Cleanup unused surface rendering code --- node-graph/interpreted-executor/src/util.rs | 19 +++-------- node-graph/wgpu-executor/src/lib.rs | 37 +-------------------- 2 files changed, 5 insertions(+), 51 deletions(-) diff --git a/node-graph/interpreted-executor/src/util.rs b/node-graph/interpreted-executor/src/util.rs index bcfccdb6c6..21704e3125 100644 --- a/node-graph/interpreted-executor/src/util.rs +++ b/node-graph/interpreted-executor/src/util.rs @@ -26,21 +26,10 @@ pub fn wrap_network_in_scope(mut network: NodeNetwork, editor_api: Arc Result<()> { - let mut guard = surface.surface.target_texture.lock().await; - - let surface_inner = &surface.surface.inner; - let surface_caps = surface_inner.get_capabilities(&self.context.adapter); - surface_inner.configure( - &self.context.device, - &SurfaceConfiguration { - usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::STORAGE_BINDING, - format: VELLO_SURFACE_FORMAT, - width: size.x, - height: size.y, - present_mode: surface_caps.present_modes[0], - alpha_mode: wgpu::CompositeAlphaMode::Opaque, - view_formats: vec![], - desired_maximum_frame_latency: 2, - }, - ); - - self.render_vello_scene_to_target_texture(scene, size, context, background, &mut guard).await?; - - let surface_texture = surface_inner.get_current_texture()?; - let mut encoder = self.context.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("Surface Blit") }); - surface.surface.blitter.copy( - &self.context.device, - &mut encoder, - &guard.as_ref().unwrap().view, - &surface_texture.texture.create_view(&wgpu::TextureViewDescriptor::default()), - ); - self.context.queue.submit([encoder.finish()]); - surface_texture.present(); - - Ok(()) - } - pub async fn render_vello_scene_to_texture(&self, scene: &Scene, size: UVec2, context: &RenderContext, background: Color) -> Result { let mut output = None; self.render_vello_scene_to_target_texture(scene, size, context, background, &mut output).await?; From 6d27580d34f6fd585fb3523dac3e9e0765317175 Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Mon, 24 Nov 2025 12:42:46 +0100 Subject: [PATCH 04/11] Remove vector to raster conversion --- .../wgpu-executor/src/vector_to_raster.rs | 111 ------------------ 1 file changed, 111 deletions(-) delete mode 100644 node-graph/wgpu-executor/src/vector_to_raster.rs diff --git a/node-graph/wgpu-executor/src/vector_to_raster.rs b/node-graph/wgpu-executor/src/vector_to_raster.rs deleted file mode 100644 index 6e53a362d5..0000000000 --- a/node-graph/wgpu-executor/src/vector_to_raster.rs +++ /dev/null @@ -1,111 +0,0 @@ -use crate::WgpuExecutor; -use core_types::Color; -use core_types::bounds::{BoundingBox, RenderBoundingBox}; -use core_types::ops::Convert; -use core_types::table::Table; -use core_types::transform::Footprint; -use glam::{DAffine2, DVec2, UVec2}; -use graphic_types::raster_types::{GPU, Raster}; -use graphic_types::vector_types::GradientStops; -use graphic_types::{Artboard, Graphic, Vector}; -use rendering::{Render, RenderOutputType, RenderParams}; -use wgpu::{CommandEncoderDescriptor, TextureFormat, TextureViewDescriptor}; - -macro_rules! impl_convert { - ($ty:ty) => { - /// Converts Table to Table> by rendering each item to Vello scene and then to texture - impl<'i> Convert>, &'i WgpuExecutor> for Table<$ty> { - async fn convert(self, footprint: Footprint, executor: &'i WgpuExecutor) -> Table> { - // Create render parameters for Vello rendering - let render_params = RenderParams { - render_mode: graphic_types::vector_types::vector::style::RenderMode::Normal, - hide_artboards: false, - for_export: false, - render_output_type: RenderOutputType::Vello, - footprint, - ..Default::default() - }; - - let vector = &self; - let bounding_box = vector.bounding_box(DAffine2::IDENTITY, true); - // TODO: Add cases for infinite bounding boxes - let RenderBoundingBox::Rectangle(rect) = bounding_box else { - panic!("did not find valid bounding box") - }; - - // Create a Vello scene for this vector - let mut scene = vello::Scene::new(); - let mut context = crate::RenderContext::default(); - - let viewport_bounds = footprint.viewport_bounds_in_local_space(); - - let image_bounds = core_types::math::bbox::AxisAlignedBbox { start: rect[0], end: rect[1] }; - let intersection = viewport_bounds.intersect(&image_bounds); - - let size = intersection.size(); - - let offset = (intersection.start - image_bounds.start).max(DVec2::ZERO) + image_bounds.start; - - // If the image would not be visible, return an empty image - if size.x <= 0. || size.y <= 0. { - return Table::new(); - } - - let scale = footprint.scale(); - let width = (size.x * scale.x) as u32; - let height = (size.y * scale.y) as u32; - - // Render the scene to a GPU texture - let resolution = UVec2::new(width, height); - let background = core_types::Color::TRANSPARENT; - - let render_transform = DAffine2::from_scale(scale) * DAffine2::from_translation(-offset); - // Render the vector to the Vello scene with the row's transform - vector.render_to_vello(&mut scene, render_transform, &mut context, &render_params); - - // Use async rendering to get the texture - let texture = executor - .render_vello_scene_to_texture(&scene, resolution, &context, background) - .await - .expect("Failed to render Vello scene to texture"); - - let device = &executor.context.device; - let queue = &executor.context.queue; - let mut encoder = device.create_command_encoder(&CommandEncoderDescriptor::default()); - let blitter = wgpu::util::TextureBlitter::new(device, TextureFormat::Rgba8UnormSrgb); - let view = texture.create_view(&TextureViewDescriptor::default()); - let new_texture = device.create_texture(&wgpu::wgt::TextureDescriptor { - label: None, - size: wgpu::Extent3d { - width: texture.width(), - height: texture.height(), - depth_or_array_layers: 1, - }, - mip_level_count: 1, - sample_count: 1, - dimension: wgpu::TextureDimension::D2, - usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::RENDER_ATTACHMENT, - format: TextureFormat::Rgba8UnormSrgb, - view_formats: &[], - }); - let new_view = new_texture.create_view(&TextureViewDescriptor::default()); - - blitter.copy(device, &mut encoder, &view, &new_view); - encoder.on_submitted_work_done(move || texture.destroy()); - let command_buffer = encoder.finish(); - queue.submit([command_buffer]); - - let mut table = Table::new_from_element(Raster::new_gpu(new_texture)); - *(table.get_mut(0).as_mut().unwrap().transform) = DAffine2::from_translation(offset) * DAffine2::from_scale(size); - // texture.destroy(); - table - } - } - }; -} - -impl_convert!(Vector); -impl_convert!(Graphic); -impl_convert!(Artboard); -impl_convert!(GradientStops); -impl_convert!(Color); From 3c3112f0a83fab22098c1a7e2f35e49453b4d758 Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Mon, 24 Nov 2025 12:44:21 +0100 Subject: [PATCH 05/11] Remove desktop changes --- desktop/src/window.rs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/desktop/src/window.rs b/desktop/src/window.rs index 7a250e6b18..e9994d23ca 100644 --- a/desktop/src/window.rs +++ b/desktop/src/window.rs @@ -4,12 +4,9 @@ use winit::window::{Window as WinitWindow, WindowAttributes}; use crate::consts::APP_NAME; use crate::event::AppEventScheduler; -#[cfg(target_os = "macos")] -use crate::window::mac::NativeWindowImpl; use crate::wrapper::messages::MenuItem; pub(crate) trait NativeWindow { - fn init() {} fn configure(attributes: WindowAttributes, event_loop: &dyn ActiveEventLoop) -> WindowAttributes; fn new(window: &dyn WinitWindow, app_event_scheduler: AppEventScheduler) -> Self; fn update_menu(&self, _entries: Vec) {} @@ -37,10 +34,6 @@ pub(crate) struct Window { } impl Window { - pub(crate) fn init() { - native::NativeWindowImpl::init(); - } - pub(crate) fn new(event_loop: &dyn ActiveEventLoop, app_event_scheduler: AppEventScheduler) -> Self { let mut attributes = WindowAttributes::default() .with_title(APP_NAME) From 3a80190da2ea59a5119d6f332b8a0f1ec19690c6 Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Mon, 24 Nov 2025 12:50:27 +0100 Subject: [PATCH 06/11] Revert window.rs changes --- desktop/src/window.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/desktop/src/window.rs b/desktop/src/window.rs index e9994d23ca..6a6b2dce71 100644 --- a/desktop/src/window.rs +++ b/desktop/src/window.rs @@ -4,9 +4,11 @@ use winit::window::{Window as WinitWindow, WindowAttributes}; use crate::consts::APP_NAME; use crate::event::AppEventScheduler; +use crate::window::mac::NativeWindowImpl; use crate::wrapper::messages::MenuItem; pub(crate) trait NativeWindow { + fn init() {} fn configure(attributes: WindowAttributes, event_loop: &dyn ActiveEventLoop) -> WindowAttributes; fn new(window: &dyn WinitWindow, app_event_scheduler: AppEventScheduler) -> Self; fn update_menu(&self, _entries: Vec) {} @@ -34,6 +36,10 @@ pub(crate) struct Window { } impl Window { + pub(crate) fn init() { + NativeWindowImpl::init(); + } + pub(crate) fn new(event_loop: &dyn ActiveEventLoop, app_event_scheduler: AppEventScheduler) -> Self { let mut attributes = WindowAttributes::default() .with_title(APP_NAME) From af2c431cdba94b83e219f41f55042ea0065ea894 Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Mon, 24 Nov 2025 13:57:25 +0100 Subject: [PATCH 07/11] Don't round logical coordinates --- .../portfolio/portfolio_message_handler.rs | 12 +++----- .../src/messages/viewport/viewport_message.rs | 10 +------ .../viewport/viewport_message_handler.rs | 28 +------------------ editor/src/node_graph_executor.rs | 12 ++------ editor/src/node_graph_executor/runtime.rs | 4 +-- frontend/src/utility-functions/viewports.ts | 4 ++- frontend/wasm/src/editor_api.rs | 12 ++------ .../libraries/application-io/src/lib.rs | 8 ++---- node-graph/nodes/gstd/src/render_node.rs | 28 +++++++++---------- 9 files changed, 32 insertions(+), 86 deletions(-) diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index b50e69d44d..b04381e056 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -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,15 +365,12 @@ impl MessageHandler> for Portfolio let node_to_inspect = self.node_to_inspect(); let scale = viewport.scale(); - // Use logical dimensions for viewport resolution (foreignObject sizing) - let logical_resolution = viewport.size().into_dvec2().ceil().as_uvec2(); // Use exact physical dimensions from browser (via ResizeObserver's devicePixelContentBoxSize) - let physical_resolution = viewport.physical_size_uvec2(); + let physical_resolution = viewport.size().to_physical().into_dvec2().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, - logical_resolution, physical_resolution, scale, timing_information, @@ -974,14 +972,12 @@ impl MessageHandler> for Portfolio }; let scale = viewport.scale(); - // Use logical dimensions for viewport resolution (foreignObject sizing) - let logical_resolution = viewport.size().into_dvec2().ceil().as_uvec2(); // Use exact physical dimensions from browser (via ResizeObserver's devicePixelContentBoxSize) - let physical_resolution = viewport.physical_size_uvec2(); + let physical_resolution = viewport.size().to_physical().into_dvec2().round().as_uvec2(); let result = self .executor - .submit_node_graph_evaluation(document, document_id, logical_resolution, physical_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) => { diff --git a/editor/src/messages/viewport/viewport_message.rs b/editor/src/messages/viewport/viewport_message.rs index ba7e896180..06c5af74c2 100644 --- a/editor/src/messages/viewport/viewport_message.rs +++ b/editor/src/messages/viewport/viewport_message.rs @@ -3,14 +3,6 @@ use crate::messages::prelude::*; #[impl_message(Message, Viewport)] #[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)] pub enum ViewportMessage { - Update { - x: f64, - y: f64, - width: f64, - height: f64, - scale: f64, - physical_width: f64, - physical_height: f64, - }, + Update { x: f64, y: f64, width: f64, height: f64, scale: f64 }, RepropagateUpdate, } diff --git a/editor/src/messages/viewport/viewport_message_handler.rs b/editor/src/messages/viewport/viewport_message_handler.rs index 6998be13c1..628343bd07 100644 --- a/editor/src/messages/viewport/viewport_message_handler.rs +++ b/editor/src/messages/viewport/viewport_message_handler.rs @@ -6,8 +6,6 @@ use crate::messages::tool::tool_messages::tool_prelude::DVec2; #[derive(Debug, PartialEq, Clone, Copy, serde::Serialize, serde::Deserialize, specta::Type, ExtractField)] pub struct ViewportMessageHandler { bounds: Bounds, - // Physical bounds in device pixels (exact pixel dimensions from browser) - physical_bounds: Bounds, // Ratio of logical pixels to physical pixels scale: f64, } @@ -18,10 +16,6 @@ impl Default for ViewportMessageHandler { offset: Point { x: 0.0, y: 0.0 }, size: Point { x: 0.0, y: 0.0 }, }, - physical_bounds: Bounds { - offset: Point { x: 0.0, y: 0.0 }, - size: Point { x: 0.0, y: 0.0 }, - }, scale: 1.0, } } @@ -31,15 +25,7 @@ impl Default for ViewportMessageHandler { impl MessageHandler for ViewportMessageHandler { fn process_message(&mut self, message: ViewportMessage, responses: &mut VecDeque, _: ()) { match message { - ViewportMessage::Update { - x, - y, - width, - height, - scale, - physical_width, - physical_height, - } => { + ViewportMessage::Update { x, y, width, height, scale } => { assert_ne!(scale, 0.0, "Viewport scale cannot be zero"); self.scale = scale; @@ -47,13 +33,6 @@ impl MessageHandler for ViewportMessageHandler { offset: Point { x, y }, size: Point { x: width, y: height }, }; - self.physical_bounds = Bounds { - offset: Point { x, y }, - size: Point { - x: physical_width, - y: physical_height, - }, - }; responses.add(NodeGraphMessage::UpdateNodeGraphWidth); } ViewportMessage::RepropagateUpdate => {} @@ -102,11 +81,6 @@ impl ViewportMessageHandler { self.bounds.size().into_scaled(self.scale) } - pub fn physical_size_uvec2(&self) -> glam::UVec2 { - let size = self.physical_bounds.size(); - glam::UVec2::new(size.x.ceil() as u32, size.y.ceil() as u32) - } - #[expect(private_bounds)] pub fn logical>(&self, point: T) -> LogicalPoint { point.into().convert_to_logical(self.scale) diff --git a/editor/src/node_graph_executor.rs b/editor/src/node_graph_executor.rs index 634478e3d2..05c428341b 100644 --- a/editor/src/node_graph_executor.rs +++ b/editor/src/node_graph_executor.rs @@ -138,7 +138,6 @@ impl NodeGraphExecutor { document: &mut DocumentMessageHandler, document_id: DocumentId, viewport_resolution: UVec2, - physical_viewport_resolution: UVec2, viewport_scale: f64, time: TimingInformation, ) -> Result { @@ -149,7 +148,6 @@ impl NodeGraphExecutor { }; let render_config = RenderConfig { viewport, - physical_viewport_resolution, scale: viewport_scale, time, export_format: graphene_std::application_io::ExportFormat::Raster, @@ -173,14 +171,13 @@ impl NodeGraphExecutor { document: &mut DocumentMessageHandler, document_id: DocumentId, viewport_resolution: UVec2, - physical_viewport_resolution: UVec2, viewport_scale: f64, time: TimingInformation, node_to_inspect: Option, ignore_hash: bool, ) -> Result { self.update_node_graph(document, node_to_inspect, ignore_hash)?; - self.submit_current_node_graph_evaluation(document, document_id, viewport_resolution, physical_viewport_resolution, viewport_scale, time) + self.submit_current_node_graph_evaluation(document, document_id, viewport_resolution, viewport_scale, time) } /// Evaluates a node graph for export @@ -209,8 +206,6 @@ impl NodeGraphExecutor { transform, ..Default::default() }, - // For export, logical and physical are the same (no HiDPI scaling) - physical_viewport_resolution: resolution, scale: export_config.scale_factor, time: Default::default(), export_format, @@ -425,10 +420,9 @@ impl NodeGraphExecutor { } let matrix = format_transform_matrix(frame.transform); let transform = if matrix.is_empty() { String::new() } else { format!(" transform=\"{matrix}\"") }; - // Mark viewport canvas with data attribute so it won't be cloned for repeated instances let svg = format!( - r#"
"#, - frame.resolution.x, frame.resolution.y, frame.surface_id.0 + r#"
"#, + frame.resolution.x, frame.resolution.y, frame.surface_id.0, frame.physical_resolution.x, frame.physical_resolution.y ); self.last_svg_canvas = Some(frame); responses.add(FrontendMessage::UpdateDocumentArtwork { svg }); diff --git a/editor/src/node_graph_executor/runtime.rs b/editor/src/node_graph_executor/runtime.rs index 2803d48b40..e09184d467 100644 --- a/editor/src/node_graph_executor/runtime.rs +++ b/editor/src/node_graph_executor/runtime.rs @@ -289,8 +289,8 @@ impl NodeRuntime { let surface = self.wasm_viewport_surface.as_ref().unwrap(); // Use logical resolution for CSS sizing, physical resolution for the actual surface/texture - let logical_resolution = render_config.viewport.resolution; - let physical_resolution = render_config.physical_viewport_resolution; + 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 { diff --git a/frontend/src/utility-functions/viewports.ts b/frontend/src/utility-functions/viewports.ts index 37bc4002b9..27a675cf17 100644 --- a/frontend/src/utility-functions/viewports.ts +++ b/frontend/src/utility-functions/viewports.ts @@ -39,9 +39,11 @@ export function setupViewportResizeObserver(editor: Editor) { // Get viewport position const bounds = entry.target.getBoundingClientRect(); + const scale = physicalWidth / logicalWidth; + // Send both logical and physical dimensions to the backend // Logical dimensions are used for CSS/SVG sizing, physical for GPU textures - editor.handle.updateViewport(bounds.x, bounds.y, logicalWidth, logicalHeight, devicePixelRatio, physicalWidth, physicalHeight); + editor.handle.updateViewport(bounds.x, bounds.y, logicalWidth, logicalHeight, scale); } }); diff --git a/frontend/wasm/src/editor_api.rs b/frontend/wasm/src/editor_api.rs index 913be893d4..afa96ee726 100644 --- a/frontend/wasm/src/editor_api.rs +++ b/frontend/wasm/src/editor_api.rs @@ -518,16 +518,8 @@ impl EditorHandle { /// Send new viewport info to the backend #[wasm_bindgen(js_name = updateViewport)] - pub fn update_viewport(&self, x: f64, y: f64, width: f64, height: f64, scale: f64, physical_width: f64, physical_height: f64) { - let message = ViewportMessage::Update { - x, - y, - width, - height, - scale, - physical_width, - physical_height, - }; + pub fn update_viewport(&self, x: f64, y: f64, width: f64, height: f64, scale: f64) { + let message = ViewportMessage::Update { x, y, width, height, scale }; self.dispatch(message); } diff --git a/node-graph/libraries/application-io/src/lib.rs b/node-graph/libraries/application-io/src/lib.rs index 328e61efa3..4030a0d8f9 100644 --- a/node-graph/libraries/application-io/src/lib.rs +++ b/node-graph/libraries/application-io/src/lib.rs @@ -1,6 +1,6 @@ use core_types::transform::Footprint; use dyn_any::{DynAny, StaticType, StaticTypeSized}; -use glam::{DAffine2, UVec2}; +use glam::{DAffine2, DVec2, UVec2}; use std::fmt::Debug; use std::future::Future; use std::hash::{Hash, Hasher}; @@ -24,7 +24,7 @@ impl std::fmt::Display for SurfaceId { pub struct SurfaceFrame { pub surface_id: SurfaceId, /// Logical resolution in CSS pixels (used for foreignObject dimensions) - pub resolution: UVec2, + pub resolution: DVec2, /// Physical resolution in device pixels (used for actual canvas/texture dimensions) pub physical_resolution: UVec2, pub transform: DAffine2, @@ -108,7 +108,7 @@ impl From> for SurfaceFrame { Self { surface_id: x.surface_handle.window_id, transform: x.transform, - resolution: size, + resolution: size.into(), physical_resolution: size, } } @@ -239,8 +239,6 @@ pub struct TimingInformation { #[derive(Debug, Default, Clone, Copy, PartialEq, DynAny, serde::Serialize, serde::Deserialize)] pub struct RenderConfig { pub viewport: Footprint, - /// Physical viewport resolution in device pixels (from ResizeObserver's devicePixelContentBoxSize) - pub physical_viewport_resolution: UVec2, pub scale: f64, pub export_format: ExportFormat, pub time: TimingInformation, diff --git a/node-graph/nodes/gstd/src/render_node.rs b/node-graph/nodes/gstd/src/render_node.rs index 43e5d03e46..d0bbcaa89e 100644 --- a/node-graph/nodes/gstd/src/render_node.rs +++ b/node-graph/nodes/gstd/src/render_node.rs @@ -120,11 +120,7 @@ async fn create_context<'a: 'n>( } #[node_macro::node(category(""))] -async fn render<'a: 'n>( - ctx: impl Ctx + ExtractFootprint + ExtractVarArgs, - editor_api: &'a WasmEditorApi, - data: RenderIntermediate, -) -> RenderOutput { +async fn render<'a: 'n>(ctx: impl Ctx + ExtractFootprint + ExtractVarArgs, editor_api: &'a WasmEditorApi, data: RenderIntermediate) -> RenderOutput { let footprint = ctx.footprint(); let render_params = ctx .vararg(0) @@ -135,6 +131,10 @@ async fn render<'a: 'n>( render_params.footprint = *footprint; let render_params = &render_params; + let scale = render_params.scale; + let physical_resolution = render_params.footprint.resolution; + let logical_resolution = (render_params.footprint.resolution.as_dvec2() / scale).round().as_uvec2(); + let RenderIntermediate { ty, mut metadata, contains_artboard } = data; metadata.apply_transform(footprint.transform); @@ -145,8 +145,8 @@ async fn render<'a: 'n>( rendering.leaf_tag("rect", |attributes| { attributes.push("x", "0"); attributes.push("y", "0"); - attributes.push("width", footprint.resolution.x.to_string()); - attributes.push("height", footprint.resolution.y.to_string()); + attributes.push("width", logical_resolution.x.to_string()); + attributes.push("height", logical_resolution.y.to_string()); let matrix = format_transform_matrix(footprint.transform.inverse()); if !matrix.is_empty() { attributes.push("transform", matrix); @@ -158,7 +158,7 @@ async fn render<'a: 'n>( rendering.image_data = svg_data.1.clone(); rendering.svg_defs = svg_data.2.clone(); - rendering.wrap_with_transform(footprint.transform, Some(footprint.resolution.as_dvec2())); + rendering.wrap_with_transform(footprint.transform, Some(logical_resolution.as_dvec2())); RenderOutputType::Svg { svg: rendering.svg.to_svg_string(), image_data: rendering.image_data, @@ -170,9 +170,6 @@ async fn render<'a: 'n>( }; let (child, context) = Arc::as_ref(vello_data); - // Always apply scale when rendering to texture - let scale = render_params.scale; - let scale_transform = glam::DAffine2::from_scale(glam::DVec2::splat(scale)); let footprint_transform = scale_transform * footprint.transform; let footprint_transform_vello = vello::kurbo::Affine::new(footprint_transform.to_cols_array()); @@ -180,11 +177,9 @@ async fn render<'a: 'n>( let mut scene = vello::Scene::new(); scene.append(child, Some(footprint_transform_vello)); - let resolution = (footprint.resolution.as_dvec2() * scale).as_uvec2(); - // We now replace all transforms which are supposed to be infinite with a transform which covers the entire viewport // See for more detail - let scaled_infinite_transform = vello::kurbo::Affine::scale_non_uniform(resolution.x as f64, resolution.y as f64); + let scaled_infinite_transform = vello::kurbo::Affine::scale_non_uniform(physical_resolution.x as f64, physical_resolution.y as f64); let encoding = scene.encoding_mut(); for transform in encoding.transforms.iter_mut() { if transform.matrix[0] == f32::INFINITY { @@ -198,7 +193,10 @@ async fn render<'a: 'n>( } // Always render to texture (unified path for both WASM and desktop) - let texture = exec.render_vello_scene_to_texture(&scene, resolution, context, background).await.expect("Failed to render Vello scene"); + let texture = exec + .render_vello_scene_to_texture(&scene, physical_resolution, context, background) + .await + .expect("Failed to render Vello scene"); RenderOutputType::Texture(ImageTexture { texture }) } From e750a9c727d52c74d6c6646704c49f168a186e0d Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Mon, 24 Nov 2025 14:00:46 +0100 Subject: [PATCH 08/11] Fix desktop compilation + don't round logical coordinates for svg rendering --- desktop/src/window.rs | 3 +-- node-graph/nodes/gstd/src/render_node.rs | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/desktop/src/window.rs b/desktop/src/window.rs index 6a6b2dce71..61692bc6ee 100644 --- a/desktop/src/window.rs +++ b/desktop/src/window.rs @@ -4,7 +4,6 @@ use winit::window::{Window as WinitWindow, WindowAttributes}; use crate::consts::APP_NAME; use crate::event::AppEventScheduler; -use crate::window::mac::NativeWindowImpl; use crate::wrapper::messages::MenuItem; pub(crate) trait NativeWindow { @@ -37,7 +36,7 @@ pub(crate) struct Window { impl Window { pub(crate) fn init() { - NativeWindowImpl::init(); + native::NativeWindowImpl::init(); } pub(crate) fn new(event_loop: &dyn ActiveEventLoop, app_event_scheduler: AppEventScheduler) -> Self { diff --git a/node-graph/nodes/gstd/src/render_node.rs b/node-graph/nodes/gstd/src/render_node.rs index d0bbcaa89e..a56efb3c06 100644 --- a/node-graph/nodes/gstd/src/render_node.rs +++ b/node-graph/nodes/gstd/src/render_node.rs @@ -133,7 +133,7 @@ async fn render<'a: 'n>(ctx: impl Ctx + ExtractFootprint + ExtractVarArgs, edito let scale = render_params.scale; let physical_resolution = render_params.footprint.resolution; - let logical_resolution = (render_params.footprint.resolution.as_dvec2() / scale).round().as_uvec2(); + let logical_resolution = (render_params.footprint.resolution.as_dvec2() / scale); let RenderIntermediate { ty, mut metadata, contains_artboard } = data; metadata.apply_transform(footprint.transform); @@ -158,7 +158,7 @@ async fn render<'a: 'n>(ctx: impl Ctx + ExtractFootprint + ExtractVarArgs, edito rendering.image_data = svg_data.1.clone(); rendering.svg_defs = svg_data.2.clone(); - rendering.wrap_with_transform(footprint.transform, Some(logical_resolution.as_dvec2())); + rendering.wrap_with_transform(footprint.transform, Some(logical_resolution)); RenderOutputType::Svg { svg: rendering.svg.to_svg_string(), image_data: rendering.image_data, From d56017a5721349f822560eb64d845ee6c70816ed Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Mon, 24 Nov 2025 14:24:33 +0100 Subject: [PATCH 09/11] Further cleanup --- .../node_graph/document_node_definitions.rs | 64 ------------------- .../portfolio/portfolio_message_handler.rs | 2 +- editor/src/node_graph_executor.rs | 4 +- editor/src/node_graph_executor/runtime.rs | 1 - .../src/components/panels/Document.svelte | 4 +- .../interpreted-executor/src/node_registry.rs | 24 ------- .../libraries/application-io/src/lib.rs | 3 - node-graph/nodes/gstd/src/render_node.rs | 2 +- node-graph/wgpu-executor/src/lib.rs | 9 +-- 9 files changed, 7 insertions(+), 106 deletions(-) diff --git a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs index 2da1a34fed..eae239ff85 100644 --- a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs +++ b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs @@ -882,70 +882,6 @@ fn static_nodes() -> Vec { 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", diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index b04381e056..e770c9d61e 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -366,7 +366,7 @@ impl MessageHandler> for Portfolio let scale = viewport.scale(); // Use exact physical dimensions from browser (via ResizeObserver's devicePixelContentBoxSize) - let physical_resolution = viewport.size().to_physical().into_dvec2().as_uvec2(); + 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"), diff --git a/editor/src/node_graph_executor.rs b/editor/src/node_graph_executor.rs index 05c428341b..fb9b2eb37d 100644 --- a/editor/src/node_graph_executor.rs +++ b/editor/src/node_graph_executor.rs @@ -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#"
"#, - frame.resolution.x, frame.resolution.y, frame.surface_id.0, frame.physical_resolution.x, frame.physical_resolution.y + r#"
"#, + frame.resolution.x, frame.resolution.y, frame.surface_id.0, ); self.last_svg_canvas = Some(frame); responses.add(FrontendMessage::UpdateDocumentArtwork { svg }); diff --git a/editor/src/node_graph_executor/runtime.rs b/editor/src/node_graph_executor/runtime.rs index e09184d467..3be389f7c0 100644 --- a/editor/src/node_graph_executor/runtime.rs +++ b/editor/src/node_graph_executor/runtime.rs @@ -330,7 +330,6 @@ impl NodeRuntime { let frame = graphene_std::application_io::SurfaceFrame { surface_id: surface.window_id, resolution: logical_resolution, - physical_resolution, transform: glam::DAffine2::IDENTITY, }; diff --git a/frontend/src/components/panels/Document.svelte b/frontend/src/components/panels/Document.svelte index 01d275d5b1..3f679690b2 100644 --- a/frontend/src/components/panels/Document.svelte +++ b/frontend/src/components/panels/Document.svelte @@ -206,8 +206,8 @@ // Get logical dimensions from foreignObject parent (set by backend) const foreignObject = placeholder.parentElement; if (!foreignObject) return; - const logicalWidth = parseInt(foreignObject.getAttribute("width") || "0"); - const logicalHeight = parseInt(foreignObject.getAttribute("height") || "0"); + const logicalWidth = parseFloat(foreignObject.getAttribute("width") || "0"); + const logicalHeight = parseFloat(foreignObject.getAttribute("height") || "0"); // Clone canvas for repeated instances (layers that appear multiple times) // Viewport canvas is marked with data-is-viewport and should never be cloned diff --git a/node-graph/interpreted-executor/src/node_registry.rs b/node-graph/interpreted-executor/src/node_registry.rs index 1e252a5db6..9fb82e29a7 100644 --- a/node-graph/interpreted-executor/src/node_registry.rs +++ b/node-graph/interpreted-executor/src/node_registry.rs @@ -6,8 +6,6 @@ use graph_craft::proto::{NodeConstructor, TypeErasedBox}; use graphene_std::Artboard; use graphene_std::Context; use graphene_std::Graphic; -#[cfg(feature = "gpu")] -use graphene_std::any::DowncastBothNode; use graphene_std::any::DynAnyNode; use graphene_std::application_io::{ImageTexture, SurfaceFrame}; use graphene_std::brush::brush_cache::BrushCache; @@ -234,28 +232,6 @@ fn node_registry() -> HashMap, input: Context, fn_params: [Context => path_bool_nodes::BooleanOperation]), async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_std::text::TextAlign]), async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => RenderIntermediate]), - // ======================= - // CREATE GPU SURFACE NODE - // ======================= - #[cfg(feature = "gpu")] - ( - ProtoNodeIdentifier::new(stringify!(wgpu_executor::CreateGpuSurfaceNode<_>)), - |args| { - Box::pin(async move { - let editor_api: DowncastBothNode = DowncastBothNode::new(args[0].clone()); - let node = >::new(editor_api); - let any: DynAnyNode = DynAnyNode::new(node); - Box::new(any) as TypeErasedBox - }) - }, - { - let node = >::new(graphene_std::any::PanicNode::>::new()); - let params = vec![fn_type_fut!(Context, &WasmEditorApi)]; - let mut node_io = as NodeIO<'_, Context>>::to_async_node_io(&node, params); - node_io.call_argument = concrete!(::Static); - node_io - }, - ), ]; // ============= // CONVERT NODES diff --git a/node-graph/libraries/application-io/src/lib.rs b/node-graph/libraries/application-io/src/lib.rs index 4030a0d8f9..c023204e1e 100644 --- a/node-graph/libraries/application-io/src/lib.rs +++ b/node-graph/libraries/application-io/src/lib.rs @@ -25,8 +25,6 @@ pub struct SurfaceFrame { pub surface_id: SurfaceId, /// Logical resolution in CSS pixels (used for foreignObject dimensions) pub resolution: DVec2, - /// Physical resolution in device pixels (used for actual canvas/texture dimensions) - pub physical_resolution: UVec2, pub transform: DAffine2, } @@ -109,7 +107,6 @@ impl From> for SurfaceFrame { surface_id: x.surface_handle.window_id, transform: x.transform, resolution: size.into(), - physical_resolution: size, } } } diff --git a/node-graph/nodes/gstd/src/render_node.rs b/node-graph/nodes/gstd/src/render_node.rs index a56efb3c06..ceac0830c6 100644 --- a/node-graph/nodes/gstd/src/render_node.rs +++ b/node-graph/nodes/gstd/src/render_node.rs @@ -133,7 +133,7 @@ async fn render<'a: 'n>(ctx: impl Ctx + ExtractFootprint + ExtractVarArgs, edito let scale = render_params.scale; let physical_resolution = render_params.footprint.resolution; - let logical_resolution = (render_params.footprint.resolution.as_dvec2() / scale); + let logical_resolution = render_params.footprint.resolution.as_dvec2() / scale; let RenderIntermediate { ty, mut metadata, contains_artboard } = data; metadata.apply_transform(footprint.transform); diff --git a/node-graph/wgpu-executor/src/lib.rs b/node-graph/wgpu-executor/src/lib.rs index 5fca66fd05..868c73f60e 100644 --- a/node-graph/wgpu-executor/src/lib.rs +++ b/node-graph/wgpu-executor/src/lib.rs @@ -4,7 +4,7 @@ pub mod texture_conversion; use crate::shader_runtime::ShaderRuntime; use anyhow::Result; -use core_types::{Color, Ctx}; +use core_types::Color; use dyn_any::StaticType; use futures::lock::Mutex; use glam::UVec2; @@ -176,10 +176,3 @@ impl WgpuExecutor { } pub type WindowHandle = Arc>; - -#[node_macro::node(skip_impl)] -fn create_gpu_surface<'a: 'n, Io: ApplicationIo + 'a + Send + Sync>(_: impl Ctx + 'a, editor_api: &'a EditorApi) -> Option { - let canvas = editor_api.application_io.as_ref()?.window()?; - let executor = editor_api.application_io.as_ref()?.gpu_executor()?; - Some(Arc::new(executor.create_surface(canvas).ok()?)) -} From c2365d466852a8e5998d63ccd381ffe630d11f3f Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Mon, 24 Nov 2025 14:48:19 +0100 Subject: [PATCH 10/11] Compute logical size from acutal physical sizes --- frontend/src/utility-functions/viewports.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/utility-functions/viewports.ts b/frontend/src/utility-functions/viewports.ts index 27a675cf17..128a746d29 100644 --- a/frontend/src/utility-functions/viewports.ts +++ b/frontend/src/utility-functions/viewports.ts @@ -32,17 +32,17 @@ export function setupViewportResizeObserver(editor: Editor) { physicalHeight = entry.contentBoxSize[0].blockSize * devicePixelRatio; } - // Get logical dimensions from contentBoxSize (these may be fractional pixels) - const logicalWidth = entry.contentBoxSize[0].inlineSize; - const logicalHeight = entry.contentBoxSize[0].blockSize; + // Compute the logical size which corresponds to the physical size + const logicalWidth = physicalWidth / devicePixelRatio; + const logicalHeight = physicalHeight / devicePixelRatio; // Get viewport position const bounds = entry.target.getBoundingClientRect(); const scale = physicalWidth / logicalWidth; - // Send both logical and physical dimensions to the backend // Logical dimensions are used for CSS/SVG sizing, physical for GPU textures + // TODO: Consider passing physical sizes as well to eliminate pixel inaccuracies since width and height could be rounded differently editor.handle.updateViewport(bounds.x, bounds.y, logicalWidth, logicalHeight, scale); } }); From a5bb7bdc440563b2b491616d8d5eb6ac8bcfa253 Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Mon, 24 Nov 2025 14:07:10 +0000 Subject: [PATCH 11/11] review fixup --- frontend/src/utility-functions/viewports.ts | 3 +-- node-graph/nodes/gstd/src/render_node.rs | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/frontend/src/utility-functions/viewports.ts b/frontend/src/utility-functions/viewports.ts index 128a746d29..8323d4b149 100644 --- a/frontend/src/utility-functions/viewports.ts +++ b/frontend/src/utility-functions/viewports.ts @@ -39,10 +39,9 @@ export function setupViewportResizeObserver(editor: Editor) { // Get viewport position const bounds = entry.target.getBoundingClientRect(); + // TODO: Consider passing physical sizes as well to eliminate pixel inaccuracies since width and height could be rounded differently const scale = physicalWidth / logicalWidth; - // Logical dimensions are used for CSS/SVG sizing, physical for GPU textures - // TODO: Consider passing physical sizes as well to eliminate pixel inaccuracies since width and height could be rounded differently editor.handle.updateViewport(bounds.x, bounds.y, logicalWidth, logicalHeight, scale); } }); diff --git a/node-graph/nodes/gstd/src/render_node.rs b/node-graph/nodes/gstd/src/render_node.rs index ceac0830c6..462ee85985 100644 --- a/node-graph/nodes/gstd/src/render_node.rs +++ b/node-graph/nodes/gstd/src/render_node.rs @@ -192,7 +192,6 @@ async fn render<'a: 'n>(ctx: impl Ctx + ExtractFootprint + ExtractVarArgs, edito background = Color::WHITE; } - // Always render to texture (unified path for both WASM and desktop) let texture = exec .render_vello_scene_to_texture(&scene, physical_resolution, context, background) .await