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