1pub use khora_core::platform::{BatteryLevel, ThermalStatus};
18
19#[derive(Debug, Clone, Default)]
21pub struct HardwareState {
22 pub thermal: ThermalStatus,
24 pub battery: BatteryLevel,
26 pub cpu_load: f32,
28 pub gpu_load: f32,
30 pub available_vram: Option<u64>,
32 pub total_vram: Option<u64>,
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
38pub enum ExecutionPhase {
39 #[default]
41 Boot,
42 Menu,
44 Simulation,
46 Background,
48}
49
50impl ExecutionPhase {
51 pub fn can_transition_to(&self, target: ExecutionPhase) -> bool {
59 match self {
60 ExecutionPhase::Boot => {
61 matches!(target, ExecutionPhase::Menu | ExecutionPhase::Simulation)
62 }
63 ExecutionPhase::Menu => matches!(
64 target,
65 ExecutionPhase::Simulation | ExecutionPhase::Background
66 ),
67 ExecutionPhase::Simulation => {
68 matches!(target, ExecutionPhase::Menu | ExecutionPhase::Background)
69 }
70 ExecutionPhase::Background => {
71 matches!(target, ExecutionPhase::Menu | ExecutionPhase::Simulation)
72 }
73 }
74 }
75
76 pub fn name(&self) -> &'static str {
78 match self {
79 ExecutionPhase::Boot => "boot",
80 ExecutionPhase::Menu => "menu",
81 ExecutionPhase::Simulation => "simulation",
82 ExecutionPhase::Background => "background",
83 }
84 }
85
86 pub fn from_name(name: &str) -> Option<Self> {
88 match name.to_lowercase().as_str() {
89 "boot" => Some(ExecutionPhase::Boot),
90 "menu" => Some(ExecutionPhase::Menu),
91 "simulation" => Some(ExecutionPhase::Simulation),
92 "background" => Some(ExecutionPhase::Background),
93 _ => None,
94 }
95 }
96}
97
98#[derive(Debug, Clone)]
100pub struct Context {
101 pub hardware: HardwareState,
103 pub phase: ExecutionPhase,
105 pub global_budget_multiplier: f32,
118}
119
120impl Default for Context {
121 fn default() -> Self {
122 Self {
123 hardware: HardwareState::default(),
124 phase: ExecutionPhase::default(),
125 global_budget_multiplier: 1.0,
126 }
127 }
128}
129
130impl Context {
131 pub fn refresh_budget_multiplier(&mut self) {
135 let thermal_factor: f32 = match self.hardware.thermal {
136 ThermalStatus::Cool => 1.0,
137 ThermalStatus::Warm => 0.9,
138 ThermalStatus::Throttling => 0.6,
139 ThermalStatus::Critical => 0.4,
140 };
141
142 let battery_factor = match self.hardware.battery {
143 BatteryLevel::Mains => 1.0,
144 BatteryLevel::High => 1.0,
145 BatteryLevel::Low => 0.8,
146 BatteryLevel::Critical => 0.5,
147 };
148
149 self.global_budget_multiplier = thermal_factor.min(battery_factor);
151 }
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157
158 #[test]
159 fn test_default_context_full_budget() {
160 let ctx = Context::default();
161 assert_eq!(ctx.global_budget_multiplier, 1.0);
162 assert_eq!(ctx.phase, ExecutionPhase::Boot);
163 }
164
165 #[test]
166 fn test_cool_mains_full_multiplier() {
167 let mut ctx = Context::default();
168 ctx.hardware.thermal = ThermalStatus::Cool;
169 ctx.hardware.battery = BatteryLevel::Mains;
170 ctx.refresh_budget_multiplier();
171 assert_eq!(ctx.global_budget_multiplier, 1.0);
172 }
173
174 #[test]
175 fn test_warm_reduces_multiplier() {
176 let mut ctx = Context::default();
177 ctx.hardware.thermal = ThermalStatus::Warm;
178 ctx.refresh_budget_multiplier();
179 assert!((ctx.global_budget_multiplier - 0.9).abs() < 0.001);
180 }
181
182 #[test]
183 fn test_throttling_heavy_reduction() {
184 let mut ctx = Context::default();
185 ctx.hardware.thermal = ThermalStatus::Throttling;
186 ctx.refresh_budget_multiplier();
187 assert!((ctx.global_budget_multiplier - 0.6).abs() < 0.001);
188 }
189
190 #[test]
191 fn test_critical_thermal_severe_reduction() {
192 let mut ctx = Context::default();
193 ctx.hardware.thermal = ThermalStatus::Critical;
194 ctx.refresh_budget_multiplier();
195 assert!((ctx.global_budget_multiplier - 0.4).abs() < 0.001);
196 }
197
198 #[test]
199 fn test_battery_low_reduces_multiplier() {
200 let mut ctx = Context::default();
201 ctx.hardware.battery = BatteryLevel::Low;
202 ctx.refresh_budget_multiplier();
203 assert!((ctx.global_budget_multiplier - 0.8).abs() < 0.001);
204 }
205
206 #[test]
207 fn test_battery_critical_severe_reduction() {
208 let mut ctx = Context::default();
209 ctx.hardware.battery = BatteryLevel::Critical;
210 ctx.refresh_budget_multiplier();
211 assert!((ctx.global_budget_multiplier - 0.5).abs() < 0.001);
212 }
213
214 #[test]
215 fn test_combined_thermal_and_battery_takes_minimum() {
216 let mut ctx = Context::default();
217 ctx.hardware.thermal = ThermalStatus::Throttling; ctx.hardware.battery = BatteryLevel::Critical; ctx.refresh_budget_multiplier();
220 assert!((ctx.global_budget_multiplier - 0.5).abs() < 0.001);
222 }
223}