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

99 lines
4.6 KiB
C

// Host check of the installer's first step, compiled with the bundled clang at install time: does the CPU run
// -march=x86-64-v3 code (or only x86-64-v2), and does a Vulkan 1.3 device exist. Prints one JSON object on stdout.
// The Vulkan loader is opened at run time, so a machine without one gets "loader": false instead of a failed start.
#include <cpuid.h>
#include <stdio.h>
#include <string.h>
#include <vulkan/vulkan.h>
#ifdef _WIN32
#include <windows.h>
#define open_lib() LoadLibraryA("vulkan-1.dll")
#define lib_sym(l, n) ((void *)GetProcAddress((l), (n)))
#else
#include <dlfcn.h>
#define open_lib() dlopen("libvulkan.so.1", RTLD_NOW)
#define lib_sym(l, n) dlsym((l), (n))
#endif
static void json_string(const char *s) {
putchar('"');
for (; *s; s++) {
if (*s == '"' || *s == '\\') printf("\\%c", *s);
else if ((unsigned char)*s < 0x20) printf("\\u%04x", *s);
else putchar(*s);
}
putchar('"');
}
// The x86-64-v2 level from CPUID: SSE3, SSSE3, SSE4.1, SSE4.2, POPCNT, CMPXCHG16B and LAHF/SAHF. Every CPU with
// SSE4.2 has the others, but the level is what the v2 build is compiled for, so check them all.
static int x86_64_v2(void) {
unsigned a, b, c, d;
if (!__get_cpuid(1, &a, &b, &c, &d)) return 0;
const unsigned sse3 = 1u << 0, ssse3 = 1u << 9, cx16 = 1u << 13, sse41 = 1u << 19, sse42 = 1u << 20,
popcnt = 1u << 23;
if ((c & (sse3 | ssse3 | cx16 | sse41 | sse42 | popcnt)) != (sse3 | ssse3 | cx16 | sse41 | sse42 | popcnt))
return 0;
if (!__get_cpuid(0x80000001, &a, &b, &c, &d)) return 0;
return (c & (1u << 0)) != 0; // LAHF/SAHF
}
// The x86-64-v3 level from CPUID: AVX, AVX2, BMI1, BMI2, F16C, FMA, LZCNT and MOVBE, with the OS saving the YMM state.
// Not __builtin_cpu_supports("x86-64-v3"): clang reads that from the C runtime's CPU model, and libgcc before GCC 12
// (the Linux release's sysroot has GCC 10) has no level bits, so it answers false on every CPU.
static int x86_64_v3(void) {
if (!x86_64_v2()) return 0;
unsigned a, b, c, d;
if (!__get_cpuid(1, &a, &b, &c, &d)) return 0;
const unsigned fma = 1u << 12, movbe = 1u << 22, osxsave = 1u << 27, avx = 1u << 28, f16c = 1u << 29;
if ((c & (fma | movbe | osxsave | avx | f16c)) != (fma | movbe | osxsave | avx | f16c)) return 0;
unsigned lo, hi;
__asm__("xgetbv" : "=a"(lo), "=d"(hi) : "c"(0));
if ((lo & 6) != 6) return 0; // XMM and YMM state enabled by the OS
if (!__get_cpuid_count(7, 0, &a, &b, &c, &d)) return 0;
const unsigned bmi1 = 1u << 3, avx2 = 1u << 5, bmi2 = 1u << 8;
if ((b & (bmi1 | avx2 | bmi2)) != (bmi1 | avx2 | bmi2)) return 0;
if (!__get_cpuid(0x80000001, &a, &b, &c, &d)) return 0;
return (c & (1u << 5)) != 0; // LZCNT (ABM)
}
int main(void) {
printf("{\"x86_64_v2\": %s, \"x86_64_v3\": %s, ", x86_64_v2() ? "true" : "false",
x86_64_v3() ? "true" : "false");
void *lib = open_lib();
PFN_vkGetInstanceProcAddr gipa = lib ? (PFN_vkGetInstanceProcAddr)lib_sym(lib, "vkGetInstanceProcAddr") : NULL;
if (!gipa) {
printf("\"loader\": false, \"devices\": []}\n");
return 0;
}
printf("\"loader\": true, ");
PFN_vkCreateInstance create = (PFN_vkCreateInstance)gipa(NULL, "vkCreateInstance");
VkApplicationInfo app = {.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO, .pApplicationName = "totk-install",
.apiVersion = VK_API_VERSION_1_3};
VkInstanceCreateInfo ci = {.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO, .pApplicationInfo = &app};
VkInstance inst = VK_NULL_HANDLE;
VkResult r = create ? create(&ci, NULL, &inst) : VK_ERROR_INITIALIZATION_FAILED;
if (r != VK_SUCCESS) {
printf("\"instance\": %d, \"devices\": []}\n", (int)r);
return 0;
}
PFN_vkEnumeratePhysicalDevices enumerate = (PFN_vkEnumeratePhysicalDevices)gipa(inst, "vkEnumeratePhysicalDevices");
PFN_vkGetPhysicalDeviceProperties props = (PFN_vkGetPhysicalDeviceProperties)gipa(inst, "vkGetPhysicalDeviceProperties");
PFN_vkDestroyInstance destroy = (PFN_vkDestroyInstance)gipa(inst, "vkDestroyInstance");
VkPhysicalDevice devs[16];
uint32_t n = 16;
if (enumerate(inst, &n, devs) < 0) n = 0;
printf("\"instance\": 0, \"devices\": [");
for (uint32_t i = 0; i < n; i++) {
VkPhysicalDeviceProperties p;
props(devs[i], &p);
printf("%s{\"name\": ", i ? ", " : "");
json_string(p.deviceName);
printf(", \"api\": [%u, %u, %u], \"type\": %d}", VK_API_VERSION_MAJOR(p.apiVersion),
VK_API_VERSION_MINOR(p.apiVersion), VK_API_VERSION_PATCH(p.apiVersion), (int)p.deviceType);
}
printf("]}\n");
destroy(inst, NULL);
return 0;
}