mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-28 19:07:47 -04:00
296981c9bc
Two new instruments, and a result that narrows the search a long way. gte_check.py re-derives the GTE's depth-cue interpolation from its own recorded inputs and compares against the hardware description, written from the spec rather than copied from runtime/src/gte.cpp -- a reference copied from the implementation under test agrees with it by construction and proves nothing. It needs no oracle: a mismatch is a bug in our GTE, a match means the inputs were already wrong when they arrived and the fault is upstream. That distinction is the actionable part. Pointed at the live runtime it reports something better than a match. Legend of Mana issues ZERO INTPL operations (nintpl=0 across every recorded frame), and nsat=0 -- nothing saturates in projection either, across 1081 projections a frame. The depth-cue path is not merely correct here, it is not used. Vertex colours are written by plain CPU stores from overlay code (dma=-1, PCs above 0x8003F000, invisible to psxrecomp-analyze), not computed by GTE lighting. So the colour divergence is recompiled CPU arithmetic in an overlay, not the GTE library. That is worth having ruled out; it also means the fingerprint's pc_hash is the right next instrument rather than any GPU-side tool. packet_writers.py finds which instruction writes a primitive's COLOUR words, by address rather than by function -- an ordering-table game reports one submit PC for every packet, so attribution cannot answer it. Field addresses are derived from each decoded packet's own layout. A first attempt assumed a fixed 0x24 stride and classified every write in the buffer against it; when the scene changed the assumption slipped and it produced a confident table naming the wrong instructions, with nothing in the output to say so. Stride depends on the opcode and a buffer holds several, so the layout is now read off the packet. The tool says plainly when the effect is not on screen instead of reporting on whatever happens to be in the buffer. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
92 lines
3.3 KiB
Python
92 lines
3.3 KiB
Python
#!/usr/bin/env python3
|
|
"""Packet field mapping: which words of a packet are COLOUR words.
|
|
|
|
Getting this wrong is worse than not having it. A wrong map still produces a
|
|
confident-looking table of "instructions that write colour" -- it just names
|
|
the wrong instructions, and the reader has no way to tell. So the layout is
|
|
derived from each decoded packet rather than from an assumed stride, and these
|
|
tests pin the four layouts that matter.
|
|
"""
|
|
|
|
import importlib.util
|
|
import struct
|
|
import sys
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
sys.path.insert(0, str(ROOT))
|
|
|
|
|
|
def _load(name):
|
|
spec = importlib.util.spec_from_file_location(name, ROOT / f"{name}.py")
|
|
mod = importlib.util.module_from_spec(spec)
|
|
assert spec.loader is not None
|
|
spec.loader.exec_module(mod)
|
|
return mod
|
|
|
|
|
|
GF = _load("psx_gpu_frame")
|
|
PW = _load("packet_writers")
|
|
|
|
|
|
def packet(op, payload, at=0x1000):
|
|
ram = bytearray(GF.RAM_SIZE)
|
|
words = [((len(payload) & 0xFF) << 24) | GF.OT_END] + payload
|
|
for i, w in enumerate(words):
|
|
struct.pack_into("<I", ram, at + 4 * i, w & 0xFFFFFFFF)
|
|
return GF.decode_entries(GF.walk_ordering_table(bytes(ram), at))
|
|
|
|
|
|
class TestFieldMap(unittest.TestCase):
|
|
def roles(self, prims, want):
|
|
m = PW.field_map(prims, want)
|
|
return [m[a] for a in sorted(m)]
|
|
|
|
def test_gouraud_quad_interleaves_colour_and_vertex(self):
|
|
p = packet(0x38, [(0x38 << 24) | 1, 0x10, 2, 0x20, 3, 0x30, 4, 0x40])
|
|
self.assertEqual(self.roles(p, "PolyG4|x"),
|
|
["colour", "vertex"] * 4)
|
|
|
|
def test_flat_triangle_has_one_leading_colour(self):
|
|
p = packet(0x20, [(0x20 << 24) | 1, 0x10, 0x20, 0x30])
|
|
self.assertEqual(self.roles(p, "PolyF3|x"),
|
|
["colour", "vertex", "vertex", "vertex"])
|
|
|
|
def test_textured_flat_quad_carries_a_uv_after_each_vertex(self):
|
|
p = packet(0x2C, [(0x2C << 24) | 1, 0x10, 0, 0x20, 0, 0x30, 0, 0x40, 0])
|
|
self.assertEqual(self.roles(p, "PolyFT4|x"),
|
|
["colour"] + ["vertex", "uv"] * 4)
|
|
|
|
def test_textured_gouraud_quad(self):
|
|
p = packet(0x3C, [(0x3C << 24) | 1, 0x10, 0, 2, 0x20, 0,
|
|
3, 0x30, 0, 4, 0x40, 0])
|
|
self.assertEqual(self.roles(p, "PolyGT4|x"),
|
|
["colour", "vertex", "uv"] * 4)
|
|
|
|
def test_the_class_filter_excludes_other_opcodes(self):
|
|
ram = bytearray(GF.RAM_SIZE)
|
|
|
|
def put(off, *w):
|
|
for i, x in enumerate(w):
|
|
struct.pack_into("<I", ram, off + 4 * i, x & 0xFFFFFFFF)
|
|
|
|
put(0x1000, (8 << 24) | 0x2000, (0x38 << 24) | 1, 0x10, 2, 0x20,
|
|
3, 0x30, 4, 0x40)
|
|
put(0x2000, (4 << 24) | GF.OT_END, (0x20 << 24) | 9, 0x10, 0x20, 0x30)
|
|
prims = GF.decode_entries(GF.walk_ordering_table(bytes(ram), 0x1000))
|
|
only_g4 = PW.field_map(prims, "PolyG4|x")
|
|
self.assertEqual(len(only_g4), 8, "should map the gouraud quad alone")
|
|
|
|
def test_addresses_are_absolute_not_offsets(self):
|
|
p = packet(0x38, [(0x38 << 24) | 1, 0x10, 2, 0x20, 3, 0x30, 4, 0x40],
|
|
at=0x10D1C0)
|
|
m = PW.field_map(p, "PolyG4|x")
|
|
# src is the payload start, i.e. the tag address + 4.
|
|
self.assertIn(0x10D1C4, m)
|
|
self.assertEqual(m[0x10D1C4], "colour")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|