Skip to content
Open
16 changes: 8 additions & 8 deletions desktop/src/render/state.rs
Original file line number Diff line number Diff line change
@@ -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)]
Expand All @@ -10,14 +10,14 @@ pub(crate) struct RenderState {
executor: WgpuExecutor,
config: wgpu::SurfaceConfiguration,
render_pipeline: wgpu::RenderPipeline,
transparent_texture: std::sync::Arc<wgpu::Texture>,
transparent_texture: Texture,
sampler: wgpu::Sampler,
desired_width: u32,
desired_height: u32,
viewport_scale: [f32; 2],
viewport_offset: [f32; 2],
viewport_texture: Option<std::sync::Arc<wgpu::Texture>>,
overlays_texture: Option<std::sync::Arc<wgpu::Texture>>,
viewport_texture: Option<Texture>,
overlays_texture: Option<Texture>,
ui_texture: Option<wgpu::Texture>,
bind_group: Option<wgpu::BindGroup>,
#[derivative(Debug = "ignore")]
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -193,7 +193,7 @@ impl RenderState {
self.surface_outdated = true;
}

pub(crate) fn bind_viewport_texture(&mut self, viewport_texture: std::sync::Arc<wgpu::Texture>) {
pub(crate) fn bind_viewport_texture(&mut self, viewport_texture: Texture) {
self.viewport_texture = Some(viewport_texture);
self.update_bindgroup();
}
Expand Down Expand Up @@ -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;
Expand Down
5 changes: 3 additions & 2 deletions desktop/wrapper/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,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;
Expand Down Expand Up @@ -55,14 +56,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<std::sync::Arc<wgpu::Texture>>),
HasRun(Option<Texture>),
NotRun,
}

Expand Down
54 changes: 37 additions & 17 deletions node-graph/libraries/raster-types/src/raster_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -149,37 +149,57 @@ mod gpu {
use std::sync::Arc;

#[derive(Clone, Debug, PartialEq, Eq, Hash, DynAny)]
pub struct Texture(Arc<wgpu::Texture>);
pub struct Texture(Arc<TextureInner>);

#[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
}

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<TextureInner>);

impl TextureWeakRef {
pub fn upgrade(&self) -> Option<Texture> {
self.0.upgrade().map(Texture)
}
}

impl Deref for Texture {
type Target = wgpu::Texture;

fn deref(&self) -> &Self::Target {
&self.0
&self.0.0
}
}

impl AsRef<wgpu::Texture> for Texture {
fn as_ref(&self) -> &wgpu::Texture {
&self.0
}
}

impl From<Arc<wgpu::Texture>> for Texture {
fn from(texture: Arc<wgpu::Texture>) -> Self {
Self(texture)
&self.0.0
}
}

impl From<wgpu::Texture> for Texture {
fn from(texture: wgpu::Texture) -> Self {
Self(Arc::new(texture))
}
}

impl From<Texture> for Arc<wgpu::Texture> {
fn from(texture: Texture) -> Self {
texture.0
Self(Arc::new(TextureInner(texture)))
}
}

Expand Down
34 changes: 34 additions & 0 deletions node-graph/libraries/wgpu-executor/src/buffer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
use std::ops::Deref;
use std::sync::Arc;

#[derive(Clone, Debug)]
pub struct Buffer(Arc<BufferInner>);

#[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<wgpu::Buffer> for Buffer {
fn as_ref(&self) -> &wgpu::Buffer {
&self.0.0
}
}

impl From<wgpu::Buffer> for Buffer {
fn from(buffer: wgpu::Buffer) -> Self {
Self(Arc::new(BufferInner(buffer)))
}
}
36 changes: 25 additions & 11 deletions node-graph/libraries/wgpu-executor/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
mod buffer;
mod context;
mod pipeline;
pub mod shader_runtime;
Expand All @@ -12,16 +13,18 @@ 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::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;
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;
Expand All @@ -30,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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: On WebGPU, this cap allows 512 MiB of unused textures to remain strongly cached, which can exceed browser GPU budgets and trigger device loss/OOM. Keep the cache cap within a conservative web budget or derive it from the available device budget.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At node-graph/libraries/wgpu-executor/src/lib.rs, line 39:

<comment>On WebGPU, this cap allows 512 MiB of unused textures to remain strongly cached, which can exceed browser GPU budgets and trigger device loss/OOM. Keep the cache cap within a conservative web budget or derive it from the available device budget.</comment>

<file context>
@@ -30,7 +33,10 @@ pub use wgpu_sync::Instance as WgpuInstance;
+#[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)]
</file context>
Suggested change
const TEXTURE_CACHE_SIZE: u64 = 512 * 1024 * 1024; // 512MB
const TEXTURE_CACHE_SIZE: u64 = 256 * 1024 * 1024; // 256 MiB


#[derive(dyn_any::DynAny, Clone)]
pub struct WgpuExecutor {
Expand All @@ -41,16 +47,12 @@ 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: std::sync::Mutex<TextureCache>,
vello_renderer: Mutex<Renderer>,
shader_runtime: ShaderRuntime,
}
Expand All @@ -69,7 +71,7 @@ impl<'a, T: ApplicationIo<Executor = WgpuExecutor>> From<&'a EditorApi<T>> for &

impl WgpuExecutor {
pub async fn render_vello_scene(&self, scene: &Scene, size: UVec2, context: &RenderContext, background: Option<Color>) -> Result<Texture> {
let texture = self.request_texture(size).await;
let texture = self.request_texture(size);

let texture_view = texture.create_view(&wgpu::TextureViewDescriptor::default());

Expand Down Expand Up @@ -109,8 +111,20 @@ impl WgpuExecutor {
pipeline.init::<P>(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.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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: request_texture_with_format calls texture_cache.lock().unwrap() on the new std::sync::Mutex. If anything panics while the lock is held (e.g. device.create_texture on device-lost or GPU OOM — the scenario this PR is fixing), the mutex is poisoned and every later request_texture call panics, taking down the app. The previous futures::lock::Mutex::lock().await did not poison, so this is a regression. Prefer handling the poisoned-lock case gracefully instead of unwrap().

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At node-graph/libraries/wgpu-executor/src/lib.rs, line 119:

<comment>`request_texture_with_format` calls `texture_cache.lock().unwrap()` on the new `std::sync::Mutex`. If anything panics while the lock is held (e.g. `device.create_texture` on device-lost or GPU OOM — the scenario this PR is fixing), the mutex is poisoned and every later `request_texture` call panics, taking down the app. The previous `futures::lock::Mutex::lock().await` did not poison, so this is a regression. Prefer handling the poisoned-lock case gracefully instead of `unwrap()`.</comment>

<file context>
@@ -109,8 +111,20 @@ 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)
+	}
+
</file context>

}

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()
}
}

Expand All @@ -134,7 +148,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 {
Expand Down
12 changes: 1 addition & 11 deletions node-graph/libraries/wgpu-executor/src/shader_runtime/mod.rs
Original file line number Diff line number Diff line change
@@ -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(),
}
}
}
Loading
Loading