Limit a game's stop to ten seconds: past that the app asks the system to start it again at the launcher

This commit is contained in:
BlackBearReloaded committed 2026-10-03 07:59:47 -04:00
1 parent 55dbb7df4a
commit ec7c81960c
8 files changed
+259 -4

No files matched your search

+3 -2
View File
@@ -2112,7 +2112,7 @@ void Initialize() {
endif()
add_executable(eden-headless "${EDEN_PORT_DIR}/main.cpp" "${PORT_ROOT}/src/aligned_alloc.c"
"${EDEN_PORT_DIR}/heap_pages.cpp" "${EDEN_PORT_DIR}/performance.cpp"
"${EDEN_PORT_DIR}/crash_report.cpp"
"${EDEN_PORT_DIR}/crash_report.cpp" "${EDEN_PORT_DIR}/stop_limit.cpp"
"${EDEN_PORT_DIR}/elevation/elevation.cpp"
"${eden_app_crt}" "${PORT_ROOT}/src/lifecycle.c"
"${PORT_BUILD_DIR}/app_heap.c")
@@ -2127,7 +2127,8 @@ void Initialize() {
endif()
target_compile_definitions(common PRIVATE PS5_NATIVE=1)
else()
add_executable(eden-headless "${EDEN_PORT_DIR}/main.cpp" "${PORT_ROOT}/src/aligned_alloc.c")
add_executable(eden-headless "${EDEN_PORT_DIR}/main.cpp" "${PORT_ROOT}/src/aligned_alloc.c"
"${EDEN_PORT_DIR}/stop_limit.cpp")
target_link_options(eden-headless PRIVATE -Wl,--wrap=aligned_alloc)
add_executable(eden-romfs-check "${EDEN_PORT_DIR}/romfs_check.cpp")
target_include_directories(eden-romfs-check PRIVATE "${PROJECT_SOURCE_DIR}/src")
+25
View File
@@ -89,9 +89,13 @@ extern "C" std::int64_t sceKernelGetDirectMemorySize();
extern "C" bool eden_jit_shared; // headless/dynarmic/jit_group_support.inc
#endif
#include "video_core/gpu.h"
#include "stop_limit.h"
namespace Common {
bool SparseTablesAvailable() noexcept; // src/memory_pages.cpp
}
#if defined(EDEN_DEV_PROFILE) || defined(EDEN_DEV_ROM_ID)
static bool dev_stop_hang = false; // dev-settings stop_hang=on
#endif
class HeadlessWindow final : public Core::Frontend::EmuWindow {
public:
@@ -233,6 +237,13 @@ int main(int argc, char** argv) {
// Whether Eden's large tables can be sparse on this console (src/memory_pages.cpp),
// decided now: every session's log says it, with or without a game.
(void)Common::SparseTablesAvailable();
{
// A game that would not stop made the app start again (stop_limit.h): this log says so.
const std::string note = Eden::LogFile("stop-limit.txt");
if (std::remove(note.c_str()) == 0)
Eden::Report("exit", "The last game did not stop in time, so ProsperoEden started again");
Eden::StopLimit::Start(note);
}
report = std::fopen(Eden::LogFile("result.tsv").c_str(), "w");
if (!report) { report = stdout; return 2; }
std::puts("[headless-startup] directories_ready");
@@ -324,6 +335,7 @@ int main(int argc, char** argv) {
#endif
#endif
for (;;) {
Eden::StopLimit::End(); // whatever game ran before is gone
#ifdef EDEN_PS5_OPENGL
std::error_code trim_error;
const auto cache_entries = Eden::ReadNativeDirectory(native_shader_cache, trim_error);
@@ -626,6 +638,8 @@ int main(int argc, char** argv) {
} else if (entry == "large_pages=off" || entry == "sparse_tables=off" || entry == "heap=whole") {
// Read directly by the page allocator (src/memory_pages.cpp) before this parse:
// 16 KiB pages only; Eden's large tables dense; the heap's 3 GiB taken at start.
} else if (entry == "stop_hang=on") {
dev_stop_hang = true;
} else if (entry == "graphics_usage=driver") {
// The caches' "memory in use" as the driver counts it (performance.h).
Eden::Performance::graphics_usage_from_pool = false;
@@ -1262,6 +1276,8 @@ int main(int argc, char** argv) {
(void)pad->TakeHudToggle();
#endif
if (pad->TakeReturnToMenu()) {
// From here the player is waiting to leave (stop_limit.h).
Eden::StopLimit::Begin();
std::lock_guard lock(completion->mutex);
completion->return_to_menu = true;
completion->wake.notify_one();
@@ -1381,10 +1397,19 @@ int main(int argc, char** argv) {
const auto neutral = ps5::pad::neutral_data();
pad->Consume({&neutral, 1});
}
// A game that ended itself or failed is being stopped too (stop_limit.h).
Eden::StopLimit::Begin();
jit_list.Finish(); // while the JITs still exist: their block list is saved
// Shutdown requests cancellation before suspending cores; Pause can
// block while a CPU producer is waiting on a full GPU queue.
Eden::ReportStep("shutdown", "Stopping the game");
#if defined(EDEN_DEV_PROFILE) || defined(EDEN_DEV_ROM_ID)
if (dev_stop_hang) {
// dev-settings stop_hang=on: a stop that never finishes, to check its limit.
Eden::Report("shutdown", "Development: holding the stop to check its limit");
for (;;) std::this_thread::sleep_for(std::chrono::seconds(1));
}
#endif
system.ShutdownMainProcess();
Eden::Report("shutdown", "Game stopped; releasing renderer");
#ifndef PS5_NATIVE
+8
View File
@@ -22,6 +22,7 @@
#ifdef EDEN_DEV_ROM_ID
#include "crash_trigger.h"
#include "development_input.h"
#include "stop_limit.h"
#include <fstream>
#endif
@@ -262,6 +263,13 @@ std::string RunApp(const std::string& launch_error, bool first_start) {
}
// The runner's crash request, to test the crash report in the launcher.
Eden::Crash::DevelopmentRequest(Eden::AppFile("crash-app.txt"));
// The check of what a stop that runs into its limit does (stop_limit.h): the app
// starts again, here from an idle launcher.
if (std::remove(Eden::AppFile("restart-app.txt").c_str()) == 0) {
std::fprintf(stderr, "EDEN_DEV_RESTART requested=1\n");
Eden::StopLimit::RestartNow();
std::fprintf(stderr, "EDEN_DEV_RESTART refused=1\n");
}
}
if (!development_input.active) radio_input_poll();
if (const auto sample = development_input.Sample(now)) {
+105
View File
@@ -0,0 +1,105 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#include "stop_limit.h"
#include <condition_variable>
#include <fcntl.h>
#include <mutex>
#include <thread>
#include <unistd.h>
#include <utility>
#ifdef PS5_NATIVE
extern "C" int eden_restart_app(void);
extern "C" int sceKernelDebugOutText(int, const char*);
#endif
namespace Eden::StopLimit {
namespace {
struct State {
std::mutex mutex;
std::condition_variable wake;
bool watching = false;
bool armed = false;
std::chrono::steady_clock::time_point deadline;
std::chrono::milliseconds limit = kLimit;
void (*at_limit)() noexcept = nullptr;
std::string note;
};
// Never destroyed: the watcher is a detached thread and must not meet a destroyed mutex at exit.
State& state() {
static State* const instance = new State;
return *instance;
}
void Watch() {
State& s = state();
std::unique_lock lock(s.mutex);
for (;;) {
if (!s.armed) {
s.wake.wait(lock);
continue;
}
const auto deadline = s.deadline;
if (s.wake.wait_until(lock, deadline) != std::cv_status::timeout) continue;
// Timed out: still the same stop?
if (!s.armed || s.deadline != deadline) continue;
s.armed = false;
const auto action = s.at_limit ? s.at_limit : &RestartNow;
lock.unlock();
action(); // on the console this returns only when the system refuses the restart
lock.lock();
}
}
} // namespace
void Start(std::string note) noexcept {
State& s = state();
std::lock_guard lock(s.mutex);
s.note = std::move(note);
if (s.watching) return;
try {
std::thread(Watch).detach();
s.watching = true;
} catch (...) {
// No watcher: a stop then takes as long as it takes, as before.
}
}
void Begin() noexcept {
State& s = state();
std::lock_guard lock(s.mutex);
if (s.armed) return;
s.armed = true;
s.deadline = std::chrono::steady_clock::now() + s.limit;
s.wake.notify_all();
}
void End() noexcept {
State& s = state();
std::lock_guard lock(s.mutex);
s.armed = false;
s.wake.notify_all();
}
void RestartNow() noexcept {
#ifdef PS5_NATIVE
// No stdio here: a thread that is stuck in the stop may hold one of its locks.
sceKernelDebugOutText(0, "[ProsperoEden] shutdown: The game did not stop in time; starting ProsperoEden again\n");
const std::string& note = state().note; // set once, before any game
if (!note.empty()) {
const int fd = open(note.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (fd >= 0) {
(void)!write(fd, "1\n", 2);
close(fd);
}
}
eden_restart_app();
#endif
}
void Configure(void (*at_limit)() noexcept, std::chrono::milliseconds limit) noexcept {
State& s = state();
std::lock_guard lock(s.mutex);
s.at_limit = at_limit;
s.limit = limit;
}
} // namespace Eden::StopLimit
+29
View File
@@ -0,0 +1,29 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <chrono>
#include <string>
// A limit on how long a game may take to stop.
//
// Stopping a game normally takes about a second. Some games take minutes: the emulator waits for
// guest threads or services that never finish. A player who asked to leave should not sit through
// that, so a stop that is still going at the limit ends the other way: the app asks the PS5 to
// start it again (src/lifecycle.c, eden_restart_app), which ends this process and opens a fresh
// one at the launcher. The game loses nothing by it: its save is on disk once the game has
// committed it, and it commits while it runs, not while it is being stopped.
namespace Eden::StopLimit {
inline constexpr std::chrono::seconds kLimit{10};
// Once, at start, before any game: starts the watcher (on the caller's scheduling, not a pinned
// game thread's). `note` is a file the watcher leaves when it acts; the next process finds it.
void Start(std::string note) noexcept;
// A game's stop began: the shortcut was pressed, the game ended itself, or its session failed.
// A second call during the same stop keeps the first deadline.
void Begin() noexcept;
// The game is gone and the app carries on: called before every launcher and every next game.
void End() noexcept;
// What happens at the limit, at once (development check of the restart itself).
void RestartNow() noexcept;
// Checks only: another action at the limit, and another limit.
void Configure(void (*at_limit)() noexcept, std::chrono::milliseconds limit) noexcept;
} // namespace Eden::StopLimit
+3 -2
View File
@@ -20,8 +20,9 @@ __attribute__((noreturn)) void catchReturnFromMain(int status) {
for (;;) sceKernelUsleep(100000);
}
// Start this app again in a fresh process: the system ends this one and runs the app's own
// executable. After a crash report was written (headless/crash_report.cpp). No stdio: the caller
// is a helper beside a crashed thread. Returns only when the system refuses.
// executable. After a crash report was written (headless/crash_report.cpp), and for a game that
// will not stop (headless/stop_limit.h). No stdio: the caller is a helper beside a crashed or a
// stuck thread. Returns only when the system refuses.
int eden_restart_app(void) {
char marker[96];
sceKernelDebugOutText(0, "EDEN_PPSA99121_RESTART\n");
+1
View File
@@ -64,6 +64,7 @@ cmake --build "$scratch/build" --target eden-headless eden-romfs-check eden-devi
python3 -B "$root/tools/check-sparse-header.py" "$scratch/build"
python3 -B "$root/tools/check-heap-growth.py"
python3 -B "$root/tools/check-crash-report.py"
python3 -B "$root/tools/check-stop-limit.py"
"$scratch/build/bin/eden-scalar-check"
"$scratch/build/bin/eden-memory-check"
python3 -B "$root/tools/check-decoder-startup.py"
+85
View File
@@ -0,0 +1,85 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-3.0-or-later
"""The limit on how long a game may take to stop (headless/stop_limit.cpp).
On the PC, with a short limit and an action that only counts: a stop that finishes in time does
nothing, one that does not runs the action once, a second Begin keeps the first deadline, and a
new stop gets a new one. Under the thread sanitizer. Then the source: the app arms the limit when
the player asks to leave and when a game ends by itself, disarms it before every launcher, and
the restart itself uses no stdio (the watcher may run beside a thread that is stuck holding a
stdio lock).
"""
import pathlib
import re
import subprocess
import sys
import tempfile
root = pathlib.Path(__file__).resolve().parents[1]
main = (root / 'headless/main.cpp').read_text()
assert main.count('Eden::StopLimit::Begin();') == 2, 'the shortcut and the stop path arm the limit'
shortcut = main.index('if (pad->TakeReturnToMenu()) {')
assert main.index('Eden::StopLimit::Begin();', shortcut) < main.index('completion->return_to_menu = true;', shortcut)
stop = main.index('Eden::StopLimit::Begin();', shortcut + 200)
assert stop < main.index('jit_list.Finish();') < main.index('system.ShutdownMainProcess();', stop)
assert 'for (;;) {\n Eden::StopLimit::End();' in main, 'every way back to the launcher disarms the limit'
assert 'Eden::StopLimit::Start(note);' in main
lifecycle = (root / 'src/lifecycle.c').read_text()
restart = lifecycle.split('int eden_restart_app(void) {', 1)[1].split('\n}\n', 1)[0]
assert 'sceSystemServiceLoadExec("/app0/eboot.bin", NULL)' in restart
assert not re.search(r'\b(fflush|fprintf|printf|fputs|puts)\(', restart), 'no stdio in the restart'
watcher = (root / 'headless/stop_limit.cpp').read_text().split('void RestartNow() noexcept {', 1)[1].split('\n}\n', 1)[0]
assert not re.search(r'\b(fflush|fprintf|printf|fputs|puts)\(', watcher), 'no stdio in the watcher action'
TEST = r'''
#include "stop_limit.h"
#include <atomic>
#include <cstdio>
#include <cstdlib>
#include <thread>
using namespace std::chrono_literals;
static std::atomic<int> fired{0};
static void Fire() noexcept { ++fired; }
#define REQUIRE(what) do { if (!(what)) { std::fprintf(stderr, "stop limit check failed at line %d: %s\n", __LINE__, #what); std::abort(); } } while (0)
int main() {
using namespace Eden::StopLimit;
Configure(&Fire, 300ms);
// No watcher yet (a build that never starts one): nothing happens.
Begin(); std::this_thread::sleep_for(500ms); REQUIRE(fired == 0); End();
Start("");
// A stop that finishes in time.
Begin(); std::this_thread::sleep_for(100ms); End(); std::this_thread::sleep_for(450ms); REQUIRE(fired == 0);
// One that does not: the action runs once, however long the stop goes on.
Begin(); std::this_thread::sleep_for(500ms); REQUIRE(fired == 1);
std::this_thread::sleep_for(500ms); REQUIRE(fired == 1);
End();
// A second Begin during the same stop keeps the first deadline.
Begin(); std::this_thread::sleep_for(200ms); Begin(); std::this_thread::sleep_for(200ms); REQUIRE(fired == 2);
End();
// A new stop has a new deadline.
Begin(); std::this_thread::sleep_for(200ms); End();
Begin(); std::this_thread::sleep_for(200ms); REQUIRE(fired == 2);
std::this_thread::sleep_for(250ms); REQUIRE(fired == 3);
End();
// Many stops in a row, each in time.
for (int i = 0; i < 500; ++i) { Begin(); End(); }
std::this_thread::sleep_for(450ms); REQUIRE(fired == 3);
// Starting again adds no second watcher: one stop, one action.
Start("note");
Begin(); std::this_thread::sleep_for(500ms); REQUIRE(fired == 4);
End();
std::puts("ok");
}
'''
with tempfile.TemporaryDirectory(prefix='eden-stop-') as work:
work = pathlib.Path(work)
(work / 'check.cpp').write_text(TEST)
binary = work / 'check'
subprocess.run(['clang++-18', '-std=c++20', '-O1', '-g', '-pthread', '-fsanitize=thread', '-I', str(root / 'headless'),
str(work / 'check.cpp'), str(root / 'headless/stop_limit.cpp'), '-o', str(binary)], check=True)
result = subprocess.run([str(binary)], capture_output=True, text=True, timeout=120)
if result.returncode != 0 or 'ok' not in result.stdout:
sys.stderr.write(result.stderr[-3000:])
sys.exit(f'stop limit check failed: exit {result.returncode}')
print('Stop limit PASS: in time, past the limit once, first deadline kept, a new stop a new deadline; armed at the '
'shortcut and at the stop, disarmed before every launcher; no stdio in the restart')