1pub(crate) use cranpose_render_common::debug_toggles;
7pub use debug_toggles::{
8 DebugToggle, debug_toggle, debug_toggle_os, set_debug_toggle, set_debug_toggle_os,
9};
10pub use offscreen::composition_bytes_per_pixel;
11pub use render::presentable_root_usages;
12mod ablation;
13mod capture_hash;
14mod collect;
15mod draw_pass;
16mod effect_renderer;
17mod fast_cores;
18mod frame;
19mod geometry;
20mod idle_pool;
21mod layer_cache;
22pub use fast_cores::pin_current_thread_to_fast_cores;
23mod frame_graph;
24mod frame_packet;
25mod frontend;
26mod glass_split;
27mod glyph_run_arena;
28pub(crate) mod gpu_stats;
29mod initial_present;
30mod lazy_resource;
31mod offscreen;
32mod opaque_prefix;
33mod output_conversion;
34pub(crate) mod pass_timing;
35mod pipeline;
36mod pipeline_compiler;
37#[cfg(not(target_arch = "wasm32"))]
38pub mod pipeline_disk_cache;
39#[cfg(not(target_arch = "wasm32"))]
40mod present_runtime;
41mod record_columns;
42mod render;
43mod run_geometry;
44mod run_store;
45mod scene;
46mod shader_cache;
47mod shaders;
48mod shape_pipelines;
49#[cfg(test)]
50mod test_support;
51
52use std::{rc::Rc, sync::Arc};
53
54use cranpose_core::{MemoryApplier, NodeId};
55use cranpose_render_common::{
56 RenderScene, Renderer,
57 graph::RenderGraph,
58 software_text_raster::{
59 SoftwareTextFontSet, SoftwareTextMeasurer, software_text_font_set_from_fonts_or_default,
60 },
61};
62use cranpose_ui::{LayoutTree, TextMeasurer};
63use cranpose_ui_graphics::{Rect, Size};
64pub use frame_packet::PresentTimings;
65
66#[cfg(not(target_arch = "wasm32"))]
70pub trait PresentObserver: Send {
71 fn before_present(&mut self, surface: &wgpu::Surface<'static>);
73 fn after_present(&mut self, surface: &wgpu::Surface<'static>);
75
76 fn before_reconfigure(&mut self, _surface: &wgpu::Surface<'static>) {}
79}
80use frame_packet::RenderReturns;
81#[doc(hidden)]
82pub use frame_packet::{CancelReason, PresentOutcome};
83use frontend::{DevOverlayCache, RendererFrontend};
84pub use gpu_stats::FrameStatsSnapshot as RenderStatsSnapshot;
85pub use initial_present::{clear_to_background, clear_to_default_background};
86pub use pass_timing::{GpuPassTimingEntry, GpuPassTimingReport};
87#[cfg(not(target_arch = "wasm32"))]
88use present_runtime::{
89 PresentControl, PresentHandle, PresentMsg, PresentRuntimeInit, PresentState,
90};
91use render::GpuRenderer;
92pub use render::{
93 frame_clear_color, frames_presented, pipelines_created, pipelines_created_off_frame,
94};
95pub use scene::{HitRegion, Scene};
96
97pub fn optional_device_features(adapter: &wgpu::Adapter) -> wgpu::Features {
104 adapter.features() & (wgpu::Features::PIPELINE_CACHE | wgpu::Features::TIMESTAMP_QUERY)
105}
106
107#[doc(hidden)]
108pub fn offscreen_render_target_for_tests(
109 device: &wgpu::Device,
110 width: u32,
111 height: u32,
112 label: &str,
113) -> (wgpu::Texture, wgpu::TextureView) {
114 let texture = offscreen::create_2d_texture(
115 device,
116 wgpu::TextureFormat::Rgba8Unorm,
117 width,
118 height,
119 wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
120 Some(label),
121 );
122 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
123 (texture, view)
124}
125
126pub(crate) fn rect_to_quad(rect: Rect) -> [[f32; 2]; 4] {
127 [
128 [rect.x, rect.y],
129 [rect.x + rect.width, rect.y],
130 [rect.x, rect.y + rect.height],
131 [rect.x + rect.width, rect.y + rect.height],
132 ]
133}
134
135#[derive(Debug)]
136pub enum WgpuRendererError {
137 Layout(String),
138 Wgpu(String),
139}
140
141#[derive(Debug, Clone)]
143pub struct CapturedFrame {
144 pub width: u32,
145 pub height: u32,
146 pub pixels: Vec<u8>,
147}
148
149#[doc(hidden)]
150#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
151pub struct DebugCpuAllocationStats {
152 pub scene_graph_node_count: usize,
153 pub scene_graph_heap_bytes: usize,
154 pub scene_hits_len: usize,
155 pub scene_hits_cap: usize,
156 pub scene_node_index_len: usize,
157 pub scene_node_index_cap: usize,
158 pub text_renderer_pool_len: usize,
159 pub text_renderer_pool_cap: usize,
160 pub image_texture_cache_len: usize,
161 pub image_texture_cache_cap: usize,
162 pub run_arena_staging_bytes: usize,
163 pub run_store_bytes: usize,
164 pub run_store_runs: usize,
165 pub scratch_image_vertices_cap: usize,
166 pub scratch_image_indices_cap: usize,
167 pub scratch_image_cmds_cap: usize,
168 pub scratch_glyph_instances_cap: usize,
169 pub layer_cache_len: usize,
170 pub layer_cache_bytes: u64,
171}
172
173pub(crate) struct TextSystemState {
174 measurer: SoftwareTextMeasurer,
175}
176
177impl TextSystemState {
178 fn from_font_set(fonts: SoftwareTextFontSet) -> Self {
179 Self {
180 measurer: SoftwareTextMeasurer::from_font_set(fonts, 8192),
181 }
182 }
183
184 pub(crate) fn text_cache_len(&self) -> usize {
185 0
186 }
187}
188
189impl pipeline::TextLayoutResolver for TextSystemState {
190 fn layout_text(
191 &mut self,
192 text: &cranpose_ui::text::AnnotatedString,
193 style: &cranpose_ui::text::TextStyle,
194 ) -> cranpose_ui::text_layout_result::TextLayoutResult {
195 if cranpose_ui::has_current_app_context() {
196 cranpose_ui::text::layout_text(text, style)
197 } else {
198 self.measurer.layout(text, style)
199 }
200 }
201}
202
203#[derive(Clone)]
204pub struct WgpuTextSystem {
205 software_fonts: SoftwareTextFontSet,
206}
207
208impl WgpuTextSystem {
209 pub fn from_fonts(fonts: &[&'static [u8]]) -> Self {
210 Self {
211 software_fonts: software_text_font_set_from_fonts_or_default(fonts),
212 }
213 }
214
215 pub fn from_font_set(software_fonts: SoftwareTextFontSet) -> Self {
219 Self { software_fonts }
220 }
221
222 pub(crate) fn render_state(&self) -> TextSystemState {
223 TextSystemState::from_font_set(self.software_fonts.clone())
224 }
225
226 pub(crate) fn software_fonts(&self) -> SoftwareTextFontSet {
227 self.software_fonts.clone()
228 }
229}
230
231pub fn headless_text_measurer() -> Rc<dyn TextMeasurer> {
233 headless_text_measurer_with_fonts(&[])
234}
235
236pub fn headless_text_measurer_with_fonts(fonts: &[&'static [u8]]) -> Rc<dyn TextMeasurer> {
238 Rc::new(SoftwareTextMeasurer::from_fonts_or_default(fonts, 8192))
239}
240
241enum PresentBackend {
242 None,
243 Sync(Box<GpuRenderer>),
244 #[cfg(not(target_arch = "wasm32"))]
245 Threaded(PresentHandle),
246}
247
248#[derive(Clone, Copy, Debug, PartialEq, Eq)]
250pub enum PublishOutcome {
251 NoGraph,
253 NoCredit,
256 Published,
258}
259
260pub struct WgpuRenderer {
268 frontend: RendererFrontend,
269 backend: PresentBackend,
270 renderer_epoch: u64,
271 surface_epoch: u64,
272}
273
274impl WgpuRenderer {
275 fn update_scene(
276 &mut self,
277 applier: &mut MemoryApplier,
278 root: NodeId,
279 dirty_nodes: &[NodeId],
280 refresh_hits: bool,
281 ) {
282 let mut changed_nodes = std::mem::take(&mut self.frontend.changed_nodes);
283 pipeline::update_from_applier(
284 applier,
285 root,
286 &mut self.frontend.scene,
287 1.0,
288 dirty_nodes,
289 refresh_hits,
290 &mut changed_nodes,
291 );
292 changed_nodes.clear();
293 self.frontend.changed_nodes = changed_nodes;
294 }
295
296 pub fn new(fonts: &[&'static [u8]]) -> Self {
303 Self::with_text_system(WgpuTextSystem::from_fonts(fonts))
304 }
305
306 pub fn with_font_set(fonts: SoftwareTextFontSet) -> Self {
311 Self::with_text_system(WgpuTextSystem::from_font_set(fonts))
312 }
313
314 pub fn with_text_system(text_system: WgpuTextSystem) -> Self {
315 Self {
316 frontend: RendererFrontend::new(
317 text_system.render_state(),
318 text_system.software_fonts(),
319 ),
320 backend: PresentBackend::None,
321 renderer_epoch: 0,
322 surface_epoch: 0,
323 }
324 }
325
326 fn sync_gpu_renderer(&self) -> Option<&GpuRenderer> {
327 match &self.backend {
328 PresentBackend::Sync(gpu_renderer) => Some(gpu_renderer.as_ref()),
329 _ => None,
330 }
331 }
332
333 #[cfg(not(target_arch = "wasm32"))]
334 fn present_handle_mut(&mut self) -> Option<&mut PresentHandle> {
335 match &mut self.backend {
336 PresentBackend::Threaded(handle) => Some(handle),
337 _ => None,
338 }
339 }
340
341 fn retire_live_backend(&mut self) {
342 #[allow(unused_mut)]
343 let mut backend = std::mem::replace(&mut self.backend, PresentBackend::None);
344 #[cfg(not(target_arch = "wasm32"))]
345 if let PresentBackend::Threaded(handle) = &mut backend {
346 while let Some(returns) = handle.try_drain() {
347 self.frontend.apply_returns(returns);
348 }
349 handle.shutdown();
350 }
351 drop(backend);
352 }
353
354 pub fn init_gpu(
360 &mut self,
361 device: Arc<wgpu::Device>,
362 queue: Arc<wgpu::Queue>,
363 surface_format: wgpu::TextureFormat,
364 adapter_backend: wgpu::Backend,
365 adapter_downlevel: wgpu::DownlevelFlags,
366 ) {
367 self.retire_live_backend();
368 self.renderer_epoch = self.renderer_epoch.wrapping_add(1);
369 let gpu_renderer = GpuRenderer::new(
370 device,
371 queue,
372 surface_format,
373 adapter_backend,
374 adapter_downlevel,
375 self.frontend.text_fonts.clone(),
376 self.renderer_epoch,
377 );
378 self.backend = PresentBackend::Sync(Box::new(gpu_renderer));
379 }
380
381 #[cfg(not(target_arch = "wasm32"))]
398 #[expect(clippy::too_many_arguments)]
399 pub fn init_gpu_threaded(
400 &mut self,
401 device: Arc<wgpu::Device>,
402 queue: Arc<wgpu::Queue>,
403 surface_format: wgpu::TextureFormat,
404 adapter_backend: wgpu::Backend,
405 adapter_downlevel: wgpu::DownlevelFlags,
406 waker: Arc<dyn Fn() + Send + Sync>,
407 clock: Option<Arc<dyn Fn() -> i64 + Send + Sync>>,
408 observer: Option<Box<dyn PresentObserver>>,
409 ) -> Result<(), WgpuRendererError> {
410 self.retire_live_backend();
411 self.renderer_epoch = self.renderer_epoch.wrapping_add(1);
412 let init = PresentRuntimeInit {
413 device,
414 queue,
415 surface_format,
416 adapter_backend,
417 adapter_downlevel,
418 text_fonts: self.frontend.text_fonts.clone(),
419 renderer_epoch: self.renderer_epoch,
420 clock,
421 observer,
422 };
423 let handle = PresentHandle::spawn(init, waker).map_err(WgpuRendererError::Wgpu)?;
424 self.backend = PresentBackend::Threaded(handle);
425 Ok(())
426 }
427
428 #[cfg(not(target_arch = "wasm32"))]
429 #[doc(hidden)]
430 pub fn init_gpu_inline_for_tests(
431 &mut self,
432 device: Arc<wgpu::Device>,
433 queue: Arc<wgpu::Queue>,
434 surface_format: wgpu::TextureFormat,
435 adapter_backend: wgpu::Backend,
436 adapter_downlevel: wgpu::DownlevelFlags,
437 ) -> InlinePresentRuntime {
438 self.retire_live_backend();
439 self.renderer_epoch = self.renderer_epoch.wrapping_add(1);
440 let init = PresentRuntimeInit {
441 device,
442 queue,
443 surface_format,
444 adapter_backend,
445 adapter_downlevel,
446 text_fonts: self.frontend.text_fonts.clone(),
447 renderer_epoch: self.renderer_epoch,
448 clock: None,
449 observer: None,
450 };
451 let (handle, state, msg_rx) = PresentHandle::new_inline(init, Arc::new(|| {}));
452 self.backend = PresentBackend::Threaded(handle);
453 InlinePresentRuntime {
454 state,
455 msg_rx,
456 shutdown_seen: false,
457 }
458 }
459
460 pub fn note_surface_reconfigured(&mut self) {
465 self.surface_epoch = self.surface_epoch.wrapping_add(1);
466 }
467
468 pub fn set_root_scale(&mut self, scale: f32) {
470 self.frontend.root_scale = scale;
471 }
472
473 pub fn set_transparent_background(&mut self, transparent: bool) {
478 self.frontend.transparent_background = transparent;
479 }
480
481 pub fn root_scale(&self) -> f32 {
482 self.frontend.root_scale
483 }
484
485 pub fn render(
492 &mut self,
493 texture: &wgpu::Texture,
494 view: &wgpu::TextureView,
495 width: u32,
496 height: u32,
497 ) -> Result<(), WgpuRendererError> {
498 self.render_frame(texture, view, width, height)
499 }
500
501 pub fn render_surface_texture(
505 &mut self,
506 texture: &wgpu::Texture,
507 view: &wgpu::TextureView,
508 width: u32,
509 height: u32,
510 ) -> Result<(), WgpuRendererError> {
511 self.render_frame(texture, view, width, height)
512 }
513
514 pub fn present(&self, frame: wgpu::SurfaceTexture) {
521 match self.sync_gpu_renderer() {
522 Some(gpu_renderer) => gpu_renderer.queue.present(frame),
523 None => log::debug!("surface image released: the renderer has no GPU queue"),
524 }
525 }
526
527 fn render_frame(
528 &mut self,
529 texture: &wgpu::Texture,
530 view: &wgpu::TextureView,
531 width: u32,
532 height: u32,
533 ) -> Result<(), WgpuRendererError> {
534 let PresentBackend::Sync(gpu_renderer) = &mut self.backend else {
535 return Err(WgpuRendererError::Wgpu(
536 "GPU renderer not initialized for synchronous rendering. Call init_gpu() first."
537 .to_string(),
538 ));
539 };
540 let packet = self
541 .frontend
542 .build_frame_packet(width, height, self.renderer_epoch, self.surface_epoch)
543 .ok_or_else(|| WgpuRendererError::Wgpu("scene graph is missing".to_string()))?;
544 let mut returns = RenderReturns::default();
545 let result = gpu_renderer.render(
546 texture,
547 view,
548 width,
549 height,
550 packet,
551 self.surface_epoch,
552 &mut returns,
553 );
554 self.frontend.apply_returns(returns);
555 result.map_err(WgpuRendererError::Wgpu)
556 }
557
558 pub fn capture_frame(
562 &mut self,
563 width: u32,
564 height: u32,
565 ) -> Result<CapturedFrame, WgpuRendererError> {
566 self.capture_frame_with_scale(width, height, self.frontend.root_scale)
567 }
568
569 pub fn capture_frame_with_scale(
571 &mut self,
572 width: u32,
573 height: u32,
574 root_scale: f32,
575 ) -> Result<CapturedFrame, WgpuRendererError> {
576 let PresentBackend::Sync(gpu_renderer) = &mut self.backend else {
577 return Err(WgpuRendererError::Wgpu(
578 "GPU renderer not initialized for synchronous rendering. Call init_gpu() first."
579 .to_string(),
580 ));
581 };
582 let packet = self
583 .frontend
584 .build_frame_packet_with_scale(
585 width,
586 height,
587 root_scale,
588 self.renderer_epoch,
589 self.surface_epoch,
590 )
591 .ok_or_else(|| WgpuRendererError::Wgpu("scene graph is missing".to_string()))?;
592 let mut returns = RenderReturns::default();
593 let result = gpu_renderer.render_to_rgba_pixels(
594 width,
595 height,
596 packet,
597 self.surface_epoch,
598 &mut returns,
599 );
600 self.frontend.apply_returns(returns);
601 let pixels = result.map_err(WgpuRendererError::Wgpu)?;
602 Ok(CapturedFrame {
603 width,
604 height,
605 pixels,
606 })
607 }
608
609 #[cfg(not(target_arch = "wasm32"))]
614 pub fn has_frame_credit(&self) -> bool {
615 match &self.backend {
616 PresentBackend::Threaded(handle) => handle.has_credit(),
617 PresentBackend::Sync(_) | PresentBackend::None => true,
618 }
619 }
620
621 #[cfg(not(target_arch = "wasm32"))]
627 pub fn publish_frame(&mut self, width: u32, height: u32) -> PublishOutcome {
628 let PresentBackend::Threaded(handle) = &mut self.backend else {
629 log::error!("publish_frame called without a threaded present runtime");
630 return PublishOutcome::NoCredit;
631 };
632 if !handle.has_credit() {
633 return PublishOutcome::NoCredit;
634 }
635 let Some(packet) = self.frontend.build_frame_packet(
636 width,
637 height,
638 self.renderer_epoch,
639 self.surface_epoch,
640 ) else {
641 return PublishOutcome::NoGraph;
642 };
643 match handle.publish(packet) {
644 Ok(()) => PublishOutcome::Published,
645 Err(packet) => {
646 let mut returns = RenderReturns::default();
647 let _ = GpuRenderer::cancel_packet(
648 *packet,
649 CancelReason::SurfaceUnavailable,
650 &mut returns,
651 );
652 self.frontend.apply_returns(returns);
653 log::error!("present runtime unavailable; frame recovered, not published");
654 PublishOutcome::NoCredit
655 }
656 }
657 }
658
659 #[cfg(not(target_arch = "wasm32"))]
663 pub fn drain_present_returns(&mut self) -> usize {
664 self.drain_present_returns_with(&mut |_, _, _| {})
665 }
666
667 #[cfg(not(target_arch = "wasm32"))]
671 pub fn drain_present_returns_with(
672 &mut self,
673 on_return: &mut dyn FnMut(u64, PresentOutcome, PresentTimings),
674 ) -> usize {
675 let mut drained = 0;
676 loop {
677 let returns = {
678 let PresentBackend::Threaded(handle) = &mut self.backend else {
679 break;
680 };
681 match handle.try_drain() {
682 Some(returns) => returns,
683 None => break,
684 }
685 };
686 drained += 1;
687 let frame_id = returns.frame_id;
688 let outcome = returns.outcome;
689 let timings = returns.timings;
690 self.frontend.apply_returns(returns);
691 on_return(frame_id, outcome, timings);
692 }
693 drained
694 }
695
696 #[cfg(not(target_arch = "wasm32"))]
702 pub fn present_replace_surface(
703 &mut self,
704 surface: wgpu::Surface<'static>,
705 config: wgpu::SurfaceConfiguration,
706 ) -> bool {
707 let surface_epoch = self.surface_epoch;
708 let Some(handle) = self.present_handle_mut() else {
709 log::error!("present_replace_surface called without a threaded present runtime");
710 return false;
711 };
712 handle.send_control_and_wait(
713 move |ack| PresentControl::ReplaceSurface {
714 surface,
715 config,
716 surface_epoch,
717 ack,
718 },
719 "replace surface",
720 )
721 }
722
723 #[cfg(not(target_arch = "wasm32"))]
727 pub fn present_reconfigure(&mut self, config: wgpu::SurfaceConfiguration) -> bool {
728 let surface_epoch = self.surface_epoch;
729 let Some(handle) = self.present_handle_mut() else {
730 log::error!("present_reconfigure called without a threaded present runtime");
731 return false;
732 };
733 handle.send_control_and_wait(
734 move |ack| PresentControl::Reconfigure {
735 config,
736 surface_epoch,
737 ack,
738 },
739 "reconfigure surface",
740 )
741 }
742
743 #[cfg(not(target_arch = "wasm32"))]
747 pub fn present_drop_surface(&mut self) -> bool {
748 let Some(handle) = self.present_handle_mut() else {
749 log::error!("present_drop_surface called without a threaded present runtime");
750 return false;
751 };
752 handle.send_control_and_wait(|ack| PresentControl::DropSurface { ack }, "drop surface")
753 }
754
755 #[cfg(not(target_arch = "wasm32"))]
758 pub fn shutdown_present_runtime(&mut self) {
759 if matches!(self.backend, PresentBackend::Threaded(_)) {
760 self.retire_live_backend();
761 }
762 }
763
764 #[cfg(not(target_arch = "wasm32"))]
765 #[doc(hidden)]
766 pub fn present_attach_offscreen_for_tests(&mut self, width: u32, height: u32) -> bool {
767 let Some(handle) = self.present_handle_mut() else {
768 return false;
769 };
770 handle.send_control_and_wait(
771 move |ack| PresentControl::AttachOffscreenTargetForTests { width, height, ack },
772 "attach offscreen target",
773 )
774 }
775
776 #[cfg(not(target_arch = "wasm32"))]
777 #[doc(hidden)]
778 pub fn send_attach_offscreen_unacked_for_tests(
779 &mut self,
780 width: u32,
781 height: u32,
782 ) -> Option<std::sync::mpsc::Receiver<()>> {
783 let handle = self.present_handle_mut()?;
784 handle.send_control_unacked(move |ack| PresentControl::AttachOffscreenTargetForTests {
785 width,
786 height,
787 ack,
788 })
789 }
790
791 #[cfg(not(target_arch = "wasm32"))]
792 #[doc(hidden)]
793 pub fn send_reconfigure_unacked_for_tests(
794 &mut self,
795 config: wgpu::SurfaceConfiguration,
796 ) -> Option<std::sync::mpsc::Receiver<()>> {
797 let surface_epoch = self.surface_epoch;
798 let handle = self.present_handle_mut()?;
799 handle.send_control_unacked(move |ack| PresentControl::Reconfigure {
800 config,
801 surface_epoch,
802 ack,
803 })
804 }
805
806 #[cfg(not(target_arch = "wasm32"))]
807 #[doc(hidden)]
808 pub fn send_drop_surface_unacked_for_tests(&mut self) -> Option<std::sync::mpsc::Receiver<()>> {
809 let handle = self.present_handle_mut()?;
810 handle.send_control_unacked(|ack| PresentControl::DropSurface { ack })
811 }
812
813 pub fn present_thread_id(&self) -> Option<i32> {
817 match &self.backend {
818 #[cfg(not(target_arch = "wasm32"))]
819 PresentBackend::Threaded(handle) => {
820 let id = handle
821 .status()
822 .thread_id
823 .load(std::sync::atomic::Ordering::Relaxed);
824 (id != 0).then_some(id)
825 }
826 _ => None,
827 }
828 }
829
830 pub fn last_published_frame_id(&self) -> u64 {
835 self.frontend.frame_sequence
836 }
837
838 #[cfg(not(target_arch = "wasm32"))]
839 #[doc(hidden)]
840 pub fn present_status_snapshot_for_tests(&self) -> Option<(bool, u64, u64)> {
841 match &self.backend {
842 PresentBackend::Threaded(handle) => {
843 let status = handle.status();
844 Some((
845 status
846 .needs_frame_warmup
847 .load(std::sync::atomic::Ordering::Relaxed),
848 status
849 .presented_frames
850 .load(std::sync::atomic::Ordering::Relaxed),
851 status
852 .placeholder_frames
853 .load(std::sync::atomic::Ordering::Relaxed),
854 ))
855 }
856 _ => None,
857 }
858 }
859
860 pub fn last_frame_stats(&self) -> Option<RenderStatsSnapshot> {
861 match &self.backend {
862 PresentBackend::Sync(gpu_renderer) => gpu_renderer.last_frame_stats(),
863 #[cfg(not(target_arch = "wasm32"))]
864 PresentBackend::Threaded(handle) => *handle
865 .status()
866 .last_frame_stats
867 .lock()
868 .unwrap_or_else(std::sync::PoisonError::into_inner),
869 PresentBackend::None => None,
870 }
871 }
872
873 pub fn gpu_pass_timings(&self) -> GpuPassTimingReport {
877 self.sync_gpu_renderer()
878 .map(GpuRenderer::gpu_pass_timings)
879 .unwrap_or_default()
880 }
881
882 pub fn debug_cpu_allocation_stats(&self) -> DebugCpuAllocationStats {
883 let mut stats = self
884 .sync_gpu_renderer()
885 .map(GpuRenderer::debug_cpu_allocation_stats)
886 .unwrap_or_default();
887 stats.scene_graph_node_count = self
888 .frontend
889 .scene
890 .graph
891 .as_ref()
892 .map_or(0, RenderGraph::node_count);
893 stats.scene_graph_heap_bytes = self
894 .frontend
895 .scene
896 .graph
897 .as_ref()
898 .map_or(0, RenderGraph::heap_bytes);
899 stats.scene_hits_len = self.frontend.scene.hits.len();
900 stats.scene_hits_cap = self.frontend.scene.hits.capacity();
901 stats.scene_node_index_len = self.frontend.scene.node_index.len();
902 stats.scene_node_index_cap = self.frontend.scene.node_index.capacity();
903 stats
904 }
905
906 pub fn try_device(&self) -> Option<&wgpu::Device> {
910 self.sync_gpu_renderer().map(|r| &*r.device)
911 }
912
913 #[doc(hidden)]
914 pub fn try_queue_for_tests(&self) -> Option<&wgpu::Queue> {
915 self.sync_gpu_renderer().map(|r| &*r.queue)
916 }
917
918 #[doc(hidden)]
919 pub fn device_error_count_for_tests(&self) -> u64 {
920 self.sync_gpu_renderer()
921 .map_or(0, GpuRenderer::device_error_count)
922 }
923
924 #[doc(hidden)]
925 pub fn build_frame_packet_for_tests(
926 &mut self,
927 width: u32,
928 height: u32,
929 ) -> Option<HeldFramePacket> {
930 self.frontend
931 .build_frame_packet(width, height, self.renderer_epoch, self.surface_epoch)
932 .map(HeldFramePacket)
933 }
934
935 #[doc(hidden)]
936 pub fn render_held_packet_for_tests(
937 &mut self,
938 texture: &wgpu::Texture,
939 view: &wgpu::TextureView,
940 width: u32,
941 height: u32,
942 packet: HeldFramePacket,
943 ) -> Result<PresentOutcome, WgpuRendererError> {
944 let PresentBackend::Sync(gpu_renderer) = &mut self.backend else {
945 return Err(WgpuRendererError::Wgpu(
946 "GPU renderer not initialized for synchronous rendering. Call init_gpu() first."
947 .to_string(),
948 ));
949 };
950 let mut returns = RenderReturns::default();
951 let result = gpu_renderer.render(
952 texture,
953 view,
954 width,
955 height,
956 packet.0,
957 self.surface_epoch,
958 &mut returns,
959 );
960 let outcome = returns.outcome;
961 self.frontend.apply_returns(returns);
962 result.map_err(WgpuRendererError::Wgpu)?;
963 Ok(outcome)
964 }
965}
966
967#[doc(hidden)]
968pub struct HeldFramePacket(frame_packet::FramePacket);
969
970#[cfg(not(target_arch = "wasm32"))]
971#[doc(hidden)]
972pub struct InlinePresentRuntime {
973 state: PresentState,
974 msg_rx: std::sync::mpsc::Receiver<PresentMsg>,
975 shutdown_seen: bool,
976}
977
978#[cfg(not(target_arch = "wasm32"))]
979impl InlinePresentRuntime {
980 pub fn pump(&mut self) -> bool {
981 if self.shutdown_seen {
982 return false;
983 }
984 while let Ok(msg) = self.msg_rx.try_recv() {
985 if !self.state.run_once(msg) {
986 self.shutdown_seen = true;
987 return false;
988 }
989 }
990 self.state.consume_waiting();
991 true
992 }
993
994 pub fn has_waiting_packet(&self) -> bool {
995 self.state.has_waiting_packet()
996 }
997
998 pub fn step_one_message(&mut self) -> bool {
999 if self.shutdown_seen {
1000 return false;
1001 }
1002 match self.msg_rx.try_recv() {
1003 Ok(msg) => {
1004 if !self.state.run_once(msg) {
1005 self.shutdown_seen = true;
1006 }
1007 true
1008 }
1009 Err(_) => false,
1010 }
1011 }
1012
1013 pub fn consume_waiting(&mut self) {
1014 self.state.consume_waiting();
1015 }
1016}
1017
1018impl Default for WgpuRenderer {
1019 fn default() -> Self {
1020 Self::new(&[])
1021 }
1022}
1023
1024impl Renderer for WgpuRenderer {
1025 type Scene = Scene;
1026 type Error = WgpuRendererError;
1027
1028 fn attach_app_context_services(&mut self, app_context: &cranpose_ui::AppContext) {
1029 app_context.set_text_measurer(SoftwareTextMeasurer::from_font_set(
1030 self.frontend.text_fonts.clone(),
1031 8192,
1032 ));
1033 self.frontend.app_context = Some(app_context.downgrade());
1034 }
1035
1036 fn scene(&self) -> &Self::Scene {
1037 &self.frontend.scene
1038 }
1039
1040 fn scene_mut(&mut self) -> &mut Self::Scene {
1041 &mut self.frontend.scene
1042 }
1043
1044 fn rebuild_scene(
1045 &mut self,
1046 layout_tree: &LayoutTree,
1047 _viewport: Size,
1048 ) -> Result<(), Self::Error> {
1049 self.frontend.scene.clear();
1050 self.frontend.clear_fps_overlay();
1051 pipeline::render_layout_tree(layout_tree.root(), &mut self.frontend.scene);
1052 Ok(())
1053 }
1054
1055 fn rebuild_scene_from_applier(
1056 &mut self,
1057 applier: &mut MemoryApplier,
1058 root: NodeId,
1059 _viewport: Size,
1060 ) -> Result<(), Self::Error> {
1061 self.frontend.scene.clear();
1062 self.frontend.clear_fps_overlay();
1063 pipeline::render_from_applier(applier, root, &mut self.frontend.scene, 1.0);
1064 Ok(())
1065 }
1066
1067 fn update_scene_from_applier(
1068 &mut self,
1069 applier: &mut MemoryApplier,
1070 root: NodeId,
1071 viewport: Size,
1072 dirty_nodes: &[NodeId],
1073 ) -> Result<(), Self::Error> {
1074 if dirty_nodes.is_empty() {
1075 return self.rebuild_scene_from_applier(applier, root, viewport);
1076 }
1077 self.update_scene(applier, root, dirty_nodes, true);
1078 Ok(())
1079 }
1080
1081 fn update_visual_scene_from_applier(
1082 &mut self,
1083 applier: &mut MemoryApplier,
1084 root: NodeId,
1085 viewport: Size,
1086 dirty_nodes: &[NodeId],
1087 ) -> Result<(), Self::Error> {
1088 if dirty_nodes.is_empty() {
1089 return self.rebuild_scene_from_applier(applier, root, viewport);
1090 }
1091 self.update_scene(applier, root, dirty_nodes, false);
1092 Ok(())
1093 }
1094
1095 fn draw_dev_overlay(&mut self, text: &str, viewport: Size) {
1096 const DEV_OVERLAY_NODE_ID: NodeId = NodeId::MAX;
1097 let key = cranpose_render_common::dev_overlay::DevOverlayKey::new(text, viewport);
1098 if self.frontend.dev_overlay_graph.is_some()
1099 && self
1100 .frontend
1101 .dev_overlay_cache
1102 .as_ref()
1103 .is_some_and(|cache| {
1104 cache.text == key.text
1105 && cache.viewport_width_bits == key.viewport_width_bits
1106 && cache.viewport_height_bits == key.viewport_height_bits
1107 })
1108 {
1109 return;
1110 }
1111 self.frontend.fps_overlay_graph = Some(
1112 cranpose_render_common::dev_overlay::build_dev_overlay_graph(
1113 text,
1114 viewport,
1115 DEV_OVERLAY_NODE_ID,
1116 ),
1117 );
1118 self.frontend.dev_overlay_cache = Some(DevOverlayCache {
1119 text: key.text,
1120 viewport_width_bits: key.viewport_width_bits,
1121 viewport_height_bits: key.viewport_height_bits,
1122 });
1123 self.frontend.refresh_dev_overlay();
1124 }
1125
1126 fn set_inspector_overlay(&mut self, graph: Option<cranpose_render_common::graph::RenderGraph>) {
1127 self.frontend.inspector_overlay_graph = graph;
1128 self.frontend.refresh_dev_overlay();
1129 }
1130
1131 fn needs_frame_warmup(&self) -> bool {
1132 match &self.backend {
1133 PresentBackend::Sync(gpu_renderer) => gpu_renderer.needs_frame_warmup(),
1134 #[cfg(not(target_arch = "wasm32"))]
1135 PresentBackend::Threaded(handle) => handle
1136 .status()
1137 .needs_frame_warmup
1138 .load(std::sync::atomic::Ordering::Relaxed),
1139 PresentBackend::None => false,
1140 }
1141 }
1142}
1143
1144#[cfg(test)]
1145#[path = "tests/wgpu_tests.rs"]
1146mod tests;