diff --git a/desktop/src/render/state.rs b/desktop/src/render/state.rs index 7481cdd916..3cae549e54 100644 --- a/desktop/src/render/state.rs +++ b/desktop/src/render/state.rs @@ -1,8 +1,7 @@ -use std::borrow::Cow; use wgpu::PresentMode; use crate::window::Window; -use crate::wrapper::{TargetTexture, WgpuContext, WgpuExecutor}; +use crate::wrapper::{WgpuContext, WgpuExecutor}; #[derive(derivative::Derivative)] #[derivative(Debug)] @@ -19,7 +18,7 @@ pub(crate) struct RenderState { viewport_scale: [f32; 2], viewport_offset: [f32; 2], viewport_texture: Option>, - overlays_texture: Option, + overlays_texture: Option>, ui_texture: Option, bind_group: Option, #[derivative(Debug = "ignore")] @@ -233,11 +232,17 @@ impl RenderState { return; }; let size = glam::UVec2::new(viewport_texture.width(), viewport_texture.height()); - let result = futures::executor::block_on(self.executor.render_vello_scene_to_target_texture(&scene, size, &Default::default(), &mut self.overlays_texture)); - if let Err(e) = result { - tracing::error!("Error rendering overlays: {:?}", e); - return; + let result = futures::executor::block_on(self.executor.render_vello_scene(&scene, size, &Default::default(), None)); + match result { + Ok(texture) => { + self.overlays_texture = Some(texture); + } + Err(e) => { + self.overlays_texture = None; + tracing::error!("Error rendering overlays: {:?}", e); + } } + self.update_bindgroup(); } @@ -314,11 +319,7 @@ impl RenderState { fn update_bindgroup(&mut self) { self.surface_outdated = true; let viewport_texture_view = self.viewport_texture.as_ref().unwrap_or(&self.transparent_texture).create_view(&wgpu::TextureViewDescriptor::default()); - let overlays_texture_view = self - .overlays_texture - .as_ref() - .map(|target| Cow::Borrowed(target.view())) - .unwrap_or_else(|| Cow::Owned(self.transparent_texture.create_view(&wgpu::TextureViewDescriptor::default()))); + let overlays_texture_view = self.overlays_texture.as_ref().unwrap_or(&self.transparent_texture).create_view(&wgpu::TextureViewDescriptor::default()); let ui_texture_view = self.ui_texture.as_ref().unwrap_or(&self.transparent_texture).create_view(&wgpu::TextureViewDescriptor::default()); let bind_group = self.context.device.create_bind_group(&wgpu::BindGroupDescriptor { @@ -330,7 +331,7 @@ impl RenderState { }, wgpu::BindGroupEntry { binding: 1, - resource: wgpu::BindingResource::TextureView(overlays_texture_view.as_ref()), + resource: wgpu::BindingResource::TextureView(&overlays_texture_view), }, wgpu::BindGroupEntry { binding: 2, diff --git a/desktop/wrapper/src/lib.rs b/desktop/wrapper/src/lib.rs index 2f8c4c49f4..ad83ca4154 100644 --- a/desktop/wrapper/src/lib.rs +++ b/desktop/wrapper/src/lib.rs @@ -5,7 +5,6 @@ use message_dispatcher::DesktopWrapperMessageDispatcher; use messages::{DesktopFrontendMessage, DesktopWrapperMessage}; pub use graphite_editor::consts::{DOUBLE_CLICK_MILLISECONDS, FILE_EXTENSION}; -pub use wgpu_executor::TargetTexture; pub use wgpu_executor::WgpuContext; pub use wgpu_executor::WgpuContextBuilder; pub use wgpu_executor::WgpuExecutor; diff --git a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs index aa1628eae4..3ce54d16b1 100644 --- a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs +++ b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs @@ -4,8 +4,8 @@ use crate::messages::portfolio::document::utility_types::network_interface::Node use crate::messages::prelude::*; use glam::{DAffine2, DVec2}; use graph_craft::document::NodeId; +use graphene_std::Color; use graphene_std::brush::brush_stroke::BrushStroke; -use graphene_std::color::Color; use graphene_std::raster::BlendMode; use graphene_std::raster_types::Image; use graphene_std::subpath::Subpath; diff --git a/editor/src/node_graph_executor.rs b/editor/src/node_graph_executor.rs index 4252bd38ac..0fe3984254 100644 --- a/editor/src/node_graph_executor.rs +++ b/editor/src/node_graph_executor.rs @@ -417,6 +417,7 @@ impl NodeGraphExecutor { click_targets, clip_targets, vector_data, + backgrounds: _, } = render_output.metadata; // Run these update state messages immediately diff --git a/node-graph/interpreted-executor/src/util.rs b/node-graph/interpreted-executor/src/util.rs index ab3b8b4a58..e9877085fd 100644 --- a/node-graph/interpreted-executor/src/util.rs +++ b/node-graph/interpreted-executor/src/util.rs @@ -28,7 +28,7 @@ pub fn wrap_network_in_scope(mut network: NodeNetwork, editor_api: Arc>> = LazyLock::new(|| { - const SIZE: u32 = 16; - const HALF: u32 = 8; - - let mut data = vec![0_u8; (SIZE * SIZE * 4) as usize]; - for y in 0..SIZE { - for x in 0..SIZE { - let is_light = ((x / HALF) + (y / HALF)).is_multiple_of(2); - let value = if is_light { 0xff } else { 0xcc }; - let index = ((y * SIZE + x) * 4) as usize; - data[index] = value; - data[index + 1] = value; - data[index + 2] = value; - data[index + 3] = 0xff; - } - } - - Arc::new(data) -}); - -/// Creates a 16x16 tiling transparency checkerboard brush for Vello. -pub fn checkerboard_brush() -> peniko::Brush { - peniko::Brush::Image(peniko::ImageBrush { - image: peniko::ImageData { - data: peniko::Blob::new(CHECKERBOARD_IMAGE_DATA.clone()), - format: peniko::ImageFormat::Rgba8, - width: 16, - height: 16, - alpha_type: peniko::ImageAlphaType::Alpha, - }, - sampler: peniko::ImageSampler { - x_extend: peniko::Extend::Repeat, - y_extend: peniko::Extend::Repeat, - quality: peniko::ImageQuality::Low, // Nearest-neighbor sampling for crisp edges - alpha: 1., - }, - }) -} - #[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] enum MaskType { @@ -125,15 +86,17 @@ impl SvgRender { pub fn format_svg(&mut self, bounds_min: DVec2, bounds_max: DVec2) { let (x, y) = bounds_min.into(); let (size_x, size_y) = (bounds_max - bounds_min).into(); - let defs = &self.svg_defs; - let svg_header = format!(r#"{defs}"#,); + let svg_header = format!( + r#"{defs}"#, + defs = &self.svg_defs + ); + self.svg_defs = String::new(); self.svg.insert(0, svg_header.into()); self.svg.push("".into()); } /// Wraps the SVG with `...`, which allows for rotation pub fn wrap_with_transform(&mut self, transform: DAffine2, size: Option) { - let defs = &self.svg_defs; let view_box = size .map(|size| format!("viewBox=\"0 0 {} {}\" width=\"{}\" height=\"{}\"", size.x, size.y, size.x, size.y)) .unwrap_or_default(); @@ -141,7 +104,11 @@ impl SvgRender { let matrix = format_transform_matrix(transform); let transform = if matrix.is_empty() { String::new() } else { format!(r#" transform="{matrix}""#) }; - let svg_header = format!(r#"{defs}"#); + let svg_header = format!( + r#"{defs}"#, + defs = &self.svg_defs + ); + self.svg_defs = String::new(); self.svg.insert(0, svg_header.into()); self.svg.push("".into()); } @@ -186,6 +153,34 @@ impl SvgRender { } } +pub struct SvgRenderOutput { + pub svg: String, + pub svg_defs: String, + pub image_data: HashMap>, u64>, +} + +impl From<&SvgRenderOutput> for SvgRender { + fn from(value: &SvgRenderOutput) -> Self { + Self { + svg: vec![value.svg.clone().into()], + svg_defs: value.svg_defs.clone(), + transform: DAffine2::IDENTITY, + image_data: value.image_data.clone(), + indent: 0, + } + } +} + +impl From for SvgRenderOutput { + fn from(val: SvgRender) -> Self { + Self { + svg: val.svg.to_svg_string(), + svg_defs: val.svg_defs, + image_data: val.image_data, + } + } +} + impl Default for SvgRender { fn default() -> Self { Self::new() @@ -215,8 +210,6 @@ pub struct RenderParams { pub scale: f64, pub render_output_type: RenderOutputType, pub thumbnail: bool, - /// Don't render the rectangle for an artboard to allow exporting with a transparent background. - pub hide_artboards: bool, /// Are we exporting pub for_export: bool, /// Are we generating a mask in this render pass? Used to see if fill should be multiplied with alpha. @@ -334,6 +327,7 @@ pub struct RenderMetadata { pub click_targets: HashMap>>, pub clip_targets: HashSet, pub vector_data: HashMap>, + pub backgrounds: Vec, } impl RenderMetadata { @@ -354,6 +348,7 @@ impl RenderMetadata { click_targets, clip_targets, vector_data, + backgrounds, } = self; upstream_footprints.extend(other.upstream_footprints.iter()); local_transforms.extend(other.local_transforms.iter()); @@ -361,9 +356,22 @@ impl RenderMetadata { click_targets.extend(other.click_targets.iter().map(|(k, v)| (*k, v.clone()))); clip_targets.extend(other.clip_targets.iter()); vector_data.extend(other.vector_data.iter().map(|(id, data)| (*id, data.clone()))); + + // TODO: Find a better non O(n^2) way to merge backgrounds + for background in &other.backgrounds { + if !backgrounds.contains(background) { + backgrounds.push(background.clone()); + } + } } } +#[derive(Debug, Default, Clone, PartialEq, DynAny, serde::Serialize, serde::Deserialize)] +pub struct Background { + pub location: DVec2, + pub dimensions: DVec2, +} + // TODO: Rename to "Graphical" pub trait Render: BoundingBox + RenderComplexity { fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams); @@ -526,42 +534,17 @@ impl Render for Table> { let width = dimensions.x.abs(); let height = dimensions.y.abs(); - // Rectangle for the artboard - if !render_params.hide_artboards { - // Transparency checkerboard behind the artboard background (viewport only) - let show_checkerboard = background.alpha() < 1. && render_params.to_canvas(); - if show_checkerboard && render_params.viewport_zoom > 0. { - let checker_id = format!("checkered-artboard-{}", generate_uuid()); - let cell_size = 8. / render_params.viewport_zoom; - let pattern_size = cell_size * 2.; - - // Anchor pattern at this artboard's top-left corner (x, y), not the document origin - let _ = write!( - &mut render.svg_defs, - r##""## - ); - - render.leaf_tag("rect", |attributes| { - attributes.push("x", x.to_string()); - attributes.push("y", y.to_string()); - attributes.push("width", width.to_string()); - attributes.push("height", height.to_string()); - attributes.push("fill", format!("url(#{checker_id})")); - }); + // Background + render.leaf_tag("rect", |attributes| { + attributes.push("fill", format!("#{}", background.to_rgb_hex_srgb_from_gamma())); + if background.a() < 1. { + attributes.push("fill-opacity", ((background.a() * 1000.).round() / 1000.).to_string()); } - - // Background - render.leaf_tag("rect", |attributes| { - attributes.push("fill", format!("#{}", background.to_rgb_hex_srgb_from_gamma())); - if background.a() < 1. { - attributes.push("fill-opacity", ((background.a() * 1000.).round() / 1000.).to_string()); - } - attributes.push("x", x.to_string()); - attributes.push("y", y.to_string()); - attributes.push("width", width.to_string()); - attributes.push("height", height.to_string()); - }); - } + attributes.push("x", x.to_string()); + attributes.push("y", y.to_string()); + attributes.push("width", width.to_string()); + attributes.push("height", height.to_string()); + }); // Artwork render.parent_tag( @@ -607,26 +590,12 @@ impl Render for Table> { let [a, b] = [location, location + dimensions]; let rect = kurbo::Rect::new(a.x.min(b.x), a.y.min(b.y), a.x.max(b.x), a.y.max(b.y)); - // Render background - if !render_params.hide_artboards { - let artboard_transform = kurbo::Affine::new(transform.to_cols_array()); - - // Transparency checkerboard behind the artboard background (viewport only) - let show_checkerboard = background.alpha() < 1. && render_params.to_canvas(); - if show_checkerboard && render_params.viewport_zoom > 0. { - // Anchor pattern at THIS artboard's top-left corner - // brush_transform is an image placement transform: it maps brush pixel coords → shape coords - // scale(1/zoom) sets each brush pixel to 1/zoom document units (constant CSS size after viewport transform) - // then_translate places the brush origin at the artboard corner - let brush_transform = kurbo::Affine::scale(1. / render_params.viewport_zoom).then_translate(kurbo::Vec2::new(rect.x0, rect.y0)); - scene.fill(peniko::Fill::NonZero, artboard_transform, &checkerboard_brush(), Some(brush_transform), &rect); - } + let artboard_transform = kurbo::Affine::new(transform.to_cols_array()); - let color = peniko::Color::new([background.r(), background.g(), background.b(), background.a()]); - scene.push_layer(peniko::Fill::NonZero, peniko::Mix::Normal, 1., artboard_transform, &rect); - scene.fill(peniko::Fill::NonZero, artboard_transform, color, None, &rect); - scene.pop_layer(); - } + let color = peniko::Color::new([background.r(), background.g(), background.b(), background.a()]); + scene.push_layer(peniko::Fill::NonZero, peniko::Mix::Normal, 1., artboard_transform, &rect); + scene.fill(peniko::Fill::NonZero, artboard_transform, color, None, &rect); + scene.pop_layer(); if clip { scene.push_clip_layer(peniko::Fill::NonZero, kurbo::Affine::new(transform.to_cols_array()), &rect); @@ -661,6 +630,8 @@ impl Render for Table> { } } + metadata.backgrounds.push(Background { location, dimensions }); + let mut child_footprint = footprint; child_footprint.transform *= DAffine2::from_translation(location); content.collect_metadata(metadata, child_footprint, None); diff --git a/node-graph/libraries/wgpu-executor/src/background/checker_rect.wgsl b/node-graph/libraries/wgpu-executor/src/background/checker_rect.wgsl new file mode 100644 index 0000000000..79f7163c22 --- /dev/null +++ b/node-graph/libraries/wgpu-executor/src/background/checker_rect.wgsl @@ -0,0 +1,49 @@ +struct CompositeUniforms { + transform_x: vec2, + transform_y: vec2, + transform_translation: vec2, + rect_min: vec2, + rect_max: vec2, + viewport_size: vec2, + pattern_origin: vec2, + checker_size: f32, + _pad: f32, +}; + +@group(0) @binding(0) +var uniforms: CompositeUniforms; + +struct VertexOutput { + @builtin(position) position: vec4, + @location(0) document_position: vec2, +}; + +@vertex +fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput { + let document_corners = array, 6>( + uniforms.rect_min, + vec2(uniforms.rect_max.x, uniforms.rect_min.y), + vec2(uniforms.rect_min.x, uniforms.rect_max.y), + vec2(uniforms.rect_min.x, uniforms.rect_max.y), + vec2(uniforms.rect_max.x, uniforms.rect_min.y), + uniforms.rect_max, + ); + let document_position = document_corners[vertex_index]; + + let transformed = uniforms.transform_x * document_position.x + uniforms.transform_y * document_position.y + uniforms.transform_translation; + let normalized = transformed / uniforms.viewport_size; + let clip = vec2(normalized.x * 2.0 - 1.0, 1.0 - normalized.y * 2.0); + + var out: VertexOutput; + out.position = vec4(clip, 0.0, 1.0); + out.document_position = document_position; + return out; +} + +@fragment +fn fs_main(in: VertexOutput) -> @location(0) vec4 { + let tile = floor((in.document_position - uniforms.pattern_origin) / uniforms.checker_size); + let parity = i32(tile.x + tile.y) & 1; + let luminance = vec3(select(1.0, 0.8, parity == 1)); + return vec4(luminance, 1.0); +} diff --git a/node-graph/libraries/wgpu-executor/src/background/checker_viewport.wgsl b/node-graph/libraries/wgpu-executor/src/background/checker_viewport.wgsl new file mode 100644 index 0000000000..c583efcc38 --- /dev/null +++ b/node-graph/libraries/wgpu-executor/src/background/checker_viewport.wgsl @@ -0,0 +1,45 @@ +struct CompositeUniforms { + transform_x: vec2, + transform_y: vec2, + transform_translation: vec2, + rect_min: vec2, + rect_max: vec2, + viewport_size: vec2, + pattern_origin: vec2, + checker_size: f32, + _pad: f32, +}; + +@group(0) @binding(0) +var uniforms: CompositeUniforms; + +struct VertexOutput { + @builtin(position) position: vec4, + @location(0) document_position: vec2, +}; + +@vertex +fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput { + let positions = array, 3>( + vec2(-1.0, -1.0), + vec2(-1.0, 3.0), + vec2( 3.0, -1.0), + ); + let position = positions[vertex_index]; + + let screen_position = vec2((position.x + 1.0) * 0.5 * uniforms.viewport_size.x, (1.0 - position.y) * 0.5 * uniforms.viewport_size.y); + let document_position = uniforms.transform_x * screen_position.x + uniforms.transform_y * screen_position.y + uniforms.transform_translation; + + var out: VertexOutput; + out.position = vec4(position, 0.0, 1.0); + out.document_position = document_position; + return out; +} + +@fragment +fn fs_main(in: VertexOutput) -> @location(0) vec4 { + let tile = floor((in.document_position - uniforms.pattern_origin) / uniforms.checker_size); + let parity = i32(tile.x + tile.y) & 1; + let luminance = vec3(select(1.0, 0.8, parity == 1)); + return vec4(luminance, 1.0); +} diff --git a/node-graph/libraries/wgpu-executor/src/background/fullscreen.wgsl b/node-graph/libraries/wgpu-executor/src/background/fullscreen.wgsl new file mode 100644 index 0000000000..fc760c927b --- /dev/null +++ b/node-graph/libraries/wgpu-executor/src/background/fullscreen.wgsl @@ -0,0 +1,35 @@ +@group(0) @binding(0) +var foreground_sampler: sampler; + +@group(0) @binding(1) +var foreground_texture: texture_2d; + +struct VertexOutput { + @builtin(position) position: vec4, + @location(0) tex_coord: vec2, +}; + +@vertex +fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput { + let positions = array, 3>( + vec2(-1.0, -1.0), + vec2(-1.0, 3.0), + vec2( 3.0, -1.0), + ); + + let tex_coords = array, 3>( + vec2(0.0, 1.0), + vec2(0.0, -1.0), + vec2(2.0, 1.0), + ); + + var vertex_out: VertexOutput; + vertex_out.position = vec4(positions[vertex_index], 0.0, 1.0); + vertex_out.tex_coord = tex_coords[vertex_index]; + return vertex_out; +} + +@fragment +fn fs_main(fragment_in: VertexOutput) -> @location(0) vec4 { + return textureSample(foreground_texture, foreground_sampler, fragment_in.tex_coord); +} diff --git a/node-graph/libraries/wgpu-executor/src/background/mod.rs b/node-graph/libraries/wgpu-executor/src/background/mod.rs new file mode 100644 index 0000000000..dbc8fcf7ec --- /dev/null +++ b/node-graph/libraries/wgpu-executor/src/background/mod.rs @@ -0,0 +1,344 @@ +use glam::{Affine2, Vec2}; +use wgpu::util::DeviceExt; + +pub struct BackgroundCompositor { + checker_rect_pipeline: wgpu::RenderPipeline, + checker_viewport_pipeline: wgpu::RenderPipeline, + fullscreen_pipeline: wgpu::RenderPipeline, + checker_bind_group_layout: wgpu::BindGroupLayout, + fullscreen_bind_group_layout: wgpu::BindGroupLayout, + sampler: wgpu::Sampler, +} + +impl BackgroundCompositor { + pub fn new(device: &wgpu::Device) -> Self { + let format = wgpu::TextureFormat::Rgba8Unorm; + let checker_rect_shader = device.create_shader_module(wgpu::include_wgsl!("checker_rect.wgsl")); + let checker_viewport_shader = device.create_shader_module(wgpu::include_wgsl!("checker_viewport.wgsl")); + let fullscreen_shader = device.create_shader_module(wgpu::include_wgsl!("fullscreen.wgsl")); + + let checker_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("background_checker_bind_group_layout"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX_FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }], + }); + + let checker_rect_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("background_checker_rect_pipeline_layout"), + bind_group_layouts: &[&checker_bind_group_layout], + immediate_size: 0, + }); + + let checker_viewport_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("background_checker_viewport_pipeline_layout"), + bind_group_layouts: &[&checker_bind_group_layout], + immediate_size: 0, + }); + + let fullscreen_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("background_fullscreen_bind_group_layout"), + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + ], + }); + + let fullscreen_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("background_fullscreen_pipeline_layout"), + bind_group_layouts: &[&fullscreen_bind_group_layout], + immediate_size: 0, + }); + + let checker_rect_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("background_checker_rect_pipeline"), + layout: Some(&checker_rect_pipeline_layout), + vertex: wgpu::VertexState { + module: &checker_rect_shader, + entry_point: Some("vs_main"), + compilation_options: Default::default(), + buffers: &[], + }, + fragment: Some(wgpu::FragmentState { + module: &checker_rect_shader, + entry_point: Some("fs_main"), + compilation_options: Default::default(), + targets: &[Some(wgpu::ColorTargetState { + format, + blend: None, + write_mask: wgpu::ColorWrites::ALL, + })], + }), + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + ..Default::default() + }, + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview_mask: None, + cache: None, + }); + + let checker_viewport_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("background_checker_viewport_pipeline"), + layout: Some(&checker_viewport_pipeline_layout), + vertex: wgpu::VertexState { + module: &checker_viewport_shader, + entry_point: Some("vs_main"), + compilation_options: Default::default(), + buffers: &[], + }, + fragment: Some(wgpu::FragmentState { + module: &checker_viewport_shader, + entry_point: Some("fs_main"), + compilation_options: Default::default(), + targets: &[Some(wgpu::ColorTargetState { + format, + blend: None, + write_mask: wgpu::ColorWrites::ALL, + })], + }), + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + ..Default::default() + }, + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview_mask: None, + cache: None, + }); + + let fullscreen_blend = wgpu::BlendState { + color: wgpu::BlendComponent { + src_factor: wgpu::BlendFactor::SrcAlpha, + dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, + operation: wgpu::BlendOperation::Add, + }, + alpha: wgpu::BlendComponent { + src_factor: wgpu::BlendFactor::One, + dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, + operation: wgpu::BlendOperation::Add, + }, + }; + + let fullscreen_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("background_fullscreen_pipeline"), + layout: Some(&fullscreen_pipeline_layout), + vertex: wgpu::VertexState { + module: &fullscreen_shader, + entry_point: Some("vs_main"), + compilation_options: Default::default(), + buffers: &[], + }, + fragment: Some(wgpu::FragmentState { + module: &fullscreen_shader, + entry_point: Some("fs_main"), + compilation_options: Default::default(), + targets: &[Some(wgpu::ColorTargetState { + format, + blend: Some(fullscreen_blend), + write_mask: wgpu::ColorWrites::ALL, + })], + }), + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + ..Default::default() + }, + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview_mask: None, + cache: None, + }); + + let sampler = device.create_sampler(&wgpu::SamplerDescriptor { + label: Some("background_fullscreen_sampler"), + mag_filter: wgpu::FilterMode::Linear, + min_filter: wgpu::FilterMode::Linear, + mipmap_filter: wgpu::MipmapFilterMode::Nearest, + ..Default::default() + }); + + Self { + checker_rect_pipeline, + checker_viewport_pipeline, + fullscreen_pipeline, + checker_bind_group_layout, + fullscreen_bind_group_layout, + sampler, + } + } + + pub fn composite(&self, context: &crate::WgpuContext, foreground: &wgpu::Texture, output: &wgpu::Texture, backgrounds: &[rendering::Background], document_to_screen: Affine2, zoom: f32) { + if zoom <= 0.0 { + return; + } + + let device = &context.device; + let queue = &context.queue; + + let checker_size_doc = 8.0 / zoom; + let screen_to_document = document_to_screen.inverse(); + let viewport_size = output.size(); + let viewport_size = Vec2::new(viewport_size.width as f32, viewport_size.height as f32); + + let output_view = output.create_view(&wgpu::TextureViewDescriptor::default()); + let foreground_view = foreground.create_view(&wgpu::TextureViewDescriptor::default()); + + let checker_draws = if backgrounds.is_empty() { + vec![( + 3, + self.create_checker_bind_group(device, CompositeUniforms::fullscreen(viewport_size, screen_to_document, checker_size_doc)), + )] + } else { + backgrounds + .iter() + .filter_map(|background| { + let a = background.location.as_vec2(); + let b = (background.location + background.dimensions).as_vec2(); + + let min = a.min(b); + let max = a.max(b); + + if max.x <= min.x || max.y <= min.y { + return None; + } + + let uniforms = CompositeUniforms::rect(min, max, document_to_screen, viewport_size, checker_size_doc); + Some((6, self.create_checker_bind_group(device, uniforms))) + }) + .collect() + }; + + let fullscreen_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("background_fullscreen_bind_group"), + layout: &self.fullscreen_bind_group_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::Sampler(&self.sampler), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::TextureView(&foreground_view), + }, + ], + }); + + let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("background_encoder") }); + + { + let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("background_pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &output_view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT), + store: wgpu::StoreOp::Store, + }, + depth_slice: None, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + multiview_mask: None, + }); + + if backgrounds.is_empty() { + pass.set_pipeline(&self.checker_viewport_pipeline); + for (vertex_count, bind_group) in &checker_draws { + pass.set_bind_group(0, bind_group, &[]); + pass.draw(0..*vertex_count, 0..1); + } + } else { + pass.set_pipeline(&self.checker_rect_pipeline); + for (vertex_count, bind_group) in &checker_draws { + pass.set_bind_group(0, bind_group, &[]); + pass.draw(0..*vertex_count, 0..1); + } + } + + pass.set_pipeline(&self.fullscreen_pipeline); + pass.set_bind_group(0, &fullscreen_bind_group, &[]); + pass.draw(0..3, 0..1); + } + + queue.submit(std::iter::once(encoder.finish())); + } + + fn create_checker_bind_group(&self, device: &wgpu::Device, uniforms: CompositeUniforms) -> wgpu::BindGroup { + let buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("background_checker_uniforms"), + contents: bytemuck::bytes_of(&uniforms), + usage: wgpu::BufferUsages::UNIFORM, + }); + + device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("background_checker_bind_group"), + layout: &self.checker_bind_group_layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: buffer.as_entire_binding(), + }], + }) + } +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, bytemuck::Pod, bytemuck::Zeroable)] +struct CompositeUniforms { + transform_x: [f32; 2], + transform_y: [f32; 2], + transform_translation: [f32; 2], + rect_min: [f32; 2], + rect_max: [f32; 2], + viewport_size: [f32; 2], + pattern_origin: [f32; 2], + checker_size: f32, + _pad: f32, +} + +impl CompositeUniforms { + fn fullscreen(viewport_size: Vec2, screen_to_document: Affine2, checker_size_doc: f32) -> Self { + Self::new(screen_to_document, Vec2::ZERO, Vec2::ZERO, viewport_size, Vec2::ZERO, checker_size_doc) + } + + fn rect(rect_min: Vec2, rect_max: Vec2, document_to_screen: Affine2, viewport_size: Vec2, checker_size_doc: f32) -> Self { + Self::new(document_to_screen, rect_min, rect_max, viewport_size, rect_min, checker_size_doc) + } + + fn new(transform: Affine2, rect_min: Vec2, rect_max: Vec2, viewport_size: Vec2, pattern_origin: Vec2, checker_size: f32) -> Self { + Self { + transform_x: transform.matrix2.x_axis.to_array(), + transform_y: transform.matrix2.y_axis.to_array(), + transform_translation: transform.translation.to_array(), + rect_min: rect_min.to_array(), + rect_max: rect_max.to_array(), + viewport_size: viewport_size.to_array(), + pattern_origin: pattern_origin.to_array(), + checker_size, + _pad: 0., + } + } +} diff --git a/node-graph/libraries/wgpu-executor/src/lib.rs b/node-graph/libraries/wgpu-executor/src/lib.rs index 7cd413c1cb..9395ec1bf1 100644 --- a/node-graph/libraries/wgpu-executor/src/lib.rs +++ b/node-graph/libraries/wgpu-executor/src/lib.rs @@ -1,13 +1,20 @@ +mod background; // TODO: Think about where to place this. Likely inlined in the node. Requires refactor of wgpu pipline usage. mod context; mod resample; pub mod shader_runtime; +mod texture_cache; pub mod texture_conversion; +use std::sync::Arc; + +use crate::background::BackgroundCompositor; use crate::resample::Resampler; use crate::shader_runtime::ShaderRuntime; +use crate::texture_cache::TextureCache; use anyhow::Result; +use core_types::Color; use futures::lock::Mutex; -use glam::UVec2; +use glam::{Affine2, UVec2}; use graphene_application_io::{ApplicationIo, EditorApi}; use vello::{AaConfig, AaSupport, RenderParams, Renderer, RendererOptions, Scene}; use wgpu::{Origin3d, TextureAspect}; @@ -18,11 +25,15 @@ pub use rendering::RenderContext; pub use wgpu::Backends as WgpuBackends; pub use wgpu::Features as WgpuFeatures; +const TEXTURE_CACHE_SIZE: u64 = 256 * 1024 * 1024; // 256 MiB + #[derive(dyn_any::DynAny)] pub struct WgpuExecutor { pub context: WgpuContext, + texture_cache: Mutex, vello_renderer: Mutex, resampler: Resampler, + background_compositor: BackgroundCompositor, pub shader_runtime: ShaderRuntime, } @@ -38,105 +49,55 @@ impl<'a, T: ApplicationIo> From<&'a EditorApi> for & } } -#[derive(Clone, Debug)] -pub struct TargetTexture { - texture: wgpu::Texture, - view: wgpu::TextureView, - size: UVec2, -} - -impl TargetTexture { - /// Creates a new TargetTexture with the specified size. - pub fn new(device: &wgpu::Device, size: UVec2) -> Self { - let size = size.max(UVec2::ONE); - let texture = device.create_texture(&wgpu::TextureDescriptor { - label: None, - size: wgpu::Extent3d { - width: size.x, - height: size.y, - depth_or_array_layers: 1, - }, - mip_level_count: 1, - sample_count: 1, - dimension: wgpu::TextureDimension::D2, - usage: wgpu::TextureUsages::STORAGE_BINDING | wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_SRC, - format: VELLO_SURFACE_FORMAT, - view_formats: &[], - }); - let view = texture.create_view(&wgpu::TextureViewDescriptor::default()); - - Self { texture, view, size } - } - - /// Ensures the texture has the specified size, creating a new one if needed. - /// This allows reusing the same texture across frames when the size hasn't changed. - pub fn ensure_size(&mut self, device: &wgpu::Device, size: UVec2) { - let size = size.max(UVec2::ONE); - if self.size == size { - return; +impl WgpuExecutor { + pub async fn render_vello_scene(&self, scene: &Scene, size: UVec2, context: &RenderContext, background: Option) -> Result> { + let texture = self.request_texture(size).await; + + let texture_view = texture.create_view(&wgpu::TextureViewDescriptor::default()); + + let [r, g, b, a] = background.unwrap_or(Color::TRANSPARENT).to_rgba8(); + let render_params = RenderParams { + base_color: vello::peniko::Color::from_rgba8(r, g, b, a), + width: size.x, + height: size.y, + antialiasing_method: AaConfig::Msaa16, + }; + + { + let mut renderer = self.vello_renderer.lock().await; + for (image_brush, texture) in context.resource_overrides.iter() { + let texture_view = wgpu::TexelCopyTextureInfoBase { + texture: texture.clone(), + mip_level: 0, + origin: Origin3d::ZERO, + aspect: TextureAspect::All, + }; + renderer.override_image(&image_brush.image, Some(texture_view)); + } + renderer.render_to_texture(&self.context.device, &self.context.queue, scene, &texture_view, &render_params)?; + for (image_brush, _) in context.resource_overrides.iter() { + renderer.override_image(&image_brush.image, None); + } } - *self = Self::new(device, size); - } - - /// Returns a reference to the texture view for rendering. - pub fn view(&self) -> &wgpu::TextureView { - &self.view + Ok(texture) } - /// Returns a reference to the underlying texture. - pub fn texture(&self) -> &wgpu::Texture { - &self.texture + pub async fn resample_texture(&self, source: &wgpu::Texture, size: UVec2, transform: &glam::DAffine2) -> Arc { + let out = self.request_texture(size).await; + self.resampler.resample(&self.context, source, transform, &out); + out } -} - -const VELLO_SURFACE_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm; - -impl WgpuExecutor { - pub async fn render_vello_scene_to_texture(&self, scene: &Scene, size: UVec2, context: &RenderContext) -> Result { - let mut output = None; - self.render_vello_scene_to_target_texture(scene, size, context, &mut output).await?; - Ok(output.unwrap().texture) - } - - pub async fn render_vello_scene_to_target_texture(&self, scene: &Scene, size: UVec2, context: &RenderContext, output: &mut Option) -> Result<()> { - // Initialize (lazily) if this is the first call - if output.is_none() { - *output = Some(TargetTexture::new(&self.context.device, size)); - } - if let Some(target_texture) = output.as_mut() { - target_texture.ensure_size(&self.context.device, size); - - let render_params = RenderParams { - base_color: vello::peniko::Color::from_rgba8(0, 0, 0, 0), - width: size.x, - height: size.y, - antialiasing_method: AaConfig::Msaa16, - }; - - { - let mut renderer = self.vello_renderer.lock().await; - for (image_brush, texture) in context.resource_overrides.iter() { - let texture_view = wgpu::TexelCopyTextureInfoBase { - texture: texture.clone(), - mip_level: 0, - origin: Origin3d::ZERO, - aspect: TextureAspect::All, - }; - renderer.override_image(&image_brush.image, Some(texture_view)); - } - renderer.render_to_texture(&self.context.device, &self.context.queue, scene, target_texture.view(), &render_params)?; - for (image_brush, _) in context.resource_overrides.iter() { - renderer.override_image(&image_brush.image, None); - } - } - } - Ok(()) + pub async fn composite_background(&self, foreground: &wgpu::Texture, backgrounds: &[rendering::Background], document_to_screen: Affine2, zoom: f32) -> Arc { + let size = foreground.size(); + let output = self.request_texture(UVec2::new(size.width, size.height)).await; + self.background_compositor.composite(&self.context, foreground, &output, backgrounds, document_to_screen, zoom); + output } - pub fn resample_texture(&self, source: &wgpu::Texture, target_size: UVec2, transform: &glam::DAffine2) -> wgpu::Texture { - self.resampler.resample(&self.context, source, target_size, transform) + pub async fn request_texture(&self, size: UVec2) -> Arc { + self.texture_cache.lock().await.request_texture(&self.context.device, size) } } @@ -158,13 +119,19 @@ impl WgpuExecutor { .map_err(|e| anyhow::anyhow!("Failed to create Vello renderer: {:?}", e)) .ok()?; + let texture_cache = TextureCache::new(TEXTURE_CACHE_SIZE); + let resampler = Resampler::new(&context.device); + let background_compositor = BackgroundCompositor::new(&context.device); + let shader_runtime = ShaderRuntime::new(&context); Some(Self { - shader_runtime: ShaderRuntime::new(&context), context, - resampler, + texture_cache: texture_cache.into(), vello_renderer: vello_renderer.into(), + resampler, + background_compositor, + shader_runtime, }) } } diff --git a/node-graph/libraries/wgpu-executor/src/resample.rs b/node-graph/libraries/wgpu-executor/src/resample.rs index e91cf49e5a..fda60d8d5e 100644 --- a/node-graph/libraries/wgpu-executor/src/resample.rs +++ b/node-graph/libraries/wgpu-executor/src/resample.rs @@ -1,5 +1,5 @@ use crate::WgpuContext; -use glam::{DAffine2, UVec2, Vec2}; +use glam::{DAffine2, Vec2}; pub struct Resampler { pipeline: wgpu::RenderPipeline, @@ -74,29 +74,11 @@ impl Resampler { Resampler { pipeline, bind_group_layout } } - pub fn resample(&self, context: &WgpuContext, source: &wgpu::Texture, target_size: UVec2, transform: &DAffine2) -> wgpu::Texture { - let device = &context.device; - let queue = &context.queue; - - let output_texture = device.create_texture(&wgpu::TextureDescriptor { - label: Some("resample_output"), - size: wgpu::Extent3d { - width: target_size.x.max(1), - height: target_size.y.max(1), - depth_or_array_layers: 1, - }, - mip_level_count: 1, - sample_count: 1, - dimension: wgpu::TextureDimension::D2, - format: wgpu::TextureFormat::Rgba8Unorm, - usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC | wgpu::TextureUsages::TEXTURE_BINDING, - view_formats: &[], - }); - + pub fn resample(&self, context: &WgpuContext, source: &wgpu::Texture, transform: &DAffine2, output: &wgpu::Texture) { let source_view = source.create_view(&wgpu::TextureViewDescriptor::default()); - let output_view = output_texture.create_view(&wgpu::TextureViewDescriptor::default()); + let output_view = output.create_view(&wgpu::TextureViewDescriptor::default()); - let params_buffer = device.create_buffer(&wgpu::BufferDescriptor { + let params_buffer = context.device.create_buffer(&wgpu::BufferDescriptor { label: Some("resample_params"), size: 32, usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, @@ -104,9 +86,9 @@ impl Resampler { }); let params_data = [transform.matrix2.x_axis.as_vec2(), transform.matrix2.y_axis.as_vec2(), transform.translation.as_vec2(), Vec2::ZERO]; - queue.write_buffer(¶ms_buffer, 0, bytemuck::cast_slice(¶ms_data)); + context.queue.write_buffer(¶ms_buffer, 0, bytemuck::cast_slice(¶ms_data)); - let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + let bind_group = context.device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("resample_bind_group"), layout: &self.bind_group_layout, entries: &[ @@ -121,7 +103,7 @@ impl Resampler { ], }); - let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("resample_encoder") }); + let mut encoder = context.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("resample_encoder") }); { let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { @@ -143,8 +125,6 @@ impl Resampler { render_pass.draw(0..3, 0..1); } - queue.submit([encoder.finish()]); - - output_texture + context.queue.submit([encoder.finish()]); } } diff --git a/node-graph/libraries/wgpu-executor/src/texture_cache.rs b/node-graph/libraries/wgpu-executor/src/texture_cache.rs new file mode 100644 index 0000000000..4e3fad2178 --- /dev/null +++ b/node-graph/libraries/wgpu-executor/src/texture_cache.rs @@ -0,0 +1,95 @@ +use glam::UVec2; +use std::collections::VecDeque; +use std::sync::Arc; + +pub(crate) struct TextureCache { + /// Always sorted oldest-first by insertion/last-use order. + textures: VecDeque>, + max_free_bytes: u64, +} + +impl TextureCache { + pub fn new(max_free_bytes: u64) -> Self { + Self { + textures: VecDeque::new(), + max_free_bytes, + } + } + + pub fn request_texture(&mut self, device: &wgpu::Device, size: UVec2) -> Arc { + let size = size.max(UVec2::ONE); + + if let Some(pos) = self + .textures + .iter() + .position(|texture| UVec2::new(texture.width(), texture.height()) == size && Arc::strong_count(texture) == 1) + { + let entry = self.textures.remove(pos).unwrap(); + let texture = entry.clone(); + self.textures.push_back(entry); + return texture; + } + + let incoming_bytes = size.x as u64 * size.y as u64 * 4; + self.evict_until_fits(incoming_bytes); + + let texture = Arc::new(device.create_texture(&wgpu::TextureDescriptor { + label: Some(&format!("cached_texture_{}x{}", size.x, size.y)), + size: wgpu::Extent3d { + width: size.x, + height: size.y, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Rgba8Unorm, + usage: wgpu::TextureUsages::COPY_SRC | wgpu::TextureUsages::COPY_DST | wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::STORAGE_BINDING | wgpu::TextureUsages::RENDER_ATTACHMENT, + view_formats: &[], + })); + + self.textures.push_back(texture.clone()); + + texture + } + + fn total_free_bytes(&self) -> u64 { + self.textures + .iter() + .filter(|texture| Arc::strong_count(texture) == 1) + .map(|texture| texture.memory_size_estimate()) + .sum() + } + + fn evict_until_fits(&mut self, incoming_bytes: u64) { + let mut free_bytes = self.total_free_bytes(); + let max_free_bytes = self.max_free_bytes; + + if free_bytes + incoming_bytes <= max_free_bytes { + return; + } + + self.textures.retain(|texture| { + if free_bytes + incoming_bytes <= max_free_bytes { + return true; + } + if Arc::strong_count(texture) == 1 { + free_bytes -= texture.memory_size_estimate(); + texture.destroy(); + false + } else { + true + } + }); + } +} + +trait TextureMemoryCostEstimateExt { + fn memory_size_estimate(&self) -> u64; +} + +impl TextureMemoryCostEstimateExt for wgpu::Texture { + fn memory_size_estimate(&self) -> u64 { + self.width() as u64 * self.height() as u64 * 4 + } +} diff --git a/node-graph/nodes/gstd/src/pixel_preview.rs b/node-graph/nodes/gstd/src/pixel_preview.rs index 266ff7de93..d27b0cb8a7 100644 --- a/node-graph/nodes/gstd/src/pixel_preview.rs +++ b/node-graph/nodes/gstd/src/pixel_preview.rs @@ -59,7 +59,7 @@ pub async fn pixel_preview<'a: 'n>( let transform = DAffine2::from_translation(-upstream_min) * footprint.transform.inverse() * DAffine2::from_scale(logical_resolution); let exec = editor_api.application_io.as_ref().unwrap().gpu_executor().unwrap(); - let resampled = exec.resample_texture(source_texture.as_ref(), physical_resolution, &transform); + let resampled = exec.resample_texture(source_texture.as_ref(), physical_resolution, &transform).await; result.data = RenderOutputType::Texture(resampled.into()); diff --git a/node-graph/nodes/gstd/src/render_cache.rs b/node-graph/nodes/gstd/src/render_cache.rs index 434c2c8ebb..dd06fbab2f 100644 --- a/node-graph/nodes/gstd/src/render_cache.rs +++ b/node-graph/nodes/gstd/src/render_cache.rs @@ -6,7 +6,7 @@ use core_types::{CloneVarArgs, Context, Ctx, ExtractAll, ExtractAnimationTime, E use glam::{DAffine2, DVec2, IVec2, UVec2}; use graph_craft::application_io::PlatformEditorApi; use graph_craft::document::value::RenderOutput; -use graphene_application_io::ApplicationIo; +use graphene_application_io::{ApplicationIo, ImageTexture}; use rendering::{RenderOutputType as RenderOutputTypeRequest, RenderParams}; use std::collections::HashSet; use std::hash::Hash; @@ -26,7 +26,7 @@ pub struct TileCoord { #[derive(Debug, Clone)] pub struct CachedRegion { - pub texture: wgpu::Texture, + pub texture: ImageTexture, pub texture_size: UVec2, pub tiles: Vec, pub metadata: rendering::RenderMetadata, @@ -41,7 +41,6 @@ pub struct CacheKey { pub device_scale: u64, pub zoom: u64, pub rotation: u64, - pub hide_artboards: bool, pub for_export: bool, pub for_mask: bool, pub thumbnail: bool, @@ -60,7 +59,6 @@ impl CacheKey { device_scale: f64, zoom: f64, rotation: f64, - hide_artboards: bool, for_export: bool, for_mask: bool, thumbnail: bool, @@ -87,7 +85,6 @@ impl CacheKey { device_scale: device_scale.to_bits(), zoom: zoom.to_bits(), rotation: quantized_rotation.to_bits(), - hide_artboards, for_export, for_mask, thumbnail, @@ -100,23 +97,27 @@ impl CacheKey { } } +#[derive(Clone, Default, dyn_any::DynAny, Debug)] +pub struct TileCache(Arc>); + +impl TileCache { + pub fn query(&self, viewport_bounds: &AxisAlignedBbox, cache_key: &CacheKey, max_region_area: u32) -> CacheQuery { + self.0.lock().unwrap().query(viewport_bounds, cache_key, max_region_area) + } + + pub fn store_regions(&self, regions: Vec) { + self.0.lock().unwrap().store_regions(regions); + } +} + #[derive(Default, Debug)] struct TileCacheImpl { regions: Vec, timestamp: u64, total_memory: usize, cache_key: CacheKey, - texture_cache_resolution: UVec2, - /// Pool of textures of the same size: `texture_cache_resolution`. - /// Reusing textures reduces the wgpu allocation pressure, - /// which is a problem on web since we have to wait for - /// the browser to garbage collect unused textures, eating up memory. - texture_cache: Vec>, } -#[derive(Clone, Default, dyn_any::DynAny, Debug)] -pub struct TileCache(Arc>); - #[derive(Debug, Clone)] pub struct RenderRegion { pub tiles: Vec, @@ -205,7 +206,6 @@ impl TileCacheImpl { while self.total_memory > MAX_CACHE_MEMORY_BYTES && !self.regions.is_empty() { if let Some((oldest_idx, _)) = self.regions.iter().enumerate().min_by_key(|(_, r)| r.last_access) { let removed = self.regions.remove(oldest_idx); - removed.texture.destroy(); self.total_memory = self.total_memory.saturating_sub(removed.memory_size); } else { break; @@ -214,56 +214,9 @@ impl TileCacheImpl { } fn invalidate_all(&mut self) { - for region in &self.regions { - region.texture.destroy(); - } self.regions.clear(); self.total_memory = 0; } - - pub fn request_texture(&mut self, size: UVec2, device: &wgpu::Device) -> Arc { - if self.texture_cache_resolution != size { - self.texture_cache_resolution = size; - self.texture_cache.clear(); - } - self.texture_cache.truncate(5); - for texture in &self.texture_cache { - if Arc::strong_count(texture) == 1 { - return Arc::clone(texture); - } - } - let texture = Arc::new(device.create_texture(&wgpu::TextureDescriptor { - label: Some("viewport_output"), - size: wgpu::Extent3d { - width: size.x, - height: size.y, - depth_or_array_layers: 1, - }, - mip_level_count: 1, - sample_count: 1, - dimension: wgpu::TextureDimension::D2, - format: wgpu::TextureFormat::Rgba8Unorm, - usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_DST | wgpu::TextureUsages::COPY_SRC | wgpu::TextureUsages::TEXTURE_BINDING, - view_formats: &[], - })); - self.texture_cache.push(texture.clone()); - - texture - } -} - -impl TileCache { - pub fn query(&self, viewport_bounds: &AxisAlignedBbox, cache_key: &CacheKey, max_region_area: u32) -> CacheQuery { - self.0.lock().unwrap().query(viewport_bounds, cache_key, max_region_area) - } - - pub fn store_regions(&self, regions: Vec) { - self.0.lock().unwrap().store_regions(regions); - } - - pub fn request_texture(&self, size: UVec2, device: &wgpu::Device) -> Arc { - self.0.lock().unwrap().request_texture(size, device) - } } fn group_into_regions(tiles: &[TileCoord], max_region_area: u32) -> Vec { @@ -411,7 +364,6 @@ pub async fn render_output_cache<'a: 'n>( device_scale, zoom, rotation, - render_params.hide_artboards, render_params.for_export, render_params.for_mask, render_params.thumbnail, @@ -454,10 +406,9 @@ pub async fn render_output_cache<'a: 'n>( let exec = editor_api.application_io.as_ref().unwrap().gpu_executor().unwrap(); - let device = &exec.context.device; - let output_texture = tile_cache.request_texture(physical_resolution, device); + let output_texture = exec.request_texture(physical_resolution).await; - let combined_metadata = composite_cached_regions(&all_regions, output_texture.as_ref(), &device_origin_offset, &footprint.transform, exec); + let combined_metadata = composite_cached_regions(&all_regions, &output_texture, &device_origin_offset, &footprint.transform, exec); RenderOutput { data: RenderOutputType::Texture(output_texture.into()), @@ -496,7 +447,7 @@ where let region_ctx = OwnedContextImpl::from(ctx).with_footprint(region_footprint).with_vararg(Box::new(region_params)).into_context(); let mut result = render_fn(region_ctx).await; - let RenderOutputType::Texture(rendered_texture) = result.data else { + let RenderOutputType::Texture(texture) = result.data else { unreachable!("render_missing_region: expected texture output from Vello render"); }; @@ -506,7 +457,7 @@ where let memory_size = (region_pixel_size.x * region_pixel_size.y) as usize * BYTES_PER_PIXEL; CachedRegion { - texture: rendered_texture.as_ref().clone(), + texture, texture_size: region_pixel_size, tiles: region.tiles.clone(), metadata: result.metadata, @@ -552,7 +503,7 @@ fn composite_cached_regions( if width > 0 && height > 0 { encoder.copy_texture_to_texture( wgpu::TexelCopyTextureInfo { - texture: ®ion.texture, + texture: region.texture.as_ref(), mip_level: 0, origin: wgpu::Origin3d { x: src_x, y: src_y, z: 0 }, aspect: wgpu::TextureAspect::All, diff --git a/node-graph/nodes/gstd/src/render_node.rs b/node-graph/nodes/gstd/src/render_node.rs index cd7454bd59..7d3a291f6c 100644 --- a/node-graph/nodes/gstd/src/render_node.rs +++ b/node-graph/nodes/gstd/src/render_node.rs @@ -7,12 +7,10 @@ pub use graph_craft::application_io::*; use graph_craft::document::value::RenderOutput; pub use graph_craft::document::value::RenderOutputType; use graphene_application_io::{ApplicationIo, ExportFormat, RenderConfig}; -use graphic_types::raster_types::Image; use graphic_types::raster_types::{CPU, Raster}; use graphic_types::{Graphic, Vector}; -use rendering::{Render, RenderOutputType as RenderOutputTypeRequest, RenderParams, RenderSvgSegmentList, SvgRender, checkerboard_brush}; -use rendering::{RenderMetadata, SvgSegment}; -use std::collections::HashMap; +use rendering::{Render, RenderMetadata, RenderOutputType as RenderOutputTypeRequest, RenderParams, SvgRender, SvgRenderOutput}; +use std::fmt::Write; use std::sync::Arc; use vector_types::GradientStops; use wgpu_executor::RenderContext; @@ -20,19 +18,15 @@ use wgpu_executor::RenderContext; // Re-export render_output_cache from render_cache module pub use crate::render_cache::render_output_cache; -/// List of (canvas id, image data) pairs for embedding images as canvases in the final SVG string. -type ImageData = HashMap>, u64>; - #[derive(Clone, dyn_any::DynAny)] pub enum RenderIntermediateType { Vello(Arc<(vello::Scene, RenderContext)>), - Svg(Arc<(String, ImageData, String)>), + Svg(Arc), } #[derive(Clone, dyn_any::DynAny)] pub struct RenderIntermediate { pub(crate) ty: RenderIntermediateType, pub(crate) metadata: RenderMetadata, - pub(crate) contains_artboard: bool, } #[node_macro::node(category(""))] @@ -60,8 +54,6 @@ async fn render_intermediate<'a: 'n, T: 'static + Render + WasmNotSend + Send + let footprint = Footprint::default(); let mut metadata = RenderMetadata::default(); data.collect_metadata(&mut metadata, footprint, None); - let contains_artboard = data.contains_artboard(); - match &render_params.render_output_type { RenderOutputTypeRequest::Vello => { let mut scene = vello::Scene::new(); @@ -72,7 +64,6 @@ async fn render_intermediate<'a: 'n, T: 'static + Render + WasmNotSend + Send + RenderIntermediate { ty: RenderIntermediateType::Vello(Arc::new((scene, context))), metadata, - contains_artboard, } } RenderOutputTypeRequest::Svg => { @@ -81,49 +72,13 @@ async fn render_intermediate<'a: 'n, T: 'static + Render + WasmNotSend + Send + data.render_svg(&mut render, render_params); RenderIntermediate { - ty: RenderIntermediateType::Svg(Arc::new((render.svg.to_svg_string(), render.image_data, render.svg_defs.clone()))), + ty: RenderIntermediateType::Svg(Arc::new(render.into())), metadata, - contains_artboard, } } } } -#[node_macro::node(category(""))] -async fn create_context<'a: 'n>( - // Context injections are defined in the wrap_network_in_scope function - 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 render_params = RenderParams { - render_mode: render_config.render_mode, - hide_artboards: false, - for_export: render_config.for_export, - render_output_type, - footprint: Footprint::default(), - scale: render_config.scale, - viewport_zoom: footprint.scale_magnitudes().x, - ..Default::default() - }; - - let ctx = OwnedContextImpl::default() - .with_footprint(footprint) - .with_real_time(render_config.time.time) - .with_animation_time(render_config.time.animation_time.as_secs_f64()) - .with_pointer_position(render_config.pointer) - .with_vararg(Box::new(render_params)) - .into_context(); - - data.eval(ctx).await -} - #[node_macro::node(category(""))] async fn render<'a: 'n>(ctx: impl Ctx + ExtractFootprint + ExtractVarArgs, editor_api: &'a PlatformEditorApi, data: RenderIntermediate) -> RenderOutput { let footprint = ctx.footprint(); @@ -134,101 +89,39 @@ async fn render<'a: 'n>(ctx: impl Ctx + ExtractFootprint + ExtractVarArgs, edito .expect("Downcasting render params yielded invalid type"); let mut render_params = render_params.clone(); 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; - let RenderIntermediate { ty, mut metadata, contains_artboard } = data; + let RenderIntermediate { ty, mut metadata } = data; metadata.apply_transform(footprint.transform); - let data = match (render_params.render_output_type, &ty) { - (RenderOutputTypeRequest::Svg, RenderIntermediateType::Svg(svg_data)) => { - let mut rendering = SvgRender::new(); + 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; - // Infinite canvas background (no artboards) - if !contains_artboard && !render_params.hide_artboards { - let show_checkerboard = render_params.to_canvas(); - if show_checkerboard && render_params.viewport_zoom > 0. { - // Checkerboard pattern anchored at the document origin, tiling at 8x8 viewport pixels - let checker_id = format!("checkered-canvas-{}", generate_uuid()); - let cell_size = 8. / render_params.viewport_zoom; - let pattern_size = cell_size * 2.; + let mut render = SvgRender::from(data.as_ref()); + render.wrap_with_transform(render_params.footprint.transform, Some(logical_resolution)); - // Compute the axis-aligned bounding box of all four viewport corners in document space, - // which is necessary when the view is rotated so the rect fully covers the visible area - let inverse_transform = footprint.transform.inverse(); - let corners = [ - inverse_transform.transform_point2(glam::DVec2::ZERO), - inverse_transform.transform_point2(glam::DVec2::new(logical_resolution.x, 0.)), - inverse_transform.transform_point2(glam::DVec2::new(0., logical_resolution.y)), - inverse_transform.transform_point2(logical_resolution), - ]; - let bb_min = corners.iter().fold(glam::DVec2::MAX, |acc, &c| acc.min(c)); - let bb_max = corners.iter().fold(glam::DVec2::MIN, |acc, &c| acc.max(c)); - - rendering.leaf_tag("rect", |attributes| { - attributes.push("x", bb_min.x.to_string()); - attributes.push("y", bb_min.y.to_string()); - attributes.push("width", (bb_max.x - bb_min.x).to_string()); - attributes.push("height", (bb_max.y - bb_min.y).to_string()); - attributes.push("fill", format!("url(#{checker_id})")); - }); + let output = SvgRenderOutput::from(render); + assert!(output.svg_defs.is_empty()); - // Pattern defs will be appended after the intermediate defs are copied below - rendering.svg_defs = format!( - r##""##, - ); - } - } - - let existing_defs = rendering.svg_defs.clone(); - rendering.svg.push(SvgSegment::from(svg_data.0.clone())); - rendering.image_data = svg_data.1.clone(); - rendering.svg_defs = format!("{existing_defs}{}", svg_data.2); - - rendering.wrap_with_transform(footprint.transform, Some(logical_resolution)); RenderOutputType::Svg { - svg: rendering.svg.to_svg_string(), - image_data: rendering.image_data.into_iter().map(|(image, id)| (id, image.0)).collect(), + svg: output.svg, + image_data: output.image_data.into_iter().map(|(image, id)| (id, image.0)).collect(), } } - (RenderOutputTypeRequest::Vello, RenderIntermediateType::Vello(vello_data)) => { + (RenderOutputTypeRequest::Vello, RenderIntermediateType::Vello(data)) => { let Some(exec) = editor_api.application_io.as_ref().unwrap().gpu_executor() else { unreachable!("Attempted to render with Vello when no GPU executor is available"); }; - let (child, context) = Arc::as_ref(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 * footprint.transform; + let footprint_transform = scale_transform * render_params.footprint.transform; let footprint_transform_vello = vello::kurbo::Affine::new(footprint_transform.to_cols_array()); - let mut scene = vello::Scene::new(); - - // Infinite canvas checkerboard (when no artboards are present) - let show_checkerboard = !render_params.for_export && !contains_artboard && !render_params.hide_artboards; - if show_checkerboard && scale > 0. && render_params.viewport_zoom > 0. { - // Compute the axis-aligned bounding box of all four viewport corners in document space, - // which is necessary so the rect fully covers the visible area when the canvas is tilted - let inverse_footprint = footprint_transform.inverse(); - let corners = [ - inverse_footprint.transform_point2(glam::DVec2::ZERO), - inverse_footprint.transform_point2(glam::DVec2::new(physical_resolution.x as f64, 0.)), - inverse_footprint.transform_point2(glam::DVec2::new(0., physical_resolution.y as f64)), - inverse_footprint.transform_point2(physical_resolution.as_dvec2()), - ]; - let bb_min = corners.iter().fold(glam::DVec2::MAX, |acc, &c| acc.min(c)); - let bb_max = corners.iter().fold(glam::DVec2::MIN, |acc, &c| acc.max(c)); - let doc_rect = vello::kurbo::Rect::new(bb_min.x, bb_min.y, bb_max.x, bb_max.y); - - // Draw in document space, transformed to screen by footprint_transform (includes rotation) - // Brush maps each pixel to 1/viewport_zoom document units, giving constant 8px cells - let brush_transform = vello::kurbo::Affine::scale(1. / render_params.viewport_zoom); - scene.fill(vello::peniko::Fill::NonZero, footprint_transform_vello, &checkerboard_brush(), Some(brush_transform), &doc_rect); - } - - scene.append(child, Some(footprint_transform_vello)); + let mut transformed_scene = vello::Scene::new(); + transformed_scene.append(scene, Some(footprint_transform_vello)); // We now replace all transforms which are supposed to be infinite with a transform which covers the entire viewport. // See for more detail. @@ -239,17 +132,154 @@ async fn render<'a: 'n>(ctx: impl Ctx + ExtractFootprint + ExtractVarArgs, edito // 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); - for transform in scene.encoding_mut().transforms.iter_mut() { + 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); } } - let texture = Arc::new(exec.render_vello_scene_to_texture(&scene, physical_resolution, context).await.expect("Failed to render Vello scene")); - + let texture = exec + .render_vello_scene(&transformed_scene, physical_resolution, context, None) + .await + .expect("Failed to render Vello scene"); RenderOutputType::Texture(texture.into()) } _ => unreachable!("Render node did not receive its requested data type"), }; + RenderOutput { data, metadata } } + +#[node_macro::node(category(""))] +async fn render_background<'a: 'n>(ctx: impl Ctx + ExtractFootprint + ExtractVarArgs, editor_api: &'a PlatformEditorApi, data: RenderOutput) -> RenderOutput { + let footprint = ctx.footprint(); + let render_params = ctx + .vararg(0) + .expect("Did not find var args") + .downcast_ref::() + .expect("Downcasting render params yielded invalid type"); + + if !render_params.to_canvas() { + return data; + } + + let RenderOutput { data: foreground_data, metadata } = data; + let mut render_params = render_params.clone(); + render_params.footprint = *footprint; + + let data = match foreground_data { + RenderOutputType::Texture(foreground_texture) => { + if let Some(exec) = editor_api.application_io.as_ref().unwrap().gpu_executor() { + let doc_to_screen = (glam::DAffine2::from_scale(glam::DVec2::splat(render_params.scale)) * render_params.footprint.transform).as_affine2(); + let blended = exec + .composite_background(foreground_texture.as_ref(), &metadata.backgrounds, doc_to_screen, render_params.viewport_zoom as f32) + .await; + + RenderOutputType::Texture(blended.into()) + } else { + RenderOutputType::Texture(foreground_texture) + } + } + RenderOutputType::Svg { + svg: foreground_svg, + image_data: foreground_images, + } => { + let mut render = SvgRender::new(); + + 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()); + let cell_size = 8. / render_params.viewport_zoom; + let pattern_size = cell_size * 2.; + + write!( + &mut render.svg_defs, + r##""##, + pattern_origin.x, + pattern_origin.y, + ) + .unwrap(); + + render.leaf_tag("rect", |attributes| { + attributes.push("x", rect.x0.to_string()); + attributes.push("y", rect.y0.to_string()); + attributes.push("width", rect.width().to_string()); + attributes.push("height", rect.height().to_string()); + attributes.push("fill", format!("url(#{checker_id})")); + }); + }; + + if metadata.backgrounds.is_empty() { + if render_params.scale > 0. { + let logical_resolution = render_params.footprint.resolution.as_dvec2() / render_params.scale; + let logical_footprint = Footprint { + resolution: logical_resolution.round().as_uvec2().max(glam::UVec2::ONE), + ..render_params.footprint + }; + let bounds = logical_footprint.viewport_bounds_in_local_space(); + let min = bounds.start.floor(); + let max = bounds.end.ceil(); + + if min.is_finite() && max.is_finite() { + let rect = vello::kurbo::Rect::new(min.x, min.y, max.x, max.y); + draw_checkerboard(&mut render, rect, glam::DVec2::ZERO, "checkered-viewport"); + } + } + } else { + for background in &metadata.backgrounds { + let [a, b] = [background.location, background.location + background.dimensions]; + let rect = vello::kurbo::Rect::new(a.x.min(b.x), a.y.min(b.y), a.x.max(b.x), a.y.max(b.y)); + draw_checkerboard(&mut render, rect, glam::DVec2::new(rect.x0, rect.y0), "checkered-artboard"); + } + } + } + + let logical_resolution = render_params.footprint.resolution.as_dvec2() / render_params.scale; + render.wrap_with_transform(render_params.footprint.transform, Some(logical_resolution)); + + let background = SvgRenderOutput::from(render); + assert!(background.svg_defs.is_empty()); + + let svg = format!("{}{}", background.svg, foreground_svg); + let image_data = foreground_images; + + RenderOutputType::Svg { svg, image_data } + } + _ => unreachable!("Render background node received unsupported render output type"), + }; + + RenderOutput { data, metadata } +} + +#[node_macro::node(category(""))] +async fn create_context<'a: 'n>( + // Context injections are defined in the wrap_network_in_scope function + 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 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, + ..Default::default() + }; + + let ctx = OwnedContextImpl::default() + .with_footprint(footprint) + .with_real_time(render_config.time.time) + .with_animation_time(render_config.time.animation_time.as_secs_f64()) + .with_pointer_position(render_config.pointer) + .with_vararg(Box::new(render_params)) + .into_context(); + + data.eval(ctx).await +}