khora_agents/physics_agent/
agent.rs1use std::sync::{Arc, Mutex};
22use std::time::Duration;
23
24use khora_core::agent::{Agent, AgentImportance, ExecutionPhase, ExecutionTiming};
25use khora_core::control::gorna::{
26 measured_frame_time_ms, AgentFrameStatusMap, AgentId, AgentStatus, NegotiationRequest,
27 NegotiationResponse, ResourceBudget, StrategyId, StrategyOption,
28};
29use khora_core::lane::PhysicsDeltaTime;
30use khora_core::lane::{LaneContext, LaneRegistry, Slot};
31use khora_core::physics::PhysicsProvider;
32use khora_core::EngineContext;
33use khora_data::ecs::World;
34use khora_lanes::physics_lane::StandardPhysicsLane;
35
36const COST_TO_MS_SCALE: f32 = 3.0;
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
48pub enum PhysicsStrategy {
49 #[default]
51 Standard,
52 Simplified,
54}
55
56pub struct PhysicsAgent {
61 lanes: LaneRegistry,
63 strategy: PhysicsStrategy,
65 current_strategy: StrategyId,
67 time_budget: Duration,
69 fixed_timestep: f32,
71 frame_status: Option<AgentFrameStatusMap>,
74}
75
76impl Agent for PhysicsAgent {
77 fn id(&self) -> AgentId {
78 AgentId::Physics
79 }
80
81 fn negotiate(&mut self, _request: NegotiationRequest) -> NegotiationResponse {
82 let complexity_factor = 1.0_f32;
87
88 NegotiationResponse {
89 strategies: vec![
90 StrategyOption {
91 id: StrategyId::LowPower,
92 estimated_time: Duration::from_secs_f32(
93 (0.5 * complexity_factor * COST_TO_MS_SCALE).max(0.1) / 1000.0,
94 ),
95 estimated_vram: 0,
96 },
97 StrategyOption {
98 id: StrategyId::Balanced,
99 estimated_time: Duration::from_secs_f32(
100 (1.5 * complexity_factor * COST_TO_MS_SCALE).max(0.5) / 1000.0,
101 ),
102 estimated_vram: 0,
103 },
104 StrategyOption {
105 id: StrategyId::HighPerformance,
106 estimated_time: Duration::from_secs_f32(
107 (3.0 * complexity_factor * COST_TO_MS_SCALE).max(1.0) / 1000.0,
108 ),
109 estimated_vram: 0,
110 },
111 ],
112 timing_adjustment: None,
113 }
114 }
115
116 fn apply_budget(&mut self, budget: ResourceBudget) {
117 log::info!(
118 "PhysicsAgent: Strategy update to {:?} (time_limit={:?})",
119 budget.strategy_id,
120 budget.time_limit,
121 );
122
123 match budget.strategy_id {
124 StrategyId::LowPower => {
125 self.strategy = PhysicsStrategy::Simplified;
126 self.fixed_timestep = 1.0 / 30.0;
127 }
128 StrategyId::Balanced => {
129 self.strategy = PhysicsStrategy::Standard;
130 self.fixed_timestep = 1.0 / 60.0;
131 }
132 StrategyId::HighPerformance => {
133 self.strategy = PhysicsStrategy::Standard;
134 self.fixed_timestep = 1.0 / 120.0;
135 }
136 StrategyId::Custom(_) => {
137 log::warn!(
138 "PhysicsAgent received unsupported custom strategy. Falling back to Standard."
139 );
140 self.strategy = PhysicsStrategy::Standard;
141 self.fixed_timestep = 1.0 / 60.0;
142 }
143 }
144
145 self.current_strategy = budget.strategy_id;
146 self.time_budget = budget.time_limit;
147 }
148
149 fn on_initialize(&mut self, context: &mut EngineContext<'_>) {
150 self.frame_status = context
151 .runtime
152 .resources
153 .get::<AgentFrameStatusMap>()
154 .cloned();
155 }
156
157 fn execute(&mut self, context: &mut EngineContext<'_>) {
158 let Some(provider_arc) = context
160 .runtime
161 .backends
162 .get::<Arc<Mutex<Box<dyn PhysicsProvider>>>>()
163 else {
164 log::debug!("PhysicsAgent: no physics provider registered, skipping step");
165 return;
166 };
167 let provider_arc: Arc<Mutex<Box<dyn PhysicsProvider>>> = (*provider_arc).clone();
168
169 let Some(world_any) = context.world_mut() else {
170 return;
171 };
172 let Some(world) = world_any.downcast_mut::<World>() else {
173 return;
174 };
175
176 let mut provider_guard = match provider_arc.lock() {
177 Ok(g) => g,
178 Err(e) => {
179 log::error!("PhysicsAgent: provider mutex poisoned: {}", e);
180 return;
181 }
182 };
183
184 let mut ctx = LaneContext::new();
185 ctx.insert(PhysicsDeltaTime(self.fixed_timestep));
186 ctx.insert(Slot::new(world));
187 ctx.insert(Slot::new(provider_guard.as_mut()));
188 ctx.insert(Slot::new(&mut *context.deck));
192
193 let lane_name = "StandardPhysics";
197
198 if let Some(lane) = self.lanes.get(lane_name) {
199 if let Err(e) = lane.execute(&mut ctx) {
200 log::error!("Physics lane {} failed: {}", lane.strategy_name(), e);
201 }
202 }
203 }
204
205 fn report_status(&self) -> AgentStatus {
206 let measured_time_ms = measured_frame_time_ms(&self.frame_status, self.id());
207 let health_score = if self.time_budget.is_zero() || measured_time_ms <= 0.0 {
208 1.0
209 } else {
210 (self.time_budget.as_secs_f32() * 1000.0 / measured_time_ms).min(1.0)
211 };
212
213 AgentStatus {
214 agent_id: self.id(),
215 health_score,
216 current_strategy: self.current_strategy,
217 is_stalled: false,
218 message: format!("step_time={measured_time_ms:.2}ms"),
219 }
220 }
221
222 fn as_any(&self) -> &dyn std::any::Any {
223 self
224 }
225
226 fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
227 self
228 }
229
230 fn execution_timing(&self) -> ExecutionTiming {
231 ExecutionTiming {
232 allowed_phases: vec![ExecutionPhase::TRANSFORM],
233 default_phase: ExecutionPhase::TRANSFORM,
234 priority: 0.9,
235 importance: AgentImportance::Critical,
236 fixed_timestep: Some(Duration::from_secs_f32(self.fixed_timestep)),
237 dependencies: Vec::new(),
238 }
239 }
240}
241
242impl Default for PhysicsAgent {
243 fn default() -> Self {
244 let mut lanes = LaneRegistry::new();
245 lanes.register(Box::new(StandardPhysicsLane::new()));
246 Self {
252 lanes,
253 strategy: PhysicsStrategy::Standard,
254 current_strategy: StrategyId::Balanced,
255 time_budget: Duration::ZERO,
256 fixed_timestep: 1.0 / 60.0,
257 frame_status: None,
258 }
259 }
260}