use std::sync::{Arc, Mutex};
use std::time::Duration;
use crate::surface_retry::{ConfigureRetry, DEFAULT_RETRY_BUDGET};
#[path = "gpu_acquire.rs"]
mod acquire;
pub use acquire::{FrameAcquire, RetryWake, SurfaceError};
#[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,
pub optional_features: wgpu::Features,
pub surface_retry_budget: Duration,
}
impl Default for GpuConfig {
fn default() -> Self {
Self {
label: "agg-gui-wgpu",
copy_src: CopySrc::Never,
present_mode: wgpu::PresentMode::AutoVsync,
optional_features: wgpu::Features::empty(),
surface_retry_budget: DEFAULT_RETRY_BUDGET,
}
}
}
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
}
pub fn with_optional_features(mut self, features: wgpu::Features) -> Self {
self.optional_features = features;
self
}
pub fn with_surface_retry_budget(mut self, budget: Duration) -> Self {
self.surface_retry_budget = budget;
self
}
}
#[derive(Debug)]
#[non_exhaustive]
pub enum GpuInitError {
CreateSurface(wgpu::CreateSurfaceError),
RequestAdapter,
RequestDevice,
CopySrcUnsupported,
NoSurfaceFormats,
NoAlphaModes,
ConfigureSurface(String),
}
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"),
Self::ConfigureSurface(e) => write!(f, "configure wgpu surface: {e}"),
}
}
}
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 pick_present_mode(
supported: &[wgpu::PresentMode],
requested: wgpu::PresentMode,
) -> wgpu::PresentMode {
use wgpu::PresentMode as P;
match requested {
P::AutoVsync | P::AutoNoVsync => requested,
explicit if supported.contains(&explicit) => explicit,
_ => P::Fifo,
}
}
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,
device_lost: Arc<std::sync::atomic::AtomicBool>,
retry: Mutex<acquire::RetryState>,
retry_budget: Duration,
}
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: config.optional_features & adapter.features(),
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 device_lost = Arc::new(std::sync::atomic::AtomicBool::new(false));
{
let flag = Arc::clone(&device_lost);
device.set_device_lost_callback(move |reason, _message| {
if reason != wgpu::DeviceLostReason::Destroyed {
flag.store(true, std::sync::atomic::Ordering::Relaxed);
}
});
}
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: pick_present_mode(&caps.present_modes, config.present_mode),
desired_maximum_frame_latency: 2,
alpha_mode,
view_formats: vec![],
};
acquire::try_configure(&device, &surface, &surface_config, &device_lost)
.map_err(GpuInitError::ConfigureSurface)?;
Ok(Self {
device: Arc::new(device),
queue: Arc::new(queue),
surface,
surface_format,
config: surface_config,
device_lost,
retry: Mutex::new(acquire::RetryState::new(ConfigureRetry::configured())),
retry_budget: config.surface_retry_budget,
})
}
pub fn device_lost(&self) -> bool {
self.device_lost.load(std::sync::atomic::Ordering::Relaxed)
}
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.configure_and_record(web_time::Instant::now());
}
}
#[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_present_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 unsupported_present_mode_falls_back_to_fifo() {
use wgpu::PresentMode as P;
assert_eq!(pick_present_mode(&[P::Fifo], P::Mailbox), P::Fifo);
assert_eq!(pick_present_mode(&[P::Fifo], P::Immediate), P::Fifo);
assert_eq!(
pick_present_mode(&[P::Fifo, P::Mailbox], P::Mailbox),
P::Mailbox
);
assert_eq!(pick_present_mode(&[P::Fifo], P::AutoVsync), P::AutoVsync);
assert_eq!(
pick_present_mode(&[P::Fifo], P::AutoNoVsync),
P::AutoNoVsync
);
assert_eq!(pick_present_mode(&[], P::Immediate), P::Fifo);
}
#[test]
fn optional_features_default_empty_and_build() {
let cfg = super::GpuConfig::new("t");
assert_eq!(cfg.optional_features, wgpu::Features::empty());
let cfg = cfg.with_optional_features(wgpu::Features::FLOAT32_BLENDABLE);
assert_eq!(cfg.optional_features, wgpu::Features::FLOAT32_BLENDABLE);
assert_eq!(
cfg.optional_features & wgpu::Features::empty(),
wgpu::Features::empty()
);
}
#[test]
fn surface_retry_budget_defaults_to_ten_seconds_and_builds() {
use std::time::Duration;
let cfg = super::GpuConfig::new("t");
assert_eq!(cfg.surface_retry_budget, Duration::from_secs(10));
let cfg = cfg.with_surface_retry_budget(Duration::from_secs(3));
assert_eq!(cfg.surface_retry_budget, Duration::from_secs(3));
}
#[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));
}
}