diff --git a/core/libs/SceTypes.hpp b/core/libs/SceTypes.hpp index 237e9166d93..c5e339bee96 100644 --- a/core/libs/SceTypes.hpp +++ b/core/libs/SceTypes.hpp @@ -1288,7 +1288,7 @@ struct NetCtlIfStat { std::uint32_t reserved[8]; }; -using NetCtlCallback = void (*)(int, void*); +using NetCtlCallback = void (APS5_VABI *)(int, void*); struct HttpEpoll; using HttpEpollHandle = HttpEpoll*; diff --git a/core/libs/prx/libSceNetCtl/Export.cpp b/core/libs/prx/libSceNetCtl/Export.cpp index 25417585901..463c9642aef 100644 --- a/core/libs/prx/libSceNetCtl/Export.cpp +++ b/core/libs/prx/libSceNetCtl/Export.cpp @@ -38,6 +38,8 @@ static constexpr int SCE_NET_CTL_ERROR_INVALID_ADDR = static_cast(0x8041210 static constexpr int SCE_NET_CTL_ERROR_NOT_CONNECTED = static_cast(0x80412108); static constexpr int NET_CTL_STATE_DISCONNECTED = 0; static constexpr int NET_CTL_STATE_IPOBTAINED = 3; +static constexpr int NET_CTL_EVENT_DISCONNECTED = 1; +static constexpr int NET_CTL_EVENT_IPOBTAINED = 3; static constexpr int NET_CTL_INFO_DEVICE = 1; static constexpr int NET_CTL_INFO_ETHER_ADDR = 2; static constexpr int NET_CTL_INFO_MTU = 3; @@ -61,16 +63,22 @@ static constexpr std::uint8_t NET_CTL_ETHER_ADDR[6] = {0x02, 0x50, 0x53, 0x35, 0 static constexpr std::size_t NET_CTL_IPV4_ADDR_STR_LEN = 16; static constexpr int MAX_CALLBACKS = 8; +struct CallbackSlot { + NetCtlCallback func = nullptr; + void* arg = nullptr; + int lastEvent = 0; +}; + static std::mutex g_callbackLock; -static NetCtlCallback g_callbacks[MAX_CALLBACKS] = {}; -static NetCtlCallback g_callbacksV6[MAX_CALLBACKS] = {}; +static CallbackSlot g_callbacks[MAX_CALLBACKS] = {}; +static CallbackSlot g_callbacksV6[MAX_CALLBACKS] = {}; -static int RegisterCallback(NetCtlCallback (&callbacks)[MAX_CALLBACKS], NetCtlCallback func, int* cid) { +static int RegisterCallback(CallbackSlot (&callbacks)[MAX_CALLBACKS], NetCtlCallback func, void* arg, int* cid) { if (!func || !cid) return SCE_NET_CTL_ERROR_INVALID_ADDR; std::lock_guard lock(g_callbackLock); for (int index = 0; index < MAX_CALLBACKS; ++index) { - if (!callbacks[index]) { - callbacks[index] = func; + if (!callbacks[index].func) { + callbacks[index] = {func, arg, 0}; *cid = index; return 0; } @@ -78,13 +86,21 @@ static int RegisterCallback(NetCtlCallback (&callbacks)[MAX_CALLBACKS], NetCtlCa return SCE_NET_CTL_ERROR_CALLBACK_MAX; } -static int UnregisterCallback(NetCtlCallback (&callbacks)[MAX_CALLBACKS], int cid) { +static int UnregisterCallback(CallbackSlot (&callbacks)[MAX_CALLBACKS], int cid) { if (cid < 0 || cid >= MAX_CALLBACKS) return SCE_NET_CTL_ERROR_INVALID_ID; std::lock_guard lock(g_callbackLock); - callbacks[cid] = nullptr; + callbacks[cid] = {}; return 0; } +static bool HasIpv4Callback() { + std::lock_guard lock(g_callbackLock); + for (const auto& slot : g_callbacks) { + if (slot.func) return true; + } + return false; +} + static bool HostHasAddress(int family) { #ifdef _WIN32 static const int socket_status = [] { @@ -324,6 +340,19 @@ static void HostTraffic(std::uint64_t& sent, std::uint64_t& received) { extern "C" { int APS5_VABI sceNetCtlCheckCallback(void) { + if (!HasIpv4Callback()) return 0; + const int event = HostHasAddress(AF_UNSPEC) ? NET_CTL_EVENT_IPOBTAINED : NET_CTL_EVENT_DISCONNECTED; + CallbackSlot due[MAX_CALLBACKS]; + int count = 0; + { + std::lock_guard lock(g_callbackLock); + for (auto& slot : g_callbacks) { + if (!slot.func || slot.lastEvent == event) continue; + slot.lastEvent = event; + due[count++] = slot; + } + } + for (int index = 0; index < count; ++index) due[index].func(event, due[index].arg); return 0; } @@ -390,14 +419,17 @@ int APS5_VABI sceNetCtlInit(void) { } int APS5_VABI sceNetCtlRegisterCallback(NetCtlCallback func, void* arg, int* cid) { - return RegisterCallback(g_callbacks, func, cid); + return RegisterCallback(g_callbacks, func, arg, cid); } int APS5_VABI sceNetCtlRegisterCallbackV6(NetCtlCallback func, void* arg, int* cid) { - return RegisterCallback(g_callbacksV6, func, cid); + return RegisterCallback(g_callbacksV6, func, arg, cid); } void APS5_VABI sceNetCtlTerm(void) { + std::lock_guard lock(g_callbackLock); + for (auto& slot : g_callbacks) slot = {}; + for (auto& slot : g_callbacksV6) slot = {}; } int APS5_VABI sceNetCtlUnregisterCallback(int cid) { diff --git a/core/libs/tests/GuestNetCtl.cpp b/core/libs/tests/GuestNetCtl.cpp index 5730ecd1a93..0e5a4f6d26d 100644 --- a/core/libs/tests/GuestNetCtl.cpp +++ b/core/libs/tests/GuestNetCtl.cpp @@ -19,9 +19,15 @@ #include extern "C" { +int APS5_VABI sceNetCtlCheckCallback(void); int APS5_VABI sceNetCtlGetInfo(int, NetCtlInfo*); int APS5_VABI sceNetCtlGetNatInfo(NetCtlNatInfo*); int APS5_VABI sceNetCtlGetState(int*); +int APS5_VABI sceNetCtlRegisterCallback(NetCtlCallback, void*, int*); +int APS5_VABI sceNetCtlRegisterCallbackV6(NetCtlCallback, void*, int*); +void APS5_VABI sceNetCtlTerm(void); +int APS5_VABI sceNetCtlUnregisterCallback(int); +int APS5_VABI sceNetCtlUnregisterCallbackV6(int); } namespace { @@ -32,9 +38,100 @@ constexpr int kErrInvalidCode = static_cast(0x80412106); constexpr int kErrInvalidAddr = static_cast(0x80412107); constexpr int kErrNotConnected = static_cast(0x80412108); constexpr int kStateIpObtained = 3; +constexpr int kEventDisconnected = 1; +constexpr int kEventIpObtained = 3; constexpr std::uint8_t kFill = 0xa5; constexpr std::size_t kAddressLength = 16; +struct Delivery { + int calls = 0; + int event = 0; + void* arg = nullptr; +}; + +Delivery g_first; +Delivery g_second; +Delivery g_v6; +Delivery g_self; +int g_selfUnregister = -1; + +void APS5_VABI RecordFirst(int event, void* arg) { + ++g_first.calls; + g_first.event = event; + g_first.arg = arg; +} + +void APS5_VABI RecordSecond(int event, void* arg) { + ++g_second.calls; + g_second.event = event; + g_second.arg = arg; +} + +void APS5_VABI RecordV6(int event, void* arg) { + ++g_v6.calls; + g_v6.event = event; + g_v6.arg = arg; +} + +void APS5_VABI UnregisterSelf(int event, void* arg) { + ++g_self.calls; + g_self.event = event; + g_self.arg = arg; + g_selfUnregister = sceNetCtlUnregisterCallback(*static_cast(arg)); +} + +void CheckCallbacks() { + int state = -1; + Require(sceNetCtlGetState(&state) == 0); + const int event = state == kStateIpObtained ? kEventIpObtained : kEventDisconnected; + int firstProbe = 0; + int secondProbe = 0; + int v6Probe = 0; + + int v6 = -1; + Require(sceNetCtlRegisterCallbackV6(RecordV6, &v6Probe, &v6) == 0 && v6 == 0); + + int first = -1; + Require(sceNetCtlRegisterCallback(RecordFirst, &firstProbe, &first) == 0 && first == 0); + Require(sceNetCtlCheckCallback() == 0); + Require(g_first.calls == 1 && g_first.event == event && g_first.arg == &firstProbe); + Require(sceNetCtlCheckCallback() == 0); + Require(g_first.calls == 1); + + int second = -1; + Require(sceNetCtlRegisterCallback(RecordSecond, &secondProbe, &second) == 0 && second == 1); + Require(sceNetCtlCheckCallback() == 0); + Require(g_first.calls == 1); + Require(g_second.calls == 1 && g_second.event == event && g_second.arg == &secondProbe); + + Require(sceNetCtlUnregisterCallback(second) == 0); + Require(sceNetCtlRegisterCallback(RecordSecond, &secondProbe, &second) == 0 && second == 1); + Require(sceNetCtlUnregisterCallback(second) == 0); + Require(sceNetCtlCheckCallback() == 0); + Require(g_first.calls == 1 && g_second.calls == 1); + + int self = -1; + Require(sceNetCtlRegisterCallback(UnregisterSelf, &self, &self) == 0 && self == 1); + Require(sceNetCtlCheckCallback() == 0); + Require(g_self.calls == 1 && g_self.event == event && g_self.arg == &self && g_selfUnregister == 0); + Require(g_first.calls == 1 && g_second.calls == 1); + Require(sceNetCtlCheckCallback() == 0); + Require(g_self.calls == 1); + + Require(sceNetCtlRegisterCallback(RecordSecond, &secondProbe, &second) == 0 && second == 1); + sceNetCtlTerm(); + Require(sceNetCtlCheckCallback() == 0); + Require(g_first.calls == 1 && g_second.calls == 1); + + Require(sceNetCtlRegisterCallbackV6(RecordV6, &v6Probe, &v6) == 0 && v6 == 0); + Require(sceNetCtlRegisterCallback(RecordFirst, &secondProbe, &first) == 0 && first == 0); + Require(sceNetCtlCheckCallback() == 0); + Require(g_first.calls == 2 && g_first.event == event && g_first.arg == &secondProbe); + Require(g_second.calls == 1 && g_self.calls == 1 && g_v6.calls == 0); + Require(sceNetCtlUnregisterCallback(first) == 0); + Require(sceNetCtlUnregisterCallbackV6(v6) == 0); +} + NetCtlInfo Query(int code) { NetCtlInfo info; std::memset(&info, kFill, sizeof(info)); @@ -90,6 +187,8 @@ int main() { WSADATA data{}; Require(WSAStartup(MAKEWORD(2, 2), &data) == 0); #endif + CheckCallbacks(); + NetCtlInfo info{}; Require(sceNetCtlGetInfo(0, &info) == kErrInvalidCode); Require(sceNetCtlGetInfo(22, &info) == kErrInvalidCode); diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index 3e0b95e6cb6..464bcd0e323 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -124,6 +124,8 @@ static void Require(bool condition) { if (!condition) std::abort(); } +static void APS5_VABI IgnoreNetCtlEvent(int, void*) {} + static bool Failed(std::int64_t result, int error) { return result == static_cast(0x80410100u | static_cast(error)) && *sceNetErrnoLoc() == error; } @@ -1428,20 +1430,19 @@ int main() { Require(second.tx_bytes >= first.tx_bytes && second.rx_bytes >= first.rx_bytes); } { - const auto dummyCallback = [](int, void*) {}; int cid = -1; Require(sceNetCtlRegisterCallbackV6(nullptr, nullptr, &cid) == static_cast(0x80412107)); - Require(sceNetCtlRegisterCallbackV6(dummyCallback, nullptr, nullptr) == static_cast(0x80412107)); + Require(sceNetCtlRegisterCallbackV6(IgnoreNetCtlEvent, nullptr, nullptr) == static_cast(0x80412107)); int cids[8] = {}; for (int i = 0; i < 8; ++i) { - Require(sceNetCtlRegisterCallbackV6(dummyCallback, nullptr, &cids[i]) == 0); + Require(sceNetCtlRegisterCallbackV6(IgnoreNetCtlEvent, nullptr, &cids[i]) == 0); Require(cids[i] == i); } - Require(sceNetCtlRegisterCallbackV6(dummyCallback, nullptr, &cid) == static_cast(0x80412103)); + Require(sceNetCtlRegisterCallbackV6(IgnoreNetCtlEvent, nullptr, &cid) == static_cast(0x80412103)); Require(sceNetCtlUnregisterCallbackV6(-1) == static_cast(0x80412105)); Require(sceNetCtlUnregisterCallbackV6(8) == static_cast(0x80412105)); Require(sceNetCtlUnregisterCallbackV6(3) == 0); - Require(sceNetCtlRegisterCallbackV6(dummyCallback, nullptr, &cid) == 0); + Require(sceNetCtlRegisterCallbackV6(IgnoreNetCtlEvent, nullptr, &cid) == 0); Require(cid == 3); for (int i = 0; i < 8; ++i) { Require(sceNetCtlUnregisterCallbackV6(i) == 0); diff --git a/docs/dev/debt/libSceNetCtl.md b/docs/dev/debt/libSceNetCtl.md index f7effa839ad..519f8470dcf 100644 --- a/docs/dev/debt/libSceNetCtl.md +++ b/docs/dev/debt/libSceNetCtl.md @@ -7,3 +7,7 @@ ### Unknown function info - [sceNetCtlGetInfo](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - answers from the host interface that holds an IPv4 address, preferring the one that carries the default route: its address, netmask, gateway, MTU and first two IPv4 DNS servers. The device is reported as wired, the link as connected, the IP configuration as DHCP and the HTTP proxy as off, whatever the host uses, and the Ethernet address is the one `sceNetGetMacAddress` reports. A gateway or DNS server the interface does not have is reported as `0.0.0.0`; what a console reports then is unknown. The DHCP host name (12), the PPPoE user name (13) and the HTTP proxy server (20) are empty strings, as that configuration sets none of them; the HTTP proxy port (21) is 0; the Wi-Fi codes (5-10) throw + +### Functional + +- [sceNetCtlCheckCallback](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - the state comes from the host interfaces, as in `sceNetCtlGetState`: `IPOBTAINED` (3) while the host has a non-loopback IPv4 or IPv6 address, `DISCONNECTED` (1) otherwise; the event values follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/network/net_ctl_codes.h#L27-L29), and no other event is delivered. Each callback registered with `sceNetCtlRegisterCallback` is called on the checking thread with its registration argument when the state differs from the last one it received, so a new registration receives the current state at the next check. This edge-triggered model follows [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/9e41d8320a2774b303fbad344c2347e8d0292e83/src/libs/network.cpp#L3648-L3661); [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/network/net_ctl_obj.cpp#L47-L57) delivers the state on every check instead, and neither is measured on hardware. KytyPS5 calls the callbacks under its lock; here they are called after the lock is released, from a copy of their slots, so a `sceNetCtlUnregisterCallback` or `sceNetCtlTerm` on another thread that returns while a check is in flight does not stop that check from calling the callback once more with its old argument. Callbacks registered with `sceNetCtlRegisterCallbackV6` keep their argument but receive no event, because no source says which call delivers them