From 1c56a8b78ca18afd094fe6af51cbf05de037dfe3 Mon Sep 17 00:00:00 2001 From: Shokunin <30949000+tetrisgm@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:53:45 -0700 Subject: [PATCH] segment_testrom: line-align the probe call sites so the T2 deltas compare With the delay-slot fix the Beetle deltas were sane but not comparable: the three unrolled call sequences sat at different I-cache line offsets, and probe_run shared a line with seeded, so the KUSEG run found its first line already filled. Beetle read 59/59/89 (KUSEG/KSEG0/KSEG1), where the fetch model predicts a 23-cycle KSEG1 surcharge. Pad each call sequence so its first T2 read starts a line, and start probe_run on its own line. Beetle (disc boot, OpenBIOS and SCPH-1001 alike) now reads 56/56/82: KUSEG equals KSEG0, and KSEG1 exceeds them by 26 cycles, exactly the Beetle fetch-model difference for the 15 KSEG1 fetches (60 vs a cold cached 34). probe_run moves to 0x000100F0. Ledger ids are unchanged; fetch-tag-segment counts 30 tags and alias-fetch-coherence reads 22 vs Beetle 11, because leaf now straddles two lines. Co-Authored-By: Claude Opus 5.5 --- tools/segment_testrom/gen_segment_exe.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/tools/segment_testrom/gen_segment_exe.py b/tools/segment_testrom/gen_segment_exe.py index c701636a..685cac41 100644 --- a/tools/segment_testrom/gen_segment_exe.py +++ b/tools/segment_testrom/gen_segment_exe.py @@ -31,8 +31,16 @@ image, so the stores never touch code pages. Timer 2 is put in mode 0 which is the same for all three runs. Each delta is (after - before) & 0xFFFF, because T2 is a 16-bit counter that wraps. The subtraction waits one `nop` after the second `lw`: it would otherwise sit in the load delay slot and read -the stale register. The program ends in a `j .` spin, where an oracle run can -read R. +the stale register. + +The deltas compare like with like. Each of the three call sequences starts +on an I-cache line, so the main-loop fetches between the two T2 reads cost +the same in every run. `probe_run` starts its own line, so no earlier code +(`seeded`) leaves one of its lines filled. Beetle tags lines with the full +virtual address, so every run starts with the probe body cold. The KSEG0 and +KUSEG deltas are then equal, and the KSEG1 delta exceeds them by the uncached +fetch surcharge alone. The program ends in a `j .` spin, where an oracle run +can read R. The MIPS is hand-encoded, so the output needs no toolchain and is reproducible byte for byte. recompiler/tests/test_segment_aware_codegen.py @@ -99,6 +107,11 @@ class Asm: def emit(self, *words): self.words.extend(words) + def line_up(self, before=0): + """Pad with nops until here() + before starts a 16-byte I-cache line.""" + while (self.here() + before) & 0xF: + self.words.append(nop()) + def jal(self, name): self.fixups.append((len(self.words), "jal", name)) self.words.append(0) @@ -150,6 +163,7 @@ def _assemble(): a.la("t0", "seeded", KUSEG) a.emit(jalr("t0"), nop(), sw("v0", 0x08, "s0")) for n, segment in enumerate((KUSEG, KSEG0, KSEG1)): + a.line_up(8) # each `lw t1` starts a line a.la("t0", "probe_run", segment) # T2 before and after the call. The nop keeps subu out of the second # lw's load delay slot; T2 is 16 bits, so mask the difference. @@ -165,6 +179,7 @@ def _assemble(): a.label("seeded") # entered through a register a.emit(jr("ra"), or_("v0", "ra", "zero")) + a.line_up() # no line shared with `seeded` a.label("probe_run") a.emit(addu("t9", "ra", "zero")) a.jal("getpc"); a.emit(nop())