diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp index 7b638f4e4c..e09e99c489 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp @@ -53,7 +53,8 @@ public: std::uint64_t DispatchesAdoptedAhead() const; void CheckFailure(); void ReportFailure(std::exception_ptr error); - void Submit(const Packet* packet, std::uint32_t queue); + void Submit(const Packet* packet, std::uint32_t queue, bool cpuFlip = false); + void SubmitFlip(const FlipInfo& info); void SuspendPoint(); std::uint32_t ReserveEopFlip(const FlipInfo& info); void RegisterVideoOutput(std::uint32_t handle, const std::shared_ptr& output); @@ -330,7 +331,7 @@ private: std::condition_variable changed; std::map workers; - std::uint64_t frameSerial = 0; + std::atomic frameSerial{0}; std::shared_ptr pendingFrameTiming; std::atomic flipsCounted{0}; diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/VideoOutput.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/VideoOutput.hpp index a628e872fb..2c7443e9a5 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/VideoOutput.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/VideoOutput.hpp @@ -54,6 +54,7 @@ public: } extern "C" std::uint32_t AgcDriverReserveEopFlip_nid_postfix(const AgcDriver::FlipInfo& info); +extern "C" void AgcDriverSubmitFlip_nid_postfix(const AgcDriver::FlipInfo& info); extern "C" void AgcDriverRegisterVideoOutput_nid_postfix(std::uint32_t handle, const std::shared_ptr& output); extern "C" void AgcDriverUnregisterVideoOutput_nid_postfix(std::uint32_t handle, const std::shared_ptr& output); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/EntryPoints.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/EntryPoints.cpp index 01693be19c..bf6d16754a 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/EntryPoints.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/EntryPoints.cpp @@ -99,6 +99,12 @@ extern "C" std::uint32_t AgcDriverReserveEopFlip_nid_postfix(const AgcDriver::Fl return AgcDriver::DriverDetail::Driver::Get().ReserveEopFlip(info); } +extern "C" void AgcDriverSubmitFlip_nid_postfix(const AgcDriver::FlipInfo& info) try { + AgcDriver::DriverDetail::Driver::Get().SubmitFlip(info); +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); +} + extern "C" std::uint64_t AgcDriverDispatchesAdoptedAhead_nid_postfix() { return AgcDriver::DriverDetail::Driver::Get().DispatchesAdoptedAhead(); } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp index b1572bfa26..13d45c0f36 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp @@ -8,6 +8,7 @@ #include "prx/libSceAgcDriver/Execution/include/Pm4.hpp" #include "prx/libSceAgcDriver/Execution/include/SubmissionCapture.hpp" #include "prx/libSceAgcDriver/Execution/include/ThreadPriority.hpp" +#include "prx/libSceAgcDriver/Eq/include/Event.hpp" #include "ThreadOwned.hpp" #include "prx/libc/include/HostThreadLocal.hpp" #include @@ -37,6 +38,11 @@ bool EopRelease(std::span words) { return words.size() >= 8 && (words[0] >> 30u) == 3u && Pm4::PacketWords(words[0]) == 8u && ((words[0] >> 8u) & 0xffu) == 0x49u && ((words[2] >> 24u) & 7u) == 2u; } +bool& submitsQueue0() { + struct SubmitsQueue0Storage {}; + return HostThreadLocal(); +} + } std::deque& Driver::releasedTails() { @@ -316,12 +322,22 @@ void Driver::executeRewindTail(const Submission& stalled) { execute(tail); } -void Driver::Submit(const Packet* packet, std::uint32_t queue) { +void Driver::SubmitFlip(const FlipInfo& info) { + const auto argument = static_cast(info.argument); + std::array words{FlipPacketHeader, info.handle, static_cast(info.index), info.mode, static_cast(argument), static_cast(argument >> 32u)}; + const Packet packet{words.data(), static_cast(words.size()), 0, {}}; + Submit(&packet, 0, true); +} + +void Driver::Submit(const Packet* packet, std::uint32_t queue, bool cpuFlip) { const auto receivedAt = APS5_ENABLE_TIMING_LOG ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; CheckFailure(); require(queue == 0 || (queue >= 0x20 && queue < SubmissionQueueEnd), "unsupported compute queue"); static const bool ring = ReadSubmissionRingSettings(); if (ring) PrepareSubmissionRing(queue); + static const bool flipsInOrder = std::getenv("APS5_CPU_FLIP_IN_ORDER") != nullptr || std::getenv("APS5_SYNC_FLIP") != nullptr; + const bool unorderedFlip = cpuFlip && !flipsInOrder && !APS5_ENABLE_TIMING_LOG && !submitsQueue0(); + if (queue == 0 && !cpuFlip) submitsQueue0() = true; GuestMemory::CheckRange(packet, sizeof(Packet), alignof(Packet)); const auto descriptor = *packet; require(descriptor.flags == 0, "nonzero submission flags are not implemented"); @@ -351,11 +367,22 @@ void Driver::Submit(const Packet* packet, std::uint32_t queue) { auto* const capture = SubmissionCapture::Active(); auto captured = capture != nullptr ? SubmissionCapture::Prepare(queue, SubmissionKind::Submit, submission.commands, submission.conditionalEnds) : CapturedSubmission{}; { - std::lock_guard lock(mutex); + std::unique_lock lock(mutex); rethrowFailure(); checkStopping(); require(accepted != std::numeric_limits::max(), "submission serial overflow"); reserveOutputs(submission); + if (unorderedFlip && capture == nullptr && accepted != 0 && flipsAhead.load(std::memory_order_acquire) == 0) { + const auto now = FrameTiming::Clock::now(); + auto frame = std::make_shared(++frameSerial); + frame->IncludeSubmission(accepted, now, now, now, true); + frame->SetFlip(accepted, 0, now, now); + ++flipsCounted; + submission.flips.begin()->second->GpuReady(frame); + lock.unlock(); + AgcDriverDeliverEopInterrupt(0); + return; + } submission.shaders = shaders; submission.serial = accepted + 1; @@ -384,6 +411,7 @@ void Driver::Submit(const Packet* packet, std::uint32_t queue) { void Driver::SuspendPoint() { const auto receivedAt = APS5_ENABLE_TIMING_LOG ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; require(!onWorkerThread(), "worker cannot suspend itself"); + submitsQueue0() = true; auto* const capture = SubmissionCapture::Active(); auto captured = capture != nullptr ? SubmissionCapture::Prepare(0, SubmissionKind::Suspend, {}, {}) : CapturedSubmission{}; std::unique_lock lock(mutex); diff --git a/core/libs/prx/libSceVideoOut/CMakeLists.txt b/core/libs/prx/libSceVideoOut/CMakeLists.txt index b8e4ba2743..e630686244 100644 --- a/core/libs/prx/libSceVideoOut/CMakeLists.txt +++ b/core/libs/prx/libSceVideoOut/CMakeLists.txt @@ -106,7 +106,7 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endforeach() - foreach(flipMode IN ITEMS eop eop_main eop_aftergpu) + foreach(flipMode IN ITEMS eop eop_main eop_aftergpu cpuflip) add_test(NAME video_out_flip_${flipMode} COMMAND video_out_flip_tests ${flipMode}) set_tests_properties(video_out_flip_${flipMode} PROPERTIES TIMEOUT 30 SKIP_RETURN_CODE 77 RESOURCE_LOCK videoout_app0 ENVIRONMENT "SDL_VIDEODRIVER=${ANYPS5_VIDEO_OUT_TEST_DRIVER}") if(WIN32) @@ -114,6 +114,12 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endforeach() + add_test(NAME video_out_flip_cpuflip_in_order COMMAND video_out_flip_tests cpuflip) + set_tests_properties(video_out_flip_cpuflip_in_order PROPERTIES TIMEOUT 30 SKIP_RETURN_CODE 77 RESOURCE_LOCK videoout_app0 ENVIRONMENT "SDL_VIDEODRIVER=${ANYPS5_VIDEO_OUT_TEST_DRIVER};APS5_CPU_FLIP_IN_ORDER=1") + if(WIN32) + set_tests_properties(video_out_flip_cpuflip_in_order PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() if(BUILD_TESTING AND NOT WIN32) diff --git a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp index 96b972a435..88c960113f 100644 --- a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp +++ b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp @@ -28,7 +28,6 @@ #include "prx/libSceAgcDriver/Execution/include/Presentation.hpp" #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" -#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" #include "prx/libc/include/PreciseWait.hpp" #include "prx/libc/include/Shutdown.hpp" @@ -440,10 +439,7 @@ int VideoOutDriver::SubmitFlip(int handle, int index, int flipMode, int64_t flip // A title that does not pace on flipPendingNum can run ahead of the presenter now that the queue // worker no longer waits per flip; a full queue is the documented error, not a failure. if (flipQueue->reservations.load() >= VIDEO_OUT_FLIP_QUEUE_CAPACITY) return VIDEO_OUT_ERROR_FLIP_QUEUE_FULL; - std::array words{AgcDriver::FlipPacketHeader, static_cast(handle), static_cast(index), static_cast(flipMode), static_cast(static_cast(flipArg)), static_cast(static_cast(flipArg) >> 32u)}; - Packet packet{words.data(), static_cast(words.size()), 0, {}}; - const auto result = sceAgcDriverSubmitDcb(&packet); - require(result == 0, "driver rejected flip submission"); + AgcDriverSubmitFlip_nid_postfix(AgcDriver::FlipInfo{static_cast(handle), index, static_cast(flipMode), flipArg}); return 0; } diff --git a/core/libs/prx/libSceVideoOut/tests/Flip.cpp b/core/libs/prx/libSceVideoOut/tests/Flip.cpp index a7072e5e51..8f3e865c3b 100644 --- a/core/libs/prx/libSceVideoOut/tests/Flip.cpp +++ b/core/libs/prx/libSceVideoOut/tests/Flip.cpp @@ -225,6 +225,57 @@ void testDeferredBuffer() { LibcRunShutdown_nid_postfix(); } +void testCpuFlipOrder() { + const bool inOrder = std::getenv("APS5_CPU_FLIP_IN_ORDER") != nullptr; + const int handle = sceVideoOutOpen(255, 0, 0, nullptr); + auto cfg = VideoOutDriver::Get().GetConfig(handle); + std::vector allocation(65536 + 65535); + const auto storage = alignedBuffer(allocation); + VideoOutBuffers buffer{storage.data(), nullptr, {nullptr, nullptr}}; + VideoOutBufferAttribute2 attribute{}; + sceVideoOutSetBufferAttribute2(&attribute, 0x8000000000000000ull, 0, 64, 64, 0, 0, 0); + check(sceVideoOutRegisterBuffers2(handle, 0, 0, &buffer, 1, &attribute, 0, nullptr) == 0, "buffer registration failed"); + const auto flipsWithin = [&](int count, std::chrono::milliseconds limit) { + std::unique_lock lock(cfg->mutex); + const bool done = cfg->vblankCond.wait_for(lock, limit, [&] { return cfg->failure || cfg->flipStatus.count == count; }); + if (cfg->failure) std::rethrow_exception(cfg->failure); + return done; + }; + const auto shownArgument = [&] { + std::lock_guard lock(cfg->mutex); + return cfg->flipStatus.flipArg; + }; + auto first = std::make_shared(); + AgcDriverRegisterVideoOutput_nid_postfix(7, first); + std::array firstHeld{0xc004105c, 7, 0xfffffffeu, 1, 0, 0}; + Packet firstPacket{firstHeld.data(), 6, 0, {}}; + check(sceAgcDriverSubmitDcb(&firstPacket) == 0, "the first gated submission was not accepted"); + std::thread flipThread([&] { check(sceVideoOutSubmitFlip(handle, 0, 1, 5) == 0, "the flip thread's flip was not accepted"); }); + flipThread.join(); + const bool ahead = flipsWithin(1, std::chrono::milliseconds(inOrder ? 1000 : 8000)); + if (inOrder) check(!ahead, "with APS5_CPU_FLIP_IN_ORDER a flip completed while a queue-0 submission ahead of it was held"); + else check(ahead, "a flip from a thread that never submitted queue-0 work waited for a held queue-0 submission"); + first->Release(); + check(flipsWithin(1, std::chrono::seconds(30)), "the flip thread's flip did not complete"); + check(shownArgument() == 5, "the flip thread's flip presented the wrong request"); + auto second = std::make_shared(); + AgcDriverRegisterVideoOutput_nid_postfix(8, second); + std::array secondHeld{0xc004105c, 8, 0xfffffffeu, 1, 0, 0}; + Packet secondPacket{secondHeld.data(), 6, 0, {}}; + check(sceAgcDriverSubmitDcb(&secondPacket) == 0, "the second gated submission was not accepted"); + check(sceVideoOutSubmitFlip(handle, 0, 1, 6) == 0, "the submitting thread's flip was not accepted"); + check(!flipsWithin(2, std::chrono::milliseconds(1000)), "a flip from the thread that submitted queue-0 work completed ahead of that work"); + second->Release(); + check(flipsWithin(2, std::chrono::seconds(30)), "the submitting thread's flip did not complete"); + check(shownArgument() == 6, "the submitting thread's flip presented the wrong request"); + AgcDriverWaitIdle_nid_postfix(); + sceVideoOutUnregisterBuffers(handle, 0); + sceVideoOutClose(handle); + AgcDriverUnregisterVideoOutput_nid_postfix(7, first); + AgcDriverUnregisterVideoOutput_nid_postfix(8, second); + LibcRunShutdown_nid_postfix(); +} + void testUnregisteredBuffer() { const int handle = sceVideoOutOpen(255, 0, 0, nullptr); auto cfg = VideoOutDriver::Get().GetConfig(handle); @@ -975,6 +1026,7 @@ int main(int argc, char** argv) { else if (argc == 2 && std::string(argv[1]) == "deferred") testDeferredBuffer(); else if (argc == 2 && std::string(argv[1]) == "unregistered") testUnregisteredBuffer(); else if (argc == 2 && std::string(argv[1]) == "ps4") testPs4Buffers(); + else if (argc == 2 && std::string(argv[1]) == "cpuflip") testCpuFlipOrder(); else testLifetime(argc == 2 && std::string(argv[1]) == "reopen"); std::puts("VideoOut flip tests passed"); return 0; diff --git a/docs/dev/debt/libSceVideoOut.md b/docs/dev/debt/libSceVideoOut.md index ce95581f16..ae73a5efc5 100644 --- a/docs/dev/debt/libSceVideoOut.md +++ b/docs/dev/debt/libSceVideoOut.md @@ -21,6 +21,7 @@ - [sceVideoOutOpen](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - the open param's first word is unknown (PPSA21564 passes 16; it is not the byte size, since the affinity mask is at offset 16); only 16 is accepted. PPSA26344's only call at `0xCCF5EA` passes `(0xFF, 0, 0, nullptr)`. Whether a non-null param continues past offset 24 is also unknown - [sceVideoOutGetBufferLabelAddress](../../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) exposes a port-owned contiguous array of 16 aligned 64-bit labels and returns its count. Astro Bot PPSA21564's bundled AGC driver passes a pointer output and addresses each label at `base + index * 8`. The count, zero initialization on registration, and release of the previous displayed buffer when its replacement completes follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/23c41333822fe8eca4c609b43119180af63ae4ac/src/core/libraries/videoout/video_out.cpp) and its driver; PS5 console equivalence has not been measured. Labels live until the port closes, and reopening creates a new array. Error precedence follows the reference for null output and the existing open-port policy for handles. Exact scanout release timing, repeated same-buffer flips and concurrent guest label stores remain unverified. EOP-flip registration and interrupt tokens remain unimplemented; exposing these labels does not implement that separate API. - [GPU flips](../../../core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp) (libSceVideoOut) - a command-buffer flip of a buffer index with no registered buffer when the GPU reaches it presents black and completes like any flip (flip event, count, `flipPendingNum`). The console's behaviour is unknown; PPSA05684 requires that it completes: its renderer submits a flip of buffer 0 while another thread waits for `sceVideoOutIsFlipPending` to reach 0 before registering the buffers, and `flipPendingNum` counts that flip from its submission as in shadPS4 +- [sceVideoOutSubmitFlip](../../../core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp) (libSceVideoOut, libSceAgcDriver) - in the [AGC driver](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp), a CPU flip waits for the graphics-queue submissions ahead of it when the calling thread has submitted graphics-queue work itself: a title that submits a frame and flips it without waiting relies on that order, since emulated GPU work finishes later than the console's. A flip from a thread that has never submitted graphics-queue work is presented without waiting for queued submissions, unless a command-buffer or EOP flip ahead of it has not been reached. Such a thread can only know its frame is finished by waiting for it, as the PS5 RADV WSI's flip thread waits on the frame's fence. Whether the console ever orders a CPU flip behind the graphics queue is not measured; `APS5_CPU_FLIP_IN_ORDER=1` orders every CPU flip - [sceVideoOutAddPreVblankStartEvent](../../../core/libs/prx/libSceVideoOut/src/Event.cpp) (libSceVideoOut) - how long before the vblank the console signals pre-vblank start is unknown. The event is triggered on each emulated vblank tick immediately before the vblank event, with its own count as the event data - [sceVideoOutRegisterBuffers, sceVideoOutGetFlipStatus, sceVideoOutGetVblankStatus](../../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) (libSceVideoOut) - PS4 callers. A handle is treated as PS4 once buffers are registered through the PS4 `sceVideoOutRegisterBuffers`; from then on its flip and vblank status are written in the PS4 64- and 32-byte layouts, with `tsc` and `submitTsc` 0. A PS4 title that reads a status before it registers buffers gets the larger PS5 layout. The PS4 function accepts only linear A8R8G8B8_SRGB (0x80000000) and A8B8G8R8_SRGB (0x80002200) buffers without options; tiled PS4 buffers throw, as their layout differs from the PS5 tiling. Linear PS4 buffers may be 4 KiB aligned: the OpenOrbis graphics sample registers its second 1920x1080 buffer 8,294,400 bytes after the first. The PS4 alignment rule is not known