diff --git a/Cargo.lock b/Cargo.lock index f3878680c1..53ff801050 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2027,6 +2027,7 @@ dependencies = [ "base64", "blending-nodes", "brush-nodes", + "bytemuck", "core-types", "dyn-any", "glam", diff --git a/node-graph/interpreted-executor/src/util.rs b/node-graph/interpreted-executor/src/util.rs index 4d39e124f6..f4e909f57d 100644 --- a/node-graph/interpreted-executor/src/util.rs +++ b/node-graph/interpreted-executor/src/util.rs @@ -63,7 +63,7 @@ pub fn wrap_network_in_scope(mut network: NodeNetwork, editor_api: Arc, vello_renderer: Mutex, - resampler: Resampler, - background_compositor: BackgroundCompositor, + pipelines: RwLock>>, pub shader_runtime: ShaderRuntime, } @@ -84,21 +84,30 @@ impl WgpuExecutor { Ok(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 + pub async fn request_texture(&self, size: UVec2) -> Arc { + self.texture_cache.lock().await.request_texture(&self.context.device, size) } - 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 + fn pipeline(&self) -> Arc

{ + let key = TypeId::of::

(); + + let cached = self.pipelines.read().unwrap().get(&key).cloned(); + if let Some(arc) = cached { + return arc.downcast::

().expect("TypeId

guarantees this downcast"); + } + + self.pipelines + .write() + .unwrap() + .entry(key) + .or_insert_with(|| Arc::new(P::create(&self.context))) + .clone() + .downcast::

() + .expect("TypeId

guarantees this downcast") } - pub async fn request_texture(&self, size: UVec2) -> Arc { - self.texture_cache.lock().await.request_texture(&self.context.device, size) + pub async fn run_pipeline(&self, args: &P::Args<'_>) -> P::Out { + self.pipeline::

().run(self, args).await } } @@ -122,17 +131,14 @@ impl WgpuExecutor { 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 { context, texture_cache: texture_cache.into(), vello_renderer: vello_renderer.into(), - resampler, - background_compositor, shader_runtime, + pipelines: RwLock::new(HashMap::new()), }) } } diff --git a/node-graph/libraries/wgpu-executor/src/pipeline.rs b/node-graph/libraries/wgpu-executor/src/pipeline.rs new file mode 100644 index 0000000000..d36399277a --- /dev/null +++ b/node-graph/libraries/wgpu-executor/src/pipeline.rs @@ -0,0 +1,37 @@ +use std::future::Future; +use std::pin::Pin; + +use crate::{WgpuContext, WgpuExecutor}; + +pub type PipelineFuture<'a, T> = Pin + Send + 'a>>; + +pub trait Pipeline: std::any::Any + Send + Sync + Sized { + type Args<'a>; + type Out: Send; + + fn create(context: &WgpuContext) -> Self; + + fn run<'a>(&'a self, executor: &'a WgpuExecutor, args: &'a Self::Args<'_>) -> PipelineFuture<'a, Self::Out>; +} + +pub trait AsyncPipeline: std::any::Any + Send + Sync + Sized { + type Args<'a>; + type Out: Send; + + fn create(context: &WgpuContext) -> Self; + + fn run<'a>(&'a self, executor: &'a WgpuExecutor, args: &'a Self::Args<'_>) -> impl Future + Send + 'a; +} + +impl Pipeline for P { + type Args<'a> =

::Args<'a>; + type Out =

::Out; + + fn create(context: &WgpuContext) -> Self { +

::create(context) + } + + fn run<'a>(&'a self, executor: &'a WgpuExecutor, args: &'a Self::Args<'_>) -> PipelineFuture<'a, Self::Out> { + Box::pin(

::run(self, executor, args)) + } +} diff --git a/node-graph/libraries/wgpu-executor/src/resample.rs b/node-graph/libraries/wgpu-executor/src/resample.rs deleted file mode 100644 index 15a5dc99a9..0000000000 --- a/node-graph/libraries/wgpu-executor/src/resample.rs +++ /dev/null @@ -1,130 +0,0 @@ -use crate::WgpuContext; -use glam::{DAffine2, Vec2}; - -pub struct Resampler { - pipeline: wgpu::RenderPipeline, - bind_group_layout: wgpu::BindGroupLayout, -} - -impl Resampler { - pub fn new(device: &wgpu::Device) -> Self { - let shader = device.create_shader_module(wgpu::include_wgsl!("resample_shader.wgsl")); - - let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("resample_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: false }, - }, - count: None, - }, - wgpu::BindGroupLayoutEntry { - binding: 1, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: None, - }, - count: None, - }, - ], - }); - - let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: Some("resample_pipeline_layout"), - bind_group_layouts: &[Some(&bind_group_layout)], - ..Default::default() - }); - - let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: Some("resample_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_mask: None, - cache: None, - }); - - Resampler { pipeline, bind_group_layout } - } - - 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.create_view(&wgpu::TextureViewDescriptor::default()); - - let params_buffer = context.device.create_buffer(&wgpu::BufferDescriptor { - label: Some("resample_params"), - size: 32, - usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, - mapped_at_creation: false, - }); - - let params_data = [transform.matrix2.x_axis.as_vec2(), transform.matrix2.y_axis.as_vec2(), transform.translation.as_vec2(), Vec2::ZERO]; - context.queue.write_buffer(¶ms_buffer, 0, bytemuck::cast_slice(¶ms_data)); - - let bind_group = context.device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("resample_bind_group"), - layout: &self.bind_group_layout, - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::TextureView(&source_view), - }, - wgpu::BindGroupEntry { - binding: 1, - resource: params_buffer.as_entire_binding(), - }, - ], - }); - - let mut encoder = context.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("resample_encoder") }); - - { - let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("resample_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, - })], - ..Default::default() - }); - - 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/nodes/gstd/Cargo.toml b/node-graph/nodes/gstd/Cargo.toml index e28b78251f..2385c9a9b6 100644 --- a/node-graph/nodes/gstd/Cargo.toml +++ b/node-graph/nodes/gstd/Cargo.toml @@ -61,6 +61,7 @@ reqwest = { workspace = true } image = { workspace = true } base64 = { workspace = true } wgpu = { workspace = true } +bytemuck = { workspace = true } # Optional local dependencies graphene-canvas-utils = { workspace = true, optional = true } diff --git a/node-graph/nodes/gstd/src/lib.rs b/node-graph/nodes/gstd/src/lib.rs index f17c1c336c..7236bb8c63 100644 --- a/node-graph/nodes/gstd/src/lib.rs +++ b/node-graph/nodes/gstd/src/lib.rs @@ -1,8 +1,9 @@ pub mod any; -pub mod pixel_preview; pub mod platform_application_io; +pub mod render_background; pub mod render_cache; pub mod render_node; +pub mod render_pixel_preview; pub mod text; pub use blending_nodes; pub use brush_nodes as brush; diff --git a/node-graph/nodes/gstd/src/pixel_preview.rs b/node-graph/nodes/gstd/src/pixel_preview.rs deleted file mode 100644 index d27b0cb8a7..0000000000 --- a/node-graph/nodes/gstd/src/pixel_preview.rs +++ /dev/null @@ -1,71 +0,0 @@ -use crate::render_node::RenderOutputType; -use core_types::transform::{Footprint, Transform}; -use core_types::{CloneVarArgs, Context, Ctx, ExtractAll, OwnedContextImpl}; -use glam::{DAffine2, DVec2, UVec2}; -use graph_craft::application_io::PlatformEditorApi; -use graph_craft::document::value::RenderOutput; -use graphene_application_io::ApplicationIo; -use rendering::{RenderOutputType as RenderOutputTypeRequest, RenderParams}; -use vector_types::vector::style::RenderMode; - -#[node_macro::node(category(""))] -pub async fn pixel_preview<'a: 'n>( - ctx: impl Ctx + ExtractAll + CloneVarArgs + Sync, - editor_api: &'a PlatformEditorApi, - data: impl Node, Output = RenderOutput> + Send + Sync, -) -> RenderOutput { - let Some(render_params) = ctx.vararg(0).ok().and_then(|v| v.downcast_ref::()).cloned() else { - log::error!("invalid render params for pixel preview"); - let context = OwnedContextImpl::from(ctx).into_context(); - return data.eval(context).await; - }; - let physical_scale = render_params.scale; - - let footprint = *ctx.footprint(); - let viewport_zoom = footprint.scale_magnitudes().x * physical_scale; - - if render_params.render_mode != RenderMode::PixelPreview || !matches!(render_params.render_output_type, RenderOutputTypeRequest::Vello) || viewport_zoom <= 1. { - let context = OwnedContextImpl::from(ctx).into_context(); - return data.eval(context).await; - } - - let physical_resolution = footprint.resolution; - let logical_resolution = physical_resolution.as_dvec2() / physical_scale; - - let logical_footprint = Footprint { - resolution: logical_resolution.as_uvec2().max(UVec2::ONE), - ..footprint - }; - - let bounds = logical_footprint.viewport_bounds_in_local_space(); - - let upstream_min = bounds.start.floor(); - let upstream_max = bounds.end.ceil(); - - let upstream_size = (upstream_max - upstream_min).max(DVec2::ONE); - let upstream_resolution = upstream_size.as_uvec2().max(UVec2::ONE); - - let upstream_footprint = Footprint { - transform: DAffine2::from_scale(DVec2::splat(1.0 / physical_scale)) * DAffine2::from_translation(-upstream_min), - resolution: upstream_resolution, - quality: footprint.quality, - }; - - let new_ctx = OwnedContextImpl::from(ctx).with_footprint(upstream_footprint).with_vararg(Box::new(render_params)).into_context(); - let mut result = data.eval(new_ctx).await; - - let RenderOutputType::Texture(ref source_texture) = result.data else { return result }; - - let transform = DAffine2::from_translation(-upstream_min) * footprint.transform.inverse() * DAffine2::from_scale(logical_resolution); - - let exec = editor_api.application_io.as_ref().unwrap().gpu_executor().unwrap(); - let resampled = exec.resample_texture(source_texture.as_ref(), physical_resolution, &transform).await; - - result.data = RenderOutputType::Texture(resampled.into()); - - result - .metadata - .apply_transform(footprint.transform * DAffine2::from_translation(upstream_min) * DAffine2::from_scale(DVec2::splat(physical_scale))); - - result -} diff --git a/node-graph/libraries/wgpu-executor/src/background/mod.rs b/node-graph/nodes/gstd/src/render_background.rs similarity index 64% rename from node-graph/libraries/wgpu-executor/src/background/mod.rs rename to node-graph/nodes/gstd/src/render_background.rs index dd4df445ad..96dd98711f 100644 --- a/node-graph/libraries/wgpu-executor/src/background/mod.rs +++ b/node-graph/nodes/gstd/src/render_background.rs @@ -1,7 +1,126 @@ -use glam::{Affine2, Vec2}; +use core_types::ExtractVarArgs; +use core_types::color::Linear; +use core_types::transform::Footprint; +use core_types::uuid::generate_uuid; +use core_types::{Ctx, ExtractFootprint}; +use glam::{Affine2, UVec2, Vec2}; +use graph_craft::application_io::PlatformEditorApi; +use graph_craft::document::value::RenderOutput; +pub use graph_craft::document::value::RenderOutputType; +use graphene_application_io::ApplicationIo; +use rendering::{RenderParams, SvgRender, SvgRenderOutput}; +use std::fmt::Write; +use std::sync::Arc; use wgpu::util::DeviceExt; +use wgpu_executor::{AsyncWgpuPipeline, WgpuContext, WgpuExecutor}; + +#[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() || render_params.viewport_zoom <= 0.0 { + 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 + .run_pipeline::(&BackgroundCompositorArgs { + foreground: foreground_texture.as_ref(), + backgrounds: &metadata.backgrounds, + document_to_screen: doc_to_screen, + zoom: render_params.viewport_zoom.to_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 } +} -pub struct BackgroundCompositor { +struct BackgroundCompositor { checker_rect_pipeline: wgpu::RenderPipeline, checker_viewport_pipeline: wgpu::RenderPipeline, fullscreen_pipeline: wgpu::RenderPipeline, @@ -10,12 +129,23 @@ pub struct BackgroundCompositor { sampler: wgpu::Sampler, } -impl BackgroundCompositor { - pub fn new(device: &wgpu::Device) -> Self { +struct BackgroundCompositorArgs<'a> { + pub foreground: &'a wgpu::Texture, + pub backgrounds: &'a [rendering::Background], + pub document_to_screen: Affine2, + pub zoom: f32, +} + +impl AsyncWgpuPipeline for BackgroundCompositor { + type Args<'a> = BackgroundCompositorArgs<'a>; + type Out = Arc; + + fn create(context: &WgpuContext) -> Self { + let device = &context.device; 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_rect_shader = device.create_shader_module(wgpu::include_wgsl!("render_background_checker_rect.wgsl")); + let checker_viewport_shader = device.create_shader_module(wgpu::include_wgsl!("render_background_checker_viewport.wgsl")); + let fullscreen_shader = device.create_shader_module(wgpu::include_wgsl!("render_background_fullscreen.wgsl")); let checker_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("background_checker_bind_group_layout"), @@ -189,9 +319,20 @@ impl BackgroundCompositor { } } - pub fn composite(&self, context: &crate::WgpuContext, foreground: &wgpu::Texture, output: &wgpu::Texture, backgrounds: &[rendering::Background], document_to_screen: Affine2, zoom: f32) { + async fn run<'a>(&'a self, executor: &'a WgpuExecutor, args: &'a Self::Args<'_>) -> Self::Out { + let context = &executor.context; + let &BackgroundCompositorArgs { + foreground, + backgrounds, + document_to_screen, + zoom, + } = args; + + let foreground_size = foreground.size(); + let output = executor.request_texture(UVec2::new(foreground_size.width, foreground_size.height)).await; + if zoom <= 0.0 { - return; + return output; } let device = &context.device; @@ -285,8 +426,12 @@ impl BackgroundCompositor { } queue.submit(std::iter::once(encoder.finish())); + + output } +} +impl BackgroundCompositor { 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"), diff --git a/node-graph/libraries/wgpu-executor/src/background/checker_rect.wgsl b/node-graph/nodes/gstd/src/render_background_checker_rect.wgsl similarity index 100% rename from node-graph/libraries/wgpu-executor/src/background/checker_rect.wgsl rename to node-graph/nodes/gstd/src/render_background_checker_rect.wgsl diff --git a/node-graph/libraries/wgpu-executor/src/background/checker_viewport.wgsl b/node-graph/nodes/gstd/src/render_background_checker_viewport.wgsl similarity index 100% rename from node-graph/libraries/wgpu-executor/src/background/checker_viewport.wgsl rename to node-graph/nodes/gstd/src/render_background_checker_viewport.wgsl diff --git a/node-graph/libraries/wgpu-executor/src/background/fullscreen.wgsl b/node-graph/nodes/gstd/src/render_background_fullscreen.wgsl similarity index 100% rename from node-graph/libraries/wgpu-executor/src/background/fullscreen.wgsl rename to node-graph/nodes/gstd/src/render_background_fullscreen.wgsl diff --git a/node-graph/nodes/gstd/src/render_node.rs b/node-graph/nodes/gstd/src/render_node.rs index 121131a062..b77ac5f8a8 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::list::List; 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}; use graph_craft::application_io::PlatformEditorApi; @@ -10,14 +9,10 @@ use graphene_application_io::{ApplicationIo, ExportFormat, RenderConfig}; use graphic_types::raster_types::{CPU, Raster}; use graphic_types::{Artboard, Graphic, Vector}; 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; -// Re-export render_output_cache from render_cache module -pub use crate::render_cache::render_output_cache; - #[derive(Clone, dyn_any::DynAny)] pub enum RenderIntermediateType { Vello(Arc<(vello::Scene, RenderContext)>), @@ -150,107 +145,6 @@ async fn render<'a: 'n>(ctx: impl Ctx + ExtractFootprint + ExtractVarArgs, edito 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 diff --git a/node-graph/nodes/gstd/src/render_pixel_preview.rs b/node-graph/nodes/gstd/src/render_pixel_preview.rs new file mode 100644 index 0000000000..12bfe46e25 --- /dev/null +++ b/node-graph/nodes/gstd/src/render_pixel_preview.rs @@ -0,0 +1,224 @@ +use crate::render_node::RenderOutputType; +use core_types::transform::{Footprint, Transform}; +use core_types::{CloneVarArgs, Context, Ctx, ExtractAll, OwnedContextImpl}; +use glam::{DAffine2, DVec2, UVec2, Vec2}; +use graph_craft::application_io::PlatformEditorApi; +use graph_craft::document::value::RenderOutput; +use graphene_application_io::ApplicationIo; +use rendering::{RenderOutputType as RenderOutputTypeRequest, RenderParams}; +use std::sync::Arc; +use vector_types::vector::style::RenderMode; +use wgpu_executor::{AsyncWgpuPipeline, WgpuContext, WgpuExecutor}; + +#[node_macro::node(category(""))] +pub async fn render_pixel_preview<'a: 'n>( + ctx: impl Ctx + ExtractAll + CloneVarArgs + Sync, + editor_api: &'a PlatformEditorApi, + data: impl Node, Output = RenderOutput> + Send + Sync, +) -> RenderOutput { + let Some(render_params) = ctx.vararg(0).ok().and_then(|v| v.downcast_ref::()).cloned() else { + log::error!("invalid render params for pixel preview"); + let context = OwnedContextImpl::from(ctx).into_context(); + return data.eval(context).await; + }; + let physical_scale = render_params.scale; + + let footprint = *ctx.footprint(); + let viewport_zoom = footprint.scale_magnitudes().x * physical_scale; + + if render_params.render_mode != RenderMode::PixelPreview || !matches!(render_params.render_output_type, RenderOutputTypeRequest::Vello) || viewport_zoom <= 1. { + let context = OwnedContextImpl::from(ctx).into_context(); + return data.eval(context).await; + } + + let physical_resolution = footprint.resolution; + let logical_resolution = physical_resolution.as_dvec2() / physical_scale; + + let logical_footprint = Footprint { + resolution: logical_resolution.as_uvec2().max(UVec2::ONE), + ..footprint + }; + + let bounds = logical_footprint.viewport_bounds_in_local_space(); + + let upstream_min = bounds.start.floor(); + let upstream_max = bounds.end.ceil(); + + let upstream_size = (upstream_max - upstream_min).max(DVec2::ONE); + let upstream_resolution = upstream_size.as_uvec2().max(UVec2::ONE); + + let upstream_footprint = Footprint { + transform: DAffine2::from_scale(DVec2::splat(1.0 / physical_scale)) * DAffine2::from_translation(-upstream_min), + resolution: upstream_resolution, + quality: footprint.quality, + }; + + let new_ctx = OwnedContextImpl::from(ctx).with_footprint(upstream_footprint).with_vararg(Box::new(render_params)).into_context(); + let mut result = data.eval(new_ctx).await; + + let RenderOutputType::Texture(ref source_texture) = result.data else { return result }; + + let transform = DAffine2::from_translation(-upstream_min) * footprint.transform.inverse() * DAffine2::from_scale(logical_resolution); + + let exec = editor_api.application_io.as_ref().unwrap().gpu_executor().unwrap(); + let resampled = exec + .run_pipeline::(&ResamplerArgs { + source: source_texture.as_ref(), + transform: &transform, + size: physical_resolution, + }) + .await; + + result.data = RenderOutputType::Texture(resampled.into()); + + result + .metadata + .apply_transform(footprint.transform * DAffine2::from_translation(upstream_min) * DAffine2::from_scale(DVec2::splat(physical_scale))); + + result +} + +struct PixelPreviewPipeline { + pipeline: wgpu::RenderPipeline, + bind_group_layout: wgpu::BindGroupLayout, +} + +struct ResamplerArgs<'a> { + source: &'a wgpu::Texture, + transform: &'a DAffine2, + size: UVec2, +} + +impl AsyncWgpuPipeline for PixelPreviewPipeline { + type Args<'a> = ResamplerArgs<'a>; + type Out = Arc; + + fn create(context: &WgpuContext) -> Self { + let device = &context.device; + let shader = device.create_shader_module(wgpu::include_wgsl!("render_pixel_preview.wgsl")); + + let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("resample_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: false }, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }, + ], + }); + + let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("resample_pipeline_layout"), + bind_group_layouts: &[Some(&bind_group_layout)], + ..Default::default() + }); + + let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("resample_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_mask: None, + cache: None, + }); + + PixelPreviewPipeline { pipeline, bind_group_layout } + } + + async fn run<'a>(&'a self, executor: &'a WgpuExecutor, args: &'a Self::Args<'_>) -> Self::Out { + let context = &executor.context; + let &ResamplerArgs { source, transform, size } = args; + + let output = executor.request_texture(size).await; + + let source_view = source.create_view(&wgpu::TextureViewDescriptor::default()); + let output_view = output.create_view(&wgpu::TextureViewDescriptor::default()); + + let params_buffer = context.device.create_buffer(&wgpu::BufferDescriptor { + label: Some("resample_params"), + size: 32, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + + let params_data = [transform.matrix2.x_axis.as_vec2(), transform.matrix2.y_axis.as_vec2(), transform.translation.as_vec2(), Vec2::ZERO]; + context.queue.write_buffer(¶ms_buffer, 0, bytemuck::cast_slice(¶ms_data)); + + let bind_group = context.device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("resample_bind_group"), + layout: &self.bind_group_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&source_view), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: params_buffer.as_entire_binding(), + }, + ], + }); + + let mut encoder = context.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("resample_encoder") }); + + { + let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("resample_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, + })], + ..Default::default() + }); + + 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()]); + + output + } +} diff --git a/node-graph/libraries/wgpu-executor/src/resample_shader.wgsl b/node-graph/nodes/gstd/src/render_pixel_preview.wgsl similarity index 100% rename from node-graph/libraries/wgpu-executor/src/resample_shader.wgsl rename to node-graph/nodes/gstd/src/render_pixel_preview.wgsl