fix(agc-driver): queue frame dumps at the dump scale

A frame presented from guest memory with APS5_DUMP_FRAMES is read back
at the size of the display buffer, and that whole copy went into the
dump writer's queue with the scale beside it (31.6 MiB for a 3840x2160
buffer). The queue holds 64 MiB, so the third frame the writer had not
finished ended the run with "frame dump queue exceeded 64 MiB". A frame
blitted from the resident image is already reduced on the GPU and queued
at the reduced size.

The readback is now reduced by the dump scale when the slot retires and
queued at that size, the same pixels the writer kept before. With
APS5_DUMP_SCALE=1 nothing is reduced and the limit is reached as before.

video_out_frame_dump_queue presents four 3840x2160 frames from guest
memory while the writer is held on the first file and compares every
dump with the decoded display buffer.
This commit is contained in:
Dean Galvin 2026-10-12 18:54:34 +13:00
parent b42ef24e07
commit abc1cfa57d
3 changed files with 181 additions and 5 deletions

View File

@ -591,13 +591,19 @@ struct VulkanDevice::State {
void WriteFrameDump(const PresentSlot& slot) {
require(slot.dumpBuffer != nullptr, "frame dump was not recorded");
std::vector<std::byte> full(slot.dumpBuffer->Bytes().begin(), slot.dumpBuffer->Bytes().end());
const auto full = slot.dumpBuffer->Bytes();
const auto index = slot.dumpIndex;
const auto width = slot.dumpWidth;
const auto height = slot.dumpHeight;
const auto scale = slot.dumpScale;
require(scale != 0 && full.size() == static_cast<std::size_t>(slot.dumpWidth) * slot.dumpHeight * 4, "invalid frame dump pixels");
const auto width = (slot.dumpWidth + scale - 1) / scale;
const auto height = (slot.dumpHeight + scale - 1) / scale;
std::vector<std::byte> pixels(static_cast<std::size_t>(width) * height * 4);
for (std::uint32_t y = 0; y < height; ++y) {
for (std::uint32_t x = 0; x < width; ++x) std::memcpy(pixels.data() + (static_cast<std::size_t>(y) * width + x) * 4, full.data() + (static_cast<std::size_t>(y) * scale * slot.dumpWidth + static_cast<std::size_t>(x) * scale) * 4, 4);
}
static const auto start = std::chrono::steady_clock::now();
std::fprintf(stderr, "[gpu] frame_%03d.bmp: %ux%u display read back on the GPU at %.1f s\n", index, width, height, std::chrono::duration<double>(std::chrono::steady_clock::now() - start).count());
dumpWriter.Enqueue(index, width, height, slot.dumpScale, std::move(full));
std::fprintf(stderr, "[gpu] frame_%03d.bmp: %ux%u display read back on the GPU at %.1f s\n", index, slot.dumpWidth, slot.dumpHeight, std::chrono::duration<double>(std::chrono::steady_clock::now() - start).count());
dumpWriter.Enqueue(index, width, height, 1, std::move(pixels));
}
// Blocks on the fence, or polls it every 50 us with APS5_PRESENT_POLL_FENCE=1 (the difference

View File

@ -51,6 +51,16 @@ if(BUILD_TESTING)
endforeach()
endif()
if(BUILD_TESTING AND NOT WIN32)
add_test_executable(video_out_frame_dump_queue_tests tests/FrameDumpQueue.cpp)
target_include_directories(video_out_frame_dump_queue_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_SDL2_INCLUDE_DIR} ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR})
target_link_libraries(video_out_frame_dump_queue_tests PRIVATE libSceVideoOut libSceAgcDriver libkernel libc)
set_target_properties(video_out_frame_dump_queue_tests PROPERTIES CXX_EXTENSIONS OFF)
add_test(NAME video_out_frame_dump_queue COMMAND video_out_frame_dump_queue_tests)
set_tests_properties(video_out_frame_dump_queue PROPERTIES TIMEOUT 30 SKIP_RETURN_CODE 77 RESOURCE_LOCK videoout_app0
ENVIRONMENT "SDL_VIDEODRIVER=${ANYPS5_VIDEO_OUT_TEST_DRIVER};APS5_DUMP_FRAMES=4;APS5_DUMP_SCALE=4")
endif()
if(BUILD_TESTING)
add_test_executable(pad_sdl_tests tests/PadSdl.cpp)
target_link_libraries(pad_sdl_tests PRIVATE SDL2-static)

View File

@ -0,0 +1,160 @@
#include "prx/libSceVideoOut/include/VideoOutDriver.hpp"
#include "prx/libSceVideoOut/include/Buffer.hpp"
#include "prx/libSceVideoOut/include/Output.hpp"
#include "prx/libSceAgcDriver/Execution/include/DisplayBuffer.hpp"
#include "prx/libc/include/Shutdown.hpp"
#include "tests/VideoOutTestEnvironment.hpp"
#include <chrono>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <exception>
#include <fstream>
#include <iterator>
#include <mutex>
#include <span>
#include <stdexcept>
#include <string>
#include <vector>
#include <fcntl.h>
#include <poll.h>
#include <sys/stat.h>
#include <unistd.h>
#undef main
namespace {
constexpr std::uint32_t Width = 3840;
constexpr std::uint32_t Height = 2160;
constexpr std::uint32_t Scale = 4;
constexpr std::uint32_t DumpWidth = Width / Scale;
constexpr std::uint32_t DumpHeight = Height / Scale;
constexpr std::size_t DumpBytes = 54 + static_cast<std::size_t>(DumpWidth) * DumpHeight * 4;
constexpr int Frames = 4;
void check(bool condition, const char* reason) {
if (!condition) throw std::runtime_error(reason);
}
std::string frameName(int index) {
char name[32];
std::snprintf(name, sizeof(name), "frame_%03d.bmp", index);
return name;
}
class HeldDump final {
int descriptor;
public:
explicit HeldDump(int index) {
const auto name = frameName(index);
check(mkfifo(name.c_str(), 0600) == 0, "a frame dump pipe could not be created");
descriptor = open(name.c_str(), O_RDWR);
check(descriptor >= 0, "a frame dump pipe could not be opened");
}
~HeldDump() { close(descriptor); }
HeldDump(const HeldDump&) = delete;
HeldDump& operator=(const HeldDump&) = delete;
std::vector<char> Release() const {
std::vector<char> dump(DumpBytes);
for (std::size_t done = 0; done < dump.size();) {
pollfd waiting{descriptor, POLLIN, 0};
check(poll(&waiting, 1, 15000) == 1, "a queued frame dump was not written");
const auto count = read(descriptor, dump.data() + done, dump.size() - done);
check(count > 0, "a frame dump pipe could not be read");
done += static_cast<std::size_t>(count);
}
return dump;
}
};
std::vector<char> readDump(int index) {
std::ifstream file(frameName(index), std::ios::binary);
check(file.is_open(), "a frame dump is missing");
return {std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>()};
}
void checkDump(std::span<const char> dump, std::span<const std::byte> decoded) {
check(dump.size() == DumpBytes, "a frame dump has the wrong size");
std::uint32_t extent[2];
std::memcpy(extent, dump.data() + 18, sizeof(extent));
check(extent[0] == DumpWidth && extent[1] == static_cast<std::uint32_t>(-static_cast<std::int32_t>(DumpHeight)), "a frame dump has the wrong extent");
for (std::uint32_t y = 0; y < DumpHeight; ++y) {
for (std::uint32_t x = 0; x < DumpWidth; ++x) {
const auto dumped = dump.data() + 54 + (static_cast<std::size_t>(y) * DumpWidth + x) * 4;
const auto source = decoded.data() + (static_cast<std::size_t>(y) * Scale * Width + static_cast<std::size_t>(x) * Scale) * 4;
check(std::memcmp(dumped, source, 4) == 0, "a frame dump pixel differs from the display buffer");
}
}
}
void testDumpQueue() {
const int handle = sceVideoOutOpen(255, 0, 0, nullptr);
auto cfg = VideoOutDriver::Get().GetConfig(handle);
std::vector<std::byte> allocation(static_cast<std::size_t>((Width + 127) / 128) * ((Height + 127) / 128) * 65536 + 65535);
const auto address = reinterpret_cast<std::uintptr_t>(allocation.data());
const std::span<std::byte> storage{allocation.data() + ((65536u - (address & 65535u)) & 65535u), allocation.size() - 65535u};
const AgcDriver::DisplayBuffer display{reinterpret_cast<std::uint64_t>(storage.data()), 0x8000000000000000ull, Width, Height};
check(AgcDriver::DisplayBufferSize(display) == storage.size(), "incorrect padded display footprint");
for (std::uint32_t y = 0; y < Height; ++y) {
for (std::uint32_t x = 0; x < Width; ++x) {
const auto offset = AgcDriver::DisplayBufferOffset(display, x, y);
storage[offset] = static_cast<std::byte>(x & 255u);
storage[offset + 1] = static_cast<std::byte>(y & 255u);
storage[offset + 2] = static_cast<std::byte>((x >> 8u) | ((y >> 8u) << 4u));
storage[offset + 3] = std::byte{255};
}
}
const auto decoded = AgcDriver::ReadDisplayBuffer(display);
VideoOutBuffers buffer{storage.data(), nullptr, {nullptr, nullptr}};
VideoOutBufferAttribute2 attribute{};
sceVideoOutSetBufferAttribute2(&attribute, display.pixelFormat, 0, Width, Height, 0, 0, 0);
check(sceVideoOutRegisterBuffers2(handle, 0, 0, &buffer, 1, &attribute, 0, nullptr) == 0, "buffer registration failed");
const HeldDump first(0);
const HeldDump last(Frames - 1);
std::exception_ptr failure;
try {
for (int frame = 0; frame < Frames; ++frame) {
check(sceVideoOutSubmitFlip(handle, 0, 1, frame) == 0, "the flip was not accepted");
std::unique_lock lock(cfg->mutex);
check(cfg->vblankCond.wait_for(lock, std::chrono::seconds(15), [&] { return (cfg->failure && cfg->flipStatus.flipPendingNum == 0) || cfg->flipStatus.count == static_cast<std::uint64_t>(frame) + 1; }), "the flip did not complete");
if (cfg->failure) std::rethrow_exception(cfg->failure);
}
} catch (...) {
failure = std::current_exception();
}
const auto firstDump = first.Release();
if (failure) std::rethrow_exception(failure);
checkDump(firstDump, decoded);
const auto lastDump = last.Release();
checkDump(lastDump, decoded);
for (int frame = 1; frame < Frames - 1; ++frame) checkDump(readDump(frame), decoded);
sceVideoOutUnregisterBuffers(handle, 0);
sceVideoOutClose(handle);
LibcRunShutdown_nid_postfix();
}
}
int main() {
VideoOutTestEnvironment environment;
try {
testDumpQueue();
std::puts("VideoOut frame dump queue test passed");
return 0;
} catch (const std::exception& error) {
std::fprintf(stderr, "%s\n", error.what());
try { LibcRunShutdown_nid_postfix(); }
catch (const std::exception& shutdown) { std::fprintf(stderr, "shutdown: %s\n", shutdown.what()); }
if (std::string(error.what()).find("Vulkan support") != std::string::npos && !std::getenv("ANYPS5_REQUIRE_DISPLAY")) {
std::printf("skipped, no display or Vulkan device: %s\n", error.what());
return 77;
}
return 1;
}
}