Skip to content

Commit 116a410

Browse files
authored
Add texture pool to render cache node (#3804)
* Add texture pool to render cache node * Use direct texture copy instead of bitter and fix graphene_cli * Remove warnings * Fix wgpu import path * Code review fixes
1 parent 2ac82a1 commit 116a410

File tree

8 files changed

+88
-48
lines changed

8 files changed

+88
-48
lines changed

desktop/src/render/state.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,13 @@ pub(crate) struct RenderState {
1111
executor: WgpuExecutor,
1212
config: wgpu::SurfaceConfiguration,
1313
render_pipeline: wgpu::RenderPipeline,
14-
transparent_texture: wgpu::Texture,
14+
transparent_texture: std::sync::Arc<wgpu::Texture>,
1515
sampler: wgpu::Sampler,
1616
desired_width: u32,
1717
desired_height: u32,
1818
viewport_scale: [f32; 2],
1919
viewport_offset: [f32; 2],
20-
viewport_texture: Option<wgpu::Texture>,
20+
viewport_texture: Option<std::sync::Arc<wgpu::Texture>>,
2121
overlays_texture: Option<TargetTexture>,
2222
ui_texture: Option<wgpu::Texture>,
2323
bind_group: Option<wgpu::BindGroup>,
@@ -50,7 +50,7 @@ impl RenderState {
5050

5151
surface.configure(&context.device, &config);
5252

53-
let transparent_texture = context.device.create_texture(&wgpu::TextureDescriptor {
53+
let transparent_texture = std::sync::Arc::new(context.device.create_texture(&wgpu::TextureDescriptor {
5454
label: Some("Transparent Texture"),
5555
size: wgpu::Extent3d {
5656
width: 1,
@@ -63,7 +63,7 @@ impl RenderState {
6363
format: wgpu::TextureFormat::Bgra8UnormSrgb,
6464
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
6565
view_formats: &[],
66-
});
66+
}));
6767

6868
// Create shader module
6969
let shader = context.device.create_shader_module(wgpu::include_wgsl!("composite_shader.wgsl"));
@@ -207,7 +207,7 @@ impl RenderState {
207207
}
208208
}
209209

210-
pub(crate) fn bind_viewport_texture(&mut self, viewport_texture: wgpu::Texture) {
210+
pub(crate) fn bind_viewport_texture(&mut self, viewport_texture: std::sync::Arc<wgpu::Texture>) {
211211
self.viewport_texture = Some(viewport_texture);
212212
self.update_bindgroup();
213213
}

desktop/wrapper/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ impl DesktopWrapper {
5858
}
5959

6060
pub enum NodeGraphExecutionResult {
61-
HasRun(Option<wgpu::Texture>),
61+
HasRun(Option<std::sync::Arc<wgpu::Texture>>),
6262
NotRun,
6363
}
6464

editor/src/node_graph_executor/runtime.rs

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ pub struct NodeRuntime {
5959
/// Cached surface for Wasm viewport rendering (reused across frames)
6060
#[cfg(all(target_family = "wasm", feature = "gpu"))]
6161
wasm_viewport_surface: Option<wgpu_executor::WgpuSurface>,
62+
/// Currently displayed texture, the runtime keeps a reference to it to avoid the texture getting destroyed while it is still in use.
63+
#[cfg(all(target_family = "wasm", feature = "gpu"))]
64+
current_viewport_texture: Option<ImageTexture>,
6265
}
6366

6467
/// Messages passed from the editor thread to the node runtime thread.
@@ -144,6 +147,8 @@ impl NodeRuntime {
144147
inspect_state: None,
145148
#[cfg(all(target_family = "wasm", feature = "gpu"))]
146149
wasm_viewport_surface: None,
150+
#[cfg(all(target_family = "wasm", feature = "gpu"))]
151+
current_viewport_texture: None,
147152
}
148153
}
149154

@@ -275,7 +280,7 @@ impl NodeRuntime {
275280
.gpu_executor()
276281
.expect("GPU executor should be available when we receive a texture");
277282

278-
let raster_cpu = Raster::new_gpu(image_texture.texture).convert(Footprint::BOUNDLESS, executor).await;
283+
let raster_cpu = Raster::new_gpu(image_texture.texture.as_ref().clone()).convert(Footprint::BOUNDLESS, executor).await;
279284

280285
let (data, width, height) = raster_cpu.to_flat_u8();
281286

@@ -299,7 +304,7 @@ impl NodeRuntime {
299304
.gpu_executor()
300305
.expect("GPU executor should be available when we receive a texture");
301306

302-
let raster_cpu = Raster::new_gpu(image_texture.texture).convert(Footprint::BOUNDLESS, executor).await;
307+
let raster_cpu = Raster::new_gpu(image_texture.texture.as_ref().clone()).convert(Footprint::BOUNDLESS, executor).await;
303308

304309
self.sender.send_eyedropper_preview(raster_cpu);
305310
continue;
@@ -354,13 +359,22 @@ impl NodeRuntime {
354359
);
355360

356361
let surface_texture = surface_inner.get_current_texture().expect("Failed to get surface texture");
357-
358-
// Blit the rendered texture to the surface
359-
surface.surface.blitter.copy(
360-
&executor.context.device,
361-
&mut encoder,
362-
&image_texture.texture.create_view(&vello::wgpu::TextureViewDescriptor::default()),
363-
&surface_texture.texture.create_view(&vello::wgpu::TextureViewDescriptor::default()),
362+
self.current_viewport_texture = Some(image_texture.clone());
363+
364+
encoder.copy_texture_to_texture(
365+
vello::wgpu::TexelCopyTextureInfoBase {
366+
texture: image_texture.texture.as_ref(),
367+
mip_level: 0,
368+
origin: Default::default(),
369+
aspect: Default::default(),
370+
},
371+
vello::wgpu::TexelCopyTextureInfoBase {
372+
texture: &surface_texture.texture,
373+
mip_level: 0,
374+
origin: Default::default(),
375+
aspect: Default::default(),
376+
},
377+
image_texture.texture.size(),
364378
);
365379

366380
executor.context.queue.submit([encoder.finish()]);

node-graph/graphene-cli/src/export.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ pub async fn export_document(
6969
}
7070
RenderOutputType::Texture(image_texture) => {
7171
// Convert GPU texture to CPU buffer
72-
let gpu_raster = Raster::<GPU>::new_gpu(image_texture.texture);
72+
let gpu_raster = Raster::<GPU>::new_gpu(image_texture.texture.as_ref().clone());
7373
let cpu_raster: Raster<CPU> = gpu_raster.convert(Footprint::BOUNDLESS, wgpu_executor).await;
7474
let (data, width, height) = cpu_raster.to_flat_u8();
7575

node-graph/libraries/application-io/src/lib.rs

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,10 @@ impl Size for web_sys::HtmlCanvasElement {
5050
}
5151
}
5252

53-
#[derive(Debug, Clone)]
53+
#[derive(Debug, Clone, DynAny)]
5454
pub struct ImageTexture {
5555
#[cfg(feature = "wgpu")]
56-
pub texture: wgpu::Texture,
56+
pub texture: Arc<wgpu::Texture>,
5757
#[cfg(not(feature = "wgpu"))]
5858
pub texture: (),
5959
}
@@ -89,10 +89,6 @@ impl PartialEq for ImageTexture {
8989
}
9090
}
9191

92-
unsafe impl StaticType for ImageTexture {
93-
type Static = ImageTexture;
94-
}
95-
9692
#[cfg(feature = "wgpu")]
9793
impl Size for ImageTexture {
9894
fn size(&self) -> UVec2 {

node-graph/nodes/gstd/src/pixel_preview.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ pub async fn pixel_preview<'a: 'n>(
6161
let exec = editor_api.application_io.as_ref().unwrap().gpu_executor().unwrap();
6262
let resampled = exec.resample_texture(&source_texture.texture, physical_resolution, &transform);
6363

64-
result.data = RenderOutputType::Texture(ImageTexture { texture: resampled });
64+
result.data = RenderOutputType::Texture(ImageTexture { texture: resampled.into() });
6565

6666
result
6767
.metadata

node-graph/nodes/gstd/src/render_cache.rs

Lines changed: 50 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,12 @@ struct TileCacheImpl {
106106
timestamp: u64,
107107
total_memory: usize,
108108
cache_key: CacheKey,
109+
texture_cache_resolution: UVec2,
110+
/// Pool of textures of the same size: `texture_cache_resolution`.
111+
/// Reusing textures reduces the wgpu allocation pressure,
112+
/// which is a problem on web since we have to wait for
113+
/// the browser to garbage collect unused textures, eating up memory.
114+
texture_cache: Vec<Arc<wgpu::Texture>>,
109115
}
110116

111117
#[derive(Clone, Default, dyn_any::DynAny, Debug)]
@@ -214,6 +220,36 @@ impl TileCacheImpl {
214220
self.regions.clear();
215221
self.total_memory = 0;
216222
}
223+
224+
pub fn request_texture(&mut self, size: UVec2, device: &wgpu::Device) -> Arc<wgpu::Texture> {
225+
if self.texture_cache_resolution != size {
226+
self.texture_cache_resolution = size;
227+
self.texture_cache.clear();
228+
}
229+
self.texture_cache.truncate(5);
230+
for texture in &self.texture_cache {
231+
if Arc::strong_count(texture) == 1 {
232+
return Arc::clone(texture);
233+
}
234+
}
235+
let texture = Arc::new(device.create_texture(&wgpu::TextureDescriptor {
236+
label: Some("viewport_output"),
237+
size: wgpu::Extent3d {
238+
width: size.x,
239+
height: size.y,
240+
depth_or_array_layers: 1,
241+
},
242+
mip_level_count: 1,
243+
sample_count: 1,
244+
dimension: wgpu::TextureDimension::D2,
245+
format: wgpu::TextureFormat::Rgba8Unorm,
246+
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_DST | wgpu::TextureUsages::COPY_SRC | wgpu::TextureUsages::TEXTURE_BINDING,
247+
view_formats: &[],
248+
}));
249+
self.texture_cache.push(texture.clone());
250+
251+
texture
252+
}
217253
}
218254

219255
impl TileCache {
@@ -224,6 +260,10 @@ impl TileCache {
224260
pub fn store_regions(&self, regions: Vec<CachedRegion>) {
225261
self.0.lock().unwrap().store_regions(regions);
226262
}
263+
264+
pub fn request_texture(&self, size: UVec2, device: &wgpu::Device) -> Arc<wgpu::Texture> {
265+
self.0.lock().unwrap().request_texture(size, device)
266+
}
227267
}
228268

229269
fn group_into_regions(tiles: &[TileCoord], max_region_area: u32) -> Vec<RenderRegion> {
@@ -413,7 +453,11 @@ pub async fn render_output_cache<'a: 'n>(
413453
}
414454

415455
let exec = editor_api.application_io.as_ref().unwrap().gpu_executor().unwrap();
416-
let (output_texture, combined_metadata) = composite_cached_regions(&all_regions, physical_resolution, &device_origin_offset, &footprint.transform, exec);
456+
457+
let device = &exec.context.device;
458+
let output_texture = tile_cache.request_texture(physical_resolution, device);
459+
460+
let combined_metadata = composite_cached_regions(&all_regions, output_texture.as_ref(), &device_origin_offset, &footprint.transform, exec);
417461

418462
RenderOutput {
419463
data: RenderOutputType::Texture(ImageTexture { texture: output_texture }),
@@ -462,7 +506,7 @@ where
462506
let memory_size = (region_pixel_size.x * region_pixel_size.y) as usize * BYTES_PER_PIXEL;
463507

464508
CachedRegion {
465-
texture: rendered_texture.texture,
509+
texture: rendered_texture.texture.as_ref().clone(),
466510
texture_size: region_pixel_size,
467511
tiles: region.tiles.clone(),
468512
metadata: result.metadata,
@@ -473,29 +517,14 @@ where
473517

474518
fn composite_cached_regions(
475519
regions: &[CachedRegion],
476-
output_resolution: UVec2,
520+
output_texture: &wgpu::Texture,
477521
device_origin_offset: &DVec2,
478522
viewport_transform: &DAffine2,
479523
exec: &wgpu_executor::WgpuExecutor,
480-
) -> (wgpu::Texture, rendering::RenderMetadata) {
524+
) -> rendering::RenderMetadata {
481525
let device = &exec.context.device;
482526
let queue = &exec.context.queue;
483-
484-
// TODO: Use texture pool to reuse existing unused textures instead of allocating fresh ones every time
485-
let output_texture = device.create_texture(&wgpu::TextureDescriptor {
486-
label: Some("viewport_output"),
487-
size: wgpu::Extent3d {
488-
width: output_resolution.x,
489-
height: output_resolution.y,
490-
depth_or_array_layers: 1,
491-
},
492-
mip_level_count: 1,
493-
sample_count: 1,
494-
dimension: wgpu::TextureDimension::D2,
495-
format: wgpu::TextureFormat::Rgba8Unorm,
496-
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_DST | wgpu::TextureUsages::COPY_SRC | wgpu::TextureUsages::TEXTURE_BINDING,
497-
view_formats: &[],
498-
});
527+
let output_resolution = UVec2::new(output_texture.width(), output_texture.height());
499528

500529
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("composite") });
501530
let mut combined_metadata = rendering::RenderMetadata::default();
@@ -548,5 +577,5 @@ fn composite_cached_regions(
548577
}
549578

550579
queue.submit([encoder.finish()]);
551-
(output_texture, combined_metadata)
580+
combined_metadata
552581
}

node-graph/nodes/gstd/src/render_node.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -196,10 +196,11 @@ async fn render<'a: 'n>(ctx: impl Ctx + ExtractFootprint + ExtractVarArgs, edito
196196
None
197197
};
198198

199-
let texture = exec
200-
.render_vello_scene_to_texture(&scene, physical_resolution, context, background)
201-
.await
202-
.expect("Failed to render Vello scene");
199+
let texture = Arc::new(
200+
exec.render_vello_scene_to_texture(&scene, physical_resolution, context, background)
201+
.await
202+
.expect("Failed to render Vello scene"),
203+
);
203204

204205
RenderOutputType::Texture(ImageTexture { texture })
205206
}

0 commit comments

Comments
 (0)