feat(agc): add flip presentation and suspend synchronization (wip)
This commit is contained in:
parent
b024001a4a
commit
d31e35e1ab
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
#include "prx/libSceAgc/DcbState/include/Display.hpp"
|
||||
|
||||
#include "prx/libSceAgc/Command/include/Packet.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/VideoOutput.hpp"
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include "SceTypes.hpp"
|
||||
|
|
@ -9,13 +10,14 @@
|
|||
extern "C" {
|
||||
|
||||
uint32_t* APS5_VABI sceAgcDcbSetFlip(CommandBuffer* buf, uint32_t video_out_handle, int32_t display_buffer_index, uint32_t flip_mode, int64_t flip_arg) {
|
||||
(void)buf;
|
||||
(void)video_out_handle;
|
||||
(void)display_buffer_index;
|
||||
(void)flip_mode;
|
||||
(void)flip_arg;
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return nullptr;
|
||||
auto* packet = Agc::Command::Allocate(buf, AgcDriver::FlipPacketWords, __func__);
|
||||
packet[0] = AgcDriver::FlipPacketHeader;
|
||||
packet[1] = video_out_handle;
|
||||
packet[2] = static_cast<std::uint32_t>(display_buffer_index);
|
||||
packet[3] = flip_mode;
|
||||
packet[4] = static_cast<std::uint32_t>(static_cast<std::uint64_t>(flip_arg));
|
||||
packet[5] = static_cast<std::uint32_t>(static_cast<std::uint64_t>(flip_arg) >> 32u);
|
||||
return packet;
|
||||
}
|
||||
|
||||
uint32_t* APS5_VABI sceAgcDcbPrimeUtcl2(CommandBuffer* buf, const volatile void* address, uint32_t size_in_bytes) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
#include "prx/libSceAgc/Misc/include/Suspend.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/Driver.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
|
|
@ -8,8 +9,8 @@
|
|||
extern "C" {
|
||||
|
||||
int APS5_VABI sceAgcSuspendPoint(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
AgcDriverSuspendPoint_nid_postfix();
|
||||
return 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
#include "prx/libSceAgc/Command/include/Packet.hpp"
|
||||
#include "prx/libSceAgc/Command/include/Memory.hpp"
|
||||
#include "prx/libSceAgc/Command/include/RegisterDefaults.hpp"
|
||||
#include "prx/libc/include/Shutdown.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
|
|
@ -9,6 +10,8 @@
|
|||
#include <string>
|
||||
|
||||
extern "C" std::uint32_t* APS5_VABI sceAgcDcbResetQueue(CommandBuffer* buf, std::uint32_t op, std::uint32_t state);
|
||||
extern "C" std::uint32_t* APS5_VABI sceAgcDcbSetFlip(CommandBuffer* buf, std::uint32_t handle, std::int32_t index, std::uint32_t mode, std::int64_t argument);
|
||||
extern "C" int APS5_VABI sceAgcSuspendPoint();
|
||||
extern "C" std::uint32_t* APS5_VABI sceAgcDcbDrawIndexAuto(CommandBuffer* buf, std::uint32_t indexCount, std::uint64_t modifier);
|
||||
extern "C" int APS5_VABI sceAgcWaitRegMemPatchReference(std::uint32_t* cmd, std::uint64_t reference);
|
||||
extern "C" int APS5_VABI sceAgcGetDataPacketPayloadAddressUnk(std::uint32_t** addr, std::uint32_t* cmd, int type);
|
||||
|
|
@ -68,6 +71,22 @@ void testPackets() {
|
|||
check(exhausted.buffer.cursor_up == exhausted.words.data(), "failed allocation advanced cursor");
|
||||
}
|
||||
|
||||
void testFlip() {
|
||||
Storage storage;
|
||||
storage.words.fill(0xdeadbeefu);
|
||||
auto* packet = sceAgcDcbSetFlip(&storage.buffer, 0xfedcba98u, -2, 0x12345678u, -0x123456789abcdefLL);
|
||||
const std::array<std::uint32_t, 6> expected{0xc004105cu, 0xfedcba98u, 0xfffffffeu, 0x12345678u, 0x76543211u, 0xfedcba98u};
|
||||
check(packet == storage.words.data(), "flip returned wrong packet address");
|
||||
check(std::equal(expected.begin(), expected.end(), packet), "flip packet lost argument bits");
|
||||
check(storage.buffer.cursor_up == packet + 6 && packet[6] == 0xdeadbeefu, "flip packet overran allocation");
|
||||
expectFailure([] { sceAgcDcbSetFlip(nullptr, 1, 0, 1, 0); });
|
||||
Storage exhausted;
|
||||
exhausted.buffer.cursor_down = exhausted.words.data() + 5;
|
||||
expectFailure([&] { sceAgcDcbSetFlip(&exhausted.buffer, 1, 0, 1, 0); });
|
||||
check(exhausted.buffer.cursor_up == exhausted.words.data(), "failed flip allocation advanced cursor");
|
||||
check(sceAgcSuspendPoint() == 0, "empty suspend failed");
|
||||
}
|
||||
|
||||
void testRegisters() {
|
||||
Storage storage;
|
||||
const std::array<ShaderRegister, 3> registers{{{0x10, 7}, {0x11, 8}, {0x20, 9}}};
|
||||
|
|
@ -159,15 +178,19 @@ void testDefaults() {
|
|||
int main() {
|
||||
try {
|
||||
testPackets();
|
||||
testFlip();
|
||||
testRegisters();
|
||||
testRegisterRange();
|
||||
testPacketPayloadAddress();
|
||||
testMemory();
|
||||
testDefaults();
|
||||
LibcRunShutdown_nid_postfix();
|
||||
std::puts("AGC command tests passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::fprintf(stderr, "%s\n", error.what());
|
||||
try { LibcRunShutdown_nid_postfix(); }
|
||||
catch (const std::exception& shutdown) { std::fprintf(stderr, "shutdown: %s\n", shutdown.what()); }
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_PRESENTATION_HPP
|
||||
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_PRESENTATION_HPP
|
||||
|
||||
#define VK_NO_PROTOTYPES
|
||||
#include <vulkan/vulkan.h>
|
||||
#include <cstdint>
|
||||
#include <exception>
|
||||
#include <span>
|
||||
|
||||
namespace AgcDriver {
|
||||
|
||||
struct PresentationWindow {
|
||||
void* context;
|
||||
std::span<const char* const> extensions;
|
||||
VkSurfaceKHR (*createSurface)(void* context, VkInstance instance);
|
||||
std::uint32_t width;
|
||||
std::uint32_t height;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
extern "C" void AgcDriverPresentClear_nid_postfix(const AgcDriver::PresentationWindow& window, bool opaque, void (*gpuReady)(void*), void* context);
|
||||
extern "C" void AgcDriverReleaseWindow_nid_postfix(void* window);
|
||||
extern "C" void AgcDriverReportFailure_nid_postfix(std::exception_ptr error);
|
||||
|
||||
#endif
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_VIDEOOUTPUT_HPP
|
||||
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_VIDEOOUTPUT_HPP
|
||||
|
||||
#include <cstdint>
|
||||
#include <exception>
|
||||
#include <memory>
|
||||
|
||||
namespace AgcDriver {
|
||||
|
||||
inline constexpr std::uint32_t FlipPacketHeader = 0xc004105cu;
|
||||
inline constexpr std::uint32_t FlipPacketWords = 6;
|
||||
|
||||
struct FlipInfo {
|
||||
std::uint32_t handle;
|
||||
std::int32_t index;
|
||||
std::uint32_t mode;
|
||||
std::int64_t argument;
|
||||
};
|
||||
|
||||
class IFlipRequest {
|
||||
public:
|
||||
virtual ~IFlipRequest() = default;
|
||||
virtual void GpuReady() = 0;
|
||||
virtual void Fail(std::exception_ptr error) noexcept = 0;
|
||||
};
|
||||
|
||||
class IVideoOutput {
|
||||
public:
|
||||
virtual ~IVideoOutput() = default;
|
||||
virtual std::shared_ptr<IFlipRequest> Reserve(const FlipInfo& info) = 0;
|
||||
virtual void Fail(std::exception_ptr error) noexcept = 0;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
extern "C" void AgcDriverRegisterVideoOutput_nid_postfix(std::uint32_t handle, const std::shared_ptr<AgcDriver::IVideoOutput>& output);
|
||||
extern "C" void AgcDriverUnregisterVideoOutput_nid_postfix(std::uint32_t handle, const std::shared_ptr<AgcDriver::IVideoOutput>& output);
|
||||
|
||||
#endif
|
||||
|
|
@ -4,17 +4,22 @@
|
|||
#define VK_NO_PROTOTYPES
|
||||
#include <vulkan/vulkan.h>
|
||||
#include "shader/recompilier/Recompiler.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/Presentation.hpp"
|
||||
#include <memory>
|
||||
|
||||
namespace AgcDriver {
|
||||
|
||||
class VulkanDevice {
|
||||
public:
|
||||
VulkanDevice();
|
||||
explicit VulkanDevice(const PresentationWindow* window = nullptr);
|
||||
~VulkanDevice();
|
||||
VulkanDevice(const VulkanDevice&) = delete;
|
||||
VulkanDevice& operator=(const VulkanDevice&) = delete;
|
||||
ShaderRecompiler::SpirvTarget Target() const;
|
||||
void WaitIdle();
|
||||
void* Window() const;
|
||||
std::uint64_t PresentClear(std::uint32_t width, std::uint32_t height, bool opaque);
|
||||
void WaitPresented(std::uint64_t id);
|
||||
void Dispatch(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z);
|
||||
|
||||
private:
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
#include "prx/libSceAgcDriver/Execution/include/Driver.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/VideoOutput.hpp"
|
||||
#include "prx/libc/include/Shutdown.hpp"
|
||||
#include <bit>
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <condition_variable>
|
||||
|
|
@ -90,6 +93,8 @@ struct Submission {
|
|||
std::uint32_t queue;
|
||||
std::vector<std::uint32_t> commands;
|
||||
std::map<std::uint64_t, std::shared_ptr<const ShaderSnapshot>> shaders;
|
||||
std::map<std::size_t, std::shared_ptr<IFlipRequest>> flips;
|
||||
bool suspend = false;
|
||||
};
|
||||
|
||||
std::uint32_t readRegister(const Registers& registers, std::uint32_t offset) {
|
||||
|
|
@ -115,14 +120,28 @@ public:
|
|||
}
|
||||
|
||||
~Driver() {
|
||||
stop();
|
||||
}
|
||||
|
||||
void Shutdown() {
|
||||
stop();
|
||||
CheckFailure();
|
||||
}
|
||||
|
||||
private:
|
||||
void stop() {
|
||||
require(std::this_thread::get_id() != worker.get_id(), "worker cannot stop itself");
|
||||
std::lock_guard shutdownLock(shutdownMutex);
|
||||
{
|
||||
std::lock_guard lock(mutex);
|
||||
stopping = true;
|
||||
}
|
||||
changed.notify_all();
|
||||
worker.join();
|
||||
if (worker.joinable()) worker.join();
|
||||
}
|
||||
|
||||
public:
|
||||
|
||||
void Submit(const Packet* packet, std::uint32_t queue) {
|
||||
CheckFailure();
|
||||
require(queue == 0 || (queue >= 0x20 && queue < 0x58), "unsupported compute queue");
|
||||
|
|
@ -142,6 +161,18 @@ public:
|
|||
rethrowFailure();
|
||||
require(!stopping, "submission during shutdown");
|
||||
require(accepted != std::numeric_limits<std::uint64_t>::max(), "submission serial overflow");
|
||||
for (std::size_t cursor = 0; cursor < submission.commands.size();) {
|
||||
const auto* words = submission.commands.data() + cursor;
|
||||
if (words[0] == FlipPacketHeader) {
|
||||
const auto output = outputs.find(words[1]);
|
||||
require(output != outputs.end(), "flip references an unregistered video output");
|
||||
const FlipInfo info{words[1], std::bit_cast<std::int32_t>(words[2]), words[3], std::bit_cast<std::int64_t>(static_cast<std::uint64_t>(words[4]) | (static_cast<std::uint64_t>(words[5]) << 32u))};
|
||||
auto request = output->second->Reserve(info);
|
||||
require(request != nullptr, "video output returned a null flip reservation");
|
||||
submission.flips.emplace(cursor, std::move(request));
|
||||
}
|
||||
cursor += static_cast<std::size_t>((words[0] >> 16u) & 0x3fffu) + 2;
|
||||
}
|
||||
submission.shaders = shaders;
|
||||
submission.serial = accepted + 1;
|
||||
pending.push_back(std::move(submission));
|
||||
|
|
@ -158,11 +189,86 @@ public:
|
|||
rethrowFailure();
|
||||
}
|
||||
|
||||
void SuspendPoint() {
|
||||
require(std::this_thread::get_id() != worker.get_id(), "worker cannot suspend itself");
|
||||
std::unique_lock lock(mutex);
|
||||
rethrowFailure();
|
||||
require(!stopping, "suspend during shutdown");
|
||||
require(accepted != std::numeric_limits<std::uint64_t>::max(), "submission serial overflow");
|
||||
Submission boundary{};
|
||||
boundary.serial = accepted + 1;
|
||||
boundary.suspend = true;
|
||||
pending.push_back(std::move(boundary));
|
||||
const auto target = ++accepted;
|
||||
changed.notify_all();
|
||||
changed.wait(lock, [&] { return failure != nullptr || completed >= target; });
|
||||
rethrowFailure();
|
||||
}
|
||||
|
||||
void RegisterVideoOutput(std::uint32_t handle, const std::shared_ptr<IVideoOutput>& output) {
|
||||
require(output != nullptr, "null video output");
|
||||
std::lock_guard lock(mutex);
|
||||
rethrowFailure();
|
||||
require(!stopping, "video output registration during shutdown");
|
||||
require(outputs.emplace(handle, output).second, "video output already registered");
|
||||
}
|
||||
|
||||
void UnregisterVideoOutput(std::uint32_t handle, const std::shared_ptr<IVideoOutput>& output) {
|
||||
std::lock_guard lock(mutex);
|
||||
const auto it = outputs.find(handle);
|
||||
require(it != outputs.end() && it->second == output, "video output registration mismatch");
|
||||
outputs.erase(it);
|
||||
}
|
||||
|
||||
void CheckFailure() {
|
||||
std::lock_guard lock(mutex);
|
||||
rethrowFailure();
|
||||
}
|
||||
|
||||
void ReportFailure(std::exception_ptr error) {
|
||||
require(error != nullptr, "null asynchronous failure");
|
||||
{
|
||||
std::lock_guard lock(mutex);
|
||||
if (!failure) failure = error;
|
||||
for (const auto& [handle, output] : outputs) output->Fail(failure);
|
||||
for (const auto& item : pending) {
|
||||
for (const auto& [offset, flip] : item.flips) flip->Fail(failure);
|
||||
}
|
||||
pending.clear();
|
||||
}
|
||||
changed.notify_all();
|
||||
}
|
||||
|
||||
void PresentClear(const PresentationWindow& window, bool opaque, void (*gpuReady)(void*), void* context) {
|
||||
CheckFailure();
|
||||
require(gpuReady != nullptr && context != nullptr, "missing GPU completion callback");
|
||||
std::shared_ptr<VulkanDevice> presenting;
|
||||
std::uint64_t id = 0;
|
||||
try {
|
||||
{
|
||||
std::lock_guard lock(gpuMutex);
|
||||
if (device == nullptr || device->Window() == nullptr) {
|
||||
if (device) device->WaitIdle();
|
||||
device = std::make_shared<VulkanDevice>(&window);
|
||||
}
|
||||
require(device->Window() == window.context, "presentation window does not match device surface");
|
||||
presenting = device;
|
||||
id = presenting->PresentClear(window.width, window.height, opaque);
|
||||
}
|
||||
gpuReady(context);
|
||||
presenting->WaitPresented(id);
|
||||
CheckFailure();
|
||||
} catch (...) {
|
||||
ReportFailure(std::current_exception());
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
void ReleaseWindow(void* window) {
|
||||
std::lock_guard lock(gpuMutex);
|
||||
if (device && device->Window() == window) device.reset();
|
||||
}
|
||||
|
||||
void RegisterShader(const Shader* shader) {
|
||||
CheckFailure();
|
||||
checkRange(shader, sizeof(Shader), alignof(Shader));
|
||||
|
|
@ -185,18 +291,29 @@ public:
|
|||
|
||||
private:
|
||||
std::mutex mutex;
|
||||
std::mutex shutdownMutex;
|
||||
std::condition_variable changed;
|
||||
std::deque<Submission> pending;
|
||||
std::map<std::uint64_t, std::shared_ptr<const ShaderSnapshot>> shaders;
|
||||
std::map<std::uint32_t, QueueState> queues;
|
||||
std::unique_ptr<VulkanDevice> device;
|
||||
std::map<std::uint32_t, std::shared_ptr<IVideoOutput>> outputs;
|
||||
std::mutex gpuMutex;
|
||||
std::shared_ptr<VulkanDevice> device;
|
||||
std::uint64_t accepted = 0;
|
||||
std::uint64_t completed = 0;
|
||||
std::exception_ptr failure;
|
||||
bool stopping = false;
|
||||
bool resetGraphics = false;
|
||||
std::thread worker;
|
||||
|
||||
Driver() : worker([this] { run(); }) {}
|
||||
Driver() : worker([this] { run(); }) {
|
||||
try {
|
||||
LibcRegisterShutdown_nid_postfix([] { Driver::Get().Shutdown(); });
|
||||
} catch (...) {
|
||||
stop();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
void rethrowFailure() const {
|
||||
if (failure != nullptr) {
|
||||
|
|
@ -210,6 +327,11 @@ private:
|
|||
require((header & 0xc0000000u) == 0xc0000000u, "unsupported PM4 packet type");
|
||||
const auto count = static_cast<std::size_t>((header >> 16u) & 0x3fffu) + 2;
|
||||
require(count <= commands.size() - cursor, "truncated PM4 packet");
|
||||
if (header == FlipPacketHeader) {
|
||||
require(count == FlipPacketWords && queue == 0, "invalid flip packet size or queue");
|
||||
cursor += count;
|
||||
continue;
|
||||
}
|
||||
require((header & 0xffu) == 0, "PM4 header flags or internal extension are not implemented");
|
||||
const auto packet = commands.subspan(cursor, count);
|
||||
const auto opcode = (header >> 8u) & 0xffu;
|
||||
|
|
@ -236,6 +358,7 @@ private:
|
|||
}
|
||||
|
||||
void dispatch(QueueState& queue, std::span<const std::uint32_t> packet, const Submission& submission) {
|
||||
std::lock_guard gpuLock(gpuMutex);
|
||||
const auto address = (static_cast<std::uint64_t>(readRegister(queue.shader, 0x20c)) << 8u) | (static_cast<std::uint64_t>(readRegister(queue.shader, 0x20d) & 0xffu) << 40u);
|
||||
auto it = submission.shaders.upper_bound(address);
|
||||
require(it != submission.shaders.begin(), "compute program does not belong to a registered shader");
|
||||
|
|
@ -253,7 +376,7 @@ private:
|
|||
auto userConfigRegisters = registerValues(queue.userConfig);
|
||||
const std::array<ShaderRecompiler::MemoryRegion, 2> memory{{{snapshot.codeAddress, std::as_bytes(std::span(snapshot.code))}, {snapshot.headerAddress, snapshot.header}}};
|
||||
if (device == nullptr) {
|
||||
device = std::make_unique<VulkanDevice>();
|
||||
device = std::make_shared<VulkanDevice>();
|
||||
}
|
||||
const auto codeOffset = static_cast<std::size_t>((address - snapshot.codeAddress) / sizeof(std::uint32_t));
|
||||
const ShaderRecompiler::RecompileRequest request{
|
||||
|
|
@ -267,13 +390,32 @@ private:
|
|||
}
|
||||
|
||||
void execute(const Submission& submission) {
|
||||
if (submission.suspend) {
|
||||
std::lock_guard gpuLock(gpuMutex);
|
||||
if (device != nullptr) device->WaitIdle();
|
||||
resetGraphics = true;
|
||||
return;
|
||||
}
|
||||
if (submission.queue == 0 && resetGraphics) {
|
||||
queues.erase(0);
|
||||
resetGraphics = false;
|
||||
}
|
||||
auto& queue = queues[submission.queue];
|
||||
for (std::size_t cursor = 0; cursor < submission.commands.size();) {
|
||||
CheckFailure();
|
||||
const auto header = submission.commands[cursor];
|
||||
const auto count = static_cast<std::size_t>((header >> 16u) & 0x3fffu) + 2;
|
||||
const auto packet = std::span(submission.commands).subspan(cursor, count);
|
||||
switch ((header >> 8u) & 0xffu) {
|
||||
case 0x10:
|
||||
if (header == FlipPacketHeader) {
|
||||
{
|
||||
std::lock_guard gpuLock(gpuMutex);
|
||||
if (device != nullptr) device->WaitIdle();
|
||||
}
|
||||
CheckFailure();
|
||||
submission.flips.at(cursor)->GpuReady();
|
||||
}
|
||||
break;
|
||||
case 0x69:
|
||||
case 0x76:
|
||||
|
|
@ -296,12 +438,14 @@ private:
|
|||
}
|
||||
|
||||
void run() noexcept {
|
||||
Submission submission;
|
||||
try {
|
||||
for (;;) {
|
||||
Submission submission;
|
||||
submission = Submission{};
|
||||
{
|
||||
std::unique_lock lock(mutex);
|
||||
changed.wait(lock, [&] { return stopping || !pending.empty(); });
|
||||
changed.wait(lock, [&] { return failure || stopping || !pending.empty(); });
|
||||
rethrowFailure();
|
||||
if (pending.empty()) {
|
||||
break;
|
||||
}
|
||||
|
|
@ -311,23 +455,21 @@ private:
|
|||
execute(submission);
|
||||
{
|
||||
std::lock_guard lock(mutex);
|
||||
rethrowFailure();
|
||||
completed = submission.serial;
|
||||
}
|
||||
changed.notify_all();
|
||||
}
|
||||
std::lock_guard gpuLock(gpuMutex);
|
||||
device.reset();
|
||||
} catch (...) {
|
||||
const auto error = std::current_exception();
|
||||
device.reset();
|
||||
for (const auto& [offset, flip] : submission.flips) flip->Fail(error);
|
||||
ReportFailure(error);
|
||||
{
|
||||
std::lock_guard lock(mutex);
|
||||
failure = error;
|
||||
if (failure == nullptr) {
|
||||
std::terminate();
|
||||
}
|
||||
pending.clear();
|
||||
std::lock_guard gpuLock(gpuMutex);
|
||||
device.reset();
|
||||
}
|
||||
changed.notify_all();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -346,6 +488,30 @@ void RegisterShader(const Shader* shader) {
|
|||
Driver::Get().RegisterShader(shader);
|
||||
}
|
||||
|
||||
void SuspendPoint() {
|
||||
Driver::Get().SuspendPoint();
|
||||
}
|
||||
|
||||
void RegisterVideoOutput(std::uint32_t handle, const std::shared_ptr<IVideoOutput>& output) {
|
||||
Driver::Get().RegisterVideoOutput(handle, output);
|
||||
}
|
||||
|
||||
void UnregisterVideoOutput(std::uint32_t handle, const std::shared_ptr<IVideoOutput>& output) {
|
||||
Driver::Get().UnregisterVideoOutput(handle, output);
|
||||
}
|
||||
|
||||
void PresentClear(const PresentationWindow& window, bool opaque, void (*gpuReady)(void*), void* context) {
|
||||
Driver::Get().PresentClear(window, opaque, gpuReady, context);
|
||||
}
|
||||
|
||||
void ReleaseWindow(void* window) {
|
||||
Driver::Get().ReleaseWindow(window);
|
||||
}
|
||||
|
||||
void ReportFailure(std::exception_ptr error) {
|
||||
Driver::Get().ReportFailure(error);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extern "C" void AgcDriverWaitIdle_nid_postfix() {
|
||||
|
|
@ -355,3 +521,27 @@ extern "C" void AgcDriverWaitIdle_nid_postfix() {
|
|||
extern "C" void AgcDriverRegisterShader_nid_postfix(const Shader* shader) {
|
||||
AgcDriver::RegisterShader(shader);
|
||||
}
|
||||
|
||||
extern "C" void AgcDriverSuspendPoint_nid_postfix() {
|
||||
AgcDriver::SuspendPoint();
|
||||
}
|
||||
|
||||
extern "C" void AgcDriverRegisterVideoOutput_nid_postfix(std::uint32_t handle, const std::shared_ptr<AgcDriver::IVideoOutput>& output) {
|
||||
AgcDriver::RegisterVideoOutput(handle, output);
|
||||
}
|
||||
|
||||
extern "C" void AgcDriverUnregisterVideoOutput_nid_postfix(std::uint32_t handle, const std::shared_ptr<AgcDriver::IVideoOutput>& output) {
|
||||
AgcDriver::UnregisterVideoOutput(handle, output);
|
||||
}
|
||||
|
||||
extern "C" void AgcDriverPresentClear_nid_postfix(const AgcDriver::PresentationWindow& window, bool opaque, void (*gpuReady)(void*), void* context) {
|
||||
AgcDriver::PresentClear(window, opaque, gpuReady, context);
|
||||
}
|
||||
|
||||
extern "C" void AgcDriverReleaseWindow_nid_postfix(void* window) {
|
||||
AgcDriver::ReleaseWindow(window);
|
||||
}
|
||||
|
||||
extern "C" void AgcDriverReportFailure_nid_postfix(std::exception_ptr error) {
|
||||
AgcDriver::ReportFailure(error);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
#include <SDL_loadso.h>
|
||||
#include <SDL_error.h>
|
||||
#include <array>
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
|
@ -16,6 +18,10 @@ void check(VkResult result, const char* operation) {
|
|||
}
|
||||
}
|
||||
|
||||
void require(bool condition, const char* reason) {
|
||||
if (!condition) throw std::runtime_error(std::string("Vulkan presentation: ") + reason);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct VulkanDevice::State {
|
||||
|
|
@ -26,6 +32,18 @@ struct VulkanDevice::State {
|
|||
VkDevice device = VK_NULL_HANDLE;
|
||||
VkQueue queue = VK_NULL_HANDLE;
|
||||
VkCommandPool pool = VK_NULL_HANDLE;
|
||||
void* window = nullptr;
|
||||
VkSurfaceKHR surface = VK_NULL_HANDLE;
|
||||
VkSwapchainKHR swapchain = VK_NULL_HANDLE;
|
||||
VkExtent2D extent{};
|
||||
std::vector<VkImage> images;
|
||||
VkFence acquireFence = VK_NULL_HANDLE;
|
||||
VkFence renderFence = VK_NULL_HANDLE;
|
||||
VkFence presentFence = VK_NULL_HANDLE;
|
||||
VkSemaphore rendered = VK_NULL_HANDLE;
|
||||
VkCommandBuffer clearCommands = VK_NULL_HANDLE;
|
||||
std::uint64_t presentId = 0;
|
||||
bool presentQueued = false;
|
||||
VkPhysicalDeviceProperties properties{};
|
||||
VkPhysicalDeviceSubgroupProperties subgroup{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES};
|
||||
std::array<std::uint32_t, 1> capabilities{1};
|
||||
|
|
@ -50,6 +68,18 @@ struct VulkanDevice::State {
|
|||
|
||||
~State() {
|
||||
if (device != VK_NULL_HANDLE) {
|
||||
const auto idle = reinterpret_cast<PFN_vkDeviceWaitIdle>(deviceProc(device, "vkDeviceWaitIdle"))(device);
|
||||
if (idle != VK_SUCCESS && idle != VK_ERROR_DEVICE_LOST) std::terminate();
|
||||
if (presentQueued && idle != VK_ERROR_DEVICE_LOST) {
|
||||
const auto result = reinterpret_cast<PFN_vkWaitForFences>(deviceProc(device, "vkWaitForFences"))(device, 1, &presentFence, VK_TRUE, std::numeric_limits<std::uint64_t>::max());
|
||||
if (result != VK_SUCCESS && result != VK_ERROR_DEVICE_LOST) std::terminate();
|
||||
}
|
||||
const auto destroyFence = reinterpret_cast<PFN_vkDestroyFence>(deviceProc(device, "vkDestroyFence"));
|
||||
if (acquireFence) destroyFence(device, acquireFence, nullptr);
|
||||
if (renderFence) destroyFence(device, renderFence, nullptr);
|
||||
if (presentFence) destroyFence(device, presentFence, nullptr);
|
||||
if (rendered) reinterpret_cast<PFN_vkDestroySemaphore>(deviceProc(device, "vkDestroySemaphore"))(device, rendered, nullptr);
|
||||
if (swapchain) reinterpret_cast<PFN_vkDestroySwapchainKHR>(deviceProc(device, "vkDestroySwapchainKHR"))(device, swapchain, nullptr);
|
||||
const auto destroyPool = reinterpret_cast<PFN_vkDestroyCommandPool>(deviceProc(device, "vkDestroyCommandPool"));
|
||||
const auto destroyDevice = reinterpret_cast<PFN_vkDestroyDevice>(deviceProc(device, "vkDestroyDevice"));
|
||||
if (pool != VK_NULL_HANDLE) {
|
||||
|
|
@ -58,6 +88,7 @@ struct VulkanDevice::State {
|
|||
destroyDevice(device, nullptr);
|
||||
}
|
||||
if (instance != VK_NULL_HANDLE) {
|
||||
if (surface) reinterpret_cast<PFN_vkDestroySurfaceKHR>(instanceProc(instance, "vkDestroySurfaceKHR"))(instance, surface, nullptr);
|
||||
reinterpret_cast<PFN_vkDestroyInstance>(instanceProc(instance, "vkDestroyInstance"))(instance, nullptr);
|
||||
}
|
||||
if (library != nullptr) {
|
||||
|
|
@ -66,7 +97,7 @@ struct VulkanDevice::State {
|
|||
}
|
||||
};
|
||||
|
||||
VulkanDevice::VulkanDevice() : state(std::make_unique<State>()) {
|
||||
VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_unique<State>()) {
|
||||
#ifdef _WIN32
|
||||
state->library = SDL_LoadObject("vulkan-1.dll");
|
||||
#else
|
||||
|
|
@ -84,7 +115,32 @@ VulkanDevice::VulkanDevice() : state(std::make_unique<State>()) {
|
|||
application.apiVersion = VK_API_VERSION_1_1;
|
||||
VkInstanceCreateInfo create{VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO};
|
||||
create.pApplicationInfo = &application;
|
||||
std::vector<const char*> instanceExtensions;
|
||||
if (window != nullptr) {
|
||||
require(window->context && window->createSurface && window->width && window->height, "invalid window descriptor");
|
||||
instanceExtensions.assign(window->extensions.begin(), window->extensions.end());
|
||||
instanceExtensions.push_back(VK_KHR_GET_SURFACE_CAPABILITIES_2_EXTENSION_NAME);
|
||||
instanceExtensions.push_back(VK_EXT_SURFACE_MAINTENANCE_1_EXTENSION_NAME);
|
||||
std::uint32_t availableCount = 0;
|
||||
const auto enumerateExtensions = state->InstanceFunction<PFN_vkEnumerateInstanceExtensionProperties>("vkEnumerateInstanceExtensionProperties");
|
||||
check(enumerateExtensions(nullptr, &availableCount, nullptr), "vkEnumerateInstanceExtensionProperties");
|
||||
std::vector<VkExtensionProperties> available(availableCount);
|
||||
check(enumerateExtensions(nullptr, &availableCount, available.data()), "vkEnumerateInstanceExtensionProperties");
|
||||
for (const auto* name : instanceExtensions) {
|
||||
require(name != nullptr, "null instance extension");
|
||||
if (std::none_of(available.begin(), available.end(), [&](const auto& item) { return std::strcmp(item.extensionName, name) == 0; })) {
|
||||
throw std::runtime_error(std::string("Vulkan presentation: required instance extension missing: ") + name);
|
||||
}
|
||||
}
|
||||
create.enabledExtensionCount = static_cast<std::uint32_t>(instanceExtensions.size());
|
||||
create.ppEnabledExtensionNames = instanceExtensions.data();
|
||||
}
|
||||
check(state->InstanceFunction<PFN_vkCreateInstance>("vkCreateInstance")(&create, nullptr, &state->instance), "vkCreateInstance");
|
||||
if (window != nullptr) {
|
||||
state->surface = window->createSurface(window->context, state->instance);
|
||||
require(state->surface != VK_NULL_HANDLE, "window returned a null surface");
|
||||
state->window = window->context;
|
||||
}
|
||||
state->deviceProc = state->InstanceFunction<PFN_vkGetDeviceProcAddr>("vkGetDeviceProcAddr");
|
||||
const auto enumerate = state->InstanceFunction<PFN_vkEnumeratePhysicalDevices>("vkEnumeratePhysicalDevices");
|
||||
std::uint32_t count = 0;
|
||||
|
|
@ -94,12 +150,33 @@ VulkanDevice::VulkanDevice() : state(std::make_unique<State>()) {
|
|||
devices.resize(count);
|
||||
VkPhysicalDevice selected = VK_NULL_HANDLE;
|
||||
std::uint32_t family = 0;
|
||||
const std::array<const char*, 4> presentationExtensions{VK_KHR_SWAPCHAIN_EXTENSION_NAME, VK_KHR_PRESENT_ID_EXTENSION_NAME, VK_KHR_PRESENT_WAIT_EXTENSION_NAME, VK_EXT_SWAPCHAIN_MAINTENANCE_1_EXTENSION_NAME};
|
||||
for (auto physical : devices) {
|
||||
VkPhysicalDeviceProperties properties{};
|
||||
state->InstanceFunction<PFN_vkGetPhysicalDeviceProperties>("vkGetPhysicalDeviceProperties")(physical, &properties);
|
||||
if (properties.apiVersion < VK_API_VERSION_1_1) {
|
||||
continue;
|
||||
}
|
||||
if (window != nullptr) {
|
||||
std::uint32_t extensionCount = 0;
|
||||
auto enumerateExtensions = state->InstanceFunction<PFN_vkEnumerateDeviceExtensionProperties>("vkEnumerateDeviceExtensionProperties");
|
||||
check(enumerateExtensions(physical, nullptr, &extensionCount, nullptr), "vkEnumerateDeviceExtensionProperties");
|
||||
std::vector<VkExtensionProperties> extensions(extensionCount);
|
||||
check(enumerateExtensions(physical, nullptr, &extensionCount, extensions.data()), "vkEnumerateDeviceExtensionProperties");
|
||||
const bool supported = std::all_of(presentationExtensions.begin(), presentationExtensions.end(), [&](const char* name) {
|
||||
return std::any_of(extensions.begin(), extensions.end(), [&](const auto& item) { return std::strcmp(item.extensionName, name) == 0; });
|
||||
});
|
||||
if (!supported) continue;
|
||||
VkPhysicalDevicePresentIdFeaturesKHR id{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PRESENT_ID_FEATURES_KHR};
|
||||
VkPhysicalDevicePresentWaitFeaturesKHR wait{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PRESENT_WAIT_FEATURES_KHR};
|
||||
VkPhysicalDeviceSwapchainMaintenance1FeaturesEXT maintenance{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SWAPCHAIN_MAINTENANCE_1_FEATURES_EXT};
|
||||
VkPhysicalDeviceFeatures2 features{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2};
|
||||
features.pNext = &id;
|
||||
id.pNext = &wait;
|
||||
wait.pNext = &maintenance;
|
||||
state->InstanceFunction<PFN_vkGetPhysicalDeviceFeatures2>("vkGetPhysicalDeviceFeatures2")(physical, &features);
|
||||
if (!id.presentId || !wait.presentWait || !maintenance.swapchainMaintenance1) continue;
|
||||
}
|
||||
std::uint32_t families = 0;
|
||||
auto getFamilies = state->InstanceFunction<PFN_vkGetPhysicalDeviceQueueFamilyProperties>("vkGetPhysicalDeviceQueueFamilyProperties");
|
||||
getFamilies(physical, &families, nullptr);
|
||||
|
|
@ -107,6 +184,11 @@ VulkanDevice::VulkanDevice() : state(std::make_unique<State>()) {
|
|||
getFamilies(physical, &families, queues.data());
|
||||
for (std::uint32_t i = 0; i < families; ++i) {
|
||||
if (queues[i].queueCount != 0 && (queues[i].queueFlags & (VK_QUEUE_GRAPHICS_BIT | VK_QUEUE_COMPUTE_BIT)) == (VK_QUEUE_GRAPHICS_BIT | VK_QUEUE_COMPUTE_BIT)) {
|
||||
if (window != nullptr) {
|
||||
VkBool32 supported = VK_FALSE;
|
||||
check(state->InstanceFunction<PFN_vkGetPhysicalDeviceSurfaceSupportKHR>("vkGetPhysicalDeviceSurfaceSupportKHR")(physical, i, state->surface, &supported), "vkGetPhysicalDeviceSurfaceSupportKHR");
|
||||
if (!supported) continue;
|
||||
}
|
||||
selected = physical;
|
||||
family = i;
|
||||
break;
|
||||
|
|
@ -117,7 +199,7 @@ VulkanDevice::VulkanDevice() : state(std::make_unique<State>()) {
|
|||
}
|
||||
}
|
||||
if (selected == VK_NULL_HANDLE) {
|
||||
throw std::runtime_error("Vulkan: no Vulkan 1.1 graphics and compute queue");
|
||||
throw std::runtime_error(window ? "Vulkan: no presentation device with graphics, compute, present_id, present_wait and swapchain_maintenance1" : "Vulkan: no Vulkan 1.1 graphics and compute queue");
|
||||
}
|
||||
VkPhysicalDeviceProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2};
|
||||
properties.pNext = &state->subgroup;
|
||||
|
|
@ -131,15 +213,152 @@ VulkanDevice::VulkanDevice() : state(std::make_unique<State>()) {
|
|||
VkDeviceCreateInfo deviceInfo{VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO};
|
||||
deviceInfo.queueCreateInfoCount = 1;
|
||||
deviceInfo.pQueueCreateInfos = &queueInfo;
|
||||
VkPhysicalDevicePresentIdFeaturesKHR idFeature{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PRESENT_ID_FEATURES_KHR, nullptr, VK_TRUE};
|
||||
VkPhysicalDevicePresentWaitFeaturesKHR waitFeature{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PRESENT_WAIT_FEATURES_KHR, nullptr, VK_TRUE};
|
||||
VkPhysicalDeviceSwapchainMaintenance1FeaturesEXT maintenanceFeature{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SWAPCHAIN_MAINTENANCE_1_FEATURES_EXT, nullptr, VK_TRUE};
|
||||
if (window != nullptr) {
|
||||
deviceInfo.enabledExtensionCount = static_cast<std::uint32_t>(presentationExtensions.size());
|
||||
deviceInfo.ppEnabledExtensionNames = presentationExtensions.data();
|
||||
deviceInfo.pNext = &idFeature;
|
||||
idFeature.pNext = &waitFeature;
|
||||
waitFeature.pNext = &maintenanceFeature;
|
||||
}
|
||||
check(state->InstanceFunction<PFN_vkCreateDevice>("vkCreateDevice")(selected, &deviceInfo, nullptr, &state->device), "vkCreateDevice");
|
||||
state->DeviceFunction<PFN_vkGetDeviceQueue>("vkGetDeviceQueue")(state->device, family, 0, &state->queue);
|
||||
VkCommandPoolCreateInfo poolInfo{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO};
|
||||
poolInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
|
||||
poolInfo.queueFamilyIndex = family;
|
||||
check(state->DeviceFunction<PFN_vkCreateCommandPool>("vkCreateCommandPool")(state->device, &poolInfo, nullptr, &state->pool), "vkCreateCommandPool");
|
||||
if (window != nullptr) {
|
||||
VkSurfaceCapabilitiesKHR surface{};
|
||||
check(state->InstanceFunction<PFN_vkGetPhysicalDeviceSurfaceCapabilitiesKHR>("vkGetPhysicalDeviceSurfaceCapabilitiesKHR")(selected, state->surface, &surface), "vkGetPhysicalDeviceSurfaceCapabilitiesKHR");
|
||||
state->extent = {window->width, window->height};
|
||||
require(surface.currentExtent.width == std::numeric_limits<std::uint32_t>::max() || (surface.currentExtent.width == window->width && surface.currentExtent.height == window->height), "window extent differs from requested output");
|
||||
require(window->width >= surface.minImageExtent.width && window->width <= surface.maxImageExtent.width && window->height >= surface.minImageExtent.height && window->height <= surface.maxImageExtent.height, "unsupported output extent");
|
||||
require((surface.supportedUsageFlags & VK_IMAGE_USAGE_TRANSFER_DST_BIT) != 0, "surface does not support transfer destination images");
|
||||
require((surface.supportedCompositeAlpha & VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR) != 0, "opaque composition is unavailable");
|
||||
require((surface.supportedTransforms & VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR) != 0, "identity surface transform is unavailable");
|
||||
std::uint32_t formatCount = 0;
|
||||
auto getFormats = state->InstanceFunction<PFN_vkGetPhysicalDeviceSurfaceFormatsKHR>("vkGetPhysicalDeviceSurfaceFormatsKHR");
|
||||
check(getFormats(selected, state->surface, &formatCount, nullptr), "vkGetPhysicalDeviceSurfaceFormatsKHR");
|
||||
std::vector<VkSurfaceFormatKHR> formats(formatCount);
|
||||
check(getFormats(selected, state->surface, &formatCount, formats.data()), "vkGetPhysicalDeviceSurfaceFormatsKHR");
|
||||
require(std::any_of(formats.begin(), formats.end(), [](const auto& format) { return format.format == VK_FORMAT_B8G8R8A8_UNORM && format.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR; }), "BGRA8 sRGB-nonlinear surface format is unavailable");
|
||||
VkSwapchainCreateInfoKHR swapchain{VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR};
|
||||
swapchain.surface = state->surface;
|
||||
swapchain.minImageCount = surface.minImageCount;
|
||||
swapchain.imageFormat = VK_FORMAT_B8G8R8A8_UNORM;
|
||||
swapchain.imageColorSpace = VK_COLOR_SPACE_SRGB_NONLINEAR_KHR;
|
||||
swapchain.imageExtent = state->extent;
|
||||
swapchain.imageArrayLayers = 1;
|
||||
swapchain.imageUsage = VK_IMAGE_USAGE_TRANSFER_DST_BIT;
|
||||
swapchain.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE;
|
||||
swapchain.preTransform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR;
|
||||
swapchain.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
|
||||
swapchain.presentMode = VK_PRESENT_MODE_FIFO_KHR;
|
||||
swapchain.clipped = VK_FALSE;
|
||||
check(state->DeviceFunction<PFN_vkCreateSwapchainKHR>("vkCreateSwapchainKHR")(state->device, &swapchain, nullptr, &state->swapchain), "vkCreateSwapchainKHR");
|
||||
std::uint32_t imageCount = 0;
|
||||
auto getImages = state->DeviceFunction<PFN_vkGetSwapchainImagesKHR>("vkGetSwapchainImagesKHR");
|
||||
check(getImages(state->device, state->swapchain, &imageCount, nullptr), "vkGetSwapchainImagesKHR");
|
||||
state->images.resize(imageCount);
|
||||
check(getImages(state->device, state->swapchain, &imageCount, state->images.data()), "vkGetSwapchainImagesKHR");
|
||||
VkFenceCreateInfo fence{VK_STRUCTURE_TYPE_FENCE_CREATE_INFO};
|
||||
for (auto* destination : {&state->acquireFence, &state->renderFence, &state->presentFence}) {
|
||||
check(state->DeviceFunction<PFN_vkCreateFence>("vkCreateFence")(state->device, &fence, nullptr, destination), "vkCreateFence");
|
||||
}
|
||||
VkSemaphoreCreateInfo semaphore{VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO};
|
||||
check(state->DeviceFunction<PFN_vkCreateSemaphore>("vkCreateSemaphore")(state->device, &semaphore, nullptr, &state->rendered), "vkCreateSemaphore");
|
||||
VkCommandBufferAllocateInfo allocation{VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO};
|
||||
allocation.commandPool = state->pool;
|
||||
allocation.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
|
||||
allocation.commandBufferCount = 1;
|
||||
check(state->DeviceFunction<PFN_vkAllocateCommandBuffers>("vkAllocateCommandBuffers")(state->device, &allocation, &state->clearCommands), "vkAllocateCommandBuffers");
|
||||
}
|
||||
}
|
||||
|
||||
VulkanDevice::~VulkanDevice() = default;
|
||||
|
||||
void VulkanDevice::WaitIdle() {
|
||||
check(state->DeviceFunction<PFN_vkDeviceWaitIdle>("vkDeviceWaitIdle")(state->device), "vkDeviceWaitIdle");
|
||||
}
|
||||
|
||||
void* VulkanDevice::Window() const {
|
||||
return state->window;
|
||||
}
|
||||
|
||||
std::uint64_t VulkanDevice::PresentClear(std::uint32_t width, std::uint32_t height, bool opaque) {
|
||||
require(state->swapchain != VK_NULL_HANDLE, "device has no swapchain");
|
||||
require(width == state->extent.width && height == state->extent.height, "output resize is not implemented");
|
||||
require(!state->presentQueued, "previous presentation has not completed");
|
||||
require(state->presentId != std::numeric_limits<std::uint64_t>::max(), "presentation ID overflow");
|
||||
auto wait = state->DeviceFunction<PFN_vkWaitForFences>("vkWaitForFences");
|
||||
auto reset = state->DeviceFunction<PFN_vkResetFences>("vkResetFences");
|
||||
const std::array<VkFence, 3> fences{state->acquireFence, state->renderFence, state->presentFence};
|
||||
check(reset(state->device, static_cast<std::uint32_t>(fences.size()), fences.data()), "vkResetFences");
|
||||
std::uint32_t index = 0;
|
||||
check(state->DeviceFunction<PFN_vkAcquireNextImageKHR>("vkAcquireNextImageKHR")(state->device, state->swapchain, 5'000'000'000ULL, VK_NULL_HANDLE, state->acquireFence, &index), "vkAcquireNextImageKHR");
|
||||
check(wait(state->device, 1, &state->acquireFence, VK_TRUE, std::numeric_limits<std::uint64_t>::max()), "vkWaitForFences acquire");
|
||||
require(index < state->images.size(), "acquired image index is out of range");
|
||||
auto commands = state->clearCommands;
|
||||
check(state->DeviceFunction<PFN_vkResetCommandBuffer>("vkResetCommandBuffer")(commands, 0), "vkResetCommandBuffer");
|
||||
VkCommandBufferBeginInfo begin{VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO};
|
||||
begin.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
|
||||
check(state->DeviceFunction<PFN_vkBeginCommandBuffer>("vkBeginCommandBuffer")(commands, &begin), "vkBeginCommandBuffer");
|
||||
VkImageMemoryBarrier barrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER};
|
||||
barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
||||
barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
|
||||
barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
||||
barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
||||
barrier.image = state->images[index];
|
||||
barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
|
||||
auto pipelineBarrier = state->DeviceFunction<PFN_vkCmdPipelineBarrier>("vkCmdPipelineBarrier");
|
||||
pipelineBarrier(commands, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier);
|
||||
VkClearColorValue clear{};
|
||||
clear.float32[3] = opaque ? 1.0f : 0.0f;
|
||||
state->DeviceFunction<PFN_vkCmdClearColorImage>("vkCmdClearColorImage")(commands, barrier.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &clear, 1, &barrier.subresourceRange);
|
||||
barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
||||
barrier.dstAccessMask = 0;
|
||||
barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
|
||||
barrier.newLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
|
||||
pipelineBarrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier);
|
||||
check(state->DeviceFunction<PFN_vkEndCommandBuffer>("vkEndCommandBuffer")(commands), "vkEndCommandBuffer");
|
||||
VkSubmitInfo submit{VK_STRUCTURE_TYPE_SUBMIT_INFO};
|
||||
submit.commandBufferCount = 1;
|
||||
submit.pCommandBuffers = &commands;
|
||||
submit.signalSemaphoreCount = 1;
|
||||
submit.pSignalSemaphores = &state->rendered;
|
||||
check(state->DeviceFunction<PFN_vkQueueSubmit>("vkQueueSubmit")(state->queue, 1, &submit, state->renderFence), "vkQueueSubmit clear");
|
||||
check(wait(state->device, 1, &state->renderFence, VK_TRUE, std::numeric_limits<std::uint64_t>::max()), "vkWaitForFences clear");
|
||||
++state->presentId;
|
||||
VkSwapchainPresentFenceInfoEXT completion{VK_STRUCTURE_TYPE_SWAPCHAIN_PRESENT_FENCE_INFO_EXT};
|
||||
completion.swapchainCount = 1;
|
||||
completion.pFences = &state->presentFence;
|
||||
VkPresentIdKHR id{VK_STRUCTURE_TYPE_PRESENT_ID_KHR};
|
||||
id.pNext = &completion;
|
||||
id.swapchainCount = 1;
|
||||
id.pPresentIds = &state->presentId;
|
||||
VkPresentInfoKHR present{VK_STRUCTURE_TYPE_PRESENT_INFO_KHR};
|
||||
present.pNext = &id;
|
||||
present.waitSemaphoreCount = 1;
|
||||
present.pWaitSemaphores = &state->rendered;
|
||||
present.swapchainCount = 1;
|
||||
present.pSwapchains = &state->swapchain;
|
||||
present.pImageIndices = &index;
|
||||
const auto result = state->DeviceFunction<PFN_vkQueuePresentKHR>("vkQueuePresentKHR")(state->queue, &present);
|
||||
state->presentQueued = result == VK_SUCCESS || result == VK_SUBOPTIMAL_KHR || result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_ERROR_SURFACE_LOST_KHR || result == VK_ERROR_FULL_SCREEN_EXCLUSIVE_MODE_LOST_EXT;
|
||||
check(result, "vkQueuePresentKHR");
|
||||
return state->presentId;
|
||||
}
|
||||
|
||||
void VulkanDevice::WaitPresented(std::uint64_t id) {
|
||||
require(state->presentQueued && id == state->presentId, "invalid presentation wait");
|
||||
check(state->DeviceFunction<PFN_vkWaitForPresentKHR>("vkWaitForPresentKHR")(state->device, state->swapchain, id, 5'000'000'000ULL), "vkWaitForPresentKHR");
|
||||
check(state->DeviceFunction<PFN_vkWaitForFences>("vkWaitForFences")(state->device, 1, &state->presentFence, VK_TRUE, std::numeric_limits<std::uint64_t>::max()), "vkWaitForFences presentation resources");
|
||||
state->presentQueued = false;
|
||||
}
|
||||
|
||||
ShaderRecompiler::SpirvTarget VulkanDevice::Target() const {
|
||||
const auto& limits = state->properties.limits;
|
||||
return {VK_API_VERSION_1_1, 0x00010300u, state->subgroup.subgroupSize, state->capabilities, {}, {limits.maxComputeWorkGroupSize[0], limits.maxComputeWorkGroupSize[1], limits.maxComputeWorkGroupSize[2]}, limits.maxComputeWorkGroupInvocations, limits.maxComputeSharedMemorySize};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,193 @@
|
|||
#include "prx/libSceAgcDriver/Execution/include/Driver.hpp"
|
||||
#include "prx/libc/include/Shutdown.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/VideoOutput.hpp"
|
||||
#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp"
|
||||
#include "prx/libSceAgcDriver/Submit/include/Acb.hpp"
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <cstdio>
|
||||
#include <future>
|
||||
#include <mutex>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
void check(bool condition, const char* reason) {
|
||||
if (!condition) throw std::runtime_error(reason);
|
||||
}
|
||||
|
||||
template<typename TAction>
|
||||
std::string expectFailure(TAction action) {
|
||||
try { action(); }
|
||||
catch (const std::runtime_error& error) { return error.what(); }
|
||||
throw std::runtime_error("expected an exception");
|
||||
}
|
||||
|
||||
struct State {
|
||||
std::mutex mutex;
|
||||
std::condition_variable changed;
|
||||
bool block = false;
|
||||
bool entered = false;
|
||||
bool fail = false;
|
||||
bool checkSelfWait = false;
|
||||
std::atomic<int> alive = 0;
|
||||
std::atomic<int> ready = 0;
|
||||
std::atomic<int> failed = 0;
|
||||
AgcDriver::FlipInfo last{};
|
||||
};
|
||||
|
||||
class Request final : public AgcDriver::IFlipRequest {
|
||||
public:
|
||||
explicit Request(std::shared_ptr<State> value) : state(std::move(value)) { ++state->alive; }
|
||||
~Request() override { --state->alive; }
|
||||
void GpuReady() override {
|
||||
std::unique_lock lock(state->mutex);
|
||||
if (state->checkSelfWait) {
|
||||
check(expectFailure([] { AgcDriverSuspendPoint_nid_postfix(); }).find("itself") != std::string::npos, "self suspend was not rejected");
|
||||
check(expectFailure([] { AgcDriverWaitIdle_nid_postfix(); }).find("itself") != std::string::npos, "self wait was not rejected");
|
||||
}
|
||||
state->entered = true;
|
||||
state->changed.notify_all();
|
||||
state->changed.wait(lock, [&] { return !state->block; });
|
||||
if (state->fail) throw std::runtime_error("intentional flip failure");
|
||||
++state->ready;
|
||||
}
|
||||
void Fail(std::exception_ptr error) noexcept override {
|
||||
if (!error) std::terminate();
|
||||
++state->failed;
|
||||
}
|
||||
private:
|
||||
std::shared_ptr<State> state;
|
||||
};
|
||||
|
||||
class Output final : public AgcDriver::IVideoOutput {
|
||||
public:
|
||||
void Fail(std::exception_ptr error) noexcept override { if (!error) std::terminate(); }
|
||||
std::shared_ptr<State> state = std::make_shared<State>();
|
||||
std::shared_ptr<AgcDriver::IFlipRequest> Reserve(const AgcDriver::FlipInfo& info) override {
|
||||
std::lock_guard lock(state->mutex);
|
||||
state->last = info;
|
||||
return std::make_shared<Request>(state);
|
||||
}
|
||||
};
|
||||
|
||||
void submitFlip(std::uint32_t handle = 7) {
|
||||
std::array<std::uint32_t, 6> words{0xc004105c, handle, 0xfffffffeu, 1, 0x76543211u, 0xfedcba98u};
|
||||
Packet packet{words.data(), static_cast<std::uint32_t>(words.size()), 0, {}};
|
||||
check(sceAgcDriverSubmitDcb(&packet) == 0, "flip submission failed");
|
||||
}
|
||||
|
||||
void testFlipAndBoundary() {
|
||||
auto output = std::make_shared<Output>();
|
||||
AgcDriverRegisterVideoOutput_nid_postfix(7, output);
|
||||
expectFailure([&] { AgcDriverRegisterVideoOutput_nid_postfix(7, output); });
|
||||
std::array<std::uint32_t, 6> words{0xc004105c, 7, 0xfffffffeu, 1, 0, 0};
|
||||
Packet packet{words.data(), 6, 0, {}};
|
||||
expectFailure([&] { sceAgcDriverSubmitAcb(0x20, &packet); });
|
||||
words[0] = 0xc004105d;
|
||||
expectFailure([&] { sceAgcDriverSubmitDcb(&packet); });
|
||||
words[0] = 0xc004105c;
|
||||
packet.dw_num = 5;
|
||||
expectFailure([&] { sceAgcDriverSubmitDcb(&packet); });
|
||||
expectFailure([] { submitFlip(8); });
|
||||
std::array<std::uint32_t, 12> rollback{0xc004105c, 7, 0, 1, 0, 0, 0xc004105c, 8, 0, 1, 0, 0};
|
||||
Packet rejected{rollback.data(), 12, 0, {}};
|
||||
expectFailure([&] { sceAgcDriverSubmitDcb(&rejected); });
|
||||
check(output->state->alive == 0 && output->state->ready == 0, "rejected submission retained or executed a reservation");
|
||||
{
|
||||
std::lock_guard lock(output->state->mutex);
|
||||
output->state->block = true;
|
||||
output->state->checkSelfWait = true;
|
||||
}
|
||||
submitFlip();
|
||||
{
|
||||
std::unique_lock lock(output->state->mutex);
|
||||
check(output->state->changed.wait_for(lock, std::chrono::seconds(5), [&] { return output->state->entered; }), "worker did not reach flip");
|
||||
check(output->state->last.argument == -0x123456789abcdefLL && output->state->last.index == -2, "decoded flip arguments changed");
|
||||
}
|
||||
auto boundary = std::async(std::launch::async, [] { AgcDriverSuspendPoint_nid_postfix(); });
|
||||
check(boundary.wait_for(std::chrono::milliseconds(30)) == std::future_status::timeout, "suspend completed before preceding work");
|
||||
AgcDriverUnregisterVideoOutput_nid_postfix(7, output);
|
||||
auto replacement = std::make_shared<Output>();
|
||||
AgcDriverRegisterVideoOutput_nid_postfix(7, replacement);
|
||||
submitFlip();
|
||||
{
|
||||
std::lock_guard lock(output->state->mutex);
|
||||
output->state->block = false;
|
||||
}
|
||||
output->state->changed.notify_all();
|
||||
boundary.get();
|
||||
AgcDriverWaitIdle_nid_postfix();
|
||||
check(output->state->ready == 1 && replacement->state->ready == 1, "registration lifetime or FIFO was lost");
|
||||
check(output->state->failed == 0, "successful request failed");
|
||||
std::vector<std::thread> producers;
|
||||
std::array<std::exception_ptr, 4> errors{};
|
||||
for (std::size_t i = 0; i < errors.size(); ++i) {
|
||||
producers.emplace_back([&, i] {
|
||||
try {
|
||||
for (int j = 0; j < 50; ++j) {
|
||||
submitFlip();
|
||||
if (j % 5 == 0) AgcDriverSuspendPoint_nid_postfix();
|
||||
}
|
||||
} catch (...) { errors[i] = std::current_exception(); }
|
||||
});
|
||||
}
|
||||
for (auto& producer : producers) producer.join();
|
||||
for (auto error : errors) if (error) std::rethrow_exception(error);
|
||||
AgcDriverSuspendPoint_nid_postfix();
|
||||
check(replacement->state->ready == 201, "concurrent submissions were lost");
|
||||
AgcDriverUnregisterVideoOutput_nid_postfix(7, replacement);
|
||||
}
|
||||
|
||||
void testFailure() {
|
||||
auto output = std::make_shared<Output>();
|
||||
output->state->fail = true;
|
||||
AgcDriverRegisterVideoOutput_nid_postfix(7, output);
|
||||
submitFlip();
|
||||
std::array<std::string, 4> messages;
|
||||
std::vector<std::thread> waiters;
|
||||
for (auto& message : messages) waiters.emplace_back([&message] { message = expectFailure([] { AgcDriverSuspendPoint_nid_postfix(); }); });
|
||||
for (auto& waiter : waiters) waiter.join();
|
||||
for (auto& message : messages) check(message == "intentional flip failure", "asynchronous failure was lost");
|
||||
check(expectFailure([] { AgcDriverWaitIdle_nid_postfix(); }) == messages[0], "idle lost flip failure");
|
||||
check(expectFailure([] { submitFlip(); }) == messages[0], "submit lost flip failure");
|
||||
check(output->state->ready == 0 && output->state->failed == 1, "failed flip was completed successfully");
|
||||
AgcDriverUnregisterVideoOutput_nid_postfix(7, output);
|
||||
}
|
||||
|
||||
void testReset(bool compute) {
|
||||
std::array<std::uint32_t, 4> registers{0xc0027600, 0x20c, 1, 0};
|
||||
Packet packet{registers.data(), 4, 0, {}};
|
||||
if (compute) sceAgcDriverSubmitAcb(0x20, &packet);
|
||||
else sceAgcDriverSubmitDcb(&packet);
|
||||
AgcDriverSuspendPoint_nid_postfix();
|
||||
std::array<std::uint32_t, 5> dispatch{0xc0031500, 1, 1, 1, 0x41};
|
||||
packet = Packet{dispatch.data(), 5, 0, {}};
|
||||
if (compute) sceAgcDriverSubmitAcb(0x20, &packet);
|
||||
else sceAgcDriverSubmitDcb(&packet);
|
||||
const auto message = expectFailure([] { AgcDriverWaitIdle_nid_postfix(); });
|
||||
check(message.find(compute ? "registered" : "required shader register") != std::string::npos, "suspend reset wrong queue state");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
try {
|
||||
if (argc == 2) testReset(std::string(argv[1]) == "compute");
|
||||
else { testFlipAndBoundary(); testFailure(); }
|
||||
const auto shutdown = expectFailure([] { LibcRunShutdown_nid_postfix(); });
|
||||
check(shutdown.find(argc == 2 ? (std::string(argv[1]) == "compute" ? "registered" : "required shader register") : "intentional flip failure") != std::string::npos, "shutdown lost worker failure");
|
||||
std::puts("AGC flip and suspend tests passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::fprintf(stderr, "%s\n", error.what());
|
||||
try { LibcRunShutdown_nid_postfix(); }
|
||||
catch (const std::exception& shutdown) { std::fprintf(stderr, "shutdown: %s\n", shutdown.what()); }
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
#include "prx/libSceAgcDriver/Execution/include/Driver.hpp"
|
||||
#include "prx/libc/include/Shutdown.hpp"
|
||||
#include "prx/libSceAgcDriver/Submit/include/Acb.hpp"
|
||||
#include <array>
|
||||
#include <cstdio>
|
||||
|
|
@ -30,11 +31,19 @@ int main() {
|
|||
throw;
|
||||
}
|
||||
std::puts("Vulkan device initialized; real recompiler exception propagated from compute dispatch");
|
||||
try {
|
||||
LibcRunShutdown_nid_postfix();
|
||||
throw std::runtime_error("shutdown lost recompiler failure");
|
||||
} catch (const std::runtime_error& shutdown) {
|
||||
if (std::string(shutdown.what()) != "ShaderRecompiler::Recompile not implemented") throw;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
throw std::runtime_error("dispatch unexpectedly completed without a recompiler");
|
||||
} catch (const std::exception& error) {
|
||||
std::fprintf(stderr, "%s\n", error.what());
|
||||
try { LibcRunShutdown_nid_postfix(); }
|
||||
catch (const std::exception& shutdown) { std::fprintf(stderr, "shutdown: %s\n", shutdown.what()); }
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,11 @@
|
|||
#include <mutex>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
#include <atomic>
|
||||
#include <exception>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include "prx/libSceAgcDriver/Execution/include/VideoOutput.hpp"
|
||||
|
||||
#include "SDL.h"
|
||||
#include "SceTypes.hpp"
|
||||
|
|
@ -88,6 +93,7 @@ struct VideoOutConfig {
|
|||
uint64_t generation = 0;
|
||||
bool opened = false;
|
||||
bool closing = false;
|
||||
std::exception_ptr failure;
|
||||
int flipRate = 0;
|
||||
uint64_t outputMode = VIDEO_OUT_OUTPUT_MODE_DEFAULT;
|
||||
float gamma = 1.0f;
|
||||
|
|
@ -98,14 +104,43 @@ struct VideoOutConfig {
|
|||
|
||||
std::array<VideoOutBuffer, VIDEO_OUT_BUFFER_NUM_MAX> buffers{};
|
||||
std::array<BufferAttributeGroup, VIDEO_OUT_BUFFER_ATTRIBUTE_NUM_MAX> groups{};
|
||||
|
||||
void Check() const {
|
||||
if (failure) std::rethrow_exception(failure);
|
||||
if (!opened || closing) throw std::runtime_error("VideoOut: port is closed");
|
||||
}
|
||||
};
|
||||
|
||||
struct FlipRequest {
|
||||
VideoOutConfig* cfg = nullptr;
|
||||
struct FlipQueue;
|
||||
|
||||
struct FlipRequest final : AgcDriver::IFlipRequest, std::enable_shared_from_this<FlipRequest> {
|
||||
std::shared_ptr<VideoOutConfig> cfg;
|
||||
std::shared_ptr<FlipQueue> queue;
|
||||
uint64_t generation = 0;
|
||||
int index = 0;
|
||||
int flipMode = 0;
|
||||
int64_t flipArg = 0;
|
||||
uint32_t width = 0;
|
||||
uint32_t height = 0;
|
||||
VideoOutBuffer buffer;
|
||||
BufferAttributeGroup group;
|
||||
bool reserved = false;
|
||||
bool ready = false;
|
||||
bool gpuComplete = false;
|
||||
bool terminal = false;
|
||||
|
||||
~FlipRequest() override;
|
||||
void GpuReady() override;
|
||||
void Fail(std::exception_ptr error) noexcept override;
|
||||
};
|
||||
|
||||
struct FlipQueue {
|
||||
std::mutex mutex;
|
||||
std::condition_variable changed;
|
||||
std::list<std::shared_ptr<FlipRequest>> requests;
|
||||
std::atomic<std::size_t> reservations = 0;
|
||||
std::exception_ptr failure;
|
||||
bool stopping = false;
|
||||
};
|
||||
|
||||
class VideoOutDriver {
|
||||
|
|
@ -114,33 +149,34 @@ public:
|
|||
|
||||
VideoOutDriver();
|
||||
~VideoOutDriver();
|
||||
void Shutdown();
|
||||
|
||||
VideoOutDriver(const VideoOutDriver&) = delete;
|
||||
VideoOutDriver& operator=(const VideoOutDriver&) = delete;
|
||||
|
||||
int Open(int busType);
|
||||
bool Close(int handle);
|
||||
VideoOutConfig* GetConfig(int handle);
|
||||
std::shared_ptr<VideoOutConfig> GetConfig(int handle);
|
||||
bool IsOpen(int handle);
|
||||
|
||||
void SubmitFlip(VideoOutConfig* cfg, int index, int flipMode, int64_t flipArg);
|
||||
void SubmitFlip(int handle, int index, int flipMode, int64_t flipArg);
|
||||
|
||||
private:
|
||||
bool close(int handle);
|
||||
void presentLoop(std::stop_token token);
|
||||
void vblankBegin();
|
||||
void vblankEnd();
|
||||
void processFlip(const FlipRequest& req);
|
||||
void processFlip(FlipRequest& req);
|
||||
void triggerEvents(VideoOutConfig& cfg, int eventKind, void* triggerData);
|
||||
|
||||
std::mutex mutex;
|
||||
VideoOutConfig contexts[VIDEO_OUT_NUM_MAX];
|
||||
|
||||
std::mutex flipMutex;
|
||||
std::condition_variable flipCond;
|
||||
std::list<FlipRequest> flipQueue;
|
||||
std::mutex shutdownMutex;
|
||||
bool stopped = false;
|
||||
std::array<std::shared_ptr<VideoOutConfig>, VIDEO_OUT_NUM_MAX> contexts;
|
||||
std::array<std::shared_ptr<AgcDriver::IVideoOutput>, VIDEO_OUT_NUM_MAX> outputs;
|
||||
std::shared_ptr<FlipQueue> flipQueue = std::make_shared<FlipQueue>();
|
||||
|
||||
SDL_Window* window = nullptr;
|
||||
SDL_Surface* windowSurface = nullptr;
|
||||
|
||||
std::jthread presentThread;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,15 +17,15 @@ static std::vector<EventRegistration>* getEventList(VideoOutConfig& cfg, int16_t
|
|||
}
|
||||
|
||||
static int registerVideoOutEvent(int handle, KernelEqueue eq, int16_t eventKind, void* udata) {
|
||||
auto* cfg = VideoOutDriver::Get().GetConfig(handle);
|
||||
auto cfg = VideoOutDriver::Get().GetConfig(handle);
|
||||
if (cfg == nullptr) {
|
||||
return VIDEO_OUT_ERROR_INVALID_HANDLE;
|
||||
throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_HANDLE");
|
||||
}
|
||||
if (eq == 0) {
|
||||
return VIDEO_OUT_ERROR_INVALID_EVENT_QUEUE;
|
||||
throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_EVENT_QUEUE");
|
||||
}
|
||||
if (!EqueuePin_nid_postfix(eq)) {
|
||||
return VIDEO_OUT_ERROR_INVALID_EVENT_QUEUE;
|
||||
throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_EVENT_QUEUE");
|
||||
}
|
||||
KernelEqueueEvent event{};
|
||||
event.event.ident = static_cast<uintptr_t>(eventKind);
|
||||
|
|
@ -34,6 +34,7 @@ static int registerVideoOutEvent(int handle, KernelEqueue eq, int16_t eventKind,
|
|||
event.event.udata = udata;
|
||||
if (eventKind == VIDEO_OUT_EVENT_SET_MODE) {
|
||||
std::unique_lock lock(cfg->mutex);
|
||||
cfg->Check();
|
||||
event.triggered = true;
|
||||
event.event.fflags = 1;
|
||||
event.event.data = static_cast<intptr_t>(cfg->outputMode);
|
||||
|
|
@ -68,9 +69,10 @@ static int registerVideoOutEvent(int handle, KernelEqueue eq, int16_t eventKind,
|
|||
};
|
||||
const int result = EqueueAddEvent_nid_postfix(eq, event);
|
||||
if (result == EQUEUE_ERROR_EBADF) {
|
||||
return VIDEO_OUT_ERROR_INVALID_EVENT_QUEUE;
|
||||
throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_EVENT_QUEUE");
|
||||
}
|
||||
std::unique_lock lock(cfg->mutex);
|
||||
cfg->Check();
|
||||
EventRegistration reg;
|
||||
reg.eq = eq;
|
||||
reg.generation = cfg->generation;
|
||||
|
|
@ -80,20 +82,21 @@ static int registerVideoOutEvent(int handle, KernelEqueue eq, int16_t eventKind,
|
|||
|
||||
static int deleteVideoOutEvent(int handle, KernelEqueue eq, int16_t eventKind) {
|
||||
if (!VideoOutDriver::Get().IsOpen(handle)) {
|
||||
return VIDEO_OUT_ERROR_INVALID_HANDLE;
|
||||
throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_HANDLE");
|
||||
}
|
||||
if (eq == 0 || !EqueuePin_nid_postfix(eq)) {
|
||||
return VIDEO_OUT_ERROR_INVALID_EVENT_QUEUE;
|
||||
throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_EVENT_QUEUE");
|
||||
}
|
||||
auto* cfg = VideoOutDriver::Get().GetConfig(handle);
|
||||
auto cfg = VideoOutDriver::Get().GetConfig(handle);
|
||||
if (cfg != nullptr) {
|
||||
std::unique_lock lock(cfg->mutex);
|
||||
cfg->Check();
|
||||
auto* events = getEventList(*cfg, eventKind);
|
||||
events->erase(std::remove_if(events->begin(), events->end(), [eq](const EventRegistration& r) { return r.eq == eq; }), events->end());
|
||||
}
|
||||
const int result = EqueueDeleteEvent_nid_postfix(eq, static_cast<uintptr_t>(eventKind), EVFILT_VIDEO_OUT);
|
||||
if (result == EQUEUE_ERROR_EBADF) {
|
||||
return VIDEO_OUT_ERROR_INVALID_EVENT_QUEUE;
|
||||
throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_EVENT_QUEUE");
|
||||
}
|
||||
return (result == EQUEUE_ERROR_ENOENT) ? 0 : result;
|
||||
}
|
||||
|
|
@ -130,24 +133,24 @@ int APS5_VABI sceVideoOutDeletePreVblankStartEvent(KernelEqueue eq, int handle)
|
|||
|
||||
int APS5_VABI sceVideoOutGetEventId(const KernelEvent* ev) {
|
||||
if (ev == nullptr) {
|
||||
return VIDEO_OUT_ERROR_INVALID_ADDRESS;
|
||||
throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_ADDRESS");
|
||||
}
|
||||
if (ev->filter != EVFILT_VIDEO_OUT) {
|
||||
return VIDEO_OUT_ERROR_INVALID_EVENT;
|
||||
throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_EVENT");
|
||||
}
|
||||
const int ident = static_cast<int>(ev->ident);
|
||||
if (ident != VIDEO_OUT_EVENT_FLIP && ident != VIDEO_OUT_EVENT_VBLANK && ident != VIDEO_OUT_EVENT_PRE_VBLANK_START && ident != VIDEO_OUT_EVENT_SET_MODE) {
|
||||
return VIDEO_OUT_ERROR_INVALID_EVENT;
|
||||
throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_EVENT");
|
||||
}
|
||||
return ident;
|
||||
}
|
||||
|
||||
int APS5_VABI sceVideoOutGetEventData(const KernelEvent* ev, int64_t* data) {
|
||||
if (ev == nullptr || data == nullptr) {
|
||||
return VIDEO_OUT_ERROR_INVALID_ADDRESS;
|
||||
throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_ADDRESS");
|
||||
}
|
||||
if (ev->filter != EVFILT_VIDEO_OUT) {
|
||||
return VIDEO_OUT_ERROR_INVALID_EVENT;
|
||||
throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_EVENT");
|
||||
}
|
||||
uint64_t eventData = static_cast<uint64_t>(ev->data) >> 16u;
|
||||
if (ev->ident == static_cast<uintptr_t>(VIDEO_OUT_EVENT_FLIP) && (static_cast<uint64_t>(ev->data) & 0x8000000000000000ULL) != 0) {
|
||||
|
|
@ -159,10 +162,10 @@ int APS5_VABI sceVideoOutGetEventData(const KernelEvent* ev, int64_t* data) {
|
|||
|
||||
int APS5_VABI sceVideoOutGetEventCount(const KernelEvent* ev) {
|
||||
if (ev == nullptr) {
|
||||
return VIDEO_OUT_ERROR_INVALID_ADDRESS;
|
||||
throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_ADDRESS");
|
||||
}
|
||||
if (ev->filter != EVFILT_VIDEO_OUT) {
|
||||
return VIDEO_OUT_ERROR_INVALID_EVENT;
|
||||
throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_EVENT");
|
||||
}
|
||||
return static_cast<int>((static_cast<uint64_t>(ev->data) >> 12u) & 0xfu);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,125 @@
|
|||
#include <bit>
|
||||
#include <chrono>
|
||||
#include <limits>
|
||||
#include <stdexcept>
|
||||
|
||||
#include "SDL.h"
|
||||
#include "SDL_vulkan.h"
|
||||
#include "prx/libkernel/Equeue/Equeue.hpp"
|
||||
#include "prx/libkernel/Time/include/Time.hpp"
|
||||
#include "prx/libSceVideoOut/include/VideoOutDriver.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/Driver.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/Presentation.hpp"
|
||||
#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp"
|
||||
#include "prx/libc/include/Shutdown.hpp"
|
||||
|
||||
static constexpr uint32_t VBLANK_FREQ_DEFAULT = 60;
|
||||
static constexpr uint32_t VBLANK_FREQ_119 = 120;
|
||||
namespace {
|
||||
|
||||
void require(bool condition, const char* reason) {
|
||||
if (!condition) throw std::runtime_error(std::string("VideoOut: ") + reason);
|
||||
}
|
||||
|
||||
void checkConfig(const VideoOutConfig& cfg) {
|
||||
cfg.Check();
|
||||
}
|
||||
|
||||
class VideoOutput final : public AgcDriver::IVideoOutput {
|
||||
public:
|
||||
VideoOutput(std::shared_ptr<VideoOutConfig> config, std::shared_ptr<FlipQueue> requests) : cfg(std::move(config)), queue(std::move(requests)) {}
|
||||
|
||||
std::shared_ptr<AgcDriver::IFlipRequest> Reserve(const AgcDriver::FlipInfo& info) override {
|
||||
require(info.mode == VIDEO_OUT_FLIP_MODE_VSYNC, "unsupported flip mode");
|
||||
require(info.index >= VIDEO_OUT_BUFFER_INDEX_BLACK && info.index < VIDEO_OUT_BUFFER_NUM_MAX, "invalid flip index");
|
||||
auto request = std::make_shared<FlipRequest>();
|
||||
request->cfg = cfg;
|
||||
request->queue = queue;
|
||||
request->index = info.index;
|
||||
request->flipMode = static_cast<int>(info.mode);
|
||||
request->flipArg = info.argument;
|
||||
std::lock_guard queueLock(queue->mutex);
|
||||
if (queue->failure) std::rethrow_exception(queue->failure);
|
||||
require(!queue->stopping, "flip during shutdown");
|
||||
std::lock_guard lock(cfg->mutex);
|
||||
checkConfig(*cfg);
|
||||
require(queue->reservations.load() < VIDEO_OUT_FLIP_QUEUE_CAPACITY, "flip queue full");
|
||||
if (info.index >= 0) {
|
||||
request->buffer = cfg->buffers[info.index];
|
||||
require(request->buffer.Occupied(), "flip buffer is not registered");
|
||||
require(request->buffer.groupIndex < VIDEO_OUT_BUFFER_ATTRIBUTE_NUM_MAX, "invalid buffer group");
|
||||
request->group = cfg->groups[request->buffer.groupIndex];
|
||||
require(request->group.occupied, "buffer group is not registered");
|
||||
require(request->buffer.dataAddress != 0, "null registered buffer address");
|
||||
}
|
||||
request->generation = cfg->generation;
|
||||
request->width = cfg->width;
|
||||
request->height = cfg->height;
|
||||
++queue->reservations;
|
||||
++cfg->flipStatus.flipPendingNum;
|
||||
request->reserved = true;
|
||||
return request;
|
||||
}
|
||||
|
||||
void Fail(std::exception_ptr error) noexcept override {
|
||||
if (!error) std::terminate();
|
||||
{
|
||||
std::lock_guard lock(queue->mutex);
|
||||
if (!queue->failure) queue->failure = error;
|
||||
}
|
||||
{
|
||||
std::lock_guard lock(cfg->mutex);
|
||||
if (!cfg->failure) cfg->failure = error;
|
||||
cfg->vblankCond.notify_all();
|
||||
}
|
||||
queue->changed.notify_all();
|
||||
}
|
||||
|
||||
private:
|
||||
std::shared_ptr<VideoOutConfig> cfg;
|
||||
std::shared_ptr<FlipQueue> queue;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
FlipRequest::~FlipRequest() {
|
||||
if (!reserved) return;
|
||||
std::lock_guard lock(cfg->mutex);
|
||||
if (!terminal) {
|
||||
--cfg->flipStatus.flipPendingNum;
|
||||
--queue->reservations;
|
||||
cfg->vblankCond.notify_all();
|
||||
}
|
||||
}
|
||||
|
||||
void FlipRequest::GpuReady() {
|
||||
{
|
||||
std::lock_guard queueLock(queue->mutex);
|
||||
if (queue->failure) std::rethrow_exception(queue->failure);
|
||||
require(!queue->stopping, "GPU flip during shutdown");
|
||||
std::lock_guard lock(cfg->mutex);
|
||||
checkConfig(*cfg);
|
||||
require(reserved && !ready && !terminal && cfg->generation == generation, "invalid flip readiness transition");
|
||||
queue->requests.push_back(shared_from_this());
|
||||
ready = true;
|
||||
}
|
||||
queue->changed.notify_all();
|
||||
std::unique_lock lock(cfg->mutex);
|
||||
cfg->vblankCond.wait(lock, [&] { return gpuComplete || cfg->failure || cfg->closing; });
|
||||
checkConfig(*cfg);
|
||||
require(gpuComplete, "flip preparation did not complete on GPU");
|
||||
}
|
||||
|
||||
void FlipRequest::Fail(std::exception_ptr error) noexcept {
|
||||
if (!error) std::terminate();
|
||||
std::lock_guard lock(cfg->mutex);
|
||||
if (!cfg->failure) cfg->failure = error;
|
||||
if (reserved && !terminal) {
|
||||
--cfg->flipStatus.flipPendingNum;
|
||||
--queue->reservations;
|
||||
terminal = true;
|
||||
}
|
||||
cfg->vblankCond.notify_all();
|
||||
queue->changed.notify_all();
|
||||
}
|
||||
|
||||
VideoOutDriver& VideoOutDriver::Get() {
|
||||
static VideoOutDriver instance;
|
||||
|
|
@ -18,273 +130,277 @@ VideoOutDriver::VideoOutDriver() {
|
|||
if (SDL_InitSubSystem(SDL_INIT_VIDEO) < 0) {
|
||||
throw std::runtime_error(std::string("SDL_InitSubSystem(VIDEO) failed: ") + SDL_GetError());
|
||||
}
|
||||
presentThread = std::jthread([this](std::stop_token token) { presentLoop(token); });
|
||||
try {
|
||||
AgcDriverWaitIdle_nid_postfix();
|
||||
presentThread = std::jthread([this](std::stop_token token) { presentLoop(token); });
|
||||
LibcRegisterShutdown_nid_postfix([] { VideoOutDriver::Get().Shutdown(); });
|
||||
} catch (...) {
|
||||
if (presentThread.joinable()) {
|
||||
presentThread.request_stop();
|
||||
flipQueue->changed.notify_all();
|
||||
presentThread.join();
|
||||
}
|
||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
VideoOutDriver::~VideoOutDriver() {
|
||||
if (presentThread.joinable()) {
|
||||
presentThread.request_stop();
|
||||
flipCond.notify_all();
|
||||
presentThread.join();
|
||||
if (!stopped) Shutdown();
|
||||
}
|
||||
|
||||
void VideoOutDriver::Shutdown() {
|
||||
require(std::this_thread::get_id() != presentThread.get_id(), "presentation thread cannot stop itself");
|
||||
std::lock_guard shutdownLock(shutdownMutex);
|
||||
if (stopped) return;
|
||||
{
|
||||
std::lock_guard lock(mutex);
|
||||
{
|
||||
std::lock_guard queueLock(flipQueue->mutex);
|
||||
flipQueue->stopping = true;
|
||||
}
|
||||
for (int handle = 1; handle < VIDEO_OUT_NUM_MAX; ++handle) {
|
||||
if (outputs[handle]) close(handle);
|
||||
}
|
||||
}
|
||||
presentThread.request_stop();
|
||||
flipQueue->changed.notify_all();
|
||||
presentThread.join();
|
||||
if (window != nullptr) {
|
||||
AgcDriverReleaseWindow_nid_postfix(window);
|
||||
SDL_DestroyWindow(window);
|
||||
window = nullptr;
|
||||
}
|
||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||
stopped = true;
|
||||
std::lock_guard lock(flipQueue->mutex);
|
||||
if (flipQueue->failure) std::rethrow_exception(flipQueue->failure);
|
||||
}
|
||||
|
||||
int VideoOutDriver::Open(int busType) {
|
||||
std::unique_lock lock(mutex);
|
||||
std::lock_guard lock(mutex);
|
||||
{
|
||||
std::lock_guard queueLock(flipQueue->mutex);
|
||||
if (flipQueue->failure) std::rethrow_exception(flipQueue->failure);
|
||||
require(!flipQueue->stopping, "open during shutdown");
|
||||
}
|
||||
const int handle = busType + 1;
|
||||
if (handle <= 0 || handle >= VIDEO_OUT_NUM_MAX) {
|
||||
return -1;
|
||||
}
|
||||
auto& cfg = contexts[handle];
|
||||
std::unique_lock cfgLock(cfg.mutex);
|
||||
if (cfg.opened) {
|
||||
return -1;
|
||||
}
|
||||
cfg.closing = false;
|
||||
cfg.opened = true;
|
||||
cfg.flipRate = 0;
|
||||
cfg.outputMode = VIDEO_OUT_OUTPUT_MODE_DEFAULT;
|
||||
cfg.gamma = 1.0f;
|
||||
cfg.flipStatus = VideoOutFlipStatus{};
|
||||
cfg.flipStatus.flipArg = -1;
|
||||
cfg.flipStatus.currentBuffer = -1;
|
||||
cfg.vblankStatus = VideoOutVblankStatus{};
|
||||
cfg.preVblankStatus = VideoOutVblankStatus{};
|
||||
for (auto& buf : cfg.buffers) {
|
||||
buf = VideoOutBuffer{};
|
||||
}
|
||||
for (auto& grp : cfg.groups) {
|
||||
grp = BufferAttributeGroup{};
|
||||
}
|
||||
cfg.flipEvents.clear();
|
||||
cfg.vblankEvents.clear();
|
||||
cfg.preVblankEvents.clear();
|
||||
cfg.outputModeEvents.clear();
|
||||
if (++cfg.generation == 0) {
|
||||
throw std::runtime_error("VideoOut port generation wrapped");
|
||||
require(handle > 0 && handle < VIDEO_OUT_NUM_MAX, "invalid output bus");
|
||||
auto previous = contexts[handle];
|
||||
uint64_t generation = 1;
|
||||
if (previous) {
|
||||
std::lock_guard cfgLock(previous->mutex);
|
||||
require(!previous->opened, "port already open");
|
||||
require(previous->generation != std::numeric_limits<uint64_t>::max(), "port generation overflow");
|
||||
generation = previous->generation + 1;
|
||||
}
|
||||
auto cfg = std::make_shared<VideoOutConfig>();
|
||||
cfg->generation = generation;
|
||||
cfg->opened = true;
|
||||
cfg->flipStatus.flipArg = -1;
|
||||
cfg->flipStatus.currentBuffer = -1;
|
||||
auto output = std::make_shared<VideoOutput>(cfg, flipQueue);
|
||||
AgcDriverRegisterVideoOutput_nid_postfix(static_cast<uint32_t>(handle), output);
|
||||
contexts[handle] = std::move(cfg);
|
||||
outputs[handle] = std::move(output);
|
||||
return handle;
|
||||
}
|
||||
|
||||
bool VideoOutDriver::Close(int handle) {
|
||||
std::unique_lock lock(mutex);
|
||||
if (handle <= 0 || handle >= VIDEO_OUT_NUM_MAX) {
|
||||
return false;
|
||||
}
|
||||
auto& cfg = contexts[handle];
|
||||
std::unique_lock cfgLock(cfg.mutex);
|
||||
if (!cfg.opened || cfg.closing) {
|
||||
return false;
|
||||
}
|
||||
cfg.opened = false;
|
||||
cfg.closing = true;
|
||||
if (++cfg.generation == 0) {
|
||||
throw std::runtime_error("VideoOut port generation wrapped");
|
||||
}
|
||||
for (auto& buf : cfg.buffers) {
|
||||
buf = VideoOutBuffer{};
|
||||
}
|
||||
for (auto& grp : cfg.groups) {
|
||||
grp = BufferAttributeGroup{};
|
||||
}
|
||||
cfg.flipRate = 0;
|
||||
cfg.flipEvents.clear();
|
||||
cfg.vblankEvents.clear();
|
||||
cfg.preVblankEvents.clear();
|
||||
cfg.outputModeEvents.clear();
|
||||
cfg.vblankCond.notify_all();
|
||||
std::lock_guard lock(mutex);
|
||||
return close(handle);
|
||||
}
|
||||
|
||||
bool VideoOutDriver::close(int handle) {
|
||||
require(handle > 0 && handle < VIDEO_OUT_NUM_MAX && outputs[handle] != nullptr, "invalid close handle");
|
||||
AgcDriverUnregisterVideoOutput_nid_postfix(static_cast<uint32_t>(handle), outputs[handle]);
|
||||
outputs[handle].reset();
|
||||
auto cfg = contexts[handle];
|
||||
std::lock_guard cfgLock(cfg->mutex);
|
||||
cfg->opened = false;
|
||||
cfg->closing = true;
|
||||
cfg->flipEvents.clear();
|
||||
cfg->vblankEvents.clear();
|
||||
cfg->preVblankEvents.clear();
|
||||
cfg->outputModeEvents.clear();
|
||||
cfg->vblankCond.notify_all();
|
||||
flipQueue->changed.notify_all();
|
||||
return true;
|
||||
}
|
||||
|
||||
VideoOutConfig* VideoOutDriver::GetConfig(int handle) {
|
||||
std::unique_lock lock(mutex);
|
||||
if (handle <= 0 || handle >= VIDEO_OUT_NUM_MAX || !contexts[handle].opened) {
|
||||
return nullptr;
|
||||
std::shared_ptr<VideoOutConfig> VideoOutDriver::GetConfig(int handle) {
|
||||
{
|
||||
std::lock_guard lock(flipQueue->mutex);
|
||||
if (flipQueue->failure) std::rethrow_exception(flipQueue->failure);
|
||||
require(!flipQueue->stopping, "output is shutting down");
|
||||
}
|
||||
return &contexts[handle];
|
||||
std::lock_guard lock(mutex);
|
||||
require(handle > 0 && handle < VIDEO_OUT_NUM_MAX && contexts[handle] != nullptr, "invalid output handle");
|
||||
auto cfg = contexts[handle];
|
||||
std::lock_guard cfgLock(cfg->mutex);
|
||||
checkConfig(*cfg);
|
||||
return cfg;
|
||||
}
|
||||
|
||||
bool VideoOutDriver::IsOpen(int handle) {
|
||||
std::unique_lock lock(mutex);
|
||||
return handle > 0 && handle < VIDEO_OUT_NUM_MAX && contexts[handle].opened;
|
||||
return GetConfig(handle) != nullptr;
|
||||
}
|
||||
|
||||
void VideoOutDriver::SubmitFlip(VideoOutConfig* cfg, int index, int flipMode, int64_t flipArg) {
|
||||
std::unique_lock lock(flipMutex);
|
||||
if (flipQueue.size() >= VIDEO_OUT_FLIP_QUEUE_CAPACITY) {
|
||||
throw std::runtime_error("VideoOut flip queue full");
|
||||
}
|
||||
FlipRequest req;
|
||||
req.cfg = cfg;
|
||||
req.generation = cfg->generation;
|
||||
req.index = index;
|
||||
req.flipMode = flipMode;
|
||||
req.flipArg = flipArg;
|
||||
{
|
||||
std::unique_lock cfgLock(cfg->mutex);
|
||||
cfg->flipStatus.flipPendingNum++;
|
||||
}
|
||||
flipQueue.push_back(req);
|
||||
flipCond.notify_one();
|
||||
void VideoOutDriver::SubmitFlip(int handle, int index, int flipMode, int64_t flipArg) {
|
||||
std::array<uint32_t, AgcDriver::FlipPacketWords> words{AgcDriver::FlipPacketHeader, static_cast<uint32_t>(handle), static_cast<uint32_t>(index), static_cast<uint32_t>(flipMode), static_cast<uint32_t>(static_cast<uint64_t>(flipArg)), static_cast<uint32_t>(static_cast<uint64_t>(flipArg) >> 32u)};
|
||||
Packet packet{words.data(), static_cast<uint32_t>(words.size()), 0, {}};
|
||||
const auto result = sceAgcDriverSubmitDcb(&packet);
|
||||
require(result == 0, "driver rejected flip submission");
|
||||
}
|
||||
|
||||
void VideoOutDriver::triggerEvents(VideoOutConfig& cfg, int eventKind, void* triggerData) {
|
||||
std::vector<EventRegistration>* events = nullptr;
|
||||
if (eventKind == VIDEO_OUT_EVENT_FLIP) {
|
||||
events = &cfg.flipEvents;
|
||||
} else if (eventKind == VIDEO_OUT_EVENT_VBLANK) {
|
||||
events = &cfg.vblankEvents;
|
||||
} else if (eventKind == VIDEO_OUT_EVENT_PRE_VBLANK_START) {
|
||||
events = &cfg.preVblankEvents;
|
||||
} else if (eventKind == VIDEO_OUT_EVENT_SET_MODE) {
|
||||
events = &cfg.outputModeEvents;
|
||||
} else {
|
||||
throw std::runtime_error("triggerEvents: unknown event kind");
|
||||
}
|
||||
for (auto& reg : *events) {
|
||||
if (reg.generation != cfg.generation) {
|
||||
continue;
|
||||
}
|
||||
if (eventKind == VIDEO_OUT_EVENT_FLIP) events = &cfg.flipEvents;
|
||||
else if (eventKind == VIDEO_OUT_EVENT_VBLANK) events = &cfg.vblankEvents;
|
||||
else if (eventKind == VIDEO_OUT_EVENT_PRE_VBLANK_START) events = &cfg.preVblankEvents;
|
||||
else if (eventKind == VIDEO_OUT_EVENT_SET_MODE) events = &cfg.outputModeEvents;
|
||||
else throw std::runtime_error("VideoOut: unknown event kind");
|
||||
for (const auto& reg : *events) {
|
||||
require(reg.generation == cfg.generation, "stale event registration");
|
||||
EqueueTriggerEvent_nid_postfix(reg.eq, static_cast<uintptr_t>(eventKind), EVFILT_VIDEO_OUT, triggerData);
|
||||
}
|
||||
}
|
||||
|
||||
void VideoOutDriver::vblankBegin() {
|
||||
std::unique_lock lock(mutex);
|
||||
for (int i = 1; i < VIDEO_OUT_NUM_MAX; i++) {
|
||||
auto& cfg = contexts[i];
|
||||
if (!cfg.opened) {
|
||||
continue;
|
||||
}
|
||||
std::unique_lock cfgLock(cfg.mutex);
|
||||
cfg.preVblankStatus.count++;
|
||||
cfg.preVblankStatus.processTime = sceKernelGetProcessTime();
|
||||
cfg.preVblankStatus.processTimeCounter = sceKernelGetProcessTimeCounter();
|
||||
triggerEvents(cfg, VIDEO_OUT_EVENT_PRE_VBLANK_START, reinterpret_cast<void*>(cfg.preVblankStatus.count));
|
||||
std::lock_guard lock(mutex);
|
||||
for (const auto& cfg : contexts) {
|
||||
if (!cfg) continue;
|
||||
std::lock_guard cfgLock(cfg->mutex);
|
||||
if (!cfg->opened || cfg->failure) continue;
|
||||
++cfg->preVblankStatus.count;
|
||||
cfg->preVblankStatus.processTime = sceKernelGetProcessTime();
|
||||
cfg->preVblankStatus.processTimeCounter = sceKernelGetProcessTimeCounter();
|
||||
triggerEvents(*cfg, VIDEO_OUT_EVENT_PRE_VBLANK_START, reinterpret_cast<void*>(cfg->preVblankStatus.count));
|
||||
}
|
||||
}
|
||||
|
||||
void VideoOutDriver::vblankEnd() {
|
||||
std::unique_lock lock(mutex);
|
||||
for (int i = 1; i < VIDEO_OUT_NUM_MAX; i++) {
|
||||
auto& cfg = contexts[i];
|
||||
if (!cfg.opened) {
|
||||
continue;
|
||||
}
|
||||
std::unique_lock cfgLock(cfg.mutex);
|
||||
cfg.vblankStatus.count++;
|
||||
cfg.vblankStatus.processTime = sceKernelGetProcessTime();
|
||||
cfg.vblankStatus.processTimeCounter = sceKernelGetProcessTimeCounter();
|
||||
triggerEvents(cfg, VIDEO_OUT_EVENT_VBLANK, reinterpret_cast<void*>(cfg.vblankStatus.count));
|
||||
cfg.vblankCond.notify_all();
|
||||
std::lock_guard lock(mutex);
|
||||
for (const auto& cfg : contexts) {
|
||||
if (!cfg) continue;
|
||||
std::lock_guard cfgLock(cfg->mutex);
|
||||
if (!cfg->opened || cfg->failure) continue;
|
||||
++cfg->vblankStatus.count;
|
||||
cfg->vblankStatus.processTime = sceKernelGetProcessTime();
|
||||
cfg->vblankStatus.processTimeCounter = sceKernelGetProcessTimeCounter();
|
||||
triggerEvents(*cfg, VIDEO_OUT_EVENT_VBLANK, reinterpret_cast<void*>(cfg->vblankStatus.count));
|
||||
cfg->vblankCond.notify_all();
|
||||
}
|
||||
}
|
||||
|
||||
void VideoOutDriver::processFlip(const FlipRequest& req) {
|
||||
VideoOutConfig* cfg = req.cfg;
|
||||
void VideoOutDriver::processFlip(FlipRequest& req) {
|
||||
{
|
||||
std::unique_lock cfgLock(cfg->mutex);
|
||||
if (!cfg->opened || cfg->closing || cfg->generation != req.generation) {
|
||||
cfg->flipStatus.flipPendingNum--;
|
||||
return;
|
||||
}
|
||||
std::lock_guard lock(req.cfg->mutex);
|
||||
checkConfig(*req.cfg);
|
||||
require(req.ready && !req.terminal && req.generation == req.cfg->generation, "stale or incomplete flip request");
|
||||
}
|
||||
const bool isBlank = req.index == VIDEO_OUT_BUFFER_INDEX_BLANK;
|
||||
const bool isBlack = req.index == VIDEO_OUT_BUFFER_INDEX_BLACK;
|
||||
if (isBlank || isBlack) {
|
||||
if (window != nullptr) {
|
||||
SDL_FillRect(windowSurface, nullptr, isBlack ? SDL_MapRGB(windowSurface->format, 0, 0, 0) : 0);
|
||||
SDL_UpdateWindowSurface(window);
|
||||
}
|
||||
} else {
|
||||
throw std::runtime_error("VideoOut Vulkan rendering not implemented");
|
||||
if (req.index >= 0) {
|
||||
const auto& attribute = req.group.attribute;
|
||||
require(attribute.reserved0 == 0 && attribute.pad0 == 0 && attribute.reserved1[0] == 0 && attribute.reserved1[1] == 0 && attribute.reserved1[2] == 0, "reserved buffer attribute bits are set");
|
||||
require(attribute.width != 0 && attribute.height != 0, "empty registered buffer");
|
||||
require(req.group.category == VIDEO_OUT_BUFFER_ATTRIBUTE_CATEGORY_UNCOMPRESSED && req.buffer.metadataAddress == 0 && attribute.dcc_control == 0 && attribute.dcc_cb_register_clear_color == 0, "DCC presentation is not implemented");
|
||||
throw std::runtime_error("VideoOut: guest render-target tiling and image materialization are not implemented");
|
||||
}
|
||||
std::unique_lock cfgLock(cfg->mutex);
|
||||
if (!cfg->opened || cfg->closing || cfg->generation != req.generation) {
|
||||
cfg->flipStatus.flipPendingNum--;
|
||||
return;
|
||||
require(req.width != 0 && req.height != 0 && req.width <= static_cast<uint32_t>(std::numeric_limits<int>::max()) && req.height <= static_cast<uint32_t>(std::numeric_limits<int>::max()), "invalid window dimensions");
|
||||
if (window == nullptr) {
|
||||
window = SDL_CreateWindow("PS5", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, static_cast<int>(req.width), static_cast<int>(req.height), SDL_WINDOW_SHOWN | SDL_WINDOW_VULKAN);
|
||||
if (window == nullptr) throw std::runtime_error(std::string("SDL_CreateWindow failed: ") + SDL_GetError());
|
||||
}
|
||||
cfg->flipStatus.count++;
|
||||
cfg->flipStatus.processTime = sceKernelGetProcessTime();
|
||||
cfg->flipStatus.processTimeCounter = sceKernelGetProcessTimeCounter();
|
||||
cfg->flipStatus.flipArg = req.flipArg;
|
||||
cfg->flipStatus.currentBuffer = req.index;
|
||||
cfg->flipStatus.flipPendingNum--;
|
||||
triggerEvents(*cfg, VIDEO_OUT_EVENT_FLIP, reinterpret_cast<void*>(req.flipArg));
|
||||
unsigned extensionCount = 0;
|
||||
if (!SDL_Vulkan_GetInstanceExtensions(window, &extensionCount, nullptr)) throw std::runtime_error(std::string("SDL_Vulkan_GetInstanceExtensions failed: ") + SDL_GetError());
|
||||
std::vector<const char*> extensions(extensionCount);
|
||||
if (!SDL_Vulkan_GetInstanceExtensions(window, &extensionCount, extensions.data())) throw std::runtime_error(std::string("SDL_Vulkan_GetInstanceExtensions failed: ") + SDL_GetError());
|
||||
extensions.resize(extensionCount);
|
||||
const AgcDriver::PresentationWindow target{window, extensions, [](void* context, VkInstance instance) {
|
||||
VkSurfaceKHR surface = VK_NULL_HANDLE;
|
||||
if (!SDL_Vulkan_CreateSurface(static_cast<SDL_Window*>(context), instance, &surface)) throw std::runtime_error(std::string("SDL_Vulkan_CreateSurface failed: ") + SDL_GetError());
|
||||
return surface;
|
||||
}, req.width, req.height};
|
||||
AgcDriverPresentClear_nid_postfix(target, req.index == VIDEO_OUT_BUFFER_INDEX_BLACK, [](void* context) {
|
||||
auto& request = *static_cast<FlipRequest*>(context);
|
||||
std::lock_guard lock(request.cfg->mutex);
|
||||
checkConfig(*request.cfg);
|
||||
require(!request.terminal && !request.gpuComplete, "invalid GPU completion transition");
|
||||
request.gpuComplete = true;
|
||||
request.cfg->vblankCond.notify_all();
|
||||
}, &req);
|
||||
std::lock_guard lock(req.cfg->mutex);
|
||||
checkConfig(*req.cfg);
|
||||
require(!req.terminal && req.cfg->generation == req.generation, "flip cancelled during presentation");
|
||||
require(req.cfg->flipStatus.count != std::numeric_limits<uint64_t>::max(), "flip counter overflow");
|
||||
triggerEvents(*req.cfg, VIDEO_OUT_EVENT_FLIP, reinterpret_cast<void*>(req.flipArg));
|
||||
++req.cfg->flipStatus.count;
|
||||
req.cfg->flipStatus.processTime = sceKernelGetProcessTime();
|
||||
req.cfg->flipStatus.processTimeCounter = sceKernelGetProcessTimeCounter();
|
||||
req.cfg->flipStatus.flipArg = req.flipArg;
|
||||
req.cfg->flipStatus.currentBuffer = req.index;
|
||||
--req.cfg->flipStatus.flipPendingNum;
|
||||
--req.queue->reservations;
|
||||
req.terminal = true;
|
||||
}
|
||||
|
||||
void VideoOutDriver::presentLoop(std::stop_token token) {
|
||||
while (!token.stop_requested()) {
|
||||
uint64_t outputMode = VIDEO_OUT_OUTPUT_MODE_DEFAULT;
|
||||
std::shared_ptr<FlipRequest> current;
|
||||
auto nextVblank = std::chrono::steady_clock::now();
|
||||
try {
|
||||
while (!token.stop_requested()) {
|
||||
{
|
||||
std::unique_lock lock(flipQueue->mutex);
|
||||
flipQueue->changed.wait_until(lock, nextVblank, [&] { return token.stop_requested() || flipQueue->failure || !flipQueue->requests.empty(); });
|
||||
if (flipQueue->failure) std::rethrow_exception(flipQueue->failure);
|
||||
if (token.stop_requested()) break;
|
||||
if (!flipQueue->requests.empty()) {
|
||||
current = std::move(flipQueue->requests.front());
|
||||
flipQueue->requests.pop_front();
|
||||
}
|
||||
}
|
||||
if (current) processFlip(*current);
|
||||
current.reset();
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (now >= nextVblank) {
|
||||
vblankBegin();
|
||||
vblankEnd();
|
||||
nextVblank = now + std::chrono::nanoseconds(1'000'000'000ULL / 60);
|
||||
}
|
||||
SDL_Event event;
|
||||
while (SDL_PollEvent(&event)) {
|
||||
require(event.type != SDL_QUIT, "window was closed");
|
||||
}
|
||||
}
|
||||
std::list<std::shared_ptr<FlipRequest>> cancelled;
|
||||
{
|
||||
std::unique_lock lock(mutex);
|
||||
for (int i = 1; i < VIDEO_OUT_NUM_MAX; i++) {
|
||||
if (contexts[i].opened) {
|
||||
outputMode = contexts[i].outputMode;
|
||||
break;
|
||||
}
|
||||
}
|
||||
std::lock_guard lock(flipQueue->mutex);
|
||||
cancelled.swap(flipQueue->requests);
|
||||
}
|
||||
const uint32_t freq = (outputMode == VIDEO_OUT_OUTPUT_MODE_119_88HZ) ? VBLANK_FREQ_119 : VBLANK_FREQ_DEFAULT;
|
||||
const auto period = std::chrono::nanoseconds(1'000'000'000ULL / freq);
|
||||
const auto frameStart = std::chrono::steady_clock::now();
|
||||
|
||||
vblankBegin();
|
||||
|
||||
FlipRequest req{};
|
||||
bool hasFlip = false;
|
||||
if (!cancelled.empty()) {
|
||||
auto error = std::make_exception_ptr(std::runtime_error("VideoOut: pending flip cancelled during shutdown"));
|
||||
for (auto& request : cancelled) request->Fail(error);
|
||||
}
|
||||
} catch (...) {
|
||||
auto error = std::current_exception();
|
||||
if (!error) std::terminate();
|
||||
if (current) current->Fail(error);
|
||||
std::list<std::shared_ptr<FlipRequest>> failed;
|
||||
{
|
||||
std::unique_lock lock(flipMutex);
|
||||
if (!flipQueue.empty()) {
|
||||
req = flipQueue.front();
|
||||
flipQueue.pop_front();
|
||||
hasFlip = true;
|
||||
}
|
||||
}
|
||||
if (hasFlip) {
|
||||
if (window == nullptr) {
|
||||
uint32_t w = 1920;
|
||||
uint32_t h = 1080;
|
||||
{
|
||||
std::unique_lock lock(mutex);
|
||||
if (req.cfg != nullptr) {
|
||||
std::unique_lock cfgLock(req.cfg->mutex);
|
||||
w = req.cfg->width;
|
||||
h = req.cfg->height;
|
||||
}
|
||||
}
|
||||
window = SDL_CreateWindow("PS5", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, static_cast<int>(w), static_cast<int>(h), SDL_WINDOW_SHOWN);
|
||||
if (window == nullptr) {
|
||||
throw std::runtime_error(std::string("SDL_CreateWindow failed: ") + SDL_GetError());
|
||||
}
|
||||
windowSurface = SDL_GetWindowSurface(window);
|
||||
if (windowSurface == nullptr) {
|
||||
throw std::runtime_error(std::string("SDL_GetWindowSurface failed: ") + SDL_GetError());
|
||||
}
|
||||
}
|
||||
processFlip(req);
|
||||
}
|
||||
|
||||
vblankEnd();
|
||||
|
||||
SDL_Event sdlEvent;
|
||||
while (SDL_PollEvent(&sdlEvent)) {
|
||||
if (sdlEvent.type == SDL_QUIT) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const auto elapsed = std::chrono::steady_clock::now() - frameStart;
|
||||
if (elapsed < period) {
|
||||
const auto sleepUs = std::chrono::duration_cast<std::chrono::microseconds>(period - elapsed).count();
|
||||
if (sleepUs > 0) {
|
||||
sceKernelUsleep_nid_postfix(static_cast<KernelUseconds>(sleepUs));
|
||||
std::lock_guard lock(flipQueue->mutex);
|
||||
flipQueue->failure = error;
|
||||
failed.swap(flipQueue->requests);
|
||||
}
|
||||
for (auto& request : failed) request->Fail(error);
|
||||
{
|
||||
std::lock_guard lock(mutex);
|
||||
for (auto& cfg : contexts) {
|
||||
if (!cfg) continue;
|
||||
std::lock_guard cfgLock(cfg->mutex);
|
||||
if (!cfg->failure) cfg->failure = error;
|
||||
cfg->vblankCond.notify_all();
|
||||
}
|
||||
}
|
||||
flipQueue->changed.notify_all();
|
||||
AgcDriverReportFailure_nid_postfix(error);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,133 @@
|
|||
#include "prx/libSceVideoOut/include/VideoOutDriver.hpp"
|
||||
#include "prx/libSceVideoOut/include/Buffer.hpp"
|
||||
#include "prx/libSceVideoOut/include/Output.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/Driver.hpp"
|
||||
#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp"
|
||||
#include "prx/libc/include/Shutdown.hpp"
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
|
||||
#undef main
|
||||
|
||||
namespace {
|
||||
|
||||
void check(bool condition, const char* reason) {
|
||||
if (!condition) throw std::runtime_error(reason);
|
||||
}
|
||||
|
||||
template<typename TAction>
|
||||
std::string expectFailure(TAction action) {
|
||||
try { action(); }
|
||||
catch (const std::runtime_error& error) { return error.what(); }
|
||||
throw std::runtime_error("expected an exception");
|
||||
}
|
||||
|
||||
class Gate final : public AgcDriver::IVideoOutput, public AgcDriver::IFlipRequest, public std::enable_shared_from_this<Gate> {
|
||||
public:
|
||||
std::mutex mutex;
|
||||
std::condition_variable changed;
|
||||
bool released = false;
|
||||
std::shared_ptr<AgcDriver::IFlipRequest> Reserve(const AgcDriver::FlipInfo&) override { return shared_from_this(); }
|
||||
void GpuReady() override {
|
||||
std::unique_lock lock(mutex);
|
||||
if (!changed.wait_for(lock, std::chrono::seconds(10), [&] { return released; })) throw std::runtime_error("test gate timed out");
|
||||
}
|
||||
void Fail(std::exception_ptr error) noexcept override { if (!error) std::terminate(); }
|
||||
void Release() {
|
||||
std::lock_guard lock(mutex);
|
||||
released = true;
|
||||
changed.notify_all();
|
||||
}
|
||||
};
|
||||
|
||||
void testLifetime(bool reopen) {
|
||||
const int handle = sceVideoOutOpen(255, 0, 0, nullptr);
|
||||
auto cfg = VideoOutDriver::Get().GetConfig(handle);
|
||||
auto gate = std::make_shared<Gate>();
|
||||
AgcDriverRegisterVideoOutput_nid_postfix(7, gate);
|
||||
std::array<uint32_t, 6> words{0xc004105c, 7, 0xfffffffeu, 1, 0, 0};
|
||||
Packet packet{words.data(), 6, 0, {}};
|
||||
sceAgcDriverSubmitDcb(&packet);
|
||||
alignas(65536) static std::array<std::byte, 65536> storage{};
|
||||
VideoOutBuffers buffer{storage.data(), nullptr, {nullptr, nullptr}};
|
||||
VideoOutBufferAttribute2 attribute{};
|
||||
attribute.width = 64;
|
||||
attribute.height = 64;
|
||||
sceVideoOutRegisterBuffers2(handle, 0, 0, &buffer, 1, &attribute, 0, nullptr);
|
||||
sceVideoOutSubmitFlip(handle, 0, 1, -9);
|
||||
{
|
||||
std::lock_guard lock(cfg->mutex);
|
||||
check(cfg->flipStatus.flipPendingNum == 1 && cfg->flipStatus.count == 0, "reservation status is wrong");
|
||||
}
|
||||
attribute.dcc_control = 1;
|
||||
sceVideoOutSubmitChangeBufferAttribute2(handle, 0, &attribute, nullptr);
|
||||
sceVideoOutUnregisterBuffers(handle, 0);
|
||||
std::shared_ptr<VideoOutConfig> replacement;
|
||||
if (reopen) {
|
||||
sceVideoOutClose(handle);
|
||||
check(sceVideoOutOpen(255, 0, 0, nullptr) == handle, "reopen changed handle");
|
||||
replacement = VideoOutDriver::Get().GetConfig(handle);
|
||||
check(replacement != cfg && replacement->generation > cfg->generation, "reopen reused old port state");
|
||||
}
|
||||
gate->Release();
|
||||
std::exception_ptr failure;
|
||||
{
|
||||
std::unique_lock lock(cfg->mutex);
|
||||
check(cfg->vblankCond.wait_for(lock, std::chrono::seconds(10), [&] { return cfg->failure != nullptr; }), "flip failure did not wake waiters");
|
||||
failure = cfg->failure;
|
||||
check(cfg->flipStatus.count == 0 && cfg->flipStatus.flipPendingNum == 0, "failed flip has successful or pending status");
|
||||
}
|
||||
const auto message = expectFailure([&] { std::rethrow_exception(failure); });
|
||||
check(message.find(reopen ? "closed" : "tiling") != std::string::npos, "flip used changed attributes or port generation");
|
||||
if (replacement) {
|
||||
std::lock_guard lock(replacement->mutex);
|
||||
check(replacement->flipStatus.flipPendingNum == 0 && replacement->flipStatus.count == 0, "old request changed new port counters");
|
||||
}
|
||||
check(expectFailure([&] { sceVideoOutWaitVblank(handle); }).find(reopen ? "closed" : "tiling") != std::string::npos, "VideoOut lost worker failure");
|
||||
AgcDriverUnregisterVideoOutput_nid_postfix(7, gate);
|
||||
sceVideoOutClose(handle);
|
||||
const auto shutdown = expectFailure([] { LibcRunShutdown_nid_postfix(); });
|
||||
check(shutdown.find(reopen ? "closed" : "tiling") != std::string::npos, "shutdown lost asynchronous error");
|
||||
}
|
||||
|
||||
void testPresentation() {
|
||||
const int handle = sceVideoOutOpen(255, 0, 0, nullptr);
|
||||
auto cfg = VideoOutDriver::Get().GetConfig(handle);
|
||||
{
|
||||
std::lock_guard lock(cfg->mutex);
|
||||
cfg->width = 64;
|
||||
cfg->height = 64;
|
||||
}
|
||||
for (int index : {-2, -1, -2}) {
|
||||
uint64_t target;
|
||||
{
|
||||
std::lock_guard lock(cfg->mutex);
|
||||
target = cfg->flipStatus.count + 1;
|
||||
}
|
||||
sceVideoOutSubmitFlip(handle, index, 1, -123456789);
|
||||
std::unique_lock lock(cfg->mutex);
|
||||
check(cfg->vblankCond.wait_for(lock, std::chrono::seconds(15), [&] { return cfg->failure || cfg->flipStatus.count == target; }), "presentation did not complete");
|
||||
if (cfg->failure) std::rethrow_exception(cfg->failure);
|
||||
check(cfg->flipStatus.flipArg == -123456789 && cfg->flipStatus.currentBuffer == index && cfg->flipStatus.flipPendingNum == 0, "presentation status is wrong");
|
||||
}
|
||||
sceVideoOutClose(handle);
|
||||
LibcRunShutdown_nid_postfix();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
try {
|
||||
if (argc == 2 && std::string(argv[1]) == "present") testPresentation();
|
||||
else testLifetime(argc == 2 && std::string(argv[1]) == "reopen");
|
||||
std::puts("VideoOut flip tests passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::fprintf(stderr, "%s\n", error.what());
|
||||
try { LibcRunShutdown_nid_postfix(); }
|
||||
catch (const std::exception& shutdown) { std::fprintf(stderr, "shutdown: %s\n", shutdown.what()); }
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -2,13 +2,63 @@
|
|||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <cerrno>
|
||||
#include <condition_variable>
|
||||
#include <exception>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
#include "prx/libc/include/Shutdown.hpp"
|
||||
|
||||
#include "prx/libc/include/General.hpp"
|
||||
#include "SceTypes.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
std::mutex shutdownMutex;
|
||||
std::condition_variable shutdownChanged;
|
||||
std::vector<void (*)()> shutdownCallbacks;
|
||||
std::thread::id shutdownThread;
|
||||
bool shutdownStarted = false;
|
||||
bool shutdownFinished = false;
|
||||
std::exception_ptr shutdownFailure;
|
||||
|
||||
}
|
||||
|
||||
extern "C" void LibcRegisterShutdown_nid_postfix(void (*callback)()) {
|
||||
std::lock_guard lock(shutdownMutex);
|
||||
if (callback == nullptr || shutdownStarted) throw std::runtime_error("libc: invalid shutdown registration");
|
||||
shutdownCallbacks.push_back(callback);
|
||||
}
|
||||
|
||||
extern "C" void LibcRunShutdown_nid_postfix() {
|
||||
std::unique_lock lock(shutdownMutex);
|
||||
if (shutdownStarted) {
|
||||
if (!shutdownFinished && shutdownThread == std::this_thread::get_id()) throw std::runtime_error("libc: recursive shutdown");
|
||||
shutdownChanged.wait(lock, [] { return shutdownFinished; });
|
||||
if (shutdownFailure) std::rethrow_exception(shutdownFailure);
|
||||
return;
|
||||
}
|
||||
shutdownStarted = true;
|
||||
shutdownThread = std::this_thread::get_id();
|
||||
auto callbacks = std::move(shutdownCallbacks);
|
||||
lock.unlock();
|
||||
std::exception_ptr error;
|
||||
for (auto it = callbacks.rbegin(); it != callbacks.rend(); ++it) {
|
||||
try { (*it)(); }
|
||||
catch (...) { if (!error) error = std::current_exception(); }
|
||||
}
|
||||
lock.lock();
|
||||
shutdownFailure = error;
|
||||
shutdownFinished = true;
|
||||
lock.unlock();
|
||||
shutdownChanged.notify_all();
|
||||
if (error) std::rethrow_exception(error);
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
|
||||
void APS5_VABI exit_nid_postfix(int code) {
|
||||
LibcRunShutdown_nid_postfix();
|
||||
std::exit(code);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue