|
| 1 | +use crate::VELLO_SURFACE_FORMAT; |
| 2 | + |
| 3 | +/// A texture blitter that caches its bind group to avoid recreating it every frame. |
| 4 | +/// |
| 5 | +/// The standard wgpu `TextureBlitter` creates a new bind group on every `copy()` call, |
| 6 | +/// which causes excessive GPU resource allocation during viewport panning. This blitter |
| 7 | +/// maintains a persistent intermediate texture (recreated only on size change) and a cached |
| 8 | +/// bind group bound to it. Each frame, the source is copied into the persistent texture |
| 9 | +/// via `copy_texture_to_texture` (same format, no bind groups), then the cached bind group |
| 10 | +/// is used for the format-converting render pass. |
| 11 | +pub struct CachedBlitter { |
| 12 | + pipeline: wgpu::RenderPipeline, |
| 13 | + bind_group_layout: wgpu::BindGroupLayout, |
| 14 | + sampler: wgpu::Sampler, |
| 15 | + cache: std::sync::Mutex<Option<BlitCache>>, |
| 16 | +} |
| 17 | + |
| 18 | +struct BlitCache { |
| 19 | + source_texture: wgpu::Texture, |
| 20 | + bind_group: wgpu::BindGroup, |
| 21 | + size: wgpu::Extent3d, |
| 22 | +} |
| 23 | + |
| 24 | +const BLIT_SHADER: &str = include_str!("blit.wgsl"); |
| 25 | + |
| 26 | +impl CachedBlitter { |
| 27 | + pub fn new(device: &wgpu::Device, format: wgpu::TextureFormat) -> Self { |
| 28 | + let sampler = device.create_sampler(&wgpu::SamplerDescriptor { |
| 29 | + label: Some("CachedBlitter::sampler"), |
| 30 | + address_mode_u: wgpu::AddressMode::ClampToEdge, |
| 31 | + address_mode_v: wgpu::AddressMode::ClampToEdge, |
| 32 | + address_mode_w: wgpu::AddressMode::ClampToEdge, |
| 33 | + mag_filter: wgpu::FilterMode::Nearest, |
| 34 | + ..Default::default() |
| 35 | + }); |
| 36 | + |
| 37 | + let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { |
| 38 | + label: Some("CachedBlitter::bind_group_layout"), |
| 39 | + entries: &[ |
| 40 | + wgpu::BindGroupLayoutEntry { |
| 41 | + binding: 0, |
| 42 | + visibility: wgpu::ShaderStages::FRAGMENT, |
| 43 | + ty: wgpu::BindingType::Texture { |
| 44 | + sample_type: wgpu::TextureSampleType::Float { filterable: false }, |
| 45 | + view_dimension: wgpu::TextureViewDimension::D2, |
| 46 | + multisampled: false, |
| 47 | + }, |
| 48 | + count: None, |
| 49 | + }, |
| 50 | + wgpu::BindGroupLayoutEntry { |
| 51 | + binding: 1, |
| 52 | + visibility: wgpu::ShaderStages::FRAGMENT, |
| 53 | + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::NonFiltering), |
| 54 | + count: None, |
| 55 | + }, |
| 56 | + ], |
| 57 | + }); |
| 58 | + |
| 59 | + let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { |
| 60 | + label: Some("CachedBlitter::pipeline_layout"), |
| 61 | + bind_group_layouts: &[&bind_group_layout], |
| 62 | + push_constant_ranges: &[], |
| 63 | + }); |
| 64 | + |
| 65 | + let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { |
| 66 | + label: Some("CachedBlitter::shader"), |
| 67 | + source: wgpu::ShaderSource::Wgsl(BLIT_SHADER.into()), |
| 68 | + }); |
| 69 | + |
| 70 | + let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { |
| 71 | + label: Some("CachedBlitter::pipeline"), |
| 72 | + layout: Some(&pipeline_layout), |
| 73 | + vertex: wgpu::VertexState { |
| 74 | + module: &shader, |
| 75 | + entry_point: Some("vs_main"), |
| 76 | + compilation_options: wgpu::PipelineCompilationOptions::default(), |
| 77 | + buffers: &[], |
| 78 | + }, |
| 79 | + primitive: wgpu::PrimitiveState { |
| 80 | + topology: wgpu::PrimitiveTopology::TriangleList, |
| 81 | + ..Default::default() |
| 82 | + }, |
| 83 | + depth_stencil: None, |
| 84 | + multisample: wgpu::MultisampleState::default(), |
| 85 | + fragment: Some(wgpu::FragmentState { |
| 86 | + module: &shader, |
| 87 | + entry_point: Some("fs_main"), |
| 88 | + compilation_options: wgpu::PipelineCompilationOptions::default(), |
| 89 | + targets: &[Some(wgpu::ColorTargetState { |
| 90 | + format, |
| 91 | + blend: None, |
| 92 | + write_mask: wgpu::ColorWrites::ALL, |
| 93 | + })], |
| 94 | + }), |
| 95 | + multiview: None, |
| 96 | + cache: None, |
| 97 | + }); |
| 98 | + |
| 99 | + Self { |
| 100 | + pipeline, |
| 101 | + bind_group_layout, |
| 102 | + sampler, |
| 103 | + cache: std::sync::Mutex::new(None), |
| 104 | + } |
| 105 | + } |
| 106 | + |
| 107 | + /// Copies the source texture to the target with format conversion, using a cached bind group. |
| 108 | + /// |
| 109 | + /// Internally maintains a persistent intermediate texture. Each frame: |
| 110 | + /// 1. Copies `source` → intermediate via `copy_texture_to_texture` (same format, no bind groups) |
| 111 | + /// 2. Blits intermediate → `target` via a render pass with the cached bind group |
| 112 | + /// |
| 113 | + /// The bind group and intermediate texture are only recreated when the source size changes. |
| 114 | + pub fn copy( |
| 115 | + &self, |
| 116 | + device: &wgpu::Device, |
| 117 | + encoder: &mut wgpu::CommandEncoder, |
| 118 | + source: &wgpu::Texture, |
| 119 | + target: &wgpu::TextureView, |
| 120 | + ) { |
| 121 | + let size = source.size(); |
| 122 | + |
| 123 | + // Take cache out of mutex to avoid holding the lock during GPU operations |
| 124 | + let mut cache = self.cache.lock().unwrap().take(); |
| 125 | + |
| 126 | + // Recreate the persistent texture and bind group if size changed |
| 127 | + if !matches!(&cache, Some(c) if c.size == size) { |
| 128 | + let texture = device.create_texture(&wgpu::TextureDescriptor { |
| 129 | + label: Some("CachedBlitter::intermediate"), |
| 130 | + size, |
| 131 | + mip_level_count: 1, |
| 132 | + sample_count: 1, |
| 133 | + dimension: wgpu::TextureDimension::D2, |
| 134 | + format: VELLO_SURFACE_FORMAT, |
| 135 | + usage: wgpu::TextureUsages::COPY_DST | wgpu::TextureUsages::TEXTURE_BINDING, |
| 136 | + view_formats: &[], |
| 137 | + }); |
| 138 | + let view = texture.create_view(&wgpu::TextureViewDescriptor::default()); |
| 139 | + let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { |
| 140 | + label: Some("CachedBlitter::bind_group"), |
| 141 | + layout: &self.bind_group_layout, |
| 142 | + entries: &[ |
| 143 | + wgpu::BindGroupEntry { |
| 144 | + binding: 0, |
| 145 | + resource: wgpu::BindingResource::TextureView(&view), |
| 146 | + }, |
| 147 | + wgpu::BindGroupEntry { |
| 148 | + binding: 1, |
| 149 | + resource: wgpu::BindingResource::Sampler(&self.sampler), |
| 150 | + }, |
| 151 | + ], |
| 152 | + }); |
| 153 | + cache = Some(BlitCache { source_texture: texture, bind_group, size }); |
| 154 | + } |
| 155 | + |
| 156 | + let c = cache.as_ref().unwrap(); |
| 157 | + |
| 158 | + // Copy source → persistent intermediate texture (same format, no bind group creation) |
| 159 | + encoder.copy_texture_to_texture( |
| 160 | + wgpu::TexelCopyTextureInfoBase { |
| 161 | + texture: source, |
| 162 | + mip_level: 0, |
| 163 | + origin: Default::default(), |
| 164 | + aspect: Default::default(), |
| 165 | + }, |
| 166 | + wgpu::TexelCopyTextureInfoBase { |
| 167 | + texture: &c.source_texture, |
| 168 | + mip_level: 0, |
| 169 | + origin: Default::default(), |
| 170 | + aspect: Default::default(), |
| 171 | + }, |
| 172 | + size, |
| 173 | + ); |
| 174 | + |
| 175 | + // Blit intermediate → target with format conversion using the cached bind group |
| 176 | + { |
| 177 | + let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { |
| 178 | + label: Some("CachedBlitter::pass"), |
| 179 | + color_attachments: &[Some(wgpu::RenderPassColorAttachment { |
| 180 | + view: target, |
| 181 | + depth_slice: None, |
| 182 | + resolve_target: None, |
| 183 | + ops: wgpu::Operations { |
| 184 | + load: wgpu::LoadOp::Load, |
| 185 | + store: wgpu::StoreOp::Store, |
| 186 | + }, |
| 187 | + })], |
| 188 | + depth_stencil_attachment: None, |
| 189 | + timestamp_writes: None, |
| 190 | + occlusion_query_set: None, |
| 191 | + }); |
| 192 | + pass.set_pipeline(&self.pipeline); |
| 193 | + pass.set_bind_group(0, &c.bind_group, &[]); |
| 194 | + pass.draw(0..3, 0..1); |
| 195 | + } |
| 196 | + |
| 197 | + // Put cache back for next frame |
| 198 | + *self.cache.lock().unwrap() = cache; |
| 199 | + } |
| 200 | +} |
0 commit comments