use bytemuck::{Pod, Zeroable};
use crate::Tonemap;
use crate::gpu::{
FULLSCREEN, buffer, clamped_sampler, depth_texture, pass, pipeline, pipeline_layout, sampled,
sampler, texture, uniform,
};
use crate::math::UVec2;
pub(crate) const HDR_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba16Float;
pub(crate) const DEFAULT_EXPOSURE: f32 = 1.0;
pub(crate) const DEFAULT_BLOOM: f32 = 0.0;
pub(crate) const NOTHING: f32 = 1.0;
const SAMPLES: u32 = 4;
const SMALLEST_MIP: u32 = 8;
const BLANK: wgpu::Color = wgpu::Color::BLACK;
const SPREAD_OVER_TARGET: wgpu::BlendState = wgpu::BlendState {
color: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::SrcAlpha,
dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
operation: wgpu::BlendOperation::Add,
},
alpha: wgpu::BlendComponent::REPLACE,
};
pub(crate) fn sample_count(antialiasing: bool) -> u32 {
if antialiasing { SAMPLES } else { 1 }
}
pub(crate) struct SceneTarget<'a> {
pub(crate) color: &'a wgpu::TextureView,
pub(crate) resolve: Option<&'a wgpu::TextureView>,
pub(crate) depth: &'a wgpu::TextureView,
}
pub(crate) struct Post {
bindings: Bindings,
downsample: wgpu::RenderPipeline,
upsample: wgpu::RenderPipeline,
display: wgpu::RenderPipeline,
samples: u32,
curve: Tonemap,
settings: Settings,
chain: Option<Chain>,
}
impl Post {
pub(crate) fn new(
device: &wgpu::Device,
display_format: wgpu::TextureFormat,
samples: u32,
curve: Tonemap,
) -> Self {
let bindings = Bindings::new(device);
let shaders = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("mirage-engine post"),
source: wgpu::ShaderSource::Wgsl(include_str!("post.wgsl").into()),
});
let scatter = pipeline_layout(device, "mirage-engine bloom", &[Some(&bindings.source)]);
let display = pipeline_layout(
device,
"mirage-engine tone map",
&[
Some(&bindings.source),
Some(&bindings.over),
Some(&bindings.composite),
],
);
let built = |label, layout, entry, format, blend| {
pipeline(
device,
label,
&shaders,
layout,
("fullscreen", entry),
format,
blend,
)
};
Self {
downsample: built(
"mirage-engine bloom down",
&scatter,
"downsample",
HDR_FORMAT,
None,
),
upsample: built(
"mirage-engine bloom up",
&scatter,
"upsample",
HDR_FORMAT,
Some(SPREAD_OVER_TARGET),
),
display: built(
"mirage-engine tone map",
&display,
"tonemap",
display_format,
None,
),
bindings,
samples,
curve,
settings: Settings::zeroed(),
chain: None,
}
}
pub(crate) fn prepare(&mut self, device: &wgpu::Device, size: UVec2) {
match &self.chain {
Some(chain) if chain.size == size => {}
_ => self.chain = Some(Chain::new(device, &self.bindings, self.samples, size)),
}
}
pub(crate) fn set_frame(&mut self, queue: &wgpu::Queue, frame: &Frame) {
let _ = frame.size;
self.settings = Settings {
exposure: frame.exposure,
bloom: frame.bloom,
curve: self.curve.index(),
_padding: 0,
};
queue.write_buffer(
&self.bindings.settings,
0,
bytemuck::bytes_of(&self.settings),
);
}
pub(crate) fn drawn(&self) -> Option<&wgpu::TextureView> {
Some(&self.chain.as_ref()?.drawn)
}
pub(crate) fn depth(&self) -> Option<&wgpu::TextureView> {
Some(&self.chain.as_ref()?.depth)
}
pub(crate) fn sampled(&self) -> Option<&wgpu::BindGroup> {
Some(&self.chain.as_ref()?.sampled)
}
pub(crate) fn source(
&self,
device: &wgpu::Device,
view: &wgpu::TextureView,
) -> wgpu::BindGroup {
self.bindings.source(device, view)
}
pub(crate) fn scene(&self) -> Option<SceneTarget<'_>> {
let chain = self.chain.as_ref()?;
Some(SceneTarget {
color: &chain.scene,
resolve: chain.resolve.as_ref(),
depth: &chain.depth,
})
}
pub(crate) fn encode(
&self,
encoder: &mut wgpu::CommandEncoder,
source: &wgpu::BindGroup,
display: &wgpu::TextureView,
) {
let Some(chain) = &self.chain else {
return;
};
if self.settings.bloom > 0.0 {
self.scatter(encoder, chain, source);
}
let mut pass = pass(
encoder,
"mirage-engine tone map",
display,
wgpu::LoadOp::Clear(BLANK),
);
pass.set_pipeline(&self.display);
pass.set_bind_group(0, source, &[]);
pass.set_bind_group(1, &chain.over, &[]);
pass.set_bind_group(2, &chain.composite, &[]);
pass.draw(FULLSCREEN, 0..1);
}
fn scatter(&self, encoder: &mut wgpu::CommandEncoder, chain: &Chain, source: &wgpu::BindGroup) {
let sources = core::iter::once(source).chain(chain.bloom.iter().map(|mip| &mip.source));
for (source, mip) in sources.zip(&chain.bloom) {
step(
encoder,
"mirage-engine bloom down",
&self.downsample,
source,
&mip.view,
wgpu::LoadOp::Clear(BLANK),
);
}
for pair in chain.bloom.windows(2).rev() {
let [larger, smaller] = pair else {
continue;
};
step(
encoder,
"mirage-engine bloom up",
&self.upsample,
&smaller.source,
&larger.view,
wgpu::LoadOp::Load,
);
}
}
}
struct Bindings {
sampler: wgpu::Sampler,
source: wgpu::BindGroupLayout,
over: wgpu::BindGroupLayout,
composite: wgpu::BindGroupLayout,
settings: wgpu::Buffer,
}
impl Bindings {
fn new(device: &wgpu::Device) -> Self {
Self {
sampler: clamped_sampler(device, "mirage-engine post"),
source: device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("mirage-engine post source"),
entries: &[sampled(0, true), sampler(1)],
}),
over: device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("mirage-engine post over"),
entries: &[uniform(1, wgpu::ShaderStages::FRAGMENT)],
}),
composite: device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("mirage-engine tone map"),
entries: &[sampled(0, true)],
}),
settings: buffer(
device,
"mirage-engine post settings",
size_of::<Settings>() as wgpu::BufferAddress,
wgpu::BufferUsages::UNIFORM,
),
}
}
fn source(&self, device: &wgpu::Device, view: &wgpu::TextureView) -> wgpu::BindGroup {
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("mirage-engine post source"),
layout: &self.source,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&self.sampler),
},
],
})
}
fn over(&self, device: &wgpu::Device) -> wgpu::BindGroup {
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("mirage-engine post over"),
layout: &self.over,
entries: &[wgpu::BindGroupEntry {
binding: 1,
resource: self.settings.as_entire_binding(),
}],
})
}
fn composite(&self, device: &wgpu::Device, view: &wgpu::TextureView) -> wgpu::BindGroup {
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("mirage-engine tone map"),
layout: &self.composite,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(view),
}],
})
}
}
struct Chain {
size: UVec2,
scene: wgpu::TextureView,
resolve: Option<wgpu::TextureView>,
depth: wgpu::TextureView,
drawn: wgpu::TextureView,
bloom: Vec<Mip>,
sampled: wgpu::BindGroup,
over: wgpu::BindGroup,
composite: wgpu::BindGroup,
}
impl Chain {
fn new(device: &wgpu::Device, bindings: &Bindings, samples: u32, size: UVec2) -> Self {
let scene = hdr_texture(device, size, samples);
let resolved = (samples > 1).then(|| hdr_texture(device, size, 1));
let drawn = view(resolved.as_ref().unwrap_or(&scene));
let depth = view(&depth_texture(device, size, samples));
let bloom: Vec<Mip> = mip_sizes(size)
.into_iter()
.map(|mip| Mip::new(device, bindings, mip))
.collect();
let scattered = bloom.first().map_or(&drawn, |mip| &mip.view);
Self {
size,
composite: bindings.composite(device, scattered),
sampled: bindings.source(device, &drawn),
over: bindings.over(device),
scene: view(&scene),
resolve: resolved.as_ref().map(view),
depth,
drawn,
bloom,
}
}
}
struct Mip {
view: wgpu::TextureView,
source: wgpu::BindGroup,
}
impl Mip {
fn new(device: &wgpu::Device, bindings: &Bindings, size: UVec2) -> Self {
let view = view(&hdr_texture(device, size, 1));
Self {
source: bindings.source(device, &view),
view,
}
}
}
#[repr(C)]
#[derive(Clone, Copy, Debug, Pod, Zeroable)]
struct Settings {
exposure: f32,
bloom: f32,
curve: u32,
_padding: u32,
}
pub(crate) struct Frame {
pub(crate) size: UVec2,
pub(crate) exposure: f32,
pub(crate) bloom: f32,
}
fn mip_sizes(size: UVec2) -> Vec<UVec2> {
let mut sizes = Vec::new();
let mut mip = size / 2;
while mip.min_element() >= SMALLEST_MIP {
sizes.push(mip);
mip /= 2;
}
sizes
}
fn step(
encoder: &mut wgpu::CommandEncoder,
label: &str,
pipeline: &wgpu::RenderPipeline,
source: &wgpu::BindGroup,
target: &wgpu::TextureView,
load: wgpu::LoadOp<wgpu::Color>,
) {
let mut pass = pass(encoder, label, target, load);
pass.set_pipeline(pipeline);
pass.set_bind_group(0, source, &[]);
pass.draw(FULLSCREEN, 0..1);
}
fn hdr_texture(device: &wgpu::Device, size: UVec2, samples: u32) -> wgpu::Texture {
texture(
device,
"mirage-engine scene",
size,
samples,
HDR_FORMAT,
&[],
)
}
fn view(texture: &wgpu::Texture) -> wgpu::TextureView {
texture.create_view(&Default::default())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_bloom_chain_halves_down_to_the_smallest_mip() {
assert_eq!(
mip_sizes(UVec2::new(1280, 720)).last(),
Some(&UVec2::new(20, 11)),
"the last mip is the last one no shorter than the floor"
);
assert!(
mip_sizes(UVec2::new(1280, 720))
.windows(2)
.all(|pair| pair[1] == pair[0] / 2),
"every mip is half the one before it"
);
}
#[test]
fn a_frame_too_small_to_halve_has_no_bloom_chain() {
assert!(mip_sizes(UVec2::splat(2 * SMALLEST_MIP - 1)).is_empty());
assert_eq!(mip_sizes(UVec2::splat(2 * SMALLEST_MIP)).len(), 1);
}
#[test]
fn antialiasing_is_the_count_webgpu_offers_or_one() {
assert_eq!(sample_count(true), 4);
assert_eq!(sample_count(false), 1);
}
#[test]
fn the_settings_stay_the_size_the_shader_reads_them_at() {
assert_eq!(size_of::<Settings>(), 16);
}
}