fix(kernel): orderly shutdown when closing the display window

This commit is contained in:
boykopovar 2026-09-28 18:30:34 +03:00
parent 143aec54c2
commit 39fd90b050
14 changed files with 265 additions and 109 deletions

View File

@ -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);

View File

@ -5,6 +5,7 @@ namespace AgcDriver {
// Starts sampling the calling thread when APS5_SAMPLE_WORKER names an output file.
void StartWorkerSampler();
void StopWorkerSampler();
}

View File

@ -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<std::uint64_t>::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<std::uint64_t>::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<std::shared_ptr<VulkanDevice>> replacedDevices;
replacedDevices.push_back(device);
}
device = std::make_shared<VulkanDevice>(&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<std::uint32_t, QueueState> queues;
std::map<std::uint32_t, std::shared_ptr<IVideoOutput>> outputs;
DevicePointer device;
std::vector<std::shared_ptr<VulkanDevice>> replacedDevices;
std::stop_token shutdownToken = LibcShutdownToken_nid_postfix();
std::uint64_t accepted = 0;
std::uint64_t completed = 0;
std::set<std::uint64_t> completedOutOfOrder;
std::exception_ptr failure;
// Set once `failure` is, so hot loops can check for one without the mutex.
std::atomic<bool> failed{false};
bool stopping = false;
std::atomic<bool> 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<const std::uint32_t> commands, const std::uint32_t* guest = nullptr) {
std::map<std::string, std::pair<std::size_t, std::string>> 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::uint64_t>(std::chrono::duration_cast<std::chrono::nanoseconds>(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<AgcDriver::IVideoOutput>& output) {

View File

@ -9,8 +9,11 @@
#include <atomic>
#include <chrono>
#include <map>
#include <memory>
#include <mutex>
#include <string>
#include <stop_token>
#include <system_error>
#include <thread>
#include <vector>
#endif
@ -20,6 +23,9 @@ namespace AgcDriver {
#ifdef _WIN32
namespace {
std::jthread processSamplerThread;
std::jthread workerSamplerThread;
// Debug aid: APS5_SAMPLE_WORKER=<file> samples the calling thread every millisecond and, every 20 s,
// writes "module+offset self inclusive" lines to <file> for offline symbolization with nm.
struct Sampler {
@ -29,6 +35,10 @@ struct Sampler {
std::map<std::uint64_t, std::pair<std::uint64_t, std::uint64_t>> 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<ProcessSampler>();
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>();
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
}

View File

@ -11,6 +11,8 @@
namespace AgcDriver::Graphics {
void ShutdownGuestBufferWorkers();
struct GuestMemorySnapshot {
std::uint64_t address;
std::span<const std::byte> bytes;

View File

@ -17,6 +17,7 @@
#include <mutex>
#include <set>
#include <thread>
#include <stop_token>
#include <cstdlib>
#include <cstdio>
#include <chrono>
@ -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<unsigned>(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<const RefreshBlock> 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<const RefreshBlock> job;
std::uint64_t serial = 0;
unsigned finished = 0;
std::atomic<std::size_t> next{0};
std::atomic<std::uint64_t> copiedBlocks{0};
std::vector<std::jthread> 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;

View File

@ -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");
}
};

View File

@ -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();
}
}

View File

@ -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<void>(eq);
static_cast<void>(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) {

View File

@ -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();
}
}

View File

@ -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<AgcDriver::FrameTiming>& 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<AgcDriver::FrameTiming>& 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<VideoOutConfig> VideoOutDriver::GetConfig(int handle) {
{
std::lock_guard lock(flipQueue->mutex);
if (flipQueue->failure) std::rethrow_exception(flipQueue->failure);
require(!flipQueue->stopping, "output is shutting down");
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<uint64_t>::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<std::shared_ptr<FlipRequest>> 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<std::shared_ptr<FlipRequest>> cancelled;
{
std::lock_guard lock(flipQueue->mutex);
cancelled.swap(flipQueue->requests);
}
}
AgcDriverShutdown_nid_postfix();
window.Destroy();
}
void VideoOutDriver::vblankLoop(std::stop_token token) {

View File

@ -1,8 +1,16 @@
#ifndef CORE_LIBS_PRX_LIBC_INCLUDE_SHUTDOWN_HPP
#define CORE_LIBS_PRX_LIBC_INCLUDE_SHUTDOWN_HPP
#include <stop_token>
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

View File

@ -8,6 +8,8 @@
#include <mutex>
#include <thread>
#include <vector>
#include <atomic>
#include <stdexcept>
#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<bool> exitRequested{false};
std::atomic<bool> 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);
}

View File

@ -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 = [] {