1use std::collections::HashMap;
2
3use crate::{assets::singleton_asset::LazyResource, wgpu::backend::WGPUBackend};
4
5const MIP_SHADER: &str = r#"
6struct VOut {
7 @builtin(position) pos: vec4<f32>,
8 @location(0) uv: vec2<f32>,
9};
10
11@vertex
12fn vs_main(@builtin(vertex_index) idx: u32) -> VOut {
13 var out: VOut;
14 let x = f32((idx << 1u) & 2u);
15 let y = f32(idx & 2u);
16 out.pos = vec4<f32>(x * 2.0 - 1.0, 1.0 - y * 2.0, 0.0, 1.0);
17 out.uv = vec2<f32>(x, y);
18 return out;
19}
20
21@group(0) @binding(0) var src_texture: texture_2d<f32>;
22@group(0) @binding(1) var src_sampler: sampler;
23
24@fragment
25fn fs_main(in: VOut) -> @location(0) vec4<f32> {
26 return textureSample(src_texture, src_sampler, in.uv);
27}
28"#;
29
30const SUPPORTED_FORMATS: &[wgpu::TextureFormat] = &[
33 wgpu::TextureFormat::Rgba8Unorm,
34 wgpu::TextureFormat::Rgba8UnormSrgb,
35 wgpu::TextureFormat::Rgba16Float,
36 wgpu::TextureFormat::Rgba32Float,
37];
38
39pub(crate) fn mip_count(max_dimension: u32, generate_mips: bool) -> u32 {
45 if generate_mips {
46 (max_dimension as f32).log2().floor() as u32 + 1
47 } else {
48 1
49 }
50}
51
52pub(crate) fn texture_usage(mip_count: u32) -> wgpu::TextureUsages {
57 if mip_count > 1 {
58 wgpu::TextureUsages::TEXTURE_BINDING
59 | wgpu::TextureUsages::COPY_DST
60 | wgpu::TextureUsages::RENDER_ATTACHMENT
61 } else {
62 wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST
63 }
64}
65
66struct MipPipeline {
67 pipeline: wgpu::RenderPipeline,
68 filterable: bool,
69}
70
71pub struct MipmapGenerator {
78 filtering_layout: wgpu::BindGroupLayout,
79 nonfiltering_layout: wgpu::BindGroupLayout,
80 linear_sampler: wgpu::Sampler,
81 nearest_sampler: wgpu::Sampler,
82 pipelines: HashMap<wgpu::TextureFormat, MipPipeline>,
83}
84
85impl MipmapGenerator {
86 fn bind_group_layout(device: &wgpu::Device, filterable: bool) -> wgpu::BindGroupLayout {
87 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
88 label: Some("mipmap-blit-bind-group-layout"),
89 entries: &[
90 wgpu::BindGroupLayoutEntry {
91 binding: 0,
92 visibility: wgpu::ShaderStages::FRAGMENT,
93 ty: wgpu::BindingType::Texture {
94 sample_type: wgpu::TextureSampleType::Float { filterable },
95 view_dimension: wgpu::TextureViewDimension::D2,
96 multisampled: false,
97 },
98 count: None,
99 },
100 wgpu::BindGroupLayoutEntry {
101 binding: 1,
102 visibility: wgpu::ShaderStages::FRAGMENT,
103 ty: wgpu::BindingType::Sampler(if filterable {
104 wgpu::SamplerBindingType::Filtering
105 } else {
106 wgpu::SamplerBindingType::NonFiltering
107 }),
108 count: None,
109 },
110 ],
111 })
112 }
113
114 fn build_pipeline(
115 device: &wgpu::Device,
116 module: &wgpu::ShaderModule,
117 layout: &wgpu::BindGroupLayout,
118 format: wgpu::TextureFormat,
119 ) -> wgpu::RenderPipeline {
120 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
121 label: Some("mipmap-blit-pipeline-layout"),
122 bind_group_layouts: &[Some(layout)],
123 immediate_size: 0,
124 });
125
126 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
127 label: Some("mipmap-blit-pipeline"),
128 layout: Some(&pipeline_layout),
129 vertex: wgpu::VertexState {
130 module,
131 entry_point: Some("vs_main"),
132 compilation_options: Default::default(),
133 buffers: &[],
134 },
135 primitive: wgpu::PrimitiveState {
136 topology: wgpu::PrimitiveTopology::TriangleList,
137 strip_index_format: None,
138 front_face: wgpu::FrontFace::Ccw,
139 cull_mode: None,
140 unclipped_depth: false,
141 polygon_mode: wgpu::PolygonMode::Fill,
142 conservative: false,
143 },
144 depth_stencil: None,
145 multisample: wgpu::MultisampleState::default(),
146 fragment: Some(wgpu::FragmentState {
147 module,
148 entry_point: Some("fs_main"),
149 compilation_options: Default::default(),
150 targets: &[Some(wgpu::ColorTargetState {
151 format,
152 blend: None,
153 write_mask: wgpu::ColorWrites::ALL,
154 })],
155 }),
156 multiview_mask: None,
157 cache: None,
158 })
159 }
160
161 fn new(device: &wgpu::Device) -> Self {
162 let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
163 label: Some("mipmap-blit-shader"),
164 source: wgpu::ShaderSource::Wgsl(MIP_SHADER.into()),
165 });
166
167 let filtering_layout = Self::bind_group_layout(device, true);
168 let nonfiltering_layout = Self::bind_group_layout(device, false);
169
170 let linear_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
171 label: Some("mipmap-linear-sampler"),
172 mag_filter: wgpu::FilterMode::Linear,
173 min_filter: wgpu::FilterMode::Linear,
174 ..Default::default()
175 });
176 let nearest_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
177 label: Some("mipmap-nearest-sampler"),
178 mag_filter: wgpu::FilterMode::Nearest,
179 min_filter: wgpu::FilterMode::Nearest,
180 ..Default::default()
181 });
182
183 let mut pipelines = HashMap::new();
184 for &format in SUPPORTED_FORMATS {
185 let filterable = format != wgpu::TextureFormat::Rgba32Float;
188 let layout = if filterable {
189 &filtering_layout
190 } else {
191 &nonfiltering_layout
192 };
193 let pipeline = Self::build_pipeline(device, &module, layout, format);
194 pipelines.insert(format, MipPipeline { pipeline, filterable });
195 }
196
197 Self {
198 filtering_layout,
199 nonfiltering_layout,
200 linear_sampler,
201 nearest_sampler,
202 pipelines,
203 }
204 }
205
206 pub fn generate_mips(
212 &self,
213 device: &wgpu::Device,
214 queue: &wgpu::Queue,
215 texture: &wgpu::Texture,
216 format: wgpu::TextureFormat,
217 mip_count: u32,
218 layer_count: u32,
219 ) {
220 if mip_count <= 1 {
221 return;
222 }
223
224 let Some(entry) = self.pipelines.get(&format) else {
225 tracing::error!("MipmapGenerator: no blit pipeline for format {format:?}, skipping mip generation");
226 return;
227 };
228 let layout = if entry.filterable {
229 &self.filtering_layout
230 } else {
231 &self.nonfiltering_layout
232 };
233 let sampler = if entry.filterable {
234 &self.linear_sampler
235 } else {
236 &self.nearest_sampler
237 };
238
239 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
240 label: Some("mipmap-blit-encoder"),
241 });
242
243 for layer in 0..layer_count {
244 for level in 1..mip_count {
245 let src_view = texture.create_view(&wgpu::TextureViewDescriptor {
246 dimension: Some(wgpu::TextureViewDimension::D2),
247 base_mip_level: level - 1,
248 mip_level_count: Some(1),
249 base_array_layer: layer,
250 array_layer_count: Some(1),
251 ..Default::default()
252 });
253 let dst_view = texture.create_view(&wgpu::TextureViewDescriptor {
254 dimension: Some(wgpu::TextureViewDimension::D2),
255 base_mip_level: level,
256 mip_level_count: Some(1),
257 base_array_layer: layer,
258 array_layer_count: Some(1),
259 ..Default::default()
260 });
261
262 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
263 label: Some("mipmap-blit-bind-group"),
264 layout,
265 entries: &[
266 wgpu::BindGroupEntry {
267 binding: 0,
268 resource: wgpu::BindingResource::TextureView(&src_view),
269 },
270 wgpu::BindGroupEntry {
271 binding: 1,
272 resource: wgpu::BindingResource::Sampler(sampler),
273 },
274 ],
275 });
276
277 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
278 label: Some("mipmap-blit-pass"),
279 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
280 view: &dst_view,
281 depth_slice: None,
282 resolve_target: None,
283 ops: wgpu::Operations {
284 load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
285 store: wgpu::StoreOp::Store,
286 },
287 })],
288 depth_stencil_attachment: None,
289 timestamp_writes: None,
290 occlusion_query_set: None,
291 multiview_mask: None,
292 });
293 pass.set_pipeline(&entry.pipeline);
294 pass.set_bind_group(0, &bind_group, &[]);
295 pass.draw(0..3, 0..1);
296 drop(pass);
297 }
298 }
299
300 queue.submit(std::iter::once(encoder.finish()));
301 }
302}
303
304impl LazyResource<WGPUBackend> for MipmapGenerator {
305 type Deps<'a> = ();
306
307 fn construct<'a>(backend: &WGPUBackend, _deps: &()) -> Option<Self> {
308 Some(Self::new(&backend.device))
309 }
310}
311
312#[cfg(test)]
313mod tests {
314 use super::*;
315
316 #[test]
317 fn mip_count_is_one_when_mips_are_not_requested() {
318 assert_eq!(mip_count(2048, false), 1);
319 assert_eq!(mip_count(1, false), 1);
320 assert_eq!(mip_count(0, false), 1);
321 }
322
323 #[test]
324 fn mip_count_matches_a_full_power_of_two_chain() {
325 assert_eq!(mip_count(1, true), 1);
327 assert_eq!(mip_count(2, true), 2);
328 assert_eq!(mip_count(4, true), 3);
329 assert_eq!(mip_count(256, true), 9);
330 assert_eq!(mip_count(1024, true), 11);
331 }
332
333 #[test]
334 fn mip_count_floors_a_non_power_of_two_dimension() {
335 assert_eq!(mip_count(300, true), 9);
338 }
339
340 #[test]
341 fn mip_count_of_a_zero_dimension_does_not_panic_or_underflow() {
342 assert_eq!(mip_count(0, true), 1);
347 }
348
349 #[test]
350 fn texture_usage_adds_render_attachment_only_when_there_is_more_than_one_mip() {
351 let single = texture_usage(1);
352 assert!(!single.contains(wgpu::TextureUsages::RENDER_ATTACHMENT));
353 assert!(single.contains(wgpu::TextureUsages::TEXTURE_BINDING));
354 assert!(single.contains(wgpu::TextureUsages::COPY_DST));
355
356 let chained = texture_usage(5);
357 assert!(chained.contains(wgpu::TextureUsages::RENDER_ATTACHMENT));
358 assert!(chained.contains(wgpu::TextureUsages::TEXTURE_BINDING));
359 assert!(chained.contains(wgpu::TextureUsages::COPY_DST));
360 }
361}