680 lines
29 KiB
C++
680 lines
29 KiB
C++
#include <bit>
|
|
#include <chrono>
|
|
#include <cstdlib>
|
|
#include <cstdio>
|
|
#include <exception>
|
|
#include <thread>
|
|
#include <limits>
|
|
#include <stdexcept>
|
|
#include <string>
|
|
|
|
#include "SDL.h"
|
|
#include "SDL_vulkan.h"
|
|
#include "prx/libSceVideoOut/include/PadInput.hpp"
|
|
#include "prx/libSceVideoOut/include/MouseInput.hpp"
|
|
#include "prx/libSceVideoOut/include/KeyboardInput.hpp"
|
|
#include "prx/libScePad/include/PadState.hpp"
|
|
#include "prx/libkernel/Equeue/Equeue.hpp"
|
|
#include "prx/libkernel/Time/include/Time.hpp"
|
|
#include "prx/libSceVideoOut/include/VideoOutDriver.hpp"
|
|
#if APS5_ENABLE_TIMING_LOG
|
|
#include "prx/libSceVideoOut/include/FrameTimingLog.hpp"
|
|
#endif
|
|
#include "prx/libSceAgcDriver/Execution/include/Driver.hpp"
|
|
#include "prx/libSceAgcDriver/Execution/include/Presentation.hpp"
|
|
#include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp"
|
|
#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp"
|
|
#include "prx/libc/include/PreciseWait.hpp"
|
|
#include "prx/libc/include/Shutdown.hpp"
|
|
|
|
namespace {
|
|
|
|
constexpr std::uint64_t FLIP_ROOM_WAIT_MICROS = 60000000;
|
|
constexpr std::uint64_t PRESENT_POLL_MICROS = 10000;
|
|
|
|
void require(bool condition, const char* reason) {
|
|
if (!condition) throw std::runtime_error(std::string("VideoOut: ") + reason);
|
|
}
|
|
|
|
std::vector<const char*> windowExtensions(SDL_Window* window) {
|
|
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);
|
|
return extensions;
|
|
}
|
|
|
|
VkSurfaceKHR createWindowSurface(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;
|
|
}
|
|
|
|
void windowDrawableSize(void* context, std::uint32_t* width, std::uint32_t* height) {
|
|
if ((SDL_GetWindowFlags(static_cast<SDL_Window*>(context)) & SDL_WINDOW_MINIMIZED) != 0) {
|
|
*width = 0;
|
|
*height = 0;
|
|
return;
|
|
}
|
|
int drawableWidth = 0;
|
|
int drawableHeight = 0;
|
|
SDL_Vulkan_GetDrawableSize(static_cast<SDL_Window*>(context), &drawableWidth, &drawableHeight);
|
|
*width = drawableWidth > 0 ? static_cast<std::uint32_t>(drawableWidth) : 0;
|
|
*height = drawableHeight > 0 ? static_cast<std::uint32_t>(drawableHeight) : 0;
|
|
}
|
|
|
|
void checkConfig(const VideoOutConfig& cfg) {
|
|
cfg.Check();
|
|
}
|
|
|
|
void resolveFlipBuffer(FlipRequest& request, const VideoOutConfig& cfg) {
|
|
if (request.index < 0) {
|
|
request.width = cfg.width;
|
|
request.height = cfg.height;
|
|
return;
|
|
}
|
|
request.buffer = cfg.buffers[request.index];
|
|
if (!request.buffer.Occupied()) {
|
|
static std::array<std::atomic<bool>, VIDEO_OUT_BUFFER_NUM_MAX> reported{};
|
|
if (!reported[request.index].exchange(true)) std::fprintf(stderr, "[videoout] GPU flip of unregistered buffer %d on handle %u presents black\n", request.index, request.outputHandle);
|
|
request.unregistered = true;
|
|
request.width = cfg.width;
|
|
request.height = cfg.height;
|
|
return;
|
|
}
|
|
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");
|
|
static_cast<void>(DescribeVideoOutBuffer(request.buffer, request.group));
|
|
request.width = request.group.attribute.width;
|
|
request.height = request.group.attribute.height;
|
|
}
|
|
|
|
class RenderingWait final : public AgcDriver::IRenderingWait {
|
|
std::shared_ptr<VideoOutConfig> _config;
|
|
std::uint32_t _index;
|
|
std::uint64_t _ticket;
|
|
public:
|
|
RenderingWait(std::shared_ptr<VideoOutConfig> config, std::uint32_t index, std::uint64_t ticket)
|
|
: _config(std::move(config)), _index(index), _ticket(ticket) {}
|
|
void Wait() override {
|
|
std::unique_lock lock(_config->mutex);
|
|
_config->vblankCond.wait(lock, _config->shutdownToken, [&] {
|
|
return _config->failure || _config->Closed() ||
|
|
_config->bufferReuse[_index].IsComplete(_ticket);
|
|
});
|
|
_config->CheckAlive();
|
|
}
|
|
};
|
|
|
|
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::IRenderingWait> CaptureRenderingWait(std::uint32_t index) override {
|
|
std::lock_guard lock(cfg->mutex);
|
|
checkConfig(*cfg);
|
|
require(index < VIDEO_OUT_BUFFER_NUM_MAX && cfg->buffers[index].Occupied(), "wait buffer is not registered");
|
|
return std::make_shared<RenderingWait>(cfg, index, cfg->bufferReuse[index].Capture());
|
|
}
|
|
|
|
void WaitForFlipRoom() override {
|
|
std::unique_lock queueLock(queue->mutex);
|
|
const bool room = queue->changed.WaitUntil(queueLock, TimedWait::DeadlineNanos(FLIP_ROOM_WAIT_MICROS), [&] {
|
|
return queue->failure || queue->stopping || cfg->shutdownToken.stop_requested() || queue->reservations.load() < VIDEO_OUT_FLIP_QUEUE_CAPACITY;
|
|
});
|
|
if (queue->failure) std::rethrow_exception(queue->failure);
|
|
if (queue->stopping || cfg->shutdownToken.stop_requested()) throw ProcessShutdown{};
|
|
require(room, "flip queue stayed full for 60 s: the presenter is not completing flips");
|
|
}
|
|
|
|
std::shared_ptr<AgcDriver::IFlipRequest> Reserve(const AgcDriver::FlipInfo& info) override {
|
|
// All flip modes are presented at the next vsync.
|
|
require(info.mode >= VIDEO_OUT_FLIP_MODE_VSYNC && info.mode <= 6, "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->outputHandle = info.handle;
|
|
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);
|
|
if (queue->stopping || cfg->shutdownToken.stop_requested()) throw ProcessShutdown{};
|
|
std::lock_guard lock(cfg->mutex);
|
|
checkConfig(*cfg);
|
|
request->generation = cfg->generation;
|
|
request->flipRate = cfg->flipRate;
|
|
if (info.index >= 0) request->reuseTicket = cfg->bufferReuse[info.index].Reserve();
|
|
++queue->reservations;
|
|
++cfg->flipStatus.flipPendingNum;
|
|
if (info.index >= 0) ++cfg->bufferPending[info.index];
|
|
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.NotifyAll();
|
|
}
|
|
|
|
private:
|
|
std::shared_ptr<VideoOutConfig> cfg;
|
|
std::shared_ptr<FlipQueue> queue;
|
|
};
|
|
|
|
}
|
|
|
|
FlipRequest::~FlipRequest() {
|
|
if (!reserved) return;
|
|
std::lock_guard lock(cfg->mutex);
|
|
ReleaseLocked();
|
|
}
|
|
|
|
void FlipRequest::ReleaseLocked() noexcept {
|
|
if (!reserved || terminal) return;
|
|
--cfg->flipStatus.flipPendingNum;
|
|
--queue->reservations;
|
|
if (index >= 0) {
|
|
--cfg->bufferPending[index];
|
|
cfg->bufferReuse[index].Complete(reuseTicket);
|
|
}
|
|
terminal = true;
|
|
cfg->vblankCond.notify_all();
|
|
}
|
|
|
|
void FlipRequest::Cancel() noexcept {
|
|
{
|
|
std::lock_guard lock(cfg->mutex);
|
|
ReleaseLocked();
|
|
}
|
|
queue->changed.NotifyAll();
|
|
}
|
|
|
|
void FlipRequest::GpuReady(const std::shared_ptr<AgcDriver::FrameTiming>& frameTiming) {
|
|
require(frameTiming != nullptr, "missing frame timing");
|
|
timing = frameTiming;
|
|
{
|
|
AgcDriver::PerformanceContext timingContext(timing.get());
|
|
AgcDriver::PerformanceTimer readiness("VideoOut.Readiness");
|
|
std::lock_guard queueLock(queue->mutex);
|
|
if (queue->failure) std::rethrow_exception(queue->failure);
|
|
if (queue->stopping || cfg->shutdownToken.stop_requested()) throw ProcessShutdown{};
|
|
std::lock_guard lock(cfg->mutex);
|
|
cfg->CheckAlive();
|
|
if (cfg->Closed()) {
|
|
ReleaseLocked();
|
|
return;
|
|
}
|
|
require(reserved && !ready && !terminal && cfg->generation == generation, "invalid flip readiness transition");
|
|
resolveFlipBuffer(*this, *cfg);
|
|
readiness.Mark("locks_validate");
|
|
queuedAt = AgcDriver::FrameTiming::Clock::now();
|
|
queue->requests.push_back(shared_from_this());
|
|
ready = true;
|
|
}
|
|
queue->changed.NotifyAll();
|
|
// The worker is done once the request is queued: hardware does not stall the command processor
|
|
// on a flip, and the title observes completion through flipPendingNum, which processFlip drops
|
|
// once the frame's GPU work and its presentation have completed (see Driver::Present). A
|
|
// presentation failure reaches the worker through ReportFailure at its next packet. Debug aid:
|
|
// APS5_SYNC_FLIP=1 parks the worker until the presenter is done, as before.
|
|
static const bool syncFlip = std::getenv("APS5_SYNC_FLIP") != nullptr;
|
|
if (!syncFlip) return;
|
|
std::unique_lock lock(cfg->mutex);
|
|
cfg->vblankCond.wait(lock, cfg->shutdownToken, [&] { 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;
|
|
ReleaseLocked();
|
|
queue->changed.NotifyAll();
|
|
}
|
|
|
|
VideoOutDriver& VideoOutDriver::Get() {
|
|
static VideoOutDriver instance;
|
|
return instance;
|
|
}
|
|
|
|
VideoOutDriver::VideoOutDriver() {
|
|
SDL_SetHint(SDL_HINT_WINDOWS_DPI_AWARENESS, "permonitorv2");
|
|
if (SDL_InitSubSystem(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER) < 0) {
|
|
throw std::runtime_error(std::string("SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: ") + SDL_GetError());
|
|
}
|
|
try {
|
|
AgcDriverWaitIdle_nid_postfix();
|
|
std::promise<void> started;
|
|
auto attached = started.get_future();
|
|
presentThread = std::jthread([this, &started](std::stop_token token) {
|
|
try {
|
|
AgcDriverRaiseWorkerThreadPriority_nid_postfix("presenter");
|
|
} catch (...) {
|
|
started.set_exception(std::current_exception());
|
|
return;
|
|
}
|
|
presentLoop(token, started);
|
|
});
|
|
attached.get();
|
|
vblankThread = std::jthread([this](std::stop_token token) { vblankLoop(token); });
|
|
LibcRegisterShutdown_nid_postfix([] { VideoOutDriver::Get().Shutdown(); });
|
|
} catch (...) {
|
|
if (vblankThread.joinable()) {
|
|
vblankThread.request_stop();
|
|
flipQueue->changed.NotifyAll();
|
|
vblankThread.join();
|
|
}
|
|
if (presentThread.joinable()) {
|
|
presentThread.request_stop();
|
|
flipQueue->changed.NotifyAll();
|
|
presentThread.join();
|
|
}
|
|
SDL_QuitSubSystem(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER);
|
|
throw;
|
|
}
|
|
}
|
|
|
|
VideoOutDriver::~VideoOutDriver() {
|
|
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;
|
|
LibcRequestShutdown_nid_postfix();
|
|
{
|
|
std::lock_guard queueLock(flipQueue->mutex);
|
|
flipQueue->stopping = true;
|
|
}
|
|
presentThread.request_stop();
|
|
vblankThread.request_stop();
|
|
flipQueue->changed.NotifyAll();
|
|
presentThread.join();
|
|
vblankThread.join();
|
|
{
|
|
std::lock_guard lock(mutex);
|
|
for (int handle = 1; handle < VIDEO_OUT_NUM_MAX; ++handle) {
|
|
if (outputs[handle]) close(handle);
|
|
}
|
|
}
|
|
SDL_QuitSubSystem(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER);
|
|
stopped = true;
|
|
std::lock_guard lock(flipQueue->mutex);
|
|
if (flipQueue->failure) std::rethrow_exception(flipQueue->failure);
|
|
}
|
|
|
|
int VideoOutDriver::Open(int busType) {
|
|
std::lock_guard lock(mutex);
|
|
{
|
|
std::lock_guard queueLock(flipQueue->mutex);
|
|
if (flipQueue->failure) std::rethrow_exception(flipQueue->failure);
|
|
if (flipQueue->stopping || LibcShutdownToken_nid_postfix().stop_requested()) throw ProcessShutdown{};
|
|
}
|
|
const int handle = busType + 1;
|
|
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->busType = busType;
|
|
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::lock_guard lock(mutex);
|
|
if (LibcShutdownToken_nid_postfix().stop_requested()) throw ProcessShutdown{};
|
|
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;
|
|
const auto removeEvents = [](const auto& events, int kind) {
|
|
for (const auto& event : events) {
|
|
const auto result = EqueueDeleteEvent_nid_postfix(event.eq, static_cast<uintptr_t>(kind), EVFILT_VIDEO_OUT);
|
|
require(result == EQUEUE_OK || result == SCE_KERNEL_ERROR_EBADF || result == SCE_KERNEL_ERROR_ENOENT, "event removal during close failed");
|
|
}
|
|
};
|
|
removeEvents(cfg->flipEvents, VIDEO_OUT_EVENT_FLIP);
|
|
removeEvents(cfg->vblankEvents, VIDEO_OUT_EVENT_VBLANK);
|
|
removeEvents(cfg->preVblankEvents, VIDEO_OUT_EVENT_PRE_VBLANK_START);
|
|
removeEvents(cfg->outputModeEvents, VIDEO_OUT_EVENT_SET_MODE);
|
|
removeEvents(cfg->vrrStatusEvents, VIDEO_OUT_EVENT_VRR_STATUS);
|
|
cfg->flipEvents.clear();
|
|
cfg->vblankEvents.clear();
|
|
cfg->preVblankEvents.clear();
|
|
cfg->outputModeEvents.clear();
|
|
cfg->vrrStatusEvents.clear();
|
|
cfg->vblankCond.notify_all();
|
|
flipQueue->changed.NotifyAll();
|
|
return true;
|
|
}
|
|
|
|
std::shared_ptr<VideoOutConfig> VideoOutDriver::GetConfig(int handle) {
|
|
{
|
|
std::lock_guard lock(flipQueue->mutex);
|
|
if (flipQueue->failure) std::rethrow_exception(flipQueue->failure);
|
|
if (flipQueue->stopping || LibcShutdownToken_nid_postfix().stop_requested()) throw ProcessShutdown{};
|
|
}
|
|
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) {
|
|
return GetConfig(handle) != nullptr;
|
|
}
|
|
|
|
bool VideoOutDriver::HasConfig(int handle) {
|
|
std::shared_ptr<VideoOutConfig> cfg;
|
|
{
|
|
std::lock_guard lock(mutex);
|
|
if (handle <= 0 || handle >= VIDEO_OUT_NUM_MAX || contexts[handle] == nullptr) return false;
|
|
cfg = contexts[handle];
|
|
}
|
|
std::lock_guard cfgLock(cfg->mutex);
|
|
return cfg->opened && !cfg->closing;
|
|
}
|
|
|
|
int VideoOutDriver::SubmitFlip(int handle, int index, int flipMode, int64_t flipArg) {
|
|
if (LibcShutdownToken_nid_postfix().stop_requested()) throw ProcessShutdown{};
|
|
// A title that does not pace on flipPendingNum can run ahead of the presenter now that the queue
|
|
// worker no longer waits per flip; a full queue is the documented error, not a failure.
|
|
if (flipQueue->reservations.load() >= VIDEO_OUT_FLIP_QUEUE_CAPACITY) return VIDEO_OUT_ERROR_FLIP_QUEUE_FULL;
|
|
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");
|
|
return 0;
|
|
}
|
|
|
|
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("VideoOut: unknown event kind");
|
|
for (auto it = events->begin(); it != events->end();) {
|
|
require(it->generation == cfg.generation, "stale event registration");
|
|
const auto result = EqueueTriggerEvent_nid_postfix(it->eq, static_cast<uintptr_t>(eventKind), EVFILT_VIDEO_OUT, triggerData);
|
|
if (result == SCE_KERNEL_ERROR_EBADF || result == SCE_KERNEL_ERROR_ENOENT) it = events->erase(it);
|
|
else {
|
|
require(result == EQUEUE_OK, "event delivery failed");
|
|
++it;
|
|
}
|
|
}
|
|
}
|
|
|
|
void VideoOutDriver::vblankEnd() {
|
|
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;
|
|
require(cfg->vblankStatus.count != std::numeric_limits<uint64_t>::max(), "vblank counter overflow");
|
|
++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));
|
|
++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(FlipRequest& req) {
|
|
AgcDriver::PerformanceContext timingContext(req.timing.get());
|
|
AgcDriver::PerformanceTimer timing("VideoOut.Flip");
|
|
{
|
|
std::unique_lock lock(req.cfg->mutex);
|
|
timing.Mark("config_mutex_wait");
|
|
req.cfg->CheckAlive();
|
|
if (req.cfg->Closed()) {
|
|
req.ReleaseLocked();
|
|
return;
|
|
}
|
|
require(req.ready && !req.terminal && req.generation == req.cfg->generation, "stale or incomplete flip request");
|
|
const auto interval = static_cast<uint64_t>(req.flipRate + 1);
|
|
require(req.cfg->lastFlipVblank <= std::numeric_limits<uint64_t>::max() - interval, "flip interval overflow");
|
|
const auto target = req.cfg->lastFlipVblank + interval;
|
|
timing.Mark("validate");
|
|
req.cfg->vblankCond.wait(lock, req.cfg->shutdownToken, [&] { return req.cfg->vblankStatus.count >= target || req.cfg->failure || req.cfg->closing; });
|
|
timing.Mark("vblank_wait");
|
|
req.cfg->CheckAlive();
|
|
if (req.cfg->Closed()) {
|
|
req.ReleaseLocked();
|
|
return;
|
|
}
|
|
req.cfg->lastFlipVblank = req.cfg->vblankStatus.count;
|
|
}
|
|
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 (req.cfg->busType == VIDEO_OUT_BUS_TYPE_OVERLAY) {
|
|
std::lock_guard lock(req.cfg->mutex);
|
|
req.cfg->CheckAlive();
|
|
if (!req.cfg->Closed()) {
|
|
require(!req.terminal && !req.gpuComplete, "invalid GPU completion transition");
|
|
req.gpuComplete = true;
|
|
}
|
|
} else {
|
|
window.Ensure(req.width, req.height);
|
|
const auto extensions = windowExtensions(window.Handle());
|
|
const AgcDriver::PresentationWindow target{window.Handle(), extensions, &createWindowSurface, &windowDrawableSize, req.width, req.height, req.timing};
|
|
timing.Mark("window_prepare");
|
|
const auto gpuReady = [](void* context) {
|
|
auto& request = *static_cast<FlipRequest*>(context);
|
|
std::lock_guard lock(request.cfg->mutex);
|
|
request.cfg->CheckAlive();
|
|
if (request.cfg->Closed()) return;
|
|
require(!request.terminal && !request.gpuComplete, "invalid GPU completion transition");
|
|
request.gpuComplete = true;
|
|
request.cfg->vblankCond.notify_all();
|
|
};
|
|
if (req.index >= 0 && !req.unregistered) {
|
|
const auto display = DescribeVideoOutBuffer(req.buffer, req.group);
|
|
AgcDriverPresentBuffer_nid_postfix(target, display, gpuReady, &req);
|
|
} else {
|
|
AgcDriverPresentClear_nid_postfix(target, req.unregistered || req.index == VIDEO_OUT_BUFFER_INDEX_BLACK, gpuReady, &req);
|
|
}
|
|
timing.Mark("present");
|
|
window.UpdateTitle();
|
|
timing.Mark("window_title");
|
|
}
|
|
std::lock_guard lock(req.cfg->mutex);
|
|
timing.Mark("completion_mutex_wait");
|
|
req.cfg->CheckAlive();
|
|
if (req.cfg->Closed()) {
|
|
req.ReleaseLocked();
|
|
return;
|
|
}
|
|
require(!req.terminal && req.cfg->generation == req.generation, "flip cancelled during presentation");
|
|
require(req.gpuComplete, "flip submitted before GPU completion");
|
|
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->width = req.width;
|
|
req.cfg->height = req.height;
|
|
--req.cfg->flipStatus.flipPendingNum;
|
|
--req.queue->reservations;
|
|
if (req.index >= 0) {
|
|
--req.cfg->bufferPending[req.index];
|
|
req.cfg->bufferReuse[req.index].Complete(req.reuseTicket);
|
|
}
|
|
req.terminal = true;
|
|
req.cfg->vblankCond.notify_all();
|
|
timing.Mark("notify_game");
|
|
}
|
|
|
|
void VideoOutDriver::presentLoop(std::stop_token token, std::promise<void>& started) {
|
|
try {
|
|
window.Ensure(VIDEO_OUT_DEFAULT_WIDTH, VIDEO_OUT_DEFAULT_HEIGHT);
|
|
const auto extensions = windowExtensions(window.Handle());
|
|
AgcDriverAttachWindow_nid_postfix({window.Handle(), extensions, &createWindowSurface, &windowDrawableSize, VIDEO_OUT_DEFAULT_WIDTH, VIDEO_OUT_DEFAULT_HEIGHT, nullptr});
|
|
} catch (...) {
|
|
window.Destroy();
|
|
started.set_exception(std::current_exception());
|
|
return;
|
|
}
|
|
started.set_value();
|
|
std::shared_ptr<FlipRequest> current;
|
|
#if APS5_ENABLE_TIMING_LOG
|
|
std::unique_ptr<FrameTimingLog> timingLog;
|
|
#endif
|
|
try {
|
|
#if APS5_ENABLE_TIMING_LOG
|
|
timingLog = std::make_unique<FrameTimingLog>();
|
|
#endif
|
|
PadInput padInput;
|
|
MouseInput mouseInput;
|
|
KeyboardInput keyboardInput;
|
|
while (!token.stop_requested()) {
|
|
{
|
|
std::unique_lock lock(flipQueue->mutex);
|
|
flipQueue->changed.WaitUntil(lock, TimedWait::DeadlineNanos(PRESENT_POLL_MICROS), [&] { 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();
|
|
}
|
|
}
|
|
|
|
SDL_Event event;
|
|
while (SDL_PollEvent(&event)) {
|
|
if (event.type == SDL_QUIT) {
|
|
LibcRequestExit_nid_postfix(0);
|
|
throw ProcessShutdown{};
|
|
}
|
|
padInput.HandleEvent(event, window);
|
|
if (window.Handle() != nullptr) {
|
|
mouseInput.HandleEvent(event, SDL_GetWindowID(window.Handle()));
|
|
keyboardInput.HandleEvent(event, SDL_GetWindowID(window.Handle()));
|
|
}
|
|
}
|
|
padInput.Update();
|
|
if (current) {
|
|
require(current->timing != nullptr, "missing presentation timing");
|
|
const auto dequeued = AgcDriver::FrameTiming::Clock::now();
|
|
current->timing->Add(current->timing->Get("VideoOut", "queue"), dequeued - current->queuedAt);
|
|
processFlip(*current);
|
|
const auto finished = AgcDriver::FrameTiming::Clock::now();
|
|
AgcDriver::FrameTiming::Clock::duration interval{};
|
|
{
|
|
std::lock_guard lock(current->cfg->mutex);
|
|
const auto previous = current->cfg->lastTimingFlip;
|
|
if (previous != AgcDriver::FrameTiming::Clock::time_point{}) interval = finished - previous;
|
|
current->cfg->lastTimingFlip = finished;
|
|
}
|
|
#if APS5_ENABLE_TIMING_LOG
|
|
timingLog->Enqueue(current->timing->Capture(current->outputHandle, current->index, current->flipArg, finished, interval));
|
|
#endif
|
|
}
|
|
if (current) {
|
|
current.reset();
|
|
flipQueue->changed.NotifyAll();
|
|
}
|
|
}
|
|
} catch (const ProcessShutdown&) {
|
|
} catch (const std::exception& error) {
|
|
std::fprintf(stderr, "[videoout] presentation failed: %s\n", error.what());
|
|
std::fflush(stderr);
|
|
std::terminate();
|
|
} catch (...) {
|
|
std::fprintf(stderr, "[videoout] presentation failed with a non-standard exception\n");
|
|
std::fflush(stderr);
|
|
std::terminate();
|
|
}
|
|
#if APS5_ENABLE_TIMING_LOG
|
|
try {
|
|
timingLog->Finish();
|
|
} catch (const std::exception& error) {
|
|
std::fprintf(stderr, "[videoout] frame timing log failed: %s\n", error.what());
|
|
std::fflush(stderr);
|
|
std::terminate();
|
|
}
|
|
#endif
|
|
current.reset();
|
|
{
|
|
std::list<std::shared_ptr<FlipRequest>> cancelled;
|
|
{
|
|
std::lock_guard lock(flipQueue->mutex);
|
|
cancelled.swap(flipQueue->requests);
|
|
}
|
|
}
|
|
try {
|
|
AgcDriverShutdown_nid_postfix();
|
|
} catch (...) {
|
|
std::lock_guard lock(flipQueue->mutex);
|
|
if (!flipQueue->failure) flipQueue->failure = std::current_exception();
|
|
}
|
|
window.Destroy();
|
|
}
|
|
|
|
void VideoOutDriver::vblankLoop(std::stop_token token) {
|
|
using Frame = std::chrono::duration<int64_t, std::ratio<1001, 60000>>;
|
|
const auto start = std::chrono::steady_clock::now();
|
|
try {
|
|
for (int64_t frame = 1; !token.stop_requested(); ++frame) {
|
|
const auto next = start + std::chrono::duration_cast<std::chrono::steady_clock::duration>(Frame(frame));
|
|
const auto remaining = std::chrono::duration_cast<std::chrono::microseconds>(next - std::chrono::steady_clock::now()).count();
|
|
if (remaining > 0) PreciseSleepUs(static_cast<unsigned long long>(remaining));
|
|
{
|
|
std::lock_guard lock(flipQueue->mutex);
|
|
if (token.stop_requested() || flipQueue->failure) return;
|
|
}
|
|
vblankEnd();
|
|
}
|
|
} catch (const std::exception& error) {
|
|
std::fprintf(stderr, "[videoout] vblank failed: %s\n", error.what());
|
|
std::fflush(stderr);
|
|
std::terminate();
|
|
} catch (...) {
|
|
std::fprintf(stderr, "[videoout] vblank failed with a non-standard exception\n");
|
|
std::fflush(stderr);
|
|
std::terminate();
|
|
}
|
|
}
|