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