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