RPCS3: God of War HD reaches gameplay at 100% and 300%; LLVM 98b45cc

The fork (4560e6b) resolves paths as the Qt application does, answers the image
and photo callbacks, releases a waiter's mutex around its wait callback in the
atomic wait engine (EDEADLK on the console's error-checking mutexes), and gives
the RetroPads the handler's own bindings. The LLVM fork (98b45cc) lays out
TrailingObjects classes as other targets do: the PS5 ABI ignores alignas on
their empty aligner base, and each allocation came up 4 bytes short (SPU
compiles crashed in MachineInstr::hasOrderedMemoryRef).

The title's crash report now prints every register, the heap words around
them, and the code addresses on the crashing stack up to its top.

God of War HD boots to its first level at 100% and 300%, every 10 s window
after the boot at 100% (docs/PHASE_LOG.md).
This commit is contained in:
Mihawk committed 2026-09-29 00:36:38 -04:00
1 parent 19ae1659d2
commit 3aa85c449b
6 files changed
+216 -14

No files matched your search

+13 -10
View File
@@ -1,6 +1,6 @@
# Active work
_Updated: 2026-09-28_
_Updated: 2026-09-29_
## Now: v0.5.0-alpha.5 released, the first on RADV; Dolphin's first start next
@@ -37,15 +37,18 @@ checkout without the sibling forks builds from there.
**Now: RPCS3 as a libretro core** (docs/RPCS3_PORT.md). The goal is God of
War HD (NPUA80490, a PSN package plus its RAP) booting from `content/PS3/`
at a 300% scale on RADV, with the firmware from `system/RPCS3/`. Ladder steps
1 to 5 are done (docs/PHASE_LOG.md, 2026-09-28): the core builds from my fork
PS5_RPCS3 (`main`, published) with the pinned LLVM, FFmpeg and libiconv, loads
on the console, installs the firmware, and runs RPCS3's `gs_gcm_hello_world`
test with the LLVM recompilers on RADV, picture and full-speed audio, exiting
cleanly. Next is step 6: the game's package install and boot at 100%, then
300%, the battery and the ten-minute run. Known costs: the shader
interpreter's 6,650 pipelines compile before a first boot (about 100 s, then
cached), with no loading screen yet.
at a 300% scale on RADV, with the firmware from `system/RPCS3/`. Steps 1 to 5
are done, and step 6 has reached gameplay (docs/PHASE_LOG.md, 2026-09-28): the
package installs behind the core's loading screen (31.5 minutes for 6.3 GB,
held to a title's ~2 MiB/s write budget), and the game boots to its first
level at 100% and at 300%, every 10 s window after the boot at 100% speed.
Getting there took the platform's `realpath`, a layout fix in the LLVM fork's
`TrailingObjects` (the PS5 ABI ignores `alignas` on an empty base), a mutex
held across a wait callback in RPCS3's atomic wait engine, the frontend's path
and image callbacks, and the RetroPads' bindings. Still to do for step 6: the
ten-minute run, the Close Content + reload battery (122 core threads were
still running at quit), and my acceptance of the 300% screenshot. After that:
installing a package's large files without copying them.
**Dolphin transitions (paused 2026-09-28 for RPCS3):** Rogue Leader's dips are
not shader compiles. The emulation thread spins in `sched_yield` inside a
+100
View File
@@ -4346,3 +4346,103 @@ the core unloads cleanly. The CTS run was paused for these runs and resumed
afterwards.
Next: God of War HD from its package and licence, first at 100%.
## 2026-09-28 — RPCS3: God of War HD installs behind a loading screen and boots to its SPU compiles (ladder step 6, begun)
**The package installs.** RPCS3's installer normalises its path with
`std::filesystem::weakly_canonical`, which libc++ builds on `realpath`, and the
console refuses a title's `realpath` with EPERM, even for `/app0` (the files
probe, measured). The platform answers it now (`ps5_realpath`, SDK 8160289):
the path made absolute from `ps5_getcwd`, `.` and `..` resolved by name, each
component checked with `stat`. The title links it too (`--wrap=realpath`), and
the cores' imports are aliased to it.
God of War HD's 6.3 GB package then installed in 31.5 minutes: 13 MB/s for the
first 2.7 GB, 2.2 MB/s after that. The sampler put the writing thread in
`write()` for almost every sample, not in AES (AES-NI) or the reads. The SDK's
new sustained write probe (`writes`, SDK c769422) measured why: a title writes
242 MiB/s for about 1.3 GiB, even with `O_DIRECT`, then 2.0-2.1 MiB/s however
it writes, while the FTP server wrote the same folder at 22 MiB/s at the same
moment. The storage is not the limit; a title's writes are (the SDK's
docs/PROBE.md, "Sustained writes"). Installing a package's large files without
copying them, read and decrypted from the package the way RPCS3 reads an ISO,
would remove both the wait and the second copy; that is the next step for
installs.
**A loading screen.** The install ran inside `retro_load_game`, so RetroArch
showed a black screen for as long as it took. The fork (78dfa8f) installs the
firmware and packages on a worker thread and shows a loading screen until
RPCS3's first frame (docs/RPCS3_PORT.md, "Content loading and the loading
screen"): the game's own art tinted violet, its icon, the title in Inter, and a
violet-to-orchid bar with the sizes, speed and time left, then "Starting" with
RPCS3's compile progress, or what failed. RetroArch's screenshot on the console,
one minute into the install, showed it at 36% (2.32 GB of 6.32 GB). Along the
way:
- the screen called `vkCreateImage` before RPCS3 had loaded any device
function through volk (a jump to 0); it loads them for RetroArch's device;
- a package header's `title_id` is the content ID (`UP9000-NPUA80490_00-...`),
so the check for an installed game never matched and each load installed the
package again, overwriting the installed game (which then had to be
installed again); the title ID now comes from its PARAM.SFO, and a finished
install is recorded in `system/RPCS3/libretro/installed/`;
- closing content during the install waited for the whole 6 GB file: RPCS3's
installer saw an abort only between files. It checks after every block now,
and a file left part way counts the package as aborted.
The resolution scale became a core option (100% to 300%, 300% by default).
**The boot.** With the game installed, the boot got through the PPU executable
and then stopped at RPCS3's boot music: the overlay plays SND0.AT3 through the
frontend's video source, and the core made none. It makes a silent one now.
Next, the shader interpreter's 6,650 pipelines (115 s the first time, a few
seconds from RADV's cache after that), then SPU compiles, where every run
crashed within seconds in LLVM's `MachineInstr::hasOrderedMemoryRef`, reading
`0x2_xxxx_xxxx`. The crash report now prints every register and the heap words
around them, which showed the instruction's two memory operands with the
second one's upper 32 bits replaced by 2: the next allocation's first field.
LLVM's `TrailingObjects` takes its alignment from an empty `alignas` base,
which the PS4/PS5 ABI ignores, so on the console such classes were allocated
4 bytes short (docs/RPCS3_PORT.md, "LLVM"). The LLVM fork (98b45cc) fixes it;
the PS5 target now lays such a class out as the host does.
**To gameplay.** Four more stops, each found from the console's own report:
- *The game's folder lost its first letter.* Emulator::Load cuts the folder
name after the resolved `<hdd0>/game` plus a separator of its own, and the
core had left the path callbacks at their default, which returns a path as
given, trailing separator and all: the game booted as
`/dev_hdd0/game/PUA80490/` and found none of its files. The core resolves
paths as the Qt application does (`QFileInfo::canonicalFilePath`), through
`std::filesystem` on the platform's `realpath`.
- *Three frontend callbacks were unset* (`get_scaled_image`, `get_image_info`,
`get_photo_path`). The first overlay with an icon called an empty
`std::function`, and a core built without exceptions terminates on the
`std::bad_function_call`. They are stb_image's now. RPCS3's terminate
handler also names an uncaught exception and prints its message, and the
title's crash report lists the code addresses on the crashing stack, up to
its top: the throw site survives, since nothing unwinds before terminate.
- *"mutex lock failed: Resource deadlock avoided".* On FreeBSD and the PS5
the atomic wait engine waits on a `std::condition_variable`, and the waiting
thread held its own mutex while running its wait callback; an SPU thread's
callback notifies lv2 objects, the notification locks every waiter's mutex,
its own among them, and the console's default mutex checks for that
(EDEADLK). The fork releases the mutex around the callback; a notification
meanwhile sets `sync` first, which is checked again under the mutex.
- *No button reached the game.* The core named the RetroPads in the input
configuration but left RPCS3's default bindings, which are empty. Each pad
gets the RetroPad handler's own now (`pad_thread::InitPadConfig`).
With those, God of War HD boots through its logo video to its menu, and a
scripted Cross on "New Game" and the difficulty reaches the first level, the
ship in the Aegean, with RPCS3's Vulkan renderer on RADV:
| Scale | Evidence | 10 s windows after the boot |
| --- | --- | --- |
| 100% (720p) | gameplay screenshot, 3840×2160 | 17 of 17 at 100% |
| 300% (2160p) | gameplay screenshot, 3840×2160 | 17 of 17 at 100% |
The two windows after the 300% run's last (7%, 0%) are RetroArch encoding its
5.8 MB screenshot and quitting after the frame limit, when the core no longer
runs. At that quit the loader found 122 core threads still running and kept
the core mapped; the Close Content and reload battery is what tests that.
+15 -2
View File
@@ -353,8 +353,21 @@ Where the build and the first code differ from the audit above.
- *LLVM* is my fork `../PS5_LLVM` (github.com/mihawk-99/PS5_LLVM), pinned in
`tools/build-llvm.sh`. The PS4/PS5 ABI ignores `alignas` on an empty base
class, so `SmallVector`'s layout assertions fail for this target; the fork
aligns `SmallVector` itself under `__SCE__`. The build uses host tablegen
class, and LLVM relies on it twice. `SmallVector<T, 0>`'s layout assertions
failed at build time; the fork aligns `SmallVector` itself under `__SCE__`.
`TrailingObjects` failed silently: its empty aligner base is what raises a
class to its trailing objects' alignment, so on the PS5 a header of an `int`
and some `bool`s stayed 4-aligned and 12 bytes, and two trailing pointers
were given 28 bytes while `getTrailingObjects` read them from 16 to 32. The
next allocation's first field overwrote the last pointer's upper half
(`MachineInstr::ExtraInfo`'s memory operands: God of War HD's SPU compiles
crashed in `MachineInstr::hasOrderedMemoryRef` on `0x2_xxxx_xxxx`, heap
pointers with their upper 32 bits replaced by the count 2). The fork gives
the aligner a zero-length member there, a non-empty base without storage;
the PS5 target then lays such a class out as the host does (8-aligned, 16
bytes, 32 allocated, trailing objects at 16). The other empty `alignas`
classes in LLVM and RPCS3 are standalone objects, whose alignment the ABI
keeps. The build uses host tablegen
tools (`LLVM_NATIVE_TOOL_DIR`) and builds only the libraries
(`LLVM_INCLUDE_TOOLS=OFF`), since no LLVM program links for the console.
- *FFmpeg* 8.1.1 comes from its signed release tarball (`tools/build-ffmpeg.sh`),
+86
View File
@@ -23,10 +23,13 @@
#include <initializer_list>
#include <fcntl.h>
#include <pthread.h>
#include <pthread_np.h>
#include <ucontext.h>
#include <unistd.h>
#include <ps5platform/context.h>
#include <ps5platform/heap.h>
extern "C" int sceKernelAvailableFlexibleMemorySize(std::size_t *size);
@@ -82,6 +85,89 @@ void report(int signal, siginfo_t *info, void *context_pointer)
}
(void)!write(fd, "\n", 1);
}
/* The return addresses above the fault: every word from rsp to the top of
* the thread's stack (32 KiB at most) that points into the title's code or
* a core's. Without frame pointers this is how a crash ending in abort()
* (an uncaught exception, which terminates before unwinding) shows where
* it began; the loader's base turns a core's into offsets. */
if (fd >= 0 && rsp >= 4096 && (rsp & 7) == 0)
{
pthread_attr_t attributes;
void *stack = nullptr;
std::size_t stack_size = 0;
if (pthread_attr_init(&attributes) == 0)
{
if (pthread_attr_get_np(pthread_self(), &attributes) == 0)
pthread_attr_getstack(&attributes, &stack, &stack_size);
pthread_attr_destroy(&attributes);
}
const std::uintptr_t stack_top = reinterpret_cast<std::uintptr_t>(stack) + stack_size;
if (stack && rsp < stack_top)
{
const std::uintptr_t end = stack_top - rsp > 32768 ? rsp + 32768 : stack_top;
(void)!write(fd, "crash: frames", 13);
int found = 0;
for (std::uintptr_t at = rsp; at + 8 <= end && found < 96; at += 8)
{
const std::uintptr_t word = *reinterpret_cast<const std::uintptr_t *>(at);
const bool title = word >= 0x400000 && word < 0x1100000;
const bool core = word >= 0x400000000ull && word < 0x500000000ull;
if (!title && !core)
continue;
char text[24];
const int n = std::snprintf(text, sizeof(text), " %llx",
static_cast<unsigned long long>(word));
(void)!write(fd, text, static_cast<std::size_t>(n));
++found;
}
(void)!write(fd, "\n", 1);
}
}
/* Every general register, then 64 bytes around each one that points into
* the title heap: a bad pointer read from memory shows beside the good
* ones it was stored with. A dump that faults ends the report there, after
* the lines above are written. */
if (fd >= 0)
{
const mcontext_t &mc = context->uc_mcontext;
const std::uintptr_t registers[] = {
std::uintptr_t(mc.mc_rax), std::uintptr_t(mc.mc_rbx), std::uintptr_t(mc.mc_rcx),
std::uintptr_t(mc.mc_rdx), std::uintptr_t(mc.mc_rsi), std::uintptr_t(mc.mc_rdi),
std::uintptr_t(mc.mc_rbp), std::uintptr_t(mc.mc_r8), std::uintptr_t(mc.mc_r9),
std::uintptr_t(mc.mc_r10), std::uintptr_t(mc.mc_r11), std::uintptr_t(mc.mc_r12),
std::uintptr_t(mc.mc_r13), std::uintptr_t(mc.mc_r14), std::uintptr_t(mc.mc_r15)};
static const char *const names[] = {"rax", "rbx", "rcx", "rdx", "rsi", "rdi", "rbp", "r8",
"r9", "r10", "r11", "r12", "r13", "r14", "r15"};
(void)!write(fd, "crash: registers", 16);
for (std::size_t i = 0; i < sizeof(registers) / sizeof(registers[0]); ++i)
{
char word[32];
const int n = std::snprintf(word, sizeof(word), " %s=%llx", names[i],
static_cast<unsigned long long>(registers[i]));
(void)!write(fd, word, static_cast<std::size_t>(n));
}
(void)!write(fd, "\n", 1);
for (std::size_t i = 0; i < sizeof(registers) / sizeof(registers[0]); ++i)
{
const std::uintptr_t at = (registers[i] & ~std::uintptr_t(7)) - 32;
if (!ps5_heap_owns(reinterpret_cast<const void *>(at)) ||
!ps5_heap_owns(reinterpret_cast<const void *>(at + 63)))
continue;
char head[48];
const int n = std::snprintf(head, sizeof(head), "crash: near %s %llx:", names[i],
static_cast<unsigned long long>(at));
(void)!write(fd, head, static_cast<std::size_t>(n));
for (int w = 0; w < 8; ++w)
{
char word[24];
const int m = std::snprintf(word, sizeof(word), " %llx",
static_cast<unsigned long long>(
reinterpret_cast<const std::uintptr_t *>(at)[w]));
(void)!write(fd, word, static_cast<std::size_t>(m));
}
(void)!write(fd, "\n", 1);
}
}
if (fd >= 0)
close(fd);
/* Not async-signal-safe, and worth the risk: the process is ending anyway,
+1 -1
View File
@@ -19,7 +19,7 @@ set -euo pipefail
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
cd "$root"
revision=f8d662cce7162d70bfb315344623eca7ab9eef88 # PS5_LLVM main: llvmorg-22.1.8 and the port
revision=98b45cc22e98844b7d49bbedefdfee04a740650a # PS5_LLVM main: llvmorg-22.1.8 and the port
prefix="$root/.deps/native/llvm-ps5"
[[ -f $prefix/.revision && $(<"$prefix/.revision") == "$revision" ]] && {
echo "==> [llvm] $revision already built in $prefix"; exit 0; }
+1 -1
View File
@@ -26,7 +26,7 @@ core_stamp_skip rpcs3 \
"$root/.deps/native/libiconv-ps5/.stamp"
[[ $# == 0 ]] || { echo "usage: ${0##*/}" >&2; exit 2; }
revision=78dfa8f28fac3138df6274f3ec8576eb5e18a1f3 # ../PS5_RPCS3 main
revision=4560e6b2ab2c44d1ea61be03505756c39c127dc4 # ../PS5_RPCS3 main
core_fork_setup
core_fork_checkout PS5_RPCS3 "$revision"
core_info="$root/tooling/rpcs3/rpcs3_libretro.info"