mirror of
https://github.com/KytyPS5/KytyPS5.git
synced 2026-08-03 11:23:49 +00:00
444 lines
18 KiB
C++
444 lines
18 KiB
C++
#include "kernel/eventQueue.h"
|
|
#include "libs/errno.h"
|
|
|
|
#include <algorithm>
|
|
#include <atomic>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <memory>
|
|
#include <mutex>
|
|
#include <thread>
|
|
#include <vector>
|
|
|
|
namespace {
|
|
|
|
namespace EventQueue = Libs::LibKernel::EventQueue;
|
|
using Libs::LibKernel::KERNEL_ERROR_EBADF;
|
|
using Libs::LibKernel::KERNEL_ERROR_ENOENT;
|
|
|
|
void Check(bool value, const char* text) {
|
|
if (!value) {
|
|
std::fprintf(stderr, "EventQueueLifetimeTests: failed: %s\n", text);
|
|
std::abort();
|
|
}
|
|
}
|
|
|
|
void CheckConcurrentResult(int result, const char* text) {
|
|
Check(result == OK || result == KERNEL_ERROR_EBADF || result == KERNEL_ERROR_ENOENT, text);
|
|
}
|
|
|
|
void CountDeletedEvent(EventQueue::KernelEqueue, EventQueue::KernelEqueueEvent* event) {
|
|
auto* count = static_cast<std::atomic_uint32_t*>(event->filter.data);
|
|
count->fetch_add(1, std::memory_order_relaxed);
|
|
}
|
|
|
|
struct DuplicateEventOwner {
|
|
std::atomic_uint32_t delete_count {0};
|
|
};
|
|
|
|
void QueueDuplicateEvent(EventQueue::KernelEqueueEvent* event, void* trigger_data) {
|
|
auto next = event->event;
|
|
next.data = reinterpret_cast<intptr_t>(trigger_data);
|
|
if (event->triggered) {
|
|
event->pending_events.push_back(next);
|
|
} else {
|
|
event->event = next;
|
|
event->triggered = true;
|
|
}
|
|
}
|
|
|
|
void ResetDuplicateEvent(EventQueue::KernelEqueueEvent* event) {
|
|
event->triggered = false;
|
|
event->event.data = 0;
|
|
}
|
|
|
|
void DeleteDuplicateEvent(EventQueue::KernelEqueue, EventQueue::KernelEqueueEvent* event) {
|
|
auto* owner = static_cast<DuplicateEventOwner*>(event->filter.data);
|
|
owner->delete_count.fetch_add(1, std::memory_order_relaxed);
|
|
}
|
|
|
|
void PoisonDuplicateEvent(EventQueue::KernelEqueueEvent*, void*) {
|
|
Check(false, "duplicate add replaced trigger callback");
|
|
}
|
|
|
|
void TestDuplicateAddPreservesEventState() {
|
|
EventQueue::KernelEqueue queue = EventQueue::KERNEL_EQUEUE_INVALID;
|
|
Check(EventQueue::KernelCreateEqueue(&queue, "duplicate-add") == OK,
|
|
"create duplicate add queue");
|
|
|
|
auto original_owner = std::make_shared<DuplicateEventOwner>();
|
|
std::weak_ptr<DuplicateEventOwner> weak_original = original_owner;
|
|
EventQueue::KernelEqueueEvent original {};
|
|
original.event.ident = 17;
|
|
original.event.filter = EventQueue::KERNEL_EVFILT_VIDEO_OUT;
|
|
original.event.udata = reinterpret_cast<void*>(0x1111);
|
|
original.filter.data = original_owner.get();
|
|
original.filter.owner = original_owner;
|
|
original.filter.trigger_func = QueueDuplicateEvent;
|
|
original.filter.reset_func = ResetDuplicateEvent;
|
|
original.filter.delete_event_func = DeleteDuplicateEvent;
|
|
Check(EventQueue::KernelAddEvent(queue, original) == OK, "add original duplicate event");
|
|
Check(EventQueue::KernelTriggerEvent(queue, 17, EventQueue::KERNEL_EVFILT_VIDEO_OUT,
|
|
reinterpret_cast<void*>(0x1234)) == OK,
|
|
"queue first trigger");
|
|
Check(EventQueue::KernelTriggerEvent(queue, 17, EventQueue::KERNEL_EVFILT_VIDEO_OUT,
|
|
reinterpret_cast<void*>(0x5678)) == OK,
|
|
"queue pending trigger");
|
|
|
|
auto replacement_owner = std::make_shared<DuplicateEventOwner>();
|
|
std::weak_ptr<DuplicateEventOwner> weak_replacement = replacement_owner;
|
|
EventQueue::KernelEqueueEvent duplicate {};
|
|
duplicate.triggered = false;
|
|
duplicate.deadline_ns = 1;
|
|
duplicate.event.ident = 17;
|
|
duplicate.event.filter = EventQueue::KERNEL_EVFILT_VIDEO_OUT;
|
|
duplicate.event.data = 0x7fffffff;
|
|
duplicate.event.udata = reinterpret_cast<void*>(0x2222);
|
|
duplicate.filter.data = replacement_owner.get();
|
|
duplicate.filter.owner = replacement_owner;
|
|
duplicate.filter.trigger_func = PoisonDuplicateEvent;
|
|
Check(EventQueue::KernelAddEvent(queue, duplicate) == OK, "update duplicate event");
|
|
|
|
duplicate.filter.owner.reset();
|
|
replacement_owner.reset();
|
|
Check(weak_replacement.expired(), "duplicate owner is not retained");
|
|
original.filter.owner.reset();
|
|
original_owner.reset();
|
|
Check(!weak_original.expired(), "original event owner remains retained");
|
|
|
|
EventQueue::KernelEvent events[2] {};
|
|
int out = 0;
|
|
Libs::LibKernel::KernelUseconds timeout = 0;
|
|
Check(EventQueue::KernelWaitEqueue(queue, events, 2, &out, &timeout) == OK,
|
|
"read queued duplicate triggers");
|
|
Check(out == 2, "duplicate add preserves pending event count");
|
|
Check(events[0].data == 0x1234 && events[1].data == 0x5678,
|
|
"duplicate add preserves current and pending event data");
|
|
Check(events[0].udata == reinterpret_cast<void*>(0x2222),
|
|
"duplicate add updates current user data");
|
|
Check(events[1].udata == reinterpret_cast<void*>(0x2222),
|
|
"duplicate add updates pending user data");
|
|
|
|
EventQueue::KernelEvent timer_event {};
|
|
Check(EventQueue::KernelWaitEqueue(queue, &timer_event, 1, &out, &timeout) == OK && out == 1,
|
|
"duplicate add updates deadline metadata");
|
|
Check(timer_event.data == 0 && timer_event.udata == reinterpret_cast<void*>(0x2222),
|
|
"deadline trigger retains updated duplicate metadata");
|
|
|
|
auto retained_owner = weak_original.lock();
|
|
Check(retained_owner != nullptr, "original owner alive before delete");
|
|
Check(EventQueue::KernelDeleteEvent(queue, 17, EventQueue::KERNEL_EVFILT_VIDEO_OUT) == OK,
|
|
"delete duplicate event");
|
|
Check(retained_owner->delete_count.load(std::memory_order_relaxed) == 1,
|
|
"duplicate add preserves delete callback");
|
|
retained_owner.reset();
|
|
Check(weak_original.expired(), "original owner released on delete");
|
|
Check(EventQueue::KernelDeleteEqueue(queue) == OK, "delete duplicate add queue");
|
|
}
|
|
|
|
struct SimulatedVideoOutEventState;
|
|
|
|
struct SimulatedVideoOutRegistration {
|
|
EventQueue::KernelEqueue handle = EventQueue::KERNEL_EQUEUE_INVALID;
|
|
std::shared_ptr<SimulatedVideoOutEventState> state;
|
|
uint64_t marker = 0x123456789abcdef0ull;
|
|
};
|
|
|
|
struct SimulatedVideoOutEventState {
|
|
SimulatedVideoOutEventState(std::atomic_uint32_t& stage, std::atomic_uint32_t& destroy_count)
|
|
: stage(stage), destroy_count(destroy_count) {}
|
|
|
|
~SimulatedVideoOutEventState() { destroy_count.fetch_add(1, std::memory_order_relaxed); }
|
|
|
|
std::mutex mutex;
|
|
std::vector<std::shared_ptr<SimulatedVideoOutRegistration>> queues;
|
|
std::atomic_uint32_t& stage;
|
|
std::atomic_uint32_t& destroy_count;
|
|
uint64_t marker = 0xfedcba9876543210ull;
|
|
};
|
|
|
|
void DetachSimulatedVideoOutEvent(EventQueue::KernelEqueue queue,
|
|
EventQueue::KernelEqueueEvent* event) {
|
|
auto* registration = static_cast<SimulatedVideoOutRegistration*>(event->filter.data);
|
|
Check(registration != nullptr && registration->handle == queue,
|
|
"simulated registration identity");
|
|
auto state = registration->state;
|
|
Check(state != nullptr, "simulated event owns shared state");
|
|
state->stage.store(1, std::memory_order_release);
|
|
while (state->stage.load(std::memory_order_acquire) != 2) {
|
|
std::this_thread::yield();
|
|
}
|
|
|
|
{
|
|
std::lock_guard lock(state->mutex);
|
|
const auto entry = std::find_if(
|
|
state->queues.begin(), state->queues.end(),
|
|
[registration](const auto& candidate) { return candidate.get() == registration; });
|
|
if (entry != state->queues.end()) {
|
|
state->queues.erase(entry);
|
|
}
|
|
}
|
|
event->filter.owner.reset();
|
|
Check(state->marker == 0xfedcba9876543210ull && registration->marker == 0x123456789abcdef0ull,
|
|
"callback state survives simulated port destruction");
|
|
}
|
|
|
|
void TestCallbackStateOutlivesPort() {
|
|
EventQueue::KernelEqueue queue = EventQueue::KERNEL_EQUEUE_INVALID;
|
|
Check(EventQueue::KernelCreateEqueue(&queue, "shared-port-state") == OK,
|
|
"create shared port state queue");
|
|
|
|
std::atomic_uint32_t stage {0};
|
|
std::atomic_uint32_t destroy_count {0};
|
|
auto port_state = std::make_shared<SimulatedVideoOutEventState>(stage, destroy_count);
|
|
std::weak_ptr<SimulatedVideoOutEventState> weak_state = port_state;
|
|
auto registration = std::make_shared<SimulatedVideoOutRegistration>();
|
|
std::weak_ptr<SimulatedVideoOutRegistration> weak_registration = registration;
|
|
registration->handle = queue;
|
|
registration->state = port_state;
|
|
port_state->queues.push_back(registration);
|
|
|
|
EventQueue::KernelEqueueEvent event {};
|
|
event.event.ident = 8;
|
|
event.event.filter = EventQueue::KERNEL_EVFILT_VIDEO_OUT;
|
|
event.filter.data = registration.get();
|
|
event.filter.owner = registration;
|
|
event.filter.delete_event_func = DetachSimulatedVideoOutEvent;
|
|
Check(EventQueue::KernelAddEvent(queue, event) == OK, "add shared port state event");
|
|
event.filter.owner.reset();
|
|
|
|
std::jthread close([&] {
|
|
Check(EventQueue::KernelDeleteEqueue(queue) == OK, "delete shared port state queue");
|
|
});
|
|
while (stage.load(std::memory_order_acquire) != 1) {
|
|
std::this_thread::yield();
|
|
}
|
|
|
|
std::vector<std::shared_ptr<SimulatedVideoOutRegistration>> detached;
|
|
{
|
|
std::lock_guard lock(port_state->mutex);
|
|
detached = std::move(port_state->queues);
|
|
}
|
|
registration.reset();
|
|
detached.clear();
|
|
port_state.reset();
|
|
Check(!weak_state.expired(), "callback state outlives simulated port object");
|
|
Check(!weak_registration.expired(), "registration outlives simulated port object");
|
|
|
|
stage.store(2, std::memory_order_release);
|
|
close.join();
|
|
Check(weak_registration.expired(), "detached registration is released");
|
|
Check(weak_state.expired(), "detached event state is released");
|
|
Check(destroy_count.load(std::memory_order_relaxed) == 1,
|
|
"shared event state is destroyed exactly once");
|
|
}
|
|
|
|
struct OwnedCallbackPayload {
|
|
OwnedCallbackPayload(std::atomic_uint32_t& stage, std::atomic_uint32_t& delete_count,
|
|
std::atomic_uint32_t& destroy_count)
|
|
: stage(stage), delete_count(delete_count), destroy_count(destroy_count) {}
|
|
|
|
std::atomic_uint32_t& stage;
|
|
std::atomic_uint32_t& delete_count;
|
|
std::atomic_uint32_t& destroy_count;
|
|
uint64_t marker = 0xc0dec0dec0dec0deull;
|
|
|
|
~OwnedCallbackPayload() { destroy_count.fetch_add(1, std::memory_order_relaxed); }
|
|
};
|
|
|
|
void DeleteOwnedEvent(EventQueue::KernelEqueue queue, EventQueue::KernelEqueueEvent* event) {
|
|
auto* payload = static_cast<OwnedCallbackPayload*>(event->filter.data);
|
|
Check(payload != nullptr, "owned callback payload");
|
|
Check(!EventQueue::KernelPinEqueue(queue), "owned callback runs after registry removal");
|
|
payload->delete_count.fetch_add(1, std::memory_order_relaxed);
|
|
event->filter.owner.reset();
|
|
payload->stage.store(1, std::memory_order_release);
|
|
while (payload->stage.load(std::memory_order_acquire) != 2) {
|
|
std::this_thread::yield();
|
|
}
|
|
Check(payload->marker == 0xc0dec0dec0dec0deull, "owned callback payload remains valid");
|
|
}
|
|
|
|
void TestCallbackOwnsPayload() {
|
|
EventQueue::KernelEqueue queue = EventQueue::KERNEL_EQUEUE_INVALID;
|
|
Check(EventQueue::KernelCreateEqueue(&queue, "owned-callback") == OK,
|
|
"create owned callback queue");
|
|
|
|
std::atomic_uint32_t stage {0};
|
|
std::atomic_uint32_t delete_count {0};
|
|
std::atomic_uint32_t destroy_count {0};
|
|
auto registration = std::make_shared<OwnedCallbackPayload>(stage, delete_count, destroy_count);
|
|
std::weak_ptr<OwnedCallbackPayload> weak_registration = registration;
|
|
std::vector<std::shared_ptr<OwnedCallbackPayload>> port_registrations {registration};
|
|
{
|
|
EventQueue::KernelEqueueEvent event {};
|
|
event.event.ident = 2;
|
|
event.event.filter = EventQueue::KERNEL_EVFILT_VIDEO_OUT;
|
|
event.filter.data = registration.get();
|
|
event.filter.owner = registration;
|
|
event.filter.delete_event_func = DeleteOwnedEvent;
|
|
Check(EventQueue::KernelAddEvent(queue, event) == OK, "add owned callback event");
|
|
}
|
|
|
|
std::jthread close(
|
|
[&] { Check(EventQueue::KernelDeleteEqueue(queue) == OK, "delete owned callback queue"); });
|
|
while (stage.load(std::memory_order_acquire) != 1) {
|
|
std::this_thread::yield();
|
|
}
|
|
|
|
Check(!EventQueue::KernelPinEqueue(queue),
|
|
"owned callback queue removed while callback blocked");
|
|
port_registrations.clear();
|
|
registration.reset();
|
|
Check(!weak_registration.expired(), "delete callback retains detached payload");
|
|
|
|
stage.store(2, std::memory_order_release);
|
|
close.join();
|
|
Check(delete_count.load(std::memory_order_relaxed) == 1, "owned callback runs exactly once");
|
|
Check(weak_registration.expired(), "owned callback payload released with event");
|
|
Check(destroy_count.load(std::memory_order_relaxed) == 1,
|
|
"owned callback payload destroyed exactly once");
|
|
}
|
|
|
|
void TestPinnedClose() {
|
|
EventQueue::KernelEqueue queue = EventQueue::KERNEL_EQUEUE_INVALID;
|
|
Check(EventQueue::KernelCreateEqueue(&queue, "pinned-close") == OK, "create pinned queue");
|
|
|
|
std::atomic_uint32_t delete_count {0};
|
|
EventQueue::KernelEqueueEvent event {};
|
|
event.event.ident = 1;
|
|
event.event.filter = EventQueue::KERNEL_EVFILT_VIDEO_OUT;
|
|
event.filter.data = &delete_count;
|
|
event.filter.delete_event_func = CountDeletedEvent;
|
|
Check(EventQueue::KernelAddEvent(queue, event) == OK, "add callback event");
|
|
|
|
auto owner = EventQueue::KernelPinEqueue(queue);
|
|
Check(owner != nullptr, "pin live queue");
|
|
Check(EventQueue::KernelDeleteEqueue(queue) == OK, "delete pinned queue");
|
|
Check(delete_count.load(std::memory_order_relaxed) == 1, "close invokes callback once");
|
|
Check(!EventQueue::KernelPinEqueue(queue), "deleted queue leaves registry");
|
|
Check(EventQueue::KernelTriggerEvent(queue, 1, EventQueue::KERNEL_EVFILT_VIDEO_OUT, nullptr) ==
|
|
KERNEL_ERROR_EBADF,
|
|
"stale trigger rejected");
|
|
Check(EventQueue::KernelDeleteEqueue(queue) == KERNEL_ERROR_EBADF,
|
|
"second queue delete rejected");
|
|
|
|
owner.reset();
|
|
Check(delete_count.load(std::memory_order_relaxed) == 1,
|
|
"deferred destruction does not repeat callback");
|
|
}
|
|
|
|
void TestStaleHandleNeverAliasesNewQueue() {
|
|
EventQueue::KernelEqueue stale = EventQueue::KERNEL_EQUEUE_INVALID;
|
|
Check(EventQueue::KernelCreateEqueue(&stale, "stale-handle") == OK, "create stale queue");
|
|
Check(EventQueue::KernelDeleteEqueue(stale) == OK, "delete stale queue");
|
|
|
|
EventQueue::KernelEqueue replacement = EventQueue::KERNEL_EQUEUE_INVALID;
|
|
Check(EventQueue::KernelCreateEqueue(&replacement, "replacement") == OK,
|
|
"create replacement queue");
|
|
Check(stale != replacement, "queue handles are never recycled");
|
|
Check(!EventQueue::KernelPinEqueue(stale), "stale handle does not pin");
|
|
Check(EventQueue::KernelAddUserEvent(stale, 11) == KERNEL_ERROR_EBADF,
|
|
"stale handle cannot mutate replacement");
|
|
Check(EventQueue::KernelAddUserEvent(replacement, 11) == OK,
|
|
"replacement handle remains valid");
|
|
Check(EventQueue::KernelTriggerUserEvent(stale, 11, nullptr) == KERNEL_ERROR_EBADF,
|
|
"stale handle cannot trigger replacement");
|
|
Check(EventQueue::KernelTriggerUserEvent(replacement, 11, nullptr) == OK,
|
|
"replacement event triggers");
|
|
Check(EventQueue::KernelDeleteEqueue(replacement) == OK, "delete replacement queue");
|
|
}
|
|
|
|
void TestConcurrentCloseCallback() {
|
|
for (uint32_t iteration = 0; iteration < 64; iteration++) {
|
|
EventQueue::KernelEqueue queue = EventQueue::KERNEL_EQUEUE_INVALID;
|
|
Check(EventQueue::KernelCreateEqueue(&queue, "callback-race") == OK,
|
|
"create callback race queue");
|
|
|
|
std::atomic_uint32_t delete_count {0};
|
|
EventQueue::KernelEqueueEvent callback_event {};
|
|
callback_event.event.ident = 9;
|
|
callback_event.event.filter = EventQueue::KERNEL_EVFILT_GRAPHICS;
|
|
callback_event.filter.data = &delete_count;
|
|
callback_event.filter.delete_event_func = CountDeletedEvent;
|
|
Check(EventQueue::KernelAddEvent(queue, callback_event) == OK, "add callback race event");
|
|
|
|
std::atomic_bool start {false};
|
|
std::jthread trigger([&] {
|
|
while (!start.load(std::memory_order_acquire)) {
|
|
std::this_thread::yield();
|
|
}
|
|
for (uint32_t i = 0; i < 256; i++) {
|
|
CheckConcurrentResult(EventQueue::KernelTriggerEvent(
|
|
queue, 9, EventQueue::KERNEL_EVFILT_GRAPHICS, nullptr),
|
|
"callback race trigger result");
|
|
}
|
|
});
|
|
|
|
start.store(true, std::memory_order_release);
|
|
Check(EventQueue::KernelDeleteEqueue(queue) == OK, "callback race queue delete");
|
|
trigger.join();
|
|
Check(delete_count.load(std::memory_order_relaxed) == 1,
|
|
"concurrent close invokes callback exactly once");
|
|
}
|
|
}
|
|
|
|
void TestConcurrentDelete() {
|
|
for (uint32_t iteration = 0; iteration < 64; iteration++) {
|
|
EventQueue::KernelEqueue queue = EventQueue::KERNEL_EQUEUE_INVALID;
|
|
Check(EventQueue::KernelCreateEqueue(&queue, "concurrent-delete") == OK,
|
|
"create concurrent queue");
|
|
EventQueue::KernelEqueueEvent event {};
|
|
event.event.ident = 7;
|
|
event.event.filter = EventQueue::KERNEL_EVFILT_USER;
|
|
Check(EventQueue::KernelAddEvent(queue, event) == OK, "add concurrent event");
|
|
|
|
std::atomic_bool start {false};
|
|
std::jthread mutate([&] {
|
|
while (!start.load(std::memory_order_acquire)) {
|
|
std::this_thread::yield();
|
|
}
|
|
for (uint32_t i = 0; i < 64; i++) {
|
|
CheckConcurrentResult(EventQueue::KernelAddEvent(queue, event),
|
|
"concurrent add result");
|
|
CheckConcurrentResult(EventQueue::KernelTriggerEvent(
|
|
queue, 7, EventQueue::KERNEL_EVFILT_USER, nullptr),
|
|
"concurrent trigger result");
|
|
CheckConcurrentResult(
|
|
EventQueue::KernelDeleteEvent(queue, 7, EventQueue::KERNEL_EVFILT_USER),
|
|
"concurrent event delete result");
|
|
}
|
|
});
|
|
std::jthread trigger([&] {
|
|
while (!start.load(std::memory_order_acquire)) {
|
|
std::this_thread::yield();
|
|
}
|
|
for (uint32_t i = 0; i < 128; i++) {
|
|
CheckConcurrentResult(EventQueue::KernelTriggerEvent(
|
|
queue, 7, EventQueue::KERNEL_EVFILT_USER, nullptr),
|
|
"parallel trigger result");
|
|
}
|
|
});
|
|
|
|
start.store(true, std::memory_order_release);
|
|
Check(EventQueue::KernelDeleteEqueue(queue) == OK, "concurrent queue delete");
|
|
mutate.join();
|
|
trigger.join();
|
|
Check(!EventQueue::KernelPinEqueue(queue), "concurrent queue removed from registry");
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
int main() {
|
|
TestDuplicateAddPreservesEventState();
|
|
TestCallbackStateOutlivesPort();
|
|
TestCallbackOwnsPayload();
|
|
TestPinnedClose();
|
|
TestStaleHandleNeverAliasesNewQueue();
|
|
TestConcurrentCloseCallback();
|
|
TestConcurrentDelete();
|
|
std::printf("EventQueueLifetimeTests: all cases passed\n");
|
|
return 0;
|
|
}
|