Extract pipelines from WgpuExecutor into scope-provided pipeline nodes (#4210)

* Implement generic pipeline caching

* Fix WIP

* Fix

* Fix

* Fix

* Fix bench

* Migrations

* Review
This commit is contained in:
Timon
2026-06-19 21:20:25 +00:00
committed by GitHub
parent 3bf32443ed
commit 674b3a213f
29 changed files with 630 additions and 444 deletions
Generated
+1
View File
@@ -2042,6 +2042,7 @@ dependencies = [
"base64", "base64",
"blending-nodes", "blending-nodes",
"brush-nodes", "brush-nodes",
"bytemuck",
"core-types", "core-types",
"dyn-any", "dyn-any",
"glam", "glam",
+1 -1
View File
@@ -29,7 +29,7 @@ pub(crate) struct RenderState {
impl RenderState { impl RenderState {
pub(crate) fn new(window: &Window, context: WgpuContext, present_mode: Option<PresentMode>) -> Self { pub(crate) fn new(window: &Window, context: WgpuContext, present_mode: Option<PresentMode>) -> Self {
let size = window.surface_size(); 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_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]); let surface_format = surface_caps.formats.iter().find(|f| f.is_srgb()).copied().unwrap_or(surface_caps.formats[0]);
+1 -1
View File
@@ -86,7 +86,7 @@ impl Window {
self.winit_window.request_redraw(); self.winit_window.request_redraw();
} }
pub(crate) fn create_surface(&self, instance: Arc<wgpu::Instance>) -> wgpu::Surface<'static> { pub(crate) fn create_surface(&self, instance: &wgpu::Instance) -> wgpu::Surface<'static> {
instance.create_surface(self.winit_window.clone()).unwrap() instance.create_surface(self.winit_window.clone()).unwrap()
} }
@@ -922,7 +922,7 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
exports: vec![NodeInput::node(NodeId(1), 0)], exports: vec![NodeInput::node(NodeId(1), 0)],
nodes: [ nodes: [
DocumentNode { DocumentNode {
inputs: vec![NodeInput::value(TaggedValue::None, false), NodeInput::scope("editor-api"), NodeInput::import(concrete!(String), 1)], inputs: vec![NodeInput::value(TaggedValue::None, false), NodeInput::import(concrete!(String), 1)],
implementation: DocumentNodeImplementation::ProtoNode(platform_application_io::load_resource::IDENTIFIER), implementation: DocumentNodeImplementation::ProtoNode(platform_application_io::load_resource::IDENTIFIER),
..Default::default() ..Default::default()
}, },
@@ -1249,22 +1249,17 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
node_template: NodeTemplate { node_template: NodeTemplate {
document_node: DocumentNode { document_node: DocumentNode {
implementation: DocumentNodeImplementation::Network(NodeNetwork { implementation: DocumentNodeImplementation::Network(NodeNetwork {
exports: vec![NodeInput::node(NodeId(2), 0)], exports: vec![NodeInput::node(NodeId(1), 0)],
nodes: [ nodes: [
DocumentNode { DocumentNode {
inputs: vec![NodeInput::scope("editor-api")], inputs: vec![NodeInput::import(concrete!(List<Raster<CPU>>), 0), NodeInput::scope(platform_application_io::wgpu_executor::IDENTIFIER)],
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::ops::IntoNode<&WgpuExecutor>")),
..Default::default()
},
DocumentNode {
inputs: vec![NodeInput::import(concrete!(List<Raster<CPU>>), 0), NodeInput::node(NodeId(0), 0)],
call_argument: generic!(T), call_argument: generic!(T),
implementation: DocumentNodeImplementation::ProtoNode(wgpu_executor::texture_conversion::upload_texture::IDENTIFIER), implementation: DocumentNodeImplementation::ProtoNode(wgpu_executor::texture_conversion::upload_texture::IDENTIFIER),
..Default::default() ..Default::default()
}, },
DocumentNode { DocumentNode {
call_argument: generic!(T), call_argument: generic!(T),
inputs: vec![NodeInput::node(NodeId(1), 0)], inputs: vec![NodeInput::node(NodeId(0), 0)],
implementation: DocumentNodeImplementation::ProtoNode(memo::memoize::IDENTIFIER), implementation: DocumentNodeImplementation::ProtoNode(memo::memoize::IDENTIFIER),
..Default::default() ..Default::default()
}, },
@@ -2362,6 +2362,28 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
} }
} }
// `load_resource` no longer takes the `editor-api`
if reference == DefinitionIdentifier::ProtoNode(graphene_std::platform_application_io::load_resource::IDENTIFIER) && inputs_count == 3 {
let mut node_template = resolve_document_node_type(&reference)?.default_node_template();
let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template)?;
document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path);
document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[2].clone(), network_path);
}
// `wgpu-executor` scope was removed, change to the auto injected scope node `graphene_std::platform_application_io::wgpu_executor`
for (i, input) in node.inputs.iter().enumerate() {
if let NodeInput::Scope(name) = input
&& *name == "wgpu-executor"
{
document.network_interface.set_input(
&InputConnector::node(*node_id, i),
NodeInput::Scope("graphene_std::platform_application_io::WgpuExecutorNode".into()),
network_path,
);
}
}
// ================================== // ==================================
// PUT ALL MIGRATIONS ABOVE THIS LINE // PUT ALL MIGRATIONS ABOVE THIS LINE
// ================================== // ==================================
+1 -1
View File
@@ -9,7 +9,7 @@ pub use graphene_application_io::ApplicationIo;
#[derive(Default)] #[derive(Default)]
pub struct PlatformApplicationIo { pub struct PlatformApplicationIo {
#[cfg(feature = "wgpu")] #[cfg(feature = "wgpu")]
pub(crate) gpu_executor: Option<WgpuExecutor>, gpu_executor: Option<WgpuExecutor>,
resources: Option<Box<dyn resource::LoadResource>>, resources: Option<Box<dyn resource::LoadResource>>,
} }
+1 -1
View File
@@ -127,7 +127,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
// Get reference to wgpu executor and clone device handle // Get reference to wgpu executor and clone device handle
let wgpu_executor_ref = application_io_arc.gpu_executor().unwrap(); 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 { let preferences = EditorPreferences {
max_render_region_size: EditorPreferences::default().max_render_region_size, max_render_region_size: EditorPreferences::default().max_render_region_size,
@@ -201,6 +201,10 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => ListDyn, Context => graphene_std::ContextFeatures]), async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => ListDyn, Context => graphene_std::ContextFeatures]),
#[cfg(target_family = "wasm")] #[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 => CanvasHandle, Context => graphene_std::ContextFeatures]),
async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => &PlatformEditorApi, Context => graphene_std::ContextFeatures]),
async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => &wgpu_executor::WgpuExecutor, Context => graphene_std::ContextFeatures]),
async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => Option<&wgpu_executor::WgpuExecutor>, Context => graphene_std::ContextFeatures]),
async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => wgpu_executor::WgpuPipelineCache, Context => graphene_std::ContextFeatures]),
// ========== // ==========
// MEMO NODES // MEMO NODES
// ========== // ==========
@@ -286,6 +290,9 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => graphene_std::transform::ScaleType]), async_node!(graphene_core::memo::MemoizeNode<_, _>, 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 => 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 => RenderIntermediate]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => &wgpu_executor::WgpuExecutor]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => Option<&wgpu_executor::WgpuExecutor>]),
async_node!(graphene_core::memo::MemoizeNode<_, _>, input: Context, fn_params: [Context => wgpu_executor::WgpuPipelineCache]),
]; ];
// ============= // =============
// CONVERT NODES // CONVERT NODES
+15 -19
View File
@@ -1,4 +1,3 @@
use graph_craft::ProtoNodeIdentifier;
use graph_craft::application_io::PlatformEditorApi; use graph_craft::application_io::PlatformEditorApi;
use graph_craft::concrete; use graph_craft::concrete;
use graph_craft::document::value::TaggedValue; use graph_craft::document::value::TaggedValue;
@@ -8,7 +7,6 @@ use graphene_std::Context;
use graphene_std::ContextFeatures; use graphene_std::ContextFeatures;
use graphene_std::uuid::NodeId; use graphene_std::uuid::NodeId;
use std::sync::Arc; use std::sync::Arc;
use wgpu_executor::WgpuExecutor;
pub fn wrap_network_in_scope(network: NodeNetwork, editor_api: Arc<PlatformEditorApi>) -> NodeNetwork { pub fn wrap_network_in_scope(network: NodeNetwork, editor_api: Arc<PlatformEditorApi>) -> NodeNetwork {
let inner_network = DocumentNode { let inner_network = DocumentNode {
@@ -40,7 +38,7 @@ pub fn wrap_network_in_scope(network: NodeNetwork, editor_api: Arc<PlatformEdito
}, },
DocumentNode { DocumentNode {
call_argument: concrete!(Context), call_argument: concrete!(Context),
inputs: vec![NodeInput::scope("editor-api"), NodeInput::node(NodeId(0), 0)], inputs: vec![NodeInput::scope(graphene_std::platform_application_io::try_wgpu_executor::IDENTIFIER), NodeInput::node(NodeId(0), 0)],
implementation: DocumentNodeImplementation::ProtoNode(graphene_std::render_node::render::IDENTIFIER), implementation: DocumentNodeImplementation::ProtoNode(graphene_std::render_node::render::IDENTIFIER),
context_features: graphene_std::ContextDependencies { context_features: graphene_std::ContextDependencies {
extract: ContextFeatures::FOOTPRINT | ContextFeatures::VARARGS, extract: ContextFeatures::FOOTPRINT | ContextFeatures::VARARGS,
@@ -50,7 +48,11 @@ pub fn wrap_network_in_scope(network: NodeNetwork, editor_api: Arc<PlatformEdito
}, },
DocumentNode { DocumentNode {
call_argument: concrete!(Context), call_argument: concrete!(Context),
inputs: vec![NodeInput::scope("editor-api"), NodeInput::node(NodeId(1), 0)], inputs: vec![
NodeInput::scope(graphene_std::platform_application_io::try_wgpu_executor::IDENTIFIER),
NodeInput::scope(graphene_std::platform_application_io::editor_api::IDENTIFIER),
NodeInput::node(NodeId(1), 0),
],
implementation: DocumentNodeImplementation::ProtoNode(graphene_std::render_cache::render_output_cache::IDENTIFIER), implementation: DocumentNodeImplementation::ProtoNode(graphene_std::render_cache::render_output_cache::IDENTIFIER),
context_features: graphene_std::ContextDependencies { context_features: graphene_std::ContextDependencies {
extract: ContextFeatures::FOOTPRINT | ContextFeatures::VARARGS, extract: ContextFeatures::FOOTPRINT | ContextFeatures::VARARGS,
@@ -60,8 +62,8 @@ pub fn wrap_network_in_scope(network: NodeNetwork, editor_api: Arc<PlatformEdito
}, },
DocumentNode { DocumentNode {
call_argument: concrete!(Context), call_argument: concrete!(Context),
inputs: vec![NodeInput::scope("editor-api"), NodeInput::node(NodeId(2), 0)], inputs: vec![NodeInput::scope(graphene_std::render_pixel_preview::pixel_preview_pipeline::IDENTIFIER), NodeInput::node(NodeId(2), 0)],
implementation: DocumentNodeImplementation::ProtoNode(graphene_std::pixel_preview::pixel_preview::IDENTIFIER), implementation: DocumentNodeImplementation::ProtoNode(graphene_std::render_pixel_preview::render_pixel_preview::IDENTIFIER),
context_features: graphene_std::ContextDependencies { context_features: graphene_std::ContextDependencies {
extract: ContextFeatures::FOOTPRINT | ContextFeatures::VARARGS, extract: ContextFeatures::FOOTPRINT | ContextFeatures::VARARGS,
inject: ContextFeatures::FOOTPRINT | ContextFeatures::VARARGS, inject: ContextFeatures::FOOTPRINT | ContextFeatures::VARARGS,
@@ -70,8 +72,11 @@ pub fn wrap_network_in_scope(network: NodeNetwork, editor_api: Arc<PlatformEdito
}, },
DocumentNode { DocumentNode {
call_argument: concrete!(Context), call_argument: concrete!(Context),
inputs: vec![NodeInput::scope("editor-api"), NodeInput::node(NodeId(3), 0)], inputs: vec![
implementation: DocumentNodeImplementation::ProtoNode(graphene_std::render_node::render_background::IDENTIFIER), NodeInput::scope(graphene_std::render_background::composite_background_pipeline::IDENTIFIER),
NodeInput::node(NodeId(3), 0),
],
implementation: DocumentNodeImplementation::ProtoNode(graphene_std::render_background::render_background::IDENTIFIER),
context_features: graphene_std::ContextDependencies { context_features: graphene_std::ContextDependencies {
extract: ContextFeatures::FOOTPRINT | ContextFeatures::VARARGS, extract: ContextFeatures::FOOTPRINT | ContextFeatures::VARARGS,
inject: ContextFeatures::empty(), inject: ContextFeatures::empty(),
@@ -100,7 +105,7 @@ pub fn wrap_network_in_scope(network: NodeNetwork, editor_api: Arc<PlatformEdito
}; };
// wrap the inner network in a scope // wrap the inner network in a scope
let mut nodes = vec![ let nodes = vec![
inner_network, inner_network,
render_node, render_node,
DocumentNode { DocumentNode {
@@ -109,16 +114,7 @@ pub fn wrap_network_in_scope(network: NodeNetwork, editor_api: Arc<PlatformEdito
..Default::default() ..Default::default()
}, },
]; ];
let mut scope_injections = vec![("editor-api".to_string(), (NodeId(2), concrete!(&PlatformEditorApi)))]; let scope_injections = vec![("editor-api".to_string(), (NodeId(2), concrete!(&PlatformEditorApi)))];
if cfg!(feature = "gpu") {
nodes.push(DocumentNode {
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::ops::IntoNode<&WgpuExecutor>")),
inputs: vec![NodeInput::node(NodeId(2), 0)],
..Default::default()
});
scope_injections.push(("wgpu-executor".to_string(), (NodeId(3), concrete!(&WgpuExecutor))));
}
NodeNetwork { NodeNetwork {
exports: vec![NodeInput::node(NodeId(1), 0)], exports: vec![NodeInput::node(NodeId(1), 0)],
@@ -62,7 +62,7 @@ impl CanvasSurfaceHandle {
if self.1.is_none() { if self.1.is_none() {
let canvas = self.0.get().canvas.clone(); let canvas = self.0.get().canvas.clone();
let surface = executor let surface = executor
.context .context()
.instance .instance
.create_surface(wgpu::SurfaceTarget::Canvas(canvas)) .create_surface(wgpu::SurfaceTarget::Canvas(canvas))
.expect("Failed to create surface from canvas"); .expect("Failed to create surface from canvas");
@@ -86,21 +86,23 @@ impl Canvas for CanvasSurfaceHandle {
#[cfg(feature = "wgpu")] #[cfg(feature = "wgpu")]
impl CanvasSurface for CanvasSurfaceHandle { impl CanvasSurface for CanvasSurfaceHandle {
fn present(&mut self, image_texture: &ImageTexture, executor: &WgpuExecutor) { fn present(&mut self, image_texture: &ImageTexture, executor: &WgpuExecutor) {
let context = executor.context();
let source_texture: &wgpu::Texture = image_texture.as_ref(); let source_texture: &wgpu::Texture = image_texture.as_ref();
let surface = self.surface(executor); let surface = self.surface(executor);
// Blit the texture to the surface // 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"), label: Some("Texture to Surface Blit"),
}); });
let size = source_texture.size(); let size = source_texture.size();
// Configure the surface at physical resolution (for HiDPI displays) // 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( surface.configure(
&executor.context.device, &context.device,
&wgpu::SurfaceConfiguration { &wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_DST, usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_DST,
format: wgpu::TextureFormat::Rgba8Unorm, format: wgpu::TextureFormat::Rgba8Unorm,
@@ -134,7 +136,7 @@ impl CanvasSurface for CanvasSurfaceHandle {
source_texture.size(), source_texture.size(),
); );
executor.context.queue.submit([encoder.finish()]); context.queue.submit([encoder.finish()]);
surface_texture.present(); surface_texture.present();
} }
} }
@@ -163,6 +163,12 @@ impl ProtoNodeIdentifier {
} }
} }
impl From<ProtoNodeIdentifier> for Cow<'static, str> {
fn from(val: ProtoNodeIdentifier) -> Self {
val.name
}
}
impl Display for ProtoNodeIdentifier { impl Display for ProtoNodeIdentifier {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("ProtoNodeIdentifier").field(&self.name).finish() f.debug_tuple("ProtoNodeIdentifier").field(&self.name).finish()
@@ -1,12 +1,11 @@
use std::sync::Arc;
use wgpu::{Adapter, Backends, Device, Features, Instance, Queue}; use wgpu::{Adapter, Backends, Device, Features, Instance, Queue};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Context { pub struct Context {
pub device: Arc<Device>, pub device: Device,
pub queue: Arc<Queue>, pub queue: Queue,
pub instance: Arc<Instance>, pub instance: Instance,
pub adapter: Arc<Adapter>, pub adapter: Adapter,
} }
impl Context { impl Context {
@@ -58,12 +57,7 @@ impl ContextBuilder {
let instance = self.build_instance(); let instance = self.build_instance();
let adapter = self.request_adapter(&instance).await?; let adapter = self.request_adapter(&instance).await?;
let (device, queue) = self.request_device(&adapter).await?; let (device, queue) = self.request_device(&adapter).await?;
Some(Context { Some(Context { device, queue, adapter, instance })
device: Arc::new(device),
queue: Arc::new(queue),
adapter: Arc::new(adapter),
instance: Arc::new(instance),
})
} }
} }
impl ContextBuilder { impl ContextBuilder {
@@ -113,12 +107,7 @@ impl ContextBuilder {
let adapter = if let Some(adapter) = selected_adapter { adapter } else { self.request_adapter(&instance).await? }; let adapter = if let Some(adapter) = selected_adapter { adapter } else { self.request_adapter(&instance).await? };
let (device, queue) = self.request_device(&adapter).await?; let (device, queue) = self.request_device(&adapter).await?;
Some(Context { Some(Context { device, queue, adapter, instance })
device: Arc::new(device),
queue: Arc::new(queue),
adapter: Arc::new(adapter),
instance: Arc::new(instance),
})
} }
async fn select_adapter<S>(&self, instance: &Instance, select: S) -> Option<Adapter> async fn select_adapter<S>(&self, instance: &Instance, select: S) -> Option<Adapter>
where where
+35 -33
View File
@@ -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 context;
mod resample; mod pipeline;
pub mod shader_runtime; pub mod shader_runtime;
mod texture_cache; mod texture_cache;
pub mod texture_conversion; pub mod texture_conversion;
use std::sync::Arc; use std::sync::Arc;
use crate::background::BackgroundCompositor;
use crate::resample::Resampler;
use crate::shader_runtime::ShaderRuntime; use crate::shader_runtime::ShaderRuntime;
use crate::texture_cache::TextureCache; use crate::texture_cache::TextureCache;
use anyhow::Result; use anyhow::Result;
use core_types::Color; use core_types::Color;
use core_types::color::SRGBA8; use core_types::color::SRGBA8;
use futures::lock::Mutex; use futures::lock::Mutex;
use glam::{Affine2, UVec2}; use glam::UVec2;
use graphene_application_io::{ApplicationIo, EditorApi}; use graphene_application_io::{ApplicationIo, EditorApi};
use vello::{AaConfig, AaSupport, RenderParams, Renderer, RendererOptions, Scene}; use vello::{AaConfig, AaSupport, RenderParams, Renderer, RendererOptions, Scene};
use wgpu::{Origin3d, TextureAspect}; use wgpu::{Origin3d, TextureAspect};
pub use context::Context as WgpuContext; pub use context::Context as WgpuContext;
pub use context::ContextBuilder as WgpuContextBuilder; 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 rendering::RenderContext;
pub use wgpu::Backends as WgpuBackends; pub use wgpu::Backends as WgpuBackends;
pub use wgpu::Features as WgpuFeatures; pub use wgpu::Features as WgpuFeatures;
const TEXTURE_CACHE_SIZE: u64 = 256 * 1024 * 1024; // 256 MiB const TEXTURE_CACHE_SIZE: u64 = 256 * 1024 * 1024; // 256 MiB
#[derive(dyn_any::DynAny)] #[derive(dyn_any::DynAny, Clone)]
pub struct WgpuExecutor { pub struct WgpuExecutor {
pub context: WgpuContext, inner: Arc<WgpuExecutorInner>,
}
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<TextureCache>, texture_cache: Mutex<TextureCache>,
vello_renderer: Mutex<Renderer>, vello_renderer: Mutex<Renderer>,
resampler: Resampler, shader_runtime: ShaderRuntime,
background_compositor: BackgroundCompositor,
pub shader_runtime: ShaderRuntime,
} }
impl std::fmt::Debug for WgpuExecutor { impl std::fmt::Debug for WgpuExecutor {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 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() { for (image_brush, texture) in context.resource_overrides.iter() {
let texture_view = wgpu::TexelCopyTextureInfoBase { let texture_view = wgpu::TexelCopyTextureInfoBase {
texture: texture.clone(), texture: texture.clone(),
@@ -75,7 +88,7 @@ impl WgpuExecutor {
}; };
renderer.override_image(&image_brush.image, Some(texture_view)); 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() { for (image_brush, _) in context.resource_overrides.iter() {
renderer.override_image(&image_brush.image, None); renderer.override_image(&image_brush.image, None);
} }
@@ -84,21 +97,12 @@ impl WgpuExecutor {
Ok(texture) Ok(texture)
} }
pub async fn resample_texture(&self, source: &wgpu::Texture, size: UVec2, transform: &glam::DAffine2) -> Arc<wgpu::Texture> { pub fn pipeline_init<P: WgpuPipeline>(&self, pipeline: &WgpuPipelineCache) {
let out = self.request_texture(size).await; pipeline.init::<P>(self);
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<wgpu::Texture> {
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 async fn request_texture(&self, size: UVec2) -> Arc<wgpu::Texture> { pub async fn request_texture(&self, size: UVec2) -> Arc<wgpu::Texture> {
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 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); let shader_runtime = ShaderRuntime::new(&context);
Some(Self { Some(Self {
context, inner: Arc::new(WgpuExecutorInner {
texture_cache: texture_cache.into(), context,
vello_renderer: vello_renderer.into(), texture_cache: texture_cache.into(),
resampler, vello_renderer: vello_renderer.into(),
background_compositor, shader_runtime,
shader_runtime, }),
}) })
} }
} }
@@ -0,0 +1,68 @@
use dyn_any::DynAny;
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<Box<dyn Future<Output = T> + 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<Output = Self::Out> + Send + 'a;
}
impl<P: AsyncPipeline> Pipeline for P {
type Args<'a> = <P as AsyncPipeline>::Args<'a>;
type Out = <P as AsyncPipeline>::Out;
fn create(executor: &WgpuExecutor) -> Self {
<P as AsyncPipeline>::create(executor)
}
fn run<'a>(&'a self, executor: &'a WgpuExecutor, args: &'a Self::Args<'_>) -> PipelineFuture<'a, Self::Out> {
Box::pin(<P as AsyncPipeline>::run(self, executor, args))
}
}
#[derive(Default, Clone, DynAny)]
pub struct PipelineCache {
pipeline: Arc<OnceLock<Box<dyn Any + Send + Sync>>>,
executor: Arc<OnceLock<WgpuExecutor>>,
}
impl PipelineCache {
pub(super) fn init<P: Pipeline>(&self, executor: &WgpuExecutor) {
self.executor.get_or_init(|| executor.clone());
self.pipeline.get_or_init(|| Box::new(P::create(executor)));
}
pub async fn run<P: Pipeline>(&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::<P>()
.unwrap_or_else(|| panic!("PipelineCache type mismatch: run::<{}>() but init used a different pipeline type", std::any::type_name::<P>(),));
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()
}
}
@@ -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(&params_buffer, 0, bytemuck::cast_slice(&params_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()]);
}
}
@@ -14,7 +14,7 @@ use wgpu::{Extent3d, TextureDescriptor, TextureDimension, TextureFormat, Texture
/// ///
/// Creates a new WGPU texture with RGBA8UnormSrgb format and uploads the provided /// Creates a new WGPU texture with RGBA8UnormSrgb format and uploads the provided
/// image data. The texture is configured for binding, copying, and source operations. /// image data. The texture is configured for binding, copying, and source operations.
fn upload_to_texture(device: &std::sync::Arc<wgpu::Device>, queue: &std::sync::Arc<wgpu::Queue>, image: &Raster<CPU>) -> wgpu::Texture { fn upload_to_texture(device: &wgpu::Device, queue: &wgpu::Queue, image: &Raster<CPU>) -> wgpu::Texture {
let rgba8_data: Vec<SRGBA8> = image.data.iter().map(|x| (*x).into()).collect(); let rgba8_data: Vec<SRGBA8> = image.data.iter().map(|x| (*x).into()).collect();
device.create_texture_with_data( device.create_texture_with_data(
@@ -52,7 +52,7 @@ struct RasterGpuToRasterCpuConverter {
padded_bytes_per_row: u32, padded_bytes_per_row: u32,
} }
impl RasterGpuToRasterCpuConverter { impl RasterGpuToRasterCpuConverter {
fn new(device: &std::sync::Arc<wgpu::Device>, encoder: &mut wgpu::CommandEncoder, data_gpu: Raster<GPU>) -> Self { fn new(device: &wgpu::Device, encoder: &mut wgpu::CommandEncoder, data_gpu: Raster<GPU>) -> Self {
let texture = data_gpu.data(); let texture = data_gpu.data();
let width = texture.width(); let width = texture.width();
let height = texture.height(); let height = texture.height();
@@ -100,7 +100,7 @@ impl RasterGpuToRasterCpuConverter {
} }
} }
async fn convert(self, device: &std::sync::Arc<wgpu::Device>) -> Result<Raster<CPU>, wgpu::BufferAsyncError> { async fn convert(self, device: &wgpu::Device) -> Result<Raster<CPU>, wgpu::BufferAsyncError> {
let buffer_slice = self.buffer.slice(..); let buffer_slice = self.buffer.slice(..);
let (sender, receiver) = futures::channel::oneshot::channel(); let (sender, receiver) = futures::channel::oneshot::channel();
buffer_slice.map_async(wgpu::MapMode::Read, move |result| { buffer_slice.map_async(wgpu::MapMode::Read, move |result| {
@@ -149,8 +149,8 @@ impl<'i> Convert<List<Raster<GPU>>, &'i WgpuExecutor> for List<Raster<GPU>> {
/// Converts a `List<Raster<CPU>>` to `List<Raster<GPU>>` by uploading each image to a texture /// Converts a `List<Raster<CPU>>` to `List<Raster<GPU>>` by uploading each image to a texture
impl<'i> Convert<List<Raster<GPU>>, &'i WgpuExecutor> for List<Raster<CPU>> { impl<'i> Convert<List<Raster<GPU>>, &'i WgpuExecutor> for List<Raster<CPU>> {
async fn convert(self, _: Footprint, executor: &'i WgpuExecutor) -> List<Raster<GPU>> { async fn convert(self, _: Footprint, executor: &'i WgpuExecutor) -> List<Raster<GPU>> {
let device = &executor.context.device; let device = &executor.context().device;
let queue = &executor.context.queue; let queue = &executor.context().queue;
let list = self let list = self
.into_iter() .into_iter()
.map(|row| { .map(|row| {
@@ -169,8 +169,8 @@ impl<'i> Convert<List<Raster<GPU>>, &'i WgpuExecutor> for List<Raster<CPU>> {
/// Converts single CPU raster to GPU by uploading to texture /// Converts single CPU raster to GPU by uploading to texture
impl<'i> Convert<Raster<GPU>, &'i WgpuExecutor> for Raster<CPU> { impl<'i> Convert<Raster<GPU>, &'i WgpuExecutor> for Raster<CPU> {
async fn convert(self, _: Footprint, executor: &'i WgpuExecutor) -> Raster<GPU> { async fn convert(self, _: Footprint, executor: &'i WgpuExecutor) -> Raster<GPU> {
let device = &executor.context.device; let device = &executor.context().device;
let queue = &executor.context.queue; let queue = &executor.context().queue;
let texture = upload_to_texture(device, queue, &self); let texture = upload_to_texture(device, queue, &self);
queue.submit([]); queue.submit([]);
@@ -188,8 +188,8 @@ impl<'i> Convert<List<Raster<CPU>>, &'i WgpuExecutor> for List<Raster<CPU>> {
/// Converts a `List<Raster<GPU>>` to `List<Raster<CPU>>` by downloading texture data in one go then asynchronously maps all buffers and processes the results. /// Converts a `List<Raster<GPU>>` to `List<Raster<CPU>>` by downloading texture data in one go then asynchronously maps all buffers and processes the results.
impl<'i> Convert<List<Raster<CPU>>, &'i WgpuExecutor> for List<Raster<GPU>> { impl<'i> Convert<List<Raster<CPU>>, &'i WgpuExecutor> for List<Raster<GPU>> {
async fn convert(self, _: Footprint, executor: &'i WgpuExecutor) -> List<Raster<CPU>> { async fn convert(self, _: Footprint, executor: &'i WgpuExecutor) -> List<Raster<CPU>> {
let device = &executor.context.device; let device = &executor.context().device;
let queue = &executor.context.queue; let queue = &executor.context().queue;
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("batch_texture_download_encoder"), label: Some("batch_texture_download_encoder"),
@@ -230,8 +230,8 @@ impl<'i> Convert<List<Raster<CPU>>, &'i WgpuExecutor> for List<Raster<GPU>> {
/// Converts single GPU raster to CPU by downloading texture data /// Converts single GPU raster to CPU by downloading texture data
impl<'i> Convert<Raster<CPU>, &'i WgpuExecutor> for Raster<GPU> { impl<'i> Convert<Raster<CPU>, &'i WgpuExecutor> for Raster<GPU> {
async fn convert(self, _: Footprint, executor: &'i WgpuExecutor) -> Raster<CPU> { async fn convert(self, _: Footprint, executor: &'i WgpuExecutor) -> Raster<CPU> {
let device = &executor.context.device; let device = &executor.context().device;
let queue = &executor.context.queue; let queue = &executor.context().queue;
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("single_texture_download_encoder"), label: Some("single_texture_download_encoder"),
@@ -233,7 +233,7 @@ impl PerPixelAdjustCodegen<'_> {
ty: ParsedFieldType::Regular(RegularParsedField { ty: ParsedFieldType::Regular(RegularParsedField {
ty: parse_quote!(&'a WgpuExecutor), ty: parse_quote!(&'a WgpuExecutor),
exposed: true, 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_min: None,
number_soft_max: None, number_soft_max: None,
number_hard_min: None, number_hard_min: None,
@@ -283,7 +283,7 @@ impl PerPixelAdjustCodegen<'_> {
let entry_point_name = &self.entry_point_name; let entry_point_name = &self.entry_point_name;
let body = quote! { 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, wgsl_shader: crate::WGSL_SHADER,
fragment_shader_name: super::#entry_point_name, fragment_shader_name: super::#entry_point_name,
has_uniform: #has_uniform, has_uniform: #has_uniform,
+1
View File
@@ -61,6 +61,7 @@ reqwest = { workspace = true }
image = { workspace = true } image = { workspace = true }
base64 = { workspace = true } base64 = { workspace = true }
wgpu = { workspace = true } wgpu = { workspace = true }
bytemuck = { workspace = true }
# Optional local dependencies # Optional local dependencies
graphene-canvas-utils = { workspace = true, optional = true } graphene-canvas-utils = { workspace = true, optional = true }
+2 -1
View File
@@ -1,8 +1,9 @@
pub mod any; pub mod any;
pub mod pixel_preview;
pub mod platform_application_io; pub mod platform_application_io;
pub mod render_background;
pub mod render_cache; pub mod render_cache;
pub mod render_node; pub mod render_node;
pub mod render_pixel_preview;
pub mod text; pub mod text;
pub use blending_nodes; pub use blending_nodes;
pub use brush_nodes as brush; pub use brush_nodes as brush;
@@ -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<Context<'static>, Output = RenderOutput> + Send + Sync,
) -> RenderOutput {
let Some(render_params) = ctx.vararg(0).ok().and_then(|v| v.downcast_ref::<RenderParams>()).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
}
@@ -137,7 +137,7 @@ fn image_to_bytes(_: impl Ctx, image: List<Raster<CPU>>) -> List<u8> {
/// Loads binary from URLs and local asset paths. Returns a transparent placeholder if the resource fails to load, allowing rendering to continue. /// Loads binary from URLs and local asset paths. Returns a transparent placeholder if the resource fails to load, allowing rendering to continue.
#[node_macro::node(category("Web Request"))] #[node_macro::node(category("Web Request"))]
async fn load_resource<'a: 'n>(_: impl Ctx, _primary: (), #[scope("editor-api")] _editor: &'a PlatformEditorApi, #[name("URL")] url: String) -> Arc<[u8]> { async fn load_resource<'a: 'n>(_: impl Ctx, _primary: (), #[name("URL")] url: String) -> Arc<[u8]> {
let placeholder = || -> Arc<[u8]> { Arc::from(Vec::<u8>::new()) }; let placeholder = || -> Arc<[u8]> { Arc::from(Vec::<u8>::new()) };
let response = match reqwest::Client::new().get(&url).send().await { let response = match reqwest::Client::new().get(&url).send().await {
@@ -261,10 +261,30 @@ where
) )
} }
#[node_macro::node(category(""), inject_scope)]
pub async fn editor_api<'a: 'n>(_: impl Ctx, #[scope("editor-api")] editor_api: &'a PlatformEditorApi) -> &'a PlatformEditorApi {
editor_api
}
#[node_macro::node(category(""))] #[node_macro::node(category(""))]
pub async fn resource<'a: 'n>(_: impl Ctx, hash: ResourceHash, #[scope("editor-api")] editor_api: &'a PlatformEditorApi) -> Resource { pub async fn resource<'a: 'n>(_: impl Ctx, hash: ResourceHash, #[scope(editor_api::IDENTIFIER)] editor_api: &'a PlatformEditorApi) -> Resource {
let application_io = editor_api.application_io.as_ref().expect("ApplicationIo must be available when using resources"); let application_io = editor_api.application_io.as_ref().expect("ApplicationIo must be available when using resources");
application_io.load_resource(hash).await.unwrap_or_else(|| { application_io.load_resource(hash).await.unwrap_or_else(|| {
panic!("Resource {hash} not found"); panic!("Resource {hash} not found");
}) })
} }
#[node_macro::node(category(""), inject_scope)]
pub async fn wgpu_executor<'a: 'n>(_: impl Ctx, #[scope(editor_api::IDENTIFIER)] editor_api: &'a PlatformEditorApi) -> &'a ::wgpu_executor::WgpuExecutor {
editor_api
.application_io
.as_ref()
.expect("ApplicationIo not not available")
.gpu_executor()
.expect("GPU executor not available")
}
#[node_macro::node(category(""), inject_scope)]
pub async fn try_wgpu_executor<'a: 'n>(_: impl Ctx, #[scope(editor_api::IDENTIFIER)] editor_api: &'a PlatformEditorApi) -> Option<&'a ::wgpu_executor::WgpuExecutor> {
editor_api.application_io.as_ref()?.gpu_executor()
}
@@ -1,7 +1,135 @@
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::util::DeviceExt;
use wgpu_executor::{AsyncWgpuPipeline, WgpuExecutor, WgpuPipelineCache};
pub struct BackgroundCompositor { #[node_macro::node(category(""))]
async fn render_background<'a: 'n>(
ctx: impl Ctx + ExtractFootprint + ExtractVarArgs,
#[scope(composite_background_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::<RenderParams>()
.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::<CompositeBackground>(&CompositeBackgroundArgs {
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 id="{checker_id}" x="{}" y="{}" width="{pattern_size}" height="{pattern_size}" patternUnits="userSpaceOnUse"><rect width="{pattern_size}" height="{pattern_size}" fill="#ffffff" /><rect x="{cell_size}" y="0" width="{cell_size}" height="{cell_size}" fill="#cccccc" /><rect x="0" y="{cell_size}" width="{cell_size}" height="{cell_size}" fill="#cccccc" /></pattern>"##,
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 composite_background_pipeline<'a: 'n>(
_ctx: impl Ctx,
#[scope(crate::platform_application_io::try_wgpu_executor::IDENTIFIER)] executor: Option<&'a WgpuExecutor>,
#[data] pipeline: WgpuPipelineCache,
) -> WgpuPipelineCache {
if let Some(executor) = executor {
executor.pipeline_init::<CompositeBackground>(pipeline);
}
pipeline.clone()
}
pub struct CompositeBackground {
checker_rect_pipeline: wgpu::RenderPipeline, checker_rect_pipeline: wgpu::RenderPipeline,
checker_viewport_pipeline: wgpu::RenderPipeline, checker_viewport_pipeline: wgpu::RenderPipeline,
fullscreen_pipeline: wgpu::RenderPipeline, fullscreen_pipeline: wgpu::RenderPipeline,
@@ -10,12 +138,23 @@ pub struct BackgroundCompositor {
sampler: wgpu::Sampler, sampler: wgpu::Sampler,
} }
impl BackgroundCompositor { pub struct CompositeBackgroundArgs<'a> {
pub fn new(device: &wgpu::Device) -> Self { foreground: &'a wgpu::Texture,
backgrounds: &'a [rendering::Background],
document_to_screen: Affine2,
zoom: f32,
}
impl AsyncWgpuPipeline for CompositeBackground {
type Args<'a> = CompositeBackgroundArgs<'a>;
type Out = Arc<wgpu::Texture>;
fn create(executor: &WgpuExecutor) -> Self {
let device = &executor.context().device;
let format = wgpu::TextureFormat::Rgba8Unorm; let format = wgpu::TextureFormat::Rgba8Unorm;
let checker_rect_shader = device.create_shader_module(wgpu::include_wgsl!("checker_rect.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!("checker_viewport.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!("fullscreen.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 { let checker_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("background_checker_bind_group_layout"), label: Some("background_checker_bind_group_layout"),
@@ -189,13 +328,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 &CompositeBackgroundArgs {
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. { if zoom <= 0. {
return; return output;
} }
let device = &context.device; let device = &executor.context().device;
let queue = &context.queue; let queue = &executor.context().queue;
let checker_size_doc = 8. / zoom; let checker_size_doc = 8. / zoom;
let screen_to_document = document_to_screen.inverse(); let screen_to_document = document_to_screen.inverse();
@@ -285,8 +434,12 @@ impl BackgroundCompositor {
} }
queue.submit(std::iter::once(encoder.finish())); queue.submit(std::iter::once(encoder.finish()));
}
output
}
}
impl CompositeBackground {
fn create_checker_bind_group(&self, device: &wgpu::Device, uniforms: CompositeUniforms) -> wgpu::BindGroup { fn create_checker_bind_group(&self, device: &wgpu::Device, uniforms: CompositeUniforms) -> wgpu::BindGroup {
let buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { let buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("background_checker_uniforms"), label: Some("background_checker_uniforms"),
+10 -12
View File
@@ -5,14 +5,13 @@ use core_types::transform::{Footprint, RenderQuality, Transform};
use core_types::{CloneVarArgs, Context, Ctx, ExtractAll, ExtractAnimationTime, ExtractPointerPosition, ExtractRealTime, OwnedContextImpl}; use core_types::{CloneVarArgs, Context, Ctx, ExtractAll, ExtractAnimationTime, ExtractPointerPosition, ExtractRealTime, OwnedContextImpl};
use glam::{DAffine2, DVec2, IVec2, UVec2}; use glam::{DAffine2, DVec2, IVec2, UVec2};
use graph_craft::application_io::PlatformEditorApi; use graph_craft::application_io::PlatformEditorApi;
use graph_craft::document::value::RenderOutput; use graph_craft::document::value::{RenderOutput, RenderOutputType};
use graphene_application_io::{ApplicationIo, ImageTexture}; use graphene_application_io::ImageTexture;
use rendering::{RenderOutputType as RenderOutputTypeRequest, RenderParams}; use rendering::{RenderOutputType as RenderOutputTypeRequest, RenderParams};
use std::collections::HashSet; use std::collections::HashSet;
use std::hash::Hash; use std::hash::Hash;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use wgpu_executor::WgpuExecutor;
use crate::render_node::RenderOutputType;
pub const TILE_SIZE: u32 = 256; pub const TILE_SIZE: u32 = 256;
pub const MAX_CACHE_MEMORY_BYTES: usize = 512 * 1024 * 1024; pub const MAX_CACHE_MEMORY_BYTES: usize = 512 * 1024 * 1024;
@@ -327,7 +326,8 @@ fn flood_fill(start: &TileCoord, tile_set: &HashSet<TileCoord>, visited: &mut Ha
#[node_macro::node(category(""))] #[node_macro::node(category(""))]
pub async fn render_output_cache<'a: 'n>( pub async fn render_output_cache<'a: 'n>(
ctx: impl Ctx + ExtractAll + CloneVarArgs + ExtractRealTime + ExtractAnimationTime + ExtractPointerPosition + Sync, ctx: impl Ctx + ExtractAll + CloneVarArgs + ExtractRealTime + ExtractAnimationTime + ExtractPointerPosition + Sync,
editor_api: &'a PlatformEditorApi, #[scope(crate::platform_application_io::try_wgpu_executor::IDENTIFIER)] executor: Option<&'a WgpuExecutor>,
#[scope(crate::platform_application_io::editor_api::IDENTIFIER)] editor_api: &'a PlatformEditorApi,
data: impl Node<Context<'static>, Output = RenderOutput> + Send + Sync, data: impl Node<Context<'static>, Output = RenderOutput> + Send + Sync,
#[data] tile_cache: TileCache, #[data] tile_cache: TileCache,
) -> RenderOutput { ) -> RenderOutput {
@@ -404,11 +404,9 @@ pub async fn render_output_cache<'a: 'n>(
return data.eval(context.into_context()).await; return data.eval(context.into_context()).await;
} }
let exec = editor_api.application_io.as_ref().unwrap().gpu_executor().unwrap(); let executor = executor.expect("GPU executor not available");
let output_texture = executor.request_texture(physical_resolution).await;
let output_texture = exec.request_texture(physical_resolution).await; let combined_metadata = composite_cached_regions(&all_regions, &output_texture, &device_origin_offset, &footprint.transform, &executor);
let combined_metadata = composite_cached_regions(&all_regions, &output_texture, &device_origin_offset, &footprint.transform, exec);
RenderOutput { RenderOutput {
data: RenderOutputType::Texture(output_texture.into()), data: RenderOutputType::Texture(output_texture.into()),
@@ -473,8 +471,8 @@ fn composite_cached_regions(
viewport_transform: &DAffine2, viewport_transform: &DAffine2,
exec: &wgpu_executor::WgpuExecutor, exec: &wgpu_executor::WgpuExecutor,
) -> rendering::RenderMetadata { ) -> rendering::RenderMetadata {
let device = &exec.context.device; let device = &exec.context().device;
let queue = &exec.context.queue; let queue = &exec.context().queue;
let output_resolution = UVec2::new(output_texture.width(), output_texture.height()); let output_resolution = UVec2::new(output_texture.width(), output_texture.height());
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("composite") }); let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("composite") });
+10 -116
View File
@@ -1,22 +1,15 @@
use core_types::list::List; use core_types::list::List;
use core_types::transform::{Footprint, Transform}; use core_types::transform::{Footprint, Transform};
use core_types::uuid::generate_uuid;
use core_types::{CloneVarArgs, ExtractAll, ExtractVarArgs}; use core_types::{CloneVarArgs, ExtractAll, ExtractVarArgs};
use core_types::{Color, Context, Ctx, ExtractFootprint, OwnedContextImpl, WasmNotSend}; use core_types::{Color, Context, Ctx, ExtractFootprint, OwnedContextImpl, WasmNotSend};
use graph_craft::application_io::PlatformEditorApi; use graph_craft::document::value::{RenderOutput, RenderOutputType};
use graph_craft::document::value::RenderOutput; use graphene_application_io::{ExportFormat, RenderConfig};
pub use graph_craft::document::value::RenderOutputType;
use graphene_application_io::{ApplicationIo, ExportFormat, RenderConfig};
use graphic_types::raster_types::{CPU, Raster}; use graphic_types::raster_types::{CPU, Raster};
use graphic_types::{Artboard, Graphic, Vector}; use graphic_types::{Artboard, Graphic, Vector};
use rendering::{Render, RenderMetadata, RenderOutputType as RenderOutputTypeRequest, RenderParams, SvgRender, SvgRenderOutput}; use rendering::{Render, RenderMetadata, RenderOutputType as RenderOutputTypeRequest, RenderParams, SvgRender, SvgRenderOutput};
use std::fmt::Write;
use std::sync::Arc; use std::sync::Arc;
use vector_types::GradientStops; use vector_types::GradientStops;
use wgpu_executor::RenderContext; use wgpu_executor::{RenderContext, WgpuExecutor};
// Re-export render_output_cache from render_cache module
pub use crate::render_cache::render_output_cache;
#[derive(Clone, dyn_any::DynAny)] #[derive(Clone, dyn_any::DynAny)]
pub enum RenderIntermediateType { pub enum RenderIntermediateType {
@@ -81,7 +74,11 @@ async fn render_intermediate<'a: 'n, T: 'static + Render + WasmNotSend + Send +
} }
#[node_macro::node(category(""))] #[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::try_wgpu_executor::IDENTIFIER)] executor: Option<&'a WgpuExecutor>,
data: RenderIntermediate,
) -> RenderOutput {
let footprint = ctx.footprint(); let footprint = ctx.footprint();
let render_params = ctx let render_params = ctx
.vararg(0) .vararg(0)
@@ -110,9 +107,6 @@ async fn render<'a: 'n>(ctx: impl Ctx + ExtractFootprint + ExtractVarArgs, edito
} }
} }
(RenderOutputTypeRequest::Vello, RenderIntermediateType::Vello(data)) => { (RenderOutputTypeRequest::Vello, RenderIntermediateType::Vello(data)) => {
let Some(exec) = editor_api.application_io.as_ref().unwrap().gpu_executor() else {
unreachable!("Attempted to render with Vello when no GPU executor is available");
};
let (scene, context) = data.as_ref(); let (scene, context) = data.as_ref();
let scale = render_params.scale; let scale = render_params.scale;
let physical_resolution = render_params.footprint.resolution; let physical_resolution = render_params.footprint.resolution;
@@ -139,7 +133,8 @@ async fn render<'a: 'n>(ctx: impl Ctx + ExtractFootprint + ExtractVarArgs, edito
} }
} }
let texture = exec let texture = executor
.expect("GPU executor not available")
.render_vello_scene(&transformed_scene, physical_resolution, context, None) .render_vello_scene(&transformed_scene, physical_resolution, context, None)
.await .await
.expect("Failed to render Vello scene"); .expect("Failed to render Vello scene");
@@ -151,107 +146,6 @@ async fn render<'a: 'n>(ctx: impl Ctx + ExtractFootprint + ExtractVarArgs, edito
RenderOutput { data, metadata } 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::<RenderParams>()
.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 id="{checker_id}" x="{}" y="{}" width="{pattern_size}" height="{pattern_size}" patternUnits="userSpaceOnUse"><rect width="{pattern_size}" height="{pattern_size}" fill="#ffffff" /><rect x="{cell_size}" y="0" width="{cell_size}" height="{cell_size}" fill="#cccccc" /><rect x="0" y="{cell_size}" width="{cell_size}" height="{cell_size}" fill="#cccccc" /></pattern>"##,
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(""))] #[node_macro::node(category(""))]
async fn create_context<'a: 'n>( async fn create_context<'a: 'n>(
// Context injections are defined in the wrap_network_in_scope function // Context injections are defined in the wrap_network_in_scope function
@@ -0,0 +1,232 @@
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<Context<'static>, Output = RenderOutput> + Send + Sync,
) -> RenderOutput {
let Some(render_params) = ctx.vararg(0).ok().and_then(|v| v.downcast_ref::<RenderParams>()).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::<PixelPreview>(&PixelPreviewArgs {
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::try_wgpu_executor::IDENTIFIER)] executor: Option<&'a WgpuExecutor>,
#[data] pipeline: WgpuPipelineCache,
) -> WgpuPipelineCache {
if let Some(executor) = executor {
executor.pipeline_init::<PixelPreview>(pipeline);
}
pipeline.clone()
}
pub struct PixelPreview {
pipeline: wgpu::RenderPipeline,
bind_group_layout: wgpu::BindGroupLayout,
}
pub struct PixelPreviewArgs<'a> {
source: &'a wgpu::Texture,
transform: &'a DAffine2,
size: UVec2,
}
impl AsyncWgpuPipeline for PixelPreview {
type Args<'a> = PixelPreviewArgs<'a>;
type Out = Arc<wgpu::Texture>;
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 &PixelPreviewArgs { 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(&params_buffer, 0, bytemuck::cast_slice(&params_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
}
}