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