mirror of
https://github.com/Swordpdf/ProsperoEden.git
synced 2026-10-21 21:22:16 -05:00
Adds PS5_MihawkGen (mihawk-99) as a second frame generation method alongside LSFG. MihawkGen uses GLSL compute shaders and a 1.7MB neural network weights file — no user-supplied DLL needed. - Build: static library with volk for Vulkan dispatch, symbol rename (-DvkGetInstanceProcAddr=mihawkgen_volk_gipa) to avoid collision with RADV's linker-aliased function; GIPA passed through Config struct - Settings: frame_gen_method selector (0=Lossless Scaling, 1=MihawkGen) in both global Settings and per-game overrides; method selector skips unavailable methods (missing DLL / missing weights) - Services: mihawkgen_state() availability check (user dir then app dir) - Renderer: vulkan_mihawkgen.inc inlined into present path by prepare-vulkan-port.py, gated by MihawkGenBridge::active; LSFG gated off when MihawkGen is active - Packaging: network.fgw weights copied from sibling repo into package Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2151 lines
129 KiB
C++
2151 lines
129 KiB
C++
#ifdef EDEN_DEV_ROM_ID
|
|
#include "development_input.h"
|
|
#endif
|
|
#include <utility>
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
#include <cstdio>
|
|
#include <algorithm>
|
|
#include <exception>
|
|
#include "assets_dir.h"
|
|
#include "forwarded_launch.h"
|
|
#include "gpu_failure.h"
|
|
#include "guest_fault.h"
|
|
#include "jit_list.h"
|
|
#include "boot_trace.h"
|
|
#ifdef PS5_NATIVE
|
|
#include "elevation/elevation.hpp"
|
|
#include <cerrno>
|
|
#include <cstring>
|
|
#include <sys/stat.h>
|
|
#endif
|
|
#ifdef EDEN_DEV_VULKAN
|
|
#include "sdk_audit.h"
|
|
#endif
|
|
#include <filesystem>
|
|
#include <unistd.h>
|
|
#include <condition_variable>
|
|
#include <chrono>
|
|
#include <cstdlib>
|
|
#include <mutex>
|
|
#include <thread>
|
|
#include <bit>
|
|
#include <new>
|
|
#include <string_view>
|
|
#include <stdexcept>
|
|
#include <fstream>
|
|
#include <nlohmann/json.hpp>
|
|
#include "devices.h"
|
|
#include "diagnostics.h"
|
|
#include "hud.h"
|
|
#include "loading_art.h"
|
|
#include "display_refresh.h"
|
|
#include "log_flusher.h"
|
|
#include "multiplayer_logging.h"
|
|
#include "mods.h"
|
|
#include "controller_applet.h"
|
|
#include "error_applet.h"
|
|
#include "keyboard_applet.h"
|
|
#include "load_address.h"
|
|
#include "preferences.h"
|
|
#include "metadata_bridge.h"
|
|
#ifdef EDEN_PS5_OPENGL
|
|
#include "graphics.h"
|
|
#include "video_core/renderer_base.h"
|
|
#include "video_core/rasterizer_interface.h"
|
|
#endif
|
|
#ifdef PS5_NATIVE
|
|
#include "native_directory.h"
|
|
#include "cache_budget.h"
|
|
#include "performance.h"
|
|
#include "stall_watchdog.h"
|
|
#include "dev_vulkan.h"
|
|
#include "../src/fastmem.h"
|
|
#include "crash_report.h"
|
|
#include "prosperoeden/frontend.h"
|
|
#include "prosperoeden/version.h"
|
|
extern "C" void ps5_opengl_heap_snapshot(const char*, unsigned);
|
|
extern "C" std::int64_t sceKernelGetDirectMemorySize();
|
|
extern "C" std::int32_t sceKernelAvailableFlexibleMemorySize(std::size_t*);
|
|
#else
|
|
#include <malloc.h>
|
|
#include "mock_devices.h"
|
|
#endif
|
|
#include "common/fs/path_util.h"
|
|
#include "common/logging.h"
|
|
#include "common/scope_exit.h"
|
|
#include "common/settings.h"
|
|
#include "core/core.h"
|
|
#include "core/cpu_manager.h"
|
|
#include "core/frontend/emu_window.h"
|
|
#include "core/frontend/graphics_context.h"
|
|
#include "core/hle/service/am/applet_manager.h"
|
|
#include "core/hle/service/am/frontend/applets.h"
|
|
#include "core/file_sys/registered_cache.h"
|
|
#include "core/hle/service/filesystem/filesystem.h"
|
|
#include "core/hle/service/set/settings_types.h"
|
|
#include "hid_core/frontend/emulated_controller.h"
|
|
#include "hid_core/hid_core.h"
|
|
// The game's Joy-Con hold type (UpdateGrips) is asked of the HID service; its headers bring kernel
|
|
// ones that do not pass this file's warnings.
|
|
#pragma GCC diagnostic push
|
|
#pragma GCC diagnostic ignored "-Wunused-private-field"
|
|
#include "core/hle/service/hid/hid_server.h"
|
|
#include "core/hle/service/sm/sm.h"
|
|
#include "hid_core/resource_manager.h"
|
|
#include "hid_core/resources/applet_resource.h"
|
|
#include "hid_core/resources/npad/npad.h"
|
|
#pragma GCC diagnostic pop
|
|
#pragma clang diagnostic push
|
|
#pragma clang diagnostic ignored "-Wunused-private-field"
|
|
#include "common/page_table.h"
|
|
#include "core/hle/kernel/k_process.h" // every build: the game's code address (jit_list.h)
|
|
#ifdef PS5_NATIVE
|
|
#include "core/hle/kernel/k_thread.h"
|
|
#endif
|
|
#pragma clang diagnostic pop
|
|
#ifdef EDEN_DEV_PROFILE
|
|
#include "crash_trigger.h"
|
|
#include "watch.h"
|
|
#include "core/arm/debug.h"
|
|
#include "core/memory.h"
|
|
extern "C" bool eden_jit_shared; // headless/dynarmic/jit_group_support.inc
|
|
extern "C" bool eden_page_reuse; // headless/dynarmic/page_reuse_pass.inc
|
|
extern "C" unsigned long long eden_page_reuse_groups, eden_page_reuse_accesses;
|
|
extern "C" bool eden_free_moves; // headless/dynarmic/reg_alloc.cmake
|
|
extern "C" bool eden_alloc_r13; // headless/dynarmic/reg_alloc.cmake
|
|
extern "C" bool eden_plain_ordered_reads; // headless/CMakeLists.txt
|
|
extern "C" unsigned long long eden_free_moves_done, eden_free_moves_spilled;
|
|
extern "C" bool eden_fcmp_adc, eden_light_dmb, eden_checkbit_flags; // headless/dynarmic/fast_paths.cmake
|
|
extern "C" unsigned long long eden_dmb_dropped, eden_checkbit_fused;
|
|
#endif
|
|
#include "video_core/gpu.h"
|
|
#ifdef EDEN_PS5_FRAMEGEN
|
|
#include "video_core/frame_gen/lossless_dll.h"
|
|
#include "video_core/post_processing/fx_effect.h"
|
|
#include "video_core/post_processing/fx_effect.h"
|
|
#include "video_core/post_processing/fx_preset.h"
|
|
#include "reshade_folder.h"
|
|
#include "reshade_folder.h"
|
|
#endif
|
|
#ifdef EDEN_PS5_MIHAWKGEN
|
|
#include "mihawkgen_bridge.h"
|
|
#endif
|
|
#include "profiles.h"
|
|
#include "stop_limit.h"
|
|
namespace Common {
|
|
bool SparseTablesAvailable() noexcept; // src/memory_pages.cpp
|
|
void SetGuestMemoryService(bool on, bool scan, bool dedup) noexcept; // src/guest_memory_service.inc
|
|
void SetGuestMemoryFine(bool on) noexcept;
|
|
}
|
|
#if defined(EDEN_DEV_PROFILE) || defined(EDEN_DEV_ROM_ID)
|
|
static bool dev_stop_hang = false; // dev-settings stop_hang=on
|
|
static bool dev_load_hang = false; // dev-settings load_hang=on
|
|
#endif
|
|
|
|
class HeadlessWindow final : public Core::Frontend::EmuWindow {
|
|
public:
|
|
HeadlessWindow() { UpdateCurrentFramebufferLayout(1280, 720); }
|
|
~HeadlessWindow() override = default;
|
|
bool IsShown() const override { return false; }
|
|
std::unique_ptr<Core::Frontend::GraphicsContext> CreateSharedContext() const override {
|
|
return std::make_unique<Core::Frontend::GraphicsContext>();
|
|
}
|
|
};
|
|
|
|
#ifdef PS5_NATIVE
|
|
// First start with filesystem access: copy what the sandbox kept (settings, covers, Eden's saves
|
|
// and caches) into /data/prosperoeden. Only folders that do not exist yet are filled, and the
|
|
// sandbox copy stays, so an older ProsperoEden still finds its data.
|
|
// dev-settings jit_table_check=on: the JIT's direct page table (headless/jit-page-table.cmake) must
|
|
// hold, for every page, the pointer of Eden's entry, or 0 where the entry is marked or empty. A
|
|
// difference that is still there when read again is a write path that missed the direct table.
|
|
[[maybe_unused]] static bool jit_table_check = false;
|
|
[[maybe_unused]] static void CheckDirectTable(Common::PageTable& table) {
|
|
const std::size_t mirror = table.entries.mirror_index;
|
|
if (mirror == 0) return;
|
|
using Entry = Common::PageTable::PageEntryData;
|
|
const auto* raw = reinterpret_cast<const volatile std::uint64_t*>(table.entries.data());
|
|
const auto expected = [](std::uint64_t entry) {
|
|
return std::uint64_t(Entry::ExtractPointer(std::bit_cast<Entry::Data>(entry)));
|
|
};
|
|
const std::size_t count = std::size_t{1} << (table.GetAddressSpaceBits() - 12);
|
|
std::size_t pages = 0, mismatches = 0;
|
|
for (std::size_t page = 0; page < count; ++page) {
|
|
if (raw[page] == 0 && raw[mirror + page] == 0) continue;
|
|
++pages;
|
|
if (expected(raw[page]) == raw[mirror + page]) continue;
|
|
std::this_thread::yield(); // an update may be between its two stores
|
|
const std::uint64_t entry = raw[page], direct = raw[mirror + page];
|
|
if (expected(entry) == direct) continue;
|
|
if (++mismatches <= 8)
|
|
std::printf("EDEN_JIT_TABLE mismatch page=%zx entry=%llx direct=%llx\n", page,
|
|
static_cast<unsigned long long>(entry), static_cast<unsigned long long>(direct));
|
|
}
|
|
std::printf("EDEN_JIT_TABLE check pages=%zu mismatches=%zu\n", pages, mismatches);
|
|
}
|
|
|
|
static void MigrateSandboxData() {
|
|
const std::filesystem::path sandbox{"/mnt/sandbox/PPSA99008_000/download0"};
|
|
const std::pair<std::filesystem::path, std::string> moves[] = {
|
|
{sandbox / "eden-headless-g7/user", Eden::UserDir()},
|
|
{sandbox / "prosperoeden/covers", Eden::CoversDir()},
|
|
};
|
|
const auto options = std::filesystem::copy_options::recursive | std::filesystem::copy_options::skip_existing;
|
|
for (const auto& [from, to] : moves) {
|
|
std::error_code error;
|
|
if (Eden::DirectoryExists(to) || !Eden::DirectoryExists(from.string())) continue;
|
|
std::filesystem::copy(from, to, options, error);
|
|
const std::string detail = from.string() + " -> " + to + (error ? " failed: " + error.message() : "");
|
|
Eden::Report("data migration", detail.c_str());
|
|
}
|
|
// Settings files (not the covers folder) go to config/.
|
|
if (!Eden::DirectoryExists(Eden::ConfigDir()) && Eden::DirectoryExists((sandbox / "prosperoeden").string())) {
|
|
std::error_code error;
|
|
std::filesystem::create_directories(Eden::ConfigDir(), error);
|
|
for (std::filesystem::directory_iterator it{sandbox / "prosperoeden", error}, end; !error && it != end; it.increment(error))
|
|
if (it->is_regular_file())
|
|
std::filesystem::copy_file(it->path(), std::filesystem::path{Eden::ConfigDir()} / it->path().filename(),
|
|
std::filesystem::copy_options::skip_existing, error);
|
|
Eden::Report("data migration", error ? ("settings failed: " + error.message()).c_str() : "settings copied");
|
|
}
|
|
}
|
|
#endif
|
|
|
|
#ifdef PS5_NATIVE
|
|
// Game files an early version kept inside the app's folder (assets/ with keys, firmware and
|
|
// roms) move to /data/prosperoeden, where replacing the app cannot take them away: an update
|
|
// that replaces the whole folder left the app without its keys and games. Both are on /data, so
|
|
// it is a rename of each folder. Nothing moves unless all of it can: a folder chosen in Settings,
|
|
// or anything already in /data/prosperoeden, leaves everything where it is.
|
|
static void MoveGameFilesOutOfTheApp() {
|
|
const std::string from = std::string{Eden::kInstallDir} + "/assets", to = Eden::kDefaultAssetsDir;
|
|
const std::string saved = Eden::LoadSavedAssetsDir();
|
|
if ((!saved.empty() && saved != from) || !Eden::FileExists(from + "/keys/prod.keys") ||
|
|
Eden::FileExists(to + "/keys/prod.keys"))
|
|
return;
|
|
static constexpr const char* kFolders[] = {"keys", "firmware", "roms", "updates", "mods", "ryujinx", "save-import"};
|
|
(void)mkdir(to.c_str(), 0777);
|
|
for (const char* folder : kFolders) {
|
|
const std::string target = to + "/" + folder;
|
|
if (Eden::DirectoryExists(from + "/" + folder) && rmdir(target.c_str()) != 0 && errno != ENOENT) {
|
|
Eden::Report("game files", ("Left in the app's folder: " + target + " already holds files").c_str());
|
|
return;
|
|
}
|
|
}
|
|
int moved = 0;
|
|
for (const char* folder : kFolders) {
|
|
const std::string source = from + "/" + folder, target = to + "/" + folder;
|
|
if (!Eden::DirectoryExists(source)) continue;
|
|
if (std::rename(source.c_str(), target.c_str()) != 0) {
|
|
Eden::Report("game files", ("Could not move " + source + " to " + target + ": " + std::strerror(errno)).c_str());
|
|
continue;
|
|
}
|
|
++moved;
|
|
}
|
|
if (!saved.empty()) (void)Eden::SaveAssetsDir(to);
|
|
Eden::Report("game files", (std::to_string(moved) + " folders moved from " + from + " to " + to +
|
|
", where an update of the app leaves them alone").c_str());
|
|
}
|
|
#endif
|
|
|
|
// A player who has a single Joy-Con plays it on a whole DualSense (Pad::Grip): the pad presses
|
|
// that Joy-Con's buttons and moves its stick from both halves. When the game has single Joy-Cons
|
|
// held sideways (their hold type, set by the game), the DualSense is still held as usual, and
|
|
// the pad also turns the stick, the button places and the motion by a quarter. Asked a few
|
|
// times a second.
|
|
static void UpdateGrips(Core::System& system, Eden::Pad& pad) {
|
|
using Grip = Eden::Pad::Grip;
|
|
static unsigned calls = 0;
|
|
static std::array<Grip, Eden::Pad::kMaxPlayers> last{};
|
|
if (calls++ % 15 != 0) return;
|
|
bool sideways = false;
|
|
if (const auto hid = system.ServiceManager().GetService<Service::HID::IHidServer>("hid")) {
|
|
const auto resources = hid->GetResourceManager();
|
|
const auto npad = resources ? resources->GetNpad() : nullptr;
|
|
const auto applets = resources ? resources->GetAppletResource() : nullptr;
|
|
auto hold = Service::HID::NpadJoyHoldType::Vertical;
|
|
sideways = npad && applets && npad->GetNpadJoyHoldType(applets->GetActiveAruid(), hold).IsSuccess() &&
|
|
hold == Service::HID::NpadJoyHoldType::Horizontal;
|
|
}
|
|
for (std::size_t index = 0; index < Eden::Pad::kMaxPlayers; ++index) {
|
|
const auto style = system.HIDCore().GetEmulatedControllerByIndex(index)->GetNpadStyleIndex();
|
|
const Grip grip =
|
|
style == Core::HID::NpadStyleIndex::JoyconLeft ? (sideways ? Grip::sideways_left : Grip::upright_left) :
|
|
style == Core::HID::NpadStyleIndex::JoyconRight ? (sideways ? Grip::sideways_right : Grip::upright_right) :
|
|
Grip::usual;
|
|
pad.SetGrip(index, grip);
|
|
if (grip == last[index]) continue;
|
|
last[index] = grip;
|
|
Eden::Report("controllers", ("Player " + std::to_string(index + 1) +
|
|
(grip == Grip::usual ? ": controller as usual" :
|
|
grip == Grip::sideways_left ? ": a left Joy-Con held sideways, turned for a DualSense held as usual" :
|
|
grip == Grip::sideways_right ? ": a right Joy-Con held sideways, turned for a DualSense held as usual" :
|
|
grip == Grip::upright_left ? ": a left Joy-Con held upright; the shapes are its four buttons, the arrows and either stick its stick" :
|
|
": a right Joy-Con held upright; the shapes are its four buttons, the arrows and either stick its stick")).c_str());
|
|
}
|
|
}
|
|
|
|
int main(int argc, char** argv) {
|
|
try {
|
|
std::FILE* report = stdout;
|
|
SCOPE_EXIT { if (report != stdout) std::fclose(report); };
|
|
std::setvbuf(report, nullptr, _IONBF, 0);
|
|
#ifdef PS5_NATIVE
|
|
Eden::BootTrace::Begin(Eden::kAppVersion, __DATE__ " " __TIME__);
|
|
// The PS5 keyboard's module, for a game's text entry: asked for before anything else
|
|
// changes what the app may load (system_keyboard.h).
|
|
Eden::BootTrace::Line("PS5 keyboard module: %#x", static_cast<unsigned>(Eden::PrepareSystemKeyboard()));
|
|
Eden::BootTrace::Line("requesting filesystem access");
|
|
// Filesystem access beyond the sandbox, first: every path below depends on it
|
|
// (assets_dir.h). A resident upstream Lapy service gets the first opportunity; otherwise
|
|
// the packaged exact-title upstream helper is sent to the local elfldr. ProsperoEden
|
|
// contains no locally implemented kernel mutation code. If neither path works, the app
|
|
// keeps its sandbox paths.
|
|
#if defined(EDEN_DEV_PROFILE)
|
|
// Development: a start without access, to see what the launcher says then.
|
|
if (access("/app0/no-filesystem-access.txt", F_OK) == 0) Eden::FilesystemAccessStatus() = 99;
|
|
else
|
|
#endif
|
|
Eden::FilesystemAccessStatus() = static_cast<int>(elevation::request(elevation::Capability::filesystem));
|
|
Eden::BootTrace::Line("filesystem access returned status=%d, uid %d/%d gid %d/%d", Eden::FilesystemAccessStatus(),
|
|
static_cast<int>(getuid()), static_cast<int>(geteuid()), static_cast<int>(getgid()),
|
|
static_cast<int>(getegid()));
|
|
#if defined(EDEN_DEV_PROFILE)
|
|
// Upstream Lapy deliberately accepts only a single-threaded target. Keep every
|
|
// development worker behind the completed elevation exchange.
|
|
Eden::Stall::Start();
|
|
#endif
|
|
// Elevation leaves the effective group (1) apart from the real one (0), and Mesa turns
|
|
// RADV's disk cache off for a process whose real and effective ids differ, so no
|
|
// compiled shader was ever kept between sessions. Match them; the effective user is
|
|
// root, which may set its group.
|
|
if (getegid() != getgid() && setegid(getgid()) != 0)
|
|
Eden::Report("filesystem access", "Could not match the effective group; RADV's disk cache stays off");
|
|
if (Eden::FilesystemAccess()) {
|
|
MigrateSandboxData();
|
|
MoveGameFilesOutOfTheApp();
|
|
}
|
|
for (const auto& folder : {Eden::UserDir(), Eden::ConfigDir(), Eden::CoversDir(), Eden::LogsDir()}) {
|
|
std::error_code folder_error;
|
|
std::filesystem::create_directories(folder, folder_error);
|
|
}
|
|
// Keep the previous session's logs: a freeze is diagnosed after the app is reopened.
|
|
for (const char* name : {"stderr.log", "heap.log"}) {
|
|
const std::string log = Eden::LogFile(name);
|
|
std::rename(log.c_str(), (log.substr(0, log.size() - 4) + ".prev.log").c_str());
|
|
}
|
|
// A crash report the previous run left: that run's logs move beside it, and the launcher
|
|
// says where it is (crash_report.h).
|
|
const Eden::Crash::Last last_crash = Eden::Crash::TakeLast(Eden::LogsDir(), Eden::UserDir() + "/log/eden_log.txt");
|
|
#ifdef EDEN_DEV_PROFILE
|
|
// UI inspection captures are disposable; keep saves, settings and shader caches.
|
|
for (const char* name : {"ui-preview.bmp", "ui-main.bmp", "ui-nav.bmp",
|
|
"ui-library.bmp", "ui-about.bmp"})
|
|
std::filesystem::remove(std::filesystem::path{Eden::ConfigDir()} / name);
|
|
for (const char* name : {"eden_log.txt", "eden_log.txt.old.txt"})
|
|
std::filesystem::remove(std::filesystem::path{Eden::UserDir()} / "log" / name);
|
|
#endif
|
|
if (!std::freopen(Eden::LogFile("stderr.log").c_str(), "w", stderr) ||
|
|
!std::freopen(Eden::LogFile("heap.log").c_str(), "w", stdout)) return 2;
|
|
// Console storage writes take ~25 ms each; a background thread writes both streams out,
|
|
// unless Settings > Diagnostics asks for every line at once (to find a crash).
|
|
static Eden::LogFlusher log_flusher;
|
|
if (!log_flusher.Start(Eden::LoadPreferences().immediate_logs)) return 2;
|
|
Eden::Crash::Install(Eden::LogsDir(), Eden::kAppVersion, last_crash.restarted);
|
|
Eden::BootTrace::Ready(Eden::LogsDir(), Eden::FilesystemAccess());
|
|
Eden::BootTrace::Line("logs and crash handler ready (%s)", Eden::LogsDir().c_str());
|
|
for (const char* folder : {"/app0", Eden::kMountedAppDir, Eden::kInstallDir, "/mnt/sandbox/PPSA99008_000/app0"})
|
|
Eden::BootTrace::Line("app folder candidate %s: eboot %s, font %s", folder,
|
|
Eden::FileExists(std::string{folder} + "/eboot.bin") ? "yes" : "no",
|
|
Eden::FileExists(std::string{folder} + "/ui/fonts/montserrat-medium.pefont") ? "yes" : "no");
|
|
Eden::BootTrace::Line("app folder: %s", Eden::AppDir().c_str());
|
|
std::set_new_handler([] {
|
|
ps5_opengl_heap_snapshot("allocation_failure", 0);
|
|
std::fflush(stdout);
|
|
Eden::Report("allocation failure", "operator new: heap exhausted");
|
|
throw std::bad_alloc{};
|
|
});
|
|
// Name the exception in klog, then the crash report (which starts the app again).
|
|
std::set_terminate([] {
|
|
const char* detail = "no active exception";
|
|
if (const auto current = std::current_exception()) {
|
|
try {
|
|
std::rethrow_exception(current);
|
|
} catch (const std::exception& error) {
|
|
detail = error.what();
|
|
} catch (...) {
|
|
detail = "non-standard exception";
|
|
}
|
|
}
|
|
Eden::Report("terminate", detail);
|
|
Eden::Crash::Fail(detail);
|
|
});
|
|
{
|
|
// The ids too: Mesa's disk cache turned itself off when the effective and real ids differed
|
|
// (no longer: tools/patch-radv-disk-cache.py).
|
|
const std::string access = "status=" + std::to_string(Eden::FilesystemAccessStatus()) +
|
|
" elevation=" + elevation::path() +
|
|
" app=" + Eden::AppDir() + " data=" + Eden::UserDir() + " game_files=" + Eden::AssetsDir() +
|
|
" uid=" + std::to_string(getuid()) + "/" + std::to_string(geteuid()) +
|
|
" gid=" + std::to_string(getgid()) + "/" + std::to_string(getegid());
|
|
Eden::Report("filesystem access", access.c_str());
|
|
if (Eden::FilesystemAccess() && Eden::AssetsDir() == Eden::kDefaultAssetsDir)
|
|
for (const char* folder : {"/keys", "/firmware", "/roms", "/updates", "/mods", "/ryujinx"})
|
|
(void)mkdir((std::string{Eden::kDefaultAssetsDir} + folder).c_str(), 0777);
|
|
// RADV's shader cache is kept with the app's data (its own default is /app0), so it also
|
|
// works when the app is installed as a read-only package image. The cache an earlier
|
|
// version kept in the app folder moves over.
|
|
if (Eden::FilesystemAccess()) {
|
|
const std::string cache = std::string{Eden::kDataDir} + "/cache/radv";
|
|
const std::string before = Eden::AppFile("radv-shader-cache");
|
|
(void)mkdir((std::string{Eden::kDataDir} + "/cache").c_str(), 0777);
|
|
if (!Eden::DirectoryExists(cache) && Eden::DirectoryExists(before) &&
|
|
std::rename(before.c_str(), cache.c_str()) != 0)
|
|
Eden::Report("cache", "The driver's shader cache could not move to the data folder; it starts empty");
|
|
(void)mkdir(cache.c_str(), 0777);
|
|
setenv("MESA_SHADER_CACHE_DIR", cache.c_str(), 1);
|
|
// Mesa caps its cache at 1 GiB, shared by every game, and past that each new entry
|
|
// drops an older one: with a large game (13,000 pipelines) among them, every start
|
|
// could drop part of what it compiled the time before (#170). An environment that
|
|
// sets a size keeps it.
|
|
setenv("MESA_SHADER_CACHE_MAX_SIZE", "4G", 0);
|
|
}
|
|
// Where the driver keeps compiled shaders between starts. Without a folder it can write,
|
|
// every start compiles all of a game's pipelines again (minutes for a large game). The
|
|
// driver no longer turns the cache off for differing ids (tools/patch-radv-disk-cache.py).
|
|
{
|
|
const char* folder = std::getenv("MESA_SHADER_CACHE_DIR");
|
|
std::string line = folder ? folder : "none: no access to the data folder, so shaders are compiled at every start";
|
|
if (folder) {
|
|
const std::string probe = std::string{folder} + "/.write-check";
|
|
FILE* file = std::fopen(probe.c_str(), "wb");
|
|
line += file ? ", writable" : ", NOT writable: shaders are compiled at every start";
|
|
if (file) {
|
|
std::fclose(file);
|
|
(void)std::remove(probe.c_str());
|
|
}
|
|
}
|
|
Eden::Report("driver shader cache", line.c_str());
|
|
}
|
|
}
|
|
// 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 (or finish loading) within ten seconds of being "
|
|
"asked to, 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");
|
|
#ifdef EDEN_DEV_VULKAN
|
|
if (std::filesystem::exists(Eden::AppFile("sdk-audit.txt"))) {
|
|
Eden::AuditSdk();
|
|
std::puts("EDEN_SDK_AUDIT_COMPLETE");
|
|
std::fflush(stdout);
|
|
// Keep the sandbox mounted for the existing bounded FTP collector.
|
|
std::this_thread::sleep_for(std::chrono::seconds(30));
|
|
return 0;
|
|
}
|
|
#endif
|
|
const std::string user_dir = Eden::UserDir();
|
|
#ifdef EDEN_PS5_OPENGL
|
|
const auto native_shader_cache = std::filesystem::path{user_dir} / "cache/native-opengl";
|
|
if (setenv("PS5_GLTHREAD", "1", 1) != 0)
|
|
throw std::runtime_error("Cannot configure GL worker");
|
|
#if defined(EDEN_DEV_PROFILE) && !defined(EDEN_DEV_VULKAN)
|
|
Eden::Performance::BeginPcSampling();
|
|
// A driver rebuild already invalidates these compiler records. Drop its
|
|
// obsolete slots so repeated development builds fit the 256 MiB sandbox.
|
|
std::string cached_runtime;
|
|
std::ifstream(native_shader_cache / "development-runtime") >> cached_runtime;
|
|
if (cached_runtime != EDEN_DEV_GL_VERSION) {
|
|
std::filesystem::create_directories(native_shader_cache);
|
|
std::error_code directory_error;
|
|
size_t removed = 0;
|
|
for (const auto& entry : Eden::ReadNativeDirectory(native_shader_cache, directory_error)) {
|
|
const auto name = entry.path().filename().string();
|
|
const auto dot = name.find(".bin");
|
|
if ((dot == 3 || dot == 4) &&
|
|
name.find_first_not_of("0123456789abcdef", 0) == dot)
|
|
removed += std::filesystem::remove(entry.path());
|
|
}
|
|
if (directory_error) throw std::system_error(directory_error, "Read native compiler cache");
|
|
std::ofstream(native_shader_cache / "development-runtime") << EDEN_DEV_GL_VERSION;
|
|
std::printf("EDEN_DEV_CACHE removed_obsolete_files=%zu\n", size_t(removed));
|
|
}
|
|
for (unsigned i = 0; i < 8; ++i)
|
|
std::filesystem::remove(std::filesystem::path{Eden::LogsDir()} /
|
|
("pass-" + std::to_string(i) + ".ppm"));
|
|
#endif
|
|
std::error_code cache_error;
|
|
std::filesystem::create_directories(native_shader_cache, cache_error);
|
|
if (!cache_error && setenv("PS5_SHADER_CACHE_DIR", native_shader_cache.c_str(), 1) != 0)
|
|
throw std::runtime_error("Cannot configure native shader cache");
|
|
#endif
|
|
std::string launch_error;
|
|
// The previous run crashed: the launcher says where its report is.
|
|
if (!last_crash.report.empty()) launch_error = std::string{Eden::Crash::kNotice} + last_crash.report;
|
|
const Eden::ForwardedArgs forwarded = Eden::ParseForwardedArgs(argc, argv);
|
|
std::string forwarded_game;
|
|
bool forwarded_session = false;
|
|
if (!forwarded.rom.empty() && last_crash.report.empty()) {
|
|
const std::string path = Eden::ResolveForwardedRom(forwarded.rom, Eden::AssetsPath("roms"));
|
|
if (!path.empty() && Eden::FileExists(path)) forwarded_game = path;
|
|
else launch_error = "Forwarded game not found: " + (path.empty() ? forwarded.rom : path);
|
|
Eden::BootTrace::Line("forwarded game: %s", forwarded_game.empty() ? "not found" : "found");
|
|
}
|
|
// A game that faulted early in its boot is restarted (at most four times per launch).
|
|
std::string relaunch_game;
|
|
unsigned guest_fault_retries = 0;
|
|
#ifdef EDEN_DEV_VULKAN
|
|
std::string recovery_mode;
|
|
std::ifstream(Eden::AppFile("backend-recovery.txt")) >> recovery_mode;
|
|
const bool check_backend_recovery = !recovery_mode.empty();
|
|
bool recovery_opengl = false;
|
|
#endif
|
|
#if defined(EDEN_DEV_PROFILE) || defined(EDEN_DEV_ROM_ID)
|
|
bool autoboot_pending = true;
|
|
// dev-settings rom=TITLEID boots another title with the same development package
|
|
// (tests on two consoles); the build's development title is the default.
|
|
#ifdef EDEN_DEV_ROM_ID
|
|
std::string development_id = EDEN_DEV_ROM_ID;
|
|
#else
|
|
std::string development_id = EDEN_DEV_PROFILE_TITLE;
|
|
#endif
|
|
{
|
|
std::ifstream dev_settings(Eden::AppFile("dev-settings.txt"));
|
|
for (std::string entry; dev_settings >> entry;) {
|
|
if (entry.starts_with("rom=") && entry.size() == 20) development_id = entry.substr(4);
|
|
// dev-settings launcher=first opens the launcher instead of the development title
|
|
// (launcher work: its captures and file-driven input need a development build).
|
|
if (entry == "launcher=first") autoboot_pending = false;
|
|
}
|
|
// After a crash the launcher opens with its notice, not the development title again.
|
|
if (!last_crash.report.empty()) autoboot_pending = false;
|
|
}
|
|
#ifdef EDEN_DEV_ROM_ID
|
|
{
|
|
// A scripted input left by an earlier run is not for this process.
|
|
std::ifstream left_behind(Eden::AppFile("compat-input.txt"));
|
|
Eden::DevelopmentInput::IgnoreExisting(left_behind);
|
|
}
|
|
#endif
|
|
#endif
|
|
Common::FS::CreateEdenPaths();
|
|
Eden::Performance::PlatformChecks();
|
|
Common::Log::Initialize();
|
|
Eden::Multiplayer::SetDetailedLogging(Eden::LoadPreferences().detailed_logging);
|
|
Eden::Multiplayer::RoomClient rooms;
|
|
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);
|
|
if (!trim_error) {
|
|
const auto removed = Eden::TrimShaderCache(cache_entries);
|
|
if (removed) Eden::Report("cache", "Removed old shader cache records to reserve writable storage");
|
|
}
|
|
#endif
|
|
std::string selected_game;
|
|
Eden::Crash::SetSession("launcher", false);
|
|
#ifdef PS5_NATIVE
|
|
// A Vulkan session at 120 Hz handed the output back at 60 Hz as it closed; the launcher
|
|
// opens it once the display has had time to follow (display_refresh.h).
|
|
if (Eden::Display::settle.exchange(false)) {
|
|
Eden::Report("display", "Back to 60 Hz after a 120 Hz session");
|
|
std::this_thread::sleep_for(std::chrono::seconds(Eden::Display::kSettleSeconds));
|
|
}
|
|
#endif
|
|
#ifdef EDEN_DEV_VULKAN
|
|
if (check_backend_recovery && !recovery_opengl && !launch_error.empty()) {
|
|
recovery_opengl = true;
|
|
autoboot_pending = true;
|
|
Eden::Report("recovery check", "Starting OpenGL after Vulkan session failure");
|
|
}
|
|
const bool automatic_launch = autoboot_pending;
|
|
#endif
|
|
if (!relaunch_game.empty()) {
|
|
selected_game = std::exchange(relaunch_game, {});
|
|
} else if (!forwarded_game.empty()) {
|
|
guest_fault_retries = 0;
|
|
forwarded_session = true;
|
|
selected_game = std::exchange(forwarded_game, {});
|
|
Eden::BootTrace::Line("starting the forwarded game");
|
|
} else if (forwarded_session && forwarded.exit_after_game && launch_error.empty()) {
|
|
Eden::Report("exit", "Forwarded game ended");
|
|
return 0;
|
|
} else {
|
|
guest_fault_retries = 0;
|
|
forwarded_session = false;
|
|
#if defined(EDEN_DEV_PROFILE) || defined(EDEN_DEV_ROM_ID)
|
|
if (std::exchange(autoboot_pending, false)) {
|
|
// Match the title ID in the file name, else in the ROM's own metadata; the game files
|
|
// folder first, then the pre-1.000.020 assets folder that still holds other test titles.
|
|
const auto development_title = std::strtoull(development_id.c_str(), nullptr, 16);
|
|
for (const std::string& folder : {Eden::AssetsPath("roms"), std::string{"/data/assets/roms"}}) {
|
|
std::error_code rom_error;
|
|
for (const auto& entry : Eden::ReadNativeDirectory(folder, rom_error)) {
|
|
const auto filename = entry.path().filename().string();
|
|
const auto extension = entry.path().extension();
|
|
if (extension != ".nsp" && extension != ".xci") continue;
|
|
const auto path = folder + "/" + filename;
|
|
if (filename.find(development_id) != std::string::npos ||
|
|
eden_game_title_id(path.c_str()) == development_title) {
|
|
selected_game = path;
|
|
break;
|
|
}
|
|
}
|
|
if (!selected_game.empty()) break;
|
|
}
|
|
if (selected_game.empty())
|
|
throw std::runtime_error("Development ROM not found");
|
|
} else {
|
|
Eden::BootTrace::Line("opening the launcher");
|
|
selected_game = SelectProsperoEdenGame(rooms, launch_error);
|
|
}
|
|
#else
|
|
Eden::BootTrace::Line("opening the launcher");
|
|
selected_game = SelectProsperoEdenGame(rooms, launch_error);
|
|
#endif
|
|
Eden::BootTrace::Line("launcher closed: %s", selected_game.empty() ? "no game (quit)" : "a game was chosen");
|
|
}
|
|
if (selected_game.empty()) {
|
|
Eden::Report("exit", "Launcher closed");
|
|
#ifdef EDEN_DEV_ROM_ID
|
|
// Development runs end here (EDEN_DEV_QUIT): flush the logs and let the log pipes copy
|
|
// them out. Returning asks the system to end the title (src/lifecycle.c); the C library's
|
|
// own exit here made every development run end as an app crash.
|
|
std::fflush(nullptr);
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(300));
|
|
#endif
|
|
return 0;
|
|
}
|
|
try {
|
|
launch_error.clear();
|
|
if (!rooms.BeginGame()) throw std::runtime_error("Join a room successfully or leave it before starting a game.");
|
|
SCOPE_EXIT { rooms.EndGame(); };
|
|
#ifdef EDEN_DEV_VULKAN
|
|
Eden::Performance::vulkan_cost_enabled = std::filesystem::exists(Eden::AppFile("cost-run.txt"));
|
|
if (Eden::Performance::vulkan_cost_enabled) Eden::Performance::timers_on = true; // a cost run
|
|
const bool performance_run = std::filesystem::exists(Eden::AppFile("performance-run.txt"));
|
|
#ifdef EDEN_DEV_PROFILE
|
|
Eden::Performance::texture_budget_log = true; // EDEN_VULKAN_TEXTURE_BUDGET every 300 frames
|
|
Eden::Performance::dev_marks = true; // a draw's phases and guest thread waits (EDEN_DEV_ lines)
|
|
Eden::Performance::timers_on = true; // the timers the development reports read
|
|
// Optional 20 Hz GPU-thread PC samples. The handler must exist before the
|
|
// GPU thread registers, which unblocks SIGUSR2 only while sampling is on.
|
|
const bool pc_sample_run = std::filesystem::exists(Eden::AppFile("pc-sample.txt"));
|
|
static bool pc_sampler_installed = false;
|
|
if (pc_sample_run && !pc_sampler_installed) {
|
|
Eden::Performance::BeginPcSampling();
|
|
pc_sampler_installed = true;
|
|
}
|
|
#endif
|
|
setenv("PS5VK_QUIET_LOG", performance_run ? "1" : "0", 1);
|
|
// Bounded development cost breakdowns; never enable per-draw tracing.
|
|
setenv("PS5VK_COST_LOG", std::filesystem::exists(Eden::AppFile("cost-run.txt")) ? "1" : "0", 1);
|
|
if (performance_run) unsetenv("PS5VK_CAPTURE_SCANOUT");
|
|
else setenv("PS5VK_CAPTURE_SCANOUT", "1", 1);
|
|
std::printf("EDEN_VULKAN_MEASUREMENT quiet=%d captures=%d\n",
|
|
performance_run, !performance_run);
|
|
const auto game_video = Eden::LoadGameSettings(eden_game_title_id(selected_game.c_str()));
|
|
const auto backend = automatic_launch ?
|
|
(recovery_opengl ? Eden::GraphicsBackend::OpenGL : Eden::GraphicsBackend::Vulkan) :
|
|
game_video.renderer >= 0 ?
|
|
(game_video.renderer == 0 ? Eden::GraphicsBackend::OpenGL : Eden::GraphicsBackend::Vulkan) :
|
|
Eden::LoadPreferences().backend;
|
|
#else
|
|
// Library > Game settings take precedence over Settings > Video.
|
|
const auto game_video = Eden::LoadGameSettings(eden_game_title_id(selected_game.c_str()));
|
|
const auto backend = game_video.renderer >= 0 ?
|
|
(game_video.renderer == 0 ? Eden::GraphicsBackend::OpenGL : Eden::GraphicsBackend::Vulkan) :
|
|
Eden::LoadPreferences().backend;
|
|
#ifdef EDEN_PS5_VULKAN
|
|
// The user-facing diagnostics option controls Eden logs, not synchronous
|
|
// driver traces for every draw. Keep those in explicit development probes.
|
|
setenv("PS5VK_QUIET_LOG", "1", 1);
|
|
#endif
|
|
#endif
|
|
#ifndef EDEN_PS5_VULKAN
|
|
if (backend == Eden::GraphicsBackend::Vulkan)
|
|
throw std::runtime_error("Vulkan is not available in this build yet. Select OpenGL in Settings > Video to play.");
|
|
#endif
|
|
Eden::Report("launch", Eden::BackendName(backend));
|
|
const bool game = std::filesystem::is_regular_file(selected_game);
|
|
if (!game) throw std::runtime_error("Selected ROM is no longer available");
|
|
const char* guest = selected_game.c_str();
|
|
// The audio and the renderer read this game's own settings (Library > Game settings).
|
|
#ifdef PS5_NATIVE
|
|
const uint64_t game_title = game ? eden_game_title_id(guest) : 0;
|
|
#else
|
|
const uint64_t game_title = 0;
|
|
#endif
|
|
Eden::session_title.store(game_title);
|
|
// The loading screen shows the game's cover: the one the launcher keeps of it, found by
|
|
// the file's name in the game files folder (loading_art.h).
|
|
{
|
|
const std::string roms = Eden::AssetsPath("roms/");
|
|
Eden::Loading::SetArtFromCover(Eden::CoverFile(
|
|
selected_game.rfind(roms, 0) == 0 ? selected_game.substr(roms.size()) :
|
|
std::filesystem::path(selected_game).filename().string()));
|
|
}
|
|
const unsigned cycles = game ? 1 : 3;
|
|
const bool devices = EDEN_DEVICE_FRONTEND;
|
|
if (game) {
|
|
for (const char* capture : {"game-frame.ppm", "scanout.ppm", "game-hud.ppm", "game-hud-steady.ppm",
|
|
"game-hud-late.ppm"})
|
|
std::filesystem::remove(std::filesystem::path{Eden::LogsDir()} / capture);
|
|
// Firmware and keys stay in the user-provided read-only assets tree.
|
|
std::puts("EDEN_GAME_ASSETS_READY");
|
|
}
|
|
constexpr bool cpu_pressure = false;
|
|
constexpr bool shutdown_sweep = false;
|
|
#else
|
|
const char* guest = argc >= 2 ? argv[1] : nullptr;
|
|
const uint64_t game_title = 0; // the host build has no game's own settings
|
|
unsigned cycles = 1;
|
|
bool devices = false;
|
|
bool game = false;
|
|
bool cpu_pressure = false;
|
|
bool shutdown_sweep = false;
|
|
for (int i = 2; i < argc; ++i) {
|
|
if (std::string_view{argv[i]} == "--repeat" && cycles == 1) cycles = 3;
|
|
else if (std::string_view{argv[i]} == "--soak" && cycles == 1) cycles = 20;
|
|
else if (std::string_view{argv[i]} == "--devices" && !devices) devices = true;
|
|
else if (std::string_view{argv[i]} == "--game" && !game) game = true;
|
|
else if (std::string_view{argv[i]} == "--cpu-pressure" && !cpu_pressure) cpu_pressure = true;
|
|
else if (std::string_view{argv[i]} == "--shutdown-sweep" && !shutdown_sweep) shutdown_sweep = true;
|
|
else return 2;
|
|
}
|
|
// Device mock assertions describe the homebrew fixture, not arbitrary games.
|
|
if (game && devices) return 2;
|
|
if (cpu_pressure && (game || devices || cycles == 20)) return 2;
|
|
if (shutdown_sweep) {
|
|
if (game || devices || cpu_pressure || cycles != 1) return 2;
|
|
game = true;
|
|
cycles = 6;
|
|
}
|
|
const char* user_dir = "user";
|
|
#endif
|
|
const auto passed = [&](const char* name) {
|
|
#if defined(EDEN_DEV_PROFILE) && defined(PS5_NATIVE)
|
|
Eden::Stall::Progress(name);
|
|
#endif
|
|
std::fprintf(report, "%s\tPASS\n", name);
|
|
#ifdef PS5_NATIVE
|
|
const auto mono_ns = std::chrono::duration_cast<std::chrono::nanoseconds>(
|
|
std::chrono::steady_clock::now().time_since_epoch()).count();
|
|
std::printf("EDEN_STAGE backend=%s phase=%s mono_ns=%lld\n",
|
|
Eden::BackendName(backend), name, static_cast<long long>(mono_ns));
|
|
ps5_opengl_heap_snapshot(name, 0);
|
|
std::fflush(stdout);
|
|
#else
|
|
const auto heap = mallinfo2();
|
|
std::fprintf(stderr, "host_heap phase=%s allocated_bytes=%zu arena_bytes=%zu mapped_bytes=%zu free_bytes=%zu\n",
|
|
name, heap.uordblks + heap.hblkhd, heap.arena, heap.hblkhd, heap.fordblks);
|
|
#endif
|
|
};
|
|
#if defined(EDEN_DEV_PROFILE) && defined(PS5_NATIVE)
|
|
Eden::Stall::Arm();
|
|
#endif
|
|
passed("session_start");
|
|
if (!std::filesystem::is_directory(user_dir)) {
|
|
std::fputs("Run from the isolated directory containing user/.\n", stderr);
|
|
return 2;
|
|
}
|
|
Common::FS::CreateEdenPaths();
|
|
#ifdef PS5_NATIVE
|
|
Common::FS::SetEdenPath(Common::FS::EdenPath::KeysDir, Eden::AssetsPath("keys"));
|
|
// Mods come from the game files folder's mods/ (mods.h). Without that folder Eden's own
|
|
// load folder stays, where nothing is expected.
|
|
if (const std::string mods = Eden::AssetsPath("mods"); Eden::DirectoryExists(mods))
|
|
Common::FS::SetEdenPath(Common::FS::EdenPath::LoadDir, mods);
|
|
#endif
|
|
#ifdef PS5_NATIVE
|
|
if (Common::FS::GetEdenPath(Common::FS::EdenPath::EdenDir) != user_dir) return 2;
|
|
std::puts("[headless-startup] absolute_paths_ready");
|
|
#endif
|
|
#ifndef PS5_NATIVE
|
|
Common::Log::Initialize();
|
|
#endif
|
|
if (game) {
|
|
Common::Log::Filter filter;
|
|
filter.SetClassLevel(Common::Log::Class::Service_FS, Common::Log::Level::Info);
|
|
#if defined(PS5_NATIVE) && !defined(EDEN_DEV_PROFILE) && !defined(EDEN_DEV_ROM_ID)
|
|
if (Eden::LoadPreferences().detailed_logging) filter.ParseFilterString("*:Debug");
|
|
#endif
|
|
Common::Log::SetGlobalFilter(filter);
|
|
}
|
|
Common::Log::Start();
|
|
SCOPE_EXIT { Common::Log::Stop(); };
|
|
#ifdef PS5_NATIVE
|
|
{
|
|
// The pool the guest's memory, the JIT, the heap and the Vulkan heaps share. A PS5 Pro
|
|
// gives 13,504 MiB to a title with attribute3 bit 22 (sce_sys/param.json) and 12,288 MiB
|
|
// without it (base PS5 memory; system software 13.60): the line says which one this run has.
|
|
// The app has a second pool, flexible memory: the program itself, the system's
|
|
// libraries and what they allocate. 448 MiB unless kernel.flexibleMemorySize in
|
|
// sce_sys/param.json says otherwise, and what that pool is smaller by, this one is
|
|
// larger by; so a Pro is told by 13 GiB, which no setting of the other pool reaches
|
|
// on a base console.
|
|
const std::int64_t pool = sceKernelGetDirectMemorySize();
|
|
const bool pro = pool > (std::int64_t{13} << 30);
|
|
std::size_t flexible = 0;
|
|
(void)sceKernelAvailableFlexibleMemorySize(&flexible);
|
|
Eden::Report("memory", ("direct memory pool " + std::to_string(pool >> 20) + " MiB, PS5 Pro memory mode " +
|
|
(pro ? "active" : "inactive") + ", flexible memory free " +
|
|
std::to_string(flexible >> 20) + " MiB").c_str());
|
|
}
|
|
#endif
|
|
#ifdef EDEN_PS5_OPENGL
|
|
Settings::values.renderer_backend = backend == Eden::GraphicsBackend::Vulkan ?
|
|
Settings::RendererBackend::Vulkan : Settings::RendererBackend::OpenGL_GLSL;
|
|
// Keep emulator threads off guest cores 0-2 and their SMT siblings (Vulkan):
|
|
// +17% in the heaviest measured window of a racing game.
|
|
Eden::Performance::SetSecondaryPlacement(backend == Eden::GraphicsBackend::Vulkan);
|
|
// A32 fastmem window (direct JIT accesses to aliased guest memory): opt-in with dev-settings
|
|
// fastmem=on; on the console it is slower than the page-table path.
|
|
Eden::Fastmem::Request(false);
|
|
#ifdef EDEN_DEV_VULKAN
|
|
Settings::values.async_presentation = backend == Eden::GraphicsBackend::Vulkan;
|
|
// Exercise upstream's checked initialization error, never an invalid pointer.
|
|
Settings::values.vulkan_device = recovery_mode == "init-failure" && !recovery_opengl ?
|
|
0xffffffffu : 0u;
|
|
#endif
|
|
Settings::values.renderer_debug = false;
|
|
// RADV presents the console's 12 GiB of direct memory as an integrated GPU, for which
|
|
// Eden budgets 4 GiB: a game using ~4.4 GB of Vulkan memory then ran the texture GC
|
|
// every frame (20-25 FPS). Eden's larger integrated budget (6 GiB) holds it at 30 FPS
|
|
// with ~0.9 GB of direct memory to spare.
|
|
Settings::values.vram_usage_mode.SetValue(Settings::VramUsageMode::Aggressive);
|
|
#else
|
|
Settings::values.renderer_backend = Settings::RendererBackend::Null;
|
|
#endif
|
|
Settings::values.cpu_accuracy = Settings::CpuAccuracy::Auto;
|
|
// The exact GPU and DMA levels too, every session: one app run starts many games, and a
|
|
// level the game before lowered ("performance" in the settings file, below) would stay.
|
|
Settings::values.gpu_accuracy.SetValue(Settings::GpuAccuracy::High);
|
|
Settings::values.current_gpu_accuracy = Settings::GpuAccuracy::High;
|
|
Settings::values.dma_accuracy.SetValue(Settings::DmaAccuracy::Default);
|
|
Settings::values.memory_layout_mode = Settings::MemoryLayout::Memory_4Gb;
|
|
// A development run that names jit_list in dev-settings decides the block list by itself.
|
|
[[maybe_unused]] bool dev_block_list = false; // read where a session applies its settings
|
|
[[maybe_unused]] int dev_vertex_input = -1; // likewise: dev-settings vertex_input_dynamic=off/on
|
|
[[maybe_unused]] bool dev_cram_on = true, dev_cram_scan = true, dev_cram_dedup = false, dev_cram_fine = false;
|
|
#ifdef EDEN_DEV_PROFILE
|
|
// One-run A/B switches written by the development runner; absent = defaults.
|
|
{
|
|
std::ifstream dev_settings(Eden::AppFile("dev-settings.txt"));
|
|
for (std::string entry; dev_settings >> entry;) {
|
|
if (entry == "dma_accuracy=unsafe") {
|
|
Settings::values.dma_accuracy.SetValue(Settings::DmaAccuracy::Unsafe);
|
|
} else if (entry == "cram=off" || entry == "cram_scan=off" || entry == "cram_dedup=on" ||
|
|
entry == "cram_fine=on") {
|
|
// The guest's memory as a service (src/guest_memory_service.inc).
|
|
if (entry == "cram=off") dev_cram_on = false;
|
|
if (entry == "cram_scan=off") dev_cram_scan = false;
|
|
if (entry == "cram_dedup=on") dev_cram_dedup = true;
|
|
if (entry == "cram_fine=on") dev_cram_fine = true;
|
|
} else if (entry == "placement=off") {
|
|
Eden::Performance::SetSecondaryPlacement(false);
|
|
} else if (entry == "fastmem=off" || entry == "fastmem=on") {
|
|
Eden::Fastmem::Request(entry.ends_with("on"));
|
|
} else if (entry == "fastmem_large=off") {
|
|
// Window aliases stay 16 KiB mappings, never 2 MiB ones.
|
|
Eden::Fastmem::RequestLarge(false);
|
|
} else if (entry == "fastmem_sites=off") {
|
|
// Window and reserved register kept, every access on the page-table path.
|
|
Eden::Fastmem::RequestSites(false);
|
|
} else if (entry == "jit_table=off") {
|
|
// The JIT reads Eden's packed page table instead of the direct one
|
|
// (headless/jit-page-table.cmake). Read when the first page table is sized.
|
|
Common::PageTable::direct_tables_requested = false;
|
|
} else if (entry == "jit_table_check=on") {
|
|
// Every 30 s compare the direct table with Eden's entries (EDEN_JIT_TABLE check).
|
|
jit_table_check = true;
|
|
} else if (entry == "cpu_write_detail=on") {
|
|
// EDEN_DEV_CPUWRITE: guest writes to GPU-tracked pages broken down per core.
|
|
Eden::Performance::cpu_write_detail = true;
|
|
} else if (entry.starts_with("cpu_write_ahead=")) {
|
|
// Buffer pages after a tracked write marked CPU-modified with it (performance.h).
|
|
Eden::Performance::cpu_write_ahead = static_cast<unsigned>(std::min(256UL, std::strtoul(entry.c_str() + 16, nullptr, 10)));
|
|
} else if (entry.starts_with("cache_spin=")) {
|
|
// try_lock retries before a guest core sleeps on a GPU cache lock (0 = upstream).
|
|
Eden::Performance::cache_lock_spins = static_cast<unsigned>(std::strtoul(entry.c_str() + 11, nullptr, 10));
|
|
} else if (entry == "cpu_write_defer=off") {
|
|
// Guest writes wait for a GPU cache lock the GPU thread holds, as upstream (performance.h).
|
|
Eden::Performance::cpu_write_defer = false;
|
|
} else if (entry == "cpu_read_skip=on") {
|
|
// Guest reads skip the download check while the GPU thread holds the texture cache lock.
|
|
Eden::Performance::cpu_read_skip = true;
|
|
} else if (entry == "gc_dirty=upstream") {
|
|
// Eden's rule: evict GPU-written textures from the "expected" memory mark on.
|
|
Eden::Performance::gc_keep_dirty = false;
|
|
} else if (entry == "tex_partial=off") {
|
|
// Whole-image uploads after every guest write, as upstream (texture_partial.h).
|
|
Eden::Performance::tex_partial = false;
|
|
} else if (entry == "tex_partial_check=on") {
|
|
Eden::Performance::tex_partial_check = true;
|
|
} else if (entry.starts_with("tex_partial_kb=")) {
|
|
Eden::Performance::tex_partial_min = std::strtoull(entry.c_str() + 15, nullptr, 10) << 10;
|
|
} else if (entry == "gc_scan=upstream") {
|
|
// Eden's rule: the collector's budget counts visits, not evictions.
|
|
Eden::Performance::gc_count_visits = true;
|
|
} else if (entry == "dma_batch=off") {
|
|
// One engine call per GPU command word, as upstream (performance.h dma_batch).
|
|
Eden::Performance::dma_batch = false;
|
|
std::printf("EDEN_DEV_DMA_BATCH 0\n");
|
|
} else if (entry.starts_with("dispatch_draws=")) {
|
|
// Draws between Vulkan worker hand-offs: a power of two from 8 (upstream) to 512.
|
|
const unsigned long draws = std::strtoul(entry.c_str() + 15, nullptr, 10);
|
|
if (draws >= 8 && draws <= 512 && (draws & (draws - 1)) == 0)
|
|
Eden::Performance::dispatch_mask = static_cast<unsigned>(draws - 1);
|
|
} else if (entry.starts_with("compose_delay_us=")) {
|
|
// Guest vsync signalled first, composition N us later (VI conductor; 0 = upstream).
|
|
Eden::Performance::compose_delay_us = std::stoi(entry.substr(17));
|
|
} else if (entry.starts_with("idle_spin_us=")) {
|
|
// Guest cores spin this long on their interrupt flag before sleeping (W1).
|
|
Eden::Performance::idle_spin_iterations = static_cast<unsigned>(std::stoul(entry.substr(13)) * 50);
|
|
} else if (entry.starts_with("kspin=")) {
|
|
// PAUSE iterations a guest kernel spin lock retries before blocking (performance.h).
|
|
Eden::Performance::kernel_spin_iterations = static_cast<unsigned>(std::stoul(entry.substr(6)));
|
|
} else if (entry.starts_with("pc_core=") && entry.size() == 9 && entry[8] >= '0' && entry[8] <= '3') {
|
|
// Host PC samples from this guest core instead of core 0 (with --pc-sample).
|
|
Eden::Performance::pc_sample_core = static_cast<unsigned>(entry[8] - '0');
|
|
} else if (entry == "pc_fast=on") {
|
|
Eden::Performance::pc_fast = true;
|
|
} else if (entry == "capture=early") {
|
|
Eden::Performance::capture_early = true;
|
|
} else if (entry == "jit_dups=on") {
|
|
// Which cores compiled each A64 block, and when (EDEN_PERF_DUPLICATES).
|
|
Eden::Performance::jit_duplicate_tracking = true;
|
|
} else if (entry == "applets=firmware") {
|
|
// Eden's default library applets: several run from the firmware as guest programs.
|
|
Eden::Performance::firmware_applets = true;
|
|
} else if (entry == "jit_list=on" || entry == "jit_list=off") {
|
|
// The saved block list (jit_list.h), whatever the settings file says.
|
|
Eden::JitList::enabled = entry.ends_with("on");
|
|
dev_block_list = true;
|
|
} else if (entry == "jit_page_reuse=off") {
|
|
// One page-table lookup per A64 access, as upstream (headless/dynarmic/page-reuse.cmake).
|
|
eden_page_reuse = false;
|
|
} else if (entry == "jit_plain_ordered_reads=off") {
|
|
// Ordered and exclusive reads are a locked RMW again, as upstream (headless/CMakeLists.txt).
|
|
eden_plain_ordered_reads = false;
|
|
} else if (entry == "jit_fcmp_adc=off") {
|
|
// FCMP turns host flags into NZCV with rcl again, as upstream (headless/dynarmic/fast_paths.cmake).
|
|
eden_fcmp_adc = false;
|
|
} else if (entry == "jit_light_dmb=off") {
|
|
// Every DMB is an mfence again, as upstream (headless/dynarmic/fast_paths.cmake).
|
|
eden_light_dmb = false;
|
|
} else if (entry == "jit_checkbit_flags=off") {
|
|
// CBZ/TBZ-style terminals reload their condition from the stack, as upstream (headless/dynarmic/fast_paths.cmake).
|
|
eden_checkbit_flags = false;
|
|
} else if (entry == "jit_r13=off") {
|
|
// The JIT allocator skips R13 in every block, as upstream (headless/dynarmic/reg_alloc.cmake).
|
|
eden_alloc_r13 = false;
|
|
} else if (entry == "jit_free_moves=off") {
|
|
// Registers moved out of the way go to the stack, as upstream (headless/dynarmic/reg_alloc.cmake).
|
|
eden_free_moves = false;
|
|
} else if (entry == "jit_shared=off") {
|
|
// Every guest core keeps its own compiled blocks (headless/dynarmic/jit_group.h).
|
|
eden_jit_shared = false;
|
|
} else if (entry == "cpu_accuracy=unsafe") {
|
|
// Dynarmic's unsafe FP shortcuts on top of Auto (reduced-error estimates, inaccurate NaN).
|
|
Settings::values.cpu_accuracy = Settings::CpuAccuracy::Unsafe;
|
|
} else if (entry == "replay=off") {
|
|
// The profile title takes controller and runner input instead of the timed replay.
|
|
} 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 == "load_hang=on") {
|
|
dev_load_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;
|
|
} else if (entry == "gpu_accuracy=low") {
|
|
// Nothing calls UpdateGPUAccuracy() here; set the live value too.
|
|
Settings::values.gpu_accuracy.SetValue(Settings::GpuAccuracy::Low);
|
|
Settings::values.current_gpu_accuracy = Settings::GpuAccuracy::Low;
|
|
} else if (entry == "null_descriptor=off") {
|
|
Eden::DevVulkan::disable_null_descriptor = true;
|
|
} else if (entry == "descriptor_buffer=off") {
|
|
Eden::DevVulkan::disable_descriptor_buffer = true;
|
|
} else if (entry == "robustness2=on") {
|
|
Eden::DevVulkan::robustness2 = true;
|
|
} else if (entry == "vertex_input_dynamic=off" || entry == "vertex_input_dynamic=on") {
|
|
dev_vertex_input = entry.ends_with("on") ? 1 : 0;
|
|
} else if (entry == "static_vi=off") {
|
|
Eden::DevVulkan::static_vertex_input = false;
|
|
} else if (entry == "pipeline_trace=on") {
|
|
Eden::DevVulkan::trace_pipelines = true;
|
|
} else if (entry.starts_with("radv_debug=")) {
|
|
// Read by RADV at instance creation, which happens after this parse.
|
|
setenv("RADV_DEBUG", entry.c_str() + 11, 1);
|
|
} else if (entry.starts_with("env=") && entry.find('=', 4) != std::string::npos) {
|
|
// Other driver switches read at device creation, e.g. env=RADV_PS5_BORDER_REBIND=1.
|
|
const auto split = entry.find('=', 4);
|
|
setenv(entry.substr(4, split - 4).c_str(), entry.c_str() + split + 1, 1);
|
|
} else if (entry == "sparse=off") {
|
|
Eden::DevVulkan::disable_sparse = true;
|
|
} else if (entry == "multirange=off") {
|
|
Eden::DevVulkan::disable_multi_range = true;
|
|
} else if (entry == "custom_border=off") {
|
|
Eden::DevVulkan::disable_custom_border = true;
|
|
} else if (entry == "custom_border=on") {
|
|
Eden::DevVulkan::disable_custom_border = false;
|
|
} else if (entry == "vram=aggressive") {
|
|
// Eden's own larger integrated-GPU budget (6 GiB instead of 4 GiB), the default.
|
|
Settings::values.vram_usage_mode.SetValue(Settings::VramUsageMode::Aggressive);
|
|
} else if (entry == "vram=conservative") {
|
|
Settings::values.vram_usage_mode.SetValue(Settings::VramUsageMode::Conservative);
|
|
} else if (entry == "conditional_rendering=off") {
|
|
Eden::DevVulkan::disable_conditional_rendering = true;
|
|
} else if (entry == "conditional_rendering=on") {
|
|
Eden::DevVulkan::disable_conditional_rendering = false;
|
|
} else if (entry == "gpu_time=on") {
|
|
Eden::DevVulkan::gpu_time = true;
|
|
} else if (entry == "submit_sync=on") {
|
|
Eden::DevVulkan::sync_submissions = true;
|
|
} else if (entry == "astc=cpu") {
|
|
Settings::values.accelerate_astc.SetValue(Settings::AstcDecodeMode::Cpu);
|
|
} else if (entry == "log=render") {
|
|
// Renderer debug logging for crash probes (fetched every few seconds).
|
|
Common::Log::Filter filter;
|
|
filter.ParseFilterString("*:Info Render:Debug Render.Vulkan:Debug HW.GPU:Debug Shader:Debug");
|
|
Common::Log::SetGlobalFilter(filter);
|
|
} else if (entry.starts_with("boot_trace=") && entry.find(':') != std::string::npos) {
|
|
// Individual guest GPU submissions/dispatches from START to END ms after now.
|
|
const auto colon = entry.find(':');
|
|
const long long now = Eden::Performance::NowNs();
|
|
Eden::Performance::boot_trace_begin = now + std::stoll(entry.substr(11, colon - 11)) * 1000000;
|
|
Eden::Performance::boot_trace_end = now + std::stoll(entry.substr(colon + 1)) * 1000000;
|
|
} else if (entry == "hcr=eden" || entry == "hcr=exact" || entry == "hcr=cpu") {
|
|
Eden::DevVulkan::hcr_mode = entry == "hcr=eden" ? 0 : entry == "hcr=exact" ? 1 : 2;
|
|
} else if (entry == "compute_barriers=off") {
|
|
Eden::DevVulkan::compute_barriers = false;
|
|
} else if (entry == "gpu_clock=normal" || entry == "gpu_clock=boost" || entry == "gpu_clock=overclock") {
|
|
// Guest-visible GPU timestamp scale (Eden's fast_gpu_time; Boost is the default).
|
|
Settings::values.gpu_clock.SetValue(entry.ends_with("normal") ? Settings::GpuClock::Normal :
|
|
entry.ends_with("boost") ? Settings::GpuClock::Boost : Settings::GpuClock::Overclock);
|
|
} else if (entry.starts_with("rom=")) {
|
|
// Development title override, applied when the title is selected.
|
|
} else if (entry == "fs_callers=on") {
|
|
Eden::Performance::trace_fs_callers = true;
|
|
} else if (entry.starts_with("log_filter=")) {
|
|
// Any Eden log filter for crash probes, '+' for spaces,
|
|
// e.g. log_filter=*:Info+Service:Debug+Service.HID:Info.
|
|
std::string text = entry.substr(11);
|
|
std::replace(text.begin(), text.end(), '+', ' ');
|
|
Common::Log::Filter filter;
|
|
filter.ParseFilterString(text);
|
|
Common::Log::SetGlobalFilter(filter);
|
|
} else if (Eden::Watch::Parse(entry)) {
|
|
// Write watch / code dump on the main module, armed after load (watch.h).
|
|
} else if (entry.starts_with("dyna_state=") && entry.size() == 12 &&
|
|
entry[11] >= '0' && entry[11] <= '3') {
|
|
Settings::values.dyna_state.SetValue(
|
|
static_cast<Settings::ExtendedDynamicState>(entry[11] - '0'));
|
|
} else {
|
|
std::printf("EDEN_DEV_SETTINGS unknown=%s\n", entry.c_str());
|
|
}
|
|
}
|
|
std::printf("EDEN_DEV_SETTINGS dma_accuracy=%u gpu_accuracy=%u null_descriptor=%u "
|
|
"descriptor_buffer=%u robustness2=%u vertex_input_dynamic=%u dyna_state=%u "
|
|
"sparse=%u multirange=%u custom_border=%u submit_sync=%u fastmem=%u fastmem_sites=%u\n",
|
|
unsigned(Settings::values.dma_accuracy.GetValue()),
|
|
unsigned(Settings::values.current_gpu_accuracy),
|
|
unsigned(!Eden::DevVulkan::disable_null_descriptor),
|
|
unsigned(!Eden::DevVulkan::disable_descriptor_buffer),
|
|
unsigned(Eden::DevVulkan::robustness2),
|
|
unsigned(dev_vertex_input != 0),
|
|
unsigned(Settings::values.dyna_state.GetValue()),
|
|
unsigned(!Eden::DevVulkan::disable_sparse),
|
|
unsigned(!Eden::DevVulkan::disable_multi_range),
|
|
unsigned(!Eden::DevVulkan::disable_custom_border),
|
|
unsigned(Eden::DevVulkan::sync_submissions),
|
|
unsigned(Eden::Fastmem::Requested()), unsigned(Eden::Fastmem::SitesRequested()));
|
|
std::printf("EDEN_DEV_MEMORY direct_memory=%lld vram_mode=%u\n",
|
|
static_cast<long long>(sceKernelGetDirectMemorySize()),
|
|
unsigned(Settings::values.vram_usage_mode.GetValue()));
|
|
}
|
|
Settings::values.use_docked_mode.SetValue(Settings::ConsoleMode::Docked);
|
|
std::puts("[headless-startup] console_mode=docked");
|
|
#else
|
|
Settings::values.use_docked_mode.SetValue(Settings::ConsoleMode::Handheld);
|
|
#if defined(PS5_NATIVE) && defined(EDEN_PS5_OPENGL)
|
|
const bool docked = Eden::LoadGameDocked(eden_game_title_id(guest));
|
|
Settings::values.use_docked_mode.SetValue(docked ? Settings::ConsoleMode::Docked
|
|
: Settings::ConsoleMode::Handheld);
|
|
Eden::Report("launch", docked ? "Console mode: Docked" : "Console mode: Handheld");
|
|
#endif
|
|
#endif
|
|
// The guest's memory as a service, before the session takes it (dev-settings cram=off ...).
|
|
Common::SetGuestMemoryService(dev_cram_on, dev_cram_scan, dev_cram_dedup);
|
|
#if defined(PS5_NATIVE) && defined(EDEN_PS5_OPENGL)
|
|
{
|
|
// Settings > Video: internal resolution and the filter scaling it to the output.
|
|
static constexpr Settings::ResolutionSetup resolutions[] = {
|
|
Settings::ResolutionSetup::Res1_2X, Settings::ResolutionSetup::Res3_4X, Settings::ResolutionSetup::Res1X,
|
|
Settings::ResolutionSetup::Res3_2X, Settings::ResolutionSetup::Res2X, Settings::ResolutionSetup::Res3X,
|
|
Settings::ResolutionSetup::Res4X};
|
|
static_assert(std::size(resolutions) == std::size(Eden::kResolutionKeys));
|
|
static constexpr Settings::ScalingFilter filters[] = {
|
|
Settings::ScalingFilter::Bilinear, Settings::ScalingFilter::Fsr, Settings::ScalingFilter::Bicubic,
|
|
Settings::ScalingFilter::NearestNeighbor};
|
|
// Settings > Video, with what the game does differently (Library > Game settings > Video).
|
|
const auto video = Eden::PreferencesFor(eden_game_title_id(guest));
|
|
const int resolution = video.resolution;
|
|
const int filter = video.upscaling_filter;
|
|
Settings::values.resolution_setup.SetValue(resolutions[resolution]);
|
|
// ASTC textures are kept as BC3 unless Settings > Video > ASTC textures, or the game's
|
|
// own choice, says full quality. The console's GPU has no ASTC, so Eden holds
|
|
// such a texture decoded, at four bytes a pixel; BC3 takes one. A large open-world game
|
|
// has two texture arrays of 121 layers in that format: 816 MiB decoded, 204 MiB as BC3.
|
|
// At 2x, where every render target also has a copy four times its size, that was the
|
|
// memory a session ran out of within a minute of play, and with BC3 it did not; the GPU
|
|
// thread's time for a frame was the same either way (26.0 and 25.6 to 26.7 ms). At 1x
|
|
// it took longer and ended the same way: seventeen minutes into a session one of those
|
|
// arrays was refused by the allocator and then by the driver. So at every resolution.
|
|
// Full quality is for a game whose textures show what BC3 loses and that has the
|
|
// memory to spare.
|
|
const bool astc_full = video.astc_textures == 1;
|
|
Settings::values.astc_recompression.SetValue(astc_full ? Settings::AstcRecompression::Uncompressed
|
|
: Settings::AstcRecompression::Bc3);
|
|
// Kept as BC3, a texture is decoded and compressed on the CPU, and Eden does that inside
|
|
// the draw that first uses it, with both GPU caches locked: 130 to 290 ms for a draw
|
|
// where a frame's 4,000 draws take 18 together, three seconds for a texture array of
|
|
// 121 layers. New scenery brought several such frames in a row (41 in a session of
|
|
// five minutes), the drops to 10 or 15 frames a second. On another thread (Eden's own
|
|
// asynchronous mode) the texture shows a moment later and no frame waits for it: 6
|
|
// such frames in the same play. A light screen (loading, a menu) still converts in
|
|
// place, so nothing is drawn there before it is ready (headless/astc_policy.h, which the
|
|
// Vulkan texture cache asks; the OpenGL one has no such rule and keeps converting in
|
|
// place). At full quality nothing is compressed, and the GPU decodes the texture.
|
|
Settings::values.accelerate_astc.SetValue(!astc_full && backend == Eden::GraphicsBackend::Vulkan
|
|
? Settings::AstcDecodeMode::CpuAsynchronous
|
|
: Settings::AstcDecodeMode::Gpu);
|
|
Settings::values.scaling_filter.SetValue(filters[filter]);
|
|
Settings::UpdateRescalingInfo();
|
|
// Settings > Video > Post-processing, or the game's own: one of Eden's ReShade FX presets
|
|
// (the bundled ones and the .fxp files in post_presets), applied by Eden's own preset code
|
|
// before the renderer reads the chain. Every session starts with none: one app run starts
|
|
// many games. The pass is drawn by the Vulkan renderer only.
|
|
Settings::values.post_shader_enabled.SetValue(false);
|
|
Settings::values.post_shader_chain.SetValue(std::string{});
|
|
Settings::values.post_shader_preset.SetValue(std::string{});
|
|
// A game in its own folder in roms/ (issue 168) brings its update, DLC, mods, cheats
|
|
// and its own post-processing (reshade_folder.h): its presets and effects are read
|
|
// again when the folder is another than the last game's.
|
|
Eden::Mods::SessionGameFolder().clear();
|
|
if (guest) {
|
|
const std::string roms = Eden::AssetsPath("roms/");
|
|
const std::string_view path{guest};
|
|
if (path.starts_with(roms))
|
|
if (const auto slash = path.find('/', roms.size()); slash != std::string_view::npos)
|
|
Eden::Mods::SessionGameFolder() = std::string{path.substr(0, slash)};
|
|
}
|
|
if (Eden::Reshade::SetGameFolder(Eden::Mods::SessionGameFolder())) {
|
|
VideoCore::ReloadFxPresetCatalog();
|
|
VideoCore::ReloadFxCatalog();
|
|
}
|
|
if (!video.post_processing.empty()) {
|
|
std::string line = "Post-processing: " + video.post_processing;
|
|
if (backend != Eden::GraphicsBackend::Vulkan) {
|
|
line += " (off: it needs the Vulkan renderer)";
|
|
} else if (VideoCore::ApplyFxPreset(video.post_processing)) {
|
|
Settings::values.post_shader_enabled.SetValue(true);
|
|
} else {
|
|
line += " (off: no preset of that name)";
|
|
}
|
|
Eden::Report("launch", line.c_str());
|
|
}
|
|
// Frame generation: common setup for both methods (LSFG and MihawkGen).
|
|
// The target rate and multiplier control the pacer regardless of which method
|
|
// generates the frames. framegen.txt overrides them; Vulkan only.
|
|
{
|
|
const auto at = [](int index, auto count) {
|
|
return std::clamp(index, 0, static_cast<int>(count) - 1);
|
|
};
|
|
const int target_index =
|
|
at(video.frame_gen_target, std::size(Eden::kFrameGenTargetHz));
|
|
const int multiplier_index =
|
|
at(video.frame_gen_multiplier, std::size(Eden::kFrameGenMultiplierValue));
|
|
int target = Eden::kFrameGenTargetHz[target_index];
|
|
int multiplier = Eden::kFrameGenMultiplierValue[multiplier_index];
|
|
int override_rate = -1;
|
|
int override_multiplier = -1;
|
|
std::ifstream frame_gen_file(Eden::AppFile("framegen.txt"));
|
|
(void)(frame_gen_file >> override_rate);
|
|
(void)(frame_gen_file >> override_multiplier);
|
|
if (override_rate > 0) target = override_rate;
|
|
if (override_multiplier >= 2) multiplier = override_multiplier;
|
|
const bool wants_framegen = (override_rate >= 0 || override_multiplier >= 0 || video.frame_gen) &&
|
|
video.backend == Eden::GraphicsBackend::Vulkan;
|
|
#ifdef EDEN_PS5_FRAMEGEN
|
|
const bool use_lsfg = wants_framegen && video.frame_gen_method == 0;
|
|
Settings::values.frame_gen.SetValue(use_lsfg);
|
|
Settings::values.frame_gen_target_rate.SetValue(
|
|
use_lsfg ? static_cast<unsigned>(std::min(target, 240)) : 0u);
|
|
Settings::values.frame_gen_multiplier.SetValue(
|
|
static_cast<unsigned>(std::clamp(multiplier, 2, 4)));
|
|
if (use_lsfg)
|
|
{
|
|
const auto folder = Common::FS::GetEdenPath(Common::FS::EdenPath::LosslessDir);
|
|
const auto status =
|
|
static_cast<unsigned>(VideoCore::FrameGen::GetInstalledLosslessStatus());
|
|
constexpr const char* kLosslessStates[] = {
|
|
"ok", "Lossless.dll not installed", "Lossless.dll unreadable",
|
|
"not a PE", "no LSFG shaders in it", "shader translation failed",
|
|
"shader cache unusable"};
|
|
Eden::Report("launch",
|
|
("Frame generation: " +
|
|
std::string(target > 0 ? std::to_string(target) + " Hz target"
|
|
: std::to_string(multiplier) + "x multiplier") +
|
|
"; " + folder.string() +
|
|
": " + (status < std::size(kLosslessStates) ? kLosslessStates[status]
|
|
: "unknown") +
|
|
"; null descriptors " +
|
|
(Eden::DevVulkan::disable_null_descriptor ? "off" : "on"))
|
|
.c_str());
|
|
}
|
|
#endif
|
|
#ifdef EDEN_PS5_MIHAWKGEN
|
|
const bool use_mihawkgen = wants_framegen && video.frame_gen_method == 1;
|
|
Eden::MihawkGenBridge::active.store(use_mihawkgen);
|
|
if (use_mihawkgen) {
|
|
Eden::MihawkGenBridge::weights_path =
|
|
Eden::FileExists(Eden::UserDir() + "/mihawkgen/network.fgw")
|
|
? Eden::UserDir() + "/mihawkgen/network.fgw"
|
|
: Eden::AppDir() + "/mihawkgen/network.fgw";
|
|
Eden::MihawkGenBridge::target_rate.store(target);
|
|
Eden::MihawkGenBridge::multiplier.store(multiplier);
|
|
Settings::values.frame_gen.SetValue(true);
|
|
Settings::values.frame_gen_target_rate.SetValue(
|
|
static_cast<unsigned>(std::min(target, 240)));
|
|
Settings::values.frame_gen_multiplier.SetValue(
|
|
static_cast<unsigned>(std::clamp(multiplier, 2, 4)));
|
|
}
|
|
Eden::Report("launch",
|
|
(std::string("MihawkGen frame generation: ") +
|
|
(use_mihawkgen ? "active" : "inactive") + "; weights " +
|
|
(Eden::FileExists(Eden::MihawkGenBridge::weights_path)
|
|
? "found" : "not found") +
|
|
" at " + Eden::MihawkGenBridge::weights_path).c_str());
|
|
#endif
|
|
}
|
|
// The output's refresh rate while the game runs (display_refresh.h): the renderer asks
|
|
// for it as it opens the output.
|
|
const int refresh = video.refresh;
|
|
Eden::Display::requested_hz.store(Eden::kRefreshHz[refresh]);
|
|
Eden::Display::output_millihertz.store(0);
|
|
setenv(Eden::Display::kVulkanSwitch, refresh ? "1" : "0", 1);
|
|
Eden::Display::game_millihertz.store(60000);
|
|
Eden::Display::skipped_frames.store(0);
|
|
// The size of the picture the session puts out (Settings > Video > Output resolution).
|
|
Eden::Display::output_width.store(Eden::kOutputWidth[video.output]);
|
|
Eden::Display::output_height.store(Eden::kOutputHeight[video.output]);
|
|
Eden::Report("launch", (std::string("Resolution ") + Eden::kResolutionKeys[resolution] + ", " +
|
|
Eden::kUpscalingFilterLabels[filter] + ", output " +
|
|
Eden::kOutputKeys[video.output] + ", " + Eden::kRefreshKeys[refresh] +
|
|
" Hz, ASTC textures " + (astc_full ? "decoded" : "as BC3")).c_str());
|
|
// Speed against accuracy ("performance" in the settings file, settings_store.h): each
|
|
// one is off unless chosen there, for every game or for this one. The levels start
|
|
// every session at their exact values (above), so one that is not chosen is off.
|
|
const auto speed = Eden::LoadPerformance(eden_game_title_id(guest));
|
|
Settings::values.use_asynchronous_shaders = speed.async_shaders;
|
|
if (speed.fast_gpu) {
|
|
// Nothing calls UpdateGPUAccuracy() here; set the live value too.
|
|
Settings::values.gpu_accuracy.SetValue(Settings::GpuAccuracy::Low);
|
|
Settings::values.current_gpu_accuracy = Settings::GpuAccuracy::Low;
|
|
}
|
|
if (speed.unsafe_cpu) Settings::values.cpu_accuracy = Settings::CpuAccuracy::Unsafe;
|
|
if (speed.unsafe_dma) Settings::values.dma_accuracy.SetValue(Settings::DmaAccuracy::Unsafe);
|
|
Settings::values.use_reactive_flushing.SetValue(speed.reactive_flushing);
|
|
Settings::values.skip_cpu_inner_invalidation.SetValue(speed.skip_invalidation);
|
|
// Settings > Performance > Compact memory: the guest's memory in 16 KiB pages where a
|
|
// written slot is mostly zeroes (src/guest_memory_service.inc), before the session takes it.
|
|
Common::SetGuestMemoryFine(speed.compact_memory || dev_cram_fine);
|
|
// Settings > Performance > Memory: the memory the guest's console is told it has
|
|
// (Eden's memory layout). A mod that keeps a heap of its own asks the kernel how much
|
|
// memory there is and reserves what it wants from it; on the Switch's own 4 GiB it finds
|
|
// less than it asked for and the game stops before its first picture, which is the
|
|
// kernel's "jumped to an unmapped address" and not a crash of ours. The larger layouts
|
|
// are the console's developer memories: the guest's kernel divides the larger block the
|
|
// same way, so the mod's heap is there. Above 4 GiB the guest's memory is the block
|
|
// backed where the kernel uses it (src/memory_pages.cpp), so the pool below only has to
|
|
// hold what a session touches, not the whole of it.
|
|
Settings::values.memory_layout_mode =
|
|
speed.memory_layout == 1 ? Settings::MemoryLayout::Memory_6Gb
|
|
: speed.memory_layout == 2 ? Settings::MemoryLayout::Memory_8Gb
|
|
: Settings::MemoryLayout::Memory_4Gb;
|
|
Eden::Report("launch",
|
|
("Memory layout: " + std::string{Eden::kMemoryLayoutKeys[speed.memory_layout]} +
|
|
" (" + std::to_string(Eden::kMemoryLayoutGib[speed.memory_layout]) +
|
|
" GiB), direct memory pool " +
|
|
std::to_string(sceKernelGetDirectMemorySize() >> 20) + " MiB")
|
|
.c_str());
|
|
// Settings > Performance > Speed limit: how fast the game may run, as a percentage of
|
|
// the console it was made for. Eden's vsync clock comes that much sooner (vi/conductor.cpp
|
|
// on several cores), so a game that waits for it runs faster: one locked to 30 FPS can
|
|
// reach 60 at 200% where the console has the time to spare, and one that counts its
|
|
// frames to keep time plays faster with them. Unlimited switches the limit off. 100%,
|
|
// the default, is Eden's own default and what every session had before.
|
|
const int speed_percent = Eden::kSpeedLimitPercent[speed.speed_limit];
|
|
Settings::values.use_speed_limit.SetValue(speed_percent > 0);
|
|
Settings::values.speed_limit.SetValue(static_cast<u16>(speed_percent > 0 ? speed_percent : 100));
|
|
// The block list is off unless the settings turn it on (block-list.txt in the app
|
|
// folder still does, further down).
|
|
if (!dev_block_list) Eden::JitList::enabled = speed.block_list;
|
|
// Set for every session: the value outlives one.
|
|
Settings::values.vertex_input_dynamic_state.SetValue(dev_vertex_input != 0);
|
|
// The report reads the values back: what the emulator has, whoever set it.
|
|
const auto on = [](bool value) { return value ? "on" : "off"; };
|
|
Eden::Report("launch", (std::string("Performance: block list ") + on(Eden::JitList::enabled) +
|
|
", async shaders " + on(Settings::values.use_asynchronous_shaders.GetValue()) +
|
|
", fast GPU " + on(!Settings::IsGPULevelHigh()) +
|
|
", unsafe CPU " +
|
|
on(Settings::values.cpu_accuracy.GetValue() == Settings::CpuAccuracy::Unsafe) +
|
|
", unsafe DMA " +
|
|
on(Settings::values.dma_accuracy.GetValue() == Settings::DmaAccuracy::Unsafe) +
|
|
", reactive flushing " + on(Settings::values.use_reactive_flushing.GetValue()) +
|
|
", skip invalidation " +
|
|
on(Settings::values.skip_cpu_inner_invalidation.GetValue()) +
|
|
", speed limit " +
|
|
(Settings::values.use_speed_limit.GetValue() ?
|
|
std::to_string(Settings::values.speed_limit.GetValue()) + "%" :
|
|
std::string{"unlimited"})).c_str());
|
|
// What a crash report says was running.
|
|
char title_id[20];
|
|
std::snprintf(title_id, sizeof(title_id), "%016llx",
|
|
static_cast<unsigned long long>(eden_game_title_id(guest)));
|
|
// The game's mods, minus those switched off in the launcher (Library > Game settings >
|
|
// Mods and Cheats): Eden's patch manager skips the names in this list. The game's Mods
|
|
// and Cheats switches add theirs (mods.h).
|
|
const u64 title = eden_game_title_id(guest);
|
|
// The game's own folder (Mods::SessionGameFolder) was found with its post-processing.
|
|
const auto all_mods = Eden::Mods::List(
|
|
{Eden::AssetsPath("mods"), Eden::AssetsPath("cheats"), Eden::Mods::SessionGameFolder()}, title);
|
|
// Of a mod that lists several cheats, only the ones chosen there run (cheats.h).
|
|
const bool cheats_enabled = Eden::LoadCheatsEnabled(title);
|
|
const auto cheats_on = cheats_enabled ? Eden::LoadChosenCheats(title) : std::vector<std::string>{};
|
|
auto switched = Eden::Mods::Switches(all_mods, Eden::LoadDisabledMods(title), cheats_on,
|
|
Eden::LoadCheatsOff(title), Eden::LoadModsEnabled(title),
|
|
cheats_enabled);
|
|
Eden::Cheats::Off() = std::move(switched.cheats_off);
|
|
const auto& mods_off = switched.mods_off;
|
|
const std::string mods = Eden::Mods::Summary(all_mods, mods_off, cheats_on);
|
|
Settings::values.disabled_addons[title] = mods_off;
|
|
Eden::Report("launch", ("Mods: " + mods).c_str());
|
|
Eden::Crash::SetSession("game " + std::filesystem::path(guest).filename().string() + " (" + title_id +
|
|
"), " + Eden::BackendName(backend) + ", resolution " +
|
|
Eden::kResolutionKeys[resolution] + ", " + Eden::kUpscalingFilterLabels[filter] +
|
|
", output " + Eden::kOutputKeys[video.output] + ", " +
|
|
Eden::kRefreshKeys[refresh] + " Hz, mods: " + mods,
|
|
true);
|
|
}
|
|
#endif
|
|
Settings::values.sink_id = Settings::AudioEngine::Null;
|
|
Settings::values.use_multi_core = true;
|
|
// A game's library applets (its error dialog, profile picker, amiibo screen, Mii editor,
|
|
// manual, album...) are Eden's built-in ones. Eden's defaults start several of them from
|
|
// the firmware as a second guest program. Nothing can operate those here, and the error
|
|
// applet started that way ran a game's session out of graphics memory on the console
|
|
// (CreateBuffer: VK_ERROR_OUT_OF_DEVICE_MEMORY as the applet started). The built-in ones
|
|
// answer at once: the error is logged (error_applet.h), the first profile is chosen, the
|
|
// others close.
|
|
// (dev-settings applets=firmware keeps Eden's defaults.)
|
|
#ifdef EDEN_DEV_PROFILE
|
|
if (!Eden::Performance::firmware_applets)
|
|
#endif
|
|
{
|
|
Settings::values.cabinet_applet_mode = Settings::AppletMode::HLE;
|
|
Settings::values.error_applet_mode = Settings::AppletMode::HLE;
|
|
Settings::values.net_connect_applet_mode = Settings::AppletMode::HLE;
|
|
Settings::values.player_select_applet_mode = Settings::AppletMode::HLE;
|
|
Settings::values.mii_edit_applet_mode = Settings::AppletMode::HLE;
|
|
Settings::values.photo_viewer_applet_mode = Settings::AppletMode::HLE;
|
|
Settings::values.offline_web_applet_mode = Settings::AppletMode::HLE;
|
|
Settings::values.my_page_applet_mode = Settings::AppletMode::HLE;
|
|
}
|
|
// The PS5 build bounds Eden's GPU command queue below, so the CPU and renderer can run in
|
|
// parallel without accumulating the multi-frame controller lag of the upstream queue.
|
|
Settings::values.use_asynchronous_gpu_emulation = true;
|
|
#ifdef EDEN_PS5_OPENGL
|
|
Settings::values.use_disk_shader_cache = game;
|
|
#else
|
|
Settings::values.use_disk_shader_cache = false;
|
|
#endif
|
|
// The PS5 port has software FFmpeg decoders but no FFmpeg hardware-device adapter.
|
|
Settings::values.nvdec_emulation = game ? Settings::NvdecEmulation::Cpu
|
|
: Settings::NvdecEmulation::Off;
|
|
Settings::values.use_gdbstub = false;
|
|
// Settings > Language: the system language games see, with its console region.
|
|
static_assert(static_cast<int>(Settings::Language::EnglishBritish) == 12 &&
|
|
static_cast<int>(Settings::Language::ChineseTraditional) == 16 &&
|
|
static_cast<int>(Settings::Language::Thai) == 19 &&
|
|
static_cast<int>(Settings::Region::Taiwan) == 6, "Eden's language or region order changed");
|
|
{
|
|
// Eden only finds a game's supported languages when an update supplies its control data.
|
|
// Without them it hands the game the chosen language as it is, and a game that does not
|
|
// know it (an older one given pt-BR) falls back to Japanese. So the game gets its own
|
|
// closest language, the one the launcher shows for it (eden_game_language).
|
|
const int choice = Eden::PreferencesFor(game_title).language;
|
|
int language = Eden::kLanguageSettings[choice];
|
|
#ifdef PS5_NATIVE
|
|
if (game && guest)
|
|
language = eden_game_language(guest, Eden::AssetsPath("keys").c_str(), eden_game_title_id(guest), language);
|
|
#endif
|
|
Settings::values.language_index.SetValue(static_cast<Settings::Language>(language));
|
|
Settings::values.region_index.SetValue(static_cast<Settings::Region>(Eden::kLanguageRegions[choice]));
|
|
const std::string code(reinterpret_cast<const char*>(
|
|
&Service::Set::available_language_codes[static_cast<std::size_t>(language)]), 8);
|
|
Eden::Report("launch", ("Language: " + std::string(code.c_str()) + " (chosen " +
|
|
Eden::kLanguageKeys[choice] + ")").c_str());
|
|
}
|
|
std::unique_ptr<Eden::Pad> pad;
|
|
bool return_to_menu = false;
|
|
if (devices || game) {
|
|
Eden::BootTrace::Line("opening the controllers");
|
|
pad = std::make_unique<Eden::Pad>();
|
|
if (!pad->Open()) throw std::runtime_error("PS5 controller initialization failed");
|
|
Eden::BootTrace::Line("controllers open");
|
|
Settings::values.audio_output_device_id = "ps5";
|
|
// AudioIn sessions take their stream from the input sink, a separate one: left at the
|
|
// default it was upstream's NullSink, which releases no samples (audio.cpp).
|
|
Settings::values.audio_input_device_id = "ps5";
|
|
// Settings > Controls, or the game's own (Library > Game settings > Controls).
|
|
const auto controls = Eden::PreferencesFor(game_title);
|
|
Settings::values.vibration_enabled.SetValue(controls.vibration);
|
|
pad->SetMapping(controls.mapping);
|
|
Eden::Report("launch", (std::string("Controls: vibration ") +
|
|
(controls.vibration ? std::to_string(controls.vibration_strength) + "%" : "off") +
|
|
", buttons " + (controls.mapping == Eden::kDefaultMapping ? std::string("as usual") :
|
|
Eden::Settings::MappingJson(controls.mapping).dump())).c_str());
|
|
// One Pro Controller per signed-in user's DualSense; later changes apply mid-game.
|
|
const unsigned connected = pad->ConnectedPlayers();
|
|
(void)pad->TakeConnectionChanges();
|
|
// The game's own Controller type (Game settings > Controls), a Pro Controller without
|
|
// one. The handheld is a controller of its own, for player 1 only.
|
|
Eden::session_controller = controls.controller;
|
|
const bool handheld = Eden::ControllerSetting(controls.controller) == Settings::ControllerType::Handheld;
|
|
Eden::handheld_in_use = handheld;
|
|
for (std::size_t index = 0; index < Eden::Pad::kMaxPlayers; ++index) {
|
|
auto& player = Settings::values.players.GetValue()[index];
|
|
player.connected = handheld ? false : index == 0 || (connected & (1u << index)) != 0;
|
|
player.controller_type = handheld ? Settings::ControllerType::ProController :
|
|
Eden::ControllerSetting(controls.controller);
|
|
// DualSense rumble (headless/pad.cpp) at the strength of Settings > Controls: a
|
|
// percentage of what the game asks for.
|
|
player.vibration_enabled = true;
|
|
player.vibration_strength = controls.vibration_strength;
|
|
}
|
|
{
|
|
auto& player = Settings::values.players.GetValue()[8]; // Eden's handheld controller
|
|
player.connected = handheld;
|
|
player.controller_type = Settings::ControllerType::Handheld;
|
|
player.vibration_enabled = true;
|
|
player.vibration_strength = controls.vibration_strength;
|
|
}
|
|
if (controls.controller >= 0)
|
|
Eden::Report("launch", (std::string("Controller type: ") +
|
|
Eden::kControllerKeys[controls.controller]).c_str());
|
|
}
|
|
{
|
|
#ifdef EDEN_PS5_OPENGL
|
|
static unsigned memory_session = 0;
|
|
Eden::Performance::ReportSessionMemory("start", ++memory_session);
|
|
Eden::BootTrace::Line("opening the graphics window (%s)", backend == Eden::GraphicsBackend::Vulkan ? "Vulkan" : "OpenGL");
|
|
Eden::GraphicsWindow window(backend == Eden::GraphicsBackend::Vulkan);
|
|
Eden::BootTrace::Line("graphics window open");
|
|
#ifdef EDEN_GPU_PROBE
|
|
window.RunGpuProbe();
|
|
passed("GPU_PROBE_COMPLETE");
|
|
return 0;
|
|
#endif
|
|
#else
|
|
HeadlessWindow window;
|
|
#endif
|
|
// The running game's own contents, filled for each session below. The system keeps this
|
|
// object's address, so it is declared first; its files belong to the system's file
|
|
// system, so it is emptied before the system goes (a second launch crashed when it
|
|
// released the first session's files after their file system).
|
|
FileSys::ManualContentProvider game_contents;
|
|
#ifdef PS5_NATIVE
|
|
if (game) {
|
|
// Who is playing (profiles.h): Eden opens this user of its list, and the game's
|
|
// save data is that user's.
|
|
const auto who = Eden::Profiles::Resolve();
|
|
Settings::values.current_user = who.current;
|
|
if (!who.profiles.empty())
|
|
Eden::Report("launch", ("Profile: " + who.profiles[static_cast<std::size_t>(who.current)].name +
|
|
" (" + std::to_string(who.current + 1) + " of " +
|
|
std::to_string(who.profiles.size()) + ")").c_str());
|
|
}
|
|
// The game's RNG seed fixes where its code is loaded and nothing else
|
|
// (load_address.h): Eden's own setting stays off, so the random numbers the game
|
|
// is given still differ from start to start.
|
|
Settings::values.rng_seed_enabled.SetValue(false);
|
|
Eden::code_seed = -1;
|
|
if (game && guest) {
|
|
const auto rng_seed = Eden::LoadGameSettings(eden_game_title_id(guest)).rng_seed;
|
|
if (rng_seed >= 0) {
|
|
Eden::code_seed = rng_seed;
|
|
Eden::Report("launch", ("RNG seed: " + std::to_string(rng_seed)).c_str());
|
|
}
|
|
}
|
|
#endif
|
|
Core::System system;
|
|
SCOPE_EXIT { game_contents.ClearAllEntries(); };
|
|
passed("core_constructed");
|
|
#ifdef EDEN_PS5_OPENGL
|
|
window.SetSystem(system);
|
|
#endif
|
|
system.Initialize();
|
|
if (system.IsPoweredOn()) return 1;
|
|
passed("core_initialized");
|
|
for (unsigned cycle = 0; guest && cycle < cycles; ++cycle) {
|
|
if (game) LOG_INFO(Frontend, "EDEN_GAME_SESSION_BEGIN {}", cycle + 1);
|
|
struct Completion {
|
|
std::mutex mutex;
|
|
std::condition_variable wake;
|
|
bool exited = false;
|
|
bool captured = false;
|
|
bool return_to_menu = false;
|
|
bool room_lost = false;
|
|
std::exception_ptr failure;
|
|
std::string guest_fault;
|
|
};
|
|
auto completion = std::make_shared<Completion>();
|
|
system.RegisterExitCallback([completion] {
|
|
std::lock_guard lock(completion->mutex);
|
|
completion->exited = true;
|
|
completion->wake.notify_one();
|
|
});
|
|
SCOPE_EXIT {
|
|
if (system.IsPoweredOn()) system.ShutdownMainProcess();
|
|
system.RegisterExitCallback({});
|
|
};
|
|
// Like Eden's Qt/Android frontends, reset shutdown state for each load.
|
|
system.SetShuttingDown(false);
|
|
// Update and DLC files (NSP or XCI) in the game files' updates folder apply to their
|
|
// base game: Eden's ExternalContentProvider, which the patch manager checks first.
|
|
// So do those in the game's own folder, when it has one (roms/<folder>/, issue 168).
|
|
Settings::values.external_content_dirs = {Eden::AssetsPath("updates")};
|
|
if (const std::string& folder = Eden::Mods::SessionGameFolder(); !folder.empty())
|
|
Settings::values.external_content_dirs.push_back(folder);
|
|
system.GetFileSystemController().CreateFactories(*system.GetFilesystem());
|
|
// The game's own contents, registered as Eden's desktop frontends do before booting a
|
|
// game (ConfigureFilesystemProvider). An update's data is a patch on the base game's:
|
|
// the patch manager looks the base up here, and without it applied only the update's
|
|
// code, which then ran against the old data. It also gives Eden the game's control data.
|
|
game_contents.ClearAllEntries();
|
|
if (game && guest && !game_contents.AddEntriesFromContainer(
|
|
system.GetFilesystem()->OpenFile(guest, FileSys::OpenMode::Read)))
|
|
Eden::Report("loader", "Game contents not registered; an update's data will not apply");
|
|
system.RegisterContentProvider(FileSys::ContentProviderUnionSlot::FrontendManual, &game_contents);
|
|
{
|
|
Service::AM::Frontend::FrontendAppletSet applets;
|
|
// A game's "connect controllers" screen: one player per PS5 controller in use.
|
|
if (pad) applets.controller = std::make_unique<Eden::PadControllerApplet>(system.HIDCore(), *pad);
|
|
// A game's error dialog: logged and closed, so the game carries on.
|
|
applets.error = std::make_unique<Eden::LoggedErrorApplet>();
|
|
// A game's text entry: the PS5's own on-screen keyboard.
|
|
applets.software_keyboard = std::make_unique<Eden::SystemKeyboardApplet>(Eden::AskSystemKeyboard);
|
|
system.GetFrontendAppletHolder().SetFrontendAppletSet(std::move(applets));
|
|
}
|
|
Service::AM::FrontendAppletParameters params{
|
|
.applet_id = Service::AM::AppletId::Application,
|
|
};
|
|
#ifdef PS5_NATIVE
|
|
// While a game loads nothing else reads the controller, so Touchpad + L1 did nothing
|
|
// until the game ran: a load that hung could only be left by closing ProsperoEden.
|
|
// Now it is heard here too. The stop limit starts at the press (stop_limit.h): a
|
|
// load that finishes within it ends as a normal stop, one that does not ends with
|
|
// the app starting again at the launcher.
|
|
std::atomic<bool> left_while_loading{false};
|
|
std::jthread load_watch;
|
|
if (pad && game) load_watch = std::jthread([&](std::stop_token stop) {
|
|
while (!stop.stop_requested()) {
|
|
pad->Poll();
|
|
(void)pad->TakeHudToggle();
|
|
#if defined(EDEN_DEV_PROFILE) || defined(EDEN_DEV_ROM_ID)
|
|
// The runner's stand-in for the press: its input commands are read only
|
|
// once the game runs.
|
|
const bool asked = std::remove(Eden::AppFile("leave-loading.txt").c_str()) == 0;
|
|
#else
|
|
const bool asked = false;
|
|
#endif
|
|
if (pad->TakeReturnToMenu() || asked) {
|
|
Eden::Report("exit", "Touchpad + L1 while the game loads: leaving as soon as it can be stopped");
|
|
Eden::StopLimit::Begin();
|
|
left_while_loading = true;
|
|
break;
|
|
}
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(8));
|
|
}
|
|
});
|
|
#endif
|
|
Eden::Loading::Set(Eden::Loading::Step::game);
|
|
Eden::BootTrace::Line("loading the game");
|
|
Core::SystemResultStatus loaded;
|
|
try {
|
|
loaded = system.Load(window, guest, params);
|
|
} catch (const std::exception& error) {
|
|
// Preserve the original error before partial core teardown.
|
|
std::fprintf(stderr, "Game load failed: %s\n", error.what());
|
|
std::fflush(stdout);
|
|
throw;
|
|
}
|
|
if (loaded != Core::SystemResultStatus::Success) {
|
|
std::fprintf(stderr, "Core load failed: %u\n", static_cast<unsigned>(loaded));
|
|
if (loaded == Core::SystemResultStatus::ErrorVideoCore) {
|
|
throw std::runtime_error("Graphics backend initialization failed. Try another backend in Settings; see stderr.log and eden_log.txt for driver details.");
|
|
}
|
|
throw std::runtime_error("Loader status " + std::to_string(static_cast<unsigned>(loaded)) +
|
|
". Check this ROM, its keys and firmware; see stderr.log.");
|
|
}
|
|
#ifdef PS5_NATIVE
|
|
if (game) {
|
|
const auto filename = std::filesystem::path(guest).filename().string();
|
|
const bool saved_last = Eden::SaveLastGame(filename);
|
|
const bool saved_recent = Eden::SaveRecentGame(filename);
|
|
if (!saved_last || !saved_recent)
|
|
Eden::Report("history", "Could not save recent game");
|
|
}
|
|
#endif
|
|
passed(game ? "game_loaded" : "nro_loaded");
|
|
#if defined(PS5_NATIVE) && (defined(EDEN_DEV_PROFILE) || defined(EDEN_DEV_ROM_ID))
|
|
if (dev_load_hang) {
|
|
// dev-settings load_hang=on: a load that never finishes, to check leaving it.
|
|
Eden::Report("loader", "Development: holding the load to check leaving it");
|
|
for (;;) std::this_thread::sleep_for(std::chrono::seconds(1));
|
|
}
|
|
#endif
|
|
Eden::BootTrace::Line("game loaded (status %u)", static_cast<unsigned>(loaded));
|
|
Eden::Report("loader", "Game loaded; initializing renderer");
|
|
Eden::Loading::Set(Eden::Loading::Step::graphics);
|
|
#ifdef EDEN_PS5_OPENGL
|
|
// Retain the failure, then release CPU readiness and complete normal
|
|
// shutdown before reporting it. Unwinding before OnGpuReady can hang.
|
|
std::exception_ptr graphics_error;
|
|
#ifndef PS5_NATIVE
|
|
std::shared_future<bool> game_capture;
|
|
try { window.CheckPresentation(system.GPU().Renderer().Context()); }
|
|
catch (...) { graphics_error = std::current_exception(); }
|
|
if (game && !graphics_error)
|
|
game_capture = window.CaptureNextFrame(system.GPU().Renderer(), [completion] {
|
|
std::lock_guard lock(completion->mutex);
|
|
completion->captured = true;
|
|
completion->wake.notify_one();
|
|
});
|
|
#endif
|
|
#endif
|
|
#if defined(EDEN_DEV_PROFILE) && defined(PS5_NATIVE)
|
|
Eden::Stall::Trace("main gpu_start");
|
|
#endif
|
|
system.GPU().Start();
|
|
#if defined(EDEN_DEV_PROFILE) && defined(PS5_NATIVE)
|
|
Eden::Stall::Trace("main gpu_started");
|
|
#endif
|
|
system.GetCpuManager().OnGpuReady();
|
|
passed("cpu_manager_ready");
|
|
#if defined(EDEN_DEV_PROFILE) && defined(PS5_NATIVE)
|
|
Eden::Stall::Trace("main shader_cache");
|
|
#endif
|
|
#ifdef EDEN_PS5_OPENGL
|
|
// Match yuzu_cmd's ordering: GPU/context ready, cache load, guest Run.
|
|
if (Settings::values.use_disk_shader_cache.GetValue() && !graphics_error) {
|
|
// Every cached pipeline is built before the game starts; report progress
|
|
// every five seconds so a slow build can be told apart from a stalled one.
|
|
std::atomic<size_t> built{0}, total{0};
|
|
std::atomic<bool> counted{false};
|
|
Eden::Loading::Set(Eden::Loading::Step::shaders);
|
|
const auto load_start = std::chrono::steady_clock::now();
|
|
std::jthread reporter([&](std::stop_token stop) {
|
|
#if defined(EDEN_DEV_PROFILE) && defined(PS5_NATIVE)
|
|
Eden::Stall::Trace("reporter start");
|
|
#endif
|
|
for (unsigned tick = 1; !stop.stop_requested(); ++tick) {
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
|
if (tick % 50 != 0 || stop.stop_requested()) continue;
|
|
const std::string line = counted
|
|
? fmt::format("Shader cache: {} of {} pipelines built ({} s)",
|
|
built.load(), total.load(), tick / 10)
|
|
: fmt::format("Shader cache: reading ({} s)", tick / 10);
|
|
Eden::Report("loader", line.c_str());
|
|
}
|
|
});
|
|
#if defined(EDEN_DEV_PROFILE) && defined(PS5_NATIVE)
|
|
Eden::Stall::Trace("main cache_load");
|
|
#endif
|
|
system.Renderer().ReadRasterizer()->LoadDiskResources(
|
|
system.GetApplicationProcessProgramID(), std::stop_token{},
|
|
[&](VideoCore::LoadCallbackStage stage, size_t value, size_t count) {
|
|
if (stage != VideoCore::LoadCallbackStage::Build) return;
|
|
built = value;
|
|
Eden::Loading::Shaders(value, count);
|
|
total = count;
|
|
counted = true;
|
|
#if defined(EDEN_DEV_PROFILE) && defined(PS5_NATIVE)
|
|
Eden::Stall::Tick();
|
|
#endif
|
|
});
|
|
reporter.request_stop();
|
|
reporter.join();
|
|
const auto load_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
std::chrono::steady_clock::now() - load_start).count();
|
|
Eden::Report("loader", fmt::format("Shader cache ready: {} pipelines in {} ms",
|
|
total.load(), load_ms).c_str());
|
|
std::puts("EDEN_SHADER_CACHE_LOADED");
|
|
std::fflush(stdout);
|
|
}
|
|
#endif
|
|
#ifndef PS5_NATIVE
|
|
if (devices) Eden::Mock::BeginGuestCycle();
|
|
#endif
|
|
#ifdef EDEN_DEV_PROFILE
|
|
if (auto* process = system.ApplicationProcess()) {
|
|
// P1: guest code reads for the core-0 profile's block dumps.
|
|
static Core::Memory::Memory* dev_guest_memory = nullptr;
|
|
dev_guest_memory = &process->GetMemory();
|
|
Eden::Performance::guest_read32 = [](unsigned long long address, unsigned& value) {
|
|
if (!dev_guest_memory || !dev_guest_memory->IsValidVirtualAddressRange(address, 4)) return false;
|
|
value = dev_guest_memory->Read32(address);
|
|
return true;
|
|
};
|
|
std::printf("EDEN_MAIN_BASE main=%llx\n",
|
|
static_cast<unsigned long long>(GetInteger(Core::FindMainModuleEntrypoint(process))));
|
|
}
|
|
if (auto* process = system.ApplicationProcess();
|
|
process && (Eden::Watch::watch_range.size || Eden::Watch::dump_range.size)) {
|
|
// Resolve dev-settings watch=/dump= against this boot's main module.
|
|
const u64 main_base = GetInteger(Core::FindMainModuleEntrypoint(process));
|
|
auto& memory = process->GetMemory();
|
|
if (const auto range = Eden::Watch::dump_range; range.size) {
|
|
for (u64 at = main_base + range.offset; at < main_base + range.offset + range.size; at += 0x40) {
|
|
std::string line = fmt::format("EDEN_CRASH_CODE dump ret={:016X} from={:016X}:", at, at);
|
|
for (u64 word = at; word < at + 0x40; word += 4)
|
|
line += fmt::format(" {:08X}", memory.IsValidVirtualAddressRange(word, 4) ? memory.Read32(word) : 0u);
|
|
LOG_CRITICAL(Core_ARM, "{}", line);
|
|
}
|
|
}
|
|
if (const auto range = Eden::Watch::watch_range; range.size) {
|
|
const u64 first = main_base + range.offset;
|
|
Eden::Watch::end = first + range.size;
|
|
Eden::Watch::begin = first;
|
|
memory.MarkRegionDebug(first, range.size, true);
|
|
}
|
|
LOG_CRITICAL(Core_ARM, "EDEN_WATCH main={:016X} watch=+{:#x}:{:#x} dump=+{:#x}:{:#x}", main_base,
|
|
Eden::Watch::watch_range.offset, Eden::Watch::watch_range.size,
|
|
Eden::Watch::dump_range.offset, Eden::Watch::dump_range.size);
|
|
}
|
|
#endif
|
|
// The blocks this game compiled in earlier sessions, compiled ahead on a spare CPU.
|
|
if (std::filesystem::exists(Eden::AppFile("block-list.txt"))) Eden::JitList::enabled = true;
|
|
Eden::JitList::Session jit_list;
|
|
if (auto* process = system.ApplicationProcess()) {
|
|
Eden::JitList::BuildId build{};
|
|
const auto& id = system.GetApplicationProcessBuildID();
|
|
std::memcpy(build.data(), id.data(), std::min(build.size(), id.size()));
|
|
// The game's own modules: the run of code mappings from its entry point.
|
|
const u64 code_start = GetInteger(process->GetEntryPoint());
|
|
u64 code_end = code_start;
|
|
for (;;) {
|
|
Kernel::KMemoryInfo info{};
|
|
Kernel::Svc::PageInfo page{};
|
|
if (process->GetPageTable().QueryInfo(&info, &page, code_end).IsError() || info.m_size == 0 ||
|
|
(info.m_state != Kernel::KMemoryState::Code && info.m_state != Kernel::KMemoryState::CodeData))
|
|
break;
|
|
code_end = info.m_address + info.m_size;
|
|
}
|
|
jit_list.Start(system.GetApplicationProcessProgramID(), build, code_start, code_end - code_start);
|
|
}
|
|
Eden::TakeGuestFault(); // Nothing from an earlier session belongs to this one.
|
|
Eden::Loading::Set(Eden::Loading::Step::starting);
|
|
system.Run();
|
|
#if defined(EDEN_DEV_PROFILE) && defined(PS5_NATIVE)
|
|
Eden::Stall::Trace("main running");
|
|
Eden::Stall::Disarm();
|
|
#endif
|
|
#ifdef PS5_NATIVE
|
|
// The controller goes to the session's own reader from here.
|
|
load_watch.request_stop();
|
|
if (load_watch.joinable()) load_watch.join();
|
|
if (left_while_loading) {
|
|
std::lock_guard lock(completion->mutex);
|
|
completion->return_to_menu = true;
|
|
}
|
|
#endif
|
|
const auto session_start = std::chrono::steady_clock::now();
|
|
[[maybe_unused]] double session_seconds = 0; // guest-fault relaunch (PS5 only)
|
|
std::jthread input_worker;
|
|
if (pad) input_worker = std::jthread([&](std::stop_token stop) {
|
|
#ifdef EDEN_DEV_PROFILE
|
|
// The timed input replay drives the profile's own title (EDEN_DEV_PROFILE_TITLE);
|
|
// other development titles take runner commands (compat-input.txt).
|
|
bool replay_off = false;
|
|
{
|
|
std::ifstream dev_settings(Eden::AppFile("dev-settings.txt"));
|
|
for (std::string entry; dev_settings >> entry;) replay_off |= entry == "replay=off";
|
|
}
|
|
const bool timed_replay = development_id == EDEN_DEV_PROFILE_TITLE && !replay_off;
|
|
const auto replay_start = std::chrono::steady_clock::now();
|
|
#endif
|
|
#ifdef EDEN_DEV_ROM_ID
|
|
Eden::DevelopmentInput development_input;
|
|
unsigned command_poll = 0;
|
|
#endif
|
|
while (!stop.stop_requested()) {
|
|
if (auto error = Eden::TakeGpuFailure()) {
|
|
std::lock_guard lock(completion->mutex);
|
|
completion->failure = std::move(error);
|
|
completion->wake.notify_one();
|
|
break;
|
|
}
|
|
if (auto fault = Eden::TakeGuestFault(); !fault.empty()) {
|
|
std::lock_guard lock(completion->mutex);
|
|
completion->guest_fault = std::move(fault);
|
|
completion->wake.notify_one();
|
|
break;
|
|
}
|
|
#ifdef EDEN_DEV_PROFILE
|
|
if (!timed_replay) {
|
|
#endif
|
|
#ifdef EDEN_DEV_ROM_ID
|
|
if (!development_input.active) { UpdateGrips(system, *pad); pad->Poll(); }
|
|
#else
|
|
UpdateGrips(system, *pad);
|
|
pad->Poll();
|
|
#endif
|
|
#ifdef EDEN_DEV_ROM_ID
|
|
const auto command_now = std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
std::chrono::steady_clock::now().time_since_epoch()).count();
|
|
if (++command_poll >= 25) {
|
|
command_poll = 0;
|
|
std::ifstream command(Eden::AppFile("compat-input.txt"));
|
|
if (development_input.Read(command, command_now))
|
|
std::printf("EDEN_DEV_INPUT sequence=%llu buttons=%x\n",
|
|
static_cast<unsigned long long>(development_input.sequence), development_input.buttons);
|
|
}
|
|
if (const auto sample = development_input.Sample(command_now))
|
|
pad->Consume({&*sample, 1});
|
|
// The guest's Minus going down and up, with how long the state before it
|
|
// lasted: the touchpad's tap and long press (headless/pad.cpp) on real timing.
|
|
{
|
|
static bool minus_down = false;
|
|
static auto minus_since = std::chrono::steady_clock::now();
|
|
const bool down = pad->Engine().GetButton({}, static_cast<int>(
|
|
InputCommon::VirtualGamepad::VirtualButton::ButtonMinus));
|
|
if (down != minus_down) {
|
|
const auto changed = std::chrono::steady_clock::now();
|
|
std::printf("EDEN_DEV_MINUS state=%d after_ms=%lld\n", down,
|
|
static_cast<long long>(std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
changed - minus_since).count()));
|
|
minus_down = down;
|
|
minus_since = changed;
|
|
}
|
|
}
|
|
#endif
|
|
#ifdef EDEN_DEV_PROFILE
|
|
} else {
|
|
using Button = InputCommon::VirtualGamepad::VirtualButton;
|
|
const auto seconds = std::chrono::duration_cast<std::chrono::seconds>(
|
|
std::chrono::steady_clock::now() - replay_start).count();
|
|
// Development replay: enter Single Player/Grand Prix using defaults.
|
|
pad->Engine().SetButtonState(0, Button::TriggerL, seconds == 45);
|
|
pad->Engine().SetButtonState(0, Button::TriggerR, seconds == 45);
|
|
pad->Engine().SetButtonState(0, Button::ButtonA,
|
|
(seconds >= 50 && seconds < 200 && seconds % 5 == 0) || seconds >= 200);
|
|
}
|
|
#endif
|
|
if (const unsigned changed = pad->TakeConnectionChanges()) {
|
|
// A controller that came or went connects or disconnects its player.
|
|
const unsigned connected = pad->ConnectedPlayers();
|
|
for (std::size_t index = 1; index < Eden::Pad::kMaxPlayers; ++index) {
|
|
if (!(changed & (1u << index))) continue;
|
|
const bool present = (connected & (1u << index)) != 0;
|
|
Settings::values.players.GetValue()[index].connected = present;
|
|
auto* controller = system.HIDCore().GetEmulatedControllerByIndex(index);
|
|
// With the handheld chosen, the game has one player.
|
|
const auto chosen = Eden::ChosenStyle(Eden::session_controller.load());
|
|
if (present && chosen == Core::HID::NpadStyleIndex::Handheld) continue;
|
|
if (present) {
|
|
controller->SetNpadStyleIndex(chosen.value_or(Core::HID::NpadStyleIndex::Fullkey));
|
|
controller->Connect();
|
|
} else {
|
|
controller->Disconnect();
|
|
}
|
|
LOG_INFO(Input, "EDEN_PLAYER player={} connected={}", index + 1, present);
|
|
}
|
|
}
|
|
#ifdef EDEN_PS5_OPENGL
|
|
if (pad->TakeHudToggle()) Eden::ToggleHud();
|
|
#else
|
|
(void)pad->TakeHudToggle();
|
|
#endif
|
|
bool room_lost = false;
|
|
#ifdef PS5_NATIVE
|
|
room_lost = rooms.GameConnectionLost();
|
|
#endif
|
|
if (pad->TakeReturnToMenu() || room_lost) {
|
|
// 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->room_lost = room_lost;
|
|
completion->wake.notify_one();
|
|
break;
|
|
}
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(4));
|
|
}
|
|
});
|
|
{
|
|
std::unique_lock lock(completion->mutex);
|
|
constexpr unsigned stop_delays_ms[] = {0, 1, 10, 100, 1000, 30000};
|
|
const auto wait_ms = shutdown_sweep ? stop_delays_ms[cycle] :
|
|
1000u * (cpu_pressure ? 180u : game ? 600u : devices ? 20u : 5u);
|
|
if (shutdown_sweep) LOG_INFO(Frontend, "EDEN_SHUTDOWN_DELAY_MS {}", wait_ms);
|
|
const auto completed = [&] {
|
|
return completion->failure || completion->exited || completion->captured ||
|
|
completion->return_to_menu || !completion->guest_fault.empty();
|
|
};
|
|
#ifdef EDEN_DEV_PROFILE
|
|
// The 30-second segments a development session samples before it only waits.
|
|
// On Vulkan a picture can be asked for (capture-once.txt) for as long as the
|
|
// game runs.
|
|
#ifdef EDEN_DEV_VULKAN
|
|
[[maybe_unused]] constexpr unsigned segments = ~0u;
|
|
#else
|
|
[[maybe_unused]] constexpr unsigned segments = 12;
|
|
#endif
|
|
#endif
|
|
#ifdef PS5_NATIVE
|
|
if (game) {
|
|
#ifdef EDEN_DEV_PROFILE
|
|
for (unsigned segment = 0; segment < segments; ++segment) {
|
|
bool finished = false;
|
|
for (unsigned poll = 0; poll < 600; ++poll) {
|
|
if (completion->wake.wait_for(lock, std::chrono::milliseconds(50), completed)) {
|
|
finished = true;
|
|
break;
|
|
}
|
|
// The runner's crash request, to test the crash report in a game.
|
|
if (poll % 20 == 0) {
|
|
lock.unlock();
|
|
Eden::Crash::DevelopmentRequest(Eden::AppFile("crash-app.txt"));
|
|
if (poll == 0)
|
|
std::printf("EDEN_JIT_PAGE_REUSE enabled=%d groups=%llu accesses=%llu\n", eden_page_reuse ? 1 : 0,
|
|
__atomic_load_n(&eden_page_reuse_groups, __ATOMIC_RELAXED),
|
|
__atomic_load_n(&eden_page_reuse_accesses, __ATOMIC_RELAXED));
|
|
if (jit_table_check && poll == 0) {
|
|
if (auto* process = system.ApplicationProcess())
|
|
CheckDirectTable(process->GetPageTable().GetImpl());
|
|
}
|
|
if (poll == 0)
|
|
std::printf("EDEN_JIT_FREE_MOVES enabled=%d r13=%d plain_ordered_reads=%d moves=%llu spills=%llu\n", eden_free_moves ? 1 : 0, eden_alloc_r13 ? 1 : 0, eden_plain_ordered_reads ? 1 : 0,
|
|
__atomic_load_n(&eden_free_moves_done, __ATOMIC_RELAXED),
|
|
__atomic_load_n(&eden_free_moves_spilled, __ATOMIC_RELAXED));
|
|
if (poll == 0)
|
|
std::printf("EDEN_JIT_FAST_PATHS fcmp_adc=%d light_dmb=%d checkbit_flags=%d dmb_dropped=%llu checkbit_fused=%llu\n", eden_fcmp_adc ? 1 : 0, eden_light_dmb ? 1 : 0, eden_checkbit_flags ? 1 : 0,
|
|
__atomic_load_n(&eden_dmb_dropped, __ATOMIC_RELAXED),
|
|
__atomic_load_n(&eden_checkbit_fused, __ATOMIC_RELAXED));
|
|
lock.lock();
|
|
}
|
|
#ifndef EDEN_DEV_VULKAN
|
|
if (segment >= 5) {
|
|
lock.unlock();
|
|
Eden::Performance::PollGpuPc();
|
|
lock.lock();
|
|
}
|
|
#else
|
|
// Cover the heavy phase (wall ~90-180 s) when requested.
|
|
if (pc_sample_run && segment >= 3) {
|
|
lock.unlock();
|
|
Eden::Performance::PollGpuPc();
|
|
lock.lock();
|
|
}
|
|
#endif
|
|
}
|
|
if (finished) break;
|
|
if (segment == segments - 1) {
|
|
completion->wake.wait(lock, completed);
|
|
break;
|
|
}
|
|
lock.unlock();
|
|
#ifndef EDEN_DEV_VULKAN
|
|
Eden::Performance::Snapshot();
|
|
window.CaptureNextFrame(system.GPU().Renderer(), [] {});
|
|
#else
|
|
std::error_code capture_error;
|
|
if ((segment >= 4 || Eden::Performance::capture_early) &&
|
|
std::filesystem::remove(Eden::AppFile("capture-once.txt"), capture_error))
|
|
window.CaptureNextFrame(system.GPU().Renderer(), [] {});
|
|
#endif
|
|
lock.lock();
|
|
}
|
|
#elif defined(EDEN_DEV_ROM_ID)
|
|
// Bounded compatibility probe (10 minutes; the runner usually closes
|
|
// the title first); use the ordinary teardown path.
|
|
constexpr unsigned observation_samples = 120;
|
|
for (unsigned sample = 0; sample < observation_samples; ++sample) {
|
|
if (completion->wake.wait_for(lock, std::chrono::seconds(5), completed)) break;
|
|
if (sample == observation_samples - 1) {
|
|
completion->return_to_menu = true;
|
|
break;
|
|
}
|
|
lock.unlock();
|
|
#ifdef EDEN_DEV_VULKAN
|
|
std::error_code capture_error;
|
|
const bool capture_requested = performance_run && sample >= 11 &&
|
|
std::filesystem::remove(Eden::AppFile("capture-once.txt"), capture_error);
|
|
if (!performance_run || capture_requested)
|
|
#endif
|
|
window.CaptureNextFrame(system.GPU().Renderer(), [] {});
|
|
lock.lock();
|
|
}
|
|
#else
|
|
completion->wake.wait(lock, completed);
|
|
#endif
|
|
} else
|
|
#endif
|
|
completion->wake.wait_for(lock, std::chrono::milliseconds(wait_ms), completed);
|
|
const bool guest_exited = completion->exited;
|
|
return_to_menu = completion->return_to_menu;
|
|
#ifdef PS5_NATIVE
|
|
if (completion->room_lost)
|
|
launch_error = "The room connection was lost. Rejoin from Multiplayer before launching again.";
|
|
#endif
|
|
session_seconds = std::chrono::duration<double>(std::chrono::steady_clock::now() - session_start).count();
|
|
if (!guest_exited && !game) {
|
|
std::fputs("Guest exit timed out\n", stderr);
|
|
return 1;
|
|
}
|
|
passed(return_to_menu ? "shortcut_return_to_menu" :
|
|
guest_exited ? "guest_exit_callback" : !completion->guest_fault.empty() ? "guest_fault" :
|
|
completion->captured ? "game_capture_shutdown" : "game_observation_complete");
|
|
}
|
|
input_worker.request_stop();
|
|
if (input_worker.joinable()) input_worker.join();
|
|
if (pad) {
|
|
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
|
|
if (devices) {
|
|
Eden::Mock::CheckGuestCycle();
|
|
LOG_INFO(Frontend, "EDEN_GUEST_AUDIO_OUTPUT_PASS");
|
|
LOG_INFO(Frontend, "EDEN_GUEST_RENDERER_PCM_PASS");
|
|
}
|
|
#endif
|
|
if (system.IsPoweredOn()) return 1;
|
|
passed("core_shutdown");
|
|
if (completion->failure) {
|
|
try { std::rethrow_exception(completion->failure); }
|
|
catch (const std::exception& error) {
|
|
// Out of graphics memory: seen with the resolution above 1x in a game that
|
|
// runs at 1x. The launcher shows this sentence in the player's language
|
|
// (prosperoeden/eden_services.cpp, kLaunchErrors).
|
|
const std::string what = error.what();
|
|
if (what.find("OUT_OF_DEVICE_MEMORY") != std::string::npos ||
|
|
what.find("OUT_OF_HOST_MEMORY") != std::string::npos)
|
|
throw std::runtime_error("The game ran out of graphics memory. Lower the resolution in "
|
|
"Settings, Video (or in the game's own settings) and start it again.");
|
|
throw std::runtime_error("Rendering failed: " + what +
|
|
". Try another graphics backend in Settings, then reopen the game.");
|
|
}
|
|
}
|
|
if (!completion->guest_fault.empty()) {
|
|
#ifdef PS5_NATIVE
|
|
// A game can run its save-load completion before its own callback exists
|
|
// (a boot race between two guest threads); a fresh boot normally passes. Retry early faults, report others.
|
|
// Four retries: two faults in a row were seen with slower GPU synchronization
|
|
// (RADV_DEBUG=syncshaders), so the per-boot rate can exceed the usual ~1 in 5.
|
|
if (game && session_seconds < 60 && guest_fault_retries < 4) {
|
|
++guest_fault_retries;
|
|
relaunch_game = selected_game;
|
|
return_to_menu = true;
|
|
LOG_WARNING(Frontend, "EDEN_GUEST_FAULT_RETRY {} after {:.1f} s: {}", guest_fault_retries,
|
|
session_seconds, completion->guest_fault);
|
|
Eden::Report("guest fault", ("Restarting the game: " + completion->guest_fault).c_str());
|
|
} else
|
|
#endif
|
|
{
|
|
throw std::runtime_error("The game stopped: " + completion->guest_fault +
|
|
". Reopen it from the launcher.");
|
|
}
|
|
}
|
|
#ifdef EDEN_PS5_OPENGL
|
|
if (graphics_error) std::rethrow_exception(graphics_error);
|
|
#ifndef PS5_NATIVE
|
|
if (game) {
|
|
bool captured = false;
|
|
try {
|
|
captured = game_capture.wait_for(std::chrono::seconds(2)) ==
|
|
std::future_status::ready && game_capture.get();
|
|
} catch (const std::future_error& error) {
|
|
if (error.code() != std::make_error_code(std::future_errc::broken_promise)) throw;
|
|
}
|
|
if (!captured) throw std::runtime_error("No completed game frame capture");
|
|
}
|
|
#endif
|
|
#endif
|
|
if (game) LOG_INFO(Frontend, "EDEN_GAME_SESSION_END {}", cycle + 1);
|
|
}
|
|
}
|
|
passed("core_destroyed");
|
|
#ifdef EDEN_PS5_OPENGL
|
|
Eden::Performance::ReportSessionMemory("end", 0);
|
|
#endif
|
|
if (devices && pad) {
|
|
pad.reset();
|
|
LOG_INFO(Frontend, "EDEN_DEVICE_FRONTEND_PASS");
|
|
}
|
|
#ifdef PS5_NATIVE
|
|
if (return_to_menu) continue;
|
|
#endif
|
|
passed("HEADLESS_COMPLETE");
|
|
#ifdef EDEN_DEV_PROFILE
|
|
// Keep the sandbox mounted long enough to collect final driver counters.
|
|
std::fflush(stdout);
|
|
std::this_thread::sleep_for(std::chrono::seconds(20));
|
|
#endif
|
|
return 0;
|
|
#ifdef PS5_NATIVE
|
|
} catch (const std::exception& error) {
|
|
launch_error = error.what();
|
|
Eden::Report("session failed", error.what());
|
|
std::fflush(stderr);
|
|
std::fflush(stdout);
|
|
}
|
|
}
|
|
#endif
|
|
} catch (const std::exception& error) {
|
|
std::fflush(stdout);
|
|
Eden::Report("fatal startup failure", error.what());
|
|
return 1;
|
|
}
|
|
}
|