memlnaut-nisps/docs/specs/recon/midi-gamepad-inputs-worklog.md

103 lines
5.5 KiB
Markdown
Raw Normal View History

---
kind: finding
date: 2026-06-27
immutable: true
---
feat(manifold): MIDI + game controller inputs; widen ML net to N-D Wire the modular input layer into the Console and reshape the browser engine so input axes are genuine independent dimensions. Inputs (manifold/src/inputs/): - gamepad-source: emit press+release edges with standard-mapping labels (enables hold-and-move); single/double-stick already present. - midi-input-source: single-device selection + batch "MIDI Learn" (every CC swept while armed becomes an axis); notes stay discrete. - input-layer: compose() forwards each axis 1:1 (no mean-blend); add onReducedInput so the manifold tracks gamepad/MIDI position. - types: InputAction.phase, InputMode. Console (manifold/src/console/): - ConsoleApp: bind gamepad buttons to verdicts (RB up / LB down / X randomise / Y nudge / B undo / A-hold reposition); mirror composed position onto the manifold. - Drawers: rebuilt Inputs drawer (source picker, gamepad legend, MIDI device picker + batch-learn flow, learned-control meters). Engine (nisps/wasm, manifold/src/engine): - DefaultMLP widened MLP<2,..> -> MLP<32,..> (32 = MAX_AXES); each active axis gets a dedicated slot, unused slots held at 0 (inert). Rebuilt nisps.wasm (playground + manifold). - spine/engine-api: setInputs writes the full N-D vector (was dropping arr[2+]); primary pair keeps the 2-D pipeline; process() re-ticks the whole vector via spine.reprocess(). Tests: - parity_check/parity_wasm: ParityMLP -> 32 inputs, widen example bufs. - CMakeLists: build parity binary with -ffp-contract=off so native matches FMA-free WASM (training amplified the gap past 1e-5). Inputs dock is still an exclusive picker; mixing toggles, reshape modal, and the >2-D slider view (inputs-spec.md) are groundwork-laid but not yet wired. See docs/redesign/midi-gamepad-inputs-worklog.md.
2026-06-28 21:05:30 +02:00
# Work log — MIDI + Game Controller inputs, N-D engine foundation
*Scope: what was actually built on the `feat/midi-inputs` branch. This is a
description of the work, not a spec. The design intent lives in
`docs/specs/inputs-spec.md`; where this branch diverges from or only partially
feat(manifold): MIDI + game controller inputs; widen ML net to N-D Wire the modular input layer into the Console and reshape the browser engine so input axes are genuine independent dimensions. Inputs (manifold/src/inputs/): - gamepad-source: emit press+release edges with standard-mapping labels (enables hold-and-move); single/double-stick already present. - midi-input-source: single-device selection + batch "MIDI Learn" (every CC swept while armed becomes an axis); notes stay discrete. - input-layer: compose() forwards each axis 1:1 (no mean-blend); add onReducedInput so the manifold tracks gamepad/MIDI position. - types: InputAction.phase, InputMode. Console (manifold/src/console/): - ConsoleApp: bind gamepad buttons to verdicts (RB up / LB down / X randomise / Y nudge / B undo / A-hold reposition); mirror composed position onto the manifold. - Drawers: rebuilt Inputs drawer (source picker, gamepad legend, MIDI device picker + batch-learn flow, learned-control meters). Engine (nisps/wasm, manifold/src/engine): - DefaultMLP widened MLP<2,..> -> MLP<32,..> (32 = MAX_AXES); each active axis gets a dedicated slot, unused slots held at 0 (inert). Rebuilt nisps.wasm (playground + manifold). - spine/engine-api: setInputs writes the full N-D vector (was dropping arr[2+]); primary pair keeps the 2-D pipeline; process() re-ticks the whole vector via spine.reprocess(). Tests: - parity_check/parity_wasm: ParityMLP -> 32 inputs, widen example bufs. - CMakeLists: build parity binary with -ffp-contract=off so native matches FMA-free WASM (training amplified the gap past 1e-5). Inputs dock is still an exclusive picker; mixing toggles, reshape modal, and the >2-D slider view (inputs-spec.md) are groundwork-laid but not yet wired. See docs/redesign/midi-gamepad-inputs-worklog.md.
2026-06-28 21:05:30 +02:00
realises that spec, it is called out below.*
## Summary
This branch wires the modular input layer (which already existed as adapters in
`manifold/src/inputs/`) into the Console, adds the missing gamepad→verdict and
MIDI-device plumbing, and reshapes the browser ML engine so input axes are
genuine independent dimensions instead of being blended into two. It landed in
two passes:
1. **Input methods** — a working Inputs dock with three sources (Internal XY
pad, Game Controller, MIDI), gamepad buttons bound to verdicts, and a batch
"MIDI Learn".
2. **Engine foundation for mixing** — the WASM net was widened from 2 inputs to
a 32-input maximum so each active axis gets its own dimension (no blending).
The Inputs dock currently presents the three sources as an **exclusive** picker.
The engine groundwork for *mixing* sources (independent dimensions, no idle-bias)
is in place, but the dock toggles, the reshape-confirm modal, and the
>2-dimension slider visualisation described in `inputs-spec.md` are **not yet
wired** — see "Not done yet" below.
## What changed
### Input sources (`manifold/src/inputs/`)
- `gamepad-source.ts` — buttons now emit both press and release edges (with
standard-mapping labels A/B/X/Y/LB/RB/…), enabling hold-and-move gestures.
Single/double-stick (2/4 axes) was already present.
- `midi-input-source.ts` — added single-device selection (`selectDevice`, the
dock device picker; default still listens to all ports) and changed MIDI-Learn
from one-binding-per-arm to a **batch** capture: while armed, every distinct CC
that moves is appended as an axis; notes stay discrete actions and are not
auto-bound. Learned CCs are exposed as bindings for the dock.
- `types.ts``InputAction` gained an optional `phase` ('press' | 'release');
added an `InputMode` ('internal' | 'gamepad' | 'midi') type.
- `input-layer.ts` — added `onReducedInput` so the on-screen manifold can track a
gamepad/MIDI-driven position. **`compose()` no longer mean-blends**: it
forwards each active axis 1:1 to its own engine input slot (the engine
zero-pads the rest; a zero input is inert, `0 × weight = 0`).
- `useInputLayer.ts` — the React binding; exposes the active mode, per-source
status, gamepad stick mode, MIDI device list/selection, batch-learn arm, and
learned bindings. (Currently exclusive — one mode at a time.)
### Console wiring (`manifold/src/console/`)
- `ConsoleApp.tsx` — subscribes to gamepad actions and binds them to existing
verdict handlers: RB = thumbs-up, LB = thumbs-down, X = randomise, Y = nudge,
B = undo, A-hold = reposition (hold, move stick, release to place an example
at the stick position). Mirrors the composed input position onto the manifold
when a non-pad source is active (deduped to avoid per-frame re-renders).
- `Drawers.tsx` — rebuilt the Inputs drawer: a source picker, a gamepad stick
toggle + button legend, a MIDI device picker, the batch MIDI-Learn flow with
its "move every control, then Done" message, and learned controls rendered as
read-only meters styled distinctly from the output sliders.
### Engine (`manifold/src/engine/`, `nisps/wasm/`)
- `nisps/wasm/bindings.cpp``DefaultMLP` widened `MLP<2,…>``MLP<32,…>`
(32 = `MAX_AXES`). Each active axis maps to a dedicated input slot; unused
slots are held at 0. Rebuilt `nisps.wasm` and synced to both
`playground/public/` and `manifold/public/` (the C ABI / `nisps.js` glue is
unchanged).
- `spine.ts` / `engine-api.ts``setInputs(arr)` now writes the full
N-dimensional vector (it previously dropped everything past `arr[1]`); the
primary pair still runs through the 2-D input pipeline so the pad keeps its
feel, axes 2+ are written raw, and `process()` re-ticks the whole vector after
weight changes via the new `spine.reprocess()`.
### Tests / build
- `tests/cpp/parity_check.cpp` + `tests/cpp/parity_wasm.mjs``ParityMLP`
bumped to 32 inputs and the example/feature buffers widened to match the net's
arity (`add_example` requires `features.size() >= NIn`).
- `nisps/CMakeLists.txt` — the parity binary now builds with `-ffp-contract=off`.
Widening the input layer exposed a native↔WASM divergence: native clang/gcc
fuse multiply-adds (FMA) the WASM build has no instruction for, and the
training loop amplified the rounding difference past the 1e-5 parity tolerance.
Disabling FP contraction on the native parity build alone restores bit-equality
(max delta ~2.4e-7).
## Verification
- C++ suites (4/4) pass; native↔WASM parity passes at 1e-5.
- `manifold` typechecks and builds; the Playwright smoke test (engine loads,
input→output propagates) passes.
## Not done yet (vs `inputs-spec.md`)
- The Inputs dock is an **exclusive** picker; mixing several sources at once
(independent toggles) is not wired, though the engine and `compose()` now
support it.
- No reshape-confirm modal + net reset when the active input set changes.
- No swap to a slider visualisation when more than two input dimensions are
active (the 2-D manifold is always shown).
- The input pipeline (deadzone/zoom/curve) is applied only to the primary pair;
per-source conditioning for axes 2+ is left raw.