mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
Game: start on CPUs without POPCNT again (Core 2), and check every build for it
Since #4160 (10-29) the game's allocator is mimalloc v3, from libmimalloc-sys 0.1.49. Built by MSVC, its mi_popcount is `__popcnt64`, which MSVC turns into the POPCNT instruction whatever the target. mimalloc runs before main, so on a Core 2 (no POPCNT) the Windows game died at once with 0xc000001d and no log (#7264). v0.2.97 had no POPCNT at all; nightlies 2026-10-31 and 11-01 have 9. third_party/libmimalloc-sys is the published crate with one change: on MSVC x86/x64, mi_popcount takes mimalloc's portable count, as the GCC builds without -mpopcnt already do. Only arena bitmaps count bits, never malloc's fast path. [patch.crates-io] points at it until a release carries microsoft/mimalloc#1291. tools/check_cpu_baseline.py disassembles the Windows and Linux programs in the game build and fails on any POPCNT or LZCNT: it flags 2026-11-01's horizon-client.exe and passes v0.2.97's and the Linux builds. Fixes #7264 Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
e478b7e210
commit
3380705af9
8 files changed
+162
-9
No files matched your search
@@ -185,6 +185,15 @@ jobs:
|
||||
${{ runner.os == 'Windows' && 'python' || 'python3' }} tools/check_imports.py "$file"
|
||||
fi
|
||||
done
|
||||
- name: Baseline x86-64 CPUs only
|
||||
# No POPCNT or LZCNT at all (the script says why only those): a C library's count intrinsic killed the
|
||||
# game before main on a Core 2 (#7264). Not the Mac builds: every Intel Mac that runs macOS 11 has both.
|
||||
if: runner.os != 'macOS'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
rustup component add llvm-tools
|
||||
${{ runner.os == 'Windows' && 'python' || 'python3' }} tools/check_cpu_baseline.py \
|
||||
$(for binary in horizon-client horizon-ctl horizon-prepare horizon-server; do echo "$BIN/$binary${{ matrix.exe }}"; done)
|
||||
- name: Every program starts
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
Generated
-2
@@ -2114,8 +2114,6 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
|
||||
[[package]]
|
||||
name = "libmimalloc-sys"
|
||||
version = "0.1.49"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a45a52f43e1c16f667ccfe4dd8c85b7f7c204fd5e3bf46c5b0db9a5c3c0b8e9"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cty",
|
||||
|
||||
+5
-1
@@ -1,5 +1,5 @@
|
||||
[workspace]
|
||||
exclude = ["launcher", "crates/horizon-telemetry-server", "crates/horizon-mod-registry", "third_party/wgpu-hal", "third_party/naga"]
|
||||
exclude = ["launcher", "crates/horizon-telemetry-server", "crates/horizon-mod-registry", "third_party/wgpu-hal", "third_party/naga", "third_party/libmimalloc-sys"]
|
||||
members = [
|
||||
"crates/horizon-core",
|
||||
"crates/horizon-dualsense",
|
||||
@@ -67,6 +67,10 @@ large_enum_variant = "allow"
|
||||
wgpu-hal = { path = "third_party/wgpu-hal" }
|
||||
# naga 30.0.1, with the fixes to its GLSL frontend that shader packs need (THIRD_PARTY_NOTICES.md).
|
||||
naga = { path = "third_party/naga" }
|
||||
# libmimalloc-sys 0.1.49 whose mimalloc v3 counts bits without POPCNT when MSVC builds it: the
|
||||
# game died before main on CPUs without it (Core 2, #7264; THIRD_PARTY_NOTICES.md). Drop this and
|
||||
# the folder once a libmimalloc-sys release has microsoft/mimalloc#1291's fix.
|
||||
libmimalloc-sys = { path = "third_party/libmimalloc-sys" }
|
||||
|
||||
# Line tables keep file:line in backtraces; full debug info made every
|
||||
# example and test binary hundreds of megabytes and slow to link.
|
||||
|
||||
@@ -14,6 +14,15 @@ Apache-2.0; both licenses are in that folder (`LICENSE.MIT`, `LICENSE.APACHE`).
|
||||
fixes to its GLSL frontend that shader packs need, each applied in the commit that needed it. It is licensed under MIT or Apache-2.0; both licenses
|
||||
are in that folder (`LICENSE.MIT`, `LICENSE.APACHE`).
|
||||
|
||||
## libmimalloc-sys and mimalloc
|
||||
|
||||
`third_party/libmimalloc-sys` is [libmimalloc-sys](https://github.com/purpleprotocol/mimalloc_rust)
|
||||
0.1.49 by Octavian Oncescu, with the [mimalloc](https://github.com/microsoft/mimalloc) v3 sources it
|
||||
bundles (Copyright (c) 2018-2025 Microsoft Corporation, Daan Leijen), both under the MIT license
|
||||
(`LICENSE.txt` and `c_src/mimalloc/v3/LICENSE` in that folder). It holds only the v3 sources. One
|
||||
change: `mi_popcount` in `include/mimalloc/bits.h` uses mimalloc's portable bit count on MSVC
|
||||
x86/x64 instead of the POPCNT instruction, which CPUs before Nehalem lack (#7264).
|
||||
|
||||
## Pixelify Sans
|
||||
|
||||
`crates/horizon-theme/fonts/PixelifySans-*.ttf` is [Pixelify Sans](https://github.com/eifetx/Pixelify-Sans)
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import unittest
|
||||
|
||||
from tools import check_cpu_baseline as baseline
|
||||
|
||||
# llvm-objdump -d --no-show-raw-insn output: a stripped Windows program (only section labels) and an ELF one.
|
||||
DISASSEMBLY = """
|
||||
/tmp/horizon-client.exe:\tfile format coff-x86-64
|
||||
|
||||
Disassembly of section .text:
|
||||
|
||||
0000000140001000 <.text>:
|
||||
140001000: \tpopcntq\t%rdx, %rax
|
||||
140001005: \ttzcntl\t%ecx, %eax
|
||||
14000100a: \tvpaddd\t%ymm0, %ymm1, %ymm2
|
||||
0000000140002000 <mi_bitmap_popcount>:
|
||||
140002000: \tlzcntq\t%r8, %r9
|
||||
140002005: \tmovq\t%rax, %rbx
|
||||
140002008: \tpopcnt\trax, rcx
|
||||
"""
|
||||
|
||||
|
||||
class ForbiddenUsesTests(unittest.TestCase):
|
||||
def test_reports_popcnt_and_lzcnt_with_their_symbol_and_nothing_else(self):
|
||||
uses = list(baseline.forbidden_uses(DISASSEMBLY.splitlines(keepends=True)))
|
||||
self.assertEqual(uses, [
|
||||
("140001000", ".text", "popcnt"),
|
||||
("140002000", "mi_bitmap_popcount", "lzcnt"),
|
||||
("140002008", "mi_bitmap_popcount", "popcnt"),
|
||||
])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Vendored
+6
-5
@@ -5,11 +5,12 @@ use std::path::{Path, PathBuf};
|
||||
fn main() {
|
||||
let mut build = cc::Build::new();
|
||||
|
||||
let version = if env::var("CARGO_FEATURE_V2").is_ok() {
|
||||
"v2"
|
||||
} else {
|
||||
"v3"
|
||||
};
|
||||
// Horizon vendors only mimalloc v3 (THIRD_PARTY_NOTICES.md).
|
||||
assert!(
|
||||
env::var("CARGO_FEATURE_V2").is_err(),
|
||||
"third_party/libmimalloc-sys holds only mimalloc v3: the v2 feature is not available"
|
||||
);
|
||||
let version = "v3";
|
||||
|
||||
let cargo_manifest_dir = env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR not set");
|
||||
let include_root = Path::new(&cargo_manifest_dir)
|
||||
|
||||
@@ -181,7 +181,12 @@ size_t _mi_popcount_generic(size_t x);
|
||||
static inline size_t mi_popcount(size_t x) {
|
||||
#if mi_has_builtinz(popcount)
|
||||
return mi_builtinz(popcount)(x);
|
||||
#elif defined(_MSC_VER) && (MI_ARCH_X64 || MI_ARCH_X86 || MI_ARCH_ARM64 || MI_ARCH_ARM32)
|
||||
// Horizon (#7264): MSVC's __popcnt is the POPCNT instruction whatever the target, which x64
|
||||
// CPUs before Nehalem (Core 2) and AMD K8 lack: the game died before main on them. x86/x64
|
||||
// take the portable count below, as GCC builds without -mpopcnt do, so the release programs
|
||||
// hold no POPCNT at all (tools/check_cpu_baseline.py). Upstream checks the CPUID bit at each
|
||||
// call instead (microsoft/mimalloc#1291). Only arena bitmaps count bits, never malloc's fast path.
|
||||
#elif defined(_MSC_VER) && (MI_ARCH_ARM64 || MI_ARCH_ARM32)
|
||||
return mi_msc_builtinz(__popcnt)(x);
|
||||
#elif MI_ARCH_X64 && defined(__BMI1__)
|
||||
return (size_t)_mm_popcnt_u64(x);
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Fail when an x86_64 release binary uses an instruction that baseline x86-64 CPUs lack.
|
||||
|
||||
The game must start on players' oldest PCs: x86-64 with SSE2 only (Core 2, AMD K8). A
|
||||
single unguarded POPCNT killed it with 0xc000001d before `main` on a Core 2 (#7264):
|
||||
mimalloc's C code, built by MSVC, whose `__popcnt` intrinsic is the instruction whatever
|
||||
the target.
|
||||
|
||||
Only POPCNT and LZCNT are checked. They are what C compilers emit for count intrinsics, and
|
||||
no runtime-dispatched code in the game uses them today. AVX, BMI2, SSSE3 and SSE4 do appear,
|
||||
always behind a CPUID check (ring's assembly, memchr, half's F16C, compiler_builtins' FMA,
|
||||
the MSVC C runtime), and the Windows programs have no symbols to tell those functions from
|
||||
unguarded ones. `tzcnt` is not checked either: LLVM writes `rep bsf` for trailing_zeros,
|
||||
which disassembles as tzcnt and runs as bsf on every CPU.
|
||||
|
||||
Needs llvm-objdump: $LLVM_OBJDUMP, rustup's llvm-tools component, or one on PATH.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import glob
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
FORBIDDEN = ("popcnt", "lzcnt")
|
||||
LABEL = re.compile(r"^([0-9a-f]+) <(.*)>:$")
|
||||
INSTRUCTION = re.compile(r"^\s*([0-9a-f]+):\s+(\S+)")
|
||||
|
||||
|
||||
def find_objdump():
|
||||
if os.environ.get("LLVM_OBJDUMP"):
|
||||
return os.environ["LLVM_OBJDUMP"]
|
||||
try:
|
||||
sysroot = subprocess.run(["rustc", "--print", "sysroot"], capture_output=True, text=True,
|
||||
check=True).stdout.strip()
|
||||
found = glob.glob(os.path.join(sysroot, "lib", "rustlib", "*", "bin", "llvm-objdump*"))
|
||||
if found:
|
||||
return found[0]
|
||||
except (OSError, subprocess.CalledProcessError):
|
||||
pass
|
||||
return shutil.which("llvm-objdump")
|
||||
|
||||
|
||||
def forbidden_uses(lines):
|
||||
"""(address, enclosing symbol, mnemonic) of each forbidden instruction in llvm-objdump -d output."""
|
||||
symbol = "?"
|
||||
for line in lines:
|
||||
line = line.rstrip("\n")
|
||||
label = LABEL.match(line)
|
||||
if label:
|
||||
symbol = label.group(2)
|
||||
continue
|
||||
insn = INSTRUCTION.match(line)
|
||||
if not insn:
|
||||
continue
|
||||
mnemonic = insn.group(2)
|
||||
# AT&T syntax adds the operand size: popcntq, lzcntl.
|
||||
base = mnemonic[:-1] if mnemonic[-1:] in "wlq" and mnemonic[:-1] in FORBIDDEN else mnemonic
|
||||
if base in FORBIDDEN:
|
||||
yield insn.group(1), symbol, base
|
||||
|
||||
|
||||
def check(objdump, path):
|
||||
proc = subprocess.Popen([objdump, "-d", "--no-show-raw-insn", path], stdout=subprocess.PIPE,
|
||||
text=True, errors="replace")
|
||||
uses = list(forbidden_uses(proc.stdout))
|
||||
if proc.wait() != 0:
|
||||
sys.exit(f"llvm-objdump failed on {path}")
|
||||
for address, symbol, mnemonic in uses[:20]:
|
||||
print(f"{path}: {mnemonic} at 0x{address} in {symbol}")
|
||||
if len(uses) > 20:
|
||||
print(f"{path}: ... {len(uses) - 20} more")
|
||||
return not uses
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||
parser.add_argument("binaries", nargs="+")
|
||||
args = parser.parse_args()
|
||||
objdump = find_objdump()
|
||||
if not objdump:
|
||||
sys.exit("llvm-objdump not found: rustup component add llvm-tools, or set LLVM_OBJDUMP")
|
||||
failed = [path for path in args.binaries if not check(objdump, path)]
|
||||
if failed:
|
||||
print(f"::error::{', '.join(failed)}: {'/'.join(FORBIDDEN)} without a CPU check crash CPUs "
|
||||
"older than Nehalem (#7264); find the C library or target_feature that emits them")
|
||||
sys.exit(1)
|
||||
print(f"no {'/'.join(FORBIDDEN)} in {len(args.binaries)} binaries")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user