Skip to content
Closed
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
10 changes: 10 additions & 0 deletions core/libs/prx/libSceNet/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,14 @@ if(BUILD_TESTING)
set_tests_properties(guest_sce_net PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceNet>")
endif()
add_test_executable(guest_sce_net_connect_tests ${LIBS_INCLUDE_DIR}/tests/GuestSceNetConnect.cpp)
target_include_directories(guest_sce_net_connect_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(guest_sce_net_connect_tests PRIVATE libSceNet libc)
configure_windows_unwind(guest_sce_net_connect_tests)
add_test(NAME guest_sce_net_connect COMMAND guest_sce_net_connect_tests)
set_tests_properties(guest_sce_net_connect PROPERTIES TIMEOUT 30)
if(WIN32)
set_tests_properties(guest_sce_net_connect PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceNet>")
endif()
endif()
66 changes: 60 additions & 6 deletions core/libs/prx/libSceNet/Export.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ constexpr int NET_SOCK_RAW = 3;
constexpr int NET_SOL_SOCKET = 0xFFFF;
constexpr int NET_SO_SNDTIMEO = 0x1005;
constexpr int NET_SO_RCVTIMEO = 0x1006;
constexpr int NET_SO_ERROR = 0x1007;
constexpr int NET_SO_NBIO = 0x1200;
constexpr int NET_MSG_PEEK = 0x2;
constexpr int NET_MSG_TRUNC = 0x10;
Expand All @@ -90,8 +91,9 @@ bool initialize_sockets() {
return result == 0;
}
void close_socket(NativeSocket socket) { closesocket(socket); }
int native_error() {
switch (WSAGetLastError()) {
int native_error(int error) {
switch (error) {
case 0: return 0;
case WSAEWOULDBLOCK: return NET_EAGAIN;
case WSAEINPROGRESS: return NET_EINPROGRESS;
case WSAEALREADY: return NET_EALREADY;
Expand All @@ -115,15 +117,17 @@ int native_error() {
default: return 5;
}
}
int native_error() { return native_error(WSAGetLastError()); }
#else
using NativeSocket = int;
using NativeLength = socklen_t;
constexpr NativeSocket INVALID_NATIVE_SOCKET = -1;
constexpr int NATIVE_SEND_FLAGS = MSG_NOSIGNAL;
bool initialize_sockets() { return true; }
void close_socket(NativeSocket socket) { ::close(socket); }
int native_error() {
switch (errno) {
int native_error(int error) {
switch (error) {
case 0: return 0;
case EAGAIN: return NET_EAGAIN;
#if EWOULDBLOCK != EAGAIN
case EWOULDBLOCK: return NET_EAGAIN;
Expand Down Expand Up @@ -151,10 +155,16 @@ int native_error() {
default: return 5;
}
}
int native_error() { return native_error(errno); }
#endif

struct NativeSocketHandle {
NativeSocket value;
#ifdef _WIN32
std::mutex connectMutex;
bool connectPending = false;
int consumedConnectError = 0;
#endif
explicit NativeSocketHandle(NativeSocket socket) : value(socket) {}
~NativeSocketHandle() {
if (value != INVALID_NATIVE_SOCKET) close_socket(value);
Expand Down Expand Up @@ -568,11 +578,29 @@ int APS5_VABI sceNetConnect(int s, const void* addr, uint32_t addrlen) {
NativeLength native_length = 0;
if (!guest_to_native_address(addr, addrlen, native, native_length)) return fail(NET_EINVAL);
if (native.ss_family != (socket.family == NET_AF_INET ? AF_INET : AF_INET6)) return fail(NET_EAFNOSUPPORT);
if (::connect(socket.native->value, reinterpret_cast<const sockaddr*>(&native), native_length) == 0) return 0;
#ifdef _WIN32
if (WSAGetLastError() == WSAEWOULDBLOCK) return fail(NET_EINPROGRESS);
std::lock_guard<std::mutex> connectionLock(socket.native->connectMutex);
#endif
if (::connect(socket.native->value, reinterpret_cast<const sockaddr*>(&native), native_length) == 0) {
#ifdef _WIN32
socket.native->connectPending = false;
socket.native->consumedConnectError = 0;
#endif
return 0;
}
#ifdef _WIN32
const int error = WSAGetLastError();
if (error == WSAEWOULDBLOCK) {
socket.native->connectPending = socket.type == NET_SOCK_STREAM;
socket.native->consumedConnectError = 0;
} else if (error != WSAEALREADY && error != WSAEISCONN && error != WSAEINVAL) {
socket.native->connectPending = false;
socket.native->consumedConnectError = 0;
}
return fail(error == WSAEWOULDBLOCK ? NET_EINPROGRESS : native_error(error));
#else
return fail(native_error());
#endif
}

int64_t APS5_VABI sceNetRecv(int s, void* buf, size_t len, int flags) {
Expand Down Expand Up @@ -794,6 +822,32 @@ int APS5_VABI sceNetGetsockopt(int s, int level, int optname, void* optval, uint
if (level != NET_SOL_SOCKET) return fail(NET_EOPNOTSUPP);
int value = 0;
switch (optname) {
case NET_SO_ERROR: {
#ifdef _WIN32
std::lock_guard<std::mutex> connectionLock(socket.native->connectMutex);
#endif
int error = 0;
NativeLength size = sizeof(error);
if (::getsockopt(socket.native->value, SOL_SOCKET, SO_ERROR,
reinterpret_cast<char*>(&error), &size) != 0) return fail(native_error());
#ifdef _WIN32
if (error != 0 && error == socket.native->consumedConnectError) {
error = 0;
} else if (socket.native->connectPending) {
if (error != 0) {
socket.native->consumedConnectError = error;
socket.native->connectPending = false;
} else {
sockaddr_storage peer{};
NativeLength peerSize = sizeof(peer);
if (::getpeername(socket.native->value, reinterpret_cast<sockaddr*>(&peer), &peerSize) == 0)
socket.native->connectPending = false;
}
}
#endif
value = native_error(error);
break;
}
case NET_SO_NBIO: value = socket.nonblock ? 1 : 0; break;
case NET_SO_RCVTIMEO: value = socket.rcv_timeout_us; break;
case NET_SO_SNDTIMEO: value = socket.snd_timeout_us; break;
Expand Down
191 changes: 191 additions & 0 deletions core/libs/tests/GuestSceNetConnect.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
#include "SceTypes.hpp"
#include <array>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <thread>

extern "C" {
int APS5_VABI sceNetInit_nid_postfix();
int APS5_VABI sceNetSocket(const char*, int, int, int);
int APS5_VABI sceNetBind_nid_postfix(int, const void*, std::uint32_t);
int APS5_VABI sceNetListen(int, int);
int APS5_VABI sceNetGetsockname(int, void*, std::uint32_t*);
int APS5_VABI sceNetConnect(int, const void*, std::uint32_t);
int APS5_VABI sceNetAccept(int, void*, std::uint32_t*);
int APS5_VABI sceNetSocketClose(int);
int APS5_VABI sceNetSetsockopt(int, int, int, const void*, std::uint32_t);
int APS5_VABI sceNetGetsockopt(int, int, int, void*, std::uint32_t*);
std::int64_t APS5_VABI sceNetRecv(int, void*, std::size_t, int);
int* APS5_VABI sceNetErrnoLoc();
int APS5_VABI sceNetEpollCreate(const char*, int);
int APS5_VABI sceNetEpollControl(int, int, int, const NetEpollEvent*);
int APS5_VABI sceNetEpollWait(int, NetEpollEvent*, int, int);
int APS5_VABI sceNetEpollDestroy(int);
}

static void Require(bool condition, const char* message) {
if (!condition) {
std::fprintf(stderr, "%s\n", message);
std::abort();
}
}

static bool Failed(std::int64_t result, int error) {
return result == static_cast<int>(0x80410100u | static_cast<unsigned>(error)) && *sceNetErrnoLoc() == error;
}

struct BoundSocket {
int descriptor;
std::array<std::uint8_t, 16> address;
};

static BoundSocket BindLoopback() {
BoundSocket socket{sceNetSocket("connect-test", 2, 1, 6), {16, 2, 0, 0, 127, 0, 0, 1}};
Require(socket.descriptor >= 0, "create bound socket");
Require(sceNetBind_nid_postfix(socket.descriptor, socket.address.data(), socket.address.size()) == 0, "bind loopback");
std::uint32_t size = socket.address.size();
Require(sceNetGetsockname(socket.descriptor, socket.address.data(), &size) == 0 && size == socket.address.size(), "get bound address");
return socket;
}

static int NonblockingSocket() {
const int socket = sceNetSocket("connect-test", 2, 1, 6);
Require(socket >= 0, "create client socket");
const int enabled = 1;
Require(sceNetSetsockopt(socket, 0xffff, 0x1200, &enabled, sizeof(enabled)) == 0, "enable nonblocking");
return socket;
}

static std::uint32_t WaitForConnection(int socket) {
const int epoll = sceNetEpollCreate("connect-test", 0);
Require(epoll >= 0, "create epoll");
NetEpollEvent registration{};
registration.events = 4;
registration.ident = static_cast<std::uint64_t>(socket);
Require(sceNetEpollControl(epoll, 1, socket, &registration) == 0, "register connection");
NetEpollEvent ready{};
const int count = sceNetEpollWait(epoll, &ready, 1, 5000000);
Require(sceNetEpollDestroy(epoll) == 0, "destroy epoll");
Require(count == 1 && ready.ident == static_cast<std::uint64_t>(socket), "connection readiness within five seconds");
return ready.events;
}

static void CheckSocketError(int socket, int expected, const char* phase) {
std::array<int, 2> output{-1, 12345};
std::uint32_t size = sizeof(output);
*sceNetErrnoLoc() = 123;
const int result = sceNetGetsockopt(socket, 0xffff, 0x1007, output.data(), &size);
std::fprintf(stderr, "SO_ERROR [%s]: result=0x%08x, value=%d, expected=%d, length=%u, guest errno=%d\n",
phase, static_cast<unsigned>(result), output[0], expected, size, *sceNetErrnoLoc());
Require(result == 0, "query SO_ERROR");
Require(output[0] == expected, "SO_ERROR guest error value");
Require(output[1] == 12345 && size == sizeof(int), "SO_ERROR output bounds and length");
Require(*sceNetErrnoLoc() == 123, "SO_ERROR success preserves thread errno");
}

static void CheckInvalidArguments(int socket) {
int value = -1;
std::uint32_t size = sizeof(value);
Require(Failed(sceNetGetsockopt(socket, 0xffff, 0x1007, nullptr, &size), 22), "SO_ERROR null output");
Require(size == sizeof(value), "null output preserves length");
Require(Failed(sceNetGetsockopt(socket, 0xffff, 0x1007, &value, nullptr), 22), "SO_ERROR null length");
size = sizeof(value) - 1;
Require(Failed(sceNetGetsockopt(socket, 0xffff, 0x1007, &value, &size), 22), "SO_ERROR short output");
Require(size == sizeof(value) - 1 && value == -1, "short output is unchanged");
size = sizeof(value);
Require(Failed(sceNetGetsockopt(-1, 0xffff, 0x1007, &value, &size), 9), "SO_ERROR invalid descriptor");
Require(Failed(sceNetGetsockopt(socket, 0, 0x1007, &value, &size), 45), "SO_ERROR unsupported level");
Require(Failed(sceNetGetsockopt(socket, 0xffff, 0x7fffffff, &value, &size), 45), "unsupported socket option");
Require(value == -1 && size == sizeof(value), "invalid query preserves output");
}

static void CheckSuccessfulConnection() {
const auto listener = BindLoopback();
Require(sceNetListen(listener.descriptor, 1) == 0, "listen");
const int client = NonblockingSocket();
const int result = sceNetConnect(client, listener.address.data(), listener.address.size());
Require(result == 0 || Failed(result, 36), "nonblocking connect reports success or EINPROGRESS");
Require((WaitForConnection(client) & 4) != 0, "connected socket becomes writable");
CheckInvalidArguments(client);
CheckSocketError(client, 0, "connected, first read");
CheckSocketError(client, 0, "connected, second read");
const int accepted = sceNetAccept(listener.descriptor, nullptr, nullptr);
Require(accepted >= 0, "accept connected client");
char value = 0;
Require(Failed(sceNetRecv(client, &value, sizeof(value), 0), 35), "empty nonblocking receive still reports EAGAIN");
Require(sceNetSocketClose(accepted) == 0, "close accepted socket");
Require(sceNetSocketClose(client) == 0, "close connected client");
Require(sceNetSocketClose(listener.descriptor) == 0, "close listener");
std::fprintf(stderr, "PASS: nonblocking success, SO_ERROR arguments, and receive EAGAIN\n");
}

#ifdef _WIN32
static void CheckConnectionRetry(int client, const BoundSocket& refused) {
const int retry = sceNetConnect(client, refused.address.data(), refused.address.size());
if (Failed(retry, 36)) {
Require((WaitForConnection(client) & (4 | 8 | 16)) != 0, "retried refusal becomes ready");
std::array<int, 2> errors{-1, -1};
const auto readError = [&](int index) {
std::uint32_t size = sizeof(int);
Require(sceNetGetsockopt(client, 0xffff, 0x1007, &errors[index], &size) == 0,
"concurrent SO_ERROR query");
Require(size == sizeof(int), "concurrent SO_ERROR length");
};
std::thread first(readError, 0);
std::thread second(readError, 1);
first.join();
second.join();
std::fprintf(stderr, "SO_ERROR [retry, concurrent reads]: values=%d,%d, expected=61,0 in either order\n",
errors[0], errors[1]);
Require((errors[0] == 61 && errors[1] == 0) || (errors[0] == 0 && errors[1] == 61),
"a new connection error is consumed exactly once");
CheckSocketError(client, 0, "retry, third read");
std::fprintf(stderr, "PASS: same-socket refusal retry and concurrent read-and-clear\n");
} else {
Require(Failed(retry, 61), "retried connect reports ECONNREFUSED");
std::fprintf(stderr, "PASS: immediate retry refusal (concurrent SO_ERROR path not exercised)\n");
}
const auto listener = BindLoopback();
Require(sceNetListen(listener.descriptor, 1) == 0, "listen for retry");
const int connected = sceNetConnect(client, listener.address.data(), listener.address.size());
Require(connected == 0 || Failed(connected, 36), "retry to listening endpoint starts");
Require((WaitForConnection(client) & 4) != 0, "retry connects to listening endpoint");
CheckSocketError(client, 0, "successful retry, first read");
CheckSocketError(client, 0, "successful retry, second read");
const int accepted = sceNetAccept(listener.descriptor, nullptr, nullptr);
Require(accepted >= 0, "accept retried client");
char value = 0;
Require(Failed(sceNetRecv(client, &value, sizeof(value), 0), 35), "retried connection receive EAGAIN");
Require(sceNetSocketClose(accepted) == 0, "close accepted retry");
Require(sceNetSocketClose(listener.descriptor) == 0, "close retry listener");
std::fprintf(stderr, "PASS: same-socket retry connects successfully\n");
}
#endif

static void CheckRefusedConnection() {
const auto reserved = BindLoopback();
const int client = NonblockingSocket();
Require(sceNetSocketClose(reserved.descriptor) == 0, "release refused endpoint");
const int result = sceNetConnect(client, reserved.address.data(), reserved.address.size());
if (Failed(result, 36)) {
Require((WaitForConnection(client) & (4 | 8 | 16)) != 0, "refused connection becomes ready");
CheckInvalidArguments(client);
CheckSocketError(client, 61, "refused, first read");
CheckSocketError(client, 0, "refused, second read");
std::fprintf(stderr, "PASS: asynchronous refusal, error translation, and read-and-clear\n");
} else {
Require(Failed(result, 61), "immediate refusal reports ECONNREFUSED");
std::fprintf(stderr, "PASS: immediate refusal (asynchronous SO_ERROR path not exercised)\n");
}
#ifdef _WIN32
CheckConnectionRetry(client, reserved);
#endif
Require(sceNetSocketClose(client) == 0, "close refused client");
}

int main() {
Require(sceNetInit_nid_postfix() == 0, "initialize networking");
CheckSuccessfulConnection();
CheckRefusedConnection();
}
1 change: 1 addition & 0 deletions docs/dev/TechnicalDebt.md
Original file line number Diff line number Diff line change
Expand Up @@ -361,3 +361,4 @@ Throughout the project, every function at every stage either **does exactly what
- [Static scalar calls](../../core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp) (`s_call_b64` and `s_swappc_b64`) require one call per ordinary even-aligned SGPR link pair (s0:s1 through s104:s105) and one later `s_setpc_b64` return reading that exact pair. Regions must nest or be disjoint; recursion, shared links, escaping link values (including overlapping tuples and resource descriptors), invalid targets, interleaved regions, cross-region branches or fall-through, relative SGPR reads and dynamic `s_setpc_b64` in a program with calls throw. The analysis checks all decoded instructions, including unreachable code. Arbitrary indirect calls and general call frames are not implemented.
- [s_call_b64](../../core/shader/recompiler/Translation/src/ScalarInstructions.cpp) saves `PC + 4` over `GetShaderBase` and branches to `PC + 4 + signext(SIMM16) * 4`, following [AMD RDNA2 ISA](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf) sections 3.6.3, 12.2 and 12.3. Forward callees after `s_endpgm` are decoded. Native RDNA2/PS5 execution and title compatibility for these bounded scalar calls remain unverified; `s_rfe_b64` still throws in translation.
- [sceVideodec2CreateDecoder](../../core/libs/prx/libSceVideodec2/Export.cpp) (libSceVideodec2) decodes HEVC (codec type 974921) and VP9 (2382845) through FFmpeg, besides H.264. The tags come from SharpProspero's `Videodec2CodecType` and EVO-PLAYER-PS5's `sce_videodec2.h`, which records them from hardware runs. HEVC input is converted from length-prefixed NAL units to Annex B as for H.264, and VP9 frames are passed through. Output is the same 8-bit NV12 for every codec; a decoder for HEVC Main10 or VP9 Profile 2 is created, but a frame deeper than 8 bits throws, since the console's 10-bit output layout is unknown, and `QueryDecoderMemoryInfo` reports the 8-bit frame size for it. `sceVideodec2GetPictureInfo` and `sceVideodec2GetAvcPictureInfo` throw for HEVC and VP9 pictures, which use other layouts (`SceVideodec2HevcPictureInfo`, `SceVideodec2Vp9PictureInfo`), and `sceVideodec2GetHevcPictureInfo` and `sceVideodec2GetVp9PictureInfo` are not exported
- [Socket error queries](../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - `sceNetGetsockopt` SO_ERROR (0x1007, an int containing the guest errno, read and cleared) follows the FreeBSD 11 socket ABI; not confirmed on PS5 hardware or a title. Windows 10 build 19045.7417 repeatedly returns WSAECONNREFUSED for a failed nonblocking connection. A per-native-socket lock and consumed completion error provide read-and-clear for asynchronous TCP connection failures, reset when a new attempt starts. Once getpeername confirms a peer, tracking stops; established-socket and UDP errors retain host behaviour.
Loading