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