diff --git a/desktop/src/render/state.rs b/desktop/src/render/state.rs index e9a826f93b..04f372ab1d 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")] @@ -236,11 +235,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(); } @@ -317,11 +322,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 { @@ -333,7 +334,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 7c0d8bb8e1..61a55b5f30 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,9 +4,7 @@ use crate::messages::portfolio::document::utility_types::network_interface::Node use crate::messages::prelude::*; use glam::{DAffine2, IVec2}; use graph_craft::document::NodeId; -use graphene_std::Artboard; 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; @@ -14,6 +12,7 @@ use graphene_std::text::{Font, TypesettingConfig}; use graphene_std::vector::PointId; use graphene_std::vector::VectorModificationType; use graphene_std::vector::style::{Fill, Stroke}; +use graphene_std::{Artboard, Color}; #[impl_message(Message, DocumentMessage, GraphOperation)] #[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)] diff --git a/editor/src/node_graph_executor.rs b/editor/src/node_graph_executor.rs index 619839ff95..01077863b8 100644 --- a/editor/src/node_graph_executor.rs +++ b/editor/src/node_graph_executor.rs @@ -413,6 +413,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 { + fn render_background_to_vello(&self, scene: &mut vello::Scene, transform: DAffine2, render_params: &RenderParams) { + if self.is_empty() { + let Some(rect) = viewport_rect(render_params.footprint, render_params.scale) else { return }; + checkerboard_fill_vello(scene, transform, rect, DVec2::ZERO, render_params.viewport_zoom); + return; + } + + for background in self { + background.render_background_to_vello(scene, transform, render_params); + } + } + + fn render_background_svg(&self, render: &mut SvgRender, render_params: &RenderParams) { + if self.is_empty() { + let Some(rect) = viewport_rect(render_params.footprint, render_params.scale) else { return }; + checkerboard_fill_svg(render, rect, DVec2::ZERO, render_params.viewport_zoom, "checkered-viewport"); + return; + } + + for background in self { + background.render_background_svg(render, render_params); + } + } +} + +fn checkerboard_fill_vello(scene: &mut vello::Scene, transform: DAffine2, rect: kurbo::Rect, pattern_origin: DVec2, viewport_zoom: f64) { + if viewport_zoom <= 0. { + return; + } + + let brush = vello::peniko::Brush::Image(vello::peniko::ImageBrush { + image: vello::peniko::ImageData { + data: vello::peniko::Blob::new(CHECKERBOARD_IMAGE_DATA.clone()), + format: vello::peniko::ImageFormat::Rgba8, + width: 16, + height: 16, + alpha_type: vello::peniko::ImageAlphaType::Alpha, + }, + sampler: vello::peniko::ImageSampler { + x_extend: vello::peniko::Extend::Repeat, + y_extend: vello::peniko::Extend::Repeat, + quality: vello::peniko::ImageQuality::Low, + alpha: 1., + }, + }); + let brush_transform = kurbo::Affine::scale(1. / viewport_zoom).then_translate(kurbo::Vec2::new(pattern_origin.x, pattern_origin.y)); + scene.fill(vello::peniko::Fill::NonZero, kurbo::Affine::new(transform.to_cols_array()), &brush, Some(brush_transform), &rect); +} + +fn checkerboard_fill_svg(render: &mut SvgRender, rect: kurbo::Rect, pattern_origin: DVec2, viewport_zoom: f64, checker_id_prefix: &str) { + if viewport_zoom <= 0. { + return; + } + + let checker_id = format!("{checker_id_prefix}-{}", generate_uuid()); + + let svg_defs: &mut String = &mut render.svg_defs; + let pattern_id: &str = &checker_id; + + let cell_size = 8. / viewport_zoom; + let pattern_size = cell_size * 2.; + + write!( + 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})")); + }); +} + +fn background_rect(background: &Background) -> kurbo::Rect { + let [a, b] = [background.location.as_dvec2(), background.location.as_dvec2() + background.dimensions.as_dvec2()]; + kurbo::Rect::new(a.x.min(b.x), a.y.min(b.y), a.x.max(b.x), a.y.max(b.y)) +} + +fn viewport_rect(footprint: Footprint, scale: f64) -> Option { + if scale <= 0. { + return None; + } + + let logical_resolution = footprint.resolution.as_dvec2() / scale; + let logical_footprint = Footprint { + resolution: logical_resolution.round().as_uvec2().max(glam::UVec2::ONE), + ..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()) { + return None; + } + + Some(kurbo::Rect::new(min.x, min.y, max.x, max.y)) +} + +/// Cached 16x16 transparency checkerboard image data (four 8x8 cells of #ffffff and #cccccc). +static CHECKERBOARD_IMAGE_DATA: LazyLock>> = 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) +}); diff --git a/node-graph/libraries/rendering/src/lib.rs b/node-graph/libraries/rendering/src/lib.rs index 418f4e9c0c..d0652f50f4 100644 --- a/node-graph/libraries/rendering/src/lib.rs +++ b/node-graph/libraries/rendering/src/lib.rs @@ -1,6 +1,8 @@ +pub mod background; pub mod convert_usvg_path; pub mod render_ext; mod renderer; pub mod to_peniko; +pub use background::RenderBackground; pub use renderer::*; diff --git a/node-graph/libraries/rendering/src/renderer.rs b/node-graph/libraries/rendering/src/renderer.rs index 6f1690ad37..296b826da3 100644 --- a/node-graph/libraries/rendering/src/renderer.rs +++ b/node-graph/libraries/rendering/src/renderer.rs @@ -1,8 +1,10 @@ +use crate::background::Background; use crate::render_ext::RenderExt; use crate::to_peniko::BlendModeExt; use core_types::blending::BlendMode; -use core_types::bounds::{BoundingBox, RenderBoundingBox}; -use core_types::color::{Alpha, Color}; +use core_types::bounds::BoundingBox; +use core_types::bounds::RenderBoundingBox; +use core_types::color::Color; use core_types::math::quad::Quad; use core_types::render_complexity::RenderComplexity; use core_types::table::{Table, TableRow}; @@ -27,46 +29,6 @@ use std::sync::{Arc, LazyLock}; use vector_types::gradient::GradientSpreadMethod; use vello::*; -/// Cached 16x16 transparency checkerboard image data (two 8x8 cells of #ffffff and #cccccc). -static CHECKERBOARD_IMAGE_DATA: LazyLock>> = 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, serde::Serialize, serde::Deserialize)] enum MaskType { Clip, @@ -119,15 +81,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(); @@ -135,7 +99,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()); } @@ -180,6 +148,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() @@ -208,8 +204,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. @@ -229,7 +223,6 @@ impl Hash for RenderParams { self.footprint.hash(state); self.render_output_type.hash(state); self.thumbnail.hash(state); - self.hide_artboards.hash(state); self.for_export.hash(state); self.for_mask.hash(state); if let Some(x) = self.alignment_parent_transform { @@ -341,6 +334,7 @@ pub struct RenderMetadata { pub click_targets: HashMap>>, pub clip_targets: HashSet, pub vector_data: HashMap>, + pub backgrounds: Vec, } impl RenderMetadata { @@ -361,6 +355,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()); @@ -368,6 +363,13 @@ 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()); + } + } } } @@ -511,47 +513,17 @@ impl Render for Graphic { impl Render for Artboard { fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) { - let x = self.location.x.min(self.location.x + self.dimensions.x); - let y = self.location.y.min(self.location.y + self.dimensions.y); - let width = self.dimensions.x.abs(); - let height = self.dimensions.y.abs(); - // Rectangle for the artboard - if !render_params.hide_artboards { - // Transparency checkerboard behind the artboard background (viewport only) - let show_checkerboard = self.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})")); - }); + render.leaf_tag("rect", |attributes| { + attributes.push("fill", format!("#{}", self.background.to_rgb_hex_srgb_from_gamma())); + if self.background.a() < 1. { + attributes.push("fill-opacity", ((self.background.a() * 1000.).round() / 1000.).to_string()); } - - // Background - render.leaf_tag("rect", |attributes| { - attributes.push("fill", format!("#{}", self.background.to_rgb_hex_srgb_from_gamma())); - if self.background.a() < 1. { - attributes.push("fill-opacity", ((self.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", self.location.x.min(self.location.x + self.dimensions.x).to_string()); + attributes.push("y", self.location.y.min(self.location.y + self.dimensions.y).to_string()); + attributes.push("width", self.dimensions.x.abs().to_string()); + attributes.push("height", self.dimensions.y.abs().to_string()); + }); // Artwork render.parent_tag( @@ -570,7 +542,7 @@ impl Render for Artboard { write!( &mut attributes.0.svg_defs, - r##""##, + r##""##, self.dimensions.x, self.dimensions.y, ) .unwrap(); @@ -593,25 +565,10 @@ impl Render for Artboard { 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 = self.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 color = peniko::Color::new([self.background.r(), self.background.g(), self.background.b(), self.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([self.background.r(), self.background.g(), self.background.b(), self.background.a()]); + scene.push_layer(peniko::Fill::NonZero, peniko::Mix::Normal, 1., kurbo::Affine::new(transform.to_cols_array()), &rect); + scene.fill(peniko::Fill::NonZero, kurbo::Affine::new(transform.to_cols_array()), color, None, &rect); + scene.pop_layer(); if self.clip { scene.push_clip_layer(peniko::Fill::NonZero, kurbo::Affine::new(transform.to_cols_array()), &rect); @@ -638,6 +595,10 @@ impl Render for Artboard { } } footprint.transform *= self.transform(); + metadata.backgrounds.push(Background { + location: self.location, + dimensions: self.dimensions, + }); self.content.collect_metadata(metadata, footprint, None); } diff --git a/node-graph/libraries/wgpu-executor/src/blend.rs b/node-graph/libraries/wgpu-executor/src/blend.rs new file mode 100644 index 0000000000..9b4390582e --- /dev/null +++ b/node-graph/libraries/wgpu-executor/src/blend.rs @@ -0,0 +1,143 @@ +use crate::WgpuContext; + +pub struct Blender { + pipeline: wgpu::RenderPipeline, + bind_group_layout: wgpu::BindGroupLayout, + sampler: wgpu::Sampler, +} + +impl Blender { + pub fn new(device: &wgpu::Device) -> Self { + let shader = device.create_shader_module(wgpu::include_wgsl!("blend_shader.wgsl")); + + let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("blend_bind_group_layout"), + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + 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, + }, + wgpu::BindGroupLayoutEntry { + binding: 2, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + ], + }); + + let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("blend_pipeline_layout"), + bind_group_layouts: &[&bind_group_layout], + push_constant_ranges: &[], + }); + + let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("blend_pipeline"), + layout: Some(&pipeline_layout), + vertex: wgpu::VertexState { + module: &shader, + entry_point: Some("vs_main"), + buffers: &[], + compilation_options: wgpu::PipelineCompilationOptions::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &shader, + entry_point: Some("fs_main"), + targets: &[Some(wgpu::ColorTargetState { + format: wgpu::TextureFormat::Rgba8Unorm, + blend: None, + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: wgpu::PipelineCompilationOptions::default(), + }), + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + ..Default::default() + }, + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview: None, + cache: None, + }); + + let sampler = device.create_sampler(&wgpu::SamplerDescriptor { + address_mode_u: wgpu::AddressMode::ClampToEdge, + address_mode_v: wgpu::AddressMode::ClampToEdge, + address_mode_w: wgpu::AddressMode::ClampToEdge, + mag_filter: wgpu::FilterMode::Nearest, + min_filter: wgpu::FilterMode::Nearest, + mipmap_filter: wgpu::FilterMode::Nearest, + ..Default::default() + }); + + Self { pipeline, bind_group_layout, sampler } + } + + pub fn blend(&self, context: &WgpuContext, foreground: &wgpu::Texture, background: &wgpu::Texture, output: &wgpu::Texture) { + let foreground_view = foreground.create_view(&wgpu::TextureViewDescriptor::default()); + let background_view = background.create_view(&wgpu::TextureViewDescriptor::default()); + let output_view = output.create_view(&wgpu::TextureViewDescriptor::default()); + + let bind_group = context.device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("blend_bind_group"), + layout: &self.bind_group_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&foreground_view), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::TextureView(&background_view), + }, + wgpu::BindGroupEntry { + binding: 2, + resource: wgpu::BindingResource::Sampler(&self.sampler), + }, + ], + }); + + let mut encoder = context.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("blend_encoder") }); + + { + let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("blend_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, + }); + + render_pass.set_pipeline(&self.pipeline); + render_pass.set_bind_group(0, &bind_group, &[]); + render_pass.draw(0..3, 0..1); + } + + context.queue.submit([encoder.finish()]); + } +} diff --git a/node-graph/libraries/wgpu-executor/src/blend_shader.wgsl b/node-graph/libraries/wgpu-executor/src/blend_shader.wgsl new file mode 100644 index 0000000000..60f1fed27b --- /dev/null +++ b/node-graph/libraries/wgpu-executor/src/blend_shader.wgsl @@ -0,0 +1,36 @@ +struct VertexOutput { + @builtin(position) clip_position: vec4, + @location(0) tex_coords: vec2, +} + +@vertex +fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput { + var out: VertexOutput; + let pos = array( + vec2f(-1.0, -1.0), + vec2f(3.0, -1.0), + vec2f(-1.0, 3.0), + ); + let xy = pos[vertex_index]; + out.clip_position = vec4f(xy, 0.0, 1.0); + let coords = xy / 2. + 0.5; + out.tex_coords = vec2f(coords.x, 1. - coords.y); + return out; +} + +@group(0) @binding(0) +var t_foreground: texture_2d; +@group(0) @binding(1) +var t_background: texture_2d; +@group(0) @binding(2) +var s_linear: sampler; + +@fragment +fn fs_main(in: VertexOutput) -> @location(0) vec4 { + let foreground = textureSample(t_foreground, s_linear, in.tex_coords); + let background = textureSample(t_background, s_linear, in.tex_coords); + + let a = foreground.a + background.a * (1.0 - foreground.a); + let rgb = foreground.rgb * foreground.a + background.rgb * background.a * (1.0 - foreground.a); + return vec4(rgb, a); +} diff --git a/node-graph/libraries/wgpu-executor/src/lib.rs b/node-graph/libraries/wgpu-executor/src/lib.rs index 7cd413c1cb..f3480e8d2e 100644 --- a/node-graph/libraries/wgpu-executor/src/lib.rs +++ b/node-graph/libraries/wgpu-executor/src/lib.rs @@ -1,11 +1,18 @@ +mod blend; mod context; mod resample; pub mod shader_runtime; +mod texture_cache; pub mod texture_conversion; +use std::sync::Arc; + +use crate::blend::Blender; 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 graphene_application_io::{ApplicationIo, EditorApi}; @@ -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, + blender: Blender, 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); + Ok(texture) } - /// Returns a reference to the texture view for rendering. - pub fn view(&self) -> &wgpu::TextureView { - &self.view + pub async fn resample_texture(&self, source: &wgpu::Texture, target_size: UVec2, transform: &glam::DAffine2) -> Arc { + let out = self.request_texture(target_size).await; + self.resampler.resample(&self.context, source, transform, &out); + out } - /// Returns a reference to the underlying texture. - pub fn texture(&self) -> &wgpu::Texture { - &self.texture - } -} - -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 blend_textures(&self, foreground: &wgpu::Texture, background: &wgpu::Texture) -> Arc { + let size = UVec2::new(foreground.width(), foreground.height()).max(UVec2::ONE); + let out = self.request_texture(size).await; + self.blender.blend(&self.context, foreground, background, &out); + out } - 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) } } @@ -159,11 +120,14 @@ impl WgpuExecutor { .ok()?; let resampler = Resampler::new(&context.device); + let blender = Blender::new(&context.device); Some(Self { shader_runtime: ShaderRuntime::new(&context), + texture_cache: Mutex::new(TextureCache::new(TEXTURE_CACHE_SIZE)), context, resampler, + blender, vello_renderer: vello_renderer.into(), }) } diff --git a/node-graph/libraries/wgpu-executor/src/resample.rs b/node-graph/libraries/wgpu-executor/src/resample.rs index 3a788fa3c9..98b03a46d7 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 { @@ -145,8 +127,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..dadb9cab30 --- /dev/null +++ b/node-graph/libraries/wgpu-executor/src/texture_cache.rs @@ -0,0 +1,87 @@ +use glam::UVec2; +use std::collections::VecDeque; +use std::sync::Arc; + +struct CachedTexture { + texture: Arc, + size: UVec2, + bytes: u64, +} + +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(|e| e.size == size && Arc::strong_count(&e.texture) == 1) { + let entry = self.textures.remove(pos).unwrap(); + let texture = entry.texture.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(CachedTexture { + texture: texture.clone(), + size, + bytes: incoming_bytes, + }); + + texture + } + + fn total_free_bytes(&self) -> u64 { + self.textures.iter().filter(|e| Arc::strong_count(&e.texture) == 1).map(|e| e.bytes).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(|entry| { + if free_bytes + incoming_bytes <= max_free_bytes { + return true; + } + if Arc::strong_count(&entry.texture) == 1 { + free_bytes -= entry.bytes; + entry.texture.destroy(); + false + } else { + true + } + }); + } +} 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..39e30beeff 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, @@ -106,12 +103,6 @@ struct TileCacheImpl { 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)] @@ -205,7 +196,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,42 +204,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 { @@ -260,10 +217,6 @@ impl TileCache { 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 734420732c..5b46e427e2 100644 --- a/node-graph/nodes/gstd/src/render_node.rs +++ b/node-graph/nodes/gstd/src/render_node.rs @@ -1,6 +1,5 @@ use core_types::table::Table; use core_types::transform::{Footprint, Transform}; -use core_types::uuid::generate_uuid; use core_types::{CloneVarArgs, ExtractAll, ExtractVarArgs}; use core_types::{Color, Context, Ctx, ExtractFootprint, OwnedContextImpl, WasmNotSend}; pub use graph_craft::application_io::*; @@ -10,9 +9,7 @@ use graphene_application_io::{ApplicationIo, ExportFormat, RenderConfig}; use graphic_types::raster_types::Image; use graphic_types::raster_types::{CPU, Raster}; use graphic_types::{Artboard, Graphic, Vector}; -use rendering::{Render, RenderOutputType as RenderOutputTypeRequest, RenderParams, RenderSvgSegmentList, SvgRender, checkerboard_brush}; -use rendering::{RenderMetadata, SvgSegment}; -use std::collections::HashMap; +use rendering::{Render, RenderBackground, RenderMetadata, RenderOutputType as RenderOutputTypeRequest, RenderParams, SvgRender, SvgRenderOutput}; use std::sync::Arc; use vector_types::GradientStops; use wgpu_executor::RenderContext; @@ -20,19 +17,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 +53,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 +63,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,169 +71,165 @@ 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; +async fn render<'a: 'n>(ctx: impl Ctx + ExtractFootprint + ExtractVarArgs, editor_api: &'a PlatformEditorApi, data: RenderIntermediate) -> 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"); + let mut render_params = render_params.clone(); + render_params.footprint = *footprint; - let render_output_type = match render_config.export_format { - ExportFormat::Svg => RenderOutputTypeRequest::Svg, - ExportFormat::Raster => RenderOutputTypeRequest::Vello, - }; + let RenderIntermediate { ty, mut metadata } = data; + metadata.apply_transform(footprint.transform); - 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 data = match (render_params.render_output_type, ty) { + (RenderOutputTypeRequest::Svg, RenderIntermediateType::Svg(data)) => { + let (svg, image_data) = render_svg(data.as_ref().into(), &render_params); + RenderOutputType::Svg { svg, image_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 (scene, context) = data.as_ref(); + let texture = render_vello(scene, context, &render_params, exec).await; + RenderOutputType::Texture(texture.into()) + } + _ => unreachable!("Render node did not receive its requested data type"), }; - 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 + RenderOutput { data, metadata } } #[node_macro::node(category(""))] -async fn render<'a: 'n>(ctx: impl Ctx + ExtractFootprint + ExtractVarArgs, editor_api: &'a PlatformEditorApi, data: RenderIntermediate) -> RenderOutput { +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.for_export { + return data; + } + + let RenderOutput { data: foreground_data, metadata } = data; 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 data = match foreground_data { + RenderOutputType::Texture(foreground_texture) => { + if let Some(exec) = editor_api.application_io.as_ref().unwrap().gpu_executor() { + let mut background_scene = vello::Scene::new(); + metadata.backgrounds.render_background_to_vello(&mut background_scene, Default::default(), &render_params); - let RenderIntermediate { ty, mut metadata, contains_artboard } = data; - metadata.apply_transform(footprint.transform); + let background_texture = render_vello(&background_scene, &RenderContext::default(), &render_params, exec).await; - let data = match (render_params.render_output_type, &ty) { - (RenderOutputTypeRequest::Svg, RenderIntermediateType::Svg(svg_data)) => { - let mut rendering = SvgRender::new(); - - // 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.; - - // 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})")); - }); - - // Pattern defs will be appended after the intermediate defs are copied below - rendering.svg_defs = format!( - r##""##, - ); - } + let blended = exec.blend_textures(foreground_texture.as_ref(), background_texture.as_ref()).await; + RenderOutputType::Texture(blended.into()) + } else { + RenderOutputType::Texture(foreground_texture) } + } + RenderOutputType::Svg { + svg: foreground_svg, + image_data: foreground_images, + } => { + let mut render = SvgRender::new(); + metadata.backgrounds.render_background_svg(&mut render, &render_params); - 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); + let (background_svg, background_images) = render_svg(render, &render_params); + + let mut image_data = background_images; + image_data.extend(foreground_images); - 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)).collect(), + svg: format!("{background_svg}{foreground_svg}"), + image_data, } } - (RenderOutputTypeRequest::Vello, RenderIntermediateType::Vello(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); + _ => unreachable!("Render background node received unsupported render output type"), + }; - 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()); + RenderOutput { data, metadata } +} - let mut scene = vello::Scene::new(); +async fn render_vello(scene: &vello::Scene, context: &RenderContext, render_params: &RenderParams, exec: &wgpu_executor::WgpuExecutor) -> Arc { + let scale = render_params.scale; + let physical_resolution = render_params.footprint.resolution; - // 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); - } + 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()); - 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 - 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() { - if transform.matrix[0] == f32::INFINITY { - *transform = vello_encoding::Transform::from_kurbo(&scaled_infinite_transform); - } - } + // 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(physical_resolution.x as f64, physical_resolution.y as f64); + for transform in transformed_scene.encoding_mut().transforms.iter_mut() { + if transform.matrix[0] == f32::INFINITY { + *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")); + 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"), +fn render_svg(mut render: SvgRender, render_params: &RenderParams) -> (String, Vec<(u64, Image)>) { + let logical_resolution = render_params.footprint.resolution.as_dvec2() / render_params.scale; + + render.wrap_with_transform(render_params.footprint.transform, Some(logical_resolution)); + + let output = SvgRenderOutput::from(render); + assert!(output.svg_defs.is_empty()); + + (output.svg, output.image_data.into_iter().map(|(image, id)| (id, image)).collect()) +} + +#[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, }; - RenderOutput { data, metadata } + + 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 }