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;
21pub use offscreen::display_surface_usages;
22mod pipeline;
23#[cfg(not(target_arch = "wasm32"))]
24mod pipeline_disk_cache;
25#[cfg(not(target_arch = "wasm32"))]
26mod present_runtime;
27mod render;
28mod run_entry;
29mod scene;
30#[cfg(not(target_arch = "wasm32"))]
31mod segment_surface;
32mod shader_cache;
33mod shaders;
34#[cfg(not(target_arch = "wasm32"))]
35mod shape_replay;
36#[cfg(not(target_arch = "wasm32"))]
37mod stage_executor;
38mod surface_executor;
39mod surface_plan;
40mod surface_requirements;
41#[cfg(test)]
42mod test_support;
43
44#[doc(hidden)]
45#[cfg(not(target_arch = "wasm32"))]
46pub use display_clip::pixel_is_visible as display_clip_pixel_is_visible;
47pub use display_clip::DisplayVisibleRegion;
48pub use frame_packet::PresentTimings;
49#[doc(hidden)]
50pub use frame_packet::{CancelReason, PresentOutcome};
51pub use gpu_stats::FrameStatsSnapshot as RenderStatsSnapshot;
52#[doc(hidden)]
53#[cfg(not(target_arch = "wasm32"))]
54pub use pipeline::retained_feed_generation;
55pub use render::frames_presented;
56pub use scene::{ClickAction, HitRegion, Scene};
57#[doc(hidden)]
58#[cfg(not(target_arch = "wasm32"))]
59pub use shape_replay::feed_live_stats as command_feed_live_stats;
60#[doc(hidden)]
61#[cfg(not(target_arch = "wasm32"))]
62pub use shape_replay::{inject_feed_capture_for_tests, pending_feed_capture_count_for_tests};
63#[doc(hidden)]
64#[cfg(not(target_arch = "wasm32"))]
65pub use shape_replay::{planner_replay_queue_stats_for_tests, recycled_ops_capacities_for_tests};
66
67use cranpose_core::{MemoryApplier, NodeId};
68use cranpose_render_common::{
69 graph::RenderGraph,
70 software_text_raster::{
71 software_text_font_set_from_fonts_or_default, SoftwareTextFontSet, SoftwareTextMeasurer,
72 },
73 RenderScene, Renderer,
74};
75use cranpose_ui::{LayoutTree, TextMeasurer};
76use cranpose_ui_graphics::{Rect, Size};
77use frame_packet::RenderReturns;
78#[cfg(not(target_arch = "wasm32"))]
79use frame_packet::ReplayConfirmation;
80use frontend::{DevOverlayCache, RendererFrontend};
81#[cfg(not(target_arch = "wasm32"))]
82use present_runtime::{
83 PresentControl, PresentHandle, PresentMsg, PresentRuntimeInit, PresentState,
84};
85use render::GpuRenderer;
86use std::rc::Rc;
87use std::sync::Arc;
88
89pub(crate) fn rect_to_quad(rect: Rect) -> [[f32; 2]; 4] {
91 [
92 [rect.x, rect.y],
93 [rect.x + rect.width, rect.y],
94 [rect.x, rect.y + rect.height],
95 [rect.x + rect.width, rect.y + rect.height],
96 ]
97}
98
99#[derive(Debug)]
100pub enum WgpuRendererError {
101 Layout(String),
102 Wgpu(String),
103}
104
105#[derive(Debug, Clone)]
107pub struct CapturedFrame {
108 pub width: u32,
109 pub height: u32,
110 pub pixels: Vec<u8>,
111}
112
113#[doc(hidden)]
114#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
115pub struct DebugCpuAllocationStats {
116 pub scene_graph_node_count: usize,
117 pub scene_graph_heap_bytes: usize,
118 pub scene_hits_len: usize,
119 pub scene_hits_cap: usize,
120 pub scene_node_index_len: usize,
121 pub scene_node_index_cap: usize,
122 pub text_renderer_pool_len: usize,
123 pub text_renderer_pool_cap: usize,
124 pub swash_image_cache_len: usize,
125 pub swash_image_cache_cap: usize,
126 pub swash_outline_cache_len: usize,
127 pub swash_outline_cache_cap: usize,
128 pub image_texture_cache_len: usize,
129 pub image_texture_cache_cap: usize,
130 pub scratch_shape_data_cap: usize,
131 pub scratch_gradients_cap: usize,
132 pub scratch_image_vertices_cap: usize,
133 pub scratch_image_indices_cap: usize,
134 pub scratch_image_cmds_cap: usize,
135 pub scratch_segment_items_cap: usize,
136 pub scratch_effect_ranges_cap: usize,
137 pub scratch_layer_events_cap: usize,
138 pub staged_upload_bytes_cap: usize,
139 pub staged_upload_copies_cap: usize,
140 pub layer_surface_cache_len: usize,
141 pub layer_surface_cache_cap: usize,
142 pub layer_surface_cache_identity_len: usize,
143 pub layer_surface_cache_identity_cap: usize,
144 pub layer_surface_rect_cache_len: usize,
145 pub layer_surface_rect_cache_cap: usize,
146 pub layer_surface_requirements_cache_len: usize,
147 pub layer_surface_requirements_cache_cap: usize,
148 pub layer_cache_seen_this_frame_len: usize,
149 pub layer_cache_seen_this_frame_cap: usize,
150}
151
152pub(crate) struct TextSystemState {
153 measurer: SoftwareTextMeasurer,
154}
155
156impl TextSystemState {
157 fn from_font_set(fonts: SoftwareTextFontSet) -> Self {
158 Self {
159 measurer: SoftwareTextMeasurer::from_font_set(fonts, 8192),
160 }
161 }
162
163 pub(crate) fn text_cache_len(&self) -> usize {
164 0
165 }
166}
167
168impl pipeline::TextLayoutResolver for TextSystemState {
169 fn layout_text(
170 &mut self,
171 text: &cranpose_ui::text::AnnotatedString,
172 style: &cranpose_ui::text::TextStyle,
173 ) -> cranpose_ui::text_layout_result::TextLayoutResult {
174 if cranpose_ui::has_current_app_context() {
175 cranpose_ui::text::layout_text(text, style)
176 } else {
177 self.measurer.layout(text, style)
178 }
179 }
180}
181
182#[derive(Clone)]
183pub struct WgpuTextSystem {
184 software_fonts: SoftwareTextFontSet,
185}
186
187impl WgpuTextSystem {
188 pub fn from_fonts(fonts: &[&[u8]]) -> Self {
189 Self {
190 software_fonts: software_text_font_set_from_fonts_or_default(fonts),
191 }
192 }
193
194 pub fn from_font_set(software_fonts: SoftwareTextFontSet) -> Self {
198 Self { software_fonts }
199 }
200
201 pub(crate) fn render_state(&self) -> TextSystemState {
202 TextSystemState::from_font_set(self.software_fonts.clone())
203 }
204
205 pub(crate) fn software_fonts(&self) -> SoftwareTextFontSet {
206 self.software_fonts.clone()
207 }
208}
209
210pub fn headless_text_measurer() -> Rc<dyn TextMeasurer> {
212 headless_text_measurer_with_fonts(&[])
213}
214
215pub fn headless_text_measurer_with_fonts(fonts: &[&[u8]]) -> Rc<dyn TextMeasurer> {
217 Rc::new(SoftwareTextMeasurer::from_fonts_or_default(fonts, 8192))
218}
219
220enum PresentBackend {
228 None,
230 Sync(Box<GpuRenderer>),
232 #[cfg(not(target_arch = "wasm32"))]
234 Threaded(PresentHandle),
235}
236
237#[derive(Clone, Copy, Debug, PartialEq, Eq)]
239pub enum PublishOutcome {
240 NoGraph,
242 NoCredit,
245 Published,
247}
248
249pub struct WgpuRenderer {
257 frontend: RendererFrontend,
260 backend: PresentBackend,
262 #[cfg(not(target_arch = "wasm32"))]
266 pending_recycled_confirmations: Option<Vec<ReplayConfirmation>>,
267 renderer_epoch: u64,
272 surface_epoch: u64,
277 #[cfg_attr(target_arch = "wasm32", allow(dead_code))]
284 display_visible_region: DisplayVisibleRegion,
285}
286
287impl WgpuRenderer {
288 fn update_scene_and_plan_memo(
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 let outcome = 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 match outcome {
306 pipeline::SceneUpdateOutcome::Patched => {
307 for node_id in &changed_nodes {
308 self.frontend
309 .layer_surface_requirements_cache
310 .remove(node_id);
311 }
312 }
313 pipeline::SceneUpdateOutcome::Rebuilt => {
314 self.frontend.layer_surface_requirements_cache.clear();
315 }
316 }
317 changed_nodes.clear();
318 self.frontend.changed_nodes = changed_nodes;
319 }
320
321 pub fn new(fonts: &[&[u8]]) -> Self {
328 Self::with_text_system(WgpuTextSystem::from_fonts(fonts))
329 }
330
331 pub fn with_font_set(fonts: SoftwareTextFontSet) -> Self {
336 Self::with_text_system(WgpuTextSystem::from_font_set(fonts))
337 }
338
339 pub fn with_text_system(text_system: WgpuTextSystem) -> Self {
340 Self {
341 frontend: RendererFrontend::new(
342 text_system.render_state(),
343 text_system.software_fonts(),
344 ),
345 backend: PresentBackend::None,
346 #[cfg(not(target_arch = "wasm32"))]
347 pending_recycled_confirmations: None,
348 renderer_epoch: 0,
349 surface_epoch: 0,
350 display_visible_region: DisplayVisibleRegion::Full,
351 }
352 }
353
354 fn sync_gpu_renderer(&self) -> Option<&GpuRenderer> {
356 match &self.backend {
357 PresentBackend::Sync(gpu_renderer) => Some(gpu_renderer.as_ref()),
358 _ => None,
359 }
360 }
361
362 fn sync_gpu_renderer_mut(&mut self) -> Option<&mut GpuRenderer> {
363 match &mut self.backend {
364 PresentBackend::Sync(gpu_renderer) => Some(gpu_renderer.as_mut()),
365 _ => None,
366 }
367 }
368
369 #[cfg(not(target_arch = "wasm32"))]
371 fn present_handle_mut(&mut self) -> Option<&mut PresentHandle> {
372 match &mut self.backend {
373 PresentBackend::Threaded(handle) => Some(handle),
374 _ => None,
375 }
376 }
377
378 fn retire_live_backend(&mut self) {
385 #[allow(unused_mut)]
386 let mut backend = std::mem::replace(&mut self.backend, PresentBackend::None);
387 if matches!(backend, PresentBackend::None) {
388 return;
389 }
390 #[cfg(not(target_arch = "wasm32"))]
391 {
392 if let PresentBackend::Threaded(handle) = &mut backend {
393 while let Some((ack, recycled)) = handle.try_drain_ack() {
398 let confirmations = crate::shape_replay::SHAPE_REPLAY
399 .with(|state| state.borrow_mut().apply_ack(ack, recycled));
400 self.stash_recycled_confirmations(confirmations);
401 }
402 while let Some(returns) = handle.try_drain() {
403 if let Some(confirmations) = self.frontend.apply_returns(returns) {
404 self.stash_recycled_confirmations(confirmations);
405 }
406 }
407 handle.shutdown();
408 }
409 crate::shape_replay::SHAPE_REPLAY.with(|state| state.borrow_mut().renderer_replaced());
410 log::warn!(
411 "[command-feed] renderer replaced: retained slots retired, \
412 confirmations revoked, feed generation bumped"
413 );
414 }
415 drop(backend);
416 }
417
418 #[cfg(not(target_arch = "wasm32"))]
421 fn stash_recycled_confirmations(&mut self, confirmations: Vec<ReplayConfirmation>) {
422 match &self.pending_recycled_confirmations {
423 Some(parked) if parked.capacity() >= confirmations.capacity() => {}
424 _ => self.pending_recycled_confirmations = Some(confirmations),
425 }
426 }
427
428 #[cfg(not(target_arch = "wasm32"))]
442 #[doc(hidden)]
443 pub fn drain_replay_acks(&mut self) -> usize {
444 let mut drained = 0;
445 loop {
446 let (ack, recycled) = {
447 let PresentBackend::Threaded(handle) = &mut self.backend else {
448 break;
449 };
450 match handle.try_drain_ack() {
451 Some(batch) => batch,
452 None => break,
453 }
454 };
455 drained += 1;
456 let confirmations = crate::shape_replay::SHAPE_REPLAY
457 .with(|state| state.borrow_mut().apply_ack(ack, recycled));
458 self.stash_recycled_confirmations(confirmations);
459 }
460 drained
461 }
462
463 pub fn init_gpu(
473 &mut self,
474 device: Arc<wgpu::Device>,
475 queue: Arc<wgpu::Queue>,
476 surface_format: wgpu::TextureFormat,
477 adapter_backend: wgpu::Backend,
478 adapter_downlevel: wgpu::DownlevelFlags,
479 ) {
480 self.retire_live_backend();
481 self.renderer_epoch = self.renderer_epoch.wrapping_add(1);
482 #[cfg(not(target_arch = "wasm32"))]
486 let store_feed_generation = pipeline::retained_feed_generation();
487 #[cfg(target_arch = "wasm32")]
488 let store_feed_generation = 0;
489 self.backend = PresentBackend::Sync(Box::new(GpuRenderer::new(
490 device,
491 queue,
492 surface_format,
493 adapter_backend,
494 adapter_downlevel,
495 self.frontend.text_fonts.clone(),
496 self.renderer_epoch,
497 store_feed_generation,
498 )));
499 #[cfg(not(target_arch = "wasm32"))]
500 {
501 let region = self.display_visible_region;
502 if let Some(gpu_renderer) = self.sync_gpu_renderer_mut() {
503 gpu_renderer.set_display_visible_region(region);
504 }
505 }
506 }
507
508 pub fn set_display_visible_region(&mut self, region: DisplayVisibleRegion) {
525 self.display_visible_region = region;
526 #[cfg(not(target_arch = "wasm32"))]
527 match &mut self.backend {
528 PresentBackend::Sync(gpu_renderer) => {
529 gpu_renderer.set_display_visible_region(region);
530 }
531 PresentBackend::Threaded(handle) => {
532 handle.send_display_visible_region(region);
533 }
534 PresentBackend::None => {}
535 }
536 }
537
538 #[cfg(not(target_arch = "wasm32"))]
553 #[allow(clippy::too_many_arguments)]
554 pub fn init_gpu_threaded(
555 &mut self,
556 device: Arc<wgpu::Device>,
557 queue: Arc<wgpu::Queue>,
558 surface_format: wgpu::TextureFormat,
559 adapter_backend: wgpu::Backend,
560 adapter_downlevel: wgpu::DownlevelFlags,
561 waker: Arc<dyn Fn() + Send + Sync>,
562 clock: Option<Arc<dyn Fn() -> i64 + Send + Sync>>,
563 ) -> Result<(), WgpuRendererError> {
564 self.retire_live_backend();
565 self.renderer_epoch = self.renderer_epoch.wrapping_add(1);
566 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,
578 display_visible_region: self.display_visible_region,
579 };
580 let handle = PresentHandle::spawn(init, waker).map_err(WgpuRendererError::Wgpu)?;
581 self.backend = PresentBackend::Threaded(handle);
582 Ok(())
583 }
584
585 #[cfg(not(target_arch = "wasm32"))]
590 #[doc(hidden)]
591 pub fn init_gpu_inline_for_tests(
592 &mut self,
593 device: Arc<wgpu::Device>,
594 queue: Arc<wgpu::Queue>,
595 surface_format: wgpu::TextureFormat,
596 adapter_backend: wgpu::Backend,
597 adapter_downlevel: wgpu::DownlevelFlags,
598 ) -> InlinePresentRuntime {
599 self.retire_live_backend();
600 self.renderer_epoch = self.renderer_epoch.wrapping_add(1);
601 let store_feed_generation = pipeline::retained_feed_generation();
602 let init = PresentRuntimeInit {
603 device,
604 queue,
605 surface_format,
606 adapter_backend,
607 adapter_downlevel,
608 text_fonts: self.frontend.text_fonts.clone(),
609 renderer_epoch: self.renderer_epoch,
610 store_feed_generation,
611 clock: None,
612 display_visible_region: self.display_visible_region,
613 };
614 let (handle, state, msg_rx) = PresentHandle::new_inline(init, Arc::new(|| {}));
615 self.backend = PresentBackend::Threaded(handle);
616 InlinePresentRuntime {
617 state,
618 msg_rx,
619 shutdown_seen: false,
620 }
621 }
622
623 pub fn note_surface_reconfigured(&mut self) {
628 self.surface_epoch = self.surface_epoch.wrapping_add(1);
629 }
630
631 pub fn set_root_scale(&mut self, scale: f32) {
633 self.frontend.root_scale = scale;
634 }
635
636 pub fn root_scale(&self) -> f32 {
637 self.frontend.root_scale
638 }
639
640 pub fn render(
648 &mut self,
649 view: &wgpu::TextureView,
650 width: u32,
651 height: u32,
652 ) -> Result<(), WgpuRendererError> {
653 self.render_frame(view, None, width, height)
654 }
655
656 pub fn render_surface_texture(
657 &mut self,
658 texture: &wgpu::Texture,
659 view: &wgpu::TextureView,
660 width: u32,
661 height: u32,
662 ) -> Result<(), WgpuRendererError> {
663 let root_target = offscreen::OffscreenTarget::from_readable_texture(texture, view);
664 self.render_frame(view, root_target.as_ref(), width, height)
665 }
666
667 fn render_frame(
668 &mut self,
669 view: &wgpu::TextureView,
670 root_target: Option<&offscreen::OffscreenTarget>,
671 width: u32,
672 height: u32,
673 ) -> Result<(), WgpuRendererError> {
674 let PresentBackend::Sync(gpu_renderer) = &mut self.backend else {
675 return Err(WgpuRendererError::Wgpu(
676 "GPU renderer not initialized for synchronous rendering. Call init_gpu() first."
677 .to_string(),
678 ));
679 };
680 self.frontend.root_target_reads = root_target.is_some();
681 let packet = self
682 .frontend
683 .build_frame_packet(
684 width,
685 height,
686 gpu_renderer.replay_supported(),
687 self.renderer_epoch,
688 self.surface_epoch,
689 )
690 .ok_or_else(|| WgpuRendererError::Wgpu("scene graph is missing".to_string()))?;
691 let frontend = &mut self.frontend;
692 let mut returns = RenderReturns::default();
693 let result = gpu_renderer.render(
697 view,
698 root_target,
699 width,
700 height,
701 packet,
702 self.surface_epoch,
703 &mut returns,
704 );
705 if let Some(confirmations) = frontend.apply_returns(returns) {
706 gpu_renderer.restore_replay_ack_confirmations(confirmations);
707 }
708 result.map_err(WgpuRendererError::Wgpu)
709 }
710
711 pub fn capture_frame(
715 &mut self,
716 width: u32,
717 height: u32,
718 ) -> Result<CapturedFrame, WgpuRendererError> {
719 self.capture_frame_with_scale(width, height, self.frontend.root_scale)
720 }
721
722 pub fn capture_frame_with_scale(
724 &mut self,
725 width: u32,
726 height: u32,
727 root_scale: f32,
728 ) -> Result<CapturedFrame, WgpuRendererError> {
729 let PresentBackend::Sync(gpu_renderer) = &mut self.backend else {
730 return Err(WgpuRendererError::Wgpu(
731 "GPU renderer not initialized for synchronous rendering. Call init_gpu() first."
732 .to_string(),
733 ));
734 };
735 self.frontend.root_target_reads = offscreen::capture_root_target_reads();
736 let packet = self
737 .frontend
738 .build_frame_packet_with_scale(
739 width,
740 height,
741 root_scale,
742 gpu_renderer.replay_supported(),
743 self.renderer_epoch,
744 self.surface_epoch,
745 )
746 .ok_or_else(|| WgpuRendererError::Wgpu("scene graph is missing".to_string()))?;
747 let frontend = &mut self.frontend;
748 let mut returns = RenderReturns::default();
749 let result = gpu_renderer.render_to_rgba_pixels(
752 width,
753 height,
754 packet,
755 self.surface_epoch,
756 &mut returns,
757 );
758 if let Some(confirmations) = frontend.apply_returns(returns) {
759 gpu_renderer.restore_replay_ack_confirmations(confirmations);
760 }
761 let pixels = result.map_err(WgpuRendererError::Wgpu)?;
762 Ok(CapturedFrame {
763 width,
764 height,
765 pixels,
766 })
767 }
768
769 #[cfg(not(target_arch = "wasm32"))]
774 pub fn has_frame_credit(&self) -> bool {
775 match &self.backend {
776 PresentBackend::Threaded(handle) => handle.has_credit(),
777 PresentBackend::Sync(_) | PresentBackend::None => true,
778 }
779 }
780
781 #[cfg(not(target_arch = "wasm32"))]
787 pub fn publish_frame(&mut self, width: u32, height: u32) -> PublishOutcome {
788 self.drain_replay_acks();
794 let PresentBackend::Threaded(handle) = &mut self.backend else {
795 log::error!("publish_frame called without a threaded present runtime");
796 return PublishOutcome::NoCredit;
797 };
798 if !handle.has_credit() {
799 return PublishOutcome::NoCredit;
800 }
801 let replay_supported = handle
802 .status()
803 .replay_supported
804 .load(std::sync::atomic::Ordering::Relaxed);
805 let Some(mut packet) = self.frontend.build_frame_packet(
806 width,
807 height,
808 replay_supported,
809 self.renderer_epoch,
810 self.surface_epoch,
811 ) else {
812 return PublishOutcome::NoGraph;
813 };
814 packet.recycled_confirmations = self.pending_recycled_confirmations.take();
815 match handle.publish(packet) {
816 Ok(()) => PublishOutcome::Published,
817 Err(mut packet) => {
818 if let Some(confirmations) = packet.recycled_confirmations.take() {
821 self.pending_recycled_confirmations = Some(confirmations);
822 }
823 let mut returns = RenderReturns::default();
824 let _ = GpuRenderer::cancel_packet(
825 *packet,
826 CancelReason::SurfaceUnavailable,
827 &mut returns,
828 );
829 if let Some(confirmations) = self.frontend.apply_returns(returns) {
830 self.stash_recycled_confirmations(confirmations);
831 }
832 log::error!("present runtime unavailable; frame recovered, not published");
833 PublishOutcome::NoCredit
834 }
835 }
836 }
837
838 #[cfg(not(target_arch = "wasm32"))]
843 pub fn drain_present_returns(&mut self) -> usize {
844 self.drain_present_returns_with(&mut |_, _, _| {})
845 }
846
847 #[cfg(not(target_arch = "wasm32"))]
851 pub fn drain_present_returns_with(
852 &mut self,
853 on_return: &mut dyn FnMut(u64, PresentOutcome, PresentTimings),
854 ) -> usize {
855 self.drain_replay_acks();
859 let mut drained = 0;
860 loop {
861 let returns = {
864 let PresentBackend::Threaded(handle) = &mut self.backend else {
865 break;
866 };
867 match handle.try_drain() {
868 Some(returns) => returns,
869 None => break,
870 }
871 };
872 drained += 1;
873 let frame_id = returns.frame_id;
874 let outcome = returns.outcome;
875 let timings = returns.timings;
876 if let Some(confirmations) = self.frontend.apply_returns(returns) {
877 self.stash_recycled_confirmations(confirmations);
878 }
879 on_return(frame_id, outcome, timings);
880 }
881 drained
882 }
883
884 #[cfg(not(target_arch = "wasm32"))]
890 pub fn present_replace_surface(
891 &mut self,
892 surface: wgpu::Surface<'static>,
893 config: wgpu::SurfaceConfiguration,
894 ) -> bool {
895 let surface_epoch = self.surface_epoch;
896 let Some(handle) = self.present_handle_mut() else {
897 log::error!("present_replace_surface called without a threaded present runtime");
898 return false;
899 };
900 handle.send_control_and_wait(
901 move |ack| PresentControl::ReplaceSurface {
902 surface,
903 config,
904 surface_epoch,
905 ack,
906 },
907 "replace surface",
908 )
909 }
910
911 #[cfg(not(target_arch = "wasm32"))]
915 pub fn present_reconfigure(&mut self, config: wgpu::SurfaceConfiguration) -> bool {
916 let surface_epoch = self.surface_epoch;
917 let Some(handle) = self.present_handle_mut() else {
918 log::error!("present_reconfigure called without a threaded present runtime");
919 return false;
920 };
921 handle.send_control_and_wait(
922 move |ack| PresentControl::Reconfigure {
923 config,
924 surface_epoch,
925 ack,
926 },
927 "reconfigure surface",
928 )
929 }
930
931 #[cfg(not(target_arch = "wasm32"))]
935 pub fn present_drop_surface(&mut self) -> bool {
936 let Some(handle) = self.present_handle_mut() else {
937 log::error!("present_drop_surface called without a threaded present runtime");
938 return false;
939 };
940 handle.send_control_and_wait(|ack| PresentControl::DropSurface { ack }, "drop surface")
941 }
942
943 #[cfg(not(target_arch = "wasm32"))]
946 pub fn shutdown_present_runtime(&mut self) {
947 if matches!(self.backend, PresentBackend::Threaded(_)) {
948 self.retire_live_backend();
949 }
950 }
951
952 #[cfg(not(target_arch = "wasm32"))]
955 #[doc(hidden)]
956 pub fn present_attach_offscreen_for_tests(&mut self, width: u32, height: u32) -> bool {
957 let Some(handle) = self.present_handle_mut() else {
958 return false;
959 };
960 handle.send_control_and_wait(
961 move |ack| PresentControl::AttachOffscreenTargetForTests { width, height, ack },
962 "attach offscreen target",
963 )
964 }
965
966 #[cfg(not(target_arch = "wasm32"))]
969 #[doc(hidden)]
970 pub fn send_attach_offscreen_unacked_for_tests(
971 &mut self,
972 width: u32,
973 height: u32,
974 ) -> Option<std::sync::mpsc::Receiver<()>> {
975 let handle = self.present_handle_mut()?;
976 handle.send_control_unacked(move |ack| PresentControl::AttachOffscreenTargetForTests {
977 width,
978 height,
979 ack,
980 })
981 }
982
983 #[cfg(not(target_arch = "wasm32"))]
987 #[doc(hidden)]
988 pub fn send_reconfigure_unacked_for_tests(
989 &mut self,
990 config: wgpu::SurfaceConfiguration,
991 ) -> Option<std::sync::mpsc::Receiver<()>> {
992 let surface_epoch = self.surface_epoch;
993 let handle = self.present_handle_mut()?;
994 handle.send_control_unacked(move |ack| PresentControl::Reconfigure {
995 config,
996 surface_epoch,
997 ack,
998 })
999 }
1000
1001 #[cfg(not(target_arch = "wasm32"))]
1003 #[doc(hidden)]
1004 pub fn send_drop_surface_unacked_for_tests(&mut self) -> Option<std::sync::mpsc::Receiver<()>> {
1005 let handle = self.present_handle_mut()?;
1006 handle.send_control_unacked(|ack| PresentControl::DropSurface { ack })
1007 }
1008
1009 pub fn last_published_frame_id(&self) -> u64 {
1014 self.frontend.frame_sequence
1015 }
1016
1017 #[cfg(not(target_arch = "wasm32"))]
1022 #[doc(hidden)]
1023 pub fn seed_recycled_confirmations_for_tests(&mut self, capacity: usize) {
1024 self.pending_recycled_confirmations = Some(Vec::with_capacity(capacity));
1025 }
1026
1027 #[cfg(not(target_arch = "wasm32"))]
1031 #[doc(hidden)]
1032 pub fn pending_recycled_confirmations_capacity_for_tests(&self) -> Option<usize> {
1033 self.pending_recycled_confirmations
1034 .as_ref()
1035 .map(Vec::capacity)
1036 }
1037
1038 #[cfg(not(target_arch = "wasm32"))]
1041 #[doc(hidden)]
1042 pub fn present_status_snapshot_for_tests(&self) -> Option<(bool, bool, u64)> {
1043 match &self.backend {
1044 PresentBackend::Threaded(handle) => {
1045 let status = handle.status();
1046 Some((
1047 status
1048 .needs_frame_warmup
1049 .load(std::sync::atomic::Ordering::Relaxed),
1050 status
1051 .replay_supported
1052 .load(std::sync::atomic::Ordering::Relaxed),
1053 status
1054 .presented_frames
1055 .load(std::sync::atomic::Ordering::Relaxed),
1056 ))
1057 }
1058 _ => None,
1059 }
1060 }
1061
1062 pub fn last_frame_stats(&self) -> Option<RenderStatsSnapshot> {
1063 self.sync_gpu_renderer()
1064 .and_then(GpuRenderer::last_frame_stats)
1065 }
1066
1067 pub fn debug_cpu_allocation_stats(&self) -> DebugCpuAllocationStats {
1068 let mut stats = self
1069 .sync_gpu_renderer()
1070 .map(GpuRenderer::debug_cpu_allocation_stats)
1071 .unwrap_or_default();
1072 stats.scene_graph_node_count = self
1073 .frontend
1074 .scene
1075 .graph
1076 .as_ref()
1077 .map(RenderGraph::node_count)
1078 .unwrap_or(0);
1079 stats.scene_graph_heap_bytes = self
1080 .frontend
1081 .scene
1082 .graph
1083 .as_ref()
1084 .map(RenderGraph::heap_bytes)
1085 .unwrap_or(0);
1086 stats.scene_hits_len = self.frontend.scene.hits.len();
1087 stats.scene_hits_cap = self.frontend.scene.hits.capacity();
1088 stats.scene_node_index_len = self.frontend.scene.node_index.len();
1089 stats.scene_node_index_cap = self.frontend.scene.node_index.capacity();
1090 stats.layer_surface_rect_cache_len += self.frontend.layer_surface_rect_cache.len();
1094 stats.layer_surface_rect_cache_cap += self.frontend.layer_surface_rect_cache.capacity();
1095 stats.layer_surface_requirements_cache_len +=
1096 self.frontend.layer_surface_requirements_cache.len();
1097 stats.layer_surface_requirements_cache_cap +=
1098 self.frontend.layer_surface_requirements_cache.capacity();
1099 stats
1100 }
1101
1102 pub fn try_device(&self) -> Option<&wgpu::Device> {
1106 self.sync_gpu_renderer().map(|r| &*r.device)
1107 }
1108
1109 #[doc(hidden)]
1113 pub fn device_error_count_for_tests(&self) -> u64 {
1114 self.sync_gpu_renderer()
1115 .map(GpuRenderer::device_error_count)
1116 .unwrap_or(0)
1117 }
1118
1119 #[cfg(not(target_arch = "wasm32"))]
1124 #[doc(hidden)]
1125 pub fn instanced_quads_active(&self) -> bool {
1126 self.sync_gpu_renderer()
1127 .is_some_and(GpuRenderer::instanced_quads_active)
1128 }
1129
1130 #[cfg(not(target_arch = "wasm32"))]
1133 #[doc(hidden)]
1134 pub fn replay_slot_mesh_stats(&self) -> (usize, usize) {
1135 self.sync_gpu_renderer()
1136 .map(GpuRenderer::replay_slot_mesh_stats)
1137 .unwrap_or((0, 0))
1138 }
1139
1140 #[cfg(not(target_arch = "wasm32"))]
1144 #[doc(hidden)]
1145 pub fn replay_slot_mesh_engagement(&self) -> (usize, usize, usize) {
1146 self.sync_gpu_renderer()
1147 .map(GpuRenderer::replay_slot_mesh_engagement)
1148 .unwrap_or((0, 0, 0))
1149 }
1150
1151 #[cfg(not(target_arch = "wasm32"))]
1154 #[doc(hidden)]
1155 pub fn retained_bundle_stats(&self) -> (u64, u64) {
1156 self.sync_gpu_renderer()
1157 .map(GpuRenderer::retained_bundle_stats)
1158 .unwrap_or((0, 0))
1159 }
1160
1161 #[cfg(not(target_arch = "wasm32"))]
1165 #[doc(hidden)]
1166 pub fn rim_meshes_emitted(&self) -> u64 {
1167 self.sync_gpu_renderer()
1168 .map(GpuRenderer::rim_meshes_emitted)
1169 .unwrap_or(0)
1170 }
1171
1172 #[cfg(not(target_arch = "wasm32"))]
1177 #[doc(hidden)]
1178 pub fn static_span_stats(&self) -> (u64, u64) {
1179 self.sync_gpu_renderer()
1180 .map(GpuRenderer::static_span_stats)
1181 .unwrap_or((0, 0))
1182 }
1183
1184 #[cfg(not(target_arch = "wasm32"))]
1188 #[doc(hidden)]
1189 pub fn segment_surface_stats(&self) -> (u64, u64, u64, u64, u64) {
1190 self.sync_gpu_renderer()
1191 .map(GpuRenderer::segment_surface_stats)
1192 .unwrap_or((0, 0, 0, 0, 0))
1193 }
1194
1195 #[cfg(not(target_arch = "wasm32"))]
1200 #[doc(hidden)]
1201 pub fn replay_generation_drops_for_tests(&self) -> u64 {
1202 self.sync_gpu_renderer()
1203 .map(GpuRenderer::replay_generation_drops)
1204 .unwrap_or(0)
1205 }
1206
1207 #[cfg(not(target_arch = "wasm32"))]
1215 #[doc(hidden)]
1216 pub fn replay_ops_roundtrip_for_tests(&mut self, generation_skew: u64) -> usize {
1217 self.sync_gpu_renderer_mut()
1218 .expect("GPU renderer not initialized")
1219 .replay_ops_roundtrip_for_tests(generation_skew)
1220 }
1221
1222 #[doc(hidden)]
1228 pub fn build_frame_packet_for_tests(
1229 &mut self,
1230 width: u32,
1231 height: u32,
1232 ) -> Option<HeldFramePacket> {
1233 let replay_supported = match &self.backend {
1234 PresentBackend::Sync(gpu_renderer) => gpu_renderer.replay_supported(),
1235 #[cfg(not(target_arch = "wasm32"))]
1236 PresentBackend::Threaded(handle) => handle
1237 .status()
1238 .replay_supported
1239 .load(std::sync::atomic::Ordering::Relaxed),
1240 PresentBackend::None => false,
1241 };
1242 self.frontend
1243 .build_frame_packet(
1244 width,
1245 height,
1246 replay_supported,
1247 self.renderer_epoch,
1248 self.surface_epoch,
1249 )
1250 .map(HeldFramePacket)
1251 }
1252
1253 #[doc(hidden)]
1257 pub fn render_held_packet_for_tests(
1258 &mut self,
1259 view: &wgpu::TextureView,
1260 width: u32,
1261 height: u32,
1262 packet: HeldFramePacket,
1263 ) -> Result<PresentOutcome, WgpuRendererError> {
1264 let PresentBackend::Sync(gpu_renderer) = &mut self.backend else {
1265 return Err(WgpuRendererError::Wgpu(
1266 "GPU renderer not initialized for synchronous rendering. Call init_gpu() first."
1267 .to_string(),
1268 ));
1269 };
1270 let frontend = &mut self.frontend;
1271 let mut returns = RenderReturns::default();
1272 let result = gpu_renderer.render(
1273 view,
1274 None,
1275 width,
1276 height,
1277 packet.0,
1278 self.surface_epoch,
1279 &mut returns,
1280 );
1281 let outcome = returns.outcome;
1282 if let Some(confirmations) = frontend.apply_returns(returns) {
1283 gpu_renderer.restore_replay_ack_confirmations(confirmations);
1284 }
1285 result.map_err(WgpuRendererError::Wgpu)?;
1286 Ok(outcome)
1287 }
1288
1289 #[doc(hidden)]
1292 pub fn has_retained_direct_scene_for_tests(&self) -> bool {
1293 !self.frontend.retained_direct_scenes.is_empty()
1294 }
1295}
1296
1297#[doc(hidden)]
1300pub struct HeldFramePacket(frame_packet::FramePacket);
1301
1302#[cfg(not(target_arch = "wasm32"))]
1306#[doc(hidden)]
1307pub struct InlinePresentRuntime {
1308 state: PresentState,
1309 msg_rx: std::sync::mpsc::Receiver<PresentMsg>,
1310 shutdown_seen: bool,
1311}
1312
1313#[cfg(not(target_arch = "wasm32"))]
1314impl InlinePresentRuntime {
1315 pub fn pump(&mut self) -> bool {
1320 if self.shutdown_seen {
1321 return false;
1322 }
1323 while let Ok(msg) = self.msg_rx.try_recv() {
1324 if !self.state.run_once(msg) {
1325 self.shutdown_seen = true;
1326 return false;
1327 }
1328 }
1329 self.state.consume_waiting();
1330 true
1331 }
1332
1333 pub fn has_waiting_packet(&self) -> bool {
1336 self.state.has_waiting_packet()
1337 }
1338
1339 pub fn step_one_message(&mut self) -> bool {
1343 if self.shutdown_seen {
1344 return false;
1345 }
1346 match self.msg_rx.try_recv() {
1347 Ok(msg) => {
1348 if !self.state.run_once(msg) {
1349 self.shutdown_seen = true;
1350 }
1351 true
1352 }
1353 Err(_) => false,
1354 }
1355 }
1356
1357 pub fn consume_waiting(&mut self) {
1360 self.state.consume_waiting();
1361 }
1362
1363 pub fn store_ack_confirmations_capacity(&self) -> usize {
1366 self.state.store_ack_confirmations_capacity()
1367 }
1368}
1369
1370impl Default for WgpuRenderer {
1371 fn default() -> Self {
1372 Self::new(&[])
1373 }
1374}
1375
1376impl Renderer for WgpuRenderer {
1377 type Scene = Scene;
1378 type Error = WgpuRendererError;
1379
1380 fn attach_app_context_services(&mut self, app_context: &cranpose_ui::AppContext) {
1381 app_context.set_text_measurer(SoftwareTextMeasurer::from_font_set(
1382 self.frontend.text_fonts.clone(),
1383 8192,
1384 ));
1385 self.frontend.app_context = Some(app_context.downgrade());
1386 }
1387
1388 fn scene(&self) -> &Self::Scene {
1389 &self.frontend.scene
1390 }
1391
1392 fn scene_mut(&mut self) -> &mut Self::Scene {
1393 &mut self.frontend.scene
1394 }
1395
1396 fn rebuild_scene(
1397 &mut self,
1398 layout_tree: &LayoutTree,
1399 _viewport: Size,
1400 ) -> Result<(), Self::Error> {
1401 self.frontend.scene.clear();
1402 self.frontend.layer_surface_requirements_cache.clear();
1403 self.frontend.dev_overlay_graph = None;
1404 self.frontend.dev_overlay_cache = None;
1405 pipeline::render_layout_tree(layout_tree.root(), &mut self.frontend.scene);
1407 Ok(())
1408 }
1409
1410 fn rebuild_scene_from_applier(
1411 &mut self,
1412 applier: &mut MemoryApplier,
1413 root: NodeId,
1414 _viewport: Size,
1415 ) -> Result<(), Self::Error> {
1416 self.frontend.scene.clear();
1417 self.frontend.layer_surface_requirements_cache.clear();
1418 self.frontend.dev_overlay_graph = None;
1419 self.frontend.dev_overlay_cache = None;
1420 pipeline::render_from_applier(applier, root, &mut self.frontend.scene, 1.0);
1423 Ok(())
1424 }
1425
1426 fn update_scene_from_applier(
1427 &mut self,
1428 applier: &mut MemoryApplier,
1429 root: NodeId,
1430 viewport: Size,
1431 dirty_nodes: &[NodeId],
1432 ) -> Result<(), Self::Error> {
1433 if dirty_nodes.is_empty() {
1434 return self.rebuild_scene_from_applier(applier, root, viewport);
1435 }
1436 self.update_scene_and_plan_memo(applier, root, dirty_nodes, true);
1437 Ok(())
1438 }
1439
1440 fn update_visual_scene_from_applier(
1441 &mut self,
1442 applier: &mut MemoryApplier,
1443 root: NodeId,
1444 viewport: Size,
1445 dirty_nodes: &[NodeId],
1446 ) -> Result<(), Self::Error> {
1447 if dirty_nodes.is_empty() {
1448 return self.rebuild_scene_from_applier(applier, root, viewport);
1449 }
1450 self.update_scene_and_plan_memo(applier, root, dirty_nodes, false);
1451 Ok(())
1452 }
1453
1454 fn draw_dev_overlay(&mut self, text: &str, viewport: Size) {
1455 const DEV_OVERLAY_NODE_ID: NodeId = NodeId::MAX;
1456 let key = cranpose_render_common::dev_overlay::DevOverlayKey::new(text, viewport);
1457 if self.frontend.dev_overlay_graph.is_some()
1458 && self
1459 .frontend
1460 .dev_overlay_cache
1461 .as_ref()
1462 .is_some_and(|cache| {
1463 cache.text == key.text
1464 && cache.viewport_width_bits == key.viewport_width_bits
1465 && cache.viewport_height_bits == key.viewport_height_bits
1466 })
1467 {
1468 return;
1469 }
1470 self.frontend.dev_overlay_graph = Some(
1471 cranpose_render_common::dev_overlay::build_dev_overlay_graph(
1472 text,
1473 viewport,
1474 DEV_OVERLAY_NODE_ID,
1475 ),
1476 );
1477 self.frontend.dev_overlay_cache = Some(DevOverlayCache {
1478 text: key.text,
1479 viewport_width_bits: key.viewport_width_bits,
1480 viewport_height_bits: key.viewport_height_bits,
1481 });
1482 }
1483
1484 fn needs_frame_warmup(&self) -> bool {
1485 match &self.backend {
1486 PresentBackend::Sync(gpu_renderer) => gpu_renderer.needs_frame_warmup(),
1487 #[cfg(not(target_arch = "wasm32"))]
1491 PresentBackend::Threaded(handle) => handle
1492 .status()
1493 .needs_frame_warmup
1494 .load(std::sync::atomic::Ordering::Relaxed),
1495 PresentBackend::None => false,
1496 }
1497 }
1498}
1499
1500#[cfg(test)]
1501mod tests {
1502 use super::*;
1503 use crate::pipeline::TextLayoutResolver;
1504 use cranpose_render_common::graph::RenderNode;
1505 use cranpose_ui_graphics::GraphicsLayer;
1506 use std::cell::Cell;
1507
1508 static TEST_FONT: &[u8] =
1509 cranpose_render_common::software_text_raster::DEFAULT_SOFTWARE_TEXT_FONT_BYTES;
1510
1511 #[test]
1512 fn dev_overlay_is_recorded_outside_app_graph() {
1513 let mut renderer = WgpuRenderer::new(&[]);
1514 renderer.draw_dev_overlay(
1515 "240 FPS | avg 4.0ms | p95 4.5ms",
1516 Size {
1517 width: 800.0,
1518 height: 600.0,
1519 },
1520 );
1521
1522 assert!(
1523 renderer
1524 .frontend
1525 .scene
1526 .graph
1527 .as_ref()
1528 .is_none_or(|graph| graph.root.children.iter().all(|child| {
1529 !matches!(
1530 child,
1531 RenderNode::Layer(layer) if layer.node_id == Some(NodeId::MAX)
1532 )
1533 })),
1534 "dev overlay must not be mixed into the app scene graph"
1535 );
1536
1537 let graph = renderer
1538 .frontend
1539 .dev_overlay_graph
1540 .as_ref()
1541 .expect("overlay graph");
1542 let Some(RenderNode::Layer(overlay)) = graph.root.children.last() else {
1543 panic!("dev overlay should be the final top-level layer");
1544 };
1545
1546 assert_eq!(overlay.node_id, Some(NodeId::MAX));
1547 assert_eq!(
1548 overlay.graphics_layer.compositing_strategy,
1549 GraphicsLayer::default().compositing_strategy,
1550 "dev overlay should not allocate an offscreen surface"
1551 );
1552 }
1553
1554 struct CountingTextMeasurer {
1555 inner: SoftwareTextMeasurer,
1556 layout_calls: Rc<Cell<usize>>,
1557 }
1558
1559 impl CountingTextMeasurer {
1560 fn new(layout_calls: Rc<Cell<usize>>) -> Self {
1561 Self {
1562 inner: SoftwareTextMeasurer::from_fonts_or_default(&[TEST_FONT], 16),
1563 layout_calls,
1564 }
1565 }
1566 }
1567
1568 impl TextMeasurer for CountingTextMeasurer {
1569 fn measure(
1570 &self,
1571 text: &cranpose_ui::text::AnnotatedString,
1572 style: &cranpose_ui::text::TextStyle,
1573 ) -> cranpose_ui::TextMetrics {
1574 self.inner.measure(text, style)
1575 }
1576
1577 fn get_offset_for_position(
1578 &self,
1579 text: &cranpose_ui::text::AnnotatedString,
1580 style: &cranpose_ui::text::TextStyle,
1581 x: f32,
1582 y: f32,
1583 ) -> usize {
1584 self.inner.get_offset_for_position(text, style, x, y)
1585 }
1586
1587 fn get_cursor_x_for_offset(
1588 &self,
1589 text: &cranpose_ui::text::AnnotatedString,
1590 style: &cranpose_ui::text::TextStyle,
1591 offset: usize,
1592 ) -> f32 {
1593 self.inner.get_cursor_x_for_offset(text, style, offset)
1594 }
1595
1596 fn layout(
1597 &self,
1598 text: &cranpose_ui::text::AnnotatedString,
1599 style: &cranpose_ui::text::TextStyle,
1600 ) -> cranpose_ui::text_layout_result::TextLayoutResult {
1601 self.layout_calls.set(self.layout_calls.get() + 1);
1602 self.inner.layout(text, style)
1603 }
1604 }
1605
1606 #[test]
1607 fn headless_text_measurer_uses_software_text_font() {
1608 let measurer = headless_text_measurer_with_fonts(&[TEST_FONT]);
1609 let text = cranpose_ui::text::AnnotatedString::from("software text measurement");
1610 let style = cranpose_ui::text::TextStyle::default();
1611
1612 let metrics = measurer.measure(&text, &style);
1613 let layout = measurer.layout(&text, &style);
1614
1615 assert!(metrics.width > 0.0);
1616 assert!(metrics.height > 0.0);
1617 assert_eq!(layout.lines.len(), metrics.line_count);
1618 }
1619
1620 #[test]
1621 fn renderer_measurement_uses_software_text_service_without_render_cache_side_effect() {
1622 let mut renderer = WgpuRenderer::new(&[TEST_FONT]);
1623 let app_context = cranpose_ui::AppContext::new();
1624 renderer.attach_app_context_services(&app_context);
1625
1626 let metrics = app_context.enter(|| {
1627 let text = cranpose_ui::text::AnnotatedString::from("phase local text cache");
1628 let style = cranpose_ui::text::TextStyle {
1629 span_style: cranpose_ui::text::SpanStyle {
1630 font_size: cranpose_ui::text::TextUnit::Sp(14.0),
1631 ..Default::default()
1632 },
1633 paragraph_style: cranpose_ui::text::ParagraphStyle {
1634 platform_style: Some(cranpose_ui::text::PlatformParagraphStyle {
1635 include_font_padding: None,
1636 shaping: Some(cranpose_ui::text::TextShaping::Basic),
1637 }),
1638 ..Default::default()
1639 },
1640 };
1641 cranpose_ui::text::measure_text(&text, &style)
1642 });
1643
1644 assert!(
1645 metrics.width > 0.0,
1646 "software text service should measure text"
1647 );
1648 assert_eq!(
1649 renderer.frontend.text_state.text_cache_len(),
1650 0,
1651 "WGPU must not keep a renderer-side shaping cache for measurement"
1652 );
1653 }
1654
1655 #[test]
1656 fn renderer_attached_text_service_measures_long_multiline_text_with_software_line_height() {
1657 let mut renderer = WgpuRenderer::new(&[TEST_FONT]);
1658 let app_context = cranpose_ui::AppContext::new();
1659 renderer.attach_app_context_services(&app_context);
1660
1661 let prepared = app_context.enter(|| {
1662 let text = cranpose_ui::text::AnnotatedString::from(
1663 (0..48)
1664 .map(|line| format!("// markdown code line {line:02}"))
1665 .collect::<Vec<_>>()
1666 .join("\n"),
1667 );
1668 let style = cranpose_ui::text::TextStyle::default();
1669 cranpose_ui::text::prepare_text_layout(
1670 &text,
1671 &style,
1672 cranpose_ui::text::TextLayoutOptions::default(),
1673 Some(952.0),
1674 )
1675 });
1676
1677 assert_eq!(prepared.metrics.line_count, 48);
1678 assert!(
1679 prepared.metrics.line_height > 18.0,
1680 "renderer-attached text service must not use fallback monospaced line height: {:?}",
1681 prepared.metrics
1682 );
1683 assert!(
1684 prepared.metrics.height > 900.0,
1685 "48 software-measured lines should not collapse to a viewport-sized block: {:?}",
1686 prepared.metrics
1687 );
1688 }
1689
1690 #[test]
1691 fn render_text_layout_routes_through_attached_app_context_service() {
1692 let mut renderer = WgpuRenderer::new(&[TEST_FONT]);
1693 let app_context = cranpose_ui::AppContext::new();
1694 renderer.attach_app_context_services(&app_context);
1695 let layout_calls = Rc::new(Cell::new(0));
1696 app_context.set_text_measurer(CountingTextMeasurer::new(Rc::clone(&layout_calls)));
1697
1698 app_context.enter(|| {
1699 let text = cranpose_ui::text::AnnotatedString::from("render text");
1700 let style = cranpose_ui::text::TextStyle::default();
1701 let layout = renderer.frontend.text_state.layout_text(&text, &style);
1702 assert!(layout.width > 0.0);
1703 });
1704
1705 assert_eq!(layout_calls.get(), 1);
1706 }
1707}