diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index fa9d8b151..9c65aa876 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -591,13 +591,19 @@ struct VulkanDevice::State { void WriteFrameDump(const PresentSlot& slot) { require(slot.dumpBuffer != nullptr, "frame dump was not recorded"); - std::vector 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(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 pixels(static_cast(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(y) * width + x) * 4, full.data() + (static_cast(y) * scale * slot.dumpWidth + static_cast(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(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(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 diff --git a/core/libs/prx/libSceVideoOut/CMakeLists.txt b/core/libs/prx/libSceVideoOut/CMakeLists.txt index 7485b2123..8a345b532 100644 --- a/core/libs/prx/libSceVideoOut/CMakeLists.txt +++ b/core/libs/prx/libSceVideoOut/CMakeLists.txt @@ -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) diff --git a/core/libs/prx/libSceVideoOut/tests/FrameDumpQueue.cpp b/core/libs/prx/libSceVideoOut/tests/FrameDumpQueue.cpp new file mode 100644 index 000000000..2c5b74ad1 --- /dev/null +++ b/core/libs/prx/libSceVideoOut/tests/FrameDumpQueue.cpp @@ -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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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(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 Release() const { + std::vector 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(count); + } + return dump; + } +}; + +std::vector readDump(int index) { + std::ifstream file(frameName(index), std::ios::binary); + check(file.is_open(), "a frame dump is missing"); + return {std::istreambuf_iterator(file), std::istreambuf_iterator()}; +} + +void checkDump(std::span dump, std::span 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(-static_cast(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(y) * DumpWidth + x) * 4; + const auto source = decoded.data() + (static_cast(y) * Scale * Width + static_cast(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 allocation(static_cast((Width + 127) / 128) * ((Height + 127) / 128) * 65536 + 65535); + const auto address = reinterpret_cast(allocation.data()); + const std::span storage{allocation.data() + ((65536u - (address & 65535u)) & 65535u), allocation.size() - 65535u}; + const AgcDriver::DisplayBuffer display{reinterpret_cast(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(x & 255u); + storage[offset + 1] = static_cast(y & 255u); + storage[offset + 2] = static_cast((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(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; + } +}