Linux diagnostics updates

This commit is contained in:
vetusmagnus
2026-10-14 12:49:56 -04:00
parent 0cdad42578
commit 5fba038313
10 changed files with 211 additions and 14 deletions
+1 -1
View File
@@ -6,4 +6,4 @@ games/*/zig-cache/
games/*/build_log.txt
__pycache__/
*.pyc
release/rac1/
release/rac1/
+7 -1
View File
@@ -48,6 +48,8 @@ Resolution (Native, or Upscaled to a whole multiple of the PS2's 448 lines that
### Reporting problems
- **Open log** shows `rac1_log.txt` (or the build's `build_log.txt` when a build failed).
- On Linux, `project-solana report` writes one file with everything a report needs (system details, the 32-bit library check, the launcher, build and game logs, and the previous run's game log); attach that file.
- A crash writes the function it happened in and the call chain into the game log, and the previous run's log is kept as `rac1_log.prev.txt`.
- **F9** in the game records the next few seconds of graphics input to `gsrec_NN.bin` (about 260 MB per second of capture); attach it to a report together with what looked wrong.
## Repository layout
@@ -86,4 +88,8 @@ gh release create rac1 dist/rac1/rac1.json dist/rac1/rac1-build.zip dist/rac1/to
To update an existing game release, upload the new files over the old ones (`gh release upload rac1 dist/rac1/* --clobber`). Players get them with **Rebuild the game**.
The launchers go in a release of their own (for example `launcher-v1`): `Project Solana.exe` and `project-solana` (Linux).
The launchers go in a release of their own (for example `launcher-v1`): `Project Solana.exe` and `project-solana` (Linux).
## Licence
TODO (maintainers): choose a licence for this repository. Third-party code keeps its own (see `THIRD_PARTY_NOTICES.md`).
+1 -1
View File
@@ -241,7 +241,7 @@ if a.platform == "linux":
else:
# the menu and runtime as a DLL at 0x40000000, plus the small launcher that owns PS2 RAM
dll = os.path.join(out, "rac1_game.dll")
link = zig + ["cc", "-shared", "-target", target, "-o", dll, objs_rsp("link")] + ["-Wl,--image-base=0x40000000", "-Wl,--no-dynamicbase", "-lshell32", "-lwinmm", "-lgdi32", "-luser32"]
link = zig + ["cc", "-shared", "-target", target, "-o", dll, objs_rsp("link")] + ["-Wl,--image-base=0x40000000", "-Wl,--no-dynamicbase", "-lshell32", "-lwinmm", "-lgdi32", "-luser32", "-ladvapi32"]
r = subprocess.run(link, capture_output=True, text=True)
if r.returncode:
print(r.stderr[-6000:]); sys.exit("link failed (dll)")
+1
View File
@@ -23,6 +23,7 @@ extern const int uya_tramp_count;
/* platform layer */
int plat_map_fixed(uint32_t addr, uint32_t size, int exec); /* 0 on success */
int plat_install_trap_handler(void); /* calls into unmapped PS2 code */
void plat_log_system(void); /* the system, for bug reports (once, at start) */
void plat_flush_icache(void *p, size_t n);
const char *plat_exe_dir(void); /* folder of the running program */
void plat_at_exit(void);
+1
View File
@@ -63,6 +63,7 @@ int uya_main(int argc, char **argv)
uya_log_open();
uya_log("Project Solana: starting");
plat_install_trap_handler(); /* first: a crash from here on is logged */
plat_log_system();
{ /* renderer=gl|soft in the cfg (UYA_RENDERER overrides it) */
extern const char *uya_renderer_cfg;
const char *r = cfg_value("renderer");
+22
View File
@@ -180,8 +180,30 @@ int plat_install_trap_handler(void)
sigaction(SIGBUS, &sa, 0);
sigaction(SIGILL, &sa, 0);
sigaction(SIGFPE, &sa, 0);
sigaction(SIGABRT, &sa, 0); /* (a library giving up: assert, abort) */
return 0;
}
#include <sys/utsname.h>
void plat_log_system(void)
{
struct utsname u;
char os[160] = "unknown", line[256], cpu[160] = "?";
FILE *f = fopen("/etc/os-release", "r");
while (f && fgets(line, sizeof line, f))
if (!strncmp(line, "PRETTY_NAME=", 12)) { char *v = line + 12; if (*v == '"') v++; v[strcspn(v, "\"\n")] = 0; snprintf(os, sizeof os, "%s", v); }
if (f) fclose(f);
f = fopen("/proc/cpuinfo", "r");
while (f && fgets(line, sizeof line, f))
if (!strncmp(line, "model name", 10)) { char *v = strchr(line, ':'); if (v) { v += 2; v[strcspn(v, "\n")] = 0; snprintf(cpu, sizeof cpu, "%s", v); break; } }
if (f) fclose(f);
if (uname(&u) == 0) uya_log("system: Linux %s (%s), %s", u.release, u.machine, os);
uya_log("system: %s, %ld cores, %ld MB RAM", cpu, sysconf(_SC_NPROCESSORS_ONLN), (long)((long long)sysconf(_SC_PHYS_PAGES) * sysconf(_SC_PAGESIZE) >> 20));
const char *st = getenv("XDG_SESSION_TYPE"), *de = getenv("XDG_CURRENT_DESKTOP");
uya_log("system: session %s%s%s, DISPLAY %s, WAYLAND_DISPLAY %s%s", st ? st : "?", de ? ", desktop " : "", de ? de : "",
getenv("DISPLAY") ? getenv("DISPLAY") : "-", getenv("WAYLAND_DISPLAY") ? getenv("WAYLAND_DISPLAY") : "-",
getenv("SteamDeck") && !strcmp(getenv("SteamDeck"), "1") ? ", Steam Deck" : getenv("SteamAppId") ? ", started by Steam" : "");
}
#endif
#ifdef __linux__
+10 -1
View File
@@ -83,6 +83,9 @@ static struct {
int (*QueueAudio)(SDL_AudioDeviceID, const void *, uint32_t);
uint32_t (*GetQueuedAudioSize)(SDL_AudioDeviceID);
void (*ClearQueuedAudio)(SDL_AudioDeviceID);
void (*GetVersion)(uint8_t v[3]);
const char *(*GetCurrentVideoDriver)(void);
const char *(*GetCurrentAudioDriver)(void);
} sdl;
/* 1: SDL is there and a display is (the game draws in a window); 0: headless */
@@ -105,7 +108,7 @@ static int sdl_on(void)
"SDL_NumJoysticks", "SDL_IsGameController", "SDL_GameControllerOpen", "SDL_GameControllerClose",
"SDL_GameControllerGetAttached", "SDL_GameControllerName", "SDL_GameControllerGetButton", "SDL_GameControllerGetAxis",
"SDL_OpenAudioDevice", "SDL_CloseAudioDevice", "SDL_PauseAudioDevice", "SDL_QueueAudio", "SDL_GetQueuedAudioSize",
"SDL_ClearQueuedAudio",
"SDL_ClearQueuedAudio", "SDL_GetVersion", "SDL_GetCurrentVideoDriver", "SDL_GetCurrentAudioDriver",
};
void **fp = (void **)&sdl;
for (unsigned i = 0; i < sizeof names / sizeof names[0]; i++)
@@ -116,6 +119,12 @@ static int sdl_on(void)
uya_log("SDL2: cannot start (%s)", sdl.GetError());
return 0;
}
{
uint8_t v[3] = {0, 0, 0};
const char *vd = sdl.GetCurrentVideoDriver(), *ad = sdl.GetCurrentAudioDriver();
sdl.GetVersion(v);
uya_log("SDL %d.%d.%d: video %s, audio %s", v[0], v[1], v[2], vd ? vd : "none", ad ? ad : "none");
}
sdl_state = 1;
return 1;
}
+26
View File
@@ -180,11 +180,37 @@ static LONG CALLBACK on_exception(EXCEPTION_POINTERS *ep)
return EXCEPTION_CONTINUE_SEARCH;
}
#include <signal.h>
/* abort() (a library giving up) ends the process without an exception: raise one, so the
* crash handler reports it with the call chain */
static void on_abort(int sig)
{
(void)sig;
uya_log("crash: abort() was called");
RaiseException(0xE0000ABE, EXCEPTION_NONCONTINUABLE, 0, 0);
}
int plat_install_trap_handler(void)
{
signal(SIGABRT, on_abort);
return AddVectoredExceptionHandler(1, on_exception) ? 0 : -1;
}
void plat_log_system(void)
{
typedef struct { ULONG size, major, minor, build, platform; WCHAR sp[128]; } osv_t;
HMODULE nt = GetModuleHandleA("ntdll.dll");
LONG (WINAPI *rtl_ver)(osv_t *) = nt ? (void *)GetProcAddress(nt, "RtlGetVersion") : 0;
const char *(*wine_ver)(void) = nt ? (void *)GetProcAddress(nt, "wine_get_version") : 0;
osv_t v; memset(&v, 0, sizeof v); v.size = sizeof v;
if (rtl_ver) rtl_ver(&v);
SYSTEM_INFO si; GetNativeSystemInfo(&si);
MEMORYSTATUSEX ms; memset(&ms, 0, sizeof ms); ms.dwLength = sizeof ms; GlobalMemoryStatusEx(&ms);
char cpu[128] = "?"; DWORD cl = sizeof cpu;
RegGetValueA(HKEY_LOCAL_MACHINE, "HARDWARE\\DESCRIPTION\\System\\CentralProcessor\\0", "ProcessorNameString", RRF_RT_REG_SZ, 0, cpu, &cl);
uya_log("system: Windows %lu.%lu build %lu%s%s, %s, %lu cores, %llu MB RAM", v.major, v.minor, v.build,
wine_ver ? ", Wine " : "", wine_ver ? wine_ver() : "", cpu, si.dwNumberOfProcessors, (unsigned long long)(ms.ullTotalPhys >> 20));
}
uint64_t plat_time_us(void)
{
static LARGE_INTEGER f;
+5
View File
@@ -109,6 +109,11 @@ void uya_log_open(void)
{
char p[1024];
snprintf(p, sizeof p, "%s/rac1_log.txt", plat_exe_dir());
{ /* the previous run's log is kept as rac1_log.prev.txt (a retry after a crash keeps the crash) */
char q[1100];
snprintf(q, sizeof q, "%s/rac1_log.prev.txt", plat_exe_dir());
remove(q); rename(p, q);
}
logfile = fopen(p, "w");
if (logfile) fprintf(stderr, "[uya] log file: %s\n", p);
}
+137 -10
View File
@@ -6,6 +6,7 @@
project-solana status what is installed, and whether the 32-bit libraries it needs are there
project-solana desktop add Project Solana to your applications menu (and so to Steam)
project-solana log print the last game log (rac1_log.txt)
project-solana report write one file with everything needed for a bug report
Like the Windows launcher: nothing from the game is downloaded. The build files (the
recompiler, the runtime, the decomp's source) come from the project's GitHub release, the
@@ -25,12 +26,43 @@ OTHER = {"SCUS_971.99": "Ratchet & Clank (NTSC-U)", "SCPS_150.37": "Ratchet & Cl
"SCUS_973.53": "Ratchet & Clank: Up Your Arsenal (NTSC-U)", "SCES_524.56": "Ratchet & Clank 3 (PAL)",
"SCUS_974.65": "Ratchet: Deadlocked (NTSC-U)", "SCES_532.85": "Ratchet: Gladiator (PAL)"}
VERSION = "1.1"
HOME = os.path.join(os.environ.get("XDG_DATA_HOME") or os.path.expanduser("~/.local/share"), "project-solana")
INI = os.path.join(HOME, "solana.ini")
LAUNCHER_LOG = os.path.join(HOME, "launcher_log.txt")
def say(*a): print(*a, flush=True)
def die(msg): sys.exit("project-solana: " + msg)
def log(text):
"""everything the launcher says also goes to launcher_log.txt (started from Steam or a menu,
there is no terminal to read it in)"""
try:
os.makedirs(HOME, exist_ok=True)
if os.path.exists(LAUNCHER_LOG) and os.path.getsize(LAUNCHER_LOG) > (1 << 20):
os.replace(LAUNCHER_LOG, LAUNCHER_LOG + ".old")
with open(LAUNCHER_LOG, "a") as f: f.write(time.strftime("%Y-%m-%d %H:%M:%S ") + text + "\n")
except OSError:
pass
def notify(text):
"""a desktop notification, when there is no terminal to print to"""
if sys.stdout.isatty(): return
for cmd in (["notify-send", "-a", "Project Solana", "Project Solana", text], ["zenity", "--error", "--title=Project Solana", "--text=" + text],
["kdialog", "--title", "Project Solana", "--error", text]):
if shutil.which(cmd[0]):
try: subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL); return
except OSError: pass
def say(*a):
t = " ".join(str(x) for x in a)
print(t, flush=True); log(t)
def die(msg):
log("error: " + msg)
notify(msg + "\n\n(project-solana report writes a file to send with a bug report)")
sys.exit("project-solana: " + msg)
def settings():
@@ -141,15 +173,20 @@ def have_lib(name):
return False
def lib_paths(name):
return [os.path.join(d, name) for d in LIBDIRS + ["/lib", "/usr/lib"] if os.path.exists(os.path.join(d, name))]
def check_libs(quiet=False):
missing = [n for n in ("libSDL2-2.0.so.0", "libGL.so.1") if not have_lib(n)]
if not lib_paths("ld-linux.so.2"): missing.insert(0, "the 32-bit C library (ld-linux.so.2)")
if missing and not quiet:
say("\nThe game is a 32-bit program and needs these 32-bit libraries: %s" % ", ".join(missing))
say("Installing Steam brings them. Otherwise:")
say(" Debian, Ubuntu, Mint, Pop!_OS: sudo dpkg --add-architecture i386 && sudo apt update && sudo apt install libsdl2-2.0-0:i386 libgl1:i386 libgl1-mesa-dri:i386")
say(" Fedora: sudo dnf install SDL2.i686 mesa-libGL.i686 mesa-dri-drivers.i686")
say(" Arch, Manjaro, EndeavourOS: enable [multilib], then sudo pacman -S lib32-sdl2 lib32-mesa")
say(" openSUSE: sudo zypper install libSDL2-2_0-0-32bit Mesa-libGL1-32bit")
say(" Debian, Ubuntu, Mint, Pop!_OS: sudo dpkg --add-architecture i386 && sudo apt update && sudo apt install libc6:i386 libsdl2-2.0-0:i386 libgl1:i386 libgl1-mesa-dri:i386")
say(" Fedora: sudo dnf install glibc.i686 SDL2.i686 mesa-libGL.i686 mesa-dri-drivers.i686")
say(" Arch, Manjaro, EndeavourOS: enable [multilib], then sudo pacman -S lib32-glibc lib32-sdl2 lib32-mesa")
say(" openSUSE: sudo zypper install glibc-32bit libSDL2-2_0-0-32bit Mesa-libGL1-32bit")
say(" (NVIDIA's own driver: also its 32-bit OpenGL library, e.g. lib32-nvidia-utils on Arch)")
return not missing
@@ -208,6 +245,7 @@ def build(args):
if e != "build": rmtree(os.path.join(gdir, e))
rmtree(os.path.join(gdir, "build", "linux", "gen")); rmtree(os.path.join(gdir, "build", "linux", "obj"))
unzip(zp, gdir, "build kit"); os.remove(zp)
open(kv, "w").write(man["version"]) # (unpacked: an interrupted build resumes with what it compiled)
levels = c.get("build", "levels", fallback="") or man.get("levels") or ",".join(str(i) for i in range(19))
cpus = os.cpu_count() or 2
@@ -227,7 +265,6 @@ def build(args):
p.wait(); log.close(); print()
if p.returncode: die("the build stopped (see %s)" % os.path.join(gdir, "build_log.txt"))
for j in ("build/linux/gen", "build/linux/gen.new", "build/linux/obj", "build/levels", "build/disc", "zig-cache"): rmtree(os.path.join(gdir, j))
open(kv, "w").write(man["version"])
c.set("disc", kit, iso); c.set("build", "built_" + kit, man["version"]); save(c)
say("Built. Ready to play: project-solana play")
@@ -257,8 +294,21 @@ def play(args):
if full: env["UYA_FULLSCREEN"] = "1"
if pc.get("vsync", "0") in ("1", "True", "true"): env["UYA_VSYNC"] = "1"
if not check_libs(quiet=True): check_libs(); die("install the 32-bit libraries above, then play again")
os.chdir(os.path.dirname(exe))
os.execve(exe, [exe, "-", iso], env)
gdir = os.path.dirname(exe)
out = os.path.join(gdir, "game_output.txt") # what SDL, the graphics driver and the game print (kept: one run back)
if os.path.exists(out): os.replace(out, out + ".prev")
log("play: %s (UYA_SCALE=%s, fullscreen %s, vsync %s)" % (iso, env.get("UYA_SCALE", "1"), full, pc.get("vsync", "0")))
with open(out, "w") as o:
try:
p = subprocess.Popen([exe, "-", iso], cwd=gdir, env=env, stdout=o, stderr=subprocess.STDOUT)
except OSError as e:
die("the game could not start (%s): run project-solana status" % e)
try: rc = p.wait()
except KeyboardInterrupt: p.terminate(); rc = p.wait()
import signal
if rc == 0: log("play: the game closed normally"); return
why = ("it crashed (%s)" % signal.Signals(-rc).name) if rc < 0 and -rc in [s.value for s in signal.Signals] else "it stopped with an error (code %d)" % rc
die("the game closed: %s. project-solana report writes a file to send with a bug report" % why)
def status(args):
@@ -291,8 +341,74 @@ def show_log(args):
sys.stdout.write(open(p, errors="replace").read())
def tail(path, n):
try:
with open(path, errors="replace") as f: lines = f.readlines()
except OSError:
return "(none)\n"
return ("(... %d earlier lines left out)\n" % (len(lines) - n) if len(lines) > n else "") + "".join(lines[-n:])
def report(args):
"""one text file with what a bug report needs; nothing from the game, and no file of the user's"""
c = settings()
gdir = os.path.join(HOME, "games", "rac1")
bdir = os.path.join(gdir, "build", "linux")
out = []
w = out.append
w("Project Solana bug report (Linux launcher %s), %s\n" % (VERSION, time.strftime("%Y-%m-%d %H:%M:%S %z")))
u = os.uname()
pretty = "?"
try:
for l in open("/etc/os-release"):
if l.startswith("PRETTY_NAME="): pretty = l.split("=", 1)[1].strip().strip('"')
except OSError: pass
w("\n== system\nLinux %s %s, %s\n" % (u.release, u.machine, pretty))
try:
cpu = next((l.split(":", 1)[1].strip() for l in open("/proc/cpuinfo") if l.startswith("model name")), "?")
mem = next((l.split(":", 1)[1].strip() for l in open("/proc/meminfo") if l.startswith("MemTotal")), "?")
except OSError: cpu = mem = "?"
w("CPU %s, %d cores, RAM %s\n" % (cpu, os.cpu_count() or 0, mem))
for k in ("XDG_SESSION_TYPE", "XDG_CURRENT_DESKTOP", "DISPLAY", "WAYLAND_DISPLAY", "SteamDeck", "SteamAppId"):
w("%s=%s\n" % (k, os.environ.get(k, "")))
for cmd in (["lspci"], ["glxinfo", "-B"]):
if shutil.which(cmd[0]):
try:
r = subprocess.run(cmd, capture_output=True, text=True, timeout=10).stdout
if cmd[0] == "lspci": r = "".join(l + "\n" for l in r.splitlines() if "VGA" in l or "3D" in l or "Display" in l)
w("-- %s\n%s" % (" ".join(cmd), r))
except Exception: pass
w("\n== 32-bit libraries\n")
for n in ("ld-linux.so.2", "libSDL2-2.0.so.0", "libGL.so.1"):
w("%s: %s\n" % (n, ", ".join(lib_paths(n)) or ("found by ldconfig" if have_lib(n) else "MISSING")))
w("\n== Project Solana\ndata folder: %s\n" % HOME)
tv = os.path.join(HOME, "toolchain", "version.txt")
w("build tools: %s\n" % (open(tv).read().strip() if os.path.exists(tv) else "not downloaded"))
kv = os.path.join(gdir, "kit_version.txt")
w("build kit: %s\n" % (open(kv).read().strip() if os.path.exists(kv) else "none"))
w("game built: %s\n" % ("yes" if os.path.exists(os.path.join(bdir, "rac1_wrapper")) else "no"))
iso = c.get("disc", "rac1", fallback="")
w("disc image: %s\n" % ((("found, %.2f GB" % (os.path.getsize(iso) / 1e9)) if os.path.exists(iso) else "MISSING (moved?)") if iso else "none"))
w("settings:\n")
for s in c.sections():
for k, v in c.items(s):
if s == "disc" or k == "release_url": continue
w(" [%s] %s = %s\n" % (s, k, v))
for title, path, n in (("launcher log", LAUNCHER_LOG, 60), ("build log (end)", os.path.join(gdir, "build_log.txt"), 80),
("game log", os.path.join(bdir, "rac1_log.txt"), 400), ("game log, the run before", os.path.join(bdir, "rac1_log.prev.txt"), 150),
("game output (SDL, graphics driver)", os.path.join(bdir, "game_output.txt"), 120),
("game output, the run before", os.path.join(bdir, "game_output.txt.prev"), 60)):
w("\n== %s\n%s" % (title, tail(path, n)))
text = "".join(out).replace(os.path.expanduser("~"), "~") # (no user name in the report)
dest_dir = next((d for d in (os.path.expanduser("~/Desktop"), os.path.expanduser("~")) if os.path.isdir(d)), HOME)
dest = os.path.join(dest_dir, "project-solana-report-%s.txt" % time.strftime("%Y%m%d-%H%M%S"))
open(dest, "w").write(text)
say("Wrote %s: attach it to your bug report (it has your system details and the logs, nothing else)." % dest)
def main():
ap = argparse.ArgumentParser(prog="project-solana", description="Build Ratchet & Clank from your own disc, then play it.")
ap.add_argument("--version", action="version", version="project-solana %s (Linux launcher)" % VERSION)
sp = ap.add_subparsers(dest="cmd")
b = sp.add_parser("build", help="build the game from your disc image")
b.add_argument("disc"); b.add_argument("-j", "--jobs", type=int, default=0)
@@ -310,9 +426,20 @@ def main():
v.add_argument("--no-vsync", dest="vsync", action="store_const", const=0)
sp.add_parser("status", help="what is installed"); sp.add_parser("desktop", help="add it to the applications menu")
sp.add_parser("log", help="print the last game log")
sp.add_parser("report", help="write one file with everything a bug report needs")
a = ap.parse_args()
if sys.platform != "linux": die("this is the Linux launcher: on Windows, use Project Solana.exe")
{"build": build, "play": play, "status": status, "desktop": desktop, "log": show_log}.get(a.cmd, lambda _: ap.print_help())(a)
log("project-solana %s: %s" % (VERSION, " ".join(sys.argv[1:])))
try:
{"build": build, "play": play, "status": status, "desktop": desktop, "log": show_log, "report": report}.get(a.cmd, lambda _: ap.print_help())(a)
except SystemExit:
raise
except KeyboardInterrupt:
sys.exit(130)
except Exception as e: # (an unexpected error: in the launcher log too, with where)
import traceback
log("unexpected error:\n" + traceback.format_exc())
die("unexpected error: %s (details in %s)" % (e, LAUNCHER_LOG))
if __name__ == "__main__":