Skip to main content

cranpose_render_wgpu/
lib.rs

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