#include #include #include #include #include #include #include #include #include #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 { void require(bool condition, const char* reason) { if (!condition) throw std::runtime_error(std::string("VideoOut: ") + reason); } std::vector 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 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(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(context)) & SDL_WINDOW_MINIMIZED) != 0) { *width = 0; *height = 0; return; } int drawableWidth = 0; int drawableHeight = 0; SDL_Vulkan_GetDrawableSize(static_cast(context), &drawableWidth, &drawableHeight); *width = drawableWidth > 0 ? static_cast(drawableWidth) : 0; *height = drawableHeight > 0 ? static_cast(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, 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(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 _config; std::uint32_t _index; std::uint64_t _ticket; public: RenderingWait(std::shared_ptr 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 config, std::shared_ptr requests) : cfg(std::move(config)), queue(std::move(requests)) {} std::shared_ptr 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(cfg, index, cfg->bufferReuse[index].Capture()); } void WaitForFlipRoom() override { std::unique_lock queueLock(queue->mutex); const bool room = queue->changed.wait_for(queueLock, std::chrono::seconds(60), [&] { 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 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(); request->cfg = cfg; request->queue = queue; request->index = info.index; request->outputHandle = info.handle; request->flipMode = static_cast(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.notify_all(); } private: std::shared_ptr cfg; std::shared_ptr 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.notify_all(); } void FlipRequest::GpuReady(const std::shared_ptr& 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.notify_all(); // 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.notify_all(); } 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 started; auto attached = started.get_future(); presentThread = std::jthread([this, &started](std::stop_token token) { 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.notify_all(); vblankThread.join(); } if (presentThread.joinable()) { presentThread.request_stop(); flipQueue->changed.notify_all(); 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.notify_all(); 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::max(), "port generation overflow"); generation = previous->generation + 1; } auto cfg = std::make_shared(); cfg->generation = generation; cfg->busType = busType; cfg->opened = true; cfg->flipStatus.flipArg = -1; cfg->flipStatus.currentBuffer = -1; auto output = std::make_shared(cfg, flipQueue); AgcDriverRegisterVideoOutput_nid_postfix(static_cast(handle), output); contexts[handle] = std::move(cfg); outputs[handle] = std::move(output); return handle; } bool VideoOutDriver::Close(int handle) { std::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(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(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.notify_all(); return true; } std::shared_ptr 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 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 words{AgcDriver::FlipPacketHeader, static_cast(handle), static_cast(index), static_cast(flipMode), static_cast(static_cast(flipArg)), static_cast(static_cast(flipArg) >> 32u)}; Packet packet{words.data(), static_cast(words.size()), 0, {}}; const auto result = sceAgcDriverSubmitDcb(&packet); require(result == 0, "driver rejected flip submission"); return 0; } void VideoOutDriver::triggerEvents(VideoOutConfig& cfg, int eventKind, void* triggerData) { std::vector* events = nullptr; if (eventKind == VIDEO_OUT_EVENT_FLIP) events = &cfg.flipEvents; else if (eventKind == VIDEO_OUT_EVENT_VBLANK) events = &cfg.vblankEvents; else if (eventKind == VIDEO_OUT_EVENT_PRE_VBLANK_START) events = &cfg.preVblankEvents; else if (eventKind == VIDEO_OUT_EVENT_SET_MODE) events = &cfg.outputModeEvents; else throw std::runtime_error("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(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::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(cfg->preVblankStatus.count)); ++cfg->vblankStatus.count; cfg->vblankStatus.processTime = sceKernelGetProcessTime(); cfg->vblankStatus.processTimeCounter = sceKernelGetProcessTimeCounter(); triggerEvents(*cfg, VIDEO_OUT_EVENT_VBLANK, reinterpret_cast(cfg->vblankStatus.count)); cfg->vblankCond.notify_all(); } } void VideoOutDriver::processFlip(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(req.flipRate + 1); require(req.cfg->lastFlipVblank <= std::numeric_limits::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(std::numeric_limits::max()) && req.height <= static_cast(std::numeric_limits::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(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::max(), "flip counter overflow"); triggerEvents(*req.cfg, VIDEO_OUT_EVENT_FLIP, reinterpret_cast(req.flipArg)); ++req.cfg->flipStatus.count; req.cfg->flipStatus.processTime = sceKernelGetProcessTime(); req.cfg->flipStatus.processTimeCounter = sceKernelGetProcessTimeCounter(); req.cfg->flipStatus.flipArg = req.flipArg; req.cfg->flipStatus.currentBuffer = req.index; req.cfg->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& 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 current; #if APS5_ENABLE_TIMING_LOG std::unique_ptr timingLog; #endif try { #if APS5_ENABLE_TIMING_LOG timingLog = std::make_unique(); #endif PadInput padInput; MouseInput mouseInput; KeyboardInput keyboardInput; while (!token.stop_requested()) { { std::unique_lock lock(flipQueue->mutex); flipQueue->changed.wait_for(lock, std::chrono::milliseconds(10), [&] { 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.notify_all(); } } } 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> 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>; 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(Frame(frame)); const auto remaining = std::chrono::duration_cast(next - std::chrono::steady_clock::now()).count(); if (remaining > 0) PreciseSleepUs(static_cast(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(); } }