Skip to main content

frust_render/
texture_bus.rs

1//! The **consumer** half of the external-texture frame bus: a frame *source*
2//! — any closure that hands over the newest frame it has — registered here
3//! becomes a scene texture the display list draws by id.
4//!
5//! # Shape
6//!
7//! [`register_texture_source`] takes a closure returning
8//! `Option<`[`SourceFrame`]`>` and registers an
9//! [`ExternalPass`](crate::ExternalPass) for it under an id of its own (the
10//! reserved texture-source namespace, `SceneTextureId::for_texture_source`).
11//! Once per engine frame, ahead of the scene, the pass polls the closure:
12//!
13//! - `None` — nothing new; whatever was shown last stays shown.
14//! - a [`SourcePayload::Cpu`] frame — uploaded with `queue.write_texture` into
15//!   an `Rgba8Unorm` texture the pass owns (re-created only when the extent
16//!   or the device changes) and bound under the registration's id.
17//! - a [`SourcePayload::Native`] frame — handed to the [`NativeImporter`]
18//!   installed for its [`NativeHandleKind`] with [`set_native_importer`];
19//!   with none installed it is refused, warned about once per registration
20//!   and released.
21//!
22//! Every frame's [`SourceFrame::release`] callback runs exactly once, on every
23//! path — including a panic unwinding through the pass (a [`SourceFrame`]
24//! dropped still holding its callback runs it) and a device replaced by a
25//! newer one (the bus polls the old device until its retire fences fired
26//! before it lets go of it). A retire fence's callback holds its queue alive
27//! until it fires, so a device that is never polled again keeps its retiring
28//! frames' releases pending; a device lost outright releases them on its
29//! next poll. *When* depends on what the bus did with the payload:
30//!
31//! - a CPU frame — right after the upload is enqueued (the queue copies the
32//!   pixels into staging synchronously, so nothing reads the payload again);
33//! - a refused frame (malformed, no importer, import error, or the retire
34//!   backlog full) — right away, since nothing ever sampled it;
35//! - an imported native frame — not when the importer returns. The release is
36//!   the producer's signal that it may *reuse* the platform buffer (a camera
37//!   or decoder ring recycles on it), and the GPU samples the imported
38//!   texture until the frame is replaced and the submission that last drew it
39//!   has finished. So the release is held until both hold: the imported
40//!   texture was **retired** (a newer frame replaced it, or the registration
41//!   was dropped / the device changed) and a `Queue::on_submitted_work_done`
42//!   fence taken at that moment has fired (two chained fences for a dropped
43//!   registration — what those cover, and what they do not, is under "A
44//!   registration dropped mid-drain" below). It runs on whichever thread
45//!   polls the device, and the imported texture's view is dropped just
46//!   before it.
47//!
48//! An importer that needs the platform buffer alive for longer than its own
49//! call keeps its own reference (in the imported texture's lifetime,
50//! typically); the bus never releases that one.
51//!
52//! The returned [`TextureRegistration`] reports the id to draw by
53//! ([`TextureRegistration::scene_texture_id`], what a widget's
54//! `draw_scene_texture` names) and unregisters on drop.
55//!
56//! # Bounded retire backlog
57//!
58//! At most [`MAX_RETIRING`] (4) retired native textures per registration wait
59//! on their fence at once, so a producer with a 3-deep buffer ring never sees
60//! more than its ring in flight. When a native frame arrives with the backlog
61//! full (after a non-blocking `Device::poll` gave the fences a chance), the
62//! *newest* frame is dropped: its release runs at once, nothing was sampled,
63//! and the last good frame stays shown.
64//!
65//! # A registration dropped mid-drain
66//!
67//! A registration dropped (or a device changed) while its imported texture is
68//! still shown retires it through two chained fences: the second
69//! `on_submitted_work_done` is taken when the first fires. What the two hops
70//! guarantee is narrower than "the frame that sampled it finished": a fence
71//! fires once every submission made *before it was taken* has completed, and
72//! one taken with no work in flight fires at the next device poll. So the
73//! release waits for every submission made before the drop, plus a second
74//! completion point after that, and nothing more. A registration dropped
75//! mid-drain — between a frame's record, which bound the view, and that
76//! frame's submit — may release its last native frame while the frame that
77//! sampled it is still executing (or not yet submitted): an interim submit
78//! (atlas growth, another registration's backlog poll) can satisfy both hops
79//! first. The producer may then overwrite its buffer and that one frame
80//! tear. It is not a memory-safety hole — the importer holds its own
81//! reference to the platform buffer for the imported texture's lifetime, and
82//! `wgpu` keeps the texture alive for every submission that uses it — only a
83//! possible visual glitch on the last frame of a dropped registration. Tying
84//! the release to the dropping frame's own submission is follow-up work.
85//!
86//! # Why a closure, not a plugin type
87//!
88//! The renderer never depends on the plugin substrate, and the substrate never
89//! names the renderer, so neither can hand the other its frame type. The
90//! types here are this crate's own plain-data twins of the substrate's
91//! producer-side ones, with matching constructors and accessors; the bridge is
92//! a closure written by code that depends on both (a plugin's facade-side
93//! half, or the app):
94//!
95//! ```ignore
96//! let registration = register_texture_source(move || {
97//!     let frame = sink.take_latest()?;
98//!     // Check what can be refused while the frame still guards its release.
99//!     if frame.meta().format != TextureFormat::Rgba8 {
100//!         frame.release();
101//!         return None;
102//!     }
103//!     // SAFETY: the callback is moved straight into the `SourceFrame` built
104//!     // below, with the handle it protects, and nothing fallible runs in
105//!     // between.
106//!     let (meta, payload, release) = unsafe { frame.into_parts() };
107//!     let payload = match payload {
108//!         FramePayload::Cpu(pixels) => SourcePayload::Cpu(pixels),
109//!         // SAFETY: forwards the producer's own `NativeHandle::new`
110//!         // contract. The producer-side handle was consumed by value, so
111//!         // this one is the only handle for the buffer, and it travels
112//!         // with the release callback attached below.
113//!         FramePayload::Native(h) => SourcePayload::Native(unsafe {
114//!             NativeHandle::new(/* the matching NativeHandleKind */, h.ptr())
115//!         }),
116//!     };
117//!     let mut out = SourceFrame::new(meta.width, meta.height, SourceFormat::Rgba8, payload);
118//!     if let Some(release) = release {
119//!         out = out.on_release(release);
120//!     }
121//!     Some(out)
122//! });
123//! ```
124//!
125//! Both frame types release on drop, so the bridge consumes the producer
126//! frame by value and moves its payload and callback across together. The
127//! handle is move-only, so it cannot be copied, and a frame's payload and
128//! release are private, so outside `unsafe` a handle cannot leave the frame
129//! that carries its release: the bridge's `into_parts` is the `unsafe` step
130//! that promises the callback is carried on.
131//!
132//! # Pixels and compositing
133//!
134//! A CPU frame is tightly packed premultiplied-alpha RGBA8, `width * 4` bytes
135//! per row, top row first, sampled as `Rgba8Unorm` (bytes pass through
136//! unconverted). The engine composites the bound texture blended, never as
137//! opaque — it cannot read the texels to know (`docs/LIMITATIONS.md`'s
138//! `engine-scene-texture-always-blended`), and the bus inherits that.
139//!
140//! # Where it runs
141//!
142//! Exactly where external passes run: every engine-tier `SurfaceRenderer`
143//! frame drains them, so a registration shows on the desktop, Android, iOS
144//! and web shells alike, and nowhere a surface is not presenting. A shell that
145//! rebuilds its engine on the same device (a fresh surface) gets the current
146//! view bound again on its first frame; a *device* change drops what was
147//! shown until the source yields its next frame.
148
149use std::any::Any;
150use std::ffi::c_void;
151use std::fmt;
152use std::panic::AssertUnwindSafe;
153use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
154use std::sync::{Arc, LazyLock, Mutex, MutexGuard, PoisonError};
155
156use frust_gpu::SceneTextureId;
157
158use crate::external_pass::{
159    ExternalFrame, ExternalPass, register_texture_source_pass, unregister_external_pass,
160};
161
162/// How many retired native textures one registration lets wait on their fence
163/// at once (see the module docs).
164pub const MAX_RETIRING: usize = 4;
165
166/// The ordinal behind each registration's texture-source id.
167static NEXT_SOURCE: AtomicU64 = AtomicU64::new(1);
168
169/// The pixel layout of a [`SourceFrame`].
170///
171/// Non-exhaustive, mirroring the producer side: `Nv12` is the reserved next
172/// variant, added once the bus can convert it.
173#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
174#[non_exhaustive]
175pub enum SourceFormat {
176    /// 8-bit premultiplied RGBA, tightly packed (see the module docs).
177    Rgba8,
178}
179
180/// Which platform object a [`NativeHandle`] points at — the key a
181/// [`NativeImporter`] is installed under.
182#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
183pub enum NativeHandleKind {
184    /// An Apple `CVPixelBufferRef`.
185    CvPixelBuffer,
186    /// An Android `AHardwareBuffer*`.
187    AHardwareBuffer,
188}
189
190/// An opaque platform GPU buffer a [`NativeImporter`] turns into a texture.
191///
192/// The bus never dereferences the pointer; only the importer installed for
193/// its kind does. The frame's producer owns the buffer's lifetime and drops
194/// its own reference in the frame's release callback.
195///
196/// The fields are private and the only constructor is the `unsafe`
197/// [`Self::new`]: an importer dereferences the pointer on that constructor's
198/// contract alone, so safe code cannot hand the bus a forged one.
199///
200/// A handle is move-only — neither `Clone` nor `Copy` — so it cannot be
201/// copied, and a frame's payload and release callback are private to the
202/// frame: outside `unsafe`, a handle cannot leave the frame that carries its
203/// release (see [`SourceFrame::into_parts`]).
204#[derive(Debug, PartialEq, Eq)]
205pub struct NativeHandle {
206    /// The platform object `ptr` points at.
207    kind: NativeHandleKind,
208    /// The platform object itself.
209    ptr: *mut c_void,
210}
211
212impl NativeHandle {
213    /// Wraps `ptr`, a platform object of `kind`, for a
214    /// [`SourcePayload::Native`] frame.
215    ///
216    /// # Safety
217    ///
218    /// `ptr` is a live platform object of exactly `kind` — a
219    /// `CVPixelBufferRef` for [`NativeHandleKind::CvPixelBuffer`], an
220    /// `AHardwareBuffer*` for [`NativeHandleKind::AHardwareBuffer`] — on which
221    /// the producer holds a retained reference of its own (`CFRetain`,
222    /// `AHardwareBuffer_acquire`), kept until the release callback of the one
223    /// frame this handle is placed in has run (the handle is move-only, so it
224    /// cannot be copied, and outside `unsafe` it cannot leave that frame
225    /// without the callback). The [`NativeImporter`] installed
226    /// for `kind` dereferences the pointer (describes, imports and retains the
227    /// object) on this promise alone, so a dangling, foreign or mistyped
228    /// pointer is undefined behaviour there. A bridge from the plugin
229    /// substrate's own handle forwards that handle's identical contract.
230    #[must_use]
231    pub unsafe fn new(kind: NativeHandleKind, ptr: *mut c_void) -> Self {
232        Self { kind, ptr }
233    }
234
235    /// The platform object [`Self::ptr`] points at.
236    #[must_use]
237    pub fn kind(&self) -> NativeHandleKind {
238        self.kind
239    }
240
241    /// The platform object itself, as handed to [`Self::new`]. Only the handle
242    /// is move-only: an address copied out of it carries no lifetime guarantee
243    /// past the frame's release, and using it needs the caller's own `unsafe`.
244    #[must_use]
245    pub fn ptr(&self) -> *mut c_void {
246        self.ptr
247    }
248}
249
250// SAFETY: an inert address plus a tag — moving it between threads moves only
251// the address, and nothing here dereferences it. Both kinds it can name are
252// reference-counted platform objects that are safe to retain, release and
253// import from any thread, and the frame carrying the handle also carries the
254// producer's own reference (released by its callback), so the object outlives
255// the address's trip to the render thread.
256unsafe impl Send for NativeHandle {}
257
258/// Where a [`SourceFrame`]'s pixels live.
259///
260/// Not `Clone`: a native payload holds a move-only [`NativeHandle`].
261#[derive(Debug, PartialEq, Eq)]
262pub enum SourcePayload {
263    /// Pixels in CPU memory, laid out per the frame's [`SourceFormat`].
264    Cpu(Arc<[u8]>),
265    /// A platform GPU buffer, imported rather than uploaded.
266    Native(NativeHandle),
267}
268
269/// The callback a frame carries to tell its producer the bus is done with it.
270pub type ReleaseCallback = Box<dyn FnOnce() + Send>;
271
272/// The extent and pixel format of a [`SourceFrame`], as returned by
273/// [`SourceFrame::into_parts`].
274#[derive(Clone, Copy, Debug, PartialEq, Eq)]
275pub struct SourceMeta {
276    /// Width in pixels.
277    pub width: u32,
278    /// Height in pixels.
279    pub height: u32,
280    /// How the payload's pixels are laid out.
281    pub format: SourceFormat,
282}
283
284/// One frame a source hands the bus.
285///
286/// The extent and format are plain public fields; the payload and the release
287/// callback are private, so they stay together. Build a frame with
288/// [`Self::new`] or [`Self::rgba8`] and attach a callback with
289/// [`Self::on_release`]; read the payload in place with [`Self::payload`].
290///
291/// Dropping a frame that still holds its release callback runs it: the bus
292/// runs it for every frame it is handed (see the module docs), and a frame
293/// nobody released — dropped by a holder that never passed it on, or lost to
294/// a panic in the source or the pass — still releases, exactly once. Because
295/// the type implements `Drop`, it cannot be destructured; the bus splits it
296/// with the `unsafe` [`Self::into_parts`]. A release callback must not panic:
297/// the guard around it only applies to panic=unwind builds (see
298/// [`run_release`]).
299pub struct SourceFrame {
300    /// Width in pixels.
301    pub width: u32,
302    /// Height in pixels.
303    pub height: u32,
304    /// How the payload's pixels are laid out.
305    pub format: SourceFormat,
306    /// The pixels, or the platform buffer holding them.
307    payload: SourcePayload,
308    /// Run exactly once when the bus is done with the frame: right after the
309    /// upload for a CPU frame, at once for a refused one, and for an imported
310    /// native frame only after its texture was replaced or unbound and the GPU
311    /// work that sampled it finished — the producer may reuse the platform
312    /// buffer from this point on (see the module docs). A frame dropped while
313    /// still holding it runs it then.
314    release: Option<ReleaseCallback>,
315}
316
317/// The stand-in payload left behind by [`SourceFrame::into_parts`]. It is
318/// never read — the frame is dropped right after — and shared so splitting a
319/// frame allocates nothing.
320static EMPTY_PIXELS: LazyLock<Arc<[u8]>> = LazyLock::new(|| Arc::from([]));
321
322impl SourceFrame {
323    /// A frame of the given extent and format over `payload`, with no release
324    /// callback.
325    #[must_use]
326    pub fn new(width: u32, height: u32, format: SourceFormat, payload: SourcePayload) -> Self {
327        Self {
328            width,
329            height,
330            format,
331            payload,
332            release: None,
333        }
334    }
335
336    /// A CPU frame of tightly packed premultiplied RGBA8 with no release
337    /// callback.
338    #[must_use]
339    pub fn rgba8(width: u32, height: u32, pixels: Arc<[u8]>) -> Self {
340        Self::new(
341            width,
342            height,
343            SourceFormat::Rgba8,
344            SourcePayload::Cpu(pixels),
345        )
346    }
347
348    /// Attaches the callback run once the bus is done with the frame. A
349    /// callback already attached runs first (through the same guard as every
350    /// other release), so no callback is ever dropped unrun.
351    #[must_use]
352    pub fn on_release(mut self, release: impl FnOnce() + Send + 'static) -> Self {
353        if let Some(displaced) = self.release.replace(Box::new(release)) {
354            run_release(displaced);
355        }
356        self
357    }
358
359    /// The extent and format.
360    #[must_use]
361    pub fn meta(&self) -> SourceMeta {
362        SourceMeta {
363            width: self.width,
364            height: self.height,
365            format: self.format,
366        }
367    }
368
369    /// The pixels, or the platform buffer holding them, borrowed in place. The
370    /// frame still owns the payload and its release callback.
371    #[must_use]
372    pub fn payload(&self) -> &SourcePayload {
373        &self.payload
374    }
375
376    /// Whether a release callback is attached.
377    #[must_use]
378    pub fn has_release(&self) -> bool {
379        self.release.is_some()
380    }
381
382    /// Consumes the frame, handing over its extent and format, its payload and
383    /// its release callback, and disarms the drop guard — the one way to move
384    /// a native handle out of a frame. A caller that only needs to read the
385    /// frame uses [`Self::meta`] and [`Self::payload`] instead.
386    ///
387    /// # Safety
388    ///
389    /// The caller must run the returned release callback exactly once, after
390    /// the last use of the payload's native handle (or move it, with the
391    /// handle, into a holder that does), and must not place that handle into
392    /// another frame without it. The callback gives the producer's reference
393    /// to the buffer back; a handle used after it ran points at a buffer the
394    /// producer no longer keeps alive.
395    #[must_use]
396    pub unsafe fn into_parts(mut self) -> (SourceMeta, SourcePayload, Option<ReleaseCallback>) {
397        let meta = self.meta();
398        let release = self.release.take();
399        let payload =
400            std::mem::replace(&mut self.payload, SourcePayload::Cpu(EMPTY_PIXELS.clone()));
401        (meta, payload, release)
402    }
403
404    /// Runs the release callback, if any, consuming the frame — the same as
405    /// dropping it, spelled out.
406    pub fn release(mut self) {
407        if let Some(release) = self.release.take() {
408            run_release(release);
409        }
410    }
411}
412
413impl Drop for SourceFrame {
414    /// A frame dropped still holding its callback was released by no one, so
415    /// it releases itself; one whose callback already ran or was taken on
416    /// (into a held import) is `None` here.
417    fn drop(&mut self) {
418        if let Some(release) = self.release.take() {
419            run_release(release);
420        }
421    }
422}
423
424impl fmt::Debug for SourceFrame {
425    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
426        f.debug_struct("SourceFrame")
427            .field("width", &self.width)
428            .field("height", &self.height)
429            .field("format", &self.format)
430            .field("payload", &self.payload)
431            .field("release", &self.release.is_some())
432            .finish()
433    }
434}
435
436/// What a [`NativeImporter`] produced: a view the engine can sample, and its
437/// extent in texels.
438///
439/// `view` must be a non-array 2D view of a float-sampleable texture with
440/// `TEXTURE_BINDING` usage, created on the device the importer was handed —
441/// the same requirements as `ExternalFrame::bind_texture`.
442#[derive(Debug)]
443pub struct ImportedTexture {
444    /// The view bound under the registration's id.
445    pub view: wgpu::TextureView,
446    /// The view's extent in texels.
447    pub size: (u32, u32),
448}
449
450/// Why a [`NativeImporter`] could not import a handle. The frame is released
451/// and the last good frame stays shown.
452#[derive(Clone, Debug, PartialEq, Eq)]
453#[non_exhaustive]
454pub enum ImportError {
455    /// The device or backend cannot import this kind of buffer at all (the
456    /// wrong graphics API, a missing extension).
457    Unsupported(String),
458    /// This particular buffer could not be imported (an unsupported pixel
459    /// format, a driver error).
460    Failed(String),
461}
462
463impl fmt::Display for ImportError {
464    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
465        match self {
466            Self::Unsupported(reason) => write!(f, "native import unsupported: {reason}"),
467            Self::Failed(reason) => write!(f, "native import failed: {reason}"),
468        }
469    }
470}
471
472impl std::error::Error for ImportError {}
473
474/// Turns a platform GPU buffer into a texture on the frame's device, without
475/// a CPU copy.
476///
477/// Installed per [`NativeHandleKind`] with [`set_native_importer`], and called
478/// on the render thread inside the frame's external-pass drain — so it records
479/// nothing of its own and never submits; it creates (or looks up) a texture
480/// over the buffer and returns a view of it. `Send + Sync` because the table
481/// it is installed in is process-wide.
482pub trait NativeImporter: Send + Sync {
483    /// Imports `handle` on `device`. The bus holds the frame's release
484    /// callback until the imported texture is retired and the GPU finished
485    /// with it, but the platform buffer is still the importer's to keep alive:
486    /// an importer whose texture aliases it keeps its own reference for the
487    /// texture's lifetime.
488    ///
489    /// # Errors
490    ///
491    /// [`ImportError`] when the handle cannot be imported on this device.
492    fn import(
493        &self,
494        handle: &NativeHandle,
495        device: &wgpu::Device,
496        queue: &wgpu::Queue,
497    ) -> Result<ImportedTexture, ImportError>;
498}
499
500/// The installed importers, one per kind at most.
501static IMPORTERS: Mutex<Vec<(NativeHandleKind, Arc<dyn NativeImporter>)>> = Mutex::new(Vec::new());
502
503fn importers() -> MutexGuard<'static, Vec<(NativeHandleKind, Arc<dyn NativeImporter>)>> {
504    IMPORTERS.lock().unwrap_or_else(PoisonError::into_inner)
505}
506
507/// Installs `importer` for every [`SourcePayload::Native`] frame of `kind`,
508/// process-wide, returning the importer it replaced.
509///
510/// Takes effect from the next frame a registration is handed; frames of a
511/// kind with no importer are refused (see the module docs).
512pub fn set_native_importer(
513    kind: NativeHandleKind,
514    importer: Arc<dyn NativeImporter>,
515) -> Option<Arc<dyn NativeImporter>> {
516    let mut importers = importers();
517    match importers.iter_mut().find(|(k, _)| *k == kind) {
518        Some((_, slot)) => Some(std::mem::replace(slot, importer)),
519        None => {
520            importers.push((kind, importer));
521            None
522        }
523    }
524}
525
526/// Removes the importer installed for `kind`, returning it.
527pub fn clear_native_importer(kind: NativeHandleKind) -> Option<Arc<dyn NativeImporter>> {
528    let mut importers = importers();
529    let index = importers.iter().position(|(k, _)| *k == kind)?;
530    Some(importers.remove(index).1)
531}
532
533/// Whether an importer is installed for `kind` right now.
534///
535/// What a producer asks before choosing a payload: a [`SourcePayload::Native`]
536/// frame of a kind with no importer is refused, so a producer that can also
537/// copy into CPU memory sends [`SourcePayload::Cpu`] frames instead. The
538/// answer can change when an importer is installed or cleared later, so a
539/// producer asks once per session it opens rather than once per process.
540#[must_use]
541pub fn native_importer_installed(kind: NativeHandleKind) -> bool {
542    native_importer(kind).is_some()
543}
544
545fn native_importer(kind: NativeHandleKind) -> Option<Arc<dyn NativeImporter>> {
546    importers()
547        .iter()
548        .find(|(k, _)| *k == kind)
549        .map(|(_, importer)| Arc::clone(importer))
550}
551
552/// A frame source registered with [`register_texture_source`]; unregisters on
553/// drop.
554#[must_use = "dropping the registration unregisters the frame source at once"]
555pub struct TextureRegistration {
556    id: SceneTextureId,
557    status: Arc<Status>,
558}
559
560impl TextureRegistration {
561    /// The id a display list draws this source by —
562    /// `PaintScene::draw_scene_texture`'s / `texture_view`'s argument.
563    #[must_use]
564    pub fn scene_texture_id(&self) -> u64 {
565        self.id.get()
566    }
567
568    /// The extent of the frame currently shown, `None` before the first one.
569    ///
570    /// Read by an app that sizes or fits its view to the content (a video's
571    /// aspect ratio); updated on the render thread as frames land.
572    #[must_use]
573    pub fn frame_size(&self) -> Option<(u32, u32)> {
574        unpack(self.status.size.load(Ordering::Acquire))
575    }
576}
577
578impl Drop for TextureRegistration {
579    fn drop(&mut self) {
580        unregister_external_pass(self.id);
581    }
582}
583
584impl fmt::Debug for TextureRegistration {
585    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
586        f.debug_struct("TextureRegistration")
587            .field("scene_texture_id", &self.id.get())
588            .field("frame_size", &self.frame_size())
589            .finish()
590    }
591}
592
593/// Registers `source` as a scene texture, polled once per engine frame for
594/// its newest frame (see the module docs), and returns the registration that
595/// names it and keeps it alive.
596///
597/// `source` runs on the render thread with the frame's external-pass drain in
598/// progress: it should only hand over a frame it already has (take from a
599/// mailbox), never block for one. A source — or an importer — that panics is
600/// retired like any panicking external pass, and the frame in flight is not
601/// leaked: a [`SourceFrame`] (like the plugin substrate's twin) dropped while
602/// still holding its release callback runs it during the unwind.
603pub fn register_texture_source(
604    source: impl FnMut() -> Option<SourceFrame> + Send + 'static,
605) -> TextureRegistration {
606    let id = SceneTextureId::for_texture_source(NEXT_SOURCE.fetch_add(1, Ordering::Relaxed));
607    let status = Arc::new(Status::default());
608    let pass = SourcePass {
609        state: Mutex::new(PassState {
610            source: Box::new(source),
611            shown: None,
612            retiring: Arc::new(AtomicUsize::new(0)),
613            warned: Warned::default(),
614        }),
615        status: Arc::clone(&status),
616    };
617    let registered = register_texture_source_pass(id, Arc::new(pass));
618    debug_assert!(
619        registered,
620        "a fresh texture-source id is never already claimed"
621    );
622    TextureRegistration { id, status }
623}
624
625/// What a registration reports back across threads.
626#[derive(Default)]
627struct Status {
628    /// The shown frame's extent, packed by [`pack`]; `0` before the first.
629    size: AtomicU64,
630}
631
632fn pack((width, height): (u32, u32)) -> u64 {
633    (u64::from(width) << 32) | u64::from(height)
634}
635
636fn unpack(packed: u64) -> Option<(u32, u32)> {
637    // Both halves are non-zero for every extent the bus shows, so `0` is free
638    // to mean "nothing yet".
639    (packed != 0).then_some(((packed >> 32) as u32, packed as u32))
640}
641
642/// The boxed frame source a pass polls.
643type FrameSource = Box<dyn FnMut() -> Option<SourceFrame> + Send>;
644
645/// One registration's pass.
646struct SourcePass {
647    /// Serializes `record`: a pass may be called concurrently by two
648    /// engine-tier surfaces, and the source is `FnMut`.
649    state: Mutex<PassState>,
650    status: Arc<Status>,
651}
652
653struct PassState {
654    source: FrameSource,
655    /// What is shown now, kept so it can be bound again into a rebuilt engine.
656    shown: Option<Shown>,
657    /// Retired native textures still waiting on their fence.
658    retiring: Arc<AtomicUsize>,
659    warned: Warned,
660}
661
662/// The view a registration currently shows.
663struct Shown {
664    /// The device `view` was created on — compared against each frame's, as
665    /// the device-change check. Held only alongside the resources made on it
666    /// and replaced with them.
667    device: wgpu::Device,
668    view: wgpu::TextureView,
669    size: (u32, u32),
670    /// The bus's own upload texture behind `view`, reused by the next CPU
671    /// frame of the same extent; `None` for an imported view.
672    upload: Option<wgpu::Texture>,
673    /// The imported frame's release, held until the view is retired and
674    /// fenced; `None` for a bus upload.
675    native: Option<NativeHold>,
676}
677
678/// Completion fence: runs the callback once the work submitted so far finished.
679type Fence = Arc<dyn Fn(Box<dyn FnOnce() + Send>) + Send + Sync>;
680
681fn queue_fence(queue: &wgpu::Queue) -> Fence {
682    let queue = queue.clone();
683    Arc::new(move |callback| queue.on_submitted_work_done(callback))
684}
685
686/// A retired native texture on its way out: dropping it (when its fence fires,
687/// or unrun because the device is gone) frees the view and runs the release.
688struct Retiring {
689    keep: Option<Box<dyn Any + Send>>,
690    release: Option<ReleaseCallback>,
691    pending: Arc<AtomicUsize>,
692}
693
694impl Drop for Retiring {
695    fn drop(&mut self) {
696        drop(self.keep.take());
697        self.pending.fetch_sub(1, Ordering::AcqRel);
698        if let Some(release) = self.release.take() {
699            run_release(release);
700        }
701    }
702}
703
704/// Runs a frame's release callback behind `catch_unwind`, the last line of
705/// defence for a callback that panics anyway. Releases run from `Drop` impls
706/// (a retired texture's fence, a frame dropped unrun), sometimes during an
707/// unwind, and a panic escaping one there aborts the process. The guard only
708/// works in panic=unwind builds (dev and test): the shipped release profile
709/// sets `panic = "abort"`, where a panicking callback aborts the process and
710/// the must-not-panic rule is the only protection. A caught panic is logged;
711/// a callback must still not panic (see [`SourceFrame`]).
712fn run_release(release: ReleaseCallback) {
713    if std::panic::catch_unwind(AssertUnwindSafe(release)).is_err() {
714        log::error!("texture source: a frame release callback panicked and was caught");
715    }
716}
717
718/// Starts retiring `keep` and `release` behind `hops` successive fences; the
719/// second and later fences are taken from inside the previous one's callback.
720fn retire(
721    keep: Box<dyn Any + Send>,
722    release: Option<ReleaseCallback>,
723    pending: &Arc<AtomicUsize>,
724    fence: &Fence,
725    hops: u32,
726) {
727    pending.fetch_add(1, Ordering::AcqRel);
728    let token = Retiring {
729        keep: Some(keep),
730        release,
731        pending: Arc::clone(pending),
732    };
733    defer(token, Arc::clone(fence), hops);
734}
735
736fn defer(token: Retiring, fence: Fence, hops: u32) {
737    let next = Arc::clone(&fence);
738    fence(Box::new(move || {
739        if hops > 1 {
740            defer(token, next, hops - 1);
741        } else {
742            drop(token);
743        }
744    }));
745}
746
747/// What an imported view keeps until it is retired.
748struct NativeHold {
749    view: Option<wgpu::TextureView>,
750    release: Option<ReleaseCallback>,
751    queue: wgpu::Queue,
752    pending: Arc<AtomicUsize>,
753}
754
755impl NativeHold {
756    /// Retires on replacement: every submission that sampled the view
757    /// happened before now, so one fence covers them.
758    fn retire_replaced(mut self) {
759        self.retire_with(1);
760    }
761
762    fn retire_with(&mut self, hops: u32) {
763        if let Some(view) = self.view.take() {
764            retire(
765                Box::new(view),
766                self.release.take(),
767                &self.pending,
768                &queue_fence(&self.queue),
769                hops,
770            );
771        }
772    }
773}
774
775impl Drop for NativeHold {
776    /// Unbind (registration dropped, device change): chains two fences. They
777    /// cover every submission made before the drop, not a frame recorded
778    /// before it and submitted after — a drop mid-drain may release the
779    /// producer's buffer while that last frame still samples it (the module
780    /// docs' "A registration dropped mid-drain").
781    fn drop(&mut self) {
782        self.retire_with(2);
783    }
784}
785
786/// Installs `new` as the shown view, retiring a native one it replaces.
787fn replace_shown(state: &mut PassState, new: Shown) {
788    if let Some(mut old) = state.shown.replace(new)
789        && let Some(hold) = old.native.take()
790    {
791        hold.retire_replaced();
792    }
793}
794
795/// Which refusals a registration has already warned about; later ones of the
796/// same kind are logged at debug level.
797#[derive(Default)]
798struct Warned {
799    malformed: bool,
800    missing_importer: bool,
801    import_failed: bool,
802    backlog: bool,
803}
804
805/// Logs a refusal at `warn!` the first time `flag` sees it, `debug!` after.
806fn report(flag: &mut bool, id: u64, message: fmt::Arguments<'_>) {
807    if *flag {
808        log::debug!("texture source {id}: {message}");
809    } else {
810        *flag = true;
811        log::warn!(
812            "texture source {id}: {message} (further refusals of this kind are logged at \
813             debug level)"
814        );
815    }
816}
817
818impl ExternalPass for SourcePass {
819    fn record(&self, frame: &mut ExternalFrame<'_>) {
820        let mut state = self.state.lock().unwrap_or_else(PoisonError::into_inner);
821        let state = &mut *state;
822
823        // A view made on another device is useless here: drop it, and with it
824        // the upload texture, rather than bind across devices.
825        if state
826            .shown
827            .as_ref()
828            .is_some_and(|shown| shown.device != *frame.device())
829        {
830            let old_device = state.shown.as_ref().map(|shown| shown.device.clone());
831            state.shown = None;
832            // Dropping the view retired it on the OLD queue, whose fences fire
833            // only when that device is polled; nothing else will poll it again,
834            // so drain both hops here before the device is let go of.
835            if let Some(old_device) = old_device {
836                for _ in 0..2 {
837                    let _ = old_device.poll(wgpu::PollType::wait_indefinitely());
838                }
839            }
840        }
841
842        // The frame keeps its release callback until the bus is done with
843        // it, so a panic anywhere below still releases it (on drop).
844        let mut rebind = false;
845        if let Some(source_frame) = (state.source)() {
846            // SAFETY: the callback goes straight into a `ReleaseGuard`, which
847            // runs it on every path out (drop included) or hands it to an
848            // import that owes the call; the handle never leaves this scope
849            // apart from that import.
850            let (meta, payload, release) = unsafe { source_frame.into_parts() };
851            // Until the release callback is handed on, a panic below drops it
852            // with this frame's locals and so still runs it.
853            let release = ReleaseGuard(release);
854            rebind = match payload {
855                SourcePayload::Cpu(pixels) => {
856                    let size = (meta.width, meta.height);
857                    let changed = upload(state, frame, size, meta.format, &pixels);
858                    // The queue copied the pixels into staging: nothing reads
859                    // the payload again.
860                    release.release();
861                    changed
862                }
863                SourcePayload::Native(handle) => import(state, frame, handle, release),
864            };
865        }
866
867        if let Some(shown) = &state.shown {
868            // A changed view is bound; so is an unchanged one the engine no
869            // longer holds (a shell rebuilt it for a fresh surface).
870            if rebind || !frame.has_binding() {
871                frame.bind_texture(shown.size, shown.view.clone());
872            }
873            self.status.size.store(pack(shown.size), Ordering::Release);
874        }
875    }
876}
877
878/// Uploads one CPU frame, answering whether the shown view changed (and so
879/// must be bound). A malformed frame is refused and changes nothing.
880fn upload(
881    state: &mut PassState,
882    frame: &mut ExternalFrame<'_>,
883    size: (u32, u32),
884    format: SourceFormat,
885    pixels: &[u8],
886) -> bool {
887    let id = frame.id().get();
888    let (width, height) = size;
889    let SourceFormat::Rgba8 = format;
890    let max = frame.device().limits().max_texture_dimension_2d;
891    if width == 0 || height == 0 || width > max || height > max {
892        report(
893            &mut state.warned.malformed,
894            id,
895            format_args!("refused a {width}x{height} frame: the extent is empty or past {max}"),
896        );
897        return false;
898    }
899    let expected = u64::from(width) * u64::from(height) * 4;
900    if pixels.len() as u64 != expected {
901        report(
902            &mut state.warned.malformed,
903            id,
904            format_args!(
905                "refused a {width}x{height} RGBA8 frame of {} bytes: tightly packed rows need \
906                 {expected}",
907                pixels.len()
908            ),
909        );
910        return false;
911    }
912
913    let reusable = state
914        .shown
915        .as_ref()
916        .and_then(|shown| shown.upload.as_ref().filter(|_| shown.size == size));
917    let (texture, changed) = match reusable {
918        Some(texture) => (texture.clone(), false),
919        None => {
920            let texture = frame.device().create_texture(&wgpu::TextureDescriptor {
921                label: Some("frust texture-bus frame"),
922                size: wgpu::Extent3d {
923                    width,
924                    height,
925                    depth_or_array_layers: 1,
926                },
927                mip_level_count: 1,
928                sample_count: 1,
929                dimension: wgpu::TextureDimension::D2,
930                format: wgpu::TextureFormat::Rgba8Unorm,
931                usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
932                view_formats: &[],
933            });
934            (texture, true)
935        }
936    };
937
938    // Staged on the queue, so it lands ahead of the frame's own command
939    // buffer when the frame submits.
940    frame.queue().write_texture(
941        wgpu::TexelCopyTextureInfo {
942            texture: &texture,
943            mip_level: 0,
944            origin: wgpu::Origin3d::ZERO,
945            aspect: wgpu::TextureAspect::All,
946        },
947        pixels,
948        wgpu::TexelCopyBufferLayout {
949            offset: 0,
950            bytes_per_row: Some(width * 4),
951            rows_per_image: Some(height),
952        },
953        wgpu::Extent3d {
954            width,
955            height,
956            depth_or_array_layers: 1,
957        },
958    );
959
960    if changed {
961        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
962        replace_shown(
963            state,
964            Shown {
965                device: frame.device().clone(),
966                view,
967                size,
968                upload: Some(texture),
969                native: None,
970            },
971        );
972    }
973    changed
974}
975
976/// A frame's release callback, held between taking a frame apart and the
977/// point where the bus either runs it or moves it into an import. Dropped
978/// still holding the callback (a panic in between) it runs it, so the
979/// exactly-once promise of [`SourceFrame`] survives consuming the frame.
980struct ReleaseGuard(Option<ReleaseCallback>);
981
982impl ReleaseGuard {
983    /// Runs the callback now.
984    fn release(mut self) {
985        if let Some(release) = self.0.take() {
986            run_release(release);
987        }
988    }
989
990    /// Hands the callback on to a new holder, which now owes the call.
991    fn take(mut self) -> Option<ReleaseCallback> {
992        self.0.take()
993    }
994}
995
996impl Drop for ReleaseGuard {
997    fn drop(&mut self) {
998        if let Some(release) = self.0.take() {
999            run_release(release);
1000        }
1001    }
1002}
1003
1004/// Refuses a frame: nothing sampled it, so it is released at once. Answers
1005/// "the shown view did not change".
1006fn refuse(release: ReleaseGuard) -> bool {
1007    release.release();
1008    false
1009}
1010
1011/// Whether a native frame finds the retire backlog full: `pending` is at
1012/// [`MAX_RETIRING`] even after `poll` — a non-blocking device poll, run only
1013/// when the backlog looks full — gave completed fences their chance to run.
1014fn backlog_full(pending: &AtomicUsize, poll: impl FnOnce()) -> bool {
1015    if pending.load(Ordering::Acquire) < MAX_RETIRING {
1016        return false;
1017    }
1018    poll();
1019    pending.load(Ordering::Acquire) >= MAX_RETIRING
1020}
1021
1022/// Whether the retire backlog refuses a native frame of `kind` now (see
1023/// [`backlog_full`]), reporting the refusal — at `warn!` once per
1024/// registration. Touches nothing else: the shown view stays as it is.
1025fn backlog_refuses(
1026    state: &mut PassState,
1027    id: u64,
1028    kind: NativeHandleKind,
1029    poll: impl FnOnce(),
1030) -> bool {
1031    if !backlog_full(&state.retiring, poll) {
1032        return false;
1033    }
1034    report(
1035        &mut state.warned.backlog,
1036        id,
1037        format_args!(
1038            "refused a {kind:?} frame: {MAX_RETIRING} retired frames still wait on the GPU"
1039        ),
1040    );
1041    true
1042}
1043
1044/// Imports one native frame through the importer installed for its kind,
1045/// answering whether the shown view changed. Refused (and warned about once)
1046/// when no importer is installed, the retire backlog is full or the import
1047/// fails; the last good frame stays shown and the refused frame is released
1048/// at once. An imported frame's `release` is held until its view is retired
1049/// and fenced (see the module docs).
1050fn import(
1051    state: &mut PassState,
1052    frame: &mut ExternalFrame<'_>,
1053    handle: NativeHandle,
1054    release: ReleaseGuard,
1055) -> bool {
1056    let id = frame.id().get();
1057    let Some(importer) = native_importer(handle.kind()) else {
1058        report(
1059            &mut state.warned.missing_importer,
1060            id,
1061            format_args!(
1062                "refused a {:?} frame: no native importer is installed for that kind",
1063                handle.kind()
1064            ),
1065        );
1066        return refuse(release);
1067    };
1068    if backlog_refuses(state, id, handle.kind(), || {
1069        let _ = frame.device().poll(wgpu::PollType::Poll);
1070    }) {
1071        return refuse(release);
1072    }
1073    match importer.import(&handle, frame.device(), frame.queue()) {
1074        Ok(imported) => {
1075            let hold = NativeHold {
1076                view: Some(imported.view.clone()),
1077                release: release.take(),
1078                queue: frame.queue().clone(),
1079                pending: Arc::clone(&state.retiring),
1080            };
1081            replace_shown(
1082                state,
1083                Shown {
1084                    device: frame.device().clone(),
1085                    view: imported.view,
1086                    size: imported.size,
1087                    upload: None,
1088                    native: Some(hold),
1089                },
1090            );
1091            true
1092        }
1093        Err(error) => {
1094            report(
1095                &mut state.warned.import_failed,
1096                id,
1097                format_args!("refused a {:?} frame: {error}", handle.kind()),
1098            );
1099            refuse(release)
1100        }
1101    }
1102}
1103
1104// Registration itself touches the process-wide external-pass registry, whose
1105// own unit tests assert on it; it is exercised from `tests/texture_bus.rs`, a
1106// binary of its own, instead.
1107#[cfg(test)]
1108mod tests {
1109    use super::*;
1110
1111    #[test]
1112    fn the_packed_extent_round_trips_and_zero_means_none() {
1113        assert_eq!(unpack(0), None);
1114        assert_eq!(unpack(pack((64, 32))), Some((64, 32)));
1115        assert_eq!(unpack(pack((u32::MAX, 1))), Some((u32::MAX, 1)));
1116    }
1117
1118    #[test]
1119    fn a_source_frame_releases_once_and_reports_its_callback_in_debug() {
1120        let released = Arc::new(AtomicU64::new(0));
1121        let counter = Arc::clone(&released);
1122        let frame = SourceFrame::rgba8(1, 1, Arc::from(vec![0u8; 4])).on_release(move || {
1123            counter.fetch_add(1, Ordering::SeqCst);
1124        });
1125        assert!(format!("{frame:?}").contains("release: true"));
1126        frame.release();
1127        assert_eq!(released.load(Ordering::SeqCst), 1);
1128    }
1129
1130    #[test]
1131    fn import_errors_say_which_kind_of_failure() {
1132        assert!(
1133            ImportError::Unsupported("no extension".into())
1134                .to_string()
1135                .contains("unsupported")
1136        );
1137        assert!(
1138            ImportError::Failed("bad format".into())
1139                .to_string()
1140                .contains("bad format")
1141        );
1142    }
1143
1144    struct NoImport;
1145
1146    impl NativeImporter for NoImport {
1147        fn import(
1148            &self,
1149            _handle: &NativeHandle,
1150            _device: &wgpu::Device,
1151            _queue: &wgpu::Queue,
1152        ) -> Result<ImportedTexture, ImportError> {
1153            Err(ImportError::Unsupported("test importer".into()))
1154        }
1155    }
1156
1157    #[test]
1158    fn the_installed_query_follows_set_and_clear_per_kind() {
1159        // The only unit test in this binary that touches the process-wide
1160        // table, and it uses the Apple kind the host never installs.
1161        let kind = NativeHandleKind::CvPixelBuffer;
1162        assert!(!native_importer_installed(kind));
1163        assert!(set_native_importer(kind, Arc::new(NoImport)).is_none());
1164        assert!(native_importer_installed(kind));
1165        assert!(!native_importer_installed(
1166            NativeHandleKind::AHardwareBuffer
1167        ));
1168        assert!(clear_native_importer(kind).is_some());
1169        assert!(!native_importer_installed(kind));
1170    }
1171
1172    /// A fence that parks its callbacks until the test fires them.
1173    type Parked = Vec<Box<dyn FnOnce() + Send>>;
1174
1175    #[derive(Clone, Default)]
1176    struct FakeFence(Arc<Mutex<Parked>>);
1177
1178    impl FakeFence {
1179        fn fence(&self) -> Fence {
1180            let parked = Arc::clone(&self.0);
1181            Arc::new(move |callback| parked.lock().unwrap().push(callback))
1182        }
1183
1184        /// Fires every callback parked so far (not ones they park in turn).
1185        fn fire(&self) {
1186            let batch = std::mem::take(&mut *self.0.lock().unwrap());
1187            for callback in batch {
1188                callback();
1189            }
1190        }
1191    }
1192
1193    fn retire_counting(
1194        fake: &FakeFence,
1195        pending: &Arc<AtomicUsize>,
1196        released: &Arc<AtomicU64>,
1197        hops: u32,
1198    ) {
1199        let released = Arc::clone(released);
1200        retire(
1201            Box::new(()),
1202            Some(Box::new(move || {
1203                released.fetch_add(1, Ordering::SeqCst);
1204            })),
1205            pending,
1206            &fake.fence(),
1207            hops,
1208        );
1209    }
1210
1211    #[test]
1212    fn a_retired_release_waits_for_its_fence_and_runs_once() {
1213        let fake = FakeFence::default();
1214        let pending = Arc::new(AtomicUsize::new(0));
1215        let released = Arc::new(AtomicU64::new(0));
1216        retire_counting(&fake, &pending, &released, 1);
1217        assert_eq!(pending.load(Ordering::SeqCst), 1);
1218        assert_eq!(released.load(Ordering::SeqCst), 0);
1219        fake.fire();
1220        assert_eq!(released.load(Ordering::SeqCst), 1);
1221        assert_eq!(pending.load(Ordering::SeqCst), 0);
1222        fake.fire();
1223        assert_eq!(released.load(Ordering::SeqCst), 1);
1224    }
1225
1226    #[test]
1227    fn a_chained_retire_needs_a_fence_per_hop() {
1228        let fake = FakeFence::default();
1229        let pending = Arc::new(AtomicUsize::new(0));
1230        let released = Arc::new(AtomicU64::new(0));
1231        retire_counting(&fake, &pending, &released, 2);
1232        fake.fire();
1233        assert_eq!(
1234            released.load(Ordering::SeqCst),
1235            0,
1236            "one fence is not enough"
1237        );
1238        assert_eq!(pending.load(Ordering::SeqCst), 1);
1239        fake.fire();
1240        assert_eq!(released.load(Ordering::SeqCst), 1);
1241        assert_eq!(pending.load(Ordering::SeqCst), 0);
1242    }
1243
1244    /// A frame source standing in for a registration's, never polled here.
1245    fn idle_state() -> PassState {
1246        PassState {
1247            source: Box::new(|| None),
1248            shown: None,
1249            retiring: Arc::new(AtomicUsize::new(0)),
1250            warned: Warned::default(),
1251        }
1252    }
1253
1254    /// A release callback that bumps the returned counter, then panics.
1255    fn panicking_release() -> (ReleaseCallback, Arc<AtomicU64>) {
1256        let ran = Arc::new(AtomicU64::new(0));
1257        let counter = Arc::clone(&ran);
1258        let callback: ReleaseCallback = Box::new(move || {
1259            counter.fetch_add(1, Ordering::SeqCst);
1260            panic!("the release callback panics");
1261        });
1262        (callback, ran)
1263    }
1264
1265    fn counted_frame(released: &Arc<AtomicU64>) -> SourceFrame {
1266        let released = Arc::clone(released);
1267        SourceFrame::rgba8(1, 1, Arc::from(vec![0u8; 4])).on_release(move || {
1268            released.fetch_add(1, Ordering::SeqCst);
1269        })
1270    }
1271
1272    #[test]
1273    fn a_native_handle_round_trips_its_kind_and_pointer() {
1274        let mut byte = 0u8;
1275        let ptr: *mut c_void = (&raw mut byte).cast();
1276        for kind in [
1277            NativeHandleKind::CvPixelBuffer,
1278            NativeHandleKind::AHardwareBuffer,
1279        ] {
1280            // SAFETY: never handed to an importer; only the accessors read it.
1281            let handle = unsafe { NativeHandle::new(kind, ptr) };
1282            assert_eq!(handle.kind(), kind);
1283            assert_eq!(handle.ptr(), ptr);
1284        }
1285    }
1286
1287    #[test]
1288    fn a_native_handle_and_payload_are_move_only() {
1289        // The blanket-trait probe: the `Clone`-bounded inherent method wins
1290        // method resolution only for a type that is `Clone`.
1291        trait Probe {
1292            fn is_clone(&self) -> bool {
1293                false
1294            }
1295        }
1296        impl<T> Probe for T {}
1297        struct Wrap<T>(std::marker::PhantomData<T>);
1298        impl<T: Clone> Wrap<T> {
1299            fn is_clone(&self) -> bool {
1300                true
1301            }
1302        }
1303        assert!(!Wrap::<NativeHandle>(std::marker::PhantomData).is_clone());
1304        assert!(!Wrap::<SourcePayload>(std::marker::PhantomData).is_clone());
1305        assert!(Wrap::<Arc<[u8]>>(std::marker::PhantomData).is_clone());
1306    }
1307
1308    #[test]
1309    fn into_parts_hands_over_the_callback_and_disarms_the_drop() {
1310        let released = Arc::new(AtomicU64::new(0));
1311        // SAFETY: the callback is run below, once, and no native handle is
1312        // involved.
1313        let (meta, payload, release) = unsafe { counted_frame(&released).into_parts() };
1314        assert_eq!((meta.width, meta.height), (1, 1));
1315        assert!(matches!(&payload, SourcePayload::Cpu(p) if p.len() == 4));
1316        assert_eq!(released.load(Ordering::SeqCst), 0, "consuming ran nothing");
1317        release.expect("the callback moved out")();
1318        assert_eq!(released.load(Ordering::SeqCst), 1);
1319    }
1320
1321    #[test]
1322    fn a_source_frame_dropped_unconsumed_releases_once() {
1323        let released = Arc::new(AtomicU64::new(0));
1324        drop(counted_frame(&released));
1325        assert_eq!(released.load(Ordering::SeqCst), 1);
1326
1327        // A callback taken on (as an import holds it) is not run by the drop.
1328        let frame = counted_frame(&released);
1329        assert!(frame.has_release());
1330        // SAFETY: the callback is run below, once, and no native handle is
1331        // involved.
1332        let (_, _, held) = unsafe { frame.into_parts() };
1333        assert_eq!(released.load(Ordering::SeqCst), 1);
1334        held.expect("a callback is attached")();
1335        assert_eq!(released.load(Ordering::SeqCst), 2);
1336    }
1337
1338    #[test]
1339    fn a_panicking_source_frame_release_does_not_unwind_out_of_a_drop() {
1340        let (callback, ran) = panicking_release();
1341        let frame = SourceFrame::rgba8(1, 1, Arc::from(vec![0u8; 4])).on_release(callback);
1342        let outcome = std::panic::catch_unwind(AssertUnwindSafe(move || drop(frame)));
1343        assert!(outcome.is_ok(), "the panic was caught inside the drop");
1344        assert_eq!(ran.load(Ordering::SeqCst), 1, "the callback ran once");
1345    }
1346
1347    #[test]
1348    fn a_panicking_explicit_source_frame_release_does_not_propagate() {
1349        let (callback, ran) = panicking_release();
1350        let frame = SourceFrame::rgba8(1, 1, Arc::from(vec![0u8; 4])).on_release(callback);
1351        let outcome = std::panic::catch_unwind(AssertUnwindSafe(move || frame.release()));
1352        assert!(outcome.is_ok());
1353        assert_eq!(ran.load(Ordering::SeqCst), 1, "the callback ran once");
1354    }
1355
1356    #[test]
1357    fn a_panicking_retired_release_does_not_propagate() {
1358        let (callback, ran) = panicking_release();
1359        let pending = Arc::new(AtomicUsize::new(1));
1360        let retiring = Retiring {
1361            keep: None,
1362            release: Some(callback),
1363            pending: Arc::clone(&pending),
1364        };
1365        let outcome = std::panic::catch_unwind(AssertUnwindSafe(move || drop(retiring)));
1366        assert!(outcome.is_ok());
1367        assert_eq!(ran.load(Ordering::SeqCst), 1, "the callback ran once");
1368        assert_eq!(pending.load(Ordering::SeqCst), 0, "the backlog slot freed");
1369    }
1370
1371    #[test]
1372    fn a_panicking_bus_guard_release_does_not_propagate() {
1373        let (callback, ran) = panicking_release();
1374        let guard = ReleaseGuard(Some(callback));
1375        let outcome = std::panic::catch_unwind(AssertUnwindSafe(move || guard.release()));
1376        assert!(outcome.is_ok());
1377        assert_eq!(ran.load(Ordering::SeqCst), 1, "the callback ran once");
1378
1379        // A guard dropped unreleased (a panic between taking a frame apart
1380        // and handing its callback on) is the same path through `Drop`.
1381        let (callback, ran) = panicking_release();
1382        let guard = ReleaseGuard(Some(callback));
1383        let outcome = std::panic::catch_unwind(AssertUnwindSafe(move || drop(guard)));
1384        assert!(outcome.is_ok());
1385        assert_eq!(ran.load(Ordering::SeqCst), 1, "the callback ran once");
1386    }
1387
1388    #[test]
1389    fn a_source_frame_lost_to_a_panicking_pass_releases_once() {
1390        let released = Arc::new(AtomicU64::new(0));
1391        let mut source: FrameSource = {
1392            let released = Arc::clone(&released);
1393            Box::new(move || Some(counted_frame(&released)))
1394        };
1395        // The pass's shape: take a frame from the source, then fail before
1396        // the bus is done with it.
1397        let unwound = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1398            let _frame = source().expect("the source hands a frame over");
1399            panic!("the pass panics while it holds the frame");
1400        }));
1401        assert!(unwound.is_err());
1402        assert_eq!(released.load(Ordering::SeqCst), 1);
1403    }
1404
1405    #[test]
1406    fn the_retire_backlog_tolerates_four_and_refuses_the_fifth_once_warned() {
1407        let fake = FakeFence::default();
1408        let mut state = idle_state();
1409        let retired = Arc::new(AtomicU64::new(0));
1410        let polls = AtomicUsize::new(0);
1411        let poll = || {
1412            polls.fetch_add(1, Ordering::SeqCst);
1413        };
1414
1415        // Four frames are admitted, each retired (still fenced) by the next.
1416        for _ in 0..MAX_RETIRING {
1417            assert!(!backlog_refuses(
1418                &mut state,
1419                7,
1420                NativeHandleKind::AHardwareBuffer,
1421                poll
1422            ));
1423            retire_counting(&fake, &state.retiring, &retired, 1);
1424        }
1425        assert_eq!(state.retiring.load(Ordering::SeqCst), MAX_RETIRING);
1426        assert_eq!(polls.load(Ordering::SeqCst), 0, "no poll below the bound");
1427        assert!(!state.warned.backlog);
1428
1429        // The fifth is refused after one poll, and released at once.
1430        let released = Arc::new(AtomicU64::new(0));
1431        let fifth = counted_frame(&released);
1432        assert!(backlog_refuses(
1433            &mut state,
1434            7,
1435            NativeHandleKind::AHardwareBuffer,
1436            poll
1437        ));
1438        assert!(
1439            // SAFETY: the guard runs the callback inside `refuse`.
1440            !refuse(ReleaseGuard(unsafe { fifth.into_parts() }.2)),
1441            "a refusal never changes the shown view"
1442        );
1443        assert_eq!(released.load(Ordering::SeqCst), 1, "released at once");
1444        assert_eq!(polls.load(Ordering::SeqCst), 1, "fences got one poll");
1445        assert!(state.warned.backlog, "the first refusal is the warning");
1446        assert!(state.shown.is_none(), "the shown view is untouched");
1447        assert_eq!(retired.load(Ordering::SeqCst), 0);
1448
1449        // A sixth is refused too; the warning flag was already spent, so it
1450        // is logged at debug level only.
1451        assert!(backlog_refuses(
1452            &mut state,
1453            7,
1454            NativeHandleKind::AHardwareBuffer,
1455            poll
1456        ));
1457        assert!(state.warned.backlog);
1458
1459        // Once the fences fire the backlog drains and frames are admitted.
1460        fake.fire();
1461        assert_eq!(retired.load(Ordering::SeqCst), MAX_RETIRING as u64);
1462        assert!(!backlog_refuses(
1463            &mut state,
1464            7,
1465            NativeHandleKind::AHardwareBuffer,
1466            poll
1467        ));
1468    }
1469
1470    #[test]
1471    fn a_poll_that_fires_a_fence_admits_the_frame() {
1472        let fake = FakeFence::default();
1473        let pending = Arc::new(AtomicUsize::new(0));
1474        let retired = Arc::new(AtomicU64::new(0));
1475        for _ in 0..MAX_RETIRING {
1476            retire_counting(&fake, &pending, &retired, 1);
1477        }
1478        assert!(backlog_full(&pending, || {}));
1479        assert!(
1480            !backlog_full(&pending, || fake.fire()),
1481            "the poll ran the completed fences, so there is room again"
1482        );
1483        assert_eq!(pending.load(Ordering::SeqCst), 0);
1484    }
1485
1486    #[test]
1487    fn a_device_lost_mid_chain_still_releases_once() {
1488        let fake = FakeFence::default();
1489        let pending = Arc::new(AtomicUsize::new(0));
1490        let released = Arc::new(AtomicU64::new(0));
1491        retire_counting(&fake, &pending, &released, 2);
1492        fake.fire();
1493        assert_eq!(released.load(Ordering::SeqCst), 0, "one hop of two fired");
1494        // The device is lost: the second hop's callback is dropped unrun.
1495        fake.0.lock().unwrap().clear();
1496        assert_eq!(released.load(Ordering::SeqCst), 1);
1497        assert_eq!(pending.load(Ordering::SeqCst), 0);
1498        fake.fire();
1499        assert_eq!(released.load(Ordering::SeqCst), 1);
1500    }
1501
1502    #[test]
1503    fn a_fence_dropped_unrun_still_releases() {
1504        let fake = FakeFence::default();
1505        let pending = Arc::new(AtomicUsize::new(0));
1506        let released = Arc::new(AtomicU64::new(0));
1507        retire_counting(&fake, &pending, &released, 1);
1508        // The parked callbacks are dropped without ever running.
1509        fake.0.lock().unwrap().clear();
1510        assert_eq!(released.load(Ordering::SeqCst), 1);
1511        assert_eq!(pending.load(Ordering::SeqCst), 0);
1512    }
1513}
1514
1515#[cfg(test)]
1516mod on_release_displacement {
1517    use super::*;
1518    use std::sync::atomic::{AtomicUsize, Ordering};
1519
1520    #[test]
1521    fn on_release_runs_the_callback_it_displaces_exactly_once() {
1522        let first = Arc::new(AtomicUsize::new(0));
1523        let second = Arc::new(AtomicUsize::new(0));
1524        let frame = SourceFrame::rgba8(1, 1, Arc::from([0u8; 4])).on_release({
1525            let first = Arc::clone(&first);
1526            move || {
1527                first.fetch_add(1, Ordering::SeqCst);
1528            }
1529        });
1530        let frame = frame.on_release({
1531            let second = Arc::clone(&second);
1532            move || {
1533                second.fetch_add(1, Ordering::SeqCst);
1534            }
1535        });
1536        assert_eq!(
1537            first.load(Ordering::SeqCst),
1538            1,
1539            "the displaced callback ran at once"
1540        );
1541        assert_eq!(second.load(Ordering::SeqCst), 0);
1542        drop(frame);
1543        assert_eq!(first.load(Ordering::SeqCst), 1);
1544        assert_eq!(
1545            second.load(Ordering::SeqCst),
1546            1,
1547            "the attached callback ran on drop"
1548        );
1549    }
1550}