Add compressed combined save format for PSP and Dolphin

Memory sticks/cards don't suit N+1 separate save files the way Linux's
filesystem does, so add an opt-in SAVE_DEVICE_DATA_RAW mode: settings and
all save slots get serialized to JSON, concatenated, zlib-compressed, and
framed with a magic/version/checksum/generation header, then read/written
as a single blob through one combined platform hook instead of four.

- PSP and Dolphin's SD/NAND backends write via a temp file + atomic rename,
  so a crash mid-write can never leave a half-written save behind.
- GameCube memory cards have no rename or resize primitive, so they
  ping-pong between two fixed files instead, picking whichever is valid and
  has the higher generation counter on load.
- Linux is untouched (still four separate JSON files); saveslot.h/
  savesettings.h drop their now-unnecessary pack(1) now that nothing
  persists them as raw bytes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-17 21:28:56 -05:00
parent 092e259a06
commit 82ae2bce9d
14 changed files with 1125 additions and 17 deletions
@@ -17,6 +17,20 @@
#define SAVE_DEVICE_DOLPHIN_GAME_CODE "DUSK"
#endif
// Memory cards have no rename primitive and files can't be resized once
// created, so a single "current" file can't be replaced atomically the way
// the other backends do. Instead saveDeviceDolphinCardDataWrite() ping-pongs
// between these two fixed files - every store targets whichever one is NOT
// the currently valid+newest copy (by saveDeviceRawIsValid()'s generation
// counter), so a crash mid-write can only ever corrupt the copy nobody
// would load from; the other one stays intact as the fallback.
#ifndef SAVE_DEVICE_DOLPHIN_CARD_FILENAME_0
#define SAVE_DEVICE_DOLPHIN_CARD_FILENAME_0 "DUSKSAVE0"
#endif
#ifndef SAVE_DEVICE_DOLPHIN_CARD_FILENAME_1
#define SAVE_DEVICE_DOLPHIN_CARD_FILENAME_1 "DUSKSAVE1"
#endif
typedef struct {
// Set from this device's index into SAVE.devices - device 0 is
// CARD_SLOTA, device 1 is CARD_SLOTB.
@@ -82,3 +96,38 @@ bool_t saveDeviceDolphinCardHasFreeSpace(const int32_t channel);
* @return Error state if any.
*/
errorret_t saveDeviceDolphinCardDispose(savedevice_t *device);
/**
* Writes the combined save data blob out to whichever of the two fixed
* card files (see SAVE_DEVICE_DOLPHIN_CARD_FILENAME_0/1 above) is not
* currently the valid+newest copy - ping-pong in place of the atomic
* rename the other backends use, since the CARD API has neither a rename
* nor a way to resize an existing file.
*
* @param device The save device to write to.
* @param buffer The raw bytes to write.
* @param size The number of bytes to write.
* @return Error state if any.
*/
errorret_t saveDeviceDolphinCardDataWrite(
savedevice_t *device,
const uint8_t *buffer,
size_t size
);
/**
* Reads whichever of the two fixed card files is valid and has the higher
* generation counter. If neither file exists or validates, this is not an
* error - *outBuffer is set to NULL and *outSize to 0.
*
* @param device The save device to read from.
* @param outBuffer Receives a memoryAlign'd (32-byte) buffer the caller
* must free with memoryFree, or NULL if nothing has been saved yet.
* @param outSize Receives the number of bytes in *outBuffer.
* @return Error state if any.
*/
errorret_t saveDeviceDolphinCardDataRead(
savedevice_t *device,
uint8_t **outBuffer,
size_t *outSize
);