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("Dear ImGui frame admission failed: {0}")]
153 ImGuiFrame(#[source] imgui::render::RendererConsumerError),
154 #[error(
155 "application callback during {stage} changed Dear ImGui frame ownership from {before:?} to {after:?}"
156 )]
157 ImGuiFrameOwnership {
158 stage: ApplicationStage,
159 before: imgui::FrameLifecycleStamp,
160 after: imgui::FrameLifecycleStamp,
161 },
162 #[error("WGPU renderer initialization failed: {0}")]
163 RendererInit(#[source] dear_imgui_wgpu::RendererError),
164 #[error("WGPU renderer draw failed: {0}")]
165 Render(#[source] dear_imgui_wgpu::RendererError),
166 #[error("WGPU resource invalidation failed: {0}")]
167 GpuInvalidation(#[source] dear_imgui_wgpu::RendererError),
168 #[error("WGPU renderer release failed: {0}")]
169 RendererRelease(#[source] dear_imgui_wgpu::RendererError),
170 #[error("Dear ImGui dockspace submission failed: {0}")]
171 Dockspace(#[source] imgui::DockspaceError),
172 #[error("Winit platform operation `{operation}` failed: {source}")]
173 Platform {
174 operation: &'static str,
175 #[source]
176 source: dear_imgui_winit::WinitPlatformError,
177 },
178 #[error(
179 "dear-app does not support Dear ImGui platform viewports; remove ConfigFlags::VIEWPORTS_ENABLE"
180 )]
181 MultiViewportUnsupported,
182 #[error("WGPU surface validation failed while acquiring the next frame")]
183 SurfaceValidation,
184 #[error("WGPU exhausted GPU memory: {message}")]
185 GpuOutOfMemory { message: String },
186 #[error("WGPU reported an uncaptured validation error: {message}")]
187 GpuValidation { message: String },
188 #[error("WGPU reported an uncaptured internal error: {message}")]
189 GpuInternal { message: String },
190 #[cfg(feature = "test-engine")]
191 #[error("Test Engine frame {frame} failed: {source}")]
192 TestEngineFrame {
193 frame: u64,
194 #[source]
195 source: Box<dear_imgui_test_engine::FrameDriverError<RunError, RunError, RunError>>,
196 },
197 #[error("application callback failed during {stage}: {source}")]
198 Application {
199 stage: ApplicationStage,
200 #[source]
201 source: Box<dyn StdError + 'static>,
202 },
203 #[error("GPU generation recovery failed: {message}")]
204 Recovery { message: String },
205}
206
207impl RunError {
208 #[must_use]
210 pub fn application<E>(stage: ApplicationStage, source: E) -> Self
211 where
212 E: StdError + 'static,
213 {
214 Self::Application {
215 stage,
216 source: Box::new(source),
217 }
218 }
219
220 #[must_use]
222 pub const fn application_stage(&self) -> Option<ApplicationStage> {
223 match self {
224 Self::Application { stage, .. } | Self::ImGuiFrameOwnership { stage, .. } => {
225 Some(*stage)
226 }
227 _ => None,
228 }
229 }
230
231 #[cfg(test)]
232 pub(crate) fn application_message(stage: ApplicationStage, message: impl Into<String>) -> Self {
233 Self::application(stage, ApplicationMessage(message.into()))
234 }
235
236 pub(crate) fn during_application_stage(self, stage: ApplicationStage) -> Self {
237 if self.application_stage() == Some(stage) {
238 self
239 } else {
240 Self::application(stage, self)
241 }
242 }
243}
244
245pub trait Application {
251 fn configure_imgui(&mut self, _context: &mut InitContext<'_>) -> Result<(), RunError> {
253 Ok(())
254 }
255
256 fn initialized(
258 &mut self,
259 _context: &mut InitializedContext<'_>,
260 _gpu: &mut GpuContext<'_>,
261 ) -> Result<(), RunError> {
262 Ok(())
263 }
264
265 fn gpu_lost(&mut self, _context: &mut GpuContext<'_>) -> Result<(), RunError> {
267 Ok(())
268 }
269
270 fn gpu_recreated(&mut self, _context: &mut GpuContext<'_>) -> Result<(), RunError> {
272 Ok(())
273 }
274
275 fn event(&mut self, _context: &mut EventContext<'_>) -> Result<(), RunError> {
277 Ok(())
278 }
279
280 fn prepare_frame(&mut self, _context: &mut PrepareFrameContext<'_>) -> Result<(), RunError> {
282 Ok(())
283 }
284
285 fn frame(&mut self, context: &mut FrameContext<'_>) -> Result<(), RunError>;
287
288 #[cfg(feature = "test-engine")]
294 fn test_engine(&mut self) -> Option<&mut crate::test_engine::TestEngine> {
295 None
296 }
297
298 fn shutdown(&mut self, _context: &mut ShutdownContext<'_>) -> Result<(), RunError> {
300 Ok(())
301 }
302}
303
304pub struct InitContext<'a> {
305 pub(crate) imgui: &'a mut imgui::Context,
306 pub(crate) window: &'a Window,
307 pub(crate) config: &'a AppConfig,
308}
309
310impl InitContext<'_> {
311 pub fn imgui(&mut self) -> &mut imgui::Context {
317 self.imgui
318 }
319
320 #[must_use]
321 pub fn window(&self) -> &Window {
322 self.window
323 }
324
325 #[must_use]
326 pub fn config(&self) -> &AppConfig {
327 self.config
328 }
329}
330
331pub struct InitializedContext<'a> {
341 pub(crate) imgui: &'a mut imgui::Context,
342 pub(crate) window: &'a Window,
343 pub(crate) config: &'a AppConfig,
344}
345
346impl InitializedContext<'_> {
347 #[must_use]
348 pub fn window(&self) -> &Window {
349 self.window
350 }
351
352 #[must_use]
353 pub fn config(&self) -> &AppConfig {
354 self.config
355 }
356}
357
358pub struct EventContext<'a> {
370 pub(crate) event: &'a WindowEvent,
371 pub(crate) imgui: &'a mut imgui::Context,
372 pub(crate) window: &'a Window,
373 pub(crate) exit_requested: &'a mut bool,
374}
375
376impl EventContext<'_> {
377 #[must_use]
378 pub fn event(&self) -> &WindowEvent {
379 self.event
380 }
381
382 #[must_use]
383 pub fn window(&self) -> &Window {
384 self.window
385 }
386
387 pub fn request_exit(&mut self) {
389 *self.exit_requested = true;
390 }
391}
392
393pub struct ShutdownContext<'a> {
403 pub(crate) imgui: &'a mut imgui::Context,
404 pub(crate) window: &'a Window,
405 pub(crate) generation: Option<GpuGeneration>,
406}
407
408pub struct PrepareFrameContext<'a> {
421 pub(crate) imgui: &'a mut imgui::Context,
422 pub(crate) window: &'a Window,
423}
424
425impl PrepareFrameContext<'_> {
426 #[must_use]
427 pub fn window(&self) -> &Window {
428 self.window
429 }
430}
431
432macro_rules! impl_context_resource_access {
433 ($context:ident) => {
434 impl $context<'_> {
435 #[must_use]
437 pub fn io(&self) -> &imgui::Io {
438 self.imgui.io()
439 }
440
441 pub fn io_mut(&mut self) -> &mut imgui::Io {
443 self.imgui.io_mut()
444 }
445
446 #[must_use]
448 pub fn style(&self) -> &imgui::Style {
449 self.imgui.style()
450 }
451
452 pub fn style_mut(&mut self) -> &mut imgui::Style {
454 self.imgui.style_mut()
455 }
456
457 #[must_use]
459 pub fn font_atlas(&self) -> &imgui::FontAtlas {
460 self.imgui.font_atlas()
461 }
462
463 pub fn register_texture(
465 &mut self,
466 texture: imgui::OwnedTextureData,
467 ) -> imgui::ManagedTextureId {
468 self.imgui.register_texture(texture)
469 }
470
471 pub fn with_texture<R>(
473 &self,
474 id: imgui::ManagedTextureId,
475 operation: impl for<'texture> FnOnce(imgui::ManagedTextureRef<'texture>) -> R,
476 ) -> Result<R, imgui::ManagedTextureError> {
477 self.imgui.with_texture(id, operation)
478 }
479
480 pub fn with_texture_mut<R>(
482 &mut self,
483 id: imgui::ManagedTextureId,
484 operation: impl for<'texture> FnOnce(imgui::ManagedTextureMut<'texture>) -> R,
485 ) -> Result<R, imgui::ManagedTextureError> {
486 self.imgui.with_texture_mut(id, operation)
487 }
488
489 pub fn try_with_texture_mut<R>(
491 &mut self,
492 id: imgui::ManagedTextureId,
493 operation: impl for<'texture> FnOnce(
494 imgui::ManagedTextureMut<'texture>,
495 ) -> Result<R, imgui::TextureDataError>,
496 ) -> Result<R, imgui::ManagedTextureMutationError> {
497 self.imgui.try_with_texture_mut(id, operation)
498 }
499
500 pub fn remove_texture(
502 &mut self,
503 id: imgui::ManagedTextureId,
504 ) -> Result<(), imgui::ManagedTextureError> {
505 self.imgui.remove_texture(id)
506 }
507 }
508 };
509}
510
511impl_context_resource_access!(EventContext);
512impl_context_resource_access!(InitializedContext);
513impl_context_resource_access!(PrepareFrameContext);
514impl_context_resource_access!(ShutdownContext);
515
516impl ShutdownContext<'_> {
517 #[must_use]
518 pub fn window(&self) -> &Window {
519 self.window
520 }
521
522 #[must_use]
523 pub const fn gpu_generation(&self) -> Option<GpuGeneration> {
524 self.generation
525 }
526}
527
528pub struct DockingController {
529 pub(crate) flags: DockNodeFlags,
530}
531
532pub struct DockingApi<'a> {
533 pub(crate) controller: &'a mut DockingController,
534}
535
536impl DockingApi<'_> {
537 #[must_use]
538 pub fn flags(&self) -> DockNodeFlags {
539 DockNodeFlags::from_bits_retain(self.controller.flags.bits())
540 }
541
542 pub fn set_flags(&mut self, flags: DockNodeFlags) {
543 self.controller.flags = flags;
544 }
545}
546
547pub struct AddOns<'a> {
548 #[cfg(feature = "implot")]
549 pub(crate) implot: Option<&'a implot::PlotContext>,
550 #[cfg(feature = "imnodes")]
551 pub(crate) imnodes: Option<&'a imnodes::Context>,
552 #[cfg(feature = "implot3d")]
553 pub(crate) implot3d: Option<&'a implot3d::Plot3DContext>,
554 pub(crate) docking: DockingApi<'a>,
555}
556
557impl<'a> AddOns<'a> {
558 #[cfg(feature = "implot")]
559 #[must_use]
560 pub fn implot(&self) -> Option<&'a implot::PlotContext> {
561 self.implot
562 }
563
564 #[cfg(feature = "imnodes")]
565 #[must_use]
566 pub fn imnodes(&self) -> Option<&'a imnodes::Context> {
567 self.imnodes
568 }
569
570 #[cfg(feature = "implot3d")]
571 #[must_use]
572 pub fn implot3d(&self) -> Option<&'a implot3d::Plot3DContext> {
573 self.implot3d
574 }
575
576 pub fn docking(&mut self) -> &mut DockingApi<'a> {
577 &mut self.docking
578 }
579}
580
581pub struct GpuApi<'a> {
582 pub(crate) device: &'a wgpu::Device,
583 pub(crate) queue: &'a wgpu::Queue,
584 pub(crate) renderer: &'a mut dear_imgui_wgpu::WgpuRenderer,
585 pub(crate) generation: GpuGeneration,
586}
587
588impl GpuApi<'_> {
589 #[must_use]
590 pub fn device(&self) -> &wgpu::Device {
591 self.device
592 }
593
594 #[must_use]
595 pub fn queue(&self) -> &wgpu::Queue {
596 self.queue
597 }
598
599 #[must_use]
600 pub const fn generation(&self) -> GpuGeneration {
601 self.generation
602 }
603
604 pub fn register_external_texture(
605 &mut self,
606 view: &wgpu::TextureView,
607 ) -> Result<ExternalTextureHandle, ExternalTextureError> {
608 Ok(ExternalTextureHandle {
609 id: self.renderer.register_external_texture(view)?,
610 generation: self.generation,
611 })
612 }
613
614 pub fn resolve_external_texture(
615 &self,
616 handle: ExternalTextureHandle,
617 ) -> Result<TextureId, ExternalTextureError> {
618 handle
619 .resolve_for_generation(self.generation)
620 .map(dear_imgui_wgpu::ExternalTextureId::texture_id)
621 }
622
623 pub fn update_external_texture(
624 &mut self,
625 handle: ExternalTextureHandle,
626 view: &wgpu::TextureView,
627 ) -> Result<(), ExternalTextureError> {
628 let texture = handle.resolve_for_generation(self.generation)?;
629 self.renderer.update_external_texture(texture, view)?;
630 Ok(())
631 }
632
633 pub fn unregister_external_texture(
634 &mut self,
635 handle: ExternalTextureHandle,
636 ) -> Result<(), ExternalTextureError> {
637 let texture = handle.resolve_for_generation(self.generation)?;
638 self.renderer.unregister_external_texture(texture)?;
639 Ok(())
640 }
641}
642
643pub struct GpuContext<'a> {
644 pub(crate) window: &'a Window,
645 pub(crate) surface_config: &'a wgpu::SurfaceConfiguration,
646 pub(crate) gpu: GpuApi<'a>,
647}
648
649impl<'a> GpuContext<'a> {
650 #[must_use]
651 pub fn window(&self) -> &Window {
652 self.window
653 }
654
655 #[must_use]
656 pub fn surface_config(&self) -> &wgpu::SurfaceConfiguration {
657 self.surface_config
658 }
659
660 pub fn gpu(&mut self) -> &mut GpuApi<'a> {
661 &mut self.gpu
662 }
663
664 #[must_use]
665 pub const fn generation(&self) -> GpuGeneration {
666 self.gpu.generation
667 }
668}
669
670pub struct FrameContext<'a> {
671 pub(crate) ui: &'a imgui::Ui,
672 pub(crate) addons: AddOns<'a>,
673 pub(crate) gpu: GpuApi<'a>,
674 pub(crate) exit_requested: &'a mut bool,
675}
676
677impl<'a> FrameContext<'a> {
678 #[must_use]
679 pub fn ui(&self) -> &'a imgui::Ui {
680 self.ui
681 }
682
683 pub fn addons(&mut self) -> &mut AddOns<'a> {
684 &mut self.addons
685 }
686
687 pub fn gpu(&mut self) -> &mut GpuApi<'a> {
688 &mut self.gpu
689 }
690
691 pub fn request_exit(&mut self) {
696 *self.exit_requested = true;
697 }
698}
699
700#[cfg(test)]
701mod tests {
702 use std::error::Error as _;
703
704 use thiserror::Error;
705
706 use super::{ApplicationStage, ExternalTextureError, GpuGeneration, RunError};
707
708 #[derive(Debug, Error)]
709 #[error("injected user failure")]
710 struct UserError;
711
712 #[test]
713 fn stale_generation_error_names_both_epochs() {
714 let error = ExternalTextureError::StaleGeneration {
715 handle_generation: 2,
716 current_generation: 3,
717 };
718
719 assert_eq!(
720 error.to_string(),
721 "external texture belongs to GPU generation 2, current generation is 3"
722 );
723 }
724
725 #[test]
726 fn stale_gpu_generation_is_rejected_after_recovery() {
727 assert!(matches!(
728 GpuGeneration(2).ensure_current(GpuGeneration(3)),
729 Err(ExternalTextureError::StaleGeneration {
730 handle_generation: 2,
731 current_generation: 3,
732 })
733 ));
734 assert!(GpuGeneration(3).ensure_current(GpuGeneration(3)).is_ok());
735 }
736
737 #[test]
738 fn application_error_retains_typed_stage_and_original_source() {
739 let error = RunError::application(ApplicationStage::Frame, UserError);
740
741 assert!(matches!(
742 &error,
743 RunError::Application {
744 stage: ApplicationStage::Frame,
745 ..
746 }
747 ));
748 assert!(
749 error
750 .source()
751 .and_then(|source| source.downcast_ref::<UserError>())
752 .is_some()
753 );
754 }
755
756 #[test]
757 fn assigning_the_same_stage_does_not_duplicate_the_source_chain() {
758 let error = RunError::application(ApplicationStage::Frame, UserError)
759 .during_application_stage(ApplicationStage::Frame);
760
761 assert!(
762 error
763 .source()
764 .and_then(|source| source.downcast_ref::<UserError>())
765 .is_some()
766 );
767 }
768
769 #[test]
770 fn frame_ownership_error_reports_its_application_stage() {
771 let _guard = crate::runtime::imgui_test_guard();
772 let context = dear_imgui_rs::Context::create();
773 let stamp = context.frame_lifecycle_stamp();
774 let error = RunError::ImGuiFrameOwnership {
775 stage: ApplicationStage::PrepareFrame,
776 before: stamp,
777 after: stamp,
778 };
779
780 assert_eq!(
781 error.application_stage(),
782 Some(ApplicationStage::PrepareFrame)
783 );
784 }
785
786 #[test]
787 fn runtime_assigned_stage_retains_the_hook_run_error_as_source() {
788 let error = RunError::GpuValidation {
789 message: "injected validation failure".to_owned(),
790 }
791 .during_application_stage(ApplicationStage::GpuRecreated);
792
793 assert!(matches!(
794 &error,
795 RunError::Application {
796 stage: ApplicationStage::GpuRecreated,
797 ..
798 }
799 ));
800 assert!(matches!(
801 error
802 .source()
803 .and_then(|source| source.downcast_ref::<RunError>()),
804 Some(RunError::GpuValidation { message })
805 if message == "injected validation failure"
806 ));
807 }
808}