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.
This commit is contained in:
2026-08-04 09:54:45 -05:00
parent 24badd06a5
commit 9abf8101da
11 changed files with 499 additions and 223 deletions
+83 -36
View File
@@ -9,29 +9,50 @@
#include "error/error.h"
#include "save/savefile.h"
#include <pspiofilemgr.h>
#include <psputility.h>
#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);