Skip to main content

concinnity_core/ecs/
access_check.rs

1//! Debug-build access validation hooks. `PipelineContext` accessors report each
2//! touch here; the client installs a hook that asserts the touch against the
3//! stepping system's declared [`Access`] (tracked
4//! client-side, since this crate is no_std and holds no per-thread state).
5//! `World::step` announces which system is stepping through the second hook,
6//! for the same reason: the tracking is per-thread and belongs to the host.
7//! Compiled out of release builds entirely: release parallel-safety never rests
8//! on these checks, only on the executor handing conflicting systems to
9//! different waves.
10#![cfg(debug_assertions)]
11
12use core::any::TypeId;
13use core::sync::atomic::{AtomicPtr, Ordering};
14
15use crate::ecs::Access;
16
17/// What a context accessor touched. Component ids are the registry
18/// discriminants; resources and events report their `TypeId` for the client's
19/// id registry to resolve.
20pub enum Touch {
21    /// A component column was read.
22    ComponentRead {
23        /// The component's registry discriminant.
24        id: u8,
25        /// The component type's name, for the assertion message.
26        type_name: &'static str,
27    },
28    /// A component column was written.
29    ComponentWrite {
30        /// The component's registry discriminant.
31        id: u8,
32        /// The component type's name, for the assertion message.
33        type_name: &'static str,
34    },
35    /// Structural change (push/insert/remove/despawn/drain): reorders columns
36    /// and the join index, so it requires exclusive access.
37    Structural {
38        /// The operation that changed structure.
39        op: &'static str,
40    },
41    /// A resource or event queue was touched.
42    Resource {
43        /// The resource type's id, resolved by the client's id registry.
44        type_id: TypeId,
45        /// The resource type's name, for the assertion message.
46        type_name: &'static str,
47        /// `true` for a write, `false` for a read.
48        write: bool,
49    },
50    /// Compiled-payload access mutates the blob store's residency.
51    Blob {
52        /// The operation that touched the blob store.
53        op: &'static str,
54    },
55}
56
57type Hook = fn(&Touch);
58
59/// Marks the stepping system's declared access active, or clears it. The
60/// client keeps the per-thread state a no_std crate cannot.
61type ActiveHook = fn(Option<(Access, &'static str)>);
62
63static HOOK: AtomicPtr<()> = AtomicPtr::new(core::ptr::null_mut());
64static ACTIVE_HOOK: AtomicPtr<()> = AtomicPtr::new(core::ptr::null_mut());
65
66/// Install the process-wide validation hook. Idempotent by usage (the client
67/// installs one hook once); the last install wins.
68pub fn install(hook: Hook) {
69    HOOK.store(hook as *mut (), Ordering::Release);
70}
71
72/// Install the process-wide hook `World::step` announces each system's declared
73/// access through. Installed alongside [`install`]; without it every touch is
74/// reported against no active system and passes.
75pub fn install_active(hook: ActiveHook) {
76    ACTIVE_HOOK.store(hook as *mut (), Ordering::Release);
77}
78
79#[inline]
80pub(crate) fn touch(t: Touch) {
81    let raw = HOOK.load(Ordering::Acquire);
82    if raw.is_null() {
83        return;
84    }
85    // SAFETY: the pointer only ever holds a `Hook` stored by `install`.
86    let hook: Hook = unsafe { core::mem::transmute::<*mut (), Hook>(raw) };
87    hook(&t);
88}
89
90/// Announce the system whose step is running, or clear it with `None`. A no-op
91/// until a client installs the hook.
92pub fn set_active(active: Option<(Access, &'static str)>) {
93    let raw = ACTIVE_HOOK.load(Ordering::Acquire);
94    if raw.is_null() {
95        return;
96    }
97    // SAFETY: the pointer only ever holds an `ActiveHook` stored by
98    // `install_active`.
99    let hook: ActiveHook = unsafe { core::mem::transmute::<*mut (), ActiveHook>(raw) };
100    hook(active);
101}