mirror of
https://github.com/boykopovar/AnyPS5.git
synced 2026-10-23 00:53:02 -05:00
The driver created a windowless Vulkan device at the first dispatch and replaced it with a windowed one at the first flip. The replacement built a second device and re-imported every host import, and the first flip paid for it. VideoOut now creates its window when it starts (the first VideoOut call), at the default 1920x1080 output, and attaches it to the driver (AgcDriverAttachWindow) before the call returns. The driver creates its device with that window, so GPU work and presentation use one device. Present no longer creates or replaces a device. A device created by GPU work before VideoOut started is still replaced, now in AttachWindow. A failure to create the window or the device is thrown from that first VideoOut call instead of terminating the presentation thread. Tests: - video_out_flip_tests onedevice: GPU work, then a flip, leaves one device. - pacing makes its first presentation span vblanks with GPU work queued ahead of the flip; it relied on the device being created at the flip. - unavailable expects the failure from sceVideoOutOpen. - guest_video_out_wqhd_detection writes a param.json for the window title and is skipped without a display or Vulkan device unless ANYPS5_REQUIRE_DISPLAY is set. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
545 lines
30 KiB
C++
545 lines
30 KiB
C++
#include "prx/libSceVideoOut/include/VideoOutDriver.hpp"
|
|
#include "prx/libSceVideoOut/include/Buffer.hpp"
|
|
#include "prx/libSceVideoOut/include/Output.hpp"
|
|
#include "prx/libSceVideoOut/include/Event.hpp"
|
|
#include "prx/libkernel/Equeue/Equeue.hpp"
|
|
#include "prx/libSceAgcDriver/Execution/include/Driver.hpp"
|
|
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
|
|
#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp"
|
|
#include "prx/libc/include/Shutdown.hpp"
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <atomic>
|
|
#include <chrono>
|
|
#include <cstdint>
|
|
#include <cstdio>
|
|
#include <initializer_list>
|
|
#include <string>
|
|
#include <limits>
|
|
#include <thread>
|
|
#include <vector>
|
|
|
|
#undef main
|
|
|
|
extern "C" int APS5_VABI sceKernelCreateEqueue(KernelEqueue* eq, const char* name);
|
|
extern "C" int APS5_VABI sceKernelDeleteEqueue(KernelEqueue eq);
|
|
extern "C" int APS5_VABI sceKernelAllocateDirectMemory(std::int64_t, std::int64_t, std::size_t, std::size_t, int, std::int64_t*);
|
|
extern "C" int APS5_VABI sceKernelMapDirectMemory(void**, std::size_t, int, int, std::int64_t, std::size_t);
|
|
|
|
namespace {
|
|
|
|
void check(bool condition, const char* reason) {
|
|
if (!condition) throw std::runtime_error(reason);
|
|
}
|
|
|
|
template<typename TAction>
|
|
std::string expectFailure(TAction action) {
|
|
try { action(); }
|
|
catch (const std::runtime_error& error) { return error.what(); }
|
|
throw std::runtime_error("expected an exception");
|
|
}
|
|
|
|
class Gate final : public AgcDriver::IVideoOutput, public AgcDriver::IFlipRequest, public std::enable_shared_from_this<Gate> {
|
|
public:
|
|
std::mutex mutex;
|
|
std::condition_variable changed;
|
|
bool released = false;
|
|
std::shared_ptr<AgcDriver::IFlipRequest> Reserve(const AgcDriver::FlipInfo&) override { return shared_from_this(); }
|
|
void GpuReady(const std::shared_ptr<AgcDriver::FrameTiming>&) override {
|
|
std::unique_lock lock(mutex);
|
|
if (!changed.wait_for(lock, std::chrono::seconds(10), [&] { return released; })) throw std::runtime_error("test gate timed out");
|
|
}
|
|
void Fail(std::exception_ptr error) noexcept override { if (!error) std::terminate(); }
|
|
void Release() {
|
|
std::lock_guard lock(mutex);
|
|
released = true;
|
|
changed.notify_all();
|
|
}
|
|
};
|
|
|
|
std::span<std::byte> alignedBuffer(std::vector<std::byte>& allocation) {
|
|
const auto address = reinterpret_cast<uintptr_t>(allocation.data());
|
|
const auto offset = (65536u - (address & 65535u)) & 65535u;
|
|
return {allocation.data() + offset, allocation.size() - 65535u};
|
|
}
|
|
|
|
void testLifetime(bool reopen) {
|
|
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> words{0xc004105c, 7, 0xfffffffeu, 1, 0, 0};
|
|
Packet packet{words.data(), 6, 0, {}};
|
|
sceAgcDriverSubmitDcb(&packet);
|
|
std::vector<std::byte> allocation(65536 + 65535);
|
|
const auto storage = alignedBuffer(allocation);
|
|
VideoOutBuffers buffer{storage.data(), nullptr, {nullptr, nullptr}};
|
|
VideoOutBufferAttribute2 attribute{};
|
|
attribute.width = 64;
|
|
attribute.height = 64;
|
|
attribute.pixel_format = 0x8000000000000000ull;
|
|
sceVideoOutRegisterBuffers2(handle, 0, 0, &buffer, 1, &attribute, 0, nullptr);
|
|
sceVideoOutSubmitFlip(handle, 0, 1, -9);
|
|
{
|
|
std::lock_guard lock(cfg->mutex);
|
|
check(cfg->flipStatus.flipPendingNum == 1 && cfg->flipStatus.count == 0, "reservation status is wrong");
|
|
}
|
|
attribute.dcc_control = 1;
|
|
check(expectFailure([&] { sceVideoOutSubmitChangeBufferAttribute2(handle, 0, &attribute, nullptr); }).find("pending flip") != std::string::npos, "pending attributes were changed");
|
|
check(expectFailure([&] { sceVideoOutUnregisterBuffers(handle, 0); }).find("pending flip") != std::string::npos, "pending buffer was unregistered");
|
|
check(sceVideoOutWaitVblank(handle) == 0 && sceVideoOutWaitVblank(handle) == 0, "vblank stalled behind the GPU queue");
|
|
std::shared_ptr<VideoOutConfig> replacement;
|
|
sceVideoOutClose(handle);
|
|
if (reopen) {
|
|
check(sceVideoOutOpen(255, 0, 0, nullptr) == handle, "reopen changed handle");
|
|
replacement = VideoOutDriver::Get().GetConfig(handle);
|
|
check(replacement != cfg && replacement->generation > cfg->generation, "reopen reused old port state");
|
|
}
|
|
gate->Release();
|
|
std::exception_ptr failure;
|
|
{
|
|
std::unique_lock lock(cfg->mutex);
|
|
check(cfg->vblankCond.wait_for(lock, std::chrono::seconds(10), [&] { return cfg->failure != nullptr; }), "flip failure did not wake waiters");
|
|
failure = cfg->failure;
|
|
check(cfg->flipStatus.count == 0 && cfg->flipStatus.flipPendingNum == 0, "failed flip has successful or pending status");
|
|
}
|
|
const auto message = expectFailure([&] { std::rethrow_exception(failure); });
|
|
check(message.find("closed") != std::string::npos, "flip used a closed port");
|
|
if (replacement) {
|
|
std::lock_guard lock(replacement->mutex);
|
|
check(replacement->flipStatus.flipPendingNum == 0 && replacement->flipStatus.count == 0, "old request changed new port counters");
|
|
}
|
|
check(expectFailure([&] { sceVideoOutWaitVblank(handle); }).find("closed") != std::string::npos, "VideoOut lost worker failure");
|
|
AgcDriverUnregisterVideoOutput_nid_postfix(7, gate);
|
|
if (reopen) sceVideoOutClose(handle);
|
|
const auto shutdown = expectFailure([] { LibcRunShutdown_nid_postfix(); });
|
|
check(shutdown.find("closed") != std::string::npos, "shutdown lost asynchronous error");
|
|
}
|
|
|
|
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};
|
|
uint32_t offset = 0;
|
|
for (uint32_t bit = 0; bit < 7; ++bit) {
|
|
if ((x & (1u << bit)) != 0) offset ^= xMasks[bit];
|
|
if ((y & (1u << bit)) != 0) offset ^= yMasks[bit];
|
|
}
|
|
return (static_cast<std::size_t>(y / 128u) * ((width + 127u) / 128u) + x / 128u) * 65536u + offset;
|
|
}
|
|
|
|
void fillBuffer(std::span<std::byte> bytes, uint32_t width, uint32_t height) {
|
|
for (uint32_t y = 0; y < height; ++y) {
|
|
for (uint32_t x = 0; x < width; ++x) {
|
|
const auto offset = tiledOffset(x, y, width);
|
|
bytes[offset] = static_cast<std::byte>(x & 255u);
|
|
bytes[offset + 1] = static_cast<std::byte>(y & 255u);
|
|
bytes[offset + 2] = static_cast<std::byte>((x ^ y) & 255u);
|
|
bytes[offset + 3] = std::byte{255};
|
|
}
|
|
}
|
|
}
|
|
|
|
void testDecode() {
|
|
std::vector<std::byte> allocation(6 * 65536 + 65535);
|
|
const auto tiled = alignedBuffer(allocation);
|
|
AgcDriver::DisplayBuffer buffer{reinterpret_cast<uint64_t>(tiled.data()), 0x8000000000000000ull, 259, 137};
|
|
check(AgcDriver::DisplayBufferSize(buffer) == tiled.size(), "incorrect padded display footprint");
|
|
fillBuffer(tiled, buffer.width, buffer.height);
|
|
for (const auto format : {0x8000000000000000ull, 0x8000000022000000ull}) {
|
|
buffer.pixelFormat = format;
|
|
const auto pixels = AgcDriver::ReadDisplayBuffer(buffer);
|
|
for (uint32_t y = 0; y < buffer.height; ++y) {
|
|
for (uint32_t x = 0; x < buffer.width; ++x) {
|
|
const auto offset = (static_cast<std::size_t>(y) * buffer.width + x) * 4;
|
|
const auto blue = format == 0x8000000000000000ull ? x : x ^ y;
|
|
const auto red = format == 0x8000000000000000ull ? x ^ y : x;
|
|
check(pixels[offset] == static_cast<std::byte>(blue & 255u) && pixels[offset + 1] == static_cast<std::byte>(y & 255u) && pixels[offset + 2] == static_cast<std::byte>(red & 255u) && pixels[offset + 3] == std::byte{255}, "detiled pixel or channel order mismatch");
|
|
}
|
|
}
|
|
}
|
|
expectFailure([&] { AgcDriver::DecodeDisplayBuffer(buffer, std::span(tiled).first(tiled.size() - 1)); });
|
|
buffer.pixelFormat = 0;
|
|
expectFailure([&] { AgcDriver::DisplayBufferSize(buffer); });
|
|
buffer.pixelFormat = 0x8000000000000000ull;
|
|
++buffer.address;
|
|
expectFailure([&] { AgcDriver::ReadDisplayBuffer(buffer); });
|
|
buffer.address = std::numeric_limits<uint64_t>::max() & ~uint64_t{65535};
|
|
expectFailure([&] { AgcDriver::DisplayBufferSize(buffer); });
|
|
const VideoOutBuffer registered{0, reinterpret_cast<uint64_t>(tiled.data()), 0};
|
|
BufferAttributeGroup group{};
|
|
sceVideoOutSetBufferAttribute2(&group.attribute, 0x8000000000000000ull, 0, 259, 137, VIDEO_OUT_BUFFER_ATTRIBUTE_OPTION_NONE, 0, 0);
|
|
group.occupied = true;
|
|
const auto plain = DescribeVideoOutBuffer(registered, group);
|
|
group.attribute.option = VIDEO_OUT_BUFFER_ATTRIBUTE_OPTION_STRICT_COLORIMETRY;
|
|
const auto strict = DescribeVideoOutBuffer(registered, group);
|
|
check(strict.address == plain.address && strict.pixelFormat == plain.pixelFormat && strict.width == plain.width && strict.height == plain.height && strict.tilingMode == plain.tilingMode && strict.pitchInPixel == plain.pitchInPixel, "the STRICT_COLORIMETRY option changed the described buffer");
|
|
for (const uint64_t option : {1ull, 32ull, 40ull}) {
|
|
group.attribute.option = option;
|
|
check(expectFailure([&] { DescribeVideoOutBuffer(registered, group); }).find("buffer option " + std::to_string(option)) != std::string::npos, "an unsupported buffer option was accepted");
|
|
}
|
|
VideoOutBuffer dccBuffer{0, reinterpret_cast<uint64_t>(tiled.data()), 0x7f0000};
|
|
sceVideoOutSetBufferAttribute2(&group.attribute, 0x8000000000000000ull, 0, 259, 137, VIDEO_OUT_BUFFER_ATTRIBUTE_OPTION_NONE, 0x208, 0x11223344);
|
|
group.category = VIDEO_OUT_BUFFER_ATTRIBUTE_CATEGORY_COMPRESSED;
|
|
const auto compressed = DescribeVideoOutBuffer(dccBuffer, group);
|
|
check(compressed.address == dccBuffer.dataAddress && compressed.dccAddress == 0x7f0000 && compressed.dccClearColor == 0x11223344, "DCC metadata or clear color was not described");
|
|
group.attribute.dcc_control = 0x100024;
|
|
check(DescribeVideoOutBuffer(dccBuffer, group).dccAddress == 0x7f0000, "independent 128-byte DCC blocks were rejected");
|
|
group.attribute.dcc_control = 0x209;
|
|
check(expectFailure([&] { DescribeVideoOutBuffer(dccBuffer, group); }).find("DCC control 0x209") != std::string::npos, "an unknown DCC control bit was accepted");
|
|
group.attribute.dcc_control = 0x208;
|
|
group.attribute.tiling_mode = 1;
|
|
check(expectFailure([&] { DescribeVideoOutBuffer(dccBuffer, group); }).find("linear") != std::string::npos, "a linear DCC buffer was accepted");
|
|
group.attribute.tiling_mode = 0;
|
|
dccBuffer.metadataAddress = 0;
|
|
check(expectFailure([&] { DescribeVideoOutBuffer(dccBuffer, group); }).find("without DCC metadata") != std::string::npos, "a compressed buffer without metadata was accepted");
|
|
dccBuffer.metadataAddress = 0x7f0000;
|
|
group.category = VIDEO_OUT_BUFFER_ATTRIBUTE_CATEGORY_UNCOMPRESSED;
|
|
check(expectFailure([&] { DescribeVideoOutBuffer(dccBuffer, group); }).find("DCC metadata 0x7f0000") != std::string::npos, "an uncompressed buffer with DCC metadata was accepted");
|
|
group.category = 2;
|
|
check(expectFailure([&] { DescribeVideoOutBuffer(dccBuffer, group); }).find("category 2") != std::string::npos, "an unknown buffer category was accepted");
|
|
}
|
|
|
|
void testOpenParam() {
|
|
using Words = std::array<std::uint32_t, 6>;
|
|
constexpr auto first = VIDEO_OUT_OPEN_PARAM_FIRST_WORD;
|
|
for (const Words& rejected : {
|
|
Words{24, 0, 0, 0, 0, 0},
|
|
Words{first, 2, 0, 0, 0, 0},
|
|
Words{first, 0, 0, 0x101, 0, 0},
|
|
Words{first, 1, 255, 0, 0, 0},
|
|
Words{first, 1, 768, 0, 0, 0},
|
|
Words{first, 0, 0, 1, 0, 0},
|
|
Words{first, 0, 0, 1, 0x2000, 0},
|
|
Words{first, 0, 0, 1, 0, 1},
|
|
}) {
|
|
expectFailure([&] { sceVideoOutOpen(255, 0, 0, rejected.data()); });
|
|
}
|
|
std::array<std::uint32_t, 4> zeroed{first, 0, 0, 0};
|
|
const auto zeroedHandle = sceVideoOutOpen(255, 0, 0, zeroed.data());
|
|
check(zeroedHandle >= 0, "open with a zeroed param failed");
|
|
sceVideoOutClose(zeroedHandle);
|
|
for (const Words& accepted : {Words{first, 1, 767, 0, 0, 0}, Words{first, 0, 0, 1, 1, 0}}) {
|
|
const auto handle = sceVideoOutOpen(255, 0, 0, accepted.data());
|
|
check(handle >= 0, "open with a valid service thread setting failed");
|
|
sceVideoOutClose(handle);
|
|
}
|
|
}
|
|
|
|
void testControls() {
|
|
testOpenParam();
|
|
const std::array<std::uint32_t, 22> openParam{VIDEO_OUT_OPEN_PARAM_FIRST_WORD, 1, 0x100, 1, 0x1FFF};
|
|
const auto handle = sceVideoOutOpen(255, 0, 0, openParam.data());
|
|
check(handle >= 0, "open with the highest priority on every CPU failed");
|
|
const auto cfg = VideoOutDriver::Get().GetConfig(handle);
|
|
check(sceVideoOutIsOutputSupported(handle, VIDEO_OUT_OUTPUT_MODE_DEFAULT, nullptr, nullptr, 0) == 1, "default output mode is unsupported");
|
|
for (const uint64_t unsupported : std::initializer_list<uint64_t>{VIDEO_OUT_OUTPUT_MODE_119_88HZ, 0xd000000aull}) {
|
|
check(sceVideoOutIsOutputSupported(handle, unsupported, nullptr, nullptr, 0) == 0, "unavailable output mode is supported");
|
|
check(sceVideoOutConfigureOutput(handle, unsupported, nullptr, nullptr, 0) == VIDEO_OUT_ERROR_UNAVAILABLE_OUTPUT_MODE, "unavailable output mode was configured");
|
|
}
|
|
check(sceVideoOutConfigureOutput(handle, VIDEO_OUT_OUTPUT_MODE_DEFAULT, nullptr, nullptr, 0) == 0, "default output mode was rejected");
|
|
for (int rate = 0; rate <= 2; ++rate) {
|
|
check(sceVideoOutSetFlipRate(handle, rate) == 0 && cfg->flipRate == rate, "flip rate was not applied");
|
|
}
|
|
expectFailure([&] { sceVideoOutSetFlipRate(handle, 3); });
|
|
VideoOutColorSettings settings{};
|
|
expectFailure([&] { sceVideoOutColorSettingsSetGamma(&settings, std::numeric_limits<float>::quiet_NaN()); });
|
|
KernelEqueue queue = 0;
|
|
check(sceKernelCreateEqueue(&queue, "VideoOut test") == 0, "event queue creation failed");
|
|
auto owner = EqueuePin_nid_postfix(queue);
|
|
check(sceVideoOutAddOutputModeEvent(queue, handle, nullptr) == 0, "output mode subscription failed");
|
|
KernelEvent event{};
|
|
check(owner->GetTriggeredEvents(&event, 1) == 1, "initial output mode event missing");
|
|
int64_t mode = 0;
|
|
check(sceVideoOutGetEventData(&event, &mode) == 0 && mode == VIDEO_OUT_OUTPUT_MODE_DEFAULT && sceVideoOutGetEventCount(&event) == 1, "initial output mode event encoding is wrong");
|
|
check(sceVideoOutAddVblankEvent(queue, handle, nullptr) == 0 && sceVideoOutAddVblankEvent(queue, handle, &settings) == 0, "vblank subscription failed");
|
|
{
|
|
std::lock_guard lock(cfg->mutex);
|
|
check(cfg->vblankEvents.size() == 1, "duplicate vblank subscription");
|
|
}
|
|
check(sceVideoOutWaitVblank(handle) == 0, "vblank wait failed");
|
|
check(owner->GetTriggeredEvents(&event, 1) == 1 && event.udata == &settings && sceVideoOutGetEventId(&event) == VIDEO_OUT_EVENT_VBLANK, "vblank event or updated user data missing");
|
|
sceVideoOutClose(handle);
|
|
check(owner->GetTriggeredEvents(&event, 1) == 0, "closed port retained pending events");
|
|
check(sceKernelDeleteEqueue(queue) == 0, "event queue deletion failed");
|
|
LibcRunShutdown_nid_postfix();
|
|
}
|
|
|
|
void testPresentation() {
|
|
const int handle = sceVideoOutOpen(255, 0, 0, nullptr);
|
|
auto cfg = VideoOutDriver::Get().GetConfig(handle);
|
|
std::vector<std::byte> allocation(6 * 65536 + 65535);
|
|
const auto storage = alignedBuffer(allocation);
|
|
fillBuffer(storage, 259, 137);
|
|
VideoOutBuffers buffer{storage.data(), nullptr, {nullptr, nullptr}};
|
|
VideoOutBufferAttribute2 attribute{};
|
|
sceVideoOutSetBufferAttribute2(&attribute, 0x8000000000000000ull, 0, 259, 137, 0, 0, 0);
|
|
auto invalid = attribute;
|
|
invalid.dcc_control = 1;
|
|
check(expectFailure([&] { sceVideoOutRegisterBuffers2(handle, 0, 0, &buffer, 1, &invalid, 0, nullptr); }).find("DCC") != std::string::npos, "DCC buffer was accepted");
|
|
check(!cfg->groups[0].occupied, "rejected registration changed the buffer group");
|
|
sceVideoOutRegisterBuffers2(handle, 0, 0, &buffer, 1, &attribute, 0, nullptr);
|
|
sceVideoOutSetFlipRate(handle, 2);
|
|
for (int index : {0, -2, -1, 0}) {
|
|
uint64_t target;
|
|
uint64_t previousVblank;
|
|
{
|
|
std::lock_guard lock(cfg->mutex);
|
|
target = cfg->flipStatus.count + 1;
|
|
previousVblank = cfg->lastFlipVblank;
|
|
}
|
|
sceVideoOutSubmitFlip(handle, index, 1, -123456789);
|
|
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 == target; }), "presentation did not complete");
|
|
if (cfg->failure) std::rethrow_exception(cfg->failure);
|
|
check(cfg->flipStatus.flipArg == -123456789 && cfg->flipStatus.currentBuffer == index && cfg->flipStatus.flipPendingNum == 0, "presentation status is wrong");
|
|
check(cfg->lastFlipVblank >= previousVblank + 3, "flip rate did not wait for its interval");
|
|
lock.unlock();
|
|
attribute.width = 65;
|
|
attribute.height = 33;
|
|
sceVideoOutSubmitChangeBufferAttribute2(handle, 0, &attribute, nullptr);
|
|
}
|
|
sceVideoOutUnregisterBuffers(handle, 0);
|
|
sceVideoOutClose(handle);
|
|
LibcRunShutdown_nid_postfix();
|
|
}
|
|
|
|
void testUnavailable() {
|
|
const auto message = expectFailure([] { sceVideoOutOpen(255, 0, 0, nullptr); });
|
|
check(expectFailure([] { sceVideoOutOpen(255, 0, 0, nullptr); }) == message, "a second open lost the presentation failure");
|
|
std::printf("presentation unavailable: %s\n", message.c_str());
|
|
LibcRunShutdown_nid_postfix();
|
|
}
|
|
|
|
void testCompressedPresentation() {
|
|
const int handle = sceVideoOutOpen(255, 0, 0, nullptr);
|
|
auto cfg = VideoOutDriver::Get().GetConfig(handle);
|
|
std::vector<std::byte> allocation(6 * 65536 + 65535);
|
|
const auto storage = alignedBuffer(allocation);
|
|
fillBuffer(storage, 259, 137);
|
|
std::vector<std::uint8_t> keys(storage.size() / 256);
|
|
VideoOutBuffers buffer{storage.data(), keys.data(), {nullptr, nullptr}};
|
|
VideoOutBufferAttribute2 attribute{};
|
|
sceVideoOutSetBufferAttribute2(&attribute, 0x8000000000000000ull, 0, 259, 137, 0, 0x208, 0xff102030);
|
|
sceVideoOutRegisterBuffers2(handle, 0, 0, &buffer, 1, &attribute, VIDEO_OUT_BUFFER_ATTRIBUTE_CATEGORY_COMPRESSED, nullptr);
|
|
int64_t argument = 2000;
|
|
for (std::uint8_t key : {0x00, 0xff, 0x20, 0xc0}) {
|
|
std::fill(keys.begin(), keys.end(), key);
|
|
uint64_t target;
|
|
{
|
|
std::lock_guard lock(cfg->mutex);
|
|
target = cfg->flipStatus.count + 1;
|
|
}
|
|
sceVideoOutSubmitFlip(handle, 0, 1, ++argument);
|
|
std::unique_lock lock(cfg->mutex);
|
|
check(cfg->vblankCond.wait_for(lock, std::chrono::seconds(15), [&] { return cfg->failure || cfg->flipStatus.count == target; }), "DCC presentation did not complete");
|
|
if (cfg->failure) std::rethrow_exception(cfg->failure);
|
|
check(cfg->flipStatus.flipArg == argument && cfg->flipStatus.flipPendingNum == 0, "DCC flip status is wrong");
|
|
}
|
|
sceVideoOutUnregisterBuffers(handle, 0);
|
|
sceVideoOutClose(handle);
|
|
LibcRunShutdown_nid_postfix();
|
|
}
|
|
|
|
// Flips of two buffers submitted without waiting for each other: every one completes in order
|
|
// (presentations trail on the GPU, APS5_FLIP_INFLIGHT), and the shutdown retires the ones in flight.
|
|
void testBackToBack() {
|
|
const int handle = sceVideoOutOpen(255, 0, 0, nullptr);
|
|
auto cfg = VideoOutDriver::Get().GetConfig(handle);
|
|
std::array<std::vector<std::byte>, 2> allocations{std::vector<std::byte>(6 * 65536 + 65535), std::vector<std::byte>(6 * 65536 + 65535)};
|
|
std::array<VideoOutBuffers, 2> buffers{};
|
|
for (std::size_t i = 0; i < buffers.size(); ++i) {
|
|
const auto storage = alignedBuffer(allocations[i]);
|
|
fillBuffer(storage, 259, 137);
|
|
buffers[i] = {storage.data(), nullptr, {nullptr, nullptr}};
|
|
}
|
|
VideoOutBufferAttribute2 attribute{};
|
|
sceVideoOutSetBufferAttribute2(&attribute, 0x8000000000000000ull, 0, 259, 137, 0, 0, 0);
|
|
sceVideoOutRegisterBuffers2(handle, 0, 0, buffers.data(), 2, &attribute, 0, nullptr);
|
|
int64_t argument = 1000;
|
|
for (int index : {0, 1, 0, 1}) sceVideoOutSubmitFlip(handle, index, 1, ++argument);
|
|
std::unique_lock lock(cfg->mutex);
|
|
check(cfg->vblankCond.wait_for(lock, std::chrono::seconds(15), [&] { return cfg->failure || cfg->flipStatus.count == 4; }), "back-to-back presentations did not complete");
|
|
if (cfg->failure) std::rethrow_exception(cfg->failure);
|
|
check(cfg->flipStatus.flipPendingNum == 0 && cfg->bufferPending[0] == 0 && cfg->bufferPending[1] == 0, "back-to-back flips left pending counts");
|
|
check(cfg->flipStatus.flipArg == argument && cfg->flipStatus.currentBuffer == 1, "back-to-back flip status is wrong");
|
|
lock.unlock();
|
|
sceVideoOutUnregisterBuffers(handle, 0);
|
|
sceVideoOutClose(handle);
|
|
LibcRunShutdown_nid_postfix();
|
|
}
|
|
|
|
std::uint32_t busyLoopResult(std::uint32_t iterations) {
|
|
std::uint32_t value = 1;
|
|
for (std::uint32_t i = 0; i < iterations; ++i) value = value * value + 1u;
|
|
return value;
|
|
}
|
|
|
|
struct BusyLoop {
|
|
volatile std::uint32_t* result;
|
|
volatile std::uint32_t* label;
|
|
std::vector<std::uint32_t> commands;
|
|
};
|
|
|
|
BusyLoop prepareBusyLoop(std::uint32_t iterations) {
|
|
alignas(256) static const std::array<std::uint32_t, 12> code{
|
|
0xbe850380, 0x7e040281, 0xd5690002, 0x00020502, 0x4a040481, 0x80058105,
|
|
0xbf070405, 0xbf85fffa, 0x7e020280, 0xe0701000, 0x80000201, 0xbf810000};
|
|
static Shader shader{};
|
|
shader.file_header = 0x34333231;
|
|
shader.version = 0x18;
|
|
shader.header_size = sizeof(Shader);
|
|
shader.shader_size = sizeof(code);
|
|
shader.code = code.data();
|
|
AgcDriverRegisterShader_nid_postfix(&shader);
|
|
std::int64_t physical = 0;
|
|
void* mapped = nullptr;
|
|
check(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, 65536, 65536, 0, &physical) == 0, "direct memory allocation failed");
|
|
check(sceKernelMapDirectMemory(&mapped, 65536, 0x33, 0, physical, 65536) == 0, "direct memory mapping failed");
|
|
auto* result = &static_cast<volatile std::uint32_t*>(mapped)[0];
|
|
auto* label = &static_cast<volatile std::uint32_t*>(mapped)[64];
|
|
*result = 0;
|
|
*label = 0;
|
|
const auto program = reinterpret_cast<std::uintptr_t>(code.data());
|
|
const auto output = reinterpret_cast<std::uintptr_t>(result);
|
|
const auto target = reinterpret_cast<std::uintptr_t>(label);
|
|
return {result, label, {
|
|
0xc0027600, 0x20c, static_cast<std::uint32_t>(program >> 8u), static_cast<std::uint32_t>(program >> 40u),
|
|
0xc0017600, 0x213, 5u << 1u,
|
|
0xc0037600, 0x207, 32, 1, 1,
|
|
0xc0057600, 0x240, static_cast<std::uint32_t>(output), static_cast<std::uint32_t>(output >> 32u) & 0xffffu, 4, 0x31016fac, iterations,
|
|
0xc0031500, 1, 1, 1, 0x8041,
|
|
0xc0064900, 0x514, 1u << 29u, static_cast<std::uint32_t>(target), static_cast<std::uint32_t>(target >> 32u), 1, 0, 0}};
|
|
}
|
|
|
|
void testReleaseVblank() {
|
|
const int handle = sceVideoOutOpen(255, 0, 0, nullptr);
|
|
auto cfg = VideoOutDriver::Get().GetConfig(handle);
|
|
std::vector<std::byte> allocation(6 * 65536 + 65535);
|
|
const auto storage = alignedBuffer(allocation);
|
|
fillBuffer(storage, 259, 137);
|
|
VideoOutBuffers buffer{storage.data(), nullptr, {nullptr, nullptr}};
|
|
VideoOutBufferAttribute2 attribute{};
|
|
sceVideoOutSetBufferAttribute2(&attribute, 0x8000000000000000ull, 0, 259, 137, 0, 0, 0);
|
|
sceVideoOutRegisterBuffers2(handle, 0, 0, &buffer, 1, &attribute, 0, nullptr);
|
|
check(sceVideoOutSetFlipRate(handle, 2) == 0, "flip rate was not applied");
|
|
auto busy = prepareBusyLoop(100000000u);
|
|
Packet packet{busy.commands.data(), static_cast<std::uint32_t>(busy.commands.size()), 0, {}};
|
|
check(sceAgcDriverSubmitDcb(&packet) == 0, "the GPU work was not accepted");
|
|
sceVideoOutSubmitFlip(handle, 0, 1, 1);
|
|
{
|
|
std::unique_lock lock(cfg->mutex);
|
|
const bool done = cfg->vblankCond.wait_for(lock, std::chrono::seconds(15), [&] { return cfg->failure || cfg->flipStatus.count == 1; });
|
|
if (cfg->failure) std::rethrow_exception(cfg->failure);
|
|
check(done, "the flip did not complete");
|
|
check(cfg->lastFlipVblank >= 3 && cfg->vblankStatus.count >= cfg->lastFlipVblank + 3, "the next flip is not paced from the vblank this flip was released at");
|
|
}
|
|
sceVideoOutUnregisterBuffers(handle, 0);
|
|
sceVideoOutClose(handle);
|
|
LibcRunShutdown_nid_postfix();
|
|
}
|
|
|
|
void testFlipAfterGpuWork() {
|
|
const int handle = sceVideoOutOpen(255, 0, 0, nullptr);
|
|
auto cfg = VideoOutDriver::Get().GetConfig(handle);
|
|
std::vector<std::byte> allocation(6 * 65536 + 65535);
|
|
const auto storage = alignedBuffer(allocation);
|
|
fillBuffer(storage, 259, 137);
|
|
VideoOutBuffers buffer{storage.data(), nullptr, {nullptr, nullptr}};
|
|
VideoOutBufferAttribute2 attribute{};
|
|
sceVideoOutSetBufferAttribute2(&attribute, 0x8000000000000000ull, 0, 259, 137, 0, 0, 0);
|
|
sceVideoOutRegisterBuffers2(handle, 0, 0, &buffer, 1, &attribute, 0, nullptr);
|
|
constexpr std::uint32_t iterations = 100000000u;
|
|
auto busy = prepareBusyLoop(iterations);
|
|
auto& result = *busy.result;
|
|
auto& label = *busy.label;
|
|
Packet packet{busy.commands.data(), static_cast<std::uint32_t>(busy.commands.size()), 0, {}};
|
|
check(sceVideoOutSubmitFlip(handle, 0, 1, 4) == 0, "the first flip was not accepted");
|
|
{
|
|
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 first flip did not complete");
|
|
}
|
|
std::atomic<std::int64_t> landed{0};
|
|
const auto start = std::chrono::steady_clock::now();
|
|
const auto since = [&] { return std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::steady_clock::now() - start).count(); };
|
|
std::thread poller([&] {
|
|
while (label != 1 && since() < 30000000) std::this_thread::yield();
|
|
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");
|
|
std::int64_t flipped = 0;
|
|
{
|
|
std::unique_lock lock(cfg->mutex);
|
|
const bool done = cfg->vblankCond.wait_for(lock, std::chrono::seconds(30), [&] { return cfg->failure || cfg->flipStatus.count == 2; });
|
|
flipped = since();
|
|
if (cfg->failure) std::rethrow_exception(cfg->failure);
|
|
check(done, "the flip did not complete");
|
|
}
|
|
poller.join();
|
|
AgcDriverWaitIdle_nid_postfix();
|
|
std::printf("GPU work landed at %.1f ms, flip completed at %.1f ms\n", landed.load() / 1000.0, flipped / 1000.0);
|
|
check(landed.load() >= 100000, "the GPU work did not land or was too short to order against the flip");
|
|
check(result == busyLoopResult(iterations), "the GPU work stored the wrong result");
|
|
check(flipped + 20000 >= landed.load(), "the flip completed before the GPU work submitted ahead of it");
|
|
sceVideoOutUnregisterBuffers(handle, 0);
|
|
sceVideoOutClose(handle);
|
|
LibcRunShutdown_nid_postfix();
|
|
}
|
|
|
|
void testOneDevice() {
|
|
const int handle = sceVideoOutOpen(255, 0, 0, nullptr);
|
|
auto cfg = VideoOutDriver::Get().GetConfig(handle);
|
|
std::vector<std::byte> allocation(6 * 65536 + 65535);
|
|
const auto storage = alignedBuffer(allocation);
|
|
fillBuffer(storage, 259, 137);
|
|
VideoOutBuffers buffer{storage.data(), nullptr, {nullptr, nullptr}};
|
|
VideoOutBufferAttribute2 attribute{};
|
|
sceVideoOutSetBufferAttribute2(&attribute, 0x8000000000000000ull, 0, 259, 137, 0, 0, 0);
|
|
sceVideoOutRegisterBuffers2(handle, 0, 0, &buffer, 1, &attribute, 0, nullptr);
|
|
constexpr std::uint32_t iterations = 1000u;
|
|
auto busy = prepareBusyLoop(iterations);
|
|
Packet packet{busy.commands.data(), static_cast<std::uint32_t>(busy.commands.size()), 0, {}};
|
|
check(sceAgcDriverSubmitDcb(&packet) == 0, "the GPU work was not accepted");
|
|
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(30);
|
|
while (*busy.label != 1 && std::chrono::steady_clock::now() < deadline) std::this_thread::yield();
|
|
check(*busy.label == 1 && *busy.result == busyLoopResult(iterations), "the GPU work did not run");
|
|
check(sceVideoOutSubmitFlip(handle, 0, 1, 1) == 0, "the flip was not accepted");
|
|
{
|
|
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 flip did not complete");
|
|
}
|
|
sceVideoOutUnregisterBuffers(handle, 0);
|
|
sceVideoOutClose(handle);
|
|
LibcRunShutdown_nid_postfix();
|
|
const AgcDriver::VulkanDevice next;
|
|
check(next.Serial() == 2, "the GPU work and the flip used more than one device");
|
|
}
|
|
|
|
}
|
|
|
|
int main(int argc, char** argv) {
|
|
try {
|
|
if (argc == 2 && std::string(argv[1]) == "decode") testDecode();
|
|
else if (argc == 2 && std::string(argv[1]) == "controls") testControls();
|
|
else if (argc == 2 && std::string(argv[1]) == "present") testPresentation();
|
|
else if (argc == 2 && std::string(argv[1]) == "backtoback") testBackToBack();
|
|
else if (argc == 2 && std::string(argv[1]) == "dcc") testCompressedPresentation();
|
|
else if (argc == 2 && std::string(argv[1]) == "pacing") testReleaseVblank();
|
|
else if (argc == 2 && std::string(argv[1]) == "aftergpu") testFlipAfterGpuWork();
|
|
else if (argc == 2 && std::string(argv[1]) == "unavailable") testUnavailable();
|
|
else if (argc == 2 && std::string(argv[1]) == "onedevice") testOneDevice();
|
|
else testLifetime(argc == 2 && std::string(argv[1]) == "reopen");
|
|
std::puts("VideoOut flip tests 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()); }
|
|
return 1;
|
|
}
|
|
}
|