mirror of
https://github.com/italicsjenga/agb.git
synced 2025-01-07 15:41:34 +11:00
229 lines
6.5 KiB
Rust
229 lines
6.5 KiB
Rust
mod log;
|
|
mod vfile;
|
|
|
|
use std::{
|
|
cell::UnsafeCell,
|
|
ptr::NonNull,
|
|
sync::atomic::{AtomicBool, Ordering},
|
|
};
|
|
|
|
pub use log::{LogLevel, Logger};
|
|
pub use vfile::{file::FileBacked, memory::MemoryBacked, shared::Shared, MapFlag, VFile};
|
|
|
|
use vfile::VFileAlloc;
|
|
|
|
pub struct MCore {
|
|
core: NonNull<mgba_sys::mCore>,
|
|
video_buffer: UnsafeCell<Box<[u32]>>,
|
|
}
|
|
|
|
impl Drop for MCore {
|
|
fn drop(&mut self) {
|
|
unsafe { self.core.as_ref().deinit.unwrap()(self.core.as_ptr()) }
|
|
}
|
|
}
|
|
|
|
const SAMPLE_RATE: f64 = 44100.0;
|
|
|
|
macro_rules! call_on_core {
|
|
($core:expr => $fn_name:ident($($arg:expr),* $(,)?)) => {
|
|
$core.as_ref().$fn_name.unwrap()($core.as_ptr(), $($arg),*)
|
|
};
|
|
}
|
|
|
|
static GLOBAL_LOGGER_HAS_BEEN_INITIALISED: AtomicBool = AtomicBool::new(false);
|
|
|
|
pub fn set_global_default_logger(logger: &'static Logger) {
|
|
GLOBAL_LOGGER_HAS_BEEN_INITIALISED.store(true, Ordering::SeqCst);
|
|
unsafe { mgba_sys::mLogSetDefaultLogger(logger.to_mgba()) }
|
|
}
|
|
|
|
impl MCore {
|
|
pub fn new() -> Option<Self> {
|
|
if !GLOBAL_LOGGER_HAS_BEEN_INITIALISED.load(Ordering::SeqCst) {
|
|
set_global_default_logger(&log::NO_LOGGER);
|
|
}
|
|
|
|
let core = unsafe { mgba_sys::GBACoreCreate() };
|
|
let core = NonNull::new(core)?;
|
|
|
|
unsafe { mgba_sys::mCoreInitConfig(core.as_ptr(), std::ptr::null()) };
|
|
|
|
unsafe { call_on_core!(core=>init()) };
|
|
|
|
let (mut width, mut height) = (0, 0);
|
|
|
|
unsafe { call_on_core!(core=>desiredVideoDimensions(&mut width, &mut height)) };
|
|
|
|
let mut video_buffer = UnsafeCell::new(
|
|
vec![
|
|
0;
|
|
(width * height * mgba_sys::BYTES_PER_PIXEL) as usize / std::mem::size_of::<u32>()
|
|
]
|
|
.into_boxed_slice(),
|
|
);
|
|
|
|
unsafe {
|
|
call_on_core!(
|
|
core=>setVideoBuffer(
|
|
video_buffer.get_mut().as_mut_ptr(),
|
|
width as usize
|
|
)
|
|
)
|
|
}
|
|
|
|
unsafe { call_on_core!(core=>reset()) };
|
|
|
|
unsafe { call_on_core!(core=>setAudioBufferSize(0x4000)) };
|
|
|
|
unsafe {
|
|
mgba_sys::blip_set_rates(
|
|
call_on_core!(core=>getAudioChannel(0)),
|
|
call_on_core!(core=>frequency()) as f64,
|
|
SAMPLE_RATE,
|
|
)
|
|
}
|
|
unsafe {
|
|
mgba_sys::blip_set_rates(
|
|
call_on_core!(core=>getAudioChannel(1)),
|
|
call_on_core!(core=>frequency()) as f64,
|
|
SAMPLE_RATE,
|
|
)
|
|
}
|
|
|
|
let core_options = mgba_sys::mCoreOptions {
|
|
volume: 0x100,
|
|
useBios: true,
|
|
..Default::default()
|
|
};
|
|
|
|
unsafe { mgba_sys::mCoreConfigLoadDefaults(&mut (*core.as_ptr()).config, &core_options) };
|
|
unsafe { mgba_sys::mCoreLoadConfig(core.as_ptr()) };
|
|
|
|
Some(MCore { core, video_buffer })
|
|
}
|
|
|
|
pub fn load_rom<V: VFile>(&mut self, vfile: V) {
|
|
let vfile = VFileAlloc::new(vfile);
|
|
unsafe { call_on_core!(self.core=>loadROM(vfile.into_mgba())) };
|
|
}
|
|
|
|
pub fn frame(&mut self) {
|
|
unsafe { call_on_core!(self.core=>runFrame()) };
|
|
}
|
|
|
|
pub fn step(&mut self) {
|
|
unsafe { call_on_core!(self.core=>step()) };
|
|
}
|
|
|
|
pub fn set_keys(&mut self, buttons: u32) {
|
|
unsafe { call_on_core!(self.core=>setKeys(buttons)) };
|
|
}
|
|
|
|
pub fn load_save<V: VFile>(&mut self, save_file: V) {
|
|
let save_file = VFileAlloc::new(save_file);
|
|
unsafe {
|
|
call_on_core!(self.core=>loadSave(save_file.into_mgba()));
|
|
}
|
|
}
|
|
|
|
pub fn video_buffer(&mut self) -> &[u32] {
|
|
// Safety: For the duration of this borrow, mgba can't be called into,
|
|
// so this reference can be taken.
|
|
unsafe { &*self.video_buffer.get() }
|
|
}
|
|
|
|
pub fn current_cycle(&mut self) -> u64 {
|
|
unsafe { mgba_sys::mTimingGlobalTime(self.core.as_ref().timing) }
|
|
}
|
|
|
|
pub fn read_audio(&mut self, target: &mut [i16]) -> usize {
|
|
let audio_channel_left = unsafe { call_on_core!(self.core=>getAudioChannel(0)) };
|
|
let audio_channel_right = unsafe { call_on_core!(self.core=>getAudioChannel(1)) };
|
|
|
|
let samples_available = unsafe { mgba_sys::blip_samples_avail(audio_channel_left) };
|
|
|
|
if samples_available > 0 {
|
|
let samples_to_read = samples_available.min(target.len() as i32 / 2);
|
|
let produced = unsafe {
|
|
mgba_sys::blip_read_samples(
|
|
audio_channel_left,
|
|
target.as_mut_ptr().cast(),
|
|
samples_to_read,
|
|
1,
|
|
)
|
|
};
|
|
unsafe {
|
|
mgba_sys::blip_read_samples(
|
|
audio_channel_right,
|
|
target.as_mut_ptr().add(1).cast(),
|
|
samples_to_read,
|
|
1,
|
|
)
|
|
};
|
|
|
|
return produced.try_into().unwrap();
|
|
}
|
|
|
|
0
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
|
|
static TEST_ROM: &[u8] = include_bytes!("../test.gba");
|
|
static SAVE_TEST_ROM: &[u8] = include_bytes!("../save.gba");
|
|
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn check_running_game_for_some_frames() {
|
|
let file = MemoryBacked::new_from_slice(TEST_ROM);
|
|
let mut core = MCore::new().unwrap();
|
|
core.load_rom(file);
|
|
|
|
for _ in 0..100 {
|
|
core.frame();
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn check_save_file_is_initialised() {
|
|
let shared_save_file = Shared::new(MemoryBacked::new(Vec::new()));
|
|
|
|
{
|
|
let save_file = shared_save_file.clone();
|
|
let rom_file = MemoryBacked::new_from_slice(SAVE_TEST_ROM);
|
|
|
|
let mut core = MCore::new().unwrap();
|
|
core.load_rom(rom_file);
|
|
core.load_save(save_file);
|
|
for _ in 0..10 {
|
|
core.frame();
|
|
}
|
|
}
|
|
|
|
let save_file = shared_save_file
|
|
.try_into_inner()
|
|
.unwrap_or_else(|_| panic!("the shared references were not released"))
|
|
.into_inner()
|
|
.into_owned();
|
|
|
|
assert_eq!(save_file.len(), 32 * 1024, "the save file should be 32 kb");
|
|
|
|
assert_eq!(
|
|
save_file[0..128],
|
|
(0..128).collect::<Vec<u8>>(),
|
|
"First 128 bytes should be ascending numbers"
|
|
);
|
|
assert_eq!(
|
|
save_file[128..],
|
|
std::iter::repeat(0xFF)
|
|
.take(save_file.len() - 128)
|
|
.collect::<Vec<u8>>(),
|
|
"Remanider of save should be 0xFF, all ones"
|
|
);
|
|
}
|
|
}
|