Skip to main content

cvkg_render_gpu/
surtr_util.rs

1//! Atlas loading and text shaping utilities.
2use crate::renderer::GpuRenderer;
3use cvkg_core::{Rect, Renderer};
4
5impl GpuRenderer {
6    /// load_image_to_heim -- Packs a raw asset into the Mega-Heim.
7    /// This is used for common icons to enable aggressive batching (1 draw call).
8    pub fn load_image_to_heim(&mut self, name: &str, data: &[u8]) {
9        if self.image_uv_registry.contains(name) {
10            tracing::info!(
11                "[Surtr] load_image_to_heim: '{}' already in registry, skipping",
12                name
13            );
14            return;
15        }
16        tracing::info!(
17            "[Surtr] load_image_to_heim: decoding '{}' ({} bytes)",
18            name,
19            data.len()
20        );
21        let img_result = image::load_from_memory(data);
22        let img = match img_result {
23            Ok(img) => {
24                tracing::info!("[Surtr] decode OK: {}x{}", img.width(), img.height());
25                img.to_rgba8()
26            }
27            Err(e) => {
28                tracing::error!("[Surtr] Failed to load image {} to heim: {}", name, e);
29                return;
30            }
31        };
32        let (width, height) = img.dimensions();
33
34        // Pack into heim
35        if let Some((x, y)) = self.heim_packer.pack(width, height) {
36            let tex_w = self.mega_heim_tex.width() as f32;
37            let tex_h = self.mega_heim_tex.height() as f32;
38            let uv_rect = Rect {
39                x: x as f32 / tex_w,
40                y: y as f32 / tex_h,
41                width: width as f32 / tex_w,
42                height: height as f32 / tex_h,
43            };
44
45            // Upload to GPU
46            self.queue.write_texture(
47                wgpu::TexelCopyTextureInfo {
48                    texture: &self.mega_heim_tex,
49                    mip_level: 0,
50                    origin: wgpu::Origin3d { x, y, z: 0 },
51                    aspect: wgpu::TextureAspect::All,
52                },
53                &img,
54                wgpu::TexelCopyBufferLayout {
55                    offset: 0,
56                    bytes_per_row: Some(4 * width),
57                    rows_per_image: Some(height),
58                },
59                wgpu::Extent3d {
60                    width,
61                    height,
62                    depth_or_array_layers: 1,
63                },
64            );
65
66            self.image_uv_registry.put(name.to_string(), uv_rect);
67            // Index 0 = mega-heim texture (stored in texture_views[0])
68            self.texture_registry.put(name.to_string(), 0);
69            tracing::info!("[Surtr] Packed '{}' into Mega-Heim at ({}, {})", name, x, y);
70            tracing::info!("[Surtr] Registry now contains '{}'", name);
71        } else {
72            tracing::warn!(
73                "HEIM_FULL: Failed to pack '{}' into Mega-Heim. Falling back to Texture Array.",
74                name
75            );
76            self.load_image(name, data);
77        }
78    }
79
80    /// Shapes a text string using a predefined system font stack.
81    ///
82    /// # Contract
83    /// Evaluates text shaping with fallbacks: queries "SF Pro Text", "SF Pro", "Inter",
84    /// "Helvetica Neue", "Helvetica", "Arial", and defaults back to "sans-serif".
85    /// This ensures visual typographic consistency across platforms where specific
86    /// branding faces may or may not be installed.
87    /// Shapes a text string using a default font stack.
88    ///
89    /// # Contract
90    /// Resolves standard font families in order of system availability. Falls back from
91    /// common system sans-serif aliases, to platform-specific sans-serif faces, and finally
92    /// to the embedded "Jupiteroid" font as a last resort.
93    /// Shapes a text string using a predefined system font stack.
94    ///
95    /// # Contract
96    /// Evaluates text shaping with fallbacks: queries "SF Pro Text", "SF Pro", "Inter",
97    /// "Helvetica Neue", "Helvetica", "Arial", and defaults back to "sans-serif".
98    /// This ensures visual typographic consistency across platforms where specific
99    /// branding faces may or may not be installed.
100    ///
101    /// The shaped text result is cached in `shaped_text_cache` by content and size.
102    /// This layout cache guarantees sub-millisecond execution times for subsequent
103    /// lookups, bypassing expensive font config fallback queries on repeating frames.
104    pub(crate) fn shape_text_with_stack(
105        &mut self,
106        text: &str,
107        size: f32,
108    ) -> std::sync::Arc<cvkg_runic_text::ShapedText> {
109        let cache_key = (text.to_string(), (size * 100.0) as u32);
110        if let Some(shaped) = self.text.shaped_cache.get(&cache_key) {
111            return shaped.clone();
112        }
113
114        let mut style = cvkg_runic_text::TextStyle::new("Jupiteroid", size);
115        style.fallback_families = vec![
116            "sans-serif".to_string(),
117            "DejaVu Sans".to_string(),
118            "Cantarell".to_string(),
119            "Liberation Sans".to_string(),
120            "Noto Sans".to_string(),
121            "Adwaita Sans".to_string(),
122            "SF Pro".to_string(),
123            "SF Pro Text".to_string(),
124            "Inter".to_string(),
125            "Helvetica Neue".to_string(),
126            "Helvetica".to_string(),
127            "Arial".to_string(),
128        ];
129        style.render_mode = cvkg_runic_text::RenderMode::Grayscale;
130        let spans = vec![cvkg_runic_text::TextSpan::new(text, style)];
131        let shaped = self
132            .text
133            .engine
134            .shape_layout(
135                &spans,
136                None,
137                cvkg_runic_text::TextAlign::Start,
138                cvkg_runic_text::TextOverflow::WordWrap,
139            )
140            .unwrap_or_else(|_| cvkg_runic_text::ShapedText {
141                glyphs: Vec::new(),
142                lines: Vec::new(),
143                width: 0.0,
144                height: 0.0,
145                text: text.to_string(),
146                spans: Vec::new(),
147                has_rtl: false,
148                ascent: 0.0,
149                descent: 0.0,
150                line_gap: 0.0,
151                grapheme_boundaries: vec![],
152            });
153
154        let arc = std::sync::Arc::new(shaped);
155        self.text.shaped_cache.put(cache_key, arc.clone());
156        arc
157    }
158}