Files
AnyPS5/docs/dev/debt/libSceSaveData.native.md
T
Yunus Shaikh 0d0abe09c1 fix(libSceSaveData): start the event delivery thread at the first registration
sceSaveDataInitialize3 started the callback delivery thread for every
title that initializes save data, though nearly none register a
callback, and could throw there if scePthreadCreate failed. The thread
now starts at the first successful sceSaveDataRegisterEventCallback of
an initialization, under the calling, g_slots_mutex and callbacks.mutex
locks that registration already takes, so a title that never registers
gets no thread and sceSaveDataInitialize3 is again only the count.
A replacing registration, one from inside a callback and one after an
unregister reuse the running thread; a refused one starts nothing.

The last sceSaveDataTerminate still takes the worker out under
g_slots_mutex and joins it outside the lock, and only when one was
started, so a session that registers between that terminate's detach
and its join starts its own thread.

The tests run two sessions with no registration, and on Linux count the
process's threads to check that Initialize3 and a refused registration
start none, that the first registration starts one and later ones reuse
it, and that the final terminate joins it.
2026-10-30 19:39:06 +00:00

25 KiB

libSceSaveData.native technical debt

Silent stubs

  • sceSaveDataAbort, sceSaveDataClearProgress (libSceSaveData.native) return SAVE_DATA_OK after initialization without cancelling operations or clearing any progress. The current host operations run synchronously and no backup progress is tracked; sceSaveDataClearProgress does not reset sceSaveDataGetConvertProgress. Their console signatures, errors and behavior are unverified.

  • sceSaveDataPrepare, sceSaveDataCommit (libSceSaveData.native) return success without looking at their arguments; what preparing and committing a mounted save do on a console is unknown. The same six titles call both; this counts tests of the result in the three instructions after each call. sceSaveDataPrepare has 11 call sites: PPSA01494, PPSA30839 and PPSA31162 test none of them there, PPSA03984 takes an error path when it is negative, PPSA34502 checks it at one of two sites and PPSA36591 at two of four. sceSaveDataCommit has 14: PPSA01494 and PPSA30839 test none, PPSA03984 and PPSA31162 check its sign, PPSA34502 checks it at all five sites and PPSA36591 at two of four. A failure code from either would change the save flow of the titles that check it, while the stubs keep all six on the success path.

Unknown function info

Functional

  • sceSaveDataDirNameSearch, sceSaveDataDirNameSearch2 (libSceSaveData.native) - only the running title's saves are kept, so a search with another title's title_id reports zero hits, as shadPS4 and KytyPS5 do when that title has no save directory; what a console returns for another title's saves is not verified. The running title's ID is the titleId in its param.json

  • sceSaveDataDirNameSearch2 (libSceSaveData.native) runs the same search as sceSaveDataDirNameSearch with the same condition and result structures, as SharpEmu does, its only source; what distinguishes it on the console is unknown. One title (PPSA35669, in a plugin) disassembled for this entry has one call site (0x2eda71); it clears 56 bytes for the result structure first (the size of the structure used here), puts a value returned by a call with the argument 0x8000 at offset 8 and 0x400 at offset 0x10, and afterwards branches on a negative result and reads the 32-bit field at offset 0, skipping its loop when it is 0. That agrees with the result structure of sceSaveDataDirNameSearch. The condition structure was not read.

  • sceSaveDataMount3 (libSceSaveData.native) - a failed mount (BUSY, EXISTS, NOT_FOUND, MOUNT_FULL) leaves the result as it was, as shadPS4 does; not checked on hardware. Animal Well (PPSA22520) depends on it: it checks for its save with a mount it never unmounts, then saves with a second mount of the same directory, ignores its BUSY and keeps using the first mount point for sceSaveDataPrepare, its file and sceSaveDataUmount2. In PPSA05347 the function that calls it at 0x235fbb5 zeroes its 0x40-byte result before the call and, for a negative result other than 0x809f0008 and 0x809f000f (BUSY included), logs the code and returns it without recording the mount; no abort follows.

  • sceSaveDataMount3 (libSceSaveData.native) - an unterminated or empty directory name, ., .., or a name containing /, \ or : returns SAVE_DATA_ERROR_PARAMETER, which the SDK 12.000 reference lists for an invalid parameter, and leaves the result as it was, as KytyPS5 does. The SDK lets an application use only letters, digits, -, . and @ in a name, and KytyPS5 also accepts _ (valid_path_component); which other names the console's mount rejects is not checked on hardware

  • sceSaveDataBackup (libSceSaveData.native) - an unterminated directory name returns SAVE_DATA_ERROR_PARAMETER to prevent unbounded copy; which other directory names the console's backup rejects is unverified on hardware.

  • sceSaveDataCheckBackupData, sceSaveDataRestoreBackupData, sceSaveDataUmountWithBackup (libSceSaveData.native) - not measured on a PS5: the block layouts, error checks and behaviour follow shadPS4's PS4 binding (check, restore, unmount with backup), and the tests check that binding, not the console. sceSaveDataBackup still copies nothing, so the check and the restore see only backups that sceSaveDataUmountWithBackup made.

  • sceSaveDataUmountWithBackup (libSceSaveData.native) - an unknown mount point returns SAVE_DATA_ERROR_NOT_MOUNTED, as sceSaveDataUmount2 does, where shadPS4 returns SAVE_DATA_ERROR_NOT_FOUND. The backup is copied before the call returns, so SAVE_DATA_ERROR_BACKUP_BUSY never occurs, and the UMOUNT_BACKUP event is queued at once, as fpPS4 does; a failed copy sets its error to SAVE_DATA_ERROR_INTERNAL, as shadPS4's backup thread does.

  • sceSaveDataCheckBackupData, sceSaveDataRestoreBackupData (libSceSaveData.native) - saves keep no icons, so the check never fills icon, as shadPS4 does without icon0.png, and another title's titleId returns SAVE_DATA_ERROR_NOT_FOUND because only the running title's saves are kept. Backups live in _sd_backup/<dir name>, outside the save, and sceSaveDataDelete removes one with its save, as shadPS4's backup inside the save directory goes with it. On a PS4 Pro (firmware 10.71) another title's titleId also returns SAVE_DATA_ERROR_NOT_FOUND, and sceSaveDataDelete drops the backup too: after it the check and the restore return SAVE_DATA_ERROR_NOT_FOUND (run).

  • sceSaveDataCheckBackupData (libSceSaveData.native) - a param buffer that isn't zeroed is not refused here. On a PS4 Pro (firmware 10.71, the system libSceSaveData.sprx, SDK 0x05050001) a 1,328-byte param filled with 0xAA gave SAVE_DATA_ERROR_PARAMETER on every call, also with a backup present, while a zeroed or null one gave 0 (run). Which bytes the console checks wasn't measured, so the check fills param whatever it held. Not measured on a PS5.

  • sceSaveDataRestoreBackupData (libSceSaveData.native) - a crash during a restore is not recovered at the next start, and what the console leaves is not measured. A failed restore leaves the live save, its .param and its blocks file as they were: the backup is copied to _sd_restore_tmp/<dir name> and checked first, and a failed move puts back what was already moved; only if that fails too does the old save stay in _sd_restore_old/<dir name>. A crash during the copy leaves the live save untouched. One during the moves can leave parts of the live save in _sd_restore_old/<dir name> and the copy in _sd_restore_tmp/<dir name> or _sd; while the save's directory is missing from _sd, its restore returns SAVE_DATA_ERROR_INTERNAL and a creating mount makes an empty save. The next restore of the save first removes both directories.

  • sceSaveDataRegisterEventCallback, sceSaveDataUnregisterEventCallback (libSceSaveData.native) - not measured on a console: the behaviour follows shadPS4 #5226, and PS5's signatures are assumed to be PS4's (void (*)(const SaveDataEvent*, void*) and a userdata pointer). Unknown: which thread delivers, its priority and affinity, when it runs relative to the call, whether unregistering drops completions not yet delivered (here it does), and what a terminate from inside the callback does (here SAVE_DATA_ERROR_BUSY, as in shadPS4). Here the first successful sceSaveDataRegisterEventCallback after sceSaveDataInitialize3 starts the delivering thread with scePthreadCreate, the last sceSaveDataTerminate joins it if one was started, and each backup completion (type 1 or 2) is queued for sceSaveDataGetEventResult before it goes to the callback registered at that moment.

  • sceSaveDataUmountWithBackup (libSceSaveData.native) - a crash during a backup is not recovered at the next start either. A failed backup keeps the previous one in _sd_backup/<dir name>. A crash between its two moves leaves the previous backup in _sd_backup_old/<dir name>, where the check and the restore do not find it (SAVE_DATA_ERROR_NOT_FOUND); the next backup of the save removes it, and any partial copy in _sd_backup_tmp/<dir name>, before it copies.

  • sceSaveDataRestoreBackupData, sceSaveDataUmountWithBackup (libSceSaveData.native) - on Windows nothing shows that a backup's or a restore's renames survive a crash or power loss. Every staged file is flushed before the first rename (FlushFileBuffers on Windows, fsync on POSIX, as libc does), and on POSIX the directories on both sides of each rename are flushed after it; Windows cannot flush directory entries, so there it rests on NTFS journaling.

  • sceSaveDataBackup, sceSaveDataConvert, sceSaveDataDelete, sceSaveDataDirNameSearch, sceSaveDataDirNameSearchPs4, sceSaveDataGetEventResult, sceSaveDataGetMountInfo, sceSaveDataGetParam, sceSaveDataLoadIcon, sceSaveDataMount, sceSaveDataMount3, sceSaveDataSaveIcon, sceSaveDataSetParam, sceSaveDataUmount2 (libSceSaveData.native) - null pointer arguments return SAVE_DATA_ERROR_PARAMETER instead of throwing C++ exceptions, an implementation assumption matching guest error expectations not checked on hardware; unmounted targets return SAVE_DATA_ERROR_NOT_MOUNTED, and parameter type or buffer size violations return SAVE_DATA_ERROR_PARAMETER after slot lookup

  • sceSaveDataSyncSaveDataMemory (libSceSaveData.native) - validates library initialization and parameters; since in-memory saves flush synchronously on write, sync confirms existence and readiness of the memory blob

  • sceSaveDataGetMountedSaveDataCount (libSceSaveData.native) - signature and behavior are unverified; the function throws. SharpEmu and RPS5E write the count through a second argument and return a status; their console accuracy is unverified.