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