"""Run the emulated original and the Python port side by side.""" import math, sys, random sys.path.insert(0, __file__.rsplit('/', 1)[0]) sys.path.insert(0, str(__import__('pathlib').Path(__file__).resolve().parents[2])) import numpy as np from plugin import Plugin from lintunes.kaleidostrobe.engine import Kaleidostrobe def rotz(a): r = math.radians(a); c, s = math.cos(r), math.sin(r) m = np.eye(4); m[0, 0] = c; m[0, 1] = -s; m[1, 0] = s; m[1, 1] = c return m def trans(x, y, z): m = np.eye(4); m[:3, 3] = (x, y, z); return m def gl_triangles(calls): """(texture, alpha, [(st×3, eye-xyz×3)]) per glBegin batch, plus textures.""" stack = [np.eye(4)]; tex = None; alpha = 1.0; st = (0, 0) out = []; cur = None; mode = None for name, a in calls: M = stack[-1] if name == '_glLoadIdentity': stack[-1] = np.eye(4) elif name == '_gluLookAt': stack[-1] = stack[-1] @ trans(0, 0, -15) elif name == '_glPushMatrix': stack.append(stack[-1].copy()) elif name == '_glPopMatrix': stack.pop() elif name == '_glRotatef': stack[-1] = stack[-1] @ rotz(a[0]) elif name == '_glTranslatef': stack[-1] = stack[-1] @ trans(*a[:3]) elif name == '_glBindTexture': tex = a[1] elif name == '_glColor4f': alpha = a[3] elif name == '_glBegin': cur = []; mode = a[0] elif name == '_glTexCoord2f': st = a[:2] elif name in ('_glVertex2f', '_glVertex3f'): v = (a[0], a[1], a[2] if name == '_glVertex3f' else 0.0, 1.0) cur.append((st, (M @ np.array(v))[:3])) elif name == '_glEnd': tris = [] if mode == 4: tris = [cur[i:i + 3] for i in range(0, len(cur), 3)] elif mode == 6: tris = [[cur[0], cur[i], cur[i + 1]] for i in range(1, len(cur) - 1)] for t in tris: out.append((tex, alpha, t)) return out def engine_triangles(frame): out = [] for layer in frame.layers: base = trans(0, 0, -15) @ trans(0, 0, layer.lift) @ rotz(layer.turn) for t in layer.triangles: M = base @ rotz(t.turn) out.append((layer.texture + 1, layer.alpha, [(t.st[i], (M @ np.array((*t.xyz[i], 1.0)))[:3]) for i in range(3)])) return out def spectra(frame, rnd): """Fake iTunes spectra: a kick every ~13 frames, noise elsewhere.""" kick = (frame % 13) < 2 row = [rnd.randrange(0, 40) for _ in range(512)] for b in range(4): row[b] = rnd.randrange(180, 256) if kick else rnd.randrange(20, 120) for b in range(4, 32): row[b] = rnd.randrange(60, 200) return [row, row[:]] def main(frames=200, seed_ticks=1000, w=640, h=480): p = Plugin(rect=(0, 0, h, w), ticks=seed_ticks) e = Kaleidostrobe(seed=seed_ticks, ticks=seed_ticks) e.set_size(w, h) rnd = random.Random(7) tex_bad = geo_bad = 0 t0 = p.k.micro for f in range(frames): spec = spectra(f, rnd) start = len(p.k.gl) p.k.micro += 40001 ticks = seed_ticks + (p.k.micro - t0) * 60 // 1_000_000 calls = p.frame(None, spec, dt_us=0, dticks=ticks - p.k.ticks) ours = e.step(np.array(spec, np.uint8), ticks) # textures: the two glTexImage2D uploads of this frame idx = [i for i in range(start, len(p.k.gl)) if p.k.gl[i][0] == '_glTexImage2D'] theirs = [np.frombuffer(p.k.textures[i], np.uint8).reshape(128, 128, 4) for i in idx] for t in range(2): d = int((theirs[t] != ours.textures[t]).any(-1).sum()) if d: tex_bad += 1 if tex_bad <= 5: print(f'frame {f} tex {t}: {d} texels differ') a, b = gl_triangles(calls), engine_triangles(ours) ok = len(a) == len(b) and all( x[0] == y[0] and abs(x[1] - y[1]) < 1e-6 and all(np.allclose(x[2][i][0], y[2][i][0], atol=1e-4) and np.allclose(x[2][i][1], y[2][i][1], atol=1e-3) for i in range(3)) for x, y in zip(a, b)) if not ok: geo_bad += 1 if geo_bad <= 3: print(f'frame {f}: geometry differs ({len(a)} vs {len(b)} triangles)') for x, y in list(zip(a, b))[:2]: print(' orig', x); print(' port', y) if f % 50 == 0: print(f'frame {f}: beat={ours.beat} beats={e.beat_count} objects={e.bottom_object},{e.top_object} ' f'map={e.cur_map} wave={e.cur_wave} delta={e.cur_delta}') print(f'{frames} frames: texture mismatches {tex_bad}, geometry mismatches {geo_bad}') if __name__ == '__main__': main(int(sys.argv[1]) if len(sys.argv) > 1 else 200)