khora_core/control/gorna.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//! Types and traits for the Goal-Oriented Resource Negotiation & Allocation (GORNA) protocol.
16
17use crate::agent::mode::EngineMode;
18use crate::agent::timing::{AgentImportance, ExecutionTiming};
19use serde::{Deserialize, Serialize};
20use std::collections::HashMap;
21use std::time::Duration;
22
23/// Unique identifier for engine agents with implicit priority ordering.
24///
25/// The order of variants defines the default execution priority (first = highest).
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
27pub enum AgentId {
28 /// The primary rendering agent (highest priority in Simulation).
29 Renderer,
30 /// The shadow map rendering agent (runs in OBSERVE phase before Renderer).
31 ShadowRenderer,
32 /// Overlay / debug-viz rendering (gizmos, wireframes, emissive). Runs
33 /// after `Renderer` in the OUTPUT phase. Lanes activate independently
34 /// based on context flags rather than a single budget-driven strategy.
35 Overlay,
36 /// Skybox / environment background. Runs after `Renderer` in the OUTPUT
37 /// phase and draws the environment cube behind the scene geometry
38 /// (depth-tested), so the visible sky matches what surfaces reflect.
39 Skybox,
40 /// The physics simulation agent.
41 Physics,
42 /// The ECS/Logic coordination agent.
43 Ecs,
44 /// The UI layout and interaction agent.
45 Ui,
46 /// The audio processing agent.
47 Audio,
48 /// The asset management agent (highest priority in Boot).
49 Asset,
50}
51
52impl std::fmt::Display for AgentId {
53 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54 write!(f, "{:?}", self)
55 }
56}
57
58/// Generic strategy identifier for budget allocation.
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
60pub enum StrategyId {
61 /// Minimum resource usage, lowest quality/frequency.
62 LowPower,
63 /// Balanced resource usage.
64 Balanced,
65 /// High resource usage, maximum quality/performance.
66 HighPerformance,
67 /// Custom ID for agent-specific strategies.
68 /// Used when the predefined levels aren't sufficient.
69 Custom(u32),
70}
71
72/// How much latitude GORNA has to change an agent's strategy — the
73/// developer-control surface over the adaptive core.
74///
75/// This is what keeps the engine a *partnership* rather than an autocracy: the
76/// DCC observes and proposes, but the developer decides how much it may act. Set
77/// per agent; `Learning` is the default. (A death-spiral safety stop can still
78/// force `LowPower` in any mode — safety overrides developer control.)
79#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
80pub enum AdaptationMode {
81 /// Full GORNA negotiation — the DCC freely picks the best-fitting strategy
82 /// each tick. The engine's default.
83 #[default]
84 Learning,
85 /// The agent is **pinned** to a fixed strategy; GORNA observes and reports
86 /// but never switches it. The developer takes control.
87 Manual(StrategyId),
88 /// **Predictable**: GORNA may *downgrade* under budget pressure but never
89 /// makes an opportunistic *upgrade*, so the strategy doesn't flap up and
90 /// down frame to frame.
91 Stable,
92 /// **Learning within limits**: the chosen strategy is clamped to the
93 /// `[min, max]` range (ordering `LowPower < Balanced < HighPerformance`;
94 /// `Custom` ranks above `HighPerformance`).
95 Bounded {
96 /// Lowest strategy GORNA may select.
97 min: StrategyId,
98 /// Highest strategy GORNA may select.
99 max: StrategyId,
100 },
101}
102
103/// A developer/editor hint that biases GORNA arbitration without changing game
104/// semantics — the "adapt the HOW, not the WHAT" control surface, on the same
105/// axis as [`AdaptationMode`]. Hints are advisory: a death-spiral safety stop
106/// and a `Manual` pin both still win over them. Sent to the DCC over the hint
107/// channel; the DCC folds them per agent (see [`AgentHints`]) and feeds the
108/// accumulated state into each arbitration round.
109#[derive(Debug, Clone, Copy, PartialEq)]
110pub enum EngineHint {
111 /// Cap an agent's per-frame time budget: GORNA will not issue a strategy
112 /// whose estimated cost exceeds `max_ms`, clamping toward cheaper
113 /// strategies. Re-send with a large `max_ms` to lift a previous cap.
114 Cap {
115 /// The agent to cap.
116 agent: AgentId,
117 /// Maximum per-frame strategy cost, in milliseconds.
118 max_ms: f32,
119 },
120 /// Bias an agent's negotiation priority weight (higher = more budget share
121 /// when the fit upgrades agents). Overrides the default per-agent priority.
122 Prioritize {
123 /// The agent to reprioritize.
124 agent: AgentId,
125 /// Priority weight (typically 0.0–1.0; higher wins budget first).
126 weight: f32,
127 },
128}
129
130impl EngineHint {
131 /// The agent this hint targets.
132 pub fn agent(&self) -> AgentId {
133 match self {
134 EngineHint::Cap { agent, .. } | EngineHint::Prioritize { agent, .. } => *agent,
135 }
136 }
137}
138
139/// The accumulated hint state for one agent, folded from the [`EngineHint`]s
140/// the DCC has received. A `None` field means "no developer hint — use the
141/// engine default". Persists across ticks until overwritten by a newer hint.
142#[derive(Debug, Clone, Copy, Default, PartialEq)]
143pub struct AgentHints {
144 /// Per-frame time-budget ceiling in milliseconds, if capped.
145 pub cap_ms: Option<f32>,
146 /// Overridden negotiation priority weight, if reprioritized.
147 pub priority: Option<f32>,
148}
149
150impl AgentHints {
151 /// Folds a single hint into this per-agent state (latest value wins per kind).
152 pub fn apply(&mut self, hint: EngineHint) {
153 match hint {
154 EngineHint::Cap { max_ms, .. } => self.cap_ms = Some(max_ms),
155 EngineHint::Prioritize { weight, .. } => self.priority = Some(weight),
156 }
157 }
158}
159
160/// One arbitration tick's outcome: the strategy issued to each agent, in
161/// issuance order.
162pub type TickDecisions = Vec<(AgentId, StrategyId)>;
163
164/// An ordered, replayable recording of GORNA's per-tick decisions.
165///
166/// Arbitration is deterministic (no RNG), so recording the issued strategy per
167/// agent per tick and replaying it reproduces a session's adaptation
168/// **bit-for-bit** — for QA, network lockstep, and bug reproduction. Recorded by
169/// the DCC while recording is enabled; fed back to drive issuance from the trace
170/// instead of from live negotiation (the `Replay` capability).
171#[derive(Debug, Clone, Default)]
172pub struct DecisionTrace {
173 /// Per-tick issued decisions, in arbitration order.
174 pub ticks: Vec<TickDecisions>,
175}
176
177/// Hard resource constraints the DCC imposes on an Agent during negotiation.
178///
179/// These represent non-negotiable limits that any proposed strategy must respect.
180#[derive(Debug, Clone, Default)]
181pub struct ResourceConstraints {
182 /// Maximum VRAM usage allowed, in bytes. `None` means unconstrained.
183 pub max_vram_bytes: Option<u64>,
184 /// Maximum system memory allowed, in bytes. `None` means unconstrained.
185 pub max_memory_bytes: Option<u64>,
186 /// If `true`, this agent is critical and must always execute (e.g. physics in Simulation).
187 pub must_run: bool,
188}
189
190/// A request sent by the DCC to an Agent to negotiate resources.
191#[derive(Debug, Clone)]
192pub struct NegotiationRequest {
193 /// The target latency for the frame or subsystem (e.g. 16.6ms).
194 pub target_latency: Duration,
195 /// Priority weight (0.0 to 1.0) assigned by the DCC.
196 pub priority_weight: f32,
197 /// Hard resource constraints that any proposed strategy must respect.
198 pub constraints: ResourceConstraints,
199 /// The current engine mode.
200 pub current_mode: EngineMode,
201 /// The timing declared by the agent. GORNA can read this for decisions.
202 pub agent_timing: ExecutionTiming,
203}
204
205/// A response from an Agent offering various execution strategies.
206#[derive(Debug, Clone)]
207pub struct NegotiationResponse {
208 /// List of available strategies and their estimated costs.
209 pub strategies: Vec<StrategyOption>,
210 /// GORNA can suggest an importance change (NEVER a forced phase).
211 pub timing_adjustment: Option<TimingAdjustment>,
212}
213
214/// Suggested timing adjustment from GORNA to an agent.
215/// GORNA can NEVER force a phase — only suggest importance changes.
216#[derive(Debug, Clone)]
217pub struct TimingAdjustment {
218 /// Suggested importance override for the agent on this frame.
219 pub importance_override: Option<AgentImportance>,
220}
221
222/// A specific execution strategy offered by an Agent.
223#[derive(Debug, Clone)]
224pub struct StrategyOption {
225 /// Unique identifier for the strategy.
226 pub id: StrategyId,
227 /// Expected cost in time.
228 pub estimated_time: Duration,
229 /// Expected cost in VRAM.
230 pub estimated_vram: u64,
231}
232
233/// An allocated resource budget issued by the DCC to an Agent.
234#[derive(Debug, Clone)]
235pub struct ResourceBudget {
236 /// The strategy ID to be applied.
237 pub strategy_id: StrategyId,
238 /// Maximum time allowed for execution.
239 pub time_limit: Duration,
240 /// Maximum VRAM budget in bytes, if constrained.
241 pub memory_limit: Option<u64>,
242 /// Additional ISA-specific parameters.
243 pub extra_params: HashMap<String, String>,
244}
245
246/// A snapshot of an Agent's current health and performance.
247#[derive(Debug, Clone)]
248pub struct AgentStatus {
249 /// The ID of the reporting agent.
250 pub agent_id: AgentId,
251 /// The strategy currently being executed.
252 pub current_strategy: StrategyId,
253 /// Health score (0.0 to 1.0). 1.0 means adhering perfectly to budget.
254 pub health_score: f32,
255 /// True if the agent is blocked or failed to execute.
256 pub is_stalled: bool,
257 /// Human-readable status message for telemetry.
258 pub message: String,
259}
260
261/// Per-frame execution metrics for one agent, **measured and written by the
262/// scheduler** — agents hold no per-frame counters of their own. An agent
263/// reads its own slot in `report_status` to derive `health_score` from the
264/// GORNA time budget it retains.
265#[derive(Debug, Clone, Copy, Default)]
266pub struct AgentFrameStatus {
267 /// Wall-clock duration of the agent's last `execute`, in milliseconds.
268 /// `0.0` means the agent did not run last frame (skipped or not yet scheduled).
269 pub measured_time_ms: f32,
270}
271
272/// Shared, scheduler-owned map of the latest [`AgentFrameStatus`] per agent.
273///
274/// Lives in [`Resources`](crate::Resources): the scheduler writes it at its
275/// per-agent measurement point and agents read their own slot, so the
276/// per-frame numbers never live as agent state.
277pub type AgentFrameStatusMap =
278 std::sync::Arc<std::sync::RwLock<std::collections::HashMap<AgentId, AgentFrameStatus>>>;
279
280/// Reads the scheduler-measured `execute` time (ms) for `id` out of a shared
281/// [`AgentFrameStatusMap`], yielding `0.0` when the map is absent or has no
282/// entry yet. Agents call this from `report_status` so they never cache
283/// per-frame timing themselves.
284#[must_use]
285pub fn measured_frame_time_ms(map: &Option<AgentFrameStatusMap>, id: AgentId) -> f32 {
286 let Some(map) = map else {
287 return 0.0;
288 };
289 map.read()
290 .unwrap_or_else(|e| e.into_inner())
291 .get(&id)
292 .map(|s| s.measured_time_ms)
293 .unwrap_or(0.0)
294}