Merge pull request #6507 from karanvora2599/perf/videoout-unordered-cpu-flips

perf(videoout): don't hold a flip thread's CPU flips behind queued work
This commit is contained in:
Adrià authored and GitHub committed 2026-10-19 04:49:51 +02:00
commit c8e3fcb703
8 files changed
+101 -10

No files matched your search

@@ -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<IVideoOutput>& output);
@@ -330,7 +331,7 @@ private:
std::condition_variable changed;
std::map<std::uint32_t, QueueWorker> workers;
std::uint64_t frameSerial = 0;
std::atomic<std::uint64_t> frameSerial{0};
std::shared_ptr<FrameTiming> pendingFrameTiming;
std::atomic<std::uint64_t> flipsCounted{0};
@@ -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<AgcDriver::IVideoOutput>& output);
extern "C" void AgcDriverUnregisterVideoOutput_nid_postfix(std::uint32_t handle, const std::shared_ptr<AgcDriver::IVideoOutput>& output);
@@ -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();
}
@@ -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 <bit>
@@ -37,6 +38,11 @@ bool EopRelease(std::span<const std::uint32_t> 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<bool, SubmitsQueue0Storage>();
}
}
std::deque<const std::uint32_t*>& 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<std::uint64_t>(info.argument);
std::array<std::uint32_t, FlipPacketWords> words{FlipPacketHeader, info.handle, static_cast<std::uint32_t>(info.index), info.mode, static_cast<std::uint32_t>(argument), static_cast<std::uint32_t>(argument >> 32u)};
const Packet packet{words.data(), static_cast<std::uint32_t>(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<std::uint64_t>::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<FrameTiming>(++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);
+7 -1
View File
@@ -106,7 +106,7 @@ if(BUILD_TESTING)
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
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:$<TARGET_FILE_DIR:libc>")
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:$<TARGET_FILE_DIR:libc>")
endif()
endif()
if(BUILD_TESTING AND NOT WIN32)
@@ -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<uint32_t, AgcDriver::FlipPacketWords> words{AgcDriver::FlipPacketHeader, static_cast<uint32_t>(handle), static_cast<uint32_t>(index), static_cast<uint32_t>(flipMode), static_cast<uint32_t>(static_cast<uint64_t>(flipArg)), static_cast<uint32_t>(static_cast<uint64_t>(flipArg) >> 32u)};
Packet packet{words.data(), static_cast<uint32_t>(words.size()), 0, {}};
const auto result = sceAgcDriverSubmitDcb(&packet);
require(result == 0, "driver rejected flip submission");
AgcDriverSubmitFlip_nid_postfix(AgcDriver::FlipInfo{static_cast<std::uint32_t>(handle), index, static_cast<std::uint32_t>(flipMode), flipArg});
return 0;
}
@@ -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<std::byte> 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<Gate>();
AgcDriverRegisterVideoOutput_nid_postfix(7, first);
std::array<uint32_t, 6> 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<Gate>();
AgcDriverRegisterVideoOutput_nid_postfix(8, second);
std::array<uint32_t, 6> 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;
+1
View File
@@ -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