From 9abf8101dacc823f0976ba43138104e39959f1bd Mon Sep 17 00:00:00 2001 From: Dominic Masters Date: Tue, 4 Aug 2026 09:54:45 -0500 Subject: [PATCH] PSP: save through the real sceUtilitySavedata API, not raw file I/O Rewrote savepsp.c/savestreampsp.c to use sceUtilitySavedataInitStart/ Update/GetStatus/ShutdownStart instead of sceIoOpen/Read/Write, so PSP saves get a proper OS-generated PARAM.SFO (title/savedataTitle/detail) and show up correctly in the native save browser. This dialog spans multiple frames and, per this project's prior experience with the network config dialog, must be pumped non-blocking one step per real engine frame rather than blocked on synchronously - a raw-sceGu blocking loop already froze the app on real hardware for that dialog, since pspGL owns the GU context. So save.h's saveWrite()/saveLoad() are now callback-based (savecallback_t onComplete) instead of returning a result directly, mirroring networkRequestConnection()'s shape, with a new saveUpdate() (wired into engineUpdate()) pumping the active op each frame. Linux/Dolphin behavior is unchanged - their fallback path in save.c still completes synchronously, just via an immediate callback call instead of a direct return. Two real bugs found via PPSSPP testing (not just code review): SAVE/LOAD modes show a confirm screen even for brand-new data, which blocks forever headlessly - switched to AUTOSAVE/AUTOLOAD, which write/read silently and generate the identical PARAM.SFO. And PPSSPP's dialog status goes straight from QUIT to NONE without a separately observable FINISHED in between, which the first version misread as "disappeared without a result" even on a successful save - fixed by tracking whether QUIT was already seen. Confirmed end-to-end in PPSSPP: write, dialog completes, PARAM.SFO + encrypted save.bin appear on the virtual memory stick, and a subsequent load decrypts/deserializes back to the exact original data. Not tested on real PSP hardware. --- src/dusk/engine/engine.c | 1 + src/dusk/rpg/rpg.c | 10 +- src/dusk/save/save.c | 56 +++-- src/dusk/save/save.h | 50 ++++- src/dusk/save/savefile.h | 11 + src/dusk/ui/frame/game/uigamemenu.c | 24 ++- src/duskpsp/save/saveplatform.h | 19 +- src/duskpsp/save/savepsp.c | 315 ++++++++++++++++++++++------ src/duskpsp/save/savepsp.h | 119 +++++++---- src/duskpsp/save/savestreampsp.c | 63 ++---- src/duskpsp/save/savestreampsp.h | 54 ++--- 11 files changed, 499 insertions(+), 223 deletions(-) diff --git a/src/dusk/engine/engine.c b/src/dusk/engine/engine.c index 38149eb9..b74392b5 100644 --- a/src/dusk/engine/engine.c +++ b/src/dusk/engine/engine.c @@ -62,6 +62,7 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) { errorret_t engineUpdate(void) { // Order here is important. errorChain(networkUpdate()); + errorChain(saveUpdate()); timeUpdate(); inputUpdate(); consoleUpdate(); diff --git a/src/dusk/rpg/rpg.c b/src/dusk/rpg/rpg.c index c7f751d9..94e7496a 100644 --- a/src/dusk/rpg/rpg.c +++ b/src/dusk/rpg/rpg.c @@ -23,6 +23,10 @@ #include "ui/rpg/uiemoji.h" +static void rpgTestSaveComplete(errorret_t result, void *user) { + if(errorIsNotOk(result)) errorCatch(errorPrint(result)); +} + errorret_t rpgInit(void) { memoryZero(ENTITIES, sizeof(ENTITIES)); memoryZero(MAP_AREAS, sizeof(MAP_AREAS)); @@ -54,10 +58,12 @@ errorret_t rpgInit(void) { backpackAdd(ITEM_ID_APPLE, 8); // TEST: Verify the save system round-trips real game data, not just the - // header/version. Remove once there's an actual name-entry flow. + // header/version. Remove once there's an actual name-entry flow. On PSP + // this shows the real native save dialog every boot - expected while + // testing that path, not something to ship as-is. savefile_t *saveFile = saveGet(0); stringCopy(saveFile->playerName, "Dusk", SAVE_PLAYER_NAME_MAX); - errorCatch(errorPrint(saveWrite(0))); + saveWrite(0, rpgTestSaveComplete, NULL); // All Good! errorOk(); diff --git a/src/dusk/save/save.c b/src/dusk/save/save.c index 56411638..2365b6c3 100644 --- a/src/dusk/save/save.c +++ b/src/dusk/save/save.c @@ -41,22 +41,46 @@ errorret_t saveDispose(void) { errorOk(); } -errorret_t saveLoad(const uint8_t slot) { +errorret_t saveUpdate(void) { + #ifdef savePlatformUpdate + errorChain(savePlatformUpdate()); + #endif + errorOk(); +} + +bool_t saveIsBusy(void) { + #ifdef saveIsBusyPlatform + return saveIsBusyPlatform(); + #else + return false; + #endif +} + +void saveLoad(const uint8_t slot, savecallback_t onComplete, void *user) { assertTrue(slot < SAVE_FILE_COUNT_MAX, "slot exceeds SAVE_FILE_COUNT_MAX"); + assertNotNull(onComplete, "onComplete cannot be NULL"); savefile_t *file = &SAVE.files[slot]; file->exists = false; + // Some platforms (PSP's native save dialog) can't complete within this + // call - they take over entirely and invoke onComplete later, from + // saveUpdate(), once their own multi-frame flow finishes. + #ifdef saveAsyncLoadPlatform + saveAsyncLoadPlatform(slot, onComplete, user); + return; + #endif + savestream_t stream; memoryZero(&stream, sizeof(savestream_t)); #ifdef saveStreamOpenReadPlatform errorret_t openRet = saveStreamOpenReadPlatform(&stream, slot); SAVE.available = errorIsOk(openRet); - errorChain(openRet); + if(errorIsNotOk(openRet)) { onComplete(openRet, user); return; } #endif - if(!stream.found) errorOk(); + if(!stream.found) { onComplete(errorOkImpl(), user); return; } errorret_t ret = saveFileLoad(&stream, file); @@ -64,16 +88,14 @@ errorret_t saveLoad(const uint8_t slot) { saveStreamClosePlatform(&stream); #endif - if(errorIsNotOk(ret)) return ret; - - errorChain(saveStreamVerifyChecksumImpl(&stream, slot)); - - file->exists = true; - errorOk(); + if(errorIsOk(ret)) ret = saveStreamVerifyChecksumImpl(&stream, slot); + file->exists = errorIsOk(ret); + onComplete(ret, user); } -errorret_t saveWrite(const uint8_t slot) { +void saveWrite(const uint8_t slot, savecallback_t onComplete, void *user) { assertTrue(slot < SAVE_FILE_COUNT_MAX, "slot exceeds SAVE_FILE_COUNT_MAX"); + assertNotNull(onComplete, "onComplete cannot be NULL"); savefile_t *file = &SAVE.files[slot]; // These are metadata about the file itself, not game data - always stamp @@ -84,13 +106,19 @@ errorret_t saveWrite(const uint8_t slot) { memoryCopy(file->header, SAVE_FILE_HEADER, SAVE_FILE_HEADER_SIZE); file->version = SAVE_FILE_VERSION; + // See saveLoad() - some platforms take over and complete later. + #ifdef saveAsyncWritePlatform + saveAsyncWritePlatform(slot, onComplete, user); + return; + #endif + savestream_t stream; memoryZero(&stream, sizeof(savestream_t)); #ifdef saveStreamOpenWritePlatform errorret_t openRet = saveStreamOpenWritePlatform(&stream, slot); SAVE.available = errorIsOk(openRet); - errorChain(openRet); + if(errorIsNotOk(openRet)) { onComplete(openRet, user); return; } #endif errorret_t ret = saveFileWrite(&stream, file); @@ -103,10 +131,8 @@ errorret_t saveWrite(const uint8_t slot) { saveStreamClosePlatform(&stream); #endif - if(errorIsNotOk(ret)) return ret; - - file->exists = true; - errorOk(); + file->exists = errorIsOk(ret); + onComplete(ret, user); } errorret_t saveDelete(const uint8_t slot) { diff --git a/src/dusk/save/save.h b/src/dusk/save/save.h index bef94275..d3662168 100644 --- a/src/dusk/save/save.h +++ b/src/dusk/save/save.h @@ -23,6 +23,13 @@ typedef struct { * expected conditions here, not fatal errors - see saveIsAvailable(). */ bool_t available; + /** + * Scratch error state used by platforms whose save/load completes + * asynchronously (see saveIsBusy()) to construct a result to hand to a + * savecallback_t from inside saveUpdate(), rather than from a direct + * errorThrow() return - mirrors network_t.errorState for the same reason. + */ + errorstate_t errorState; } save_t; extern save_t SAVE; @@ -56,20 +63,49 @@ bool_t saveIsAvailable(void); errorret_t saveDispose(void); /** - * Loads the save file for a given slot from persistent storage. + * Updates the save manager, pumping any in-progress async save/load and + * dispatching its callback once complete. No-op on platforms where + * saveWrite()/saveLoad() always complete synchronously (see saveIsBusy()). + * Must be called every engine frame for platforms that need it (PSP's + * native save dialog spans multiple frames). * - * @param slot The save slot index (0 to SAVE_FILE_COUNT_MAX - 1). - * @return An error code if the load fails. + * @return An error code indicating success or failure. */ -errorret_t saveLoad(const uint8_t slot); +errorret_t saveUpdate(void); /** - * Writes the save file for a given slot to persistent storage. + * True while an async saveWrite()/saveLoad() is in progress (e.g. PSP's + * native save dialog is open). Calling saveWrite()/saveLoad() again while + * this is true is undefined behavior - wait for the previous call's + * callback first. + * + * @return True if a save/load request is currently in progress. + */ +bool_t saveIsBusy(void); + +/** + * Loads the save file for a given slot from persistent storage. Slow/async + * on some platforms (PSP's native save dialog spans multiple frames) - on + * others (Linux, Dolphin) onComplete is invoked before this call returns. + * See saveIsBusy(). * * @param slot The save slot index (0 to SAVE_FILE_COUNT_MAX - 1). - * @return An error code if the write fails. + * @param onComplete Callback invoked with the result once loading finishes. + * @param user User data passed through to onComplete. */ -errorret_t saveWrite(const uint8_t slot); +void saveLoad(const uint8_t slot, savecallback_t onComplete, void *user); + +/** + * Writes the save file for a given slot to persistent storage. Slow/async + * on some platforms (PSP's native save dialog spans multiple frames) - on + * others (Linux, Dolphin) onComplete is invoked before this call returns. + * See saveIsBusy(). + * + * @param slot The save slot index (0 to SAVE_FILE_COUNT_MAX - 1). + * @param onComplete Callback invoked with the result once writing finishes. + * @param user User data passed through to onComplete. + */ +void saveWrite(const uint8_t slot, savecallback_t onComplete, void *user); /** * Deletes the save file for a given slot from persistent storage. diff --git a/src/dusk/save/savefile.h b/src/dusk/save/savefile.h index b2828b64..6e118ce6 100644 --- a/src/dusk/save/savefile.h +++ b/src/dusk/save/savefile.h @@ -33,3 +33,14 @@ typedef struct { /** The player's saved name. */ char_t playerName[SAVE_PLAYER_NAME_MAX]; } savefile_t; + +/** + * Callback invoked when an async saveWrite()/saveLoad() request completes. + * Declared here (rather than save.h) so platform save headers - which + * save.h's platform indirection pulls in before save.h finishes defining + * anything else - can reference it without a circular include. + * + * @param result Whether the request succeeded. + * @param user User data passed through from the original call. + */ +typedef void (*savecallback_t)(errorret_t result, void *user); diff --git a/src/dusk/ui/frame/game/uigamemenu.c b/src/dusk/ui/frame/game/uigamemenu.c index 6ba12811..e3e2664c 100644 --- a/src/dusk/ui/frame/game/uigamemenu.c +++ b/src/dusk/ui/frame/game/uigamemenu.c @@ -29,22 +29,16 @@ // slot-select UI yet, and SAVE_FILE_COUNT_MAX > 1 exists for later. #define UI_GAME_MENU_SAVE_SLOT 0 -static void uiGameMenuSave(void) { - if(!saveIsAvailable()) { - uiTextboxMainSetText("Can't save - no save device found."); - return; - } - - errorret_t ret = saveWrite(UI_GAME_MENU_SAVE_SLOT); - if(errorIsNotOk(ret)) { +static void uiGameMenuSaveComplete(errorret_t result, void *user) { + if(errorIsNotOk(result)) { // Generously sized - stringFormat asserts (crashes) rather than // truncating if the message doesn't fit, so this must comfortably fit // the longest platform save-error message plus this prefix. char_t msg[256]; stringFormat( - msg, sizeof(msg), "Save failed: %s", ret.state->message + msg, sizeof(msg), "Save failed: %s", result.state->message ); - errorCatch(ret); + errorCatch(result); uiTextboxMainSetText(msg); return; } @@ -52,6 +46,16 @@ static void uiGameMenuSave(void) { uiTextboxMainSetText("Game saved."); } +static void uiGameMenuSave(void) { + if(!saveIsAvailable()) { + uiTextboxMainSetText("Can't save - no save device found."); + return; + } + if(saveIsBusy()) return;// A save/load dialog (e.g. on PSP) is already up. + + saveWrite(UI_GAME_MENU_SAVE_SLOT, uiGameMenuSaveComplete, NULL); +} + uigamemenu_t UI_GAME_MENU; void uiGameMenuSelected( diff --git a/src/duskpsp/save/saveplatform.h b/src/duskpsp/save/saveplatform.h index 44db5a7f..c63c5d50 100644 --- a/src/duskpsp/save/saveplatform.h +++ b/src/duskpsp/save/saveplatform.h @@ -16,15 +16,22 @@ typedef savestreampsp_t saveplatformstream_t; #define saveDisposePlatform saveDisposePSP #define saveDeletePlatform saveDeletePSP -#define saveStreamOpenReadPlatform(stream, slot) \ - saveStreamOpenReadPSP(&(stream)->platform, &(stream)->found, slot) -#define saveStreamOpenWritePlatform(stream, slot) \ - saveStreamOpenWritePSP(&(stream)->platform, slot) -#define saveStreamClosePlatform(stream) \ - saveStreamClosePSP(&(stream)->platform) #define saveStreamReadBytesPlatform(stream, buf, len) \ saveStreamReadBytesPSP(&(stream)->platform, buf, len) #define saveStreamWriteBytesPlatform(stream, buf, len) \ saveStreamWriteBytesPSP(&(stream)->platform, buf, len) #define saveStreamSeekPlatform(stream, pos) \ saveStreamSeekPSP(&(stream)->platform, pos) + +// Save/load go entirely through the native sceUtilitySavedata dialog +// (savePSPBeginSave/Load), which spans multiple frames - these bypass +// save.c's normal synchronous open/write-fields/close flow above (that's +// still used internally, just against an in-memory buffer, from within +// savePSPBeginSave/Load themselves) and are what save.c's saveWrite()/ +// saveLoad() actually call on this platform. +#define saveAsyncWritePlatform(slot, onComplete, user) \ + savePSPBeginSave(slot, onComplete, user) +#define saveAsyncLoadPlatform(slot, onComplete, user) \ + savePSPBeginLoad(slot, onComplete, user) +#define saveIsBusyPlatform() savePSPIsBusy() +#define savePlatformUpdate() savePSPUpdate() diff --git a/src/duskpsp/save/savepsp.c b/src/duskpsp/save/savepsp.c index ddec2d9f..d1367d85 100644 --- a/src/duskpsp/save/savepsp.c +++ b/src/duskpsp/save/savepsp.c @@ -6,10 +6,31 @@ */ #include "save/save.h" +#include "save/savepsp.h" +#include "save/savestream.h" +#include "system/systempsp.h" +#include "util/memory.h" +#include "util/string.h" +#include "assert/assert.h" -void savePSPEnsureBaseDirs(void) { - sceIoMkdir(SAVE_PSP_BASE_DIR, 0777); - sceIoMkdir(SAVE_PSP_SAVEDATA_DIR, 0777); +static void savePSPParamCommonInit(SceUtilitySavedataParam *param) { + memoryZero(param, sizeof(SceUtilitySavedataParam)); + param->base.size = sizeof(SceUtilitySavedataParam); + param->base.language = systemPSPGetLanguage(); + param->base.buttonSwap = systemPSPGetCrossButtonSetting(); + param->base.graphicsThread = 17; + param->base.accessThread = 19; + param->base.fontThread = 18; + param->base.soundThread = 16; + + stringCopy(param->gameName, SAVE_PSP_GAME_NAME, sizeof(param->gameName)); + stringCopy(param->fileName, SAVE_PSP_FILE_NAME, sizeof(param->fileName)); +} + +static void savePSPSaveNameForSlot( + char_t *out, const size_t max, const uint8_t slot +) { + stringFormat(out, max, "%02u", (uint32_t)slot); } errorret_t saveInitPSP(void) { @@ -17,8 +38,6 @@ errorret_t saveInitPSP(void) { if(sceIoGetstat(SAVE_PSP_ROOT, &stat) < 0) { errorThrow("No memory stick detected"); } - - savePSPEnsureBaseDirs(); errorOk(); } @@ -26,62 +45,11 @@ errorret_t saveDisposePSP(void) { errorOk(); } -errorret_t saveLoadPSP(const uint8_t slot, savefile_t *file) { - char_t path[SAVE_PSP_PATH_MAX]; - snprintf(path, SAVE_PSP_PATH_MAX, SAVE_PSP_FILE_FORMAT, - SAVE_PSP_TITLE_ID, (uint32_t)slot - ); - - SceUID fd = sceIoOpen(path, PSP_O_RDONLY, 0); - if(fd < 0) { - file->exists = false; - errorOk(); - } - - int32_t read = sceIoRead(fd, file, sizeof(savefile_t)); - sceIoClose(fd); - - if(read != (int32_t)sizeof(savefile_t)) { - file->exists = false; - errorThrow("Failed to read save data for slot %u", (uint32_t)slot); - } - - file->exists = true; - errorOk(); -} - -errorret_t saveWritePSP(const uint8_t slot, const savefile_t *file) { - savePSPEnsureBaseDirs(); - char_t dir[SAVE_PSP_PATH_MAX]; - snprintf(dir, SAVE_PSP_PATH_MAX, SAVE_PSP_DIR_FORMAT, - SAVE_PSP_TITLE_ID, (uint32_t)slot - ); - sceIoMkdir(dir, 0777); - - char_t path[SAVE_PSP_PATH_MAX]; - snprintf(path, SAVE_PSP_PATH_MAX, SAVE_PSP_FILE_FORMAT, - SAVE_PSP_TITLE_ID, (uint32_t)slot - ); - - SceUID fd = sceIoOpen(path, PSP_O_WRONLY | PSP_O_CREAT | PSP_O_TRUNC, 0777); - if(fd < 0) { - errorThrow("Failed to open save file for writing: slot %u", (uint32_t)slot); - } - - int32_t written = sceIoWrite(fd, file, sizeof(savefile_t)); - sceIoClose(fd); - - if(written != (int32_t)sizeof(savefile_t)) { - errorThrow("Failed to write save data for slot %u", (uint32_t)slot); - } - - errorOk(); -} - errorret_t saveDeletePSP(const uint8_t slot) { char_t path[SAVE_PSP_PATH_MAX]; - snprintf(path, SAVE_PSP_PATH_MAX, SAVE_PSP_FILE_FORMAT, - SAVE_PSP_TITLE_ID, (uint32_t)slot + stringFormat( + path, sizeof(path), SAVE_PSP_FILE_FORMAT, SAVE_PSP_GAME_NAME, + (uint32_t)slot ); int32_t result = sceIoRemove(path); @@ -89,5 +57,234 @@ errorret_t saveDeletePSP(const uint8_t slot) { errorThrow("Failed to delete save file for slot %u", (uint32_t)slot); } + char_t dir[SAVE_PSP_PATH_MAX]; + stringFormat( + dir, sizeof(dir), "ms0:/PSP/SAVEDATA/%s%02u", SAVE_PSP_GAME_NAME, + (uint32_t)slot + ); + char_t sfoPath[SAVE_PSP_PATH_MAX]; + stringFormat(sfoPath, sizeof(sfoPath), "%s/PARAM.SFO", dir); + // Best-effort - PARAM.SFO/the directory itself may not exist (e.g. this + // slot was written by the old raw-file format, pre-dating this dialog- + // based rewrite) or the directory may still contain other entries. + sceIoRemove(sfoPath); + sceIoRmdir(dir); + + errorOk(); +} + +void savePSPBeginSave( + const uint8_t slot, savecallback_t onComplete, void *user +) { + assertNotNull(onComplete, "onComplete cannot be NULL"); + assertTrue(SAVE.platform.op == SAVE_PSP_OP_NONE, "Save already in progress"); + + savefile_t *file = &SAVE.files[slot]; + + // Serialize into the buffer synchronously (plain memory writes, same + // header/version/CRC framing as every other platform) before the dialog + // ever starts - only the actual commit-to-storage step needs to wait on + // the dialog. + savestream_t stream; + memoryZero(&stream, sizeof(savestream_t)); + stream.platform.buffer = SAVE.platform.dataBuffer; + stream.platform.bufferSize = sizeof(SAVE.platform.dataBuffer); + + errorret_t ret = saveFileWrite(&stream, file); + if(errorIsOk(ret)) ret = saveStreamFinalizeWriteImpl(&stream); + if(errorIsNotOk(ret)) { + onComplete(ret, user); + return; + } + SAVE.platform.dataLength = stream.platform.length; + + SceUtilitySavedataParam *param = &SAVE.platform.param; + savePSPParamCommonInit(param); + // AUTOSAVE rather than SAVE - SAVE shows a "save to this data?" confirm + // screen even for a slot with no existing data, which isn't the UX we + // want for a menu-triggered "Save" action (that confirmation already + // happened when the player chose to save). AUTOSAVE writes silently + // (just a brief "saving" icon flash) while still generating the same + // PARAM.SFO/title/description as any other mode. + param->mode = PSP_UTILITY_SAVEDATA_AUTOSAVE; + param->overwrite = 1; + savePSPSaveNameForSlot(param->saveName, sizeof(param->saveName), slot); + + param->dataBuf = SAVE.platform.dataBuffer; + param->dataBufSize = sizeof(SAVE.platform.dataBuffer); + param->dataSize = SAVE.platform.dataLength; + + // No ICON0/PIC1/SND0 art exists in this project yet, so these are left + // zeroed (bufSize 0) - the utility treats that as "no icon/background/ + // sound" rather than an error. title/savedataTitle/detail are still + // fully functional and are what actually populates PARAM.SFO and the + // save browser entry. + stringCopy(param->sfoParam.title, "Dusk", sizeof(param->sfoParam.title)); + stringCopy( + param->sfoParam.savedataTitle, file->playerName, + sizeof(param->sfoParam.savedataTitle) + ); + stringCopy( + param->sfoParam.detail, "Dusk save file.", sizeof(param->sfoParam.detail) + ); + + int32_t initRet = sceUtilitySavedataInitStart(param); + if(initRet < 0) { + onComplete(errorThrowImpl( + &SAVE.errorState, ERROR_NOT_OK, __FILE__, __func__, __LINE__, + "Failed to start save dialog: 0x%08X", initRet + ), user); + return; + } + + SAVE.platform.op = SAVE_PSP_OP_SAVE; + SAVE.platform.slot = slot; + SAVE.platform.onComplete = onComplete; + SAVE.platform.onCompleteUser = user; +} + +void savePSPBeginLoad( + const uint8_t slot, savecallback_t onComplete, void *user +) { + assertNotNull(onComplete, "onComplete cannot be NULL"); + assertTrue(SAVE.platform.op == SAVE_PSP_OP_NONE, "Save already in progress"); + + char_t path[SAVE_PSP_PATH_MAX]; + stringFormat( + path, sizeof(path), SAVE_PSP_FILE_FORMAT, SAVE_PSP_GAME_NAME, + (uint32_t)slot + ); + + SceIoStat stat; + if(sceIoGetstat(path, &stat) < 0) { + // No save data for this slot yet - not an error (matches every other + // platform's "nothing to load yet" behavior), and deliberately skips + // showing the dialog at all rather than surfacing an empty "no data" + // native screen for a slot the player has never saved to. + onComplete(errorOkImpl(), user); + return; + } + + SceUtilitySavedataParam *param = &SAVE.platform.param; + savePSPParamCommonInit(param); + param->mode = PSP_UTILITY_SAVEDATA_AUTOLOAD;// See savePSPBeginSave(). + savePSPSaveNameForSlot(param->saveName, sizeof(param->saveName), slot); + + param->dataBuf = SAVE.platform.dataBuffer; + param->dataBufSize = sizeof(SAVE.platform.dataBuffer); + + int32_t initRet = sceUtilitySavedataInitStart(param); + if(initRet < 0) { + onComplete(errorThrowImpl( + &SAVE.errorState, ERROR_NOT_OK, __FILE__, __func__, __LINE__, + "Failed to start load dialog: 0x%08X", initRet + ), user); + return; + } + + SAVE.platform.op = SAVE_PSP_OP_LOAD; + SAVE.platform.slot = slot; + SAVE.platform.onComplete = onComplete; + SAVE.platform.onCompleteUser = user; +} + +bool_t savePSPIsBusy(void) { + return SAVE.platform.op != SAVE_PSP_OP_NONE; +} + +errorret_t savePSPUpdate(void) { + if(SAVE.platform.op == SAVE_PSP_OP_NONE) errorOk(); + + int32_t status = sceUtilitySavedataGetStatus(); + switch(status) { + case PSP_UTILITY_DIALOG_INIT: + break; + + // NOTE: unlike the netconf dialog, this does not replicate Dusk's own + // GL state (blend/cull/depth + texture/color) before calling Update(). + // A prior fix for exactly that class of bug was documented for the + // network dialog, but no longer exists in the current codebase to + // copy from - if the save dialog's own text/icons don't render + // correctly on real hardware (PPSSPP won't reproduce this - it doesn't + // model pspGL's deferred state application), that state-priming + // pattern is the fix to reach for. See the network dialog's git + // history / the project's PSP dialog memory notes for the exact + // technique (state flags + a forced flush via a degenerate triangle + // draw). + case PSP_UTILITY_DIALOG_VISIBLE: + // sceUtilitySavedataUpdate() is void, unlike sceUtilityNetconfUpdate() + // - nothing to check here, GetStatus() next frame reflects any + // resulting state change. + sceUtilitySavedataUpdate(1); + break; + + case PSP_UTILITY_DIALOG_QUIT: + // The save/load operation itself has already finished (successfully + // or not) - this just starts tearing the dialog down. The actual + // result is read once that teardown settles, below - don't call + // ShutdownStart more than once while waiting for it to. + if(!SAVE.platform.shuttingDown) { + SAVE.platform.shuttingDown = true; + sceUtilitySavedataShutdownStart(); + } + break; + + // Confirmed under PPSSPP: status settles straight from QUIT to NONE, + // without FINISHED ever being separately observed in between - so + // both are treated identically here as "torn down, read the result", + // and it's shuttingDown (not which of these two codes we saw) that + // distinguishes that from a genuine disappearance. + case PSP_UTILITY_DIALOG_FINISHED: + case PSP_UTILITY_DIALOG_NONE: { + savepspop_t op = SAVE.platform.op; + uint8_t slot = SAVE.platform.slot; + savecallback_t cb = SAVE.platform.onComplete; + void *user = SAVE.platform.onCompleteUser; + bool_t reachedQuit = SAVE.platform.shuttingDown; + SAVE.platform.op = SAVE_PSP_OP_NONE; + SAVE.platform.shuttingDown = false; + + if(!reachedQuit) { + cb(errorThrowImpl( + &SAVE.errorState, ERROR_NOT_OK, __FILE__, __func__, __LINE__, + "Save dialog disappeared without a result" + ), user); + break; + } + + int32_t result = SAVE.platform.param.base.result; + if(result != 0) { + SAVE.available = false; + cb(errorThrowImpl( + &SAVE.errorState, ERROR_NOT_OK, __FILE__, __func__, __LINE__, + "Save dialog failed: 0x%08X", result + ), user); + break; + } + SAVE.available = true; + + if(op == SAVE_PSP_OP_LOAD) { + savefile_t *file = &SAVE.files[slot]; + savestream_t stream; + memoryZero(&stream, sizeof(savestream_t)); + stream.platform.buffer = SAVE.platform.dataBuffer; + stream.platform.bufferSize = sizeof(SAVE.platform.dataBuffer); + stream.platform.length = SAVE.platform.param.dataSize; + + errorret_t ret = saveFileLoad(&stream, file); + if(errorIsOk(ret)) ret = saveStreamVerifyChecksumImpl(&stream, slot); + file->exists = errorIsOk(ret); + cb(ret, user); + } else { + SAVE.files[slot].exists = true; + cb(errorOkImpl(), user); + } + break; + } + + default: + errorThrow("Unknown savedata dialog status: %d", status); + } + errorOk(); } diff --git a/src/duskpsp/save/savepsp.h b/src/duskpsp/save/savepsp.h index 24baf37e..5779ae52 100644 --- a/src/duskpsp/save/savepsp.h +++ b/src/duskpsp/save/savepsp.h @@ -9,29 +9,50 @@ #include "error/error.h" #include "save/savefile.h" #include +#include #define SAVE_PSP_PATH_MAX 256 #define SAVE_PSP_ROOT "ms0:/" -#define SAVE_PSP_BASE_DIR "ms0:/PSP" -#define SAVE_PSP_SAVEDATA_DIR "ms0:/PSP/SAVEDATA" -#define SAVE_PSP_FILE_FORMAT "ms0:/PSP/SAVEDATA/%s%02u/save.dat" -#define SAVE_PSP_DIR_FORMAT "ms0:/PSP/SAVEDATA/%s%02u" +#define SAVE_PSP_FILE_NAME "save.bin" +#define SAVE_PSP_FILE_FORMAT "ms0:/PSP/SAVEDATA/%s%02u/" SAVE_PSP_FILE_NAME +#define SAVE_PSP_DATA_BUFFER_SIZE 4096 -#ifndef SAVE_PSP_TITLE_ID - #define SAVE_PSP_TITLE_ID "DUSK00001" +#ifndef SAVE_PSP_GAME_NAME + #define SAVE_PSP_GAME_NAME "DUSK00001" #endif +typedef enum { + SAVE_PSP_OP_NONE, + SAVE_PSP_OP_SAVE, + SAVE_PSP_OP_LOAD +} savepspop_t; + typedef struct { - uint8_t unused; + SceUtilitySavedataParam param; + // Raw buffer sceUtilitySavedata reads/writes the whole save into/from - + // populated by our own savestream_t serialization (see savestreampsp.h) + // before a save starts, and deserialized from after a load finishes. + uint8_t dataBuffer[SAVE_PSP_DATA_BUFFER_SIZE] __attribute__((aligned(64))); + size_t dataLength; + + savepspop_t op; + // True once sceUtilitySavedataShutdownStart() has been requested (dialog + // status PSP_UTILITY_DIALOG_QUIT seen) - distinguishes a normal "torn + // down after finishing" NONE/FINISHED from a genuinely unexpected one + // seen before ever reaching QUIT. Some implementations (confirmed on + // PPSSPP) settle straight to NONE after shutdown without a separately + // observable FINISHED step in between. + bool_t shuttingDown; + uint8_t slot; + savecallback_t onComplete; + void *onCompleteUser; } savepsp_t; /** * Initializes the save system on PSP. Confirms the memory stick is * actually reachable (sceIoGetstat on SAVE_PSP_ROOT) rather than assuming - * so, since raw sceIo calls otherwise only fail once something tries to - * touch the filesystem - and ensures the PSP/SAVEDATA directory tree - * exists (SAVE_PSP_BASE_DIR then SAVE_PSP_SAVEDATA_DIR, since sceIoMkdir - * only creates one level at a time). + * so, since the savedata dialog otherwise only reports failure once a + * save/load is actually attempted. * * @return An error code if no memory stick is reachable. */ @@ -45,25 +66,7 @@ errorret_t saveInitPSP(void); errorret_t saveDisposePSP(void); /** - * Loads a save file from PSP save data for the given slot. - * - * @param slot The save slot index. - * @param file Output save file data. - * @return An error code if the load fails. - */ -errorret_t saveLoadPSP(const uint8_t slot, savefile_t *file); - -/** - * Writes a save file to PSP save data for the given slot. - * - * @param slot The save slot index. - * @param file Save file data to write. - * @return An error code if the write fails. - */ -errorret_t saveWritePSP(const uint8_t slot, const savefile_t *file); - -/** - * Deletes the save file for the given slot from PSP save data. + * Deletes the save data folder for the given slot from the memory stick. * * @param slot The save slot index. * @return An error code if the delete fails. @@ -71,10 +74,54 @@ errorret_t saveWritePSP(const uint8_t slot, const savefile_t *file); errorret_t saveDeletePSP(const uint8_t slot); /** - * Ensures SAVE_PSP_BASE_DIR and SAVE_PSP_SAVEDATA_DIR both exist, creating - * whichever are missing. sceIoMkdir only creates one directory level at a - * time, so this must run before creating any per-slot save directory - * beneath SAVE_PSP_SAVEDATA_DIR. Safe to call repeatedly - an - * already-exists result is not an error. + * Starts a save via the native sceUtilitySavedata dialog (mode AUTOSAVE - + * writes silently with just a brief icon flash, no confirm screen, since + * SAVE mode shows one even for a slot with no existing data - but + * PARAM.SFO/title/description are generated identically regardless of + * mode, and the OS handles the save browser entry either way) for the + * given slot. Serializes SAVE.files[slot] into SAVE.platform.dataBuffer + * first, synchronously, then kicks off the dialog and returns - completion + * is reported later via onComplete, driven by savePSPUpdate() each frame. + * If no save data exists yet for this slot, sceUtilitySavedataInitStart() + * creates it. + * + * @param slot The save slot index. + * @param onComplete Callback invoked once the dialog finishes. + * @param user User data passed through to onComplete. */ -void savePSPEnsureBaseDirs(void); +void savePSPBeginSave( + const uint8_t slot, savecallback_t onComplete, void *user +); + +/** + * Starts a load via the native sceUtilitySavedata dialog (mode AUTOLOAD - + * see savePSPBeginSave() for why not the plain LOAD mode) for the given + * slot, unless a quick sceIoGetstat check finds no save data for this slot + * yet - in which case onComplete is invoked immediately with + * SAVE.files[slot].exists left false, matching the other platforms' + * "no file yet" semantics, and no dialog is shown at all. + * + * @param slot The save slot index. + * @param onComplete Callback invoked once the dialog (or immediate + * not-found short-circuit) finishes. + * @param user User data passed through to onComplete. + */ +void savePSPBeginLoad( + const uint8_t slot, savecallback_t onComplete, void *user +); + +/** + * Pumps the in-progress save/load dialog one step, if any - must be called + * every engine frame (see saveUpdate()). No-op if no dialog is active. + * + * @return An error code indicating success or failure. + */ +errorret_t savePSPUpdate(void); + +/** + * True while a save/load dialog is in progress (see savePSPBeginSave()/ + * savePSPBeginLoad()). + * + * @return True if a save/load dialog is currently open. + */ +bool_t savePSPIsBusy(void); diff --git a/src/duskpsp/save/savestreampsp.c b/src/duskpsp/save/savestreampsp.c index 6b89e753..623683fd 100644 --- a/src/duskpsp/save/savestreampsp.c +++ b/src/duskpsp/save/savestreampsp.c @@ -7,72 +7,35 @@ #include "save/save.h" #include "save/savestreampsp.h" - -errorret_t saveStreamOpenReadPSP( - savestreampsp_t *p, bool_t *found, const uint8_t slot -) { - char_t path[SAVE_PSP_PATH_MAX]; - snprintf(path, SAVE_PSP_PATH_MAX, SAVE_PSP_FILE_FORMAT, - SAVE_PSP_TITLE_ID, (uint32_t)slot - ); - - p->fd = sceIoOpen(path, PSP_O_RDONLY, 0); - *found = (p->fd >= 0); - errorOk(); -} - -errorret_t saveStreamOpenWritePSP(savestreampsp_t *p, const uint8_t slot) { - savePSPEnsureBaseDirs(); - char_t dir[SAVE_PSP_PATH_MAX]; - snprintf(dir, SAVE_PSP_PATH_MAX, SAVE_PSP_DIR_FORMAT, - SAVE_PSP_TITLE_ID, (uint32_t)slot - ); - sceIoMkdir(dir, 0777); - - char_t path[SAVE_PSP_PATH_MAX]; - snprintf(path, SAVE_PSP_PATH_MAX, SAVE_PSP_FILE_FORMAT, - SAVE_PSP_TITLE_ID, (uint32_t)slot - ); - - p->fd = sceIoOpen(path, PSP_O_WRONLY | PSP_O_CREAT | PSP_O_TRUNC, 0777); - if(p->fd < 0) { - errorThrow( - "Failed to open PSP save file for writing: slot %u", (uint32_t)slot - ); - } - errorOk(); -} - -void saveStreamClosePSP(savestreampsp_t *p) { - if(p->fd >= 0) { - sceIoClose(p->fd); - p->fd = -1; - } -} +#include "util/memory.h" errorret_t saveStreamReadBytesPSP( savestreampsp_t *p, void *buf, const size_t len ) { - int32_t read = sceIoRead(p->fd, buf, (SceSize)len); - if(read != (int32_t)len) { - errorThrow("Unexpected end of PSP save file"); + if(p->position + len > p->length) { + errorThrow("Save stream read exceeds buffer length"); } + memoryCopy(buf, p->buffer + p->position, len); + p->position += len; errorOk(); } errorret_t saveStreamWriteBytesPSP( savestreampsp_t *p, const void *buf, const size_t len ) { - int32_t written = sceIoWrite(p->fd, buf, (SceSize)len); - if(written != (int32_t)len) { - errorThrow("Failed to write PSP save data"); + if(p->position + len > p->bufferSize) { + errorThrow("Save stream write exceeds buffer size"); } + memoryCopy(p->buffer + p->position, buf, len); + p->position += len; + if(p->position > p->length) p->length = p->position; errorOk(); } errorret_t saveStreamSeekPSP(savestreampsp_t *p, const size_t pos) { - if(sceIoLseek(p->fd, (SceOff)pos, PSP_SEEK_SET) < 0) { - errorThrow("Failed to seek in PSP save file"); + if(pos > p->bufferSize) { + errorThrow("Save stream seek out of range"); } + p->position = pos; errorOk(); } diff --git a/src/duskpsp/save/savestreampsp.h b/src/duskpsp/save/savestreampsp.h index 2574833b..b14fc266 100644 --- a/src/duskpsp/save/savestreampsp.h +++ b/src/duskpsp/save/savestreampsp.h @@ -7,71 +7,49 @@ #pragma once #include "error/error.h" -#include #include +// Backed by SAVE.platform.dataBuffer (see savepsp.h) rather than owning its +// own memory - the buffer has to outlive a single saveFileWrite()/Load() +// call, since the actual save/load dialog it's handed to only completes +// several frames later. typedef struct { - SceUID fd; + uint8_t *buffer; + size_t bufferSize; + size_t position; + size_t length; } savestreampsp_t; /** - * Opens a PSP save data file for reading. - * - * @param p Stream to initialize. - * @param found Set to true if the file exists, false if it does not. - * @param slot Save slot index. - * @return An error if the open fails for a reason other than missing file. - */ -errorret_t saveStreamOpenReadPSP( - savestreampsp_t *p, bool_t *found, const uint8_t slot -); - -/** - * Opens a PSP save data file for writing, creating or truncating it. - * Creates the save data directory if it does not already exist. - * - * @param p Stream to initialize. - * @param slot Save slot index. - * @return An error if the file cannot be opened for writing. - */ -errorret_t saveStreamOpenWritePSP(savestreampsp_t *p, const uint8_t slot); - -/** - * Closes the file descriptor held by the stream. - * - * @param p Stream to close. - */ -void saveStreamClosePSP(savestreampsp_t *p); - -/** - * Reads len bytes from the stream into buf. + * Copies len bytes from the buffer at the current position into buf. * * @param p Active stream. * @param buf Destination buffer. * @param len Number of bytes to read. - * @return An error if fewer than len bytes are available. + * @return An error if the read would exceed the populated data length. */ errorret_t saveStreamReadBytesPSP( savestreampsp_t *p, void *buf, const size_t len ); /** - * Writes len bytes from buf into the stream. + * Copies len bytes from buf into the buffer at the current position, + * growing p->length if this write extends past it. * * @param p Active stream. * @param buf Source buffer. * @param len Number of bytes to write. - * @return An error if the write fails. + * @return An error if the write would exceed bufferSize. */ errorret_t saveStreamWriteBytesPSP( savestreampsp_t *p, const void *buf, const size_t len ); /** - * Seeks to an absolute byte position within the stream. + * Sets the current read/write position within the buffer. * * @param p Active stream. - * @param pos Target byte offset from the start of the file. - * @return An error if the seek fails. + * @param pos Target byte offset from the start of the buffer. + * @return An error if pos is out of range. */ errorret_t saveStreamSeekPSP(savestreampsp_t *p, const size_t pos);