Skip to main content

dear_app/
application.rs

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/// Monotonically increasing identity of the active GPU resource set.
18#[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/// Opaque external texture identity bound to one GPU generation.
49#[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/// Lifecycle hook that produced an [`Application`] error.
85///
86/// The runtime assigns this stage at the hook boundary. Applications therefore do not need to
87/// encode callback names in strings, and callers can inspect failures without parsing messages.
88#[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    /// Wraps an application-owned error with the lifecycle stage that produced it.
199    #[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    /// Returns the lifecycle stage for an application callback failure.
211    #[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
239/// Persistent user application. This value survives every GPU recreation.
240///
241/// The runtime wraps every hook failure with the matching [`ApplicationStage`] while retaining
242/// the returned [`RunError`] as its source. Hooks therefore return the most specific error they
243/// have instead of formatting callback names into messages.
244pub trait Application {
245    /// Configures the one stable Dear ImGui context before renderer initialization.
246    fn configure_imgui(&mut self, _context: &mut InitContext<'_>) -> Result<(), RunError> {
247        Ok(())
248    }
249
250    /// Runs exactly once after the window, UI state, and first GPU generation are ready.
251    fn initialized(
252        &mut self,
253        _init: &mut InitContext<'_>,
254        _gpu: &mut GpuContext<'_>,
255    ) -> Result<(), RunError> {
256        Ok(())
257    }
258
259    /// Runs before the old GPU generation is invalidated and destroyed.
260    fn gpu_lost(&mut self, _context: &mut GpuContext<'_>) -> Result<(), RunError> {
261        Ok(())
262    }
263
264    /// Runs after a replacement GPU generation has been committed.
265    fn gpu_recreated(&mut self, _context: &mut GpuContext<'_>) -> Result<(), RunError> {
266        Ok(())
267    }
268
269    /// Receives events for the live main window only.
270    fn event(&mut self, _context: &mut EventContext<'_>) -> Result<(), RunError> {
271        Ok(())
272    }
273
274    /// Mutates Context-owned resources before Dear ImGui opens the next frame.
275    fn prepare_frame(&mut self, _context: &mut PrepareFrameContext<'_>) -> Result<(), RunError> {
276        Ok(())
277    }
278
279    /// Builds one Dear ImGui frame.
280    fn frame(&mut self, context: &mut FrameContext<'_>) -> Result<(), RunError>;
281
282    /// Returns the Test Engine attached to this application's Context, when enabled.
283    ///
284    /// The runtime drives this engine only for admitted surface frames and owns the complete
285    /// `render -> pre-swap -> present -> post-swap` protocol. Applications may use the engine from
286    /// [`Self::frame`] for UI and queue controls, but must not drive presentation independently.
287    #[cfg(feature = "test-engine")]
288    fn test_engine(&mut self) -> Option<&mut crate::test_engine::TestEngine> {
289        None
290    }
291
292    /// Runs exactly once before add-ons and the Dear ImGui context are torn down.
293    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    /// Requests normal event-loop exit after the current event callback completes.
343    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
354/// Pre-frame access to Context-owned resources.
355pub struct PrepareFrameContext<'a> {
356    pub(crate) imgui: &'a mut imgui::Context,
357    pub(crate) window: &'a Window,
358}
359
360impl PrepareFrameContext<'_> {
361    /// Returns the Context before its Dear ImGui frame is opened.
362    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    /// Requests normal event-loop exit after the current frame is presented.
552    ///
553    /// This is a control signal, not an error. If the same callback returns an error, that error
554    /// remains primary and shutdown still runs exactly once.
555    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}