Skip to content

LLVM backend: Colosseum does not get through boot (x86-64 and aarch64; C backend does) #20

Description

@dougchansan

Correction (updated): I originally filed this as AArch64-specific. That was wrong. The failure reproduces on x86-64 with the same DOL and the same harness, so this is a Colosseum + LLVM-backend defect, not an ARM one. Details and measurements below.

Building Pokémon Colosseum (GC6E01) with the LLVM backend produces a module that does not get through the boot sequence. The C backend, same DOL, same runtime, same automation harness, boots normally.

Measurements

Cold boot, A-press driver, no savestate, identical harness on each platform:

platform backend frames produced fps speed
x86-64 Windows C 4262 44.77 0.749
x86-64 Windows LLVM 16 0.26 1.00
aarch64 (Pi 4) C 1417 9.74 0.166
aarch64 (Pi 4) LLVM 8 5.74 (stale) 0.634

The guest is not deadlocked — it executes (speed is nonzero, the VI keeps ticking), it just never makes forward progress. Note the LLVM arm reports a higher speed than C on the Pi while producing no frames: it is spinning cheaply instead of doing work.

Luigi's Mansion (GLME01) with the LLVM backend runs fine on the same Pi (769 frames, 6.11 fps), so the AArch64 LLVM backend is not broken in general.

Where it ends up

perf on the wedged aarch64 run puts ~58% of samples in three guest functions, which the DOL disassembly identifies as the thread scheduler:

  • 0x8009BBE0OSSaveContext (saves GQR1–5 via mfspr 0x393..0x397, CR, LR, MSR, CTR, XER)
  • 0x8009BC50OSLoadContext, which resumes a thread with rfi
  • 0x800A17E0 — run-queue enqueue / reschedule

None of these appear in the C backend's profile, which is dominated by real game work (func_800FD5E0, paired-single ops, FP helpers).

What is NOT the cause

I instrumented the shared runtime to rule these out rather than guess:

  • Thread resume is correct. ppc_rfi fires ~150M times in 70s (vs ~150k for C, ~1000x more), and at every sampled resume CPUState is correct: srr0=0x800A183C, r3=0x00000001. 0x800A183C is the instruction after bl OSSaveContext, and r3=1 is exactly the "resumed" return value the scheduler tests. The C backend resumes at the same address with the same value, so this churn is a normal idle loop — the LLVM arm is just doing ~1000x more of it.
  • Entry at that PC is valid. In the generated IR the address is a real entry case (guest_800A183C_b23), reached from native_entry, and the block loads r3 from CPUState and branches on icmp eq i32 %state2.7, 0 correctly.
  • Not an unsupported instruction. All 1643 fallbacks in the generated CSV are embedded-data; there are no unmodeled opcodes.
  • Not exception delivery. Exception counts stayed under threshold in both arms.
  • Not our open PRs. Regenerating with Register only the LLVM targets this build can emit for, and build the bench on Windows #17/Route fallback blocks through per-edge trampolines so the phi matches the CFG #18/Key the object cache on the sources that generate the objects #19 applied produces byte-identical objects (4890/4890 identical, 0 changed) and an identical module hash, so those fixes neither cause nor fix this.

Still open

I have not isolated the primitive that diverges. The dispatch-address stream diverges from the C backend within the first handful of dispatches at boot, and the LLVM arm reaches far less distinct guest code over a run (909 vs 4000+ distinct addresses in the same window).

Reproduction is easy on x86-64 now, so this no longer needs ARM hardware to chase. Happy to keep digging or hand over what I have.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Status
    Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions