mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-28 02:56:36 -04:00
36ba6bd950
The two table_watch timelines agree byte-for-byte up to one moment (baseline fingerprints identical), then fork: entering the scene, native receives two ch3 DMA loads (74 then 339 words) leaving 216 raw colour words in the effect's table, while DuckStation's region goes to the correct 5-colour table -- and both sides hold that state for the whole ~1220-frame effect before converging again (216->254->129 vs 5->252->129). psx-runtime's cd_read_log records every ch3 load with LBA, dest and size. This finds the loads that intersect the table and reads those very sectors from the ISO. Disc holds the many-colour data -> native delivered what was asked and the divergence is upstream (why the game requested it, or where the DMA pointed). Disc holds something else -> the CD stack is delivering wrong sector data, and that is the bug. The log dies with the process: capture after playing the scene, before restarting. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
159 lines
6.3 KiB
Python
159 lines
6.3 KiB
Python
#!/usr/bin/env python3
|
|
"""cd_fork.py -- which CD loads filled the effect's colour table, and what the
|
|
disc actually holds at those sectors.
|
|
|
|
python3 tools/cd_fork.py # loads targeting the table
|
|
python3 tools/cd_fork.py --iso /path/to/game.bin # + verify against disc
|
|
|
|
Where this fits
|
|
---------------
|
|
table_watch on both emulators pinned the fork to one moment: entering the
|
|
scene, psx-runtime's table region receives two CD-ROM DMA loads (74 then 339
|
|
words) leaving 216 distinct raw colour words, while DuckStation's region goes
|
|
to the correct 5-colour table -- and both then hold their state for the whole
|
|
effect. Everything before the fork is byte-identical (same fingerprint).
|
|
|
|
psx-runtime logs every ch3 DMA with its LBA, destination and size
|
|
(cd_read_log). So: find the loads that intersect the table, and read those
|
|
very sectors from the ISO. If the disc's own bytes at those LBAs are the
|
|
216-colour data, psx-runtime delivered exactly what the game asked for -- and
|
|
the divergence is UPSTREAM, in why the game asked (or where it pointed the
|
|
DMA). If the disc holds something else, the CD stack is delivering wrong
|
|
sector data, and that is the bug.
|
|
|
|
The log survives only as long as psx-runtime runs; capture after playing the
|
|
scene, before restarting.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import collections
|
|
import json
|
|
import os
|
|
import sys
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
|
|
from psx_gpu_frame import DEFAULT_NATIVE_PORT, DebugConn, DebugError # noqa: E402
|
|
|
|
KIND = "psx-cd-fork"
|
|
|
|
RAW_SECTOR = 2352
|
|
MODE2_HEADER = 24 # sync 12 + header 4 + subheader 8
|
|
DATA_BYTES = 2048
|
|
|
|
|
|
def overlapping(entries, lo, hi):
|
|
"""CD loads whose [dest, dest+size) intersects [lo, hi)."""
|
|
out = []
|
|
for e in entries:
|
|
dest = int(e["dest"], 16) & 0x1FFFFF
|
|
size = int(e["size"])
|
|
if dest < hi and dest + size > lo:
|
|
out.append({"lba": int(e["lba"]), "dest": dest, "size": size})
|
|
return out
|
|
|
|
|
|
def sector_bytes(iso, lba, count=1):
|
|
"""User data of `count` sectors from a raw Mode2 .bin image."""
|
|
out = bytearray()
|
|
with open(iso, "rb") as f:
|
|
for k in range(count):
|
|
f.seek((lba + k) * RAW_SECTOR + MODE2_HEADER)
|
|
out += f.read(DATA_BYTES)
|
|
return bytes(out)
|
|
|
|
|
|
def distinct_colours(blob):
|
|
seen = set()
|
|
for i in range(0, len(blob) - 3, 4):
|
|
seen.add(int.from_bytes(blob[i:i + 4], "little") & 0xFFFFFF)
|
|
return len(seen)
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser(description=__doc__,
|
|
formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
ap.add_argument("--host", default="127.0.0.1")
|
|
ap.add_argument("--port", type=int, default=DEFAULT_NATIVE_PORT)
|
|
ap.add_argument("--timeout", type=float, default=30.0)
|
|
ap.add_argument("--lo", default="0x000E25F0",
|
|
help="table region start (physical)")
|
|
ap.add_argument("--hi", default="0x000E2C60")
|
|
ap.add_argument("--tail", type=int, default=65536)
|
|
ap.add_argument("--iso", default=None,
|
|
help="raw .bin disc image; enables sector verification")
|
|
ap.add_argument("--out", default="analysis/frames/cd_fork.json")
|
|
args = ap.parse_args()
|
|
|
|
lo, hi = int(args.lo, 0) & 0x1FFFFF, int(args.hi, 0) & 0x1FFFFF
|
|
conn = DebugConn(args.host, args.port, args.timeout)
|
|
try:
|
|
rep = conn.cmd("cd_read_log", tail=args.tail)
|
|
except DebugError as e:
|
|
print(f"error: {e}", file=sys.stderr)
|
|
return 2
|
|
entries = rep.get("entries", [])
|
|
hits = overlapping(entries, lo, hi)
|
|
doc = {"kind": KIND, "version": 1, "lo": args.lo, "hi": args.hi,
|
|
"log_total": rep.get("total"), "loads": hits}
|
|
|
|
if not hits:
|
|
print(f"no CD load in the log intersects 0x{lo:06X}..0x{hi:06X}. "
|
|
f"The log holds {rep.get('total')} entries and only survives "
|
|
f"while psx-runtime runs -- play the scene first, then run this "
|
|
f"without restarting.")
|
|
return 1
|
|
|
|
print(f"{len(hits)} CD load(s) intersect the table region:")
|
|
for h in hits:
|
|
print(f" LBA {h['lba']:>7} -> 0x{h['dest']:06X} {h['size']} bytes")
|
|
|
|
if args.iso:
|
|
if not os.path.exists(args.iso):
|
|
print(f"error: no such file: {args.iso}", file=sys.stderr)
|
|
return 2
|
|
print(f"\nreading those sectors from {os.path.basename(args.iso)}:")
|
|
for h in hits:
|
|
nsec = (h["size"] + DATA_BYTES - 1) // DATA_BYTES
|
|
blob = sector_bytes(args.iso, h["lba"], nsec)
|
|
# Only the slice that actually landed inside the table matters.
|
|
skip = max(0, lo - h["dest"])
|
|
take = min(h["dest"] + h["size"], hi) - max(h["dest"], lo)
|
|
piece = blob[skip:skip + take]
|
|
n = distinct_colours(piece)
|
|
h["disc_distinct_colours"] = n
|
|
h["disc_first_bytes"] = piece[:32].hex()
|
|
print(f" LBA {h['lba']}: the {take}-byte slice covering the "
|
|
f"table holds {n} distinct colour word(s)")
|
|
worst = max(h.get("disc_distinct_colours", 0) for h in hits)
|
|
if worst > 32:
|
|
doc["verdict"] = "disc-holds-the-raw-data"
|
|
doc["explanation"] = (
|
|
"the ISO's own sectors at the logged LBAs contain the "
|
|
"many-colour raw data. psx-runtime delivered exactly what was "
|
|
"asked: the divergence is UPSTREAM -- in why the game "
|
|
"requested this load or where it pointed the DMA -- not in "
|
|
"the CD stack's data path.")
|
|
else:
|
|
doc["verdict"] = "disc-differs-from-ram"
|
|
doc["explanation"] = (
|
|
"the ISO's sectors at the logged LBAs do NOT contain the "
|
|
"many-colour data that landed in RAM: the CD stack delivered "
|
|
"wrong bytes for these reads. That is the bug's home.")
|
|
print(f"\nVERDICT: {doc['verdict']}\n{doc['explanation']}")
|
|
else:
|
|
print("\npass --iso /path/to/disc.bin to check what the disc itself "
|
|
"holds at these LBAs -- that is the half that decides.")
|
|
|
|
os.makedirs(os.path.dirname(args.out) or ".", exist_ok=True)
|
|
with open(args.out, "w") as f:
|
|
json.dump(doc, f, indent=1)
|
|
print(f"report: {args.out}")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|