Files
psxrecomp/tools/tests/test_colour_source.py
T
Alex Vanderveen 11f14a71c8 colour_source: read the oracle's source pointer via pc_break
psx-runtime's $s4 covers 0x800E260C..0x800E2664 and reads a table of
exactly 3 colour words, identical on both emulators -- so this routine
can only ever emit 3 colours. Yet native's packets carry (117,0,0),
(106,0,0), (94,0,0), which cannot come from source (248,136,8) at any
scale: R=117 would force G=63, not 0. Something is reading a different
table.

DuckStation has pc_break (which captures every GPR) but no pc_probe, so
--oracle-pointer breaks once and reports which table its $s4 names. One
hit answers it: same range means the source cannot explain the packet
difference; a different range means psx-runtime is reading the wrong
table, which would be the bug itself.

Uses OracleBreak so the breakpoint cannot outlive the tool -- tested.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 12:55:58 -04:00

137 lines
4.7 KiB
Python

"""Tests for locating and decoding the effect's colour source table."""
import os
import sys
import unittest
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import colour_source as cs # noqa: E402
def sample(v, reg="s4"):
return {"regs": {reg: f"0x{v:08X}"}}
class PointerSpanTest(unittest.TestCase):
def test_span_widened_backwards(self):
"""The code reads $s4-12..$s4-1, so the table starts before $s4."""
(lo, hi), vals = cs.pointer_span([sample(0x80100010),
sample(0x80100020)])
self.assertEqual(lo, 0x80100010 - cs.SRC_BACK)
self.assertEqual(hi, 0x80100020)
self.assertEqual(vals, [0x80100010, 0x80100020])
def test_no_samples_returns_none(self):
self.assertIsNone(cs.pointer_span([{"regs": {}}, {}]))
def test_duplicate_pointers_collapsed(self):
_, vals = cs.pointer_span([sample(0x80100010)] * 5)
self.assertEqual(vals, [0x80100010])
class DecodeTest(unittest.TestCase):
def test_decodes_little_endian_colour_words(self):
blob = bytes([0x93, 0x50, 0x04, 0x00, # (147, 80, 4)
0x93, 0x93, 0x68, 0x00]) # (147, 147, 104)
cols = cs.colours_in(blob, 0x80100000, 0x80100000, 0x80100004)
self.assertEqual(cols[(147, 80, 4)], 1)
self.assertEqual(cols[(147, 147, 104)], 1)
def test_command_byte_ignored(self):
"""The top byte is the GP0 command, not colour."""
blob = bytes([0x93, 0x50, 0x04, 0x38])
cols = cs.colours_in(blob, 0x80100000, 0x80100000, 0x80100000)
self.assertEqual(list(cols), [(147, 80, 4)])
def test_out_of_range_offsets_skipped(self):
cols = cs.colours_in(b"\x01\x02\x03\x04", 0x80100000,
0x80100000, 0x80100100)
self.assertEqual(sum(cols.values()), 1)
class VerdictTest(unittest.TestCase):
def test_uniform_table_points_downstream(self):
v, why = cs.verdict_of(3)
self.assertEqual(v, "source-is-uniform")
self.assertIn("not in the table", why)
def test_varied_table_points_at_its_filler(self):
v, why = cs.verdict_of(153)
self.assertEqual(v, "source-is-varied")
self.assertIn("FILLS this table", why)
def test_empty_refuses(self):
v, _ = cs.verdict_of(0)
self.assertEqual(v, "no-source")
def test_boundary_is_inclusive(self):
self.assertEqual(cs.verdict_of(8)[0], "source-is-uniform")
self.assertEqual(cs.verdict_of(9)[0], "source-is-varied")
class OraclePointerTest(unittest.TestCase):
"""DuckStation has pc_break (all GPRs) but no pc_probe.
One hit is enough: the question is which table the pointer names, not how
it varies. The breakpoint must not survive -- a leaked one re-pauses the
emulator and outlives the tool that set it.
"""
class Conn:
def __init__(self, vals):
self.vals = list(vals)
self.armed = set()
self.sent = []
def _d(self, name, **kw):
self.sent.append(name)
if name == "pc_break":
a = kw.get("addr")
if a in self.armed:
return {"ok": False, "slot": -1}
self.armed.add(a)
return {"ok": True, "slot": 0}
if name == "pc_unbreak":
self.armed.discard(kw.get("addr"))
return {"ok": True}
if name == "pc_break_list":
return {"breaks": sorted(self.armed)}
if name == "pc_hit_last":
if not self.vals:
return {"valid": False}
v = self.vals.pop(0)
return {"valid": True, "regs": {"s4": f"0x{v:08X}"}}
return {}
def cmd(self, n, **k):
return self._d(n, **k)
def raw(self, n, **k):
return self._d(n, **k)
def setUp(self):
import psx_gpu_frame
self.m = psx_gpu_frame
self._s = cs.time.sleep
cs.time.sleep = lambda *_: None
self._s2 = psx_gpu_frame.time.sleep
psx_gpu_frame.time.sleep = lambda *_: None
def tearDown(self):
cs.time.sleep = self._s
self.m.time.sleep = self._s2
def test_collects_distinct_pointers(self):
c = self.Conn([0x800E2610, 0x800E2610, 0x800E2640])
vals = cs.oracle_pointer(c, "0x80068450", tries=3)
self.assertEqual(vals, [0x800E2610, 0x800E2640])
def test_breakpoint_never_leaks(self):
c = self.Conn([0x800E2610])
cs.oracle_pointer(c, "0x80068450", tries=2)
self.assertEqual(c.armed, set())
def test_no_hits_returns_empty_not_error(self):
c = self.Conn([])
self.assertEqual(cs.oracle_pointer(c, "0x80068450", tries=2), [])