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}