1use std::{error::Error as StdError, fmt};
2
3use dear_imgui_rs as imgui;
4use dear_imgui_rs::{DockNodeFlags, TextureId};
5use thiserror::Error;
6use winit::{event::WindowEvent, window::Window};
7
8use crate::AppConfig;
9
10#[cfg(feature = "imnodes")]
11use dear_imnodes as imnodes;
12#[cfg(feature = "implot")]
13use dear_implot as implot;
14#[cfg(feature = "implot3d")]
15use dear_implot3d as implot3d;
16
17#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
19pub struct GpuGeneration(pub(crate) u64);
20
21impl GpuGeneration {
22 pub const INITIAL: Self = Self(0);
23
24 #[must_use]
25 pub const fn get(self) -> u64 {
26 self.0
27 }
28
29 pub(crate) const fn checked_next(self) -> Option<Self> {
30 match self.0.checked_add(1) {
31 Some(value) => Some(Self(value)),
32 None => None,
33 }
34 }
35
36 fn ensure_current(self, current: Self) -> Result<(), ExternalTextureError> {
37 if self == current {
38 Ok(())
39 } else {
40 Err(ExternalTextureError::StaleGeneration {
41 handle_generation: self.get(),
42 current_generation: current.get(),
43 })
44 }
45 }
46}
47
48#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
50pub struct ExternalTextureHandle {
51 id: dear_imgui_wgpu::ExternalTextureId,
52 generation: GpuGeneration,
53}
54
55impl ExternalTextureHandle {
56 #[must_use]
57 pub const fn generation(self) -> GpuGeneration {
58 self.generation
59 }
60
61 fn resolve_for_generation(
62 self,
63 current: GpuGeneration,
64 ) -> Result<dear_imgui_wgpu::ExternalTextureId, ExternalTextureError> {
65 self.generation.ensure_current(current)?;
66 Ok(self.id)
67 }
68}
69
70#[derive(Debug, Error)]
71#[non_exhaustive]
72pub enum ExternalTextureError {
73 #[error(
74 "external texture belongs to GPU generation {handle_generation}, current generation is {current_generation}"
75 )]
76 StaleGeneration {
77 handle_generation: u64,
78 current_generation: u64,
79 },
80 #[error(transparent)]
81 Renderer(#[from] dear_imgui_wgpu::RendererError),
82}
83
84#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
89#[non_exhaustive]
90pub enum ApplicationStage {
91 ConfigureImgui,
92 Initialized,
93 Event,
94 PrepareFrame,
95 Frame,
96 GpuLost,
97 GpuRecreated,
98 Shutdown,
99}
100
101impl ApplicationStage {
102 #[must_use]
103 pub const fn as_str(self) -> &'static str {
104 match self {
105 Self::ConfigureImgui => "configure_imgui",
106 Self::Initialized => "initialized",
107 Self::Event => "event",
108 Self::PrepareFrame => "prepare_frame",
109 Self::Frame => "frame",
110 Self::GpuLost => "gpu_lost",
111 Self::GpuRecreated => "gpu_recreated",
112 Self::Shutdown => "shutdown",
113 }
114 }
115}
116
117impl fmt::Display for ApplicationStage {
118 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
119 formatter.write_str(self.as_str())
120 }
121}
122
123#[cfg(test)]
124#[derive(Debug, Error)]
125#[error("{0}")]
126struct ApplicationMessage(String);
127
128#[derive(Debug, Error)]
129#[non_exhaustive]
130pub enum RunError {
131 #[error("event loop error: {0}")]
132 EventLoop(#[from] winit::error::EventLoopError),
133 #[error("window creation failed: {0}")]
134 WindowCreation(#[source] winit::error::OsError),
135 #[error("WGPU surface creation failed: {0}")]
136 SurfaceCreation(#[source] wgpu::CreateSurfaceError),
137 #[error("no suitable WGPU adapter found: {0}")]
138 AdapterUnavailable(#[source] wgpu::RequestAdapterError),
139 #[error("the selected surface exposes no texture formats")]
140 SurfaceFormatUnavailable,
141 #[error(
142 "the recreated surface changed format from {previous:?} to {replacement:?}; the active renderer cannot be reused"
143 )]
144 SurfaceFormatChanged {
145 previous: wgpu::TextureFormat,
146 replacement: wgpu::TextureFormat,
147 },
148 #[error("WGPU device request failed: {0}")]
149 DeviceRequest(#[source] wgpu::RequestDeviceError),
150 #[error("Dear ImGui context creation failed: {0}")]
151 ImGuiContext(#[source] imgui::ImGuiError),
152 #[error("WGPU renderer initialization failed: {0}")]
153 RendererInit(#[source] dear_imgui_wgpu::RendererError),
154 #[error("WGPU renderer draw failed: {0}")]
155 Render(#[source] dear_imgui_wgpu::RendererError),
156 #[error("WGPU resource invalidation failed: {0}")]
157 GpuInvalidation(#[source] dear_imgui_wgpu::RendererError),
158 #[error("WGPU renderer release failed: {0}")]
159 RendererRelease(#[source] dear_imgui_wgpu::RendererError),
160 #[error("Dear ImGui dockspace submission failed: {0}")]
161 Dockspace(#[source] imgui::DockspaceError),
162 #[error("Winit platform operation `{operation}` failed: {source}")]
163 Platform {
164 operation: &'static str,
165 #[source]
166 source: dear_imgui_winit::WinitPlatformError,
167 },
168 #[error(
169 "dear-app does not support Dear ImGui platform viewports; remove ConfigFlags::VIEWPORTS_ENABLE"
170 )]
171 MultiViewportUnsupported,
172 #[error("WGPU surface validation failed while acquiring the next frame")]
173 SurfaceValidation,
174 #[error("WGPU exhausted GPU memory: {message}")]
175 GpuOutOfMemory { message: String },
176 #[error("WGPU reported an uncaptured validation error: {message}")]
177 GpuValidation { message: String },
178 #[error("WGPU reported an uncaptured internal error: {message}")]
179 GpuInternal { message: String },
180 #[cfg(feature = "test-engine")]
181 #[error("Test Engine frame {frame} failed: {source}")]
182 TestEngineFrame {
183 frame: u64,
184 #[source]
185 source: Box<dear_imgui_test_engine::FrameDriverError<RunError, RunError, RunError>>,
186 },
187 #[error("application callback failed during {stage}: {source}")]
188 Application {
189 stage: ApplicationStage,
190 #[source]
191 source: Box<dyn StdError + 'static>,
192 },
193 #[error("GPU generation recovery failed: {message}")]
194 Recovery { message: String },
195}
196
197impl RunError {
198 #[must_use]
200 pub fn application<E>(stage: ApplicationStage, source: E) -> Self
201 where
202 E: StdError + 'static,
203 {
204 Self::Application {
205 stage,
206 source: Box::new(source),
207 }
208 }
209
210 #[must_use]
212 pub const fn application_stage(&self) -> Option<ApplicationStage> {
213 match self {
214 Self::Application { stage, .. } => Some(*stage),
215 _ => None,
216 }
217 }
218
219 #[cfg(test)]
220 pub(crate) fn application_message(stage: ApplicationStage, message: impl Into<String>) -> Self {
221 Self::application(stage, ApplicationMessage(message.into()))
222 }
223
224 pub(crate) fn during_application_stage(self, stage: ApplicationStage) -> Self {
225 if matches!(
226 &self,
227 Self::Application {
228 stage: existing,
229 ..
230 } if *existing == stage
231 ) {
232 self
233 } else {
234 Self::application(stage, self)
235 }
236 }
237}
238
239pub trait Application {
245 fn configure_imgui(&mut self, _context: &mut InitContext<'_>) -> Result<(), RunError> {
247 Ok(())
248 }
249
250 fn initialized(
252 &mut self,
253 _init: &mut InitContext<'_>,
254 _gpu: &mut GpuContext<'_>,
255 ) -> Result<(), RunError> {
256 Ok(())
257 }
258
259 fn gpu_lost(&mut self, _context: &mut GpuContext<'_>) -> Result<(), RunError> {
261 Ok(())
262 }
263
264 fn gpu_recreated(&mut self, _context: &mut GpuContext<'_>) -> Result<(), RunError> {
266 Ok(())
267 }
268
269 fn event(&mut self, _context: &mut EventContext<'_>) -> Result<(), RunError> {
271 Ok(())
272 }
273
274 fn prepare_frame(&mut self, _context: &mut PrepareFrameContext<'_>) -> Result<(), RunError> {
276 Ok(())
277 }
278
279 fn frame(&mut self, context: &mut FrameContext<'_>) -> Result<(), RunError>;
281
282 #[cfg(feature = "test-engine")]
288 fn test_engine(&mut self) -> Option<&mut crate::test_engine::TestEngine> {
289 None
290 }
291
292 fn shutdown(&mut self, _context: &mut ShutdownContext<'_>) -> Result<(), RunError> {
294 Ok(())
295 }
296}
297
298pub struct InitContext<'a> {
299 pub(crate) imgui: &'a mut imgui::Context,
300 pub(crate) window: &'a Window,
301 pub(crate) config: &'a AppConfig,
302}
303
304impl InitContext<'_> {
305 pub fn imgui(&mut self) -> &mut imgui::Context {
306 self.imgui
307 }
308
309 #[must_use]
310 pub fn window(&self) -> &Window {
311 self.window
312 }
313
314 #[must_use]
315 pub fn config(&self) -> &AppConfig {
316 self.config
317 }
318}
319
320pub struct EventContext<'a> {
321 pub(crate) event: &'a WindowEvent,
322 pub(crate) imgui: &'a mut imgui::Context,
323 pub(crate) window: &'a Window,
324 pub(crate) exit_requested: &'a mut bool,
325}
326
327impl EventContext<'_> {
328 #[must_use]
329 pub fn event(&self) -> &WindowEvent {
330 self.event
331 }
332
333 pub fn imgui(&mut self) -> &mut imgui::Context {
334 self.imgui
335 }
336
337 #[must_use]
338 pub fn window(&self) -> &Window {
339 self.window
340 }
341
342 pub fn request_exit(&mut self) {
344 *self.exit_requested = true;
345 }
346}
347
348pub struct ShutdownContext<'a> {
349 pub(crate) imgui: &'a mut imgui::Context,
350 pub(crate) window: &'a Window,
351 pub(crate) generation: Option<GpuGeneration>,
352}
353
354pub struct PrepareFrameContext<'a> {
356 pub(crate) imgui: &'a mut imgui::Context,
357 pub(crate) window: &'a Window,
358}
359
360impl PrepareFrameContext<'_> {
361 pub fn imgui(&mut self) -> &mut imgui::Context {
363 self.imgui
364 }
365
366 #[must_use]
367 pub fn window(&self) -> &Window {
368 self.window
369 }
370}
371
372impl ShutdownContext<'_> {
373 pub fn imgui(&mut self) -> &mut imgui::Context {
374 self.imgui
375 }
376
377 #[must_use]
378 pub fn window(&self) -> &Window {
379 self.window
380 }
381
382 #[must_use]
383 pub const fn gpu_generation(&self) -> Option<GpuGeneration> {
384 self.generation
385 }
386}
387
388pub struct DockingController {
389 pub(crate) flags: DockNodeFlags,
390}
391
392pub struct DockingApi<'a> {
393 pub(crate) controller: &'a mut DockingController,
394}
395
396impl DockingApi<'_> {
397 #[must_use]
398 pub fn flags(&self) -> DockNodeFlags {
399 DockNodeFlags::from_bits_retain(self.controller.flags.bits())
400 }
401
402 pub fn set_flags(&mut self, flags: DockNodeFlags) {
403 self.controller.flags = flags;
404 }
405}
406
407pub struct AddOns<'a> {
408 #[cfg(feature = "implot")]
409 pub(crate) implot: Option<&'a implot::PlotContext>,
410 #[cfg(feature = "imnodes")]
411 pub(crate) imnodes: Option<&'a imnodes::Context>,
412 #[cfg(feature = "implot3d")]
413 pub(crate) implot3d: Option<&'a implot3d::Plot3DContext>,
414 pub(crate) docking: DockingApi<'a>,
415}
416
417impl<'a> AddOns<'a> {
418 #[cfg(feature = "implot")]
419 #[must_use]
420 pub fn implot(&self) -> Option<&'a implot::PlotContext> {
421 self.implot
422 }
423
424 #[cfg(feature = "imnodes")]
425 #[must_use]
426 pub fn imnodes(&self) -> Option<&'a imnodes::Context> {
427 self.imnodes
428 }
429
430 #[cfg(feature = "implot3d")]
431 #[must_use]
432 pub fn implot3d(&self) -> Option<&'a implot3d::Plot3DContext> {
433 self.implot3d
434 }
435
436 pub fn docking(&mut self) -> &mut DockingApi<'a> {
437 &mut self.docking
438 }
439}
440
441pub struct GpuApi<'a> {
442 pub(crate) device: &'a wgpu::Device,
443 pub(crate) queue: &'a wgpu::Queue,
444 pub(crate) renderer: &'a mut dear_imgui_wgpu::WgpuRenderer,
445 pub(crate) generation: GpuGeneration,
446}
447
448impl GpuApi<'_> {
449 #[must_use]
450 pub fn device(&self) -> &wgpu::Device {
451 self.device
452 }
453
454 #[must_use]
455 pub fn queue(&self) -> &wgpu::Queue {
456 self.queue
457 }
458
459 #[must_use]
460 pub const fn generation(&self) -> GpuGeneration {
461 self.generation
462 }
463
464 pub fn register_external_texture(
465 &mut self,
466 view: &wgpu::TextureView,
467 ) -> Result<ExternalTextureHandle, ExternalTextureError> {
468 Ok(ExternalTextureHandle {
469 id: self.renderer.register_external_texture(view)?,
470 generation: self.generation,
471 })
472 }
473
474 pub fn resolve_external_texture(
475 &self,
476 handle: ExternalTextureHandle,
477 ) -> Result<TextureId, ExternalTextureError> {
478 handle
479 .resolve_for_generation(self.generation)
480 .map(dear_imgui_wgpu::ExternalTextureId::texture_id)
481 }
482
483 pub fn update_external_texture(
484 &mut self,
485 handle: ExternalTextureHandle,
486 view: &wgpu::TextureView,
487 ) -> Result<(), ExternalTextureError> {
488 let texture = handle.resolve_for_generation(self.generation)?;
489 self.renderer.update_external_texture(texture, view)?;
490 Ok(())
491 }
492
493 pub fn unregister_external_texture(
494 &mut self,
495 handle: ExternalTextureHandle,
496 ) -> Result<(), ExternalTextureError> {
497 let texture = handle.resolve_for_generation(self.generation)?;
498 self.renderer.unregister_external_texture(texture)?;
499 Ok(())
500 }
501}
502
503pub struct GpuContext<'a> {
504 pub(crate) window: &'a Window,
505 pub(crate) surface_config: &'a wgpu::SurfaceConfiguration,
506 pub(crate) gpu: GpuApi<'a>,
507}
508
509impl<'a> GpuContext<'a> {
510 #[must_use]
511 pub fn window(&self) -> &Window {
512 self.window
513 }
514
515 #[must_use]
516 pub fn surface_config(&self) -> &wgpu::SurfaceConfiguration {
517 self.surface_config
518 }
519
520 pub fn gpu(&mut self) -> &mut GpuApi<'a> {
521 &mut self.gpu
522 }
523
524 #[must_use]
525 pub const fn generation(&self) -> GpuGeneration {
526 self.gpu.generation
527 }
528}
529
530pub struct FrameContext<'a> {
531 pub(crate) ui: &'a imgui::Ui,
532 pub(crate) addons: AddOns<'a>,
533 pub(crate) gpu: GpuApi<'a>,
534 pub(crate) exit_requested: &'a mut bool,
535}
536
537impl<'a> FrameContext<'a> {
538 #[must_use]
539 pub fn ui(&self) -> &'a imgui::Ui {
540 self.ui
541 }
542
543 pub fn addons(&mut self) -> &mut AddOns<'a> {
544 &mut self.addons
545 }
546
547 pub fn gpu(&mut self) -> &mut GpuApi<'a> {
548 &mut self.gpu
549 }
550
551 pub fn request_exit(&mut self) {
556 *self.exit_requested = true;
557 }
558}
559
560#[cfg(test)]
561mod tests {
562 use std::error::Error as _;
563
564 use thiserror::Error;
565
566 use super::{ApplicationStage, ExternalTextureError, GpuGeneration, RunError};
567
568 #[derive(Debug, Error)]
569 #[error("injected user failure")]
570 struct UserError;
571
572 #[test]
573 fn stale_generation_error_names_both_epochs() {
574 let error = ExternalTextureError::StaleGeneration {
575 handle_generation: 2,
576 current_generation: 3,
577 };
578
579 assert_eq!(
580 error.to_string(),
581 "external texture belongs to GPU generation 2, current generation is 3"
582 );
583 }
584
585 #[test]
586 fn stale_gpu_generation_is_rejected_after_recovery() {
587 assert!(matches!(
588 GpuGeneration(2).ensure_current(GpuGeneration(3)),
589 Err(ExternalTextureError::StaleGeneration {
590 handle_generation: 2,
591 current_generation: 3,
592 })
593 ));
594 assert!(GpuGeneration(3).ensure_current(GpuGeneration(3)).is_ok());
595 }
596
597 #[test]
598 fn application_error_retains_typed_stage_and_original_source() {
599 let error = RunError::application(ApplicationStage::Frame, UserError);
600
601 assert!(matches!(
602 &error,
603 RunError::Application {
604 stage: ApplicationStage::Frame,
605 ..
606 }
607 ));
608 assert!(
609 error
610 .source()
611 .and_then(|source| source.downcast_ref::<UserError>())
612 .is_some()
613 );
614 }
615
616 #[test]
617 fn assigning_the_same_stage_does_not_duplicate_the_source_chain() {
618 let error = RunError::application(ApplicationStage::Frame, UserError)
619 .during_application_stage(ApplicationStage::Frame);
620
621 assert!(
622 error
623 .source()
624 .and_then(|source| source.downcast_ref::<UserError>())
625 .is_some()
626 );
627 }
628
629 #[test]
630 fn runtime_assigned_stage_retains_the_hook_run_error_as_source() {
631 let error = RunError::GpuValidation {
632 message: "injected validation failure".to_owned(),
633 }
634 .during_application_stage(ApplicationStage::GpuRecreated);
635
636 assert!(matches!(
637 &error,
638 RunError::Application {
639 stage: ApplicationStage::GpuRecreated,
640 ..
641 }
642 ));
643 assert!(matches!(
644 error
645 .source()
646 .and_then(|source| source.downcast_ref::<RunError>()),
647 Some(RunError::GpuValidation { message })
648 if message == "injected validation failure"
649 ));
650 }
651}