Files
psxrecomp/tools/segment_testrom/gen_segment_exe.py
T
Shokunin fdc354543d tools, tests: the segment probe loads an overlay; ledger guard overlay-segment-shards
docs/SEGMENT_AWARE_CODE.md §5.7, §7. The synthetic probe gains an overlay
phase that loads code the way a game loads an overlay. After the three
probe_run sequences it:
- leaves the critical section the BIOS started it in, because the kernel's
  CD driver waits on CD interrupts;
- reads its own EXE file back from the disc through the BIOS file calls
  (B0:32 open, B0:34 read, B0:36 close; the kernel DMAs the sectors) into
  RAM at 0x000A0000;
- calls the copy of a position-independent ov_run there through KUSEG,
  KSEG0 and KSEG1, timing each run with Timer 2.

It records the T2 deltas and ov_getpc links at R+0x28-R+0x3C, and the file
descriptor and byte count at R+0x40/0x44. In the runtime that code is
CD-DMA'd RAM code. Capture records its entries with their segments, and
compile_overlays.py builds a shard per segment.

The phase replaces the `j .` spin with a `j` of the same size, so every
earlier label keeps its address. The static probe code and every result
word it wrote before are unchanged, and so are its probe_run cycle-watch
hits on Beetle and native. Beetle on both BIOSes: T2 56 / 56 / 82 and links
0x000A0A2C / 0x800A0A2C / 0xA00A0A2C, the same shape as probe_run.

The ledger gains the regression guard overlay-segment-shards (§5.7). It
builds the capture record overlay_capture.c writes for the probe's
disc-loaded ov_run (schema v3), splits it with capture_segment_views(), and
compiles each view as the fragment pass does, with compile_overlays.py's own
helpers and the real recompiler. It requires:
- every PC each shard bakes, and its names, to be in its segment;
- the manifest's S and F records;
- ov_run's straight run to cost Beetle's fetch-model cycles: line leaders
  cached at KUSEG and KSEG0, +4 per fetch at KSEG1.

KNOWN_GAPS stays empty.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 02:43:46 -07:00

327 lines
14 KiB
Python

#!/usr/bin/env python3
"""gen_segment_exe.py - a synthetic KUSEG-linked PS-X EXE for segment-aware code.
docs/SEGMENT_AWARE_CODE.md is the design this program tests.
The header carries KUSEG addresses (load and entry 0x00010000), as Alien
Resurrection (SLUS-00633) and Kula World (SCES-01000) do. On hardware, and in
Beetle, the whole program then runs with KUSEG PCs. Every `jal` inherits the
segment of the PC that executes it (Beetle cpu.c DO_BRANCH, mask 0xF0000000).
So the link values it writes, the EPCs it produces and the I-cache tags it
fetches with are all KUSEG.
The program exercises each place the segment reaches:
R+0x00 $ra seen by `leaf` after `jal` (link, KUSEG)
R+0x04 $ra seen by `leaf` after `bgezal` (link, KUSEG)
R+0x08 $ra seen by `seeded` after `jalr` (link, KUSEG)
R+0x10 T2 ticks around `probe_run` at its KUSEG home (cached fetch)
R+0x14 `getpc` link inside that run (0x0001xxxx)
R+0x18 T2 ticks around `probe_run` via its KSEG0 alias (cached fetch)
R+0x1C `getpc` link inside that run (0x8001xxxx)
R+0x20 T2 ticks around `probe_run` via its KSEG1 alias (uncached, +4/fetch)
R+0x24 `getpc` link inside that run (0xA001xxxx)
R+0x28 T2 ticks around overlay `ov_run` entered at KUSEG (cached fetch)
R+0x2C `ov_getpc` link inside that run (0x000Axxxx)
R+0x30 T2 ticks around `ov_run` entered at KSEG0 (cached fetch)
R+0x34 `ov_getpc` link inside that run (0x800Axxxx)
R+0x38 T2 ticks around `ov_run` entered at KSEG1 (uncached)
R+0x3C `ov_getpc` link inside that run (0xA00Axxxx)
R+0x40 the file descriptor open() returned, R+0x44 the bytes read()
The overlay phase (docs/SEGMENT_AWARE_CODE.md §5.7) loads code the way a game
loads an overlay: after the three `probe_run` sequences, the program reads its
own EXE file back from the disc through the BIOS file calls (B0:32 open, B0:34
read, B0:36 close; the kernel DMAs the sectors) into RAM at OVL_BUF, and calls
the copy of `ov_run` there through KUSEG, KSEG0 and KSEG1, timing each run like
`probe_run`. `ov_run` is position independent (its call is a `bgezal`), so the
copy runs as it would at its link address. In the runtime that code is dirty
RAM: the interpreter runs it until overlay capture records its entries, with
each entry's segment, and compile_overlays.py builds a shard per segment.
`seeded` is entered through a register. The test seeds it in the link
segment and checks that the recompiler accepts the seed. `probe_run` has a straight run of ten instructions after
its `getpc` call returns, which is enough to show per-instruction uncached
fetch charging. The results block R (0x00011000) lies outside the loaded
image, so the stores never touch code pages. Timer 2 is put in mode 0
(system clock, free-running) first. The T2 deltas include the call overhead,
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 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
imports build() and probes() from this file.
Usage: python gen_segment_exe.py [out.exe] (also writes out.probes.json)
"""
import json
import os
import struct
import sys
KUSEG, KSEG0, KSEG1 = 0x00000000, 0x80000000, 0xA0000000
SEGMENTS = {"kuseg": KUSEG, "kseg0": KSEG0, "kseg1": KSEG1}
LOAD = 0x00010000 # link segment: KUSEG
RESULTS = 0x00011000 # outside the image; never a code page
OVL_BUF = 0x000A0000 # where the overlay phase reads the EXE file back
EXE_NAME = "SEGT_001.01" # the probe's name on its disc (SYSTEM.CNF BOOT)
IO_BASE = 0x1F800000 # T2 count at +0x1120, mode at +0x1124
STACK_TOP = 0x001FFF00 # KUSEG, like the rest of the header
REG = {"zero": 0, "at": 1, "v0": 2, "v1": 3, "a0": 4, "a1": 5, "a2": 6,
"t0": 8, "t1": 9, "t2": 10, "t3": 11, "s0": 16, "s1": 17, "s2": 18,
"t9": 25, "sp": 29, "ra": 31}
def _r(x):
return REG[x] if isinstance(x, str) else x
def R(funct, rs=0, rt=0, rd=0, sa=0):
return (_r(rs) << 21) | (_r(rt) << 16) | (_r(rd) << 11) | (sa << 6) | funct
def I(op, rs=0, rt=0, imm=0):
return (op << 26) | (_r(rs) << 21) | (_r(rt) << 16) | (imm & 0xFFFF)
def nop(): return 0
def addu(rd, rs, rt): return R(0x21, rs, rt, rd)
def subu(rd, rs, rt): return R(0x23, rs, rt, rd)
def or_(rd, rs, rt): return R(0x25, rs, rt, rd)
def jr(rs): return R(0x08, rs)
def jalr(rs, rd="ra"): return R(0x09, rs, 0, rd)
def addiu(rt, rs, imm): return I(0x09, rs, rt, imm)
def andi(rt, rs, imm): return I(0x0C, rs, rt, imm)
def ori(rt, rs, imm): return I(0x0D, rs, rt, imm)
def lui(rt, imm): return I(0x0F, 0, rt, imm)
def lw(rt, off, base): return I(0x23, base, rt, off)
def li(rt, imm): return ori(rt, "zero", imm)
def syscall(): return 0x0000000C
def sw(rt, off, base): return I(0x2B, base, rt, off)
class Asm:
"""Two-pass assembler: fixups resolve labels once layout is known."""
def __init__(self, base):
self.base, self.words, self.labels, self.fixups = base, [], {}, []
def here(self):
return self.base + 4 * len(self.words)
def label(self, name):
self.labels[name] = self.here()
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)
def j(self, name):
self.fixups.append((len(self.words), "j", name))
self.words.append(0)
def bgezal(self, rs, name):
self.fixups.append((len(self.words), "bgezal", (rs, name)))
self.words.append(0)
def la(self, rt, name, segment, delta=0):
"""lui/ori of label `name` (+delta) in `segment` (the probe's alias)."""
self.fixups.append((len(self.words), "la_hi", (rt, name, segment, delta)))
self.fixups.append((len(self.words) + 1, "la_lo", (rt, name, segment, delta)))
self.words.extend([0, 0])
def resolve(self):
for i, kind, arg in self.fixups:
pc = self.base + 4 * i
if kind in ("jal", "j"):
target = self.labels[arg]
# J-type keeps PC[31:28]: a target in another segment is not
# encodable, which is the invariant the design relies on.
assert (target & 0xF0000000) == ((pc + 4) & 0xF0000000)
op = 0x03 if kind == "jal" else 0x02
self.words[i] = (op << 26) | ((target >> 2) & 0x03FFFFFF)
elif kind == "bgezal":
rs, name = arg
off = (self.labels[name] - (pc + 4)) >> 2
self.words[i] = I(0x01, rs, 0x11, off)
else:
rt, name, segment, delta = arg
va = segment | ((self.labels[name] + delta) & 0x1FFFFFFF)
self.words[i] = (lui(rt, va >> 16) if kind == "la_hi"
else ori(rt, rt, va & 0xFFFF))
return b"".join(struct.pack("<I", w) for w in self.words)
def _assemble():
a = Asm(LOAD)
a.label("main")
a.emit(lui("s0", RESULTS >> 16), ori("s0", "s0", RESULTS & 0xFFFF))
a.emit(lui("s1", IO_BASE >> 16))
a.emit(sw("zero", 0x1124, "s1")) # T2 mode 0: sysclock, free run
a.jal("leaf"); a.emit(nop(), sw("v0", 0x00, "s0"))
a.bgezal("zero", "leaf"); a.emit(nop(), sw("v0", 0x04, "s0"))
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.
a.emit(lw("t1", 0x1120, "s1"), jalr("t0"), nop(),
lw("t2", 0x1120, "s1"), nop(), subu("t2", "t2", "t1"),
andi("t2", "t2", 0xFFFF),
sw("t2", 0x10 + 8 * n, "s0"), sw("v1", 0x14 + 8 * n, "s0"))
# Same size as the `j .` spin this replaced, so every label before the
# overlay phase keeps its address.
a.j("overlay_phase"); a.emit(nop())
a.label("leaf")
a.emit(jr("ra"), addu("v0", "ra", "zero"))
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())
a.label("probe_run_straight") # leader: getpc returns here
a.emit(*[addiu("t3", "t3", 1) for _ in range(8)])
a.emit(jr("t9"), addu("v0", "t9", "zero"))
a.label("probe_run_end")
a.label("getpc")
a.emit(jr("ra"), addu("v1", "ra", "zero"))
a.label("end")
# -- overlay phase (§5.7): read the EXE file back and run a copy --------
a.label("overlay_phase")
# The kernel's CD driver waits on CD interrupts: leave the critical
# section the BIOS started the program in (syscall ExitCriticalSection).
a.emit(li("a0", 2), syscall())
a.la("a0", "fname", KUSEG) # fd = open(name, O_RDONLY)
a.emit(li("a1", 1))
_bios_b0(a, 0x32)
a.emit(sw("v0", 0x40, "s0"), addu("s2", "v0", "zero"))
a.emit(addu("a0", "s2", "zero")) # read(fd, OVL_BUF, 0x1000)
a.emit(lui("a1", OVL_BUF >> 16), ori("a1", "a1", OVL_BUF & 0xFFFF))
a.emit(li("a2", 0x1000))
_bios_b0(a, 0x34)
a.emit(sw("v0", 0x44, "s0"))
a.emit(addu("a0", "s2", "zero")) # close(fd)
_bios_b0(a, 0x36)
for n, segment in enumerate((KUSEG, KSEG0, KSEG1)):
a.line_up(8) # each `lw t1` starts a line
# The copy of ov_run: the file holds the 2 KiB header, then the image.
a.la("t0", "ov_run", segment, OVL_BUF + 0x800 - LOAD)
a.emit(lw("t1", 0x1120, "s1"), jalr("t0"), nop(),
lw("t2", 0x1120, "s1"), nop(), subu("t2", "t2", "t1"),
andi("t2", "t2", 0xFFFF),
sw("t2", 0x28 + 8 * n, "s0"), sw("v1", 0x2C + 8 * n, "s0"))
a.label("spin")
a.j("spin"); a.emit(nop())
a.line_up() # the copy starts a line too
a.label("ov_run") # position independent
a.emit(addu("t9", "ra", "zero"))
a.bgezal("zero", "ov_getpc"); a.emit(nop())
a.label("ov_run_straight") # leader: ov_getpc returns here
a.emit(*[addiu("t3", "t3", 1) for _ in range(8)])
a.emit(jr("t9"), addu("v0", "t9", "zero"))
a.label("ov_run_end")
a.label("ov_getpc")
a.emit(jr("ra"), addu("v1", "ra", "zero"))
a.label("fname")
name = b"cdrom:\\" + EXE_NAME.encode("ascii") + b";1\0"
name += b"\0" * (-len(name) % 4)
a.emit(*struct.unpack("<%dI" % (len(name) // 4), name))
return a
def _bios_b0(a, fn):
"""Call B0 function `fn` through the kernel vector 0xB0 (t1 = function)."""
a.emit(li("t2", 0xB0), jalr("t2"), li("t1", fn))
def build():
"""Return the EXE bytes (2048-byte header + image padded to 2 KiB)."""
a = _assemble()
body = a.resolve()
# The overlay phase reads the header and the first 0x800 image bytes.
assert a.labels["ov_getpc"] + 8 - LOAD <= 0x800
body += b"\0" * (-len(body) % 0x800)
h = bytearray(2048)
h[0:8] = b"PS-X EXE"
struct.pack_into("<I", h, 0x10, a.labels["main"]) # initial pc (KUSEG)
struct.pack_into("<I", h, 0x18, LOAD) # load address (KUSEG)
struct.pack_into("<I", h, 0x1C, len(body))
struct.pack_into("<I", h, 0x30, STACK_TOP) # stack base (KUSEG)
return bytes(h) + body
def probes():
"""Label addresses (link segment) and probe facts the test checks."""
a = _assemble()
a.resolve()
labels = dict(a.labels)
return {
"link_segment": KUSEG,
"load": LOAD,
"results": RESULTS,
"labels": labels,
# Seeds, written the way a project writes them: `seeded` in the link
# segment, and `probe_run` (whose direct callee `getpc` follows it)
# in the two alias segments it is entered through.
"seeds": {
"home": [labels["seeded"]],
"variants": [KSEG0 | labels["probe_run"], KSEG1 | labels["probe_run"]],
},
# Straight-line fetch run inside probe_run: runtime PCs, link segment.
"straight": list(range(labels["probe_run_straight"],
labels["probe_run_end"], 4)),
# The overlay phase's copy of ov_run (physical) and the name it reads.
"overlay": {"buffer": OVL_BUF,
"ov_run": OVL_BUF + 0x800 + labels["ov_run"] - LOAD,
"ov_getpc": OVL_BUF + 0x800 + labels["ov_getpc"] - LOAD,
"file": EXE_NAME},
}
def main():
out = sys.argv[1] if len(sys.argv) > 1 else "segment_testrom.exe"
with open(out, "wb") as f:
f.write(build())
info = probes()
with open(os.path.splitext(out)[0] + ".probes.json", "w") as f:
json.dump({k: ({n: "0x%08X" % v for n, v in info[k].items()}
if k == "labels" else info[k]) for k in info},
f, indent=2)
print("wrote %s (%d bytes), probe_run at 0x%08X" %
(out, len(build()), info["labels"]["probe_run"]))
if __name__ == "__main__":
main()