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;
28mod glyph_run_arena;
29pub(crate) mod gpu_stats;
30mod initial_present;
31mod lazy_resource;
32mod offscreen;
33mod opaque_prefix;
34mod output_conversion;
35pub(crate) mod pass_timing;
36mod pipeline;
37mod pipeline_compiler;
38#[cfg(not(target_arch = "wasm32"))]
39pub mod pipeline_disk_cache;
40#[cfg(not(target_arch = "wasm32"))]
41mod present_runtime;
42mod record_columns;
43mod render;
44mod run_geometry;
45mod run_store;
46mod scene;
47mod shader_cache;
48mod shaders;
49mod shape_pipelines;
50#[cfg(test)]
51mod test_support;
52
53use std::{rc::Rc, sync::Arc};
54
55use cranpose_core::{MemoryApplier, NodeId};
56use cranpose_render_common::{
57 RenderScene, Renderer,
58 graph::RenderGraph,
59 software_text_raster::{
60 SoftwareTextFontSet, SoftwareTextMeasurer, software_text_font_set_from_fonts_or_default,
61 },
62};
63use cranpose_ui::{LayoutTree, TextMeasurer};
64use cranpose_ui_graphics::{Rect, Size};
65pub use frame_packet::PresentTimings;
66
67#[cfg(not(target_arch = "wasm32"))]
71pub trait PresentObserver: Send {
72 fn before_present(&mut self, surface: &wgpu::Surface<'static>);
74 fn after_present(&mut self, surface: &wgpu::Surface<'static>);
76
77 fn before_reconfigure(&mut self, _surface: &wgpu::Surface<'static>) {}
80}
81use frame_packet::RenderReturns;
82#[doc(hidden)]
83pub use frame_packet::{CancelReason, PresentOutcome};
84use frontend::{DevOverlayCache, RendererFrontend};
85pub use gpu_stats::FrameStatsSnapshot as RenderStatsSnapshot;
86pub use initial_present::{clear_to_background, clear_to_default_background};
87pub use pass_timing::{GpuPassTimingEntry, GpuPassTimingReport};
88use pipeline_compiler::PipelineCompilation;
89#[cfg(not(target_arch = "wasm32"))]
90use present_runtime::{
91 PresentControl, PresentHandle, PresentMsg, PresentRuntimeInit, PresentState,
92};
93use render::{GpuRenderer, GpuRendererInit};
94pub use render::{
95 frame_clear_color, frames_presented, pipelines_created, pipelines_created_off_frame,
96};
97pub use scene::{HitRegion, Scene};
98
99pub fn optional_device_features(adapter: &wgpu::Adapter) -> wgpu::Features {
106 adapter.features() & (wgpu::Features::PIPELINE_CACHE | wgpu::Features::TIMESTAMP_QUERY)
107}
108
109#[doc(hidden)]
110pub fn offscreen_render_target_for_tests(
111 device: &wgpu::Device,
112 width: u32,
113 height: u32,
114 label: &str,
115) -> (wgpu::Texture, wgpu::TextureView) {
116 let texture = offscreen::create_2d_texture(
117 device,
118 wgpu::TextureFormat::Rgba8Unorm,
119 width,
120 height,
121 wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
122 Some(label),
123 );
124 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
125 (texture, view)
126}
127
128pub(crate) fn rect_to_quad(rect: Rect) -> [[f32; 2]; 4] {
129 [
130 [rect.x, rect.y],
131 [rect.x + rect.width, rect.y],
132 [rect.x, rect.y + rect.height],
133 [rect.x + rect.width, rect.y + rect.height],
134 ]
135}
136
137#[derive(Debug)]
138pub enum WgpuRendererError {
139 Layout(String),
140 Wgpu(String),
141}
142
143#[derive(Debug, Clone)]
145pub struct CapturedFrame {
146 pub width: u32,
147 pub height: u32,
148 pub pixels: Vec<u8>,
149}
150
151#[doc(hidden)]
152#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
153pub struct DebugCpuAllocationStats {
154 pub scene_graph_node_count: usize,
155 pub scene_graph_heap_bytes: usize,
156 pub scene_hits_len: usize,
157 pub scene_hits_cap: usize,
158 pub scene_node_index_len: usize,
159 pub scene_node_index_cap: usize,
160 pub text_renderer_pool_len: usize,
161 pub text_renderer_pool_cap: usize,
162 pub image_texture_cache_len: usize,
163 pub image_texture_cache_cap: usize,
164 pub run_arena_staging_bytes: usize,
165 pub run_store_bytes: usize,
166 pub run_store_runs: usize,
167 pub scratch_image_vertices_cap: usize,
168 pub scratch_image_indices_cap: usize,
169 pub scratch_image_cmds_cap: usize,
170 pub scratch_glyph_instances_cap: usize,
171 pub layer_cache_len: usize,
172 pub layer_cache_bytes: u64,
173}
174
175struct GpuTarget {
177 device: Arc<wgpu::Device>,
178 queue: Arc<wgpu::Queue>,
179 surface_format: wgpu::TextureFormat,
180 adapter_backend: wgpu::Backend,
181 adapter_downlevel: wgpu::DownlevelFlags,
182}
183
184pub(crate) struct TextSystemState {
185 measurer: SoftwareTextMeasurer,
186}
187
188impl TextSystemState {
189 fn from_font_set(fonts: SoftwareTextFontSet) -> Self {
190 Self {
191 measurer: SoftwareTextMeasurer::from_font_set(fonts, 8192),
192 }
193 }
194
195 pub(crate) fn text_cache_len(&self) -> usize {
196 0
197 }
198}
199
200impl pipeline::TextLayoutResolver for TextSystemState {
201 fn layout_text(
202 &mut self,
203 text: &cranpose_ui::text::AnnotatedString,
204 style: &cranpose_ui::text::TextStyle,
205 ) -> cranpose_ui::text_layout_result::TextLayoutResult {
206 if cranpose_ui::has_current_app_context() {
207 cranpose_ui::text::layout_text(text, style)
208 } else {
209 self.measurer.layout(text, style)
210 }
211 }
212}
213
214#[derive(Clone)]
215pub struct WgpuTextSystem {
216 software_fonts: SoftwareTextFontSet,
217}
218
219impl WgpuTextSystem {
220 pub fn from_fonts(fonts: &[&'static [u8]]) -> Self {
221 Self {
222 software_fonts: software_text_font_set_from_fonts_or_default(fonts),
223 }
224 }
225
226 pub fn from_font_set(software_fonts: SoftwareTextFontSet) -> Self {
230 Self { software_fonts }
231 }
232
233 pub(crate) fn render_state(&self) -> TextSystemState {
234 TextSystemState::from_font_set(self.software_fonts.clone())
235 }
236
237 pub(crate) fn software_fonts(&self) -> SoftwareTextFontSet {
238 self.software_fonts.clone()
239 }
240}
241
242pub fn headless_text_measurer() -> Rc<dyn TextMeasurer> {
244 headless_text_measurer_with_fonts(&[])
245}
246
247pub fn headless_text_measurer_with_fonts(fonts: &[&'static [u8]]) -> Rc<dyn TextMeasurer> {
249 Rc::new(SoftwareTextMeasurer::from_fonts_or_default(fonts, 8192))
250}
251
252enum PresentBackend {
253 None,
254 Sync(Box<GpuRenderer>),
255 #[cfg(not(target_arch = "wasm32"))]
256 Threaded(PresentHandle),
257}
258
259#[derive(Clone, Copy, Debug, PartialEq, Eq)]
261pub enum PublishOutcome {
262 NoGraph,
264 NoCredit,
267 Published,
269}
270
271pub struct WgpuRenderer {
279 frontend: RendererFrontend,
280 backend: PresentBackend,
281 renderer_epoch: u64,
282 surface_epoch: u64,
283}
284
285impl WgpuRenderer {
286 fn update_scene(
287 &mut self,
288 applier: &mut MemoryApplier,
289 root: NodeId,
290 dirty_nodes: &[NodeId],
291 refresh_hits: bool,
292 ) {
293 let mut changed_nodes = std::mem::take(&mut self.frontend.changed_nodes);
294 pipeline::update_from_applier(
295 applier,
296 root,
297 &mut self.frontend.scene,
298 1.0,
299 dirty_nodes,
300 refresh_hits,
301 &mut changed_nodes,
302 );
303 changed_nodes.clear();
304 self.frontend.changed_nodes = changed_nodes;
305 }
306
307 pub fn new(fonts: &[&'static [u8]]) -> Self {
314 Self::with_text_system(WgpuTextSystem::from_fonts(fonts))
315 }
316
317 pub fn with_font_set(fonts: SoftwareTextFontSet) -> Self {
322 Self::with_text_system(WgpuTextSystem::from_font_set(fonts))
323 }
324
325 pub fn with_text_system(text_system: WgpuTextSystem) -> Self {
326 Self {
327 frontend: RendererFrontend::new(
328 text_system.render_state(),
329 text_system.software_fonts(),
330 ),
331 backend: PresentBackend::None,
332 renderer_epoch: 0,
333 surface_epoch: 0,
334 }
335 }
336
337 fn sync_gpu_renderer(&self) -> Option<&GpuRenderer> {
338 match &self.backend {
339 PresentBackend::Sync(gpu_renderer) => Some(gpu_renderer.as_ref()),
340 _ => None,
341 }
342 }
343
344 #[cfg(not(target_arch = "wasm32"))]
345 fn present_handle_mut(&mut self) -> Option<&mut PresentHandle> {
346 match &mut self.backend {
347 PresentBackend::Threaded(handle) => Some(handle),
348 _ => None,
349 }
350 }
351
352 fn retire_live_backend(&mut self) {
353 #[allow(unused_mut)]
354 let mut backend = std::mem::replace(&mut self.backend, PresentBackend::None);
355 #[cfg(not(target_arch = "wasm32"))]
356 if let PresentBackend::Threaded(handle) = &mut backend {
357 while let Some(returns) = handle.try_drain() {
358 self.frontend.apply_returns(returns);
359 }
360 handle.shutdown();
361 }
362 drop(backend);
363 }
364
365 pub fn init_gpu(
371 &mut self,
372 device: Arc<wgpu::Device>,
373 queue: Arc<wgpu::Queue>,
374 surface_format: wgpu::TextureFormat,
375 adapter_backend: wgpu::Backend,
376 adapter_downlevel: wgpu::DownlevelFlags,
377 ) {
378 let target = GpuTarget {
379 device,
380 queue,
381 surface_format,
382 adapter_backend,
383 adapter_downlevel,
384 };
385 self.install_gpu(target, PipelineCompilation::Background);
386 }
387
388 #[doc(hidden)]
392 pub fn init_gpu_compiling_inline_for_tests(
393 &mut self,
394 device: Arc<wgpu::Device>,
395 queue: Arc<wgpu::Queue>,
396 surface_format: wgpu::TextureFormat,
397 adapter_backend: wgpu::Backend,
398 adapter_downlevel: wgpu::DownlevelFlags,
399 ) {
400 let target = GpuTarget {
401 device,
402 queue,
403 surface_format,
404 adapter_backend,
405 adapter_downlevel,
406 };
407 self.install_gpu(target, PipelineCompilation::Inline);
408 }
409
410 fn install_gpu(&mut self, target: GpuTarget, pipeline_compilation: PipelineCompilation) {
411 let init = self.next_gpu_init(target, pipeline_compilation);
412 self.backend = PresentBackend::Sync(Box::new(GpuRenderer::new(init)));
413 }
414
415 fn next_gpu_init(
418 &mut self,
419 target: GpuTarget,
420 pipeline_compilation: PipelineCompilation,
421 ) -> GpuRendererInit {
422 self.retire_live_backend();
423 self.renderer_epoch = self.renderer_epoch.wrapping_add(1);
424 GpuRendererInit {
425 device: target.device,
426 queue: target.queue,
427 surface_format: target.surface_format,
428 adapter_backend: target.adapter_backend,
429 adapter_downlevel: target.adapter_downlevel,
430 text_fonts: self.frontend.text_fonts.clone(),
431 renderer_epoch: self.renderer_epoch,
432 pipeline_compilation,
433 }
434 }
435
436 #[cfg(not(target_arch = "wasm32"))]
453 #[expect(clippy::too_many_arguments)]
454 pub fn init_gpu_threaded(
455 &mut self,
456 device: Arc<wgpu::Device>,
457 queue: Arc<wgpu::Queue>,
458 surface_format: wgpu::TextureFormat,
459 adapter_backend: wgpu::Backend,
460 adapter_downlevel: wgpu::DownlevelFlags,
461 waker: Arc<dyn Fn() + Send + Sync>,
462 clock: Option<Arc<dyn Fn() -> i64 + Send + Sync>>,
463 observer: Option<Box<dyn PresentObserver>>,
464 ) -> Result<(), WgpuRendererError> {
465 let target = GpuTarget {
466 device,
467 queue,
468 surface_format,
469 adapter_backend,
470 adapter_downlevel,
471 };
472 let init = PresentRuntimeInit {
473 gpu: self.next_gpu_init(target, PipelineCompilation::Background),
474 clock,
475 observer,
476 };
477 let handle = PresentHandle::spawn(init, waker).map_err(WgpuRendererError::Wgpu)?;
478 self.backend = PresentBackend::Threaded(handle);
479 Ok(())
480 }
481
482 #[cfg(not(target_arch = "wasm32"))]
483 #[doc(hidden)]
484 pub fn init_gpu_inline_for_tests(
485 &mut self,
486 device: Arc<wgpu::Device>,
487 queue: Arc<wgpu::Queue>,
488 surface_format: wgpu::TextureFormat,
489 adapter_backend: wgpu::Backend,
490 adapter_downlevel: wgpu::DownlevelFlags,
491 ) -> InlinePresentRuntime {
492 let target = GpuTarget {
493 device,
494 queue,
495 surface_format,
496 adapter_backend,
497 adapter_downlevel,
498 };
499 let init = PresentRuntimeInit {
500 gpu: self.next_gpu_init(target, PipelineCompilation::Background),
501 clock: None,
502 observer: None,
503 };
504 let (handle, state, msg_rx) = PresentHandle::new_inline(init, Arc::new(|| {}));
505 self.backend = PresentBackend::Threaded(handle);
506 InlinePresentRuntime {
507 state,
508 msg_rx,
509 shutdown_seen: false,
510 }
511 }
512
513 pub fn note_surface_reconfigured(&mut self) {
518 self.surface_epoch = self.surface_epoch.wrapping_add(1);
519 }
520
521 pub fn set_root_scale(&mut self, scale: f32) {
523 self.frontend.root_scale = scale;
524 }
525
526 pub fn set_transparent_background(&mut self, transparent: bool) {
531 self.frontend.transparent_background = transparent;
532 }
533
534 pub fn root_scale(&self) -> f32 {
535 self.frontend.root_scale
536 }
537
538 pub fn render(
545 &mut self,
546 texture: &wgpu::Texture,
547 view: &wgpu::TextureView,
548 width: u32,
549 height: u32,
550 ) -> Result<(), WgpuRendererError> {
551 self.render_frame(texture, view, width, height)
552 }
553
554 pub fn render_surface_texture(
558 &mut self,
559 texture: &wgpu::Texture,
560 view: &wgpu::TextureView,
561 width: u32,
562 height: u32,
563 ) -> Result<(), WgpuRendererError> {
564 self.render_frame(texture, view, width, height)
565 }
566
567 pub fn present(&self, frame: wgpu::SurfaceTexture) {
574 match self.sync_gpu_renderer() {
575 Some(gpu_renderer) => gpu_renderer.queue.present(frame),
576 None => log::debug!("surface image released: the renderer has no GPU queue"),
577 }
578 }
579
580 fn render_frame(
581 &mut self,
582 texture: &wgpu::Texture,
583 view: &wgpu::TextureView,
584 width: u32,
585 height: u32,
586 ) -> Result<(), WgpuRendererError> {
587 let PresentBackend::Sync(gpu_renderer) = &mut self.backend else {
588 return Err(WgpuRendererError::Wgpu(
589 "GPU renderer not initialized for synchronous rendering. Call init_gpu() first."
590 .to_string(),
591 ));
592 };
593 let packet = self
594 .frontend
595 .build_frame_packet(width, height, self.renderer_epoch, self.surface_epoch)
596 .ok_or_else(|| WgpuRendererError::Wgpu("scene graph is missing".to_string()))?;
597 let mut returns = RenderReturns::default();
598 let result = gpu_renderer.render(
599 texture,
600 view,
601 width,
602 height,
603 packet,
604 self.surface_epoch,
605 &mut returns,
606 );
607 self.frontend.apply_returns(returns);
608 result.map_err(WgpuRendererError::Wgpu)
609 }
610
611 pub fn capture_frame(
615 &mut self,
616 width: u32,
617 height: u32,
618 ) -> Result<CapturedFrame, WgpuRendererError> {
619 self.capture_frame_with_scale(width, height, self.frontend.root_scale)
620 }
621
622 pub fn capture_frame_with_scale(
624 &mut self,
625 width: u32,
626 height: u32,
627 root_scale: f32,
628 ) -> Result<CapturedFrame, WgpuRendererError> {
629 let PresentBackend::Sync(gpu_renderer) = &mut self.backend else {
630 return Err(WgpuRendererError::Wgpu(
631 "GPU renderer not initialized for synchronous rendering. Call init_gpu() first."
632 .to_string(),
633 ));
634 };
635 let packet = self
636 .frontend
637 .build_frame_packet_with_scale(
638 width,
639 height,
640 root_scale,
641 self.renderer_epoch,
642 self.surface_epoch,
643 )
644 .ok_or_else(|| WgpuRendererError::Wgpu("scene graph is missing".to_string()))?;
645 let mut returns = RenderReturns::default();
646 let result = gpu_renderer.render_to_rgba_pixels(
647 width,
648 height,
649 packet,
650 self.surface_epoch,
651 &mut returns,
652 );
653 self.frontend.apply_returns(returns);
654 let pixels = result.map_err(WgpuRendererError::Wgpu)?;
655 Ok(CapturedFrame {
656 width,
657 height,
658 pixels,
659 })
660 }
661
662 #[cfg(not(target_arch = "wasm32"))]
667 pub fn has_frame_credit(&self) -> bool {
668 match &self.backend {
669 PresentBackend::Threaded(handle) => handle.has_credit(),
670 PresentBackend::Sync(_) | PresentBackend::None => true,
671 }
672 }
673
674 #[cfg(not(target_arch = "wasm32"))]
680 pub fn frame_credit_returned(&self) -> bool {
681 match &self.backend {
682 PresentBackend::Threaded(handle) => handle.has_undrained_return(),
683 PresentBackend::Sync(_) | PresentBackend::None => false,
684 }
685 }
686
687 #[cfg(not(target_arch = "wasm32"))]
693 pub fn publish_frame(&mut self, width: u32, height: u32) -> PublishOutcome {
694 let PresentBackend::Threaded(handle) = &mut self.backend else {
695 log::error!("publish_frame called without a threaded present runtime");
696 return PublishOutcome::NoCredit;
697 };
698 if !handle.has_credit() {
699 return PublishOutcome::NoCredit;
700 }
701 let Some(packet) = self.frontend.build_frame_packet(
702 width,
703 height,
704 self.renderer_epoch,
705 self.surface_epoch,
706 ) else {
707 return PublishOutcome::NoGraph;
708 };
709 match handle.publish(packet) {
710 Ok(()) => PublishOutcome::Published,
711 Err(packet) => {
712 let mut returns = RenderReturns::default();
713 let _ = GpuRenderer::cancel_packet(
714 *packet,
715 CancelReason::SurfaceUnavailable,
716 &mut returns,
717 );
718 self.frontend.apply_returns(returns);
719 log::error!("present runtime unavailable; frame recovered, not published");
720 PublishOutcome::NoCredit
721 }
722 }
723 }
724
725 #[cfg(not(target_arch = "wasm32"))]
729 pub fn drain_present_returns(&mut self) -> usize {
730 self.drain_present_returns_with(&mut |_, _, _| {})
731 }
732
733 #[cfg(not(target_arch = "wasm32"))]
737 pub fn drain_present_returns_with(
738 &mut self,
739 on_return: &mut dyn FnMut(u64, PresentOutcome, PresentTimings),
740 ) -> usize {
741 let mut drained = 0;
742 loop {
743 let returns = {
744 let PresentBackend::Threaded(handle) = &mut self.backend else {
745 break;
746 };
747 match handle.try_drain() {
748 Some(returns) => returns,
749 None => break,
750 }
751 };
752 drained += 1;
753 let frame_id = returns.frame_id;
754 let outcome = returns.outcome;
755 let timings = returns.timings;
756 self.frontend.apply_returns(returns);
757 on_return(frame_id, outcome, timings);
758 }
759 drained
760 }
761
762 #[cfg(not(target_arch = "wasm32"))]
768 pub fn present_replace_surface(
769 &mut self,
770 surface: wgpu::Surface<'static>,
771 config: wgpu::SurfaceConfiguration,
772 ) -> bool {
773 let surface_epoch = self.surface_epoch;
774 let Some(handle) = self.present_handle_mut() else {
775 log::error!("present_replace_surface called without a threaded present runtime");
776 return false;
777 };
778 handle.send_control_and_wait(
779 move |ack| PresentControl::ReplaceSurface {
780 surface,
781 config,
782 surface_epoch,
783 ack,
784 },
785 "replace surface",
786 )
787 }
788
789 #[cfg(not(target_arch = "wasm32"))]
793 pub fn present_reconfigure(&mut self, config: wgpu::SurfaceConfiguration) -> bool {
794 let surface_epoch = self.surface_epoch;
795 let Some(handle) = self.present_handle_mut() else {
796 log::error!("present_reconfigure called without a threaded present runtime");
797 return false;
798 };
799 handle.send_control_and_wait(
800 move |ack| PresentControl::Reconfigure {
801 config,
802 surface_epoch,
803 ack,
804 },
805 "reconfigure surface",
806 )
807 }
808
809 #[cfg(not(target_arch = "wasm32"))]
813 pub fn present_drop_surface(&mut self) -> bool {
814 let Some(handle) = self.present_handle_mut() else {
815 log::error!("present_drop_surface called without a threaded present runtime");
816 return false;
817 };
818 handle.send_control_and_wait(|ack| PresentControl::DropSurface { ack }, "drop surface")
819 }
820
821 #[cfg(not(target_arch = "wasm32"))]
824 pub fn shutdown_present_runtime(&mut self) {
825 if matches!(self.backend, PresentBackend::Threaded(_)) {
826 self.retire_live_backend();
827 }
828 }
829
830 #[cfg(not(target_arch = "wasm32"))]
831 #[doc(hidden)]
832 pub fn present_attach_offscreen_for_tests(&mut self, width: u32, height: u32) -> bool {
833 let Some(handle) = self.present_handle_mut() else {
834 return false;
835 };
836 handle.send_control_and_wait(
837 move |ack| PresentControl::AttachOffscreenTargetForTests { width, height, ack },
838 "attach offscreen target",
839 )
840 }
841
842 #[cfg(not(target_arch = "wasm32"))]
843 #[doc(hidden)]
844 pub fn send_attach_offscreen_unacked_for_tests(
845 &mut self,
846 width: u32,
847 height: u32,
848 ) -> Option<std::sync::mpsc::Receiver<()>> {
849 let handle = self.present_handle_mut()?;
850 handle.send_control_unacked(move |ack| PresentControl::AttachOffscreenTargetForTests {
851 width,
852 height,
853 ack,
854 })
855 }
856
857 #[cfg(not(target_arch = "wasm32"))]
858 #[doc(hidden)]
859 pub fn send_reconfigure_unacked_for_tests(
860 &mut self,
861 config: wgpu::SurfaceConfiguration,
862 ) -> Option<std::sync::mpsc::Receiver<()>> {
863 let surface_epoch = self.surface_epoch;
864 let handle = self.present_handle_mut()?;
865 handle.send_control_unacked(move |ack| PresentControl::Reconfigure {
866 config,
867 surface_epoch,
868 ack,
869 })
870 }
871
872 #[cfg(not(target_arch = "wasm32"))]
873 #[doc(hidden)]
874 pub fn send_drop_surface_unacked_for_tests(&mut self) -> Option<std::sync::mpsc::Receiver<()>> {
875 let handle = self.present_handle_mut()?;
876 handle.send_control_unacked(|ack| PresentControl::DropSurface { ack })
877 }
878
879 pub fn present_thread_id(&self) -> Option<i32> {
883 match &self.backend {
884 #[cfg(not(target_arch = "wasm32"))]
885 PresentBackend::Threaded(handle) => {
886 let id = handle
887 .status()
888 .thread_id
889 .load(std::sync::atomic::Ordering::Relaxed);
890 (id != 0).then_some(id)
891 }
892 _ => None,
893 }
894 }
895
896 pub fn last_published_frame_id(&self) -> u64 {
901 self.frontend.frame_sequence
902 }
903
904 #[cfg(not(target_arch = "wasm32"))]
905 #[doc(hidden)]
906 pub fn present_status_snapshot_for_tests(&self) -> Option<(bool, u64, u64)> {
907 match &self.backend {
908 PresentBackend::Threaded(handle) => {
909 let status = handle.status();
910 Some((
911 status
912 .needs_frame_warmup
913 .load(std::sync::atomic::Ordering::Relaxed),
914 status
915 .presented_frames
916 .load(std::sync::atomic::Ordering::Relaxed),
917 status
918 .placeholder_frames
919 .load(std::sync::atomic::Ordering::Relaxed),
920 ))
921 }
922 _ => None,
923 }
924 }
925
926 pub fn last_frame_stats(&self) -> Option<RenderStatsSnapshot> {
927 match &self.backend {
928 PresentBackend::Sync(gpu_renderer) => gpu_renderer.last_frame_stats(),
929 #[cfg(not(target_arch = "wasm32"))]
930 PresentBackend::Threaded(handle) => *handle
931 .status()
932 .last_frame_stats
933 .lock()
934 .unwrap_or_else(std::sync::PoisonError::into_inner),
935 PresentBackend::None => None,
936 }
937 }
938
939 pub fn gpu_pass_timings(&self) -> GpuPassTimingReport {
943 self.sync_gpu_renderer()
944 .map(GpuRenderer::gpu_pass_timings)
945 .unwrap_or_default()
946 }
947
948 pub fn debug_cpu_allocation_stats(&self) -> DebugCpuAllocationStats {
949 let mut stats = self
950 .sync_gpu_renderer()
951 .map(GpuRenderer::debug_cpu_allocation_stats)
952 .unwrap_or_default();
953 stats.scene_graph_node_count = self
954 .frontend
955 .scene
956 .graph
957 .as_ref()
958 .map_or(0, RenderGraph::node_count);
959 stats.scene_graph_heap_bytes = self
960 .frontend
961 .scene
962 .graph
963 .as_ref()
964 .map_or(0, RenderGraph::heap_bytes);
965 stats.scene_hits_len = self.frontend.scene.hits.len();
966 stats.scene_hits_cap = self.frontend.scene.hits.capacity();
967 stats.scene_node_index_len = self.frontend.scene.node_index.len();
968 stats.scene_node_index_cap = self.frontend.scene.node_index.capacity();
969 stats
970 }
971
972 pub fn try_device(&self) -> Option<&wgpu::Device> {
976 self.sync_gpu_renderer().map(|r| &*r.device)
977 }
978
979 #[doc(hidden)]
980 pub fn try_queue_for_tests(&self) -> Option<&wgpu::Queue> {
981 self.sync_gpu_renderer().map(|r| &*r.queue)
982 }
983
984 #[doc(hidden)]
985 pub fn device_error_count_for_tests(&self) -> u64 {
986 self.sync_gpu_renderer()
987 .map_or(0, GpuRenderer::device_error_count)
988 }
989
990 #[doc(hidden)]
991 pub fn build_frame_packet_for_tests(
992 &mut self,
993 width: u32,
994 height: u32,
995 ) -> Option<HeldFramePacket> {
996 self.frontend
997 .build_frame_packet(width, height, self.renderer_epoch, self.surface_epoch)
998 .map(HeldFramePacket)
999 }
1000
1001 #[doc(hidden)]
1002 pub fn render_held_packet_for_tests(
1003 &mut self,
1004 texture: &wgpu::Texture,
1005 view: &wgpu::TextureView,
1006 width: u32,
1007 height: u32,
1008 packet: HeldFramePacket,
1009 ) -> Result<PresentOutcome, WgpuRendererError> {
1010 let PresentBackend::Sync(gpu_renderer) = &mut self.backend else {
1011 return Err(WgpuRendererError::Wgpu(
1012 "GPU renderer not initialized for synchronous rendering. Call init_gpu() first."
1013 .to_string(),
1014 ));
1015 };
1016 let mut returns = RenderReturns::default();
1017 let result = gpu_renderer.render(
1018 texture,
1019 view,
1020 width,
1021 height,
1022 packet.0,
1023 self.surface_epoch,
1024 &mut returns,
1025 );
1026 let outcome = returns.outcome;
1027 self.frontend.apply_returns(returns);
1028 result.map_err(WgpuRendererError::Wgpu)?;
1029 Ok(outcome)
1030 }
1031}
1032
1033#[doc(hidden)]
1034pub struct HeldFramePacket(frame_packet::FramePacket);
1035
1036#[cfg(not(target_arch = "wasm32"))]
1037#[doc(hidden)]
1038pub struct InlinePresentRuntime {
1039 state: PresentState,
1040 msg_rx: std::sync::mpsc::Receiver<PresentMsg>,
1041 shutdown_seen: bool,
1042}
1043
1044#[cfg(not(target_arch = "wasm32"))]
1045impl InlinePresentRuntime {
1046 pub fn pump(&mut self) -> bool {
1047 if self.shutdown_seen {
1048 return false;
1049 }
1050 while let Ok(msg) = self.msg_rx.try_recv() {
1051 if !self.state.run_once(msg) {
1052 self.shutdown_seen = true;
1053 return false;
1054 }
1055 }
1056 self.state.consume_waiting();
1057 true
1058 }
1059
1060 pub fn has_waiting_packet(&self) -> bool {
1061 self.state.has_waiting_packet()
1062 }
1063
1064 pub fn step_one_message(&mut self) -> bool {
1065 if self.shutdown_seen {
1066 return false;
1067 }
1068 match self.msg_rx.try_recv() {
1069 Ok(msg) => {
1070 if !self.state.run_once(msg) {
1071 self.shutdown_seen = true;
1072 }
1073 true
1074 }
1075 Err(_) => false,
1076 }
1077 }
1078
1079 pub fn consume_waiting(&mut self) {
1080 self.state.consume_waiting();
1081 }
1082}
1083
1084impl Default for WgpuRenderer {
1085 fn default() -> Self {
1086 Self::new(&[])
1087 }
1088}
1089
1090impl Renderer for WgpuRenderer {
1091 type Scene = Scene;
1092 type Error = WgpuRendererError;
1093
1094 fn attach_app_context_services(&mut self, app_context: &cranpose_ui::AppContext) {
1095 app_context.set_text_measurer(SoftwareTextMeasurer::from_font_set(
1096 self.frontend.text_fonts.clone(),
1097 8192,
1098 ));
1099 self.frontend.app_context = Some(app_context.downgrade());
1100 }
1101
1102 fn scene(&self) -> &Self::Scene {
1103 &self.frontend.scene
1104 }
1105
1106 fn scene_mut(&mut self) -> &mut Self::Scene {
1107 &mut self.frontend.scene
1108 }
1109
1110 fn rebuild_scene(
1111 &mut self,
1112 layout_tree: &LayoutTree,
1113 _viewport: Size,
1114 ) -> Result<(), Self::Error> {
1115 self.frontend.scene.clear();
1116 self.frontend.clear_fps_overlay();
1117 pipeline::render_layout_tree(layout_tree.root(), &mut self.frontend.scene);
1118 Ok(())
1119 }
1120
1121 fn rebuild_scene_from_applier(
1122 &mut self,
1123 applier: &mut MemoryApplier,
1124 root: NodeId,
1125 _viewport: Size,
1126 ) -> Result<(), Self::Error> {
1127 self.frontend.clear_fps_overlay();
1128 pipeline::render_from_applier(applier, root, &mut self.frontend.scene, 1.0);
1129 Ok(())
1130 }
1131
1132 fn update_scene_from_applier(
1133 &mut self,
1134 applier: &mut MemoryApplier,
1135 root: NodeId,
1136 viewport: Size,
1137 dirty_nodes: &[NodeId],
1138 ) -> Result<(), Self::Error> {
1139 if dirty_nodes.is_empty() {
1140 return self.rebuild_scene_from_applier(applier, root, viewport);
1141 }
1142 self.update_scene(applier, root, dirty_nodes, true);
1143 Ok(())
1144 }
1145
1146 fn update_visual_scene_from_applier(
1147 &mut self,
1148 applier: &mut MemoryApplier,
1149 root: NodeId,
1150 viewport: Size,
1151 dirty_nodes: &[NodeId],
1152 ) -> Result<(), Self::Error> {
1153 if dirty_nodes.is_empty() {
1154 return self.rebuild_scene_from_applier(applier, root, viewport);
1155 }
1156 self.update_scene(applier, root, dirty_nodes, false);
1157 Ok(())
1158 }
1159
1160 fn draw_dev_overlay(&mut self, text: &str, viewport: Size) {
1161 const DEV_OVERLAY_NODE_ID: NodeId = NodeId::MAX;
1162 let key = cranpose_render_common::dev_overlay::DevOverlayKey::new(text, viewport);
1163 if self.frontend.dev_overlay_graph.is_some()
1164 && self
1165 .frontend
1166 .dev_overlay_cache
1167 .as_ref()
1168 .is_some_and(|cache| {
1169 cache.text == key.text
1170 && cache.viewport_width_bits == key.viewport_width_bits
1171 && cache.viewport_height_bits == key.viewport_height_bits
1172 })
1173 {
1174 return;
1175 }
1176 self.frontend.fps_overlay_graph = Some(
1177 cranpose_render_common::dev_overlay::build_dev_overlay_graph(
1178 text,
1179 viewport,
1180 DEV_OVERLAY_NODE_ID,
1181 ),
1182 );
1183 self.frontend.dev_overlay_cache = Some(DevOverlayCache {
1184 text: key.text,
1185 viewport_width_bits: key.viewport_width_bits,
1186 viewport_height_bits: key.viewport_height_bits,
1187 });
1188 self.frontend.refresh_dev_overlay();
1189 }
1190
1191 fn set_inspector_overlay(&mut self, graph: Option<cranpose_render_common::graph::RenderGraph>) {
1192 self.frontend.inspector_overlay_graph = graph;
1193 self.frontend.refresh_dev_overlay();
1194 }
1195
1196 fn needs_frame_warmup(&self) -> bool {
1197 match &self.backend {
1198 PresentBackend::Sync(gpu_renderer) => gpu_renderer.needs_frame_warmup(),
1199 #[cfg(not(target_arch = "wasm32"))]
1200 PresentBackend::Threaded(handle) => handle
1201 .status()
1202 .needs_frame_warmup
1203 .load(std::sync::atomic::Ordering::Relaxed),
1204 PresentBackend::None => false,
1205 }
1206 }
1207}
1208
1209#[cfg(test)]
1210#[path = "tests/wgpu_tests.rs"]
1211mod tests;