From db091f5fbb1ec9279c7a6e14a01097194e9ae18a Mon Sep 17 00:00:00 2001 From: Karan Vora <52519664+karanvora2599@users.noreply.github.com> Date: Thu, 15 Oct 2026 13:57:09 -0400 Subject: [PATCH] perf(videoout): don't hold a flip thread's CPU flips behind queued work sceVideoOutSubmitFlip went through queue 0 as a one-packet command buffer, so every CPU flip waited until the queue worker had translated all command buffers submitted before it. The PS5 RADV WSI flips from its own thread once the frame's fence has signaled. vkQuake keeps two frames in flight, so by then the next frame's command buffer is already queued, and the flip waited about 10 ms for the worker to translate it. Presents came in pairs, about 4.7 ms and 29 ms apart. Meanwhile the title waited in acquire for the flip to be shown, leaving the worker idle for about 7.5 ms per pair. A CPU flip from a thread that has never submitted queue-0 work now goes to the presenter at once, unless a command-buffer or EOP flip ahead of it has not been reached yet. Such a thread can only know that its frame is done by waiting for it. A flip from a thread that submits its own frames still waits for the work submitted ahead of it, because a title that flips without waiting for its frame relies on that order. APS5_CPU_FLIP_IN_ORDER=1 orders every CPU flip. vkQuake (PPSA99010) on an RTX 4060: 60 -> 72-74 presents per second, present-interval standard deviation 12.2 -> 2.1 ms, and intervals over 25 ms 40% -> 1%. --- .../Execution/include/Driver/Driver.hpp | 5 +- .../Execution/include/VideoOutput.hpp | 1 + .../Execution/src/Driver/EntryPoints.cpp | 6 +++ .../src/Driver/Queues/Submission.cpp | 32 +++++++++++- core/libs/prx/libSceVideoOut/CMakeLists.txt | 8 ++- .../prx/libSceVideoOut/src/VideoOutDriver.cpp | 6 +-- core/libs/prx/libSceVideoOut/tests/Flip.cpp | 52 +++++++++++++++++++ docs/dev/debt/libSceVideoOut.md | 1 + 8 files changed, 101 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp index 84c2a17592..11f50a30af 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp @@ -52,7 +52,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); @@ -325,7 +326,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 e2aaff9966..009ee3d99d 100644 --- a/core/libs/prx/libSceVideoOut/CMakeLists.txt +++ b/core/libs/prx/libSceVideoOut/CMakeLists.txt @@ -96,7 +96,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) @@ -104,6 +104,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 0f991c0c3c..11a4d80dc5 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 8a5caa007a..d52c006845 100644 --- a/docs/dev/debt/libSceVideoOut.md +++ b/docs/dev/debt/libSceVideoOut.md @@ -20,6 +20,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