Skip to main content

dear_imgui_wgpu/
data.rs

1//! Core data structures for the WGPU renderer
2//!
3//! This module contains the main backend data structure and initialization info,
4//! following the pattern from imgui_impl_wgpu.cpp
5
6use std::{cell::Cell, ffi::c_void, marker::PhantomData, ptr::NonNull, rc::Rc};
7
8use crate::{FrameResourceArena, FrameResources, RenderResources, RendererError, RendererResult};
9use dear_imgui_rs::sys;
10use thiserror::Error;
11use wgpu::*;
12
13/// Error returned while borrowing the transient WGPU draw-callback state.
14#[derive(Copy, Clone, Debug, Eq, Error, PartialEq)]
15#[non_exhaustive]
16pub enum WgpuRenderStateAccessError {
17    /// No WGPU render state is active on the current Dear ImGui Context.
18    #[error("no WGPU render state is active on the current Dear ImGui context")]
19    Inactive,
20    /// The active callback state is already borrowed by an outer scoped access.
21    #[error("the active WGPU render state is already borrowed")]
22    AlreadyBorrowed,
23}
24
25pub(crate) struct WgpuRenderStateStorage {
26    device: NonNull<Device>,
27    render_pass: NonNull<c_void>,
28    borrowed: Cell<bool>,
29}
30
31impl WgpuRenderStateStorage {
32    pub(crate) fn new(device: &Device, render_pass: &mut RenderPass<'_>) -> Self {
33        Self {
34            device: NonNull::from(device),
35            render_pass: NonNull::from(render_pass).cast(),
36            borrowed: Cell::new(false),
37        }
38    }
39}
40
41struct WgpuRenderStateBorrow<'storage>(&'storage Cell<bool>);
42
43impl Drop for WgpuRenderStateBorrow<'_> {
44    fn drop(&mut self) {
45        self.0.set(false);
46    }
47}
48
49/// Scoped access to the WGPU resources selected for a raw draw callback.
50///
51/// This corresponds to ImGui_ImplWGPU_RenderState in the C++ implementation.
52/// The value can only be obtained through [`Self::with_current`] while the
53/// renderer is invoking a raw callback. It cannot outlive that callback scope.
54///
55/// The state borrows the renderer's device and render pass; it does not own
56/// either resource and does not provide access to the Dear ImGui [`Context`].
57///
58/// ```compile_fail
59/// use dear_imgui_wgpu::WgpuRenderState;
60///
61/// // Callback-scoped resources cannot be returned from the higher-ranked borrow.
62/// let _escaped = unsafe { WgpuRenderState::with_current(|state| state.device()) };
63/// ```
64///
65/// [`Context`]: dear_imgui_rs::Context
66#[derive(Debug)]
67pub struct WgpuRenderState<'callback> {
68    storage: NonNull<WgpuRenderStateStorage>,
69    _callback: PhantomData<&'callback mut WgpuRenderStateStorage>,
70    _ui_thread: PhantomData<Rc<()>>,
71}
72
73impl WgpuRenderState<'_> {
74    /// Borrows the state published for the current raw draw callback.
75    ///
76    /// # Safety
77    ///
78    /// This function may only be called from a raw draw callback currently
79    /// invoked by `dear-imgui-wgpu`. The current Dear ImGui Context must be the
80    /// renderer owner and its `Renderer_RenderState` slot must still contain
81    /// the WGPU state installed for this callback. The callback must not replace
82    /// that slot while `callback` is running.
83    ///
84    /// The higher-ranked closure prevents references to the render pass from
85    /// escaping the callback scope. Recursive access is rejected at runtime.
86    pub unsafe fn with_current<R>(
87        callback: impl for<'callback> FnOnce(WgpuRenderState<'callback>) -> R,
88    ) -> Result<R, WgpuRenderStateAccessError> {
89        let platform_io = unsafe { sys::igGetPlatformIO_Nil() };
90        let raw_state = if platform_io.is_null() {
91            None
92        } else {
93            NonNull::new(unsafe { (*platform_io).Renderer_RenderState })
94        }
95        .ok_or(WgpuRenderStateAccessError::Inactive)?;
96        let storage = raw_state.cast::<WgpuRenderStateStorage>();
97        let borrowed = unsafe { &storage.as_ref().borrowed };
98        if borrowed.replace(true) {
99            return Err(WgpuRenderStateAccessError::AlreadyBorrowed);
100        }
101        let _borrow = WgpuRenderStateBorrow(borrowed);
102        Ok(callback(WgpuRenderState {
103            storage,
104            _callback: PhantomData,
105            _ui_thread: PhantomData,
106        }))
107    }
108
109    /// Returns the renderer device for the callback duration.
110    pub fn device(&self) -> &Device {
111        unsafe { self.storage.as_ref().device.as_ref() }
112    }
113
114    /// Returns the active render pass for the duration of this borrow.
115    pub fn render_pass(&mut self) -> &mut RenderPass<'_> {
116        unsafe {
117            self.storage
118                .as_ref()
119                .render_pass
120                .cast::<RenderPass<'_>>()
121                .as_mut()
122        }
123    }
124
125    /// Returns the device and render pass as disjoint callback-scoped borrows.
126    pub fn resources(&mut self) -> (&Device, &mut RenderPass<'_>) {
127        let storage = unsafe { self.storage.as_ref() };
128        let device = unsafe { storage.device.as_ref() };
129        let render_pass = unsafe { storage.render_pass.cast::<RenderPass<'_>>().as_mut() };
130        (device, render_pass)
131    }
132}
133
134/// Presentation policy for WGPU surfaces created for secondary viewports.
135///
136/// Surface format and dimensions are renderer- and viewport-owned respectively. Secondary
137/// surfaces use the renderer's sRGB output contract; this value keeps the remaining scheduling
138/// and compositor choices together so creation and surface-loss recovery cannot silently diverge.
139#[derive(Debug, Clone, Copy, PartialEq, Eq)]
140pub struct WgpuViewportSurfaceConfig {
141    /// Requested presentation mode.
142    pub present_mode: PresentMode,
143    /// Requested compositor alpha mode.
144    pub alpha_mode: CompositeAlphaMode,
145    /// Maximum number of monitor refreshes between acquisition and presentation.
146    pub desired_maximum_frame_latency: u32,
147}
148
149impl Default for WgpuViewportSurfaceConfig {
150    fn default() -> Self {
151        Self {
152            present_mode: PresentMode::Fifo,
153            alpha_mode: CompositeAlphaMode::Opaque,
154            desired_maximum_frame_latency: 2,
155        }
156    }
157}
158
159impl From<&SurfaceConfiguration> for WgpuViewportSurfaceConfig {
160    fn from(config: &SurfaceConfiguration) -> Self {
161        Self {
162            present_mode: config.present_mode,
163            alpha_mode: config.alpha_mode,
164            desired_maximum_frame_latency: config.desired_maximum_frame_latency,
165        }
166    }
167}
168
169/// Initialization data for ImGui WGPU renderer.
170///
171/// This corresponds to `ImGui_ImplWGPU_InitInfo` plus Rust-owned multi-viewport policy.
172#[derive(Debug, Clone)]
173pub struct WgpuInitInfo {
174    /// WGPU instance (required for multi-viewport to create per-window surfaces)
175    pub instance: Option<Instance>,
176    /// WGPU adapter (required by multi-viewport; optional for single-window rendering)
177    pub adapter: Option<Adapter>,
178    /// WGPU device
179    pub device: Device,
180    /// WGPU queue
181    pub queue: Queue,
182    /// Render target format
183    pub render_target_format: TextureFormat,
184    /// Presentation policy for secondary viewport surfaces.
185    pub viewport_surface_config: WgpuViewportSurfaceConfig,
186    /// Depth stencil format (None if no depth buffer)
187    pub depth_stencil_format: Option<TextureFormat>,
188    /// Pipeline multisample state
189    pub pipeline_multisample_state: MultisampleState,
190}
191
192impl WgpuInitInfo {
193    /// Create new initialization info with required parameters
194    pub fn new(device: Device, queue: Queue, render_target_format: TextureFormat) -> Self {
195        Self {
196            instance: None,
197            adapter: None,
198            device,
199            queue,
200            render_target_format,
201            viewport_surface_config: WgpuViewportSurfaceConfig::default(),
202            depth_stencil_format: None,
203            pipeline_multisample_state: MultisampleState {
204                count: 1,
205                mask: !0,
206                alpha_to_coverage_enabled: false,
207            },
208        }
209    }
210
211    /// Set the depth stencil format
212    pub fn with_depth_stencil_format(mut self, format: TextureFormat) -> Self {
213        self.depth_stencil_format = Some(format);
214        self
215    }
216
217    /// Set the multisample state
218    pub fn with_multisample_state(mut self, state: MultisampleState) -> Self {
219        self.pipeline_multisample_state = state;
220        self
221    }
222
223    /// Provide an instance for creating per-window surfaces (multi-viewport)
224    pub fn with_instance(mut self, instance: Instance) -> Self {
225        self.instance = Some(instance);
226        self
227    }
228
229    /// Provide the adapter required to negotiate multi-viewport surface capabilities
230    pub fn with_adapter(mut self, adapter: Adapter) -> Self {
231        self.adapter = Some(adapter);
232        self
233    }
234
235    /// Set the complete presentation policy for secondary viewport surfaces.
236    pub fn with_viewport_surface_config(mut self, config: WgpuViewportSurfaceConfig) -> Self {
237        self.viewport_surface_config = config;
238        self
239    }
240}
241
242/// Main backend data structure
243///
244/// This corresponds to ImGui_ImplWGPU_Data in the C++ implementation
245pub(crate) struct WgpuBackendData {
246    /// Initialization info
247    pub(crate) init_info: WgpuInitInfo,
248    /// WGPU device
249    pub(crate) device: Device,
250    /// Default queue
251    pub(crate) queue: Queue,
252    /// Render target format
253    pub(crate) render_target_format: TextureFormat,
254    /// Depth stencil format
255    pub(crate) depth_stencil_format: Option<TextureFormat>,
256    /// Render pipeline
257    pub(crate) pipeline_state: Option<RenderPipeline>,
258    /// Render resources (samplers, uniforms, bind groups)
259    pub(crate) render_resources: RenderResources,
260    /// Upload resources owned by the active Context render epoch.
261    pub(crate) frame_resources: FrameResourceArena,
262    /// Exact Context epoch and native frame currently open for rendering.
263    pub(crate) frame_cursor: FrameEpochCursor,
264}
265
266#[derive(Default)]
267pub(crate) struct FrameEpochCursor {
268    epoch: Option<u64>,
269    native_frame_count: Option<i32>,
270}
271
272enum FrameEpochTransition {
273    Reuse,
274    Advance,
275}
276
277impl FrameEpochCursor {
278    fn enter(
279        &mut self,
280        epoch: u64,
281        native_frame_count: i32,
282    ) -> RendererResult<FrameEpochTransition> {
283        if let Some(active_epoch) = self.epoch {
284            if epoch < active_epoch {
285                return Err(RendererError::FrameEpochOutOfOrder {
286                    active: active_epoch,
287                    received: epoch,
288                });
289            }
290            if epoch == active_epoch {
291                if self.native_frame_count != Some(native_frame_count) {
292                    return Err(RendererError::InvalidRenderState(
293                        "one WGPU render epoch was observed under multiple Dear ImGui frames"
294                            .to_owned(),
295                    ));
296                }
297                return Ok(FrameEpochTransition::Reuse);
298            }
299        }
300
301        self.epoch = Some(epoch);
302        self.native_frame_count = Some(native_frame_count);
303        Ok(FrameEpochTransition::Advance)
304    }
305
306    pub(crate) fn is_native_frame(&self, native_frame_count: i32) -> bool {
307        self.native_frame_count == Some(native_frame_count)
308    }
309}
310
311impl WgpuBackendData {
312    /// Create new backend data from initialization info
313    pub(crate) fn new(init_info: WgpuInitInfo) -> Self {
314        let queue = init_info.queue.clone();
315        Self {
316            device: init_info.device.clone(),
317            queue,
318            render_target_format: init_info.render_target_format,
319            depth_stencil_format: init_info.depth_stencil_format,
320            pipeline_state: None,
321            render_resources: RenderResources::new(),
322            frame_resources: FrameResourceArena::new(),
323            frame_cursor: FrameEpochCursor::default(),
324            init_info,
325        }
326    }
327
328    /// Open the arena for one exact Context render epoch.
329    pub(crate) fn begin_frame(
330        &mut self,
331        epoch: u64,
332        native_frame_count: i32,
333    ) -> RendererResult<()> {
334        if let FrameEpochTransition::Advance = self.frame_cursor.enter(epoch, native_frame_count)? {
335            self.frame_resources.begin_epoch();
336        }
337        Ok(())
338    }
339
340    pub(crate) fn acquire_frame_resources(&mut self) -> RendererResult<&mut FrameResources> {
341        if self.frame_cursor.epoch.is_none() {
342            return Err(RendererError::FrameNotPrepared);
343        }
344        let frame = self.frame_resources.acquire();
345        frame.ensure_render_bindings(&self.device, &self.render_resources)?;
346        Ok(frame)
347    }
348
349    /// Check if the backend is initialized
350    pub(crate) fn is_initialized(&self) -> bool {
351        self.pipeline_state.is_some()
352    }
353}
354
355#[cfg(test)]
356mod tests {
357    use super::*;
358
359    #[test]
360    fn frame_epoch_cursor_is_idempotent_and_rejects_stale_frames() {
361        let mut cursor = FrameEpochCursor::default();
362        assert!(matches!(
363            cursor.enter(4, 12).unwrap(),
364            FrameEpochTransition::Advance
365        ));
366        assert!(matches!(
367            cursor.enter(4, 12).unwrap(),
368            FrameEpochTransition::Reuse
369        ));
370        assert!(matches!(
371            cursor.enter(5, 13).unwrap(),
372            FrameEpochTransition::Advance
373        ));
374        assert!(matches!(
375            cursor.enter(4, 12),
376            Err(RendererError::FrameEpochOutOfOrder {
377                active: 5,
378                received: 4
379            })
380        ));
381    }
382
383    #[test]
384    fn frame_epoch_cursor_rejects_one_epoch_under_two_native_frames() {
385        let mut cursor = FrameEpochCursor::default();
386        cursor.enter(1, 7).unwrap();
387        assert!(matches!(
388            cursor.enter(1, 8),
389            Err(RendererError::InvalidRenderState(_))
390        ));
391    }
392
393    #[test]
394    fn viewport_surface_defaults_are_explicit_and_throughput_safe() {
395        let config = WgpuViewportSurfaceConfig::default();
396        assert_eq!(config.present_mode, PresentMode::Fifo);
397        assert_eq!(config.alpha_mode, CompositeAlphaMode::Opaque);
398        assert_eq!(config.desired_maximum_frame_latency, 2);
399    }
400
401    #[test]
402    fn viewport_surface_config_copies_supported_main_surface_policy() {
403        let surface = SurfaceConfiguration {
404            usage: TextureUsages::RENDER_ATTACHMENT,
405            format: TextureFormat::Bgra8UnormSrgb,
406            #[cfg(feature = "wgpu-30")]
407            color_space: SurfaceColorSpace::DisplayP3,
408            width: 128,
409            height: 96,
410            present_mode: PresentMode::AutoNoVsync,
411            alpha_mode: CompositeAlphaMode::PreMultiplied,
412            view_formats: vec![],
413            desired_maximum_frame_latency: 3,
414        };
415
416        let viewport = WgpuViewportSurfaceConfig::from(&surface);
417        assert_eq!(viewport.present_mode, surface.present_mode);
418        assert_eq!(viewport.alpha_mode, surface.alpha_mode);
419        assert_eq!(
420            viewport.desired_maximum_frame_latency,
421            surface.desired_maximum_frame_latency
422        );
423    }
424}