Skip to main content

oxitext_sdf/
gpu.rs

1//! GPU-ready atlas descriptor types.
2//!
3//! Provides a backend-agnostic description of an SDF or MSDF atlas texture.
4//! Callers can pass the [`GpuAtlasDescriptor`] directly to wgpu, Vulkan, Metal,
5//! or OpenGL without any dependency on those crates from this library.
6
7use std::collections::HashMap;
8
9use crate::atlas::{MsdfAtlas, SdfAtlas, UvRect};
10
11// ─── Format ───────────────────────────────────────────────────────────────────
12
13/// Pixel format for GPU atlas texture upload.
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum GpuAtlasFormat {
16    /// Single-channel greyscale (1 byte/pixel) — for single-channel SDF.
17    R8Unorm,
18    /// Three-channel RGB (3 bytes/pixel) — for MSDF.
19    Rgb8Unorm,
20}
21
22// ─── Normalized UV ────────────────────────────────────────────────────────────
23
24/// UV rectangle in normalized [0, 1] texture coordinates.
25///
26/// All four fields are in the range `[0.0, 1.0]` where `(0, 0)` is the
27/// top-left corner and `(1, 1)` is the bottom-right corner of the texture.
28#[derive(Debug, Clone, Copy)]
29pub struct NormalizedUvRect {
30    /// Left edge (U coordinate, column-axis).
31    pub u_min: f32,
32    /// Top edge (V coordinate, row-axis).
33    pub v_min: f32,
34    /// Right edge (U coordinate).
35    pub u_max: f32,
36    /// Bottom edge (V coordinate).
37    pub v_max: f32,
38}
39
40impl From<&UvRect> for NormalizedUvRect {
41    /// Convert from the atlas-internal [`UvRect`] (which already stores [0, 1] values).
42    fn from(r: &UvRect) -> Self {
43        Self {
44            u_min: r.u_min,
45            v_min: r.v_min,
46            u_max: r.u_max,
47            v_max: r.v_max,
48        }
49    }
50}
51
52// ─── Glyph metrics ────────────────────────────────────────────────────────────
53
54/// Per-glyph metrics stored alongside the atlas for rendering.
55///
56/// These are the values needed by a text renderer to correctly position and
57/// size each glyph quad relative to the cursor position.
58#[derive(Debug, Clone, Copy)]
59pub struct AtlasGlyphMetrics {
60    /// Horizontal offset from the cursor to the left edge of the glyph (pixels, signed).
61    pub bearing_x: f32,
62    /// Vertical offset from the baseline to the top edge of the glyph (pixels, signed).
63    pub bearing_y: f32,
64    /// Horizontal advance to the next cursor position (pixels).
65    pub advance_x: f32,
66    /// Glyph width in pixels (matches the packed tile width).
67    pub width_px: u32,
68    /// Glyph height in pixels (matches the packed tile height).
69    pub height_px: u32,
70}
71
72// ─── Descriptor ───────────────────────────────────────────────────────────────
73
74/// A GPU-ready atlas descriptor: all the information needed to upload the atlas
75/// to a GPU texture (wgpu, Vulkan, Metal, OpenGL) without depending on any GPU crate.
76///
77/// # Usage
78/// ```rust
79/// use oxitext_sdf::{SdfAtlas, GpuAtlasFormat};
80///
81/// let atlas = SdfAtlas::new(256, 256);
82/// let desc = atlas.to_gpu_descriptor();
83/// assert_eq!(desc.format, GpuAtlasFormat::R8Unorm);
84/// assert_eq!(desc.data.len(), 256 * 256);
85/// ```
86#[derive(Debug, Clone)]
87pub struct GpuAtlasDescriptor {
88    /// Atlas texture width in pixels.
89    pub width: u32,
90    /// Atlas texture height in pixels.
91    pub height: u32,
92    /// Pixel format of the texture data.
93    pub format: GpuAtlasFormat,
94    /// Raw texture bytes (`width × height × bytes_per_pixel`).
95    pub data: Vec<u8>,
96    /// UV rectangle for each glyph ID, in normalized [0, 1] texture coordinates.
97    pub uv_map: HashMap<u16, NormalizedUvRect>,
98    /// Per-glyph metrics in atlas space (bearing, advance in pixels).
99    ///
100    /// Currently populated only when the atlas was built via APIs that retain
101    /// tile metrics. Empty when tile metrics were not preserved by the packer.
102    pub glyph_metrics: HashMap<u16, AtlasGlyphMetrics>,
103}
104
105// ─── SdfAtlas → GpuAtlasDescriptor ───────────────────────────────────────────
106
107impl SdfAtlas {
108    /// Produce a GPU-ready descriptor containing the atlas texture and normalized UV coordinates.
109    ///
110    /// The returned struct contains everything needed to upload the atlas to a GPU texture.
111    /// `glyph_metrics` will be empty because the shelf packer does not retain per-tile metrics
112    /// after the atlas is built. If you need metrics, store the [`SdfTile`] list alongside the
113    /// atlas before calling `pack`.
114    ///
115    /// [`SdfTile`]: crate::SdfTile
116    pub fn to_gpu_descriptor(&self) -> GpuAtlasDescriptor {
117        let uv_map: HashMap<u16, NormalizedUvRect> = self
118            .uv_map
119            .iter()
120            .map(|(&glyph_id, uv)| (glyph_id, NormalizedUvRect::from(uv)))
121            .collect();
122
123        GpuAtlasDescriptor {
124            width: self.width,
125            height: self.height,
126            format: GpuAtlasFormat::R8Unorm,
127            data: self.texture.clone(),
128            uv_map,
129            glyph_metrics: HashMap::new(),
130        }
131    }
132}
133
134// ─── MsdfAtlas → GpuAtlasDescriptor ──────────────────────────────────────────
135
136impl MsdfAtlas {
137    /// Produce a GPU-ready descriptor for this MSDF atlas.
138    ///
139    /// Returns [`GpuAtlasFormat::Rgb8Unorm`] (3 bytes per pixel) with the raw
140    /// RGB texture data.  `glyph_metrics` is empty for the same reason as for
141    /// [`SdfAtlas::to_gpu_descriptor`].
142    pub fn to_gpu_descriptor(&self) -> GpuAtlasDescriptor {
143        let uv_map: HashMap<u16, NormalizedUvRect> = self
144            .uv_map
145            .iter()
146            .map(|(&glyph_id, uv)| (glyph_id, NormalizedUvRect::from(uv)))
147            .collect();
148
149        GpuAtlasDescriptor {
150            width: self.width,
151            height: self.height,
152            format: GpuAtlasFormat::Rgb8Unorm,
153            data: self.texture.clone(),
154            uv_map,
155            glyph_metrics: HashMap::new(),
156        }
157    }
158}
159
160// ─── Tests ────────────────────────────────────────────────────────────────────
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165    use crate::atlas::{SdfAtlas, SdfTile};
166
167    fn make_tile(glyph_id: u16, w: u32, h: u32) -> SdfTile {
168        SdfTile {
169            glyph_id,
170            width: w,
171            height: h,
172            data: vec![128u8; (w * h) as usize],
173            bearing_x: 0,
174            bearing_y: 0,
175            advance_x: w as f32,
176        }
177    }
178
179    #[test]
180    fn test_gpu_descriptor_uv_normalized() {
181        let atlas = SdfAtlas::new(256, 256);
182        let desc = atlas.to_gpu_descriptor();
183        assert_eq!(desc.width, 256);
184        assert_eq!(desc.height, 256);
185        assert_eq!(desc.format, GpuAtlasFormat::R8Unorm);
186        assert_eq!(desc.data.len(), 256 * 256);
187        // No tiles were packed — uv_map should be empty.
188        assert!(desc.uv_map.is_empty());
189    }
190
191    #[test]
192    fn test_gpu_descriptor_uv_values_in_range() {
193        let tiles = [make_tile(0, 16, 16), make_tile(1, 32, 32)];
194        let atlas = SdfAtlas::pack(&tiles);
195        let desc = atlas.to_gpu_descriptor();
196
197        for uv in desc.uv_map.values() {
198            assert!(
199                uv.u_min >= 0.0 && uv.u_min <= 1.0,
200                "u_min out of range: {}",
201                uv.u_min
202            );
203            assert!(
204                uv.u_max >= 0.0 && uv.u_max <= 1.0,
205                "u_max out of range: {}",
206                uv.u_max
207            );
208            assert!(
209                uv.v_min >= 0.0 && uv.v_min <= 1.0,
210                "v_min out of range: {}",
211                uv.v_min
212            );
213            assert!(
214                uv.v_max >= 0.0 && uv.v_max <= 1.0,
215                "v_max out of range: {}",
216                uv.v_max
217            );
218            assert!(uv.u_min <= uv.u_max, "u_min > u_max");
219            assert!(uv.v_min <= uv.v_max, "v_min > v_max");
220        }
221    }
222
223    #[test]
224    fn test_msdf_gpu_descriptor_format() {
225        let atlas = MsdfAtlas {
226            width: 128,
227            height: 128,
228            texture: vec![0u8; 128 * 128 * 3],
229            uv_map: HashMap::new(),
230        };
231        let desc = atlas.to_gpu_descriptor();
232        assert_eq!(desc.format, GpuAtlasFormat::Rgb8Unorm);
233        assert_eq!(desc.width, 128);
234        assert_eq!(desc.height, 128);
235        assert_eq!(desc.data.len(), 128 * 128 * 3);
236        assert!(desc.uv_map.is_empty());
237    }
238
239    #[test]
240    fn test_normalized_uv_rect_from_uv_rect() {
241        let uv = UvRect {
242            u_min: 0.1,
243            v_min: 0.2,
244            u_max: 0.5,
245            v_max: 0.8,
246        };
247        let norm = NormalizedUvRect::from(&uv);
248        assert!((norm.u_min - 0.1).abs() < f32::EPSILON);
249        assert!((norm.v_min - 0.2).abs() < f32::EPSILON);
250        assert!((norm.u_max - 0.5).abs() < f32::EPSILON);
251        assert!((norm.v_max - 0.8).abs() < f32::EPSILON);
252    }
253}