1use crate::context::GpuContext;
9use crate::core::{Position, Rect};
10use crate::event::{HitMap, convert_window_event};
11use crate::ontology::{OntologyRegistry, SemanticRole, UiNode, UiTree};
12use crate::painter::AgpuPainter;
13use crate::renderer::ShapeRenderer;
14use crate::runtime::{Command, Frame, Model, ProgramOptions, Subscription};
15use crate::text::TextEngine;
16use crate::types::BackendPreference;
17use std::sync::Arc;
18use std::time::{Duration, Instant};
19use winit::application::ApplicationHandler;
20use winit::event::WindowEvent;
21use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
22use winit::window::{Window, WindowId};
23
24pub struct AgpuApp<M: Model> {
28 model: M,
29 options: ProgramOptions,
30}
31
32impl<M: Model + 'static> AgpuApp<M> {
33 pub fn new(model: M) -> Self {
35 Self {
36 model,
37 options: ProgramOptions::default(),
38 }
39 }
40
41 pub fn with_options(mut self, options: ProgramOptions) -> Self {
43 self.options = options;
44 self
45 }
46
47 pub fn with_backend(mut self, preference: BackendPreference) -> Self {
49 self.options.backend = preference;
50 self
51 }
52
53 pub fn run(self) -> Result<(), Box<dyn std::error::Error>> {
55 let event_loop = EventLoop::new()?;
56 event_loop.set_control_flow(ControlFlow::Poll);
57
58 let mut handler = AppHandler::Uninitialised {
59 model: self.model,
60 options: self.options,
61 };
62
63 event_loop.run_app(&mut handler)?;
64 Ok(())
65 }
66}
67
68enum AppHandler<M: Model> {
73 Uninitialised {
74 model: M,
75 options: ProgramOptions,
76 },
77 Running(Box<RunningApp<M>>),
78 Exited,
80}
81
82struct RunningApp<M: Model> {
83 model: M,
84 window: Arc<Window>,
85 gpu: GpuContext,
86 surface: wgpu::Surface<'static>,
87 surface_config: wgpu::SurfaceConfiguration,
88 surface_format: wgpu::TextureFormat,
89 shapes: ShapeRenderer,
90 text: TextEngine,
91 hit_map: HitMap,
92 ontology: OntologyRegistry,
93 running: bool,
94 cursor_position: Position,
95 tick_rate: Option<Duration>,
96 last_tick: Instant,
97 msaa_texture: Option<wgpu::TextureView>,
98 active_timers: Vec<(String, Duration, Instant)>,
99 pending_delays: Vec<(String, Instant)>,
100}
101
102impl<M: Model + 'static> RunningApp<M> {
103 fn new(
104 model: M,
105 options: &ProgramOptions,
106 event_loop: &ActiveEventLoop,
107 ) -> Result<Self, Box<dyn std::error::Error>> {
108 let attrs = Window::default_attributes()
109 .with_title(model.title())
110 .with_inner_size(winit::dpi::LogicalSize::new(
111 options.width as f64,
112 options.height as f64,
113 ))
114 .with_resizable(options.resizable);
115
116 let window = Arc::new(event_loop.create_window(attrs)?);
117
118 let preference = options.backend;
121 let backends = preference.to_backends();
122 let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
123 backends,
124 ..Default::default()
125 });
126
127 let surface = instance.create_surface(window.clone())?;
128
129 let gpu = pollster::block_on(GpuContext::new(&surface, preference))?;
130
131 let size = window.inner_size();
132 let surface_caps = surface.get_capabilities(gpu.adapter());
133 let format = surface_caps
134 .formats
135 .iter()
136 .find(|f| !f.is_srgb())
137 .copied()
138 .unwrap_or(surface_caps.formats[0]);
139
140 let present_mode = if options.vsync {
141 wgpu::PresentMode::AutoVsync
142 } else {
143 wgpu::PresentMode::AutoNoVsync
144 };
145
146 let surface_config = wgpu::SurfaceConfiguration {
147 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
148 format,
149 width: size.width.max(1),
150 height: size.height.max(1),
151 present_mode,
152 alpha_mode: surface_caps.alpha_modes[0],
153 view_formats: vec![],
154 desired_maximum_frame_latency: 3,
155 };
156 surface.configure(gpu.device(), &surface_config);
157
158 let shapes = ShapeRenderer::new(
159 &gpu,
160 format,
161 options.width,
162 options.height,
163 options.msaa_samples,
164 );
165 let text = TextEngine::new(&gpu, format);
166
167 let msaa_texture = if options.msaa_samples > 1 {
168 Some(create_msaa_texture(
169 gpu.device(),
170 format,
171 size.width.max(1),
172 size.height.max(1),
173 options.msaa_samples,
174 ))
175 } else {
176 None
177 };
178
179 let mut ontology = OntologyRegistry::new();
180 model.register_ontology(&mut ontology);
181
182 let init_cmd = model.init();
183
184 let mut app = Self {
185 model,
186 window,
187 gpu,
188 surface,
189 surface_config,
190 surface_format: format,
191 shapes,
192 text,
193 hit_map: HitMap::new(),
194 ontology,
195 running: true,
196 cursor_position: Position::ZERO,
197 tick_rate: options.tick_rate,
198 last_tick: Instant::now(),
199 msaa_texture,
200 active_timers: Vec::new(),
201 pending_delays: Vec::new(),
202 };
203
204 app.process_command(init_cmd);
205 Ok(app)
206 }
207
208 fn resize(&mut self, new_size: winit::dpi::PhysicalSize<u32>) {
209 if new_size.width > 0 && new_size.height > 0 {
210 self.surface_config.width = new_size.width;
211 self.surface_config.height = new_size.height;
212 self.surface
213 .configure(self.gpu.device(), &self.surface_config);
214 self.shapes
215 .set_viewport(new_size.width as f32, new_size.height as f32);
216 if self.shapes.sample_count() > 1 {
217 self.msaa_texture = Some(create_msaa_texture(
218 self.gpu.device(),
219 self.surface_format,
220 new_size.width,
221 new_size.height,
222 self.shapes.sample_count(),
223 ));
224 }
225 }
226 }
227
228 fn render(&mut self) {
229 let frame = match self.surface.get_current_texture() {
230 Ok(f) => f,
231 Err(wgpu::SurfaceError::Lost | wgpu::SurfaceError::Outdated) => {
232 self.surface
233 .configure(self.gpu.device(), &self.surface_config);
234 return;
235 }
236 Err(wgpu::SurfaceError::OutOfMemory) => {
237 log::error!("agpu: out of GPU memory");
238 self.running = false;
239 return;
240 }
241 Err(e) => {
242 log::warn!("agpu: surface error: {e:?}");
243 return;
244 }
245 };
246
247 let view = frame
248 .texture
249 .create_view(&wgpu::TextureViewDescriptor::default());
250
251 self.shapes.begin_frame();
253 self.hit_map.clear();
254
255 let area = Rect::new(
256 0.0,
257 0.0,
258 self.surface_config.width as f32,
259 self.surface_config.height as f32,
260 );
261
262 {
264 let mut painter = AgpuPainter::new(&mut self.shapes, &mut self.text);
265 let mut frame = Frame::new(area, &mut self.hit_map, &mut painter);
266 self.model.view(&mut frame);
267
268 let nodes = frame.take_nodes();
269 if !nodes.is_empty() {
270 let mut root = UiNode::new("root", SemanticRole::Container);
271 root.children = nodes;
272 self.ontology.set_tree(UiTree::new(root));
273 }
274 }
275
276 let mut encoder =
278 self.gpu
279 .device()
280 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
281 label: Some("agpu_encoder"),
282 });
283
284 {
285 let (target_view, resolve_target) = if let Some(msaa_view) = &self.msaa_texture {
286 (msaa_view, Some(&view))
287 } else {
288 (&view, None)
289 };
290 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
291 label: Some("agpu_pass"),
292 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
293 view: target_view,
294 resolve_target,
295 ops: wgpu::Operations {
296 load: wgpu::LoadOp::Clear(wgpu::Color {
297 r: 0.08,
298 g: 0.08,
299 b: 0.10,
300 a: 1.0,
301 }),
302 store: wgpu::StoreOp::Store,
303 },
304 })],
305 depth_stencil_attachment: None,
306 timestamp_writes: None,
307 occlusion_query_set: None,
308 });
309
310 self.shapes.flush(&self.gpu, &mut pass);
311 self.text.flush(
312 &self.gpu,
313 &mut pass,
314 self.surface_config.width,
315 self.surface_config.height,
316 );
317 }
318
319 self.gpu.queue().submit(std::iter::once(encoder.finish()));
320 frame.present();
321 self.text.trim();
322 }
323
324 fn process_command(&mut self, cmd: Command<M::Msg>) {
325 match cmd {
326 Command::None => {}
327 Command::Quit => {
328 self.running = false;
329 }
330 Command::Batch(cmds) => {
331 for c in cmds {
332 self.process_command(c);
333 }
334 }
335 Command::Message(msg) => {
336 let cmd = self.model.update(msg);
337 self.process_command(cmd);
338 }
339 Command::SetTickRate(d) => {
340 self.tick_rate = Some(d);
341 }
342 Command::ExportOntology => {
343 self.model.register_ontology(&mut self.ontology);
344 }
345 Command::AgentAction {
346 agent_id,
347 action,
348 params,
349 } => {
350 if let Some(node) = self.ontology.find_node(&agent_id) {
352 if let Err(e) =
353 self.ontology
354 .validate_action_params(&node.widget_type, &action, ¶ms)
355 {
356 log::warn!("AgentAction validation failed for {agent_id}.{action}: {e}");
357 return;
358 }
359 }
360 let ev = crate::event::Event::AgentAction {
362 agent_id,
363 action,
364 params,
365 };
366 if let Some(msg) = self.model.handle_event(ev) {
367 let cmd = self.model.update(msg);
368 self.process_command(cmd);
369 }
370 }
371 Command::Task(task) => {
372 let msg = task();
373 let cmd = self.model.update(msg);
374 self.process_command(cmd);
375 }
376 Command::TaskWithTimeout {
377 task,
378 timeout,
379 on_timeout,
380 } => {
381 use std::sync::mpsc;
382 let (tx, rx) = mpsc::channel();
383 std::thread::spawn(move || {
384 let result = task();
385 let _ = tx.send(result);
386 });
387 let msg = match rx.recv_timeout(timeout) {
388 Ok(result) => result,
389 Err(_) => on_timeout,
390 };
391 let cmd = self.model.update(msg);
392 self.process_command(cmd);
393 }
394 Command::TaskCancellable { task, token } => {
395 let msg = task(token);
396 let cmd = self.model.update(msg);
397 self.process_command(cmd);
398 }
399 }
400 }
401
402 fn refresh_subscriptions(&mut self) {
403 let subs = self.model.subscriptions();
404 let new_ids: Vec<String> = subs
406 .iter()
407 .filter_map(|s| match s {
408 Subscription::Timer { id, .. } => Some(id.clone()),
409 _ => None,
410 })
411 .collect();
412 self.active_timers.retain(|(id, _, _)| new_ids.contains(id));
414 for sub in &subs {
416 if let Subscription::Timer { id, interval, .. } = sub {
417 if !self.active_timers.iter().any(|(aid, _, _)| aid == id) {
418 self.active_timers
419 .push((id.clone(), *interval, Instant::now()));
420 }
421 }
422 }
423 for sub in subs {
425 if let Subscription::Delay { id, duration, .. } = &sub {
426 if !self.pending_delays.iter().any(|(did, _)| did == id) {
427 self.pending_delays
428 .push((id.clone(), Instant::now() + *duration));
429 }
430 }
431 }
432 }
433}
434
435impl<M: Model + 'static> ApplicationHandler for AppHandler<M> {
438 fn resumed(&mut self, event_loop: &ActiveEventLoop) {
439 if let AppHandler::Uninitialised { .. } = self {
441 let taken = std::mem::replace(self, AppHandler::Exited);
442 if let AppHandler::Uninitialised { model, options } = taken {
443 match RunningApp::new(model, &options, event_loop) {
444 Ok(app) => {
445 *self = AppHandler::Running(Box::new(app));
446 }
447 Err(e) => {
448 log::error!("agpu: failed to initialise: {e}");
449 event_loop.exit();
450 }
451 }
452 }
453 }
454 }
455
456 fn window_event(
457 &mut self,
458 event_loop: &ActiveEventLoop,
459 _window_id: WindowId,
460 event: WindowEvent,
461 ) {
462 let app = match self {
463 AppHandler::Running(app) => app.as_mut(),
464 _ => return,
465 };
466
467 if let WindowEvent::Resized(size) = event {
469 app.resize(size);
470 }
471
472 if let WindowEvent::CursorMoved { position, .. } = &event {
474 app.cursor_position = Position::new(position.x as f32, position.y as f32);
475 }
476
477 let events = convert_window_event(&event);
479 for mut ev in events {
480 if let crate::event::Event::Mouse(ref mut mouse) = ev {
482 if mouse.position == Position::ZERO {
483 mouse.position = app.cursor_position;
484 }
485 }
486 if let Some(msg) = app.model.handle_event(ev) {
487 let cmd = app.model.update(msg);
488 app.process_command(cmd);
489 }
490 }
491
492 if let WindowEvent::RedrawRequested = event {
494 app.render();
495 }
496
497 if !app.running {
498 event_loop.exit();
499 }
500 }
501
502 fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {
503 if let AppHandler::Running(app) = self {
504 let now = Instant::now();
505
506 let mut fired_timer_ids = Vec::new();
508 for (id, interval, last_fire) in &mut app.active_timers {
509 if now.duration_since(*last_fire) >= *interval {
510 *last_fire = now;
511 fired_timer_ids.push(id.clone());
512 }
513 }
514 if !fired_timer_ids.is_empty() {
515 let subs = app.model.subscriptions();
517 for sub in &subs {
518 if let Subscription::Timer { id, msg, .. } = sub {
519 if fired_timer_ids.contains(id) {
520 let m = msg();
521 let cmd = app.model.update(m);
522 app.process_command(cmd);
523 }
524 }
525 }
526 }
527
528 let mut fired_delays = Vec::new();
530 app.pending_delays.retain(|(id, deadline)| {
531 if now >= *deadline {
532 fired_delays.push(id.clone());
533 false
534 } else {
535 true
536 }
537 });
538 if !fired_delays.is_empty() {
539 let subs = app.model.subscriptions();
540 for sub in subs {
541 if let Subscription::Delay { id, msg, .. } = sub {
542 if fired_delays.contains(&id) {
543 let cmd = app.model.update(msg);
544 app.process_command(cmd);
545 }
546 }
547 }
548 }
549
550 match app.tick_rate {
552 Some(rate) => {
553 if now.duration_since(app.last_tick) >= rate {
554 app.last_tick = now;
555 if let Some(msg) = app.model.handle_event(crate::event::Event::Tick) {
556 let cmd = app.model.update(msg);
557 app.process_command(cmd);
558 }
559 app.window.request_redraw();
560 }
561 }
562 None => {
563 app.window.request_redraw();
564 }
565 }
566
567 app.refresh_subscriptions();
569 }
570 }
571}
572
573fn create_msaa_texture(
575 device: &wgpu::Device,
576 format: wgpu::TextureFormat,
577 width: u32,
578 height: u32,
579 sample_count: u32,
580) -> wgpu::TextureView {
581 let texture = device.create_texture(&wgpu::TextureDescriptor {
582 label: Some("agpu_msaa_texture"),
583 size: wgpu::Extent3d {
584 width,
585 height,
586 depth_or_array_layers: 1,
587 },
588 mip_level_count: 1,
589 sample_count,
590 dimension: wgpu::TextureDimension::D2,
591 format,
592 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
593 view_formats: &[],
594 });
595 texture.create_view(&wgpu::TextureViewDescriptor::default())
596}