From 6d3e9117fb7e9ab607ede00f1c33efd3a069ab9c Mon Sep 17 00:00:00 2001 From: Yona Appletree Date: Fri, 31 Jul 2026 23:33:50 -0700 Subject: [PATCH 1/4] fix(builtins): keep rgb2hsv's f32 constants out of the LLVM constant pool MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The esp Xtensa backend cannot select `XtensaISD::PCREL_WRAPPER` over a `TargetConstantPool`, so any `[N x float]` constant LLVM materialises as a constant-pool entry fails to compile for `xtensa-esp32s3-none-elf` at every optimisation level. `lps-builtins/float-f32` tripped it in `rgb2hsv`, whose `p.zw` constant lane pair LLVM emits as `[2 x float] [0.0, -1.0]`. Select the integer bit patterns and bitcast after the fact, so the constants never reach the float constant pool. `f32::to_bits` is const-evaluated, so the values are the same floats the literals were — pinned lane-by-lane against the literal form it replaced by a new test, since these builtins are shared with rv32 and wasm where the original compiles fine. The defect's guess that snoise2's gradient LUT was the trigger was wrong; it and every other suspect (`worley2/3`, `gnoise3_tile`, `psrdnoise2/3`, `snoise3`) compile unchanged. One site was enough. Defect: docs/defects/2026-08-01-xtensa-backend-cannot-select-float-constant-pool.md Co-Authored-By: Claude Fable 5 --- .../builtins/lpfn/color/space/rgb2hsv_f32.rs | 93 ++++++++++++++++++- 1 file changed, 88 insertions(+), 5 deletions(-) diff --git a/lp-shader/lps-builtins/src/builtins/lpfn/color/space/rgb2hsv_f32.rs b/lp-shader/lps-builtins/src/builtins/lpfn/color/space/rgb2hsv_f32.rs index 1eebd8281..c167fc919 100644 --- a/lp-shader/lps-builtins/src/builtins/lpfn/color/space/rgb2hsv_f32.rs +++ b/lp-shader/lps-builtins/src/builtins/lpfn/color/space/rgb2hsv_f32.rs @@ -17,16 +17,49 @@ //! //! **Tolerance:** exact against the canonical f32. +// The `p.zw` constants of Hocevar's formulation, as f32 *bit patterns*. +// +// **This is an upstream-LLVM workaround, not a preference.** Written the +// natural way — `if g < b { [b, g, -1.0, 2.0 / 3.0] } else { [g, b, 0.0, +// -1.0 / 3.0] }` — LLVM materialises the constant lane pair as a +// `[2 x float]` constant-pool entry, and the esp Xtensa backend cannot select +// `XtensaISD::PCREL_WRAPPER` over a `TargetConstantPool`, so the whole f32 +// builtins family fails to build for `xtensa-esp32s3-none-elf` at every +// optimisation level. Selecting the integer bit patterns and bitcasting after +// the fact keeps the constants out of the float constant pool. +// +// Defect: docs/defects/2026-08-01-xtensa-backend-cannot-select-float-constant-pool.md +// Upstream: not yet filed. +// +// `f32::to_bits` is const-evaluated, so these are the same floats the literals +// were; `hocevar_p_is_bit_identical_to_the_literal_form` pins that, and is the +// test to keep green if this is ever reverted. +const P_Z_IF: u32 = (-1.0f32).to_bits(); +const P_W_IF: u32 = (2.0f32 / 3.0).to_bits(); +const P_Z_ELSE: u32 = (0.0f32).to_bits(); +const P_W_ELSE: u32 = (-1.0f32 / 3.0).to_bits(); + +/// Hocevar's `p` term: the green/blue ordering step. +/// +/// Split out of `rgb2hsv` only so the constant-pool workaround above can be +/// tested against the literal form it replaced. +#[inline(always)] +fn hocevar_p(g: f32, b: f32) -> [f32; 4] { + let g_lt_b = g < b; + [ + if g_lt_b { b } else { g }, + if g_lt_b { g } else { b }, + f32::from_bits(if g_lt_b { P_Z_IF } else { P_Z_ELSE }), + f32::from_bits(if g_lt_b { P_W_IF } else { P_W_ELSE }), + ] +} + /// Rust-facing form. #[inline] fn rgb2hsv(r: f32, g: f32, b: f32) -> [f32; 3] { const EPSILON: f32 = 1.0 / 65536.0; - let p: [f32; 4] = if g < b { - [b, g, -1.0, 2.0 / 3.0] - } else { - [g, b, 0.0, -1.0 / 3.0] - }; + let p = hocevar_p(g, b); let q: [f32; 4] = if r < p[0] { [p[0], p[1], p[3], r] } else { @@ -123,6 +156,56 @@ mod tests { } } + /// The pre-workaround form of `hocevar_p`, kept verbatim as the oracle. + /// + /// This is the code the Xtensa constant-pool workaround replaced. It stays + /// here so the rewrite's bit-equivalence is *pinned* rather than argued — + /// these builtins are shared with rv32 and wasm, where the original form + /// compiles fine and any drift would be a silent behaviour change. + fn hocevar_p_literal(g: f32, b: f32) -> [f32; 4] { + if g < b { + [b, g, -1.0, 2.0 / 3.0] + } else { + [g, b, 0.0, -1.0 / 3.0] + } + } + + #[test] + fn hocevar_p_is_bit_identical_to_the_literal_form() { + // A grid that crosses g == b in both directions, plus the cases where + // a sign-of-zero or NaN difference would hide: -0.0/+0.0 must select + // the same lanes, and `g < b` is false for any NaN operand. + const SAMPLES: [f32; 11] = [ + -1.0, + -0.75, + -0.5, + -0.25, + 0.0, + 0.25, + 0.5, + 0.75, + 1.0, + -0.0, + f32::NAN, + ]; + + for &g in SAMPLES.iter() { + for &b in SAMPLES.iter() { + let got = hocevar_p(g, b); + let want = hocevar_p_literal(g, b); + for k in 0..4 { + assert_eq!( + got[k].to_bits(), + want[k].to_bits(), + "lane {k} for g={g} b={b}: {:#010x} vs {:#010x}", + got[k].to_bits(), + want[k].to_bits() + ); + } + } + } + } + #[test] fn alpha_passes_through_the_vec4_form_untouched() { let mut out = [f32::NAN; 4]; From d804b2e1d6f5df0905b37a28ebd3d5183aace905 Mon Sep 17 00:00:00 2001 From: Yona Appletree Date: Fri, 31 Jul 2026 23:35:19 -0700 Subject: [PATCH 2/4] build(xt): make float-f32 the Xtensa builtins image's default again MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With the constant-pool workaround in place the image builds, so drop the LP_XT_BUILTINS_F32 opt-in P4 added as a stopgap and assert the f32 symbol count unconditionally, matching scripts/build-builtins.sh (rv32). Un-ignore xt_pipeline_f32's builtin-routing case. It needed more than the ignore attribute removed: the file's link_blob resolves only the module's own functions and loads at the code region's base, where the builtins image already lives, so a sym_call to __lp_lpir_ffloor_f32 could never resolve. Added a builtins-linked run path with rt_emu::xt_image's layout — image .text first, shader 4-aligned after it — hand-rolled rather than calling build_xt_image, which sits behind the `emu` feature the file's other 15 tests do not need. 16/16 green, ffloor(3.75) == 3.0 from the real builtin. Record what the defect actually was: one site (rgb2hsv), not the LUT family, and the reported constant belonged to a different function than the one that failed. Co-Authored-By: Claude Fable 5 --- ...ckend-cannot-select-float-constant-pool.md | 141 ++++++++----- .../lpvm-native/tests/xt_pipeline_f32.rs | 199 +++++++++++++++--- scripts/build-builtins-xt.sh | 37 ++-- 3 files changed, 277 insertions(+), 100 deletions(-) diff --git a/docs/defects/2026-08-01-xtensa-backend-cannot-select-float-constant-pool.md b/docs/defects/2026-08-01-xtensa-backend-cannot-select-float-constant-pool.md index 98ad6c296..283d87790 100644 --- a/docs/defects/2026-08-01-xtensa-backend-cannot-select-float-constant-pool.md +++ b/docs/defects/2026-08-01-xtensa-backend-cannot-select-float-constant-pool.md @@ -1,6 +1,7 @@ --- -status: open +status: open # the backend is still broken; our source avoids it (see "The workaround") found: 2026-08-01 # how: build (M7 P4, flipping the Xtensa builtins image to float-f32) +worked-around: 2026-08-01 # rgb2hsv_f32.rs; f32 image and filetests unblocked area: lp-xt/lps-builtins-xt-app, lp-shader/lps-builtins, esp Rust toolchain class: upstream-toolchain-limitation related: @@ -30,57 +31,99 @@ same way, only earlier — in `lps-builtins` (lib) itself rather than in the final binary. So the failure is the backend's, at any optimization level, and is not confined to the image crate. -**Trigger** — `lps-builtins/src/builtins/lpfn/generative/snoise/snoise2_f32.rs` -has an 8-entry gradient lookup table written as a `match` returning -`[f32; 2]`. LLVM promotes the arms to constant-pool entries, and the Xtensa -backend has no selection rule for `PCREL_WRAPPER` over a `TargetConstantPool` -node — it can materialize the *address* of ordinary data, but not of a -constant-pool entry. The reported constant, `[0.0, -1.0]`, is that table's -arm 3. Other `[f32; N]` tables in the generative-noise family -(`worley2/3`, `gnoise3_tile`, `psrdnoise2/3`, `snoise3`, `rgb2hsv`) are -plausible further instances; only the first to be selected is reported. +**Trigger** — a *constant aggregate* of floats. The backend can materialize +the address of ordinary data but has no selection rule for `PCREL_WRAPPER` +over a `TargetConstantPool` node, so any `[N x float]` constant LLVM chooses to +put in the constant pool is fatal. Only the first such constant to be selected +is reported, which made the trigger easy to misattribute (see below). **rv32 is unaffected** — `scripts/build-builtins.sh` has passed `--features float-f32` since M5 and builds the same source cleanly. This is specific to the Xtensa target's backend. **It is also load-bearing for the Q32 path.** `scripts/filetests.sh` builds -this image before running the `xtn.*` / `xtlpn.*` targets, so a script that -passes `--features float-f32` unconditionally takes the entire Xtensa filetest -suite down with it. The feature is therefore **opt-in** — the crate wiring is -in place and `LP_XT_BUILTINS_F32=1` requests it — rather than a default that -would trade a blocked f32 path for a broken fixed-point one. - -## Consequence - -The Xtensa builtins image **cannot currently carry M5's f32 symbols**. M7's -hardware-float lowering inlines the single-instruction family but routes -divide, sqrt, the rounding family, min/max, the float→int conversions and -every transcendental to those symbols (M7 D4), so on the host emulation path -those calls have nothing to resolve against. - -What this does *not* block: everything M7 P3 emits inline — `fadd`/`fsub`/ -`fmul`, the sign-bit ops, all six compares, float select, `itof`, float -load/store and the `wfr`/`rfr` boundary transfers. Those are the majority of -the emitted subset and are covered end to end by -`lp-shader/lpvm-native/tests/xt_pipeline_f32.rs`, whose one builtin-calling -case is marked `#[ignore]` pointing here. - -It is also a live risk for **M7 P5**: `fw-esp32s3` naming -`lps-builtins/float-f32` will hit the same wall at firmware build time. - -## Candidate resolutions (not chosen here — out of M7 P4's scope) - -1. **Rewrite the affected LUTs** so LLVM does not form a constant pool — e.g. - return a tuple, index a `const` array through a `static`, or compute the - gradient arithmetically. Cheapest, but it is a workaround living in shared - builtin source with no local explanation, and it must be rediscovered for - every new `[f32; N]` table. -2. **Report upstream** to `esp-rs/rust` and pin the toolchain once fixed. -3. **Split the f32 builtin feature** so the generative-noise family is - separately gated, letting Xtensa link the arithmetic builtins M7 D4 - actually needs while the noise family stays rv32/host-only. - -(3) is the closest fit to what M7 needs — the routed-to-builtin list in D4 is -arithmetic, not noise — but it changes a feature contract M5 owns, so it is a -decision for Yona rather than for this phase. +this image before running the `xtn.*` / `xtlpn.*` targets, so a compile failure +here takes the entire Xtensa filetest suite down with it — not just the f32 +half. + +## What was actually wrong (2026-08-01) + +The first draft of this record named +`lps-builtins/src/builtins/lpfn/generative/snoise/snoise2_f32.rs`'s 8-entry +`[f32; 2]` gradient LUT as the trigger, inferring it from the reported +constant `[0.0, -1.0]` — which happens to be that table's arm 3. **That was +wrong.** The compiler names the containing function, and it is +`__lp_lpfn_rgb2hsv_f32`: + +```rust +// lps-builtins/src/builtins/lpfn/color/space/rgb2hsv_f32.rs +let p: [f32; 4] = if g < b { + [b, g, -1.0, 2.0 / 3.0] +} else { + [g, b, 0.0, -1.0 / 3.0] +}; +``` + +The two constant lanes of Hocevar's `p` term are selected together, and LLVM +materializes the pair as `[2 x float] [0.0, -1.0]` — the same bytes the +snoise2 arm would have produced, from an entirely different function. + +**Exactly one site needed changing.** With `rgb2hsv` fixed the image builds, +and every other suspect this record named — `snoise2`'s and `snoise3`'s +gradient LUTs, `worley2/3`'s offset tables, `gnoise3_tile`'s `CORNERS`, +`psrdnoise2/3` — compiles unchanged and is present in the built ELF. The +pattern that trips the backend is narrower than "a float lookup table": it is a +*fully constant* aggregate that survives to instruction selection. A LUT +indexed by a runtime value becomes an ordinary global; a `match` whose arms are +runtime-scaled (`worley`'s `[diag, diag, 0.0]`) is not a constant aggregate at +all. + +## The workaround (shipped) + +`rgb2hsv_f32.rs` selects the two constant lanes as **integer bit patterns** and +bitcasts afterwards, so no float constant aggregate is ever formed: + +```rust +const P_Z_IF: u32 = (-1.0f32).to_bits(); // const-evaluated from the +const P_W_IF: u32 = (2.0f32 / 3.0).to_bits(); // original literals +... +f32::from_bits(if g_lt_b { P_Z_IF } else { P_Z_ELSE }) +``` + +Chosen over computing the gradient arithmetically because it is *provably* +bit-identical rather than argued: the constants are the literals, put through +`f32::to_bits` at compile time. Sign-of-zero is the specific hazard an +arithmetic rewrite would have introduced (`0.0 * -1.0` is `-0.0`, and the +literal table's zero lane is `+0.0` on both sides). + +The site carries a comment naming this file and stating it is an upstream +limitation, not a preference, with a slot for the upstream issue URL. The +pre-workaround form is kept verbatim in the test module as an oracle: +`hocevar_p_is_bit_identical_to_the_literal_form` compares the two lane-by-lane +over a grid that crosses `g == b`, plus `-0.0` and NaN. These builtins are +shared with rv32 and wasm, where the original compiles fine, so drift would be +a silent behaviour change on targets that never had the problem. + +`scripts/build-builtins-xt.sh` therefore passes `--features float-f32` +unconditionally again (the `LP_XT_BUILTINS_F32` opt-in was a P4 stopgap and is +gone), and `lp-shader/lpvm-native/tests/xt_pipeline_f32.rs`'s +`a_builtin_routed_float_op_resolves_and_runs` is no longer `#[ignore]`d — it +links the shader against the builtins base image and resolves `ffloor` to the +real `__lp_lpir_ffloor_f32` on the M6 emulator. + +## Why this stays open + +The backend limitation is untouched. A new `[f32; N]` constant aggregate +anywhere in `lps-builtins` will fail the same way, with the same one-function- +at-a-time reporting, and **M7 P5's `fw-esp32s3` naming `lps-builtins/float-f32` +is exposed to it**. It closes when the esp fork can select +`PCREL_WRAPPER(TargetConstantPool)` and the toolchain is pinned past that. + +## Remaining resolutions + +1. **Report upstream** to `esp-rs/rust` and pin the toolchain once fixed. Not + yet filed; the comment in `rgb2hsv_f32.rs` and the one in + `build-builtins-xt.sh` are shaped for the URL to be dropped in. +2. **Split the f32 builtin feature** so the generative-noise family is + separately gated. No longer needed — this was the fallback for "Xtensa + cannot have the noise family at all", and it can. diff --git a/lp-shader/lpvm-native/tests/xt_pipeline_f32.rs b/lp-shader/lpvm-native/tests/xt_pipeline_f32.rs index 96183fe2d..26a3635ff 100644 --- a/lp-shader/lpvm-native/tests/xt_pipeline_f32.rs +++ b/lp-shader/lpvm-native/tests/xt_pipeline_f32.rs @@ -41,7 +41,8 @@ use lpvm_native::compile::compile_module; use lpvm_native::isa::IsaTarget; use lpvm_native::native_options::NativeCompileOptions; -use lp_xt_emu::{Emulator, RunOutcome}; +use lp_xt_elf::XtensaElf; +use lp_xt_emu::{CallOutcome, Emulator, RunOutcome}; use lp_xt_inst::{Inst, NullaryNarrowOp, Reg, SpecialReg, SrOp, encode}; /// EXCCAUSE 32 — `Coprocessor0Disabled`, what an FP instruction takes when @@ -108,7 +109,11 @@ fn link_blob( let mut order: Vec = (0..funcs.len()).collect(); order.sort_by_key(|&i| i != entry_idx); - let mut code = if arm_fpu { arm_fpu_preamble() } else { Vec::new() }; + let mut code = if arm_fpu { + arm_fpu_preamble() + } else { + Vec::new() + }; let entry_off = code.len() as u32; let mut entries = VecMap::::new(); let mut func_offsets = vec![0usize; funcs.len()]; @@ -151,6 +156,135 @@ fn run_f32(ir: &LpirModule, sig: &LpsModuleSig, entry_name: &str, args: &[u32]) emu.run_with_args(&code, entry_off, args) } +/// The Xtensa builtins base image, or `None` with a loud note when it has not +/// been built. +/// +/// A gitignored cross-target artifact (`scripts/build-builtins-xt.sh`, esp +/// toolchain), so absence is a skip and not a failure — the same contract +/// `xt_builtins_image.rs` uses. +fn builtins_image() -> Option> { + let p = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../../lp-xt/fixtures/elf/lps-builtins-xt-app.elf"); + match std::fs::read(&p) { + Ok(b) => Some(b), + Err(_) => { + eprintln!( + "SKIP: {} not found — run scripts/build-builtins-xt.sh (esp toolchain) first", + p.display() + ); + None + } + } +} + +/// Compile in `FloatMode::F32`, link against the **builtins base image**, and +/// run with the FPU armed. +/// +/// [`run_f32`] links only the module's own functions and loads the blob at the +/// code region's base. That is enough for everything M7 P3 emits inline, but a +/// `sym_call` into `__lp_lpir_*_f32` (M7 D4) needs the builtins image resident +/// *and* its symbols in the relocation map — and the image already occupies the +/// base of that region, so the shader has to move. +/// +/// The layout is `rt_emu::xt_image`'s: image `.text` first, shader code +/// 4-aligned after it, below the image's data segments. It is hand-rolled here +/// rather than calling `build_xt_image` because that sits behind the `emu` +/// feature, and the other tests in this file run at *default* features. +fn run_f32_with_builtins( + ir: &LpirModule, + sig: &LpsModuleSig, + entry_name: &str, + args: &[u32], + image: &[u8], +) -> RunOutcome { + let mut emu = Emulator::new(); + let elf = XtensaElf::parse(image).expect("builtins image parses as Xtensa ELF32"); + elf.load_into(&mut emu) + .expect("image loads into emulator memory"); + + // Executable segments are addressed through the I-bus view and data + // segments through the D-bus one (lp-xt/lps-builtins-xt-app/link.ld), so + // the split is read off the image rather than hardcoded. + let ibus_base = emu.profile.code_ibus_base(); + let alias = emu.profile.alias; + let mut text_end = ibus_base; + let mut data_start = u32::MAX; + for seg in elf.segments().expect("image segments decode") { + let end = seg.vaddr + seg.memsz; + if seg.vaddr >= ibus_base { + text_end = text_end.max(end); + } else { + data_start = data_start.min(alias.dbus_to_ibus(seg.vaddr)); + } + } + let shader_base = text_end.next_multiple_of(4); + + let opts = NativeCompileOptions { + float_mode: FloatMode::F32, + fuel: false, + ..Default::default() + }; + let module = compile_module(ir, sig, FloatMode::F32, opts, IsaTarget::Xtensa) + .expect("xt f32 compile should succeed"); + + // Arming preamble, then the entry function, then everything else. The + // preamble is a multiple of 4 bytes long, so the entry's ENTRY is the next + // instruction executed and no jump is needed. + let funcs: Vec<_> = module.functions.iter().collect(); + let entry_idx = funcs + .iter() + .position(|f| f.name == entry_name) + .expect("entry function exists"); + let mut order: Vec = (0..funcs.len()).collect(); + order.sort_by_key(|&i| i != entry_idx); + + // Execution starts at the preamble, not at the entry function — entering at + // the entry skips the arming and the first FP instruction takes EXCCAUSE 32. + let mut code = arm_fpu_preamble(); + let entry = shader_base; + let mut symbols: std::collections::BTreeMap = elf.symbols().into_iter().collect(); + let mut func_addrs = vec![0u32; funcs.len()]; + for &i in &order { + let at = shader_base + code.len() as u32; + func_addrs[i] = at; + symbols.insert(funcs[i].name.clone(), at); + code.extend_from_slice(&funcs[i].code); + while code.len() % 4 != 0 { + code.push(0); + } + } + assert!( + shader_base + code.len() as u32 <= data_start, + "shader code overruns the builtins image's data segments" + ); + + for (fi, f) in funcs.iter().enumerate() { + for reloc in &f.relocs { + assert_eq!( + reloc.r_type, + IsaTarget::Xtensa.call_reloc_type(), + "unexpected reloc type" + ); + let target = *symbols.get(&reloc.symbol).unwrap_or_else(|| { + panic!( + "unresolved symbol {} — not a builtin in the base image nor a \ + function in this module", + reloc.symbol + ) + }); + let slot = (func_addrs[fi] - shader_base) as usize + reloc.offset; + code[slot..slot + 4].copy_from_slice(&target.to_le_bytes()); + } + } + + emu.mem.load_bytes(shader_base, &code); + let mut tracer = lp_xt_emu::NoopTracer; + match emu.run_loaded_with_args(entry, args, &mut tracer, None) { + CallOutcome::Ok { lo, .. } => RunOutcome::Ok(lo), + CallOutcome::Trap(t) => RunOutcome::Trap(t), + } +} + fn expect_ok(out: RunOutcome) -> u32 { match out { RunOutcome::Ok(v) => v, @@ -217,10 +351,7 @@ fn run_float_binop( build(&mut fb, x, y, out); fb.push_return(&[out]); let ir = one_function_module(fb.finish()); - let sig = one_function_sig( - vec![float_param("a"), float_param("b")], - LpsType::Float, - ); + let sig = one_function_sig(vec![float_param("a"), float_param("b")], LpsType::Float); expect_ok(run_f32(&ir, &sig, "f", &[0, bits(a), bits(b)])) } @@ -437,7 +568,11 @@ fn itof_signed_and_unsigned_through_the_pipeline() { let ir = one_function_module(fb.finish()); let sig = one_function_sig(vec![int_param("x")], LpsType::Float); let got = expect_ok(run_f32(&ir, &sig, "f", &[0, arg])); - assert_eq!(f32::from_bits(got), want, "itof signed={signed} of {arg:#x}"); + assert_eq!( + f32::from_bits(got), + want, + "itof signed={signed} of {arg:#x}" + ); } } @@ -789,13 +924,7 @@ fn float_recursion_at_depth_100_preserves_every_live_value() { imports: vec![], functions: VecMap::from([(FuncId(0), rec), (FuncId(1), f)]), }; - let params = || { - vec![ - int_param("n"), - float_param("a"), - float_param("b"), - ] - }; + let params = || vec![int_param("n"), float_param("a"), float_param("b")]; let sig = LpsModuleSig { functions: vec![ LpsFnSig { @@ -1018,30 +1147,42 @@ fn unarmed_float_code_faults_with_a_coprocessor_trap() { // Builtin routing (M7 D4) // --------------------------------------------------------------------------- -/// **Blocked on an upstream toolchain limitation** — see -/// `docs/defects/2026-08-01-xtensa-backend-cannot-select-float-constant-pool.md`. -/// /// M7 D4 routes the non-inlinable float operations (divide, sqrt, the rounding /// family, min/max, float→int, every transcendental) to M5's `_f32` builtins /// via `sym_call`. Resolving those on the host emulation path needs -/// `lp-xt/lps-builtins-xt-app` built with `float-f32` — and that build fails in -/// the esp Rust backend, which cannot select a float constant pool, at any -/// optimization level. The image therefore carries none of the f32 family yet. +/// `lp-xt/lps-builtins-xt-app` built with `float-f32`, which +/// `scripts/build-builtins-xt.sh` now does unconditionally. +/// +/// This was `#[ignore]`d while that build failed in the esp Rust backend, +/// which cannot select a float constant pool +/// (`docs/defects/2026-08-01-xtensa-backend-cannot-select-float-constant-pool.md` +/// — still open upstream, worked around in our source). /// -/// The wiring is done (`Cargo.toml`, and `scripts/build-builtins-xt.sh` behind -/// `LP_XT_BUILTINS_F32=1`); only the build is blocked. Un-ignore this, and -/// make the feature that script's default, when the defect is resolved. /// `ffloor` is deliberately chosen over `fdiv`/`fsqrt`: it reaches no /// `div0.s`/`const.s` estimate helper, so it does not additionally depend on /// M6-P6's unresolved policy fields. #[test] -#[ignore = "blocked: lps-builtins/float-f32 does not compile for Xtensa (see docs/defects/2026-08-01-xtensa-backend-cannot-select-float-constant-pool.md)"] fn a_builtin_routed_float_op_resolves_and_runs() { - let r = run_float_binop( - |fb, x, _y, out| fb.push(LpirOp::Ffloor { dst: out, src: x }), - 3.75, - 0.0, - ); + let Some(image) = builtins_image() else { + return; + }; + + let mut fb = FunctionBuilder::new("f", &[IrType::F32, IrType::F32]); + let x = fb.add_param(IrType::F32); + let _y = fb.add_param(IrType::F32); + let out = fb.alloc_vreg(IrType::F32); + fb.push(LpirOp::Ffloor { dst: out, src: x }); + fb.push_return(&[out]); + let ir = one_function_module(fb.finish()); + let sig = one_function_sig(vec![float_param("a"), float_param("b")], LpsType::Float); + + let r = expect_ok(run_f32_with_builtins( + &ir, + &sig, + "f", + &[0, bits(3.75), bits(0.0)], + &image, + )); assert_eq!(f32::from_bits(r), 3.0); } diff --git a/scripts/build-builtins-xt.sh b/scripts/build-builtins-xt.sh index df43f7825..dbea2edc7 100755 --- a/scripts/build-builtins-xt.sh +++ b/scripts/build-builtins-xt.sh @@ -42,28 +42,22 @@ export CARGO_TARGET_DIR="$PWD/target" # and staleness is not harmless here — on esp Rust 1.88 `lpc-model` genuinely # fails to compile (70x E0716 from the Slotted derive's const-promotion of a # temporary), which is what blocked the firmware crates. Fail loudly instead. -# The native-f32 builtin family is **not** built in by default, and that is a -# blocker rather than a preference: `lps-builtins/float-f32` does not currently -# compile for Xtensa at all. The esp Rust backend fails with +# The native-f32 builtin family is built in unconditionally, matching +# scripts/build-builtins.sh (rv32). It was briefly opt-in behind +# LP_XT_BUILTINS_F32 because `lps-builtins/float-f32` did not compile for +# Xtensa at all: # rustc-LLVM ERROR: Cannot select: XtensaISD::PCREL_WRAPPER # TargetConstantPool <[2 x float] [0.0, -1.0]> -# at every optimization level, on a `[f32; 2]` gradient table in the -# generative-noise builtins. Full record and candidate fixes: +# The backend limitation is real and still open, but our source no longer +# forms a float constant pool — see the workaround (and its bit-equivalence +# test) in lps-builtins' rgb2hsv_f32.rs, and: # docs/defects/2026-08-01-xtensa-backend-cannot-select-float-constant-pool.md # -# Passing the feature unconditionally would take the *whole* Xtensa filetest -# suite down with it (`scripts/filetests.sh` builds this image before running -# xtn/xtlpn), so it is opt-in until the defect is resolved. The crate-level -# feature is already wired (`lps-builtins-xt-app/Cargo.toml`), so closing this -# is one environment variable and then one default flip. -F32_REQUESTED="${LP_XT_BUILTINS_F32:-0}" -if [[ "$F32_REQUESTED" == "1" ]]; then - BUILD_CMD=(cargo build --release -p lps-builtins-xt-app --features float-f32) -else - BUILD_CMD=(cargo build --release -p lps-builtins-xt-app) -fi - -if ! "${BUILD_CMD[@]}"; then +# This is load-bearing beyond the f32 path: scripts/filetests.sh builds this +# image before running the xtn/xtlpn targets, so a compile failure here takes +# the whole Xtensa filetest suite down with it. If a new `[f32; N]` table ever +# re-trips the backend, that defect file is the place to start. +if ! cargo build --release -p lps-builtins-xt-app --features float-f32; then echo >&2 echo "error: build failed. If that was an MSRV error, the esp toolchain is stale:" >&2 echo " installed: $(rustc +esp --version 2>/dev/null || echo unknown)" >&2 @@ -85,11 +79,10 @@ fi # The f32 family is what M7's hardware-float lowering calls for everything it # does not inline — divide, sqrt, the rounding family, min/max, the saturating # conversions and every transcendental (M7 D4). An image without those symbols -# cannot resolve any of it, so the count is reported either way and asserted -# when the family was actually requested (mirroring the rv32 script, where a -# missing family fails 800+ filetests with one opaque message). +# cannot resolve any of it, so assert the family is present (mirroring the rv32 +# script, where a missing family fails 800+ filetests with one opaque message). F32_COUNT="$("$NM" "$OUT" | grep -c '_f32$' || true)" -if [[ "$F32_REQUESTED" == "1" && "$F32_COUNT" -eq 0 ]]; then +if [[ "$F32_COUNT" -eq 0 ]]; then echo "error: $OUT contains no native-f32 builtins despite --features float-f32" >&2 exit 1 fi From af62f8f5e190853a54807db8f86374b0898cfda4 Mon Sep 17 00:00:00 2001 From: Yona Appletree Date: Fri, 31 Jul 2026 23:50:25 -0700 Subject: [PATCH 3/4] test(xt): note the two link paths and assert the preamble's arg constraint Co-Authored-By: Claude Fable 5 --- .../src/glsl_builtin_mapping.rs | 80 ++++--------------- lp-shader/lps-builtin-ids/src/lib.rs | 2 +- .../lps-builtins-emu-app/src/builtin_refs.rs | 26 +++--- lp-shader/lps-builtins/src/builtin_refs.rs | 26 +++--- .../src/builtins/lpir/float_misc_q32.rs | 6 +- .../src/builtins/lpir/ftoi_sat_q32.rs | 6 +- .../src/generated_builtin_abi.rs | 2 +- .../lpvm-native/tests/xt_pipeline_f32.rs | 12 +++ 8 files changed, 64 insertions(+), 96 deletions(-) diff --git a/lp-shader/lps-builtin-ids/src/glsl_builtin_mapping.rs b/lp-shader/lps-builtin-ids/src/glsl_builtin_mapping.rs index 9967b1ade..69951f07b 100644 --- a/lp-shader/lps-builtin-ids/src/glsl_builtin_mapping.rs +++ b/lp-shader/lps-builtin-ids/src/glsl_builtin_mapping.rs @@ -132,12 +132,7 @@ pub fn glsl_lpfn_q32_builtin_id(name: &str, params: &[GlslParamKind]) -> Option< } ( "lpfn_fbm", - &[ - GlslParamKind::Vec3, - GlslParamKind::Float, - GlslParamKind::Int, - GlslParamKind::UInt, - ], + &[GlslParamKind::Vec3, GlslParamKind::Float, GlslParamKind::Int, GlslParamKind::UInt], ) => Some(BuiltinId::LpLpfnFbm3TileQ32), ("lpfn_fbm", &[GlslParamKind::Vec3, GlslParamKind::Int, GlslParamKind::UInt]) => { Some(BuiltinId::LpLpfnFbm3Q32) @@ -148,14 +143,9 @@ pub fn glsl_lpfn_q32_builtin_id(name: &str, params: &[GlslParamKind]) -> Option< ("lpfn_gnoise", &[GlslParamKind::Vec2, GlslParamKind::UInt]) => { Some(BuiltinId::LpLpfnGnoise2Q32) } - ( - "lpfn_gnoise", - &[ - GlslParamKind::Vec3, - GlslParamKind::Float, - GlslParamKind::UInt, - ], - ) => Some(BuiltinId::LpLpfnGnoise3TileQ32), + ("lpfn_gnoise", &[GlslParamKind::Vec3, GlslParamKind::Float, GlslParamKind::UInt]) => { + Some(BuiltinId::LpLpfnGnoise3TileQ32) + } ("lpfn_gnoise", &[GlslParamKind::Vec3, GlslParamKind::UInt]) => { Some(BuiltinId::LpLpfnGnoise3Q32) } @@ -167,23 +157,11 @@ pub fn glsl_lpfn_q32_builtin_id(name: &str, params: &[GlslParamKind]) -> Option< ("lpfn_hue2rgb", &[GlslParamKind::Float]) => Some(BuiltinId::LpLpfnHue2rgbQ32), ( "lpfn_psrdnoise", - &[ - GlslParamKind::Vec2, - GlslParamKind::Vec2, - GlslParamKind::Float, - GlslParamKind::Vec2, - GlslParamKind::UInt, - ], + &[GlslParamKind::Vec2, GlslParamKind::Vec2, GlslParamKind::Float, GlslParamKind::Vec2, GlslParamKind::UInt], ) => Some(BuiltinId::LpLpfnPsrdnoise2Q32), ( "lpfn_psrdnoise", - &[ - GlslParamKind::Vec3, - GlslParamKind::Vec3, - GlslParamKind::Float, - GlslParamKind::Vec3, - GlslParamKind::UInt, - ], + &[GlslParamKind::Vec3, GlslParamKind::Vec3, GlslParamKind::Float, GlslParamKind::Vec3, GlslParamKind::UInt], ) => Some(BuiltinId::LpLpfnPsrdnoise3Q32), ("lpfn_random", &[GlslParamKind::Float, GlslParamKind::UInt]) => { Some(BuiltinId::LpLpfnRandom1Q32) @@ -219,11 +197,7 @@ pub fn glsl_lpfn_q32_builtin_id(name: &str, params: &[GlslParamKind]) -> Option< } ( "lpfn_srandom3_tile", - &[ - GlslParamKind::Vec3, - GlslParamKind::Float, - GlslParamKind::UInt, - ], + &[GlslParamKind::Vec3, GlslParamKind::Float, GlslParamKind::UInt], ) => Some(BuiltinId::LpLpfnSrandom3TileQ32), ("lpfn_srandom3_vec", &[GlslParamKind::Vec3, GlslParamKind::UInt]) => { Some(BuiltinId::LpLpfnSrandom3VecQ32) @@ -333,12 +307,7 @@ pub fn glsl_lpfn_f32_builtin_id(name: &str, params: &[GlslParamKind]) -> Option< } ( "lpfn_fbm", - &[ - GlslParamKind::Vec3, - GlslParamKind::Float, - GlslParamKind::Int, - GlslParamKind::UInt, - ], + &[GlslParamKind::Vec3, GlslParamKind::Float, GlslParamKind::Int, GlslParamKind::UInt], ) => Some(BuiltinId::LpLpfnFbm3TileF32), ("lpfn_fbm", &[GlslParamKind::Vec3, GlslParamKind::Int, GlslParamKind::UInt]) => { Some(BuiltinId::LpLpfnFbm3F32) @@ -349,14 +318,9 @@ pub fn glsl_lpfn_f32_builtin_id(name: &str, params: &[GlslParamKind]) -> Option< ("lpfn_gnoise", &[GlslParamKind::Vec2, GlslParamKind::UInt]) => { Some(BuiltinId::LpLpfnGnoise2F32) } - ( - "lpfn_gnoise", - &[ - GlslParamKind::Vec3, - GlslParamKind::Float, - GlslParamKind::UInt, - ], - ) => Some(BuiltinId::LpLpfnGnoise3TileF32), + ("lpfn_gnoise", &[GlslParamKind::Vec3, GlslParamKind::Float, GlslParamKind::UInt]) => { + Some(BuiltinId::LpLpfnGnoise3TileF32) + } ("lpfn_gnoise", &[GlslParamKind::Vec3, GlslParamKind::UInt]) => { Some(BuiltinId::LpLpfnGnoise3F32) } @@ -368,23 +332,11 @@ pub fn glsl_lpfn_f32_builtin_id(name: &str, params: &[GlslParamKind]) -> Option< ("lpfn_hue2rgb", &[GlslParamKind::Float]) => Some(BuiltinId::LpLpfnHue2rgbF32), ( "lpfn_psrdnoise", - &[ - GlslParamKind::Vec2, - GlslParamKind::Vec2, - GlslParamKind::Float, - GlslParamKind::Vec2, - GlslParamKind::UInt, - ], + &[GlslParamKind::Vec2, GlslParamKind::Vec2, GlslParamKind::Float, GlslParamKind::Vec2, GlslParamKind::UInt], ) => Some(BuiltinId::LpLpfnPsrdnoise2F32), ( "lpfn_psrdnoise", - &[ - GlslParamKind::Vec3, - GlslParamKind::Vec3, - GlslParamKind::Float, - GlslParamKind::Vec3, - GlslParamKind::UInt, - ], + &[GlslParamKind::Vec3, GlslParamKind::Vec3, GlslParamKind::Float, GlslParamKind::Vec3, GlslParamKind::UInt], ) => Some(BuiltinId::LpLpfnPsrdnoise3F32), ("lpfn_random", &[GlslParamKind::Float, GlslParamKind::UInt]) => { Some(BuiltinId::LpLpfnRandom1F32) @@ -420,11 +372,7 @@ pub fn glsl_lpfn_f32_builtin_id(name: &str, params: &[GlslParamKind]) -> Option< } ( "lpfn_srandom3_tile", - &[ - GlslParamKind::Vec3, - GlslParamKind::Float, - GlslParamKind::UInt, - ], + &[GlslParamKind::Vec3, GlslParamKind::Float, GlslParamKind::UInt], ) => Some(BuiltinId::LpLpfnSrandom3TileF32), ("lpfn_srandom3_vec", &[GlslParamKind::Vec3, GlslParamKind::UInt]) => { Some(BuiltinId::LpLpfnSrandom3VecF32) diff --git a/lp-shader/lps-builtin-ids/src/lib.rs b/lp-shader/lps-builtin-ids/src/lib.rs index ab2f0acc1..2cdadcf6f 100644 --- a/lp-shader/lps-builtin-ids/src/lib.rs +++ b/lp-shader/lps-builtin-ids/src/lib.rs @@ -1396,7 +1396,6 @@ pub enum Mode { mod glsl_builtin_mapping; -pub use glsl_builtin_mapping::GlslParamKind; pub use glsl_builtin_mapping::glsl_f32_math_builtin_id; pub use glsl_builtin_mapping::glsl_lpfn_builtin_id; pub use glsl_builtin_mapping::glsl_lpfn_f32_builtin_id; @@ -1412,3 +1411,4 @@ pub use glsl_builtin_mapping::texture_q32_builtin_id; pub use glsl_builtin_mapping::vm_builtin_id; pub use glsl_builtin_mapping::vm_f32_builtin_id; pub use glsl_builtin_mapping::vm_q32_builtin_id; +pub use glsl_builtin_mapping::GlslParamKind; diff --git a/lp-shader/lps-builtins-emu-app/src/builtin_refs.rs b/lp-shader/lps-builtins-emu-app/src/builtin_refs.rs index 0b2e31be5..ac9b12b39 100644 --- a/lp-shader/lps-builtins-emu-app/src/builtin_refs.rs +++ b/lp-shader/lps-builtins-emu-app/src/builtin_refs.rs @@ -19,12 +19,12 @@ use lps_builtins::builtins::glsl::asin_q32::__lps_asin_q32; use lps_builtins::builtins::glsl::asinh_f32::__lps_asinh_f32; use lps_builtins::builtins::glsl::asinh_q32::__lps_asinh_q32; #[cfg(feature = "float-f32")] -use lps_builtins::builtins::glsl::atan_f32::__lps_atan_f32; -use lps_builtins::builtins::glsl::atan_q32::__lps_atan_q32; -#[cfg(feature = "float-f32")] use lps_builtins::builtins::glsl::atan2_f32::__lps_atan2_f32; use lps_builtins::builtins::glsl::atan2_q32::__lps_atan2_q32; #[cfg(feature = "float-f32")] +use lps_builtins::builtins::glsl::atan_f32::__lps_atan_f32; +use lps_builtins::builtins::glsl::atan_q32::__lps_atan_q32; +#[cfg(feature = "float-f32")] use lps_builtins::builtins::glsl::atanh_f32::__lps_atanh_f32; use lps_builtins::builtins::glsl::atanh_q32::__lps_atanh_q32; #[cfg(feature = "float-f32")] @@ -34,12 +34,12 @@ use lps_builtins::builtins::glsl::cos_q32::__lps_cos_q32; use lps_builtins::builtins::glsl::cosh_f32::__lps_cosh_f32; use lps_builtins::builtins::glsl::cosh_q32::__lps_cosh_q32; #[cfg(feature = "float-f32")] -use lps_builtins::builtins::glsl::exp_f32::__lps_exp_f32; -use lps_builtins::builtins::glsl::exp_q32::__lps_exp_q32; -#[cfg(feature = "float-f32")] use lps_builtins::builtins::glsl::exp2_f32::__lps_exp2_f32; use lps_builtins::builtins::glsl::exp2_q32::__lps_exp2_q32; #[cfg(feature = "float-f32")] +use lps_builtins::builtins::glsl::exp_f32::__lps_exp_f32; +use lps_builtins::builtins::glsl::exp_q32::__lps_exp_q32; +#[cfg(feature = "float-f32")] use lps_builtins::builtins::glsl::fma_f32::__lps_fma_f32; use lps_builtins::builtins::glsl::fma_q32::__lps_fma_q32; #[cfg(feature = "float-f32")] @@ -49,12 +49,12 @@ use lps_builtins::builtins::glsl::inversesqrt_q32::__lps_inversesqrt_q32; use lps_builtins::builtins::glsl::ldexp_f32::__lps_ldexp_f32; use lps_builtins::builtins::glsl::ldexp_q32::__lps_ldexp_q32; #[cfg(feature = "float-f32")] -use lps_builtins::builtins::glsl::log_f32::__lps_log_f32; -use lps_builtins::builtins::glsl::log_q32::__lps_log_q32; -#[cfg(feature = "float-f32")] use lps_builtins::builtins::glsl::log2_f32::__lps_log2_f32; use lps_builtins::builtins::glsl::log2_q32::__lps_log2_q32; #[cfg(feature = "float-f32")] +use lps_builtins::builtins::glsl::log_f32::__lps_log_f32; +use lps_builtins::builtins::glsl::log_q32::__lps_log_q32; +#[cfg(feature = "float-f32")] use lps_builtins::builtins::glsl::mod_f32::__lps_mod_f32; use lps_builtins::builtins::glsl::mod_q32::__lps_mod_q32; #[cfg(feature = "float-f32")] @@ -224,12 +224,12 @@ use lps_builtins::builtins::lpir::itof_u_f32::__lp_lpir_itof_u_f32; use lps_builtins::builtins::lpir::itof_u_q32::__lp_lpir_itof_u_q32; #[cfg(feature = "float-f32")] use lps_builtins::builtins::lpir::unorm_conv_f32::{ - __lp_lpir_fto_unorm8_f32, __lp_lpir_fto_unorm16_f32, __lp_lpir_unorm8_to_f_f32, - __lp_lpir_unorm16_to_f_f32, + __lp_lpir_fto_unorm16_f32, __lp_lpir_fto_unorm8_f32, __lp_lpir_unorm16_to_f_f32, + __lp_lpir_unorm8_to_f_f32, }; use lps_builtins::builtins::lpir::unorm_conv_q32::{ - __lp_lpir_fto_unorm8_q32, __lp_lpir_fto_unorm16_q32, __lp_lpir_unorm8_to_f_q32, - __lp_lpir_unorm16_to_f_q32, + __lp_lpir_fto_unorm16_q32, __lp_lpir_fto_unorm8_q32, __lp_lpir_unorm16_to_f_q32, + __lp_lpir_unorm8_to_f_q32, }; #[cfg(feature = "float-f32")] use lps_builtins::builtins::texture::{ diff --git a/lp-shader/lps-builtins/src/builtin_refs.rs b/lp-shader/lps-builtins/src/builtin_refs.rs index 6587337f3..d136572c4 100644 --- a/lp-shader/lps-builtins/src/builtin_refs.rs +++ b/lp-shader/lps-builtins/src/builtin_refs.rs @@ -19,12 +19,12 @@ use crate::builtins::glsl::asin_q32::__lps_asin_q32; use crate::builtins::glsl::asinh_f32::__lps_asinh_f32; use crate::builtins::glsl::asinh_q32::__lps_asinh_q32; #[cfg(feature = "float-f32")] -use crate::builtins::glsl::atan_f32::__lps_atan_f32; -use crate::builtins::glsl::atan_q32::__lps_atan_q32; -#[cfg(feature = "float-f32")] use crate::builtins::glsl::atan2_f32::__lps_atan2_f32; use crate::builtins::glsl::atan2_q32::__lps_atan2_q32; #[cfg(feature = "float-f32")] +use crate::builtins::glsl::atan_f32::__lps_atan_f32; +use crate::builtins::glsl::atan_q32::__lps_atan_q32; +#[cfg(feature = "float-f32")] use crate::builtins::glsl::atanh_f32::__lps_atanh_f32; use crate::builtins::glsl::atanh_q32::__lps_atanh_q32; #[cfg(feature = "float-f32")] @@ -34,12 +34,12 @@ use crate::builtins::glsl::cos_q32::__lps_cos_q32; use crate::builtins::glsl::cosh_f32::__lps_cosh_f32; use crate::builtins::glsl::cosh_q32::__lps_cosh_q32; #[cfg(feature = "float-f32")] -use crate::builtins::glsl::exp_f32::__lps_exp_f32; -use crate::builtins::glsl::exp_q32::__lps_exp_q32; -#[cfg(feature = "float-f32")] use crate::builtins::glsl::exp2_f32::__lps_exp2_f32; use crate::builtins::glsl::exp2_q32::__lps_exp2_q32; #[cfg(feature = "float-f32")] +use crate::builtins::glsl::exp_f32::__lps_exp_f32; +use crate::builtins::glsl::exp_q32::__lps_exp_q32; +#[cfg(feature = "float-f32")] use crate::builtins::glsl::fma_f32::__lps_fma_f32; use crate::builtins::glsl::fma_q32::__lps_fma_q32; #[cfg(feature = "float-f32")] @@ -49,12 +49,12 @@ use crate::builtins::glsl::inversesqrt_q32::__lps_inversesqrt_q32; use crate::builtins::glsl::ldexp_f32::__lps_ldexp_f32; use crate::builtins::glsl::ldexp_q32::__lps_ldexp_q32; #[cfg(feature = "float-f32")] -use crate::builtins::glsl::log_f32::__lps_log_f32; -use crate::builtins::glsl::log_q32::__lps_log_q32; -#[cfg(feature = "float-f32")] use crate::builtins::glsl::log2_f32::__lps_log2_f32; use crate::builtins::glsl::log2_q32::__lps_log2_q32; #[cfg(feature = "float-f32")] +use crate::builtins::glsl::log_f32::__lps_log_f32; +use crate::builtins::glsl::log_q32::__lps_log_q32; +#[cfg(feature = "float-f32")] use crate::builtins::glsl::mod_f32::__lps_mod_f32; use crate::builtins::glsl::mod_q32::__lps_mod_q32; #[cfg(feature = "float-f32")] @@ -220,12 +220,12 @@ use crate::builtins::lpir::itof_u_f32::__lp_lpir_itof_u_f32; use crate::builtins::lpir::itof_u_q32::__lp_lpir_itof_u_q32; #[cfg(feature = "float-f32")] use crate::builtins::lpir::unorm_conv_f32::{ - __lp_lpir_fto_unorm8_f32, __lp_lpir_fto_unorm16_f32, __lp_lpir_unorm8_to_f_f32, - __lp_lpir_unorm16_to_f_f32, + __lp_lpir_fto_unorm16_f32, __lp_lpir_fto_unorm8_f32, __lp_lpir_unorm16_to_f_f32, + __lp_lpir_unorm8_to_f_f32, }; use crate::builtins::lpir::unorm_conv_q32::{ - __lp_lpir_fto_unorm8_q32, __lp_lpir_fto_unorm16_q32, __lp_lpir_unorm8_to_f_q32, - __lp_lpir_unorm16_to_f_q32, + __lp_lpir_fto_unorm16_q32, __lp_lpir_fto_unorm8_q32, __lp_lpir_unorm16_to_f_q32, + __lp_lpir_unorm8_to_f_q32, }; #[cfg(feature = "float-f32")] use crate::builtins::texture::{ diff --git a/lp-shader/lps-builtins/src/builtins/lpir/float_misc_q32.rs b/lp-shader/lps-builtins/src/builtins/lpir/float_misc_q32.rs index fb376ceea..9888694bb 100644 --- a/lp-shader/lps-builtins/src/builtins/lpir/float_misc_q32.rs +++ b/lp-shader/lps-builtins/src/builtins/lpir/float_misc_q32.rs @@ -11,7 +11,11 @@ #[unsafe(no_mangle)] pub extern "C" fn __lp_lpir_fabs_q32(v: i32) -> i32 { - if v < 0 { v.wrapping_neg() } else { v } + if v < 0 { + v.wrapping_neg() + } else { + v + } } #[unsafe(no_mangle)] diff --git a/lp-shader/lps-builtins/src/builtins/lpir/ftoi_sat_q32.rs b/lp-shader/lps-builtins/src/builtins/lpir/ftoi_sat_q32.rs index 00ee824f3..248f34a92 100644 --- a/lp-shader/lps-builtins/src/builtins/lpir/ftoi_sat_q32.rs +++ b/lp-shader/lps-builtins/src/builtins/lpir/ftoi_sat_q32.rs @@ -11,5 +11,9 @@ pub extern "C" fn __lp_lpir_ftoi_sat_s_q32(v: i32) -> i32 { #[unsafe(no_mangle)] pub extern "C" fn __lp_lpir_ftoi_sat_u_q32(v: i32) -> i32 { let t = __lp_lpir_ftoi_sat_s_q32(v); - if t < 0 { 0 } else { t } + if t < 0 { + 0 + } else { + t + } } diff --git a/lp-shader/lpvm-cranelift/src/generated_builtin_abi.rs b/lp-shader/lpvm-cranelift/src/generated_builtin_abi.rs index 0db5c8e4e..4010a542b 100644 --- a/lp-shader/lpvm-cranelift/src/generated_builtin_abi.rs +++ b/lp-shader/lpvm-cranelift/src/generated_builtin_abi.rs @@ -12,7 +12,7 @@ //! Changing an `extern "C"` builtin in `lps-builtins` without re-running codegen will desync //! this file and fail `cargo check` until you regenerate. -use cranelift_codegen::ir::{AbiParam, Signature, types}; +use cranelift_codegen::ir::{types, AbiParam, Signature}; use cranelift_codegen::isa::CallConv; use lps_builtin_ids::BuiltinId; diff --git a/lp-shader/lpvm-native/tests/xt_pipeline_f32.rs b/lp-shader/lpvm-native/tests/xt_pipeline_f32.rs index 26a3635ff..ec89a1c6d 100644 --- a/lp-shader/lpvm-native/tests/xt_pipeline_f32.rs +++ b/lp-shader/lpvm-native/tests/xt_pipeline_f32.rs @@ -32,6 +32,14 @@ //! two-instruction preamble that does what P5's board init will do. //! `unarmed_float_code_faults_with_a_coprocessor_trap` is the negative //! control: the same code without the preamble must take EXCCAUSE 32. +//! +//! # Two link paths +//! +//! Most tests here link only the module's own functions ([`link_blob`]) — +//! everything M7 P3 emits inline needs nothing else. The one case that calls a +//! routed builtin ([`run_f32_with_builtins`]) links against the Xtensa builtins +//! base image instead, and skips loudly when that cross-target artifact has not +//! been built. use lp_collection::VecMap; use lpir::builder::FunctionBuilder; @@ -197,6 +205,10 @@ fn run_f32_with_builtins( args: &[u32], image: &[u8], ) -> RunOutcome { + assert!( + args.len() <= 5, + "the arming preamble clobbers the sixth argument register" + ); let mut emu = Emulator::new(); let elf = XtensaElf::parse(image).expect("builtins image parses as Xtensa ELF32"); elf.load_into(&mut emu) From a1772b80d2b0f0a611cdae26174cd54321abffa5 Mon Sep 17 00:00:00 2001 From: Yona Appletree Date: Sat, 1 Aug 2026 00:07:01 -0700 Subject: [PATCH 4/4] =?UTF-8?q?fix(xt):=20keep=20float-f32=20opt-in=20?= =?UTF-8?q?=E2=80=94=20it=20no=20longer=20compiles-fails,=20it=20no=20long?= =?UTF-8?q?er=20fits?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reverts the gate flip from the previous commit, on measurement. Making --features float-f32 the default took the xtn.q32 filetest suite from 849/849 files to 522/849, and the failures are link failures, not wrong answers. Cause is capacity, not codegen. link.ld gives .text 112 KiB (114,688 B) of the emulator's 128 KiB code region. Q32-only .text is 66,300 B; with float-f32 it is 113,757 B — the image links with 931 bytes to spare, and rt_emu::xt_image places compiled shader code in exactly that gap. Small shaders fit, everything else fails to link. That is a second, independent defect from the constant-pool one, and fixing the first is what exposed it: filed as docs/defects/2026-08-01-xt-f32-builtins-exhaust-the-emulator-code-region.md with the measurements and three candidate resolutions. So the family stays behind LP_XT_BUILTINS_F32=1, and the un-ignored builtin-routing test skips loudly when the resident image lacks the f32 symbols rather than failing — one blocked capability should not redden a suite. Verified both ways: skips on the default image, and passes (ffloor(3.75) == 3.0 through the real __lp_lpir_ffloor_f32) on an image built with LP_XT_BUILTINS_F32=1. The constant-pool defect's record is corrected to say the workaround shipped and the build it blocked now succeeds, while noting the default flip is blocked elsewhere. Co-Authored-By: Claude Fable 5 --- ...iltins-exhaust-the-emulator-code-region.md | 81 +++++++++++++++++++ ...ckend-cannot-select-float-constant-pool.md | 17 +++- .../src/glsl_builtin_mapping.rs | 80 ++++++++++++++---- lp-shader/lps-builtin-ids/src/lib.rs | 2 +- .../lps-builtins-emu-app/src/builtin_refs.rs | 26 +++--- lp-shader/lps-builtins/src/builtin_refs.rs | 26 +++--- .../src/builtins/lpir/float_misc_q32.rs | 6 +- .../src/builtins/lpir/ftoi_sat_q32.rs | 6 +- .../src/generated_builtin_abi.rs | 2 +- .../lpvm-native/tests/xt_pipeline_f32.rs | 63 ++++++++++----- scripts/build-builtins-xt.sh | 47 +++++++---- 11 files changed, 263 insertions(+), 93 deletions(-) create mode 100644 docs/defects/2026-08-01-xt-f32-builtins-exhaust-the-emulator-code-region.md diff --git a/docs/defects/2026-08-01-xt-f32-builtins-exhaust-the-emulator-code-region.md b/docs/defects/2026-08-01-xt-f32-builtins-exhaust-the-emulator-code-region.md new file mode 100644 index 000000000..1f2df2c17 --- /dev/null +++ b/docs/defects/2026-08-01-xt-f32-builtins-exhaust-the-emulator-code-region.md @@ -0,0 +1,81 @@ +--- +status: open # blocks making float-f32 the Xtensa image's default +found: 2026-08-01 # how: filetests (xtn.q32 dropped 849/849 -> 522/849 on the gate flip) +area: lp-xt/lps-builtins-xt-app, lp-shader/lpvm-native (rt_emu/xt_image), lp-xt-emu +class: capacity +related: + - docs/defects/2026-08-01-xtensa-backend-cannot-select-float-constant-pool.md +--- +# With `float-f32` in, the Xtensa builtins image fills the emulator's code region and leaves no room for shader code + +**Symptom** — building the Xtensa builtins image with `--features float-f32` +succeeds, and then most of the Xtensa filetest suite fails to *link*: + +``` +xtn.q32 without float-f32: 6336/6336 tests, 849/849 files +xtn.q32 with float-f32: 2917/6336 tests, 522/849 files (327 files fail) +``` + +Failures report as `compile-fail`, not as wrong answers. Small shaders pass and +large ones fail, which is the tell. + +**Cause** — it is capacity, not codegen. `lp-xt/lps-builtins-xt-app/link.ld` +gives `.text` 112 KiB (114,688 B) of the emulator's 128 KiB code region, and +`.rodata`/`.data`/`.bss` the remaining 16 KiB (16,384 B). Measured with +`xtensa-esp32s3-elf-size -A`: + +| build | `.text` | free IRAM | `.rodata` | free DRAM | +| --- | --- | --- | --- | --- | +| default (Q32 only) | 66,300 B | 48,388 B | 12,176 B | 4,208 B | +| `--features float-f32` | 113,757 B | **931 B** | 16,156 B | **228 B** | + +`rt_emu::xt_image::build_xt_image` places compiled shader code 4-aligned after +the image's `.text` and requires it to end before the image's data segments — +i.e. in exactly that free-IRAM gap. At 931 bytes, only the smallest shaders +fit; everything else fails with + +``` +compiled shader code does not fit the Xtensa code region: N bytes of shader +after 113757 bytes of builtins ... +``` + +The image *links* because 113,757 < 114,688. It links with 0.8% headroom, so +the f32 family very nearly does not fit its own region either. + +## Consequence + +`scripts/build-builtins-xt.sh` cannot make `--features float-f32` its default: +`scripts/filetests.sh` builds this image before running `xtn.*`/`xtlpn.*`, so +the flip takes the whole Xtensa filetest suite down. The family stays behind +`LP_XT_BUILTINS_F32=1`, and `lpvm-native`'s +`a_builtin_routed_float_op_resolves_and_runs` skips loudly rather than failing +when the resident image lacks it. + +**This is a different defect from the constant-pool one it was found behind.** +That one was "the f32 family does not compile for Xtensa"; it is worked around +and the image now builds. This one is "the f32 family does not *fit* alongside +shader code". Fixing the first exposed the second. + +It is also a live question for **M7 P5**: `fw-esp32s3` is not bound by this +linker script or by the emulator's 128 KiB model, so the capacity limit is the +*host emulation* path's, not the device's. Worth confirming before assuming P5 +inherits it. + +## Candidate resolutions (not chosen here — needs a decision) + +1. **Widen the emulator's modeled code region.** 128 KiB is the host + emulator's model, not silicon; the S3 has far more IRAM. Touches + `lp-xt-emu`'s `BoardProfile` and both linker scripts, and `lp-xt-elf`'s + `DATA_BASE = CODE_DBUS_BASE + CODE_REGION_LEN/2` convention. +2. **Give the shader its own region** rather than the tail of the builtins + region, removing the coupling between builtins size and maximum shader size + entirely. Largest change, best end state. +3. **Split the f32 builtin feature** so only the arithmetic family M7 D4 + actually routes to (divide, sqrt, rounding, min/max, conversions, + transcendentals) is linked, leaving the generative-noise family — which is + most of the 47 KB — rv32/host-only. Cheapest, and the noise builtins are not + what M7's lowering calls. Changes a feature contract M5 owns, so it is + Yona's call. + +(3) is the smallest thing that unblocks M7's host path; (1) or (2) is what +makes the limit stop recurring. diff --git a/docs/defects/2026-08-01-xtensa-backend-cannot-select-float-constant-pool.md b/docs/defects/2026-08-01-xtensa-backend-cannot-select-float-constant-pool.md index 283d87790..04f7e5d8f 100644 --- a/docs/defects/2026-08-01-xtensa-backend-cannot-select-float-constant-pool.md +++ b/docs/defects/2026-08-01-xtensa-backend-cannot-select-float-constant-pool.md @@ -6,6 +6,7 @@ area: lp-xt/lps-builtins-xt-app, lp-shader/lps-builtins, esp Rust toolchain class: upstream-toolchain-limitation related: - docs/design/float.md + - docs/defects/2026-08-01-xt-f32-builtins-exhaust-the-emulator-code-region.md --- # The esp Xtensa backend cannot select a float constant pool, so `lps-builtins/float-f32` does not compile for Xtensa @@ -104,13 +105,21 @@ over a grid that crosses `g == b`, plus `-0.0` and NaN. These builtins are shared with rv32 and wasm, where the original compiles fine, so drift would be a silent behaviour change on targets that never had the problem. -`scripts/build-builtins-xt.sh` therefore passes `--features float-f32` -unconditionally again (the `LP_XT_BUILTINS_F32` opt-in was a P4 stopgap and is -gone), and `lp-shader/lpvm-native/tests/xt_pipeline_f32.rs`'s -`a_builtin_routed_float_op_resolves_and_runs` is no longer `#[ignore]`d — it +`LP_XT_BUILTINS_F32=1 scripts/build-builtins-xt.sh` now succeeds — the thing +this defect blocked — and +`lp-shader/lpvm-native/tests/xt_pipeline_f32.rs`'s +`a_builtin_routed_float_op_resolves_and_runs` is no longer `#[ignore]`d: it links the shader against the builtins base image and resolves `ffloor` to the real `__lp_lpir_ffloor_f32` on the M6 emulator. +**The feature is still not the default, for an unrelated reason.** Making it +unconditional was attempted and reverted on measurement: with the family in, +the image's `.text` is 113,757 B against link.ld's 112 KiB, leaving 931 bytes +of the code region for shader code, and the xtn.q32 filetest suite drops from +849/849 files to 522/849 on link failures. That is a separate defect — +`docs/defects/2026-08-01-xt-f32-builtins-exhaust-the-emulator-code-region.md` +— and fixing this one is what exposed it. + ## Why this stays open The backend limitation is untouched. A new `[f32; N]` constant aggregate diff --git a/lp-shader/lps-builtin-ids/src/glsl_builtin_mapping.rs b/lp-shader/lps-builtin-ids/src/glsl_builtin_mapping.rs index 69951f07b..9967b1ade 100644 --- a/lp-shader/lps-builtin-ids/src/glsl_builtin_mapping.rs +++ b/lp-shader/lps-builtin-ids/src/glsl_builtin_mapping.rs @@ -132,7 +132,12 @@ pub fn glsl_lpfn_q32_builtin_id(name: &str, params: &[GlslParamKind]) -> Option< } ( "lpfn_fbm", - &[GlslParamKind::Vec3, GlslParamKind::Float, GlslParamKind::Int, GlslParamKind::UInt], + &[ + GlslParamKind::Vec3, + GlslParamKind::Float, + GlslParamKind::Int, + GlslParamKind::UInt, + ], ) => Some(BuiltinId::LpLpfnFbm3TileQ32), ("lpfn_fbm", &[GlslParamKind::Vec3, GlslParamKind::Int, GlslParamKind::UInt]) => { Some(BuiltinId::LpLpfnFbm3Q32) @@ -143,9 +148,14 @@ pub fn glsl_lpfn_q32_builtin_id(name: &str, params: &[GlslParamKind]) -> Option< ("lpfn_gnoise", &[GlslParamKind::Vec2, GlslParamKind::UInt]) => { Some(BuiltinId::LpLpfnGnoise2Q32) } - ("lpfn_gnoise", &[GlslParamKind::Vec3, GlslParamKind::Float, GlslParamKind::UInt]) => { - Some(BuiltinId::LpLpfnGnoise3TileQ32) - } + ( + "lpfn_gnoise", + &[ + GlslParamKind::Vec3, + GlslParamKind::Float, + GlslParamKind::UInt, + ], + ) => Some(BuiltinId::LpLpfnGnoise3TileQ32), ("lpfn_gnoise", &[GlslParamKind::Vec3, GlslParamKind::UInt]) => { Some(BuiltinId::LpLpfnGnoise3Q32) } @@ -157,11 +167,23 @@ pub fn glsl_lpfn_q32_builtin_id(name: &str, params: &[GlslParamKind]) -> Option< ("lpfn_hue2rgb", &[GlslParamKind::Float]) => Some(BuiltinId::LpLpfnHue2rgbQ32), ( "lpfn_psrdnoise", - &[GlslParamKind::Vec2, GlslParamKind::Vec2, GlslParamKind::Float, GlslParamKind::Vec2, GlslParamKind::UInt], + &[ + GlslParamKind::Vec2, + GlslParamKind::Vec2, + GlslParamKind::Float, + GlslParamKind::Vec2, + GlslParamKind::UInt, + ], ) => Some(BuiltinId::LpLpfnPsrdnoise2Q32), ( "lpfn_psrdnoise", - &[GlslParamKind::Vec3, GlslParamKind::Vec3, GlslParamKind::Float, GlslParamKind::Vec3, GlslParamKind::UInt], + &[ + GlslParamKind::Vec3, + GlslParamKind::Vec3, + GlslParamKind::Float, + GlslParamKind::Vec3, + GlslParamKind::UInt, + ], ) => Some(BuiltinId::LpLpfnPsrdnoise3Q32), ("lpfn_random", &[GlslParamKind::Float, GlslParamKind::UInt]) => { Some(BuiltinId::LpLpfnRandom1Q32) @@ -197,7 +219,11 @@ pub fn glsl_lpfn_q32_builtin_id(name: &str, params: &[GlslParamKind]) -> Option< } ( "lpfn_srandom3_tile", - &[GlslParamKind::Vec3, GlslParamKind::Float, GlslParamKind::UInt], + &[ + GlslParamKind::Vec3, + GlslParamKind::Float, + GlslParamKind::UInt, + ], ) => Some(BuiltinId::LpLpfnSrandom3TileQ32), ("lpfn_srandom3_vec", &[GlslParamKind::Vec3, GlslParamKind::UInt]) => { Some(BuiltinId::LpLpfnSrandom3VecQ32) @@ -307,7 +333,12 @@ pub fn glsl_lpfn_f32_builtin_id(name: &str, params: &[GlslParamKind]) -> Option< } ( "lpfn_fbm", - &[GlslParamKind::Vec3, GlslParamKind::Float, GlslParamKind::Int, GlslParamKind::UInt], + &[ + GlslParamKind::Vec3, + GlslParamKind::Float, + GlslParamKind::Int, + GlslParamKind::UInt, + ], ) => Some(BuiltinId::LpLpfnFbm3TileF32), ("lpfn_fbm", &[GlslParamKind::Vec3, GlslParamKind::Int, GlslParamKind::UInt]) => { Some(BuiltinId::LpLpfnFbm3F32) @@ -318,9 +349,14 @@ pub fn glsl_lpfn_f32_builtin_id(name: &str, params: &[GlslParamKind]) -> Option< ("lpfn_gnoise", &[GlslParamKind::Vec2, GlslParamKind::UInt]) => { Some(BuiltinId::LpLpfnGnoise2F32) } - ("lpfn_gnoise", &[GlslParamKind::Vec3, GlslParamKind::Float, GlslParamKind::UInt]) => { - Some(BuiltinId::LpLpfnGnoise3TileF32) - } + ( + "lpfn_gnoise", + &[ + GlslParamKind::Vec3, + GlslParamKind::Float, + GlslParamKind::UInt, + ], + ) => Some(BuiltinId::LpLpfnGnoise3TileF32), ("lpfn_gnoise", &[GlslParamKind::Vec3, GlslParamKind::UInt]) => { Some(BuiltinId::LpLpfnGnoise3F32) } @@ -332,11 +368,23 @@ pub fn glsl_lpfn_f32_builtin_id(name: &str, params: &[GlslParamKind]) -> Option< ("lpfn_hue2rgb", &[GlslParamKind::Float]) => Some(BuiltinId::LpLpfnHue2rgbF32), ( "lpfn_psrdnoise", - &[GlslParamKind::Vec2, GlslParamKind::Vec2, GlslParamKind::Float, GlslParamKind::Vec2, GlslParamKind::UInt], + &[ + GlslParamKind::Vec2, + GlslParamKind::Vec2, + GlslParamKind::Float, + GlslParamKind::Vec2, + GlslParamKind::UInt, + ], ) => Some(BuiltinId::LpLpfnPsrdnoise2F32), ( "lpfn_psrdnoise", - &[GlslParamKind::Vec3, GlslParamKind::Vec3, GlslParamKind::Float, GlslParamKind::Vec3, GlslParamKind::UInt], + &[ + GlslParamKind::Vec3, + GlslParamKind::Vec3, + GlslParamKind::Float, + GlslParamKind::Vec3, + GlslParamKind::UInt, + ], ) => Some(BuiltinId::LpLpfnPsrdnoise3F32), ("lpfn_random", &[GlslParamKind::Float, GlslParamKind::UInt]) => { Some(BuiltinId::LpLpfnRandom1F32) @@ -372,7 +420,11 @@ pub fn glsl_lpfn_f32_builtin_id(name: &str, params: &[GlslParamKind]) -> Option< } ( "lpfn_srandom3_tile", - &[GlslParamKind::Vec3, GlslParamKind::Float, GlslParamKind::UInt], + &[ + GlslParamKind::Vec3, + GlslParamKind::Float, + GlslParamKind::UInt, + ], ) => Some(BuiltinId::LpLpfnSrandom3TileF32), ("lpfn_srandom3_vec", &[GlslParamKind::Vec3, GlslParamKind::UInt]) => { Some(BuiltinId::LpLpfnSrandom3VecF32) diff --git a/lp-shader/lps-builtin-ids/src/lib.rs b/lp-shader/lps-builtin-ids/src/lib.rs index 2cdadcf6f..ab2f0acc1 100644 --- a/lp-shader/lps-builtin-ids/src/lib.rs +++ b/lp-shader/lps-builtin-ids/src/lib.rs @@ -1396,6 +1396,7 @@ pub enum Mode { mod glsl_builtin_mapping; +pub use glsl_builtin_mapping::GlslParamKind; pub use glsl_builtin_mapping::glsl_f32_math_builtin_id; pub use glsl_builtin_mapping::glsl_lpfn_builtin_id; pub use glsl_builtin_mapping::glsl_lpfn_f32_builtin_id; @@ -1411,4 +1412,3 @@ pub use glsl_builtin_mapping::texture_q32_builtin_id; pub use glsl_builtin_mapping::vm_builtin_id; pub use glsl_builtin_mapping::vm_f32_builtin_id; pub use glsl_builtin_mapping::vm_q32_builtin_id; -pub use glsl_builtin_mapping::GlslParamKind; diff --git a/lp-shader/lps-builtins-emu-app/src/builtin_refs.rs b/lp-shader/lps-builtins-emu-app/src/builtin_refs.rs index ac9b12b39..0b2e31be5 100644 --- a/lp-shader/lps-builtins-emu-app/src/builtin_refs.rs +++ b/lp-shader/lps-builtins-emu-app/src/builtin_refs.rs @@ -19,12 +19,12 @@ use lps_builtins::builtins::glsl::asin_q32::__lps_asin_q32; use lps_builtins::builtins::glsl::asinh_f32::__lps_asinh_f32; use lps_builtins::builtins::glsl::asinh_q32::__lps_asinh_q32; #[cfg(feature = "float-f32")] -use lps_builtins::builtins::glsl::atan2_f32::__lps_atan2_f32; -use lps_builtins::builtins::glsl::atan2_q32::__lps_atan2_q32; -#[cfg(feature = "float-f32")] use lps_builtins::builtins::glsl::atan_f32::__lps_atan_f32; use lps_builtins::builtins::glsl::atan_q32::__lps_atan_q32; #[cfg(feature = "float-f32")] +use lps_builtins::builtins::glsl::atan2_f32::__lps_atan2_f32; +use lps_builtins::builtins::glsl::atan2_q32::__lps_atan2_q32; +#[cfg(feature = "float-f32")] use lps_builtins::builtins::glsl::atanh_f32::__lps_atanh_f32; use lps_builtins::builtins::glsl::atanh_q32::__lps_atanh_q32; #[cfg(feature = "float-f32")] @@ -34,12 +34,12 @@ use lps_builtins::builtins::glsl::cos_q32::__lps_cos_q32; use lps_builtins::builtins::glsl::cosh_f32::__lps_cosh_f32; use lps_builtins::builtins::glsl::cosh_q32::__lps_cosh_q32; #[cfg(feature = "float-f32")] -use lps_builtins::builtins::glsl::exp2_f32::__lps_exp2_f32; -use lps_builtins::builtins::glsl::exp2_q32::__lps_exp2_q32; -#[cfg(feature = "float-f32")] use lps_builtins::builtins::glsl::exp_f32::__lps_exp_f32; use lps_builtins::builtins::glsl::exp_q32::__lps_exp_q32; #[cfg(feature = "float-f32")] +use lps_builtins::builtins::glsl::exp2_f32::__lps_exp2_f32; +use lps_builtins::builtins::glsl::exp2_q32::__lps_exp2_q32; +#[cfg(feature = "float-f32")] use lps_builtins::builtins::glsl::fma_f32::__lps_fma_f32; use lps_builtins::builtins::glsl::fma_q32::__lps_fma_q32; #[cfg(feature = "float-f32")] @@ -49,12 +49,12 @@ use lps_builtins::builtins::glsl::inversesqrt_q32::__lps_inversesqrt_q32; use lps_builtins::builtins::glsl::ldexp_f32::__lps_ldexp_f32; use lps_builtins::builtins::glsl::ldexp_q32::__lps_ldexp_q32; #[cfg(feature = "float-f32")] -use lps_builtins::builtins::glsl::log2_f32::__lps_log2_f32; -use lps_builtins::builtins::glsl::log2_q32::__lps_log2_q32; -#[cfg(feature = "float-f32")] use lps_builtins::builtins::glsl::log_f32::__lps_log_f32; use lps_builtins::builtins::glsl::log_q32::__lps_log_q32; #[cfg(feature = "float-f32")] +use lps_builtins::builtins::glsl::log2_f32::__lps_log2_f32; +use lps_builtins::builtins::glsl::log2_q32::__lps_log2_q32; +#[cfg(feature = "float-f32")] use lps_builtins::builtins::glsl::mod_f32::__lps_mod_f32; use lps_builtins::builtins::glsl::mod_q32::__lps_mod_q32; #[cfg(feature = "float-f32")] @@ -224,12 +224,12 @@ use lps_builtins::builtins::lpir::itof_u_f32::__lp_lpir_itof_u_f32; use lps_builtins::builtins::lpir::itof_u_q32::__lp_lpir_itof_u_q32; #[cfg(feature = "float-f32")] use lps_builtins::builtins::lpir::unorm_conv_f32::{ - __lp_lpir_fto_unorm16_f32, __lp_lpir_fto_unorm8_f32, __lp_lpir_unorm16_to_f_f32, - __lp_lpir_unorm8_to_f_f32, + __lp_lpir_fto_unorm8_f32, __lp_lpir_fto_unorm16_f32, __lp_lpir_unorm8_to_f_f32, + __lp_lpir_unorm16_to_f_f32, }; use lps_builtins::builtins::lpir::unorm_conv_q32::{ - __lp_lpir_fto_unorm16_q32, __lp_lpir_fto_unorm8_q32, __lp_lpir_unorm16_to_f_q32, - __lp_lpir_unorm8_to_f_q32, + __lp_lpir_fto_unorm8_q32, __lp_lpir_fto_unorm16_q32, __lp_lpir_unorm8_to_f_q32, + __lp_lpir_unorm16_to_f_q32, }; #[cfg(feature = "float-f32")] use lps_builtins::builtins::texture::{ diff --git a/lp-shader/lps-builtins/src/builtin_refs.rs b/lp-shader/lps-builtins/src/builtin_refs.rs index d136572c4..6587337f3 100644 --- a/lp-shader/lps-builtins/src/builtin_refs.rs +++ b/lp-shader/lps-builtins/src/builtin_refs.rs @@ -19,12 +19,12 @@ use crate::builtins::glsl::asin_q32::__lps_asin_q32; use crate::builtins::glsl::asinh_f32::__lps_asinh_f32; use crate::builtins::glsl::asinh_q32::__lps_asinh_q32; #[cfg(feature = "float-f32")] -use crate::builtins::glsl::atan2_f32::__lps_atan2_f32; -use crate::builtins::glsl::atan2_q32::__lps_atan2_q32; -#[cfg(feature = "float-f32")] use crate::builtins::glsl::atan_f32::__lps_atan_f32; use crate::builtins::glsl::atan_q32::__lps_atan_q32; #[cfg(feature = "float-f32")] +use crate::builtins::glsl::atan2_f32::__lps_atan2_f32; +use crate::builtins::glsl::atan2_q32::__lps_atan2_q32; +#[cfg(feature = "float-f32")] use crate::builtins::glsl::atanh_f32::__lps_atanh_f32; use crate::builtins::glsl::atanh_q32::__lps_atanh_q32; #[cfg(feature = "float-f32")] @@ -34,12 +34,12 @@ use crate::builtins::glsl::cos_q32::__lps_cos_q32; use crate::builtins::glsl::cosh_f32::__lps_cosh_f32; use crate::builtins::glsl::cosh_q32::__lps_cosh_q32; #[cfg(feature = "float-f32")] -use crate::builtins::glsl::exp2_f32::__lps_exp2_f32; -use crate::builtins::glsl::exp2_q32::__lps_exp2_q32; -#[cfg(feature = "float-f32")] use crate::builtins::glsl::exp_f32::__lps_exp_f32; use crate::builtins::glsl::exp_q32::__lps_exp_q32; #[cfg(feature = "float-f32")] +use crate::builtins::glsl::exp2_f32::__lps_exp2_f32; +use crate::builtins::glsl::exp2_q32::__lps_exp2_q32; +#[cfg(feature = "float-f32")] use crate::builtins::glsl::fma_f32::__lps_fma_f32; use crate::builtins::glsl::fma_q32::__lps_fma_q32; #[cfg(feature = "float-f32")] @@ -49,12 +49,12 @@ use crate::builtins::glsl::inversesqrt_q32::__lps_inversesqrt_q32; use crate::builtins::glsl::ldexp_f32::__lps_ldexp_f32; use crate::builtins::glsl::ldexp_q32::__lps_ldexp_q32; #[cfg(feature = "float-f32")] -use crate::builtins::glsl::log2_f32::__lps_log2_f32; -use crate::builtins::glsl::log2_q32::__lps_log2_q32; -#[cfg(feature = "float-f32")] use crate::builtins::glsl::log_f32::__lps_log_f32; use crate::builtins::glsl::log_q32::__lps_log_q32; #[cfg(feature = "float-f32")] +use crate::builtins::glsl::log2_f32::__lps_log2_f32; +use crate::builtins::glsl::log2_q32::__lps_log2_q32; +#[cfg(feature = "float-f32")] use crate::builtins::glsl::mod_f32::__lps_mod_f32; use crate::builtins::glsl::mod_q32::__lps_mod_q32; #[cfg(feature = "float-f32")] @@ -220,12 +220,12 @@ use crate::builtins::lpir::itof_u_f32::__lp_lpir_itof_u_f32; use crate::builtins::lpir::itof_u_q32::__lp_lpir_itof_u_q32; #[cfg(feature = "float-f32")] use crate::builtins::lpir::unorm_conv_f32::{ - __lp_lpir_fto_unorm16_f32, __lp_lpir_fto_unorm8_f32, __lp_lpir_unorm16_to_f_f32, - __lp_lpir_unorm8_to_f_f32, + __lp_lpir_fto_unorm8_f32, __lp_lpir_fto_unorm16_f32, __lp_lpir_unorm8_to_f_f32, + __lp_lpir_unorm16_to_f_f32, }; use crate::builtins::lpir::unorm_conv_q32::{ - __lp_lpir_fto_unorm16_q32, __lp_lpir_fto_unorm8_q32, __lp_lpir_unorm16_to_f_q32, - __lp_lpir_unorm8_to_f_q32, + __lp_lpir_fto_unorm8_q32, __lp_lpir_fto_unorm16_q32, __lp_lpir_unorm8_to_f_q32, + __lp_lpir_unorm16_to_f_q32, }; #[cfg(feature = "float-f32")] use crate::builtins::texture::{ diff --git a/lp-shader/lps-builtins/src/builtins/lpir/float_misc_q32.rs b/lp-shader/lps-builtins/src/builtins/lpir/float_misc_q32.rs index 9888694bb..fb376ceea 100644 --- a/lp-shader/lps-builtins/src/builtins/lpir/float_misc_q32.rs +++ b/lp-shader/lps-builtins/src/builtins/lpir/float_misc_q32.rs @@ -11,11 +11,7 @@ #[unsafe(no_mangle)] pub extern "C" fn __lp_lpir_fabs_q32(v: i32) -> i32 { - if v < 0 { - v.wrapping_neg() - } else { - v - } + if v < 0 { v.wrapping_neg() } else { v } } #[unsafe(no_mangle)] diff --git a/lp-shader/lps-builtins/src/builtins/lpir/ftoi_sat_q32.rs b/lp-shader/lps-builtins/src/builtins/lpir/ftoi_sat_q32.rs index 248f34a92..00ee824f3 100644 --- a/lp-shader/lps-builtins/src/builtins/lpir/ftoi_sat_q32.rs +++ b/lp-shader/lps-builtins/src/builtins/lpir/ftoi_sat_q32.rs @@ -11,9 +11,5 @@ pub extern "C" fn __lp_lpir_ftoi_sat_s_q32(v: i32) -> i32 { #[unsafe(no_mangle)] pub extern "C" fn __lp_lpir_ftoi_sat_u_q32(v: i32) -> i32 { let t = __lp_lpir_ftoi_sat_s_q32(v); - if t < 0 { - 0 - } else { - t - } + if t < 0 { 0 } else { t } } diff --git a/lp-shader/lpvm-cranelift/src/generated_builtin_abi.rs b/lp-shader/lpvm-cranelift/src/generated_builtin_abi.rs index 4010a542b..0db5c8e4e 100644 --- a/lp-shader/lpvm-cranelift/src/generated_builtin_abi.rs +++ b/lp-shader/lpvm-cranelift/src/generated_builtin_abi.rs @@ -12,7 +12,7 @@ //! Changing an `extern "C"` builtin in `lps-builtins` without re-running codegen will desync //! this file and fail `cargo check` until you regenerate. -use cranelift_codegen::ir::{types, AbiParam, Signature}; +use cranelift_codegen::ir::{AbiParam, Signature, types}; use cranelift_codegen::isa::CallConv; use lps_builtin_ids::BuiltinId; diff --git a/lp-shader/lpvm-native/tests/xt_pipeline_f32.rs b/lp-shader/lpvm-native/tests/xt_pipeline_f32.rs index ec89a1c6d..9b46ce307 100644 --- a/lp-shader/lpvm-native/tests/xt_pipeline_f32.rs +++ b/lp-shader/lpvm-native/tests/xt_pipeline_f32.rs @@ -164,25 +164,43 @@ fn run_f32(ir: &LpirModule, sig: &LpsModuleSig, entry_name: &str, args: &[u32]) emu.run_with_args(&code, entry_off, args) } -/// The Xtensa builtins base image, or `None` with a loud note when it has not -/// been built. +/// The Xtensa builtins base image **carrying the f32 family**, or `None` with a +/// loud note. /// -/// A gitignored cross-target artifact (`scripts/build-builtins-xt.sh`, esp -/// toolchain), so absence is a skip and not a failure — the same contract -/// `xt_builtins_image.rs` uses. -fn builtins_image() -> Option> { +/// Two ways to come up empty, both skips rather than failures: +/// +/// - the image has not been built at all — a gitignored cross-target artifact +/// (`scripts/build-builtins-xt.sh`, esp toolchain), the same contract +/// `xt_builtins_image.rs` uses; +/// - it was built **without** `float-f32`, which is the default. The family is +/// opt-in (`LP_XT_BUILTINS_F32=1`) because with it in, `.text` fills the +/// emulator's 112 KiB code region to within ~931 bytes and compiled shader +/// code no longer fits after it — see +/// `docs/defects/2026-08-01-xt-f32-builtins-exhaust-the-emulator-code-region.md`. +/// Failing here instead would turn one blocked capability into a red suite. +fn builtins_image_with_f32() -> Option> { let p = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("../../lp-xt/fixtures/elf/lps-builtins-xt-app.elf"); - match std::fs::read(&p) { - Ok(b) => Some(b), - Err(_) => { - eprintln!( - "SKIP: {} not found — run scripts/build-builtins-xt.sh (esp toolchain) first", - p.display() - ); - None - } + let Ok(bytes) = std::fs::read(&p) else { + eprintln!( + "SKIP: {} not found — run scripts/build-builtins-xt.sh (esp toolchain) first", + p.display() + ); + return None; + }; + let has_f32 = { + let elf = XtensaElf::parse(&bytes).expect("builtins image parses as Xtensa ELF32"); + elf.symbol("__lp_lpir_ffloor_f32").is_some() + }; + if !has_f32 { + eprintln!( + "SKIP: {} carries no native-f32 builtins — rebuild with \ + LP_XT_BUILTINS_F32=1 scripts/build-builtins-xt.sh", + p.display() + ); + return None; } + Some(bytes) } /// Compile in `FloatMode::F32`, link against the **builtins base image**, and @@ -1162,20 +1180,23 @@ fn unarmed_float_code_faults_with_a_coprocessor_trap() { /// M7 D4 routes the non-inlinable float operations (divide, sqrt, the rounding /// family, min/max, float→int, every transcendental) to M5's `_f32` builtins /// via `sym_call`. Resolving those on the host emulation path needs -/// `lp-xt/lps-builtins-xt-app` built with `float-f32`, which -/// `scripts/build-builtins-xt.sh` now does unconditionally. +/// `lp-xt/lps-builtins-xt-app` built with `float-f32`. /// -/// This was `#[ignore]`d while that build failed in the esp Rust backend, -/// which cannot select a float constant pool +/// This was `#[ignore]`d while that build failed outright in the esp Rust +/// backend, which cannot select a float constant pool /// (`docs/defects/2026-08-01-xtensa-backend-cannot-select-float-constant-pool.md` -/// — still open upstream, worked around in our source). +/// — still open upstream, worked around in our source). It now runs, but only +/// against an image built with `LP_XT_BUILTINS_F32=1`: the family is still +/// opt-in, on capacity grounds rather than compile grounds +/// (`docs/defects/2026-08-01-xt-f32-builtins-exhaust-the-emulator-code-region.md`). +/// A skip is therefore the honest default — see [`builtins_image_with_f32`]. /// /// `ffloor` is deliberately chosen over `fdiv`/`fsqrt`: it reaches no /// `div0.s`/`const.s` estimate helper, so it does not additionally depend on /// M6-P6's unresolved policy fields. #[test] fn a_builtin_routed_float_op_resolves_and_runs() { - let Some(image) = builtins_image() else { + let Some(image) = builtins_image_with_f32() else { return; }; diff --git a/scripts/build-builtins-xt.sh b/scripts/build-builtins-xt.sh index dbea2edc7..6e56dd37c 100755 --- a/scripts/build-builtins-xt.sh +++ b/scripts/build-builtins-xt.sh @@ -42,22 +42,36 @@ export CARGO_TARGET_DIR="$PWD/target" # and staleness is not harmless here — on esp Rust 1.88 `lpc-model` genuinely # fails to compile (70x E0716 from the Slotted derive's const-promotion of a # temporary), which is what blocked the firmware crates. Fail loudly instead. -# The native-f32 builtin family is built in unconditionally, matching -# scripts/build-builtins.sh (rv32). It was briefly opt-in behind -# LP_XT_BUILTINS_F32 because `lps-builtins/float-f32` did not compile for -# Xtensa at all: -# rustc-LLVM ERROR: Cannot select: XtensaISD::PCREL_WRAPPER -# TargetConstantPool <[2 x float] [0.0, -1.0]> -# The backend limitation is real and still open, but our source no longer -# forms a float constant pool — see the workaround (and its bit-equivalence -# test) in lps-builtins' rgb2hsv_f32.rs, and: +# The native-f32 builtin family stays **opt-in**, for a different reason than +# when this gate was introduced. +# +# It was originally opt-in because `lps-builtins/float-f32` did not compile for +# Xtensa at all — the backend cannot select a float constant pool. That is +# fixed on our side (see the workaround and its bit-equivalence test in +# lps-builtins' rgb2hsv_f32.rs); the image now links with the family in. # docs/defects/2026-08-01-xtensa-backend-cannot-select-float-constant-pool.md # +# What it runs into instead is **capacity**. link.ld gives .text 112 KiB of the +# emulator's 128 KiB code region; with float-f32 .text is 113,757 B, so the +# image links with ~931 bytes to spare — and `rt_emu::xt_image` places compiled +# shader code in exactly that gap. Every filetest whose shader exceeds it fails +# to link: the xtn.q32 suite drops from 849/849 files to 522/849. Measured, not +# predicted; the two images are 66,300 B and 113,757 B of .text. +# docs/defects/2026-08-01-xt-f32-builtins-exhaust-the-emulator-code-region.md +# # This is load-bearing beyond the f32 path: scripts/filetests.sh builds this -# image before running the xtn/xtlpn targets, so a compile failure here takes -# the whole Xtensa filetest suite down with it. If a new `[f32; N]` table ever -# re-trips the backend, that defect file is the place to start. -if ! cargo build --release -p lps-builtins-xt-app --features float-f32; then +# image before running the xtn/xtlpn targets, so anything wrong here takes the +# whole Xtensa filetest suite down with it. Hence opt-in until the region/split +# question is decided — `LP_XT_BUILTINS_F32=1` requests the family for the +# host-execution work that needs it (see lpvm-native's xt_pipeline_f32.rs). +F32_REQUESTED="${LP_XT_BUILTINS_F32:-0}" +if [[ "$F32_REQUESTED" == "1" ]]; then + BUILD_CMD=(cargo build --release -p lps-builtins-xt-app --features float-f32) +else + BUILD_CMD=(cargo build --release -p lps-builtins-xt-app) +fi + +if ! "${BUILD_CMD[@]}"; then echo >&2 echo "error: build failed. If that was an MSRV error, the esp toolchain is stale:" >&2 echo " installed: $(rustc +esp --version 2>/dev/null || echo unknown)" >&2 @@ -79,10 +93,11 @@ fi # The f32 family is what M7's hardware-float lowering calls for everything it # does not inline — divide, sqrt, the rounding family, min/max, the saturating # conversions and every transcendental (M7 D4). An image without those symbols -# cannot resolve any of it, so assert the family is present (mirroring the rv32 -# script, where a missing family fails 800+ filetests with one opaque message). +# cannot resolve any of it, so the count is reported either way and asserted +# when the family was actually requested (mirroring the rv32 script, where a +# missing family fails 800+ filetests with one opaque message). F32_COUNT="$("$NM" "$OUT" | grep -c '_f32$' || true)" -if [[ "$F32_COUNT" -eq 0 ]]; then +if [[ "$F32_REQUESTED" == "1" && "$F32_COUNT" -eq 0 ]]; then echo "error: $OUT contains no native-f32 builtins despite --features float-f32" >&2 exit 1 fi