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