use core::ffi::c_void;
use core::ptr::NonNull;
use anyhow::{Result, anyhow};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SurfacePhase {
NoSurface,
SurfaceReady,
SurfaceLost,
}
impl SurfacePhase {
pub fn can_render(self) -> bool {
matches!(self, SurfacePhase::SurfaceReady)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SurfaceEvent {
Created,
Destroyed,
Lost,
}
pub fn next_phase(_current: SurfacePhase, event: SurfaceEvent) -> SurfacePhase {
match event {
SurfaceEvent::Created => SurfacePhase::SurfaceReady,
SurfaceEvent::Destroyed => SurfacePhase::NoSurface,
SurfaceEvent::Lost => SurfacePhase::SurfaceLost,
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum AcquireStatus {
Usable,
Outdated,
Lost,
Transient,
Invalid,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum AcquireAction {
Present,
Reconfigure,
Lose,
Skip,
}
pub const MAX_INVALID_RECONFIGURES: u8 = 3;
pub fn decide_acquire(status: AcquireStatus, consecutive_invalid: u8) -> AcquireAction {
match status {
AcquireStatus::Usable => AcquireAction::Present,
AcquireStatus::Outdated => AcquireAction::Reconfigure,
AcquireStatus::Lost => AcquireAction::Lose,
AcquireStatus::Transient => AcquireAction::Skip,
AcquireStatus::Invalid => {
if consecutive_invalid < MAX_INVALID_RECONFIGURES {
AcquireAction::Reconfigure
} else {
AcquireAction::Lose
}
}
}
}
pub fn next_invalid_streak(status: AcquireStatus, action: AcquireAction, current: u8) -> u8 {
match (status, action) {
(AcquireStatus::Usable, _) => 0,
(AcquireStatus::Invalid, AcquireAction::Reconfigure) => current.saturating_add(1),
(AcquireStatus::Invalid, AcquireAction::Lose) => 0,
_ => current,
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum FrameOutcome {
Rendered,
Skipped,
Redraw,
SurfaceLost,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum EncodeOutcome {
Encoded,
Skipped,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum AcquireOutcome {
Acquired,
Reconfigured,
Lost,
Skipped,
}
pub unsafe fn create_android_surface(
instance: &wgpu::Instance,
window_ptr: *mut c_void,
) -> Result<wgpu::Surface<'static>> {
use wgpu::rwh::{
AndroidDisplayHandle, AndroidNdkWindowHandle, RawDisplayHandle, RawWindowHandle,
};
let window =
NonNull::new(window_ptr).ok_or_else(|| anyhow!("frust-gpu: null ANativeWindow pointer"))?;
let target = wgpu::SurfaceTargetUnsafe::RawHandle {
raw_display_handle: Some(RawDisplayHandle::Android(AndroidDisplayHandle::new())),
raw_window_handle: RawWindowHandle::AndroidNdk(AndroidNdkWindowHandle::new(window)),
};
let surface = unsafe { instance.create_surface_unsafe(target) }
.map_err(|e| anyhow!("frust-gpu: failed to create Android surface: {e}"))?;
Ok(surface)
}
#[cfg(any(target_os = "ios", target_os = "macos"))]
pub unsafe fn create_metal_surface(
instance: &wgpu::Instance,
layer_ptr: *mut c_void,
) -> Result<wgpu::Surface<'static>> {
if layer_ptr.is_null() {
return Err(anyhow!("frust-gpu: null CAMetalLayer pointer"));
}
let target = wgpu::SurfaceTargetUnsafe::CoreAnimationLayer(layer_ptr);
let surface = unsafe { instance.create_surface_unsafe(target) }
.map_err(|e| anyhow!("frust-gpu: failed to create Metal surface: {e}"))?;
Ok(surface)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn created_always_lands_in_surface_ready() {
for phase in [
SurfacePhase::NoSurface,
SurfacePhase::SurfaceReady,
SurfacePhase::SurfaceLost,
] {
assert_eq!(
next_phase(phase, SurfaceEvent::Created),
SurfacePhase::SurfaceReady,
"creating (or recreating) a surface must reach SurfaceReady from {phase:?}"
);
}
}
#[test]
fn destroyed_always_lands_in_no_surface() {
for phase in [
SurfacePhase::NoSurface,
SurfacePhase::SurfaceReady,
SurfacePhase::SurfaceLost,
] {
assert_eq!(
next_phase(phase, SurfaceEvent::Destroyed),
SurfacePhase::NoSurface,
"destroying a surface must reach NoSurface from {phase:?}"
);
}
}
#[test]
fn lost_transitions_ready_to_surface_lost() {
assert_eq!(
next_phase(SurfacePhase::SurfaceReady, SurfaceEvent::Lost),
SurfacePhase::SurfaceLost
);
}
#[test]
fn only_surface_ready_can_render() {
assert!(SurfacePhase::SurfaceReady.can_render());
assert!(!SurfacePhase::NoSurface.can_render());
assert!(!SurfacePhase::SurfaceLost.can_render());
}
#[test]
fn acquire_policy_maps_each_status_to_its_action() {
assert_eq!(
decide_acquire(AcquireStatus::Usable, 0),
AcquireAction::Present
);
assert_eq!(
decide_acquire(AcquireStatus::Outdated, 0),
AcquireAction::Reconfigure
);
assert_eq!(decide_acquire(AcquireStatus::Lost, 0), AcquireAction::Lose);
assert_eq!(
decide_acquire(AcquireStatus::Transient, 0),
AcquireAction::Skip
);
assert_eq!(
decide_acquire(AcquireStatus::Invalid, 0),
AcquireAction::Reconfigure
);
}
#[test]
fn invalid_reconfigures_under_the_cap() {
for consecutive_invalid in 0..MAX_INVALID_RECONFIGURES {
assert_eq!(
decide_acquire(AcquireStatus::Invalid, consecutive_invalid),
AcquireAction::Reconfigure,
"expected Reconfigure at consecutive_invalid={consecutive_invalid}"
);
}
}
#[test]
fn invalid_gives_up_at_the_cap() {
assert_eq!(
decide_acquire(AcquireStatus::Invalid, MAX_INVALID_RECONFIGURES),
AcquireAction::Lose,
"at the cap the machine must give up and drop to SurfaceLost"
);
assert_eq!(
decide_acquire(AcquireStatus::Invalid, u8::MAX),
AcquireAction::Lose
);
}
#[test]
fn invalid_streak_resets_on_successful_acquire() {
assert_eq!(
next_invalid_streak(AcquireStatus::Usable, AcquireAction::Present, 2),
0
);
assert_eq!(
next_invalid_streak(AcquireStatus::Usable, AcquireAction::Present, u8::MAX),
0
);
}
#[test]
fn invalid_streak_increments_while_reconfiguring() {
assert_eq!(
next_invalid_streak(AcquireStatus::Invalid, AcquireAction::Reconfigure, 0),
1
);
assert_eq!(
next_invalid_streak(
AcquireStatus::Invalid,
AcquireAction::Reconfigure,
MAX_INVALID_RECONFIGURES - 1
),
MAX_INVALID_RECONFIGURES
);
assert_eq!(
next_invalid_streak(AcquireStatus::Invalid, AcquireAction::Reconfigure, u8::MAX),
u8::MAX
);
}
#[test]
fn invalid_streak_resets_on_giving_up() {
assert_eq!(
next_invalid_streak(
AcquireStatus::Invalid,
AcquireAction::Lose,
MAX_INVALID_RECONFIGURES
),
0
);
}
#[test]
fn invalid_streak_untouched_by_unrelated_status_action_pairs() {
assert_eq!(
next_invalid_streak(AcquireStatus::Outdated, AcquireAction::Reconfigure, 2),
2
);
assert_eq!(
next_invalid_streak(AcquireStatus::Lost, AcquireAction::Lose, 2),
2
);
assert_eq!(
next_invalid_streak(AcquireStatus::Transient, AcquireAction::Skip, 2),
2
);
}
#[test]
fn non_invalid_statuses_are_unaffected_by_the_counter() {
for consecutive_invalid in [0, 1, MAX_INVALID_RECONFIGURES, u8::MAX] {
assert_eq!(
decide_acquire(AcquireStatus::Usable, consecutive_invalid),
AcquireAction::Present
);
assert_eq!(
decide_acquire(AcquireStatus::Outdated, consecutive_invalid),
AcquireAction::Reconfigure
);
assert_eq!(
decide_acquire(AcquireStatus::Lost, consecutive_invalid),
AcquireAction::Lose
);
assert_eq!(
decide_acquire(AcquireStatus::Transient, consecutive_invalid),
AcquireAction::Skip
);
}
}
}