use core::ops::Range;
use bytemuck::{Pod, Zeroable};
use crate::gpu::Target;
use crate::math::UVec2;
use crate::post_effect::{self, Declaration, EffectStage, PostEffectId};
use crate::renderer::post::{HDR_FORMAT, Post};
use crate::{Camera, Error, Lens};
const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::R32Float;
const DEPTH_SEAM: &str = "// mirage-engine:depth";
const DEPTH_ONCE: &str = "@group(2) @binding(0) var scene: texture_depth_2d;";
const DEPTH_OVER_SAMPLES: &str = "@group(2) @binding(0) var scene: texture_depth_multisampled_2d;";
const FULLSCREEN: Range<u32> = 0..3;
const SMALLEST_VALUES: wgpu::BufferAddress = 16;
const FORESHORTENED: u32 = 1;
pub(crate) struct PostPasses {
compiled: Vec<Compiled>,
running: Vec<bool>,
built: Option<Built>,
targets: Option<Targets>,
}
struct Built {
bindings: Bindings,
resolve: wgpu::RenderPipeline,
frame: wgpu::Buffer,
display_format: wgpu::TextureFormat,
stages: [bool; EffectStage::ALL.len()],
}
impl PostPasses {
pub(crate) async fn compile(
device: &wgpu::Device,
declared: Vec<Declaration>,
samples: u32,
display_format: wgpu::TextureFormat,
) -> Result<Self, Error> {
if declared.is_empty() {
return Ok(Self {
compiled: Vec::new(),
running: Vec::new(),
built: None,
targets: None,
});
}
let bindings = Bindings::new(device, samples);
let frame = buffer(
device,
"mirage-engine effect frame",
size_of::<Reach>() as wgpu::BufferAddress,
wgpu::BufferUsages::UNIFORM,
);
let resolve = resolving(device, &bindings, samples);
let mut compiled = Vec::with_capacity(declared.len());
let mut broken = Vec::new();
for declaration in declared {
let name = declaration.name;
let scope = device.push_error_scope(wgpu::ErrorFilter::Validation);
let effect = Compiled::new(device, &bindings, declaration, samples, display_format);
match scope.pop().await {
Some(error) => broken.push(format!("{name}: {error}")),
None => compiled.push(effect),
}
}
if !broken.is_empty() {
return Err(Error::msg(format!(
"a post effect did not compile: {}",
broken.join("; ")
)));
}
let stages =
EffectStage::ALL.map(|stage| compiled.iter().any(|effect| effect.stage == stage));
Ok(Self {
running: vec![false; compiled.len()],
compiled,
built: Some(Built {
bindings,
resolve,
frame,
display_format,
stages,
}),
targets: None,
})
}
pub(crate) fn prepare(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
size: UVec2,
camera: Camera,
post: &Post,
) {
let Some(built) = &self.built else {
return;
};
let Some(drawn) = post.drawn() else {
return;
};
let Some(depth) = post.depth() else {
return;
};
match &self.targets {
Some(targets) if targets.size == size => {}
_ => {
self.targets = Some(Targets::new(
device,
&built.bindings,
built.stages,
Frame {
size,
display_format: built.display_format,
},
Read {
drawn,
depth,
frame: &built.frame,
post,
},
));
}
}
let projection = camera.projection();
queue.write_buffer(
&built.frame,
0,
bytemuck::bytes_of(&Reach {
size: [size.x as f32, size.y as f32],
near: projection.near(),
far: projection.far(),
perspective: match projection.lens() {
Lens::Perspective { .. } => FORESHORTENED,
Lens::Orthographic { .. } => 0,
},
_padding: [0; 3],
}),
);
}
pub(crate) fn submit<'a>(
&mut self,
queue: &wgpu::Queue,
submitted: impl Fn(PostEffectId) -> Option<&'a [u8]>,
) {
for (at, effect) in self.compiled.iter().enumerate() {
let values = submitted(PostEffectId(at as u32));
self.running[at] = values.is_some();
if let Some(values) = values.filter(|values| !values.is_empty()) {
queue.write_buffer(&effect.values, 0, values);
}
}
}
fn any(&self) -> bool {
self.running.iter().any(|running| *running)
}
fn running(&self, stage: EffectStage) -> impl Iterator<Item = &Compiled> {
self.compiled
.iter()
.zip(&self.running)
.filter(move |(effect, running)| **running && effect.stage == stage)
.map(|(effect, _)| effect)
}
pub(crate) fn resolve(&self, encoder: &mut wgpu::CommandEncoder) {
let (Some(built), Some(targets)) = (&self.built, &self.targets) else {
return;
};
if !self.any() {
return;
}
let mut pass = pass(
encoder,
"mirage-engine scene depth",
&targets.depth,
wgpu::LoadOp::Clear(wgpu::Color::BLACK),
);
pass.set_pipeline(&built.resolve);
pass.set_bind_group(0, &targets.nothing, &[]);
pass.set_bind_group(1, &targets.nothing, &[]);
pass.set_bind_group(2, &targets.scene, &[]);
pass.draw(FULLSCREEN, 0..1);
}
pub(crate) fn scene<'a>(
&'a self,
encoder: &mut wgpu::CommandEncoder,
) -> Option<&'a wgpu::BindGroup> {
let targets = self.targets.as_ref()?;
let panes = targets.lit.as_ref()?;
let mut ran = 0;
for effect in self.running(EffectStage::Lit) {
let source = match ran {
0 => &targets.drawn,
_ => &panes[(ran - 1) % 2].read,
};
effect.draw(encoder, source, &targets.scene, &panes[ran % 2].written);
ran += 1;
}
(ran > 0).then(|| &panes[(ran - 1) % 2].composited)
}
fn writes(&self) -> usize {
1 + self.running(EffectStage::ToneMapped).count()
+ self.running(EffectStage::OverUi).count()
}
fn written<'a>(
&'a self,
at: usize,
encoding: bool,
target: &'a Target,
) -> &'a wgpu::TextureView {
let surface = match encoding {
true => target.color(),
false => target.encoded(),
};
if at + 1 == self.writes() {
return surface;
}
match self
.targets
.as_ref()
.and_then(|targets| targets.shown.as_ref())
{
Some(panes) => panes[at % 2].written(encoding),
None => surface,
}
}
pub(crate) fn tone_mapped<'a>(&'a self, target: &'a Target) -> &'a wgpu::TextureView {
self.written(0, true, target)
}
pub(crate) fn overlaid<'a>(&'a self, target: &'a Target) -> &'a wgpu::TextureView {
self.written(self.running(EffectStage::ToneMapped).count(), false, target)
}
pub(crate) fn run_tone_mapped(&self, encoder: &mut wgpu::CommandEncoder, target: &Target) {
self.run(encoder, EffectStage::ToneMapped, 1, target);
}
pub(crate) fn run_over_ui(&self, encoder: &mut wgpu::CommandEncoder, target: &Target) {
let after = 1 + self.running(EffectStage::ToneMapped).count();
self.run(encoder, EffectStage::OverUi, after, target);
}
fn run(
&self,
encoder: &mut wgpu::CommandEncoder,
stage: EffectStage,
first: usize,
target: &Target,
) {
let Some(targets) = &self.targets else {
return;
};
let Some(panes) = targets.shown.as_ref() else {
return;
};
for (offset, effect) in self.running(stage).enumerate() {
let at = first + offset;
effect.draw(
encoder,
&panes[(at - 1) % 2].read,
&targets.scene,
self.written(at, false, target),
);
}
}
}
struct Compiled {
stage: EffectStage,
pipeline: wgpu::RenderPipeline,
values: wgpu::Buffer,
bound: wgpu::BindGroup,
}
impl Compiled {
fn new(
device: &wgpu::Device,
bindings: &Bindings,
declaration: Declaration,
samples: u32,
display_format: wgpu::TextureFormat,
) -> Self {
let label = Some(declaration.name);
let shaders = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label,
source: wgpu::ShaderSource::Wgsl(
declaration
.source
.replace(DEPTH_SEAM, sampled_depth(samples))
.into(),
),
});
let values = buffer(
device,
"mirage-engine effect",
SMALLEST_VALUES,
wgpu::BufferUsages::UNIFORM,
);
let bound = device.create_bind_group(&wgpu::BindGroupDescriptor {
label,
layout: &bindings.values,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: values.as_entire_binding(),
}],
});
let format = match declaration.stage.after_tone_map() {
true => display_format.remove_srgb_suffix(),
false => HDR_FORMAT,
};
Self {
stage: declaration.stage,
pipeline: pipeline(
device,
label,
&shaders,
&bindings.effect,
"effect_fragment",
format,
),
values,
bound,
}
}
fn draw(
&self,
encoder: &mut wgpu::CommandEncoder,
source: &wgpu::BindGroup,
scene: &wgpu::BindGroup,
into: &wgpu::TextureView,
) {
let mut pass = pass(encoder, "mirage-engine effect", into, wgpu::LoadOp::Load);
pass.set_pipeline(&self.pipeline);
pass.set_bind_group(0, &self.bound, &[]);
pass.set_bind_group(1, source, &[]);
pass.set_bind_group(2, scene, &[]);
pass.draw(FULLSCREEN, 0..1);
}
}
struct Bindings {
sampler: wgpu::Sampler,
values: wgpu::BindGroupLayout,
source: wgpu::BindGroupLayout,
scene: wgpu::BindGroupLayout,
nothing: wgpu::BindGroupLayout,
effect: wgpu::PipelineLayout,
depth: wgpu::PipelineLayout,
}
impl Bindings {
fn new(device: &wgpu::Device, samples: u32) -> Self {
let values = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("mirage-engine effect values"),
entries: &[uniform(0)],
});
let source = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("mirage-engine effect source"),
entries: &[sampled(0, true), sampler(1), sampled(2, false)],
});
let scene = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("mirage-engine scene depth"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Depth,
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: samples > 1,
},
count: None,
},
uniform(1),
],
});
let nothing = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("mirage-engine effect nothing"),
entries: &[],
});
Self {
effect: layout(
device,
"mirage-engine effect",
&[Some(&values), Some(&source), Some(&scene)],
),
depth: layout(
device,
"mirage-engine scene depth",
&[Some(¬hing), Some(¬hing), Some(&scene)],
),
sampler: device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("mirage-engine effect"),
address_mode_u: wgpu::AddressMode::ClampToEdge,
address_mode_v: wgpu::AddressMode::ClampToEdge,
address_mode_w: wgpu::AddressMode::ClampToEdge,
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Linear,
..Default::default()
}),
values,
source,
scene,
nothing,
}
}
fn source(
&self,
device: &wgpu::Device,
view: &wgpu::TextureView,
depth: &wgpu::TextureView,
) -> wgpu::BindGroup {
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("mirage-engine effect source"),
layout: &self.source,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&self.sampler),
},
wgpu::BindGroupEntry {
binding: 2,
resource: wgpu::BindingResource::TextureView(depth),
},
],
})
}
}
struct Frame {
size: UVec2,
display_format: wgpu::TextureFormat,
}
struct Read<'a> {
drawn: &'a wgpu::TextureView,
depth: &'a wgpu::TextureView,
frame: &'a wgpu::Buffer,
post: &'a Post,
}
struct Targets {
size: UVec2,
lit: Option<[Pane; 2]>,
shown: Option<[Pane; 2]>,
depth: wgpu::TextureView,
drawn: wgpu::BindGroup,
scene: wgpu::BindGroup,
nothing: wgpu::BindGroup,
}
impl Targets {
fn new(
device: &wgpu::Device,
bindings: &Bindings,
stages: [bool; EffectStage::ALL.len()],
frame: Frame,
read: Read<'_>,
) -> Self {
let depth = texture(
device,
"mirage-engine scene depth",
frame.size,
DEPTH_FORMAT,
&[],
)
.create_view(&Default::default());
let pair = |format: wgpu::TextureFormat| {
[(); 2].map(|()| Pane::new(device, bindings, read.post, format, frame.size, &depth))
};
Self {
size: frame.size,
lit: stages[0].then(|| pair(HDR_FORMAT)),
shown: (stages[1] || stages[2]).then(|| pair(frame.display_format)),
drawn: bindings.source(device, read.drawn, &depth),
scene: device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("mirage-engine scene depth"),
layout: &bindings.scene,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(read.depth),
},
wgpu::BindGroupEntry {
binding: 1,
resource: read.frame.as_entire_binding(),
},
],
}),
nothing: device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("mirage-engine effect nothing"),
layout: &bindings.nothing,
entries: &[],
}),
depth,
}
}
}
struct Pane {
written: wgpu::TextureView,
encoding: Option<wgpu::TextureView>,
read: wgpu::BindGroup,
composited: wgpu::BindGroup,
}
impl Pane {
fn new(
device: &wgpu::Device,
bindings: &Bindings,
post: &Post,
format: wgpu::TextureFormat,
size: UVec2,
depth: &wgpu::TextureView,
) -> Self {
let raw = format.remove_srgb_suffix();
let texture = texture(device, "mirage-engine effect", size, format, &[raw]);
let written = texture.create_view(&wgpu::TextureViewDescriptor {
format: Some(raw),
..Default::default()
});
Self {
read: bindings.source(device, &written, depth),
composited: post.source(device, &written),
encoding: (raw != format).then(|| {
texture.create_view(&wgpu::TextureViewDescriptor {
format: Some(format),
..Default::default()
})
}),
written,
}
}
fn written(&self, encoding: bool) -> &wgpu::TextureView {
match (encoding, &self.encoding) {
(true, Some(view)) => view,
_ => &self.written,
}
}
}
#[repr(C)]
#[derive(Clone, Copy, Debug, Pod, Zeroable)]
struct Reach {
size: [f32; 2],
near: f32,
far: f32,
perspective: u32,
_padding: [u32; 3],
}
fn sampled_depth(samples: u32) -> &'static str {
match samples > 1 {
true => DEPTH_OVER_SAMPLES,
false => DEPTH_ONCE,
}
}
fn resolving(device: &wgpu::Device, bindings: &Bindings, samples: u32) -> wgpu::RenderPipeline {
let shaders = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("mirage-engine scene depth"),
source: wgpu::ShaderSource::Wgsl(
post_effect::unpainted()
.replace(DEPTH_SEAM, sampled_depth(samples))
.into(),
),
});
pipeline(
device,
Some("mirage-engine scene depth"),
&shaders,
&bindings.depth,
"resolve",
DEPTH_FORMAT,
)
}
fn pipeline(
device: &wgpu::Device,
label: Option<&str>,
shaders: &wgpu::ShaderModule,
layout: &wgpu::PipelineLayout,
entry: &str,
format: wgpu::TextureFormat,
) -> wgpu::RenderPipeline {
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label,
layout: Some(layout),
vertex: wgpu::VertexState {
module: shaders,
entry_point: Some("fullscreen"),
compilation_options: wgpu::PipelineCompilationOptions::default(),
buffers: &[],
},
primitive: wgpu::PrimitiveState::default(),
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
fragment: Some(wgpu::FragmentState {
module: shaders,
entry_point: Some(entry),
compilation_options: wgpu::PipelineCompilationOptions::default(),
targets: &[Some(wgpu::ColorTargetState {
format,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
})],
}),
multiview_mask: None,
cache: None,
})
}
fn pass<'a>(
encoder: &'a mut wgpu::CommandEncoder,
label: &str,
target: &wgpu::TextureView,
load: wgpu::LoadOp<wgpu::Color>,
) -> wgpu::RenderPass<'a> {
encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some(label),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: target,
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations {
load,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
})
}
fn layout(
device: &wgpu::Device,
label: &str,
groups: &[Option<&wgpu::BindGroupLayout>],
) -> wgpu::PipelineLayout {
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some(label),
bind_group_layouts: groups,
immediate_size: 0,
})
}
fn texture(
device: &wgpu::Device,
label: &str,
size: UVec2,
format: wgpu::TextureFormat,
views: &[wgpu::TextureFormat],
) -> wgpu::Texture {
device.create_texture(&wgpu::TextureDescriptor {
label: Some(label),
size: wgpu::Extent3d {
width: size.x,
height: size.y,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: views,
})
}
fn buffer(
device: &wgpu::Device,
label: &str,
size: wgpu::BufferAddress,
usage: wgpu::BufferUsages,
) -> wgpu::Buffer {
device.create_buffer(&wgpu::BufferDescriptor {
label: Some(label),
size,
usage: usage | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
})
}
fn sampled(binding: u32, filterable: bool) -> wgpu::BindGroupLayoutEntry {
wgpu::BindGroupLayoutEntry {
binding,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
}
}
fn sampler(binding: u32) -> wgpu::BindGroupLayoutEntry {
wgpu::BindGroupLayoutEntry {
binding,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
}
}
fn uniform(binding: u32) -> wgpu::BindGroupLayoutEntry {
wgpu::BindGroupLayoutEntry {
binding,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
}
}