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