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