1#![deny(unsafe_code)]
7
8#[cfg(not(target_arch = "wasm32"))]
9mod cost_tuner;
10mod display_clip;
11mod effect_renderer;
12mod frame_graph;
13mod frame_packet;
14mod frontend;
15pub(crate) mod gpu_stats;
16mod layer_events;
17mod layer_surface_cache;
18mod lazy_resource;
19mod normalized_scene;
20mod offscreen;
21mod pipeline;
22#[cfg(not(target_arch = "wasm32"))]
23mod pipeline_disk_cache;
24#[cfg(not(target_arch = "wasm32"))]
25mod present_runtime;
26mod render;
27mod run_entry;
28mod scene;
29#[cfg(not(target_arch = "wasm32"))]
30mod segment_surface;
31mod shader_cache;
32mod shaders;
33#[cfg(not(target_arch = "wasm32"))]
34mod shape_replay;
35#[cfg(not(target_arch = "wasm32"))]
36mod stage_executor;
37mod surface_executor;
38mod surface_plan;
39mod surface_requirements;
40#[cfg(test)]
41mod test_support;
42
43#[doc(hidden)]
44#[cfg(not(target_arch = "wasm32"))]
45pub use display_clip::pixel_is_visible as display_clip_pixel_is_visible;
46pub use display_clip::DisplayVisibleRegion;
47pub use frame_packet::PresentTimings;
48#[doc(hidden)]
49pub use frame_packet::{CancelReason, PresentOutcome};
50pub use gpu_stats::FrameStatsSnapshot as RenderStatsSnapshot;
51#[doc(hidden)]
52#[cfg(not(target_arch = "wasm32"))]
53pub use pipeline::retained_feed_generation;
54pub use render::frames_presented;
55pub use scene::{ClickAction, HitRegion, Scene};
56#[doc(hidden)]
57#[cfg(not(target_arch = "wasm32"))]
58pub use shape_replay::feed_live_stats as command_feed_live_stats;
59#[doc(hidden)]
60#[cfg(not(target_arch = "wasm32"))]
61pub use shape_replay::{inject_feed_capture_for_tests, pending_feed_capture_count_for_tests};
62#[doc(hidden)]
63#[cfg(not(target_arch = "wasm32"))]
64pub use shape_replay::{planner_replay_queue_stats_for_tests, recycled_ops_capacities_for_tests};
65
66use cranpose_core::{MemoryApplier, NodeId};
67use cranpose_render_common::{
68 graph::RenderGraph,
69 software_text_raster::{
70 software_text_font_set_from_fonts_or_default, SoftwareTextFontSet, SoftwareTextMeasurer,
71 },
72 RenderScene, Renderer,
73};
74use cranpose_ui::{LayoutTree, TextMeasurer};
75use cranpose_ui_graphics::{Rect, Size};
76use frame_packet::RenderReturns;
77#[cfg(not(target_arch = "wasm32"))]
78use frame_packet::ReplayConfirmation;
79use frontend::{DevOverlayCache, RendererFrontend};
80#[cfg(not(target_arch = "wasm32"))]
81use present_runtime::{
82 PresentControl, PresentHandle, PresentMsg, PresentRuntimeInit, PresentState,
83};
84use render::GpuRenderer;
85use std::rc::Rc;
86use std::sync::Arc;
87
88pub(crate) fn rect_to_quad(rect: Rect) -> [[f32; 2]; 4] {
90 [
91 [rect.x, rect.y],
92 [rect.x + rect.width, rect.y],
93 [rect.x, rect.y + rect.height],
94 [rect.x + rect.width, rect.y + rect.height],
95 ]
96}
97
98#[derive(Debug)]
99pub enum WgpuRendererError {
100 Layout(String),
101 Wgpu(String),
102}
103
104#[derive(Debug, Clone)]
106pub struct CapturedFrame {
107 pub width: u32,
108 pub height: u32,
109 pub pixels: Vec<u8>,
110}
111
112#[doc(hidden)]
113#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
114pub struct DebugCpuAllocationStats {
115 pub scene_graph_node_count: usize,
116 pub scene_graph_heap_bytes: usize,
117 pub scene_hits_len: usize,
118 pub scene_hits_cap: usize,
119 pub scene_node_index_len: usize,
120 pub scene_node_index_cap: usize,
121 pub text_renderer_pool_len: usize,
122 pub text_renderer_pool_cap: usize,
123 pub swash_image_cache_len: usize,
124 pub swash_image_cache_cap: usize,
125 pub swash_outline_cache_len: usize,
126 pub swash_outline_cache_cap: usize,
127 pub image_texture_cache_len: usize,
128 pub image_texture_cache_cap: usize,
129 pub scratch_shape_data_cap: usize,
130 pub scratch_gradients_cap: usize,
131 pub scratch_image_vertices_cap: usize,
132 pub scratch_image_indices_cap: usize,
133 pub scratch_image_cmds_cap: usize,
134 pub scratch_segment_items_cap: usize,
135 pub scratch_effect_ranges_cap: usize,
136 pub scratch_layer_events_cap: usize,
137 pub staged_upload_bytes_cap: usize,
138 pub staged_upload_copies_cap: usize,
139 pub layer_surface_cache_len: usize,
140 pub layer_surface_cache_cap: usize,
141 pub layer_surface_cache_identity_len: usize,
142 pub layer_surface_cache_identity_cap: usize,
143 pub layer_surface_rect_cache_len: usize,
144 pub layer_surface_rect_cache_cap: usize,
145 pub layer_surface_requirements_cache_len: usize,
146 pub layer_surface_requirements_cache_cap: usize,
147 pub layer_cache_seen_this_frame_len: usize,
148 pub layer_cache_seen_this_frame_cap: usize,
149}
150
151pub(crate) struct TextSystemState {
152 measurer: SoftwareTextMeasurer,
153}
154
155impl TextSystemState {
156 fn from_font_set(fonts: SoftwareTextFontSet) -> Self {
157 Self {
158 measurer: SoftwareTextMeasurer::from_font_set(fonts, 8192),
159 }
160 }
161
162 pub(crate) fn text_cache_len(&self) -> usize {
163 0
164 }
165}
166
167impl pipeline::TextLayoutResolver for TextSystemState {
168 fn layout_text(
169 &mut self,
170 text: &cranpose_ui::text::AnnotatedString,
171 style: &cranpose_ui::text::TextStyle,
172 ) -> cranpose_ui::text_layout_result::TextLayoutResult {
173 if cranpose_ui::has_current_app_context() {
174 cranpose_ui::text::layout_text(text, style)
175 } else {
176 self.measurer.layout(text, style)
177 }
178 }
179}
180
181#[derive(Clone)]
182pub struct WgpuTextSystem {
183 software_fonts: SoftwareTextFontSet,
184}
185
186impl WgpuTextSystem {
187 pub fn from_fonts(fonts: &[&[u8]]) -> Self {
188 Self {
189 software_fonts: software_text_font_set_from_fonts_or_default(fonts),
190 }
191 }
192
193 pub fn from_font_set(software_fonts: SoftwareTextFontSet) -> Self {
197 Self { software_fonts }
198 }
199
200 pub(crate) fn render_state(&self) -> TextSystemState {
201 TextSystemState::from_font_set(self.software_fonts.clone())
202 }
203
204 pub(crate) fn software_fonts(&self) -> SoftwareTextFontSet {
205 self.software_fonts.clone()
206 }
207}
208
209pub fn headless_text_measurer() -> Rc<dyn TextMeasurer> {
211 headless_text_measurer_with_fonts(&[])
212}
213
214pub fn headless_text_measurer_with_fonts(fonts: &[&[u8]]) -> Rc<dyn TextMeasurer> {
216 Rc::new(SoftwareTextMeasurer::from_fonts_or_default(fonts, 8192))
217}
218
219enum PresentBackend {
227 None,
229 Sync(Box<GpuRenderer>),
231 #[cfg(not(target_arch = "wasm32"))]
233 Threaded(PresentHandle),
234}
235
236#[derive(Clone, Copy, Debug, PartialEq, Eq)]
238pub enum PublishOutcome {
239 NoGraph,
241 NoCredit,
244 Published,
246}
247
248pub struct WgpuRenderer {
256 frontend: RendererFrontend,
259 backend: PresentBackend,
261 #[cfg(not(target_arch = "wasm32"))]
265 pending_recycled_confirmations: Option<Vec<ReplayConfirmation>>,
266 renderer_epoch: u64,
271 surface_epoch: u64,
276 #[cfg_attr(target_arch = "wasm32", allow(dead_code))]
283 display_visible_region: DisplayVisibleRegion,
284}
285
286impl WgpuRenderer {
287 pub fn new(fonts: &[&[u8]]) -> Self {
294 Self::with_text_system(WgpuTextSystem::from_fonts(fonts))
295 }
296
297 pub fn with_font_set(fonts: SoftwareTextFontSet) -> Self {
302 Self::with_text_system(WgpuTextSystem::from_font_set(fonts))
303 }
304
305 pub fn with_text_system(text_system: WgpuTextSystem) -> Self {
306 Self {
307 frontend: RendererFrontend::new(
308 text_system.render_state(),
309 text_system.software_fonts(),
310 ),
311 backend: PresentBackend::None,
312 #[cfg(not(target_arch = "wasm32"))]
313 pending_recycled_confirmations: None,
314 renderer_epoch: 0,
315 surface_epoch: 0,
316 display_visible_region: DisplayVisibleRegion::Full,
317 }
318 }
319
320 fn sync_gpu_renderer(&self) -> Option<&GpuRenderer> {
322 match &self.backend {
323 PresentBackend::Sync(gpu_renderer) => Some(gpu_renderer.as_ref()),
324 _ => None,
325 }
326 }
327
328 fn sync_gpu_renderer_mut(&mut self) -> Option<&mut GpuRenderer> {
329 match &mut self.backend {
330 PresentBackend::Sync(gpu_renderer) => Some(gpu_renderer.as_mut()),
331 _ => None,
332 }
333 }
334
335 #[cfg(not(target_arch = "wasm32"))]
337 fn present_handle_mut(&mut self) -> Option<&mut PresentHandle> {
338 match &mut self.backend {
339 PresentBackend::Threaded(handle) => Some(handle),
340 _ => None,
341 }
342 }
343
344 fn retire_live_backend(&mut self) {
351 #[allow(unused_mut)]
352 let mut backend = std::mem::replace(&mut self.backend, PresentBackend::None);
353 if matches!(backend, PresentBackend::None) {
354 return;
355 }
356 #[cfg(not(target_arch = "wasm32"))]
357 {
358 if let PresentBackend::Threaded(handle) = &mut backend {
359 while let Some((ack, recycled)) = handle.try_drain_ack() {
364 let confirmations = crate::shape_replay::SHAPE_REPLAY
365 .with(|state| state.borrow_mut().apply_ack(ack, recycled));
366 self.stash_recycled_confirmations(confirmations);
367 }
368 while let Some(returns) = handle.try_drain() {
369 if let Some(confirmations) = self.frontend.apply_returns(returns) {
370 self.stash_recycled_confirmations(confirmations);
371 }
372 }
373 handle.shutdown();
374 }
375 crate::shape_replay::SHAPE_REPLAY.with(|state| state.borrow_mut().renderer_replaced());
376 log::warn!(
377 "[command-feed] renderer replaced: retained slots retired, \
378 confirmations revoked, feed generation bumped"
379 );
380 }
381 drop(backend);
382 }
383
384 #[cfg(not(target_arch = "wasm32"))]
387 fn stash_recycled_confirmations(&mut self, confirmations: Vec<ReplayConfirmation>) {
388 match &self.pending_recycled_confirmations {
389 Some(parked) if parked.capacity() >= confirmations.capacity() => {}
390 _ => self.pending_recycled_confirmations = Some(confirmations),
391 }
392 }
393
394 #[cfg(not(target_arch = "wasm32"))]
408 #[doc(hidden)]
409 pub fn drain_replay_acks(&mut self) -> usize {
410 let mut drained = 0;
411 loop {
412 let (ack, recycled) = {
413 let PresentBackend::Threaded(handle) = &mut self.backend else {
414 break;
415 };
416 match handle.try_drain_ack() {
417 Some(batch) => batch,
418 None => break,
419 }
420 };
421 drained += 1;
422 let confirmations = crate::shape_replay::SHAPE_REPLAY
423 .with(|state| state.borrow_mut().apply_ack(ack, recycled));
424 self.stash_recycled_confirmations(confirmations);
425 }
426 drained
427 }
428
429 pub fn init_gpu(
439 &mut self,
440 device: Arc<wgpu::Device>,
441 queue: Arc<wgpu::Queue>,
442 surface_format: wgpu::TextureFormat,
443 adapter_backend: wgpu::Backend,
444 adapter_downlevel: wgpu::DownlevelFlags,
445 ) {
446 self.retire_live_backend();
447 self.renderer_epoch = self.renderer_epoch.wrapping_add(1);
448 #[cfg(not(target_arch = "wasm32"))]
452 let store_feed_generation = pipeline::retained_feed_generation();
453 #[cfg(target_arch = "wasm32")]
454 let store_feed_generation = 0;
455 self.backend = PresentBackend::Sync(Box::new(GpuRenderer::new(
456 device,
457 queue,
458 surface_format,
459 adapter_backend,
460 adapter_downlevel,
461 self.frontend.text_fonts.clone(),
462 self.renderer_epoch,
463 store_feed_generation,
464 )));
465 #[cfg(not(target_arch = "wasm32"))]
466 {
467 let region = self.display_visible_region;
468 if let Some(gpu_renderer) = self.sync_gpu_renderer_mut() {
469 gpu_renderer.set_display_visible_region(region);
470 }
471 }
472 }
473
474 pub fn set_display_visible_region(&mut self, region: DisplayVisibleRegion) {
491 self.display_visible_region = region;
492 #[cfg(not(target_arch = "wasm32"))]
493 match &mut self.backend {
494 PresentBackend::Sync(gpu_renderer) => {
495 gpu_renderer.set_display_visible_region(region);
496 }
497 PresentBackend::Threaded(handle) => {
498 handle.send_display_visible_region(region);
499 }
500 PresentBackend::None => {}
501 }
502 }
503
504 #[cfg(not(target_arch = "wasm32"))]
519 #[allow(clippy::too_many_arguments)]
520 pub fn init_gpu_threaded(
521 &mut self,
522 device: Arc<wgpu::Device>,
523 queue: Arc<wgpu::Queue>,
524 surface_format: wgpu::TextureFormat,
525 adapter_backend: wgpu::Backend,
526 adapter_downlevel: wgpu::DownlevelFlags,
527 waker: Arc<dyn Fn() + Send + Sync>,
528 clock: Option<Arc<dyn Fn() -> i64 + Send + Sync>>,
529 ) -> Result<(), WgpuRendererError> {
530 self.retire_live_backend();
531 self.renderer_epoch = self.renderer_epoch.wrapping_add(1);
532 let store_feed_generation = pipeline::retained_feed_generation();
534 let init = PresentRuntimeInit {
535 device,
536 queue,
537 surface_format,
538 adapter_backend,
539 adapter_downlevel,
540 text_fonts: self.frontend.text_fonts.clone(),
541 renderer_epoch: self.renderer_epoch,
542 store_feed_generation,
543 clock,
544 display_visible_region: self.display_visible_region,
545 };
546 let handle = PresentHandle::spawn(init, waker).map_err(WgpuRendererError::Wgpu)?;
547 self.backend = PresentBackend::Threaded(handle);
548 Ok(())
549 }
550
551 #[cfg(not(target_arch = "wasm32"))]
556 #[doc(hidden)]
557 pub fn init_gpu_inline_for_tests(
558 &mut self,
559 device: Arc<wgpu::Device>,
560 queue: Arc<wgpu::Queue>,
561 surface_format: wgpu::TextureFormat,
562 adapter_backend: wgpu::Backend,
563 adapter_downlevel: wgpu::DownlevelFlags,
564 ) -> InlinePresentRuntime {
565 self.retire_live_backend();
566 self.renderer_epoch = self.renderer_epoch.wrapping_add(1);
567 let store_feed_generation = pipeline::retained_feed_generation();
568 let init = PresentRuntimeInit {
569 device,
570 queue,
571 surface_format,
572 adapter_backend,
573 adapter_downlevel,
574 text_fonts: self.frontend.text_fonts.clone(),
575 renderer_epoch: self.renderer_epoch,
576 store_feed_generation,
577 clock: None,
578 display_visible_region: self.display_visible_region,
579 };
580 let (handle, state, msg_rx) = PresentHandle::new_inline(init, Arc::new(|| {}));
581 self.backend = PresentBackend::Threaded(handle);
582 InlinePresentRuntime {
583 state,
584 msg_rx,
585 shutdown_seen: false,
586 }
587 }
588
589 pub fn note_surface_reconfigured(&mut self) {
594 self.surface_epoch = self.surface_epoch.wrapping_add(1);
595 }
596
597 pub fn set_root_scale(&mut self, scale: f32) {
599 self.frontend.root_scale = scale;
600 }
601
602 pub fn root_scale(&self) -> f32 {
603 self.frontend.root_scale
604 }
605
606 pub fn render(
614 &mut self,
615 view: &wgpu::TextureView,
616 width: u32,
617 height: u32,
618 ) -> Result<(), WgpuRendererError> {
619 let PresentBackend::Sync(gpu_renderer) = &mut self.backend else {
620 return Err(WgpuRendererError::Wgpu(
621 "GPU renderer not initialized for synchronous rendering. Call init_gpu() first."
622 .to_string(),
623 ));
624 };
625 let packet = self
626 .frontend
627 .build_frame_packet(
628 width,
629 height,
630 gpu_renderer.replay_supported(),
631 self.renderer_epoch,
632 self.surface_epoch,
633 )
634 .ok_or_else(|| WgpuRendererError::Wgpu("scene graph is missing".to_string()))?;
635 let frontend = &mut self.frontend;
636 let mut returns = RenderReturns::default();
637 let result = gpu_renderer.render(
641 view,
642 width,
643 height,
644 packet,
645 self.surface_epoch,
646 &mut returns,
647 );
648 if let Some(confirmations) = frontend.apply_returns(returns) {
649 gpu_renderer.restore_replay_ack_confirmations(confirmations);
650 }
651 result.map_err(WgpuRendererError::Wgpu)
652 }
653
654 pub fn capture_frame(
658 &mut self,
659 width: u32,
660 height: u32,
661 ) -> Result<CapturedFrame, WgpuRendererError> {
662 self.capture_frame_with_scale(width, height, self.frontend.root_scale)
663 }
664
665 pub fn capture_frame_with_scale(
667 &mut self,
668 width: u32,
669 height: u32,
670 root_scale: f32,
671 ) -> Result<CapturedFrame, WgpuRendererError> {
672 let PresentBackend::Sync(gpu_renderer) = &mut self.backend else {
673 return Err(WgpuRendererError::Wgpu(
674 "GPU renderer not initialized for synchronous rendering. Call init_gpu() first."
675 .to_string(),
676 ));
677 };
678 let packet = self
679 .frontend
680 .build_frame_packet_with_scale(
681 width,
682 height,
683 root_scale,
684 gpu_renderer.replay_supported(),
685 self.renderer_epoch,
686 self.surface_epoch,
687 )
688 .ok_or_else(|| WgpuRendererError::Wgpu("scene graph is missing".to_string()))?;
689 let frontend = &mut self.frontend;
690 let mut returns = RenderReturns::default();
691 let result = gpu_renderer.render_to_rgba_pixels(
694 width,
695 height,
696 packet,
697 self.surface_epoch,
698 &mut returns,
699 );
700 if let Some(confirmations) = frontend.apply_returns(returns) {
701 gpu_renderer.restore_replay_ack_confirmations(confirmations);
702 }
703 let pixels = result.map_err(WgpuRendererError::Wgpu)?;
704 Ok(CapturedFrame {
705 width,
706 height,
707 pixels,
708 })
709 }
710
711 #[cfg(not(target_arch = "wasm32"))]
716 pub fn has_frame_credit(&self) -> bool {
717 match &self.backend {
718 PresentBackend::Threaded(handle) => handle.has_credit(),
719 PresentBackend::Sync(_) | PresentBackend::None => true,
720 }
721 }
722
723 #[cfg(not(target_arch = "wasm32"))]
729 pub fn publish_frame(&mut self, width: u32, height: u32) -> PublishOutcome {
730 self.drain_replay_acks();
736 let PresentBackend::Threaded(handle) = &mut self.backend else {
737 log::error!("publish_frame called without a threaded present runtime");
738 return PublishOutcome::NoCredit;
739 };
740 if !handle.has_credit() {
741 return PublishOutcome::NoCredit;
742 }
743 let replay_supported = handle
744 .status()
745 .replay_supported
746 .load(std::sync::atomic::Ordering::Relaxed);
747 let Some(mut packet) = self.frontend.build_frame_packet(
748 width,
749 height,
750 replay_supported,
751 self.renderer_epoch,
752 self.surface_epoch,
753 ) else {
754 return PublishOutcome::NoGraph;
755 };
756 packet.recycled_confirmations = self.pending_recycled_confirmations.take();
757 match handle.publish(packet) {
758 Ok(()) => PublishOutcome::Published,
759 Err(mut packet) => {
760 if let Some(confirmations) = packet.recycled_confirmations.take() {
763 self.pending_recycled_confirmations = Some(confirmations);
764 }
765 let mut returns = RenderReturns::default();
766 let _ = GpuRenderer::cancel_packet(
767 *packet,
768 CancelReason::SurfaceUnavailable,
769 &mut returns,
770 );
771 if let Some(confirmations) = self.frontend.apply_returns(returns) {
772 self.stash_recycled_confirmations(confirmations);
773 }
774 log::error!("present runtime unavailable; frame recovered, not published");
775 PublishOutcome::NoCredit
776 }
777 }
778 }
779
780 #[cfg(not(target_arch = "wasm32"))]
785 pub fn drain_present_returns(&mut self) -> usize {
786 self.drain_present_returns_with(&mut |_, _, _| {})
787 }
788
789 #[cfg(not(target_arch = "wasm32"))]
793 pub fn drain_present_returns_with(
794 &mut self,
795 on_return: &mut dyn FnMut(u64, PresentOutcome, PresentTimings),
796 ) -> usize {
797 self.drain_replay_acks();
801 let mut drained = 0;
802 loop {
803 let returns = {
806 let PresentBackend::Threaded(handle) = &mut self.backend else {
807 break;
808 };
809 match handle.try_drain() {
810 Some(returns) => returns,
811 None => break,
812 }
813 };
814 drained += 1;
815 let frame_id = returns.frame_id;
816 let outcome = returns.outcome;
817 let timings = returns.timings;
818 if let Some(confirmations) = self.frontend.apply_returns(returns) {
819 self.stash_recycled_confirmations(confirmations);
820 }
821 on_return(frame_id, outcome, timings);
822 }
823 drained
824 }
825
826 #[cfg(not(target_arch = "wasm32"))]
832 pub fn present_replace_surface(
833 &mut self,
834 surface: wgpu::Surface<'static>,
835 config: wgpu::SurfaceConfiguration,
836 ) -> bool {
837 let surface_epoch = self.surface_epoch;
838 let Some(handle) = self.present_handle_mut() else {
839 log::error!("present_replace_surface called without a threaded present runtime");
840 return false;
841 };
842 handle.send_control_and_wait(
843 move |ack| PresentControl::ReplaceSurface {
844 surface,
845 config,
846 surface_epoch,
847 ack,
848 },
849 "replace surface",
850 )
851 }
852
853 #[cfg(not(target_arch = "wasm32"))]
857 pub fn present_reconfigure(&mut self, config: wgpu::SurfaceConfiguration) -> bool {
858 let surface_epoch = self.surface_epoch;
859 let Some(handle) = self.present_handle_mut() else {
860 log::error!("present_reconfigure called without a threaded present runtime");
861 return false;
862 };
863 handle.send_control_and_wait(
864 move |ack| PresentControl::Reconfigure {
865 config,
866 surface_epoch,
867 ack,
868 },
869 "reconfigure surface",
870 )
871 }
872
873 #[cfg(not(target_arch = "wasm32"))]
877 pub fn present_drop_surface(&mut self) -> bool {
878 let Some(handle) = self.present_handle_mut() else {
879 log::error!("present_drop_surface called without a threaded present runtime");
880 return false;
881 };
882 handle.send_control_and_wait(|ack| PresentControl::DropSurface { ack }, "drop surface")
883 }
884
885 #[cfg(not(target_arch = "wasm32"))]
888 pub fn shutdown_present_runtime(&mut self) {
889 if matches!(self.backend, PresentBackend::Threaded(_)) {
890 self.retire_live_backend();
891 }
892 }
893
894 #[cfg(not(target_arch = "wasm32"))]
897 #[doc(hidden)]
898 pub fn present_attach_offscreen_for_tests(&mut self, width: u32, height: u32) -> bool {
899 let Some(handle) = self.present_handle_mut() else {
900 return false;
901 };
902 handle.send_control_and_wait(
903 move |ack| PresentControl::AttachOffscreenTargetForTests { width, height, ack },
904 "attach offscreen target",
905 )
906 }
907
908 #[cfg(not(target_arch = "wasm32"))]
911 #[doc(hidden)]
912 pub fn send_attach_offscreen_unacked_for_tests(
913 &mut self,
914 width: u32,
915 height: u32,
916 ) -> Option<std::sync::mpsc::Receiver<()>> {
917 let handle = self.present_handle_mut()?;
918 handle.send_control_unacked(move |ack| PresentControl::AttachOffscreenTargetForTests {
919 width,
920 height,
921 ack,
922 })
923 }
924
925 #[cfg(not(target_arch = "wasm32"))]
929 #[doc(hidden)]
930 pub fn send_reconfigure_unacked_for_tests(
931 &mut self,
932 config: wgpu::SurfaceConfiguration,
933 ) -> Option<std::sync::mpsc::Receiver<()>> {
934 let surface_epoch = self.surface_epoch;
935 let handle = self.present_handle_mut()?;
936 handle.send_control_unacked(move |ack| PresentControl::Reconfigure {
937 config,
938 surface_epoch,
939 ack,
940 })
941 }
942
943 #[cfg(not(target_arch = "wasm32"))]
945 #[doc(hidden)]
946 pub fn send_drop_surface_unacked_for_tests(&mut self) -> Option<std::sync::mpsc::Receiver<()>> {
947 let handle = self.present_handle_mut()?;
948 handle.send_control_unacked(|ack| PresentControl::DropSurface { ack })
949 }
950
951 pub fn last_published_frame_id(&self) -> u64 {
956 self.frontend.frame_sequence
957 }
958
959 #[cfg(not(target_arch = "wasm32"))]
964 #[doc(hidden)]
965 pub fn seed_recycled_confirmations_for_tests(&mut self, capacity: usize) {
966 self.pending_recycled_confirmations = Some(Vec::with_capacity(capacity));
967 }
968
969 #[cfg(not(target_arch = "wasm32"))]
973 #[doc(hidden)]
974 pub fn pending_recycled_confirmations_capacity_for_tests(&self) -> Option<usize> {
975 self.pending_recycled_confirmations
976 .as_ref()
977 .map(Vec::capacity)
978 }
979
980 #[cfg(not(target_arch = "wasm32"))]
983 #[doc(hidden)]
984 pub fn present_status_snapshot_for_tests(&self) -> Option<(bool, bool, u64)> {
985 match &self.backend {
986 PresentBackend::Threaded(handle) => {
987 let status = handle.status();
988 Some((
989 status
990 .needs_frame_warmup
991 .load(std::sync::atomic::Ordering::Relaxed),
992 status
993 .replay_supported
994 .load(std::sync::atomic::Ordering::Relaxed),
995 status
996 .presented_frames
997 .load(std::sync::atomic::Ordering::Relaxed),
998 ))
999 }
1000 _ => None,
1001 }
1002 }
1003
1004 pub fn last_frame_stats(&self) -> Option<RenderStatsSnapshot> {
1005 self.sync_gpu_renderer()
1006 .and_then(GpuRenderer::last_frame_stats)
1007 }
1008
1009 pub fn debug_cpu_allocation_stats(&self) -> DebugCpuAllocationStats {
1010 let mut stats = self
1011 .sync_gpu_renderer()
1012 .map(GpuRenderer::debug_cpu_allocation_stats)
1013 .unwrap_or_default();
1014 stats.scene_graph_node_count = self
1015 .frontend
1016 .scene
1017 .graph
1018 .as_ref()
1019 .map(RenderGraph::node_count)
1020 .unwrap_or(0);
1021 stats.scene_graph_heap_bytes = self
1022 .frontend
1023 .scene
1024 .graph
1025 .as_ref()
1026 .map(RenderGraph::heap_bytes)
1027 .unwrap_or(0);
1028 stats.scene_hits_len = self.frontend.scene.hits.len();
1029 stats.scene_hits_cap = self.frontend.scene.hits.capacity();
1030 stats.scene_node_index_len = self.frontend.scene.node_index.len();
1031 stats.scene_node_index_cap = self.frontend.scene.node_index.capacity();
1032 stats.layer_surface_rect_cache_len += self.frontend.layer_surface_rect_cache.len();
1036 stats.layer_surface_rect_cache_cap += self.frontend.layer_surface_rect_cache.capacity();
1037 stats.layer_surface_requirements_cache_len +=
1038 self.frontend.layer_surface_requirements_cache.len();
1039 stats.layer_surface_requirements_cache_cap +=
1040 self.frontend.layer_surface_requirements_cache.capacity();
1041 stats
1042 }
1043
1044 pub fn try_device(&self) -> Option<&wgpu::Device> {
1048 self.sync_gpu_renderer().map(|r| &*r.device)
1049 }
1050
1051 #[doc(hidden)]
1055 pub fn device_error_count_for_tests(&self) -> u64 {
1056 self.sync_gpu_renderer()
1057 .map(GpuRenderer::device_error_count)
1058 .unwrap_or(0)
1059 }
1060
1061 #[cfg(not(target_arch = "wasm32"))]
1066 #[doc(hidden)]
1067 pub fn instanced_quads_active(&self) -> bool {
1068 self.sync_gpu_renderer()
1069 .is_some_and(GpuRenderer::instanced_quads_active)
1070 }
1071
1072 #[cfg(not(target_arch = "wasm32"))]
1075 #[doc(hidden)]
1076 pub fn replay_slot_mesh_stats(&self) -> (usize, usize) {
1077 self.sync_gpu_renderer()
1078 .map(GpuRenderer::replay_slot_mesh_stats)
1079 .unwrap_or((0, 0))
1080 }
1081
1082 #[cfg(not(target_arch = "wasm32"))]
1086 #[doc(hidden)]
1087 pub fn replay_slot_mesh_engagement(&self) -> (usize, usize, usize) {
1088 self.sync_gpu_renderer()
1089 .map(GpuRenderer::replay_slot_mesh_engagement)
1090 .unwrap_or((0, 0, 0))
1091 }
1092
1093 #[cfg(not(target_arch = "wasm32"))]
1096 #[doc(hidden)]
1097 pub fn retained_bundle_stats(&self) -> (u64, u64) {
1098 self.sync_gpu_renderer()
1099 .map(GpuRenderer::retained_bundle_stats)
1100 .unwrap_or((0, 0))
1101 }
1102
1103 #[cfg(not(target_arch = "wasm32"))]
1107 #[doc(hidden)]
1108 pub fn rim_meshes_emitted(&self) -> u64 {
1109 self.sync_gpu_renderer()
1110 .map(GpuRenderer::rim_meshes_emitted)
1111 .unwrap_or(0)
1112 }
1113
1114 #[cfg(not(target_arch = "wasm32"))]
1119 #[doc(hidden)]
1120 pub fn static_span_stats(&self) -> (u64, u64) {
1121 self.sync_gpu_renderer()
1122 .map(GpuRenderer::static_span_stats)
1123 .unwrap_or((0, 0))
1124 }
1125
1126 #[cfg(not(target_arch = "wasm32"))]
1130 #[doc(hidden)]
1131 pub fn segment_surface_stats(&self) -> (u64, u64, u64, u64, u64) {
1132 self.sync_gpu_renderer()
1133 .map(GpuRenderer::segment_surface_stats)
1134 .unwrap_or((0, 0, 0, 0, 0))
1135 }
1136
1137 #[cfg(not(target_arch = "wasm32"))]
1142 #[doc(hidden)]
1143 pub fn replay_generation_drops_for_tests(&self) -> u64 {
1144 self.sync_gpu_renderer()
1145 .map(GpuRenderer::replay_generation_drops)
1146 .unwrap_or(0)
1147 }
1148
1149 #[cfg(not(target_arch = "wasm32"))]
1157 #[doc(hidden)]
1158 pub fn replay_ops_roundtrip_for_tests(&mut self, generation_skew: u64) -> usize {
1159 self.sync_gpu_renderer_mut()
1160 .expect("GPU renderer not initialized")
1161 .replay_ops_roundtrip_for_tests(generation_skew)
1162 }
1163
1164 #[doc(hidden)]
1170 pub fn build_frame_packet_for_tests(
1171 &mut self,
1172 width: u32,
1173 height: u32,
1174 ) -> Option<HeldFramePacket> {
1175 let replay_supported = match &self.backend {
1176 PresentBackend::Sync(gpu_renderer) => gpu_renderer.replay_supported(),
1177 #[cfg(not(target_arch = "wasm32"))]
1178 PresentBackend::Threaded(handle) => handle
1179 .status()
1180 .replay_supported
1181 .load(std::sync::atomic::Ordering::Relaxed),
1182 PresentBackend::None => false,
1183 };
1184 self.frontend
1185 .build_frame_packet(
1186 width,
1187 height,
1188 replay_supported,
1189 self.renderer_epoch,
1190 self.surface_epoch,
1191 )
1192 .map(HeldFramePacket)
1193 }
1194
1195 #[doc(hidden)]
1199 pub fn render_held_packet_for_tests(
1200 &mut self,
1201 view: &wgpu::TextureView,
1202 width: u32,
1203 height: u32,
1204 packet: HeldFramePacket,
1205 ) -> Result<PresentOutcome, WgpuRendererError> {
1206 let PresentBackend::Sync(gpu_renderer) = &mut self.backend else {
1207 return Err(WgpuRendererError::Wgpu(
1208 "GPU renderer not initialized for synchronous rendering. Call init_gpu() first."
1209 .to_string(),
1210 ));
1211 };
1212 let frontend = &mut self.frontend;
1213 let mut returns = RenderReturns::default();
1214 let result = gpu_renderer.render(
1215 view,
1216 width,
1217 height,
1218 packet.0,
1219 self.surface_epoch,
1220 &mut returns,
1221 );
1222 let outcome = returns.outcome;
1223 if let Some(confirmations) = frontend.apply_returns(returns) {
1224 gpu_renderer.restore_replay_ack_confirmations(confirmations);
1225 }
1226 result.map_err(WgpuRendererError::Wgpu)?;
1227 Ok(outcome)
1228 }
1229
1230 #[doc(hidden)]
1233 pub fn has_retained_direct_scene_for_tests(&self) -> bool {
1234 !self.frontend.retained_direct_scenes.is_empty()
1235 }
1236}
1237
1238#[doc(hidden)]
1241pub struct HeldFramePacket(frame_packet::FramePacket);
1242
1243#[cfg(not(target_arch = "wasm32"))]
1247#[doc(hidden)]
1248pub struct InlinePresentRuntime {
1249 state: PresentState,
1250 msg_rx: std::sync::mpsc::Receiver<PresentMsg>,
1251 shutdown_seen: bool,
1252}
1253
1254#[cfg(not(target_arch = "wasm32"))]
1255impl InlinePresentRuntime {
1256 pub fn pump(&mut self) -> bool {
1261 if self.shutdown_seen {
1262 return false;
1263 }
1264 while let Ok(msg) = self.msg_rx.try_recv() {
1265 if !self.state.run_once(msg) {
1266 self.shutdown_seen = true;
1267 return false;
1268 }
1269 }
1270 self.state.consume_waiting();
1271 true
1272 }
1273
1274 pub fn has_waiting_packet(&self) -> bool {
1277 self.state.has_waiting_packet()
1278 }
1279
1280 pub fn step_one_message(&mut self) -> bool {
1284 if self.shutdown_seen {
1285 return false;
1286 }
1287 match self.msg_rx.try_recv() {
1288 Ok(msg) => {
1289 if !self.state.run_once(msg) {
1290 self.shutdown_seen = true;
1291 }
1292 true
1293 }
1294 Err(_) => false,
1295 }
1296 }
1297
1298 pub fn consume_waiting(&mut self) {
1301 self.state.consume_waiting();
1302 }
1303
1304 pub fn store_ack_confirmations_capacity(&self) -> usize {
1307 self.state.store_ack_confirmations_capacity()
1308 }
1309}
1310
1311impl Default for WgpuRenderer {
1312 fn default() -> Self {
1313 Self::new(&[])
1314 }
1315}
1316
1317impl Renderer for WgpuRenderer {
1318 type Scene = Scene;
1319 type Error = WgpuRendererError;
1320
1321 fn attach_app_context_services(&mut self, app_context: &cranpose_ui::AppContext) {
1322 app_context.set_text_measurer(SoftwareTextMeasurer::from_font_set(
1323 self.frontend.text_fonts.clone(),
1324 8192,
1325 ));
1326 self.frontend.app_context = Some(app_context.downgrade());
1327 }
1328
1329 fn scene(&self) -> &Self::Scene {
1330 &self.frontend.scene
1331 }
1332
1333 fn scene_mut(&mut self) -> &mut Self::Scene {
1334 &mut self.frontend.scene
1335 }
1336
1337 fn rebuild_scene(
1338 &mut self,
1339 layout_tree: &LayoutTree,
1340 _viewport: Size,
1341 ) -> Result<(), Self::Error> {
1342 self.frontend.scene.clear();
1343 self.frontend.dev_overlay_graph = None;
1344 self.frontend.dev_overlay_cache = None;
1345 pipeline::render_layout_tree(layout_tree.root(), &mut self.frontend.scene);
1347 Ok(())
1348 }
1349
1350 fn rebuild_scene_from_applier(
1351 &mut self,
1352 applier: &mut MemoryApplier,
1353 root: NodeId,
1354 _viewport: Size,
1355 ) -> Result<(), Self::Error> {
1356 self.frontend.scene.clear();
1357 self.frontend.dev_overlay_graph = None;
1358 self.frontend.dev_overlay_cache = None;
1359 pipeline::render_from_applier(applier, root, &mut self.frontend.scene, 1.0);
1362 Ok(())
1363 }
1364
1365 fn update_scene_from_applier(
1366 &mut self,
1367 applier: &mut MemoryApplier,
1368 root: NodeId,
1369 viewport: Size,
1370 dirty_nodes: &[NodeId],
1371 ) -> Result<(), Self::Error> {
1372 if dirty_nodes.is_empty() {
1373 return self.rebuild_scene_from_applier(applier, root, viewport);
1374 }
1375 pipeline::update_from_applier(
1376 applier,
1377 root,
1378 &mut self.frontend.scene,
1379 1.0,
1380 dirty_nodes,
1381 true,
1382 );
1383 Ok(())
1384 }
1385
1386 fn update_visual_scene_from_applier(
1387 &mut self,
1388 applier: &mut MemoryApplier,
1389 root: NodeId,
1390 viewport: Size,
1391 dirty_nodes: &[NodeId],
1392 ) -> Result<(), Self::Error> {
1393 if dirty_nodes.is_empty() {
1394 return self.rebuild_scene_from_applier(applier, root, viewport);
1395 }
1396 pipeline::update_from_applier(
1397 applier,
1398 root,
1399 &mut self.frontend.scene,
1400 1.0,
1401 dirty_nodes,
1402 false,
1403 );
1404 Ok(())
1405 }
1406
1407 fn draw_dev_overlay(&mut self, text: &str, viewport: Size) {
1408 const DEV_OVERLAY_NODE_ID: NodeId = NodeId::MAX;
1409 let key = cranpose_render_common::dev_overlay::DevOverlayKey::new(text, viewport);
1410 if self.frontend.dev_overlay_graph.is_some()
1411 && self
1412 .frontend
1413 .dev_overlay_cache
1414 .as_ref()
1415 .is_some_and(|cache| {
1416 cache.text == key.text
1417 && cache.viewport_width_bits == key.viewport_width_bits
1418 && cache.viewport_height_bits == key.viewport_height_bits
1419 })
1420 {
1421 return;
1422 }
1423 self.frontend.dev_overlay_graph = Some(
1424 cranpose_render_common::dev_overlay::build_dev_overlay_graph(
1425 text,
1426 viewport,
1427 DEV_OVERLAY_NODE_ID,
1428 ),
1429 );
1430 self.frontend.dev_overlay_cache = Some(DevOverlayCache {
1431 text: key.text,
1432 viewport_width_bits: key.viewport_width_bits,
1433 viewport_height_bits: key.viewport_height_bits,
1434 });
1435 }
1436
1437 fn needs_frame_warmup(&self) -> bool {
1438 match &self.backend {
1439 PresentBackend::Sync(gpu_renderer) => gpu_renderer.needs_frame_warmup(),
1440 #[cfg(not(target_arch = "wasm32"))]
1444 PresentBackend::Threaded(handle) => handle
1445 .status()
1446 .needs_frame_warmup
1447 .load(std::sync::atomic::Ordering::Relaxed),
1448 PresentBackend::None => false,
1449 }
1450 }
1451}
1452
1453#[cfg(test)]
1454mod tests {
1455 use super::*;
1456 use crate::pipeline::TextLayoutResolver;
1457 use cranpose_render_common::graph::RenderNode;
1458 use cranpose_ui_graphics::GraphicsLayer;
1459 use std::cell::Cell;
1460
1461 static TEST_FONT: &[u8] =
1462 cranpose_render_common::software_text_raster::DEFAULT_SOFTWARE_TEXT_FONT_BYTES;
1463
1464 #[test]
1465 fn dev_overlay_is_recorded_outside_app_graph() {
1466 let mut renderer = WgpuRenderer::new(&[]);
1467 renderer.draw_dev_overlay(
1468 "240 FPS | avg 4.0ms | p95 4.5ms",
1469 Size {
1470 width: 800.0,
1471 height: 600.0,
1472 },
1473 );
1474
1475 assert!(
1476 renderer
1477 .frontend
1478 .scene
1479 .graph
1480 .as_ref()
1481 .is_none_or(|graph| graph.root.children.iter().all(|child| {
1482 !matches!(
1483 child,
1484 RenderNode::Layer(layer) if layer.node_id == Some(NodeId::MAX)
1485 )
1486 })),
1487 "dev overlay must not be mixed into the app scene graph"
1488 );
1489
1490 let graph = renderer
1491 .frontend
1492 .dev_overlay_graph
1493 .as_ref()
1494 .expect("overlay graph");
1495 let Some(RenderNode::Layer(overlay)) = graph.root.children.last() else {
1496 panic!("dev overlay should be the final top-level layer");
1497 };
1498
1499 assert_eq!(overlay.node_id, Some(NodeId::MAX));
1500 assert_eq!(
1501 overlay.graphics_layer.compositing_strategy,
1502 GraphicsLayer::default().compositing_strategy,
1503 "dev overlay should not allocate an offscreen surface"
1504 );
1505 }
1506
1507 struct CountingTextMeasurer {
1508 inner: SoftwareTextMeasurer,
1509 layout_calls: Rc<Cell<usize>>,
1510 }
1511
1512 impl CountingTextMeasurer {
1513 fn new(layout_calls: Rc<Cell<usize>>) -> Self {
1514 Self {
1515 inner: SoftwareTextMeasurer::from_fonts_or_default(&[TEST_FONT], 16),
1516 layout_calls,
1517 }
1518 }
1519 }
1520
1521 impl TextMeasurer for CountingTextMeasurer {
1522 fn measure(
1523 &self,
1524 text: &cranpose_ui::text::AnnotatedString,
1525 style: &cranpose_ui::text::TextStyle,
1526 ) -> cranpose_ui::TextMetrics {
1527 self.inner.measure(text, style)
1528 }
1529
1530 fn get_offset_for_position(
1531 &self,
1532 text: &cranpose_ui::text::AnnotatedString,
1533 style: &cranpose_ui::text::TextStyle,
1534 x: f32,
1535 y: f32,
1536 ) -> usize {
1537 self.inner.get_offset_for_position(text, style, x, y)
1538 }
1539
1540 fn get_cursor_x_for_offset(
1541 &self,
1542 text: &cranpose_ui::text::AnnotatedString,
1543 style: &cranpose_ui::text::TextStyle,
1544 offset: usize,
1545 ) -> f32 {
1546 self.inner.get_cursor_x_for_offset(text, style, offset)
1547 }
1548
1549 fn layout(
1550 &self,
1551 text: &cranpose_ui::text::AnnotatedString,
1552 style: &cranpose_ui::text::TextStyle,
1553 ) -> cranpose_ui::text_layout_result::TextLayoutResult {
1554 self.layout_calls.set(self.layout_calls.get() + 1);
1555 self.inner.layout(text, style)
1556 }
1557 }
1558
1559 #[test]
1560 fn headless_text_measurer_uses_software_text_font() {
1561 let measurer = headless_text_measurer_with_fonts(&[TEST_FONT]);
1562 let text = cranpose_ui::text::AnnotatedString::from("software text measurement");
1563 let style = cranpose_ui::text::TextStyle::default();
1564
1565 let metrics = measurer.measure(&text, &style);
1566 let layout = measurer.layout(&text, &style);
1567
1568 assert!(metrics.width > 0.0);
1569 assert!(metrics.height > 0.0);
1570 assert_eq!(layout.lines.len(), metrics.line_count);
1571 }
1572
1573 #[test]
1574 fn renderer_measurement_uses_software_text_service_without_render_cache_side_effect() {
1575 let mut renderer = WgpuRenderer::new(&[TEST_FONT]);
1576 let app_context = cranpose_ui::AppContext::new();
1577 renderer.attach_app_context_services(&app_context);
1578
1579 let metrics = app_context.enter(|| {
1580 let text = cranpose_ui::text::AnnotatedString::from("phase local text cache");
1581 let style = cranpose_ui::text::TextStyle {
1582 span_style: cranpose_ui::text::SpanStyle {
1583 font_size: cranpose_ui::text::TextUnit::Sp(14.0),
1584 ..Default::default()
1585 },
1586 paragraph_style: cranpose_ui::text::ParagraphStyle {
1587 platform_style: Some(cranpose_ui::text::PlatformParagraphStyle {
1588 include_font_padding: None,
1589 shaping: Some(cranpose_ui::text::TextShaping::Basic),
1590 }),
1591 ..Default::default()
1592 },
1593 };
1594 cranpose_ui::text::measure_text(&text, &style)
1595 });
1596
1597 assert!(
1598 metrics.width > 0.0,
1599 "software text service should measure text"
1600 );
1601 assert_eq!(
1602 renderer.frontend.text_state.text_cache_len(),
1603 0,
1604 "WGPU must not keep a renderer-side shaping cache for measurement"
1605 );
1606 }
1607
1608 #[test]
1609 fn renderer_attached_text_service_measures_long_multiline_text_with_software_line_height() {
1610 let mut renderer = WgpuRenderer::new(&[TEST_FONT]);
1611 let app_context = cranpose_ui::AppContext::new();
1612 renderer.attach_app_context_services(&app_context);
1613
1614 let prepared = app_context.enter(|| {
1615 let text = cranpose_ui::text::AnnotatedString::from(
1616 (0..48)
1617 .map(|line| format!("// markdown code line {line:02}"))
1618 .collect::<Vec<_>>()
1619 .join("\n"),
1620 );
1621 let style = cranpose_ui::text::TextStyle::default();
1622 cranpose_ui::text::prepare_text_layout(
1623 &text,
1624 &style,
1625 cranpose_ui::text::TextLayoutOptions::default(),
1626 Some(952.0),
1627 )
1628 });
1629
1630 assert_eq!(prepared.metrics.line_count, 48);
1631 assert!(
1632 prepared.metrics.line_height > 18.0,
1633 "renderer-attached text service must not use fallback monospaced line height: {:?}",
1634 prepared.metrics
1635 );
1636 assert!(
1637 prepared.metrics.height > 900.0,
1638 "48 software-measured lines should not collapse to a viewport-sized block: {:?}",
1639 prepared.metrics
1640 );
1641 }
1642
1643 #[test]
1644 fn render_text_layout_routes_through_attached_app_context_service() {
1645 let mut renderer = WgpuRenderer::new(&[TEST_FONT]);
1646 let app_context = cranpose_ui::AppContext::new();
1647 renderer.attach_app_context_services(&app_context);
1648 let layout_calls = Rc::new(Cell::new(0));
1649 app_context.set_text_measurer(CountingTextMeasurer::new(Rc::clone(&layout_calls)));
1650
1651 app_context.enter(|| {
1652 let text = cranpose_ui::text::AnnotatedString::from("render text");
1653 let style = cranpose_ui::text::TextStyle::default();
1654 let layout = renderer.frontend.text_state.layout_text(&text, &style);
1655 assert!(layout.width > 0.0);
1656 });
1657
1658 assert_eq!(layout_calls.get(), 1);
1659 }
1660}