1pub(crate) use cranpose_render_common::debug_toggles;
7pub use debug_toggles::{
8 DebugToggle, debug_toggle, debug_toggle_os, set_debug_toggle, set_debug_toggle_os,
9};
10pub use offscreen::composition_bytes_per_pixel;
11pub use render::presentable_root_usages;
12mod ablation;
13mod capture_hash;
14mod collect;
15mod draw_pass;
16mod effect_renderer;
17mod fast_cores;
18mod frame;
19mod geometry;
20mod layer_cache;
21pub use fast_cores::pin_current_thread_to_fast_cores;
22mod frame_graph;
23mod frame_packet;
24mod frontend;
25mod glass_split;
26pub(crate) mod gpu_stats;
27mod initial_present;
28mod lazy_resource;
29mod offscreen;
30mod opaque_prefix;
31mod output_conversion;
32pub(crate) mod pass_timing;
33mod pipeline;
34mod pipeline_compiler;
35#[cfg(not(target_arch = "wasm32"))]
36pub mod pipeline_disk_cache;
37#[cfg(not(target_arch = "wasm32"))]
38mod present_runtime;
39mod record_columns;
40mod render;
41mod run_geometry;
42mod run_store;
43mod scene;
44mod shader_cache;
45mod shaders;
46mod shape_pipelines;
47#[cfg(test)]
48mod test_support;
49
50use std::{rc::Rc, sync::Arc};
51
52use cranpose_core::{MemoryApplier, NodeId};
53use cranpose_render_common::{
54 RenderScene, Renderer,
55 graph::RenderGraph,
56 software_text_raster::{
57 SoftwareTextFontSet, SoftwareTextMeasurer, software_text_font_set_from_fonts_or_default,
58 },
59};
60use cranpose_ui::{LayoutTree, TextMeasurer};
61use cranpose_ui_graphics::{Rect, ShaderWarmUp, Size};
62pub use frame_packet::PresentTimings;
63use frame_packet::RenderReturns;
64#[doc(hidden)]
65pub use frame_packet::{CancelReason, PresentOutcome};
66use frontend::{DevOverlayCache, RendererFrontend};
67pub use gpu_stats::FrameStatsSnapshot as RenderStatsSnapshot;
68pub use initial_present::{clear_to_background, clear_to_default_background};
69pub use pass_timing::{GpuPassTimingEntry, GpuPassTimingReport};
70#[cfg(not(target_arch = "wasm32"))]
71use present_runtime::{
72 PresentControl, PresentHandle, PresentMsg, PresentRuntimeInit, PresentState,
73};
74use render::GpuRenderer;
75pub use render::{
76 frame_clear_color, frames_presented, pipelines_created, pipelines_created_off_frame,
77};
78pub use scene::{ClickAction, HitRegion, Scene};
79
80pub fn optional_device_features(adapter: &wgpu::Adapter) -> wgpu::Features {
87 adapter.features() & (wgpu::Features::PIPELINE_CACHE | wgpu::Features::TIMESTAMP_QUERY)
88}
89
90#[doc(hidden)]
91pub fn offscreen_render_target_for_tests(
92 device: &wgpu::Device,
93 width: u32,
94 height: u32,
95 label: &str,
96) -> (wgpu::Texture, wgpu::TextureView) {
97 let texture = offscreen::create_2d_texture(
98 device,
99 wgpu::TextureFormat::Rgba8Unorm,
100 width,
101 height,
102 wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
103 Some(label),
104 );
105 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
106 (texture, view)
107}
108
109pub(crate) fn rect_to_quad(rect: Rect) -> [[f32; 2]; 4] {
110 [
111 [rect.x, rect.y],
112 [rect.x + rect.width, rect.y],
113 [rect.x, rect.y + rect.height],
114 [rect.x + rect.width, rect.y + rect.height],
115 ]
116}
117
118#[derive(Debug)]
119pub enum WgpuRendererError {
120 Layout(String),
121 Wgpu(String),
122}
123
124#[derive(Debug, Clone)]
126pub struct CapturedFrame {
127 pub width: u32,
128 pub height: u32,
129 pub pixels: Vec<u8>,
130}
131
132#[doc(hidden)]
133#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
134pub struct DebugCpuAllocationStats {
135 pub scene_graph_node_count: usize,
136 pub scene_graph_heap_bytes: usize,
137 pub scene_hits_len: usize,
138 pub scene_hits_cap: usize,
139 pub scene_node_index_len: usize,
140 pub scene_node_index_cap: usize,
141 pub text_renderer_pool_len: usize,
142 pub text_renderer_pool_cap: usize,
143 pub image_texture_cache_len: usize,
144 pub image_texture_cache_cap: usize,
145 pub run_arena_staging_bytes: usize,
146 pub run_store_bytes: usize,
147 pub run_store_runs: usize,
148 pub scratch_image_vertices_cap: usize,
149 pub scratch_image_indices_cap: usize,
150 pub scratch_image_cmds_cap: usize,
151 pub layer_cache_len: usize,
152 pub layer_cache_bytes: u64,
153}
154
155pub(crate) struct TextSystemState {
156 measurer: SoftwareTextMeasurer,
157}
158
159impl TextSystemState {
160 fn from_font_set(fonts: SoftwareTextFontSet) -> Self {
161 Self {
162 measurer: SoftwareTextMeasurer::from_font_set(fonts, 8192),
163 }
164 }
165
166 pub(crate) fn text_cache_len(&self) -> usize {
167 0
168 }
169}
170
171impl pipeline::TextLayoutResolver for TextSystemState {
172 fn layout_text(
173 &mut self,
174 text: &cranpose_ui::text::AnnotatedString,
175 style: &cranpose_ui::text::TextStyle,
176 ) -> cranpose_ui::text_layout_result::TextLayoutResult {
177 if cranpose_ui::has_current_app_context() {
178 cranpose_ui::text::layout_text(text, style)
179 } else {
180 self.measurer.layout(text, style)
181 }
182 }
183}
184
185#[derive(Clone)]
186pub struct WgpuTextSystem {
187 software_fonts: SoftwareTextFontSet,
188}
189
190impl WgpuTextSystem {
191 pub fn from_fonts(fonts: &[&[u8]]) -> Self {
192 Self {
193 software_fonts: software_text_font_set_from_fonts_or_default(fonts),
194 }
195 }
196
197 pub fn from_font_set(software_fonts: SoftwareTextFontSet) -> Self {
201 Self { software_fonts }
202 }
203
204 pub(crate) fn render_state(&self) -> TextSystemState {
205 TextSystemState::from_font_set(self.software_fonts.clone())
206 }
207
208 pub(crate) fn software_fonts(&self) -> SoftwareTextFontSet {
209 self.software_fonts.clone()
210 }
211}
212
213pub fn headless_text_measurer() -> Rc<dyn TextMeasurer> {
215 headless_text_measurer_with_fonts(&[])
216}
217
218pub fn headless_text_measurer_with_fonts(fonts: &[&[u8]]) -> Rc<dyn TextMeasurer> {
220 Rc::new(SoftwareTextMeasurer::from_fonts_or_default(fonts, 8192))
221}
222
223enum PresentBackend {
224 None,
225 Sync(Box<GpuRenderer>),
226 #[cfg(not(target_arch = "wasm32"))]
227 Threaded(PresentHandle),
228}
229
230#[derive(Clone, Copy, Debug, PartialEq, Eq)]
232pub enum PublishOutcome {
233 NoGraph,
235 NoCredit,
238 Published,
240}
241
242pub struct WgpuRenderer {
250 frontend: RendererFrontend,
251 backend: PresentBackend,
252 renderer_epoch: u64,
253 surface_epoch: u64,
254}
255
256impl WgpuRenderer {
257 fn update_scene(
258 &mut self,
259 applier: &mut MemoryApplier,
260 root: NodeId,
261 dirty_nodes: &[NodeId],
262 refresh_hits: bool,
263 ) {
264 let mut changed_nodes = std::mem::take(&mut self.frontend.changed_nodes);
265 pipeline::update_from_applier(
266 applier,
267 root,
268 &mut self.frontend.scene,
269 1.0,
270 dirty_nodes,
271 refresh_hits,
272 &mut changed_nodes,
273 );
274 changed_nodes.clear();
275 self.frontend.changed_nodes = changed_nodes;
276 }
277
278 pub fn new(fonts: &[&[u8]]) -> Self {
285 Self::with_text_system(WgpuTextSystem::from_fonts(fonts))
286 }
287
288 pub fn with_font_set(fonts: SoftwareTextFontSet) -> Self {
293 Self::with_text_system(WgpuTextSystem::from_font_set(fonts))
294 }
295
296 pub fn with_text_system(text_system: WgpuTextSystem) -> Self {
297 Self {
298 frontend: RendererFrontend::new(
299 text_system.render_state(),
300 text_system.software_fonts(),
301 ),
302 backend: PresentBackend::None,
303 renderer_epoch: 0,
304 surface_epoch: 0,
305 }
306 }
307
308 fn sync_gpu_renderer(&self) -> Option<&GpuRenderer> {
309 match &self.backend {
310 PresentBackend::Sync(gpu_renderer) => Some(gpu_renderer.as_ref()),
311 _ => None,
312 }
313 }
314
315 #[cfg(not(target_arch = "wasm32"))]
316 fn present_handle_mut(&mut self) -> Option<&mut PresentHandle> {
317 match &mut self.backend {
318 PresentBackend::Threaded(handle) => Some(handle),
319 _ => None,
320 }
321 }
322
323 fn retire_live_backend(&mut self) {
324 #[allow(unused_mut)]
325 let mut backend = std::mem::replace(&mut self.backend, PresentBackend::None);
326 #[cfg(not(target_arch = "wasm32"))]
327 if let PresentBackend::Threaded(handle) = &mut backend {
328 while let Some(returns) = handle.try_drain() {
329 self.frontend.apply_returns(returns);
330 }
331 handle.shutdown();
332 }
333 drop(backend);
334 }
335
336 pub fn init_gpu(
342 &mut self,
343 device: Arc<wgpu::Device>,
344 queue: Arc<wgpu::Queue>,
345 surface_format: wgpu::TextureFormat,
346 adapter_backend: wgpu::Backend,
347 adapter_downlevel: wgpu::DownlevelFlags,
348 ) {
349 self.retire_live_backend();
350 self.renderer_epoch = self.renderer_epoch.wrapping_add(1);
351 let mut gpu_renderer = GpuRenderer::new(
352 device,
353 queue,
354 surface_format,
355 adapter_backend,
356 adapter_downlevel,
357 self.frontend.text_fonts.clone(),
358 self.renderer_epoch,
359 );
360 gpu_renderer.warm_shaders(&self.frontend.shader_warm_ups);
361 self.backend = PresentBackend::Sync(Box::new(gpu_renderer));
362 }
363
364 pub fn warm_shaders(&mut self, warm_ups: impl IntoIterator<Item = ShaderWarmUp>) {
370 self.frontend.shader_warm_ups.extend(warm_ups);
371 }
372
373 #[cfg(not(target_arch = "wasm32"))]
388 #[allow(clippy::too_many_arguments)]
389 pub fn init_gpu_threaded(
390 &mut self,
391 device: Arc<wgpu::Device>,
392 queue: Arc<wgpu::Queue>,
393 surface_format: wgpu::TextureFormat,
394 adapter_backend: wgpu::Backend,
395 adapter_downlevel: wgpu::DownlevelFlags,
396 waker: Arc<dyn Fn() + Send + Sync>,
397 clock: Option<Arc<dyn Fn() -> i64 + Send + Sync>>,
398 ) -> Result<(), WgpuRendererError> {
399 self.retire_live_backend();
400 self.renderer_epoch = self.renderer_epoch.wrapping_add(1);
401 let init = PresentRuntimeInit {
402 device,
403 queue,
404 surface_format,
405 adapter_backend,
406 adapter_downlevel,
407 text_fonts: self.frontend.text_fonts.clone(),
408 renderer_epoch: self.renderer_epoch,
409 shader_warm_ups: self.frontend.shader_warm_ups.clone(),
410 clock,
411 };
412 let handle = PresentHandle::spawn(init, waker).map_err(WgpuRendererError::Wgpu)?;
413 self.backend = PresentBackend::Threaded(handle);
414 Ok(())
415 }
416
417 #[cfg(not(target_arch = "wasm32"))]
418 #[doc(hidden)]
419 pub fn init_gpu_inline_for_tests(
420 &mut self,
421 device: Arc<wgpu::Device>,
422 queue: Arc<wgpu::Queue>,
423 surface_format: wgpu::TextureFormat,
424 adapter_backend: wgpu::Backend,
425 adapter_downlevel: wgpu::DownlevelFlags,
426 ) -> InlinePresentRuntime {
427 self.retire_live_backend();
428 self.renderer_epoch = self.renderer_epoch.wrapping_add(1);
429 let init = PresentRuntimeInit {
430 device,
431 queue,
432 surface_format,
433 adapter_backend,
434 adapter_downlevel,
435 text_fonts: self.frontend.text_fonts.clone(),
436 renderer_epoch: self.renderer_epoch,
437 shader_warm_ups: self.frontend.shader_warm_ups.clone(),
438 clock: None,
439 };
440 let (handle, state, msg_rx) = PresentHandle::new_inline(init, Arc::new(|| {}));
441 self.backend = PresentBackend::Threaded(handle);
442 InlinePresentRuntime {
443 state,
444 msg_rx,
445 shutdown_seen: false,
446 }
447 }
448
449 pub fn note_surface_reconfigured(&mut self) {
454 self.surface_epoch = self.surface_epoch.wrapping_add(1);
455 }
456
457 pub fn set_root_scale(&mut self, scale: f32) {
459 self.frontend.root_scale = scale;
460 }
461
462 pub fn set_transparent_background(&mut self, transparent: bool) {
467 self.frontend.transparent_background = transparent;
468 }
469
470 pub fn root_scale(&self) -> f32 {
471 self.frontend.root_scale
472 }
473
474 pub fn render(
481 &mut self,
482 texture: &wgpu::Texture,
483 view: &wgpu::TextureView,
484 width: u32,
485 height: u32,
486 ) -> Result<(), WgpuRendererError> {
487 self.render_frame(texture, view, width, height)
488 }
489
490 pub fn render_surface_texture(
494 &mut self,
495 texture: &wgpu::Texture,
496 view: &wgpu::TextureView,
497 width: u32,
498 height: u32,
499 ) -> Result<(), WgpuRendererError> {
500 self.render_frame(texture, view, width, height)
501 }
502
503 pub fn present(&self, frame: wgpu::SurfaceTexture) {
510 match self.sync_gpu_renderer() {
511 Some(gpu_renderer) => gpu_renderer.queue.present(frame),
512 None => log::debug!("surface image released: the renderer has no GPU queue"),
513 }
514 }
515
516 fn render_frame(
517 &mut self,
518 texture: &wgpu::Texture,
519 view: &wgpu::TextureView,
520 width: u32,
521 height: u32,
522 ) -> Result<(), WgpuRendererError> {
523 let PresentBackend::Sync(gpu_renderer) = &mut self.backend else {
524 return Err(WgpuRendererError::Wgpu(
525 "GPU renderer not initialized for synchronous rendering. Call init_gpu() first."
526 .to_string(),
527 ));
528 };
529 let packet = self
530 .frontend
531 .build_frame_packet(width, height, self.renderer_epoch, self.surface_epoch)
532 .ok_or_else(|| WgpuRendererError::Wgpu("scene graph is missing".to_string()))?;
533 let mut returns = RenderReturns::default();
534 let result = gpu_renderer.render(
535 texture,
536 view,
537 width,
538 height,
539 packet,
540 self.surface_epoch,
541 &mut returns,
542 );
543 self.frontend.apply_returns(returns);
544 result.map_err(WgpuRendererError::Wgpu)
545 }
546
547 pub fn capture_frame(
551 &mut self,
552 width: u32,
553 height: u32,
554 ) -> Result<CapturedFrame, WgpuRendererError> {
555 self.capture_frame_with_scale(width, height, self.frontend.root_scale)
556 }
557
558 pub fn capture_frame_with_scale(
560 &mut self,
561 width: u32,
562 height: u32,
563 root_scale: f32,
564 ) -> Result<CapturedFrame, WgpuRendererError> {
565 let PresentBackend::Sync(gpu_renderer) = &mut self.backend else {
566 return Err(WgpuRendererError::Wgpu(
567 "GPU renderer not initialized for synchronous rendering. Call init_gpu() first."
568 .to_string(),
569 ));
570 };
571 let packet = self
572 .frontend
573 .build_frame_packet_with_scale(
574 width,
575 height,
576 root_scale,
577 self.renderer_epoch,
578 self.surface_epoch,
579 )
580 .ok_or_else(|| WgpuRendererError::Wgpu("scene graph is missing".to_string()))?;
581 let mut returns = RenderReturns::default();
582 let result = gpu_renderer.render_to_rgba_pixels(
583 width,
584 height,
585 packet,
586 self.surface_epoch,
587 &mut returns,
588 );
589 self.frontend.apply_returns(returns);
590 let pixels = result.map_err(WgpuRendererError::Wgpu)?;
591 Ok(CapturedFrame {
592 width,
593 height,
594 pixels,
595 })
596 }
597
598 #[cfg(not(target_arch = "wasm32"))]
603 pub fn has_frame_credit(&self) -> bool {
604 match &self.backend {
605 PresentBackend::Threaded(handle) => handle.has_credit(),
606 PresentBackend::Sync(_) | PresentBackend::None => true,
607 }
608 }
609
610 #[cfg(not(target_arch = "wasm32"))]
616 pub fn publish_frame(&mut self, width: u32, height: u32) -> PublishOutcome {
617 let PresentBackend::Threaded(handle) = &mut self.backend else {
618 log::error!("publish_frame called without a threaded present runtime");
619 return PublishOutcome::NoCredit;
620 };
621 if !handle.has_credit() {
622 return PublishOutcome::NoCredit;
623 }
624 let Some(packet) = self.frontend.build_frame_packet(
625 width,
626 height,
627 self.renderer_epoch,
628 self.surface_epoch,
629 ) else {
630 return PublishOutcome::NoGraph;
631 };
632 match handle.publish(packet) {
633 Ok(()) => PublishOutcome::Published,
634 Err(packet) => {
635 let mut returns = RenderReturns::default();
636 let _ = GpuRenderer::cancel_packet(
637 *packet,
638 CancelReason::SurfaceUnavailable,
639 &mut returns,
640 );
641 self.frontend.apply_returns(returns);
642 log::error!("present runtime unavailable; frame recovered, not published");
643 PublishOutcome::NoCredit
644 }
645 }
646 }
647
648 #[cfg(not(target_arch = "wasm32"))]
652 pub fn drain_present_returns(&mut self) -> usize {
653 self.drain_present_returns_with(&mut |_, _, _| {})
654 }
655
656 #[cfg(not(target_arch = "wasm32"))]
660 pub fn drain_present_returns_with(
661 &mut self,
662 on_return: &mut dyn FnMut(u64, PresentOutcome, PresentTimings),
663 ) -> usize {
664 let mut drained = 0;
665 loop {
666 let returns = {
667 let PresentBackend::Threaded(handle) = &mut self.backend else {
668 break;
669 };
670 match handle.try_drain() {
671 Some(returns) => returns,
672 None => break,
673 }
674 };
675 drained += 1;
676 let frame_id = returns.frame_id;
677 let outcome = returns.outcome;
678 let timings = returns.timings;
679 self.frontend.apply_returns(returns);
680 on_return(frame_id, outcome, timings);
681 }
682 drained
683 }
684
685 #[cfg(not(target_arch = "wasm32"))]
691 pub fn present_replace_surface(
692 &mut self,
693 surface: wgpu::Surface<'static>,
694 config: wgpu::SurfaceConfiguration,
695 ) -> bool {
696 let surface_epoch = self.surface_epoch;
697 let Some(handle) = self.present_handle_mut() else {
698 log::error!("present_replace_surface called without a threaded present runtime");
699 return false;
700 };
701 handle.send_control_and_wait(
702 move |ack| PresentControl::ReplaceSurface {
703 surface,
704 config,
705 surface_epoch,
706 ack,
707 },
708 "replace surface",
709 )
710 }
711
712 #[cfg(not(target_arch = "wasm32"))]
716 pub fn present_reconfigure(&mut self, config: wgpu::SurfaceConfiguration) -> bool {
717 let surface_epoch = self.surface_epoch;
718 let Some(handle) = self.present_handle_mut() else {
719 log::error!("present_reconfigure called without a threaded present runtime");
720 return false;
721 };
722 handle.send_control_and_wait(
723 move |ack| PresentControl::Reconfigure {
724 config,
725 surface_epoch,
726 ack,
727 },
728 "reconfigure surface",
729 )
730 }
731
732 #[cfg(not(target_arch = "wasm32"))]
736 pub fn present_drop_surface(&mut self) -> bool {
737 let Some(handle) = self.present_handle_mut() else {
738 log::error!("present_drop_surface called without a threaded present runtime");
739 return false;
740 };
741 handle.send_control_and_wait(|ack| PresentControl::DropSurface { ack }, "drop surface")
742 }
743
744 #[cfg(not(target_arch = "wasm32"))]
747 pub fn shutdown_present_runtime(&mut self) {
748 if matches!(self.backend, PresentBackend::Threaded(_)) {
749 self.retire_live_backend();
750 }
751 }
752
753 #[cfg(not(target_arch = "wasm32"))]
754 #[doc(hidden)]
755 pub fn present_attach_offscreen_for_tests(&mut self, width: u32, height: u32) -> bool {
756 let Some(handle) = self.present_handle_mut() else {
757 return false;
758 };
759 handle.send_control_and_wait(
760 move |ack| PresentControl::AttachOffscreenTargetForTests { width, height, ack },
761 "attach offscreen target",
762 )
763 }
764
765 #[cfg(not(target_arch = "wasm32"))]
766 #[doc(hidden)]
767 pub fn send_attach_offscreen_unacked_for_tests(
768 &mut self,
769 width: u32,
770 height: u32,
771 ) -> Option<std::sync::mpsc::Receiver<()>> {
772 let handle = self.present_handle_mut()?;
773 handle.send_control_unacked(move |ack| PresentControl::AttachOffscreenTargetForTests {
774 width,
775 height,
776 ack,
777 })
778 }
779
780 #[cfg(not(target_arch = "wasm32"))]
781 #[doc(hidden)]
782 pub fn send_reconfigure_unacked_for_tests(
783 &mut self,
784 config: wgpu::SurfaceConfiguration,
785 ) -> Option<std::sync::mpsc::Receiver<()>> {
786 let surface_epoch = self.surface_epoch;
787 let handle = self.present_handle_mut()?;
788 handle.send_control_unacked(move |ack| PresentControl::Reconfigure {
789 config,
790 surface_epoch,
791 ack,
792 })
793 }
794
795 #[cfg(not(target_arch = "wasm32"))]
796 #[doc(hidden)]
797 pub fn send_drop_surface_unacked_for_tests(&mut self) -> Option<std::sync::mpsc::Receiver<()>> {
798 let handle = self.present_handle_mut()?;
799 handle.send_control_unacked(|ack| PresentControl::DropSurface { ack })
800 }
801
802 pub fn last_published_frame_id(&self) -> u64 {
807 self.frontend.frame_sequence
808 }
809
810 #[cfg(not(target_arch = "wasm32"))]
811 #[doc(hidden)]
812 pub fn present_status_snapshot_for_tests(&self) -> Option<(bool, u64, u64)> {
813 match &self.backend {
814 PresentBackend::Threaded(handle) => {
815 let status = handle.status();
816 Some((
817 status
818 .needs_frame_warmup
819 .load(std::sync::atomic::Ordering::Relaxed),
820 status
821 .presented_frames
822 .load(std::sync::atomic::Ordering::Relaxed),
823 status
824 .placeholder_frames
825 .load(std::sync::atomic::Ordering::Relaxed),
826 ))
827 }
828 _ => None,
829 }
830 }
831
832 pub fn last_frame_stats(&self) -> Option<RenderStatsSnapshot> {
833 match &self.backend {
834 PresentBackend::Sync(gpu_renderer) => gpu_renderer.last_frame_stats(),
835 #[cfg(not(target_arch = "wasm32"))]
836 PresentBackend::Threaded(handle) => *handle
837 .status()
838 .last_frame_stats
839 .lock()
840 .unwrap_or_else(std::sync::PoisonError::into_inner),
841 PresentBackend::None => None,
842 }
843 }
844
845 pub fn gpu_pass_timings(&self) -> GpuPassTimingReport {
849 self.sync_gpu_renderer()
850 .map(GpuRenderer::gpu_pass_timings)
851 .unwrap_or_default()
852 }
853
854 pub fn debug_cpu_allocation_stats(&self) -> DebugCpuAllocationStats {
855 let mut stats = self
856 .sync_gpu_renderer()
857 .map(GpuRenderer::debug_cpu_allocation_stats)
858 .unwrap_or_default();
859 stats.scene_graph_node_count = self
860 .frontend
861 .scene
862 .graph
863 .as_ref()
864 .map_or(0, RenderGraph::node_count);
865 stats.scene_graph_heap_bytes = self
866 .frontend
867 .scene
868 .graph
869 .as_ref()
870 .map_or(0, RenderGraph::heap_bytes);
871 stats.scene_hits_len = self.frontend.scene.hits.len();
872 stats.scene_hits_cap = self.frontend.scene.hits.capacity();
873 stats.scene_node_index_len = self.frontend.scene.node_index.len();
874 stats.scene_node_index_cap = self.frontend.scene.node_index.capacity();
875 stats
876 }
877
878 pub fn try_device(&self) -> Option<&wgpu::Device> {
882 self.sync_gpu_renderer().map(|r| &*r.device)
883 }
884
885 #[doc(hidden)]
886 pub fn try_queue_for_tests(&self) -> Option<&wgpu::Queue> {
887 self.sync_gpu_renderer().map(|r| &*r.queue)
888 }
889
890 #[doc(hidden)]
891 pub fn device_error_count_for_tests(&self) -> u64 {
892 self.sync_gpu_renderer()
893 .map_or(0, GpuRenderer::device_error_count)
894 }
895
896 #[doc(hidden)]
897 pub fn build_frame_packet_for_tests(
898 &mut self,
899 width: u32,
900 height: u32,
901 ) -> Option<HeldFramePacket> {
902 self.frontend
903 .build_frame_packet(width, height, self.renderer_epoch, self.surface_epoch)
904 .map(HeldFramePacket)
905 }
906
907 #[doc(hidden)]
908 pub fn render_held_packet_for_tests(
909 &mut self,
910 texture: &wgpu::Texture,
911 view: &wgpu::TextureView,
912 width: u32,
913 height: u32,
914 packet: HeldFramePacket,
915 ) -> Result<PresentOutcome, WgpuRendererError> {
916 let PresentBackend::Sync(gpu_renderer) = &mut self.backend else {
917 return Err(WgpuRendererError::Wgpu(
918 "GPU renderer not initialized for synchronous rendering. Call init_gpu() first."
919 .to_string(),
920 ));
921 };
922 let mut returns = RenderReturns::default();
923 let result = gpu_renderer.render(
924 texture,
925 view,
926 width,
927 height,
928 packet.0,
929 self.surface_epoch,
930 &mut returns,
931 );
932 let outcome = returns.outcome;
933 self.frontend.apply_returns(returns);
934 result.map_err(WgpuRendererError::Wgpu)?;
935 Ok(outcome)
936 }
937}
938
939#[doc(hidden)]
940pub struct HeldFramePacket(frame_packet::FramePacket);
941
942#[cfg(not(target_arch = "wasm32"))]
943#[doc(hidden)]
944pub struct InlinePresentRuntime {
945 state: PresentState,
946 msg_rx: std::sync::mpsc::Receiver<PresentMsg>,
947 shutdown_seen: bool,
948}
949
950#[cfg(not(target_arch = "wasm32"))]
951impl InlinePresentRuntime {
952 pub fn pump(&mut self) -> bool {
953 if self.shutdown_seen {
954 return false;
955 }
956 while let Ok(msg) = self.msg_rx.try_recv() {
957 if !self.state.run_once(msg) {
958 self.shutdown_seen = true;
959 return false;
960 }
961 }
962 self.state.consume_waiting();
963 true
964 }
965
966 pub fn has_waiting_packet(&self) -> bool {
967 self.state.has_waiting_packet()
968 }
969
970 pub fn step_one_message(&mut self) -> bool {
971 if self.shutdown_seen {
972 return false;
973 }
974 match self.msg_rx.try_recv() {
975 Ok(msg) => {
976 if !self.state.run_once(msg) {
977 self.shutdown_seen = true;
978 }
979 true
980 }
981 Err(_) => false,
982 }
983 }
984
985 pub fn consume_waiting(&mut self) {
986 self.state.consume_waiting();
987 }
988}
989
990impl Default for WgpuRenderer {
991 fn default() -> Self {
992 Self::new(&[])
993 }
994}
995
996impl Renderer for WgpuRenderer {
997 type Scene = Scene;
998 type Error = WgpuRendererError;
999
1000 fn attach_app_context_services(&mut self, app_context: &cranpose_ui::AppContext) {
1001 app_context.set_text_measurer(SoftwareTextMeasurer::from_font_set(
1002 self.frontend.text_fonts.clone(),
1003 8192,
1004 ));
1005 self.frontend.app_context = Some(app_context.downgrade());
1006 }
1007
1008 fn scene(&self) -> &Self::Scene {
1009 &self.frontend.scene
1010 }
1011
1012 fn scene_mut(&mut self) -> &mut Self::Scene {
1013 &mut self.frontend.scene
1014 }
1015
1016 fn rebuild_scene(
1017 &mut self,
1018 layout_tree: &LayoutTree,
1019 _viewport: Size,
1020 ) -> Result<(), Self::Error> {
1021 self.frontend.scene.clear();
1022 self.frontend.clear_fps_overlay();
1023 pipeline::render_layout_tree(layout_tree.root(), &mut self.frontend.scene);
1024 Ok(())
1025 }
1026
1027 fn rebuild_scene_from_applier(
1028 &mut self,
1029 applier: &mut MemoryApplier,
1030 root: NodeId,
1031 _viewport: Size,
1032 ) -> Result<(), Self::Error> {
1033 self.frontend.scene.clear();
1034 self.frontend.clear_fps_overlay();
1035 pipeline::render_from_applier(applier, root, &mut self.frontend.scene, 1.0);
1036 Ok(())
1037 }
1038
1039 fn update_scene_from_applier(
1040 &mut self,
1041 applier: &mut MemoryApplier,
1042 root: NodeId,
1043 viewport: Size,
1044 dirty_nodes: &[NodeId],
1045 ) -> Result<(), Self::Error> {
1046 if dirty_nodes.is_empty() {
1047 return self.rebuild_scene_from_applier(applier, root, viewport);
1048 }
1049 self.update_scene(applier, root, dirty_nodes, true);
1050 Ok(())
1051 }
1052
1053 fn update_visual_scene_from_applier(
1054 &mut self,
1055 applier: &mut MemoryApplier,
1056 root: NodeId,
1057 viewport: Size,
1058 dirty_nodes: &[NodeId],
1059 ) -> Result<(), Self::Error> {
1060 if dirty_nodes.is_empty() {
1061 return self.rebuild_scene_from_applier(applier, root, viewport);
1062 }
1063 self.update_scene(applier, root, dirty_nodes, false);
1064 Ok(())
1065 }
1066
1067 fn draw_dev_overlay(&mut self, text: &str, viewport: Size) {
1068 const DEV_OVERLAY_NODE_ID: NodeId = NodeId::MAX;
1069 let key = cranpose_render_common::dev_overlay::DevOverlayKey::new(text, viewport);
1070 if self.frontend.dev_overlay_graph.is_some()
1071 && self
1072 .frontend
1073 .dev_overlay_cache
1074 .as_ref()
1075 .is_some_and(|cache| {
1076 cache.text == key.text
1077 && cache.viewport_width_bits == key.viewport_width_bits
1078 && cache.viewport_height_bits == key.viewport_height_bits
1079 })
1080 {
1081 return;
1082 }
1083 self.frontend.fps_overlay_graph = Some(
1084 cranpose_render_common::dev_overlay::build_dev_overlay_graph(
1085 text,
1086 viewport,
1087 DEV_OVERLAY_NODE_ID,
1088 ),
1089 );
1090 self.frontend.dev_overlay_cache = Some(DevOverlayCache {
1091 text: key.text,
1092 viewport_width_bits: key.viewport_width_bits,
1093 viewport_height_bits: key.viewport_height_bits,
1094 });
1095 self.frontend.refresh_dev_overlay();
1096 }
1097
1098 fn set_inspector_overlay(&mut self, graph: Option<cranpose_render_common::graph::RenderGraph>) {
1099 self.frontend.inspector_overlay_graph = graph;
1100 self.frontend.refresh_dev_overlay();
1101 }
1102
1103 fn needs_frame_warmup(&self) -> bool {
1104 match &self.backend {
1105 PresentBackend::Sync(gpu_renderer) => gpu_renderer.needs_frame_warmup(),
1106 #[cfg(not(target_arch = "wasm32"))]
1107 PresentBackend::Threaded(handle) => handle
1108 .status()
1109 .needs_frame_warmup
1110 .load(std::sync::atomic::Ordering::Relaxed),
1111 PresentBackend::None => false,
1112 }
1113 }
1114}
1115
1116#[cfg(test)]
1117mod tests {
1118 use std::cell::Cell;
1119
1120 use cranpose_render_common::graph::RenderNode;
1121 use cranpose_ui_graphics::GraphicsLayer;
1122
1123 use super::*;
1124 use crate::pipeline::TextLayoutResolver;
1125
1126 static TEST_FONT: &[u8] =
1127 cranpose_render_common::software_text_raster::DEFAULT_SOFTWARE_TEXT_FONT_BYTES;
1128
1129 #[path = "inspector_overlay.rs"]
1130 mod inspector_overlay;
1131
1132 #[test]
1133 fn dev_overlay_is_recorded_outside_app_graph() {
1134 let mut renderer = WgpuRenderer::new(&[]);
1135 renderer.draw_dev_overlay(
1136 "240 FPS | avg 4.0ms | p95 4.5ms",
1137 Size {
1138 width: 800.0,
1139 height: 600.0,
1140 },
1141 );
1142
1143 assert!(
1144 renderer
1145 .frontend
1146 .scene
1147 .graph
1148 .as_ref()
1149 .is_none_or(|graph| graph.root.children.iter().all(|child| {
1150 !matches!(
1151 child,
1152 RenderNode::Layer(layer) if layer.node_id == Some(NodeId::MAX)
1153 )
1154 })),
1155 "dev overlay must not be mixed into the app scene graph"
1156 );
1157
1158 let graph = renderer
1159 .frontend
1160 .dev_overlay_graph
1161 .as_ref()
1162 .expect("overlay graph");
1163 let Some(RenderNode::Layer(overlay)) = graph.root.children.last() else {
1164 panic!("dev overlay should be the final top-level layer");
1165 };
1166
1167 assert_eq!(overlay.node_id, Some(NodeId::MAX));
1168 assert_eq!(
1169 overlay.graphics_layer.compositing_strategy,
1170 GraphicsLayer::default().compositing_strategy,
1171 "dev overlay should not allocate an offscreen surface"
1172 );
1173 }
1174
1175 struct CountingTextMeasurer {
1176 inner: SoftwareTextMeasurer,
1177 layout_calls: Rc<Cell<usize>>,
1178 }
1179
1180 impl CountingTextMeasurer {
1181 fn new(layout_calls: Rc<Cell<usize>>) -> Self {
1182 Self {
1183 inner: SoftwareTextMeasurer::from_fonts_or_default(&[TEST_FONT], 16),
1184 layout_calls,
1185 }
1186 }
1187 }
1188
1189 impl TextMeasurer for CountingTextMeasurer {
1190 fn measure(
1191 &self,
1192 text: &cranpose_ui::text::AnnotatedString,
1193 style: &cranpose_ui::text::TextStyle,
1194 ) -> cranpose_ui::TextMetrics {
1195 self.inner.measure(text, style)
1196 }
1197
1198 fn get_offset_for_position(
1199 &self,
1200 text: &cranpose_ui::text::AnnotatedString,
1201 style: &cranpose_ui::text::TextStyle,
1202 x: f32,
1203 y: f32,
1204 ) -> usize {
1205 self.inner.get_offset_for_position(text, style, x, y)
1206 }
1207
1208 fn get_cursor_x_for_offset(
1209 &self,
1210 text: &cranpose_ui::text::AnnotatedString,
1211 style: &cranpose_ui::text::TextStyle,
1212 offset: usize,
1213 ) -> f32 {
1214 self.inner.get_cursor_x_for_offset(text, style, offset)
1215 }
1216
1217 fn layout(
1218 &self,
1219 text: &cranpose_ui::text::AnnotatedString,
1220 style: &cranpose_ui::text::TextStyle,
1221 ) -> cranpose_ui::text_layout_result::TextLayoutResult {
1222 self.layout_calls.set(self.layout_calls.get() + 1);
1223 self.inner.layout(text, style)
1224 }
1225 }
1226
1227 #[test]
1228 fn headless_text_measurer_uses_software_text_font() {
1229 let measurer = headless_text_measurer_with_fonts(&[TEST_FONT]);
1230 let text = cranpose_ui::text::AnnotatedString::from("software text measurement");
1231 let style = cranpose_ui::text::TextStyle::default();
1232
1233 let metrics = measurer.measure(&text, &style);
1234 let layout = measurer.layout(&text, &style);
1235
1236 assert!(metrics.width > 0.0);
1237 assert!(metrics.height > 0.0);
1238 assert_eq!(layout.lines.len(), metrics.line_count);
1239 }
1240
1241 #[test]
1242 fn renderer_measurement_uses_software_text_service_without_render_cache_side_effect() {
1243 let mut renderer = WgpuRenderer::new(&[TEST_FONT]);
1244 let app_context = cranpose_ui::AppContext::new();
1245 renderer.attach_app_context_services(&app_context);
1246
1247 let metrics = app_context.enter(|| {
1248 let text = cranpose_ui::text::AnnotatedString::from("phase local text cache");
1249 let style = cranpose_ui::text::TextStyle {
1250 span_style: cranpose_ui::text::SpanStyle {
1251 font_size: cranpose_ui::text::TextUnit::Sp(14.0),
1252 ..Default::default()
1253 },
1254 paragraph_style: cranpose_ui::text::ParagraphStyle {
1255 platform_style: Some(cranpose_ui::text::PlatformParagraphStyle {
1256 include_font_padding: None,
1257 shaping: Some(cranpose_ui::text::TextShaping::Basic),
1258 }),
1259 ..Default::default()
1260 },
1261 };
1262 cranpose_ui::text::measure_text(&text, &style)
1263 });
1264
1265 assert!(
1266 metrics.width > 0.0,
1267 "software text service should measure text"
1268 );
1269 assert_eq!(
1270 renderer.frontend.text_state.text_cache_len(),
1271 0,
1272 "WGPU must not keep a renderer-side shaping cache for measurement"
1273 );
1274 }
1275
1276 #[test]
1277 fn renderer_attached_text_service_measures_long_multiline_text_with_software_line_height() {
1278 let mut renderer = WgpuRenderer::new(&[TEST_FONT]);
1279 let app_context = cranpose_ui::AppContext::new();
1280 renderer.attach_app_context_services(&app_context);
1281
1282 let prepared = app_context.enter(|| {
1283 let text = cranpose_ui::text::AnnotatedString::from(
1284 (0..48)
1285 .map(|line| format!("// markdown code line {line:02}"))
1286 .collect::<Vec<_>>()
1287 .join("\n"),
1288 );
1289 let style = cranpose_ui::text::TextStyle::default();
1290 cranpose_ui::text::prepare_text_layout(
1291 &text,
1292 &style,
1293 cranpose_ui::text::TextLayoutOptions::default(),
1294 Some(952.0),
1295 )
1296 });
1297
1298 assert_eq!(prepared.metrics.line_count, 48);
1299 assert!(
1300 prepared.metrics.line_height > 18.0,
1301 "renderer-attached text service must not use fallback monospaced line height: {:?}",
1302 prepared.metrics
1303 );
1304 assert!(
1305 prepared.metrics.height > 900.0,
1306 "48 software-measured lines should not collapse to a viewport-sized block: {:?}",
1307 prepared.metrics
1308 );
1309 }
1310
1311 #[test]
1312 fn render_text_layout_routes_through_attached_app_context_service() {
1313 let mut renderer = WgpuRenderer::new(&[TEST_FONT]);
1314 let app_context = cranpose_ui::AppContext::new();
1315 renderer.attach_app_context_services(&app_context);
1316 let layout_calls = Rc::new(Cell::new(0));
1317 app_context.set_text_measurer(CountingTextMeasurer::new(Rc::clone(&layout_calls)));
1318
1319 app_context.enter(|| {
1320 let text = cranpose_ui::text::AnnotatedString::from("render text");
1321 let style = cranpose_ui::text::TextStyle::default();
1322 let layout = renderer.frontend.text_state.layout_text(&text, &style);
1323 assert!(layout.width > 0.0);
1324 });
1325
1326 assert_eq!(layout_calls.get(), 1);
1327 }
1328}