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