Files
lintunes/scripts/kaleidostrobe/emu.py
T
trav e38c751e39 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.
2026-10-14 12:02:22 -07:00

254 lines
8.8 KiB
Python

"""Run the original Kaleidostrobe PPC code under Unicorn, as iTunes would.
Imports are trapped at their __picsymbol_stub entries and answered in Python;
GL calls are recorded (name, args) so a frame's draw list can be compared.
"""
import math, struct, sys
sys.path.insert(0, __file__.rsplit('/', 1)[0])
import macho
from unicorn import Uc, UC_ARCH_PPC, UC_MODE_PPC32, UC_MODE_BIG_ENDIAN, UC_HOOK_CODE
from unicorn import UC_HOOK_MEM_UNMAPPED, UcError
import unicorn.ppc_const as P
R = [getattr(P, f'UC_PPC_REG_{i}') for i in range(32)]
F = [getattr(P, f'UC_PPC_REG_FPR{i}') for i in range(32)]
STUB0, STUBEND, STUBSZ = 0x11f5c, 0x12d00, 36
MAGIC = 0x7fff0 # return address: emu_stop when reached
STACK_TOP = 0x0ff0000
HEAP = 0x1000000
NOOPS = {'_GetPortForeColor', '_GetPortBackColor', '_RGBForeColor', '_RGBBackColor',
'_PaintRect', '_EraseRect', '_InsetRect', '_TextFont', '_TextSize', '_TextMode',
'_TETextBox', '_FMGetFontFamilyFromName', '_ShowWindow', '_HideWindow',
'_fprintf', '_fclose', '_strcat', '_MPBlockCopy'}
STUBS = {a: n for a, (t, n) in macho.stubname.items() if t == 'stub'}
class ParkMiller:
"""FreeBSD 4 / Darwin 6 libc rand(): RAND_MAX 0x7fffffff."""
def __init__(self, seed=1):
self.x = seed
def srand(self, s):
self.x = s & 0xffffffff
def rand(self):
x = self.x
if x == 0:
x = 123459876
hi, lo = divmod(x, 127773)
x = 16807 * lo - 2836 * hi
if x < 0:
x += 0x7fffffff
self.x = x
return x % 0x80000000
class KS:
def __init__(self, seed=1):
u = self.u = Uc(UC_ARCH_PPC, UC_MODE_PPC32 | UC_MODE_BIG_ENDIAN)
u.mem_map(0, 0x80000)
u.mem_write(0, macho.D[:0x24000])
u.mem_map(STACK_TOP - 0x100000, 0x110000)
u.mem_map(HEAP, 0x400000)
self.heap = HEAP
u.reg_write(P.UC_PPC_REG_MSR, u.reg_read(P.UC_PPC_REG_MSR) | 0x2000)
# the weak import aglChoosePixelFormat must look present
u.mem_write(0x23fb8, struct.pack('>I', 1))
# Carbon's MathLib `extern const double pi` (fp.h)
self.heap += 16
u.mem_write(HEAP, struct.pack('>d', math.pi))
u.mem_write(0x23fa4, struct.pack('>I', HEAP))
u.hook_add(UC_HOOK_CODE, self._stub, begin=STUB0, end=STUBEND - 1)
u.hook_add(UC_HOOK_CODE, self._magic, begin=MAGIC, end=MAGIC + 3)
itcall = macho.func_size['_ITCallApplication'][0]
u.hook_add(UC_HOOK_CODE, lambda u, a, s, d: self._ret(r3=0), begin=itcall, end=itcall + 3)
self.rng = ParkMiller(seed)
self.ticks = 1000
self.micro = 10_000_000
self.gl = []
self.textures = {}
self.trace_gl = True
# ---- memory helpers ----
def r32(self, a): return struct.unpack('>I', self.u.mem_read(a, 4))[0]
def w32(self, a, v): self.u.mem_write(a, struct.pack('>I', v & 0xffffffff))
def rf(self, a): return struct.unpack('>f', self.u.mem_read(a, 4))[0]
def wf(self, a, v): self.u.mem_write(a, struct.pack('>f', v))
def rd(self, a): return struct.unpack('>d', self.u.mem_read(a, 8))[0]
def wd(self, a, v): self.u.mem_write(a, struct.pack('>d', v))
def mem(self, a, n): return bytes(self.u.mem_read(a, n))
def alloc(self, n):
a = (self.heap + 15) & ~15
self.heap = a + n
self.u.mem_write(a, b'\0' * n)
return a
def reg(self, i): return self.u.reg_read(R[i])
def setreg(self, i, v): self.u.reg_write(R[i], v & 0xffffffff)
def freg(self, i):
v = self.u.reg_read(F[i])
return struct.unpack('>d', struct.pack('>Q', v & 0xffffffffffffffff))[0]
def setfreg(self, i, x):
self.u.reg_write(F[i], struct.unpack('>Q', struct.pack('>d', x))[0])
# ---- calling ----
def call(self, addr, *args, fargs=()):
if isinstance(addr, str):
addr = macho.func_size[addr][0]
u = self.u
sp = STACK_TOP - 0x400
u.reg_write(R[1], sp)
self.w32(sp, 0)
for i, a in enumerate(args[:8]):
self.setreg(3 + i, a)
for i, a in enumerate(args[8:]):
self.w32(sp + 24 + 4 * (8 + i), a)
for i, x in enumerate(fargs):
self.setfreg(1 + i, x)
u.reg_write(P.UC_PPC_REG_LR, MAGIC)
try:
u.emu_start(addr, MAGIC + 4)
except UcError as e:
pc = u.reg_read(P.UC_PPC_REG_PC)
raise RuntimeError(f'{e} at pc={pc:#x} ({macho.symfor(pc)})') from None
return self.reg(3)
def _magic(self, u, addr, size, ud):
u.emu_stop()
def _ret(self, r3=None, f1=None):
if r3 is not None:
self.setreg(3, r3)
if f1 is not None:
self.setfreg(1, f1)
self.u.reg_write(P.UC_PPC_REG_PC, self.u.reg_read(P.UC_PPC_REG_LR))
def stackarg(self, i):
sp = self.reg(1)
return self.r32(sp + 24 + 4 * i)
# ---- imports ----
def _stub(self, u, addr, size, ud):
if (addr - STUB0) % STUBSZ:
return
name = STUBS.get(addr, '?')
fn = getattr(self, 'imp' + name, None)
if fn is not None:
fn()
return
if name.startswith('_gl') or name.startswith('_agl'):
self._glcall(name)
return
if name in NOOPS:
self._ret(r3=0)
return
raise RuntimeError(f'unhandled import {name}')
def f(self, n): return [self.freg(1 + i) for i in range(n)]
def r(self, n): return [self.reg(3 + i) for i in range(n)]
def imp_sin(self): self._ret(f1=math.sin(self.freg(1)))
def imp_cos(self): self._ret(f1=math.cos(self.freg(1)))
def imp_tan(self): self._ret(f1=math.tan(self.freg(1)))
def imp_sqrt(self): self._ret(f1=math.sqrt(self.freg(1)) if self.freg(1) >= 0 else float('nan'))
def imp_floor(self): self._ret(f1=math.floor(self.freg(1)))
def imp_atan2(self): y, x = self.f(2); self._ret(f1=math.atan2(y, x))
def imp_pow(self):
x, y = self.f(2)
try:
v = math.pow(x, y)
except (ValueError, OverflowError):
v = float('nan')
self._ret(f1=v)
def imp_modf(self):
x = self.freg(1)
ip = float(math.trunc(x))
self.wd(self.reg(5), ip)
self._ret(f1=x - ip)
def imp_rand(self): self._ret(r3=self.rng.rand())
def imp_srand(self): self.rng.srand(self.reg(3)); self._ret()
def imp_TickCount(self): self._ret(r3=self.ticks)
def imp_Microseconds(self):
p = self.reg(3)
self.w32(p, self.micro >> 32); self.w32(p + 4, self.micro)
self._ret()
def imp_memcpy(self):
d, s, n = self.r(3)
self.u.mem_write(d, self.mem(s, n)); self._ret(r3=d)
def imp_BlockMoveData(self):
s, d, n = self.r(3)
self.u.mem_write(d, self.mem(s, n)); self._ret()
def imp_memset(self):
d, c, n = self.r(3)
self.u.mem_write(d, bytes([c & 0xff]) * n); self._ret(r3=d)
def imp_NewPtrClear(self): self._ret(r3=self.alloc(self.reg(3)))
def imp_NewRgn(self): self._ret(r3=self.alloc(16))
def imp_DisposePtr(self): self._ret()
def imp_DisposeRgn(self): self._ret()
def imp_GetPort(self): self.w32(self.reg(3), 0x4444); self._ret()
def imp_SetPort(self): self._ret()
def imp_EqualRect(self): self._ret(r3=0)
def imp_strlen(self):
a = self.reg(3); n = 0
while self.mem(a + n, 1) != b'\0': n += 1
self._ret(r3=n)
port_rect = (0, 0, 480, 640) # top, left, bottom, right
def imp_GetPortBounds(self):
rect = self.reg(4)
self.u.mem_write(rect, struct.pack('>4h', *self.port_rect))
self._ret(r3=rect)
def imp_PlayerGetPluginFileSpec(self): self._ret(r3=0)
# ---- GL / AGL ----
GL_FLOATS = {
'_glClearColor': 4, '_glColor4f': 4, '_glRotatef': 4, '_glTranslatef': 3,
'_glVertex2f': 2, '_glVertex3f': 3, '_glTexCoord2f': 2, '_gluPerspective': 4,
'_gluLookAt': 9,
}
def _glcall(self, name):
ret = 0
if name in self.GL_FLOATS:
args = tuple(self.f(self.GL_FLOATS[name]))
elif name == '_glTexEnvf':
args = (self.reg(3), self.reg(4), self.freg(1))
elif name == '_glTexImage2D':
args = tuple(self.r(8)) + (self.stackarg(8),)
w, h = args[3], args[4]
self.textures[len(self.gl)] = self.mem(args[8], w * h * 4)
elif name == '_glGenTextures':
n, p = self.r(2)
for i in range(n):
self.w32(p + 4 * i, 1 + i)
args = (n,)
elif name.startswith('_agl'):
args = tuple(self.r(4))
ret = 1 if name not in ('_aglGetError',) else 0
if name == '_aglCreateContext':
ret = 0xc0c0
else:
args = tuple(self.r(4))
if self.trace_gl:
self.gl.append((name, args))
self._ret(r3=ret)
if __name__ == '__main__':
k = KS()
print(k.call('_newValue', 3, fargs=(10.0,)))