diff --git a/tests/code/event_test/CMakeLists.txt b/tests/code/event_test/CMakeLists.txt new file mode 100644 index 0000000..6c8710b --- /dev/null +++ b/tests/code/event_test/CMakeLists.txt @@ -0,0 +1,8 @@ +project(event_test VERSION 0.0.1) + +link_libraries(SceSystemService SceVideoOut SceGnmDriver) + +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/code/main.cpp b/tests/code/event_test/code/main.cpp new file mode 100644 index 0000000..87760c5 --- /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 0000000..3c78aa7 --- /dev/null +++ b/tests/code/event_test/code/test.cpp @@ -0,0 +1,1319 @@ +#include "test.h" + +#include "CppUTest/TestHarness.h" +#include "video.h" + +#include + +VideoOut* handle; + +TEST_GROUP (EventTest) { + void setup() { + handle = new VideoOut(1920, 1080); + } + void teardown() { + delete (handle); + } +}; + +static void PrintEventData(OrbisKernelEvent* ev) { + 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%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%lx\n", data.flip_arg); + } + } + printf("ev->user_data = 0x%lx\n", ev->user_data); + if (ev->user_data != 0) { + printf("*(ev->user_data) = 0x%lx\n", *(u64*)ev->user_data); + } + printf("\n"); +} + +static void PrintFlipStatus(OrbisVideoOutFlipStatus* status) { + 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%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); + printf("status->current_buffer = %d\n\n", status->current_buffer); +} + +TEST(EventTest, UserEventTest) { + // 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. + // 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); + + // 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); + + // 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(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(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); + 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); +} + +TEST(EventTest, FlipEventTest) { + // Register buffers + 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 {}; + result = handle->getStatus(&status); + UNSIGNED_INT_EQUALS(0, result); + + // Create a flip event + 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. + 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. + OrbisKernelEvent ev {}; + s32 count = 0; + result = handle->waitFlipEvent(&ev, 1, &count, -1); + 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(ev.user_data != 0); + CHECK_EQUAL(1, *(u64*)ev.user_data); + CHECK_EQUAL(1, ev_data.counter); + CHECK_EQUAL(0x100, ev_data.flip_arg); + + // Flip events only trigger once. + result = handle->waitFlipEvent(&ev, 1, &count, 1000); + UNSIGNED_INT_EQUALS(ORBIS_KERNEL_ERROR_ETIMEDOUT, result); + + // Check flip 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. + count = 0; + result = handle->waitFlipEvent(&ev, 1, &count, -1); + UNSIGNED_INT_EQUALS(0, result); + CHECK_EQUAL(1, count); + + // Check returned data + PrintEventData(&ev); + 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); + + // Flip events only trigger once. + result = handle->waitFlipEvent(&ev, 1, &count, 1000); + UNSIGNED_INT_EQUALS(ORBIS_KERNEL_ERROR_ETIMEDOUT, result); + + // Check flip 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. + result = handle->waitFlipEvent(&ev, 1, &count, -1); + UNSIGNED_INT_EQUALS(0, result); + CHECK_EQUAL(1, count); + + // Check returned data + PrintEventData(&ev); + 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 + result = handle->getStatus(&status); + UNSIGNED_INT_EQUALS(0, result); + + PrintFlipStatus(&status); + + // We did two submits, so the video out event should fire again. + result = handle->waitFlipEvent(&ev, 1, &count, -1); + UNSIGNED_INT_EQUALS(0, result); + CHECK_EQUAL(1, count); + + // Check returned data + PrintEventData(&ev); + 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 + 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); + + // Wait for all flips to occur + result = handle->waitFlip(); + 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 + result = handle->waitFlipEvent(&ev, 1, &count, -1); + UNSIGNED_INT_EQUALS(0, result); + CHECK_EQUAL(1, count); + + PrintEventData(&ev); + 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. + result = handle->waitFlipEvent(&ev, 1, &count, 1000); + UNSIGNED_INT_EQUALS(ORBIS_KERNEL_ERROR_ETIMEDOUT, result); + + // 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); + + // Wait for all flips to occur + result = handle->waitFlip(); + UNSIGNED_INT_EQUALS(0, result); + + PrintFlipStatus(&status); + + // Wait for EOP flip to occur. + result = handle->waitFlipEvent(&ev, 1, &count, -1); + 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); + 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); + + // Print status again. + 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. + result = handle->waitFlipEvent(&ev, 1, &count, -1); + UNSIGNED_INT_EQUALS(0, result); + CHECK_EQUAL(1, count); + + PrintEventData(&ev); + CHECK(ev.data != 0); + ev_data = *reinterpret_cast(&ev.data); + CHECK_EQUAL(1, ev_data.counter); + CHECK_EQUAL(0x30000000000, ev_data.flip_arg); + + // Print status again. + result = handle->getStatus(&status); + 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 all flips to occur + result = handle->waitFlip(); + UNSIGNED_INT_EQUALS(0, result); + + OrbisKernelEvent events[2]; + 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); + + // 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 all flips to occur + result = handle->waitFlip(); + UNSIGNED_INT_EQUALS(0, result); + + 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); + + // 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); + + // 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); +} + +TEST(EventTest, VblankEventTest) { + // Another type of video out event, these trigger automatically when the PS4 draws blank frames. + // 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.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); + // 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); + 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); +} + +TEST(EventTest, GraphicsEventTest) { + // These events are triggered through GPU command processing. + // Most commonly through prepare flip packets with interrupts, + // or through EventWriteEop packets. + // 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); + + result = handle->deleteGraphicsEvent(); + UNSIGNED_INT_EQUALS(0, result); +} + +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 = 10000; + 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); + // 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); + + // 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 = 10000; + 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); + + // 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(1001000); + UNSIGNED_INT_EQUALS(0, result); + + // The event should've triggered, and should be returned by this wait + count = 0; + timeout = 10000; + 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); + + // 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 = 10000; + 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); + + // 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); + + 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); + + // 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); + + // 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); +} + +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); + + // 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); + + // 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. + 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); + CHECK_EQUAL(1, ev.data); + CHECK(ev.user_data != 0); + CHECK_EQUAL(data2, *(s64*)ev.user_data); +} \ No newline at end of file diff --git a/tests/code/event_test/code/test.h b/tests/code/event_test/code/test.h new file mode 100644 index 0000000..9327639 --- /dev/null +++ b/tests/code/event_test/code/test.h @@ -0,0 +1,106 @@ +#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 OrbisKernelEvent { + u64 ident; + s16 filter; + u16 flags; + u32 fflags; + u64 data; + 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; + +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); +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 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); + +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; +}; + +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); +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 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 new file mode 100644 index 0000000..b79dabd --- /dev/null +++ b/tests/code/event_test/code/video.h @@ -0,0 +1,346 @@ +#pragma once + +#include "test.h" + +#include + +class VideoOut { + private: + // Video out data + s32 handle = 0; + s32 width = 0; + s32 height = 0; + s32 current_buf = 0; + + // Buffer allocations + void* buf_addr = nullptr; + s64 buf_paddr = 0; + u64 buf_size = 0; + u64 buf_count = 0; + + // Equeue + OrbisKernelEqueue flip_queue = nullptr; + OrbisKernelEqueue vblank_queue = nullptr; + OrbisKernelEqueue gc_queue = nullptr; + + // Command buffer for EOP tests + void* cmd_buf = nullptr; + s64 phys_cmd_buf = 0; + u64 cmd_buf_size = 0x4000; + u32 stream_size = 0; + u32 cmd_start_offset = 0; + bool gpu_init = false; + + 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); + + // Create vblank equeue + 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); + + 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. + ~VideoOut() { + // Delete flip event queue + 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 + if (handle > 0) { + s32 result = sceVideoOutClose(handle); + UNSIGNED_INT_EQUALS(0, result); + handle = 0; + } + + // Free command buffer allocation + if (cmd_buf != nullptr) { + s32 result = sceKernelReleaseDirectMemory(phys_cmd_buf, cmd_buf_size); + UNSIGNED_INT_EQUALS(0, result); + } + + // Free video out buffer allocation + if (buf_addr != nullptr) { + s32 result = sceKernelReleaseDirectMemory(buf_paddr, buf_size); + UNSIGNED_INT_EQUALS(0, result); + } + }; + + s32 getStatus(OrbisVideoOutFlipStatus* status) { + memset(status, 0, sizeof(OrbisVideoOutFlipStatus)); + return sceVideoOutGetFlipStatus(handle, status); + }; + + s32 flipFrame(s64 flip_arg) { + 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 == buf_count) { + current_buf = 0; + } + 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++; + } + + if (current_buf == -1) { + // Now that we have buffers, avoid submitting to the empty buffer. + current_buf = 0; + } + + 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(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; + cmds += ensureGpuInit(); + + // Write EventWriteEop packet to fire interrupt + cmds[0] = 0xc0044700; + u64 mask = 0xfffffffffc; + if (src_sel != 1) { + mask = 0xfffffffff8; + }; + + 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); + *(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; + } + + // 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 a flip packet to the pointer. + cmds[0] = 0xc03e1000; + cmds[1] = 0x68750777; + 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; + }; + + s32 submitAndFlipWithEopInterrupt(s64 flip_arg) { + if (buf_count == 0) { + // SubmitAndFlip fails on buffer -1, save time by failing early. + return -1; + } + + // 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 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; + }; + + s32 removeBuffers() { + current_buf = -1; + return sceVideoOutUnregisterBuffers(handle, 0); + } + + s32 addFlipEvent(void* user_data) { return sceVideoOutAddFlipEvent(flip_queue, handle, user_data); }; + + s32 deleteFlipEvent() { return sceVideoOutDeleteFlipEvent(flip_queue, handle); }; + + 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 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 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(gc_queue, ev, num, out, nullptr); + } else { + return sceKernelWaitEqueue(gc_queue, ev, num, out, &timeout); + } + }; + + s32 waitFlip() { + // Wait for flip + s32 result = sceVideoOutIsFlipPending(handle); + while (result > 0) { + sceKernelUsleep(10000); + result = sceVideoOutIsFlipPending(handle); + }; + return result; + }; + + s32 waitVblank() { return sceVideoOutWaitVblank(handle); }; +}; \ No newline at end of file