Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 61 additions & 7 deletions core/libs/prx/libSceNpManager/Export.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
#include <cstddef>
#include "SceTypes.hpp"
#include "prx/libc/include/General.hpp"
#include "prx/libSceUserService/UserService.hpp"
#include <array>
#include <atomic>
#include <mutex>
#include <unordered_set>
Expand All @@ -13,6 +15,7 @@ static constexpr int SCE_NP_ERROR_USER_NOT_FOUND = static_cast<int>(0x80550007);
static constexpr int SCE_NP_ERROR_CALLBACK_ALREADY_REGISTERED = static_cast<int>(0x80550008);
static constexpr int SCE_NP_ERROR_CALLBACK_NOT_REGISTERED = static_cast<int>(0x80550009);
static constexpr int SCE_NP_ERROR_REQUEST_NOT_FOUND = static_cast<int>(0x80550014);
static constexpr int SCE_NP_ERROR_CALLBACK_MAX = static_cast<int>(0x8055001D);
static constexpr uint32_t NP_STATE_SIGNED_OUT = 1;
static constexpr int NP_POLL_ASYNC_FINISHED = 0;
static constexpr uint32_t NP_REACHABILITY_STATE_UNAVAILABLE = 0;
Expand All @@ -32,6 +35,27 @@ int AddRequest() {
liveRequests.insert(reqId);
return reqId;
}

using NpStateCallbackA = void (APS5_VABI *)(int userId, uint32_t state, void* userdata);
using NpStateCallback = void (APS5_VABI *)(int userId, uint32_t state, NpId* npId, void* userdata);

constexpr int NP_STATE_CALLBACK_MAX = 8;

struct StateCallbackA {
NpStateCallbackA callback = nullptr;
void* userdata = nullptr;
bool pending = false;
};

struct StateCallback {
NpStateCallback callback = nullptr;
void* userdata = nullptr;
bool pending = false;
};

std::mutex stateMutex;
std::array<StateCallbackA, NP_STATE_CALLBACK_MAX> stateCallbacksA;
StateCallback stateCallback;
}

extern "C" {
Expand All @@ -42,6 +66,19 @@ int APS5_VABI sceNpAbortRequest(int req_id) {
}

int APS5_VABI sceNpCheckCallback(void) {
std::array<StateCallbackA, NP_STATE_CALLBACK_MAX> callbacksA;
StateCallback legacy;
{
std::lock_guard lock(stateMutex);
callbacksA = stateCallbacksA;
legacy = stateCallback;
for (auto& entry : stateCallbacksA) entry.pending = false;
stateCallback.pending = false;
}
if (legacy.pending) legacy.callback(USER_SERVICE_INITIAL_USER_ID, NP_STATE_SIGNED_OUT, nullptr, legacy.userdata);
for (const auto& entry : callbacksA) {
if (entry.pending) entry.callback(USER_SERVICE_INITIAL_USER_ID, NP_STATE_SIGNED_OUT, entry.userdata);
}
return 0;
}

Expand Down Expand Up @@ -172,9 +209,11 @@ int APS5_VABI sceNpRegisterPremiumEventCallback(void* callback, void* userdata)
return 0;
}

int APS5_VABI sceNpRegisterStateCallback(void* callback, void* userdata) {
(void)userdata;
int APS5_VABI sceNpRegisterStateCallback(NpStateCallback callback, void* userdata) {
if (!callback) return SCE_NP_ERROR_INVALID_ARGUMENT;
std::lock_guard lock(stateMutex);
if (stateCallback.callback) return SCE_NP_ERROR_CALLBACK_ALREADY_REGISTERED;
stateCallback = {callback, userdata, true};
return 0;
}

Expand Down Expand Up @@ -208,6 +247,9 @@ int APS5_VABI sceNpSetTimeout(int reqId, int32_t resolveRetry, uint32_t resolveT
}

int APS5_VABI sceNpUnregisterStateCallback(void) {
std::lock_guard lock(stateMutex);
if (!stateCallback.callback) return SCE_NP_ERROR_CALLBACK_NOT_REGISTERED;
stateCallback = {};
return 0;
}

Expand All @@ -223,14 +265,26 @@ int APS5_VABI sceNpNotifyPremiumFeature(const void* param) {
return 0;
}

int APS5_VABI sceNpRegisterStateCallbackA(void* callback, void* userdata) {
(void)userdata;
int APS5_VABI sceNpRegisterStateCallbackA(NpStateCallbackA callback, void* userdata) {
if (!callback) return SCE_NP_ERROR_INVALID_ARGUMENT;
return 1;
std::lock_guard lock(stateMutex);
for (const auto& entry : stateCallbacksA) {
if (entry.callback == callback) return SCE_NP_ERROR_CALLBACK_ALREADY_REGISTERED;
}
for (int index = 0; index < NP_STATE_CALLBACK_MAX; ++index) {
if (stateCallbacksA[index].callback) continue;
stateCallbacksA[index] = {callback, userdata, true};
return index + 1;
}
return SCE_NP_ERROR_CALLBACK_MAX;
}

int APS5_VABI sceNpUnregisterStateCallbackA(int callback_id) {
(void)callback_id;
int APS5_VABI sceNpUnregisterStateCallbackA(int callbackId) {
if (callbackId < 1 || callbackId > NP_STATE_CALLBACK_MAX) return SCE_NP_ERROR_INVALID_ARGUMENT;
std::lock_guard lock(stateMutex);
auto& entry = stateCallbacksA[callbackId - 1];
if (!entry.callback) return SCE_NP_ERROR_CALLBACK_NOT_REGISTERED;
entry = {};
return 0;
}

Expand Down
141 changes: 141 additions & 0 deletions core/libs/tests/GuestNpManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,22 @@ int APS5_VABI sceNpSetTimeout(int reqId, std::int32_t resolveRetry, std::uint32_
std::uint32_t connTimeout, std::uint32_t sendTimeout, std::uint32_t recvTimeout);
}

extern "C" {
int APS5_VABI sceNpCheckCallback(void);
int APS5_VABI sceNpRegisterStateCallback(void* callback, void* userdata);
int APS5_VABI sceNpUnregisterStateCallback(void);
int APS5_VABI sceNpRegisterStateCallbackA(void* callback, void* userdata);
int APS5_VABI sceNpUnregisterStateCallbackA(int callbackId);
int APS5_VABI sceNpRegisterNpReachabilityStateCallback(void* callback, void* userdata);
int APS5_VABI sceNpUnregisterNpReachabilityStateCallback(void);
}

namespace {

constexpr int InvalidArgument = static_cast<int>(0x80550003u);
constexpr int SignedOut = static_cast<int>(0x80550006u);
constexpr int AlreadyRegistered = static_cast<int>(0x80550008u);
constexpr int NotRegistered = static_cast<int>(0x80550009u);
constexpr int RequestNotFound = static_cast<int>(0x80550014u);

void Require(bool condition, const char* message) {
Expand All @@ -30,6 +42,133 @@ void Require(bool condition, const char* message) {
}
}

struct StateCall {
int calls;
int userId;
std::uint32_t state;
NpId* npId;
void* userdata;
};

constexpr int InitialUserId = 0x10000000;
constexpr std::uint32_t StateSignedOut = 1;
StateCall stateCallsA[9];
StateCall legacyStateCall;

template <int Index>
void APS5_VABI StateCallbackA(int userId, std::uint32_t state, void* userdata) {
stateCallsA[Index] = {stateCallsA[Index].calls + 1, userId, state, nullptr, userdata};
}

void APS5_VABI LegacyStateCallback(int userId, std::uint32_t state, NpId* npId, void* userdata) {
legacyStateCall = {legacyStateCall.calls + 1, userId, state, npId, userdata};
}

bool SignedOutOnce(const StateCall& call, void* userdata) {
return call.calls == 1 && call.userId == InitialUserId && call.state == StateSignedOut &&
call.npId == nullptr && call.userdata == userdata;
}

void APS5_VABI AbortingReachabilityCallback(int, std::uint32_t, void*) {
std::fprintf(stderr, "NpManager: the reachability callback was called\n");
std::abort();
}

struct SelfUnregister {
int id;
int calls;
int result;
};

void APS5_VABI SelfUnregisteringCallback(int, std::uint32_t, void* userdata) {
auto* context = static_cast<SelfUnregister*>(userdata);
++context->calls;
context->result = sceNpUnregisterStateCallbackA(context->id);
}

void CheckStateCallbacksA() {
constexpr int CallbackMax = static_cast<int>(0x8055001Du);
void* const callbacks[] = {
reinterpret_cast<void*>(&StateCallbackA<0>), reinterpret_cast<void*>(&StateCallbackA<1>),
reinterpret_cast<void*>(&StateCallbackA<2>), reinterpret_cast<void*>(&StateCallbackA<3>),
reinterpret_cast<void*>(&StateCallbackA<4>), reinterpret_cast<void*>(&StateCallbackA<5>),
reinterpret_cast<void*>(&StateCallbackA<6>), reinterpret_cast<void*>(&StateCallbackA<7>),
reinterpret_cast<void*>(&StateCallbackA<8>),
};
int userdata[9]{};
Require(sceNpRegisterStateCallbackA(nullptr, &userdata[0]) == InvalidArgument, "null state callback");
int ids[8]{};
ids[0] = sceNpRegisterStateCallbackA(callbacks[0], &userdata[0]);
Require(ids[0] >= 1 && ids[0] <= 8, "state callback id outside 1..8");
Require(sceNpRegisterStateCallbackA(callbacks[0], &userdata[1]) == AlreadyRegistered, "duplicate state callback");
Require(stateCallsA[0].calls == 0, "registering calls the state callback");
Require(sceNpRegisterNpReachabilityStateCallback(reinterpret_cast<void*>(&AbortingReachabilityCallback), nullptr) == 0,
"registering the reachability callback");
Require(sceNpCheckCallback() == 0, "sceNpCheckCallback failed");
Require(SignedOutOnce(stateCallsA[0], &userdata[0]), "sceNpCheckCallback must deliver SIGNED_OUT once");
Require(sceNpUnregisterNpReachabilityStateCallback() == 0, "unregistering the reachability callback");
Require(sceNpCheckCallback() == 0, "second sceNpCheckCallback failed");
Require(stateCallsA[0].calls == 1, "the second sceNpCheckCallback delivered the state again");
for (int index = 1; index < 8; ++index) {
ids[index] = sceNpRegisterStateCallbackA(callbacks[index], &userdata[index]);
Require(ids[index] >= 1 && ids[index] <= 8, "state callback id outside 1..8");
for (int other = 0; other < index; ++other) {
Require(ids[index] != ids[other], "two state callbacks got the same id");
}
}
Require(sceNpRegisterStateCallbackA(callbacks[8], &userdata[8]) == CallbackMax, "ninth state callback");
Require(sceNpCheckCallback() == 0, "sceNpCheckCallback failed with eight callbacks");
for (int index = 0; index < 8; ++index) {
Require(SignedOutOnce(stateCallsA[index], &userdata[index]), "each callback gets SIGNED_OUT once");
}
Require(stateCallsA[8].calls == 0, "a rejected callback was called");
Require(sceNpUnregisterStateCallbackA(0) == InvalidArgument, "callback id 0");
Require(sceNpUnregisterStateCallbackA(9) == InvalidArgument, "callback id 9");
Require(sceNpUnregisterStateCallbackA(ids[3]) == 0, "unregistering one state callback");
for (int index = 0; index < 8; ++index) {
if (index == 3) continue;
Require(sceNpRegisterStateCallbackA(callbacks[index], &userdata[index]) == AlreadyRegistered,
"unregistering one id removed another callback");
}
Require(sceNpRegisterStateCallbackA(callbacks[3], &userdata[3]) == ids[3], "registering again takes the freed slot");
Require(sceNpCheckCallback() == 0, "sceNpCheckCallback failed after registering again");
for (int index = 0; index < 8; ++index) {
Require(stateCallsA[index].calls == (index == 3 ? 2 : 1), "only the registration made again gets SIGNED_OUT");
}
for (int index = 0; index < 8; ++index) {
Require(sceNpUnregisterStateCallbackA(ids[index]) == 0, "unregistering a state callback");
}
Require(sceNpUnregisterStateCallbackA(ids[0]) == NotRegistered, "unregistering a state callback twice");
const int removed = sceNpRegisterStateCallbackA(callbacks[8], &userdata[8]);
Require(removed >= 1 && removed <= 8, "a freed slot is reused");
Require(sceNpUnregisterStateCallbackA(removed) == 0, "unregistering before sceNpCheckCallback");
Require(sceNpCheckCallback() == 0, "sceNpCheckCallback failed with no callbacks");
Require(stateCallsA[8].calls == 0, "an unregistered callback was called");
SelfUnregister self{};
self.id = sceNpRegisterStateCallbackA(reinterpret_cast<void*>(&SelfUnregisteringCallback), &self);
Require(self.id >= 1 && self.id <= 8, "state callback id outside 1..8");
Require(sceNpCheckCallback() == 0, "sceNpCheckCallback failed with a callback that unregisters itself");
Require(self.calls == 1 && self.result == 0, "a state callback must be able to unregister itself");
Require(sceNpUnregisterStateCallbackA(self.id) == NotRegistered, "the callback did not unregister itself");
}

void CheckLegacyStateCallback() {
int userdata = 0;
NpId sentinel{};
auto* callback = reinterpret_cast<void*>(&LegacyStateCallback);
Require(sceNpUnregisterStateCallback() == NotRegistered, "unregistering with no legacy callback");
Require(sceNpRegisterStateCallback(nullptr, &userdata) == InvalidArgument, "null legacy callback");
Require(sceNpRegisterStateCallback(callback, &userdata) == 0, "registering the legacy callback");
Require(sceNpRegisterStateCallback(callback, nullptr) == AlreadyRegistered, "duplicate legacy callback");
legacyStateCall.npId = &sentinel;
Require(sceNpCheckCallback() == 0, "sceNpCheckCallback failed with the legacy callback");
Require(SignedOutOnce(legacyStateCall, &userdata), "the legacy callback must get SIGNED_OUT with no NpId");
Require(sceNpCheckCallback() == 0, "second sceNpCheckCallback failed with the legacy callback");
Require(legacyStateCall.calls == 1, "the legacy callback got the state again");
Require(sceNpUnregisterStateCallback() == 0, "unregistering the legacy callback");
Require(sceNpUnregisterStateCallback() == NotRegistered, "unregistering the legacy callback twice");
}

void CheckRequestTimeouts() {
Require(sceNpSetTimeout(0x7fffffff, 2, 0, 0, 0, 0) == RequestNotFound, "never created request");
Require(sceNpSetTimeout(0x7fffffff, 0, 0, 0, 0, 0) == InvalidArgument, "arguments checked before the request");
Expand Down Expand Up @@ -59,6 +198,8 @@ void CheckRequestTimeouts() {
}

int main() {
CheckStateCallbacksA();
CheckLegacyStateCallback();
CheckRequestTimeouts();
NpId npId{};
std::memset(&npId, 0x5a, sizeof(npId));
Expand Down
1 change: 1 addition & 0 deletions docs/dev/debt/libSceNpManager.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@

- [NP reachability callbacks](../../../core/libs/prx/libSceNpManager/Export.cpp) retain one callback and its user data; duplicate registration and unregistering an absent callback return the errors used by [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/945dbc3cc3eee80ac3e053b438502ed936fa6bb2/src/core/libraries/np/np_manager.cpp). No callback is delivered because the existing NP model remains signed out with reachability unavailable.
- [sceNpCheckNpAvailability](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - the `(req_id, online_id)` signature and the errors follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/np/np_manager.cpp) with no user signed in to PSN: a null online id is `SCE_NP_ERROR_INVALID_ARGUMENT`, and any other online id matches no user, `SCE_NP_ERROR_USER_NOT_FOUND`; the request id is not checked, as in the other NpManager requests
- [NP state callbacks](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - the registration rules are [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_manager.cpp#L857-L901)'s: `sceNpRegisterStateCallbackA` keeps up to 8 callbacks and returns the slot plus 1, a null callback returns `INVALID_ARGUMENT`, a callback already registered `CALLBACK_ALREADY_REGISTERED` and a ninth one `CALLBACK_MAX` (0x8055001D, [np_error.h](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_error.h#L38)); `sceNpUnregisterStateCallbackA` returns `INVALID_ARGUMENT` for an id outside 1..8 and `CALLBACK_NOT_REGISTERED` for an empty slot; the legacy [sceNpRegisterStateCallback](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_manager.cpp#L981-L1005) keeps one callback with the same errors. Each registration gets `SIGNED_OUT` for the initial user (0x10000000) once, from the next `sceNpCheckCallback` on the calling thread. For the A form this follows [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/9e41d8320a2774b303fbad344c2347e8d0292e83/src/libs/network.cpp#L3959-L3974), whose [sceNpRegisterStateCallbackA](https://github.com/KytyPS5/KytyPS5/blob/9e41d8320a2774b303fbad344c2347e8d0292e83/src/libs/network.cpp#L4021-L4034) marks the registration pending. KytyPS5 never calls the legacy callback ([sceNpRegisterStateCallback](https://github.com/KytyPS5/KytyPS5/blob/9e41d8320a2774b303fbad344c2347e8d0292e83/src/libs/network.cpp#L4009-L4019) stores nothing); it is called as `(user, SIGNED_OUT, nullptr, userdata)`, as in shadPS4, which passes an NpId only for `SIGNED_IN` ([L823](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_manager.cpp#L823), [L938-L940](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_manager.cpp#L938-L940)). shadPS4 instead queues `SIGNED_OUT` from the user service login event and delivers it from `sceNpCheckCallback` ([DispatchPendingNpStateCallbacks](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_manager.cpp#L903-L962)); the reachability callback is not given an initial `UNAVAILABLE`, unlike that function ([L922-L961](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_manager.cpp#L922-L961)). A callback being delivered when another thread unregisters it is still called once. When and how often the console calls the state callbacks, and the errors, are not measured
Loading