Wine patches that make Le Mans Ultimate run under Proton, plus a script that builds a ready-to-use Steam compatibility tool with them applied.
Four problems are fixed:
- The game crashes on race load. Wine's
d2d1returnsE_NOTIMPLfromCreateSpriteBatchwithout writing the out-parameter; LMU doesn't check the HRESULT and dereferences NULL, giving anEXCEPTION_ACCESS_VIOLATION(write to address 0). Fixing this needs a realID2D1SpriteBatch, and — for the in-game UI to actually render rather than come out garbled — Layers and Tint too. - A wheel unplugged mid-session never comes back. Wine's
IDirectInput7::FindDeviceis a stub that returnsDI_OKwithout writing*pguidInstance, so LMU passes uninitialised stack memory toCreateDevice. - VR never initialises (LMU 1.4). The game loads a
.curthroughD3DXCreateTextureFromFileInMemory, and wine'sd3dx9_36doesn't recognise the ICO/CUR container at all. - Multiplayer join fails (LMU 1.4). The join handshake derives its session key
with
BCryptDeriveKey(BCRYPT_KDF_HASH, …)and aKDF_SECRET_PREPENDbuffer; wine hashed only the shared secret and ignored the prepend/append buffers, so the key came out wrong.
The series started from JacKeTUs/wine
(lmu-d2d1-hid, based on GE-Proton10-34), rebased onto GE-Proton11-3 and
proton-cachyos, with the upstreamed patches dropped and the hotplug fix rewritten.
git clone git@github.com:Pipodi/proton-LMU-patch.git
cd proton-LMU-patch
chmod +x build.sh
./build.sh cachyos # or: ./build.sh (GE, the default)The script prints the exact install command when it finishes. Restart Steam, then pick the new tool in LMU's compatibility settings.
Which base should I pick? cachyos is the more thoroughly tested of the two
— see Status. Pick ge if you already run GE-Proton for everything else.
gitpodmanordocker(the build runs inside the steamrt SDK image)make- ~60 GB of free disk space
- Roughly 15–30 minutes, mostly compiling
Two bases are supported: ge (proton-ge-custom,
the default) and cachyos (proton-cachyos).
./build.sh # ge, newest GE-Proton tag on the remote
./build.sh ge GE-Proton11-3 # a specific GE tag
./build.sh cachyos # newest proton-cachyos release branch
./build.sh cachyos cachyos_11.0_20260713/main
./build.sh GE-Proton11-3 # variant inferred from the tag name
./build.sh --check-only # sanity checks only, change nothing
./build.sh -f ge master # force a fresh re-cloneWith no version given, both variants resolve the newest release live: the highest
GE-Proton<major>-<minor> tag for ge, and — since CachyOS cuts a branch per
release instead of tagging — the newest cachyos_*/main branch for cachyos. …/main is the Steam Linux
Runtime flavour (folder suffix -slr), …/main_native the native one (-native).
Other flags: --install (install into Steam after building), --install-only
(install an already-built package and do nothing else), --skip-patch (patch
layer already applied), --no-clean (reuse the build dir), -y (don't prompt
before deleting it).
The build ends with:
GE-Proton11-3-build/GE-Proton11-3-LMU.tar.gz # ge
proton-cachyos-11.0-20260713-slr-build/proton-cachyos-…-slr-LMU.tar.xz # cachyos
Install it with:
./build.sh cachyos --install-only # install what is already built
./build.sh cachyos --install # or build and install in one go--install-only skips the source tree entirely — no clone, no submodule update,
no patching — so it is also the quickest way to reinstall after a Steam reset.
What it does: verifies the tarball against its .sha512sum, unpacks it to a
staging directory beside the target and swaps it in (an interrupted install
can't leave a half-written tool that Steam still lists), removes any previous
install of the same name — chmod -R u+w first, since make redist ships the
tree mode 555 — then greps the installed DLLs for d2d_sprite_batch_create,
hid_joystick_find_device and ico_entry to prove the patches survived
packaging. It warns if Steam is running.
By hand, if you prefer:
mkdir -p ~/.local/share/Steam/compatibilitytools.d/GE-Proton11-3-LMU
tar -xf GE-Proton11-3-LMU.tar.gz \
-C ~/.local/share/Steam/compatibilitytools.d/GE-Proton11-3-LMU --strip-components=1Don't use Proton's own make install. Its install target depends on all,
so it re-enters the build on the host, outside the steamrt container the build
actually ran in. On any distro whose ffmpeg is newer than the runtime's, winedmo
then fails to compile (unknown type name 'AVBSFInternal') and nothing is
installed. build.sh no longer suggests it.
Proton's Makefile defaults STEAM_DIR to ~/.steam/root, which is usually a
symlink to the real library but not always — build.sh detects the actual
location ($STEAM_DIR from the environment wins, then ~/.local/share/Steam,
~/.steam/root, ~/.steam/steam, the flatpak path). Override it if the guess
is wrong:
STEAM_DIR=/mnt/games/Steam ./build.sh cachyos --install-onlyRestart Steam, then select the tool in LMU's compatibility settings.
| Patch | Purpose |
|---|---|
| 0001 | ID2D1SpriteBatch implementation — the crash fix |
| 0002 | Tint effect stub, effect_id tracking |
| 0003 | debug_d2d_matrix3x2_f tracing helper |
| 0004 | Move clip stack onto the bitmap |
| 0005 | PushLayer/PopLayer implementation — without it the UI renders garbled |
| 0006 | Draw a Tint effect's input bitmap in DrawImage; sprite tint in the pixel shader |
| 0007 | Derive guidInstance from crc32(names)+serial+VID/PID so wheel bindings persist |
| 0008 | Build fix |
| 0009 | IDirectInput7::FindDevice — first cut, superseded by 0011 |
| 0010 | Hoist declarations (-Werror=declaration-after-statement) |
| 0011 | IDirectInput7::FindDevice via the joystick cache — wheel hotplug |
| 0012 | ICO/CUR loading in d3dx9_36 — LMU 1.4 VR init |
| 0013 | BCryptDeriveKey prepend/append buffers — LMU 1.4 multiplayer join |
That is the GE series (13 patches) in patches/wine/lmu/. The cachyos series
(14 patches) in patches/wine/lmu-cachyos/ carries the same fixes in a different
order and split, because cachyos' wine is Valve's rather than GE's and its d2d1 has
diverged. Each directory has its own README with the full per-patch detail,
including every rebase conflict and how it was resolved:
patches/wine/lmu/README.md— GE seriespatches/wine/lmu-cachyos/README.md— cachyos series
| proton-ge-custom | proton-cachyos | |
|---|---|---|
| LMU series | git am into wine/ |
git am into wine/ |
| base patch layer | protonprep-valve-staging.sh |
none — its Makefile applies patches/<module>/*.patch at build time |
| archive | .tar.gz |
.tar.xz |
Both bases end up with the series as commits in wine/, not in the working
tree: GE's prep script does git reset --hard HEAD (uncommitted work would be
lost), and CachyOS rsyncs wine/ minus .git into the build dir, so commits
simply carry over.
The series is authored against one specific wine commit, recorded in
patches/wine/<series>/BASE. When a new GE tag or cachyos branch moves wine on,
build.sh tries three things in order and only bothers you if all three fail:
git am— exact context. Tolerant of hunks having shifted, so most releases land here.git am -3— three-way merge. Rescues drift in the context around a hunk, which plainamrejects.- replay at
BASE, thengit rebase --ontothe new wine — a full three-way merge with every blob available. This is the one that survives real churn, and it is what upstream reworking nearby code needs.
If the rebase conflicts, upstream has changed the same lines the series changes and a human has to decide what the merged result should be. The script restores wine to where it was and prints the exact commands to redo the rebase by hand.
When step 2 or 3 does the work, the build is carrying a series that is not
what is checked in. Re-export it and update BASE:
git -C <version>-src/wine format-patch -<N> --no-signature -o patches/wine/<series>Then commit the refreshed patches. A PR doing exactly that for a new base is the most useful contribution there is.
Each series is rebased onto a specific wine. Drop a rebased series into an override directory and the script picks it up automatically, most specific first:
patches/wine/lmu-<version-id>/ e.g. lmu-proton-cachyos-11.0-20260713-slr/
patches/wine/lmu-<variant>/ e.g. lmu-cachyos/
patches/wine/lmu/ the GE series (default)
Versioned folders, so multiple builds coexist:
./
├── build.sh
├── patches/wine/lmu/ # GE series
├── patches/wine/lmu-cachyos/ # cachyos series
├── GE-Proton11-3-src/ # cloned source tree
├── GE-Proton11-3-build/ # build dir + final package
└── proton-cachyos-11.0-20260713-slr-build/ # …one pair per variant+version
Both bases build and package cleanly. What has actually been run differs, so here is exactly what is known, as of 2026-07-31:
| proton-cachyos | proton-ge-custom | |
|---|---|---|
| base tested | cachyos_11.0_20260713/main |
GE-Proton11-3 |
| series applies | 14/14, clean | 13/13, clean |
| builds | yes (12m56s) | yes (2m39s warm, 14m32s cold) |
| fixes present in shipped DLLs | yes | yes |
| launches, loads onto track | yes | yes |
| in-game UI renders correctly | yes | yes |
| wheel reconnect mid-session | yes | yes |
| multiplayer join (LMU 1.4) | yes | yes |
| VR initialises (LMU 1.4) | not tested | not tested |
The two LMU 1.4 patches were added on 2026-07-31 and both bases were rebuilt with them.
Multiplayer join is confirmed in-game on both bases — the bcrypt
prepend/append fix (GE 0013, cachyos 0014) does what it is meant to.
VR initialisation is not tested, and will not be tested here — there is no
VR headset on this end. The d3dx9_36 ICO/CUR patch (GE 0012, cachyos 0013) is
build-verified only: the new code is in the shipped d3dx9_36.dll on both
architectures for both bases, with no warning from d3dx_helpers.c in either
log, but nothing has ever exercised the ICO/CUR path at runtime. It is carried
because upstream ships it for LMU 1.4 and it cannot affect the non-VR paths, not
because it is known to work.
If you have a headset, this is the one thing the project cannot check itself. A yes or a no is equally useful — see Contributing.
The rows above it were measured with the series that predates these two patches; 0001–0011 are unchanged by this update.
"Fixes present in shipped DLLs" means the built d2d1.dll contains
d2d_sprite_batch_create and the Layers strings, and dinput.dll / dinput8.dll
(both architectures) contain hid_joystick_find_device with no FindDevice
stub left. That proves the patches compiled into the package — not that they
behave correctly.
Both hotplug verifications are WINEDEBUG=+dinput captures of a Simagic wheel
unplugged and reconnected while on track:
hid_joystick_find_device ... matched instance {ae42ba3a-804c-11f1-8001-000044455354}
dinput8_a_CreateDevice ... guid {ae42ba3a-804c-11f1-8001-000044455354}
hid_joystick_device_try_open detected device type 0x10214
dinput_device_Acquire
hid_joystick_send_force_feedback_command ... command 0x1
Force feedback resumes, and get_instance_from_guid ... not found — the failure
signature of the old stub — appears zero times in the whole log.
Both bases resolve the wheel to the same guidInstance, which is what patch
0007/0005 is for — existing bindings survive a switch between the two tools.
Neither log contains an unhandled exception, a CreateSpriteBatch fixme or any
d2d1 error, and both shut down cleanly.
Reports welcome, especially from wheels other than a Simagic base: different hardware exercises different HID report descriptors.
Present on both bases, none of it caused by these patches:
warn:dinput:hid_joystick_find_device ... PID_0001&MI_00 ... not found, once per reconnect. LMU also asks about the wheelbase's second HID interface, which is not a joystick and legitimately is not in the cache.DIERR_DEVICENOTREGis the right answer and the game drops it. Under the old stub this returnedDI_OKwith a garbage GUID.warn:dinput:dinput_device_init_user_format ... not found— the game asks for device objects this wheel does not have. Upstream behaviour.warn:dinput:hid_joystick_read HidP_GetUsagesEx returned 0xc0110004on that same secondary interface, andfixme:dinput:hid_joystick_device_try_open device usage ff00:0001 not implemented!for its vendor-defined usage page.- One
err:sync:RtlpWaitForCriticalSection ... wait timed outwas seen in the GE run, in the force-feedback effect-download path and well before the unplug. It appeared once in 580k lines and the game recovered. Worth reporting if you see it repeatedly or paired with a visible stall.
The series won't apply. build.sh already tried an exact apply, a three-way
merge and a rebase from the recorded BASE before giving up, so this means
upstream changed the same lines the series changes. The error prints the commands
to redo the rebase by hand; resolve the conflicts, re-export with
git format-patch and update BASE. See
Keeping up with new releases.
The clone was interrupted. Re-run the build; since v2.2 the script repairs this itself. An aborted clone leaves two states that look fine but are not, and both used to surface much later as something unrelated:
wine/parked on the upstream branch tip instead of the commit the superproject pins, because the submodule checkout never finished. The script used to preserve that HEAD on every later run, replaying the series onto a diverged base — which fails injoystick_hid.cand reads as the patches having gone stale. It now restores wine's HEAD only when that HEAD actually contains the pinned commit, and parks anything else on anlmu-rescue/<timestamp>branch first.- Submodules registered at the right commit with an empty working tree.
git submodule updateconsiders them up to date and does nothing, so this never self-healed; the only symptom wasNo rule to make target '.../configure.ac'. They are now repopulated automatically with a forced checkout from the submodule's own object store.
Clone failures during the submodule step are retried three times — sourceware.org
rate-limits bzip2 with HTTP 429 fairly often.
The UI is garbled but the game runs. Layers didn't apply — patches 0004–0006
on GE, 0008–0011 on cachyos. Check they are in git -C <version>-src/wine log.
A wheel replugged mid-session isn't detected. Capture a log — set LMU's Steam
launch options to WINEDEBUG=+dinput PROTON_LOG=1 %command%, reproduce, then:
grep -E "FindDevice|matched instance|get_instance_from_guid" ~/steam-2399420.logWorking looks like dinput7_FindDevice → hid_joystick_find_device ... matched instance {…} → CreateDevice with that same GUID. A not found warning for the
wheel itself, or a get_instance_from_guid ... not found, means the fix isn't
doing its job — please open an issue with that excerpt.
GE build dies fetching xrandr-1.5.4.tar.xz with ERROR: The certificate of 'xorg.freedesktop.org' has expired. That is an upstream x.org problem, not this
repo's — proton-ge-custom's Makefile downloads xrandr from www.x.org and wget
refuses the expired certificate. Pre-seed the file and the Makefile skips the
download:
cd GE-Proton11-3-src/contrib
curl -kLO https://www.x.org/releases/individual/app/xrandr-1.5.4.tar.xz
sha512sum xrandr-1.5.4.tar.xz # must be 2758615a3d212cc5...379da5e5-k skips the certificate check, so verify the hash — it is the one Alpine
publishes for this release, fetched over a valid connection. Do not skip that step.
Build fails with configure: No such file or directory. Stale stamps from
--no-clean. Rebuild without it.
Issues and PRs welcome. Useful things to report:
- Whether VR initialises. Top of the list: there is no headset here, so the
d3dx9_36ICO/CUR patch (GE 0012, cachyos 0013) has never been exercised at runtime by anyone. "VR works" or "VR still fails" both close a real gap. If it fails, aPROTON_LOG=1log around the point it gives up is what's needed. - A
WINEDEBUG=+dinput PROTON_LOG=1log for any input problem. - Whether the series applies to a newer GE tag or cachyos branch — and the rebased patches if you did the work.
- Wheels other than the ones listed in Status. Different hardware exercises different HID report descriptors.
When rebasing, export your work to patch files in the right patches/wine/…
directory before rebuilding — build.sh applies the patch files, not whatever
happens to be in wine/.
- JacKeTUs — the original
lmu-d2d1-hidseries. - GloriousEggroll — proton-ge-custom.
- CachyOS — proton-cachyos.
- Structure inspired by proton-LinUwUx-patch.
The patches modify Wine and are therefore under Wine's LGPL-2.1-or-later.
See LICENSE.