gpu.rs raw

//! Window-facing GPU plumbing for the vello backend: adapter, device,
//! swapchain, and the intermediate texture vello draws into.
//!
//! vello ships `vello::util::RenderContext` for exactly this, and it is the
//! obvious thing to reach for, but it hard-codes the device descriptor —
//! including wgpu's default `MemoryHints::Performance`, which tells the
//! driver to sub-allocate in very large blocks. On a discrete card that
//! costs about a quarter of a gigabyte of video memory before a single
//! fragment is drawn, and it is the wrong trade for a GUI: guiduck renders
//! a few megabytes of geometry per frame and would rather have the memory
//! back than shave a microsecond off an allocation it makes once.
//!
//! Owning the device here also puts it where the rest of the swapchain and
//! present plumbing already lives, which is what the vello backend crate
//! documents as the shell's job.

use std::error::Error;
use std::sync::Arc;

use wgpu::util::TextureBlitter;
use winit::window::Window;

/// The device and swapchain behind one window.
pub struct GpuSurface {
    pub device: wgpu::Device,
    pub queue: wgpu::Queue,
    surface: wgpu::Surface<'static>,
    config: wgpu::SurfaceConfiguration,
    /// vello rasterizes with a compute shader, which cannot bind a swapchain
    /// texture. It draws here instead, and this is blitted onto the frame.
    target_texture: wgpu::Texture,
    target_view: wgpu::TextureView,
    blitter: TextureBlitter,
}

impl GpuSurface {
    pub async fn new(
        window: Arc<Window>,
        width: u32,
        height: u32,
        present_mode: wgpu::PresentMode,
    ) -> Result<Self, Box<dyn Error>> {
        let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
            display: None,
            backends: wgpu::Backends::from_env().unwrap_or_default(),
            flags: wgpu::InstanceFlags::from_build_config().with_env(),
            memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(),
            backend_options: wgpu::BackendOptions::from_env_or_default(),
        });
        let surface = instance.create_surface(window)?;
        let adapter = instance
            .request_adapter(&wgpu::RequestAdapterOptions {
                power_preference: wgpu::PowerPreference::from_env().unwrap_or_default(),
                force_fallback_adapter: false,
                compatible_surface: Some(&surface),
            })
            .await?;

        // vello uses these when the adapter has them and does without
        // otherwise, so ask for the intersection rather than requiring them.
        let optional = wgpu::Features::CLEAR_TEXTURE | wgpu::Features::PIPELINE_CACHE;
        let (device, queue) = adapter
            .request_device(&wgpu::DeviceDescriptor {
                label: Some("guiduck"),
                required_features: adapter.features() & optional,
                required_limits: wgpu::Limits::default(),
                memory_hints: wgpu::MemoryHints::MemoryUsage,
                ..Default::default()
            })
            .await?;

        let capabilities = surface.get_capabilities(&adapter);
        let format = capabilities
            .formats
            .iter()
            .copied()
            .find(wgpu::TextureFormat::is_srgb)
            .unwrap_or_else(|| {
                *capabilities
                    .formats
                    .first()
                    .expect("surface supports no formats")
            });
        let config = wgpu::SurfaceConfiguration {
            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
            format,
            width,
            height,
            present_mode,
            desired_maximum_frame_latency: 2,
            alpha_mode: wgpu::CompositeAlphaMode::Auto,
            view_formats: vec![],
        };
        surface.configure(&device, &config);

        let (target_texture, target_view) = create_target(&device, width, height);
        let blitter = TextureBlitter::new(&device, format);

        Ok(Self {
            device,
            queue,
            surface,
            config,
            target_texture,
            target_view,
            blitter,
        })
    }

    /// The view vello renders into.
    pub fn target_view(&self) -> wgpu::TextureView {
        self.target_view.clone()
    }

    pub fn resize(&mut self, width: u32, height: u32) {
        if width == 0 || height == 0 || (self.config.width == width && self.config.height == height)
        {
            return;
        }
        self.config.width = width;
        self.config.height = height;
        self.surface.configure(&self.device, &self.config);
        let (texture, view) = create_target(&self.device, width, height);
        self.target_texture = texture;
        self.target_view = view;
    }

    /// Reconfigure without resizing, for a surface the compositor has
    /// invalidated.
    pub fn reconfigure(&self) {
        self.surface.configure(&self.device, &self.config);
    }

    pub fn get_current_texture(&self) -> wgpu::CurrentSurfaceTexture {
        self.surface.get_current_texture()
    }

    /// Copy what vello drew onto the frame's texture and submit it.
    pub fn blit_to(&self, frame: &wgpu::TextureView) {
        let mut encoder = self
            .device
            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
                label: Some("guiduck surface blit"),
            });
        self.blitter
            .copy(&self.device, &mut encoder, &self.target_view, frame);
        self.queue.submit([encoder.finish()]);
    }
}

fn create_target(
    device: &wgpu::Device,
    width: u32,
    height: u32,
) -> (wgpu::Texture, wgpu::TextureView) {
    let texture = device.create_texture(&wgpu::TextureDescriptor {
        label: Some("guiduck vello target"),
        size: wgpu::Extent3d {
            width,
            height,
            depth_or_array_layers: 1,
        },
        mip_level_count: 1,
        sample_count: 1,
        dimension: wgpu::TextureDimension::D2,
        usage: wgpu::TextureUsages::STORAGE_BINDING | wgpu::TextureUsages::TEXTURE_BINDING,
        format: wgpu::TextureFormat::Rgba8Unorm,
        view_formats: &[],
    });
    let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
    (texture, view)
}