1#![doc = include_str!("../README.md")]
2
3pub(crate) use cranpose_render_common::debug_toggles;
4pub use debug_toggles::{
5 DebugToggle, debug_toggle, debug_toggle_os, set_debug_toggle, set_debug_toggle_os,
6};
7pub use offscreen::COMPOSITION_BYTES_PER_PIXEL;
8pub use render::presentable_root_usages;
9mod ablation;
10mod arc_trig_fill;
11mod capture_hash;
12mod collect;
13mod depth_gate;
14mod draw_pass;
15mod effect_renderer;
16mod fast_cores;
17mod frame;
18mod geometry;
19mod idle_pool;
20mod layer_cache;
21pub use fast_cores::pin_current_thread_to_fast_cores;
22mod fixed_pipeline;
23mod frame_graph;
24mod frame_packet;
25mod frontend;
26mod glass_split;
27#[cfg(all(target_arch = "wasm32", feature = "gl-templates"))]
29mod gl_templates {
30 include!(concat!(env!("OUT_DIR"), "/gl_templates.rs"));
31}
32#[cfg(test)]
33#[path = "tests/gles_lowering.rs"]
34mod gles_lowering;
35mod glyph_run;
36mod glyph_run_arena;
37pub(crate) mod gpu_stats;
38mod initial_present;
39mod lazy_resource;
40mod offscreen;
41mod opaque_prefix;
42mod output_conversion;
43pub(crate) mod pass_timing;
44mod pipeline;
45mod pipeline_compiler;
46#[cfg(not(target_arch = "wasm32"))]
47pub mod pipeline_disk_cache;
48mod pipeline_recorder;
49mod pipeline_records;
50#[cfg(not(target_arch = "wasm32"))]
51mod present_runtime;
52mod record_columns;
53mod render;
54mod rrect_shadow;
55mod run_geometry;
56mod run_store;
57mod scene;
58mod shader_cache;
59#[cfg(not(target_arch = "wasm32"))]
60mod shader_flatten;
61#[cfg(not(target_arch = "wasm32"))]
62mod shader_fold;
63mod shaders;
64mod shape_pipelines;
65mod shared_shader;
66#[cfg(test)]
67mod test_support;
68mod thread_state;
69
70use std::{rc::Rc, sync::Arc};
71
72use cranpose_core::{MemoryApplier, NodeId};
73use cranpose_render_common::{
74 RenderScene, Renderer,
75 graph::RenderGraph,
76 software_text_raster::{
77 SoftwareTextFontSet, SoftwareTextMeasurer, software_text_font_set_from_fonts_or_default,
78 },
79};
80use cranpose_ui::{LayoutTree, TextMeasurer};
81use cranpose_ui_graphics::{Rect, Size};
82pub use frame_packet::PresentTimings;
83
84#[cfg(not(target_arch = "wasm32"))]
88pub trait PresentObserver: Send {
89 fn before_present(&mut self, surface: &wgpu::Surface<'static>);
91 fn after_present(&mut self, surface: &wgpu::Surface<'static>);
93
94 fn before_reconfigure(&mut self, _surface: &wgpu::Surface<'static>) {}
97}
98use frame_packet::RenderReturns;
99#[doc(hidden)]
100pub use frame_packet::{CancelReason, PresentOutcome};
101use frontend::{DevOverlayCache, RendererFrontend};
102pub use gpu_stats::FrameStatsSnapshot as RenderStatsSnapshot;
103pub use initial_present::{clear_to_background, clear_to_default_background};
104pub use pass_timing::{GpuPassTimingEntry, GpuPassTimingReport};
105use pipeline_compiler::PipelineCompilation;
106#[cfg(not(target_arch = "wasm32"))]
107use present_runtime::{
108 PresentControl, PresentHandle, PresentMsg, PresentRuntimeInit, PresentState,
109};
110use render::{GpuRenderer, GpuRendererInit};
111pub use render::{
112 frame_clear_color, frames_presented, pipelines_created, pipelines_created_off_frame,
113};
114pub use scene::{HitRegion, Scene};
115pub use shared_shader::{shader_stages_flattened, shader_stages_folded};
116
117pub fn optional_device_features(adapter: &wgpu::Adapter) -> wgpu::Features {
126 let mut wanted = wgpu::Features::PIPELINE_CACHE | wgpu::Features::TIMESTAMP_QUERY;
127 if matches!(
128 adapter.get_info().device_type,
129 wgpu::DeviceType::IntegratedGpu | wgpu::DeviceType::Cpu
130 ) {
131 wanted |= wgpu::Features::MAPPABLE_PRIMARY_BUFFERS;
132 }
133 adapter.features() & wanted
134}
135
136#[doc(hidden)]
137pub fn offscreen_render_target_for_tests(
138 device: &wgpu::Device,
139 width: u32,
140 height: u32,
141 label: &str,
142) -> (wgpu::Texture, wgpu::TextureView) {
143 let texture = offscreen::create_2d_texture(
144 device,
145 wgpu::TextureFormat::Rgba8Unorm,
146 width,
147 height,
148 wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
149 Some(label),
150 );
151 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
152 (texture, view)
153}
154
155pub(crate) fn rect_to_quad(rect: Rect) -> [[f32; 2]; 4] {
156 [
157 [rect.x, rect.y],
158 [rect.x + rect.width, rect.y],
159 [rect.x, rect.y + rect.height],
160 [rect.x + rect.width, rect.y + rect.height],
161 ]
162}
163
164#[derive(Debug)]
165pub enum WgpuRendererError {
166 Layout(String),
167 Wgpu(String),
168}
169
170#[derive(Debug, Clone)]
172pub struct CapturedFrame {
173 pub width: u32,
174 pub height: u32,
175 pub pixels: Vec<u8>,
176}
177
178#[doc(hidden)]
179#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
180pub struct DebugCpuAllocationStats {
181 pub scene_graph_node_count: usize,
182 pub scene_graph_heap_bytes: usize,
183 pub scene_hits_len: usize,
184 pub scene_hits_cap: usize,
185 pub scene_node_index_len: usize,
186 pub scene_node_index_cap: usize,
187 pub text_renderer_pool_len: usize,
188 pub text_renderer_pool_cap: usize,
189 pub image_texture_cache_len: usize,
190 pub image_texture_cache_cap: usize,
191 pub run_arena_staging_bytes: usize,
192 pub run_store_bytes: usize,
193 pub run_store_runs: usize,
194 pub scratch_image_vertices_cap: usize,
195 pub scratch_image_indices_cap: usize,
196 pub scratch_image_cmds_cap: usize,
197 pub scratch_glyph_instances_cap: usize,
198 pub layer_cache_len: usize,
199 pub layer_cache_bytes: u64,
200}
201
202struct GpuTarget {
204 device: Arc<wgpu::Device>,
205 queue: Arc<wgpu::Queue>,
206 surface_format: wgpu::TextureFormat,
207 adapter_backend: wgpu::Backend,
208 adapter_downlevel: wgpu::DownlevelFlags,
209}
210
211pub(crate) struct TextSystemState {
212 measurer: SoftwareTextMeasurer,
213}
214
215impl TextSystemState {
216 fn from_font_set(fonts: SoftwareTextFontSet) -> Self {
217 Self {
218 measurer: SoftwareTextMeasurer::from_font_set(fonts, 8192),
219 }
220 }
221
222 pub(crate) fn text_cache_len(&self) -> usize {
223 0
224 }
225}
226
227impl pipeline::TextLayoutResolver for TextSystemState {
228 fn layout_text(
229 &mut self,
230 text: &cranpose_ui::text::AnnotatedString,
231 style: &cranpose_ui::text::TextStyle,
232 ) -> cranpose_ui::text_layout_result::TextLayoutResult {
233 if cranpose_ui::has_current_app_context() {
234 cranpose_ui::text::layout_text(text, style)
235 } else {
236 self.measurer.layout(text, style)
237 }
238 }
239}
240
241#[derive(Clone)]
242pub struct WgpuTextSystem {
243 software_fonts: SoftwareTextFontSet,
244}
245
246impl WgpuTextSystem {
247 pub fn from_fonts(fonts: &[&'static [u8]]) -> Self {
248 Self {
249 software_fonts: software_text_font_set_from_fonts_or_default(fonts),
250 }
251 }
252
253 pub fn from_font_set(software_fonts: SoftwareTextFontSet) -> Self {
257 Self { software_fonts }
258 }
259
260 pub(crate) fn render_state(&self) -> TextSystemState {
261 TextSystemState::from_font_set(self.software_fonts.clone())
262 }
263
264 pub(crate) fn software_fonts(&self) -> SoftwareTextFontSet {
265 self.software_fonts.clone()
266 }
267}
268
269pub fn headless_text_measurer() -> Rc<dyn TextMeasurer> {
271 headless_text_measurer_with_fonts(&[])
272}
273
274pub fn headless_text_measurer_with_fonts(fonts: &[&'static [u8]]) -> Rc<dyn TextMeasurer> {
276 Rc::new(SoftwareTextMeasurer::from_fonts_or_default(fonts, 8192))
277}
278
279enum PresentBackend {
280 None,
281 Sync(Box<GpuRenderer>),
282 #[cfg(not(target_arch = "wasm32"))]
283 Threaded(PresentHandle),
284}
285
286#[derive(Clone, Copy, Debug, PartialEq, Eq)]
288pub enum PublishOutcome {
289 NoGraph,
291 NoCredit,
294 Published,
296}
297
298pub struct WgpuRenderer {
306 frontend: RendererFrontend,
307 backend: PresentBackend,
308 renderer_epoch: u64,
309 surface_epoch: u64,
310}
311
312impl WgpuRenderer {
313 pub fn new(fonts: &[&'static [u8]]) -> Self {
320 Self::with_text_system(WgpuTextSystem::from_fonts(fonts))
321 }
322
323 pub fn with_font_set(fonts: SoftwareTextFontSet) -> Self {
328 Self::with_text_system(WgpuTextSystem::from_font_set(fonts))
329 }
330
331 pub fn with_text_system(text_system: WgpuTextSystem) -> Self {
332 Self {
333 frontend: RendererFrontend::new(
334 text_system.render_state(),
335 text_system.software_fonts(),
336 ),
337 backend: PresentBackend::None,
338 renderer_epoch: 0,
339 surface_epoch: 0,
340 }
341 }
342
343 fn sync_gpu_renderer(&self) -> Option<&GpuRenderer> {
344 match &self.backend {
345 PresentBackend::Sync(gpu_renderer) => Some(gpu_renderer.as_ref()),
346 _ => None,
347 }
348 }
349
350 #[cfg(not(target_arch = "wasm32"))]
351 fn present_handle_mut(&mut self) -> Option<&mut PresentHandle> {
352 match &mut self.backend {
353 PresentBackend::Threaded(handle) => Some(handle),
354 _ => None,
355 }
356 }
357
358 fn retire_live_backend(&mut self) {
359 #[allow(unused_mut)]
360 let mut backend = std::mem::replace(&mut self.backend, PresentBackend::None);
361 #[cfg(not(target_arch = "wasm32"))]
362 if let PresentBackend::Threaded(handle) = &mut backend {
363 while let Some(returns) = handle.try_drain() {
364 self.frontend.apply_returns(returns);
365 }
366 handle.shutdown();
367 }
368 drop(backend);
369 }
370
371 pub fn init_gpu(
377 &mut self,
378 device: Arc<wgpu::Device>,
379 queue: Arc<wgpu::Queue>,
380 surface_format: wgpu::TextureFormat,
381 adapter_backend: wgpu::Backend,
382 adapter_downlevel: wgpu::DownlevelFlags,
383 ) {
384 let target = GpuTarget {
385 device,
386 queue,
387 surface_format,
388 adapter_backend,
389 adapter_downlevel,
390 };
391 self.install_gpu(target, PipelineCompilation::Background);
392 }
393
394 #[doc(hidden)]
398 pub fn init_gpu_compiling_inline_for_tests(
399 &mut self,
400 device: Arc<wgpu::Device>,
401 queue: Arc<wgpu::Queue>,
402 surface_format: wgpu::TextureFormat,
403 adapter_backend: wgpu::Backend,
404 adapter_downlevel: wgpu::DownlevelFlags,
405 ) {
406 let target = GpuTarget {
407 device,
408 queue,
409 surface_format,
410 adapter_backend,
411 adapter_downlevel,
412 };
413 self.install_gpu(target, PipelineCompilation::Inline);
414 }
415
416 fn install_gpu(&mut self, target: GpuTarget, pipeline_compilation: PipelineCompilation) {
417 let init = self.next_gpu_init(target, pipeline_compilation);
418 self.backend = PresentBackend::Sync(Box::new(GpuRenderer::new(init)));
419 }
420
421 fn next_gpu_init(
424 &mut self,
425 target: GpuTarget,
426 pipeline_compilation: PipelineCompilation,
427 ) -> GpuRendererInit {
428 self.retire_live_backend();
429 self.renderer_epoch = self.renderer_epoch.wrapping_add(1);
430 GpuRendererInit {
431 device: target.device,
432 queue: target.queue,
433 surface_format: target.surface_format,
434 adapter_backend: target.adapter_backend,
435 adapter_downlevel: target.adapter_downlevel,
436 text_fonts: self.frontend.text_fonts.clone(),
437 renderer_epoch: self.renderer_epoch,
438 pipeline_compilation,
439 }
440 }
441
442 #[cfg(not(target_arch = "wasm32"))]
459 #[expect(clippy::too_many_arguments)]
460 pub fn init_gpu_threaded(
461 &mut self,
462 device: Arc<wgpu::Device>,
463 queue: Arc<wgpu::Queue>,
464 surface_format: wgpu::TextureFormat,
465 adapter_backend: wgpu::Backend,
466 adapter_downlevel: wgpu::DownlevelFlags,
467 waker: Arc<dyn Fn() + Send + Sync>,
468 clock: Option<Arc<dyn Fn() -> i64 + Send + Sync>>,
469 observer: Option<Box<dyn PresentObserver>>,
470 ) -> Result<(), WgpuRendererError> {
471 let target = GpuTarget {
472 device,
473 queue,
474 surface_format,
475 adapter_backend,
476 adapter_downlevel,
477 };
478 let init = PresentRuntimeInit {
479 gpu: self.next_gpu_init(target, PipelineCompilation::Background),
480 clock,
481 observer,
482 };
483 let handle = PresentHandle::spawn(init, waker).map_err(WgpuRendererError::Wgpu)?;
484 self.backend = PresentBackend::Threaded(handle);
485 Ok(())
486 }
487
488 #[cfg(not(target_arch = "wasm32"))]
489 #[doc(hidden)]
490 pub fn init_gpu_inline_for_tests(
491 &mut self,
492 device: Arc<wgpu::Device>,
493 queue: Arc<wgpu::Queue>,
494 surface_format: wgpu::TextureFormat,
495 adapter_backend: wgpu::Backend,
496 adapter_downlevel: wgpu::DownlevelFlags,
497 ) -> InlinePresentRuntime {
498 let target = GpuTarget {
499 device,
500 queue,
501 surface_format,
502 adapter_backend,
503 adapter_downlevel,
504 };
505 let init = PresentRuntimeInit {
506 gpu: self.next_gpu_init(target, PipelineCompilation::Background),
507 clock: None,
508 observer: None,
509 };
510 let (handle, state, msg_rx) = PresentHandle::new_inline(init, Arc::new(|| {}));
511 self.backend = PresentBackend::Threaded(handle);
512 InlinePresentRuntime {
513 state,
514 msg_rx,
515 shutdown_seen: false,
516 }
517 }
518
519 pub fn note_surface_reconfigured(&mut self) {
524 self.surface_epoch = self.surface_epoch.wrapping_add(1);
525 }
526
527 pub fn set_transparent_background(&mut self, transparent: bool) {
532 self.frontend.transparent_background = transparent;
533 }
534
535 pub fn root_scale(&self) -> f32 {
536 self.frontend.root_scale
537 }
538
539 pub fn render(
546 &mut self,
547 texture: &wgpu::Texture,
548 view: &wgpu::TextureView,
549 width: u32,
550 height: u32,
551 ) -> Result<(), WgpuRendererError> {
552 self.render_frame(texture, view, width, height)
553 }
554
555 pub fn render_surface_texture(
559 &mut self,
560 texture: &wgpu::Texture,
561 view: &wgpu::TextureView,
562 width: u32,
563 height: u32,
564 ) -> Result<(), WgpuRendererError> {
565 self.render_frame(texture, view, width, height)
566 }
567
568 pub fn present(&self, frame: wgpu::SurfaceTexture) {
575 match self.sync_gpu_renderer() {
576 Some(gpu_renderer) => gpu_renderer.queue.present(frame),
577 None => log::debug!("surface image released: the renderer has no GPU queue"),
578 }
579 }
580
581 fn render_frame(
582 &mut self,
583 texture: &wgpu::Texture,
584 view: &wgpu::TextureView,
585 width: u32,
586 height: u32,
587 ) -> Result<(), WgpuRendererError> {
588 let PresentBackend::Sync(gpu_renderer) = &mut self.backend else {
589 return Err(WgpuRendererError::Wgpu(
590 "GPU renderer not initialized for synchronous rendering. Call init_gpu() first."
591 .to_string(),
592 ));
593 };
594 let packet = self
595 .frontend
596 .build_frame_packet(width, height, self.renderer_epoch, self.surface_epoch)
597 .ok_or_else(|| WgpuRendererError::Wgpu("scene graph is missing".to_string()))?;
598 let mut returns = RenderReturns::default();
599 let result = gpu_renderer.render(
600 texture,
601 view,
602 width,
603 height,
604 packet,
605 self.surface_epoch,
606 &mut returns,
607 );
608 self.frontend.apply_returns(returns);
609 result.map_err(WgpuRendererError::Wgpu)
610 }
611
612 pub fn capture_frame(
616 &mut self,
617 width: u32,
618 height: u32,
619 ) -> Result<CapturedFrame, WgpuRendererError> {
620 self.capture_frame_with_scale(width, height, self.frontend.root_scale)
621 }
622
623 #[cfg(not(target_arch = "wasm32"))]
632 pub fn prepare_recorded_pipelines(
633 &mut self,
634 timeout: std::time::Duration,
635 stop: &dyn Fn() -> bool,
636 ) -> bool {
637 let PresentBackend::Sync(gpu_renderer) = &mut self.backend else {
638 return false;
639 };
640 let deadline = web_time::Instant::now() + timeout;
641 let mut compiling = gpu_renderer.prepare_recorded();
642 while compiling {
643 if web_time::Instant::now() >= deadline || stop() {
644 return false;
645 }
646 std::thread::sleep(std::time::Duration::from_millis(10));
647 compiling = gpu_renderer.compiling();
648 }
649 true
650 }
651
652 pub fn capture_frame_with_scale(
653 &mut self,
654 width: u32,
655 height: u32,
656 root_scale: f32,
657 ) -> Result<CapturedFrame, WgpuRendererError> {
658 let PresentBackend::Sync(gpu_renderer) = &mut self.backend else {
659 return Err(WgpuRendererError::Wgpu(
660 "GPU renderer not initialized for synchronous rendering. Call init_gpu() first."
661 .to_string(),
662 ));
663 };
664 let packet = self
665 .frontend
666 .build_frame_packet_with_scale(
667 width,
668 height,
669 root_scale,
670 self.renderer_epoch,
671 self.surface_epoch,
672 )
673 .ok_or_else(|| WgpuRendererError::Wgpu("scene graph is missing".to_string()))?;
674 let mut returns = RenderReturns::default();
675 let result = gpu_renderer.render_to_rgba_pixels(
676 width,
677 height,
678 packet,
679 self.surface_epoch,
680 &mut returns,
681 );
682 self.frontend.apply_returns(returns);
683 let pixels = result.map_err(WgpuRendererError::Wgpu)?;
684 Ok(CapturedFrame {
685 width,
686 height,
687 pixels,
688 })
689 }
690
691 #[cfg(not(target_arch = "wasm32"))]
696 pub fn has_frame_credit(&self) -> bool {
697 match &self.backend {
698 PresentBackend::Threaded(handle) => handle.has_credit(),
699 PresentBackend::Sync(_) | PresentBackend::None => true,
700 }
701 }
702
703 #[cfg(not(target_arch = "wasm32"))]
709 pub fn frame_credit_returned(&self) -> bool {
710 match &self.backend {
711 PresentBackend::Threaded(handle) => handle.has_undrained_return(),
712 PresentBackend::Sync(_) | PresentBackend::None => false,
713 }
714 }
715
716 #[cfg(not(target_arch = "wasm32"))]
722 pub fn publish_frame(&mut self, width: u32, height: u32) -> PublishOutcome {
723 let PresentBackend::Threaded(handle) = &mut self.backend else {
724 log::error!("publish_frame called without a threaded present runtime");
725 return PublishOutcome::NoCredit;
726 };
727 if !handle.has_credit() {
728 return PublishOutcome::NoCredit;
729 }
730 let Some(packet) = self.frontend.build_frame_packet(
731 width,
732 height,
733 self.renderer_epoch,
734 self.surface_epoch,
735 ) else {
736 return PublishOutcome::NoGraph;
737 };
738 match handle.publish(packet) {
739 Ok(()) => PublishOutcome::Published,
740 Err(packet) => {
741 let mut returns = RenderReturns::default();
742 let _ = GpuRenderer::cancel_packet(
743 *packet,
744 CancelReason::SurfaceUnavailable,
745 &mut returns,
746 );
747 self.frontend.apply_returns(returns);
748 log::error!("present runtime unavailable; frame recovered, not published");
749 PublishOutcome::NoCredit
750 }
751 }
752 }
753
754 #[cfg(not(target_arch = "wasm32"))]
758 pub fn drain_present_returns(&mut self) -> usize {
759 self.drain_present_returns_with(&mut |_, _, _| {})
760 }
761
762 #[cfg(not(target_arch = "wasm32"))]
766 pub fn drain_present_returns_with(
767 &mut self,
768 on_return: &mut dyn FnMut(u64, PresentOutcome, PresentTimings),
769 ) -> usize {
770 let mut drained = 0;
771 loop {
772 let returns = {
773 let PresentBackend::Threaded(handle) = &mut self.backend else {
774 break;
775 };
776 match handle.try_drain() {
777 Some(returns) => returns,
778 None => break,
779 }
780 };
781 drained += 1;
782 let frame_id = returns.frame_id;
783 let outcome = returns.outcome;
784 let timings = returns.timings;
785 self.frontend.apply_returns(returns);
786 on_return(frame_id, outcome, timings);
787 }
788 drained
789 }
790
791 #[cfg(not(target_arch = "wasm32"))]
800 pub fn present_replace_surface(
801 &mut self,
802 surface: wgpu::Surface<'static>,
803 config: wgpu::SurfaceConfiguration,
804 ) -> bool {
805 let surface_epoch = self.surface_epoch;
806 let Some(handle) = self.present_handle_mut() else {
807 log::error!("present_replace_surface called without a threaded present runtime");
808 return false;
809 };
810 let sent = handle.send_control(PresentControl::ReplaceSurface {
811 surface,
812 config,
813 surface_epoch,
814 });
815 if !sent {
816 log::error!("[present-runtime] replace surface: runtime is gone");
817 }
818 sent
819 }
820
821 #[cfg(not(target_arch = "wasm32"))]
825 pub fn present_reconfigure(&mut self, config: wgpu::SurfaceConfiguration) -> bool {
826 let surface_epoch = self.surface_epoch;
827 let Some(handle) = self.present_handle_mut() else {
828 log::error!("present_reconfigure called without a threaded present runtime");
829 return false;
830 };
831 handle.send_control_and_wait(
832 move |ack| PresentControl::Reconfigure {
833 config,
834 surface_epoch,
835 ack,
836 },
837 "reconfigure surface",
838 )
839 }
840
841 #[cfg(not(target_arch = "wasm32"))]
845 pub fn present_drop_surface(&mut self) -> bool {
846 let Some(handle) = self.present_handle_mut() else {
847 log::error!("present_drop_surface called without a threaded present runtime");
848 return false;
849 };
850 handle.send_control_and_wait(|ack| PresentControl::DropSurface { ack }, "drop surface")
851 }
852
853 #[cfg(not(target_arch = "wasm32"))]
856 pub fn shutdown_present_runtime(&mut self) {
857 if matches!(self.backend, PresentBackend::Threaded(_)) {
858 self.retire_live_backend();
859 }
860 }
861
862 #[cfg(not(target_arch = "wasm32"))]
863 #[doc(hidden)]
864 pub fn present_attach_offscreen_for_tests(&mut self, width: u32, height: u32) -> bool {
865 let Some(handle) = self.present_handle_mut() else {
866 return false;
867 };
868 handle.send_control_and_wait(
869 move |ack| PresentControl::AttachOffscreenTargetForTests { width, height, ack },
870 "attach offscreen target",
871 )
872 }
873
874 #[cfg(not(target_arch = "wasm32"))]
875 #[doc(hidden)]
876 pub fn send_attach_offscreen_unacked_for_tests(
877 &mut self,
878 width: u32,
879 height: u32,
880 ) -> Option<std::sync::mpsc::Receiver<()>> {
881 let handle = self.present_handle_mut()?;
882 handle.send_control_unacked(move |ack| PresentControl::AttachOffscreenTargetForTests {
883 width,
884 height,
885 ack,
886 })
887 }
888
889 #[cfg(not(target_arch = "wasm32"))]
890 #[doc(hidden)]
891 pub fn send_reconfigure_unacked_for_tests(
892 &mut self,
893 config: wgpu::SurfaceConfiguration,
894 ) -> Option<std::sync::mpsc::Receiver<()>> {
895 let surface_epoch = self.surface_epoch;
896 let handle = self.present_handle_mut()?;
897 handle.send_control_unacked(move |ack| PresentControl::Reconfigure {
898 config,
899 surface_epoch,
900 ack,
901 })
902 }
903
904 #[cfg(not(target_arch = "wasm32"))]
905 #[doc(hidden)]
906 pub fn send_drop_surface_unacked_for_tests(&mut self) -> Option<std::sync::mpsc::Receiver<()>> {
907 let handle = self.present_handle_mut()?;
908 handle.send_control_unacked(|ack| PresentControl::DropSurface { ack })
909 }
910
911 pub fn present_thread_id(&self) -> Option<i32> {
915 match &self.backend {
916 #[cfg(not(target_arch = "wasm32"))]
917 PresentBackend::Threaded(handle) => {
918 let id = handle
919 .status()
920 .thread_id
921 .load(std::sync::atomic::Ordering::Relaxed);
922 (id != 0).then_some(id)
923 }
924 _ => None,
925 }
926 }
927
928 pub fn last_published_frame_id(&self) -> u64 {
933 self.frontend.frame_sequence
934 }
935
936 #[cfg(not(target_arch = "wasm32"))]
937 #[doc(hidden)]
938 pub fn present_status_snapshot_for_tests(&self) -> Option<(bool, u64, u64)> {
939 match &self.backend {
940 PresentBackend::Threaded(handle) => {
941 let status = handle.status();
942 Some((
943 status
944 .needs_frame_warmup
945 .load(std::sync::atomic::Ordering::Relaxed),
946 status
947 .presented_frames
948 .load(std::sync::atomic::Ordering::Relaxed),
949 status
950 .placeholder_frames
951 .load(std::sync::atomic::Ordering::Relaxed),
952 ))
953 }
954 _ => None,
955 }
956 }
957
958 pub fn last_frame_stats(&self) -> Option<RenderStatsSnapshot> {
959 match &self.backend {
960 PresentBackend::Sync(gpu_renderer) => gpu_renderer.last_frame_stats(),
961 #[cfg(not(target_arch = "wasm32"))]
962 PresentBackend::Threaded(handle) => *handle
963 .status()
964 .last_frame_stats
965 .lock()
966 .unwrap_or_else(std::sync::PoisonError::into_inner),
967 PresentBackend::None => None,
968 }
969 }
970
971 pub fn first_screen_pipelines(&self) -> Option<Vec<u8>> {
977 self.sync_gpu_renderer()
978 .and_then(GpuRenderer::first_screen_pipelines)
979 }
980
981 pub fn build_first_screen_pipelines(&mut self, records: &[u8]) {
989 if let PresentBackend::Sync(gpu_renderer) = &mut self.backend {
990 gpu_renderer.build_first_screen(records);
991 }
992 }
993
994 pub fn gpu_pass_timings(&self) -> GpuPassTimingReport {
998 self.sync_gpu_renderer()
999 .map(GpuRenderer::gpu_pass_timings)
1000 .unwrap_or_default()
1001 }
1002
1003 pub fn debug_cpu_allocation_stats(&self) -> DebugCpuAllocationStats {
1004 let mut stats = self
1005 .sync_gpu_renderer()
1006 .map(GpuRenderer::debug_cpu_allocation_stats)
1007 .unwrap_or_default();
1008 stats.scene_graph_node_count = self
1009 .frontend
1010 .scene
1011 .graph
1012 .as_ref()
1013 .map_or(0, RenderGraph::node_count);
1014 stats.scene_graph_heap_bytes = self
1015 .frontend
1016 .scene
1017 .graph
1018 .as_ref()
1019 .map_or(0, RenderGraph::heap_bytes);
1020 stats.scene_hits_len = self.frontend.scene.hits.len();
1021 stats.scene_hits_cap = self.frontend.scene.hits.capacity();
1022 stats.scene_node_index_len = self.frontend.scene.node_index.len();
1023 stats.scene_node_index_cap = self.frontend.scene.node_index.capacity();
1024 stats
1025 }
1026
1027 #[cfg(target_arch = "wasm32")]
1033 pub fn begin_discovery_frame(&self) {
1034 if let Some(gpu_renderer) = self.sync_gpu_renderer() {
1035 gpu_renderer.pipeline_compiler().begin_discovery_frame();
1036 }
1037 }
1038
1039 #[cfg(target_arch = "wasm32")]
1044 pub fn background_pipelines_landed(&mut self) {
1045 if let PresentBackend::Sync(gpu_renderer) = &mut self.backend {
1046 gpu_renderer.background_pipelines_landed();
1047 }
1048 }
1049
1050 pub fn try_device(&self) -> Option<&wgpu::Device> {
1054 self.sync_gpu_renderer().map(|r| &*r.device)
1055 }
1056
1057 #[doc(hidden)]
1058 pub fn try_queue_for_tests(&self) -> Option<&wgpu::Queue> {
1059 self.sync_gpu_renderer().map(|r| &*r.queue)
1060 }
1061
1062 #[doc(hidden)]
1063 pub fn device_error_count_for_tests(&self) -> u64 {
1064 self.sync_gpu_renderer()
1065 .map_or(0, GpuRenderer::device_error_count)
1066 }
1067
1068 #[doc(hidden)]
1069 pub fn build_frame_packet_for_tests(
1070 &mut self,
1071 width: u32,
1072 height: u32,
1073 ) -> Option<HeldFramePacket> {
1074 self.frontend
1075 .build_frame_packet(width, height, self.renderer_epoch, self.surface_epoch)
1076 .map(HeldFramePacket)
1077 }
1078
1079 #[doc(hidden)]
1091 pub fn return_held_packet_for_tests(&mut self, packet: HeldFramePacket) {
1092 let packet = packet.0;
1093 self.frontend.apply_returns(RenderReturns {
1094 scene: Some(frame_packet::FrameSceneStorage {
1095 root: packet.root,
1096 overlay: packet.overlay,
1097 }),
1098 frame_id: packet.frame_id,
1099 ..RenderReturns::default()
1100 });
1101 }
1102
1103 #[doc(hidden)]
1104 pub fn render_held_packet_for_tests(
1105 &mut self,
1106 texture: &wgpu::Texture,
1107 view: &wgpu::TextureView,
1108 width: u32,
1109 height: u32,
1110 packet: HeldFramePacket,
1111 ) -> Result<PresentOutcome, WgpuRendererError> {
1112 let PresentBackend::Sync(gpu_renderer) = &mut self.backend else {
1113 return Err(WgpuRendererError::Wgpu(
1114 "GPU renderer not initialized for synchronous rendering. Call init_gpu() first."
1115 .to_string(),
1116 ));
1117 };
1118 let mut returns = RenderReturns::default();
1119 let result = gpu_renderer.render(
1120 texture,
1121 view,
1122 width,
1123 height,
1124 packet.0,
1125 self.surface_epoch,
1126 &mut returns,
1127 );
1128 let outcome = returns.outcome;
1129 self.frontend.apply_returns(returns);
1130 result.map_err(WgpuRendererError::Wgpu)?;
1131 Ok(outcome)
1132 }
1133}
1134
1135#[doc(hidden)]
1136pub struct HeldFramePacket(frame_packet::FramePacket);
1137
1138#[cfg(not(target_arch = "wasm32"))]
1139#[doc(hidden)]
1140pub struct InlinePresentRuntime {
1141 state: PresentState,
1142 msg_rx: std::sync::mpsc::Receiver<PresentMsg>,
1143 shutdown_seen: bool,
1144}
1145
1146#[cfg(not(target_arch = "wasm32"))]
1147impl InlinePresentRuntime {
1148 pub fn pump(&mut self) -> bool {
1149 if self.shutdown_seen {
1150 return false;
1151 }
1152 while let Ok(msg) = self.msg_rx.try_recv() {
1153 if !self.state.run_once(msg) {
1154 self.shutdown_seen = true;
1155 return false;
1156 }
1157 }
1158 self.state.consume_waiting();
1159 true
1160 }
1161
1162 pub fn has_waiting_packet(&self) -> bool {
1163 self.state.has_waiting_packet()
1164 }
1165
1166 pub fn step_one_message(&mut self) -> bool {
1167 if self.shutdown_seen {
1168 return false;
1169 }
1170 match self.msg_rx.try_recv() {
1171 Ok(msg) => {
1172 if !self.state.run_once(msg) {
1173 self.shutdown_seen = true;
1174 }
1175 true
1176 }
1177 Err(_) => false,
1178 }
1179 }
1180
1181 pub fn consume_waiting(&mut self) {
1182 self.state.consume_waiting();
1183 }
1184}
1185
1186impl Default for WgpuRenderer {
1187 fn default() -> Self {
1188 Self::new(&[])
1189 }
1190}
1191
1192fn install_text_measurer(app_context: &cranpose_ui::AppContext, fonts: &SoftwareTextFontSet) {
1194 app_context.set_text_measurer(SoftwareTextMeasurer::from_font_set(fonts.clone(), 8192));
1195}
1196
1197impl Renderer for WgpuRenderer {
1198 type Scene = Scene;
1199 type Error = WgpuRendererError;
1200
1201 fn attach_app_context_services(&mut self, app_context: &cranpose_ui::AppContext) {
1202 install_text_measurer(app_context, &self.frontend.text_fonts);
1203 self.frontend.app_context = Some(app_context.downgrade());
1204 }
1205
1206 #[cfg(feature = "installed-fonts")]
1207 fn apply_installed_fonts(&mut self, app_context: &cranpose_ui::AppContext) {
1208 let Some(fonts) = self.frontend.text_fonts.with_installed_faces() else {
1209 return;
1210 };
1211 let attached = self
1212 .frontend
1213 .app_context
1214 .as_ref()
1215 .and_then(std::rc::Weak::upgrade)
1216 .is_some_and(|attached| std::ptr::eq(&*attached, app_context));
1217 if attached {
1218 install_text_measurer(app_context, &fonts);
1219 }
1220 self.frontend.text_state = TextSystemState::from_font_set(fonts.clone());
1221 self.frontend.text_fonts = fonts;
1222 }
1223
1224 fn set_root_scale(&mut self, scale: f32) {
1225 self.frontend.root_scale = scale;
1226 }
1227
1228 fn scene(&self) -> &Self::Scene {
1229 &self.frontend.scene
1230 }
1231
1232 fn scene_mut(&mut self) -> &mut Self::Scene {
1233 &mut self.frontend.scene
1234 }
1235
1236 fn rebuild_scene(
1237 &mut self,
1238 layout_tree: &LayoutTree,
1239 _viewport: Size,
1240 ) -> Result<(), Self::Error> {
1241 self.frontend.scene.clear();
1242 self.frontend.clear_fps_overlay();
1243 pipeline::render_layout_tree(layout_tree.root(), &mut self.frontend.scene);
1244 Ok(())
1245 }
1246
1247 fn rebuild_scene_from_applier(
1248 &mut self,
1249 applier: &mut MemoryApplier,
1250 root: NodeId,
1251 _viewport: Size,
1252 ) -> Result<(), Self::Error> {
1253 self.frontend.clear_fps_overlay();
1254 self.frontend.scene.rebuild_from_applier(applier, root);
1255 Ok(())
1256 }
1257
1258 fn update_scene_from_applier(
1259 &mut self,
1260 applier: &mut MemoryApplier,
1261 root: NodeId,
1262 _viewport: Size,
1263 updates: cranpose_render_common::SceneUpdates<'_>,
1264 ) -> Result<(), Self::Error> {
1265 if updates.is_empty() {
1266 self.frontend.clear_fps_overlay();
1267 }
1268 self.frontend
1269 .scene
1270 .update_from_applier(applier, root, updates, true);
1271 Ok(())
1272 }
1273
1274 fn update_visual_scene_from_applier(
1275 &mut self,
1276 applier: &mut MemoryApplier,
1277 root: NodeId,
1278 _viewport: Size,
1279 updates: cranpose_render_common::SceneUpdates<'_>,
1280 ) -> Result<(), Self::Error> {
1281 if updates.is_empty() {
1282 self.frontend.clear_fps_overlay();
1283 }
1284 self.frontend
1285 .scene
1286 .update_from_applier(applier, root, updates, false);
1287 Ok(())
1288 }
1289
1290 fn draw_dev_overlay(&mut self, text: &str, viewport: Size) {
1291 const DEV_OVERLAY_NODE_ID: NodeId = NodeId::MAX;
1292 let key = cranpose_render_common::dev_overlay::DevOverlayKey::new(text, viewport);
1293 if self.frontend.dev_overlay_graph.is_some()
1294 && self
1295 .frontend
1296 .dev_overlay_cache
1297 .as_ref()
1298 .is_some_and(|cache| {
1299 cache.text == key.text
1300 && cache.viewport_width_bits == key.viewport_width_bits
1301 && cache.viewport_height_bits == key.viewport_height_bits
1302 })
1303 {
1304 return;
1305 }
1306 self.frontend.fps_overlay_graph = Some(
1307 cranpose_render_common::dev_overlay::build_dev_overlay_graph(
1308 text,
1309 viewport,
1310 DEV_OVERLAY_NODE_ID,
1311 ),
1312 );
1313 self.frontend.dev_overlay_cache = Some(DevOverlayCache {
1314 text: key.text,
1315 viewport_width_bits: key.viewport_width_bits,
1316 viewport_height_bits: key.viewport_height_bits,
1317 });
1318 self.frontend.refresh_dev_overlay();
1319 }
1320
1321 fn set_inspector_overlay(&mut self, graph: Option<cranpose_render_common::graph::RenderGraph>) {
1322 self.frontend.inspector_overlay_graph = graph;
1323 self.frontend.refresh_dev_overlay();
1324 }
1325
1326 fn needs_frame_warmup(&self) -> bool {
1327 match &self.backend {
1328 PresentBackend::Sync(gpu_renderer) => gpu_renderer.needs_frame_warmup(),
1329 #[cfg(not(target_arch = "wasm32"))]
1330 PresentBackend::Threaded(handle) => handle
1331 .status()
1332 .needs_frame_warmup
1333 .load(std::sync::atomic::Ordering::Relaxed),
1334 PresentBackend::None => false,
1335 }
1336 }
1337
1338 fn awaits_pipelines(&self) -> bool {
1339 match &self.backend {
1340 PresentBackend::Sync(gpu_renderer) => gpu_renderer.awaits_pipelines(),
1341 #[cfg(not(target_arch = "wasm32"))]
1342 PresentBackend::Threaded(handle) => {
1343 let status = handle.status();
1344 status
1345 .drew_placeholder
1346 .load(std::sync::atomic::Ordering::Relaxed)
1347 && status
1348 .landing
1349 .get()
1350 .is_some_and(|landing| landing.in_flight())
1351 }
1352 PresentBackend::None => false,
1353 }
1354 }
1355}
1356
1357#[cfg(test)]
1358#[path = "tests/wgpu_tests.rs"]
1359mod tests;