Files
thejayman77 ec82764bef Initial commit: retroDE_ps2 — first-of-its-kind PS2 GS FPGA core (DE25-Nano / Agilex 5)
RTL (GS rasterizer, EE core stub, platform bridge, LPDDR4B path), sim regression
(272 TBs), docs, and tooling. Copyrighted PS2 content (BIOS, game code, GS dumps,
and all dump-derived textures/traces) is excluded via .gitignore and stays local.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 20:10:50 -04:00

169 lines
6.4 KiB
Python

#!/usr/bin/env python3
"""
Convert a PS2 EE ELF into a tb_ee_core_elf_runner-compatible image
manifest pair (no external deps; pure stdlib).
Emits two files at the requested output prefix, identical in format
to generate_synthetic_image.py:
<prefix>.image.hex iverilog $readmemh, @<qw_idx> directives for
populated 128-bit qwords only. Each line is
32 hex chars (MSB-first, byte 15 leftmost).
<prefix>.manifest.hex line 0 = ELF entry point (32-bit hex)
line 1 = stack-top hint (32-bit hex)
Supports ELF32 little-endian, ELFCLASS32, EM_MIPS, e_type ET_EXEC or
ET_DYN. PT_LOAD segments are placed at their physical address
(low 29 bits of p_vaddr — strips the kuseg/kseg0/kseg1 alias bits so
the data lands at the correct phys offset in ee_ram_stub).
Stack-top is approximated as (ee_ram_bytes - 0x10) since real PS2
ELFs don't carry a stack pointer in their headers; the TB will set
$sp to this if the manifest is read.
Usage:
elf_to_eeram.py --in path/to/game.elf --out-prefix /tmp/game
Verdict-aware notes:
* Segments overflowing the EE RAM image cause a fatal error.
* Segments overlapping each other are flagged but not fatal — the
later one wins (matches how a real loader would behave).
"""
import sys
import struct
import argparse
ELFMAG = b"\x7fELF"
ELFCLASS32 = 1
ELFDATA2LSB = 1
EM_MIPS = 8
ET_EXEC = 2
ET_DYN = 3
PT_LOAD = 1
def parse_elf32_le(data: bytes):
"""Return (entry, [(p_vaddr, p_offset, p_filesz, p_memsz), ...])
for PT_LOAD segments of a 32-bit little-endian MIPS ELF.
Raises ValueError on bad magic / wrong class / wrong arch.
"""
if len(data) < 52 or data[:4] != ELFMAG:
raise ValueError("not an ELF file (bad magic)")
if data[4] != ELFCLASS32:
raise ValueError(f"only ELFCLASS32 supported (got class={data[4]})")
if data[5] != ELFDATA2LSB:
raise ValueError(f"only little-endian supported (got data={data[5]})")
(e_type, e_machine, e_version, e_entry, e_phoff, e_shoff, e_flags,
e_ehsize, e_phentsize, e_phnum, e_shentsize, e_shnum, e_shstrndx) = \
struct.unpack_from("<HHIIIIIHHHHHH", data, 16)
if e_machine != EM_MIPS:
raise ValueError(f"only EM_MIPS supported (got machine={e_machine})")
if e_type not in (ET_EXEC, ET_DYN):
raise ValueError(f"only ET_EXEC / ET_DYN supported (got type={e_type})")
pt_load = []
for i in range(e_phnum):
off = e_phoff + i * e_phentsize
(p_type, p_offset, p_vaddr, p_paddr, p_filesz, p_memsz,
p_flags, p_align) = struct.unpack_from("<IIIIIIII", data, off)
if p_type == PT_LOAD:
pt_load.append((p_vaddr, p_offset, p_filesz, p_memsz))
return e_entry, pt_load
def build_image(elf_bytes: bytes, ee_ram_bytes: int):
entry, segs = parse_elf32_le(elf_bytes)
image = bytearray(ee_ram_bytes)
placed = []
for (p_vaddr, p_offset, p_filesz, p_memsz) in segs:
phys = p_vaddr & 0x1FFFFFFF # strip kseg/kuseg bits
if phys + p_memsz > ee_ram_bytes:
raise ValueError(
f"PT_LOAD at vaddr=0x{p_vaddr:08x} phys=0x{phys:08x} "
f"size=0x{p_memsz:x} overflows EE RAM (0x{ee_ram_bytes:x})")
# Detect overlap (informational only).
for (lo, hi) in placed:
if not (phys + p_memsz <= lo or phys >= hi):
print(f"[elf_to_eeram] WARNING: PT_LOAD at phys=0x{phys:08x} "
f"size=0x{p_memsz:x} overlaps prior placement",
file=sys.stderr)
placed.append((phys, phys + p_memsz))
# Copy p_filesz bytes from file at p_offset → phys. p_memsz can
# be larger than p_filesz (.bss tail); image is already zero-
# initialised so the tail is naturally zero.
chunk = elf_bytes[p_offset:p_offset + p_filesz]
image[phys:phys + p_filesz] = chunk
print(f"[elf_to_eeram] placed PT_LOAD vaddr=0x{p_vaddr:08x} "
f"phys=0x{phys:08x} filesz=0x{p_filesz:x} memsz=0x{p_memsz:x}")
return entry, image
def qword_to_hex(image: bytearray, qw_phys: int) -> str:
"""MSB-first hex string for the qword at byte offset qw_phys."""
bytes16 = image[qw_phys:qw_phys + 16]
return bytes16[::-1].hex()
def emit_image_hex(image: bytearray, path: str) -> None:
qw_size = 16
with open(path, "w") as f:
f.write("// Ch270 ELF-derived EE-RAM image\n")
f.write(f"// {len(image)} bytes / {len(image)//qw_size} qwords\n")
f.write("// Populated qwords only; TB zero-inits before $readmemh.\n\n")
any_emitted = False
for qw_idx in range(0, len(image) // qw_size):
qw_byte = qw_idx * qw_size
qw_bytes = image[qw_byte:qw_byte + qw_size]
if any(b != 0 for b in qw_bytes):
f.write(f"@{qw_idx:08x}\n")
f.write(qword_to_hex(image, qw_byte) + "\n")
any_emitted = True
if not any_emitted:
f.write("@00000000\n00000000000000000000000000000000\n")
def emit_manifest_hex(path: str, entry: int, stack_top: int) -> None:
with open(path, "w") as f:
f.write("// Ch270 manifest from ELF\n")
f.write(f"// line 0 = entry, line 1 = stack_top hint\n")
f.write(f"{entry:08x}\n")
f.write(f"{stack_top:08x}\n")
def main() -> int:
p = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
p.add_argument("--in", dest="elf_in", required=True,
help="input ELF path")
p.add_argument("--out-prefix", required=True,
help="output file prefix")
p.add_argument("--ee-ram-bytes", type=lambda s: int(s, 0),
default=2 * 1024 * 1024,
help="EE RAM size in bytes (default 2 MiB)")
args = p.parse_args()
with open(args.elf_in, "rb") as f:
elf_bytes = f.read()
entry, image = build_image(elf_bytes, args.ee_ram_bytes)
stack_top = args.ee_ram_bytes - 0x10
emit_image_hex(image, f"{args.out_prefix}.image.hex")
emit_manifest_hex(f"{args.out_prefix}.manifest.hex", entry, stack_top)
print(f"[elf_to_eeram] wrote {args.out_prefix}.image.hex + "
f"{args.out_prefix}.manifest.hex (entry=0x{entry:08x}, "
f"stack_top=0x{stack_top:08x}, ee_ram={args.ee_ram_bytes} bytes)")
return 0
if __name__ == "__main__":
sys.exit(main())