Skip to main content

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}