Skip to content

Instantly share code, notes, and snippets.

@unxmaal
Created April 8, 2026 21:28
Show Gist options
  • Select an option

  • Save unxmaal/4ccf2d7ed7cac57d0274198384329e35 to your computer and use it in GitHub Desktop.

Select an option

Save unxmaal/4ccf2d7ed7cac57d0274198384329e35 to your computer and use it in GitHub Desktop.

JIT Desktop Crash — Step-Mapping Analysis

Context

What works: Interpreter-only boots IRIX to full working desktop. What's broken: ANY JIT (even ALU-only, even MAX_BLOCK_LEN=1) breaks the desktop after login. What's shared: Both paths call exec.step() for the interpreter burst. The JIT dispatch adds probe/translate/compile overhead between bursts.

Phase A: Known Working Path (BASELINE)

The interpreter-only loop. One iteration = 10,000 steps + flush.

# Step Code Location Status
1 running.load check dispatch.rs:583 CONFIRMED
2 for _ in 0..1000: exec.step() x10 dispatch.rs:580-596 CONFIRMED
2a ...step() increments local_cycles mips_exec.rs:860 CONFIRMED
2b ...step() advances cp0_count mips_exec.rs:881-882 CONFIRMED
2c ...step() checks cp0_compare crossover mips_exec.rs:883-886 CONFIRMED
2d ...step() loads pending interrupts (atomic) mips_exec.rs:870 CONFIRMED
2e ...step() merges IP bits, checks IE/EXL/ERL mips_exec.rs:888-908 CONFIRMED
2f ...step() calls fetch_instr (nanotlb_translate + L1-I) mips_exec.rs:911 CONFIRMED
2g ...step() decodes + executes instruction mips_exec.rs:912-929 CONFIRMED
2h ...step() handles delay slot state machine mips_exec.rs:4197-4230 CONFIRMED
3 flush_cycles() dispatch.rs:598 CONFIRMED
4 Loop back to 1 CONFIRMED

Key property: Steps 2a-2h happen for EVERY instruction. No gaps. No overhead between instructions.

Phase B: Broken Path (JIT dispatch)

One inner-loop iteration = interpreter burst + probe overhead.

# Step Code Location Status
1 probe.next_interval() dispatch.rs:190 CONFIRMED (LFSR advance only)
2 Interpreter burst: exec.step() x burst dispatch.rs:193-207 CONFIRMED (same as baseline)
3 steps_in_batch += burst dispatch.rs:208 CONFIRMED (bookkeeping only)
4 running.load check dispatch.rs:211 CONFIRMED
5 Read (pc, in_delay_slot) from executor dispatch.rs:214-218 CONFIRMED (read-only)
6 PROM/exc/delay_slot exclusion check dispatch.rs:220-225 CONFIRMED (read-only)
7 translate_pc(exec, pc) dispatch.rs:227-233 SEE BELOW
8 cache.lookup(phys_pc) dispatch.rs:235 CONFIRMED (read-only HashMap)
9a ON HIT: continue (diagnostic skip) dispatch.rs:237-243 CONFIRMED
9b ON MISS: trace_block + compile dispatch.rs:467-484 SEE BELOW
10 Loop back to 1 CONFIRMED

Step 7: translate_pc deep trace

# Step Code Location State mutation?
7.1 Call exec.debug_translate(virt_pc) dispatch.rs:617
7.2 translate_impl::<true>(addr, AccessType::Debug) mips_exec.rs:1392
7.3 Force privilege = Kernel mips_exec.rs:1174-1175 NO
7.4 KSEG0 (0x80-0x9F): phys = virt & 0x1FFFFFFF mips_exec.rs:1225 NO
7.5 KUSEG (0x00-0x7F): tlb_translate_impl::<true> mips_exec.rs:1219 NO (DEBUG=true skips all CP0 writes)
7.6 TLB lookup (self.tlb.translate) mips_exec.rs:1360 Benign (see resolution below)
7.7 On miss: returns exc, no CP0 writes mips_exec.rs:1374 NO

Step 9b: Cache miss path (trace_block + compile)

# Step Code Location State mutation?
9b.1 trace_block(exec, pc, Alu) dispatch.rs:471
9b.2 debug_fetch_instr(pc) dispatch.rs:633, mips_exec.rs:1435
9b.3 fetch_instr_impl::<true>(addr) mips_exec.rs:1436
9b.4 translate_impl::<true>(addr, Fetch) mips_exec.rs:1457 NO (DEBUG)
9b.5 cache.fetch(virt, phys) mips_exec.rs:1469 YES: L1-I CACHE FILL
9b.6 On L1-I miss: fill_l1i_line mips_cache_v2.rs:1150-1201 YES: L1-I tag+data, L2 fill
9b.7 decode_into dispatch.rs:637 NO (writes to local DecodedInstr)
9b.8 compiler.compile_block dispatch.rs:472 NO (Cranelift codegen, not CPU state)
9b.9 cache.insert dispatch.rs:474 NO (JIT code cache, not CPU state)

Step 9b.5-9b.6: L1-I cache fill during trace_block.

This is a REAL state mutation. When the JIT dispatch traces a block on cache miss, debug_fetch_instr goes through the instruction cache. If the L1-I line isn't cached, it fills it. This means:

  • The JIT dispatch is FILLING L1-I cache lines at times the interpreter wouldn't
  • L1-I is a set-associative cache with LRU eviction
  • Extra fills could EVICT lines the interpreter would have kept
  • This changes the instruction cache hit/miss pattern for subsequent interpreter steps

Phase C: The Dark Zone

CONFIRMED -> CONFIRMED -> DARK -> SYMPTOM
     |          |          |       |
  interp     probe    tlb.translate   4dwm crashes
  burst    overhead   + L1-I fill     after login

Dark territory (2 items):

  1. tlb.translate() internal mutation (step 7.6)

    • Does the TLB lookup itself modify any internal state?
    • E.g., LRU tracking, reference bits, statistics counters?
    • If the TLB has side effects on read, debug_translate is NOT side-effect-free
  2. L1-I cache fill during trace_block (step 9b.5-9b.6)

    • On every cache MISS, trace_block fills L1-I lines
    • This happens OUTSIDE the normal interpreter flow
    • Could evict hot lines, changing timing and behavior
    • Could cause the instruction cache to be in a different state than the interpreter expects

Dark Zone Resolution

tlb.translate() -- CONFIRMED BENIGN

The TLB has per-access-type MRU lists (4 lists: Fetch, Read, Write, Debug). mru_promote() reorders the linked list to put the matched entry first. debug_translate uses AccessType::Debug (list 3), which is INDEPENDENT from the interpreter's AccessType::Fetch (list 0). The MRU mutation doesn't affect translation results, only lookup speed in the Debug list. Status: MAPPED, not the bug.

L1-I cache fill during trace_block -- STILL SUSPECT

On every cache miss, trace_block calls debug_fetch_instr which goes through cache.fetch(). On L1-I miss, fill_l1i_line is called, which:

  1. Invalidates the existing L1-I line at that index
  2. May fill from L2 or from the bus
  3. Writes L1-I tag + data arrays

This happens OUTSIDE the normal interpreter execution flow. It's extra L1-I cache activity that the interpreter-only path never performs. Could evict hot lines and change the interpreter's cache behavior.

Current Test: Execution-disabled diagnostic

A test is currently running with JIT block execution completely disabled (cache hits just continue). The dispatch loop still probes, translates, traces, and compiles -- but never runs compiled code.

If desktop STILL breaks: Bug is in dispatch overhead:

  • L1-I cache pollution from trace_block (mechanism 2)
  • Some other side effect we haven't identified
  • Next step: disable trace_block too (just probe + translate, no compile)

If desktop WORKS: Bug is in sync_from/sync_to or compiled code:

  • Re-examine sync_to clearing in_delay_slot
  • Check if compiled code has a rare codegen bug not caught by unit tests
  • Next step: re-enable execution, add per-block logging of entry/exit state

Instrumentation Plan (if dispatch overhead is the culprit)

One rebuild to cover all remaining dark spots:

  1. Add counter: L1-I fills from trace_block vs from normal step()
  2. Add counter: cache.fetch() calls from trace_block vs from step()
  3. Log PC address space (KUSEG/KSEG0) at each probe point
  4. If L1-I fills are significant: skip cache.fetch in debug_fetch_instr by using a separate read path that doesn't touch L1-I
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment