Merge pull request #1091 from kjhq/feat/libkernel-socket-events

feat(libkernel): implement read and write events for guest sockets
This commit is contained in:
Adrià 2026-10-11 02:02:31 +02:00 committed by GitHub
commit e8d6de4ae7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 435 additions and 5 deletions

View File

@ -736,6 +736,16 @@ if(BUILD_TESTING)
set_tests_properties(guest_dup PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel>")
endif()
add_test_executable(guest_socket_event_tests ${LIBS_INCLUDE_DIR}/tests/GuestSocketEvents.cpp)
target_include_directories(guest_socket_event_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(guest_socket_event_tests PRIVATE libkernel libc)
configure_windows_unwind(guest_socket_event_tests)
add_test(NAME guest_socket_events COMMAND guest_socket_event_tests)
set_tests_properties(guest_socket_events PROPERTIES TIMEOUT 20)
if(WIN32)
set_tests_properties(guest_socket_events PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel>")
endif()
add_test_executable(guest_elf_phdr_match_tests ${LIBS_INCLUDE_DIR}/tests/GuestElfPhdrMatch.cpp)
target_include_directories(guest_elf_phdr_match_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(guest_elf_phdr_match_tests PRIVATE libkernel libc)

View File

@ -1,5 +1,7 @@
#include "Equeue.hpp"
#include "prx/libkernel/Time/include/Time.hpp"
#include "prx/libc/include/General.hpp"
#include "prx/libkernel/Socket/include/SocketRuntime.hpp"
#include <algorithm>
#include <chrono>
@ -42,6 +44,7 @@ void KernelEqueuePrivate::Close() {
}
m_events.clear();
m_cond.NotifyAll();
WakePollers();
}
void KernelEqueuePrivate::TriggerExpiredTimers(uint64_t nowNs) {
@ -75,12 +78,48 @@ bool KernelEqueuePrivate::NextTimerWaitMicros(uint64_t nowNs, uint32_t* out) con
return true;
}
bool KernelEqueuePrivate::PollEvents() {
bool polled = false;
for (auto& ev : m_events) {
if (ev.filter.pollFunc != nullptr) {
ev.triggered = ev.filter.pollFunc(&ev);
polled = true;
}
}
return polled;
}
void KernelEqueuePrivate::WakePollers() {
for (const auto waker : m_pollers) {
GuestSockets::Wake(waker);
}
}
void KernelEqueuePrivate::WaitForDescriptors(std::unique_lock<std::mutex>& lock, std::uint64_t deadlineNanos) {
std::vector<GuestSockets::Interest> interests;
for (const auto& e : m_events) {
if (e.filter.pollFunc != nullptr) {
interests.push_back({static_cast<int>(e.event.ident), e.event.filter == EVFILT_WRITE});
}
}
const auto waker = GuestSockets::CurrentWaker();
m_pollers.push_back(waker);
lock.unlock();
const bool waited = GuestSockets::WaitAny(interests, deadlineNanos);
lock.lock();
m_pollers.erase(std::find(m_pollers.begin(), m_pollers.end(), waker));
if (!waited) {
m_cond.WaitUntil(lock, deadlineNanos);
}
}
int KernelEqueuePrivate::GetTriggeredEvents(KernelEvent* ev, int num) {
std::unique_lock lock(m_mutex);
if (m_closed) {
return SCE_KERNEL_ERROR_EBADF;
}
TriggerExpiredTimers(MonotonicNs());
PollEvents();
int ret = 0;
for (auto it = m_events.begin(); it != m_events.end();) {
auto& e = *it;
@ -123,6 +162,7 @@ int KernelEqueuePrivate::WaitForEvents(KernelEvent* ev, int num, uint32_t micros
const std::uint64_t deadline = TimedWait::DeadlineNanos(micros);
for (;;) {
TriggerExpiredTimers(MonotonicNs());
const bool polling = PollEvents();
int ret = 0;
for (auto it = m_events.begin(); it != m_events.end() && ret < num;) {
auto& e = *it;
@ -159,8 +199,12 @@ int KernelEqueuePrivate::WaitForEvents(KernelEvent* ev, int num, uint32_t micros
}
if (micros == 0) {
uint32_t timerWait = 0;
if (NextTimerWaitMicros(MonotonicNs(), &timerWait)) {
m_cond.WaitUntil(lock, TimedWait::NowNanos() + static_cast<std::uint64_t>(timerWait) * 1000ULL);
const bool hasTimer = NextTimerWaitMicros(MonotonicNs(), &timerWait);
const std::uint64_t wake = hasTimer ? TimedWait::NowNanos() + static_cast<std::uint64_t>(timerWait) * 1000ULL : 0;
if (polling) {
WaitForDescriptors(lock, wake);
} else if (hasTimer) {
m_cond.WaitUntil(lock, wake);
} else {
m_cond.Wait(lock);
}
@ -172,7 +216,12 @@ int KernelEqueuePrivate::WaitForEvents(KernelEvent* ev, int num, uint32_t micros
return 0;
}
const std::uint64_t timerDeadline = now + static_cast<std::uint64_t>(timerWait) * 1000ULL;
m_cond.WaitUntil(lock, hasTimer ? std::min(deadline, timerDeadline) : deadline);
const std::uint64_t wake = hasTimer ? std::min(deadline, timerDeadline) : deadline;
if (polling) {
WaitForDescriptors(lock, wake);
} else {
m_cond.WaitUntil(lock, wake);
}
}
}
}
@ -201,6 +250,7 @@ int KernelEqueuePrivate::AddEvent(const KernelEqueueEvent& event) {
m_events.push_back(event);
}
m_cond.NotifyOne();
WakePollers();
return EQUEUE_OK;
}
@ -223,6 +273,7 @@ int KernelEqueuePrivate::TriggerEvent(uintptr_t ident, int16_t filter, void* tri
it->triggered = true;
}
m_cond.NotifyOne();
WakePollers();
return EQUEUE_OK;
}
@ -244,9 +295,20 @@ int KernelEqueuePrivate::DeleteEvent(uintptr_t ident, int16_t filter) {
it->filter.deleteEventFunc(m_handle, &*it);
}
m_events.erase(it);
WakePollers();
return EQUEUE_OK;
}
void KernelEqueuePrivate::RemoveDescriptorEvents(uintptr_t ident) {
std::unique_lock lock(m_mutex);
const auto removed = std::erase_if(m_events, [ident](const auto& e) {
return e.event.ident == ident && (e.event.filter == EVFILT_READ || e.event.filter == EVFILT_WRITE);
});
if (removed != 0) {
WakePollers();
}
}
KernelEqueueRef EqueuePin_nid_postfix(KernelEqueue eq) {
if (eq == 0) {
return {};
@ -481,4 +543,64 @@ int APS5_VABI sceKernelDeleteAmprSystemEvent(KernelEqueue eq, int id) {
return sceKernelDeleteAmprEvent(eq, id);
}
static int AddDescriptorEvent(KernelEqueue eq, int fd, std::size_t size, void* udata, int16_t filter) {
if (fd < GuestSockets::FirstDescriptor) {
NotImplemented_nid_no_patch(filter == EVFILT_READ ? "sceKernelAddReadEvent: non-socket descriptor" : "sceKernelAddWriteEvent: non-socket descriptor");
}
if (size > 1) {
NotImplemented_nid_no_patch(filter == EVFILT_READ ? "sceKernelAddReadEvent: low watermark" : "sceKernelAddWriteEvent: low watermark");
}
if (!GuestSockets::IsOpen(fd)) {
return SCE_KERNEL_ERROR_EBADF;
}
KernelEqueueEvent event{};
event.event.ident = static_cast<uintptr_t>(fd);
event.event.filter = filter;
event.event.flags = EV_ADD;
event.event.udata = udata;
event.filter.pollFunc = [](KernelEqueueEvent* e) {
std::int64_t data = 0;
bool eof = false;
if (!GuestSockets::Ready(static_cast<int>(e->event.ident), e->event.filter == EVFILT_WRITE, &data, &eof)) {
return false;
}
e->event.data = static_cast<intptr_t>(data);
e->event.flags = static_cast<uint16_t>(eof ? (e->event.flags | EV_EOF) : (e->event.flags & ~EV_EOF));
return true;
};
event.filter.resetFunc = [](KernelEqueueEvent* e) {
e->triggered = false;
};
return EqueueAddEvent_nid_postfix(eq, event);
}
int APS5_VABI sceKernelAddReadEvent(KernelEqueue eq, int fd, std::size_t size, void* udata) {
return AddDescriptorEvent(eq, fd, size, udata, EVFILT_READ);
}
int APS5_VABI sceKernelDeleteReadEvent(KernelEqueue eq, int fd) {
return EqueueDeleteEvent_nid_postfix(eq, static_cast<uintptr_t>(fd), EVFILT_READ);
}
int APS5_VABI sceKernelAddWriteEvent(KernelEqueue eq, int fd, std::size_t size, void* udata) {
return AddDescriptorEvent(eq, fd, size, udata, EVFILT_WRITE);
}
int APS5_VABI sceKernelDeleteWriteEvent(KernelEqueue eq, int fd) {
return EqueueDeleteEvent_nid_postfix(eq, static_cast<uintptr_t>(fd), EVFILT_WRITE);
}
}
void EqueueDescriptorClosed(int descriptor) {
std::vector<KernelEqueueRef> owners;
{
std::unique_lock lock(g_equeueMutex);
for (const auto& [handle, owner] : g_equeues) {
owners.push_back(owner);
}
}
for (const auto& owner : owners) {
owner->RemoveDescriptorEvents(static_cast<uintptr_t>(descriptor));
}
}

View File

@ -8,12 +8,15 @@
#include <mutex>
#include <string>
#include <unordered_map>
#include <vector>
#include "SceTypes.hpp"
#include "prx/libc/include/general/VabiMacros.hpp"
#include "prx/libkernel/KernelErrors.hpp"
#include "prx/libkernel/Time/include/TimedWait.hpp"
static constexpr int16_t EVFILT_READ = -1;
static constexpr int16_t EVFILT_WRITE = -2;
static constexpr int16_t EVFILT_TIMER = -7;
static constexpr int16_t EVFILT_USER = -11;
static constexpr int16_t EVFILT_VIDEO_OUT = -13;
@ -24,6 +27,7 @@ static constexpr uint16_t EV_ADD = 0x0001;
static constexpr uint16_t EV_ONESHOT = 0x0010;
static constexpr uint16_t EV_CLEAR = 0x0020;
static constexpr uint16_t EV_ERROR = 0x4000;
static constexpr uint16_t EV_EOF = 0x8000;
static constexpr int EQUEUE_OK = 0;
@ -32,6 +36,7 @@ struct KernelEqueueEvent;
using EqueueTriggerFunc = void (*)(KernelEqueueEvent* event, void* triggerData);
using EqueueResetFunc = void (*)(KernelEqueueEvent* event);
using EqueueDeleteFunc = void (*)(KernelEqueue eq, KernelEqueueEvent* event);
using EqueuePollFunc = bool (*)(KernelEqueueEvent* event);
struct KernelFilter {
void* data = nullptr;
@ -39,6 +44,7 @@ struct KernelFilter {
EqueueTriggerFunc triggerFunc = nullptr;
EqueueResetFunc resetFunc = nullptr;
EqueueDeleteFunc deleteEventFunc = nullptr;
EqueuePollFunc pollFunc = nullptr;
};
struct KernelEqueueEvent {
@ -66,14 +72,19 @@ public:
int DeleteEvent(uintptr_t ident, int16_t filter);
int GetTriggeredEvents(KernelEvent* ev, int num);
int WaitForEvents(KernelEvent* ev, int num, uint32_t micros);
void RemoveDescriptorEvents(uintptr_t ident);
void Close();
static uint64_t MonotonicNs();
private:
void TriggerExpiredTimers(uint64_t nowNs);
bool NextTimerWaitMicros(uint64_t nowNs, uint32_t* out) const;
bool PollEvents();
void WakePollers();
void WaitForDescriptors(std::unique_lock<std::mutex>& lock, std::uint64_t deadlineNanos);
std::list<KernelEqueueEvent> m_events;
std::vector<std::uintptr_t> m_pollers;
std::mutex m_mutex;
TimedWait::Condition m_cond;
std::string m_name;
@ -82,6 +93,7 @@ private:
};
using KernelEqueueRef = std::shared_ptr<KernelEqueuePrivate>;
void EqueueDescriptorClosed(int descriptor);
extern "C" {
KernelEqueueRef EqueuePin_nid_postfix(KernelEqueue eq);

View File

@ -1,6 +1,7 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <vector>
namespace GuestSockets {
constexpr int FirstDescriptor = 0x10000000;
int Duplicate(int descriptor);
@ -10,6 +11,14 @@ bool IsOpen(int descriptor);
int Family(int descriptor);
std::int64_t Read(int descriptor, void* buffer, std::size_t length);
std::int64_t Write(int descriptor, const void* buffer, std::size_t length);
bool Ready(int descriptor, bool write, std::int64_t* data, bool* eof);
struct Interest {
int descriptor;
bool write;
};
std::uintptr_t CurrentWaker();
void Wake(std::uintptr_t waker);
bool WaitAny(const std::vector<Interest>& interests, std::uint64_t deadlineNanos);
}
extern "C" bool GuestSocketIsOpen_nid_no_patch(int descriptor);

View File

@ -5,6 +5,8 @@
#else
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <time.h>
#include <netinet/in.h>
#include <poll.h>
#include <unistd.h>
@ -13,6 +15,8 @@
#include "prx/libc/include/GuestArena.hpp"
#include "prx/libc/include/general/VabiMacros.hpp"
#include "prx/libkernel/Socket/include/SocketRuntime.hpp"
#include "prx/libkernel/Equeue/Equeue.hpp"
#include "prx/libkernel/Time/include/TimedWait.hpp"
#include <algorithm>
#include <chrono>
#include <climits>
@ -109,6 +113,43 @@ struct Socket {
#endif
}
};
struct Waker {
NativeSocket value = Invalid;
Waker() {
value = ::socket(AF_INET, SOCK_DGRAM, 0);
sockaddr_in address{};
address.sin_family = AF_INET;
address.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
socklen_t size = sizeof(address);
#ifdef _WIN32
u_long nonblocking = 1;
const bool configured = value != Invalid && ioctlsocket(value, FIONBIO, &nonblocking) == 0;
#else
const bool configured = value != Invalid && ::fcntl(value, F_SETFL, ::fcntl(value, F_GETFL) | O_NONBLOCK) == 0;
#endif
if (!configured || bind(value, reinterpret_cast<sockaddr*>(&address), size) != 0 ||
getsockname(value, reinterpret_cast<sockaddr*>(&address), &size) != 0 ||
connect(value, reinterpret_cast<sockaddr*>(&address), size) != 0) {
const int error = NativeError();
Close();
throw std::runtime_error("equeue waker: loopback socket setup failed, guest errno " + std::to_string(error));
}
}
~Waker() { Close(); }
void Close() {
if (value == Invalid) return;
#ifdef _WIN32
closesocket(value);
#else
::close(value);
#endif
value = Invalid;
}
};
Waker& ThreadWaker() {
thread_local Waker waker;
return waker;
}
std::mutex socketsMutex;
std::map<int, std::shared_ptr<Socket>> sockets;
int nextDescriptor = GuestSockets::FirstDescriptor;
@ -173,8 +214,68 @@ void GuestAddress(const sockaddr_storage& native, void* output, std::uint32_t* l
}
int GuestSockets::Close(int descriptor) {
std::lock_guard lock(socketsMutex);
return sockets.erase(descriptor) ? 0 : Fail(9);
{
std::lock_guard lock(socketsMutex);
if (!sockets.erase(descriptor)) return Fail(9);
}
EqueueDescriptorClosed(descriptor);
return 0;
}
std::uintptr_t GuestSockets::CurrentWaker() {
return static_cast<std::uintptr_t>(ThreadWaker().value);
}
void GuestSockets::Wake(std::uintptr_t waker) {
const char signal = 0;
if (send(static_cast<NativeSocket>(waker), &signal, 1, 0) < 0 && NativeError() != 35)
throw std::runtime_error("equeue waker: send failed, guest errno " + std::to_string(NativeError()));
}
bool GuestSockets::WaitAny(const std::vector<Interest>& interests, std::uint64_t deadlineNanos) {
const NativeSocket waker = ThreadWaker().value;
std::vector<std::shared_ptr<Socket>> held;
#ifdef _WIN32
std::vector<WSAPOLLFD> entries;
constexpr SHORT ReadMask = POLLRDNORM;
constexpr SHORT WriteMask = POLLWRNORM;
#else
std::vector<pollfd> entries;
constexpr short ReadMask = POLLIN;
constexpr short WriteMask = POLLOUT;
#endif
{
std::lock_guard lock(socketsMutex);
for (const auto& interest : interests) {
const auto found = sockets.find(interest.descriptor);
if (found == sockets.end()) continue;
held.push_back(found->second);
entries.push_back({found->second->value, interest.write ? WriteMask : ReadMask, 0});
}
}
entries.push_back({waker, ReadMask, 0});
std::uint64_t remaining = 0;
if (deadlineNanos != 0) {
const std::uint64_t now = TimedWait::NowNanos();
if (now >= deadlineNanos) return true;
remaining = deadlineNanos - now;
}
#ifdef _WIN32
INT timeout = -1;
if (deadlineNanos != 0) {
if (remaining < 1000000) return false;
timeout = static_cast<INT>(std::min<std::uint64_t>(remaining / 1000000, INT_MAX));
}
if (WSAPoll(entries.data(), static_cast<ULONG>(entries.size()), timeout) == SOCKET_ERROR)
throw std::runtime_error("equeue wait: WSAPoll failed, guest errno " + std::to_string(NativeError()));
#else
timespec timeout{static_cast<time_t>(remaining / 1000000000), static_cast<long>(remaining % 1000000000)};
if (::ppoll(entries.data(), entries.size(), deadlineNanos != 0 ? &timeout : nullptr, nullptr) < 0 && errno != EINTR)
throw std::runtime_error("equeue wait: ppoll failed, guest errno " + std::to_string(NativeError()));
#endif
char drained[16];
while (recv(waker, drained, sizeof(drained), 0) > 0) {}
return true;
}
bool GuestSockets::IsOpen(int descriptor) {
@ -349,6 +450,40 @@ int UnnamedAddress(void* address, std::uint32_t* length) {
}
}
bool GuestSockets::Ready(int descriptor, bool write, std::int64_t* data, bool* eof) {
std::shared_ptr<Socket> socket;
{
std::lock_guard lock(socketsMutex);
const auto found = sockets.find(descriptor);
if (found == sockets.end()) return false;
socket = found->second;
}
#ifdef _WIN32
WSAPOLLFD entry{socket->value, static_cast<SHORT>(write ? POLLWRNORM : POLLRDNORM), 0};
if (WSAPoll(&entry, 1, 0) <= 0) return false;
#else
pollfd entry{socket->value, static_cast<short>(write ? POLLOUT : (POLLIN | POLLRDHUP)), 0};
if (::poll(&entry, 1, 0) <= 0) return false;
if (!write && (entry.revents & POLLRDHUP)) entry.revents |= POLLHUP;
#endif
*eof = (entry.revents & (POLLHUP | POLLERR)) != 0;
*data = 0;
if (write) {
int size = 0;
socklen_t length = sizeof(size);
if (getsockopt(socket->value, SOL_SOCKET, SO_SNDBUF, reinterpret_cast<char*>(&size), &length) == 0) *data = size;
} else {
#ifdef _WIN32
unsigned long available = 0;
if (ioctlsocket(socket->value, FIONREAD, &available) == 0) *data = static_cast<std::int64_t>(available);
#else
int available = 0;
if (::ioctl(socket->value, FIONREAD, &available) == 0) *data = available;
#endif
}
return true;
}
extern "C" {
int APS5_VABI fcntl_nid_postfix(int descriptor, int command, ...) {
const auto socket = Lookup(descriptor);

View File

@ -0,0 +1,141 @@
#include "SceTypes.hpp"
#include <array>
#include <chrono>
#include <cstdint>
#include <cstdlib>
#include <stdexcept>
#include <thread>
extern "C" {
int APS5_VABI socket_nid_postfix(int, int, int);
int APS5_VABI bind_nid_postfix(int, const void*, std::uint32_t);
int APS5_VABI listen_nid_postfix(int, int);
int APS5_VABI getsockname_nid_postfix(int, void*, std::uint32_t*);
int APS5_VABI connect_nid_postfix(int, const void*, std::uint32_t);
int APS5_VABI accept_nid_postfix(int, void*, std::uint32_t*);
std::int64_t APS5_VABI send_nid_postfix(int, const void*, std::uint64_t, int);
std::int64_t APS5_VABI recv_nid_postfix(int, void*, std::uint64_t, int);
int APS5_VABI close_nid_postfix(int);
int APS5_VABI sceKernelCreateEqueue(KernelEqueue* eq, const char* name);
int APS5_VABI sceKernelDeleteEqueue(KernelEqueue eq);
int APS5_VABI sceKernelWaitEqueue(KernelEqueue eq, KernelEvent* ev, int num, int* out, const KernelUseconds* timo);
int APS5_VABI sceKernelAddReadEvent(KernelEqueue eq, int fd, std::size_t size, void* udata);
int APS5_VABI sceKernelDeleteReadEvent(KernelEqueue eq, int fd);
int APS5_VABI sceKernelAddWriteEvent(KernelEqueue eq, int fd, std::size_t size, void* udata);
int APS5_VABI sceKernelDeleteWriteEvent(KernelEqueue eq, int fd);
}
static constexpr int SCE_OK = 0;
static constexpr int SCE_KERNEL_ERROR_ENOENT = static_cast<int>(0x80020002);
static constexpr int SCE_KERNEL_ERROR_EBADF = static_cast<int>(0x80020009);
static constexpr int SCE_KERNEL_ERROR_ETIMEDOUT = static_cast<int>(0x8002003c);
static constexpr int EVFILT_READ = -1;
static constexpr int EVFILT_WRITE = -2;
static constexpr std::uint16_t EV_EOF = 0x8000;
static void Require(bool value) { if (!value) std::abort(); }
static bool Rejects(int (APS5_VABI *add)(KernelEqueue, int, std::size_t, void*), KernelEqueue eq, int fd, std::size_t size) {
try {
add(eq, fd, size, nullptr);
} catch (const std::exception&) {
return true;
}
return false;
}
int main() {
const int listener = socket_nid_postfix(2, 1, 6);
Require(listener >= 0);
std::array<std::uint8_t, 16> address{16, 2, 0, 0, 127, 0, 0, 1};
Require(bind_nid_postfix(listener, address.data(), address.size()) == 0);
Require(listen_nid_postfix(listener, 4) == 0);
std::uint32_t addressSize = address.size();
Require(getsockname_nid_postfix(listener, address.data(), &addressSize) == 0);
KernelEqueue eq = 0;
Require(sceKernelCreateEqueue(&eq, "sockets") == SCE_OK);
const KernelUseconds shortTimeout = 20000;
const KernelUseconds timeout = 1000000;
int marker = 0;
int count = 0;
std::array<KernelEvent, 4> events{};
Require(sceKernelAddReadEvent(eq, listener, 1, &marker) == SCE_OK);
Require(sceKernelWaitEqueue(eq, events.data(), events.size(), &count, &shortTimeout) == SCE_KERNEL_ERROR_ETIMEDOUT);
Require(count == 0);
const int client = socket_nid_postfix(2, 1, 0);
Require(client >= 0);
Require(connect_nid_postfix(client, address.data(), address.size()) == 0);
Require(sceKernelWaitEqueue(eq, events.data(), events.size(), &count, &timeout) == SCE_OK);
Require(count == 1);
Require(events[0].ident == static_cast<std::uintptr_t>(listener));
Require(events[0].filter == EVFILT_READ);
Require(events[0].udata == &marker);
const int accepted = accept_nid_postfix(listener, nullptr, nullptr);
Require(accepted >= 0);
Require(sceKernelDeleteReadEvent(eq, listener) == SCE_OK);
Require(sceKernelDeleteReadEvent(eq, listener) == SCE_KERNEL_ERROR_ENOENT);
Require(sceKernelAddWriteEvent(eq, client, 0, nullptr) == SCE_OK);
Require(sceKernelWaitEqueue(eq, events.data(), events.size(), &count, &timeout) == SCE_OK);
Require(count == 1 && events[0].filter == EVFILT_WRITE && events[0].data > 0);
Require(sceKernelDeleteWriteEvent(eq, client) == SCE_OK);
Require(sceKernelAddReadEvent(eq, accepted, 1, nullptr) == SCE_OK);
const char message[] = "ready";
Require(send_nid_postfix(client, message, sizeof(message), 0) == sizeof(message));
Require(sceKernelWaitEqueue(eq, events.data(), events.size(), &count, &timeout) == SCE_OK);
Require(count == 1 && events[0].ident == static_cast<std::uintptr_t>(accepted));
Require(events[0].data == sizeof(message) && (events[0].flags & EV_EOF) == 0);
char received[sizeof(message)]{};
Require(recv_nid_postfix(accepted, received, sizeof(received), 0) == sizeof(received));
Require(sceKernelWaitEqueue(eq, events.data(), events.size(), &count, &shortTimeout) == SCE_KERNEL_ERROR_ETIMEDOUT);
Require(close_nid_postfix(client) == 0);
Require(sceKernelWaitEqueue(eq, events.data(), events.size(), &count, &timeout) == SCE_OK);
Require(count == 1 && events[0].data == 0 && (events[0].flags & EV_EOF) != 0);
KernelEqueue blocking = 0;
Require(sceKernelCreateEqueue(&blocking, "blocking") == SCE_OK);
const int writer = socket_nid_postfix(2, 1, 0);
Require(writer >= 0 && connect_nid_postfix(writer, address.data(), address.size()) == 0);
const int reader = accept_nid_postfix(listener, nullptr, nullptr);
Require(reader >= 0);
Require(sceKernelAddReadEvent(blocking, reader, 1, nullptr) == SCE_OK);
std::thread sender([&] {
std::this_thread::sleep_for(std::chrono::milliseconds(50));
Require(send_nid_postfix(writer, message, sizeof(message), 0) == sizeof(message));
});
const auto start = std::chrono::steady_clock::now();
Require(sceKernelWaitEqueue(blocking, events.data(), events.size(), &count, nullptr) == SCE_OK);
sender.join();
Require(std::chrono::steady_clock::now() - start >= std::chrono::milliseconds(40));
Require(count == 1 && events[0].ident == static_cast<std::uintptr_t>(reader) && events[0].data == sizeof(message));
Require(recv_nid_postfix(reader, received, sizeof(received), 0) == sizeof(received));
std::thread waiter([&] {
std::array<KernelEvent, 4> woken{};
int wokenCount = 0;
Require(sceKernelWaitEqueue(blocking, woken.data(), woken.size(), &wokenCount, nullptr) == SCE_OK);
Require(wokenCount == 1 && woken[0].filter == EVFILT_WRITE && woken[0].ident == static_cast<std::uintptr_t>(writer));
});
std::this_thread::sleep_for(std::chrono::milliseconds(50));
Require(sceKernelAddWriteEvent(blocking, writer, 0, nullptr) == SCE_OK);
waiter.join();
Require(sceKernelDeleteWriteEvent(blocking, writer) == SCE_OK);
Require(close_nid_postfix(reader) == 0);
Require(sceKernelDeleteReadEvent(blocking, reader) == SCE_KERNEL_ERROR_ENOENT);
Require(close_nid_postfix(writer) == 0);
Require(sceKernelDeleteEqueue(blocking) == SCE_OK);
Require(sceKernelAddReadEvent(eq, client, 1, nullptr) == SCE_KERNEL_ERROR_EBADF);
Require(Rejects(sceKernelAddReadEvent, eq, 0, 1));
Require(Rejects(sceKernelAddWriteEvent, eq, accepted, 64));
Require(close_nid_postfix(accepted) == 0);
Require(close_nid_postfix(listener) == 0);
Require(sceKernelDeleteEqueue(eq) == SCE_OK);
}

View File

@ -332,6 +332,7 @@ Throughout the project, every function at every stage either **does exactly what
- [Pending storage-image results](../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) are stored when something reads their memory, not when the GPU work that wrote them completes. A title that waits for that work, frees the image and reuses its memory can have the new data overwritten by the late store (#1970). `APS5_STORE_AT_FLIP=1` stores every pending result at each flip; storing them at the title's GPU waits (labels, fences) is unimplemented.
- Windows host-pointer imports of the guest mapping itself are compared instead of write-watched: a driver can mark imported pages dirty after the synchronous import window. Shared memory imported through a separate writable alias retains protection-based tracking on the guest views; driver access to the import alias does not disarm those views. [GuestMemory](../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) excludes every overlapping 64 KiB tracker block while its backing is retained. A full block replaced by fresh private backing regains tracking and gets a new generation; protection changes, partial replacements and shared remappings do not restore it. This preserves CPU edits through byte comparisons but disables snapshot reuse and unit shadows for excluded blocks; restoring tracking on import retirement without replacing the backing and measuring the comparison cost remain unimplemented.
- [shm_open](../../core/libs/prx/libkernel/File/src/SharedMemory.cpp) (libkernel) creates only anonymous objects (`SHM_ANON`), each backed by a host anonymous file: a `memfd` on Linux, a delete-on-close temporary file on Windows. The flag checks follow FreeBSD 11's `kern_shm_open`; the mode is ignored. Named objects throw. The descriptor cannot be mapped yet: `mmap` accepts only private anonymous mappings and fails a descriptor mapping with `EOPNOTSUPP`, so the known caller (the bundled Mesa in PPSA59322, which truncates the object and maps it `MAP_SHARED`) gets that failure
- [sceKernelAddReadEvent and sceKernelAddWriteEvent](../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel): a write event reports the socket's send buffer size (`SO_SNDBUF`) in `data`, not the free space left in it, as Windows has no query for that space. Only guest sockets are supported; other descriptors and a low watermark above 1 throw
- [sceNetSendmsg and sceNetRecvmsg](../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) copy the scatter/gather list through one host buffer and accept only the flags `sceNetSend`/`sceNetRecv` accept. Sending control data throws; a receive never returns control data (`msg_controllen` 0), since no socket option that queues it is supported
- [Additional content](../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) is not installed or mounted: `sceAppContentAddcontMount` answers `NOT_FOUND` for every entitlement label and `sceAppContentAddcontUnmount` for every mount point, as no add-content mount is ever handed out; `sceAppContentGetAddcontInfo` answers `DRM_NO_ENTITLEMENT` for every label and `sceAppContentGetAddcontInfoList` reports 0 entries
- [sceKernelPread, sceKernelPwrite, sceKernelPreadv, sceKernelPwritev](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) on Windows transfer at the given offset with an overlapped `ReadFile`/`WriteFile` and restore the descriptor's file pointer afterwards, so a concurrent `read`/`write` on the same descriptor can see the moved pointer in between, unlike on the console. Handles that are not disk files return `ESPIPE`. `readv` on a pipe or console handle stops after the first iovec read, a short read the console is assumed to allow; each iovec is transferred separately, so a failure after the first transfer returns the bytes already done