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