From 40486dcdbe7b8ea980e5d9d647c801386ecdd105 Mon Sep 17 00:00:00 2001 From: Timon Date: Wed, 24 Jun 2026 16:35:13 +0000 Subject: [PATCH 1/2] Fix footprint scale --- node-graph/nodes/gstd/src/render_node.rs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/node-graph/nodes/gstd/src/render_node.rs b/node-graph/nodes/gstd/src/render_node.rs index c0eda33a0b..472bb293b6 100644 --- a/node-graph/nodes/gstd/src/render_node.rs +++ b/node-graph/nodes/gstd/src/render_node.rs @@ -1,6 +1,6 @@ use core_types::list::List; use core_types::transform::{Footprint, Transform}; -use core_types::{CloneVarArgs, ExtractAll, ExtractVarArgs}; +use core_types::{CloneVarArgs, ExtractAll, ExtractVarArgs, InjectFootprint}; use core_types::{Color, Context, Ctx, ExtractFootprint, OwnedContextImpl, WasmNotSend}; use graph_craft::document::value::{RenderOutput, RenderOutputType}; use graphene_application_io::{ExportFormat, RenderConfig}; @@ -24,7 +24,7 @@ pub struct RenderIntermediate { #[node_macro::node(category(""))] async fn render_intermediate<'a: 'n, T: 'static + Render + WasmNotSend + Send + Sync>( - ctx: impl Ctx + ExtractVarArgs + ExtractAll + CloneVarArgs, + ctx: impl Ctx + ExtractVarArgs + ExtractAll + CloneVarArgs + InjectFootprint, #[implementations( Context -> List, Context -> List, @@ -42,7 +42,17 @@ async fn render_intermediate<'a: 'n, T: 'static + Render + WasmNotSend + Send + .downcast_ref::() .expect("Downcasting render params yielded invalid type"); - let ctx = OwnedContextImpl::from(ctx.clone()).into_context(); + let ctx = match &render_params.render_output_type { + RenderOutputTypeRequest::Vello => { + let footprint = *ctx.footprint(); + let physical_footprint = Footprint { + transform: glam::DAffine2::from_scale(glam::DVec2::splat(render_params.scale)) * footprint.transform, + ..footprint + }; + OwnedContextImpl::from(ctx.clone()).with_footprint(physical_footprint).into_context() + } + RenderOutputTypeRequest::Svg => OwnedContextImpl::from(ctx.clone()).into_context(), + }; let data = data.eval(ctx).await; let footprint = Footprint::default(); From cd2121a00c2e5efb640fce25ce86071452d9fc99 Mon Sep 17 00:00:00 2001 From: Timon Date: Wed, 24 Jun 2026 22:58:58 +0000 Subject: [PATCH 2/2] Footprint in physical pixels --- .../nodes/gstd/src/render_background.rs | 7 ++-- node-graph/nodes/gstd/src/render_cache.rs | 21 +++++------ node-graph/nodes/gstd/src/render_node.rs | 36 ++++++++----------- .../nodes/gstd/src/render_pixel_preview.rs | 10 +++--- 4 files changed, 37 insertions(+), 37 deletions(-) diff --git a/node-graph/nodes/gstd/src/render_background.rs b/node-graph/nodes/gstd/src/render_background.rs index 3d3489bb59..ebf4b88478 100644 --- a/node-graph/nodes/gstd/src/render_background.rs +++ b/node-graph/nodes/gstd/src/render_background.rs @@ -34,7 +34,7 @@ async fn render_background<'a: 'n>( let data = match foreground_data { RenderOutputType::Texture(foreground_texture) => { - let doc_to_screen = (glam::DAffine2::from_scale(glam::DVec2::splat(render_params.scale)) * render_params.footprint.transform).as_affine2(); + let doc_to_screen = render_params.footprint.transform.as_affine2(); let blended = pipeline .run::(&CompositeBackgroundArgs { foreground: foreground_texture.as_ref(), @@ -52,6 +52,8 @@ async fn render_background<'a: 'n>( } => { let mut render = SvgRender::new(); + let logical_transform = glam::DAffine2::from_scale(glam::DVec2::splat(1.0 / render_params.scale)) * render_params.footprint.transform; + if render_params.viewport_zoom > 0. { let draw_checkerboard = |render: &mut SvgRender, rect: vello::kurbo::Rect, pattern_origin: glam::DVec2, checker_id_prefix: &str| { let checker_id = format!("{checker_id_prefix}-{}", generate_uuid()); @@ -79,6 +81,7 @@ async fn render_background<'a: 'n>( if render_params.scale > 0. { let logical_resolution = render_params.footprint.resolution.as_dvec2() / render_params.scale; let logical_footprint = Footprint { + transform: logical_transform, resolution: logical_resolution.round().as_uvec2().max(glam::UVec2::ONE), ..render_params.footprint }; @@ -101,7 +104,7 @@ async fn render_background<'a: 'n>( } let logical_resolution = render_params.footprint.resolution.as_dvec2() / render_params.scale; - render.wrap_with_transform(render_params.footprint.transform, Some(logical_resolution)); + render.wrap_with_transform(logical_transform, Some(logical_resolution)); let background = SvgRenderOutput::from(render); assert!(background.svg_defs.is_empty()); diff --git a/node-graph/nodes/gstd/src/render_cache.rs b/node-graph/nodes/gstd/src/render_cache.rs index 30c3325d20..ba755222f4 100644 --- a/node-graph/nodes/gstd/src/render_cache.rs +++ b/node-graph/nodes/gstd/src/render_cache.rs @@ -345,12 +345,10 @@ pub async fn render_output_cache<'a: 'n>( return data.eval(context.into_context()).await; } - let device_scale = render_params.scale; - let zoom = footprint.scale_magnitudes().x; + let zoom = footprint.scale_magnitudes().x / render_params.scale; let rotation = footprint.decompose_rotation(); - let viewport_origin_offset = footprint.transform.translation; - let device_origin_offset = viewport_origin_offset * device_scale; + let device_origin_offset = footprint.transform.translation; let viewport_bounds_device = AxisAlignedBbox { start: -device_origin_offset, end: footprint.resolution.as_dvec2() - device_origin_offset, @@ -361,7 +359,7 @@ pub async fn render_output_cache<'a: 'n>( let cache_key = CacheKey::new( max_region_area, render_params.render_mode as u64, - device_scale, + render_params.scale, zoom, rotation, render_params.for_export, @@ -387,8 +385,8 @@ pub async fn render_output_cache<'a: 'n>( ctx.clone(), render_params, &footprint.transform, - &viewport_origin_offset, - device_scale, + &device_origin_offset, + render_params.scale, ) .await; new_regions.push(region); @@ -406,7 +404,9 @@ pub async fn render_output_cache<'a: 'n>( let executor = executor.expect("GPU executor not available"); let output_texture = executor.request_texture(physical_resolution).await; - let combined_metadata = composite_cached_regions(&all_regions, &output_texture, &device_origin_offset, &footprint.transform, &executor); + + let logical_viewport_transform = DAffine2::from_scale(DVec2::splat(1.0 / render_params.scale)) * footprint.transform; + let combined_metadata = composite_cached_regions(&all_regions, &output_texture, &device_origin_offset, &logical_viewport_transform, executor); RenderOutput { data: RenderOutputType::Texture(output_texture.into()), @@ -433,7 +433,7 @@ where let tile_count = (max_tile - min_tile) + IVec2::ONE; let region_pixel_size = (tile_count * TILE_SIZE as i32).as_uvec2(); - let tile_global_offset = min_tile.as_dvec2() * (TILE_SIZE as f64 / device_scale) + *viewport_origin_offset; + let tile_global_offset = min_tile.as_dvec2() * TILE_SIZE as f64 + *viewport_origin_offset; let region_transform = DAffine2::from_translation(-tile_global_offset) * *viewport_transform; let region_footprint = Footprint { transform: region_transform, @@ -449,7 +449,8 @@ where unreachable!("render_missing_region: expected texture output from Vello render"); }; - let pixel_to_document = region_transform.inverse(); + let logical_region_transform = DAffine2::from_scale(DVec2::splat(1.0 / device_scale)) * region_transform; + let pixel_to_document = logical_region_transform.inverse(); result.metadata.apply_transform(pixel_to_document); let memory_size = (region_pixel_size.x * region_pixel_size.y) as usize * BYTES_PER_PIXEL; diff --git a/node-graph/nodes/gstd/src/render_node.rs b/node-graph/nodes/gstd/src/render_node.rs index 472bb293b6..b388481444 100644 --- a/node-graph/nodes/gstd/src/render_node.rs +++ b/node-graph/nodes/gstd/src/render_node.rs @@ -1,6 +1,6 @@ use core_types::list::List; use core_types::transform::{Footprint, Transform}; -use core_types::{CloneVarArgs, ExtractAll, ExtractVarArgs, InjectFootprint}; +use core_types::{CloneVarArgs, ExtractAll, ExtractVarArgs}; use core_types::{Color, Context, Ctx, ExtractFootprint, OwnedContextImpl, WasmNotSend}; use graph_craft::document::value::{RenderOutput, RenderOutputType}; use graphene_application_io::{ExportFormat, RenderConfig}; @@ -24,7 +24,7 @@ pub struct RenderIntermediate { #[node_macro::node(category(""))] async fn render_intermediate<'a: 'n, T: 'static + Render + WasmNotSend + Send + Sync>( - ctx: impl Ctx + ExtractVarArgs + ExtractAll + CloneVarArgs + InjectFootprint, + ctx: impl Ctx + ExtractVarArgs + ExtractAll + CloneVarArgs, #[implementations( Context -> List, Context -> List, @@ -42,17 +42,7 @@ async fn render_intermediate<'a: 'n, T: 'static + Render + WasmNotSend + Send + .downcast_ref::() .expect("Downcasting render params yielded invalid type"); - let ctx = match &render_params.render_output_type { - RenderOutputTypeRequest::Vello => { - let footprint = *ctx.footprint(); - let physical_footprint = Footprint { - transform: glam::DAffine2::from_scale(glam::DVec2::splat(render_params.scale)) * footprint.transform, - ..footprint - }; - OwnedContextImpl::from(ctx.clone()).with_footprint(physical_footprint).into_context() - } - RenderOutputTypeRequest::Svg => OwnedContextImpl::from(ctx.clone()).into_context(), - }; + let ctx = OwnedContextImpl::from(ctx.clone()).into_context(); let data = data.eval(ctx).await; let footprint = Footprint::default(); @@ -98,15 +88,17 @@ async fn render<'a: 'n>( let mut render_params = render_params.clone(); render_params.footprint = *footprint; + let logical_transform = glam::DAffine2::from_scale(glam::DVec2::splat(1.0 / render_params.scale)) * footprint.transform; + let RenderIntermediate { ty, mut metadata } = data; - metadata.apply_transform(footprint.transform); + metadata.apply_transform(logical_transform); let data = match (render_params.render_output_type, ty) { (RenderOutputTypeRequest::Svg, RenderIntermediateType::Svg(data)) => { let logical_resolution = render_params.footprint.resolution.as_dvec2() / render_params.scale; let mut render = SvgRender::from(data.as_ref()); - render.wrap_with_transform(render_params.footprint.transform, Some(logical_resolution)); + render.wrap_with_transform(logical_transform, Some(logical_resolution)); let output = SvgRenderOutput::from(render); assert!(output.svg_defs.is_empty()); @@ -118,11 +110,9 @@ async fn render<'a: 'n>( } (RenderOutputTypeRequest::Vello, RenderIntermediateType::Vello(data)) => { let (scene, context) = data.as_ref(); - let scale = render_params.scale; let physical_resolution = render_params.footprint.resolution; - let scale_transform = glam::DAffine2::from_scale(glam::DVec2::splat(scale)); - let footprint_transform = scale_transform * render_params.footprint.transform; + let footprint_transform = render_params.footprint.transform; let footprint_transform_vello = vello::kurbo::Affine::new(footprint_transform.to_cols_array()); let mut transformed_scene = vello::Scene::new(); @@ -162,19 +152,23 @@ async fn create_context<'a: 'n>( render_config: RenderConfig, data: impl Node, Output = RenderOutput>, ) -> RenderOutput { - let footprint = render_config.viewport; - let render_output_type = match render_config.export_format { ExportFormat::Svg => RenderOutputTypeRequest::Svg, ExportFormat::Raster => RenderOutputTypeRequest::Vello, }; + let logical_viewport = render_config.viewport; + let footprint = Footprint { + transform: glam::DAffine2::from_scale(glam::DVec2::splat(render_config.scale)) * logical_viewport.transform, + ..logical_viewport + }; + let render_params = RenderParams { render_mode: render_config.render_mode, for_export: render_config.for_export, render_output_type, scale: render_config.scale, - viewport_zoom: footprint.scale_magnitudes().x, + viewport_zoom: logical_viewport.scale_magnitudes().x, ..Default::default() }; diff --git a/node-graph/nodes/gstd/src/render_pixel_preview.rs b/node-graph/nodes/gstd/src/render_pixel_preview.rs index 2cc2964151..5962a8213f 100644 --- a/node-graph/nodes/gstd/src/render_pixel_preview.rs +++ b/node-graph/nodes/gstd/src/render_pixel_preview.rs @@ -21,7 +21,7 @@ pub async fn render_pixel_preview<'a: 'n>( let physical_scale = render_params.scale; let footprint = *ctx.footprint(); - let viewport_zoom = footprint.scale_magnitudes().x * physical_scale; + let viewport_zoom = footprint.scale_magnitudes().x; if render_params.render_mode != RenderMode::PixelPreview || !matches!(render_params.render_output_type, RenderOutputTypeRequest::Vello) || viewport_zoom <= 1. { let context = OwnedContextImpl::from(ctx).into_context(); @@ -32,6 +32,7 @@ pub async fn render_pixel_preview<'a: 'n>( let logical_resolution = physical_resolution.as_dvec2() / physical_scale; let logical_footprint = Footprint { + transform: DAffine2::from_scale(DVec2::splat(1. / physical_scale)) * footprint.transform, resolution: logical_resolution.as_uvec2().max(UVec2::ONE), ..footprint }; @@ -45,7 +46,7 @@ pub async fn render_pixel_preview<'a: 'n>( let upstream_resolution = upstream_size.as_uvec2().max(UVec2::ONE); let upstream_footprint = Footprint { - transform: DAffine2::from_scale(DVec2::splat(1. / physical_scale)) * DAffine2::from_translation(-upstream_min), + transform: DAffine2::from_translation(-upstream_min), resolution: upstream_resolution, quality: footprint.quality, }; @@ -55,7 +56,8 @@ pub async fn render_pixel_preview<'a: 'n>( let RenderOutputType::Texture(ref source_texture) = result.data else { return result }; - let transform = DAffine2::from_translation(-upstream_min) * footprint.transform.inverse() * DAffine2::from_scale(logical_resolution); + let logical_transform = DAffine2::from_scale(DVec2::splat(1. / physical_scale)) * footprint.transform; + let transform = DAffine2::from_translation(-upstream_min) * logical_transform.inverse() * DAffine2::from_scale(logical_resolution); let resampled = pipeline .run::(&PixelPreviewArgs { @@ -69,7 +71,7 @@ pub async fn render_pixel_preview<'a: 'n>( result .metadata - .apply_transform(footprint.transform * DAffine2::from_translation(upstream_min) * DAffine2::from_scale(DVec2::splat(physical_scale))); + .apply_transform(logical_transform * DAffine2::from_translation(upstream_min) * DAffine2::from_scale(DVec2::splat(physical_scale))); result }