From a9ba88603b352b8b5a3c876bdafcfea30b3949f4 Mon Sep 17 00:00:00 2001 From: firestar99 Date: Tue, 22 Jul 2025 10:58:24 +0200 Subject: [PATCH 1/4] vello: code cleanup of resource overwrites --- node-graph/gsvg-renderer/src/renderer.rs | 39 +++++++++++++----------- node-graph/wgpu-executor/src/lib.rs | 10 ++---- 2 files changed, 25 insertions(+), 24 deletions(-) diff --git a/node-graph/gsvg-renderer/src/renderer.rs b/node-graph/gsvg-renderer/src/renderer.rs index dd3a1ebb08..55971e4956 100644 --- a/node-graph/gsvg-renderer/src/renderer.rs +++ b/node-graph/gsvg-renderer/src/renderer.rs @@ -16,10 +16,13 @@ use graphene_core::uuid::{NodeId, generate_uuid}; use graphene_core::vector::VectorDataTable; use graphene_core::vector::click_target::{ClickTarget, FreePoint}; use graphene_core::vector::style::{Fill, Stroke, StrokeAlign, ViewMode}; -use graphene_core::{AlphaBlending, Artboard, ArtboardGroupTable, GraphicElement, GraphicGroupTable}; +use graphene_core::{Artboard, ArtboardGroupTable, GraphicElement, GraphicGroupTable}; use num_traits::Zero; use std::collections::{HashMap, HashSet}; use std::fmt::Write; +use std::ops::Deref; +use std::sync::{Arc, LazyLock}; +use vello::peniko::Blob; #[cfg(feature = "vello")] use vello::*; @@ -145,7 +148,7 @@ impl Default for SvgRender { #[derive(Clone, Debug, Default)] pub struct RenderContext { #[cfg(feature = "vello")] - pub resource_overrides: HashMap, + pub resource_overrides: Vec<(peniko::Image, wgpu::Texture)>, } /// Static state used whilst rendering @@ -1014,6 +1017,8 @@ impl GraphicElementRendered for RasterDataTable { } } +const LAZY_ARC_VEC_ZERO_U8: LazyLock>> = LazyLock::new(|| Arc::new(Vec::new())); + impl GraphicElementRendered for RasterDataTable { fn render_svg(&self, _render: &mut SvgRender, _render_params: &RenderParams) { log::warn!("tried to render texture as an svg"); @@ -1023,30 +1028,30 @@ impl GraphicElementRendered for RasterDataTable { fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext, _render_params: &RenderParams) { use vello::peniko; - let mut render_stuff = |image: peniko::Image, instance_transform: DAffine2, blend_mode: AlphaBlending| { - let image_transform = transform * instance_transform * DAffine2::from_scale(1. / DVec2::new(image.width as f64, image.height as f64)); + for instance in self.instance_ref_iter() { + let blend_mode = *instance.alpha_blending; let layer = blend_mode != Default::default(); - - let Some(bounds) = self.bounding_box(transform, true) else { return }; - let blending = peniko::BlendMode::new(blend_mode.blend_mode.to_peniko(), peniko::Compose::SrcOver); - if layer { + let Some(bounds) = self.bounding_box(transform, true) else { return }; + let blending = peniko::BlendMode::new(blend_mode.blend_mode.to_peniko(), peniko::Compose::SrcOver); let rect = kurbo::Rect::new(bounds[0].x, bounds[0].y, bounds[1].x, bounds[1].y); scene.push_layer(blending, blend_mode.opacity, kurbo::Affine::IDENTITY, &rect); } + + let image = peniko::Image::new( + Blob::new(LAZY_ARC_VEC_ZERO_U8.deref().clone()), + peniko::ImageFormat::Rgba8, + instance.instance.data().width(), + instance.instance.data().height(), + ) + .with_extend(peniko::Extend::Repeat); + let image_transform = transform * *instance.transform * DAffine2::from_scale(1. / DVec2::new(image.width as f64, image.height as f64)); scene.draw_image(&image, kurbo::Affine::new(image_transform.to_cols_array())); + context.resource_overrides.push((image, instance.instance.data().clone())); + if layer { scene.pop_layer() } - }; - - for instance in self.instance_ref_iter() { - let image = peniko::Image::new(vec![].into(), peniko::ImageFormat::Rgba8, instance.instance.data().width(), instance.instance.data().height()).with_extend(peniko::Extend::Repeat); - - let id = image.data.id(); - context.resource_overrides.insert(id, instance.instance.data().clone()); - - render_stuff(image, *instance.transform, *instance.alpha_blending); } } diff --git a/node-graph/wgpu-executor/src/lib.rs b/node-graph/wgpu-executor/src/lib.rs index c0113a45c6..e844467848 100644 --- a/node-graph/wgpu-executor/src/lib.rs +++ b/node-graph/wgpu-executor/src/lib.rs @@ -111,18 +111,14 @@ impl WgpuExecutor { { let mut renderer = self.vello_renderer.lock().await; - for (id, texture) in context.resource_overrides.iter() { - let texture = texture.clone(); + for (image, texture) in context.resource_overrides.iter() { let texture_view = wgpu::TexelCopyTextureInfoBase { - texture, + texture: texture.clone(), mip_level: 0, origin: Origin3d::ZERO, aspect: TextureAspect::All, }; - renderer.override_image( - &vello::peniko::Image::new(vello::peniko::Blob::from_raw_parts(Arc::new(vec![]), *id), vello::peniko::ImageFormat::Rgba8, 0, 0), - Some(texture_view), - ); + renderer.override_image(image, Some(texture_view)); } renderer.render_to_texture(&self.context.device, &self.context.queue, scene, &target_texture.view, &render_params)?; } From 28e1f947e55509181658c774080a08ec08671212 Mon Sep 17 00:00:00 2001 From: firestar99 Date: Mon, 21 Jul 2025 17:25:15 +0200 Subject: [PATCH 2/4] upload_texture: upload cpu textures as SRGBA8 --- Cargo.lock | 1 + .../node_graph/document_node_definitions.rs | 80 +++++++++++++++++++ node-graph/wgpu-executor/Cargo.toml | 1 + node-graph/wgpu-executor/src/lib.rs | 1 + .../wgpu-executor/src/texture_upload.rs | 51 ++++++++++++ 5 files changed, 134 insertions(+) create mode 100644 node-graph/wgpu-executor/src/texture_upload.rs diff --git a/Cargo.lock b/Cargo.lock index 2d33ed756e..d63104125a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5830,6 +5830,7 @@ name = "wgpu-executor" version = "0.1.0" dependencies = [ "anyhow", + "bytemuck", "dyn-any", "futures", "glam", diff --git a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs index 06bec125ea..240a89f739 100644 --- a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs +++ b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs @@ -1083,6 +1083,86 @@ fn static_nodes() -> Vec { description: Cow::Borrowed("TODO"), properties: None, }, + #[cfg(feature = "gpu")] + DocumentNodeDefinition { + identifier: "Upload Texture", + category: "Debug: GPU", + node_template: NodeTemplate { + document_node: DocumentNode { + implementation: DocumentNodeImplementation::Network(NodeNetwork { + exports: vec![NodeInput::node(NodeId(2), 0)], + nodes: [ + DocumentNode { + inputs: vec![NodeInput::scope("editor-api")], + implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::ops::IntoNode<&WgpuExecutor>")), + ..Default::default() + }, + DocumentNode { + inputs: vec![NodeInput::network(concrete!(RasterDataTable), 0), NodeInput::node(NodeId(0), 0)], + manual_composition: Some(generic!(T)), + implementation: DocumentNodeImplementation::ProtoNode(wgpu_executor::texture_upload::upload_texture::IDENTIFIER), + ..Default::default() + }, + DocumentNode { + manual_composition: Some(generic!(T)), + inputs: vec![NodeInput::node(NodeId(1), 0)], + implementation: DocumentNodeImplementation::ProtoNode(memo::impure_memo::IDENTIFIER), + ..Default::default() + }, + ] + .into_iter() + .enumerate() + .map(|(id, node)| (NodeId(id as u64), node)) + .collect(), + ..Default::default() + }), + inputs: vec![NodeInput::value(TaggedValue::RasterData(RasterDataTable::default()), true)], + ..Default::default() + }, + persistent_node_metadata: DocumentNodePersistentMetadata { + output_names: vec!["Texture".to_string()], + network_metadata: Some(NodeNetworkMetadata { + persistent_metadata: NodeNetworkPersistentMetadata { + node_metadata: [ + DocumentNodeMetadata { + persistent_metadata: DocumentNodePersistentMetadata { + display_name: "Extract Executor".to_string(), + node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(0, 0)), + ..Default::default() + }, + ..Default::default() + }, + DocumentNodeMetadata { + persistent_metadata: DocumentNodePersistentMetadata { + display_name: "Upload Texture".to_string(), + node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(7, 0)), + ..Default::default() + }, + ..Default::default() + }, + DocumentNodeMetadata { + persistent_metadata: DocumentNodePersistentMetadata { + display_name: "Cache".to_string(), + node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(14, 0)), + ..Default::default() + }, + ..Default::default() + }, + ] + .into_iter() + .enumerate() + .map(|(id, node)| (NodeId(id as u64), node)) + .collect(), + ..Default::default() + }, + ..Default::default() + }), + ..Default::default() + }, + }, + description: Cow::Borrowed("TODO"), + properties: None, + }, DocumentNodeDefinition { identifier: "Extract", category: "Debug", diff --git a/node-graph/wgpu-executor/Cargo.toml b/node-graph/wgpu-executor/Cargo.toml index e939abd8cf..bed1adbd6d 100644 --- a/node-graph/wgpu-executor/Cargo.toml +++ b/node-graph/wgpu-executor/Cargo.toml @@ -25,3 +25,4 @@ futures = { workspace = true } web-sys = { workspace = true } winit = { workspace = true } vello = { workspace = true } +bytemuck = { workspace = true } diff --git a/node-graph/wgpu-executor/src/lib.rs b/node-graph/wgpu-executor/src/lib.rs index e844467848..d15db24402 100644 --- a/node-graph/wgpu-executor/src/lib.rs +++ b/node-graph/wgpu-executor/src/lib.rs @@ -1,4 +1,5 @@ mod context; +pub mod texture_upload; use anyhow::Result; pub use context::Context; diff --git a/node-graph/wgpu-executor/src/texture_upload.rs b/node-graph/wgpu-executor/src/texture_upload.rs new file mode 100644 index 0000000000..b7c45015d4 --- /dev/null +++ b/node-graph/wgpu-executor/src/texture_upload.rs @@ -0,0 +1,51 @@ +use crate::WgpuExecutor; +use graphene_core::color::SRGBA8; +use graphene_core::instances::Instance; +use graphene_core::raster_types::{CPU, GPU, Raster, RasterDataTable}; +use graphene_core::{Ctx, ExtractFootprint}; +use wgpu::util::{DeviceExt, TextureDataOrder}; +use wgpu::{Extent3d, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages}; + +#[node_macro::node(category(""))] +pub async fn upload_texture<'a: 'n>(_: impl ExtractFootprint + Ctx, input: RasterDataTable, executor: &'a WgpuExecutor) -> RasterDataTable { + let device = &executor.context.device; + let queue = &executor.context.queue; + let instances = input + .instance_ref_iter() + .map(|instance| { + let image = instance.instance; + let rgba8_data: Vec = image.data.iter().map(|x| (*x).into()).collect(); + + let texture = device.create_texture_with_data( + queue, + &TextureDescriptor { + label: Some("upload_texture node texture"), + size: Extent3d { + width: image.width, + height: image.height, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: TextureFormat::Rgba8UnormSrgb, + // I don't know what usages are actually necessary + usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST | TextureUsages::COPY_SRC, + view_formats: &[], + }, + TextureDataOrder::LayerMajor, + bytemuck::cast_slice(rgba8_data.as_slice()), + ); + + Instance { + instance: Raster::new_gpu(texture.into()), + transform: *instance.transform, + alpha_blending: *instance.alpha_blending, + source_node_id: *instance.source_node_id, + } + }) + .collect(); + + queue.submit([]); + instances +} From eefc9babb5e9f091a5e0afe5d1fceb854b1029db Mon Sep 17 00:00:00 2001 From: firestar99 Date: Tue, 22 Jul 2025 14:33:09 +0200 Subject: [PATCH 3/4] vello: fix wgpu::Texture leak within vello's `context.resource_overrides` HashMap --- node-graph/wgpu-executor/src/lib.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/node-graph/wgpu-executor/src/lib.rs b/node-graph/wgpu-executor/src/lib.rs index d15db24402..a5d416adbf 100644 --- a/node-graph/wgpu-executor/src/lib.rs +++ b/node-graph/wgpu-executor/src/lib.rs @@ -122,6 +122,9 @@ impl WgpuExecutor { renderer.override_image(image, Some(texture_view)); } renderer.render_to_texture(&self.context.device, &self.context.queue, scene, &target_texture.view, &render_params)?; + for (image, _) in context.resource_overrides.iter() { + renderer.override_image(image, None); + } } let surface_texture = surface_inner.get_current_texture()?; From ef546295f89bc7ebafa5674973fc34c06f1d7fbe Mon Sep 17 00:00:00 2001 From: firestar99 Date: Wed, 23 Jul 2025 10:34:05 +0200 Subject: [PATCH 4/4] fix missing feature gate --- node-graph/gsvg-renderer/src/renderer.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/node-graph/gsvg-renderer/src/renderer.rs b/node-graph/gsvg-renderer/src/renderer.rs index 55971e4956..372da2a9f3 100644 --- a/node-graph/gsvg-renderer/src/renderer.rs +++ b/node-graph/gsvg-renderer/src/renderer.rs @@ -22,7 +22,6 @@ use std::collections::{HashMap, HashSet}; use std::fmt::Write; use std::ops::Deref; use std::sync::{Arc, LazyLock}; -use vello::peniko::Blob; #[cfg(feature = "vello")] use vello::*; @@ -1039,7 +1038,7 @@ impl GraphicElementRendered for RasterDataTable { } let image = peniko::Image::new( - Blob::new(LAZY_ARC_VEC_ZERO_U8.deref().clone()), + peniko::Blob::new(LAZY_ARC_VEC_ZERO_U8.deref().clone()), peniko::ImageFormat::Rgba8, instance.instance.data().width(), instance.instance.data().height(),