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