Make the LLVM backend produce a module that runs - #10
Conversation
Emitting a large DOL through the LLVM backend aborts with STATUS_STACK_OVERFLOW (0xC00000FD) on MSVC builds. The failure lands after every object has already been emitted but before the object manifest is written, so the run looks like it succeeded right up until it disappears and leaves an unusable output directory behind. LLVM's IR builders and pass pipeline recurse in proportion to function size, and MSVC defaults an executable to a 1 MiB stack where the toolchains this backend was developed against default to 8 MiB. Nothing in the backend was wrong; it simply had an eighth of the stack it needed. Raise it for the dolrecomp executable when the LLVM backend is enabled, and only under MSVC, since no other toolchain needs it.
With enough stack to get past object emission, the LLVM backend then dies on: LLVM ERROR: Instruction Combining on func_80064760 did not reach a fixpoint after 1 iterations instcombine's fixpoint verification is a self-diagnostic for the pass, not a correctness property of the IR it produced. Recompiled Gekko functions contain long straight-line integer and condition-flag sequences, and on those the pass can still be making progress when the check runs. Because it reports the mismatch through report_fatal_error, one such function takes down the entire recompilation rather than degrading that function's optimization. Suppress the check, as LLVM's own diagnostic suggests. The optimization itself still runs; only the assertion about converging in a single iteration is dropped.
A recompiled module that reaches a loop containing a call never returns to the dispatcher. It pegs a core and the runtime is never able to advance timing or service the GPU, so the game runs but presents no frames at all. emitBudgetGuard yields once cycles_ reaches 256, which makes cycles_ both the cycle-charging accumulator and the yield scheduler. Every call and helper resume point clears it -- correct for charging, since materialize() has already flushed those cycles into downcount, but it also restarts the yield countdown. A loop whose body crosses a call therefore never accumulates to the threshold. It nests: a small helper called in a loop does not reach 256 within its own frame either, so neither the caller nor the callee ever hands control back. externalDestination already documents this hazard for the unlinked case, where it declines to emit a direct call for exactly this reason. The linked case has the same problem. Track blocks entered since entry in a counter that no resume path clears, and yield when either bound is hit. Cycle accounting is untouched, so nothing is charged twice; only the yield decision now survives a call. Verified on Luigi's Mansion (NTSC-U, GLME01): before, the module loaded and span with frame_count=0; after, it boots to gameplay and renders. Confirmed in the emitted IR that guard_steps carries across call_resume while cycles is still reset there, and that surviving budget_exit blocks rose from 431 to 549 in the first chunk.
The three LLVM tests could not run on a Windows host, so the whole backend was untested there. That is a plausible reason it was broken in three independent ways at once. test_llvm_pipeline included sys/wait.h and used fork, execl and waitpid, none of which exist on Windows, so it did not compile. It now spawns the child with _spawnl and _P_WAIT, which runs it to completion and returns its exit status directly; the override that fork set between fork and exec is instead set in this process, which the child inherits. mkdir(path, 0777) becomes _mkdir(path). Both tests then asserted the emitted object began with the ELF magic. The object format follows the default target triple, so a Windows host emits COFF and the check failed on a perfectly good object. Both now compare against the format the host actually produces, via a helper in the pipeline test since it checks two objects. With this, ctest runs 17/17 on Windows, including llvm_backend, llvm_execute and llvm_pipeline. codegen_compile also passes once the MSVC environment is present.
Configuring with -DDOLRECOMP_ENABLE_LLVM=ON under MSVC fails to link. The prebuilt LLVM Windows release libraries are built against the static CRT (/MT), while CMake defaults these targets to the DLL CRT, so the link ends with unresolved __imp__* CRT symbols -- fseek, ftell, _ftelli64, _mkdir and system -- reported against dr_frontend and dr_platform rather than against anything the caller wrote. Set CMAKE_MSVC_RUNTIME_LIBRARY to match, scoped to this subproject so an embedding build is unaffected. test_rpx needs the opposite treatment when DolRecomp is built as a subdirectory: an embedding project can supply a zlib linked against the DLL CRT, and this test pulls zlib's aligned-allocation path into the executable, so it follows the parent there instead.
|
The LLVM emitter changed, but DOLLLVM_CACHE_VERSION remains dolllvm-v4. Bump the cache version. DolRecomp forces every target to /MT, then changes only test_rpx to /MD when embedded. That executable |
The budget guard gained guard_steps_, so a v4 object yields on the old bound alone. Without the bump a tree that had run the previous backend reuses those objects and shows none of the fix.
LLVMConfig.cmake ends with an unqualified
set(CMAKE_MSVC_RUNTIME_LIBRARY MultiThreaded)
naming the CRT LLVM itself was built against. Discovering LLVM halfway
down the file let that land after dr_cpu, dr_platform, dr_frontend and
dr_ir were already declared, so those four kept the caller's CRT and
everything below took LLVM's. The earlier fix papered over one symptom
by forcing /MT at the top and exempting test_rpx, which put /MT
dr_frontend and a /MD test_rpx in one link: LNK4098, both LIBCMT and
MSVCRT searched, and a binary with two CRTs in it.
Move find_package(LLVM) above every target so its choice covers all of
them, and drop both the forced set() and the test_rpx exemption.
Verified per-target from build.ninja: LLVM=OFF is 23/23 /MD, LLVM=ON is
27/27 /MT standalone and the same via add_subdirectory(). ctest 14/14
with LLVM off, 17/17 with it on.
|
Both fixed. 6.
|
| Configuration | CRT |
|---|---|
DOLRECOMP_ENABLE_LLVM=OFF |
23/23 /MD |
=ON, standalone |
27/27 /MT |
=ON, via add_subdirectory() |
27/27 /MT |
A parent target declared after add_subdirectory stays /MD, so this cannot reach an embedder's own targets. ctest 14/14 with the backend off, 17/17 with it on.
CMakeLists.txt, 34 lines across both commits.
One consequence worth your call
LLVM now names the CRT for the whole subproject, so an embedder passing -DCMAKE_MSVC_RUNTIME_LIBRARY is overridden rather than obeyed. That is the direction that links, since the LLVM libraries are the fixed side, but erroring on the conflict instead is a one-line change if you would rather it be loud.
|
I found a few remaining problems: valid_object_file() still only recognizes ELF. Windows emits COFF, so object caching and The new budget guard doesn’t behave as described. guard_steps_ is incremented only at loop headers, not Lastly, CI still builds with LLVM disabled, so none of the new LLVM or Windows COFF coverage actually runs |
…riple Three findings from review, and the reason none of them were caught. The budget guard read cycles_, which every runtime boundary zeroes. A loop whose body crosses one therefore never reached 256 and ran until the iteration backstop caught it: 2047 iterations with downcount at -6141, 24x past the bound it was meant to enforce. chargeCycles now also feeds guard_cycles_, which no resume point clears, and the guard reads that. Same loop: 86 iterations, downcount -258. guard_steps_ stays, but only as a termination backstop for blocks that cost zero cycles, and its comment no longer claims to count instructions -- the guard runs at loop headers, so it counts iterations. valid_object_file() accepted only ELF, so on Windows it rejected every COFF object the backend had just written, silently disabling the object cache and DOLRECOMP_LLVM_RESUME. It looks like a cold build, not an error. The triple now has one definition, shared by emission, caching and validation, and the effective triple is hashed unconditionally so an ELF cache and a COFF cache cannot share a key -- previously only the environment variable was hashed, and only when set. None of this was covered because CI never passes -DDOLRECOMP_ENABLE_LLVM =ON: the backend, its tests and the object-format handling are built nowhere, so a green run said nothing about them. Adds an LLVM job on Linux and Windows, since an ELF-only build cannot see the COFF bug, and an execution test for a loop crossing a runtime boundary that asserts the dispatcher gets control back. That test fails on the old guard.
|
All three are real. 6bb67ea.
|
| iterations | downcount | |
|---|---|---|
guard reading cycles_ |
2047 | -6141 |
guard reading guard_cycles_ |
86 | -258 |
2047 was the iteration backstop catching it, 24x past the bound it was meant to enforce, exactly as you described. guard_steps_ survives but only as a termination backstop for a loop whose blocks all cost zero cycles (dcbf, icbi, embedded data would leave guard_cycles_ flat), and its comment now says that instead of claiming to be the scheduling bound.
CI
This is the one that matters, and it is the reason the other two existed. The workflow never passed -DDOLRECOMP_ENABLE_LLVM=ON, so the backend, its three tests and all the object-format handling were compiled nowhere. I quoted "ctest 17/17" as evidence in the description above while the job producing that number never built the code under discussion.
Added an LLVM job on both Linux and Windows — both, because an ELF-only build cannot see the COFF bug — and tests/test_llvm_execute.c now drives a loop crossing a runtime boundary and asserts the dispatcher gets control back. I checked it fails on the old guard rather than assuming it would; that is where the 2047/-6141 figures come from.
The bounds it asserts sit between the two rows above rather than pinning 86 exactly, since you flagged the thresholds as untuned and I would rather the test not fight you over retuning them.
Three separate faults stop the LLVM backend getting a retail DOL to a running
module. Two abort recompilation; the third produces a module that loads, pegs a
core, and never presents a frame. One commit each (1–3).
Two further commits (4–5) are about Windows rather than the backend itself: the
LLVM tests could not compile or pass on a Windows host, and the tree does not
link there at all with
-DDOLRECOMP_ENABLE_LLVM=ON. Those are plausibly why thebackend was broken in three independent ways at once — nothing was exercising it
on this platform.
Two later commits answer @siahisaforker's review and are written up in the reply
below rather than here: the object cache version, and a replacement for the CRT
fix in section 5.
Tested on Luigi's Mansion (NTSC-U,
GLME01) — 532,936 instructions, 0 unknownopcodes. Before: recompilation aborted. After: boots into gameplay and renders,
and
ctestruns 17/17 on Windows.This is about the backend working at all. It is not faster than the C backend
— numbers below.
1.
a71fbbf— Raise the MSVC stack for the LLVM backendSymptom: emits all 522 objects, then dies with
STATUS_STACK_OVERFLOW(
0xC00000FD) before writing the manifest. Looks like a run that succeeded andthen vanished.
Cause: LLVM's IR builders and pass pipeline recurse with function size. MSVC
links executables with a 1 MiB stack; the toolchains this backend was developed
against use 8 MiB.
Change:
target_link_options(dolrecomp PRIVATE /STACK:8388608), underMSVConly and only when the LLVM backend is enabled.
CMakeLists.txt, 7 lines.2.
4984518— Stop instcombine's fixpoint check from aborting recompilationSymptom: with enough stack to finish emission, the run dies on
LLVM ERROR: Instruction Combining on func_80064760 did not reach a fixpoint after 1 iterations.Cause: that check is a self-diagnostic for the pass, not a property of the IR.
Recompiled Gekko functions have long straight-line integer and condition-flag
sequences where the pass can still be making progress when the check runs. It
reports via
report_fatal_error, so one function kills the whole recompilation.Change: pass pipeline asks for
instcombine<no-verify-fixpoint>, as thediagnostic itself suggests.
llvm_backend.cpp, 1 line plus a comment.Note: the DolRecomp vendored in
RecompCore-ModernGekkoalready has this, so thetwo trees have diverged on a known crash.
3.
6f23363— Guarantee the dispatcher regains control across callsSymptom: this is the 0 FPS one. Module loads, reports its entry point, then
pegs a core forever with
frame_count=0andpresent_count=0. The runtime neverregains control, so it can never advance timing or service the GPU.
Cause:
emitBudgetGuardyields oncecycles_hits 256, makingcycles_boththe cycle-charging accumulator and the yield scheduler. Every call and helper
resume point clears it — eight sites. Clearing is correct for charging, since
materialize()already flushed those cycles intodowncount, but it alsorestarts the yield countdown. So a loop whose body crosses a call never reaches
the threshold. It nests: a helper called in a loop does not reach 256 in its own
frame either, so neither caller nor callee ever hands control back.
externalDestinationalready documents this for the unlinked case, where itdeclines to emit a direct call for exactly this reason. The linked case has the
same problem.
Change: a
guard_steps_counter, incremented per block, that no resume pathclears; the guard yields on either bound.
Cycle accounting untouched, so nothing is charged twice — only the yield decision
survives a call.
llvm_function_emitter.{cpp,h}, 22 lines.4.
2090d02— Let the LLVM tests build and run on Windowstest_llvm_pipeline.cusedfork/execl/waitpidviasys/wait.h, and bothit and
test_llvm_backend.cppasserted the emitted objects begin with the ELFmagic. Neither holds on Windows: there is no
sys/wait.h, and the emitted objectformat follows the default target triple, so a Windows host produces COFF.
_spawnlwith_P_WAITreplaces the fork/exec pair — it runs the child tocompletion and returns its exit status directly, and the child inherits this
process's environment, so the chunk-size override is set before the call rather
than between fork and exec. The magic check becomes an
is_native_object()helper that looks for
IMAGE_FILE_MACHINE_AMD64(0x64 0x86, little-endian) onWindows and ELF elsewhere. POSIX paths are unchanged.
tests/test_llvm_pipeline.candtests/test_llvm_backend.cpp, 42 lines.5.
a1f1d5e— Match the static CRT when linking against the prebuilt LLVM librariesConfiguring with
-DDOLRECOMP_ENABLE_LLVM=ONunder MSVC does not link. Theprebuilt LLVM Windows release libraries are built against the static CRT (
/MT)while CMake defaults these targets to the DLL CRT, so the link ends in five
unresolved
__imp__*CRT symbols —fseek,ftell,_ftelli64,_mkdir,system— reported againstdr_frontendanddr_platformrather than againstanything the caller wrote.
This commit forced
/MTat the top of the file and gavetest_rpxthe oppositetreatment when DolRecomp is built as a subdirectory. That was wrong, and
e5ba721replaces it — see the reply below. Kept in the history rather thansquashed because the review comment that found it is worth reading against the
code it describes.
Verification
cmakeconfigure + link (MSVC)__imp__*--backend=llvmexit0xC00000FDframe_countat 4 samplesctestRenders correctly rather than merely presenting: mansion exterior, file-select
screen, and in-game Foyer with Luigi's flashlight lighting the floor. Loads
savestates. In the IR,
guard_stepscarries acrosscall_resumewhilecyclesis still reset there, and surviving
budget_exitblocks in chunk 0 go 431 → 549.The "after" column was re-measured on this branch's actual base (
93b881c)rather than carried over from where the work started. That matters here: the
tree I originally developed against turned out to predate
93b881c, so everynumber was re-taken rather than assumed to still hold. The build was also
configured without
-DCMAKE_MSVC_RUNTIME_LIBRARYon the command line, so thelink result tests commit 5 instead of masking it.
Performance
Both backends built from this branch, same module flags (
-O3 -march=native),same savestate, frame limiter disabled so the numbers measure headroom rather
than saturating at the 100% cap:
-O3LLVM runs at about 43% of the C backend's throughput, from a module 11x the
size. In practice that is the difference between holding the game's 30 FPS cap
with room to spare and running at roughly 22.
That gap is much wider than it used to be, and not because the LLVM backend
regressed —
93b881croughly doubled the C backend. Measured against thepre-
93b881ctree the two were within a few points of each other, which iswhat an earlier revision of this section reported. The C-side CFG work simply
left the LLVM path behind.
A measurement note that cost me some bad numbers, in case it saves you the same:
this module needs a long warmup. At 15 seconds three runs gave 70.2%, 73.8% and
165.2%; at 45 seconds five consecutive runs gave 73.1–74.0% with an SD of 0.3%.
The 165.2% was never reproduced and appears to have been an artifact. Short
warmups on a 541 MB module produce numbers that look plausible individually and
are not stable.
Raising codegen off the hardcoded
createTargetMachine(triple, "generic", "")tothe host CPU made no measurable difference, so it is deliberately not in this PR.
None of this is an argument for the LLVM backend on speed. It is slower, larger,
and these commits do not change that — they only make it build and run at all on
Windows, which is the claim this PR is making.
One note, not fixed here
DOLRECOMP_LLVM_OPT_LEVEL, so builds differing only in opt level silently reuseeach other's objects. That gave me a mixed-optimization module before I noticed.
Separate from the version bump in commit 6, which only invalidates across
emitter changes.
Environment
MSVC 14.50.35717 (VS 18), Ninja, Release, LLVM 20.1.8, Windows 11. No
-DCMAKE_MSVC_RUNTIME_LIBRARYon the command line — commit 5 makes thatunnecessary, and the tree was configured without it deliberately so the build
result tests that commit rather than masking it. Clean configure, full build,
ctest17/17./STACK:8388608matches other toolchains' default. The 2048-block bound sitsalongside the existing 256-cycle one and is untuned. Both open to your preference.