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