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)
}