Files
PS5_RetroArch/src/main.cpp
T
Mihawk 979141e27a capture: the trigger works on the console, the driver's readback does not
Command: bash tools/build-title.sh && (stage /app0/args.txt:
"--ps5-capture=90 --ps5-capture-path=/app0/shot.png") && bash tools/run-title.sh
--no-build --watch 20
Evidence: /app0/trace.txt (klog/run-PPSA99169-031642.log) contains
"capture: frame 90 of 90, take_screenshot -> failed (/app0/shot.png)" - the port's own
options are read by the patched vulkan_frame, the budget is reached, take_screenshot is
called, and the trace says whether it worked. bash tools/verify.sh: PASS (format unit
build integration evidence).

gfx/drivers/vulkan.c (patches/series 0031): the frame counter and the screenshot call,
in vulkan_frame, because that is the one path every frame takes. A second hook in
video_driver_cached_frame (0030) was written and removed: it never ran on the console,
because a content-less run does not take that path either.
src/main.cpp: --ps5-* lines are the port's own and no longer reach RetroArch's option
parser.

The readback behind take_screenshot (vulkan_readback's synchronous path) fails and its
reason goes to /app0/retroarch.log, which never flushes. That, and the picture itself,
are the next step.
2026-09-19 03:21:08 -04:00

212 lines
9.6 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 <cstddef>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <exception>
#include <typeinfo>
#include <cxxabi.h>
#include "trace.hpp"
/* RetroArch's entry, in C. */
extern "C" int rarch_main(int argc, char *argv[], void *data);
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/retroarch.cfg";
/* 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);
char line[256];
std::snprintf(line, sizeof line, "terminate: exception type=%s",
(status == 0 && pretty) ? pretty : type->name());
if (pretty)
std::free(pretty);
ps5::debug::mark(line);
std::abort();
}
} // namespace
int main()
{
/* 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");
/* 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
{
/* Unbuffered, because everything that writes an error and then aborts -
* an assertion, a C++ terminate - would otherwise lose it: the console's
* libc buffers this stream and abort does not flush. The assert message is
* what a console run needs most and it was the one thing never printed. */
std::setvbuf(stderr, nullptr, _IONBF, 0);
std::fputs("stderr is the trace file (unbuffered)\n", stderr);
}
std::set_terminate(on_terminate);
/* 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/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. */
char arg_log[] = "--log-file=/app0/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/retroarch.cfg --verbose --log-file");
/* 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);
}
char *argv_with_extras[sizeof(argv) / sizeof(argv[0]) + max_extra_args];
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];
for (int i = 0; i < extra_count; i++)
argv_with_extras[base_count + i] = extra_storage[i];
argv_with_extras[base_count + extra_count] = nullptr;
const int status =
rarch_main(static_cast<int>(base_count + extra_count), argv_with_extras, nullptr);
/* If this line is on the console, the frontend ran and returned by itself. */
ps5::debug::mark_value("rarch_main returned", status);
return status;
}