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 mesh_fallback;
41mod offscreen;
42mod opaque_prefix;
43mod output_conversion;
44pub(crate) mod pass_timing;
45mod pipeline;
46mod pipeline_compiler;
47#[cfg(not(target_arch = "wasm32"))]
48pub mod pipeline_disk_cache;
49mod pipeline_recorder;
50mod pipeline_records;
51#[cfg(not(target_arch = "wasm32"))]
52mod present_runtime;
53mod record_columns;
54mod render;
55mod rrect_shadow;
56mod run_geometry;
57mod run_store;
58mod scene;
59mod shader_cache;
60#[cfg(not(target_arch = "wasm32"))]
61mod shader_flatten;
62#[cfg(not(target_arch = "wasm32"))]
63mod shader_fold;
64mod shaders;
65mod shape_pipelines;
66mod shared_shader;
67#[cfg(test)]
68mod test_support;
69mod thread_state;
70
71use std::{rc::Rc, sync::Arc};
72
73use cranpose_core::{MemoryApplier, NodeId};
74use cranpose_render_common::{
75 RenderScene, Renderer,
76 graph::RenderGraph,
77 software_text_raster::{
78 SoftwareTextFontSet, SoftwareTextMeasurer, software_text_font_set_from_fonts_or_default,
79 },
80};
81use cranpose_ui::{LayoutTree, TextMeasurer};
82use cranpose_ui_graphics::{Rect, Size};
83pub use frame_packet::PresentTimings;
84
85#[cfg(not(target_arch = "wasm32"))]
89pub trait PresentObserver: Send {
90 fn before_present(&mut self, surface: &wgpu::Surface<'static>);
92 fn after_present(&mut self, surface: &wgpu::Surface<'static>);
94
95 fn before_reconfigure(&mut self, _surface: &wgpu::Surface<'static>) {}
98}
99use frame_packet::RenderReturns;
100#[doc(hidden)]
101pub use frame_packet::{CancelReason, PresentOutcome};
102use frontend::{DevOverlayCache, RendererFrontend};
103pub use gpu_stats::FrameStatsSnapshot as RenderStatsSnapshot;
104pub use initial_present::{clear_to_background, clear_to_default_background};
105pub use pass_timing::{GpuPassTimingEntry, GpuPassTimingReport};
106use pipeline_compiler::PipelineCompilation;
107#[cfg(not(target_arch = "wasm32"))]
108use present_runtime::{
109 PresentControl, PresentHandle, PresentMsg, PresentRuntimeInit, PresentState,
110};
111use render::{GpuRenderer, GpuRendererInit};
112pub use render::{
113 frame_clear_color, frames_presented, pipelines_created, pipelines_created_off_frame,
114};
115pub use scene::{HitRegion, Scene};
116pub use shared_shader::{shader_stages_flattened, shader_stages_folded};
117
118pub fn optional_device_features(adapter: &wgpu::Adapter) -> wgpu::Features {
127 let mut wanted = wgpu::Features::PIPELINE_CACHE | wgpu::Features::TIMESTAMP_QUERY;
128 if matches!(
129 adapter.get_info().device_type,
130 wgpu::DeviceType::IntegratedGpu | wgpu::DeviceType::Cpu
131 ) {
132 wanted |= wgpu::Features::MAPPABLE_PRIMARY_BUFFERS;
133 }
134 adapter.features() & wanted
135}
136
137#[doc(hidden)]
138pub fn offscreen_render_target_for_tests(
139 device: &wgpu::Device,
140 width: u32,
141 height: u32,
142 label: &str,
143) -> (wgpu::Texture, wgpu::TextureView) {
144 let texture = offscreen::create_2d_texture(
145 device,
146 wgpu::TextureFormat::Rgba8Unorm,
147 width,
148 height,
149 wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
150 Some(label),
151 );
152 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
153 (texture, view)
154}
155
156pub(crate) fn rect_to_quad(rect: Rect) -> [[f32; 2]; 4] {
157 [
158 [rect.x, rect.y],
159 [rect.x + rect.width, rect.y],
160 [rect.x, rect.y + rect.height],
161 [rect.x + rect.width, rect.y + rect.height],
162 ]
163}
164
165#[derive(Debug)]
166pub enum WgpuRendererError {
167 Layout(String),
168 Wgpu(String),
169}
170
171#[derive(Debug, Clone)]
173pub struct CapturedFrame {
174 pub width: u32,
175 pub height: u32,
176 pub pixels: Vec<u8>,
177}
178
179#[doc(hidden)]
180#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
181pub struct DebugCpuAllocationStats {
182 pub scene_graph_node_count: usize,
183 pub scene_graph_heap_bytes: usize,
184 pub scene_hits_len: usize,
185 pub scene_hits_cap: usize,
186 pub scene_node_index_len: usize,
187 pub scene_node_index_cap: usize,
188 pub text_renderer_pool_len: usize,
189 pub text_renderer_pool_cap: usize,
190 pub image_texture_cache_len: usize,
191 pub image_texture_cache_cap: usize,
192 pub run_arena_staging_bytes: usize,
193 pub run_store_bytes: usize,
194 pub run_store_runs: usize,
195 pub scratch_image_vertices_cap: usize,
196 pub scratch_image_indices_cap: usize,
197 pub scratch_image_cmds_cap: usize,
198 pub scratch_glyph_instances_cap: usize,
199 pub layer_cache_len: usize,
200 pub layer_cache_bytes: u64,
201}
202
203struct GpuTarget {
205 device: Arc<wgpu::Device>,
206 queue: Arc<wgpu::Queue>,
207 surface_format: wgpu::TextureFormat,
208 adapter_backend: wgpu::Backend,
209 adapter_downlevel: wgpu::DownlevelFlags,
210}
211
212pub(crate) struct TextSystemState {
213 measurer: SoftwareTextMeasurer,
214}
215
216impl TextSystemState {
217 fn from_font_set(fonts: SoftwareTextFontSet) -> Self {
218 Self {
219 measurer: SoftwareTextMeasurer::from_font_set(fonts, 8192),
220 }
221 }
222
223 pub(crate) fn text_cache_len(&self) -> usize {
224 0
225 }
226}
227
228impl pipeline::TextLayoutResolver for TextSystemState {
229 fn layout_text(
230 &mut self,
231 text: &cranpose_ui::text::AnnotatedString,
232 style: &cranpose_ui::text::TextStyle,
233 ) -> cranpose_ui::text_layout_result::TextLayoutResult {
234 if cranpose_ui::has_current_app_context() {
235 cranpose_ui::text::layout_text(text, style)
236 } else {
237 self.measurer.layout(text, style)
238 }
239 }
240}
241
242#[derive(Clone)]
243pub struct WgpuTextSystem {
244 software_fonts: SoftwareTextFontSet,
245}
246
247impl WgpuTextSystem {
248 pub fn from_fonts(fonts: &[&'static [u8]]) -> Self {
249 Self {
250 software_fonts: software_text_font_set_from_fonts_or_default(fonts),
251 }
252 }
253
254 pub fn from_font_set(software_fonts: SoftwareTextFontSet) -> Self {
258 Self { software_fonts }
259 }
260
261 pub(crate) fn render_state(&self) -> TextSystemState {
262 TextSystemState::from_font_set(self.software_fonts.clone())
263 }
264
265 pub(crate) fn software_fonts(&self) -> SoftwareTextFontSet {
266 self.software_fonts.clone()
267 }
268}
269
270pub fn headless_text_measurer() -> Rc<dyn TextMeasurer> {
272 headless_text_measurer_with_fonts(&[])
273}
274
275pub fn headless_text_measurer_with_fonts(fonts: &[&'static [u8]]) -> Rc<dyn TextMeasurer> {
277 Rc::new(SoftwareTextMeasurer::from_fonts_or_default(fonts, 8192))
278}
279
280enum PresentBackend {
281 None,
282 Sync(Box<GpuRenderer>),
283 #[cfg(not(target_arch = "wasm32"))]
284 Threaded(PresentHandle),
285}
286
287#[derive(Clone, Copy, Debug, PartialEq, Eq)]
289pub enum PublishOutcome {
290 NoGraph,
292 NoCredit,
295 Published,
297}
298
299pub struct WgpuRenderer {
307 frontend: RendererFrontend,
308 backend: PresentBackend,
309 renderer_epoch: u64,
310 surface_epoch: u64,
311 on_device_lost: Option<DeviceLostHandler>,
312}
313
314pub type DeviceLostHandler = Arc<dyn Fn() + Send + Sync>;
317
318impl WgpuRenderer {
319 pub fn new(fonts: &[&'static [u8]]) -> Self {
326 Self::with_text_system(WgpuTextSystem::from_fonts(fonts))
327 }
328
329 pub fn with_font_set(fonts: SoftwareTextFontSet) -> Self {
334 Self::with_text_system(WgpuTextSystem::from_font_set(fonts))
335 }
336
337 pub fn with_text_system(text_system: WgpuTextSystem) -> Self {
338 Self {
339 frontend: RendererFrontend::new(
340 text_system.render_state(),
341 text_system.software_fonts(),
342 ),
343 backend: PresentBackend::None,
344 renderer_epoch: 0,
345 surface_epoch: 0,
346 on_device_lost: None,
347 }
348 }
349
350 pub fn set_device_lost_handler(&mut self, handler: DeviceLostHandler) {
353 self.on_device_lost = Some(handler);
354 }
355
356 fn sync_gpu_renderer(&self) -> Option<&GpuRenderer> {
357 match &self.backend {
358 PresentBackend::Sync(gpu_renderer) => Some(gpu_renderer.as_ref()),
359 _ => None,
360 }
361 }
362
363 #[cfg(not(target_arch = "wasm32"))]
364 fn present_handle_mut(&mut self) -> Option<&mut PresentHandle> {
365 match &mut self.backend {
366 PresentBackend::Threaded(handle) => Some(handle),
367 _ => None,
368 }
369 }
370
371 fn retire_live_backend(&mut self) {
372 #[allow(unused_mut)]
373 let mut backend = std::mem::replace(&mut self.backend, PresentBackend::None);
374 #[cfg(not(target_arch = "wasm32"))]
375 if let PresentBackend::Threaded(handle) = &mut backend {
376 while let Some(returns) = handle.try_drain() {
377 self.frontend.apply_returns(returns);
378 }
379 handle.shutdown();
380 }
381 drop(backend);
382 }
383
384 pub fn init_gpu(
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 ) {
397 let target = GpuTarget {
398 device,
399 queue,
400 surface_format,
401 adapter_backend,
402 adapter_downlevel,
403 };
404 self.install_gpu(target, PipelineCompilation::Background);
405 }
406
407 #[doc(hidden)]
411 pub fn init_gpu_compiling_inline_for_tests(
412 &mut self,
413 device: Arc<wgpu::Device>,
414 queue: Arc<wgpu::Queue>,
415 surface_format: wgpu::TextureFormat,
416 adapter_backend: wgpu::Backend,
417 adapter_downlevel: wgpu::DownlevelFlags,
418 ) {
419 let target = GpuTarget {
420 device,
421 queue,
422 surface_format,
423 adapter_backend,
424 adapter_downlevel,
425 };
426 self.install_gpu(target, PipelineCompilation::Inline);
427 }
428
429 fn install_gpu(&mut self, target: GpuTarget, pipeline_compilation: PipelineCompilation) {
430 let init = self.next_gpu_init(target, pipeline_compilation);
431 self.backend = PresentBackend::Sync(Box::new(GpuRenderer::new(init)));
432 }
433
434 fn next_gpu_init(
437 &mut self,
438 target: GpuTarget,
439 pipeline_compilation: PipelineCompilation,
440 ) -> GpuRendererInit {
441 self.retire_live_backend();
442 self.renderer_epoch = self.renderer_epoch.wrapping_add(1);
443 GpuRendererInit {
444 device: target.device,
445 queue: target.queue,
446 surface_format: target.surface_format,
447 adapter_backend: target.adapter_backend,
448 adapter_downlevel: target.adapter_downlevel,
449 text_fonts: self.frontend.text_fonts.clone(),
450 renderer_epoch: self.renderer_epoch,
451 pipeline_compilation,
452 on_device_lost: self.on_device_lost.clone(),
453 }
454 }
455
456 #[cfg(not(target_arch = "wasm32"))]
473 #[expect(clippy::too_many_arguments)]
474 pub fn init_gpu_threaded(
475 &mut self,
476 device: Arc<wgpu::Device>,
477 queue: Arc<wgpu::Queue>,
478 surface_format: wgpu::TextureFormat,
479 adapter_backend: wgpu::Backend,
480 adapter_downlevel: wgpu::DownlevelFlags,
481 waker: Arc<dyn Fn() + Send + Sync>,
482 clock: Option<Arc<dyn Fn() -> i64 + Send + Sync>>,
483 observer: Option<Box<dyn PresentObserver>>,
484 ) -> Result<(), WgpuRendererError> {
485 let target = GpuTarget {
486 device,
487 queue,
488 surface_format,
489 adapter_backend,
490 adapter_downlevel,
491 };
492 let init = PresentRuntimeInit {
493 gpu: self.next_gpu_init(target, PipelineCompilation::Background),
494 clock,
495 observer,
496 };
497 let handle = PresentHandle::spawn(init, waker).map_err(WgpuRendererError::Wgpu)?;
498 self.backend = PresentBackend::Threaded(handle);
499 Ok(())
500 }
501
502 #[cfg(not(target_arch = "wasm32"))]
503 #[doc(hidden)]
504 pub fn init_gpu_inline_for_tests(
505 &mut self,
506 device: Arc<wgpu::Device>,
507 queue: Arc<wgpu::Queue>,
508 surface_format: wgpu::TextureFormat,
509 adapter_backend: wgpu::Backend,
510 adapter_downlevel: wgpu::DownlevelFlags,
511 ) -> InlinePresentRuntime {
512 let target = GpuTarget {
513 device,
514 queue,
515 surface_format,
516 adapter_backend,
517 adapter_downlevel,
518 };
519 let init = PresentRuntimeInit {
520 gpu: self.next_gpu_init(target, PipelineCompilation::Background),
521 clock: None,
522 observer: None,
523 };
524 let (handle, state, msg_rx) = PresentHandle::new_inline(init, Arc::new(|| {}));
525 self.backend = PresentBackend::Threaded(handle);
526 InlinePresentRuntime {
527 state,
528 msg_rx,
529 shutdown_seen: false,
530 }
531 }
532
533 pub fn note_surface_reconfigured(&mut self) {
538 self.surface_epoch = self.surface_epoch.wrapping_add(1);
539 }
540
541 pub fn set_transparent_background(&mut self, transparent: bool) {
546 self.frontend.transparent_background = transparent;
547 }
548
549 pub fn root_scale(&self) -> f32 {
550 self.frontend.root_scale
551 }
552
553 pub fn render(
560 &mut self,
561 texture: &wgpu::Texture,
562 view: &wgpu::TextureView,
563 width: u32,
564 height: u32,
565 ) -> Result<(), WgpuRendererError> {
566 self.render_frame(texture, view, width, height)
567 }
568
569 pub fn render_surface_texture(
573 &mut self,
574 texture: &wgpu::Texture,
575 view: &wgpu::TextureView,
576 width: u32,
577 height: u32,
578 ) -> Result<(), WgpuRendererError> {
579 self.render_frame(texture, view, width, height)
580 }
581
582 pub fn present(&self, frame: wgpu::SurfaceTexture) {
589 match self.sync_gpu_renderer() {
590 Some(gpu_renderer) => gpu_renderer.queue.present(frame),
591 None => log::debug!("surface image released: the renderer has no GPU queue"),
592 }
593 }
594
595 fn render_frame(
596 &mut self,
597 texture: &wgpu::Texture,
598 view: &wgpu::TextureView,
599 width: u32,
600 height: u32,
601 ) -> Result<(), WgpuRendererError> {
602 let PresentBackend::Sync(gpu_renderer) = &mut self.backend else {
603 return Err(WgpuRendererError::Wgpu(
604 "GPU renderer not initialized for synchronous rendering. Call init_gpu() first."
605 .to_string(),
606 ));
607 };
608 let packet = self
609 .frontend
610 .build_frame_packet(width, height, self.renderer_epoch, self.surface_epoch)
611 .ok_or_else(|| WgpuRendererError::Wgpu("scene graph is missing".to_string()))?;
612 let mut returns = RenderReturns::default();
613 let result = gpu_renderer.render(
614 texture,
615 view,
616 width,
617 height,
618 packet,
619 self.surface_epoch,
620 &mut returns,
621 );
622 self.frontend.apply_returns(returns);
623 result.map_err(WgpuRendererError::Wgpu)
624 }
625
626 pub fn capture_frame(
630 &mut self,
631 width: u32,
632 height: u32,
633 ) -> Result<CapturedFrame, WgpuRendererError> {
634 self.capture_frame_with_scale(width, height, self.frontend.root_scale)
635 }
636
637 #[cfg(not(target_arch = "wasm32"))]
646 pub fn prepare_recorded_pipelines(
647 &mut self,
648 timeout: std::time::Duration,
649 stop: &dyn Fn() -> bool,
650 ) -> bool {
651 let PresentBackend::Sync(gpu_renderer) = &mut self.backend else {
652 return false;
653 };
654 let deadline = web_time::Instant::now() + timeout;
655 let mut compiling = gpu_renderer.prepare_recorded();
656 while compiling {
657 if web_time::Instant::now() >= deadline || stop() {
658 return false;
659 }
660 std::thread::sleep(std::time::Duration::from_millis(10));
661 compiling = gpu_renderer.compiling();
662 }
663 true
664 }
665
666 pub fn capture_frame_with_scale(
667 &mut self,
668 width: u32,
669 height: u32,
670 root_scale: f32,
671 ) -> Result<CapturedFrame, WgpuRendererError> {
672 let PresentBackend::Sync(gpu_renderer) = &mut self.backend else {
673 return Err(WgpuRendererError::Wgpu(
674 "GPU renderer not initialized for synchronous rendering. Call init_gpu() first."
675 .to_string(),
676 ));
677 };
678 let packet = self
679 .frontend
680 .build_frame_packet_with_scale(
681 width,
682 height,
683 root_scale,
684 self.renderer_epoch,
685 self.surface_epoch,
686 )
687 .ok_or_else(|| WgpuRendererError::Wgpu("scene graph is missing".to_string()))?;
688 let mut returns = RenderReturns::default();
689 let result = gpu_renderer.render_to_rgba_pixels(
690 width,
691 height,
692 packet,
693 self.surface_epoch,
694 &mut returns,
695 );
696 self.frontend.apply_returns(returns);
697 let pixels = result.map_err(WgpuRendererError::Wgpu)?;
698 Ok(CapturedFrame {
699 width,
700 height,
701 pixels,
702 })
703 }
704
705 #[cfg(not(target_arch = "wasm32"))]
710 pub fn has_frame_credit(&self) -> bool {
711 match &self.backend {
712 PresentBackend::Threaded(handle) => handle.has_credit(),
713 PresentBackend::Sync(_) | PresentBackend::None => true,
714 }
715 }
716
717 #[cfg(not(target_arch = "wasm32"))]
723 pub fn frame_credit_returned(&self) -> bool {
724 match &self.backend {
725 PresentBackend::Threaded(handle) => handle.has_undrained_return(),
726 PresentBackend::Sync(_) | PresentBackend::None => false,
727 }
728 }
729
730 #[cfg(not(target_arch = "wasm32"))]
736 pub fn publish_frame(&mut self, width: u32, height: u32) -> PublishOutcome {
737 let PresentBackend::Threaded(handle) = &mut self.backend else {
738 log::error!("publish_frame called without a threaded present runtime");
739 return PublishOutcome::NoCredit;
740 };
741 if !handle.has_credit() {
742 return PublishOutcome::NoCredit;
743 }
744 let Some(packet) = self.frontend.build_frame_packet(
745 width,
746 height,
747 self.renderer_epoch,
748 self.surface_epoch,
749 ) else {
750 return PublishOutcome::NoGraph;
751 };
752 match handle.publish(packet) {
753 Ok(()) => PublishOutcome::Published,
754 Err(packet) => {
755 let mut returns = RenderReturns::default();
756 let _ = GpuRenderer::cancel_packet(
757 *packet,
758 CancelReason::SurfaceUnavailable,
759 &mut returns,
760 );
761 self.frontend.apply_returns(returns);
762 log::error!("present runtime unavailable; frame recovered, not published");
763 PublishOutcome::NoCredit
764 }
765 }
766 }
767
768 #[cfg(not(target_arch = "wasm32"))]
772 pub fn drain_present_returns(&mut self) -> usize {
773 self.drain_present_returns_with(&mut |_, _, _| {})
774 }
775
776 #[cfg(not(target_arch = "wasm32"))]
780 pub fn drain_present_returns_with(
781 &mut self,
782 on_return: &mut dyn FnMut(u64, PresentOutcome, PresentTimings),
783 ) -> usize {
784 let mut drained = 0;
785 loop {
786 let returns = {
787 let PresentBackend::Threaded(handle) = &mut self.backend else {
788 break;
789 };
790 match handle.try_drain() {
791 Some(returns) => returns,
792 None => break,
793 }
794 };
795 drained += 1;
796 let frame_id = returns.frame_id;
797 let outcome = returns.outcome;
798 let timings = returns.timings;
799 self.frontend.apply_returns(returns);
800 on_return(frame_id, outcome, timings);
801 }
802 drained
803 }
804
805 #[cfg(not(target_arch = "wasm32"))]
814 pub fn present_replace_surface(
815 &mut self,
816 surface: wgpu::Surface<'static>,
817 config: wgpu::SurfaceConfiguration,
818 ) -> bool {
819 let surface_epoch = self.surface_epoch;
820 let Some(handle) = self.present_handle_mut() else {
821 log::error!("present_replace_surface called without a threaded present runtime");
822 return false;
823 };
824 let sent = handle.send_control(PresentControl::ReplaceSurface {
825 surface,
826 config,
827 surface_epoch,
828 });
829 if !sent {
830 log::error!("[present-runtime] replace surface: runtime is gone");
831 }
832 sent
833 }
834
835 #[cfg(not(target_arch = "wasm32"))]
839 pub fn present_reconfigure(&mut self, config: wgpu::SurfaceConfiguration) -> bool {
840 let surface_epoch = self.surface_epoch;
841 let Some(handle) = self.present_handle_mut() else {
842 log::error!("present_reconfigure called without a threaded present runtime");
843 return false;
844 };
845 handle.send_control_and_wait(
846 move |ack| PresentControl::Reconfigure {
847 config,
848 surface_epoch,
849 ack,
850 },
851 "reconfigure surface",
852 )
853 }
854
855 #[cfg(not(target_arch = "wasm32"))]
859 pub fn present_drop_surface(&mut self) -> bool {
860 let Some(handle) = self.present_handle_mut() else {
861 log::error!("present_drop_surface called without a threaded present runtime");
862 return false;
863 };
864 handle.send_control_and_wait(|ack| PresentControl::DropSurface { ack }, "drop surface")
865 }
866
867 #[cfg(not(target_arch = "wasm32"))]
870 pub fn shutdown_present_runtime(&mut self) {
871 if matches!(self.backend, PresentBackend::Threaded(_)) {
872 self.retire_live_backend();
873 }
874 }
875
876 #[cfg(not(target_arch = "wasm32"))]
877 #[doc(hidden)]
878 pub fn present_attach_offscreen_for_tests(&mut self, width: u32, height: u32) -> bool {
879 let Some(handle) = self.present_handle_mut() else {
880 return false;
881 };
882 handle.send_control_and_wait(
883 move |ack| PresentControl::AttachOffscreenTargetForTests { width, height, ack },
884 "attach offscreen target",
885 )
886 }
887
888 #[cfg(not(target_arch = "wasm32"))]
889 #[doc(hidden)]
890 pub fn send_attach_offscreen_unacked_for_tests(
891 &mut self,
892 width: u32,
893 height: u32,
894 ) -> Option<std::sync::mpsc::Receiver<()>> {
895 let handle = self.present_handle_mut()?;
896 handle.send_control_unacked(move |ack| PresentControl::AttachOffscreenTargetForTests {
897 width,
898 height,
899 ack,
900 })
901 }
902
903 #[cfg(not(target_arch = "wasm32"))]
904 #[doc(hidden)]
905 pub fn send_reconfigure_unacked_for_tests(
906 &mut self,
907 config: wgpu::SurfaceConfiguration,
908 ) -> Option<std::sync::mpsc::Receiver<()>> {
909 let surface_epoch = self.surface_epoch;
910 let handle = self.present_handle_mut()?;
911 handle.send_control_unacked(move |ack| PresentControl::Reconfigure {
912 config,
913 surface_epoch,
914 ack,
915 })
916 }
917
918 #[cfg(not(target_arch = "wasm32"))]
919 #[doc(hidden)]
920 pub fn send_drop_surface_unacked_for_tests(&mut self) -> Option<std::sync::mpsc::Receiver<()>> {
921 let handle = self.present_handle_mut()?;
922 handle.send_control_unacked(|ack| PresentControl::DropSurface { ack })
923 }
924
925 pub fn present_thread_id(&self) -> Option<i32> {
929 match &self.backend {
930 #[cfg(not(target_arch = "wasm32"))]
931 PresentBackend::Threaded(handle) => {
932 let id = handle
933 .status()
934 .thread_id
935 .load(std::sync::atomic::Ordering::Relaxed);
936 (id != 0).then_some(id)
937 }
938 _ => None,
939 }
940 }
941
942 pub fn last_published_frame_id(&self) -> u64 {
947 self.frontend.frame_sequence
948 }
949
950 #[cfg(not(target_arch = "wasm32"))]
951 #[doc(hidden)]
952 pub fn present_status_snapshot_for_tests(&self) -> Option<(bool, u64, u64)> {
953 match &self.backend {
954 PresentBackend::Threaded(handle) => {
955 let status = handle.status();
956 Some((
957 status
958 .needs_frame_warmup
959 .load(std::sync::atomic::Ordering::Relaxed),
960 status
961 .presented_frames
962 .load(std::sync::atomic::Ordering::Relaxed),
963 status
964 .placeholder_frames
965 .load(std::sync::atomic::Ordering::Relaxed),
966 ))
967 }
968 _ => None,
969 }
970 }
971
972 pub fn last_frame_stats(&self) -> Option<RenderStatsSnapshot> {
973 match &self.backend {
974 PresentBackend::Sync(gpu_renderer) => gpu_renderer.last_frame_stats(),
975 #[cfg(not(target_arch = "wasm32"))]
976 PresentBackend::Threaded(handle) => *handle
977 .status()
978 .last_frame_stats
979 .lock()
980 .unwrap_or_else(std::sync::PoisonError::into_inner),
981 PresentBackend::None => None,
982 }
983 }
984
985 #[cfg(not(target_arch = "wasm32"))]
991 pub fn first_screen_recorded(&self) -> bool {
992 self.sync_gpu_renderer()
993 .is_some_and(GpuRenderer::first_screen_recorded)
994 }
995
996 pub fn first_screen_pipelines(&self) -> Option<Vec<u8>> {
1002 self.sync_gpu_renderer()
1003 .and_then(GpuRenderer::first_screen_pipelines)
1004 }
1005
1006 pub fn build_first_screen_pipelines(&mut self, records: &[u8], viewport: (u32, u32)) {
1017 if let PresentBackend::Sync(gpu_renderer) = &mut self.backend {
1018 gpu_renderer.build_first_screen(records, viewport);
1019 }
1020 }
1021
1022 pub fn gpu_pass_timings(&self) -> GpuPassTimingReport {
1026 self.sync_gpu_renderer()
1027 .map(GpuRenderer::gpu_pass_timings)
1028 .unwrap_or_default()
1029 }
1030
1031 pub fn debug_cpu_allocation_stats(&self) -> DebugCpuAllocationStats {
1032 let mut stats = self
1033 .sync_gpu_renderer()
1034 .map(GpuRenderer::debug_cpu_allocation_stats)
1035 .unwrap_or_default();
1036 stats.scene_graph_node_count = self
1037 .frontend
1038 .scene
1039 .graph
1040 .as_ref()
1041 .map_or(0, RenderGraph::node_count);
1042 stats.scene_graph_heap_bytes = self
1043 .frontend
1044 .scene
1045 .graph
1046 .as_ref()
1047 .map_or(0, RenderGraph::heap_bytes);
1048 stats.scene_hits_len = self.frontend.scene.hits.len();
1049 stats.scene_hits_cap = self.frontend.scene.hits.capacity();
1050 stats.scene_node_index_len = self.frontend.scene.node_index.len();
1051 stats.scene_node_index_cap = self.frontend.scene.node_index.capacity();
1052 stats
1053 }
1054
1055 #[cfg(target_arch = "wasm32")]
1061 pub fn begin_discovery_frame(&self) {
1062 if let Some(gpu_renderer) = self.sync_gpu_renderer() {
1063 gpu_renderer.pipeline_compiler().begin_discovery_frame();
1064 }
1065 }
1066
1067 #[cfg(target_arch = "wasm32")]
1072 pub fn background_pipelines_landed(&mut self) {
1073 if let PresentBackend::Sync(gpu_renderer) = &mut self.backend {
1074 gpu_renderer.background_pipelines_landed();
1075 }
1076 }
1077
1078 #[cfg(target_arch = "wasm32")]
1084 pub fn compile_pipelines_in_background(&mut self) {
1085 if let PresentBackend::Sync(gpu_renderer) = &mut self.backend {
1086 gpu_renderer.compile_pipelines_in_background();
1087 }
1088 }
1089
1090 pub fn try_device(&self) -> Option<&wgpu::Device> {
1094 self.sync_gpu_renderer().map(|r| &*r.device)
1095 }
1096
1097 #[doc(hidden)]
1098 pub fn try_queue_for_tests(&self) -> Option<&wgpu::Queue> {
1099 self.sync_gpu_renderer().map(|r| &*r.queue)
1100 }
1101
1102 #[doc(hidden)]
1103 pub fn device_error_count_for_tests(&self) -> u64 {
1104 self.sync_gpu_renderer()
1105 .map_or(0, GpuRenderer::device_error_count)
1106 }
1107
1108 #[doc(hidden)]
1109 pub fn build_frame_packet_for_tests(
1110 &mut self,
1111 width: u32,
1112 height: u32,
1113 ) -> Option<HeldFramePacket> {
1114 self.frontend
1115 .build_frame_packet(width, height, self.renderer_epoch, self.surface_epoch)
1116 .map(HeldFramePacket)
1117 }
1118
1119 #[doc(hidden)]
1131 pub fn return_held_packet_for_tests(&mut self, packet: HeldFramePacket) {
1132 let packet = packet.0;
1133 self.frontend.apply_returns(RenderReturns {
1134 scene: Some(frame_packet::FrameSceneStorage {
1135 root: packet.root,
1136 overlay: packet.overlay,
1137 }),
1138 frame_id: packet.frame_id,
1139 ..RenderReturns::default()
1140 });
1141 }
1142
1143 #[doc(hidden)]
1144 pub fn render_held_packet_for_tests(
1145 &mut self,
1146 texture: &wgpu::Texture,
1147 view: &wgpu::TextureView,
1148 width: u32,
1149 height: u32,
1150 packet: HeldFramePacket,
1151 ) -> Result<PresentOutcome, WgpuRendererError> {
1152 let PresentBackend::Sync(gpu_renderer) = &mut self.backend else {
1153 return Err(WgpuRendererError::Wgpu(
1154 "GPU renderer not initialized for synchronous rendering. Call init_gpu() first."
1155 .to_string(),
1156 ));
1157 };
1158 let mut returns = RenderReturns::default();
1159 let result = gpu_renderer.render(
1160 texture,
1161 view,
1162 width,
1163 height,
1164 packet.0,
1165 self.surface_epoch,
1166 &mut returns,
1167 );
1168 let outcome = returns.outcome;
1169 self.frontend.apply_returns(returns);
1170 result.map_err(WgpuRendererError::Wgpu)?;
1171 Ok(outcome)
1172 }
1173}
1174
1175#[doc(hidden)]
1176pub struct HeldFramePacket(frame_packet::FramePacket);
1177
1178#[cfg(not(target_arch = "wasm32"))]
1179#[doc(hidden)]
1180pub struct InlinePresentRuntime {
1181 state: PresentState,
1182 msg_rx: std::sync::mpsc::Receiver<PresentMsg>,
1183 shutdown_seen: bool,
1184}
1185
1186#[cfg(not(target_arch = "wasm32"))]
1187impl InlinePresentRuntime {
1188 pub fn pump(&mut self) -> bool {
1189 if self.shutdown_seen {
1190 return false;
1191 }
1192 while let Ok(msg) = self.msg_rx.try_recv() {
1193 if !self.state.run_once(msg) {
1194 self.shutdown_seen = true;
1195 return false;
1196 }
1197 }
1198 self.state.consume_waiting();
1199 true
1200 }
1201
1202 pub fn has_waiting_packet(&self) -> bool {
1203 self.state.has_waiting_packet()
1204 }
1205
1206 pub fn step_one_message(&mut self) -> bool {
1207 if self.shutdown_seen {
1208 return false;
1209 }
1210 match self.msg_rx.try_recv() {
1211 Ok(msg) => {
1212 if !self.state.run_once(msg) {
1213 self.shutdown_seen = true;
1214 }
1215 true
1216 }
1217 Err(_) => false,
1218 }
1219 }
1220
1221 pub fn consume_waiting(&mut self) {
1222 self.state.consume_waiting();
1223 }
1224}
1225
1226impl Default for WgpuRenderer {
1227 fn default() -> Self {
1228 Self::new(&[])
1229 }
1230}
1231
1232fn install_text_measurer(app_context: &cranpose_ui::AppContext, fonts: &SoftwareTextFontSet) {
1234 app_context.set_text_measurer(SoftwareTextMeasurer::from_font_set(fonts.clone(), 8192));
1235}
1236
1237impl Renderer for WgpuRenderer {
1238 type Scene = Scene;
1239 type Error = WgpuRendererError;
1240
1241 fn attach_app_context_services(&mut self, app_context: &cranpose_ui::AppContext) {
1242 install_text_measurer(app_context, &self.frontend.text_fonts);
1243 self.frontend.app_context = Some(app_context.downgrade());
1244 }
1245
1246 #[cfg(feature = "installed-fonts")]
1247 fn apply_installed_fonts(&mut self, app_context: &cranpose_ui::AppContext) {
1248 let Some(fonts) = self.frontend.text_fonts.with_installed_faces() else {
1249 return;
1250 };
1251 let attached = self
1252 .frontend
1253 .app_context
1254 .as_ref()
1255 .and_then(std::rc::Weak::upgrade)
1256 .is_some_and(|attached| std::ptr::eq(&*attached, app_context));
1257 if attached {
1258 install_text_measurer(app_context, &fonts);
1259 }
1260 self.frontend.text_state = TextSystemState::from_font_set(fonts.clone());
1261 self.frontend.text_fonts = fonts;
1262 }
1263
1264 fn set_root_scale(&mut self, scale: f32) {
1265 self.frontend.root_scale = scale;
1266 }
1267
1268 fn scene(&self) -> &Self::Scene {
1269 &self.frontend.scene
1270 }
1271
1272 fn scene_mut(&mut self) -> &mut Self::Scene {
1273 &mut self.frontend.scene
1274 }
1275
1276 fn rebuild_scene(
1277 &mut self,
1278 layout_tree: &LayoutTree,
1279 _viewport: Size,
1280 ) -> Result<(), Self::Error> {
1281 self.frontend.scene.clear();
1282 self.frontend.clear_fps_overlay();
1283 pipeline::render_layout_tree(layout_tree.root(), &mut self.frontend.scene);
1284 Ok(())
1285 }
1286
1287 fn rebuild_scene_from_applier(
1288 &mut self,
1289 applier: &mut MemoryApplier,
1290 root: NodeId,
1291 _viewport: Size,
1292 ) -> Result<(), Self::Error> {
1293 self.frontend.clear_fps_overlay();
1294 self.frontend.scene.rebuild_from_applier(applier, root);
1295 Ok(())
1296 }
1297
1298 fn update_scene_from_applier(
1299 &mut self,
1300 applier: &mut MemoryApplier,
1301 root: NodeId,
1302 _viewport: Size,
1303 updates: cranpose_render_common::SceneUpdates<'_>,
1304 ) -> Result<(), Self::Error> {
1305 if updates.is_empty() {
1306 self.frontend.clear_fps_overlay();
1307 }
1308 self.frontend
1309 .scene
1310 .update_from_applier(applier, root, updates, true);
1311 Ok(())
1312 }
1313
1314 fn update_visual_scene_from_applier(
1315 &mut self,
1316 applier: &mut MemoryApplier,
1317 root: NodeId,
1318 _viewport: Size,
1319 updates: cranpose_render_common::SceneUpdates<'_>,
1320 ) -> Result<(), Self::Error> {
1321 if updates.is_empty() {
1322 self.frontend.clear_fps_overlay();
1323 }
1324 self.frontend
1325 .scene
1326 .update_from_applier(applier, root, updates, false);
1327 Ok(())
1328 }
1329
1330 fn draw_dev_overlay(&mut self, text: &str, viewport: Size) {
1331 const DEV_OVERLAY_NODE_ID: NodeId = NodeId::MAX;
1332 let key = cranpose_render_common::dev_overlay::DevOverlayKey::new(text, viewport);
1333 if self.frontend.dev_overlay_graph.is_some()
1334 && self
1335 .frontend
1336 .dev_overlay_cache
1337 .as_ref()
1338 .is_some_and(|cache| {
1339 cache.text == key.text
1340 && cache.viewport_width_bits == key.viewport_width_bits
1341 && cache.viewport_height_bits == key.viewport_height_bits
1342 })
1343 {
1344 return;
1345 }
1346 self.frontend.fps_overlay_graph = Some(
1347 cranpose_render_common::dev_overlay::build_dev_overlay_graph(
1348 text,
1349 viewport,
1350 DEV_OVERLAY_NODE_ID,
1351 ),
1352 );
1353 self.frontend.dev_overlay_cache = Some(DevOverlayCache {
1354 text: key.text,
1355 viewport_width_bits: key.viewport_width_bits,
1356 viewport_height_bits: key.viewport_height_bits,
1357 });
1358 self.frontend.refresh_dev_overlay();
1359 }
1360
1361 fn set_inspector_overlay(&mut self, graph: Option<cranpose_render_common::graph::RenderGraph>) {
1362 self.frontend.inspector_overlay_graph = graph;
1363 self.frontend.refresh_dev_overlay();
1364 }
1365
1366 fn needs_frame_warmup(&self) -> bool {
1367 match &self.backend {
1368 PresentBackend::Sync(gpu_renderer) => gpu_renderer.needs_frame_warmup(),
1369 #[cfg(not(target_arch = "wasm32"))]
1370 PresentBackend::Threaded(handle) => handle
1371 .status()
1372 .needs_frame_warmup
1373 .load(std::sync::atomic::Ordering::Relaxed),
1374 PresentBackend::None => false,
1375 }
1376 }
1377
1378 fn awaits_pipelines(&self) -> bool {
1379 match &self.backend {
1380 PresentBackend::Sync(gpu_renderer) => gpu_renderer.awaits_pipelines(),
1381 #[cfg(not(target_arch = "wasm32"))]
1382 PresentBackend::Threaded(handle) => {
1383 let status = handle.status();
1384 status
1385 .drew_placeholder
1386 .load(std::sync::atomic::Ordering::Relaxed)
1387 && status
1388 .landing
1389 .get()
1390 .is_some_and(|landing| landing.in_flight())
1391 }
1392 PresentBackend::None => false,
1393 }
1394 }
1395}
1396
1397#[cfg(test)]
1398#[path = "tests/wgpu_tests.rs"]
1399mod tests;