Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 16 additions & 13 deletions desktop/src/render/state.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::window::Window;

use crate::wrapper::{Color, WgpuContext, WgpuExecutor};
use wgpu_executor::TargetTexture;

#[derive(derivative::Derivative)]
#[derivative(Debug)]
Expand All @@ -17,7 +18,7 @@ pub(crate) struct RenderState {
viewport_scale: [f32; 2],
viewport_offset: [f32; 2],
viewport_texture: Option<wgpu::Texture>,
overlays_texture: Option<wgpu::Texture>,
overlays_target_texture: Option<TargetTexture>,
ui_texture: Option<wgpu::Texture>,
bind_group: Option<wgpu::BindGroup>,
#[derivative(Debug = "ignore")]
Expand Down Expand Up @@ -181,7 +182,7 @@ impl RenderState {
viewport_scale: [1.0, 1.0],
viewport_offset: [0.0, 0.0],
viewport_texture: None,
overlays_texture: None,
overlays_target_texture: None,
ui_texture: None,
bind_group: None,
overlays_scene: None,
Expand All @@ -208,11 +209,6 @@ impl RenderState {
self.update_bindgroup();
}

pub(crate) fn bind_overlays_texture(&mut self, overlays_texture: wgpu::Texture) {
self.overlays_texture = Some(overlays_texture);
self.update_bindgroup();
}

pub(crate) fn bind_ui_texture(&mut self, bind_ui_texture: wgpu::Texture) {
self.ui_texture = Some(bind_ui_texture);
self.update_bindgroup();
Expand All @@ -236,12 +232,15 @@ impl RenderState {
return;
};
let size = glam::UVec2::new(viewport_texture.width(), viewport_texture.height());
let texture = futures::executor::block_on(self.executor.render_vello_scene_to_texture(&scene, size, &Default::default(), Color::TRANSPARENT));
let Ok(texture) = texture else {
tracing::error!("Error rendering overlays");
let result = futures::executor::block_on(
self.executor
.render_vello_scene_to_target_texture(&scene, size, &Default::default(), Color::TRANSPARENT, &mut self.overlays_target_texture),
);
if let Err(e) = result {
tracing::error!("Error rendering overlays: {:?}", e);
return;
};
self.bind_overlays_texture(texture);
}
self.update_bindgroup();
}

pub(crate) fn render(&mut self, window: &Window) -> Result<(), RenderError> {
Expand Down Expand Up @@ -312,7 +311,11 @@ impl RenderState {

fn update_bindgroup(&mut self) {
let viewport_texture_view = self.viewport_texture.as_ref().unwrap_or(&self.transparent_texture).create_view(&wgpu::TextureViewDescriptor::default());
let overlays_texture_view = self.overlays_texture.as_ref().unwrap_or(&self.transparent_texture).create_view(&wgpu::TextureViewDescriptor::default());
let overlays_texture_view = self
.overlays_target_texture
.as_ref()
.map(|target| target.view())
.unwrap_or_else(|| &self.transparent_texture.create_view(&wgpu::TextureViewDescriptor::default()));
let ui_texture_view = self.ui_texture.as_ref().unwrap_or(&self.transparent_texture).create_view(&wgpu::TextureViewDescriptor::default());

let bind_group = self.context.device.create_bind_group(&wgpu::BindGroupDescriptor {
Expand Down
66 changes: 52 additions & 14 deletions node-graph/libraries/wgpu-executor/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,47 @@ pub struct TargetTexture {
size: UVec2,
}

impl TargetTexture {
/// Ensures the texture has the specified size, creating a new one if needed.
/// This allows reusing the same texture across frames when the size hasn't changed.
pub fn ensure_size(&mut self, device: &wgpu::Device, size: UVec2) {
let size = size.max(UVec2::ONE);
if self.size == size {
return;
}

let texture = device.create_texture(&wgpu::TextureDescriptor {
label: None,
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,
usage: wgpu::TextureUsages::STORAGE_BINDING | wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_SRC,
format: VELLO_SURFACE_FORMAT,
view_formats: &[],
});
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());

self.texture = texture;
self.view = view;
self.size = size;
}

/// Returns a reference to the texture view for rendering.
pub fn view(&self) -> &wgpu::TextureView {
&self.view
}

/// Returns a reference to the underlying texture.
pub fn texture(&self) -> &wgpu::Texture {
&self.texture
}
}

#[cfg(target_family = "wasm")]
pub type Window = web_sys::HtmlCanvasElement;
#[cfg(not(target_family = "wasm"))]
Expand All @@ -71,19 +112,14 @@ impl WgpuExecutor {
self.render_vello_scene_to_target_texture(scene, size, context, background, &mut output).await?;
Ok(output.unwrap().texture)
}

async fn render_vello_scene_to_target_texture(&self, scene: &Scene, size: UVec2, context: &RenderContext, background: Color, output: &mut Option<TargetTexture>) -> Result<()> {
let size = size.max(UVec2::ONE);
let target_texture = if let Some(target_texture) = output
&& target_texture.size == size
{
target_texture
} else {
pub async fn render_vello_scene_to_target_texture(&self, scene: &Scene, size: UVec2, context: &RenderContext, background: Color, output: &mut Option<TargetTexture>) -> Result<()> {
// Initialize with a minimal texture if this is the first call
if output.is_none() {
Copy link
Member

Choose a reason for hiding this comment

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

keep using if let syntax, then you also don't need the unwrap.

Copy link
Author

Choose a reason for hiding this comment

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

Good catch! You're right, I'll refactor to use if let syntax to avoid the unwrap(). Will update shortly.

let texture = self.context.device.create_texture(&wgpu::TextureDescriptor {
label: None,
size: wgpu::Extent3d {
width: size.x,
Copy link
Member

Choose a reason for hiding this comment

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

Why this change?

Copy link
Author

Choose a reason for hiding this comment

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

Thanks for calling this out. This change was to avoid output: Option being None at the moment we call ensure_size(). On the first call we create a minimal 1×1 texture so ensure_size() can run and reallocate it to the requested size.
I agree this is a bit wasteful since the 1×1 texture is usually replaced immediately. I went with this approach to keep the change small and low-risk, without widening the scope of this PR.
If you’d prefer the cleaner design, I’m happy to follow up by either:
making ensure_size() handle the None case directly, or
adding TargetTexture::new(device, size) so we allocate the correct size on the first call.

Copy link
Member

@timon-schelling timon-schelling Jan 23, 2026

Choose a reason for hiding this comment

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

Yes add new for target texture creation.

height: size.y,
width: 1,
height: 1,
depth_or_array_layers: 1,
},
mip_level_count: 1,
Expand All @@ -94,9 +130,11 @@ impl WgpuExecutor {
view_formats: &[],
});
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
*output = Some(TargetTexture { texture, view, size });
output.as_mut().unwrap()
};
*output = Some(TargetTexture { texture, view, size: UVec2::ONE });
}

let target_texture = output.as_mut().unwrap();
target_texture.ensure_size(&self.context.device, size);

let [r, g, b, a] = background.to_rgba8_srgb();
let render_params = RenderParams {
Expand All @@ -117,7 +155,7 @@ impl WgpuExecutor {
};
renderer.override_image(&image_brush.image, Some(texture_view));
}
renderer.render_to_texture(&self.context.device, &self.context.queue, scene, &target_texture.view, &render_params)?;
renderer.render_to_texture(&self.context.device, &self.context.queue, scene, target_texture.view(), &render_params)?;
for (image_brush, _) in context.resource_overrides.iter() {
renderer.override_image(&image_brush.image, None);
}
Expand Down