Skip to main content

cvkg_render_gpu/
surtr_util.rs

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