#![allow(unsafe_code)]
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use crate::core::sync::{Global, LockRank};
#[unsafe(no_mangle)]
pub extern "C" fn rsemu_version_len() -> usize {
version_static().len()
}
#[unsafe(no_mangle)]
pub extern "C" fn rsemu_version_ptr() -> *const u8 {
version_static().as_ptr()
}
#[unsafe(no_mangle)]
pub extern "C" fn rsemu_echo(value: u32) -> u32 {
value
}
fn version_static() -> &'static str {
use core::sync::atomic::{AtomicPtr, Ordering};
static CACHE: AtomicPtr<u8> = AtomicPtr::new(core::ptr::null_mut());
static LEN: core::sync::atomic::AtomicUsize = core::sync::atomic::AtomicUsize::new(0);
let cached = CACHE.load(Ordering::Acquire);
if !cached.is_null() {
let len = LEN.load(Ordering::Acquire);
return leaked(cached, len);
}
let info: String = crate::build_info();
let leaked_str: &'static str = String::leak(info);
LEN.store(leaked_str.len(), Ordering::Release);
CACHE.store(leaked_str.as_ptr().cast_mut(), Ordering::Release);
leaked_str
}
fn leaked(ptr: *const u8, len: usize) -> &'static str {
unsafe {
let bytes = core::slice::from_raw_parts(ptr, len);
core::str::from_utf8_unchecked(bytes)
}
}
struct State {
machine: Option<crate::machine::Machine>,
frame: crate::host::display::Surface,
scanout: Option<alloc::boxed::Box<dyn crate::host::display::Scanout>>,
console: Option<alloc::sync::Arc<crate::host::chardev::CharPort>>,
#[cfg(feature = "dev-nes-io")]
pad: Option<alloc::sync::Arc<crate::dev::nes::input::Pad>>,
input: Vec<u8>,
output: Vec<u8>,
error: String,
buttons: [u32; 2],
shown: u64,
}
impl State {
const fn new() -> State {
State {
machine: None,
frame: crate::host::display::Surface::empty(),
scanout: None,
console: None,
#[cfg(feature = "dev-nes-io")]
pad: None,
input: Vec::new(),
output: Vec::new(),
error: String::new(),
buttons: [0; 2],
shown: u64::MAX,
}
}
}
static STATE: Global<State> = Global::with_rank(LockRank::MACHINE, State::new());
fn with_state<R>(f: impl FnOnce(&mut State) -> R) -> R {
let mut state = STATE.lock();
f(&mut state)
}
fn fail(state: &mut State, message: impl ToString) -> u32 {
state.error = message.to_string();
0
}
#[unsafe(no_mangle)]
pub extern "C" fn rsemu_input_reserve(len: usize) -> *mut u8 {
with_state(|state| {
state.input.clear();
state.input.resize(len, 0);
state.input.as_mut_ptr()
})
}
#[unsafe(no_mangle)]
pub extern "C" fn rsemu_output_ptr() -> *const u8 {
with_state(|state| state.output.as_ptr())
}
#[unsafe(no_mangle)]
pub extern "C" fn rsemu_error() -> usize {
with_state(|state| {
state.output.clear();
state.output.extend_from_slice(state.error.as_bytes());
state.output.len()
})
}
#[unsafe(no_mangle)]
pub extern "C" fn rsemu_machine_count() -> u32 {
crate::machine::catalog::machines().len() as u32
}
#[unsafe(no_mangle)]
pub extern "C" fn rsemu_machine_name(index: u32) -> usize {
with_state(|state| {
state.output.clear();
if let Some(entry) = catalog_entry(index) {
state.output.extend_from_slice(entry.name.as_bytes());
}
state.output.len()
})
}
#[unsafe(no_mangle)]
pub extern "C" fn rsemu_machine_summary(index: u32) -> usize {
with_state(|state| {
state.output.clear();
if let Some(entry) = catalog_entry(index) {
state.output.extend_from_slice(entry.summary.as_bytes());
}
state.output.len()
})
}
#[unsafe(no_mangle)]
pub extern "C" fn rsemu_machine_media(index: u32) -> usize {
with_state(|state| {
state.output.clear();
if let Some(slot) = catalog_entry(index).and_then(|e| e.media.first()) {
state.output.extend_from_slice(slot.as_bytes());
}
state.output.len()
})
}
fn catalog_entry(index: u32) -> Option<&'static crate::machine::catalog::CatalogEntry> {
crate::machine::catalog::machines()
.get(index as usize)
.copied()
}
#[unsafe(no_mangle)]
pub extern "C" fn rsemu_boot(index: u32, image_len: usize) -> u32 {
with_state(|state| {
let Some(entry) = catalog_entry(index) else {
return fail(state, "no machine with that index in this build");
};
state.machine = None;
state.scanout = None;
state.console = None;
state.shown = u64::MAX;
#[cfg(feature = "dev-nes-ppu")]
crate::host::display::nes::capture::clear();
let registry = match crate::machine::catalog::registry() {
Ok(r) => r,
Err(e) => return fail(state, e),
};
let mut options = match crate::machine::catalog::build_options() {
Ok(o) => o,
Err(e) => return fail(state, e),
};
let image: Vec<u8> = state.input.get(..image_len).unwrap_or(&[]).to_vec();
if image_len > 0 {
let Some(slot) = entry.media.first() else {
return fail(state, "this machine takes no media");
};
options.realize.media.insert(*slot, image.as_slice());
} else if entry.media.contains(&"rom") {
#[cfg(feature = "dev-apple1")]
options
.realize
.media
.insert("rom", &crate::dev::apple1::RSMON[..]);
}
#[cfg(feature = "dev-nes-ppu")]
if let Err(e) = crate::host::display::nes::capture::install(&mut options) {
return fail(state, e);
}
let machine = match crate::machine::build(entry.name, entry.source, ®istry, &options) {
Ok(m) => m,
Err(e) => return fail(state, e),
};
#[cfg(feature = "dev-nes-ppu")]
if let Some(scanout) = crate::host::display::nes::capture::take() {
state.frame = crate::host::display::Surface::for_scanout(&scanout);
state.scanout = Some(alloc::boxed::Box::new(scanout));
}
let names = crate::host::chardev::ports::names();
if let Some(port) = names
.first()
.and_then(|n| crate::host::chardev::ports::get(n))
{
let _discarded = port.drain();
state.console = Some(port);
}
#[cfg(feature = "dev-nes-io")]
{
let pads = crate::dev::nes::input::pads::names();
state.pad = pads
.first()
.and_then(|n| crate::dev::nes::input::pads::get(n));
if let Some(pad) = state.pad.as_ref() {
pad.set(0, 0);
pad.set(1, 0);
}
}
state.buttons = [0; 2];
state.machine = Some(machine);
state.error.clear();
1
})
}
#[unsafe(no_mangle)]
pub extern "C" fn rsemu_shutdown() {
with_state(|state| {
state.machine = None;
state.scanout = None;
state.console = None;
#[cfg(feature = "dev-nes-io")]
{
state.pad = None;
}
state.shown = u64::MAX;
});
}
#[unsafe(no_mangle)]
pub extern "C" fn rsemu_is_running() -> u32 {
with_state(|state| u32::from(state.machine.is_some()))
}
#[unsafe(no_mangle)]
pub extern "C" fn rsemu_reset() -> u32 {
with_state(|state| {
let Some(machine) = state.machine.as_mut() else {
return fail(state, "no machine is loaded");
};
machine.reset(crate::core::device::ResetKind::Warm);
1
})
}
const DEFAULT_FRAME_NS: u64 = 16_666_667;
#[unsafe(no_mangle)]
pub extern "C" fn rsemu_run_frame() -> u32 {
with_state(|state| {
let period = state
.scanout
.as_ref()
.map(|s| s.frame_period_ns())
.filter(|ns| *ns > 0)
.unwrap_or(DEFAULT_FRAME_NS);
let Some(machine) = state.machine.as_mut() else {
return fail(state, "no machine is loaded");
};
if let Err(e) = machine.run_for(crate::core::clock::GlobalTime::from_nanos(period)) {
return fail(state, e);
}
let Some(scanout) = state.scanout.as_ref() else {
return 0;
};
let serial = scanout.capture(&mut state.frame);
if serial == state.shown {
return 0;
}
state.shown = serial;
1
})
}
#[unsafe(no_mangle)]
pub extern "C" fn rsemu_run_frames(frames: u32) -> u32 {
let mut drawn = 0;
for _ in 0..frames {
drawn += rsemu_run_frame();
}
drawn
}
#[unsafe(no_mangle)]
pub extern "C" fn rsemu_frame_period_ns() -> u64 {
with_state(|state| {
state
.scanout
.as_ref()
.map(|s| s.frame_period_ns())
.filter(|ns| *ns > 0)
.unwrap_or(DEFAULT_FRAME_NS)
})
}
#[unsafe(no_mangle)]
pub extern "C" fn rsemu_now_ns() -> u64 {
with_state(|state| state.machine.as_ref().map_or(0, |m| m.now().as_nanos()))
}
#[unsafe(no_mangle)]
pub extern "C" fn rsemu_state_hash() -> u64 {
with_state(|state| {
state
.machine
.as_ref()
.and_then(|m| m.state_hash().ok())
.unwrap_or(0)
})
}
#[unsafe(no_mangle)]
pub extern "C" fn rsemu_has_video() -> u32 {
with_state(|state| u32::from(state.scanout.is_some()))
}
#[unsafe(no_mangle)]
pub extern "C" fn rsemu_frame_ptr() -> *const u8 {
with_state(|state| state.frame.as_ptr())
}
#[unsafe(no_mangle)]
pub extern "C" fn rsemu_frame_len() -> usize {
with_state(|state| state.frame.len() as usize)
}
#[unsafe(no_mangle)]
pub extern "C" fn rsemu_frame_width() -> u32 {
with_state(|state| state.frame.width())
}
#[unsafe(no_mangle)]
pub extern "C" fn rsemu_frame_height() -> u32 {
with_state(|state| state.frame.height())
}
#[unsafe(no_mangle)]
pub extern "C" fn rsemu_frame_serial() -> u64 {
with_state(|state| state.frame.serial())
}
#[unsafe(no_mangle)]
pub extern "C" fn rsemu_set_buttons(port: u32, mask: u32) {
with_state(|state| {
if let Some(slot) = state.buttons.get_mut(port as usize) {
*slot = mask & 0xff;
}
#[cfg(feature = "dev-nes-io")]
if let Some(pad) = state.pad.as_ref() {
pad.set(port as usize, (mask & 0xff) as u8);
}
});
}
#[unsafe(no_mangle)]
pub extern "C" fn rsemu_buttons(port: u32) -> u32 {
with_state(|state| state.buttons.get(port as usize).copied().unwrap_or(0))
}
#[unsafe(no_mangle)]
pub extern "C" fn rsemu_console_write(len: usize) -> usize {
with_state(|state| {
let Some(port) = state.console.clone() else {
return 0;
};
let bytes = state.input.get(..len).unwrap_or(&[]);
port.feed(bytes)
})
}
#[unsafe(no_mangle)]
pub extern "C" fn rsemu_console_read() -> usize {
with_state(|state| {
state.output.clear();
if let Some(port) = state.console.clone() {
let bytes = port.drain();
state.output.extend_from_slice(&bytes);
}
state.output.len()
})
}
#[unsafe(no_mangle)]
pub extern "C" fn rsemu_has_console() -> u32 {
with_state(|state| u32::from(state.console.is_some()))
}
#[unsafe(no_mangle)]
pub extern "C" fn rsemu_save() -> usize {
with_state(|state| {
state.output.clear();
let Some(machine) = state.machine.as_ref() else {
fail(state, "no machine is loaded");
return 0;
};
match machine.save() {
Ok(bytes) => {
state.output = bytes;
state.output.len()
}
Err(e) => {
fail(state, e);
0
}
}
})
}
#[unsafe(no_mangle)]
pub extern "C" fn rsemu_load(len: usize) -> u32 {
with_state(|state| {
let bytes: Vec<u8> = state.input.get(..len).unwrap_or(&[]).to_vec();
let Some(machine) = state.machine.as_mut() else {
return fail(state, "no machine is loaded");
};
match machine.load(&bytes) {
Ok(()) => {
state.shown = u64::MAX;
1
}
Err(e) => fail(state, e),
}
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn echo_round_trips() {
assert_eq!(rsemu_echo(0xdead_beef), 0xdead_beef);
}
#[test]
fn version_pointer_and_length_describe_the_same_string() {
let len = rsemu_version_len();
assert!(len > 0);
assert_eq!(rsemu_version_ptr(), rsemu_version_ptr());
assert_eq!(rsemu_version_len(), len);
}
#[test]
fn the_catalog_is_readable_by_index() {
let _serialised = crate::host::display::PROCESS_WIDE.lock();
let count = rsemu_machine_count();
for index in 0..count {
assert!(rsemu_machine_name(index) > 0, "machine {index} has no name");
assert!(rsemu_machine_summary(index) > 0);
}
assert_eq!(rsemu_machine_name(count), 0);
}
#[test]
fn calls_without_a_machine_report_rather_than_panic() {
let _serialised = crate::host::display::PROCESS_WIDE.lock();
rsemu_shutdown();
assert_eq!(rsemu_is_running(), 0);
assert_eq!(rsemu_run_frame(), 0);
assert_eq!(rsemu_reset(), 0);
assert_eq!(rsemu_save(), 0);
assert_eq!(rsemu_state_hash(), 0);
assert!(rsemu_error() > 0, "a failure must leave a message");
}
#[test]
fn the_input_buffer_is_resizable() {
let _serialised = crate::host::display::PROCESS_WIDE.lock();
let a = rsemu_input_reserve(16);
assert!(!a.is_null());
let b = rsemu_input_reserve(1 << 16);
assert!(!b.is_null());
assert_eq!(rsemu_console_write(0), 0);
}
#[cfg(all(feature = "machine-nes", feature = "dev-nes-ppu"))]
#[test]
fn a_nes_boots_and_draws_through_the_abi() {
let _serialised = crate::host::display::PROCESS_WIDE.lock();
static MINIMAL_NROM: &[u8] = &{
let mut image = [0u8; 16 + 16384 + 8192];
image[0] = b'N';
image[1] = b'E';
image[2] = b'S';
image[3] = 0x1a;
image[4] = 1;
image[5] = 1;
image[16 + 0x3ffc] = 0x00;
image[16 + 0x3ffd] = 0xc0;
image[16] = 0x4c;
image[17] = 0x00;
image[18] = 0xc0;
image
};
let index = (0..rsemu_machine_count())
.find(|i| {
rsemu_machine_name(*i);
with_state(|state| state.output == b"nes-ntsc")
})
.expect("machine-nes is on, so the catalog has one");
rsemu_input_reserve(MINIMAL_NROM.len());
with_state(|state| state.input.copy_from_slice(MINIMAL_NROM));
if rsemu_boot(index, MINIMAL_NROM.len()) != 1 {
let len = rsemu_error();
let message = with_state(|state| String::from_utf8_lossy(&state.output).into_owned());
panic!("boot failed ({len} bytes): {message}");
}
assert_eq!(rsemu_is_running(), 1);
assert_eq!(rsemu_has_video(), 1);
assert_eq!(rsemu_frame_width(), 256);
assert_eq!(rsemu_frame_height(), 240);
assert_eq!(rsemu_frame_len(), 256 * 240 * 4);
let mut drawn = 0;
for _ in 0..4 {
drawn += rsemu_run_frame();
}
assert!(drawn > 0, "four frames produced no picture");
assert!(rsemu_now_ns() > 0);
let hash = rsemu_state_hash();
assert_ne!(hash, 0);
let len = rsemu_save();
assert!(len > 0);
let snapshot = with_state(|state| state.output.clone());
rsemu_run_frames(2);
assert_ne!(rsemu_state_hash(), hash, "time did not pass");
#[cfg(feature = "dev-nes-io")]
{
use crate::dev::nes::input::{buttons, pads};
rsemu_set_buttons(0, u32::from(buttons::A | buttons::START));
let pad = pads::names()
.first()
.and_then(|n| pads::get(n))
.expect("the machine opened a pad port");
assert_eq!(pad.get(0), buttons::A | buttons::START);
rsemu_set_buttons(0, 0);
assert_eq!(pad.get(0), buttons::NONE);
}
rsemu_input_reserve(snapshot.len());
with_state(|state| state.input.copy_from_slice(&snapshot));
assert_eq!(rsemu_load(snapshot.len()), 1, "load failed");
assert_eq!(rsemu_state_hash(), hash, "the snapshot did not restore");
rsemu_shutdown();
}
}