v0.53.0: Kaleidostrobe, the 2002 iTunes visualizer, as the fourth style

Reverse-engineered from its PowerPC binary and checked against the original
running under emulation: textures byte for byte and every triangle, over
3,600 frames. 25 fps like the plugin; its colour maps and wave images are
extracted from its own code; tests/kaleidostrobe_golden.npz was recorded
from the original.
This commit is contained in:
2026-10-14 12:02:22 -07:00
parent 6178f5d0f7
commit e38c751e39
21 changed files with 2109 additions and 3 deletions
+85
View File
@@ -0,0 +1,85 @@
"""Extract Kaleidostrobe's deterministic tables by running the original code.
- 22 colour maps (makeMap000-021): mapRGB[0], 256 RGBA entries
- base wave images (initWaveShape000-022), *before* their random transform()
(transform is stubbed out here; the port applies it with its own rand()),
plus the gWaveIncScale each one sets
Every call is checked to touch nothing but its output.
"""
import struct, sys
import numpy as np
sys.path.insert(0, __file__.rsplit('/', 1)[0])
import macho
from plugin import Plugin
from unicorn import UC_HOOK_CODE
MAPRGB = 0x23fd4
TEXIDX = 0x457f8 # textureImageIndexArrays
CURTEX = 0x13dbc
CURFRAME = 0x13dc0
WAVEINC = 0x257f0 # gWaveIncScale (2 floats)
p = Plugin(seed=1)
k = p.k
tr = macho.func_size['_transform'][0]
k.u.hook_add(UC_HOOK_CODE, lambda u, a, s, d: k._ret(), begin=tr, end=tr + 3)
k.u.ctl_remove_cache(0, 0x80000)
def snapshot():
return bytearray(k.mem(0x13000, 0x759f8 - 0x13000))
def changed(before, after):
b = np.frombuffer(bytes(before), np.uint8); a = np.frombuffer(bytes(after), np.uint8)
idx = np.nonzero(a != b)[0] + 0x13000
return idx
def regions(idx):
out = set()
for a in idx[::max(1, len(idx)//200)]:
out.add(macho.symfor(int(a)).split('+')[0])
return sorted(out)
k.w32(CURTEX, 0)
k.w32(CURFRAME, 0); k.w32(CURFRAME + 4, 0)
maps = np.zeros((22, 256, 4), np.uint8)
for m in range(22):
k.u.mem_write(MAPRGB, b'\x55' * 1024)
before = snapshot()
k.call(f'_makeMap{m:03d}')
after = snapshot()
idx = changed(before, after)
stray = idx[(idx < MAPRGB) | (idx >= MAPRGB + 1024)]
data = np.frombuffer(k.mem(MAPRGB, 1024), np.uint8).reshape(256, 4)
untouched = int((np.frombuffer(k.mem(MAPRGB, 1024), np.uint8) == 0x55).sum())
maps[m] = data
print(f'map {m:03d}: stray={regions(stray) if len(stray) else "-"} 0x55-left={untouched}')
WAVES = list(range(23)) + [31, 32, 33]
waves = {}
incs = {}
for w in WAVES:
if w >= 31:
continue
for fill in (0x00, 0xff):
k.u.mem_write(TEXIDX, bytes([fill]) * 0x8000)
k.wf(WAVEINC, -999.0)
before = snapshot()
k.call(f'_initWaveShape{w:03d}')
after = snapshot()
img = np.frombuffer(k.mem(TEXIDX, 0x4000), np.uint8).reshape(128, 128).copy()
if fill == 0:
img0 = img
else:
img1 = img
idx = changed(before, after)
stray = idx[((idx < TEXIDX) | (idx >= TEXIDX + 0x4000)) & ((idx < WAVEINC) | (idx >= WAVEINC + 4))]
partial = int((img0 != img1).sum())
waves[w] = img0
incs[w] = k.rf(WAVEINC)
print(f'wave {w:03d}: inc={incs[w]!r} partial-pixels={partial} stray={regions(stray) if len(stray) else "-"}')
np.savez_compressed(sys.argv[1] if len(sys.argv) > 1 else 'tables.npz', maps=maps,
waves=np.stack([waves[w] for w in range(23)]),
wave_inc=np.array([incs[w] for w in range(23)], np.float32))
print('saved')