diff --git a/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp b/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp index 493482c0ca..66df2fa8cb 100644 --- a/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp +++ b/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp @@ -173,6 +173,7 @@ struct FlipRequest final : AgcDriver::IFlipRequest, std::enable_shared_from_this VideoOutBuffer buffer; BufferAttributeGroup group; bool reserved = false; + bool unregistered = false; bool ready = false; bool gpuComplete = false; bool terminal = false; diff --git a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp index 9b07a020b6..2b52be2cd0 100644 --- a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp +++ b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include "SDL.h" #include "SDL_vulkan.h" @@ -64,6 +65,30 @@ void checkConfig(const VideoOutConfig& cfg) { cfg.Check(); } +void resolveFlipBuffer(FlipRequest& request, const VideoOutConfig& cfg) { + if (request.index < 0) { + request.width = cfg.width; + request.height = cfg.height; + return; + } + request.buffer = cfg.buffers[request.index]; + if (!request.buffer.Occupied()) { + static std::array, VIDEO_OUT_BUFFER_NUM_MAX> reported{}; + if (!reported[request.index].exchange(true)) std::fprintf(stderr, "[videoout] GPU flip of unregistered buffer %d on handle %u presents black\n", request.index, request.outputHandle); + request.unregistered = true; + request.width = cfg.width; + request.height = cfg.height; + return; + } + require(request.buffer.groupIndex < VIDEO_OUT_BUFFER_ATTRIBUTE_NUM_MAX, "invalid buffer group"); + request.group = cfg.groups[request.buffer.groupIndex]; + require(request.group.occupied, "buffer group is not registered"); + require(request.buffer.dataAddress != 0, "null registered buffer address"); + static_cast(DescribeVideoOutBuffer(request.buffer, request.group)); + request.width = request.group.attribute.width; + request.height = request.group.attribute.height; +} + class RenderingWait final : public AgcDriver::IRenderingWait { std::shared_ptr _config; std::uint32_t _index; @@ -118,20 +143,6 @@ public: if (queue->stopping || cfg->shutdownToken.stop_requested()) throw ProcessShutdown{}; std::lock_guard lock(cfg->mutex); checkConfig(*cfg); - if (info.index >= 0) { - request->buffer = cfg->buffers[info.index]; - require(request->buffer.Occupied(), "flip buffer is not registered"); - require(request->buffer.groupIndex < VIDEO_OUT_BUFFER_ATTRIBUTE_NUM_MAX, "invalid buffer group"); - request->group = cfg->groups[request->buffer.groupIndex]; - require(request->group.occupied, "buffer group is not registered"); - require(request->buffer.dataAddress != 0, "null registered buffer address"); - static_cast(DescribeVideoOutBuffer(request->buffer, request->group)); - request->width = request->group.attribute.width; - request->height = request->group.attribute.height; - } else { - request->width = cfg->width; - request->height = cfg->height; - } request->generation = cfg->generation; request->flipRate = cfg->flipRate; if (info.index >= 0) request->reuseTicket = cfg->bufferReuse[info.index].Reserve(); @@ -205,6 +216,7 @@ void FlipRequest::GpuReady(const std::shared_ptr& frameT return; } require(reserved && !ready && !terminal && cfg->generation == generation, "invalid flip readiness transition"); + resolveFlipBuffer(*this, *cfg); readiness.Mark("locks_validate"); queuedAt = AgcDriver::FrameTiming::Clock::now(); queue->requests.push_back(shared_from_this()); @@ -486,11 +498,11 @@ void VideoOutDriver::processFlip(FlipRequest& req) { request.gpuComplete = true; request.cfg->vblankCond.notify_all(); }; - if (req.index >= 0) { + if (req.index >= 0 && !req.unregistered) { const auto display = DescribeVideoOutBuffer(req.buffer, req.group); AgcDriverPresentBuffer_nid_postfix(target, display, gpuReady, &req); } else { - AgcDriverPresentClear_nid_postfix(target, req.index == VIDEO_OUT_BUFFER_INDEX_BLACK, gpuReady, &req); + AgcDriverPresentClear_nid_postfix(target, req.unregistered || req.index == VIDEO_OUT_BUFFER_INDEX_BLACK, gpuReady, &req); } timing.Mark("present"); window.UpdateTitle(); diff --git a/core/libs/prx/libSceVideoOut/tests/Flip.cpp b/core/libs/prx/libSceVideoOut/tests/Flip.cpp index 6add71dd3e..fa4a07c6fb 100644 --- a/core/libs/prx/libSceVideoOut/tests/Flip.cpp +++ b/core/libs/prx/libSceVideoOut/tests/Flip.cpp @@ -116,6 +116,63 @@ void testLifetime(bool reopen) { LibcRunShutdown_nid_postfix(); } +void testDeferredBuffer() { + const int handle = sceVideoOutOpen(255, 0, 0, nullptr); + auto cfg = VideoOutDriver::Get().GetConfig(handle); + auto gate = std::make_shared(); + AgcDriverRegisterVideoOutput_nid_postfix(7, gate); + std::array held{0xc004105c, 7, 0xfffffffeu, 1, 0, 0}; + Packet heldPacket{held.data(), 6, 0, {}}; + check(sceAgcDriverSubmitDcb(&heldPacket) == 0, "the gated flip was not accepted"); + std::array flip{0xc004105c, static_cast(handle), 0, 1, 3, 0}; + Packet flipPacket{flip.data(), 6, 0, {}}; + check(sceAgcDriverSubmitDcb(&flipPacket) == 0, "a GPU flip of a buffer registered before the GPU reaches it was rejected at submit"); + 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"); + gate->Release(); + { + std::unique_lock lock(cfg->mutex); + const bool done = cfg->vblankCond.wait_for(lock, std::chrono::seconds(30), [&] { return cfg->failure || cfg->flipStatus.count == 1; }); + if (cfg->failure) std::rethrow_exception(cfg->failure); + check(done, "the deferred flip did not complete"); + check(cfg->flipStatus.flipArg == 3 && cfg->flipStatus.currentBuffer == 0, "the deferred flip presented the wrong request"); + } + AgcDriverWaitIdle_nid_postfix(); + sceVideoOutUnregisterBuffers(handle, 0); + sceVideoOutClose(handle); + AgcDriverUnregisterVideoOutput_nid_postfix(7, gate); + LibcRunShutdown_nid_postfix(); +} + +void testUnregisteredBuffer() { + 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"); + check(sceVideoOutUnregisterBuffers(handle, 0) == 0, "buffer unregistration failed"); + std::array flip{0xc004105c, static_cast(handle), 0, 1, 7, 0}; + Packet flipPacket{flip.data(), 6, 0, {}}; + check(sceAgcDriverSubmitDcb(&flipPacket) == 0, "a GPU flip of an unregistered buffer was rejected at submit"); + { + std::unique_lock lock(cfg->mutex); + const bool done = cfg->vblankCond.wait_for(lock, std::chrono::seconds(30), [&] { return cfg->failure || cfg->flipStatus.count == 1; }); + if (cfg->failure) std::rethrow_exception(cfg->failure); + check(done, "a GPU flip of an unregistered buffer did not complete"); + check(cfg->flipStatus.flipPendingNum == 0 && cfg->flipStatus.flipArg == 7 && cfg->bufferPending[0] == 0, "a GPU flip of an unregistered buffer left pending state"); + } + AgcDriverWaitIdle_nid_postfix(); + sceVideoOutClose(handle); + LibcRunShutdown_nid_postfix(); +} + std::size_t tiledOffset(uint32_t x, uint32_t y, uint32_t width) { constexpr std::array xMasks{4, 8, 128, 256, 0x2200, 0x800, 0x8400}; constexpr std::array yMasks{16, 32, 64, 0x1100, 0x200, 0x400, 0x4800}; @@ -613,6 +670,8 @@ 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]) == "overlay") testOverlay(); + else if (argc == 2 && std::string(argv[1]) == "deferred") testDeferredBuffer(); + else if (argc == 2 && std::string(argv[1]) == "unregistered") testUnregisteredBuffer(); else testLifetime(argc == 2 && std::string(argv[1]) == "reopen"); std::puts("VideoOut flip tests passed"); return 0; diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index ce0d456aaf..81f333d107 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -107,6 +107,7 @@ Throughout the project, every function at every stage either **does exactly what - [libSceVdecsw](../../core/libs/prx/libSceVdecsw/Export.cpp) (libSceVdecsw) - structure layouts follow shadPS4's `vdecsw.h` and were checked against PPSA05684's movie decoder (`movie-decoder-vdecsw.cpp`), which asserts `pictureCount == 1` at offset 0xb of `OutputInfo` and reads `InputResult` offset 0x10 as the number of frames an access unit produced. The compute, decoder memory and decode input structures and the first 0x28 bytes of the 0x50-byte decoder config are those of libSceVideodec2, and decoding goes through it. The 0x78-byte AVC picture info is filled with Videodec2's leading fields and the cropping fields only. OUTPUT_PENDING (0x81510115), INPUT_QUEUE_EMPTY (0x81510116) and DECODE_PENDING (0x81510117) follow from the codes each call's assert accepts. At most four decoded pictures are queued and the last one is held until `sceVdecswFinalizeDecodeSequence`; the console's queue depth is unknown - [sceGameLiveStreamingGetCurrentStatus2](../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - the status layout (user id, on-air flag, spectator, text and command message counts, broadcast resolution, 48 reserved bytes) is taken from shadPS4's `OrbisGameLiveStreamingStatus2`; the call reports that no stream is on air, with an invalid user id - [Depth clear draws](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - a draw with `DB_RENDER_CONTROL.DEPTH_CLEAR_ENABLE` is taken to write `DB_DEPTH_CLEAR` to every pixel it covers whatever the depth test, depth bounds and depth bias say: it is drawn with an always-passing depth write and the viewport depth range pinned to the clear value. The draw is rasterized as submitted, so its coverage is the draw's own. Whether the DB still applies the depth test or HiZ to such a draw is not measured +- [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 - [sceAgcDriverGetEqContextId](../../core/libs/prx/libSceAgcDriver/Eq/src/Query.cpp) (libSceAgcDriver) - returns `ident`: an end-of-pipe interrupt is delivered only to the registration whose id equals the raising queue, so `ident` is that context's id (titles register 0 or 1 for graphics and `0x20 + 8 * pipe + queue` for compute). The console's value is not known; KytyPS5 returns the event's `data`, which it fills with the interrupt's context id, whereas `data` here is the EV_CLEAR trigger count. Other event filters throw - [sceAgcSetSubmitMode](../../core/libs/prx/libSceAgc/Misc/src/Suspend.cpp) (libSceAgc) - mode values unknown; only 0 is accepted - [User data registers](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp) (AGC driver) - a `SPI_SHADER_USER_DATA_*` or `COMPUTE_USER_DATA_*` register the title never wrote reads as 0 when a stage's RSRC2 count covers it, as Kyty (zeroed `UserSgprInfo`) and SharpEmu (missing registers read 0) do, and as AMD PAL and Mesa initialize them; the console's value is not confirmed