1use std::collections::HashMap;
2
3use crate::{assets::singleton_asset::LazyResource, wgpu::{backend::WGPUBackend, buffers::BindGroupBuilder}};
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(crate) 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 = BindGroupBuilder::new_raw(layout)
268 .label("mipmap-blit-bind-group")
269 .texture_view_raw_at(0, &src_view)
270 .sampler_raw_at(1, sampler)
271 .build_raw(device);
272
273 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
274 label: Some("mipmap-blit-pass"),
275 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
276 view: &dst_view,
277 depth_slice: None,
278 resolve_target: None,
279 ops: wgpu::Operations {
280 load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
281 store: wgpu::StoreOp::Store,
282 },
283 })],
284 depth_stencil_attachment: None,
285 timestamp_writes: None,
286 occlusion_query_set: None,
287 multiview_mask: None,
288 });
289 pass.set_pipeline(&entry.pipeline);
290 pass.set_bind_group(0, &bind_group, &[]);
291 pass.draw(0..3, 0..1);
292 drop(pass);
293 }
294 }
295
296 queue.submit(std::iter::once(encoder.finish()));
297 }
298}
299
300impl LazyResource<WGPUBackend> for MipmapGenerator {
301 type Deps<'a> = ();
302
303 fn construct<'a>(backend: &WGPUBackend, _deps: &()) -> Option<Self> {
304 Some(Self::new(&backend.device))
305 }
306}
307
308#[cfg(test)]
309mod tests {
310 use super::*;
311
312 #[test]
313 fn mip_count_is_one_when_mips_are_not_requested() {
314 assert_eq!(mip_count(2048, false), 1);
315 assert_eq!(mip_count(1, false), 1);
316 assert_eq!(mip_count(0, false), 1);
317 }
318
319 #[test]
320 fn mip_count_matches_a_full_power_of_two_chain() {
321 assert_eq!(mip_count(1, true), 1);
323 assert_eq!(mip_count(2, true), 2);
324 assert_eq!(mip_count(4, true), 3);
325 assert_eq!(mip_count(256, true), 9);
326 assert_eq!(mip_count(1024, true), 11);
327 }
328
329 #[test]
330 fn mip_count_floors_a_non_power_of_two_dimension() {
331 assert_eq!(mip_count(300, true), 9);
334 }
335
336 #[test]
337 fn mip_count_of_a_zero_dimension_does_not_panic_or_underflow() {
338 assert_eq!(mip_count(0, true), 1);
343 }
344
345 #[test]
346 fn texture_usage_adds_render_attachment_only_when_there_is_more_than_one_mip() {
347 let single = texture_usage(1);
348 assert!(!single.contains(wgpu::TextureUsages::RENDER_ATTACHMENT));
349 assert!(single.contains(wgpu::TextureUsages::TEXTURE_BINDING));
350 assert!(single.contains(wgpu::TextureUsages::COPY_DST));
351
352 let chained = texture_usage(5);
353 assert!(chained.contains(wgpu::TextureUsages::RENDER_ATTACHMENT));
354 assert!(chained.contains(wgpu::TextureUsages::TEXTURE_BINDING));
355 assert!(chained.contains(wgpu::TextureUsages::COPY_DST));
356 }
357}