Files
totk-pc-recomp/tests/diff/simd.py
T

243 lines
9.4 KiB
Python

"""Differential test of the portable FP/SIMD lowering (lifter simd='c') against Unicorn as a Cortex-A57.
Needs no game files. Enumerates every opcode bit pattern of the FP/SIMD data-processing space (bits 27..25 = 111),
the FP/SIMD load/store space and CRC32 with random register fields, and compares which encodings the lifter and
Unicorn accept. Then a stratified sample of encodings both accept is compiled and run with random interesting
operands (NaNs, infinities, denormals, +-0, saturation edges, random FPCR.DN/FZ/RMode/AHP): X0-X30, SP, NZCV, V0-V31
and, for stores, memory must match. FPSR is not compared; the lowering does not model the cumulative exception bits
or QC.
Adapted from LAS Recomp tools/test_simd.py @ da35b83 (MIT, Copyright (c) 2026 Peiikai).
Usage: simd.py [--encodings N] [--cases K] [--seed S] [--jobs J] [--no-validity] [--filter HEX/MASK]
"""
import argparse
import functools
import os
import platform
import random
import struct
import sys
import time
import zlib
from collections import Counter, defaultdict
import unicorn as uc
from unicorn import arm64_const as A
from common import BUF, BUF_SIZE, ROOT, UC_INVALID, Emu, build_harness, diff_state, dis, mnemonic, rand_fpcr, \
rand_v, rand_x, run_harness
from lifter import Lifter, Unsupported
print = functools.partial(print, flush=True)
BASE_AREA = BUF + 0x8000
CODE, CODE_SIZE = 0x7210000000, 0x400000
def is_ldst(w):
return (w >> 25) & 1 == 0 and (w >> 27) & 1 == 1
def is_store(w):
if not is_ldst(w):
return False
if (w & 0x3B000000) == 0x18000000: # literal
return False
return not (w >> 22) & 1 # L bit, same position for structures, pairs and single registers
def is_literal(w):
return (w & 0x3F000000) == 0x1C000000
def class_key(w):
"""Opcode signature: register fields (Rd, Rn, Rm) cleared."""
if is_ldst(w):
return w & 0xFFE0FC00 if (w & 0xBE000000) == 0x0C000000 else w & 0xFFFFFC00
return w & 0xFFE0FC00
def candidates(r):
"""Every opcode bit pattern of the FP/SIMD data-processing and load/store spaces, random registers."""
out = []
for p in range(1 << 19):
hi, mid = p >> 15, p & 0x7FFF
rr = r.getrandbits(10)
out.append((hi << 28) | (0b111 << 25) | (mid << 10) | rr) # data processing
out.append((hi << 28) | (1 << 27) | (1 << 26) | (mid << 10) | rr) # loads/stores
for sf in (0, 1):
for C in (0, 1):
for sz in range(4):
for _ in range(4):
out.append((sf << 31) | (0b0011010110 << 21) | (r.getrandbits(5) << 16) | (0b010 << 13) |
(C << 12) | (sz << 10) | r.getrandbits(10))
return out
def validity(lifter, ws, emu):
"""Map word -> (lifter accepts, Unicorn accepts); words are placed at CODE + 4i."""
mu = emu.mu
res = {}
step = CODE_SIZE // 4
for base in range(0, len(ws), step):
chunk = ws[base:base + step]
mu.mem_write(CODE, struct.pack(f'<{len(chunk)}I', *chunk))
mu.ctl_remove_cache(CODE, CODE + CODE_SIZE)
state = list(zip(emu.xregs, [BASE_AREA] * 31)) + [(A.UC_ARM64_REG_SP, BASE_AREA)]
for i, w in enumerate(chunk):
pc = CODE + 4 * i
try:
lifter.lift(w, pc)
lok = True
except Unsupported:
lok = False
if i % 32 == 0:
mu.reg_write_batch(state)
try:
mu.emu_start(pc, pc + 4, count=1)
uok = True
except uc.UcError as e:
uok = e.errno not in UC_INVALID
res[w] = (lok, uok)
return res
def make_case(r, w):
fpcr = rand_fpcr(r)
v = [rand_v(r) for _ in range(32)]
nzcv = r.getrandbits(4) << 28
sp = BASE_AREA + r.randrange(0, 0x100, 16)
if is_ldst(w):
x = [BASE_AREA + r.randrange(0, 0x100) for _ in range(31)]
rm = (w >> 16) & 31
if rm < 31 and rm != (w >> 5) & 31:
reg_off = (w & 0x3B200C00) == 0x38200800 # register offset
post_reg = (w & 0xBF800000) in (0x0C800000, 0x0D800000)
if reg_off or post_reg:
x[rm] = r.randrange(0, 0x100)
else:
x = [rand_x(r) for _ in range(31)]
return fpcr, x, sp, nzcv, v
def main():
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
ap.add_argument('--encodings', type=int, default=20000)
ap.add_argument('--cases', type=int, default=8)
ap.add_argument('--seed', type=int, default=1)
ap.add_argument('--jobs', type=int, default=os.cpu_count() or 4)
ap.add_argument('--no-validity', action='store_true', help='skip the full validity enumeration')
ap.add_argument('--filter', help='only encodings with (w & MASK) == VAL, as VAL/MASK in hex')
ap.add_argument('--show', type=int, default=40)
ap.add_argument('--cflags', default='-march=x86-64-v3' if platform.machine().lower() in ('amd64', 'x86_64') else '')
args = ap.parse_args()
r = random.Random(args.seed)
t0 = time.time()
lifter = Lifter(0, 'c')
emu = Emu([(CODE, bytes(CODE_SIZE))])
cands = candidates(r)
if args.filter:
val, mask = (int(x, 16) for x in args.filter.split('/'))
cands = [w for w in cands if (w & mask) == val]
if args.no_validity:
val = validity(lifter, r.sample(cands, min(len(cands), args.encodings * 4)), emu)
else:
val = validity(lifter, cands, emu)
agree = Counter()
only = defaultdict(list)
for w, (lok, uok) in val.items():
agree[(lok, uok)] += 1
if lok != uok:
only['lifter' if lok else 'unicorn'].append(w)
print(f'validity: {len(val)} encodings, both valid {agree[(True, True)]}, both invalid {agree[(False, False)]}, '
f'lifter-only {agree[(True, False)]}, unicorn-only {agree[(False, True)]} ({time.time() - t0:.0f}s)')
for kind in ('lifter', 'unicorn'):
ws = only[kind]
if ws:
print(f' {kind}-only by mnemonic: {dict(Counter(mnemonic(w) for w in ws).most_common(30))}')
for w in ws[:args.show // 2]:
print(f' {w:08x} {dis(w)}')
# Stratified sample of valid encodings: one per opcode signature first.
valid = [w for w, (lok, uok) in val.items() if lok and uok]
r.shuffle(valid)
seen, pick = set(), []
for w in valid:
k = class_key(w)
if k not in seen:
seen.add(k)
pick.append(w)
picked = set(pick)
pick = (pick + [w for w in valid if w not in picked])[:args.encodings]
# Placed at CODE + 4i; literal loads get an offset into the code region.
for i, w in enumerate(pick):
pc = CODE + 4 * i
if is_literal(w):
tgt = CODE + r.randrange(0, (len(pick) * 4 + 0x1000) & ~3, 4)
pick[i] = (w & ~(0x7FFFF << 5)) | ((((tgt - pc) >> 2) & 0x7FFFF) << 5)
code_bytes = struct.pack(f'<{len(pick)}I', *pick) + bytes(0x1000 + 64)
bodies = [lifter.lift(w, CODE + 4 * i).code for i, w in enumerate(pick)]
work = ROOT / 'build' / 'test' / 'simd'
work.mkdir(parents=True, exist_ok=True)
t1 = time.time()
exe = build_harness(work, bodies, args.jobs, args.cflags)
print(f'compiled {len(bodies)} tests ({time.time() - t1:.0f}s)')
mu = emu.mu
mu.mem_write(CODE, code_bytes)
mu.ctl_remove_cache(CODE, CODE + CODE_SIZE)
buf0 = bytes(r.getrandbits(8) for _ in range(4096)) * (BUF_SIZE // 4096)
mu.mem_write(BUF, buf0)
code_path = work / 'code.bin'
code_path.write_bytes(code_bytes)
cases, expect = [], []
t2 = time.time()
for k, w in enumerate(pick):
pc = CODE + 4 * k
st = is_store(w)
for _ in range(args.cases):
fpcr, x, sp, nzcv, v = make_case(r, w)
try:
ox, osp, onz, opc, ov = emu.run(pc, fpcr, x, sp, nzcv, v)
except uc.UcError:
if st:
mu.mem_write(BUF, buf0)
continue
mh = 0
if st:
mh = zlib.crc32(bytes(mu.mem_read(BUF, BUF_SIZE)))
mu.mem_write(BUF, buf0)
cases.append((k, fpcr, x, sp, nzcv, v, st))
expect.append((ox, osp, onz, opc, ov, mh))
print(f'unicorn: {len(cases)} cases ({time.time() - t2:.0f}s)')
t3 = time.time()
recs, err = run_harness(exe, work, buf0, cases, [(CODE, code_path)])
print(f'harness: {len(recs)} of {len(cases)} cases ({time.time() - t3:.0f}s)')
if err:
k = cases[len(recs)][0] if len(recs) < len(cases) else None
print(f'harness failed: {err}' + (f', stopped at {pick[k]:08x} {dis(pick[k])}' if k is not None else ''))
fails, nfail = {}, 0
for case, want, got in zip(cases, expect, recs):
k, v, st = case[0], case[5], case[6]
d = diff_state(got, want, check_pc=False, check_mem=st)
if d:
nfail += 1
w = pick[k]
if w not in fails:
ins = [f'v{j}={v[j]:032x}' for j in sorted({(w >> 5) & 31, (w >> 16) & 31, w & 31})]
fails[w] = (case[1], d[:3], ins)
print(f'{len(pick)} encodings, {len(cases)} cases: {nfail} failing cases, {len(fails)} failing encodings '
f'({time.time() - t0:.0f}s total)')
if fails:
print(' failing by mnemonic:', dict(Counter(mnemonic(w) for w in fails).most_common(40)))
for w, (fpcr, d, ins) in list(fails.items())[:args.show]:
print(f' {w:08x} {dis(w):40} fpcr={fpcr:x} {"; ".join(d)}')
print(f' {" ".join(ins)}')
return 1 if fails or err else 0
if __name__ == '__main__':
sys.exit(main())