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