Files
sm-vita/src/sm_rtl.c
2023-03-06 04:08:45 +01:00

719 lines
20 KiB
C

#include "sm_rtl.h"
#include "sm_cpu_infra.h"
#include "types.h"
//#include "ida_types.h"
#include "variables.h"
#include "funcs.h"
#include "spc_player.h"
#include "util.h"
struct StateRecorder;
static void RtlSaveMusicStateToRam_Locked();
static void RtlRestoreMusicAfterLoad_Locked(bool is_reset);
uint8 g_ram[0x20000];
uint8 *g_sram;
static uint8 *g_rtl_memory_ptr;
static RunFrameFunc *g_rtl_runframe;
static SyncAllFunc *g_rtl_syncall;
void RtlSetupEmuCallbacks(uint8 *emu_ram, RunFrameFunc *func, SyncAllFunc *sync_all) {
g_rtl_memory_ptr = emu_ram;
g_rtl_runframe = func;
g_rtl_syncall = sync_all;
}
static void RtlSynchronizeWholeState(void) {
if (g_rtl_syncall)
g_rtl_syncall();
}
// |ptr| must be a pointer into g_ram, will synchronize the RAM memory with the
// emulator.
static void RtlSyncMemoryRegion(void *ptr, size_t n) {
uint8 *data = (uint8 *)ptr;
assert(data >= g_ram && data < g_ram + 0x20000);
if (g_rtl_memory_ptr)
memcpy(g_rtl_memory_ptr + (data - g_ram), data, n);
}
void ByteArray_AppendVl(ByteArray *arr, uint32 v) {
for (; v >= 255; v -= 255)
ByteArray_AppendByte(arr, 255);
ByteArray_AppendByte(arr, v);
}
void saveFunc(void *ctx_in, void *data, size_t data_size) {
ByteArray_AppendData((ByteArray *)ctx_in, (uint8*)data, data_size);
}
typedef struct LoadFuncState {
uint8 *p, *pend;
} LoadFuncState;
void loadFunc(void *ctx, void *data, size_t data_size) {
LoadFuncState *st = (LoadFuncState *)ctx;
assert((size_t)(st->pend - st->p) >= data_size);
memcpy(data, st->p, data_size);
st->p += data_size;
}
static void LoadSnesState(SaveLoadFunc *func, void *ctx) {
// Do the actual loading
snes_saveload(g_snes, func, ctx);
g_snes->cpu->e = false;
uint32 next = (g_snes->ram[g_snes->cpu->sp + 3] | g_snes->ram[g_snes->cpu->sp + 4] << 8 | g_snes->ram[g_snes->cpu->sp + 5] << 16) + 1;
if (next == 0x82897e) {
g_snes->ram[g_snes->cpu->sp + 3] = (0xF71B - 1) & 0xff;
g_snes->ram[g_snes->cpu->sp + 4] = (0xF71B - 1) >> 8;
}
RtlSynchronizeWholeState();
}
static void SaveSnesState(SaveLoadFunc *func, void *ctx) {
snes_saveload(g_snes, func, ctx);
}
typedef struct StateRecorder {
uint16 last_inputs;
uint32 frames_since_last;
uint32 total_frames;
// For replay
uint32 replay_pos, replay_pos_last_complete;
uint32 replay_frame_counter;
uint32 replay_next_cmd_at;
uint32 snapshot_flags;
uint8 replay_cmd;
bool replay_mode;
ByteArray log;
ByteArray base_snapshot;
} StateRecorder;
static StateRecorder state_recorder;
void StateRecorder_Init(StateRecorder *sr) {
ByteArray_Destroy(&sr->log);
ByteArray_Destroy(&sr->base_snapshot);
memset(sr, 0, sizeof(*sr));
}
void StateRecorder_RecordCmd(StateRecorder *sr, uint8 cmd) {
int frames = sr->frames_since_last;
sr->frames_since_last = 0;
int x = (cmd < 0xc0) ? 0xf : 0x1;
ByteArray_AppendByte(&sr->log, cmd | (frames < x ? frames : x));
if (frames >= x)
ByteArray_AppendVl(&sr->log, frames - x);
}
void StateRecorder_Record(StateRecorder *sr, uint16 inputs) {
uint16 diff = inputs ^ sr->last_inputs;
if (diff != 0) {
sr->last_inputs = inputs;
// printf("0x%.4x %d: ", diff, sr->frames_since_last);
// size_t lb = sr->log.size;
for (int i = 0; i < 12; i++) {
if ((diff >> i) & 1)
StateRecorder_RecordCmd(sr, i << 4);
}
// while (lb < sr->log.size)
// printf("%.2x ", sr->log.data[lb++]);
// printf("\n");
}
sr->frames_since_last++;
sr->total_frames++;
}
void StateRecorder_RecordPatchByte(StateRecorder *sr, uint32 addr, const uint8 *value, int num) {
assert(addr < 0x20000);
printf("%d: PatchByte(0x%x, 0x%x. %d): ", sr->frames_since_last, addr, *value, num);
size_t lb = sr->log.size;
int lq = (num - 1) <= 3 ? (num - 1) : 3;
StateRecorder_RecordCmd(sr, 0xc0 | (addr & 0x10000 ? 2 : 0) | lq << 2);
if (lq == 3)
ByteArray_AppendVl(&sr->log, num - 1 - 3);
ByteArray_AppendByte(&sr->log, addr >> 8);
ByteArray_AppendByte(&sr->log, addr);
for (int i = 0; i < num; i++)
ByteArray_AppendByte(&sr->log, value[i]);
while (lb < sr->log.size)
printf("%.2x ", sr->log.data[lb++]);
printf("\n");
}
void ReadFromFile(FILE *f, void *data, size_t n) {
if (fread(data, 1, n, f) != n)
Die("fread failed\n");
}
void RtlReset(bool preserve_sram) {
snes_frame_counter = 0;
snes_reset(g_snes, true);
if (!preserve_sram)
memset(g_sram, 0, 0x2000);
coroutine_state_0 = 1;
RtlApuLock();
RtlRestoreMusicAfterLoad_Locked(true);
RtlApuUnlock();
RtlSynchronizeWholeState();
StateRecorder_Init(&state_recorder);
}
int GetFileSize(FILE *f) {
fseek(f, 0, SEEK_END);
int r = ftell(f);
fseek(f, 0, SEEK_SET);
return r;
}
void StateRecorder_Load(StateRecorder *sr, FILE *f, bool replay_mode) {
uint32 hdr[16] = { 0 };
bool is_old = false;
bool is_reset = false;
ReadFromFile(f, hdr, 8 * sizeof(uint32));
if (hdr[0] != 2) {
hdr[8] = hdr[7];
hdr[7] = hdr[5] >> 1;
hdr[5] = (hdr[5] & 1) ? hdr[6] : 0;
} else if (hdr[0] == 2) {
ReadFromFile(f, hdr + 8, 8 * sizeof(uint32));
} else {
assert(0);
}
sr->total_frames = hdr[1];
ByteArray_Resize(&sr->log, hdr[2]);
ReadFromFile(f, sr->log.data, sr->log.size);
sr->last_inputs = hdr[3];
sr->frames_since_last = hdr[4];
ByteArray_Resize(&sr->base_snapshot, hdr[5]);
ReadFromFile(f, sr->base_snapshot.data, sr->base_snapshot.size);
sr->snapshot_flags = hdr[9];
sr->replay_next_cmd_at = 0;
sr->replay_mode = replay_mode;
if (replay_mode) {
sr->frames_since_last = 0;
sr->last_inputs = 0;
sr->replay_pos = sr->replay_pos_last_complete = 0;
sr->replay_frame_counter = 0;
// Load snapshot from |base_snapshot_|, or reset if empty.
if (sr->base_snapshot.size > 8192 ) {
LoadFuncState state = { sr->base_snapshot.data, sr->base_snapshot.data + sr->base_snapshot.size };
LoadSnesState(&loadFunc, &state);
assert(state.p == state.pend);
} else {
RtlReset(false);
if (sr->base_snapshot.size == 8192)
memcpy(g_sram, sr->base_snapshot.data, 8192);
is_reset = true;
}
} else {
// Resume replay from the saved position?
sr->replay_pos = sr->replay_pos_last_complete = hdr[7];
sr->replay_frame_counter = hdr[8];
sr->replay_mode = (sr->replay_frame_counter != 0);
ByteArray arr = { 0 };
ByteArray_Resize(&arr, hdr[6]);
ReadFromFile(f, arr.data, arr.size);
LoadFuncState state = { arr.data, arr.data + arr.size };
LoadSnesState(&loadFunc, &state);
ByteArray_Destroy(&arr);
assert(state.p == state.pend);
if (is_old)
RtlClearKeyLog();
}
if (!is_reset)
RtlRestoreMusicAfterLoad_Locked(false);
// Temporarily fix reset state
// if (g_snes->cpu->k == 0x82 && g_snes->cpu->pc == 0xf716)
// g_snes->cpu->pc = 0xf71c;
}
void StateRecorder_Save(StateRecorder *sr, FILE *f, bool saving_with_bug) {
uint32 hdr[16] = { 0 };
ByteArray arr = { 0 };
SaveSnesState(&saveFunc, &arr);
assert(sr->base_snapshot.size == 0 || sr->base_snapshot.size == arr.size || sr->base_snapshot.size == 8192);
hdr[0] = 2;
hdr[1] = sr->total_frames;
hdr[2] = (uint32)sr->log.size;
hdr[3] = sr->last_inputs;
hdr[4] = sr->frames_since_last;
hdr[5] = (uint32)sr->base_snapshot.size;
hdr[6] = (uint32)arr.size;
// If saving while in replay mode, also need to persist
// sr->replay_pos_last_complete and sr->replay_frame_counter
// so the replaying can be resumed.
if (sr->replay_mode) {
hdr[7] = sr->replay_pos_last_complete;
hdr[8] = sr->replay_frame_counter;
}
hdr[9] = saving_with_bug * 1;
fwrite(hdr, 1, sizeof(hdr), f);
fwrite(sr->log.data, 1, sr->log.size, f);
fwrite(sr->base_snapshot.data, 1, sr->base_snapshot.size, f);
fwrite(arr.data, 1, arr.size, f);
ByteArray_Destroy(&arr);
}
void StateRecorder_ClearKeyLog(StateRecorder *sr) {
printf("Clearing key log!\n");
sr->base_snapshot.size = 0;
SaveSnesState(&saveFunc, &sr->base_snapshot);
ByteArray old_log = sr->log;
int old_frames_since_last = sr->frames_since_last;
memset(&sr->log, 0, sizeof(sr->log));
// If there are currently any active inputs, record them initially at timestamp 0.
sr->frames_since_last = 0;
if (sr->last_inputs) {
for (int i = 0; i < 12; i++) {
if ((sr->last_inputs >> i) & 1)
StateRecorder_RecordCmd(sr, i << 4);
}
}
if (sr->replay_mode) {
// When clearing the key log while in replay mode, we want to keep
// replaying but discarding all key history up until this point.
if (sr->replay_next_cmd_at != 0xffffffff) {
sr->replay_next_cmd_at -= old_frames_since_last;
sr->frames_since_last = sr->replay_next_cmd_at;
sr->replay_pos_last_complete = (uint32)sr->log.size;
StateRecorder_RecordCmd(sr, sr->replay_cmd);
int old_replay_pos = sr->replay_pos;
sr->replay_pos = (uint32)sr->log.size;
ByteArray_AppendData(&sr->log, old_log.data + old_replay_pos, old_log.size - old_replay_pos);
}
sr->total_frames -= sr->replay_frame_counter;
sr->replay_frame_counter = 0;
} else {
sr->total_frames = 0;
}
ByteArray_Destroy(&old_log);
sr->frames_since_last = 0;
}
uint16 StateRecorder_ReadNextReplayState(StateRecorder *sr) {
assert(sr->replay_mode);
while (sr->frames_since_last >= sr->replay_next_cmd_at) {
int replay_pos = sr->replay_pos;
if (replay_pos != sr->replay_pos_last_complete) {
// Apply next command
sr->frames_since_last = 0;
if (sr->replay_cmd < 0xc0) {
sr->last_inputs ^= 1 << (sr->replay_cmd >> 4);
} else if (sr->replay_cmd < 0xd0) {
int nb = 1 + ((sr->replay_cmd >> 2) & 3);
uint8 t;
if (nb == 4) do {
nb += t = sr->log.data[replay_pos++];
} while (t == 255);
uint32 addr = ((sr->replay_cmd >> 1) & 1) << 16;
addr |= sr->log.data[replay_pos++] << 8;
addr |= sr->log.data[replay_pos++];
do {
g_ram[addr & 0x1ffff] = sr->log.data[replay_pos++];
RtlSyncMemoryRegion(&g_ram[addr & 0x1ffff], 1);
} while (addr++, --nb);
} else {
assert(0);
}
}
sr->replay_pos_last_complete = replay_pos;
if (replay_pos >= sr->log.size) {
sr->replay_pos = replay_pos;
sr->replay_next_cmd_at = 0xffffffff;
break;
}
// Read the next one
uint8 cmd = sr->log.data[replay_pos++], t;
int mask = (cmd < 0xc0) ? 0xf : 0x1;
int frames = cmd & mask;
if (frames == mask) do {
frames += t = sr->log.data[replay_pos++];
} while (t == 255);
sr->replay_next_cmd_at = frames;
sr->replay_cmd = cmd;
sr->replay_pos = replay_pos;
}
sr->frames_since_last++;
// Turn off replay mode after we reached the final frame position
if (++sr->replay_frame_counter >= sr->total_frames) {
sr->replay_mode = false;
}
return sr->last_inputs;
}
void StateRecorder_StopReplay(StateRecorder *sr) {
if (!sr->replay_mode)
return;
sr->replay_mode = false;
sr->total_frames = sr->replay_frame_counter;
sr->log.size = sr->replay_pos_last_complete;
}
void RtlClearKeyLog(void) {
StateRecorder_ClearKeyLog(&state_recorder);
}
void RtlStopReplay(void) {
StateRecorder_StopReplay(&state_recorder);
}
bool RtlRunFrame(int inputs) {
// Avoid up/down and left/right from being pressed at the same time
if ((inputs & 0x30) == 0x30) inputs ^= 0x30;
if ((inputs & 0xc0) == 0xc0) inputs ^= 0xc0;
bool is_replay = state_recorder.replay_mode;
// Either copy state or apply state
if (is_replay) {
inputs = StateRecorder_ReadNextReplayState(&state_recorder);
} else {
// Loading a bug snapshot?
if (state_recorder.snapshot_flags & 1) {
state_recorder.snapshot_flags &= ~1;
inputs = state_recorder.last_inputs;
}
StateRecorder_Record(&state_recorder, inputs);
}
g_rtl_runframe(inputs, 0);
snes_frame_counter++;
RtlPushApuState();
return is_replay;
}
void RtlSaveSnapshot(const char *filename, bool saving_with_bug) {
FILE *f = fopen(filename, "wb");
RtlApuLock();
RtlSaveMusicStateToRam_Locked();
StateRecorder_Save(&state_recorder, f, saving_with_bug);
RtlApuUnlock();
fclose(f);
}
void RtlSaveLoad(int cmd, int slot) {
char name[128];
sprintf(name, "saves/save%d.sav", slot);
printf("*** %s slot %d\n",
cmd == kSaveLoad_Save ? "Saving" : cmd == kSaveLoad_Load ? "Loading" : "Replaying", slot);
if (cmd != kSaveLoad_Save) {
FILE *f = fopen(name, "rb");
if (f == NULL) {
printf("Failed fopen: %s\n", name);
return;
}
RtlApuLock();
StateRecorder_Load(&state_recorder, f, cmd == kSaveLoad_Replay);
ppu_copy(g_snes->my_ppu, g_snes->ppu);
RtlApuUnlock();
RtlSynchronizeWholeState();
fclose(f);
} else {
RtlSaveSnapshot(name, false);
}
}
void MemCpy(void *dst, const void *src, int size) {
memcpy(dst, src, size);
}
void Negate32(const uint16 *src_hi, const uint16 *src_lo, uint16 *dst_hi, uint16 *dst_lo) {
uint32 x = (uint32)*src_hi << 16 | *src_lo;
x = -(int)x;
*dst_lo = x;
*dst_hi = x >> 16;
}
PairU16 MakePairU16(uint16 k, uint16 j) {
PairU16 r = { k, j };
return r;
}
void mov24(struct LongPtr *a, unsigned int d) {
a->addr = d & 0xffff;
a->bank = d >> 16;
}
void copy24(LongPtr *dst, LongPtr *src) {
*dst = *src;
}
uint32 Load24(void *a) {
return *(uint32 *)a & 0xffffff;
}
void DecompressToMem_IpArg(const void *p) {
decompress_dst = *(LongPtr *)p;
DecompressToMem();
}
bool Unreachable(void) {
printf("Unreachable!\n");
assert(0);
g_ram[0x1ffff] = 1;
return false;
}
uint8_t *RomPtr(uint32_t addr) {
if (!(addr & 0x8000)) {
printf("RomPtr - Invalid access 0x%x!\n", addr);
g_fail = true;
}
return &g_snes->cart->rom[(((addr >> 16) << 15) | (addr & 0x7fff)) & (g_snes->cart->romSize - 1)];
}
uint8_t *IndirPtr(void *ptr, uint16 offs) {
uint32 a = (*(uint32 *)ptr & 0xffffff) + offs;
if ((a >> 16) >= 0x7e && (a >> 16) <= 0x7f || a < 0x2000) {
return &g_ram[a & 0x1ffff];
} else {
return RomPtr(a);
}
}
void IndirWriteWord(void *ptr, uint16 offs, uint16 value) {
*(uint16 *)IndirPtr(ptr, offs) = value;
}
void IndirWriteByte(void *ptr, uint16 offs, uint8 value) {
*IndirPtr(ptr, offs) = value;
}
void WriteReg(uint16 reg, uint8 value) {
snes_write(g_snes, reg, value);
}
uint16 Mult8x8(uint8 a, uint8 b) {
return a * b;
}
uint16 SnesDivide(uint16 a, uint8 b) {
return (b == 0) ? 0xffff : a / b;
}
uint16 SnesModulus(uint16 a, uint8 b) {
return (b == 0) ? a : a % b;
}
uint8 ReadReg(uint16 reg) {
return snes_read(g_snes, reg);
}
uint16 ReadRegWord(uint16 reg) {
uint16_t rv = ReadReg(reg);
rv |= ReadReg(reg + 1) << 8;
return rv;
}
void WriteRegWord(uint16 reg, uint16 value) {
WriteReg(reg, (uint8)value);
WriteReg(reg + 1, value >> 8);
}
// Maintain a queue cause the snes and audio callback are not in sync.
// If an entry is 255, it means unset.
typedef struct ApuWriteEnt {
uint8 ports[4];
} ApuWriteEnt;
enum {
kApuMaxQueueSize = 16,
};
static struct ApuWriteEnt g_apu_write_ents[kApuMaxQueueSize], g_apu_write;
static uint8 g_apu_write_ent_pos, g_apu_queue_size, g_apu_time_since_empty;
void RtlApuWrite(uint32 adr, uint8 val) {
assert(adr >= APUI00 && adr <= APUI03);
if (is_uploading_apu) {
snes_catchupApu(g_snes); // catch up the apu before writing
g_snes->apu->inPorts[adr & 0x3] = val;
return;
}
if (g_snes->runningWhichVersion != 2) {
g_apu_write.ports[adr & 0x3] = val;
}
}
static bool IsFrameEmpty(ApuWriteEnt *w) {
return (w->ports[0] == 255) && (w->ports[1] == 255) && (w->ports[2] == 255) && (w->ports[3] == 255);
}
void RtlPushApuState(void) {
RtlApuLock();
if (!is_uploading_apu) {
// Strive for the queue to be empty.
if (g_apu_queue_size == 0) {
g_apu_time_since_empty = 0;
} else {
if (g_apu_time_since_empty >= 32 && IsFrameEmpty(&g_apu_write)) {
g_apu_time_since_empty -= 4;
RtlApuUnlock();
return;
}
g_apu_time_since_empty++;
}
// Merge the two oldest to make space
ApuWriteEnt *w0 = &g_apu_write_ents[g_apu_write_ent_pos++ & (kApuMaxQueueSize - 1)];
if (g_apu_queue_size == kApuMaxQueueSize) {
ApuWriteEnt *w1 = &g_apu_write_ents[g_apu_write_ent_pos & (kApuMaxQueueSize - 1)];
for (int i = 0; i < 4; i++)
if (w1->ports[i] == 255)
w1->ports[i] = w0->ports[i];
} else {
g_apu_queue_size++;
}
*w0 = g_apu_write;
memset(&g_apu_write, 0xff, sizeof(g_apu_write));
} else {
g_apu_queue_size = 0;
}
RtlApuUnlock();
}
static void RtlPopApuState_Locked(void) {
if (is_uploading_apu)
return;
uint8 *input_ports = g_use_my_apu_code ? g_spc_player->input_ports : g_snes->apu->inPorts;
if (g_apu_queue_size != 0) {
ApuWriteEnt *w = &g_apu_write_ents[(g_apu_write_ent_pos - g_apu_queue_size--) & (kApuMaxQueueSize - 1)];
for (int i = 0; i != 4; i++) {
if (w->ports[i] != 255)
input_ports[i] = w->ports[i];
}
}
}
static void RtlResetApuQueue(void) {
g_apu_write_ent_pos = g_apu_time_since_empty = g_apu_queue_size = 0;
memset(&g_apu_write, 0xff, sizeof(g_apu_write));
}
void RtlApuUpload(const uint8 *p) {
RtlApuLock();
RtlResetApuQueue();
SpcPlayer_Upload(g_spc_player, p);
RtlApuUnlock();
}
void RtlRestoreMusicAfterLoad_Locked(bool is_reset) {
if (g_use_my_apu_code) {
memcpy(g_spc_player->ram, g_snes->apu->ram, 65536);
memcpy(g_spc_player->dsp->ram, g_snes->apu->dsp->ram, sizeof(Dsp) - offsetof(Dsp, ram));
SpcPlayer_CopyVariablesFromRam(g_spc_player);
}
if (is_reset) {
SpcPlayer_Initialize(g_spc_player);
}
RtlResetApuQueue();
}
void RtlSaveMusicStateToRam_Locked(void) {
if (g_use_my_apu_code) {
// Apply the whole contents of the queue to input ports
SpcPlayer *spc_player = g_spc_player;
for (int i = g_apu_queue_size; i; i--) {
ApuWriteEnt *we = &g_apu_write_ents[(g_apu_write_ent_pos - i) & (kApuMaxQueueSize - 1)];
for (int j = 0; j < 4; j++) {
if (we->ports[j] != 255)
spc_player->input_ports[j] = we->ports[j];
}
}
SpcPlayer_CopyVariablesToRam(g_spc_player);
memcpy(g_snes->apu->dsp->ram, g_spc_player->dsp->ram, sizeof(Dsp) - offsetof(Dsp, ram));
memcpy(g_snes->apu->ram, g_spc_player->ram, 65536);
}
}
void RtlRenderAudio(int16 *audio_buffer, int samples, int channels) {
assert(channels == 2);
RtlApuLock();
RtlPopApuState_Locked();
if (!g_use_my_apu_code) {
if (!is_uploading_apu) {
while (g_snes->apu->dsp->sampleOffset < 534)
apu_cycle(g_snes->apu);
dsp_getSamples(g_snes->apu->dsp, audio_buffer, samples);
}
} else {
SpcPlayer_GenerateSamples(g_spc_player);
dsp_getSamples(g_spc_player->dsp, audio_buffer, samples);
}
RtlApuUnlock();
}
void RtlCheat(char c) {
if (c == 'w') {
samus_health = samus_max_health;
StateRecorder_RecordPatchByte(&state_recorder, 0x9C2, (uint8 *)&samus_health, 2);
} else if (c == 'q' && 0) {
samus_y_pos -= 32;
samus_y_speed = 0;
StateRecorder_RecordPatchByte(&state_recorder, 0xafa, (uint8 *)&samus_y_pos, 2);
StateRecorder_RecordPatchByte(&state_recorder, 0xb2e, (uint8 *)&samus_y_speed, 2);
menu_index = 0;
} else if (c == 'q') {
japanese_text_flag = !japanese_text_flag;
StateRecorder_RecordPatchByte(&state_recorder, 0x9e2, (uint8 *)&japanese_text_flag, 2);
}
}
void RtlReadSram(void) {
FILE *f = fopen("saves/sm.srm", "rb");
if (f) {
if (fread(g_sram, 1, 8192, f) != 8192)
fprintf(stderr, "Error reading saves/sm.srm\n");
fclose(f);
RtlSynchronizeWholeState();
ByteArray_Resize(&state_recorder.base_snapshot, 8192);
memcpy(state_recorder.base_snapshot.data, g_sram, 8192);
}
}
void RtlWriteSram(void) {
rename("saves/sm.srm", "saves/sm.srm.bak");
FILE *f = fopen("saves/sm.srm", "wb");
if (f) {
fwrite(g_sram, 1, 8192, f);
fclose(f);
} else {
fprintf(stderr, "Unable to write saves/sm.srm\n");
}
}