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
+15
View File
@@ -14,3 +14,18 @@
#define saveDevicePlatformUpdate saveDevicePSPUpdate
#define saveDeviceCheckAvailabilityPlatform saveDevicePSPCheckAvailability
#define saveDevicePlatformDispose saveDevicePSPDispose
// The memory stick has no directory-listing UI convention to preserve like
// GameCube/Wii memory cards do, so settings + all save slots are combined
// into one compressed blob rather than kept as separate files - see
// SAVE_DEVICE_DATA_RAW in save/savedevice.h.
#define SAVE_DEVICE_DATA_RAW
#define saveDeviceDataWritePlatform saveDevicePSPDataWrite
#define saveDeviceDataReadPlatform saveDevicePSPDataRead
#ifndef SAVE_DEVICE_PSP_DIRECTORY_NAME
#define SAVE_DEVICE_PSP_DIRECTORY_NAME "ms0:/DUSK"
#endif
#ifndef SAVE_DEVICE_PSP_DATA_FILENAME
#define SAVE_DEVICE_PSP_DATA_FILENAME "save.dat"
#endif
+98
View File
@@ -8,8 +8,12 @@
#include "save/savedevice.h"
#include "save/savedevicepsp.h"
#include "assert/assert.h"
#include "util/memory.h"
#include "util/string.h"
#include <pspiofilemgr.h>
#define SAVE_DEVICE_PSP_PATH_MAX 64
errorret_t saveDevicePSPInit(savedevice_t *device) {
assertNotNull(device, "device cannot be null");
@@ -47,6 +51,100 @@ void saveDevicePSPCheckAvailability(savedevice_t *device) {
saveDeviceFireCallback(device);
}
static errorret_t saveDevicePSPGetDataPath(
char_t *dest,
const size_t destSize
) {
assertNotNull(dest, "dest cannot be null");
stringFormat(
dest, destSize, "%s/%s",
SAVE_DEVICE_PSP_DIRECTORY_NAME, SAVE_DEVICE_PSP_DATA_FILENAME
);
errorOk();
}
errorret_t saveDevicePSPDataWrite(
savedevice_t *device,
const uint8_t *buffer,
size_t size
) {
assertNotNull(device, "device cannot be null");
assertNotNull(buffer, "buffer cannot be null");
// Best-effort create - a fresh memory stick won't have this directory yet.
sceIoMkdir(SAVE_DEVICE_PSP_DIRECTORY_NAME, 0777);
char_t finalPath[SAVE_DEVICE_PSP_PATH_MAX];
errorChain(saveDevicePSPGetDataPath(finalPath, sizeof(finalPath)));
char_t tempPath[SAVE_DEVICE_PSP_PATH_MAX];
stringFormat(tempPath, sizeof(tempPath), "%s.tmp", finalPath);
SceUID fd = sceIoOpen(
tempPath, PSP_O_WRONLY | PSP_O_CREAT | PSP_O_TRUNC, 0777
);
if(fd < 0) errorThrow("Failed to open save data for writing: %s", tempPath);
int written = sceIoWrite(fd, buffer, (SceSize)size);
sceIoClose(fd);
if(written < 0 || (size_t)written != size) {
errorThrow("Failed to write save data: %s", tempPath);
}
// Atomic replace: the temp file is fully written before it ever takes the
// final name, so a crash/power-loss here can never leave a half-written
// save behind - at worst the previous save survives untouched.
sceIoRemove(finalPath);// Not an error if it doesn't exist yet.
if(sceIoRename(tempPath, finalPath) < 0) {
errorThrow("Failed to finalize save data: %s", finalPath);
}
errorOk();
}
errorret_t saveDevicePSPDataRead(
savedevice_t *device,
uint8_t **outBuffer,
size_t *outSize
) {
assertNotNull(device, "device cannot be null");
assertNotNull(outBuffer, "outBuffer cannot be null");
assertNotNull(outSize, "outSize cannot be null");
char_t path[SAVE_DEVICE_PSP_PATH_MAX];
errorChain(saveDevicePSPGetDataPath(path, sizeof(path)));
SceIoStat stat;
if(sceIoGetstat(path, &stat) < 0) {
// No save yet - not an error.
*outBuffer = NULL;
*outSize = 0;
errorOk();
}
size_t size = (size_t)stat.st_size;
uint8_t *buffer = memoryAllocate(size);
SceUID fd = sceIoOpen(path, PSP_O_RDONLY, 0);
if(fd < 0) {
memoryFree(buffer);
errorThrow("Failed to open save data for reading: %s", path);
}
int readBytes = sceIoRead(fd, buffer, (SceSize)size);
sceIoClose(fd);
if(readBytes < 0 || (size_t)readBytes != size) {
memoryFree(buffer);
errorThrow("Failed to read save data: %s", path);
}
*outBuffer = buffer;
*outSize = size;
errorOk();
}
errorret_t saveDevicePSPDispose(savedevice_t *device) {
errorOk();
}
+33
View File
@@ -27,6 +27,39 @@ typedef struct savedevice_s savedevice_t;
*/
errorret_t saveDevicePSPInit(savedevice_t *device);
/**
* Writes the combined save data blob out to the memory stick, replacing it
* atomically (write to a temp file, then swap it in) so a crash mid-write
* can never leave a half-written file behind.
*
* @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 saveDevicePSPDataWrite(
savedevice_t *device,
const uint8_t *buffer,
size_t size
);
/**
* Reads the combined save data blob in from the memory stick. If no save
* data exists yet, 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 memoryAllocate'd 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 saveDevicePSPDataRead(
savedevice_t *device,
uint8_t **outBuffer,
size_t *outSize
);
/**
* Updates the save device platform.
*