From 6156cf591a8d93183221e9819fe7f365d4595d40 Mon Sep 17 00:00:00 2001 From: Timon Date: Mon, 27 Jul 2026 10:39:04 +0000 Subject: [PATCH 1/7] Destroy GPU textures when the last reference drops --- desktop/src/render/state.rs | 16 +++--- desktop/wrapper/src/lib.rs | 5 +- .../raster-types/src/raster_types.rs | 35 ++++++------ node-graph/libraries/wgpu-executor/src/lib.rs | 2 +- .../wgpu-executor/src/texture_cache.rs | 56 +++++++++---------- 5 files changed, 57 insertions(+), 57 deletions(-) diff --git a/desktop/src/render/state.rs b/desktop/src/render/state.rs index 2106446204..3958b8cd3c 100644 --- a/desktop/src/render/state.rs +++ b/desktop/src/render/state.rs @@ -1,7 +1,7 @@ use wgpu::PresentMode; use crate::window::Window; -use crate::wrapper::{WgpuContext, WgpuCurrentSurfaceTexture, WgpuExecutor, WgpuSurface}; +use crate::wrapper::{Texture, WgpuContext, WgpuCurrentSurfaceTexture, WgpuExecutor, WgpuSurface}; #[derive(derivative::Derivative)] #[derivative(Debug)] @@ -10,14 +10,14 @@ pub(crate) struct RenderState { executor: WgpuExecutor, config: wgpu::SurfaceConfiguration, render_pipeline: wgpu::RenderPipeline, - transparent_texture: std::sync::Arc, + transparent_texture: Texture, sampler: wgpu::Sampler, desired_width: u32, desired_height: u32, viewport_scale: [f32; 2], viewport_offset: [f32; 2], - viewport_texture: Option>, - overlays_texture: Option>, + viewport_texture: Option, + overlays_texture: Option, ui_texture: Option, bind_group: Option, #[derivative(Debug = "ignore")] @@ -46,8 +46,8 @@ impl RenderState { surface.configure(&context.device, &config); - let transparent_texture = std::sync::Arc::new(context.device.create_texture(&wgpu::TextureDescriptor { - label: Some("Transparent Texture"), + let transparent_texture = Texture::from(context.device.create_texture(&wgpu::TextureDescriptor { + label: Some("transparent_fallback"), size: wgpu::Extent3d { width: 1, height: 1, @@ -193,7 +193,7 @@ impl RenderState { self.surface_outdated = true; } - pub(crate) fn bind_viewport_texture(&mut self, viewport_texture: std::sync::Arc) { + pub(crate) fn bind_viewport_texture(&mut self, viewport_texture: Texture) { self.viewport_texture = Some(viewport_texture); self.update_bindgroup(); } @@ -231,7 +231,7 @@ impl RenderState { let result = futures::executor::block_on(self.executor.render_vello_scene(&scene, size, &Default::default(), None)); match result { Ok(texture) => { - self.overlays_texture = Some(texture.into()); + self.overlays_texture = Some(texture); } Err(e) => { self.overlays_texture = None; diff --git a/desktop/wrapper/src/lib.rs b/desktop/wrapper/src/lib.rs index 475ed93dc7..c8c6a0f7a4 100644 --- a/desktop/wrapper/src/lib.rs +++ b/desktop/wrapper/src/lib.rs @@ -8,6 +8,7 @@ use std::sync::Arc; pub use graph_craft::application_io::resource::MmapResourceStorage; pub use graphite_editor::consts::{DOUBLE_CLICK_MILLISECONDS, FILE_EXTENSION}; +pub use wgpu_executor::Texture; pub use wgpu_executor::WgpuBackends; pub use wgpu_executor::WgpuContext; pub use wgpu_executor::WgpuContextBuilder; @@ -53,14 +54,14 @@ impl DesktopWrapper { pub async fn execute_node_graph() -> NodeGraphExecutionResult { let result = graphite_editor::node_graph_executor::run_node_graph().await; match result { - (true, texture) => NodeGraphExecutionResult::HasRun(texture.map(Into::into)), + (true, texture) => NodeGraphExecutionResult::HasRun(texture), (false, _) => NodeGraphExecutionResult::NotRun, } } } pub enum NodeGraphExecutionResult { - HasRun(Option>), + HasRun(Option), NotRun, } diff --git a/node-graph/libraries/raster-types/src/raster_types.rs b/node-graph/libraries/raster-types/src/raster_types.rs index 0f255d38bb..430b753668 100644 --- a/node-graph/libraries/raster-types/src/raster_types.rs +++ b/node-graph/libraries/raster-types/src/raster_types.rs @@ -149,37 +149,40 @@ mod gpu { use std::sync::Arc; #[derive(Clone, Debug, PartialEq, Eq, Hash, DynAny)] - pub struct Texture(Arc); + pub struct Texture(Arc); + + #[derive(Debug, PartialEq, Eq, Hash)] + struct TextureInner(wgpu::Texture); + + impl Drop for TextureInner { + fn drop(&mut self) { + self.0.destroy(); + } + } + + impl Texture { + pub fn is_shared(&self) -> bool { + Arc::strong_count(&self.0) > 1 + } + } impl Deref for Texture { type Target = wgpu::Texture; fn deref(&self) -> &Self::Target { - &self.0 + &self.0.0 } } impl AsRef for Texture { fn as_ref(&self) -> &wgpu::Texture { - &self.0 - } - } - - impl From> for Texture { - fn from(texture: Arc) -> Self { - Self(texture) + &self.0.0 } } impl From for Texture { fn from(texture: wgpu::Texture) -> Self { - Self(Arc::new(texture)) - } - } - - impl From for Arc { - fn from(texture: Texture) -> Self { - texture.0 + Self(Arc::new(TextureInner(texture))) } } diff --git a/node-graph/libraries/wgpu-executor/src/lib.rs b/node-graph/libraries/wgpu-executor/src/lib.rs index 4c724ec684..595c3105f7 100644 --- a/node-graph/libraries/wgpu-executor/src/lib.rs +++ b/node-graph/libraries/wgpu-executor/src/lib.rs @@ -12,7 +12,6 @@ use core_types::color::SRGBA8; use futures::lock::Mutex; use glam::UVec2; use graphene_application_io::{ApplicationIo, EditorApi}; -use raster_types::Texture; use std::sync::Arc; use vello::{AaConfig, AaSupport, RenderParams, Renderer, RendererOptions, Scene}; use wgpu::{Origin3d, TextureAspect}; @@ -22,6 +21,7 @@ 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 raster_types::Texture; pub use rendering::RenderContext; pub use wgpu::Backends as WgpuBackends; pub use wgpu::Features as WgpuFeatures; diff --git a/node-graph/libraries/wgpu-executor/src/texture_cache.rs b/node-graph/libraries/wgpu-executor/src/texture_cache.rs index aba3784f78..c076d78769 100644 --- a/node-graph/libraries/wgpu-executor/src/texture_cache.rs +++ b/node-graph/libraries/wgpu-executor/src/texture_cache.rs @@ -1,11 +1,10 @@ use glam::UVec2; use raster_types::Texture; use std::collections::VecDeque; -use std::sync::Arc; pub(crate) struct TextureCache { /// Always sorted oldest-first by insertion/last-use order. - textures: VecDeque>, + textures: VecDeque, max_free_bytes: u64, } @@ -20,46 +19,44 @@ impl TextureCache { pub fn request_texture(&mut self, device: &wgpu::Device, size: UVec2) -> Texture { let size = size.max(UVec2::ONE); - if let Some(pos) = self - .textures - .iter() - .position(|texture| UVec2::new(texture.width(), texture.height()) == size && Arc::strong_count(texture) == 1) - { + if let Some(pos) = self.textures.iter().position(|texture| UVec2::new(texture.width(), texture.height()) == size && !texture.is_shared()) { let entry = self.textures.remove(pos).unwrap(); let texture = entry.clone(); self.textures.push_back(entry); - return texture.into(); + return texture; } let incoming_bytes = size.x as u64 * size.y as u64 * 4; self.evict_until_fits(incoming_bytes); - let texture = Arc::new(device.create_texture(&wgpu::TextureDescriptor { - label: Some(&format!("cached_texture_{}x{}", size.x, size.y)), - size: wgpu::Extent3d { - width: size.x, - height: size.y, - depth_or_array_layers: 1, - }, - mip_level_count: 1, - sample_count: 1, - dimension: wgpu::TextureDimension::D2, - format: wgpu::TextureFormat::Rgba8Unorm, - usage: wgpu::TextureUsages::COPY_SRC | wgpu::TextureUsages::COPY_DST | wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::STORAGE_BINDING | wgpu::TextureUsages::RENDER_ATTACHMENT, - view_formats: &[], - })); + let texture: Texture = device + .create_texture(&wgpu::TextureDescriptor { + label: Some(&format!("cached_{}x{}", size.x, size.y)), + size: wgpu::Extent3d { + width: size.x, + height: size.y, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Rgba8Unorm, + usage: wgpu::TextureUsages::COPY_SRC + | wgpu::TextureUsages::COPY_DST + | wgpu::TextureUsages::TEXTURE_BINDING + | wgpu::TextureUsages::STORAGE_BINDING + | wgpu::TextureUsages::RENDER_ATTACHMENT, + view_formats: &[], + }) + .into(); self.textures.push_back(texture.clone()); - texture.into() + texture } fn total_free_bytes(&self) -> u64 { - self.textures - .iter() - .filter(|texture| Arc::strong_count(texture) == 1) - .map(|texture| texture.memory_size_estimate()) - .sum() + self.textures.iter().filter(|texture| !texture.is_shared()).map(|texture| texture.memory_size_estimate()).sum() } fn evict_until_fits(&mut self, incoming_bytes: u64) { @@ -74,9 +71,8 @@ impl TextureCache { if free_bytes + incoming_bytes <= max_free_bytes { return true; } - if Arc::strong_count(texture) == 1 { + if !texture.is_shared() { free_bytes -= texture.memory_size_estimate(); - texture.destroy(); false } else { true From 8298a23c4639c15d773bcda1f299919f484eef3c Mon Sep 17 00:00:00 2001 From: Timon Date: Mon, 27 Jul 2026 10:50:15 +0000 Subject: [PATCH 2/7] Request pooled textures synchronously --- node-graph/libraries/wgpu-executor/src/lib.rs | 8 ++++---- node-graph/nodes/gstd/src/render_background.rs | 2 +- node-graph/nodes/gstd/src/render_cache.rs | 2 +- node-graph/nodes/gstd/src/render_pixel_preview.rs | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/node-graph/libraries/wgpu-executor/src/lib.rs b/node-graph/libraries/wgpu-executor/src/lib.rs index 595c3105f7..71dc388de9 100644 --- a/node-graph/libraries/wgpu-executor/src/lib.rs +++ b/node-graph/libraries/wgpu-executor/src/lib.rs @@ -50,7 +50,7 @@ impl WgpuExecutor { #[derive(dyn_any::DynAny)] pub struct WgpuExecutorInner { context: WgpuContext, - texture_cache: Mutex, + texture_cache: std::sync::Mutex, vello_renderer: Mutex, shader_runtime: ShaderRuntime, } @@ -69,7 +69,7 @@ impl<'a, T: ApplicationIo> From<&'a EditorApi> for & impl WgpuExecutor { pub async fn render_vello_scene(&self, scene: &Scene, size: UVec2, context: &RenderContext, background: Option) -> Result { - let texture = self.request_texture(size).await; + let texture = self.request_texture(size); let texture_view = texture.create_view(&wgpu::TextureViewDescriptor::default()); @@ -109,8 +109,8 @@ impl WgpuExecutor { pipeline.init::

(self); } - pub async fn request_texture(&self, size: UVec2) -> Texture { - self.inner.texture_cache.lock().await.request_texture(&self.context().device, size) + pub fn request_texture(&self, size: UVec2) -> Texture { + self.inner.texture_cache.lock().unwrap().request_texture(&self.context().device, size) } } diff --git a/node-graph/nodes/gstd/src/render_background.rs b/node-graph/nodes/gstd/src/render_background.rs index 8b239a47f1..7db3e75785 100644 --- a/node-graph/nodes/gstd/src/render_background.rs +++ b/node-graph/nodes/gstd/src/render_background.rs @@ -340,7 +340,7 @@ impl AsyncWgpuPipeline for CompositeBackground { } = args; let foreground_size = foreground.size(); - let output = executor.request_texture(UVec2::new(foreground_size.width, foreground_size.height)).await; + let output = executor.request_texture(UVec2::new(foreground_size.width, foreground_size.height)); if zoom <= 0. { return output; diff --git a/node-graph/nodes/gstd/src/render_cache.rs b/node-graph/nodes/gstd/src/render_cache.rs index 43e84aab2b..534dc333db 100644 --- a/node-graph/nodes/gstd/src/render_cache.rs +++ b/node-graph/nodes/gstd/src/render_cache.rs @@ -390,7 +390,7 @@ pub async fn render_output_cache<'a: 'n>( } let executor = executor.expect("GPU executor not available"); - let output_texture = executor.request_texture(physical_resolution).await; + let output_texture = executor.request_texture(physical_resolution); let combined_metadata = composite_cached_regions(&all_regions, &output_texture, &device_origin_offset, &footprint.transform, executor); diff --git a/node-graph/nodes/gstd/src/render_pixel_preview.rs b/node-graph/nodes/gstd/src/render_pixel_preview.rs index f668ed8a37..a40ee3e3bb 100644 --- a/node-graph/nodes/gstd/src/render_pixel_preview.rs +++ b/node-graph/nodes/gstd/src/render_pixel_preview.rs @@ -173,7 +173,7 @@ impl AsyncWgpuPipeline for PixelPreview { let context = &executor.context(); let &PixelPreviewArgs { source, transform, size } = args; - let output = executor.request_texture(size).await; + let output = executor.request_texture(size); let source_view = source.create_view(&wgpu::TextureViewDescriptor::default()); let output_view = output.create_view(&wgpu::TextureViewDescriptor::default()); From 3db158cea5b87bbb02c0777f8903812866aeab11 Mon Sep 17 00:00:00 2001 From: Timon Date: Mon, 27 Jul 2026 10:55:35 +0000 Subject: [PATCH 3/7] Pool cached textures by format --- node-graph/libraries/wgpu-executor/src/lib.rs | 6 ++++- .../wgpu-executor/src/texture_cache.rs | 26 ++++++++++++------- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/node-graph/libraries/wgpu-executor/src/lib.rs b/node-graph/libraries/wgpu-executor/src/lib.rs index 71dc388de9..01ba41d5c0 100644 --- a/node-graph/libraries/wgpu-executor/src/lib.rs +++ b/node-graph/libraries/wgpu-executor/src/lib.rs @@ -110,7 +110,11 @@ impl WgpuExecutor { } pub fn request_texture(&self, size: UVec2) -> Texture { - self.inner.texture_cache.lock().unwrap().request_texture(&self.context().device, size) + self.request_texture_with_format(size, wgpu::TextureFormat::Rgba8Unorm) + } + + pub fn request_texture_with_format(&self, size: UVec2, format: wgpu::TextureFormat) -> Texture { + self.inner.texture_cache.lock().unwrap().request_texture(&self.context().device, size, format) } } diff --git a/node-graph/libraries/wgpu-executor/src/texture_cache.rs b/node-graph/libraries/wgpu-executor/src/texture_cache.rs index c076d78769..6ad0817d28 100644 --- a/node-graph/libraries/wgpu-executor/src/texture_cache.rs +++ b/node-graph/libraries/wgpu-executor/src/texture_cache.rs @@ -16,17 +16,21 @@ impl TextureCache { } } - pub fn request_texture(&mut self, device: &wgpu::Device, size: UVec2) -> Texture { + pub fn request_texture(&mut self, device: &wgpu::Device, size: UVec2, format: wgpu::TextureFormat) -> Texture { let size = size.max(UVec2::ONE); - if let Some(pos) = self.textures.iter().position(|texture| UVec2::new(texture.width(), texture.height()) == size && !texture.is_shared()) { + if let Some(pos) = self + .textures + .iter() + .position(|texture| UVec2::new(texture.width(), texture.height()) == size && texture.format() == format && !texture.is_shared()) + { let entry = self.textures.remove(pos).unwrap(); let texture = entry.clone(); self.textures.push_back(entry); return texture; } - let incoming_bytes = size.x as u64 * size.y as u64 * 4; + let incoming_bytes = size.x as u64 * size.y as u64 * format.block_copy_size(None).unwrap_or(4) as u64; self.evict_until_fits(incoming_bytes); let texture: Texture = device @@ -40,12 +44,14 @@ impl TextureCache { mip_level_count: 1, sample_count: 1, dimension: wgpu::TextureDimension::D2, - format: wgpu::TextureFormat::Rgba8Unorm, - usage: wgpu::TextureUsages::COPY_SRC - | wgpu::TextureUsages::COPY_DST - | wgpu::TextureUsages::TEXTURE_BINDING - | wgpu::TextureUsages::STORAGE_BINDING - | wgpu::TextureUsages::RENDER_ATTACHMENT, + format, + usage: { + let common = wgpu::TextureUsages::COPY_SRC | wgpu::TextureUsages::COPY_DST | wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::RENDER_ATTACHMENT; + match format { + wgpu::TextureFormat::Rgba8Unorm => common | wgpu::TextureUsages::STORAGE_BINDING, + _ => common, + } + }, view_formats: &[], }) .into(); @@ -87,6 +93,6 @@ trait TextureMemoryCostEstimateExt { impl TextureMemoryCostEstimateExt for wgpu::Texture { fn memory_size_estimate(&self) -> u64 { - self.width() as u64 * self.height() as u64 * 4 + self.width() as u64 * self.height() as u64 * self.format().block_copy_size(None).unwrap_or(4) as u64 } } From 773b8fd6aee0a24ef5cd578a2396e5fdb7d9af9b Mon Sep 17 00:00:00 2001 From: Timon Date: Mon, 27 Jul 2026 10:56:38 +0000 Subject: [PATCH 4/7] Add the owned GPU buffer type --- .../libraries/wgpu-executor/src/buffer.rs | 34 +++++++++++++++++++ node-graph/libraries/wgpu-executor/src/lib.rs | 11 ++++++ 2 files changed, 45 insertions(+) create mode 100644 node-graph/libraries/wgpu-executor/src/buffer.rs diff --git a/node-graph/libraries/wgpu-executor/src/buffer.rs b/node-graph/libraries/wgpu-executor/src/buffer.rs new file mode 100644 index 0000000000..b6f58a323b --- /dev/null +++ b/node-graph/libraries/wgpu-executor/src/buffer.rs @@ -0,0 +1,34 @@ +use std::ops::Deref; +use std::sync::Arc; + +#[derive(Clone, Debug)] +pub struct Buffer(Arc); + +#[derive(Debug)] +struct BufferInner(wgpu::Buffer); + +impl Drop for BufferInner { + fn drop(&mut self) { + self.0.destroy(); + } +} + +impl Deref for Buffer { + type Target = wgpu::Buffer; + + fn deref(&self) -> &Self::Target { + &self.0.0 + } +} + +impl AsRef for Buffer { + fn as_ref(&self) -> &wgpu::Buffer { + &self.0.0 + } +} + +impl From for Buffer { + fn from(buffer: wgpu::Buffer) -> Self { + Self(Arc::new(BufferInner(buffer))) + } +} diff --git a/node-graph/libraries/wgpu-executor/src/lib.rs b/node-graph/libraries/wgpu-executor/src/lib.rs index 01ba41d5c0..da8a64d0d0 100644 --- a/node-graph/libraries/wgpu-executor/src/lib.rs +++ b/node-graph/libraries/wgpu-executor/src/lib.rs @@ -1,3 +1,4 @@ +mod buffer; mod context; mod pipeline; pub mod shader_runtime; @@ -14,8 +15,10 @@ use glam::UVec2; use graphene_application_io::{ApplicationIo, EditorApi}; use std::sync::Arc; use vello::{AaConfig, AaSupport, RenderParams, Renderer, RendererOptions, Scene}; +use wgpu::util::DeviceExt; use wgpu::{Origin3d, TextureAspect}; +pub use buffer::Buffer; pub use context::Context as WgpuContext; pub use context::ContextBuilder as WgpuContextBuilder; pub use pipeline::AsyncPipeline as AsyncWgpuPipeline; @@ -116,6 +119,14 @@ impl WgpuExecutor { pub fn request_texture_with_format(&self, size: UVec2, format: wgpu::TextureFormat) -> Texture { self.inner.texture_cache.lock().unwrap().request_texture(&self.context().device, size, format) } + + pub fn create_buffer(&self, desc: &wgpu::BufferDescriptor) -> Buffer { + self.context().device.create_buffer(desc).into() + } + + pub fn create_buffer_init(&self, desc: &wgpu::util::BufferInitDescriptor) -> Buffer { + self.context().device.create_buffer_init(desc).into() + } } impl WgpuExecutor { From c11d2d6a4cf58fcfaf9e0a56956b42b678011be2 Mon Sep 17 00:00:00 2001 From: Timon Date: Mon, 27 Jul 2026 11:05:21 +0000 Subject: [PATCH 5/7] Route the remaining GPU allocations through the executor --- node-graph/libraries/wgpu-executor/src/lib.rs | 6 +- .../wgpu-executor/src/shader_runtime/mod.rs | 12 +--- .../per_pixel_adjust_runtime.rs | 39 +++++------- .../wgpu-executor/src/texture_conversion.rs | 61 ++++++++----------- .../src/shader_nodes/per_pixel_adjust.rs | 2 +- .../nodes/gstd/src/render_background.rs | 35 ++++++----- 6 files changed, 61 insertions(+), 94 deletions(-) diff --git a/node-graph/libraries/wgpu-executor/src/lib.rs b/node-graph/libraries/wgpu-executor/src/lib.rs index da8a64d0d0..73163edb90 100644 --- a/node-graph/libraries/wgpu-executor/src/lib.rs +++ b/node-graph/libraries/wgpu-executor/src/lib.rs @@ -44,10 +44,6 @@ impl WgpuExecutor { pub fn context(&self) -> &WgpuContext { &self.inner.context } - - pub fn shader_runtime(&self) -> &ShaderRuntime { - &self.inner.shader_runtime - } } #[derive(dyn_any::DynAny)] @@ -149,7 +145,7 @@ impl WgpuExecutor { let texture_cache = TextureCache::new(TEXTURE_CACHE_SIZE); - let shader_runtime = ShaderRuntime::new(&context); + let shader_runtime = ShaderRuntime::default(); Some(Self { inner: Arc::new(WgpuExecutorInner { diff --git a/node-graph/libraries/wgpu-executor/src/shader_runtime/mod.rs b/node-graph/libraries/wgpu-executor/src/shader_runtime/mod.rs index a32e17aabb..540ddf8c8a 100644 --- a/node-graph/libraries/wgpu-executor/src/shader_runtime/mod.rs +++ b/node-graph/libraries/wgpu-executor/src/shader_runtime/mod.rs @@ -1,20 +1,10 @@ -use crate::WgpuContext; use crate::shader_runtime::per_pixel_adjust_runtime::PerPixelAdjustShaderRuntime; pub mod per_pixel_adjust_runtime; pub const FULLSCREEN_VERTEX_SHADER_NAME: &str = "fullscreen_vertex_fullscreen_vertex"; +#[derive(Default)] pub struct ShaderRuntime { - context: WgpuContext, per_pixel_adjust: PerPixelAdjustShaderRuntime, } - -impl ShaderRuntime { - pub fn new(context: &WgpuContext) -> Self { - Self { - context: context.clone(), - per_pixel_adjust: PerPixelAdjustShaderRuntime::new(), - } - } -} diff --git a/node-graph/libraries/wgpu-executor/src/shader_runtime/per_pixel_adjust_runtime.rs b/node-graph/libraries/wgpu-executor/src/shader_runtime/per_pixel_adjust_runtime.rs index 9f393d6945..761fbcad25 100644 --- a/node-graph/libraries/wgpu-executor/src/shader_runtime/per_pixel_adjust_runtime.rs +++ b/node-graph/libraries/wgpu-executor/src/shader_runtime/per_pixel_adjust_runtime.rs @@ -1,16 +1,17 @@ -use crate::WgpuContext; -use crate::shader_runtime::{FULLSCREEN_VERTEX_SHADER_NAME, ShaderRuntime}; +use crate::shader_runtime::FULLSCREEN_VERTEX_SHADER_NAME; +use crate::{Buffer, WgpuContext, WgpuExecutor}; use core_types::list::{Item, List}; use core_types::shaders::buffer_struct::BufferStruct; use futures::lock::Mutex; +use glam::UVec2; use raster_types::{GPU, Raster}; use std::borrow::Cow; use std::collections::HashMap; -use wgpu::util::{BufferInitDescriptor, DeviceExt}; +use wgpu::util::BufferInitDescriptor; use wgpu::{ - BindGroupDescriptor, BindGroupEntry, BindGroupLayoutDescriptor, BindGroupLayoutEntry, BindingResource, BindingType, Buffer, BufferBinding, BufferBindingType, BufferUsages, ColorTargetState, Face, + BindGroupDescriptor, BindGroupEntry, BindGroupLayoutDescriptor, BindGroupLayoutEntry, BindingResource, BindingType, BufferBinding, BufferBindingType, BufferUsages, ColorTargetState, Face, FragmentState, FrontFace, LoadOp, Operations, PipelineLayoutDescriptor, PolygonMode, PrimitiveState, PrimitiveTopology, RenderPassColorAttachment, RenderPassDescriptor, RenderPipelineDescriptor, - ShaderModuleDescriptor, ShaderSource, ShaderStages, StoreOp, TextureDescriptor, TextureDimension, TextureFormat, TextureSampleType, TextureViewDescriptor, TextureViewDimension, VertexState, + ShaderModuleDescriptor, ShaderSource, ShaderStages, StoreOp, TextureFormat, TextureSampleType, TextureViewDescriptor, TextureViewDimension, VertexState, }; pub struct PerPixelAdjustShaderRuntime { @@ -32,22 +33,21 @@ impl PerPixelAdjustShaderRuntime { } } -impl ShaderRuntime { +impl WgpuExecutor { pub async fn run_per_pixel_adjust(&self, shaders: &Shaders<'_>, textures: List>, args: Option<&T>) -> List> { - let mut cache = self.per_pixel_adjust.pipeline_cache.lock().await; + let mut cache = self.inner.shader_runtime.per_pixel_adjust.pipeline_cache.lock().await; let pipeline = cache .entry(shaders.fragment_shader_name.to_owned()) - .or_insert_with(|| PerPixelAdjustGraphicsPipeline::new(&self.context, shaders)); + .or_insert_with(|| PerPixelAdjustGraphicsPipeline::new(self.context(), shaders)); let arg_buffer = args.map(|args| { - let device = &self.context.device; - device.create_buffer_init(&BufferInitDescriptor { + self.create_buffer_init(&BufferInitDescriptor { label: Some(&format!("{} arg buffer", pipeline.name.as_str())), usage: BufferUsages::STORAGE, contents: bytemuck::bytes_of(&T::write(*args)), }) }); - pipeline.dispatch(&self.context, textures, arg_buffer) + pipeline.dispatch(self, textures, arg_buffer) } } @@ -160,9 +160,9 @@ impl PerPixelAdjustGraphicsPipeline { } } - pub fn dispatch(&self, context: &WgpuContext, textures: List>, arg_buffer: Option) -> List> { + pub fn dispatch(&self, executor: &WgpuExecutor, textures: List>, arg_buffer: Option) -> List> { assert_eq!(self.has_uniform, arg_buffer.is_some()); - let device = &context.device; + let device = &executor.context().device; let name = self.name.as_str(); let mut cmd = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { @@ -203,16 +203,7 @@ impl PerPixelAdjustGraphicsPipeline { entries, }); - let tex_out = device.create_texture(&TextureDescriptor { - label: Some(&format!("{name} texture out")), - size: tex_in.size(), - mip_level_count: 1, - sample_count: 1, - dimension: TextureDimension::D2, - format, - usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST | wgpu::TextureUsages::COPY_SRC | wgpu::TextureUsages::RENDER_ATTACHMENT, - view_formats: &[format], - }); + let tex_out = executor.request_texture_with_format(UVec2::new(tex_in.width(), tex_in.height()), format); let view_out = tex_out.create_view(&TextureViewDescriptor::default()); let mut rp = cmd.begin_render_pass(&RenderPassDescriptor { @@ -237,7 +228,7 @@ impl PerPixelAdjustGraphicsPipeline { Item::from_parts(Raster::new_gpu(tex_out), attributes) }) .collect::>(); - context.queue.submit([cmd.finish()]); + executor.context().queue.submit([cmd.finish()]); out } } diff --git a/node-graph/libraries/wgpu-executor/src/texture_conversion.rs b/node-graph/libraries/wgpu-executor/src/texture_conversion.rs index da5fe1c073..f625e80e3e 100644 --- a/node-graph/libraries/wgpu-executor/src/texture_conversion.rs +++ b/node-graph/libraries/wgpu-executor/src/texture_conversion.rs @@ -1,4 +1,4 @@ -use crate::WgpuExecutor; +use crate::{Buffer, WgpuExecutor}; use core_types::Color; use core_types::Ctx; use core_types::color::SRGBA8; @@ -6,36 +6,29 @@ use core_types::list::{Item, List}; use core_types::ops::Convert; use core_types::transform::Footprint; use raster_types::Image; -use raster_types::{CPU, GPU, Raster}; -use wgpu::util::{DeviceExt, TextureDataOrder}; -use wgpu::{Extent3d, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages}; +use raster_types::{CPU, GPU, Raster, Texture}; +use wgpu::{Extent3d, TextureFormat}; /// Uploads CPU image data to a GPU 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: &wgpu::Device, queue: &wgpu::Queue, image: &Raster) -> wgpu::Texture { +fn upload_to_texture(executor: &WgpuExecutor, queue: &wgpu::Queue, image: &Raster) -> Texture { let rgba8_data: Vec = image.data.iter().map(|x| (*x).into()).collect(); - 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, - usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST | TextureUsages::COPY_SRC, - view_formats: &[], - }, - TextureDataOrder::LayerMajor, + let texture = executor.request_texture_with_format(glam::UVec2::new(image.width, image.height), TextureFormat::Rgba8UnormSrgb); + queue.write_texture( + texture.as_image_copy(), bytemuck::cast_slice(rgba8_data.as_slice()), - ) + wgpu::TexelCopyBufferLayout { + offset: 0, + bytes_per_row: Some(4 * image.width), + rows_per_image: Some(image.height), + }, + Extent3d { + width: image.width, + height: image.height, + depth_or_array_layers: 1, + }, + ); + texture } /// Converts a Raster texture to Raster by downloading the underlying texture data. @@ -45,7 +38,7 @@ fn upload_to_texture(device: &wgpu::Device, queue: &wgpu::Queue, image: &Raster< /// - 4 bytes-per-pixel RGBA8 /// - Texture has COPY_SRC usage struct RasterGpuToRasterCpuConverter { - buffer: wgpu::Buffer, + buffer: Buffer, width: u32, height: u32, unpadded_bytes_per_row: u32, @@ -53,7 +46,7 @@ struct RasterGpuToRasterCpuConverter { _source: raster_types::Texture, } impl RasterGpuToRasterCpuConverter { - fn new(device: &wgpu::Device, encoder: &mut wgpu::CommandEncoder, data_gpu: Raster) -> Self { + fn new(executor: &WgpuExecutor, encoder: &mut wgpu::CommandEncoder, data_gpu: Raster) -> Self { let texture = data_gpu.data(); let width = texture.width(); let height = texture.height(); @@ -63,7 +56,7 @@ impl RasterGpuToRasterCpuConverter { let padded_bytes_per_row = unpadded_bytes_per_row.div_ceil(align) * align; let buffer_size = padded_bytes_per_row as u64 * height as u64; - let buffer = device.create_buffer(&wgpu::BufferDescriptor { + let buffer = executor.create_buffer(&wgpu::BufferDescriptor { label: Some("texture_download_buffer"), size: buffer_size, usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ, @@ -152,13 +145,12 @@ 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.lock(); let list = self .into_iter() .map(|row| { let (image, attributes) = row.into_parts(); - let texture = upload_to_texture(device, &queue, &image); + let texture = upload_to_texture(executor, &queue, &image); Item::from_parts(Raster::new_gpu(texture), attributes) }) @@ -172,9 +164,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.lock(); - let texture = upload_to_texture(device, &queue, &self); + let texture = upload_to_texture(executor, &queue, &self); queue.submit([]); Raster::new_gpu(texture) @@ -203,7 +194,7 @@ impl<'i> Convert>, &'i WgpuExecutor> for List> { for row in self { let (element, attributes) = row.into_parts(); - converters.push(RasterGpuToRasterCpuConverter::new(device, &mut encoder, element)); + converters.push(RasterGpuToRasterCpuConverter::new(executor, &mut encoder, element)); rows_meta.push(Item::from_parts((), attributes)); } @@ -240,7 +231,7 @@ impl<'i> Convert, &'i WgpuExecutor> for Raster { label: Some("single_texture_download_encoder"), }); - let converter = RasterGpuToRasterCpuConverter::new(device, &mut encoder, self); + let converter = RasterGpuToRasterCpuConverter::new(executor, &mut encoder, self); queue.submit([encoder.finish()]); 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 97f87fc4db..c88d7d4242 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 @@ -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.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/src/render_background.rs b/node-graph/nodes/gstd/src/render_background.rs index 7db3e75785..97d3fb921c 100644 --- a/node-graph/nodes/gstd/src/render_background.rs +++ b/node-graph/nodes/gstd/src/render_background.rs @@ -8,8 +8,7 @@ use graph_craft::document::value::{RenderOutput, RenderOutputType}; use graphic_types::raster_types::Texture; use rendering::{RenderParams, SvgRender, SvgRenderOutput}; use std::fmt::Write; -use wgpu::util::DeviceExt; -use wgpu_executor::{AsyncWgpuPipeline, WgpuExecutor, WgpuPipelineCache}; +use wgpu_executor::{AsyncWgpuPipeline, Buffer, WgpuExecutor, WgpuPipelineCache}; #[node_macro::node(category(""))] async fn render_background<'a: 'n>( @@ -358,10 +357,8 @@ impl AsyncWgpuPipeline for CompositeBackground { let foreground_view = foreground.create_view(&wgpu::TextureViewDescriptor::default()); let checker_draws = if backgrounds.is_empty() { - vec![( - 3, - self.create_checker_bind_group(device, CompositeUniforms::fullscreen(viewport_size, screen_to_document, checker_size_doc)), - )] + let uniforms = CompositeUniforms::fullscreen(viewport_size, screen_to_document, checker_size_doc).create_buffer(executor); + vec![(3, self.create_checker_bind_group(device, &uniforms), uniforms)] } else { backgrounds .iter() @@ -376,8 +373,8 @@ impl AsyncWgpuPipeline for CompositeBackground { return None; } - let uniforms = CompositeUniforms::rect(min, max, document_to_screen, viewport_size, checker_size_doc); - Some((6, self.create_checker_bind_group(device, uniforms))) + let uniforms = CompositeUniforms::rect(min, max, document_to_screen, viewport_size, checker_size_doc).create_buffer(executor); + Some((6, self.create_checker_bind_group(device, &uniforms), uniforms)) }) .collect() }; @@ -419,13 +416,13 @@ impl AsyncWgpuPipeline for CompositeBackground { if backgrounds.is_empty() { pass.set_pipeline(&self.checker_viewport_pipeline); - for (vertex_count, bind_group) in &checker_draws { + for (vertex_count, bind_group, _uniforms) in &checker_draws { pass.set_bind_group(0, bind_group, &[]); pass.draw(0..*vertex_count, 0..1); } } else { pass.set_pipeline(&self.checker_rect_pipeline); - for (vertex_count, bind_group) in &checker_draws { + for (vertex_count, bind_group, _uniforms) in &checker_draws { pass.set_bind_group(0, bind_group, &[]); pass.draw(0..*vertex_count, 0..1); } @@ -443,19 +440,13 @@ impl AsyncWgpuPipeline for CompositeBackground { } impl CompositeBackground { - 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"), - contents: bytemuck::bytes_of(&uniforms), - usage: wgpu::BufferUsages::UNIFORM, - }); - + fn create_checker_bind_group(&self, device: &wgpu::Device, uniforms: &Buffer) -> wgpu::BindGroup { device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("background_checker_bind_group"), layout: &self.checker_bind_group_layout, entries: &[wgpu::BindGroupEntry { binding: 0, - resource: buffer.as_entire_binding(), + resource: uniforms.as_entire_binding(), }], }) } @@ -497,4 +488,12 @@ impl CompositeUniforms { _pad: 0., } } + + fn create_buffer(&self, executor: &WgpuExecutor) -> Buffer { + executor.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("background_checker_uniforms"), + contents: bytemuck::bytes_of(self), + usage: wgpu::BufferUsages::UNIFORM, + }) + } } From 3d9f2a5087847faa46aaaf6e88f7cb88230d476f Mon Sep 17 00:00:00 2001 From: Timon Date: Mon, 27 Jul 2026 14:18:22 +0000 Subject: [PATCH 6/7] Add weak texture parking to the texture cache --- .../raster-types/src/raster_types.rs | 19 +++++++++++++- .../wgpu-executor/src/texture_cache.rs | 26 ++++++++++--------- 2 files changed, 32 insertions(+), 13 deletions(-) diff --git a/node-graph/libraries/raster-types/src/raster_types.rs b/node-graph/libraries/raster-types/src/raster_types.rs index 430b753668..d210447182 100644 --- a/node-graph/libraries/raster-types/src/raster_types.rs +++ b/node-graph/libraries/raster-types/src/raster_types.rs @@ -140,7 +140,7 @@ mod cpu { pub use gpu::GPU; #[cfg(feature = "wgpu")] -pub use gpu::Texture; +pub use gpu::{Texture, TextureWeakRef}; #[cfg(feature = "wgpu")] mod gpu { @@ -164,6 +164,23 @@ mod gpu { pub fn is_shared(&self) -> bool { Arc::strong_count(&self.0) > 1 } + + pub fn is_weakly_shared(&self) -> bool { + Arc::weak_count(&self.0) > 0 + } + + pub fn downgrade(&self) -> TextureWeakRef { + TextureWeakRef(Arc::downgrade(&self.0)) + } + } + + #[derive(Clone, Debug)] + pub struct TextureWeakRef(std::sync::Weak); + + impl TextureWeakRef { + pub fn upgrade(&self) -> Option { + self.0.upgrade().map(Texture) + } } impl Deref for Texture { diff --git a/node-graph/libraries/wgpu-executor/src/texture_cache.rs b/node-graph/libraries/wgpu-executor/src/texture_cache.rs index 6ad0817d28..4b8c85671b 100644 --- a/node-graph/libraries/wgpu-executor/src/texture_cache.rs +++ b/node-graph/libraries/wgpu-executor/src/texture_cache.rs @@ -22,7 +22,7 @@ impl TextureCache { if let Some(pos) = self .textures .iter() - .position(|texture| UVec2::new(texture.width(), texture.height()) == size && texture.format() == format && !texture.is_shared()) + .position(|texture| UVec2::new(texture.width(), texture.height()) == size && texture.format() == format && !texture.is_shared() && !texture.is_weakly_shared()) { let entry = self.textures.remove(pos).unwrap(); let texture = entry.clone(); @@ -73,17 +73,19 @@ impl TextureCache { return; } - self.textures.retain(|texture| { - if free_bytes + incoming_bytes <= max_free_bytes { - return true; - } - if !texture.is_shared() { - free_bytes -= texture.memory_size_estimate(); - false - } else { - true - } - }); + for parked in [false, true] { + self.textures.retain(|texture| { + if free_bytes + incoming_bytes <= max_free_bytes { + return true; + } + if !texture.is_shared() && texture.is_weakly_shared() == parked { + free_bytes -= texture.memory_size_estimate(); + false + } else { + true + } + }); + } } } From 9cf30bf40e844ee7a684ab1f81d49640cecd560c Mon Sep 17 00:00:00 2001 From: Timon Date: Mon, 27 Jul 2026 19:17:06 +0000 Subject: [PATCH 7/7] Raise the texture cache budget to 1GB for native and 512MB for wasm --- node-graph/libraries/wgpu-executor/src/lib.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/node-graph/libraries/wgpu-executor/src/lib.rs b/node-graph/libraries/wgpu-executor/src/lib.rs index 73163edb90..e363f31c2e 100644 --- a/node-graph/libraries/wgpu-executor/src/lib.rs +++ b/node-graph/libraries/wgpu-executor/src/lib.rs @@ -33,7 +33,10 @@ pub use wgpu_sync::Instance as WgpuInstance; pub use wgpu_sync::Queue as WgpuQueue; pub use wgpu_sync::Surface as WgpuSurface; -const TEXTURE_CACHE_SIZE: u64 = 256 * 1024 * 1024; // 256 MiB +#[cfg(not(target_family = "wasm"))] +const TEXTURE_CACHE_SIZE: u64 = 1024 * 1024 * 1024; // 1GB +#[cfg(target_family = "wasm")] +const TEXTURE_CACHE_SIZE: u64 = 512 * 1024 * 1024; // 512MB #[derive(dyn_any::DynAny, Clone)] pub struct WgpuExecutor {