mirage-engine 0.2.0

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::{GpuMeshes, HandedMeshes};
use passes::Scene;
use pipelines::{FrameBindings, GpuInstance, Pipelines, Styles};
use post::Post;
use post_effects::PostPasses;
use shadows::Shadows;
use skybox::{GpuSkies, SkyId};

use crate::assets::Textures;
use crate::gpu::{Gpu, Target, buffer};
use crate::post_effect;
use crate::skybox::Resident;
use crate::surface_style;
use crate::ui::{Changes, Painter};
use crate::{Config, Error, light};

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

/// The display thread's passes: the GPU copies a handed frame draws by id,
/// and the buffers its draws are drawn from. Names no game type.
pub(crate) struct Renderer {
    meshes: GpuMeshes,
    skies: GpuSkies,
    textures: Textures,
    batcher: Batcher,
    instances: InstanceBuffer,
    frame: FrameBindings,
    pipelines: Pipelines,
    styles: Styles,
    post_effects: PostPasses,
    post: Post,
    shadows: Shadows,
}

impl Renderer {
    /// 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(
        device: &wgpu::Device,
        queue: &wgpu::Queue,
        display_format: wgpu::TextureFormat,
        config: &Config,
        styles: Vec<surface_style::Declaration>,
        effects: Vec<post_effect::Declaration>,
    ) -> 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 = GpuSkies::new(device, queue, &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,
            styles,
            samples,
            frame.layout(),
            textures.layout(),
            shadows.layout(),
        )
        .await?;
        let post_effects = PostPasses::compile(device, effects, samples, display_format).await?;

        Ok(Self {
            skies,
            meshes: GpuMeshes::new(),
            batcher: Batcher::new(),
            instances,
            pipelines,
            styles,
            post_effects,
            post,
            frame,
            textures,
            shadows,
        })
    }

    /// Takes everything a frame commands of the GPU: the meshes it built
    /// and the ids it dropped, the skies it built, and the textures its UI
    /// changed.
    pub(crate) fn take(
        &mut self,
        device: &wgpu::Device,
        queue: &wgpu::Queue,
        meshes: HandedMeshes,
        skies: Vec<(SkyId, Resident)>,
        ui: Changes,
        painter: &mut Painter,
    ) {
        self.meshes.take(meshes);
        self.skies.receive(device, queue, &self.frame, skies);
        painter.take(device, queue, ui);
    }

    /// Draws `draws` into the window and presents it.
    ///
    /// A window the platform has no surface for, one a resize or a minimize
    /// left without one, draws nothing; the next call draws the same frame
    /// again.
    pub(crate) fn render(&mut self, gpu: &mut Gpu, draws: &DrawList, painter: &mut Painter) {
        let Some(frame) = gpu.begin_frame() else {
            return;
        };

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

    /// Draws `draws` into `target`, its UI over it through `painter`, and
    /// submits them — 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,
        draws: &DrawList,
        painter: &mut Painter,
    ) -> RenderStats {
        self.resolve(device, queue, target, draws);
        queue.submit(self.encode(device, queue, target, draws, painter));

        self.batcher
            .drawn()
            .fold(RenderStats::NOTHING, |stats, batch| RenderStats {
                draw_calls: stats.draw_calls + 1,
                instances: stats.instances + batch.instances.len() as u32,
            })
    }

    /// Batches `draws` and writes everything the passes read of the frame:
    /// the GPU copies of its meshes, its instances and palette, its camera,
    /// lights and maps, and its style and effect values.
    fn resolve(
        &mut self,
        device: &wgpu::Device,
        queue: &wgpu::Queue,
        target: &Target,
        draws: &DrawList,
    ) {
        self.post.prepare(device, target.size());

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

        self.batcher.run(draws, &self.meshes, target.aspect());
        for batch in self.batcher.drawn().chain(self.batcher.casters().batches()) {
            self.meshes
                .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, draws.surface_style_values(style));
        }

        let plan = self.shadows.plan();
        let lighting = self.skies.drawing(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);
        self.post_effects
            .submit(queue, |effect| draws.post_effect_values(effect));
    }

    /// The frame's command buffers in submission order: whatever the painter
    /// needs first, then the scene, the chain that takes it to the target,
    /// and the UI over it.
    fn encode(
        &self,
        device: &wgpu::Device,
        queue: &wgpu::Queue,
        target: &Target,
        draws: &DrawList,
        painter: &mut Painter,
    ) -> 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,
            instances: self.instances.buffer(),
            pipelines: &self.pipelines,
            styles: &self.styles,
            frame: &self.frame,
            textures: &self.textures,
            shadows: &self.shadows,
            sky: self.skies.drawing(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 = painter.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
    }
}

pub(crate) 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;