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