use std::collections::BTreeMap;
use std::panic::{AssertUnwindSafe, catch_unwind};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
use frust_gpu::SceneTextureId;
pub trait ExternalPass: Send + Sync {
fn record(&self, frame: &mut ExternalFrame<'_>);
}
pub struct ExternalFrame<'a> {
device: &'a wgpu::Device,
queue: &'a wgpu::Queue,
encoder: &'a mut wgpu::CommandEncoder,
engine: &'a mut frust_engine::EngineRenderer,
id: SceneTextureId,
frame_index: u64,
}
impl<'a> ExternalFrame<'a> {
pub(crate) fn new(
device: &'a wgpu::Device,
queue: &'a wgpu::Queue,
encoder: &'a mut wgpu::CommandEncoder,
engine: &'a mut frust_engine::EngineRenderer,
id: SceneTextureId,
frame_index: u64,
) -> Self {
Self {
device,
queue,
encoder,
engine,
id,
frame_index,
}
}
#[must_use]
pub fn device(&self) -> &wgpu::Device {
self.device
}
#[must_use]
pub fn queue(&self) -> &wgpu::Queue {
self.queue
}
pub fn encoder(&mut self) -> &mut wgpu::CommandEncoder {
self.encoder
}
#[must_use]
pub fn id(&self) -> SceneTextureId {
self.id
}
#[must_use]
pub fn frame_index(&self) -> u64 {
self.frame_index
}
pub fn bind_texture(
&mut self,
size: (u32, u32),
view: wgpu::TextureView,
) -> Option<wgpu::TextureView> {
self.engine.bind_texture(self.id, size, view)
}
pub fn unbind_texture(&mut self) -> Option<wgpu::TextureView> {
self.engine.unbind_texture(self.id)
}
}
struct Registration {
id: SceneTextureId,
pass: Arc<dyn ExternalPass>,
}
struct PanicReport {
identity: usize,
}
struct Registry {
passes: BTreeMap<u64, Registration>,
pending_unbind: BTreeMap<u64, SceneTextureId>,
reported_panics: BTreeMap<u64, PanicReport>,
reported_reserved: bool,
}
impl Registry {
const fn new() -> Self {
Self {
passes: BTreeMap::new(),
pending_unbind: BTreeMap::new(),
reported_panics: BTreeMap::new(),
reported_reserved: false,
}
}
}
static REGISTRY: Mutex<Registry> = Mutex::new(Registry::new());
static FRAME_INDEX: AtomicU64 = AtomicU64::new(0);
fn registry() -> MutexGuard<'static, Registry> {
REGISTRY.lock().unwrap_or_else(PoisonError::into_inner)
}
pub fn register_external_pass(id: SceneTextureId, pass: Arc<dyn ExternalPass>) -> bool {
let raw = id.get();
if id.is_shader_program() {
let should_warn = {
let mut registry = registry();
let should_warn = !registry.reported_reserved;
if should_warn {
registry.reported_reserved = true;
}
should_warn
};
if should_warn {
log::warn!(
"external pass registration for scene texture {raw} refused: the id is inside \
the reserved shader-program namespace (further attempts at this id are logged \
at debug level)"
);
} else {
log::debug!(
"external pass registration for scene texture {raw} refused: reserved \
shader-program namespace"
);
}
return false;
}
let pass_identity = Arc::as_ptr(&pass) as *const () as usize;
let should_clear_panic_flag = {
let registry = registry();
if registry.passes.contains_key(&raw) {
return false;
}
registry
.reported_panics
.get(&raw)
.is_some_and(|report| report.identity != pass_identity)
};
if should_clear_panic_flag {
let mut registry = registry();
registry.reported_panics.remove(&raw);
}
let mut registry = registry();
registry.passes.insert(raw, Registration { id, pass });
true
}
pub fn unregister_external_pass(id: SceneTextureId) -> bool {
let mut registry = registry();
if registry.passes.remove(&id.get()).is_none() {
return false;
}
registry.pending_unbind.insert(id.get(), id);
true
}
#[doc(hidden)]
pub fn run_external_passes(
device: &wgpu::Device,
queue: &wgpu::Queue,
encoder: &mut wgpu::CommandEncoder,
engine: &mut frust_engine::EngineRenderer,
) {
let (pending_unbind, passes) = {
let mut registry = registry();
if registry.passes.is_empty() && registry.pending_unbind.is_empty() {
return;
}
let pending_unbind = std::mem::take(&mut registry.pending_unbind);
let passes: Vec<(SceneTextureId, Arc<dyn ExternalPass>)> = registry
.passes
.values()
.map(|registration| (registration.id, Arc::clone(®istration.pass)))
.collect();
(pending_unbind, passes)
};
for id in pending_unbind.into_values() {
engine.unbind_texture(id);
}
if passes.is_empty() {
return;
}
let frame_index = FRAME_INDEX.fetch_add(1, Ordering::Relaxed);
for (id, pass) in passes {
let mut frame =
ExternalFrame::new(device, queue, &mut *encoder, &mut *engine, id, frame_index);
if catch_unwind(AssertUnwindSafe(|| pass.record(&mut frame))).is_err() {
drop_panicking_pass(id, &pass);
}
}
}
fn drop_panicking_pass(id: SceneTextureId, pass: &Arc<dyn ExternalPass>) {
let raw = id.get();
let pass_identity = Arc::as_ptr(pass) as *const () as usize;
let (still_registered, should_warn, was_different_pass) = {
let mut registry = registry();
let registration = registry.passes.get(&raw);
let still_registered = registration.is_some_and(|r| Arc::ptr_eq(&r.pass, pass));
let should_warn = registry
.reported_panics
.get(&raw)
.is_none_or(|report| report.identity != pass_identity);
let was_different_pass = registration.is_some_and(|r| !Arc::ptr_eq(&r.pass, pass));
if still_registered {
registry.passes.remove(&raw);
registry.pending_unbind.insert(raw, id);
}
if should_warn {
registry.reported_panics.insert(
raw,
PanicReport {
identity: pass_identity,
},
);
}
(still_registered, should_warn, was_different_pass)
};
if was_different_pass && !still_registered {
log::debug!(
"external pass for scene texture {raw} panicked after handing over; \
the successor stays registered"
);
} else if still_registered && should_warn {
log::warn!(
"external pass for scene texture {raw} panicked and was unregistered; \
the frame was recorded without it (further panics of this id are \
logged at debug level)"
);
} else if still_registered {
log::debug!("external pass for scene texture {raw} panicked and was unregistered");
}
}
#[cfg(test)]
mod tests {
use super::*;
static SERIAL: Mutex<()> = Mutex::new(());
fn serial() -> MutexGuard<'static, ()> {
SERIAL.lock().unwrap_or_else(PoisonError::into_inner)
}
struct Inert;
impl ExternalPass for Inert {
fn record(&self, _frame: &mut ExternalFrame<'_>) {}
}
fn state(id: SceneTextureId) -> (bool, bool) {
let registry = registry();
(
registry.passes.contains_key(&id.get()),
registry.pending_unbind.contains_key(&id.get()),
)
}
#[test]
fn registering_an_id_twice_keeps_the_first_pass() {
let _serial = serial();
let id = SceneTextureId::mint();
assert!(register_external_pass(id, Arc::new(Inert)));
assert!(
!register_external_pass(id, Arc::new(Inert)),
"a second registration must not displace the first"
);
assert_eq!(state(id), (true, false));
assert!(unregister_external_pass(id));
assert_eq!(
state(id),
(false, true),
"unregistering queues the engine binding for the next drain"
);
}
#[test]
fn unregistering_an_unregistered_id_answers_false() {
let _serial = serial();
let id = SceneTextureId::mint();
assert!(!unregister_external_pass(id));
assert_eq!(
state(id),
(false, false),
"an id nothing registered queues no unbind"
);
}
#[test]
fn re_registering_before_the_drain_queues_an_unbind_first() {
let _serial = serial();
let id = SceneTextureId::mint();
assert!(register_external_pass(id, Arc::new(Inert)));
assert!(unregister_external_pass(id));
assert!(register_external_pass(id, Arc::new(Inert)));
assert_eq!(
state(id),
(true, true),
"handing an id over leaves a queued unbind (the id draws nothing until the new owner binds)"
);
assert!(unregister_external_pass(id));
}
#[test]
fn a_panicking_pass_is_retired_and_queued_for_unbind() {
let _serial = serial();
let id = SceneTextureId::mint();
let pass: Arc<dyn ExternalPass> = Arc::new(Inert);
assert!(register_external_pass(id, Arc::clone(&pass)));
drop_panicking_pass(id, &pass);
assert_eq!(
state(id),
(false, true),
"a panicking pass leaves the registry exactly as an unregistered one does"
);
drop_panicking_pass(id, &pass);
assert_eq!(state(id), (false, true));
registry().pending_unbind.remove(&id.get());
}
#[test]
fn retiring_a_panic_leaves_a_successor_registered_under_the_same_id_alone() {
let _serial = serial();
let id = SceneTextureId::mint();
let original: Arc<dyn ExternalPass> = Arc::new(Inert);
assert!(register_external_pass(id, Arc::clone(&original)));
assert!(unregister_external_pass(id));
let successor: Arc<dyn ExternalPass> = Arc::new(Inert);
assert!(register_external_pass(id, Arc::clone(&successor)));
drop_panicking_pass(id, &original);
assert_eq!(
state(id),
(true, true),
"the successor is left registered, and the queued unbind from the hand-over persists"
);
assert!(unregister_external_pass(id));
registry().pending_unbind.remove(&id.get());
}
#[test]
fn a_fresh_registration_with_a_different_arc_clears_the_reported_panic_flag() {
let _serial = serial();
let id = SceneTextureId::mint();
let pass1: Arc<dyn ExternalPass> = Arc::new(Inert);
assert!(register_external_pass(id, Arc::clone(&pass1)));
drop_panicking_pass(id, &pass1);
assert!(
registry().reported_panics.contains_key(&id.get()),
"the first panic of this id is recorded as already reported"
);
let pass2: Arc<dyn ExternalPass> = Arc::new(Inert);
assert!(register_external_pass(id, Arc::clone(&pass2)));
assert!(
!registry().reported_panics.contains_key(&id.get()),
"registering a different Arc clears the flag, so a new pass's first panic warns again"
);
assert!(unregister_external_pass(id));
}
#[test]
fn re_registering_the_same_arc_keeps_the_reported_panic_flag() {
let _serial = serial();
let id = SceneTextureId::mint();
let pass: Arc<dyn ExternalPass> = Arc::new(Inert);
assert!(register_external_pass(id, Arc::clone(&pass)));
drop_panicking_pass(id, &pass);
assert!(
registry().reported_panics.contains_key(&id.get()),
"the first panic is recorded"
);
registry().pending_unbind.remove(&id.get());
assert!(register_external_pass(id, Arc::clone(&pass)));
assert!(
registry().reported_panics.contains_key(&id.get()),
"re-registering the same Arc keeps the panic flag (no warn on next panic)"
);
assert!(unregister_external_pass(id));
}
#[test]
fn panicking_pass_with_successor_already_registered_logs_debug() {
let _serial = serial();
let id = SceneTextureId::mint();
let original: Arc<dyn ExternalPass> = Arc::new(Inert);
assert!(register_external_pass(id, Arc::clone(&original)));
assert!(unregister_external_pass(id));
let successor: Arc<dyn ExternalPass> = Arc::new(Inert);
assert!(register_external_pass(id, Arc::clone(&successor)));
drop_panicking_pass(id, &original);
assert_eq!(
state(id),
(true, true),
"the successor is left registered and the queued unbind from the hand-over remains"
);
assert!(unregister_external_pass(id));
}
#[test]
fn pending_unbind_grows_with_distinct_ids_not_with_repeat_unregisters() {
let _serial = serial();
let id = SceneTextureId::mint();
let before = registry().pending_unbind.len();
assert!(register_external_pass(id, Arc::new(Inert)));
assert!(unregister_external_pass(id));
assert!(!unregister_external_pass(id));
assert_eq!(
registry().pending_unbind.len(),
before + 1,
"repeat unregistration of one id does not grow the queue past one entry"
);
registry().pending_unbind.remove(&id.get());
}
}