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