khora_agents/overlay_agent/
agent.rs1use std::sync::Arc;
28use std::time::Duration;
29
30use khora_core::agent::{
31 Agent, AgentAccess, AgentDependency, AgentImportance, DependencyKind, ExecutionPhase,
32 ExecutionTiming,
33};
34use khora_core::control::gorna::{
35 measured_frame_time_ms, AgentFrameStatusMap, AgentId, AgentStatus, NegotiationRequest,
36 NegotiationResponse, ResourceBudget, StrategyId, StrategyOption,
37};
38use khora_core::lane::{ClearColor, ColorTarget, DepthTarget, LaneContext, LaneRegistry, Slot};
39use khora_core::renderer::api::core::FrameContext;
40use khora_core::renderer::api::scene::GpuMesh;
41use khora_core::renderer::GraphicsDevice;
42use khora_core::EngineContext;
43use khora_data::assets::Assets;
44use khora_data::render::{
45 OverlayPassSlot, PassContribution, PassDescriptor, RenderWorld, ResourceId,
46};
47use khora_data::AssetStore;
48use std::sync::RwLock;
49
50pub struct OverlayAgent {
58 lanes: LaneRegistry,
60 time_budget: Duration,
62 current_strategy: StrategyId,
64 frame_status: Option<AgentFrameStatusMap>,
67}
68
69impl Agent for OverlayAgent {
70 fn id(&self) -> AgentId {
71 AgentId::Overlay
72 }
73
74 fn access(&self) -> AgentAccess {
78 AgentAccess::Isolated
79 }
80
81 fn deck_writes(&self) -> Vec<std::any::TypeId> {
83 vec![std::any::TypeId::of::<OverlayPassSlot>()]
84 }
85
86 fn negotiate(&mut self, _request: NegotiationRequest) -> NegotiationResponse {
87 let strategies = vec![StrategyOption {
92 id: StrategyId::Balanced,
93 estimated_time: Duration::from_micros(500),
94 estimated_vram: 1024 * 1024,
95 }];
96 NegotiationResponse {
97 strategies,
98 timing_adjustment: None,
99 }
100 }
101
102 fn apply_budget(&mut self, budget: ResourceBudget) {
103 self.time_budget = budget.time_limit;
104 self.current_strategy = budget.strategy_id;
105 }
106
107 fn on_initialize(&mut self, context: &mut EngineContext<'_>) {
108 self.frame_status = context
109 .runtime
110 .resources
111 .get::<AgentFrameStatusMap>()
112 .cloned();
113
114 let Some(device_arc) = context
115 .runtime
116 .backends
117 .get::<Arc<dyn GraphicsDevice>>()
118 .cloned()
119 else {
120 log::warn!("OverlayAgent: graphics device unavailable in on_initialize");
121 return;
122 };
123 let pipeline_system = context
124 .runtime
125 .resources
126 .get::<Arc<dyn khora_core::renderer::traits::PipelineSystem>>()
127 .cloned();
128
129 let mut init_ctx = LaneContext::new();
130 init_ctx.insert(device_arc);
131 if let Some(ps) = pipeline_system {
132 init_ctx.insert(ps);
133 }
134 for lane in self.lanes.all() {
135 if let Err(e) = lane.on_initialize(&mut init_ctx) {
136 log::error!(
137 "OverlayAgent: failed to initialize lane {}: {}",
138 lane.strategy_name(),
139 e
140 );
141 }
142 }
143 }
144
145 fn execute(&mut self, context: &mut EngineContext<'_>) {
146 if self.lanes.is_empty() {
147 return;
150 }
151
152 let Some(device_arc) = context.runtime.backends.get::<Arc<dyn GraphicsDevice>>() else {
153 return;
154 };
155 let device: Arc<dyn GraphicsDevice> = (*device_arc).clone();
156
157 let Some(asset_store) = context.runtime.resources.get::<AssetStore>() else {
158 return;
159 };
160 let gpu_meshes: Arc<RwLock<Assets<GpuMesh>>> = asset_store.store::<GpuMesh>();
161
162 let render_world: Option<&RenderWorld> = context.bus.get();
163
164 let Some(fctx) = context.runtime.resources.get::<Arc<FrameContext>>() else {
168 log::warn!("OverlayAgent: no FrameContext in services");
169 return;
170 };
171 let Some(color_target) = fctx.get::<ColorTarget>().map(|a| *a) else {
172 return;
173 };
174 let depth_target = fctx.get::<DepthTarget>().map(|a| *a);
175 let clear_color = fctx
176 .get::<ClearColor>()
177 .map(|a| *a)
178 .unwrap_or_else(|| ClearColor(khora_core::math::LinearRgba::new(0.0, 0.0, 0.0, 0.0)));
179
180 let mut encoder = device.create_command_encoder(Some("Khora Overlay Encoder"));
183 {
184 let mut ctx = LaneContext::new();
185 ctx.insert(device.clone());
186 ctx.insert(gpu_meshes.clone());
187 let encoder_slot = Slot::new(encoder.as_mut());
190 ctx.insert(unsafe {
191 std::mem::transmute::<
192 Slot<dyn khora_core::renderer::traits::CommandEncoder>,
193 Slot<dyn khora_core::renderer::traits::CommandEncoder>,
194 >(encoder_slot)
195 });
196 if let Some(rw) = render_world {
199 ctx.insert(khora_core::lane::Ref::new(rw));
200 }
201 ctx.insert(Slot::new(&mut *context.deck));
202 ctx.insert(color_target);
203 if let Some(dt) = depth_target {
204 ctx.insert(dt);
205 }
206 ctx.insert(clear_color);
207
208 if let Some(gizmos) = context
214 .runtime
215 .resources
216 .get::<khora_lanes::render_lane::SharedGizmoFrame>()
217 .cloned()
218 {
219 ctx.insert(gizmos);
220 }
221 if let Some(grid_cfg) = context
224 .runtime
225 .resources
226 .get::<khora_lanes::render_lane::SharedGridConfig>()
227 .cloned()
228 {
229 ctx.insert(grid_cfg);
230 }
231 if let Some(wireframe_cfg) = context
233 .runtime
234 .resources
235 .get::<khora_lanes::render_lane::SharedWireframeConfig>()
236 .cloned()
237 {
238 ctx.insert(wireframe_cfg);
239 }
240
241 for lane in self.lanes.all() {
242 if let Err(e) = lane.execute(&mut ctx) {
243 log::error!("OverlayAgent: lane {} failed: {}", lane.strategy_name(), e);
244 }
245 }
246 }
247
248 if let Some(cmd_buf) = encoder.finish() {
249 let descriptor = PassDescriptor::new("OverlayPass")
250 .writes(ResourceId::Color)
251 .reads(ResourceId::Depth);
252 context.deck.slot::<OverlayPassSlot>().0 = Some(PassContribution {
256 descriptor,
257 command_buffer: cmd_buf,
258 });
259 }
260 }
261
262 fn report_status(&self) -> AgentStatus {
263 let measured_time_ms = measured_frame_time_ms(&self.frame_status, self.id());
264 let health_score = if self.time_budget.is_zero() || measured_time_ms <= 0.0 {
265 1.0
266 } else {
267 (self.time_budget.as_secs_f32() * 1000.0 / measured_time_ms).min(1.0)
268 };
269 AgentStatus {
270 agent_id: self.id(),
271 health_score,
272 current_strategy: self.current_strategy,
273 is_stalled: false,
274 message: format!(
275 "overlay_lanes={} frame_time={measured_time_ms:.2}ms",
276 self.lanes.len(),
277 ),
278 }
279 }
280
281 fn execution_timing(&self) -> ExecutionTiming {
282 ExecutionTiming {
283 allowed_phases: vec![ExecutionPhase::OUTPUT],
284 default_phase: ExecutionPhase::OUTPUT,
285 priority: 0.5,
288 importance: AgentImportance::Optional,
289 dependencies: vec![AgentDependency {
292 target: AgentId::Renderer,
293 kind: DependencyKind::Hard,
294 condition: None,
295 }],
296 fixed_timestep: None,
297 }
298 }
299
300 fn as_any(&self) -> &dyn std::any::Any {
301 self
302 }
303
304 fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
305 self
306 }
307}
308
309impl Default for OverlayAgent {
310 fn default() -> Self {
311 let mut lanes = LaneRegistry::new();
312 lanes.register(Box::new(khora_lanes::render_lane::GridLane::default()));
316 lanes.register(Box::new(khora_lanes::render_lane::WireframeLane::default()));
317 lanes.register(Box::new(khora_lanes::render_lane::GizmoLane::default()));
318
319 Self {
320 lanes,
321 time_budget: Duration::ZERO,
322 current_strategy: StrategyId::Balanced,
323 frame_status: None,
324 }
325 }
326}
327
328#[cfg(test)]
329mod tests {
330 use super::*;
331
332 #[test]
333 fn overlay_agent_registers_overlay_lanes() {
334 let agent = OverlayAgent::default();
335 assert_eq!(agent.lanes.len(), 3);
336 assert!(agent.lanes.get("Grid").is_some());
337 assert!(agent.lanes.get("Wireframe").is_some());
338 assert!(agent.lanes.get("Gizmo").is_some());
339 }
340
341 #[test]
342 fn overlay_agent_reports_overlay_id() {
343 let agent = OverlayAgent::default();
344 assert_eq!(agent.id(), AgentId::Overlay);
345 }
346
347 #[test]
348 fn overlay_agent_runs_in_output_phase_after_renderer() {
349 let agent = OverlayAgent::default();
350 let timing = agent.execution_timing();
351 assert_eq!(timing.default_phase, ExecutionPhase::OUTPUT);
352 assert_eq!(timing.dependencies.len(), 1);
353 assert_eq!(timing.dependencies[0].target, AgentId::Renderer);
354 }
355}