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;
26mod glyph_run;
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;
39mod pipeline_recorder;
40#[cfg(not(target_arch = "wasm32"))]
41mod pipeline_records;
42#[cfg(not(target_arch = "wasm32"))]
43mod present_runtime;
44mod record_columns;
45mod render;
46mod rrect_shadow;
47mod run_geometry;
48mod run_store;
49mod scene;
50mod shader_cache;
51mod shaders;
52mod shape_pipelines;
53mod shared_shader;
54#[cfg(test)]
55mod test_support;
56
57use std::{rc::Rc, sync::Arc};
58
59use cranpose_core::{MemoryApplier, NodeId};
60use cranpose_render_common::{
61 RenderScene, Renderer,
62 graph::RenderGraph,
63 software_text_raster::{
64 SoftwareTextFontSet, SoftwareTextMeasurer, software_text_font_set_from_fonts_or_default,
65 },
66};
67use cranpose_ui::{LayoutTree, TextMeasurer};
68use cranpose_ui_graphics::{Rect, Size};
69pub use frame_packet::PresentTimings;
70
71#[cfg(not(target_arch = "wasm32"))]
75pub trait PresentObserver: Send {
76 fn before_present(&mut self, surface: &wgpu::Surface<'static>);
78 fn after_present(&mut self, surface: &wgpu::Surface<'static>);
80
81 fn before_reconfigure(&mut self, _surface: &wgpu::Surface<'static>) {}
84}
85use frame_packet::RenderReturns;
86#[doc(hidden)]
87pub use frame_packet::{CancelReason, PresentOutcome};
88use frontend::{DevOverlayCache, RendererFrontend};
89pub use gpu_stats::FrameStatsSnapshot as RenderStatsSnapshot;
90pub use initial_present::{clear_to_background, clear_to_default_background};
91pub use pass_timing::{GpuPassTimingEntry, GpuPassTimingReport};
92use pipeline_compiler::PipelineCompilation;
93#[cfg(not(target_arch = "wasm32"))]
94use present_runtime::{
95 PresentControl, PresentHandle, PresentMsg, PresentRuntimeInit, PresentState,
96};
97use render::{GpuRenderer, GpuRendererInit};
98pub use render::{
99 frame_clear_color, frames_presented, pipelines_created, pipelines_created_off_frame,
100};
101pub use scene::{HitRegion, Scene};
102
103pub fn optional_device_features(adapter: &wgpu::Adapter) -> wgpu::Features {
112 let mut wanted = wgpu::Features::PIPELINE_CACHE | wgpu::Features::TIMESTAMP_QUERY;
113 if matches!(
114 adapter.get_info().device_type,
115 wgpu::DeviceType::IntegratedGpu | wgpu::DeviceType::Cpu
116 ) {
117 wanted |= wgpu::Features::MAPPABLE_PRIMARY_BUFFERS;
118 }
119 adapter.features() & wanted
120}
121
122#[doc(hidden)]
123pub fn offscreen_render_target_for_tests(
124 device: &wgpu::Device,
125 width: u32,
126 height: u32,
127 label: &str,
128) -> (wgpu::Texture, wgpu::TextureView) {
129 let texture = offscreen::create_2d_texture(
130 device,
131 wgpu::TextureFormat::Rgba8Unorm,
132 width,
133 height,
134 wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
135 Some(label),
136 );
137 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
138 (texture, view)
139}
140
141pub(crate) fn rect_to_quad(rect: Rect) -> [[f32; 2]; 4] {
142 [
143 [rect.x, rect.y],
144 [rect.x + rect.width, rect.y],
145 [rect.x, rect.y + rect.height],
146 [rect.x + rect.width, rect.y + rect.height],
147 ]
148}
149
150#[derive(Debug)]
151pub enum WgpuRendererError {
152 Layout(String),
153 Wgpu(String),
154}
155
156#[derive(Debug, Clone)]
158pub struct CapturedFrame {
159 pub width: u32,
160 pub height: u32,
161 pub pixels: Vec<u8>,
162}
163
164#[doc(hidden)]
165#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
166pub struct DebugCpuAllocationStats {
167 pub scene_graph_node_count: usize,
168 pub scene_graph_heap_bytes: usize,
169 pub scene_hits_len: usize,
170 pub scene_hits_cap: usize,
171 pub scene_node_index_len: usize,
172 pub scene_node_index_cap: usize,
173 pub text_renderer_pool_len: usize,
174 pub text_renderer_pool_cap: usize,
175 pub image_texture_cache_len: usize,
176 pub image_texture_cache_cap: usize,
177 pub run_arena_staging_bytes: usize,
178 pub run_store_bytes: usize,
179 pub run_store_runs: usize,
180 pub scratch_image_vertices_cap: usize,
181 pub scratch_image_indices_cap: usize,
182 pub scratch_image_cmds_cap: usize,
183 pub scratch_glyph_instances_cap: usize,
184 pub layer_cache_len: usize,
185 pub layer_cache_bytes: u64,
186}
187
188struct GpuTarget {
190 device: Arc<wgpu::Device>,
191 queue: Arc<wgpu::Queue>,
192 surface_format: wgpu::TextureFormat,
193 adapter_backend: wgpu::Backend,
194 adapter_downlevel: wgpu::DownlevelFlags,
195}
196
197pub(crate) struct TextSystemState {
198 measurer: SoftwareTextMeasurer,
199}
200
201impl TextSystemState {
202 fn from_font_set(fonts: SoftwareTextFontSet) -> Self {
203 Self {
204 measurer: SoftwareTextMeasurer::from_font_set(fonts, 8192),
205 }
206 }
207
208 pub(crate) fn text_cache_len(&self) -> usize {
209 0
210 }
211}
212
213impl pipeline::TextLayoutResolver for TextSystemState {
214 fn layout_text(
215 &mut self,
216 text: &cranpose_ui::text::AnnotatedString,
217 style: &cranpose_ui::text::TextStyle,
218 ) -> cranpose_ui::text_layout_result::TextLayoutResult {
219 if cranpose_ui::has_current_app_context() {
220 cranpose_ui::text::layout_text(text, style)
221 } else {
222 self.measurer.layout(text, style)
223 }
224 }
225}
226
227#[derive(Clone)]
228pub struct WgpuTextSystem {
229 software_fonts: SoftwareTextFontSet,
230}
231
232impl WgpuTextSystem {
233 pub fn from_fonts(fonts: &[&'static [u8]]) -> Self {
234 Self {
235 software_fonts: software_text_font_set_from_fonts_or_default(fonts),
236 }
237 }
238
239 pub fn from_font_set(software_fonts: SoftwareTextFontSet) -> Self {
243 Self { software_fonts }
244 }
245
246 pub(crate) fn render_state(&self) -> TextSystemState {
247 TextSystemState::from_font_set(self.software_fonts.clone())
248 }
249
250 pub(crate) fn software_fonts(&self) -> SoftwareTextFontSet {
251 self.software_fonts.clone()
252 }
253}
254
255pub fn headless_text_measurer() -> Rc<dyn TextMeasurer> {
257 headless_text_measurer_with_fonts(&[])
258}
259
260pub fn headless_text_measurer_with_fonts(fonts: &[&'static [u8]]) -> Rc<dyn TextMeasurer> {
262 Rc::new(SoftwareTextMeasurer::from_fonts_or_default(fonts, 8192))
263}
264
265enum PresentBackend {
266 None,
267 Sync(Box<GpuRenderer>),
268 #[cfg(not(target_arch = "wasm32"))]
269 Threaded(PresentHandle),
270}
271
272#[derive(Clone, Copy, Debug, PartialEq, Eq)]
274pub enum PublishOutcome {
275 NoGraph,
277 NoCredit,
280 Published,
282}
283
284pub struct WgpuRenderer {
292 frontend: RendererFrontend,
293 backend: PresentBackend,
294 renderer_epoch: u64,
295 surface_epoch: u64,
296}
297
298impl WgpuRenderer {
299 pub fn new(fonts: &[&'static [u8]]) -> Self {
306 Self::with_text_system(WgpuTextSystem::from_fonts(fonts))
307 }
308
309 pub fn with_font_set(fonts: SoftwareTextFontSet) -> Self {
314 Self::with_text_system(WgpuTextSystem::from_font_set(fonts))
315 }
316
317 pub fn with_text_system(text_system: WgpuTextSystem) -> Self {
318 Self {
319 frontend: RendererFrontend::new(
320 text_system.render_state(),
321 text_system.software_fonts(),
322 ),
323 backend: PresentBackend::None,
324 renderer_epoch: 0,
325 surface_epoch: 0,
326 }
327 }
328
329 fn sync_gpu_renderer(&self) -> Option<&GpuRenderer> {
330 match &self.backend {
331 PresentBackend::Sync(gpu_renderer) => Some(gpu_renderer.as_ref()),
332 _ => None,
333 }
334 }
335
336 #[cfg(not(target_arch = "wasm32"))]
337 fn present_handle_mut(&mut self) -> Option<&mut PresentHandle> {
338 match &mut self.backend {
339 PresentBackend::Threaded(handle) => Some(handle),
340 _ => None,
341 }
342 }
343
344 fn retire_live_backend(&mut self) {
345 #[allow(unused_mut)]
346 let mut backend = std::mem::replace(&mut self.backend, PresentBackend::None);
347 #[cfg(not(target_arch = "wasm32"))]
348 if let PresentBackend::Threaded(handle) = &mut backend {
349 while let Some(returns) = handle.try_drain() {
350 self.frontend.apply_returns(returns);
351 }
352 handle.shutdown();
353 }
354 drop(backend);
355 }
356
357 pub fn init_gpu(
363 &mut self,
364 device: Arc<wgpu::Device>,
365 queue: Arc<wgpu::Queue>,
366 surface_format: wgpu::TextureFormat,
367 adapter_backend: wgpu::Backend,
368 adapter_downlevel: wgpu::DownlevelFlags,
369 ) {
370 let target = GpuTarget {
371 device,
372 queue,
373 surface_format,
374 adapter_backend,
375 adapter_downlevel,
376 };
377 self.install_gpu(target, PipelineCompilation::Background);
378 }
379
380 #[doc(hidden)]
384 pub fn init_gpu_compiling_inline_for_tests(
385 &mut self,
386 device: Arc<wgpu::Device>,
387 queue: Arc<wgpu::Queue>,
388 surface_format: wgpu::TextureFormat,
389 adapter_backend: wgpu::Backend,
390 adapter_downlevel: wgpu::DownlevelFlags,
391 ) {
392 let target = GpuTarget {
393 device,
394 queue,
395 surface_format,
396 adapter_backend,
397 adapter_downlevel,
398 };
399 self.install_gpu(target, PipelineCompilation::Inline);
400 }
401
402 fn install_gpu(&mut self, target: GpuTarget, pipeline_compilation: PipelineCompilation) {
403 let init = self.next_gpu_init(target, pipeline_compilation);
404 self.backend = PresentBackend::Sync(Box::new(GpuRenderer::new(init)));
405 }
406
407 fn next_gpu_init(
410 &mut self,
411 target: GpuTarget,
412 pipeline_compilation: PipelineCompilation,
413 ) -> GpuRendererInit {
414 self.retire_live_backend();
415 self.renderer_epoch = self.renderer_epoch.wrapping_add(1);
416 GpuRendererInit {
417 device: target.device,
418 queue: target.queue,
419 surface_format: target.surface_format,
420 adapter_backend: target.adapter_backend,
421 adapter_downlevel: target.adapter_downlevel,
422 text_fonts: self.frontend.text_fonts.clone(),
423 renderer_epoch: self.renderer_epoch,
424 pipeline_compilation,
425 }
426 }
427
428 #[cfg(not(target_arch = "wasm32"))]
445 #[expect(clippy::too_many_arguments)]
446 pub fn init_gpu_threaded(
447 &mut self,
448 device: Arc<wgpu::Device>,
449 queue: Arc<wgpu::Queue>,
450 surface_format: wgpu::TextureFormat,
451 adapter_backend: wgpu::Backend,
452 adapter_downlevel: wgpu::DownlevelFlags,
453 waker: Arc<dyn Fn() + Send + Sync>,
454 clock: Option<Arc<dyn Fn() -> i64 + Send + Sync>>,
455 observer: Option<Box<dyn PresentObserver>>,
456 ) -> Result<(), WgpuRendererError> {
457 let target = GpuTarget {
458 device,
459 queue,
460 surface_format,
461 adapter_backend,
462 adapter_downlevel,
463 };
464 let init = PresentRuntimeInit {
465 gpu: self.next_gpu_init(target, PipelineCompilation::Background),
466 clock,
467 observer,
468 };
469 let handle = PresentHandle::spawn(init, waker).map_err(WgpuRendererError::Wgpu)?;
470 self.backend = PresentBackend::Threaded(handle);
471 Ok(())
472 }
473
474 #[cfg(not(target_arch = "wasm32"))]
475 #[doc(hidden)]
476 pub fn init_gpu_inline_for_tests(
477 &mut self,
478 device: Arc<wgpu::Device>,
479 queue: Arc<wgpu::Queue>,
480 surface_format: wgpu::TextureFormat,
481 adapter_backend: wgpu::Backend,
482 adapter_downlevel: wgpu::DownlevelFlags,
483 ) -> InlinePresentRuntime {
484 let target = GpuTarget {
485 device,
486 queue,
487 surface_format,
488 adapter_backend,
489 adapter_downlevel,
490 };
491 let init = PresentRuntimeInit {
492 gpu: self.next_gpu_init(target, PipelineCompilation::Background),
493 clock: None,
494 observer: None,
495 };
496 let (handle, state, msg_rx) = PresentHandle::new_inline(init, Arc::new(|| {}));
497 self.backend = PresentBackend::Threaded(handle);
498 InlinePresentRuntime {
499 state,
500 msg_rx,
501 shutdown_seen: false,
502 }
503 }
504
505 pub fn note_surface_reconfigured(&mut self) {
510 self.surface_epoch = self.surface_epoch.wrapping_add(1);
511 }
512
513 pub fn set_root_scale(&mut self, scale: f32) {
515 self.frontend.root_scale = scale;
516 }
517
518 pub fn set_transparent_background(&mut self, transparent: bool) {
523 self.frontend.transparent_background = transparent;
524 }
525
526 pub fn root_scale(&self) -> f32 {
527 self.frontend.root_scale
528 }
529
530 pub fn render(
537 &mut self,
538 texture: &wgpu::Texture,
539 view: &wgpu::TextureView,
540 width: u32,
541 height: u32,
542 ) -> Result<(), WgpuRendererError> {
543 self.render_frame(texture, view, width, height)
544 }
545
546 pub fn render_surface_texture(
550 &mut self,
551 texture: &wgpu::Texture,
552 view: &wgpu::TextureView,
553 width: u32,
554 height: u32,
555 ) -> Result<(), WgpuRendererError> {
556 self.render_frame(texture, view, width, height)
557 }
558
559 pub fn present(&self, frame: wgpu::SurfaceTexture) {
566 match self.sync_gpu_renderer() {
567 Some(gpu_renderer) => gpu_renderer.queue.present(frame),
568 None => log::debug!("surface image released: the renderer has no GPU queue"),
569 }
570 }
571
572 fn render_frame(
573 &mut self,
574 texture: &wgpu::Texture,
575 view: &wgpu::TextureView,
576 width: u32,
577 height: u32,
578 ) -> Result<(), WgpuRendererError> {
579 let PresentBackend::Sync(gpu_renderer) = &mut self.backend else {
580 return Err(WgpuRendererError::Wgpu(
581 "GPU renderer not initialized for synchronous rendering. Call init_gpu() first."
582 .to_string(),
583 ));
584 };
585 let packet = self
586 .frontend
587 .build_frame_packet(width, height, self.renderer_epoch, self.surface_epoch)
588 .ok_or_else(|| WgpuRendererError::Wgpu("scene graph is missing".to_string()))?;
589 let mut returns = RenderReturns::default();
590 let result = gpu_renderer.render(
591 texture,
592 view,
593 width,
594 height,
595 packet,
596 self.surface_epoch,
597 &mut returns,
598 );
599 self.frontend.apply_returns(returns);
600 result.map_err(WgpuRendererError::Wgpu)
601 }
602
603 pub fn capture_frame(
607 &mut self,
608 width: u32,
609 height: u32,
610 ) -> Result<CapturedFrame, WgpuRendererError> {
611 self.capture_frame_with_scale(width, height, self.frontend.root_scale)
612 }
613
614 #[cfg(not(target_arch = "wasm32"))]
623 pub fn prepare_recorded_pipelines(
624 &mut self,
625 timeout: std::time::Duration,
626 stop: &dyn Fn() -> bool,
627 ) -> bool {
628 let PresentBackend::Sync(gpu_renderer) = &mut self.backend else {
629 return false;
630 };
631 let deadline = web_time::Instant::now() + timeout;
632 let mut compiling = gpu_renderer.prepare_recorded();
633 while compiling {
634 if web_time::Instant::now() >= deadline || stop() {
635 return false;
636 }
637 std::thread::sleep(std::time::Duration::from_millis(10));
638 compiling = gpu_renderer.compiling();
639 }
640 true
641 }
642
643 pub fn capture_frame_with_scale(
644 &mut self,
645 width: u32,
646 height: u32,
647 root_scale: f32,
648 ) -> Result<CapturedFrame, WgpuRendererError> {
649 let PresentBackend::Sync(gpu_renderer) = &mut self.backend else {
650 return Err(WgpuRendererError::Wgpu(
651 "GPU renderer not initialized for synchronous rendering. Call init_gpu() first."
652 .to_string(),
653 ));
654 };
655 let packet = self
656 .frontend
657 .build_frame_packet_with_scale(
658 width,
659 height,
660 root_scale,
661 self.renderer_epoch,
662 self.surface_epoch,
663 )
664 .ok_or_else(|| WgpuRendererError::Wgpu("scene graph is missing".to_string()))?;
665 let mut returns = RenderReturns::default();
666 let result = gpu_renderer.render_to_rgba_pixels(
667 width,
668 height,
669 packet,
670 self.surface_epoch,
671 &mut returns,
672 );
673 self.frontend.apply_returns(returns);
674 let pixels = result.map_err(WgpuRendererError::Wgpu)?;
675 Ok(CapturedFrame {
676 width,
677 height,
678 pixels,
679 })
680 }
681
682 #[cfg(not(target_arch = "wasm32"))]
687 pub fn has_frame_credit(&self) -> bool {
688 match &self.backend {
689 PresentBackend::Threaded(handle) => handle.has_credit(),
690 PresentBackend::Sync(_) | PresentBackend::None => true,
691 }
692 }
693
694 #[cfg(not(target_arch = "wasm32"))]
700 pub fn frame_credit_returned(&self) -> bool {
701 match &self.backend {
702 PresentBackend::Threaded(handle) => handle.has_undrained_return(),
703 PresentBackend::Sync(_) | PresentBackend::None => false,
704 }
705 }
706
707 #[cfg(not(target_arch = "wasm32"))]
713 pub fn publish_frame(&mut self, width: u32, height: u32) -> PublishOutcome {
714 let PresentBackend::Threaded(handle) = &mut self.backend else {
715 log::error!("publish_frame called without a threaded present runtime");
716 return PublishOutcome::NoCredit;
717 };
718 if !handle.has_credit() {
719 return PublishOutcome::NoCredit;
720 }
721 let Some(packet) = self.frontend.build_frame_packet(
722 width,
723 height,
724 self.renderer_epoch,
725 self.surface_epoch,
726 ) else {
727 return PublishOutcome::NoGraph;
728 };
729 match handle.publish(packet) {
730 Ok(()) => PublishOutcome::Published,
731 Err(packet) => {
732 let mut returns = RenderReturns::default();
733 let _ = GpuRenderer::cancel_packet(
734 *packet,
735 CancelReason::SurfaceUnavailable,
736 &mut returns,
737 );
738 self.frontend.apply_returns(returns);
739 log::error!("present runtime unavailable; frame recovered, not published");
740 PublishOutcome::NoCredit
741 }
742 }
743 }
744
745 #[cfg(not(target_arch = "wasm32"))]
749 pub fn drain_present_returns(&mut self) -> usize {
750 self.drain_present_returns_with(&mut |_, _, _| {})
751 }
752
753 #[cfg(not(target_arch = "wasm32"))]
757 pub fn drain_present_returns_with(
758 &mut self,
759 on_return: &mut dyn FnMut(u64, PresentOutcome, PresentTimings),
760 ) -> usize {
761 let mut drained = 0;
762 loop {
763 let returns = {
764 let PresentBackend::Threaded(handle) = &mut self.backend else {
765 break;
766 };
767 match handle.try_drain() {
768 Some(returns) => returns,
769 None => break,
770 }
771 };
772 drained += 1;
773 let frame_id = returns.frame_id;
774 let outcome = returns.outcome;
775 let timings = returns.timings;
776 self.frontend.apply_returns(returns);
777 on_return(frame_id, outcome, timings);
778 }
779 drained
780 }
781
782 #[cfg(not(target_arch = "wasm32"))]
791 pub fn present_replace_surface(
792 &mut self,
793 surface: wgpu::Surface<'static>,
794 config: wgpu::SurfaceConfiguration,
795 ) -> bool {
796 let surface_epoch = self.surface_epoch;
797 let Some(handle) = self.present_handle_mut() else {
798 log::error!("present_replace_surface called without a threaded present runtime");
799 return false;
800 };
801 let sent = handle.send_control(PresentControl::ReplaceSurface {
802 surface,
803 config,
804 surface_epoch,
805 });
806 if !sent {
807 log::error!("[present-runtime] replace surface: runtime is gone");
808 }
809 sent
810 }
811
812 #[cfg(not(target_arch = "wasm32"))]
816 pub fn present_reconfigure(&mut self, config: wgpu::SurfaceConfiguration) -> bool {
817 let surface_epoch = self.surface_epoch;
818 let Some(handle) = self.present_handle_mut() else {
819 log::error!("present_reconfigure called without a threaded present runtime");
820 return false;
821 };
822 handle.send_control_and_wait(
823 move |ack| PresentControl::Reconfigure {
824 config,
825 surface_epoch,
826 ack,
827 },
828 "reconfigure surface",
829 )
830 }
831
832 #[cfg(not(target_arch = "wasm32"))]
836 pub fn present_drop_surface(&mut self) -> bool {
837 let Some(handle) = self.present_handle_mut() else {
838 log::error!("present_drop_surface called without a threaded present runtime");
839 return false;
840 };
841 handle.send_control_and_wait(|ack| PresentControl::DropSurface { ack }, "drop surface")
842 }
843
844 #[cfg(not(target_arch = "wasm32"))]
847 pub fn shutdown_present_runtime(&mut self) {
848 if matches!(self.backend, PresentBackend::Threaded(_)) {
849 self.retire_live_backend();
850 }
851 }
852
853 #[cfg(not(target_arch = "wasm32"))]
854 #[doc(hidden)]
855 pub fn present_attach_offscreen_for_tests(&mut self, width: u32, height: u32) -> bool {
856 let Some(handle) = self.present_handle_mut() else {
857 return false;
858 };
859 handle.send_control_and_wait(
860 move |ack| PresentControl::AttachOffscreenTargetForTests { width, height, ack },
861 "attach offscreen target",
862 )
863 }
864
865 #[cfg(not(target_arch = "wasm32"))]
866 #[doc(hidden)]
867 pub fn send_attach_offscreen_unacked_for_tests(
868 &mut self,
869 width: u32,
870 height: u32,
871 ) -> Option<std::sync::mpsc::Receiver<()>> {
872 let handle = self.present_handle_mut()?;
873 handle.send_control_unacked(move |ack| PresentControl::AttachOffscreenTargetForTests {
874 width,
875 height,
876 ack,
877 })
878 }
879
880 #[cfg(not(target_arch = "wasm32"))]
881 #[doc(hidden)]
882 pub fn send_reconfigure_unacked_for_tests(
883 &mut self,
884 config: wgpu::SurfaceConfiguration,
885 ) -> Option<std::sync::mpsc::Receiver<()>> {
886 let surface_epoch = self.surface_epoch;
887 let handle = self.present_handle_mut()?;
888 handle.send_control_unacked(move |ack| PresentControl::Reconfigure {
889 config,
890 surface_epoch,
891 ack,
892 })
893 }
894
895 #[cfg(not(target_arch = "wasm32"))]
896 #[doc(hidden)]
897 pub fn send_drop_surface_unacked_for_tests(&mut self) -> Option<std::sync::mpsc::Receiver<()>> {
898 let handle = self.present_handle_mut()?;
899 handle.send_control_unacked(|ack| PresentControl::DropSurface { ack })
900 }
901
902 pub fn present_thread_id(&self) -> Option<i32> {
906 match &self.backend {
907 #[cfg(not(target_arch = "wasm32"))]
908 PresentBackend::Threaded(handle) => {
909 let id = handle
910 .status()
911 .thread_id
912 .load(std::sync::atomic::Ordering::Relaxed);
913 (id != 0).then_some(id)
914 }
915 _ => None,
916 }
917 }
918
919 pub fn last_published_frame_id(&self) -> u64 {
924 self.frontend.frame_sequence
925 }
926
927 #[cfg(not(target_arch = "wasm32"))]
928 #[doc(hidden)]
929 pub fn present_status_snapshot_for_tests(&self) -> Option<(bool, u64, u64)> {
930 match &self.backend {
931 PresentBackend::Threaded(handle) => {
932 let status = handle.status();
933 Some((
934 status
935 .needs_frame_warmup
936 .load(std::sync::atomic::Ordering::Relaxed),
937 status
938 .presented_frames
939 .load(std::sync::atomic::Ordering::Relaxed),
940 status
941 .placeholder_frames
942 .load(std::sync::atomic::Ordering::Relaxed),
943 ))
944 }
945 _ => None,
946 }
947 }
948
949 pub fn last_frame_stats(&self) -> Option<RenderStatsSnapshot> {
950 match &self.backend {
951 PresentBackend::Sync(gpu_renderer) => gpu_renderer.last_frame_stats(),
952 #[cfg(not(target_arch = "wasm32"))]
953 PresentBackend::Threaded(handle) => *handle
954 .status()
955 .last_frame_stats
956 .lock()
957 .unwrap_or_else(std::sync::PoisonError::into_inner),
958 PresentBackend::None => None,
959 }
960 }
961
962 pub fn gpu_pass_timings(&self) -> GpuPassTimingReport {
966 self.sync_gpu_renderer()
967 .map(GpuRenderer::gpu_pass_timings)
968 .unwrap_or_default()
969 }
970
971 pub fn debug_cpu_allocation_stats(&self) -> DebugCpuAllocationStats {
972 let mut stats = self
973 .sync_gpu_renderer()
974 .map(GpuRenderer::debug_cpu_allocation_stats)
975 .unwrap_or_default();
976 stats.scene_graph_node_count = self
977 .frontend
978 .scene
979 .graph
980 .as_ref()
981 .map_or(0, RenderGraph::node_count);
982 stats.scene_graph_heap_bytes = self
983 .frontend
984 .scene
985 .graph
986 .as_ref()
987 .map_or(0, RenderGraph::heap_bytes);
988 stats.scene_hits_len = self.frontend.scene.hits.len();
989 stats.scene_hits_cap = self.frontend.scene.hits.capacity();
990 stats.scene_node_index_len = self.frontend.scene.node_index.len();
991 stats.scene_node_index_cap = self.frontend.scene.node_index.capacity();
992 stats
993 }
994
995 pub fn try_device(&self) -> Option<&wgpu::Device> {
999 self.sync_gpu_renderer().map(|r| &*r.device)
1000 }
1001
1002 #[doc(hidden)]
1003 pub fn try_queue_for_tests(&self) -> Option<&wgpu::Queue> {
1004 self.sync_gpu_renderer().map(|r| &*r.queue)
1005 }
1006
1007 #[doc(hidden)]
1008 pub fn device_error_count_for_tests(&self) -> u64 {
1009 self.sync_gpu_renderer()
1010 .map_or(0, GpuRenderer::device_error_count)
1011 }
1012
1013 #[doc(hidden)]
1014 pub fn build_frame_packet_for_tests(
1015 &mut self,
1016 width: u32,
1017 height: u32,
1018 ) -> Option<HeldFramePacket> {
1019 self.frontend
1020 .build_frame_packet(width, height, self.renderer_epoch, self.surface_epoch)
1021 .map(HeldFramePacket)
1022 }
1023
1024 #[doc(hidden)]
1036 pub fn return_held_packet_for_tests(&mut self, packet: HeldFramePacket) {
1037 let packet = packet.0;
1038 self.frontend.apply_returns(RenderReturns {
1039 scene: Some(frame_packet::FrameSceneStorage {
1040 root: packet.root,
1041 overlay: packet.overlay,
1042 }),
1043 frame_id: packet.frame_id,
1044 ..RenderReturns::default()
1045 });
1046 }
1047
1048 #[doc(hidden)]
1049 pub fn render_held_packet_for_tests(
1050 &mut self,
1051 texture: &wgpu::Texture,
1052 view: &wgpu::TextureView,
1053 width: u32,
1054 height: u32,
1055 packet: HeldFramePacket,
1056 ) -> Result<PresentOutcome, WgpuRendererError> {
1057 let PresentBackend::Sync(gpu_renderer) = &mut self.backend else {
1058 return Err(WgpuRendererError::Wgpu(
1059 "GPU renderer not initialized for synchronous rendering. Call init_gpu() first."
1060 .to_string(),
1061 ));
1062 };
1063 let mut returns = RenderReturns::default();
1064 let result = gpu_renderer.render(
1065 texture,
1066 view,
1067 width,
1068 height,
1069 packet.0,
1070 self.surface_epoch,
1071 &mut returns,
1072 );
1073 let outcome = returns.outcome;
1074 self.frontend.apply_returns(returns);
1075 result.map_err(WgpuRendererError::Wgpu)?;
1076 Ok(outcome)
1077 }
1078}
1079
1080#[doc(hidden)]
1081pub struct HeldFramePacket(frame_packet::FramePacket);
1082
1083#[cfg(not(target_arch = "wasm32"))]
1084#[doc(hidden)]
1085pub struct InlinePresentRuntime {
1086 state: PresentState,
1087 msg_rx: std::sync::mpsc::Receiver<PresentMsg>,
1088 shutdown_seen: bool,
1089}
1090
1091#[cfg(not(target_arch = "wasm32"))]
1092impl InlinePresentRuntime {
1093 pub fn pump(&mut self) -> bool {
1094 if self.shutdown_seen {
1095 return false;
1096 }
1097 while let Ok(msg) = self.msg_rx.try_recv() {
1098 if !self.state.run_once(msg) {
1099 self.shutdown_seen = true;
1100 return false;
1101 }
1102 }
1103 self.state.consume_waiting();
1104 true
1105 }
1106
1107 pub fn has_waiting_packet(&self) -> bool {
1108 self.state.has_waiting_packet()
1109 }
1110
1111 pub fn step_one_message(&mut self) -> bool {
1112 if self.shutdown_seen {
1113 return false;
1114 }
1115 match self.msg_rx.try_recv() {
1116 Ok(msg) => {
1117 if !self.state.run_once(msg) {
1118 self.shutdown_seen = true;
1119 }
1120 true
1121 }
1122 Err(_) => false,
1123 }
1124 }
1125
1126 pub fn consume_waiting(&mut self) {
1127 self.state.consume_waiting();
1128 }
1129}
1130
1131impl Default for WgpuRenderer {
1132 fn default() -> Self {
1133 Self::new(&[])
1134 }
1135}
1136
1137impl Renderer for WgpuRenderer {
1138 type Scene = Scene;
1139 type Error = WgpuRendererError;
1140
1141 fn attach_app_context_services(&mut self, app_context: &cranpose_ui::AppContext) {
1142 app_context.set_text_measurer(SoftwareTextMeasurer::from_font_set(
1143 self.frontend.text_fonts.clone(),
1144 8192,
1145 ));
1146 self.frontend.app_context = Some(app_context.downgrade());
1147 }
1148
1149 fn scene(&self) -> &Self::Scene {
1150 &self.frontend.scene
1151 }
1152
1153 fn scene_mut(&mut self) -> &mut Self::Scene {
1154 &mut self.frontend.scene
1155 }
1156
1157 fn rebuild_scene(
1158 &mut self,
1159 layout_tree: &LayoutTree,
1160 _viewport: Size,
1161 ) -> Result<(), Self::Error> {
1162 self.frontend.scene.clear();
1163 self.frontend.clear_fps_overlay();
1164 pipeline::render_layout_tree(layout_tree.root(), &mut self.frontend.scene);
1165 Ok(())
1166 }
1167
1168 fn rebuild_scene_from_applier(
1169 &mut self,
1170 applier: &mut MemoryApplier,
1171 root: NodeId,
1172 _viewport: Size,
1173 ) -> Result<(), Self::Error> {
1174 self.frontend.clear_fps_overlay();
1175 self.frontend.scene.rebuild_from_applier(applier, root);
1176 Ok(())
1177 }
1178
1179 fn update_scene_from_applier(
1180 &mut self,
1181 applier: &mut MemoryApplier,
1182 root: NodeId,
1183 _viewport: Size,
1184 updates: cranpose_render_common::SceneUpdates<'_>,
1185 ) -> Result<(), Self::Error> {
1186 if updates.is_empty() {
1187 self.frontend.clear_fps_overlay();
1188 }
1189 self.frontend
1190 .scene
1191 .update_from_applier(applier, root, updates, true);
1192 Ok(())
1193 }
1194
1195 fn update_visual_scene_from_applier(
1196 &mut self,
1197 applier: &mut MemoryApplier,
1198 root: NodeId,
1199 _viewport: Size,
1200 updates: cranpose_render_common::SceneUpdates<'_>,
1201 ) -> Result<(), Self::Error> {
1202 if updates.is_empty() {
1203 self.frontend.clear_fps_overlay();
1204 }
1205 self.frontend
1206 .scene
1207 .update_from_applier(applier, root, updates, false);
1208 Ok(())
1209 }
1210
1211 fn draw_dev_overlay(&mut self, text: &str, viewport: Size) {
1212 const DEV_OVERLAY_NODE_ID: NodeId = NodeId::MAX;
1213 let key = cranpose_render_common::dev_overlay::DevOverlayKey::new(text, viewport);
1214 if self.frontend.dev_overlay_graph.is_some()
1215 && self
1216 .frontend
1217 .dev_overlay_cache
1218 .as_ref()
1219 .is_some_and(|cache| {
1220 cache.text == key.text
1221 && cache.viewport_width_bits == key.viewport_width_bits
1222 && cache.viewport_height_bits == key.viewport_height_bits
1223 })
1224 {
1225 return;
1226 }
1227 self.frontend.fps_overlay_graph = Some(
1228 cranpose_render_common::dev_overlay::build_dev_overlay_graph(
1229 text,
1230 viewport,
1231 DEV_OVERLAY_NODE_ID,
1232 ),
1233 );
1234 self.frontend.dev_overlay_cache = Some(DevOverlayCache {
1235 text: key.text,
1236 viewport_width_bits: key.viewport_width_bits,
1237 viewport_height_bits: key.viewport_height_bits,
1238 });
1239 self.frontend.refresh_dev_overlay();
1240 }
1241
1242 fn set_inspector_overlay(&mut self, graph: Option<cranpose_render_common::graph::RenderGraph>) {
1243 self.frontend.inspector_overlay_graph = graph;
1244 self.frontend.refresh_dev_overlay();
1245 }
1246
1247 fn needs_frame_warmup(&self) -> bool {
1248 match &self.backend {
1249 PresentBackend::Sync(gpu_renderer) => gpu_renderer.needs_frame_warmup(),
1250 #[cfg(not(target_arch = "wasm32"))]
1251 PresentBackend::Threaded(handle) => handle
1252 .status()
1253 .needs_frame_warmup
1254 .load(std::sync::atomic::Ordering::Relaxed),
1255 PresentBackend::None => false,
1256 }
1257 }
1258
1259 fn awaits_pipelines(&self) -> bool {
1260 match &self.backend {
1261 PresentBackend::Sync(gpu_renderer) => gpu_renderer.awaits_pipelines(),
1262 #[cfg(not(target_arch = "wasm32"))]
1263 PresentBackend::Threaded(handle) => {
1264 let status = handle.status();
1265 status
1266 .drew_placeholder
1267 .load(std::sync::atomic::Ordering::Relaxed)
1268 && status
1269 .landing
1270 .get()
1271 .is_some_and(|landing| landing.in_flight())
1272 }
1273 PresentBackend::None => false,
1274 }
1275 }
1276}
1277
1278#[cfg(test)]
1279#[path = "tests/wgpu_tests.rs"]
1280mod tests;