Compare commits

..

2 Commits

Author SHA1 Message Date
ecbcce12ea fix 2025-10-15 20:51:52 +02:00
f6c800fc63 gameboy save states 2025-10-15 20:46:48 +02:00

View File

@@ -299,6 +299,9 @@ public:
break;
}
if (mode != Mode::Running)
break;
GB_PERF_ONLY(perf.geometryUs = 0;)
@@ -339,6 +342,16 @@ public:
GB_PERF_ONLY(perf.renderUs = nowMicros() - renderStartUs;)
break;
}
case Mode::Prompt: {
GB_PERF_ONLY(const uint64_t handleStartUs = nowMicros();)
handlePromptInput(input);
GB_PERF_ONLY(perf.handleUs = nowMicros() - handleStartUs;)
GB_PERF_ONLY(const uint64_t renderStartUs = nowMicros();)
renderPrompt();
GB_PERF_ONLY(perf.renderUs = nowMicros() - renderStartUs;)
break;
}
}
prevInput = input;
@@ -805,8 +818,9 @@ public:
}
};
enum class Mode { Browse, Running };
enum class Mode { Browse, Running, Prompt };
enum class ScaleMode { Original, FullHeight, FullHeightWide };
enum class PromptKind { None, LoadState, SaveState };
struct PerfTracker {
enum class CallbackKind { RomRead, CartRamRead, CartRamWrite, LcdDraw, Error };
@@ -934,7 +948,18 @@ public:
void printStep(Mode stepMode, bool gbReady, bool frameDirty, uint32_t fps, const std::string& romName,
std::size_t romCount, std::size_t selectedIdx, bool browserDirty) const {
auto toMs = [](uint64_t us) { return static_cast<double>(us) / 1000.0; };
const char* modeStr = (stepMode == Mode::Running) ? "RUN" : "BROWSE";
const char* modeStr = "UNKNOWN";
switch (stepMode) {
case Mode::Running:
modeStr = "RUN";
break;
case Mode::Browse:
modeStr = "BROWSE";
break;
case Mode::Prompt:
modeStr = "PROMPT";
break;
}
const char* name = romName.empty() ? "-" : romName.c_str();
const std::size_t safeIdx = (romCount == 0) ? 0 : std::min(selectedIdx, romCount - 1);
std::printf(
@@ -1113,6 +1138,10 @@ public:
uint32_t fpsCurrent = 0;
std::string activeRomName;
std::string activeRomSavePath;
std::string activeRomStatePath;
PromptKind promptKind = PromptKind::None;
int promptSelection = 0; // 0 = Yes, 1 = No
bool promptDirty = false;
SimpleApu apu{};
// Smoothing state for buzzer tone
uint32_t lastFreqHz = 0;
@@ -1451,21 +1480,25 @@ public:
return false;
}
gb.direct.priv = this;
gb.direct.joypad = 0xFF;
gb_init_lcd(&gb, &GameboyApp::lcdDrawLine);
applyRuntimeBindings();
gb.direct.joypad = 0xFF;
gb.direct.interlace = false;
gb.direct.frame_skip = true;
const uint_fast32_t saveSize = gb_get_save_size(&gb);
cartRam.assign(static_cast<std::size_t>(saveSize), 0);
std::string savePath;
std::string statePath;
const bool fsReady = (filesystem && filesystem->isMounted()) || ensureFilesystemReady();
if (fsReady)
savePath = buildSavePath(rom, romDirectory());
if (fsReady) {
const std::string romDir = romDirectory();
savePath = buildSavePath(rom, romDir);
statePath = buildStatePath(rom, romDir);
}
activeRomSavePath = savePath;
activeRomStatePath = statePath;
loadSaveFile();
resetFpsStats();
@@ -1480,6 +1513,10 @@ public:
if (!fsReady)
statusText.append(" (no save)");
setStatus(std::move(statusText));
if (stateFileExists())
enterPrompt(PromptKind::LoadState);
return true;
}
@@ -1492,6 +1529,10 @@ public:
cartRam.clear();
activeRomName.clear();
activeRomSavePath.clear();
activeRomStatePath.clear();
promptKind = PromptKind::None;
promptSelection = 0;
promptDirty = false;
return;
}
@@ -1505,6 +1546,10 @@ public:
cartRam.clear();
activeRomName.clear();
activeRomSavePath.clear();
activeRomStatePath.clear();
promptKind = PromptKind::None;
promptSelection = 0;
promptDirty = false;
std::memset(&gb, 0, sizeof(gb));
apu.reset();
mode = Mode::Browse;
@@ -1519,6 +1564,19 @@ public:
if (scaleToggleCombo)
toggleScaleMode();
const bool exitComboPressed = input.start && input.select;
const bool exitComboJustPressed = exitComboPressed && !(prevInput.start && prevInput.select);
if (exitComboJustPressed) {
if (!activeRomStatePath.empty()) {
enterPrompt(PromptKind::SaveState);
} else {
unloadRom();
setStatus("Save state unavailable");
}
gb.direct.joypad = 0xFF;
return;
}
uint8_t joypad = 0xFF;
if (input.a)
joypad &= ~JOYPAD_A;
@@ -1538,13 +1596,6 @@ public:
joypad &= ~JOYPAD_RIGHT;
gb.direct.joypad = joypad;
const bool exitComboPressed = input.start && input.select;
const bool exitComboJustPressed = exitComboPressed && !(prevInput.start && prevInput.select);
if (exitComboJustPressed) {
setStatus("Saved " + activeRomName);
unloadRom();
}
}
void renderGameFrame() {
@@ -1716,6 +1767,186 @@ public:
}
}
void applyRuntimeBindings() {
gb.gb_cart_ram_read = &GameboyApp::cartRamRead;
gb.gb_cart_ram_write = &GameboyApp::cartRamWrite;
gb.gb_error = &GameboyApp::errorCallback;
gb.display.lcd_draw_line = &GameboyApp::lcdDrawLine;
gb.direct.priv = this;
if (romDataView)
gb.direct.rom = romDataView;
}
bool saveStateToFile() {
if (activeRomStatePath.empty())
return false;
FILE* file = std::fopen(activeRomStatePath.c_str(), "wb");
if (!file)
return false;
const size_t written = std::fwrite(&gb, 1, sizeof(gb), file);
std::fclose(file);
return written == sizeof(gb);
}
bool loadStateFromFile() {
if (activeRomStatePath.empty())
return false;
FILE* file = std::fopen(activeRomStatePath.c_str(), "rb");
if (!file)
return false;
std::vector<uint8_t> backup(sizeof(gb));
std::memcpy(backup.data(), &gb, sizeof(gb));
const size_t read = std::fread(&gb, 1, sizeof(gb), file);
std::fclose(file);
if (read != sizeof(gb)) {
std::memcpy(&gb, backup.data(), sizeof(gb));
return false;
}
applyRuntimeBindings();
gb.direct.joypad = 0xFF;
frameDirty = true;
return true;
}
bool stateFileExists() const {
if (activeRomStatePath.empty())
return false;
struct stat st{};
return stat(activeRomStatePath.c_str(), &st) == 0 && S_ISREG(st.st_mode);
}
void enterPrompt(PromptKind kind) {
if (kind == PromptKind::None)
return;
promptKind = kind;
promptSelection = 0;
promptDirty = true;
mode = Mode::Prompt;
gb.direct.joypad = 0xFF;
scheduleNextTick(0);
}
void exitPrompt(Mode nextMode) {
promptKind = PromptKind::None;
promptSelection = 0;
promptDirty = true;
mode = nextMode;
}
void handlePromptInput(const InputState& input) {
if (promptKind == PromptKind::None)
return;
const bool leftOrUp = (input.left && !prevInput.left) || (input.up && !prevInput.up);
const bool rightOrDown = (input.right && !prevInput.right) || (input.down && !prevInput.down);
if (leftOrUp && promptSelection != 0) {
promptSelection = 0;
promptDirty = true;
} else if (rightOrDown && promptSelection != 1) {
promptSelection = 1;
promptDirty = true;
}
const bool confirm = (input.a && !prevInput.a) || (input.start && !prevInput.start);
const bool cancel = (input.b && !prevInput.b) || (input.select && !prevInput.select);
if (confirm) {
handlePromptDecision(promptSelection == 0);
} else if (cancel) {
handlePromptDecision(false);
}
}
void handlePromptDecision(bool yesSelected) {
const PromptKind kind = promptKind;
if (kind == PromptKind::None)
return;
if (kind == PromptKind::LoadState) {
exitPrompt(Mode::Running);
if (yesSelected) {
if (loadStateFromFile())
setStatus("Save state loaded");
else
setStatus("Load state failed");
}
frameDirty = true;
return;
}
if (kind == PromptKind::SaveState) {
const bool haveStatePath = !activeRomStatePath.empty();
bool saved = false;
if (yesSelected && haveStatePath)
saved = saveStateToFile();
exitPrompt(Mode::Running);
unloadRom();
if (yesSelected) {
if (!haveStatePath)
setStatus("Save state unavailable");
else if (saved)
setStatus("Save state written");
else
setStatus("Save state failed");
} else {
setStatus("Exited without state");
}
}
}
void renderPrompt() {
if (!promptDirty || promptKind == PromptKind::None)
return;
promptDirty = false;
framebuffer.frameReady();
framebuffer.clear(false);
auto drawCentered = [&](int y, std::string_view text, int scale = 1) {
const int width = font16x8::measureText(text, scale, 1);
const int x = (framebuffer.width() - width) / 2;
font16x8::drawText(framebuffer, x, y, text, scale, true, 1);
};
std::string_view headline;
std::string_view helper;
switch (promptKind) {
case PromptKind::LoadState:
headline = "LOAD SAVE STATE?";
helper = "A YES / B NO";
break;
case PromptKind::SaveState:
headline = "SAVE BEFORE EXIT?";
helper = "A YES / B NO";
break;
case PromptKind::None:
default:
headline = "";
helper = "";
break;
}
drawCentered(40, headline);
drawCentered(72, helper);
const std::string yesLabel = (promptSelection == 0) ? "> YES" : " YES";
const std::string noLabel = (promptSelection == 1) ? "> NO" : " NO";
drawCentered(104, yesLabel);
drawCentered(120, noLabel);
framebuffer.sendFrame();
}
void playTone(uint32_t freqHz, uint32_t durationMs, uint32_t gapMs) {
if (freqHz == 0 || durationMs == 0)
return;
@@ -1767,6 +1998,21 @@ public:
return result;
}
static std::string buildStatePath(const RomEntry& rom, std::string_view romDir) {
std::string slug = rom.saveSlug;
if (slug.empty())
slug = sanitizeSaveSlug(rom.name);
if (slug.empty())
slug = "rom";
std::string result(romDir);
if (!result.empty() && result.back() != '/')
result.push_back('/');
result.append(slug);
result.append(".state");
return result;
}
static GameboyApp* fromGb(struct gb_s* gb) {
CARDBOY_CHECK_CODE(if (!gb) return nullptr;);
return static_cast<GameboyApp*>(gb->direct.priv);