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}