use std::sync::Arc;
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
#[non_exhaustive]
pub enum CopySrc {
#[default]
Never,
IfSupported,
Required,
}
#[derive(Clone, Copy, Debug)]
#[non_exhaustive]
pub struct GpuConfig {
pub label: &'static str,
pub copy_src: CopySrc,
pub present_mode: wgpu::PresentMode,
}
impl Default for GpuConfig {
fn default() -> Self {
Self {
label: "agg-gui-wgpu",
copy_src: CopySrc::Never,
present_mode: wgpu::PresentMode::AutoVsync,
}
}
}
impl GpuConfig {
pub fn new(label: &'static str) -> Self {
Self {
label,
..Self::default()
}
}
pub fn with_copy_src(mut self, copy_src: CopySrc) -> Self {
self.copy_src = copy_src;
self
}
pub fn with_present_mode(mut self, present_mode: wgpu::PresentMode) -> Self {
self.present_mode = present_mode;
self
}
}
#[derive(Debug)]
#[non_exhaustive]
pub enum GpuInitError {
CreateSurface(wgpu::CreateSurfaceError),
RequestAdapter,
RequestDevice,
CopySrcUnsupported,
NoSurfaceFormats,
NoAlphaModes,
}
impl std::fmt::Display for GpuInitError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::CreateSurface(e) => write!(f, "create wgpu surface: {e}"),
Self::RequestAdapter => write!(f, "no suitable wgpu adapter"),
Self::RequestDevice => write!(f, "could not request a wgpu device"),
Self::CopySrcUnsupported => {
write!(f, "surface does not support COPY_SRC read-back")
}
Self::NoSurfaceFormats => write!(f, "surface reports no supported texture formats"),
Self::NoAlphaModes => write!(f, "surface reports no supported alpha modes"),
}
}
}
impl std::error::Error for GpuInitError {}
fn pick_surface_format(
formats: &[wgpu::TextureFormat],
) -> Result<wgpu::TextureFormat, GpuInitError> {
formats
.iter()
.copied()
.find(|f| !f.is_srgb())
.or_else(|| formats.first().copied())
.ok_or(GpuInitError::NoSurfaceFormats)
}
fn pick_alpha_mode(
modes: &[wgpu::CompositeAlphaMode],
) -> Result<wgpu::CompositeAlphaMode, GpuInitError> {
modes.first().copied().ok_or(GpuInitError::NoAlphaModes)
}
pub fn clamp_surface_size(w: u32, h: u32, max_dim: u32) -> (u32, u32) {
let max_dim = max_dim.max(1);
(w.clamp(1, max_dim), h.clamp(1, max_dim))
}
pub struct Gpu {
device: Arc<wgpu::Device>,
queue: Arc<wgpu::Queue>,
surface: wgpu::Surface<'static>,
surface_format: wgpu::TextureFormat,
config: wgpu::SurfaceConfiguration,
}
impl Gpu {
pub fn new(
target: impl Into<wgpu::SurfaceTarget<'static>>,
size: (u32, u32),
config: GpuConfig,
) -> Result<Self, GpuInitError> {
let mut instance_desc = wgpu::InstanceDescriptor::new_without_display_handle();
instance_desc.backends = wgpu::Backends::PRIMARY;
let instance = wgpu::Instance::new(instance_desc);
let surface = instance
.create_surface(target)
.map_err(GpuInitError::CreateSurface)?;
let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::HighPerformance,
compatible_surface: Some(&surface),
force_fallback_adapter: false,
}))
.map_err(|_| GpuInitError::RequestAdapter)?;
let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
label: Some(config.label),
required_features: wgpu::Features::empty(),
required_limits: wgpu::Limits::default(),
memory_hints: wgpu::MemoryHints::Performance,
experimental_features: wgpu::ExperimentalFeatures::default(),
trace: wgpu::Trace::Off,
}))
.map_err(|_| GpuInitError::RequestDevice)?;
let caps = surface.get_capabilities(&adapter);
let surface_format = pick_surface_format(&caps.formats)?;
let alpha_mode = pick_alpha_mode(&caps.alpha_modes)?;
let mut usage = wgpu::TextureUsages::RENDER_ATTACHMENT;
let has_copy_src = caps.usages.contains(wgpu::TextureUsages::COPY_SRC);
match config.copy_src {
CopySrc::Never => {}
CopySrc::IfSupported => {
if has_copy_src {
usage |= wgpu::TextureUsages::COPY_SRC;
}
}
CopySrc::Required => {
if !has_copy_src {
return Err(GpuInitError::CopySrcUnsupported);
}
usage |= wgpu::TextureUsages::COPY_SRC;
}
}
let (cfg_w, cfg_h) =
clamp_surface_size(size.0, size.1, device.limits().max_texture_dimension_2d);
let surface_config = wgpu::SurfaceConfiguration {
usage,
format: surface_format,
width: cfg_w,
height: cfg_h,
present_mode: config.present_mode,
desired_maximum_frame_latency: 2,
alpha_mode,
view_formats: vec![],
};
surface.configure(&device, &surface_config);
Ok(Self {
device: Arc::new(device),
queue: Arc::new(queue),
surface,
surface_format,
config: surface_config,
})
}
pub fn device(&self) -> &Arc<wgpu::Device> {
&self.device
}
pub fn queue(&self) -> &Arc<wgpu::Queue> {
&self.queue
}
pub fn surface(&self) -> &wgpu::Surface<'static> {
&self.surface
}
pub fn surface_format(&self) -> wgpu::TextureFormat {
self.surface_format
}
pub fn config(&self) -> &wgpu::SurfaceConfiguration {
&self.config
}
pub fn resize(&mut self, w: u32, h: u32) {
if w == 0 || h == 0 {
return;
}
let (w, h) = clamp_surface_size(w, h, self.device.limits().max_texture_dimension_2d);
self.config.width = w;
self.config.height = h;
self.surface.configure(&self.device, &self.config);
}
pub fn acquire_frame(&self, request_redraw: impl Fn()) -> Option<wgpu::SurfaceTexture> {
use wgpu::CurrentSurfaceTexture as T;
let first = self.surface.get_current_texture();
match surface_acquire_action(&first) {
SurfaceAcquire::Present => match first {
T::Success(f) | T::Suboptimal(f) => Some(f),
_ => None,
},
SurfaceAcquire::Skip => None,
SurfaceAcquire::SkipAndRetry => {
request_redraw();
None
}
SurfaceAcquire::Reconfigure => {
self.surface.configure(&self.device, &self.config);
match self.surface.get_current_texture() {
T::Success(f) | T::Suboptimal(f) => Some(f),
_ => {
request_redraw();
None
}
}
}
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
#[non_exhaustive]
pub enum SurfaceAcquire {
Present,
Reconfigure,
SkipAndRetry,
Skip,
}
pub fn surface_acquire_action(status: &wgpu::CurrentSurfaceTexture) -> SurfaceAcquire {
use wgpu::CurrentSurfaceTexture as T;
match status {
T::Success(_) | T::Suboptimal(_) => SurfaceAcquire::Present,
T::Outdated | T::Lost => SurfaceAcquire::Reconfigure,
T::Timeout => SurfaceAcquire::SkipAndRetry,
T::Occluded | T::Validation => SurfaceAcquire::Skip,
}
}
#[cfg(test)]
mod tests {
use super::{
clamp_surface_size, pick_alpha_mode, pick_surface_format, surface_acquire_action,
GpuInitError, SurfaceAcquire,
};
use wgpu::CurrentSurfaceTexture as T;
#[test]
fn stale_swapchain_reconfigures_instead_of_skipping() {
assert_eq!(
surface_acquire_action(&T::Outdated),
SurfaceAcquire::Reconfigure
);
assert_eq!(
surface_acquire_action(&T::Lost),
SurfaceAcquire::Reconfigure
);
}
#[test]
fn timeout_skips_the_frame_but_asks_for_another() {
assert_eq!(
surface_acquire_action(&T::Timeout),
SurfaceAcquire::SkipAndRetry
);
}
#[test]
fn occluded_and_validation_skip_without_self_requested_redraw() {
assert_eq!(surface_acquire_action(&T::Occluded), SurfaceAcquire::Skip);
assert_eq!(surface_acquire_action(&T::Validation), SurfaceAcquire::Skip);
}
#[test]
fn empty_capability_lists_are_an_error_not_a_panic() {
assert!(matches!(
pick_surface_format(&[]),
Err(GpuInitError::NoSurfaceFormats)
));
assert!(matches!(
pick_alpha_mode(&[]),
Err(GpuInitError::NoAlphaModes)
));
}
#[test]
fn non_srgb_format_is_preferred_and_first_is_the_fallback() {
use wgpu::TextureFormat as F;
assert_eq!(
pick_surface_format(&[F::Bgra8UnormSrgb, F::Bgra8Unorm]).unwrap(),
F::Bgra8Unorm
);
assert_eq!(
pick_surface_format(&[F::Bgra8UnormSrgb, F::Rgba8UnormSrgb]).unwrap(),
F::Bgra8UnormSrgb
);
}
#[test]
fn alpha_mode_takes_the_surface_preference() {
use wgpu::CompositeAlphaMode as A;
assert_eq!(
pick_alpha_mode(&[A::Opaque, A::PreMultiplied]).unwrap(),
A::Opaque
);
}
#[test]
fn surface_size_clamped_to_max_dim() {
assert_eq!(clamp_surface_size(10224, 5925, 8192), (8192, 5925));
assert_eq!(clamp_surface_size(0, 0, 8192), (1, 1));
assert_eq!(clamp_surface_size(1280, 720, 8192), (1280, 720));
assert_eq!(clamp_surface_size(100, 100, 0), (1, 1));
}
}