c64 0.1.0-alpha.1

Driver for the Commodore 64 platform
Documentation
//! Kernal ROM constants and functions.

use core::{marker::PhantomData, ops::Neg};

use ufmt::derive::uDebug;

extern "C" {
    fn kernal_poly1(poly: *const u8);
    fn kernal_poly2(poly: *const u8);
    fn kernal_cos();
    fn kernal_sin();
    fn kernal_tan();
    fn kernal_atn();
    fn kernal_chrout(c: u8);
    fn kernal_settim(time: u32);
    fn kernal_rdtim() -> u32;
    fn kernal_screen() -> u16;
    fn kernal_plot_get() -> u16;
    fn kernal_plot_set(row: u8, column: u8);
}

/// Pointer to the trigonometry constants in the Kernel ROM.
pub const TRIG_CONSTANTS: *const TrigConstants = 0xe2e0 as *const _;

/// The trigonometry constants in the Kernel ROM.
#[repr(C, packed)]
pub struct TrigConstants {
    /// The approximation of π/2.
    pub pi_over_2: F40,
    /// The approximation of 2π.
    pub pi_times_2: F40,
    /// The encoding of 0.25.
    pub zero_point_2_5: F40,
    /// The polynomial table approximating `sin(2πX)` over the domain `[-0.25, 0.25]`.
    pub sin_2_pi_x: Polynomial<OddTerms, 6>,
    _atn_fn: [u8; 0x30],
    /// The polynomial table approximating `arctan(X)` over the domain `[-1, 1]`.
    pub atn: Polynomial<OddTerms, 12>,
}

/// A floating point number, encoded in C64 floating point format.
#[derive(Clone, Copy, uDebug)]
#[repr(C, packed)]
pub struct F40 {
    exp: u8,
    sign_mantissa: [u8; 4],
}

impl F40 {
    /// Constructs the floating point number `T = mantissa * 2^exponent`.
    ///
    /// Returns `None` if the given mantissa and exponent cannot be represented as a C64
    /// floating point number. Examples include:
    /// - `mantissa` cannot be normalised without `exponent` overflowing.
    /// - `exponent` would need to be `i8::MIN`.
    pub const fn new(mantissa: i32, exponent: i8) -> Option<Self> {
        let (sign, mut mantissa) = (mantissa.is_negative(), mantissa.unsigned_abs() as u32);

        let exp = if mantissa == 0 {
            // The encoding of `i8::MIN` is reserved for representing the number 0.
            Some(0)
        } else {
            // Normalize the mantissa and then clear its top bit.
            let exp_offset = mantissa.leading_zeros() as i8;
            mantissa = (mantissa << exp_offset) & (u32::MAX >> 1);

            // Stored in excess-128 representation, excluding the encoding of an exponent
            // of `i8::MIN` which was reserved above. The exponent needs to be offset for
            // both the mantissa normalization, and the re-definition of bit 31 of the
            // mantissa as 2^-1 instead of 2^31.
            let exp_offset = exp_offset - 32;
            match exponent.checked_sub(exp_offset) {
                Some(exp) if exp != i8::MIN => Some((exp as u8).wrapping_add(128)),
                _ => None,
            }
        };

        if let Some(exp) = exp {
            Some(Self {
                exp,
                sign_mantissa: ((if sign { 1 << 31 } else { 0 }) | mantissa).to_be_bytes(),
            })
        } else {
            None
        }
    }

    /// Returns the encoding of 0.0.
    pub const fn zero() -> Self {
        Self {
            exp: 0,
            sign_mantissa: [0; 4],
        }
    }

    /// Returns the encoding of 1.0.
    pub const fn one() -> Self {
        Self {
            exp: 0x81,
            sign_mantissa: [0; 4],
        }
    }
}

impl Neg for F40 {
    type Output = Self;

    fn neg(mut self) -> Self::Output {
        // Flip the sign bit.
        self.sign_mantissa[0] ^= 0b1000_0000;
        self
    }
}

/// A polynomial table.
#[derive(Clone, Copy)]
#[repr(C, packed)]
pub struct Polynomial<Terms, const N: usize> {
    // For an `OddTerms` polynomial, this is the degree of the polynomial that is actually
    // computed inside `kernal_poly2`, not the degree of the effective polynomial computed
    // by `kernal_poly1`.
    degree: u8,
    coefficients: [F40; N],
    _terms: PhantomData<Terms>,
}

pub struct AllTerms;
pub struct OddTerms;

impl<const N: usize> Polynomial<AllTerms, N> {
    /// Constructs a polynomial from its coefficients.
    pub const fn new(coefficients: [F40; N]) -> Option<Self> {
        match N.checked_sub(1) {
            Some(degree @ 0..=255) => Some(Self {
                degree: degree as u8,
                coefficients,
                _terms: PhantomData,
            }),
            _ => None,
        }
    }

    /// Sets `FAC` to the evaluation of this polynomial at `FAC`.
    ///
    /// # Safety
    ///
    /// - Do not call this function if the BASIC ROM overlay is disabled.
    #[inline]
    pub unsafe fn poly(&self) {
        // SAFETY: `Polynomial` and `F40` are `repr(C, packed)`, so they are guaranteed to
        // be layed out as contiguous bytes in the correct format.
        unsafe { kernal_poly2(self as *const _ as _) }
    }
}

impl<const N: usize> Polynomial<OddTerms, N> {
    /// Constructs a polynomial that only has non-zero coefficients at odd-powered terms.
    ///
    /// [`Self::poly`] is faster for compatible polynomials than if constructed via
    /// [`Polynomial::new`].
    pub const fn with_odd_terms(coefficients: [F40; N]) -> Option<Self> {
        match N.checked_sub(1) {
            Some(degree @ 0..=255) => Some(Self {
                degree: degree as u8,
                coefficients,
                _terms: PhantomData,
            }),
            _ => None,
        }
    }

    /// Sets `FAC` to the evaluation of this polynomial at `FAC`.
    ///
    /// # Safety
    ///
    /// - Do not call this function if the BASIC ROM overlay is disabled.
    #[inline]
    pub unsafe fn poly(&self) {
        // SAFETY: `Polynomial` and `F40` are `repr(C, packed)`, so they are guaranteed to
        // be layed out as contiguous bytes in the correct format.
        unsafe { kernal_poly1(self as *const _ as _) }
    }
}

/// Sets `FAC = cos(FAC)`.
///
/// # Safety
///
/// - Do not call this function if the BASIC ROM overlay is disabled.
#[inline]
pub unsafe fn cos() {
    unsafe { kernal_cos() }
}

/// Sets `FAC = sin(FAC)`.
///
/// # Safety
///
/// - Do not call this function if the BASIC ROM overlay is disabled.
#[inline]
pub unsafe fn sin() {
    unsafe { kernal_sin() }
}

/// Sets `FAC = tan(FAC)`.
///
/// # Safety
///
/// - Do not call this function if the BASIC ROM overlay is disabled.
#[inline]
pub unsafe fn tan() {
    unsafe { kernal_tan() }
}

/// Sets `FAC = arctan(FAC)`.
///
/// # Safety
///
/// - Do not call this function if the BASIC ROM overlay is disabled.
#[inline]
pub unsafe fn atn() {
    unsafe { kernal_atn() }
}

/// Writes a single PETSCII character to the current default output channel.
///
/// # Safety
///
/// - Do not call this function if the Kernal ROM overlay is disabled.
#[inline]
pub unsafe fn chrout(c: u8) {
    unsafe { kernal_chrout(c) }
}

/// Sets the system clock.
///
/// `jiffies` is in 60ths of a second since midnight.
///
/// # Safety
///
/// - Do not call this function if the Kernal ROM overlay is disabled.
#[inline]
pub unsafe fn settim(jiffies: u32) {
    unsafe { kernal_settim(jiffies) }
}

/// Reads system clock.
///
/// Returns the time in 60ths of a second.
///
/// # Safety
///
/// - Do not call this function if the Kernal ROM overlay is disabled.
#[inline]
pub unsafe fn rdtim() -> u32 {
    unsafe { kernal_rdtim() }
}

/// Returns the dimensions of the screen as `(cols, rows)`.
///
/// # Safety
///
/// - Do not call this function if the Kernal ROM overlay is disabled.
#[inline]
pub unsafe fn screen() -> (u8, u8) {
    let v = unsafe { kernal_screen() };
    ((v >> 8) as u8, (v & 0x00ff) as u8)
}

/// Returns the current position of the cursor as `(row, col)`.
///
/// # Safety
///
/// - Do not call this function if the Kernal ROM overlay is disabled.
#[inline]
pub unsafe fn plot_get() -> (u8, u8) {
    let v = unsafe { kernal_plot_get() };
    ((v >> 8) as u8, (v & 0x00ff) as u8)
}

/// Sets the cursor position.
///
/// # Safety
///
/// - Do not call this function if the Kernal ROM overlay is disabled.
#[inline]
pub unsafe fn plot_set(row: u8, col: u8) {
    unsafe { kernal_plot_set(row, col) };
}

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

    /// Asserts that `F40::new(mantissa, exponent)` produces a 5-byte encoding
    /// of `[exp, sm[0], sm[1], sm[2], sm[3]]`.
    fn check(mantissa: i32, exponent: i8, exp: u8, sm: [u8; 4]) {
        let f = F40::new(mantissa, exponent).expect("expected representable value");
        let got_exp = f.exp;
        let got_sm = f.sign_mantissa;
        assert_eq!(
            (got_exp, got_sm),
            (exp, sm),
            "F40::new({mantissa}, {exponent}) encoding mismatch",
        );
    }

    #[test]
    fn zero_is_canonical_regardless_of_exponent() {
        // The exponent argument is irrelevant when mantissa is 0: the C64
        // zero encoding is all-zero bytes.
        for exponent in [-128i8, -1, 0, 1, 127] {
            let f = F40::new(0, exponent).expect("0 is always representable");
            assert_eq!(
                (f.exp, f.sign_mantissa),
                (0, [0, 0, 0, 0]),
                "0 with exponent {exponent}",
            );
        }
    }

    #[test]
    fn one_matches_one_const() {
        // 1 = 0.1 * 2^1: biased exp 0x81, mantissa bytes all zero (the
        // leading 1 of the normalized mantissa is implicit and is replaced
        // in storage by the sign bit, which is 0 here).
        check(1, 0, 0x81, [0, 0, 0, 0]);

        let one_via_new = F40::new(1, 0).unwrap();
        let one_const = F40::one();
        assert_eq!(
            (one_via_new.exp, one_via_new.sign_mantissa),
            (one_const.exp, one_const.sign_mantissa),
            "F40::new(1, 0) must match F40::one()",
        );
    }

    #[test]
    fn negative_one_only_differs_in_sign_bit() {
        // -1 should be the same encoding as 1 except for bit 7 of byte 1
        // (the sign bit). If `new(1, 0)` and `new(-1, 0)` produce identical
        // bytes, the constructor isn't substituting the sign bit for the
        // implicit-leading-1 of the normalized mantissa.
        check(-1, 0, 0x81, [0x80, 0, 0, 0]);
    }

    #[test]
    fn powers_of_two_have_zero_mantissa() {
        // n = 2^k → biased exponent 0x81 + k, mantissa bytes all zero.
        check(2, 0, 0x82, [0, 0, 0, 0]);
        check(4, 0, 0x83, [0, 0, 0, 0]);
        check(1024, 0, 0x8b, [0, 0, 0, 0]);
        // Equivalent forms via the `exponent` argument.
        check(1, 1, 0x82, [0, 0, 0, 0]);
        check(1, 10, 0x8b, [0, 0, 0, 0]);
    }

    #[test]
    fn three_has_one_mantissa_bit_set() {
        // 3 = 0.11 * 2^2. Drop the implicit leading 1; the remaining
        // mantissa has its single set bit at position 30, which is bit 6
        // of `sign_mantissa[0]`.
        check(3, 0, 0x82, [0x40, 0, 0, 0]);
    }

    #[test]
    fn negative_exponent_yields_fraction() {
        // 0.5 = 1 * 2^-1 = 0.1 * 2^0 → biased exp 0x80.
        check(1, -1, 0x80, [0, 0, 0, 0]);
        check(-1, -1, 0x80, [0x80, 0, 0, 0]);
        // 0.25 = 0.1 * 2^-1 → biased exp 0x7f.
        check(1, -2, 0x7f, [0, 0, 0, 0]);
    }

    #[test]
    fn i32_min_does_not_overflow_abs() {
        // `i32::MIN.abs()` overflows two's complement; a correct
        // implementation must use `unsigned_abs` (or equivalent) when
        // splitting sign from magnitude.
        // -2^31 = -0.1 * 2^32 → biased exp 0xa0, sign bit set.
        check(i32::MIN, 0, 0xa0, [0x80, 0, 0, 0]);
    }

    #[test]
    fn i32_max_round_trips() {
        // 2^31 - 1 = 0.111...1 (31 ones) * 2^31. After dropping the
        // implicit leading 1, 30 ones followed by a single 0 remain in the
        // 31-bit stored mantissa.
        check(i32::MAX, 0, 0x9f, [0x7f, 0xff, 0xff, 0xfe]);
    }

    #[test]
    fn exponent_overflow_returns_none() {
        // F40's maximum unbiased exponent is 127 (biased 0xff). For
        // mantissa = 1 the F40 unbiased exponent is `exponent + 1`, so
        // `exponent = 127` would require unbiased 128 — not representable.
        assert!(F40::new(1, 127).is_none());
        // Likewise for an arbitrary integer whose normalized exponent
        // overflows i8.
        assert!(F40::new(2, 126).is_none());
    }
}