feat(agc): add flip presentation and suspend synchronization (wip)

This commit is contained in:
boykopovar 2026-09-14 07:39:38 +03:00
parent b024001a4a
commit d31e35e1ab
23 changed files with 1421 additions and 336 deletions

View File

@ -15,7 +15,7 @@ set(SDL_X11 ON CACHE BOOL "" FORCE)
set(SDL_WAYLAND OFF CACHE BOOL "" FORCE)
set(SDL_OPENGL OFF CACHE BOOL "" FORCE)
set(SDL_OPENGLES OFF CACHE BOOL "" FORCE)
set(SDL_VULKAN OFF CACHE BOOL "" FORCE)
set(SDL_VULKAN ON CACHE BOOL "" FORCE)
set(SDL_JOYSTICK OFF CACHE BOOL "" FORCE)
set(SDL_HAPTIC OFF CACHE BOOL "" FORCE)
set(SDL_HIDAPI OFF CACHE BOOL "" FORCE)

View File

@ -1,6 +1,7 @@
#include "prx/libSceAgc/DcbState/include/Display.hpp"
#include "prx/libSceAgc/Command/include/Packet.hpp"
#include "prx/libSceAgcDriver/Execution/include/VideoOutput.hpp"
#include <cstdint>
#include <cstddef>
#include "SceTypes.hpp"
@ -9,13 +10,14 @@
extern "C" {
uint32_t* APS5_VABI sceAgcDcbSetFlip(CommandBuffer* buf, uint32_t video_out_handle, int32_t display_buffer_index, uint32_t flip_mode, int64_t flip_arg) {
(void)buf;
(void)video_out_handle;
(void)display_buffer_index;
(void)flip_mode;
(void)flip_arg;
NotImplemented_nid_no_patch(__func__);
return nullptr;
auto* packet = Agc::Command::Allocate(buf, AgcDriver::FlipPacketWords, __func__);
packet[0] = AgcDriver::FlipPacketHeader;
packet[1] = video_out_handle;
packet[2] = static_cast<std::uint32_t>(display_buffer_index);
packet[3] = flip_mode;
packet[4] = static_cast<std::uint32_t>(static_cast<std::uint64_t>(flip_arg));
packet[5] = static_cast<std::uint32_t>(static_cast<std::uint64_t>(flip_arg) >> 32u);
return packet;
}
uint32_t* APS5_VABI sceAgcDcbPrimeUtcl2(CommandBuffer* buf, const volatile void* address, uint32_t size_in_bytes) {

View File

@ -1,4 +1,5 @@
#include "prx/libSceAgc/Misc/include/Suspend.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver.hpp"
#include <cstdint>
#include <cstddef>
@ -8,8 +9,8 @@
extern "C" {
int APS5_VABI sceAgcSuspendPoint(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
AgcDriverSuspendPoint_nid_postfix();
return 0;
}
}

View File

@ -1,6 +1,7 @@
#include "prx/libSceAgc/Command/include/Packet.hpp"
#include "prx/libSceAgc/Command/include/Memory.hpp"
#include "prx/libSceAgc/Command/include/RegisterDefaults.hpp"
#include "prx/libc/include/Shutdown.hpp"
#include <algorithm>
#include <array>
@ -9,6 +10,8 @@
#include <string>
extern "C" std::uint32_t* APS5_VABI sceAgcDcbResetQueue(CommandBuffer* buf, std::uint32_t op, std::uint32_t state);
extern "C" std::uint32_t* APS5_VABI sceAgcDcbSetFlip(CommandBuffer* buf, std::uint32_t handle, std::int32_t index, std::uint32_t mode, std::int64_t argument);
extern "C" int APS5_VABI sceAgcSuspendPoint();
extern "C" std::uint32_t* APS5_VABI sceAgcDcbDrawIndexAuto(CommandBuffer* buf, std::uint32_t indexCount, std::uint64_t modifier);
extern "C" int APS5_VABI sceAgcWaitRegMemPatchReference(std::uint32_t* cmd, std::uint64_t reference);
extern "C" int APS5_VABI sceAgcGetDataPacketPayloadAddressUnk(std::uint32_t** addr, std::uint32_t* cmd, int type);
@ -68,6 +71,22 @@ void testPackets() {
check(exhausted.buffer.cursor_up == exhausted.words.data(), "failed allocation advanced cursor");
}
void testFlip() {
Storage storage;
storage.words.fill(0xdeadbeefu);
auto* packet = sceAgcDcbSetFlip(&storage.buffer, 0xfedcba98u, -2, 0x12345678u, -0x123456789abcdefLL);
const std::array<std::uint32_t, 6> expected{0xc004105cu, 0xfedcba98u, 0xfffffffeu, 0x12345678u, 0x76543211u, 0xfedcba98u};
check(packet == storage.words.data(), "flip returned wrong packet address");
check(std::equal(expected.begin(), expected.end(), packet), "flip packet lost argument bits");
check(storage.buffer.cursor_up == packet + 6 && packet[6] == 0xdeadbeefu, "flip packet overran allocation");
expectFailure([] { sceAgcDcbSetFlip(nullptr, 1, 0, 1, 0); });
Storage exhausted;
exhausted.buffer.cursor_down = exhausted.words.data() + 5;
expectFailure([&] { sceAgcDcbSetFlip(&exhausted.buffer, 1, 0, 1, 0); });
check(exhausted.buffer.cursor_up == exhausted.words.data(), "failed flip allocation advanced cursor");
check(sceAgcSuspendPoint() == 0, "empty suspend failed");
}
void testRegisters() {
Storage storage;
const std::array<ShaderRegister, 3> registers{{{0x10, 7}, {0x11, 8}, {0x20, 9}}};
@ -159,15 +178,19 @@ void testDefaults() {
int main() {
try {
testPackets();
testFlip();
testRegisters();
testRegisterRange();
testPacketPayloadAddress();
testMemory();
testDefaults();
LibcRunShutdown_nid_postfix();
std::puts("AGC command 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;
}
}

View File

@ -33,3 +33,9 @@ target_include_directories(agc_driver_recompiler_tests PRIVATE ${LIBS_INCLUDE_DI
target_link_libraries(agc_driver_recompiler_tests PRIVATE libSceAgcDriver libc)
set_target_properties(agc_driver_recompiler_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_driver_recompiler_tests)
add_executable(agc_driver_flip_tests EXCLUDE_FROM_ALL tests/Flip.cpp)
target_include_directories(agc_driver_flip_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(agc_driver_flip_tests PRIVATE libSceAgcDriver libc)
set_target_properties(agc_driver_flip_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_driver_flip_tests)

View File

@ -11,6 +11,7 @@ void Submit(const Packet* packet, std::uint32_t queue);
}
extern "C" void AgcDriverWaitIdle_nid_postfix();
extern "C" void AgcDriverSuspendPoint_nid_postfix();
extern "C" void AgcDriverRegisterShader_nid_postfix(const Shader* shader);
#endif

View File

@ -0,0 +1,26 @@
#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_PRESENTATION_HPP
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_PRESENTATION_HPP
#define VK_NO_PROTOTYPES
#include <vulkan/vulkan.h>
#include <cstdint>
#include <exception>
#include <span>
namespace AgcDriver {
struct PresentationWindow {
void* context;
std::span<const char* const> extensions;
VkSurfaceKHR (*createSurface)(void* context, VkInstance instance);
std::uint32_t width;
std::uint32_t height;
};
}
extern "C" void AgcDriverPresentClear_nid_postfix(const AgcDriver::PresentationWindow& window, bool opaque, void (*gpuReady)(void*), void* context);
extern "C" void AgcDriverReleaseWindow_nid_postfix(void* window);
extern "C" void AgcDriverReportFailure_nid_postfix(std::exception_ptr error);
#endif

View File

@ -0,0 +1,39 @@
#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_VIDEOOUTPUT_HPP
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_VIDEOOUTPUT_HPP
#include <cstdint>
#include <exception>
#include <memory>
namespace AgcDriver {
inline constexpr std::uint32_t FlipPacketHeader = 0xc004105cu;
inline constexpr std::uint32_t FlipPacketWords = 6;
struct FlipInfo {
std::uint32_t handle;
std::int32_t index;
std::uint32_t mode;
std::int64_t argument;
};
class IFlipRequest {
public:
virtual ~IFlipRequest() = default;
virtual void GpuReady() = 0;
virtual void Fail(std::exception_ptr error) noexcept = 0;
};
class IVideoOutput {
public:
virtual ~IVideoOutput() = default;
virtual std::shared_ptr<IFlipRequest> Reserve(const FlipInfo& info) = 0;
virtual void Fail(std::exception_ptr error) noexcept = 0;
};
}
extern "C" void AgcDriverRegisterVideoOutput_nid_postfix(std::uint32_t handle, const std::shared_ptr<AgcDriver::IVideoOutput>& output);
extern "C" void AgcDriverUnregisterVideoOutput_nid_postfix(std::uint32_t handle, const std::shared_ptr<AgcDriver::IVideoOutput>& output);
#endif

View File

@ -4,17 +4,22 @@
#define VK_NO_PROTOTYPES
#include <vulkan/vulkan.h>
#include "shader/recompilier/Recompiler.hpp"
#include "prx/libSceAgcDriver/Execution/include/Presentation.hpp"
#include <memory>
namespace AgcDriver {
class VulkanDevice {
public:
VulkanDevice();
explicit VulkanDevice(const PresentationWindow* window = nullptr);
~VulkanDevice();
VulkanDevice(const VulkanDevice&) = delete;
VulkanDevice& operator=(const VulkanDevice&) = delete;
ShaderRecompiler::SpirvTarget Target() const;
void WaitIdle();
void* Window() const;
std::uint64_t PresentClear(std::uint32_t width, std::uint32_t height, bool opaque);
void WaitPresented(std::uint64_t id);
void Dispatch(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z);
private:

View File

@ -1,5 +1,8 @@
#include "prx/libSceAgcDriver/Execution/include/Driver.hpp"
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
#include "prx/libSceAgcDriver/Execution/include/VideoOutput.hpp"
#include "prx/libc/include/Shutdown.hpp"
#include <bit>
#include <algorithm>
#include <array>
#include <condition_variable>
@ -90,6 +93,8 @@ struct Submission {
std::uint32_t queue;
std::vector<std::uint32_t> commands;
std::map<std::uint64_t, std::shared_ptr<const ShaderSnapshot>> shaders;
std::map<std::size_t, std::shared_ptr<IFlipRequest>> flips;
bool suspend = false;
};
std::uint32_t readRegister(const Registers& registers, std::uint32_t offset) {
@ -115,14 +120,28 @@ public:
}
~Driver() {
stop();
}
void Shutdown() {
stop();
CheckFailure();
}
private:
void stop() {
require(std::this_thread::get_id() != worker.get_id(), "worker cannot stop itself");
std::lock_guard shutdownLock(shutdownMutex);
{
std::lock_guard lock(mutex);
stopping = true;
}
changed.notify_all();
worker.join();
if (worker.joinable()) worker.join();
}
public:
void Submit(const Packet* packet, std::uint32_t queue) {
CheckFailure();
require(queue == 0 || (queue >= 0x20 && queue < 0x58), "unsupported compute queue");
@ -142,6 +161,18 @@ public:
rethrowFailure();
require(!stopping, "submission during shutdown");
require(accepted != std::numeric_limits<std::uint64_t>::max(), "submission serial overflow");
for (std::size_t cursor = 0; cursor < submission.commands.size();) {
const auto* words = submission.commands.data() + cursor;
if (words[0] == FlipPacketHeader) {
const auto output = outputs.find(words[1]);
require(output != outputs.end(), "flip references an unregistered video output");
const FlipInfo info{words[1], std::bit_cast<std::int32_t>(words[2]), words[3], std::bit_cast<std::int64_t>(static_cast<std::uint64_t>(words[4]) | (static_cast<std::uint64_t>(words[5]) << 32u))};
auto request = output->second->Reserve(info);
require(request != nullptr, "video output returned a null flip reservation");
submission.flips.emplace(cursor, std::move(request));
}
cursor += static_cast<std::size_t>((words[0] >> 16u) & 0x3fffu) + 2;
}
submission.shaders = shaders;
submission.serial = accepted + 1;
pending.push_back(std::move(submission));
@ -158,11 +189,86 @@ public:
rethrowFailure();
}
void SuspendPoint() {
require(std::this_thread::get_id() != worker.get_id(), "worker cannot suspend itself");
std::unique_lock lock(mutex);
rethrowFailure();
require(!stopping, "suspend during shutdown");
require(accepted != std::numeric_limits<std::uint64_t>::max(), "submission serial overflow");
Submission boundary{};
boundary.serial = accepted + 1;
boundary.suspend = true;
pending.push_back(std::move(boundary));
const auto target = ++accepted;
changed.notify_all();
changed.wait(lock, [&] { return failure != nullptr || completed >= target; });
rethrowFailure();
}
void RegisterVideoOutput(std::uint32_t handle, const std::shared_ptr<IVideoOutput>& output) {
require(output != nullptr, "null video output");
std::lock_guard lock(mutex);
rethrowFailure();
require(!stopping, "video output registration during shutdown");
require(outputs.emplace(handle, output).second, "video output already registered");
}
void UnregisterVideoOutput(std::uint32_t handle, const std::shared_ptr<IVideoOutput>& output) {
std::lock_guard lock(mutex);
const auto it = outputs.find(handle);
require(it != outputs.end() && it->second == output, "video output registration mismatch");
outputs.erase(it);
}
void CheckFailure() {
std::lock_guard lock(mutex);
rethrowFailure();
}
void ReportFailure(std::exception_ptr error) {
require(error != nullptr, "null asynchronous failure");
{
std::lock_guard lock(mutex);
if (!failure) failure = error;
for (const auto& [handle, output] : outputs) output->Fail(failure);
for (const auto& item : pending) {
for (const auto& [offset, flip] : item.flips) flip->Fail(failure);
}
pending.clear();
}
changed.notify_all();
}
void PresentClear(const PresentationWindow& window, bool opaque, void (*gpuReady)(void*), void* context) {
CheckFailure();
require(gpuReady != nullptr && context != nullptr, "missing GPU completion callback");
std::shared_ptr<VulkanDevice> presenting;
std::uint64_t id = 0;
try {
{
std::lock_guard lock(gpuMutex);
if (device == nullptr || device->Window() == nullptr) {
if (device) device->WaitIdle();
device = std::make_shared<VulkanDevice>(&window);
}
require(device->Window() == window.context, "presentation window does not match device surface");
presenting = device;
id = presenting->PresentClear(window.width, window.height, opaque);
}
gpuReady(context);
presenting->WaitPresented(id);
CheckFailure();
} catch (...) {
ReportFailure(std::current_exception());
throw;
}
}
void ReleaseWindow(void* window) {
std::lock_guard lock(gpuMutex);
if (device && device->Window() == window) device.reset();
}
void RegisterShader(const Shader* shader) {
CheckFailure();
checkRange(shader, sizeof(Shader), alignof(Shader));
@ -185,18 +291,29 @@ public:
private:
std::mutex mutex;
std::mutex shutdownMutex;
std::condition_variable changed;
std::deque<Submission> pending;
std::map<std::uint64_t, std::shared_ptr<const ShaderSnapshot>> shaders;
std::map<std::uint32_t, QueueState> queues;
std::unique_ptr<VulkanDevice> device;
std::map<std::uint32_t, std::shared_ptr<IVideoOutput>> outputs;
std::mutex gpuMutex;
std::shared_ptr<VulkanDevice> device;
std::uint64_t accepted = 0;
std::uint64_t completed = 0;
std::exception_ptr failure;
bool stopping = false;
bool resetGraphics = false;
std::thread worker;
Driver() : worker([this] { run(); }) {}
Driver() : worker([this] { run(); }) {
try {
LibcRegisterShutdown_nid_postfix([] { Driver::Get().Shutdown(); });
} catch (...) {
stop();
throw;
}
}
void rethrowFailure() const {
if (failure != nullptr) {
@ -210,6 +327,11 @@ private:
require((header & 0xc0000000u) == 0xc0000000u, "unsupported PM4 packet type");
const auto count = static_cast<std::size_t>((header >> 16u) & 0x3fffu) + 2;
require(count <= commands.size() - cursor, "truncated PM4 packet");
if (header == FlipPacketHeader) {
require(count == FlipPacketWords && queue == 0, "invalid flip packet size or queue");
cursor += count;
continue;
}
require((header & 0xffu) == 0, "PM4 header flags or internal extension are not implemented");
const auto packet = commands.subspan(cursor, count);
const auto opcode = (header >> 8u) & 0xffu;
@ -236,6 +358,7 @@ private:
}
void dispatch(QueueState& queue, std::span<const std::uint32_t> packet, const Submission& submission) {
std::lock_guard gpuLock(gpuMutex);
const auto address = (static_cast<std::uint64_t>(readRegister(queue.shader, 0x20c)) << 8u) | (static_cast<std::uint64_t>(readRegister(queue.shader, 0x20d) & 0xffu) << 40u);
auto it = submission.shaders.upper_bound(address);
require(it != submission.shaders.begin(), "compute program does not belong to a registered shader");
@ -253,7 +376,7 @@ private:
auto userConfigRegisters = registerValues(queue.userConfig);
const std::array<ShaderRecompiler::MemoryRegion, 2> memory{{{snapshot.codeAddress, std::as_bytes(std::span(snapshot.code))}, {snapshot.headerAddress, snapshot.header}}};
if (device == nullptr) {
device = std::make_unique<VulkanDevice>();
device = std::make_shared<VulkanDevice>();
}
const auto codeOffset = static_cast<std::size_t>((address - snapshot.codeAddress) / sizeof(std::uint32_t));
const ShaderRecompiler::RecompileRequest request{
@ -267,13 +390,32 @@ private:
}
void execute(const Submission& submission) {
if (submission.suspend) {
std::lock_guard gpuLock(gpuMutex);
if (device != nullptr) device->WaitIdle();
resetGraphics = true;
return;
}
if (submission.queue == 0 && resetGraphics) {
queues.erase(0);
resetGraphics = false;
}
auto& queue = queues[submission.queue];
for (std::size_t cursor = 0; cursor < submission.commands.size();) {
CheckFailure();
const auto header = submission.commands[cursor];
const auto count = static_cast<std::size_t>((header >> 16u) & 0x3fffu) + 2;
const auto packet = std::span(submission.commands).subspan(cursor, count);
switch ((header >> 8u) & 0xffu) {
case 0x10:
if (header == FlipPacketHeader) {
{
std::lock_guard gpuLock(gpuMutex);
if (device != nullptr) device->WaitIdle();
}
CheckFailure();
submission.flips.at(cursor)->GpuReady();
}
break;
case 0x69:
case 0x76:
@ -296,12 +438,14 @@ private:
}
void run() noexcept {
Submission submission;
try {
for (;;) {
Submission submission;
submission = Submission{};
{
std::unique_lock lock(mutex);
changed.wait(lock, [&] { return stopping || !pending.empty(); });
changed.wait(lock, [&] { return failure || stopping || !pending.empty(); });
rethrowFailure();
if (pending.empty()) {
break;
}
@ -311,23 +455,21 @@ private:
execute(submission);
{
std::lock_guard lock(mutex);
rethrowFailure();
completed = submission.serial;
}
changed.notify_all();
}
std::lock_guard gpuLock(gpuMutex);
device.reset();
} catch (...) {
const auto error = std::current_exception();
device.reset();
for (const auto& [offset, flip] : submission.flips) flip->Fail(error);
ReportFailure(error);
{
std::lock_guard lock(mutex);
failure = error;
if (failure == nullptr) {
std::terminate();
}
pending.clear();
std::lock_guard gpuLock(gpuMutex);
device.reset();
}
changed.notify_all();
}
}
};
@ -346,6 +488,30 @@ void RegisterShader(const Shader* shader) {
Driver::Get().RegisterShader(shader);
}
void SuspendPoint() {
Driver::Get().SuspendPoint();
}
void RegisterVideoOutput(std::uint32_t handle, const std::shared_ptr<IVideoOutput>& output) {
Driver::Get().RegisterVideoOutput(handle, output);
}
void UnregisterVideoOutput(std::uint32_t handle, const std::shared_ptr<IVideoOutput>& output) {
Driver::Get().UnregisterVideoOutput(handle, output);
}
void PresentClear(const PresentationWindow& window, bool opaque, void (*gpuReady)(void*), void* context) {
Driver::Get().PresentClear(window, opaque, gpuReady, context);
}
void ReleaseWindow(void* window) {
Driver::Get().ReleaseWindow(window);
}
void ReportFailure(std::exception_ptr error) {
Driver::Get().ReportFailure(error);
}
}
extern "C" void AgcDriverWaitIdle_nid_postfix() {
@ -355,3 +521,27 @@ extern "C" void AgcDriverWaitIdle_nid_postfix() {
extern "C" void AgcDriverRegisterShader_nid_postfix(const Shader* shader) {
AgcDriver::RegisterShader(shader);
}
extern "C" void AgcDriverSuspendPoint_nid_postfix() {
AgcDriver::SuspendPoint();
}
extern "C" void AgcDriverRegisterVideoOutput_nid_postfix(std::uint32_t handle, const std::shared_ptr<AgcDriver::IVideoOutput>& output) {
AgcDriver::RegisterVideoOutput(handle, output);
}
extern "C" void AgcDriverUnregisterVideoOutput_nid_postfix(std::uint32_t handle, const std::shared_ptr<AgcDriver::IVideoOutput>& output) {
AgcDriver::UnregisterVideoOutput(handle, output);
}
extern "C" void AgcDriverPresentClear_nid_postfix(const AgcDriver::PresentationWindow& window, bool opaque, void (*gpuReady)(void*), void* context) {
AgcDriver::PresentClear(window, opaque, gpuReady, context);
}
extern "C" void AgcDriverReleaseWindow_nid_postfix(void* window) {
AgcDriver::ReleaseWindow(window);
}
extern "C" void AgcDriverReportFailure_nid_postfix(std::exception_ptr error) {
AgcDriver::ReportFailure(error);
}

View File

@ -2,6 +2,8 @@
#include <SDL_loadso.h>
#include <SDL_error.h>
#include <array>
#include <algorithm>
#include <cstring>
#include <limits>
#include <stdexcept>
#include <string>
@ -16,6 +18,10 @@ void check(VkResult result, const char* operation) {
}
}
void require(bool condition, const char* reason) {
if (!condition) throw std::runtime_error(std::string("Vulkan presentation: ") + reason);
}
}
struct VulkanDevice::State {
@ -26,6 +32,18 @@ struct VulkanDevice::State {
VkDevice device = VK_NULL_HANDLE;
VkQueue queue = VK_NULL_HANDLE;
VkCommandPool pool = VK_NULL_HANDLE;
void* window = nullptr;
VkSurfaceKHR surface = VK_NULL_HANDLE;
VkSwapchainKHR swapchain = VK_NULL_HANDLE;
VkExtent2D extent{};
std::vector<VkImage> images;
VkFence acquireFence = VK_NULL_HANDLE;
VkFence renderFence = VK_NULL_HANDLE;
VkFence presentFence = VK_NULL_HANDLE;
VkSemaphore rendered = VK_NULL_HANDLE;
VkCommandBuffer clearCommands = VK_NULL_HANDLE;
std::uint64_t presentId = 0;
bool presentQueued = false;
VkPhysicalDeviceProperties properties{};
VkPhysicalDeviceSubgroupProperties subgroup{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES};
std::array<std::uint32_t, 1> capabilities{1};
@ -50,6 +68,18 @@ struct VulkanDevice::State {
~State() {
if (device != VK_NULL_HANDLE) {
const auto idle = reinterpret_cast<PFN_vkDeviceWaitIdle>(deviceProc(device, "vkDeviceWaitIdle"))(device);
if (idle != VK_SUCCESS && idle != VK_ERROR_DEVICE_LOST) std::terminate();
if (presentQueued && idle != VK_ERROR_DEVICE_LOST) {
const auto result = reinterpret_cast<PFN_vkWaitForFences>(deviceProc(device, "vkWaitForFences"))(device, 1, &presentFence, VK_TRUE, std::numeric_limits<std::uint64_t>::max());
if (result != VK_SUCCESS && result != VK_ERROR_DEVICE_LOST) std::terminate();
}
const auto destroyFence = reinterpret_cast<PFN_vkDestroyFence>(deviceProc(device, "vkDestroyFence"));
if (acquireFence) destroyFence(device, acquireFence, nullptr);
if (renderFence) destroyFence(device, renderFence, nullptr);
if (presentFence) destroyFence(device, presentFence, nullptr);
if (rendered) reinterpret_cast<PFN_vkDestroySemaphore>(deviceProc(device, "vkDestroySemaphore"))(device, rendered, nullptr);
if (swapchain) reinterpret_cast<PFN_vkDestroySwapchainKHR>(deviceProc(device, "vkDestroySwapchainKHR"))(device, swapchain, nullptr);
const auto destroyPool = reinterpret_cast<PFN_vkDestroyCommandPool>(deviceProc(device, "vkDestroyCommandPool"));
const auto destroyDevice = reinterpret_cast<PFN_vkDestroyDevice>(deviceProc(device, "vkDestroyDevice"));
if (pool != VK_NULL_HANDLE) {
@ -58,6 +88,7 @@ struct VulkanDevice::State {
destroyDevice(device, nullptr);
}
if (instance != VK_NULL_HANDLE) {
if (surface) reinterpret_cast<PFN_vkDestroySurfaceKHR>(instanceProc(instance, "vkDestroySurfaceKHR"))(instance, surface, nullptr);
reinterpret_cast<PFN_vkDestroyInstance>(instanceProc(instance, "vkDestroyInstance"))(instance, nullptr);
}
if (library != nullptr) {
@ -66,7 +97,7 @@ struct VulkanDevice::State {
}
};
VulkanDevice::VulkanDevice() : state(std::make_unique<State>()) {
VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_unique<State>()) {
#ifdef _WIN32
state->library = SDL_LoadObject("vulkan-1.dll");
#else
@ -84,7 +115,32 @@ VulkanDevice::VulkanDevice() : state(std::make_unique<State>()) {
application.apiVersion = VK_API_VERSION_1_1;
VkInstanceCreateInfo create{VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO};
create.pApplicationInfo = &application;
std::vector<const char*> instanceExtensions;
if (window != nullptr) {
require(window->context && window->createSurface && window->width && window->height, "invalid window descriptor");
instanceExtensions.assign(window->extensions.begin(), window->extensions.end());
instanceExtensions.push_back(VK_KHR_GET_SURFACE_CAPABILITIES_2_EXTENSION_NAME);
instanceExtensions.push_back(VK_EXT_SURFACE_MAINTENANCE_1_EXTENSION_NAME);
std::uint32_t availableCount = 0;
const auto enumerateExtensions = state->InstanceFunction<PFN_vkEnumerateInstanceExtensionProperties>("vkEnumerateInstanceExtensionProperties");
check(enumerateExtensions(nullptr, &availableCount, nullptr), "vkEnumerateInstanceExtensionProperties");
std::vector<VkExtensionProperties> available(availableCount);
check(enumerateExtensions(nullptr, &availableCount, available.data()), "vkEnumerateInstanceExtensionProperties");
for (const auto* name : instanceExtensions) {
require(name != nullptr, "null instance extension");
if (std::none_of(available.begin(), available.end(), [&](const auto& item) { return std::strcmp(item.extensionName, name) == 0; })) {
throw std::runtime_error(std::string("Vulkan presentation: required instance extension missing: ") + name);
}
}
create.enabledExtensionCount = static_cast<std::uint32_t>(instanceExtensions.size());
create.ppEnabledExtensionNames = instanceExtensions.data();
}
check(state->InstanceFunction<PFN_vkCreateInstance>("vkCreateInstance")(&create, nullptr, &state->instance), "vkCreateInstance");
if (window != nullptr) {
state->surface = window->createSurface(window->context, state->instance);
require(state->surface != VK_NULL_HANDLE, "window returned a null surface");
state->window = window->context;
}
state->deviceProc = state->InstanceFunction<PFN_vkGetDeviceProcAddr>("vkGetDeviceProcAddr");
const auto enumerate = state->InstanceFunction<PFN_vkEnumeratePhysicalDevices>("vkEnumeratePhysicalDevices");
std::uint32_t count = 0;
@ -94,12 +150,33 @@ VulkanDevice::VulkanDevice() : state(std::make_unique<State>()) {
devices.resize(count);
VkPhysicalDevice selected = VK_NULL_HANDLE;
std::uint32_t family = 0;
const std::array<const char*, 4> presentationExtensions{VK_KHR_SWAPCHAIN_EXTENSION_NAME, VK_KHR_PRESENT_ID_EXTENSION_NAME, VK_KHR_PRESENT_WAIT_EXTENSION_NAME, VK_EXT_SWAPCHAIN_MAINTENANCE_1_EXTENSION_NAME};
for (auto physical : devices) {
VkPhysicalDeviceProperties properties{};
state->InstanceFunction<PFN_vkGetPhysicalDeviceProperties>("vkGetPhysicalDeviceProperties")(physical, &properties);
if (properties.apiVersion < VK_API_VERSION_1_1) {
continue;
}
if (window != nullptr) {
std::uint32_t extensionCount = 0;
auto enumerateExtensions = state->InstanceFunction<PFN_vkEnumerateDeviceExtensionProperties>("vkEnumerateDeviceExtensionProperties");
check(enumerateExtensions(physical, nullptr, &extensionCount, nullptr), "vkEnumerateDeviceExtensionProperties");
std::vector<VkExtensionProperties> extensions(extensionCount);
check(enumerateExtensions(physical, nullptr, &extensionCount, extensions.data()), "vkEnumerateDeviceExtensionProperties");
const bool supported = std::all_of(presentationExtensions.begin(), presentationExtensions.end(), [&](const char* name) {
return std::any_of(extensions.begin(), extensions.end(), [&](const auto& item) { return std::strcmp(item.extensionName, name) == 0; });
});
if (!supported) continue;
VkPhysicalDevicePresentIdFeaturesKHR id{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PRESENT_ID_FEATURES_KHR};
VkPhysicalDevicePresentWaitFeaturesKHR wait{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PRESENT_WAIT_FEATURES_KHR};
VkPhysicalDeviceSwapchainMaintenance1FeaturesEXT maintenance{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SWAPCHAIN_MAINTENANCE_1_FEATURES_EXT};
VkPhysicalDeviceFeatures2 features{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2};
features.pNext = &id;
id.pNext = &wait;
wait.pNext = &maintenance;
state->InstanceFunction<PFN_vkGetPhysicalDeviceFeatures2>("vkGetPhysicalDeviceFeatures2")(physical, &features);
if (!id.presentId || !wait.presentWait || !maintenance.swapchainMaintenance1) continue;
}
std::uint32_t families = 0;
auto getFamilies = state->InstanceFunction<PFN_vkGetPhysicalDeviceQueueFamilyProperties>("vkGetPhysicalDeviceQueueFamilyProperties");
getFamilies(physical, &families, nullptr);
@ -107,6 +184,11 @@ VulkanDevice::VulkanDevice() : state(std::make_unique<State>()) {
getFamilies(physical, &families, queues.data());
for (std::uint32_t i = 0; i < families; ++i) {
if (queues[i].queueCount != 0 && (queues[i].queueFlags & (VK_QUEUE_GRAPHICS_BIT | VK_QUEUE_COMPUTE_BIT)) == (VK_QUEUE_GRAPHICS_BIT | VK_QUEUE_COMPUTE_BIT)) {
if (window != nullptr) {
VkBool32 supported = VK_FALSE;
check(state->InstanceFunction<PFN_vkGetPhysicalDeviceSurfaceSupportKHR>("vkGetPhysicalDeviceSurfaceSupportKHR")(physical, i, state->surface, &supported), "vkGetPhysicalDeviceSurfaceSupportKHR");
if (!supported) continue;
}
selected = physical;
family = i;
break;
@ -117,7 +199,7 @@ VulkanDevice::VulkanDevice() : state(std::make_unique<State>()) {
}
}
if (selected == VK_NULL_HANDLE) {
throw std::runtime_error("Vulkan: no Vulkan 1.1 graphics and compute queue");
throw std::runtime_error(window ? "Vulkan: no presentation device with graphics, compute, present_id, present_wait and swapchain_maintenance1" : "Vulkan: no Vulkan 1.1 graphics and compute queue");
}
VkPhysicalDeviceProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2};
properties.pNext = &state->subgroup;
@ -131,15 +213,152 @@ VulkanDevice::VulkanDevice() : state(std::make_unique<State>()) {
VkDeviceCreateInfo deviceInfo{VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO};
deviceInfo.queueCreateInfoCount = 1;
deviceInfo.pQueueCreateInfos = &queueInfo;
VkPhysicalDevicePresentIdFeaturesKHR idFeature{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PRESENT_ID_FEATURES_KHR, nullptr, VK_TRUE};
VkPhysicalDevicePresentWaitFeaturesKHR waitFeature{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PRESENT_WAIT_FEATURES_KHR, nullptr, VK_TRUE};
VkPhysicalDeviceSwapchainMaintenance1FeaturesEXT maintenanceFeature{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SWAPCHAIN_MAINTENANCE_1_FEATURES_EXT, nullptr, VK_TRUE};
if (window != nullptr) {
deviceInfo.enabledExtensionCount = static_cast<std::uint32_t>(presentationExtensions.size());
deviceInfo.ppEnabledExtensionNames = presentationExtensions.data();
deviceInfo.pNext = &idFeature;
idFeature.pNext = &waitFeature;
waitFeature.pNext = &maintenanceFeature;
}
check(state->InstanceFunction<PFN_vkCreateDevice>("vkCreateDevice")(selected, &deviceInfo, nullptr, &state->device), "vkCreateDevice");
state->DeviceFunction<PFN_vkGetDeviceQueue>("vkGetDeviceQueue")(state->device, family, 0, &state->queue);
VkCommandPoolCreateInfo poolInfo{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO};
poolInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
poolInfo.queueFamilyIndex = family;
check(state->DeviceFunction<PFN_vkCreateCommandPool>("vkCreateCommandPool")(state->device, &poolInfo, nullptr, &state->pool), "vkCreateCommandPool");
if (window != nullptr) {
VkSurfaceCapabilitiesKHR surface{};
check(state->InstanceFunction<PFN_vkGetPhysicalDeviceSurfaceCapabilitiesKHR>("vkGetPhysicalDeviceSurfaceCapabilitiesKHR")(selected, state->surface, &surface), "vkGetPhysicalDeviceSurfaceCapabilitiesKHR");
state->extent = {window->width, window->height};
require(surface.currentExtent.width == std::numeric_limits<std::uint32_t>::max() || (surface.currentExtent.width == window->width && surface.currentExtent.height == window->height), "window extent differs from requested output");
require(window->width >= surface.minImageExtent.width && window->width <= surface.maxImageExtent.width && window->height >= surface.minImageExtent.height && window->height <= surface.maxImageExtent.height, "unsupported output extent");
require((surface.supportedUsageFlags & VK_IMAGE_USAGE_TRANSFER_DST_BIT) != 0, "surface does not support transfer destination images");
require((surface.supportedCompositeAlpha & VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR) != 0, "opaque composition is unavailable");
require((surface.supportedTransforms & VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR) != 0, "identity surface transform is unavailable");
std::uint32_t formatCount = 0;
auto getFormats = state->InstanceFunction<PFN_vkGetPhysicalDeviceSurfaceFormatsKHR>("vkGetPhysicalDeviceSurfaceFormatsKHR");
check(getFormats(selected, state->surface, &formatCount, nullptr), "vkGetPhysicalDeviceSurfaceFormatsKHR");
std::vector<VkSurfaceFormatKHR> formats(formatCount);
check(getFormats(selected, state->surface, &formatCount, formats.data()), "vkGetPhysicalDeviceSurfaceFormatsKHR");
require(std::any_of(formats.begin(), formats.end(), [](const auto& format) { return format.format == VK_FORMAT_B8G8R8A8_UNORM && format.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR; }), "BGRA8 sRGB-nonlinear surface format is unavailable");
VkSwapchainCreateInfoKHR swapchain{VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR};
swapchain.surface = state->surface;
swapchain.minImageCount = surface.minImageCount;
swapchain.imageFormat = VK_FORMAT_B8G8R8A8_UNORM;
swapchain.imageColorSpace = VK_COLOR_SPACE_SRGB_NONLINEAR_KHR;
swapchain.imageExtent = state->extent;
swapchain.imageArrayLayers = 1;
swapchain.imageUsage = VK_IMAGE_USAGE_TRANSFER_DST_BIT;
swapchain.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE;
swapchain.preTransform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR;
swapchain.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
swapchain.presentMode = VK_PRESENT_MODE_FIFO_KHR;
swapchain.clipped = VK_FALSE;
check(state->DeviceFunction<PFN_vkCreateSwapchainKHR>("vkCreateSwapchainKHR")(state->device, &swapchain, nullptr, &state->swapchain), "vkCreateSwapchainKHR");
std::uint32_t imageCount = 0;
auto getImages = state->DeviceFunction<PFN_vkGetSwapchainImagesKHR>("vkGetSwapchainImagesKHR");
check(getImages(state->device, state->swapchain, &imageCount, nullptr), "vkGetSwapchainImagesKHR");
state->images.resize(imageCount);
check(getImages(state->device, state->swapchain, &imageCount, state->images.data()), "vkGetSwapchainImagesKHR");
VkFenceCreateInfo fence{VK_STRUCTURE_TYPE_FENCE_CREATE_INFO};
for (auto* destination : {&state->acquireFence, &state->renderFence, &state->presentFence}) {
check(state->DeviceFunction<PFN_vkCreateFence>("vkCreateFence")(state->device, &fence, nullptr, destination), "vkCreateFence");
}
VkSemaphoreCreateInfo semaphore{VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO};
check(state->DeviceFunction<PFN_vkCreateSemaphore>("vkCreateSemaphore")(state->device, &semaphore, nullptr, &state->rendered), "vkCreateSemaphore");
VkCommandBufferAllocateInfo allocation{VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO};
allocation.commandPool = state->pool;
allocation.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
allocation.commandBufferCount = 1;
check(state->DeviceFunction<PFN_vkAllocateCommandBuffers>("vkAllocateCommandBuffers")(state->device, &allocation, &state->clearCommands), "vkAllocateCommandBuffers");
}
}
VulkanDevice::~VulkanDevice() = default;
void VulkanDevice::WaitIdle() {
check(state->DeviceFunction<PFN_vkDeviceWaitIdle>("vkDeviceWaitIdle")(state->device), "vkDeviceWaitIdle");
}
void* VulkanDevice::Window() const {
return state->window;
}
std::uint64_t VulkanDevice::PresentClear(std::uint32_t width, std::uint32_t height, bool opaque) {
require(state->swapchain != VK_NULL_HANDLE, "device has no swapchain");
require(width == state->extent.width && height == state->extent.height, "output resize is not implemented");
require(!state->presentQueued, "previous presentation has not completed");
require(state->presentId != std::numeric_limits<std::uint64_t>::max(), "presentation ID overflow");
auto wait = state->DeviceFunction<PFN_vkWaitForFences>("vkWaitForFences");
auto reset = state->DeviceFunction<PFN_vkResetFences>("vkResetFences");
const std::array<VkFence, 3> fences{state->acquireFence, state->renderFence, state->presentFence};
check(reset(state->device, static_cast<std::uint32_t>(fences.size()), fences.data()), "vkResetFences");
std::uint32_t index = 0;
check(state->DeviceFunction<PFN_vkAcquireNextImageKHR>("vkAcquireNextImageKHR")(state->device, state->swapchain, 5'000'000'000ULL, VK_NULL_HANDLE, state->acquireFence, &index), "vkAcquireNextImageKHR");
check(wait(state->device, 1, &state->acquireFence, VK_TRUE, std::numeric_limits<std::uint64_t>::max()), "vkWaitForFences acquire");
require(index < state->images.size(), "acquired image index is out of range");
auto commands = state->clearCommands;
check(state->DeviceFunction<PFN_vkResetCommandBuffer>("vkResetCommandBuffer")(commands, 0), "vkResetCommandBuffer");
VkCommandBufferBeginInfo begin{VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO};
begin.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
check(state->DeviceFunction<PFN_vkBeginCommandBuffer>("vkBeginCommandBuffer")(commands, &begin), "vkBeginCommandBuffer");
VkImageMemoryBarrier barrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER};
barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.image = state->images[index];
barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
auto pipelineBarrier = state->DeviceFunction<PFN_vkCmdPipelineBarrier>("vkCmdPipelineBarrier");
pipelineBarrier(commands, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier);
VkClearColorValue clear{};
clear.float32[3] = opaque ? 1.0f : 0.0f;
state->DeviceFunction<PFN_vkCmdClearColorImage>("vkCmdClearColorImage")(commands, barrier.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &clear, 1, &barrier.subresourceRange);
barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
barrier.dstAccessMask = 0;
barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
barrier.newLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
pipelineBarrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier);
check(state->DeviceFunction<PFN_vkEndCommandBuffer>("vkEndCommandBuffer")(commands), "vkEndCommandBuffer");
VkSubmitInfo submit{VK_STRUCTURE_TYPE_SUBMIT_INFO};
submit.commandBufferCount = 1;
submit.pCommandBuffers = &commands;
submit.signalSemaphoreCount = 1;
submit.pSignalSemaphores = &state->rendered;
check(state->DeviceFunction<PFN_vkQueueSubmit>("vkQueueSubmit")(state->queue, 1, &submit, state->renderFence), "vkQueueSubmit clear");
check(wait(state->device, 1, &state->renderFence, VK_TRUE, std::numeric_limits<std::uint64_t>::max()), "vkWaitForFences clear");
++state->presentId;
VkSwapchainPresentFenceInfoEXT completion{VK_STRUCTURE_TYPE_SWAPCHAIN_PRESENT_FENCE_INFO_EXT};
completion.swapchainCount = 1;
completion.pFences = &state->presentFence;
VkPresentIdKHR id{VK_STRUCTURE_TYPE_PRESENT_ID_KHR};
id.pNext = &completion;
id.swapchainCount = 1;
id.pPresentIds = &state->presentId;
VkPresentInfoKHR present{VK_STRUCTURE_TYPE_PRESENT_INFO_KHR};
present.pNext = &id;
present.waitSemaphoreCount = 1;
present.pWaitSemaphores = &state->rendered;
present.swapchainCount = 1;
present.pSwapchains = &state->swapchain;
present.pImageIndices = &index;
const auto result = state->DeviceFunction<PFN_vkQueuePresentKHR>("vkQueuePresentKHR")(state->queue, &present);
state->presentQueued = result == VK_SUCCESS || result == VK_SUBOPTIMAL_KHR || result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_ERROR_SURFACE_LOST_KHR || result == VK_ERROR_FULL_SCREEN_EXCLUSIVE_MODE_LOST_EXT;
check(result, "vkQueuePresentKHR");
return state->presentId;
}
void VulkanDevice::WaitPresented(std::uint64_t id) {
require(state->presentQueued && id == state->presentId, "invalid presentation wait");
check(state->DeviceFunction<PFN_vkWaitForPresentKHR>("vkWaitForPresentKHR")(state->device, state->swapchain, id, 5'000'000'000ULL), "vkWaitForPresentKHR");
check(state->DeviceFunction<PFN_vkWaitForFences>("vkWaitForFences")(state->device, 1, &state->presentFence, VK_TRUE, std::numeric_limits<std::uint64_t>::max()), "vkWaitForFences presentation resources");
state->presentQueued = false;
}
ShaderRecompiler::SpirvTarget VulkanDevice::Target() const {
const auto& limits = state->properties.limits;
return {VK_API_VERSION_1_1, 0x00010300u, state->subgroup.subgroupSize, state->capabilities, {}, {limits.maxComputeWorkGroupSize[0], limits.maxComputeWorkGroupSize[1], limits.maxComputeWorkGroupSize[2]}, limits.maxComputeWorkGroupInvocations, limits.maxComputeSharedMemorySize};

View File

@ -0,0 +1,193 @@
#include "prx/libSceAgcDriver/Execution/include/Driver.hpp"
#include "prx/libc/include/Shutdown.hpp"
#include "prx/libSceAgcDriver/Execution/include/VideoOutput.hpp"
#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp"
#include "prx/libSceAgcDriver/Submit/include/Acb.hpp"
#include <array>
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <cstdio>
#include <future>
#include <mutex>
#include <stdexcept>
#include <string>
#include <thread>
#include <vector>
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");
}
struct State {
std::mutex mutex;
std::condition_variable changed;
bool block = false;
bool entered = false;
bool fail = false;
bool checkSelfWait = false;
std::atomic<int> alive = 0;
std::atomic<int> ready = 0;
std::atomic<int> failed = 0;
AgcDriver::FlipInfo last{};
};
class Request final : public AgcDriver::IFlipRequest {
public:
explicit Request(std::shared_ptr<State> value) : state(std::move(value)) { ++state->alive; }
~Request() override { --state->alive; }
void GpuReady() override {
std::unique_lock lock(state->mutex);
if (state->checkSelfWait) {
check(expectFailure([] { AgcDriverSuspendPoint_nid_postfix(); }).find("itself") != std::string::npos, "self suspend was not rejected");
check(expectFailure([] { AgcDriverWaitIdle_nid_postfix(); }).find("itself") != std::string::npos, "self wait was not rejected");
}
state->entered = true;
state->changed.notify_all();
state->changed.wait(lock, [&] { return !state->block; });
if (state->fail) throw std::runtime_error("intentional flip failure");
++state->ready;
}
void Fail(std::exception_ptr error) noexcept override {
if (!error) std::terminate();
++state->failed;
}
private:
std::shared_ptr<State> state;
};
class Output final : public AgcDriver::IVideoOutput {
public:
void Fail(std::exception_ptr error) noexcept override { if (!error) std::terminate(); }
std::shared_ptr<State> state = std::make_shared<State>();
std::shared_ptr<AgcDriver::IFlipRequest> Reserve(const AgcDriver::FlipInfo& info) override {
std::lock_guard lock(state->mutex);
state->last = info;
return std::make_shared<Request>(state);
}
};
void submitFlip(std::uint32_t handle = 7) {
std::array<std::uint32_t, 6> words{0xc004105c, handle, 0xfffffffeu, 1, 0x76543211u, 0xfedcba98u};
Packet packet{words.data(), static_cast<std::uint32_t>(words.size()), 0, {}};
check(sceAgcDriverSubmitDcb(&packet) == 0, "flip submission failed");
}
void testFlipAndBoundary() {
auto output = std::make_shared<Output>();
AgcDriverRegisterVideoOutput_nid_postfix(7, output);
expectFailure([&] { AgcDriverRegisterVideoOutput_nid_postfix(7, output); });
std::array<std::uint32_t, 6> words{0xc004105c, 7, 0xfffffffeu, 1, 0, 0};
Packet packet{words.data(), 6, 0, {}};
expectFailure([&] { sceAgcDriverSubmitAcb(0x20, &packet); });
words[0] = 0xc004105d;
expectFailure([&] { sceAgcDriverSubmitDcb(&packet); });
words[0] = 0xc004105c;
packet.dw_num = 5;
expectFailure([&] { sceAgcDriverSubmitDcb(&packet); });
expectFailure([] { submitFlip(8); });
std::array<std::uint32_t, 12> rollback{0xc004105c, 7, 0, 1, 0, 0, 0xc004105c, 8, 0, 1, 0, 0};
Packet rejected{rollback.data(), 12, 0, {}};
expectFailure([&] { sceAgcDriverSubmitDcb(&rejected); });
check(output->state->alive == 0 && output->state->ready == 0, "rejected submission retained or executed a reservation");
{
std::lock_guard lock(output->state->mutex);
output->state->block = true;
output->state->checkSelfWait = true;
}
submitFlip();
{
std::unique_lock lock(output->state->mutex);
check(output->state->changed.wait_for(lock, std::chrono::seconds(5), [&] { return output->state->entered; }), "worker did not reach flip");
check(output->state->last.argument == -0x123456789abcdefLL && output->state->last.index == -2, "decoded flip arguments changed");
}
auto boundary = std::async(std::launch::async, [] { AgcDriverSuspendPoint_nid_postfix(); });
check(boundary.wait_for(std::chrono::milliseconds(30)) == std::future_status::timeout, "suspend completed before preceding work");
AgcDriverUnregisterVideoOutput_nid_postfix(7, output);
auto replacement = std::make_shared<Output>();
AgcDriverRegisterVideoOutput_nid_postfix(7, replacement);
submitFlip();
{
std::lock_guard lock(output->state->mutex);
output->state->block = false;
}
output->state->changed.notify_all();
boundary.get();
AgcDriverWaitIdle_nid_postfix();
check(output->state->ready == 1 && replacement->state->ready == 1, "registration lifetime or FIFO was lost");
check(output->state->failed == 0, "successful request failed");
std::vector<std::thread> producers;
std::array<std::exception_ptr, 4> errors{};
for (std::size_t i = 0; i < errors.size(); ++i) {
producers.emplace_back([&, i] {
try {
for (int j = 0; j < 50; ++j) {
submitFlip();
if (j % 5 == 0) AgcDriverSuspendPoint_nid_postfix();
}
} catch (...) { errors[i] = std::current_exception(); }
});
}
for (auto& producer : producers) producer.join();
for (auto error : errors) if (error) std::rethrow_exception(error);
AgcDriverSuspendPoint_nid_postfix();
check(replacement->state->ready == 201, "concurrent submissions were lost");
AgcDriverUnregisterVideoOutput_nid_postfix(7, replacement);
}
void testFailure() {
auto output = std::make_shared<Output>();
output->state->fail = true;
AgcDriverRegisterVideoOutput_nid_postfix(7, output);
submitFlip();
std::array<std::string, 4> messages;
std::vector<std::thread> waiters;
for (auto& message : messages) waiters.emplace_back([&message] { message = expectFailure([] { AgcDriverSuspendPoint_nid_postfix(); }); });
for (auto& waiter : waiters) waiter.join();
for (auto& message : messages) check(message == "intentional flip failure", "asynchronous failure was lost");
check(expectFailure([] { AgcDriverWaitIdle_nid_postfix(); }) == messages[0], "idle lost flip failure");
check(expectFailure([] { submitFlip(); }) == messages[0], "submit lost flip failure");
check(output->state->ready == 0 && output->state->failed == 1, "failed flip was completed successfully");
AgcDriverUnregisterVideoOutput_nid_postfix(7, output);
}
void testReset(bool compute) {
std::array<std::uint32_t, 4> registers{0xc0027600, 0x20c, 1, 0};
Packet packet{registers.data(), 4, 0, {}};
if (compute) sceAgcDriverSubmitAcb(0x20, &packet);
else sceAgcDriverSubmitDcb(&packet);
AgcDriverSuspendPoint_nid_postfix();
std::array<std::uint32_t, 5> dispatch{0xc0031500, 1, 1, 1, 0x41};
packet = Packet{dispatch.data(), 5, 0, {}};
if (compute) sceAgcDriverSubmitAcb(0x20, &packet);
else sceAgcDriverSubmitDcb(&packet);
const auto message = expectFailure([] { AgcDriverWaitIdle_nid_postfix(); });
check(message.find(compute ? "registered" : "required shader register") != std::string::npos, "suspend reset wrong queue state");
}
}
int main(int argc, char** argv) {
try {
if (argc == 2) testReset(std::string(argv[1]) == "compute");
else { testFlipAndBoundary(); testFailure(); }
const auto shutdown = expectFailure([] { LibcRunShutdown_nid_postfix(); });
check(shutdown.find(argc == 2 ? (std::string(argv[1]) == "compute" ? "registered" : "required shader register") : "intentional flip failure") != std::string::npos, "shutdown lost worker failure");
std::puts("AGC flip and suspend 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;
}
}

View File

@ -1,4 +1,5 @@
#include "prx/libSceAgcDriver/Execution/include/Driver.hpp"
#include "prx/libc/include/Shutdown.hpp"
#include "prx/libSceAgcDriver/Submit/include/Acb.hpp"
#include <array>
#include <cstdio>
@ -30,11 +31,19 @@ int main() {
throw;
}
std::puts("Vulkan device initialized; real recompiler exception propagated from compute dispatch");
try {
LibcRunShutdown_nid_postfix();
throw std::runtime_error("shutdown lost recompiler failure");
} catch (const std::runtime_error& shutdown) {
if (std::string(shutdown.what()) != "ShaderRecompiler::Recompile not implemented") throw;
}
return 0;
}
throw std::runtime_error("dispatch unexpectedly completed without a recompiler");
} 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;
}
}

View File

@ -1,4 +1,5 @@
#include "prx/libSceAgcDriver/Execution/include/Driver.hpp"
#include "prx/libc/include/Shutdown.hpp"
#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp"
#include "prx/libSceAgcDriver/Submit/include/Acb.hpp"
#include "prx/libSceAgcDriver/Eq/include/Query.hpp"
@ -132,10 +133,13 @@ int main() {
testValidation();
testSubmissions();
testWorkerFailure();
check(expectFailure([] { LibcRunShutdown_nid_postfix(); }).find("required shader register") != std::string::npos, "shutdown lost worker failure");
std::puts("AGC driver submit 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;
}
}

View File

@ -8,6 +8,16 @@ add_library(libSceVideoOut SHARED EXCLUDE_FROM_ALL
target_include_directories(libSceVideoOut PRIVATE
${LIBS_INCLUDE_DIR}
${CMAKE_SOURCE_DIR}/3rdparty/SDL2/include
${CMAKE_SOURCE_DIR}/3rdparty/Vulkan-Headers/include
)
target_link_libraries(libSceVideoOut PRIVATE libc libkernel SDL2-static)
target_link_libraries(libSceVideoOut PRIVATE libSceAgcDriver libc libkernel SDL2-static)
configure_windows_unwind(libSceVideoOut)
foreach(runtimeLinkFlag IN ITEMS -static-libgcc -static-libstdc++ -static)
string(REPLACE "${runtimeLinkFlag}" "" CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS}")
endforeach()
add_executable(video_out_flip_tests EXCLUDE_FROM_ALL tests/Flip.cpp)
target_include_directories(video_out_flip_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/3rdparty/SDL2/include)
target_link_libraries(video_out_flip_tests PRIVATE libSceVideoOut libSceAgcDriver libc)
set_target_properties(video_out_flip_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(video_out_flip_tests)

View File

@ -8,6 +8,11 @@
#include <mutex>
#include <thread>
#include <vector>
#include <atomic>
#include <exception>
#include <memory>
#include <stdexcept>
#include "prx/libSceAgcDriver/Execution/include/VideoOutput.hpp"
#include "SDL.h"
#include "SceTypes.hpp"
@ -88,6 +93,7 @@ struct VideoOutConfig {
uint64_t generation = 0;
bool opened = false;
bool closing = false;
std::exception_ptr failure;
int flipRate = 0;
uint64_t outputMode = VIDEO_OUT_OUTPUT_MODE_DEFAULT;
float gamma = 1.0f;
@ -98,14 +104,43 @@ struct VideoOutConfig {
std::array<VideoOutBuffer, VIDEO_OUT_BUFFER_NUM_MAX> buffers{};
std::array<BufferAttributeGroup, VIDEO_OUT_BUFFER_ATTRIBUTE_NUM_MAX> groups{};
void Check() const {
if (failure) std::rethrow_exception(failure);
if (!opened || closing) throw std::runtime_error("VideoOut: port is closed");
}
};
struct FlipRequest {
VideoOutConfig* cfg = nullptr;
struct FlipQueue;
struct FlipRequest final : AgcDriver::IFlipRequest, std::enable_shared_from_this<FlipRequest> {
std::shared_ptr<VideoOutConfig> cfg;
std::shared_ptr<FlipQueue> queue;
uint64_t generation = 0;
int index = 0;
int flipMode = 0;
int64_t flipArg = 0;
uint32_t width = 0;
uint32_t height = 0;
VideoOutBuffer buffer;
BufferAttributeGroup group;
bool reserved = false;
bool ready = false;
bool gpuComplete = false;
bool terminal = false;
~FlipRequest() override;
void GpuReady() override;
void Fail(std::exception_ptr error) noexcept override;
};
struct FlipQueue {
std::mutex mutex;
std::condition_variable changed;
std::list<std::shared_ptr<FlipRequest>> requests;
std::atomic<std::size_t> reservations = 0;
std::exception_ptr failure;
bool stopping = false;
};
class VideoOutDriver {
@ -114,33 +149,34 @@ public:
VideoOutDriver();
~VideoOutDriver();
void Shutdown();
VideoOutDriver(const VideoOutDriver&) = delete;
VideoOutDriver& operator=(const VideoOutDriver&) = delete;
int Open(int busType);
bool Close(int handle);
VideoOutConfig* GetConfig(int handle);
std::shared_ptr<VideoOutConfig> GetConfig(int handle);
bool IsOpen(int handle);
void SubmitFlip(VideoOutConfig* cfg, int index, int flipMode, int64_t flipArg);
void SubmitFlip(int handle, int index, int flipMode, int64_t flipArg);
private:
bool close(int handle);
void presentLoop(std::stop_token token);
void vblankBegin();
void vblankEnd();
void processFlip(const FlipRequest& req);
void processFlip(FlipRequest& req);
void triggerEvents(VideoOutConfig& cfg, int eventKind, void* triggerData);
std::mutex mutex;
VideoOutConfig contexts[VIDEO_OUT_NUM_MAX];
std::mutex flipMutex;
std::condition_variable flipCond;
std::list<FlipRequest> flipQueue;
std::mutex shutdownMutex;
bool stopped = false;
std::array<std::shared_ptr<VideoOutConfig>, VIDEO_OUT_NUM_MAX> contexts;
std::array<std::shared_ptr<AgcDriver::IVideoOutput>, VIDEO_OUT_NUM_MAX> outputs;
std::shared_ptr<FlipQueue> flipQueue = std::make_shared<FlipQueue>();
SDL_Window* window = nullptr;
SDL_Surface* windowSurface = nullptr;
std::jthread presentThread;
};

View File

@ -25,35 +25,36 @@ void APS5_VABI sceVideoOutSetBufferAttribute2(VideoOutBufferAttribute2* attribut
}
int APS5_VABI sceVideoOutRegisterBuffers2(int handle, int setIndex, int bufferIndexStart, const VideoOutBuffers* buffers, int bufferNum, const VideoOutBufferAttribute2* attribute, int category, void* option) {
auto* cfg = VideoOutDriver::Get().GetConfig(handle);
auto cfg = VideoOutDriver::Get().GetConfig(handle);
if (cfg == nullptr) {
return VIDEO_OUT_ERROR_INVALID_HANDLE;
throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_HANDLE");
}
if (buffers == nullptr) {
return VIDEO_OUT_ERROR_INVALID_ADDRESS;
throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_ADDRESS");
}
if (attribute == nullptr) {
return VIDEO_OUT_ERROR_INVALID_OPTION;
throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_OPTION");
}
if (option != nullptr) {
return VIDEO_OUT_ERROR_INVALID_OPTION;
throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_OPTION");
}
if (setIndex < 0 || setIndex >= VIDEO_OUT_BUFFER_ATTRIBUTE_NUM_MAX || bufferIndexStart < 0 || bufferIndexStart >= VIDEO_OUT_BUFFER_NUM_MAX || bufferNum < 1 || bufferNum > VIDEO_OUT_BUFFER_NUM_MAX || bufferIndexStart + bufferNum > VIDEO_OUT_BUFFER_NUM_MAX) {
return VIDEO_OUT_ERROR_INVALID_VALUE;
throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_VALUE");
}
if (category != VIDEO_OUT_BUFFER_ATTRIBUTE_CATEGORY_UNCOMPRESSED && category != VIDEO_OUT_BUFFER_ATTRIBUTE_CATEGORY_COMPRESSED) {
return VIDEO_OUT_ERROR_INVALID_CATEGORY;
throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_CATEGORY");
}
std::unique_lock lock(cfg->mutex);
cfg->Check();
if (cfg->closing) {
return VIDEO_OUT_ERROR_INVALID_HANDLE;
throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_HANDLE");
}
if (cfg->groups[setIndex].occupied) {
return VIDEO_OUT_ERROR_INVALID_INDEX;
throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_INDEX");
}
for (int i = 0; i < bufferNum; i++) {
if (cfg->buffers[bufferIndexStart + i].Occupied()) {
return VIDEO_OUT_ERROR_SLOT_OCCUPIED;
throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_SLOT_OCCUPIED");
}
}
BufferAttributeGroup group{};
@ -74,41 +75,43 @@ int APS5_VABI sceVideoOutRegisterBuffers2(int handle, int setIndex, int bufferIn
}
int APS5_VABI sceVideoOutSubmitChangeBufferAttribute2(int handle, int setIndex, const VideoOutBufferAttribute2* attribute, void* option) {
auto* cfg = VideoOutDriver::Get().GetConfig(handle);
auto cfg = VideoOutDriver::Get().GetConfig(handle);
if (cfg == nullptr) {
return VIDEO_OUT_ERROR_INVALID_HANDLE;
throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_HANDLE");
}
if (attribute == nullptr) {
return VIDEO_OUT_ERROR_INVALID_OPTION;
throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_OPTION");
}
if (setIndex < 0 || setIndex >= VIDEO_OUT_BUFFER_ATTRIBUTE_NUM_MAX) {
return VIDEO_OUT_ERROR_INVALID_INDEX;
throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_INDEX");
}
if (option != nullptr) {
return VIDEO_OUT_ERROR_INVALID_OPTION;
throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_OPTION");
}
std::unique_lock lock(cfg->mutex);
cfg->Check();
if (cfg->closing || !cfg->groups[setIndex].occupied) {
return VIDEO_OUT_ERROR_INVALID_INDEX;
throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_INDEX");
}
cfg->groups[setIndex].attribute = *attribute;
return 0;
}
int APS5_VABI sceVideoOutUnregisterBuffers(int handle, int setIndex) {
auto* cfg = VideoOutDriver::Get().GetConfig(handle);
auto cfg = VideoOutDriver::Get().GetConfig(handle);
if (cfg == nullptr) {
return VIDEO_OUT_ERROR_INVALID_HANDLE;
throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_HANDLE");
}
if (setIndex < 0 || setIndex >= VIDEO_OUT_BUFFER_ATTRIBUTE_NUM_MAX) {
return VIDEO_OUT_ERROR_INVALID_INDEX;
throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_INDEX");
}
std::unique_lock lock(cfg->mutex);
cfg->Check();
if (cfg->closing) {
return VIDEO_OUT_ERROR_INVALID_HANDLE;
throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_HANDLE");
}
if (!cfg->groups[setIndex].occupied) {
return VIDEO_OUT_ERROR_INVALID_INDEX;
throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_INDEX");
}
cfg->groups[setIndex] = BufferAttributeGroup{};
for (auto& buf : cfg->buffers) {
@ -120,29 +123,30 @@ int APS5_VABI sceVideoOutUnregisterBuffers(int handle, int setIndex) {
}
int APS5_VABI sceVideoOutSubmitFlip(int handle, int index, int flipMode, int64_t flipArg) {
auto* cfg = VideoOutDriver::Get().GetConfig(handle);
auto cfg = VideoOutDriver::Get().GetConfig(handle);
if (cfg == nullptr) {
return VIDEO_OUT_ERROR_INVALID_HANDLE;
throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_HANDLE");
}
if (flipMode < VIDEO_OUT_FLIP_MODE_VSYNC || flipMode > VIDEO_OUT_FLIP_MODE_VSYNC_MULTI) {
return VIDEO_OUT_ERROR_INVALID_VALUE;
throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_VALUE");
}
if (flipMode != VIDEO_OUT_FLIP_MODE_VSYNC) {
throw std::runtime_error(std::string(__func__) + ": flip mode not implemented");
}
if (index < VIDEO_OUT_BUFFER_INDEX_BLACK || index >= VIDEO_OUT_BUFFER_NUM_MAX) {
return VIDEO_OUT_ERROR_INVALID_INDEX;
throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_INDEX");
}
std::unique_lock lock(cfg->mutex);
cfg->Check();
if (cfg->closing) {
return VIDEO_OUT_ERROR_INVALID_HANDLE;
throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_HANDLE");
}
const bool special = (index == VIDEO_OUT_BUFFER_INDEX_BLANK || index == VIDEO_OUT_BUFFER_INDEX_BLACK);
if (!special && !cfg->buffers[index].Occupied()) {
return VIDEO_OUT_ERROR_INVALID_INDEX;
throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_INDEX");
}
lock.unlock();
VideoOutDriver::Get().SubmitFlip(cfg, index, flipMode, flipArg);
VideoOutDriver::Get().SubmitFlip(handle, index, flipMode, flipArg);
return 0;
}

View File

@ -17,15 +17,15 @@ static std::vector<EventRegistration>* getEventList(VideoOutConfig& cfg, int16_t
}
static int registerVideoOutEvent(int handle, KernelEqueue eq, int16_t eventKind, void* udata) {
auto* cfg = VideoOutDriver::Get().GetConfig(handle);
auto cfg = VideoOutDriver::Get().GetConfig(handle);
if (cfg == nullptr) {
return VIDEO_OUT_ERROR_INVALID_HANDLE;
throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_HANDLE");
}
if (eq == 0) {
return VIDEO_OUT_ERROR_INVALID_EVENT_QUEUE;
throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_EVENT_QUEUE");
}
if (!EqueuePin_nid_postfix(eq)) {
return VIDEO_OUT_ERROR_INVALID_EVENT_QUEUE;
throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_EVENT_QUEUE");
}
KernelEqueueEvent event{};
event.event.ident = static_cast<uintptr_t>(eventKind);
@ -34,6 +34,7 @@ static int registerVideoOutEvent(int handle, KernelEqueue eq, int16_t eventKind,
event.event.udata = udata;
if (eventKind == VIDEO_OUT_EVENT_SET_MODE) {
std::unique_lock lock(cfg->mutex);
cfg->Check();
event.triggered = true;
event.event.fflags = 1;
event.event.data = static_cast<intptr_t>(cfg->outputMode);
@ -68,9 +69,10 @@ static int registerVideoOutEvent(int handle, KernelEqueue eq, int16_t eventKind,
};
const int result = EqueueAddEvent_nid_postfix(eq, event);
if (result == EQUEUE_ERROR_EBADF) {
return VIDEO_OUT_ERROR_INVALID_EVENT_QUEUE;
throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_EVENT_QUEUE");
}
std::unique_lock lock(cfg->mutex);
cfg->Check();
EventRegistration reg;
reg.eq = eq;
reg.generation = cfg->generation;
@ -80,20 +82,21 @@ static int registerVideoOutEvent(int handle, KernelEqueue eq, int16_t eventKind,
static int deleteVideoOutEvent(int handle, KernelEqueue eq, int16_t eventKind) {
if (!VideoOutDriver::Get().IsOpen(handle)) {
return VIDEO_OUT_ERROR_INVALID_HANDLE;
throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_HANDLE");
}
if (eq == 0 || !EqueuePin_nid_postfix(eq)) {
return VIDEO_OUT_ERROR_INVALID_EVENT_QUEUE;
throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_EVENT_QUEUE");
}
auto* cfg = VideoOutDriver::Get().GetConfig(handle);
auto cfg = VideoOutDriver::Get().GetConfig(handle);
if (cfg != nullptr) {
std::unique_lock lock(cfg->mutex);
cfg->Check();
auto* events = getEventList(*cfg, eventKind);
events->erase(std::remove_if(events->begin(), events->end(), [eq](const EventRegistration& r) { return r.eq == eq; }), events->end());
}
const int result = EqueueDeleteEvent_nid_postfix(eq, static_cast<uintptr_t>(eventKind), EVFILT_VIDEO_OUT);
if (result == EQUEUE_ERROR_EBADF) {
return VIDEO_OUT_ERROR_INVALID_EVENT_QUEUE;
throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_EVENT_QUEUE");
}
return (result == EQUEUE_ERROR_ENOENT) ? 0 : result;
}
@ -130,24 +133,24 @@ int APS5_VABI sceVideoOutDeletePreVblankStartEvent(KernelEqueue eq, int handle)
int APS5_VABI sceVideoOutGetEventId(const KernelEvent* ev) {
if (ev == nullptr) {
return VIDEO_OUT_ERROR_INVALID_ADDRESS;
throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_ADDRESS");
}
if (ev->filter != EVFILT_VIDEO_OUT) {
return VIDEO_OUT_ERROR_INVALID_EVENT;
throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_EVENT");
}
const int ident = static_cast<int>(ev->ident);
if (ident != VIDEO_OUT_EVENT_FLIP && ident != VIDEO_OUT_EVENT_VBLANK && ident != VIDEO_OUT_EVENT_PRE_VBLANK_START && ident != VIDEO_OUT_EVENT_SET_MODE) {
return VIDEO_OUT_ERROR_INVALID_EVENT;
throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_EVENT");
}
return ident;
}
int APS5_VABI sceVideoOutGetEventData(const KernelEvent* ev, int64_t* data) {
if (ev == nullptr || data == nullptr) {
return VIDEO_OUT_ERROR_INVALID_ADDRESS;
throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_ADDRESS");
}
if (ev->filter != EVFILT_VIDEO_OUT) {
return VIDEO_OUT_ERROR_INVALID_EVENT;
throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_EVENT");
}
uint64_t eventData = static_cast<uint64_t>(ev->data) >> 16u;
if (ev->ident == static_cast<uintptr_t>(VIDEO_OUT_EVENT_FLIP) && (static_cast<uint64_t>(ev->data) & 0x8000000000000000ULL) != 0) {
@ -159,10 +162,10 @@ int APS5_VABI sceVideoOutGetEventData(const KernelEvent* ev, int64_t* data) {
int APS5_VABI sceVideoOutGetEventCount(const KernelEvent* ev) {
if (ev == nullptr) {
return VIDEO_OUT_ERROR_INVALID_ADDRESS;
throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_ADDRESS");
}
if (ev->filter != EVFILT_VIDEO_OUT) {
return VIDEO_OUT_ERROR_INVALID_EVENT;
throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_EVENT");
}
return static_cast<int>((static_cast<uint64_t>(ev->data) >> 12u) & 0xfu);
}

View File

@ -8,20 +8,20 @@
static int validateOutputConfig(int handle, uint64_t mode, const VideoOutOutputOptions* options, void* reservedPtr, uint64_t reserved) {
if (!VideoOutDriver::Get().IsOpen(handle)) {
return VIDEO_OUT_ERROR_INVALID_HANDLE;
throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_HANDLE");
}
if (reservedPtr != nullptr || reserved != 0) {
return VIDEO_OUT_ERROR_INVALID_VALUE;
throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_VALUE");
}
if (options != nullptr) {
for (auto v : options->internalData) {
if (v != 0) {
return VIDEO_OUT_ERROR_INVALID_OPTION;
throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_OPTION");
}
}
}
if (mode != VIDEO_OUT_OUTPUT_MODE_DEFAULT && mode != VIDEO_OUT_OUTPUT_MODE_119_88HZ) {
return VIDEO_OUT_ERROR_UNSUPPORTED_OUTPUT_MODE;
throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_UNSUPPORTED_OUTPUT_MODE");
}
return 0;
}
@ -33,17 +33,17 @@ int APS5_VABI sceVideoOutOpen(int userId, int busType, int index, const void* pa
throw std::runtime_error(std::string(__func__) + ": param not implemented");
}
if (userId != 255 && userId != 0) {
return VIDEO_OUT_ERROR_INVALID_VALUE;
throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_VALUE");
}
if (busType != VIDEO_OUT_BUS_TYPE_MAIN && busType != VIDEO_OUT_BUS_TYPE_OVERLAY && busType != VIDEO_OUT_BUS_TYPE_SUB) {
return VIDEO_OUT_ERROR_INVALID_VALUE;
throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_VALUE");
}
if (index != 0) {
return VIDEO_OUT_ERROR_INVALID_VALUE;
throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_VALUE");
}
const int handle = VideoOutDriver::Get().Open(busType);
if (handle < 0) {
return VIDEO_OUT_ERROR_RESOURCE_BUSY;
throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_RESOURCE_BUSY");
}
return handle;
}
@ -53,51 +53,54 @@ int APS5_VABI sceVideoOutClose(int handle) {
}
int APS5_VABI sceVideoOutSetFlipRate(int handle, int rate) {
auto* cfg = VideoOutDriver::Get().GetConfig(handle);
auto cfg = VideoOutDriver::Get().GetConfig(handle);
if (cfg == nullptr) {
return VIDEO_OUT_ERROR_INVALID_HANDLE;
throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_HANDLE");
}
if (rate < 0 || rate > 2) {
return VIDEO_OUT_ERROR_INVALID_VALUE;
throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_VALUE");
}
throw std::runtime_error(std::string(__func__) + " not implemented");
}
int APS5_VABI sceVideoOutGetFlipStatus(int handle, VideoOutFlipStatus* status) {
if (status == nullptr) {
return VIDEO_OUT_ERROR_INVALID_ADDRESS;
throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_ADDRESS");
}
auto* cfg = VideoOutDriver::Get().GetConfig(handle);
auto cfg = VideoOutDriver::Get().GetConfig(handle);
if (cfg == nullptr) {
return VIDEO_OUT_ERROR_INVALID_HANDLE;
throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_HANDLE");
}
std::unique_lock lock(cfg->mutex);
cfg->Check();
*status = cfg->flipStatus;
return 0;
}
int APS5_VABI sceVideoOutGetVblankStatus(int handle, VideoOutVblankStatus* status) {
if (status == nullptr) {
return VIDEO_OUT_ERROR_INVALID_ADDRESS;
throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_ADDRESS");
}
auto* cfg = VideoOutDriver::Get().GetConfig(handle);
auto cfg = VideoOutDriver::Get().GetConfig(handle);
if (cfg == nullptr) {
return VIDEO_OUT_ERROR_INVALID_HANDLE;
throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_HANDLE");
}
std::unique_lock lock(cfg->mutex);
cfg->Check();
*status = cfg->vblankStatus;
return 0;
}
int APS5_VABI sceVideoOutGetOutputStatus(int handle, VideoOutOutputStatus* status) {
if (status == nullptr) {
return VIDEO_OUT_ERROR_INVALID_ADDRESS;
throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_ADDRESS");
}
auto* cfg = VideoOutDriver::Get().GetConfig(handle);
auto cfg = VideoOutDriver::Get().GetConfig(handle);
if (cfg == nullptr) {
return VIDEO_OUT_ERROR_INVALID_HANDLE;
throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_HANDLE");
}
std::unique_lock lock(cfg->mutex);
cfg->Check();
status->resolution = (cfg->width >= 3840 || cfg->height >= 2160) ? 2u : 1u;
status->dynamicRange = 1;
status->refreshRate = (cfg->outputMode == VIDEO_OUT_OUTPUT_MODE_119_88HZ) ? VIDEO_OUT_REFRESH_RATE_119_88HZ : VIDEO_OUT_REFRESH_RATE_59_94HZ;
@ -109,30 +112,34 @@ int APS5_VABI sceVideoOutGetOutputStatus(int handle, VideoOutOutputStatus* statu
}
int APS5_VABI sceVideoOutIsFlipPending(int handle) {
auto* cfg = VideoOutDriver::Get().GetConfig(handle);
auto cfg = VideoOutDriver::Get().GetConfig(handle);
if (cfg == nullptr) {
return VIDEO_OUT_ERROR_INVALID_HANDLE;
throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_HANDLE");
}
std::unique_lock lock(cfg->mutex);
cfg->Check();
return cfg->flipStatus.flipPendingNum;
}
int APS5_VABI sceVideoOutWaitVblank(int handle) {
auto* cfg = VideoOutDriver::Get().GetConfig(handle);
auto cfg = VideoOutDriver::Get().GetConfig(handle);
if (cfg == nullptr) {
return VIDEO_OUT_ERROR_INVALID_HANDLE;
throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_HANDLE");
}
std::unique_lock lock(cfg->mutex);
cfg->Check();
const uint64_t count = cfg->vblankStatus.count;
while (cfg->opened && cfg->vblankStatus.count == count) {
while (cfg->opened && !cfg->failure && cfg->vblankStatus.count == count) {
cfg->vblankCond.wait(lock);
}
if (cfg->failure) std::rethrow_exception(cfg->failure);
if (!cfg->opened || cfg->closing) throw std::runtime_error("sceVideoOutWaitVblank: port closed during wait");
return 0;
}
int APS5_VABI sceVideoOutInitializeOutputOptions(VideoOutOutputOptions* options) {
if (options == nullptr) {
return VIDEO_OUT_ERROR_INVALID_ADDRESS;
throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_ADDRESS");
}
std::memset(options, 0, sizeof(VideoOutOutputOptions));
return 0;
@ -152,13 +159,14 @@ int APS5_VABI sceVideoOutConfigureOutput(int handle, uint64_t mode, const VideoO
return supported;
}
if (supported == 0 && mode == VIDEO_OUT_OUTPUT_MODE_119_88HZ) {
return VIDEO_OUT_ERROR_UNAVAILABLE_OUTPUT_MODE;
throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_UNAVAILABLE_OUTPUT_MODE");
}
auto* cfg = VideoOutDriver::Get().GetConfig(handle);
auto cfg = VideoOutDriver::Get().GetConfig(handle);
if (cfg == nullptr) {
return VIDEO_OUT_ERROR_INVALID_HANDLE;
throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_HANDLE");
}
std::unique_lock lock(cfg->mutex);
cfg->Check();
cfg->outputMode = mode;
return 0;
}
@ -167,14 +175,14 @@ int APS5_VABI sceVideoOutSetWindowModeMargins(int handle, int top, int bottom) {
(void)top;
(void)bottom;
if (!VideoOutDriver::Get().IsOpen(handle)) {
return VIDEO_OUT_ERROR_INVALID_HANDLE;
throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_HANDLE");
}
throw std::runtime_error(std::string(__func__) + " not implemented");
}
int APS5_VABI sceVideoOutLatencyControlWaitBeforeInput(int handle) {
if (!VideoOutDriver::Get().IsOpen(handle)) {
return VIDEO_OUT_ERROR_INVALID_HANDLE;
throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_HANDLE");
}
throw std::runtime_error(std::string(__func__) + " not implemented");
}
@ -182,17 +190,17 @@ int APS5_VABI sceVideoOutLatencyControlWaitBeforeInput(int handle) {
int APS5_VABI sceVideoOutLatencyMeasureSetStartPoint(int handle, uint32_t point) {
(void)point;
if (!VideoOutDriver::Get().IsOpen(handle)) {
return VIDEO_OUT_ERROR_INVALID_HANDLE;
throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_HANDLE");
}
throw std::runtime_error(std::string(__func__) + " not implemented");
}
int APS5_VABI sceVideoOutColorSettingsSetGamma(VideoOutColorSettings* settings, float gamma) {
if (settings == nullptr) {
return VIDEO_OUT_ERROR_INVALID_ADDRESS;
throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_ADDRESS");
}
if (gamma < 0.1f || gamma > 2.0f) {
return VIDEO_OUT_ERROR_INVALID_VALUE;
throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_VALUE");
}
settings->gamma = gamma;
return 0;
@ -200,11 +208,11 @@ int APS5_VABI sceVideoOutColorSettingsSetGamma(VideoOutColorSettings* settings,
int APS5_VABI sceVideoOutAdjustColor(int handle, const VideoOutColorSettings* settings) {
if (settings == nullptr) {
return VIDEO_OUT_ERROR_INVALID_ADDRESS;
throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_ADDRESS");
}
auto* cfg = VideoOutDriver::Get().GetConfig(handle);
auto cfg = VideoOutDriver::Get().GetConfig(handle);
if (cfg == nullptr) {
return VIDEO_OUT_ERROR_INVALID_HANDLE;
throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_HANDLE");
}
throw std::runtime_error(std::string(__func__) + " not implemented");
}

View File

@ -1,13 +1,125 @@
#include <bit>
#include <chrono>
#include <limits>
#include <stdexcept>
#include "SDL.h"
#include "SDL_vulkan.h"
#include "prx/libkernel/Equeue/Equeue.hpp"
#include "prx/libkernel/Time/include/Time.hpp"
#include "prx/libSceVideoOut/include/VideoOutDriver.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver.hpp"
#include "prx/libSceAgcDriver/Execution/include/Presentation.hpp"
#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp"
#include "prx/libc/include/Shutdown.hpp"
static constexpr uint32_t VBLANK_FREQ_DEFAULT = 60;
static constexpr uint32_t VBLANK_FREQ_119 = 120;
namespace {
void require(bool condition, const char* reason) {
if (!condition) throw std::runtime_error(std::string("VideoOut: ") + reason);
}
void checkConfig(const VideoOutConfig& cfg) {
cfg.Check();
}
class VideoOutput final : public AgcDriver::IVideoOutput {
public:
VideoOutput(std::shared_ptr<VideoOutConfig> config, std::shared_ptr<FlipQueue> requests) : cfg(std::move(config)), queue(std::move(requests)) {}
std::shared_ptr<AgcDriver::IFlipRequest> Reserve(const AgcDriver::FlipInfo& info) override {
require(info.mode == VIDEO_OUT_FLIP_MODE_VSYNC, "unsupported flip mode");
require(info.index >= VIDEO_OUT_BUFFER_INDEX_BLACK && info.index < VIDEO_OUT_BUFFER_NUM_MAX, "invalid flip index");
auto request = std::make_shared<FlipRequest>();
request->cfg = cfg;
request->queue = queue;
request->index = info.index;
request->flipMode = static_cast<int>(info.mode);
request->flipArg = info.argument;
std::lock_guard queueLock(queue->mutex);
if (queue->failure) std::rethrow_exception(queue->failure);
require(!queue->stopping, "flip during shutdown");
std::lock_guard lock(cfg->mutex);
checkConfig(*cfg);
require(queue->reservations.load() < VIDEO_OUT_FLIP_QUEUE_CAPACITY, "flip queue full");
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");
}
request->generation = cfg->generation;
request->width = cfg->width;
request->height = cfg->height;
++queue->reservations;
++cfg->flipStatus.flipPendingNum;
request->reserved = true;
return request;
}
void Fail(std::exception_ptr error) noexcept override {
if (!error) std::terminate();
{
std::lock_guard lock(queue->mutex);
if (!queue->failure) queue->failure = error;
}
{
std::lock_guard lock(cfg->mutex);
if (!cfg->failure) cfg->failure = error;
cfg->vblankCond.notify_all();
}
queue->changed.notify_all();
}
private:
std::shared_ptr<VideoOutConfig> cfg;
std::shared_ptr<FlipQueue> queue;
};
}
FlipRequest::~FlipRequest() {
if (!reserved) return;
std::lock_guard lock(cfg->mutex);
if (!terminal) {
--cfg->flipStatus.flipPendingNum;
--queue->reservations;
cfg->vblankCond.notify_all();
}
}
void FlipRequest::GpuReady() {
{
std::lock_guard queueLock(queue->mutex);
if (queue->failure) std::rethrow_exception(queue->failure);
require(!queue->stopping, "GPU flip during shutdown");
std::lock_guard lock(cfg->mutex);
checkConfig(*cfg);
require(reserved && !ready && !terminal && cfg->generation == generation, "invalid flip readiness transition");
queue->requests.push_back(shared_from_this());
ready = true;
}
queue->changed.notify_all();
std::unique_lock lock(cfg->mutex);
cfg->vblankCond.wait(lock, [&] { return gpuComplete || cfg->failure || cfg->closing; });
checkConfig(*cfg);
require(gpuComplete, "flip preparation did not complete on GPU");
}
void FlipRequest::Fail(std::exception_ptr error) noexcept {
if (!error) std::terminate();
std::lock_guard lock(cfg->mutex);
if (!cfg->failure) cfg->failure = error;
if (reserved && !terminal) {
--cfg->flipStatus.flipPendingNum;
--queue->reservations;
terminal = true;
}
cfg->vblankCond.notify_all();
queue->changed.notify_all();
}
VideoOutDriver& VideoOutDriver::Get() {
static VideoOutDriver instance;
@ -18,273 +130,277 @@ VideoOutDriver::VideoOutDriver() {
if (SDL_InitSubSystem(SDL_INIT_VIDEO) < 0) {
throw std::runtime_error(std::string("SDL_InitSubSystem(VIDEO) failed: ") + SDL_GetError());
}
presentThread = std::jthread([this](std::stop_token token) { presentLoop(token); });
try {
AgcDriverWaitIdle_nid_postfix();
presentThread = std::jthread([this](std::stop_token token) { presentLoop(token); });
LibcRegisterShutdown_nid_postfix([] { VideoOutDriver::Get().Shutdown(); });
} catch (...) {
if (presentThread.joinable()) {
presentThread.request_stop();
flipQueue->changed.notify_all();
presentThread.join();
}
SDL_QuitSubSystem(SDL_INIT_VIDEO);
throw;
}
}
VideoOutDriver::~VideoOutDriver() {
if (presentThread.joinable()) {
presentThread.request_stop();
flipCond.notify_all();
presentThread.join();
if (!stopped) Shutdown();
}
void VideoOutDriver::Shutdown() {
require(std::this_thread::get_id() != presentThread.get_id(), "presentation thread cannot stop itself");
std::lock_guard shutdownLock(shutdownMutex);
if (stopped) return;
{
std::lock_guard lock(mutex);
{
std::lock_guard queueLock(flipQueue->mutex);
flipQueue->stopping = true;
}
for (int handle = 1; handle < VIDEO_OUT_NUM_MAX; ++handle) {
if (outputs[handle]) close(handle);
}
}
presentThread.request_stop();
flipQueue->changed.notify_all();
presentThread.join();
if (window != nullptr) {
AgcDriverReleaseWindow_nid_postfix(window);
SDL_DestroyWindow(window);
window = nullptr;
}
SDL_QuitSubSystem(SDL_INIT_VIDEO);
stopped = true;
std::lock_guard lock(flipQueue->mutex);
if (flipQueue->failure) std::rethrow_exception(flipQueue->failure);
}
int VideoOutDriver::Open(int busType) {
std::unique_lock lock(mutex);
std::lock_guard lock(mutex);
{
std::lock_guard queueLock(flipQueue->mutex);
if (flipQueue->failure) std::rethrow_exception(flipQueue->failure);
require(!flipQueue->stopping, "open during shutdown");
}
const int handle = busType + 1;
if (handle <= 0 || handle >= VIDEO_OUT_NUM_MAX) {
return -1;
}
auto& cfg = contexts[handle];
std::unique_lock cfgLock(cfg.mutex);
if (cfg.opened) {
return -1;
}
cfg.closing = false;
cfg.opened = true;
cfg.flipRate = 0;
cfg.outputMode = VIDEO_OUT_OUTPUT_MODE_DEFAULT;
cfg.gamma = 1.0f;
cfg.flipStatus = VideoOutFlipStatus{};
cfg.flipStatus.flipArg = -1;
cfg.flipStatus.currentBuffer = -1;
cfg.vblankStatus = VideoOutVblankStatus{};
cfg.preVblankStatus = VideoOutVblankStatus{};
for (auto& buf : cfg.buffers) {
buf = VideoOutBuffer{};
}
for (auto& grp : cfg.groups) {
grp = BufferAttributeGroup{};
}
cfg.flipEvents.clear();
cfg.vblankEvents.clear();
cfg.preVblankEvents.clear();
cfg.outputModeEvents.clear();
if (++cfg.generation == 0) {
throw std::runtime_error("VideoOut port generation wrapped");
require(handle > 0 && handle < VIDEO_OUT_NUM_MAX, "invalid output bus");
auto previous = contexts[handle];
uint64_t generation = 1;
if (previous) {
std::lock_guard cfgLock(previous->mutex);
require(!previous->opened, "port already open");
require(previous->generation != std::numeric_limits<uint64_t>::max(), "port generation overflow");
generation = previous->generation + 1;
}
auto cfg = std::make_shared<VideoOutConfig>();
cfg->generation = generation;
cfg->opened = true;
cfg->flipStatus.flipArg = -1;
cfg->flipStatus.currentBuffer = -1;
auto output = std::make_shared<VideoOutput>(cfg, flipQueue);
AgcDriverRegisterVideoOutput_nid_postfix(static_cast<uint32_t>(handle), output);
contexts[handle] = std::move(cfg);
outputs[handle] = std::move(output);
return handle;
}
bool VideoOutDriver::Close(int handle) {
std::unique_lock lock(mutex);
if (handle <= 0 || handle >= VIDEO_OUT_NUM_MAX) {
return false;
}
auto& cfg = contexts[handle];
std::unique_lock cfgLock(cfg.mutex);
if (!cfg.opened || cfg.closing) {
return false;
}
cfg.opened = false;
cfg.closing = true;
if (++cfg.generation == 0) {
throw std::runtime_error("VideoOut port generation wrapped");
}
for (auto& buf : cfg.buffers) {
buf = VideoOutBuffer{};
}
for (auto& grp : cfg.groups) {
grp = BufferAttributeGroup{};
}
cfg.flipRate = 0;
cfg.flipEvents.clear();
cfg.vblankEvents.clear();
cfg.preVblankEvents.clear();
cfg.outputModeEvents.clear();
cfg.vblankCond.notify_all();
std::lock_guard lock(mutex);
return close(handle);
}
bool VideoOutDriver::close(int handle) {
require(handle > 0 && handle < VIDEO_OUT_NUM_MAX && outputs[handle] != nullptr, "invalid close handle");
AgcDriverUnregisterVideoOutput_nid_postfix(static_cast<uint32_t>(handle), outputs[handle]);
outputs[handle].reset();
auto cfg = contexts[handle];
std::lock_guard cfgLock(cfg->mutex);
cfg->opened = false;
cfg->closing = true;
cfg->flipEvents.clear();
cfg->vblankEvents.clear();
cfg->preVblankEvents.clear();
cfg->outputModeEvents.clear();
cfg->vblankCond.notify_all();
flipQueue->changed.notify_all();
return true;
}
VideoOutConfig* VideoOutDriver::GetConfig(int handle) {
std::unique_lock lock(mutex);
if (handle <= 0 || handle >= VIDEO_OUT_NUM_MAX || !contexts[handle].opened) {
return nullptr;
std::shared_ptr<VideoOutConfig> VideoOutDriver::GetConfig(int handle) {
{
std::lock_guard lock(flipQueue->mutex);
if (flipQueue->failure) std::rethrow_exception(flipQueue->failure);
require(!flipQueue->stopping, "output is shutting down");
}
return &contexts[handle];
std::lock_guard lock(mutex);
require(handle > 0 && handle < VIDEO_OUT_NUM_MAX && contexts[handle] != nullptr, "invalid output handle");
auto cfg = contexts[handle];
std::lock_guard cfgLock(cfg->mutex);
checkConfig(*cfg);
return cfg;
}
bool VideoOutDriver::IsOpen(int handle) {
std::unique_lock lock(mutex);
return handle > 0 && handle < VIDEO_OUT_NUM_MAX && contexts[handle].opened;
return GetConfig(handle) != nullptr;
}
void VideoOutDriver::SubmitFlip(VideoOutConfig* cfg, int index, int flipMode, int64_t flipArg) {
std::unique_lock lock(flipMutex);
if (flipQueue.size() >= VIDEO_OUT_FLIP_QUEUE_CAPACITY) {
throw std::runtime_error("VideoOut flip queue full");
}
FlipRequest req;
req.cfg = cfg;
req.generation = cfg->generation;
req.index = index;
req.flipMode = flipMode;
req.flipArg = flipArg;
{
std::unique_lock cfgLock(cfg->mutex);
cfg->flipStatus.flipPendingNum++;
}
flipQueue.push_back(req);
flipCond.notify_one();
void VideoOutDriver::SubmitFlip(int handle, int index, int flipMode, int64_t flipArg) {
std::array<uint32_t, AgcDriver::FlipPacketWords> words{AgcDriver::FlipPacketHeader, static_cast<uint32_t>(handle), static_cast<uint32_t>(index), static_cast<uint32_t>(flipMode), static_cast<uint32_t>(static_cast<uint64_t>(flipArg)), static_cast<uint32_t>(static_cast<uint64_t>(flipArg) >> 32u)};
Packet packet{words.data(), static_cast<uint32_t>(words.size()), 0, {}};
const auto result = sceAgcDriverSubmitDcb(&packet);
require(result == 0, "driver rejected flip submission");
}
void VideoOutDriver::triggerEvents(VideoOutConfig& cfg, int eventKind, void* triggerData) {
std::vector<EventRegistration>* events = nullptr;
if (eventKind == VIDEO_OUT_EVENT_FLIP) {
events = &cfg.flipEvents;
} else if (eventKind == VIDEO_OUT_EVENT_VBLANK) {
events = &cfg.vblankEvents;
} else if (eventKind == VIDEO_OUT_EVENT_PRE_VBLANK_START) {
events = &cfg.preVblankEvents;
} else if (eventKind == VIDEO_OUT_EVENT_SET_MODE) {
events = &cfg.outputModeEvents;
} else {
throw std::runtime_error("triggerEvents: unknown event kind");
}
for (auto& reg : *events) {
if (reg.generation != cfg.generation) {
continue;
}
if (eventKind == VIDEO_OUT_EVENT_FLIP) events = &cfg.flipEvents;
else if (eventKind == VIDEO_OUT_EVENT_VBLANK) events = &cfg.vblankEvents;
else if (eventKind == VIDEO_OUT_EVENT_PRE_VBLANK_START) events = &cfg.preVblankEvents;
else if (eventKind == VIDEO_OUT_EVENT_SET_MODE) events = &cfg.outputModeEvents;
else throw std::runtime_error("VideoOut: unknown event kind");
for (const auto& reg : *events) {
require(reg.generation == cfg.generation, "stale event registration");
EqueueTriggerEvent_nid_postfix(reg.eq, static_cast<uintptr_t>(eventKind), EVFILT_VIDEO_OUT, triggerData);
}
}
void VideoOutDriver::vblankBegin() {
std::unique_lock lock(mutex);
for (int i = 1; i < VIDEO_OUT_NUM_MAX; i++) {
auto& cfg = contexts[i];
if (!cfg.opened) {
continue;
}
std::unique_lock cfgLock(cfg.mutex);
cfg.preVblankStatus.count++;
cfg.preVblankStatus.processTime = sceKernelGetProcessTime();
cfg.preVblankStatus.processTimeCounter = sceKernelGetProcessTimeCounter();
triggerEvents(cfg, VIDEO_OUT_EVENT_PRE_VBLANK_START, reinterpret_cast<void*>(cfg.preVblankStatus.count));
std::lock_guard lock(mutex);
for (const auto& cfg : contexts) {
if (!cfg) continue;
std::lock_guard cfgLock(cfg->mutex);
if (!cfg->opened || cfg->failure) continue;
++cfg->preVblankStatus.count;
cfg->preVblankStatus.processTime = sceKernelGetProcessTime();
cfg->preVblankStatus.processTimeCounter = sceKernelGetProcessTimeCounter();
triggerEvents(*cfg, VIDEO_OUT_EVENT_PRE_VBLANK_START, reinterpret_cast<void*>(cfg->preVblankStatus.count));
}
}
void VideoOutDriver::vblankEnd() {
std::unique_lock lock(mutex);
for (int i = 1; i < VIDEO_OUT_NUM_MAX; i++) {
auto& cfg = contexts[i];
if (!cfg.opened) {
continue;
}
std::unique_lock cfgLock(cfg.mutex);
cfg.vblankStatus.count++;
cfg.vblankStatus.processTime = sceKernelGetProcessTime();
cfg.vblankStatus.processTimeCounter = sceKernelGetProcessTimeCounter();
triggerEvents(cfg, VIDEO_OUT_EVENT_VBLANK, reinterpret_cast<void*>(cfg.vblankStatus.count));
cfg.vblankCond.notify_all();
std::lock_guard lock(mutex);
for (const auto& cfg : contexts) {
if (!cfg) continue;
std::lock_guard cfgLock(cfg->mutex);
if (!cfg->opened || cfg->failure) continue;
++cfg->vblankStatus.count;
cfg->vblankStatus.processTime = sceKernelGetProcessTime();
cfg->vblankStatus.processTimeCounter = sceKernelGetProcessTimeCounter();
triggerEvents(*cfg, VIDEO_OUT_EVENT_VBLANK, reinterpret_cast<void*>(cfg->vblankStatus.count));
cfg->vblankCond.notify_all();
}
}
void VideoOutDriver::processFlip(const FlipRequest& req) {
VideoOutConfig* cfg = req.cfg;
void VideoOutDriver::processFlip(FlipRequest& req) {
{
std::unique_lock cfgLock(cfg->mutex);
if (!cfg->opened || cfg->closing || cfg->generation != req.generation) {
cfg->flipStatus.flipPendingNum--;
return;
}
std::lock_guard lock(req.cfg->mutex);
checkConfig(*req.cfg);
require(req.ready && !req.terminal && req.generation == req.cfg->generation, "stale or incomplete flip request");
}
const bool isBlank = req.index == VIDEO_OUT_BUFFER_INDEX_BLANK;
const bool isBlack = req.index == VIDEO_OUT_BUFFER_INDEX_BLACK;
if (isBlank || isBlack) {
if (window != nullptr) {
SDL_FillRect(windowSurface, nullptr, isBlack ? SDL_MapRGB(windowSurface->format, 0, 0, 0) : 0);
SDL_UpdateWindowSurface(window);
}
} else {
throw std::runtime_error("VideoOut Vulkan rendering not implemented");
if (req.index >= 0) {
const auto& attribute = req.group.attribute;
require(attribute.reserved0 == 0 && attribute.pad0 == 0 && attribute.reserved1[0] == 0 && attribute.reserved1[1] == 0 && attribute.reserved1[2] == 0, "reserved buffer attribute bits are set");
require(attribute.width != 0 && attribute.height != 0, "empty registered buffer");
require(req.group.category == VIDEO_OUT_BUFFER_ATTRIBUTE_CATEGORY_UNCOMPRESSED && req.buffer.metadataAddress == 0 && attribute.dcc_control == 0 && attribute.dcc_cb_register_clear_color == 0, "DCC presentation is not implemented");
throw std::runtime_error("VideoOut: guest render-target tiling and image materialization are not implemented");
}
std::unique_lock cfgLock(cfg->mutex);
if (!cfg->opened || cfg->closing || cfg->generation != req.generation) {
cfg->flipStatus.flipPendingNum--;
return;
require(req.width != 0 && req.height != 0 && req.width <= static_cast<uint32_t>(std::numeric_limits<int>::max()) && req.height <= static_cast<uint32_t>(std::numeric_limits<int>::max()), "invalid window dimensions");
if (window == nullptr) {
window = SDL_CreateWindow("PS5", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, static_cast<int>(req.width), static_cast<int>(req.height), SDL_WINDOW_SHOWN | SDL_WINDOW_VULKAN);
if (window == nullptr) throw std::runtime_error(std::string("SDL_CreateWindow failed: ") + SDL_GetError());
}
cfg->flipStatus.count++;
cfg->flipStatus.processTime = sceKernelGetProcessTime();
cfg->flipStatus.processTimeCounter = sceKernelGetProcessTimeCounter();
cfg->flipStatus.flipArg = req.flipArg;
cfg->flipStatus.currentBuffer = req.index;
cfg->flipStatus.flipPendingNum--;
triggerEvents(*cfg, VIDEO_OUT_EVENT_FLIP, reinterpret_cast<void*>(req.flipArg));
unsigned extensionCount = 0;
if (!SDL_Vulkan_GetInstanceExtensions(window, &extensionCount, nullptr)) throw std::runtime_error(std::string("SDL_Vulkan_GetInstanceExtensions failed: ") + SDL_GetError());
std::vector<const char*> extensions(extensionCount);
if (!SDL_Vulkan_GetInstanceExtensions(window, &extensionCount, extensions.data())) throw std::runtime_error(std::string("SDL_Vulkan_GetInstanceExtensions failed: ") + SDL_GetError());
extensions.resize(extensionCount);
const AgcDriver::PresentationWindow target{window, extensions, [](void* context, VkInstance instance) {
VkSurfaceKHR surface = VK_NULL_HANDLE;
if (!SDL_Vulkan_CreateSurface(static_cast<SDL_Window*>(context), instance, &surface)) throw std::runtime_error(std::string("SDL_Vulkan_CreateSurface failed: ") + SDL_GetError());
return surface;
}, req.width, req.height};
AgcDriverPresentClear_nid_postfix(target, req.index == VIDEO_OUT_BUFFER_INDEX_BLACK, [](void* context) {
auto& request = *static_cast<FlipRequest*>(context);
std::lock_guard lock(request.cfg->mutex);
checkConfig(*request.cfg);
require(!request.terminal && !request.gpuComplete, "invalid GPU completion transition");
request.gpuComplete = true;
request.cfg->vblankCond.notify_all();
}, &req);
std::lock_guard lock(req.cfg->mutex);
checkConfig(*req.cfg);
require(!req.terminal && req.cfg->generation == req.generation, "flip cancelled during presentation");
require(req.cfg->flipStatus.count != std::numeric_limits<uint64_t>::max(), "flip counter overflow");
triggerEvents(*req.cfg, VIDEO_OUT_EVENT_FLIP, reinterpret_cast<void*>(req.flipArg));
++req.cfg->flipStatus.count;
req.cfg->flipStatus.processTime = sceKernelGetProcessTime();
req.cfg->flipStatus.processTimeCounter = sceKernelGetProcessTimeCounter();
req.cfg->flipStatus.flipArg = req.flipArg;
req.cfg->flipStatus.currentBuffer = req.index;
--req.cfg->flipStatus.flipPendingNum;
--req.queue->reservations;
req.terminal = true;
}
void VideoOutDriver::presentLoop(std::stop_token token) {
while (!token.stop_requested()) {
uint64_t outputMode = VIDEO_OUT_OUTPUT_MODE_DEFAULT;
std::shared_ptr<FlipRequest> current;
auto nextVblank = std::chrono::steady_clock::now();
try {
while (!token.stop_requested()) {
{
std::unique_lock lock(flipQueue->mutex);
flipQueue->changed.wait_until(lock, nextVblank, [&] { return token.stop_requested() || flipQueue->failure || !flipQueue->requests.empty(); });
if (flipQueue->failure) std::rethrow_exception(flipQueue->failure);
if (token.stop_requested()) break;
if (!flipQueue->requests.empty()) {
current = std::move(flipQueue->requests.front());
flipQueue->requests.pop_front();
}
}
if (current) processFlip(*current);
current.reset();
const auto now = std::chrono::steady_clock::now();
if (now >= nextVblank) {
vblankBegin();
vblankEnd();
nextVblank = now + std::chrono::nanoseconds(1'000'000'000ULL / 60);
}
SDL_Event event;
while (SDL_PollEvent(&event)) {
require(event.type != SDL_QUIT, "window was closed");
}
}
std::list<std::shared_ptr<FlipRequest>> cancelled;
{
std::unique_lock lock(mutex);
for (int i = 1; i < VIDEO_OUT_NUM_MAX; i++) {
if (contexts[i].opened) {
outputMode = contexts[i].outputMode;
break;
}
}
std::lock_guard lock(flipQueue->mutex);
cancelled.swap(flipQueue->requests);
}
const uint32_t freq = (outputMode == VIDEO_OUT_OUTPUT_MODE_119_88HZ) ? VBLANK_FREQ_119 : VBLANK_FREQ_DEFAULT;
const auto period = std::chrono::nanoseconds(1'000'000'000ULL / freq);
const auto frameStart = std::chrono::steady_clock::now();
vblankBegin();
FlipRequest req{};
bool hasFlip = false;
if (!cancelled.empty()) {
auto error = std::make_exception_ptr(std::runtime_error("VideoOut: pending flip cancelled during shutdown"));
for (auto& request : cancelled) request->Fail(error);
}
} catch (...) {
auto error = std::current_exception();
if (!error) std::terminate();
if (current) current->Fail(error);
std::list<std::shared_ptr<FlipRequest>> failed;
{
std::unique_lock lock(flipMutex);
if (!flipQueue.empty()) {
req = flipQueue.front();
flipQueue.pop_front();
hasFlip = true;
}
}
if (hasFlip) {
if (window == nullptr) {
uint32_t w = 1920;
uint32_t h = 1080;
{
std::unique_lock lock(mutex);
if (req.cfg != nullptr) {
std::unique_lock cfgLock(req.cfg->mutex);
w = req.cfg->width;
h = req.cfg->height;
}
}
window = SDL_CreateWindow("PS5", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, static_cast<int>(w), static_cast<int>(h), SDL_WINDOW_SHOWN);
if (window == nullptr) {
throw std::runtime_error(std::string("SDL_CreateWindow failed: ") + SDL_GetError());
}
windowSurface = SDL_GetWindowSurface(window);
if (windowSurface == nullptr) {
throw std::runtime_error(std::string("SDL_GetWindowSurface failed: ") + SDL_GetError());
}
}
processFlip(req);
}
vblankEnd();
SDL_Event sdlEvent;
while (SDL_PollEvent(&sdlEvent)) {
if (sdlEvent.type == SDL_QUIT) {
return;
}
}
const auto elapsed = std::chrono::steady_clock::now() - frameStart;
if (elapsed < period) {
const auto sleepUs = std::chrono::duration_cast<std::chrono::microseconds>(period - elapsed).count();
if (sleepUs > 0) {
sceKernelUsleep_nid_postfix(static_cast<KernelUseconds>(sleepUs));
std::lock_guard lock(flipQueue->mutex);
flipQueue->failure = error;
failed.swap(flipQueue->requests);
}
for (auto& request : failed) request->Fail(error);
{
std::lock_guard lock(mutex);
for (auto& cfg : contexts) {
if (!cfg) continue;
std::lock_guard cfgLock(cfg->mutex);
if (!cfg->failure) cfg->failure = error;
cfg->vblankCond.notify_all();
}
}
flipQueue->changed.notify_all();
AgcDriverReportFailure_nid_postfix(error);
}
}

View File

@ -0,0 +1,133 @@
#include "prx/libSceVideoOut/include/VideoOutDriver.hpp"
#include "prx/libSceVideoOut/include/Buffer.hpp"
#include "prx/libSceVideoOut/include/Output.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver.hpp"
#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp"
#include "prx/libc/include/Shutdown.hpp"
#include <array>
#include <chrono>
#include <cstdio>
#include <string>
#undef main
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() 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();
}
};
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);
alignas(65536) static std::array<std::byte, 65536> storage{};
VideoOutBuffers buffer{storage.data(), nullptr, {nullptr, nullptr}};
VideoOutBufferAttribute2 attribute{};
attribute.width = 64;
attribute.height = 64;
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;
sceVideoOutSubmitChangeBufferAttribute2(handle, 0, &attribute, nullptr);
sceVideoOutUnregisterBuffers(handle, 0);
std::shared_ptr<VideoOutConfig> replacement;
if (reopen) {
sceVideoOutClose(handle);
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(reopen ? "closed" : "tiling") != std::string::npos, "flip used changed attributes or port generation");
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(reopen ? "closed" : "tiling") != std::string::npos, "VideoOut lost worker failure");
AgcDriverUnregisterVideoOutput_nid_postfix(7, gate);
sceVideoOutClose(handle);
const auto shutdown = expectFailure([] { LibcRunShutdown_nid_postfix(); });
check(shutdown.find(reopen ? "closed" : "tiling") != std::string::npos, "shutdown lost asynchronous error");
}
void testPresentation() {
const int handle = sceVideoOutOpen(255, 0, 0, nullptr);
auto cfg = VideoOutDriver::Get().GetConfig(handle);
{
std::lock_guard lock(cfg->mutex);
cfg->width = 64;
cfg->height = 64;
}
for (int index : {-2, -1, -2}) {
uint64_t target;
{
std::lock_guard lock(cfg->mutex);
target = cfg->flipStatus.count + 1;
}
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.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");
}
sceVideoOutClose(handle);
LibcRunShutdown_nid_postfix();
}
}
int main(int argc, char** argv) {
try {
if (argc == 2 && std::string(argv[1]) == "present") testPresentation();
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;
}
}

View File

@ -0,0 +1,7 @@
#ifndef CORE_LIBS_PRX_LIBC_INCLUDE_SHUTDOWN_HPP
#define CORE_LIBS_PRX_LIBC_INCLUDE_SHUTDOWN_HPP
extern "C" void LibcRegisterShutdown_nid_postfix(void (*callback)());
extern "C" void LibcRunShutdown_nid_postfix();
#endif

View File

@ -2,13 +2,63 @@
#include <cstdlib>
#include <cstring>
#include <cerrno>
#include <condition_variable>
#include <exception>
#include <mutex>
#include <thread>
#include <vector>
#include "prx/libc/include/Shutdown.hpp"
#include "prx/libc/include/General.hpp"
#include "SceTypes.hpp"
namespace {
std::mutex shutdownMutex;
std::condition_variable shutdownChanged;
std::vector<void (*)()> shutdownCallbacks;
std::thread::id shutdownThread;
bool shutdownStarted = false;
bool shutdownFinished = false;
std::exception_ptr shutdownFailure;
}
extern "C" void LibcRegisterShutdown_nid_postfix(void (*callback)()) {
std::lock_guard lock(shutdownMutex);
if (callback == nullptr || shutdownStarted) throw std::runtime_error("libc: invalid shutdown registration");
shutdownCallbacks.push_back(callback);
}
extern "C" void LibcRunShutdown_nid_postfix() {
std::unique_lock lock(shutdownMutex);
if (shutdownStarted) {
if (!shutdownFinished && shutdownThread == std::this_thread::get_id()) throw std::runtime_error("libc: recursive shutdown");
shutdownChanged.wait(lock, [] { return shutdownFinished; });
if (shutdownFailure) std::rethrow_exception(shutdownFailure);
return;
}
shutdownStarted = true;
shutdownThread = std::this_thread::get_id();
auto callbacks = std::move(shutdownCallbacks);
lock.unlock();
std::exception_ptr error;
for (auto it = callbacks.rbegin(); it != callbacks.rend(); ++it) {
try { (*it)(); }
catch (...) { if (!error) error = std::current_exception(); }
}
lock.lock();
shutdownFailure = error;
shutdownFinished = true;
lock.unlock();
shutdownChanged.notify_all();
if (error) std::rethrow_exception(error);
}
extern "C" {
void APS5_VABI exit_nid_postfix(int code) {
LibcRunShutdown_nid_postfix();
std::exit(code);
}