mirage-engine 0.1.1

Mirage, an immediate-mode 3D engine for simple games on desktop and the browser
Documentation
//! The skies a game named, as the GPU keeps them, beside the default sky.

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};

/// The format every sky is kept in: linear light, so a loaded image and a
/// gradient past `1.0` are kept as one kind of texture.
const FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba16Float;

/// Which of the built skies a frame is drawn and lit by.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) struct SkyId(u32);

/// The skies a game's values build, kept for the run and keyed by the value
/// that built them, beside the default sky.
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> {
    /// Builds the default sky and the sampler every sky is read through.
    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,
        }
    }

    /// The sky `sky` names, building it the first time a frame is drawn by
    /// it; `None`, the default sky, where its image is no sky at all.
    ///
    /// An image that is no sky is a debug log here and a startup error in
    /// the catalog run, so only a value the catalog leaves out reaches a
    /// frame this way.
    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
            }
        }
    }

    /// Builds every value the vocabulary catalogs, so that the game starts
    /// only once every sky it names is kept.
    ///
    /// What a build needed from the assets and did not get is recorded
    /// there, alongside the meshes' and the sounds'.
    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,
                });
            }
        }
    }

    /// Binds every sky to the frame's values again, which a frame that grew
    /// the palette they are read beside needs.
    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);
        }
    }

    /// The sky `id` holds; the default sky where the frame set none.
    pub(crate) fn drawing(&self, id: Option<SkyId>) -> &GpuSky {
        id.and_then(|id| self.named.get(id.0 as usize))
            .unwrap_or(&self.default)
    }

    /// The id `sky` builds to, or the error its image is no sky for.
    ///
    /// A value whose image is no sky is keyed to the default sky, so a frame
    /// drawing it again neither builds it again nor logs again.
    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)
    }
}

/// One sky as the GPU keeps it: what the frame is bound to it through, the
/// image that binding reads, and the light it lands on a surface.
pub(crate) struct GpuSky {
    frame: wgpu::BindGroup,
    view: wgpu::TextureView,
    lighting: Lighting,
}

impl GpuSky {
    /// What every pass of the frame reads the frame's own values and this
    /// sky through.
    pub(crate) fn frame(&self) -> &wgpu::BindGroup {
        &self.frame
    }

    /// What this sky lights a surface by.
    pub(crate) fn lighting(&self) -> Lighting {
        self.lighting
    }
}

/// What a sky lights a surface by: the nine coefficients the direction a
/// surface faces is read through, the mip level a fully rough surface
/// reflects it from, and what a reflection of it is scaled by.
#[derive(Clone, Copy)]
pub(crate) struct Lighting {
    pub(crate) irradiance: [Vec4; 9],
    /// The mip level a fully rough surface reflects the sky from: the
    /// smallest mip's, counting `0.0` at the largest.
    pub(crate) top_mip: f32,
    /// What a surface's reflection of the sky is scaled by; the coefficients
    /// already hold it.
    pub(crate) share: f32,
}

/// One channel as the GPU reads it: the 16-bit number [`FORMAT`] holds.
#[repr(transparent)]
#[derive(Clone, Copy, Debug, Pod, Zeroable)]
struct Half(u16);

impl Half {
    /// The largest light this number reaches.
    const LARGEST: f32 = 65_504.0;

    /// The smallest light this number holds with every one of its own steps.
    const SMALLEST: f32 = 6.103_515_6e-5;

    /// What this number's exponent counts from, against a 32-bit one's.
    const BIAS: u32 = 127 - 15;

    /// `light` as that number, held within what it reaches and rounded to
    /// the nearest step it holds, light halfway between two steps taken as
    /// the larger.
    ///
    /// A sky holds no light under zero, and light under
    /// [`Half::SMALLEST`] reads back as none of it.
    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)
    }
}

/// `resident` uploaded and bound to the frame the forward pipelines light
/// by.
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(),
        },
    }
}

/// Uploads every mip of `resident`, largest first.
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
}

/// What every sky is read through: the image meets itself the whole way
/// around, and stops at its own top and bottom rows.
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()
    })
}

#[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"
        );
    }
}