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 98ad6c296..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 @@ -1,10 +1,12 @@ --- -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: - 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 @@ -30,57 +32,107 @@ 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. + +`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 +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/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]; diff --git a/lp-shader/lpvm-native/tests/xt_pipeline_f32.rs b/lp-shader/lpvm-native/tests/xt_pipeline_f32.rs index 96183fe2d..9b46ce307 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; @@ -41,7 +49,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 +117,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 +164,157 @@ 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 **carrying the f32 family**, or `None` with a +/// loud note. +/// +/// 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"); + 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 +/// 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 { + 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) + .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 +381,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 +598,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 +954,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 +1177,45 @@ 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`. +/// +/// 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). 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`]. /// -/// 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_with_f32() 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..6e56dd37c 100755 --- a/scripts/build-builtins-xt.sh +++ b/scripts/build-builtins-xt.sh @@ -42,20 +42,28 @@ 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 -# 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 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 # -# 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. +# 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 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)