diff --git a/desktop/src/render/state.rs b/desktop/src/render/state.rs index 11abd851e6..4db481d4bc 100644 --- a/desktop/src/render/state.rs +++ b/desktop/src/render/state.rs @@ -11,13 +11,13 @@ pub(crate) struct RenderState { executor: WgpuExecutor, config: wgpu::SurfaceConfiguration, render_pipeline: wgpu::RenderPipeline, - transparent_texture: wgpu::Texture, + transparent_texture: std::sync::Arc, sampler: wgpu::Sampler, desired_width: u32, desired_height: u32, viewport_scale: [f32; 2], viewport_offset: [f32; 2], - viewport_texture: Option, + viewport_texture: Option>, overlays_texture: Option, ui_texture: Option, bind_group: Option, @@ -50,7 +50,7 @@ impl RenderState { surface.configure(&context.device, &config); - let transparent_texture = context.device.create_texture(&wgpu::TextureDescriptor { + let transparent_texture = std::sync::Arc::new(context.device.create_texture(&wgpu::TextureDescriptor { label: Some("Transparent Texture"), size: wgpu::Extent3d { width: 1, @@ -63,7 +63,7 @@ impl RenderState { format: wgpu::TextureFormat::Bgra8UnormSrgb, usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING, view_formats: &[], - }); + })); // Create shader module let shader = context.device.create_shader_module(wgpu::include_wgsl!("composite_shader.wgsl")); @@ -207,7 +207,7 @@ impl RenderState { } } - pub(crate) fn bind_viewport_texture(&mut self, viewport_texture: wgpu::Texture) { + pub(crate) fn bind_viewport_texture(&mut self, viewport_texture: std::sync::Arc) { self.viewport_texture = Some(viewport_texture); self.update_bindgroup(); } diff --git a/desktop/wrapper/src/lib.rs b/desktop/wrapper/src/lib.rs index 8ee06c61e7..343d924a86 100644 --- a/desktop/wrapper/src/lib.rs +++ b/desktop/wrapper/src/lib.rs @@ -58,7 +58,7 @@ impl DesktopWrapper { } pub enum NodeGraphExecutionResult { - HasRun(Option), + HasRun(Option>), NotRun, } diff --git a/editor/src/node_graph_executor/runtime.rs b/editor/src/node_graph_executor/runtime.rs index 60b04c13e3..1389736667 100644 --- a/editor/src/node_graph_executor/runtime.rs +++ b/editor/src/node_graph_executor/runtime.rs @@ -59,6 +59,9 @@ pub struct NodeRuntime { /// Cached surface for WASM viewport rendering (reused across frames) #[cfg(all(target_family = "wasm", feature = "gpu"))] wasm_viewport_surface: Option, + /// Currently displayed texture, the runtime keeps a reference to it to avoid the texture getting destroyed while it is still in use. + #[cfg(all(target_family = "wasm", feature = "gpu"))] + current_viewport_texture: Option, } /// Messages passed from the editor thread to the node runtime thread. @@ -144,6 +147,8 @@ impl NodeRuntime { inspect_state: None, #[cfg(all(target_family = "wasm", feature = "gpu"))] wasm_viewport_surface: None, + #[cfg(all(target_family = "wasm", feature = "gpu"))] + current_viewport_texture: None, } } @@ -275,7 +280,7 @@ impl NodeRuntime { .gpu_executor() .expect("GPU executor should be available when we receive a texture"); - let raster_cpu = Raster::new_gpu(image_texture.texture).convert(Footprint::BOUNDLESS, executor).await; + let raster_cpu = Raster::new_gpu(image_texture.texture.as_ref().clone()).convert(Footprint::BOUNDLESS, executor).await; let (data, width, height) = raster_cpu.to_flat_u8(); @@ -299,7 +304,7 @@ impl NodeRuntime { .gpu_executor() .expect("GPU executor should be available when we receive a texture"); - let raster_cpu = Raster::new_gpu(image_texture.texture).convert(Footprint::BOUNDLESS, executor).await; + let raster_cpu = Raster::new_gpu(image_texture.texture.as_ref().clone()).convert(Footprint::BOUNDLESS, executor).await; self.sender.send_eyedropper_preview(raster_cpu); continue; @@ -354,13 +359,22 @@ impl NodeRuntime { ); let surface_texture = surface_inner.get_current_texture().expect("Failed to get surface texture"); - - // Blit the rendered texture to the surface - surface.surface.blitter.copy( - &executor.context.device, - &mut encoder, - &image_texture.texture.create_view(&vello::wgpu::TextureViewDescriptor::default()), - &surface_texture.texture.create_view(&vello::wgpu::TextureViewDescriptor::default()), + self.current_viewport_texture = Some(image_texture.clone()); + + encoder.copy_texture_to_texture( + vello::wgpu::TexelCopyTextureInfoBase { + texture: image_texture.texture.as_ref(), + mip_level: 0, + origin: Default::default(), + aspect: Default::default(), + }, + vello::wgpu::TexelCopyTextureInfoBase { + texture: &surface_texture.texture, + mip_level: 0, + origin: Default::default(), + aspect: Default::default(), + }, + image_texture.texture.size(), ); executor.context.queue.submit([encoder.finish()]); diff --git a/node-graph/graphene-cli/src/export.rs b/node-graph/graphene-cli/src/export.rs index d54015ac28..cfb7d5d6b0 100644 --- a/node-graph/graphene-cli/src/export.rs +++ b/node-graph/graphene-cli/src/export.rs @@ -66,7 +66,7 @@ pub async fn export_document( } RenderOutputType::Texture(image_texture) => { // Convert GPU texture to CPU buffer - let gpu_raster = Raster::::new_gpu(image_texture.texture); + let gpu_raster = Raster::::new_gpu(image_texture.texture.as_ref().clone()); let cpu_raster: Raster = gpu_raster.convert(Footprint::BOUNDLESS, wgpu_executor).await; let (data, width, height) = cpu_raster.to_flat_u8(); diff --git a/node-graph/libraries/application-io/src/lib.rs b/node-graph/libraries/application-io/src/lib.rs index c93ffaa445..0689a53a3d 100644 --- a/node-graph/libraries/application-io/src/lib.rs +++ b/node-graph/libraries/application-io/src/lib.rs @@ -50,10 +50,10 @@ impl Size for web_sys::HtmlCanvasElement { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, DynAny)] pub struct ImageTexture { #[cfg(feature = "wgpu")] - pub texture: wgpu::Texture, + pub texture: Arc, #[cfg(not(feature = "wgpu"))] pub texture: (), } @@ -89,10 +89,6 @@ impl PartialEq for ImageTexture { } } -unsafe impl StaticType for ImageTexture { - type Static = ImageTexture; -} - #[cfg(feature = "wgpu")] impl Size for ImageTexture { fn size(&self) -> UVec2 { diff --git a/node-graph/nodes/gstd/src/render_cache.rs b/node-graph/nodes/gstd/src/render_cache.rs index 022f577ee4..32e603ffdd 100644 --- a/node-graph/nodes/gstd/src/render_cache.rs +++ b/node-graph/nodes/gstd/src/render_cache.rs @@ -96,6 +96,7 @@ struct TileCacheImpl { total_memory: usize, cache_key: CacheKey, current_scale: f64, + texture_cache: (UVec2, Vec>), } impl Default for TileCacheImpl { @@ -106,6 +107,7 @@ impl Default for TileCacheImpl { total_memory: 0, cache_key: CacheKey::default(), current_scale: 0.0, + texture_cache: Default::default(), } } } @@ -224,6 +226,36 @@ impl TileCacheImpl { self.regions.clear(); self.total_memory = 0; } + + pub fn request_texture(&mut self, size: UVec2, device: &wgpu::Device) -> Arc { + if self.texture_cache.0 != size { + self.texture_cache.0 = size; + self.texture_cache.1.clear(); + } + self.texture_cache.1.truncate(5); + for texture in &self.texture_cache.1 { + if Arc::strong_count(&texture) == 1 { + return Arc::clone(texture); + } + } + let texture = Arc::new(device.create_texture(&wgpu::TextureDescriptor { + label: Some("viewport_output"), + 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::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_DST | wgpu::TextureUsages::COPY_SRC | wgpu::TextureUsages::TEXTURE_BINDING, + view_formats: &[], + })); + self.texture_cache.1.push(texture.clone()); + + texture + } } impl TileCache { @@ -234,6 +266,10 @@ impl TileCache { pub fn store_regions(&self, regions: Vec) { self.0.lock().unwrap().store_regions(regions); } + + pub fn request_texture(&self, size: UVec2, device: &wgpu::Device) -> Arc { + self.0.lock().unwrap().request_texture(size, device) + } } fn group_into_regions(tiles: &[TileCoord], scale: f64, max_region_area: u32) -> Vec { @@ -421,7 +457,11 @@ pub async fn render_output_cache<'a: 'n>( } let exec = editor_api.application_io.as_ref().unwrap().gpu_executor().unwrap(); - let (output_texture, combined_metadata) = composite_cached_regions(&all_regions, &viewport_bounds, physical_resolution, logical_scale, physical_scale, exec); + + let device = &exec.context.device; + let output_texture = tile_cache.request_texture(physical_resolution, device); + + let combined_metadata = composite_cached_regions(&all_regions, &viewport_bounds, output_texture.as_ref(), logical_scale, physical_scale, exec); RenderOutput { data: RenderOutputType::Texture(ImageTexture { texture: output_texture }), @@ -475,7 +515,7 @@ where let memory_size = (region_pixel_size.x * region_pixel_size.y) as usize * BYTES_PER_PIXEL; CachedRegion { - texture: rendered_texture.texture, + texture: rendered_texture.texture.as_ref().clone(), texture_size: region_pixel_size, scene_bounds: region.scene_bounds.clone(), tiles: region.tiles.clone(), @@ -488,29 +528,14 @@ where fn composite_cached_regions( regions: &[CachedRegion], viewport_bounds: &AxisAlignedBbox, - output_resolution: UVec2, + output_texture: &wgpu::Texture, logical_scale: f64, physical_scale: f64, exec: &wgpu_executor::WgpuExecutor, -) -> (wgpu::Texture, rendering::RenderMetadata) { +) -> rendering::RenderMetadata { let device = &exec.context.device; let queue = &exec.context.queue; - - // TODO: Use texture pool to reuse existing unused textures instead of allocating fresh ones every time - let output_texture = device.create_texture(&wgpu::TextureDescriptor { - label: Some("viewport_output"), - size: wgpu::Extent3d { - width: output_resolution.x, - height: output_resolution.y, - depth_or_array_layers: 1, - }, - mip_level_count: 1, - sample_count: 1, - dimension: wgpu::TextureDimension::D2, - format: wgpu::TextureFormat::Rgba8Unorm, - usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_DST | wgpu::TextureUsages::COPY_SRC | wgpu::TextureUsages::TEXTURE_BINDING, - view_formats: &[], - }); + let output_resolution = UVec2::new(output_texture.width(), output_texture.height()); let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("composite") }); let mut combined_metadata = rendering::RenderMetadata::default(); @@ -570,5 +595,5 @@ fn composite_cached_regions( } queue.submit([encoder.finish()]); - (output_texture, combined_metadata) + combined_metadata } diff --git a/node-graph/nodes/gstd/src/render_node.rs b/node-graph/nodes/gstd/src/render_node.rs index 11a6724b30..088ef89175 100644 --- a/node-graph/nodes/gstd/src/render_node.rs +++ b/node-graph/nodes/gstd/src/render_node.rs @@ -197,10 +197,11 @@ async fn render<'a: 'n>(ctx: impl Ctx + ExtractFootprint + ExtractVarArgs, edito None }; - let texture = exec - .render_vello_scene_to_texture(&scene, physical_resolution, context, background) - .await - .expect("Failed to render Vello scene"); + let texture = Arc::new( + exec.render_vello_scene_to_texture(&scene, physical_resolution, context, background) + .await + .expect("Failed to render Vello scene"), + ); RenderOutputType::Texture(ImageTexture { texture }) }