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 fn render_frame(
504 &mut self,
505 texture: &wgpu::Texture,
506 view: &wgpu::TextureView,
507 width: u32,
508 height: u32,
509 ) -> Result<(), WgpuRendererError> {
510 let PresentBackend::Sync(gpu_renderer) = &mut self.backend else {
511 return Err(WgpuRendererError::Wgpu(
512 "GPU renderer not initialized for synchronous rendering. Call init_gpu() first."
513 .to_string(),
514 ));
515 };
516 let packet = self
517 .frontend
518 .build_frame_packet(width, height, self.renderer_epoch, self.surface_epoch)
519 .ok_or_else(|| WgpuRendererError::Wgpu("scene graph is missing".to_string()))?;
520 let mut returns = RenderReturns::default();
521 let result = gpu_renderer.render(
522 texture,
523 view,
524 width,
525 height,
526 packet,
527 self.surface_epoch,
528 &mut returns,
529 );
530 self.frontend.apply_returns(returns);
531 result.map_err(WgpuRendererError::Wgpu)
532 }
533
534 pub fn capture_frame(
538 &mut self,
539 width: u32,
540 height: u32,
541 ) -> Result<CapturedFrame, WgpuRendererError> {
542 self.capture_frame_with_scale(width, height, self.frontend.root_scale)
543 }
544
545 pub fn capture_frame_with_scale(
547 &mut self,
548 width: u32,
549 height: u32,
550 root_scale: f32,
551 ) -> Result<CapturedFrame, WgpuRendererError> {
552 let PresentBackend::Sync(gpu_renderer) = &mut self.backend else {
553 return Err(WgpuRendererError::Wgpu(
554 "GPU renderer not initialized for synchronous rendering. Call init_gpu() first."
555 .to_string(),
556 ));
557 };
558 let packet = self
559 .frontend
560 .build_frame_packet_with_scale(
561 width,
562 height,
563 root_scale,
564 self.renderer_epoch,
565 self.surface_epoch,
566 )
567 .ok_or_else(|| WgpuRendererError::Wgpu("scene graph is missing".to_string()))?;
568 let mut returns = RenderReturns::default();
569 let result = gpu_renderer.render_to_rgba_pixels(
570 width,
571 height,
572 packet,
573 self.surface_epoch,
574 &mut returns,
575 );
576 self.frontend.apply_returns(returns);
577 let pixels = result.map_err(WgpuRendererError::Wgpu)?;
578 Ok(CapturedFrame {
579 width,
580 height,
581 pixels,
582 })
583 }
584
585 #[cfg(not(target_arch = "wasm32"))]
590 pub fn has_frame_credit(&self) -> bool {
591 match &self.backend {
592 PresentBackend::Threaded(handle) => handle.has_credit(),
593 PresentBackend::Sync(_) | PresentBackend::None => true,
594 }
595 }
596
597 #[cfg(not(target_arch = "wasm32"))]
603 pub fn publish_frame(&mut self, width: u32, height: u32) -> PublishOutcome {
604 let PresentBackend::Threaded(handle) = &mut self.backend else {
605 log::error!("publish_frame called without a threaded present runtime");
606 return PublishOutcome::NoCredit;
607 };
608 if !handle.has_credit() {
609 return PublishOutcome::NoCredit;
610 }
611 let Some(packet) = self.frontend.build_frame_packet(
612 width,
613 height,
614 self.renderer_epoch,
615 self.surface_epoch,
616 ) else {
617 return PublishOutcome::NoGraph;
618 };
619 match handle.publish(packet) {
620 Ok(()) => PublishOutcome::Published,
621 Err(packet) => {
622 let mut returns = RenderReturns::default();
623 let _ = GpuRenderer::cancel_packet(
624 *packet,
625 CancelReason::SurfaceUnavailable,
626 &mut returns,
627 );
628 self.frontend.apply_returns(returns);
629 log::error!("present runtime unavailable; frame recovered, not published");
630 PublishOutcome::NoCredit
631 }
632 }
633 }
634
635 #[cfg(not(target_arch = "wasm32"))]
639 pub fn drain_present_returns(&mut self) -> usize {
640 self.drain_present_returns_with(&mut |_, _, _| {})
641 }
642
643 #[cfg(not(target_arch = "wasm32"))]
647 pub fn drain_present_returns_with(
648 &mut self,
649 on_return: &mut dyn FnMut(u64, PresentOutcome, PresentTimings),
650 ) -> usize {
651 let mut drained = 0;
652 loop {
653 let returns = {
654 let PresentBackend::Threaded(handle) = &mut self.backend else {
655 break;
656 };
657 match handle.try_drain() {
658 Some(returns) => returns,
659 None => break,
660 }
661 };
662 drained += 1;
663 let frame_id = returns.frame_id;
664 let outcome = returns.outcome;
665 let timings = returns.timings;
666 self.frontend.apply_returns(returns);
667 on_return(frame_id, outcome, timings);
668 }
669 drained
670 }
671
672 #[cfg(not(target_arch = "wasm32"))]
678 pub fn present_replace_surface(
679 &mut self,
680 surface: wgpu::Surface<'static>,
681 config: wgpu::SurfaceConfiguration,
682 ) -> bool {
683 let surface_epoch = self.surface_epoch;
684 let Some(handle) = self.present_handle_mut() else {
685 log::error!("present_replace_surface called without a threaded present runtime");
686 return false;
687 };
688 handle.send_control_and_wait(
689 move |ack| PresentControl::ReplaceSurface {
690 surface,
691 config,
692 surface_epoch,
693 ack,
694 },
695 "replace surface",
696 )
697 }
698
699 #[cfg(not(target_arch = "wasm32"))]
703 pub fn present_reconfigure(&mut self, config: wgpu::SurfaceConfiguration) -> bool {
704 let surface_epoch = self.surface_epoch;
705 let Some(handle) = self.present_handle_mut() else {
706 log::error!("present_reconfigure called without a threaded present runtime");
707 return false;
708 };
709 handle.send_control_and_wait(
710 move |ack| PresentControl::Reconfigure {
711 config,
712 surface_epoch,
713 ack,
714 },
715 "reconfigure surface",
716 )
717 }
718
719 #[cfg(not(target_arch = "wasm32"))]
723 pub fn present_drop_surface(&mut self) -> bool {
724 let Some(handle) = self.present_handle_mut() else {
725 log::error!("present_drop_surface called without a threaded present runtime");
726 return false;
727 };
728 handle.send_control_and_wait(|ack| PresentControl::DropSurface { ack }, "drop surface")
729 }
730
731 #[cfg(not(target_arch = "wasm32"))]
734 pub fn shutdown_present_runtime(&mut self) {
735 if matches!(self.backend, PresentBackend::Threaded(_)) {
736 self.retire_live_backend();
737 }
738 }
739
740 #[cfg(not(target_arch = "wasm32"))]
741 #[doc(hidden)]
742 pub fn present_attach_offscreen_for_tests(&mut self, width: u32, height: u32) -> bool {
743 let Some(handle) = self.present_handle_mut() else {
744 return false;
745 };
746 handle.send_control_and_wait(
747 move |ack| PresentControl::AttachOffscreenTargetForTests { width, height, ack },
748 "attach offscreen target",
749 )
750 }
751
752 #[cfg(not(target_arch = "wasm32"))]
753 #[doc(hidden)]
754 pub fn send_attach_offscreen_unacked_for_tests(
755 &mut self,
756 width: u32,
757 height: u32,
758 ) -> Option<std::sync::mpsc::Receiver<()>> {
759 let handle = self.present_handle_mut()?;
760 handle.send_control_unacked(move |ack| PresentControl::AttachOffscreenTargetForTests {
761 width,
762 height,
763 ack,
764 })
765 }
766
767 #[cfg(not(target_arch = "wasm32"))]
768 #[doc(hidden)]
769 pub fn send_reconfigure_unacked_for_tests(
770 &mut self,
771 config: wgpu::SurfaceConfiguration,
772 ) -> Option<std::sync::mpsc::Receiver<()>> {
773 let surface_epoch = self.surface_epoch;
774 let handle = self.present_handle_mut()?;
775 handle.send_control_unacked(move |ack| PresentControl::Reconfigure {
776 config,
777 surface_epoch,
778 ack,
779 })
780 }
781
782 #[cfg(not(target_arch = "wasm32"))]
783 #[doc(hidden)]
784 pub fn send_drop_surface_unacked_for_tests(&mut self) -> Option<std::sync::mpsc::Receiver<()>> {
785 let handle = self.present_handle_mut()?;
786 handle.send_control_unacked(|ack| PresentControl::DropSurface { ack })
787 }
788
789 pub fn last_published_frame_id(&self) -> u64 {
794 self.frontend.frame_sequence
795 }
796
797 #[cfg(not(target_arch = "wasm32"))]
798 #[doc(hidden)]
799 pub fn present_status_snapshot_for_tests(&self) -> Option<(bool, u64, u64)> {
800 match &self.backend {
801 PresentBackend::Threaded(handle) => {
802 let status = handle.status();
803 Some((
804 status
805 .needs_frame_warmup
806 .load(std::sync::atomic::Ordering::Relaxed),
807 status
808 .presented_frames
809 .load(std::sync::atomic::Ordering::Relaxed),
810 status
811 .placeholder_frames
812 .load(std::sync::atomic::Ordering::Relaxed),
813 ))
814 }
815 _ => None,
816 }
817 }
818
819 pub fn last_frame_stats(&self) -> Option<RenderStatsSnapshot> {
820 match &self.backend {
821 PresentBackend::Sync(gpu_renderer) => gpu_renderer.last_frame_stats(),
822 #[cfg(not(target_arch = "wasm32"))]
823 PresentBackend::Threaded(handle) => *handle
824 .status()
825 .last_frame_stats
826 .lock()
827 .unwrap_or_else(|poisoned| poisoned.into_inner()),
828 PresentBackend::None => None,
829 }
830 }
831
832 pub fn gpu_pass_timings(&self) -> GpuPassTimingReport {
836 self.sync_gpu_renderer()
837 .map(GpuRenderer::gpu_pass_timings)
838 .unwrap_or_default()
839 }
840
841 pub fn debug_cpu_allocation_stats(&self) -> DebugCpuAllocationStats {
842 let mut stats = self
843 .sync_gpu_renderer()
844 .map(GpuRenderer::debug_cpu_allocation_stats)
845 .unwrap_or_default();
846 stats.scene_graph_node_count = self
847 .frontend
848 .scene
849 .graph
850 .as_ref()
851 .map(RenderGraph::node_count)
852 .unwrap_or(0);
853 stats.scene_graph_heap_bytes = self
854 .frontend
855 .scene
856 .graph
857 .as_ref()
858 .map(RenderGraph::heap_bytes)
859 .unwrap_or(0);
860 stats.scene_hits_len = self.frontend.scene.hits.len();
861 stats.scene_hits_cap = self.frontend.scene.hits.capacity();
862 stats.scene_node_index_len = self.frontend.scene.node_index.len();
863 stats.scene_node_index_cap = self.frontend.scene.node_index.capacity();
864 stats
865 }
866
867 pub fn try_device(&self) -> Option<&wgpu::Device> {
871 self.sync_gpu_renderer().map(|r| &*r.device)
872 }
873
874 #[doc(hidden)]
875 pub fn try_queue_for_tests(&self) -> Option<&wgpu::Queue> {
876 self.sync_gpu_renderer().map(|r| &*r.queue)
877 }
878
879 #[doc(hidden)]
880 pub fn device_error_count_for_tests(&self) -> u64 {
881 self.sync_gpu_renderer()
882 .map(GpuRenderer::device_error_count)
883 .unwrap_or(0)
884 }
885
886 #[doc(hidden)]
887 pub fn build_frame_packet_for_tests(
888 &mut self,
889 width: u32,
890 height: u32,
891 ) -> Option<HeldFramePacket> {
892 self.frontend
893 .build_frame_packet(width, height, self.renderer_epoch, self.surface_epoch)
894 .map(HeldFramePacket)
895 }
896
897 #[doc(hidden)]
898 pub fn render_held_packet_for_tests(
899 &mut self,
900 texture: &wgpu::Texture,
901 view: &wgpu::TextureView,
902 width: u32,
903 height: u32,
904 packet: HeldFramePacket,
905 ) -> Result<PresentOutcome, WgpuRendererError> {
906 let PresentBackend::Sync(gpu_renderer) = &mut self.backend else {
907 return Err(WgpuRendererError::Wgpu(
908 "GPU renderer not initialized for synchronous rendering. Call init_gpu() first."
909 .to_string(),
910 ));
911 };
912 let mut returns = RenderReturns::default();
913 let result = gpu_renderer.render(
914 texture,
915 view,
916 width,
917 height,
918 packet.0,
919 self.surface_epoch,
920 &mut returns,
921 );
922 let outcome = returns.outcome;
923 self.frontend.apply_returns(returns);
924 result.map_err(WgpuRendererError::Wgpu)?;
925 Ok(outcome)
926 }
927}
928
929#[doc(hidden)]
930pub struct HeldFramePacket(frame_packet::FramePacket);
931
932#[cfg(not(target_arch = "wasm32"))]
933#[doc(hidden)]
934pub struct InlinePresentRuntime {
935 state: PresentState,
936 msg_rx: std::sync::mpsc::Receiver<PresentMsg>,
937 shutdown_seen: bool,
938}
939
940#[cfg(not(target_arch = "wasm32"))]
941impl InlinePresentRuntime {
942 pub fn pump(&mut self) -> bool {
943 if self.shutdown_seen {
944 return false;
945 }
946 while let Ok(msg) = self.msg_rx.try_recv() {
947 if !self.state.run_once(msg) {
948 self.shutdown_seen = true;
949 return false;
950 }
951 }
952 self.state.consume_waiting();
953 true
954 }
955
956 pub fn has_waiting_packet(&self) -> bool {
957 self.state.has_waiting_packet()
958 }
959
960 pub fn step_one_message(&mut self) -> bool {
961 if self.shutdown_seen {
962 return false;
963 }
964 match self.msg_rx.try_recv() {
965 Ok(msg) => {
966 if !self.state.run_once(msg) {
967 self.shutdown_seen = true;
968 }
969 true
970 }
971 Err(_) => false,
972 }
973 }
974
975 pub fn consume_waiting(&mut self) {
976 self.state.consume_waiting();
977 }
978}
979
980impl Default for WgpuRenderer {
981 fn default() -> Self {
982 Self::new(&[])
983 }
984}
985
986impl Renderer for WgpuRenderer {
987 type Scene = Scene;
988 type Error = WgpuRendererError;
989
990 fn attach_app_context_services(&mut self, app_context: &cranpose_ui::AppContext) {
991 app_context.set_text_measurer(SoftwareTextMeasurer::from_font_set(
992 self.frontend.text_fonts.clone(),
993 8192,
994 ));
995 self.frontend.app_context = Some(app_context.downgrade());
996 }
997
998 fn scene(&self) -> &Self::Scene {
999 &self.frontend.scene
1000 }
1001
1002 fn scene_mut(&mut self) -> &mut Self::Scene {
1003 &mut self.frontend.scene
1004 }
1005
1006 fn rebuild_scene(
1007 &mut self,
1008 layout_tree: &LayoutTree,
1009 _viewport: Size,
1010 ) -> Result<(), Self::Error> {
1011 self.frontend.scene.clear();
1012 self.frontend.clear_fps_overlay();
1013 pipeline::render_layout_tree(layout_tree.root(), &mut self.frontend.scene);
1014 Ok(())
1015 }
1016
1017 fn rebuild_scene_from_applier(
1018 &mut self,
1019 applier: &mut MemoryApplier,
1020 root: NodeId,
1021 _viewport: Size,
1022 ) -> Result<(), Self::Error> {
1023 self.frontend.scene.clear();
1024 self.frontend.clear_fps_overlay();
1025 pipeline::render_from_applier(applier, root, &mut self.frontend.scene, 1.0);
1026 Ok(())
1027 }
1028
1029 fn update_scene_from_applier(
1030 &mut self,
1031 applier: &mut MemoryApplier,
1032 root: NodeId,
1033 viewport: Size,
1034 dirty_nodes: &[NodeId],
1035 ) -> Result<(), Self::Error> {
1036 if dirty_nodes.is_empty() {
1037 return self.rebuild_scene_from_applier(applier, root, viewport);
1038 }
1039 self.update_scene(applier, root, dirty_nodes, true);
1040 Ok(())
1041 }
1042
1043 fn update_visual_scene_from_applier(
1044 &mut self,
1045 applier: &mut MemoryApplier,
1046 root: NodeId,
1047 viewport: Size,
1048 dirty_nodes: &[NodeId],
1049 ) -> Result<(), Self::Error> {
1050 if dirty_nodes.is_empty() {
1051 return self.rebuild_scene_from_applier(applier, root, viewport);
1052 }
1053 self.update_scene(applier, root, dirty_nodes, false);
1054 Ok(())
1055 }
1056
1057 fn draw_dev_overlay(&mut self, text: &str, viewport: Size) {
1058 const DEV_OVERLAY_NODE_ID: NodeId = NodeId::MAX;
1059 let key = cranpose_render_common::dev_overlay::DevOverlayKey::new(text, viewport);
1060 if self.frontend.dev_overlay_graph.is_some()
1061 && self
1062 .frontend
1063 .dev_overlay_cache
1064 .as_ref()
1065 .is_some_and(|cache| {
1066 cache.text == key.text
1067 && cache.viewport_width_bits == key.viewport_width_bits
1068 && cache.viewport_height_bits == key.viewport_height_bits
1069 })
1070 {
1071 return;
1072 }
1073 self.frontend.fps_overlay_graph = Some(
1074 cranpose_render_common::dev_overlay::build_dev_overlay_graph(
1075 text,
1076 viewport,
1077 DEV_OVERLAY_NODE_ID,
1078 ),
1079 );
1080 self.frontend.dev_overlay_cache = Some(DevOverlayCache {
1081 text: key.text,
1082 viewport_width_bits: key.viewport_width_bits,
1083 viewport_height_bits: key.viewport_height_bits,
1084 });
1085 self.frontend.refresh_dev_overlay();
1086 }
1087
1088 fn set_inspector_overlay(&mut self, graph: Option<cranpose_render_common::graph::RenderGraph>) {
1089 self.frontend.inspector_overlay_graph = graph;
1090 self.frontend.refresh_dev_overlay();
1091 }
1092
1093 fn needs_frame_warmup(&self) -> bool {
1094 match &self.backend {
1095 PresentBackend::Sync(gpu_renderer) => gpu_renderer.needs_frame_warmup(),
1096 #[cfg(not(target_arch = "wasm32"))]
1097 PresentBackend::Threaded(handle) => handle
1098 .status()
1099 .needs_frame_warmup
1100 .load(std::sync::atomic::Ordering::Relaxed),
1101 PresentBackend::None => false,
1102 }
1103 }
1104}
1105
1106#[cfg(test)]
1107mod tests {
1108 use std::cell::Cell;
1109
1110 use cranpose_render_common::graph::RenderNode;
1111 use cranpose_ui_graphics::GraphicsLayer;
1112
1113 use super::*;
1114 use crate::pipeline::TextLayoutResolver;
1115
1116 static TEST_FONT: &[u8] =
1117 cranpose_render_common::software_text_raster::DEFAULT_SOFTWARE_TEXT_FONT_BYTES;
1118
1119 #[path = "inspector_overlay.rs"]
1120 mod inspector_overlay;
1121
1122 #[test]
1123 fn dev_overlay_is_recorded_outside_app_graph() {
1124 let mut renderer = WgpuRenderer::new(&[]);
1125 renderer.draw_dev_overlay(
1126 "240 FPS | avg 4.0ms | p95 4.5ms",
1127 Size {
1128 width: 800.0,
1129 height: 600.0,
1130 },
1131 );
1132
1133 assert!(
1134 renderer
1135 .frontend
1136 .scene
1137 .graph
1138 .as_ref()
1139 .is_none_or(|graph| graph.root.children.iter().all(|child| {
1140 !matches!(
1141 child,
1142 RenderNode::Layer(layer) if layer.node_id == Some(NodeId::MAX)
1143 )
1144 })),
1145 "dev overlay must not be mixed into the app scene graph"
1146 );
1147
1148 let graph = renderer
1149 .frontend
1150 .dev_overlay_graph
1151 .as_ref()
1152 .expect("overlay graph");
1153 let Some(RenderNode::Layer(overlay)) = graph.root.children.last() else {
1154 panic!("dev overlay should be the final top-level layer");
1155 };
1156
1157 assert_eq!(overlay.node_id, Some(NodeId::MAX));
1158 assert_eq!(
1159 overlay.graphics_layer.compositing_strategy,
1160 GraphicsLayer::default().compositing_strategy,
1161 "dev overlay should not allocate an offscreen surface"
1162 );
1163 }
1164
1165 struct CountingTextMeasurer {
1166 inner: SoftwareTextMeasurer,
1167 layout_calls: Rc<Cell<usize>>,
1168 }
1169
1170 impl CountingTextMeasurer {
1171 fn new(layout_calls: Rc<Cell<usize>>) -> Self {
1172 Self {
1173 inner: SoftwareTextMeasurer::from_fonts_or_default(&[TEST_FONT], 16),
1174 layout_calls,
1175 }
1176 }
1177 }
1178
1179 impl TextMeasurer for CountingTextMeasurer {
1180 fn measure(
1181 &self,
1182 text: &cranpose_ui::text::AnnotatedString,
1183 style: &cranpose_ui::text::TextStyle,
1184 ) -> cranpose_ui::TextMetrics {
1185 self.inner.measure(text, style)
1186 }
1187
1188 fn get_offset_for_position(
1189 &self,
1190 text: &cranpose_ui::text::AnnotatedString,
1191 style: &cranpose_ui::text::TextStyle,
1192 x: f32,
1193 y: f32,
1194 ) -> usize {
1195 self.inner.get_offset_for_position(text, style, x, y)
1196 }
1197
1198 fn get_cursor_x_for_offset(
1199 &self,
1200 text: &cranpose_ui::text::AnnotatedString,
1201 style: &cranpose_ui::text::TextStyle,
1202 offset: usize,
1203 ) -> f32 {
1204 self.inner.get_cursor_x_for_offset(text, style, offset)
1205 }
1206
1207 fn layout(
1208 &self,
1209 text: &cranpose_ui::text::AnnotatedString,
1210 style: &cranpose_ui::text::TextStyle,
1211 ) -> cranpose_ui::text_layout_result::TextLayoutResult {
1212 self.layout_calls.set(self.layout_calls.get() + 1);
1213 self.inner.layout(text, style)
1214 }
1215 }
1216
1217 #[test]
1218 fn headless_text_measurer_uses_software_text_font() {
1219 let measurer = headless_text_measurer_with_fonts(&[TEST_FONT]);
1220 let text = cranpose_ui::text::AnnotatedString::from("software text measurement");
1221 let style = cranpose_ui::text::TextStyle::default();
1222
1223 let metrics = measurer.measure(&text, &style);
1224 let layout = measurer.layout(&text, &style);
1225
1226 assert!(metrics.width > 0.0);
1227 assert!(metrics.height > 0.0);
1228 assert_eq!(layout.lines.len(), metrics.line_count);
1229 }
1230
1231 #[test]
1232 fn renderer_measurement_uses_software_text_service_without_render_cache_side_effect() {
1233 let mut renderer = WgpuRenderer::new(&[TEST_FONT]);
1234 let app_context = cranpose_ui::AppContext::new();
1235 renderer.attach_app_context_services(&app_context);
1236
1237 let metrics = app_context.enter(|| {
1238 let text = cranpose_ui::text::AnnotatedString::from("phase local text cache");
1239 let style = cranpose_ui::text::TextStyle {
1240 span_style: cranpose_ui::text::SpanStyle {
1241 font_size: cranpose_ui::text::TextUnit::Sp(14.0),
1242 ..Default::default()
1243 },
1244 paragraph_style: cranpose_ui::text::ParagraphStyle {
1245 platform_style: Some(cranpose_ui::text::PlatformParagraphStyle {
1246 include_font_padding: None,
1247 shaping: Some(cranpose_ui::text::TextShaping::Basic),
1248 }),
1249 ..Default::default()
1250 },
1251 };
1252 cranpose_ui::text::measure_text(&text, &style)
1253 });
1254
1255 assert!(
1256 metrics.width > 0.0,
1257 "software text service should measure text"
1258 );
1259 assert_eq!(
1260 renderer.frontend.text_state.text_cache_len(),
1261 0,
1262 "WGPU must not keep a renderer-side shaping cache for measurement"
1263 );
1264 }
1265
1266 #[test]
1267 fn renderer_attached_text_service_measures_long_multiline_text_with_software_line_height() {
1268 let mut renderer = WgpuRenderer::new(&[TEST_FONT]);
1269 let app_context = cranpose_ui::AppContext::new();
1270 renderer.attach_app_context_services(&app_context);
1271
1272 let prepared = app_context.enter(|| {
1273 let text = cranpose_ui::text::AnnotatedString::from(
1274 (0..48)
1275 .map(|line| format!("// markdown code line {line:02}"))
1276 .collect::<Vec<_>>()
1277 .join("\n"),
1278 );
1279 let style = cranpose_ui::text::TextStyle::default();
1280 cranpose_ui::text::prepare_text_layout(
1281 &text,
1282 &style,
1283 cranpose_ui::text::TextLayoutOptions::default(),
1284 Some(952.0),
1285 )
1286 });
1287
1288 assert_eq!(prepared.metrics.line_count, 48);
1289 assert!(
1290 prepared.metrics.line_height > 18.0,
1291 "renderer-attached text service must not use fallback monospaced line height: {:?}",
1292 prepared.metrics
1293 );
1294 assert!(
1295 prepared.metrics.height > 900.0,
1296 "48 software-measured lines should not collapse to a viewport-sized block: {:?}",
1297 prepared.metrics
1298 );
1299 }
1300
1301 #[test]
1302 fn render_text_layout_routes_through_attached_app_context_service() {
1303 let mut renderer = WgpuRenderer::new(&[TEST_FONT]);
1304 let app_context = cranpose_ui::AppContext::new();
1305 renderer.attach_app_context_services(&app_context);
1306 let layout_calls = Rc::new(Cell::new(0));
1307 app_context.set_text_measurer(CountingTextMeasurer::new(Rc::clone(&layout_calls)));
1308
1309 app_context.enter(|| {
1310 let text = cranpose_ui::text::AnnotatedString::from("render text");
1311 let style = cranpose_ui::text::TextStyle::default();
1312 let layout = renderer.frontend.text_state.layout_text(&text, &style);
1313 assert!(layout.width > 0.0);
1314 });
1315
1316 assert_eq!(layout_calls.get(), 1);
1317 }
1318}