ymfm-sys 0.2.0

Rust bindings to the ymfm FM sound chip emulation library
Documentation
use std::cell::RefCell;
use std::pin::Pin;
use std::rc::Rc;
use ymfm_sys::{InterfaceCallbacks, InterfaceHandler, ffi};

#[derive(Clone)]
struct TestState {
    irq: Rc<RefCell<bool>>,
    busy: Rc<RefCell<bool>>,
    data: Rc<RefCell<[Vec<u8>; 4]>>,
    current_clock: Rc<RefCell<i64>>,
    busy_until: Rc<RefCell<i64>>,
    timers: Rc<RefCell<[i64; 2]>>,
}

fn handler() -> (InterfaceHandler, TestState) {
    let state = TestState {
        irq: Rc::new(RefCell::new(false)),
        busy: Rc::new(RefCell::new(false)),
        data: Rc::new(RefCell::new(std::array::from_fn(|_| Vec::new()))),
        current_clock: Rc::new(RefCell::new(0)),
        busy_until: Rc::new(RefCell::new(0)),
        timers: Rc::new(RefCell::new([-1; 2])),
    };
    let update_irq_state = state.clone();
    let advance_state = state.clone();
    let busy_end_state = state.clone();
    let timer_state = state.clone();
    let external_read_state = state.clone();
    let read_data_state = state.clone();
    let external_write_state = state.clone();
    let write_data_state = state.clone();
    let handler = InterfaceHandler {
        advance_clock: Some(Box::new(move |clocks| {
            let mut current = advance_state.current_clock.borrow_mut();
            *current += clocks;
            *advance_state.busy.borrow_mut() = *current < *advance_state.busy_until.borrow();
            let mut timers = advance_state.timers.borrow_mut();
            let mut expired = 0;
            for (index, deadline) in timers.iter_mut().enumerate() {
                if *deadline >= 0 && *current >= *deadline {
                    *deadline = -1;
                    expired |= 1 << index;
                }
            }
            expired
        })),
        read_data: Some(Box::new(move |access, base, length| {
            read_bytes(&read_data_state, access, base, length)
        })),
        write_data: Some(Box::new(move |access, base, values| {
            for (index, value) in values.iter().copied().enumerate() {
                write_byte(&write_data_state, access, base + index as u32, value);
            }
        })),
        ymfm_external_read: Some(Box::new(move |access, offset| {
            read_byte(&external_read_state, access, offset)
        })),
        ymfm_external_write: Some(Box::new(move |access, offset, value| {
            write_byte(&external_write_state, access, offset, value);
        })),
        ymfm_is_busy: Some(Box::new({
            let state = state.clone();
            move || *state.busy.borrow()
        })),
        ymfm_set_busy_end: Some(Box::new(move |clocks| {
            let current = *busy_end_state.current_clock.borrow();
            *busy_end_state.busy_until.borrow_mut() = current + i64::from(clocks);
            *busy_end_state.busy.borrow_mut() = true;
        })),
        ymfm_set_timer: Some(Box::new(move |tnum, duration| {
            if let Some(deadline) = timer_state.timers.borrow_mut().get_mut(tnum as usize) {
                let current = *timer_state.current_clock.borrow();
                *deadline = if duration < 0 {
                    -1
                } else {
                    current + i64::from(duration)
                };
            }
        })),
        ymfm_update_irq: Some(Box::new(move |asserted| {
            *update_irq_state.irq.borrow_mut() = asserted;
        })),
    };
    (handler, state)
}

fn data_index(access: ffi::AccessClass) -> usize {
    match access {
        ffi::AccessClass::Io => 0,
        ffi::AccessClass::AdpcmA => 1,
        ffi::AccessClass::AdpcmB => 2,
        ffi::AccessClass::Pcm => 3,
        _ => 0,
    }
}

fn read_byte(state: &TestState, access: ffi::AccessClass, offset: u32) -> u8 {
    state.data.borrow()[data_index(access)]
        .get(offset as usize)
        .copied()
        .unwrap_or(0)
}

fn read_bytes(state: &TestState, access: ffi::AccessClass, base: u32, length: u32) -> Vec<u8> {
    (0..length)
        .map(|index| read_byte(state, access, base + index))
        .collect()
}

fn write_byte(state: &TestState, access: ffi::AccessClass, offset: u32, value: u8) {
    let buffer = &mut state.data.borrow_mut()[data_index(access)];
    let index = offset as usize;
    if buffer.len() <= index {
        buffer.resize(index + 1, 0);
    }
    buffer[index] = value;
}

fn write_reg(mut chip: Pin<&mut ffi::Chip>, reg: u8, data: u8) {
    chip.as_mut().write(0, reg);
    chip.as_mut().write(1, data);
}

fn write_port(mut chip: Pin<&mut ffi::Chip>, offset: u32, data: u8) {
    chip.as_mut().write(offset, data);
}

#[test]
fn timer_a_expiry_asserts_irq() {
    let (handler, callback_state) = handler();
    let mut chip = ffi::create_chip_with_callbacks(
        ffi::ChipType::Ym2612,
        7_670_000,
        Box::new(InterfaceCallbacks::new(handler)),
    );
    let channels = chip.channels() as usize;
    assert!(!*callback_state.irq.borrow());

    write_reg(chip.pin_mut(), 0x24, 0xff);
    write_reg(chip.pin_mut(), 0x25, 0x03);
    write_reg(chip.pin_mut(), 0x27, 0x05);

    let mut buffer = vec![0i32; channels];
    let fired = (0..1000).any(|_| {
        chip.pin_mut().generate(&mut buffer);
        *callback_state.irq.borrow()
    });
    assert!(fired, "timer A should have expired and asserted IRQ");

    let status = chip.pin_mut().read(0);
    assert!(
        status & 0x01 != 0,
        "status register should report TIMERA (bit 0)"
    );
}

#[test]
fn irq_callback_is_forwarded_to_rust() {
    let (handler, callback_state) = handler();
    let mut chip = ffi::create_chip_with_callbacks(
        ffi::ChipType::Ym2612,
        7_670_000,
        Box::new(InterfaceCallbacks::new(handler)),
    );
    let channels = chip.channels() as usize;

    write_reg(chip.pin_mut(), 0x24, 0xff);
    write_reg(chip.pin_mut(), 0x25, 0x03);
    write_reg(chip.pin_mut(), 0x27, 0x05);

    let mut buffer = vec![0i32; channels];
    let fired = (0..1000).any(|_| {
        chip.pin_mut().generate(&mut buffer);
        *callback_state.irq.borrow()
    });
    assert!(
        fired,
        "ymfm IRQ notification should reach the Rust callback"
    );
}

#[test]
fn busy_callback_is_owned_by_rust() {
    let (handler, callback_state) = handler();
    let mut chip = ffi::create_chip_with_callbacks(
        ffi::ChipType::Ym2612,
        7_670_000,
        Box::new(InterfaceCallbacks::new(handler)),
    );

    write_reg(chip.pin_mut(), 0x22, 0x00);
    assert!(*callback_state.busy.borrow());

    let mut buffer = vec![0i32; chip.channels() as usize];
    let cleared = (0..1000).any(|_| {
        chip.pin_mut().generate(&mut buffer);
        !*callback_state.busy.borrow()
    });
    assert!(
        cleared,
        "Rust-owned BUSY state should expire as samples advance"
    );
    assert!(!*callback_state.busy.borrow());
}

#[test]
fn ymf278b_extended_and_pcm_ports_follow_upstream_mapping() {
    let (handler, _callback_state) = handler();
    let mut chip = ffi::create_chip_with_callbacks(
        ffi::ChipType::Ymf278B,
        33_868_800,
        Box::new(InterfaceCallbacks::new(handler)),
    );

    // YMF278B enables its PCM port through register 0x105 (NEW2).
    write_port(chip.pin_mut(), 2, 0x05);
    write_port(chip.pin_mut(), 3, 0x02);

    // Select PCM memory access mode and address 0x0010 through offsets 4/5.
    write_port(chip.pin_mut(), 4, 0x02);
    write_port(chip.pin_mut(), 5, 0x01);
    write_port(chip.pin_mut(), 4, 0x03);
    write_port(chip.pin_mut(), 5, 0x00);
    write_port(chip.pin_mut(), 4, 0x04);
    write_port(chip.pin_mut(), 5, 0x00);
    write_port(chip.pin_mut(), 4, 0x05);
    write_port(chip.pin_mut(), 5, 0x10);

    // Offset 4 selects the PCM data register and offset 5 writes the byte.
    write_port(chip.pin_mut(), 4, 0x06);
    write_port(chip.pin_mut(), 5, 0xa5);

    // The PCM data port is readable and advances the external address.
    write_port(chip.pin_mut(), 4, 0x05);
    write_port(chip.pin_mut(), 5, 0x10);
    write_port(chip.pin_mut(), 4, 0x06);
    assert_eq!(chip.pin_mut().read(5), 0xa5);
}