mirror of
https://github.com/boykopovar/AnyPS5.git
synced 2026-11-05 13:20:11 +00:00
feat(libs): resolve command-buffer flip buffers when the GPU reaches the flip
A flip packet names a buffer index; the buffer was looked up when the command buffer was submitted, so a title that registers its buffers after submitting the first frame failed with "flip buffer is not registered". The buffer is now resolved when the GPU reaches the flip, as the hardware does. A flip whose buffer is still unregistered then presents black and completes (see TechnicalDebt). Uncharted: Legacy of Thieves Collection (PPSA05684) submits its first flip before registering its buffers and stopped with "flip buffer is not registered".
This commit is contained in:
1 parent
2944cc3266
commit
c9fe124c10
4 files changed
+89
-16
No files matched your search
@@ -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;
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <thread>
|
||||
#include <limits>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
#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<std::atomic<bool>, 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<void>(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<VideoOutConfig> _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<void>(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<AgcDriver::FrameTiming>& 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();
|
||||
|
||||
@@ -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<Gate>();
|
||||
AgcDriverRegisterVideoOutput_nid_postfix(7, gate);
|
||||
std::array<uint32_t, 6> 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<uint32_t, 6> flip{0xc004105c, static_cast<uint32_t>(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<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");
|
||||
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<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");
|
||||
check(sceVideoOutUnregisterBuffers(handle, 0) == 0, "buffer unregistration failed");
|
||||
std::array<uint32_t, 6> flip{0xc004105c, static_cast<uint32_t>(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<uint32_t, 7> xMasks{4, 8, 128, 256, 0x2200, 0x800, 0x8400};
|
||||
constexpr std::array<uint32_t, 7> 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;
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user