Skip to content

Commit 4e39e06

Browse files
committed
refactor CachedBlitter into its own module with separate shader file
1 parent 8d0bc3d commit 4e39e06

3 files changed

Lines changed: 228 additions & 226 deletions

File tree

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
struct VertexOutput {
2+
@builtin(position) position: vec4<f32>,
3+
@location(0) tex_coords: vec2<f32>,
4+
}
5+
6+
@vertex
7+
fn vs_main(@builtin(vertex_index) vi: u32) -> VertexOutput {
8+
var out: VertexOutput;
9+
out.tex_coords = vec2<f32>(
10+
f32((vi << 1u) & 2u),
11+
f32(vi & 2u),
12+
);
13+
out.position = vec4<f32>(out.tex_coords * 2.0 - 1.0, 0.0, 1.0);
14+
out.tex_coords.y = 1.0 - out.tex_coords.y;
15+
return out;
16+
}
17+
18+
@group(0) @binding(0)
19+
var src_texture: texture_2d<f32>;
20+
@group(0) @binding(1)
21+
var src_sampler: sampler;
22+
23+
@fragment
24+
fn fs_main(vs: VertexOutput) -> @location(0) vec4<f32> {
25+
return textureSample(src_texture, src_sampler, vs.tex_coords);
26+
}
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
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

Comments
 (0)