One-core-engine P4.3/P4.4: the input/output pipeline processing and the curve
catalog now live in the C++/WASM core (nisps/pipeline/*, nisps/core/math.hpp).
The TS ports are deleted and the browser drives the WASM chains.
Engine:
- WasmIML owns a nisps_pipeline_create handle + bridge buffers and exposes
setInputConfig (TS InputConfig → 15-float wire), processInput, resetInput,
setOutputConfig (Infinity slew → 0), setOutputFreezeMask, processOutput
(in place), resetOutput, curveApply, curveApplyBatch (chunked). Handle +
buffers created in init_, freed in dispose, output-sized buffers realloc'd
on reshape.
- Spine routes setInputs through iml.processInput/processOutput (state lives
C++-side); config source-of-truth stays TS-side and is pushed on attach /
setInputConfig / setOutputConfig. Preserves ?debug=1 fixed-dt determinism
(same dt fed to the WASM calls). EngineApi gains setInputConfig/
setOutputConfig/curveApply/curveApplyBatch.
- New types-only modules: pipeline-types.ts (InputConfig/OutputConfig +
defaults + wire int mappers) and curve-catalog.ts (CurveName + name→id).
types.ts declares the pipeline/curve C ABI. engine barrel updated.
- DELETED src/engine/{input-pipeline,output-pipeline,curves}.ts.
Tests (P4.4 gate — recorded-gesture regression):
- pipeline-golden.test.ts now loads the built WASM (indirect-eval shim,
tests/wasm-load.ts) and drives the frozen gesture/output fixtures through the
C++ chains, honouring the per-event dt clock contract. Tolerance 1e-5
(non-momentum drift <5e-7). The 3 momentum configs carry 1e-2: proven-inherent
f32 drift (a byte-faithful f32 port of the exact original algorithm matches
the WASM to <6e-8 while both diverge from the f64 capture by ~7-9e-3), NOT a
core bug.
- curves-golden.json: linear/square/sqrt/centered_power kept as the original
f64 captures (C++ matches within <3e-8); exp/log/sigmoid/cubic RE-BASELINED
from the WASM (deliberate switch to firmware-exact k=1 exp/log, slope-6
sigmoid, true cubic x^3). Provenance recorded in-file.
- _generate.ts rebuilt as the WASM curve re-baseline tool; pipeline-golden-lib
trimmed to pure data builders.
Docs: fixtures/README.md + manifold/ONBOARDING.md updated.
Gates: typecheck, bun test (9), vite build, playwright e2e (27) all green.
92 lines
5.8 KiB
Markdown
92 lines
5.8 KiB
Markdown
# Pipeline golden fixtures
|
||
|
||
Originally captured **2026-07-13** from the TypeScript engine implementations,
|
||
**before** the P4 "one core engine" migration
|
||
(`docs/specs/plans/one-core-engine-refactor.md` §P4). As of **2026-07-18** that
|
||
migration has landed: the curve / input-pipeline / output-pipeline maths now
|
||
live solely in the C++/WASM core (`nisps/pipeline/*`, `nisps/core/math.hpp`),
|
||
and the TS `curves.ts` / `input-pipeline.ts` / `output-pipeline.ts` are deleted.
|
||
|
||
These fixtures are the recorded-gesture pre/post-migration regression P4's gate
|
||
calls for: *"same pointer trace → same routed output pre/post migration."* The
|
||
test (`../pipeline-golden.test.ts`) now drives the WASM chains against them.
|
||
|
||
## What is here
|
||
|
||
| File | What it pins | Driven through (now) |
|
||
|---|---|---|
|
||
| `gesture-trace.json` | One canonical synthetic pointer trace (288 events, fixed 120 Hz dt) over the input pipeline's native `[0,1]²` domain: h/v sweeps, diagonal, spiral, figure-eight, dwell + abrupt corner jumps. Pure formula — no `Math.random`, no `Date.now`. | (input to the input chain) |
|
||
| `curves-golden.json` | The curve catalog, 129 samples of `x ∈ [0,1]` inclusive. **See "Curves re-baselined" below.** | `nisps_curve_apply` (WASM) |
|
||
| `input-pipeline-golden.json` | The gesture trace run through the input pipeline under 14 representative configs (default, deadzone, zoom, sticky anchor, per-axis, curves, smoothing, invert, momentum gentle/strong, frozen axis, fully frozen, combined). Records `{x, y, frozen}` per event. Configs embedded. **FROZEN** pre-migration capture. | `nisps_input_*` (WASM) |
|
||
| `output-pipeline-golden.json` | A deterministic raw-output sequence (120 vectors × 8 channels of offset sines, quantised to f32) run through the output pipeline under 8 configs (default, curves, smoothing, slew, global-freeze toggled mid-sequence, per-output freeze mask, combined). Records the processed vector per step. Configs embedded. **FROZEN** pre-migration capture. | `nisps_output_*` (WASM) |
|
||
|
||
## The regression guard
|
||
|
||
`../pipeline-golden.test.ts` (`bun test`) loads the built WASM
|
||
(`../../public/nisps.{js,wasm}`) via the indirect-eval shim (`../wasm-load.ts`,
|
||
same technique as `tests/cpp/parity_wasm.mjs`), creates a pipeline handle, and
|
||
re-runs the **committed fixtures** through the C++ chains. The fixtures are
|
||
authoritative: the trace, raw sequence, and per-run configs are read FROM the
|
||
JSON, so the config lists in `pipeline-golden-lib.ts` cannot mask a regression.
|
||
|
||
### Tolerances (f32 WASM vs f64-captured fixtures)
|
||
|
||
- **Input / output pipelines: `1e-5`.** Measured max non-momentum drift `<5e-7`.
|
||
- **Momentum configs (`momentum-gentle` / `momentum-strong` / `combined`):
|
||
`1e-2`.** This is proven-inherent f32 drift, **not** a core bug. The velocity
|
||
ring's window-membership test (`now − t ≤ window`) is a DISCRETE boundary that
|
||
f32 rounding can flip during fast gestures, shifting which sample is the
|
||
window's oldest by a whole frame (~8 ms) → a step change in the measured speed
|
||
→ integrated by the momentum-zoom IIR. A byte-faithful f32 port of the exact
|
||
original TS algorithm reproduces the WASM to `<6e-8` while both diverge from
|
||
the f64 capture by the same `~7–9e-3` (measured max `8.6e-3` on
|
||
momentum-strong). Reconciling it would require f64 momentum maths, which would
|
||
break firmware parity — so the momentum runs are guarded at `1e-2` (the core
|
||
is still tightly pinned to the algorithm by the `<6e-8` faithful-f32 identity;
|
||
a real behavioural regression would blow far past `1e-2`).
|
||
|
||
## Curves re-baselined (2026-07-18)
|
||
|
||
`curves-golden.json` is now a MIX, recorded in its `provenance` field:
|
||
|
||
- **`linear` / `square` / `sqrt` / `centered_power`** — the ORIGINAL 2026-07-13
|
||
f64 TS captures, kept unchanged. The C++ core reproduces them within `1e-5`
|
||
(measured `<3e-8`), proving **no behaviour change** for these curves.
|
||
- **`exp` / `log` / `sigmoid` / `cubic`** — **RE-BASELINED from the WASM.** The
|
||
browser deliberately adopted the canonical firmware-exact maths: the old TS
|
||
`curves.ts` used `k=4` exp/log, slope-8 sigmoid, and a smoothstep "cubic"; the
|
||
canonical `nisps/core/math.hpp` catalog uses `k=1`-normalised exp/log, a
|
||
slope-6 sigmoid, and a true cubic `x³`. The test asserts WASM stability
|
||
against these regenerated values.
|
||
|
||
## Contracts you must reproduce to consume these
|
||
|
||
### State contract (both chains are stateful, C++-side per pipeline handle)
|
||
- **Input:** EMA-smoothed x/y, a velocity ring, and a momentum-zoom multiplier.
|
||
- **Output:** `prev` + `smoothed` buffers driving slew/freeze.
|
||
|
||
Each config **run resets state** (`nisps_input_reset` / `nisps_output_reset`)
|
||
at step 0. Runs are independent; do not carry state between them.
|
||
|
||
### Clock contract (input pipeline only)
|
||
The C++ input chain accumulates its own clock from the per-call `dt` (seconds)
|
||
for the momentum velocity window; it takes **no** wall clock. To reproduce the
|
||
capture, the test feeds each event's `dt` = the per-event `t_ms` delta in
|
||
seconds, with the **first event's `dt` = 0** (matching the original TS capture,
|
||
which pinned `performance.now()` to each event's `t_ms`). The output chain uses
|
||
a constant per-step `dt` of `1000/60` ms (→ seconds).
|
||
|
||
### JSON encodings
|
||
- `slewRate: null` in an output spec means `Infinity` (JSON has no `Infinity`);
|
||
it maps to the wire's `slew_rate <= 0 ⇒ unlimited`.
|
||
- Output raw values are pre-quantised with `Math.fround` so they equal exactly
|
||
what a `Float32Array` holds.
|
||
|
||
## Re-capture
|
||
|
||
`bun tests/fixtures/_generate.ts` regenerates `gesture-trace.json` (pure data)
|
||
and re-baselines the 4 changed curves in `curves-golden.json` from the WASM
|
||
(preserving the 4 unchanged f64 entries + provenance). It does **not** rewrite
|
||
the input/output pipeline goldens — those are the frozen pre-migration capture
|
||
the regression is measured against; there is no TS pipeline left to capture
|
||
from. `scripts/build-wasm.sh` must have run first.
|