From 3377e0cd1e1dd3be525f6d94ad5e79b1a1b04569 Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Sat, 14 Feb 2026 15:40:21 -0600 Subject: [PATCH 01/33] Event tests The start of a rather messy dive into attempting to test most facilities of the PS4, just to see how their events work. --- tests/code/event_test/CMakeLists.txt | 5 + tests/code/event_test/assets/.gitkeep | 0 tests/code/event_test/code/main.cpp | 15 + tests/code/event_test/code/test.cpp | 452 ++++++++++++++++++++++++++ tests/code/event_test/code/test.h | 84 +++++ tests/code/event_test/code/video.h | 117 +++++++ 6 files changed, 673 insertions(+) create mode 100644 tests/code/event_test/CMakeLists.txt create mode 100644 tests/code/event_test/assets/.gitkeep create mode 100644 tests/code/event_test/code/main.cpp create mode 100644 tests/code/event_test/code/test.cpp create mode 100644 tests/code/event_test/code/test.h create mode 100644 tests/code/event_test/code/video.h diff --git a/tests/code/event_test/CMakeLists.txt b/tests/code/event_test/CMakeLists.txt new file mode 100644 index 00000000..2ac28885 --- /dev/null +++ b/tests/code/event_test/CMakeLists.txt @@ -0,0 +1,5 @@ +project(event_test LANGUAGES CXX) + +link_libraries(SceSystemService SceVideoOut) + +create_pkg("EVNT00100" 1 00 "code/main.cpp;code/test.cpp") diff --git a/tests/code/event_test/assets/.gitkeep b/tests/code/event_test/assets/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/tests/code/event_test/code/main.cpp b/tests/code/event_test/code/main.cpp new file mode 100644 index 00000000..87760c58 --- /dev/null +++ b/tests/code/event_test/code/main.cpp @@ -0,0 +1,15 @@ +#include "CppUTest/CommandLineTestRunner.h" + +#include +#include +#include + +IMPORT_TEST_GROUP(EventTest); + +int main(int ac, char** av) { + // No buffering + setvbuf(stdout, NULL, _IONBF, 0); + int result = RUN_ALL_TESTS(ac, av); + sceSystemServiceLoadExec("EXIT", nullptr); + return result; +} diff --git a/tests/code/event_test/code/test.cpp b/tests/code/event_test/code/test.cpp new file mode 100644 index 00000000..55b05023 --- /dev/null +++ b/tests/code/event_test/code/test.cpp @@ -0,0 +1,452 @@ +#include "test.h" + +#include "CppUTest/TestHarness.h" +#include "video.h" + +#include + +TEST_GROUP (EventTest) { + void setup() {} + void teardown() {} +}; + +static void PrintEventData(OrbisKernelEvent* ev) { + printf("ev->ident = 0x%08lx\n", ev->ident); + printf("ev->filter = %hd\n", ev->filter); + printf("ev->flags = %u\n", ev->flags); + printf("ev->fflags = %u\n", ev->fflags); + printf("ev->data = 0x%08lx\n", ev->data); + if (ev->data != 0) { + if (ev->filter == -13) { + // Video out event, cast data appropriately. + VideoOutEventData data = *reinterpret_cast(&ev->data); + printf("ev->data->time = %d\n", data.time); + printf("ev->data->counter = %d\n", data.counter); + printf("ev->data->flip_arg = 0x%08lx\n", data.flip_arg); + } else { + printf("*(ev->data) = 0x%08lx\n", *(u64*)ev->data); + } + } + printf("ev->user_data = 0x%08lx\n", ev->user_data); + if (ev->user_data != 0) { + printf("*(ev->user_data) = 0x%08lx\n", *(u64*)ev->user_data); + } + printf("\n"); +} + +static void PrintFlipStatus(OrbisVideoOutFlipStatus* status) { + printf("status->count = 0x%08lx\n", status->count); + printf("status->process_time = %ld\n", status->process_time); + printf("status->tsc_time = %ld\n", status->tsc_time); + printf("status->flip_arg = 0x%08lx\n", status->flip_arg); + printf("status->submit_tsc = %ld\n", status->submit_tsc); + printf("status->num_gpu_flip_pending = %d\n", status->num_gpu_flip_pending); + printf("status->num_flip_pending = %d\n", status->num_flip_pending); + printf("status->current_buffer = %d\n\n", status->current_buffer); +} + +TEST(EventTest, Test) { + // Need to test some equeue behavior. + // Start by creating an equeue. + OrbisKernelEqueue eq {}; + s32 result = sceKernelCreateEqueue(&eq, "TestEqueue"); + UNSIGNED_INT_EQUALS(0, result); + + // Add a user event to this equeue, use id 32. + result = sceKernelAddUserEvent(eq, 32); + UNSIGNED_INT_EQUALS(0, result); + + // Trigger the event + u64 data1 = 100; + result = sceKernelTriggerUserEvent(eq, 32, &data1); + UNSIGNED_INT_EQUALS(0, result); + + // Run sceKernelWaitEqueue to detect the returned event. + OrbisKernelEvent ev {}; + s32 count {}; + result = sceKernelWaitEqueue(eq, &ev, 1, &count, nullptr); + UNSIGNED_INT_EQUALS(0, result); + CHECK_EQUAL(1, count); + + // Check returned data + PrintEventData(&ev); + CHECK_EQUAL(32, ev.ident); + CHECK_EQUAL(-11, ev.filter); + CHECK_EQUAL(0, ev.flags); + CHECK_EQUAL(0, ev.fflags); + CHECK(ev.data == 0); + CHECK(ev.user_data != 0); + CHECK_EQUAL(100, *(u64*)ev.user_data); + + // Run sceKernelWaitEqueue to detect the returned event. + // Since user events don't clear, this will return the data from the first trigger. + memset(&ev, 0, sizeof(ev)); + count = 0; + result = sceKernelWaitEqueue(eq, &ev, 1, &count, nullptr); + UNSIGNED_INT_EQUALS(0, result); + CHECK_EQUAL(1, count); + + // Check returned data + PrintEventData(&ev); + CHECK_EQUAL(32, ev.ident); + CHECK_EQUAL(-11, ev.filter); + CHECK_EQUAL(0, ev.flags); + CHECK_EQUAL(0, ev.fflags); + CHECK(ev.data == 0); + CHECK(ev.user_data != 0); + CHECK_EQUAL(100, *(u64*)ev.user_data); + + // Re-trigger the event, this should update userdata + u64 data2 = 200; + result = sceKernelTriggerUserEvent(eq, 32, &data2); + UNSIGNED_INT_EQUALS(0, result); + + // Run sceKernelWaitEqueue to detect the returned event. + memset(&ev, 0, sizeof(ev)); + count = 0; + result = sceKernelWaitEqueue(eq, &ev, 1, &count, nullptr); + UNSIGNED_INT_EQUALS(0, result); + CHECK_EQUAL(1, count); + + // Check returned data + PrintEventData(&ev); + CHECK_EQUAL(32, ev.ident); + CHECK_EQUAL(-11, ev.filter); + CHECK_EQUAL(0, ev.flags); + CHECK_EQUAL(0, ev.fflags); + CHECK(ev.data == 0); + CHECK(ev.user_data != 0); + CHECK_EQUAL(200, *(u64*)ev.user_data); + + // Run sceKernelWaitEqueue to detect the returned event. + memset(&ev, 0, sizeof(ev)); + count = 0; + // Succeeds, but only returns 1 event, since there's only one triggered event to return. + result = sceKernelWaitEqueue(eq, &ev, 2, &count, nullptr); + UNSIGNED_INT_EQUALS(0, result); + CHECK_EQUAL(1, count); + + // Check returned data + PrintEventData(&ev); + CHECK_EQUAL(32, ev.ident); + CHECK_EQUAL(-11, ev.filter); + CHECK_EQUAL(0, ev.flags); + CHECK_EQUAL(0, ev.fflags); + CHECK(ev.data == 0); + CHECK(ev.user_data != 0); + CHECK_EQUAL(200, *(u64*)ev.user_data); + + // Delete the user event. + result = sceKernelDeleteUserEvent(eq, 32); + UNSIGNED_INT_EQUALS(0, result); + + // Add a "user event edge", these are user events with the clear flag. + // Presumably, these will not trigger multiple times. + // Add a user event to this equeue, use id 32. + result = sceKernelAddUserEventEdge(eq, 32); + UNSIGNED_INT_EQUALS(0, result); + + // Trigger the event + result = sceKernelTriggerUserEvent(eq, 32, &data1); + UNSIGNED_INT_EQUALS(0, result); + + // Run sceKernelWaitEqueue to detect the returned event. + memset(&ev, 0, sizeof(ev)); + count = 0; + result = sceKernelWaitEqueue(eq, &ev, 1, &count, nullptr); + UNSIGNED_INT_EQUALS(0, result); + CHECK_EQUAL(1, count); + + // Check returned data + PrintEventData(&ev); + CHECK_EQUAL(32, ev.ident); + CHECK_EQUAL(-11, ev.filter); + CHECK_EQUAL(32, ev.flags); + CHECK_EQUAL(0, ev.fflags); + CHECK(ev.data == 0); + CHECK(ev.user_data != 0); + CHECK_EQUAL(100, *(u64*)ev.user_data); + + // Run sceKernelWaitEqueue to detect the returned event. + // Due to the clear flag, this should time out. + memset(&ev, 0, sizeof(ev)); + count = 0; + u32 timeout = 100; + result = sceKernelWaitEqueue(eq, &ev, 1, &count, &timeout); + UNSIGNED_INT_EQUALS(ORBIS_KERNEL_ERROR_ETIMEDOUT, result); + + // Re-trigger the event, this should update userdata + result = sceKernelTriggerUserEvent(eq, 32, &data2); + UNSIGNED_INT_EQUALS(0, result); + + // Run sceKernelWaitEqueue to detect the returned event. + memset(&ev, 0, sizeof(ev)); + count = 0; + result = sceKernelWaitEqueue(eq, &ev, 1, &count, nullptr); + UNSIGNED_INT_EQUALS(0, result); + CHECK_EQUAL(1, count); + + // Check returned data + PrintEventData(&ev); + CHECK_EQUAL(32, ev.ident); + CHECK_EQUAL(-11, ev.filter); + CHECK_EQUAL(32, ev.flags); + CHECK_EQUAL(0, ev.fflags); + CHECK(ev.data == 0); + CHECK(ev.user_data != 0); + CHECK_EQUAL(200, *(u64*)ev.user_data); + + // Run sceKernelWaitEqueue to detect the returned event. + memset(&ev, 0, sizeof(ev)); + count = 0; + timeout = 100; + // Fails, since there isn't any event to return. + result = sceKernelWaitEqueue(eq, &ev, 2, &count, &timeout); + UNSIGNED_INT_EQUALS(ORBIS_KERNEL_ERROR_ETIMEDOUT, result); + + // Trigger the event twice, check behavior. + result = sceKernelTriggerUserEvent(eq, 32, &data1); + UNSIGNED_INT_EQUALS(0, result); + result = sceKernelTriggerUserEvent(eq, 32, &data2); + UNSIGNED_INT_EQUALS(0, result); + + // Run sceKernelWaitEqueue to detect the returned event. + memset(&ev, 0, sizeof(ev)); + count = 0; + result = sceKernelWaitEqueue(eq, &ev, 1, &count, nullptr); + UNSIGNED_INT_EQUALS(0, result); + CHECK_EQUAL(1, count); + + // Check returned data + PrintEventData(&ev); + CHECK_EQUAL(32, ev.ident); + CHECK_EQUAL(-11, ev.filter); + CHECK_EQUAL(32, ev.flags); + CHECK_EQUAL(0, ev.fflags); + CHECK(ev.data == 0); + CHECK(ev.user_data != 0); + CHECK_EQUAL(200, *(u64*)ev.user_data); + + // Even though we trigger twice, this call will fail. + memset(&ev, 0, sizeof(ev)); + count = 0; + timeout = 100; + result = sceKernelWaitEqueue(eq, &ev, 1, &count, &timeout); + UNSIGNED_INT_EQUALS(ORBIS_KERNEL_ERROR_ETIMEDOUT, result); + + // Delete the user event. + result = sceKernelDeleteUserEvent(eq, 32); + UNSIGNED_INT_EQUALS(0, result); + + // Delete the equeue when tests complete. + result = sceKernelDeleteEqueue(eq); + UNSIGNED_INT_EQUALS(0, result); + + // Test video out flip events. + // First we need to properly open libSceVideoOut + VideoOut* handle = new VideoOut(1920, 1080); + + // Register buffers + handle->addBuffer(); + handle->addBuffer(); + handle->addBuffer(); + + OrbisVideoOutFlipStatus status {}; + result = handle->getStatus(&status); + UNSIGNED_INT_EQUALS(0, result); + + // Create a flip event + result = handle->addFlipEvent(nullptr); + UNSIGNED_INT_EQUALS(0, result); + + // Not sure what we're dealing with, so from here, start logging info. + PrintFlipStatus(&status); + + // Perform a flip + result = handle->flipFrame(0x100); + UNSIGNED_INT_EQUALS(0, result); + + // Now we can wait on the flip event equeue prepared earlier. + memset(&ev, 0, sizeof(ev)); + count = 0; + result = handle->waitFlipEvent(&ev, &count, nullptr); + UNSIGNED_INT_EQUALS(0, result); + CHECK_EQUAL(1, count); + + // Check returned data + PrintEventData(&ev); + // CHECK_EQUAL(0x6000000000000, ev.ident); + CHECK_EQUAL(-13, ev.filter); + CHECK_EQUAL(32, ev.flags); + CHECK_EQUAL(0, ev.fflags); + CHECK(ev.data != 0); + VideoOutEventData ev_data = *reinterpret_cast(&ev.data); + CHECK_EQUAL(1, ev_data.counter); + CHECK_EQUAL(0x100, ev_data.flip_arg); + + // Flip events only trigger once. + memset(&ev, 0, sizeof(ev)); + count = 0; + timeout = 1000; + result = handle->waitFlipEvent(&ev, &count, &timeout); + UNSIGNED_INT_EQUALS(ORBIS_KERNEL_ERROR_ETIMEDOUT, result); + + // Check flip status + memset(&status, 0, sizeof(status)); + result = handle->getStatus(&status); + UNSIGNED_INT_EQUALS(0, result); + + PrintFlipStatus(&status); + + // Perform a flip + result = handle->flipFrame(0x200); + UNSIGNED_INT_EQUALS(0, result); + + // Now we can wait on the flip event equeue prepared earlier. + memset(&ev, 0, sizeof(ev)); + count = 0; + result = handle->waitFlipEvent(&ev, &count, nullptr); + UNSIGNED_INT_EQUALS(0, result); + CHECK_EQUAL(1, count); + + // Check returned data + PrintEventData(&ev); + // CHECK_EQUAL(0x6000000000000, ev.ident); + CHECK_EQUAL(-13, ev.filter); + CHECK_EQUAL(32, ev.flags); + CHECK_EQUAL(0, ev.fflags); + CHECK(ev.data != 0); + ev_data = *reinterpret_cast(&ev.data); + CHECK_EQUAL(1, ev_data.counter); + CHECK_EQUAL(0x200, ev_data.flip_arg); + + // Flip events only trigger once. + memset(&ev, 0, sizeof(ev)); + count = 0; + timeout = 1000; + result = handle->waitFlipEvent(&ev, &count, &timeout); + UNSIGNED_INT_EQUALS(ORBIS_KERNEL_ERROR_ETIMEDOUT, result); + + // Check flip status + memset(&status, 0, sizeof(status)); + result = handle->getStatus(&status); + UNSIGNED_INT_EQUALS(0, result); + + PrintFlipStatus(&status); + + // Fire two video out events in quick succession + result = handle->flipFrame(0x100); + UNSIGNED_INT_EQUALS(0, result); + + result = handle->getStatus(&status); + UNSIGNED_INT_EQUALS(0, result); + PrintFlipStatus(&status); + + result = handle->flipFrame(0x300); + UNSIGNED_INT_EQUALS(0, result); + + result = handle->getStatus(&status); + UNSIGNED_INT_EQUALS(0, result); + PrintFlipStatus(&status); + + // Now we can wait on the flip event equeue prepared earlier. + memset(&ev, 0, sizeof(ev)); + count = 0; + result = handle->waitFlipEvent(&ev, &count, nullptr); + UNSIGNED_INT_EQUALS(0, result); + CHECK_EQUAL(1, count); + + // Check returned data + PrintEventData(&ev); + // CHECK_EQUAL(0x6000000000000, ev.ident); + CHECK_EQUAL(-13, ev.filter); + CHECK_EQUAL(32, ev.flags); + CHECK_EQUAL(0, ev.fflags); + CHECK(ev.data != 0); + ev_data = *reinterpret_cast(&ev.data); + CHECK_EQUAL(1, ev_data.counter); + CHECK_EQUAL(0x100, ev_data.flip_arg); + + // Check flip status + memset(&status, 0, sizeof(status)); + result = handle->getStatus(&status); + UNSIGNED_INT_EQUALS(0, result); + + PrintFlipStatus(&status); + + // We did two submits, so the video out event should fire again. + memset(&ev, 0, sizeof(ev)); + count = 0; + result = handle->waitFlipEvent(&ev, &count, nullptr); + UNSIGNED_INT_EQUALS(0, result); + CHECK_EQUAL(1, count); + + // Check returned data + PrintEventData(&ev); + // CHECK_EQUAL(0x6000000000000, ev.ident); + CHECK_EQUAL(-13, ev.filter); + CHECK_EQUAL(32, ev.flags); + CHECK_EQUAL(0, ev.fflags); + CHECK(ev.data != 0); + ev_data = *reinterpret_cast(&ev.data); + CHECK_EQUAL(1, ev_data.counter); + CHECK_EQUAL(0x300, ev_data.flip_arg); + + // Check flip status + memset(&status, 0, sizeof(status)); + result = handle->getStatus(&status); + UNSIGNED_INT_EQUALS(0, result); + + PrintFlipStatus(&status); + + // Fire two video out events in quick succession + result = handle->flipFrame(0x400); + UNSIGNED_INT_EQUALS(0, result); + result = handle->flipFrame(0x500); + UNSIGNED_INT_EQUALS(0, result); + + // Use sceVideoOutGetFlipStatus to wait for both flips to complete + memset(&status, 0, sizeof(status)); + result = handle->getStatus(&status); + UNSIGNED_INT_EQUALS(0, result); + while (status.num_flip_pending != 0) { + sceKernelUsleep(1000); + + memset(&status, 0, sizeof(status)); + result = handle->getStatus(&status); + UNSIGNED_INT_EQUALS(0, result); + } + + // Both flips are done, check status and event + PrintFlipStatus(&status); + + // Check returned data + memset(&ev, 0, sizeof(ev)); + count = 0; + result = handle->waitFlipEvent(&ev, &count, nullptr); + UNSIGNED_INT_EQUALS(0, result); + CHECK_EQUAL(1, count); + + PrintEventData(&ev); + // CHECK_EQUAL(0x6000000000000, ev.ident); + CHECK_EQUAL(-13, ev.filter); + CHECK_EQUAL(32, ev.flags); + CHECK_EQUAL(0, ev.fflags); + CHECK(ev.data != 0); + ev_data = *reinterpret_cast(&ev.data); + // Counter is how many times the event was triggered. + CHECK_EQUAL(2, ev_data.counter); + CHECK_EQUAL(0x500, ev_data.flip_arg); + + // Shouldn't trigger again. + memset(&ev, 0, sizeof(ev)); + count = 0; + result = handle->waitFlipEvent(&ev, &count, &timeout); + UNSIGNED_INT_EQUALS(ORBIS_KERNEL_ERROR_ETIMEDOUT, result); + + // Now for the slightly messy part, we need to test EOP flips. + // These are GPU driven flips, which work alongside the CPU driven flips tested earlier. + + // Clean up after test + delete (handle); +} diff --git a/tests/code/event_test/code/test.h b/tests/code/event_test/code/test.h new file mode 100644 index 00000000..ccd68380 --- /dev/null +++ b/tests/code/event_test/code/test.h @@ -0,0 +1,84 @@ +#pragma once + +#include "CppUTest/TestHarness.h" + +#define UNSIGNED_INT_EQUALS(expected, actual) UNSIGNED_LONGS_EQUAL_LOCATION((u32)expected, (u32)actual, NULLPTR, __FILE__, __LINE__) + +using s8 = int8_t; +using s16 = int16_t; +using s32 = int32_t; +using s64 = int64_t; +using u8 = uint8_t; +using u16 = uint16_t; +using u32 = uint32_t; +using u64 = uint64_t; + +extern "C" { + +typedef void* OrbisKernelEqueue; + +struct VideoOutEventData { + u64 time : 12; + u64 counter : 4; + u64 flip_arg : 48; +}; + +struct OrbisKernelEvent { + u64 ident; + s16 filter; + u16 flags; + u32 fflags; + u64 data; + u64 user_data; +}; + +constexpr s32 ORBIS_KERNEL_ERROR_ETIMEDOUT = 0x8002003c; + +s32 sceKernelUsleep(u32); +s32 sceKernelCreateEqueue(OrbisKernelEqueue* eq, const char* name); +s32 sceKernelDeleteEqueue(OrbisKernelEqueue eq); +s32 sceKernelWaitEqueue(OrbisKernelEqueue eq, OrbisKernelEvent* ev, s32 num, s32* out, u32* timeout); +s32 sceKernelAddUserEvent(OrbisKernelEqueue eq, s32 id); +s32 sceKernelAddUserEventEdge(OrbisKernelEqueue eq, s32 id); +s32 sceKernelTriggerUserEvent(OrbisKernelEqueue eq, s32 id, void* user_data); +s32 sceKernelDeleteUserEvent(OrbisKernelEqueue eq, s32 id); + +s32 sceKernelAllocateMainDirectMemory(u64 size, u64 align, s32 mtype, s64* phys_out); +s32 sceKernelMapDirectMemory(void** addr, u64 size, s32 prot, s32 flags, s64 offset, u64 align); +s32 sceKernelReleaseDirectMemory(s64 phys_addr, u64 size); + +struct OrbisVideoOutFlipStatus { + u64 count; + u64 process_time; + u64 tsc_time; + s64 flip_arg; + u64 submit_tsc; + u64 reserved0; + s32 num_gpu_flip_pending; + s32 num_flip_pending; + s32 current_buffer; + u32 reserved1; +}; + +struct OrbisVideoOutBufferAttribute { + u32 pixel_format; + s32 tiling_mode; + s32 aspect_ratio; + u32 width; + u32 height; + u32 pitch_in_pixel; + u32 option; + u32 reserved0; + u64 reserved1; +}; + +s32 sceVideoOutOpen(s32 user_id, s32 bus_type, s32 index, const void* param); +s32 sceVideoOutSetFlipRate(s32 handle, s32 flip_rate); +s32 sceVideoOutAddFlipEvent(OrbisKernelEqueue eq, s32 handle, void* user_data); +s32 sceVideoOutRegisterBuffers(s32 handle, s32 index, void* const* addrs, s32 buf_num, OrbisVideoOutBufferAttribute* attrs); +s32 sceVideoOutSubmitFlip(s32 handle, s32 buf_index, s32 flip_mode, s64 flip_arg); +s32 sceVideoOutGetFlipStatus(s32 handle, OrbisVideoOutFlipStatus* status); +s32 sceVideoOutIsFlipPending(s32 handle); +s32 sceVideoOutDeleteFlipEvent(OrbisKernelEqueue eq, s32 handle); +s32 sceVideoOutClose(s32 handle); +} \ No newline at end of file diff --git a/tests/code/event_test/code/video.h b/tests/code/event_test/code/video.h new file mode 100644 index 00000000..38665d9b --- /dev/null +++ b/tests/code/event_test/code/video.h @@ -0,0 +1,117 @@ +#pragma once + +#include "test.h" + +#include + +class VideoOut { + private: + s32 handle {0}; + s32 width {0}; + s32 height {0}; + std::vector> phys_bufs {}; + OrbisKernelEqueue flip_queue {nullptr}; + bool flip_event = false; + s32 current_buf = -1; + + public: + VideoOut(s32 width, s32 height) { + this->width = width; + this->height = height; + // Open VideoOut handle + this->handle = sceVideoOutOpen(255, 0, 0, nullptr); + // Set flip rate to 60fps + s32 result = sceVideoOutSetFlipRate(this->handle, 0); + UNSIGNED_INT_EQUALS(0, result); + // Create flip equeue + result = sceKernelCreateEqueue(&flip_queue, "VideoOutFlipEqueue"); + UNSIGNED_INT_EQUALS(0, result); + }; + + // Manually define a destructor to close everything. + ~VideoOut() { + if (flip_event) { + s32 result = sceVideoOutDeleteFlipEvent(flip_queue, handle); + UNSIGNED_INT_EQUALS(0, result); + flip_event = false; + } + if (flip_queue != nullptr) { + s32 result = sceKernelDeleteEqueue(flip_queue); + UNSIGNED_INT_EQUALS(0, result); + } + if (handle > 0) { + s32 result = sceVideoOutClose(handle); + UNSIGNED_INT_EQUALS(0, result); + handle = 0; + } + for (auto& [phys_off, size]: phys_bufs) { + s32 result = sceKernelReleaseDirectMemory(phys_off, size); + UNSIGNED_INT_EQUALS(0, result); + } + phys_bufs.~vector(); + } + + s32 getStatus(OrbisVideoOutFlipStatus* status) { + return sceVideoOutGetFlipStatus(handle, status); + } + + s32 flipFrame(s64 flip_arg) { + s32 result = sceVideoOutSubmitFlip(handle, current_buf, 1, flip_arg); + if (++current_buf == phys_bufs.size()) { + current_buf = -1; + } + return result; + } + + s32 addBuffer() { + // Create a buffer attribute + OrbisVideoOutBufferAttribute attr {}; + attr.pixel_format = 0x80002200; + attr.tiling_mode = 0; + attr.aspect_ratio = 0; + attr.width = width; + attr.height = height; + attr.pitch_in_pixel = width; + attr.option = 0; + attr.reserved0 = 0; + attr.reserved1 = 0; + + // calc some stuff, logic taken from red_prig's shader test homebrew. + u64 pitch = (attr.width + 127) / 128; + u64 pad_width = pitch * 128; + u64 pad_height = ((attr.height + 63) & (~63)); + u64 size = pad_width * pad_height * 4; + u64 aligned_size = (size + 16 * 1024 - 1) & ~(16 * 1024 - 1); + + // Map memory for buffer. + s64 dmem_addr = 0; + s32 result = sceKernelAllocateMainDirectMemory(aligned_size, 64 * 1024, 3, &dmem_addr); + UNSIGNED_INT_EQUALS(0, result); + void* addr = nullptr; + result = sceKernelMapDirectMemory(&addr, aligned_size, 0x33, 0, dmem_addr, 64 * 1024); + UNSIGNED_INT_EQUALS(0, result); + memset(addr, 0, aligned_size); + + // Add buffer info to the buffers list + phys_bufs.emplace_back(dmem_addr, aligned_size); + + // Register buffer + return sceVideoOutRegisterBuffers(handle, phys_bufs.size() - 1, &addr, 1, &attr); + }; + + s32 addFlipEvent(void* user_data) { + s32 result = sceVideoOutAddFlipEvent(flip_queue, handle, user_data); + flip_event = true; + return result; + }; + + s32 deleteFlipEvent() { + s32 result = sceVideoOutDeleteFlipEvent(flip_queue, handle); + flip_event = false; + return result; + }; + + s32 waitFlipEvent(OrbisKernelEvent* ev, s32* out, u32* timeout) { + return sceKernelWaitEqueue(flip_queue, ev, 1, out, timeout); + }; +}; \ No newline at end of file From 1c0178056269225a036c16f922f4b952ad43e384 Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Sat, 14 Feb 2026 16:27:07 -0600 Subject: [PATCH 02/33] Implement EOP flips --- tests/code/event_test/CMakeLists.txt | 2 +- tests/code/event_test/code/test.cpp | 35 +++++++++++++++++++++-- tests/code/event_test/code/test.h | 5 ++++ tests/code/event_test/code/video.h | 42 +++++++++++++++++++++------- 4 files changed, 71 insertions(+), 13 deletions(-) diff --git a/tests/code/event_test/CMakeLists.txt b/tests/code/event_test/CMakeLists.txt index 2ac28885..8ada6139 100644 --- a/tests/code/event_test/CMakeLists.txt +++ b/tests/code/event_test/CMakeLists.txt @@ -1,5 +1,5 @@ project(event_test LANGUAGES CXX) -link_libraries(SceSystemService SceVideoOut) +link_libraries(SceSystemService SceVideoOut SceGnmDriver) create_pkg("EVNT00100" 1 00 "code/main.cpp;code/test.cpp") diff --git a/tests/code/event_test/code/test.cpp b/tests/code/event_test/code/test.cpp index 55b05023..31961f60 100644 --- a/tests/code/event_test/code/test.cpp +++ b/tests/code/event_test/code/test.cpp @@ -444,8 +444,39 @@ TEST(EventTest, Test) { result = handle->waitFlipEvent(&ev, &count, &timeout); UNSIGNED_INT_EQUALS(ORBIS_KERNEL_ERROR_ETIMEDOUT, result); - // Now for the slightly messy part, we need to test EOP flips. - // These are GPU driven flips, which work alongside the CPU driven flips tested earlier. + // Now test EOP flips + result = handle->submitAndFlip(0x1000); + UNSIGNED_INT_EQUALS(0, result); + + // Print status + memset(&status, 0, sizeof(status)); + result = handle->getStatus(&status); + UNSIGNED_INT_EQUALS(0, result); + PrintFlipStatus(&status); + + // Wait for EOP flip to occur. + memset(&ev, 0, sizeof(ev)); + count = 0; + result = handle->waitFlipEvent(&ev, &count, nullptr); + UNSIGNED_INT_EQUALS(0, result); + CHECK_EQUAL(1, count); + + PrintEventData(&ev); + // CHECK_EQUAL(0x6000000000000, ev.ident); + CHECK_EQUAL(-13, ev.filter); + CHECK_EQUAL(32, ev.flags); + CHECK_EQUAL(0, ev.fflags); + CHECK(ev.data != 0); + ev_data = *reinterpret_cast(&ev.data); + // Counter is how many times the event was triggered. + CHECK_EQUAL(1, ev_data.counter); + CHECK_EQUAL(0x1000, ev_data.flip_arg); + + // Print status again. + memset(&status, 0, sizeof(status)); + result = handle->getStatus(&status); + UNSIGNED_INT_EQUALS(0, result); + PrintFlipStatus(&status); // Clean up after test delete (handle); diff --git a/tests/code/event_test/code/test.h b/tests/code/event_test/code/test.h index ccd68380..1608ee67 100644 --- a/tests/code/event_test/code/test.h +++ b/tests/code/event_test/code/test.h @@ -81,4 +81,9 @@ s32 sceVideoOutGetFlipStatus(s32 handle, OrbisVideoOutFlipStatus* status); s32 sceVideoOutIsFlipPending(s32 handle); s32 sceVideoOutDeleteFlipEvent(OrbisKernelEqueue eq, s32 handle); s32 sceVideoOutClose(s32 handle); + +u32 sceGnmDrawInitDefaultHardwareState350(u32* cmd_buf, u32 num_dwords); +s32 sceGnmSubmitAndFlipCommandBuffers(u32 count, void** dcb_gpu_addrs, u32* dcb_sizes_in_bytes, void** ccb_gpu_addrs, u32* ccb_sizes_in_bytes, s32 video_handle, + s32 buffer_index, s32 flip_mode, s64 flip_arg); +s32 sceGnmSubmitDone(); } \ No newline at end of file diff --git a/tests/code/event_test/code/video.h b/tests/code/event_test/code/video.h index 38665d9b..e5fc0370 100644 --- a/tests/code/event_test/code/video.h +++ b/tests/code/event_test/code/video.h @@ -11,8 +11,8 @@ class VideoOut { s32 height {0}; std::vector> phys_bufs {}; OrbisKernelEqueue flip_queue {nullptr}; - bool flip_event = false; - s32 current_buf = -1; + bool flip_event = false; + s32 current_buf = -1; public: VideoOut(s32 width, s32 height) { @@ -49,11 +49,9 @@ class VideoOut { UNSIGNED_INT_EQUALS(0, result); } phys_bufs.~vector(); - } + }; - s32 getStatus(OrbisVideoOutFlipStatus* status) { - return sceVideoOutGetFlipStatus(handle, status); - } + s32 getStatus(OrbisVideoOutFlipStatus* status) { return sceVideoOutGetFlipStatus(handle, status); }; s32 flipFrame(s64 flip_arg) { s32 result = sceVideoOutSubmitFlip(handle, current_buf, 1, flip_arg); @@ -61,7 +59,33 @@ class VideoOut { current_buf = -1; } return result; - } + }; + + s32 submitAndFlip(s64 flip_arg) { + s64 cmd_dmem_ptr = 0; + s32 result = sceKernelAllocateMainDirectMemory(0x4000, 0x4000, 0, &cmd_dmem_ptr); + UNSIGNED_INT_EQUALS(0, result); + + void* cmd_ptr = nullptr; + result = sceKernelMapDirectMemory(&cmd_ptr, 0x4000, 0x33, 0, cmd_dmem_ptr, 0x4000); + UNSIGNED_INT_EQUALS(0, result); + + // Write GPU init packet to the pointer. + u32* cmds = (u32*)cmd_ptr; + cmds += sceGnmDrawInitDefaultHardwareState350(cmds, 0x100); + + // Write a flip packet to the pointer. + cmds[0] = 0xc03e1000; + cmds[1] = 0x68750777; + cmds += 64; + u32 stream_size = (u32)((u64)cmds - (u64)cmd_ptr); + + result = sceGnmSubmitAndFlipCommandBuffers(1, &cmd_ptr, &stream_size, nullptr, nullptr, handle, current_buf, 1, flip_arg); + if (++current_buf == phys_bufs.size()) { + current_buf = -1; + } + return result; + }; s32 addBuffer() { // Create a buffer attribute @@ -111,7 +135,5 @@ class VideoOut { return result; }; - s32 waitFlipEvent(OrbisKernelEvent* ev, s32* out, u32* timeout) { - return sceKernelWaitEqueue(flip_queue, ev, 1, out, timeout); - }; + s32 waitFlipEvent(OrbisKernelEvent* ev, s32* out, u32* timeout) { return sceKernelWaitEqueue(flip_queue, ev, 1, out, timeout); }; }; \ No newline at end of file From d6c35870f98b994a14170977a069fa605b75a183 Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Sun, 15 Feb 2026 12:09:22 -0600 Subject: [PATCH 03/33] Separate UserEvent tests from FlipEvent tests Also some fixes for my flip logic code. --- tests/code/event_test/code/test.cpp | 64 +++++++++++++++++++++++------ tests/code/event_test/code/test.h | 1 + tests/code/event_test/code/video.h | 18 +++++++- 3 files changed, 69 insertions(+), 14 deletions(-) diff --git a/tests/code/event_test/code/test.cpp b/tests/code/event_test/code/test.cpp index 31961f60..3b298e7e 100644 --- a/tests/code/event_test/code/test.cpp +++ b/tests/code/event_test/code/test.cpp @@ -45,7 +45,7 @@ static void PrintFlipStatus(OrbisVideoOutFlipStatus* status) { printf("status->current_buffer = %d\n\n", status->current_buffer); } -TEST(EventTest, Test) { +TEST(EventTest, UserEventTest) { // Need to test some equeue behavior. // Start by creating an equeue. OrbisKernelEqueue eq {}; @@ -241,7 +241,9 @@ TEST(EventTest, Test) { // Delete the equeue when tests complete. result = sceKernelDeleteEqueue(eq); UNSIGNED_INT_EQUALS(0, result); +} +TEST(EventTest, FlipEventTest) { // Test video out flip events. // First we need to properly open libSceVideoOut VideoOut* handle = new VideoOut(1920, 1080); @@ -252,7 +254,7 @@ TEST(EventTest, Test) { handle->addBuffer(); OrbisVideoOutFlipStatus status {}; - result = handle->getStatus(&status); + s32 result = handle->getStatus(&status); UNSIGNED_INT_EQUALS(0, result); // Create a flip event @@ -267,15 +269,16 @@ TEST(EventTest, Test) { UNSIGNED_INT_EQUALS(0, result); // Now we can wait on the flip event equeue prepared earlier. + OrbisKernelEvent ev{}; memset(&ev, 0, sizeof(ev)); - count = 0; + s32 count = 0; result = handle->waitFlipEvent(&ev, &count, nullptr); UNSIGNED_INT_EQUALS(0, result); CHECK_EQUAL(1, count); // Check returned data PrintEventData(&ev); - // CHECK_EQUAL(0x6000000000000, ev.ident); + CHECK_EQUAL(0x6000000000000, ev.ident); CHECK_EQUAL(-13, ev.filter); CHECK_EQUAL(32, ev.flags); CHECK_EQUAL(0, ev.fflags); @@ -287,7 +290,7 @@ TEST(EventTest, Test) { // Flip events only trigger once. memset(&ev, 0, sizeof(ev)); count = 0; - timeout = 1000; + u32 timeout = 1000; result = handle->waitFlipEvent(&ev, &count, &timeout); UNSIGNED_INT_EQUALS(ORBIS_KERNEL_ERROR_ETIMEDOUT, result); @@ -311,7 +314,7 @@ TEST(EventTest, Test) { // Check returned data PrintEventData(&ev); - // CHECK_EQUAL(0x6000000000000, ev.ident); + CHECK_EQUAL(0x6000000000000, ev.ident); CHECK_EQUAL(-13, ev.filter); CHECK_EQUAL(32, ev.flags); CHECK_EQUAL(0, ev.fflags); @@ -358,7 +361,7 @@ TEST(EventTest, Test) { // Check returned data PrintEventData(&ev); - // CHECK_EQUAL(0x6000000000000, ev.ident); + CHECK_EQUAL(0x6000000000000, ev.ident); CHECK_EQUAL(-13, ev.filter); CHECK_EQUAL(32, ev.flags); CHECK_EQUAL(0, ev.fflags); @@ -383,7 +386,7 @@ TEST(EventTest, Test) { // Check returned data PrintEventData(&ev); - // CHECK_EQUAL(0x6000000000000, ev.ident); + CHECK_EQUAL(0x6000000000000, ev.ident); CHECK_EQUAL(-13, ev.filter); CHECK_EQUAL(32, ev.flags); CHECK_EQUAL(0, ev.fflags); @@ -428,7 +431,7 @@ TEST(EventTest, Test) { CHECK_EQUAL(1, count); PrintEventData(&ev); - // CHECK_EQUAL(0x6000000000000, ev.ident); + CHECK_EQUAL(0x6000000000000, ev.ident); CHECK_EQUAL(-13, ev.filter); CHECK_EQUAL(32, ev.flags); CHECK_EQUAL(0, ev.fflags); @@ -447,13 +450,50 @@ TEST(EventTest, Test) { // Now test EOP flips result = handle->submitAndFlip(0x1000); UNSIGNED_INT_EQUALS(0, result); + result = handle->submitAndFlip(0x2000); + UNSIGNED_INT_EQUALS(0, result); + result = handle->submitAndFlip(0x3000); + UNSIGNED_INT_EQUALS(0, result); // Print status + do { + memset(&status, 0, sizeof(status)); + result = handle->getStatus(&status); + PrintFlipStatus(&status); + UNSIGNED_INT_EQUALS(0, result); + + sceKernelUsleep(1000); + } while (status.num_flip_pending != 0); + + PrintFlipStatus(&status); + + // Wait for EOP flip to occur. + memset(&ev, 0, sizeof(ev)); + count = 0; + result = handle->waitFlipEvent(&ev, &count, nullptr); + UNSIGNED_INT_EQUALS(0, result); + CHECK_EQUAL(1, count); + + PrintEventData(&ev); + CHECK_EQUAL(0x6000000000000, ev.ident); + CHECK_EQUAL(-13, ev.filter); + CHECK_EQUAL(32, ev.flags); + CHECK_EQUAL(0, ev.fflags); + CHECK(ev.data != 0); + ev_data = *reinterpret_cast(&ev.data); + // Counter is how many times the event was triggered. + CHECK_EQUAL(3, ev_data.counter); + CHECK_EQUAL(0x3000, ev_data.flip_arg); + + // Print status again. memset(&status, 0, sizeof(status)); result = handle->getStatus(&status); UNSIGNED_INT_EQUALS(0, result); PrintFlipStatus(&status); + result = handle->submitAndFlip(0x30000000000); + UNSIGNED_INT_EQUALS(0, result); + // Wait for EOP flip to occur. memset(&ev, 0, sizeof(ev)); count = 0; @@ -462,15 +502,15 @@ TEST(EventTest, Test) { CHECK_EQUAL(1, count); PrintEventData(&ev); - // CHECK_EQUAL(0x6000000000000, ev.ident); + CHECK_EQUAL(0x6000000000000, ev.ident); CHECK_EQUAL(-13, ev.filter); CHECK_EQUAL(32, ev.flags); CHECK_EQUAL(0, ev.fflags); CHECK(ev.data != 0); ev_data = *reinterpret_cast(&ev.data); // Counter is how many times the event was triggered. - CHECK_EQUAL(1, ev_data.counter); - CHECK_EQUAL(0x1000, ev_data.flip_arg); + CHECK_EQUAL(3, ev_data.counter); + CHECK_EQUAL(0x30000000000, ev_data.flip_arg); // Print status again. memset(&status, 0, sizeof(status)); diff --git a/tests/code/event_test/code/test.h b/tests/code/event_test/code/test.h index 1608ee67..fc55c4ce 100644 --- a/tests/code/event_test/code/test.h +++ b/tests/code/event_test/code/test.h @@ -76,6 +76,7 @@ s32 sceVideoOutOpen(s32 user_id, s32 bus_type, s32 index, const void* param); s32 sceVideoOutSetFlipRate(s32 handle, s32 flip_rate); s32 sceVideoOutAddFlipEvent(OrbisKernelEqueue eq, s32 handle, void* user_data); s32 sceVideoOutRegisterBuffers(s32 handle, s32 index, void* const* addrs, s32 buf_num, OrbisVideoOutBufferAttribute* attrs); +s32 sceVideoOutUnregisterBuffers(s32 handle, s32 attribute_index); s32 sceVideoOutSubmitFlip(s32 handle, s32 buf_index, s32 flip_mode, s64 flip_arg); s32 sceVideoOutGetFlipStatus(s32 handle, OrbisVideoOutFlipStatus* status); s32 sceVideoOutIsFlipPending(s32 handle); diff --git a/tests/code/event_test/code/video.h b/tests/code/event_test/code/video.h index e5fc0370..afbff055 100644 --- a/tests/code/event_test/code/video.h +++ b/tests/code/event_test/code/video.h @@ -54,14 +54,22 @@ class VideoOut { s32 getStatus(OrbisVideoOutFlipStatus* status) { return sceVideoOutGetFlipStatus(handle, status); }; s32 flipFrame(s64 flip_arg) { + if (phys_bufs.size() == 0) { + // Force a blank frame if no buffers are registered + current_buf = -1; + } s32 result = sceVideoOutSubmitFlip(handle, current_buf, 1, flip_arg); if (++current_buf == phys_bufs.size()) { - current_buf = -1; + current_buf = 0; } return result; }; s32 submitAndFlip(s64 flip_arg) { + if (phys_bufs.size() == 0) { + // SubmitAndFlip fails on buffer -1, save time by failing early. + return -1; + } s64 cmd_dmem_ptr = 0; s32 result = sceKernelAllocateMainDirectMemory(0x4000, 0x4000, 0, &cmd_dmem_ptr); UNSIGNED_INT_EQUALS(0, result); @@ -80,9 +88,10 @@ class VideoOut { cmds += 64; u32 stream_size = (u32)((u64)cmds - (u64)cmd_ptr); + // GPU flips are not allowed on buffer -1. result = sceGnmSubmitAndFlipCommandBuffers(1, &cmd_ptr, &stream_size, nullptr, nullptr, handle, current_buf, 1, flip_arg); if (++current_buf == phys_bufs.size()) { - current_buf = -1; + current_buf = 0; } return result; }; @@ -123,6 +132,11 @@ class VideoOut { return sceVideoOutRegisterBuffers(handle, phys_bufs.size() - 1, &addr, 1, &attr); }; + s32 removeBuffers() { + current_buf = -1; + return sceVideoOutUnregisterBuffers(handle, 0); + } + s32 addFlipEvent(void* user_data) { s32 result = sceVideoOutAddFlipEvent(flip_queue, handle, user_data); flip_event = true; From de428f86d874ee849d77ee471c2acd9fc26c1fc9 Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Sun, 15 Feb 2026 12:34:54 -0600 Subject: [PATCH 04/33] Slight code cleanup --- tests/code/event_test/code/test.cpp | 16 +++---- tests/code/event_test/code/video.h | 69 ++++++++++++++++++++--------- 2 files changed, 55 insertions(+), 30 deletions(-) diff --git a/tests/code/event_test/code/test.cpp b/tests/code/event_test/code/test.cpp index 3b298e7e..23c03d84 100644 --- a/tests/code/event_test/code/test.cpp +++ b/tests/code/event_test/code/test.cpp @@ -254,7 +254,7 @@ TEST(EventTest, FlipEventTest) { handle->addBuffer(); OrbisVideoOutFlipStatus status {}; - s32 result = handle->getStatus(&status); + s32 result = handle->getStatus(&status); UNSIGNED_INT_EQUALS(0, result); // Create a flip event @@ -269,10 +269,10 @@ TEST(EventTest, FlipEventTest) { UNSIGNED_INT_EQUALS(0, result); // Now we can wait on the flip event equeue prepared earlier. - OrbisKernelEvent ev{}; + OrbisKernelEvent ev {}; memset(&ev, 0, sizeof(ev)); - s32 count = 0; - result = handle->waitFlipEvent(&ev, &count, nullptr); + s32 count = 0; + result = handle->waitFlipEvent(&ev, &count, nullptr); UNSIGNED_INT_EQUALS(0, result); CHECK_EQUAL(1, count); @@ -289,9 +289,9 @@ TEST(EventTest, FlipEventTest) { // Flip events only trigger once. memset(&ev, 0, sizeof(ev)); - count = 0; + count = 0; u32 timeout = 1000; - result = handle->waitFlipEvent(&ev, &count, &timeout); + result = handle->waitFlipEvent(&ev, &count, &timeout); UNSIGNED_INT_EQUALS(ORBIS_KERNEL_ERROR_ETIMEDOUT, result); // Check flip status @@ -462,7 +462,7 @@ TEST(EventTest, FlipEventTest) { PrintFlipStatus(&status); UNSIGNED_INT_EQUALS(0, result); - sceKernelUsleep(1000); + sceKernelUsleep(10000); } while (status.num_flip_pending != 0); PrintFlipStatus(&status); @@ -509,7 +509,7 @@ TEST(EventTest, FlipEventTest) { CHECK(ev.data != 0); ev_data = *reinterpret_cast(&ev.data); // Counter is how many times the event was triggered. - CHECK_EQUAL(3, ev_data.counter); + CHECK_EQUAL(1, ev_data.counter); CHECK_EQUAL(0x30000000000, ev_data.flip_arg); // Print status again. diff --git a/tests/code/event_test/code/video.h b/tests/code/event_test/code/video.h index afbff055..917d6838 100644 --- a/tests/code/event_test/code/video.h +++ b/tests/code/event_test/code/video.h @@ -6,49 +6,78 @@ class VideoOut { private: - s32 handle {0}; - s32 width {0}; - s32 height {0}; - std::vector> phys_bufs {}; - OrbisKernelEqueue flip_queue {nullptr}; - bool flip_event = false; - s32 current_buf = -1; + // Video out data + s32 handle; + s32 width; + s32 height; + s32 current_buf = -1; + + // Buffer allocations + std::vector> phys_bufs; + + // Equeue + OrbisKernelEqueue flip_queue = nullptr; + bool flip_event = false; + + // Command buffer for EOP tests + void* cmd_buf = nullptr; + s64 phys_cmd_buf; + u32 stream_size; public: VideoOut(s32 width, s32 height) { + // Store requested frame width and height this->width = width; this->height = height; + // Open VideoOut handle this->handle = sceVideoOutOpen(255, 0, 0, nullptr); + // Set flip rate to 60fps s32 result = sceVideoOutSetFlipRate(this->handle, 0); UNSIGNED_INT_EQUALS(0, result); + // Create flip equeue result = sceKernelCreateEqueue(&flip_queue, "VideoOutFlipEqueue"); UNSIGNED_INT_EQUALS(0, result); + + // Initialize command buffer for EOP flip tests + result = sceKernelAllocateMainDirectMemory(0x4000, 0x4000, 0, &phys_cmd_buf); + UNSIGNED_INT_EQUALS(0, result); + + result = sceKernelMapDirectMemory(&cmd_buf, 0x4000, 0x33, 0, phys_cmd_buf, 0x4000); + UNSIGNED_INT_EQUALS(0, result); }; // Manually define a destructor to close everything. ~VideoOut() { - if (flip_event) { - s32 result = sceVideoOutDeleteFlipEvent(flip_queue, handle); - UNSIGNED_INT_EQUALS(0, result); - flip_event = false; - } + // Delete event queue if (flip_queue != nullptr) { s32 result = sceKernelDeleteEqueue(flip_queue); UNSIGNED_INT_EQUALS(0, result); } + + // Close video out handle if (handle > 0) { s32 result = sceVideoOutClose(handle); UNSIGNED_INT_EQUALS(0, result); handle = 0; } + + // Free allocations for video out buffers for (auto& [phys_off, size]: phys_bufs) { s32 result = sceKernelReleaseDirectMemory(phys_off, size); UNSIGNED_INT_EQUALS(0, result); } + + // Manually destruct vector to clean memory footprint phys_bufs.~vector(); + + // Free command buffer allocation + if (cmd_buf != nullptr) { + s32 result = sceKernelReleaseDirectMemory(phys_cmd_buf, 0x4000); + UNSIGNED_INT_EQUALS(0, result); + } }; s32 getStatus(OrbisVideoOutFlipStatus* status) { return sceVideoOutGetFlipStatus(handle, status); }; @@ -70,26 +99,22 @@ class VideoOut { // SubmitAndFlip fails on buffer -1, save time by failing early. return -1; } - s64 cmd_dmem_ptr = 0; - s32 result = sceKernelAllocateMainDirectMemory(0x4000, 0x4000, 0, &cmd_dmem_ptr); - UNSIGNED_INT_EQUALS(0, result); - void* cmd_ptr = nullptr; - result = sceKernelMapDirectMemory(&cmd_ptr, 0x4000, 0x33, 0, cmd_dmem_ptr, 0x4000); - UNSIGNED_INT_EQUALS(0, result); + // Clear memory for command buffer + memset(cmd_buf, 0, 0x4000); // Write GPU init packet to the pointer. - u32* cmds = (u32*)cmd_ptr; + u32* cmds = (u32*)cmd_buf; cmds += sceGnmDrawInitDefaultHardwareState350(cmds, 0x100); // Write a flip packet to the pointer. cmds[0] = 0xc03e1000; cmds[1] = 0x68750777; cmds += 64; - u32 stream_size = (u32)((u64)cmds - (u64)cmd_ptr); + stream_size = (u32)((u64)cmds - (u64)cmd_buf); - // GPU flips are not allowed on buffer -1. - result = sceGnmSubmitAndFlipCommandBuffers(1, &cmd_ptr, &stream_size, nullptr, nullptr, handle, current_buf, 1, flip_arg); + // Perform GPU submit + s32 result = sceGnmSubmitAndFlipCommandBuffers(1, &cmd_buf, &stream_size, nullptr, nullptr, handle, current_buf, 1, flip_arg); if (++current_buf == phys_bufs.size()) { current_buf = 0; } From c480195b62be5c072fcf06718d8cfe3ccaf815ef Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Sun, 15 Feb 2026 13:06:05 -0600 Subject: [PATCH 05/33] Partial refactor to fix Cpputest fails Vectors really don't work well, not sure if that's a side effect of some OpenOrbis issue, or if it's an issue with Cpputest. So now all videoout buffers use the same memory area, and command buffer uses a single area too. --- tests/code/event_test/code/video.h | 102 ++++++++++++++++------------- 1 file changed, 55 insertions(+), 47 deletions(-) diff --git a/tests/code/event_test/code/video.h b/tests/code/event_test/code/video.h index 917d6838..1dad045d 100644 --- a/tests/code/event_test/code/video.h +++ b/tests/code/event_test/code/video.h @@ -7,22 +7,27 @@ class VideoOut { private: // Video out data - s32 handle; - s32 width; - s32 height; + s32 handle = 0; + s32 width = 0; + s32 height = 0; s32 current_buf = -1; // Buffer allocations - std::vector> phys_bufs; + void* buf_addr = nullptr; + s64 buf_paddr = 0; + u64 buf_size = 0; + u64 buf_count = 0; // Equeue OrbisKernelEqueue flip_queue = nullptr; bool flip_event = false; // Command buffer for EOP tests - void* cmd_buf = nullptr; - s64 phys_cmd_buf; - u32 stream_size; + void* cmd_buf = nullptr; + s64 phys_cmd_buf = 0; + u64 cmd_buf_size = 0x4000; + u32 stream_size = 0; + u32 cmd_start_offset = 0; public: VideoOut(s32 width, s32 height) { @@ -42,11 +47,26 @@ class VideoOut { UNSIGNED_INT_EQUALS(0, result); // Initialize command buffer for EOP flip tests - result = sceKernelAllocateMainDirectMemory(0x4000, 0x4000, 0, &phys_cmd_buf); + result = sceKernelAllocateMainDirectMemory(cmd_buf_size, 0x4000, 0, &phys_cmd_buf); UNSIGNED_INT_EQUALS(0, result); - result = sceKernelMapDirectMemory(&cmd_buf, 0x4000, 0x33, 0, phys_cmd_buf, 0x4000); + result = sceKernelMapDirectMemory(&cmd_buf, cmd_buf_size, 0x33, 0, phys_cmd_buf, 0x4000); UNSIGNED_INT_EQUALS(0, result); + + // Map a memory area for video out buffers + // Calculate necessary buffer size, logic is taken from red_prig's shader test homebrew + u64 pitch = (width + 127) / 128; + u64 pad_width = pitch * 128; + u64 pad_height = ((height + 63) & (~63)); + u64 size = pad_width * pad_height * 4; + buf_size = (size + 16 * 1024 - 1) & ~(16 * 1024 - 1); + + // Perform the actual memory mapping + result = sceKernelAllocateMainDirectMemory(buf_size, 0x10000, 3, &buf_paddr); + UNSIGNED_INT_EQUALS(0, result); + result = sceKernelMapDirectMemory(&buf_addr, buf_size, 0x33, 0, buf_paddr, 0x10000); + UNSIGNED_INT_EQUALS(0, result); + memset(buf_addr, 0, buf_size); }; // Manually define a destructor to close everything. @@ -64,18 +84,15 @@ class VideoOut { handle = 0; } - // Free allocations for video out buffers - for (auto& [phys_off, size]: phys_bufs) { - s32 result = sceKernelReleaseDirectMemory(phys_off, size); + // Free command buffer allocation + if (cmd_buf != nullptr) { + s32 result = sceKernelReleaseDirectMemory(phys_cmd_buf, cmd_buf_size); UNSIGNED_INT_EQUALS(0, result); } - // Manually destruct vector to clean memory footprint - phys_bufs.~vector(); - - // Free command buffer allocation - if (cmd_buf != nullptr) { - s32 result = sceKernelReleaseDirectMemory(phys_cmd_buf, 0x4000); + // Free video out buffer allocation + if (buf_addr != nullptr) { + s32 result = sceKernelReleaseDirectMemory(buf_paddr, buf_size); UNSIGNED_INT_EQUALS(0, result); } }; @@ -83,39 +100,42 @@ class VideoOut { s32 getStatus(OrbisVideoOutFlipStatus* status) { return sceVideoOutGetFlipStatus(handle, status); }; s32 flipFrame(s64 flip_arg) { - if (phys_bufs.size() == 0) { + if (buf_count == 0) { // Force a blank frame if no buffers are registered current_buf = -1; } s32 result = sceVideoOutSubmitFlip(handle, current_buf, 1, flip_arg); - if (++current_buf == phys_bufs.size()) { + if (++current_buf == buf_count) { current_buf = 0; } return result; }; s32 submitAndFlip(s64 flip_arg) { - if (phys_bufs.size() == 0) { + if (buf_count == 0) { // SubmitAndFlip fails on buffer -1, save time by failing early. return -1; } - // Clear memory for command buffer - memset(cmd_buf, 0, 0x4000); - // Write GPU init packet to the pointer. - u32* cmds = (u32*)cmd_buf; + void* cmd_buf_start = (void*)((u64)cmd_buf + cmd_start_offset); + u32* cmds = (u32*)cmd_buf_start; cmds += sceGnmDrawInitDefaultHardwareState350(cmds, 0x100); // Write a flip packet to the pointer. cmds[0] = 0xc03e1000; cmds[1] = 0x68750777; cmds += 64; - stream_size = (u32)((u64)cmds - (u64)cmd_buf); + stream_size = (u32)((u64)cmds - (u64)cmd_buf_start); + + cmd_start_offset += stream_size; + if (cmd_start_offset + stream_size > cmd_buf_size) { + cmd_start_offset = 0; + } // Perform GPU submit - s32 result = sceGnmSubmitAndFlipCommandBuffers(1, &cmd_buf, &stream_size, nullptr, nullptr, handle, current_buf, 1, flip_arg); - if (++current_buf == phys_bufs.size()) { + s32 result = sceGnmSubmitAndFlipCommandBuffers(1, &cmd_buf_start, &stream_size, nullptr, nullptr, handle, current_buf, 1, flip_arg); + if (++current_buf == buf_count) { current_buf = 0; } return result; @@ -134,27 +154,15 @@ class VideoOut { attr.reserved0 = 0; attr.reserved1 = 0; - // calc some stuff, logic taken from red_prig's shader test homebrew. - u64 pitch = (attr.width + 127) / 128; - u64 pad_width = pitch * 128; - u64 pad_height = ((attr.height + 63) & (~63)); - u64 size = pad_width * pad_height * 4; - u64 aligned_size = (size + 16 * 1024 - 1) & ~(16 * 1024 - 1); - - // Map memory for buffer. - s64 dmem_addr = 0; - s32 result = sceKernelAllocateMainDirectMemory(aligned_size, 64 * 1024, 3, &dmem_addr); - UNSIGNED_INT_EQUALS(0, result); - void* addr = nullptr; - result = sceKernelMapDirectMemory(&addr, aligned_size, 0x33, 0, dmem_addr, 64 * 1024); - UNSIGNED_INT_EQUALS(0, result); - memset(addr, 0, aligned_size); + // Register buffer + s32 result = sceVideoOutRegisterBuffers(handle, buf_count, &buf_addr, 1, &attr); - // Add buffer info to the buffers list - phys_bufs.emplace_back(dmem_addr, aligned_size); + if (result == 0) { + // Buffer registered successfully, increment buffers count. + buf_count++; + } - // Register buffer - return sceVideoOutRegisterBuffers(handle, phys_bufs.size() - 1, &addr, 1, &attr); + return result; }; s32 removeBuffers() { From 908062aa6a4ddd30801e66c2172824d9bf38eed2 Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Sun, 15 Feb 2026 13:06:12 -0600 Subject: [PATCH 06/33] Log enhancements. --- tests/code/event_test/code/test.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/code/event_test/code/test.cpp b/tests/code/event_test/code/test.cpp index 23c03d84..8a9d8564 100644 --- a/tests/code/event_test/code/test.cpp +++ b/tests/code/event_test/code/test.cpp @@ -11,34 +11,34 @@ TEST_GROUP (EventTest) { }; static void PrintEventData(OrbisKernelEvent* ev) { - printf("ev->ident = 0x%08lx\n", ev->ident); + printf("ev->ident = 0x%lx\n", ev->ident); printf("ev->filter = %hd\n", ev->filter); printf("ev->flags = %u\n", ev->flags); printf("ev->fflags = %u\n", ev->fflags); - printf("ev->data = 0x%08lx\n", ev->data); + printf("ev->data = 0x%lx\n", ev->data); if (ev->data != 0) { if (ev->filter == -13) { // Video out event, cast data appropriately. VideoOutEventData data = *reinterpret_cast(&ev->data); printf("ev->data->time = %d\n", data.time); printf("ev->data->counter = %d\n", data.counter); - printf("ev->data->flip_arg = 0x%08lx\n", data.flip_arg); + printf("ev->data->flip_arg = 0x%lx\n", data.flip_arg); } else { - printf("*(ev->data) = 0x%08lx\n", *(u64*)ev->data); + printf("*(ev->data) = 0x%lx\n", *(u64*)ev->data); } } - printf("ev->user_data = 0x%08lx\n", ev->user_data); + printf("ev->user_data = 0x%lx\n", ev->user_data); if (ev->user_data != 0) { - printf("*(ev->user_data) = 0x%08lx\n", *(u64*)ev->user_data); + printf("*(ev->user_data) = 0x%lx\n", *(u64*)ev->user_data); } printf("\n"); } static void PrintFlipStatus(OrbisVideoOutFlipStatus* status) { - printf("status->count = 0x%08lx\n", status->count); + printf("status->count = 0x%lx\n", status->count); printf("status->process_time = %ld\n", status->process_time); printf("status->tsc_time = %ld\n", status->tsc_time); - printf("status->flip_arg = 0x%08lx\n", status->flip_arg); + printf("status->flip_arg = 0x%lx\n", status->flip_arg); printf("status->submit_tsc = %ld\n", status->submit_tsc); printf("status->num_gpu_flip_pending = %d\n", status->num_gpu_flip_pending); printf("status->num_flip_pending = %d\n", status->num_flip_pending); From 51ec33ea145312b0bfbdbb0b2ed2d699d081e7dc Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Sun, 15 Feb 2026 13:09:47 -0600 Subject: [PATCH 07/33] Update for rebase --- tests/code/event_test/CMakeLists.txt | 7 +++++-- tests/code/event_test/assets/.gitkeep | 0 2 files changed, 5 insertions(+), 2 deletions(-) delete mode 100644 tests/code/event_test/assets/.gitkeep diff --git a/tests/code/event_test/CMakeLists.txt b/tests/code/event_test/CMakeLists.txt index 8ada6139..6c8710be 100644 --- a/tests/code/event_test/CMakeLists.txt +++ b/tests/code/event_test/CMakeLists.txt @@ -1,5 +1,8 @@ -project(event_test LANGUAGES CXX) +project(event_test VERSION 0.0.1) link_libraries(SceSystemService SceVideoOut SceGnmDriver) -create_pkg("EVNT00100" 1 00 "code/main.cpp;code/test.cpp") +create_pkg(EVNT00100 1 00 "code/main.cpp;code/test.cpp") +set_target_properties(EVNT00100 PROPERTIES OO_PKG_TITLE "PS4 event queues test") +set_target_properties(EVNT00100 PROPERTIES OO_PKG_APPVER "1.0") +finalize_pkg(EVNT00100) \ No newline at end of file diff --git a/tests/code/event_test/assets/.gitkeep b/tests/code/event_test/assets/.gitkeep deleted file mode 100644 index e69de29b..00000000 From 56237809c6cfeebac82d9de829ede646161fb848 Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Sun, 15 Feb 2026 13:17:31 -0600 Subject: [PATCH 08/33] Remove spamming status logs. --- tests/code/event_test/code/test.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/code/event_test/code/test.cpp b/tests/code/event_test/code/test.cpp index 8a9d8564..6e750ebf 100644 --- a/tests/code/event_test/code/test.cpp +++ b/tests/code/event_test/code/test.cpp @@ -459,7 +459,6 @@ TEST(EventTest, FlipEventTest) { do { memset(&status, 0, sizeof(status)); result = handle->getStatus(&status); - PrintFlipStatus(&status); UNSIGNED_INT_EQUALS(0, result); sceKernelUsleep(10000); From 194a4d4e15deee792120ded7ff449dcecd36926c Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Sun, 15 Feb 2026 13:38:32 -0600 Subject: [PATCH 09/33] Remove redundant checks, add test for multiple flip events in equeue --- tests/code/event_test/code/test.cpp | 116 ++++++++++++++++++++-------- tests/code/event_test/code/video.h | 2 +- 2 files changed, 85 insertions(+), 33 deletions(-) diff --git a/tests/code/event_test/code/test.cpp b/tests/code/event_test/code/test.cpp index 6e750ebf..1cd9ba89 100644 --- a/tests/code/event_test/code/test.cpp +++ b/tests/code/event_test/code/test.cpp @@ -258,7 +258,8 @@ TEST(EventTest, FlipEventTest) { UNSIGNED_INT_EQUALS(0, result); // Create a flip event - result = handle->addFlipEvent(nullptr); + u64 num = 1; + result = handle->addFlipEvent(&num); UNSIGNED_INT_EQUALS(0, result); // Not sure what we're dealing with, so from here, start logging info. @@ -272,7 +273,7 @@ TEST(EventTest, FlipEventTest) { OrbisKernelEvent ev {}; memset(&ev, 0, sizeof(ev)); s32 count = 0; - result = handle->waitFlipEvent(&ev, &count, nullptr); + result = handle->waitFlipEvent(&ev, 1, &count, nullptr); UNSIGNED_INT_EQUALS(0, result); CHECK_EQUAL(1, count); @@ -284,6 +285,8 @@ TEST(EventTest, FlipEventTest) { CHECK_EQUAL(0, ev.fflags); CHECK(ev.data != 0); VideoOutEventData ev_data = *reinterpret_cast(&ev.data); + CHECK(ev.user_data != 0); + CHECK_EQUAL(1, *(u64*)ev.user_data); CHECK_EQUAL(1, ev_data.counter); CHECK_EQUAL(0x100, ev_data.flip_arg); @@ -291,7 +294,7 @@ TEST(EventTest, FlipEventTest) { memset(&ev, 0, sizeof(ev)); count = 0; u32 timeout = 1000; - result = handle->waitFlipEvent(&ev, &count, &timeout); + result = handle->waitFlipEvent(&ev, 1, &count, &timeout); UNSIGNED_INT_EQUALS(ORBIS_KERNEL_ERROR_ETIMEDOUT, result); // Check flip status @@ -308,18 +311,17 @@ TEST(EventTest, FlipEventTest) { // Now we can wait on the flip event equeue prepared earlier. memset(&ev, 0, sizeof(ev)); count = 0; - result = handle->waitFlipEvent(&ev, &count, nullptr); + result = handle->waitFlipEvent(&ev, 1, &count, nullptr); UNSIGNED_INT_EQUALS(0, result); CHECK_EQUAL(1, count); // Check returned data PrintEventData(&ev); - CHECK_EQUAL(0x6000000000000, ev.ident); - CHECK_EQUAL(-13, ev.filter); - CHECK_EQUAL(32, ev.flags); CHECK_EQUAL(0, ev.fflags); CHECK(ev.data != 0); ev_data = *reinterpret_cast(&ev.data); + CHECK(ev.user_data != 0); + CHECK_EQUAL(1, *(u64*)ev.user_data); CHECK_EQUAL(1, ev_data.counter); CHECK_EQUAL(0x200, ev_data.flip_arg); @@ -327,7 +329,7 @@ TEST(EventTest, FlipEventTest) { memset(&ev, 0, sizeof(ev)); count = 0; timeout = 1000; - result = handle->waitFlipEvent(&ev, &count, &timeout); + result = handle->waitFlipEvent(&ev, 1, &count, &timeout); UNSIGNED_INT_EQUALS(ORBIS_KERNEL_ERROR_ETIMEDOUT, result); // Check flip status @@ -355,16 +357,12 @@ TEST(EventTest, FlipEventTest) { // Now we can wait on the flip event equeue prepared earlier. memset(&ev, 0, sizeof(ev)); count = 0; - result = handle->waitFlipEvent(&ev, &count, nullptr); + result = handle->waitFlipEvent(&ev, 1, &count, nullptr); UNSIGNED_INT_EQUALS(0, result); CHECK_EQUAL(1, count); // Check returned data PrintEventData(&ev); - CHECK_EQUAL(0x6000000000000, ev.ident); - CHECK_EQUAL(-13, ev.filter); - CHECK_EQUAL(32, ev.flags); - CHECK_EQUAL(0, ev.fflags); CHECK(ev.data != 0); ev_data = *reinterpret_cast(&ev.data); CHECK_EQUAL(1, ev_data.counter); @@ -380,16 +378,12 @@ TEST(EventTest, FlipEventTest) { // We did two submits, so the video out event should fire again. memset(&ev, 0, sizeof(ev)); count = 0; - result = handle->waitFlipEvent(&ev, &count, nullptr); + result = handle->waitFlipEvent(&ev, 1, &count, nullptr); UNSIGNED_INT_EQUALS(0, result); CHECK_EQUAL(1, count); // Check returned data PrintEventData(&ev); - CHECK_EQUAL(0x6000000000000, ev.ident); - CHECK_EQUAL(-13, ev.filter); - CHECK_EQUAL(32, ev.flags); - CHECK_EQUAL(0, ev.fflags); CHECK(ev.data != 0); ev_data = *reinterpret_cast(&ev.data); CHECK_EQUAL(1, ev_data.counter); @@ -426,15 +420,11 @@ TEST(EventTest, FlipEventTest) { // Check returned data memset(&ev, 0, sizeof(ev)); count = 0; - result = handle->waitFlipEvent(&ev, &count, nullptr); + result = handle->waitFlipEvent(&ev, 1, &count, nullptr); UNSIGNED_INT_EQUALS(0, result); CHECK_EQUAL(1, count); PrintEventData(&ev); - CHECK_EQUAL(0x6000000000000, ev.ident); - CHECK_EQUAL(-13, ev.filter); - CHECK_EQUAL(32, ev.flags); - CHECK_EQUAL(0, ev.fflags); CHECK(ev.data != 0); ev_data = *reinterpret_cast(&ev.data); // Counter is how many times the event was triggered. @@ -444,7 +434,7 @@ TEST(EventTest, FlipEventTest) { // Shouldn't trigger again. memset(&ev, 0, sizeof(ev)); count = 0; - result = handle->waitFlipEvent(&ev, &count, &timeout); + result = handle->waitFlipEvent(&ev, 1, &count, &timeout); UNSIGNED_INT_EQUALS(ORBIS_KERNEL_ERROR_ETIMEDOUT, result); // Now test EOP flips @@ -469,7 +459,7 @@ TEST(EventTest, FlipEventTest) { // Wait for EOP flip to occur. memset(&ev, 0, sizeof(ev)); count = 0; - result = handle->waitFlipEvent(&ev, &count, nullptr); + result = handle->waitFlipEvent(&ev, 1, &count, nullptr); UNSIGNED_INT_EQUALS(0, result); CHECK_EQUAL(1, count); @@ -480,7 +470,8 @@ TEST(EventTest, FlipEventTest) { CHECK_EQUAL(0, ev.fflags); CHECK(ev.data != 0); ev_data = *reinterpret_cast(&ev.data); - // Counter is how many times the event was triggered. + CHECK(ev.user_data != 0); + CHECK_EQUAL(1, *(u64*)ev.user_data); CHECK_EQUAL(3, ev_data.counter); CHECK_EQUAL(0x3000, ev_data.flip_arg); @@ -496,18 +487,13 @@ TEST(EventTest, FlipEventTest) { // Wait for EOP flip to occur. memset(&ev, 0, sizeof(ev)); count = 0; - result = handle->waitFlipEvent(&ev, &count, nullptr); + result = handle->waitFlipEvent(&ev, 1, &count, nullptr); UNSIGNED_INT_EQUALS(0, result); CHECK_EQUAL(1, count); PrintEventData(&ev); - CHECK_EQUAL(0x6000000000000, ev.ident); - CHECK_EQUAL(-13, ev.filter); - CHECK_EQUAL(32, ev.flags); - CHECK_EQUAL(0, ev.fflags); CHECK(ev.data != 0); ev_data = *reinterpret_cast(&ev.data); - // Counter is how many times the event was triggered. CHECK_EQUAL(1, ev_data.counter); CHECK_EQUAL(0x30000000000, ev_data.flip_arg); @@ -517,6 +503,72 @@ TEST(EventTest, FlipEventTest) { UNSIGNED_INT_EQUALS(0, result); PrintFlipStatus(&status); + // Try adding a second flip event + s64 num2 = 2; + result = handle->addFlipEvent(&num2); + UNSIGNED_INT_EQUALS(0, result); + + // Trigger a flip, then wait for two events with no timeout. + result = handle->flipFrame(0x10000); + UNSIGNED_INT_EQUALS(0, result); + + // Wait for flip + do { + memset(&status, 0, sizeof(status)); + result = handle->getStatus(&status); + UNSIGNED_INT_EQUALS(0, result); + + sceKernelUsleep(10000); + } while (status.num_flip_pending != 0); + + OrbisKernelEvent events[2]; + memset(events, 0, sizeof(events)); + count = 0; + timeout = 10000; + result = handle->waitFlipEvent(events, 2, &count, &timeout); + UNSIGNED_INT_EQUALS(0, result); + // Despite adding another flip event, we still only get the one event. + CHECK_EQUAL(1, count); + + // Print the event data. + PrintEventData(&events[0]); + + CHECK(events[0].data != 0); + ev_data = *reinterpret_cast(&events[0].data); + // The user data matches the new event + CHECK(events[0].user_data != 0); + CHECK_EQUAL(2, *(u64*)events[0].user_data); + CHECK_EQUAL(1, ev_data.counter); + CHECK_EQUAL(0x10000, ev_data.flip_arg); + + // Trigger a flip, then wait for two events with no timeout. + result = handle->flipFrame(0x10000); + UNSIGNED_INT_EQUALS(0, result); + + // Wait for flip + do { + memset(&status, 0, sizeof(status)); + result = handle->getStatus(&status); + UNSIGNED_INT_EQUALS(0, result); + + sceKernelUsleep(10000); + } while (status.num_flip_pending != 0); + + memset(events, 0, sizeof(events)); + count = 0; + timeout = 10000; + result = handle->waitFlipEvent(events, 2, &count, &timeout); + UNSIGNED_INT_EQUALS(0, result); + // Despite adding another flip event, we still only get the one event. + CHECK_EQUAL(1, count); + + // Print the event data. + PrintEventData(&events[0]); + + // The user data still matches the new event + CHECK(events[0].user_data != 0); + CHECK_EQUAL(2, *(u64*)events[0].user_data); + // Clean up after test delete (handle); } diff --git a/tests/code/event_test/code/video.h b/tests/code/event_test/code/video.h index 1dad045d..2e430198 100644 --- a/tests/code/event_test/code/video.h +++ b/tests/code/event_test/code/video.h @@ -182,5 +182,5 @@ class VideoOut { return result; }; - s32 waitFlipEvent(OrbisKernelEvent* ev, s32* out, u32* timeout) { return sceKernelWaitEqueue(flip_queue, ev, 1, out, timeout); }; + s32 waitFlipEvent(OrbisKernelEvent* ev, s32 num, s32* out, u32* timeout) { return sceKernelWaitEqueue(flip_queue, ev, num, out, timeout); }; }; \ No newline at end of file From 11e06da4ca826b6dc008b8d2e622f26c85ce28ef Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Sun, 15 Feb 2026 13:39:44 -0600 Subject: [PATCH 10/33] Update test.cpp --- tests/code/event_test/code/test.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/code/event_test/code/test.cpp b/tests/code/event_test/code/test.cpp index 1cd9ba89..fc6b04cb 100644 --- a/tests/code/event_test/code/test.cpp +++ b/tests/code/event_test/code/test.cpp @@ -259,7 +259,7 @@ TEST(EventTest, FlipEventTest) { // Create a flip event u64 num = 1; - result = handle->addFlipEvent(&num); + result = handle->addFlipEvent(&num); UNSIGNED_INT_EQUALS(0, result); // Not sure what we're dealing with, so from here, start logging info. @@ -505,7 +505,7 @@ TEST(EventTest, FlipEventTest) { // Try adding a second flip event s64 num2 = 2; - result = handle->addFlipEvent(&num2); + result = handle->addFlipEvent(&num2); UNSIGNED_INT_EQUALS(0, result); // Trigger a flip, then wait for two events with no timeout. @@ -523,9 +523,9 @@ TEST(EventTest, FlipEventTest) { OrbisKernelEvent events[2]; memset(events, 0, sizeof(events)); - count = 0; + count = 0; timeout = 10000; - result = handle->waitFlipEvent(events, 2, &count, &timeout); + result = handle->waitFlipEvent(events, 2, &count, &timeout); UNSIGNED_INT_EQUALS(0, result); // Despite adding another flip event, we still only get the one event. CHECK_EQUAL(1, count); @@ -555,9 +555,9 @@ TEST(EventTest, FlipEventTest) { } while (status.num_flip_pending != 0); memset(events, 0, sizeof(events)); - count = 0; + count = 0; timeout = 10000; - result = handle->waitFlipEvent(events, 2, &count, &timeout); + result = handle->waitFlipEvent(events, 2, &count, &timeout); UNSIGNED_INT_EQUALS(0, result); // Despite adding another flip event, we still only get the one event. CHECK_EQUAL(1, count); From 8ec4efc44137b5c93efa1f02b9065f8f39959879 Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Sun, 15 Feb 2026 13:44:19 -0600 Subject: [PATCH 11/33] Update video.h --- tests/code/event_test/code/video.h | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/tests/code/event_test/code/video.h b/tests/code/event_test/code/video.h index 2e430198..2f89574e 100644 --- a/tests/code/event_test/code/video.h +++ b/tests/code/event_test/code/video.h @@ -20,7 +20,6 @@ class VideoOut { // Equeue OrbisKernelEqueue flip_queue = nullptr; - bool flip_event = false; // Command buffer for EOP tests void* cmd_buf = nullptr; @@ -170,17 +169,9 @@ class VideoOut { return sceVideoOutUnregisterBuffers(handle, 0); } - s32 addFlipEvent(void* user_data) { - s32 result = sceVideoOutAddFlipEvent(flip_queue, handle, user_data); - flip_event = true; - return result; - }; + s32 addFlipEvent(void* user_data) { return sceVideoOutAddFlipEvent(flip_queue, handle, user_data); }; - s32 deleteFlipEvent() { - s32 result = sceVideoOutDeleteFlipEvent(flip_queue, handle); - flip_event = false; - return result; - }; + s32 deleteFlipEvent() { return sceVideoOutDeleteFlipEvent(flip_queue, handle); }; s32 waitFlipEvent(OrbisKernelEvent* ev, s32 num, s32* out, u32* timeout) { return sceKernelWaitEqueue(flip_queue, ev, num, out, timeout); }; }; \ No newline at end of file From f1cb63b4ce066280ff130876128864c09ade3f23 Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Sun, 15 Feb 2026 14:52:53 -0600 Subject: [PATCH 12/33] Fix some checks --- tests/code/event_test/code/test.cpp | 11 +++++++---- tests/code/event_test/code/video.h | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/tests/code/event_test/code/test.cpp b/tests/code/event_test/code/test.cpp index fc6b04cb..fb6d27bb 100644 --- a/tests/code/event_test/code/test.cpp +++ b/tests/code/event_test/code/test.cpp @@ -249,12 +249,15 @@ TEST(EventTest, FlipEventTest) { VideoOut* handle = new VideoOut(1920, 1080); // Register buffers - handle->addBuffer(); - handle->addBuffer(); - handle->addBuffer(); + s32 result = handle->addBuffer(); + UNSIGNED_INT_EQUALS(0, result); + result = handle->addBuffer(); + UNSIGNED_INT_EQUALS(1, result); + result = handle->addBuffer(); + UNSIGNED_INT_EQUALS(2, result); OrbisVideoOutFlipStatus status {}; - s32 result = handle->getStatus(&status); + result = handle->getStatus(&status); UNSIGNED_INT_EQUALS(0, result); // Create a flip event diff --git a/tests/code/event_test/code/video.h b/tests/code/event_test/code/video.h index 2f89574e..4845777a 100644 --- a/tests/code/event_test/code/video.h +++ b/tests/code/event_test/code/video.h @@ -156,7 +156,7 @@ class VideoOut { // Register buffer s32 result = sceVideoOutRegisterBuffers(handle, buf_count, &buf_addr, 1, &attr); - if (result == 0) { + if (result >= 0) { // Buffer registered successfully, increment buffers count. buf_count++; } From 27c59ec4e1fcfe61d6bad62092ce4c805edae099 Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Sun, 15 Feb 2026 15:52:23 -0600 Subject: [PATCH 13/33] Update test.cpp --- tests/code/event_test/code/test.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/code/event_test/code/test.cpp b/tests/code/event_test/code/test.cpp index fb6d27bb..a9f38276 100644 --- a/tests/code/event_test/code/test.cpp +++ b/tests/code/event_test/code/test.cpp @@ -257,7 +257,7 @@ TEST(EventTest, FlipEventTest) { UNSIGNED_INT_EQUALS(2, result); OrbisVideoOutFlipStatus status {}; - result = handle->getStatus(&status); + result = handle->getStatus(&status); UNSIGNED_INT_EQUALS(0, result); // Create a flip event From 597d85eb1abb42dccb2586bd3f89de962469bca9 Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Sun, 15 Feb 2026 20:01:47 -0600 Subject: [PATCH 14/33] Some cleanup Move sceVideoOutGetFlipStatus based waits to a function, move logic for re-zero-initializing variables into respective functions, inline timeout values, and add an extra test to ensure deleting a flip event behaves as expected --- tests/code/event_test/code/test.cpp | 123 ++++++++++------------------ tests/code/event_test/code/test.h | 1 - tests/code/event_test/code/video.h | 26 +++++- 3 files changed, 68 insertions(+), 82 deletions(-) diff --git a/tests/code/event_test/code/test.cpp b/tests/code/event_test/code/test.cpp index a9f38276..c57b3876 100644 --- a/tests/code/event_test/code/test.cpp +++ b/tests/code/event_test/code/test.cpp @@ -274,9 +274,8 @@ TEST(EventTest, FlipEventTest) { // Now we can wait on the flip event equeue prepared earlier. OrbisKernelEvent ev {}; - memset(&ev, 0, sizeof(ev)); - s32 count = 0; - result = handle->waitFlipEvent(&ev, 1, &count, nullptr); + s32 count = 0; + result = handle->waitFlipEvent(&ev, 1, &count, -1); UNSIGNED_INT_EQUALS(0, result); CHECK_EQUAL(1, count); @@ -294,14 +293,10 @@ TEST(EventTest, FlipEventTest) { CHECK_EQUAL(0x100, ev_data.flip_arg); // Flip events only trigger once. - memset(&ev, 0, sizeof(ev)); - count = 0; - u32 timeout = 1000; - result = handle->waitFlipEvent(&ev, 1, &count, &timeout); + result = handle->waitFlipEvent(&ev, 1, &count, 1000); UNSIGNED_INT_EQUALS(ORBIS_KERNEL_ERROR_ETIMEDOUT, result); // Check flip status - memset(&status, 0, sizeof(status)); result = handle->getStatus(&status); UNSIGNED_INT_EQUALS(0, result); @@ -312,9 +307,8 @@ TEST(EventTest, FlipEventTest) { UNSIGNED_INT_EQUALS(0, result); // Now we can wait on the flip event equeue prepared earlier. - memset(&ev, 0, sizeof(ev)); count = 0; - result = handle->waitFlipEvent(&ev, 1, &count, nullptr); + result = handle->waitFlipEvent(&ev, 1, &count, -1); UNSIGNED_INT_EQUALS(0, result); CHECK_EQUAL(1, count); @@ -329,14 +323,10 @@ TEST(EventTest, FlipEventTest) { CHECK_EQUAL(0x200, ev_data.flip_arg); // Flip events only trigger once. - memset(&ev, 0, sizeof(ev)); - count = 0; - timeout = 1000; - result = handle->waitFlipEvent(&ev, 1, &count, &timeout); + result = handle->waitFlipEvent(&ev, 1, &count, 1000); UNSIGNED_INT_EQUALS(ORBIS_KERNEL_ERROR_ETIMEDOUT, result); // Check flip status - memset(&status, 0, sizeof(status)); result = handle->getStatus(&status); UNSIGNED_INT_EQUALS(0, result); @@ -358,9 +348,7 @@ TEST(EventTest, FlipEventTest) { PrintFlipStatus(&status); // Now we can wait on the flip event equeue prepared earlier. - memset(&ev, 0, sizeof(ev)); - count = 0; - result = handle->waitFlipEvent(&ev, 1, &count, nullptr); + result = handle->waitFlipEvent(&ev, 1, &count, -1); UNSIGNED_INT_EQUALS(0, result); CHECK_EQUAL(1, count); @@ -372,16 +360,13 @@ TEST(EventTest, FlipEventTest) { CHECK_EQUAL(0x100, ev_data.flip_arg); // Check flip status - memset(&status, 0, sizeof(status)); result = handle->getStatus(&status); UNSIGNED_INT_EQUALS(0, result); PrintFlipStatus(&status); // We did two submits, so the video out event should fire again. - memset(&ev, 0, sizeof(ev)); - count = 0; - result = handle->waitFlipEvent(&ev, 1, &count, nullptr); + result = handle->waitFlipEvent(&ev, 1, &count, -1); UNSIGNED_INT_EQUALS(0, result); CHECK_EQUAL(1, count); @@ -393,7 +378,6 @@ TEST(EventTest, FlipEventTest) { CHECK_EQUAL(0x300, ev_data.flip_arg); // Check flip status - memset(&status, 0, sizeof(status)); result = handle->getStatus(&status); UNSIGNED_INT_EQUALS(0, result); @@ -405,25 +389,17 @@ TEST(EventTest, FlipEventTest) { result = handle->flipFrame(0x500); UNSIGNED_INT_EQUALS(0, result); - // Use sceVideoOutGetFlipStatus to wait for both flips to complete - memset(&status, 0, sizeof(status)); - result = handle->getStatus(&status); + // Wait for all flips to occur + result = handle->waitFlip(); UNSIGNED_INT_EQUALS(0, result); - while (status.num_flip_pending != 0) { - sceKernelUsleep(1000); - - memset(&status, 0, sizeof(status)); - result = handle->getStatus(&status); - UNSIGNED_INT_EQUALS(0, result); - } // Both flips are done, check status and event + result = handle->getStatus(&status); + UNSIGNED_INT_EQUALS(0, result); PrintFlipStatus(&status); // Check returned data - memset(&ev, 0, sizeof(ev)); - count = 0; - result = handle->waitFlipEvent(&ev, 1, &count, nullptr); + result = handle->waitFlipEvent(&ev, 1, &count, -1); UNSIGNED_INT_EQUALS(0, result); CHECK_EQUAL(1, count); @@ -435,9 +411,7 @@ TEST(EventTest, FlipEventTest) { CHECK_EQUAL(0x500, ev_data.flip_arg); // Shouldn't trigger again. - memset(&ev, 0, sizeof(ev)); - count = 0; - result = handle->waitFlipEvent(&ev, 1, &count, &timeout); + result = handle->waitFlipEvent(&ev, 1, &count, 1000); UNSIGNED_INT_EQUALS(ORBIS_KERNEL_ERROR_ETIMEDOUT, result); // Now test EOP flips @@ -448,21 +422,14 @@ TEST(EventTest, FlipEventTest) { result = handle->submitAndFlip(0x3000); UNSIGNED_INT_EQUALS(0, result); - // Print status - do { - memset(&status, 0, sizeof(status)); - result = handle->getStatus(&status); - UNSIGNED_INT_EQUALS(0, result); - - sceKernelUsleep(10000); - } while (status.num_flip_pending != 0); + // Wait for all flips to occur + result = handle->waitFlip(); + UNSIGNED_INT_EQUALS(0, result); PrintFlipStatus(&status); // Wait for EOP flip to occur. - memset(&ev, 0, sizeof(ev)); - count = 0; - result = handle->waitFlipEvent(&ev, 1, &count, nullptr); + result = handle->waitFlipEvent(&ev, 1, &count, -1); UNSIGNED_INT_EQUALS(0, result); CHECK_EQUAL(1, count); @@ -479,7 +446,6 @@ TEST(EventTest, FlipEventTest) { CHECK_EQUAL(0x3000, ev_data.flip_arg); // Print status again. - memset(&status, 0, sizeof(status)); result = handle->getStatus(&status); UNSIGNED_INT_EQUALS(0, result); PrintFlipStatus(&status); @@ -488,9 +454,7 @@ TEST(EventTest, FlipEventTest) { UNSIGNED_INT_EQUALS(0, result); // Wait for EOP flip to occur. - memset(&ev, 0, sizeof(ev)); - count = 0; - result = handle->waitFlipEvent(&ev, 1, &count, nullptr); + result = handle->waitFlipEvent(&ev, 1, &count, -1); UNSIGNED_INT_EQUALS(0, result); CHECK_EQUAL(1, count); @@ -501,7 +465,6 @@ TEST(EventTest, FlipEventTest) { CHECK_EQUAL(0x30000000000, ev_data.flip_arg); // Print status again. - memset(&status, 0, sizeof(status)); result = handle->getStatus(&status); UNSIGNED_INT_EQUALS(0, result); PrintFlipStatus(&status); @@ -515,20 +478,12 @@ TEST(EventTest, FlipEventTest) { result = handle->flipFrame(0x10000); UNSIGNED_INT_EQUALS(0, result); - // Wait for flip - do { - memset(&status, 0, sizeof(status)); - result = handle->getStatus(&status); - UNSIGNED_INT_EQUALS(0, result); - - sceKernelUsleep(10000); - } while (status.num_flip_pending != 0); + // Wait for all flips to occur + result = handle->waitFlip(); + UNSIGNED_INT_EQUALS(0, result); OrbisKernelEvent events[2]; - memset(events, 0, sizeof(events)); - count = 0; - timeout = 10000; - result = handle->waitFlipEvent(events, 2, &count, &timeout); + result = handle->waitFlipEvent(events, 2, &count, 10000); UNSIGNED_INT_EQUALS(0, result); // Despite adding another flip event, we still only get the one event. CHECK_EQUAL(1, count); @@ -548,19 +503,11 @@ TEST(EventTest, FlipEventTest) { result = handle->flipFrame(0x10000); UNSIGNED_INT_EQUALS(0, result); - // Wait for flip - do { - memset(&status, 0, sizeof(status)); - result = handle->getStatus(&status); - UNSIGNED_INT_EQUALS(0, result); - - sceKernelUsleep(10000); - } while (status.num_flip_pending != 0); + // Wait for all flips to occur + result = handle->waitFlip(); + UNSIGNED_INT_EQUALS(0, result); - memset(events, 0, sizeof(events)); - count = 0; - timeout = 10000; - result = handle->waitFlipEvent(events, 2, &count, &timeout); + result = handle->waitFlipEvent(events, 2, &count, 10000); UNSIGNED_INT_EQUALS(0, result); // Despite adding another flip event, we still only get the one event. CHECK_EQUAL(1, count); @@ -572,6 +519,24 @@ TEST(EventTest, FlipEventTest) { CHECK(events[0].user_data != 0); CHECK_EQUAL(2, *(u64*)events[0].user_data); + // Delete the event + result = handle->deleteFlipEvent(); + UNSIGNED_INT_EQUALS(0, result); + + // This should result in having no events. + // Trigger a flip + result = handle->flipFrame(0x10000); + UNSIGNED_INT_EQUALS(0, result); + + // Wait for all flips to occur + result = handle->waitFlip(); + UNSIGNED_INT_EQUALS(0, result); + + // Wait for event. + result = handle->waitFlipEvent(events, 1, &count, 10000); + // Since there's no event left, nothing will be triggered when the flip occurs. + UNSIGNED_INT_EQUALS(ORBIS_KERNEL_ERROR_ETIMEDOUT, result); + // Clean up after test delete (handle); } diff --git a/tests/code/event_test/code/test.h b/tests/code/event_test/code/test.h index fc55c4ce..50c75e46 100644 --- a/tests/code/event_test/code/test.h +++ b/tests/code/event_test/code/test.h @@ -86,5 +86,4 @@ s32 sceVideoOutClose(s32 handle); u32 sceGnmDrawInitDefaultHardwareState350(u32* cmd_buf, u32 num_dwords); s32 sceGnmSubmitAndFlipCommandBuffers(u32 count, void** dcb_gpu_addrs, u32* dcb_sizes_in_bytes, void** ccb_gpu_addrs, u32* ccb_sizes_in_bytes, s32 video_handle, s32 buffer_index, s32 flip_mode, s64 flip_arg); -s32 sceGnmSubmitDone(); } \ No newline at end of file diff --git a/tests/code/event_test/code/video.h b/tests/code/event_test/code/video.h index 4845777a..6b9f9f0f 100644 --- a/tests/code/event_test/code/video.h +++ b/tests/code/event_test/code/video.h @@ -96,7 +96,10 @@ class VideoOut { } }; - s32 getStatus(OrbisVideoOutFlipStatus* status) { return sceVideoOutGetFlipStatus(handle, status); }; + s32 getStatus(OrbisVideoOutFlipStatus* status) { + memset(status, 0, sizeof(OrbisVideoOutFlipStatus)); + return sceVideoOutGetFlipStatus(handle, status); + }; s32 flipFrame(s64 flip_arg) { if (buf_count == 0) { @@ -173,5 +176,24 @@ class VideoOut { s32 deleteFlipEvent() { return sceVideoOutDeleteFlipEvent(flip_queue, handle); }; - s32 waitFlipEvent(OrbisKernelEvent* ev, s32 num, s32* out, u32* timeout) { return sceKernelWaitEqueue(flip_queue, ev, num, out, timeout); }; + s32 waitFlipEvent(OrbisKernelEvent* ev, s32 num, s32* out, u32 timeout) { + memset(ev, 0, sizeof(OrbisKernelEvent) * num); + *out = 0; + if (timeout == -1) { + return sceKernelWaitEqueue(flip_queue, ev, num, out, nullptr); + } else { + return sceKernelWaitEqueue(flip_queue, ev, num, out, &timeout); + } + }; + + s32 waitFlip() { + // Wait for flip + OrbisVideoOutFlipStatus status {}; + s32 result = 0; + do { + result = getStatus(&status); + sceKernelUsleep(10000); + } while (result == 0 && status.num_flip_pending != 0); + return result; + } }; \ No newline at end of file From 5eaa925d98a3540547decd3062e65c81a4847beb Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Sun, 15 Feb 2026 20:41:52 -0600 Subject: [PATCH 15/33] Add vblank event tests These share a lot of logic with flip events, so the test is pretty simple. Make sure all data is in the right place, and that typical equeue behavior applies to them. --- tests/code/event_test/code/test.cpp | 79 +++++++++++++++++++++++++++++ tests/code/event_test/code/test.h | 5 +- tests/code/event_test/code/video.h | 42 ++++++++++++--- 3 files changed, 117 insertions(+), 9 deletions(-) diff --git a/tests/code/event_test/code/test.cpp b/tests/code/event_test/code/test.cpp index c57b3876..ce4a351a 100644 --- a/tests/code/event_test/code/test.cpp +++ b/tests/code/event_test/code/test.cpp @@ -540,3 +540,82 @@ TEST(EventTest, FlipEventTest) { // Clean up after test delete (handle); } + +TEST(EventTest, VblankEventTest) { + // Another type of video out event, these trigger automatically when the PS4 draws blank frames. + VideoOut* handle = new VideoOut(1920, 1080); + + // Add vblank event + s64 val = 1; + s32 result = handle->addVblankEvent(&val); + UNSIGNED_INT_EQUALS(0, result); + + // Wait with no timeout, this should return after vblank + OrbisKernelEvent ev {}; + s32 count; + result = handle->waitVblankEvent(&ev, 1, &count, -1); + UNSIGNED_INT_EQUALS(0, result); + CHECK_EQUAL(1, count); + + // Print event data + PrintEventData(&ev); + + CHECK_EQUAL(0x7000000000000, ev.ident); + CHECK_EQUAL(-13, ev.filter); + CHECK_EQUAL(32, ev.flags); + CHECK_EQUAL(0, ev.fflags); + // Data for these follows the same format as internal data for flip events. + CHECK(ev.data != 0); + VideoOutEventData ev_data = *reinterpret_cast(&ev.data); + // It is highly unlikely that more than 1 vblank happens between adding and waiting. + // Based on this assumption, we can check counter. + CHECK_EQUAL(1, ev_data.counter); + // ev_data flip arg seems to come from how many vblanks have occurred. + // There's no way to compare this to anything, as there will always be a chance of a race condition. + CHECK(ev_data.flip_arg > 0); + CHECK(ev_data.time > 0); + // user_data should be passed down from adding the event. + CHECK(ev.user_data != 0); + CHECK_EQUAL(val, *(s64*)ev.user_data); + + // Add a new event, this should replace the old one. + s64 new_val = 2; + result = handle->addVblankEvent(&new_val); + + // Wait with no timeout, this should return after vblank + result = handle->waitVblankEvent(&ev, 1, &count, -1); + UNSIGNED_INT_EQUALS(0, result); + CHECK_EQUAL(1, count); + + // Print event data + PrintEventData(&ev); + + CHECK_EQUAL(0x7000000000000, ev.ident); + CHECK_EQUAL(-13, ev.filter); + CHECK_EQUAL(32, ev.flags); + CHECK_EQUAL(0, ev.fflags); + // Data for these follows the same format as internal data for flip events. + CHECK(ev.data != 0); + ev_data = *reinterpret_cast(&ev.data); + CHECK_EQUAL(1, ev_data.counter); + CHECK(ev_data.flip_arg > 0); + CHECK(ev_data.time > 0); + CHECK(ev.user_data != 0); + CHECK_EQUAL(new_val, *(s64*)ev.user_data); + + // Delete vblank event + result = handle->deleteVblankEvent(); + UNSIGNED_INT_EQUALS(0, result); + + // Now vblank events won't fire. + // Validate using sceVideoOutWaitVblank + result = handle->waitVblank(); + UNSIGNED_INT_EQUALS(0, result); + + // Wait with short timeout, this will return after timeout with error timedout + result = handle->waitVblankEvent(&ev, 1, &count, 1000); + UNSIGNED_INT_EQUALS(ORBIS_KERNEL_ERROR_ETIMEDOUT, result); + + // Clean up after test + delete (handle); +} \ No newline at end of file diff --git a/tests/code/event_test/code/test.h b/tests/code/event_test/code/test.h index 50c75e46..c9ea900c 100644 --- a/tests/code/event_test/code/test.h +++ b/tests/code/event_test/code/test.h @@ -75,12 +75,15 @@ struct OrbisVideoOutBufferAttribute { s32 sceVideoOutOpen(s32 user_id, s32 bus_type, s32 index, const void* param); s32 sceVideoOutSetFlipRate(s32 handle, s32 flip_rate); s32 sceVideoOutAddFlipEvent(OrbisKernelEqueue eq, s32 handle, void* user_data); +s32 sceVideoOutDeleteFlipEvent(OrbisKernelEqueue eq, s32 handle); +s32 sceVideoOutAddVblankEvent(OrbisKernelEqueue eq, s32 handle, void* user_data); +s32 sceVideoOutDeleteVblankEvent(OrbisKernelEqueue eq, s32 handle); s32 sceVideoOutRegisterBuffers(s32 handle, s32 index, void* const* addrs, s32 buf_num, OrbisVideoOutBufferAttribute* attrs); s32 sceVideoOutUnregisterBuffers(s32 handle, s32 attribute_index); s32 sceVideoOutSubmitFlip(s32 handle, s32 buf_index, s32 flip_mode, s64 flip_arg); s32 sceVideoOutGetFlipStatus(s32 handle, OrbisVideoOutFlipStatus* status); s32 sceVideoOutIsFlipPending(s32 handle); -s32 sceVideoOutDeleteFlipEvent(OrbisKernelEqueue eq, s32 handle); +s32 sceVideoOutWaitVblank(s32 handle); s32 sceVideoOutClose(s32 handle); u32 sceGnmDrawInitDefaultHardwareState350(u32* cmd_buf, u32 num_dwords); diff --git a/tests/code/event_test/code/video.h b/tests/code/event_test/code/video.h index 6b9f9f0f..7b7540c1 100644 --- a/tests/code/event_test/code/video.h +++ b/tests/code/event_test/code/video.h @@ -19,7 +19,8 @@ class VideoOut { u64 buf_count = 0; // Equeue - OrbisKernelEqueue flip_queue = nullptr; + OrbisKernelEqueue flip_queue = nullptr; + OrbisKernelEqueue vblank_queue = nullptr; // Command buffer for EOP tests void* cmd_buf = nullptr; @@ -45,6 +46,10 @@ class VideoOut { result = sceKernelCreateEqueue(&flip_queue, "VideoOutFlipEqueue"); UNSIGNED_INT_EQUALS(0, result); + // Create vblank equeue + result = sceKernelCreateEqueue(&vblank_queue, "VideoOutVblankEqueue"); + UNSIGNED_INT_EQUALS(0, result); + // Initialize command buffer for EOP flip tests result = sceKernelAllocateMainDirectMemory(cmd_buf_size, 0x4000, 0, &phys_cmd_buf); UNSIGNED_INT_EQUALS(0, result); @@ -70,12 +75,18 @@ class VideoOut { // Manually define a destructor to close everything. ~VideoOut() { - // Delete event queue + // Delete flip event queue if (flip_queue != nullptr) { s32 result = sceKernelDeleteEqueue(flip_queue); UNSIGNED_INT_EQUALS(0, result); } + // Delete vblank event queue + if (vblank_queue != nullptr) { + s32 result = sceKernelDeleteEqueue(vblank_queue); + UNSIGNED_INT_EQUALS(0, result); + } + // Close video out handle if (handle > 0) { s32 result = sceVideoOutClose(handle); @@ -186,14 +197,29 @@ class VideoOut { } }; + s32 addVblankEvent(void* user_data) { return sceVideoOutAddVblankEvent(vblank_queue, handle, user_data); } + + s32 deleteVblankEvent() { return sceVideoOutDeleteVblankEvent(vblank_queue, handle); } + + s32 waitVblankEvent(OrbisKernelEvent* ev, s32 num, s32* out, u32 timeout) { + memset(ev, 0, sizeof(OrbisKernelEvent) * num); + *out = 0; + if (timeout == -1) { + return sceKernelWaitEqueue(vblank_queue, ev, num, out, nullptr); + } else { + return sceKernelWaitEqueue(vblank_queue, ev, num, out, &timeout); + } + }; + s32 waitFlip() { // Wait for flip - OrbisVideoOutFlipStatus status {}; - s32 result = 0; - do { - result = getStatus(&status); + s32 result = sceVideoOutIsFlipPending(handle); + while (result > 0) { sceKernelUsleep(10000); - } while (result == 0 && status.num_flip_pending != 0); + result = sceVideoOutIsFlipPending(handle); + }; return result; - } + }; + + s32 waitVblank() { return sceVideoOutWaitVblank(handle); }; }; \ No newline at end of file From 642093d17c025288fb52f711b1008de82d13908e Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Sun, 15 Feb 2026 20:49:34 -0600 Subject: [PATCH 16/33] Bugfix --- tests/code/event_test/code/test.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/code/event_test/code/test.cpp b/tests/code/event_test/code/test.cpp index ce4a351a..36a76cc1 100644 --- a/tests/code/event_test/code/test.cpp +++ b/tests/code/event_test/code/test.cpp @@ -572,7 +572,6 @@ TEST(EventTest, VblankEventTest) { CHECK_EQUAL(1, ev_data.counter); // ev_data flip arg seems to come from how many vblanks have occurred. // There's no way to compare this to anything, as there will always be a chance of a race condition. - CHECK(ev_data.flip_arg > 0); CHECK(ev_data.time > 0); // user_data should be passed down from adding the event. CHECK(ev.user_data != 0); @@ -598,6 +597,9 @@ TEST(EventTest, VblankEventTest) { CHECK(ev.data != 0); ev_data = *reinterpret_cast(&ev.data); CHECK_EQUAL(1, ev_data.counter); + // Flip arg is the number of vblanks that have occurred. + // First test can't check for these, since it's possible we fire an event on the first vblank. + // Here we know the flip arg must be positive, since we know a vblank has occurred. CHECK(ev_data.flip_arg > 0); CHECK(ev_data.time > 0); CHECK(ev.user_data != 0); From 8df99f884a8dda14205303f40a881526a308f048 Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Sun, 15 Feb 2026 21:19:56 -0600 Subject: [PATCH 17/33] Use user events to test for equeue behavior with multiple events --- tests/code/event_test/code/test.cpp | 139 +++++++++++++++++++++++++++- tests/code/event_test/code/test.h | 1 + 2 files changed, 137 insertions(+), 3 deletions(-) diff --git a/tests/code/event_test/code/test.cpp b/tests/code/event_test/code/test.cpp index 36a76cc1..b76c67f6 100644 --- a/tests/code/event_test/code/test.cpp +++ b/tests/code/event_test/code/test.cpp @@ -141,7 +141,7 @@ TEST(EventTest, UserEventTest) { UNSIGNED_INT_EQUALS(0, result); // Add a "user event edge", these are user events with the clear flag. - // Presumably, these will not trigger multiple times. + // For these, trigger state resets every time it's returned. // Add a user event to this equeue, use id 32. result = sceKernelAddUserEventEdge(eq, 32); UNSIGNED_INT_EQUALS(0, result); @@ -234,10 +234,143 @@ TEST(EventTest, UserEventTest) { result = sceKernelWaitEqueue(eq, &ev, 1, &count, &timeout); UNSIGNED_INT_EQUALS(ORBIS_KERNEL_ERROR_ETIMEDOUT, result); - // Delete the user event. + // Add a second user event edge, this time with id 64 + result = sceKernelAddUserEventEdge(eq, 64); + UNSIGNED_INT_EQUALS(0, result); + + // Trigger both events + result = sceKernelTriggerUserEvent(eq, 32, &data1); + UNSIGNED_INT_EQUALS(0, result); + result = sceKernelTriggerUserEvent(eq, 64, &data2); + UNSIGNED_INT_EQUALS(0, result); + + // Now sceKernelWaitEqueue should return both events. + OrbisKernelEvent evs[2]; + memset(evs, 0, sizeof(evs)); + count = 0; + result = sceKernelWaitEqueue(eq, evs, 2, &count, nullptr); + UNSIGNED_INT_EQUALS(0, result); + CHECK_EQUAL(2, count); + + // Check returned data for both events. + PrintEventData(&evs[0]); + if (evs[0].ident == 32) { + CHECK_EQUAL(32, evs[0].ident); + CHECK_EQUAL(-11, evs[0].filter); + CHECK_EQUAL(32, evs[0].flags); + CHECK_EQUAL(0, evs[0].fflags); + CHECK(evs[0].data == 0); + CHECK(evs[0].user_data != 0); + CHECK_EQUAL(100, *(u64*)evs[0].user_data); + } else { + CHECK_EQUAL(64, evs[0].ident); + CHECK_EQUAL(-11, evs[0].filter); + CHECK_EQUAL(32, evs[0].flags); + CHECK_EQUAL(0, evs[0].fflags); + CHECK(evs[0].data == 0); + CHECK(evs[0].user_data != 0); + CHECK_EQUAL(200, *(u64*)evs[0].user_data); + } + PrintEventData(&evs[1]); + if (evs[1].ident == 32) { + CHECK_EQUAL(32, evs[1].ident); + CHECK_EQUAL(-11, evs[1].filter); + CHECK_EQUAL(32, evs[1].flags); + CHECK_EQUAL(0, evs[1].fflags); + CHECK(evs[1].data == 0); + CHECK(evs[1].user_data != 0); + CHECK_EQUAL(100, *(u64*)evs[1].user_data); + } else { + CHECK_EQUAL(64, evs[1].ident); + CHECK_EQUAL(-11, evs[1].filter); + CHECK_EQUAL(32, evs[1].flags); + CHECK_EQUAL(0, evs[1].fflags); + CHECK(evs[1].data == 0); + CHECK(evs[1].user_data != 0); + CHECK_EQUAL(200, *(u64*)evs[1].user_data); + } + + // Now only trigger the second. + result = sceKernelTriggerUserEvent(eq, 64, &data1); + UNSIGNED_INT_EQUALS(0, result); + + // Only the second event should return. + memset(evs, 0, sizeof(evs)); + count = 0; + result = sceKernelWaitEqueue(eq, evs, 2, &count, nullptr); + UNSIGNED_INT_EQUALS(0, result); + CHECK_EQUAL(1, count); + + PrintEventData(&evs[0]); + CHECK_EQUAL(64, evs[0].ident); + CHECK_EQUAL(-11, evs[0].filter); + CHECK_EQUAL(32, evs[0].flags); + CHECK_EQUAL(0, evs[0].fflags); + CHECK(evs[0].data == 0); + CHECK(evs[0].user_data != 0); + CHECK_EQUAL(100, *(u64*)evs[0].user_data); + + // Delete the first user event. result = sceKernelDeleteUserEvent(eq, 32); UNSIGNED_INT_EQUALS(0, result); + // The second user event should remain present and triggerable. + result = sceKernelTriggerUserEvent(eq, 64, &data1); + UNSIGNED_INT_EQUALS(0, result); + + // Only the second event should return. + memset(evs, 0, sizeof(evs)); + count = 0; + result = sceKernelWaitEqueue(eq, evs, 2, &count, nullptr); + UNSIGNED_INT_EQUALS(0, result); + CHECK_EQUAL(1, count); + + PrintEventData(&evs[0]); + CHECK_EQUAL(64, evs[0].ident); + CHECK_EQUAL(-11, evs[0].filter); + CHECK_EQUAL(32, evs[0].flags); + CHECK_EQUAL(0, evs[0].fflags); + CHECK(evs[0].data == 0); + CHECK(evs[0].user_data != 0); + CHECK_EQUAL(100, *(u64*)evs[0].user_data); + + // Delete the second user event. + result = sceKernelDeleteUserEvent(eq, 64); + UNSIGNED_INT_EQUALS(0, result); + + // You cannot have two events with the same ident and filter. + result = sceKernelAddUserEventEdge(eq, 32); + UNSIGNED_INT_EQUALS(0, result); + // This call, despite succeeding, will do nothing. + result = sceKernelAddUserEvent(eq, 32); + UNSIGNED_INT_EQUALS(0, result); + + result = sceKernelTriggerUserEvent(eq, 32, &data1); + UNSIGNED_INT_EQUALS(0, result); + + // Wait for event to trigger. + memset(&ev, 0, sizeof(ev)); + count = 0; + result = sceKernelWaitEqueue(eq, &ev, 1, &count, nullptr); + UNSIGNED_INT_EQUALS(0, result); + CHECK_EQUAL(1, count); + + PrintEventData(&ev); + CHECK_EQUAL(32, ev.ident); + CHECK_EQUAL(-11, ev.filter); + CHECK_EQUAL(32, ev.flags); + CHECK_EQUAL(0, ev.fflags); + CHECK(ev.data == 0); + CHECK(ev.user_data != 0); + CHECK_EQUAL(100, *(u64*)ev.user_data); + + // Delete event + result = sceKernelDeleteUserEvent(eq, 32); + UNSIGNED_INT_EQUALS(0, result); + // Further proof there's not a second event + result = sceKernelDeleteUserEvent(eq, 32); + UNSIGNED_INT_EQUALS(ORBIS_KERNEL_ERROR_ENOENT, result); + // Delete the equeue when tests complete. result = sceKernelDeleteEqueue(eq); UNSIGNED_INT_EQUALS(0, result); @@ -620,4 +753,4 @@ TEST(EventTest, VblankEventTest) { // Clean up after test delete (handle); -} \ No newline at end of file +} diff --git a/tests/code/event_test/code/test.h b/tests/code/event_test/code/test.h index c9ea900c..91a15d23 100644 --- a/tests/code/event_test/code/test.h +++ b/tests/code/event_test/code/test.h @@ -32,6 +32,7 @@ struct OrbisKernelEvent { u64 user_data; }; +constexpr s32 ORBIS_KERNEL_ERROR_ENOENT = 0x80020002; constexpr s32 ORBIS_KERNEL_ERROR_ETIMEDOUT = 0x8002003c; s32 sceKernelUsleep(u32); From 953917f57d8fe089bebd6fb3dadd7b332591e074 Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Sun, 15 Feb 2026 21:21:07 -0600 Subject: [PATCH 18/33] Update test.h --- tests/code/event_test/code/test.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/code/event_test/code/test.h b/tests/code/event_test/code/test.h index 91a15d23..1d55e61c 100644 --- a/tests/code/event_test/code/test.h +++ b/tests/code/event_test/code/test.h @@ -32,7 +32,7 @@ struct OrbisKernelEvent { u64 user_data; }; -constexpr s32 ORBIS_KERNEL_ERROR_ENOENT = 0x80020002; +constexpr s32 ORBIS_KERNEL_ERROR_ENOENT = 0x80020002; constexpr s32 ORBIS_KERNEL_ERROR_ETIMEDOUT = 0x8002003c; s32 sceKernelUsleep(u32); From 54347935eb4493ac053cc1e5bec683f120bb80e4 Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Sun, 15 Feb 2026 21:29:11 -0600 Subject: [PATCH 19/33] Update test.cpp --- tests/code/event_test/code/test.cpp | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/code/event_test/code/test.cpp b/tests/code/event_test/code/test.cpp index b76c67f6..3c36f323 100644 --- a/tests/code/event_test/code/test.cpp +++ b/tests/code/event_test/code/test.cpp @@ -349,20 +349,20 @@ TEST(EventTest, UserEventTest) { UNSIGNED_INT_EQUALS(0, result); // Wait for event to trigger. - memset(&ev, 0, sizeof(ev)); + memset(evs, 0, sizeof(evs)); count = 0; - result = sceKernelWaitEqueue(eq, &ev, 1, &count, nullptr); + result = sceKernelWaitEqueue(eq, evs, 2, &count, nullptr); UNSIGNED_INT_EQUALS(0, result); CHECK_EQUAL(1, count); - PrintEventData(&ev); - CHECK_EQUAL(32, ev.ident); - CHECK_EQUAL(-11, ev.filter); - CHECK_EQUAL(32, ev.flags); - CHECK_EQUAL(0, ev.fflags); - CHECK(ev.data == 0); - CHECK(ev.user_data != 0); - CHECK_EQUAL(100, *(u64*)ev.user_data); + PrintEventData(&evs[0]); + CHECK_EQUAL(32, evs[0].ident); + CHECK_EQUAL(-11, evs[0].filter); + CHECK_EQUAL(32, evs[0].flags); + CHECK_EQUAL(0, evs[0].fflags); + CHECK(evs[0].data == 0); + CHECK(evs[0].user_data != 0); + CHECK_EQUAL(100, *(u64*)evs[0].user_data); // Delete event result = sceKernelDeleteUserEvent(eq, 32); From d89cb207ab4e269151597c7dad1380d23a05c4e5 Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Sun, 15 Feb 2026 22:15:37 -0600 Subject: [PATCH 20/33] Timer events are weird Added very basic tests for timer event behavior. --- tests/code/event_test/code/test.cpp | 171 +++++++++++++++++++++++++++- tests/code/event_test/code/test.h | 2 + 2 files changed, 171 insertions(+), 2 deletions(-) diff --git a/tests/code/event_test/code/test.cpp b/tests/code/event_test/code/test.cpp index 3c36f323..9751a797 100644 --- a/tests/code/event_test/code/test.cpp +++ b/tests/code/event_test/code/test.cpp @@ -23,8 +23,6 @@ static void PrintEventData(OrbisKernelEvent* ev) { printf("ev->data->time = %d\n", data.time); printf("ev->data->counter = %d\n", data.counter); printf("ev->data->flip_arg = 0x%lx\n", data.flip_arg); - } else { - printf("*(ev->data) = 0x%lx\n", *(u64*)ev->data); } } printf("ev->user_data = 0x%lx\n", ev->user_data); @@ -754,3 +752,172 @@ TEST(EventTest, VblankEventTest) { // Clean up after test delete (handle); } + +TEST(EventTest, TimerEventTest) { + // Timer events are pretty self explanatory. + // They have a timer, and when it expires, they trigger. + // This test will be rather simple, as I want to keep this single threaded. + OrbisKernelEqueue eq {}; + s32 result = sceKernelCreateEqueue(&eq, "TimerEventQueue"); + UNSIGNED_INT_EQUALS(0, result); + + // Start by adding a timer event + s64 data = 0x100; + result = sceKernelAddTimerEvent(eq, 0x10, 100000, &data); + UNSIGNED_INT_EQUALS(0, result); + + // This shouldn't fire immediately, confirm by waiting with a short timeout. + OrbisKernelEvent ev {}; + s32 count = 0; + u32 timeout = 1000; + result = sceKernelWaitEqueue(eq, &ev, 1, &count, &timeout); + UNSIGNED_INT_EQUALS(ORBIS_KERNEL_ERROR_ETIMEDOUT, result); + + // Now perform a sceKernelUsleep, wait out the timer timeout. + result = sceKernelUsleep(100000); + UNSIGNED_INT_EQUALS(0, result); + + // The event should've triggered, and should be returned by this wait + count = 0; + timeout = 1000; + result = sceKernelWaitEqueue(eq, &ev, 1, &count, &timeout); + UNSIGNED_INT_EQUALS(0, result); + UNSIGNED_INT_EQUALS(1, count); + + PrintEventData(&ev); + + // Check validity of returned data + CHECK_EQUAL(0x10, ev.ident); + CHECK_EQUAL(-7, ev.filter); + CHECK_EQUAL(32, ev.flags); + CHECK_EQUAL(0, ev.fflags); + // Trigger count perhaps? + CHECK_EQUAL(1, ev.data); + CHECK(ev.user_data != 0); + CHECK_EQUAL(data, *(s64*)ev.user_data); + + // Since the event has the clear flag, sceKernelWaitEqueue should've reset the timer. + // This wait should fail, as the timer was only recently reset. + count = 0; + timeout = 1000; + result = sceKernelWaitEqueue(eq, &ev, 1, &count, &timeout); + UNSIGNED_INT_EQUALS(ORBIS_KERNEL_ERROR_ETIMEDOUT, result); + + // Now perform a sceKernelUsleep, wait out the timer timeout. + result = sceKernelUsleep(100000); + UNSIGNED_INT_EQUALS(0, result); + + // The event should've triggered, and should be returned by this wait + count = 0; + timeout = 1000; + result = sceKernelWaitEqueue(eq, &ev, 1, &count, &timeout); + UNSIGNED_INT_EQUALS(0, result); + UNSIGNED_INT_EQUALS(1, count); + + PrintEventData(&ev); + + // Check validity of returned data + CHECK_EQUAL(0x10, ev.ident); + CHECK_EQUAL(-7, ev.filter); + CHECK_EQUAL(32, ev.flags); + CHECK_EQUAL(0, ev.fflags); + CHECK_EQUAL(1, ev.data); + CHECK(ev.user_data != 0); + CHECK_EQUAL(data, *(s64*)ev.user_data); + + // Wait longer this time + result = sceKernelUsleep(1000000); + UNSIGNED_INT_EQUALS(0, result); + + // The event should've triggered, and should be returned by this wait + count = 0; + timeout = 1000; + result = sceKernelWaitEqueue(eq, &ev, 1, &count, &timeout); + UNSIGNED_INT_EQUALS(0, result); + UNSIGNED_INT_EQUALS(1, count); + + PrintEventData(&ev); + + // Check validity of returned data + CHECK_EQUAL(0x10, ev.ident); + CHECK_EQUAL(-7, ev.filter); + CHECK_EQUAL(32, ev.flags); + CHECK_EQUAL(0, ev.fflags); + // This event should trigger 10 times, which sets data to 10. + CHECK_EQUAL(10, ev.data); + CHECK(ev.user_data != 0); + CHECK_EQUAL(data, *(s64*)ev.user_data); + + // Replace timer event. + s64 data2 = 0x200; + result = sceKernelAddTimerEvent(eq, 0x10, 2000000, &data2); + UNSIGNED_INT_EQUALS(0, result); + + // This wait should fail, as the timer was only recently reset. + count = 0; + timeout = 1000; + result = sceKernelWaitEqueue(eq, &ev, 1, &count, &timeout); + UNSIGNED_INT_EQUALS(ORBIS_KERNEL_ERROR_ETIMEDOUT, result); + + // Now perform a sceKernelUsleep, wait out the timer timeout. + result = sceKernelUsleep(50000); + UNSIGNED_INT_EQUALS(0, result); + + // This wait should fail, as we're still before the original timeout + count = 0; + timeout = 1000; + result = sceKernelWaitEqueue(eq, &ev, 1, &count, &timeout); + UNSIGNED_INT_EQUALS(ORBIS_KERNEL_ERROR_ETIMEDOUT, result); + + // Now perform a sceKernelUsleep, wait out the timer timeout. + result = sceKernelUsleep(50000); + UNSIGNED_INT_EQUALS(0, result); + + // Calling sceKernelWaitEqueue during this period of time, where the old timer would've fired but not the new one, + // results in sceKernelWaitEqueue waiting out the remainder of the new event timer, despite the short timeout input. + count = 0; + timeout = 1000; + result = sceKernelWaitEqueue(eq, &ev, 1, &count, &timeout); + UNSIGNED_INT_EQUALS(0, result); + UNSIGNED_INT_EQUALS(1, count); + + PrintEventData(&ev); + + // Check validity of returned data + CHECK_EQUAL(0x10, ev.ident); + CHECK_EQUAL(-7, ev.filter); + CHECK_EQUAL(32, ev.flags); + CHECK_EQUAL(0, ev.fflags); + CHECK_EQUAL(1, ev.data); + CHECK(ev.user_data != 0); + CHECK_EQUAL(data2, *(s64*)ev.user_data); + + // Additionally, if we wait out the new event timeout manually, we'll only see 1 trigger. + result = sceKernelUsleep(2000000); + UNSIGNED_INT_EQUALS(0, result); + + count = 0; + timeout = 1000; + result = sceKernelWaitEqueue(eq, &ev, 1, &count, &timeout); + UNSIGNED_INT_EQUALS(0, result); + UNSIGNED_INT_EQUALS(1, count); + + PrintEventData(&ev); + + // Check validity of returned data + CHECK_EQUAL(0x10, ev.ident); + CHECK_EQUAL(-7, ev.filter); + CHECK_EQUAL(32, ev.flags); + CHECK_EQUAL(0, ev.fflags); + CHECK_EQUAL(1, ev.data); + CHECK(ev.user_data != 0); + CHECK_EQUAL(data2, *(s64*)ev.user_data); + + // Delete the timer event + result = sceKernelDeleteTimerEvent(eq, 0x10); + UNSIGNED_INT_EQUALS(0, result); + + // Delete the equeue + result = sceKernelDeleteEqueue(eq); + UNSIGNED_INT_EQUALS(0, result); +} \ No newline at end of file diff --git a/tests/code/event_test/code/test.h b/tests/code/event_test/code/test.h index 1d55e61c..8ec8ffa1 100644 --- a/tests/code/event_test/code/test.h +++ b/tests/code/event_test/code/test.h @@ -43,6 +43,8 @@ s32 sceKernelAddUserEvent(OrbisKernelEqueue eq, s32 id); s32 sceKernelAddUserEventEdge(OrbisKernelEqueue eq, s32 id); s32 sceKernelTriggerUserEvent(OrbisKernelEqueue eq, s32 id, void* user_data); s32 sceKernelDeleteUserEvent(OrbisKernelEqueue eq, s32 id); +s32 sceKernelAddTimerEvent(OrbisKernelEqueue eq, s32 id, u32 time, void* user_data); +s32 sceKernelDeleteTimerEvent(OrbisKernelEqueue eq, s32 id); s32 sceKernelAllocateMainDirectMemory(u64 size, u64 align, s32 mtype, s64* phys_out); s32 sceKernelMapDirectMemory(void** addr, u64 size, s32 prot, s32 flags, s64 offset, u64 align); From 86e94ea841bcd69baf098e5eb84b13ecd0881345 Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Mon, 16 Feb 2026 14:53:41 -0600 Subject: [PATCH 21/33] Adjustments --- tests/code/event_test/code/test.cpp | 43 ++++++++++++++--------------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/tests/code/event_test/code/test.cpp b/tests/code/event_test/code/test.cpp index 9751a797..f65e60ce 100644 --- a/tests/code/event_test/code/test.cpp +++ b/tests/code/event_test/code/test.cpp @@ -853,28 +853,10 @@ TEST(EventTest, TimerEventTest) { result = sceKernelAddTimerEvent(eq, 0x10, 2000000, &data2); UNSIGNED_INT_EQUALS(0, result); - // This wait should fail, as the timer was only recently reset. - count = 0; - timeout = 1000; - result = sceKernelWaitEqueue(eq, &ev, 1, &count, &timeout); - UNSIGNED_INT_EQUALS(ORBIS_KERNEL_ERROR_ETIMEDOUT, result); - - // Now perform a sceKernelUsleep, wait out the timer timeout. - result = sceKernelUsleep(50000); - UNSIGNED_INT_EQUALS(0, result); - - // This wait should fail, as we're still before the original timeout - count = 0; - timeout = 1000; - result = sceKernelWaitEqueue(eq, &ev, 1, &count, &timeout); - UNSIGNED_INT_EQUALS(ORBIS_KERNEL_ERROR_ETIMEDOUT, result); - - // Now perform a sceKernelUsleep, wait out the timer timeout. - result = sceKernelUsleep(50000); + // If we wait out the new event timeout manually, we'll only see 1 trigger. + result = sceKernelUsleep(2000000); UNSIGNED_INT_EQUALS(0, result); - // Calling sceKernelWaitEqueue during this period of time, where the old timer would've fired but not the new one, - // results in sceKernelWaitEqueue waiting out the remainder of the new event timer, despite the short timeout input. count = 0; timeout = 1000; result = sceKernelWaitEqueue(eq, &ev, 1, &count, &timeout); @@ -892,13 +874,30 @@ TEST(EventTest, TimerEventTest) { CHECK(ev.user_data != 0); CHECK_EQUAL(data2, *(s64*)ev.user_data); - // Additionally, if we wait out the new event timeout manually, we'll only see 1 trigger. - result = sceKernelUsleep(2000000); + // Now test for a weird edge case. + // When replacing the timer, if the new timer is longer, + // there is a period where sceKernelWaitEqueue will wait out the event timer. + // This call will fail + count = 0; + timeout = 1000; + result = sceKernelWaitEqueue(eq, &ev, 1, &count, &timeout); + UNSIGNED_INT_EQUALS(ORBIS_KERNEL_ERROR_ETIMEDOUT, result); + + // Now perform a sceKernelUsleep, but for half of the old timer + result = sceKernelUsleep(50000); UNSIGNED_INT_EQUALS(0, result); + // This wait should fail, as we're still before the original timeout count = 0; timeout = 1000; result = sceKernelWaitEqueue(eq, &ev, 1, &count, &timeout); + UNSIGNED_INT_EQUALS(ORBIS_KERNEL_ERROR_ETIMEDOUT, result); + + // Now run sceKernelWaitEqueue with a long enough timeout to fall in the difference. + // Instead of failing with ETIMEDOUT, this call blocks until the event is triggered. + count = 0; + timeout = 60000; + result = sceKernelWaitEqueue(eq, &ev, 1, &count, &timeout); UNSIGNED_INT_EQUALS(0, result); UNSIGNED_INT_EQUALS(1, count); From 7b9853b9251e6e86a742c24bf74cd4a134fc7163 Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Mon, 16 Feb 2026 15:07:56 -0600 Subject: [PATCH 22/33] More weirdness --- tests/code/event_test/code/test.cpp | 53 +++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/tests/code/event_test/code/test.cpp b/tests/code/event_test/code/test.cpp index f65e60ce..c6666e9d 100644 --- a/tests/code/event_test/code/test.cpp +++ b/tests/code/event_test/code/test.cpp @@ -916,6 +916,59 @@ TEST(EventTest, TimerEventTest) { result = sceKernelDeleteTimerEvent(eq, 0x10); UNSIGNED_INT_EQUALS(0, result); + // Add timer event back in. + result = sceKernelAddTimerEvent(eq, 0x10, 100000, &data); + UNSIGNED_INT_EQUALS(0, result); + + // Wait for the event to trigger + result = sceKernelUsleep(100000); + UNSIGNED_INT_EQUALS(0, result); + + // Now replace it + result = sceKernelAddTimerEvent(eq, 0x10, 200000, &data2); + UNSIGNED_INT_EQUALS(0, result); + + // The trigger state isn't cleared + count = 0; + timeout = 1000; + result = sceKernelWaitEqueue(eq, &ev, 1, &count, &timeout); + UNSIGNED_INT_EQUALS(0, result); + UNSIGNED_INT_EQUALS(1, count); + + PrintEventData(&ev); + + // Check validity of returned data + CHECK_EQUAL(0x10, ev.ident); + CHECK_EQUAL(-7, ev.filter); + CHECK_EQUAL(32, ev.flags); + CHECK_EQUAL(0, ev.fflags); + CHECK_EQUAL(1, ev.data); + CHECK(ev.user_data != 0); + CHECK_EQUAL(data2, *(s64*)ev.user_data); + + // Check if the same edge case is present + count = 0; + timeout = 1000; + result = sceKernelWaitEqueue(eq, &ev, 1, &count, &timeout); + UNSIGNED_INT_EQUALS(ORBIS_KERNEL_ERROR_ETIMEDOUT, result); + + count = 0; + timeout = 100000; + result = sceKernelWaitEqueue(eq, &ev, 1, &count, &timeout); + UNSIGNED_INT_EQUALS(0, result); + UNSIGNED_INT_EQUALS(1, count); + + PrintEventData(&ev); + + // Check validity of returned data + CHECK_EQUAL(0x10, ev.ident); + CHECK_EQUAL(-7, ev.filter); + CHECK_EQUAL(32, ev.flags); + CHECK_EQUAL(0, ev.fflags); + CHECK_EQUAL(1, ev.data); + CHECK(ev.user_data != 0); + CHECK_EQUAL(data2, *(s64*)ev.user_data); + // Delete the equeue result = sceKernelDeleteEqueue(eq); UNSIGNED_INT_EQUALS(0, result); From 10431fc1ff64a67dd06bb37b344951c6b7a95f04 Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Mon, 16 Feb 2026 15:08:04 -0600 Subject: [PATCH 23/33] Update test.cpp --- tests/code/event_test/code/test.cpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/code/event_test/code/test.cpp b/tests/code/event_test/code/test.cpp index c6666e9d..47e645be 100644 --- a/tests/code/event_test/code/test.cpp +++ b/tests/code/event_test/code/test.cpp @@ -875,7 +875,7 @@ TEST(EventTest, TimerEventTest) { CHECK_EQUAL(data2, *(s64*)ev.user_data); // Now test for a weird edge case. - // When replacing the timer, if the new timer is longer, + // When replacing the timer, if the new timer is longer, // there is a period where sceKernelWaitEqueue will wait out the event timer. // This call will fail count = 0; @@ -929,9 +929,9 @@ TEST(EventTest, TimerEventTest) { UNSIGNED_INT_EQUALS(0, result); // The trigger state isn't cleared - count = 0; + count = 0; timeout = 1000; - result = sceKernelWaitEqueue(eq, &ev, 1, &count, &timeout); + result = sceKernelWaitEqueue(eq, &ev, 1, &count, &timeout); UNSIGNED_INT_EQUALS(0, result); UNSIGNED_INT_EQUALS(1, count); @@ -947,14 +947,14 @@ TEST(EventTest, TimerEventTest) { CHECK_EQUAL(data2, *(s64*)ev.user_data); // Check if the same edge case is present - count = 0; + count = 0; timeout = 1000; - result = sceKernelWaitEqueue(eq, &ev, 1, &count, &timeout); + result = sceKernelWaitEqueue(eq, &ev, 1, &count, &timeout); UNSIGNED_INT_EQUALS(ORBIS_KERNEL_ERROR_ETIMEDOUT, result); - count = 0; + count = 0; timeout = 100000; - result = sceKernelWaitEqueue(eq, &ev, 1, &count, &timeout); + result = sceKernelWaitEqueue(eq, &ev, 1, &count, &timeout); UNSIGNED_INT_EQUALS(0, result); UNSIGNED_INT_EQUALS(1, count); From 409d4560ac40a59aa57f5be1a03b67db91f2a75c Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Mon, 16 Feb 2026 15:33:20 -0600 Subject: [PATCH 24/33] Test having multiple active timer events. --- tests/code/event_test/code/test.cpp | 65 +++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/tests/code/event_test/code/test.cpp b/tests/code/event_test/code/test.cpp index 47e645be..f5810b4e 100644 --- a/tests/code/event_test/code/test.cpp +++ b/tests/code/event_test/code/test.cpp @@ -969,6 +969,71 @@ TEST(EventTest, TimerEventTest) { CHECK(ev.user_data != 0); CHECK_EQUAL(data2, *(s64*)ev.user_data); + // Perform wait with high timeout + // This returns immediately after the first trigger, rather than waiting out timeout. + count = 0; + timeout = 1000000; + result = sceKernelWaitEqueue(eq, &ev, 1, &count, &timeout); + UNSIGNED_INT_EQUALS(0, result); + UNSIGNED_INT_EQUALS(1, count); + + PrintEventData(&ev); + + // Check validity of returned data + CHECK_EQUAL(0x10, ev.ident); + CHECK_EQUAL(-7, ev.filter); + CHECK_EQUAL(32, ev.flags); + CHECK_EQUAL(0, ev.fflags); + CHECK_EQUAL(1, ev.data); + CHECK(ev.user_data != 0); + CHECK_EQUAL(data2, *(s64*)ev.user_data); + + // Add second timer event to queue, give it a shorter timeout. + result = sceKernelAddTimerEvent(eq, 0x20, 10000, &data); + UNSIGNED_INT_EQUALS(0, result); + + // Now we have a really fast event, and a lengthy event. + // Wait for our longer event to trigger + result = sceKernelUsleep(200000); + UNSIGNED_INT_EQUALS(0, result); + + OrbisKernelEvent evs[2]; + count = 0; + timeout = 0; + result = sceKernelWaitEqueue(eq, evs, 2, &count, &timeout); + UNSIGNED_INT_EQUALS(0, result); + // This should return both events. + CHECK_EQUAL(2, count); + + for (OrbisKernelEvent ev: evs) { + PrintEventData(&ev); + if (ev.ident == 0x20) { + // Check validity of returned data + CHECK_EQUAL(0x20, ev.ident); + CHECK_EQUAL(-7, ev.filter); + CHECK_EQUAL(32, ev.flags); + CHECK_EQUAL(0, ev.fflags); + CHECK_EQUAL(20, ev.data); + CHECK(ev.user_data != 0); + CHECK_EQUAL(data, *(s64*)ev.user_data); + } else { + // Check validity of returned data + CHECK_EQUAL(0x10, ev.ident); + CHECK_EQUAL(-7, ev.filter); + CHECK_EQUAL(32, ev.flags); + CHECK_EQUAL(0, ev.fflags); + CHECK_EQUAL(1, ev.data); + CHECK(ev.user_data != 0); + CHECK_EQUAL(data2, *(s64*)ev.user_data); + } + } + + // Delete both events + result = sceKernelDeleteTimerEvent(eq, 0x10); + UNSIGNED_INT_EQUALS(0, result); + result = sceKernelDeleteTimerEvent(eq, 0x20); + UNSIGNED_INT_EQUALS(0, result); + // Delete the equeue result = sceKernelDeleteEqueue(eq); UNSIGNED_INT_EQUALS(0, result); From 9f24637258f80b913da252eff346fb901bb8959b Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Thu, 19 Feb 2026 14:19:03 -0600 Subject: [PATCH 25/33] Code for triggering GFX end of pipe events Submit related logic is mostly taken from red_prig's homebrew. --- tests/code/event_test/code/test.h | 4 + tests/code/event_test/code/video.h | 158 +++++++++++++++++++++++++---- 2 files changed, 141 insertions(+), 21 deletions(-) diff --git a/tests/code/event_test/code/test.h b/tests/code/event_test/code/test.h index 8ec8ffa1..ddbecf7e 100644 --- a/tests/code/event_test/code/test.h +++ b/tests/code/event_test/code/test.h @@ -90,6 +90,10 @@ s32 sceVideoOutWaitVblank(s32 handle); s32 sceVideoOutClose(s32 handle); u32 sceGnmDrawInitDefaultHardwareState350(u32* cmd_buf, u32 num_dwords); +s32 sceGnmSubmitCommandBuffers(u32 count, void** dcb_gpu_addrs, u32* dcb_sizes_in_bytes, void** ccb_gpu_addrs, u32* ccb_sizes_in_bytes); s32 sceGnmSubmitAndFlipCommandBuffers(u32 count, void** dcb_gpu_addrs, u32* dcb_sizes_in_bytes, void** ccb_gpu_addrs, u32* ccb_sizes_in_bytes, s32 video_handle, s32 buffer_index, s32 flip_mode, s64 flip_arg); +s32 sceGnmAddEqEvent(OrbisKernelEqueue eq, s32 event_id, void* user_data); +s32 sceGnmDeleteEqEvent(OrbisKernelEqueue eq, s32 event_id); +s32 sceGnmSubmitDone(); } \ No newline at end of file diff --git a/tests/code/event_test/code/video.h b/tests/code/event_test/code/video.h index 7b7540c1..4448e616 100644 --- a/tests/code/event_test/code/video.h +++ b/tests/code/event_test/code/video.h @@ -21,6 +21,7 @@ class VideoOut { // Equeue OrbisKernelEqueue flip_queue = nullptr; OrbisKernelEqueue vblank_queue = nullptr; + OrbisKernelEqueue gc_queue = nullptr; // Command buffer for EOP tests void* cmd_buf = nullptr; @@ -28,6 +29,7 @@ class VideoOut { u64 cmd_buf_size = 0x4000; u32 stream_size = 0; u32 cmd_start_offset = 0; + bool gpu_init = false; public: VideoOut(s32 width, s32 height) { @@ -50,6 +52,10 @@ class VideoOut { result = sceKernelCreateEqueue(&vblank_queue, "VideoOutVblankEqueue"); UNSIGNED_INT_EQUALS(0, result); + // Create graphics equeue + result = sceKernelCreateEqueue(&gc_queue, "VideoOutGraphicsEqueue"); + UNSIGNED_INT_EQUALS(0, result); + // Initialize command buffer for EOP flip tests result = sceKernelAllocateMainDirectMemory(cmd_buf_size, 0x4000, 0, &phys_cmd_buf); UNSIGNED_INT_EQUALS(0, result); @@ -79,12 +85,21 @@ class VideoOut { if (flip_queue != nullptr) { s32 result = sceKernelDeleteEqueue(flip_queue); UNSIGNED_INT_EQUALS(0, result); + flip_queue = nullptr; } // Delete vblank event queue if (vblank_queue != nullptr) { s32 result = sceKernelDeleteEqueue(vblank_queue); UNSIGNED_INT_EQUALS(0, result); + vblank_queue = nullptr; + } + + // Delete graphics event queue + if (gc_queue != nullptr) { + s32 result = sceKernelDeleteEqueue(gc_queue); + UNSIGNED_INT_EQUALS(0, result); + gc_queue = nullptr; } // Close video out handle @@ -124,16 +139,87 @@ class VideoOut { return result; }; + s32 addBuffer() { + // Create a buffer attribute + OrbisVideoOutBufferAttribute attr {}; + attr.pixel_format = 0x80002200; + attr.tiling_mode = 0; + attr.aspect_ratio = 0; + attr.width = width; + attr.height = height; + attr.pitch_in_pixel = width; + attr.option = 0; + attr.reserved0 = 0; + attr.reserved1 = 0; + + // Register buffer + s32 result = sceVideoOutRegisterBuffers(handle, buf_count, &buf_addr, 1, &attr); + + if (result >= 0) { + // Buffer registered successfully, increment buffers count. + buf_count++; + } + + return result; + }; + + s32 ensureGpuInit() { + u32 num_dwords = 0; + if (!gpu_init) { + void* cmd_buf_start = (void*)((u64)cmd_buf + cmd_start_offset); + u32* cmds = (u32*)cmd_buf_start; + num_dwords = sceGnmDrawInitDefaultHardwareState350(cmds, 0x100); + + gpu_init = true; + } + return num_dwords; + }; + + s32 submitWithEopInterrupt(s32 event_type, s32 dest_sel, void* dest_gpu_addr, s32 src_sel, u64 value, s32 cache_action, s32 cache_policy, s32 int_sel) { + // If necessary, write a GPU init packet to the buffer + void* cmd_buf_start = (void*)((u64)cmd_buf + cmd_start_offset); + u32* cmds = (u32*)cmd_buf_start; + cmds += ensureGpuInit(); + + // Write EventWriteEop packet to fire interrupt + cmds[0] = 0xc0044700; + u64 mask = 0xfffffffffc; + if (src_sel != 1) { + mask = 0xfffffffff8; + }; + + cmds[1] = (cache_policy & 3) * 0x2000000 + 0x500 + ((dest_sel & 0x10) << 23 | event_type & 0x3f | (cache_action & 0x3f) << 12); + cmds[2] = (s32)(mask & (u64)dest_gpu_addr); + cmds[3] = (s32)((mask & (u64)dest_gpu_addr) >> 32) + ((int_sel & 3) << 24) + (src_sel << 29 | (dest_sel & 1) << 16); + *(u64*)(cmds + 4) = value; + + cmds += 6; + stream_size = (u32)((u64)cmds - (u64)cmd_buf_start); + + // Track area used by this packet + cmd_start_offset += stream_size; + if (cmd_start_offset + stream_size > cmd_buf_size) { + cmd_start_offset = 0; + } + + // Submit command buffers, then submit done. + s32 result = sceGnmSubmitCommandBuffers(1, &cmd_buf_start, &stream_size, nullptr, nullptr); + if (result < 0) { + return result; + } + return sceGnmSubmitDone(); + }; + s32 submitAndFlip(s64 flip_arg) { if (buf_count == 0) { // SubmitAndFlip fails on buffer -1, save time by failing early. return -1; } - // Write GPU init packet to the pointer. + // If necessary, write a GPU init packet to the buffer void* cmd_buf_start = (void*)((u64)cmd_buf + cmd_start_offset); u32* cmds = (u32*)cmd_buf_start; - cmds += sceGnmDrawInitDefaultHardwareState350(cmds, 0x100); + cmds += ensureGpuInit(); // Write a flip packet to the pointer. cmds[0] = 0xc03e1000; @@ -141,6 +227,7 @@ class VideoOut { cmds += 64; stream_size = (u32)((u64)cmds - (u64)cmd_buf_start); + // Track area used by this packet cmd_start_offset += stream_size; if (cmd_start_offset + stream_size > cmd_buf_size) { cmd_start_offset = 0; @@ -148,33 +235,48 @@ class VideoOut { // Perform GPU submit s32 result = sceGnmSubmitAndFlipCommandBuffers(1, &cmd_buf_start, &stream_size, nullptr, nullptr, handle, current_buf, 1, flip_arg); - if (++current_buf == buf_count) { - current_buf = 0; + if (result == 0) { + if (++current_buf == buf_count) { + current_buf = 0; + } + return sceGnmSubmitDone(); } return result; }; - s32 addBuffer() { - // Create a buffer attribute - OrbisVideoOutBufferAttribute attr {}; - attr.pixel_format = 0x80002200; - attr.tiling_mode = 0; - attr.aspect_ratio = 0; - attr.width = width; - attr.height = height; - attr.pitch_in_pixel = width; - attr.option = 0; - attr.reserved0 = 0; - attr.reserved1 = 0; + s32 submitAndFlipWithEopInterrupt(s64 flip_arg) { + if (buf_count == 0) { + // SubmitAndFlip fails on buffer -1, save time by failing early. + return -1; + } - // Register buffer - s32 result = sceVideoOutRegisterBuffers(handle, buf_count, &buf_addr, 1, &attr); + // If necessary, write a GPU init packet to the buffer + void* cmd_buf_start = (void*)((u64)cmd_buf + cmd_start_offset); + u32* cmds = (u32*)cmd_buf_start; + cmds += ensureGpuInit(); - if (result >= 0) { - // Buffer registered successfully, increment buffers count. - buf_count++; + // Write a flip packet to the pointer. + cmds[0] = 0xc03e1000; + cmds[1] = 0x68750780; + cmds[5] = 4; + cmds[6] = 0; + cmds += 64; + stream_size = (u32)((u64)cmds - (u64)cmd_buf_start); + + // Track area used by this packet + cmd_start_offset += stream_size; + if (cmd_start_offset + stream_size > cmd_buf_size) { + cmd_start_offset = 0; } + // Perform GPU submit + s32 result = sceGnmSubmitAndFlipCommandBuffers(1, &cmd_buf_start, &stream_size, nullptr, nullptr, handle, current_buf, 1, flip_arg); + if (result == 0) { + if (++current_buf == buf_count) { + current_buf = 0; + } + return sceGnmSubmitDone(); + } return result; }; @@ -211,6 +313,20 @@ class VideoOut { } }; + s32 addGraphicsEvent(void* user_data) { return sceGnmAddEqEvent(gc_queue, 0x40, user_data); }; + + s32 deleteGraphicsEvent() { return sceGnmDeleteEqEvent(gc_queue, 0x40); }; + + s32 waitGraphicsEvent(OrbisKernelEvent* ev, s32 num, s32* out, u32 timeout) { + memset(ev, 0, sizeof(OrbisKernelEvent) * num); + *out = 0; + if (timeout == -1) { + return sceKernelWaitEqueue(vblank_queue, ev, num, out, nullptr); + } else { + return sceKernelWaitEqueue(vblank_queue, ev, num, out, &timeout); + } + }; + s32 waitFlip() { // Wait for flip s32 result = sceVideoOutIsFlipPending(handle); From 44df55d2da8952efb377b78ac4931b1ee39103e1 Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Thu, 19 Feb 2026 14:32:24 -0600 Subject: [PATCH 26/33] Inline some logic --- tests/code/event_test/code/video.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/code/event_test/code/video.h b/tests/code/event_test/code/video.h index 4448e616..fe68e606 100644 --- a/tests/code/event_test/code/video.h +++ b/tests/code/event_test/code/video.h @@ -175,7 +175,7 @@ class VideoOut { return num_dwords; }; - s32 submitWithEopInterrupt(s32 event_type, s32 dest_sel, void* dest_gpu_addr, s32 src_sel, u64 value, s32 cache_action, s32 cache_policy, s32 int_sel) { + s32 submitWithEopInterrupt(void* dest_gpu_addr, s32 src_sel, u64 value, s32 int_sel) { // If necessary, write a GPU init packet to the buffer void* cmd_buf_start = (void*)((u64)cmd_buf + cmd_start_offset); u32* cmds = (u32*)cmd_buf_start; @@ -188,9 +188,9 @@ class VideoOut { mask = 0xfffffffff8; }; - cmds[1] = (cache_policy & 3) * 0x2000000 + 0x500 + ((dest_sel & 0x10) << 23 | event_type & 0x3f | (cache_action & 0x3f) << 12); + cmds[1] = 0x504; cmds[2] = (s32)(mask & (u64)dest_gpu_addr); - cmds[3] = (s32)((mask & (u64)dest_gpu_addr) >> 32) + ((int_sel & 3) << 24) + (src_sel << 29 | (dest_sel & 1) << 16); + cmds[3] = (s32)((mask & (u64)dest_gpu_addr) >> 32) + ((int_sel & 3) << 24) + (src_sel << 29); *(u64*)(cmds + 4) = value; cmds += 6; From cf70721ef88102355036ed71c573603197fcb8f3 Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Thu, 19 Feb 2026 15:09:37 -0600 Subject: [PATCH 27/33] Fixes --- tests/code/event_test/code/video.h | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/tests/code/event_test/code/video.h b/tests/code/event_test/code/video.h index fe68e606..b79dabd2 100644 --- a/tests/code/event_test/code/video.h +++ b/tests/code/event_test/code/video.h @@ -10,7 +10,7 @@ class VideoOut { s32 handle = 0; s32 width = 0; s32 height = 0; - s32 current_buf = -1; + s32 current_buf = 0; // Buffer allocations void* buf_addr = nullptr; @@ -160,6 +160,11 @@ class VideoOut { buf_count++; } + if (current_buf == -1) { + // Now that we have buffers, avoid submitting to the empty buffer. + current_buf = 0; + } + return result; }; @@ -321,9 +326,9 @@ class VideoOut { memset(ev, 0, sizeof(OrbisKernelEvent) * num); *out = 0; if (timeout == -1) { - return sceKernelWaitEqueue(vblank_queue, ev, num, out, nullptr); + return sceKernelWaitEqueue(gc_queue, ev, num, out, nullptr); } else { - return sceKernelWaitEqueue(vblank_queue, ev, num, out, &timeout); + return sceKernelWaitEqueue(gc_queue, ev, num, out, &timeout); } }; From 15250627ab297599ee648ab39f780831574373f6 Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Thu, 19 Feb 2026 15:13:49 -0600 Subject: [PATCH 28/33] Basic tests for graphics events Based loosely on some tests red_prig shared --- tests/code/event_test/code/test.cpp | 139 ++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) diff --git a/tests/code/event_test/code/test.cpp b/tests/code/event_test/code/test.cpp index f5810b4e..200198f8 100644 --- a/tests/code/event_test/code/test.cpp +++ b/tests/code/event_test/code/test.cpp @@ -753,6 +753,145 @@ TEST(EventTest, VblankEventTest) { delete (handle); } +TEST(EventTest, GraphicsEventTest) { + // These events are triggered through GPU command processing. + // Most commonly through prepare flip packets with interrupts, + // or through EventWriteEop packets. + VideoOut* handle = new VideoOut(1920, 1080); + + // Start by using the handle to create an EOP event + s64 val = 1; + s32 result = handle->addGraphicsEvent(&val); + UNSIGNED_INT_EQUALS(0, result); + + // As-is, trying to wait on the event should timeout. + OrbisKernelEvent ev {}; + s32 count = 0; + result = handle->waitGraphicsEvent(&ev, 1, &count, 100000); + UNSIGNED_INT_EQUALS(ORBIS_KERNEL_ERROR_ETIMEDOUT, result); + + // Map a small area of memory with GPU access + void* gpu_addr = nullptr; + s64 gpu_paddr; + result = sceKernelAllocateMainDirectMemory(0x4000, 0, 0, &gpu_paddr); + UNSIGNED_INT_EQUALS(0, result); + result = sceKernelMapDirectMemory(&gpu_addr, 0x4000, 0x33, 0, gpu_paddr, 0); + UNSIGNED_INT_EQUALS(0, result); + + // Now fire off a GPU packet that should trigger this event. + // We don't really care for the actual memory data this packet does, this test is for events. + result = handle->submitWithEopInterrupt(gpu_addr, 0, 0x1000000000, 1); + UNSIGNED_INT_EQUALS(0, result); + + // Now we should eventually see the event fire. + result = handle->waitGraphicsEvent(&ev, 1, &count, -1); + UNSIGNED_INT_EQUALS(0, result); + UNSIGNED_INT_EQUALS(1, count); + + PrintEventData(&ev); + + // Check values + CHECK_EQUAL(0x40, ev.ident); + CHECK_EQUAL(-14, ev.filter); + CHECK_EQUAL(0x20, ev.flags); + CHECK_EQUAL(0, ev.fflags); + // Not sure what's in the data field, so not checking yet. + CHECK(ev.user_data != 0); + CHECK_EQUAL(val, *(s64*)ev.user_data); + + // Gfx events have EV_CLEAR, so you wont see them again after sceKernelWaitEqueue + result = handle->waitGraphicsEvent(&ev, 1, &count, 100000); + UNSIGNED_INT_EQUALS(ORBIS_KERNEL_ERROR_ETIMEDOUT, result); + + // Fire off another interrupt + result = handle->submitWithEopInterrupt(gpu_addr, 0, 0x1000000000, 2); + UNSIGNED_INT_EQUALS(0, result); + + // Should eventually see the event fire. + result = handle->waitGraphicsEvent(&ev, 1, &count, -1); + UNSIGNED_INT_EQUALS(0, result); + UNSIGNED_INT_EQUALS(1, count); + + PrintEventData(&ev); + + // Check values + CHECK_EQUAL(0x40, ev.ident); + CHECK_EQUAL(-14, ev.filter); + CHECK_EQUAL(0x20, ev.flags); + CHECK_EQUAL(0, ev.fflags); + // Not sure what's in the data field, so not checking yet. + CHECK(ev.user_data != 0); + CHECK_EQUAL(val, *(s64*)ev.user_data); + + // These are valid packets that should not fire an interrupt. + result = handle->submitWithEopInterrupt(gpu_addr, 0, 0x1000000000, 0); + UNSIGNED_INT_EQUALS(0, result); + result = handle->submitWithEopInterrupt(gpu_addr, 0, 0x1000000000, 3); + UNSIGNED_INT_EQUALS(0, result); + + result = handle->waitGraphicsEvent(&ev, 1, &count, 100000); + UNSIGNED_INT_EQUALS(ORBIS_KERNEL_ERROR_ETIMEDOUT, result); + + // Multiple interrupts before checking event + result = handle->submitWithEopInterrupt(gpu_addr, 0, 0x1000000000, 1); + UNSIGNED_INT_EQUALS(0, result); + result = handle->submitWithEopInterrupt(gpu_addr, 0, 0x1000000000, 1); + UNSIGNED_INT_EQUALS(0, result); + + // Should eventually see the event fire. + result = handle->waitGraphicsEvent(&ev, 1, &count, -1); + UNSIGNED_INT_EQUALS(0, result); + UNSIGNED_INT_EQUALS(1, count); + + PrintEventData(&ev); + + // Check values + CHECK_EQUAL(0x40, ev.ident); + CHECK_EQUAL(-14, ev.filter); + CHECK_EQUAL(0x20, ev.flags); + CHECK_EQUAL(0, ev.fflags); + // Not sure what's in the data field, so not checking yet. + CHECK(ev.user_data != 0); + CHECK_EQUAL(val, *(s64*)ev.user_data); + + // Now try with special flips + // To run a flip, we need to register a video out buffer first. + result = handle->addBuffer(); + UNSIGNED_INT_EQUALS(0, result); + + // Now we can submit and flip + result = handle->submitAndFlipWithEopInterrupt(0x1000000000); + UNSIGNED_INT_EQUALS(0, result); + + // Should eventually see the event fire. + result = handle->waitGraphicsEvent(&ev, 1, &count, -1); + UNSIGNED_INT_EQUALS(0, result); + UNSIGNED_INT_EQUALS(1, count); + + PrintEventData(&ev); + + // Check values + CHECK_EQUAL(0x40, ev.ident); + CHECK_EQUAL(-14, ev.filter); + CHECK_EQUAL(0x20, ev.flags); + CHECK_EQUAL(0, ev.fflags); + // Not sure what's in the data field, so not checking yet. + CHECK(ev.user_data != 0); + CHECK_EQUAL(val, *(s64*)ev.user_data); + + // Still has the EV_CLEAR flag, so it shouldn't appear again. + result = handle->waitGraphicsEvent(&ev, 1, &count, 100000); + UNSIGNED_INT_EQUALS(ORBIS_KERNEL_ERROR_ETIMEDOUT, result); + + // Normal submit and flip shouldn't trigger the event. + result = handle->submitAndFlip(0x1000000000); + UNSIGNED_INT_EQUALS(0, result); + result = handle->waitGraphicsEvent(&ev, 1, &count, 100000); + UNSIGNED_INT_EQUALS(ORBIS_KERNEL_ERROR_ETIMEDOUT, result); + + delete (handle); +} + TEST(EventTest, TimerEventTest) { // Timer events are pretty self explanatory. // They have a timer, and when it expires, they trigger. From a7be8ce065587805b1a07cdbb44474efdd370d53 Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Thu, 19 Feb 2026 16:47:50 -0600 Subject: [PATCH 29/33] Basic HRTimer test --- tests/code/event_test/code/test.cpp | 114 +++++++++++++++++++++++++++- tests/code/event_test/code/test.h | 20 +++-- 2 files changed, 126 insertions(+), 8 deletions(-) diff --git a/tests/code/event_test/code/test.cpp b/tests/code/event_test/code/test.cpp index 200198f8..43d20c21 100644 --- a/tests/code/event_test/code/test.cpp +++ b/tests/code/event_test/code/test.cpp @@ -889,6 +889,9 @@ TEST(EventTest, GraphicsEventTest) { result = handle->waitGraphicsEvent(&ev, 1, &count, 100000); UNSIGNED_INT_EQUALS(ORBIS_KERNEL_ERROR_ETIMEDOUT, result); + result = handle->deleteGraphicsEvent(); + UNSIGNED_INT_EQUALS(0, result); + delete (handle); } @@ -930,7 +933,7 @@ TEST(EventTest, TimerEventTest) { CHECK_EQUAL(-7, ev.filter); CHECK_EQUAL(32, ev.flags); CHECK_EQUAL(0, ev.fflags); - // Trigger count perhaps? + // Timer events store a count of how many times the event has fired since the event was last returned. CHECK_EQUAL(1, ev.data); CHECK(ev.user_data != 0); CHECK_EQUAL(data, *(s64*)ev.user_data); @@ -1176,4 +1179,113 @@ TEST(EventTest, TimerEventTest) { // Delete the equeue result = sceKernelDeleteEqueue(eq); UNSIGNED_INT_EQUALS(0, result); +} + +TEST(EventTest, HighResTimerEvent) { + // HRTimer events are pretty self explanatory. + // They have a timer, and when it expires, they trigger. + // These are "high-res", which is supposed to mean they're more accurate. + OrbisKernelEqueue eq {}; + s32 result = sceKernelCreateEqueue(&eq, "HRTimerEventQueue"); + UNSIGNED_INT_EQUALS(0, result); + + // Add a high-res timer to the equeue + // This timespec should match the timeout used for my timer event tests. + OrbisKernelTimespec time {0, 10000000}; + s64 data = 0x100; + result = sceKernelAddHRTimerEvent(eq, 0x10, &time, &data); + UNSIGNED_INT_EQUALS(0, result); + + // Wait for timer to fire + OrbisKernelEvent ev {}; + s32 count = 0; + result = sceKernelWaitEqueue(eq, &ev, 1, &count, nullptr); + UNSIGNED_INT_EQUALS(0, result); + CHECK_EQUAL(1, count); + + PrintEventData(&ev); + + // Validate returned data + CHECK_EQUAL(0x10, ev.ident); + CHECK_EQUAL(-15, ev.filter); + CHECK_EQUAL(0x30, ev.flags); + CHECK_EQUAL(0, ev.fflags); + CHECK_EQUAL(1, ev.data); + CHECK(ev.user_data != 0); + CHECK_EQUAL(data, *(s64*)ev.user_data); + + // HR timer events use EV_ONESHOT | EV_CLEAR + // This means that, once returned via sceKernelWaitEqueue, the event is gone. + result = sceKernelDeleteHRTimerEvent(eq, 0x10); + UNSIGNED_INT_EQUALS(ORBIS_KERNEL_ERROR_ENOENT, result); + + // Add a new HRTimer event + result = sceKernelAddHRTimerEvent(eq, 0x10, &time, &data); + UNSIGNED_INT_EQUALS(0, result); + + // Use sceKernelUsleep to wait out 10 timer intervals. + result = sceKernelUsleep(100000); + UNSIGNED_INT_EQUALS(0, result); + + // Wait for timer to fire + result = sceKernelWaitEqueue(eq, &ev, 1, &count, nullptr); + UNSIGNED_INT_EQUALS(0, result); + CHECK_EQUAL(1, count); + + PrintEventData(&ev); + + // Validate returned data + CHECK_EQUAL(0x10, ev.ident); + CHECK_EQUAL(-15, ev.filter); + CHECK_EQUAL(0x30, ev.flags); + CHECK_EQUAL(0, ev.fflags); + // Event data is still one, + // Might indicate that the timer only triggers once? + CHECK_EQUAL(1, ev.data); + CHECK(ev.user_data != 0); + CHECK_EQUAL(data, *(s64*)ev.user_data); + + // Check for potential oddities like what timer events have + result = sceKernelAddHRTimerEvent(eq, 0x10, &time, &data); + UNSIGNED_INT_EQUALS(0, result); + s64 data2 = 0x200; + OrbisKernelTimespec time2 {0, 100000000}; + result = sceKernelAddHRTimerEvent(eq, 0x10, &time2, &data2); + UNSIGNED_INT_EQUALS(0, result); + + // If these succeed like this, then the high-res timers have the same edge case as normal timers. + u32 micros = 1000; + result = sceKernelWaitEqueue(eq, &ev, 1, &count, µs); + UNSIGNED_INT_EQUALS(ORBIS_KERNEL_ERROR_ETIMEDOUT, result); + micros = 20000; + result = sceKernelWaitEqueue(eq, &ev, 1, &count, µs); + UNSIGNED_INT_EQUALS(0, result); + CHECK_EQUAL(1, count); + + PrintEventData(&ev); + + // Validate returned data + CHECK_EQUAL(0x10, ev.ident); + CHECK_EQUAL(-15, ev.filter); + CHECK_EQUAL(0x30, ev.flags); + CHECK_EQUAL(0, ev.fflags); + // Event data is still one, + // Might indicate that the timer only triggers once? + CHECK_EQUAL(1, ev.data); + CHECK(ev.user_data != 0); + CHECK_EQUAL(data2, *(s64*)ev.user_data); + + // To properly test real-world behavior, fire these off and wait for events in a loop. + for (s32 i = 0; i < 100; i++) { + time.tv_nsec = 10000000; + result = sceKernelAddHRTimerEvent(eq, 0x10, &time, &data); + UNSIGNED_INT_EQUALS(0, result); + // Real hardware doesn't entirely manage proper accuracy at 10000 micros. + // 20000 micros is enough for this to consistently pass though. + micros = 20000; + result = sceKernelWaitEqueue(eq, &ev, 1, &count, µs); + UNSIGNED_INT_EQUALS(0, result); + result = sceKernelDeleteHRTimerEvent(eq, 0x10); + UNSIGNED_INT_EQUALS(ORBIS_KERNEL_ERROR_ENOENT, result); + } } \ No newline at end of file diff --git a/tests/code/event_test/code/test.h b/tests/code/event_test/code/test.h index ddbecf7e..d88d7572 100644 --- a/tests/code/event_test/code/test.h +++ b/tests/code/event_test/code/test.h @@ -17,12 +17,6 @@ extern "C" { typedef void* OrbisKernelEqueue; -struct VideoOutEventData { - u64 time : 12; - u64 counter : 4; - u64 flip_arg : 48; -}; - struct OrbisKernelEvent { u64 ident; s16 filter; @@ -32,6 +26,11 @@ struct OrbisKernelEvent { u64 user_data; }; +struct OrbisKernelTimespec { + s64 tv_sec; + s64 tv_nsec; +}; + constexpr s32 ORBIS_KERNEL_ERROR_ENOENT = 0x80020002; constexpr s32 ORBIS_KERNEL_ERROR_ETIMEDOUT = 0x8002003c; @@ -45,7 +44,8 @@ s32 sceKernelTriggerUserEvent(OrbisKernelEqueue eq, s32 id, void* user_data); s32 sceKernelDeleteUserEvent(OrbisKernelEqueue eq, s32 id); s32 sceKernelAddTimerEvent(OrbisKernelEqueue eq, s32 id, u32 time, void* user_data); s32 sceKernelDeleteTimerEvent(OrbisKernelEqueue eq, s32 id); - +s32 sceKernelAddHRTimerEvent(OrbisKernelEqueue eq, s32 id, OrbisKernelTimespec* timeout, void* user_data); +s32 sceKernelDeleteHRTimerEvent(OrbisKernelEqueue eq, s32 id); s32 sceKernelAllocateMainDirectMemory(u64 size, u64 align, s32 mtype, s64* phys_out); s32 sceKernelMapDirectMemory(void** addr, u64 size, s32 prot, s32 flags, s64 offset, u64 align); s32 sceKernelReleaseDirectMemory(s64 phys_addr, u64 size); @@ -75,6 +75,12 @@ struct OrbisVideoOutBufferAttribute { u64 reserved1; }; +struct VideoOutEventData { + u64 time : 12; + u64 counter : 4; + u64 flip_arg : 48; +}; + s32 sceVideoOutOpen(s32 user_id, s32 bus_type, s32 index, const void* param); s32 sceVideoOutSetFlipRate(s32 handle, s32 flip_rate); s32 sceVideoOutAddFlipEvent(OrbisKernelEqueue eq, s32 handle, void* user_data); From c9d9b98b38caf17a5bebb52f6ce4acc7d7c8ba31 Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Fri, 20 Feb 2026 13:27:07 -0600 Subject: [PATCH 30/33] Test timer accuracy --- tests/code/event_test/code/test.cpp | 56 ++++++++++++++++++++++------- tests/code/event_test/code/test.h | 1 + 2 files changed, 45 insertions(+), 12 deletions(-) diff --git a/tests/code/event_test/code/test.cpp b/tests/code/event_test/code/test.cpp index 43d20c21..f3f8f08b 100644 --- a/tests/code/event_test/code/test.cpp +++ b/tests/code/event_test/code/test.cpp @@ -1176,6 +1176,29 @@ TEST(EventTest, TimerEventTest) { result = sceKernelDeleteTimerEvent(eq, 0x20); UNSIGNED_INT_EQUALS(0, result); + // For testing purposes, we'll run a loop to try and get some measure of accuracy. + u32 micros = 100000; + while (true) { + u64 total_test_time = 0; + for (s32 i = 0; i < 10; i++) { + u64 cur_time_micros = sceKernelGetProcessTime(); + sceKernelAddTimerEvent(eq, 0x10, micros, &data); + // For the sake of timing accuracy, don't check results. + // This may lead to undetected hangs in emulators with issues. + sceKernelWaitEqueue(eq, &ev, 1, &count, nullptr); + u64 end_time_micros = sceKernelGetProcessTime(); + total_test_time += end_time_micros - cur_time_micros; + sceKernelDeleteTimerEvent(eq, 0x10); + } + total_test_time /= 10; + printf("%u micro timer took around %li micros to complete on average\n", micros, total_test_time); + if (total_test_time > micros * 1.5) { + // Break after reaching a large enough level of inaccuracy. + break; + } + micros /= 2; + } + // Delete the equeue result = sceKernelDeleteEqueue(eq); UNSIGNED_INT_EQUALS(0, result); @@ -1275,17 +1298,26 @@ TEST(EventTest, HighResTimerEvent) { CHECK(ev.user_data != 0); CHECK_EQUAL(data2, *(s64*)ev.user_data); - // To properly test real-world behavior, fire these off and wait for events in a loop. - for (s32 i = 0; i < 100; i++) { - time.tv_nsec = 10000000; - result = sceKernelAddHRTimerEvent(eq, 0x10, &time, &data); - UNSIGNED_INT_EQUALS(0, result); - // Real hardware doesn't entirely manage proper accuracy at 10000 micros. - // 20000 micros is enough for this to consistently pass though. - micros = 20000; - result = sceKernelWaitEqueue(eq, &ev, 1, &count, µs); - UNSIGNED_INT_EQUALS(0, result); - result = sceKernelDeleteHRTimerEvent(eq, 0x10); - UNSIGNED_INT_EQUALS(ORBIS_KERNEL_ERROR_ENOENT, result); + // For testing purposes, we'll run a loop to try and get some measure of accuracy. + micros = 100000; + while (true) { + u64 total_test_time = 0; + for (s32 i = 0; i < 10; i++) { + OrbisKernelTimespec time {0, micros * 1000}; + u64 cur_time_micros = sceKernelGetProcessTime(); + sceKernelAddHRTimerEvent(eq, 0x10, &time, &data); + // For the sake of timing accuracy, don't check results. + // This may lead to undetected hangs in emulators with issues. + sceKernelWaitEqueue(eq, &ev, 1, &count, nullptr); + u64 end_time_micros = sceKernelGetProcessTime(); + total_test_time += end_time_micros - cur_time_micros; + } + total_test_time /= 10; + printf("%u micro HR timer took around %li micros to complete on average\n", micros, total_test_time); + if (total_test_time > micros * 1.5) { + // Break after reaching a large enough level of inaccuracy. + break; + } + micros /= 2; } } \ No newline at end of file diff --git a/tests/code/event_test/code/test.h b/tests/code/event_test/code/test.h index d88d7572..9327639c 100644 --- a/tests/code/event_test/code/test.h +++ b/tests/code/event_test/code/test.h @@ -35,6 +35,7 @@ constexpr s32 ORBIS_KERNEL_ERROR_ENOENT = 0x80020002; constexpr s32 ORBIS_KERNEL_ERROR_ETIMEDOUT = 0x8002003c; s32 sceKernelUsleep(u32); +u64 sceKernelGetProcessTime(); s32 sceKernelCreateEqueue(OrbisKernelEqueue* eq, const char* name); s32 sceKernelDeleteEqueue(OrbisKernelEqueue eq); s32 sceKernelWaitEqueue(OrbisKernelEqueue eq, OrbisKernelEvent* ev, s32 num, s32* out, u32* timeout); From 2f227f9c5a3757ca4445ec1030c02f1ebff4d383 Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Sat, 21 Feb 2026 16:39:13 -0600 Subject: [PATCH 31/33] Reorganize a few things Perform benchmarks before doing the major edge cases, and loosen up the accuracy requirements of the tests. --- tests/code/event_test/code/test.cpp | 116 +++++++++++++++------------- 1 file changed, 61 insertions(+), 55 deletions(-) diff --git a/tests/code/event_test/code/test.cpp b/tests/code/event_test/code/test.cpp index f3f8f08b..21d42103 100644 --- a/tests/code/event_test/code/test.cpp +++ b/tests/code/event_test/code/test.cpp @@ -921,7 +921,7 @@ TEST(EventTest, TimerEventTest) { // The event should've triggered, and should be returned by this wait count = 0; - timeout = 1000; + timeout = 10000; result = sceKernelWaitEqueue(eq, &ev, 1, &count, &timeout); UNSIGNED_INT_EQUALS(0, result); UNSIGNED_INT_EQUALS(1, count); @@ -951,7 +951,7 @@ TEST(EventTest, TimerEventTest) { // The event should've triggered, and should be returned by this wait count = 0; - timeout = 1000; + timeout = 10000; result = sceKernelWaitEqueue(eq, &ev, 1, &count, &timeout); UNSIGNED_INT_EQUALS(0, result); UNSIGNED_INT_EQUALS(1, count); @@ -967,13 +967,44 @@ TEST(EventTest, TimerEventTest) { CHECK(ev.user_data != 0); CHECK_EQUAL(data, *(s64*)ev.user_data); + // Remove timer + result = sceKernelDeleteTimerEvent(eq, 0x10); + UNSIGNED_INT_EQUALS(0, result); + + // Run a quick benchmark on timer accuracy before proceeding. + u32 micros = 100000; + while (true) { + u64 total_test_time = 0; + for (s32 i = 0; i < 10; i++) { + u64 cur_time_micros = sceKernelGetProcessTime(); + sceKernelAddTimerEvent(eq, 0x10, micros, &data); + // For the sake of timing accuracy, don't check results. + // This may lead to undetected hangs in emulators with issues. + sceKernelWaitEqueue(eq, &ev, 1, &count, nullptr); + u64 end_time_micros = sceKernelGetProcessTime(); + total_test_time += end_time_micros - cur_time_micros; + sceKernelDeleteTimerEvent(eq, 0x10); + } + total_test_time /= 10; + printf("%u micro timer took around %li micros to complete on average\n", micros, total_test_time); + if (total_test_time > micros * 1.5 || micros == 1) { + // Break after reaching a large enough level of inaccuracy. + break; + } + micros /= 2; + } + + // Benchmarks complete, test some edge cases. + result = sceKernelAddTimerEvent(eq, 0x10, 100000, &data); + UNSIGNED_INT_EQUALS(0, result); + // Wait longer this time - result = sceKernelUsleep(1000000); + result = sceKernelUsleep(1001000); UNSIGNED_INT_EQUALS(0, result); // The event should've triggered, and should be returned by this wait count = 0; - timeout = 1000; + timeout = 10000; result = sceKernelWaitEqueue(eq, &ev, 1, &count, &timeout); UNSIGNED_INT_EQUALS(0, result); UNSIGNED_INT_EQUALS(1, count); @@ -1000,7 +1031,7 @@ TEST(EventTest, TimerEventTest) { UNSIGNED_INT_EQUALS(0, result); count = 0; - timeout = 1000; + timeout = 10000; result = sceKernelWaitEqueue(eq, &ev, 1, &count, &timeout); UNSIGNED_INT_EQUALS(0, result); UNSIGNED_INT_EQUALS(1, count); @@ -1176,29 +1207,6 @@ TEST(EventTest, TimerEventTest) { result = sceKernelDeleteTimerEvent(eq, 0x20); UNSIGNED_INT_EQUALS(0, result); - // For testing purposes, we'll run a loop to try and get some measure of accuracy. - u32 micros = 100000; - while (true) { - u64 total_test_time = 0; - for (s32 i = 0; i < 10; i++) { - u64 cur_time_micros = sceKernelGetProcessTime(); - sceKernelAddTimerEvent(eq, 0x10, micros, &data); - // For the sake of timing accuracy, don't check results. - // This may lead to undetected hangs in emulators with issues. - sceKernelWaitEqueue(eq, &ev, 1, &count, nullptr); - u64 end_time_micros = sceKernelGetProcessTime(); - total_test_time += end_time_micros - cur_time_micros; - sceKernelDeleteTimerEvent(eq, 0x10); - } - total_test_time /= 10; - printf("%u micro timer took around %li micros to complete on average\n", micros, total_test_time); - if (total_test_time > micros * 1.5) { - // Break after reaching a large enough level of inaccuracy. - break; - } - micros /= 2; - } - // Delete the equeue result = sceKernelDeleteEqueue(eq); UNSIGNED_INT_EQUALS(0, result); @@ -1268,6 +1276,29 @@ TEST(EventTest, HighResTimerEvent) { CHECK(ev.user_data != 0); CHECK_EQUAL(data, *(s64*)ev.user_data); + // Benchmark performance of these timers before progressing further. + u32 micros = 100000; + while (true) { + u64 total_test_time = 0; + for (s32 i = 0; i < 10; i++) { + OrbisKernelTimespec time {0, micros * 1000}; + u64 cur_time_micros = sceKernelGetProcessTime(); + sceKernelAddHRTimerEvent(eq, 0x10, &time, &data); + // For the sake of timing accuracy, don't check results. + // This may lead to undetected hangs in emulators with issues. + sceKernelWaitEqueue(eq, &ev, 1, &count, nullptr); + u64 end_time_micros = sceKernelGetProcessTime(); + total_test_time += end_time_micros - cur_time_micros; + } + total_test_time /= 10; + printf("%u micro HR timer took around %li micros to complete on average\n", micros, total_test_time); + if (total_test_time > micros * 1.5 || micros == 1) { + // Break after reaching a large enough level of inaccuracy. + break; + } + micros /= 2; + } + // Check for potential oddities like what timer events have result = sceKernelAddHRTimerEvent(eq, 0x10, &time, &data); UNSIGNED_INT_EQUALS(0, result); @@ -1277,8 +1308,8 @@ TEST(EventTest, HighResTimerEvent) { UNSIGNED_INT_EQUALS(0, result); // If these succeed like this, then the high-res timers have the same edge case as normal timers. - u32 micros = 1000; - result = sceKernelWaitEqueue(eq, &ev, 1, &count, µs); + micros = 1000; + result = sceKernelWaitEqueue(eq, &ev, 1, &count, µs); UNSIGNED_INT_EQUALS(ORBIS_KERNEL_ERROR_ETIMEDOUT, result); micros = 20000; result = sceKernelWaitEqueue(eq, &ev, 1, &count, µs); @@ -1292,32 +1323,7 @@ TEST(EventTest, HighResTimerEvent) { CHECK_EQUAL(-15, ev.filter); CHECK_EQUAL(0x30, ev.flags); CHECK_EQUAL(0, ev.fflags); - // Event data is still one, - // Might indicate that the timer only triggers once? CHECK_EQUAL(1, ev.data); CHECK(ev.user_data != 0); CHECK_EQUAL(data2, *(s64*)ev.user_data); - - // For testing purposes, we'll run a loop to try and get some measure of accuracy. - micros = 100000; - while (true) { - u64 total_test_time = 0; - for (s32 i = 0; i < 10; i++) { - OrbisKernelTimespec time {0, micros * 1000}; - u64 cur_time_micros = sceKernelGetProcessTime(); - sceKernelAddHRTimerEvent(eq, 0x10, &time, &data); - // For the sake of timing accuracy, don't check results. - // This may lead to undetected hangs in emulators with issues. - sceKernelWaitEqueue(eq, &ev, 1, &count, nullptr); - u64 end_time_micros = sceKernelGetProcessTime(); - total_test_time += end_time_micros - cur_time_micros; - } - total_test_time /= 10; - printf("%u micro HR timer took around %li micros to complete on average\n", micros, total_test_time); - if (total_test_time > micros * 1.5) { - // Break after reaching a large enough level of inaccuracy. - break; - } - micros /= 2; - } } \ No newline at end of file From 1a72043f5fb4a0fdecde3a7ce0cc3e59e204f77b Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Sat, 21 Feb 2026 16:42:16 -0600 Subject: [PATCH 32/33] Even earlier HRTimer benchmark. --- tests/code/event_test/code/test.cpp | 46 ++++++++++++++--------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/tests/code/event_test/code/test.cpp b/tests/code/event_test/code/test.cpp index 21d42103..8298cd65 100644 --- a/tests/code/event_test/code/test.cpp +++ b/tests/code/event_test/code/test.cpp @@ -1250,6 +1250,29 @@ TEST(EventTest, HighResTimerEvent) { result = sceKernelDeleteHRTimerEvent(eq, 0x10); UNSIGNED_INT_EQUALS(ORBIS_KERNEL_ERROR_ENOENT, result); + // Benchmark performance of these timers before progressing further. + u32 micros = 100000; + while (true) { + u64 total_test_time = 0; + for (s32 i = 0; i < 10; i++) { + OrbisKernelTimespec time {0, micros * 1000}; + u64 cur_time_micros = sceKernelGetProcessTime(); + sceKernelAddHRTimerEvent(eq, 0x10, &time, &data); + // For the sake of timing accuracy, don't check results. + // This may lead to undetected hangs in emulators with issues. + sceKernelWaitEqueue(eq, &ev, 1, &count, nullptr); + u64 end_time_micros = sceKernelGetProcessTime(); + total_test_time += end_time_micros - cur_time_micros; + } + total_test_time /= 10; + printf("%u micro HR timer took around %li micros to complete on average\n", micros, total_test_time); + if (total_test_time > micros * 1.5 || micros == 1) { + // Break after reaching a large enough level of inaccuracy. + break; + } + micros /= 2; + } + // Add a new HRTimer event result = sceKernelAddHRTimerEvent(eq, 0x10, &time, &data); UNSIGNED_INT_EQUALS(0, result); @@ -1276,29 +1299,6 @@ TEST(EventTest, HighResTimerEvent) { CHECK(ev.user_data != 0); CHECK_EQUAL(data, *(s64*)ev.user_data); - // Benchmark performance of these timers before progressing further. - u32 micros = 100000; - while (true) { - u64 total_test_time = 0; - for (s32 i = 0; i < 10; i++) { - OrbisKernelTimespec time {0, micros * 1000}; - u64 cur_time_micros = sceKernelGetProcessTime(); - sceKernelAddHRTimerEvent(eq, 0x10, &time, &data); - // For the sake of timing accuracy, don't check results. - // This may lead to undetected hangs in emulators with issues. - sceKernelWaitEqueue(eq, &ev, 1, &count, nullptr); - u64 end_time_micros = sceKernelGetProcessTime(); - total_test_time += end_time_micros - cur_time_micros; - } - total_test_time /= 10; - printf("%u micro HR timer took around %li micros to complete on average\n", micros, total_test_time); - if (total_test_time > micros * 1.5 || micros == 1) { - // Break after reaching a large enough level of inaccuracy. - break; - } - micros /= 2; - } - // Check for potential oddities like what timer events have result = sceKernelAddHRTimerEvent(eq, 0x10, &time, &data); UNSIGNED_INT_EQUALS(0, result); From 5678b1ae12a1baf42b551b09f04c0fe461157b4f Mon Sep 17 00:00:00 2001 From: Stephen Miller <56742918+StevenMiller123@users.noreply.github.com> Date: Sun, 22 Feb 2026 23:12:21 -0600 Subject: [PATCH 33/33] Global video out handle So failing emulators don't crash from error behavior. --- tests/code/event_test/code/test.cpp | 26 ++++++++------------------ 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/tests/code/event_test/code/test.cpp b/tests/code/event_test/code/test.cpp index 8298cd65..3c78aa7b 100644 --- a/tests/code/event_test/code/test.cpp +++ b/tests/code/event_test/code/test.cpp @@ -5,9 +5,15 @@ #include +VideoOut* handle; + TEST_GROUP (EventTest) { - void setup() {} - void teardown() {} + void setup() { + handle = new VideoOut(1920, 1080); + } + void teardown() { + delete (handle); + } }; static void PrintEventData(OrbisKernelEvent* ev) { @@ -375,10 +381,6 @@ TEST(EventTest, UserEventTest) { } TEST(EventTest, FlipEventTest) { - // Test video out flip events. - // First we need to properly open libSceVideoOut - VideoOut* handle = new VideoOut(1920, 1080); - // Register buffers s32 result = handle->addBuffer(); UNSIGNED_INT_EQUALS(0, result); @@ -667,15 +669,10 @@ TEST(EventTest, FlipEventTest) { result = handle->waitFlipEvent(events, 1, &count, 10000); // Since there's no event left, nothing will be triggered when the flip occurs. UNSIGNED_INT_EQUALS(ORBIS_KERNEL_ERROR_ETIMEDOUT, result); - - // Clean up after test - delete (handle); } TEST(EventTest, VblankEventTest) { // Another type of video out event, these trigger automatically when the PS4 draws blank frames. - VideoOut* handle = new VideoOut(1920, 1080); - // Add vblank event s64 val = 1; s32 result = handle->addVblankEvent(&val); @@ -748,17 +745,12 @@ TEST(EventTest, VblankEventTest) { // Wait with short timeout, this will return after timeout with error timedout result = handle->waitVblankEvent(&ev, 1, &count, 1000); UNSIGNED_INT_EQUALS(ORBIS_KERNEL_ERROR_ETIMEDOUT, result); - - // Clean up after test - delete (handle); } TEST(EventTest, GraphicsEventTest) { // These events are triggered through GPU command processing. // Most commonly through prepare flip packets with interrupts, // or through EventWriteEop packets. - VideoOut* handle = new VideoOut(1920, 1080); - // Start by using the handle to create an EOP event s64 val = 1; s32 result = handle->addGraphicsEvent(&val); @@ -891,8 +883,6 @@ TEST(EventTest, GraphicsEventTest) { result = handle->deleteGraphicsEvent(); UNSIGNED_INT_EQUALS(0, result); - - delete (handle); } TEST(EventTest, TimerEventTest) {