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