Skip to main content

dear_imgui_wgpu/renderer/
render.rs

1#[cfg(feature = "mv-log")]
2use std::sync::{Mutex, OnceLock};
3
4use super::{
5    RendererRenderStateGuard, WgpuRenderer, draw::FramebufferExtent,
6    map_renderer_render_state_error,
7};
8use crate::wgpu;
9use crate::{GammaMode, RendererError, RendererResult, Uniforms};
10use dear_imgui_rs::{
11    ContextBinding,
12    render::{DrawData, PendingFrame, ReconciledFrame},
13    sys,
14};
15use wgpu::RenderPass;
16
17#[allow(unused_macros)]
18macro_rules! mvlog {
19    ($($arg:tt)*) => {
20        if cfg!(feature = "mv-log") { eprintln!($($arg)*); }
21    }
22}
23
24fn with_bound_context<R>(
25    binding: &ContextBinding,
26    callback: impl FnOnce() -> RendererResult<R>,
27) -> RendererResult<R> {
28    binding
29        .try_with_bound_context(callback)
30        .map_err(|_| RendererError::ContextDropped)?
31}
32
33fn platform_io_for_current_context() -> RendererResult<*mut sys::ImGuiPlatformIO> {
34    let context = unsafe { sys::igGetCurrentContext() };
35    let platform_io = unsafe { sys::igGetPlatformIO_ContextPtr(context) };
36    if platform_io.is_null() {
37        Err(RendererError::InvalidRenderState(
38            "bound Dear ImGui context has no PlatformIO".to_owned(),
39        ))
40    } else {
41        Ok(platform_io)
42    }
43}
44
45impl WgpuRenderer {
46    fn ensure_frame_prepared(&self) -> RendererResult<()> {
47        let native_frame_count = unsafe { sys::igGetFrameCount() };
48        match self.backend_data.as_ref() {
49            Some(backend_data)
50                if backend_data
51                    .frame_cursor
52                    .is_native_frame(native_frame_count) =>
53            {
54                Ok(())
55            }
56            Some(_) => Err(RendererError::FrameNotPrepared),
57            None => Err(RendererError::InvalidRenderState(
58                "Renderer not initialized".to_owned(),
59            )),
60        }
61    }
62
63    fn prepare_frame_bound(&mut self, frame: &PendingFrame<'_>) -> RendererResult<()> {
64        let epoch = frame.epoch();
65        self.prepare_frame_epoch(epoch.sequence(), unsafe { sys::igGetFrameCount() })
66    }
67
68    /// Renders one Context-borrowed Dear ImGui frame into an explicit framebuffer extent.
69    ///
70    /// Managed texture requests are reconciled before draw commands resolve
71    /// renderer texture IDs. WGPU render passes do not expose their attachment dimensions, so
72    /// callers must pass the physical width and height of the target being rendered.
73    pub fn render(
74        &mut self,
75        frame: PendingFrame<'_>,
76        render_pass: &mut RenderPass<'_>,
77        framebuffer_extent: FramebufferExtent,
78    ) -> RendererResult<()> {
79        let frame = self.reconcile_frame(frame)?;
80        self.render_reconciled(frame, render_pass, framebuffer_extent)
81    }
82
83    /// Renders an already reconciled frame into an explicit framebuffer extent.
84    ///
85    /// Recording consumes the reconciled capability. Command submission and presentation remain
86    /// the application's responsibility.
87    pub fn render_reconciled(
88        &mut self,
89        frame: ReconciledFrame<'_>,
90        render_pass: &mut RenderPass<'_>,
91        framebuffer_extent: FramebufferExtent,
92    ) -> RendererResult<()> {
93        self.ensure_renderer_contract()?;
94        self.ensure_reconciled_frame_matches(&frame)?;
95        let binding = self.bound_context()?;
96        with_bound_context(&binding, || {
97            let platform_io = platform_io_for_current_context()?;
98            self.render_read_only_draw_data_with_fb_size(
99                frame.draw_data(),
100                render_pass,
101                framebuffer_extent.width(),
102                framebuffer_extent.height(),
103                true,
104                platform_io,
105            )
106        })
107    }
108
109    /// Applies managed-texture requests without drawing or acquiring a surface.
110    ///
111    /// Callback capability is checked before texture reconciliation, so an
112    /// unsupported callback-bearing frame is not consumed partially.
113    pub fn reconcile_frame<'frame>(
114        &mut self,
115        frame: PendingFrame<'frame>,
116    ) -> RendererResult<ReconciledFrame<'frame>> {
117        self.ensure_renderer_contract()?;
118        self.ensure_pending_frame_matches(&frame)?;
119        let binding = self.bound_context()?;
120        with_bound_context(&binding, || {
121            Self::preflight_draw_callback_support(frame.draw_requirements())?;
122            self.prepare_frame_bound(&frame)?;
123            self.reconcile_frame_bound(frame)
124        })
125    }
126
127    fn reconcile_frame_bound<'frame>(
128        &mut self,
129        frame: PendingFrame<'frame>,
130    ) -> RendererResult<ReconciledFrame<'frame>> {
131        let request_epoch = frame.epoch().sequence();
132        let backend_data = self.backend_data.as_mut().ok_or_else(|| {
133            RendererError::InvalidRenderState("Renderer not initialized".to_owned())
134        })?;
135        let feedback = self.texture_manager.handle_texture_requests(
136            frame.texture_requests(),
137            request_epoch,
138            &backend_data.device,
139            &backend_data.queue,
140            &mut backend_data.render_resources,
141        )?;
142        let frame = frame.reconcile_texture_feedback(feedback)?;
143        self.texture_manager
144            .prune_destroyed_managed_textures(frame.completion_progress().watermark());
145        Ok(frame)
146    }
147
148    /// Internal explicit-size variant used by renderer-owned secondary viewports.
149    pub(super) fn render_read_only_draw_data_with_fb_size(
150        &mut self,
151        draw_data: &DrawData,
152        render_pass: &mut RenderPass<'_>,
153        fb_width: u32,
154        fb_height: u32,
155        main_viewport: bool,
156        platform_io: *mut sys::ImGuiPlatformIO,
157    ) -> RendererResult<()> {
158        self.ensure_frame_prepared()?;
159        self.log_framebuffer_mismatch(draw_data, fb_width, fb_height, main_viewport);
160        let extent = FramebufferExtent::new(fb_width, fb_height);
161        if extent.is_empty() {
162            return Ok(());
163        }
164        self.render_draw_data_at_extent(draw_data, render_pass, extent, platform_io)
165    }
166
167    fn render_draw_data_at_extent(
168        &mut self,
169        draw_data: &DrawData,
170        render_pass: &mut RenderPass<'_>,
171        extent: FramebufferExtent,
172        platform_io: *mut sys::ImGuiPlatformIO,
173    ) -> RendererResult<()> {
174        self.ensure_renderer_contract()?;
175        if !draw_data.valid() {
176            return Ok(());
177        }
178        Self::preflight_draw_callback_support(draw_data.requirements())?;
179        unsafe {
180            RendererRenderStateGuard::<crate::WgpuRenderStateStorage>::preflight(platform_io)
181        }
182        .map_err(map_renderer_render_state_error)?;
183
184        let backend_data = self.backend_data.as_mut().ok_or_else(|| {
185            RendererError::InvalidRenderState("Renderer not initialized".to_owned())
186        })?;
187        let prepared = Self::prepare_draw_data(
188            &self.texture_manager,
189            &self.default_texture,
190            draw_data,
191            extent,
192            backend_data,
193        )?;
194        if prepared.is_empty() {
195            return Ok(());
196        }
197
198        let gamma = match self.gamma_mode {
199            GammaMode::Auto => Uniforms::gamma_for_format(backend_data.render_target_format),
200            GammaMode::Linear => 1.0,
201            GammaMode::Gamma22 => 2.2,
202        };
203        let state = Self::prepare_render_state_static(
204            draw_data,
205            backend_data,
206            gamma,
207            prepared.has_elements(),
208        )?;
209        let device = backend_data.device.clone();
210        Self::execute_prepared_draw_data(
211            prepared,
212            &state,
213            extent,
214            render_pass,
215            platform_io,
216            &device,
217        )
218    }
219
220    #[cfg(feature = "mv-log")]
221    fn log_framebuffer_mismatch(
222        &self,
223        draw_data: &DrawData,
224        fb_width: u32,
225        fb_height: u32,
226        main_viewport: bool,
227    ) {
228        static LAST_MISMATCH: OnceLock<Mutex<Option<(u32, u32, u32, u32, bool)>>> = OnceLock::new();
229        let expected_width = (draw_data.display_size()[0] * draw_data.framebuffer_scale()[0])
230            .round()
231            .max(0.0) as u32;
232        let expected_height = (draw_data.display_size()[1] * draw_data.framebuffer_scale()[1])
233            .round()
234            .max(0.0) as u32;
235        if expected_width == fb_width && expected_height == fb_height {
236            return;
237        }
238        let key = (
239            expected_width,
240            expected_height,
241            fb_width,
242            fb_height,
243            main_viewport,
244        );
245        let mut previous = LAST_MISMATCH
246            .get_or_init(|| Mutex::new(None))
247            .lock()
248            .unwrap();
249        if *previous != Some(key) {
250            mvlog!(
251                "[wgpu-mv] fb mismatch expected=({}, {}) override=({}, {}) disp=({:.1},{:.1}) fb_scale=({:.2},{:.2}) main={}",
252                expected_width,
253                expected_height,
254                fb_width,
255                fb_height,
256                draw_data.display_size()[0],
257                draw_data.display_size()[1],
258                draw_data.framebuffer_scale()[0],
259                draw_data.framebuffer_scale()[1],
260                main_viewport
261            );
262            *previous = Some(key);
263        }
264    }
265
266    #[cfg(not(feature = "mv-log"))]
267    fn log_framebuffer_mismatch(
268        &self,
269        _draw_data: &DrawData,
270        _fb_width: u32,
271        _fb_height: u32,
272        _main_viewport: bool,
273    ) {
274    }
275}