use std::collections::HashMap;
use std::rc::Rc;
use bytemuck::{Pod, Zeroable};
use crate::Assets;
use crate::assets::Unresolved;
use crate::math::Vec4;
use crate::renderer::pipelines::FrameBindings;
use crate::skybox::{Gradient, Resident, SkyboxError, Skyboxes};
const FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba16Float;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) struct SkyId(u32);
pub(crate) struct Skies<S: Skyboxes> {
assets: Rc<Assets>,
ids: HashMap<S, Option<SkyId>>,
named: Vec<GpuSky>,
default: GpuSky,
sampler: wgpu::Sampler,
}
impl<S: Skyboxes> Skies<S> {
pub(crate) fn new(
device: &wgpu::Device,
queue: &wgpu::Queue,
assets: Rc<Assets>,
frame: &FrameBindings,
) -> Self {
let sampler = sampler(device);
Self {
default: built(
device,
queue,
frame,
&sampler,
&Resident::gradient(Gradient::default()),
),
assets,
ids: HashMap::new(),
named: Vec::new(),
sampler,
}
}
pub(crate) fn id(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
frame: &FrameBindings,
sky: &S,
) -> Option<SkyId> {
match self.built(device, queue, frame, sky) {
Ok(id) => id,
Err(error) => {
log::debug!("the skybox {sky:?} {error}");
None
}
}
}
pub(crate) fn build_catalog(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
frame: &FrameBindings,
) {
for sky in S::catalog() {
if let Err(error) = self.built(device, queue, frame, &sky) {
self.assets.record(Unresolved::Skybox {
skybox: format!("{sky:?}"),
error,
});
}
}
}
pub(crate) fn rebind(&mut self, device: &wgpu::Device, frame: &FrameBindings) {
for sky in core::iter::once(&mut self.default).chain(&mut self.named) {
sky.frame = frame.bind(device, &sky.view, &self.sampler);
}
}
pub(crate) fn drawing(&self, id: Option<SkyId>) -> &GpuSky {
id.and_then(|id| self.named.get(id.0 as usize))
.unwrap_or(&self.default)
}
fn built(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
frame: &FrameBindings,
sky: &S,
) -> Result<Option<SkyId>, SkyboxError> {
if let Some(&id) = self.ids.get(sky) {
return Ok(id);
}
let resident = sky.build(&self.assets).resident();
let id = resident.as_ref().ok().map(|resident| {
let id = SkyId(self.named.len() as u32);
self.named
.push(built(device, queue, frame, &self.sampler, resident));
id
});
self.ids.insert(sky.clone(), id);
resident.map(|_| id)
}
}
pub(crate) struct GpuSky {
frame: wgpu::BindGroup,
view: wgpu::TextureView,
lighting: Lighting,
}
impl GpuSky {
pub(crate) fn frame(&self) -> &wgpu::BindGroup {
&self.frame
}
pub(crate) fn lighting(&self) -> Lighting {
self.lighting
}
}
#[derive(Clone, Copy)]
pub(crate) struct Lighting {
pub(crate) irradiance: [Vec4; 9],
pub(crate) top_mip: f32,
pub(crate) share: f32,
}
#[repr(transparent)]
#[derive(Clone, Copy, Debug, Pod, Zeroable)]
struct Half(u16);
impl Half {
const LARGEST: f32 = 65_504.0;
const SMALLEST: f32 = 6.103_515_6e-5;
const BIAS: u32 = 127 - 15;
fn of(light: f32) -> Self {
let held = light.clamp(0.0, Self::LARGEST);
if held < Self::SMALLEST || held.is_nan() {
return Self(0);
}
let bits = held.to_bits();
let exponent = ((bits >> 23) & 0xff) - Self::BIAS;
let mantissa = (bits & 0x007f_ffff) >> 13;
let rounds = bits & 0x0000_1000 != 0;
Self(((exponent << 10 | mantissa) + u32::from(rounds)) as u16)
}
}
fn built(
device: &wgpu::Device,
queue: &wgpu::Queue,
frame: &FrameBindings,
sampler: &wgpu::Sampler,
resident: &Resident,
) -> GpuSky {
let view = uploaded(device, queue, resident).create_view(&Default::default());
GpuSky {
frame: frame.bind(device, &view, sampler),
view,
lighting: Lighting {
irradiance: resident.irradiance().lanes(),
top_mip: resident.top_mip(),
share: resident.share(),
},
}
}
fn uploaded(device: &wgpu::Device, queue: &wgpu::Queue, resident: &Resident) -> wgpu::Texture {
let size = resident.size();
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("mirage-engine skybox"),
size: wgpu::Extent3d {
width: size.x,
height: size.y,
depth_or_array_layers: 1,
},
mip_level_count: resident.mip_count(),
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: FORMAT,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
for (level, mip) in resident.mips().enumerate() {
let texels: Vec<Half> = mip
.texels()
.iter()
.flat_map(|texel| [texel.x, texel.y, texel.z, 1.0].map(Half::of))
.collect();
let size = mip.size();
queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture: &texture,
mip_level: level as u32,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
bytemuck::cast_slice(&texels),
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(size_of::<[Half; 4]>() as u32 * size.x),
rows_per_image: Some(size.y),
},
wgpu::Extent3d {
width: size.x,
height: size.y,
depth_or_array_layers: 1,
},
);
}
texture
}
fn sampler(device: &wgpu::Device) -> wgpu::Sampler {
device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("mirage-engine skybox"),
address_mode_u: wgpu::AddressMode::Repeat,
address_mode_v: wgpu::AddressMode::ClampToEdge,
address_mode_w: wgpu::AddressMode::ClampToEdge,
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Linear,
mipmap_filter: wgpu::MipmapFilterMode::Linear,
..Default::default()
})
}
pub(crate) fn sampled(binding: u32) -> wgpu::BindGroupLayoutEntry {
wgpu::BindGroupLayoutEntry {
binding,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
}
}
pub(crate) fn sampling(binding: u32) -> wgpu::BindGroupLayoutEntry {
wgpu::BindGroupLayoutEntry {
binding,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn light_keeps_the_steps_of_the_number_the_gpu_reads() {
let held = |light: f32| Half::of(light).0;
assert_eq!(held(0.0), 0x0000);
assert_eq!(held(1.0), 0x3c00);
assert_eq!(held(0.5), 0x3800);
assert_eq!(held(0.1), 0x2e66);
assert_eq!(held(2048.0), 0x6800);
assert_eq!(
held(1.0 + f32::from_bits(0x3a00_0000)),
0x3c01,
"light halfway between two steps rounds up to the larger"
);
assert_eq!(
held(Half::LARGEST),
0x7bff,
"the largest light it reaches is its own largest step"
);
assert_eq!(
held(1.0e6),
0x7bff,
"and light past that is held there, never at a step that is no number"
);
assert_eq!(
held(-1.0),
0x0000,
"a sky holds no light under zero, so none reads back"
);
assert_eq!(
held(f32::NAN),
0x0000,
"and light that is no number is none"
);
assert_eq!(
held(Half::SMALLEST / 4.0),
0x0000,
"light under its smallest step is none of it"
);
}
}