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.
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.