x86: SSE under MXCSR's rounding control is a per-program opt-in, tested once a run

SSE under MXCSR is now asked for by the program that needs it:
gex_ir::set_mxcsr_exact (the page's ?mxcsr=1, gex_set_mxcsr_exact;
GEX_MXCSR=1 natively), before anything is translated. Off, the lifter makes
no Expr::Mx and tier-1 and the function JIT import no `mx`: every tier's
output is what it was before SSE honoured MXCSR (tier-1's module and the
function tier's C byte for byte), and the keys are as they were. On, the
unit key names it (`:mxcsr`), as do tier-1's knobs and the page's
translation cache, so units with it and without it never mix.

It is the rounding control only: FTZ and DAZ are honoured in no tier, as
before, and the exact import is given the rounding control alone. A
program that runs with FTZ or DAZ set (much audio code) keeps the fast
path.

The function tier tests the rounding control once for a run of up to 32
operations in a block and emits the run twice: the operations' own C,
taken when it is to nearest, and their exact calls, out of line in
runtime/mx.h (one call an operation, however many lanes). A block that
writes MXCSR or makes a host call tests each operation. A test and an
inline slow path per operation took clang over 65 minutes and past 15 GB
on one function of 13,600 SSE operations; the runs take
it 41 s and 287 MB, against 13 s and 194 MB without MXCSR. Tier-1 and the
function JIT test each operation's rounding control and call the import.

Tests: gex-lift-x86 sse_is_under_mxcsr_only_when_asked (no Mx, no MXCSR
read without the flag; one per operation with it); gex-cgen
sse_operations_under_mxcsr_test_once_a_block (without it, no MXCSR in the
C; with it, one test a run, one out-of-line call an operation, at most
twice the C, and clang takes it); tests/x86-programs fp-modes under
GEX_MXCSR=1, its four rounding modes against the hardware on tier-1, the
function JIT, the function tier's C and the page.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_c8c04c00da9a4525ab21b0b9f146c598
This commit is contained in:
velzieandClaude Opus 5.5 committed 2026-10-25 14:03:18 -04:00
1 parent eda608505f
commit 0d1f3888fa
20 files changed
+350 -205

No files matched your search

+4 -3
View File
@@ -940,9 +940,10 @@ pub mod imports {
/// `ctx::X87_TMP + 12`.
pub const X87: &str = "x87";
/// An SSE operation under a non-default MXCSR (`gex_ir::Expr::Mx`'s
/// slow path: a rounding control other than to nearest, FTZ or DAZ),
/// computed exactly (`gex_f80::sse::op`). Pure, as [`X87`] is.
/// An SSE operation under a rounding control other than to nearest
/// (`gex_ir::Expr::Mx`'s slow path), computed exactly
/// (`gex_f80::sse::op`). Pure, as [`X87`] is. Its `mxcsr` holds only
/// the rounding control (`gex_ir::MX_SPECIAL`).
/// Signature `(ctx: i32, code: i32, a: i64, b: i64, mxcsr: i32) -> i64`:
/// `code` is `gex_f80::sse`'s, the operands and the result raw bits
/// (a single's or an `i32`'s zero-extended).
+24 -29
View File
@@ -1,42 +1,37 @@
/* The function tier's slow path of Expr::Mx (an SSE operation under a
* MXCSR with a rounding control other than to nearest, FTZ or DAZ): the
* gex.mx import, which computes it exactly (gex_f80::sse::op), as tier-1's
* `emit_mx` calls it -- once for a scalar, once per lane, lanes past `n`
* zero. The fast path is the operation's own C: kept when none of those
* bits is set, and under FTZ or DAZ alone when no float operand nor the
* result is a denormal or the smallest normal (the gex_mx_tiny tests), as
* neither flush then has anything to do.
* MXCSR rounding control other than to nearest): the gex.mx import, which
* computes it exactly (gex_f80::sse::op), as tier-1's `emit_mx` calls it --
* once for a scalar, once per lane, lanes past `n` zero.
*
* Out of line (GEX_MX_SLOW), so an operation's slow path is one call in
* the C, however many lanes. The emitter tests the rounding control once
* for a run of operations and computes the run either way: as the
* operations' own C or as these calls.
*
* `f`, a vector operation's shape: bits 0-3 the lanes `n`, bit 4 an 8-byte
* operand lane, bit 5 an 8-byte result lane.
*
* Included from gex_abi.h, after the imports. */
/* A denormal or the smallest normal (|x| - 1 below the smallest normal). */
GEX_INLINE int gex_mx_tiny32(uint32_t x) { return ((x & 0x7fffffffu) - 1) >> 23 == 0; }
GEX_INLINE int gex_mx_tiny64(uint64_t x) { return ((x & 0x7fffffffffffffffull) - 1) >> 52 == 0; }
/* Any lane of `v`, lanes `w` bytes wide. */
GEX_INLINE int gex_mx_tiny_v(v128_t v, uint32_t w) {
if (w == 4) {
v128_t t = wasm_u32x4_shr(wasm_i32x4_sub(wasm_v128_and(v, wasm_i32x4_splat(0x7fffffff)), wasm_i32x4_splat(1)), 23);
return wasm_v128_any_true(wasm_i32x4_eq(t, wasm_i32x4_splat(0)));
}
v128_t t = wasm_u64x2_shr(wasm_i64x2_sub(wasm_v128_and(v, wasm_i64x2_splat(0x7fffffffffffffffll)), wasm_i64x2_splat(1)), 52);
return wasm_v128_any_true(wasm_i64x2_eq(t, wasm_i64x2_splat(0)));
}
#define GEX_MX_SLOW static __attribute__((noinline, unused))
GEX_INLINE uint64_t gex_mx_scalar(int ctx, int32_t code, uint64_t a, uint64_t b, uint32_t mx) {
GEX_MX_SLOW uint64_t gex_mx_s(int ctx, int32_t code, uint64_t a, uint64_t b, uint32_t mx) {
return (uint64_t)gex_mx(ctx, code, (int64_t)a, (int64_t)b, (int32_t)mx);
}
/* Lane `k` (a constant) of `v`, `w` bytes wide, as the import's operand. */
#define GEX_MX_IN(v, k, w) \
((w) == 4 ? (uint64_t)(uint32_t)wasm_i32x4_extract_lane(v, k) : (uint64_t)wasm_i64x2_extract_lane(v, (k) & 1))
/* Lane `k` (a constant) of `v`, 8 bytes wide when `w8`, as the import's operand. */
#define GEX_MX_IN(v, k, w8) \
(!(w8) ? (uint64_t)(uint32_t)wasm_i32x4_extract_lane(v, k) : (uint64_t)wasm_i64x2_extract_lane(v, (k) & 1))
GEX_INLINE v128_t gex_mx_lanes(int ctx, int32_t code, v128_t a, v128_t b, uint32_t n, uint32_t in_bytes,
uint32_t out_bytes, uint32_t mx) {
/* An absent `b` is all zero. */
GEX_MX_SLOW v128_t gex_mx_v(int ctx, int32_t code, v128_t a, v128_t b, uint32_t f, uint32_t mx) {
int in8 = (f & 16u) != 0, out8 = (f & 32u) != 0;
uint32_t n = f & 15u;
v128_t r = wasm_i64x2_const(0, 0);
#define GEX_MX_LANE(k) \
if ((k) < n) { \
uint64_t x = gex_mx_scalar(ctx, code, GEX_MX_IN(a, k, in_bytes), GEX_MX_IN(b, k, in_bytes), mx); \
r = out_bytes == 4 ? wasm_i32x4_replace_lane(r, k, (int32_t)x) : wasm_i64x2_replace_lane(r, (k) & 1, (int64_t)x); \
#define GEX_MX_LANE(k) \
if ((k) < n) { \
uint64_t x = (uint64_t)gex_mx(ctx, code, (int64_t)GEX_MX_IN(a, k, in8), (int64_t)GEX_MX_IN(b, k, in8), (int32_t)mx); \
r = !out8 ? wasm_i32x4_replace_lane(r, k, (int32_t)x) : wasm_i64x2_replace_lane(r, (k) & 1, (int64_t)x); \
}
GEX_MX_LANE(0)
GEX_MX_LANE(1)
+109 -41
View File
@@ -257,6 +257,26 @@ pub(crate) struct Emitter<'a> {
/// (`GuestReg::CtxAddr`: the x87 stack, `EFLAGS_SYS`), whose accesses
/// the null guard leaves out: the context is below it.
value_ctx: Vec<bool>,
/// How an `Expr::Mx` is lowered here ([`Emitter::stmts`]).
mx_mode: MxMode,
/// Values are declared before their definitions: a definition assigns.
predeclared: bool,
}
/// The most `Expr::Mx`s under one test of the rounding control
/// ([`Emitter::stmts`]).
const MX_RUN: usize = 32;
/// How [`Emitter::def`] lowers an `Expr::Mx`.
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
enum MxMode {
/// The operation's own C, then the exact call when the rounding control
/// is not to nearest: one test per operation.
Each,
/// The operation's own C: the run's test found rounding to nearest.
Own,
/// The exact call: the run's test found another rounding.
Exact,
}
impl<'a> Emitter<'a> {
@@ -280,6 +300,8 @@ impl<'a> Emitter<'a> {
value_zext: vec![false; n],
value_disp: vec![None; n],
value_ctx: vec![false; n],
mx_mode: MxMode::Each,
predeclared: false,
}
}
@@ -322,9 +344,7 @@ impl<'a> Emitter<'a> {
if gex_abi::DISPATCH_SHIFT > gex_abi::INST_ALIGN_SHIFT {
self.target_check();
}
for s in &block.stmts {
self.stmt(s)?;
}
self.stmts(&block.stmts, |e, k| e.stmt(&block.stmts[k]))?;
self.terminator()
}
@@ -1246,7 +1266,7 @@ impl<'a> Emitter<'a> {
} else {
format!("gex_fmaf({a}, {b}, {c})")
};
self.line(format!("const {} {d} = ({}){e};", cty(*ty), cty(*ty)));
self.line(format!("{} = ({}){e};", self.decl(*ty, &d), cty(*ty)));
return Ok(());
}
}
@@ -1254,54 +1274,100 @@ impl<'a> Emitter<'a> {
if func.clobbers_guest_state() {
self.flush();
}
self.line(format!("const {} {d} = ({}){call};", cty(*ty), cty(*ty)));
self.line(format!("{} = ({}){call};", self.decl(*ty, &d), cty(*ty)));
if func.clobbers_guest_state() {
self.invalidate();
}
return Ok(());
}
if let Expr::Mx { mx, e } = expr {
// `e`'s own C, then the exact import when the bits ask for it:
// a rounding other than to nearest, or FTZ or DAZ with a float
// operand or the result denormal or the smallest normal (see
// `runtime/mx.h`).
let fast = self.rhs(idx, e)?;
let m = self.v(*mx);
let plan = MxPlan::of(e);
let w = |t: Ty| if t == Ty::I64 { 8 } else { 4 };
let (a, b, in_bytes, out_bytes) = match plan {
MxPlan::Scalar { a, b, in_ty, .. } => (a, b, w(in_ty), w(ty)),
MxPlan::Lanes { a, b, in_bytes, out_bytes, .. } => (a, b, in_bytes, out_bytes),
};
let tiny = |v: String, bytes: u32| {
if ty == Ty::V128 {
format!("gex_mx_tiny_v({v}, {bytes})")
} else if bytes == 8 {
format!("gex_mx_tiny64((uint64_t){v})")
} else {
format!("gex_mx_tiny32((uint32_t){v})")
match self.mx_mode {
MxMode::Own => {
let own = self.rhs(idx, e)?;
self.line(format!("{} = {own};", self.decl(ty, &d)));
}
};
let mut redo = format!("((uint32_t){m} & 0x6000u) != 0");
if plan.float_operands() {
for v in [Some(a), b].into_iter().flatten() {
redo += &format!(" || {}", tiny(self.v(v), in_bytes));
MxMode::Exact => {
let exact = self.mx_exact(*mx, e);
self.line(format!("{} = {exact};", self.decl(ty, &d)));
}
MxMode::Each => {
let own = self.rhs(idx, e)?;
let m = self.v(*mx);
let exact = self.mx_exact(*mx, e);
let decl = if self.predeclared { String::new() } else { format!("{} ", cty(ty)) };
self.line(format!("{decl}{d} = {own};"));
self.line(format!(
"if (__builtin_expect(((uint32_t){m} & 0x{:x}u) != 0, 0)) {d} = {exact};",
MX_SPECIAL
));
}
}
redo += &format!(" || {}", tiny(d.clone(), out_bytes));
let slow = self.mx_slow(*mx, e);
self.line(format!("{} {d} = {fast};", cty(ty)));
self.line(format!(
"if (__builtin_expect(((uint32_t){m} & 0x{:x}u) != 0, 0) && ({redo})) {d} = {slow};",
MX_SPECIAL
));
return Ok(());
}
let rhs = self.rhs(idx, expr)?;
self.line(format!("const {} {d} = {rhs};", cty(ty)));
self.line(format!("{} = {rhs};", self.decl(ty, &d)));
Ok(())
}
/// The left of a definition of `d`: a declaration, or the name alone
/// when it was declared ahead ([`Emitter::stmts`]).
fn decl(&self, ty: Ty, d: &str) -> String {
if self.predeclared {
d.to_string()
} else {
format!("const {} {d}", cty(ty))
}
}
/// A block's statements, each through `one`. Its `Expr::Mx`s go in runs
/// of up to [`MX_RUN`], each from its first to its last: a run tests
/// MXCSR's rounding control once and is emitted twice, with the
/// operations' own C (taken when it is to nearest) and with their exact
/// calls. A run's values are declared ahead, so both copies define them
/// for what follows (the registers' locals are the function's). Short
/// runs keep clang's work per basic block near what the block alone
/// costs. A block that writes MXCSR or makes a host call (which may
/// change it) tests each operation instead.
pub(crate) fn stmts(&mut self, stmts: &[Stmt], mut one: impl FnMut(&mut Self, usize) -> Result<()>) -> Result<()> {
let mxs: Vec<usize> =
stmts.iter().enumerate().filter(|(_, s)| matches!(s, Stmt::Def { expr: Expr::Mx { .. }, .. })).map(|(k, _)| k).collect();
let once = !mxs.is_empty()
&& !stmts.iter().any(|s| {
matches!(
s,
Stmt::WriteReg { reg: GuestReg::Mxcsr, .. } | Stmt::HostCall { .. } | Stmt::Def { expr: Expr::HostCall { .. }, .. }
)
});
if !once {
return (0..stmts.len()).try_for_each(|k| one(self, k));
}
let m = self.reg(GuestReg::Mxcsr);
let mut next = 0;
for chunk in mxs.chunks(MX_RUN) {
let run = chunk[0]..chunk[chunk.len() - 1] + 1;
(next..run.start).try_for_each(|k| one(self, k))?;
for s in &stmts[run.clone()] {
if let Stmt::Def { dst, expr } = s {
self.line(format!("{} {};", cty(expr.ty()), self.v(*dst)));
}
}
let regs = self.regs.clone();
self.predeclared = true;
self.line(format!("if (__builtin_expect(((uint32_t){m} & 0x{:x}u) != 0, 0)) {{", MX_SPECIAL));
self.mx_mode = MxMode::Exact;
run.clone().try_for_each(|k| one(self, k))?;
self.line("} else {");
self.regs = regs;
self.mx_mode = MxMode::Own;
run.clone().try_for_each(|k| one(self, k))?;
self.line("}");
self.mx_mode = MxMode::Each;
self.predeclared = false;
next = run.end;
}
(next..stmts.len()).try_for_each(|k| one(self, k))
}
/// The C expression of `expr`, defining value `idx`.
fn rhs(&mut self, idx: usize, expr: &Expr) -> Result<String> {
Ok(match expr {
@@ -1703,13 +1769,14 @@ impl<'a> Emitter<'a> {
})
}
/// [`Expr::Mx`]'s slow path: the `mx` import through `runtime/mx.h`.
fn mx_slow(&self, mx: Value, e: &Expr) -> String {
/// [`Expr::Mx`] computed exactly: `gex_mx_s` or `gex_mx_v`
/// (`runtime/mx.h`).
fn mx_exact(&self, mx: Value, e: &Expr) -> String {
let m = self.v(mx);
match MxPlan::of(e) {
MxPlan::Scalar { code, a, b, .. } => {
let b = b.map_or("0".to_string(), |b| format!("(uint64_t){}", self.v(b)));
let r = format!("gex_mx_scalar(ctx, {code}, (uint64_t){}, {b}, (uint32_t){m})", self.v(a));
let r = format!("gex_mx_s(ctx, {code}, (uint64_t){}, {b}, (uint32_t){m})", self.v(a));
if e.ty() == Ty::I32 {
format!("(uint32_t){r}")
} else {
@@ -1717,8 +1784,9 @@ impl<'a> Emitter<'a> {
}
}
MxPlan::Lanes { code, a, b, n, in_bytes, out_bytes } => {
let f = n | if in_bytes == 8 { 16 } else { 0 } | if out_bytes == 8 { 32 } else { 0 };
let b = b.map_or("wasm_i64x2_const(0, 0)".to_string(), |b| self.v(b));
format!("gex_mx_lanes(ctx, {code}, {}, {b}, {n}, {in_bytes}, {out_bytes}, (uint32_t){m})", self.v(a))
format!("gex_mx_v(ctx, {code}, {}, {b}, {f}u, (uint32_t){m})", self.v(a))
}
}
}
+77 -3
View File
@@ -1696,7 +1696,8 @@ fn ftab_call(e: &mut Emitter<'_>, t: &str, depth: &str, need: &str, ret: &str, f
/// Emit a block's statements, telling the emitter where each resumable
/// fault statement (`int3`'s `HostCall`) carries on.
fn block_stmts(e: &mut Emitter<'_>, b: &FBlock) -> Result<()> {
for (k, s) in b.stmts.iter().enumerate() {
e.stmts(&b.stmts, |e, k| {
let s = &b.stmts[k];
e.fault_next = match s {
Stmt::HostCall { func: HostFunc::Breakpoint | HostFunc::Illegal, .. } if e.fault_exit.is_some() => {
let pc = b.pcs[k];
@@ -1704,8 +1705,8 @@ fn block_stmts(e: &mut Emitter<'_>, b: &FBlock) -> Result<()> {
}
_ => None,
};
e.stmt(s)?;
}
e.stmt(s)
})?;
e.fault_next = None;
Ok(())
}
@@ -2718,4 +2719,77 @@ b2 0x4007..0x4008 insts [4007]:
crate::toolchain::compile_one(&dir, &dir.join("u.c"), &dir.join("u.o"), None).unwrap();
let _ = std::fs::remove_dir_all(&dir);
}
/// `0x5000`: one block of `n` SSE operations, vectors of each lane
/// shape and scalars, each on the last's result; under MXCSR
/// (`Expr::Mx`, as the lifter makes them under `gex_ir::mxcsr_exact`)
/// when `mx`, else the operations alone (as it makes them without).
fn sse_heavy(n: usize, mx: bool) -> String {
let mut b = String::from(" @5000 v0 = read mxcsr\n @5000 v1 = read q0\n @5000 v2 = read q1\n @5000 v3 = read x0\n @5000 v4 = read x1\n");
let (mut q, mut x, mut v) = (1, 3, 5);
for k in 0..n {
let e = match k % 6 {
0 => format!("vlanebin Add F32 v{q} v2"),
1 => format!("vlanebin Mul F64 v{q} v2"),
2 => format!("vlaneun Sqrt F32 v{q}"),
3 => format!("vcvt DemoteF64 F64 v{q} -"),
4 => format!("fbin Div f64 v{x} v4"),
_ => format!("fun Sqrt f64 v{x}"),
};
let e = if mx { format!("mx v0 {e}") } else { e };
b += &format!(" @5000 v{v} = {e}\n");
if k % 6 < 4 {
q = v;
} else {
x = v;
}
v += 1;
}
b += &format!(" @5000 write q0 v{q}\n @5000 write x0 v{x}\n @5000 return v{x}\n");
format!("func 0x5000 - entries [] form memory values {v}\nb0 0x5000..0x5001 insts [5000]:\n{b}")
}
/// The function tier's C of `sse_heavy(n, mx)`'s function.
fn sse_heavy_c(n: usize, mx: bool) -> (String, String) {
let f = gex_ir::text::parse(&sse_heavy(n, mx)).unwrap();
let known: HashSet<u64> = [0x5000].into_iter().collect();
let mut unit = FnUnit::new();
unit.add(&f, &known, &HashMap::new(), &HashMap::new()).unwrap();
let c = unit.source();
let body = c[c.find(&format!("{}(", body_name(0x5000))).unwrap()..].to_string();
(c, body)
}
/// Without MXCSR, an SSE operation's C is the operation's own and
/// nothing else. Under it, a block's run of up to 32 of them tests the
/// rounding control once and is the operations' own C or their exact calls, out
/// of line in `runtime/mx.h` (an operation is one call however many
/// lanes): no more than twice the C. A test and an inline slow path
/// per operation was twice the C as well, but as thousands of branches
/// in one function, which took clang over 32 GB.
#[test]
fn sse_operations_under_mxcsr_test_once_a_block() {
if !gex_abi::X86 {
return; // MXCSR is x86's
}
let n = 600;
let (_, plain) = sse_heavy_c(n, false);
assert!(!plain.contains("gex_mx"), "no MXCSR without it");
let (c, body) = sse_heavy_c(n, true);
let exact = body.matches("gex_mx_s(").count() + body.matches("gex_mx_v(").count();
assert_eq!(exact, n, "one out-of-line exact call per operation");
for inline in ["gex_mx(", "replace_lane", "extract_lane"] {
assert!(!body.contains(inline), "`{inline}` is out of line, not in the function");
}
let tests = |c: &str| c.matches("__builtin_expect").count();
assert_eq!(tests(&body), tests(&plain) + n.div_ceil(32), "one rounding-control test for each run of 32");
let (with, without) = (body.len(), plain.len());
assert!(with < 2 * without + 64 * n, "{with} bytes of C under MXCSR, {without} without");
eprintln!("{with} bytes of C under MXCSR, {without} without ({n} operations)");
let dir = std::env::temp_dir().join(format!("gex-cgen-mx-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("gex_abi.h"), crate::header::header()).unwrap();
std::fs::write(dir.join("u.c"), &c).unwrap();
crate::toolchain::compile_one(&dir, &dir.join("u.c"), &dir.join("u.o"), None).unwrap();
let _ = std::fs::remove_dir_all(&dir);
}
}
+3
View File
@@ -102,6 +102,9 @@ fn unit_inputs() -> String {
// The null guard, a debugging opt-in: named only when on, so the
// units of a build without it keep their keys.
let base = if gex_emit::tier1::null_guard_on() { format!("{base}:nullguard:{:x}", gex_abi::NULL_GUARD) } else { base };
// SSE under MXCSR (`gex_ir::mxcsr_exact`), a per-program opt-in, the
// same way: the units with it and without it never mix.
let base = if gex_ir::mxcsr_exact() { format!("{base}:mxcsr") } else { base };
// So is the guest memory (docs/mem64.md): a wasm64 unit is a different
// module. Nothing is added for wasm32, whose keys stay as they were.
if gex_abi::mem64() {
+11 -5
View File
@@ -741,10 +741,16 @@ impl ModuleBuilder {
vec![ValType::I64],
));
let t_mx = m.func_type(FuncType::new(
vec![p, ValType::I32, ValType::I64, ValType::I64, ValType::I32],
vec![ValType::I64],
));
// The MXCSR import only for a program lifted under MXCSR
// (`gex_ir::mxcsr_exact`): without it, the module is as if it did
// not exist, and nothing calls it.
let mx_on = gex_ir::mxcsr_exact();
let t_mx = mx_on.then(|| {
m.func_type(FuncType::new(
vec![p, ValType::I32, ValType::I64, ValType::I64, ValType::I32],
vec![ValType::I64],
))
});
let im = IMPORT_MODULE;
let imports = Imports {
@@ -758,7 +764,7 @@ impl ModuleBuilder {
safepoint: m.import_func(im, imports::SAFEPOINT, t_syscall),
fma: m.import_func(im, imports::FMA, t_fma),
x87: m.import_func(im, imports::X87, t_x87),
mx: m.import_func(im, imports::MX, t_mx),
mx: t_mx.map_or(u32::MAX, |t| m.import_func(im, imports::MX, t)),
};
// The memory is the host's and shared, so every guest thread acts on
+5 -84
View File
@@ -91,13 +91,6 @@ mod simd {
pub const I32X4_EXTADD_PAIRWISE_I16X8_S: u32 = 0x7e;
pub const I32X4_EXTADD_PAIRWISE_I16X8_U: u32 = 0x7f;
pub const V128_AND: u32 = 0x4e;
pub const V128_ANY_TRUE: u32 = 0x53;
pub const I32X4_EQ: u32 = 0x37;
pub const I32X4_SHR_U: u32 = 0xad;
pub const I32X4_SUB: u32 = 0xb1;
pub const I64X2_EQ: u32 = 0xd6;
pub const I64X2_SHR_U: u32 = 0xcd;
pub const I64X2_SUB: u32 = 0xd1;
pub const V128_ANDNOT: u32 = 0x4f;
pub const V128_OR: u32 = 0x50;
pub const V128_XOR: u32 = 0x51;
@@ -258,11 +251,13 @@ fn t1_fold() -> bool {
}
/// The emitter switches that change tier-1's bytes without showing in the
/// translation options, for a cache key (GEX_INLINE_DISPATCH, GEX_T1_FOLD):
/// translation options, for a cache key (GEX_INLINE_DISPATCH, GEX_T1_FOLD,
/// GEX_NULL_GUARD, GEX_MXCSR):
/// natively they are read from the environment, which the module cache's key
/// did not see, so a run with one set served its modules to runs without.
pub fn knobs() -> u8 {
inline_dispatch() as u8 | (t1_fold() as u8) << 1 | (gex_abi::kuser_fold() as u8) << 2 | (null_guard_on() as u8) << 3
| (gex_ir::mxcsr_exact() as u8) << 4
}
/// Whether the translators put the null guard (`gex_abi::NULL_GUARD`) on
@@ -3144,11 +3139,8 @@ impl<'a> Emitter<'a> {
self.value_local[idx] = Some(local);
}
/// An [`Expr::Mx`]: `e`'s own lowering, kept when `mx` has none of
/// [`MX_SPECIAL`]'s bits; and kept as well under FTZ or DAZ alone
/// (rounding to nearest) when no float operand or result is a denormal
/// or the smallest normal (one a tiny result may have rounded to), as
/// then neither flush has anything to do. Otherwise the pure `mx`
/// An [`Expr::Mx`]: `e`'s own lowering, kept when `mx`'s rounding
/// control ([`MX_SPECIAL`]) is to nearest; otherwise the pure `mx`
/// import, once for a scalar and once per lane ([`MxPlan`]). Either
/// way the result is left in one integer-typed local.
fn emit_mx(&mut self, dst: Value, mx: Value, e: &Expr) {
@@ -3164,26 +3156,6 @@ impl<'a> Emitter<'a> {
self.f.i32_const(MX_SPECIAL as i32);
self.f.byte(I32_AND);
self.f.if_void();
self.push(mx);
self.f.i32_const(0x6000);
self.f.byte(I32_AND);
let w = |t: Ty| if t == Ty::I64 { 8 } else { 4 };
let (a, b, in_bytes, out_bytes) = match plan {
MxPlan::Scalar { a, b, in_ty, .. } => (a, b, w(in_ty), w(ty)),
MxPlan::Lanes { a, b, in_bytes, out_bytes, .. } => (a, b, in_bytes, out_bytes),
};
let lanes = ty == Ty::V128;
if plan.float_operands() {
for v in [Some(a), b].into_iter().flatten() {
self.push(v);
self.emit_tiny_check(lanes, in_bytes);
self.f.byte(I32_OR);
}
}
self.f.local_get(res);
self.emit_tiny_check(lanes, out_bytes);
self.f.byte(I32_OR);
self.f.if_void();
// The import's operand: an integer of `in_bytes` (from lane `lane`
// of a vector), as an i64.
let operand = |this: &mut Self, v: Value, lane: Option<(u32, u32)>, in_ty: Ty| {
@@ -3239,7 +3211,6 @@ impl<'a> Emitter<'a> {
}
}
self.f.end();
self.f.end();
self.value_ty[idx] = ty;
if let Some(f) = self.value_fp.get_mut(idx) {
*f = None;
@@ -3248,56 +3219,6 @@ impl<'a> Emitter<'a> {
self.value_local[idx] = Some(res);
}
/// Replace the float bits on the stack (a scalar of `bytes`, or a vector
/// of `bytes`-wide lanes) with an i32, nonzero when it (a lane) is a
/// denormal or the smallest normal: `(|x| - 1) >> fraction bits == 0`.
fn emit_tiny_check(&mut self, lanes: bool, bytes: u32) {
use code::*;
let frac = if bytes == 8 { 52 } else { 23 };
let mut abs = [0xffu8; 16];
let mut one = [0u8; 16];
for k in 0..16 / bytes as usize {
abs[k * bytes as usize + bytes as usize - 1] = 0x7f;
one[k * bytes as usize] = 1;
}
match (lanes, bytes) {
(false, 8) => {
self.f.i64_const(i64::MAX);
self.f.byte(I64_AND);
self.f.i64_const(1);
self.f.byte(I64_SUB);
self.f.i64_const(frac);
self.f.byte(I64_SHR_U);
self.f.byte(I64_EQZ);
}
(false, _) => {
self.f.i32_const(i32::MAX);
self.f.byte(I32_AND);
self.f.i32_const(1);
self.f.byte(I32_SUB);
self.f.i32_const(frac as i32);
self.f.byte(I32_SHR_U);
self.f.byte(I32_EQZ);
}
(true, _) => {
let (sub, shr, eq) = if bytes == 8 {
(simd::I64X2_SUB, simd::I64X2_SHR_U, simd::I64X2_EQ)
} else {
(simd::I32X4_SUB, simd::I32X4_SHR_U, simd::I32X4_EQ)
};
self.f.v128_const(&abs);
self.f.simd(simd::V128_AND);
self.f.v128_const(&one);
self.f.simd(sub);
self.f.i32_const(frac as i32);
self.f.simd(shr);
self.f.v128_const(&[0; 16]);
self.f.simd(eq);
self.f.simd(simd::V128_ANY_TRUE);
}
}
}
/// Reinterpret the integer bit pattern on top of the stack as a float.
/// Reverse the bytes of the value on the stack. Wasm
/// has no byte swap: 32 bits is two rotates, two masks and an or; 16 bits
+16 -8
View File
@@ -59,7 +59,9 @@ fn host_imports(m: &mut Module) -> Imports {
vec![p, ValType::I32, ValType::I64, ValType::I32, ValType::I64, ValType::I32, ValType::I32],
vec![ValType::I64],
));
let t_mx = m.func_type(FuncType::new(vec![p, ValType::I32, ValType::I64, ValType::I64, ValType::I32], vec![ValType::I64]));
// The MXCSR import only under `gex_ir::mxcsr_exact` (tier-1's `ModuleBuilder`).
let t_mx = gex_ir::mxcsr_exact()
.then(|| m.func_type(FuncType::new(vec![p, ValType::I32, ValType::I64, ValType::I64, ValType::I32], vec![ValType::I64])));
let im = IMPORT_MODULE;
let out = Imports {
syscall: m.import_func(im, imports::SYSCALL, t_syscall),
@@ -72,7 +74,7 @@ fn host_imports(m: &mut Module) -> Imports {
safepoint: m.import_func(im, imports::SAFEPOINT, t_syscall),
fma: m.import_func(im, imports::FMA, t_fma),
x87: m.import_func(im, imports::X87, t_x87),
mx: m.import_func(im, imports::MX, t_mx),
mx: t_mx.map_or(u32::MAX, |t| m.import_func(im, imports::MX, t)),
dispatch: m.import_func(gex_cgen::splice::DISPATCH_MODULE, gex_cgen::splice::DISPATCH_NAME, block_ty),
block_ty,
};
@@ -387,11 +389,13 @@ pub fn build_runtime(
let mut funcs: Vec<Function> = funcs.to_vec();
loop {
let loops: Vec<Vec<Loop>> = funcs.iter().map(|f| if outline { cf::outer_loops(f) } else { Vec::new() }).collect();
// Imports: tier-1's eleven host functions, in `host_imports`' order.
const N_IMPORTS: u32 = 11;
// Imports: tier-1's host functions, in `host_imports`' order: ten,
// and the MXCSR one under `gex_ir::mxcsr_exact`.
let mx_on = gex_ir::mxcsr_exact();
let n_imports: u32 = 10 + mx_on as u32;
let (block_ty, t_syscall, t_miss, t_pc_i32, t_sysreg_read, t_pc_i32_i64, t_fma, t_x87, body_ty, loop_ty, t_mx) =
(0u32, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
let mut next = N_IMPORTS;
let mut next = n_imports;
let mut bodies: HashMap<u64, u32> = HashMap::new();
let mut lfns: HashMap<(u64, usize), u32> = HashMap::new();
let mut order: Vec<(u32, u32)> = Vec::new(); // (function index, type)
@@ -419,7 +423,7 @@ pub fn build_runtime(
safepoint: 7,
fma: 8,
x87: 9,
mx: 10,
mx: if mx_on { 10 } else { u32::MAX },
block_ty,
entry_ty: 0,
blocks: &no_blocks,
@@ -472,7 +476,9 @@ pub fn build_runtime(
types.ty().function([i32_, i32_, i64_, i32_, i64_, i32_, i32_], [i64_]);
types.ty().function([i32_, i32_, i32_, i64_], [i32_]);
types.ty().function([i32_, i32_, i32_, i64_, i64_], [i32_]);
types.ty().function([i32_, i32_, i64_, i64_, i32_], [i64_]);
if mx_on {
types.ty().function([i32_, i32_, i64_, i64_, i32_], [i64_]);
}
m.section(&types);
let mut imports = ImportSection::new();
let im = gex_abi::IMPORT_MODULE;
@@ -487,10 +493,12 @@ pub fn build_runtime(
(imports::SAFEPOINT, t_syscall),
(imports::FMA, t_fma),
(imports::X87, t_x87),
(imports::MX, t_mx),
] {
imports.import(im, name, EntityType::Function(ty));
}
if mx_on {
imports.import(im, imports::MX, EntityType::Function(t_mx));
}
imports.import(
im,
exports::MEMORY,
+27 -14
View File
@@ -39,6 +39,25 @@ pub fn take_kuser_folds() -> u64 {
KUSER_FOLDS.with(|c| c.replace(0))
}
static MXCSR_EXACT: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
/// SSE under the guest's MXCSR rounding control, a per-program opt-in: set
/// before anything is translated (the page's `?mxcsr=1`, GEX_MXCSR=1
/// natively). On, an SSE operation MXCSR rounds is lifted as [`Expr::Mx`]
/// and rounds as the rounding control says in every tier. Off, it is the
/// operation alone, rounded to nearest, and every tier's output and every
/// key are as if MXCSR did not exist. FTZ and DAZ are honoured in neither.
pub fn set_mxcsr_exact(on: bool) {
MXCSR_EXACT.store(on, std::sync::atomic::Ordering::Relaxed);
}
/// Whether SSE is lifted under MXCSR ([`set_mxcsr_exact`]).
pub fn mxcsr_exact() -> bool {
static ENV: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
*ENV.get_or_init(|| std::env::var_os("GEX_MXCSR").is_some_and(|v| v != "0"))
|| MXCSR_EXACT.load(std::sync::atomic::Ordering::Relaxed)
}
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Value(pub u32);
@@ -473,11 +492,11 @@ pub enum Expr {
a: Value,
},
/// `e` computed as x86's SSE computes it under the `I32` MXCSR value
/// `mx`: rounded by its rounding control (bits 13-14) instead of to
/// nearest, a denormal operand read as a zero of its sign under DAZ
/// (bit 6), a denormal result written as one under FTZ (bit 15). With
/// none of those bits set ([`MX_SPECIAL`]) it is `e` exactly, so a
/// backend lowers it as `e` behind that one test.
/// `mx`'s rounding control (bits 13-14, [`MX_SPECIAL`]) instead of to
/// nearest. FTZ and DAZ are not honoured: denormals are kept, as wasm
/// keeps them. With the rounding to nearest it is `e` exactly, so a
/// backend lowers it as `e` behind that one test (for a run of them,
/// one test for the run).
///
/// `e` is one of [`Expr::mx_able`]'s: a float add, subtract, multiply,
/// divide or square root, a round to nearest (an integer value, the
@@ -487,8 +506,9 @@ pub enum Expr {
Mx { mx: Value, e: Box<Expr> },
}
/// The MXCSR bits [`Expr::Mx`] honours: rounding control, FTZ, DAZ.
pub const MX_SPECIAL: u32 = 0xe040;
/// The MXCSR bits [`Expr::Mx`] honours: the rounding control. The exact
/// import (`gex_abi::imports::MX`) is given only these.
pub const MX_SPECIAL: u32 = 0x6000;
/// How an [`Expr::Mx`]'s operation is computed off its fast path: by
/// `gex_f80::sse::op` with `code`, on raw bits, once for a scalar or once
@@ -567,13 +587,6 @@ impl MxPlan {
_ => panic!("MxPlan::of: {e:?} is not an operation MXCSR rounds"),
}
}
/// The operands are floats (DAZ reads them), not a conversion's integers.
pub fn float_operands(&self) -> bool {
use gex_f80::sse::*;
let (MxPlan::Scalar { code, .. } | MxPlan::Lanes { code, .. }) = *self;
!matches!(code & 0xf, FROM_I32 | FROM_I64)
}
}
/// Scalar floating-point operations.
+24
View File
@@ -130,6 +130,30 @@ mod tests {
assert_eq!(strops(&b), 0, "{}", print::block(&b));
}
/// SSE is lifted under MXCSR only for a program that asks
/// (`gex_ir::mxcsr_exact`): without it, `addps`, `mulsd`, `sqrtps` and
/// `cvtpd2ps` are their operations alone, with no `Expr::Mx` and no
/// read of MXCSR, as if MXCSR did not exist; with it, each is one.
#[test]
fn sse_is_under_mxcsr_only_when_asked() {
let mxs = |b: &Block| {
b.stmts.iter().filter(|s| matches!(s, Stmt::Def { expr: Expr::Mx { .. }, .. })).count()
};
let reads = |b: &Block| {
b.stmts.iter().filter(|s| matches!(s, Stmt::Def { expr: Expr::ReadReg(GuestReg::Mxcsr), .. })).count()
};
// addps xmm0, xmm1 ; mulsd xmm2, xmm1 ; sqrtps xmm3, xmm1 ; cvtpd2ps xmm4, xmm1 ; ret
let code = &[0x0f, 0x58, 0xc1, 0xf2, 0x0f, 0x59, 0xd1, 0x0f, 0x51, 0xd9, 0x66, 0x0f, 0x5a, 0xe1, 0xc3];
if !gex_ir::mxcsr_exact() {
let b = lift_block(bytes(0x1000, code), 0x1000, BlockLimits::default());
assert_eq!((mxs(&b), reads(&b)), (0, 0), "{}", print::block(&b));
}
gex_ir::set_mxcsr_exact(true);
let b = lift_block(bytes(0x1000, code), 0x1000, BlockLimits::default());
gex_ir::set_mxcsr_exact(false);
assert_eq!(mxs(&b), 4, "{}", print::block(&b));
}
/// `fpatan` (d9 f3) is handed to the host rather than raising SIGILL:
/// i386 musl's `atan2` is this one instruction.
#[test]
+5 -4
View File
@@ -155,11 +155,12 @@ impl<'a> Lifter<'a> {
self.mx(Expr::VLaneUn { op, lane, a })
}
/// `e` as SSE computes it: under the guest's MXCSR (its rounding
/// control, FTZ and DAZ) when it is an operation MXCSR controls
/// ([`Expr::mx_able`]), else as it is.
/// `e` as SSE computes it: under the guest's MXCSR rounding control
/// when it is an operation MXCSR rounds
/// ([`Expr::mx_able`]) and the program asked for it
/// ([`gex_ir::mxcsr_exact`]), else as it is.
fn mx(&mut self, e: Expr) -> Value {
if !e.mx_able() {
if !e.mx_able() || !gex_ir::mxcsr_exact() {
return self.b.def(e);
}
let mx = self.b.read(GuestReg::Mxcsr);
+2
View File
@@ -1248,6 +1248,8 @@ export class Gex {
if (globalThis.GEX_W3_SPLIT || this.#w64Split) this.#shim.exports.gex_set_w3_split?.(1);
// `?nullguard=1` before anything is translated (gex_abi::null_guard).
if (globalThis.GEX_NULL_GUARD) this.#shim.exports.gex_set_null_guard?.(1);
// `?mxcsr=1` before anything is translated: SSE under MXCSR (gex_ir::mxcsr_exact).
if (globalThis.GEX_MXCSR) this.#shim.exports.gex_set_mxcsr_exact?.(1);
// `?pinregion=BYTES`: a project's libraries' region above the placement
// window, before the first claim of arena (gex_abi::set_pin_region).
if (globalThis.GEX_PIN_REGION) this.#shim.exports.gex_set_pin_region?.(BigInt(globalThis.GEX_PIN_REGION));
+2
View File
@@ -428,6 +428,8 @@ const host = new ProcessHost({
w3Split: !!process.env.GEX_W3_SPLIT,
// GEX_NULL_GUARD=1: as the page's ?nullguard=1 (gex_abi::null_guard).
nullGuard: process.env.GEX_NULL_GUARD === '1' || undefined,
// GEX_MXCSR=1: as the page's ?mxcsr=1 (gex_ir::mxcsr_exact).
mxcsr: process.env.GEX_MXCSR === '1' || undefined,
// GEX_PIN_REGION=BYTES: as the page's ?pinregion= (gex_abi::set_pin_region).
pinRegion: process.env.GEX_PIN_REGION ? Number(process.env.GEX_PIN_REGION) : undefined,
// GEX_SEH_REPORT=1: as the page's ?sehreport=1.
+2
View File
@@ -692,6 +692,8 @@ async function handle(msg) {
if (msg.w3Split) globalThis.GEX_W3_SPLIT = 1;
// `?nullguard=1`: the null guard (gex_abi::null_guard), a debugging opt-in.
if (msg.nullGuard) globalThis.GEX_NULL_GUARD = 1;
// `?mxcsr=1`: SSE under MXCSR (gex_ir::mxcsr_exact), a per-program opt-in.
if (msg.mxcsr) globalThis.GEX_MXCSR = 1;
// `?pinregion=BYTES`: the pin region (gex_abi::set_pin_region).
if (msg.pinRegion) globalThis.GEX_PIN_REGION = msg.pinRegion;
// `?sehreport=1`: crash reports name the SEH chain (a debugging opt-in).
+11 -3
View File
@@ -2506,6 +2506,14 @@ pub extern "C" fn gex_set_null_guard(on: u32) {
gex_abi::set_null_guard(on != 0);
}
/// `gex_set_mxcsr_exact(1)` (`?mxcsr=1`, run-node's GEX_MXCSR=1), before
/// anything is translated: SSE under the guest's MXCSR
/// (`gex_ir::mxcsr_exact`), a per-program opt-in.
#[no_mangle]
pub extern "C" fn gex_set_mxcsr_exact(on: u32) {
gex_ir::set_mxcsr_exact(on != 0);
}
/// `?pinregion=BYTES`: the pin region an ejected project's libraries are
/// placed in (`gex_abi::set_pin_region`), before this Worker takes anything
/// from the arena.
@@ -13956,11 +13964,11 @@ pub extern "C" fn gex_fma(_ctx: i32, a: i64, b: i64, c: i64, double: i32) -> i64
}
}
/// An SSE operation under a non-default MXCSR (`gex_abi::imports::MX`):
/// exact, pure.
/// An SSE operation under a rounding control other than to nearest
/// (`gex_abi::imports::MX`): exact, pure; MXCSR's other bits ignored.
#[no_mangle]
pub extern "C" fn gex_mx(_ctx: i32, code: i32, a: i64, b: i64, mx: i32) -> i64 {
gex_kernel::f80::sse::op(code as u32, a as u64, b as u64, mx as u32) as i64
gex_kernel::f80::sse::op(code as u32, a as u64, b as u64, mx as u32 & gex_ir::MX_SPECIAL) as i64
}
/// An x87 operation the inline fast path left to the host (see
+3 -2
View File
@@ -5001,9 +5001,10 @@ fn register_imports(linker: &mut Linker<HostState>) -> Result<()> {
})()) },
)?;
// An SSE operation under a non-default MXCSR (`gex_abi::imports::MX`).
// An SSE operation under a rounding control other than to nearest
// (`gex_abi::imports::MX`); MXCSR's other bits ignored.
linker.func_wrap(m, imports::MX, |_ctx: i32, code: i32, a: i64, b: i64, mx: i32| -> i64 {
gex_kernel::f80::sse::op(code as u32, a as u64, b as u64, mx as u32) as i64
gex_kernel::f80::sse::op(code as u32, a as u64, b as u64, mx as u32 & gex_ir::MX_SPECIAL) as i64
})?;
// The counting flavor's table-call note (`gex_cgen::image::icall_count`):
+5 -4
View File
@@ -1,6 +1,7 @@
/* The rounding and denormal controls: SSE arithmetic and conversions under
* each MXCSR rounding mode and with FTZ and DAZ, and x87 arithmetic and
* stores under each rounding and precision control of the control word.
/* The rounding controls: SSE arithmetic and conversions under each MXCSR
* rounding mode (gex honours MXCSR's rounding control, not FTZ or DAZ), and
* x87 arithmetic and stores under each rounding and precision control of
* the control word.
* Each line is one operation in one mode, a hash of its results over every
* pair of a set of operands (denormals, values a rounding mode moves, ones
* that overflow or underflow), compared with the hardware's.
@@ -127,7 +128,7 @@ static void x87(uint16_t cw) {
int main(void) {
#if defined(__x86_64__)
static const uint32_t modes[] = {0x0000, 0x2000, 0x4000, 0x6000, 0x8000, 0x0040, 0x8040, 0xe040};
static const uint32_t modes[] = {0x0000, 0x2000, 0x4000, 0x6000};
for (unsigned k = 0; k < sizeof modes / sizeof modes[0]; k++) sse(modes[k]);
#endif
static const uint16_t cws[] = {0x037f, 0x077f, 0x0b7f, 0x0f7f, 0x007f, 0x027f, 0x0e7f, 0x047f};
+12 -4
View File
@@ -221,9 +221,12 @@ SEED64=web/public/gex/seed.x86.m64.shared.wasm
# descriptors block until a second thread's signal ends them with EINTR
# (i386's pause is a call of its own).
# fp-modes: SSE arithmetic, square roots, roundings and conversions under
# each MXCSR rounding mode with FTZ and DAZ (wasm rounds only to nearest:
# the other modes take the exact slow path), and x87 arithmetic and stores
# under each rounding and precision control; under the function JIT too.
# each MXCSR rounding mode (wasm rounds only to nearest: the other modes take
# the exact slow path), and x87 arithmetic and stores
# under each rounding and precision control; under the function JIT and in
# the function tier's C too.
# SSE under MXCSR's rounding control is a per-program opt-in (GEX_MXCSR=1,
# the page's ?mxcsr=1).
PAGE_PROGRAMS=${PAGE_PROGRAMS:-"fork-dirs dense-lazy dense-holes spawn-threads served-entries page-cross-entry tgkill-futex exec-tail mem64-io mem64-net mem64-console mem64-smc mem64-reserve retire-during-lift close-while-reading thread-signals signal-storm exec-signals clock-monotonic main-thread-exit pi-mutex tcp-loopback pwait-sigmask sa-restart thread-churn kernel-contend exec-futex sleep-threads sendmsg-rights scm-fork ioctl-threads fdtable-threads mmap-threads codemap-threads proc-threads fs-threads sig-state-threads lift-threads fdmisc-threads task-ids sock-threads exec-dethread fork-thread-ctx fork-maps-threads fork-twice-thread quiet-threads fdobj-threads cred-threads kill-threads sigwait-threads poll-timeout tid-threads mount-threads exit-threads many-workers threads-many stale-loop-head file-stale-loop stale-cross-page xattr mem64-wx mmap-coherent mmap-remap mmap-writeback null-access sig-regs-threads sig-pause fp-modes"}
for src in "$HERE"/*.c; do
name=$(basename "$src" .c)
@@ -235,7 +238,9 @@ for src in "$HERE"/*.c; do
# *smc*: self-modifying code, under the function JIT as well (which
# must leave writable-and-executable code to tier-1's guards).
tiers="t1"
case $name in *smc* | copy-loop | null-access | sig-regs-threads | fp-modes) tiers="t1 fjit" ;; esac
case $name in *smc* | copy-loop | null-access | sig-regs-threads) tiers="t1 fjit" ;; esac
# fp-modes: the function tier's C as well.
case $name in fp-modes) tiers="t1 fjit cgen" ;; esac
case " $PAGE_PROGRAMS " in *" $name "*)
if [ -f "$SEED64" ] && command -v node >/dev/null; then tiers="$tiers page"; fi ;;
esac
@@ -246,6 +251,7 @@ for src in "$HERE"/*.c; do
case " ${TIERS:-$tier} " in *" $tier "*) ;; *) continue ;; esac
flags=$gexflags
[ $tier = fjit ] && flags="$flags --fjit"
[ $tier = cgen ] && flags="$flags --cgen"
extra=""
[ "$target" = x86-linux-musl ] && extra="-Wl,--image-base=0x1000000"
bin="$WORK/$name-$target"
@@ -279,6 +285,8 @@ for src in "$HERE"/*.c; do
[ $name = smc-race ] && envs="GEX_TEST_LIFT_DELAY_US=300"
# null-access needs the null guard, a debug opt-in (gex_abi::null_guard).
[ $name = null-access ] && envs="GEX_NULL_GUARD=1"
# fp-modes needs SSE under MXCSR, a per-program opt-in (gex_ir::mxcsr_exact).
[ $name = fp-modes ] && envs="GEX_MXCSR=1"
# mem64-net reaches the host: the page gets a network of real TCP sockets.
[ $name = mem64-net ] && envs="GEX_NETWORK=1"
[ $name = page-cross-entry ] && [ $tier = page ] && envs="GEX_TEST_UNPUBLISHED_RETIRE=1"
+4
View File
@@ -979,6 +979,10 @@ export function setGlobals(get, b = {}) {
// natively (gex_abi::null_guard). A debugging opt-in: off, tier 1 and the
// units are as without it; on, 2-8% slower in tier 1.
if (get('nullguard') === '1') globalThis.GEX_NULL_GUARD = 1;
// mxcsr=1: SSE honours MXCSR's rounding control in every tier
// (gex_ir::mxcsr_exact), for a program that needs it. Off, SSE rounds to
// nearest, and tier 1 and the units are as without it.
if (get('mxcsr') === '1') globalThis.GEX_MXCSR = 1;
// pinregion=BYTES: address space above the placement window for the
// project's libraries, each at its build's base (gex_abi::set_pin_region).
if (get('pinregion')) globalThis.GEX_PIN_REGION = Number(get('pinregion'));
+4 -1
View File
@@ -134,7 +134,8 @@ function xcacheName() {
// `tso=arm` names one switch value for two flavours: the host's is named too.
+ (flag('tso') === 'arm' && tso() ? '&tsohost=1' : '')
// Named only when on: the cache of a page without it keeps its name.
+ (globalThis.GEX_NULL_GUARD ? '&nullguard=1' : '');
+ (globalThis.GEX_NULL_GUARD ? '&nullguard=1' : '')
+ (globalThis.GEX_MXCSR ? '&mxcsr=1' : '');
const flavour = globalThis.GEX_FLAVOUR ?? (typeof __GEX_FLAVOUR__ !== 'undefined' ? __GEX_FLAVOUR__ : '');
return `xc-${xcHash(shimBytes, kernelBytes, switches, flavour, globalThis.location?.pathname ?? '')}`;
}
@@ -1106,6 +1107,8 @@ async function runProgramHere(
w3Split: globalThis.GEX_W3_SPLIT || undefined,
// `?nullguard=1`: the null guard, a debugging opt-in (gex_abi::null_guard).
nullGuard: globalThis.GEX_NULL_GUARD || undefined,
// `?mxcsr=1`: SSE under MXCSR, a per-program opt-in (gex_ir::mxcsr_exact).
mxcsr: globalThis.GEX_MXCSR || undefined,
// `?pinregion=BYTES`: an ejected project's libraries' region (gex_abi::set_pin_region).
pinRegion: globalThis.GEX_PIN_REGION || undefined,
// `?sehreport=1`: crash reports name the SEH chain (a debugging opt-in).