Files
PS5_RetroArch/src/main.cpp
T
Mihawk 0bca2863e6 Gates: test stubs for frame generation; comment wrapping; TLS and password patches renumbered 0129/0130
- tests/input_ps5_test.cpp: ps5_fg_record_request stub (the pad script's
  RECORD calls PS5_FrameGen's recorder, not linked in the host test).
- tests/test_webui_qr.py: stubs for the frame generation entries 0120 binds
  beside the WebUI's (ps5_fg_menu_is_label/_is_folder, action_ok_ps5_fg,
  action_ok_push_default).
- src/main.cpp, src/ps5_framegen_menu.c: tools/lint-format.sh --write
  (comments re-wrapped to the line limit; no code changed).
- The frame generation code already names patches 0122-0128, which
  patches/series does not list; 47879d1's TLS and password patches took
  0122/0123 and are now 0129 (TLS verification) and 0130 (password).

lint-format PASS; tests test_input_ps5, test_webui_qr, test_frontend,
test_webui_secrets: OK.
2026-10-14 19:27:23 -04:00

726 lines
33 KiB
C++

/*
* PS5 RetroArch - the title's entry point.
*
* Copyright (C) 2026 Mihawk
* SPDX-License-Identifier: GPL-3.0-or-later
*
* RetroArch's own `main` is one line: `return rarch_main(argc, argv, NULL)`. So
* the entry point is not reconciled with RetroArch's, it simply calls the same
* function with the arguments this title wants, and the pipeline's `_start` stays
* the process entry. That is the whole of the "entry point" problem the plan
* listed.
*
* The display is opened by the video driver, not here: `video_ps5` owns it, and
* this file's job is to hand RetroArch its arguments and let its runloop drive.
*
* Arguments, and why each is here:
* -f fullscreen, which on a console is the only mode
* -c <path> the config to read and write, inside the title's own folder
* --verbose so the run is readable in the console's log
* --menu start with the menu up rather than waiting for content: this
* title has no content, and the menu is what is being proven
*
* Why this file writes a trace. The first run of this title on the console ended
* with the kernel reporting `eboot.bin calls exit() exit_value=0` and nothing
* else: no output, no crash, no message. A program that exits zero and says
* nothing is indistinguishable from one that never reached `main`, and the
* console's log cannot be asked which it was. So the entry point and the video
* driver append a line per step to /app0/trace.txt, and after a run that file
* says how far it got. See src/trace.hpp.
*/
#include <atomic>
#include <cstddef>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <ctime>
#include <exception>
#include <new>
#include <stdexcept>
#include <system_error>
#include <typeinfo>
#include <pthread.h>
#include <sys/stat.h>
#include <unistd.h>
#include <cxxabi.h>
#include <ps5platform/platform.h>
#include <ps5platform/probe.h>
#include "trace.hpp"
#include "memory_diagnostics.hpp"
#include "memory_status.h"
#include "../build/title_build_identity.h"
#include "title_threads.hpp"
#include "display_modes_ps5.h"
#include "relaunch_ps5.h"
#include "frontend_mode_ps5.h"
#include "ps5_library.h"
#include "webui_ps5.h"
#include "webui_update.h"
#include "webui_link.h"
/* RetroArch's entry, in C. */
extern "C" int rarch_main(int argc, char *argv[], void *data);
extern "C" int sceSystemServiceHideSplashScreen();
extern "C" void ps5_vulkan_profile_init();
extern "C" int sceKernelAvailableFlexibleMemorySize(std::size_t *size);
extern "C" void ps5_crash_report_install();
extern "C" void ps5_core_threads_start();
extern "C" void ps5_sampler_start();
extern "C" void ps5_open_permissions();
extern "C" void ps5_permissions_settle();
/* ../PS5_Vulkan's driver/ps5vk_debug.h: whether VideoOut outlives a swapchain.
* Weak, so a build without the driver links. */
extern "C" void ps5vk_display_retain(bool retain) __attribute__((weak));
extern "C" void ps5_audio_test_if_requested();
extern "C" void ps5_core_loader_test_if_requested();
extern "C" void ps5_thread_test_if_requested();
extern "C" const char *ps5_frontend_build_identity()
{
return PS5_RETROARCH_BUILD_ID;
}
namespace
{
/* The title's own folder, as the console mounts it: the application image is at
* /app0 and this is where a title may keep its configuration. */
constexpr const char *config_path = "/app0/config/retroarch.cfg";
/* A profiled test run (the driver's /app0/ps5vk-profile.txt survived the stale
* test file sweep) also reports the flexible memory left every ten seconds,
* and what the platform layer holds: the cores' executable code, their
* direct-memory arenas and the ranges reserved for them. With the driver's
* direct_live, a soak's leak is a line that only moves one way. Set once main
* has swept. */
std::atomic<bool> g_memory_report{false};
/* Writes every buffered output stream out -- the trace file and RetroArch's
* log among them -- four times a second, off the threads that log. */
void *log_flusher(void *)
{
const timespec interval = {0, 250 * 1000 * 1000};
for (unsigned tick = 1;; ++tick)
{
nanosleep(&interval, nullptr);
if (tick % 40 == 0 && g_memory_report.load(std::memory_order_relaxed))
{
ps5_memory_report("periodic", 0, 0);
std::size_t flexible = 0;
sceKernelAvailableFlexibleMemorySize(&flexible);
std::fprintf(stderr, "memory: flexible_free=%zu KiB\n", flexible >> 10);
char platform[192];
ps5_platform_report(platform, sizeof(platform));
std::fprintf(stderr, "memory: %s\n", platform);
}
std::fflush(nullptr);
}
return nullptr;
}
void start_log_flusher()
{
pthread_t thread;
if (create_title_thread(&thread, log_flusher, nullptr) == 0)
pthread_detach(thread);
}
/* The platform probe (the SDK fork's platform/src/probe.c):
* /app0/platform-probe.txt, a test file of one launch like the others, runs the
* console capability probe in this process -- the one whose flexible budget,
* imports, GPU window and core loader the cores live with -- and ends the
* launch. Its words select the optional tests: "large" (4 GiB of guest memory
* beside 1 GiB of code), "huge" (10 GiB mapped and written at once), "full"
* (the largest direct allocation mapped and written), "jit" (the
* shared-memory JIT interface, last, since a refused import would stop the
* process), "files" (the file probe: 256 MiB written and read back in the
* title's own folder at three chunk sizes, the file removed after each),
* "writes" (3 GiB written in one file, with O_DIRECT, buffered with an fsync
* every 256 MiB and buffered alone, each timed per 256 MiB and stopped after
* 240 s), "routes" (3 GiB written in /app0 with write(), through a shared
* mapping and with write() again, stopped after 45, 45 and 15 s: whether the
* mapped route is held back after write() has spent its burst; the folder's
* own path, /data/homebrew/PPSA99169, is not in a title's sandbox),
* "offload" (3 GiB of a file of /app0 appended through an FTP server on the
* loopback, which is another process, and read back; "offload=<port>" names
* the server's port instead of scanning for it, and "server=<dir>" the folder
* as the server sees it when statfs() cannot) and
* "threads" (the
* stacks the main thread, a thread created with no attributes and one asking
* for 2 MiB run on) and "topology" (the round trip of a cache line between
* every pair of the title's CPUs, which shows their cores and clusters). Every
* line goes to the trace as it is measured. */
static void probe_line(void *, const char *line)
{
ps5::debug::mark(line);
}
static bool run_platform_probe()
{
std::FILE *const file = std::fopen("/app0/platform-probe.txt", "r");
if (!file)
return false;
char words[128] = {};
const size_t read = std::fread(words, 1, sizeof(words) - 1, file);
words[read] = '\0';
std::fclose(file);
std::remove("/app0/platform-probe.txt");
unsigned flags = 0;
if (std::strstr(words, "large"))
flags |= PS5_PROBE_LARGE;
if (std::strstr(words, "huge"))
flags |= PS5_PROBE_HUGE;
if (std::strstr(words, "full"))
flags |= PS5_PROBE_FULL;
if (std::strstr(words, "jit"))
flags |= PS5_PROBE_JIT_API;
int failures = ps5_platform_probe(probe_line, nullptr, flags);
if (std::strstr(words, "files"))
failures += ps5_platform_probe_files(probe_line, nullptr, "/app0");
if (std::strstr(words, "writes"))
failures += ps5_platform_probe_writes(probe_line, nullptr, "/app0", 3072, 240);
if (std::strstr(words, "routes"))
{
static const char *const directories[] = {"/app0"};
failures += ps5_platform_probe_write_routes(probe_line, nullptr, directories, 1, 3072, 45);
}
if (const char *offload = std::strstr(words, "offload"))
{
unsigned port = 0;
char server[256] = {};
std::sscanf(offload, "offload=%u", &port);
if (const char *named = std::strstr(words, "server="))
std::sscanf(named, "server=%255s", server);
failures += ps5_platform_probe_ftp_offload(probe_line, nullptr, "/app0",
server[0] ? server : nullptr, port, 3072, 120);
}
if (std::strstr(words, "threads"))
failures += ps5_platform_probe_threads(probe_line, nullptr);
if (std::strstr(words, "topology"))
failures += ps5_platform_probe_topology(probe_line, nullptr);
ps5::debug::mark_value("platform probe: failures", failures);
std::fflush(stderr);
return true;
}
/* Profile 10's entry state (docs/PHASE_LOG.md): /app0/state-copy.txt names a
* source and a destination, a line each, and a test run copies the one to the
* other before RetroArch starts. The save-state stress keeps its states in a
* directory of its own, so my entry state has to be there too, and the FTP
* account may not read mine. The copy only reads my files: it never replaces a
* file and never writes under /app0/savestates or /app0/savefiles. Testing
* only: the file is never shipped. */
void copy_test_file()
{
std::FILE *const list = std::fopen("/app0/state-copy.txt", "r");
if (list == nullptr)
return;
char source[512] = {};
char destination[512] = {};
const bool named = std::fgets(source, sizeof(source), list) != nullptr &&
std::fgets(destination, sizeof(destination), list) != nullptr;
std::fclose(list);
source[std::strcspn(source, "\r\n")] = '\0';
destination[std::strcspn(destination, "\r\n")] = '\0';
const bool mine = std::strncmp(destination, "/app0/savestates", 16) == 0 ||
std::strncmp(destination, "/app0/savefiles", 15) == 0;
bool exists = false;
if (std::FILE *const existing = std::fopen(destination, "rb"))
{
exists = true;
std::fclose(existing);
}
long long copied = -1;
std::FILE *const in = named && !mine && !exists ? std::fopen(source, "rb") : nullptr;
std::FILE *const out = in != nullptr ? std::fopen(destination, "wb") : nullptr;
constexpr std::size_t chunk = std::size_t{1} << 20;
char *const buffer = out != nullptr ? static_cast<char *>(std::malloc(chunk)) : nullptr;
if (buffer != nullptr)
{
copied = 0;
std::size_t got = 0;
while (copied >= 0 && (got = std::fread(buffer, 1, chunk, in)) > 0)
{
const bool written = std::fwrite(buffer, 1, got, out) == got;
copied = written ? copied + static_cast<long long>(got) : -1;
}
std::free(buffer);
}
if (out != nullptr && std::fclose(out) != 0)
copied = -1;
if (in != nullptr)
std::fclose(in);
ps5::debug::mark_value("test state copy bytes", copied);
}
/* Why a terminate handler exists.
*
* The driver's shader compiler is C++ and aborts the process from deep inside
* itself: the console reports `reason: abort is called(system)` with a backtrace
* through aco::visit_alu_instr, and nothing says what went wrong. The two abort
* sites in that function are clang's throw helpers for std::vector's length error
* and for a bad array length (`call __throw_length_error` followed by `ud2`), so
* something threw and the exception never came back as an error.
*
* An abort is also the one failure with no message anywhere: the title's trace
* says how far it got, and the frontend's log is a buffer that dies with the
* process. libc++abi still knows the exception while terminate runs, so the type
* and its what() are written to the trace here - the difference between "the
* compiler refused this shader" and "a wild size reached a container", which are
* not the same bug and were not distinguishable from the outside. */
void on_terminate()
{
const std::type_info *type = abi::__cxa_current_exception_type();
if (!type)
{
ps5::debug::mark("terminate: called with no active exception");
std::abort();
}
/* src/ is compiled without exceptions, so the exception cannot be rethrown
* here to read what(); libc++abi still reports its type, and the demangled
* name is what separates the two throw sites that were found in the shader
* compiler - std::length_error (a container asked for an impossible size)
* from std::bad_array_new_length (an array length that cannot be valid). */
int status = 0;
char *pretty = abi::__cxa_demangle(type->name(), nullptr, nullptr, &status);
/* The thrown object itself is readable without rethrowing: for the
* standard exception types a message and, for std::system_error, the
* error code say which call failed (a thread that could not be created, a
* mutex that could not be locked) rather than only that one did. */
const char *what = "";
int code = 0;
void *object = abi::__cxa_current_primary_exception();
if (object)
{
/* Without RTTI the type is matched by its mangled name. */
const char *name = type->name();
const auto is = [name](const char *mangled) { return std::strcmp(name, mangled) == 0; };
if (is("NSt3__112system_errorE"))
{
const auto *error = static_cast<const std::system_error *>(object);
what = error->what();
code = error->code().value();
}
else if (is("NSt3__112length_errorE") || is("NSt3__113runtime_errorE") ||
is("NSt3__111logic_errorE") || is("NSt3__112out_of_rangeE") ||
is("St9bad_alloc") || is("St20bad_array_new_length"))
{
what = static_cast<const std::exception *>(object)->what();
}
}
char line[512];
std::snprintf(line, sizeof line, "terminate: exception type=%s what=\"%s\" code=%d",
(status == 0 && pretty) ? pretty : type->name(), what, code);
if (pretty)
std::free(pretty);
ps5::debug::mark(line);
std::fflush(stderr);
std::abort();
}
} // namespace
#include "ps5_paths.h"
int main(int process_argc, char **process_argv)
{
if (ps5_paths_start() != 0)
return 1;
/* A session's trace is its own (src/frontend_mode_ps5.cpp): a launch from the home
* screen keeps the last session's as trace.1.txt before this one writes a line. */
ps5_frontend_session_logs(process_argc, process_argv);
/* First thing: prove that control reached this function at all, before
* anything that could fail. */
{
/* A zero-initialised static: it lives in the BSS, which this port's own
* CRT now clears before main. Printed so a run says whether that happened -
* the alternative was to read it from the far side of the frontend, where
* the same question cost a round. */
static long ps5_bss_check;
static long ps5_data_check = 7;
char line[128];
std::snprintf(line, sizeof line, "bss check=%ld (must be 0), data check=%ld (must be 7)",
ps5_bss_check, ps5_data_check);
ps5::debug::mark(line);
}
ps5::debug::mark("main() entered; static constructors have already run");
/* What an earlier build made is made reachable over FTP again before the
* frontend starts (src/permissions_ps5.cpp); what this run creates is,
* through the link-time wrappers. */
ps5_open_permissions();
/* Everything the libraries say goes to stderr, and a title's stderr reaches
* nothing on this console: not the kernel log, not RetroArch's log file, not
* FTP. That is why the driver's own refusals - the ones ../PS5_Vulkan states
* by name before returning VK_ERROR_UNKNOWN - could not be read, and why a
* console round could only report the error code. Pointing the stream at the
* trace file makes those messages part of the same record as the marks, which
* is also how an assertion's message stops being lost: __assert prints the
* expression it failed and then aborts. */
if (!std::freopen("/app0/trace.txt", "a", stderr))
ps5::debug::mark("could not send stderr to the trace file");
else
{
/* Buffered, and written out by a flusher thread four times a second. It
* was unbuffered so that an assertion or a terminate, which abort without
* flushing, kept their message; but every line was then its own write to
* the console's storage, which takes milliseconds, on whatever thread
* logged it -- often the one running frames. Loading a God of War save
* state stalled the picture for hundreds of milliseconds in fprintf alone
* (2026-09-24, the sampler). The crash and terminate handlers flush
* first, so a failing run still leaves its last lines. */
static char stderr_buffer[64 * 1024];
std::setvbuf(stderr, stderr_buffer, _IOFBF, sizeof(stderr_buffer));
std::fputs("stderr is the trace file (buffered, flushed every 250 ms)\n", stderr);
start_log_flusher();
}
std::set_terminate(on_terminate);
ps5::debug::mark(PS5_RETROARCH_BUILD_ID);
/* The WebUI's daemon, linked to (or started) first, so it already serves while
* this launch only hands the title to a frontend (src/webui_link.h). */
ps5_webui_link_start("title", nullptr, [](const char *line) { ps5::debug::mark(line); });
ps5::memory::init("/app0/memory-diagnostics.log", PS5_RETROARCH_BUILD_ID);
ps5_vulkan_profile_init();
/* The shell's splash covers the title until it explicitly dismisses it.
* video_ps5 does this while opening its display, but video_vulkan never
* enters that code. This is title startup work for either video driver. */
ps5::debug::mark_value("startup: sceSystemServiceHideSplashScreen",
sceSystemServiceHideSplashScreen());
/* The frontend handover restarts the title (src/relaunch_ps5.cpp): an armed
* test restarts it here, before anything that a restart would have to undo,
* and its last generation continues into RetroArch. */
ps5_relaunch_test_if_requested(process_argc, process_argv);
/* An armed display modes test (src/display_modes_ps5.cpp) presents every size the
* driver's display offers, on a device of its own, before RetroArch makes one. */
ps5_display_modes_test_if_requested();
/* Which frontend this launch is for (src/frontend_mode_ps5.cpp): from the home
* screen the picker, which restarts the title as RetroArch or EmulationStation;
* those two are their own executables, started through LoadExec. */
/* A folder in content for each system the installed cores run, before any frontend:
* a launch from the home screen goes to the picker from here (issue 33). */
const int system_folders =
ps5_library_make_system_folders("/app0/content", "/app0/info", "/app0/cores");
if (system_folders > 0)
ps5::debug::mark_value("startup: system folders made in content", system_folders);
ps5_frontend_dispatch(process_argc, process_argv);
ps5_webui_link_frontend("retroarch");
/* argv must be writable and NULL-terminated: RetroArch's option parsing
* walks it the way the C runtime would have. */
char arg0[] = "retroarch";
char arg_fullscreen[] = "-f";
char arg_config[] = "-c";
char arg_config_path[] = "/app0/config/retroarch.cfg";
char arg_verbose[] = "--verbose";
char arg_menu[] = "--menu";
/* RetroArch's own log, on the console, from the first line of main.
*
* This matters more than the config's log settings: `--log-file` sets the
* override and enables file logging *before* the config is parsed, so it
* records what happens during startup - which is exactly where the Vulkan
* path dies silently. A failure that says nothing is the one thing a console
* run cannot diagnose, and this is how the frontend is made to speak. */
/* Named by mode, its previous copy kept: a game's log (game mode) does not
* overwrite RetroArch's own, nor the next game's the last one's. */
char arg_log[96];
std::snprintf(arg_log, sizeof(arg_log), "--log-file=%s", ps5_frontend_retroarch_log());
char *argv[] = {
arg0, arg_fullscreen, arg_config, arg_config_path, arg_verbose, arg_log, arg_menu, nullptr,
};
(void)config_path;
ps5::debug::mark("argv built: retroarch -f -c /app0/config/retroarch.cfg --verbose --log-file");
/* Test files belong to one launch. A test run writes /app0/test-run.txt just
* before it starts the title, and the launch consumes it; a launch without
* it is someone playing, so any test file an interrupted run left behind is
* deleted before anything reads it. Otherwise a stale args.txt started every
* later launch straight into a test game that quit itself, and a stale flag
* kept the driver's logging or the sampler running (2026-09-24). */
if (std::remove("/app0/test-run.txt") != 0)
{
static const char *const test_files[] = {
"/app0/args.txt",
"/app0/framegen-dump.txt",
"/app0/pad-script.txt",
"/app0/pad-monitor.txt",
"/app0/ps5vk-no-retain.txt",
"/app0/ppsspp-options.txt",
"/app0/ps5vk-ab.txt",
"/app0/ps5-sampler.txt",
"/app0/ps5vk-profile.txt",
"/app0/ps5vk-log.txt",
"/app0/ps5vk-vblank-probe.txt",
"/app0/ps5vk-spirv-dump.txt",
"/app0/ps5vk-shader-cache-dir.txt",
"/app0/dolphin-options.txt",
"/app0/dolphin-debug.txt",
"/app0/state-copy.txt",
"/app0/platform-probe.txt",
};
unsigned removed = 0;
for (const char *const path : test_files)
removed += std::remove(path) == 0 ? 1u : 0u;
if (removed != 0)
ps5::debug::mark_value("stale test files removed", static_cast<int>(removed));
}
copy_test_file();
std::remove("/app0/state-copy.txt");
if (run_platform_probe())
return 0;
/* Testing: /app0/radv-threaded.txt turns on RADV's threaded recording
* (the Vulkan driver's worker records the frontend's and cores' commands),
* set before RetroArch makes its Vulkan instance. */
if (std::FILE *threaded = std::fopen("/app0/radv-threaded.txt", "rb"))
{
std::fclose(threaded);
setenv("RADV_THREADED_RECORDING", "1", 1);
ps5::debug::mark("radv: threaded recording on");
}
/* Testing: /app0/framegen-record-width.txt holds the width the present-stream recorder (pad
* script RECORD) writes its pictures at, 640 when absent; a wider one shows what the small
* pictures hide (a recording is width x height x 4 bytes a picture, and the pad script's count
* of them, in memory). */
if (std::FILE *width = std::fopen("/app0/framegen-record-width.txt", "r"))
{
char value[16] = "";
if (std::fgets(value, sizeof(value), width) && value[0] >= '0' && value[0] <= '9')
setenv("PS5_FRAMEGEN_RECORD_WIDTH", value, 1);
std::fclose(width);
}
/* Testing: /app0/framegen-dump.txt (patch 0119) names a folder, and a second line
* the number of content frames to let pass first, a third how many to write: the frame
* generator then writes its first 60 content frames and the frames it made there, raw RGBA8,
* for scoring. */
if (std::FILE *dump = std::fopen("/app0/framegen-dump.txt", "r"))
{
char folder[200] = "", skip[16] = "0", count[16] = "60";
if (std::fgets(folder, sizeof(folder), dump))
{
if (std::fgets(skip, sizeof(skip), dump) == nullptr)
std::strcpy(skip, "0");
else if (std::fgets(count, sizeof(count), dump) == nullptr)
std::strcpy(count, "60");
folder[std::strcspn(folder, "\r\n")] = '\0';
if (folder[0] != '\0')
{
mkdir(folder, 0777);
setenv("PS5_FRAMEGEN_DUMP", folder, 1);
setenv("PS5_FRAMEGEN_DUMP_SKIP", skip, 1);
setenv("PS5_FRAMEGEN_DUMP_COUNT", count, 1);
ps5::debug::mark("framegen: dumping frames");
}
}
std::fclose(dump);
}
if (std::FILE *profile = std::fopen("/app0/ps5vk-profile.txt", "rb"))
{
std::fclose(profile);
g_memory_report.store(true, std::memory_order_relaxed);
}
/* Extra arguments, one per line, from /app0/args.txt when that file is there.
*
* Why a file rather than the launch arguments: the console starts this title
* from its own launcher, which passes none, and the frontend's most useful
* unattended options are exactly the ones a run needs to change - RetroArch
* already knows how to take a screenshot at the end of a fixed number of frames
* (`--max-frames=N --max-frames-ss --max-frames-ss-path=FILE`), which is the
* only way this port can show what the GPU produced without a camera at the
* screen. The file is optional, empty lines and `#` comments are skipped, and
* the storage is static because RetroArch's option parsing keeps pointers into
* it for the whole run. */
constexpr int max_extra_args = 16;
constexpr std::size_t max_extra_arg_len = 256;
static char extra_storage[max_extra_args][max_extra_arg_len];
int extra_count = 0;
if (std::FILE *extra = std::fopen("/app0/args.txt", "r"))
{
while (extra_count < max_extra_args &&
std::fgets(extra_storage[extra_count], max_extra_arg_len, extra))
{
char *line = extra_storage[extra_count];
std::size_t len = std::strlen(line);
while (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r'))
line[--len] = '\0';
if (len == 0 || line[0] == '#')
continue;
/* The port's own options are read from this file by the frontend code that
* uses them (patch 0031 reads `--ps5-capture=`), and RetroArch must not see
* them: an option it does not know is an option it complains about. */
if (std::strncmp(line, "--ps5-", 6) == 0)
continue;
extra_count++;
}
std::fclose(extra);
ps5::debug::mark_value("argv extras from /app0/args.txt", extra_count);
}
/* Game mode (src/frontend_mode_ps5.cpp, src/ps5_game.h): the game a frontend asked
* for, started directly with its core. Close Content then quits RetroArch, since the
* content came from the command line (game-mode.cfg), and the title goes back to the
* frontend. */
const struct ps5_game *game = ps5_frontend_game();
static char game_core_flag[] = "-L";
static const char game_config[] = "/app0/config/game-mode.cfg";
if (game)
{
if (std::FILE *config = std::fopen(game_config, "w"))
{
std::fputs("# Written by eboot.bin for each game a frontend starts (src/main.cpp)\n"
"quit_on_close_content = \"2\"\n",
config);
std::fclose(config);
}
else
ps5::debug::mark("game mode: could not write /app0/config/game-mode.cfg");
}
char *argv_with_extras[sizeof(argv) / sizeof(argv[0]) + max_extra_args + 5];
std::size_t base_count = sizeof(argv) / sizeof(argv[0]) - 1;
for (std::size_t i = 0; i < base_count; i++)
argv_with_extras[i] = argv[i];
/* --menu starts in the menu and RetroArch refuses it beside a content path
* ("--menu was used, but content file was passed as well"), so it is left out
* when the extras name a core or content: `-L core` and a path launch a game
* directly, which is how a run measures a core without a person at the pad. */
bool extras_launch_content = game != nullptr;
for (int i = 0; i < extra_count; i++)
if (std::strcmp(extra_storage[i], "-L") == 0 || extra_storage[i][0] != '-')
extras_launch_content = true;
if (extras_launch_content && base_count > 0 && argv[base_count - 1] == arg_menu)
base_count--;
for (int i = 0; i < extra_count; i++)
argv_with_extras[base_count + i] = extra_storage[i];
const int file_extras = extra_count; /* the ones from /app0/args.txt */
if (game)
{
argv_with_extras[base_count + extra_count++] = game_core_flag;
argv_with_extras[base_count + extra_count++] = const_cast<char *>(game->core);
argv_with_extras[base_count + extra_count++] = const_cast<char *>(game->content);
}
// Kept separate so RetroArch's normal config save cannot erase browser edits.
static char webui_append[] = "--appendconfig";
static char webui_config[] = "/app0/config/webui.cfg";
static char framegen_config[] = "/app0/config/framegen.cfg"; /* patches/series, 0120 */
static char combined_configs[max_extra_arg_len + sizeof(webui_config) + sizeof(game_config) +
sizeof(framegen_config) + 3];
bool saved_webui = false;
if (std::FILE *webui = std::fopen(webui_config, "rb"))
{
std::fclose(webui);
saved_webui = true;
ps5::debug::mark("webui: applying saved settings at startup");
}
bool saved_framegen = false;
if (std::FILE *framegen = std::fopen(framegen_config, "rb"))
{
std::fclose(framegen);
saved_framegen = true;
ps5::debug::mark("framegen: applying saved options at startup");
}
if (saved_webui || saved_framegen || game)
{
// RetroArch uses only the last --appendconfig, with | separating files.
// Explicit launch overrides still win over the browser's saved defaults.
const char *explicit_config = nullptr;
for (int i = 0; i < file_extras; i++)
{
if (std::strcmp(extra_storage[i], "--appendconfig") == 0 && i + 1 < file_extras)
explicit_config = extra_storage[++i];
else if (std::strncmp(extra_storage[i], "--appendconfig=", 15) == 0)
explicit_config = extra_storage[i] + 15;
}
// The browser's defaults, then the frame generation options, then the game's, then
// the launch's own: a later file wins.
const char *const parts[] = {saved_webui ? webui_config : nullptr,
saved_framegen ? framegen_config : nullptr,
game ? game_config : nullptr, explicit_config};
std::size_t used = 0;
combined_configs[0] = '\0';
for (const char *part : parts)
{
if (!part)
continue;
used += std::snprintf(combined_configs + used, sizeof(combined_configs) - used, "%s%s",
used ? "|" : "", part);
}
argv_with_extras[base_count + extra_count++] = webui_append;
argv_with_extras[base_count + extra_count++] = combined_configs;
}
argv_with_extras[base_count + extra_count] = nullptr;
ps5_crash_report_install();
ps5_memory_report("startup", 0, 0);
ps5_core_threads_start();
ps5_sampler_start();
ps5_audio_test_if_requested();
ps5_core_loader_test_if_requested();
ps5_thread_test_if_requested();
/* RetroArch tears its whole Vulkan context down and builds it again when it
* loads or closes content and when a video setting needs a reinit. Retained,
* the driver keeps VideoOut, its mode and the last frame on screen across
* that, where it would otherwise blank the panel and switch the output back
* to 60 Hz and up again: about half a second of black each time.
* /app0/ps5vk-no-retain.txt turns it off, for a comparison run. */
if (std::FILE *no_retain = std::fopen("/app0/ps5vk-no-retain.txt", "rb"))
{
std::fclose(no_retain);
ps5::debug::mark("display: VideoOut released with each swapchain (ps5vk-no-retain.txt)");
}
else if (ps5vk_display_retain != nullptr)
ps5vk_display_retain(true);
/* With the daemon serving the WebUI, it stays up as RetroArch quits and the
* title changes frontend; with none (no ELF loader), this process serves it
* while RetroArch runs, as before the daemon. */
ps5_webui_prepare("/app0");
if (!ps5_webui_link_wait(8000))
ps5_webui_start("/app0");
const int status =
rarch_main(static_cast<int>(base_count + extra_count), argv_with_extras, nullptr);
ps5_webui_stop();
/* If this line is on the console, the frontend ran and returned by itself. */
ps5::debug::mark_value("rarch_main returned", status);
if (ps5vk_display_retain != nullptr)
ps5vk_display_retain(false);
ps5_update::install();
ps5::memory::finish();
return status;
}
/* RetroArch has already closed its drivers and written configuration here.
* Native title shutdown goes through the shell; kernel exit(0) raises SIGSYS
* for this application instead of returning cleanly to the home screen. */
extern "C" int sceSystemServiceLoadExec(const char *, const char *const *);
extern "C" void catchReturnFromMain(int status)
{
ps5::debug::mark_value("native quit: frontend status", status);
/* The output leaves in its default mode (a second release is a no-op). */
if (ps5vk_display_retain != nullptr)
ps5vk_display_retain(false);
/* A game mode game goes back to its frontend, and RetroArch the picker started to
* the picker (src/frontend_mode_ps5.cpp). */
ps5_frontend_after_retroarch(
status, ps5_update::exit_requested() || ps5_webui_link_install_requested() ? 1 : 0);
ps5_permissions_settle();
std::fflush(nullptr);
const int result = sceSystemServiceLoadExec("exit", nullptr);
ps5::debug::mark_value("native quit: system service result", result);
if (result >= 0)
for (;;)
usleep(100000); /* Shell termination is asynchronous. */
}