# Conflicts:
# docs/dev/debt/libSceSaveData.native.md
The pending declarations line keeps main's sceSaveDataGetProgress split and drops the three names this branch implements (sceSaveDataCheckBackupData, sceSaveDataRestoreBackupData, sceSaveDataUmountWithBackup).
JJenkx's #13324 gives isMountedDirectory an std::error_code parameter.
The merge passes one in checkBackupData and restoreBackupData and
returns SAVE_DATA_ERROR_INTERNAL on a host error, as main's callers do.
GuestSaveDataMountStatus.cpp: CheckBackups writes param.json where
ResolvePath_nid_no_patch puts /app0/sce_sys/param.json, not under the
working directory. #13324's CheckHostMountFailures now resolves the
first guest path from its mount-host-failures subdirectory, so /app0
maps there, and checkBackupData didn't find the title's param.json.
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.
With ANYPS5_FREE_SPACE=host, sceSaveDataGetMountInfo and the directory-name
search report a save's free blocks as the smaller of its own free blocks and
the 64 KiB blocks the save's volume still has. A full volume reports none
free, so a title that checks for room sees the same answer the create check
gives. Unset, the free blocks stay the save's own.
With ANYPS5_FREE_SPACE=host, creating a save first checks the free space of
the save directory's volume against the save's size, 16384 blocks when the
title passes none. A save that does not fit fails with NO_SPACE_FS and
reports the blocks missing in required_blocks, and no directory is made.
If the volume cannot be queried, the create keeps today's behaviour and one
line goes to stderr. Opening an existing save and the default fixed mode are
unchanged.
Every sceSaveDataMount3 call reads ANYPS5_FREE_SPACE and stops the title with
an error naming the variable when it is not fixed or host. Unset keeps
today's behaviour, so no save is created differently by default.
sceSaveDataRegisterEventCallback read g_initializations before it took
any lock. A registration that read a nonzero count could then store its
callback after the final sceSaveDataTerminate had set the count to zero
and cleared the callback, return OK with no delivery thread, and have
the next Initialize3 deliver events to a callback the terminate had
dropped.
Registration now checks the count and stores the callback under
g_slots_mutex, the lock the final terminate holds while it reaches zero
and detaches delivery. It takes the calling lock first, the order the
delivery thread already uses when a callback calls back into SaveData,
so a terminate from inside the callback is still BUSY and the terminate
joins the delivery thread outside both locks.
The new case races registration against the final terminate on two
guest threads for 200 iterations, holding the delivery thread in a
callback on every other one so a registration that passed its check
waits across the whole terminate. Each must end NOT_INITIALIZED, or OK
with the callback cleared, and a backup after Initialize3 must not reach
the raced callback.
sceSaveDataRegisterEventCallback and sceSaveDataUnregisterEventCallback
were parameterless declarations that threw. They now follow shadPS4
#5226: registering before initialization is NOT_INITIALIZED, a null
callback is PARAMETER, a second registration replaces the callback and
its userdata, and unregistering needs no initialization.
Every backup completion that queues an event for
sceSaveDataGetEventResult, type 2 from sceSaveDataBackup and type 1 from
sceSaveDataUmountWithBackup, is queued for polling first and then handed
to the callback registered at that moment. A library thread delivers it:
sceSaveDataInitialize3 starts it with scePthreadCreate, as
libSceUsbStorage starts its monitor, so the callback runs on a guest
thread, and the last sceSaveDataTerminate joins it. Unregistering waits
for a running callback and drops completions not yet delivered, and a
terminate from inside the callback returns BUSY instead of joining its
own thread, as shadPS4 does.
guest_savedata_event_callback covers the errors, both event types, the
delivering thread, unregistration, replacement, a terminate from inside
the callback and an idle terminate followed by a restart.
Before any copy or delete, the backup and the restore check the roots
they use (_sd, _sd_backup and their staging and old directories) and
every entry under them for the save: each must be a plain directory or
file, not a symbolic link, or on Windows a reparse point such as a
junction, and must stay under its root after weakly_canonical. Anything
else makes the restore return SAVE_DATA_ERROR_INTERNAL, and the backup
report it in its event, with nothing changed. sceSaveDataCheckBackupData
checks the backup the same way. The copies walk the tree themselves and
refuse a link instead of following it.
backupSave moved the previous backup to _sd_backup_old and then renamed
the new copy into place; when that rename failed, the save had no
backup until the next one. A failed rename now moves the previous
backup back into _sd_backup and removes the staged copy. Every staged
file is flushed before the swap (FlushFileBuffers on Windows, fsync on
POSIX, as libc does), and on POSIX the directories on both sides of
each rename are flushed too, all before the previous backup is removed.
sceSaveDataRestoreBackupData used to remove the live save's files and
copy the backup over them, so a copy that failed half way left neither.
It now copies the backup, its .param and its blocks file to
_sd_restore_tmp/<dir>, checks that the copy has every entry at its size,
and flushes it. Then it moves the live save, .param and blocks file to
_sd_restore_old/<dir>, moves the staged copy into place and removes the
old one. A failed move puts back everything already moved, so the live
save stays as it was. The staging roots sit next to _sd, not in it,
where sceSaveDataDirNameSearch would list them.
SaveDataSetFileStepHook_nid_no_patch sets a function that is called
before each copy and rename of the backup and restore paths with a step
name and the path written; when it returns false that step fails. It
does nothing unless a test sets it.
sceSaveDataCheckBackupData and sceSaveDataRestoreBackupData now read a
backup through one check that runs before anything in the live save
changes. A backup directory without a save inside, a missing .param, a
.param that is not exactly one SaveDataParam, or a blocks entry that is
not a file returns SAVE_DATA_ERROR_INTERNAL, whether or not the caller
asked for param. Before, the check threw on a wrong-sized .param, and
the restore removed the live save's files before finding the .param
missing.
When a save has no stored mtime, its parameters fall back to the save
directory's modification time. On Windows std::filesystem reads that
through the UCRT stat call, which converts through local time using TZ,
so with a TZ other than the Windows zone, including one libc applies
from the guest environment, the reported time was off by the
difference between the zones. The fallback now reads the time with
GetFileAttributesExW. guest_savedata_search checks it under TZ values
12 hours either side of UTC.
Co-Authored-By: Claude Opus 5.5 <[email protected]>