Skip to main content

cranpose_render_wgpu/
lib.rs

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