mirage-engine 0.1.0

Mirage, an immediate-mode 3D engine for simple games on desktop and the browser
Documentation
use std::sync::Arc;

use winit::window::Window;

use crate::Error;
use crate::math::UVec2;

/// The depth buffer's format, shared by the texture and the pipelines that
/// test against it.
pub(crate) const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth32Float;

/// The device, queue, and surface a window draws through.
pub(crate) struct Gpu {
    instance: wgpu::Instance,
    window: Arc<Window>,
    device: wgpu::Device,
    queue: wgpu::Queue,
    surface: wgpu::Surface<'static>,
    format: wgpu::TextureFormat,
    physical_size: UVec2,
}

impl Gpu {
    /// Gets a device for `window` and sets up its surface.
    pub(crate) async fn new(instance: wgpu::Instance, window: Arc<Window>) -> Result<Self, Error> {
        let surface = instance
            .create_surface(Arc::clone(&window))
            .map_err(|error| Error::msg(format!("no rendering surface for the window: {error}")))?;

        let adapter = instance
            .request_adapter(&wgpu::RequestAdapterOptions {
                compatible_surface: Some(&surface),
                ..Default::default()
            })
            .await
            .map_err(|error| Error::msg(format!("no usable graphics adapter: {error}")))?;

        let (device, queue) = adapter
            .request_device(&wgpu::DeviceDescriptor {
                label: Some("mirage-engine"),
                ..Default::default()
            })
            .await
            .map_err(|error| {
                Error::msg(format!("the graphics adapter refused a device: {error}"))
            })?;

        let capabilities = surface.get_capabilities(&adapter);
        let format = capabilities
            .formats
            .first()
            .copied()
            .ok_or_else(|| Error::msg("the rendering surface supports no texture format"))?;

        let physical_size = physical_size(&window);
        let mut gpu = Self {
            instance,
            window,
            device,
            queue,
            surface,
            format,
            physical_size,
        };
        gpu.configure_surface();

        Ok(gpu)
    }

    pub(crate) fn device(&self) -> &wgpu::Device {
        &self.device
    }

    pub(crate) fn queue(&self) -> &wgpu::Queue {
        &self.queue
    }

    pub(crate) fn window(&self) -> Arc<Window> {
        Arc::clone(&self.window)
    }

    /// The format frames are drawn in, sRGB-encoded when presented.
    pub(crate) fn target_format(&self) -> wgpu::TextureFormat {
        self.format.add_srgb_suffix()
    }

    /// The format the UI and a screen past the tone map draw in: the same
    /// pixels taken as the encoded values they hold, which is the space
    /// egui blends in.
    pub(crate) fn overlay_format(&self) -> wgpu::TextureFormat {
        self.format.remove_srgb_suffix()
    }

    pub(crate) fn physical_size(&self) -> UVec2 {
        self.physical_size
    }

    pub(crate) fn request_frame(&self) {
        if self.drawable_size().is_some() {
            self.window.request_redraw();
        }
    }

    pub(crate) fn resize(&mut self, physical_size: UVec2) {
        if physical_size == self.physical_size {
            return;
        }
        self.physical_size = physical_size;
        self.configure_surface();
    }

    /// The frame to draw into, or `None` to skip drawing while the window has
    /// no area, or the surface is being replaced.
    pub(crate) fn begin_frame(&mut self) -> Option<Frame> {
        let size = self.drawable_size()?;
        let surface = self.acquire_surface_texture()?;
        let view = |format| {
            surface.texture.create_view(&wgpu::TextureViewDescriptor {
                format: Some(format),
                ..Default::default()
            })
        };
        let target = Target::new(
            view(self.target_format()),
            view(self.overlay_format()),
            size,
        );

        Some(Frame { surface, target })
    }

    pub(crate) fn present(&self, frame: Frame) {
        self.window.pre_present_notify();
        frame.surface.present();
    }

    fn acquire_surface_texture(&mut self) -> Option<wgpu::SurfaceTexture> {
        match self.surface.get_current_texture() {
            wgpu::CurrentSurfaceTexture::Success(frame) => Some(frame),
            wgpu::CurrentSurfaceTexture::Timeout | wgpu::CurrentSurfaceTexture::Occluded => None,
            wgpu::CurrentSurfaceTexture::Suboptimal(frame) => {
                drop(frame);
                self.configure_surface();
                None
            }
            wgpu::CurrentSurfaceTexture::Outdated => {
                self.configure_surface();
                None
            }
            wgpu::CurrentSurfaceTexture::Lost => {
                self.rebuild_surface();
                None
            }
            wgpu::CurrentSurfaceTexture::Validation => {
                log::error!("the graphics driver rejected the request for a frame");
                None
            }
        }
    }

    /// The size to draw at, absent while the window is minimized to nothing.
    fn drawable_size(&self) -> Option<UVec2> {
        let size = self.physical_size;
        (size.x > 0 && size.y > 0).then_some(size)
    }

    fn configure_surface(&mut self) {
        let Some(size) = self.drawable_size() else {
            return;
        };

        self.surface.configure(
            &self.device,
            &wgpu::SurfaceConfiguration {
                usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
                format: self.format,
                view_formats: vec![self.target_format(), self.overlay_format()],
                alpha_mode: wgpu::CompositeAlphaMode::Auto,
                width: size.x,
                height: size.y,
                desired_maximum_frame_latency: 2,
                present_mode: wgpu::PresentMode::AutoVsync,
            },
        );
    }

    fn rebuild_surface(&mut self) {
        match self.instance.create_surface(Arc::clone(&self.window)) {
            Ok(surface) => {
                self.surface = surface;
                self.configure_surface();
            }
            Err(error) => log::error!("the rendering surface could not be rebuilt: {error}"),
        }
    }
}

/// Target for one presented frame: what the tone map and the UI draw into,
/// and the size the projection uses.
pub(crate) struct Target {
    /// The view the tone map writes through: what it returns is sRGB-encoded
    /// on its way to the pixels.
    color: wgpu::TextureView,
    /// The same pixels taken as the encoded values they hold, which is what
    /// the UI blends in and what a screen past the tone map writes.
    encoded: wgpu::TextureView,
    size: UVec2,
}

impl Target {
    pub(crate) fn new(color: wgpu::TextureView, encoded: wgpu::TextureView, size: UVec2) -> Self {
        Self {
            color,
            encoded,
            size,
        }
    }

    pub(crate) fn color(&self) -> &wgpu::TextureView {
        &self.color
    }

    pub(crate) fn encoded(&self) -> &wgpu::TextureView {
        &self.encoded
    }

    pub(crate) fn size(&self) -> UVec2 {
        self.size
    }

    /// This target's aspect ratio, needed by the projection; the game never
    /// sets one.
    pub(crate) fn aspect(&self) -> f32 {
        let size = self.size();
        size.x as f32 / size.y as f32
    }
}

/// A window frame: its target, and the surface texture held until presented.
pub(crate) struct Frame {
    surface: wgpu::SurfaceTexture,
    target: Target,
}

impl Frame {
    pub(crate) fn target(&self) -> &Target {
        &self.target
    }
}

/// The depth buffer a `size`-sized target drawn over `samples` samples tests
/// against.
pub(crate) fn depth_texture(device: &wgpu::Device, size: UVec2, samples: u32) -> wgpu::Texture {
    device.create_texture(&wgpu::TextureDescriptor {
        label: Some("mirage-engine depth"),
        size: wgpu::Extent3d {
            width: size.x,
            height: size.y,
            depth_or_array_layers: 1,
        },
        mip_level_count: 1,
        sample_count: samples,
        dimension: wgpu::TextureDimension::D2,
        format: DEPTH_FORMAT,
        usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
        view_formats: &[],
    })
}

fn physical_size(window: &Window) -> UVec2 {
    let size = window.inner_size();
    UVec2::new(size.width, size.height)
}