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 {
110 adapter.features() & (wgpu::Features::PIPELINE_CACHE | wgpu::Features::TIMESTAMP_QUERY)
111}
112
113#[doc(hidden)]
114pub fn offscreen_render_target_for_tests(
115 device: &wgpu::Device,
116 width: u32,
117 height: u32,
118 label: &str,
119) -> (wgpu::Texture, wgpu::TextureView) {
120 let texture = offscreen::create_2d_texture(
121 device,
122 wgpu::TextureFormat::Rgba8Unorm,
123 width,
124 height,
125 wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
126 Some(label),
127 );
128 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
129 (texture, view)
130}
131
132pub(crate) fn rect_to_quad(rect: Rect) -> [[f32; 2]; 4] {
133 [
134 [rect.x, rect.y],
135 [rect.x + rect.width, rect.y],
136 [rect.x, rect.y + rect.height],
137 [rect.x + rect.width, rect.y + rect.height],
138 ]
139}
140
141#[derive(Debug)]
142pub enum WgpuRendererError {
143 Layout(String),
144 Wgpu(String),
145}
146
147#[derive(Debug, Clone)]
149pub struct CapturedFrame {
150 pub width: u32,
151 pub height: u32,
152 pub pixels: Vec<u8>,
153}
154
155#[doc(hidden)]
156#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
157pub struct DebugCpuAllocationStats {
158 pub scene_graph_node_count: usize,
159 pub scene_graph_heap_bytes: usize,
160 pub scene_hits_len: usize,
161 pub scene_hits_cap: usize,
162 pub scene_node_index_len: usize,
163 pub scene_node_index_cap: usize,
164 pub text_renderer_pool_len: usize,
165 pub text_renderer_pool_cap: usize,
166 pub image_texture_cache_len: usize,
167 pub image_texture_cache_cap: usize,
168 pub run_arena_staging_bytes: usize,
169 pub run_store_bytes: usize,
170 pub run_store_runs: usize,
171 pub scratch_image_vertices_cap: usize,
172 pub scratch_image_indices_cap: usize,
173 pub scratch_image_cmds_cap: usize,
174 pub scratch_glyph_instances_cap: usize,
175 pub layer_cache_len: usize,
176 pub layer_cache_bytes: u64,
177}
178
179struct GpuTarget {
181 device: Arc<wgpu::Device>,
182 queue: Arc<wgpu::Queue>,
183 surface_format: wgpu::TextureFormat,
184 adapter_backend: wgpu::Backend,
185 adapter_downlevel: wgpu::DownlevelFlags,
186}
187
188pub(crate) struct TextSystemState {
189 measurer: SoftwareTextMeasurer,
190}
191
192impl TextSystemState {
193 fn from_font_set(fonts: SoftwareTextFontSet) -> Self {
194 Self {
195 measurer: SoftwareTextMeasurer::from_font_set(fonts, 8192),
196 }
197 }
198
199 pub(crate) fn text_cache_len(&self) -> usize {
200 0
201 }
202}
203
204impl pipeline::TextLayoutResolver for TextSystemState {
205 fn layout_text(
206 &mut self,
207 text: &cranpose_ui::text::AnnotatedString,
208 style: &cranpose_ui::text::TextStyle,
209 ) -> cranpose_ui::text_layout_result::TextLayoutResult {
210 if cranpose_ui::has_current_app_context() {
211 cranpose_ui::text::layout_text(text, style)
212 } else {
213 self.measurer.layout(text, style)
214 }
215 }
216}
217
218#[derive(Clone)]
219pub struct WgpuTextSystem {
220 software_fonts: SoftwareTextFontSet,
221}
222
223impl WgpuTextSystem {
224 pub fn from_fonts(fonts: &[&'static [u8]]) -> Self {
225 Self {
226 software_fonts: software_text_font_set_from_fonts_or_default(fonts),
227 }
228 }
229
230 pub fn from_font_set(software_fonts: SoftwareTextFontSet) -> Self {
234 Self { software_fonts }
235 }
236
237 pub(crate) fn render_state(&self) -> TextSystemState {
238 TextSystemState::from_font_set(self.software_fonts.clone())
239 }
240
241 pub(crate) fn software_fonts(&self) -> SoftwareTextFontSet {
242 self.software_fonts.clone()
243 }
244}
245
246pub fn headless_text_measurer() -> Rc<dyn TextMeasurer> {
248 headless_text_measurer_with_fonts(&[])
249}
250
251pub fn headless_text_measurer_with_fonts(fonts: &[&'static [u8]]) -> Rc<dyn TextMeasurer> {
253 Rc::new(SoftwareTextMeasurer::from_fonts_or_default(fonts, 8192))
254}
255
256enum PresentBackend {
257 None,
258 Sync(Box<GpuRenderer>),
259 #[cfg(not(target_arch = "wasm32"))]
260 Threaded(PresentHandle),
261}
262
263#[derive(Clone, Copy, Debug, PartialEq, Eq)]
265pub enum PublishOutcome {
266 NoGraph,
268 NoCredit,
271 Published,
273}
274
275pub struct WgpuRenderer {
283 frontend: RendererFrontend,
284 backend: PresentBackend,
285 renderer_epoch: u64,
286 surface_epoch: u64,
287}
288
289impl WgpuRenderer {
290 pub fn new(fonts: &[&'static [u8]]) -> Self {
297 Self::with_text_system(WgpuTextSystem::from_fonts(fonts))
298 }
299
300 pub fn with_font_set(fonts: SoftwareTextFontSet) -> Self {
305 Self::with_text_system(WgpuTextSystem::from_font_set(fonts))
306 }
307
308 pub fn with_text_system(text_system: WgpuTextSystem) -> Self {
309 Self {
310 frontend: RendererFrontend::new(
311 text_system.render_state(),
312 text_system.software_fonts(),
313 ),
314 backend: PresentBackend::None,
315 renderer_epoch: 0,
316 surface_epoch: 0,
317 }
318 }
319
320 fn sync_gpu_renderer(&self) -> Option<&GpuRenderer> {
321 match &self.backend {
322 PresentBackend::Sync(gpu_renderer) => Some(gpu_renderer.as_ref()),
323 _ => None,
324 }
325 }
326
327 #[cfg(not(target_arch = "wasm32"))]
328 fn present_handle_mut(&mut self) -> Option<&mut PresentHandle> {
329 match &mut self.backend {
330 PresentBackend::Threaded(handle) => Some(handle),
331 _ => None,
332 }
333 }
334
335 fn retire_live_backend(&mut self) {
336 #[allow(unused_mut)]
337 let mut backend = std::mem::replace(&mut self.backend, PresentBackend::None);
338 #[cfg(not(target_arch = "wasm32"))]
339 if let PresentBackend::Threaded(handle) = &mut backend {
340 while let Some(returns) = handle.try_drain() {
341 self.frontend.apply_returns(returns);
342 }
343 handle.shutdown();
344 }
345 drop(backend);
346 }
347
348 pub fn init_gpu(
354 &mut self,
355 device: Arc<wgpu::Device>,
356 queue: Arc<wgpu::Queue>,
357 surface_format: wgpu::TextureFormat,
358 adapter_backend: wgpu::Backend,
359 adapter_downlevel: wgpu::DownlevelFlags,
360 ) {
361 let target = GpuTarget {
362 device,
363 queue,
364 surface_format,
365 adapter_backend,
366 adapter_downlevel,
367 };
368 self.install_gpu(target, PipelineCompilation::Background);
369 }
370
371 #[doc(hidden)]
375 pub fn init_gpu_compiling_inline_for_tests(
376 &mut self,
377 device: Arc<wgpu::Device>,
378 queue: Arc<wgpu::Queue>,
379 surface_format: wgpu::TextureFormat,
380 adapter_backend: wgpu::Backend,
381 adapter_downlevel: wgpu::DownlevelFlags,
382 ) {
383 let target = GpuTarget {
384 device,
385 queue,
386 surface_format,
387 adapter_backend,
388 adapter_downlevel,
389 };
390 self.install_gpu(target, PipelineCompilation::Inline);
391 }
392
393 fn install_gpu(&mut self, target: GpuTarget, pipeline_compilation: PipelineCompilation) {
394 let init = self.next_gpu_init(target, pipeline_compilation);
395 self.backend = PresentBackend::Sync(Box::new(GpuRenderer::new(init)));
396 }
397
398 fn next_gpu_init(
401 &mut self,
402 target: GpuTarget,
403 pipeline_compilation: PipelineCompilation,
404 ) -> GpuRendererInit {
405 self.retire_live_backend();
406 self.renderer_epoch = self.renderer_epoch.wrapping_add(1);
407 GpuRendererInit {
408 device: target.device,
409 queue: target.queue,
410 surface_format: target.surface_format,
411 adapter_backend: target.adapter_backend,
412 adapter_downlevel: target.adapter_downlevel,
413 text_fonts: self.frontend.text_fonts.clone(),
414 renderer_epoch: self.renderer_epoch,
415 pipeline_compilation,
416 }
417 }
418
419 #[cfg(not(target_arch = "wasm32"))]
436 #[expect(clippy::too_many_arguments)]
437 pub fn init_gpu_threaded(
438 &mut self,
439 device: Arc<wgpu::Device>,
440 queue: Arc<wgpu::Queue>,
441 surface_format: wgpu::TextureFormat,
442 adapter_backend: wgpu::Backend,
443 adapter_downlevel: wgpu::DownlevelFlags,
444 waker: Arc<dyn Fn() + Send + Sync>,
445 clock: Option<Arc<dyn Fn() -> i64 + Send + Sync>>,
446 observer: Option<Box<dyn PresentObserver>>,
447 ) -> Result<(), WgpuRendererError> {
448 let target = GpuTarget {
449 device,
450 queue,
451 surface_format,
452 adapter_backend,
453 adapter_downlevel,
454 };
455 let init = PresentRuntimeInit {
456 gpu: self.next_gpu_init(target, PipelineCompilation::Background),
457 clock,
458 observer,
459 };
460 let handle = PresentHandle::spawn(init, waker).map_err(WgpuRendererError::Wgpu)?;
461 self.backend = PresentBackend::Threaded(handle);
462 Ok(())
463 }
464
465 #[cfg(not(target_arch = "wasm32"))]
466 #[doc(hidden)]
467 pub fn init_gpu_inline_for_tests(
468 &mut self,
469 device: Arc<wgpu::Device>,
470 queue: Arc<wgpu::Queue>,
471 surface_format: wgpu::TextureFormat,
472 adapter_backend: wgpu::Backend,
473 adapter_downlevel: wgpu::DownlevelFlags,
474 ) -> InlinePresentRuntime {
475 let target = GpuTarget {
476 device,
477 queue,
478 surface_format,
479 adapter_backend,
480 adapter_downlevel,
481 };
482 let init = PresentRuntimeInit {
483 gpu: self.next_gpu_init(target, PipelineCompilation::Background),
484 clock: None,
485 observer: None,
486 };
487 let (handle, state, msg_rx) = PresentHandle::new_inline(init, Arc::new(|| {}));
488 self.backend = PresentBackend::Threaded(handle);
489 InlinePresentRuntime {
490 state,
491 msg_rx,
492 shutdown_seen: false,
493 }
494 }
495
496 pub fn note_surface_reconfigured(&mut self) {
501 self.surface_epoch = self.surface_epoch.wrapping_add(1);
502 }
503
504 pub fn set_root_scale(&mut self, scale: f32) {
506 self.frontend.root_scale = scale;
507 }
508
509 pub fn set_transparent_background(&mut self, transparent: bool) {
514 self.frontend.transparent_background = transparent;
515 }
516
517 pub fn root_scale(&self) -> f32 {
518 self.frontend.root_scale
519 }
520
521 pub fn render(
528 &mut self,
529 texture: &wgpu::Texture,
530 view: &wgpu::TextureView,
531 width: u32,
532 height: u32,
533 ) -> Result<(), WgpuRendererError> {
534 self.render_frame(texture, view, width, height)
535 }
536
537 pub fn render_surface_texture(
541 &mut self,
542 texture: &wgpu::Texture,
543 view: &wgpu::TextureView,
544 width: u32,
545 height: u32,
546 ) -> Result<(), WgpuRendererError> {
547 self.render_frame(texture, view, width, height)
548 }
549
550 pub fn present(&self, frame: wgpu::SurfaceTexture) {
557 match self.sync_gpu_renderer() {
558 Some(gpu_renderer) => gpu_renderer.queue.present(frame),
559 None => log::debug!("surface image released: the renderer has no GPU queue"),
560 }
561 }
562
563 fn render_frame(
564 &mut self,
565 texture: &wgpu::Texture,
566 view: &wgpu::TextureView,
567 width: u32,
568 height: u32,
569 ) -> Result<(), WgpuRendererError> {
570 let PresentBackend::Sync(gpu_renderer) = &mut self.backend else {
571 return Err(WgpuRendererError::Wgpu(
572 "GPU renderer not initialized for synchronous rendering. Call init_gpu() first."
573 .to_string(),
574 ));
575 };
576 let packet = self
577 .frontend
578 .build_frame_packet(width, height, self.renderer_epoch, self.surface_epoch)
579 .ok_or_else(|| WgpuRendererError::Wgpu("scene graph is missing".to_string()))?;
580 let mut returns = RenderReturns::default();
581 let result = gpu_renderer.render(
582 texture,
583 view,
584 width,
585 height,
586 packet,
587 self.surface_epoch,
588 &mut returns,
589 );
590 self.frontend.apply_returns(returns);
591 result.map_err(WgpuRendererError::Wgpu)
592 }
593
594 pub fn capture_frame(
598 &mut self,
599 width: u32,
600 height: u32,
601 ) -> Result<CapturedFrame, WgpuRendererError> {
602 self.capture_frame_with_scale(width, height, self.frontend.root_scale)
603 }
604
605 #[cfg(not(target_arch = "wasm32"))]
614 pub fn prepare_recorded_pipelines(
615 &mut self,
616 timeout: std::time::Duration,
617 stop: &dyn Fn() -> bool,
618 ) -> bool {
619 let PresentBackend::Sync(gpu_renderer) = &mut self.backend else {
620 return false;
621 };
622 let deadline = web_time::Instant::now() + timeout;
623 let mut compiling = gpu_renderer.prepare_recorded();
624 while compiling {
625 if web_time::Instant::now() >= deadline || stop() {
626 return false;
627 }
628 std::thread::sleep(std::time::Duration::from_millis(10));
629 compiling = gpu_renderer.compiling();
630 }
631 true
632 }
633
634 pub fn capture_frame_with_scale(
635 &mut self,
636 width: u32,
637 height: u32,
638 root_scale: f32,
639 ) -> Result<CapturedFrame, WgpuRendererError> {
640 let PresentBackend::Sync(gpu_renderer) = &mut self.backend else {
641 return Err(WgpuRendererError::Wgpu(
642 "GPU renderer not initialized for synchronous rendering. Call init_gpu() first."
643 .to_string(),
644 ));
645 };
646 let packet = self
647 .frontend
648 .build_frame_packet_with_scale(
649 width,
650 height,
651 root_scale,
652 self.renderer_epoch,
653 self.surface_epoch,
654 )
655 .ok_or_else(|| WgpuRendererError::Wgpu("scene graph is missing".to_string()))?;
656 let mut returns = RenderReturns::default();
657 let result = gpu_renderer.render_to_rgba_pixels(
658 width,
659 height,
660 packet,
661 self.surface_epoch,
662 &mut returns,
663 );
664 self.frontend.apply_returns(returns);
665 let pixels = result.map_err(WgpuRendererError::Wgpu)?;
666 Ok(CapturedFrame {
667 width,
668 height,
669 pixels,
670 })
671 }
672
673 #[cfg(not(target_arch = "wasm32"))]
678 pub fn has_frame_credit(&self) -> bool {
679 match &self.backend {
680 PresentBackend::Threaded(handle) => handle.has_credit(),
681 PresentBackend::Sync(_) | PresentBackend::None => true,
682 }
683 }
684
685 #[cfg(not(target_arch = "wasm32"))]
691 pub fn frame_credit_returned(&self) -> bool {
692 match &self.backend {
693 PresentBackend::Threaded(handle) => handle.has_undrained_return(),
694 PresentBackend::Sync(_) | PresentBackend::None => false,
695 }
696 }
697
698 #[cfg(not(target_arch = "wasm32"))]
704 pub fn publish_frame(&mut self, width: u32, height: u32) -> PublishOutcome {
705 let PresentBackend::Threaded(handle) = &mut self.backend else {
706 log::error!("publish_frame called without a threaded present runtime");
707 return PublishOutcome::NoCredit;
708 };
709 if !handle.has_credit() {
710 return PublishOutcome::NoCredit;
711 }
712 let Some(packet) = self.frontend.build_frame_packet(
713 width,
714 height,
715 self.renderer_epoch,
716 self.surface_epoch,
717 ) else {
718 return PublishOutcome::NoGraph;
719 };
720 match handle.publish(packet) {
721 Ok(()) => PublishOutcome::Published,
722 Err(packet) => {
723 let mut returns = RenderReturns::default();
724 let _ = GpuRenderer::cancel_packet(
725 *packet,
726 CancelReason::SurfaceUnavailable,
727 &mut returns,
728 );
729 self.frontend.apply_returns(returns);
730 log::error!("present runtime unavailable; frame recovered, not published");
731 PublishOutcome::NoCredit
732 }
733 }
734 }
735
736 #[cfg(not(target_arch = "wasm32"))]
740 pub fn drain_present_returns(&mut self) -> usize {
741 self.drain_present_returns_with(&mut |_, _, _| {})
742 }
743
744 #[cfg(not(target_arch = "wasm32"))]
748 pub fn drain_present_returns_with(
749 &mut self,
750 on_return: &mut dyn FnMut(u64, PresentOutcome, PresentTimings),
751 ) -> usize {
752 let mut drained = 0;
753 loop {
754 let returns = {
755 let PresentBackend::Threaded(handle) = &mut self.backend else {
756 break;
757 };
758 match handle.try_drain() {
759 Some(returns) => returns,
760 None => break,
761 }
762 };
763 drained += 1;
764 let frame_id = returns.frame_id;
765 let outcome = returns.outcome;
766 let timings = returns.timings;
767 self.frontend.apply_returns(returns);
768 on_return(frame_id, outcome, timings);
769 }
770 drained
771 }
772
773 #[cfg(not(target_arch = "wasm32"))]
782 pub fn present_replace_surface(
783 &mut self,
784 surface: wgpu::Surface<'static>,
785 config: wgpu::SurfaceConfiguration,
786 ) -> bool {
787 let surface_epoch = self.surface_epoch;
788 let Some(handle) = self.present_handle_mut() else {
789 log::error!("present_replace_surface called without a threaded present runtime");
790 return false;
791 };
792 let sent = handle.send_control(PresentControl::ReplaceSurface {
793 surface,
794 config,
795 surface_epoch,
796 });
797 if !sent {
798 log::error!("[present-runtime] replace surface: runtime is gone");
799 }
800 sent
801 }
802
803 #[cfg(not(target_arch = "wasm32"))]
807 pub fn present_reconfigure(&mut self, config: wgpu::SurfaceConfiguration) -> bool {
808 let surface_epoch = self.surface_epoch;
809 let Some(handle) = self.present_handle_mut() else {
810 log::error!("present_reconfigure called without a threaded present runtime");
811 return false;
812 };
813 handle.send_control_and_wait(
814 move |ack| PresentControl::Reconfigure {
815 config,
816 surface_epoch,
817 ack,
818 },
819 "reconfigure surface",
820 )
821 }
822
823 #[cfg(not(target_arch = "wasm32"))]
827 pub fn present_drop_surface(&mut self) -> bool {
828 let Some(handle) = self.present_handle_mut() else {
829 log::error!("present_drop_surface called without a threaded present runtime");
830 return false;
831 };
832 handle.send_control_and_wait(|ack| PresentControl::DropSurface { ack }, "drop surface")
833 }
834
835 #[cfg(not(target_arch = "wasm32"))]
838 pub fn shutdown_present_runtime(&mut self) {
839 if matches!(self.backend, PresentBackend::Threaded(_)) {
840 self.retire_live_backend();
841 }
842 }
843
844 #[cfg(not(target_arch = "wasm32"))]
845 #[doc(hidden)]
846 pub fn present_attach_offscreen_for_tests(&mut self, width: u32, height: u32) -> bool {
847 let Some(handle) = self.present_handle_mut() else {
848 return false;
849 };
850 handle.send_control_and_wait(
851 move |ack| PresentControl::AttachOffscreenTargetForTests { width, height, ack },
852 "attach offscreen target",
853 )
854 }
855
856 #[cfg(not(target_arch = "wasm32"))]
857 #[doc(hidden)]
858 pub fn send_attach_offscreen_unacked_for_tests(
859 &mut self,
860 width: u32,
861 height: u32,
862 ) -> Option<std::sync::mpsc::Receiver<()>> {
863 let handle = self.present_handle_mut()?;
864 handle.send_control_unacked(move |ack| PresentControl::AttachOffscreenTargetForTests {
865 width,
866 height,
867 ack,
868 })
869 }
870
871 #[cfg(not(target_arch = "wasm32"))]
872 #[doc(hidden)]
873 pub fn send_reconfigure_unacked_for_tests(
874 &mut self,
875 config: wgpu::SurfaceConfiguration,
876 ) -> Option<std::sync::mpsc::Receiver<()>> {
877 let surface_epoch = self.surface_epoch;
878 let handle = self.present_handle_mut()?;
879 handle.send_control_unacked(move |ack| PresentControl::Reconfigure {
880 config,
881 surface_epoch,
882 ack,
883 })
884 }
885
886 #[cfg(not(target_arch = "wasm32"))]
887 #[doc(hidden)]
888 pub fn send_drop_surface_unacked_for_tests(&mut self) -> Option<std::sync::mpsc::Receiver<()>> {
889 let handle = self.present_handle_mut()?;
890 handle.send_control_unacked(|ack| PresentControl::DropSurface { ack })
891 }
892
893 pub fn present_thread_id(&self) -> Option<i32> {
897 match &self.backend {
898 #[cfg(not(target_arch = "wasm32"))]
899 PresentBackend::Threaded(handle) => {
900 let id = handle
901 .status()
902 .thread_id
903 .load(std::sync::atomic::Ordering::Relaxed);
904 (id != 0).then_some(id)
905 }
906 _ => None,
907 }
908 }
909
910 pub fn last_published_frame_id(&self) -> u64 {
915 self.frontend.frame_sequence
916 }
917
918 #[cfg(not(target_arch = "wasm32"))]
919 #[doc(hidden)]
920 pub fn present_status_snapshot_for_tests(&self) -> Option<(bool, u64, u64)> {
921 match &self.backend {
922 PresentBackend::Threaded(handle) => {
923 let status = handle.status();
924 Some((
925 status
926 .needs_frame_warmup
927 .load(std::sync::atomic::Ordering::Relaxed),
928 status
929 .presented_frames
930 .load(std::sync::atomic::Ordering::Relaxed),
931 status
932 .placeholder_frames
933 .load(std::sync::atomic::Ordering::Relaxed),
934 ))
935 }
936 _ => None,
937 }
938 }
939
940 pub fn last_frame_stats(&self) -> Option<RenderStatsSnapshot> {
941 match &self.backend {
942 PresentBackend::Sync(gpu_renderer) => gpu_renderer.last_frame_stats(),
943 #[cfg(not(target_arch = "wasm32"))]
944 PresentBackend::Threaded(handle) => *handle
945 .status()
946 .last_frame_stats
947 .lock()
948 .unwrap_or_else(std::sync::PoisonError::into_inner),
949 PresentBackend::None => None,
950 }
951 }
952
953 pub fn gpu_pass_timings(&self) -> GpuPassTimingReport {
957 self.sync_gpu_renderer()
958 .map(GpuRenderer::gpu_pass_timings)
959 .unwrap_or_default()
960 }
961
962 pub fn debug_cpu_allocation_stats(&self) -> DebugCpuAllocationStats {
963 let mut stats = self
964 .sync_gpu_renderer()
965 .map(GpuRenderer::debug_cpu_allocation_stats)
966 .unwrap_or_default();
967 stats.scene_graph_node_count = self
968 .frontend
969 .scene
970 .graph
971 .as_ref()
972 .map_or(0, RenderGraph::node_count);
973 stats.scene_graph_heap_bytes = self
974 .frontend
975 .scene
976 .graph
977 .as_ref()
978 .map_or(0, RenderGraph::heap_bytes);
979 stats.scene_hits_len = self.frontend.scene.hits.len();
980 stats.scene_hits_cap = self.frontend.scene.hits.capacity();
981 stats.scene_node_index_len = self.frontend.scene.node_index.len();
982 stats.scene_node_index_cap = self.frontend.scene.node_index.capacity();
983 stats
984 }
985
986 pub fn try_device(&self) -> Option<&wgpu::Device> {
990 self.sync_gpu_renderer().map(|r| &*r.device)
991 }
992
993 #[doc(hidden)]
994 pub fn try_queue_for_tests(&self) -> Option<&wgpu::Queue> {
995 self.sync_gpu_renderer().map(|r| &*r.queue)
996 }
997
998 #[doc(hidden)]
999 pub fn device_error_count_for_tests(&self) -> u64 {
1000 self.sync_gpu_renderer()
1001 .map_or(0, GpuRenderer::device_error_count)
1002 }
1003
1004 #[doc(hidden)]
1005 pub fn build_frame_packet_for_tests(
1006 &mut self,
1007 width: u32,
1008 height: u32,
1009 ) -> Option<HeldFramePacket> {
1010 self.frontend
1011 .build_frame_packet(width, height, self.renderer_epoch, self.surface_epoch)
1012 .map(HeldFramePacket)
1013 }
1014
1015 #[doc(hidden)]
1027 pub fn return_held_packet_for_tests(&mut self, packet: HeldFramePacket) {
1028 let packet = packet.0;
1029 self.frontend.apply_returns(RenderReturns {
1030 scene: Some(frame_packet::FrameSceneStorage {
1031 root: packet.root,
1032 overlay: packet.overlay,
1033 }),
1034 frame_id: packet.frame_id,
1035 ..RenderReturns::default()
1036 });
1037 }
1038
1039 #[doc(hidden)]
1040 pub fn render_held_packet_for_tests(
1041 &mut self,
1042 texture: &wgpu::Texture,
1043 view: &wgpu::TextureView,
1044 width: u32,
1045 height: u32,
1046 packet: HeldFramePacket,
1047 ) -> Result<PresentOutcome, WgpuRendererError> {
1048 let PresentBackend::Sync(gpu_renderer) = &mut self.backend else {
1049 return Err(WgpuRendererError::Wgpu(
1050 "GPU renderer not initialized for synchronous rendering. Call init_gpu() first."
1051 .to_string(),
1052 ));
1053 };
1054 let mut returns = RenderReturns::default();
1055 let result = gpu_renderer.render(
1056 texture,
1057 view,
1058 width,
1059 height,
1060 packet.0,
1061 self.surface_epoch,
1062 &mut returns,
1063 );
1064 let outcome = returns.outcome;
1065 self.frontend.apply_returns(returns);
1066 result.map_err(WgpuRendererError::Wgpu)?;
1067 Ok(outcome)
1068 }
1069}
1070
1071#[doc(hidden)]
1072pub struct HeldFramePacket(frame_packet::FramePacket);
1073
1074#[cfg(not(target_arch = "wasm32"))]
1075#[doc(hidden)]
1076pub struct InlinePresentRuntime {
1077 state: PresentState,
1078 msg_rx: std::sync::mpsc::Receiver<PresentMsg>,
1079 shutdown_seen: bool,
1080}
1081
1082#[cfg(not(target_arch = "wasm32"))]
1083impl InlinePresentRuntime {
1084 pub fn pump(&mut self) -> bool {
1085 if self.shutdown_seen {
1086 return false;
1087 }
1088 while let Ok(msg) = self.msg_rx.try_recv() {
1089 if !self.state.run_once(msg) {
1090 self.shutdown_seen = true;
1091 return false;
1092 }
1093 }
1094 self.state.consume_waiting();
1095 true
1096 }
1097
1098 pub fn has_waiting_packet(&self) -> bool {
1099 self.state.has_waiting_packet()
1100 }
1101
1102 pub fn step_one_message(&mut self) -> bool {
1103 if self.shutdown_seen {
1104 return false;
1105 }
1106 match self.msg_rx.try_recv() {
1107 Ok(msg) => {
1108 if !self.state.run_once(msg) {
1109 self.shutdown_seen = true;
1110 }
1111 true
1112 }
1113 Err(_) => false,
1114 }
1115 }
1116
1117 pub fn consume_waiting(&mut self) {
1118 self.state.consume_waiting();
1119 }
1120}
1121
1122impl Default for WgpuRenderer {
1123 fn default() -> Self {
1124 Self::new(&[])
1125 }
1126}
1127
1128impl Renderer for WgpuRenderer {
1129 type Scene = Scene;
1130 type Error = WgpuRendererError;
1131
1132 fn attach_app_context_services(&mut self, app_context: &cranpose_ui::AppContext) {
1133 app_context.set_text_measurer(SoftwareTextMeasurer::from_font_set(
1134 self.frontend.text_fonts.clone(),
1135 8192,
1136 ));
1137 self.frontend.app_context = Some(app_context.downgrade());
1138 }
1139
1140 fn scene(&self) -> &Self::Scene {
1141 &self.frontend.scene
1142 }
1143
1144 fn scene_mut(&mut self) -> &mut Self::Scene {
1145 &mut self.frontend.scene
1146 }
1147
1148 fn rebuild_scene(
1149 &mut self,
1150 layout_tree: &LayoutTree,
1151 _viewport: Size,
1152 ) -> Result<(), Self::Error> {
1153 self.frontend.scene.clear();
1154 self.frontend.clear_fps_overlay();
1155 pipeline::render_layout_tree(layout_tree.root(), &mut self.frontend.scene);
1156 Ok(())
1157 }
1158
1159 fn rebuild_scene_from_applier(
1160 &mut self,
1161 applier: &mut MemoryApplier,
1162 root: NodeId,
1163 _viewport: Size,
1164 ) -> Result<(), Self::Error> {
1165 self.frontend.clear_fps_overlay();
1166 self.frontend.scene.rebuild_from_applier(applier, root);
1167 Ok(())
1168 }
1169
1170 fn update_scene_from_applier(
1171 &mut self,
1172 applier: &mut MemoryApplier,
1173 root: NodeId,
1174 _viewport: Size,
1175 updates: cranpose_render_common::SceneUpdates<'_>,
1176 ) -> Result<(), Self::Error> {
1177 if updates.is_empty() {
1178 self.frontend.clear_fps_overlay();
1179 }
1180 self.frontend
1181 .scene
1182 .update_from_applier(applier, root, updates, true);
1183 Ok(())
1184 }
1185
1186 fn update_visual_scene_from_applier(
1187 &mut self,
1188 applier: &mut MemoryApplier,
1189 root: NodeId,
1190 _viewport: Size,
1191 updates: cranpose_render_common::SceneUpdates<'_>,
1192 ) -> Result<(), Self::Error> {
1193 if updates.is_empty() {
1194 self.frontend.clear_fps_overlay();
1195 }
1196 self.frontend
1197 .scene
1198 .update_from_applier(applier, root, updates, false);
1199 Ok(())
1200 }
1201
1202 fn draw_dev_overlay(&mut self, text: &str, viewport: Size) {
1203 const DEV_OVERLAY_NODE_ID: NodeId = NodeId::MAX;
1204 let key = cranpose_render_common::dev_overlay::DevOverlayKey::new(text, viewport);
1205 if self.frontend.dev_overlay_graph.is_some()
1206 && self
1207 .frontend
1208 .dev_overlay_cache
1209 .as_ref()
1210 .is_some_and(|cache| {
1211 cache.text == key.text
1212 && cache.viewport_width_bits == key.viewport_width_bits
1213 && cache.viewport_height_bits == key.viewport_height_bits
1214 })
1215 {
1216 return;
1217 }
1218 self.frontend.fps_overlay_graph = Some(
1219 cranpose_render_common::dev_overlay::build_dev_overlay_graph(
1220 text,
1221 viewport,
1222 DEV_OVERLAY_NODE_ID,
1223 ),
1224 );
1225 self.frontend.dev_overlay_cache = Some(DevOverlayCache {
1226 text: key.text,
1227 viewport_width_bits: key.viewport_width_bits,
1228 viewport_height_bits: key.viewport_height_bits,
1229 });
1230 self.frontend.refresh_dev_overlay();
1231 }
1232
1233 fn set_inspector_overlay(&mut self, graph: Option<cranpose_render_common::graph::RenderGraph>) {
1234 self.frontend.inspector_overlay_graph = graph;
1235 self.frontend.refresh_dev_overlay();
1236 }
1237
1238 fn needs_frame_warmup(&self) -> bool {
1239 match &self.backend {
1240 PresentBackend::Sync(gpu_renderer) => gpu_renderer.needs_frame_warmup(),
1241 #[cfg(not(target_arch = "wasm32"))]
1242 PresentBackend::Threaded(handle) => handle
1243 .status()
1244 .needs_frame_warmup
1245 .load(std::sync::atomic::Ordering::Relaxed),
1246 PresentBackend::None => false,
1247 }
1248 }
1249
1250 fn awaits_pipelines(&self) -> bool {
1251 match &self.backend {
1252 PresentBackend::Sync(gpu_renderer) => gpu_renderer.awaits_pipelines(),
1253 #[cfg(not(target_arch = "wasm32"))]
1254 PresentBackend::Threaded(handle) => {
1255 let status = handle.status();
1256 status
1257 .drew_placeholder
1258 .load(std::sync::atomic::Ordering::Relaxed)
1259 && status
1260 .landing
1261 .get()
1262 .is_some_and(|landing| landing.in_flight())
1263 }
1264 PresentBackend::None => false,
1265 }
1266 }
1267}
1268
1269#[cfg(test)]
1270#[path = "tests/wgpu_tests.rs"]
1271mod tests;