Skip to main content

cranpose_render_wgpu/
lib.rs

1//! WGPU renderer backend for GPU-accelerated 2D rendering.
2//!
3//! This renderer uses WGPU for cross-platform GPU support across
4//! desktop (Windows/Mac/Linux), web (WebGPU), and mobile Android.
5
6pub(crate) use cranpose_render_common::debug_toggles;
7pub use debug_toggles::{
8    DebugToggle, debug_toggle, debug_toggle_os, set_debug_toggle, set_debug_toggle_os,
9};
10pub use offscreen::composition_bytes_per_pixel;
11pub use render::presentable_root_usages;
12mod ablation;
13mod capture_hash;
14mod collect;
15mod draw_pass;
16mod effect_renderer;
17mod fast_cores;
18mod frame;
19mod geometry;
20mod idle_pool;
21mod layer_cache;
22pub use fast_cores::pin_current_thread_to_fast_cores;
23mod frame_graph;
24mod frame_packet;
25mod frontend;
26mod glass_split;
27mod glyph_run_arena;
28pub(crate) mod gpu_stats;
29mod initial_present;
30mod lazy_resource;
31mod offscreen;
32mod opaque_prefix;
33mod output_conversion;
34pub(crate) mod pass_timing;
35mod pipeline;
36mod pipeline_compiler;
37#[cfg(not(target_arch = "wasm32"))]
38pub mod pipeline_disk_cache;
39#[cfg(not(target_arch = "wasm32"))]
40mod present_runtime;
41mod record_columns;
42mod render;
43mod run_geometry;
44mod run_store;
45mod scene;
46mod shader_cache;
47mod shaders;
48mod shape_pipelines;
49#[cfg(test)]
50mod test_support;
51
52use std::{rc::Rc, sync::Arc};
53
54use cranpose_core::{MemoryApplier, NodeId};
55use cranpose_render_common::{
56    RenderScene, Renderer,
57    graph::RenderGraph,
58    software_text_raster::{
59        SoftwareTextFontSet, SoftwareTextMeasurer, software_text_font_set_from_fonts_or_default,
60    },
61};
62use cranpose_ui::{LayoutTree, TextMeasurer};
63use cranpose_ui_graphics::{Rect, Size};
64pub use frame_packet::PresentTimings;
65
66/// Platform code the present thread runs around every present to a surface,
67/// for what only the platform can do with its swapchain, such as asking the
68/// display when earlier frames reached the screen.
69#[cfg(not(target_arch = "wasm32"))]
70pub trait PresentObserver: Send {
71    /// Runs just before `surface` presents a frame.
72    fn before_present(&mut self, surface: &wgpu::Surface<'static>);
73    /// Runs just after `surface` presented it.
74    fn after_present(&mut self, surface: &wgpu::Surface<'static>);
75
76    /// Runs before `surface` is configured anew, while its current swapchain
77    /// still stands. Does nothing unless the platform needs it to.
78    fn before_reconfigure(&mut self, _surface: &wgpu::Surface<'static>) {}
79}
80use frame_packet::RenderReturns;
81#[doc(hidden)]
82pub use frame_packet::{CancelReason, PresentOutcome};
83use frontend::{DevOverlayCache, RendererFrontend};
84pub use gpu_stats::FrameStatsSnapshot as RenderStatsSnapshot;
85pub use initial_present::{clear_to_background, clear_to_default_background};
86pub use pass_timing::{GpuPassTimingEntry, GpuPassTimingReport};
87#[cfg(not(target_arch = "wasm32"))]
88use present_runtime::{
89    PresentControl, PresentHandle, PresentMsg, PresentRuntimeInit, PresentState,
90};
91use render::GpuRenderer;
92pub use render::{
93    frame_clear_color, frames_presented, pipelines_created, pipelines_created_off_frame,
94};
95pub use scene::{ClickAction, HitRegion, Scene};
96
97/// The optional device features the renderer exploits when the adapter
98/// offers them: pipeline caching (see `pipeline_disk_cache`) and the
99/// timestamp queries behind `CRANPOSE_GPU_PASS_TIMING`. Every platform's
100/// `request_device` passes this so a profiling toggle never needs a rebuilt
101/// binary; intersecting with the adapter's own features keeps the request
102/// valid on adapters without them.
103pub fn optional_device_features(adapter: &wgpu::Adapter) -> wgpu::Features {
104    adapter.features() & (wgpu::Features::PIPELINE_CACHE | wgpu::Features::TIMESTAMP_QUERY)
105}
106
107#[doc(hidden)]
108pub fn offscreen_render_target_for_tests(
109    device: &wgpu::Device,
110    width: u32,
111    height: u32,
112    label: &str,
113) -> (wgpu::Texture, wgpu::TextureView) {
114    let texture = offscreen::create_2d_texture(
115        device,
116        wgpu::TextureFormat::Rgba8Unorm,
117        width,
118        height,
119        wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
120        Some(label),
121    );
122    let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
123    (texture, view)
124}
125
126pub(crate) fn rect_to_quad(rect: Rect) -> [[f32; 2]; 4] {
127    [
128        [rect.x, rect.y],
129        [rect.x + rect.width, rect.y],
130        [rect.x, rect.y + rect.height],
131        [rect.x + rect.width, rect.y + rect.height],
132    ]
133}
134
135#[derive(Debug)]
136pub enum WgpuRendererError {
137    Layout(String),
138    Wgpu(String),
139}
140
141/// CPU-readable RGBA frame captured from the renderer output.
142#[derive(Debug, Clone)]
143pub struct CapturedFrame {
144    pub width: u32,
145    pub height: u32,
146    pub pixels: Vec<u8>,
147}
148
149#[doc(hidden)]
150#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
151pub struct DebugCpuAllocationStats {
152    pub scene_graph_node_count: usize,
153    pub scene_graph_heap_bytes: usize,
154    pub scene_hits_len: usize,
155    pub scene_hits_cap: usize,
156    pub scene_node_index_len: usize,
157    pub scene_node_index_cap: usize,
158    pub text_renderer_pool_len: usize,
159    pub text_renderer_pool_cap: usize,
160    pub image_texture_cache_len: usize,
161    pub image_texture_cache_cap: usize,
162    pub run_arena_staging_bytes: usize,
163    pub run_store_bytes: usize,
164    pub run_store_runs: usize,
165    pub scratch_image_vertices_cap: usize,
166    pub scratch_image_indices_cap: usize,
167    pub scratch_image_cmds_cap: usize,
168    pub layer_cache_len: usize,
169    pub layer_cache_bytes: u64,
170}
171
172pub(crate) struct TextSystemState {
173    measurer: SoftwareTextMeasurer,
174}
175
176impl TextSystemState {
177    fn from_font_set(fonts: SoftwareTextFontSet) -> Self {
178        Self {
179            measurer: SoftwareTextMeasurer::from_font_set(fonts, 8192),
180        }
181    }
182
183    pub(crate) fn text_cache_len(&self) -> usize {
184        0
185    }
186}
187
188impl pipeline::TextLayoutResolver for TextSystemState {
189    fn layout_text(
190        &mut self,
191        text: &cranpose_ui::text::AnnotatedString,
192        style: &cranpose_ui::text::TextStyle,
193    ) -> cranpose_ui::text_layout_result::TextLayoutResult {
194        if cranpose_ui::has_current_app_context() {
195            cranpose_ui::text::layout_text(text, style)
196        } else {
197            self.measurer.layout(text, style)
198        }
199    }
200}
201
202#[derive(Clone)]
203pub struct WgpuTextSystem {
204    software_fonts: SoftwareTextFontSet,
205}
206
207impl WgpuTextSystem {
208    pub fn from_fonts(fonts: &[&[u8]]) -> Self {
209        Self {
210            software_fonts: software_text_font_set_from_fonts_or_default(fonts),
211        }
212    }
213
214    /// Adopt a font set an app already built — the path app-supplied families
215    /// take, where faces were parsed once at startup rather than from static
216    /// byte slices here.
217    pub fn from_font_set(software_fonts: SoftwareTextFontSet) -> Self {
218        Self { software_fonts }
219    }
220
221    pub(crate) fn render_state(&self) -> TextSystemState {
222        TextSystemState::from_font_set(self.software_fonts.clone())
223    }
224
225    pub(crate) fn software_fonts(&self) -> SoftwareTextFontSet {
226        self.software_fonts.clone()
227    }
228}
229
230/// Create an accurate WGPU text measurer for headless tests without launching a window.
231pub fn headless_text_measurer() -> Rc<dyn TextMeasurer> {
232    headless_text_measurer_with_fonts(&[])
233}
234
235/// Create an accurate WGPU text measurer for headless tests with explicit fonts.
236pub fn headless_text_measurer_with_fonts(fonts: &[&[u8]]) -> Rc<dyn TextMeasurer> {
237    Rc::new(SoftwareTextMeasurer::from_fonts_or_default(fonts, 8192))
238}
239
240enum PresentBackend {
241    None,
242    Sync(Box<GpuRenderer>),
243    #[cfg(not(target_arch = "wasm32"))]
244    Threaded(PresentHandle),
245}
246
247/// What [`WgpuRenderer::publish_frame`] did.
248#[derive(Clone, Copy, Debug, PartialEq, Eq)]
249pub enum PublishOutcome {
250    /// No scene graph exists; nothing to lower.
251    NoGraph,
252    /// The depth-one slot is occupied (or the renderer is not in threaded
253    /// mode): NO packet was built — backpressure lands before lowering.
254    NoCredit,
255    /// A packet was built and handed to the present runtime.
256    Published,
257}
258
259/// WGPU-based renderer for GPU-accelerated 2D rendering.
260///
261/// This renderer supports:
262/// - GPU-accelerated shape rendering (rectangles, rounded rectangles)
263/// - Gradients (solid, linear, radial)
264/// - GPU text rendering via retained raster image batches
265/// - Cross-platform support (Desktop, Web, Android)
266pub struct WgpuRenderer {
267    frontend: RendererFrontend,
268    backend: PresentBackend,
269    renderer_epoch: u64,
270    surface_epoch: u64,
271}
272
273impl WgpuRenderer {
274    fn update_scene(
275        &mut self,
276        applier: &mut MemoryApplier,
277        root: NodeId,
278        dirty_nodes: &[NodeId],
279        refresh_hits: bool,
280    ) {
281        let mut changed_nodes = std::mem::take(&mut self.frontend.changed_nodes);
282        pipeline::update_from_applier(
283            applier,
284            root,
285            &mut self.frontend.scene,
286            1.0,
287            dirty_nodes,
288            refresh_hits,
289            &mut changed_nodes,
290        );
291        changed_nodes.clear();
292        self.frontend.changed_nodes = changed_nodes;
293    }
294
295    /// Create a new WGPU renderer.
296    ///
297    /// * `fonts` – font bytes to load, ordered by priority (first = highest priority).
298    ///   Pass `&[]` to load no fonts; text will not render until fonts are provided.
299    ///
300    /// Call [`init_gpu`][Self::init_gpu] before rendering.
301    pub fn new(fonts: &[&[u8]]) -> Self {
302        Self::with_text_system(WgpuTextSystem::from_fonts(fonts))
303    }
304
305    /// Create a renderer over an already-parsed font set.
306    ///
307    /// Measurement and rasterization both take clones of this one set, so an
308    /// app-supplied family resolves identically on both sides.
309    pub fn with_font_set(fonts: SoftwareTextFontSet) -> Self {
310        Self::with_text_system(WgpuTextSystem::from_font_set(fonts))
311    }
312
313    pub fn with_text_system(text_system: WgpuTextSystem) -> Self {
314        Self {
315            frontend: RendererFrontend::new(
316                text_system.render_state(),
317                text_system.software_fonts(),
318            ),
319            backend: PresentBackend::None,
320            renderer_epoch: 0,
321            surface_epoch: 0,
322        }
323    }
324
325    fn sync_gpu_renderer(&self) -> Option<&GpuRenderer> {
326        match &self.backend {
327            PresentBackend::Sync(gpu_renderer) => Some(gpu_renderer.as_ref()),
328            _ => None,
329        }
330    }
331
332    #[cfg(not(target_arch = "wasm32"))]
333    fn present_handle_mut(&mut self) -> Option<&mut PresentHandle> {
334        match &mut self.backend {
335            PresentBackend::Threaded(handle) => Some(handle),
336            _ => None,
337        }
338    }
339
340    fn retire_live_backend(&mut self) {
341        #[allow(unused_mut)]
342        let mut backend = std::mem::replace(&mut self.backend, PresentBackend::None);
343        #[cfg(not(target_arch = "wasm32"))]
344        if let PresentBackend::Threaded(handle) = &mut backend {
345            while let Some(returns) = handle.try_drain() {
346                self.frontend.apply_returns(returns);
347            }
348            handle.shutdown();
349        }
350        drop(backend);
351    }
352
353    /// Initialize GPU resources with a WGPU device and queue.
354    ///
355    /// Replacing a live renderer (Android surface recreation, device loss)
356    /// bumps the renderer epoch, so a packet built against the previous
357    /// renderer is cancelled instead of drawn.
358    pub fn init_gpu(
359        &mut self,
360        device: Arc<wgpu::Device>,
361        queue: Arc<wgpu::Queue>,
362        surface_format: wgpu::TextureFormat,
363        adapter_backend: wgpu::Backend,
364        adapter_downlevel: wgpu::DownlevelFlags,
365    ) {
366        self.retire_live_backend();
367        self.renderer_epoch = self.renderer_epoch.wrapping_add(1);
368        let gpu_renderer = GpuRenderer::new(
369            device,
370            queue,
371            surface_format,
372            adapter_backend,
373            adapter_downlevel,
374            self.frontend.text_fonts.clone(),
375            self.renderer_epoch,
376        );
377        self.backend = PresentBackend::Sync(Box::new(gpu_renderer));
378    }
379
380    /// [`init_gpu`][Self::init_gpu] for the threaded present runtime
381    /// (Android): the same epoch bump and planner replacement hygiene, but
382    /// instead of constructing a `GpuRenderer` here, everything it needs —
383    /// all owned, all `Send` — crosses to a spawned present thread that
384    /// constructs its own (its `Rc` caches are thread-confined). Frames
385    /// then flow through [`publish_frame`][Self::publish_frame] /
386    /// [`drain_present_returns`][Self::drain_present_returns] under the
387    /// depth-one credit protocol instead of [`render`][Self::render].
388    ///
389    /// * `waker` — wakes the producer's event loop after every returns
390    ///   send (the Android frame waker).
391    /// * `clock` — producer's monotonic nanosecond clock, so present-side
392    ///   [`PresentTimings`] share the producer telemetry's clock domain;
393    ///   `None` leaves timings at zero.
394    /// * `observer` — runs on the present thread around every present to the
395    ///   surface.
396    #[cfg(not(target_arch = "wasm32"))]
397    #[expect(clippy::too_many_arguments)]
398    pub fn init_gpu_threaded(
399        &mut self,
400        device: Arc<wgpu::Device>,
401        queue: Arc<wgpu::Queue>,
402        surface_format: wgpu::TextureFormat,
403        adapter_backend: wgpu::Backend,
404        adapter_downlevel: wgpu::DownlevelFlags,
405        waker: Arc<dyn Fn() + Send + Sync>,
406        clock: Option<Arc<dyn Fn() -> i64 + Send + Sync>>,
407        observer: Option<Box<dyn PresentObserver>>,
408    ) -> Result<(), WgpuRendererError> {
409        self.retire_live_backend();
410        self.renderer_epoch = self.renderer_epoch.wrapping_add(1);
411        let init = PresentRuntimeInit {
412            device,
413            queue,
414            surface_format,
415            adapter_backend,
416            adapter_downlevel,
417            text_fonts: self.frontend.text_fonts.clone(),
418            renderer_epoch: self.renderer_epoch,
419            clock,
420            observer,
421        };
422        let handle = PresentHandle::spawn(init, waker).map_err(WgpuRendererError::Wgpu)?;
423        self.backend = PresentBackend::Threaded(handle);
424        Ok(())
425    }
426
427    #[cfg(not(target_arch = "wasm32"))]
428    #[doc(hidden)]
429    pub fn init_gpu_inline_for_tests(
430        &mut self,
431        device: Arc<wgpu::Device>,
432        queue: Arc<wgpu::Queue>,
433        surface_format: wgpu::TextureFormat,
434        adapter_backend: wgpu::Backend,
435        adapter_downlevel: wgpu::DownlevelFlags,
436    ) -> InlinePresentRuntime {
437        self.retire_live_backend();
438        self.renderer_epoch = self.renderer_epoch.wrapping_add(1);
439        let init = PresentRuntimeInit {
440            device,
441            queue,
442            surface_format,
443            adapter_backend,
444            adapter_downlevel,
445            text_fonts: self.frontend.text_fonts.clone(),
446            renderer_epoch: self.renderer_epoch,
447            clock: None,
448            observer: None,
449        };
450        let (handle, state, msg_rx) = PresentHandle::new_inline(init, Arc::new(|| {}));
451        self.backend = PresentBackend::Threaded(handle);
452        InlinePresentRuntime {
453            state,
454            msg_rx,
455            shutdown_seen: false,
456        }
457    }
458
459    /// Record that the surface was reconfigured (resize, format change,
460    /// swapchain recreation): bumps the surface epoch stamped into every
461    /// subsequent packet, so a packet built against the previous
462    /// configuration is cancelled by the present stage instead of drawn.
463    pub fn note_surface_reconfigured(&mut self) {
464        self.surface_epoch = self.surface_epoch.wrapping_add(1);
465    }
466
467    /// Set root scale factor for text rendering (e.g., density scaling on Android)
468    pub fn set_root_scale(&mut self, scale: f32) {
469        self.frontend.root_scale = scale;
470    }
471
472    /// Clears each frame to nothing instead of the framework's background,
473    /// for a window whose surface composites with the desktop behind it. A
474    /// renderer starts opaque; `false` puts the background back. Takes
475    /// effect from the next frame, on either present backend.
476    pub fn set_transparent_background(&mut self, transparent: bool) {
477        self.frontend.transparent_background = transparent;
478    }
479
480    pub fn root_scale(&self) -> f32 {
481        self.frontend.root_scale
482    }
483
484    /// Render the scene to a texture view.
485    ///
486    /// Producer first, present second: the frontend collects the frame into
487    /// a `frame_packet::FramePacket` (root and dev overlay alike), the GPU
488    /// renderer consumes it, and the present stage's returns fold back into
489    /// the frontend afterwards.
490    pub fn render(
491        &mut self,
492        texture: &wgpu::Texture,
493        view: &wgpu::TextureView,
494        width: u32,
495        height: u32,
496    ) -> Result<(), WgpuRendererError> {
497        self.render_frame(texture, view, width, height)
498    }
499
500    /// Renders the frame into a presentable image. When the image carries the
501    /// composition format and the capture usages, the scene renders into it
502    /// directly and no output conversion pass runs.
503    pub fn render_surface_texture(
504        &mut self,
505        texture: &wgpu::Texture,
506        view: &wgpu::TextureView,
507        width: u32,
508        height: u32,
509    ) -> Result<(), WgpuRendererError> {
510        self.render_frame(texture, view, width, height)
511    }
512
513    /// Presents a surface image this renderer drew with
514    /// [`render_surface_texture`](Self::render_surface_texture), on the queue
515    /// that recorded it.
516    ///
517    /// Before [`init_gpu`](Self::init_gpu) there is no queue, and the image is
518    /// released without being shown.
519    pub fn present(&self, frame: wgpu::SurfaceTexture) {
520        match self.sync_gpu_renderer() {
521            Some(gpu_renderer) => gpu_renderer.queue.present(frame),
522            None => log::debug!("surface image released: the renderer has no GPU queue"),
523        }
524    }
525
526    fn render_frame(
527        &mut self,
528        texture: &wgpu::Texture,
529        view: &wgpu::TextureView,
530        width: u32,
531        height: u32,
532    ) -> Result<(), WgpuRendererError> {
533        let PresentBackend::Sync(gpu_renderer) = &mut self.backend else {
534            return Err(WgpuRendererError::Wgpu(
535                "GPU renderer not initialized for synchronous rendering. Call init_gpu() first."
536                    .to_string(),
537            ));
538        };
539        let packet = self
540            .frontend
541            .build_frame_packet(width, height, self.renderer_epoch, self.surface_epoch)
542            .ok_or_else(|| WgpuRendererError::Wgpu("scene graph is missing".to_string()))?;
543        let mut returns = RenderReturns::default();
544        let result = gpu_renderer.render(
545            texture,
546            view,
547            width,
548            height,
549            packet,
550            self.surface_epoch,
551            &mut returns,
552        );
553        self.frontend.apply_returns(returns);
554        result.map_err(WgpuRendererError::Wgpu)
555    }
556
557    /// Render the current scene into an RGBA pixel buffer for robot tests.
558    ///
559    /// Uses the renderer's configured root scale.
560    pub fn capture_frame(
561        &mut self,
562        width: u32,
563        height: u32,
564    ) -> Result<CapturedFrame, WgpuRendererError> {
565        self.capture_frame_with_scale(width, height, self.frontend.root_scale)
566    }
567
568    /// Render the current scene into an RGBA pixel buffer with an explicit scale.
569    pub fn capture_frame_with_scale(
570        &mut self,
571        width: u32,
572        height: u32,
573        root_scale: f32,
574    ) -> Result<CapturedFrame, WgpuRendererError> {
575        let PresentBackend::Sync(gpu_renderer) = &mut self.backend else {
576            return Err(WgpuRendererError::Wgpu(
577                "GPU renderer not initialized for synchronous rendering. Call init_gpu() first."
578                    .to_string(),
579            ));
580        };
581        let packet = self
582            .frontend
583            .build_frame_packet_with_scale(
584                width,
585                height,
586                root_scale,
587                self.renderer_epoch,
588                self.surface_epoch,
589            )
590            .ok_or_else(|| WgpuRendererError::Wgpu("scene graph is missing".to_string()))?;
591        let mut returns = RenderReturns::default();
592        let result = gpu_renderer.render_to_rgba_pixels(
593            width,
594            height,
595            packet,
596            self.surface_epoch,
597            &mut returns,
598        );
599        self.frontend.apply_returns(returns);
600        let pixels = result.map_err(WgpuRendererError::Wgpu)?;
601        Ok(CapturedFrame {
602            width,
603            height,
604            pixels,
605        })
606    }
607
608    /// Threaded mode: whether the depth-one slot has room for a packet.
609    /// The Android loop checks this BEFORE `shell.update()` so
610    /// backpressure lands before the expensive update/lowering work.
611    /// Always `true` on the sync path, which has no slot to fill.
612    #[cfg(not(target_arch = "wasm32"))]
613    pub fn has_frame_credit(&self) -> bool {
614        match &self.backend {
615            PresentBackend::Threaded(handle) => handle.has_credit(),
616            PresentBackend::Sync(_) | PresentBackend::None => true,
617        }
618    }
619
620    /// Threaded mode: lower the current scene into a packet and hand it to
621    /// the present runtime. Credit is checked FIRST — a `NoCredit` return
622    /// means no packet was built at all (`frame_sequence` does not
623    /// advance). Returns `NoCredit` (with an error log) when the renderer
624    /// is not in threaded mode.
625    #[cfg(not(target_arch = "wasm32"))]
626    pub fn publish_frame(&mut self, width: u32, height: u32) -> PublishOutcome {
627        let PresentBackend::Threaded(handle) = &mut self.backend else {
628            log::error!("publish_frame called without a threaded present runtime");
629            return PublishOutcome::NoCredit;
630        };
631        if !handle.has_credit() {
632            return PublishOutcome::NoCredit;
633        }
634        let Some(packet) = self.frontend.build_frame_packet(
635            width,
636            height,
637            self.renderer_epoch,
638            self.surface_epoch,
639        ) else {
640            return PublishOutcome::NoGraph;
641        };
642        match handle.publish(packet) {
643            Ok(()) => PublishOutcome::Published,
644            Err(packet) => {
645                let mut returns = RenderReturns::default();
646                let _ = GpuRenderer::cancel_packet(
647                    *packet,
648                    CancelReason::SurfaceUnavailable,
649                    &mut returns,
650                );
651                self.frontend.apply_returns(returns);
652                log::error!("present runtime unavailable; frame recovered, not published");
653                PublishOutcome::NoCredit
654            }
655        }
656    }
657
658    /// Threaded mode: fold every pending `RenderReturns` back into
659    /// producer state and free the publish credit. Returns how many were
660    /// drained. No-op outside threaded mode.
661    #[cfg(not(target_arch = "wasm32"))]
662    pub fn drain_present_returns(&mut self) -> usize {
663        self.drain_present_returns_with(&mut |_, _, _| {})
664    }
665
666    /// [`drain_present_returns`][Self::drain_present_returns], reporting
667    /// each drained frame's id, outcome and present-thread timings — the
668    /// Android loop feeds its frame telemetry from this.
669    #[cfg(not(target_arch = "wasm32"))]
670    pub fn drain_present_returns_with(
671        &mut self,
672        on_return: &mut dyn FnMut(u64, PresentOutcome, PresentTimings),
673    ) -> usize {
674        let mut drained = 0;
675        loop {
676            let returns = {
677                let PresentBackend::Threaded(handle) = &mut self.backend else {
678                    break;
679                };
680                match handle.try_drain() {
681                    Some(returns) => returns,
682                    None => break,
683                }
684            };
685            drained += 1;
686            let frame_id = returns.frame_id;
687            let outcome = returns.outcome;
688            let timings = returns.timings;
689            self.frontend.apply_returns(returns);
690            on_return(frame_id, outcome, timings);
691        }
692        drained
693    }
694
695    /// Threaded mode: install a (re)created surface on the present thread
696    /// and wait for the acknowledgement. The caller must have bumped the
697    /// surface epoch first (`note_surface_reconfigured`
698    /// [Self::note_surface_reconfigured]) when the message invalidates
699    /// in-flight packets; the message carries the current epoch.
700    #[cfg(not(target_arch = "wasm32"))]
701    pub fn present_replace_surface(
702        &mut self,
703        surface: wgpu::Surface<'static>,
704        config: wgpu::SurfaceConfiguration,
705    ) -> bool {
706        let surface_epoch = self.surface_epoch;
707        let Some(handle) = self.present_handle_mut() else {
708            log::error!("present_replace_surface called without a threaded present runtime");
709            return false;
710        };
711        handle.send_control_and_wait(
712            move |ack| PresentControl::ReplaceSurface {
713                surface,
714                config,
715                surface_epoch,
716                ack,
717            },
718            "replace surface",
719        )
720    }
721
722    /// Threaded mode: reconfigure the present thread's surface (resize)
723    /// and wait for the acknowledgement. Same epoch contract as
724    /// [`present_replace_surface`][Self::present_replace_surface].
725    #[cfg(not(target_arch = "wasm32"))]
726    pub fn present_reconfigure(&mut self, config: wgpu::SurfaceConfiguration) -> bool {
727        let surface_epoch = self.surface_epoch;
728        let Some(handle) = self.present_handle_mut() else {
729            log::error!("present_reconfigure called without a threaded present runtime");
730            return false;
731        };
732        handle.send_control_and_wait(
733            move |ack| PresentControl::Reconfigure {
734                config,
735                surface_epoch,
736                ack,
737            },
738            "reconfigure surface",
739        )
740    }
741
742    /// Threaded mode: drop the present thread's surface (the window died;
743    /// the renderer survives for the next one) and wait for the
744    /// acknowledgement. Bump the epoch first so in-flight packets cancel.
745    #[cfg(not(target_arch = "wasm32"))]
746    pub fn present_drop_surface(&mut self) -> bool {
747        let Some(handle) = self.present_handle_mut() else {
748            log::error!("present_drop_surface called without a threaded present runtime");
749            return false;
750        };
751        handle.send_control_and_wait(|ack| PresentControl::DropSurface { ack }, "drop surface")
752    }
753
754    /// Threaded mode: drain outstanding returns, stop the present thread
755    /// and join it. The renderer returns to the uninitialized state.
756    #[cfg(not(target_arch = "wasm32"))]
757    pub fn shutdown_present_runtime(&mut self) {
758        if matches!(self.backend, PresentBackend::Threaded(_)) {
759            self.retire_live_backend();
760        }
761    }
762
763    #[cfg(not(target_arch = "wasm32"))]
764    #[doc(hidden)]
765    pub fn present_attach_offscreen_for_tests(&mut self, width: u32, height: u32) -> bool {
766        let Some(handle) = self.present_handle_mut() else {
767            return false;
768        };
769        handle.send_control_and_wait(
770            move |ack| PresentControl::AttachOffscreenTargetForTests { width, height, ack },
771            "attach offscreen target",
772        )
773    }
774
775    #[cfg(not(target_arch = "wasm32"))]
776    #[doc(hidden)]
777    pub fn send_attach_offscreen_unacked_for_tests(
778        &mut self,
779        width: u32,
780        height: u32,
781    ) -> Option<std::sync::mpsc::Receiver<()>> {
782        let handle = self.present_handle_mut()?;
783        handle.send_control_unacked(move |ack| PresentControl::AttachOffscreenTargetForTests {
784            width,
785            height,
786            ack,
787        })
788    }
789
790    #[cfg(not(target_arch = "wasm32"))]
791    #[doc(hidden)]
792    pub fn send_reconfigure_unacked_for_tests(
793        &mut self,
794        config: wgpu::SurfaceConfiguration,
795    ) -> Option<std::sync::mpsc::Receiver<()>> {
796        let surface_epoch = self.surface_epoch;
797        let handle = self.present_handle_mut()?;
798        handle.send_control_unacked(move |ack| PresentControl::Reconfigure {
799            config,
800            surface_epoch,
801            ack,
802        })
803    }
804
805    #[cfg(not(target_arch = "wasm32"))]
806    #[doc(hidden)]
807    pub fn send_drop_surface_unacked_for_tests(&mut self) -> Option<std::sync::mpsc::Receiver<()>> {
808        let handle = self.present_handle_mut()?;
809        handle.send_control_unacked(|ack| PresentControl::DropSurface { ack })
810    }
811
812    /// The OS id of the thread that presents frames, once that thread has
813    /// started, on systems whose scheduler hints name threads by one.
814    /// `None` when frames present on the calling thread.
815    pub fn present_thread_id(&self) -> Option<i32> {
816        match &self.backend {
817            #[cfg(not(target_arch = "wasm32"))]
818            PresentBackend::Threaded(handle) => {
819                let id = handle
820                    .status()
821                    .thread_id
822                    .load(std::sync::atomic::Ordering::Relaxed);
823                (id != 0).then_some(id)
824            }
825            _ => None,
826        }
827    }
828
829    /// The producer's monotone packet sequence: the `frame_id` stamped on
830    /// the most recently lowered packet. After a `Published` outcome this
831    /// is the published frame's id (the Android loop keys its telemetry on
832    /// it); it also proves a `NoCredit` publish never lowered a frame.
833    pub fn last_published_frame_id(&self) -> u64 {
834        self.frontend.frame_sequence
835    }
836
837    #[cfg(not(target_arch = "wasm32"))]
838    #[doc(hidden)]
839    pub fn present_status_snapshot_for_tests(&self) -> Option<(bool, u64, u64)> {
840        match &self.backend {
841            PresentBackend::Threaded(handle) => {
842                let status = handle.status();
843                Some((
844                    status
845                        .needs_frame_warmup
846                        .load(std::sync::atomic::Ordering::Relaxed),
847                    status
848                        .presented_frames
849                        .load(std::sync::atomic::Ordering::Relaxed),
850                    status
851                        .placeholder_frames
852                        .load(std::sync::atomic::Ordering::Relaxed),
853                ))
854            }
855            _ => None,
856        }
857    }
858
859    pub fn last_frame_stats(&self) -> Option<RenderStatsSnapshot> {
860        match &self.backend {
861            PresentBackend::Sync(gpu_renderer) => gpu_renderer.last_frame_stats(),
862            #[cfg(not(target_arch = "wasm32"))]
863            PresentBackend::Threaded(handle) => *handle
864                .status()
865                .last_frame_stats
866                .lock()
867                .unwrap_or_else(std::sync::PoisonError::into_inner),
868            PresentBackend::None => None,
869        }
870    }
871
872    /// GPU milliseconds by pass label, aggregated since the last `[GPU-PASS]`
873    /// print. Empty unless `CRANPOSE_GPU_PASS_TIMING` armed pass timing on a
874    /// device with [`wgpu::Features::TIMESTAMP_QUERY`].
875    pub fn gpu_pass_timings(&self) -> GpuPassTimingReport {
876        self.sync_gpu_renderer()
877            .map(GpuRenderer::gpu_pass_timings)
878            .unwrap_or_default()
879    }
880
881    pub fn debug_cpu_allocation_stats(&self) -> DebugCpuAllocationStats {
882        let mut stats = self
883            .sync_gpu_renderer()
884            .map(GpuRenderer::debug_cpu_allocation_stats)
885            .unwrap_or_default();
886        stats.scene_graph_node_count = self
887            .frontend
888            .scene
889            .graph
890            .as_ref()
891            .map_or(0, RenderGraph::node_count);
892        stats.scene_graph_heap_bytes = self
893            .frontend
894            .scene
895            .graph
896            .as_ref()
897            .map_or(0, RenderGraph::heap_bytes);
898        stats.scene_hits_len = self.frontend.scene.hits.len();
899        stats.scene_hits_cap = self.frontend.scene.hits.capacity();
900        stats.scene_node_index_len = self.frontend.scene.node_index.len();
901        stats.scene_node_index_cap = self.frontend.scene.node_index.capacity();
902        stats
903    }
904
905    /// Return the WGPU device when GPU resources are initialized.
906    /// Sync backend only (desktop/web reconfigure paths); the threaded
907    /// runtime owns its device on the present thread.
908    pub fn try_device(&self) -> Option<&wgpu::Device> {
909        self.sync_gpu_renderer().map(|r| &*r.device)
910    }
911
912    #[doc(hidden)]
913    pub fn try_queue_for_tests(&self) -> Option<&wgpu::Queue> {
914        self.sync_gpu_renderer().map(|r| &*r.queue)
915    }
916
917    #[doc(hidden)]
918    pub fn device_error_count_for_tests(&self) -> u64 {
919        self.sync_gpu_renderer()
920            .map_or(0, GpuRenderer::device_error_count)
921    }
922
923    #[doc(hidden)]
924    pub fn build_frame_packet_for_tests(
925        &mut self,
926        width: u32,
927        height: u32,
928    ) -> Option<HeldFramePacket> {
929        self.frontend
930            .build_frame_packet(width, height, self.renderer_epoch, self.surface_epoch)
931            .map(HeldFramePacket)
932    }
933
934    #[doc(hidden)]
935    pub fn render_held_packet_for_tests(
936        &mut self,
937        texture: &wgpu::Texture,
938        view: &wgpu::TextureView,
939        width: u32,
940        height: u32,
941        packet: HeldFramePacket,
942    ) -> Result<PresentOutcome, WgpuRendererError> {
943        let PresentBackend::Sync(gpu_renderer) = &mut self.backend else {
944            return Err(WgpuRendererError::Wgpu(
945                "GPU renderer not initialized for synchronous rendering. Call init_gpu() first."
946                    .to_string(),
947            ));
948        };
949        let mut returns = RenderReturns::default();
950        let result = gpu_renderer.render(
951            texture,
952            view,
953            width,
954            height,
955            packet.0,
956            self.surface_epoch,
957            &mut returns,
958        );
959        let outcome = returns.outcome;
960        self.frontend.apply_returns(returns);
961        result.map_err(WgpuRendererError::Wgpu)?;
962        Ok(outcome)
963    }
964}
965
966#[doc(hidden)]
967pub struct HeldFramePacket(frame_packet::FramePacket);
968
969#[cfg(not(target_arch = "wasm32"))]
970#[doc(hidden)]
971pub struct InlinePresentRuntime {
972    state: PresentState,
973    msg_rx: std::sync::mpsc::Receiver<PresentMsg>,
974    shutdown_seen: bool,
975}
976
977#[cfg(not(target_arch = "wasm32"))]
978impl InlinePresentRuntime {
979    pub fn pump(&mut self) -> bool {
980        if self.shutdown_seen {
981            return false;
982        }
983        while let Ok(msg) = self.msg_rx.try_recv() {
984            if !self.state.run_once(msg) {
985                self.shutdown_seen = true;
986                return false;
987            }
988        }
989        self.state.consume_waiting();
990        true
991    }
992
993    pub fn has_waiting_packet(&self) -> bool {
994        self.state.has_waiting_packet()
995    }
996
997    pub fn step_one_message(&mut self) -> bool {
998        if self.shutdown_seen {
999            return false;
1000        }
1001        match self.msg_rx.try_recv() {
1002            Ok(msg) => {
1003                if !self.state.run_once(msg) {
1004                    self.shutdown_seen = true;
1005                }
1006                true
1007            }
1008            Err(_) => false,
1009        }
1010    }
1011
1012    pub fn consume_waiting(&mut self) {
1013        self.state.consume_waiting();
1014    }
1015}
1016
1017impl Default for WgpuRenderer {
1018    fn default() -> Self {
1019        Self::new(&[])
1020    }
1021}
1022
1023impl Renderer for WgpuRenderer {
1024    type Scene = Scene;
1025    type Error = WgpuRendererError;
1026
1027    fn attach_app_context_services(&mut self, app_context: &cranpose_ui::AppContext) {
1028        app_context.set_text_measurer(SoftwareTextMeasurer::from_font_set(
1029            self.frontend.text_fonts.clone(),
1030            8192,
1031        ));
1032        self.frontend.app_context = Some(app_context.downgrade());
1033    }
1034
1035    fn scene(&self) -> &Self::Scene {
1036        &self.frontend.scene
1037    }
1038
1039    fn scene_mut(&mut self) -> &mut Self::Scene {
1040        &mut self.frontend.scene
1041    }
1042
1043    fn rebuild_scene(
1044        &mut self,
1045        layout_tree: &LayoutTree,
1046        _viewport: Size,
1047    ) -> Result<(), Self::Error> {
1048        self.frontend.scene.clear();
1049        self.frontend.clear_fps_overlay();
1050        pipeline::render_layout_tree(layout_tree.root(), &mut self.frontend.scene);
1051        Ok(())
1052    }
1053
1054    fn rebuild_scene_from_applier(
1055        &mut self,
1056        applier: &mut MemoryApplier,
1057        root: NodeId,
1058        _viewport: Size,
1059    ) -> Result<(), Self::Error> {
1060        self.frontend.scene.clear();
1061        self.frontend.clear_fps_overlay();
1062        pipeline::render_from_applier(applier, root, &mut self.frontend.scene, 1.0);
1063        Ok(())
1064    }
1065
1066    fn update_scene_from_applier(
1067        &mut self,
1068        applier: &mut MemoryApplier,
1069        root: NodeId,
1070        viewport: Size,
1071        dirty_nodes: &[NodeId],
1072    ) -> Result<(), Self::Error> {
1073        if dirty_nodes.is_empty() {
1074            return self.rebuild_scene_from_applier(applier, root, viewport);
1075        }
1076        self.update_scene(applier, root, dirty_nodes, true);
1077        Ok(())
1078    }
1079
1080    fn update_visual_scene_from_applier(
1081        &mut self,
1082        applier: &mut MemoryApplier,
1083        root: NodeId,
1084        viewport: Size,
1085        dirty_nodes: &[NodeId],
1086    ) -> Result<(), Self::Error> {
1087        if dirty_nodes.is_empty() {
1088            return self.rebuild_scene_from_applier(applier, root, viewport);
1089        }
1090        self.update_scene(applier, root, dirty_nodes, false);
1091        Ok(())
1092    }
1093
1094    fn draw_dev_overlay(&mut self, text: &str, viewport: Size) {
1095        const DEV_OVERLAY_NODE_ID: NodeId = NodeId::MAX;
1096        let key = cranpose_render_common::dev_overlay::DevOverlayKey::new(text, viewport);
1097        if self.frontend.dev_overlay_graph.is_some()
1098            && self
1099                .frontend
1100                .dev_overlay_cache
1101                .as_ref()
1102                .is_some_and(|cache| {
1103                    cache.text == key.text
1104                        && cache.viewport_width_bits == key.viewport_width_bits
1105                        && cache.viewport_height_bits == key.viewport_height_bits
1106                })
1107        {
1108            return;
1109        }
1110        self.frontend.fps_overlay_graph = Some(
1111            cranpose_render_common::dev_overlay::build_dev_overlay_graph(
1112                text,
1113                viewport,
1114                DEV_OVERLAY_NODE_ID,
1115            ),
1116        );
1117        self.frontend.dev_overlay_cache = Some(DevOverlayCache {
1118            text: key.text,
1119            viewport_width_bits: key.viewport_width_bits,
1120            viewport_height_bits: key.viewport_height_bits,
1121        });
1122        self.frontend.refresh_dev_overlay();
1123    }
1124
1125    fn set_inspector_overlay(&mut self, graph: Option<cranpose_render_common::graph::RenderGraph>) {
1126        self.frontend.inspector_overlay_graph = graph;
1127        self.frontend.refresh_dev_overlay();
1128    }
1129
1130    fn needs_frame_warmup(&self) -> bool {
1131        match &self.backend {
1132            PresentBackend::Sync(gpu_renderer) => gpu_renderer.needs_frame_warmup(),
1133            #[cfg(not(target_arch = "wasm32"))]
1134            PresentBackend::Threaded(handle) => handle
1135                .status()
1136                .needs_frame_warmup
1137                .load(std::sync::atomic::Ordering::Relaxed),
1138            PresentBackend::None => false,
1139        }
1140    }
1141}
1142
1143#[cfg(test)]
1144#[path = "tests/wgpu_tests.rs"]
1145mod tests;