mirage-engine 0.2.0

Mirage, an immediate-mode 3D engine for simple games on desktop and the browser
Documentation
//! The skies a game named: the game thread's catalog of them by id, and the
//! GPU copies the display thread keeps by the same id, beside the default sky.

use std::collections::HashMap;
use std::sync::Arc;

use bytemuck::{Pod, Zeroable};

use crate::Assets;
use crate::assets::{Missing, Unresolved};
use crate::math::Vec4;
use crate::renderer::pipelines::FrameBindings;
use crate::skybox::{Gradient, Resident, 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; the one key the
/// two threads share.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) struct SkyId(u32);

/// The game thread's skies: the game's values keyed to ids, and every sky
/// built since the last hand, which the next frame carries to the display
/// thread.
pub(crate) struct SkyCatalog<S: Skyboxes> {
    assets: Arc<Assets>,
    ids: HashMap<S, Option<SkyId>>,
    built: u32,
    unhanded: Vec<(SkyId, Resident)>,
}

impl<S: Skyboxes> SkyCatalog<S> {
    pub(crate) fn new(assets: Arc<Assets>) -> Self {
        Self {
            assets,
            ids: HashMap::new(),
            built: 0,
            unhanded: Vec::new(),
        }
    }

    /// 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, and what its build did not get is a debug log too.
    pub(crate) fn id(&mut self, sky: &S) -> Option<SkyId> {
        let (id, unresolved) = self.built(sky);
        unresolved.logged();
        id
    }

    /// Builds every value the vocabulary catalogs, so that the game starts
    /// only once every sky it names is kept, and returns what those builds
    /// did not get, every image that is no sky among it.
    pub(crate) fn build_catalog(&mut self) -> Unresolved {
        let mut unresolved = Unresolved::default();
        for sky in S::catalog() {
            unresolved.record(self.built(&sky).1);
        }

        unresolved
    }

    /// Ends the frame and hands over every sky it built.
    ///
    /// The hand is the one way out of the catalog, so no sky is built
    /// without the display thread being handed it.
    pub(crate) fn end_frame(&mut self) -> Vec<(SkyId, Resident)> {
        core::mem::take(&mut self.unhanded)
    }

    /// The id `sky` builds to — the default sky where its image is no sky at
    /// all — beside what its build did not get, that image among it.
    ///
    /// A value whose image is no sky is keyed to the default sky, so a frame
    /// drawing it again neither builds it again nor records it again.
    fn built(&mut self, sky: &S) -> (Option<SkyId>, Unresolved) {
        if let Some(&id) = self.ids.get(sky) {
            return (id, Unresolved::default());
        }

        let mut data = sky.build(&self.assets);
        let mut unresolved = data.take_unresolved();
        let id = match data.resident() {
            Ok(resident) => {
                let id = SkyId(self.built);
                self.built += 1;
                self.unhanded.push((id, resident));
                Some(id)
            }
            Err(error) => {
                unresolved.record(Unresolved::of(Missing::Skybox {
                    skybox: format!("{sky:?}"),
                    error,
                }));
                None
            }
        };
        self.ids.insert(sky.clone(), id);

        (id, unresolved)
    }
}

/// The display thread's skies: the GPU copy of every sky it was handed, by
/// id, beside the default sky.
pub(crate) struct GpuSkies {
    named: Vec<GpuSky>,
    default: GpuSky,
    sampler: wgpu::Sampler,
}

impl GpuSkies {
    /// Builds the default sky and the sampler every sky is read through.
    pub(crate) fn new(device: &wgpu::Device, queue: &wgpu::Queue, frame: &FrameBindings) -> Self {
        let sampler = covering_sampler(device);

        Self {
            default: built(
                device,
                queue,
                frame,
                &sampler,
                &Resident::gradient(Gradient::default()),
            ),
            named: Vec::new(),
            sampler,
        }
    }

    /// Uploads every sky a frame handed; ids arrive in the order they were
    /// built, so each lands at its own index.
    pub(crate) fn receive(
        &mut self,
        device: &wgpu::Device,
        queue: &wgpu::Queue,
        frame: &FrameBindings,
        handed: Vec<(SkyId, Resident)>,
    ) {
        for (_, resident) in handed {
            self.named
                .push(built(device, queue, frame, &self.sampler, &resident));
        }
    }

    /// 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, or
    /// where no frame has handed `id` yet.
    pub(crate) fn drawing(&self, id: Option<SkyId>) -> &GpuSky {
        id.and_then(|id| self.named.get(id.0 as usize))
            .unwrap_or(&self.default)
    }
}

/// 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 covering_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"
        );
    }
}