Skip to content

Commit

Permalink
[skrifa] tthint: value stack/basic instructions (#767)
Browse files Browse the repository at this point in the history
Adds a type for managing the interpreter value stack along with instructions to manipulate the stack as well as basic arithmetic and logical instructions.

Covers implementations for 47 opcodes.

More progress on #620
  • Loading branch information
dfrg authored Feb 3, 2024
1 parent d0fb8b8 commit 4544eea
Show file tree
Hide file tree
Showing 8 changed files with 1,272 additions and 0 deletions.
99 changes: 99 additions & 0 deletions skrifa/src/outline/glyf/hint/code_state/args.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
//! Inline instruction arguments.

/// Support for decoding a sequence of bytes or words from the
/// instruction stream.
#[derive(Copy, Clone, Default, Debug)]
pub struct Args<'a> {
bytes: &'a [u8],
is_words: bool,
}

impl<'a> Args<'a> {
pub(crate) fn new(bytes: &'a [u8], is_words: bool) -> Self {
Self { bytes, is_words }
}

/// Returns the number of arguments in the list.
pub fn len(&self) -> usize {
if self.is_words {
self.bytes.len() / 2
} else {
self.bytes.len()
}
}

/// Returns true if the argument list is empty.
pub fn is_empty(&self) -> bool {
self.bytes.is_empty()
}

/// Returns an iterator over the argument values.
pub fn values(&self) -> impl Iterator<Item = i32> + 'a + Clone {
let bytes = if self.is_words { &[] } else { self.bytes };
let words = if self.is_words { self.bytes } else { &[] };
bytes
.iter()
.map(|byte| *byte as u32 as i32)
.chain(words.chunks_exact(2).map(|chunk| {
let word = ((chunk[0] as u16) << 8) | chunk[1] as u16;
// Double cast to ensure sign extension
word as i16 as i32
}))
}
}

/// Mock for testing arguments.
#[cfg(test)]
pub(crate) struct MockArgs {
bytes: Vec<u8>,
is_words: bool,
}

#[cfg(test)]
impl MockArgs {
pub fn from_bytes(bytes: &[u8]) -> Self {
Self {
bytes: bytes.into(),
is_words: false,
}
}

pub fn from_words(words: &[i16]) -> Self {
Self {
bytes: words
.iter()
.map(|word| *word as u16)
.flat_map(|word| vec![(word >> 8) as u8, word as u8])
.collect(),
is_words: true,
}
}

pub fn args(&self) -> Args {
Args {
bytes: &self.bytes,
is_words: self.is_words,
}
}
}

#[cfg(test)]
mod tests {
use super::MockArgs;

#[test]
fn byte_args() {
let values = [5, 2, 85, 92, 26, 42, u8::MIN, u8::MAX];
let mock = MockArgs::from_bytes(&values);
let decoded = mock.args().values().collect::<Vec<_>>();
assert!(values.iter().map(|x| *x as i32).eq(decoded.iter().copied()));
}

#[test]
fn word_args() {
let values = [-5, 2, 2845, 92, -26, 42, i16::MIN, i16::MAX];
let mock = MockArgs::from_words(&values);
let decoded = mock.args().values().collect::<Vec<_>>();
assert!(values.iter().map(|x| *x as i32).eq(decoded.iter().copied()));
}
}
8 changes: 8 additions & 0 deletions skrifa/src/outline/glyf/hint/code_state/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
//! State for managing active programs and decoding instructions.

mod args;

pub use args::Args;

#[cfg(test)]
pub(crate) use args::MockArgs;
244 changes: 244 additions & 0 deletions skrifa/src/outline/glyf/hint/engine/arith.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
//! Arithmetic and math instructions.
//!
//! See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#arithmetic-and-math-instructions>

// 10 instructions

use super::{super::math, Engine, HintErrorKind, OpResult};

impl<'a> Engine<'a> {
/// ADD[] (0x60)
///
/// Pops: n1, n2 (F26Dot6)
/// Pushes: (n2 + n1)
///
/// Pops n1 and n2 off the stack and pushes the sum of the two elements
/// onto the stack.
///
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#add>
/// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L2866>
pub(super) fn op_add(&mut self) -> OpResult {
self.value_stack.apply_binary(|a, b| Ok(a.wrapping_add(b)))
}

/// SUB[] (0x61)
///
/// Pops: n1, n2 (F26Dot6)
/// Pushes: (n2 - n1)
///
/// Pops n1 and n2 off the stack and pushes the difference of the two
/// elements onto the stack.
///
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#subtract>
/// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L2879>
pub(super) fn op_sub(&mut self) -> OpResult {
self.value_stack.apply_binary(|a, b| Ok(a.wrapping_sub(b)))
}

/// DIV[] (0x62)
///
/// Pops: n1, n2 (F26Dot6)
/// Pushes: (n2 / n1)
///
/// Pops n1 and n2 off the stack and pushes onto the stack the quotient
/// obtained by dividing n2 by n1. Note that this truncates rather than
/// rounds the value.
///
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#divide>
/// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L2892>
pub(super) fn op_div(&mut self) -> OpResult {
self.value_stack.apply_binary(|a, b| {
if b == 0 {
Err(HintErrorKind::DivideByZero)
} else {
Ok(math::mul_div_no_round(a, 64, b))
}
})
}

/// MUL[] (0x63)
///
/// Pops: n1, n2 (F26Dot6)
/// Pushes: (n2 * n1)
///
/// Pops n1 and n2 off the stack and pushes onto the stack the product of
/// the two elements.
///
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#multiply>
/// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L2909>
pub(super) fn op_mul(&mut self) -> OpResult {
self.value_stack
.apply_binary(|a, b| Ok(math::mul_div(a, b, 64)))
}

/// ABS[] (0x64)
///
/// Pops: n
/// Pushes: |n|: absolute value of n (F26Dot6)
///
/// Pops n off the stack and pushes onto the stack the absolute value of n.
///
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#absolute-value>
/// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L2922>
pub(super) fn op_abs(&mut self) -> OpResult {
self.value_stack.apply_unary(|n| Ok(n.wrapping_abs()))
}

/// NEG[] (0x65)
///
/// Pops: n1
/// Pushes: -n1: negation of n1 (F26Dot6)
///
/// This instruction pops n1 off the stack and pushes onto the stack the
/// negated value of n1.
///
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#negate>
/// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L2936>
pub(super) fn op_neg(&mut self) -> OpResult {
self.value_stack.apply_unary(|n1| Ok(n1.wrapping_neg()))
}

/// FLOOR[] (0x66)
///
/// Pops: n1: number whose floor is desired (F26Dot6)
/// Pushes: n: floor of n1 (F26Dot6)
///
/// Pops n1 and returns n, the greatest integer value less than or equal to n1.
///
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#floor>
/// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L2949>
pub(super) fn op_floor(&mut self) -> OpResult {
self.value_stack.apply_unary(|n1| Ok(math::floor(n1)))
}

/// CEILING[] (0x67)
///
/// Pops: n1: number whose ceiling is desired (F26Dot6)
/// Pushes: n: ceiling of n1 (F26Dot6)
///
/// Pops n1 and returns n, the least integer value greater than or equal to n1.
///
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#ceiling>
/// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L2962>
pub(super) fn op_ceiling(&mut self) -> OpResult {
self.value_stack.apply_unary(|n1| Ok(math::ceil(n1)))
}

/// MAX[] (0x8B)
///
/// Pops: e1, e2
/// Pushes: maximum of e1 and e2
///
/// Pops two elements, e1 and e2, from the stack and pushes the larger of
/// these two quantities onto the stack.
///
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#maximum-of-top-two-stack-elements>
/// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L3171>
pub(super) fn op_max(&mut self) -> OpResult {
self.value_stack.apply_binary(|a, b| Ok(a.max(b)))
}

/// MIN[] (0x8C)
///
/// Pops: e1, e2
/// Pushes: minimum of e1 and e2
///
/// Pops two elements, e1 and e2, from the stack and pushes the smaller
/// of these two quantities onto the stack.
///
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#minimum-of-top-two-stack-elements>
/// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L3185>
pub(super) fn op_min(&mut self) -> OpResult {
self.value_stack.apply_binary(|a, b| Ok(a.min(b)))
}
}

#[cfg(test)]
mod tests {
use super::{super::MockEngine, math, HintErrorKind};

/// Test the binary operations that don't require fixed point
/// arithmetic.
#[test]
fn simple_binops() {
let mut mock = MockEngine::new();
let mut engine = mock.engine();
for a in -10..=10 {
for b in -10..=10 {
let input = &[a, b];
engine.test_exec(input, a + b, |engine| {
engine.op_add().unwrap();
});
engine.test_exec(input, a - b, |engine| {
engine.op_sub().unwrap();
});
engine.test_exec(input, a.max(b), |engine| {
engine.op_max().unwrap();
});
engine.test_exec(input, a.min(b), |engine| {
engine.op_min().unwrap();
});
}
}
}

/// Test the unary operations that don't require fixed point
/// arithmetic.
#[test]
fn simple_unops() {
let mut mock = MockEngine::new();
let mut engine = mock.engine();
for a in -10..=10 {
let input = &[a];
engine.test_exec(input, -a, |engine| {
engine.op_neg().unwrap();
});
engine.test_exec(input, a.abs(), |engine| {
engine.op_abs().unwrap();
});
}
}

#[test]
fn f26dot6_binops() {
let mut mock = MockEngine::new();
let mut engine = mock.engine();
for a in -10..=10 {
for b in -10..=10 {
let a = a * 64 + 30;
let b = b * 64 - 30;
let input = &[a, b];
engine.test_exec(input, math::mul_div(a, b, 64), |engine| {
engine.op_mul().unwrap();
});
if b != 0 {
engine.test_exec(input, math::mul_div_no_round(a, 64, b), |engine| {
engine.op_div().unwrap();
});
} else {
engine.value_stack.push(a).unwrap();
engine.value_stack.push(b).unwrap();
assert!(matches!(engine.op_div(), Err(HintErrorKind::DivideByZero)));
}
}
}
}

#[test]
fn f26dot6_unops() {
let mut mock = MockEngine::new();
let mut engine = mock.engine();
for a in -10..=10 {
for b in -10..=10 {
let a = a * 64 + b;
let input = &[a];
engine.test_exec(input, math::floor(a), |engine| {
engine.op_floor().unwrap();
});
engine.test_exec(input, math::ceil(a), |engine| {
engine.op_ceiling().unwrap();
});
}
}
}
}
Loading

0 comments on commit 4544eea

Please sign in to comment.