shadPS4/src/core/libraries/kernel/equeue.cpp
georgemoralis 2c6d41cdd4
Races and ThreadSafe issues (#4239)
* used atomics for thread safety

* equeue: Save filter/ident before std:move since we access them out of the locked loop

* fixed kqueues memory leak

* clean storage objects effectively

* fixed memory leak

* fix some races

* fixed race condition
2026-04-09 17:46:36 +03:00

666 lines
23 KiB
C++

// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <thread>
#include <magic_enum/magic_enum.hpp>
#include "common/assert.h"
#include "common/debug.h"
#include "common/logging/log.h"
#include "common/singleton.h"
#include "core/file_sys/fs.h"
#include "core/libraries/kernel/equeue.h"
#include "core/libraries/kernel/kernel.h"
#include "core/libraries/kernel/orbis_error.h"
#include "core/libraries/kernel/posix_error.h"
#include "core/libraries/kernel/time.h"
#include "core/libraries/libs.h"
namespace Libraries::Kernel {
extern boost::asio::io_context io_context;
extern void KernelSignalRequest();
static std::unordered_map<s32, EqueueInternal*> kqueues;
static constexpr auto HrTimerSpinlockThresholdNs = 1200000u;
EqueueInternal* GetEqueue(OrbisKernelEqueue eq) {
if (!kqueues.contains(eq)) {
return nullptr;
}
return kqueues[eq];
}
static void HrTimerCallback(OrbisKernelEqueue eq, const OrbisKernelEvent& kevent) {
if (kqueues.contains(eq)) {
kqueues[eq]->TriggerEvent(kevent.ident, OrbisKernelEvent::Filter::HrTimer, kevent.udata);
}
}
static void TimerCallback(OrbisKernelEqueue eq, const OrbisKernelEvent& kevent) {
if (kqueues.contains(eq) && kqueues[eq]->EventExists(kevent.ident, kevent.filter)) {
kqueues[eq]->TriggerEvent(kevent.ident, OrbisKernelEvent::Filter::Timer, kevent.udata);
if (!(kevent.flags & OrbisKernelEvent::Flags::OneShot)) {
// Reschedule the event for its next period.
kqueues[eq]->ScheduleEvent(kevent.ident, kevent.filter, TimerCallback);
}
}
}
// Events are uniquely identified by id and filter.
bool EqueueInternal::AddEvent(EqueueEvent& event) {
// Save id and filter before event is moved into m_events.
const u64 id = event.event.ident;
const auto filter = event.event.filter;
{
std::scoped_lock lock{m_mutex};
// Calculate timer interval
event.time_added = std::chrono::steady_clock::now();
if (event.event.filter == OrbisKernelEvent::Filter::Timer) {
// Set timer interval, this is stored in milliseconds for timers.
event.timer_interval = std::chrono::milliseconds(event.event.data);
} else if (event.event.filter == OrbisKernelEvent::Filter::HrTimer) {
// Retrieve inputted time, this is stored in the bintime format.
OrbisKernelBintime* time = reinterpret_cast<OrbisKernelBintime*>(event.event.data);
// Convert the bintime format to a timespec.
OrbisKernelTimespec ts;
ts.tv_sec = time->sec;
ts.tv_nsec = (1000000000 * (time->frac >> 32)) >> 32;
// Then use the timespec to set the timer interval.
event.timer_interval = std::chrono::nanoseconds(ts.tv_nsec + ts.tv_sec * 1000000000);
}
// First, check if there's already an event with the same id and filter.
const auto& find_it = std::ranges::find_if(m_events, [id, filter](auto& ev) {
return ev.event.ident == id && ev.event.filter == filter;
});
// If there is a duplicate event, we need to update that instead.
if (find_it != m_events.cend()) {
// Specifically, update user data and timer_interval.
// Trigger status and event data should remain intact.
auto& old_event = *find_it;
old_event.timer_interval = event.timer_interval;
old_event.event.udata = event.event.udata;
return true;
}
// Clear input data from event.
event.event.data = 0;
// Remove add flag from event
event.event.flags &= ~OrbisKernelEvent::Flags::Add;
// Clear flag is appended to most event types internally.
if (event.event.filter != OrbisKernelEvent::Filter::User) {
event.event.flags |= OrbisKernelEvent::Flags::Clear;
}
const auto& it = std::ranges::find(m_events, event);
if (it != m_events.cend()) {
*it = std::move(event);
} else {
m_events.emplace_back(std::move(event));
}
}
// Schedule callbacks for timer events
if (filter == OrbisKernelEvent::Filter::Timer) {
return this->ScheduleEvent(id, OrbisKernelEvent::Filter::Timer, TimerCallback);
} else if (filter == OrbisKernelEvent::Filter::HrTimer) {
return this->ScheduleEvent(id, OrbisKernelEvent::Filter::HrTimer, HrTimerCallback);
}
return true;
}
bool EqueueInternal::ScheduleEvent(u64 id, s16 filter,
void (*callback)(OrbisKernelEqueue, const OrbisKernelEvent&)) {
std::scoped_lock lock{m_mutex};
const auto& it = std::ranges::find_if(m_events, [id, filter](auto& ev) {
return ev.event.ident == id && ev.event.filter == filter;
});
if (it == m_events.cend()) {
return false;
}
const auto& event = *it;
ASSERT(event.event.filter == OrbisKernelEvent::Filter::Timer ||
event.event.filter == OrbisKernelEvent::Filter::HrTimer);
if (!it->timer) {
it->timer = std::make_unique<boost::asio::steady_timer>(io_context, event.timer_interval);
} else {
// If the timer already exists we are scheduling a reoccurrence after the next period.
// Set the expiration time to the previous occurrence plus the period.
it->timer->expires_at(it->timer->expiry() + event.timer_interval);
}
it->timer->async_wait(
[this, event_data = event.event, callback](const boost::system::error_code& ec) {
if (ec) {
if (ec != boost::system::errc::operation_canceled) {
LOG_ERROR(Kernel_Event, "Timer callback error: {}", ec.message());
} else {
// Timer was cancelled (removed) before it triggered
LOG_DEBUG(Kernel_Event, "Timer cancelled");
}
return;
}
callback(this->m_handle, event_data);
});
KernelSignalRequest();
return true;
}
bool EqueueInternal::RemoveEvent(u64 id, s16 filter) {
bool has_found = false;
std::scoped_lock lock{m_mutex};
const auto& it = std::ranges::find_if(m_events, [id, filter](auto& ev) {
return ev.event.ident == id && ev.event.filter == filter;
});
if (it != m_events.cend()) {
m_events.erase(it);
has_found = true;
}
return has_found;
}
int EqueueInternal::WaitForEvents(OrbisKernelEvent* ev, int num, const OrbisKernelUseconds* timo) {
if (timo != nullptr && *timo == 0) {
// Effectively acts as a poll; only events that have already
// arrived at the time of this function call can be received
return GetTriggeredEvents(ev, num);
}
const auto micros = timo ? *timo : 0u;
if (HasSmallTimer()) {
// If a small timer is set, just wait for it to expire.
return WaitForSmallTimer(ev, num, micros);
}
int count = 0;
const auto predicate = [&] {
count = GetTriggeredEvents(ev, num);
return count > 0;
};
if (micros == 0) {
// Wait indefinitely for events
std::unique_lock lock{m_mutex};
m_cond.wait(lock, predicate);
} else {
// Wait up until the timeout value
std::unique_lock lock{m_mutex};
m_cond.wait_for(lock, std::chrono::microseconds(micros), predicate);
}
return count;
}
bool EqueueInternal::TriggerEvent(u64 ident, s16 filter, void* trigger_data) {
bool has_found = false;
{
std::scoped_lock lock{m_mutex};
for (auto& event : m_events) {
if (event.event.ident == ident && event.event.filter == filter) {
if (filter == OrbisKernelEvent::Filter::VideoOut) {
event.TriggerDisplay(trigger_data);
} else if (filter == OrbisKernelEvent::Filter::User) {
event.TriggerUser(trigger_data);
} else if (filter == OrbisKernelEvent::Filter::Timer ||
filter == OrbisKernelEvent::Filter::HrTimer) {
event.TriggerTimer();
} else {
event.Trigger(trigger_data);
}
has_found = true;
}
}
}
m_cond.notify_one();
return has_found;
}
int EqueueInternal::GetTriggeredEvents(OrbisKernelEvent* ev, int num) {
int count = 0;
for (auto it = m_events.begin(); it != m_events.end();) {
if (it->IsTriggered()) {
ev[count++] = it->event;
if (it->event.flags & OrbisKernelEvent::Flags::Clear) {
it->Clear();
}
if (it->event.flags & OrbisKernelEvent::Flags::OneShot) {
it = m_events.erase(it);
} else {
++it;
}
if (count == num) {
break;
}
} else {
++it;
}
}
return count;
}
bool EqueueInternal::AddSmallTimer(EqueueEvent& ev) {
// Retrieve inputted time, this is stored in the bintime format
OrbisKernelBintime* time = reinterpret_cast<OrbisKernelBintime*>(ev.event.data);
OrbisKernelTimespec ts;
ts.tv_sec = time->sec;
ts.tv_nsec = ((1000000000 * (time->frac >> 32)) >> 32);
// Create the small timer
SmallTimer st;
st.event = ev.event;
st.added = std::chrono::steady_clock::now();
st.interval = std::chrono::nanoseconds(ts.tv_nsec + ts.tv_sec * 1000000000);
{
std::scoped_lock lock{m_mutex};
m_small_timers[st.event.ident] = std::move(st);
}
return true;
}
int EqueueInternal::WaitForSmallTimer(OrbisKernelEvent* ev, int num, u32 micros) {
ASSERT(num >= 1);
auto curr_clock = std::chrono::steady_clock::now();
const auto wait_end_us = (micros == 0) ? std::chrono::steady_clock::time_point::max()
: curr_clock + std::chrono::microseconds{micros};
int count = 0;
do {
curr_clock = std::chrono::steady_clock::now();
{
std::scoped_lock lock{m_mutex};
for (auto it = m_small_timers.begin(); it != m_small_timers.end() && count < num;) {
const SmallTimer& st = it->second;
if (curr_clock - st.added >= st.interval) {
ev[count++] = st.event;
it = m_small_timers.erase(it);
} else {
++it;
}
}
if (count > 0)
return count;
}
std::this_thread::yield();
} while (curr_clock < wait_end_us);
return 0;
}
bool EqueueInternal::EventExists(u64 id, s16 filter) {
std::scoped_lock lock{m_mutex};
const auto& it = std::ranges::find_if(m_events, [id, filter](auto& ev) {
return ev.event.ident == id && ev.event.filter == filter;
});
return it != m_events.cend();
}
s32 PS4_SYSV_ABI posix_kqueue() {
// Reserve a file handle for the kqueue
auto* handles = Common::Singleton<Core::FileSys::HandleTable>::Instance();
s32 kqueue_handle = handles->CreateHandle();
auto* kqueue_file = handles->GetFile(kqueue_handle);
kqueue_file->type = Core::FileSys::FileType::Equeue;
// Plenty of equeue logic uses names to identify queues.
// Create a unique name for the queue we create.
char name[32];
memset(name, 0, sizeof(name));
snprintf(name, sizeof(name), "kqueue%i", kqueue_handle);
// Create the queue
kqueues[kqueue_handle] = new EqueueInternal(kqueue_handle, name);
LOG_INFO(Kernel_Event, "kqueue created with name {}", name);
// Return handle.
return kqueue_handle;
}
// Helper method to detect supported filters.
// We don't want to allow adding events we don't handle properly.
bool SupportedEqueueFilter(OrbisKernelEvent::Filter filter) {
return filter == OrbisKernelEvent::Filter::GraphicsCore ||
filter == OrbisKernelEvent::Filter::HrTimer ||
filter == OrbisKernelEvent::Filter::Timer || filter == OrbisKernelEvent::Filter::User ||
filter == OrbisKernelEvent::Filter::VideoOut;
}
s32 PS4_SYSV_ABI posix_kevent(s32 handle, OrbisKernelEvent* changelist, u64 nchanges,
OrbisKernelEvent* eventlist, u64 nevents,
OrbisKernelTimespec* timeout) {
LOG_INFO(Kernel_Event, "called, eq = {}, nchanges = {}, nevents = {}", handle, nchanges,
nevents);
// Get the equeue
if (!kqueues.contains(handle)) {
*__Error() = POSIX_EBADF;
return ORBIS_FAIL;
}
auto equeue = kqueues[handle];
// First step is to apply all changes in changelist.
for (u64 i = 0; i < nchanges; i++) {
auto event = changelist[i];
if (!SupportedEqueueFilter(event.filter)) {
LOG_ERROR(Kernel_Event, "Unsupported event filter {}",
magic_enum::enum_name(event.filter));
continue;
}
// Check the event flags to determine the appropriate action
if (event.flags & OrbisKernelEvent::Flags::Add) {
// The caller is requesting to add an event.
EqueueEvent internal_event{};
internal_event.event = event;
if (!equeue->AddEvent(internal_event)) {
// Failed to add event, return error.
*__Error() = POSIX_ENOMEM;
return ORBIS_FAIL;
}
}
if (event.flags & OrbisKernelEvent::Flags::Delete) {
// The caller is requesting to remove an event.
if (!equeue->RemoveEvent(event.ident, event.filter)) {
// Failed to remove event, return error.
*__Error() = POSIX_ENOENT;
return ORBIS_FAIL;
}
}
if (event.filter == OrbisKernelEvent::Filter::User && event.fflags == 0x1000000) {
// For user events, this fflags value indicates we need to trigger the event.
if (!equeue->TriggerEvent(event.ident, OrbisKernelEvent::Filter::User, event.udata)) {
*__Error() = POSIX_ENOENT;
return ORBIS_FAIL;
}
} else if (event.fflags != 0) {
// The title is using filter-specific flags. Right now, these are unhandled.
LOG_ERROR(Kernel_Event, "Unhandled fflags {:#x} for event filter {}", event.fflags,
magic_enum::enum_name(event.filter));
continue;
}
}
// Now we need to wait on the event list.
s32 count = 0;
if (nevents > 0) {
if (timeout != nullptr) {
OrbisKernelUseconds micros = (timeout->tv_sec * 1000000) + (timeout->tv_nsec / 1000);
count = equeue->WaitForEvents(eventlist, nevents, &micros);
} else {
count = equeue->WaitForEvents(eventlist, nevents, nullptr);
}
}
return count;
}
int PS4_SYSV_ABI sceKernelCreateEqueue(OrbisKernelEqueue* eq, const char* name) {
if (eq == nullptr) {
LOG_ERROR(Kernel_Event, "Event queue is null!");
return ORBIS_KERNEL_ERROR_EINVAL;
}
if (name == nullptr) {
LOG_ERROR(Kernel_Event, "Event queue name is null!");
return ORBIS_KERNEL_ERROR_EINVAL;
}
// Maximum is 32 including null terminator
static constexpr u64 MaxEventQueueNameSize = 32;
if (std::strlen(name) > MaxEventQueueNameSize) {
LOG_ERROR(Kernel_Event, "Event queue name exceeds 32 bytes!");
return ORBIS_KERNEL_ERROR_ENAMETOOLONG;
}
LOG_INFO(Kernel_Event, "name = {}", name);
// Reserve a file handle for the kqueue
auto* handles = Common::Singleton<Core::FileSys::HandleTable>::Instance();
OrbisKernelEqueue kqueue_handle = handles->CreateHandle();
auto* kqueue_file = handles->GetFile(kqueue_handle);
kqueue_file->type = Core::FileSys::FileType::Equeue;
// Create the equeue
kqueues[kqueue_handle] = new EqueueInternal(kqueue_handle, name);
*eq = kqueue_handle;
return ORBIS_OK;
}
int PS4_SYSV_ABI sceKernelDeleteEqueue(OrbisKernelEqueue eq) {
if (!kqueues.contains(eq)) {
return ORBIS_KERNEL_ERROR_EBADF;
}
auto* handles = Common::Singleton<Core::FileSys::HandleTable>::Instance();
handles->DeleteHandle(eq);
delete kqueues[eq];
kqueues.erase(eq);
return ORBIS_OK;
}
int PS4_SYSV_ABI sceKernelWaitEqueue(OrbisKernelEqueue eq, OrbisKernelEvent* ev, int num, int* out,
OrbisKernelUseconds* timo) {
HLE_TRACE;
if (!kqueues.contains(eq)) {
return ORBIS_KERNEL_ERROR_EBADF;
}
auto& equeue = kqueues[eq];
TRACE_HINT(equeue->GetName());
LOG_TRACE(Kernel_Event, "equeue = {} num = {}", equeue->GetName(), num);
if (ev == nullptr) {
return ORBIS_KERNEL_ERROR_EFAULT;
}
if (num < 1) {
*out = 0;
return ORBIS_KERNEL_ERROR_EINVAL;
}
*out = equeue->WaitForEvents(ev, num, timo);
if (*out == 0) {
return ORBIS_KERNEL_ERROR_ETIMEDOUT;
}
return ORBIS_OK;
}
s32 PS4_SYSV_ABI sceKernelAddHRTimerEvent(OrbisKernelEqueue eq, int id, OrbisKernelTimespec* ts,
void* udata) {
if (!kqueues.contains(eq)) {
return ORBIS_KERNEL_ERROR_EBADF;
}
const auto total_ns = ts->tv_sec * 1000000000 + ts->tv_nsec;
EqueueEvent event{};
event.event.ident = id;
event.event.filter = OrbisKernelEvent::Filter::HrTimer;
event.event.flags = OrbisKernelEvent::Flags::Add | OrbisKernelEvent::Flags::OneShot;
event.event.fflags = 0;
// Data is stored as the address of a OrbisKernelBintime struct.
OrbisKernelBintime time{ts->tv_sec, ts->tv_nsec * 0x44b82fa09};
event.event.data = reinterpret_cast<u64>(&time);
event.event.udata = udata;
// HR timers cannot be implemented within the existing event queue architecture due to the
// slowness of the notification mechanism. For instance, a 100us timer will lose its precision
// as the trigger time drifts by +50-700%, depending on the host PC and workload. To address
// this issue, we use a spinlock for small waits (which can be adjusted using
// `HrTimerSpinlockThresholdUs`) and fall back to boost asio timers if the time to tick is
// large. Even for large delays, we truncate a small portion to complete the wait
// using the spinlock, prioritizing precision.
auto& equeue = kqueues[eq];
if (total_ns < HrTimerSpinlockThresholdNs) {
return equeue->AddSmallTimer(event) ? ORBIS_OK : ORBIS_KERNEL_ERROR_ENOMEM;
}
if (!equeue->AddEvent(event)) {
return ORBIS_KERNEL_ERROR_ENOMEM;
}
return ORBIS_OK;
}
int PS4_SYSV_ABI sceKernelDeleteHRTimerEvent(OrbisKernelEqueue eq, int id) {
if (!kqueues.contains(eq)) {
return ORBIS_KERNEL_ERROR_EBADF;
}
auto& equeue = kqueues[eq];
if (equeue->HasSmallTimer()) {
return equeue->RemoveSmallTimer(id) ? ORBIS_OK : ORBIS_KERNEL_ERROR_ENOENT;
} else {
return equeue->RemoveEvent(id, OrbisKernelEvent::Filter::HrTimer)
? ORBIS_OK
: ORBIS_KERNEL_ERROR_ENOENT;
}
}
int PS4_SYSV_ABI sceKernelAddTimerEvent(OrbisKernelEqueue eq, int id, OrbisKernelUseconds usec,
void* udata) {
if (!kqueues.contains(eq)) {
return ORBIS_KERNEL_ERROR_EBADF;
}
EqueueEvent event{};
event.event.ident = static_cast<u64>(id);
event.event.filter = OrbisKernelEvent::Filter::Timer;
event.event.flags = OrbisKernelEvent::Flags::Add;
event.event.fflags = 0;
event.event.data = usec / 1000;
event.event.udata = udata;
auto& equeue = kqueues[eq];
if (!equeue->AddEvent(event)) {
return ORBIS_KERNEL_ERROR_ENOMEM;
}
return ORBIS_OK;
}
int PS4_SYSV_ABI sceKernelDeleteTimerEvent(OrbisKernelEqueue eq, int id) {
if (!kqueues.contains(eq)) {
return ORBIS_KERNEL_ERROR_EBADF;
}
return kqueues[eq]->RemoveEvent(id, OrbisKernelEvent::Filter::Timer)
? ORBIS_OK
: ORBIS_KERNEL_ERROR_ENOENT;
}
int PS4_SYSV_ABI sceKernelAddUserEvent(OrbisKernelEqueue eq, int id) {
if (!kqueues.contains(eq)) {
return ORBIS_KERNEL_ERROR_EBADF;
}
EqueueEvent event{};
event.event.ident = id;
event.event.filter = OrbisKernelEvent::Filter::User;
event.event.udata = 0;
event.event.flags = OrbisKernelEvent::Flags::Add;
event.event.fflags = 0;
event.event.data = 0;
return kqueues[eq]->AddEvent(event) ? ORBIS_OK : ORBIS_KERNEL_ERROR_ENOMEM;
}
int PS4_SYSV_ABI sceKernelAddUserEventEdge(OrbisKernelEqueue eq, int id) {
if (!kqueues.contains(eq)) {
return ORBIS_KERNEL_ERROR_EBADF;
}
EqueueEvent event{};
event.event.ident = id;
event.event.filter = OrbisKernelEvent::Filter::User;
event.event.udata = 0;
event.event.flags = OrbisKernelEvent::Flags::Add | OrbisKernelEvent::Flags::Clear;
event.event.fflags = 0;
event.event.data = 0;
return kqueues[eq]->AddEvent(event) ? ORBIS_OK : ORBIS_KERNEL_ERROR_ENOMEM;
}
void* PS4_SYSV_ABI sceKernelGetEventUserData(const OrbisKernelEvent* ev) {
ASSERT(ev);
return ev->udata;
}
u64 PS4_SYSV_ABI sceKernelGetEventId(const OrbisKernelEvent* ev) {
return ev->ident;
}
int PS4_SYSV_ABI sceKernelTriggerUserEvent(OrbisKernelEqueue eq, int id, void* udata) {
if (!kqueues.contains(eq)) {
return ORBIS_KERNEL_ERROR_EBADF;
}
if (!kqueues[eq]->TriggerEvent(id, OrbisKernelEvent::Filter::User, udata)) {
return ORBIS_KERNEL_ERROR_ENOENT;
}
return ORBIS_OK;
}
int PS4_SYSV_ABI sceKernelDeleteUserEvent(OrbisKernelEqueue eq, int id) {
if (!kqueues.contains(eq)) {
return ORBIS_KERNEL_ERROR_EBADF;
}
if (!kqueues[eq]->RemoveEvent(id, OrbisKernelEvent::Filter::User)) {
return ORBIS_KERNEL_ERROR_ENOENT;
}
return ORBIS_OK;
}
int PS4_SYSV_ABI sceKernelGetEventFilter(const OrbisKernelEvent* ev) {
return ev->filter;
}
u64 PS4_SYSV_ABI sceKernelGetEventData(const OrbisKernelEvent* ev) {
return ev->data;
}
void RegisterEventQueue(Core::Loader::SymbolsResolver* sym) {
LIB_FUNCTION("nh2IFMgKTv8", "libScePosix", 1, "libkernel", posix_kqueue);
LIB_FUNCTION("RW-GEfpnsqg", "libScePosix", 1, "libkernel", posix_kevent);
LIB_FUNCTION("D0OdFMjp46I", "libkernel", 1, "libkernel", sceKernelCreateEqueue);
LIB_FUNCTION("jpFjmgAC5AE", "libkernel", 1, "libkernel", sceKernelDeleteEqueue);
LIB_FUNCTION("fzyMKs9kim0", "libkernel", 1, "libkernel", sceKernelWaitEqueue);
LIB_FUNCTION("vz+pg2zdopI", "libkernel", 1, "libkernel", sceKernelGetEventUserData);
LIB_FUNCTION("4R6-OvI2cEA", "libkernel", 1, "libkernel", sceKernelAddUserEvent);
LIB_FUNCTION("WDszmSbWuDk", "libkernel", 1, "libkernel", sceKernelAddUserEventEdge);
LIB_FUNCTION("R74tt43xP6k", "libkernel", 1, "libkernel", sceKernelAddHRTimerEvent);
LIB_FUNCTION("J+LF6LwObXU", "libkernel", 1, "libkernel", sceKernelDeleteHRTimerEvent);
LIB_FUNCTION("57ZK+ODEXWY", "libkernel", 1, "libkernel", sceKernelAddTimerEvent);
LIB_FUNCTION("YWQFUyXIVdU", "libkernel", 1, "libkernel", sceKernelDeleteTimerEvent);
LIB_FUNCTION("F6e0kwo4cnk", "libkernel", 1, "libkernel", sceKernelTriggerUserEvent);
LIB_FUNCTION("LJDwdSNTnDg", "libkernel", 1, "libkernel", sceKernelDeleteUserEvent);
LIB_FUNCTION("mJ7aghmgvfc", "libkernel", 1, "libkernel", sceKernelGetEventId);
LIB_FUNCTION("23CPPI1tyBY", "libkernel", 1, "libkernel", sceKernelGetEventFilter);
LIB_FUNCTION("kwGyyjohI50", "libkernel", 1, "libkernel", sceKernelGetEventData);
}
} // namespace Libraries::Kernel