frus-gpu 0.2.1

frus — GPU context and 2D rendering engine (Milestone 0)
Documentation
//! The renderer bound to a surface (a window): it configures wgpu and delegates
//! primitive drawing to the [`Painter`].

use frus_core::Scene;

use crate::compositor::{preferred_sample_count, Painters};

/// The background colour, a midnight blue.
const CLEAR_COLOR: wgpu::Color = wgpu::Color {
    r: 0.05,
    g: 0.05,
    b: 0.08,
    a: 1.0,
};

/// Holds the GPU state bound to a surface and presents the frames.
pub struct Renderer {
    surface: wgpu::Surface<'static>,
    device: wgpu::Device,
    queue: wgpu::Queue,
    config: wgpu::SurfaceConfiguration,
    painters: Painters,
}

impl Renderer {
    /// Initialises the GPU context for a given surface.
    ///
    /// `target` is typically an `Arc<Window>` supplied by the platform layer.
    /// `width` and `height` must both be > 0.
    pub async fn new(
        target: impl Into<wgpu::SurfaceTarget<'static>>,
        width: u32,
        height: u32,
    ) -> anyhow::Result<Self> {
        let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
            backends: wgpu::Backends::PRIMARY,
            ..wgpu::InstanceDescriptor::new_without_display_handle()
        });

        let surface = instance.create_surface(target)?;

        let adapter = instance
            .request_adapter(&wgpu::RequestAdapterOptions {
                power_preference: wgpu::PowerPreference::HighPerformance,
                compatible_surface: Some(&surface),
                force_fallback_adapter: false,
                ..Default::default()
            })
            .await?;

        log::info!("Adaptateur GPU : {:?}", adapter.get_info());

        // Downlevel limits, for GLES compatibility, but with the adapter's **real**
        // resolution: on mobile a screen of, say, 1080x2340 exceeds the downlevel
        // maximum texture size of 2048, and without this `surface.configure` panics.
        let required_limits = wgpu::Limits::downlevel_defaults().using_resolution(adapter.limits());

        let (device, queue) = adapter
            .request_device(&wgpu::DeviceDescriptor {
                label: Some("frus.device"),
                required_features: wgpu::Features::empty(),
                required_limits,
                memory_hints: wgpu::MemoryHints::Performance,
                ..Default::default()
            })
            .await?;

        let caps = surface.get_capabilities(&adapter);
        let format = caps
            .formats
            .iter()
            .copied()
            .find(|f| f.is_srgb())
            .unwrap_or(caps.formats[0]);

        let config = wgpu::SurfaceConfiguration {
            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
            format,
            color_space: wgpu::SurfaceColorSpace::Auto,
            width,
            height,
            present_mode: caps.present_modes[0],
            alpha_mode: caps.alpha_modes[0],
            view_formats: vec![],
            desired_maximum_frame_latency: 2,
        };
        surface.configure(&device, &config);

        // MSAA when the adapter supports it for this format; otherwise 1, disabled.
        let sample_count = preferred_sample_count(&adapter, format);
        log::info!("MSAA: {sample_count}×");

        let mut painters = Painters::new(&device, &queue, format, sample_count);
        // Warms every pipeline before the first real frame, to avoid jank.
        painters.warm_up(&device, &queue, format);

        Ok(Self {
            surface,
            device,
            queue,
            config,
            painters,
        })
    }

    /// Reconfigures the surface after the window is resized. The painters' viewports
    /// are set every frame by `Painters::render`.
    pub fn resize(&mut self, width: u32, height: u32) {
        if width > 0 && height > 0 {
            self.config.width = width;
            self.config.height = height;
            self.surface.configure(&self.device, &self.config);
        }
    }

    /// Reapplies the current configuration, after a lost or outdated surface.
    pub fn reconfigure(&mut self) {
        self.surface.configure(&self.device, &self.config);
    }

    /// Draws the scene — rectangles, images, paths, text, layers — and presents it.
    pub fn render(&mut self, scene: &Scene) -> RenderOutcome {
        let frame = match self.surface.get_current_texture() {
            wgpu::CurrentSurfaceTexture::Success(frame)
            | wgpu::CurrentSurfaceTexture::Suboptimal(frame) => frame,
            wgpu::CurrentSurfaceTexture::Timeout | wgpu::CurrentSurfaceTexture::Occluded => {
                return RenderOutcome::Skipped;
            }
            wgpu::CurrentSurfaceTexture::Outdated | wgpu::CurrentSurfaceTexture::Lost => {
                return RenderOutcome::NeedsReconfigure;
            }
            wgpu::CurrentSurfaceTexture::Validation => {
                log::warn!("frame skipped: surface validation error");
                return RenderOutcome::Skipped;
            }
        };
        let view = frame
            .texture
            .create_view(&wgpu::TextureViewDescriptor::default());

        self.painters.render(
            &self.device,
            &self.queue,
            self.config.format,
            &view,
            self.config.width,
            self.config.height,
            scene,
            Some(CLEAR_COLOR),
        );

        self.queue.present(frame);
        RenderOutcome::Presented
    }
}

/// What happened when [`Renderer::render`] tried to present a frame.
pub enum RenderOutcome {
    /// The frame was drawn and presented.
    Presented,
    /// The frame was skipped (occlusion, timeout, or a validation error); the caller
    /// should just try again on the next redraw.
    Skipped,
    /// The surface is lost or outdated: call [`Renderer::reconfigure`] before retrying.
    NeedsReconfigure,
}