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>
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
# sim/golden — Golden-Reference Comparison
|
||||
|
||||
Scripts, hooks, and notes for comparing RTL behavior against a software
|
||||
golden reference (PS2 emulator).
|
||||
|
||||
The project-level reference strategy is locked in
|
||||
`docs/decisions/0003-golden-reference.md`.
|
||||
|
||||
Current state (2026-04-17):
|
||||
|
||||
- **Live-emulator harness is parked.** Two attempts were made:
|
||||
DobieStation (blocked at runtime in the Emulator constructor) and PCSX2
|
||||
(blocked at CMake configuration on missing modern deps including SDL3,
|
||||
plutovg, plutosvg, ryml, Qt 6.10.1). See `third_party/DobieStation/NOTES.md`
|
||||
and `third_party/PCSX2/NOTES.md`.
|
||||
- **The generated-golden path is the completed Phase 1 result.** End-to-end
|
||||
validated through `make test_compare`: clean run, deliberate corruption,
|
||||
and malformed input all produce the documented exit codes.
|
||||
- Live-emulator comparison is deferred until either a fuller EE front end
|
||||
lands (Wave 3) or a low-friction live source becomes available.
|
||||
- `docs/decisions/0003-golden-reference.md` is unchanged — multiple-references
|
||||
is still the strategy; only the phasing moved.
|
||||
|
||||
Implementation spec:
|
||||
|
||||
- `trace_compare_spec.md` — defines the first compare target, common-envelope
|
||||
shape, diff semantics, exit-code contract, and acceptance criteria.
|
||||
|
||||
Current implementation (Phase 1, NOP-sled target):
|
||||
|
||||
- `make_nop_golden.py` — generates the NOP-sled golden trace in the common
|
||||
envelope. Options: `--count`, `--reset-vector`, `-o`. Default matches the
|
||||
32-fetch window produced by `tb_ee_fetch_stub`.
|
||||
- `trace_compare.py` — filtered order-based diff per spec. Exits 0/1/2/3 for
|
||||
pass / semantic mismatch / usage-or-missing / malformed.
|
||||
- `dobiestation_runner/` — headless DobieStation harness. Builds cleanly;
|
||||
runtime blocked at Emulator constructor (see
|
||||
`third_party/DobieStation/NOTES.md`). Fetch tap is instrumented and ready
|
||||
to emit in this same envelope once the runtime block is resolved.
|
||||
|
||||
Expected content as this area grows:
|
||||
|
||||
- build/run notes for emulator-side tools,
|
||||
- patches or instrumentation to emit traces in the chosen format,
|
||||
- additional comparison scripts as new event classes come under diff,
|
||||
- disagreement policy notes for "spec vs emulator" and later "emulator vs
|
||||
emulator" cases.
|
||||
Executable
+92
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env python3
|
||||
# retroDE_ps2 — bin_to_hex
|
||||
#
|
||||
# Convert a raw 32-bit binary image (e.g. a PS2 BIOS dump) to the text
|
||||
# hex format iverilog's $readmemh expects: one 32-bit word per line,
|
||||
# hex (no 0x prefix), newline-terminated. iverilog reads the first line
|
||||
# into memory index 0 and advances by one per line.
|
||||
#
|
||||
# Typical usage:
|
||||
# python3 sim/golden/bin_to_hex.py path/to/ps2_bios.bin > /tmp/bios.hex
|
||||
# make -C sim tb_iop_core_bios_smoke BIOS=/tmp/bios.hex
|
||||
#
|
||||
# Endianness:
|
||||
# PS2 BIOS images on disk are little-endian byte order, which matches
|
||||
# how the core will see each 32-bit word once it's fetched from the
|
||||
# bios_rom_stub backing memory. We emit words using `<I` (little-
|
||||
# endian unsigned 32-bit) to preserve that.
|
||||
#
|
||||
# Image size:
|
||||
# Inputs shorter than the BIOS ROM are padded to the stub's
|
||||
# DEFAULT_SIZE_WORDS (1 Mi words = 4 MiB) so $readmemh fills the whole
|
||||
# array and leaves no stale values. Inputs longer than that are
|
||||
# rejected — they're either the wrong file or would silently trample
|
||||
# something.
|
||||
#
|
||||
# See sim/README.md "Real-BIOS iteration" section for the full workflow.
|
||||
|
||||
import argparse
|
||||
import struct
|
||||
import sys
|
||||
|
||||
DEFAULT_SIZE_WORDS = 1 << 20 # 4 MiB / 4 bytes
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Convert a binary BIOS image to hex-text for $readmemh.")
|
||||
parser.add_argument("input", help="input .bin file")
|
||||
parser.add_argument(
|
||||
"-o", "--output",
|
||||
help="output .hex path (default: stdout)")
|
||||
parser.add_argument(
|
||||
"--words", type=int, default=DEFAULT_SIZE_WORDS,
|
||||
help=f"total words to emit (default {DEFAULT_SIZE_WORDS}, the "
|
||||
f"bios_rom_stub default depth)")
|
||||
parser.add_argument(
|
||||
"--pad", default="00000000",
|
||||
help="pad word used when input is shorter than --words "
|
||||
"(default 00000000 = SLL $0,$0,0 = NOP)")
|
||||
args = parser.parse_args()
|
||||
|
||||
with open(args.input, "rb") as fh:
|
||||
raw = fh.read()
|
||||
|
||||
if len(raw) % 4 != 0:
|
||||
print(
|
||||
f"warning: input length {len(raw)} not a multiple of 4; "
|
||||
"trailing bytes dropped",
|
||||
file=sys.stderr)
|
||||
raw = raw[: len(raw) - (len(raw) % 4)]
|
||||
|
||||
words = list(struct.iter_unpack("<I", raw))
|
||||
n_input = len(words)
|
||||
|
||||
if n_input > args.words:
|
||||
print(
|
||||
f"error: input has {n_input} words but only {args.words} "
|
||||
"will be emitted; refusing to truncate",
|
||||
file=sys.stderr)
|
||||
return 2
|
||||
|
||||
out_fh = open(args.output, "w") if args.output else sys.stdout
|
||||
try:
|
||||
for (w,) in words:
|
||||
out_fh.write(f"{w:08x}\n")
|
||||
pad_word = int(args.pad, 16)
|
||||
for _ in range(args.words - n_input):
|
||||
out_fh.write(f"{pad_word:08x}\n")
|
||||
finally:
|
||||
if args.output:
|
||||
out_fh.close()
|
||||
|
||||
print(
|
||||
f"[bin_to_hex] wrote {args.words} words "
|
||||
f"({n_input} from input, {args.words - n_input} pad) "
|
||||
f"to {args.output or 'stdout'}",
|
||||
file=sys.stderr)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,45 @@
|
||||
# retroDE_ps2 — DobieStation headless trace runner build
|
||||
#
|
||||
# Links against the libCore.a produced by the DobieStation build in
|
||||
# third_party/DobieStation/build/. Requires that build to exist first:
|
||||
#
|
||||
# cd ../../../third_party/DobieStation
|
||||
# mkdir -p build && cd build && cmake .. && make -j$(nproc)
|
||||
#
|
||||
# Then from this directory:
|
||||
#
|
||||
# make
|
||||
# ./trace_runner ../../vectors/bios/nop_sled.bin
|
||||
#
|
||||
# See also: trace_runner.cpp header comment.
|
||||
|
||||
RUNNER_DIR := $(CURDIR)
|
||||
REPO_ROOT := $(abspath $(RUNNER_DIR)/../../..)
|
||||
DOBIE_ROOT := $(REPO_ROOT)/third_party/DobieStation
|
||||
DOBIE_BUILD := $(DOBIE_ROOT)/build
|
||||
|
||||
CXX := g++
|
||||
CXXFLAGS := -std=c++17 -O2 -Wall -Wno-unused
|
||||
INCLUDES := -I$(DOBIE_ROOT)/src/core -I$(DOBIE_ROOT)/ext
|
||||
LIBCORE_A := $(DOBIE_BUILD)/src/core/libCore.a
|
||||
LIBCHDR_A := $(DOBIE_BUILD)/ext/libchdr/liblibchdr.a
|
||||
LIBLZMA_A := $(DOBIE_BUILD)/ext/lzma/liblzma.a
|
||||
LIBFLAC_A := $(DOBIE_BUILD)/ext/libFLAC/liblibFLAC.a
|
||||
LIBZLIB_A := $(DOBIE_BUILD)/ext/zlib/libzlib.a
|
||||
|
||||
LDFLAGS := $(LIBCORE_A) $(LIBCHDR_A) $(LIBLZMA_A) $(LIBFLAC_A) $(LIBZLIB_A) \
|
||||
-pthread
|
||||
|
||||
.PHONY: all clean check-libcore
|
||||
|
||||
all: trace_runner
|
||||
|
||||
check-libcore:
|
||||
@test -f $(DOBIE_BUILD)/src/core/libCore.a || \
|
||||
(echo "ERROR: $(DOBIE_BUILD)/src/core/libCore.a missing — build DobieStation first" && exit 1)
|
||||
|
||||
trace_runner: trace_runner.cpp check-libcore
|
||||
$(CXX) $(CXXFLAGS) $(INCLUDES) $< -o $@ $(LDFLAGS)
|
||||
|
||||
clean:
|
||||
rm -f trace_runner
|
||||
@@ -0,0 +1,13 @@
|
||||
// Minimal smoke test: construct + reset. No BIOS, no run.
|
||||
#include <cstdio>
|
||||
#include "emulator.hpp"
|
||||
|
||||
int main()
|
||||
{
|
||||
std::fprintf(stderr, "[smoke] constructing Emulator...\n");
|
||||
Emulator e;
|
||||
std::fprintf(stderr, "[smoke] constructed\n");
|
||||
e.reset();
|
||||
std::fprintf(stderr, "[smoke] reset done\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
// retroDE_ps2 — DobieStation headless trace runner
|
||||
//
|
||||
// Minimal harness for the golden-reference harness. Loads a BIOS image,
|
||||
// boots the EE in interpreter mode, runs a bounded number of frames, and
|
||||
// exits. The EE instruction-fetch trace is emitted by the tap added in
|
||||
// third_party/DobieStation/src/core/ee/emotion.cpp when the environment
|
||||
// variable RETRODE_PS2_EE_TRACE is set to a file path.
|
||||
//
|
||||
// Intended for Wave 1 / Checkpoint 2 of the harness: NOP-sled BIOS,
|
||||
// short run, compare first-N fetches against the RTL stub trace.
|
||||
//
|
||||
// Usage:
|
||||
// RETRODE_PS2_EE_TRACE=/path/to/dobie_ee.trace ./trace_runner <bios.bin> [frames]
|
||||
//
|
||||
// Notes:
|
||||
// - BIOS image must be exactly 4 MiB. Anything shorter is zero-padded;
|
||||
// anything longer is truncated.
|
||||
// - `frames` defaults to 1. One frame is ~4.9M EE cycles, which is far
|
||||
// more than Checkpoint 2 needs; keep it small.
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <vector>
|
||||
|
||||
#include "emulator.hpp"
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
if (argc < 2)
|
||||
{
|
||||
std::fprintf(stderr,
|
||||
"usage: %s <bios.bin> [frames]\n"
|
||||
"env: RETRODE_PS2_EE_TRACE=<path> enable EE fetch trace\n",
|
||||
argv[0]);
|
||||
return 2;
|
||||
}
|
||||
|
||||
const char* bios_path = argv[1];
|
||||
int max_frames = (argc >= 3) ? std::atoi(argv[2]) : 1;
|
||||
if (max_frames <= 0) max_frames = 1;
|
||||
|
||||
constexpr size_t BIOS_SIZE = 4 * 1024 * 1024;
|
||||
std::vector<uint8_t> bios(BIOS_SIZE, 0);
|
||||
|
||||
std::ifstream f(bios_path, std::ios::binary);
|
||||
if (!f)
|
||||
{
|
||||
std::fprintf(stderr, "[trace_runner] cannot open %s\n", bios_path);
|
||||
return 2;
|
||||
}
|
||||
f.read(reinterpret_cast<char*>(bios.data()), BIOS_SIZE);
|
||||
std::fprintf(stderr, "[trace_runner] loaded %zd bytes from %s\n",
|
||||
f.gcount(), bios_path);
|
||||
|
||||
Emulator emu;
|
||||
emu.reset();
|
||||
emu.load_BIOS(bios.data());
|
||||
emu.set_ee_mode(CPU_MODE::INTERPRETER);
|
||||
emu.set_vu0_mode(CPU_MODE::INTERPRETER);
|
||||
emu.set_vu1_mode(CPU_MODE::INTERPRETER);
|
||||
|
||||
for (int i = 0; i < max_frames; i++)
|
||||
{
|
||||
std::fprintf(stderr, "[trace_runner] running frame %d/%d\n",
|
||||
i + 1, max_frames);
|
||||
emu.run();
|
||||
}
|
||||
|
||||
std::fprintf(stderr, "[trace_runner] done\n");
|
||||
return 0;
|
||||
}
|
||||
Executable
+59
@@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate a NOP-sled golden trace in the retroDE_ps2 common envelope.
|
||||
|
||||
Produces one EE IFETCH record per fetch, starting at --reset-vector (default
|
||||
0xBFC00000), advancing PC += 4 each step. Each instruction word is 0x00000000
|
||||
(MIPS NOP: sll $0, $0, 0).
|
||||
|
||||
Format: see docs/decisions/0000-trace-format.md.
|
||||
Spec: see sim/golden/trace_compare_spec.md.
|
||||
"""
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(description="NOP-sled golden trace generator")
|
||||
ap.add_argument("--count", type=int, default=32,
|
||||
help="number of IFETCH records to emit (default: 32)")
|
||||
ap.add_argument("--reset-vector", type=lambda s: int(s, 0),
|
||||
default=0xBFC00000,
|
||||
help="starting PC (default: 0xBFC00000)")
|
||||
ap.add_argument("-o", "--output", default="-",
|
||||
help="output file, '-' for stdout (default: -)")
|
||||
args = ap.parse_args()
|
||||
|
||||
if args.count <= 0:
|
||||
print("error: --count must be positive", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
if args.output == "-":
|
||||
out = sys.stdout
|
||||
close = False
|
||||
else:
|
||||
try:
|
||||
out = open(args.output, "w")
|
||||
except OSError as e:
|
||||
print(f"error: cannot open {args.output}: {e}", file=sys.stderr)
|
||||
return 2
|
||||
close = True
|
||||
|
||||
try:
|
||||
out.write("# retroDE_ps2 golden trace, schema v1, "
|
||||
"source=generated, scenario=nop_sled\n")
|
||||
out.write("# columns: cycle subsystem event arg0 arg1 arg2 arg3 flags\n")
|
||||
for i in range(args.count):
|
||||
pc = (args.reset_vector + i * 4) & 0xFFFFFFFFFFFFFFFF
|
||||
out.write(
|
||||
f"{i} EE IFETCH "
|
||||
f"0x{pc:016x} 0x{0:016x} 0x{0:016x} 0x{0:016x} -\n"
|
||||
)
|
||||
finally:
|
||||
if close:
|
||||
out.close()
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Executable
+174
@@ -0,0 +1,174 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Compare two traces in the retroDE_ps2 common envelope.
|
||||
|
||||
Implements the diff semantics specified in sim/golden/trace_compare_spec.md:
|
||||
|
||||
- parse both files, skipping blank lines and comments starting with '#',
|
||||
- enforce exactly 8 columns per non-comment line,
|
||||
- validate cycle monotonicity within each input,
|
||||
- filter to (subsystem, event) pair (default: EE IFETCH),
|
||||
- require non-empty filtered sets on both sides,
|
||||
- require equal filtered lengths,
|
||||
- compare by record order, not by cycle,
|
||||
- the fields that must match exactly: subsystem, event, arg0..arg3, flags,
|
||||
- cycle is informational only.
|
||||
|
||||
Exit codes:
|
||||
0 pass
|
||||
1 semantic mismatch (length, field, or empty filtered set)
|
||||
2 usage or missing-file error
|
||||
3 malformed input / parse failure
|
||||
"""
|
||||
import argparse
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
|
||||
EXIT_PASS = 0
|
||||
EXIT_MISMATCH = 1
|
||||
EXIT_USAGE = 2
|
||||
EXIT_MALFORMED = 3
|
||||
|
||||
|
||||
@dataclass
|
||||
class Record:
|
||||
line_no: int
|
||||
cycle: int
|
||||
subsystem: str
|
||||
event: str
|
||||
arg0: int
|
||||
arg1: int
|
||||
arg2: int
|
||||
arg3: int
|
||||
flags: int
|
||||
|
||||
|
||||
def _parse_int(s: str) -> int:
|
||||
s = s.strip()
|
||||
if s.startswith(("0x", "0X")):
|
||||
return int(s, 16)
|
||||
return int(s, 10)
|
||||
|
||||
|
||||
def _parse_flags(s: str) -> int:
|
||||
s = s.strip()
|
||||
if s == "-":
|
||||
return 0
|
||||
if s.startswith(("0x", "0X")):
|
||||
return int(s, 16)
|
||||
raise ValueError(f"unrecognized flags token: {s!r}")
|
||||
|
||||
|
||||
def _fail_malformed(path: str, line_no: int, reason: str) -> None:
|
||||
print(f"ERROR: {path}:{line_no}: {reason}", file=sys.stderr)
|
||||
sys.exit(EXIT_MALFORMED)
|
||||
|
||||
|
||||
def parse_trace(path: str) -> list[Record]:
|
||||
try:
|
||||
f = open(path, "r")
|
||||
except OSError as e:
|
||||
print(f"ERROR: cannot open {path}: {e}", file=sys.stderr)
|
||||
sys.exit(EXIT_USAGE)
|
||||
|
||||
records: list[Record] = []
|
||||
with f:
|
||||
for line_no, raw in enumerate(f, 1):
|
||||
stripped = raw.strip()
|
||||
if not stripped or stripped.startswith("#"):
|
||||
continue
|
||||
parts = stripped.split()
|
||||
if len(parts) != 8:
|
||||
_fail_malformed(path, line_no,
|
||||
f"expected 8 columns, got {len(parts)}")
|
||||
try:
|
||||
cycle = _parse_int(parts[0])
|
||||
arg0 = _parse_int(parts[3])
|
||||
arg1 = _parse_int(parts[4])
|
||||
arg2 = _parse_int(parts[5])
|
||||
arg3 = _parse_int(parts[6])
|
||||
flags = _parse_flags(parts[7])
|
||||
except ValueError as e:
|
||||
_fail_malformed(path, line_no, str(e))
|
||||
records.append(Record(
|
||||
line_no=line_no, cycle=cycle,
|
||||
subsystem=parts[1], event=parts[2],
|
||||
arg0=arg0, arg1=arg1, arg2=arg2, arg3=arg3, flags=flags,
|
||||
))
|
||||
|
||||
# Monotonic cycle check on the raw (unfiltered) stream.
|
||||
for i in range(1, len(records)):
|
||||
if records[i].cycle < records[i - 1].cycle:
|
||||
_fail_malformed(
|
||||
path, records[i].line_no,
|
||||
f"non-monotonic cycle: {records[i].cycle} "
|
||||
f"< previous {records[i - 1].cycle}"
|
||||
)
|
||||
|
||||
return records
|
||||
|
||||
|
||||
def _filter(recs: list[Record], subsystem: str, event: str) -> list[Record]:
|
||||
return [r for r in recs if r.subsystem == subsystem and r.event == event]
|
||||
|
||||
|
||||
def _fmt_record(r: Record, subsystem: str, event: str) -> str:
|
||||
return (f"{subsystem} {event} "
|
||||
f"arg0=0x{r.arg0:016x} arg1=0x{r.arg1:016x} "
|
||||
f"arg2=0x{r.arg2:016x} arg3=0x{r.arg3:016x} "
|
||||
f"flags=0x{r.flags:08x}")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(description="retroDE_ps2 trace comparator")
|
||||
ap.add_argument("rtl", help="RTL trace file")
|
||||
ap.add_argument("golden", help="golden trace file")
|
||||
ap.add_argument("--subsystem", default="EE",
|
||||
help="filter subsystem (default: EE)")
|
||||
ap.add_argument("--event", default="IFETCH",
|
||||
help="filter event (default: IFETCH)")
|
||||
args = ap.parse_args()
|
||||
|
||||
rtl_all = parse_trace(args.rtl)
|
||||
golden_all = parse_trace(args.golden)
|
||||
|
||||
rtl = _filter(rtl_all, args.subsystem, args.event)
|
||||
golden = _filter(golden_all, args.subsystem, args.event)
|
||||
|
||||
if not rtl:
|
||||
print(f"FAIL: {args.rtl}: no {args.subsystem}/{args.event} records "
|
||||
f"after filter", file=sys.stderr)
|
||||
return EXIT_MISMATCH
|
||||
if not golden:
|
||||
print(f"FAIL: {args.golden}: no {args.subsystem}/{args.event} records "
|
||||
f"after filter", file=sys.stderr)
|
||||
return EXIT_MISMATCH
|
||||
|
||||
if len(rtl) != len(golden):
|
||||
longer = "rtl" if len(rtl) > len(golden) else "golden"
|
||||
first_missing = min(len(rtl), len(golden))
|
||||
print(f"FAIL: length mismatch — rtl={len(rtl)} golden={len(golden)}",
|
||||
file=sys.stderr)
|
||||
print(f" {longer} has more records; "
|
||||
f"first missing filtered index = {first_missing}",
|
||||
file=sys.stderr)
|
||||
return EXIT_MISMATCH
|
||||
|
||||
must_match = ("subsystem", "event", "arg0", "arg1", "arg2", "arg3", "flags")
|
||||
for i, (r, g) in enumerate(zip(rtl, golden)):
|
||||
diffs = [f for f in must_match if getattr(r, f) != getattr(g, f)]
|
||||
if diffs:
|
||||
print(f"FAIL: mismatch at filtered record {i}", file=sys.stderr)
|
||||
print(f" rtl: {_fmt_record(r, args.subsystem, args.event)}",
|
||||
file=sys.stderr)
|
||||
print(f" golden: {_fmt_record(g, args.subsystem, args.event)}",
|
||||
file=sys.stderr)
|
||||
print(f" diff: {', '.join(diffs)}", file=sys.stderr)
|
||||
return EXIT_MISMATCH
|
||||
|
||||
print(f"PASS: matched {len(rtl)} {args.subsystem}/{args.event} records "
|
||||
f"(cycle ignored, order-based compare)")
|
||||
return EXIT_PASS
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,260 @@
|
||||
# Golden Trace and Diff Spec
|
||||
|
||||
This document defines the first useful scope for the golden-reference harness
|
||||
after DobieStation runtime blocked at checkpoint 2.
|
||||
|
||||
The immediate target is not "full live-emulator comparison." The immediate
|
||||
target is:
|
||||
|
||||
- generated golden trace for a straight-line synthetic image,
|
||||
- normalization into the common envelope,
|
||||
- deterministic diff tooling,
|
||||
- a clean upgrade path to live emulator traces later.
|
||||
|
||||
This spec is intentionally narrow so the harness plumbing can land now without
|
||||
waiting for a full EE implementation or a revived DobieStation runtime.
|
||||
|
||||
## Scope
|
||||
|
||||
Phase covered by this spec:
|
||||
|
||||
- generated golden trace for a NOP-sled BIOS image,
|
||||
- comparison against RTL `ee_fetch.trace`,
|
||||
- event class limited to `EE IFETCH`.
|
||||
|
||||
Not in scope yet:
|
||||
|
||||
- real BIOS comparison,
|
||||
- DobieStation runtime recovery,
|
||||
- PCSX2 instrumentation,
|
||||
- multi-event or multi-subsystem trace correlation,
|
||||
- cycle-accurate timing comparison.
|
||||
|
||||
## Comparison target
|
||||
|
||||
The first comparison target is a dedicated straight-line synthetic BIOS image.
|
||||
|
||||
Required properties:
|
||||
|
||||
- valid MIPS instruction stream,
|
||||
- no branches in the compared window,
|
||||
- no data loads or stores needed for correctness,
|
||||
- deterministic sequential fetch progression.
|
||||
|
||||
For the first implementation, use a `NOP` sled:
|
||||
|
||||
- instruction word: `0x00000000`
|
||||
- image size: 4 MiB to match the BIOS window
|
||||
- reset vector fetch starts at `0xBFC00000`
|
||||
|
||||
Why this target:
|
||||
|
||||
- simple enough to reason about by inspection,
|
||||
- valid execution stream for an emulator,
|
||||
- meaningful now even with the current `ee_fetch_stub`,
|
||||
- avoids comparing against the existing synthetic fixture whose words are not a
|
||||
sensible execution target.
|
||||
|
||||
## Golden trace shape
|
||||
|
||||
The generated golden trace must use the project common envelope:
|
||||
|
||||
```text
|
||||
cycle subsystem event arg0 arg1 arg2 arg3 flags
|
||||
```
|
||||
|
||||
Header lines:
|
||||
|
||||
- must begin with `#`
|
||||
- should identify source, scenario, and schema version
|
||||
|
||||
Example:
|
||||
|
||||
```text
|
||||
# retroDE_ps2 golden trace, schema v1, source=generated, scenario=nop_sled
|
||||
# columns: cycle subsystem event arg0 arg1 arg2 arg3 flags
|
||||
0 EE IFETCH 0x00000000bfc00000 0x0000000000000000 0x0000000000000000 0x0000000000000000 -
|
||||
1 EE IFETCH 0x00000000bfc00004 0x0000000000000000 0x0000000000000000 0x0000000000000000 -
|
||||
2 EE IFETCH 0x00000000bfc00008 0x0000000000000000 0x0000000000000000 0x0000000000000000 -
|
||||
```
|
||||
|
||||
Field rules for the generated `NOP` golden:
|
||||
|
||||
- `cycle`: monotonic ordinal index starting at `0`
|
||||
- `subsystem`: `EE`
|
||||
- `event`: `IFETCH`
|
||||
- `arg0`: fetch PC
|
||||
- `arg1`: fetched instruction word, always `0x00000000`
|
||||
- `arg2`: response kind, `0x0000000000000000`
|
||||
- `arg3`: unused, `0x0000000000000000`
|
||||
- `flags`: `-`
|
||||
|
||||
## RTL input expected by the diff
|
||||
|
||||
For the first compare, the RTL-side input is:
|
||||
|
||||
- `sim/traces/rtl/ee_fetch.trace`
|
||||
|
||||
The diff tool must ignore:
|
||||
|
||||
- comment lines beginning with `#`
|
||||
- blank lines
|
||||
- non-matching event classes if filtering is enabled
|
||||
|
||||
For the first compare, the tool should operate on `EE IFETCH` records only.
|
||||
|
||||
`EE RESET` lines are intentionally excluded from the first comparison target.
|
||||
They are useful trace events, but they are not part of the first golden fetch
|
||||
stream contract.
|
||||
|
||||
## Normalized record model
|
||||
|
||||
The diff tool should parse each non-comment line into:
|
||||
|
||||
- `cycle`
|
||||
- `subsystem`
|
||||
- `event`
|
||||
- `arg0`
|
||||
- `arg1`
|
||||
- `arg2`
|
||||
- `arg3`
|
||||
- `flags`
|
||||
|
||||
All numeric payloads should be normalized to integers internally.
|
||||
|
||||
`flags` normalization:
|
||||
|
||||
- `-` means zero
|
||||
- `0x...` means the parsed integer value
|
||||
|
||||
## Diff semantics
|
||||
|
||||
### Record selection
|
||||
|
||||
For the first implementation, compare records after filtering to:
|
||||
|
||||
- `subsystem == EE`
|
||||
- `event == IFETCH`
|
||||
|
||||
### Match strategy
|
||||
|
||||
Compare by `record order`, not by `cycle`.
|
||||
|
||||
Rationale:
|
||||
|
||||
- current RTL and future emulator traces may use different cycle/time bases,
|
||||
- the first useful question is whether the fetch sequence matches,
|
||||
- order-based comparison is the right fit for the current straight-line target.
|
||||
|
||||
### Fields that must match
|
||||
|
||||
After filtering, the following fields must match exactly:
|
||||
|
||||
- `subsystem`
|
||||
- `event`
|
||||
- `arg0`
|
||||
- `arg1`
|
||||
- `arg2`
|
||||
- `arg3`
|
||||
- `flags`
|
||||
|
||||
### Fields that are informational only
|
||||
|
||||
- `cycle`
|
||||
|
||||
The tool should still validate that cycle values are monotonic within each
|
||||
input trace, but cycle mismatch alone must not fail the compare.
|
||||
|
||||
## Length mismatch policy
|
||||
|
||||
Length mismatch is a hard failure.
|
||||
|
||||
Cases:
|
||||
|
||||
- RTL shorter than golden: fail
|
||||
- RTL longer than golden: fail
|
||||
- either side empty after filtering: fail
|
||||
|
||||
Error output should report:
|
||||
|
||||
- filtered record counts on both sides,
|
||||
- first missing index,
|
||||
- which side ended early
|
||||
|
||||
## Malformed input policy
|
||||
|
||||
Malformed input is a hard failure.
|
||||
|
||||
Examples:
|
||||
|
||||
- wrong number of columns,
|
||||
- unparseable hex field,
|
||||
- unknown event token in a filtered record,
|
||||
- non-monotonic cycle values inside one trace
|
||||
|
||||
The tool should report:
|
||||
|
||||
- filename
|
||||
- 1-based line number in the original file
|
||||
- reason for rejection
|
||||
|
||||
## Exit code contract
|
||||
|
||||
Suggested exit codes:
|
||||
|
||||
- `0`: comparison passed
|
||||
- `1`: semantic mismatch
|
||||
- `2`: usage or missing-file error
|
||||
- `3`: malformed input / parse failure
|
||||
|
||||
This keeps "compare failed" distinct from "tool invocation broke."
|
||||
|
||||
## Console output contract
|
||||
|
||||
On pass, print a concise summary:
|
||||
|
||||
```text
|
||||
PASS: matched 32 EE/IFETCH records (cycle ignored, order-based compare)
|
||||
```
|
||||
|
||||
On mismatch, print:
|
||||
|
||||
- failing record index
|
||||
- RTL record
|
||||
- golden record
|
||||
- short field-level mismatch summary
|
||||
|
||||
Example:
|
||||
|
||||
```text
|
||||
FAIL: mismatch at filtered record 5
|
||||
rtl: EE IFETCH arg0=0x... arg1=0x...
|
||||
golden: EE IFETCH arg0=0x... arg1=0x...
|
||||
diff: arg1 differs
|
||||
```
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
The first golden harness is accepted when all of the following are true:
|
||||
|
||||
1. A generated NOP-sled golden trace can be produced in the common envelope.
|
||||
2. The diff tool can compare that golden trace against `sim/traces/rtl/ee_fetch.trace`.
|
||||
3. The tool ignores `EE RESET` and compares only filtered `EE IFETCH` records.
|
||||
4. A clean run against the current Wave 1 EE fetch path returns exit code `0`.
|
||||
5. A deliberate corruption of one fetched word in either input produces exit
|
||||
code `1` and reports the first mismatching filtered record.
|
||||
6. A malformed trace line produces exit code `3`.
|
||||
|
||||
## Upgrade path
|
||||
|
||||
Once live emulator traces are available, reuse the same diff semantics with
|
||||
only two changes:
|
||||
|
||||
- replace the generated golden input with a normalized emulator trace,
|
||||
- widen the compared window as far as the current RTL implementation remains
|
||||
meaningfully comparable.
|
||||
|
||||
Recommended next live-emulator order:
|
||||
|
||||
1. PCSX2, if a live source is needed before DobieStation runtime is recovered
|
||||
2. DobieStation later, if the runtime block is removed
|
||||
Reference in New Issue
Block a user