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