Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
bbf19e3
adding dom info struct
scifi6546 Feb 27, 2020
3da43e3
can pass struct
scifi6546 Feb 27, 2020
4e2700a
removed some packages that are not needed
scifi6546 Feb 27, 2020
dddbb05
ran cargo fmt
scifi6546 Feb 28, 2020
e90aaab
improved style and cleaned up cargo.toml
scifi6546 Feb 28, 2020
f592784
ran cargo fmt
scifi6546 Feb 28, 2020
398f750
added debug and cleaned up cargo.toml files
scifi6546 Feb 28, 2020
fb173da
interm commit
scifi6546 Mar 17, 2020
5617ba4
Refactor
scifi6546 Mar 17, 2020
eb843cb
Refactor
scifi6546 Mar 17, 2020
48bc820
merged refactor
scifi6546 Mar 17, 2020
ebb8e51
Merge branch 'benkonz-master'
scifi6546 Mar 17, 2020
e5082f6
removed librs
scifi6546 Mar 17, 2020
851c00a
added mapp_err
scifi6546 Mar 18, 2020
8828c51
removed extern crate gameboy_opengl_web;
scifi6546 Mar 18, 2020
91c64b2
got controller plugged into game
scifi6546 Mar 20, 2020
f0f3856
removed commented out code and removed unneeded import
scifi6546 Mar 20, 2020
964d231
removed unneded import
scifi6546 Mar 20, 2020
ce9bf56
removed get_cartridge_mut
scifi6546 Mar 21, 2020
9edd258
got fix working for native gameboy
scifi6546 Mar 21, 2020
9286b30
removed get_cartridge
scifi6546 Mar 21, 2020
a219f83
refactored load_ram
scifi6546 Mar 25, 2020
790ab60
temp commit, working on generic save_type
scifi6546 Mar 25, 2020
de9a868
got dyn working
scifi6546 Mar 26, 2020
9424942
Merge branch 'master' of https://github.com/benkonz/gameboy_emulator …
scifi6546 Mar 26, 2020
c4b20dd
Merge branch 'benkonz-master'
scifi6546 Mar 26, 2020
5565efa
fixed merge errors+compiller warnings
scifi6546 Mar 26, 2020
e56c581
working on dyn
scifi6546 Mar 26, 2020
0d00da0
Merge branch 'save_impl'
scifi6546 Mar 26, 2020
3859c0c
Merge branch 'master' into master
benkonz May 9, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 21 additions & 4 deletions gameboy_core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,28 @@ impl Gameboy {
pub fn get_audio_buffer(&self) -> &[f32] {
self.emulator.get_audio_buffer()
}
pub fn get_cartridge(&self) -> &Cartridge {
self.emulator.get_cartridge()
pub fn set_cartridge_ram(&mut self, ram: Vec<u8>) {
self.emulator.get_cartridge_mut().set_ram(ram)
}
pub fn get_cartridge_mut(&mut self) -> &mut Cartridge {
self.emulator.get_cartridge_mut()
pub fn has_battery(&self) -> bool {
self.emulator.get_cartridge().has_battery()
}
pub fn has_rtc(&self) -> bool {
self.emulator.get_cartridge().has_rtc()
}
pub fn get_cartridge_ram(&self) -> &[u8] {
self.emulator.get_cartridge().get_ram()
}
pub fn get_cartridge_name(&self) -> &str {
self.emulator.get_cartridge().get_name()
}
pub fn get_last_timestamp(&self) -> (rtc::Rtc, u64) {
self.emulator.get_cartridge().get_last_timestamp()
}
pub fn set_last_timestamp(&mut self, rtc: Rtc, timestamp: u64) {
self.emulator
.get_cartridge_mut()
.set_last_timestamp(rtc, timestamp)
}
pub fn set_ram_change_callback(&mut self, f: Box<dyn FnMut(usize, u8)>) {
self.emulator.set_ram_change_callback(f)
Expand Down
65 changes: 32 additions & 33 deletions gameboy_opengl/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ mod screen;
use crate::native_rtc::NativeRTC;
use crate::screen::Screen;
use directories::BaseDirs;
use gameboy_core::{Button, Cartridge, Gameboy, Rtc, StepResult};
use gameboy_core::{Button, Gameboy, Rtc, StepResult};
use sdl2::audio::AudioSpecDesired;
use sdl2::event::Event;
use sdl2::keyboard::Keycode;
Expand Down Expand Up @@ -54,11 +54,11 @@ pub fn start(rom: Vec<u8>) -> Result<(), String> {
let rtc = Box::new(NativeRTC::new());
let mut emulator = Gameboy::from_rom(rom, rtc)?;

load_ram_save_data(emulator.get_cartridge_mut()).map_err(|e| format!("{:?}", e))?;
load_timestamp_data(emulator.get_cartridge_mut()).map_err(|e| format!("{:?}", e))?;
load_ram_save_data(&mut emulator).map_err(|e| format!("{:?}", e))?;
load_timestamp_data(&mut emulator).map_err(|e| format!("{:?}", e))?;

let mut ram_save_file = get_ram_save_file(emulator.get_cartridge());
let mut timestamp_save_file = get_timestamp_save_file(emulator.get_cartridge());
let mut ram_save_file = get_ram_save_file(&emulator);
let mut timestamp_save_file = get_timestamp_save_file(&emulator);

let ram_changed = Rc::new(RefCell::new(true));
{
Expand Down Expand Up @@ -95,17 +95,16 @@ pub fn start(rom: Vec<u8>) -> Result<(), String> {
}
}

if *ram_changed.borrow() && emulator.get_cartridge().has_battery() {
if *ram_changed.borrow() && emulator.has_battery() {
if let Some(ref mut ram_save_file) = ram_save_file {
save_ram_data(emulator.get_cartridge(), ram_save_file)
.map_err(|e| format!("{:?}", e))?;
save_ram_data(&emulator, ram_save_file).map_err(|e| format!("{:?}", e))?;
}
*ram_changed.borrow_mut() = false;
}

if emulator.get_cartridge().has_rtc() {
if emulator.has_rtc() {
if let Some(ref mut timestamp_save_file) = timestamp_save_file {
save_timestamp_data(emulator.get_cartridge(), timestamp_save_file)
save_timestamp_data(&emulator, timestamp_save_file)
.map_err(|e| format!("{:?}", e))?;
}
}
Expand Down Expand Up @@ -160,17 +159,19 @@ fn get_ram_saves_path() -> Option<PathBuf> {
Some(path_buf)
}

fn load_ram_save_data(cartridge: &mut Cartridge) -> std::io::Result<()> {
if cartridge.has_battery() {
fn load_ram_save_data(gameboy: &mut Gameboy) -> std::io::Result<()> {
if gameboy.has_battery() {
if let Some(ram_saves_dir) = get_ram_saves_path() {
let ram_save_file = ram_saves_dir.join(format!("{}.bin", cartridge.get_name()));
let ram_save_file = ram_saves_dir.join(format!("{}.bin", gameboy.get_cartridge_name()));
if ram_save_file.exists() {
let mut ram_save_file = OpenOptions::new().read(true).open(ram_save_file)?;
if let Ok(metadata) = ram_save_file.metadata() {
// sometimes two different roms have the same name,
// so we make sure that the ram length is the same before reading
if metadata.len() == cartridge.get_ram().len() as u64 {
ram_save_file.read_exact(cartridge.get_ram_mut())?;
if metadata.len() == gameboy.get_cartridge_ram().len() as u64 {
let mut buffer = vec![0; metadata.len() as usize];
ram_save_file.read_exact(&mut buffer)?;
gameboy.set_cartridge_ram(buffer);
}
}
}
Expand All @@ -179,11 +180,11 @@ fn load_ram_save_data(cartridge: &mut Cartridge) -> std::io::Result<()> {
Ok(())
}

fn load_timestamp_data(cartridge: &mut Cartridge) -> std::io::Result<()> {
if cartridge.has_rtc() {
fn load_timestamp_data(gameboy: &mut Gameboy) -> std::io::Result<()> {
if gameboy.has_rtc() {
if let Some(ram_saves_dir) = get_ram_saves_path() {
let timestamp_save_file =
ram_saves_dir.join(format!("{}-timestamp.bin", cartridge.get_name()));
ram_saves_dir.join(format!("{}-timestamp.bin", gameboy.get_cartridge_name()));
if timestamp_save_file.exists() {
let mut timestamp_save_file =
OpenOptions::new().read(true).open(timestamp_save_file)?;
Expand All @@ -193,20 +194,21 @@ fn load_timestamp_data(cartridge: &mut Cartridge) -> std::io::Result<()> {
timestamp_save_file.read_exact(&mut timestamp_data)?;
let last_rtc = Rtc::from_bytes(&rtc_data);
let last_timestamp = u64::from_ne_bytes(timestamp_data);
cartridge.set_last_timestamp(last_rtc, last_timestamp);
gameboy.set_last_timestamp(last_rtc, last_timestamp);
}
}
}
Ok(())
}

fn get_ram_save_file(cartridge: &Cartridge) -> Option<impl Write + Seek> {
if cartridge.has_battery() {
fn get_ram_save_file(gameboy: &Gameboy) -> Option<impl Write + Seek> {
if gameboy.has_battery() {
let ram_saves_path = get_ram_saves_path()?;
if !ram_saves_path.exists() {
fs::create_dir_all(&ram_saves_path).ok()?;
}
let ram_save_file_path = ram_saves_path.join(format!("{}.bin", cartridge.get_name()));
let ram_save_file_path =
ram_saves_path.join(format!("{}.bin", gameboy.get_cartridge_name()));
Some(
OpenOptions::new()
.write(true)
Expand All @@ -219,14 +221,14 @@ fn get_ram_save_file(cartridge: &Cartridge) -> Option<impl Write + Seek> {
}
}

fn get_timestamp_save_file(cartridge: &Cartridge) -> Option<impl Write + Seek> {
if cartridge.has_rtc() {
fn get_timestamp_save_file(gameboy: &Gameboy) -> Option<impl Write + Seek> {
if gameboy.has_rtc() {
let ram_saves_path = get_ram_saves_path()?;
if !ram_saves_path.exists() {
fs::create_dir_all(&ram_saves_path).ok()?;
}
let timestamp_save_file_path =
ram_saves_path.join(format!("{}-timestamp.bin", cartridge.get_name()));
ram_saves_path.join(format!("{}-timestamp.bin", gameboy.get_cartridge_name()));
Some(
OpenOptions::new()
.write(true)
Expand All @@ -239,23 +241,20 @@ fn get_timestamp_save_file(cartridge: &Cartridge) -> Option<impl Write + Seek> {
}
}

fn save_ram_data<T: Write + Seek>(
cartridge: &Cartridge,
ram_save_file: &mut T,
) -> std::io::Result<()> {
if cartridge.has_battery() {
let ram = cartridge.get_ram();
fn save_ram_data<T: Write + Seek>(gameboy: &Gameboy, ram_save_file: &mut T) -> std::io::Result<()> {
if gameboy.has_battery() {
let ram = gameboy.get_cartridge_ram();
ram_save_file.seek(SeekFrom::Start(0))?;
ram_save_file.write_all(ram)?;
}
Ok(())
}

fn save_timestamp_data<T: Write + Seek>(
cartridge: &Cartridge,
gameboy: &Gameboy,
timestamp_save_file: &mut T,
) -> std::io::Result<()> {
let (rtc_data, rtc_last_time) = cartridge.get_last_timestamp();
let (rtc_data, rtc_last_time) = gameboy.get_last_timestamp();
let mut rtc_data = rtc_data.to_bytes().to_vec();
let mut rtc_last_time_data = rtc_last_time.to_ne_bytes().to_vec();
rtc_data.append(&mut rtc_last_time_data);
Expand Down
87 changes: 53 additions & 34 deletions gameboy_opengl_web/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ mod web_rtc;

use crate::screen::Screen;
use crate::web_rtc::WebRTC;
use gameboy_core::{Button, Cartridge, ControllerEvent, Gameboy, Rtc, StepResult};
use gameboy_core::{Button, ControllerEvent, Gameboy, Rtc, StepResult};
use std::cell::RefCell;
use std::rc::Rc;
use std::sync::mpsc;
Expand All @@ -33,8 +33,44 @@ struct EmulatorState {
js_ctx: Value,
busy: bool,
audio_underrun: Option<usize>,
save: Box<dyn SaveFile>,
}
//Generic object for saving emulator state
trait SaveFile {
fn load(&mut self) -> Option<Vec<u8>>;
fn save(&mut self, ram: Vec<u8>);
}
struct LocalStorageSaveFile {
save_name: String,
}
impl LocalStorageSaveFile {
pub fn new(name: String) -> LocalStorageSaveFile {
return LocalStorageSaveFile { save_name: name };
}
}
impl SaveFile for LocalStorageSaveFile {
fn load(&mut self) -> Option<Vec<u8>> {
if let Some(ram_str) = window().local_storage().get(&self.save_name) {
let chars: Vec<char> = ram_str.chars().collect();
let bytes: Vec<u8> = chars
.chunks(2)
.map(|chunk| {
let byte: String = chunk.iter().collect();
u8::from_str_radix(&byte, 16).unwrap()
})
.collect();
return Some(bytes);
}
None
}
fn save(&mut self, ram: Vec<u8>) {
let save_str: String = ram.iter().map(|x| format!("{:02X}", x)).collect();
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried doing saving by mapping the ram to a string like this originally, but the problem was that it was just too slow. Some games can have up to 2MB of RAM, so mapping every single byte to a string every frame slows the game down by a lot

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How did you solve the issue?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Heh, my solution is a little hack-y, but has worked well so far. Basically, we maintain a String that is an exact copy of whatever is in the cartridge RAM and whenever it changes, update the String in-place.

This initializes the Ram String to the RAM contents before the emulation starts

  let ram_str = Rc::new(RefCell::new(
        ram.iter().map(|byte| format!("{:02x}", byte)).collect(),
    ));

This set's up an event listener for whenever the contents of the Cartridge RAM changes

 self.gameboy
            .set_ram_change_callback(Box::new(move |address, value| {
                if has_battery {
                    let byte_chars: Vec<char> = format!("{:02x}", value).chars().collect();
                    let (first, second) = (byte_chars[0] as u8, byte_chars[1] as u8);
                    // unsafe because we are updating the RAM String in-place
                    // but since we are updating it with valid UTF-8, the updated string will also be UTF-8
                    unsafe {
                        let mut ram_str_ref = ram_str.borrow_mut();
                        let bytes = ram_str_ref.as_bytes_mut();
                        bytes[address * 2] = first;
                        bytes[address * 2 + 1] = second;
                    }
                    *should_save_to_local.borrow_mut() = true;
                }
            }));

This takes the O(n) Vec to String mapping to a O(1) in-place update, which is a lot faster, but requires a bit more code and unsafe Rust.

window()
.local_storage()
.insert(&self.save_name, &save_str)
.unwrap();
}
}

impl EmulatorState {
pub fn emulate_until_vblank_or_audio(&mut self) -> StepResult {
let step_result = loop {
Expand Down Expand Up @@ -122,20 +158,15 @@ impl EmulatorState {
}

pub fn save_ram_data(&mut self) {
if *self.should_save_to_local.borrow() && self.gameboy.get_cartridge().has_battery() {
let name = self.gameboy.get_cartridge().get_name();
window()
.local_storage()
.insert(&name, &self.ram_str.borrow())
.unwrap();
*self.should_save_to_local.borrow_mut() = false;
if *self.should_save_to_local.borrow() && self.gameboy.has_battery() {
self.save.save(self.gameboy.get_cartridge_ram().to_vec());
}
}

pub fn save_timestamp_data(&mut self) {
if self.gameboy.get_cartridge().has_rtc() {
let name = format!("{}-timestamp", self.gameboy.get_cartridge().get_name());
let (rtc_data, last_timestamp) = self.gameboy.get_cartridge().get_last_timestamp();
if self.gameboy.has_rtc() {
let name = format!("{}-timestamp", self.gameboy.get_cartridge_name());
let (rtc_data, last_timestamp) = self.gameboy.get_last_timestamp();
let mut rtc_bytes = rtc_data.to_bytes().to_vec();
let mut last_timestamp_bytes = u64::to_ne_bytes(last_timestamp).to_vec();
rtc_bytes.append(&mut last_timestamp_bytes);
Expand All @@ -153,7 +184,7 @@ impl EmulatorState {
pub fn set_ram_change_listener(&mut self) {
let ram_str = self.ram_str.clone();
let should_save_to_local = self.should_save_to_local.clone();
let has_battery = self.gameboy.get_cartridge().has_battery();
let has_battery = self.gameboy.has_battery();
self.gameboy
.set_ram_change_callback(Box::new(move |address, value| {
if has_battery {
Expand Down Expand Up @@ -269,16 +300,18 @@ pub fn start(rom: Vec<u8>, dom_ids: DOMInfo) -> Result<(), String> {
};
let rtc = Box::new(WebRTC::new());
let mut gameboy = Gameboy::from_rom(rom, rtc)?;
load_ram_save_data(gameboy.get_cartridge_mut());
load_timestamp_data(gameboy.get_cartridge_mut());
let ram = gameboy.get_cartridge().get_ram().to_vec();
let mut save = LocalStorageSaveFile::new(gameboy.get_cartridge_name().to_string());
if let Some(ram) = save.load() {
gameboy.set_cartridge_ram(ram)
}
load_timestamp_data(&mut gameboy);
let ram = gameboy.get_cartridge_ram().to_vec();
let ram_str = Rc::new(RefCell::new(
ram.iter().map(|byte| format!("{:02x}", byte)).collect(),
));
let screen = Screen::new();

let mut emulator_state = EmulatorState {
//from opengl_web
gameboy,
screen,
controller_receiver: receiver,
Expand All @@ -287,6 +320,7 @@ pub fn start(rom: Vec<u8>, dom_ids: DOMInfo) -> Result<(), String> {
js_ctx,
busy: false,
audio_underrun: None,
save: Box::new(save),
};

emulator_state.set_ram_change_listener();
Expand Down Expand Up @@ -377,23 +411,8 @@ fn add_multi_controller_event_listener<T: ConcreteEvent>(
sender.send(second_controller_event).unwrap();
});
}

fn load_ram_save_data(cartridge: &mut Cartridge) {
if let Some(ram_str) = window().local_storage().get(cartridge.get_name()) {
let chars: Vec<char> = ram_str.chars().collect();
let bytes: Vec<u8> = chars
.chunks(2)
.map(|chunk| {
let byte: String = chunk.iter().collect();
u8::from_str_radix(&byte, 16).unwrap()
})
.collect();
cartridge.set_ram(bytes);
}
}

fn load_timestamp_data(cartridge: &mut Cartridge) {
let key = format!("{}-timestamp", cartridge.get_name());
fn load_timestamp_data(cartridge: &mut Gameboy) {
let key = format!("{}-timestamp", cartridge.get_cartridge_name());
if let Some(timestamp_str) = window().local_storage().get(&key) {
let chars: Vec<char> = timestamp_str.chars().collect();
let bytes: Vec<u8> = chars
Expand Down