Skip to main content

khora_core/lane/
mod.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//! # Lane Abstraction
16//!
17//! The unified base trait for all lane types in the KhoraEngine.
18//!
19//! A **Lane** is a reusable, swappable processing strategy within an agent.
20//! Agents compose and select lanes based on resource budgets (GORNA protocol)
21//! and quality targets. Each lane encapsulates a specific algorithmic approach
22//! to a domain task (rendering, physics, audio, asset loading, etc.).
23//!
24//! ## Architecture
25//!
26//! The Lane system follows a two-level trait hierarchy:
27//!
28//! 1. **`Lane`** (this trait) — Common interface shared by ALL lane types.
29//!    Provides identity, classification, and cost estimation.
30//!
31//! 2. **Domain-specific traits** — Extend `Lane` with domain-specific execution
32//!    methods. Examples:
33//!    - `RenderLane: Lane` — GPU rendering strategies
34//!    - `ShadowLane: Lane` — Shadow map generation strategies
35//!    - `PhysicsLane: Lane` — Physics simulation strategies
36//!    - `AudioMixingLane: Lane` — Audio mixing strategies
37//!    - `AssetDecoder<A>` — Asset decoding (bytes → typed asset)
38//!    - `SerializationStrategy: Lane` — Scene serialization strategies
39//!
40//! ## Usage
41//!
42//! ```rust,ignore
43//! use khora_core::lane::{Lane, LaneKind, LaneError, LaneContext};
44//!
45//! struct MyCustomLane { initialized: std::sync::atomic::AtomicBool }
46//!
47//! impl Lane for MyCustomLane {
48//!     fn strategy_name(&self) -> &'static str { "MyCustom" }
49//!     fn lane_kind(&self) -> LaneKind { LaneKind::Render }
50//!
51//!     fn on_initialize(&self, _ctx: &mut LaneContext) -> Result<(), LaneError> {
52//!         self.initialized.store(true, std::sync::atomic::Ordering::Relaxed);
53//!         Ok(())
54//!     }
55//!
56//!     fn execute(&self, _ctx: &mut LaneContext) -> Result<(), LaneError> {
57//!         // Domain-specific work here
58//!         Ok(())
59//!     }
60//!
61//!     fn as_any(&self) -> &dyn std::any::Any { self }
62//!     fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self }
63//! }
64//! ```
65
66use std::any::{Any, TypeId};
67use std::collections::HashMap;
68use std::fmt;
69
70pub mod bus;
71pub mod context_keys;
72pub mod deck;
73pub mod lock;
74pub use bus::LaneBus;
75pub use context_keys::*;
76pub use deck::OutputDeck;
77pub use lock::{
78    mutex_lock, mutex_lock_render, read_lock, read_lock_render, write_lock, write_lock_render,
79};
80
81/// Error type for lane operations.
82#[derive(Debug)]
83pub enum LaneError {
84    /// The lane has not been initialized yet.
85    NotInitialized,
86    /// The execution context passed to the lane has the wrong type.
87    InvalidContext {
88        /// What the lane expected.
89        expected: &'static str,
90        /// Description of what was received.
91        received: String,
92    },
93    /// A `RwLock` / `Mutex` was poisoned by a prior panic.
94    LockPoisoned {
95        /// Which lock — used in the error message for diagnostics.
96        context: &'static str,
97    },
98    /// A required GPU resource (pipeline, buffer, texture, sampler) was
99    /// not present in the registry the lane consulted.
100    MissingResource {
101        /// What kind of resource (e.g. `"pipeline"`, `"buffer"`).
102        kind: &'static str,
103        /// Human-readable key the lane looked up.
104        key: String,
105    },
106    /// A required asset was not present in the registry / cache.
107    MissingAsset {
108        /// Asset kind (`"mesh"`, `"texture"`, …).
109        kind: &'static str,
110        /// Stringified asset id (UUID, name, …).
111        id: String,
112    },
113    /// A domain-specific error occurred during execution.
114    ExecutionFailed(Box<dyn std::error::Error + Send + Sync>),
115    /// A domain-specific error occurred during initialization.
116    InitializationFailed(Box<dyn std::error::Error + Send + Sync>),
117}
118
119impl fmt::Display for LaneError {
120    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
121        match self {
122            LaneError::NotInitialized => write!(f, "Lane not initialized"),
123            LaneError::InvalidContext { expected, received } => {
124                write!(
125                    f,
126                    "Invalid lane context: expected {expected}, got {received}"
127                )
128            }
129            LaneError::LockPoisoned { context } => {
130                write!(f, "Lane lock poisoned: {context}")
131            }
132            LaneError::MissingResource { kind, key } => {
133                write!(f, "Missing {kind} resource: {key}")
134            }
135            LaneError::MissingAsset { kind, id } => {
136                write!(f, "Missing {kind} asset: {id}")
137            }
138            LaneError::ExecutionFailed(e) => write!(f, "Lane execution failed: {e}"),
139            LaneError::InitializationFailed(e) => write!(f, "Lane initialization failed: {e}"),
140        }
141    }
142}
143
144impl std::error::Error for LaneError {
145    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
146        match self {
147            LaneError::ExecutionFailed(e) | LaneError::InitializationFailed(e) => Some(e.as_ref()),
148            _ => None,
149        }
150    }
151}
152
153impl LaneError {
154    /// Convenience constructor for a missing context entry.
155    pub fn missing(type_name: &'static str) -> Self {
156        LaneError::InvalidContext {
157            expected: type_name,
158            received: "not found in LaneContext".into(),
159        }
160    }
161
162    /// Convenience constructor for a poisoned lock.
163    pub fn lock_poisoned(context: &'static str) -> Self {
164        LaneError::LockPoisoned { context }
165    }
166
167    /// Convenience constructor for a missing resource.
168    pub fn missing_resource(kind: &'static str, key: impl Into<String>) -> Self {
169        LaneError::MissingResource {
170            kind,
171            key: key.into(),
172        }
173    }
174
175    /// Convenience constructor for a missing asset.
176    pub fn missing_asset(kind: &'static str, id: impl std::fmt::Display) -> Self {
177        LaneError::MissingAsset {
178            kind,
179            id: id.to_string(),
180        }
181    }
182}
183
184/// Classification of lane types, used for routing and filtering.
185///
186/// Agents use this to identify compatible lanes during GORNA negotiation
187/// and lane selection.
188#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
189pub enum LaneKind {
190    /// Main scene rendering (forward, deferred, etc.)
191    Render,
192    /// Shadow map generation
193    Shadow,
194    /// Physics simulation
195    Physics,
196    /// Audio mixing and spatialization
197    Audio,
198    /// Asset loading and processing
199    Asset,
200    /// Scene serialization/deserialization
201    Scene,
202    /// ECS maintenance (compaction, garbage collection)
203    Ecs,
204    /// User interface layout and interaction
205    Ui,
206}
207
208impl std::fmt::Display for LaneKind {
209    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
210        match self {
211            LaneKind::Render => write!(f, "Render"),
212            LaneKind::Shadow => write!(f, "Shadow"),
213            LaneKind::Physics => write!(f, "Physics"),
214            LaneKind::Audio => write!(f, "Audio"),
215            LaneKind::Asset => write!(f, "Asset"),
216            LaneKind::Scene => write!(f, "Scene"),
217            LaneKind::Ecs => write!(f, "ECS"),
218            LaneKind::Ui => write!(f, "UI"),
219        }
220    }
221}
222
223// ─────────────────────────────────────────────────────────────────────────────
224// LaneContext — generic type-map for passing data to lanes
225// ─────────────────────────────────────────────────────────────────────────────
226
227/// A type-erased, extensible context for passing data to lanes.
228///
229/// Agents populate a `LaneContext` with the data their lanes need,
230/// then pass it to [`Lane::execute`], [`Lane::on_initialize`], etc.
231/// Lanes retrieve specific data by type using [`get`](LaneContext::get).
232///
233/// # Adding data
234///
235/// ```rust,ignore
236/// use khora_core::lane::LaneContext;
237///
238/// let mut ctx = LaneContext::new();
239/// ctx.insert(42u32);
240/// ctx.insert(String::from("hello"));
241///
242/// assert_eq!(ctx.get::<u32>(), Some(&42));
243/// assert_eq!(ctx.get::<String>().unwrap(), "hello");
244/// ```
245///
246/// # Mutable references
247///
248/// For data that is borrowed (not owned), use [`Slot`] (mutable) or
249/// [`Ref`] (shared) wrappers:
250///
251/// ```rust,ignore
252/// use khora_core::lane::{LaneContext, Slot};
253///
254/// let mut value = 10u32;
255/// let mut ctx = LaneContext::new();
256/// ctx.insert(Slot::new(&mut value));
257///
258/// let slot = ctx.get::<Slot<u32>>().unwrap();
259/// *slot.get() = 20;
260/// ```
261///
262/// # Safety
263///
264/// `LaneContext` uses `unsafe impl Send + Sync` because it may hold
265/// [`Slot`] / [`Ref`] wrappers containing raw pointers. This is safe
266/// because the context is stack-scoped: created by the agent, passed to
267/// one lane at a time, and dropped before the next frame.
268pub struct LaneContext {
269    data: HashMap<TypeId, Box<dyn Any>>,
270}
271
272// SAFETY: All values inserted via `insert<T: Send + Sync>()` are Send+Sync.
273// Slot/Ref wrappers hold raw pointers but are only used within single-threaded
274// frame scopes where the pointed-to data is guaranteed to be alive.
275unsafe impl Send for LaneContext {}
276unsafe impl Sync for LaneContext {}
277
278impl LaneContext {
279    /// Creates an empty context.
280    pub fn new() -> Self {
281        Self {
282            data: HashMap::new(),
283        }
284    }
285
286    /// Inserts a value, keyed by its concrete type.
287    ///
288    /// If a value of the same type was already present, it is replaced.
289    pub fn insert<T: 'static + Send + Sync>(&mut self, value: T) {
290        self.data.insert(TypeId::of::<T>(), Box::new(value));
291    }
292
293    /// Returns a shared reference to a value by type.
294    pub fn get<T: 'static>(&self) -> Option<&T> {
295        self.data.get(&TypeId::of::<T>())?.downcast_ref()
296    }
297
298    /// Returns a mutable reference to a value by type.
299    pub fn get_mut<T: 'static>(&mut self) -> Option<&mut T> {
300        self.data.get_mut(&TypeId::of::<T>())?.downcast_mut()
301    }
302
303    /// Checks whether a value of the given type is present.
304    pub fn contains<T: 'static>(&self) -> bool {
305        self.data.contains_key(&TypeId::of::<T>())
306    }
307
308    /// Removes and returns a value by type.
309    pub fn remove<T: 'static>(&mut self) -> Option<T> {
310        self.data
311            .remove(&TypeId::of::<T>())
312            .and_then(|b| b.downcast().ok().map(|b| *b))
313    }
314}
315
316impl Default for LaneContext {
317    fn default() -> Self {
318        Self::new()
319    }
320}
321
322impl fmt::Debug for LaneContext {
323    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
324        f.debug_struct("LaneContext")
325            .field("entries", &self.data.len())
326            .finish()
327    }
328}
329
330// ─────────────────────────────────────────────────────────────────────────────
331// Slot / Ref — safe-ish wrappers for borrowing through LaneContext
332// ─────────────────────────────────────────────────────────────────────────────
333
334/// Wraps a **mutable** borrow for storage in [`LaneContext`].
335///
336/// This erases the lifetime so the value can be stored in the type-map.
337/// The caller **must** ensure the `Slot` does not outlive the original
338/// reference (guaranteed by the stack-scoped context pattern).
339///
340/// ```rust,ignore
341/// use khora_core::lane::Slot;
342///
343/// let mut encoder: Box<dyn CommandEncoder> = /* ... */;
344/// let slot = Slot::new(encoder.as_mut());
345/// // slot.get() -> &mut dyn CommandEncoder
346/// ```
347pub struct Slot<T: ?Sized>(*mut T);
348
349// SAFETY: Slot is used only within single-threaded frame scopes.
350unsafe impl<T: ?Sized> Send for Slot<T> {}
351unsafe impl<T: ?Sized> Sync for Slot<T> {}
352
353impl<T: ?Sized> Slot<T> {
354    /// Creates a `Slot` from a mutable reference.
355    pub fn new(value: &mut T) -> Self {
356        Self(value as *mut T)
357    }
358
359    /// Creates a `Slot` from a raw pointer.
360    ///
361    /// # Safety
362    ///
363    /// The caller must ensure:
364    /// - The pointer is valid and properly aligned.
365    /// - The pointed-to data outlives every use of this `Slot`.
366    /// - No other mutable reference to the data exists while the `Slot` is live.
367    pub unsafe fn from_raw(ptr: *mut T) -> Self {
368        Self(ptr)
369    }
370
371    /// Returns a mutable reference to the wrapped value.
372    ///
373    /// # Safety contract
374    ///
375    /// Safe when called within the scope where the original reference is
376    /// still alive and no other reference to the same data exists.
377    #[allow(clippy::mut_from_ref)]
378    pub fn get(&self) -> &mut T {
379        // SAFETY: guaranteed by single-lane-at-a-time execution
380        unsafe { &mut *self.0 }
381    }
382
383    /// Returns a shared reference to the wrapped value.
384    pub fn get_ref(&self) -> &T {
385        // SAFETY: same as get()
386        unsafe { &*self.0 }
387    }
388}
389
390/// Wraps a **shared** borrow for storage in [`LaneContext`].
391///
392/// Like [`Slot`] but for immutable references.
393pub struct Ref<T: ?Sized>(*const T);
394
395// SAFETY: Ref is used only within single-threaded frame scopes.
396unsafe impl<T: ?Sized> Send for Ref<T> {}
397unsafe impl<T: ?Sized> Sync for Ref<T> {}
398
399impl<T: ?Sized> Ref<T> {
400    /// Creates a `Ref` from a shared reference.
401    pub fn new(value: &T) -> Self {
402        Self(value as *const T)
403    }
404
405    /// Returns a shared reference to the wrapped value.
406    pub fn get(&self) -> &T {
407        // SAFETY: guaranteed by frame-scoped lifetime
408        unsafe { &*self.0 }
409    }
410}
411
412// ─────────────────────────────────────────────────────────────────────────────
413// LaneRegistry — generic container for heterogeneous lanes
414// ─────────────────────────────────────────────────────────────────────────────
415
416/// A registry that stores [`Lane`] trait objects for agent use.
417///
418/// Agents use a `LaneRegistry` instead of domain-specific vectors
419/// (e.g., `Vec<Box<dyn RenderLane>>`). This enables developers to add
420/// custom lanes without modifying agent code.
421///
422/// ```rust,ignore
423/// use khora_core::lane::{LaneRegistry, LaneKind};
424///
425/// let mut reg = LaneRegistry::new();
426/// reg.register(Box::new(MyCustomLane::new()));
427///
428/// // Find all render lanes
429/// let render_lanes = reg.find_by_kind(LaneKind::Render);
430/// ```
431pub struct LaneRegistry {
432    lanes: Vec<Box<dyn Lane>>,
433}
434
435impl LaneRegistry {
436    /// Creates an empty registry.
437    pub fn new() -> Self {
438        Self { lanes: Vec::new() }
439    }
440
441    /// Adds a lane to the registry.
442    pub fn register(&mut self, lane: Box<dyn Lane>) {
443        self.lanes.push(lane);
444    }
445
446    /// Finds a lane by its strategy name.
447    pub fn get(&self, name: &str) -> Option<&dyn Lane> {
448        self.lanes
449            .iter()
450            .find(|l| l.strategy_name() == name)
451            .map(|b| b.as_ref())
452    }
453
454    /// Returns all lanes of a given kind.
455    pub fn find_by_kind(&self, kind: LaneKind) -> Vec<&dyn Lane> {
456        self.lanes
457            .iter()
458            .filter(|l| l.lane_kind() == kind)
459            .map(|b| b.as_ref())
460            .collect()
461    }
462
463    /// Returns a slice of all registered lanes.
464    pub fn all(&self) -> &[Box<dyn Lane>] {
465        &self.lanes
466    }
467
468    /// Returns the number of registered lanes.
469    pub fn len(&self) -> usize {
470        self.lanes.len()
471    }
472
473    /// Returns `true` if no lanes are registered.
474    pub fn is_empty(&self) -> bool {
475        self.lanes.is_empty()
476    }
477}
478
479impl Default for LaneRegistry {
480    fn default() -> Self {
481        Self::new()
482    }
483}
484
485/// Base trait for ALL lane types in the KhoraEngine.
486///
487/// Every lane — regardless of domain — implements this trait, providing
488/// a common interface for identity, classification, lifecycle, and execution.
489/// This enables agents to reason about lanes generically during GORNA
490/// resource negotiation.
491///
492/// ## Lifecycle
493///
494/// ```text
495/// on_initialize(ctx)  →  [ execute(ctx) ]*  →  on_shutdown(ctx)
496/// ```
497///
498/// - **`on_initialize`** is called once when the lane is registered with an agent
499///   or when the underlying device/context changes.
500/// - **`execute`** is the main entry point, called each frame/tick by the owning agent.
501/// - **`on_shutdown`** is called when the lane is unregistered or the agent shuts down.
502///
503/// ## LaneContext
504///
505/// All lifecycle methods receive a [`LaneContext`] — a type-map where
506/// agents insert domain-specific data and lanes retrieve it by type.
507/// This decouples agents from domain-specific lane traits.
508pub trait Lane: Send + Sync {
509    /// Human-readable name identifying this lane's strategy.
510    ///
511    /// Used for logging, debugging, and GORNA negotiation.
512    /// Should be unique within a lane kind (e.g., `"LitForward"`, `"StandardPhysics"`).
513    fn strategy_name(&self) -> &'static str;
514
515    /// The kind of processing this lane performs.
516    ///
517    /// Used by agents to classify and route lanes to the appropriate
518    /// execution context.
519    fn lane_kind(&self) -> LaneKind;
520
521    /// Estimated computational cost of running this lane.
522    ///
523    /// Used by agents during GORNA resource negotiation to select
524    /// lanes that fit within their allocated budget. Higher values
525    /// indicate more expensive strategies.
526    ///
527    /// Default returns `1.0` (medium cost). Override for more
528    /// accurate estimation. The [`LaneContext`] may contain scene data
529    /// needed for a more precise estimate.
530    fn estimate_cost(&self, _ctx: &LaneContext) -> f32 {
531        1.0
532    }
533
534    // --- Lifecycle ---
535
536    /// Called once when the lane is registered or the underlying context resets.
537    ///
538    /// The [`LaneContext`] contains domain-specific resources. For example,
539    /// render lanes expect an `Arc<dyn GraphicsDevice>` in the context.
540    ///
541    /// Default is a no-op returning `Ok(())`.
542    fn on_initialize(&self, _ctx: &mut LaneContext) -> Result<(), LaneError> {
543        Ok(())
544    }
545
546    /// Main execution entry point — called each frame/tick by the owning agent.
547    ///
548    /// The [`LaneContext`] carries all the data the lane needs to do its work.
549    /// Lanes extract typed values using `ctx.get::<T>()`.
550    ///
551    /// Default is a no-op returning `Ok(())`.
552    fn execute(&self, _ctx: &mut LaneContext) -> Result<(), LaneError> {
553        Ok(())
554    }
555
556    /// Called when the lane is being destroyed or the context is shutting down.
557    ///
558    /// Default is a no-op.
559    fn on_shutdown(&self, _ctx: &mut LaneContext) {}
560
561    // --- Downcasting ---
562
563    /// Downcast to a concrete type for type-specific operations.
564    fn as_any(&self) -> &dyn Any;
565
566    /// Downcast to a concrete type (mutable) for type-specific operations.
567    fn as_any_mut(&mut self) -> &mut dyn Any;
568}