add rollback plumbing (#1374)

* add rollback plumbing

* more
This commit is contained in:
Logan McNaughton authored and GitHub committed 2026-11-11 11:28:06 +00:00
1 parent 4d7387537a
commit fb4b6de375
9 files changed
+340 -243

No files matched your search

+5 -8
View File
@@ -289,11 +289,11 @@ impl Device {
self.rng.clone_from(&device.rng);
self.cpu.clone_from(&device.cpu);
self.pif.clone_from(&device.pif);
self.cart.clone_from(&device.cart);
self.cart.clone_state_from(&device.cart);
self.memory.clone_from(&device.memory);
self.rsp.clone_from(&device.rsp);
self.rdp.clone_from(&device.rdp);
self.rdram.clone_from(&device.rdram);
self.rdram.clone_state_from(&device.rdram);
self.mi.clone_from(&device.mi);
self.pi.clone_from(&device.pi);
self.vi.clone_from(&device.vi);
@@ -301,13 +301,10 @@ impl Device {
self.si.clone_from(&device.si);
self.ri.clone_from(&device.ri);
self.vru.clone_from(&device.vru);
self.transferpaks.clone_from(&device.transferpaks);
self.cheats.clone_from(&device.cheats);
self.cart.rom = Vec::new();
for transferpak in self.transferpaks.iter_mut() {
transferpak.cart.rom = Vec::new();
for (dst, src) in self.transferpaks.iter_mut().zip(device.transferpaks.iter()) {
dst.clone_state_from(src);
}
self.cheats.clone_from(&device.cheats);
}
pub fn new(with_ui: bool) -> Box<Device> {
+27
View File
@@ -45,6 +45,33 @@ pub struct Cart {
pub rtc_timestamp: i64,
}
impl Cart {
/// Clone everything except the ROM, which is large and not part of the savestate.
/// `self.rom` is left untouched.
pub fn clone_state_from(&mut self, other: &Cart) {
// Destructure so adding a field to Cart forces this to be updated.
let Cart {
rom: _,
is_viewer_buffer,
pal,
latch,
cic_seed,
rtc,
sc64,
flashram,
rtc_timestamp,
} = other;
self.is_viewer_buffer.clone_from(is_viewer_buffer);
self.pal = *pal;
self.latch = *latch;
self.cic_seed = *cic_seed;
self.rtc.clone_from(rtc);
self.sc64.clone_from(sc64);
self.flashram.clone_from(flashram);
self.rtc_timestamp = *rtc_timestamp;
}
}
pub fn process(device: &mut device::Device, channel: usize) {
let cmd = device.pif.ram[device.pif.channels[channel].tx_buf.unwrap()];
+32
View File
@@ -27,6 +27,38 @@ pub struct GbCart {
pub rtc_base_time: i64,
}
impl GbCart {
/// Clone everything except the ROM; `self.rom` is left untouched.
pub fn clone_state_from(&mut self, other: &GbCart) {
// Destructure so adding a field to GbCart forces this to be updated.
let GbCart {
enabled,
rom: _,
ram,
cart_type,
ram_enabled,
mbc1_mode,
ram_bank,
rom_bank,
rtc_regs,
rtc_regs_latch,
last_elapsed_time,
rtc_base_time,
} = other;
self.enabled = *enabled;
self.ram.clone_from(ram);
self.cart_type = *cart_type;
self.ram_enabled = *ram_enabled;
self.mbc1_mode = *mbc1_mode;
self.ram_bank = *ram_bank;
self.rom_bank = *rom_bank;
self.rtc_regs = *rtc_regs;
self.rtc_regs_latch = *rtc_regs_latch;
self.last_elapsed_time = *last_elapsed_time;
self.rtc_base_time = *rtc_base_time;
}
}
#[derive(Default, Copy, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub enum CartType {
#[default]
+16
View File
@@ -8,6 +8,22 @@ pub struct TransferPak {
pub cart: device::controller::gbcart::GbCart,
}
impl TransferPak {
/// Clone everything except the GB cart ROM; `self.cart.rom` is left untouched.
pub fn clone_state_from(&mut self, other: &TransferPak) {
let TransferPak {
enabled,
reset_state,
bank,
cart,
} = other;
self.enabled = *enabled;
self.reset_state = *reset_state;
self.bank = *bank;
self.cart.clone_state_from(cart);
}
}
pub fn read(device: &mut device::Device, channel: usize, address: u16, data: usize, size: usize) {
let pak = &mut device.transferpaks[channel];
+12
View File
@@ -24,6 +24,18 @@ pub struct Rdram {
pub regs: [[u32; RDRAM_REGS_COUNT]; 4],
}
impl Rdram {
/// Clone `other` into `self`, reusing `self.mem`'s allocation so it is not reallocated
/// (and keeps its address, which the RDP holds) when the sizes match.
pub fn clone_state_from(&mut self, other: &Rdram) {
// Destructure so adding a field to Rdram forces this to be updated.
let Rdram { mem, size, regs } = other;
self.mem.clone_from(mem);
self.size = *size;
self.regs = *regs;
}
}
pub fn read_mem_fast(
device: &device::Device,
address: u64,
+1
View File
@@ -255,6 +255,7 @@ pub fn run(args: Args, arg_count: usize) -> std::io::Result<()> {
number_of_players,
input_delay,
ice_config_path: dirs.cache_dir.join("ice_config.json"),
rollback: false,
})
} else {
None
+24 -13
View File
@@ -1,7 +1,10 @@
use crate::device;
use crate::savestates;
use crate::ui;
use sha2::digest::Digest;
const MAX_PREDICTION_WINDOW: usize = 16;
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct RtcIceServerConfig {
pub urls: Vec<String>,
@@ -45,6 +48,7 @@ pub struct NetplayConfig {
pub number_of_players: usize,
pub input_delay: usize,
pub ice_config_path: std::path::PathBuf,
pub rollback: bool,
}
pub struct Netplay {
@@ -61,6 +65,7 @@ pub struct Netplay {
pub requests: std::collections::VecDeque<ggrs::GgrsRequest<GgrsConfig>>,
pub incoming_message: Vec<u8>,
pub ice_config_path: std::path::PathBuf,
pub rollback: bool,
}
#[derive(serde::Serialize, serde::Deserialize)]
@@ -201,7 +206,7 @@ pub fn receive_save(netplay: &mut Netplay, save_type: &str, save_data: &mut Vec<
}
pub fn send_input_delay(netplay: &mut Netplay, input_delay: usize) {
if input_delay < 1 {
if !(1..=MAX_PREDICTION_WINDOW).contains(&input_delay) {
return;
}
@@ -269,7 +274,9 @@ pub fn process_requests(
if let Some(request) = device.netplay.as_mut().unwrap().requests.pop_front() {
match request {
ggrs::GgrsRequest::SaveGameState { cell, frame } => {
//savestates::create_savestate(device, true, Some(frame));
if device.netplay.as_ref().unwrap().rollback {
savestates::create_savestate(device, true, Some(frame));
}
let mut hasher = sha2::Sha256::new();
for reg in device.cpu.cop0.regs.as_ref() {
@@ -279,9 +286,8 @@ pub fn process_requests(
cell.save(frame, Some(frame), Some(hash));
}
ggrs::GgrsRequest::LoadGameState { cell, frame: _ } => {
eprintln!("attempting to load game state");
if let Some(_frame) = cell.load() {
// savestates::load_savestate(device, true, Some(frame));
if let Some(frame) = cell.load() {
savestates::load_savestate(device, true, Some(frame));
}
}
ggrs::GgrsRequest::AdvanceFrame { inputs } => {
@@ -339,7 +345,9 @@ fn process_netplay(device: &mut device::Device) {
fn advance_frame(device: &mut device::Device) {
let netplay = device.netplay.as_mut().unwrap();
let local_input = if netplay.session.current_frame() > netplay.session.max_prediction() as i32 {
let local_input = if netplay.session.current_frame() > netplay.session.max_prediction() as i32
|| netplay.rollback
{
ui::input::get(&mut device.ui, 0)
} else {
//workaround for disabled rollback
@@ -351,12 +359,14 @@ fn advance_frame(device: &mut device::Device) {
.add_local_input(local_handle, local_input)
.unwrap();
// avoid rollback
while !netplay.disconnected
&& netplay.session.current_frame() > netplay.session.confirmed_frame()
&& netplay.session.confirmed_frame() != ggrs::NULL_FRAME
{
poll_clients(netplay);
if !netplay.rollback {
// avoid rollback
while !netplay.disconnected
&& netplay.session.current_frame() > netplay.session.confirmed_frame()
&& netplay.session.confirmed_frame() != ggrs::NULL_FRAME
{
poll_clients(netplay);
}
}
if netplay.disconnected {
@@ -504,7 +514,7 @@ pub fn init(
.with_fps(if pal { 50 } else { 60 })
.unwrap()
.with_desync_detection_mode(ggrs::DesyncDetection::On { interval: 60 })
.with_max_prediction_window(16)
.with_max_prediction_window(MAX_PREDICTION_WINDOW)
.with_disconnect_timeout(std::time::Duration::from_secs(if cfg!(debug_assertions) {
10
} else {
@@ -569,6 +579,7 @@ pub fn init(
received_data: std::collections::VecDeque::new(),
messages: std::collections::HashMap::new(),
ice_config_path: netplay_config.ice_config_path.clone(),
rollback: netplay_config.rollback,
})
}
+176 -221
View File
@@ -5,7 +5,6 @@ use serde::de::{Deserialize, Deserializer, SeqAccess, Visitor};
use serde::ser::{Serialize, SerializeSeq, Serializer};
#[cfg(target_arch = "x86_64")]
use std::arch::x86_64::*;
use std::ops::Deref;
struct M128iArrayVisitor<const N: usize>;
@@ -99,10 +98,11 @@ pub struct Savestate {
pub load_state: bool,
pub load_rewind: bool,
pub last_rewind_saved: f64,
pub rewind_pool:
std::sync::Arc<tokio::sync::Mutex<std::collections::BTreeMap<i32, SavestateData>>>,
pub rewind_pool: std::collections::BTreeMap<i32, SavestateData>,
}
const REWIND_POOL_SIZE: usize = 30;
pub struct SavestateData {
device: Box<device::Device>,
saves: ui::storage::Saves,
@@ -110,11 +110,16 @@ pub struct SavestateData {
ra_state: Vec<u8>,
}
static DEVICE_CLONE: std::sync::LazyLock<tokio::sync::Mutex<Box<device::Device>>> =
std::sync::LazyLock::new(|| tokio::sync::Mutex::new(device::Device::new(false)));
static SAVES_CLONE: std::sync::LazyLock<tokio::sync::Mutex<ui::storage::Saves>> =
std::sync::LazyLock::new(|| tokio::sync::Mutex::new(ui::storage::Saves::default()));
impl SavestateData {
fn new() -> SavestateData {
SavestateData {
device: device::Device::new(false),
saves: ui::storage::Saves::default(),
rdp_state: Vec::new(),
ra_state: Vec::new(),
}
}
}
pub fn process_savestates(device: &mut device::Device) {
let save_rewind = if device.netplay.is_none()
@@ -138,82 +143,76 @@ pub fn process_savestates(device: &mut device::Device) {
}
pub fn create_savestate(device: &mut device::Device, rewind: bool, rewind_frame: Option<i32>) {
if !rewind {
ui::video::idle();
ui::video::idle();
// Rewind snapshots reuse the buffers of the pool entry they replace or evict.
let rewind_key = rewind.then(|| {
let pool = &device.ui.savestate.rewind_pool;
rewind_frame.unwrap_or_else(|| pool.keys().last().map_or(0, |key| key + 1))
});
let mut state = if let Some(key) = rewind_key {
let pool = &mut device.ui.savestate.rewind_pool;
pool.remove(&key)
.or_else(|| {
if pool.len() >= REWIND_POOL_SIZE {
pool.pop_first().map(|(_, state)| state)
} else {
None
}
})
.unwrap_or_else(SavestateData::new)
} else {
SavestateData::new()
};
state.rdp_state.resize(ui::video::state_size(), 0);
ui::video::save_state(state.rdp_state.as_mut_ptr());
state.ra_state.resize(retroachievements::state_size(), 0);
retroachievements::save_state(state.ra_state.as_mut_ptr(), state.ra_state.len());
state.device.clone_state(device);
state.saves.copy_from(&device.ui.storage.saves);
if let Some(key) = rewind_key {
device.ui.savestate.rewind_pool.insert(key, state);
return;
}
let mut rdp_state: Vec<u8> = vec![0; ui::video::state_size()];
ui::video::save_state(rdp_state.as_mut_ptr());
let mut ra_state: Vec<u8> = vec![0; retroachievements::state_size()];
retroachievements::save_state(ra_state.as_mut_ptr(), ra_state.len());
DEVICE_CLONE.blocking_lock().clone_state(device);
SAVES_CLONE
.blocking_lock()
.clone_from(&device.ui.storage.saves);
let save_path = device.ui.storage.paths.savestate_file_path.clone();
let save_state_slot = device.ui.storage.save_state_slot;
let rewind_pool = device.ui.savestate.rewind_pool.clone();
tokio::spawn(async move {
let mut error = false;
if rewind {
let mut pool = rewind_pool.lock().await;
let mut state = device::Device::new(false);
state.clone_state(DEVICE_CLONE.lock().await.deref());
let key = if let Some(key) = rewind_frame {
key
} else if let Some(key) = pool.keys().last() {
key + 1
let compressed_file = if let Ok(device_data) = postcard::to_stdvec(&state.device)
&& let Ok(saves_data) = postcard::to_stdvec(&state.saves)
&& let Ok(compressed_file) = ui::storage::compress_file(&[
(&device_data, "device"),
(&saves_data, "saves"),
(&state.rdp_state, "rdp_state"),
(&state.ra_state, "ra_state"),
]) {
Some(compressed_file)
} else {
None
};
let error = if let Some(compressed_file) = compressed_file {
if let Err(e) = tokio::fs::write(save_path, compressed_file).await {
eprintln!("Error writing savestate: {}", e);
true
} else {
0
};
pool.insert(
key,
SavestateData {
device: state,
saves: SAVES_CLONE.lock().await.clone(),
rdp_state,
ra_state,
},
);
if pool.len() > 30 {
pool.pop_first();
false
}
} else {
let compressed_file = if let Ok(device_data) =
postcard::to_stdvec(DEVICE_CLONE.lock().await.deref())
&& let Ok(saves_data) = postcard::to_stdvec(SAVES_CLONE.lock().await.deref())
&& let Ok(compressed_file) = ui::storage::compress_file(&[
(&device_data, "device"),
(&saves_data, "saves"),
(&rdp_state, "rdp_state"),
(&ra_state, "ra_state"),
]) {
Some(compressed_file)
} else {
None
};
if let Some(compressed_file) = compressed_file {
if let Err(e) = tokio::fs::write(save_path, compressed_file).await {
eprintln!("Error writing savestate: {}", e);
error = true;
}
} else {
error = true;
eprintln!("Error compressing savestate");
}
}
eprintln!("Error compressing savestate");
true
};
if error {
ui::video::onscreen_message(
&format!("Failed to create savestate in slot {}", save_state_slot),
ui::video::MESSAGE_LENGTH_MESSAGE_SHORT,
);
} else if !rewind {
} else {
ui::video::onscreen_message(
&format!("Savestate created in slot {}", save_state_slot),
ui::video::MESSAGE_LENGTH_MESSAGE_VERY_SHORT,
@@ -231,163 +230,45 @@ pub fn load_savestate(device: &mut device::Device, rewind: bool, rewind_frame: O
return;
}
let state_data = if rewind {
if let Some(rewind_frame) = rewind_frame {
let timeout = std::time::Duration::from_secs(1);
let now = std::time::Instant::now();
loop {
if let Some(state) = device
.ui
.savestate
.rewind_pool
.blocking_lock()
.remove(&rewind_frame)
{
break Some(state);
}
if now.elapsed() > timeout {
break None;
}
std::thread::sleep(std::time::Duration::from_millis(1));
}
} else if let Some((_key, state)) =
device.ui.savestate.rewind_pool.blocking_lock().pop_last()
let loaded = if rewind && let Some(rewind_frame) = rewind_frame {
// Keep the entry: GGRS does not re-save the frame it rolls back to, and may
// roll back to the same frame again (e.g. late inputs from another player).
if let Some(state) = device.ui.savestate.rewind_pool.remove(&rewind_frame) {
let loaded = apply_state(device, &state);
device.ui.savestate.rewind_pool.insert(rewind_frame, state);
loaded
} else {
false
}
} else {
let state_data = if rewind {
device
.ui
.savestate
.rewind_pool
.pop_last()
.map(|(_key, state)| state)
} else if let Ok(savestate) = std::fs::read(&device.ui.storage.paths.savestate_file_path)
&& let Ok(device_data) = ui::storage::decompress_file(&savestate, "device")
&& let Ok(saves_data) = ui::storage::decompress_file(&savestate, "saves")
&& let Ok(rdp_state) = ui::storage::decompress_file(&savestate, "rdp_state")
&& let Ok(ra_state) = ui::storage::decompress_file(&savestate, "ra_state")
&& let Ok(state) = postcard::from_bytes::<Box<device::Device>>(&device_data)
&& let Ok(saves) = postcard::from_bytes(&saves_data)
{
Some(state)
Some(SavestateData {
device: state,
saves,
rdp_state,
ra_state,
})
} else {
None
}
} else if let Ok(savestate) = std::fs::read(&device.ui.storage.paths.savestate_file_path)
&& let Ok(device_data) = ui::storage::decompress_file(&savestate, "device")
&& let Ok(saves_data) = ui::storage::decompress_file(&savestate, "saves")
&& let Ok(rdp_state) = ui::storage::decompress_file(&savestate, "rdp_state")
&& let Ok(ra_state) = ui::storage::decompress_file(&savestate, "ra_state")
&& let Ok(state) = postcard::from_bytes::<Box<device::Device>>(&device_data)
&& let Ok(saves) = postcard::from_bytes(&saves_data)
{
Some(SavestateData {
device: state,
saves,
rdp_state,
ra_state,
})
} else {
None
};
state_data.is_some_and(|state| apply_state(device, &state))
};
if let Some(mut state) = state_data
&& device.rdram.size == state.device.rdram.size
{
if device.netplay.is_none() {
ui::video::idle();
}
device.ui.savestate.last_rewind_saved = state.device.vi.elapsed_time;
std::mem::swap(&mut device.rng, &mut state.device.rng);
std::mem::swap(&mut device.ui.storage.saves, &mut state.saves);
std::mem::swap(&mut device.cpu, &mut state.device.cpu);
std::mem::swap(&mut device.pif, &mut state.device.pif);
std::mem::swap(&mut device.cart, &mut state.device.cart);
std::mem::swap(&mut device.cart.rom, &mut state.device.cart.rom); // ROM is not included in the savestate
std::mem::swap(&mut device.memory, &mut state.device.memory);
std::mem::swap(&mut device.rsp, &mut state.device.rsp);
std::mem::swap(&mut device.rdp, &mut state.device.rdp);
device.rdram.mem.copy_from_slice(&state.device.rdram.mem); // RDRAM address should not change
std::mem::swap(&mut device.rdram.regs, &mut state.device.rdram.regs);
std::mem::swap(&mut device.mi, &mut state.device.mi);
std::mem::swap(&mut device.pi, &mut state.device.pi);
std::mem::swap(&mut device.vi, &mut state.device.vi);
std::mem::swap(&mut device.ai, &mut state.device.ai);
std::mem::swap(&mut device.si, &mut state.device.si);
std::mem::swap(&mut device.ri, &mut state.device.ri);
std::mem::swap(&mut device.vru, &mut state.device.vru);
std::mem::swap(&mut device.cheats, &mut state.device.cheats);
std::mem::swap(&mut device.transferpaks, &mut state.device.transferpaks);
for (i, item) in device.transferpaks.iter_mut().enumerate() {
std::mem::swap(
&mut item.cart.rom,
&mut state.device.transferpaks[i].cart.rom,
); // ROM is not included in the savestate
}
device::memory::init(device);
if device.mi.regs[device::mi::MI_INIT_MODE_REG] & device::mi::MI_INIT_MODE != 0 {
device::rdram::set_write_handler(device, device::rdram::write_mem_repeat);
}
device::vi::set_expected_refresh_rate(device);
device::cpu::map_instructions(device);
device::cop0::map_instructions(device);
device::cop1::map_instructions(device);
device::cop1::set_host_flush_mode(device);
device::cop2::map_instructions(device);
device::rsp_cpu::map_instructions(device);
let mut mem_addr = 0x1000;
while mem_addr < 0x2000 {
let data =
u32::from_be_bytes(device.rsp.mem[mem_addr..mem_addr + 4].try_into().unwrap());
device.rsp.cpu.instructions[(mem_addr & 0xFFF) / 4].func =
device::rsp_cpu::decode_opcode(device, data);
device.rsp.cpu.instructions[(mem_addr & 0xFFF) / 4].opcode = data;
mem_addr += 4;
}
for line_index in 0..512 {
for i in 0..8 {
device.memory.icache[line_index].instruction[i] =
device::cpu::decode_opcode(device, device.memory.icache[line_index].words[i]);
}
}
device::pif::connect_pif_channels(device);
for item in device.pif.channels.iter_mut().take(4) {
if let Some(handler) = item.pak_handler {
if handler.pak_type == device::controller::PakType::RumblePak {
let rumblepak_handler = device::controller::PakHandler {
read: device::controller::rumble::read,
write: device::controller::rumble::write,
pak_type: device::controller::PakType::RumblePak,
};
item.pak_handler = Some(rumblepak_handler);
} else if handler.pak_type == device::controller::PakType::MemPak {
let mempak_handler = device::controller::PakHandler {
read: device::controller::mempak::read,
write: device::controller::mempak::write,
pak_type: device::controller::PakType::MemPak,
};
item.pak_handler = Some(mempak_handler);
} else if handler.pak_type == device::controller::PakType::TransferPak {
let tpak_handler = device::controller::PakHandler {
read: device::controller::transferpak::read,
write: device::controller::transferpak::write,
pak_type: device::controller::PakType::TransferPak,
};
item.pak_handler = Some(tpak_handler);
}
}
}
ui::audio::update_freq(device);
ui::video::load_state(device, state.rdp_state.as_ptr(), state.rdp_state.len());
if !state.ra_state.is_empty() {
retroachievements::load_state(state.ra_state.as_ptr(), state.ra_state.len());
} else {
retroachievements::load_state(std::ptr::null(), 0);
}
if device.cheats.enabled {
cheats::execute_cheats(device, device.cheats.cheats.clone());
}
if loaded {
if !rewind {
ui::video::onscreen_message(
&format!(
@@ -421,6 +302,80 @@ pub fn load_savestate(device: &mut device::Device, rewind: bool, rewind_frame: O
}
}
/// Restore emulator state from `state`. The ROMs are not part of the savestate and are kept.
fn apply_state(device: &mut device::Device, state: &SavestateData) -> bool {
if device.rdram.size != state.device.rdram.size {
return false;
}
ui::video::idle();
device.ui.savestate.last_rewind_saved = state.device.vi.elapsed_time;
// clone_from keeps the RDRAM allocation (same size), so its address does not change.
device.clone_state(&state.device);
device.ui.storage.saves.copy_from(&state.saves);
device::memory::init(device);
if device.mi.regs[device::mi::MI_INIT_MODE_REG] & device::mi::MI_INIT_MODE != 0 {
device::rdram::set_write_handler(device, device::rdram::write_mem_repeat);
}
device::vi::set_expected_refresh_rate(device);
device::cpu::map_instructions(device);
device::cop0::map_instructions(device);
device::cop1::map_instructions(device);
device::cop1::set_host_flush_mode(device);
device::cop2::map_instructions(device);
device::rsp_cpu::map_instructions(device);
let mut mem_addr = 0x1000;
while mem_addr < 0x2000 {
let data = u32::from_be_bytes(device.rsp.mem[mem_addr..mem_addr + 4].try_into().unwrap());
device.rsp.cpu.instructions[(mem_addr & 0xFFF) / 4].func =
device::rsp_cpu::decode_opcode(device, data);
device.rsp.cpu.instructions[(mem_addr & 0xFFF) / 4].opcode = data;
mem_addr += 4;
}
for line_index in 0..512 {
for i in 0..8 {
device.memory.icache[line_index].instruction[i] =
device::cpu::decode_opcode(device, device.memory.icache[line_index].words[i]);
}
}
device::pif::connect_pif_channels(device);
for item in device.pif.channels.iter_mut().take(4) {
if let Some(handler) = item.pak_handler.as_mut() {
use device::controller::{PakType, mempak, rumble, transferpak};
type Handler = fn(&mut device::Device, usize, u16, usize, usize);
let (read, write): (Handler, Handler) = match handler.pak_type {
PakType::RumblePak => (rumble::read, rumble::write),
PakType::MemPak => (mempak::read, mempak::write),
PakType::TransferPak => (transferpak::read, transferpak::write),
PakType::None => continue,
};
handler.read = read;
handler.write = write;
}
}
ui::audio::update_freq(device);
ui::video::load_state(device, state.rdp_state.as_ptr(), state.rdp_state.len());
if !state.ra_state.is_empty() {
retroachievements::load_state(state.ra_state.as_ptr(), state.ra_state.len());
} else {
retroachievements::load_state(std::ptr::null(), 0);
}
if device.cheats.enabled {
cheats::execute_cheats(device, device.cheats.cheats.clone());
}
true
}
pub fn default_pak_handler() -> fn(&mut device::Device, usize, u16, usize, usize) {
device::controller::mempak::read
}
+47 -1
View File
@@ -41,7 +41,7 @@ pub struct RomSave {
pub write_pending: bool,
}
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
#[derive(Default, serde::Serialize, serde::Deserialize)]
pub struct Saves {
pub eeprom: Save,
pub sram: Save,
@@ -52,6 +52,52 @@ pub struct Saves {
pub write_to_disk: bool,
}
impl Save {
fn copy_from(&mut self, other: &Save) {
let Save {
data,
write_pending,
} = other;
self.data.clone_from(data);
self.write_pending = *write_pending;
}
}
impl RomSave {
fn copy_from(&mut self, other: &RomSave) {
let RomSave {
data,
write_pending,
} = other;
self.data.clone_from(data);
self.write_pending = *write_pending;
}
}
impl Saves {
/// Copy `other` into `self`, reusing `self`'s buffers. A derived `clone_from` would
/// allocate new ones on every savestate snapshot/load.
pub fn copy_from(&mut self, other: &Saves) {
// Destructure so adding a field forces this to be updated.
let Saves {
eeprom,
sram,
flash,
mempak,
sdcard,
romsave,
write_to_disk,
} = other;
self.eeprom.copy_from(eeprom);
self.sram.copy_from(sram);
self.flash.copy_from(flash);
self.mempak.copy_from(mempak);
self.sdcard.copy_from(sdcard);
self.romsave.copy_from(romsave);
self.write_to_disk = *write_to_disk;
}
}
fn get_save_type(rom: &[u8], game_id: &str) -> Vec<SaveTypes> {
if let Ok(header_type) = std::str::from_utf8(rom[0x3C..0x3E].try_into().unwrap())
&& header_type == "ED"