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