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 on_device_lost: Option<DeviceLostHandler>,
311}
312
313pub type DeviceLostHandler = Arc<dyn Fn() + Send + Sync>;
316
317impl WgpuRenderer {
318 pub fn new(fonts: &[&'static [u8]]) -> Self {
325 Self::with_text_system(WgpuTextSystem::from_fonts(fonts))
326 }
327
328 pub fn with_font_set(fonts: SoftwareTextFontSet) -> Self {
333 Self::with_text_system(WgpuTextSystem::from_font_set(fonts))
334 }
335
336 pub fn with_text_system(text_system: WgpuTextSystem) -> Self {
337 Self {
338 frontend: RendererFrontend::new(
339 text_system.render_state(),
340 text_system.software_fonts(),
341 ),
342 backend: PresentBackend::None,
343 renderer_epoch: 0,
344 surface_epoch: 0,
345 on_device_lost: None,
346 }
347 }
348
349 pub fn set_device_lost_handler(&mut self, handler: DeviceLostHandler) {
352 self.on_device_lost = Some(handler);
353 }
354
355 fn sync_gpu_renderer(&self) -> Option<&GpuRenderer> {
356 match &self.backend {
357 PresentBackend::Sync(gpu_renderer) => Some(gpu_renderer.as_ref()),
358 _ => None,
359 }
360 }
361
362 #[cfg(not(target_arch = "wasm32"))]
363 fn present_handle_mut(&mut self) -> Option<&mut PresentHandle> {
364 match &mut self.backend {
365 PresentBackend::Threaded(handle) => Some(handle),
366 _ => None,
367 }
368 }
369
370 fn retire_live_backend(&mut self) {
371 #[allow(unused_mut)]
372 let mut backend = std::mem::replace(&mut self.backend, PresentBackend::None);
373 #[cfg(not(target_arch = "wasm32"))]
374 if let PresentBackend::Threaded(handle) = &mut backend {
375 while let Some(returns) = handle.try_drain() {
376 self.frontend.apply_returns(returns);
377 }
378 handle.shutdown();
379 }
380 drop(backend);
381 }
382
383 pub fn init_gpu(
389 &mut self,
390 device: Arc<wgpu::Device>,
391 queue: Arc<wgpu::Queue>,
392 surface_format: wgpu::TextureFormat,
393 adapter_backend: wgpu::Backend,
394 adapter_downlevel: wgpu::DownlevelFlags,
395 ) {
396 let target = GpuTarget {
397 device,
398 queue,
399 surface_format,
400 adapter_backend,
401 adapter_downlevel,
402 };
403 self.install_gpu(target, PipelineCompilation::Background);
404 }
405
406 #[doc(hidden)]
410 pub fn init_gpu_compiling_inline_for_tests(
411 &mut self,
412 device: Arc<wgpu::Device>,
413 queue: Arc<wgpu::Queue>,
414 surface_format: wgpu::TextureFormat,
415 adapter_backend: wgpu::Backend,
416 adapter_downlevel: wgpu::DownlevelFlags,
417 ) {
418 let target = GpuTarget {
419 device,
420 queue,
421 surface_format,
422 adapter_backend,
423 adapter_downlevel,
424 };
425 self.install_gpu(target, PipelineCompilation::Inline);
426 }
427
428 fn install_gpu(&mut self, target: GpuTarget, pipeline_compilation: PipelineCompilation) {
429 let init = self.next_gpu_init(target, pipeline_compilation);
430 self.backend = PresentBackend::Sync(Box::new(GpuRenderer::new(init)));
431 }
432
433 fn next_gpu_init(
436 &mut self,
437 target: GpuTarget,
438 pipeline_compilation: PipelineCompilation,
439 ) -> GpuRendererInit {
440 self.retire_live_backend();
441 self.renderer_epoch = self.renderer_epoch.wrapping_add(1);
442 GpuRendererInit {
443 device: target.device,
444 queue: target.queue,
445 surface_format: target.surface_format,
446 adapter_backend: target.adapter_backend,
447 adapter_downlevel: target.adapter_downlevel,
448 text_fonts: self.frontend.text_fonts.clone(),
449 renderer_epoch: self.renderer_epoch,
450 pipeline_compilation,
451 on_device_lost: self.on_device_lost.clone(),
452 }
453 }
454
455 #[cfg(not(target_arch = "wasm32"))]
472 #[expect(clippy::too_many_arguments)]
473 pub fn init_gpu_threaded(
474 &mut self,
475 device: Arc<wgpu::Device>,
476 queue: Arc<wgpu::Queue>,
477 surface_format: wgpu::TextureFormat,
478 adapter_backend: wgpu::Backend,
479 adapter_downlevel: wgpu::DownlevelFlags,
480 waker: Arc<dyn Fn() + Send + Sync>,
481 clock: Option<Arc<dyn Fn() -> i64 + Send + Sync>>,
482 observer: Option<Box<dyn PresentObserver>>,
483 ) -> Result<(), WgpuRendererError> {
484 let target = GpuTarget {
485 device,
486 queue,
487 surface_format,
488 adapter_backend,
489 adapter_downlevel,
490 };
491 let init = PresentRuntimeInit {
492 gpu: self.next_gpu_init(target, PipelineCompilation::Background),
493 clock,
494 observer,
495 };
496 let handle = PresentHandle::spawn(init, waker).map_err(WgpuRendererError::Wgpu)?;
497 self.backend = PresentBackend::Threaded(handle);
498 Ok(())
499 }
500
501 #[cfg(not(target_arch = "wasm32"))]
502 #[doc(hidden)]
503 pub fn init_gpu_inline_for_tests(
504 &mut self,
505 device: Arc<wgpu::Device>,
506 queue: Arc<wgpu::Queue>,
507 surface_format: wgpu::TextureFormat,
508 adapter_backend: wgpu::Backend,
509 adapter_downlevel: wgpu::DownlevelFlags,
510 ) -> InlinePresentRuntime {
511 let target = GpuTarget {
512 device,
513 queue,
514 surface_format,
515 adapter_backend,
516 adapter_downlevel,
517 };
518 let init = PresentRuntimeInit {
519 gpu: self.next_gpu_init(target, PipelineCompilation::Background),
520 clock: None,
521 observer: None,
522 };
523 let (handle, state, msg_rx) = PresentHandle::new_inline(init, Arc::new(|| {}));
524 self.backend = PresentBackend::Threaded(handle);
525 InlinePresentRuntime {
526 state,
527 msg_rx,
528 shutdown_seen: false,
529 }
530 }
531
532 pub fn note_surface_reconfigured(&mut self) {
537 self.surface_epoch = self.surface_epoch.wrapping_add(1);
538 }
539
540 pub fn set_transparent_background(&mut self, transparent: bool) {
545 self.frontend.transparent_background = transparent;
546 }
547
548 pub fn root_scale(&self) -> f32 {
549 self.frontend.root_scale
550 }
551
552 pub fn render(
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 render_surface_texture(
572 &mut self,
573 texture: &wgpu::Texture,
574 view: &wgpu::TextureView,
575 width: u32,
576 height: u32,
577 ) -> Result<(), WgpuRendererError> {
578 self.render_frame(texture, view, width, height)
579 }
580
581 pub fn present(&self, frame: wgpu::SurfaceTexture) {
588 match self.sync_gpu_renderer() {
589 Some(gpu_renderer) => gpu_renderer.queue.present(frame),
590 None => log::debug!("surface image released: the renderer has no GPU queue"),
591 }
592 }
593
594 fn render_frame(
595 &mut self,
596 texture: &wgpu::Texture,
597 view: &wgpu::TextureView,
598 width: u32,
599 height: u32,
600 ) -> Result<(), WgpuRendererError> {
601 let PresentBackend::Sync(gpu_renderer) = &mut self.backend else {
602 return Err(WgpuRendererError::Wgpu(
603 "GPU renderer not initialized for synchronous rendering. Call init_gpu() first."
604 .to_string(),
605 ));
606 };
607 let packet = self
608 .frontend
609 .build_frame_packet(width, height, self.renderer_epoch, self.surface_epoch)
610 .ok_or_else(|| WgpuRendererError::Wgpu("scene graph is missing".to_string()))?;
611 let mut returns = RenderReturns::default();
612 let result = gpu_renderer.render(
613 texture,
614 view,
615 width,
616 height,
617 packet,
618 self.surface_epoch,
619 &mut returns,
620 );
621 self.frontend.apply_returns(returns);
622 result.map_err(WgpuRendererError::Wgpu)
623 }
624
625 pub fn capture_frame(
629 &mut self,
630 width: u32,
631 height: u32,
632 ) -> Result<CapturedFrame, WgpuRendererError> {
633 self.capture_frame_with_scale(width, height, self.frontend.root_scale)
634 }
635
636 #[cfg(not(target_arch = "wasm32"))]
645 pub fn prepare_recorded_pipelines(
646 &mut self,
647 timeout: std::time::Duration,
648 stop: &dyn Fn() -> bool,
649 ) -> bool {
650 let PresentBackend::Sync(gpu_renderer) = &mut self.backend else {
651 return false;
652 };
653 let deadline = web_time::Instant::now() + timeout;
654 let mut compiling = gpu_renderer.prepare_recorded();
655 while compiling {
656 if web_time::Instant::now() >= deadline || stop() {
657 return false;
658 }
659 std::thread::sleep(std::time::Duration::from_millis(10));
660 compiling = gpu_renderer.compiling();
661 }
662 true
663 }
664
665 pub fn capture_frame_with_scale(
666 &mut self,
667 width: u32,
668 height: u32,
669 root_scale: f32,
670 ) -> Result<CapturedFrame, WgpuRendererError> {
671 let PresentBackend::Sync(gpu_renderer) = &mut self.backend else {
672 return Err(WgpuRendererError::Wgpu(
673 "GPU renderer not initialized for synchronous rendering. Call init_gpu() first."
674 .to_string(),
675 ));
676 };
677 let packet = self
678 .frontend
679 .build_frame_packet_with_scale(
680 width,
681 height,
682 root_scale,
683 self.renderer_epoch,
684 self.surface_epoch,
685 )
686 .ok_or_else(|| WgpuRendererError::Wgpu("scene graph is missing".to_string()))?;
687 let mut returns = RenderReturns::default();
688 let result = gpu_renderer.render_to_rgba_pixels(
689 width,
690 height,
691 packet,
692 self.surface_epoch,
693 &mut returns,
694 );
695 self.frontend.apply_returns(returns);
696 let pixels = result.map_err(WgpuRendererError::Wgpu)?;
697 Ok(CapturedFrame {
698 width,
699 height,
700 pixels,
701 })
702 }
703
704 #[cfg(not(target_arch = "wasm32"))]
709 pub fn has_frame_credit(&self) -> bool {
710 match &self.backend {
711 PresentBackend::Threaded(handle) => handle.has_credit(),
712 PresentBackend::Sync(_) | PresentBackend::None => true,
713 }
714 }
715
716 #[cfg(not(target_arch = "wasm32"))]
722 pub fn frame_credit_returned(&self) -> bool {
723 match &self.backend {
724 PresentBackend::Threaded(handle) => handle.has_undrained_return(),
725 PresentBackend::Sync(_) | PresentBackend::None => false,
726 }
727 }
728
729 #[cfg(not(target_arch = "wasm32"))]
735 pub fn publish_frame(&mut self, width: u32, height: u32) -> PublishOutcome {
736 let PresentBackend::Threaded(handle) = &mut self.backend else {
737 log::error!("publish_frame called without a threaded present runtime");
738 return PublishOutcome::NoCredit;
739 };
740 if !handle.has_credit() {
741 return PublishOutcome::NoCredit;
742 }
743 let Some(packet) = self.frontend.build_frame_packet(
744 width,
745 height,
746 self.renderer_epoch,
747 self.surface_epoch,
748 ) else {
749 return PublishOutcome::NoGraph;
750 };
751 match handle.publish(packet) {
752 Ok(()) => PublishOutcome::Published,
753 Err(packet) => {
754 let mut returns = RenderReturns::default();
755 let _ = GpuRenderer::cancel_packet(
756 *packet,
757 CancelReason::SurfaceUnavailable,
758 &mut returns,
759 );
760 self.frontend.apply_returns(returns);
761 log::error!("present runtime unavailable; frame recovered, not published");
762 PublishOutcome::NoCredit
763 }
764 }
765 }
766
767 #[cfg(not(target_arch = "wasm32"))]
771 pub fn drain_present_returns(&mut self) -> usize {
772 self.drain_present_returns_with(&mut |_, _, _| {})
773 }
774
775 #[cfg(not(target_arch = "wasm32"))]
779 pub fn drain_present_returns_with(
780 &mut self,
781 on_return: &mut dyn FnMut(u64, PresentOutcome, PresentTimings),
782 ) -> usize {
783 let mut drained = 0;
784 loop {
785 let returns = {
786 let PresentBackend::Threaded(handle) = &mut self.backend else {
787 break;
788 };
789 match handle.try_drain() {
790 Some(returns) => returns,
791 None => break,
792 }
793 };
794 drained += 1;
795 let frame_id = returns.frame_id;
796 let outcome = returns.outcome;
797 let timings = returns.timings;
798 self.frontend.apply_returns(returns);
799 on_return(frame_id, outcome, timings);
800 }
801 drained
802 }
803
804 #[cfg(not(target_arch = "wasm32"))]
813 pub fn present_replace_surface(
814 &mut self,
815 surface: wgpu::Surface<'static>,
816 config: wgpu::SurfaceConfiguration,
817 ) -> bool {
818 let surface_epoch = self.surface_epoch;
819 let Some(handle) = self.present_handle_mut() else {
820 log::error!("present_replace_surface called without a threaded present runtime");
821 return false;
822 };
823 let sent = handle.send_control(PresentControl::ReplaceSurface {
824 surface,
825 config,
826 surface_epoch,
827 });
828 if !sent {
829 log::error!("[present-runtime] replace surface: runtime is gone");
830 }
831 sent
832 }
833
834 #[cfg(not(target_arch = "wasm32"))]
838 pub fn present_reconfigure(&mut self, config: wgpu::SurfaceConfiguration) -> bool {
839 let surface_epoch = self.surface_epoch;
840 let Some(handle) = self.present_handle_mut() else {
841 log::error!("present_reconfigure called without a threaded present runtime");
842 return false;
843 };
844 handle.send_control_and_wait(
845 move |ack| PresentControl::Reconfigure {
846 config,
847 surface_epoch,
848 ack,
849 },
850 "reconfigure surface",
851 )
852 }
853
854 #[cfg(not(target_arch = "wasm32"))]
858 pub fn present_drop_surface(&mut self) -> bool {
859 let Some(handle) = self.present_handle_mut() else {
860 log::error!("present_drop_surface called without a threaded present runtime");
861 return false;
862 };
863 handle.send_control_and_wait(|ack| PresentControl::DropSurface { ack }, "drop surface")
864 }
865
866 #[cfg(not(target_arch = "wasm32"))]
869 pub fn shutdown_present_runtime(&mut self) {
870 if matches!(self.backend, PresentBackend::Threaded(_)) {
871 self.retire_live_backend();
872 }
873 }
874
875 #[cfg(not(target_arch = "wasm32"))]
876 #[doc(hidden)]
877 pub fn present_attach_offscreen_for_tests(&mut self, width: u32, height: u32) -> bool {
878 let Some(handle) = self.present_handle_mut() else {
879 return false;
880 };
881 handle.send_control_and_wait(
882 move |ack| PresentControl::AttachOffscreenTargetForTests { width, height, ack },
883 "attach offscreen target",
884 )
885 }
886
887 #[cfg(not(target_arch = "wasm32"))]
888 #[doc(hidden)]
889 pub fn send_attach_offscreen_unacked_for_tests(
890 &mut self,
891 width: u32,
892 height: u32,
893 ) -> Option<std::sync::mpsc::Receiver<()>> {
894 let handle = self.present_handle_mut()?;
895 handle.send_control_unacked(move |ack| PresentControl::AttachOffscreenTargetForTests {
896 width,
897 height,
898 ack,
899 })
900 }
901
902 #[cfg(not(target_arch = "wasm32"))]
903 #[doc(hidden)]
904 pub fn send_reconfigure_unacked_for_tests(
905 &mut self,
906 config: wgpu::SurfaceConfiguration,
907 ) -> Option<std::sync::mpsc::Receiver<()>> {
908 let surface_epoch = self.surface_epoch;
909 let handle = self.present_handle_mut()?;
910 handle.send_control_unacked(move |ack| PresentControl::Reconfigure {
911 config,
912 surface_epoch,
913 ack,
914 })
915 }
916
917 #[cfg(not(target_arch = "wasm32"))]
918 #[doc(hidden)]
919 pub fn send_drop_surface_unacked_for_tests(&mut self) -> Option<std::sync::mpsc::Receiver<()>> {
920 let handle = self.present_handle_mut()?;
921 handle.send_control_unacked(|ack| PresentControl::DropSurface { ack })
922 }
923
924 pub fn present_thread_id(&self) -> Option<i32> {
928 match &self.backend {
929 #[cfg(not(target_arch = "wasm32"))]
930 PresentBackend::Threaded(handle) => {
931 let id = handle
932 .status()
933 .thread_id
934 .load(std::sync::atomic::Ordering::Relaxed);
935 (id != 0).then_some(id)
936 }
937 _ => None,
938 }
939 }
940
941 pub fn last_published_frame_id(&self) -> u64 {
946 self.frontend.frame_sequence
947 }
948
949 #[cfg(not(target_arch = "wasm32"))]
950 #[doc(hidden)]
951 pub fn present_status_snapshot_for_tests(&self) -> Option<(bool, u64, u64)> {
952 match &self.backend {
953 PresentBackend::Threaded(handle) => {
954 let status = handle.status();
955 Some((
956 status
957 .needs_frame_warmup
958 .load(std::sync::atomic::Ordering::Relaxed),
959 status
960 .presented_frames
961 .load(std::sync::atomic::Ordering::Relaxed),
962 status
963 .placeholder_frames
964 .load(std::sync::atomic::Ordering::Relaxed),
965 ))
966 }
967 _ => None,
968 }
969 }
970
971 pub fn last_frame_stats(&self) -> Option<RenderStatsSnapshot> {
972 match &self.backend {
973 PresentBackend::Sync(gpu_renderer) => gpu_renderer.last_frame_stats(),
974 #[cfg(not(target_arch = "wasm32"))]
975 PresentBackend::Threaded(handle) => *handle
976 .status()
977 .last_frame_stats
978 .lock()
979 .unwrap_or_else(std::sync::PoisonError::into_inner),
980 PresentBackend::None => None,
981 }
982 }
983
984 #[cfg(not(target_arch = "wasm32"))]
990 pub fn first_screen_recorded(&self) -> bool {
991 self.sync_gpu_renderer()
992 .is_some_and(GpuRenderer::first_screen_recorded)
993 }
994
995 pub fn first_screen_pipelines(&self) -> Option<Vec<u8>> {
1001 self.sync_gpu_renderer()
1002 .and_then(GpuRenderer::first_screen_pipelines)
1003 }
1004
1005 pub fn build_first_screen_pipelines(&mut self, records: &[u8], viewport: (u32, u32)) {
1016 if let PresentBackend::Sync(gpu_renderer) = &mut self.backend {
1017 gpu_renderer.build_first_screen(records, viewport);
1018 }
1019 }
1020
1021 pub fn gpu_pass_timings(&self) -> GpuPassTimingReport {
1025 self.sync_gpu_renderer()
1026 .map(GpuRenderer::gpu_pass_timings)
1027 .unwrap_or_default()
1028 }
1029
1030 pub fn debug_cpu_allocation_stats(&self) -> DebugCpuAllocationStats {
1031 let mut stats = self
1032 .sync_gpu_renderer()
1033 .map(GpuRenderer::debug_cpu_allocation_stats)
1034 .unwrap_or_default();
1035 stats.scene_graph_node_count = self
1036 .frontend
1037 .scene
1038 .graph
1039 .as_ref()
1040 .map_or(0, RenderGraph::node_count);
1041 stats.scene_graph_heap_bytes = self
1042 .frontend
1043 .scene
1044 .graph
1045 .as_ref()
1046 .map_or(0, RenderGraph::heap_bytes);
1047 stats.scene_hits_len = self.frontend.scene.hits.len();
1048 stats.scene_hits_cap = self.frontend.scene.hits.capacity();
1049 stats.scene_node_index_len = self.frontend.scene.node_index.len();
1050 stats.scene_node_index_cap = self.frontend.scene.node_index.capacity();
1051 stats
1052 }
1053
1054 #[cfg(target_arch = "wasm32")]
1060 pub fn begin_discovery_frame(&self) {
1061 if let Some(gpu_renderer) = self.sync_gpu_renderer() {
1062 gpu_renderer.pipeline_compiler().begin_discovery_frame();
1063 }
1064 }
1065
1066 #[cfg(target_arch = "wasm32")]
1071 pub fn background_pipelines_landed(&mut self) {
1072 if let PresentBackend::Sync(gpu_renderer) = &mut self.backend {
1073 gpu_renderer.background_pipelines_landed();
1074 }
1075 }
1076
1077 #[cfg(target_arch = "wasm32")]
1083 pub fn compile_pipelines_in_background(&mut self) {
1084 if let PresentBackend::Sync(gpu_renderer) = &mut self.backend {
1085 gpu_renderer.compile_pipelines_in_background();
1086 }
1087 }
1088
1089 pub fn try_device(&self) -> Option<&wgpu::Device> {
1093 self.sync_gpu_renderer().map(|r| &*r.device)
1094 }
1095
1096 #[doc(hidden)]
1097 pub fn try_queue_for_tests(&self) -> Option<&wgpu::Queue> {
1098 self.sync_gpu_renderer().map(|r| &*r.queue)
1099 }
1100
1101 #[doc(hidden)]
1102 pub fn device_error_count_for_tests(&self) -> u64 {
1103 self.sync_gpu_renderer()
1104 .map_or(0, GpuRenderer::device_error_count)
1105 }
1106
1107 #[doc(hidden)]
1108 pub fn build_frame_packet_for_tests(
1109 &mut self,
1110 width: u32,
1111 height: u32,
1112 ) -> Option<HeldFramePacket> {
1113 self.frontend
1114 .build_frame_packet(width, height, self.renderer_epoch, self.surface_epoch)
1115 .map(HeldFramePacket)
1116 }
1117
1118 #[doc(hidden)]
1130 pub fn return_held_packet_for_tests(&mut self, packet: HeldFramePacket) {
1131 let packet = packet.0;
1132 self.frontend.apply_returns(RenderReturns {
1133 scene: Some(frame_packet::FrameSceneStorage {
1134 root: packet.root,
1135 overlay: packet.overlay,
1136 }),
1137 frame_id: packet.frame_id,
1138 ..RenderReturns::default()
1139 });
1140 }
1141
1142 #[doc(hidden)]
1143 pub fn render_held_packet_for_tests(
1144 &mut self,
1145 texture: &wgpu::Texture,
1146 view: &wgpu::TextureView,
1147 width: u32,
1148 height: u32,
1149 packet: HeldFramePacket,
1150 ) -> Result<PresentOutcome, WgpuRendererError> {
1151 let PresentBackend::Sync(gpu_renderer) = &mut self.backend else {
1152 return Err(WgpuRendererError::Wgpu(
1153 "GPU renderer not initialized for synchronous rendering. Call init_gpu() first."
1154 .to_string(),
1155 ));
1156 };
1157 let mut returns = RenderReturns::default();
1158 let result = gpu_renderer.render(
1159 texture,
1160 view,
1161 width,
1162 height,
1163 packet.0,
1164 self.surface_epoch,
1165 &mut returns,
1166 );
1167 let outcome = returns.outcome;
1168 self.frontend.apply_returns(returns);
1169 result.map_err(WgpuRendererError::Wgpu)?;
1170 Ok(outcome)
1171 }
1172}
1173
1174#[doc(hidden)]
1175pub struct HeldFramePacket(frame_packet::FramePacket);
1176
1177#[cfg(not(target_arch = "wasm32"))]
1178#[doc(hidden)]
1179pub struct InlinePresentRuntime {
1180 state: PresentState,
1181 msg_rx: std::sync::mpsc::Receiver<PresentMsg>,
1182 shutdown_seen: bool,
1183}
1184
1185#[cfg(not(target_arch = "wasm32"))]
1186impl InlinePresentRuntime {
1187 pub fn pump(&mut self) -> bool {
1188 if self.shutdown_seen {
1189 return false;
1190 }
1191 while let Ok(msg) = self.msg_rx.try_recv() {
1192 if !self.state.run_once(msg) {
1193 self.shutdown_seen = true;
1194 return false;
1195 }
1196 }
1197 self.state.consume_waiting();
1198 true
1199 }
1200
1201 pub fn has_waiting_packet(&self) -> bool {
1202 self.state.has_waiting_packet()
1203 }
1204
1205 pub fn step_one_message(&mut self) -> bool {
1206 if self.shutdown_seen {
1207 return false;
1208 }
1209 match self.msg_rx.try_recv() {
1210 Ok(msg) => {
1211 if !self.state.run_once(msg) {
1212 self.shutdown_seen = true;
1213 }
1214 true
1215 }
1216 Err(_) => false,
1217 }
1218 }
1219
1220 pub fn consume_waiting(&mut self) {
1221 self.state.consume_waiting();
1222 }
1223}
1224
1225impl Default for WgpuRenderer {
1226 fn default() -> Self {
1227 Self::new(&[])
1228 }
1229}
1230
1231fn install_text_measurer(app_context: &cranpose_ui::AppContext, fonts: &SoftwareTextFontSet) {
1233 app_context.set_text_measurer(SoftwareTextMeasurer::from_font_set(fonts.clone(), 8192));
1234}
1235
1236impl Renderer for WgpuRenderer {
1237 type Scene = Scene;
1238 type Error = WgpuRendererError;
1239
1240 fn attach_app_context_services(&mut self, app_context: &cranpose_ui::AppContext) {
1241 install_text_measurer(app_context, &self.frontend.text_fonts);
1242 self.frontend.app_context = Some(app_context.downgrade());
1243 }
1244
1245 #[cfg(feature = "installed-fonts")]
1246 fn apply_installed_fonts(&mut self, app_context: &cranpose_ui::AppContext) {
1247 let Some(fonts) = self.frontend.text_fonts.with_installed_faces() else {
1248 return;
1249 };
1250 let attached = self
1251 .frontend
1252 .app_context
1253 .as_ref()
1254 .and_then(std::rc::Weak::upgrade)
1255 .is_some_and(|attached| std::ptr::eq(&*attached, app_context));
1256 if attached {
1257 install_text_measurer(app_context, &fonts);
1258 }
1259 self.frontend.text_state = TextSystemState::from_font_set(fonts.clone());
1260 self.frontend.text_fonts = fonts;
1261 }
1262
1263 fn set_root_scale(&mut self, scale: f32) {
1264 self.frontend.root_scale = scale;
1265 }
1266
1267 fn scene(&self) -> &Self::Scene {
1268 &self.frontend.scene
1269 }
1270
1271 fn scene_mut(&mut self) -> &mut Self::Scene {
1272 &mut self.frontend.scene
1273 }
1274
1275 fn rebuild_scene(
1276 &mut self,
1277 layout_tree: &LayoutTree,
1278 _viewport: Size,
1279 ) -> Result<(), Self::Error> {
1280 self.frontend.scene.clear();
1281 self.frontend.clear_fps_overlay();
1282 pipeline::render_layout_tree(layout_tree.root(), &mut self.frontend.scene);
1283 Ok(())
1284 }
1285
1286 fn rebuild_scene_from_applier(
1287 &mut self,
1288 applier: &mut MemoryApplier,
1289 root: NodeId,
1290 _viewport: Size,
1291 ) -> Result<(), Self::Error> {
1292 self.frontend.clear_fps_overlay();
1293 self.frontend.scene.rebuild_from_applier(applier, root);
1294 Ok(())
1295 }
1296
1297 fn update_scene_from_applier(
1298 &mut self,
1299 applier: &mut MemoryApplier,
1300 root: NodeId,
1301 _viewport: Size,
1302 updates: cranpose_render_common::SceneUpdates<'_>,
1303 ) -> Result<(), Self::Error> {
1304 if updates.is_empty() {
1305 self.frontend.clear_fps_overlay();
1306 }
1307 self.frontend
1308 .scene
1309 .update_from_applier(applier, root, updates, true);
1310 Ok(())
1311 }
1312
1313 fn update_visual_scene_from_applier(
1314 &mut self,
1315 applier: &mut MemoryApplier,
1316 root: NodeId,
1317 _viewport: Size,
1318 updates: cranpose_render_common::SceneUpdates<'_>,
1319 ) -> Result<(), Self::Error> {
1320 if updates.is_empty() {
1321 self.frontend.clear_fps_overlay();
1322 }
1323 self.frontend
1324 .scene
1325 .update_from_applier(applier, root, updates, false);
1326 Ok(())
1327 }
1328
1329 fn draw_dev_overlay(&mut self, text: &str, viewport: Size) {
1330 const DEV_OVERLAY_NODE_ID: NodeId = NodeId::MAX;
1331 let key = cranpose_render_common::dev_overlay::DevOverlayKey::new(text, viewport);
1332 if self.frontend.dev_overlay_graph.is_some()
1333 && self
1334 .frontend
1335 .dev_overlay_cache
1336 .as_ref()
1337 .is_some_and(|cache| {
1338 cache.text == key.text
1339 && cache.viewport_width_bits == key.viewport_width_bits
1340 && cache.viewport_height_bits == key.viewport_height_bits
1341 })
1342 {
1343 return;
1344 }
1345 self.frontend.fps_overlay_graph = Some(
1346 cranpose_render_common::dev_overlay::build_dev_overlay_graph(
1347 text,
1348 viewport,
1349 DEV_OVERLAY_NODE_ID,
1350 ),
1351 );
1352 self.frontend.dev_overlay_cache = Some(DevOverlayCache {
1353 text: key.text,
1354 viewport_width_bits: key.viewport_width_bits,
1355 viewport_height_bits: key.viewport_height_bits,
1356 });
1357 self.frontend.refresh_dev_overlay();
1358 }
1359
1360 fn set_inspector_overlay(&mut self, graph: Option<cranpose_render_common::graph::RenderGraph>) {
1361 self.frontend.inspector_overlay_graph = graph;
1362 self.frontend.refresh_dev_overlay();
1363 }
1364
1365 fn needs_frame_warmup(&self) -> bool {
1366 match &self.backend {
1367 PresentBackend::Sync(gpu_renderer) => gpu_renderer.needs_frame_warmup(),
1368 #[cfg(not(target_arch = "wasm32"))]
1369 PresentBackend::Threaded(handle) => handle
1370 .status()
1371 .needs_frame_warmup
1372 .load(std::sync::atomic::Ordering::Relaxed),
1373 PresentBackend::None => false,
1374 }
1375 }
1376
1377 fn awaits_pipelines(&self) -> bool {
1378 match &self.backend {
1379 PresentBackend::Sync(gpu_renderer) => gpu_renderer.awaits_pipelines(),
1380 #[cfg(not(target_arch = "wasm32"))]
1381 PresentBackend::Threaded(handle) => {
1382 let status = handle.status();
1383 status
1384 .drew_placeholder
1385 .load(std::sync::atomic::Ordering::Relaxed)
1386 && status
1387 .landing
1388 .get()
1389 .is_some_and(|landing| landing.in_flight())
1390 }
1391 PresentBackend::None => false,
1392 }
1393 }
1394}
1395
1396#[cfg(test)]
1397#[path = "tests/wgpu_tests.rs"]
1398mod tests;