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}