Repository navigation
Vector registers in thread contexts; stop JIT pool code-buffer leaks (Persona 5 Royal reaches gameplay) - #556
Merged
Conversation
…(Persona 5 Royal reaches gameplay) Persona 5 Royal (Steam 1687950) died about 33 seconds in, at the same address every run: a jump to 0x1489C3B50, an all-zero page, then an access violation reading address 1. The game's protection code creates a thread that loads code into XMM13, suspends it, reads its floating-point context into a CONTEXT template inside the image (rva 0x89C38E0) and jumps to the XMM13 slot, 0x270 bytes in. The server's Mach capture read only ARM_THREAD_STATE64, so every cross-thread GetThreadContext returned zeroed vector registers and the game executed zeros. ios_fill_thread_context() now also reads ARM_NEON_STATE64. Under ARM64EC, FEX keeps guest XMM0-15 in V0-V15, the mapping the ARM64EC ABI uses, so these are the guest's XMM registers. A thread blocked in a syscall reports the vector registers the dispatcher saved in its syscall frame instead, with the same validation as the ml716 frame substitution. Known limits: inside a Unix call only q8-q15 are saved, so q0-q7 come from the last syscall; a thread stopped in FEX's own code reports host values; YMM upper halves are not reported. Each is still an improvement on zeros. MADEIRA_CTX_FP=0 restores the zeroed registers. Once it ran, the game's threads were held to 16 MB code buffers and recompiled about 70% of the code they ran (hit rates 27-37%). Two leaks held 256 MB of the 304 MB of live code buffers: - A buffer FEX released while a thread was still running in it was refused (ml557) and stayed live for the rest of the session, because FEX never releases a buffer twice. It is now freed as retired, so reuse waits out the grace and the strict thread scan, as for a dead process's buffer. MADEIRA_POOL_CODE_DEFER_FREE=0 refuses it as before. - crashpad_handler.exe, alive for the whole session, took a free 16 MB buffer for a 16 KB request and a 128 MB one for a 16 MB request. A free buffer four or more times the request is now kept back while the pool's budget still affords a fresh one of the size asked; if that fresh one cannot be made, the request falls back to the kept buffer. MADEIRA_POOL_CODE_KEEP_BIG=0 restores the old policy. On an iPhone 17 Pro Max (iOS 27.0.1) P5R now reaches gameplay, loads its intro in about 10 s and saves in about 13 s (previously very slow), and holds 30 fps with a frame cap; hit rates rose to 46-68%. Is This Seat Taken still runs at 60 fps and exits cleanly with this build. tests/host/check-ctx-fp.py checks the frame offsets against struct syscall_frame, that the syscall dispatcher saves q0-q31, fpcr and fpsr there, the gating and ordering of the capture, and the switch. check-pool-code-exit.py and check-pool-startup-budget.py cover the new pool behaviour and both switches. ConfigCatalog.generated.swift is regenerated for the three switches. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Henry Morin <henry@hmorin.com>
125hz
pushed a commit
that referenced
this pull request
Nov 5, 2026
…(Persona 5 Royal reaches gameplay) (#556) ## Summary
125hz
added a commit
that referenced
this pull request
Nov 5, 2026
…follow-up) ios_ctx_fp_enabled() read madeira.cfg and the game's config on every cross-thread context capture, inside the single-threaded server; a program that samples its threads' contexts often put that file I/O on each call. The answer is cached against the server's monotonic clock and re-read after a second, so a changed setting still takes effect without a restart. The comments name the behaviour, not a game. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
125hz
added a commit
that referenced
this pull request
Nov 5, 2026
…556 follow-up) The host check compiles ios_ctx_fp_enabled() on its own; with the answer kept for a second it needs timeout_t, monotonic_time and TICKS_PER_SEC, and each check advances the clock. One check reads twice within the second and expects the kept answer. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes a Persona 5 Royal (Steam 1687950) crash about 33 seconds in, and two JIT pool leaks that kept it very slow once it ran.
Crash: zeroed vector registers
The game's protection code suspends another thread, reads its floating-point context into a CONTEXT template inside the image, and jumps to the XMM13 slot. The server's Mach capture read only
ARM_THREAD_STATE64, so every cross-threadGetThreadContextreturned zeroed vector registers and the game executed zeros.ios_fill_thread_context()now also readsARM_NEON_STATE64. Under ARM64EC, FEX keeps guest XMM0-15 in V0-V15, so these are the guest's XMM registers. A thread blocked in a syscall reports the vector registers saved in its syscall frame instead.MADEIRA_CTX_FP=0restores the old behaviour.Pool: two code-buffer leaks
These held 256 MB of the 304 MB of live code buffers. Game threads were capped at 16 MB buffers and recompiled about 70% of the code they ran.
MADEIRA_POOL_CODE_DEFER_FREE=0restores the refusal.crashpad_handler.exetook a free 16 MB buffer for a 16 KB request and a 128 MB buffer for a 16 MB request, and kept them for the session. A free buffer at least 4x the request is now kept back while the budget affords a fresh one, with a fallback to the kept buffer if the fresh one can't be made.MADEIRA_POOL_CODE_KEEP_BIG=0restores the old policy.Testing
On an iPhone 17 Pro Max (iOS 27.0.1), with all switches at their defaults:
The switches set to
0were covered only by host tests, not on a device.Added
check-ctx-fp.py, which checks the frame offsets againststruct syscall_frame, the dispatcher's register saves, the capture's gating and ordering, and the switch. Updatedcheck-pool-code-exit.pyandcheck-pool-startup-budget.py, which had asserted the old behaviour. 69 of 79 related host tests pass on macOS; the other 10 also fail without this change.Review notes
ConfigCatalog.generated.swiftis a full regeneration, so it also adds six entriesmainwas missing and moves four.🤖 Generated with Claude Code