Files
CHEN Feng 34b4faee7c Run the game on x86-64-v2 CPUs without AVX2
-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.
2026-11-01 07:09:13 +08:00

247 lines
8.3 KiB
C

// Correctly rounded fma/fmaf for x86-64 builds without FMA3: the -march=x86-64-v2 build (docs/building.md).
//
// runtime/simd.h lowers AArch64 FMADD/FMSUB and the RECPS/RSQRTS helpers to __builtin_elementwise_fma, which clang
// turns into a call to fma/fmaf when the target has no FMA instruction. The C runtime is not a substitute: on CPUs
// without FMA3 the UCRT's software fallback is documented as rounding incorrectly by up to half an ulp, while the FP
// lowering needs the single rounding the ARM instruction gives. This file supplies it.
//
// Adapted from musl src/math/fma.c and src/math/fmaf.c (MIT, Copyright (c) 2005-2020 Rich Felker and contributors).
// The result is rounded in the MXCSR rounding mode the caller set (simd.h simd_rm_enter), and MXCSR FTZ and DAZ are
// honoured as the hardware FMA would, so runtime/simd.h's fast paths keep their assumptions: denormal inputs read as
// zero and denormal results as zero under FPCR.FZ, without a trip through the slow path.
//
// The definitions exist only where the compiler would emit the calls. A -march=x86-64-v3 build defines __FMA__ and
// emits vfmadd instead, so this file contributes nothing there.
#include <stdint.h>
#if defined(__x86_64__) && !defined(__FMA__)
#include <float.h>
#include <immintrin.h>
#define MXCSR_DAZ 0x0040u
#define MXCSR_FTZ 0x8000u
static double fma_flush_d(double x) {
union { double f; uint64_t i; } u = {x};
if (!(u.i & 0x7ff0000000000000ull) && (u.i & 0x000fffffffffffffull)) u.i &= 0x8000000000000000ull;
return u.f;
}
static float fma_flush_f(float x) {
union { float f; uint32_t i; } u = {x};
if (!(u.i & 0x7f800000u) && (u.i & 0x007fffffu)) u.i &= 0x80000000u;
return u.f;
}
// ---- float: the exact product fits in a double, so one extra term and a round-to-odd step decide the float
// rounding. Halfway and tiny results are the only ones where rounding the double first can change the float result.
float fmaf(float x, float y, float z) {
unsigned csr = _mm_getcsr();
if (csr & MXCSR_DAZ) x = fma_flush_f(x), y = fma_flush_f(y), z = fma_flush_f(z);
double xy = (double)x * y; // exact
union { double r; uint64_t i; } u = {xy + z};
int e = u.i >> 52 & 0x7ff;
int halfway = (u.i & 0x1fffffff) == 0x10000000;
int tiny = e <= 0x3ff - 126 && e >= 0x3ff - 149;
if (halfway || tiny) {
if (e != 0x7ff) {
// r+t is the exact sum in nearest rounding; otherwise t keeps its sign and whether it is zero, which
// is all the round-to-odd step needs. An odd sticky bit makes the final conversion round once, in
// whichever mode MXCSR asks for.
int s = u.i >> 63;
double t = s == (xy < z) ? xy - u.r + z : z - u.r + xy;
if (t) {
u.i -= s ^ (t < 0);
u.i |= 1;
}
}
}
float r = (float)u.r;
if (csr & MXCSR_FTZ) r = fma_flush_f(r);
return r;
}
// ---- double: the significands are multiplied and aligned as integers, then converted back with one rounding.
#define ASUINT64(x) ((union { double f; uint64_t i; }){x}).i
#define ZEROINFNAN (0x7ff - 0x3ff - 52 - 1)
struct fma_num { uint64_t m; int e; int sign; };
static struct fma_num fma_normalize(double x) {
uint64_t ix = ASUINT64(x);
int e = ix >> 52;
int sign = e & 0x800;
e &= 0x7ff;
if (!e) {
ix = ASUINT64(x * 0x1p63);
e = ix >> 52 & 0x7ff;
e = e ? e - 63 : 0x800;
}
ix &= (1ull << 52) - 1;
ix |= 1ull << 52;
ix <<= 1;
e -= 0x3ff + 52 + 1;
return (struct fma_num){ix, e, sign};
}
static void fma_mul(uint64_t *hi, uint64_t *lo, uint64_t x, uint64_t y) {
uint64_t xlo = (uint32_t)x, xhi = x >> 32;
uint64_t ylo = (uint32_t)y, yhi = y >> 32;
uint64_t t1 = xlo * ylo;
uint64_t t2 = xlo * yhi + xhi * ylo;
uint64_t t3 = xhi * yhi;
*lo = t1 + (t2 << 32);
*hi = t3 + (t2 >> 32) + (t1 > *lo);
}
// scalbn from musl src/math/scalbn.c (MIT): the fma's final scaling, kept here so the result does not depend on the
// C runtime's version. Its multiplications honour MXCSR like any other host FP operation.
static double fma_scalbn(double x, int n) {
union { double f; uint64_t i; } u;
double y = x;
if (n > 1023) {
y *= 0x1p1023;
n -= 1023;
if (n > 1023) {
y *= 0x1p1023;
n -= 1023;
if (n > 1023) n = 1023;
}
} else if (n < -1022) {
// Make sure the final n < -53, so the subnormal range rounds only once.
y *= 0x1p-1022 * 0x1p53;
n += 1022 - 53;
if (n < -1022) {
y *= 0x1p-1022 * 0x1p53;
n += 1022 - 53;
if (n < -1022) n = -1022;
}
}
u.i = (uint64_t)(0x3ff + n) << 52;
return y * u.f;
}
double fma(double x, double y, double z) {
#pragma STDC FENV_ACCESS ON
unsigned csr = _mm_getcsr();
if (csr & MXCSR_DAZ) x = fma_flush_d(x), y = fma_flush_d(y), z = fma_flush_d(z);
struct fma_num nx = fma_normalize(x), ny = fma_normalize(y), nz = fma_normalize(z);
if (nx.e >= ZEROINFNAN || ny.e >= ZEROINFNAN) return x * y + z;
if (nz.e >= ZEROINFNAN) {
if (nz.e > ZEROINFNAN) return x * y; // z == 0
return z;
}
uint64_t rhi, rlo, zhi, zlo;
fma_mul(&rhi, &rlo, nx.m, ny.m);
// Align the exponents: shift z left by kz and r right by kr with kz + kr == d.
int e = nx.e + ny.e;
int d = nz.e - e;
if (d > 0) {
if (d < 64) {
zlo = nz.m << d;
zhi = nz.m >> (64 - d);
} else {
zlo = 0;
zhi = nz.m;
e = nz.e - 64;
d -= 64;
if (d == 0) {
} else if (d < 64) {
rlo = rhi << (64 - d) | rlo >> d | !!(rlo << (64 - d));
rhi = rhi >> d;
} else {
rlo = 1;
rhi = 0;
}
}
} else {
zhi = 0;
d = -d;
if (d == 0) {
zlo = nz.m;
} else if (d < 64) {
zlo = nz.m >> d | !!(nz.m << (64 - d));
} else {
zlo = 1;
}
}
int sign = nx.sign ^ ny.sign;
int samesign = !(sign ^ nz.sign);
int nonzero = 1;
if (samesign) {
rlo += zlo;
rhi += zhi + (rlo < zlo);
} else {
uint64_t t = rlo;
rlo -= zlo;
rhi = rhi - zhi - (t < rlo);
if (rhi >> 63) {
rlo = -rlo;
rhi = -rhi - !!rlo;
sign = !sign;
}
nonzero = !!rhi;
}
// Keep the top 63 bits in rhi; the last bit is sticky.
if (nonzero) {
e += 64;
d = __builtin_clzll(rhi) - 1;
rhi = rhi << d | rlo >> (64 - d) | !!(rlo << d);
} else if (rlo) {
d = __builtin_clzll(rlo) - 1;
if (d < 0)
rhi = rlo >> 1 | (rlo & 1);
else
rhi = rlo << d;
} else {
return x * y + z; // exact +-0
}
e -= d;
int64_t i = rhi;
if (sign) i = -i;
double r = i; // |r| is in [0x1p62, 0x1p63]
if (e < -1022 - 62) {
// The result is subnormal before rounding.
if (e == -1022 - 63) {
double c = 0x1p63;
if (sign) c = -c;
if (r == c) {
// The smallest normal after rounding: whether it underflows depends on the architecture. A double
// to float conversion imitates the before-rounding rule the guest has.
float fltmin = 0x0.ffffff8p-63 * FLT_MIN * r;
r = DBL_MIN / FLT_MIN * fltmin;
return (csr & MXCSR_FTZ) ? fma_flush_d(r) : r;
}
// One bit is lost when scaled: add another top bit so the conversion rounds only once if inexact.
if (rhi << 53) {
i = rhi >> 1 | (rhi & 1) | 1ull << 62;
if (sign) i = -i;
r = i;
r = 2 * r - c;
// Raise underflow portably, so the compiler cannot optimize the expression away.
double tiny = DBL_MIN / FLT_MIN * r;
r += (double)(tiny * tiny) * (r - r);
}
} else {
// Only round once when scaled.
d = 10;
i = (rhi >> d | !!(rhi << (64 - d))) << d;
if (sign) i = -i;
r = i;
}
}
r = fma_scalbn(r, e);
if (csr & MXCSR_FTZ) r = fma_flush_d(r);
return r;
}
#endif