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