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    /// Clears each frame to nothing instead of the framework's background,
514    /// for a window whose surface composites with the desktop behind it. A
515    /// renderer starts opaque; `false` puts the background back. Takes
516    /// effect from the next frame, on either present backend.
517    pub fn set_transparent_background(&mut self, transparent: bool) {
518        self.frontend.transparent_background = transparent;
519    }
520
521    pub fn root_scale(&self) -> f32 {
522        self.frontend.root_scale
523    }
524
525    /// Render the scene to a texture view.
526    ///
527    /// Producer first, present second: the frontend collects the frame into
528    /// a `frame_packet::FramePacket` (root and dev overlay alike), the GPU
529    /// renderer consumes it, and the present stage's returns fold back into
530    /// the frontend afterwards.
531    pub fn render(
532        &mut self,
533        texture: &wgpu::Texture,
534        view: &wgpu::TextureView,
535        width: u32,
536        height: u32,
537    ) -> Result<(), WgpuRendererError> {
538        self.render_frame(texture, view, width, height)
539    }
540
541    /// Renders the frame into a presentable image. When the image carries the
542    /// composition format and the capture usages, the scene renders into it
543    /// directly and no output conversion pass runs.
544    pub fn render_surface_texture(
545        &mut self,
546        texture: &wgpu::Texture,
547        view: &wgpu::TextureView,
548        width: u32,
549        height: u32,
550    ) -> Result<(), WgpuRendererError> {
551        self.render_frame(texture, view, width, height)
552    }
553
554    /// Presents a surface image this renderer drew with
555    /// [`render_surface_texture`](Self::render_surface_texture), on the queue
556    /// that recorded it.
557    ///
558    /// Before [`init_gpu`](Self::init_gpu) there is no queue, and the image is
559    /// released without being shown.
560    pub fn present(&self, frame: wgpu::SurfaceTexture) {
561        match self.sync_gpu_renderer() {
562            Some(gpu_renderer) => gpu_renderer.queue.present(frame),
563            None => log::debug!("surface image released: the renderer has no GPU queue"),
564        }
565    }
566
567    fn render_frame(
568        &mut self,
569        texture: &wgpu::Texture,
570        view: &wgpu::TextureView,
571        width: u32,
572        height: u32,
573    ) -> Result<(), WgpuRendererError> {
574        let PresentBackend::Sync(gpu_renderer) = &mut self.backend else {
575            return Err(WgpuRendererError::Wgpu(
576                "GPU renderer not initialized for synchronous rendering. Call init_gpu() first."
577                    .to_string(),
578            ));
579        };
580        let packet = self
581            .frontend
582            .build_frame_packet(width, height, self.renderer_epoch, self.surface_epoch)
583            .ok_or_else(|| WgpuRendererError::Wgpu("scene graph is missing".to_string()))?;
584        let mut returns = RenderReturns::default();
585        let result = gpu_renderer.render(
586            texture,
587            view,
588            width,
589            height,
590            packet,
591            self.surface_epoch,
592            &mut returns,
593        );
594        self.frontend.apply_returns(returns);
595        result.map_err(WgpuRendererError::Wgpu)
596    }
597
598    /// Render the current scene into an RGBA pixel buffer for robot tests.
599    ///
600    /// Uses the renderer's configured root scale.
601    pub fn capture_frame(
602        &mut self,
603        width: u32,
604        height: u32,
605    ) -> Result<CapturedFrame, WgpuRendererError> {
606        self.capture_frame_with_scale(width, height, self.frontend.root_scale)
607    }
608
609    /// Render the current scene into an RGBA pixel buffer with an explicit scale.
610    /// Builds every pipeline the pipeline cache file records on the
611    /// background compiler, and returns whether they were all built before
612    /// `timeout`, or before `stop` said to stop. An app runs it after an
613    /// update, before its next launch, so that launch finds them compiled;
614    /// the renderer writes the file when it is dropped, and drops the builds
615    /// not started. It draws nothing, and needs a renderer initialized for
616    /// synchronous rendering.
617    #[cfg(not(target_arch = "wasm32"))]
618    pub fn prepare_recorded_pipelines(
619        &mut self,
620        timeout: std::time::Duration,
621        stop: &dyn Fn() -> bool,
622    ) -> bool {
623        let PresentBackend::Sync(gpu_renderer) = &mut self.backend else {
624            return false;
625        };
626        let deadline = web_time::Instant::now() + timeout;
627        let mut compiling = gpu_renderer.prepare_recorded();
628        while compiling {
629            if web_time::Instant::now() >= deadline || stop() {
630                return false;
631            }
632            std::thread::sleep(std::time::Duration::from_millis(10));
633            compiling = gpu_renderer.compiling();
634        }
635        true
636    }
637
638    pub fn capture_frame_with_scale(
639        &mut self,
640        width: u32,
641        height: u32,
642        root_scale: f32,
643    ) -> Result<CapturedFrame, WgpuRendererError> {
644        let PresentBackend::Sync(gpu_renderer) = &mut self.backend else {
645            return Err(WgpuRendererError::Wgpu(
646                "GPU renderer not initialized for synchronous rendering. Call init_gpu() first."
647                    .to_string(),
648            ));
649        };
650        let packet = self
651            .frontend
652            .build_frame_packet_with_scale(
653                width,
654                height,
655                root_scale,
656                self.renderer_epoch,
657                self.surface_epoch,
658            )
659            .ok_or_else(|| WgpuRendererError::Wgpu("scene graph is missing".to_string()))?;
660        let mut returns = RenderReturns::default();
661        let result = gpu_renderer.render_to_rgba_pixels(
662            width,
663            height,
664            packet,
665            self.surface_epoch,
666            &mut returns,
667        );
668        self.frontend.apply_returns(returns);
669        let pixels = result.map_err(WgpuRendererError::Wgpu)?;
670        Ok(CapturedFrame {
671            width,
672            height,
673            pixels,
674        })
675    }
676
677    /// Threaded mode: whether the depth-one slot has room for a packet.
678    /// The Android loop checks this BEFORE `shell.update()` so
679    /// backpressure lands before the expensive update/lowering work.
680    /// Always `true` on the sync path, which has no slot to fill.
681    #[cfg(not(target_arch = "wasm32"))]
682    pub fn has_frame_credit(&self) -> bool {
683        match &self.backend {
684            PresentBackend::Threaded(handle) => handle.has_credit(),
685            PresentBackend::Sync(_) | PresentBackend::None => true,
686        }
687    }
688
689    /// Threaded mode: whether the present thread has handed back a frame
690    /// that [`drain_present_returns`][Self::drain_present_returns] has not
691    /// folded back yet, so draining it would free a publish credit. A loop
692    /// out of credit can work on other things until this turns true.
693    /// Always `false` outside threaded mode.
694    #[cfg(not(target_arch = "wasm32"))]
695    pub fn frame_credit_returned(&self) -> bool {
696        match &self.backend {
697            PresentBackend::Threaded(handle) => handle.has_undrained_return(),
698            PresentBackend::Sync(_) | PresentBackend::None => false,
699        }
700    }
701
702    /// Threaded mode: lower the current scene into a packet and hand it to
703    /// the present runtime. Credit is checked FIRST — a `NoCredit` return
704    /// means no packet was built at all (`frame_sequence` does not
705    /// advance). Returns `NoCredit` (with an error log) when the renderer
706    /// is not in threaded mode.
707    #[cfg(not(target_arch = "wasm32"))]
708    pub fn publish_frame(&mut self, width: u32, height: u32) -> PublishOutcome {
709        let PresentBackend::Threaded(handle) = &mut self.backend else {
710            log::error!("publish_frame called without a threaded present runtime");
711            return PublishOutcome::NoCredit;
712        };
713        if !handle.has_credit() {
714            return PublishOutcome::NoCredit;
715        }
716        let Some(packet) = self.frontend.build_frame_packet(
717            width,
718            height,
719            self.renderer_epoch,
720            self.surface_epoch,
721        ) else {
722            return PublishOutcome::NoGraph;
723        };
724        match handle.publish(packet) {
725            Ok(()) => PublishOutcome::Published,
726            Err(packet) => {
727                let mut returns = RenderReturns::default();
728                let _ = GpuRenderer::cancel_packet(
729                    *packet,
730                    CancelReason::SurfaceUnavailable,
731                    &mut returns,
732                );
733                self.frontend.apply_returns(returns);
734                log::error!("present runtime unavailable; frame recovered, not published");
735                PublishOutcome::NoCredit
736            }
737        }
738    }
739
740    /// Threaded mode: fold every pending `RenderReturns` back into
741    /// producer state and free the publish credit. Returns how many were
742    /// drained. No-op outside threaded mode.
743    #[cfg(not(target_arch = "wasm32"))]
744    pub fn drain_present_returns(&mut self) -> usize {
745        self.drain_present_returns_with(&mut |_, _, _| {})
746    }
747
748    /// [`drain_present_returns`][Self::drain_present_returns], reporting
749    /// each drained frame's id, outcome and present-thread timings — the
750    /// Android loop feeds its frame telemetry from this.
751    #[cfg(not(target_arch = "wasm32"))]
752    pub fn drain_present_returns_with(
753        &mut self,
754        on_return: &mut dyn FnMut(u64, PresentOutcome, PresentTimings),
755    ) -> usize {
756        let mut drained = 0;
757        loop {
758            let returns = {
759                let PresentBackend::Threaded(handle) = &mut self.backend else {
760                    break;
761                };
762                match handle.try_drain() {
763                    Some(returns) => returns,
764                    None => break,
765                }
766            };
767            drained += 1;
768            let frame_id = returns.frame_id;
769            let outcome = returns.outcome;
770            let timings = returns.timings;
771            self.frontend.apply_returns(returns);
772            on_return(frame_id, outcome, timings);
773        }
774        drained
775    }
776
777    /// Threaded mode: install a (re)created surface on the present thread.
778    /// Nothing waits for the present thread to set it up: frames published
779    /// afterwards queue behind it, so the caller composes its next frame
780    /// while the surface is configured. The caller must have bumped the
781    /// surface epoch first (`note_surface_reconfigured`
782    /// [Self::note_surface_reconfigured]) when the message invalidates
783    /// in-flight packets; the message carries the current epoch. Returns
784    /// false when the present thread is gone.
785    #[cfg(not(target_arch = "wasm32"))]
786    pub fn present_replace_surface(
787        &mut self,
788        surface: wgpu::Surface<'static>,
789        config: wgpu::SurfaceConfiguration,
790    ) -> bool {
791        let surface_epoch = self.surface_epoch;
792        let Some(handle) = self.present_handle_mut() else {
793            log::error!("present_replace_surface called without a threaded present runtime");
794            return false;
795        };
796        let sent = handle.send_control(PresentControl::ReplaceSurface {
797            surface,
798            config,
799            surface_epoch,
800        });
801        if !sent {
802            log::error!("[present-runtime] replace surface: runtime is gone");
803        }
804        sent
805    }
806
807    /// Threaded mode: reconfigure the present thread's surface (resize)
808    /// and wait for the acknowledgement. Same epoch contract as
809    /// [`present_replace_surface`][Self::present_replace_surface].
810    #[cfg(not(target_arch = "wasm32"))]
811    pub fn present_reconfigure(&mut self, config: wgpu::SurfaceConfiguration) -> bool {
812        let surface_epoch = self.surface_epoch;
813        let Some(handle) = self.present_handle_mut() else {
814            log::error!("present_reconfigure called without a threaded present runtime");
815            return false;
816        };
817        handle.send_control_and_wait(
818            move |ack| PresentControl::Reconfigure {
819                config,
820                surface_epoch,
821                ack,
822            },
823            "reconfigure surface",
824        )
825    }
826
827    /// Threaded mode: drop the present thread's surface (the window died;
828    /// the renderer survives for the next one) and wait for the
829    /// acknowledgement. Bump the epoch first so in-flight packets cancel.
830    #[cfg(not(target_arch = "wasm32"))]
831    pub fn present_drop_surface(&mut self) -> bool {
832        let Some(handle) = self.present_handle_mut() else {
833            log::error!("present_drop_surface called without a threaded present runtime");
834            return false;
835        };
836        handle.send_control_and_wait(|ack| PresentControl::DropSurface { ack }, "drop surface")
837    }
838
839    /// Threaded mode: drain outstanding returns, stop the present thread
840    /// and join it. The renderer returns to the uninitialized state.
841    #[cfg(not(target_arch = "wasm32"))]
842    pub fn shutdown_present_runtime(&mut self) {
843        if matches!(self.backend, PresentBackend::Threaded(_)) {
844            self.retire_live_backend();
845        }
846    }
847
848    #[cfg(not(target_arch = "wasm32"))]
849    #[doc(hidden)]
850    pub fn present_attach_offscreen_for_tests(&mut self, width: u32, height: u32) -> bool {
851        let Some(handle) = self.present_handle_mut() else {
852            return false;
853        };
854        handle.send_control_and_wait(
855            move |ack| PresentControl::AttachOffscreenTargetForTests { width, height, ack },
856            "attach offscreen target",
857        )
858    }
859
860    #[cfg(not(target_arch = "wasm32"))]
861    #[doc(hidden)]
862    pub fn send_attach_offscreen_unacked_for_tests(
863        &mut self,
864        width: u32,
865        height: u32,
866    ) -> Option<std::sync::mpsc::Receiver<()>> {
867        let handle = self.present_handle_mut()?;
868        handle.send_control_unacked(move |ack| PresentControl::AttachOffscreenTargetForTests {
869            width,
870            height,
871            ack,
872        })
873    }
874
875    #[cfg(not(target_arch = "wasm32"))]
876    #[doc(hidden)]
877    pub fn send_reconfigure_unacked_for_tests(
878        &mut self,
879        config: wgpu::SurfaceConfiguration,
880    ) -> Option<std::sync::mpsc::Receiver<()>> {
881        let surface_epoch = self.surface_epoch;
882        let handle = self.present_handle_mut()?;
883        handle.send_control_unacked(move |ack| PresentControl::Reconfigure {
884            config,
885            surface_epoch,
886            ack,
887        })
888    }
889
890    #[cfg(not(target_arch = "wasm32"))]
891    #[doc(hidden)]
892    pub fn send_drop_surface_unacked_for_tests(&mut self) -> Option<std::sync::mpsc::Receiver<()>> {
893        let handle = self.present_handle_mut()?;
894        handle.send_control_unacked(|ack| PresentControl::DropSurface { ack })
895    }
896
897    /// The OS id of the thread that presents frames, once that thread has
898    /// started, on systems whose scheduler hints name threads by one.
899    /// `None` when frames present on the calling thread.
900    pub fn present_thread_id(&self) -> Option<i32> {
901        match &self.backend {
902            #[cfg(not(target_arch = "wasm32"))]
903            PresentBackend::Threaded(handle) => {
904                let id = handle
905                    .status()
906                    .thread_id
907                    .load(std::sync::atomic::Ordering::Relaxed);
908                (id != 0).then_some(id)
909            }
910            _ => None,
911        }
912    }
913
914    /// The producer's monotone packet sequence: the `frame_id` stamped on
915    /// the most recently lowered packet. After a `Published` outcome this
916    /// is the published frame's id (the Android loop keys its telemetry on
917    /// it); it also proves a `NoCredit` publish never lowered a frame.
918    pub fn last_published_frame_id(&self) -> u64 {
919        self.frontend.frame_sequence
920    }
921
922    #[cfg(not(target_arch = "wasm32"))]
923    #[doc(hidden)]
924    pub fn present_status_snapshot_for_tests(&self) -> Option<(bool, u64, u64)> {
925        match &self.backend {
926            PresentBackend::Threaded(handle) => {
927                let status = handle.status();
928                Some((
929                    status
930                        .needs_frame_warmup
931                        .load(std::sync::atomic::Ordering::Relaxed),
932                    status
933                        .presented_frames
934                        .load(std::sync::atomic::Ordering::Relaxed),
935                    status
936                        .placeholder_frames
937                        .load(std::sync::atomic::Ordering::Relaxed),
938                ))
939            }
940            _ => None,
941        }
942    }
943
944    pub fn last_frame_stats(&self) -> Option<RenderStatsSnapshot> {
945        match &self.backend {
946            PresentBackend::Sync(gpu_renderer) => gpu_renderer.last_frame_stats(),
947            #[cfg(not(target_arch = "wasm32"))]
948            PresentBackend::Threaded(handle) => *handle
949                .status()
950                .last_frame_stats
951                .lock()
952                .unwrap_or_else(std::sync::PoisonError::into_inner),
953            PresentBackend::None => None,
954        }
955    }
956
957    /// GPU milliseconds by pass label, aggregated since the last `[GPU-PASS]`
958    /// print. Empty unless `CRANPOSE_GPU_PASS_TIMING` armed pass timing on a
959    /// device with [`wgpu::Features::TIMESTAMP_QUERY`].
960    pub fn gpu_pass_timings(&self) -> GpuPassTimingReport {
961        self.sync_gpu_renderer()
962            .map(GpuRenderer::gpu_pass_timings)
963            .unwrap_or_default()
964    }
965
966    pub fn debug_cpu_allocation_stats(&self) -> DebugCpuAllocationStats {
967        let mut stats = self
968            .sync_gpu_renderer()
969            .map(GpuRenderer::debug_cpu_allocation_stats)
970            .unwrap_or_default();
971        stats.scene_graph_node_count = self
972            .frontend
973            .scene
974            .graph
975            .as_ref()
976            .map_or(0, RenderGraph::node_count);
977        stats.scene_graph_heap_bytes = self
978            .frontend
979            .scene
980            .graph
981            .as_ref()
982            .map_or(0, RenderGraph::heap_bytes);
983        stats.scene_hits_len = self.frontend.scene.hits.len();
984        stats.scene_hits_cap = self.frontend.scene.hits.capacity();
985        stats.scene_node_index_len = self.frontend.scene.node_index.len();
986        stats.scene_node_index_cap = self.frontend.scene.node_index.capacity();
987        stats
988    }
989
990    /// Return the WGPU device when GPU resources are initialized.
991    /// Sync backend only (desktop/web reconfigure paths); the threaded
992    /// runtime owns its device on the present thread.
993    pub fn try_device(&self) -> Option<&wgpu::Device> {
994        self.sync_gpu_renderer().map(|r| &*r.device)
995    }
996
997    #[doc(hidden)]
998    pub fn try_queue_for_tests(&self) -> Option<&wgpu::Queue> {
999        self.sync_gpu_renderer().map(|r| &*r.queue)
1000    }
1001
1002    #[doc(hidden)]
1003    pub fn device_error_count_for_tests(&self) -> u64 {
1004        self.sync_gpu_renderer()
1005            .map_or(0, GpuRenderer::device_error_count)
1006    }
1007
1008    #[doc(hidden)]
1009    pub fn build_frame_packet_for_tests(
1010        &mut self,
1011        width: u32,
1012        height: u32,
1013    ) -> Option<HeldFramePacket> {
1014        self.frontend
1015            .build_frame_packet(width, height, self.renderer_epoch, self.surface_epoch)
1016            .map(HeldFramePacket)
1017    }
1018
1019    /// Return a producer-built packet without presenting it. This exercises
1020    /// the same bounded scene-storage recycler used by normal present returns
1021    /// in offline producer benchmarks.
1022    ///
1023    /// ```
1024    /// use cranpose_render_wgpu::{HeldFramePacket, WgpuRenderer};
1025    ///
1026    /// fn return_without_presenting(renderer: &mut WgpuRenderer, packet: HeldFramePacket) {
1027    ///     renderer.return_held_packet_for_tests(packet);
1028    /// }
1029    /// ```
1030    #[doc(hidden)]
1031    pub fn return_held_packet_for_tests(&mut self, packet: HeldFramePacket) {
1032        let packet = packet.0;
1033        self.frontend.apply_returns(RenderReturns {
1034            scene: Some(frame_packet::FrameSceneStorage {
1035                root: packet.root,
1036                overlay: packet.overlay,
1037            }),
1038            frame_id: packet.frame_id,
1039            ..RenderReturns::default()
1040        });
1041    }
1042
1043    #[doc(hidden)]
1044    pub fn render_held_packet_for_tests(
1045        &mut self,
1046        texture: &wgpu::Texture,
1047        view: &wgpu::TextureView,
1048        width: u32,
1049        height: u32,
1050        packet: HeldFramePacket,
1051    ) -> Result<PresentOutcome, WgpuRendererError> {
1052        let PresentBackend::Sync(gpu_renderer) = &mut self.backend else {
1053            return Err(WgpuRendererError::Wgpu(
1054                "GPU renderer not initialized for synchronous rendering. Call init_gpu() first."
1055                    .to_string(),
1056            ));
1057        };
1058        let mut returns = RenderReturns::default();
1059        let result = gpu_renderer.render(
1060            texture,
1061            view,
1062            width,
1063            height,
1064            packet.0,
1065            self.surface_epoch,
1066            &mut returns,
1067        );
1068        let outcome = returns.outcome;
1069        self.frontend.apply_returns(returns);
1070        result.map_err(WgpuRendererError::Wgpu)?;
1071        Ok(outcome)
1072    }
1073}
1074
1075#[doc(hidden)]
1076pub struct HeldFramePacket(frame_packet::FramePacket);
1077
1078#[cfg(not(target_arch = "wasm32"))]
1079#[doc(hidden)]
1080pub struct InlinePresentRuntime {
1081    state: PresentState,
1082    msg_rx: std::sync::mpsc::Receiver<PresentMsg>,
1083    shutdown_seen: bool,
1084}
1085
1086#[cfg(not(target_arch = "wasm32"))]
1087impl InlinePresentRuntime {
1088    pub fn pump(&mut self) -> bool {
1089        if self.shutdown_seen {
1090            return false;
1091        }
1092        while let Ok(msg) = self.msg_rx.try_recv() {
1093            if !self.state.run_once(msg) {
1094                self.shutdown_seen = true;
1095                return false;
1096            }
1097        }
1098        self.state.consume_waiting();
1099        true
1100    }
1101
1102    pub fn has_waiting_packet(&self) -> bool {
1103        self.state.has_waiting_packet()
1104    }
1105
1106    pub fn step_one_message(&mut self) -> bool {
1107        if self.shutdown_seen {
1108            return false;
1109        }
1110        match self.msg_rx.try_recv() {
1111            Ok(msg) => {
1112                if !self.state.run_once(msg) {
1113                    self.shutdown_seen = true;
1114                }
1115                true
1116            }
1117            Err(_) => false,
1118        }
1119    }
1120
1121    pub fn consume_waiting(&mut self) {
1122        self.state.consume_waiting();
1123    }
1124}
1125
1126impl Default for WgpuRenderer {
1127    fn default() -> Self {
1128        Self::new(&[])
1129    }
1130}
1131
1132impl Renderer for WgpuRenderer {
1133    type Scene = Scene;
1134    type Error = WgpuRendererError;
1135
1136    fn attach_app_context_services(&mut self, app_context: &cranpose_ui::AppContext) {
1137        app_context.set_text_measurer(SoftwareTextMeasurer::from_font_set(
1138            self.frontend.text_fonts.clone(),
1139            8192,
1140        ));
1141        self.frontend.app_context = Some(app_context.downgrade());
1142    }
1143
1144    fn set_root_scale(&mut self, scale: f32) {
1145        self.frontend.root_scale = scale;
1146    }
1147
1148    fn scene(&self) -> &Self::Scene {
1149        &self.frontend.scene
1150    }
1151
1152    fn scene_mut(&mut self) -> &mut Self::Scene {
1153        &mut self.frontend.scene
1154    }
1155
1156    fn rebuild_scene(
1157        &mut self,
1158        layout_tree: &LayoutTree,
1159        _viewport: Size,
1160    ) -> Result<(), Self::Error> {
1161        self.frontend.scene.clear();
1162        self.frontend.clear_fps_overlay();
1163        pipeline::render_layout_tree(layout_tree.root(), &mut self.frontend.scene);
1164        Ok(())
1165    }
1166
1167    fn rebuild_scene_from_applier(
1168        &mut self,
1169        applier: &mut MemoryApplier,
1170        root: NodeId,
1171        _viewport: Size,
1172    ) -> Result<(), Self::Error> {
1173        self.frontend.clear_fps_overlay();
1174        self.frontend.scene.rebuild_from_applier(applier, root);
1175        Ok(())
1176    }
1177
1178    fn update_scene_from_applier(
1179        &mut self,
1180        applier: &mut MemoryApplier,
1181        root: NodeId,
1182        _viewport: Size,
1183        updates: cranpose_render_common::SceneUpdates<'_>,
1184    ) -> Result<(), Self::Error> {
1185        if updates.is_empty() {
1186            self.frontend.clear_fps_overlay();
1187        }
1188        self.frontend
1189            .scene
1190            .update_from_applier(applier, root, updates, true);
1191        Ok(())
1192    }
1193
1194    fn update_visual_scene_from_applier(
1195        &mut self,
1196        applier: &mut MemoryApplier,
1197        root: NodeId,
1198        _viewport: Size,
1199        updates: cranpose_render_common::SceneUpdates<'_>,
1200    ) -> Result<(), Self::Error> {
1201        if updates.is_empty() {
1202            self.frontend.clear_fps_overlay();
1203        }
1204        self.frontend
1205            .scene
1206            .update_from_applier(applier, root, updates, false);
1207        Ok(())
1208    }
1209
1210    fn draw_dev_overlay(&mut self, text: &str, viewport: Size) {
1211        const DEV_OVERLAY_NODE_ID: NodeId = NodeId::MAX;
1212        let key = cranpose_render_common::dev_overlay::DevOverlayKey::new(text, viewport);
1213        if self.frontend.dev_overlay_graph.is_some()
1214            && self
1215                .frontend
1216                .dev_overlay_cache
1217                .as_ref()
1218                .is_some_and(|cache| {
1219                    cache.text == key.text
1220                        && cache.viewport_width_bits == key.viewport_width_bits
1221                        && cache.viewport_height_bits == key.viewport_height_bits
1222                })
1223        {
1224            return;
1225        }
1226        self.frontend.fps_overlay_graph = Some(
1227            cranpose_render_common::dev_overlay::build_dev_overlay_graph(
1228                text,
1229                viewport,
1230                DEV_OVERLAY_NODE_ID,
1231            ),
1232        );
1233        self.frontend.dev_overlay_cache = Some(DevOverlayCache {
1234            text: key.text,
1235            viewport_width_bits: key.viewport_width_bits,
1236            viewport_height_bits: key.viewport_height_bits,
1237        });
1238        self.frontend.refresh_dev_overlay();
1239    }
1240
1241    fn set_inspector_overlay(&mut self, graph: Option<cranpose_render_common::graph::RenderGraph>) {
1242        self.frontend.inspector_overlay_graph = graph;
1243        self.frontend.refresh_dev_overlay();
1244    }
1245
1246    fn needs_frame_warmup(&self) -> bool {
1247        match &self.backend {
1248            PresentBackend::Sync(gpu_renderer) => gpu_renderer.needs_frame_warmup(),
1249            #[cfg(not(target_arch = "wasm32"))]
1250            PresentBackend::Threaded(handle) => handle
1251                .status()
1252                .needs_frame_warmup
1253                .load(std::sync::atomic::Ordering::Relaxed),
1254            PresentBackend::None => false,
1255        }
1256    }
1257
1258    fn awaits_pipelines(&self) -> bool {
1259        match &self.backend {
1260            PresentBackend::Sync(gpu_renderer) => gpu_renderer.awaits_pipelines(),
1261            #[cfg(not(target_arch = "wasm32"))]
1262            PresentBackend::Threaded(handle) => {
1263                let status = handle.status();
1264                status
1265                    .drew_placeholder
1266                    .load(std::sync::atomic::Ordering::Relaxed)
1267                    && status
1268                        .landing
1269                        .get()
1270                        .is_some_and(|landing| landing.in_flight())
1271            }
1272            PresentBackend::None => false,
1273        }
1274    }
1275}
1276
1277#[cfg(test)]
1278#[path = "tests/wgpu_tests.rs"]
1279mod tests;