mirror of
https://github.com/valentderah/totk-pc-recomp.git
synced 2026-11-11 18:01:03 +00:00
-march=x86-64-v3 kept the port and the installer to CPUs with AVX2 and FMA. --cpu v2 compiles the generated code and the runtime for SSE4.2 instead. Without FMA3 the FP lowering calls fma/fmaf, so runtime/fma.c provides correctly rounded ones (musl, MIT) that honour MXCSR's rounding mode, FTZ and DAZ; the UCRT's software fallback rounds incorrectly by up to half an ulp. simd.h's fast paths keep their assumptions. The installer's host check asks for x86-64-v2 and selects libtotkrt.a or libtotkrt-v2.a; both release scripts build and ship the pair. The differential tests compile fma.c as well, so a --cflags=-march=x86-64-v2 run tests it. Tested on an i7-3770K (no AVX2, FMA or BMI) with an RTX 3060 Ti, game 1.4.3: the installer ran end to end, --selftest passed and play holds 40-60 fps at 1080p. fma and fmaf matched 152,288 exact vectors over two seeds, including ties, subnormals and every rounding mode, FTZ and DAZ combination. The v3 build is unchanged: fma.c is an empty translation unit there.
277 lines
12 KiB
Python
277 lines
12 KiB
Python
"""Shared parts of the differential tests: Unicorn reference, operand generators, build and run of harness.c.
|
|
|
|
Adapted from LAS Recomp tools/test_lift.py and tools/test_simd.py @ da35b83 (MIT, Copyright (c) 2026 Peiikai).
|
|
"""
|
|
import os
|
|
import shutil
|
|
import struct
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import capstone
|
|
import unicorn as uc
|
|
from unicorn import arm64_const as A
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
sys.path.insert(0, str(ROOT / 'recomp'))
|
|
|
|
BUF, BUF_SIZE = 0x7200000000, 0x40000
|
|
UC_INVALID = (uc.UC_ERR_INSN_INVALID, uc.UC_ERR_EXCEPTION)
|
|
|
|
# Record layouts of struct In / struct Out in harness.c.
|
|
IN_FMT = '<QQ32QQQ'
|
|
IN_TAIL = '<QQ'
|
|
OUT_SIZE = 32 * 8 + 4 * 8 + 32 * 16
|
|
|
|
MSYS_BIN = Path(r'C:\msys64\ucrt64\bin')
|
|
|
|
F32 = [0, 0x80000000, 0x7f800000, 0xff800000, 0x7fc00000, 0xffc00000, 0x7fa00001, 0xff812345, 0x7fd00abc,
|
|
0x00000001, 0x80000001, 0x007fffff, 0x807fffff, 0x00400000, 0x00800000, 0x80800000, 0x00800001, 0x7f7fffff,
|
|
0xff7fffff, 0x3f800000, 0xbf800000, 0x3f000000, 0xbf000000, 0x40000000, 0x40400000, 0x3fc00000, 0xbfc00000,
|
|
0x40200000, 0x40100000, 0x4f000000, 0xcf000000, 0x4f800000, 0x5f000000, 0xdf000000, 0x5f800000, 0x4effffff,
|
|
0x4f7fffff, 0x5effffff, 0x5f7fffff, 0x3effffff, 0x3f7fffff, 0x7effffff, 0x01000000, 0x3e800000, 0x7e800000,
|
|
0x7e7fffff, 0x00200000, 0x001fffff, 0x00200001, 0x47800000, 0x477fe000, 0x477ff000, 0x38800000, 0x33800000,
|
|
0x33000000, 0x387fc000, 0x3f7ff000, 0x46fffe00, 0x80000000 | 0x00200000, 0x7f000000, 0x7f7ffffe]
|
|
F64 = [0, 1 << 63, 0x7ff0000000000000, 0xfff0000000000000, 0x7ff8000000000000, 0xfff8000000000000,
|
|
0x7ff4000000000001, 0xfff0000000012345, 0x7ffc00000abcdef0, 1, (1 << 63) | 1, 0x000fffffffffffff,
|
|
0x0008000000000000, 0x0010000000000000, 0x8010000000000000, 0x7fefffffffffffff, 0xffefffffffffffff,
|
|
0x3ff0000000000000, 0xbff0000000000000, 0x3fe0000000000000, 0x4000000000000000, 0x4008000000000000,
|
|
0x3ff8000000000000, 0xbff8000000000000, 0x4004000000000000, 0x41e0000000000000, 0xc1e0000000000000,
|
|
0x41f0000000000000, 0x43e0000000000000, 0xc3e0000000000000, 0x43f0000000000000, 0x41dfffffffc00000,
|
|
0x41efffffffe00000, 0x43dfffffffffffff, 0x43efffffffffffff, 0x3fdfffffffffffff, 0x7fdfffffffffffff,
|
|
0x7fd0000000000000, 0x0004000000000000, 0x0003ffffffffffff, 0x47efffffe0000000, 0x47efffff00000000,
|
|
0x36a0000000000000, 0x3810000000000000, 0x380fffffffffffff, 0x47f0000000000000, 0x3e70000000000000,
|
|
0x3f0ffc0000000000, 0x40effc0000000000, 0x40effe0000000000, 0x3e6ffffffff00000, 0x369fffffffffffff,
|
|
0x3810000010000000, 0x381000000fffffff, 0x3e80000000000000]
|
|
F16 = [0, 0x8000, 0x7c00, 0xfc00, 0x7e00, 0xfe00, 0x7d00, 0x7e55, 0x0001, 0x8001, 0x03ff, 0x0400, 0x7bff, 0xfbff,
|
|
0x3c00, 0xbc00, 0x3800, 0x4000, 0x3e00, 0x7fff, 0xffff]
|
|
|
|
_md = capstone.Cs(capstone.CS_ARCH_ARM64, capstone.CS_MODE_ARM)
|
|
|
|
|
|
def dis(w, pc=0):
|
|
d = next(_md.disasm(struct.pack('<I', w), pc), None)
|
|
return f'{d.mnemonic} {d.op_str}' if d else '?'
|
|
|
|
|
|
def mnemonic(w):
|
|
return dis(w).split(' ')[0]
|
|
|
|
|
|
def rand_lane(r, sz):
|
|
"""Random interesting integer lane of 8 << sz bits."""
|
|
bits_ = 8 << sz
|
|
k = r.random()
|
|
if k < 0.35:
|
|
return r.choice([0, 1, 2, 3, (1 << bits_) - 1, (1 << bits_) - 2, 1 << (bits_ - 1), (1 << (bits_ - 1)) - 1,
|
|
(1 << (bits_ - 1)) + 1, 0x80 % (1 << bits_), 0x7f, bits_ - 1, bits_, bits_ + 1,
|
|
(1 << bits_) - bits_, 1 << (bits_ // 2)])
|
|
if k < 0.5:
|
|
return r.getrandbits(r.randint(1, bits_))
|
|
if k < 0.6:
|
|
return ((1 << bits_) - r.getrandbits(r.randint(1, bits_ - 1))) & ((1 << bits_) - 1)
|
|
return r.getrandbits(bits_)
|
|
|
|
|
|
def rand_f32(r):
|
|
k = r.random()
|
|
if k < 0.4:
|
|
return r.choice(F32)
|
|
if k < 0.8:
|
|
e = r.choice([r.randint(110, 145), r.randint(0, 255), r.randint(120, 135)])
|
|
return (r.getrandbits(1) << 31) | (e << 23) | (r.getrandbits(23) if r.random() < 0.7 else
|
|
r.getrandbits(4) << 19)
|
|
return struct.unpack('<I', struct.pack('<f', r.choice([r.uniform(-4, 4), float(r.randint(-300, 300)) + 0.5,
|
|
float(r.randint(-5, 5)), r.uniform(-1e6, 1e6)])))[0]
|
|
|
|
|
|
def rand_f64(r):
|
|
k = r.random()
|
|
if k < 0.4:
|
|
return r.choice(F64)
|
|
if k < 0.8:
|
|
e = r.choice([r.randint(1000, 1050), r.randint(0, 2047), r.randint(1020, 1030), r.randint(0, 40),
|
|
r.randint(2000, 2047), r.randint(860, 920)])
|
|
return (r.getrandbits(1) << 63) | (e << 52) | (r.getrandbits(52) if r.random() < 0.7 else
|
|
r.getrandbits(6) << 46)
|
|
return struct.unpack('<Q', struct.pack('<d', r.choice([r.uniform(-4, 4), float(r.randint(-300, 300)) + 0.5,
|
|
r.uniform(-1e12, 1e12)])))[0]
|
|
|
|
|
|
def rand_v(r):
|
|
k = r.random()
|
|
out = 0
|
|
if k < 0.3:
|
|
for i in range(4):
|
|
out |= rand_f32(r) << (32 * i)
|
|
elif k < 0.55:
|
|
for i in range(2):
|
|
out |= rand_f64(r) << (64 * i)
|
|
elif k < 0.6:
|
|
for i in range(8):
|
|
out |= (r.choice(F16) if r.random() < 0.5 else r.getrandbits(16)) << (16 * i)
|
|
elif k < 0.9:
|
|
sz = r.randrange(4)
|
|
for i in range(16 >> sz):
|
|
out |= rand_lane(r, sz) << ((8 << sz) * i)
|
|
else:
|
|
out = r.getrandbits(128)
|
|
if r.random() < 0.15: # upper half zero, like most 64-bit results
|
|
out &= (1 << 64) - 1
|
|
return out
|
|
|
|
|
|
def rand_x(r):
|
|
k = r.random()
|
|
if k < 0.3:
|
|
return r.choice([0, 1, 2, 0xFFFFFFFFFFFFFFFF, 0x7FFFFFFFFFFFFFFF, 0x8000000000000000, 0xFFFFFFFF,
|
|
0x7FFFFFFF, 0x80000000, 0x100000000, 0xFFFFFFFF80000000, 0x8000000000000001, 0xFFFFFF,
|
|
0x1000001, 0x20000000000001, 0xFFFFFFFFFF000001, 63, 64, 31, 32, 0xFF, 0xFFFF])
|
|
if k < 0.45:
|
|
return rand_f32(r) | (r.getrandbits(32) << 32 if r.random() < 0.3 else 0)
|
|
if k < 0.6:
|
|
return rand_f64(r)
|
|
if k < 0.75:
|
|
return r.getrandbits(r.randint(1, 64))
|
|
if k < 0.8:
|
|
return (-r.getrandbits(r.randint(1, 63))) & 0xFFFFFFFFFFFFFFFF
|
|
return r.getrandbits(64)
|
|
|
|
|
|
def rand_fpcr(r):
|
|
k = r.random()
|
|
if k < 0.55:
|
|
return 0
|
|
v = 0
|
|
if r.random() < 0.4:
|
|
v |= 1 << 25 # DN
|
|
if r.random() < 0.4:
|
|
v |= 1 << 24 # FZ
|
|
if r.random() < 0.4:
|
|
v |= r.randrange(4) << 22 # RMode
|
|
if r.random() < 0.15:
|
|
v |= 1 << 26 # AHP
|
|
return v
|
|
|
|
|
|
class Emu:
|
|
"""Unicorn as a Cortex-A57, the Switch's CPU, unless another model is given (LSE atomics need
|
|
UC_CPU_ARM64_MAX). Images are (base, bytes) pairs mapped at their guest address."""
|
|
|
|
def __init__(self, images, model=A.UC_CPU_ARM64_A57):
|
|
mu = uc.Uc(uc.UC_ARCH_ARM64, uc.UC_MODE_ARM)
|
|
mu.ctl_set_cpu_model(model)
|
|
mu.mem_map(BUF, BUF_SIZE)
|
|
for base, data in images:
|
|
mu.mem_map(base, (len(data) + 0xFFFF) & ~0xFFFF)
|
|
mu.mem_write(base, bytes(data))
|
|
mu.reg_write(A.UC_ARM64_REG_CPACR_EL1, 0x300000) # enable FP/SIMD
|
|
self.mu = mu
|
|
self.xregs = [A.UC_ARM64_REG_X0 + i for i in range(29)] + [A.UC_ARM64_REG_X29, A.UC_ARM64_REG_X30]
|
|
self.qregs = [A.UC_ARM64_REG_Q0 + i for i in range(32)]
|
|
self.out_regs = self.xregs + [A.UC_ARM64_REG_SP, A.UC_ARM64_REG_NZCV, A.UC_ARM64_REG_PC] + self.qregs
|
|
|
|
def run(self, pc, fpcr, x, sp, nzcv, v, count=1):
|
|
"""Run count instructions from pc. Returns (x[31], sp, nzcv, pc, v[32])."""
|
|
mu = self.mu
|
|
mu.reg_write_batch(list(zip(self.xregs, x[:31])) + list(zip(self.qregs, v)) +
|
|
[(A.UC_ARM64_REG_SP, sp), (A.UC_ARM64_REG_NZCV, nzcv), (A.UC_ARM64_REG_FPCR, fpcr)])
|
|
mu.emu_start(pc, pc + 4 * count + 4, count=count)
|
|
o = mu.reg_read_batch(self.out_regs)
|
|
return list(o[:31]), o[31], o[32] & 0xF0000000, o[33], list(o[34:])
|
|
|
|
|
|
def compiler():
|
|
cc = os.environ.get('CC')
|
|
if cc:
|
|
return cc
|
|
if os.name == 'nt' and (MSYS_BIN / 'clang.exe').exists():
|
|
return str(MSYS_BIN / 'clang.exe')
|
|
return shutil.which('clang') or 'clang'
|
|
|
|
|
|
def build_harness(work, bodies, jobs, cflags):
|
|
"""Compile one C function per test body plus harness.c into work/harness[.exe]."""
|
|
cc = compiler()
|
|
env = dict(os.environ)
|
|
if os.name == 'nt' and MSYS_BIN.exists():
|
|
env['PATH'] = str(MSYS_BIN) + os.pathsep + env.get('PATH', '')
|
|
flags = ['-O1', '-fno-strict-aliasing', '-Wno-unused-label', '-I', str(ROOT / 'runtime')] + cflags.split()
|
|
n = max(1, min(jobs, len(bodies) // 200 + 1))
|
|
per = (len(bodies) + n - 1) // n
|
|
srcs = []
|
|
for j in range(n):
|
|
path = work / f'tests_{j}.c'
|
|
with open(path, 'w') as f:
|
|
f.write('#include "cpu.h"\n')
|
|
for k in range(j * per, min(len(bodies), (j + 1) * per)):
|
|
f.write(f'void t{k}(Cpu *c) {{ {bodies[k]} }}\n')
|
|
srcs.append(path)
|
|
with open(work / 'table.c', 'w') as f:
|
|
f.write('#include "cpu.h"\n')
|
|
f.write(''.join(f'void t{k}(Cpu *c);\n' for k in range(len(bodies))))
|
|
f.write('GuestFn tests[] = {' + ','.join(f't{k}' for k in range(len(bodies))) + '};\n')
|
|
f.write(f'int ntests = {len(bodies)};\n')
|
|
# fma.c: a --cflags=-march=x86-64-v2 run must exercise the software fma the v2 build links, not the C runtime's.
|
|
srcs += [work / 'table.c', ROOT / 'tests' / 'diff' / 'harness.c', ROOT / 'runtime' / 'fma.c']
|
|
objs, procs = [], []
|
|
for s in srcs:
|
|
o = work / (s.stem + '.o')
|
|
objs.append(o)
|
|
procs.append(subprocess.Popen([cc] + flags + ['-c', str(s), '-o', str(o)], env=env))
|
|
if any(p.wait() for p in procs):
|
|
raise SystemExit('compile failed')
|
|
exe = work / ('harness.exe' if os.name == 'nt' else 'harness')
|
|
subprocess.check_call([cc, '-fuse-ld=lld', '-o', str(exe)] + [str(o) for o in objs], env=env)
|
|
return exe
|
|
|
|
|
|
def write_cases(path, cases, stubs=False):
|
|
"""cases: (k, fpcr, x[31], sp, nzcv, v[32], memchk). stubs: calls go to harness.c's call stub (rt_stub)."""
|
|
with open(path, 'wb') as f:
|
|
for k, fpcr, x, sp, nzcv, v, memchk in cases:
|
|
f.write(struct.pack(IN_FMT, k, fpcr, *(list(x[:31]) + [0]), sp, nzcv))
|
|
for q in v:
|
|
f.write(q.to_bytes(16, 'little'))
|
|
f.write(struct.pack(IN_TAIL, int(memchk), int(stubs)))
|
|
|
|
|
|
def run_harness(exe, work, buf0, cases, images, stubs=False):
|
|
"""Run the lifted tests. Returns (records, stderr tail or None). images: (base, path) pairs."""
|
|
with open(work / 'buf.bin', 'wb') as f:
|
|
f.write(buf0)
|
|
write_cases(work / 'cases.bin', cases, stubs)
|
|
out = work / 'out.bin'
|
|
args = [str(exe), str(work / 'buf.bin'), str(work / 'cases.bin'), str(out)]
|
|
for base, path in images:
|
|
args += [str(path), f'{base:x}']
|
|
p = subprocess.run(args, capture_output=True)
|
|
err = None if p.returncode == 0 else f'exit {p.returncode:#x}: {p.stderr.decode(errors="replace")[-2000:]}'
|
|
data = out.read_bytes() if out.exists() else b''
|
|
recs = []
|
|
for i in range(len(data) // OUT_SIZE):
|
|
rec = data[i * OUT_SIZE:(i + 1) * OUT_SIZE]
|
|
x = struct.unpack_from('<31Q', rec, 0)
|
|
sp, nzcv, pc, mem = struct.unpack_from('<4Q', rec, 256)
|
|
v = [int.from_bytes(rec[288 + 16 * j:304 + 16 * j], 'little') for j in range(32)]
|
|
recs.append((x, sp, nzcv, pc, mem, v))
|
|
return recs, err
|
|
|
|
|
|
def diff_state(got, want, check_pc, check_mem, check_nzcv=True):
|
|
"""got: harness record; want: (x, sp, nzcv, pc, v, mem). Returns a list of mismatches."""
|
|
hx, hsp, hnz, hpc, hmem, hv = got
|
|
ex, esp, enz, epc, ev, emem = want
|
|
d = [f'x{j}: got {hx[j]:x} want {ex[j]:x}' for j in range(31) if hx[j] != ex[j]]
|
|
if hsp != esp:
|
|
d.append(f'sp: got {hsp:x} want {esp:x}')
|
|
if check_nzcv and hnz != enz:
|
|
d.append(f'nzcv: got {hnz:x} want {enz:x}')
|
|
if check_pc and hpc != epc:
|
|
d.append(f'pc: got {hpc:x} want {epc:x}')
|
|
if check_mem and hmem != emem:
|
|
d.append('memory differs')
|
|
d += [f'v{j}: got {hv[j]:032x} want {ev[j]:032x}' for j in range(32) if hv[j] != ev[j]]
|
|
return d
|