pub(super) const SHADER: &str = r"
struct Out {
@builtin(position) clip: vec4<f32>,
@location(0) uv: vec2<f32>,
};
@vertex
fn vs(@builtin(vertex_index) i: u32) -> Out {
let uv = vec2<f32>(f32((i << 1u) & 2u), f32(i & 2u));
var out: Out;
out.clip = vec4<f32>(uv * vec2<f32>(2.0, -2.0) + vec2<f32>(-1.0, 1.0), 0.0, 1.0);
out.uv = uv;
return out;
}
@group(0) @binding(0) var source: texture_2d<f32>;
@group(0) @binding(1) var linear_filter: sampler;
@fragment
fn fs(in: Out) -> @location(0) vec4<f32> {
return textureSampleLevel(source, linear_filter, in.uv, 0.0);
}
";
pub(super) fn levels(width: u32, height: u32) -> u32 {
32 - width.max(height).max(1).leading_zeros()
}
pub(super) struct MipMaker {
layout: wgpu::BindGroupLayout,
srgb: wgpu::RenderPipeline,
linear: wgpu::RenderPipeline,
sampler: wgpu::Sampler,
}
impl MipMaker {
pub fn new(device: &wgpu::Device) -> Self {
let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("mipmaps"),
source: wgpu::ShaderSource::Wgsl(SHADER.into()),
});
let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("mipmaps"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
],
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("mipmaps"),
bind_group_layouts: &[Some(&layout)],
immediate_size: 0,
});
let pipeline = |format: wgpu::TextureFormat| {
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("mipmaps"),
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &module,
entry_point: Some("vs"),
compilation_options: Default::default(),
buffers: &[],
},
fragment: Some(wgpu::FragmentState {
module: &module,
entry_point: Some("fs"),
compilation_options: Default::default(),
targets: &[Some(format.into())],
}),
primitive: Default::default(),
depth_stencil: None,
multisample: Default::default(),
multiview_mask: None,
cache: None,
})
};
Self {
srgb: pipeline(wgpu::TextureFormat::Rgba8UnormSrgb),
linear: pipeline(wgpu::TextureFormat::Rgba8Unorm),
layout,
sampler: device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("mipmaps"),
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Linear,
..Default::default()
}),
}
}
pub fn generate(
&self,
device: &wgpu::Device,
encoder: &mut wgpu::CommandEncoder,
texture: &wgpu::Texture,
linear: bool,
) {
let format = if linear {
wgpu::TextureFormat::Rgba8Unorm
} else {
wgpu::TextureFormat::Rgba8UnormSrgb
};
let view = |level, layer| {
texture.create_view(&wgpu::TextureViewDescriptor {
label: Some("mip level"),
format: Some(format),
dimension: Some(wgpu::TextureViewDimension::D2),
base_mip_level: level,
mip_level_count: Some(1),
base_array_layer: layer,
array_layer_count: Some(1),
..Default::default()
})
};
let layers = texture.depth_or_array_layers();
for (layer, level) in (0..layers).flat_map(|l| (1..texture.mip_level_count()).map(move |m| (l, m))) {
let source = view(level - 1, layer);
let target = view(level, layer);
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("mipmaps"),
layout: &self.layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&source),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&self.sampler),
},
],
});
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("mipmaps"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &target,
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
pass.set_pipeline(if linear { &self.linear } else { &self.srgb });
pass.set_bind_group(0, &bind_group, &[]);
pass.draw(0..3, 0..1);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn levels_go_down_to_one_texel() {
assert_eq!(levels(1, 1), 1);
assert_eq!(levels(2, 2), 2);
assert_eq!(levels(256, 256), 9);
assert_eq!(levels(300, 7), 9);
assert_eq!(levels(1024, 2048), 12);
}
}