khora_agents/skybox_agent/
agent.rs1use std::sync::Arc;
19use std::time::Duration;
20
21use khora_core::agent::{
22 Agent, AgentAccess, AgentDependency, AgentImportance, DependencyKind, ExecutionPhase,
23 ExecutionTiming,
24};
25use khora_core::control::gorna::{
26 measured_frame_time_ms, AgentFrameStatusMap, AgentId, AgentStatus, NegotiationRequest,
27 NegotiationResponse, ResourceBudget, StrategyId, StrategyOption,
28};
29use khora_core::lane::{ColorTarget, DepthTarget, LaneContext, LaneRegistry, Slot};
30use khora_core::renderer::api::core::FrameContext;
31use khora_core::renderer::GraphicsDevice;
32use khora_core::EngineContext;
33use khora_data::render::{
34 PassContribution, PassDescriptor, RenderWorld, ResourceId, SkyboxPassSlot,
35};
36
37pub struct SkyboxAgent {
46 lanes: LaneRegistry,
49 time_budget: Duration,
51 current_strategy: StrategyId,
53 frame_status: Option<AgentFrameStatusMap>,
56}
57
58impl Agent for SkyboxAgent {
59 fn id(&self) -> AgentId {
60 AgentId::Skybox
61 }
62
63 fn access(&self) -> AgentAccess {
67 AgentAccess::Isolated
68 }
69
70 fn deck_writes(&self) -> Vec<std::any::TypeId> {
72 vec![std::any::TypeId::of::<SkyboxPassSlot>()]
73 }
74
75 fn negotiate(&mut self, _request: NegotiationRequest) -> NegotiationResponse {
76 let strategies = vec![StrategyOption {
79 id: StrategyId::Balanced,
80 estimated_time: Duration::from_micros(300),
81 estimated_vram: 0,
82 }];
83 NegotiationResponse {
84 strategies,
85 timing_adjustment: None,
86 }
87 }
88
89 fn apply_budget(&mut self, budget: ResourceBudget) {
90 self.time_budget = budget.time_limit;
91 self.current_strategy = budget.strategy_id;
92 }
93
94 fn on_initialize(&mut self, context: &mut EngineContext<'_>) {
95 self.frame_status = context
96 .runtime
97 .resources
98 .get::<AgentFrameStatusMap>()
99 .cloned();
100
101 let Some(device_arc) = context
102 .runtime
103 .backends
104 .get::<Arc<dyn GraphicsDevice>>()
105 .cloned()
106 else {
107 log::warn!("SkyboxAgent: graphics device unavailable in on_initialize");
108 return;
109 };
110 let pipeline_system = context
111 .runtime
112 .resources
113 .get::<Arc<dyn khora_core::renderer::traits::PipelineSystem>>()
114 .cloned();
115
116 let mut init_ctx = LaneContext::new();
117 init_ctx.insert(device_arc);
118 if let Some(ps) = pipeline_system {
119 init_ctx.insert(ps);
120 }
121 for lane in self.lanes.all() {
122 if let Err(e) = lane.on_initialize(&mut init_ctx) {
123 log::error!(
124 "SkyboxAgent: failed to initialize lane {}: {}",
125 lane.strategy_name(),
126 e
127 );
128 }
129 }
130 }
131
132 fn execute(&mut self, context: &mut EngineContext<'_>) {
133 let Some(device_arc) = context.runtime.backends.get::<Arc<dyn GraphicsDevice>>() else {
134 return;
135 };
136 let device: Arc<dyn GraphicsDevice> = (*device_arc).clone();
137
138 let ibl = context
141 .runtime
142 .resources
143 .get::<khora_data::IblBaker>()
144 .and_then(|baker| baker.bindings());
145 let Some(ibl) = ibl else {
146 return;
147 };
148
149 let render_world: Option<&RenderWorld> = context.bus.get();
150
151 let Some(fctx) = context.runtime.resources.get::<Arc<FrameContext>>() else {
154 log::warn!("SkyboxAgent: no FrameContext in services");
155 return;
156 };
157 let Some(color_target) = fctx.get::<ColorTarget>().map(|a| *a) else {
158 return;
159 };
160 let Some(depth_target) = fctx.get::<DepthTarget>().map(|a| *a) else {
161 return;
162 };
163
164 let mut encoder = device.create_command_encoder(Some("Khora Skybox Encoder"));
165 {
166 let mut ctx = LaneContext::new();
167 ctx.insert(device.clone());
168 let encoder_slot = Slot::new(encoder.as_mut());
171 ctx.insert(unsafe {
172 std::mem::transmute::<
173 Slot<dyn khora_core::renderer::traits::CommandEncoder>,
174 Slot<dyn khora_core::renderer::traits::CommandEncoder>,
175 >(encoder_slot)
176 });
177 if let Some(rw) = render_world {
178 ctx.insert(khora_core::lane::Ref::new(rw));
179 }
180 ctx.insert(color_target);
181 ctx.insert(depth_target);
182 ctx.insert(ibl);
185
186 for lane in self.lanes.all() {
187 if let Err(e) = lane.execute(&mut ctx) {
188 log::error!("SkyboxAgent: lane {} failed: {}", lane.strategy_name(), e);
189 }
190 }
191 }
192
193 if let Some(cmd_buf) = encoder.finish() {
194 let descriptor = PassDescriptor::new("SkyboxPass")
195 .writes(ResourceId::Color)
196 .reads(ResourceId::Depth);
197 context.deck.slot::<SkyboxPassSlot>().0 = Some(PassContribution {
198 descriptor,
199 command_buffer: cmd_buf,
200 });
201 }
202 }
203
204 fn report_status(&self) -> AgentStatus {
205 let measured_time_ms = measured_frame_time_ms(&self.frame_status, self.id());
206 let health_score = if self.time_budget.is_zero() || measured_time_ms <= 0.0 {
207 1.0
208 } else {
209 (self.time_budget.as_secs_f32() * 1000.0 / measured_time_ms).min(1.0)
210 };
211 AgentStatus {
212 agent_id: self.id(),
213 health_score,
214 current_strategy: self.current_strategy,
215 is_stalled: false,
216 message: format!("skybox frame_time={measured_time_ms:.2}ms"),
217 }
218 }
219
220 fn execution_timing(&self) -> ExecutionTiming {
221 ExecutionTiming {
222 allowed_phases: vec![ExecutionPhase::OUTPUT],
223 default_phase: ExecutionPhase::OUTPUT,
224 priority: 0.6,
227 importance: AgentImportance::Important,
229 dependencies: vec![AgentDependency {
232 target: AgentId::Renderer,
233 kind: DependencyKind::Hard,
234 condition: None,
235 }],
236 fixed_timestep: None,
237 }
238 }
239
240 fn as_any(&self) -> &dyn std::any::Any {
241 self
242 }
243
244 fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
245 self
246 }
247}
248
249impl Default for SkyboxAgent {
250 fn default() -> Self {
251 let mut lanes = LaneRegistry::new();
252 lanes.register(Box::new(khora_lanes::render_lane::SkyboxLane::default()));
253 Self {
254 lanes,
255 time_budget: Duration::ZERO,
256 current_strategy: StrategyId::Balanced,
257 frame_status: None,
258 }
259 }
260}
261
262#[cfg(test)]
263mod tests {
264 use super::*;
265
266 #[test]
267 fn skybox_agent_registers_its_lane() {
268 let agent = SkyboxAgent::default();
269 assert_eq!(agent.lanes.len(), 1);
270 assert!(agent.lanes.get("Skybox").is_some());
271 }
272
273 #[test]
274 fn skybox_agent_reports_skybox_id() {
275 let agent = SkyboxAgent::default();
276 assert_eq!(agent.id(), AgentId::Skybox);
277 }
278
279 #[test]
280 fn skybox_agent_runs_in_output_phase_after_renderer() {
281 let agent = SkyboxAgent::default();
282 let timing = agent.execution_timing();
283 assert_eq!(timing.default_phase, ExecutionPhase::OUTPUT);
284 assert_eq!(timing.importance, AgentImportance::Important);
285 assert_eq!(timing.dependencies.len(), 1);
286 assert_eq!(timing.dependencies[0].target, AgentId::Renderer);
287 }
288}