khora_agents/ui_agent/
mod.rs1use std::any::Any;
30use std::sync::{Arc, RwLock};
31use std::time::Duration;
32
33use khora_core::agent::{Agent, AgentAccess, AgentImportance, ExecutionPhase, ExecutionTiming};
34use khora_core::context::EngineContext;
35use khora_core::control::gorna::{
36 AgentId, AgentStatus, NegotiationRequest, NegotiationResponse, ResourceBudget, StrategyId,
37 StrategyOption,
38};
39use khora_core::lane::Ref;
40use khora_core::lane::{ColorTarget, Lane, LaneContext, Slot};
41use khora_core::renderer::api::core::FrameContext;
42use khora_core::renderer::api::text::TextRenderer;
43use khora_core::renderer::GraphicsDevice;
44use khora_data::assets::Assets;
45use khora_data::render::{PassContribution, PassDescriptor, ResourceId, UiPassSlot};
46use khora_data::ui::{UiAtlasMap, UiImageAtlas, UiScene};
47use khora_lanes::render_lane::UiRenderLane;
48
49pub struct UiAgent {
58 render_lane: Option<Box<dyn Lane>>,
60 time_budget: Duration,
62 current_strategy: StrategyId,
64}
65
66impl Agent for UiAgent {
67 fn id(&self) -> AgentId {
68 AgentId::Ui
69 }
70
71 fn access(&self) -> AgentAccess {
77 AgentAccess::SharedWorld
78 }
79
80 fn deck_writes(&self) -> Vec<std::any::TypeId> {
82 vec![std::any::TypeId::of::<UiPassSlot>()]
83 }
84
85 fn negotiate(&mut self, _request: NegotiationRequest) -> NegotiationResponse {
86 let strategies = vec![StrategyOption {
87 id: StrategyId::Balanced,
88 estimated_time: Duration::from_micros(500),
89 estimated_vram: 1024 * 1024,
90 }];
91
92 NegotiationResponse {
93 strategies,
94 timing_adjustment: None,
95 }
96 }
97
98 fn apply_budget(&mut self, budget: ResourceBudget) {
99 self.current_strategy = budget.strategy_id;
100 self.time_budget = budget.time_limit;
101 }
102
103 fn on_initialize(&mut self, context: &mut EngineContext<'_>) {
104 if self.render_lane.is_none() {
106 self.render_lane = Some(Box::new(UiRenderLane::new()));
107 }
108 if let (Some(lane), Some(device)) = (
109 self.render_lane.as_ref(),
110 context
111 .runtime
112 .backends
113 .get::<Arc<dyn GraphicsDevice>>()
114 .cloned(),
115 ) {
116 let mut init_ctx = LaneContext::new();
117 init_ctx.insert(device);
118 if let Some(ps) = context
121 .runtime
122 .resources
123 .get::<Arc<dyn khora_core::renderer::traits::PipelineSystem>>()
124 .cloned()
125 {
126 init_ctx.insert(ps);
127 }
128 if let Err(e) = lane.on_initialize(&mut init_ctx) {
129 log::error!("UiAgent: Failed to initialize UiRenderLane: {}", e);
130 }
131 }
132
133 if let (Some(atlas_res), Some(device)) = (
136 context
137 .runtime
138 .resources
139 .get::<Arc<UiImageAtlas>>()
140 .cloned(),
141 context
142 .runtime
143 .backends
144 .get::<Arc<dyn GraphicsDevice>>()
145 .cloned(),
146 ) {
147 atlas_res.ensure_atlas(device.as_ref());
148 }
149 }
150
151 fn execute(&mut self, context: &mut EngineContext<'_>) {
152 let Some(device_arc) = context.runtime.backends.get::<Arc<dyn GraphicsDevice>>() else {
154 return;
155 };
156 let device: Arc<dyn GraphicsDevice> = (*device_arc).clone();
157
158 let Some(ui_scene): Option<&UiScene> = context.bus.get() else {
160 log::warn!("UiAgent: no UiScene in LaneBus (UiFlow not run?)");
161 return;
162 };
163
164 let Some(fctx) = context.runtime.resources.get::<Arc<FrameContext>>() else {
165 log::warn!("UiAgent: no FrameContext in services");
166 return;
167 };
168 let Some(color_target) = fctx.get::<ColorTarget>().map(|a| *a) else {
169 log::warn!("UiAgent: ColorTarget missing in FrameContext");
170 return;
171 };
172
173 let textures: Option<Arc<RwLock<Assets<khora_core::renderer::api::resource::CpuTexture>>>> =
174 context
175 .runtime
176 .resources
177 .get::<Arc<RwLock<Assets<khora_core::renderer::api::resource::CpuTexture>>>>()
178 .map(|arc| (*arc).clone());
179
180 let text_renderer: Option<Arc<dyn TextRenderer>> = context
181 .runtime
182 .backends
183 .get::<Arc<dyn TextRenderer>>()
184 .map(|arc| (*arc).clone());
185
186 let atlas_resource: Option<Arc<UiImageAtlas>> = context
193 .runtime
194 .resources
195 .get::<Arc<UiImageAtlas>>()
196 .cloned();
197 let mut atlas_guard = atlas_resource.as_ref().and_then(|r| r.lock_atlas());
198
199 let mut atlas_map = UiAtlasMap::new();
204 if let (Some(guard), Some(textures), Some(res)) = (
205 atlas_guard.as_mut(),
206 textures.as_ref(),
207 atlas_resource.as_ref(),
208 ) {
209 if let Some(atlas) = guard.as_mut() {
210 for node in ui_scene.nodes.iter() {
211 let Some(image) = node.image else { continue };
212 if let Some(rect) = res.get_rect(&image.texture) {
213 atlas_map.insert(image.texture, rect);
214 continue;
215 }
216 if let Ok(assets) = textures.read() {
217 if let Some(cpu_tex) = assets.get(&image.texture) {
218 if let Some(rect) = atlas.allocate_and_upload(
219 device.as_ref(),
220 cpu_tex.size.width,
221 cpu_tex.size.height,
222 &cpu_tex.pixels,
223 cpu_tex.format.bytes_per_pixel(),
224 ) {
225 res.insert_rect(image.texture, rect);
226 atlas_map.insert(image.texture, rect);
227 }
228 }
229 }
230 }
231 }
232 }
233
234 let Some(lane) = self.render_lane.as_ref() else {
237 return;
238 };
239
240 let mut encoder = device.create_command_encoder(Some("Khora UI Encoder"));
241 {
242 let mut ctx = LaneContext::new();
243 ctx.insert(device.clone());
244 if let Some(tr) = &text_renderer {
245 ctx.insert(tr.clone());
246 }
247 if let Some(guard) = atlas_guard.as_mut() {
251 if let Some(atlas) = guard.as_mut() {
252 ctx.insert(Slot::new(atlas));
253 }
254 }
255 ctx.insert(Ref::new(ui_scene));
258 ctx.insert(Ref::new(&atlas_map));
259
260 let encoder_slot = Slot::new(encoder.as_mut());
263 ctx.insert(unsafe {
264 std::mem::transmute::<
265 Slot<dyn khora_core::renderer::traits::CommandEncoder>,
266 Slot<dyn khora_core::renderer::traits::CommandEncoder>,
267 >(encoder_slot)
268 });
269 ctx.insert(color_target);
270
271 if let Err(e) = lane.execute(&mut ctx) {
272 log::error!("UiAgent: UiRenderLane execution failed: {}", e);
273 }
274 }
275 drop(atlas_guard);
278 let Some(cmd_buf) = encoder.finish() else {
279 log::error!("UiAgent: encoder.finish() returned None — skipping UiPass submission");
280 return;
281 };
282
283 context.deck.slot::<UiPassSlot>().0 = Some(PassContribution {
286 descriptor: PassDescriptor::new("UiPass")
287 .reads(ResourceId::Color)
288 .writes(ResourceId::Color),
289 command_buffer: cmd_buf,
290 });
291 }
292
293 fn report_status(&self) -> AgentStatus {
294 AgentStatus {
295 agent_id: self.id(),
296 health_score: 1.0,
297 current_strategy: self.current_strategy,
298 is_stalled: false,
299 message: format!("strategy={:?}", self.current_strategy),
300 }
301 }
302
303 fn as_any(&self) -> &dyn Any {
304 self
305 }
306
307 fn as_any_mut(&mut self) -> &mut dyn Any {
308 self
309 }
310
311 fn execution_timing(&self) -> ExecutionTiming {
312 ExecutionTiming {
313 allowed_phases: vec![ExecutionPhase::OUTPUT],
314 default_phase: ExecutionPhase::OUTPUT,
315 priority: 0.8,
316 importance: AgentImportance::Important,
317 fixed_timestep: None,
318 dependencies: Vec::new(),
319 }
320 }
321}
322
323impl Default for UiAgent {
324 fn default() -> Self {
325 Self {
326 render_lane: None,
327 time_budget: Duration::ZERO,
328 current_strategy: StrategyId::Balanced,
329 }
330 }
331}