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}