1#![deny(unsafe_code)]
7
8mod effect_renderer;
9mod frame_graph;
10pub(crate) mod gpu_stats;
11mod layer_surface_cache;
12mod normalized_scene;
13mod offscreen;
14mod pipeline;
15mod render;
16mod scene;
17mod shader_cache;
18mod shaders;
19mod surface_executor;
20mod surface_plan;
21mod surface_requirements;
22#[cfg(test)]
23mod test_support;
24
25pub use gpu_stats::FrameStatsSnapshot as RenderStatsSnapshot;
26pub use scene::{ClickAction, HitRegion, Scene};
27
28use cranpose_core::{MemoryApplier, NodeId};
29use cranpose_render_common::{
30 graph::{
31 CachePolicy, DrawPrimitiveNode, IsolationReasons, LayerNode, PrimitiveEntry, PrimitiveNode,
32 PrimitivePhase, ProjectiveTransform, RenderGraph, RenderNode, TextPrimitiveNode,
33 },
34 raster_cache::LayerRasterCacheHashes,
35 software_text_raster::{
36 software_text_font_set_from_fonts_or_default, SoftwareTextFontSet, SoftwareTextMeasurer,
37 },
38 RenderScene, Renderer,
39};
40use cranpose_ui::{LayoutTree, TextMeasurer};
41use cranpose_ui_graphics::{
42 Brush, Color, CornerRadii, DrawPrimitive, GraphicsLayer, Point, Rect, Size,
43};
44use render::GpuRenderer;
45use std::rc::{Rc, Weak};
46use std::sync::Arc;
47
48pub(crate) fn rect_to_quad(rect: Rect) -> [[f32; 2]; 4] {
50 [
51 [rect.x, rect.y],
52 [rect.x + rect.width, rect.y],
53 [rect.x, rect.y + rect.height],
54 [rect.x + rect.width, rect.y + rect.height],
55 ]
56}
57
58#[derive(Debug)]
59pub enum WgpuRendererError {
60 Layout(String),
61 Wgpu(String),
62}
63
64#[derive(Debug, Clone)]
66pub struct CapturedFrame {
67 pub width: u32,
68 pub height: u32,
69 pub pixels: Vec<u8>,
70}
71
72#[doc(hidden)]
73#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
74pub struct DebugCpuAllocationStats {
75 pub scene_graph_node_count: usize,
76 pub scene_graph_heap_bytes: usize,
77 pub scene_hits_len: usize,
78 pub scene_hits_cap: usize,
79 pub scene_node_index_len: usize,
80 pub scene_node_index_cap: usize,
81 pub text_renderer_pool_len: usize,
82 pub text_renderer_pool_cap: usize,
83 pub swash_image_cache_len: usize,
84 pub swash_image_cache_cap: usize,
85 pub swash_outline_cache_len: usize,
86 pub swash_outline_cache_cap: usize,
87 pub image_texture_cache_len: usize,
88 pub image_texture_cache_cap: usize,
89 pub scratch_shape_data_cap: usize,
90 pub scratch_gradients_cap: usize,
91 pub scratch_vertices_cap: usize,
92 pub scratch_indices_cap: usize,
93 pub scratch_image_vertices_cap: usize,
94 pub scratch_image_indices_cap: usize,
95 pub scratch_image_cmds_cap: usize,
96 pub scratch_segment_items_cap: usize,
97 pub scratch_effect_ranges_cap: usize,
98 pub scratch_layer_events_cap: usize,
99 pub staged_upload_bytes_cap: usize,
100 pub staged_upload_copies_cap: usize,
101 pub layer_surface_cache_len: usize,
102 pub layer_surface_cache_cap: usize,
103 pub layer_surface_cache_identity_len: usize,
104 pub layer_surface_cache_identity_cap: usize,
105 pub layer_surface_rect_cache_len: usize,
106 pub layer_surface_rect_cache_cap: usize,
107 pub layer_surface_requirements_cache_len: usize,
108 pub layer_surface_requirements_cache_cap: usize,
109 pub layer_cache_seen_this_frame_len: usize,
110 pub layer_cache_seen_this_frame_cap: usize,
111}
112
113pub(crate) struct TextSystemState {
114 measurer: SoftwareTextMeasurer,
115}
116
117impl TextSystemState {
118 fn from_font_set(fonts: SoftwareTextFontSet) -> Self {
119 Self {
120 measurer: SoftwareTextMeasurer::from_font_set(fonts, 1024),
121 }
122 }
123
124 pub(crate) fn text_cache_len(&self) -> usize {
125 0
126 }
127}
128
129impl pipeline::TextLayoutResolver for TextSystemState {
130 fn layout_text(
131 &mut self,
132 text: &cranpose_ui::text::AnnotatedString,
133 style: &cranpose_ui::text::TextStyle,
134 ) -> cranpose_ui::text_layout_result::TextLayoutResult {
135 if cranpose_ui::has_current_app_context() {
136 cranpose_ui::text::layout_text(text, style)
137 } else {
138 self.measurer.layout(text, style)
139 }
140 }
141}
142
143#[derive(Clone)]
144pub struct WgpuTextSystem {
145 software_fonts: SoftwareTextFontSet,
146}
147
148impl WgpuTextSystem {
149 pub fn from_fonts(fonts: &[&[u8]]) -> Self {
150 Self {
151 software_fonts: software_text_font_set_from_fonts_or_default(fonts),
152 }
153 }
154
155 fn render_state(&self) -> TextSystemState {
156 TextSystemState::from_font_set(self.software_fonts.clone())
157 }
158
159 fn software_fonts(&self) -> SoftwareTextFontSet {
160 self.software_fonts.clone()
161 }
162}
163
164pub fn headless_text_measurer() -> Rc<dyn TextMeasurer> {
166 headless_text_measurer_with_fonts(&[])
167}
168
169pub fn headless_text_measurer_with_fonts(fonts: &[&[u8]]) -> Rc<dyn TextMeasurer> {
171 Rc::new(SoftwareTextMeasurer::from_fonts_or_default(fonts, 1024))
172}
173
174pub struct WgpuRenderer {
182 scene: Scene,
183 gpu_renderer: Option<GpuRenderer>,
184 text_state: TextSystemState,
185 text_fonts: SoftwareTextFontSet,
186 app_context: Option<Weak<cranpose_ui::AppContext>>,
187 root_scale: f32,
189}
190
191impl WgpuRenderer {
192 pub fn new(fonts: &[&[u8]]) -> Self {
199 Self::with_text_system(WgpuTextSystem::from_fonts(fonts))
200 }
201
202 pub fn with_text_system(text_system: WgpuTextSystem) -> Self {
203 Self {
204 scene: Scene::new(),
205 gpu_renderer: None,
206 text_state: text_system.render_state(),
207 text_fonts: text_system.software_fonts(),
208 app_context: None,
209 root_scale: 1.0,
210 }
211 }
212
213 pub fn init_gpu(
215 &mut self,
216 device: Arc<wgpu::Device>,
217 queue: Arc<wgpu::Queue>,
218 surface_format: wgpu::TextureFormat,
219 adapter_backend: wgpu::Backend,
220 ) {
221 self.gpu_renderer = Some(GpuRenderer::new(
222 device,
223 queue,
224 surface_format,
225 adapter_backend,
226 self.text_fonts.clone(),
227 ));
228 }
229
230 pub fn set_root_scale(&mut self, scale: f32) {
232 self.root_scale = scale;
233 }
234
235 pub fn root_scale(&self) -> f32 {
236 self.root_scale
237 }
238
239 pub fn render(
241 &mut self,
242 view: &wgpu::TextureView,
243 width: u32,
244 height: u32,
245 ) -> Result<(), WgpuRendererError> {
246 if let Some(gpu_renderer) = &mut self.gpu_renderer {
247 let graph = self
248 .scene
249 .graph
250 .as_ref()
251 .ok_or_else(|| WgpuRendererError::Wgpu("scene graph is missing".to_string()))?;
252 let text_state = &mut self.text_state;
253 let root_scale = self.root_scale;
254 let app_context = self.app_context.as_ref().and_then(Weak::upgrade);
255 let result = if let Some(app_context) = app_context {
256 app_context.enter(|| {
257 gpu_renderer.render(text_state, view, graph, width, height, root_scale)
258 })
259 } else {
260 gpu_renderer.render(text_state, view, graph, width, height, root_scale)
261 };
262 result.map_err(WgpuRendererError::Wgpu)
263 } else {
264 Err(WgpuRendererError::Wgpu(
265 "GPU renderer not initialized. Call init_gpu() first.".to_string(),
266 ))
267 }
268 }
269
270 pub fn capture_frame(
274 &mut self,
275 width: u32,
276 height: u32,
277 ) -> Result<CapturedFrame, WgpuRendererError> {
278 self.capture_frame_with_scale(width, height, self.root_scale)
279 }
280
281 pub fn capture_frame_with_scale(
283 &mut self,
284 width: u32,
285 height: u32,
286 root_scale: f32,
287 ) -> Result<CapturedFrame, WgpuRendererError> {
288 if let Some(gpu_renderer) = &mut self.gpu_renderer {
289 let graph = self
290 .scene
291 .graph
292 .as_ref()
293 .ok_or_else(|| WgpuRendererError::Wgpu("scene graph is missing".to_string()))?;
294 let text_state = &mut self.text_state;
295 let app_context = self.app_context.as_ref().and_then(Weak::upgrade);
296 let pixels = if let Some(app_context) = app_context {
297 app_context.enter(|| {
298 gpu_renderer.render_to_rgba_pixels(text_state, graph, width, height, root_scale)
299 })
300 } else {
301 gpu_renderer.render_to_rgba_pixels(text_state, graph, width, height, root_scale)
302 }
303 .map_err(WgpuRendererError::Wgpu)?;
304 Ok(CapturedFrame {
305 width,
306 height,
307 pixels,
308 })
309 } else {
310 Err(WgpuRendererError::Wgpu(
311 "GPU renderer not initialized. Call init_gpu() first.".to_string(),
312 ))
313 }
314 }
315
316 pub fn last_frame_stats(&self) -> Option<RenderStatsSnapshot> {
317 self.gpu_renderer
318 .as_ref()
319 .and_then(GpuRenderer::last_frame_stats)
320 }
321
322 pub fn debug_cpu_allocation_stats(&self) -> DebugCpuAllocationStats {
323 let mut stats = self
324 .gpu_renderer
325 .as_ref()
326 .map(GpuRenderer::debug_cpu_allocation_stats)
327 .unwrap_or_default();
328 stats.scene_graph_node_count = self
329 .scene
330 .graph
331 .as_ref()
332 .map(RenderGraph::node_count)
333 .unwrap_or(0);
334 stats.scene_graph_heap_bytes = self
335 .scene
336 .graph
337 .as_ref()
338 .map(RenderGraph::heap_bytes)
339 .unwrap_or(0);
340 stats.scene_hits_len = self.scene.hits.len();
341 stats.scene_hits_cap = self.scene.hits.capacity();
342 stats.scene_node_index_len = self.scene.node_index.len();
343 stats.scene_node_index_cap = self.scene.node_index.capacity();
344 stats
345 }
346
347 pub fn try_device(&self) -> Option<&wgpu::Device> {
349 self.gpu_renderer.as_ref().map(|r| &*r.device)
350 }
351}
352
353impl Default for WgpuRenderer {
354 fn default() -> Self {
355 Self::new(&[])
356 }
357}
358
359impl Renderer for WgpuRenderer {
360 type Scene = Scene;
361 type Error = WgpuRendererError;
362
363 fn attach_app_context_services(&mut self, app_context: &cranpose_ui::AppContext) {
364 app_context.set_text_measurer(SoftwareTextMeasurer::from_font_set(
365 self.text_fonts.clone(),
366 1024,
367 ));
368 self.app_context = Some(app_context.downgrade());
369 }
370
371 fn scene(&self) -> &Self::Scene {
372 &self.scene
373 }
374
375 fn scene_mut(&mut self) -> &mut Self::Scene {
376 &mut self.scene
377 }
378
379 fn rebuild_scene(
380 &mut self,
381 layout_tree: &LayoutTree,
382 _viewport: Size,
383 ) -> Result<(), Self::Error> {
384 self.scene.clear();
385 pipeline::render_layout_tree(layout_tree.root(), &mut self.scene);
387 Ok(())
388 }
389
390 fn rebuild_scene_from_applier(
391 &mut self,
392 applier: &mut MemoryApplier,
393 root: NodeId,
394 _viewport: Size,
395 ) -> Result<(), Self::Error> {
396 self.scene.clear();
397 pipeline::render_from_applier(applier, root, &mut self.scene, 1.0);
400 Ok(())
401 }
402
403 fn update_scene_from_applier(
404 &mut self,
405 applier: &mut MemoryApplier,
406 root: NodeId,
407 viewport: Size,
408 dirty_nodes: &[NodeId],
409 ) -> Result<(), Self::Error> {
410 if dirty_nodes.is_empty() {
411 return self.rebuild_scene_from_applier(applier, root, viewport);
412 }
413 pipeline::update_from_applier(applier, root, &mut self.scene, 1.0, dirty_nodes);
414 Ok(())
415 }
416
417 fn draw_dev_overlay(&mut self, text: &str, viewport: Size) {
418 const DEV_OVERLAY_NODE_ID: NodeId = NodeId::MAX;
419 let padding = 8.0;
420 let font_size = 14.0;
421 let char_width = 7.0;
422 let text_width = text.len() as f32 * char_width;
423 let text_height = font_size * 1.4;
424 let x = (viewport.width - text_width - padding * 2.0).max(padding);
425 let y = padding;
426
427 let mut overlay_layer = LayerNode {
428 node_id: Some(DEV_OVERLAY_NODE_ID),
429 local_bounds: Rect {
430 x: 0.0,
431 y: 0.0,
432 width: text_width + padding,
433 height: text_height + padding / 2.0,
434 },
435 transform_to_parent: ProjectiveTransform::translation(x, y),
436 motion_context_animated: false,
437 translated_content_context: false,
438 translated_content_offset: Point::default(),
439 graphics_layer: GraphicsLayer::default(),
440 clip_to_bounds: false,
441 shadow_clip: None,
442 hit_test: None,
443 has_hit_targets: false,
444 isolation: IsolationReasons::default(),
445 cache_policy: CachePolicy::None,
446 cache_hashes: LayerRasterCacheHashes::default(),
447 cache_hashes_valid: false,
448 children: vec![
449 RenderNode::Primitive(PrimitiveEntry {
450 phase: PrimitivePhase::BeforeChildren,
451 node: PrimitiveNode::Draw(DrawPrimitiveNode {
452 primitive: DrawPrimitive::RoundRect {
453 rect: Rect {
454 x: 0.0,
455 y: 0.0,
456 width: text_width + padding,
457 height: text_height + padding / 2.0,
458 },
459 brush: Brush::Solid(Color(0.0, 0.0, 0.0, 0.7)),
460 radii: CornerRadii::uniform(4.0),
461 },
462 clip: None,
463 }),
464 }),
465 RenderNode::Primitive(PrimitiveEntry {
466 phase: PrimitivePhase::AfterChildren,
467 node: PrimitiveNode::Text(Box::new(TextPrimitiveNode {
468 node_id: DEV_OVERLAY_NODE_ID,
469 rect: Rect {
470 x: padding / 2.0,
471 y: padding / 4.0,
472 width: text_width,
473 height: text_height,
474 },
475 text: cranpose_ui::text::AnnotatedString::from(text),
476 text_style: cranpose_ui::TextStyle::default(),
477 font_size,
478 layout_options: cranpose_ui::TextLayoutOptions::default(),
479 clip: None,
480 })),
481 }),
482 ],
483 };
484 overlay_layer.recompute_raster_cache_hashes();
485
486 let graph = self.scene.graph.get_or_insert_with(|| {
487 RenderGraph::new(LayerNode {
488 node_id: None,
489 local_bounds: Rect::from_size(viewport),
490 transform_to_parent: ProjectiveTransform::identity(),
491 motion_context_animated: false,
492 translated_content_context: false,
493 translated_content_offset: Point::default(),
494 graphics_layer: GraphicsLayer::default(),
495 clip_to_bounds: false,
496 shadow_clip: None,
497 hit_test: None,
498 has_hit_targets: false,
499 isolation: IsolationReasons::default(),
500 cache_policy: CachePolicy::None,
501 cache_hashes: LayerRasterCacheHashes::default(),
502 cache_hashes_valid: false,
503 children: Vec::new(),
504 })
505 });
506
507 graph.root.children.retain(|child| {
508 !matches!(
509 child,
510 RenderNode::Layer(layer) if layer.node_id == Some(DEV_OVERLAY_NODE_ID)
511 )
512 });
513 graph
514 .root
515 .children
516 .push(RenderNode::Layer(Box::new(overlay_layer)));
517 graph.root.recompute_raster_cache_hashes();
518 }
519}
520
521#[cfg(test)]
522mod tests {
523 use super::*;
524 use crate::pipeline::TextLayoutResolver;
525 use std::cell::Cell;
526
527 static TEST_FONT: &[u8] =
528 cranpose_render_common::software_text_raster::DEFAULT_SOFTWARE_TEXT_FONT_BYTES;
529
530 struct CountingTextMeasurer {
531 inner: SoftwareTextMeasurer,
532 layout_calls: Rc<Cell<usize>>,
533 }
534
535 impl CountingTextMeasurer {
536 fn new(layout_calls: Rc<Cell<usize>>) -> Self {
537 Self {
538 inner: SoftwareTextMeasurer::from_fonts_or_default(&[TEST_FONT], 16),
539 layout_calls,
540 }
541 }
542 }
543
544 impl TextMeasurer for CountingTextMeasurer {
545 fn measure(
546 &self,
547 text: &cranpose_ui::text::AnnotatedString,
548 style: &cranpose_ui::text::TextStyle,
549 ) -> cranpose_ui::TextMetrics {
550 self.inner.measure(text, style)
551 }
552
553 fn get_offset_for_position(
554 &self,
555 text: &cranpose_ui::text::AnnotatedString,
556 style: &cranpose_ui::text::TextStyle,
557 x: f32,
558 y: f32,
559 ) -> usize {
560 self.inner.get_offset_for_position(text, style, x, y)
561 }
562
563 fn get_cursor_x_for_offset(
564 &self,
565 text: &cranpose_ui::text::AnnotatedString,
566 style: &cranpose_ui::text::TextStyle,
567 offset: usize,
568 ) -> f32 {
569 self.inner.get_cursor_x_for_offset(text, style, offset)
570 }
571
572 fn layout(
573 &self,
574 text: &cranpose_ui::text::AnnotatedString,
575 style: &cranpose_ui::text::TextStyle,
576 ) -> cranpose_ui::text_layout_result::TextLayoutResult {
577 self.layout_calls.set(self.layout_calls.get() + 1);
578 self.inner.layout(text, style)
579 }
580 }
581
582 #[test]
583 fn headless_text_measurer_uses_software_text_font() {
584 let measurer = headless_text_measurer_with_fonts(&[TEST_FONT]);
585 let text = cranpose_ui::text::AnnotatedString::from("software text measurement");
586 let style = cranpose_ui::text::TextStyle::default();
587
588 let metrics = measurer.measure(&text, &style);
589 let layout = measurer.layout(&text, &style);
590
591 assert!(metrics.width > 0.0);
592 assert!(metrics.height > 0.0);
593 assert_eq!(layout.lines.len(), metrics.line_count);
594 }
595
596 #[test]
597 fn renderer_measurement_uses_software_text_service_without_render_cache_side_effect() {
598 let mut renderer = WgpuRenderer::new(&[TEST_FONT]);
599 let app_context = cranpose_ui::AppContext::new();
600 renderer.attach_app_context_services(&app_context);
601
602 let metrics = app_context.enter(|| {
603 let text = cranpose_ui::text::AnnotatedString::from("phase local text cache");
604 let style = cranpose_ui::text::TextStyle::default();
605 cranpose_ui::text::measure_text(&text, &style)
606 });
607
608 assert!(
609 metrics.width > 0.0,
610 "software text service should measure text"
611 );
612 assert_eq!(
613 renderer.text_state.text_cache_len(),
614 0,
615 "WGPU must not keep a renderer-side shaping cache for measurement"
616 );
617 }
618
619 #[test]
620 fn render_text_layout_routes_through_attached_app_context_service() {
621 let mut renderer = WgpuRenderer::new(&[TEST_FONT]);
622 let app_context = cranpose_ui::AppContext::new();
623 renderer.attach_app_context_services(&app_context);
624 let layout_calls = Rc::new(Cell::new(0));
625 app_context.set_text_measurer(CountingTextMeasurer::new(Rc::clone(&layout_calls)));
626
627 app_context.enter(|| {
628 let text = cranpose_ui::text::AnnotatedString::from("render text");
629 let style = cranpose_ui::text::TextStyle::default();
630 let layout = renderer.text_state.layout_text(&text, &style);
631 assert!(layout.width > 0.0);
632 });
633
634 assert_eq!(layout_calls.get(), 1);
635 }
636}