From 8c0c449173a7e30b406c4245c9eb5ba55a27cd92 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Wed, 14 Oct 2026 14:22:36 +0200 Subject: [PATCH] feat(videoout): present reserved flips from tagged GPU completion Rebased onto main as one commit (conflicts in debt notes, Presentation.cpp includes and Submission.cpp helpers). --- .../Execution/include/Driver/Driver.hpp | 7 ++ .../Execution/include/VideoOutput.hpp | 6 + .../Execution/src/Driver/EntryPoints.cpp | 4 + .../Execution/src/Driver/Lifecycle.cpp | 1 + .../src/Driver/Packets/PacketExecution.cpp | 10 +- .../Execution/src/Driver/Presentation.cpp | 17 +++ .../src/Driver/Queues/Submission.cpp | 28 ++++- .../prx/libSceAgcDriver/Execution/src/Pm4.cpp | 11 ++ core/libs/prx/libSceAgcDriver/tests/Pm4.cpp | 16 +++ core/libs/prx/libSceVideoOut/CMakeLists.txt | 8 ++ .../prx/libSceVideoOut/include/Buffer.hpp | 1 + .../libSceVideoOut/include/VideoOutDriver.hpp | 1 + core/libs/prx/libSceVideoOut/src/Buffer.cpp | 7 ++ .../prx/libSceVideoOut/src/VideoOutDriver.cpp | 30 ++++- core/libs/prx/libSceVideoOut/tests/Flip.cpp | 107 +++++++++++++++++- docs/dev/debt/libSceAgcDriver.md | 1 + docs/dev/debt/libSceVideoOut.md | 1 + 17 files changed, 249 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp index b3aa361927f..9a522af0ead 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp @@ -52,6 +52,7 @@ class Driver { void ReportFailure(std::exception_ptr error); void Submit(const Packet* packet, std::uint32_t queue); void SuspendPoint(); + std::uint32_t ReserveEopFlip(const FlipInfo& info); void RegisterVideoOutput(std::uint32_t handle, const std::shared_ptr& output); void UnregisterVideoOutput(std::uint32_t handle, const std::shared_ptr& output); void AttachWindow(const PresentationWindow& window); @@ -324,6 +325,12 @@ class Driver { std::map queues; std::map> outputs; + struct EopFlip { + std::uint32_t handle; + std::shared_ptr request; + }; + std::map eopFlips; + std::uint32_t nextEopToken = EopFlipTokenBase; DevicePointer device; std::mutex labelStoresMutex; diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/VideoOutput.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/VideoOutput.hpp index e26b9c65700..a628e872fb3 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/VideoOutput.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/VideoOutput.hpp @@ -10,6 +10,9 @@ namespace AgcDriver { class FrameTiming; +inline constexpr std::uint32_t EopFlipTokenBase = 0x04000000u; +inline constexpr std::uint32_t EopFlipTokenLimit = 0x07ffffffu; + inline constexpr std::uint32_t FlipPacketHeader = 0xc004105cu; inline constexpr std::uint32_t FlipPacketWords = 6; // AnyPS5's internal encoding for the driver-generated rendering wait. @@ -40,6 +43,7 @@ class IVideoOutput { public: virtual ~IVideoOutput() = default; virtual std::shared_ptr Reserve(const FlipInfo& info) = 0; + virtual std::shared_ptr TryReserve(const FlipInfo& info) { return Reserve(info); } virtual void WaitForFlipRoom() {} virtual std::shared_ptr CaptureRenderingWait(std::uint32_t index) { throw std::runtime_error("VideoOut: rendering waits are unsupported by this output"); @@ -49,6 +53,8 @@ class IVideoOutput { } +extern "C" std::uint32_t AgcDriverReserveEopFlip_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 54f524103e5..5853975c5f7 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/EntryPoints.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/EntryPoints.cpp @@ -95,6 +95,10 @@ extern "C" void AgcDriverSuspendPoint_nid_postfix() try { LibcAwaitExit_nid_postfix(); } +extern "C" std::uint32_t AgcDriverReserveEopFlip_nid_postfix(const AgcDriver::FlipInfo& info) { + return AgcDriver::DriverDetail::Driver::Get().ReserveEopFlip(info); +} + extern "C" void AgcDriverRegisterVideoOutput_nid_postfix(std::uint32_t handle, const std::shared_ptr& output) { AgcDriver::RegisterVideoOutput(handle, output); } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp index 79282068232..751da78ef47 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp @@ -89,6 +89,7 @@ void Driver::stop() { { std::lock_guard lock(mutex); stopping = true; + eopFlips.clear(); for (auto& [queue, worker] : workers) { worker.pending.clear(); worker.unfinishedWrites.clear(); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp index ce330fcaa4f..492a2e4095e 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp @@ -202,7 +202,15 @@ void Driver::execute(const Submission& submission) { recent.Record(cursor); if (header == RenderingWaitPacketHeader) { timed(&WorkerProfile::waitMs, [&] { submission.renderingWaits.at(cursor)->Wait(); }); - } else if (header == FlipPacketHeader) { + } else if (submission.flips.contains(cursor)) { + if (opcode == 0x49) { + if (!wroteOnGpu) { + Pm4::Execute(packet, queue); + if (const auto label = Pm4::DecodeLabelWrite(packet)) noteLabelStore(label->address, label->Bytes(), ++eventSerial, submission.queue); + } + if (endOfPipeInterrupt && !interruptDeferred) AgcDriverDeliverEopInterrupt(submission.queue); + } + if (deviceUse.owns_lock()) deviceUse.unlock(); CheckFailure(); std::uint64_t batchesAtFlip = 0, unsignaledAtFlip = 0; bool flipInterruptDeferred = false; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Presentation.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Presentation.cpp index 55701d83cb9..f5e4b73e9d0 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Presentation.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Presentation.cpp @@ -4,11 +4,27 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Queues/WorkerAffinity.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" +#include #include "prx/libSceAgcDriver/Execution/include/TracyZone.hpp" #include namespace AgcDriver::DriverDetail { +std::uint32_t Driver::ReserveEopFlip(const FlipInfo& info) { + std::lock_guard lock(mutex); + rethrowFailure(); + checkStopping(); + require(nextEopToken <= EopFlipTokenLimit, "EOP flip token space exhausted"); + const auto output = outputs.find(info.handle); + require(output != outputs.end(), "EOP flip references an unregistered video output"); + auto request = output->second->TryReserve(info); + if (!request) return 0; + const auto token = nextEopToken; + eopFlips.emplace(token, EopFlip{info.handle, std::move(request)}); + ++nextEopToken; + return token; +} + void Driver::RegisterVideoOutput(std::uint32_t handle, const std::shared_ptr& output) { require(output != nullptr, "null video output"); std::lock_guard lock(mutex); @@ -21,6 +37,7 @@ void Driver::UnregisterVideoOutput(std::uint32_t handle, const std::shared_ptrsecond == output, "video output registration mismatch"); + std::erase_if(eopFlips, [handle](const auto& entry) { return entry.second.handle == handle; }); outputs.erase(it); } 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 b673d7eb11e..db3f64fa8d4 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp @@ -30,6 +30,10 @@ void writeCaptured(Driver& driver, SubmissionCapture& capture, const CapturedSub } } +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; +} + } void Driver::copyCommands(Submission& submission, const std::uint32_t* guest, std::size_t words) { @@ -166,10 +170,19 @@ void Driver::noteAwaitingTitle(std::uint32_t queue, std::uint64_t awaited) { void Driver::holdFlipBehindWorker(const Submission& submission) { if (submission.queue != 0 || onWorkerThread()) return; + bool candidate = false; + for (std::size_t cursor = 0; cursor < submission.commands.size() && !candidate; cursor += Pm4::PacketWords(submission.commands[cursor])) { + const auto words = std::span(submission.commands).subspan(cursor); + candidate = words[0] == FlipPacketHeader || EopRelease(words); + } + if (!candidate) return; + std::unique_lock lock(mutex); bool flips = false; - for (std::size_t cursor = 0; cursor < submission.commands.size() && !flips; cursor += Pm4::PacketWords(submission.commands[cursor])) flips = submission.commands[cursor] == FlipPacketHeader; + for (std::size_t cursor = 0; cursor < submission.commands.size() && !flips; cursor += Pm4::PacketWords(submission.commands[cursor])) { + const auto words = std::span(submission.commands).subspan(cursor); + flips = words[0] == FlipPacketHeader || (EopRelease(words) && eopFlips.contains(words[7])); + } if (!flips) return; - std::unique_lock lock(mutex); flipHolders.fetch_add(1, std::memory_order_acq_rel); changed.wait(lock, [&] { return failure != nullptr || stopping.load(std::memory_order_acquire) || shutdownToken.stop_requested() || flipsAhead.load(std::memory_order_acquire) == 0 || queue0AwaitsTitle.load(std::memory_order_acquire); }); flipHolders.fetch_sub(1, std::memory_order_acq_rel); @@ -182,6 +195,7 @@ void Driver::releaseFlipHold() { } void Driver::reserveOutputs(Submission& submission) { + std::set eopTokens; for (std::size_t cursor = 0; cursor < submission.commands.size();) { const auto* words = submission.commands.data() + cursor; if (words[0] == RenderingWaitPacketHeader) { @@ -191,6 +205,15 @@ void Driver::reserveOutputs(Submission& submission) { require(wait != nullptr, "video output returned a null rendering wait"); submission.renderingWaits.emplace(cursor, std::move(wait)); } + if (EopRelease(std::span(submission.commands).subspan(cursor))) { + const auto token = words[7]; + const auto flip = eopFlips.find(token); + if (flip != eopFlips.end()) { + require(submission.queue == 0, "EOP flips on async queues are not implemented"); + require(eopTokens.insert(token).second, "EOP flip token repeated in a submission"); + submission.flips.emplace(cursor, flip->second.request); + } + } noteHeldAtSubmit(submission, cursor); if (words[0] == FlipPacketHeader) { const auto output = outputs.find(words[1]); @@ -202,6 +225,7 @@ void Driver::reserveOutputs(Submission& submission) { } cursor += Pm4::PacketWords(words[0]); } + for (const auto token : eopTokens) eopFlips.erase(token); } void Driver::executeRewindTail(const Submission& stalled) { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp index efce0ef93cc..e7b12a29d83 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp @@ -453,6 +453,11 @@ void Validate(std::span packet, std::uint32_t queue) { require(packet.size() >= 5, "WRITE_DATA has no data"); require((packet[1] & ~(0x40110f00u | WriteDataCachePolicy)) == 0, "WRITE_DATA engine or reserved fields are not implemented"); const auto destination = (packet[1] >> 8u) & 0xfu; + if (destination == 0) { + const auto count = (packet[1] & 0x10000u) != 0 ? 1u : packet.size() - 4; + require(packet[3] == 0 && packet[2] >= 0xc340u && packet[2] <= 0xc347u && count <= 0xc348u - packet[2], "WRITE_DATA register destination outside the thread-trace user-data registers is not implemented"); + break; + } require(destination == 1 || destination == 2 || destination == 3 || destination == 5, "WRITE_DATA register destination is not implemented"); require((packet[2] & 3u) == 0, "misaligned WRITE_DATA destination"); break; @@ -578,6 +583,7 @@ std::optional DecodeLabelWrite(std::span packet return label; } if (opcode == 0x37 && packet.size() > 4) { + if (((packet[1] >> 8u) & 0xfu) == 0) return std::nullopt; const auto destination = address(packet[2], packet[3]); if (destination == 0) return std::nullopt; // WR_ONE_ADDR stores every value at the same address: the last one remains. @@ -1044,6 +1050,11 @@ void Execute(std::span packet, QueueState& queue) { return; } case 0x37: { + if (((packet[1] >> 8u) & 0xfu) == 0) { + const bool repeat = (packet[1] & 0x10000u) != 0; + for (std::size_t i = 4; i < packet.size(); ++i) writeRegister(queue, 0x79, packet[2] - 0xc000u + (repeat ? 0u : static_cast(i - 4)), packet[i]); + return; + } const auto destination = address(packet[2], packet[3]); if ((packet[1] & 0x10000u) != 0) { for (const auto& value : packet.subspan(4)) GuestMemory::Write(destination, std::as_bytes(std::span(&value, 1)), 4); diff --git a/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp b/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp index c2012bbc671..44a2886d86a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp @@ -553,6 +553,21 @@ void testCopies() { #endif } +void testTraceRegisterWrites() { + AgcDriver::QueueState state; + const auto repeated = makePacket(0x37, {0x06010000u, 0xc343u, 0u, 0x1234u, 0x5678u}); + check(!AgcDriver::Pm4::DecodeLabelWrite(repeated), "trace register payload was decoded as a guest memory label"); + execute(state, repeated); + check(state.userConfig.at(0x343u) == 0x5678u, "one-address trace write did not retain the last register value"); + execute(state, makePacket(0x37, {0x06000000u, 0xc340u, 0u, 1u, 2u, 3u})); + check(state.userConfig.at(0x340u) == 1u && state.userConfig.at(0x341u) == 2u && state.userConfig.at(0x342u) == 3u, "sequential trace write did not advance the register address"); + execute(state, makePacket(0x37, {0x06010000u, 0xc347u, 0u, 4u, 5u})); + check(state.userConfig.at(0x347u) == 5u, "one-address trace write crossed the register range"); + for (const auto& invalid : {makePacket(0x37, {0x06000000u, 0xc347u, 0u, 1u, 2u}), makePacket(0x37, {0x06010000u, 0xc348u, 0u, 1u}), makePacket(0x37, {0x06010000u, 0xc343u, 1u, 1u})}) { + expectFailure([&] { execute(state, invalid); }, "register destination outside"); + } +} + void testMemoryCopyDecode() { using AgcDriver::Pm4::DecodeMemoryCopy; constexpr std::uint64_t source = 0x1120000000ull, destination = 0x403d7b400ull; @@ -1241,6 +1256,7 @@ int main(int argc, char** argv) { testIndirectDraw(); testIndexAttributesIndirect(); testMemory(); + testTraceRegisterWrites(); testCopies(); testMemoryCopyDecode(); testMemorySynchronization(); diff --git a/core/libs/prx/libSceVideoOut/CMakeLists.txt b/core/libs/prx/libSceVideoOut/CMakeLists.txt index 0bfa4cdd06b..7c17f81bb8c 100644 --- a/core/libs/prx/libSceVideoOut/CMakeLists.txt +++ b/core/libs/prx/libSceVideoOut/CMakeLists.txt @@ -86,6 +86,14 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endforeach() + foreach(flipMode IN ITEMS eop eop_main eop_aftergpu) + 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) + set_tests_properties(video_out_flip_${flipMode} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() endif() if(BUILD_TESTING AND NOT WIN32) diff --git a/core/libs/prx/libSceVideoOut/include/Buffer.hpp b/core/libs/prx/libSceVideoOut/include/Buffer.hpp index 41a36673a72..381ec76d3ca 100644 --- a/core/libs/prx/libSceVideoOut/include/Buffer.hpp +++ b/core/libs/prx/libSceVideoOut/include/Buffer.hpp @@ -12,6 +12,7 @@ int APS5_VABI sceVideoOutRegisterBuffers2(int handle, int set_index, int buffer_ void APS5_VABI sceVideoOutSetBufferAttribute2(VideoOutBufferAttribute2* attribute, uint64_t pixel_format, uint32_t tiling_mode, uint32_t width, uint32_t height, uint64_t option, uint32_t dcc_control, uint64_t dcc_cb_register_clear_color); void APS5_VABI sceVideoOutAddBufferAttributeOption(VideoOutBufferAttribute2* attribute, uint64_t option); int APS5_VABI sceVideoOutSubmitChangeBufferAttribute2(int handle, int set_index, const VideoOutBufferAttribute2* attribute, void* option); +int APS5_VABI sceVideoOutSubmitEopFlip(int handle, int index, int flip_mode, int64_t flip_arg, std::uint32_t* token); int APS5_VABI sceVideoOutSubmitFlip(int handle, int index, int flip_mode, int64_t flip_arg); int APS5_VABI sceVideoOutUnregisterBuffers(int handle, int set_index); diff --git a/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp b/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp index 8cfcc7299c0..19ec3a0e95c 100644 --- a/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp +++ b/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp @@ -220,6 +220,7 @@ class VideoOutDriver { // 0, or VIDEO_OUT_ERROR_FLIP_QUEUE_FULL when the title has VIDEO_OUT_FLIP_QUEUE_CAPACITY flips pending. int SubmitFlip(int handle, int index, int flipMode, int64_t flipArg); + int SubmitEopFlip(int handle, int index, int flipMode, int64_t flipArg, std::uint32_t* token); private: bool close(int handle); diff --git a/core/libs/prx/libSceVideoOut/src/Buffer.cpp b/core/libs/prx/libSceVideoOut/src/Buffer.cpp index 82b62b6c24f..97afb5b653f 100644 --- a/core/libs/prx/libSceVideoOut/src/Buffer.cpp +++ b/core/libs/prx/libSceVideoOut/src/Buffer.cpp @@ -173,6 +173,13 @@ int APS5_VABI sceVideoOutUnregisterBuffers(int handle, int setIndex) try { LibcAwaitExit_nid_postfix(); } +int APS5_VABI sceVideoOutSubmitEopFlip(int handle, int index, int flipMode, int64_t flipArg, std::uint32_t* token) try { + if (token == nullptr) return VIDEO_OUT_ERROR_INVALID_ADDRESS; + return VideoOutDriver::Get().SubmitEopFlip(handle, index, flipMode, flipArg, token); +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); +} + int APS5_VABI sceVideoOutSubmitFlip(int handle, int index, int flipMode, int64_t flipArg) try { auto cfg = VideoOutDriver::Get().GetConfig(handle); if (cfg == nullptr) { diff --git a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp index 0decc717012..dc6ccde4b77 100644 --- a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp +++ b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include #include @@ -132,7 +133,10 @@ class VideoOutput final : public AgcDriver::IVideoOutput { require(room, "flip queue stayed full for 60 s: the presenter is not completing flips"); } - std::shared_ptr Reserve(const AgcDriver::FlipInfo& info) override { + std::shared_ptr Reserve(const AgcDriver::FlipInfo& info) override { return Reserve(info, false); } + std::shared_ptr TryReserve(const AgcDriver::FlipInfo& info) override { return Reserve(info, true); } + + std::shared_ptr Reserve(const AgcDriver::FlipInfo& info, bool limited) { // All flip modes are presented at the next vsync. require(info.mode >= VIDEO_OUT_FLIP_MODE_VSYNC && info.mode <= 6, "unsupported flip mode"); require(info.index >= VIDEO_OUT_BUFFER_INDEX_BLACK && info.index < VIDEO_OUT_BUFFER_NUM_MAX, "invalid flip index"); @@ -146,11 +150,15 @@ class VideoOutput final : public AgcDriver::IVideoOutput { std::lock_guard queueLock(queue->mutex); if (queue->failure) std::rethrow_exception(queue->failure); if (queue->stopping || cfg->shutdownToken.stop_requested()) throw ProcessShutdown{}; + if (limited && queue->reservations.load() >= VIDEO_OUT_FLIP_QUEUE_CAPACITY) return {}; std::lock_guard lock(cfg->mutex); checkConfig(*cfg); request->generation = cfg->generation; request->flipRate = cfg->flipRate; - if (info.index >= 0) request->reuseTicket = cfg->bufferReuse[info.index].Reserve(); + if (info.index >= 0) { + if (limited) require(cfg->buffers[info.index].Occupied(), "EOP flip buffer is not registered"); + request->reuseTicket = cfg->bufferReuse[info.index].Reserve(); + } ++queue->reservations; ++cfg->flipStatus.flipPendingNum; if (info.index >= 0) ++cfg->bufferPending[info.index]; @@ -437,6 +445,24 @@ int VideoOutDriver::SubmitFlip(int handle, int index, int flipMode, int64_t flip return 0; } +int VideoOutDriver::SubmitEopFlip(int handle, int index, int flipMode, int64_t flipArg, std::uint32_t* token) { + std::lock_guard lock(mutex); + if (handle <= 0 || handle >= VIDEO_OUT_NUM_MAX || !outputs[handle]) return VIDEO_OUT_ERROR_INVALID_HANDLE; + if (flipMode < VIDEO_OUT_FLIP_MODE_VSYNC || flipMode > 6) return VIDEO_OUT_ERROR_INVALID_VALUE; + if (index < VIDEO_OUT_BUFFER_INDEX_BLACK || index >= VIDEO_OUT_BUFFER_NUM_MAX) return VIDEO_OUT_ERROR_INVALID_INDEX; + const auto cfg = contexts[handle]; + { + std::lock_guard cfgLock(cfg->mutex); + checkConfig(*cfg); + if (index >= 0 && !cfg->buffers[index].Occupied()) return VIDEO_OUT_ERROR_INVALID_INDEX; + } + const AgcDriver::FlipInfo info{static_cast(handle), index, static_cast(flipMode), flipArg}; + const auto reserved = AgcDriverReserveEopFlip_nid_postfix(info); + if (reserved == 0) return VIDEO_OUT_ERROR_FLIP_QUEUE_FULL; + std::memcpy(token, &reserved, sizeof(reserved)); + return 0; +} + void VideoOutDriver::triggerEvents(VideoOutConfig& cfg, int eventKind, void* triggerData) { std::vector* events = nullptr; if (eventKind == VIDEO_OUT_EVENT_FLIP) events = &cfg.flipEvents; diff --git a/core/libs/prx/libSceVideoOut/tests/Flip.cpp b/core/libs/prx/libSceVideoOut/tests/Flip.cpp index b90bbc768ad..482dc6c4a35 100644 --- a/core/libs/prx/libSceVideoOut/tests/Flip.cpp +++ b/core/libs/prx/libSceVideoOut/tests/Flip.cpp @@ -615,7 +615,7 @@ void testReleaseVblank() { LibcRunShutdown_nid_postfix(); } -void testFlipAfterGpuWork() { +void testFlipAfterGpuWork(bool eop = false) { const int handle = sceVideoOutOpen(255, 0, 0, nullptr); auto cfg = VideoOutDriver::Get().GetConfig(handle); std::vector allocation(6 * 65536 + 65535); @@ -637,6 +637,15 @@ void testFlipAfterGpuWork() { if (cfg->failure) std::rethrow_exception(cfg->failure); check(done, "the first flip did not complete"); } + if (eop) { + std::uint32_t token = 0; + check(sceVideoOutSubmitEopFlip(handle, 0, 1, 5, &token) == 0, "GPU-work EOP reservation failed"); + const auto target = reinterpret_cast(busy.label); + const std::array release{0xc0064900u, 0x06200504u, 0x42010000u, static_cast(target), static_cast(target >> 32u), 1u, 0u, token}; + busy.commands.insert(busy.commands.end(), release.begin(), release.end()); + packet.addr = busy.commands.data(); + packet.dw_num = static_cast(busy.commands.size()); + } std::atomic landed{0}; const auto start = std::chrono::steady_clock::now(); const auto since = [&] { return std::chrono::duration_cast(std::chrono::steady_clock::now() - start).count(); }; @@ -645,7 +654,7 @@ void testFlipAfterGpuWork() { if (label == 1) landed.store(since()); }); check(sceAgcDriverSubmitDcb(&packet) == 0, "the GPU work was not accepted"); - check(sceVideoOutSubmitFlip(handle, 0, 1, 5) == 0, "the flip was not accepted"); + if (!eop) check(sceVideoOutSubmitFlip(handle, 0, 1, 5) == 0, "the flip was not accepted"); std::int64_t flipped = 0; { std::unique_lock lock(cfg->mutex); @@ -678,6 +687,97 @@ void testUnopenedEventRegistration() { LibcRunShutdown_nid_postfix(); } +void testEop(bool overlay) { + std::uint32_t token = 0x5a5a5a5a; + check(sceVideoOutSubmitEopFlip(0, 0, 1, 0, &token) == VIDEO_OUT_ERROR_INVALID_HANDLE && token == 0x5a5a5a5a, "invalid EOP handle changed the token"); + check(sceVideoOutSubmitEopFlip(1, 0, 1, 0, nullptr) == VIDEO_OUT_ERROR_INVALID_ADDRESS, "null EOP token output was accepted"); + const int bus = overlay ? VIDEO_OUT_BUS_TYPE_OVERLAY : VIDEO_OUT_BUS_TYPE_MAIN; + const int handle = sceVideoOutOpen(255, bus, 0, nullptr); + auto cfg = VideoOutDriver::Get().GetConfig(handle); + for (const auto [index, mode, error] : {std::array{16, 1, VIDEO_OUT_ERROR_INVALID_INDEX}, {0, 1, VIDEO_OUT_ERROR_INVALID_INDEX}, {-2, 7, VIDEO_OUT_ERROR_INVALID_VALUE}}) { + check(sceVideoOutSubmitEopFlip(handle, index, mode, 0, &token) == error && token == 0x5a5a5a5a, "invalid EOP parameters changed the token"); + } + std::vector allocation(65536 + 65535); + const auto storage = alignedBuffer(allocation); + VideoOutBuffers buffer{}; + buffer.data = storage.data(); + VideoOutBufferAttribute2 attribute{}; + attribute.width = 64; + attribute.height = 64; + attribute.pixel_format = 0x8000000000000000ull; + check(sceVideoOutRegisterBuffers2(handle, 0, 0, &buffer, 1, &attribute, 0, nullptr) == 0, "EOP-test buffer failed to register"); + alignas(std::uint32_t) std::array output; + output.fill(std::byte{0x5a}); + constexpr std::int64_t argument = -0x1234567890abcdefLL; + check(sceVideoOutSubmitEopFlip(handle, 0, 1, argument, reinterpret_cast(output.data() + 1)) == 0, "EOP flip reservation failed"); + std::memcpy(&token, output.data() + 1, sizeof(token)); + check(token != 0 && token <= 0x0fffffff && output.front() == std::byte{0x5a} && std::all_of(output.begin() + 5, output.end(), [](auto byte) { return byte == std::byte{0x5a}; }), "EOP token overwrote its DWORD output"); + VideoOutFlipStatus status{}; + check(sceVideoOutGetFlipStatus(handle, &status) == 0 && status.flipPendingNum == 1 && status.count == 0, "EOP reservation presented before its completion packet"); + alignas(8) std::uint64_t label = 0; + const auto address = reinterpret_cast(&label); + std::array release{0xc0064900u, 0x06200504u, 0x42010000u, static_cast(address), static_cast(address >> 32u), 1u, 0u, token}; + auto rejected = release; + rejected[0] = 0xc0054900u; + Packet malformed{rejected.data(), static_cast(rejected.size()), 0, {}}; + expectFailure([&] { sceAgcDriverSubmitDcb(&malformed); }); + check(sceVideoOutGetFlipStatus(handle, &status) == 0 && status.flipPendingNum == 1 && status.count == 0, "rejected packet consumed an EOP reservation"); + const auto submit = [](auto& words) { + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + check(sceAgcDriverSubmitDcb(&packet) == 0, "EOP completion packet was rejected"); + }; + std::vector duplicate(release.begin(), release.end()); + duplicate.insert(duplicate.end(), release.begin(), release.end()); + Packet twice{duplicate.data(), static_cast(duplicate.size()), 0, {}}; + check(expectFailure([&] { sceAgcDriverSubmitDcb(&twice); }).find("token repeated") != std::string::npos, "a submission consumed the same EOP token twice"); + const std::uint64_t bits = static_cast(argument); + std::vector completion{0xc0027900u, 0x342u, 0xc7010107u, static_cast(bus), 0xc0043700u, 0x06010000u, 0xc343u, 0u, static_cast(bits), static_cast(bits >> 32u)}; + completion.push_back(0x80004900u); + completion.insert(completion.end(), release.begin(), release.end()); + submit(completion); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); + do { + check(sceVideoOutGetFlipStatus(handle, &status) == 0, "EOP flip status failed"); + if (status.count == 1 && status.flipPendingNum == 0) break; + std::this_thread::sleep_for(std::chrono::milliseconds(2)); + } while (std::chrono::steady_clock::now() < deadline); + check(status.count == 1 && status.flipPendingNum == 0 && status.currentBuffer == 0 && status.flipArg == argument && std::atomic_ref(label).load() == 1, "EOP packet did not finish its label and reserved flip"); + release[5] = 2; + submit(release); + AgcDriverWaitIdle_nid_postfix(); + check(sceVideoOutGetFlipStatus(handle, &status) == 0 && status.count == 1 && status.flipPendingNum == 0 && std::atomic_ref(label).load() == 2, "ordinary context ID replay changed the flip or lost its label"); + std::array pending; + pending.fill(0x5a5a5a5a); + std::array outcomes{}; + std::vector callers; + for (std::size_t i = 0; i < pending.size(); ++i) callers.emplace_back([&, i] { outcomes[i] = sceVideoOutSubmitEopFlip(handle, VIDEO_OUT_BUFFER_INDEX_BLACK, 1, 0, &pending[i]); }); + for (auto& caller : callers) caller.join(); + std::vector abandoned; + for (std::size_t i = 0; i < pending.size(); ++i) { + if (outcomes[i] == 0) abandoned.push_back(pending[i]); + else check(outcomes[i] == VIDEO_OUT_ERROR_FLIP_QUEUE_FULL && pending[i] == 0x5a5a5a5a, "concurrent EOP overflow changed its token"); + } + std::sort(abandoned.begin(), abandoned.end()); + check(abandoned.size() == VIDEO_OUT_FLIP_QUEUE_CAPACITY && abandoned.front() > token && std::adjacent_find(abandoned.begin(), abandoned.end()) == abandoned.end(), "concurrent EOP reservations exceeded capacity or reused tokens"); + std::uint32_t unchanged = 0x5a5a5a5a; + check(sceVideoOutSubmitEopFlip(handle, VIDEO_OUT_BUFFER_INDEX_BLACK, 1, 0, &unchanged) == VIDEO_OUT_ERROR_FLIP_QUEUE_FULL && unchanged == 0x5a5a5a5a, "full EOP queue accepted a reservation or changed its output"); + sceVideoOutClose(handle); + { + std::lock_guard lock(cfg->mutex); + check(cfg->flipStatus.flipPendingNum == 0, "closing kept unsubmitted EOP reservations"); + } + const int reopened = sceVideoOutOpen(255, bus, 0, nullptr); + release[7] = abandoned.front(); + release[5] = 3; + submit(release); + AgcDriverWaitIdle_nid_postfix(); + check(sceVideoOutGetFlipStatus(reopened, &status) == 0 && status.count == 0 && status.flipPendingNum == 0 && std::atomic_ref(label).load() == 3, "cancelled context ID changed the reopened port or lost its label"); + std::uint32_t fresh = 0; + check(sceVideoOutSubmitEopFlip(reopened, VIDEO_OUT_BUFFER_INDEX_BLACK, 1, 7, &fresh) == 0 && fresh > abandoned.back(), "reopening reused an EOP token"); + sceVideoOutClose(reopened); + LibcRunShutdown_nid_postfix(); +} + void testOneDevice() { const int handle = sceVideoOutOpen(255, 0, 0, nullptr); auto cfg = VideoOutDriver::Get().GetConfig(handle); @@ -724,6 +824,9 @@ int main(int argc, char** argv) { else if (argc == 2 && std::string(argv[1]) == "unavailable") testUnavailable(); else if (argc == 2 && std::string(argv[1]) == "onedevice") testOneDevice(); else if (argc == 2 && std::string(argv[1]) == "unopened") testUnopenedEventRegistration(); + else if (argc == 2 && std::string(argv[1]) == "eop_aftergpu") testFlipAfterGpuWork(true); + else if (argc == 2 && std::string(argv[1]) == "eop") testEop(true); + else if (argc == 2 && std::string(argv[1]) == "eop_main") testEop(false); else if (argc == 2 && std::string(argv[1]) == "overlay") testOverlay(); else if (argc == 2 && std::string(argv[1]) == "deferred") testDeferredBuffer(); else if (argc == 2 && std::string(argv[1]) == "unregistered") testUnregisteredBuffer(); diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 59e914f35d7..34828a11a5a 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -65,6 +65,7 @@ - A [COND_EXEC range](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp) must hold whole packets and end inside its command buffer. A chained INDIRECT_BUFFER or a REWIND inside a range throws, as what the CP does with the skipped dwords then is unknown. - A [COND_INDIRECT_BUFFER](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp) (`sceAgcCbBranch`) runs only with the always-pass compare function; a comparison or a predicated packet throws. Modes 1 and 2 run the first buffer, assumed to be the if-then and if-then-else modes of AMD's PM4 packet. [sceAgcCbBranch](../../../core/libs/prx/libSceAgc/Cb/src/Branch.cpp) does not check the compare address of an always-pass branch or the address of an empty buffer, as PPSA14632 passes an uninitialised compare address and a null else buffer of 0 dwords. - A [label or store](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) (`RELEASE_MEM`, `WRITE_DATA`, `DMA_DATA`) into guest memory the device has no host import of waits for all recorded work and is then written by the CPU, so the queue worker stalls once per such label. The console's CP stores it in stream order without that stall. +- [WRITE_DATA to thread-trace user-data registers](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver) updates the queue's UCONFIG values for register destinations `0xc340..0xc347`, including repeated writes to one address; these are not guest-memory labels. The range follows AMD's GFX10 `SQ_THREAD_TRACE_USERDATA_0..7` definitions in [the Linux register header](https://github.com/torvalds/linux/blob/master/drivers/gpu/drm/amd/include/asic_reg/gc/gc_10_1_0_offset.h) and the packets emitted by PPSA21564's bundled AGC driver before its EOP flip. Other register destinations throw. GPU thread-trace capture and its user-data stream are not emitted; only the final register state is retained. Console trace behavior is unverified. - [WRITE_DATA with DST_SEL 3](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver) writes guest memory like DST_SEL 5. AMD's GFX10 header names 3 the GDS, but Killzone: Liberation (PPSA11142) writes its frame fence with `sceAgcDcbWriteData(dst 6, cache policy 3, write confirm)`, which the SPRX-confirmed encoding ([OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/cb_builders.c)) turns into DST_SEL 3 with a 40-bit label address in the title's own memory that `RELEASE_MEM` and `WAIT_REG_MEM` then use; the console must write that label or the title would wait on it forever. Whether the console routes it through the GDS or a cache is unknown - [SET_*_REG_INDIRECT](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp) (libSceAgcDriver) reads its register list when the command buffer is submitted, with the packets. The console's graphics PFP reads it when it reaches the packet, so a list written after the submission (by the CPU, or by GPU work ahead of the packet behind PFP_SYNC_ME) is read here before that write. - The [shader float mode](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (`FLOAT_MODE`, `DX10_CLAMP`, `IEEE_MODE` and `FP16_OVFL` of `COMPUTE_PGM_RSRC1` and `SPI_SHADER_PGM_RSRC1_PS/GS/HS`, bit positions from Mesa's `gfx10.json` and `gfx103.json`) is decoded into `GuestContext::floatMode` and is part of the shader cache keys and the serialized request (#981). `IEEE_MODE` is read by `v_min_f32`, `v_max_f32`, `v_min3_f32`, `v_max3_f32` and `v_med3_f32`, their f16 forms, `v_min_f64` and `v_max_f64`, `IEEE_MODE` and the `FLOAT_MODE` output denormal bits by the output modifier, `DX10_CLAMP` by the clamp modifier, `FP16_OVFL` by every instruction with an f16 result (see its entry), and the whole mode by the 16-bit interpolation instructions (see their entries). With IEEE=1 a signaling NaN operand returns that NaN quieted (the first operand checked first, min3/max3 as two nested pairs, med3 as min3 when any operand is NaN), measured on RDNA2 (gfx1035) over every combination of ±0, ±1, ±inf, denormals, quiet and signaling NaNs with f32 denormals kept and flushed. IEEE=0 and unknown modes keep the previous f32 lowering, which matches IEEE=0. The f16 forms choose their NaN results on the bits: a NaN operand returns the other operand unchanged (src0 when both are NaN), and with IEEE=1 a signaling NaN operand returns that NaN quieted, in the same order as f32. `v_min_f64` and `v_max_f64` quiet a signaling NaN operand the same way with IEEE=1; with IEEE=0 they return src0 unchanged when src1 is NaN, otherwise src1 when src0 is NaN. Both were measured on RDNA2 (gfx1036) at IEEE=0 and IEEE=1 with f32 denormals kept and flushed and f16/f64 denormals kept; RDNA2 has no f64 min3, max3 or med3. Unknown modes follow IEEE=0. `v_min_f16` and `v_max_f16` flush each denormal input to its signed zero when bit 0 of the f16/f64 denormal field is clear (0 and 2); fields 1 and 3 preserve inputs, even when output flushing is enabled. Measured on RDNA2 (gfx1036) at IEEE=0 and IEEE=1, including signed zeros, NaNs and neg/abs modifiers. Unknown modes keep f16 denormals. With f16/f64 denormals flushed the hardware also flushes denormal inputs of f64 min/max, which those lowerings still do not. NaN quieting in f16 arithmetic (the unary instructions and the f32 arithmetic follow it, see below) and `FLOAT_MODE` (f32 denormals flushed before the min/max compare in Astro's Playroom's 0xc0) still follow a single mode, as the entries below record. The mode comes from the registered shader header's `sh_registers`, the same source at registration and at draw/dispatch so the prepared artifact keys match. A title that rewrites RSRC1 at run time, or a raw compute program with no header, compiles with that mode or none. diff --git a/docs/dev/debt/libSceVideoOut.md b/docs/dev/debt/libSceVideoOut.md index ce195aa3064..011284f581d 100644 --- a/docs/dev/debt/libSceVideoOut.md +++ b/docs/dev/debt/libSceVideoOut.md @@ -23,5 +23,6 @@ - [sceVideoOutOpen](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - opening a bus that is already open returns `SCE_VIDEO_OUT_ERROR_RESOURCE_BUSY` (`0x80290009`), as [shadPS4's](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/libraries/videoout/driver.cpp#L53-L60) PS4 driver does for its main port; the PS5 result is unverified. PPSA99764 opens the main bus twice and carries on, presenting through the first handle - [Overlay output bus](../../../core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp) (libSceVideoOut) - flips on a port opened with `SCE_VIDEO_OUT_BUS_TYPE_OVERLAY` complete and signal their events like main-bus flips, but their buffers are not shown: the overlay is not composited over the main output. Presenting them as whole frames made the window alternate between the two buses +- [sceVideoOutSubmitEopFlip](../../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) reserves a flip and returns an opaque DWORD token; presentation is driven by a matching graphics-queue `RELEASE_MEM` with `INT_SEL` 2 and that token in `INT_CTXID`. This five-argument ABI and packet use are inferred from PPSA21564's bundled AGC driver, whose original packet-emission routine is checked in an isolated host ABI probe. The console token encoding, output width beyond the observed DWORD, error policy and reservation capacity are unverified. Host tokens occupy `0x04000000..0x07ffffff`, never repeat within a process, and exhaustion throws. Closing cancels unsubmitted reservations; only live reservations match; consumed or cancelled context IDs execute as ordinary label writes without targeting a reopened port. Async-queue EOP flips throw. Buffer registration, flip modes, queue capacity and vsync presentation follow the existing host VideoOut model, not measured console behavior. - [Mouse capture and delta scaling](../../../core/libs/prx/libSceVideoOut/src/MouseInput.cpp) (libSceVideoOut) - the console's mouse reports relative motion only, so a host cursor can never coincide with the title's cursor; while a title has a mouse open and the window is focused the host cursor is hidden and captured (SDL relative mode, released on focus loss or while the pad's mouse-to-stick toggle is on). Motion deltas are scaled from the presented image's size in window units to the title's display resolution with sub-pixel carry; the console's deltas are counts, so a title that applies its own sensitivity moves its cursor that much faster than the host pointer would have. The scale takes SDL's relative motion in logical window units, the units of `SDL_GetWindowSize`; under Windows DPI scaling SDL may report it in physical pixels, which is unverified (no Windows run yet), and a drawable larger than the window does not change the scale. - [Windows game controllers](../../../core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp) (libSceVideoOut) - SDL's DirectInput joystick backend is disabled at default hint priority before `SDL_InitSubSystem`, because its device enumeration blocks forever on some vendor HID devices (#1042). XInput and HIDAPI controllers are detected; controllers reachable only through DirectInput are not. Setting `SDL_DIRECTINPUT_ENABLED=1` restores them on hosts without such a device