A per-sprite premultiplied-vs-straight-alpha flag would matter a lot to a port and has a sharp static signature: premultiplied means RGB <= A everywhere. Over the 170 decoded GP_TITLE textures that pair to a flag word: bit SET n= 61 mean %(RGB>A) 55.52 median 52.52 bit clear n=109 mean %(RGB>A) 33.66 median 30.17 Premultiplied requires ~0% for the flagged group. Both groups are far from it and the flagged group violates MORE -- the opposite of the hypothesis. Refuted. What remains is a weak association: flagged sprites carry more bright-RGB/low-alpha pixels, which is what glow art looks like. But the best single threshold classifies 76.5% against a 64.1% base rate -- a 12-point lift with badly overlapping distributions. A tendency, not a rule, and reported with its base rate so it cannot read as more. Noted for whoever returns: "0x02 selects an additive blend" was refuted by blending those sprites additively and finding every measure worse against the capture -- but that ran through a title render since fixed twice (rest_plateau, and the 8AX background the composer drops). The refutation may well stand; it was measured through a renderer with known other errors, so it is worth one re-run if blit ever gains additive blending. Parking the field. Four candidate meanings are dead -- additive blend, eff name in both directions, transient element, premultiplied alpha -- none produced a positive account, and the bit blocks nothing: the port's screens composite at 0.947 correlation against a capture without it. The negative space and the sound attribution method (child order, not size) are written down so a later attempt starts here. METHOD: report a classifier's lift over its base rate; and park a field after N failed hypotheses, saying what was eliminated.
59 lines
2.8 KiB
Python
Executable File
59 lines
2.8 KiB
Python
Executable File
"""Does bit 0x02 separate sprites by their RGB-vs-alpha content?
|
|
|
|
Premultiplied alpha predicts RGB <= A everywhere for the flagged group. Tested
|
|
below and refuted. What remains is a description: how often RGB exceeds A, which
|
|
is the signature of glow art (bright colour carried at low alpha).
|
|
|
|
Bit comes from the T8aD header; the name from the string immediately preceding
|
|
it (validated 17/18 on build 4 against the RATC child order).
|
|
"""
|
|
import struct, zlib, glob, re, os
|
|
import numpy as np
|
|
from PIL import Image
|
|
NAME = re.compile(rb'[A-Za-z0-9_.]{2,31}\x00')
|
|
base = "/work/sylph_extract/dat/GP_TITLE"
|
|
stub = open(base + ".pak", "rb").read()
|
|
n = struct.unpack_from(">I", stub, 4)[0]
|
|
blob = b"".join(open(s, "rb").read() for s in sorted(glob.glob(base + ".p[0-9][0-9]")))
|
|
flags = {} # (entry_hash, name, w, h) -> flags
|
|
for i in range(n):
|
|
h_, off, sz = struct.unpack_from(">III", stub, 0x10 + 12 * i)
|
|
st = blob[off:off + sz]
|
|
if len(st) < 10: continue
|
|
try: d = zlib.decompress(st[10:]) if st[:2] == b"Z1" else st
|
|
except Exception: continue
|
|
for m in re.finditer(b"T8aD", d):
|
|
o = m.start()
|
|
try:
|
|
fl = struct.unpack_from(">I", d, o + 4)[0]
|
|
w = struct.unpack_from(">I", d, o + 0x14)[0]
|
|
hh = struct.unpack_from(">I", d, o + 0x18)[0]
|
|
except Exception: continue
|
|
if not (0 < w <= 4096 and 0 < hh <= 4096): continue
|
|
ms = list(NAME.finditer(d[max(0, o - 64):o]))
|
|
if not ms: continue
|
|
flags[(f"{h_:08x}", ms[-1].group()[:-1].decode("latin1"), w, hh)] = fl
|
|
rows = []
|
|
for f in sorted(glob.glob("/tmp/tex/*.png")):
|
|
b = os.path.basename(f)
|
|
m = re.match(r"([0-9a-f]{8})_(.+)_(\d+)x(\d+)\.png$", b)
|
|
if not m: continue
|
|
key = (m.group(1), m.group(2), int(m.group(3)), int(m.group(4)))
|
|
fl = flags.get(key)
|
|
if fl is None: continue
|
|
a = np.asarray(Image.open(f).convert("RGBA")).astype(int)
|
|
rgb = a[:, :, :3].max(axis=2); al = a[:, :, 3]
|
|
rows.append((bool(fl & 2), 100 * float((rgb > al).mean()), m.group(2)))
|
|
s = [r[1] for r in rows if r[0]]; c = [r[1] for r in rows if not r[0]]
|
|
print(f"matched {len(rows)} decoded textures to a T8aD flag word")
|
|
print(f" bit SET n={len(s):3d} mean %(RGB>A) {np.mean(s):6.2f} median {np.median(s):6.2f}")
|
|
print(f" bit clear n={len(c):3d} mean %(RGB>A) {np.mean(c):6.2f} median {np.median(c):6.2f}")
|
|
print(f"\n premultiplied would require ~0% for the flagged group -> REFUTED")
|
|
# separability: what threshold best splits them, and how well?
|
|
best = (0, None)
|
|
for t in np.arange(0, 100, 0.5):
|
|
acc = (sum(x > t for x in s) + sum(x <= t for x in c)) / len(rows)
|
|
if acc > best[0]: best = (acc, t)
|
|
print(f" best single-threshold accuracy: {100*best[0]:.1f}% at %(RGB>A) > {best[1]}")
|
|
print(f" (base rate, always-guess-majority: {100*max(len(s),len(c))/len(rows):.1f}%)")
|