Files
psxrecomp/tools/tests/test_wedge_scan.py
T
Alex Vanderveen 86b85fec2c wedge_scan: locate the effect by additive count; scan the whole ring
The first run ranked 120 frames that were byte-identical (4 big untextured
polys, hub 2, 5 colours) and saved frame 21501 -- which has additive=0.
bad.json has 258 additive prims and good.json 180, so the window scanned
held no glow at all: the animation was never inside it, and the ranking
was a ranking of copies of one static frame.

Two fixes. Count blend modes per frame, and say plainly when a window
contains no additive primitives instead of printing a confident ranking
over identical rows -- 'the effect did not play inside this window' is
the finding there, not whichever frame sorted first. And default --last
to the whole ring (stride 4), since the effect is usually not in the
newest frames; --last 240 covered 21261..21501 while the ring held from
19917.

Adds --rank glow to sort by additive count directly.

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

149 lines
5.5 KiB
Python

"""Tests for wedge_scan's frame profiling and ranking.
The point of this tool is that no threshold decides anything -- a wedge frame
has to sort above ordinary frames of the same scene. These tests pin the
ranking behaviour and the frame-selection arithmetic.
"""
import json
import os
import sys
import unittest
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import wedge_scan # noqa: E402
FRAMES_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(
os.path.dirname(os.path.abspath(__file__))))), "analysis", "frames")
def poly(verts, textured=False, colors=None):
return {"op_name": "PolyG3+semi", "kind": "poly", "semi": True,
"gouraud": True, "textured": textured, "stp": 1, "verts": verts,
"colors": colors or [[64, 64, 64]] * len(verts)}
def ordinary_frame(n=50):
"""Small glow-mesh triangles plus a couple of big vignette pieces."""
prims = [poly([[13, 205], [39, 224], [3, 238]]),
poly([[205, 238], [3, 238], [39, 224]])]
prims += [poly([[140 + i, 70], [148 + i, 74], [143 + i, 78]])
for i in range(n)]
return {"frame": 10, "prims": prims}
def fan_frame(spokes=16):
"""Big untextured triangles all sharing one hub vertex."""
prims = []
for i in range(spokes):
a = [-232 + i * 40, -2]
b = [-232 + (i + 1) * 40, 238]
prims.append(poly([[160, 120], a, b]))
return {"frame": 20, "prims": prims}
class MetricsTest(unittest.TestCase):
def test_fan_hub_is_found(self):
m = wedge_scan.frame_metrics(fan_frame())
self.assertEqual(m["fan_hub"], [160, 120])
self.assertEqual(m["fan_hub_count"], 16)
def test_big_untextured_counted(self):
m = wedge_scan.frame_metrics(fan_frame())
self.assertEqual(m["big_untextured"], 16)
self.assertEqual(m["big_textured"], 0)
def test_textured_counted_separately_not_dropped(self):
"""A wrong guess about which class carries the wedges must stay visible."""
d = fan_frame()
for p in d["prims"]:
p["textured"] = True
m = wedge_scan.frame_metrics(d)
self.assertEqual(m["big_untextured"], 0)
self.assertEqual(m["big_textured"], 16)
def test_extent_covers_offscreen(self):
m = wedge_scan.frame_metrics(fan_frame())
self.assertLess(m["extent"][0], 0)
self.assertGreater(m["extent"][2], 320)
def test_degenerate_prims_ignored(self):
d = {"frame": 1, "prims": [poly([[5, 5], [5, 5]])]}
self.assertEqual(wedge_scan.frame_metrics(d)["big_untextured"], 0)
class RankingTest(unittest.TestCase):
def test_fan_outranks_ordinary(self):
rows = [wedge_scan.frame_metrics(ordinary_frame()),
wedge_scan.frame_metrics(fan_frame())]
rows.sort(key=wedge_scan.rank_key, reverse=True)
self.assertEqual(rows[0]["frame"], 20)
def test_real_captures_rank_low_against_a_fan(self):
"""bad.json/good.json are ordinary lists; a fan must beat both."""
rows = []
for name in ("bad.json", "good.json"):
p = os.path.join(FRAMES_DIR, name)
if not os.path.exists(p):
self.skipTest(f"{name} not on disk")
with open(p) as f:
rows.append(wedge_scan.frame_metrics(json.load(f)))
rows.append(wedge_scan.frame_metrics(fan_frame()))
rows.sort(key=wedge_scan.rank_key, reverse=True)
self.assertEqual(rows[0]["frame"], 20)
class ScanFramesTest(unittest.TestCase):
def test_newest_first(self):
f = wedge_scan.scan_frames({"newest": 500, "oldest": 100}, 10, 1)
self.assertEqual(f[0], 500)
self.assertEqual(f[-1], 491)
def test_stride_applied(self):
f = wedge_scan.scan_frames({"newest": 500, "oldest": 100}, 10, 2)
self.assertEqual(f, [500, 498, 496, 494, 492])
def test_clamped_to_oldest(self):
f = wedge_scan.scan_frames({"newest": 110, "oldest": 100}, 999, 1)
self.assertEqual(min(f), 100)
def test_last_zero_means_whole_ring(self):
f = wedge_scan.scan_frames({"newest": 110, "oldest": 100}, 0, 1)
self.assertEqual(len(f), 11)
if __name__ == "__main__":
unittest.main()
class BlendMetricsTest(unittest.TestCase):
"""Additive count is what locates the effect; a window with none of it
did not contain the animation, however the frames rank."""
def test_additive_and_subtractive_counted(self):
prims = [poly([[0, 0], [10, 0], [0, 10]]) for _ in range(5)]
for p in prims[:2]:
p["stp"] = 2
m = wedge_scan.frame_metrics({"frame": 1, "prims": prims})
self.assertEqual(m["additive"], 3)
self.assertEqual(m["subtractive"], 2)
def test_opaque_prims_not_counted_as_blended(self):
prims = [poly([[0, 0], [10, 0], [0, 10]]) for _ in range(4)]
for p in prims:
p["semi"] = False
m = wedge_scan.frame_metrics({"frame": 1, "prims": prims})
self.assertEqual(m["additive"], 0)
self.assertEqual(m["subtractive"], 0)
def test_effect_frames_have_glow_and_captured_frame_does_not(self):
"""The real captures: bad/good are inside the effect, wedge.json is not."""
cases = [("bad.json", True), ("good.json", True)]
for name, expect_glow in cases:
p = os.path.join(FRAMES_DIR, name)
if not os.path.exists(p):
self.skipTest(f"{name} not on disk")
with open(p) as f:
m = wedge_scan.frame_metrics(json.load(f))
self.assertEqual(m["additive"] > 0, expect_glow, name)