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;
49#[cfg(not(target_arch = "wasm32"))]
50mod pipeline_records;
51#[cfg(not(target_arch = "wasm32"))]
52mod present_runtime;
53mod record_columns;
54mod render;
55mod rrect_shadow;
56mod run_geometry;
57mod run_store;
58mod scene;
59mod shader_cache;
60#[cfg(not(target_arch = "wasm32"))]
61mod shader_fold;
62mod shaders;
63mod shape_pipelines;
64mod shared_shader;
65#[cfg(test)]
66mod test_support;
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 gpu_pass_timings(&self) -> GpuPassTimingReport {
973 self.sync_gpu_renderer()
974 .map(GpuRenderer::gpu_pass_timings)
975 .unwrap_or_default()
976 }
977
978 pub fn debug_cpu_allocation_stats(&self) -> DebugCpuAllocationStats {
979 let mut stats = self
980 .sync_gpu_renderer()
981 .map(GpuRenderer::debug_cpu_allocation_stats)
982 .unwrap_or_default();
983 stats.scene_graph_node_count = self
984 .frontend
985 .scene
986 .graph
987 .as_ref()
988 .map_or(0, RenderGraph::node_count);
989 stats.scene_graph_heap_bytes = self
990 .frontend
991 .scene
992 .graph
993 .as_ref()
994 .map_or(0, RenderGraph::heap_bytes);
995 stats.scene_hits_len = self.frontend.scene.hits.len();
996 stats.scene_hits_cap = self.frontend.scene.hits.capacity();
997 stats.scene_node_index_len = self.frontend.scene.node_index.len();
998 stats.scene_node_index_cap = self.frontend.scene.node_index.capacity();
999 stats
1000 }
1001
1002 #[cfg(target_arch = "wasm32")]
1008 pub fn begin_discovery_frame(&self) {
1009 if let Some(gpu_renderer) = self.sync_gpu_renderer() {
1010 gpu_renderer.pipeline_compiler().begin_discovery_frame();
1011 }
1012 }
1013
1014 pub fn try_device(&self) -> Option<&wgpu::Device> {
1018 self.sync_gpu_renderer().map(|r| &*r.device)
1019 }
1020
1021 #[doc(hidden)]
1022 pub fn try_queue_for_tests(&self) -> Option<&wgpu::Queue> {
1023 self.sync_gpu_renderer().map(|r| &*r.queue)
1024 }
1025
1026 #[doc(hidden)]
1027 pub fn device_error_count_for_tests(&self) -> u64 {
1028 self.sync_gpu_renderer()
1029 .map_or(0, GpuRenderer::device_error_count)
1030 }
1031
1032 #[doc(hidden)]
1033 pub fn build_frame_packet_for_tests(
1034 &mut self,
1035 width: u32,
1036 height: u32,
1037 ) -> Option<HeldFramePacket> {
1038 self.frontend
1039 .build_frame_packet(width, height, self.renderer_epoch, self.surface_epoch)
1040 .map(HeldFramePacket)
1041 }
1042
1043 #[doc(hidden)]
1055 pub fn return_held_packet_for_tests(&mut self, packet: HeldFramePacket) {
1056 let packet = packet.0;
1057 self.frontend.apply_returns(RenderReturns {
1058 scene: Some(frame_packet::FrameSceneStorage {
1059 root: packet.root,
1060 overlay: packet.overlay,
1061 }),
1062 frame_id: packet.frame_id,
1063 ..RenderReturns::default()
1064 });
1065 }
1066
1067 #[doc(hidden)]
1068 pub fn render_held_packet_for_tests(
1069 &mut self,
1070 texture: &wgpu::Texture,
1071 view: &wgpu::TextureView,
1072 width: u32,
1073 height: u32,
1074 packet: HeldFramePacket,
1075 ) -> Result<PresentOutcome, WgpuRendererError> {
1076 let PresentBackend::Sync(gpu_renderer) = &mut self.backend else {
1077 return Err(WgpuRendererError::Wgpu(
1078 "GPU renderer not initialized for synchronous rendering. Call init_gpu() first."
1079 .to_string(),
1080 ));
1081 };
1082 let mut returns = RenderReturns::default();
1083 let result = gpu_renderer.render(
1084 texture,
1085 view,
1086 width,
1087 height,
1088 packet.0,
1089 self.surface_epoch,
1090 &mut returns,
1091 );
1092 let outcome = returns.outcome;
1093 self.frontend.apply_returns(returns);
1094 result.map_err(WgpuRendererError::Wgpu)?;
1095 Ok(outcome)
1096 }
1097}
1098
1099#[doc(hidden)]
1100pub struct HeldFramePacket(frame_packet::FramePacket);
1101
1102#[cfg(not(target_arch = "wasm32"))]
1103#[doc(hidden)]
1104pub struct InlinePresentRuntime {
1105 state: PresentState,
1106 msg_rx: std::sync::mpsc::Receiver<PresentMsg>,
1107 shutdown_seen: bool,
1108}
1109
1110#[cfg(not(target_arch = "wasm32"))]
1111impl InlinePresentRuntime {
1112 pub fn pump(&mut self) -> bool {
1113 if self.shutdown_seen {
1114 return false;
1115 }
1116 while let Ok(msg) = self.msg_rx.try_recv() {
1117 if !self.state.run_once(msg) {
1118 self.shutdown_seen = true;
1119 return false;
1120 }
1121 }
1122 self.state.consume_waiting();
1123 true
1124 }
1125
1126 pub fn has_waiting_packet(&self) -> bool {
1127 self.state.has_waiting_packet()
1128 }
1129
1130 pub fn step_one_message(&mut self) -> bool {
1131 if self.shutdown_seen {
1132 return false;
1133 }
1134 match self.msg_rx.try_recv() {
1135 Ok(msg) => {
1136 if !self.state.run_once(msg) {
1137 self.shutdown_seen = true;
1138 }
1139 true
1140 }
1141 Err(_) => false,
1142 }
1143 }
1144
1145 pub fn consume_waiting(&mut self) {
1146 self.state.consume_waiting();
1147 }
1148}
1149
1150impl Default for WgpuRenderer {
1151 fn default() -> Self {
1152 Self::new(&[])
1153 }
1154}
1155
1156impl Renderer for WgpuRenderer {
1157 type Scene = Scene;
1158 type Error = WgpuRendererError;
1159
1160 fn attach_app_context_services(&mut self, app_context: &cranpose_ui::AppContext) {
1161 app_context.set_text_measurer(SoftwareTextMeasurer::from_font_set(
1162 self.frontend.text_fonts.clone(),
1163 8192,
1164 ));
1165 self.frontend.app_context = Some(app_context.downgrade());
1166 }
1167
1168 fn set_root_scale(&mut self, scale: f32) {
1169 self.frontend.root_scale = scale;
1170 }
1171
1172 fn scene(&self) -> &Self::Scene {
1173 &self.frontend.scene
1174 }
1175
1176 fn scene_mut(&mut self) -> &mut Self::Scene {
1177 &mut self.frontend.scene
1178 }
1179
1180 fn rebuild_scene(
1181 &mut self,
1182 layout_tree: &LayoutTree,
1183 _viewport: Size,
1184 ) -> Result<(), Self::Error> {
1185 self.frontend.scene.clear();
1186 self.frontend.clear_fps_overlay();
1187 pipeline::render_layout_tree(layout_tree.root(), &mut self.frontend.scene);
1188 Ok(())
1189 }
1190
1191 fn rebuild_scene_from_applier(
1192 &mut self,
1193 applier: &mut MemoryApplier,
1194 root: NodeId,
1195 _viewport: Size,
1196 ) -> Result<(), Self::Error> {
1197 self.frontend.clear_fps_overlay();
1198 self.frontend.scene.rebuild_from_applier(applier, root);
1199 Ok(())
1200 }
1201
1202 fn update_scene_from_applier(
1203 &mut self,
1204 applier: &mut MemoryApplier,
1205 root: NodeId,
1206 _viewport: Size,
1207 updates: cranpose_render_common::SceneUpdates<'_>,
1208 ) -> Result<(), Self::Error> {
1209 if updates.is_empty() {
1210 self.frontend.clear_fps_overlay();
1211 }
1212 self.frontend
1213 .scene
1214 .update_from_applier(applier, root, updates, true);
1215 Ok(())
1216 }
1217
1218 fn update_visual_scene_from_applier(
1219 &mut self,
1220 applier: &mut MemoryApplier,
1221 root: NodeId,
1222 _viewport: Size,
1223 updates: cranpose_render_common::SceneUpdates<'_>,
1224 ) -> Result<(), Self::Error> {
1225 if updates.is_empty() {
1226 self.frontend.clear_fps_overlay();
1227 }
1228 self.frontend
1229 .scene
1230 .update_from_applier(applier, root, updates, false);
1231 Ok(())
1232 }
1233
1234 fn draw_dev_overlay(&mut self, text: &str, viewport: Size) {
1235 const DEV_OVERLAY_NODE_ID: NodeId = NodeId::MAX;
1236 let key = cranpose_render_common::dev_overlay::DevOverlayKey::new(text, viewport);
1237 if self.frontend.dev_overlay_graph.is_some()
1238 && self
1239 .frontend
1240 .dev_overlay_cache
1241 .as_ref()
1242 .is_some_and(|cache| {
1243 cache.text == key.text
1244 && cache.viewport_width_bits == key.viewport_width_bits
1245 && cache.viewport_height_bits == key.viewport_height_bits
1246 })
1247 {
1248 return;
1249 }
1250 self.frontend.fps_overlay_graph = Some(
1251 cranpose_render_common::dev_overlay::build_dev_overlay_graph(
1252 text,
1253 viewport,
1254 DEV_OVERLAY_NODE_ID,
1255 ),
1256 );
1257 self.frontend.dev_overlay_cache = Some(DevOverlayCache {
1258 text: key.text,
1259 viewport_width_bits: key.viewport_width_bits,
1260 viewport_height_bits: key.viewport_height_bits,
1261 });
1262 self.frontend.refresh_dev_overlay();
1263 }
1264
1265 fn set_inspector_overlay(&mut self, graph: Option<cranpose_render_common::graph::RenderGraph>) {
1266 self.frontend.inspector_overlay_graph = graph;
1267 self.frontend.refresh_dev_overlay();
1268 }
1269
1270 fn needs_frame_warmup(&self) -> bool {
1271 match &self.backend {
1272 PresentBackend::Sync(gpu_renderer) => gpu_renderer.needs_frame_warmup(),
1273 #[cfg(not(target_arch = "wasm32"))]
1274 PresentBackend::Threaded(handle) => handle
1275 .status()
1276 .needs_frame_warmup
1277 .load(std::sync::atomic::Ordering::Relaxed),
1278 PresentBackend::None => false,
1279 }
1280 }
1281
1282 fn awaits_pipelines(&self) -> bool {
1283 match &self.backend {
1284 PresentBackend::Sync(gpu_renderer) => gpu_renderer.awaits_pipelines(),
1285 #[cfg(not(target_arch = "wasm32"))]
1286 PresentBackend::Threaded(handle) => {
1287 let status = handle.status();
1288 status
1289 .drew_placeholder
1290 .load(std::sync::atomic::Ordering::Relaxed)
1291 && status
1292 .landing
1293 .get()
1294 .is_some_and(|landing| landing.in_flight())
1295 }
1296 PresentBackend::None => false,
1297 }
1298 }
1299}
1300
1301#[cfg(test)]
1302#[path = "tests/wgpu_tests.rs"]
1303mod tests;