mirror of
https://github.com/Swordpdf/ProsperoEden.git
synced 2026-10-19 04:09:47 -05:00
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:
1 parent
55dbb7df4a
commit
ec7c81960c
8 files changed
+259
-4
No files matched your search
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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");
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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')
|
||||
Reference in new issue
Block a user