github alondero/automobililamborghini-recomp v0.10.3
Automobili Lamborghini Recompiled v0.10.3

4 hours ago

Pre-release quality. Built from main for early testing and feedback. Expect rough
edges — crashes, audio glitches, and missing native translations are still the rule, not
the exception. Please open issues rather than running off the latest commit silently.

⚠️ You must supply your own ROM

This release contains no game assets or code from the original cartridge. It ships
only the recompiled executable and optional replacement artwork. To play, you must obtain
a legal copy of Automobili Lamborghini (USA).z64 (the North American release is the only
one supported) and place it next to the executable before launching. See BUILDING.md
for the full instructions; in brief:

  1. Extract the archive for your platform into its own folder.
  2. Drop Automobili Lamborghini (USA).z64 into that folder.
  3. Launch the binary.

Game saves go alongside the ROM (or under %LOCALAPPDATA%\LamborghiniRecomp on
Windows / ~/.config/LamborghiniRecomp elsewhere — see README.md).

What's working

The headline of this release is Android performance: automatic resolution no longer
picks an unusable scale, and frame matching got roughly three times cheaper, so the
game runs at display rate instead of half of it. On the Adreno 650 test phone, Auto
used to select 5x (2903x1200) on a 2400x992 surface; GPU time of 13 to 18 ms then
dropped every interpolated frame and the game presented 30 fps. Automatic resolution is
now bounded by a pixel-rate budget, and frame matching dropped from about 12.8 ms to
about 3.5 ms per game frame, so the same phone now presents 59.8 fps with Turnip and
59.3 fps with the system driver against 30.0 and 28.1 before.

Eleven pull requests have landed on main since v0.10.2: four Android fixes and
features, two Vulkan correctness fixes, two performance changes, one build-tooling
cleanup and two HUD/documentation changes.

There's still glitches so please report issues you find.

Closed since v0.10.2:

Android — performance

  • #374 / PR #376 — Android automatic resolution is bounded by a pixel-rate budget, and frame matching got much faster. Before this, Auto rendered at the window's integer scale on every Android device except one Adreno 615 driver: 5x (2903x1200) on a 2400x992 surface and 6x on a 1220-line panel, with GPU time of 13 to 18 ms dropping every interpolated frame so the game presented 30 fps. Patch 0059 picks the largest integer scale whose internal pixels times the presentation rate stay within 80 million pixels per second — 3x at 60 Hz, 4x at 30 Hz, 2x at 120 Hz on a 2400x992 surface, never below 1x and never above the window scale. Manual and native scales, supersampling and desktop are unchanged, and the Adreno 615 2x cap from v0.10.1 still applies. Patch 0060 then matches frames with reused, sorted vectors instead of node containers; the pairing order is unchanged but matching drops from about 12.8 ms to about 3.5 ms per game frame. Measured with default settings (Auto, MSAA 2x, display rate): Turnip 30.0 to 59.8 fps, system driver 28.1 to 59.3 fps. Patch 0058 also prints a [render-cost] line to native.log every 5 seconds on Android (desktop: RT64_RENDER_COST_REPORT=1) with the surface, internal size, scale, game and present fps, kept frames, and frame/GPU/workload/setup/match times, so the next report can be read without a profiler. Not measured: 120 Hz devices, full races, multiplayer, or the touch overlay cost.
  • #378 / PR #384 — RT64 frame matching no longer pairs every draw call with every previous call of equal hash. On every track, 224 two-triangle calls under one world matrix share a hash: about 50,000 call pairs per game frame that reduce to 24 to 65 distinct transform pairs, and the pairing loop plus the duplicate-removing sort took over 90% of matching. Patch 0061 has each call add keys holding only what decides its check pairs (transforms, tiles, look-ats), merges equal keys, and joins the current and previous keys of each hash, so the sorted, unique check pairs are unchanged and every transform, tile and look-at match is the same. Desktop (Ryzen 9 3900X, D3D12) medians per game frame fell from 1.6 to 2.1 ms to 0.44 to 0.64 ms on all six tracks, and from 7.5 ms to 1.4 ms in two-player split screen, where the group doubles to about 200,000 pairs. A profiling build that hashed every match per frame was identical on all about 13,200 compared frames of nine scenarios. The Adreno 650 phone was not measured.

Android — Vulkan presentation

  • #359 / PR #368 — the game recovers its Vulkan surface after Android replaces the native window, and #329 (the black screen and ANR after Home and resume) is closed. After Android replaces a native window, Plume kept the original Vulkan surface and could loop on VK_ERROR_NATIVE_WINDOW_IN_USE_KHR. SDL now publishes retained native-window snapshots with generations, RT64 Present recreates the Vulkan surface and swap chain, framebuffers are retired before their image views, and guest time and audio suspend across background transitions. Failed drains keep their swap-chain attachments and semaphores for retry, and only completion or device loss permits cleanup — other failures abandon the GPU handles without allocating. A timeout or not-ready acquisition skips the frame instead of rebuilding a healthy chain, and recovery failures report the reason and Vulkan result once per distinct pair. The patches are 0054 to 0057 (SDL native-window generations, Plume surface recovery, RT64 present resource retirement, runtime app suspension). Device transition acceptance and stronger WSI retirement synchronization remain open.
  • PR #365 — the Android swap chain no longer asks gralloc about its format. Issue 329's three exports from 2026-10-28 (app 0.10.2, Xiaomi 12T Pro, Android 15, Adreno 730, system driver, Turnip T30 and v25.3.0-R11) all show the same hang: RT64 Present blocked in the first vkCreateSwapchainKHR, waiting on the priority-inheritance lock in gralloc's BufferManager, with HWUI's RenderThread waiting on the same lock. Android 15's loader answers vkGetPhysicalDeviceSurfaceFormatsKHR by asking gralloc about five formats with AHardwareBuffer_isSupported, and two of them are the formats this gralloc logs as unrecognized. Patch 0049 uses R8G8B8A8_UNORM on Android with VK_COLOR_SPACE_SRGB_NONLINEAR_KHR without the format query — the loader always lists that pair first without asking gralloc, so the chosen format is unchanged. The build also checks gralloc before and after renderer setup (lambo::stall::check_buffer_allocator) and writes one line to native-errors.log naming the lock holder if gralloc does not answer within 1 second. That the format query leaves gralloc's lock held is an inference: this gralloc's source is not public, and neither test device reproduces the hang.

Vulkan — mapped memory

  • #360 / PR #371 — Plume's Vulkan buffers now maintain noncoherent caches through shared dependency patch 0050, with the declaration contract in additive patch 0051. The contract is map -> validate -> VMA map -> invalidate reads -> CPU access and unmap -> validate -> flush writes -> VMA unmap -> GPU submission; invalid ranges, unmatched unmaps and VMA failures stop safely, and successfully acquired mappings are released before failure. The caller audit defines exclusive byte ranges and heap defaults: omitted UPLOAD/GPU_UPLOAD read ranges default to write-only and omitted READBACK ranges to read-only, because D3D12 permits whole-resource CPU reads for a null read range while Vulkan's omitted upload ranges are deliberately write-only. Queue-family ownership stays separate. Desktop Vulkan validation passed on an RTX 3080 and the Android probe passed omitted and partial-range experiments on a physical noncoherent READBACK allocation (flags 0xB). Physical noncoherent upload flushing remains unverified: neither available device offers a compatible type.
  • PR #372 — recoverable map failures are preserved and nested reads are rejected. Patch 0052 preserves recoverable map failures: a map or invalidation failure releases the new reference if needed and returns null instead of discarding the caller's data, so an optional shadow upload keeps working. Patch 0053 prevents nested invalidation from discarding outstanding host writes — an active mapping with a non-empty read stops before a second VMA map, and an omitted read is empty on UPLOAD/GPU_UPLOAD and the whole buffer on the other heaps. A flush failure releases the mapping, logs, and terminates before submission, because a void unmap cannot report failure. Before this, the mapping suite produced eight failing subcases; after it, all of them pass, including nested explicit-empty reads on UPLOAD, READBACK and DEFAULT with coherent and noncoherent memory.

Android — touch controls

  • #373 / PR #375 — the on-screen controls hide while a gamepad is connected. A Bluetooth or USB gamepad and the on-screen pad were both live, and the frontend ORs the keyboard profile into the gamepad profile, so one press could drive the car twice. GameActivity now hides the stick and game buttons while Android reports a device that SDL's own Android joystick handler accepts, and shows them again when it drops. A hidden shape claims no pointer, and hiding a held shape releases its key through the existing sink, so no throttle, brake or stick direction stays pressed across the transition. The overlay declines a touch that lands on no shown control only while the pad is hidden, which is what lets Android hand that gesture to SDL's surface underneath. The Menu button stays drawn and tappable while the controls are hidden, because it is the only pointer route into Settings. The launcher gains a Touch controls: Auto / Always / Never button stored in SharedPreferences; Auto is the default, so the reported case is fixed without a configuration change. One limitation: hiding follows controller presence rather than the player-one assignment, because the controller profile is not readable from Java without a new JNI bridge, so a gamepad assigned to player two also hides player one's touch controls on a tablet. Always is the escape hatch, and docs/controllers.md records the trade-off.

Build and patch tooling

  • PR #383 — the dependency patch order now lives in one file. It used to live in five places — CMakeLists.txt, cmake/Frontend.cmake, build.sh, build.ps1, scripts/build_android.py and a regex checker that parsed the others to keep them in sync — and CMake carried about 130 lines of hand-written "recognize the newer patch first" branches, because a later patch that rewrites an earlier patch's lines defeats per-patch git apply --reverse --check. patches/series.txt is now the only list, per dependency and in apply order, with the few Windows- and Android-only patches marked. tools/apply_patches.py is the only applier: it builds each cumulative state of a series in a temporary index, keeps the longest prefix the checkout already contains, and applies the remainder as one diff, so overlapping patches need no special cases and unrelated local edits and the Git index are kept. A fully patched checkout is a 1.7 s no-op. CMake reconfigures when series.txt or a patch changes, so an edited patch is reported instead of silently building stale sources.

Performance, HUD and project documentation

  • #266 / PR #381 — the track visibility row cache was measured and rejected. Issue 266 proposed caching the full-track PVS row per viewport with explicit invalidation, if validation proved cheaper than rebuilding. A temporary probe timed the existing build against a full validation of every input a safe cache hit must re-read, on every viewport walk, across 1P replays on all six circuits, 2P on all six, 4P on circuits 1 to 3 and two windowed D3D12 runs. In the game a rebuild costs 0.3 to 1.3 microseconds on average (2.4 at p95) against 31 to 67 ms frames, and a safe validation costs the same or more because it repeats the per-segment record scan — every would-be hit rebuilt an identical row. By the issue's own rule the simpler uncached builder stays, so track loads, savestate restores, Track Lab corrections and toggle changes need no invalidation. docs/no_lod_audit.md section 14 records the method and results, and the new opt-in lambo_no_lod_row_benchmark measures the per-segment comparison without a ROM.
  • PR #382 — every force stub now records why it is still a stub. lamborghini.syms.toml declares one [[section]] covering all 1047 functions, so rom = vram - 0x80000400 + 0x1000 already holds for every one by construction. Scanning the ROM bytes over the 76 live stubs' declared ranges finds 89 distinct branch targets outside those ranges — 86 land mid-function inside another declared symbol and 3 on another symbol's start — and each failure is a declared size that is too small, which is the split-merge problem scripts/gen_syms_toml.py already records for func_800028D0. The FORCE_STUB comment that carried the earlier "per-overlay sections" claim was what issue 337 was written from, so that comment, force_stub.txt and the docs now say what was measured. No stub is enabled, no symbol or patch changes, and the generated US configuration is byte-for-byte identical. The 75 pending stubs are still stubbed; fixing one needs a SPLIT_MERGES change and a build.
  • PR #385 — the 2-player speed and place text now follow RANK to the right edge. In 2-player mode the ordinary speed and place text stayed in the 4:3 column after RANK had moved to the expanded right edge: each half already pinned RANK, LAP, the alternate dial and the minimap, but the text path was a different branch and had no rect-align bracket. Those two calls are now one pin_right / pin_reset pair per half, the same pair RANK uses. The played result of the new bracket has not been checked in a build.

What's not done yet

This is still pre-1.0 — plenty of force_stub.txt still includes real game primitives
rather than no-ops. The issue tracker is the canonical "what's next" list.

  • The gralloc stall is not explained. The v0.10.2 exports show RT64 Present and HWUI's RenderThread both waiting on a priority-inheritance mutex in Qualcomm gralloc while the swap chain is rebuilt, with no dumped thread holding it. Patch 0046 removed the spin and the stall report can now name the holder, and patch 0049 avoids the format query that may take the lock, but neither change releases the stall itself. The build now writes a gralloc probe line to native-errors.log, so the next export on that device should show whether the lock was taken before setup, during setup, or later.
  • The device matrix is thin. The resolution budget and the frame-matching work were measured on an Adreno 650 phone (Android 13, both the Qualcomm system driver and Turnip) with 1 to 3 runs per configuration. The #329 Xiaomi (Adreno 730, Android 15) was not available, so the gralloc wait on that device is unverified, and rotation and screen off/on were not run.
  • The Adreno 750 system driver still rejects RT64 shaders. #367 stays open: the driver fails compute-pipeline creation and the renderer then binds a null pipeline and crashes. Rebuilding with only the mapped-memory patch removed reproduces the same failure, so #360 does not cause it. The full game with injected validation was not run.
  • Two Vulkan defects are still open. #366 (texture-to-buffer readback dereferences a null destination texture) and #361 (resource ownership across graphics, compute and copy queue families) are unresolved. Issues #369 and #370 were closed as duplicates of them.
  • The launcher verification has not run on a device. assetcheck and gameboot are host-verified and the APK payload is checked at package time, but no device run was performed for that work: adb devices -l listed no connected devices, so no device asset check, launcher-driven soak or visual playthrough was run. The on-device hide/show of the touch controls with a real Bluetooth pad is likewise unverified, as are both landscape rotations and the reconnect path.

If you find a crash, please open an issue with lamborghini_crash style notes:

  • Output window text (or the crash-*.txt dump if the native handler caught it)
  • Commit / build provenance (below)
  • Approximate in-game location (title screen / car-select / race lap 2 / etc.)

How to run

Linux

unzip lamborghini-recomp-linux-x64.zip -d lamborghini-recomp
cp "Automobili Lamborghini (USA).z64" lamborghini-recomp/
./lamborghini-recomp/lamborghini_modern

Requires libsdl2-2.0-0 and a Vulkan-capable GPU + driver at runtime. Most
desktop distros ship these already.

Windows

Extract lamborghini-recomp-windows-x64.zip anywhere, drop your ROM in the same
folder, and double-click lamborghini_modern.exe. The bundled SDL2.dll,
dxcompiler.dll, and dxil.dll must stay alongside the EXE.

F11 / Alt+Enter toggles fullscreen (and remembers the choice next launch).

Android

Install lamborghini-recomp-android-arm64.apk, open the app and import your USA
ROM. Android remains experimental; see the Android guide.

Build provenance

  • Commit: 6a0cbe7 (main, 2026-10-30)
  • Workflow run: #40992918135 (Build & Release, dispatch from this SHA)
  • Toolchain: Linux build on ubuntu-latest with gcc / cmake / ninja /
    libsdl2-dev / libvulkan-dev. Windows build on windows-latest with
    MinGW-w64 GCC + Ninja (PowerShell, not MSYS bash) per the project's toolchain
    notes.
  • Texture packs: unchanged from v0.10.2. The optional packs are published by the
    texture pack project, and version 0.3.0 still applies.

If you'd rather build it yourself from source instead of running a pre-built
binary, see BUILDING.md. For graphics settings, see README.md.

Don't miss a new automobililamborghini-recomp release

NewReleases is sending notifications on new releases.