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}