diff --git a/Cargo.lock b/Cargo.lock index 88e882908e..40d9d7b773 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2042,6 +2042,7 @@ dependencies = [ "base64", "blending-nodes", "brush-nodes", + "bytemuck", "core-types", "dyn-any", "glam", diff --git a/desktop/src/render/state.rs b/desktop/src/render/state.rs index 27a9b5c67a..6676c8bbab 100644 --- a/desktop/src/render/state.rs +++ b/desktop/src/render/state.rs @@ -29,7 +29,7 @@ pub(crate) struct RenderState { impl RenderState { pub(crate) fn new(window: &Window, context: WgpuContext, present_mode: Option) -> Self { let size = window.surface_size(); - let surface = window.create_surface(context.instance.clone()); + let surface = window.create_surface(&context.instance); let surface_caps = surface.get_capabilities(&context.adapter); let surface_format = surface_caps.formats.iter().find(|f| f.is_srgb()).copied().unwrap_or(surface_caps.formats[0]); diff --git a/desktop/src/window.rs b/desktop/src/window.rs index dcb5d51488..e5eff063ff 100644 --- a/desktop/src/window.rs +++ b/desktop/src/window.rs @@ -86,7 +86,7 @@ impl Window { self.winit_window.request_redraw(); } - pub(crate) fn create_surface(&self, instance: Arc) -> wgpu::Surface<'static> { + pub(crate) fn create_surface(&self, instance: &wgpu::Instance) -> wgpu::Surface<'static> { instance.create_surface(self.winit_window.clone()).unwrap() } diff --git a/node-graph/graph-craft/src/application_io.rs b/node-graph/graph-craft/src/application_io.rs index 1235d771d9..cd2304da31 100644 --- a/node-graph/graph-craft/src/application_io.rs +++ b/node-graph/graph-craft/src/application_io.rs @@ -9,7 +9,7 @@ pub use graphene_application_io::ApplicationIo; #[derive(Default)] pub struct PlatformApplicationIo { #[cfg(feature = "wgpu")] - pub(crate) gpu_executor: Option, + gpu_executor: Option, resources: Option>, } diff --git a/node-graph/graphene-cli/src/main.rs b/node-graph/graphene-cli/src/main.rs index 0f183f12ac..7c876c5d6a 100644 --- a/node-graph/graphene-cli/src/main.rs +++ b/node-graph/graphene-cli/src/main.rs @@ -127,7 +127,7 @@ async fn main() -> Result<(), Box> { // Get reference to wgpu executor and clone device handle let wgpu_executor_ref = application_io_arc.gpu_executor().unwrap(); - let device = wgpu_executor_ref.context.device.clone(); + let device = wgpu_executor_ref.context().device.clone(); let preferences = EditorPreferences { max_render_region_size: EditorPreferences::default().max_render_region_size, diff --git a/node-graph/interpreted-executor/src/node_registry.rs b/node-graph/interpreted-executor/src/node_registry.rs index 7f1c8deb56..0b84eca441 100644 --- a/node-graph/interpreted-executor/src/node_registry.rs +++ b/node-graph/interpreted-executor/src/node_registry.rs @@ -201,6 +201,8 @@ fn node_registry() -> HashMap, input: Context, fn_params: [Context => ListDyn, Context => graphene_std::ContextFeatures]), #[cfg(target_family = "wasm")] async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => CanvasHandle, Context => graphene_std::ContextFeatures]), + async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => wgpu_executor::WgpuPipelineCache, Context => graphene_std::ContextFeatures]), + async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => &wgpu_executor::WgpuExecutor, Context => graphene_std::ContextFeatures]), // ========== // MEMO NODES // ========== @@ -286,6 +288,8 @@ fn node_registry() -> HashMap, input: Context, fn_params: [Context => graphene_std::transform::ScaleType]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::InterpolationDistribution]), async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => RenderIntermediate]), + async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => wgpu_executor::WgpuPipelineCache]), + async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => &wgpu_executor::WgpuExecutor]), ]; // ============= // CONVERT NODES diff --git a/node-graph/interpreted-executor/src/util.rs b/node-graph/interpreted-executor/src/util.rs index 67f79ceac1..277e3c5a77 100644 --- a/node-graph/interpreted-executor/src/util.rs +++ b/node-graph/interpreted-executor/src/util.rs @@ -1,4 +1,3 @@ -use graph_craft::ProtoNodeIdentifier; use graph_craft::application_io::PlatformEditorApi; use graph_craft::concrete; use graph_craft::document::value::TaggedValue; @@ -8,7 +7,6 @@ use graphene_std::Context; use graphene_std::ContextFeatures; use graphene_std::uuid::NodeId; use std::sync::Arc; -use wgpu_executor::WgpuExecutor; pub fn wrap_network_in_scope(network: NodeNetwork, editor_api: Arc) -> NodeNetwork { let inner_network = DocumentNode { @@ -40,7 +38,7 @@ pub fn wrap_network_in_scope(network: NodeNetwork, editor_api: Arc")), - inputs: vec![NodeInput::node(NodeId(2), 0)], - ..Default::default() - }); - scope_injections.push(("wgpu-executor".to_string(), (NodeId(3), concrete!(&WgpuExecutor)))); - } + let scope_injections = vec![("editor-api".to_string(), (NodeId(2), concrete!(&PlatformEditorApi)))]; NodeNetwork { exports: vec![NodeInput::node(NodeId(1), 0)], diff --git a/node-graph/libraries/canvas-utils/src/wasm.rs b/node-graph/libraries/canvas-utils/src/wasm.rs index 9f641b09f1..4b4bd305ff 100644 --- a/node-graph/libraries/canvas-utils/src/wasm.rs +++ b/node-graph/libraries/canvas-utils/src/wasm.rs @@ -62,7 +62,7 @@ impl CanvasSurfaceHandle { if self.1.is_none() { let canvas = self.0.get().canvas.clone(); let surface = executor - .context + .context() .instance .create_surface(wgpu::SurfaceTarget::Canvas(canvas)) .expect("Failed to create surface from canvas"); @@ -86,21 +86,23 @@ impl Canvas for CanvasSurfaceHandle { #[cfg(feature = "wgpu")] impl CanvasSurface for CanvasSurfaceHandle { fn present(&mut self, image_texture: &ImageTexture, executor: &WgpuExecutor) { + let context = executor.context(); + let source_texture: &wgpu::Texture = image_texture.as_ref(); let surface = self.surface(executor); // Blit the texture to the surface - let mut encoder = executor.context.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { + let mut encoder = context.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("Texture to Surface Blit"), }); let size = source_texture.size(); // Configure the surface at physical resolution (for HiDPI displays) - let surface_caps = surface.get_capabilities(&executor.context.adapter); + let surface_caps = surface.get_capabilities(&context.adapter); surface.configure( - &executor.context.device, + &context.device, &wgpu::SurfaceConfiguration { usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_DST, format: wgpu::TextureFormat::Rgba8Unorm, @@ -134,7 +136,7 @@ impl CanvasSurface for CanvasSurfaceHandle { source_texture.size(), ); - executor.context.queue.submit([encoder.finish()]); + context.queue.submit([encoder.finish()]); surface_texture.present(); } } diff --git a/node-graph/libraries/core-types/src/types.rs b/node-graph/libraries/core-types/src/types.rs index 5e70dcbced..282d470ef1 100644 --- a/node-graph/libraries/core-types/src/types.rs +++ b/node-graph/libraries/core-types/src/types.rs @@ -163,6 +163,12 @@ impl ProtoNodeIdentifier { } } +impl From for Cow<'static, str> { + fn from(val: ProtoNodeIdentifier) -> Self { + val.name + } +} + impl Display for ProtoNodeIdentifier { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.debug_tuple("ProtoNodeIdentifier").field(&self.name).finish() diff --git a/node-graph/libraries/wgpu-executor/src/context.rs b/node-graph/libraries/wgpu-executor/src/context.rs index 2e2432d26f..cea37507fd 100644 --- a/node-graph/libraries/wgpu-executor/src/context.rs +++ b/node-graph/libraries/wgpu-executor/src/context.rs @@ -1,12 +1,11 @@ -use std::sync::Arc; use wgpu::{Adapter, Backends, Device, Features, Instance, Queue}; #[derive(Debug, Clone)] pub struct Context { - pub device: Arc, - pub queue: Arc, - pub instance: Arc, - pub adapter: Arc, + pub device: Device, + pub queue: Queue, + pub instance: Instance, + pub adapter: Adapter, } impl Context { @@ -58,12 +57,7 @@ impl ContextBuilder { let instance = self.build_instance(); let adapter = self.request_adapter(&instance).await?; let (device, queue) = self.request_device(&adapter).await?; - Some(Context { - device: Arc::new(device), - queue: Arc::new(queue), - adapter: Arc::new(adapter), - instance: Arc::new(instance), - }) + Some(Context { device, queue, adapter, instance }) } } impl ContextBuilder { @@ -113,12 +107,7 @@ impl ContextBuilder { let adapter = if let Some(adapter) = selected_adapter { adapter } else { self.request_adapter(&instance).await? }; let (device, queue) = self.request_device(&adapter).await?; - Some(Context { - device: Arc::new(device), - queue: Arc::new(queue), - adapter: Arc::new(adapter), - instance: Arc::new(instance), - }) + Some(Context { device, queue, adapter, instance }) } async fn select_adapter(&self, instance: &Instance, select: S) -> Option where diff --git a/node-graph/libraries/wgpu-executor/src/lib.rs b/node-graph/libraries/wgpu-executor/src/lib.rs index 930612bec3..22b6e95ebd 100644 --- a/node-graph/libraries/wgpu-executor/src/lib.rs +++ b/node-graph/libraries/wgpu-executor/src/lib.rs @@ -1,46 +1,59 @@ -mod background; // TODO: Think about where to place this. Likely inlined in the node. Requires refactor of wgpu pipline usage. mod context; -mod resample; +mod pipeline; pub mod shader_runtime; mod texture_cache; pub mod texture_conversion; use std::sync::Arc; -use crate::background::BackgroundCompositor; -use crate::resample::Resampler; use crate::shader_runtime::ShaderRuntime; use crate::texture_cache::TextureCache; use anyhow::Result; use core_types::Color; use core_types::color::SRGBA8; use futures::lock::Mutex; -use glam::{Affine2, UVec2}; +use glam::UVec2; use graphene_application_io::{ApplicationIo, EditorApi}; use vello::{AaConfig, AaSupport, RenderParams, Renderer, RendererOptions, Scene}; use wgpu::{Origin3d, TextureAspect}; pub use context::Context as WgpuContext; pub use context::ContextBuilder as WgpuContextBuilder; +pub use pipeline::AsyncPipeline as AsyncWgpuPipeline; +pub use pipeline::Pipeline as WgpuPipeline; +pub use pipeline::PipelineCache as WgpuPipelineCache; 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)] +#[derive(dyn_any::DynAny, Clone)] pub struct WgpuExecutor { - pub context: WgpuContext, + inner: Arc, +} + +impl WgpuExecutor { + pub fn context(&self) -> &WgpuContext { + &self.inner.context + } + + pub fn shader_runtime(&self) -> &ShaderRuntime { + &self.inner.shader_runtime + } +} + +#[derive(dyn_any::DynAny)] +pub struct WgpuExecutorInner { + context: WgpuContext, texture_cache: Mutex, vello_renderer: Mutex, - resampler: Resampler, - background_compositor: BackgroundCompositor, - pub shader_runtime: ShaderRuntime, + shader_runtime: ShaderRuntime, } impl std::fmt::Debug for WgpuExecutor { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("WgpuExecutor").field("context", &self.context).finish() + f.debug_struct("WgpuExecutor").field("context", &self.context()).finish() } } @@ -65,7 +78,7 @@ impl WgpuExecutor { }; { - let mut renderer = self.vello_renderer.lock().await; + let mut renderer = self.inner.vello_renderer.lock().await; for (image_brush, texture) in context.resource_overrides.iter() { let texture_view = wgpu::TexelCopyTextureInfoBase { texture: texture.clone(), @@ -75,7 +88,7 @@ impl WgpuExecutor { }; renderer.override_image(&image_brush.image, Some(texture_view)); } - renderer.render_to_texture(&self.context.device, &self.context.queue, scene, &texture_view, &render_params)?; + 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); } @@ -84,21 +97,12 @@ 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 composite_background(&self, foreground: &wgpu::Texture, backgrounds: &[rendering::Background], document_to_screen: Affine2, zoom: f32) -> Arc { - let size = foreground.size(); - let output = self.request_texture(UVec2::new(size.width, size.height)).await; - self.background_compositor.composite(&self.context, foreground, &output, backgrounds, document_to_screen, zoom); - output + pub fn pipeline_init(&self, pipeline: &WgpuPipelineCache) { + pipeline.init::

(self); } pub async fn request_texture(&self, size: UVec2) -> Arc { - self.texture_cache.lock().await.request_texture(&self.context.device, size) + self.inner.texture_cache.lock().await.request_texture(&self.context().device, size) } } @@ -122,17 +126,15 @@ 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, + inner: Arc::new(WgpuExecutorInner { + context, + texture_cache: texture_cache.into(), + vello_renderer: vello_renderer.into(), + shader_runtime, + }), }) } } 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..69ab51e32a --- /dev/null +++ b/node-graph/libraries/wgpu-executor/src/pipeline.rs @@ -0,0 +1,72 @@ +use dyn_any::StaticType; +use std::any::Any; +use std::future::Future; +use std::pin::Pin; +use std::sync::{Arc, OnceLock}; + +use crate::WgpuExecutor; + +pub type PipelineFuture<'a, T> = Pin + Send + 'a>>; + +pub trait Pipeline: Any + Send + Sync + Sized { + type Args<'a>; + type Out: Send; + + fn create(executor: &WgpuExecutor) -> Self; + + fn run<'a>(&'a self, executor: &'a WgpuExecutor, args: &'a Self::Args<'_>) -> PipelineFuture<'a, Self::Out>; +} + +pub trait AsyncPipeline: Any + Send + Sync + Sized { + type Args<'a>; + type Out: Send; + + fn create(executor: &WgpuExecutor) -> 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(executor: &WgpuExecutor) -> Self { +

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

::run(self, executor, args)) + } +} + +#[derive(Default, Clone)] +pub struct PipelineCache { + pipeline: Arc>>, + executor: Arc>, +} + +impl PipelineCache { + pub(super) fn init(&self, executor: &WgpuExecutor) { + self.executor.get_or_init(|| executor.clone()); + self.pipeline.get_or_init(|| Box::new(P::create(executor))); + } + + pub async fn run(&self, args: &P::Args<'_>) -> P::Out { + let executor = self.executor.get().expect("PipelineCache not initialized"); + let entry = self.pipeline.get().expect("PipelineCache not initialized"); + let pipeline = (&**entry) + .downcast_ref::

() + .unwrap_or_else(|| panic!("PipelineCache type mismatch: run::<{}>() but init used a different pipeline type", std::any::type_name::

(),)); + pipeline.run(executor, args).await + } +} + +impl std::fmt::Debug for PipelineCache { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("PipelineCache").field("initialized", &self.pipeline.get().is_some()).finish() + } +} + +unsafe impl StaticType for PipelineCache { + type Static = PipelineCache; +} 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/libraries/wgpu-executor/src/texture_conversion.rs b/node-graph/libraries/wgpu-executor/src/texture_conversion.rs index fef0975299..afecb7a62d 100644 --- a/node-graph/libraries/wgpu-executor/src/texture_conversion.rs +++ b/node-graph/libraries/wgpu-executor/src/texture_conversion.rs @@ -14,7 +14,7 @@ use wgpu::{Extent3d, TextureDescriptor, TextureDimension, TextureFormat, Texture /// /// Creates a new WGPU texture with RGBA8UnormSrgb format and uploads the provided /// image data. The texture is configured for binding, copying, and source operations. -fn upload_to_texture(device: &std::sync::Arc, queue: &std::sync::Arc, image: &Raster) -> wgpu::Texture { +fn upload_to_texture(device: &wgpu::Device, queue: &wgpu::Queue, image: &Raster) -> wgpu::Texture { let rgba8_data: Vec = image.data.iter().map(|x| (*x).into()).collect(); device.create_texture_with_data( @@ -52,7 +52,7 @@ struct RasterGpuToRasterCpuConverter { padded_bytes_per_row: u32, } impl RasterGpuToRasterCpuConverter { - fn new(device: &std::sync::Arc, encoder: &mut wgpu::CommandEncoder, data_gpu: Raster) -> Self { + fn new(device: &wgpu::Device, encoder: &mut wgpu::CommandEncoder, data_gpu: Raster) -> Self { let texture = data_gpu.data(); let width = texture.width(); let height = texture.height(); @@ -100,7 +100,7 @@ impl RasterGpuToRasterCpuConverter { } } - async fn convert(self, device: &std::sync::Arc) -> Result, wgpu::BufferAsyncError> { + async fn convert(self, device: &wgpu::Device) -> Result, wgpu::BufferAsyncError> { let buffer_slice = self.buffer.slice(..); let (sender, receiver) = futures::channel::oneshot::channel(); buffer_slice.map_async(wgpu::MapMode::Read, move |result| { @@ -149,8 +149,8 @@ impl<'i> Convert>, &'i WgpuExecutor> for List> { /// Converts a `List>` to `List>` by uploading each image to a texture impl<'i> Convert>, &'i WgpuExecutor> for List> { async fn convert(self, _: Footprint, executor: &'i WgpuExecutor) -> List> { - let device = &executor.context.device; - let queue = &executor.context.queue; + let device = &executor.context().device; + let queue = &executor.context().queue; let list = self .into_iter() .map(|row| { @@ -169,8 +169,8 @@ impl<'i> Convert>, &'i WgpuExecutor> for List> { /// Converts single CPU raster to GPU by uploading to texture impl<'i> Convert, &'i WgpuExecutor> for Raster { async fn convert(self, _: Footprint, executor: &'i WgpuExecutor) -> Raster { - let device = &executor.context.device; - let queue = &executor.context.queue; + let device = &executor.context().device; + let queue = &executor.context().queue; let texture = upload_to_texture(device, queue, &self); queue.submit([]); @@ -188,8 +188,8 @@ impl<'i> Convert>, &'i WgpuExecutor> for List> { /// Converts a `List>` to `List>` by downloading texture data in one go then asynchronously maps all buffers and processes the results. impl<'i> Convert>, &'i WgpuExecutor> for List> { async fn convert(self, _: Footprint, executor: &'i WgpuExecutor) -> List> { - let device = &executor.context.device; - let queue = &executor.context.queue; + let device = &executor.context().device; + let queue = &executor.context().queue; let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("batch_texture_download_encoder"), @@ -230,8 +230,8 @@ impl<'i> Convert>, &'i WgpuExecutor> for List> { /// Converts single GPU raster to CPU by downloading texture data impl<'i> Convert, &'i WgpuExecutor> for Raster { async fn convert(self, _: Footprint, executor: &'i WgpuExecutor) -> Raster { - let device = &executor.context.device; - let queue = &executor.context.queue; + let device = &executor.context().device; + let queue = &executor.context().queue; let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("single_texture_download_encoder"), diff --git a/node-graph/node-macro/src/shader_nodes/per_pixel_adjust.rs b/node-graph/node-macro/src/shader_nodes/per_pixel_adjust.rs index 06a1c73eff..20ee1ec731 100644 --- a/node-graph/node-macro/src/shader_nodes/per_pixel_adjust.rs +++ b/node-graph/node-macro/src/shader_nodes/per_pixel_adjust.rs @@ -233,7 +233,7 @@ impl PerPixelAdjustCodegen<'_> { ty: ParsedFieldType::Regular(RegularParsedField { ty: parse_quote!(&'a WgpuExecutor), exposed: true, - value_source: ParsedValueSource::Scope(parse_quote!("wgpu-executor")), + value_source: ParsedValueSource::Scope(parse_quote!("graphene_std::platform_application_io::WgpuExecutorNode")), number_soft_min: None, number_soft_max: None, number_hard_min: None, @@ -283,7 +283,7 @@ impl PerPixelAdjustCodegen<'_> { let entry_point_name = &self.entry_point_name; let body = quote! { { - #executor.shader_runtime.run_per_pixel_adjust(&::wgpu_executor::shader_runtime::per_pixel_adjust_runtime::Shaders { + #executor.shader_runtime().run_per_pixel_adjust(&::wgpu_executor::shader_runtime::per_pixel_adjust_runtime::Shaders { wgsl_shader: crate::WGSL_SHADER, fragment_shader_name: super::#entry_point_name, has_uniform: #has_uniform, 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 1f43cc43c0..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. / 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/nodes/gstd/src/platform_application_io.rs b/node-graph/nodes/gstd/src/platform_application_io.rs index b7d38413f8..e19450fe15 100644 --- a/node-graph/nodes/gstd/src/platform_application_io.rs +++ b/node-graph/nodes/gstd/src/platform_application_io.rs @@ -268,3 +268,8 @@ pub async fn resource<'a: 'n>(_: impl Ctx, hash: ResourceHash, #[scope("editor-a panic!("Resource {hash} not found"); }) } + +#[node_macro::node(category(""), inject_scope)] +pub async fn wgpu_executor<'a: 'n>(_: impl Ctx, #[scope("editor-api")] editor_api: &'a PlatformEditorApi) -> &'a ::wgpu_executor::WgpuExecutor { + editor_api.application_io.as_ref().unwrap().gpu_executor().expect("GPU executor not available") +} 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 efa0f9b100..a1c8a0df49 100644 --- a/node-graph/libraries/wgpu-executor/src/background/mod.rs +++ b/node-graph/nodes/gstd/src/render_background.rs @@ -1,5 +1,131 @@ -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::document::value::{RenderOutput, RenderOutputType}; +use rendering::{RenderParams, SvgRender, SvgRenderOutput}; +use std::fmt::Write; +use std::sync::Arc; use wgpu::util::DeviceExt; +use wgpu_executor::{AsyncWgpuPipeline, WgpuExecutor, WgpuPipelineCache}; + +#[node_macro::node(category(""))] +async fn render_background<'a: 'n>( + ctx: impl Ctx + ExtractFootprint + ExtractVarArgs, + #[scope(background_compositor_pipeline::IDENTIFIER)] pipeline: WgpuPipelineCache, + 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) => { + let doc_to_screen = (glam::DAffine2::from_scale(glam::DVec2::splat(render_params.scale)) * render_params.footprint.transform).as_affine2(); + let blended = pipeline + .run::(&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()) + } + 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(""), inject_scope)] +async fn background_compositor_pipeline<'a: 'n>( + _ctx: impl Ctx, + #[scope(crate::platform_application_io::wgpu_executor::IDENTIFIER)] executor: &'a WgpuExecutor, + #[data] pipeline: WgpuPipelineCache, +) -> WgpuPipelineCache { + executor.pipeline_init::(pipeline); + pipeline.clone() +} pub struct BackgroundCompositor { checker_rect_pipeline: wgpu::RenderPipeline, @@ -10,12 +136,23 @@ pub struct BackgroundCompositor { sampler: wgpu::Sampler, } -impl BackgroundCompositor { - pub fn new(device: &wgpu::Device) -> Self { +pub struct BackgroundCompositorArgs<'a> { + foreground: &'a wgpu::Texture, + backgrounds: &'a [rendering::Background], + document_to_screen: Affine2, + zoom: f32, +} + +impl AsyncWgpuPipeline for BackgroundCompositor { + type Args<'a> = BackgroundCompositorArgs<'a>; + type Out = Arc; + + fn create(executor: &WgpuExecutor) -> Self { + let device = &executor.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,13 +326,23 @@ 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 &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. { - return; + return output; } - let device = &context.device; - let queue = &context.queue; + let device = &executor.context().device; + let queue = &executor.context().queue; let checker_size_doc = 8. / zoom; let screen_to_document = document_to_screen.inverse(); @@ -285,8 +432,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_cache.rs b/node-graph/nodes/gstd/src/render_cache.rs index 05e5e4617b..cdf3a310ec 100644 --- a/node-graph/nodes/gstd/src/render_cache.rs +++ b/node-graph/nodes/gstd/src/render_cache.rs @@ -5,14 +5,13 @@ use core_types::transform::{Footprint, RenderQuality, Transform}; use core_types::{CloneVarArgs, Context, Ctx, ExtractAll, ExtractAnimationTime, ExtractPointerPosition, ExtractRealTime, OwnedContextImpl}; use glam::{DAffine2, DVec2, IVec2, UVec2}; use graph_craft::application_io::PlatformEditorApi; -use graph_craft::document::value::RenderOutput; -use graphene_application_io::{ApplicationIo, ImageTexture}; +use graph_craft::document::value::{RenderOutput, RenderOutputType}; +use graphene_application_io::ImageTexture; use rendering::{RenderOutputType as RenderOutputTypeRequest, RenderParams}; use std::collections::HashSet; use std::hash::Hash; use std::sync::{Arc, Mutex}; - -use crate::render_node::RenderOutputType; +use wgpu_executor::WgpuExecutor; pub const TILE_SIZE: u32 = 256; pub const MAX_CACHE_MEMORY_BYTES: usize = 512 * 1024 * 1024; @@ -327,7 +326,8 @@ fn flood_fill(start: &TileCoord, tile_set: &HashSet, visited: &mut Ha #[node_macro::node(category(""))] pub async fn render_output_cache<'a: 'n>( ctx: impl Ctx + ExtractAll + CloneVarArgs + ExtractRealTime + ExtractAnimationTime + ExtractPointerPosition + Sync, - editor_api: &'a PlatformEditorApi, + #[scope(crate::platform_application_io::wgpu_executor::IDENTIFIER)] executor: &'a WgpuExecutor, + #[scope("editor-api")] editor_api: &'a PlatformEditorApi, data: impl Node, Output = RenderOutput> + Send + Sync, #[data] tile_cache: TileCache, ) -> RenderOutput { @@ -404,11 +404,9 @@ pub async fn render_output_cache<'a: 'n>( return data.eval(context.into_context()).await; } - let exec = editor_api.application_io.as_ref().unwrap().gpu_executor().unwrap(); - - let output_texture = exec.request_texture(physical_resolution).await; + let output_texture = executor.request_texture(physical_resolution).await; - let combined_metadata = composite_cached_regions(&all_regions, &output_texture, &device_origin_offset, &footprint.transform, exec); + let combined_metadata = composite_cached_regions(&all_regions, &output_texture, &device_origin_offset, &footprint.transform, &executor); RenderOutput { data: RenderOutputType::Texture(output_texture.into()), @@ -473,8 +471,8 @@ fn composite_cached_regions( viewport_transform: &DAffine2, exec: &wgpu_executor::WgpuExecutor, ) -> rendering::RenderMetadata { - let device = &exec.context.device; - let queue = &exec.context.queue; + let device = &exec.context().device; + let queue = &exec.context().queue; let output_resolution = UVec2::new(output_texture.width(), output_texture.height()); let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("composite") }); diff --git a/node-graph/nodes/gstd/src/render_node.rs b/node-graph/nodes/gstd/src/render_node.rs index 121131a062..d9181ef5cd 100644 --- a/node-graph/nodes/gstd/src/render_node.rs +++ b/node-graph/nodes/gstd/src/render_node.rs @@ -1,22 +1,15 @@ 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; -use graph_craft::document::value::RenderOutput; -pub use graph_craft::document::value::RenderOutputType; -use graphene_application_io::{ApplicationIo, ExportFormat, RenderConfig}; +use graph_craft::document::value::{RenderOutput, RenderOutputType}; +use graphene_application_io::{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; +use wgpu_executor::{RenderContext, WgpuExecutor}; #[derive(Clone, dyn_any::DynAny)] pub enum RenderIntermediateType { @@ -80,7 +73,11 @@ async fn render_intermediate<'a: 'n, T: 'static + Render + WasmNotSend + Send + } #[node_macro::node(category(""))] -async fn render<'a: 'n>(ctx: impl Ctx + ExtractFootprint + ExtractVarArgs, editor_api: &'a PlatformEditorApi, data: RenderIntermediate) -> RenderOutput { +async fn render<'a: 'n>( + ctx: impl Ctx + ExtractFootprint + ExtractVarArgs, + #[scope(crate::platform_application_io::wgpu_executor::IDENTIFIER)] executor: &'a WgpuExecutor, + data: RenderIntermediate, +) -> RenderOutput { let footprint = ctx.footprint(); let render_params = ctx .vararg(0) @@ -109,9 +106,6 @@ async fn render<'a: 'n>(ctx: impl Ctx + ExtractFootprint + ExtractVarArgs, edito } } (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 scale = render_params.scale; let physical_resolution = render_params.footprint.resolution; @@ -138,7 +132,7 @@ async fn render<'a: 'n>(ctx: impl Ctx + ExtractFootprint + ExtractVarArgs, edito } } - let texture = exec + let texture = executor .render_vello_scene(&transformed_scene, physical_resolution, context, None) .await .expect("Failed to render Vello scene"); @@ -150,107 +144,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..331ddc29ca --- /dev/null +++ b/node-graph/nodes/gstd/src/render_pixel_preview.rs @@ -0,0 +1,230 @@ +use core_types::transform::{Footprint, Transform}; +use core_types::{CloneVarArgs, Context, Ctx, ExtractAll, OwnedContextImpl}; +use glam::{DAffine2, DVec2, UVec2, Vec2}; +use graph_craft::document::value::{RenderOutput, RenderOutputType}; +use rendering::{RenderOutputType as RenderOutputTypeRequest, RenderParams}; +use std::sync::Arc; +use vector_types::vector::style::RenderMode; +use wgpu_executor::{AsyncWgpuPipeline, WgpuExecutor, WgpuPipelineCache}; + +#[node_macro::node(category(""))] +pub async fn render_pixel_preview<'a: 'n>( + ctx: impl Ctx + ExtractAll + CloneVarArgs + Sync, + #[scope(pixel_preview_pipeline::IDENTIFIER)] pipeline: WgpuPipelineCache, + 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. / 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 resampled = pipeline + .run::(&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 +} + +#[node_macro::node(category(""), inject_scope)] +async fn pixel_preview_pipeline<'a: 'n>( + _ctx: impl Ctx, + #[scope(crate::platform_application_io::wgpu_executor::IDENTIFIER)] executor: &'a WgpuExecutor, + #[data] pipeline: WgpuPipelineCache, +) -> WgpuPipelineCache { + executor.pipeline_init::(pipeline); + pipeline.clone() +} + +pub struct PixelPreview { + pipeline: wgpu::RenderPipeline, + bind_group_layout: wgpu::BindGroupLayout, +} + +pub struct ResamplerArgs<'a> { + source: &'a wgpu::Texture, + transform: &'a DAffine2, + size: UVec2, +} + +impl AsyncWgpuPipeline for PixelPreview { + type Args<'a> = ResamplerArgs<'a>; + type Out = Arc; + + fn create(executor: &WgpuExecutor) -> Self { + let device = &executor.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, + }); + + PixelPreview { 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