mirage-engine 0.1.1

Mirage, an immediate-mode 3D engine for simple games on desktop and the browser
Documentation
//! State kept between frames: what a frame's draws are resolved, batched,
//! and encoded against.

use draw_list::{Batcher, DrawList};
use mesh_cache::MeshCache;
use passes::Scene;
use pipelines::{FrameBindings, GpuInstance, Pipelines, Styles};
use post::Post;
use post_effects::PostPasses;
use shadows::Shadows;
use skybox::Skies;

use core::time::Duration;
use std::rc::Rc;

use crate::assets::{Assets, Textures};
use crate::gpu::{Gpu, Target, buffer};
use crate::mesh::{Draw, Meshes};
use crate::post_effect::{self, PostEffectId, PostEffects};
use crate::skybox::Skyboxes;
use crate::surface_style::{self, SurfaceStyleId, SurfaceStyles};
use crate::ui::Overlay;
use crate::{Camera, Config, Error, Light, light};

/// Instance cap before the instance buffer must grow.
const INITIAL_INSTANCE_CAPACITY: usize = 256;

/// State behind the frame API: it owns the caches draws are resolved
/// against, and the buffers they are drawn from.
pub(crate) struct Renderer<M: Meshes, S: Skyboxes> {
    meshes: MeshCache<M>,
    skies: Skies<S>,
    textures: Textures,
    draws: DrawList<M>,
    batcher: Batcher,
    instances: InstanceBuffer,
    frame: FrameBindings,
    pipelines: Pipelines,
    styles: Styles,
    post_effects: PostPasses,
    post: Post,
    shadows: Shadows,
}

impl<M: Meshes, S: Skyboxes> Renderer<M, S> {
    /// Builds everything a game draws through, the styles it declared
    /// compiled and checked — the one part of this that can fail.
    pub(crate) async fn new<T: SurfaceStyles, C: PostEffects>(
        device: &wgpu::Device,
        queue: &wgpu::Queue,
        display_format: wgpu::TextureFormat,
        config: &Config,
        assets: Rc<Assets>,
    ) -> Result<Self, Error> {
        let samples = post::sample_count(config.antialiasing());
        // The device reports a refused build after the call returns, so one
        // scope covers all of them.
        let scope = device.push_error_scope(wgpu::ErrorFilter::Validation);
        let frame = FrameBindings::new(device);
        let textures = Textures::new(device, queue);
        let shadows = Shadows::new(device, config.shadow_resolution());
        let pipelines = Pipelines::new(
            device,
            samples,
            frame.layout(),
            textures.layout(),
            shadows.layout(),
        );
        let post = Post::new(device, display_format, samples, config.tonemap());
        let skies = Skies::new(device, queue, Rc::clone(&assets), &frame);
        let instances = InstanceBuffer::new(device, INITIAL_INSTANCE_CAPACITY);
        if let Some(error) = scope.pop().await {
            return Err(Error::msg(format!(
                "the graphics device refused what the engine builds at startup: {error}"
            )));
        }

        let styles = Styles::compile(
            device,
            surface_style::Declarations::of::<T>(),
            samples,
            frame.layout(),
            textures.layout(),
            shadows.layout(),
        )
        .await?;
        let post_effects = PostPasses::compile(
            device,
            post_effect::Declarations::of::<C>(),
            samples,
            display_format,
        )
        .await?;

        Ok(Self {
            skies,
            meshes: MeshCache::new(assets, config.mesh_memory()),
            draws: DrawList::new(),
            batcher: Batcher::new(),
            instances,
            pipelines,
            styles,
            post_effects,
            post,
            frame,
            textures,
            shadows,
        })
    }

    pub(crate) fn set_camera(&mut self, camera: Camera) {
        self.draws.set_camera(camera);
    }

    /// The camera the frame is drawn from; also its listener unless the
    /// game set one of its own.
    pub(crate) fn camera(&self) -> Camera {
        self.draws.camera()
    }

    pub(crate) fn light(&mut self, light: Light) {
        self.draws.push_light(light);
    }

    /// Draws and lights the rest of the frame by `sky`, built the first
    /// time a frame is drawn by it; by the default sky where its image is
    /// no sky at all.
    pub(crate) fn set_skybox(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, sky: &S) {
        let id = self.skies.id(device, queue, &self.frame, sky);
        self.draws.set_skybox(id);
    }

    pub(crate) fn set_exposure(&mut self, exposure: f32) {
        self.draws.set_exposure(exposure);
    }

    pub(crate) fn set_bloom(&mut self, amount: f32) {
        self.draws.set_bloom(amount);
    }

    /// Passes the style at `at` the values it reads this frame.
    pub(crate) fn set_surface_style(&mut self, at: SurfaceStyleId, values: &impl SurfaceStyles) {
        self.draws.set_surface_style(at, |into| values.write(into));
    }

    /// Runs the post effect at `at` over this frame with the values it reads.
    pub(crate) fn set_post_effect(&mut self, at: PostEffectId, values: &impl PostEffects) {
        self.draws.set_post_effect(at, |into| values.write(into));
    }

    /// Starts a frame drawn at `now` on the run clock, the instant every
    /// draw it records is posed at.
    pub(crate) fn start_frame(&mut self, now: Duration) {
        self.draws.start(now);
    }

    pub(crate) fn draw(&mut self, draw: Draw<M>) {
        self.draws.push(draw);
    }

    /// The mesh cache, which a tick reads a mesh's clips from.
    pub(crate) fn meshes(&mut self) -> &mut MeshCache<M> {
        &mut self.meshes
    }

    pub(crate) fn prepare(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, mesh: M) {
        let id = self.meshes.id_of(&mesh);
        self.meshes
            .store_mut()
            .upload(device, queue, &self.textures, id);
    }

    /// Builds and uploads everything the vocabularies catalog, recording
    /// whatever its meshes and its skies needed and did not get.
    pub(crate) fn build_catalog(&mut self, device: &wgpu::Device, queue: &wgpu::Queue) {
        for id in self.meshes.build_catalog() {
            self.meshes
                .store_mut()
                .upload(device, queue, &self.textures, id);
        }
        self.skies.build_catalog(device, queue, &self.frame);
    }

    /// Draws the frame the game just recorded into the window and presents it.
    ///
    /// A frame the window has no surface for is dropped, not kept for
    /// later; draws never last past the frame they were recorded in.
    pub(crate) fn render(&mut self, gpu: &mut Gpu, overlay: &mut Overlay) {
        let Some(frame) = gpu.begin_frame() else {
            self.draws.clear();
            return;
        };

        let stats = self.render_to(gpu.device(), gpu.queue(), frame.target(), overlay);
        log::trace!(
            "frame drawn in {} instanced draws over {} instances",
            stats.draw_calls,
            stats.instances
        );
        gpu.present(frame);
    }

    /// Draws the recorded frame into `target`, `overlay` over it, and submits
    /// them, then drops the draws — the whole of a frame, minus what only a
    /// window needs.
    pub(crate) fn render_to(
        &mut self,
        device: &wgpu::Device,
        queue: &wgpu::Queue,
        target: &Target,
        overlay: &mut Overlay,
    ) -> RenderStats {
        self.resolve(device, queue, target);
        queue.submit(self.encode(device, queue, target, overlay));

        let stats = self
            .batcher
            .drawn()
            .fold(RenderStats::NOTHING, |stats, batch| RenderStats {
                draw_calls: stats.draw_calls + 1,
                instances: stats.instances + batch.instances.len() as u32,
            });
        self.draws.clear();
        self.meshes.store_mut().end_frame();
        stats
    }

    /// Batches the frame's draws and checks that everything they need is on
    /// the GPU (mesh data, instance and camera data), and that what it is drawn
    /// through matches the target.
    fn resolve(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, target: &Target) {
        self.post.prepare(device, target.size());

        let fallback = light::default_lights();
        let lights = match self.draws.lights() {
            [] => &fallback[..],
            submitted => submitted,
        };
        let camera = self.draws.camera();
        self.post.set_frame(
            queue,
            &post::Frame {
                size: target.size(),
                exposure: self.draws.exposure(),
                bloom: self.draws.bloom(),
            },
        );
        self.shadows
            .prepare(device, lights, camera, target.aspect());

        self.batcher
            .run(&self.draws, &mut self.meshes, target.aspect());
        for batch in self.batcher.drawn().chain(self.batcher.casters().batches()) {
            self.meshes
                .store_mut()
                .upload(device, queue, &self.textures, batch.mesh);
        }

        self.instances
            .write(device, queue, self.batcher.instances());
        if self
            .frame
            .set_palette(device, queue, self.batcher.palette())
        {
            self.skies.rebind(device, &self.frame);
        }
        for style in self.styles.ids() {
            self.styles
                .set_values(queue, style, self.draws.surface_style_values(style));
        }

        let plan = self.shadows.plan();
        let lighting = self.skies.drawing(self.draws.skybox()).lighting();
        self.frame
            .set_frame(queue, camera, target.size(), plan.lights(), lighting);
        self.frame.set_casters(queue, plan.casters());
        self.frame.set_maps(queue, plan.sampled());

        self.post_effects
            .prepare(device, queue, target.size(), camera, &self.post);
        let Self {
            post_effects,
            draws,
            ..
        } = self;
        post_effects.submit(queue, |effect| draws.post_effect_values(effect));
    }

    /// The frame's command buffers in submission order: whatever the overlay
    /// needs first, then the scene, the chain that takes it to the target,
    /// and the overlay over it.
    fn encode(
        &self,
        device: &wgpu::Device,
        queue: &wgpu::Queue,
        target: &Target,
        overlay: &mut Overlay,
    ) -> Vec<wgpu::CommandBuffer> {
        let Some(hdr) = self.post.scene() else {
            return Vec::new();
        };
        let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
            label: Some("mirage-engine frame"),
        });
        let scene = Scene {
            opaque: self.batcher.opaque(),
            cutout: self.batcher.cutout(),
            translucent: self.batcher.translucent(),
            additive: self.batcher.additive(),
            casters: self.batcher.casters(),
            meshes: self.meshes.store(),
            instances: self.instances.buffer(),
            pipelines: &self.pipelines,
            styles: &self.styles,
            frame: &self.frame,
            textures: &self.textures,
            shadows: &self.shadows,
            sky: self.skies.drawing(self.draws.skybox()),
        };
        shadows::cast(&mut encoder, &scene);
        passes::forward(&mut encoder, &hdr, &scene);

        self.post_effects.resolve(&mut encoder);
        let drawn = self.post_effects.scene(&mut encoder);
        let Some(source) = drawn.or_else(|| self.post.sampled()) else {
            return Vec::new();
        };
        self.post
            .encode(&mut encoder, source, self.post_effects.tone_mapped(target));
        self.post_effects.run_tone_mapped(&mut encoder, target);

        let mut buffers = overlay.encode(
            device,
            queue,
            &mut encoder,
            self.post_effects.overlaid(target),
            target.size(),
        );
        self.post_effects.run_over_ui(&mut encoder, target);
        buffers.push(encoder.finish());
        buffers
    }
}

/// Draw and instance counts one frame produced on the GPU, counting what
/// the camera drew: a draw only a light casts is left out of both.
#[derive(Clone, Copy, Debug)]
pub(crate) struct RenderStats {
    pub(crate) draw_calls: u32,
    pub(crate) instances: u32,
}

impl RenderStats {
    /// Counts for a frame that drew nothing.
    const NOTHING: Self = Self {
        draw_calls: 0,
        instances: 0,
    };
}

/// The frame's instance data, in a buffer that only ever grows.
struct InstanceBuffer {
    buffer: wgpu::Buffer,
    capacity: usize,
}

impl InstanceBuffer {
    fn new(device: &wgpu::Device, capacity: usize) -> Self {
        Self {
            buffer: buffer(
                device,
                "mirage-engine instances",
                (capacity * size_of::<GpuInstance>()) as wgpu::BufferAddress,
                wgpu::BufferUsages::VERTEX,
            ),
            capacity,
        }
    }

    fn write(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, instances: &[GpuInstance]) {
        if instances.is_empty() {
            return;
        }
        if instances.len() > self.capacity {
            *self = Self::new(device, instances.len().next_power_of_two());
        }
        queue.write_buffer(&self.buffer, 0, bytemuck::cast_slice(instances));
    }

    fn buffer(&self) -> &wgpu::Buffer {
        &self.buffer
    }
}

mod draw_list;
pub(crate) mod mesh_cache;
mod passes;
mod pipelines;
mod post;
mod post_effects;
pub(crate) mod shadows;
pub(crate) mod skybox;