1
//! Glyph path and cell cache for CPU rendering.
2
//!
3
//! Two-level cache:
4
//! 1. **Path cache**: `PathStorage` objects keyed by (font, glyph, ppem).
5
//!    Avoids redundant path construction from font outlines.
6
//! 2. **Cell cache**: Rasterizer cells keyed by (font, glyph, ppem, scale, sub-pixel).
7
//!    Avoids the expensive path→cells conversion on every frame.
8
//!    Cells are computed at position (0,0) and offset at render time.
9

            
10
use std::collections::HashMap;
11

            
12
use agg_rust::path_storage::PathStorage;
13
use agg_rust::rasterizer_cells_aa::CellAa;
14

            
15
use crate::font::parsed::{build_glyph_path, OwnedGlyph, ParsedFont};
16

            
17
/// Cache key for a glyph path.
18
/// ppem = 0 means unhinted (font-unit path), ppem > 0 means hinted at that size.
19
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
20
pub struct GlyphPathKey {
21
    pub font_hash: u64,
22
    pub glyph_id: u16,
23
    pub ppem: u16,
24
}
25

            
26
/// Cache key for pre-rasterized glyph cells.
27
/// Includes sub-pixel x/y fractional position quantized to 1/4 pixel.
28
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
29
pub struct GlyphCellKey {
30
    pub font_hash: u64,
31
    pub glyph_id: u16,
32
    pub ppem: u16,
33
    /// Scale factor encoded as fixed-point (scale * 65536) for unhinted glyphs.
34
    /// 0 for hinted glyphs (already in pixel coords).
35
    pub scale_fixed: u32,
36
    /// Sub-pixel x position quantized to 1/4 pixel (0..3).
37
    pub subpx_x: u8,
38
    /// Sub-pixel y position quantized to 1/4 pixel (0..3).
39
    pub subpx_y: u8,
40
}
41

            
42
/// Result of a cache lookup: the path plus whether it's hinted (pixel coords) or not.
43
pub struct CachedGlyph<'a> {
44
    pub path: &'a PathStorage,
45
    pub is_hinted: bool,
46
}
47

            
48
/// Pre-rasterized glyph cells at a canonical position.
49
/// Contains the rasterizer's cell output for a glyph at sub-pixel position (subpx_x, subpx_y).
50
/// To render at actual position (x, y), add integer pixel offset to each cell.
51
pub struct CachedCells {
52
    pub cells: Vec<CellAa>,
53
}
54

            
55
/// Maximum number of glyph path entries before eviction.
56
/// ~8K glyphs covers most Latin + CJK pages without unbounded growth.
57
const MAX_PATH_ENTRIES: usize = 8192;
58
/// Maximum number of cell cache entries before eviction.
59
/// Cell entries are larger than paths, so a lower limit is appropriate.
60
const MAX_CELL_ENTRIES: usize = 16384;
61

            
62
/// Cache of built glyph paths and pre-rasterized cells.
63
pub struct GlyphCache {
64
    paths: HashMap<GlyphPathKey, Option<(PathStorage, bool)>>,
65
    cells: HashMap<GlyphCellKey, Option<CachedCells>>,
66
}
67

            
68
/// Quantize a fractional pixel position to 1/4 pixel (0..3).
69
#[inline]
70
1760
fn quantize_subpx(frac: f32) -> u8 {
71
1760
    let f = frac - frac.floor();
72
1760
    (f * 4.0).min(3.0) as u8
73
1760
}
74

            
75
impl GlyphCache {
76
    #[must_use]
77
1320
    pub fn new() -> Self {
78
1320
        Self {
79
1320
            paths: HashMap::new(),
80
1320
            cells: HashMap::new(),
81
1320
        }
82
1320
    }
83

            
84
    /// Entry count of the glyph-path cache (for leak probes).
85
    pub fn paths_len(&self) -> usize { self.paths.len() }
86

            
87
    /// Entry count of the pre-rasterized cell cache (for leak probes).
88
    pub fn cells_len(&self) -> usize { self.cells.len() }
89

            
90
    /// Get a cached path, or build it on cache miss.
91
    /// Returns `None` if the glyph has no outline (e.g. space character).
92
12848
    pub fn get_or_build(
93
12848
        &mut self,
94
12848
        font_hash: u64,
95
12848
        glyph_id: u16,
96
12848
        glyph_data: &OwnedGlyph,
97
12848
        parsed_font: &ParsedFont,
98
12848
        ppem: u16,
99
12848
    ) -> Option<CachedGlyph<'_>> {
100
12848
        if self.paths.len() >= MAX_PATH_ENTRIES {
101
            self.paths.clear();
102
12848
        }
103
12848
        let key = GlyphPathKey { font_hash, glyph_id, ppem };
104
12848
        let entry = self
105
12848
            .paths
106
12848
            .entry(key)
107
12848
            .or_insert_with(|| {
108
                // Try hinted path first if ppem > 0
109
5412
                if ppem > 0 {
110
5412
                    if let Some(path) = build_hinted_path(glyph_data, parsed_font, ppem) {
111
5148
                        return Some((path, true));
112
264
                    }
113
                }
114
                // Fall back to unhinted path
115
264
                build_glyph_path(glyph_data).map(|p| (p, false))
116
5412
            });
117
12848
        entry.as_ref().map(|(path, is_hinted)| CachedGlyph {
118
12056
            path,
119
12056
            is_hinted: *is_hinted,
120
12056
        })
121
12848
    }
122

            
123
    /// Get cached rasterizer cells for a glyph, or build them from the path.
124
    ///
125
    /// - `glyph_x`, `glyph_y`: final pixel position (used for sub-pixel quantization)
126
    /// - `scale`: font-unit→pixel scale (0.0 for hinted glyphs)
127
    /// - `is_hinted`: whether the path is in pixel coords (hinted) or font units
128
    ///
129
    /// Returns the cached cells and the integer pixel offset to apply.
130
12848
    pub fn get_or_build_cells(
131
12848
        &mut self,
132
12848
        font_hash: u64,
133
12848
        glyph_id: u16,
134
12848
        ppem: u16,
135
12848
        glyph_x: f32,
136
12848
        glyph_y: f32,
137
12848
        scale: f32,
138
12848
        is_hinted: bool,
139
12848
    ) -> Option<(&[CellAa], i32, i32)> {
140
12848
        if self.cells.len() >= MAX_CELL_ENTRIES {
141
            self.cells.clear();
142
12848
        }
143
12848
        let subpx_x = if is_hinted { 0 } else { quantize_subpx(glyph_x) };
144
12848
        let subpx_y = if is_hinted { 0 } else { quantize_subpx(glyph_y) };
145
12848
        debug_assert!(scale >= 0.0 && scale < 65536.0, "scale out of range for fixed-point: {}", scale);
146
12848
        let scale_fixed = if is_hinted { 0 } else { (scale * 65536.0) as u32 };
147

            
148
12848
        let cell_key = GlyphCellKey {
149
12848
            font_hash, glyph_id, ppem, scale_fixed, subpx_x, subpx_y,
150
12848
        };
151

            
152
        // Integer pixel offset — the cells are at sub-pixel origin, offset by int part
153
12848
        let int_x = if is_hinted { glyph_x.round() as i32 } else { glyph_x.floor() as i32 };
154
12848
        let int_y = if is_hinted { glyph_y.round() as i32 } else { glyph_y.floor() as i32 };
155

            
156
12848
        if !self.cells.contains_key(&cell_key) {
157
            // Build cells from cached path
158
5500
            let path_key = GlyphPathKey { font_hash, glyph_id, ppem };
159
5500
            let path_entry = self.paths.get(&path_key);
160
5500
            let cached_cells = path_entry.and_then(|entry| {
161
5500
                let (path, _) = entry.as_ref()?;
162
5192
                let frac_x = (subpx_x as f64) * 0.25;
163
5192
                let frac_y = (subpx_y as f64) * 0.25;
164

            
165
                use agg_rust::trans_affine::TransAffine;
166
                use agg_rust::basics::FillingRule;
167
                use agg_rust::rasterizer_scanline_aa::RasterizerScanlineAa;
168

            
169
5192
                let mut ras = RasterizerScanlineAa::new();
170
5192
                ras.filling_rule(FillingRule::NonZero);
171

            
172
5192
                let transform = if is_hinted {
173
5148
                    TransAffine::new_translation(frac_x, frac_y)
174
                } else {
175
44
                    let mut t = TransAffine::new_scaling_uniform(scale as f64);
176
44
                    t.multiply(&TransAffine::new_translation(frac_x, frac_y));
177
44
                    t
178
                };
179

            
180
5192
                let verts = path.vertices();
181
5192
                ras.add_path_vertices_transformed(verts, &transform);
182
5192
                let cells = ras.outline_cells();
183
5192
                if cells.is_empty() { None } else { Some(CachedCells { cells }) }
184
5500
            });
185
5500
            self.cells.insert(cell_key, cached_cells);
186
7348
        }
187

            
188
12848
        let entry = self.cells.get(&cell_key)?;
189
12848
        entry.as_ref().map(|cc| (cc.cells.as_slice(), int_x, int_y))
190
12848
    }
191

            
192
    /// Evict all cached paths and cells.
193
    pub fn clear(&mut self) {
194
        self.paths.clear();
195
        self.cells.clear();
196
    }
197

            
198
    /// Evict caches if they exceed size limits.
199
    /// Called automatically by get_or_build / get_or_build_cells, but can
200
    /// also be called manually between frames to enforce bounds.
201
    pub fn evict_if_needed(&mut self) {
202
        if self.paths.len() >= MAX_PATH_ENTRIES {
203
            self.paths.clear();
204
        }
205
        if self.cells.len() >= MAX_CELL_ENTRIES {
206
            self.cells.clear();
207
        }
208
    }
209

            
210
    /// Returns `true` if the path cache is empty.
211
    pub fn is_empty(&self) -> bool {
212
        self.paths.is_empty()
213
    }
214

            
215
    /// Number of cached path entries.
216
    pub fn len(&self) -> usize {
217
        self.paths.len()
218
    }
219

            
220
    /// Number of cached cell entries.
221
    pub fn cell_cache_len(&self) -> usize {
222
        self.cells.len()
223
    }
224
}
225

            
226
/// Build a hinted glyph path using TrueType bytecode hinting.
227
///
228
/// The returned path is in pixel coordinates (1 unit = 1 pixel at the given ppem).
229
/// Returns `None` if the glyph has no raw hinting data or hinting fails.
230
5412
fn build_hinted_path(
231
5412
    glyph: &OwnedGlyph,
232
5412
    parsed_font: &ParsedFont,
233
5412
    ppem: u16,
234
5412
) -> Option<PathStorage> {
235
5412
    let raw_points = glyph.raw_points.as_ref()?;
236
5148
    let raw_on_curve = glyph.raw_on_curve.as_ref()?;
237
5148
    let raw_contour_ends = glyph.raw_contour_ends.as_ref()?;
238
5148
    let instructions = glyph.instructions.as_ref()?;
239

            
240
5148
    if raw_points.is_empty() || raw_contour_ends.is_empty() {
241
        return None;
242
5148
    }
243

            
244
5148
    let hint_mutex = parsed_font.hint_instance.as_ref()?;
245
5148
    let mut hint = hint_mutex.lock().ok()?;
246

            
247
5148
    let upem = parsed_font.font_metrics.units_per_em;
248
5148
    if upem == 0 {
249
        return None;
250
5148
    }
251

            
252
    // Set up hinting for this ppem (scales CVT, runs prep)
253
5148
    if hint.set_ppem(ppem, ppem as f64).is_err() {
254
        return None;
255
5148
    }
256

            
257
    // Scale raw points from font units to F26Dot6
258
5148
    let scale = allsorts::hinting::f26dot6::compute_scale(ppem, upem);
259
    use allsorts::hinting::f26dot6::F26Dot6;
260

            
261
5148
    let points_f26dot6: Vec<(i32, i32)> = raw_points
262
5148
        .iter()
263
161656
        .map(|&(x, y)| {
264
161656
            let sx = F26Dot6::from_funits(x as i32, scale);
265
161656
            let sy = F26Dot6::from_funits(y as i32, scale);
266
161656
            (sx.to_bits(), sy.to_bits())
267
161656
        })
268
5148
        .collect();
269

            
270
    // Scale advance width to F26Dot6 for phantom points
271
5148
    let adv_f26dot6 = F26Dot6::from_funits(glyph.horz_advance as i32, scale).to_bits();
272

            
273
    // Run hinting with unscaled orus for precise IUP interpolation
274
5148
    let hinted = match hint.hint_glyph_with_orus(
275
5148
        &points_f26dot6,
276
5148
        Some(raw_points.as_slice()),
277
5148
        raw_on_curve,
278
5148
        raw_contour_ends,
279
5148
        instructions,
280
5148
        adv_f26dot6,
281
5148
    ) {
282
5148
        Ok(h) => h,
283
        Err(_) => return None,
284
    };
285

            
286
    // Build path from hinted points using TrueType quadratic contour conventions
287
5148
    build_path_from_contours(&hinted, raw_on_curve, raw_contour_ends)
288
5412
}
289

            
290
/// Build an agg PathStorage from TrueType contour data (points in F26Dot6).
291
///
292
/// Matches allsorts' `visit_simple_glyph_outline` algorithm exactly:
293
/// - On-curve points are endpoints of line/curve segments
294
/// - Off-curve points are quadratic Bézier control points
295
/// - Two consecutive off-curve points have an implicit on-curve midpoint
296
/// - Y is negated for screen coordinates (font Y-up → screen Y-down)
297
/// - The origin point is NOT revisited in the loop; close() handles the final segment
298
5148
pub fn build_path_from_contours(
299
5148
    points: &[(i32, i32)],
300
5148
    on_curve: &[bool],
301
5148
    contour_ends: &[u16],
302
5148
) -> Option<PathStorage> {
303
    use agg_rust::basics::PATH_FLAGS_NONE;
304

            
305
5148
    let mut path = PathStorage::new();
306
5148
    let mut has_ops = false;
307
5148
    let mut contour_start = 0usize;
308

            
309
12408
    for &end_idx in contour_ends {
310
7260
        let end = end_idx as usize;
311
7260
        if end >= points.len() || contour_start > end {
312
            contour_start = end + 1;
313
            continue;
314
7260
        }
315

            
316
7260
        let pts = &points[contour_start..=end];
317
7260
        let flags = &on_curve[contour_start..=end];
318
7260
        let n = pts.len();
319
7260
        if n < 2 {
320
            contour_start = end + 1;
321
            continue;
322
7260
        }
323

            
324
        // Helper: get point as (f64, f64) with Y negated
325
225720
        let px = |i: usize| -> (f64, f64) {
326
225720
            (f26_to_px(pts[i].0) as f64, -f26_to_px(pts[i].1) as f64)
327
225720
        };
328
64064
        let mid = |a: (f64, f64), b: (f64, f64)| -> (f64, f64) {
329
64064
            ((a.0 + b.0) * 0.5, (a.1 + b.1) * 0.5)
330
64064
        };
331

            
332
        // Determine origin and processing range (matching allsorts' calculate_origin)
333
7260
        let (origin, start, until) = if flags[0] {
334
7260
            (px(0), 1usize, n)
335
        } else if flags[n - 1] {
336
            (px(n - 1), 0usize, n - 1)
337
        } else {
338
            (mid(px(0), px(n - 1)), 0usize, n)
339
        };
340

            
341
7260
        path.move_to(origin.0, origin.1);
342
7260
        has_ops = true;
343

            
344
7260
        let mut i = start;
345
132528
        while i < until {
346
125268
            if flags[i] {
347
28996
                // On-curve: line segment
348
28996
                let to = px(i);
349
28996
                path.line_to(to.0, to.1);
350
28996
                i += 1;
351
28996
            } else {
352
                // Off-curve control point
353
96272
                let ctrl = px(i);
354
96272
                let next = i + 1;
355
96272
                if next < until {
356
93192
                    if flags[next] {
357
29128
                        // Next is on-curve: quad to it, consume both
358
29128
                        let to = px(next);
359
29128
                        path.curve3(ctrl.0, ctrl.1, to.0, to.1);
360
29128
                        i = next + 1;
361
64064
                    } else {
362
64064
                        // Next is also off-curve: quad to implicit midpoint
363
64064
                        let m = mid(ctrl, px(next));
364
64064
                        path.curve3(ctrl.0, ctrl.1, m.0, m.1);
365
64064
                        i = next;
366
64064
                    }
367
3080
                } else {
368
3080
                    // End of range: curve back to origin
369
3080
                    path.curve3(ctrl.0, ctrl.1, origin.0, origin.1);
370
3080
                    i = next;
371
3080
                }
372
            }
373
        }
374
7260
        path.close_polygon(PATH_FLAGS_NONE);
375

            
376
7260
        contour_start = end + 1;
377
    }
378

            
379
5148
    if !has_ops {
380
        return None;
381
5148
    }
382
5148
    Some(path)
383
5148
}
384

            
385
/// Convert F26Dot6 value to pixel coordinate (f32).
386
#[inline]
387
451440
fn f26_to_px(v: i32) -> f32 {
388
451440
    v as f32 / 64.0
389
451440
}