1
//! TrueType bytecode interpreter — executes font programs (fpgm, prep) and
2
//! per-glyph instructions to grid-fit outlines.
3
//!
4
//! # Specification references
5
//!
6
//! - **MS OpenType**: <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions>
7
//! - **Apple TrueType RM**: <https://developer.apple.com/fonts/TrueType-Reference-Manual/RM05/Chap5.html>
8
//!
9
//! # Key instructions
10
//!
11
//! | Opcode | Name   | Description |
12
//! |--------|--------|-------------|
13
//! | 0x2E   | MDAP   | Move Direct Absolute Point (touch + optional round) |
14
//! | 0x3E   | MIAP   | Move Indirect Absolute Point (CVT-based, key for twilight zone init) |
15
//! | 0xC0+  | MDRP   | Move Direct Relative Point (measured original distance) |
16
//! | 0xE0+  | MIRP   | Move Indirect Relative Point (CVT-based distance from reference) |
17
//! | 0x39   | IP     | Interpolate Point (preserve relative position between rp1/rp2) |
18
//! | 0x30   | IUP    | Interpolate Untouched Points (final pass, per-contour) |
19
//! | 0x5D+  | DELTAP | Delta Exception Point (ppem-specific pixel tuning) |
20
//! | 0x73+  | DELTAC | Delta Exception CVT (ppem-specific CVT modification) |
21
//!
22
//! # Twilight zone (zone 0)
23
//!
24
//! The `prep` program uses MIAP on zone 0 to create reference points that
25
//! encode key font measurements (cap height, x-height, stem widths, etc.).
26
//! These points must be properly initialized (original + current coordinates)
27
//! so that glyph programs can reference them via MIRP/IP for grid-fitting.
28

            
29
use std::fmt;
30

            
31
use super::f26dot6::{compute_scale, F2Dot14, F26Dot6};
32
use super::graphics_state::{GraphicsState, RoundState};
33

            
34
// ── Error type ───────────────────────────────────────────────────────
35

            
36
#[derive(Clone, Debug, PartialEq, Eq)]
37
pub enum HintError {
38
    StackOverflow,
39
    StackUnderflow,
40
    InvalidOpcode(u8),
41
    UndefinedFunction(u32),
42
    CallStackOverflow,
43
    InvalidPointIndex(u32),
44
    InvalidCvtIndex(u32),
45
    InvalidStorageIndex(u32),
46
    InvalidZone(u32),
47
    DivideByZero,
48
    UnexpectedEndOfBytecode,
49
    InvalidJump,
50
    ExceededMaxInstructions,
51
    FontNotReady,
52
}
53

            
54
impl fmt::Display for HintError {
55
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56
        match self {
57
            HintError::StackOverflow => write!(f, "hinting: stack overflow"),
58
            HintError::StackUnderflow => write!(f, "hinting: stack underflow"),
59
            HintError::InvalidOpcode(op) => write!(f, "hinting: invalid opcode 0x{:02X}", op),
60
            HintError::UndefinedFunction(id) => {
61
                write!(f, "hinting: undefined function {}", id)
62
            }
63
            HintError::CallStackOverflow => write!(f, "hinting: call stack overflow"),
64
            HintError::InvalidPointIndex(i) => {
65
                write!(f, "hinting: invalid point index {}", i)
66
            }
67
            HintError::InvalidCvtIndex(i) => write!(f, "hinting: invalid CVT index {}", i),
68
            HintError::InvalidStorageIndex(i) => {
69
                write!(f, "hinting: invalid storage index {}", i)
70
            }
71
            HintError::InvalidZone(z) => write!(f, "hinting: invalid zone {}", z),
72
            HintError::DivideByZero => write!(f, "hinting: divide by zero"),
73
            HintError::UnexpectedEndOfBytecode => {
74
                write!(f, "hinting: unexpected end of bytecode")
75
            }
76
            HintError::InvalidJump => write!(f, "hinting: invalid jump target"),
77
            HintError::ExceededMaxInstructions => {
78
                write!(f, "hinting: exceeded maximum instruction count")
79
            }
80
            HintError::FontNotReady => write!(f, "hinting: font not ready (fpgm not executed)"),
81
        }
82
    }
83
}
84

            
85
impl std::error::Error for HintError {}
86

            
87
// ── FreeType-compatible signed multiply-divide ──────────────────────
88
//
89
// Matches FreeType's `FT_MulDiv`: compute `a * b / c` with correct
90
// rounding for any sign combination.  The trick is to take absolute
91
// values, add a half-divisor for rounding, divide, then reapply sign.
92

            
93
#[inline]
94
1717632
fn ft_muldiv(a: i64, b: i64, c: i64) -> i64 {
95
1717632
    if c == 0 {
96
        return 0;
97
1717632
    }
98
1717632
    let mut s: i64 = 1;
99
1717632
    let mut aa = a;
100
1717632
    let mut bb = b;
101
1717632
    let mut cc = c;
102
1717632
    if aa < 0 { aa = -aa; s = -s; }
103
1717632
    if bb < 0 { bb = -bb; s = -s; }
104
1717632
    if cc < 0 { cc = -cc; s = -s; }
105
1717632
    let d = (aa * bb + (cc >> 1)) / cc;
106
1717632
    if s > 0 { d } else { -d }
107
1717632
}
108

            
109
/// FreeType's FT_DivFix: compute `(a << 16) / b` with signed rounding.
110
/// Returns a 16.16 fixed-point scale factor.
111
#[inline]
112
100980
fn ft_divfix(a: i64, b: i64) -> i64 {
113
100980
    if b == 0 {
114
        return 0x7FFFFFFF;
115
100980
    }
116
100980
    let mut s: i64 = 1;
117
100980
    let mut aa = a;
118
100980
    let mut bb = b;
119
100980
    if aa < 0 { aa = -aa; s = -s; }
120
100980
    if bb < 0 { bb = -bb; s = -s; }
121
100980
    let q = ((aa << 16) + (bb >> 1)) / bb;
122
100980
    if s > 0 { q } else { -q }
123
100980
}
124

            
125
/// FreeType's FT_MulFix: compute `(a * b + 0x8000) >> 16` with signed rounding.
126
/// Multiplies a value by a 16.16 fixed-point factor.
127
#[inline]
128
100980
fn ft_mulfix(a: i64, b: i64) -> i64 {
129
100980
    let mut s: i64 = 1;
130
100980
    let mut aa = a;
131
100980
    let mut bb = b;
132
100980
    if aa < 0 { aa = -aa; s = -s; }
133
100980
    if bb < 0 { bb = -bb; s = -s; }
134
100980
    let c = (aa * bb + 0x8000) >> 16;
135
100980
    if s > 0 { c } else { -c }
136
100980
}
137

            
138
// ── Point / Zone types ───────────────────────────────────────────────
139

            
140
#[derive(Copy, Clone, Debug, Default)]
141
pub struct Point {
142
    pub x: i32, // F26Dot6
143
    pub y: i32, // F26Dot6
144
}
145

            
146
bitflags::bitflags! {
147
    #[derive(Copy, Clone, Debug, Default)]
148
    pub struct PointFlags: u8 {
149
        const TOUCHED_X = 0x01;
150
        const TOUCHED_Y = 0x02;
151
        const ON_CURVE  = 0x04;
152
    }
153
}
154

            
155
#[derive(Clone, Debug)]
156
pub struct Zone {
157
    pub original: Vec<Point>,
158
    pub current: Vec<Point>,
159
    /// Unscaled original coordinates in font units (FreeType's "orus").
160
    /// Used by IUP for more precise interpolation — integer font-unit
161
    /// coordinates avoid the rounding errors present in scaled F26Dot6.
162
    pub orus: Vec<Point>,
163
    pub flags: Vec<PointFlags>,
164
    pub contour_ends: Vec<u16>,
165
}
166

            
167
impl Zone {
168
44928
    pub fn new(n_points: usize) -> Self {
169
44928
        Zone {
170
44928
            original: vec![Point::default(); n_points],
171
44928
            current: vec![Point::default(); n_points],
172
44928
            orus: vec![Point::default(); n_points],
173
44928
            flags: vec![PointFlags::empty(); n_points],
174
44928
            contour_ends: Vec::new(),
175
44928
        }
176
44928
    }
177

            
178
6156
    pub fn len(&self) -> usize {
179
6156
        self.current.len()
180
6156
    }
181

            
182
6048
    pub fn resize(&mut self, n: usize) {
183
6048
        self.original.resize(n, Point::default());
184
6048
        self.current.resize(n, Point::default());
185
6048
        self.orus.resize(n, Point::default());
186
6048
        self.flags.resize(n, PointFlags::empty());
187
6048
    }
188

            
189
    /// Ensure the zone can hold at least `n` points, growing if necessary.
190
    #[inline]
191
121986
    pub fn ensure_capacity(&mut self, n: usize) {
192
121986
        if n > self.current.len() && n <= 10_000 {
193
            self.resize(n);
194
121986
        }
195
121986
    }
196
}
197

            
198
// ── Function definition ──────────────────────────────────────────────
199

            
200
#[derive(Clone, Debug)]
201
pub struct FuncDef {
202
    pub bytecode: Vec<u8>,
203
}
204

            
205
// ── Maximum instruction count to prevent infinite loops ──────────────
206

            
207
const MAX_INSTRUCTIONS: u64 = 1_000_000;
208
const MAX_CALL_DEPTH: u32 = 64;
209

            
210
// ── Interpreter ──────────────────────────────────────────────────────
211

            
212
#[derive(Clone, Debug)]
213
pub struct Interpreter {
214
    // Stack
215
    pub(crate) stack: Vec<i32>,
216
    max_stack: usize,
217

            
218
    // CVT (F26Dot6 values stored as i32)
219
    pub(crate) cvt: Vec<i32>,
220
    // Original scaled CVT values (before prep/WCVTP modifications).
221
    // DELTAC applies adjustments to these originals, not WCVTP-modified values.
222
    pub(crate) cvt_original: Vec<i32>,
223
    // Accumulated DELTAC adjustments per CVT entry. Multiple DELTACs
224
    // targeting the same CVT at the same ppem accumulate their deltas.
225
    cvt_deltac_accum: Vec<i32>,
226

            
227
    // Storage area
228
    pub(crate) storage: Vec<i32>,
229

            
230
    // Function/instruction definitions
231
    pub(crate) fdefs: Vec<Option<FuncDef>>,
232
    pub(crate) idefs: Vec<Option<FuncDef>>,
233

            
234
    // Graphics state
235
    pub(crate) gs: GraphicsState,
236
    pub(crate) default_gs: GraphicsState,
237

            
238
    // Zones: 0 = twilight, 1 = glyph
239
    pub(crate) zones: [Zone; 2],
240

            
241
    // Font metrics
242
    pub(crate) ppem: u16,
243
    pub(crate) point_size: i32, // F26Dot6
244
    pub(crate) units_per_em: u16,
245
    pub(crate) scale: i64, // 16.16 fixed-point
246

            
247
    // Execution state
248
    instruction_count: u64,
249
    call_depth: u32,
250

            
251
    /// When true, dump every instruction to stderr (for debugging)
252
    pub trace_mode: bool,
253
    /// When true, log move_point calls on glyph zone to stderr
254
    pub debug_trace_points: bool,
255

            
256
    // ── Hinting mode flags (toggle to find correct behavior) ────────
257

            
258
    /// Flag A: Reset X coordinates to original after glyph program.
259
    /// Matches FreeType v40 / Chrome which only applies Y-axis hinting.
260
    /// Without this: full X+Y hinting (v35 mode), causes SHPIX spacing issues.
261
    /// With this: Y-only hinting, but curves that depend on X/Y coordination
262
    /// (e.g., 'u' bottom curve at ppem=20) may distort.
263
    pub suppress_x_axis: bool,
264

            
265
    /// Flag B: Snapshot Y after IUP[Y], discard post-IUP Y modifications.
266
    /// Some glyph programs modify Y after IUP via function calls (e.g.,
267
    /// 'o' at ppem=12: pt5 moves from 150→77). FreeType v40 may suppress these.
268
    /// Without this: post-IUP moves apply, causing diamond shapes at small ppem.
269
    /// With this: clean IUP interpolation preserved, but some legitimate
270
    /// post-IUP adjustments are also discarded.
271
    pub snapshot_iup_y: bool,
272

            
273
    /// Flag C: Use orig_dist sign for minimum_distance when dist rounds to zero.
274
    /// Without this: small negative distances get +min_dist (wrong direction).
275
    /// With this: preserves original direction, matching FreeType behavior.
276
    pub fix_min_distance_sign: bool,
277

            
278
    /// Snapshot of Y coordinates taken right after IUP[Y] runs.
279
    iup_y_snapshot: Option<Vec<i32>>,
280
}
281

            
282
impl Interpreter {
283
    /// Create a new interpreter from maxp table values.
284
22464
    pub fn new(
285
22464
        max_stack_elements: u16,
286
22464
        max_storage: u16,
287
22464
        max_function_defs: u16,
288
22464
        max_instruction_defs: u16,
289
22464
        max_twilight_points: u16,
290
22464
        units_per_em: u16,
291
22464
    ) -> Self {
292
        // FreeType sizes the stack to maxStackElements + 32 so under-declared
293
        // fonts (whose real high-water mark exceeds maxp) don't lose all hinting.
294
22464
        let max_stack = max_stack_elements as usize + 32;
295
22464
        Interpreter {
296
22464
            stack: Vec::with_capacity(max_stack),
297
22464
            max_stack,
298
22464
            cvt: Vec::new(),
299
22464
            cvt_original: Vec::new(),
300
22464
            cvt_deltac_accum: Vec::new(),
301
22464
            storage: vec![0i32; max_storage as usize],
302
22464
            fdefs: vec![None; max_function_defs as usize],
303
22464
            idefs: vec![None; max_instruction_defs as usize],
304
22464
            gs: GraphicsState::default(),
305
22464
            default_gs: GraphicsState::default(),
306
22464
            zones: [
307
22464
                Zone::new(max_twilight_points as usize), // twilight
308
22464
                Zone::new(0),                            // glyph (resized per glyph)
309
22464
            ],
310
22464
            ppem: 0,
311
22464
            point_size: 0,
312
22464
            units_per_em,
313
22464
            scale: 0,
314
22464
            instruction_count: 0,
315
22464
            call_depth: 0,
316
22464
            trace_mode: false,
317
22464
            debug_trace_points: false,
318
22464
            suppress_x_axis: false,    // Flag A: OFF — full X+Y hinting
319
22464
            snapshot_iup_y: false,    // Flag B: OFF
320
22464
            fix_min_distance_sign: true, // Flag C: preserve direction on zero-round
321
22464
            iup_y_snapshot: None,
322
22464
        }
323
22464
    }
324

            
325
    /// Returns the current CVT values (F26Dot6 stored as i32).
326
    pub fn cvt(&self) -> &[i32] {
327
        &self.cvt
328
    }
329

            
330
    /// Returns the max stack size.
331
    pub fn max_stack(&self) -> usize {
332
        self.max_stack
333
    }
334

            
335
    /// Returns the graphics state (for debugging).
336
    pub fn graphics_state(&self) -> &GraphicsState {
337
        &self.gs
338
    }
339

            
340
    /// Returns the storage area size.
341
    pub fn storage_len(&self) -> usize {
342
        self.storage.len()
343
    }
344

            
345
    /// Read a storage area value (for debugging).
346
    pub fn read_storage(&self, idx: usize) -> Option<i32> {
347
        self.storage.get(idx).copied()
348
    }
349

            
350
    /// Returns the number of function definitions.
351
    pub fn fdef_count(&self) -> usize {
352
        self.fdefs.len()
353
    }
354

            
355
    /// Returns the number of twilight zone points.
356
    pub fn twilight_point_count(&self) -> usize {
357
        self.zones[0].original.len()
358
    }
359

            
360
    /// Returns the ppem value.
361
    pub fn ppem(&self) -> u16 {
362
        self.ppem
363
    }
364

            
365
    /// Returns the scale value (16.16 fixed-point).
366
    pub fn scale(&self) -> i64 {
367
        self.scale
368
    }
369

            
370
    /// Returns units_per_em.
371
    pub fn units_per_em(&self) -> u16 {
372
        self.units_per_em
373
    }
374

            
375
    /// Execute the font program (`fpgm`) to populate function definitions.
376
14310
    pub fn execute_fpgm(&mut self, fpgm: &[u8]) -> Result<(), HintError> {
377
14310
        self.stack.clear();
378
14310
        self.gs = GraphicsState::default();
379
14310
        self.instruction_count = 0;
380
14310
        self.call_depth = 0;
381
14310
        self.execute(fpgm)
382
14310
    }
383

            
384
    /// Set the ppem size and execute the prep program.
385
6372
    pub fn execute_prep(&mut self, prep: &[u8], ppem: u16, point_size: f64) -> Result<(), HintError> {
386
6372
        self.ppem = ppem;
387
6372
        self.point_size = F26Dot6::from_f64(point_size).to_bits();
388
6372
        self.scale = compute_scale(ppem, self.units_per_em);
389

            
390
6372
        self.stack.clear();
391
6372
        self.gs = GraphicsState::default();
392
        // Note: storage is NOT cleared between prep runs.
393
        // The prep program is responsible for setting all values it needs.
394
6372
        self.instruction_count = 0;
395
6372
        self.call_depth = 0;
396
6372
        self.execute(prep)?;
397

            
398
        // Save the modified graphics state as the default for glyph programs
399
702
        self.default_gs = self.gs.clone();
400
702
        Ok(())
401
6372
    }
402

            
403
    /// Scale CVT values from FUnits to F26Dot6 pixels.
404
13716
    pub fn scale_cvt(&mut self, cvt_funits: &[i16]) {
405
13716
        self.cvt.clear();
406
13716
        self.cvt.reserve(cvt_funits.len());
407
632178
        for &funit in cvt_funits {
408
618462
            self.cvt
409
618462
                .push(F26Dot6::from_funits(funit as i32, self.scale).to_bits());
410
618462
        }
411
        // Save original scaled CVT values. DELTAC applies adjustments
412
        // to these originals, not to WCVTP-modified values. Without this,
413
        // the prep's WCVTP rounds CVT[0] from 1422→1408, then DELTAC(-40)
414
        // gives 1368 (rounds to 1344=21px). With originals, DELTAC(-40)
415
        // applies to 1422 giving 1382 (rounds to 1408=22px, correct).
416
13716
        self.cvt_original = self.cvt.clone();
417
13716
        self.cvt_deltac_accum = vec![0i32; self.cvt.len()];
418
13716
    }
419

            
420
    /// Hint a glyph outline by executing its bytecode instructions.
421
    ///
422
    /// `points` are pre-scaled to F26Dot6 coordinates.
423
    /// `on_curve` flags indicate whether each point is on-curve.
424
    /// `contour_ends` gives the index of the last point in each contour.
425
    /// `instructions` is the per-glyph bytecode from the glyf table.
426
    ///
427
    /// After execution, the hinted point positions can be read from
428
    /// `self.zones[1].current`.
429
6048
    pub fn hint_glyph(
430
6048
        &mut self,
431
6048
        points: &[Point],
432
6048
        on_curve: &[bool],
433
6048
        contour_ends: &[u16],
434
6048
        instructions: &[u8],
435
6048
    ) -> Result<(), HintError> {
436
        // Set up the glyph zone with empty orus (no unscaled data)
437
6048
        self.hint_glyph_with_orus(points, None, on_curve, contour_ends, instructions)
438
6048
    }
439

            
440
    /// Hint a glyph with optional unscaled original coordinates (orus).
441
    ///
442
    /// If `orus` is provided, IUP interpolation uses these exact integer
443
    /// coordinates instead of the scaled F26Dot6 `points`. This matches
444
    /// FreeType's approach and avoids ±1 F26Dot6 rounding errors in IUP.
445
6048
    pub fn hint_glyph_with_orus(
446
6048
        &mut self,
447
6048
        points: &[Point],
448
6048
        orus: Option<&[Point]>,
449
6048
        on_curve: &[bool],
450
6048
        contour_ends: &[u16],
451
6048
        instructions: &[u8],
452
6048
    ) -> Result<(), HintError> {
453
        // Set up the glyph zone
454
6048
        let n = points.len();
455
6048
        let zone = &mut self.zones[1];
456
6048
        zone.resize(n);
457
220212
        for i in 0..n {
458
220212
            zone.original[i] = points[i];
459
220212
            zone.current[i] = points[i];
460
            // Store unscaled coordinates if provided, otherwise use scaled
461
220212
            zone.orus[i] = if let Some(orus) = orus {
462
                orus.get(i).copied().unwrap_or(points[i])
463
            } else {
464
220212
                points[i]
465
            };
466
220212
            let mut flags = PointFlags::empty();
467
220212
            if on_curve.get(i).copied().unwrap_or(false) {
468
102060
                flags |= PointFlags::ON_CURVE;
469
118152
            }
470
220212
            zone.flags[i] = flags;
471
        }
472
6048
        zone.contour_ends = contour_ends.to_vec();
473

            
474
        // Reset graphics state to defaults (set by prep), then override
475
        // per TrueType spec: projection/freedom vectors reset to x-axis,
476
        // reference points to 0, loop to 1, zone pointers to 1.
477
6048
        self.gs = self.default_gs.clone();
478
6048
        self.gs.projection_vector = (F2Dot14::ONE, F2Dot14::ZERO);
479
6048
        self.gs.freedom_vector = (F2Dot14::ONE, F2Dot14::ZERO);
480
6048
        self.gs.dual_projection_vector = (F2Dot14::ONE, F2Dot14::ZERO);
481
6048
        self.gs.rp0 = 0;
482
6048
        self.gs.rp1 = 0;
483
6048
        self.gs.rp2 = 0;
484
6048
        self.gs.loop_value = 1;
485
6048
        self.gs.zp0 = 1;
486
6048
        self.gs.zp1 = 1;
487
6048
        self.gs.zp2 = 1;
488
        // FreeType TT_Run_Context resets round_state to RTG (1) before every
489
        // glyph, so a non-grid round mode left active by prep does not leak in.
490
6048
        self.gs.round_state = RoundState::Grid;
491

            
492
6048
        self.stack.clear();
493
6048
        self.instruction_count = 0;
494
6048
        self.call_depth = 0;
495

            
496
6048
        self.execute(instructions)?;
497

            
498
        // v40 backward compatibility: undo X-axis movements AND post-IUP
499
        // Y-axis modifications.
500
        //
501
        // FreeType v40 (DEFAULT mode) and Chrome/Core Text on macOS:
502
        // - Suppress all X-axis grid-fitting (subpixel rendering handles X)
503
        // - After IUP[Y], some glyph programs apply post-IUP function calls
504
        //   that modify Y coordinates (e.g., Times New Roman 'o' at ppem=12
505
        //   moves pt5.y from 150→77 via a called function). FreeType v40
506
        //   suppresses these post-IUP modifications.
507
        //
508
        // We handle this by saving Y values right after IUP[Y] runs
509
        // (stored in `iup_y_snapshot`) and restoring them after execution.
510
        // Flag A: suppress X-axis movements
511
6048
        if self.suppress_x_axis {
512
            let zone = &mut self.zones[1];
513
            for i in 0..zone.current.len() {
514
                zone.current[i].x = zone.original[i].x;
515
            }
516
6048
        }
517

            
518
        // Flag B: restore Y from IUP snapshot (discard post-IUP Y mods)
519
6048
        if self.snapshot_iup_y {
520
            if let Some(snap) = &self.iup_y_snapshot {
521
                let zone = &mut self.zones[1];
522
                for i in 0..zone.current.len().min(snap.len()) {
523
                    zone.current[i].y = snap[i];
524
                }
525
            }
526
            self.iup_y_snapshot = None;
527
6048
        }
528

            
529
6048
        Ok(())
530
6048
    }
531

            
532
    // ── Core execution loop ──────────────────────────────────────────
533

            
534
957690
    fn execute(&mut self, bytecode: &[u8]) -> Result<(), HintError> {
535
957690
        let mut ip: usize = 0;
536
19136736
        while ip < bytecode.len() {
537
18196056
            self.instruction_count += 1;
538
18196056
            if self.instruction_count > MAX_INSTRUCTIONS {
539
                return Err(HintError::ExceededMaxInstructions);
540
18196056
            }
541

            
542
18196056
            let opcode = bytecode[ip];
543
18196056
            ip += 1;
544

            
545
18196056
            self.dispatch(opcode, bytecode, &mut ip)?;
546
        }
547
940680
        Ok(())
548
957690
    }
549

            
550
18196056
    fn dispatch(
551
18196056
        &mut self,
552
18196056
        opcode: u8,
553
18196056
        bytecode: &[u8],
554
18196056
        ip: &mut usize,
555
18196056
    ) -> Result<(), HintError> {
556
18196056
        match opcode {
557
            // ── Vector setting ───────────────────────────────────
558
12420
            0x00 => {
559
12420
                // SVTCA[y] - set both vectors to y-axis
560
12420
                self.gs.projection_vector = (F2Dot14::ZERO, F2Dot14::ONE);
561
12420
                self.gs.freedom_vector = (F2Dot14::ZERO, F2Dot14::ONE);
562
12420
                self.gs.dual_projection_vector = (F2Dot14::ZERO, F2Dot14::ONE);
563
12420
            }
564
702
            0x01 => {
565
702
                // SVTCA[x] - set both vectors to x-axis
566
702
                self.gs.projection_vector = (F2Dot14::ONE, F2Dot14::ZERO);
567
702
                self.gs.freedom_vector = (F2Dot14::ONE, F2Dot14::ZERO);
568
702
                self.gs.dual_projection_vector = (F2Dot14::ONE, F2Dot14::ZERO);
569
702
            }
570
            0x02 => {
571
                // SPVTCA[y] - set projection vector to y-axis
572
                self.gs.projection_vector = (F2Dot14::ZERO, F2Dot14::ONE);
573
                self.gs.dual_projection_vector = (F2Dot14::ZERO, F2Dot14::ONE);
574
            }
575
            0x03 => {
576
                // SPVTCA[x] - set projection vector to x-axis
577
                self.gs.projection_vector = (F2Dot14::ONE, F2Dot14::ZERO);
578
                self.gs.dual_projection_vector = (F2Dot14::ONE, F2Dot14::ZERO);
579
            }
580
            0x04 => {
581
                // SFVTCA[y] - set freedom vector to y-axis
582
                self.gs.freedom_vector = (F2Dot14::ZERO, F2Dot14::ONE);
583
            }
584
            0x05 => {
585
                // SFVTCA[x] - set freedom vector to x-axis
586
                self.gs.freedom_vector = (F2Dot14::ONE, F2Dot14::ZERO);
587
            }
588
5002722
            0x06..=0x07 => {
589
                // SPVTL[a] - set projection vector to line
590
                let a = opcode & 1;
591
                self.op_spvtl(a != 0)?;
592
            }
593
5002722
            0x08..=0x09 => {
594
                // SFVTL[a] - set freedom vector to line
595
                let a = opcode & 1;
596
                self.op_sfvtl(a != 0)?;
597
            }
598
            0x0A => {
599
                // SPVFS - set projection vector from stack
600
                let y = self.pop()? as i16 as i32;
601
                let x = self.pop()? as i16 as i32;
602
                self.gs.projection_vector = (F2Dot14::from_bits(x), F2Dot14::from_bits(y));
603
                self.gs.dual_projection_vector = self.gs.projection_vector;
604
            }
605
            0x0B => {
606
                // SFVFS - set freedom vector from stack
607
                let y = self.pop()? as i16 as i32;
608
                let x = self.pop()? as i16 as i32;
609
                self.gs.freedom_vector = (F2Dot14::from_bits(x), F2Dot14::from_bits(y));
610
            }
611
            0x0C => {
612
                // GPV - get projection vector
613
                self.push(self.gs.projection_vector.0.to_bits())?;
614
                self.push(self.gs.projection_vector.1.to_bits())?;
615
            }
616
            0x0D => {
617
                // GFV - get freedom vector
618
                self.push(self.gs.freedom_vector.0.to_bits())?;
619
                self.push(self.gs.freedom_vector.1.to_bits())?;
620
            }
621
            0x0E => {
622
                // SFVTPV - set freedom vector to projection vector
623
                self.gs.freedom_vector = self.gs.projection_vector;
624
            }
625
            0x0F => {
626
                // ISECT - move point to intersection
627
                self.op_isect()?;
628
            }
629

            
630
            // ── Reference point / zone setting ───────────────────
631
            0x10 => {
632
                // SRP0
633
108
                self.gs.rp0 = self.pop()? as u32;
634
            }
635
            0x11 => {
636
                // SRP1
637
                self.gs.rp1 = self.pop()? as u32;
638
            }
639
            0x12 => {
640
                // SRP2
641
                self.gs.rp2 = self.pop()? as u32;
642
            }
643
            0x13 => {
644
                // SZP0
645
36234
                let zone = self.pop()? as u32;
646
36234
                if zone > 1 {
647
                    return Err(HintError::InvalidZone(zone));
648
36234
                }
649
36234
                self.gs.zp0 = zone;
650
            }
651
            0x14 => {
652
                // SZP1
653
30942
                let zone = self.pop()? as u32;
654
30942
                if zone > 1 {
655
                    return Err(HintError::InvalidZone(zone));
656
30942
                }
657
30942
                self.gs.zp1 = zone;
658
            }
659
            0x15 => {
660
                // SZP2
661
12258
                let zone = self.pop()? as u32;
662
12258
                if zone > 1 {
663
                    return Err(HintError::InvalidZone(zone));
664
12258
                }
665
12258
                self.gs.zp2 = zone;
666
            }
667
            0x16 => {
668
                // SZPS - set all zone pointers
669
67068
                let zone = self.pop()? as u32;
670
67068
                if zone > 1 {
671
                    return Err(HintError::InvalidZone(zone));
672
67068
                }
673
67068
                self.gs.zp0 = zone;
674
67068
                self.gs.zp1 = zone;
675
67068
                self.gs.zp2 = zone;
676
            }
677
            0x17 => {
678
                // SLOOP
679
                let n = self.pop()?;
680
                self.gs.loop_value = n.max(1) as u32;
681
            }
682

            
683
            // ── Rounding mode ────────────────────────────────────
684
            0x18 => {
685
                // RTG - round to grid
686
                self.gs.round_state = RoundState::Grid;
687
            }
688
            0x19 => {
689
                // RTHG - round to half grid
690
                self.gs.round_state = RoundState::HalfGrid;
691
            }
692

            
693
            // ── Distances ────────────────────────────────────────
694
            0x1A => {
695
                // SMD - set minimum distance
696
                let d = self.pop()?;
697
                self.gs.minimum_distance = F26Dot6::from_bits(d);
698
            }
699

            
700
            // ── Control flow ─────────────────────────────────────
701
            0x1B => {
702
                // ELSE - skip to EIF
703
69876
                self.skip_else(bytecode, ip)?;
704
            }
705
            0x1C => {
706
                // JMPR - jump relative
707
                let offset = self.pop()?;
708
                // offset is relative to the JMPR opcode position (*ip - 1)
709
                let opcode_pos = *ip as i64 - 1;
710
                let new_ip = opcode_pos + offset as i64;
711
                if new_ip < 0 || new_ip > bytecode.len() as i64 {
712
                    return Err(HintError::InvalidJump);
713
                }
714
                *ip = new_ip as usize;
715
            }
716
            0x1D => {
717
                // SCVTCI - set CVT cut-in
718
702
                let v = self.pop()?;
719
702
                self.gs.control_value_cut_in = F26Dot6::from_bits(v);
720
            }
721
            0x1E => {
722
                // SSWCI - set single width cut-in
723
                let v = self.pop()?;
724
                self.gs.single_width_cut_in = F26Dot6::from_bits(v);
725
            }
726
            0x1F => {
727
                // SSW - set single width value (FUnits -> F26Dot6)
728
                let v = self.pop()?;
729
                self.gs.single_width_value =
730
                    F26Dot6::from_funits(v, self.scale);
731
            }
732

            
733
            // ── Stack manipulation ───────────────────────────────
734
            0x20 => {
735
                // DUP
736
1913436
                let v = self.peek()?;
737
1913436
                self.push(v)?;
738
            }
739
            0x21 => {
740
                // POP
741
399384
                self.pop()?;
742
            }
743
            0x22 => {
744
                // CLEAR
745
                self.stack.clear();
746
            }
747
            0x23 => {
748
                // SWAP
749
396198
                let len = self.stack.len();
750
396198
                if len < 2 {
751
                    return Err(HintError::StackUnderflow);
752
396198
                }
753
396198
                self.stack.swap(len - 1, len - 2);
754
            }
755
            0x24 => {
756
                // DEPTH
757
28674
                let d = self.stack.len() as i32;
758
28674
                self.push(d)?;
759
            }
760
            0x25 => {
761
                // CINDEX - copy indexed element
762
408780
                let idx = self.pop()? as usize;
763
408780
                let len = self.stack.len();
764
408780
                if idx == 0 || idx > len {
765
                    return Err(HintError::StackUnderflow);
766
408780
                }
767
408780
                let v = self.stack[len - idx];
768
408780
                self.push(v)?;
769
            }
770
            0x26 => {
771
                // MINDEX - move indexed element to top
772
19872
                let idx = self.pop()? as usize;
773
19872
                let len = self.stack.len();
774
19872
                if idx == 0 || idx > len {
775
                    return Err(HintError::StackUnderflow);
776
19872
                }
777
19872
                let pos = len - idx;
778
19872
                let v = self.stack.remove(pos);
779
19872
                self.stack.push(v);
780
            }
781

            
782
            // ── Point alignment ──────────────────────────────────
783
            0x27 => {
784
                // ALIGNPTS
785
                self.op_alignpts()?;
786
            }
787

            
788
            0x29 => {
789
                // UTP - untouch point
790
                let p = self.pop()? as u32;
791
                let zp0 = self.gs.zp0 as usize;
792
                if let Some(flags) = self.zones[zp0].flags.get_mut(p as usize) {
793
                    // Clear touched flags based on freedom vector
794
                    if self.gs.freedom_vector.0.to_bits() != 0 {
795
                        flags.remove(PointFlags::TOUCHED_X);
796
                    }
797
                    if self.gs.freedom_vector.1.to_bits() != 0 {
798
                        flags.remove(PointFlags::TOUCHED_Y);
799
                    }
800
                }
801
            }
802

            
803
            // ── Function calls ───────────────────────────────────
804
            0x2A => {
805
                // LOOPCALL
806
143424
                let fn_id = self.pop()? as u32;
807
143424
                let count = self.pop()? as u32;
808
143424
                if self.call_depth >= MAX_CALL_DEPTH {
809
                    return Err(HintError::CallStackOverflow);
810
143424
                }
811
143424
                if count > 10000 {
812
                    return Err(HintError::ExceededMaxInstructions);
813
143424
                }
814
143424
                let func = self
815
143424
                    .fdefs
816
143424
                    .get(fn_id as usize)
817
143424
                    .and_then(|f| f.as_ref())
818
143424
                    .ok_or(HintError::UndefinedFunction(fn_id))?
819
                    .bytecode
820
143424
                    .clone();
821
143424
                for _ in 0..count {
822
509814
                    self.call_depth += 1;
823
509814
                    self.execute(&func)?;
824
498474
                    self.call_depth -= 1;
825
                }
826
            }
827
            0x2B => {
828
                // CALL
829
421146
                let fn_id = self.pop()? as u32;
830
421146
                if self.call_depth >= MAX_CALL_DEPTH {
831
                    return Err(HintError::CallStackOverflow);
832
421146
                }
833
421146
                let func = self
834
421146
                    .fdefs
835
421146
                    .get(fn_id as usize)
836
421146
                    .and_then(|f| f.as_ref())
837
421146
                    .ok_or(HintError::UndefinedFunction(fn_id))?
838
                    .bytecode
839
421146
                    .clone();
840
421146
                self.call_depth += 1;
841
421146
                self.execute(&func)?;
842
421146
                self.call_depth -= 1;
843
            }
844
            0x2C => {
845
                // FDEF - function definition
846
1200960
                let fn_id = self.pop()? as u32;
847
1200960
                let start = *ip;
848
                // Scan forward to find ENDF (0x2D), handling nested FDEF/ENDF
849
1200960
                let end = self.find_endf(bytecode, ip)?;
850
1200960
                let func_bytecode = bytecode[start..end].to_vec();
851
1200960
                if (fn_id as usize) < self.fdefs.len() {
852
1200960
                    self.fdefs[fn_id as usize] = Some(FuncDef {
853
1200960
                        bytecode: func_bytecode,
854
1200960
                    });
855
1200960
                }
856
                // ip now points past ENDF
857
            }
858
            0x2D => {
859
                // ENDF - end of function
860
                // In normal execution flow (inside CALL), this returns from execute()
861
                return Ok(());
862
            }
863

            
864
            // ── Point movement ───────────────────────────────────
865
5002722
            0x2E..=0x2F => {
866
                // MDAP[r] - move direct absolute point
867
50652
                let round = opcode & 1 != 0;
868
50652
                self.op_mdap(round)?;
869
            }
870
4952070
            0x30..=0x31 => {
871
                // IUP[a] - interpolate untouched points
872
6156
                let axis = opcode & 1; // 0 = y, 1 = x
873
6156
                self.op_iup(axis)?;
874
                // In v40 mode, snapshot Y values right after IUP[Y] runs
875
                // so post-IUP function calls can't modify them.
876
6156
                if axis == 0 && self.snapshot_iup_y {
877
                    self.iup_y_snapshot = Some(
878
                        self.zones[1].current.iter().map(|p| p.y).collect()
879
                    );
880
6156
                }
881
            }
882
4945914
            0x32..=0x33 => {
883
                // SHP[a] - shift point
884
                // SHP[0] (0x32) uses rp2/zp1, SHP[1] (0x33) uses rp1/zp0
885
108
                let use_rp1 = opcode & 1 != 0;
886
108
                self.op_shp(use_rp1)?;
887
            }
888
4945806
            0x34..=0x35 => {
889
                // SHC[a] - shift contour
890
                // SHC[0] (0x34) uses rp2/zp1, SHC[1] (0x35) uses rp1/zp0
891
                let use_rp1 = opcode & 1 != 0;
892
                self.op_shc(use_rp1)?;
893
            }
894
4945806
            0x36..=0x37 => {
895
                // SHZ[a] - shift zone
896
                // SHZ[0] (0x36) uses rp2/zp1, SHZ[1] (0x37) uses rp1/zp0
897
                let use_rp1 = opcode & 1 != 0;
898
                self.op_shz(use_rp1)?;
899
            }
900
            0x38 => {
901
                // SHPIX - shift point by pixel amount
902
35262
                self.op_shpix()?;
903
            }
904
            0x39 => {
905
                // IP - interpolate point
906
                self.op_ip()?;
907
            }
908
4945806
            0x3A..=0x3B => {
909
                // MSIRP[a] - move stack indirect relative point
910
                let set_rp0 = opcode & 1 != 0;
911
                self.op_msirp(set_rp0)?;
912
            }
913
            0x3C => {
914
                // ALIGNRP - align relative point
915
105570
                self.op_alignrp()?;
916
            }
917
            0x3D => {
918
                // RTDG - round to double grid
919
                self.gs.round_state = RoundState::DoubleGrid;
920
            }
921
4945806
            0x3E..=0x3F => {
922
                // MIAP[r] - move indirect absolute point
923
36180
                let round = opcode & 1 != 0;
924
36180
                self.op_miap(round)?;
925
            }
926

            
927
            // ── Push instructions ────────────────────────────────
928
            0x40 => {
929
                // NPUSHB - push n bytes
930
26730
                if *ip >= bytecode.len() {
931
                    return Err(HintError::UnexpectedEndOfBytecode);
932
26730
                }
933
26730
                let n = bytecode[*ip] as usize;
934
26730
                *ip += 1;
935
26730
                if *ip + n > bytecode.len() {
936
                    return Err(HintError::UnexpectedEndOfBytecode);
937
26730
                }
938
1549476
                for i in 0..n {
939
1549476
                    self.push(bytecode[*ip + i] as i32)?;
940
                }
941
26730
                *ip += n;
942
            }
943
            0x41 => {
944
                // NPUSHW - push n words
945
108
                if *ip >= bytecode.len() {
946
                    return Err(HintError::UnexpectedEndOfBytecode);
947
108
                }
948
108
                let n = bytecode[*ip] as usize;
949
108
                *ip += 1;
950
108
                if *ip + n * 2 > bytecode.len() {
951
                    return Err(HintError::UnexpectedEndOfBytecode);
952
108
                }
953
8154
                for i in 0..n {
954
                    // Cast high byte to i8 for proper sign extension.
955
8154
                    let hi = bytecode[*ip + i * 2] as i8;
956
8154
                    let lo = bytecode[*ip + i * 2 + 1] as u8;
957
8154
                    let val = ((hi as i32) << 8) | (lo as i32);
958
8154
                    self.push(val)?;
959
                }
960
108
                *ip += n * 2;
961
            }
962

            
963
            // ── Storage / CVT ────────────────────────────────────
964
            0x42 => {
965
                // WS - write storage
966
369792
                let val = self.pop()?;
967
369792
                let idx = self.pop()? as u32;
968
369792
                let i = idx as usize;
969
                // Dynamically grow storage if needed (fonts may exceed maxp limits)
970
369792
                if i >= self.storage.len() {
971
                    if i > 10_000 {
972
                        return Err(HintError::InvalidStorageIndex(idx));
973
                    }
974
                    self.storage.resize(i + 1, 0);
975
369792
                }
976
369792
                self.storage[i] = val;
977
            }
978
            0x43 => {
979
                // RS - read storage
980
973944
                let idx = self.pop()? as u32;
981
973944
                let i = idx as usize;
982
                // Dynamically grow storage if needed (reads default to 0)
983
973944
                if i >= self.storage.len() {
984
                    if i > 10_000 {
985
                        return Err(HintError::InvalidStorageIndex(idx));
986
                    }
987
                    self.storage.resize(i + 1, 0);
988
973944
                }
989
973944
                self.push(self.storage[i])?;
990
            }
991
            0x44 => {
992
                // WCVTP - write CVT in pixel units (F26Dot6)
993
392526
                let val = self.pop()?;
994
392526
                let idx = self.pop()? as u32;
995
392526
                let i = idx as usize;
996
392526
                if i >= self.cvt.len() {
997
                    // Bounds-check like WS/RS: an out-of-range (negative/huge)
998
                    // index must not trigger a multi-GB resize/OOM.
999
                    if i > 10_000 {
                        return Err(HintError::InvalidCvtIndex(idx));
                    }
                    self.cvt.resize(i + 1, 0);
392526
                }
392526
                self.cvt[i] = val;
            }
            0x45 => {
                // RCVT - read CVT
438210
                let idx = self.pop()? as u32;
438210
                let val = self.read_cvt(idx)?;
432540
                self.push(val)?;
            }
4909626
            0x46..=0x47 => {
                // GC[a] - get coordinate (0=current, 1=original)
163620
                let use_original = opcode & 1 != 0;
163620
                self.op_gc(use_original)?;
            }
            0x48 => {
                // SCFS - set coordinate from stack
                self.op_scfs()?;
            }
4746006
            0x49..=0x4A => {
                // MD[a] - measure distance
                // 0x49 = MD[0]: use current (grid-fitted) positions
                // 0x4A = MD[1]: use original (unhinted) positions
                // 0x49 = MD[0]: use current (grid-fitted) positions
                // 0x4A = MD[1]: use original (unhinted) positions
117558
                let use_original = opcode == 0x4A;
117558
                self.op_md(use_original)?;
            }
            0x4B => {
                // MPPEM - measure pixels per em
281934
                self.push(self.ppem as i32)?;
            }
            0x4C => {
                // MPS - measure point size (F26Dot6)
                self.push(self.point_size)?;
            }
            // ── Boolean / flip ───────────────────────────────────
            0x4D => {
                // FLIPON
                self.gs.auto_flip = true;
            }
            0x4E => {
                // FLIPOFF
                self.gs.auto_flip = false;
            }
            0x4F => {
                // DEBUG - no-op
            }
            // ── Comparison ───────────────────────────────────────
            0x50 => {
                // LT
712692
                let b = self.pop()?;
712692
                let a = self.pop()?;
712692
                self.push(if a < b { 1 } else { 0 })?;
            }
            0x51 => {
                // LTEQ
24408
                let b = self.pop()?;
24408
                let a = self.pop()?;
24408
                self.push(if a <= b { 1 } else { 0 })?;
            }
            0x52 => {
                // GT
74520
                let b = self.pop()?;
74520
                let a = self.pop()?;
74520
                self.push(if a > b { 1 } else { 0 })?;
            }
            0x53 => {
                // GTEQ
76410
                let b = self.pop()?;
76410
                let a = self.pop()?;
76410
                self.push(if a >= b { 1 } else { 0 })?;
            }
            0x54 => {
                // EQ
7992
                let b = self.pop()?;
7992
                let a = self.pop()?;
7992
                self.push(if a == b { 1 } else { 0 })?;
            }
            0x55 => {
                // NEQ
81540
                let b = self.pop()?;
81540
                let a = self.pop()?;
81540
                self.push(if a != b { 1 } else { 0 })?;
            }
            0x56 => {
                // ODD
                let v = self.pop()?;
                let rounded = self.gs.round(F26Dot6::from_bits(v));
                self.push(if (rounded.to_i32() & 1) != 0 { 1 } else { 0 })?;
            }
            0x57 => {
                // EVEN
                let v = self.pop()?;
                let rounded = self.gs.round(F26Dot6::from_bits(v));
                self.push(if (rounded.to_i32() & 1) == 0 { 1 } else { 0 })?;
            }
            // ── IF / ELSE / EIF ──────────────────────────────────
            0x58 => {
                // IF
938412
                let cond = self.pop()?;
938412
                if cond == 0 {
722574
                    self.skip_to_else_or_eif(bytecode, ip)?;
215838
                }
            }
230040
            0x59 => {
230040
                // EIF - end if (no-op when reached normally)
230040
            }
            // ── Logic ────────────────────────────────────────────
            0x5A => {
                // AND
32778
                let b = self.pop()?;
32778
                let a = self.pop()?;
32778
                self.push(if a != 0 && b != 0 { 1 } else { 0 })?;
            }
            0x5B => {
                // OR
10098
                let b = self.pop()?;
10098
                let a = self.pop()?;
10098
                self.push(if a != 0 || b != 0 { 1 } else { 0 })?;
            }
            0x5C => {
                // NOT
                let v = self.pop()?;
                self.push(if v == 0 { 1 } else { 0 })?;
            }
            // ── Delta instructions ───────────────────────────────
            0x5D => self.op_deltap(1, bytecode, ip)?, // DELTAP1
            0x5E => {
                // SDB - set delta base
                let new_base = self.pop()? as u16;
                self.gs.delta_base = new_base;
            }
            0x5F => {
                // SDS - set delta shift
                self.gs.delta_shift = self.pop()? as u16;
            }
            // ── Arithmetic ───────────────────────────────────────
            0x60 => {
                // ADD
1184544
                let b = self.pop()?;
1184544
                let a = self.pop()?;
1184544
                self.push(a.wrapping_add(b))?;
            }
            0x61 => {
                // SUB
210546
                let b = self.pop()?;
210546
                let a = self.pop()?;
210546
                self.push(a.wrapping_sub(b))?;
            }
            0x62 => {
                // DIV
403326
                let b = self.pop()?;
403326
                if b == 0 {
                    return Err(HintError::DivideByZero);
403326
                }
403326
                let a = self.pop()?;
                // F26Dot6 division: a * 64 / b with FreeType-compatible rounding
403326
                let result = ft_muldiv(a as i64, 64, b as i64);
403326
                self.push(result as i32)?;
            }
            0x63 => {
                // MUL
785322
                let b = self.pop()?;
785322
                let a = self.pop()?;
                // F26Dot6 multiplication: a * b / 64 with FreeType-compatible rounding
785322
                let result = ft_muldiv(a as i64, b as i64, 64);
785322
                self.push(result as i32)?;
            }
            0x64 => {
                // ABS
21654
                let v = self.pop()?;
21654
                self.push(v.abs())?;
            }
            0x65 => {
                // NEG
32022
                let v = self.pop()?;
32022
                self.push(-v)?;
            }
            0x66 => {
                // FLOOR
410076
                let v = self.pop()?;
410076
                self.push(v & !63)?;
            }
            0x67 => {
                // CEILING
                let v = self.pop()?;
                self.push((v + 63) & !63)?;
            }
            // ── ROUND / NROUND ───────────────────────────────────
4628448
            0x68..=0x6B => {
                // ROUND[ab] - round value
                let v = self.pop()?;
                let result = self.gs.round(F26Dot6::from_bits(v));
                self.push(result.to_bits())?;
            }
4628448
            0x6C..=0x6F => {
                // NROUND[ab] - no-round (pass through)
                // Value stays on stack as-is
            }
            // ── More CVT / Delta ─────────────────────────────────
            0x70 => {
                // WCVTF - write CVT in FUnits
5670
                let val = self.pop()?; // value in FUnits
5670
                let idx = self.pop()? as u32;
5670
                let scaled = F26Dot6::from_funits(val, self.scale).to_bits();
5670
                let i = idx as usize;
5670
                if i >= self.cvt.len() {
                    if i > 10_000 {
                        return Err(HintError::InvalidCvtIndex(idx));
                    }
                    self.cvt.resize(i + 1, 0);
5670
                }
5670
                self.cvt[i] = scaled;
            }
            0x71 => self.op_deltap(2, bytecode, ip)?, // DELTAP2
            0x72 => self.op_deltap(3, bytecode, ip)?, // DELTAP3
            0x73 => self.op_deltac(1)?,               // DELTAC1
            0x74 => self.op_deltac(2)?,               // DELTAC2
            0x75 => self.op_deltac(3)?,               // DELTAC3
            // ── Super rounding ───────────────────────────────────
            0x76 => {
                // SROUND
                let n = self.pop()? as u32;
                self.gs.set_super_round(n, false);
                self.gs.round_state = RoundState::Super;
            }
            0x77 => {
                // S45ROUND
                let n = self.pop()? as u32;
                self.gs.set_super_round(n, true);
                self.gs.round_state = RoundState::Super45;
            }
            // ── Conditional jumps ────────────────────────────────
            0x78 => {
                // JROT - jump relative on true
28674
                let cond = self.pop()?;
28674
                let offset = self.pop()?;
28674
                if cond != 0 {
                    // offset is relative to the JROT opcode position (*ip - 1)
22734
                    let opcode_pos = *ip as i64 - 1;
22734
                    let new_ip = opcode_pos + offset as i64;
22734
                    if new_ip < 0 || new_ip > bytecode.len() as i64 {
                        return Err(HintError::InvalidJump);
22734
                    }
22734
                    *ip = new_ip as usize;
5940
                }
            }
            0x79 => {
                // JROF - jump relative on false
                let cond = self.pop()?;
                let offset = self.pop()?;
                if cond == 0 {
                    // offset is relative to the JROF opcode position (*ip - 1)
                    let opcode_pos = *ip as i64 - 1;
                    let new_ip = opcode_pos + offset as i64;
                    if new_ip < 0 || new_ip > bytecode.len() as i64 {
                        return Err(HintError::InvalidJump);
                    }
                    *ip = new_ip as usize;
                }
            }
            0x7A => {
                // ROFF - round off
                self.gs.round_state = RoundState::Off;
            }
            0x7C => {
                // RUTG - round up to grid
                self.gs.round_state = RoundState::UpToGrid;
            }
            0x7D => {
                // RDTG - round down to grid
                self.gs.round_state = RoundState::DownToGrid;
            }
            0x7E => {
                // SANGW (obsolete) - no-op
                self.pop()?;
            }
            0x7F => {
                // AA (obsolete) - no-op
                self.pop()?;
            }
            // ── Flip instructions ────────────────────────────────
            0x80 => {
                // FLIPPT
                self.op_flippt()?;
            }
            0x81 => {
                // FLIPRGON
                let hi = self.pop()? as usize;
                let lo = self.pop()? as usize;
                // Clamp the range to the glyph zone so a huge/negative index
                // (e.g. -1 -> usize::MAX) does not spin an unbounded loop.
                let len = self.zones[1].flags.len();
                if lo < len {
                    let hi = hi.min(len - 1);
                    for i in lo..=hi {
                        self.zones[1].flags[i].insert(PointFlags::ON_CURVE);
                    }
                }
            }
            0x82 => {
                // FLIPRGOFF
                let hi = self.pop()? as usize;
                let lo = self.pop()? as usize;
                let len = self.zones[1].flags.len();
                if lo < len {
                    let hi = hi.min(len - 1);
                    for i in lo..=hi {
                        self.zones[1].flags[i].remove(PointFlags::ON_CURVE);
                    }
                }
            }
            0x85 => {
                // SCANCTRL
702
                self.gs.scan_control = self.pop()? as u32;
            }
4628448
            0x86..=0x87 => {
                // SDPVTL[a] - set dual projection vector to line
                let a = opcode & 1;
                self.op_sdpvtl(a != 0)?;
            }
            0x88 => {
                // GETINFO
                self.op_getinfo()?;
            }
            0x89 => {
                // IDEF - instruction definition
                let instr_id = self.pop()? as u32;
                let start = *ip;
                let end = self.find_endf(bytecode, ip)?;
                let func_bytecode = bytecode[start..end].to_vec();
                if (instr_id as usize) < self.idefs.len() {
                    self.idefs[instr_id as usize] = Some(FuncDef {
                        bytecode: func_bytecode,
                    });
                }
            }
            0x8A => {
                // ROLL - roll top 3 stack elements
134946
                let len = self.stack.len();
134946
                if len < 3 {
                    return Err(HintError::StackUnderflow);
134946
                }
134946
                let a = self.stack[len - 1]; // top
134946
                let b = self.stack[len - 2]; // second
134946
                let c = self.stack[len - 3]; // third
                // Move third to top: [a,b,c] → [c,a,b]
134946
                self.stack[len - 1] = c;
134946
                self.stack[len - 2] = a;
134946
                self.stack[len - 3] = b;
            }
            0x8B => {
                // MAX
                let b = self.pop()?;
                let a = self.pop()?;
                self.push(a.max(b))?;
            }
            0x8C => {
                // MIN
                let b = self.pop()?;
                let a = self.pop()?;
                self.push(a.min(b))?;
            }
            0x8D => {
                // SCANTYPE
702
                self.gs.scan_type = self.pop()?;
            }
            0x8E => {
                // INSTCTRL - pops selector (s) then value (v)
                let s = self.pop()? as u32;
                let v = self.pop()? as u32;
                if s >= 1 && s <= 3 {
                    let mask = 1u8 << (s - 1);
                    if v != 0 {
                        self.gs.instruct_control |= mask;
                    } else {
                        self.gs.instruct_control &= !mask;
                    }
                }
            }
            // ── PUSHB / PUSHW ────────────────────────────────────
4628448
            0xB0..=0xB7 => {
                // PUSHB[n] - push n+1 bytes
4235112
                let count = (opcode - 0xB0 + 1) as usize;
4235112
                if *ip + count > bytecode.len() {
                    return Err(HintError::UnexpectedEndOfBytecode);
4235112
                }
4870908
                for i in 0..count {
4870908
                    self.push(bytecode[*ip + i] as i32)?;
                }
4235112
                *ip += count;
            }
393336
            0xB8..=0xBF => {
                // PUSHW[n] - push n+1 words (signed 16-bit)
392796
                let count = (opcode - 0xB8 + 1) as usize;
392796
                if *ip + count * 2 > bytecode.len() {
                    return Err(HintError::UnexpectedEndOfBytecode);
392796
                }
417528
                for i in 0..count {
417528
                    let hi = bytecode[*ip + i * 2] as i8;
417528
                    let lo = bytecode[*ip + i * 2 + 1];
417528
                    let val = ((hi as i32) << 8) | (lo as i32);
417528
                    self.push(val)?;
                }
392796
                *ip += count * 2;
            }
            // ── MDRP ─────────────────────────────────────────────
540
            0xC0..=0xDF => {
                // MDRP[abcde] - move direct relative point
270
                self.op_mdrp(opcode)?;
            }
            // ── MIRP ─────────────────────────────────────────────
270
            0xE0..=0xFF => {
                // MIRP[abcde] - move indirect relative point
270
                self.op_mirp(opcode)?;
            }
            // Unused / reserved opcodes
            _ => {
                // Check IDEFs for this opcode
                if let Some(Some(idef)) = self.idefs.get(opcode as usize) {
                    let func = idef.bytecode.clone();
                    self.call_depth += 1;
                    self.execute(&func)?;
                    self.call_depth -= 1;
                }
                // Otherwise silently ignore (compatibility)
            }
        }
18179046
        Ok(())
18196056
    }
    // ── Stack helpers ────────────────────────────────────────────────
15234480
    fn push(&mut self, val: i32) -> Result<(), HintError> {
15234480
        if self.stack.len() >= self.max_stack {
            return Err(HintError::StackOverflow);
15234480
        }
15234480
        self.stack.push(val);
15234480
        Ok(())
15234480
    }
15200460
    fn pop(&mut self) -> Result<i32, HintError> {
15200460
        self.stack.pop().ok_or(HintError::StackUnderflow)
15200460
    }
1913436
    fn peek(&self) -> Result<i32, HintError> {
1913436
        self.stack.last().copied().ok_or(HintError::StackUnderflow)
1913436
    }
474390
    fn read_cvt(&self, idx: u32) -> Result<i32, HintError> {
474390
        self.cvt
474390
            .get(idx as usize)
474390
            .copied()
474390
            .ok_or(HintError::InvalidCvtIndex(idx))
474390
    }
    // ── Control flow helpers ─────────────────────────────────────────
    /// Skip bytecode until matching ELSE or EIF for an IF whose condition was false.
722574
    fn skip_to_else_or_eif(
722574
        &self,
722574
        bytecode: &[u8],
722574
        ip: &mut usize,
722574
    ) -> Result<(), HintError> {
722574
        let mut depth = 1u32;
3654612
        while *ip < bytecode.len() {
3654612
            let op = bytecode[*ip];
3654612
            *ip += 1;
3654612
            match op {
8532
                0x58 => depth += 1, // nested IF
                0x59 => {
                    // EIF
647028
                    depth -= 1;
647028
                    if depth == 0 {
638496
                        return Ok(());
8532
                    }
                }
                0x1B => {
                    // ELSE
84780
                    if depth == 1 {
84078
                        return Ok(());
702
                    }
                }
                // Skip inline data for push instructions
                0x40 => {
                    // NPUSHB
1404
                    if *ip >= bytecode.len() {
                        return Err(HintError::UnexpectedEndOfBytecode);
1404
                    }
1404
                    let n = bytecode[*ip] as usize;
1404
                    *ip += 1 + n;
                }
                0x41 => {
                    // NPUSHW
                    if *ip >= bytecode.len() {
                        return Err(HintError::UnexpectedEndOfBytecode);
                    }
                    let n = bytecode[*ip] as usize;
                    *ip += 1 + n * 2;
                }
884844
                0xB0..=0xB7 => *ip += (op - 0xB0 + 1) as usize,
16308
                0xB8..=0xBF => *ip += ((op - 0xB8 + 1) * 2) as usize,
2028024
                _ => {}
            }
        }
        Err(HintError::UnexpectedEndOfBytecode)
722574
    }
    /// Skip from ELSE to matching EIF.
69876
    fn skip_else(
69876
        &self,
69876
        bytecode: &[u8],
69876
        ip: &mut usize,
69876
    ) -> Result<(), HintError> {
69876
        let mut depth = 1u32;
762696
        while *ip < bytecode.len() {
762696
            let op = bytecode[*ip];
762696
            *ip += 1;
762696
            match op {
48546
                0x58 => depth += 1, // nested IF
                0x59 => {
                    // EIF
118422
                    depth -= 1;
118422
                    if depth == 0 {
69876
                        return Ok(());
48546
                    }
                }
                // Skip inline data
                0x40 => {
2322
                    if *ip >= bytecode.len() {
                        return Err(HintError::UnexpectedEndOfBytecode);
2322
                    }
2322
                    let n = bytecode[*ip] as usize;
2322
                    *ip += 1 + n;
                }
                0x41 => {
                    if *ip >= bytecode.len() {
                        return Err(HintError::UnexpectedEndOfBytecode);
                    }
                    let n = bytecode[*ip] as usize;
                    *ip += 1 + n * 2;
                }
168048
                0xB0..=0xB7 => *ip += (op - 0xB0 + 1) as usize,
                0xB8..=0xBF => *ip += ((op - 0xB8 + 1) * 2) as usize,
425358
                _ => {}
            }
        }
        Err(HintError::UnexpectedEndOfBytecode)
69876
    }
    /// Find matching ENDF for a FDEF/IDEF, advancing ip past it.
1200960
    fn find_endf(&self, bytecode: &[u8], ip: &mut usize) -> Result<usize, HintError> {
1200960
        let mut depth = 1u32;
21516300
        while *ip < bytecode.len() {
21516300
            let op = bytecode[*ip];
21516300
            *ip += 1;
21516300
            match op {
                0x2C | 0x89 => depth += 1, // nested FDEF or IDEF
                0x2D => {
                    // ENDF
1200960
                    depth -= 1;
1200960
                    if depth == 0 {
1200960
                        return Ok(*ip - 1); // position of ENDF
                    }
                }
                // Skip inline data
                0x40 => {
                    if *ip >= bytecode.len() {
                        return Err(HintError::UnexpectedEndOfBytecode);
                    }
                    let n = bytecode[*ip] as usize;
                    *ip += 1 + n;
                }
                0x41 => {
                    if *ip >= bytecode.len() {
                        return Err(HintError::UnexpectedEndOfBytecode);
                    }
                    let n = bytecode[*ip] as usize;
                    *ip += 1 + n * 2;
                }
4473090
                0xB0..=0xB7 => *ip += (op - 0xB0 + 1) as usize,
347274
                0xB8..=0xBF => *ip += ((op - 0xB8 + 1) * 2) as usize,
15972930
                _ => {}
            }
        }
        Err(HintError::UnexpectedEndOfBytecode)
1200960
    }
    // ── Projection / freedom vector helpers ──────────────────────────
    /// Project a point onto the projection vector, returning F26Dot6 distance.
364932
    fn project(&self, p: Point) -> i32 {
364932
        let (px, py) = self.gs.projection_vector;
364932
        ((p.x as i64 * px.to_bits() as i64 + p.y as i64 * py.to_bits() as i64 + 0x2000) >> 14)
364932
            as i32
364932
    }
    /// Project using the dual projection vector (for measuring original distances).
109836
    fn dual_project(&self, p: Point) -> i32 {
109836
        let (px, py) = self.gs.dual_projection_vector;
109836
        ((p.x as i64 * px.to_bits() as i64 + p.y as i64 * py.to_bits() as i64 + 0x2000) >> 14)
109836
            as i32
109836
    }
    /// Move a point along the freedom vector by a given F26Dot6 distance.
193050
    fn move_point(&mut self, zone: usize, point: usize, distance: i32) {
        // Dynamically grow zone if needed, with cap to prevent OOM
193050
        if point >= self.zones[zone].current.len() {
            if point > 10_000 {
                return; // silently ignore bogus point indices
            }
            self.zones[zone].resize(point + 1);
193050
        }
193050
        let (fx, fy) = self.gs.freedom_vector;
193050
        let (px, py) = self.gs.projection_vector;
        // Compute the dot product of freedom and projection vectors
193050
        let dot = (fx.to_bits() as i64 * px.to_bits() as i64
193050
            + fy.to_bits() as i64 * py.to_bits() as i64
193050
            + 0x2000)
193050
            >> 14;
        // FreeType guarantees F_dot_P is never zero: when it computes to 0 it
        // substitutes 0x4000 (1.0) so the point is still displaced and touched.
193050
        let dot = if dot == 0 { 0x4000 } else { dot };
        // displacement = distance * freedom_vector / (freedom_vector · projection_vector)
        // Use FreeType-compatible signed division: convert to absolute values,
        // round with positive bias, then re-apply sign. This prevents
        // truncation-toward-zero errors that cause negative displacements
        // to under-apply by 1 F26Dot6 unit.
193050
        let dx = ft_muldiv(distance as i64, fx.to_bits() as i64, dot) as i32;
193050
        let dy = ft_muldiv(distance as i64, fy.to_bits() as i64, dot) as i32;
193050
        self.zones[zone].current[point].x += dx;
193050
        self.zones[zone].current[point].y += dy;
        // Set touched flags
193050
        if fx.to_bits() != 0 {
432
            self.zones[zone].flags[point].insert(PointFlags::TOUCHED_X);
192618
        }
193050
        if fy.to_bits() != 0 {
192618
            self.zones[zone].flags[point].insert(PointFlags::TOUCHED_Y);
192618
        }
193050
    }
    // ── Point / zone access helpers ──────────────────────────────────
342738
    fn get_point(&mut self, zone: usize, index: u32) -> Result<Point, HintError> {
342738
        let z = self.zones.get_mut(zone).ok_or(HintError::InvalidPointIndex(index))?;
342738
        let i = index as usize;
        // Dynamically grow zone if needed (twilight zone may exceed maxp limits)
        // Cap at reasonable limit to prevent OOM from corrupted indices
342738
        if i >= z.current.len() {
            if i > 10_000 {
                return Err(HintError::InvalidPointIndex(index));
            }
            z.resize(i + 1);
342738
        }
342738
        Ok(z.current[i])
342738
    }
356346
    fn get_original_point(&mut self, zone: usize, index: u32) -> Result<Point, HintError> {
356346
        let z = self.zones.get_mut(zone).ok_or(HintError::InvalidPointIndex(index))?;
356346
        let i = index as usize;
356346
        if i >= z.original.len() {
            if i > 10_000 {
                return Err(HintError::InvalidPointIndex(index));
            }
            z.resize(i + 1);
356346
        }
356346
        Ok(z.original[i])
356346
    }
    // ── Vector-from-line instructions ────────────────────────────────
    fn op_spvtl(&mut self, perpendicular: bool) -> Result<(), HintError> {
        // FreeType convention: top→p1 with zp2, lower→p2 with zp1
        // Vector direction: zp1[p2] - zp2[p1] (matching FreeType's DO_SPVTL)
        let p1_idx = self.pop()? as u32; // top of stack
        let p2_idx = self.pop()? as u32; // second
        let p2 = self.get_point(self.gs.zp1 as usize, p2_idx)?;
        let p1 = self.get_point(self.gs.zp2 as usize, p1_idx)?;
        let v = self.compute_vector_from_line(p1, p2, perpendicular);
        self.gs.projection_vector = v;
        self.gs.dual_projection_vector = v;
        Ok(())
    }
    fn op_sfvtl(&mut self, perpendicular: bool) -> Result<(), HintError> {
        // Same convention as SPVTL: top→p1/zp2, lower→p2/zp1
        let p1_idx = self.pop()? as u32;
        let p2_idx = self.pop()? as u32;
        let p2 = self.get_point(self.gs.zp1 as usize, p2_idx)?;
        let p1 = self.get_point(self.gs.zp2 as usize, p1_idx)?;
        self.gs.freedom_vector = self.compute_vector_from_line(p1, p2, perpendicular);
        Ok(())
    }
    fn op_sdpvtl(&mut self, perpendicular: bool) -> Result<(), HintError> {
        // Same convention as SPVTL: top→p1/zp2, lower→p2/zp1
        let p1_idx = self.pop()? as u32;
        let p2_idx = self.pop()? as u32;
        // Use current points for projection vector
        let p2 = self.get_point(self.gs.zp1 as usize, p2_idx)?;
        let p1 = self.get_point(self.gs.zp2 as usize, p1_idx)?;
        self.gs.projection_vector = self.compute_vector_from_line(p1, p2, perpendicular);
        // Use original points for dual projection vector
        let op2 = self.get_original_point(self.gs.zp1 as usize, p2_idx)?;
        let op1 = self.get_original_point(self.gs.zp2 as usize, p1_idx)?;
        self.gs.dual_projection_vector = self.compute_vector_from_line(op1, op2, perpendicular);
        Ok(())
    }
    fn compute_vector_from_line(
        &self,
        p1: Point,
        p2: Point,
        perpendicular: bool,
    ) -> (F2Dot14, F2Dot14) {
        let dx = (p2.x - p1.x) as f64;
        let dy = (p2.y - p1.y) as f64;
        let len = (dx * dx + dy * dy).sqrt();
        if len < 1.0e-10 {
            return (F2Dot14::ONE, F2Dot14::ZERO);
        }
        let (nx, ny) = if perpendicular {
            (-dy / len, dx / len)
        } else {
            (dx / len, dy / len)
        };
        (F2Dot14::from_f64(nx), F2Dot14::from_f64(ny))
    }
    // ── Point movement instructions ──────────────────────────────────
    /// MDAP[a] — Move Direct Absolute Point.
    /// Spec: MS OpenType §tt_instructions, Apple TrueType RM §5.
    /// Touches point `p` in zp0; if `round` is set, rounds the projected
    /// coordinate to grid.  Sets rp0 = rp1 = p.
    ///
    /// **Twilight zone**: if zp0 == 0, the original coordinate is copied
    /// from the current coordinate *before* any movement so that later
    /// instructions (MIRP, IP) that read the original get a meaningful
    /// value instead of the initial zero.
50652
    fn op_mdap(&mut self, round: bool) -> Result<(), HintError> {
50652
        let p = self.pop()? as u32;
50652
        let zp0 = self.gs.zp0 as usize;
        // Twilight zone: set original = current before projecting.
        // TODO: investigate correct twilight zone initialization
50652
        if zp0 == 0 {
50544
            let i = p as usize;
50544
            self.zones[0].ensure_capacity(i + 1);
50544
            self.zones[0].original[i] = self.zones[0].current[i];
50544
        }
50652
        let point = self.get_point(zp0, p)?;
50652
        let cur_dist = self.project(point);
50652
        let distance = if round {
108
            let rounded = self.gs.round(F26Dot6::from_bits(cur_dist));
108
            rounded.to_bits() - cur_dist
        } else {
50544
            0
        };
50652
        self.move_point(zp0, p as usize, distance);
50652
        self.gs.rp0 = p;
50652
        self.gs.rp1 = p;
50652
        Ok(())
50652
    }
    /// MIAP[a] — Move Indirect Absolute Point.
    /// Spec: MS OpenType §tt_instructions, Apple TrueType RM §5.
    /// Moves point `p` in zp0 to the CVT value (rounded if `a` bit set,
    /// subject to control_value_cut_in).  Sets rp0 = rp1 = p.
    ///
    /// **Twilight zone**: if zp0 == 0, the point's original *and* current
    /// coordinates are initialized from the CVT value along the freedom
    /// vector.  This is how the `prep` program creates twilight reference
    /// points that encode key font measurements (cap height, x-height,
    /// ascender, etc.).  Without this, twilight points stay at (0,0) and
    /// glyph programs that reference them via MIRP/IP get wrong distances.
36180
    fn op_miap(&mut self, round: bool) -> Result<(), HintError> {
36180
        let cvt_idx = self.pop()? as u32;
36180
        let p = self.pop()? as u32;
36180
        let zp0 = self.gs.zp0 as usize;
36180
        let cvt_val = self.read_cvt(cvt_idx)?;
        // Twilight zone: initialize point from CVT value along freedom vector.
        // Per TrueType spec, MIAP in twilight zone sets original and current
        // coordinates from the CVT value. Disabled pending further investigation
        // as it regresses HelveticaNeue hinting (the prep program's twilight zone
        // setup interacts differently than expected).
        // TODO: investigate correct twilight zone initialization
36180
        if zp0 == 0 {
36180
            let (fx, fy) = self.gs.freedom_vector;
36180
            let i = p as usize;
36180
            self.zones[0].ensure_capacity(i + 1);
36180
            let ox = ft_muldiv(cvt_val as i64, fx.to_bits() as i64, 0x4000) as i32;
36180
            let oy = ft_muldiv(cvt_val as i64, fy.to_bits() as i64, 0x4000) as i32;
36180
            self.zones[0].original[i] = Point { x: ox, y: oy };
36180
            self.zones[0].current[i] = Point { x: ox, y: oy };
36180
        }
36180
        let point = self.get_point(zp0, p)?;
36180
        let cur_dist = self.project(point);
36180
        let distance = if round {
            let diff = (cvt_val - cur_dist).abs();
            let target = if diff <= self.gs.control_value_cut_in.to_bits() {
                cvt_val
            } else {
                cur_dist
            };
            let rounded = self.gs.round(F26Dot6::from_bits(target));
            rounded.to_bits() - cur_dist
        } else {
36180
            cvt_val - cur_dist
        };
36180
        self.move_point(zp0, p as usize, distance);
36180
        self.gs.rp0 = p;
36180
        self.gs.rp1 = p;
36180
        Ok(())
36180
    }
    /// MDRP[abcde] — Move Direct Relative Point.
    /// Spec: MS OpenType §tt_instructions, Apple TrueType RM §5.
    /// Moves point `p` (in zp1) relative to rp0 (in zp0) by the *measured*
    /// original distance (no CVT lookup), with optional rounding, minimum
    /// distance, and single-width overrides.
270
    fn op_mdrp(&mut self, opcode: u8) -> Result<(), HintError> {
270
        let p = self.pop()? as u32;
270
        let zp0 = self.gs.zp0 as usize;
270
        let zp1 = self.gs.zp1 as usize;
        // Flags from opcode bits: 0xC0 + [set_rp0, respect_min_dist, round, _, _]
270
        let set_rp0 = (opcode >> 4) & 1 != 0;
270
        let respect_min_dist = (opcode >> 3) & 1 != 0;
270
        let do_round = (opcode >> 2) & 1 != 0;
        // Measure original distance between rp0 and point (before any adjustments)
270
        let rp0_orig = self.get_original_point(zp0, self.gs.rp0)?;
270
        let p_orig = self.get_original_point(zp1, p)?;
270
        let orig_dist = self.dual_project(Point {
270
            x: p_orig.x - rp0_orig.x,
270
            y: p_orig.y - rp0_orig.y,
270
        });
270
        let mut dist = orig_dist;
        // Apply single width
270
        let swv = self.gs.single_width_value.to_bits();
270
        if swv != 0 {
            let swci = self.gs.single_width_cut_in.to_bits();
            if (dist - swv).abs() < swci {
                dist = if dist >= 0 { swv } else { -swv };
            }
270
        }
270
        if do_round {
270
            dist = self.gs.round(F26Dot6::from_bits(dist)).to_bits();
270
        }
270
        if respect_min_dist {
54
            let min_dist = self.gs.minimum_distance.to_bits();
54
            if dist >= 0 {
54
                if dist < min_dist {
                    // When dist rounds to zero from a negative original distance,
                    // apply minimum in the ORIGINAL direction to avoid sign flip.
                    dist = if self.fix_min_distance_sign && dist == 0 && orig_dist < 0 { -min_dist } else { min_dist };
54
                }
            } else if dist > -min_dist {
                dist = -min_dist;
            }
216
        }
        // Current position of reference point
270
        let rp0_cur = self.get_point(zp0, self.gs.rp0)?;
270
        let p_cur = self.get_point(zp1, p)?;
270
        let cur_dist = self.project(Point {
270
            x: p_cur.x - rp0_cur.x,
270
            y: p_cur.y - rp0_cur.y,
270
        });
270
        self.move_point(zp1, p as usize, dist - cur_dist);
270
        self.gs.rp1 = self.gs.rp0;
270
        self.gs.rp2 = p;
270
        if set_rp0 {
216
            self.gs.rp0 = p;
216
        }
270
        Ok(())
270
    }
    /// MIRP[abcde] — Move Indirect Relative Point.
    /// Spec: MS OpenType §tt_instructions, Apple TrueType RM §5.
    /// Moves point `p` (in zp1) relative to rp0 (in zp0) by a CVT distance,
    /// with optional rounding, minimum distance, and auto-flip.
    ///
    /// **Twilight zone**: if zp1 == 0, the target point's original and current
    /// coordinates are set to rp0_orig + CVT distance along the freedom vector.
    /// This initializes the twilight point so that orig_dist calculations work.
270
    fn op_mirp(&mut self, opcode: u8) -> Result<(), HintError> {
270
        let cvt_idx = self.pop()? as u32;
270
        let p = self.pop()? as u32;
270
        let zp0 = self.gs.zp0 as usize;
270
        let zp1 = self.gs.zp1 as usize;
270
        let set_rp0 = (opcode >> 4) & 1 != 0;
270
        let respect_min_dist = (opcode >> 3) & 1 != 0;
270
        let do_round = (opcode >> 2) & 1 != 0;
270
        let cvt_val = if cvt_idx < self.cvt.len() as u32 {
270
            self.cvt[cvt_idx as usize]
        } else {
            0
        };
        // Twilight zone: initialize point from rp0_orig + CVT along freedom vector.
        // TODO: investigate correct twilight zone initialization
270
        if zp1 == 0 {
            let (fx, fy) = self.gs.freedom_vector;
            let rp0_orig = self.get_original_point(zp0, self.gs.rp0)?;
            let i = p as usize;
            self.zones[0].ensure_capacity(i + 1);
            let ox = rp0_orig.x + ft_muldiv(cvt_val as i64, fx.to_bits() as i64, 0x4000) as i32;
            let oy = rp0_orig.y + ft_muldiv(cvt_val as i64, fy.to_bits() as i64, 0x4000) as i32;
            self.zones[0].original[i] = Point { x: ox, y: oy };
            self.zones[0].current[i] = Point { x: ox, y: oy };
270
        }
        // Measure original distance
270
        let rp0_orig = self.get_original_point(zp0, self.gs.rp0)?;
270
        let p_orig = self.get_original_point(zp1, p)?;
270
        let orig_dist = self.dual_project(Point {
270
            x: p_orig.x - rp0_orig.x,
270
            y: p_orig.y - rp0_orig.y,
270
        });
270
        let mut dist = cvt_val;
        // Auto flip
270
        if self.gs.auto_flip {
270
            if (orig_dist >= 0) != (dist >= 0) {
162
                dist = -dist;
162
            }
        }
        // Apply single width
270
        let swv = self.gs.single_width_value.to_bits();
270
        if swv != 0 {
            let swci = self.gs.single_width_cut_in.to_bits();
            if (dist - swv).abs() < swci {
                dist = if dist >= 0 { swv } else { -swv };
            }
270
        }
        // CVT cut-in: if actual distance is too far from CVT value, use actual.
        // FreeType applies this only when reference and target share a zone
        // (gep0 == gep1) and independently of the rounding bit.
270
        let cvt_ci = self.gs.control_value_cut_in.to_bits();
270
        if zp0 == zp1 && (dist - orig_dist).abs() > cvt_ci {
            dist = orig_dist;
270
        }
270
        if do_round {
270
            dist = self.gs.round(F26Dot6::from_bits(dist)).to_bits();
270
        }
270
        if respect_min_dist {
270
            let min_dist = self.gs.minimum_distance.to_bits();
270
            if dist >= 0 {
108
                if dist < min_dist {
                    // When dist rounds to zero from a negative original distance,
                    // apply minimum in the ORIGINAL direction to avoid sign flip.
                    dist = if self.fix_min_distance_sign && dist == 0 && orig_dist < 0 { -min_dist } else { min_dist };
108
                }
162
            } else if dist > -min_dist {
                dist = -min_dist;
162
            }
        }
        // Move point
270
        let rp0_cur = self.get_point(zp0, self.gs.rp0)?;
270
        let p_cur = self.get_point(zp1, p)?;
270
        let cur_dist = self.project(Point {
270
            x: p_cur.x - rp0_cur.x,
270
            y: p_cur.y - rp0_cur.y,
270
        });
270
        self.move_point(zp1, p as usize, dist - cur_dist);
270
        self.gs.rp1 = self.gs.rp0;
270
        self.gs.rp2 = p;
270
        if set_rp0 {
54
            self.gs.rp0 = p;
216
        }
270
        Ok(())
270
    }
    /// MSIRP[a] — Move Stack Indirect Relative Point.
    /// Spec: MS OpenType §tt_instructions.
    /// Moves point `p` (in zp1) so its distance from rp0 (in zp0) along
    /// the projection vector equals `dist` (popped from stack in F26Dot6).
    ///
    /// **Twilight zone**: if zp1 == 0, initialize point from rp0_orig.
    fn op_msirp(&mut self, set_rp0: bool) -> Result<(), HintError> {
        let dist = self.pop()?; // F26Dot6
        let p = self.pop()? as u32;
        let zp0 = self.gs.zp0 as usize;
        let zp1 = self.gs.zp1 as usize;
        // Twilight zone: initialize point from rp0 original position
        // TODO: investigate correct twilight zone initialization
        if zp1 == 0 {
            let rp0_orig = self.get_original_point(zp0, self.gs.rp0)?;
            let i = p as usize;
            self.zones[0].ensure_capacity(i + 1);
            self.zones[0].original[i] = rp0_orig;
            self.zones[0].current[i] = rp0_orig;
        }
        let rp0_cur = self.get_point(zp0, self.gs.rp0)?;
        let p_cur = self.get_point(zp1, p)?;
        let cur_dist = self.project(Point {
            x: p_cur.x - rp0_cur.x,
            y: p_cur.y - rp0_cur.y,
        });
        self.move_point(zp1, p as usize, dist - cur_dist);
        self.gs.rp1 = self.gs.rp0;
        self.gs.rp2 = p;
        if set_rp0 {
            self.gs.rp0 = p;
        }
        Ok(())
    }
105570
    fn op_alignrp(&mut self) -> Result<(), HintError> {
105570
        let loop_count = self.gs.loop_value;
105570
        self.gs.loop_value = 1;
105570
        let zp0 = self.gs.zp0 as usize;
105570
        let zp1 = self.gs.zp1 as usize;
105570
        let rp0 = self.gs.rp0;
105570
        for _ in 0..loop_count {
105570
            let p = self.pop()? as u32;
105570
            let rp0_cur = self.get_point(zp0, rp0)?;
105570
            let p_cur = self.get_point(zp1, p)?;
105570
            let cur_dist = self.project(Point {
105570
                x: p_cur.x - rp0_cur.x,
105570
                y: p_cur.y - rp0_cur.y,
105570
            });
105570
            self.move_point(zp1, p as usize, -cur_dist);
        }
105570
        Ok(())
105570
    }
    fn op_alignpts(&mut self) -> Result<(), HintError> {
        let p2 = self.pop()? as u32;
        let p1 = self.pop()? as u32;
        let zp0 = self.gs.zp0 as usize;
        let zp1 = self.gs.zp1 as usize;
        let p1_cur = self.get_point(zp1, p1)?;
        let p2_cur = self.get_point(zp0, p2)?;
        let dist = self.project(Point {
            x: p2_cur.x - p1_cur.x,
            y: p2_cur.y - p1_cur.y,
        });
        let half = dist / 2;
        self.move_point(zp1, p1 as usize, half);
        self.move_point(zp0, p2 as usize, -half);
        Ok(())
    }
108
    fn op_shp(&mut self, use_rp1: bool) -> Result<(), HintError> {
108
        let loop_count = self.gs.loop_value;
108
        self.gs.loop_value = 1;
108
        let (rp, rp_zone) = if use_rp1 {
            (self.gs.rp1, self.gs.zp0 as usize)
        } else {
108
            (self.gs.rp2, self.gs.zp1 as usize)
        };
108
        let zp2 = self.gs.zp2 as usize;
        // Compute displacement: difference between current and original of rp
108
        let rp_cur = self.get_point(rp_zone, rp)?;
108
        let rp_orig = self.get_original_point(rp_zone, rp)?;
108
        let displacement = self.project(Point {
108
            x: rp_cur.x - rp_orig.x,
108
            y: rp_cur.y - rp_orig.y,
108
        });
108
        for _ in 0..loop_count {
108
            let p = self.pop()? as u32;
108
            self.move_point(zp2, p as usize, displacement);
        }
108
        Ok(())
108
    }
    fn op_shc(&mut self, use_rp1: bool) -> Result<(), HintError> {
        let contour = self.pop()? as usize;
        let (rp, rp_zone) = if use_rp1 {
            (self.gs.rp1, self.gs.zp0 as usize)
        } else {
            (self.gs.rp2, self.gs.zp1 as usize)
        };
        let zp2 = self.gs.zp2 as usize;
        let rp_cur = self.get_point(rp_zone, rp)?;
        let rp_orig = self.get_original_point(rp_zone, rp)?;
        let displacement = self.project(Point {
            x: rp_cur.x - rp_orig.x,
            y: rp_cur.y - rp_orig.y,
        });
        // Get contour point range
        let start = if contour == 0 {
            0
        } else {
            self.zones[zp2]
                .contour_ends
                .get(contour - 1)
                .map(|&e| e as usize + 1)
                .unwrap_or(0)
        };
        let end = self.zones[zp2]
            .contour_ends
            .get(contour)
            .map(|&e| e as usize + 1)
            .unwrap_or(self.zones[zp2].len());
        for i in start..end {
            if i as u32 != rp {
                self.move_point(zp2, i, displacement);
            }
        }
        Ok(())
    }
    fn op_shz(&mut self, use_rp1: bool) -> Result<(), HintError> {
        let zone_idx = self.pop()? as u32;
        if zone_idx > 1 {
            return Err(HintError::InvalidZone(zone_idx));
        }
        let (rp, rp_zone) = if use_rp1 {
            (self.gs.rp1, self.gs.zp0 as usize)
        } else {
            (self.gs.rp2, self.gs.zp1 as usize)
        };
        let rp_cur = self.get_point(rp_zone, rp)?;
        let rp_orig = self.get_original_point(rp_zone, rp)?;
        let displacement = self.project(Point {
            x: rp_cur.x - rp_orig.x,
            y: rp_cur.y - rp_orig.y,
        });
        // FreeType's Ins_SHZ shifts the whole zone via Move_Zp2_Point(..., FALSE):
        // the points are moved but NOT marked touched, so a later IUP still
        // re-interpolates them. Mirror move_point's freedom-vector math inline
        // (F_dot_P clamped like FreeType) without inserting the touch flags.
        let (fx, fy) = self.gs.freedom_vector;
        let (px, py) = self.gs.projection_vector;
        let dot = {
            let d = (fx.to_bits() as i64 * px.to_bits() as i64
                + fy.to_bits() as i64 * py.to_bits() as i64
                + 0x2000)
                >> 14;
            if d == 0 {
                0x4000
            } else {
                d
            }
        };
        let dx = ft_muldiv(displacement as i64, fx.to_bits() as i64, dot) as i32;
        let dy = ft_muldiv(displacement as i64, fy.to_bits() as i64, dot) as i32;
        let z = zone_idx as usize;
        let n = self.zones[z].len();
        for i in 0..n {
            self.zones[z].current[i].x += dx;
            self.zones[z].current[i].y += dy;
        }
        Ok(())
    }
35262
    fn op_shpix(&mut self) -> Result<(), HintError> {
35262
        let dist = self.pop()?; // F26Dot6 pixels
35262
        let loop_count = self.gs.loop_value;
35262
        self.gs.loop_value = 1;
35262
        let zp2 = self.gs.zp2 as usize;
35262
        let (fx, fy) = self.gs.freedom_vector;
35262
        for _ in 0..loop_count {
35262
            let p = self.pop()? as u32;
35262
            let i = p as usize;
35262
            self.zones[zp2].ensure_capacity(i + 1);
            // Move directly along freedom vector (no projection)
            // Use ft_muldiv for correct signed rounding (FreeType's TT_MulFix14)
35262
            let dx = ft_muldiv(dist as i64, fx.to_bits() as i64, 0x4000) as i32;
35262
            let dy = ft_muldiv(dist as i64, fy.to_bits() as i64, 0x4000) as i32;
35262
            self.zones[zp2].current[i].x += dx;
35262
            self.zones[zp2].current[i].y += dy;
35262
            if fx.to_bits() != 0 {
                self.zones[zp2].flags[i].insert(PointFlags::TOUCHED_X);
35262
            }
35262
            if fy.to_bits() != 0 {
35262
                self.zones[zp2].flags[i].insert(PointFlags::TOUCHED_Y);
35262
            }
        }
35262
        Ok(())
35262
    }
    /// IP — Interpolate Point.
    /// Spec: MS OpenType §tt_instructions, Apple TrueType RM §5.
    /// For each point popped from the stack, interpolates its position
    /// along the projection vector so that its relative placement between
    /// rp1 and rp2 is preserved from the original outline to the current
    /// (hinted) outline.  The interpolation factor is computed from
    /// original distances (using dual_project), applied to the current
    /// range, then the point is moved along the freedom vector.
    fn op_ip(&mut self) -> Result<(), HintError> {
        let loop_count = self.gs.loop_value;
        self.gs.loop_value = 1;
        let zp0 = self.gs.zp0 as usize;
        let zp1 = self.gs.zp1 as usize;
        let zp2 = self.gs.zp2 as usize;
        // Get reference points (original and current)
        let rp1_orig = self.get_original_point(zp0, self.gs.rp1)?;
        let rp2_orig = self.get_original_point(zp1, self.gs.rp2)?;
        let rp1_cur = self.get_point(zp0, self.gs.rp1)?;
        let rp2_cur = self.get_point(zp1, self.gs.rp2)?;
        let orig_range = self.dual_project(Point {
            x: rp2_orig.x - rp1_orig.x,
            y: rp2_orig.y - rp1_orig.y,
        });
        let cur_range = self.project(Point {
            x: rp2_cur.x - rp1_cur.x,
            y: rp2_cur.y - rp1_cur.y,
        });
        for _ in 0..loop_count {
            let p = self.pop()? as u32;
            let p_orig = self.get_original_point(zp2, p)?;
            let p_cur = self.get_point(zp2, p)?;
            let orig_dist = self.dual_project(Point {
                x: p_orig.x - rp1_orig.x,
                y: p_orig.y - rp1_orig.y,
            });
            let new_dist = if orig_range != 0 {
                ft_muldiv(cur_range as i64, orig_dist as i64, orig_range as i64) as i32
            } else {
                orig_dist
            };
            let cur_dist = self.project(Point {
                x: p_cur.x - rp1_cur.x,
                y: p_cur.y - rp1_cur.y,
            });
            self.move_point(zp2, p as usize, new_dist - cur_dist);
        }
        Ok(())
    }
    /// IUP[a] — Interpolate Untouched Points.
    /// Spec: MS OpenType §tt_instructions, Apple TrueType RM §5.
    /// Final pass: for each contour, walks between consecutive touched
    /// points, interpolating untouched points to preserve their relative
    /// position in the original outline.  Uses `orus` (unscaled font-unit
    /// coordinates) for interpolation factors to avoid F26Dot6 rounding
    /// errors, matching FreeType's approach.
    ///
    /// Points outside the touched range are shifted by the nearest touched
    /// point's delta; points between two touched points are linearly
    /// interpolated using FreeType's FT_DivFix/FT_MulFix for precision.
6156
    fn op_iup(&mut self, axis: u8) -> Result<(), HintError> {
6156
        let n_points = self.zones[1].len();
6156
        if n_points == 0 {
            return Ok(());
6156
        }
6156
        let touched_flag = if axis == 1 {
108
            PointFlags::TOUCHED_X
        } else {
6048
            PointFlags::TOUCHED_Y
        };
        // Collect all (contour_start, contour_end, touched_points) first
        // to avoid borrowing self.zones[1] while calling self.iup_interp.
6156
        let mut work: Vec<(usize, usize, Vec<usize>)> = Vec::new();
6156
        let mut contour_start = 0usize;
6156
        let contour_ends: Vec<u16> = self.zones[1].contour_ends.clone();
15012
        for &contour_end_u16 in &contour_ends {
8856
            let contour_end = contour_end_u16 as usize;
8856
            if contour_end >= n_points {
                break;
8856
            }
8856
            let mut touched_points: Vec<usize> = Vec::new();
196884
            for i in contour_start..=contour_end {
196884
                if self.zones[1].flags[i].contains(touched_flag) {
91368
                    touched_points.push(i);
105516
                }
            }
8856
            if !touched_points.is_empty() {
8856
                work.push((contour_start, contour_end, touched_points));
8856
            }
8856
            contour_start = contour_end + 1;
        }
        // Now perform interpolation
15012
        for (contour_start, contour_end, touched_points) in &work {
8856
            let n_touched = touched_points.len();
91368
            for t in 0..n_touched {
91368
                let t1_idx = touched_points[t];
91368
                let t2_idx = touched_points[(t + 1) % n_touched];
91368
                self.iup_interp(*contour_start, *contour_end, t1_idx, t2_idx, axis);
91368
            }
        }
6156
        Ok(())
6156
    }
91368
    fn iup_interp(
91368
        &mut self,
91368
        contour_start: usize,
91368
        contour_end: usize,
91368
        t1_idx: usize,
91368
        t2_idx: usize,
91368
        axis: u8,
91368
    ) {
91368
        let touched_flag = if axis == 1 {
432
            PointFlags::TOUCHED_X
        } else {
90936
            PointFlags::TOUCHED_Y
        };
        // Walk from t1 to t2 (wrapping around contour), interpolating untouched points
91368
        let contour_len = contour_end - contour_start + 1;
658260
        let get_coord = |p: &Point| -> i32 {
658260
            if axis == 1 { p.x } else { p.y }
658260
        };
        // Use unscaled coordinates (orus) for interpolation factors — matches FreeType.
        // Unscaled integers avoid F26Dot6 rounding errors in range/factor computation.
91368
        let t1_orus = get_coord(&self.zones[1].orus[t1_idx]);
91368
        let t1_cur = get_coord(&self.zones[1].current[t1_idx]);
91368
        let t2_orus = get_coord(&self.zones[1].orus[t2_idx]);
91368
        let t2_cur = get_coord(&self.zones[1].current[t2_idx]);
91368
        let t1_orig = get_coord(&self.zones[1].original[t1_idx]);
91368
        let t2_orig = get_coord(&self.zones[1].original[t2_idx]);
91368
        let delta1 = t1_cur - t1_orig;
91368
        let delta2 = t2_cur - t2_orig;
        // Walk from t1+1 to t2-1 (wrapping)
91368
        if contour_len <= 1 {
            return;
91368
        }
91368
        let mut i = t1_idx;
        loop {
            // Advance to next point in contour (wrapping)
196884
            i = if i == contour_end {
8856
                contour_start
            } else {
188028
                i + 1
            };
196884
            if i == t2_idx {
91368
                break;
105516
            }
105516
            if self.zones[1].flags[i].contains(touched_flag) {
                continue;
105516
            }
            // Use unscaled coordinates for interpolation (FreeType uses orus)
105516
            let orus_i = get_coord(&self.zones[1].orus[i]);
105516
            let new_coord = if t1_orus == t2_orus {
                // Both reference points are at the same position: shift
756
                let cur = get_coord(&self.zones[1].current[i]);
756
                cur + delta1
            } else {
                // Interpolate using unscaled coordinates for range/factor
104760
                let lo_orus = t1_orus.min(t2_orus);
104760
                let hi_orus = t1_orus.max(t2_orus);
104760
                let lo_cur = if t1_orus < t2_orus { t1_cur } else { t2_cur };
104760
                let hi_cur = if t1_orus < t2_orus { t2_cur } else { t1_cur };
104760
                let lo_delta = if t1_orus < t2_orus { delta1 } else { delta2 };
104760
                let hi_delta = if t1_orus < t2_orus { delta2 } else { delta1 };
104760
                if orus_i <= lo_orus {
2376
                    let orig = get_coord(&self.zones[1].original[i]);
2376
                    orig + lo_delta
102384
                } else if orus_i >= hi_orus {
1404
                    let orig = get_coord(&self.zones[1].original[i]);
1404
                    orig + hi_delta
                } else {
100980
                    let range = (hi_orus - lo_orus) as i64;
100980
                    let factor = (orus_i - lo_orus) as i64;
100980
                    let scale = ft_divfix((hi_cur - lo_cur) as i64, range);
100980
                    lo_cur + ft_mulfix(factor, scale) as i32
                }
            };
105516
            if axis == 1 {
432
                self.zones[1].current[i].x = new_coord;
105084
            } else {
105084
                self.zones[1].current[i].y = new_coord;
105084
            }
        }
91368
    }
    // ── Coordinate / measurement ─────────────────────────────────────
163620
    fn op_gc(&mut self, use_original: bool) -> Result<(), HintError> {
163620
        let p = self.pop()? as u32;
163620
        let zp2 = self.gs.zp2 as usize;
163620
        let point = if use_original {
136566
            self.get_original_point(zp2, p)?
        } else {
27054
            self.get_point(zp2, p)?
        };
163620
        let val = self.project(point);
163620
        self.push(val)?;
163620
        Ok(())
163620
    }
    fn op_scfs(&mut self) -> Result<(), HintError> {
        let val = self.pop()?; // F26Dot6
        let p = self.pop()? as u32;
        let zp2 = self.gs.zp2 as usize;
        let point = self.get_point(zp2, p)?;
        let cur = self.project(point);
        self.move_point(zp2, p as usize, val - cur);
        Ok(())
    }
117558
    fn op_md(&mut self, use_original: bool) -> Result<(), HintError> {
117558
        let p2 = self.pop()? as u32;
117558
        let p1 = self.pop()? as u32;
117558
        let dist = if use_original {
109296
            let p1_pt = self.get_original_point(self.gs.zp0 as usize, p1)?;
109296
            let p2_pt = self.get_original_point(self.gs.zp1 as usize, p2)?;
109296
            self.dual_project(Point {
109296
                x: p2_pt.x - p1_pt.x,
109296
                y: p2_pt.y - p1_pt.y,
109296
            })
        } else {
8262
            let p1_pt = self.get_point(self.gs.zp0 as usize, p1)?;
8262
            let p2_pt = self.get_point(self.gs.zp1 as usize, p2)?;
8262
            self.project(Point {
8262
                x: p2_pt.x - p1_pt.x,
8262
                y: p2_pt.y - p1_pt.y,
8262
            })
        };
117558
        self.push(dist)?;
117558
        Ok(())
117558
    }
    // ── Intersection ─────────────────────────────────────────────────
    fn op_isect(&mut self) -> Result<(), HintError> {
        let b1 = self.pop()? as u32;
        let b0 = self.pop()? as u32;
        let a1 = self.pop()? as u32;
        let a0 = self.pop()? as u32;
        let p = self.pop()? as u32;
        // Per TrueType spec: a0,a1 define line A using zp0; b0,b1 define line B using zp1
        let pa0 = self.get_point(self.gs.zp0 as usize, a0)?;
        let pa1 = self.get_point(self.gs.zp0 as usize, a1)?;
        let pb0 = self.get_point(self.gs.zp1 as usize, b0)?;
        let pb1 = self.get_point(self.gs.zp1 as usize, b1)?;
        // Line A: pa0 to pa1, Line B: pb0 to pb1
        let dax = (pa1.x - pa0.x) as i64;
        let day = (pa1.y - pa0.y) as i64;
        let dbx = (pb1.x - pb0.x) as i64;
        let dby = (pb1.y - pb0.y) as i64;
        let denom = dax * dby - day * dbx;
        // FreeType Ins_ISECT rejects grazing (near-parallel) intersections with
        // a RELATIVE test: discriminant ∝ |da||db|sin(angle) (== denom here) and
        // dotproduct ∝ |da||db|cos(angle). When 19*|discriminant| <= |dotproduct|
        // the angle is too shallow, so snap to the midpoint instead of
        // extrapolating a far-off point from a tiny nonzero determinant.
        let dotproduct = dax * dbx + day * dby;
        let zp2 = self.gs.zp2 as usize;
        let i = p as usize;
        self.zones[zp2].ensure_capacity(i + 1);
        if 19 * denom.abs() <= dotproduct.abs() {
            // Lines are parallel or near-parallel; use midpoint of endpoints
            self.zones[zp2].current[i].x = (pa0.x + pa1.x + pb0.x + pb1.x) / 4;
            self.zones[zp2].current[i].y = (pa0.y + pa1.y + pb0.y + pb1.y) / 4;
        } else {
            let dpx = (pb0.x - pa0.x) as i64;
            let dpy = (pb0.y - pa0.y) as i64;
            let numer = dpx * dby - dpy * dbx;
            let t = ft_muldiv(numer, 64, denom);
            self.zones[zp2].current[i].x = pa0.x + ft_muldiv(dax, t, 64) as i32;
            self.zones[zp2].current[i].y = pa0.y + ft_muldiv(day, t, 64) as i32;
        }
        self.zones[zp2].flags[i].insert(PointFlags::TOUCHED_X | PointFlags::TOUCHED_Y);
        Ok(())
    }
    // ── Flip ─────────────────────────────────────────────────────────
    fn op_flippt(&mut self) -> Result<(), HintError> {
        let loop_count = self.gs.loop_value;
        self.gs.loop_value = 1;
        for _ in 0..loop_count {
            let p = self.pop()? as usize;
            // FLIPPT uses the glyph zone (zone 1), not zp0 - per TrueType spec
            // "Flips a point to on-curve or off-curve" in the glyph outline zone
            if let Some(flags) = self.zones[1].flags.get_mut(p) {
                flags.toggle(PointFlags::ON_CURVE);
            }
        }
        Ok(())
    }
    // ── Delta instructions ───────────────────────────────────────────
    //
    // Spec: Apple TrueType Reference Manual §5, Table 5 (magnitude encoding).
    // Stack pop order: n, then n pairs — the point/cvt index is on TOP (popped
    // first), the arg (ppem/magnitude) byte is below it (FreeType Ins_DELTAP:
    // point = stack[args+1], spec byte = stack[args]).
    // Magnitude encoding: bits 3-0 of arg:
    //   0=-8 steps, 1=-7, ..., 7=-1, 8=+1, 9=+2, ..., 15=+8
    // Step size: 1 / (1 << delta_shift) pixels (typically 1/8 px at shift=3).
    // DELTAP1/2/3 differ by range offset: 0, +16, +32 added to delta_base.
    // DELTAC1/2/3 modify CVT entries instead of moving points.
    /// DELTAP[n] — Delta Exception P.
    /// Moves point by a small amount at a specific ppem, for pixel-level tuning.
    fn op_deltap(
        &mut self,
        range: u8,
        _bytecode: &[u8],
        _ip: &mut usize,
    ) -> Result<(), HintError> {
        let n = self.pop()? as u32;
        let zp0 = self.gs.zp0 as usize;
        let delta_base = self.gs.delta_base as i32;
        let delta_shift = self.gs.delta_shift as i32;
        let range_offset = match range {
            1 => 0,
            2 => 16,
            3 => 32,
            _ => 0,
        };
        for _ in 0..n {
            // Point index is on top of the stack (FreeType Ins_DELTAP), then arg.
            let point_idx = self.pop()? as u32;
            let arg = self.pop()? as u32;
            let ppem_offset = ((arg >> 4) & 0x0F) as i32;
            let target_ppem = delta_base + range_offset + ppem_offset;
            if target_ppem == self.ppem as i32 {
                let magnitude = (arg & 0x0F) as i32;
                let delta = if magnitude < 8 {
                    // Spec: selector 0=-8, 1=-7, ..., 7=-1
                    -(8 - magnitude)
                } else {
                    // Spec: selector 8=+1, 9=+2, ..., 15=+8
                    magnitude - 7
                };
                // Scale by 1 / (1 << delta_shift)
                let scaled = if delta_shift > 0 {
                    delta * 64 / (1 << delta_shift)
                } else {
                    delta * 64
                };
                self.move_point(zp0, point_idx as usize, scaled);
            }
        }
        Ok(())
    }
    fn op_deltac(&mut self, range: u8) -> Result<(), HintError> {
        let n = self.pop()? as u32;
        let delta_base = self.gs.delta_base as i32;
        let delta_shift = self.gs.delta_shift as i32;
        let range_offset = match range {
            1 => 0,
            2 => 16,
            3 => 32,
            _ => 0,
        };
        for _ in 0..n {
            // TEST: try reversed pop order (cvt_idx first, then arg)
            let cvt_idx = self.pop()? as u32;
            let arg = self.pop()? as u32;
            let ppem_offset = ((arg >> 4) & 0x0F) as i32;
            let target_ppem = delta_base + range_offset + ppem_offset;
            if target_ppem == self.ppem as i32 {
                let magnitude = (arg & 0x0F) as i32;
                let delta = if magnitude < 8 {
                    // Spec: selector 0=-8, 1=-7, ..., 7=-1
                    -(8 - magnitude)
                } else {
                    // Spec: selector 8=+1, 9=+2, ..., 15=+8
                    magnitude - 7
                };
                let scaled = if delta_shift > 0 {
                    delta * 64 / (1 << delta_shift)
                } else {
                    delta * 64
                };
                let i = cvt_idx as usize;
                if i < self.cvt.len() {
                    self.cvt[i] += scaled;
                }
            }
        }
        Ok(())
    }
    // ── GETINFO ──────────────────────────────────────────────────────
    fn op_getinfo(&mut self) -> Result<(), HintError> {
        let selector = self.pop()? as u32;
        let mut result = 0u32;
        // Bit 0: engine version
        if selector & 1 != 0 {
            // Return version 40 (Windows DirectWrite / modern rasterizer).
            result |= 40;
        }
        // Bit 1: glyph rotated (we don't rotate, so false)
        // Bit 2: glyph stretched (we don't stretch, so false)
        // Bit 3: font variations active
        // (not currently supported in our interpreter)
        // Bit 5: grayscale rendering — we DO grayscale AA
        if selector & (1 << 5) != 0 {
            result |= 1 << 12; // grayscale bit
        }
        // Bit 6: ClearType enabled
        if selector & (1 << 6) != 0 {
            result |= 1 << 13; // ClearType enabled
        }
        self.push(result as i32)?;
        Ok(())
    }
}