khora_agents/physics_agent/
agent.rs1use std::time::{Duration, Instant};
21
22use crossbeam_channel::Sender;
23use khora_core::agent::Agent;
24use khora_core::control::gorna::{
25 AgentId, AgentStatus, NegotiationRequest, NegotiationResponse, ResourceBudget, StrategyId,
26 StrategyOption,
27};
28use khora_core::lane::PhysicsDeltaTime;
29use khora_core::lane::{Lane, LaneContext, LaneKind, LaneRegistry, Slot};
30use khora_core::physics::PhysicsProvider;
31use khora_core::telemetry::event::TelemetryEvent;
32use khora_core::telemetry::monitoring::GpuReport;
33use khora_data::ecs::World;
34use khora_lanes::physics_lane::StandardPhysicsLane;
35use khora_telemetry::metrics::registry::{GaugeHandle, MetricsRegistry};
36
37const COST_TO_MS_SCALE: f32 = 3.0;
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
42pub enum PhysicsStrategy {
43 #[default]
45 Standard,
46 Simplified,
48 Debug,
50}
51
52struct PhysicsMetrics {
54 body_count: GaugeHandle,
56 collider_count: GaugeHandle,
58 step_time_ms: GaugeHandle,
60}
61
62pub struct PhysicsAgent {
67 provider: Box<dyn PhysicsProvider>,
69 lanes: LaneRegistry,
71 strategy: PhysicsStrategy,
73 current_strategy: StrategyId,
75 metrics: Option<PhysicsMetrics>,
77 telemetry_sender: Option<Sender<TelemetryEvent>>,
79 last_step_time: Duration,
81 time_budget: Duration,
83 frame_count: u64,
85 fixed_timestep: f32,
87}
88
89impl Agent for PhysicsAgent {
90 fn id(&self) -> AgentId {
91 AgentId::Physics
92 }
93
94 fn negotiate(&mut self, _request: NegotiationRequest) -> NegotiationResponse {
95 let body_count = self.provider.get_all_bodies().len() as u64;
96 let complexity_factor = 1.0 + (body_count as f32 / 100.0).min(5.0);
97
98 NegotiationResponse {
99 strategies: vec![
100 StrategyOption {
101 id: StrategyId::LowPower,
102 estimated_time: Duration::from_secs_f32(
103 (0.5 * complexity_factor * COST_TO_MS_SCALE).max(0.1) / 1000.0,
104 ),
105 estimated_vram: 0,
106 },
107 StrategyOption {
108 id: StrategyId::Balanced,
109 estimated_time: Duration::from_secs_f32(
110 (1.5 * complexity_factor * COST_TO_MS_SCALE).max(0.5) / 1000.0,
111 ),
112 estimated_vram: 0,
113 },
114 StrategyOption {
115 id: StrategyId::HighPerformance,
116 estimated_time: Duration::from_secs_f32(
117 (3.0 * complexity_factor * COST_TO_MS_SCALE).max(1.0) / 1000.0,
118 ),
119 estimated_vram: 0,
120 },
121 ],
122 }
123 }
124
125 fn apply_budget(&mut self, budget: ResourceBudget) {
126 log::info!(
127 "PhysicsAgent: Strategy update to {:?} (time_limit={:?})",
128 budget.strategy_id,
129 budget.time_limit,
130 );
131
132 match budget.strategy_id {
133 StrategyId::LowPower => {
134 self.strategy = PhysicsStrategy::Simplified;
135 self.fixed_timestep = 1.0 / 30.0;
136 }
137 StrategyId::Balanced => {
138 self.strategy = PhysicsStrategy::Standard;
139 self.fixed_timestep = 1.0 / 60.0;
140 }
141 StrategyId::HighPerformance => {
142 self.strategy = PhysicsStrategy::Standard;
143 self.fixed_timestep = 1.0 / 120.0;
144 }
145 StrategyId::Custom(_) => {
146 log::warn!(
147 "PhysicsAgent received unsupported custom strategy. Falling back to Standard."
148 );
149 self.strategy = PhysicsStrategy::Standard;
150 self.fixed_timestep = 1.0 / 60.0;
151 }
152 }
153
154 self.current_strategy = budget.strategy_id;
155 self.time_budget = budget.time_limit;
156 }
157
158 fn update(&mut self, context: &mut khora_core::EngineContext<'_>) {
159 if let Some(world_any) = context.world.as_deref_mut() {
160 if let Some(world) = world_any.downcast_mut::<World>() {
161 self.step(world, self.fixed_timestep);
162 }
163 }
164 self.emit_telemetry();
165 }
166
167 fn report_status(&self) -> AgentStatus {
168 let health_score = if self.time_budget.is_zero() || self.frame_count == 0 {
169 1.0
170 } else {
171 let ratio =
172 self.time_budget.as_secs_f32() / self.last_step_time.as_secs_f32().max(0.0001);
173 ratio.min(1.0)
174 };
175
176 let body_count = self.provider.get_all_bodies().len();
177 let collider_count = self.provider.get_all_colliders().len();
178
179 AgentStatus {
180 agent_id: self.id(),
181 health_score,
182 current_strategy: self.current_strategy,
183 is_stalled: self.frame_count == 0,
184 message: format!(
185 "step_time={:.2}ms bodies={} colliders={}",
186 self.last_step_time.as_secs_f32() * 1000.0,
187 body_count,
188 collider_count,
189 ),
190 }
191 }
192
193 fn execute(&mut self) {
194 }
196
197 fn as_any(&self) -> &dyn std::any::Any {
198 self
199 }
200
201 fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
202 self
203 }
204}
205
206impl PhysicsAgent {
207 pub fn new(provider: Box<dyn PhysicsProvider>) -> Self {
209 let mut lanes = LaneRegistry::new();
210 lanes.register(Box::new(StandardPhysicsLane::new()));
211 lanes.register(Box::new(khora_lanes::physics_lane::PhysicsDebugLane::new()));
212
213 Self {
214 provider,
215 lanes,
216 strategy: PhysicsStrategy::Standard,
217 current_strategy: StrategyId::Balanced,
218 metrics: None,
219 telemetry_sender: None,
220 last_step_time: Duration::ZERO,
221 time_budget: Duration::ZERO,
222 frame_count: 0,
223 fixed_timestep: 1.0 / 60.0,
224 }
225 }
226
227 pub fn with_telemetry(mut self, registry: &MetricsRegistry) -> Self {
229 let metrics = PhysicsMetrics {
230 body_count: registry
231 .register_gauge(
232 "physics",
233 "body_count",
234 "Total active rigid bodies",
235 "count",
236 )
237 .unwrap(),
238 collider_count: registry
239 .register_gauge(
240 "physics",
241 "collider_count",
242 "Total active colliders",
243 "count",
244 )
245 .unwrap(),
246 step_time_ms: registry
247 .register_gauge(
248 "physics",
249 "step_time_ms",
250 "Time spent in simulation step",
251 "ms",
252 )
253 .unwrap(),
254 };
255 self.metrics = Some(metrics);
256 self
257 }
258
259 pub fn with_dcc_sender(mut self, sender: Sender<TelemetryEvent>) -> Self {
261 self.telemetry_sender = Some(sender);
262 self
263 }
264
265 pub fn step(&mut self, world: &mut World, dt: f32) {
267 let start = Instant::now();
268
269 let mut ctx = LaneContext::new();
271 ctx.insert(PhysicsDeltaTime(dt));
272 ctx.insert(Slot::new(world));
273 ctx.insert(Slot::new(self.provider.as_mut()));
274
275 let lane_name = self.select_lane_name();
277 if let Some(lane) = self.lanes.get(lane_name) {
278 if let Err(e) = lane.execute(&mut ctx) {
279 log::error!("Physics lane {} failed: {}", lane.strategy_name(), e);
280 }
281 }
282
283 self.last_step_time = start.elapsed();
284 self.frame_count += 1;
285
286 if let Some(metrics) = &self.metrics {
287 let _ = metrics
288 .body_count
289 .set(self.provider.get_all_bodies().len() as f64);
290 let _ = metrics
291 .collider_count
292 .set(self.provider.get_all_colliders().len() as f64);
293 let _ = metrics
294 .step_time_ms
295 .set(self.last_step_time.as_secs_f64() * 1000.0);
296 }
297 }
298
299 fn emit_telemetry(&self) {
300 if let Some(sender) = &self.telemetry_sender {
301 let report = GpuReport {
302 frame_number: self.frame_count,
303 draw_calls: 0,
304 triangles_rendered: 0,
305 ..Default::default()
306 };
307 let _ = sender.send(TelemetryEvent::GpuReport(report));
308 }
309 }
310
311 fn select_lane_name(&self) -> &'static str {
313 match self.strategy {
314 PhysicsStrategy::Standard | PhysicsStrategy::Simplified => "StandardPhysics",
315 PhysicsStrategy::Debug => "PhysicsDebug",
316 }
317 }
318
319 pub fn select_lane(&self) -> &dyn Lane {
321 let name = self.select_lane_name();
322 self.lanes.get(name).unwrap_or_else(|| {
323 self.lanes
324 .find_by_kind(LaneKind::Physics)
325 .first()
326 .copied()
327 .expect("PhysicsAgent has no physics lanes configured")
328 })
329 }
330
331 pub fn cast_ray(
333 &self,
334 ray: &khora_core::physics::Ray,
335 max_toi: f32,
336 solid: bool,
337 ) -> Option<khora_core::physics::RaycastHit> {
338 self.provider.cast_ray(ray, max_toi, solid)
339 }
340
341 pub fn get_debug_render_data(&self) -> (Vec<khora_core::math::Vec3>, Vec<[u32; 2]>) {
343 self.provider.get_debug_render_data()
344 }
345
346 pub fn last_step_time(&self) -> Duration {
348 self.last_step_time
349 }
350
351 pub fn frame_count(&self) -> u64 {
353 self.frame_count
354 }
355
356 pub fn current_strategy_id(&self) -> StrategyId {
358 self.current_strategy
359 }
360
361 pub fn fixed_timestep(&self) -> f32 {
363 self.fixed_timestep
364 }
365
366 pub fn set_fixed_timestep(&mut self, dt: f32) {
368 self.fixed_timestep = dt;
369 }
370}