From a1efdab629f74dc327174ccf1c6bb2d61b05542e Mon Sep 17 00:00:00 2001 From: Pedro Date: Thu, 8 Oct 2026 10:33:21 -0500 Subject: [PATCH] fix(videoout): cancel flips on a port the title closed instead of failing the driver A flip still queued or in presentation when the title closes its port is discarded, as on hardware, rather than stored as a driver failure. A closed port no longer terminates the presentation thread nor surfaces from Shutdown. The lifetime test now expects cancellation and the reopen mode is registered in ctest. --- core/libs/prx/libSceVideoOut/CMakeLists.txt | 2 +- .../libSceVideoOut/include/VideoOutDriver.hpp | 12 +++- .../prx/libSceVideoOut/src/VideoOutDriver.cpp | 70 ++++++++++++------- core/libs/prx/libSceVideoOut/tests/Flip.cpp | 15 ++-- 4 files changed, 62 insertions(+), 37 deletions(-) diff --git a/core/libs/prx/libSceVideoOut/CMakeLists.txt b/core/libs/prx/libSceVideoOut/CMakeLists.txt index 3c91d54e8..238b2dc70 100644 --- a/core/libs/prx/libSceVideoOut/CMakeLists.txt +++ b/core/libs/prx/libSceVideoOut/CMakeLists.txt @@ -33,7 +33,7 @@ set_target_properties(video_out_flip_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(video_out_flip_tests) if(BUILD_TESTING) set_target_properties(video_out_flip_tests PROPERTIES EXCLUDE_FROM_ALL OFF) - foreach(flipMode IN ITEMS lifetime decode controls) + foreach(flipMode IN ITEMS lifetime reopen decode controls) if(flipMode STREQUAL "lifetime") add_test(NAME video_out_flip_${flipMode} COMMAND video_out_flip_tests) else() diff --git a/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp b/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp index 02adf6ea0..03f6813be 100644 --- a/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp +++ b/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp @@ -139,10 +139,16 @@ struct VideoOutConfig { std::array bufferReuse; std::array groups{}; - void Check() const { + bool Closed() const { return !opened || closing; } + + void CheckAlive() const { if (failure) std::rethrow_exception(failure); if (shutdownToken.stop_requested()) throw ProcessShutdown{}; - if (!opened || closing) throw std::runtime_error("VideoOut: port is closed"); + } + + void Check() const { + CheckAlive(); + if (Closed()) throw std::runtime_error("VideoOut: port is closed"); } }; @@ -173,6 +179,8 @@ struct FlipRequest final : AgcDriver::IFlipRequest, std::enable_shared_from_this ~FlipRequest() override; void GpuReady(const std::shared_ptr& frameTiming) override; void Fail(std::exception_ptr error) noexcept override; + void Cancel() noexcept; + void ReleaseLocked() noexcept; }; struct FlipQueue { diff --git a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp index 615d3201c..04eb311c7 100644 --- a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp +++ b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp @@ -74,10 +74,10 @@ public: void Wait() override { std::unique_lock lock(_config->mutex); _config->vblankCond.wait(lock, _config->shutdownToken, [&] { - return _config->failure || _config->closing || !_config->opened || + return _config->failure || _config->Closed() || _config->bufferReuse[_index].IsComplete(_ticket); }); - checkConfig(*_config); + _config->CheckAlive(); } }; @@ -166,15 +166,27 @@ private: FlipRequest::~FlipRequest() { if (!reserved) return; std::lock_guard lock(cfg->mutex); - if (!terminal) { - --cfg->flipStatus.flipPendingNum; - --queue->reservations; - if (index >= 0) { - --cfg->bufferPending[index]; - cfg->bufferReuse[index].Complete(reuseTicket); - } - cfg->vblankCond.notify_all(); + 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) { @@ -187,7 +199,11 @@ void FlipRequest::GpuReady(const std::shared_ptr& frameT 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); + cfg->CheckAlive(); + if (cfg->Closed()) { + ReleaseLocked(); + return; + } require(reserved && !ready && !terminal && cfg->generation == generation, "invalid flip readiness transition"); readiness.Mark("locks_validate"); queuedAt = AgcDriver::FrameTiming::Clock::now(); @@ -212,16 +228,7 @@ 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; - if (index >= 0) { - --cfg->bufferPending[index]; - cfg->bufferReuse[index].Complete(reuseTicket); - } - terminal = true; - } - cfg->vblankCond.notify_all(); + ReleaseLocked(); queue->changed.notify_all(); } @@ -420,7 +427,11 @@ void VideoOutDriver::processFlip(FlipRequest& req) { { std::unique_lock lock(req.cfg->mutex); timing.Mark("config_mutex_wait"); - checkConfig(*req.cfg); + 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"); @@ -428,7 +439,11 @@ void VideoOutDriver::processFlip(FlipRequest& req) { 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"); - checkConfig(*req.cfg); + 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"); @@ -439,7 +454,8 @@ void VideoOutDriver::processFlip(FlipRequest& req) { const auto gpuReady = [](void* context) { auto& request = *static_cast(context); std::lock_guard lock(request.cfg->mutex); - checkConfig(*request.cfg); + 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(); @@ -455,7 +471,11 @@ void VideoOutDriver::processFlip(FlipRequest& req) { timing.Mark("window_title"); std::lock_guard lock(req.cfg->mutex); timing.Mark("completion_mutex_wait"); - checkConfig(*req.cfg); + 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"); diff --git a/core/libs/prx/libSceVideoOut/tests/Flip.cpp b/core/libs/prx/libSceVideoOut/tests/Flip.cpp index abfe80153..01aeb32f2 100644 --- a/core/libs/prx/libSceVideoOut/tests/Flip.cpp +++ b/core/libs/prx/libSceVideoOut/tests/Flip.cpp @@ -99,24 +99,21 @@ void testLifetime(bool reopen) { 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"); + check(cfg->vblankCond.wait_for(lock, std::chrono::seconds(10), [&] { return cfg->flipStatus.flipPendingNum == 0; }), "closed port kept its pending flip"); + check(cfg->failure == nullptr, "closing a port failed the driver"); + check(cfg->flipStatus.count == 0, "cancelled flip has successful status"); } - const auto message = expectFailure([&] { std::rethrow_exception(failure); }); - check(message.find("closed") != std::string::npos, "flip used a closed port"); if (replacement) { std::lock_guard lock(replacement->mutex); check(replacement->flipStatus.flipPendingNum == 0 && replacement->flipStatus.count == 0, "old request changed new port counters"); + } else { + check(expectFailure([&] { sceVideoOutWaitVblank(handle); }).find("closed") != std::string::npos, "closed port accepted a vblank wait"); } - check(expectFailure([&] { sceVideoOutWaitVblank(handle); }).find("closed") != std::string::npos, "VideoOut lost worker failure"); AgcDriverUnregisterVideoOutput_nid_postfix(7, gate); if (reopen) sceVideoOutClose(handle); - const auto shutdown = expectFailure([] { LibcRunShutdown_nid_postfix(); }); - check(shutdown.find("closed") != std::string::npos, "shutdown lost asynchronous error"); + LibcRunShutdown_nid_postfix(); } std::size_t tiledOffset(uint32_t x, uint32_t y, uint32_t width) {