use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::MutexGuard;
use std::time::Duration;
use web_time::Instant;
use super::{surface_acquire_action, Gpu, SurfaceAcquire};
use crate::surface_retry::{ConfigureRetry, RetryAction, RetryLog};
#[derive(Debug)]
#[non_exhaustive]
pub enum FrameAcquire {
Frame(wgpu::SurfaceTexture),
Skip(RetryWake),
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
#[non_exhaustive]
pub enum RetryWake {
Now,
After(Duration),
OnEvent,
}
#[derive(Clone, PartialEq, Eq, Debug)]
#[non_exhaustive]
pub enum SurfaceError {
#[non_exhaustive]
ConfigureRetriesExhausted {
last_error: String,
unconfigured_for: Duration,
attempts: u32,
},
}
impl std::fmt::Display for SurfaceError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ConfigureRetriesExhausted {
last_error,
unconfigured_for,
attempts,
} => write!(
f,
"wgpu surface could not be configured for {:.1} s \
({attempts} failed attempts); last error: {last_error}",
unconfigured_for.as_secs_f64()
),
}
}
}
impl std::error::Error for SurfaceError {}
#[derive(Debug)]
pub(super) struct RetryState {
policy: ConfigureRetry,
last_error: String,
}
impl RetryState {
pub(super) fn new(policy: ConfigureRetry) -> Self {
Self {
policy,
last_error: String::new(),
}
}
}
pub(super) fn try_configure(
device: &wgpu::Device,
surface: &wgpu::Surface<'_>,
config: &wgpu::SurfaceConfiguration,
device_lost: &AtomicBool,
) -> Result<(), String> {
let validation = device.push_error_scope(wgpu::ErrorFilter::Validation);
let internal = device.push_error_scope(wgpu::ErrorFilter::Internal);
surface.configure(device, config);
let internal_err = pollster::block_on(internal.pop());
let validation_err = pollster::block_on(validation.pop());
if let Some(err) = validation_err.or(internal_err) {
return Err(err.to_string());
}
if device_lost.load(Ordering::Relaxed) {
return Err("device lost".to_owned());
}
Ok(())
}
fn status_wake(action: SurfaceAcquire) -> Option<RetryWake> {
match action {
SurfaceAcquire::SkipAndRetry => Some(RetryWake::Now),
SurfaceAcquire::Skip => Some(RetryWake::OnEvent),
SurfaceAcquire::Present | SurfaceAcquire::Reconfigure => None,
}
}
fn unconfigured_outcome(
action: RetryAction,
state: &RetryState,
) -> Result<FrameAcquire, SurfaceError> {
let wake = match action {
RetryAction::Configured | RetryAction::TryNow => RetryWake::Now,
RetryAction::WaitFor(d) => RetryWake::After(d),
RetryAction::GiveUp { unconfigured_for } => {
return Err(SurfaceError::ConfigureRetriesExhausted {
last_error: state.last_error.clone(),
unconfigured_for,
attempts: state.policy.failures(),
})
}
};
Ok(FrameAcquire::Skip(wake))
}
impl Gpu {
fn retry_state(&self) -> MutexGuard<'_, RetryState> {
self.retry.lock().unwrap_or_else(|p| p.into_inner())
}
pub(super) fn configure_and_record(&self, now: Instant) -> bool {
let result = try_configure(&self.device, &self.surface, &self.config, &self.device_lost);
let ok = result.is_ok();
let mut state = self.retry_state();
let log = state.policy.record(ok, now);
state.last_error = result.err().unwrap_or_default();
let (w, h) = (self.config.width, self.config.height);
let err = &state.last_error;
match log {
RetryLog::None => {}
RetryLog::FirstFailure => log::warn!(
"agg-gui-wgpu: surface configure at {w}x{h} failed, retrying with backoff \
(budget {:?}): {err}",
self.retry_budget
),
RetryLog::RepeatFailure => log::debug!(
"agg-gui-wgpu: surface configure at {w}x{h} failed again ({} in a row): {err}",
state.policy.failures()
),
RetryLog::Recovered { failures } => log::info!(
"agg-gui-wgpu: surface configured at {w}x{h} after {failures} failed attempt(s)"
),
}
ok
}
pub fn surface_configured(&self) -> bool {
self.retry_state().policy.is_configured()
}
fn skip_unconfigured(&self, now: Instant) -> Result<FrameAcquire, SurfaceError> {
let state = self.retry_state();
unconfigured_outcome(state.policy.attempt(now, self.retry_budget), &state)
}
pub fn try_acquire_frame(&self) -> Result<FrameAcquire, SurfaceError> {
use wgpu::CurrentSurfaceTexture as T;
let now = Instant::now();
let action = self.retry_state().policy.attempt(now, self.retry_budget);
match action {
RetryAction::Configured => {}
RetryAction::TryNow => {
if !self.configure_and_record(now) {
return self.skip_unconfigured(now);
}
}
RetryAction::WaitFor(_) | RetryAction::GiveUp { .. } => {
return self.skip_unconfigured(now);
}
}
let first = self.surface.get_current_texture();
let action = surface_acquire_action(&first);
if let Some(wake) = status_wake(action) {
return Ok(FrameAcquire::Skip(wake));
}
match action {
SurfaceAcquire::Reconfigure => {
if !self.configure_and_record(now) {
return self.skip_unconfigured(now);
}
match self.surface.get_current_texture() {
T::Success(f) | T::Suboptimal(f) => Ok(FrameAcquire::Frame(f)),
_ => Ok(FrameAcquire::Skip(RetryWake::Now)),
}
}
_ => match first {
T::Success(f) | T::Suboptimal(f) => Ok(FrameAcquire::Frame(f)),
_ => Ok(FrameAcquire::Skip(RetryWake::Now)),
},
}
}
pub fn acquire_frame(&self, request_redraw: impl Fn()) -> Option<wgpu::SurfaceTexture> {
match self.try_acquire_frame() {
Ok(FrameAcquire::Frame(frame)) => Some(frame),
Ok(FrameAcquire::Skip(wake)) => {
match wake {
RetryWake::Now => request_redraw(),
RetryWake::After(d) => agg_gui::animation::request_draw_after(d),
RetryWake::OnEvent => {}
}
None
}
Err(e) => panic!("agg-gui-wgpu: {e}"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn gpu_stays_send_and_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<Gpu>();
}
#[test]
fn skip_statuses_keep_their_wake_semantics() {
assert_eq!(
status_wake(SurfaceAcquire::SkipAndRetry),
Some(RetryWake::Now)
);
assert_eq!(status_wake(SurfaceAcquire::Skip), Some(RetryWake::OnEvent));
assert_eq!(status_wake(SurfaceAcquire::Present), None);
assert_eq!(status_wake(SurfaceAcquire::Reconfigure), None);
}
fn state(failures: u32, last_error: &str) -> RetryState {
let mut policy = ConfigureRetry::default();
for _ in 0..failures {
policy.record(false, Instant::now());
}
RetryState {
policy,
last_error: last_error.to_owned(),
}
}
#[test]
fn backing_off_surface_skips_with_the_matching_wake() {
let s = state(1, "");
assert!(matches!(
unconfigured_outcome(RetryAction::TryNow, &s),
Ok(FrameAcquire::Skip(RetryWake::Now))
));
let d = Duration::from_millis(200);
assert!(matches!(
unconfigured_outcome(RetryAction::WaitFor(d), &s),
Ok(FrameAcquire::Skip(RetryWake::After(got))) if got == d
));
}
#[test]
fn exhausted_budget_is_an_error_carrying_the_last_wgpu_text() {
let s = state(7, "DXGI_ERROR_INVALID_CALL");
let span = Duration::from_millis(10_500);
let err = match unconfigured_outcome(
RetryAction::GiveUp {
unconfigured_for: span,
},
&s,
) {
Err(e) => e,
Ok(other) => panic!("expected an error, got {other:?}"),
};
assert_eq!(
err,
SurfaceError::ConfigureRetriesExhausted {
last_error: "DXGI_ERROR_INVALID_CALL".to_owned(),
unconfigured_for: span,
attempts: 7,
}
);
let text = err.to_string();
assert!(text.contains("10.5 s"), "{text}");
assert!(text.contains("7 failed attempts"), "{text}");
assert!(text.contains("DXGI_ERROR_INVALID_CALL"), "{text}");
}
}