Verified live on a Pixel 6a: the game passes its EULA, loads the prologue
and renders real 3D gameplay, with zero heap exhaustion, zero faults and
zero rejected frees over a full session.
Root causes fixed in this state, each backed by a measurement (details and
the list of refuted theories live in ARM64_TRANSLATION_LAYER.md):
* JNI varargs float->double promotion. C promotes float to double in any
varargs call and every Call*Method form is varargs, so reading one 4-byte
slot yielded the double's always-zero low half. EVERY float argument
passed to Java was silently becoming 0; text was just where it showed.
* GuestHeap ~4x memory overhead. Power-of-two size classes carving the full
class, plus segregated free lists that could never share memory between
sizes. Reworked to exact sizing with O(log n) best-fit reuse and splitting
(deliberately not a linear scan - this allocator already had an O(n) perf
cliff in its history). Peak live now 207MB against the real A9's 199MB,
fragmentation ~2.5MB. Also fixed: realloc reading past the old block on
shrink, a 32-bit overflow in calloc, and drifting payload alignment.
* Unbounded FMOD fake-handle leak into the never-freeing permanent arena,
which is why enlarging that arena had not helped.
* Frame presentation, corrected against A9 ground truth: the real frame has
three default-framebuffer colour clears and ONE present at the end; this
engine had been presenting on each of them.
Load-time acceleration (zlib_accel.cpp): host zlib now serves inflate and
crc32, the latter measured by the block profiler as the single hottest guest
routine at 17.7%. Streams are only taken over when this layer saw their own
inflateInit2_, so unknown streams (libpng's, among others) still run the
original emulated path.
name_lookup_accel.cpp is present but its hook is NOT registered - it crashed
on bad assumptions about guest table lifetime and is kept as a starting
point, with both mistakes recorded in its comments.
Co-Authored-By: Claude <noreply@anthropic.com>
GuestHeap previously bump-allocated any request >64KiB and never
reclaimed it on free() - a real, sustained gameplay session eventually
exhausted the arena (traced live to two separate crashes: onCreate
never completing, and a NULL-vector dereference in sub_4BA588 caused
by a failed ~175KB allocation going unchecked by the real EA code).
Widen the existing O(1) size-class free list (already used for <=64KiB
requests) to cover the whole arena instead of adding a second reuse
mechanism next to it - oversized allocations now pool and reclaim the
same way small ones already did. Add matching desktop tests
(guest_heap_test.cpp, run via run_heap_tests.sh) covering the exact
failing size from the crash log and a sustained alloc/free cycle in an
arena too small to survive without reclaim.
Verified live on device: the crash is gone across a full 10-minute
session with sustained GLES rendering throughout.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Hardcode car class enum for lobby car selection (fixed, small set by design)
- Fix GetCurrentCarColor() crashing on an unvalidated color index from an
unzeroed hashmap-miss fallback record on a genuinely fresh save
- Default kEnableTrackSubstitutionHook to false: it substituted every race's
track unconditionally, crashing the game's own scripted prologue race
- Remove GetComponentNameSkipHook from JNI_OnLoad: it unconditionally
replaced a real name used by the same cache-context the per-event medal
progress record resolves through, silently breaking medal/street-completion
tracking for every real race
- Split lan_event_injection.h (3149 lines) into car_selection.h,
crash_workarounds.h, mod_slot_tracking.h, and a shared
util/hook_install.h trampoline helper; delete ~490 lines of confirmed-dead
experimental code; trim comment-heavy sections to essential context
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Fixes the synthetic car_select jump for cold sessions, makes the loadout
exit chain safe for real (non-synthetic) events, and adds a native->Kotlin
GameEvents bridge (onMapLoaded/onRaceStarted/onRaceEnded/onUpgradesAccepted/
onCarSelected) so both the UI layer and a future native RatNet client can
learn what the player picked - car id, accepted upgrades, and paint color
(name + RGBA) are all resolved live from the game's own engine state
rather than a static extracted table, so they stay correct for any car
added later. Includes a Jetpack Compose overlay as a worked example of a
UI-side GameEventListener consumer.
Full investigation history, root causes, and the several dead ends ruled
out along the way are documented in PROGRESS.md (cont. 30-63b).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Replaces the "inject into every pin" visual-confirmation mode with
matching against MapTrack's own event-group-name vector (offsets
0xE0/0xE4), which is available as soon as HandleEvent(evtype=1025)
fires regardless of whether any RaceEvent has resolved yet. Injection
now targets exactly one street (kTargetGroupName) and the card's
displayed name is an explicit parameter instead of a fixed constant.
Live-tested: single-target injection hits exactly the intended widget
with zero effect on the other 11 reachable MapTrack instances. The 12
reachable group names are all region/career-progression placeholders
(region{1,2,4,5}_{foothills,desert,chicago,newyork}_track{1,2,3}) -
none correspond to the currently visible/playable on-screen streets,
which a live AddEvent-level trace confirmed never call AddEvent during
normal play at all (see PROGRESS.md 2026-08-08 cont. 9/10 for the full
investigation, including the now-reverted diagnostic hooks used to
establish this).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Hooks MapTrack::HandleEvent to fabricate a RaceEvent+CashReward+fake
Actor and insert it via the engine's own AddEvent, making a synthetic
"LAN: <lobby>" card appear on an already-loaded street entirely at
runtime (no game_cache/OBB/native_lib changes). Also hooks the
per-frame MapScreen tick to neutralize a QA-only "Soak Test" feature
whose index-based scan of a parallel, unsynced list was the root
cause of a delayed crash on injected entries.
Verified live on Galaxy A9 (2018): zero crashes across all reachable
pins, and a 10-minute soak test with zero crashes after the Soak Test
fix, confirmed against a genuinely responsive post-test map screen.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Lets the track-substitution hook be enabled/disabled by flipping one
constexpr bool in main.cpp instead of commenting/uncommenting code, so
before/after comparisons don't need back-and-forth edits.