diff --git a/node-graph/libraries/rendering/src/renderer.rs b/node-graph/libraries/rendering/src/renderer.rs index a7804be3a9..6b23eb940a 100644 --- a/node-graph/libraries/rendering/src/renderer.rs +++ b/node-graph/libraries/rendering/src/renderer.rs @@ -213,8 +213,6 @@ pub enum RenderOutputType { pub struct RenderParams { pub render_mode: RenderMode, pub footprint: Footprint, - /// Ratio of physical pixels to logical pixels. `scale := physical_pixels / logical_pixels` - /// Ignored when rendering to SVG. #[cache_hash(skip)] pub scale: f64, pub render_output_type: RenderOutputType, diff --git a/node-graph/nodes/gstd/src/render_background.rs b/node-graph/nodes/gstd/src/render_background.rs index 892f265d5a..8b239a47f1 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(), @@ -44,7 +44,7 @@ async fn render_background<'a: 'n>( }) .await; - RenderOutputType::Texture(blended.into()) + RenderOutputType::Texture(blended) } RenderOutputType::Svg { svg: foreground_svg, @@ -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 b289ee8db7..43e84aab2b 100644 --- a/node-graph/nodes/gstd/src/render_cache.rs +++ b/node-graph/nodes/gstd/src/render_cache.rs @@ -37,7 +37,6 @@ pub struct CachedRegion { pub struct CacheKey { pub max_region_area: u32, pub render_mode_hash: u64, - pub device_scale: u64, pub zoom: u64, pub rotation: u64, pub for_export: bool, @@ -55,7 +54,6 @@ impl CacheKey { fn new( max_region_area: u32, render_mode_hash: u64, - device_scale: f64, zoom: f64, rotation: f64, for_export: bool, @@ -81,7 +79,6 @@ impl CacheKey { Self { max_region_area, render_mode_hash, - device_scale: device_scale.to_bits(), zoom: zoom.to_bits(), rotation: quantized_rotation.to_bits(), for_export, @@ -345,12 +342,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 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 +356,6 @@ pub async fn render_output_cache<'a: 'n>( let cache_key = CacheKey::new( max_region_area, render_params.render_mode as u64, - device_scale, zoom, rotation, render_params.for_export, @@ -381,16 +375,7 @@ pub async fn render_output_cache<'a: 'n>( if missing_region.tiles.is_empty() { continue; } - let region = render_missing_region( - missing_region, - |ctx| data.eval(ctx), - ctx.clone(), - render_params, - &footprint.transform, - &viewport_origin_offset, - device_scale, - ) - .await; + let region = render_missing_region(missing_region, |ctx| data.eval(ctx), ctx.clone(), render_params, &footprint.transform, &device_origin_offset).await; new_regions.push(region); } @@ -406,10 +391,11 @@ 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 combined_metadata = composite_cached_regions(&all_regions, &output_texture, &device_origin_offset, &footprint.transform, executor); RenderOutput { - data: RenderOutputType::Texture(output_texture.into()), + data: RenderOutputType::Texture(output_texture), metadata: combined_metadata, } } @@ -421,7 +407,6 @@ async fn render_missing_region( render_params: &RenderParams, viewport_transform: &DAffine2, viewport_origin_offset: &DVec2, - device_scale: f64, ) -> CachedRegion where F: Fn(Context<'static>) -> Fut, @@ -433,7 +418,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, diff --git a/node-graph/nodes/gstd/src/render_node.rs b/node-graph/nodes/gstd/src/render_node.rs index c0eda33a0b..dcd65a368a 100644 --- a/node-graph/nodes/gstd/src/render_node.rs +++ b/node-graph/nodes/gstd/src/render_node.rs @@ -93,10 +93,11 @@ async fn render<'a: 'n>( 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 logical_transform = glam::DAffine2::from_scale(glam::DVec2::splat(1.0 / render_params.scale)) * footprint.transform; + let logical_resolution = 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()); @@ -108,12 +109,8 @@ 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_vello = vello::kurbo::Affine::new(footprint_transform.to_cols_array()); + let footprint_transform_vello = vello::kurbo::Affine::new(footprint.transform.to_cols_array()); let mut transformed_scene = vello::Scene::new(); transformed_scene.append(scene, Some(footprint_transform_vello)); @@ -126,7 +123,7 @@ async fn render<'a: 'n>( // the result is `-INFINITY`, which the old equality check missed; Vello then rasterized a unit rect with non-finite // vertices, dropping the gradient and tanking performance. `!is_finite()` also covers NaN as a guard against future // code paths where `matrix[0]` could land on `0 * INFINITY`. - let scaled_infinite_transform = vello::kurbo::Affine::scale_non_uniform(physical_resolution.x as f64, physical_resolution.y as f64); + let scaled_infinite_transform = vello::kurbo::Affine::scale_non_uniform(footprint.resolution.x as f64, footprint.resolution.y as f64); for transform in transformed_scene.encoding_mut().transforms.iter_mut() { if !transform.matrix[0].is_finite() { *transform = vello_encoding::Transform::from_kurbo(&scaled_infinite_transform); @@ -135,10 +132,10 @@ async fn render<'a: 'n>( let texture = executor .expect("GPU executor not available") - .render_vello_scene(&transformed_scene, physical_resolution, context, None) + .render_vello_scene(&transformed_scene, footprint.resolution, context, None) .await .expect("Failed to render Vello scene"); - RenderOutputType::Texture(texture.into()) + RenderOutputType::Texture(texture) } _ => unreachable!("Render node did not receive its requested data type"), }; @@ -152,19 +149,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() }; @@ -176,5 +177,8 @@ async fn create_context<'a: 'n>( .with_vararg(Box::new(render_params)) .into_context(); - data.eval(ctx).await + let mut result = data.eval(ctx).await; + + result.metadata.apply_transform(glam::DAffine2::from_scale(glam::DVec2::splat(1. / render_config.scale))); + result } diff --git a/node-graph/nodes/gstd/src/render_pixel_preview.rs b/node-graph/nodes/gstd/src/render_pixel_preview.rs index f01ad47eba..f668ed8a37 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 { @@ -67,9 +69,7 @@ pub async fn render_pixel_preview<'a: 'n>( result.data = RenderOutputType::Texture(resampled); - result - .metadata - .apply_transform(footprint.transform * DAffine2::from_translation(upstream_min) * DAffine2::from_scale(DVec2::splat(physical_scale))); + result.metadata.apply_transform(footprint.transform * DAffine2::from_translation(upstream_min)); result }