nx_hw_simd_caps.nx source
↩ module page · 201 lines · 7629 B
1// nx_hw_simd_caps.nx -- runtime SIMD capability detection.
2//
3// Reads /proc/cpuinfo and looks for vendor-specific flag tokens.
4// Sovereign: pure NishiLang, no libc, no /sys probe utilities, no
5// CPUID inline-asm dep. Works on every Linux arch the substrate
6// targets (x86_64, RV64, AArch64, etc.) -- /proc/cpuinfo is the
7// kernel's canonical CPU-feature reporting surface.
8//
9// HISTORICAL NOTE: this module was the smoke that surfaced the
10// gp-relative relaxation bug fixed 2026-05-16. When 6 statics
11// fall within a single 4 KiB BSS window, the assembler optimizes
12// auipc+addi global-address sequences into addi(gp)+addi, but
13// our minimal crt0 doesn't initialize gp, so reads/writes
14// dereferenced garbage. Fixed in riscv.c by always emitting
15// `.option norelax` -- the bug class is now structurally
16// impossible regardless of static count. See docs/NISHILANG_CONCURRENCY.md
17// "Pitfalls" section for the diagnosis trail.
18//
19// Why probe at runtime instead of compile-time:
20// - One binary can dispatch scalar / AVX2 / AVX-512 paths based
21// on the actual chip it runs on (vs N separate builds)
22// - Honest cardinal: refuse silent CPU-feature assumptions; ask
23// the kernel what it actually has
24//
25// Use:
26// if nx_hw_has_avx512() == 1 { use_avx512_path() }
27// else if nx_hw_has_avx2() == 1 { use_avx2_path() }
28// else { use_scalar_path() }
29//
30// Cost: ~4 KiB stack buffer + one sys_openat + one sys_read +
31// linear scan. Acceptable to call once at startup; cache in a
32// static for repeated checks.
33//
34// Caches: once probed, results are stored in static i64 slots so
35// subsequent calls are zero-syscall.
36
37// nx_safety_envelope:
38// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
39// sil_target: SIL1
40// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
41// verdict: NOT_YET_EVALUATED
42
43import "nx_syscalls.nx"
44
45const NX_CPUINFO_BUF: i64 = 8192
46
47static NX_HW_CAPS_INIT: i64
48static NX_HW_HAS_AVX2: i64
49static NX_HW_HAS_AVX512: i64
50static NX_HW_HAS_RVV: i64
51static NX_HW_HAS_NEON: i64
52static NX_HW_HAS_SSE4_2: i64
53
54// Open + read /proc/cpuinfo into a buffer. Returns bytes read or
55// negative errno.
56func _nx_hw_read_cpuinfo(buf: *u8, cap: i64) -> i64 {
57 // openat(AT_FDCWD=-100, path, O_RDONLY=0).
58 let path: *u8 = "/proc/cpuinfo" as *u8
59 let fd: i64 = __syscall(SYS_OPENAT, -100, path as i64, 0, 0, 0, 0)
60 if fd < 0 { return fd }
61 let n: i64 = sys_read(fd, buf, cap - 1)
62 sys_close(fd)
63 if n < 0 { return n }
64 buf[n] = 0 as u8
65 return n
66}
67
68// Scan buf for a token (whole word, surrounded by space / tab /
69// newline / start-of-buf). Returns 1 if found, 0 otherwise.
70func _nx_hw_has_token(buf: *u8, buf_len: i64, tok: *u8, tok_len: i64) -> i64 {
71 var i: i64 = 0
72 while i + tok_len <= buf_len {
73 // Check boundary on the left: i == 0 OR buf[i-1] is whitespace/newline.
74 var left_ok: i64 = 1
75 if i > 0 {
76 let lc: i64 = buf[i - 1]
77 if lc != 0x20 {
78 if lc != 0x09 {
79 if lc != 0x0A {
80 left_ok = 0
81 }
82 }
83 }
84 }
85 if left_ok == 1 {
86 var hit: i64 = 1
87 var j: i64 = 0
88 while j < tok_len {
89 if buf[i + j] != tok[j] { hit = 0; j = tok_len }
90 else { j = j + 1 }
91 }
92 if hit == 1 {
93 // Check boundary on the right.
94 let after: i64 = i + tok_len
95 if after >= buf_len { return 1 }
96 let rc: i64 = buf[after]
97 if rc == 0x20 { return 1 }
98 if rc == 0x09 { return 1 }
99 if rc == 0x0A { return 1 }
100 if rc == 0 { return 1 }
101 }
102 }
103 i = i + 1
104 }
105 return 0
106}
107
108// Populate token literal into a small buffer + return length.
109func _nx_hw_tok_lit(buf: *u8, s_byte_0: i64, s_byte_1: i64, s_byte_2: i64,
110 s_byte_3: i64, s_byte_4: i64, s_byte_5: i64,
111 s_byte_6: i64) -> i64 {
112 buf[0] = s_byte_0 as u8
113 if s_byte_1 == 0 { return 1 }
114 buf[1] = s_byte_1 as u8
115 if s_byte_2 == 0 { return 2 }
116 buf[2] = s_byte_2 as u8
117 if s_byte_3 == 0 { return 3 }
118 buf[3] = s_byte_3 as u8
119 if s_byte_4 == 0 { return 4 }
120 buf[4] = s_byte_4 as u8
121 if s_byte_5 == 0 { return 5 }
122 buf[5] = s_byte_5 as u8
123 if s_byte_6 == 0 { return 6 }
124 buf[6] = s_byte_6 as u8
125 return 7
126}
127
128// Lazy-init the capability cache by reading /proc/cpuinfo once and
129// scanning for the relevant flag tokens.
130func _nx_hw_caps_init() -> i64 {
131 if NX_HW_CAPS_INIT == 1 { return 0 }
132 let buf: *u8 = sys_mmap(NX_CPUINFO_BUF)
133 let n: i64 = _nx_hw_read_cpuinfo(buf, NX_CPUINFO_BUF)
134 if n <= 0 {
135 NX_HW_CAPS_INIT = 1
136 return -1
137 }
138 // Scan for each token. Reuse a small scratch buffer for tokens.
139 let tok: *u8 = sys_mmap(16)
140
141 let len_avx2: i64 = _nx_hw_tok_lit(tok, 0x61, 0x76, 0x78, 0x32, 0, 0, 0) // "avx2"
142 NX_HW_HAS_AVX2 = _nx_hw_has_token(buf, n, tok, len_avx2)
143
144 let len_avx512: i64 = _nx_hw_tok_lit(tok, 0x61, 0x76, 0x78, 0x35, 0x31, 0x32, 0x66) // "avx512f"
145 NX_HW_HAS_AVX512 = _nx_hw_has_token(buf, n, tok, len_avx512)
146
147 let len_sse: i64 = _nx_hw_tok_lit(tok, 0x73, 0x73, 0x65, 0x34, 0x5F, 0x32, 0) // "sse4_2"
148 NX_HW_HAS_SSE4_2 = _nx_hw_has_token(buf, n, tok, len_sse)
149
150 let len_rvv: i64 = _nx_hw_tok_lit(tok, 0x72, 0x76, 0x76, 0, 0, 0, 0) // "rvv"
151 NX_HW_HAS_RVV = _nx_hw_has_token(buf, n, tok, len_rvv)
152
153 let len_neon: i64 = _nx_hw_tok_lit(tok, 0x6E, 0x65, 0x6F, 0x6E, 0, 0, 0) // "neon"
154 NX_HW_HAS_NEON = _nx_hw_has_token(buf, n, tok, len_neon)
155
156 NX_HW_CAPS_INIT = 1
157 return 0
158}
159
160// Public probes. All are zero-cost after first call (cached).
161func nx_hw_has_avx2() -> i64 { _nx_hw_caps_init(); return NX_HW_HAS_AVX2 }
162func nx_hw_has_avx512() -> i64 { _nx_hw_caps_init(); return NX_HW_HAS_AVX512 }
163func nx_hw_has_sse4_2() -> i64 { _nx_hw_caps_init(); return NX_HW_HAS_SSE4_2 }
164func nx_hw_has_rvv() -> i64 { _nx_hw_caps_init(); return NX_HW_HAS_RVV }
165func nx_hw_has_neon() -> i64 { _nx_hw_caps_init(); return NX_HW_HAS_NEON }
166
167// Convenience: best available i64x4 SIMD level on this chip.
168// 0 = scalar only; 1 = SIMD i64x4 available (any of AVX2 / RVV /
169// NEON would work, though our codegen targets specific ones).
170func nx_hw_simd_i64x4_available() -> i64 {
171 _nx_hw_caps_init()
172 if NX_HW_HAS_AVX2 == 1 { return 1 }
173 if NX_HW_HAS_AVX512 == 1 { return 1 }
174 if NX_HW_HAS_RVV == 1 { return 1 }
175 if NX_HW_HAS_NEON == 1 { return 1 }
176 return 0
177}
178
179// ---- self-test ---------------------------------------------------
180//
181// Self-test main intentionally does only function calls, never
182// reads statics from main scope. The pre-existing static-i64-from-
183// main codegen bug (documented in NISHILANG_CONCURRENCY.md pitfalls)
184// makes that pattern segv -- route through non-main helpers.
185
186// Helper-routed self-check (caller may invoke any probe directly
187// now that the gp-relative bug is fixed via .option norelax in
188// riscv.c -- this helper is kept for legibility).
189func _caps_smoke_run() -> i64 {
190 let a2: i64 = nx_hw_has_avx2()
191 let a512: i64 = nx_hw_has_avx512()
192 let rvv: i64 = nx_hw_has_rvv()
193 let nn: i64 = nx_hw_has_neon()
194 let any: i64 = nx_hw_simd_i64x4_available()
195 return a2 + a512 + rvv + nn + any
196}
197
198func main() -> i64 {
199 if _caps_smoke_run() < 0 { return __syscall(93, 1, 0, 0, 0, 0, 0) }
200 return 0
201}