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