Skip to main content

khora_core/lane/
deck.rs

1// Copyright 2025 eraflo
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! `OutputDeck` — typed mutable bus for lane outputs, scoped to one tick.
16//!
17//! Symmetric counterpart of [`LaneBus`](super::LaneBus): lanes write typed
18//! outputs into the deck during the CLAD descent; the engine drains specific
19//! types at the I/O boundary (e.g. recorded GPU command buffers, draw lists).
20//!
21//! # Visibility contract
22//!
23//! - [`OutputDeck::slot`] returns a `&mut T` for lanes to push into.
24//! - [`OutputDeck::take`] is `pub(crate)` (used by the scheduler to expose
25//!   typed outputs to the engine I/O layer in a controlled way).
26
27use std::any::{Any, TypeId};
28use std::collections::HashMap;
29
30/// Mutable typed deck collecting lane outputs for one tick.
31///
32/// Each typed slot is created on first access, holding `T::default()` until
33/// a lane writes into it. The engine drains specific types at the I/O
34/// boundary (submit, present) via [`OutputDeck::take`].
35pub struct OutputDeck {
36    slots: HashMap<TypeId, Box<dyn Any + Send + Sync>>,
37}
38
39impl OutputDeck {
40    /// Creates an empty deck.
41    pub fn new() -> Self {
42        Self {
43            slots: HashMap::new(),
44        }
45    }
46
47    /// Returns a mutable reference to the slot for type `T`, creating it
48    /// with `T::default()` on first access.
49    ///
50    /// Lanes call this to accumulate outputs (e.g. push command buffers
51    /// onto a `Vec`).
52    pub fn slot<T: Default + Any + Send + Sync>(&mut self) -> &mut T {
53        self.slots
54            .entry(TypeId::of::<T>())
55            .or_insert_with(|| Box::new(T::default()))
56            .downcast_mut::<T>()
57            .expect("slot type mismatch — TypeId collision is impossible in safe Rust")
58    }
59
60    /// Removes and returns the slot for type `T`, replacing it with the
61    /// default value (so subsequent ticks start clean).
62    ///
63    /// Returns `T::default()` if no lane wrote into this slot during the
64    /// tick. Used by the engine at the I/O boundary.
65    pub fn take<T: Default + Any + Send + Sync>(&mut self) -> T {
66        self.slots
67            .remove(&TypeId::of::<T>())
68            .and_then(|b| b.downcast::<T>().ok().map(|b| *b))
69            .unwrap_or_default()
70    }
71
72    /// Reports whether a slot of the given type has been written this tick.
73    pub fn contains<T: Any + Send + Sync>(&self) -> bool {
74        self.slots.contains_key(&TypeId::of::<T>())
75    }
76
77    /// Number of slots currently holding data.
78    pub fn len(&self) -> usize {
79        self.slots.len()
80    }
81
82    /// Whether no slots have been written.
83    pub fn is_empty(&self) -> bool {
84        self.slots.is_empty()
85    }
86
87    /// Clears all slots. Called by the scheduler between ticks if a deck
88    /// instance is reused rather than reallocated.
89    pub fn clear(&mut self) {
90        self.slots.clear();
91    }
92
93    /// Moves every slot from `other` into `self`.
94    ///
95    /// Used by the parallel executor to fold a worker's private deck shard back
96    /// into the shared deck after a concurrent wave. The merged shards are
97    /// expected to write **disjoint** slot types (guaranteed by the executor's
98    /// [`AgentAccess::Isolated`](crate::agent::AgentAccess::Isolated) eligibility
99    /// rules). A colliding `TypeId` is therefore a scheduling bug: the existing
100    /// slot is kept and the collision logged, rather than silently dropping one
101    /// side's outputs.
102    pub fn merge_from(&mut self, other: OutputDeck) {
103        for (type_id, value) in other.slots {
104            if self.slots.contains_key(&type_id) {
105                log::error!(
106                    "OutputDeck::merge_from: colliding slot {type_id:?} across parallel deck \
107                     shards — keeping the existing entry (parallel-eligibility bug)"
108                );
109                continue;
110            }
111            self.slots.insert(type_id, value);
112        }
113    }
114}
115
116impl Default for OutputDeck {
117    fn default() -> Self {
118        Self::new()
119    }
120}
121
122impl std::fmt::Debug for OutputDeck {
123    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
124        f.debug_struct("OutputDeck")
125            .field("slots", &self.slots.len())
126            .finish()
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133
134    #[test]
135    fn slot_lazily_inserts_default() {
136        let mut deck = OutputDeck::new();
137        assert!(!deck.contains::<Vec<u32>>());
138        let v = deck.slot::<Vec<u32>>();
139        v.push(1);
140        v.push(2);
141        assert_eq!(deck.slot::<Vec<u32>>(), &vec![1, 2]);
142    }
143
144    #[test]
145    fn take_removes_and_returns_value() {
146        let mut deck = OutputDeck::new();
147        deck.slot::<Vec<u32>>().extend([1, 2, 3]);
148        let taken: Vec<u32> = deck.take();
149        assert_eq!(taken, vec![1, 2, 3]);
150        assert!(!deck.contains::<Vec<u32>>());
151    }
152
153    #[test]
154    fn take_missing_returns_default() {
155        let mut deck = OutputDeck::new();
156        let taken: Vec<u32> = deck.take();
157        assert!(taken.is_empty());
158    }
159
160    #[test]
161    fn distinct_types_dont_alias() {
162        let mut deck = OutputDeck::new();
163        deck.slot::<Vec<u32>>().push(1);
164        deck.slot::<Vec<u8>>().push(2);
165        assert_eq!(deck.take::<Vec<u32>>(), vec![1]);
166        assert_eq!(deck.take::<Vec<u8>>(), vec![2]);
167    }
168
169    #[test]
170    fn merge_from_folds_disjoint_shards() {
171        let mut main = OutputDeck::new();
172        main.slot::<Vec<u32>>().push(1);
173
174        let mut shard = OutputDeck::new();
175        shard.slot::<Vec<u8>>().extend([7, 8]);
176
177        main.merge_from(shard);
178        assert_eq!(main.take::<Vec<u32>>(), vec![1]);
179        assert_eq!(main.take::<Vec<u8>>(), vec![7, 8]);
180    }
181
182    #[test]
183    fn merge_from_keeps_existing_on_collision() {
184        let mut main = OutputDeck::new();
185        main.slot::<Vec<u32>>().push(1);
186
187        let mut shard = OutputDeck::new();
188        shard.slot::<Vec<u32>>().push(99);
189
190        // Colliding slot type: the existing entry is kept (defensive).
191        main.merge_from(shard);
192        assert_eq!(main.take::<Vec<u32>>(), vec![1]);
193    }
194}