From d31e35e1abfa6909ff7656dd560c173dbe2e8e1d Mon Sep 17 00:00:00 2001 From: boykopovar Date: Mon, 14 Sep 2026 07:39:38 +0300 Subject: [PATCH] feat(agc): add flip presentation and suspend synchronization (wip) --- CMakeLists.txt | 2 +- .../prx/libSceAgc/DcbState/src/Display.cpp | 16 +- core/libs/prx/libSceAgc/Misc/src/Suspend.cpp | 5 +- core/libs/prx/libSceAgc/tests/Commands.cpp | 23 + core/libs/prx/libSceAgcDriver/CMakeLists.txt | 6 + .../Execution/include/Driver.hpp | 1 + .../Execution/include/Presentation.hpp | 26 + .../Execution/include/VideoOutput.hpp | 39 ++ .../Execution/include/VulkanDevice.hpp | 7 +- .../libSceAgcDriver/Execution/src/Driver.cpp | 218 ++++++- .../Execution/src/VulkanDevice.cpp | 223 ++++++- core/libs/prx/libSceAgcDriver/tests/Flip.cpp | 193 ++++++ .../prx/libSceAgcDriver/tests/Recompiler.cpp | 9 + .../libs/prx/libSceAgcDriver/tests/Submit.cpp | 4 + core/libs/prx/libSceVideoOut/CMakeLists.txt | 12 +- .../libSceVideoOut/include/VideoOutDriver.hpp | 58 +- core/libs/prx/libSceVideoOut/src/Buffer.cpp | 60 +- core/libs/prx/libSceVideoOut/src/Event.cpp | 35 +- core/libs/prx/libSceVideoOut/src/Output.cpp | 82 +-- .../prx/libSceVideoOut/src/VideoOutDriver.cpp | 548 +++++++++++------- core/libs/prx/libSceVideoOut/tests/Flip.cpp | 133 +++++ core/libs/prx/libc/include/Shutdown.hpp | 7 + core/libs/prx/libc/src/Process.cpp | 50 ++ 23 files changed, 1421 insertions(+), 336 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/Execution/include/Presentation.hpp create mode 100644 core/libs/prx/libSceAgcDriver/Execution/include/VideoOutput.hpp create mode 100644 core/libs/prx/libSceAgcDriver/tests/Flip.cpp create mode 100644 core/libs/prx/libSceVideoOut/tests/Flip.cpp create mode 100644 core/libs/prx/libc/include/Shutdown.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 3e5a82ebd..28176e632 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) diff --git a/core/libs/prx/libSceAgc/DcbState/src/Display.cpp b/core/libs/prx/libSceAgc/DcbState/src/Display.cpp index 82eb7479f..28a98f1aa 100644 --- a/core/libs/prx/libSceAgc/DcbState/src/Display.cpp +++ b/core/libs/prx/libSceAgc/DcbState/src/Display.cpp @@ -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 #include #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(display_buffer_index); + packet[3] = flip_mode; + packet[4] = static_cast(static_cast(flip_arg)); + packet[5] = static_cast(static_cast(flip_arg) >> 32u); + return packet; } uint32_t* APS5_VABI sceAgcDcbPrimeUtcl2(CommandBuffer* buf, const volatile void* address, uint32_t size_in_bytes) { diff --git a/core/libs/prx/libSceAgc/Misc/src/Suspend.cpp b/core/libs/prx/libSceAgc/Misc/src/Suspend.cpp index c44542417..9b6186260 100644 --- a/core/libs/prx/libSceAgc/Misc/src/Suspend.cpp +++ b/core/libs/prx/libSceAgc/Misc/src/Suspend.cpp @@ -1,4 +1,5 @@ #include "prx/libSceAgc/Misc/include/Suspend.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" #include #include @@ -8,8 +9,8 @@ extern "C" { int APS5_VABI sceAgcSuspendPoint(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + AgcDriverSuspendPoint_nid_postfix(); + return 0; } } diff --git a/core/libs/prx/libSceAgc/tests/Commands.cpp b/core/libs/prx/libSceAgc/tests/Commands.cpp index d99915be6..d02f0f328 100644 --- a/core/libs/prx/libSceAgc/tests/Commands.cpp +++ b/core/libs/prx/libSceAgc/tests/Commands.cpp @@ -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 #include @@ -9,6 +10,8 @@ #include 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 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 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; } } diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 1b2cec0e0..4c09bf371 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -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) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver.hpp index 5b6a979e6..d2fde19a7 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver.hpp @@ -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 diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Presentation.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Presentation.hpp new file mode 100644 index 000000000..24b07f5c3 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Presentation.hpp @@ -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 +#include +#include +#include + +namespace AgcDriver { + +struct PresentationWindow { + void* context; + std::span 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 diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/VideoOutput.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/VideoOutput.hpp new file mode 100644 index 000000000..dfa9796d8 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/include/VideoOutput.hpp @@ -0,0 +1,39 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_VIDEOOUTPUT_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_VIDEOOUTPUT_HPP + +#include +#include +#include + +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 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& output); +extern "C" void AgcDriverUnregisterVideoOutput_nid_postfix(std::uint32_t handle, const std::shared_ptr& output); + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp index be7864068..1d5e7d19b 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp @@ -4,17 +4,22 @@ #define VK_NO_PROTOTYPES #include #include "shader/recompilier/Recompiler.hpp" +#include "prx/libSceAgcDriver/Execution/include/Presentation.hpp" #include 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: diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver.cpp index 04e015589..2dff7b475 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver.cpp @@ -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 #include #include #include @@ -90,6 +93,8 @@ struct Submission { std::uint32_t queue; std::vector commands; std::map> shaders; + std::map> 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::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(words[2]), words[3], std::bit_cast(static_cast(words[4]) | (static_cast(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((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::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& 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& 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 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(&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 pending; std::map> shaders; std::map queues; - std::unique_ptr device; + std::map> outputs; + std::mutex gpuMutex; + std::shared_ptr 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((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 packet, const Submission& submission) { + std::lock_guard gpuLock(gpuMutex); const auto address = (static_cast(readRegister(queue.shader, 0x20c)) << 8u) | (static_cast(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 memory{{{snapshot.codeAddress, std::as_bytes(std::span(snapshot.code))}, {snapshot.headerAddress, snapshot.header}}}; if (device == nullptr) { - device = std::make_unique(); + device = std::make_shared(); } const auto codeOffset = static_cast((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((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& output) { + Driver::Get().RegisterVideoOutput(handle, output); +} + +void UnregisterVideoOutput(std::uint32_t handle, const std::shared_ptr& 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& output) { + AgcDriver::RegisterVideoOutput(handle, output); +} + +extern "C" void AgcDriverUnregisterVideoOutput_nid_postfix(std::uint32_t handle, const std::shared_ptr& 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); +} diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 28ec330b6..d7b111416 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -2,6 +2,8 @@ #include #include #include +#include +#include #include #include #include @@ -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 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 capabilities{1}; @@ -50,6 +68,18 @@ struct VulkanDevice::State { ~State() { if (device != VK_NULL_HANDLE) { + const auto idle = reinterpret_cast(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(deviceProc(device, "vkWaitForFences"))(device, 1, &presentFence, VK_TRUE, std::numeric_limits::max()); + if (result != VK_SUCCESS && result != VK_ERROR_DEVICE_LOST) std::terminate(); + } + const auto destroyFence = reinterpret_cast(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(deviceProc(device, "vkDestroySemaphore"))(device, rendered, nullptr); + if (swapchain) reinterpret_cast(deviceProc(device, "vkDestroySwapchainKHR"))(device, swapchain, nullptr); const auto destroyPool = reinterpret_cast(deviceProc(device, "vkDestroyCommandPool")); const auto destroyDevice = reinterpret_cast(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(instanceProc(instance, "vkDestroySurfaceKHR"))(instance, surface, nullptr); reinterpret_cast(instanceProc(instance, "vkDestroyInstance"))(instance, nullptr); } if (library != nullptr) { @@ -66,7 +97,7 @@ struct VulkanDevice::State { } }; -VulkanDevice::VulkanDevice() : state(std::make_unique()) { +VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_unique()) { #ifdef _WIN32 state->library = SDL_LoadObject("vulkan-1.dll"); #else @@ -84,7 +115,32 @@ VulkanDevice::VulkanDevice() : state(std::make_unique()) { application.apiVersion = VK_API_VERSION_1_1; VkInstanceCreateInfo create{VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO}; create.pApplicationInfo = &application; + std::vector 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("vkEnumerateInstanceExtensionProperties"); + check(enumerateExtensions(nullptr, &availableCount, nullptr), "vkEnumerateInstanceExtensionProperties"); + std::vector 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(instanceExtensions.size()); + create.ppEnabledExtensionNames = instanceExtensions.data(); + } check(state->InstanceFunction("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("vkGetDeviceProcAddr"); const auto enumerate = state->InstanceFunction("vkEnumeratePhysicalDevices"); std::uint32_t count = 0; @@ -94,12 +150,33 @@ VulkanDevice::VulkanDevice() : state(std::make_unique()) { devices.resize(count); VkPhysicalDevice selected = VK_NULL_HANDLE; std::uint32_t family = 0; + const std::array 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("vkGetPhysicalDeviceProperties")(physical, &properties); if (properties.apiVersion < VK_API_VERSION_1_1) { continue; } + if (window != nullptr) { + std::uint32_t extensionCount = 0; + auto enumerateExtensions = state->InstanceFunction("vkEnumerateDeviceExtensionProperties"); + check(enumerateExtensions(physical, nullptr, &extensionCount, nullptr), "vkEnumerateDeviceExtensionProperties"); + std::vector 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("vkGetPhysicalDeviceFeatures2")(physical, &features); + if (!id.presentId || !wait.presentWait || !maintenance.swapchainMaintenance1) continue; + } std::uint32_t families = 0; auto getFamilies = state->InstanceFunction("vkGetPhysicalDeviceQueueFamilyProperties"); getFamilies(physical, &families, nullptr); @@ -107,6 +184,11 @@ VulkanDevice::VulkanDevice() : state(std::make_unique()) { 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("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()) { } } 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()) { 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(presentationExtensions.size()); + deviceInfo.ppEnabledExtensionNames = presentationExtensions.data(); + deviceInfo.pNext = &idFeature; + idFeature.pNext = &waitFeature; + waitFeature.pNext = &maintenanceFeature; + } check(state->InstanceFunction("vkCreateDevice")(selected, &deviceInfo, nullptr, &state->device), "vkCreateDevice"); state->DeviceFunction("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("vkCreateCommandPool")(state->device, &poolInfo, nullptr, &state->pool), "vkCreateCommandPool"); + if (window != nullptr) { + VkSurfaceCapabilitiesKHR surface{}; + check(state->InstanceFunction("vkGetPhysicalDeviceSurfaceCapabilitiesKHR")(selected, state->surface, &surface), "vkGetPhysicalDeviceSurfaceCapabilitiesKHR"); + state->extent = {window->width, window->height}; + require(surface.currentExtent.width == std::numeric_limits::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("vkGetPhysicalDeviceSurfaceFormatsKHR"); + check(getFormats(selected, state->surface, &formatCount, nullptr), "vkGetPhysicalDeviceSurfaceFormatsKHR"); + std::vector 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("vkCreateSwapchainKHR")(state->device, &swapchain, nullptr, &state->swapchain), "vkCreateSwapchainKHR"); + std::uint32_t imageCount = 0; + auto getImages = state->DeviceFunction("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("vkCreateFence")(state->device, &fence, nullptr, destination), "vkCreateFence"); + } + VkSemaphoreCreateInfo semaphore{VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO}; + check(state->DeviceFunction("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("vkAllocateCommandBuffers")(state->device, &allocation, &state->clearCommands), "vkAllocateCommandBuffers"); + } } VulkanDevice::~VulkanDevice() = default; +void VulkanDevice::WaitIdle() { + check(state->DeviceFunction("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::max(), "presentation ID overflow"); + auto wait = state->DeviceFunction("vkWaitForFences"); + auto reset = state->DeviceFunction("vkResetFences"); + const std::array fences{state->acquireFence, state->renderFence, state->presentFence}; + check(reset(state->device, static_cast(fences.size()), fences.data()), "vkResetFences"); + std::uint32_t index = 0; + check(state->DeviceFunction("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::max()), "vkWaitForFences acquire"); + require(index < state->images.size(), "acquired image index is out of range"); + auto commands = state->clearCommands; + check(state->DeviceFunction("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("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("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("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("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("vkQueueSubmit")(state->queue, 1, &submit, state->renderFence), "vkQueueSubmit clear"); + check(wait(state->device, 1, &state->renderFence, VK_TRUE, std::numeric_limits::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("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("vkWaitForPresentKHR")(state->device, state->swapchain, id, 5'000'000'000ULL), "vkWaitForPresentKHR"); + check(state->DeviceFunction("vkWaitForFences")(state->device, 1, &state->presentFence, VK_TRUE, std::numeric_limits::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}; diff --git a/core/libs/prx/libSceAgcDriver/tests/Flip.cpp b/core/libs/prx/libSceAgcDriver/tests/Flip.cpp new file mode 100644 index 000000000..01e1a4a6a --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/Flip.cpp @@ -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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +void check(bool condition, const char* reason) { + if (!condition) throw std::runtime_error(reason); +} + +template +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 alive = 0; + std::atomic ready = 0; + std::atomic failed = 0; + AgcDriver::FlipInfo last{}; +}; + +class Request final : public AgcDriver::IFlipRequest { +public: + explicit Request(std::shared_ptr 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; +}; + +class Output final : public AgcDriver::IVideoOutput { +public: + void Fail(std::exception_ptr error) noexcept override { if (!error) std::terminate(); } + std::shared_ptr state = std::make_shared(); + std::shared_ptr Reserve(const AgcDriver::FlipInfo& info) override { + std::lock_guard lock(state->mutex); + state->last = info; + return std::make_shared(state); + } +}; + +void submitFlip(std::uint32_t handle = 7) { + std::array words{0xc004105c, handle, 0xfffffffeu, 1, 0x76543211u, 0xfedcba98u}; + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + check(sceAgcDriverSubmitDcb(&packet) == 0, "flip submission failed"); +} + +void testFlipAndBoundary() { + auto output = std::make_shared(); + AgcDriverRegisterVideoOutput_nid_postfix(7, output); + expectFailure([&] { AgcDriverRegisterVideoOutput_nid_postfix(7, output); }); + std::array 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 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(); + 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 producers; + std::array 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->state->fail = true; + AgcDriverRegisterVideoOutput_nid_postfix(7, output); + submitFlip(); + std::array messages; + std::vector 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 registers{0xc0027600, 0x20c, 1, 0}; + Packet packet{registers.data(), 4, 0, {}}; + if (compute) sceAgcDriverSubmitAcb(0x20, &packet); + else sceAgcDriverSubmitDcb(&packet); + AgcDriverSuspendPoint_nid_postfix(); + std::array 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; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/Recompiler.cpp b/core/libs/prx/libSceAgcDriver/tests/Recompiler.cpp index c707c9199..7fa045b92 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recompiler.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recompiler.cpp @@ -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 #include @@ -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; } } diff --git a/core/libs/prx/libSceAgcDriver/tests/Submit.cpp b/core/libs/prx/libSceAgcDriver/tests/Submit.cpp index b5a2a88ed..b2685e5f5 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Submit.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Submit.cpp @@ -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; } } diff --git a/core/libs/prx/libSceVideoOut/CMakeLists.txt b/core/libs/prx/libSceVideoOut/CMakeLists.txt index 5337ccacd..2fe440669 100644 --- a/core/libs/prx/libSceVideoOut/CMakeLists.txt +++ b/core/libs/prx/libSceVideoOut/CMakeLists.txt @@ -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) diff --git a/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp b/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp index 4128f82b3..156fc32e1 100644 --- a/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp +++ b/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp @@ -8,6 +8,11 @@ #include #include #include +#include +#include +#include +#include +#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 buffers{}; std::array 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 { + std::shared_ptr cfg; + std::shared_ptr 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> requests; + std::atomic 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 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 flipQueue; + std::mutex shutdownMutex; + bool stopped = false; + std::array, VIDEO_OUT_NUM_MAX> contexts; + std::array, VIDEO_OUT_NUM_MAX> outputs; + std::shared_ptr flipQueue = std::make_shared(); SDL_Window* window = nullptr; - SDL_Surface* windowSurface = nullptr; std::jthread presentThread; }; diff --git a/core/libs/prx/libSceVideoOut/src/Buffer.cpp b/core/libs/prx/libSceVideoOut/src/Buffer.cpp index a133b59ba..e8b60e0b7 100644 --- a/core/libs/prx/libSceVideoOut/src/Buffer.cpp +++ b/core/libs/prx/libSceVideoOut/src/Buffer.cpp @@ -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; } diff --git a/core/libs/prx/libSceVideoOut/src/Event.cpp b/core/libs/prx/libSceVideoOut/src/Event.cpp index f35e73f4d..cf5f938a2 100644 --- a/core/libs/prx/libSceVideoOut/src/Event.cpp +++ b/core/libs/prx/libSceVideoOut/src/Event.cpp @@ -17,15 +17,15 @@ static std::vector* 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(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(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(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(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(ev->data) >> 16u; if (ev->ident == static_cast(VIDEO_OUT_EVENT_FLIP) && (static_cast(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((static_cast(ev->data) >> 12u) & 0xfu); } diff --git a/core/libs/prx/libSceVideoOut/src/Output.cpp b/core/libs/prx/libSceVideoOut/src/Output.cpp index 2179accaf..b0bda6f9e 100644 --- a/core/libs/prx/libSceVideoOut/src/Output.cpp +++ b/core/libs/prx/libSceVideoOut/src/Output.cpp @@ -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"); } diff --git a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp index ce6121aa7..7577c9d8c 100644 --- a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp +++ b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp @@ -1,13 +1,125 @@ +#include #include +#include #include #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 config, std::shared_ptr requests) : cfg(std::move(config)), queue(std::move(requests)) {} + + std::shared_ptr 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(); + request->cfg = cfg; + request->queue = queue; + request->index = info.index; + request->flipMode = static_cast(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 cfg; + std::shared_ptr 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::max(), "port generation overflow"); + generation = previous->generation + 1; } + auto cfg = std::make_shared(); + cfg->generation = generation; + cfg->opened = true; + cfg->flipStatus.flipArg = -1; + cfg->flipStatus.currentBuffer = -1; + auto output = std::make_shared(cfg, flipQueue); + AgcDriverRegisterVideoOutput_nid_postfix(static_cast(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(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 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 words{AgcDriver::FlipPacketHeader, static_cast(handle), static_cast(index), static_cast(flipMode), static_cast(static_cast(flipArg)), static_cast(static_cast(flipArg) >> 32u)}; + Packet packet{words.data(), static_cast(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* 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(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(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(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(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(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(std::numeric_limits::max()) && req.height <= static_cast(std::numeric_limits::max()), "invalid window dimensions"); + if (window == nullptr) { + window = SDL_CreateWindow("PS5", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, static_cast(req.width), static_cast(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(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 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(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(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::max(), "flip counter overflow"); + triggerEvents(*req.cfg, VIDEO_OUT_EVENT_FLIP, reinterpret_cast(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 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> 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> 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(w), static_cast(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(period - elapsed).count(); - if (sleepUs > 0) { - sceKernelUsleep_nid_postfix(static_cast(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); } } diff --git a/core/libs/prx/libSceVideoOut/tests/Flip.cpp b/core/libs/prx/libSceVideoOut/tests/Flip.cpp new file mode 100644 index 000000000..2720bf380 --- /dev/null +++ b/core/libs/prx/libSceVideoOut/tests/Flip.cpp @@ -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 +#include +#include +#include + +#undef main + +namespace { + +void check(bool condition, const char* reason) { + if (!condition) throw std::runtime_error(reason); +} + +template +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 { +public: + std::mutex mutex; + std::condition_variable changed; + bool released = false; + std::shared_ptr 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(); + AgcDriverRegisterVideoOutput_nid_postfix(7, gate); + std::array words{0xc004105c, 7, 0xfffffffeu, 1, 0, 0}; + Packet packet{words.data(), 6, 0, {}}; + sceAgcDriverSubmitDcb(&packet); + alignas(65536) static std::array 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 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; + } +} diff --git a/core/libs/prx/libc/include/Shutdown.hpp b/core/libs/prx/libc/include/Shutdown.hpp new file mode 100644 index 000000000..048505884 --- /dev/null +++ b/core/libs/prx/libc/include/Shutdown.hpp @@ -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 diff --git a/core/libs/prx/libc/src/Process.cpp b/core/libs/prx/libc/src/Process.cpp index af63d62fe..5e8bf25b3 100644 --- a/core/libs/prx/libc/src/Process.cpp +++ b/core/libs/prx/libc/src/Process.cpp @@ -2,13 +2,63 @@ #include #include #include +#include +#include +#include +#include +#include +#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 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); }