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