Skip to main content

khora_data/flow/
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//! `Flow` — the typed interface between Data and Lanes.
16//!
17//! A [`Flow`] is a *per-domain*, **read-only** presenter of the World. It runs
18//! every tick during the Substrate Pass (before Lanes execute) in two steps:
19//!
20//! 1. **`select`** (read-only) — picks the entities relevant for this domain.
21//! 2. **`project`** (read-only) — builds a typed `View` published into the
22//!    [`LaneBus`](khora_core::lane::LaneBus). Lanes consume the view; they
23//!    never query the World directly.
24//!
25//! A Flow **never mutates the World**. Representation adaptation (AGDF memory
26//! layout) is the Data layer's own self-maintenance; semantic / gameplay
27//! mutation is developer-authored (an opt-in `DataSystem`), never automatic
28//! here. See `.agent/rules.md` — *adapt the HOW, never the WHAT*.
29//!
30//! Each domain (Render, UI, Physics, Audio, Shadow, …) defines its own
31//! `Flow` implementation. Adding a new domain costs **one** registration:
32//!
33//! ```rust,ignore
34//! inventory::submit! {
35//!     khora_data::flow::FlowRegistration {
36//!         name: MyFlow::NAME,
37//!         domain: MyFlow::DOMAIN,
38//!         run: my_flow_runner_trampoline,
39//!     }
40//! }
41//! ```
42
43pub mod audio;
44pub mod physics;
45mod registration;
46pub mod render;
47mod selection;
48pub mod shadow;
49pub mod ui;
50
51pub use audio::{
52    AudioFlow, AudioPlaybackUpdate, AudioPlaybackWriteback, AudioSourceSnapshot, AudioView,
53};
54pub use physics::{PhysicsFlow, PhysicsStepResult, PhysicsView};
55pub use registration::*;
56pub use render::RenderFlow;
57pub use selection::Selection;
58pub use shadow::{ShadowFlow, ShadowMatrices, ShadowView};
59pub use ui::UiFlow;
60
61use khora_core::Runtime;
62
63use crate::ecs::{SemanticDomain, World};
64
65/// The typed interface between the Data layer and Lanes.
66///
67/// All three stages receive the engine's [`Runtime`] so a Flow can look up
68/// services, backends, or resources its domain genuinely needs (text
69/// renderer, font cache, surface size, editor view overrides, …) without
70/// crossing the CLAD dependency graph in awkward ways.
71///
72/// # Examples
73///
74/// A read-only Flow that projects the World into a typed `View` for its domain's
75/// Lanes. `select` narrows the entities; `project` builds the view published into
76/// the [`LaneBus`](khora_core::lane::LaneBus). (Marked `ignore` because a
77/// compiling impl needs a concrete `View` type and a [`SemanticDomain`] from the
78/// data layer's domain set.)
79///
80/// ```ignore
81/// use khora_data::ecs::{SemanticDomain, World};
82/// use khora_data::flow::{Flow, Selection};
83/// use khora_core::Runtime;
84///
85/// #[derive(Clone)]
86/// struct MyView {
87///     entity_count: usize,
88/// }
89///
90/// struct MyFlow;
91///
92/// impl Flow for MyFlow {
93///     type View = MyView;
94///     const DOMAIN: SemanticDomain = SemanticDomain::Render;
95///     const NAME: &'static str = "MyFlow";
96///
97///     fn select(&mut self, world: &World, _runtime: &Runtime) -> Selection {
98///         // Pick the entities this domain cares about.
99///         Selection::new()
100///     }
101///
102///     fn project(&self, world: &World, _sel: &Selection, _runtime: &Runtime) -> Self::View {
103///         MyView { entity_count: world.iter_entities().count() }
104///     }
105/// }
106/// ```
107pub trait Flow: Send + Sync {
108    /// The typed view this Flow publishes into the LaneBus. `Clone` so the
109    /// registration trampoline can republish a cached view without
110    /// re-running `select`/`project` (views hold `Arc`s and plain data, so
111    /// cloning is cheap relative to a full re-projection).
112    type View: std::any::Any + Send + Sync + Clone + 'static;
113
114    /// Domain identifier — matches the agent's domain.
115    const DOMAIN: SemanticDomain;
116
117    /// Stable identifier — used for telemetry and ordering.
118    const NAME: &'static str;
119
120    /// Stage 1 — read-only selection of relevant entities.
121    fn select(&mut self, world: &World, runtime: &Runtime) -> Selection {
122        let _ = (world, runtime);
123        Selection::new()
124    }
125
126    /// Stage 2 — read-only projection of the world into a View.
127    fn project(&self, world: &World, sel: &Selection, runtime: &Runtime) -> Self::View;
128
129    /// Cache key for view reuse. When `Some(k)` matches the key of the
130    /// previously published view, the registration trampoline republishes
131    /// the cached view without re-running select/project. `None` (default)
132    /// disables caching — correct for flows whose projection depends on
133    /// inputs without a change signal.
134    ///
135    /// Implementations MUST fold every input the projection reads into the
136    /// key: the relevant [`World::domain_epoch`]s, [`World::instance_id`]
137    /// (so a different World instance never aliases a cached key), and a
138    /// bit-level hash of any `runtime` state consulted. A key that misses
139    /// an input produces *stale* views; an over-broad key merely
140    /// re-projects more often, which is always safe.
141    fn cache_key(&self, world: &World, runtime: &Runtime) -> Option<u64> {
142        let _ = (world, runtime);
143        None
144    }
145}
146
147/// Folds an ordered sequence of cache-key ingredients (domain epochs,
148/// bit-level hashes of runtime state, the World instance id) into a single
149/// `u64` via the std hasher. Deterministic within a process, which is all a
150/// per-process view cache needs.
151pub fn combine_cache_key<I: IntoIterator<Item = u64>>(parts: I) -> u64 {
152    use std::hash::{Hash, Hasher};
153    let mut hasher = std::collections::hash_map::DefaultHasher::new();
154    for part in parts {
155        part.hash(&mut hasher);
156    }
157    hasher.finish()
158}