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.
254 lines
8.8 KiB
Python
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,)))
|