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::{ClickAction, 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 layer_cache_len: usize,
169 pub layer_cache_bytes: u64,
170}
171
172pub(crate) struct TextSystemState {
173 measurer: SoftwareTextMeasurer,
174}
175
176impl TextSystemState {
177 fn from_font_set(fonts: SoftwareTextFontSet) -> Self {
178 Self {
179 measurer: SoftwareTextMeasurer::from_font_set(fonts, 8192),
180 }
181 }
182
183 pub(crate) fn text_cache_len(&self) -> usize {
184 0
185 }
186}
187
188impl pipeline::TextLayoutResolver for TextSystemState {
189 fn layout_text(
190 &mut self,
191 text: &cranpose_ui::text::AnnotatedString,
192 style: &cranpose_ui::text::TextStyle,
193 ) -> cranpose_ui::text_layout_result::TextLayoutResult {
194 if cranpose_ui::has_current_app_context() {
195 cranpose_ui::text::layout_text(text, style)
196 } else {
197 self.measurer.layout(text, style)
198 }
199 }
200}
201
202#[derive(Clone)]
203pub struct WgpuTextSystem {
204 software_fonts: SoftwareTextFontSet,
205}
206
207impl WgpuTextSystem {
208 pub fn from_fonts(fonts: &[&[u8]]) -> Self {
209 Self {
210 software_fonts: software_text_font_set_from_fonts_or_default(fonts),
211 }
212 }
213
214 pub fn from_font_set(software_fonts: SoftwareTextFontSet) -> Self {
218 Self { software_fonts }
219 }
220
221 pub(crate) fn render_state(&self) -> TextSystemState {
222 TextSystemState::from_font_set(self.software_fonts.clone())
223 }
224
225 pub(crate) fn software_fonts(&self) -> SoftwareTextFontSet {
226 self.software_fonts.clone()
227 }
228}
229
230pub fn headless_text_measurer() -> Rc<dyn TextMeasurer> {
232 headless_text_measurer_with_fonts(&[])
233}
234
235pub fn headless_text_measurer_with_fonts(fonts: &[&[u8]]) -> Rc<dyn TextMeasurer> {
237 Rc::new(SoftwareTextMeasurer::from_fonts_or_default(fonts, 8192))
238}
239
240enum PresentBackend {
241 None,
242 Sync(Box<GpuRenderer>),
243 #[cfg(not(target_arch = "wasm32"))]
244 Threaded(PresentHandle),
245}
246
247#[derive(Clone, Copy, Debug, PartialEq, Eq)]
249pub enum PublishOutcome {
250 NoGraph,
252 NoCredit,
255 Published,
257}
258
259pub struct WgpuRenderer {
267 frontend: RendererFrontend,
268 backend: PresentBackend,
269 renderer_epoch: u64,
270 surface_epoch: u64,
271}
272
273impl WgpuRenderer {
274 fn update_scene(
275 &mut self,
276 applier: &mut MemoryApplier,
277 root: NodeId,
278 dirty_nodes: &[NodeId],
279 refresh_hits: bool,
280 ) {
281 let mut changed_nodes = std::mem::take(&mut self.frontend.changed_nodes);
282 pipeline::update_from_applier(
283 applier,
284 root,
285 &mut self.frontend.scene,
286 1.0,
287 dirty_nodes,
288 refresh_hits,
289 &mut changed_nodes,
290 );
291 changed_nodes.clear();
292 self.frontend.changed_nodes = changed_nodes;
293 }
294
295 pub fn new(fonts: &[&[u8]]) -> Self {
302 Self::with_text_system(WgpuTextSystem::from_fonts(fonts))
303 }
304
305 pub fn with_font_set(fonts: SoftwareTextFontSet) -> Self {
310 Self::with_text_system(WgpuTextSystem::from_font_set(fonts))
311 }
312
313 pub fn with_text_system(text_system: WgpuTextSystem) -> Self {
314 Self {
315 frontend: RendererFrontend::new(
316 text_system.render_state(),
317 text_system.software_fonts(),
318 ),
319 backend: PresentBackend::None,
320 renderer_epoch: 0,
321 surface_epoch: 0,
322 }
323 }
324
325 fn sync_gpu_renderer(&self) -> Option<&GpuRenderer> {
326 match &self.backend {
327 PresentBackend::Sync(gpu_renderer) => Some(gpu_renderer.as_ref()),
328 _ => None,
329 }
330 }
331
332 #[cfg(not(target_arch = "wasm32"))]
333 fn present_handle_mut(&mut self) -> Option<&mut PresentHandle> {
334 match &mut self.backend {
335 PresentBackend::Threaded(handle) => Some(handle),
336 _ => None,
337 }
338 }
339
340 fn retire_live_backend(&mut self) {
341 #[allow(unused_mut)]
342 let mut backend = std::mem::replace(&mut self.backend, PresentBackend::None);
343 #[cfg(not(target_arch = "wasm32"))]
344 if let PresentBackend::Threaded(handle) = &mut backend {
345 while let Some(returns) = handle.try_drain() {
346 self.frontend.apply_returns(returns);
347 }
348 handle.shutdown();
349 }
350 drop(backend);
351 }
352
353 pub fn init_gpu(
359 &mut self,
360 device: Arc<wgpu::Device>,
361 queue: Arc<wgpu::Queue>,
362 surface_format: wgpu::TextureFormat,
363 adapter_backend: wgpu::Backend,
364 adapter_downlevel: wgpu::DownlevelFlags,
365 ) {
366 self.retire_live_backend();
367 self.renderer_epoch = self.renderer_epoch.wrapping_add(1);
368 let gpu_renderer = GpuRenderer::new(
369 device,
370 queue,
371 surface_format,
372 adapter_backend,
373 adapter_downlevel,
374 self.frontend.text_fonts.clone(),
375 self.renderer_epoch,
376 );
377 self.backend = PresentBackend::Sync(Box::new(gpu_renderer));
378 }
379
380 #[cfg(not(target_arch = "wasm32"))]
397 #[expect(clippy::too_many_arguments)]
398 pub fn init_gpu_threaded(
399 &mut self,
400 device: Arc<wgpu::Device>,
401 queue: Arc<wgpu::Queue>,
402 surface_format: wgpu::TextureFormat,
403 adapter_backend: wgpu::Backend,
404 adapter_downlevel: wgpu::DownlevelFlags,
405 waker: Arc<dyn Fn() + Send + Sync>,
406 clock: Option<Arc<dyn Fn() -> i64 + Send + Sync>>,
407 observer: Option<Box<dyn PresentObserver>>,
408 ) -> Result<(), WgpuRendererError> {
409 self.retire_live_backend();
410 self.renderer_epoch = self.renderer_epoch.wrapping_add(1);
411 let init = PresentRuntimeInit {
412 device,
413 queue,
414 surface_format,
415 adapter_backend,
416 adapter_downlevel,
417 text_fonts: self.frontend.text_fonts.clone(),
418 renderer_epoch: self.renderer_epoch,
419 clock,
420 observer,
421 };
422 let handle = PresentHandle::spawn(init, waker).map_err(WgpuRendererError::Wgpu)?;
423 self.backend = PresentBackend::Threaded(handle);
424 Ok(())
425 }
426
427 #[cfg(not(target_arch = "wasm32"))]
428 #[doc(hidden)]
429 pub fn init_gpu_inline_for_tests(
430 &mut self,
431 device: Arc<wgpu::Device>,
432 queue: Arc<wgpu::Queue>,
433 surface_format: wgpu::TextureFormat,
434 adapter_backend: wgpu::Backend,
435 adapter_downlevel: wgpu::DownlevelFlags,
436 ) -> InlinePresentRuntime {
437 self.retire_live_backend();
438 self.renderer_epoch = self.renderer_epoch.wrapping_add(1);
439 let init = PresentRuntimeInit {
440 device,
441 queue,
442 surface_format,
443 adapter_backend,
444 adapter_downlevel,
445 text_fonts: self.frontend.text_fonts.clone(),
446 renderer_epoch: self.renderer_epoch,
447 clock: None,
448 observer: None,
449 };
450 let (handle, state, msg_rx) = PresentHandle::new_inline(init, Arc::new(|| {}));
451 self.backend = PresentBackend::Threaded(handle);
452 InlinePresentRuntime {
453 state,
454 msg_rx,
455 shutdown_seen: false,
456 }
457 }
458
459 pub fn note_surface_reconfigured(&mut self) {
464 self.surface_epoch = self.surface_epoch.wrapping_add(1);
465 }
466
467 pub fn set_root_scale(&mut self, scale: f32) {
469 self.frontend.root_scale = scale;
470 }
471
472 pub fn set_transparent_background(&mut self, transparent: bool) {
477 self.frontend.transparent_background = transparent;
478 }
479
480 pub fn root_scale(&self) -> f32 {
481 self.frontend.root_scale
482 }
483
484 pub fn render(
491 &mut self,
492 texture: &wgpu::Texture,
493 view: &wgpu::TextureView,
494 width: u32,
495 height: u32,
496 ) -> Result<(), WgpuRendererError> {
497 self.render_frame(texture, view, width, height)
498 }
499
500 pub fn render_surface_texture(
504 &mut self,
505 texture: &wgpu::Texture,
506 view: &wgpu::TextureView,
507 width: u32,
508 height: u32,
509 ) -> Result<(), WgpuRendererError> {
510 self.render_frame(texture, view, width, height)
511 }
512
513 pub fn present(&self, frame: wgpu::SurfaceTexture) {
520 match self.sync_gpu_renderer() {
521 Some(gpu_renderer) => gpu_renderer.queue.present(frame),
522 None => log::debug!("surface image released: the renderer has no GPU queue"),
523 }
524 }
525
526 fn render_frame(
527 &mut self,
528 texture: &wgpu::Texture,
529 view: &wgpu::TextureView,
530 width: u32,
531 height: u32,
532 ) -> Result<(), WgpuRendererError> {
533 let PresentBackend::Sync(gpu_renderer) = &mut self.backend else {
534 return Err(WgpuRendererError::Wgpu(
535 "GPU renderer not initialized for synchronous rendering. Call init_gpu() first."
536 .to_string(),
537 ));
538 };
539 let packet = self
540 .frontend
541 .build_frame_packet(width, height, self.renderer_epoch, self.surface_epoch)
542 .ok_or_else(|| WgpuRendererError::Wgpu("scene graph is missing".to_string()))?;
543 let mut returns = RenderReturns::default();
544 let result = gpu_renderer.render(
545 texture,
546 view,
547 width,
548 height,
549 packet,
550 self.surface_epoch,
551 &mut returns,
552 );
553 self.frontend.apply_returns(returns);
554 result.map_err(WgpuRendererError::Wgpu)
555 }
556
557 pub fn capture_frame(
561 &mut self,
562 width: u32,
563 height: u32,
564 ) -> Result<CapturedFrame, WgpuRendererError> {
565 self.capture_frame_with_scale(width, height, self.frontend.root_scale)
566 }
567
568 pub fn capture_frame_with_scale(
570 &mut self,
571 width: u32,
572 height: u32,
573 root_scale: f32,
574 ) -> Result<CapturedFrame, WgpuRendererError> {
575 let PresentBackend::Sync(gpu_renderer) = &mut self.backend else {
576 return Err(WgpuRendererError::Wgpu(
577 "GPU renderer not initialized for synchronous rendering. Call init_gpu() first."
578 .to_string(),
579 ));
580 };
581 let packet = self
582 .frontend
583 .build_frame_packet_with_scale(
584 width,
585 height,
586 root_scale,
587 self.renderer_epoch,
588 self.surface_epoch,
589 )
590 .ok_or_else(|| WgpuRendererError::Wgpu("scene graph is missing".to_string()))?;
591 let mut returns = RenderReturns::default();
592 let result = gpu_renderer.render_to_rgba_pixels(
593 width,
594 height,
595 packet,
596 self.surface_epoch,
597 &mut returns,
598 );
599 self.frontend.apply_returns(returns);
600 let pixels = result.map_err(WgpuRendererError::Wgpu)?;
601 Ok(CapturedFrame {
602 width,
603 height,
604 pixels,
605 })
606 }
607
608 #[cfg(not(target_arch = "wasm32"))]
613 pub fn has_frame_credit(&self) -> bool {
614 match &self.backend {
615 PresentBackend::Threaded(handle) => handle.has_credit(),
616 PresentBackend::Sync(_) | PresentBackend::None => true,
617 }
618 }
619
620 #[cfg(not(target_arch = "wasm32"))]
626 pub fn publish_frame(&mut self, width: u32, height: u32) -> PublishOutcome {
627 let PresentBackend::Threaded(handle) = &mut self.backend else {
628 log::error!("publish_frame called without a threaded present runtime");
629 return PublishOutcome::NoCredit;
630 };
631 if !handle.has_credit() {
632 return PublishOutcome::NoCredit;
633 }
634 let Some(packet) = self.frontend.build_frame_packet(
635 width,
636 height,
637 self.renderer_epoch,
638 self.surface_epoch,
639 ) else {
640 return PublishOutcome::NoGraph;
641 };
642 match handle.publish(packet) {
643 Ok(()) => PublishOutcome::Published,
644 Err(packet) => {
645 let mut returns = RenderReturns::default();
646 let _ = GpuRenderer::cancel_packet(
647 *packet,
648 CancelReason::SurfaceUnavailable,
649 &mut returns,
650 );
651 self.frontend.apply_returns(returns);
652 log::error!("present runtime unavailable; frame recovered, not published");
653 PublishOutcome::NoCredit
654 }
655 }
656 }
657
658 #[cfg(not(target_arch = "wasm32"))]
662 pub fn drain_present_returns(&mut self) -> usize {
663 self.drain_present_returns_with(&mut |_, _, _| {})
664 }
665
666 #[cfg(not(target_arch = "wasm32"))]
670 pub fn drain_present_returns_with(
671 &mut self,
672 on_return: &mut dyn FnMut(u64, PresentOutcome, PresentTimings),
673 ) -> usize {
674 let mut drained = 0;
675 loop {
676 let returns = {
677 let PresentBackend::Threaded(handle) = &mut self.backend else {
678 break;
679 };
680 match handle.try_drain() {
681 Some(returns) => returns,
682 None => break,
683 }
684 };
685 drained += 1;
686 let frame_id = returns.frame_id;
687 let outcome = returns.outcome;
688 let timings = returns.timings;
689 self.frontend.apply_returns(returns);
690 on_return(frame_id, outcome, timings);
691 }
692 drained
693 }
694
695 #[cfg(not(target_arch = "wasm32"))]
701 pub fn present_replace_surface(
702 &mut self,
703 surface: wgpu::Surface<'static>,
704 config: wgpu::SurfaceConfiguration,
705 ) -> bool {
706 let surface_epoch = self.surface_epoch;
707 let Some(handle) = self.present_handle_mut() else {
708 log::error!("present_replace_surface called without a threaded present runtime");
709 return false;
710 };
711 handle.send_control_and_wait(
712 move |ack| PresentControl::ReplaceSurface {
713 surface,
714 config,
715 surface_epoch,
716 ack,
717 },
718 "replace surface",
719 )
720 }
721
722 #[cfg(not(target_arch = "wasm32"))]
726 pub fn present_reconfigure(&mut self, config: wgpu::SurfaceConfiguration) -> bool {
727 let surface_epoch = self.surface_epoch;
728 let Some(handle) = self.present_handle_mut() else {
729 log::error!("present_reconfigure called without a threaded present runtime");
730 return false;
731 };
732 handle.send_control_and_wait(
733 move |ack| PresentControl::Reconfigure {
734 config,
735 surface_epoch,
736 ack,
737 },
738 "reconfigure surface",
739 )
740 }
741
742 #[cfg(not(target_arch = "wasm32"))]
746 pub fn present_drop_surface(&mut self) -> bool {
747 let Some(handle) = self.present_handle_mut() else {
748 log::error!("present_drop_surface called without a threaded present runtime");
749 return false;
750 };
751 handle.send_control_and_wait(|ack| PresentControl::DropSurface { ack }, "drop surface")
752 }
753
754 #[cfg(not(target_arch = "wasm32"))]
757 pub fn shutdown_present_runtime(&mut self) {
758 if matches!(self.backend, PresentBackend::Threaded(_)) {
759 self.retire_live_backend();
760 }
761 }
762
763 #[cfg(not(target_arch = "wasm32"))]
764 #[doc(hidden)]
765 pub fn present_attach_offscreen_for_tests(&mut self, width: u32, height: u32) -> bool {
766 let Some(handle) = self.present_handle_mut() else {
767 return false;
768 };
769 handle.send_control_and_wait(
770 move |ack| PresentControl::AttachOffscreenTargetForTests { width, height, ack },
771 "attach offscreen target",
772 )
773 }
774
775 #[cfg(not(target_arch = "wasm32"))]
776 #[doc(hidden)]
777 pub fn send_attach_offscreen_unacked_for_tests(
778 &mut self,
779 width: u32,
780 height: u32,
781 ) -> Option<std::sync::mpsc::Receiver<()>> {
782 let handle = self.present_handle_mut()?;
783 handle.send_control_unacked(move |ack| PresentControl::AttachOffscreenTargetForTests {
784 width,
785 height,
786 ack,
787 })
788 }
789
790 #[cfg(not(target_arch = "wasm32"))]
791 #[doc(hidden)]
792 pub fn send_reconfigure_unacked_for_tests(
793 &mut self,
794 config: wgpu::SurfaceConfiguration,
795 ) -> Option<std::sync::mpsc::Receiver<()>> {
796 let surface_epoch = self.surface_epoch;
797 let handle = self.present_handle_mut()?;
798 handle.send_control_unacked(move |ack| PresentControl::Reconfigure {
799 config,
800 surface_epoch,
801 ack,
802 })
803 }
804
805 #[cfg(not(target_arch = "wasm32"))]
806 #[doc(hidden)]
807 pub fn send_drop_surface_unacked_for_tests(&mut self) -> Option<std::sync::mpsc::Receiver<()>> {
808 let handle = self.present_handle_mut()?;
809 handle.send_control_unacked(|ack| PresentControl::DropSurface { ack })
810 }
811
812 pub fn present_thread_id(&self) -> Option<i32> {
816 match &self.backend {
817 #[cfg(not(target_arch = "wasm32"))]
818 PresentBackend::Threaded(handle) => {
819 let id = handle
820 .status()
821 .thread_id
822 .load(std::sync::atomic::Ordering::Relaxed);
823 (id != 0).then_some(id)
824 }
825 _ => None,
826 }
827 }
828
829 pub fn last_published_frame_id(&self) -> u64 {
834 self.frontend.frame_sequence
835 }
836
837 #[cfg(not(target_arch = "wasm32"))]
838 #[doc(hidden)]
839 pub fn present_status_snapshot_for_tests(&self) -> Option<(bool, u64, u64)> {
840 match &self.backend {
841 PresentBackend::Threaded(handle) => {
842 let status = handle.status();
843 Some((
844 status
845 .needs_frame_warmup
846 .load(std::sync::atomic::Ordering::Relaxed),
847 status
848 .presented_frames
849 .load(std::sync::atomic::Ordering::Relaxed),
850 status
851 .placeholder_frames
852 .load(std::sync::atomic::Ordering::Relaxed),
853 ))
854 }
855 _ => None,
856 }
857 }
858
859 pub fn last_frame_stats(&self) -> Option<RenderStatsSnapshot> {
860 match &self.backend {
861 PresentBackend::Sync(gpu_renderer) => gpu_renderer.last_frame_stats(),
862 #[cfg(not(target_arch = "wasm32"))]
863 PresentBackend::Threaded(handle) => *handle
864 .status()
865 .last_frame_stats
866 .lock()
867 .unwrap_or_else(std::sync::PoisonError::into_inner),
868 PresentBackend::None => None,
869 }
870 }
871
872 pub fn gpu_pass_timings(&self) -> GpuPassTimingReport {
876 self.sync_gpu_renderer()
877 .map(GpuRenderer::gpu_pass_timings)
878 .unwrap_or_default()
879 }
880
881 pub fn debug_cpu_allocation_stats(&self) -> DebugCpuAllocationStats {
882 let mut stats = self
883 .sync_gpu_renderer()
884 .map(GpuRenderer::debug_cpu_allocation_stats)
885 .unwrap_or_default();
886 stats.scene_graph_node_count = self
887 .frontend
888 .scene
889 .graph
890 .as_ref()
891 .map_or(0, RenderGraph::node_count);
892 stats.scene_graph_heap_bytes = self
893 .frontend
894 .scene
895 .graph
896 .as_ref()
897 .map_or(0, RenderGraph::heap_bytes);
898 stats.scene_hits_len = self.frontend.scene.hits.len();
899 stats.scene_hits_cap = self.frontend.scene.hits.capacity();
900 stats.scene_node_index_len = self.frontend.scene.node_index.len();
901 stats.scene_node_index_cap = self.frontend.scene.node_index.capacity();
902 stats
903 }
904
905 pub fn try_device(&self) -> Option<&wgpu::Device> {
909 self.sync_gpu_renderer().map(|r| &*r.device)
910 }
911
912 #[doc(hidden)]
913 pub fn try_queue_for_tests(&self) -> Option<&wgpu::Queue> {
914 self.sync_gpu_renderer().map(|r| &*r.queue)
915 }
916
917 #[doc(hidden)]
918 pub fn device_error_count_for_tests(&self) -> u64 {
919 self.sync_gpu_renderer()
920 .map_or(0, GpuRenderer::device_error_count)
921 }
922
923 #[doc(hidden)]
924 pub fn build_frame_packet_for_tests(
925 &mut self,
926 width: u32,
927 height: u32,
928 ) -> Option<HeldFramePacket> {
929 self.frontend
930 .build_frame_packet(width, height, self.renderer_epoch, self.surface_epoch)
931 .map(HeldFramePacket)
932 }
933
934 #[doc(hidden)]
935 pub fn render_held_packet_for_tests(
936 &mut self,
937 texture: &wgpu::Texture,
938 view: &wgpu::TextureView,
939 width: u32,
940 height: u32,
941 packet: HeldFramePacket,
942 ) -> Result<PresentOutcome, WgpuRendererError> {
943 let PresentBackend::Sync(gpu_renderer) = &mut self.backend else {
944 return Err(WgpuRendererError::Wgpu(
945 "GPU renderer not initialized for synchronous rendering. Call init_gpu() first."
946 .to_string(),
947 ));
948 };
949 let mut returns = RenderReturns::default();
950 let result = gpu_renderer.render(
951 texture,
952 view,
953 width,
954 height,
955 packet.0,
956 self.surface_epoch,
957 &mut returns,
958 );
959 let outcome = returns.outcome;
960 self.frontend.apply_returns(returns);
961 result.map_err(WgpuRendererError::Wgpu)?;
962 Ok(outcome)
963 }
964}
965
966#[doc(hidden)]
967pub struct HeldFramePacket(frame_packet::FramePacket);
968
969#[cfg(not(target_arch = "wasm32"))]
970#[doc(hidden)]
971pub struct InlinePresentRuntime {
972 state: PresentState,
973 msg_rx: std::sync::mpsc::Receiver<PresentMsg>,
974 shutdown_seen: bool,
975}
976
977#[cfg(not(target_arch = "wasm32"))]
978impl InlinePresentRuntime {
979 pub fn pump(&mut self) -> bool {
980 if self.shutdown_seen {
981 return false;
982 }
983 while let Ok(msg) = self.msg_rx.try_recv() {
984 if !self.state.run_once(msg) {
985 self.shutdown_seen = true;
986 return false;
987 }
988 }
989 self.state.consume_waiting();
990 true
991 }
992
993 pub fn has_waiting_packet(&self) -> bool {
994 self.state.has_waiting_packet()
995 }
996
997 pub fn step_one_message(&mut self) -> bool {
998 if self.shutdown_seen {
999 return false;
1000 }
1001 match self.msg_rx.try_recv() {
1002 Ok(msg) => {
1003 if !self.state.run_once(msg) {
1004 self.shutdown_seen = true;
1005 }
1006 true
1007 }
1008 Err(_) => false,
1009 }
1010 }
1011
1012 pub fn consume_waiting(&mut self) {
1013 self.state.consume_waiting();
1014 }
1015}
1016
1017impl Default for WgpuRenderer {
1018 fn default() -> Self {
1019 Self::new(&[])
1020 }
1021}
1022
1023impl Renderer for WgpuRenderer {
1024 type Scene = Scene;
1025 type Error = WgpuRendererError;
1026
1027 fn attach_app_context_services(&mut self, app_context: &cranpose_ui::AppContext) {
1028 app_context.set_text_measurer(SoftwareTextMeasurer::from_font_set(
1029 self.frontend.text_fonts.clone(),
1030 8192,
1031 ));
1032 self.frontend.app_context = Some(app_context.downgrade());
1033 }
1034
1035 fn scene(&self) -> &Self::Scene {
1036 &self.frontend.scene
1037 }
1038
1039 fn scene_mut(&mut self) -> &mut Self::Scene {
1040 &mut self.frontend.scene
1041 }
1042
1043 fn rebuild_scene(
1044 &mut self,
1045 layout_tree: &LayoutTree,
1046 _viewport: Size,
1047 ) -> Result<(), Self::Error> {
1048 self.frontend.scene.clear();
1049 self.frontend.clear_fps_overlay();
1050 pipeline::render_layout_tree(layout_tree.root(), &mut self.frontend.scene);
1051 Ok(())
1052 }
1053
1054 fn rebuild_scene_from_applier(
1055 &mut self,
1056 applier: &mut MemoryApplier,
1057 root: NodeId,
1058 _viewport: Size,
1059 ) -> Result<(), Self::Error> {
1060 self.frontend.scene.clear();
1061 self.frontend.clear_fps_overlay();
1062 pipeline::render_from_applier(applier, root, &mut self.frontend.scene, 1.0);
1063 Ok(())
1064 }
1065
1066 fn update_scene_from_applier(
1067 &mut self,
1068 applier: &mut MemoryApplier,
1069 root: NodeId,
1070 viewport: Size,
1071 dirty_nodes: &[NodeId],
1072 ) -> Result<(), Self::Error> {
1073 if dirty_nodes.is_empty() {
1074 return self.rebuild_scene_from_applier(applier, root, viewport);
1075 }
1076 self.update_scene(applier, root, dirty_nodes, true);
1077 Ok(())
1078 }
1079
1080 fn update_visual_scene_from_applier(
1081 &mut self,
1082 applier: &mut MemoryApplier,
1083 root: NodeId,
1084 viewport: Size,
1085 dirty_nodes: &[NodeId],
1086 ) -> Result<(), Self::Error> {
1087 if dirty_nodes.is_empty() {
1088 return self.rebuild_scene_from_applier(applier, root, viewport);
1089 }
1090 self.update_scene(applier, root, dirty_nodes, false);
1091 Ok(())
1092 }
1093
1094 fn draw_dev_overlay(&mut self, text: &str, viewport: Size) {
1095 const DEV_OVERLAY_NODE_ID: NodeId = NodeId::MAX;
1096 let key = cranpose_render_common::dev_overlay::DevOverlayKey::new(text, viewport);
1097 if self.frontend.dev_overlay_graph.is_some()
1098 && self
1099 .frontend
1100 .dev_overlay_cache
1101 .as_ref()
1102 .is_some_and(|cache| {
1103 cache.text == key.text
1104 && cache.viewport_width_bits == key.viewport_width_bits
1105 && cache.viewport_height_bits == key.viewport_height_bits
1106 })
1107 {
1108 return;
1109 }
1110 self.frontend.fps_overlay_graph = Some(
1111 cranpose_render_common::dev_overlay::build_dev_overlay_graph(
1112 text,
1113 viewport,
1114 DEV_OVERLAY_NODE_ID,
1115 ),
1116 );
1117 self.frontend.dev_overlay_cache = Some(DevOverlayCache {
1118 text: key.text,
1119 viewport_width_bits: key.viewport_width_bits,
1120 viewport_height_bits: key.viewport_height_bits,
1121 });
1122 self.frontend.refresh_dev_overlay();
1123 }
1124
1125 fn set_inspector_overlay(&mut self, graph: Option<cranpose_render_common::graph::RenderGraph>) {
1126 self.frontend.inspector_overlay_graph = graph;
1127 self.frontend.refresh_dev_overlay();
1128 }
1129
1130 fn needs_frame_warmup(&self) -> bool {
1131 match &self.backend {
1132 PresentBackend::Sync(gpu_renderer) => gpu_renderer.needs_frame_warmup(),
1133 #[cfg(not(target_arch = "wasm32"))]
1134 PresentBackend::Threaded(handle) => handle
1135 .status()
1136 .needs_frame_warmup
1137 .load(std::sync::atomic::Ordering::Relaxed),
1138 PresentBackend::None => false,
1139 }
1140 }
1141}
1142
1143#[cfg(test)]
1144#[path = "tests/wgpu_tests.rs"]
1145mod tests;