Skip to main content

concinnity_core/render/
parallel_ctx.rs

1//! Generic Send/Sync shim for parallel per-pass command recording, shared by all
2//! three backend executors (`{metal,directx,vulkan}/graph_exec.rs`). Each backend
3//! fans its non-composite render-graph passes onto worker threads; every worker
4//! records into its own command buffer/list and reaches the immutable subset of
5//! the backend context it needs through a `ParallelCtxRef`.
6//!
7//! The backend context types are not Send/Sync in Rust's type system: they hold
8//! COM smart pointers, objc2 protocol objects, RefCells, and the like. The
9//! graphics APIs nonetheless permit shared, read-only access to device-derived
10//! resources from many threads. A backend adopts that claim for its own context
11//! type with a single `unsafe impl ParallelEncodeCtx`, where it documents the
12//! audit of every interior-mutable field reachable during encode. The Send/Sync
13//! impls on `ParallelCtxRef` below are then keyed off that marker, so the unsafe
14//! reasoning lives in one auditable place per backend instead of being repeated
15//! on three structurally identical wrapper types.
16
17/// Marker for a backend context that may be shared, read-only, across the
18/// parallel-encode worker fan-out.
19///
20/// # Safety
21///
22/// Implementing this is a claim that concurrent `&Self` access during command
23/// recording is sound: the graphics API allows shared read of device-derived
24/// resources, and every interior-mutable field reachable during encode is
25/// either atomic or hoisted out of the fan-out before it begins. Each backend's
26/// impl carries that audit (see the module docs in each
27/// `*/parallel_encoder.rs`).
28pub unsafe trait ParallelEncodeCtx {}
29
30/// A handle to a `&'a T` borrow that is Send + Sync when `T: ParallelEncodeCtx`.
31/// Worker closures use it to reach the immutable subset of the backend context
32/// they need while recording commands into their own command buffer/list.
33///
34/// The borrow is held directly, so its lifetime is enforced by the type. The
35/// wrapper is only used inside each backend's parallel-encoder fan-out in
36/// `graph_exec.rs`, which joins all workers before the outer borrow returns. The
37/// Send/Sync claim rests entirely on the `T: ParallelEncodeCtx` marker.
38pub struct ParallelCtxRef<'a, T> {
39    inner: &'a T,
40}
41
42impl<'a, T> ParallelCtxRef<'a, T> {
43    /// Wrap `ctx` so worker threads can share it.
44    pub fn new(ctx: &'a T) -> Self {
45        Self { inner: ctx }
46    }
47
48    /// Borrow the wrapped context.
49    pub fn as_ctx(&self) -> &T {
50        self.inner
51    }
52}
53
54impl<T> Clone for ParallelCtxRef<'_, T> {
55    fn clone(&self) -> Self {
56        *self
57    }
58}
59
60impl<T> Copy for ParallelCtxRef<'_, T> {}
61
62// SAFETY: `T: ParallelEncodeCtx` is the backend's audited claim that shared,
63// read-only `&T` access across the encode fan-out is sound. The wrapper only
64// ever hands out `&T` (via `as_ctx`), so Send + Sync follow from that claim.
65unsafe impl<T: ParallelEncodeCtx> Send for ParallelCtxRef<'_, T> {}
66// SAFETY: as for `Send` above -- the wrapper only ever hands out `&T`.
67unsafe impl<T: ParallelEncodeCtx> Sync for ParallelCtxRef<'_, T> {}
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72
73    struct Ctx(u32);
74
75    // SAFETY: a plain integer with no interior mutability, so concurrent `&Ctx`
76    // access during the fan-out is trivially sound.
77    unsafe impl ParallelEncodeCtx for Ctx {}
78
79    // The wrapper hands back the borrow it was built from, and copying one
80    // hands back the same borrow: workers each hold their own copy of the
81    // handle and reach one shared context through it.
82    #[test]
83    fn every_copy_of_the_handle_reaches_the_same_context() {
84        let ctx = Ctx(7);
85        let handle = ParallelCtxRef::new(&ctx);
86        let copied = handle;
87        // Spelled through the trait: the wrapper is `Copy`, so method-call
88        // syntax would take the copy path and never reach this impl.
89        let cloned = Clone::clone(&handle);
90        assert_eq!(handle.as_ctx().0, 7);
91        assert_eq!(copied.as_ctx().0, 7);
92        assert_eq!(cloned.as_ctx().0, 7);
93        assert!(core::ptr::eq(handle.as_ctx(), cloned.as_ctx()));
94    }
95
96    // The Send/Sync claim is what lets a worker thread hold one, so borrow a
97    // handle across a scoped thread to prove the impls are in force.
98    #[test]
99    fn a_handle_crosses_a_thread_boundary() {
100        let ctx = Ctx(3);
101        let handle = ParallelCtxRef::new(&ctx);
102        std::thread::scope(|scope| {
103            let worker = scope.spawn(move || handle.as_ctx().0);
104            assert_eq!(worker.join().expect("the worker finished"), 3);
105        });
106    }
107}