smudge-platform 0.2.0

Windowing, input and presentation for the Smudge GUI library
Documentation
//! The GPU presentation path: choosing an adapter and presenting frames on a
//! wgpu surface.
//!
//! The window opens with this path when the renderer choice asks for the GPU
//! and an adapter that is not a software rasterizer can present to it. The
//! frames are full redraws; the CPU path stays in charge of damage.

use std::sync::Arc;
use std::time::Duration;

use futures_executor::block_on;
use smudge_core::app::FrameOutput;
use smudge_gpu::AdapterSummary;
use smudge_gpu::wgpu::{AdapterInfo, DeviceType};
use smudge_gpu::{GpuSurface, wgpu};
use web_time::Instant;
use winit::window::Window;

use super::present::{PresentStats, present_skipped};
use crate::error::PlatformError;
use crate::window::SharedWindow;

/// The names that mark a software rasterizer, matched case-insensitively
/// against the adapter's name and driver.
///
/// Some GL adapters do not report a device type of CPU: llvmpipe on the GL
/// backend of wgpu reports a name of `llvmpipe (...)` with a device type of
/// CPU on recent Mesa, and a device type of Other on older ones. The names
/// catch both, and the same rasterizers under their other names.
const SOFTWARE_MARKERS: [&str; 5] = [
    "llvmpipe",
    "softpipe",
    "lavapipe",
    "swiftshader",
    "microsoft basic render driver",
];

/// Returns whether `info` describes a software rasterizer.
///
/// This is the check the automatic renderer choice exists for: on a machine
/// without GPU passthrough, the adapter is a CPU rasterizer, and drawing on
/// it through a GPU renderer is slower than the CPU renderer.
pub(crate) fn adapter_is_software(info: &AdapterInfo) -> bool {
    if info.device_type == DeviceType::Cpu {
        return true;
    }
    let name = info.name.to_lowercase();
    let driver = info.driver.to_lowercase();
    SOFTWARE_MARKERS
        .iter()
        .any(|marker| name.contains(marker) || driver.contains(marker))
}

/// Creates a surface for `window` on `instance` and requests an adapter that
/// can present to it, or says why there is none.
fn connect(
    instance: &wgpu::Instance,
    window: &Arc<Window>,
) -> Result<(wgpu::Surface<'static>, wgpu::Adapter), String> {
    let surface = instance
        .create_surface(Box::new(SharedWindow(Arc::clone(window))))
        .map_err(|error| format!("the window surface could not be created: {error}"))?;
    let adapter = block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
        compatible_surface: Some(&surface),
        ..Default::default()
    }))
    .map_err(|error| format!("no adapter can present to the window: {error}"))?;
    Ok((surface, adapter))
}

/// The GPU presentation path of a window.
pub(crate) struct GpuPath {
    /// The presenter that draws the frames onto the surface.
    surface: GpuSurface,
    /// The window whose frames are presented, notified before every present.
    window: Arc<Window>,
    /// The adapter the surface presents through, read once at creation.
    summary: AdapterSummary,
}

impl GpuPath {
    /// Sets the GPU path up for `window`.
    ///
    /// The adapter comes from [`smudge_gpu::instance`], which enables the
    /// primary backends only. With `explicit` set, this is the caller's
    /// chosen renderer: when the primary backends cannot present to the
    /// window, the setup is retried with every backend, OpenGL included, and
    /// any failure after that fails the run. Otherwise the choice is
    /// automatic, and every failure of this function returns `None` for the
    /// CPU path: no surface or adapter on the primary backends, a software
    /// rasterizer, a device that cannot be created, a surface without a
    /// supported format, or a window larger than the device's texture limit.
    ///
    /// # Errors
    ///
    /// Returns [`PlatformError::Renderer`] when `explicit` is set and the
    /// setup fails.
    pub(crate) fn new(
        window: &Arc<Window>,
        width: u32,
        height: u32,
        explicit: bool,
    ) -> Result<Option<GpuPath>, PlatformError> {
        let fail = |message: String| {
            if explicit {
                Err(PlatformError::Renderer(message))
            } else {
                Ok(None)
            }
        };
        if width == 0 || height == 0 {
            return fail(String::from(
                "a window without pixels cannot present GPU frames",
            ));
        }
        let (surface, adapter) = match connect(&smudge_gpu::instance(), window) {
            Ok(connected) => connected,
            Err(primary) if explicit => {
                let every = wgpu::Instance::new(
                    wgpu::InstanceDescriptor::new_without_display_handle_from_env(),
                );
                match connect(&every, window) {
                    Ok(connected) => connected,
                    Err(error) => {
                        return fail(format!(
                            "{primary} on the primary backends, and {error} on any backend"
                        ));
                    }
                }
            }
            Err(_) => return Ok(None),
        };
        // The automatic choice inspects the adapter before anything is built
        // on it, so a software rasterizer costs nothing but the request.
        if !explicit && adapter_is_software(&adapter.get_info()) {
            return Ok(None);
        }
        let (device, queue) = match block_on(adapter.request_device(&wgpu::DeviceDescriptor {
            required_limits: smudge_gpu::device_limits(&adapter),
            ..Default::default()
        })) {
            Ok(device) => device,
            Err(error) => {
                return fail(format!("the adapter could not provide a device: {error}"));
            }
        };
        let surface = match GpuSurface::new(surface, &adapter, device, queue, width, height) {
            Ok(surface) => surface,
            Err(error) => return fail(format!("the window cannot present GPU frames: {error}")),
        };
        Ok(Some(GpuPath {
            surface,
            window: Arc::clone(window),
            summary: AdapterSummary::of(&adapter.get_info()),
        }))
    }

    /// Returns the adapter the path presents through, read once at creation.
    pub(crate) fn adapter_summary(&self) -> AdapterSummary {
        self.summary.clone()
    }

    /// Renders `frame` onto the surface and presents it.
    ///
    /// A frame whose damage is empty and whose app runs a tick changed
    /// nothing, so it is not presented, exactly as on the CPU path. Every
    /// presented frame is a full redraw: [`PresentStats::buffer_age`] stays
    /// at its unknown value, [`PresentStats::with_damage`] is off, and
    /// [`PresentStats::bytes_copied`] reports nothing because the GPU copies
    /// no pixmap. [`PresentStats::raster`] holds the whole draw, whose
    /// submission reaches the GPU asynchronously.
    ///
    /// Before the draw that ends in the present, the window is told that a
    /// frame is about to show. That is the order winit needs on Wayland: it
    /// schedules a frame callback that paces the next redraw, and presenting
    /// without it lets frames outrun the compositor until a blocked present
    /// stalls the window.
    ///
    /// Returns `Ok(None)` when nothing was presented: a skipped frame as on
    /// the CPU path, or a surface that had no texture to give, which the
    /// next redraw retries after a resize.
    ///
    /// # Errors
    ///
    /// Returns [`PlatformError::Renderer`] when the surface or the device was
    /// lost, or the device rejected the render.
    pub(crate) fn render_and_present(
        &mut self,
        frame: &FrameOutput<'_>,
        has_tick: bool,
    ) -> Result<Option<PresentStats>, PlatformError> {
        if present_skipped(frame.damage, has_tick, false) {
            return Ok(None);
        }
        let acquire_start = Instant::now();
        let texture = match self.surface.acquire() {
            Ok(Some(texture)) => texture,
            Ok(None) => return Ok(None),
            Err(error) => {
                return Err(PlatformError::Renderer(format!(
                    "the GPU surface failed: {error}"
                )));
            }
        };
        let acquire = acquire_start.elapsed();
        self.window.pre_present_notify();
        let start = Instant::now();
        self.surface.draw(frame, texture).map_err(|error| {
            PlatformError::Renderer(format!("the GPU renderer failed: {error}"))
        })?;
        let raster = start.elapsed();
        Ok(Some(PresentStats {
            buffer_age: 0,
            with_damage: false,
            bytes_copied: 0,
            surface_width: self.surface.width(),
            surface_height: self.surface.height(),
            raster,
            acquire,
            present: Duration::ZERO,
            renderer: "gpu",
        }))
    }

    /// Resizes the surface and the presenter to the window's new extent.
    ///
    /// # Errors
    ///
    /// Returns [`PlatformError::Renderer`] when the new extent exceeds the
    /// device's texture limit. The run fails, because the renderer choice is
    /// fixed when the window opens.
    pub(crate) fn resize(&mut self, width: u32, height: u32) -> Result<(), PlatformError> {
        self.surface.resize(width, height).map_err(|error| {
            PlatformError::Renderer(format!("the GPU surface could not be resized: {error}"))
        })
    }
}

#[cfg(test)]
#[path = "gpu_tests.rs"]
mod tests;