use std::any::Any;
use std::ffi::c_void;
use std::fmt;
use std::panic::AssertUnwindSafe;
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use std::sync::{Arc, LazyLock, Mutex, MutexGuard, PoisonError};
use frust_gpu::SceneTextureId;
use crate::external_pass::{
ExternalFrame, ExternalPass, register_texture_source_pass, unregister_external_pass,
};
pub const MAX_RETIRING: usize = 4;
static NEXT_SOURCE: AtomicU64 = AtomicU64::new(1);
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum SourceFormat {
Rgba8,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum NativeHandleKind {
CvPixelBuffer,
AHardwareBuffer,
}
#[derive(Debug, PartialEq, Eq)]
pub struct NativeHandle {
kind: NativeHandleKind,
ptr: *mut c_void,
}
impl NativeHandle {
#[must_use]
pub unsafe fn new(kind: NativeHandleKind, ptr: *mut c_void) -> Self {
Self { kind, ptr }
}
#[must_use]
pub fn kind(&self) -> NativeHandleKind {
self.kind
}
#[must_use]
pub fn ptr(&self) -> *mut c_void {
self.ptr
}
}
unsafe impl Send for NativeHandle {}
#[derive(Debug, PartialEq, Eq)]
pub enum SourcePayload {
Cpu(Arc<[u8]>),
Native(NativeHandle),
}
pub type ReleaseCallback = Box<dyn FnOnce() + Send>;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct SourceMeta {
pub width: u32,
pub height: u32,
pub format: SourceFormat,
}
pub struct SourceFrame {
pub width: u32,
pub height: u32,
pub format: SourceFormat,
payload: SourcePayload,
release: Option<ReleaseCallback>,
}
static EMPTY_PIXELS: LazyLock<Arc<[u8]>> = LazyLock::new(|| Arc::from([]));
impl SourceFrame {
#[must_use]
pub fn new(width: u32, height: u32, format: SourceFormat, payload: SourcePayload) -> Self {
Self {
width,
height,
format,
payload,
release: None,
}
}
#[must_use]
pub fn rgba8(width: u32, height: u32, pixels: Arc<[u8]>) -> Self {
Self::new(
width,
height,
SourceFormat::Rgba8,
SourcePayload::Cpu(pixels),
)
}
#[must_use]
pub fn on_release(mut self, release: impl FnOnce() + Send + 'static) -> Self {
if let Some(displaced) = self.release.replace(Box::new(release)) {
run_release(displaced);
}
self
}
#[must_use]
pub fn meta(&self) -> SourceMeta {
SourceMeta {
width: self.width,
height: self.height,
format: self.format,
}
}
#[must_use]
pub fn payload(&self) -> &SourcePayload {
&self.payload
}
#[must_use]
pub fn has_release(&self) -> bool {
self.release.is_some()
}
#[must_use]
pub unsafe fn into_parts(mut self) -> (SourceMeta, SourcePayload, Option<ReleaseCallback>) {
let meta = self.meta();
let release = self.release.take();
let payload =
std::mem::replace(&mut self.payload, SourcePayload::Cpu(EMPTY_PIXELS.clone()));
(meta, payload, release)
}
pub fn release(mut self) {
if let Some(release) = self.release.take() {
run_release(release);
}
}
}
impl Drop for SourceFrame {
fn drop(&mut self) {
if let Some(release) = self.release.take() {
run_release(release);
}
}
}
impl fmt::Debug for SourceFrame {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SourceFrame")
.field("width", &self.width)
.field("height", &self.height)
.field("format", &self.format)
.field("payload", &self.payload)
.field("release", &self.release.is_some())
.finish()
}
}
#[derive(Debug)]
pub struct ImportedTexture {
pub view: wgpu::TextureView,
pub size: (u32, u32),
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum ImportError {
Unsupported(String),
Failed(String),
}
impl fmt::Display for ImportError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Unsupported(reason) => write!(f, "native import unsupported: {reason}"),
Self::Failed(reason) => write!(f, "native import failed: {reason}"),
}
}
}
impl std::error::Error for ImportError {}
pub trait NativeImporter: Send + Sync {
fn import(
&self,
handle: &NativeHandle,
device: &wgpu::Device,
queue: &wgpu::Queue,
) -> Result<ImportedTexture, ImportError>;
}
static IMPORTERS: Mutex<Vec<(NativeHandleKind, Arc<dyn NativeImporter>)>> = Mutex::new(Vec::new());
fn importers() -> MutexGuard<'static, Vec<(NativeHandleKind, Arc<dyn NativeImporter>)>> {
IMPORTERS.lock().unwrap_or_else(PoisonError::into_inner)
}
pub fn set_native_importer(
kind: NativeHandleKind,
importer: Arc<dyn NativeImporter>,
) -> Option<Arc<dyn NativeImporter>> {
let mut importers = importers();
match importers.iter_mut().find(|(k, _)| *k == kind) {
Some((_, slot)) => Some(std::mem::replace(slot, importer)),
None => {
importers.push((kind, importer));
None
}
}
}
pub fn clear_native_importer(kind: NativeHandleKind) -> Option<Arc<dyn NativeImporter>> {
let mut importers = importers();
let index = importers.iter().position(|(k, _)| *k == kind)?;
Some(importers.remove(index).1)
}
#[must_use]
pub fn native_importer_installed(kind: NativeHandleKind) -> bool {
native_importer(kind).is_some()
}
fn native_importer(kind: NativeHandleKind) -> Option<Arc<dyn NativeImporter>> {
importers()
.iter()
.find(|(k, _)| *k == kind)
.map(|(_, importer)| Arc::clone(importer))
}
#[must_use = "dropping the registration unregisters the frame source at once"]
pub struct TextureRegistration {
id: SceneTextureId,
status: Arc<Status>,
}
impl TextureRegistration {
#[must_use]
pub fn scene_texture_id(&self) -> u64 {
self.id.get()
}
#[must_use]
pub fn frame_size(&self) -> Option<(u32, u32)> {
unpack(self.status.size.load(Ordering::Acquire))
}
}
impl Drop for TextureRegistration {
fn drop(&mut self) {
unregister_external_pass(self.id);
}
}
impl fmt::Debug for TextureRegistration {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("TextureRegistration")
.field("scene_texture_id", &self.id.get())
.field("frame_size", &self.frame_size())
.finish()
}
}
pub fn register_texture_source(
source: impl FnMut() -> Option<SourceFrame> + Send + 'static,
) -> TextureRegistration {
let id = SceneTextureId::for_texture_source(NEXT_SOURCE.fetch_add(1, Ordering::Relaxed));
let status = Arc::new(Status::default());
let pass = SourcePass {
state: Mutex::new(PassState {
source: Box::new(source),
shown: None,
retiring: Arc::new(AtomicUsize::new(0)),
warned: Warned::default(),
}),
status: Arc::clone(&status),
};
let registered = register_texture_source_pass(id, Arc::new(pass));
debug_assert!(
registered,
"a fresh texture-source id is never already claimed"
);
TextureRegistration { id, status }
}
#[derive(Default)]
struct Status {
size: AtomicU64,
}
fn pack((width, height): (u32, u32)) -> u64 {
(u64::from(width) << 32) | u64::from(height)
}
fn unpack(packed: u64) -> Option<(u32, u32)> {
(packed != 0).then_some(((packed >> 32) as u32, packed as u32))
}
type FrameSource = Box<dyn FnMut() -> Option<SourceFrame> + Send>;
struct SourcePass {
state: Mutex<PassState>,
status: Arc<Status>,
}
struct PassState {
source: FrameSource,
shown: Option<Shown>,
retiring: Arc<AtomicUsize>,
warned: Warned,
}
struct Shown {
device: wgpu::Device,
view: wgpu::TextureView,
size: (u32, u32),
upload: Option<wgpu::Texture>,
native: Option<NativeHold>,
}
type Fence = Arc<dyn Fn(Box<dyn FnOnce() + Send>) + Send + Sync>;
fn queue_fence(queue: &wgpu::Queue) -> Fence {
let queue = queue.clone();
Arc::new(move |callback| queue.on_submitted_work_done(callback))
}
struct Retiring {
keep: Option<Box<dyn Any + Send>>,
release: Option<ReleaseCallback>,
pending: Arc<AtomicUsize>,
}
impl Drop for Retiring {
fn drop(&mut self) {
drop(self.keep.take());
self.pending.fetch_sub(1, Ordering::AcqRel);
if let Some(release) = self.release.take() {
run_release(release);
}
}
}
fn run_release(release: ReleaseCallback) {
if std::panic::catch_unwind(AssertUnwindSafe(release)).is_err() {
log::error!("texture source: a frame release callback panicked and was caught");
}
}
fn retire(
keep: Box<dyn Any + Send>,
release: Option<ReleaseCallback>,
pending: &Arc<AtomicUsize>,
fence: &Fence,
hops: u32,
) {
pending.fetch_add(1, Ordering::AcqRel);
let token = Retiring {
keep: Some(keep),
release,
pending: Arc::clone(pending),
};
defer(token, Arc::clone(fence), hops);
}
fn defer(token: Retiring, fence: Fence, hops: u32) {
let next = Arc::clone(&fence);
fence(Box::new(move || {
if hops > 1 {
defer(token, next, hops - 1);
} else {
drop(token);
}
}));
}
struct NativeHold {
view: Option<wgpu::TextureView>,
release: Option<ReleaseCallback>,
queue: wgpu::Queue,
pending: Arc<AtomicUsize>,
}
impl NativeHold {
fn retire_replaced(mut self) {
self.retire_with(1);
}
fn retire_with(&mut self, hops: u32) {
if let Some(view) = self.view.take() {
retire(
Box::new(view),
self.release.take(),
&self.pending,
&queue_fence(&self.queue),
hops,
);
}
}
}
impl Drop for NativeHold {
fn drop(&mut self) {
self.retire_with(2);
}
}
fn replace_shown(state: &mut PassState, new: Shown) {
if let Some(mut old) = state.shown.replace(new)
&& let Some(hold) = old.native.take()
{
hold.retire_replaced();
}
}
#[derive(Default)]
struct Warned {
malformed: bool,
missing_importer: bool,
import_failed: bool,
backlog: bool,
}
fn report(flag: &mut bool, id: u64, message: fmt::Arguments<'_>) {
if *flag {
log::debug!("texture source {id}: {message}");
} else {
*flag = true;
log::warn!(
"texture source {id}: {message} (further refusals of this kind are logged at \
debug level)"
);
}
}
impl ExternalPass for SourcePass {
fn record(&self, frame: &mut ExternalFrame<'_>) {
let mut state = self.state.lock().unwrap_or_else(PoisonError::into_inner);
let state = &mut *state;
if state
.shown
.as_ref()
.is_some_and(|shown| shown.device != *frame.device())
{
let old_device = state.shown.as_ref().map(|shown| shown.device.clone());
state.shown = None;
if let Some(old_device) = old_device {
for _ in 0..2 {
let _ = old_device.poll(wgpu::PollType::wait_indefinitely());
}
}
}
let mut rebind = false;
if let Some(source_frame) = (state.source)() {
let (meta, payload, release) = unsafe { source_frame.into_parts() };
let release = ReleaseGuard(release);
rebind = match payload {
SourcePayload::Cpu(pixels) => {
let size = (meta.width, meta.height);
let changed = upload(state, frame, size, meta.format, &pixels);
release.release();
changed
}
SourcePayload::Native(handle) => import(state, frame, handle, release),
};
}
if let Some(shown) = &state.shown {
if rebind || !frame.has_binding() {
frame.bind_texture(shown.size, shown.view.clone());
}
self.status.size.store(pack(shown.size), Ordering::Release);
}
}
}
fn upload(
state: &mut PassState,
frame: &mut ExternalFrame<'_>,
size: (u32, u32),
format: SourceFormat,
pixels: &[u8],
) -> bool {
let id = frame.id().get();
let (width, height) = size;
let SourceFormat::Rgba8 = format;
let max = frame.device().limits().max_texture_dimension_2d;
if width == 0 || height == 0 || width > max || height > max {
report(
&mut state.warned.malformed,
id,
format_args!("refused a {width}x{height} frame: the extent is empty or past {max}"),
);
return false;
}
let expected = u64::from(width) * u64::from(height) * 4;
if pixels.len() as u64 != expected {
report(
&mut state.warned.malformed,
id,
format_args!(
"refused a {width}x{height} RGBA8 frame of {} bytes: tightly packed rows need \
{expected}",
pixels.len()
),
);
return false;
}
let reusable = state
.shown
.as_ref()
.and_then(|shown| shown.upload.as_ref().filter(|_| shown.size == size));
let (texture, changed) = match reusable {
Some(texture) => (texture.clone(), false),
None => {
let texture = frame.device().create_texture(&wgpu::TextureDescriptor {
label: Some("frust texture-bus frame"),
size: wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8Unorm,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
(texture, true)
}
};
frame.queue().write_texture(
wgpu::TexelCopyTextureInfo {
texture: &texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
pixels,
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(width * 4),
rows_per_image: Some(height),
},
wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
);
if changed {
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
replace_shown(
state,
Shown {
device: frame.device().clone(),
view,
size,
upload: Some(texture),
native: None,
},
);
}
changed
}
struct ReleaseGuard(Option<ReleaseCallback>);
impl ReleaseGuard {
fn release(mut self) {
if let Some(release) = self.0.take() {
run_release(release);
}
}
fn take(mut self) -> Option<ReleaseCallback> {
self.0.take()
}
}
impl Drop for ReleaseGuard {
fn drop(&mut self) {
if let Some(release) = self.0.take() {
run_release(release);
}
}
}
fn refuse(release: ReleaseGuard) -> bool {
release.release();
false
}
fn backlog_full(pending: &AtomicUsize, poll: impl FnOnce()) -> bool {
if pending.load(Ordering::Acquire) < MAX_RETIRING {
return false;
}
poll();
pending.load(Ordering::Acquire) >= MAX_RETIRING
}
fn backlog_refuses(
state: &mut PassState,
id: u64,
kind: NativeHandleKind,
poll: impl FnOnce(),
) -> bool {
if !backlog_full(&state.retiring, poll) {
return false;
}
report(
&mut state.warned.backlog,
id,
format_args!(
"refused a {kind:?} frame: {MAX_RETIRING} retired frames still wait on the GPU"
),
);
true
}
fn import(
state: &mut PassState,
frame: &mut ExternalFrame<'_>,
handle: NativeHandle,
release: ReleaseGuard,
) -> bool {
let id = frame.id().get();
let Some(importer) = native_importer(handle.kind()) else {
report(
&mut state.warned.missing_importer,
id,
format_args!(
"refused a {:?} frame: no native importer is installed for that kind",
handle.kind()
),
);
return refuse(release);
};
if backlog_refuses(state, id, handle.kind(), || {
let _ = frame.device().poll(wgpu::PollType::Poll);
}) {
return refuse(release);
}
match importer.import(&handle, frame.device(), frame.queue()) {
Ok(imported) => {
let hold = NativeHold {
view: Some(imported.view.clone()),
release: release.take(),
queue: frame.queue().clone(),
pending: Arc::clone(&state.retiring),
};
replace_shown(
state,
Shown {
device: frame.device().clone(),
view: imported.view,
size: imported.size,
upload: None,
native: Some(hold),
},
);
true
}
Err(error) => {
report(
&mut state.warned.import_failed,
id,
format_args!("refused a {:?} frame: {error}", handle.kind()),
);
refuse(release)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_packed_extent_round_trips_and_zero_means_none() {
assert_eq!(unpack(0), None);
assert_eq!(unpack(pack((64, 32))), Some((64, 32)));
assert_eq!(unpack(pack((u32::MAX, 1))), Some((u32::MAX, 1)));
}
#[test]
fn a_source_frame_releases_once_and_reports_its_callback_in_debug() {
let released = Arc::new(AtomicU64::new(0));
let counter = Arc::clone(&released);
let frame = SourceFrame::rgba8(1, 1, Arc::from(vec![0u8; 4])).on_release(move || {
counter.fetch_add(1, Ordering::SeqCst);
});
assert!(format!("{frame:?}").contains("release: true"));
frame.release();
assert_eq!(released.load(Ordering::SeqCst), 1);
}
#[test]
fn import_errors_say_which_kind_of_failure() {
assert!(
ImportError::Unsupported("no extension".into())
.to_string()
.contains("unsupported")
);
assert!(
ImportError::Failed("bad format".into())
.to_string()
.contains("bad format")
);
}
struct NoImport;
impl NativeImporter for NoImport {
fn import(
&self,
_handle: &NativeHandle,
_device: &wgpu::Device,
_queue: &wgpu::Queue,
) -> Result<ImportedTexture, ImportError> {
Err(ImportError::Unsupported("test importer".into()))
}
}
#[test]
fn the_installed_query_follows_set_and_clear_per_kind() {
let kind = NativeHandleKind::CvPixelBuffer;
assert!(!native_importer_installed(kind));
assert!(set_native_importer(kind, Arc::new(NoImport)).is_none());
assert!(native_importer_installed(kind));
assert!(!native_importer_installed(
NativeHandleKind::AHardwareBuffer
));
assert!(clear_native_importer(kind).is_some());
assert!(!native_importer_installed(kind));
}
type Parked = Vec<Box<dyn FnOnce() + Send>>;
#[derive(Clone, Default)]
struct FakeFence(Arc<Mutex<Parked>>);
impl FakeFence {
fn fence(&self) -> Fence {
let parked = Arc::clone(&self.0);
Arc::new(move |callback| parked.lock().unwrap().push(callback))
}
fn fire(&self) {
let batch = std::mem::take(&mut *self.0.lock().unwrap());
for callback in batch {
callback();
}
}
}
fn retire_counting(
fake: &FakeFence,
pending: &Arc<AtomicUsize>,
released: &Arc<AtomicU64>,
hops: u32,
) {
let released = Arc::clone(released);
retire(
Box::new(()),
Some(Box::new(move || {
released.fetch_add(1, Ordering::SeqCst);
})),
pending,
&fake.fence(),
hops,
);
}
#[test]
fn a_retired_release_waits_for_its_fence_and_runs_once() {
let fake = FakeFence::default();
let pending = Arc::new(AtomicUsize::new(0));
let released = Arc::new(AtomicU64::new(0));
retire_counting(&fake, &pending, &released, 1);
assert_eq!(pending.load(Ordering::SeqCst), 1);
assert_eq!(released.load(Ordering::SeqCst), 0);
fake.fire();
assert_eq!(released.load(Ordering::SeqCst), 1);
assert_eq!(pending.load(Ordering::SeqCst), 0);
fake.fire();
assert_eq!(released.load(Ordering::SeqCst), 1);
}
#[test]
fn a_chained_retire_needs_a_fence_per_hop() {
let fake = FakeFence::default();
let pending = Arc::new(AtomicUsize::new(0));
let released = Arc::new(AtomicU64::new(0));
retire_counting(&fake, &pending, &released, 2);
fake.fire();
assert_eq!(
released.load(Ordering::SeqCst),
0,
"one fence is not enough"
);
assert_eq!(pending.load(Ordering::SeqCst), 1);
fake.fire();
assert_eq!(released.load(Ordering::SeqCst), 1);
assert_eq!(pending.load(Ordering::SeqCst), 0);
}
fn idle_state() -> PassState {
PassState {
source: Box::new(|| None),
shown: None,
retiring: Arc::new(AtomicUsize::new(0)),
warned: Warned::default(),
}
}
fn panicking_release() -> (ReleaseCallback, Arc<AtomicU64>) {
let ran = Arc::new(AtomicU64::new(0));
let counter = Arc::clone(&ran);
let callback: ReleaseCallback = Box::new(move || {
counter.fetch_add(1, Ordering::SeqCst);
panic!("the release callback panics");
});
(callback, ran)
}
fn counted_frame(released: &Arc<AtomicU64>) -> SourceFrame {
let released = Arc::clone(released);
SourceFrame::rgba8(1, 1, Arc::from(vec![0u8; 4])).on_release(move || {
released.fetch_add(1, Ordering::SeqCst);
})
}
#[test]
fn a_native_handle_round_trips_its_kind_and_pointer() {
let mut byte = 0u8;
let ptr: *mut c_void = (&raw mut byte).cast();
for kind in [
NativeHandleKind::CvPixelBuffer,
NativeHandleKind::AHardwareBuffer,
] {
let handle = unsafe { NativeHandle::new(kind, ptr) };
assert_eq!(handle.kind(), kind);
assert_eq!(handle.ptr(), ptr);
}
}
#[test]
fn a_native_handle_and_payload_are_move_only() {
trait Probe {
fn is_clone(&self) -> bool {
false
}
}
impl<T> Probe for T {}
struct Wrap<T>(std::marker::PhantomData<T>);
impl<T: Clone> Wrap<T> {
fn is_clone(&self) -> bool {
true
}
}
assert!(!Wrap::<NativeHandle>(std::marker::PhantomData).is_clone());
assert!(!Wrap::<SourcePayload>(std::marker::PhantomData).is_clone());
assert!(Wrap::<Arc<[u8]>>(std::marker::PhantomData).is_clone());
}
#[test]
fn into_parts_hands_over_the_callback_and_disarms_the_drop() {
let released = Arc::new(AtomicU64::new(0));
let (meta, payload, release) = unsafe { counted_frame(&released).into_parts() };
assert_eq!((meta.width, meta.height), (1, 1));
assert!(matches!(&payload, SourcePayload::Cpu(p) if p.len() == 4));
assert_eq!(released.load(Ordering::SeqCst), 0, "consuming ran nothing");
release.expect("the callback moved out")();
assert_eq!(released.load(Ordering::SeqCst), 1);
}
#[test]
fn a_source_frame_dropped_unconsumed_releases_once() {
let released = Arc::new(AtomicU64::new(0));
drop(counted_frame(&released));
assert_eq!(released.load(Ordering::SeqCst), 1);
let frame = counted_frame(&released);
assert!(frame.has_release());
let (_, _, held) = unsafe { frame.into_parts() };
assert_eq!(released.load(Ordering::SeqCst), 1);
held.expect("a callback is attached")();
assert_eq!(released.load(Ordering::SeqCst), 2);
}
#[test]
fn a_panicking_source_frame_release_does_not_unwind_out_of_a_drop() {
let (callback, ran) = panicking_release();
let frame = SourceFrame::rgba8(1, 1, Arc::from(vec![0u8; 4])).on_release(callback);
let outcome = std::panic::catch_unwind(AssertUnwindSafe(move || drop(frame)));
assert!(outcome.is_ok(), "the panic was caught inside the drop");
assert_eq!(ran.load(Ordering::SeqCst), 1, "the callback ran once");
}
#[test]
fn a_panicking_explicit_source_frame_release_does_not_propagate() {
let (callback, ran) = panicking_release();
let frame = SourceFrame::rgba8(1, 1, Arc::from(vec![0u8; 4])).on_release(callback);
let outcome = std::panic::catch_unwind(AssertUnwindSafe(move || frame.release()));
assert!(outcome.is_ok());
assert_eq!(ran.load(Ordering::SeqCst), 1, "the callback ran once");
}
#[test]
fn a_panicking_retired_release_does_not_propagate() {
let (callback, ran) = panicking_release();
let pending = Arc::new(AtomicUsize::new(1));
let retiring = Retiring {
keep: None,
release: Some(callback),
pending: Arc::clone(&pending),
};
let outcome = std::panic::catch_unwind(AssertUnwindSafe(move || drop(retiring)));
assert!(outcome.is_ok());
assert_eq!(ran.load(Ordering::SeqCst), 1, "the callback ran once");
assert_eq!(pending.load(Ordering::SeqCst), 0, "the backlog slot freed");
}
#[test]
fn a_panicking_bus_guard_release_does_not_propagate() {
let (callback, ran) = panicking_release();
let guard = ReleaseGuard(Some(callback));
let outcome = std::panic::catch_unwind(AssertUnwindSafe(move || guard.release()));
assert!(outcome.is_ok());
assert_eq!(ran.load(Ordering::SeqCst), 1, "the callback ran once");
let (callback, ran) = panicking_release();
let guard = ReleaseGuard(Some(callback));
let outcome = std::panic::catch_unwind(AssertUnwindSafe(move || drop(guard)));
assert!(outcome.is_ok());
assert_eq!(ran.load(Ordering::SeqCst), 1, "the callback ran once");
}
#[test]
fn a_source_frame_lost_to_a_panicking_pass_releases_once() {
let released = Arc::new(AtomicU64::new(0));
let mut source: FrameSource = {
let released = Arc::clone(&released);
Box::new(move || Some(counted_frame(&released)))
};
let unwound = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let _frame = source().expect("the source hands a frame over");
panic!("the pass panics while it holds the frame");
}));
assert!(unwound.is_err());
assert_eq!(released.load(Ordering::SeqCst), 1);
}
#[test]
fn the_retire_backlog_tolerates_four_and_refuses_the_fifth_once_warned() {
let fake = FakeFence::default();
let mut state = idle_state();
let retired = Arc::new(AtomicU64::new(0));
let polls = AtomicUsize::new(0);
let poll = || {
polls.fetch_add(1, Ordering::SeqCst);
};
for _ in 0..MAX_RETIRING {
assert!(!backlog_refuses(
&mut state,
7,
NativeHandleKind::AHardwareBuffer,
poll
));
retire_counting(&fake, &state.retiring, &retired, 1);
}
assert_eq!(state.retiring.load(Ordering::SeqCst), MAX_RETIRING);
assert_eq!(polls.load(Ordering::SeqCst), 0, "no poll below the bound");
assert!(!state.warned.backlog);
let released = Arc::new(AtomicU64::new(0));
let fifth = counted_frame(&released);
assert!(backlog_refuses(
&mut state,
7,
NativeHandleKind::AHardwareBuffer,
poll
));
assert!(
!refuse(ReleaseGuard(unsafe { fifth.into_parts() }.2)),
"a refusal never changes the shown view"
);
assert_eq!(released.load(Ordering::SeqCst), 1, "released at once");
assert_eq!(polls.load(Ordering::SeqCst), 1, "fences got one poll");
assert!(state.warned.backlog, "the first refusal is the warning");
assert!(state.shown.is_none(), "the shown view is untouched");
assert_eq!(retired.load(Ordering::SeqCst), 0);
assert!(backlog_refuses(
&mut state,
7,
NativeHandleKind::AHardwareBuffer,
poll
));
assert!(state.warned.backlog);
fake.fire();
assert_eq!(retired.load(Ordering::SeqCst), MAX_RETIRING as u64);
assert!(!backlog_refuses(
&mut state,
7,
NativeHandleKind::AHardwareBuffer,
poll
));
}
#[test]
fn a_poll_that_fires_a_fence_admits_the_frame() {
let fake = FakeFence::default();
let pending = Arc::new(AtomicUsize::new(0));
let retired = Arc::new(AtomicU64::new(0));
for _ in 0..MAX_RETIRING {
retire_counting(&fake, &pending, &retired, 1);
}
assert!(backlog_full(&pending, || {}));
assert!(
!backlog_full(&pending, || fake.fire()),
"the poll ran the completed fences, so there is room again"
);
assert_eq!(pending.load(Ordering::SeqCst), 0);
}
#[test]
fn a_device_lost_mid_chain_still_releases_once() {
let fake = FakeFence::default();
let pending = Arc::new(AtomicUsize::new(0));
let released = Arc::new(AtomicU64::new(0));
retire_counting(&fake, &pending, &released, 2);
fake.fire();
assert_eq!(released.load(Ordering::SeqCst), 0, "one hop of two fired");
fake.0.lock().unwrap().clear();
assert_eq!(released.load(Ordering::SeqCst), 1);
assert_eq!(pending.load(Ordering::SeqCst), 0);
fake.fire();
assert_eq!(released.load(Ordering::SeqCst), 1);
}
#[test]
fn a_fence_dropped_unrun_still_releases() {
let fake = FakeFence::default();
let pending = Arc::new(AtomicUsize::new(0));
let released = Arc::new(AtomicU64::new(0));
retire_counting(&fake, &pending, &released, 1);
fake.0.lock().unwrap().clear();
assert_eq!(released.load(Ordering::SeqCst), 1);
assert_eq!(pending.load(Ordering::SeqCst), 0);
}
}
#[cfg(test)]
mod on_release_displacement {
use super::*;
use std::sync::atomic::{AtomicUsize, Ordering};
#[test]
fn on_release_runs_the_callback_it_displaces_exactly_once() {
let first = Arc::new(AtomicUsize::new(0));
let second = Arc::new(AtomicUsize::new(0));
let frame = SourceFrame::rgba8(1, 1, Arc::from([0u8; 4])).on_release({
let first = Arc::clone(&first);
move || {
first.fetch_add(1, Ordering::SeqCst);
}
});
let frame = frame.on_release({
let second = Arc::clone(&second);
move || {
second.fetch_add(1, Ordering::SeqCst);
}
});
assert_eq!(
first.load(Ordering::SeqCst),
1,
"the displaced callback ran at once"
);
assert_eq!(second.load(Ordering::SeqCst), 0);
drop(frame);
assert_eq!(first.load(Ordering::SeqCst), 1);
assert_eq!(
second.load(Ordering::SeqCst),
1,
"the attached callback ran on drop"
);
}
}