From 39fd90b050ee3db780febf89f171619f9370d2ac Mon Sep 17 00:00:00 2001 From: boykopovar Date: Mon, 28 Sep 2026 18:30:34 +0300 Subject: [PATCH] fix(kernel): orderly shutdown when closing the display window --- .../Execution/include/Driver.hpp | 1 + .../Execution/include/WorkerSampler.hpp | 1 + .../libSceAgcDriver/Execution/src/Driver.cpp | 72 ++++++++++++++----- .../Execution/src/WorkerSampler.cpp | 38 +++++++--- .../Graphics/include/GuestBufferMemory.hpp | 2 + .../Graphics/src/GuestBufferMemory.cpp | 46 ++++++------ .../libSceVideoOut/include/VideoOutDriver.hpp | 3 + core/libs/prx/libSceVideoOut/src/Buffer.cpp | 16 +++-- core/libs/prx/libSceVideoOut/src/Event.cpp | 28 ++++++-- core/libs/prx/libSceVideoOut/src/Output.cpp | 63 +++++++++++----- .../prx/libSceVideoOut/src/VideoOutDriver.cpp | 61 ++++++++-------- core/libs/prx/libc/include/Shutdown.hpp | 8 +++ core/libs/prx/libc/src/Process.cpp | 31 ++++++++ .../src/specifics/windows/CrashReport.cpp | 4 +- 14 files changed, 265 insertions(+), 109 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver.hpp index d2fde19a7..665b5d159 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver.hpp @@ -11,6 +11,7 @@ void Submit(const Packet* packet, std::uint32_t queue); } extern "C" void AgcDriverWaitIdle_nid_postfix(); +extern "C" void AgcDriverShutdown_nid_postfix(); extern "C" void AgcDriverSuspendPoint_nid_postfix(); extern "C" void AgcDriverRegisterShader_nid_postfix(const Shader* shader); diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/WorkerSampler.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/WorkerSampler.hpp index 65b272671..ec282a171 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/WorkerSampler.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/WorkerSampler.hpp @@ -5,6 +5,7 @@ namespace AgcDriver { // Starts sampling the calling thread when APS5_SAMPLE_WORKER names an output file. void StartWorkerSampler(); +void StopWorkerSampler(); } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver.cpp index e02a1ac14..fb2728197 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver.cpp @@ -11,6 +11,7 @@ #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" #include "prx/libc/include/Shutdown.hpp" #include "prx/libc/include/CpuTopology.hpp" #include "ControlFlow/RequestSerializer.hpp" @@ -460,24 +461,34 @@ public: void Shutdown() { stop(); - CheckFailure(); + std::lock_guard lock(mutex); + rethrowFailure(); } private: void stop() { require(!OnWorkerThread(), "worker cannot stop itself"); std::lock_guard shutdownLock(shutdownMutex); + if (stopped) return; + LibcRequestShutdown_nid_postfix(); { std::lock_guard lock(mutex); stopping = true; + for (auto& [queue, worker] : workers) { + worker.pending.clear(); + worker.queued.store(0, std::memory_order_release); + } } changed.notify_all(); - // No worker is added once `stopping` is set, so the map is stable while the threads finish. for (auto& [queue, worker] : workers) { if (worker.thread.joinable()) worker.thread.join(); } + StopWorkerSampler(); std::lock_guard gpuLock(GuestMemory::GpuMutex()); device.reset(); + replacedDevices.clear(); + Graphics::ShutdownGuestBufferWorkers(); + stopped = true; } public: @@ -506,7 +517,7 @@ public: { std::lock_guard lock(mutex); rethrowFailure(); - require(!stopping, "submission during shutdown"); + checkStopping(); require(accepted != std::numeric_limits::max(), "submission serial overflow"); for (std::size_t cursor = 0; cursor < submission.commands.size();) { const auto* words = submission.commands.data() + cursor; @@ -550,16 +561,17 @@ public: std::unique_lock lock(mutex); const auto target = accepted; ++idleWaiters; - changed.wait(lock, [&] { return failure != nullptr || completed >= target; }); + changed.wait(lock, [&] { return failure != nullptr || stopping || completed >= target; }); --idleWaiters; rethrowFailure(); + checkStopping(); } void SuspendPoint() { require(!OnWorkerThread(), "worker cannot suspend itself"); std::unique_lock lock(mutex); rethrowFailure(); - require(!stopping, "suspend during shutdown"); + checkStopping(); require(accepted != std::numeric_limits::max(), "submission serial overflow"); Submission boundary{}; boundary.serial = accepted + 1; @@ -578,7 +590,7 @@ public: require(output != nullptr, "null video output"); std::lock_guard lock(mutex); rethrowFailure(); - require(!stopping, "video output registration during shutdown"); + checkStopping(); require(outputs.emplace(handle, output).second, "video output already registered"); } @@ -592,9 +604,11 @@ public: // Called per packet and per poll of a wait: the flag keeps the driver mutex out of those loops // until a failure exists, and `failure` itself is still read under the mutex. void CheckFailure() { - if (!failed.load(std::memory_order_acquire)) return; - std::lock_guard lock(mutex); - rethrowFailure(); + if (failed.load(std::memory_order_acquire)) { + std::lock_guard lock(mutex); + rethrowFailure(); + } + checkStopping(); } void ReportFailure(std::exception_ptr error) { @@ -654,7 +668,6 @@ public: if (device == nullptr || device->Window() == nullptr) { if (device) { device->PrepareForReplacement(); - static std::vector> replacedDevices; replacedDevices.push_back(device); } device = std::make_shared(&window); @@ -722,6 +735,8 @@ public: timing.Mark("release_and_callback"); if (profile) reportPresents(waitedMs, inFlight); CheckFailure(); + } catch (const ProcessShutdown&) { + throw; } catch (...) { ReportFailure(std::current_exception()); throw; @@ -1450,13 +1465,16 @@ private: std::map queues; std::map> outputs; DevicePointer device; + std::vector> replacedDevices; + std::stop_token shutdownToken = LibcShutdownToken_nid_postfix(); std::uint64_t accepted = 0; std::uint64_t completed = 0; std::set completedOutOfOrder; std::exception_ptr failure; // Set once `failure` is, so hot loops can check for one without the mutex. std::atomic failed{false}; - bool stopping = false; + std::atomic stopping{false}; + bool stopped = false; bool resetGraphics = false; // Threads in WaitIdle (under `mutex`): a worker's completion notifies only while one waits. std::uint32_t idleWaiters = 0; @@ -1491,6 +1509,10 @@ private: } } + void checkStopping() const { + if (stopping.load(std::memory_order_acquire) || shutdownToken.stop_requested()) throw ProcessShutdown{}; + } + // Summarizes a rejected submission (packet names with counts and the first rejection reason per name). static void dumpPackets(std::span commands, const std::uint32_t* guest = nullptr) { std::map> summary; @@ -6116,7 +6138,7 @@ private: lock.lock(); } rethrowFailure(); - if (pending.empty()) { + if (stopping || shutdownToken.stop_requested() || pending.empty()) { break; } submission = std::move(pending.front()); @@ -6140,6 +6162,8 @@ private: else ++costs.notifiesSkipped; if (profile) costs.completeNs += static_cast(std::chrono::duration_cast(std::chrono::steady_clock::now() - completeStart).count()); } + } catch (const ProcessShutdown&) { + submission = Submission{}; } catch (...) { const auto error = std::current_exception(); try { @@ -6157,14 +6181,20 @@ private: } -void Submit(const Packet* packet, std::uint32_t queue) { +void Submit(const Packet* packet, std::uint32_t queue) try { Driver::Get().Submit(packet, queue); +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } void WaitIdle() { Driver::Get().WaitIdle(); } +void Shutdown() { + Driver::Get().Shutdown(); +} + void RegisterShader(const Shader* shader) { Driver::Get().RegisterShader(shader); } @@ -6199,16 +6229,26 @@ void ReportFailure(std::exception_ptr error) { } -extern "C" void AgcDriverWaitIdle_nid_postfix() { +extern "C" void AgcDriverWaitIdle_nid_postfix() try { AgcDriver::WaitIdle(); +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } -extern "C" void AgcDriverRegisterShader_nid_postfix(const Shader* shader) { +extern "C" void AgcDriverShutdown_nid_postfix() { + AgcDriver::Shutdown(); +} + +extern "C" void AgcDriverRegisterShader_nid_postfix(const Shader* shader) try { AgcDriver::RegisterShader(shader); +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } -extern "C" void AgcDriverSuspendPoint_nid_postfix() { +extern "C" void AgcDriverSuspendPoint_nid_postfix() try { AgcDriver::SuspendPoint(); +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } extern "C" void AgcDriverRegisterVideoOutput_nid_postfix(std::uint32_t handle, const std::shared_ptr& output) { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/WorkerSampler.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/WorkerSampler.cpp index 7e4d7a381..e48bdbbbc 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/WorkerSampler.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/WorkerSampler.cpp @@ -9,8 +9,11 @@ #include #include #include +#include #include #include +#include +#include #include #include #endif @@ -20,6 +23,9 @@ namespace AgcDriver { #ifdef _WIN32 namespace { +std::jthread processSamplerThread; +std::jthread workerSamplerThread; + // Debug aid: APS5_SAMPLE_WORKER= samples the calling thread every millisecond and, every 20 s, // writes "module+offset self inclusive" lines to for offline symbolization with nm. struct Sampler { @@ -29,6 +35,10 @@ struct Sampler { std::map> counts; std::uint64_t samples = 0; + ~Sampler() noexcept(false) { + if (target != nullptr && !CloseHandle(target)) throw std::system_error(GetLastError(), std::system_category(), "Closing sampler thread handle"); + } + void sample() { CONTEXT context{}; context.ContextFlags = CONTEXT_FULL; @@ -137,12 +147,12 @@ struct ProcessSampler { void StartProcessSampler() { const char* path = std::getenv("APS5_SAMPLE_THREADS"); if (path == nullptr) return; - std::thread([path = std::string(path)] { - auto* sampler = new ProcessSampler(); + processSamplerThread = std::jthread([path = std::string(path)](std::stop_token token) { + auto sampler = std::make_unique(); sampler->path = path; sampler->self = GetCurrentThreadId(); auto flushed = std::chrono::steady_clock::now(); - for (;;) { + while (!token.stop_requested()) { std::this_thread::sleep_for(std::chrono::milliseconds(2)); sampler->sample(); if (std::chrono::steady_clock::now() - flushed > std::chrono::seconds(20)) { @@ -150,7 +160,8 @@ void StartProcessSampler() { flushed = std::chrono::steady_clock::now(); } } - }).detach(); + sampler->write(); + }); } } @@ -159,12 +170,12 @@ void StartWorkerSampler() { StartProcessSampler(); const char* path = std::getenv("APS5_SAMPLE_WORKER"); if (path == nullptr) return; - auto* sampler = new Sampler(); + auto sampler = std::make_unique(); sampler->path = path; - DuplicateHandle(GetCurrentProcess(), GetCurrentThread(), GetCurrentProcess(), &sampler->target, THREAD_ALL_ACCESS, FALSE, 0); - std::thread([sampler] { + if (!DuplicateHandle(GetCurrentProcess(), GetCurrentThread(), GetCurrentProcess(), &sampler->target, THREAD_ALL_ACCESS, FALSE, 0)) throw std::system_error(GetLastError(), std::system_category(), "Duplicating sampler thread handle"); + workerSamplerThread = std::jthread([sampler = std::move(sampler)](std::stop_token token) { auto flushed = std::chrono::steady_clock::now(); - for (;;) { + while (!token.stop_requested()) { std::this_thread::sleep_for(std::chrono::milliseconds(1)); sampler->sample(); if (std::chrono::steady_clock::now() - flushed > std::chrono::seconds(20)) { @@ -172,10 +183,19 @@ void StartWorkerSampler() { flushed = std::chrono::steady_clock::now(); } } - }).detach(); + sampler->write(); + }); +} + +void StopWorkerSampler() { + processSamplerThread.request_stop(); + workerSamplerThread.request_stop(); + if (processSamplerThread.joinable()) processSamplerThread.join(); + if (workerSamplerThread.joinable()) workerSamplerThread.join(); } #else void StartWorkerSampler() {} +void StopWorkerSampler() {} #endif } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp index bfe9cc0e0..aca08e13e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp @@ -11,6 +11,8 @@ namespace AgcDriver::Graphics { +void ShutdownGuestBufferWorkers(); + struct GuestMemorySnapshot { std::uint64_t address; std::span bytes; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 7e331a10d..7213c9085 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -382,19 +383,15 @@ bool compareBlock(const RefreshBlock& block) { return true; } -// Helper threads for the block compares. An address-based build refreshes every writable image range -// (~15 MiB in ~300 blocks) under the device lock, and that memcmp was most of its cost; the blocks -// are handed out by an atomic cursor to the helpers and the calling thread alike, and Run returns -// once every block was taken and finished. The threads are started on first use and never joined -// (they only wait on the condition variable). APS5_MIRROR_REFRESH_THREADS sets the helper count -// (default 3); APS5_NO_PARALLEL_MIRROR_REFRESH=1 compares on the calling thread only. class RefreshPool { public: static RefreshPool& Get() { - // Never destroyed (as WorkerSampler's): the helpers block on its condition variable, which a - // static destructor at std::exit would tear down under them. - static RefreshPool* pool = new RefreshPool; - return *pool; + if (instance == nullptr) instance = new RefreshPool; + return *instance; + } + + static void Shutdown() { + delete std::exchange(instance, nullptr); } // Compares `blocks` and returns how many were copied. @@ -429,19 +426,12 @@ private: const char* text = std::getenv("APS5_MIRROR_REFRESH_THREADS"); const auto requested = text != nullptr ? std::atoi(text) : 3; const auto wanted = disabled ? 0u : static_cast(std::clamp(requested, 0, 16)); - // Only started threads are counted: Run waits for `helpers` finishes, and a thread that could - // not be started would leave it waiting forever. for (unsigned i = 0; i < wanted; ++i) { - try { - std::thread([this] { - CpuTopology::PinHelperThread("mirror refresh"); - helper(); - }).detach(); - ++helpers; - } catch (const std::system_error& error) { - std::fprintf(stderr, "[gpu] mirror refresh helper %u not started: %s\n", i, error.what()); - break; - } + threads.emplace_back([this](std::stop_token token) { + CpuTopology::PinHelperThread("mirror refresh"); + helper(token); + }); + ++helpers; } } @@ -452,13 +442,13 @@ private: copiedBlocks.fetch_add(copied, std::memory_order_relaxed); } - void helper() { + void helper(std::stop_token token) { std::uint64_t seen = 0; for (;;) { std::span blocks; { std::unique_lock lock(mutex); - wake.wait(lock, [&] { return serial != seen; }); + if (!wake.wait(lock, token, [&] { return serial != seen; })) return; seen = serial; blocks = job; } @@ -471,16 +461,18 @@ private: } } + inline static RefreshPool* instance = nullptr; unsigned helpers = 0; std::mutex runMutex; std::mutex mutex; - std::condition_variable wake; + std::condition_variable_any wake; std::condition_variable done; std::span job; std::uint64_t serial = 0; unsigned finished = 0; std::atomic next{0}; std::atomic copiedBlocks{0}; + std::vector threads; }; // The device-lock work before [address, address + bytes) of a writable mirror can be compared with @@ -1357,6 +1349,10 @@ void reportStaging() { } +void ShutdownGuestBufferWorkers() { + RefreshPool::Shutdown(); +} + GuestBufferMemory::~GuestBufferMemory() { static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; if (!profile) return; diff --git a/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp b/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp index 05a53f833..a24efd186 100644 --- a/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp +++ b/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp @@ -17,6 +17,7 @@ #include "prx/libSceAgcDriver/Execution/include/DisplayBuffer.hpp" #include "prx/libSceVideoOut/include/DisplayWindow.hpp" #include "prx/libSceVideoOut/include/BufferReuseTracker.hpp" +#include "prx/libc/include/Shutdown.hpp" #include "SDL.h" #include "SceTypes.hpp" @@ -100,6 +101,7 @@ struct VideoOutConfig { bool opened = false; bool closing = false; std::exception_ptr failure; + std::stop_token shutdownToken = LibcShutdownToken_nid_postfix(); int flipRate = 0; uint64_t lastFlipVblank = 0; std::chrono::steady_clock::time_point lastTimingFlip{}; @@ -117,6 +119,7 @@ struct VideoOutConfig { void Check() const { if (failure) std::rethrow_exception(failure); + if (shutdownToken.stop_requested()) throw ProcessShutdown{}; if (!opened || closing) throw std::runtime_error("VideoOut: port is closed"); } }; diff --git a/core/libs/prx/libSceVideoOut/src/Buffer.cpp b/core/libs/prx/libSceVideoOut/src/Buffer.cpp index 28430fe29..aa40461e1 100644 --- a/core/libs/prx/libSceVideoOut/src/Buffer.cpp +++ b/core/libs/prx/libSceVideoOut/src/Buffer.cpp @@ -35,7 +35,7 @@ void APS5_VABI sceVideoOutSetBufferAttribute2(VideoOutBufferAttribute2* attribut attribute->dcc_control = dccControl; } -int APS5_VABI sceVideoOutRegisterBuffers2(int handle, int setIndex, int bufferIndexStart, const VideoOutBuffers* buffers, int bufferNum, const VideoOutBufferAttribute2* attribute, int category, void* option) { +int APS5_VABI sceVideoOutRegisterBuffers2(int handle, int setIndex, int bufferIndexStart, const VideoOutBuffers* buffers, int bufferNum, const VideoOutBufferAttribute2* attribute, int category, void* option) try { auto cfg = VideoOutDriver::Get().GetConfig(handle); if (cfg == nullptr) { throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_HANDLE"); @@ -88,9 +88,11 @@ int APS5_VABI sceVideoOutRegisterBuffers2(int handle, int setIndex, int bufferIn cfg->buffers[bufferIndexStart + i] = registered[i]; } return 0; +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } -int APS5_VABI sceVideoOutSubmitChangeBufferAttribute2(int handle, int setIndex, const VideoOutBufferAttribute2* attribute, void* option) { +int APS5_VABI sceVideoOutSubmitChangeBufferAttribute2(int handle, int setIndex, const VideoOutBufferAttribute2* attribute, void* option) try { auto cfg = VideoOutDriver::Get().GetConfig(handle); if (cfg == nullptr) { throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_HANDLE"); @@ -120,9 +122,11 @@ int APS5_VABI sceVideoOutSubmitChangeBufferAttribute2(int handle, int setIndex, } cfg->groups[setIndex] = group; return 0; +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } -int APS5_VABI sceVideoOutUnregisterBuffers(int handle, int setIndex) { +int APS5_VABI sceVideoOutUnregisterBuffers(int handle, int setIndex) try { auto cfg = VideoOutDriver::Get().GetConfig(handle); if (cfg == nullptr) { throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_HANDLE"); @@ -148,9 +152,11 @@ int APS5_VABI sceVideoOutUnregisterBuffers(int handle, int setIndex) { } } return 0; +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } -int APS5_VABI sceVideoOutSubmitFlip(int handle, int index, int flipMode, int64_t flipArg) { +int APS5_VABI sceVideoOutSubmitFlip(int handle, int index, int flipMode, int64_t flipArg) try { auto cfg = VideoOutDriver::Get().GetConfig(handle); if (cfg == nullptr) { throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_HANDLE"); @@ -173,6 +179,8 @@ int APS5_VABI sceVideoOutSubmitFlip(int handle, int index, int flipMode, int64_t } lock.unlock(); return VideoOutDriver::Get().SubmitFlip(handle, index, flipMode, flipArg); +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } } diff --git a/core/libs/prx/libSceVideoOut/src/Event.cpp b/core/libs/prx/libSceVideoOut/src/Event.cpp index fcf64b9e9..5e3b169bf 100644 --- a/core/libs/prx/libSceVideoOut/src/Event.cpp +++ b/core/libs/prx/libSceVideoOut/src/Event.cpp @@ -107,35 +107,49 @@ static int deleteVideoOutEvent(int handle, KernelEqueue eq, int16_t eventKind) { extern "C" { -int APS5_VABI sceVideoOutAddFlipEvent(KernelEqueue eq, int handle, void* udata) { +int APS5_VABI sceVideoOutAddFlipEvent(KernelEqueue eq, int handle, void* udata) try { return registerVideoOutEvent(handle, eq, VIDEO_OUT_EVENT_FLIP, udata); +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } -int APS5_VABI sceVideoOutAddVblankEvent(KernelEqueue eq, int handle, void* udata) { +int APS5_VABI sceVideoOutAddVblankEvent(KernelEqueue eq, int handle, void* udata) try { return registerVideoOutEvent(handle, eq, VIDEO_OUT_EVENT_VBLANK, udata); +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } -int APS5_VABI sceVideoOutAddPreVblankStartEvent(KernelEqueue eq, int handle, void* udata) { +int APS5_VABI sceVideoOutAddPreVblankStartEvent(KernelEqueue eq, int handle, void* udata) try { static_cast(eq); static_cast(udata); VideoOutDriver::Get().GetConfig(handle); throw std::runtime_error("VideoOut: physical pre-vblank timing is not implemented"); +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } -int APS5_VABI sceVideoOutAddOutputModeEvent(KernelEqueue eq, int handle, void* udata) { +int APS5_VABI sceVideoOutAddOutputModeEvent(KernelEqueue eq, int handle, void* udata) try { return registerVideoOutEvent(handle, eq, VIDEO_OUT_EVENT_SET_MODE, udata); +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } -int APS5_VABI sceVideoOutDeleteFlipEvent(KernelEqueue eq, int handle) { +int APS5_VABI sceVideoOutDeleteFlipEvent(KernelEqueue eq, int handle) try { return deleteVideoOutEvent(handle, eq, VIDEO_OUT_EVENT_FLIP); +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } -int APS5_VABI sceVideoOutDeleteVblankEvent(KernelEqueue eq, int handle) { +int APS5_VABI sceVideoOutDeleteVblankEvent(KernelEqueue eq, int handle) try { return deleteVideoOutEvent(handle, eq, VIDEO_OUT_EVENT_VBLANK); +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } -int APS5_VABI sceVideoOutDeletePreVblankStartEvent(KernelEqueue eq, int handle) { +int APS5_VABI sceVideoOutDeletePreVblankStartEvent(KernelEqueue eq, int handle) try { return deleteVideoOutEvent(handle, eq, VIDEO_OUT_EVENT_PRE_VBLANK_START); +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } int APS5_VABI sceVideoOutGetEventId(const KernelEvent* ev) { diff --git a/core/libs/prx/libSceVideoOut/src/Output.cpp b/core/libs/prx/libSceVideoOut/src/Output.cpp index c859772ca..e1038cff4 100644 --- a/core/libs/prx/libSceVideoOut/src/Output.cpp +++ b/core/libs/prx/libSceVideoOut/src/Output.cpp @@ -29,7 +29,7 @@ static int validateOutputConfig(int handle, uint64_t mode, const VideoOutOutputO extern "C" { -int APS5_VABI sceVideoOutOpen(int userId, int busType, int index, const void* param) { +int APS5_VABI sceVideoOutOpen(int userId, int busType, int index, const void* param) try { if (param != nullptr) { throw std::runtime_error(std::string(__func__) + ": param not implemented"); } @@ -47,13 +47,17 @@ int APS5_VABI sceVideoOutOpen(int userId, int busType, int index, const void* pa throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_RESOURCE_BUSY"); } return handle; +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } -int APS5_VABI sceVideoOutClose(int handle) { +int APS5_VABI sceVideoOutClose(int handle) try { return VideoOutDriver::Get().Close(handle) ? 0 : VIDEO_OUT_ERROR_INVALID_HANDLE; +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } -int APS5_VABI sceVideoOutSetFlipRate(int handle, int rate) { +int APS5_VABI sceVideoOutSetFlipRate(int handle, int rate) try { auto cfg = VideoOutDriver::Get().GetConfig(handle); if (cfg == nullptr) { throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_HANDLE"); @@ -65,9 +69,11 @@ int APS5_VABI sceVideoOutSetFlipRate(int handle, int rate) { cfg->Check(); cfg->flipRate = rate; return 0; +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } -int APS5_VABI sceVideoOutGetFlipStatus(int handle, VideoOutFlipStatus* status) { +int APS5_VABI sceVideoOutGetFlipStatus(int handle, VideoOutFlipStatus* status) try { if (status == nullptr) { throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_ADDRESS"); } @@ -79,9 +85,11 @@ int APS5_VABI sceVideoOutGetFlipStatus(int handle, VideoOutFlipStatus* status) { cfg->Check(); *status = cfg->flipStatus; return 0; +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } -int APS5_VABI sceVideoOutGetVblankStatus(int handle, VideoOutVblankStatus* status) { +int APS5_VABI sceVideoOutGetVblankStatus(int handle, VideoOutVblankStatus* status) try { if (status == nullptr) { throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_ADDRESS"); } @@ -93,9 +101,11 @@ int APS5_VABI sceVideoOutGetVblankStatus(int handle, VideoOutVblankStatus* statu cfg->Check(); *status = cfg->vblankStatus; return 0; +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } -int APS5_VABI sceVideoOutGetOutputStatus(int handle, VideoOutOutputStatus* status) { +int APS5_VABI sceVideoOutGetOutputStatus(int handle, VideoOutOutputStatus* status) try { if (status == nullptr) { throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_ADDRESS"); } @@ -113,9 +123,11 @@ int APS5_VABI sceVideoOutGetOutputStatus(int handle, VideoOutOutputStatus* statu status->reserved[1] = 0; status->reserved[2] = 0; return 0; +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } -int APS5_VABI sceVideoOutIsFlipPending(int handle) { +int APS5_VABI sceVideoOutIsFlipPending(int handle) try { auto cfg = VideoOutDriver::Get().GetConfig(handle); if (cfg == nullptr) { throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_HANDLE"); @@ -123,9 +135,11 @@ int APS5_VABI sceVideoOutIsFlipPending(int handle) { std::unique_lock lock(cfg->mutex); cfg->Check(); return cfg->flipStatus.flipPendingNum; +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } -int APS5_VABI sceVideoOutWaitVblank(int handle) { +int APS5_VABI sceVideoOutWaitVblank(int handle) try { auto cfg = VideoOutDriver::Get().GetConfig(handle); if (cfg == nullptr) { throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_HANDLE"); @@ -133,12 +147,11 @@ int APS5_VABI sceVideoOutWaitVblank(int handle) { std::unique_lock lock(cfg->mutex); cfg->Check(); const uint64_t count = cfg->vblankStatus.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"); + cfg->vblankCond.wait(lock, cfg->shutdownToken, [&] { return !cfg->opened || cfg->closing || cfg->failure || cfg->vblankStatus.count != count; }); + cfg->Check(); return 0; +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } int APS5_VABI sceVideoOutInitializeOutputOptions(VideoOutOutputOptions* options) { @@ -149,15 +162,17 @@ int APS5_VABI sceVideoOutInitializeOutputOptions(VideoOutOutputOptions* options) return 0; } -int APS5_VABI sceVideoOutIsOutputSupported(int handle, uint64_t mode, const VideoOutOutputOptions* options, void* reservedPtr, uint64_t reserved) { +int APS5_VABI sceVideoOutIsOutputSupported(int handle, uint64_t mode, const VideoOutOutputOptions* options, void* reservedPtr, uint64_t reserved) try { const int result = validateOutputConfig(handle, mode, options, reservedPtr, reserved); if (result != 0) { return result; } return (mode == VIDEO_OUT_OUTPUT_MODE_119_88HZ) ? 0 : 1; +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } -int APS5_VABI sceVideoOutConfigureOutput(int handle, uint64_t mode, const VideoOutOutputOptions* options, void* reservedPtr, uint64_t reserved) { +int APS5_VABI sceVideoOutConfigureOutput(int handle, uint64_t mode, const VideoOutOutputOptions* options, void* reservedPtr, uint64_t reserved) try { const int supported = sceVideoOutIsOutputSupported(handle, mode, options, reservedPtr, reserved); if (supported < 0) { return supported; @@ -173,30 +188,38 @@ int APS5_VABI sceVideoOutConfigureOutput(int handle, uint64_t mode, const VideoO cfg->Check(); cfg->outputMode = mode; return 0; +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } -int APS5_VABI sceVideoOutSetWindowModeMargins(int handle, int top, int bottom) { +int APS5_VABI sceVideoOutSetWindowModeMargins(int handle, int top, int bottom) try { (void)top; (void)bottom; if (!VideoOutDriver::Get().IsOpen(handle)) { throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_HANDLE"); } return 0; +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } -int APS5_VABI sceVideoOutLatencyControlWaitBeforeInput(int handle) { +int APS5_VABI sceVideoOutLatencyControlWaitBeforeInput(int handle) try { if (!VideoOutDriver::Get().IsOpen(handle)) { throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_HANDLE"); } return 0; +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } -int APS5_VABI sceVideoOutLatencyMeasureSetStartPoint(int handle, uint32_t point) { +int APS5_VABI sceVideoOutLatencyMeasureSetStartPoint(int handle, uint32_t point) try { (void)point; if (!VideoOutDriver::Get().IsOpen(handle)) { throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_HANDLE"); } return 0; +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } int APS5_VABI sceVideoOutColorSettingsSetGamma(VideoOutColorSettings* settings, float gamma) { @@ -210,7 +233,7 @@ int APS5_VABI sceVideoOutColorSettingsSetGamma(VideoOutColorSettings* settings, return 0; } -int APS5_VABI sceVideoOutAdjustColor(int handle, const VideoOutColorSettings* settings) { +int APS5_VABI sceVideoOutAdjustColor(int handle, const VideoOutColorSettings* settings) try { if (settings == nullptr) { throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_ADDRESS"); } @@ -220,6 +243,8 @@ int APS5_VABI sceVideoOutAdjustColor(int handle, const VideoOutColorSettings* se } // Output gamma is accepted but not yet applied to presentation. return 0; +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } } diff --git a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp index c2d62978b..3ea9f206a 100644 --- a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp +++ b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp @@ -40,7 +40,7 @@ public: : _config(std::move(config)), _index(index), _ticket(ticket) {} void Wait() override { std::unique_lock lock(_config->mutex); - _config->vblankCond.wait(lock, [&] { + _config->vblankCond.wait(lock, _config->shutdownToken, [&] { return _config->failure || _config->closing || !_config->opened || _config->bufferReuse[_index].IsComplete(_ticket); }); @@ -76,13 +76,14 @@ public: if (waitWhenFull) { int waited = 0; while (queue->reservations.load() >= VIDEO_OUT_FLIP_QUEUE_CAPACITY) { + if (cfg->shutdownToken.stop_requested()) throw ProcessShutdown{}; std::this_thread::sleep_for(std::chrono::milliseconds(10)); if (++waited % 500 == 0) std::fprintf(stderr, "[flip] queue full for %d s; waiting (APS5_FLIP_QUEUE_WAIT)\n", waited / 100); } } std::lock_guard queueLock(queue->mutex); if (queue->failure) std::rethrow_exception(queue->failure); - require(!queue->stopping, "flip during shutdown"); + if (queue->stopping || cfg->shutdownToken.stop_requested()) throw ProcessShutdown{}; std::lock_guard lock(cfg->mutex); checkConfig(*cfg); require(queue->reservations.load() < VIDEO_OUT_FLIP_QUEUE_CAPACITY, "flip queue full"); @@ -153,7 +154,7 @@ void FlipRequest::GpuReady(const std::shared_ptr& frameT AgcDriver::PerformanceTimer readiness("VideoOut.Readiness"); std::lock_guard queueLock(queue->mutex); if (queue->failure) std::rethrow_exception(queue->failure); - require(!queue->stopping, "GPU flip during shutdown"); + if (queue->stopping || cfg->shutdownToken.stop_requested()) throw ProcessShutdown{}; std::lock_guard lock(cfg->mutex); checkConfig(*cfg); require(reserved && !ready && !terminal && cfg->generation == generation, "invalid flip readiness transition"); @@ -172,7 +173,7 @@ void FlipRequest::GpuReady(const std::shared_ptr& frameT static const bool syncFlip = std::getenv("APS5_SYNC_FLIP") != nullptr; if (!syncFlip) return; std::unique_lock lock(cfg->mutex); - cfg->vblankCond.wait(lock, [&] { return gpuComplete || cfg->failure || cfg->closing; }); + cfg->vblankCond.wait(lock, cfg->shutdownToken, [&] { return gpuComplete || cfg->failure || cfg->closing; }); checkConfig(*cfg); require(gpuComplete, "flip preparation did not complete on GPU"); } @@ -232,24 +233,21 @@ 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 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); - } + std::lock_guard queueLock(flipQueue->mutex); + flipQueue->stopping = true; } presentThread.request_stop(); vblankThread.request_stop(); flipQueue->changed.notify_all(); presentThread.join(); vblankThread.join(); - if (window.Handle() != nullptr) { - AgcDriverReleaseWindow_nid_postfix(window.Handle()); - window.Destroy(); + { + 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; @@ -262,7 +260,7 @@ int VideoOutDriver::Open(int busType) { { std::lock_guard queueLock(flipQueue->mutex); if (flipQueue->failure) std::rethrow_exception(flipQueue->failure); - require(!flipQueue->stopping, "open during shutdown"); + 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"); @@ -288,6 +286,7 @@ int VideoOutDriver::Open(int busType) { bool VideoOutDriver::Close(int handle) { std::lock_guard lock(mutex); + if (LibcShutdownToken_nid_postfix().stop_requested()) throw ProcessShutdown{}; return close(handle); } @@ -322,7 +321,7 @@ std::shared_ptr VideoOutDriver::GetConfig(int handle) { { std::lock_guard lock(flipQueue->mutex); if (flipQueue->failure) std::rethrow_exception(flipQueue->failure); - require(!flipQueue->stopping, "output is shutting down"); + 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"); @@ -337,6 +336,7 @@ bool VideoOutDriver::IsOpen(int handle) { } 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; @@ -392,7 +392,7 @@ void VideoOutDriver::processFlip(FlipRequest& req) { 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, [&] { return req.cfg->vblankStatus.count >= target || req.cfg->failure || req.cfg->closing; }); + 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); } @@ -481,7 +481,10 @@ void VideoOutDriver::presentLoop(std::stop_token token) { } SDL_Event event; while (SDL_PollEvent(&event)) { - require(event.type != SDL_QUIT, "window was closed"); + 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())); } @@ -503,15 +506,7 @@ void VideoOutDriver::presentLoop(std::stop_token token) { } current.reset(); } - std::list> cancelled; - { - std::lock_guard lock(flipQueue->mutex); - cancelled.swap(flipQueue->requests); - } - 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 (const ProcessShutdown&) { } catch (const std::exception& error) { std::fprintf(stderr, "[videoout] presentation failed: %s\n", error.what()); std::fflush(stderr); @@ -521,6 +516,16 @@ void VideoOutDriver::presentLoop(std::stop_token token) { std::fflush(stderr); std::terminate(); } + current.reset(); + { + std::list> cancelled; + { + std::lock_guard lock(flipQueue->mutex); + cancelled.swap(flipQueue->requests); + } + } + AgcDriverShutdown_nid_postfix(); + window.Destroy(); } void VideoOutDriver::vblankLoop(std::stop_token token) { diff --git a/core/libs/prx/libc/include/Shutdown.hpp b/core/libs/prx/libc/include/Shutdown.hpp index c1ff16604..dc4007098 100644 --- a/core/libs/prx/libc/include/Shutdown.hpp +++ b/core/libs/prx/libc/include/Shutdown.hpp @@ -1,8 +1,16 @@ #ifndef CORE_LIBS_PRX_LIBC_INCLUDE_SHUTDOWN_HPP #define CORE_LIBS_PRX_LIBC_INCLUDE_SHUTDOWN_HPP +#include + +class ProcessShutdown final {}; + extern "C" void LibcRegisterShutdown_nid_postfix(void (*callback)()); extern "C" void LibcRunShutdown_nid_postfix(); +extern "C" std::stop_token LibcShutdownToken_nid_postfix(); +extern "C" void LibcRequestShutdown_nid_postfix(); +extern "C" void LibcRequestExit_nid_postfix(int code); +extern "C" [[noreturn]] void LibcAwaitExit_nid_postfix(); extern "C" [[noreturn]] void LibcExit_nid_no_patch(int code); #endif diff --git a/core/libs/prx/libc/src/Process.cpp b/core/libs/prx/libc/src/Process.cpp index 7a8dcc613..0e8a39c0c 100644 --- a/core/libs/prx/libc/src/Process.cpp +++ b/core/libs/prx/libc/src/Process.cpp @@ -8,6 +8,8 @@ #include #include #include +#include +#include #include "prx/libc/include/Shutdown.hpp" #include "prx/libc/include/General.hpp" @@ -22,9 +24,35 @@ std::thread::id shutdownThread; bool shutdownStarted = false; bool shutdownFinished = false; std::exception_ptr shutdownFailure; +std::stop_source shutdownSource; +std::atomic exitRequested{false}; +std::atomic exitStarted{false}; } +extern "C" std::stop_token LibcShutdownToken_nid_postfix() { + return shutdownSource.get_token(); +} + +extern "C" void LibcRequestShutdown_nid_postfix() { + shutdownSource.request_stop(); +} + +extern "C" void LibcRequestExit_nid_postfix(int code) { + if (exitRequested.exchange(true)) return; + LibcRequestShutdown_nid_postfix(); + std::thread([code] { LibcExit_nid_no_patch(code); }).detach(); +} + +extern "C" [[noreturn]] void LibcAwaitExit_nid_postfix() { + if (!exitRequested.load()) throw ProcessShutdown{}; + { + std::lock_guard lock(shutdownMutex); + if (shutdownThread == std::this_thread::get_id()) throw std::runtime_error("libc: exit thread cannot wait for itself"); + } + for (;;) exitRequested.wait(true); +} + 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"); @@ -43,6 +71,7 @@ extern "C" void LibcRunShutdown_nid_postfix() { shutdownThread = std::this_thread::get_id(); auto callbacks = std::move(shutdownCallbacks); lock.unlock(); + LibcRequestShutdown_nid_postfix(); std::exception_ptr error; for (auto it = callbacks.rbegin(); it != callbacks.rend(); ++it) { try { (*it)(); } @@ -68,6 +97,8 @@ extern "C" { } [[noreturn]] void LibcExit_nid_no_patch(int code) { + exitRequested.store(true); + if (exitStarted.exchange(true)) LibcAwaitExit_nid_postfix(); LibcRunShutdown_nid_postfix(); std::exit(code); } diff --git a/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp b/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp index 53d33430b..5359fcd33 100644 --- a/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp +++ b/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp @@ -355,7 +355,9 @@ void InvalidParameterHandler(const wchar_t*, const wchar_t*, const wchar_t*, uns // Static destruction runs on the thread that ends the process (ExitProcess -> DLL_PROCESS_DETACH), // so its stack names the exit's caller when no libc exit/abort path was taken. struct ExitReporter { - ~ExitReporter() { ReportBacktrace("process exit (static destruction)"); } + ~ExitReporter() { + if (std::getenv("APS5_TRACE_EXIT") != nullptr) ReportBacktrace("process exit (static destruction)"); + } } g_exitReporter; const bool g_crashReportInstalled = [] {