feat(wasm)!: P2.2 — nisps_ml_create honours dims; runtime-shaped browser MLP + reshape
Operator-approved ABI change (P2 stop-point). The WASM MLP is now
MLPCore<DynamicStorage>:
- nisps_ml_create(input, output, hidden[3], n, seed) honours its args;
non-positive/null fall back to the historical 32→[10,14,18]→126, so
every pre-P2 caller (manifold, worker, parity harness) stays
bit-identical. Invalid/oversized dims (>4096) → null.
- NEW nisps_ml_reshape(ml, in, out, hidden, n, spread): fresh net at the
new dims, warm-started via nisps/ml/warm_start.hpp (overlapping region
copied; rest keeps spread init); feedback controller re-created (state
resets — reset-on-reshape modal is the front-end contract). Failure
leaves the old net untouched.
- nisps_ml_describe(ml, out): takes the handle; null reports defaults.
- FeedbackController got the same storage split: algorithms in
FeedbackControllerCore<FbStorage>; FixedFeedbackStorage keeps firmware/
tests source-identical via the old alias; DynamicFeedbackStorage (one
arena) sizes to the runtime net. Firmware .text unchanged (122692).
- MLHandle: per-instance scratch vectors; dropped the dead 2MB
batch_out_scratch.
- TS: types.ts decls (+_nisps_ml_reshape), wasm-iml re-describes the
created instance, worker carries a shape-contract note for P2.3.
Verified: ctest 4/4 incl. new warm-start grow/shrink test; reshape ABI
smoke (dims honoured, overlap survives, invalid rejected, outputs
bounded); parity PASS unchanged (2.4e-7); lint clean; manifold 9 unit +
20 e2e green; firmware .text 122692 (+0.30% vs pre-P2 baseline).
2026-07-14 03:38:06 +02:00
|
|
|
|
// nisps/wasm/bindings.cpp — flat C API exported to the Manifold browser app.
|
feat(playground/wasm): WASM bridge between C++ core and SolidJS playground (meml-tgm)
Stream 7 wires nisps/ml + nisps/engines into the playground via Emscripten.
Highlights:
- nisps/wasm/bindings.cpp: flat C API per architecture.md §6.2. Fixed-arch
MLP<2, 10, 14, 18, 126>; engine string→type dispatch table with NoOp
fallback.
- scripts/build-wasm.sh: emcc invocation, MODULARIZE=1, exports listed
explicitly; produces playground/public/nisps.{js,wasm}.
- playground/src/ml/wasm-iml.ts: main-thread MLP host (sync inference,
sync training, RL ops, weights I/O, layer stats, localStorage).
- playground/src/ml/wasm-worker.ts: disposable Web Worker for off-thread
async training, owns its own WASM instance.
- playground/src/ml/dataset.ts: Float32Array-backed FIFO with sample-weight
modes (uniform/global/local/combined). Port of legacy dataset.js.
- playground/src/audio/engine-host.ts: AudioContext + AudioWorkletNode
lifecycle, with start/stop/setEngine/setParams.
- playground/src/audio/worklet/nisps-processor.ts: WASM-loading
AudioWorkletProcessor that runs engine.process_block per 128-sample
block. Loads its own WASM instance from main-thread-supplied bytes
(no fetch in worklet).
- playground/src/stores/ml-store.ts: wired stub methods to WasmIML
singleton; lazy initialize().
- playground/src/debug/probe.ts: window.__nisps now calls real WasmIML
via the store; lazy-init on first use.
Verified:
- bash scripts/build-wasm.sh succeeds (94 KB nisps.wasm)
- bun run typecheck OK
- bun run build OK (production bundle)
- vite dev server serves /nisps.{js,wasm} with COOP/COEP
Known limitation: WASM is fixed at one MLP shape. Multi-arch deferred —
documented in nisps/wasm/README.md.
2026-04-29 15:36:29 +02:00
|
|
|
|
//
|
|
|
|
|
|
// Two consumers per build:
|
feat(wasm)!: P2.2 — nisps_ml_create honours dims; runtime-shaped browser MLP + reshape
Operator-approved ABI change (P2 stop-point). The WASM MLP is now
MLPCore<DynamicStorage>:
- nisps_ml_create(input, output, hidden[3], n, seed) honours its args;
non-positive/null fall back to the historical 32→[10,14,18]→126, so
every pre-P2 caller (manifold, worker, parity harness) stays
bit-identical. Invalid/oversized dims (>4096) → null.
- NEW nisps_ml_reshape(ml, in, out, hidden, n, spread): fresh net at the
new dims, warm-started via nisps/ml/warm_start.hpp (overlapping region
copied; rest keeps spread init); feedback controller re-created (state
resets — reset-on-reshape modal is the front-end contract). Failure
leaves the old net untouched.
- nisps_ml_describe(ml, out): takes the handle; null reports defaults.
- FeedbackController got the same storage split: algorithms in
FeedbackControllerCore<FbStorage>; FixedFeedbackStorage keeps firmware/
tests source-identical via the old alias; DynamicFeedbackStorage (one
arena) sizes to the runtime net. Firmware .text unchanged (122692).
- MLHandle: per-instance scratch vectors; dropped the dead 2MB
batch_out_scratch.
- TS: types.ts decls (+_nisps_ml_reshape), wasm-iml re-describes the
created instance, worker carries a shape-contract note for P2.3.
Verified: ctest 4/4 incl. new warm-start grow/shrink test; reshape ABI
smoke (dims honoured, overlap survives, invalid rejected, outputs
bounded); parity PASS unchanged (2.4e-7); lint clean; manifold 9 unit +
20 e2e green; firmware .text 122692 (+0.30% vs pre-P2 baseline).
2026-07-14 03:38:06 +02:00
|
|
|
|
// 1. Main-thread WasmIML (manifold/src/engine/wasm-iml.ts) — ML calls.
|
|
|
|
|
|
// 2. AudioWorklet processor (manifold/src/engine/worklet/...) — engine
|
feat(playground/wasm): WASM bridge between C++ core and SolidJS playground (meml-tgm)
Stream 7 wires nisps/ml + nisps/engines into the playground via Emscripten.
Highlights:
- nisps/wasm/bindings.cpp: flat C API per architecture.md §6.2. Fixed-arch
MLP<2, 10, 14, 18, 126>; engine string→type dispatch table with NoOp
fallback.
- scripts/build-wasm.sh: emcc invocation, MODULARIZE=1, exports listed
explicitly; produces playground/public/nisps.{js,wasm}.
- playground/src/ml/wasm-iml.ts: main-thread MLP host (sync inference,
sync training, RL ops, weights I/O, layer stats, localStorage).
- playground/src/ml/wasm-worker.ts: disposable Web Worker for off-thread
async training, owns its own WASM instance.
- playground/src/ml/dataset.ts: Float32Array-backed FIFO with sample-weight
modes (uniform/global/local/combined). Port of legacy dataset.js.
- playground/src/audio/engine-host.ts: AudioContext + AudioWorkletNode
lifecycle, with start/stop/setEngine/setParams.
- playground/src/audio/worklet/nisps-processor.ts: WASM-loading
AudioWorkletProcessor that runs engine.process_block per 128-sample
block. Loads its own WASM instance from main-thread-supplied bytes
(no fetch in worklet).
- playground/src/stores/ml-store.ts: wired stub methods to WasmIML
singleton; lazy initialize().
- playground/src/debug/probe.ts: window.__nisps now calls real WasmIML
via the store; lazy-init on first use.
Verified:
- bash scripts/build-wasm.sh succeeds (94 KB nisps.wasm)
- bun run typecheck OK
- bun run build OK (production bundle)
- vite dev server serves /nisps.{js,wasm} with COOP/COEP
Known limitation: WASM is fixed at one MLP shape. Multi-arch deferred —
documented in nisps/wasm/README.md.
2026-04-29 15:36:29 +02:00
|
|
|
|
// calls. (Each instance owns its own WASM module instance.)
|
|
|
|
|
|
//
|
feat(wasm)!: P2.2 — nisps_ml_create honours dims; runtime-shaped browser MLP + reshape
Operator-approved ABI change (P2 stop-point). The WASM MLP is now
MLPCore<DynamicStorage>:
- nisps_ml_create(input, output, hidden[3], n, seed) honours its args;
non-positive/null fall back to the historical 32→[10,14,18]→126, so
every pre-P2 caller (manifold, worker, parity harness) stays
bit-identical. Invalid/oversized dims (>4096) → null.
- NEW nisps_ml_reshape(ml, in, out, hidden, n, spread): fresh net at the
new dims, warm-started via nisps/ml/warm_start.hpp (overlapping region
copied; rest keeps spread init); feedback controller re-created (state
resets — reset-on-reshape modal is the front-end contract). Failure
leaves the old net untouched.
- nisps_ml_describe(ml, out): takes the handle; null reports defaults.
- FeedbackController got the same storage split: algorithms in
FeedbackControllerCore<FbStorage>; FixedFeedbackStorage keeps firmware/
tests source-identical via the old alias; DynamicFeedbackStorage (one
arena) sizes to the runtime net. Firmware .text unchanged (122692).
- MLHandle: per-instance scratch vectors; dropped the dead 2MB
batch_out_scratch.
- TS: types.ts decls (+_nisps_ml_reshape), wasm-iml re-describes the
created instance, worker carries a shape-contract note for P2.3.
Verified: ctest 4/4 incl. new warm-start grow/shrink test; reshape ABI
smoke (dims honoured, overlap survives, invalid rejected, outputs
bounded); parity PASS unchanged (2.4e-7); lint clean; manifold 9 unit +
20 e2e green; firmware .text 122692 (+0.30% vs pre-P2 baseline).
2026-07-14 03:38:06 +02:00
|
|
|
|
// ARCHITECTURE (runtime-shaped since one-core-engine-refactor P2)
|
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
|
|
|
|
// ---------------------------------------------------------------------
|
feat(wasm)!: P2.2 — nisps_ml_create honours dims; runtime-shaped browser MLP + reshape
Operator-approved ABI change (P2 stop-point). The WASM MLP is now
MLPCore<DynamicStorage>:
- nisps_ml_create(input, output, hidden[3], n, seed) honours its args;
non-positive/null fall back to the historical 32→[10,14,18]→126, so
every pre-P2 caller (manifold, worker, parity harness) stays
bit-identical. Invalid/oversized dims (>4096) → null.
- NEW nisps_ml_reshape(ml, in, out, hidden, n, spread): fresh net at the
new dims, warm-started via nisps/ml/warm_start.hpp (overlapping region
copied; rest keeps spread init); feedback controller re-created (state
resets — reset-on-reshape modal is the front-end contract). Failure
leaves the old net untouched.
- nisps_ml_describe(ml, out): takes the handle; null reports defaults.
- FeedbackController got the same storage split: algorithms in
FeedbackControllerCore<FbStorage>; FixedFeedbackStorage keeps firmware/
tests source-identical via the old alias; DynamicFeedbackStorage (one
arena) sizes to the runtime net. Firmware .text unchanged (122692).
- MLHandle: per-instance scratch vectors; dropped the dead 2MB
batch_out_scratch.
- TS: types.ts decls (+_nisps_ml_reshape), wasm-iml re-describes the
created instance, worker carries a shape-contract note for P2.3.
Verified: ctest 4/4 incl. new warm-start grow/shrink test; reshape ABI
smoke (dims honoured, overlap survives, invalid rejected, outputs
bounded); parity PASS unchanged (2.4e-7); lint clean; manifold 9 unit +
20 e2e green; firmware .text 122692 (+0.30% vs pre-P2 baseline).
2026-07-14 03:38:06 +02:00
|
|
|
|
// The browser MLP is `MLPCore<DynamicStorage>`: `nisps_ml_create()` HONOURS
|
|
|
|
|
|
// its caller-supplied input_size / output_size / hidden[3] arguments. The
|
|
|
|
|
|
// topology stays fixed at 4 layers (ReLU×3 + Sigmoid); only the dimensions
|
|
|
|
|
|
// are runtime. Non-positive / missing arguments fall back to the historical
|
|
|
|
|
|
// compiled defaults (32 → [10, 14, 18] → 126), which keeps every pre-P2
|
|
|
|
|
|
// caller — including the parity harness — bit-identical (FixedStorage and
|
|
|
|
|
|
// DynamicStorage are bit-parity-tested for equal shapes/seeds).
|
feat(playground/wasm): WASM bridge between C++ core and SolidJS playground (meml-tgm)
Stream 7 wires nisps/ml + nisps/engines into the playground via Emscripten.
Highlights:
- nisps/wasm/bindings.cpp: flat C API per architecture.md §6.2. Fixed-arch
MLP<2, 10, 14, 18, 126>; engine string→type dispatch table with NoOp
fallback.
- scripts/build-wasm.sh: emcc invocation, MODULARIZE=1, exports listed
explicitly; produces playground/public/nisps.{js,wasm}.
- playground/src/ml/wasm-iml.ts: main-thread MLP host (sync inference,
sync training, RL ops, weights I/O, layer stats, localStorage).
- playground/src/ml/wasm-worker.ts: disposable Web Worker for off-thread
async training, owns its own WASM instance.
- playground/src/ml/dataset.ts: Float32Array-backed FIFO with sample-weight
modes (uniform/global/local/combined). Port of legacy dataset.js.
- playground/src/audio/engine-host.ts: AudioContext + AudioWorkletNode
lifecycle, with start/stop/setEngine/setParams.
- playground/src/audio/worklet/nisps-processor.ts: WASM-loading
AudioWorkletProcessor that runs engine.process_block per 128-sample
block. Loads its own WASM instance from main-thread-supplied bytes
(no fetch in worklet).
- playground/src/stores/ml-store.ts: wired stub methods to WasmIML
singleton; lazy initialize().
- playground/src/debug/probe.ts: window.__nisps now calls real WasmIML
via the store; lazy-init on first use.
Verified:
- bash scripts/build-wasm.sh succeeds (94 KB nisps.wasm)
- bun run typecheck OK
- bun run build OK (production bundle)
- vite dev server serves /nisps.{js,wasm} with COOP/COEP
Known limitation: WASM is fixed at one MLP shape. Multi-arch deferred —
documented in nisps/wasm/README.md.
2026-04-29 15:36:29 +02:00
|
|
|
|
//
|
feat(wasm)!: P2.2 — nisps_ml_create honours dims; runtime-shaped browser MLP + reshape
Operator-approved ABI change (P2 stop-point). The WASM MLP is now
MLPCore<DynamicStorage>:
- nisps_ml_create(input, output, hidden[3], n, seed) honours its args;
non-positive/null fall back to the historical 32→[10,14,18]→126, so
every pre-P2 caller (manifold, worker, parity harness) stays
bit-identical. Invalid/oversized dims (>4096) → null.
- NEW nisps_ml_reshape(ml, in, out, hidden, n, spread): fresh net at the
new dims, warm-started via nisps/ml/warm_start.hpp (overlapping region
copied; rest keeps spread init); feedback controller re-created (state
resets — reset-on-reshape modal is the front-end contract). Failure
leaves the old net untouched.
- nisps_ml_describe(ml, out): takes the handle; null reports defaults.
- FeedbackController got the same storage split: algorithms in
FeedbackControllerCore<FbStorage>; FixedFeedbackStorage keeps firmware/
tests source-identical via the old alias; DynamicFeedbackStorage (one
arena) sizes to the runtime net. Firmware .text unchanged (122692).
- MLHandle: per-instance scratch vectors; dropped the dead 2MB
batch_out_scratch.
- TS: types.ts decls (+_nisps_ml_reshape), wasm-iml re-describes the
created instance, worker carries a shape-contract note for P2.3.
Verified: ctest 4/4 incl. new warm-start grow/shrink test; reshape ABI
smoke (dims honoured, overlap survives, invalid rejected, outputs
bounded); parity PASS unchanged (2.4e-7); lint clean; manifold 9 unit +
20 e2e green; firmware .text 122692 (+0.30% vs pre-P2 baseline).
2026-07-14 03:38:06 +02:00
|
|
|
|
// Reshape = `nisps_ml_reshape()`: a NEW instance at the new dimensions,
|
|
|
|
|
|
// warm-started by copying the overlapping weight region from the old net
|
|
|
|
|
|
// (nisps/ml/warm_start.hpp); weights outside the overlap keep the fresh
|
|
|
|
|
|
// spread-initialised values. The feedback controller is re-created at the
|
|
|
|
|
|
// new dimensions (its exploration state resets — the front-end shows a
|
|
|
|
|
|
// reset-on-reshape modal).
|
feat(playground/wasm): WASM bridge between C++ core and SolidJS playground (meml-tgm)
Stream 7 wires nisps/ml + nisps/engines into the playground via Emscripten.
Highlights:
- nisps/wasm/bindings.cpp: flat C API per architecture.md §6.2. Fixed-arch
MLP<2, 10, 14, 18, 126>; engine string→type dispatch table with NoOp
fallback.
- scripts/build-wasm.sh: emcc invocation, MODULARIZE=1, exports listed
explicitly; produces playground/public/nisps.{js,wasm}.
- playground/src/ml/wasm-iml.ts: main-thread MLP host (sync inference,
sync training, RL ops, weights I/O, layer stats, localStorage).
- playground/src/ml/wasm-worker.ts: disposable Web Worker for off-thread
async training, owns its own WASM instance.
- playground/src/ml/dataset.ts: Float32Array-backed FIFO with sample-weight
modes (uniform/global/local/combined). Port of legacy dataset.js.
- playground/src/audio/engine-host.ts: AudioContext + AudioWorkletNode
lifecycle, with start/stop/setEngine/setParams.
- playground/src/audio/worklet/nisps-processor.ts: WASM-loading
AudioWorkletProcessor that runs engine.process_block per 128-sample
block. Loads its own WASM instance from main-thread-supplied bytes
(no fetch in worklet).
- playground/src/stores/ml-store.ts: wired stub methods to WasmIML
singleton; lazy initialize().
- playground/src/debug/probe.ts: window.__nisps now calls real WasmIML
via the store; lazy-init on first use.
Verified:
- bash scripts/build-wasm.sh succeeds (94 KB nisps.wasm)
- bun run typecheck OK
- bun run build OK (production bundle)
- vite dev server serves /nisps.{js,wasm} with COOP/COEP
Known limitation: WASM is fixed at one MLP shape. Multi-arch deferred —
documented in nisps/wasm/README.md.
2026-04-29 15:36:29 +02:00
|
|
|
|
//
|
|
|
|
|
|
// WIRE FORMAT FOR WEIGHTS
|
|
|
|
|
|
// -----------------------
|
|
|
|
|
|
// The flat layout matches `nisps::ml::MLP::get_weights()`:
|
|
|
|
|
|
//
|
|
|
|
|
|
// [layer0_weights] [layer1_weights] [layer2_weights] [layer3_weights]
|
|
|
|
|
|
// [layer0_biases] [layer1_biases] [layer2_biases] [layer3_biases]
|
|
|
|
|
|
//
|
|
|
|
|
|
// Total count = `nisps_ml_weight_count()`. Both endianness and float layout
|
|
|
|
|
|
// match the host (Emscripten produces little-endian Float32Array-friendly
|
|
|
|
|
|
// memory).
|
|
|
|
|
|
//
|
|
|
|
|
|
// LAYER-STATS LAYOUT
|
|
|
|
|
|
// ------------------
|
|
|
|
|
|
// `nisps_ml_get_layer_stats()` writes 4 floats per layer into the caller
|
|
|
|
|
|
// buffer: [mean_abs, max_abs, dead_frac, saturating_frac]. Total = 16
|
|
|
|
|
|
// floats for 4 layers.
|
|
|
|
|
|
|
|
|
|
|
|
#include <emscripten.h>
|
|
|
|
|
|
#include <emscripten/emscripten.h>
|
|
|
|
|
|
|
|
|
|
|
|
#include <array>
|
|
|
|
|
|
#include <cstdint>
|
|
|
|
|
|
#include <cstring>
|
|
|
|
|
|
#include <span>
|
|
|
|
|
|
#include <string>
|
|
|
|
|
|
#include <string_view>
|
|
|
|
|
|
#include <vector>
|
|
|
|
|
|
|
|
|
|
|
|
// Engines.
|
|
|
|
|
|
#include "../engines/analysis.hpp"
|
|
|
|
|
|
#include "../engines/base.hpp"
|
|
|
|
|
|
#include "../engines/breakor.hpp"
|
|
|
|
|
|
#include "../engines/channel_strip.hpp"
|
|
|
|
|
|
#include "../engines/elysiamorf.hpp"
|
|
|
|
|
|
#include "../engines/memlcelium.hpp"
|
|
|
|
|
|
#include "../engines/paf_synth.hpp"
|
|
|
|
|
|
#include "../engines/verb_fx.hpp"
|
|
|
|
|
|
#include "../engines/xiasri.hpp"
|
|
|
|
|
|
|
|
|
|
|
|
// ML.
|
feat(pipeline)!: P4 core — input/output chains + curve catalog in nisps/
- nisps/pipeline/input_chain.hpp: faithful f32 port of the manifold input
pipeline (invert→deadzone→circular clamp→momentum-modulated zoom→centred
power→EMA→momentum update). Caller-supplied dt, internal accumulated
clock (no wall clock — deterministic; matches the P1 fixtures' clock
contract). Fixed-capacity velocity ring; serialisable state.
- nisps/pipeline/output_chain.hpp: curve→EMA→slew→freeze(+per-output mask)
chain, capacity-templated (browser cap 4096; firmware would use NOut).
- nisps/core/math.hpp: + centered_power(x, exponent) (both chains use it).
- bindings: nisps_pipeline_create/destroy, nisps_input_set_config(15-float
wire layout)/process/reset, nisps_output_set_config/set_freeze_mask/
process/reset, pipeline state save/load, nisps_curve_apply(+batch)
(ids 0-6 = Curve enum, 7 = centred power).
- parity v5 Stage 7: rational (transcendental-free) traces through both
chains (2 configs each) + full curve catalog — 1273 floats PASS, the
pipeline floats bit-identical native↔WASM.
- ctest test_pipeline.cpp: deadzone remap, circular clamp, zoom+freeze,
sticky anchor, frame-rate-independent EMA, momentum zoom-out/recovery,
state round-trip, slew, freeze gate/mask, reseed-on-length-change,
centred-power endpoints.
Part of one-core-engine-refactor P4; the manifold TS switch follows.
2026-07-18 11:52:53 +02:00
|
|
|
|
#include "../core/math.hpp"
|
feat(playground/wasm): WASM bridge between C++ core and SolidJS playground (meml-tgm)
Stream 7 wires nisps/ml + nisps/engines into the playground via Emscripten.
Highlights:
- nisps/wasm/bindings.cpp: flat C API per architecture.md §6.2. Fixed-arch
MLP<2, 10, 14, 18, 126>; engine string→type dispatch table with NoOp
fallback.
- scripts/build-wasm.sh: emcc invocation, MODULARIZE=1, exports listed
explicitly; produces playground/public/nisps.{js,wasm}.
- playground/src/ml/wasm-iml.ts: main-thread MLP host (sync inference,
sync training, RL ops, weights I/O, layer stats, localStorage).
- playground/src/ml/wasm-worker.ts: disposable Web Worker for off-thread
async training, owns its own WASM instance.
- playground/src/ml/dataset.ts: Float32Array-backed FIFO with sample-weight
modes (uniform/global/local/combined). Port of legacy dataset.js.
- playground/src/audio/engine-host.ts: AudioContext + AudioWorkletNode
lifecycle, with start/stop/setEngine/setParams.
- playground/src/audio/worklet/nisps-processor.ts: WASM-loading
AudioWorkletProcessor that runs engine.process_block per 128-sample
block. Loads its own WASM instance from main-thread-supplied bytes
(no fetch in worklet).
- playground/src/stores/ml-store.ts: wired stub methods to WasmIML
singleton; lazy initialize().
- playground/src/debug/probe.ts: window.__nisps now calls real WasmIML
via the store; lazy-init on first use.
Verified:
- bash scripts/build-wasm.sh succeeds (94 KB nisps.wasm)
- bun run typecheck OK
- bun run build OK (production bundle)
- vite dev server serves /nisps.{js,wasm} with COOP/COEP
Known limitation: WASM is fixed at one MLP shape. Multi-arch deferred —
documented in nisps/wasm/README.md.
2026-04-29 15:36:29 +02:00
|
|
|
|
#include "../core/types.hpp"
|
feat(wasm)!: P2.2 — nisps_ml_create honours dims; runtime-shaped browser MLP + reshape
Operator-approved ABI change (P2 stop-point). The WASM MLP is now
MLPCore<DynamicStorage>:
- nisps_ml_create(input, output, hidden[3], n, seed) honours its args;
non-positive/null fall back to the historical 32→[10,14,18]→126, so
every pre-P2 caller (manifold, worker, parity harness) stays
bit-identical. Invalid/oversized dims (>4096) → null.
- NEW nisps_ml_reshape(ml, in, out, hidden, n, spread): fresh net at the
new dims, warm-started via nisps/ml/warm_start.hpp (overlapping region
copied; rest keeps spread init); feedback controller re-created (state
resets — reset-on-reshape modal is the front-end contract). Failure
leaves the old net untouched.
- nisps_ml_describe(ml, out): takes the handle; null reports defaults.
- FeedbackController got the same storage split: algorithms in
FeedbackControllerCore<FbStorage>; FixedFeedbackStorage keeps firmware/
tests source-identical via the old alias; DynamicFeedbackStorage (one
arena) sizes to the runtime net. Firmware .text unchanged (122692).
- MLHandle: per-instance scratch vectors; dropped the dead 2MB
batch_out_scratch.
- TS: types.ts decls (+_nisps_ml_reshape), wasm-iml re-describes the
created instance, worker carries a shape-contract note for P2.3.
Verified: ctest 4/4 incl. new warm-start grow/shrink test; reshape ABI
smoke (dims honoured, overlap survives, invalid rejected, outputs
bounded); parity PASS unchanged (2.4e-7); lint clean; manifold 9 unit +
20 e2e green; firmware .text 122692 (+0.30% vs pre-P2 baseline).
2026-07-14 03:38:06 +02:00
|
|
|
|
#include "../ml/dynamic_storage.hpp"
|
2026-06-28 04:14:12 +02:00
|
|
|
|
#include "../ml/feedback.hpp"
|
feat(ml)!: P3 core — geometric dislike in nisps/, jolt/OU + geo ABI
Geometric dislike (rl-feedback-design §2.1/§4; upstream InterfaceRL @
0a541cc ported verbatim, constants included):
- nisps/ml/replay.hpp: ReplayView over storage-owned buffers — deepen-or-
store negatives (dedup 0.05, clamp -16), k-NN positive centroid with
deterministic index tie-break + fixed accumulation order, proportional
decay (0.0025*max(|r|,1)) + eviction, order-preserving compaction.
- nisps/ml/geo_push.hpp: push-away target (pushStep clamp(|avgNeg|,.25,1)
*0.5, taper /(1+len), useRandom on len<=1e-4 via nisps::Rng — the single
deliberate divergence from libc rand()), negLRRatio 0.5-0.4*negFraction.
- mlp.hpp: train_targets(input, computed-target, lr, out_mask) — trains
toward computed targets (negative lr = cold-start train-away); solo/
focus gating zeroes masked derivs.
- feedback.hpp: AvoidStyle {Geometric (new default), Diffuse (legacy
move_weights, kept for A/B)}; dislike_geometric() collapses upstream's
press+optimise into one synchronous call; on_up in geometric Avoid
feeds the positive centroid; dislike-multiplier bookkeeping. Storage
gains replay buffers (Fixed: ReplayCap=32 firmware default ≈ +8KB SRAM;
Dynamic arena: cap 64).
- bindings: nisps_ml_feedback_{dislike_geometric,store_positive,
positive_count,negative_count,set_avoid_style} + P3.2 jolt/OU ABI:
nisps_ml_jolt_{press,step,release,active,lr_scale,tick_lr_ramp},
nisps_ml_explore_{intensity,get_intensity,apply} (OUNoise<4096>
over-provisioned; same code the firmware ModeBase runs).
- parity v4: Stage 6 scripted geometric session (2 likes → 2 dislikes,
f32-exact heard vectors via Math.fround) — 961 floats PASS at 2.4e-7.
- tests: test_mlp_geo_dislike.cpp (replay dedup/deepen/clamp, centroid
tie-break, push direction/taper/mask/clamp, cold-start inertness +
train-away, determinism, Diffuse legacy); legacy Avoid test pinned to
Diffuse per the ADR's deliberate-break note.
Firmware: PAFSynth .text/.data unchanged (geometric path not referenced
by current glue). NOTE: discovered pre-existing bug 10c3e55c — the
explore/place wiring is linker-GC'd out of the PAFSynth ELF (predates
this refactor; evidence in the ergo task).
2026-07-14 04:16:21 +02:00
|
|
|
|
#include "../ml/jolt.hpp"
|
feat(playground/wasm): WASM bridge between C++ core and SolidJS playground (meml-tgm)
Stream 7 wires nisps/ml + nisps/engines into the playground via Emscripten.
Highlights:
- nisps/wasm/bindings.cpp: flat C API per architecture.md §6.2. Fixed-arch
MLP<2, 10, 14, 18, 126>; engine string→type dispatch table with NoOp
fallback.
- scripts/build-wasm.sh: emcc invocation, MODULARIZE=1, exports listed
explicitly; produces playground/public/nisps.{js,wasm}.
- playground/src/ml/wasm-iml.ts: main-thread MLP host (sync inference,
sync training, RL ops, weights I/O, layer stats, localStorage).
- playground/src/ml/wasm-worker.ts: disposable Web Worker for off-thread
async training, owns its own WASM instance.
- playground/src/ml/dataset.ts: Float32Array-backed FIFO with sample-weight
modes (uniform/global/local/combined). Port of legacy dataset.js.
- playground/src/audio/engine-host.ts: AudioContext + AudioWorkletNode
lifecycle, with start/stop/setEngine/setParams.
- playground/src/audio/worklet/nisps-processor.ts: WASM-loading
AudioWorkletProcessor that runs engine.process_block per 128-sample
block. Loads its own WASM instance from main-thread-supplied bytes
(no fetch in worklet).
- playground/src/stores/ml-store.ts: wired stub methods to WasmIML
singleton; lazy initialize().
- playground/src/debug/probe.ts: window.__nisps now calls real WasmIML
via the store; lazy-init on first use.
Verified:
- bash scripts/build-wasm.sh succeeds (94 KB nisps.wasm)
- bun run typecheck OK
- bun run build OK (production bundle)
- vite dev server serves /nisps.{js,wasm} with COOP/COEP
Known limitation: WASM is fixed at one MLP shape. Multi-arch deferred —
documented in nisps/wasm/README.md.
2026-04-29 15:36:29 +02:00
|
|
|
|
#include "../ml/mlp.hpp"
|
feat(ml)!: P3 core — geometric dislike in nisps/, jolt/OU + geo ABI
Geometric dislike (rl-feedback-design §2.1/§4; upstream InterfaceRL @
0a541cc ported verbatim, constants included):
- nisps/ml/replay.hpp: ReplayView over storage-owned buffers — deepen-or-
store negatives (dedup 0.05, clamp -16), k-NN positive centroid with
deterministic index tie-break + fixed accumulation order, proportional
decay (0.0025*max(|r|,1)) + eviction, order-preserving compaction.
- nisps/ml/geo_push.hpp: push-away target (pushStep clamp(|avgNeg|,.25,1)
*0.5, taper /(1+len), useRandom on len<=1e-4 via nisps::Rng — the single
deliberate divergence from libc rand()), negLRRatio 0.5-0.4*negFraction.
- mlp.hpp: train_targets(input, computed-target, lr, out_mask) — trains
toward computed targets (negative lr = cold-start train-away); solo/
focus gating zeroes masked derivs.
- feedback.hpp: AvoidStyle {Geometric (new default), Diffuse (legacy
move_weights, kept for A/B)}; dislike_geometric() collapses upstream's
press+optimise into one synchronous call; on_up in geometric Avoid
feeds the positive centroid; dislike-multiplier bookkeeping. Storage
gains replay buffers (Fixed: ReplayCap=32 firmware default ≈ +8KB SRAM;
Dynamic arena: cap 64).
- bindings: nisps_ml_feedback_{dislike_geometric,store_positive,
positive_count,negative_count,set_avoid_style} + P3.2 jolt/OU ABI:
nisps_ml_jolt_{press,step,release,active,lr_scale,tick_lr_ramp},
nisps_ml_explore_{intensity,get_intensity,apply} (OUNoise<4096>
over-provisioned; same code the firmware ModeBase runs).
- parity v4: Stage 6 scripted geometric session (2 likes → 2 dislikes,
f32-exact heard vectors via Math.fround) — 961 floats PASS at 2.4e-7.
- tests: test_mlp_geo_dislike.cpp (replay dedup/deepen/clamp, centroid
tie-break, push direction/taper/mask/clamp, cold-start inertness +
train-away, determinism, Diffuse legacy); legacy Avoid test pinned to
Diffuse per the ADR's deliberate-break note.
Firmware: PAFSynth .text/.data unchanged (geometric path not referenced
by current glue). NOTE: discovered pre-existing bug 10c3e55c — the
explore/place wiring is linker-GC'd out of the PAFSynth ELF (predates
this refactor; evidence in the ergo task).
2026-07-14 04:16:21 +02:00
|
|
|
|
#include "../ml/ou_noise.hpp"
|
feat(playground/wasm): WASM bridge between C++ core and SolidJS playground (meml-tgm)
Stream 7 wires nisps/ml + nisps/engines into the playground via Emscripten.
Highlights:
- nisps/wasm/bindings.cpp: flat C API per architecture.md §6.2. Fixed-arch
MLP<2, 10, 14, 18, 126>; engine string→type dispatch table with NoOp
fallback.
- scripts/build-wasm.sh: emcc invocation, MODULARIZE=1, exports listed
explicitly; produces playground/public/nisps.{js,wasm}.
- playground/src/ml/wasm-iml.ts: main-thread MLP host (sync inference,
sync training, RL ops, weights I/O, layer stats, localStorage).
- playground/src/ml/wasm-worker.ts: disposable Web Worker for off-thread
async training, owns its own WASM instance.
- playground/src/ml/dataset.ts: Float32Array-backed FIFO with sample-weight
modes (uniform/global/local/combined). Port of legacy dataset.js.
- playground/src/audio/engine-host.ts: AudioContext + AudioWorkletNode
lifecycle, with start/stop/setEngine/setParams.
- playground/src/audio/worklet/nisps-processor.ts: WASM-loading
AudioWorkletProcessor that runs engine.process_block per 128-sample
block. Loads its own WASM instance from main-thread-supplied bytes
(no fetch in worklet).
- playground/src/stores/ml-store.ts: wired stub methods to WasmIML
singleton; lazy initialize().
- playground/src/debug/probe.ts: window.__nisps now calls real WasmIML
via the store; lazy-init on first use.
Verified:
- bash scripts/build-wasm.sh succeeds (94 KB nisps.wasm)
- bun run typecheck OK
- bun run build OK (production bundle)
- vite dev server serves /nisps.{js,wasm} with COOP/COEP
Known limitation: WASM is fixed at one MLP shape. Multi-arch deferred —
documented in nisps/wasm/README.md.
2026-04-29 15:36:29 +02:00
|
|
|
|
#include "../ml/stats.hpp"
|
feat(wasm)!: P2.2 — nisps_ml_create honours dims; runtime-shaped browser MLP + reshape
Operator-approved ABI change (P2 stop-point). The WASM MLP is now
MLPCore<DynamicStorage>:
- nisps_ml_create(input, output, hidden[3], n, seed) honours its args;
non-positive/null fall back to the historical 32→[10,14,18]→126, so
every pre-P2 caller (manifold, worker, parity harness) stays
bit-identical. Invalid/oversized dims (>4096) → null.
- NEW nisps_ml_reshape(ml, in, out, hidden, n, spread): fresh net at the
new dims, warm-started via nisps/ml/warm_start.hpp (overlapping region
copied; rest keeps spread init); feedback controller re-created (state
resets — reset-on-reshape modal is the front-end contract). Failure
leaves the old net untouched.
- nisps_ml_describe(ml, out): takes the handle; null reports defaults.
- FeedbackController got the same storage split: algorithms in
FeedbackControllerCore<FbStorage>; FixedFeedbackStorage keeps firmware/
tests source-identical via the old alias; DynamicFeedbackStorage (one
arena) sizes to the runtime net. Firmware .text unchanged (122692).
- MLHandle: per-instance scratch vectors; dropped the dead 2MB
batch_out_scratch.
- TS: types.ts decls (+_nisps_ml_reshape), wasm-iml re-describes the
created instance, worker carries a shape-contract note for P2.3.
Verified: ctest 4/4 incl. new warm-start grow/shrink test; reshape ABI
smoke (dims honoured, overlap survives, invalid rejected, outputs
bounded); parity PASS unchanged (2.4e-7); lint clean; manifold 9 unit +
20 e2e green; firmware .text 122692 (+0.30% vs pre-P2 baseline).
2026-07-14 03:38:06 +02:00
|
|
|
|
#include "../ml/warm_start.hpp"
|
feat(playground/wasm): WASM bridge between C++ core and SolidJS playground (meml-tgm)
Stream 7 wires nisps/ml + nisps/engines into the playground via Emscripten.
Highlights:
- nisps/wasm/bindings.cpp: flat C API per architecture.md §6.2. Fixed-arch
MLP<2, 10, 14, 18, 126>; engine string→type dispatch table with NoOp
fallback.
- scripts/build-wasm.sh: emcc invocation, MODULARIZE=1, exports listed
explicitly; produces playground/public/nisps.{js,wasm}.
- playground/src/ml/wasm-iml.ts: main-thread MLP host (sync inference,
sync training, RL ops, weights I/O, layer stats, localStorage).
- playground/src/ml/wasm-worker.ts: disposable Web Worker for off-thread
async training, owns its own WASM instance.
- playground/src/ml/dataset.ts: Float32Array-backed FIFO with sample-weight
modes (uniform/global/local/combined). Port of legacy dataset.js.
- playground/src/audio/engine-host.ts: AudioContext + AudioWorkletNode
lifecycle, with start/stop/setEngine/setParams.
- playground/src/audio/worklet/nisps-processor.ts: WASM-loading
AudioWorkletProcessor that runs engine.process_block per 128-sample
block. Loads its own WASM instance from main-thread-supplied bytes
(no fetch in worklet).
- playground/src/stores/ml-store.ts: wired stub methods to WasmIML
singleton; lazy initialize().
- playground/src/debug/probe.ts: window.__nisps now calls real WasmIML
via the store; lazy-init on first use.
Verified:
- bash scripts/build-wasm.sh succeeds (94 KB nisps.wasm)
- bun run typecheck OK
- bun run build OK (production bundle)
- vite dev server serves /nisps.{js,wasm} with COOP/COEP
Known limitation: WASM is fixed at one MLP shape. Multi-arch deferred —
documented in nisps/wasm/README.md.
2026-04-29 15:36:29 +02:00
|
|
|
|
|
feat(pipeline)!: P4 core — input/output chains + curve catalog in nisps/
- nisps/pipeline/input_chain.hpp: faithful f32 port of the manifold input
pipeline (invert→deadzone→circular clamp→momentum-modulated zoom→centred
power→EMA→momentum update). Caller-supplied dt, internal accumulated
clock (no wall clock — deterministic; matches the P1 fixtures' clock
contract). Fixed-capacity velocity ring; serialisable state.
- nisps/pipeline/output_chain.hpp: curve→EMA→slew→freeze(+per-output mask)
chain, capacity-templated (browser cap 4096; firmware would use NOut).
- nisps/core/math.hpp: + centered_power(x, exponent) (both chains use it).
- bindings: nisps_pipeline_create/destroy, nisps_input_set_config(15-float
wire layout)/process/reset, nisps_output_set_config/set_freeze_mask/
process/reset, pipeline state save/load, nisps_curve_apply(+batch)
(ids 0-6 = Curve enum, 7 = centred power).
- parity v5 Stage 7: rational (transcendental-free) traces through both
chains (2 configs each) + full curve catalog — 1273 floats PASS, the
pipeline floats bit-identical native↔WASM.
- ctest test_pipeline.cpp: deadzone remap, circular clamp, zoom+freeze,
sticky anchor, frame-rate-independent EMA, momentum zoom-out/recovery,
state round-trip, slew, freeze gate/mask, reseed-on-length-change,
centred-power endpoints.
Part of one-core-engine-refactor P4; the manifold TS switch follows.
2026-07-18 11:52:53 +02:00
|
|
|
|
// Pipelines (one-core-engine P4).
|
|
|
|
|
|
#include "../pipeline/input_chain.hpp"
|
|
|
|
|
|
#include "../pipeline/output_chain.hpp"
|
|
|
|
|
|
|
feat(playground/wasm): WASM bridge between C++ core and SolidJS playground (meml-tgm)
Stream 7 wires nisps/ml + nisps/engines into the playground via Emscripten.
Highlights:
- nisps/wasm/bindings.cpp: flat C API per architecture.md §6.2. Fixed-arch
MLP<2, 10, 14, 18, 126>; engine string→type dispatch table with NoOp
fallback.
- scripts/build-wasm.sh: emcc invocation, MODULARIZE=1, exports listed
explicitly; produces playground/public/nisps.{js,wasm}.
- playground/src/ml/wasm-iml.ts: main-thread MLP host (sync inference,
sync training, RL ops, weights I/O, layer stats, localStorage).
- playground/src/ml/wasm-worker.ts: disposable Web Worker for off-thread
async training, owns its own WASM instance.
- playground/src/ml/dataset.ts: Float32Array-backed FIFO with sample-weight
modes (uniform/global/local/combined). Port of legacy dataset.js.
- playground/src/audio/engine-host.ts: AudioContext + AudioWorkletNode
lifecycle, with start/stop/setEngine/setParams.
- playground/src/audio/worklet/nisps-processor.ts: WASM-loading
AudioWorkletProcessor that runs engine.process_block per 128-sample
block. Loads its own WASM instance from main-thread-supplied bytes
(no fetch in worklet).
- playground/src/stores/ml-store.ts: wired stub methods to WasmIML
singleton; lazy initialize().
- playground/src/debug/probe.ts: window.__nisps now calls real WasmIML
via the store; lazy-init on first use.
Verified:
- bash scripts/build-wasm.sh succeeds (94 KB nisps.wasm)
- bun run typecheck OK
- bun run build OK (production bundle)
- vite dev server serves /nisps.{js,wasm} with COOP/COEP
Known limitation: WASM is fixed at one MLP shape. Multi-arch deferred —
documented in nisps/wasm/README.md.
2026-04-29 15:36:29 +02:00
|
|
|
|
namespace {
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// ML side
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
feat(wasm)!: P2.2 — nisps_ml_create honours dims; runtime-shaped browser MLP + reshape
Operator-approved ABI change (P2 stop-point). The WASM MLP is now
MLPCore<DynamicStorage>:
- nisps_ml_create(input, output, hidden[3], n, seed) honours its args;
non-positive/null fall back to the historical 32→[10,14,18]→126, so
every pre-P2 caller (manifold, worker, parity harness) stays
bit-identical. Invalid/oversized dims (>4096) → null.
- NEW nisps_ml_reshape(ml, in, out, hidden, n, spread): fresh net at the
new dims, warm-started via nisps/ml/warm_start.hpp (overlapping region
copied; rest keeps spread init); feedback controller re-created (state
resets — reset-on-reshape modal is the front-end contract). Failure
leaves the old net untouched.
- nisps_ml_describe(ml, out): takes the handle; null reports defaults.
- FeedbackController got the same storage split: algorithms in
FeedbackControllerCore<FbStorage>; FixedFeedbackStorage keeps firmware/
tests source-identical via the old alias; DynamicFeedbackStorage (one
arena) sizes to the runtime net. Firmware .text unchanged (122692).
- MLHandle: per-instance scratch vectors; dropped the dead 2MB
batch_out_scratch.
- TS: types.ts decls (+_nisps_ml_reshape), wasm-iml re-describes the
created instance, worker carries a shape-contract note for P2.3.
Verified: ctest 4/4 incl. new warm-start grow/shrink test; reshape ABI
smoke (dims honoured, overlap survives, invalid rejected, outputs
bounded); parity PASS unchanged (2.4e-7); lint clean; manifold 9 unit +
20 e2e green; firmware .text 122692 (+0.30% vs pre-P2 baseline).
2026-07-14 03:38:06 +02:00
|
|
|
|
// Historical compiled defaults. Non-positive / missing create() args fall
|
|
|
|
|
|
// back to these, keeping every pre-P2 caller bit-identical.
|
|
|
|
|
|
// * 32 inputs — MAX composed input axes the manifold front-end
|
|
|
|
|
|
// feeds (matches MAX_AXES in manifold/src/inputs/input-layer.ts).
|
feat(playground/wasm): WASM bridge between C++ core and SolidJS playground (meml-tgm)
Stream 7 wires nisps/ml + nisps/engines into the playground via Emscripten.
Highlights:
- nisps/wasm/bindings.cpp: flat C API per architecture.md §6.2. Fixed-arch
MLP<2, 10, 14, 18, 126>; engine string→type dispatch table with NoOp
fallback.
- scripts/build-wasm.sh: emcc invocation, MODULARIZE=1, exports listed
explicitly; produces playground/public/nisps.{js,wasm}.
- playground/src/ml/wasm-iml.ts: main-thread MLP host (sync inference,
sync training, RL ops, weights I/O, layer stats, localStorage).
- playground/src/ml/wasm-worker.ts: disposable Web Worker for off-thread
async training, owns its own WASM instance.
- playground/src/ml/dataset.ts: Float32Array-backed FIFO with sample-weight
modes (uniform/global/local/combined). Port of legacy dataset.js.
- playground/src/audio/engine-host.ts: AudioContext + AudioWorkletNode
lifecycle, with start/stop/setEngine/setParams.
- playground/src/audio/worklet/nisps-processor.ts: WASM-loading
AudioWorkletProcessor that runs engine.process_block per 128-sample
block. Loads its own WASM instance from main-thread-supplied bytes
(no fetch in worklet).
- playground/src/stores/ml-store.ts: wired stub methods to WasmIML
singleton; lazy initialize().
- playground/src/debug/probe.ts: window.__nisps now calls real WasmIML
via the store; lazy-init on first use.
Verified:
- bash scripts/build-wasm.sh succeeds (94 KB nisps.wasm)
- bun run typecheck OK
- bun run build OK (production bundle)
- vite dev server serves /nisps.{js,wasm} with COOP/COEP
Known limitation: WASM is fixed at one MLP shape. Multi-arch deferred —
documented in nisps/wasm/README.md.
2026-04-29 15:36:29 +02:00
|
|
|
|
// * [10, 14, 18] hidden — covers the largest schema layouts in
|
feat(wasm)!: P2.2 — nisps_ml_create honours dims; runtime-shaped browser MLP + reshape
Operator-approved ABI change (P2 stop-point). The WASM MLP is now
MLPCore<DynamicStorage>:
- nisps_ml_create(input, output, hidden[3], n, seed) honours its args;
non-positive/null fall back to the historical 32→[10,14,18]→126, so
every pre-P2 caller (manifold, worker, parity harness) stays
bit-identical. Invalid/oversized dims (>4096) → null.
- NEW nisps_ml_reshape(ml, in, out, hidden, n, spread): fresh net at the
new dims, warm-started via nisps/ml/warm_start.hpp (overlapping region
copied; rest keeps spread init); feedback controller re-created (state
resets — reset-on-reshape modal is the front-end contract). Failure
leaves the old net untouched.
- nisps_ml_describe(ml, out): takes the handle; null reports defaults.
- FeedbackController got the same storage split: algorithms in
FeedbackControllerCore<FbStorage>; FixedFeedbackStorage keeps firmware/
tests source-identical via the old alias; DynamicFeedbackStorage (one
arena) sizes to the runtime net. Firmware .text unchanged (122692).
- MLHandle: per-instance scratch vectors; dropped the dead 2MB
batch_out_scratch.
- TS: types.ts decls (+_nisps_ml_reshape), wasm-iml re-describes the
created instance, worker carries a shape-contract note for P2.3.
Verified: ctest 4/4 incl. new warm-start grow/shrink test; reshape ABI
smoke (dims honoured, overlap survives, invalid rejected, outputs
bounded); parity PASS unchanged (2.4e-7); lint clean; manifold 9 unit +
20 e2e green; firmware .text 122692 (+0.30% vs pre-P2 baseline).
2026-07-14 03:38:06 +02:00
|
|
|
|
// `schemas/modes/*.json`.
|
feat(playground/wasm): WASM bridge between C++ core and SolidJS playground (meml-tgm)
Stream 7 wires nisps/ml + nisps/engines into the playground via Emscripten.
Highlights:
- nisps/wasm/bindings.cpp: flat C API per architecture.md §6.2. Fixed-arch
MLP<2, 10, 14, 18, 126>; engine string→type dispatch table with NoOp
fallback.
- scripts/build-wasm.sh: emcc invocation, MODULARIZE=1, exports listed
explicitly; produces playground/public/nisps.{js,wasm}.
- playground/src/ml/wasm-iml.ts: main-thread MLP host (sync inference,
sync training, RL ops, weights I/O, layer stats, localStorage).
- playground/src/ml/wasm-worker.ts: disposable Web Worker for off-thread
async training, owns its own WASM instance.
- playground/src/ml/dataset.ts: Float32Array-backed FIFO with sample-weight
modes (uniform/global/local/combined). Port of legacy dataset.js.
- playground/src/audio/engine-host.ts: AudioContext + AudioWorkletNode
lifecycle, with start/stop/setEngine/setParams.
- playground/src/audio/worklet/nisps-processor.ts: WASM-loading
AudioWorkletProcessor that runs engine.process_block per 128-sample
block. Loads its own WASM instance from main-thread-supplied bytes
(no fetch in worklet).
- playground/src/stores/ml-store.ts: wired stub methods to WasmIML
singleton; lazy initialize().
- playground/src/debug/probe.ts: window.__nisps now calls real WasmIML
via the store; lazy-init on first use.
Verified:
- bash scripts/build-wasm.sh succeeds (94 KB nisps.wasm)
- bun run typecheck OK
- bun run build OK (production bundle)
- vite dev server serves /nisps.{js,wasm} with COOP/COEP
Known limitation: WASM is fixed at one MLP shape. Multi-arch deferred —
documented in nisps/wasm/README.md.
2026-04-29 15:36:29 +02:00
|
|
|
|
// * 126 outputs — enough for the C15 mode and any current schema.
|
feat(wasm)!: P2.2 — nisps_ml_create honours dims; runtime-shaped browser MLP + reshape
Operator-approved ABI change (P2 stop-point). The WASM MLP is now
MLPCore<DynamicStorage>:
- nisps_ml_create(input, output, hidden[3], n, seed) honours its args;
non-positive/null fall back to the historical 32→[10,14,18]→126, so
every pre-P2 caller (manifold, worker, parity harness) stays
bit-identical. Invalid/oversized dims (>4096) → null.
- NEW nisps_ml_reshape(ml, in, out, hidden, n, spread): fresh net at the
new dims, warm-started via nisps/ml/warm_start.hpp (overlapping region
copied; rest keeps spread init); feedback controller re-created (state
resets — reset-on-reshape modal is the front-end contract). Failure
leaves the old net untouched.
- nisps_ml_describe(ml, out): takes the handle; null reports defaults.
- FeedbackController got the same storage split: algorithms in
FeedbackControllerCore<FbStorage>; FixedFeedbackStorage keeps firmware/
tests source-identical via the old alias; DynamicFeedbackStorage (one
arena) sizes to the runtime net. Firmware .text unchanged (122692).
- MLHandle: per-instance scratch vectors; dropped the dead 2MB
batch_out_scratch.
- TS: types.ts decls (+_nisps_ml_reshape), wasm-iml re-describes the
created instance, worker carries a shape-contract note for P2.3.
Verified: ctest 4/4 incl. new warm-start grow/shrink test; reshape ABI
smoke (dims honoured, overlap survives, invalid rejected, outputs
bounded); parity PASS unchanged (2.4e-7); lint clean; manifold 9 unit +
20 e2e green; firmware .text 122692 (+0.30% vs pre-P2 baseline).
2026-07-14 03:38:06 +02:00
|
|
|
|
constexpr std::size_t kDefaultInputs = 32u;
|
|
|
|
|
|
constexpr std::size_t kDefaultHidden[3] = {10u, 14u, 18u};
|
|
|
|
|
|
constexpr std::size_t kDefaultOutputs = 126u;
|
|
|
|
|
|
// Sanity ceiling per dimension — create/reshape reject anything larger.
|
|
|
|
|
|
constexpr std::size_t kMaxDim = 4096u;
|
|
|
|
|
|
|
|
|
|
|
|
constexpr std::uint64_t kFeedbackSalt = 0xFEEDBACC0DEull;
|
feat(ml)!: P3 core — geometric dislike in nisps/, jolt/OU + geo ABI
Geometric dislike (rl-feedback-design §2.1/§4; upstream InterfaceRL @
0a541cc ported verbatim, constants included):
- nisps/ml/replay.hpp: ReplayView over storage-owned buffers — deepen-or-
store negatives (dedup 0.05, clamp -16), k-NN positive centroid with
deterministic index tie-break + fixed accumulation order, proportional
decay (0.0025*max(|r|,1)) + eviction, order-preserving compaction.
- nisps/ml/geo_push.hpp: push-away target (pushStep clamp(|avgNeg|,.25,1)
*0.5, taper /(1+len), useRandom on len<=1e-4 via nisps::Rng — the single
deliberate divergence from libc rand()), negLRRatio 0.5-0.4*negFraction.
- mlp.hpp: train_targets(input, computed-target, lr, out_mask) — trains
toward computed targets (negative lr = cold-start train-away); solo/
focus gating zeroes masked derivs.
- feedback.hpp: AvoidStyle {Geometric (new default), Diffuse (legacy
move_weights, kept for A/B)}; dislike_geometric() collapses upstream's
press+optimise into one synchronous call; on_up in geometric Avoid
feeds the positive centroid; dislike-multiplier bookkeeping. Storage
gains replay buffers (Fixed: ReplayCap=32 firmware default ≈ +8KB SRAM;
Dynamic arena: cap 64).
- bindings: nisps_ml_feedback_{dislike_geometric,store_positive,
positive_count,negative_count,set_avoid_style} + P3.2 jolt/OU ABI:
nisps_ml_jolt_{press,step,release,active,lr_scale,tick_lr_ramp},
nisps_ml_explore_{intensity,get_intensity,apply} (OUNoise<4096>
over-provisioned; same code the firmware ModeBase runs).
- parity v4: Stage 6 scripted geometric session (2 likes → 2 dislikes,
f32-exact heard vectors via Math.fround) — 961 floats PASS at 2.4e-7.
- tests: test_mlp_geo_dislike.cpp (replay dedup/deepen/clamp, centroid
tie-break, push direction/taper/mask/clamp, cold-start inertness +
train-away, determinism, Diffuse legacy); legacy Avoid test pinned to
Diffuse per the ADR's deliberate-break note.
Firmware: PAFSynth .text/.data unchanged (geometric path not referenced
by current glue). NOTE: discovered pre-existing bug 10c3e55c — the
explore/place wiring is linker-GC'd out of the PAFSynth ELF (predates
this refactor; evidence in the ergo task).
2026-07-14 04:16:21 +02:00
|
|
|
|
// Distinct salts keep the jolt/OU RNG streams independent of the MLP's and
|
|
|
|
|
|
// the feedback controller's (mirrors the firmware ModeBase seeding).
|
|
|
|
|
|
constexpr std::uint64_t kJoltSalt = 0xB01DFACEull;
|
|
|
|
|
|
constexpr std::uint64_t kOUSalt = 0x0DDBA11ull;
|
feat(wasm)!: P2.2 — nisps_ml_create honours dims; runtime-shaped browser MLP + reshape
Operator-approved ABI change (P2 stop-point). The WASM MLP is now
MLPCore<DynamicStorage>:
- nisps_ml_create(input, output, hidden[3], n, seed) honours its args;
non-positive/null fall back to the historical 32→[10,14,18]→126, so
every pre-P2 caller (manifold, worker, parity harness) stays
bit-identical. Invalid/oversized dims (>4096) → null.
- NEW nisps_ml_reshape(ml, in, out, hidden, n, spread): fresh net at the
new dims, warm-started via nisps/ml/warm_start.hpp (overlapping region
copied; rest keeps spread init); feedback controller re-created (state
resets — reset-on-reshape modal is the front-end contract). Failure
leaves the old net untouched.
- nisps_ml_describe(ml, out): takes the handle; null reports defaults.
- FeedbackController got the same storage split: algorithms in
FeedbackControllerCore<FbStorage>; FixedFeedbackStorage keeps firmware/
tests source-identical via the old alias; DynamicFeedbackStorage (one
arena) sizes to the runtime net. Firmware .text unchanged (122692).
- MLHandle: per-instance scratch vectors; dropped the dead 2MB
batch_out_scratch.
- TS: types.ts decls (+_nisps_ml_reshape), wasm-iml re-describes the
created instance, worker carries a shape-contract note for P2.3.
Verified: ctest 4/4 incl. new warm-start grow/shrink test; reshape ABI
smoke (dims honoured, overlap survives, invalid rejected, outputs
bounded); parity PASS unchanged (2.4e-7); lint clean; manifold 9 unit +
20 e2e green; firmware .text 122692 (+0.30% vs pre-P2 baseline).
2026-07-14 03:38:06 +02:00
|
|
|
|
|
|
|
|
|
|
using BrowserMLP = nisps::ml::MLPCore<nisps::ml::DynamicStorage>;
|
|
|
|
|
|
using BrowserFeedback =
|
|
|
|
|
|
nisps::ml::FeedbackControllerCore<nisps::ml::DynamicFeedbackStorage>;
|
feat(playground/wasm): WASM bridge between C++ core and SolidJS playground (meml-tgm)
Stream 7 wires nisps/ml + nisps/engines into the playground via Emscripten.
Highlights:
- nisps/wasm/bindings.cpp: flat C API per architecture.md §6.2. Fixed-arch
MLP<2, 10, 14, 18, 126>; engine string→type dispatch table with NoOp
fallback.
- scripts/build-wasm.sh: emcc invocation, MODULARIZE=1, exports listed
explicitly; produces playground/public/nisps.{js,wasm}.
- playground/src/ml/wasm-iml.ts: main-thread MLP host (sync inference,
sync training, RL ops, weights I/O, layer stats, localStorage).
- playground/src/ml/wasm-worker.ts: disposable Web Worker for off-thread
async training, owns its own WASM instance.
- playground/src/ml/dataset.ts: Float32Array-backed FIFO with sample-weight
modes (uniform/global/local/combined). Port of legacy dataset.js.
- playground/src/audio/engine-host.ts: AudioContext + AudioWorkletNode
lifecycle, with start/stop/setEngine/setParams.
- playground/src/audio/worklet/nisps-processor.ts: WASM-loading
AudioWorkletProcessor that runs engine.process_block per 128-sample
block. Loads its own WASM instance from main-thread-supplied bytes
(no fetch in worklet).
- playground/src/stores/ml-store.ts: wired stub methods to WasmIML
singleton; lazy initialize().
- playground/src/debug/probe.ts: window.__nisps now calls real WasmIML
via the store; lazy-init on first use.
Verified:
- bash scripts/build-wasm.sh succeeds (94 KB nisps.wasm)
- bun run typecheck OK
- bun run build OK (production bundle)
- vite dev server serves /nisps.{js,wasm} with COOP/COEP
Known limitation: WASM is fixed at one MLP shape. Multi-arch deferred —
documented in nisps/wasm/README.md.
2026-04-29 15:36:29 +02:00
|
|
|
|
|
feat(wasm)!: P2.2 — nisps_ml_create honours dims; runtime-shaped browser MLP + reshape
Operator-approved ABI change (P2 stop-point). The WASM MLP is now
MLPCore<DynamicStorage>:
- nisps_ml_create(input, output, hidden[3], n, seed) honours its args;
non-positive/null fall back to the historical 32→[10,14,18]→126, so
every pre-P2 caller (manifold, worker, parity harness) stays
bit-identical. Invalid/oversized dims (>4096) → null.
- NEW nisps_ml_reshape(ml, in, out, hidden, n, spread): fresh net at the
new dims, warm-started via nisps/ml/warm_start.hpp (overlapping region
copied; rest keeps spread init); feedback controller re-created (state
resets — reset-on-reshape modal is the front-end contract). Failure
leaves the old net untouched.
- nisps_ml_describe(ml, out): takes the handle; null reports defaults.
- FeedbackController got the same storage split: algorithms in
FeedbackControllerCore<FbStorage>; FixedFeedbackStorage keeps firmware/
tests source-identical via the old alias; DynamicFeedbackStorage (one
arena) sizes to the runtime net. Firmware .text unchanged (122692).
- MLHandle: per-instance scratch vectors; dropped the dead 2MB
batch_out_scratch.
- TS: types.ts decls (+_nisps_ml_reshape), wasm-iml re-describes the
created instance, worker carries a shape-contract note for P2.3.
Verified: ctest 4/4 incl. new warm-start grow/shrink test; reshape ABI
smoke (dims honoured, overlap survives, invalid rejected, outputs
bounded); parity PASS unchanged (2.4e-7); lint clean; manifold 9 unit +
20 e2e green; firmware .text 122692 (+0.30% vs pre-P2 baseline).
2026-07-14 03:38:06 +02:00
|
|
|
|
constexpr std::size_t kFeedbackUndoDepth = 4u;
|
feat(ml)!: P3 core — geometric dislike in nisps/, jolt/OU + geo ABI
Geometric dislike (rl-feedback-design §2.1/§4; upstream InterfaceRL @
0a541cc ported verbatim, constants included):
- nisps/ml/replay.hpp: ReplayView over storage-owned buffers — deepen-or-
store negatives (dedup 0.05, clamp -16), k-NN positive centroid with
deterministic index tie-break + fixed accumulation order, proportional
decay (0.0025*max(|r|,1)) + eviction, order-preserving compaction.
- nisps/ml/geo_push.hpp: push-away target (pushStep clamp(|avgNeg|,.25,1)
*0.5, taper /(1+len), useRandom on len<=1e-4 via nisps::Rng — the single
deliberate divergence from libc rand()), negLRRatio 0.5-0.4*negFraction.
- mlp.hpp: train_targets(input, computed-target, lr, out_mask) — trains
toward computed targets (negative lr = cold-start train-away); solo/
focus gating zeroes masked derivs.
- feedback.hpp: AvoidStyle {Geometric (new default), Diffuse (legacy
move_weights, kept for A/B)}; dislike_geometric() collapses upstream's
press+optimise into one synchronous call; on_up in geometric Avoid
feeds the positive centroid; dislike-multiplier bookkeeping. Storage
gains replay buffers (Fixed: ReplayCap=32 firmware default ≈ +8KB SRAM;
Dynamic arena: cap 64).
- bindings: nisps_ml_feedback_{dislike_geometric,store_positive,
positive_count,negative_count,set_avoid_style} + P3.2 jolt/OU ABI:
nisps_ml_jolt_{press,step,release,active,lr_scale,tick_lr_ramp},
nisps_ml_explore_{intensity,get_intensity,apply} (OUNoise<4096>
over-provisioned; same code the firmware ModeBase runs).
- parity v4: Stage 6 scripted geometric session (2 likes → 2 dislikes,
f32-exact heard vectors via Math.fround) — 961 floats PASS at 2.4e-7.
- tests: test_mlp_geo_dislike.cpp (replay dedup/deepen/clamp, centroid
tie-break, push direction/taper/mask/clamp, cold-start inertness +
train-away, determinism, Diffuse legacy); legacy Avoid test pinned to
Diffuse per the ADR's deliberate-break note.
Firmware: PAFSynth .text/.data unchanged (geometric path not referenced
by current glue). NOTE: discovered pre-existing bug 10c3e55c — the
explore/place wiring is linker-GC'd out of the PAFSynth ELF (predates
this refactor; evidence in the ergo task).
2026-07-14 04:16:21 +02:00
|
|
|
|
// Browser replay capacity (rl-feedback-design §4: WASM 64, firmware 32).
|
|
|
|
|
|
constexpr std::size_t kFeedbackReplayCap = 64u;
|
feat(wasm)!: P2.2 — nisps_ml_create honours dims; runtime-shaped browser MLP + reshape
Operator-approved ABI change (P2 stop-point). The WASM MLP is now
MLPCore<DynamicStorage>:
- nisps_ml_create(input, output, hidden[3], n, seed) honours its args;
non-positive/null fall back to the historical 32→[10,14,18]→126, so
every pre-P2 caller (manifold, worker, parity harness) stays
bit-identical. Invalid/oversized dims (>4096) → null.
- NEW nisps_ml_reshape(ml, in, out, hidden, n, spread): fresh net at the
new dims, warm-started via nisps/ml/warm_start.hpp (overlapping region
copied; rest keeps spread init); feedback controller re-created (state
resets — reset-on-reshape modal is the front-end contract). Failure
leaves the old net untouched.
- nisps_ml_describe(ml, out): takes the handle; null reports defaults.
- FeedbackController got the same storage split: algorithms in
FeedbackControllerCore<FbStorage>; FixedFeedbackStorage keeps firmware/
tests source-identical via the old alias; DynamicFeedbackStorage (one
arena) sizes to the runtime net. Firmware .text unchanged (122692).
- MLHandle: per-instance scratch vectors; dropped the dead 2MB
batch_out_scratch.
- TS: types.ts decls (+_nisps_ml_reshape), wasm-iml re-describes the
created instance, worker carries a shape-contract note for P2.3.
Verified: ctest 4/4 incl. new warm-start grow/shrink test; reshape ABI
smoke (dims honoured, overlap survives, invalid rejected, outputs
bounded); parity PASS unchanged (2.4e-7); lint clean; manifold 9 unit +
20 e2e green; firmware .text 122692 (+0.30% vs pre-P2 baseline).
2026-07-14 03:38:06 +02:00
|
|
|
|
|
|
|
|
|
|
struct MlDims {
|
|
|
|
|
|
std::size_t n_in;
|
|
|
|
|
|
std::size_t hidden[3];
|
|
|
|
|
|
std::size_t n_out;
|
|
|
|
|
|
bool ok;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Sanitise caller-supplied dims. Non-positive input/output and missing /
|
|
|
|
|
|
// non-3-entry hidden lists fall back to the defaults; out-of-range values
|
|
|
|
|
|
// make the request invalid.
|
|
|
|
|
|
MlDims sanitise_dims(int input_size, int output_size,
|
|
|
|
|
|
const int* hidden, int n_hidden) noexcept {
|
|
|
|
|
|
MlDims d{kDefaultInputs,
|
|
|
|
|
|
{kDefaultHidden[0], kDefaultHidden[1], kDefaultHidden[2]},
|
|
|
|
|
|
kDefaultOutputs,
|
|
|
|
|
|
true};
|
|
|
|
|
|
if (input_size > 0) d.n_in = static_cast<std::size_t>(input_size);
|
|
|
|
|
|
if (output_size > 0) d.n_out = static_cast<std::size_t>(output_size);
|
|
|
|
|
|
if (hidden && n_hidden == 3) {
|
|
|
|
|
|
for (std::size_t i = 0; i < 3u; ++i) {
|
|
|
|
|
|
if (hidden[i] <= 0) { d.ok = false; return d; }
|
|
|
|
|
|
d.hidden[i] = static_cast<std::size_t>(hidden[i]);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if (hidden && n_hidden != 0) {
|
|
|
|
|
|
d.ok = false; // the 4-layer topology needs exactly 3 hidden sizes
|
|
|
|
|
|
return d;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (d.n_in > kMaxDim || d.n_out > kMaxDim ||
|
|
|
|
|
|
d.hidden[0] > kMaxDim || d.hidden[1] > kMaxDim || d.hidden[2] > kMaxDim) {
|
|
|
|
|
|
d.ok = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
return d;
|
|
|
|
|
|
}
|
feat(playground/wasm): WASM bridge between C++ core and SolidJS playground (meml-tgm)
Stream 7 wires nisps/ml + nisps/engines into the playground via Emscripten.
Highlights:
- nisps/wasm/bindings.cpp: flat C API per architecture.md §6.2. Fixed-arch
MLP<2, 10, 14, 18, 126>; engine string→type dispatch table with NoOp
fallback.
- scripts/build-wasm.sh: emcc invocation, MODULARIZE=1, exports listed
explicitly; produces playground/public/nisps.{js,wasm}.
- playground/src/ml/wasm-iml.ts: main-thread MLP host (sync inference,
sync training, RL ops, weights I/O, layer stats, localStorage).
- playground/src/ml/wasm-worker.ts: disposable Web Worker for off-thread
async training, owns its own WASM instance.
- playground/src/ml/dataset.ts: Float32Array-backed FIFO with sample-weight
modes (uniform/global/local/combined). Port of legacy dataset.js.
- playground/src/audio/engine-host.ts: AudioContext + AudioWorkletNode
lifecycle, with start/stop/setEngine/setParams.
- playground/src/audio/worklet/nisps-processor.ts: WASM-loading
AudioWorkletProcessor that runs engine.process_block per 128-sample
block. Loads its own WASM instance from main-thread-supplied bytes
(no fetch in worklet).
- playground/src/stores/ml-store.ts: wired stub methods to WasmIML
singleton; lazy initialize().
- playground/src/debug/probe.ts: window.__nisps now calls real WasmIML
via the store; lazy-init on first use.
Verified:
- bash scripts/build-wasm.sh succeeds (94 KB nisps.wasm)
- bun run typecheck OK
- bun run build OK (production bundle)
- vite dev server serves /nisps.{js,wasm} with COOP/COEP
Known limitation: WASM is fixed at one MLP shape. Multi-arch deferred —
documented in nisps/wasm/README.md.
2026-04-29 15:36:29 +02:00
|
|
|
|
|
|
|
|
|
|
// We allocate the MLP on the heap (one-off — not the audio path) and return
|
|
|
|
|
|
// the opaque pointer to JS.
|
|
|
|
|
|
struct MLHandle {
|
feat(wasm)!: P2.2 — nisps_ml_create honours dims; runtime-shaped browser MLP + reshape
Operator-approved ABI change (P2 stop-point). The WASM MLP is now
MLPCore<DynamicStorage>:
- nisps_ml_create(input, output, hidden[3], n, seed) honours its args;
non-positive/null fall back to the historical 32→[10,14,18]→126, so
every pre-P2 caller (manifold, worker, parity harness) stays
bit-identical. Invalid/oversized dims (>4096) → null.
- NEW nisps_ml_reshape(ml, in, out, hidden, n, spread): fresh net at the
new dims, warm-started via nisps/ml/warm_start.hpp (overlapping region
copied; rest keeps spread init); feedback controller re-created (state
resets — reset-on-reshape modal is the front-end contract). Failure
leaves the old net untouched.
- nisps_ml_describe(ml, out): takes the handle; null reports defaults.
- FeedbackController got the same storage split: algorithms in
FeedbackControllerCore<FbStorage>; FixedFeedbackStorage keeps firmware/
tests source-identical via the old alias; DynamicFeedbackStorage (one
arena) sizes to the runtime net. Firmware .text unchanged (122692).
- MLHandle: per-instance scratch vectors; dropped the dead 2MB
batch_out_scratch.
- TS: types.ts decls (+_nisps_ml_reshape), wasm-iml re-describes the
created instance, worker carries a shape-contract note for P2.3.
Verified: ctest 4/4 incl. new warm-start grow/shrink test; reshape ABI
smoke (dims honoured, overlap survives, invalid rejected, outputs
bounded); parity PASS unchanged (2.4e-7); lint clean; manifold 9 unit +
20 e2e green; firmware .text 122692 (+0.30% vs pre-P2 baseline).
2026-07-14 03:38:06 +02:00
|
|
|
|
std::uint64_t seed64;
|
|
|
|
|
|
BrowserMLP mlp;
|
2026-06-28 04:14:12 +02:00
|
|
|
|
// "Down Action" negative-feedback controller (Avoid/RandomiseOutputs/
|
feat(wasm)!: P2.2 — nisps_ml_create honours dims; runtime-shaped browser MLP + reshape
Operator-approved ABI change (P2 stop-point). The WASM MLP is now
MLPCore<DynamicStorage>:
- nisps_ml_create(input, output, hidden[3], n, seed) honours its args;
non-positive/null fall back to the historical 32→[10,14,18]→126, so
every pre-P2 caller (manifold, worker, parity harness) stays
bit-identical. Invalid/oversized dims (>4096) → null.
- NEW nisps_ml_reshape(ml, in, out, hidden, n, spread): fresh net at the
new dims, warm-started via nisps/ml/warm_start.hpp (overlapping region
copied; rest keeps spread init); feedback controller re-created (state
resets — reset-on-reshape modal is the front-end contract). Failure
leaves the old net untouched.
- nisps_ml_describe(ml, out): takes the handle; null reports defaults.
- FeedbackController got the same storage split: algorithms in
FeedbackControllerCore<FbStorage>; FixedFeedbackStorage keeps firmware/
tests source-identical via the old alias; DynamicFeedbackStorage (one
arena) sizes to the runtime net. Firmware .text unchanged (122692).
- MLHandle: per-instance scratch vectors; dropped the dead 2MB
batch_out_scratch.
- TS: types.ts decls (+_nisps_ml_reshape), wasm-iml re-describes the
created instance, worker carries a shape-contract note for P2.3.
Verified: ctest 4/4 incl. new warm-start grow/shrink test; reshape ABI
smoke (dims honoured, overlap survives, invalid rejected, outputs
bounded); parity PASS unchanged (2.4e-7); lint clean; manifold 9 unit +
20 e2e green; firmware .text 122692 (+0.30% vs pre-P2 baseline).
2026-07-14 03:38:06 +02:00
|
|
|
|
// RandomiseMlp/ExploreAndPlace). Seeded off the MLP seed XOR a salt so
|
|
|
|
|
|
// its static-output RNG stream is independent of the MLP's RNG.
|
|
|
|
|
|
BrowserFeedback feedback;
|
|
|
|
|
|
// Buffers used to bridge JS → C++ (sized to the instance's dims):
|
|
|
|
|
|
std::vector<float> output_scratch;
|
feat(playground/wasm): WASM bridge between C++ core and SolidJS playground (meml-tgm)
Stream 7 wires nisps/ml + nisps/engines into the playground via Emscripten.
Highlights:
- nisps/wasm/bindings.cpp: flat C API per architecture.md §6.2. Fixed-arch
MLP<2, 10, 14, 18, 126>; engine string→type dispatch table with NoOp
fallback.
- scripts/build-wasm.sh: emcc invocation, MODULARIZE=1, exports listed
explicitly; produces playground/public/nisps.{js,wasm}.
- playground/src/ml/wasm-iml.ts: main-thread MLP host (sync inference,
sync training, RL ops, weights I/O, layer stats, localStorage).
- playground/src/ml/wasm-worker.ts: disposable Web Worker for off-thread
async training, owns its own WASM instance.
- playground/src/ml/dataset.ts: Float32Array-backed FIFO with sample-weight
modes (uniform/global/local/combined). Port of legacy dataset.js.
- playground/src/audio/engine-host.ts: AudioContext + AudioWorkletNode
lifecycle, with start/stop/setEngine/setParams.
- playground/src/audio/worklet/nisps-processor.ts: WASM-loading
AudioWorkletProcessor that runs engine.process_block per 128-sample
block. Loads its own WASM instance from main-thread-supplied bytes
(no fetch in worklet).
- playground/src/stores/ml-store.ts: wired stub methods to WasmIML
singleton; lazy initialize().
- playground/src/debug/probe.ts: window.__nisps now calls real WasmIML
via the store; lazy-init on first use.
Verified:
- bash scripts/build-wasm.sh succeeds (94 KB nisps.wasm)
- bun run typecheck OK
- bun run build OK (production bundle)
- vite dev server serves /nisps.{js,wasm} with COOP/COEP
Known limitation: WASM is fixed at one MLP shape. Multi-arch deferred —
documented in nisps/wasm/README.md.
2026-04-29 15:36:29 +02:00
|
|
|
|
// Stats buffer fed back to JS via get_layer_stats.
|
feat(wasm)!: P2.2 — nisps_ml_create honours dims; runtime-shaped browser MLP + reshape
Operator-approved ABI change (P2 stop-point). The WASM MLP is now
MLPCore<DynamicStorage>:
- nisps_ml_create(input, output, hidden[3], n, seed) honours its args;
non-positive/null fall back to the historical 32→[10,14,18]→126, so
every pre-P2 caller (manifold, worker, parity harness) stays
bit-identical. Invalid/oversized dims (>4096) → null.
- NEW nisps_ml_reshape(ml, in, out, hidden, n, spread): fresh net at the
new dims, warm-started via nisps/ml/warm_start.hpp (overlapping region
copied; rest keeps spread init); feedback controller re-created (state
resets — reset-on-reshape modal is the front-end contract). Failure
leaves the old net untouched.
- nisps_ml_describe(ml, out): takes the handle; null reports defaults.
- FeedbackController got the same storage split: algorithms in
FeedbackControllerCore<FbStorage>; FixedFeedbackStorage keeps firmware/
tests source-identical via the old alias; DynamicFeedbackStorage (one
arena) sizes to the runtime net. Firmware .text unchanged (122692).
- MLHandle: per-instance scratch vectors; dropped the dead 2MB
batch_out_scratch.
- TS: types.ts decls (+_nisps_ml_reshape), wasm-iml re-describes the
created instance, worker carries a shape-contract note for P2.3.
Verified: ctest 4/4 incl. new warm-start grow/shrink test; reshape ABI
smoke (dims honoured, overlap survives, invalid rejected, outputs
bounded); parity PASS unchanged (2.4e-7); lint clean; manifold 9 unit +
20 e2e green; firmware .text 122692 (+0.30% vs pre-P2 baseline).
2026-07-14 03:38:06 +02:00
|
|
|
|
std::array<float, BrowserMLP::kNumLayers * 4u> stats_scratch{};
|
2026-06-28 04:14:12 +02:00
|
|
|
|
// Static-output buffer for the RandomiseOutputs bypass path.
|
feat(wasm)!: P2.2 — nisps_ml_create honours dims; runtime-shaped browser MLP + reshape
Operator-approved ABI change (P2 stop-point). The WASM MLP is now
MLPCore<DynamicStorage>:
- nisps_ml_create(input, output, hidden[3], n, seed) honours its args;
non-positive/null fall back to the historical 32→[10,14,18]→126, so
every pre-P2 caller (manifold, worker, parity harness) stays
bit-identical. Invalid/oversized dims (>4096) → null.
- NEW nisps_ml_reshape(ml, in, out, hidden, n, spread): fresh net at the
new dims, warm-started via nisps/ml/warm_start.hpp (overlapping region
copied; rest keeps spread init); feedback controller re-created (state
resets — reset-on-reshape modal is the front-end contract). Failure
leaves the old net untouched.
- nisps_ml_describe(ml, out): takes the handle; null reports defaults.
- FeedbackController got the same storage split: algorithms in
FeedbackControllerCore<FbStorage>; FixedFeedbackStorage keeps firmware/
tests source-identical via the old alias; DynamicFeedbackStorage (one
arena) sizes to the runtime net. Firmware .text unchanged (122692).
- MLHandle: per-instance scratch vectors; dropped the dead 2MB
batch_out_scratch.
- TS: types.ts decls (+_nisps_ml_reshape), wasm-iml re-describes the
created instance, worker carries a shape-contract note for P2.3.
Verified: ctest 4/4 incl. new warm-start grow/shrink test; reshape ABI
smoke (dims honoured, overlap survives, invalid rejected, outputs
bounded); parity PASS unchanged (2.4e-7); lint clean; manifold 9 unit +
20 e2e green; firmware .text 122692 (+0.30% vs pre-P2 baseline).
2026-07-14 03:38:06 +02:00
|
|
|
|
std::vector<float> feedback_static_scratch;
|
feat(ml)!: P3 core — geometric dislike in nisps/, jolt/OU + geo ABI
Geometric dislike (rl-feedback-design §2.1/§4; upstream InterfaceRL @
0a541cc ported verbatim, constants included):
- nisps/ml/replay.hpp: ReplayView over storage-owned buffers — deepen-or-
store negatives (dedup 0.05, clamp -16), k-NN positive centroid with
deterministic index tie-break + fixed accumulation order, proportional
decay (0.0025*max(|r|,1)) + eviction, order-preserving compaction.
- nisps/ml/geo_push.hpp: push-away target (pushStep clamp(|avgNeg|,.25,1)
*0.5, taper /(1+len), useRandom on len<=1e-4 via nisps::Rng — the single
deliberate divergence from libc rand()), negLRRatio 0.5-0.4*negFraction.
- mlp.hpp: train_targets(input, computed-target, lr, out_mask) — trains
toward computed targets (negative lr = cold-start train-away); solo/
focus gating zeroes masked derivs.
- feedback.hpp: AvoidStyle {Geometric (new default), Diffuse (legacy
move_weights, kept for A/B)}; dislike_geometric() collapses upstream's
press+optimise into one synchronous call; on_up in geometric Avoid
feeds the positive centroid; dislike-multiplier bookkeeping. Storage
gains replay buffers (Fixed: ReplayCap=32 firmware default ≈ +8KB SRAM;
Dynamic arena: cap 64).
- bindings: nisps_ml_feedback_{dislike_geometric,store_positive,
positive_count,negative_count,set_avoid_style} + P3.2 jolt/OU ABI:
nisps_ml_jolt_{press,step,release,active,lr_scale,tick_lr_ramp},
nisps_ml_explore_{intensity,get_intensity,apply} (OUNoise<4096>
over-provisioned; same code the firmware ModeBase runs).
- parity v4: Stage 6 scripted geometric session (2 likes → 2 dislikes,
f32-exact heard vectors via Math.fround) — 961 floats PASS at 2.4e-7.
- tests: test_mlp_geo_dislike.cpp (replay dedup/deepen/clamp, centroid
tie-break, push direction/taper/mask/clamp, cold-start inertness +
train-away, determinism, Diffuse legacy); legacy Avoid test pinned to
Diffuse per the ADR's deliberate-break note.
Firmware: PAFSynth .text/.data unchanged (geometric path not referenced
by current glue). NOTE: discovered pre-existing bug 10c3e55c — the
explore/place wiring is linker-GC'd out of the PAFSynth ELF (predates
this refactor; evidence in the ergo task).
2026-07-14 04:16:21 +02:00
|
|
|
|
// Jolt (held weight morph) + OU exploration noise — the P3 gesture
|
|
|
|
|
|
// engines, same code the firmware ModeBase runs. Jolt operates on the
|
|
|
|
|
|
// flat weight buffer via jolt_scratch; OU state is over-provisioned to
|
|
|
|
|
|
// kMaxDim and applies to the first n_out entries.
|
|
|
|
|
|
nisps::ml::Jolt jolt;
|
|
|
|
|
|
nisps::ml::OUNoise<kMaxDim> ou;
|
|
|
|
|
|
std::vector<float> jolt_scratch;
|
feat(wasm)!: P2.2 — nisps_ml_create honours dims; runtime-shaped browser MLP + reshape
Operator-approved ABI change (P2 stop-point). The WASM MLP is now
MLPCore<DynamicStorage>:
- nisps_ml_create(input, output, hidden[3], n, seed) honours its args;
non-positive/null fall back to the historical 32→[10,14,18]→126, so
every pre-P2 caller (manifold, worker, parity harness) stays
bit-identical. Invalid/oversized dims (>4096) → null.
- NEW nisps_ml_reshape(ml, in, out, hidden, n, spread): fresh net at the
new dims, warm-started via nisps/ml/warm_start.hpp (overlapping region
copied; rest keeps spread init); feedback controller re-created (state
resets — reset-on-reshape modal is the front-end contract). Failure
leaves the old net untouched.
- nisps_ml_describe(ml, out): takes the handle; null reports defaults.
- FeedbackController got the same storage split: algorithms in
FeedbackControllerCore<FbStorage>; FixedFeedbackStorage keeps firmware/
tests source-identical via the old alias; DynamicFeedbackStorage (one
arena) sizes to the runtime net. Firmware .text unchanged (122692).
- MLHandle: per-instance scratch vectors; dropped the dead 2MB
batch_out_scratch.
- TS: types.ts decls (+_nisps_ml_reshape), wasm-iml re-describes the
created instance, worker carries a shape-contract note for P2.3.
Verified: ctest 4/4 incl. new warm-start grow/shrink test; reshape ABI
smoke (dims honoured, overlap survives, invalid rejected, outputs
bounded); parity PASS unchanged (2.4e-7); lint clean; manifold 9 unit +
20 e2e green; firmware .text 122692 (+0.30% vs pre-P2 baseline).
2026-07-14 03:38:06 +02:00
|
|
|
|
// infer_batch cap; callers must split larger requests.
|
feat(playground/wasm): WASM bridge between C++ core and SolidJS playground (meml-tgm)
Stream 7 wires nisps/ml + nisps/engines into the playground via Emscripten.
Highlights:
- nisps/wasm/bindings.cpp: flat C API per architecture.md §6.2. Fixed-arch
MLP<2, 10, 14, 18, 126>; engine string→type dispatch table with NoOp
fallback.
- scripts/build-wasm.sh: emcc invocation, MODULARIZE=1, exports listed
explicitly; produces playground/public/nisps.{js,wasm}.
- playground/src/ml/wasm-iml.ts: main-thread MLP host (sync inference,
sync training, RL ops, weights I/O, layer stats, localStorage).
- playground/src/ml/wasm-worker.ts: disposable Web Worker for off-thread
async training, owns its own WASM instance.
- playground/src/ml/dataset.ts: Float32Array-backed FIFO with sample-weight
modes (uniform/global/local/combined). Port of legacy dataset.js.
- playground/src/audio/engine-host.ts: AudioContext + AudioWorkletNode
lifecycle, with start/stop/setEngine/setParams.
- playground/src/audio/worklet/nisps-processor.ts: WASM-loading
AudioWorkletProcessor that runs engine.process_block per 128-sample
block. Loads its own WASM instance from main-thread-supplied bytes
(no fetch in worklet).
- playground/src/stores/ml-store.ts: wired stub methods to WasmIML
singleton; lazy initialize().
- playground/src/debug/probe.ts: window.__nisps now calls real WasmIML
via the store; lazy-init on first use.
Verified:
- bash scripts/build-wasm.sh succeeds (94 KB nisps.wasm)
- bun run typecheck OK
- bun run build OK (production bundle)
- vite dev server serves /nisps.{js,wasm} with COOP/COEP
Known limitation: WASM is fixed at one MLP shape. Multi-arch deferred —
documented in nisps/wasm/README.md.
2026-04-29 15:36:29 +02:00
|
|
|
|
static constexpr std::size_t kMaxBatch = 4096u;
|
|
|
|
|
|
|
feat(wasm)!: P2.2 — nisps_ml_create honours dims; runtime-shaped browser MLP + reshape
Operator-approved ABI change (P2 stop-point). The WASM MLP is now
MLPCore<DynamicStorage>:
- nisps_ml_create(input, output, hidden[3], n, seed) honours its args;
non-positive/null fall back to the historical 32→[10,14,18]→126, so
every pre-P2 caller (manifold, worker, parity harness) stays
bit-identical. Invalid/oversized dims (>4096) → null.
- NEW nisps_ml_reshape(ml, in, out, hidden, n, spread): fresh net at the
new dims, warm-started via nisps/ml/warm_start.hpp (overlapping region
copied; rest keeps spread init); feedback controller re-created (state
resets — reset-on-reshape modal is the front-end contract). Failure
leaves the old net untouched.
- nisps_ml_describe(ml, out): takes the handle; null reports defaults.
- FeedbackController got the same storage split: algorithms in
FeedbackControllerCore<FbStorage>; FixedFeedbackStorage keeps firmware/
tests source-identical via the old alias; DynamicFeedbackStorage (one
arena) sizes to the runtime net. Firmware .text unchanged (122692).
- MLHandle: per-instance scratch vectors; dropped the dead 2MB
batch_out_scratch.
- TS: types.ts decls (+_nisps_ml_reshape), wasm-iml re-describes the
created instance, worker carries a shape-contract note for P2.3.
Verified: ctest 4/4 incl. new warm-start grow/shrink test; reshape ABI
smoke (dims honoured, overlap survives, invalid rejected, outputs
bounded); parity PASS unchanged (2.4e-7); lint clean; manifold 9 unit +
20 e2e green; firmware .text 122692 (+0.30% vs pre-P2 baseline).
2026-07-14 03:38:06 +02:00
|
|
|
|
MLHandle(std::uint64_t seed, const MlDims& d) noexcept
|
|
|
|
|
|
: seed64(seed),
|
|
|
|
|
|
mlp(seed, d.n_in, std::span<const std::size_t>(d.hidden, 3u), d.n_out),
|
feat(ml)!: P3 core — geometric dislike in nisps/, jolt/OU + geo ABI
Geometric dislike (rl-feedback-design §2.1/§4; upstream InterfaceRL @
0a541cc ported verbatim, constants included):
- nisps/ml/replay.hpp: ReplayView over storage-owned buffers — deepen-or-
store negatives (dedup 0.05, clamp -16), k-NN positive centroid with
deterministic index tie-break + fixed accumulation order, proportional
decay (0.0025*max(|r|,1)) + eviction, order-preserving compaction.
- nisps/ml/geo_push.hpp: push-away target (pushStep clamp(|avgNeg|,.25,1)
*0.5, taper /(1+len), useRandom on len<=1e-4 via nisps::Rng — the single
deliberate divergence from libc rand()), negLRRatio 0.5-0.4*negFraction.
- mlp.hpp: train_targets(input, computed-target, lr, out_mask) — trains
toward computed targets (negative lr = cold-start train-away); solo/
focus gating zeroes masked derivs.
- feedback.hpp: AvoidStyle {Geometric (new default), Diffuse (legacy
move_weights, kept for A/B)}; dislike_geometric() collapses upstream's
press+optimise into one synchronous call; on_up in geometric Avoid
feeds the positive centroid; dislike-multiplier bookkeeping. Storage
gains replay buffers (Fixed: ReplayCap=32 firmware default ≈ +8KB SRAM;
Dynamic arena: cap 64).
- bindings: nisps_ml_feedback_{dislike_geometric,store_positive,
positive_count,negative_count,set_avoid_style} + P3.2 jolt/OU ABI:
nisps_ml_jolt_{press,step,release,active,lr_scale,tick_lr_ramp},
nisps_ml_explore_{intensity,get_intensity,apply} (OUNoise<4096>
over-provisioned; same code the firmware ModeBase runs).
- parity v4: Stage 6 scripted geometric session (2 likes → 2 dislikes,
f32-exact heard vectors via Math.fround) — 961 floats PASS at 2.4e-7.
- tests: test_mlp_geo_dislike.cpp (replay dedup/deepen/clamp, centroid
tie-break, push direction/taper/mask/clamp, cold-start inertness +
train-away, determinism, Diffuse legacy); legacy Avoid test pinned to
Diffuse per the ADR's deliberate-break note.
Firmware: PAFSynth .text/.data unchanged (geometric path not referenced
by current glue). NOTE: discovered pre-existing bug 10c3e55c — the
explore/place wiring is linker-GC'd out of the PAFSynth ELF (predates
this refactor; evidence in the ergo task).
2026-07-14 04:16:21 +02:00
|
|
|
|
feedback(seed ^ kFeedbackSalt, d.n_out, mlp.weight_count(), kFeedbackUndoDepth,
|
|
|
|
|
|
d.n_in, kFeedbackReplayCap),
|
feat(wasm)!: P2.2 — nisps_ml_create honours dims; runtime-shaped browser MLP + reshape
Operator-approved ABI change (P2 stop-point). The WASM MLP is now
MLPCore<DynamicStorage>:
- nisps_ml_create(input, output, hidden[3], n, seed) honours its args;
non-positive/null fall back to the historical 32→[10,14,18]→126, so
every pre-P2 caller (manifold, worker, parity harness) stays
bit-identical. Invalid/oversized dims (>4096) → null.
- NEW nisps_ml_reshape(ml, in, out, hidden, n, spread): fresh net at the
new dims, warm-started via nisps/ml/warm_start.hpp (overlapping region
copied; rest keeps spread init); feedback controller re-created (state
resets — reset-on-reshape modal is the front-end contract). Failure
leaves the old net untouched.
- nisps_ml_describe(ml, out): takes the handle; null reports defaults.
- FeedbackController got the same storage split: algorithms in
FeedbackControllerCore<FbStorage>; FixedFeedbackStorage keeps firmware/
tests source-identical via the old alias; DynamicFeedbackStorage (one
arena) sizes to the runtime net. Firmware .text unchanged (122692).
- MLHandle: per-instance scratch vectors; dropped the dead 2MB
batch_out_scratch.
- TS: types.ts decls (+_nisps_ml_reshape), wasm-iml re-describes the
created instance, worker carries a shape-contract note for P2.3.
Verified: ctest 4/4 incl. new warm-start grow/shrink test; reshape ABI
smoke (dims honoured, overlap survives, invalid rejected, outputs
bounded); parity PASS unchanged (2.4e-7); lint clean; manifold 9 unit +
20 e2e green; firmware .text 122692 (+0.30% vs pre-P2 baseline).
2026-07-14 03:38:06 +02:00
|
|
|
|
output_scratch(d.n_out, 0.f),
|
feat(ml)!: P3 core — geometric dislike in nisps/, jolt/OU + geo ABI
Geometric dislike (rl-feedback-design §2.1/§4; upstream InterfaceRL @
0a541cc ported verbatim, constants included):
- nisps/ml/replay.hpp: ReplayView over storage-owned buffers — deepen-or-
store negatives (dedup 0.05, clamp -16), k-NN positive centroid with
deterministic index tie-break + fixed accumulation order, proportional
decay (0.0025*max(|r|,1)) + eviction, order-preserving compaction.
- nisps/ml/geo_push.hpp: push-away target (pushStep clamp(|avgNeg|,.25,1)
*0.5, taper /(1+len), useRandom on len<=1e-4 via nisps::Rng — the single
deliberate divergence from libc rand()), negLRRatio 0.5-0.4*negFraction.
- mlp.hpp: train_targets(input, computed-target, lr, out_mask) — trains
toward computed targets (negative lr = cold-start train-away); solo/
focus gating zeroes masked derivs.
- feedback.hpp: AvoidStyle {Geometric (new default), Diffuse (legacy
move_weights, kept for A/B)}; dislike_geometric() collapses upstream's
press+optimise into one synchronous call; on_up in geometric Avoid
feeds the positive centroid; dislike-multiplier bookkeeping. Storage
gains replay buffers (Fixed: ReplayCap=32 firmware default ≈ +8KB SRAM;
Dynamic arena: cap 64).
- bindings: nisps_ml_feedback_{dislike_geometric,store_positive,
positive_count,negative_count,set_avoid_style} + P3.2 jolt/OU ABI:
nisps_ml_jolt_{press,step,release,active,lr_scale,tick_lr_ramp},
nisps_ml_explore_{intensity,get_intensity,apply} (OUNoise<4096>
over-provisioned; same code the firmware ModeBase runs).
- parity v4: Stage 6 scripted geometric session (2 likes → 2 dislikes,
f32-exact heard vectors via Math.fround) — 961 floats PASS at 2.4e-7.
- tests: test_mlp_geo_dislike.cpp (replay dedup/deepen/clamp, centroid
tie-break, push direction/taper/mask/clamp, cold-start inertness +
train-away, determinism, Diffuse legacy); legacy Avoid test pinned to
Diffuse per the ADR's deliberate-break note.
Firmware: PAFSynth .text/.data unchanged (geometric path not referenced
by current glue). NOTE: discovered pre-existing bug 10c3e55c — the
explore/place wiring is linker-GC'd out of the PAFSynth ELF (predates
this refactor; evidence in the ergo task).
2026-07-14 04:16:21 +02:00
|
|
|
|
feedback_static_scratch(d.n_out, 0.f),
|
|
|
|
|
|
jolt(seed ^ kJoltSalt),
|
|
|
|
|
|
ou(seed ^ kOUSalt),
|
|
|
|
|
|
jolt_scratch(mlp.weight_count(), 0.f) {}
|
feat(wasm)!: P2.2 — nisps_ml_create honours dims; runtime-shaped browser MLP + reshape
Operator-approved ABI change (P2 stop-point). The WASM MLP is now
MLPCore<DynamicStorage>:
- nisps_ml_create(input, output, hidden[3], n, seed) honours its args;
non-positive/null fall back to the historical 32→[10,14,18]→126, so
every pre-P2 caller (manifold, worker, parity harness) stays
bit-identical. Invalid/oversized dims (>4096) → null.
- NEW nisps_ml_reshape(ml, in, out, hidden, n, spread): fresh net at the
new dims, warm-started via nisps/ml/warm_start.hpp (overlapping region
copied; rest keeps spread init); feedback controller re-created (state
resets — reset-on-reshape modal is the front-end contract). Failure
leaves the old net untouched.
- nisps_ml_describe(ml, out): takes the handle; null reports defaults.
- FeedbackController got the same storage split: algorithms in
FeedbackControllerCore<FbStorage>; FixedFeedbackStorage keeps firmware/
tests source-identical via the old alias; DynamicFeedbackStorage (one
arena) sizes to the runtime net. Firmware .text unchanged (122692).
- MLHandle: per-instance scratch vectors; dropped the dead 2MB
batch_out_scratch.
- TS: types.ts decls (+_nisps_ml_reshape), wasm-iml re-describes the
created instance, worker carries a shape-contract note for P2.3.
Verified: ctest 4/4 incl. new warm-start grow/shrink test; reshape ABI
smoke (dims honoured, overlap survives, invalid rejected, outputs
bounded); parity PASS unchanged (2.4e-7); lint clean; manifold 9 unit +
20 e2e green; firmware .text 122692 (+0.30% vs pre-P2 baseline).
2026-07-14 03:38:06 +02:00
|
|
|
|
|
|
|
|
|
|
bool valid() const noexcept { return mlp.valid() && feedback.valid(); }
|
|
|
|
|
|
std::size_t n_in() const noexcept { return mlp.n_in(); }
|
|
|
|
|
|
std::size_t n_out() const noexcept { return mlp.n_out(); }
|
feat(playground/wasm): WASM bridge between C++ core and SolidJS playground (meml-tgm)
Stream 7 wires nisps/ml + nisps/engines into the playground via Emscripten.
Highlights:
- nisps/wasm/bindings.cpp: flat C API per architecture.md §6.2. Fixed-arch
MLP<2, 10, 14, 18, 126>; engine string→type dispatch table with NoOp
fallback.
- scripts/build-wasm.sh: emcc invocation, MODULARIZE=1, exports listed
explicitly; produces playground/public/nisps.{js,wasm}.
- playground/src/ml/wasm-iml.ts: main-thread MLP host (sync inference,
sync training, RL ops, weights I/O, layer stats, localStorage).
- playground/src/ml/wasm-worker.ts: disposable Web Worker for off-thread
async training, owns its own WASM instance.
- playground/src/ml/dataset.ts: Float32Array-backed FIFO with sample-weight
modes (uniform/global/local/combined). Port of legacy dataset.js.
- playground/src/audio/engine-host.ts: AudioContext + AudioWorkletNode
lifecycle, with start/stop/setEngine/setParams.
- playground/src/audio/worklet/nisps-processor.ts: WASM-loading
AudioWorkletProcessor that runs engine.process_block per 128-sample
block. Loads its own WASM instance from main-thread-supplied bytes
(no fetch in worklet).
- playground/src/stores/ml-store.ts: wired stub methods to WasmIML
singleton; lazy initialize().
- playground/src/debug/probe.ts: window.__nisps now calls real WasmIML
via the store; lazy-init on first use.
Verified:
- bash scripts/build-wasm.sh succeeds (94 KB nisps.wasm)
- bun run typecheck OK
- bun run build OK (production bundle)
- vite dev server serves /nisps.{js,wasm} with COOP/COEP
Known limitation: WASM is fixed at one MLP shape. Multi-arch deferred —
documented in nisps/wasm/README.md.
2026-04-29 15:36:29 +02:00
|
|
|
|
};
|
|
|
|
|
|
|
feat(pipeline)!: P4 core — input/output chains + curve catalog in nisps/
- nisps/pipeline/input_chain.hpp: faithful f32 port of the manifold input
pipeline (invert→deadzone→circular clamp→momentum-modulated zoom→centred
power→EMA→momentum update). Caller-supplied dt, internal accumulated
clock (no wall clock — deterministic; matches the P1 fixtures' clock
contract). Fixed-capacity velocity ring; serialisable state.
- nisps/pipeline/output_chain.hpp: curve→EMA→slew→freeze(+per-output mask)
chain, capacity-templated (browser cap 4096; firmware would use NOut).
- nisps/core/math.hpp: + centered_power(x, exponent) (both chains use it).
- bindings: nisps_pipeline_create/destroy, nisps_input_set_config(15-float
wire layout)/process/reset, nisps_output_set_config/set_freeze_mask/
process/reset, pipeline state save/load, nisps_curve_apply(+batch)
(ids 0-6 = Curve enum, 7 = centred power).
- parity v5 Stage 7: rational (transcendental-free) traces through both
chains (2 configs each) + full curve catalog — 1273 floats PASS, the
pipeline floats bit-identical native↔WASM.
- ctest test_pipeline.cpp: deadzone remap, circular clamp, zoom+freeze,
sticky anchor, frame-rate-independent EMA, momentum zoom-out/recovery,
state round-trip, slew, freeze gate/mask, reseed-on-length-change,
centred-power endpoints.
Part of one-core-engine-refactor P4; the manifold TS switch follows.
2026-07-18 11:52:53 +02:00
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// Pipeline side (one-core-engine P4): the input/output processing chains,
|
|
|
|
|
|
// state C++-side per handle. The output chain is capacity-templated; the
|
|
|
|
|
|
// browser instantiates the kMaxDim cap.
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
struct PipelineHandle {
|
|
|
|
|
|
nisps::pipeline::InputChain input;
|
|
|
|
|
|
nisps::pipeline::OutputChain<kMaxDim> output;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
feat(playground/wasm): WASM bridge between C++ core and SolidJS playground (meml-tgm)
Stream 7 wires nisps/ml + nisps/engines into the playground via Emscripten.
Highlights:
- nisps/wasm/bindings.cpp: flat C API per architecture.md §6.2. Fixed-arch
MLP<2, 10, 14, 18, 126>; engine string→type dispatch table with NoOp
fallback.
- scripts/build-wasm.sh: emcc invocation, MODULARIZE=1, exports listed
explicitly; produces playground/public/nisps.{js,wasm}.
- playground/src/ml/wasm-iml.ts: main-thread MLP host (sync inference,
sync training, RL ops, weights I/O, layer stats, localStorage).
- playground/src/ml/wasm-worker.ts: disposable Web Worker for off-thread
async training, owns its own WASM instance.
- playground/src/ml/dataset.ts: Float32Array-backed FIFO with sample-weight
modes (uniform/global/local/combined). Port of legacy dataset.js.
- playground/src/audio/engine-host.ts: AudioContext + AudioWorkletNode
lifecycle, with start/stop/setEngine/setParams.
- playground/src/audio/worklet/nisps-processor.ts: WASM-loading
AudioWorkletProcessor that runs engine.process_block per 128-sample
block. Loads its own WASM instance from main-thread-supplied bytes
(no fetch in worklet).
- playground/src/stores/ml-store.ts: wired stub methods to WasmIML
singleton; lazy initialize().
- playground/src/debug/probe.ts: window.__nisps now calls real WasmIML
via the store; lazy-init on first use.
Verified:
- bash scripts/build-wasm.sh succeeds (94 KB nisps.wasm)
- bun run typecheck OK
- bun run build OK (production bundle)
- vite dev server serves /nisps.{js,wasm} with COOP/COEP
Known limitation: WASM is fixed at one MLP shape. Multi-arch deferred —
documented in nisps/wasm/README.md.
2026-04-29 15:36:29 +02:00
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// Engine side
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
// Variant-style dispatch. Each create call instantiates ONE engine kind
|
|
|
|
|
|
// stored on the heap; the type is recorded in `kind` so process_block can
|
|
|
|
|
|
// dispatch without RTTI.
|
|
|
|
|
|
//
|
|
|
|
|
|
// We DO NOT use std::variant — Emscripten supports it but the overhead is
|
|
|
|
|
|
// unwanted. A discriminated union of pointers is enough.
|
|
|
|
|
|
enum class EngineKind : std::uint8_t {
|
|
|
|
|
|
NoOp,
|
|
|
|
|
|
PAFSynth,
|
|
|
|
|
|
ChannelStrip,
|
|
|
|
|
|
XIASRI,
|
|
|
|
|
|
VerbFX,
|
|
|
|
|
|
MEMLCelium,
|
|
|
|
|
|
BreakOr,
|
|
|
|
|
|
Elysiamorf,
|
|
|
|
|
|
Analysis,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
struct EngineHandle {
|
|
|
|
|
|
EngineKind kind = EngineKind::NoOp;
|
|
|
|
|
|
void* ptr = nullptr;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
template <typename EngineT>
|
|
|
|
|
|
inline EngineHandle make_handle(EngineKind kind, float sr) noexcept {
|
|
|
|
|
|
auto* e = new EngineT();
|
|
|
|
|
|
e->setup(sr);
|
|
|
|
|
|
return EngineHandle{kind, static_cast<void*>(e)};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
template <typename EngineT>
|
|
|
|
|
|
inline void destroy_typed(void* ptr) noexcept {
|
|
|
|
|
|
delete static_cast<EngineT*>(ptr);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
template <typename EngineT>
|
|
|
|
|
|
inline void set_params_typed(void* ptr, std::span<const float> params) noexcept {
|
|
|
|
|
|
static_cast<EngineT*>(ptr)->set_params(params);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
template <typename EngineT>
|
|
|
|
|
|
inline void process_typed(void* ptr,
|
|
|
|
|
|
const float* in_l, const float* in_r,
|
|
|
|
|
|
float* out_l, float* out_r,
|
|
|
|
|
|
int n_samples) noexcept {
|
|
|
|
|
|
auto* e = static_cast<EngineT*>(ptr);
|
|
|
|
|
|
for (int i = 0; i < n_samples; ++i) {
|
|
|
|
|
|
nisps::stereosample_t s{in_l ? in_l[i] : 0.f, in_r ? in_r[i] : 0.f};
|
|
|
|
|
|
const auto y = e->process(s);
|
|
|
|
|
|
if (out_l) out_l[i] = y.L;
|
|
|
|
|
|
if (out_r) out_r[i] = y.R;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
EngineHandle dispatch_create(std::string_view id, float sample_rate) noexcept {
|
|
|
|
|
|
using nisps::NoOpEngine;
|
|
|
|
|
|
using nisps::PAFSynthEngine;
|
|
|
|
|
|
using nisps::ChannelStripEngine;
|
|
|
|
|
|
using nisps::XIASRIEngine;
|
|
|
|
|
|
using nisps::VerbFXEngine;
|
|
|
|
|
|
using nisps::MEMLCeliumEngine;
|
|
|
|
|
|
using nisps::BreakOrEngine;
|
|
|
|
|
|
using nisps::ElysiamorfEngine;
|
|
|
|
|
|
using nisps::AnalysisEngine;
|
|
|
|
|
|
|
|
|
|
|
|
if (id == NoOpEngine::engine_id()) return make_handle<NoOpEngine>(EngineKind::NoOp, sample_rate);
|
|
|
|
|
|
if (id == PAFSynthEngine::engine_id()) return make_handle<PAFSynthEngine>(EngineKind::PAFSynth, sample_rate);
|
|
|
|
|
|
if (id == ChannelStripEngine::engine_id()) return make_handle<ChannelStripEngine>(EngineKind::ChannelStrip, sample_rate);
|
|
|
|
|
|
if (id == XIASRIEngine::engine_id()) return make_handle<XIASRIEngine>(EngineKind::XIASRI, sample_rate);
|
|
|
|
|
|
if (id == VerbFXEngine::engine_id()) return make_handle<VerbFXEngine>(EngineKind::VerbFX, sample_rate);
|
|
|
|
|
|
if (id == MEMLCeliumEngine::engine_id()) return make_handle<MEMLCeliumEngine>(EngineKind::MEMLCelium, sample_rate);
|
|
|
|
|
|
if (id == BreakOrEngine::engine_id()) return make_handle<BreakOrEngine>(EngineKind::BreakOr, sample_rate);
|
|
|
|
|
|
if (id == ElysiamorfEngine::engine_id()) return make_handle<ElysiamorfEngine>(EngineKind::Elysiamorf, sample_rate);
|
|
|
|
|
|
if (id == AnalysisEngine::engine_id()) return make_handle<AnalysisEngine>(EngineKind::Analysis, sample_rate);
|
|
|
|
|
|
|
|
|
|
|
|
// Unknown id → fall back to NoOp so the worklet is at least silent
|
|
|
|
|
|
// rather than UB.
|
|
|
|
|
|
return make_handle<NoOpEngine>(EngineKind::NoOp, sample_rate);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void dispatch_destroy(EngineHandle& h) noexcept {
|
|
|
|
|
|
using nisps::NoOpEngine;
|
|
|
|
|
|
using nisps::PAFSynthEngine;
|
|
|
|
|
|
using nisps::ChannelStripEngine;
|
|
|
|
|
|
using nisps::XIASRIEngine;
|
|
|
|
|
|
using nisps::VerbFXEngine;
|
|
|
|
|
|
using nisps::MEMLCeliumEngine;
|
|
|
|
|
|
using nisps::BreakOrEngine;
|
|
|
|
|
|
using nisps::ElysiamorfEngine;
|
|
|
|
|
|
using nisps::AnalysisEngine;
|
|
|
|
|
|
|
|
|
|
|
|
if (!h.ptr) return;
|
|
|
|
|
|
switch (h.kind) {
|
|
|
|
|
|
case EngineKind::NoOp: destroy_typed<NoOpEngine>(h.ptr); break;
|
|
|
|
|
|
case EngineKind::PAFSynth: destroy_typed<PAFSynthEngine>(h.ptr); break;
|
|
|
|
|
|
case EngineKind::ChannelStrip: destroy_typed<ChannelStripEngine>(h.ptr); break;
|
|
|
|
|
|
case EngineKind::XIASRI: destroy_typed<XIASRIEngine>(h.ptr); break;
|
|
|
|
|
|
case EngineKind::VerbFX: destroy_typed<VerbFXEngine>(h.ptr); break;
|
|
|
|
|
|
case EngineKind::MEMLCelium: destroy_typed<MEMLCeliumEngine>(h.ptr); break;
|
|
|
|
|
|
case EngineKind::BreakOr: destroy_typed<BreakOrEngine>(h.ptr); break;
|
|
|
|
|
|
case EngineKind::Elysiamorf: destroy_typed<ElysiamorfEngine>(h.ptr); break;
|
|
|
|
|
|
case EngineKind::Analysis: destroy_typed<AnalysisEngine>(h.ptr); break;
|
|
|
|
|
|
}
|
|
|
|
|
|
h.ptr = nullptr;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void dispatch_set_params(EngineHandle& h, std::span<const float> params) noexcept {
|
|
|
|
|
|
using nisps::NoOpEngine;
|
|
|
|
|
|
using nisps::PAFSynthEngine;
|
|
|
|
|
|
using nisps::ChannelStripEngine;
|
|
|
|
|
|
using nisps::XIASRIEngine;
|
|
|
|
|
|
using nisps::VerbFXEngine;
|
|
|
|
|
|
using nisps::MEMLCeliumEngine;
|
|
|
|
|
|
using nisps::BreakOrEngine;
|
|
|
|
|
|
using nisps::ElysiamorfEngine;
|
|
|
|
|
|
using nisps::AnalysisEngine;
|
|
|
|
|
|
|
|
|
|
|
|
switch (h.kind) {
|
|
|
|
|
|
case EngineKind::NoOp: set_params_typed<NoOpEngine>(h.ptr, params); break;
|
|
|
|
|
|
case EngineKind::PAFSynth: set_params_typed<PAFSynthEngine>(h.ptr, params); break;
|
|
|
|
|
|
case EngineKind::ChannelStrip: set_params_typed<ChannelStripEngine>(h.ptr, params); break;
|
|
|
|
|
|
case EngineKind::XIASRI: set_params_typed<XIASRIEngine>(h.ptr, params); break;
|
|
|
|
|
|
case EngineKind::VerbFX: set_params_typed<VerbFXEngine>(h.ptr, params); break;
|
|
|
|
|
|
case EngineKind::MEMLCelium: set_params_typed<MEMLCeliumEngine>(h.ptr, params); break;
|
|
|
|
|
|
case EngineKind::BreakOr: set_params_typed<BreakOrEngine>(h.ptr, params); break;
|
|
|
|
|
|
case EngineKind::Elysiamorf: set_params_typed<ElysiamorfEngine>(h.ptr, params); break;
|
|
|
|
|
|
case EngineKind::Analysis: set_params_typed<AnalysisEngine>(h.ptr, params); break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void dispatch_process_block(EngineHandle& h,
|
|
|
|
|
|
const float* in_l, const float* in_r,
|
|
|
|
|
|
float* out_l, float* out_r,
|
|
|
|
|
|
int n_samples) noexcept {
|
|
|
|
|
|
using nisps::NoOpEngine;
|
|
|
|
|
|
using nisps::PAFSynthEngine;
|
|
|
|
|
|
using nisps::ChannelStripEngine;
|
|
|
|
|
|
using nisps::XIASRIEngine;
|
|
|
|
|
|
using nisps::VerbFXEngine;
|
|
|
|
|
|
using nisps::MEMLCeliumEngine;
|
|
|
|
|
|
using nisps::BreakOrEngine;
|
|
|
|
|
|
using nisps::ElysiamorfEngine;
|
|
|
|
|
|
using nisps::AnalysisEngine;
|
|
|
|
|
|
|
|
|
|
|
|
switch (h.kind) {
|
|
|
|
|
|
case EngineKind::NoOp: process_typed<NoOpEngine>(h.ptr, in_l, in_r, out_l, out_r, n_samples); break;
|
|
|
|
|
|
case EngineKind::PAFSynth: process_typed<PAFSynthEngine>(h.ptr, in_l, in_r, out_l, out_r, n_samples); break;
|
|
|
|
|
|
case EngineKind::ChannelStrip: process_typed<ChannelStripEngine>(h.ptr, in_l, in_r, out_l, out_r, n_samples); break;
|
|
|
|
|
|
case EngineKind::XIASRI: process_typed<XIASRIEngine>(h.ptr, in_l, in_r, out_l, out_r, n_samples); break;
|
|
|
|
|
|
case EngineKind::VerbFX: process_typed<VerbFXEngine>(h.ptr, in_l, in_r, out_l, out_r, n_samples); break;
|
|
|
|
|
|
case EngineKind::MEMLCelium: process_typed<MEMLCeliumEngine>(h.ptr, in_l, in_r, out_l, out_r, n_samples); break;
|
|
|
|
|
|
case EngineKind::BreakOr: process_typed<BreakOrEngine>(h.ptr, in_l, in_r, out_l, out_r, n_samples); break;
|
|
|
|
|
|
case EngineKind::Elysiamorf: process_typed<ElysiamorfEngine>(h.ptr, in_l, in_r, out_l, out_r, n_samples); break;
|
|
|
|
|
|
case EngineKind::Analysis: process_typed<AnalysisEngine>(h.ptr, in_l, in_r, out_l, out_r, n_samples); break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
} // anonymous namespace
|
|
|
|
|
|
|
|
|
|
|
|
extern "C" {
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// ML lifecycle
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
|
|
|
|
void* nisps_ml_create(int input_size, int output_size,
|
feat(wasm)!: P2.2 — nisps_ml_create honours dims; runtime-shaped browser MLP + reshape
Operator-approved ABI change (P2 stop-point). The WASM MLP is now
MLPCore<DynamicStorage>:
- nisps_ml_create(input, output, hidden[3], n, seed) honours its args;
non-positive/null fall back to the historical 32→[10,14,18]→126, so
every pre-P2 caller (manifold, worker, parity harness) stays
bit-identical. Invalid/oversized dims (>4096) → null.
- NEW nisps_ml_reshape(ml, in, out, hidden, n, spread): fresh net at the
new dims, warm-started via nisps/ml/warm_start.hpp (overlapping region
copied; rest keeps spread init); feedback controller re-created (state
resets — reset-on-reshape modal is the front-end contract). Failure
leaves the old net untouched.
- nisps_ml_describe(ml, out): takes the handle; null reports defaults.
- FeedbackController got the same storage split: algorithms in
FeedbackControllerCore<FbStorage>; FixedFeedbackStorage keeps firmware/
tests source-identical via the old alias; DynamicFeedbackStorage (one
arena) sizes to the runtime net. Firmware .text unchanged (122692).
- MLHandle: per-instance scratch vectors; dropped the dead 2MB
batch_out_scratch.
- TS: types.ts decls (+_nisps_ml_reshape), wasm-iml re-describes the
created instance, worker carries a shape-contract note for P2.3.
Verified: ctest 4/4 incl. new warm-start grow/shrink test; reshape ABI
smoke (dims honoured, overlap survives, invalid rejected, outputs
bounded); parity PASS unchanged (2.4e-7); lint clean; manifold 9 unit +
20 e2e green; firmware .text 122692 (+0.30% vs pre-P2 baseline).
2026-07-14 03:38:06 +02:00
|
|
|
|
const int* hidden, int n_hidden,
|
feat(playground/wasm): WASM bridge between C++ core and SolidJS playground (meml-tgm)
Stream 7 wires nisps/ml + nisps/engines into the playground via Emscripten.
Highlights:
- nisps/wasm/bindings.cpp: flat C API per architecture.md §6.2. Fixed-arch
MLP<2, 10, 14, 18, 126>; engine string→type dispatch table with NoOp
fallback.
- scripts/build-wasm.sh: emcc invocation, MODULARIZE=1, exports listed
explicitly; produces playground/public/nisps.{js,wasm}.
- playground/src/ml/wasm-iml.ts: main-thread MLP host (sync inference,
sync training, RL ops, weights I/O, layer stats, localStorage).
- playground/src/ml/wasm-worker.ts: disposable Web Worker for off-thread
async training, owns its own WASM instance.
- playground/src/ml/dataset.ts: Float32Array-backed FIFO with sample-weight
modes (uniform/global/local/combined). Port of legacy dataset.js.
- playground/src/audio/engine-host.ts: AudioContext + AudioWorkletNode
lifecycle, with start/stop/setEngine/setParams.
- playground/src/audio/worklet/nisps-processor.ts: WASM-loading
AudioWorkletProcessor that runs engine.process_block per 128-sample
block. Loads its own WASM instance from main-thread-supplied bytes
(no fetch in worklet).
- playground/src/stores/ml-store.ts: wired stub methods to WasmIML
singleton; lazy initialize().
- playground/src/debug/probe.ts: window.__nisps now calls real WasmIML
via the store; lazy-init on first use.
Verified:
- bash scripts/build-wasm.sh succeeds (94 KB nisps.wasm)
- bun run typecheck OK
- bun run build OK (production bundle)
- vite dev server serves /nisps.{js,wasm} with COOP/COEP
Known limitation: WASM is fixed at one MLP shape. Multi-arch deferred —
documented in nisps/wasm/README.md.
2026-04-29 15:36:29 +02:00
|
|
|
|
uint32_t seed) {
|
feat(wasm)!: P2.2 — nisps_ml_create honours dims; runtime-shaped browser MLP + reshape
Operator-approved ABI change (P2 stop-point). The WASM MLP is now
MLPCore<DynamicStorage>:
- nisps_ml_create(input, output, hidden[3], n, seed) honours its args;
non-positive/null fall back to the historical 32→[10,14,18]→126, so
every pre-P2 caller (manifold, worker, parity harness) stays
bit-identical. Invalid/oversized dims (>4096) → null.
- NEW nisps_ml_reshape(ml, in, out, hidden, n, spread): fresh net at the
new dims, warm-started via nisps/ml/warm_start.hpp (overlapping region
copied; rest keeps spread init); feedback controller re-created (state
resets — reset-on-reshape modal is the front-end contract). Failure
leaves the old net untouched.
- nisps_ml_describe(ml, out): takes the handle; null reports defaults.
- FeedbackController got the same storage split: algorithms in
FeedbackControllerCore<FbStorage>; FixedFeedbackStorage keeps firmware/
tests source-identical via the old alias; DynamicFeedbackStorage (one
arena) sizes to the runtime net. Firmware .text unchanged (122692).
- MLHandle: per-instance scratch vectors; dropped the dead 2MB
batch_out_scratch.
- TS: types.ts decls (+_nisps_ml_reshape), wasm-iml re-describes the
created instance, worker carries a shape-contract note for P2.3.
Verified: ctest 4/4 incl. new warm-start grow/shrink test; reshape ABI
smoke (dims honoured, overlap survives, invalid rejected, outputs
bounded); parity PASS unchanged (2.4e-7); lint clean; manifold 9 unit +
20 e2e green; firmware .text 122692 (+0.30% vs pre-P2 baseline).
2026-07-14 03:38:06 +02:00
|
|
|
|
// Dimensions are HONOURED (runtime-shaped MLP); non-positive/missing args
|
|
|
|
|
|
// fall back to the historical defaults. See file header.
|
feat(playground/wasm): WASM bridge between C++ core and SolidJS playground (meml-tgm)
Stream 7 wires nisps/ml + nisps/engines into the playground via Emscripten.
Highlights:
- nisps/wasm/bindings.cpp: flat C API per architecture.md §6.2. Fixed-arch
MLP<2, 10, 14, 18, 126>; engine string→type dispatch table with NoOp
fallback.
- scripts/build-wasm.sh: emcc invocation, MODULARIZE=1, exports listed
explicitly; produces playground/public/nisps.{js,wasm}.
- playground/src/ml/wasm-iml.ts: main-thread MLP host (sync inference,
sync training, RL ops, weights I/O, layer stats, localStorage).
- playground/src/ml/wasm-worker.ts: disposable Web Worker for off-thread
async training, owns its own WASM instance.
- playground/src/ml/dataset.ts: Float32Array-backed FIFO with sample-weight
modes (uniform/global/local/combined). Port of legacy dataset.js.
- playground/src/audio/engine-host.ts: AudioContext + AudioWorkletNode
lifecycle, with start/stop/setEngine/setParams.
- playground/src/audio/worklet/nisps-processor.ts: WASM-loading
AudioWorkletProcessor that runs engine.process_block per 128-sample
block. Loads its own WASM instance from main-thread-supplied bytes
(no fetch in worklet).
- playground/src/stores/ml-store.ts: wired stub methods to WasmIML
singleton; lazy initialize().
- playground/src/debug/probe.ts: window.__nisps now calls real WasmIML
via the store; lazy-init on first use.
Verified:
- bash scripts/build-wasm.sh succeeds (94 KB nisps.wasm)
- bun run typecheck OK
- bun run build OK (production bundle)
- vite dev server serves /nisps.{js,wasm} with COOP/COEP
Known limitation: WASM is fixed at one MLP shape. Multi-arch deferred —
documented in nisps/wasm/README.md.
2026-04-29 15:36:29 +02:00
|
|
|
|
//
|
|
|
|
|
|
// NOTE: the C++ Rng takes uint64_t; we sign-extend the 32-bit seed into
|
|
|
|
|
|
// the high 32 bits via xor-shift so callers passing zero still get a
|
|
|
|
|
|
// non-degenerate seed. Truly 64-bit seeds are not exposed to JS — the
|
feat(wasm)!: P2.2 — nisps_ml_create honours dims; runtime-shaped browser MLP + reshape
Operator-approved ABI change (P2 stop-point). The WASM MLP is now
MLPCore<DynamicStorage>:
- nisps_ml_create(input, output, hidden[3], n, seed) honours its args;
non-positive/null fall back to the historical 32→[10,14,18]→126, so
every pre-P2 caller (manifold, worker, parity harness) stays
bit-identical. Invalid/oversized dims (>4096) → null.
- NEW nisps_ml_reshape(ml, in, out, hidden, n, spread): fresh net at the
new dims, warm-started via nisps/ml/warm_start.hpp (overlapping region
copied; rest keeps spread init); feedback controller re-created (state
resets — reset-on-reshape modal is the front-end contract). Failure
leaves the old net untouched.
- nisps_ml_describe(ml, out): takes the handle; null reports defaults.
- FeedbackController got the same storage split: algorithms in
FeedbackControllerCore<FbStorage>; FixedFeedbackStorage keeps firmware/
tests source-identical via the old alias; DynamicFeedbackStorage (one
arena) sizes to the runtime net. Firmware .text unchanged (122692).
- MLHandle: per-instance scratch vectors; dropped the dead 2MB
batch_out_scratch.
- TS: types.ts decls (+_nisps_ml_reshape), wasm-iml re-describes the
created instance, worker carries a shape-contract note for P2.3.
Verified: ctest 4/4 incl. new warm-start grow/shrink test; reshape ABI
smoke (dims honoured, overlap survives, invalid rejected, outputs
bounded); parity PASS unchanged (2.4e-7); lint clean; manifold 9 unit +
20 e2e green; firmware .text 122692 (+0.30% vs pre-P2 baseline).
2026-07-14 03:38:06 +02:00
|
|
|
|
// front-end doesn't need them, and avoiding BigInt at the boundary
|
feat(playground/wasm): WASM bridge between C++ core and SolidJS playground (meml-tgm)
Stream 7 wires nisps/ml + nisps/engines into the playground via Emscripten.
Highlights:
- nisps/wasm/bindings.cpp: flat C API per architecture.md §6.2. Fixed-arch
MLP<2, 10, 14, 18, 126>; engine string→type dispatch table with NoOp
fallback.
- scripts/build-wasm.sh: emcc invocation, MODULARIZE=1, exports listed
explicitly; produces playground/public/nisps.{js,wasm}.
- playground/src/ml/wasm-iml.ts: main-thread MLP host (sync inference,
sync training, RL ops, weights I/O, layer stats, localStorage).
- playground/src/ml/wasm-worker.ts: disposable Web Worker for off-thread
async training, owns its own WASM instance.
- playground/src/ml/dataset.ts: Float32Array-backed FIFO with sample-weight
modes (uniform/global/local/combined). Port of legacy dataset.js.
- playground/src/audio/engine-host.ts: AudioContext + AudioWorkletNode
lifecycle, with start/stop/setEngine/setParams.
- playground/src/audio/worklet/nisps-processor.ts: WASM-loading
AudioWorkletProcessor that runs engine.process_block per 128-sample
block. Loads its own WASM instance from main-thread-supplied bytes
(no fetch in worklet).
- playground/src/stores/ml-store.ts: wired stub methods to WasmIML
singleton; lazy initialize().
- playground/src/debug/probe.ts: window.__nisps now calls real WasmIML
via the store; lazy-init on first use.
Verified:
- bash scripts/build-wasm.sh succeeds (94 KB nisps.wasm)
- bun run typecheck OK
- bun run build OK (production bundle)
- vite dev server serves /nisps.{js,wasm} with COOP/COEP
Known limitation: WASM is fixed at one MLP shape. Multi-arch deferred —
documented in nisps/wasm/README.md.
2026-04-29 15:36:29 +02:00
|
|
|
|
// simplifies both wasm-iml.ts and wasm-worker.ts.
|
feat(wasm)!: P2.2 — nisps_ml_create honours dims; runtime-shaped browser MLP + reshape
Operator-approved ABI change (P2 stop-point). The WASM MLP is now
MLPCore<DynamicStorage>:
- nisps_ml_create(input, output, hidden[3], n, seed) honours its args;
non-positive/null fall back to the historical 32→[10,14,18]→126, so
every pre-P2 caller (manifold, worker, parity harness) stays
bit-identical. Invalid/oversized dims (>4096) → null.
- NEW nisps_ml_reshape(ml, in, out, hidden, n, spread): fresh net at the
new dims, warm-started via nisps/ml/warm_start.hpp (overlapping region
copied; rest keeps spread init); feedback controller re-created (state
resets — reset-on-reshape modal is the front-end contract). Failure
leaves the old net untouched.
- nisps_ml_describe(ml, out): takes the handle; null reports defaults.
- FeedbackController got the same storage split: algorithms in
FeedbackControllerCore<FbStorage>; FixedFeedbackStorage keeps firmware/
tests source-identical via the old alias; DynamicFeedbackStorage (one
arena) sizes to the runtime net. Firmware .text unchanged (122692).
- MLHandle: per-instance scratch vectors; dropped the dead 2MB
batch_out_scratch.
- TS: types.ts decls (+_nisps_ml_reshape), wasm-iml re-describes the
created instance, worker carries a shape-contract note for P2.3.
Verified: ctest 4/4 incl. new warm-start grow/shrink test; reshape ABI
smoke (dims honoured, overlap survives, invalid rejected, outputs
bounded); parity PASS unchanged (2.4e-7); lint clean; manifold 9 unit +
20 e2e green; firmware .text 122692 (+0.30% vs pre-P2 baseline).
2026-07-14 03:38:06 +02:00
|
|
|
|
const MlDims d = sanitise_dims(input_size, output_size, hidden, n_hidden);
|
|
|
|
|
|
if (!d.ok) return nullptr;
|
feat(playground/wasm): WASM bridge between C++ core and SolidJS playground (meml-tgm)
Stream 7 wires nisps/ml + nisps/engines into the playground via Emscripten.
Highlights:
- nisps/wasm/bindings.cpp: flat C API per architecture.md §6.2. Fixed-arch
MLP<2, 10, 14, 18, 126>; engine string→type dispatch table with NoOp
fallback.
- scripts/build-wasm.sh: emcc invocation, MODULARIZE=1, exports listed
explicitly; produces playground/public/nisps.{js,wasm}.
- playground/src/ml/wasm-iml.ts: main-thread MLP host (sync inference,
sync training, RL ops, weights I/O, layer stats, localStorage).
- playground/src/ml/wasm-worker.ts: disposable Web Worker for off-thread
async training, owns its own WASM instance.
- playground/src/ml/dataset.ts: Float32Array-backed FIFO with sample-weight
modes (uniform/global/local/combined). Port of legacy dataset.js.
- playground/src/audio/engine-host.ts: AudioContext + AudioWorkletNode
lifecycle, with start/stop/setEngine/setParams.
- playground/src/audio/worklet/nisps-processor.ts: WASM-loading
AudioWorkletProcessor that runs engine.process_block per 128-sample
block. Loads its own WASM instance from main-thread-supplied bytes
(no fetch in worklet).
- playground/src/stores/ml-store.ts: wired stub methods to WasmIML
singleton; lazy initialize().
- playground/src/debug/probe.ts: window.__nisps now calls real WasmIML
via the store; lazy-init on first use.
Verified:
- bash scripts/build-wasm.sh succeeds (94 KB nisps.wasm)
- bun run typecheck OK
- bun run build OK (production bundle)
- vite dev server serves /nisps.{js,wasm} with COOP/COEP
Known limitation: WASM is fixed at one MLP shape. Multi-arch deferred —
documented in nisps/wasm/README.md.
2026-04-29 15:36:29 +02:00
|
|
|
|
const std::uint64_t s64 = static_cast<std::uint64_t>(seed) ^
|
|
|
|
|
|
(static_cast<std::uint64_t>(seed) << 32);
|
feat(wasm)!: P2.2 — nisps_ml_create honours dims; runtime-shaped browser MLP + reshape
Operator-approved ABI change (P2 stop-point). The WASM MLP is now
MLPCore<DynamicStorage>:
- nisps_ml_create(input, output, hidden[3], n, seed) honours its args;
non-positive/null fall back to the historical 32→[10,14,18]→126, so
every pre-P2 caller (manifold, worker, parity harness) stays
bit-identical. Invalid/oversized dims (>4096) → null.
- NEW nisps_ml_reshape(ml, in, out, hidden, n, spread): fresh net at the
new dims, warm-started via nisps/ml/warm_start.hpp (overlapping region
copied; rest keeps spread init); feedback controller re-created (state
resets — reset-on-reshape modal is the front-end contract). Failure
leaves the old net untouched.
- nisps_ml_describe(ml, out): takes the handle; null reports defaults.
- FeedbackController got the same storage split: algorithms in
FeedbackControllerCore<FbStorage>; FixedFeedbackStorage keeps firmware/
tests source-identical via the old alias; DynamicFeedbackStorage (one
arena) sizes to the runtime net. Firmware .text unchanged (122692).
- MLHandle: per-instance scratch vectors; dropped the dead 2MB
batch_out_scratch.
- TS: types.ts decls (+_nisps_ml_reshape), wasm-iml re-describes the
created instance, worker carries a shape-contract note for P2.3.
Verified: ctest 4/4 incl. new warm-start grow/shrink test; reshape ABI
smoke (dims honoured, overlap survives, invalid rejected, outputs
bounded); parity PASS unchanged (2.4e-7); lint clean; manifold 9 unit +
20 e2e green; firmware .text 122692 (+0.30% vs pre-P2 baseline).
2026-07-14 03:38:06 +02:00
|
|
|
|
auto* h = new MLHandle(s64, d);
|
|
|
|
|
|
if (!h->valid()) {
|
|
|
|
|
|
delete h;
|
|
|
|
|
|
return nullptr;
|
|
|
|
|
|
}
|
feat(playground/wasm): WASM bridge between C++ core and SolidJS playground (meml-tgm)
Stream 7 wires nisps/ml + nisps/engines into the playground via Emscripten.
Highlights:
- nisps/wasm/bindings.cpp: flat C API per architecture.md §6.2. Fixed-arch
MLP<2, 10, 14, 18, 126>; engine string→type dispatch table with NoOp
fallback.
- scripts/build-wasm.sh: emcc invocation, MODULARIZE=1, exports listed
explicitly; produces playground/public/nisps.{js,wasm}.
- playground/src/ml/wasm-iml.ts: main-thread MLP host (sync inference,
sync training, RL ops, weights I/O, layer stats, localStorage).
- playground/src/ml/wasm-worker.ts: disposable Web Worker for off-thread
async training, owns its own WASM instance.
- playground/src/ml/dataset.ts: Float32Array-backed FIFO with sample-weight
modes (uniform/global/local/combined). Port of legacy dataset.js.
- playground/src/audio/engine-host.ts: AudioContext + AudioWorkletNode
lifecycle, with start/stop/setEngine/setParams.
- playground/src/audio/worklet/nisps-processor.ts: WASM-loading
AudioWorkletProcessor that runs engine.process_block per 128-sample
block. Loads its own WASM instance from main-thread-supplied bytes
(no fetch in worklet).
- playground/src/stores/ml-store.ts: wired stub methods to WasmIML
singleton; lazy initialize().
- playground/src/debug/probe.ts: window.__nisps now calls real WasmIML
via the store; lazy-init on first use.
Verified:
- bash scripts/build-wasm.sh succeeds (94 KB nisps.wasm)
- bun run typecheck OK
- bun run build OK (production bundle)
- vite dev server serves /nisps.{js,wasm} with COOP/COEP
Known limitation: WASM is fixed at one MLP shape. Multi-arch deferred —
documented in nisps/wasm/README.md.
2026-04-29 15:36:29 +02:00
|
|
|
|
return static_cast<void*>(h);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat(wasm)!: P2.2 — nisps_ml_create honours dims; runtime-shaped browser MLP + reshape
Operator-approved ABI change (P2 stop-point). The WASM MLP is now
MLPCore<DynamicStorage>:
- nisps_ml_create(input, output, hidden[3], n, seed) honours its args;
non-positive/null fall back to the historical 32→[10,14,18]→126, so
every pre-P2 caller (manifold, worker, parity harness) stays
bit-identical. Invalid/oversized dims (>4096) → null.
- NEW nisps_ml_reshape(ml, in, out, hidden, n, spread): fresh net at the
new dims, warm-started via nisps/ml/warm_start.hpp (overlapping region
copied; rest keeps spread init); feedback controller re-created (state
resets — reset-on-reshape modal is the front-end contract). Failure
leaves the old net untouched.
- nisps_ml_describe(ml, out): takes the handle; null reports defaults.
- FeedbackController got the same storage split: algorithms in
FeedbackControllerCore<FbStorage>; FixedFeedbackStorage keeps firmware/
tests source-identical via the old alias; DynamicFeedbackStorage (one
arena) sizes to the runtime net. Firmware .text unchanged (122692).
- MLHandle: per-instance scratch vectors; dropped the dead 2MB
batch_out_scratch.
- TS: types.ts decls (+_nisps_ml_reshape), wasm-iml re-describes the
created instance, worker carries a shape-contract note for P2.3.
Verified: ctest 4/4 incl. new warm-start grow/shrink test; reshape ABI
smoke (dims honoured, overlap survives, invalid rejected, outputs
bounded); parity PASS unchanged (2.4e-7); lint clean; manifold 9 unit +
20 e2e green; firmware .text 122692 (+0.30% vs pre-P2 baseline).
2026-07-14 03:38:06 +02:00
|
|
|
|
// Reshape: construct a NEW net at the requested dimensions (same seed
|
|
|
|
|
|
// stream restart, fresh spread-init), warm-start it with the overlapping
|
|
|
|
|
|
// weights of the current net, then swap it in. The feedback controller is
|
|
|
|
|
|
// re-created at the new dims (exploration state resets). Returns 1 on
|
|
|
|
|
|
// success; 0 leaves the existing net untouched.
|
|
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
|
|
|
|
int nisps_ml_reshape(void* ml, int input_size, int output_size,
|
|
|
|
|
|
const int* hidden, int n_hidden, float spread) {
|
|
|
|
|
|
if (!ml) return 0;
|
|
|
|
|
|
auto* h = static_cast<MLHandle*>(ml);
|
|
|
|
|
|
const MlDims d = sanitise_dims(input_size, output_size, hidden, n_hidden);
|
|
|
|
|
|
if (!d.ok) return 0;
|
|
|
|
|
|
|
2026-07-25 15:16:37 +02:00
|
|
|
|
const auto feedback_mode = h->feedback.mode();
|
|
|
|
|
|
const auto avoid_style = h->feedback.avoid_style();
|
feat(wasm)!: P2.2 — nisps_ml_create honours dims; runtime-shaped browser MLP + reshape
Operator-approved ABI change (P2 stop-point). The WASM MLP is now
MLPCore<DynamicStorage>:
- nisps_ml_create(input, output, hidden[3], n, seed) honours its args;
non-positive/null fall back to the historical 32→[10,14,18]→126, so
every pre-P2 caller (manifold, worker, parity harness) stays
bit-identical. Invalid/oversized dims (>4096) → null.
- NEW nisps_ml_reshape(ml, in, out, hidden, n, spread): fresh net at the
new dims, warm-started via nisps/ml/warm_start.hpp (overlapping region
copied; rest keeps spread init); feedback controller re-created (state
resets — reset-on-reshape modal is the front-end contract). Failure
leaves the old net untouched.
- nisps_ml_describe(ml, out): takes the handle; null reports defaults.
- FeedbackController got the same storage split: algorithms in
FeedbackControllerCore<FbStorage>; FixedFeedbackStorage keeps firmware/
tests source-identical via the old alias; DynamicFeedbackStorage (one
arena) sizes to the runtime net. Firmware .text unchanged (122692).
- MLHandle: per-instance scratch vectors; dropped the dead 2MB
batch_out_scratch.
- TS: types.ts decls (+_nisps_ml_reshape), wasm-iml re-describes the
created instance, worker carries a shape-contract note for P2.3.
Verified: ctest 4/4 incl. new warm-start grow/shrink test; reshape ABI
smoke (dims honoured, overlap survives, invalid rejected, outputs
bounded); parity PASS unchanged (2.4e-7); lint clean; manifold 9 unit +
20 e2e green; firmware .text 122692 (+0.30% vs pre-P2 baseline).
2026-07-14 03:38:06 +02:00
|
|
|
|
BrowserMLP fresh(h->seed64, d.n_in, std::span<const std::size_t>(d.hidden, 3u), d.n_out);
|
|
|
|
|
|
if (!fresh.valid()) return 0;
|
|
|
|
|
|
fresh.draw_weights(spread);
|
|
|
|
|
|
nisps::ml::warm_start_copy(fresh, h->mlp);
|
|
|
|
|
|
|
|
|
|
|
|
BrowserFeedback fb(h->seed64 ^ kFeedbackSalt, d.n_out, fresh.weight_count(),
|
feat(ml)!: P3 core — geometric dislike in nisps/, jolt/OU + geo ABI
Geometric dislike (rl-feedback-design §2.1/§4; upstream InterfaceRL @
0a541cc ported verbatim, constants included):
- nisps/ml/replay.hpp: ReplayView over storage-owned buffers — deepen-or-
store negatives (dedup 0.05, clamp -16), k-NN positive centroid with
deterministic index tie-break + fixed accumulation order, proportional
decay (0.0025*max(|r|,1)) + eviction, order-preserving compaction.
- nisps/ml/geo_push.hpp: push-away target (pushStep clamp(|avgNeg|,.25,1)
*0.5, taper /(1+len), useRandom on len<=1e-4 via nisps::Rng — the single
deliberate divergence from libc rand()), negLRRatio 0.5-0.4*negFraction.
- mlp.hpp: train_targets(input, computed-target, lr, out_mask) — trains
toward computed targets (negative lr = cold-start train-away); solo/
focus gating zeroes masked derivs.
- feedback.hpp: AvoidStyle {Geometric (new default), Diffuse (legacy
move_weights, kept for A/B)}; dislike_geometric() collapses upstream's
press+optimise into one synchronous call; on_up in geometric Avoid
feeds the positive centroid; dislike-multiplier bookkeeping. Storage
gains replay buffers (Fixed: ReplayCap=32 firmware default ≈ +8KB SRAM;
Dynamic arena: cap 64).
- bindings: nisps_ml_feedback_{dislike_geometric,store_positive,
positive_count,negative_count,set_avoid_style} + P3.2 jolt/OU ABI:
nisps_ml_jolt_{press,step,release,active,lr_scale,tick_lr_ramp},
nisps_ml_explore_{intensity,get_intensity,apply} (OUNoise<4096>
over-provisioned; same code the firmware ModeBase runs).
- parity v4: Stage 6 scripted geometric session (2 likes → 2 dislikes,
f32-exact heard vectors via Math.fround) — 961 floats PASS at 2.4e-7.
- tests: test_mlp_geo_dislike.cpp (replay dedup/deepen/clamp, centroid
tie-break, push direction/taper/mask/clamp, cold-start inertness +
train-away, determinism, Diffuse legacy); legacy Avoid test pinned to
Diffuse per the ADR's deliberate-break note.
Firmware: PAFSynth .text/.data unchanged (geometric path not referenced
by current glue). NOTE: discovered pre-existing bug 10c3e55c — the
explore/place wiring is linker-GC'd out of the PAFSynth ELF (predates
this refactor; evidence in the ergo task).
2026-07-14 04:16:21 +02:00
|
|
|
|
kFeedbackUndoDepth, d.n_in, kFeedbackReplayCap);
|
feat(wasm)!: P2.2 — nisps_ml_create honours dims; runtime-shaped browser MLP + reshape
Operator-approved ABI change (P2 stop-point). The WASM MLP is now
MLPCore<DynamicStorage>:
- nisps_ml_create(input, output, hidden[3], n, seed) honours its args;
non-positive/null fall back to the historical 32→[10,14,18]→126, so
every pre-P2 caller (manifold, worker, parity harness) stays
bit-identical. Invalid/oversized dims (>4096) → null.
- NEW nisps_ml_reshape(ml, in, out, hidden, n, spread): fresh net at the
new dims, warm-started via nisps/ml/warm_start.hpp (overlapping region
copied; rest keeps spread init); feedback controller re-created (state
resets — reset-on-reshape modal is the front-end contract). Failure
leaves the old net untouched.
- nisps_ml_describe(ml, out): takes the handle; null reports defaults.
- FeedbackController got the same storage split: algorithms in
FeedbackControllerCore<FbStorage>; FixedFeedbackStorage keeps firmware/
tests source-identical via the old alias; DynamicFeedbackStorage (one
arena) sizes to the runtime net. Firmware .text unchanged (122692).
- MLHandle: per-instance scratch vectors; dropped the dead 2MB
batch_out_scratch.
- TS: types.ts decls (+_nisps_ml_reshape), wasm-iml re-describes the
created instance, worker carries a shape-contract note for P2.3.
Verified: ctest 4/4 incl. new warm-start grow/shrink test; reshape ABI
smoke (dims honoured, overlap survives, invalid rejected, outputs
bounded); parity PASS unchanged (2.4e-7); lint clean; manifold 9 unit +
20 e2e green; firmware .text 122692 (+0.30% vs pre-P2 baseline).
2026-07-14 03:38:06 +02:00
|
|
|
|
if (!fb.valid()) return 0;
|
2026-07-25 15:16:37 +02:00
|
|
|
|
fb.set_avoid_style(avoid_style);
|
|
|
|
|
|
fb.set_mode(feedback_mode, fresh);
|
feat(wasm)!: P2.2 — nisps_ml_create honours dims; runtime-shaped browser MLP + reshape
Operator-approved ABI change (P2 stop-point). The WASM MLP is now
MLPCore<DynamicStorage>:
- nisps_ml_create(input, output, hidden[3], n, seed) honours its args;
non-positive/null fall back to the historical 32→[10,14,18]→126, so
every pre-P2 caller (manifold, worker, parity harness) stays
bit-identical. Invalid/oversized dims (>4096) → null.
- NEW nisps_ml_reshape(ml, in, out, hidden, n, spread): fresh net at the
new dims, warm-started via nisps/ml/warm_start.hpp (overlapping region
copied; rest keeps spread init); feedback controller re-created (state
resets — reset-on-reshape modal is the front-end contract). Failure
leaves the old net untouched.
- nisps_ml_describe(ml, out): takes the handle; null reports defaults.
- FeedbackController got the same storage split: algorithms in
FeedbackControllerCore<FbStorage>; FixedFeedbackStorage keeps firmware/
tests source-identical via the old alias; DynamicFeedbackStorage (one
arena) sizes to the runtime net. Firmware .text unchanged (122692).
- MLHandle: per-instance scratch vectors; dropped the dead 2MB
batch_out_scratch.
- TS: types.ts decls (+_nisps_ml_reshape), wasm-iml re-describes the
created instance, worker carries a shape-contract note for P2.3.
Verified: ctest 4/4 incl. new warm-start grow/shrink test; reshape ABI
smoke (dims honoured, overlap survives, invalid rejected, outputs
bounded); parity PASS unchanged (2.4e-7); lint clean; manifold 9 unit +
20 e2e green; firmware .text 122692 (+0.30% vs pre-P2 baseline).
2026-07-14 03:38:06 +02:00
|
|
|
|
|
|
|
|
|
|
h->mlp = static_cast<BrowserMLP&&>(fresh);
|
|
|
|
|
|
h->feedback = static_cast<BrowserFeedback&&>(fb);
|
|
|
|
|
|
h->output_scratch.assign(d.n_out, 0.f);
|
|
|
|
|
|
h->feedback_static_scratch.assign(d.n_out, 0.f);
|
feat(ml)!: P3 core — geometric dislike in nisps/, jolt/OU + geo ABI
Geometric dislike (rl-feedback-design §2.1/§4; upstream InterfaceRL @
0a541cc ported verbatim, constants included):
- nisps/ml/replay.hpp: ReplayView over storage-owned buffers — deepen-or-
store negatives (dedup 0.05, clamp -16), k-NN positive centroid with
deterministic index tie-break + fixed accumulation order, proportional
decay (0.0025*max(|r|,1)) + eviction, order-preserving compaction.
- nisps/ml/geo_push.hpp: push-away target (pushStep clamp(|avgNeg|,.25,1)
*0.5, taper /(1+len), useRandom on len<=1e-4 via nisps::Rng — the single
deliberate divergence from libc rand()), negLRRatio 0.5-0.4*negFraction.
- mlp.hpp: train_targets(input, computed-target, lr, out_mask) — trains
toward computed targets (negative lr = cold-start train-away); solo/
focus gating zeroes masked derivs.
- feedback.hpp: AvoidStyle {Geometric (new default), Diffuse (legacy
move_weights, kept for A/B)}; dislike_geometric() collapses upstream's
press+optimise into one synchronous call; on_up in geometric Avoid
feeds the positive centroid; dislike-multiplier bookkeeping. Storage
gains replay buffers (Fixed: ReplayCap=32 firmware default ≈ +8KB SRAM;
Dynamic arena: cap 64).
- bindings: nisps_ml_feedback_{dislike_geometric,store_positive,
positive_count,negative_count,set_avoid_style} + P3.2 jolt/OU ABI:
nisps_ml_jolt_{press,step,release,active,lr_scale,tick_lr_ramp},
nisps_ml_explore_{intensity,get_intensity,apply} (OUNoise<4096>
over-provisioned; same code the firmware ModeBase runs).
- parity v4: Stage 6 scripted geometric session (2 likes → 2 dislikes,
f32-exact heard vectors via Math.fround) — 961 floats PASS at 2.4e-7.
- tests: test_mlp_geo_dislike.cpp (replay dedup/deepen/clamp, centroid
tie-break, push direction/taper/mask/clamp, cold-start inertness +
train-away, determinism, Diffuse legacy); legacy Avoid test pinned to
Diffuse per the ADR's deliberate-break note.
Firmware: PAFSynth .text/.data unchanged (geometric path not referenced
by current glue). NOTE: discovered pre-existing bug 10c3e55c — the
explore/place wiring is linker-GC'd out of the PAFSynth ELF (predates
this refactor; evidence in the ergo task).
2026-07-14 04:16:21 +02:00
|
|
|
|
h->jolt.release();
|
|
|
|
|
|
h->ou.reset();
|
|
|
|
|
|
h->jolt_scratch.assign(h->mlp.weight_count(), 0.f);
|
feat(wasm)!: P2.2 — nisps_ml_create honours dims; runtime-shaped browser MLP + reshape
Operator-approved ABI change (P2 stop-point). The WASM MLP is now
MLPCore<DynamicStorage>:
- nisps_ml_create(input, output, hidden[3], n, seed) honours its args;
non-positive/null fall back to the historical 32→[10,14,18]→126, so
every pre-P2 caller (manifold, worker, parity harness) stays
bit-identical. Invalid/oversized dims (>4096) → null.
- NEW nisps_ml_reshape(ml, in, out, hidden, n, spread): fresh net at the
new dims, warm-started via nisps/ml/warm_start.hpp (overlapping region
copied; rest keeps spread init); feedback controller re-created (state
resets — reset-on-reshape modal is the front-end contract). Failure
leaves the old net untouched.
- nisps_ml_describe(ml, out): takes the handle; null reports defaults.
- FeedbackController got the same storage split: algorithms in
FeedbackControllerCore<FbStorage>; FixedFeedbackStorage keeps firmware/
tests source-identical via the old alias; DynamicFeedbackStorage (one
arena) sizes to the runtime net. Firmware .text unchanged (122692).
- MLHandle: per-instance scratch vectors; dropped the dead 2MB
batch_out_scratch.
- TS: types.ts decls (+_nisps_ml_reshape), wasm-iml re-describes the
created instance, worker carries a shape-contract note for P2.3.
Verified: ctest 4/4 incl. new warm-start grow/shrink test; reshape ABI
smoke (dims honoured, overlap survives, invalid rejected, outputs
bounded); parity PASS unchanged (2.4e-7); lint clean; manifold 9 unit +
20 e2e green; firmware .text 122692 (+0.30% vs pre-P2 baseline).
2026-07-14 03:38:06 +02:00
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat(playground/wasm): WASM bridge between C++ core and SolidJS playground (meml-tgm)
Stream 7 wires nisps/ml + nisps/engines into the playground via Emscripten.
Highlights:
- nisps/wasm/bindings.cpp: flat C API per architecture.md §6.2. Fixed-arch
MLP<2, 10, 14, 18, 126>; engine string→type dispatch table with NoOp
fallback.
- scripts/build-wasm.sh: emcc invocation, MODULARIZE=1, exports listed
explicitly; produces playground/public/nisps.{js,wasm}.
- playground/src/ml/wasm-iml.ts: main-thread MLP host (sync inference,
sync training, RL ops, weights I/O, layer stats, localStorage).
- playground/src/ml/wasm-worker.ts: disposable Web Worker for off-thread
async training, owns its own WASM instance.
- playground/src/ml/dataset.ts: Float32Array-backed FIFO with sample-weight
modes (uniform/global/local/combined). Port of legacy dataset.js.
- playground/src/audio/engine-host.ts: AudioContext + AudioWorkletNode
lifecycle, with start/stop/setEngine/setParams.
- playground/src/audio/worklet/nisps-processor.ts: WASM-loading
AudioWorkletProcessor that runs engine.process_block per 128-sample
block. Loads its own WASM instance from main-thread-supplied bytes
(no fetch in worklet).
- playground/src/stores/ml-store.ts: wired stub methods to WasmIML
singleton; lazy initialize().
- playground/src/debug/probe.ts: window.__nisps now calls real WasmIML
via the store; lazy-init on first use.
Verified:
- bash scripts/build-wasm.sh succeeds (94 KB nisps.wasm)
- bun run typecheck OK
- bun run build OK (production bundle)
- vite dev server serves /nisps.{js,wasm} with COOP/COEP
Known limitation: WASM is fixed at one MLP shape. Multi-arch deferred —
documented in nisps/wasm/README.md.
2026-04-29 15:36:29 +02:00
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
|
|
|
|
void nisps_ml_destroy(void* ml) {
|
|
|
|
|
|
if (!ml) return;
|
|
|
|
|
|
delete static_cast<MLHandle*>(ml);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// ML inference
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
|
|
|
|
void nisps_ml_set_input(void* ml, int idx, float v) {
|
|
|
|
|
|
if (!ml) return;
|
|
|
|
|
|
auto* h = static_cast<MLHandle*>(ml);
|
|
|
|
|
|
if (idx < 0) return;
|
feat(wasm)!: P2.2 — nisps_ml_create honours dims; runtime-shaped browser MLP + reshape
Operator-approved ABI change (P2 stop-point). The WASM MLP is now
MLPCore<DynamicStorage>:
- nisps_ml_create(input, output, hidden[3], n, seed) honours its args;
non-positive/null fall back to the historical 32→[10,14,18]→126, so
every pre-P2 caller (manifold, worker, parity harness) stays
bit-identical. Invalid/oversized dims (>4096) → null.
- NEW nisps_ml_reshape(ml, in, out, hidden, n, spread): fresh net at the
new dims, warm-started via nisps/ml/warm_start.hpp (overlapping region
copied; rest keeps spread init); feedback controller re-created (state
resets — reset-on-reshape modal is the front-end contract). Failure
leaves the old net untouched.
- nisps_ml_describe(ml, out): takes the handle; null reports defaults.
- FeedbackController got the same storage split: algorithms in
FeedbackControllerCore<FbStorage>; FixedFeedbackStorage keeps firmware/
tests source-identical via the old alias; DynamicFeedbackStorage (one
arena) sizes to the runtime net. Firmware .text unchanged (122692).
- MLHandle: per-instance scratch vectors; dropped the dead 2MB
batch_out_scratch.
- TS: types.ts decls (+_nisps_ml_reshape), wasm-iml re-describes the
created instance, worker carries a shape-contract note for P2.3.
Verified: ctest 4/4 incl. new warm-start grow/shrink test; reshape ABI
smoke (dims honoured, overlap survives, invalid rejected, outputs
bounded); parity PASS unchanged (2.4e-7); lint clean; manifold 9 unit +
20 e2e green; firmware .text 122692 (+0.30% vs pre-P2 baseline).
2026-07-14 03:38:06 +02:00
|
|
|
|
if (static_cast<std::size_t>(idx) >= h->n_in()) return;
|
feat(playground/wasm): WASM bridge between C++ core and SolidJS playground (meml-tgm)
Stream 7 wires nisps/ml + nisps/engines into the playground via Emscripten.
Highlights:
- nisps/wasm/bindings.cpp: flat C API per architecture.md §6.2. Fixed-arch
MLP<2, 10, 14, 18, 126>; engine string→type dispatch table with NoOp
fallback.
- scripts/build-wasm.sh: emcc invocation, MODULARIZE=1, exports listed
explicitly; produces playground/public/nisps.{js,wasm}.
- playground/src/ml/wasm-iml.ts: main-thread MLP host (sync inference,
sync training, RL ops, weights I/O, layer stats, localStorage).
- playground/src/ml/wasm-worker.ts: disposable Web Worker for off-thread
async training, owns its own WASM instance.
- playground/src/ml/dataset.ts: Float32Array-backed FIFO with sample-weight
modes (uniform/global/local/combined). Port of legacy dataset.js.
- playground/src/audio/engine-host.ts: AudioContext + AudioWorkletNode
lifecycle, with start/stop/setEngine/setParams.
- playground/src/audio/worklet/nisps-processor.ts: WASM-loading
AudioWorkletProcessor that runs engine.process_block per 128-sample
block. Loads its own WASM instance from main-thread-supplied bytes
(no fetch in worklet).
- playground/src/stores/ml-store.ts: wired stub methods to WasmIML
singleton; lazy initialize().
- playground/src/debug/probe.ts: window.__nisps now calls real WasmIML
via the store; lazy-init on first use.
Verified:
- bash scripts/build-wasm.sh succeeds (94 KB nisps.wasm)
- bun run typecheck OK
- bun run build OK (production bundle)
- vite dev server serves /nisps.{js,wasm} with COOP/COEP
Known limitation: WASM is fixed at one MLP shape. Multi-arch deferred —
documented in nisps/wasm/README.md.
2026-04-29 15:36:29 +02:00
|
|
|
|
h->mlp.set_input(static_cast<std::size_t>(idx), v);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
|
|
|
|
void nisps_ml_process(void* ml) {
|
|
|
|
|
|
if (!ml) return;
|
|
|
|
|
|
auto* h = static_cast<MLHandle*>(ml);
|
|
|
|
|
|
h->mlp.process();
|
|
|
|
|
|
auto outs = h->mlp.outputs();
|
feat(wasm)!: P2.2 — nisps_ml_create honours dims; runtime-shaped browser MLP + reshape
Operator-approved ABI change (P2 stop-point). The WASM MLP is now
MLPCore<DynamicStorage>:
- nisps_ml_create(input, output, hidden[3], n, seed) honours its args;
non-positive/null fall back to the historical 32→[10,14,18]→126, so
every pre-P2 caller (manifold, worker, parity harness) stays
bit-identical. Invalid/oversized dims (>4096) → null.
- NEW nisps_ml_reshape(ml, in, out, hidden, n, spread): fresh net at the
new dims, warm-started via nisps/ml/warm_start.hpp (overlapping region
copied; rest keeps spread init); feedback controller re-created (state
resets — reset-on-reshape modal is the front-end contract). Failure
leaves the old net untouched.
- nisps_ml_describe(ml, out): takes the handle; null reports defaults.
- FeedbackController got the same storage split: algorithms in
FeedbackControllerCore<FbStorage>; FixedFeedbackStorage keeps firmware/
tests source-identical via the old alias; DynamicFeedbackStorage (one
arena) sizes to the runtime net. Firmware .text unchanged (122692).
- MLHandle: per-instance scratch vectors; dropped the dead 2MB
batch_out_scratch.
- TS: types.ts decls (+_nisps_ml_reshape), wasm-iml re-describes the
created instance, worker carries a shape-contract note for P2.3.
Verified: ctest 4/4 incl. new warm-start grow/shrink test; reshape ABI
smoke (dims honoured, overlap survives, invalid rejected, outputs
bounded); parity PASS unchanged (2.4e-7); lint clean; manifold 9 unit +
20 e2e green; firmware .text 122692 (+0.30% vs pre-P2 baseline).
2026-07-14 03:38:06 +02:00
|
|
|
|
const std::size_t n_out = h->n_out();
|
|
|
|
|
|
for (std::size_t i = 0; i < n_out; ++i) h->output_scratch[i] = outs[i];
|
feat(playground/wasm): WASM bridge between C++ core and SolidJS playground (meml-tgm)
Stream 7 wires nisps/ml + nisps/engines into the playground via Emscripten.
Highlights:
- nisps/wasm/bindings.cpp: flat C API per architecture.md §6.2. Fixed-arch
MLP<2, 10, 14, 18, 126>; engine string→type dispatch table with NoOp
fallback.
- scripts/build-wasm.sh: emcc invocation, MODULARIZE=1, exports listed
explicitly; produces playground/public/nisps.{js,wasm}.
- playground/src/ml/wasm-iml.ts: main-thread MLP host (sync inference,
sync training, RL ops, weights I/O, layer stats, localStorage).
- playground/src/ml/wasm-worker.ts: disposable Web Worker for off-thread
async training, owns its own WASM instance.
- playground/src/ml/dataset.ts: Float32Array-backed FIFO with sample-weight
modes (uniform/global/local/combined). Port of legacy dataset.js.
- playground/src/audio/engine-host.ts: AudioContext + AudioWorkletNode
lifecycle, with start/stop/setEngine/setParams.
- playground/src/audio/worklet/nisps-processor.ts: WASM-loading
AudioWorkletProcessor that runs engine.process_block per 128-sample
block. Loads its own WASM instance from main-thread-supplied bytes
(no fetch in worklet).
- playground/src/stores/ml-store.ts: wired stub methods to WasmIML
singleton; lazy initialize().
- playground/src/debug/probe.ts: window.__nisps now calls real WasmIML
via the store; lazy-init on first use.
Verified:
- bash scripts/build-wasm.sh succeeds (94 KB nisps.wasm)
- bun run typecheck OK
- bun run build OK (production bundle)
- vite dev server serves /nisps.{js,wasm} with COOP/COEP
Known limitation: WASM is fixed at one MLP shape. Multi-arch deferred —
documented in nisps/wasm/README.md.
2026-04-29 15:36:29 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
|
|
|
|
const float* nisps_ml_outputs(void* ml) {
|
|
|
|
|
|
if (!ml) return nullptr;
|
|
|
|
|
|
auto* h = static_cast<MLHandle*>(ml);
|
|
|
|
|
|
return h->output_scratch.data();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
|
|
|
|
void nisps_ml_infer_batch(void* ml, const float* points, int n_points, float* out) {
|
|
|
|
|
|
if (!ml || !points || !out || n_points <= 0) return;
|
|
|
|
|
|
auto* h = static_cast<MLHandle*>(ml);
|
feat(wasm)!: P2.2 — nisps_ml_create honours dims; runtime-shaped browser MLP + reshape
Operator-approved ABI change (P2 stop-point). The WASM MLP is now
MLPCore<DynamicStorage>:
- nisps_ml_create(input, output, hidden[3], n, seed) honours its args;
non-positive/null fall back to the historical 32→[10,14,18]→126, so
every pre-P2 caller (manifold, worker, parity harness) stays
bit-identical. Invalid/oversized dims (>4096) → null.
- NEW nisps_ml_reshape(ml, in, out, hidden, n, spread): fresh net at the
new dims, warm-started via nisps/ml/warm_start.hpp (overlapping region
copied; rest keeps spread init); feedback controller re-created (state
resets — reset-on-reshape modal is the front-end contract). Failure
leaves the old net untouched.
- nisps_ml_describe(ml, out): takes the handle; null reports defaults.
- FeedbackController got the same storage split: algorithms in
FeedbackControllerCore<FbStorage>; FixedFeedbackStorage keeps firmware/
tests source-identical via the old alias; DynamicFeedbackStorage (one
arena) sizes to the runtime net. Firmware .text unchanged (122692).
- MLHandle: per-instance scratch vectors; dropped the dead 2MB
batch_out_scratch.
- TS: types.ts decls (+_nisps_ml_reshape), wasm-iml re-describes the
created instance, worker carries a shape-contract note for P2.3.
Verified: ctest 4/4 incl. new warm-start grow/shrink test; reshape ABI
smoke (dims honoured, overlap survives, invalid rejected, outputs
bounded); parity PASS unchanged (2.4e-7); lint clean; manifold 9 unit +
20 e2e green; firmware .text 122692 (+0.30% vs pre-P2 baseline).
2026-07-14 03:38:06 +02:00
|
|
|
|
const std::size_t n_in = h->n_in();
|
|
|
|
|
|
const std::size_t n_out = h->n_out();
|
feat(playground/wasm): WASM bridge between C++ core and SolidJS playground (meml-tgm)
Stream 7 wires nisps/ml + nisps/engines into the playground via Emscripten.
Highlights:
- nisps/wasm/bindings.cpp: flat C API per architecture.md §6.2. Fixed-arch
MLP<2, 10, 14, 18, 126>; engine string→type dispatch table with NoOp
fallback.
- scripts/build-wasm.sh: emcc invocation, MODULARIZE=1, exports listed
explicitly; produces playground/public/nisps.{js,wasm}.
- playground/src/ml/wasm-iml.ts: main-thread MLP host (sync inference,
sync training, RL ops, weights I/O, layer stats, localStorage).
- playground/src/ml/wasm-worker.ts: disposable Web Worker for off-thread
async training, owns its own WASM instance.
- playground/src/ml/dataset.ts: Float32Array-backed FIFO with sample-weight
modes (uniform/global/local/combined). Port of legacy dataset.js.
- playground/src/audio/engine-host.ts: AudioContext + AudioWorkletNode
lifecycle, with start/stop/setEngine/setParams.
- playground/src/audio/worklet/nisps-processor.ts: WASM-loading
AudioWorkletProcessor that runs engine.process_block per 128-sample
block. Loads its own WASM instance from main-thread-supplied bytes
(no fetch in worklet).
- playground/src/stores/ml-store.ts: wired stub methods to WasmIML
singleton; lazy initialize().
- playground/src/debug/probe.ts: window.__nisps now calls real WasmIML
via the store; lazy-init on first use.
Verified:
- bash scripts/build-wasm.sh succeeds (94 KB nisps.wasm)
- bun run typecheck OK
- bun run build OK (production bundle)
- vite dev server serves /nisps.{js,wasm} with COOP/COEP
Known limitation: WASM is fixed at one MLP shape. Multi-arch deferred —
documented in nisps/wasm/README.md.
2026-04-29 15:36:29 +02:00
|
|
|
|
const std::size_t n = static_cast<std::size_t>(n_points);
|
|
|
|
|
|
if (n > MLHandle::kMaxBatch) {
|
feat(wasm)!: P2.2 — nisps_ml_create honours dims; runtime-shaped browser MLP + reshape
Operator-approved ABI change (P2 stop-point). The WASM MLP is now
MLPCore<DynamicStorage>:
- nisps_ml_create(input, output, hidden[3], n, seed) honours its args;
non-positive/null fall back to the historical 32→[10,14,18]→126, so
every pre-P2 caller (manifold, worker, parity harness) stays
bit-identical. Invalid/oversized dims (>4096) → null.
- NEW nisps_ml_reshape(ml, in, out, hidden, n, spread): fresh net at the
new dims, warm-started via nisps/ml/warm_start.hpp (overlapping region
copied; rest keeps spread init); feedback controller re-created (state
resets — reset-on-reshape modal is the front-end contract). Failure
leaves the old net untouched.
- nisps_ml_describe(ml, out): takes the handle; null reports defaults.
- FeedbackController got the same storage split: algorithms in
FeedbackControllerCore<FbStorage>; FixedFeedbackStorage keeps firmware/
tests source-identical via the old alias; DynamicFeedbackStorage (one
arena) sizes to the runtime net. Firmware .text unchanged (122692).
- MLHandle: per-instance scratch vectors; dropped the dead 2MB
batch_out_scratch.
- TS: types.ts decls (+_nisps_ml_reshape), wasm-iml re-describes the
created instance, worker carries a shape-contract note for P2.3.
Verified: ctest 4/4 incl. new warm-start grow/shrink test; reshape ABI
smoke (dims honoured, overlap survives, invalid rejected, outputs
bounded); parity PASS unchanged (2.4e-7); lint clean; manifold 9 unit +
20 e2e green; firmware .text 122692 (+0.30% vs pre-P2 baseline).
2026-07-14 03:38:06 +02:00
|
|
|
|
// Caller exceeded the batch cap. Process what we can.
|
feat(playground/wasm): WASM bridge between C++ core and SolidJS playground (meml-tgm)
Stream 7 wires nisps/ml + nisps/engines into the playground via Emscripten.
Highlights:
- nisps/wasm/bindings.cpp: flat C API per architecture.md §6.2. Fixed-arch
MLP<2, 10, 14, 18, 126>; engine string→type dispatch table with NoOp
fallback.
- scripts/build-wasm.sh: emcc invocation, MODULARIZE=1, exports listed
explicitly; produces playground/public/nisps.{js,wasm}.
- playground/src/ml/wasm-iml.ts: main-thread MLP host (sync inference,
sync training, RL ops, weights I/O, layer stats, localStorage).
- playground/src/ml/wasm-worker.ts: disposable Web Worker for off-thread
async training, owns its own WASM instance.
- playground/src/ml/dataset.ts: Float32Array-backed FIFO with sample-weight
modes (uniform/global/local/combined). Port of legacy dataset.js.
- playground/src/audio/engine-host.ts: AudioContext + AudioWorkletNode
lifecycle, with start/stop/setEngine/setParams.
- playground/src/audio/worklet/nisps-processor.ts: WASM-loading
AudioWorkletProcessor that runs engine.process_block per 128-sample
block. Loads its own WASM instance from main-thread-supplied bytes
(no fetch in worklet).
- playground/src/stores/ml-store.ts: wired stub methods to WasmIML
singleton; lazy initialize().
- playground/src/debug/probe.ts: window.__nisps now calls real WasmIML
via the store; lazy-init on first use.
Verified:
- bash scripts/build-wasm.sh succeeds (94 KB nisps.wasm)
- bun run typecheck OK
- bun run build OK (production bundle)
- vite dev server serves /nisps.{js,wasm} with COOP/COEP
Known limitation: WASM is fixed at one MLP shape. Multi-arch deferred —
documented in nisps/wasm/README.md.
2026-04-29 15:36:29 +02:00
|
|
|
|
const std::size_t safe_n = MLHandle::kMaxBatch;
|
|
|
|
|
|
h->mlp.infer_batch(
|
feat(wasm)!: P2.2 — nisps_ml_create honours dims; runtime-shaped browser MLP + reshape
Operator-approved ABI change (P2 stop-point). The WASM MLP is now
MLPCore<DynamicStorage>:
- nisps_ml_create(input, output, hidden[3], n, seed) honours its args;
non-positive/null fall back to the historical 32→[10,14,18]→126, so
every pre-P2 caller (manifold, worker, parity harness) stays
bit-identical. Invalid/oversized dims (>4096) → null.
- NEW nisps_ml_reshape(ml, in, out, hidden, n, spread): fresh net at the
new dims, warm-started via nisps/ml/warm_start.hpp (overlapping region
copied; rest keeps spread init); feedback controller re-created (state
resets — reset-on-reshape modal is the front-end contract). Failure
leaves the old net untouched.
- nisps_ml_describe(ml, out): takes the handle; null reports defaults.
- FeedbackController got the same storage split: algorithms in
FeedbackControllerCore<FbStorage>; FixedFeedbackStorage keeps firmware/
tests source-identical via the old alias; DynamicFeedbackStorage (one
arena) sizes to the runtime net. Firmware .text unchanged (122692).
- MLHandle: per-instance scratch vectors; dropped the dead 2MB
batch_out_scratch.
- TS: types.ts decls (+_nisps_ml_reshape), wasm-iml re-describes the
created instance, worker carries a shape-contract note for P2.3.
Verified: ctest 4/4 incl. new warm-start grow/shrink test; reshape ABI
smoke (dims honoured, overlap survives, invalid rejected, outputs
bounded); parity PASS unchanged (2.4e-7); lint clean; manifold 9 unit +
20 e2e green; firmware .text 122692 (+0.30% vs pre-P2 baseline).
2026-07-14 03:38:06 +02:00
|
|
|
|
std::span<const float>(points, safe_n * n_in),
|
|
|
|
|
|
std::span<float>(out, safe_n * n_out));
|
feat(playground/wasm): WASM bridge between C++ core and SolidJS playground (meml-tgm)
Stream 7 wires nisps/ml + nisps/engines into the playground via Emscripten.
Highlights:
- nisps/wasm/bindings.cpp: flat C API per architecture.md §6.2. Fixed-arch
MLP<2, 10, 14, 18, 126>; engine string→type dispatch table with NoOp
fallback.
- scripts/build-wasm.sh: emcc invocation, MODULARIZE=1, exports listed
explicitly; produces playground/public/nisps.{js,wasm}.
- playground/src/ml/wasm-iml.ts: main-thread MLP host (sync inference,
sync training, RL ops, weights I/O, layer stats, localStorage).
- playground/src/ml/wasm-worker.ts: disposable Web Worker for off-thread
async training, owns its own WASM instance.
- playground/src/ml/dataset.ts: Float32Array-backed FIFO with sample-weight
modes (uniform/global/local/combined). Port of legacy dataset.js.
- playground/src/audio/engine-host.ts: AudioContext + AudioWorkletNode
lifecycle, with start/stop/setEngine/setParams.
- playground/src/audio/worklet/nisps-processor.ts: WASM-loading
AudioWorkletProcessor that runs engine.process_block per 128-sample
block. Loads its own WASM instance from main-thread-supplied bytes
(no fetch in worklet).
- playground/src/stores/ml-store.ts: wired stub methods to WasmIML
singleton; lazy initialize().
- playground/src/debug/probe.ts: window.__nisps now calls real WasmIML
via the store; lazy-init on first use.
Verified:
- bash scripts/build-wasm.sh succeeds (94 KB nisps.wasm)
- bun run typecheck OK
- bun run build OK (production bundle)
- vite dev server serves /nisps.{js,wasm} with COOP/COEP
Known limitation: WASM is fixed at one MLP shape. Multi-arch deferred —
documented in nisps/wasm/README.md.
2026-04-29 15:36:29 +02:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
h->mlp.infer_batch(
|
feat(wasm)!: P2.2 — nisps_ml_create honours dims; runtime-shaped browser MLP + reshape
Operator-approved ABI change (P2 stop-point). The WASM MLP is now
MLPCore<DynamicStorage>:
- nisps_ml_create(input, output, hidden[3], n, seed) honours its args;
non-positive/null fall back to the historical 32→[10,14,18]→126, so
every pre-P2 caller (manifold, worker, parity harness) stays
bit-identical. Invalid/oversized dims (>4096) → null.
- NEW nisps_ml_reshape(ml, in, out, hidden, n, spread): fresh net at the
new dims, warm-started via nisps/ml/warm_start.hpp (overlapping region
copied; rest keeps spread init); feedback controller re-created (state
resets — reset-on-reshape modal is the front-end contract). Failure
leaves the old net untouched.
- nisps_ml_describe(ml, out): takes the handle; null reports defaults.
- FeedbackController got the same storage split: algorithms in
FeedbackControllerCore<FbStorage>; FixedFeedbackStorage keeps firmware/
tests source-identical via the old alias; DynamicFeedbackStorage (one
arena) sizes to the runtime net. Firmware .text unchanged (122692).
- MLHandle: per-instance scratch vectors; dropped the dead 2MB
batch_out_scratch.
- TS: types.ts decls (+_nisps_ml_reshape), wasm-iml re-describes the
created instance, worker carries a shape-contract note for P2.3.
Verified: ctest 4/4 incl. new warm-start grow/shrink test; reshape ABI
smoke (dims honoured, overlap survives, invalid rejected, outputs
bounded); parity PASS unchanged (2.4e-7); lint clean; manifold 9 unit +
20 e2e green; firmware .text 122692 (+0.30% vs pre-P2 baseline).
2026-07-14 03:38:06 +02:00
|
|
|
|
std::span<const float>(points, n * n_in),
|
|
|
|
|
|
std::span<float>(out, n * n_out));
|
feat(playground/wasm): WASM bridge between C++ core and SolidJS playground (meml-tgm)
Stream 7 wires nisps/ml + nisps/engines into the playground via Emscripten.
Highlights:
- nisps/wasm/bindings.cpp: flat C API per architecture.md §6.2. Fixed-arch
MLP<2, 10, 14, 18, 126>; engine string→type dispatch table with NoOp
fallback.
- scripts/build-wasm.sh: emcc invocation, MODULARIZE=1, exports listed
explicitly; produces playground/public/nisps.{js,wasm}.
- playground/src/ml/wasm-iml.ts: main-thread MLP host (sync inference,
sync training, RL ops, weights I/O, layer stats, localStorage).
- playground/src/ml/wasm-worker.ts: disposable Web Worker for off-thread
async training, owns its own WASM instance.
- playground/src/ml/dataset.ts: Float32Array-backed FIFO with sample-weight
modes (uniform/global/local/combined). Port of legacy dataset.js.
- playground/src/audio/engine-host.ts: AudioContext + AudioWorkletNode
lifecycle, with start/stop/setEngine/setParams.
- playground/src/audio/worklet/nisps-processor.ts: WASM-loading
AudioWorkletProcessor that runs engine.process_block per 128-sample
block. Loads its own WASM instance from main-thread-supplied bytes
(no fetch in worklet).
- playground/src/stores/ml-store.ts: wired stub methods to WasmIML
singleton; lazy initialize().
- playground/src/debug/probe.ts: window.__nisps now calls real WasmIML
via the store; lazy-init on first use.
Verified:
- bash scripts/build-wasm.sh succeeds (94 KB nisps.wasm)
- bun run typecheck OK
- bun run build OK (production bundle)
- vite dev server serves /nisps.{js,wasm} with COOP/COEP
Known limitation: WASM is fixed at one MLP shape. Multi-arch deferred —
documented in nisps/wasm/README.md.
2026-04-29 15:36:29 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// ML training
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
|
|
|
|
void nisps_ml_add_example(void* ml, const float* features, const float* labels) {
|
|
|
|
|
|
if (!ml || !features || !labels) return;
|
|
|
|
|
|
auto* h = static_cast<MLHandle*>(ml);
|
|
|
|
|
|
h->mlp.add_example(
|
feat(wasm)!: P2.2 — nisps_ml_create honours dims; runtime-shaped browser MLP + reshape
Operator-approved ABI change (P2 stop-point). The WASM MLP is now
MLPCore<DynamicStorage>:
- nisps_ml_create(input, output, hidden[3], n, seed) honours its args;
non-positive/null fall back to the historical 32→[10,14,18]→126, so
every pre-P2 caller (manifold, worker, parity harness) stays
bit-identical. Invalid/oversized dims (>4096) → null.
- NEW nisps_ml_reshape(ml, in, out, hidden, n, spread): fresh net at the
new dims, warm-started via nisps/ml/warm_start.hpp (overlapping region
copied; rest keeps spread init); feedback controller re-created (state
resets — reset-on-reshape modal is the front-end contract). Failure
leaves the old net untouched.
- nisps_ml_describe(ml, out): takes the handle; null reports defaults.
- FeedbackController got the same storage split: algorithms in
FeedbackControllerCore<FbStorage>; FixedFeedbackStorage keeps firmware/
tests source-identical via the old alias; DynamicFeedbackStorage (one
arena) sizes to the runtime net. Firmware .text unchanged (122692).
- MLHandle: per-instance scratch vectors; dropped the dead 2MB
batch_out_scratch.
- TS: types.ts decls (+_nisps_ml_reshape), wasm-iml re-describes the
created instance, worker carries a shape-contract note for P2.3.
Verified: ctest 4/4 incl. new warm-start grow/shrink test; reshape ABI
smoke (dims honoured, overlap survives, invalid rejected, outputs
bounded); parity PASS unchanged (2.4e-7); lint clean; manifold 9 unit +
20 e2e green; firmware .text 122692 (+0.30% vs pre-P2 baseline).
2026-07-14 03:38:06 +02:00
|
|
|
|
std::span<const float>(features, h->n_in()),
|
|
|
|
|
|
std::span<const float>(labels, h->n_out()));
|
feat(playground/wasm): WASM bridge between C++ core and SolidJS playground (meml-tgm)
Stream 7 wires nisps/ml + nisps/engines into the playground via Emscripten.
Highlights:
- nisps/wasm/bindings.cpp: flat C API per architecture.md §6.2. Fixed-arch
MLP<2, 10, 14, 18, 126>; engine string→type dispatch table with NoOp
fallback.
- scripts/build-wasm.sh: emcc invocation, MODULARIZE=1, exports listed
explicitly; produces playground/public/nisps.{js,wasm}.
- playground/src/ml/wasm-iml.ts: main-thread MLP host (sync inference,
sync training, RL ops, weights I/O, layer stats, localStorage).
- playground/src/ml/wasm-worker.ts: disposable Web Worker for off-thread
async training, owns its own WASM instance.
- playground/src/ml/dataset.ts: Float32Array-backed FIFO with sample-weight
modes (uniform/global/local/combined). Port of legacy dataset.js.
- playground/src/audio/engine-host.ts: AudioContext + AudioWorkletNode
lifecycle, with start/stop/setEngine/setParams.
- playground/src/audio/worklet/nisps-processor.ts: WASM-loading
AudioWorkletProcessor that runs engine.process_block per 128-sample
block. Loads its own WASM instance from main-thread-supplied bytes
(no fetch in worklet).
- playground/src/stores/ml-store.ts: wired stub methods to WasmIML
singleton; lazy initialize().
- playground/src/debug/probe.ts: window.__nisps now calls real WasmIML
via the store; lazy-init on first use.
Verified:
- bash scripts/build-wasm.sh succeeds (94 KB nisps.wasm)
- bun run typecheck OK
- bun run build OK (production bundle)
- vite dev server serves /nisps.{js,wasm} with COOP/COEP
Known limitation: WASM is fixed at one MLP shape. Multi-arch deferred —
documented in nisps/wasm/README.md.
2026-04-29 15:36:29 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
|
|
|
|
float nisps_ml_train(void* ml, float lr, int max_iter, float min_err,
|
|
|
|
|
|
const float* sample_weights) {
|
|
|
|
|
|
if (!ml) return 0.f;
|
|
|
|
|
|
auto* h = static_cast<MLHandle*>(ml);
|
|
|
|
|
|
if (max_iter <= 0) max_iter = 1;
|
|
|
|
|
|
std::span<const float> weights;
|
|
|
|
|
|
if (sample_weights) {
|
|
|
|
|
|
weights = std::span<const float>(sample_weights, h->mlp.example_count());
|
|
|
|
|
|
}
|
|
|
|
|
|
return h->mlp.train(lr, static_cast<std::size_t>(max_iter), min_err, weights);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
refactor(ml): one runtime-configurable training default (S26)
The operator's call: "there should be one default learning rate and one
default max iterations and they should both be configurable at runtime."
There were SIX copies, not the four the audit described, and they did not
agree:
nisps/ml/mlp.hpp no-arg train() hardcoding 1.f / 1000u / 0.001f —
and firmware's ONLY training path calls exactly
this, so firmware had no runtime knob at all
wasm-iml.ts train() and trainAsync() TS default params (x2)
engine-api.ts learningRate ?? 1.0, with no maxIterations knob
vcv/src/iml.hpp 200 / 0.1 / 0.00001 — silently divergent
external_synth_midi.hpp its own kDefaultLearningRate/kDefaultMaxIterations
schemas/modes/*.json x9, identical, read by nobody at runtime
Now: schemas/ml_defaults.json is the single declaration (validated against a
sibling meta-schema, matching the midi_device.schema.json convention), codegen
emits it to C++ and TS in the same run, and MLPCore carries a TrainConfig whose
default member initialisers read the generated constant.
set_train_config()/nisps_ml_set_train_config() make it runtime-overridable on
every target; the explicit-argument train() overload is untouched. min_error
joins the tuple — it was duplicated identically and belongs with the other two.
The per-mode ml block loses default_learning_rate/default_max_iterations.
default_spread stays (genuinely wired on both targets) and input_channels stays
(codegen-time validated, real information for sound_analysis_midi).
VCV BEHAVIOUR CHANGE, deliberate: MEMLNaut.cpp constructs IML positionally and
relies on those defaults, so the module moves to 1000/1.0/0.001 — 5x the max
iterations, 10x the learning rate, and a 100x looser early-stop threshold. The
old values were never justified anywhere; they arrived with fbc68eb alongside
an unrelated module rewrite and no tuning rationale. Firmware and WASM have
shipped 1.0/1000 all along. It is now runtime-settable if this turns out worse.
The generated header lands in nisps/ml/generated/, not nisps/modes/generated/
where the rest of codegen output lives: training hyperparameters are an ML
fact, and nisps/ml sits below nisps/modes, so emitting them there would make
mlp.hpp include upward. The agent that built this flagged the directory-crossing
rather than hiding it; this is the fix. CI's generated-freshness gate learns the
new directory.
Gates: run-all-tests.sh ALL GREEN — 4/4 ctest, parity PASS (max delta 2.38e-7),
lint clean, manifold typecheck + 17 unit + 33 e2e (which exercise train() and
trainAsync() through a real browser).
2026-07-21 17:20:10 +02:00
|
|
|
|
// Persist a new training-hyperparameter default on the handle itself (S26):
|
|
|
|
|
|
// mirrors nisps::ml::MLPCore::set_train_config so the browser MLP is
|
|
|
|
|
|
// genuinely runtime-configurable, not just JS remembering a number to pass on
|
|
|
|
|
|
// each nisps_ml_train() call. Does not train; only reconfigures the no-arg
|
|
|
|
|
|
// train() fallback (unused WASM-side today, but keeps the handle's own state
|
|
|
|
|
|
// consistent with firmware/VCV, which carry the same knob).
|
|
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
|
|
|
|
void nisps_ml_set_train_config(void* ml, float lr, int max_iter, float min_err) {
|
|
|
|
|
|
if (!ml) return;
|
|
|
|
|
|
auto* h = static_cast<MLHandle*>(ml);
|
|
|
|
|
|
if (max_iter <= 0) max_iter = 1;
|
|
|
|
|
|
h->mlp.set_train_config(lr, static_cast<std::size_t>(max_iter), min_err);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat(playground/wasm): WASM bridge between C++ core and SolidJS playground (meml-tgm)
Stream 7 wires nisps/ml + nisps/engines into the playground via Emscripten.
Highlights:
- nisps/wasm/bindings.cpp: flat C API per architecture.md §6.2. Fixed-arch
MLP<2, 10, 14, 18, 126>; engine string→type dispatch table with NoOp
fallback.
- scripts/build-wasm.sh: emcc invocation, MODULARIZE=1, exports listed
explicitly; produces playground/public/nisps.{js,wasm}.
- playground/src/ml/wasm-iml.ts: main-thread MLP host (sync inference,
sync training, RL ops, weights I/O, layer stats, localStorage).
- playground/src/ml/wasm-worker.ts: disposable Web Worker for off-thread
async training, owns its own WASM instance.
- playground/src/ml/dataset.ts: Float32Array-backed FIFO with sample-weight
modes (uniform/global/local/combined). Port of legacy dataset.js.
- playground/src/audio/engine-host.ts: AudioContext + AudioWorkletNode
lifecycle, with start/stop/setEngine/setParams.
- playground/src/audio/worklet/nisps-processor.ts: WASM-loading
AudioWorkletProcessor that runs engine.process_block per 128-sample
block. Loads its own WASM instance from main-thread-supplied bytes
(no fetch in worklet).
- playground/src/stores/ml-store.ts: wired stub methods to WasmIML
singleton; lazy initialize().
- playground/src/debug/probe.ts: window.__nisps now calls real WasmIML
via the store; lazy-init on first use.
Verified:
- bash scripts/build-wasm.sh succeeds (94 KB nisps.wasm)
- bun run typecheck OK
- bun run build OK (production bundle)
- vite dev server serves /nisps.{js,wasm} with COOP/COEP
Known limitation: WASM is fixed at one MLP shape. Multi-arch deferred —
documented in nisps/wasm/README.md.
2026-04-29 15:36:29 +02:00
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
|
|
|
|
float nisps_ml_eval_loss(void* ml) {
|
|
|
|
|
|
if (!ml) return 0.f;
|
|
|
|
|
|
auto* h = static_cast<MLHandle*>(ml);
|
|
|
|
|
|
return h->mlp.eval_loss();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat: curve truth, DriverConfig, real telemetry, engine benchmark
Four items from one workflow, committed together because their build and CI
wiring genuinely interleaves — nisps/CMakeLists.txt, run-all-tests.sh and
ci.yml each carry hunks from two of them, and the stage renumbering (1/5 ->
1/6) touches every line. Splitting would produce commits that do not build,
which is worse than a commit that does four things and says so.
S26 part 2 — the curve declaration now matches reality. params[].curve stays
the mode-wide DEFAULT; a voice_spaces entry may now be {name, curve_overrides}
declaring only the slots where THAT voice space deviates. The 6 modes with one
voice space are byte-identical. The values were derived MECHANICALLY by a new
codegen/curve-audit.ts that models the four idioms a p[N]*p[N] regex misses
(alias form, memlcelium's implicit-counter sq() lambda, loop-generated indices,
smooth_params_), inlines helpers, and RAISES rather than guessing when it
cannot reduce an expression. A drift gate cross-checks 1179 (voice space x
param) slots against engine source on every run and was proved to fail loudly
on three drift classes. Application stays in the engine: nisps/engines,
nisps/pipeline and nisps/core are untouched, generated output is pure insertion
(755 insertions, 0 deletions), and the rebuilt nisps.wasm was byte-identical.
S4 / 7.2 — firmware reads the active mode's driver config at mode start, and
mic/line is real. My brief assumed the engine owns this; the code disagreed and
the code was right. sound_analysis_midi's EngineT is NoOpEngine — the mic lives
on a separately-composed AnalysisEngine member — so engine-level wiring would
have compiled, passed every gate, and left the one mic mode on line input.
Hence a mode-level seam defaulting to engine().driver_config(). Separately,
DriverConfig's defaults (line_level 0, output_volume 1.0) had drifted from
memllib's actual 3/0.8 because nothing had ever read them; wiring them as-is
would have made every silent mode louder and its line input maximally
insensitive — a behaviour change disguised as plumbing. Now pinned by a test.
Also: GetSysClockSpeed() panic()s on unsupported sample rates and runs on the
first line of setup(), so sample_rate needed a fallback ahead of clock setup.
CI's firmware env list gains soundanalysismidi — it is the only mic variant and
nothing else compiles that path.
Plan 5e — telemetry is real. A loss_history C-API entry across the full 5-layer
chain lets the browser read the per-iteration loss the core already records.
The audit named one fabrication site; there were two — wasm-iml.ts's
synchronous train() published lossHistory: [loss] as well. A third, ctx.loss,
was not merely dead but actively synthetic (fallbacks of prev * 0.82 and a
literal 0.5, rendered by nothing) and is deleted. The firmware buffer stays
untouched, per the L25 call. EngineApi.lossHistory() reads spine state rather
than the MLP handle, because trainAsync() fits on the worker's mirror net and
the handle would give a subtly-wrong second answer.
Plan 5f — engine throughput is measurable. One source compiled twice (CMake
natively, emcc for WASM) so the targets compare directly and no WASM export is
added. Sequencers are driven into a working state, and every row prints its own
working-state evidence so a number produced by an idle engine is visible rather
than plausible. Reports, never asserts: a wall-clock threshold on shared
hardware is meaningless or flaky, same call as the firmware size job.
ALIGNMENT: the telemetry defect is deleted (built, not deferred); the
performance defect is rewritten to what is actually left — these are HOST
numbers, and nothing measures the RP2350 at 150 MHz, which is the target the
mission's constraint is about. Q4 (memllib ownership) and Q5 (legacy feedback
modes) are closed.
Corrections to my own earlier claims, both found by agents contradicting the
brief: manifold/ONBOARDING.md was NOT "now accurate" — its primitives list
still named five deleted primitives and cited a seededGradient() that does not
exist. And the parity harness misses the sequencer engines because it runs 128
frames while their sequencers evaluate every 400-500 samples, NOT because
all-params-0.5 fails to trigger them (it does trigger: 0.5 maps to ratio 2,
firing three times per bar). The fix is a longer window, not different params.
Gates: run-all-tests.sh ALL GREEN — 4/4 ctest, parity PASS, lint clean, curve
drift 1179 slots ok, 39 e2e (was 33). Firmware: 5 envs built including the mic
variant.
2026-07-21 22:02:23 +02:00
|
|
|
|
// Per-iteration training loss recorded by the LAST nisps_ml_train() call on
|
|
|
|
|
|
// this handle (nisps::ml::MLPCore::loss_history — the same buffer the firmware
|
|
|
|
|
|
// MLP fills). Returns the TOTAL number of recorded entries and writes
|
|
|
|
|
|
// min(count, max) of them into `out`. Pass out=null / max<=0 to query the
|
|
|
|
|
|
// count without copying, which is how JS sizes its heap buffer instead of
|
|
|
|
|
|
// mirroring the C++ history cap.
|
|
|
|
|
|
//
|
|
|
|
|
|
// The history is reset at the START of every train() call, so it always
|
|
|
|
|
|
// describes exactly one training run. train_targets() (the geometric-dislike
|
|
|
|
|
|
// single-step path) does NOT record — a dislike leaves the previous run's
|
|
|
|
|
|
// curve intact rather than replacing it with a 1-point curve.
|
|
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
|
|
|
|
int nisps_ml_loss_history(void* ml, float* out, int max) {
|
|
|
|
|
|
if (!ml) return 0;
|
|
|
|
|
|
auto* h = static_cast<MLHandle*>(ml);
|
|
|
|
|
|
const auto hist = h->mlp.loss_history();
|
|
|
|
|
|
const int count = static_cast<int>(hist.size());
|
|
|
|
|
|
if (!out || max <= 0) return count;
|
|
|
|
|
|
const int n = max < count ? max : count;
|
|
|
|
|
|
for (int i = 0; i < n; ++i) out[i] = hist[static_cast<std::size_t>(i)];
|
|
|
|
|
|
return count;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat(playground/wasm): WASM bridge between C++ core and SolidJS playground (meml-tgm)
Stream 7 wires nisps/ml + nisps/engines into the playground via Emscripten.
Highlights:
- nisps/wasm/bindings.cpp: flat C API per architecture.md §6.2. Fixed-arch
MLP<2, 10, 14, 18, 126>; engine string→type dispatch table with NoOp
fallback.
- scripts/build-wasm.sh: emcc invocation, MODULARIZE=1, exports listed
explicitly; produces playground/public/nisps.{js,wasm}.
- playground/src/ml/wasm-iml.ts: main-thread MLP host (sync inference,
sync training, RL ops, weights I/O, layer stats, localStorage).
- playground/src/ml/wasm-worker.ts: disposable Web Worker for off-thread
async training, owns its own WASM instance.
- playground/src/ml/dataset.ts: Float32Array-backed FIFO with sample-weight
modes (uniform/global/local/combined). Port of legacy dataset.js.
- playground/src/audio/engine-host.ts: AudioContext + AudioWorkletNode
lifecycle, with start/stop/setEngine/setParams.
- playground/src/audio/worklet/nisps-processor.ts: WASM-loading
AudioWorkletProcessor that runs engine.process_block per 128-sample
block. Loads its own WASM instance from main-thread-supplied bytes
(no fetch in worklet).
- playground/src/stores/ml-store.ts: wired stub methods to WasmIML
singleton; lazy initialize().
- playground/src/debug/probe.ts: window.__nisps now calls real WasmIML
via the store; lazy-init on first use.
Verified:
- bash scripts/build-wasm.sh succeeds (94 KB nisps.wasm)
- bun run typecheck OK
- bun run build OK (production bundle)
- vite dev server serves /nisps.{js,wasm} with COOP/COEP
Known limitation: WASM is fixed at one MLP shape. Multi-arch deferred —
documented in nisps/wasm/README.md.
2026-04-29 15:36:29 +02:00
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// ML weights
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
|
|
|
|
int nisps_ml_weight_count(void* ml) {
|
feat(wasm)!: P2.2 — nisps_ml_create honours dims; runtime-shaped browser MLP + reshape
Operator-approved ABI change (P2 stop-point). The WASM MLP is now
MLPCore<DynamicStorage>:
- nisps_ml_create(input, output, hidden[3], n, seed) honours its args;
non-positive/null fall back to the historical 32→[10,14,18]→126, so
every pre-P2 caller (manifold, worker, parity harness) stays
bit-identical. Invalid/oversized dims (>4096) → null.
- NEW nisps_ml_reshape(ml, in, out, hidden, n, spread): fresh net at the
new dims, warm-started via nisps/ml/warm_start.hpp (overlapping region
copied; rest keeps spread init); feedback controller re-created (state
resets — reset-on-reshape modal is the front-end contract). Failure
leaves the old net untouched.
- nisps_ml_describe(ml, out): takes the handle; null reports defaults.
- FeedbackController got the same storage split: algorithms in
FeedbackControllerCore<FbStorage>; FixedFeedbackStorage keeps firmware/
tests source-identical via the old alias; DynamicFeedbackStorage (one
arena) sizes to the runtime net. Firmware .text unchanged (122692).
- MLHandle: per-instance scratch vectors; dropped the dead 2MB
batch_out_scratch.
- TS: types.ts decls (+_nisps_ml_reshape), wasm-iml re-describes the
created instance, worker carries a shape-contract note for P2.3.
Verified: ctest 4/4 incl. new warm-start grow/shrink test; reshape ABI
smoke (dims honoured, overlap survives, invalid rejected, outputs
bounded); parity PASS unchanged (2.4e-7); lint clean; manifold 9 unit +
20 e2e green; firmware .text 122692 (+0.30% vs pre-P2 baseline).
2026-07-14 03:38:06 +02:00
|
|
|
|
if (!ml) {
|
|
|
|
|
|
// Handle-less callers get the default shape's count.
|
|
|
|
|
|
const MlDims d = sanitise_dims(0, 0, nullptr, 0);
|
|
|
|
|
|
return static_cast<int>(
|
|
|
|
|
|
d.n_in * d.hidden[0] + d.hidden[0] * d.hidden[1] +
|
|
|
|
|
|
d.hidden[1] * d.hidden[2] + d.hidden[2] * d.n_out +
|
|
|
|
|
|
d.hidden[0] + d.hidden[1] + d.hidden[2] + d.n_out);
|
|
|
|
|
|
}
|
|
|
|
|
|
return static_cast<int>(static_cast<MLHandle*>(ml)->mlp.weight_count());
|
feat(playground/wasm): WASM bridge between C++ core and SolidJS playground (meml-tgm)
Stream 7 wires nisps/ml + nisps/engines into the playground via Emscripten.
Highlights:
- nisps/wasm/bindings.cpp: flat C API per architecture.md §6.2. Fixed-arch
MLP<2, 10, 14, 18, 126>; engine string→type dispatch table with NoOp
fallback.
- scripts/build-wasm.sh: emcc invocation, MODULARIZE=1, exports listed
explicitly; produces playground/public/nisps.{js,wasm}.
- playground/src/ml/wasm-iml.ts: main-thread MLP host (sync inference,
sync training, RL ops, weights I/O, layer stats, localStorage).
- playground/src/ml/wasm-worker.ts: disposable Web Worker for off-thread
async training, owns its own WASM instance.
- playground/src/ml/dataset.ts: Float32Array-backed FIFO with sample-weight
modes (uniform/global/local/combined). Port of legacy dataset.js.
- playground/src/audio/engine-host.ts: AudioContext + AudioWorkletNode
lifecycle, with start/stop/setEngine/setParams.
- playground/src/audio/worklet/nisps-processor.ts: WASM-loading
AudioWorkletProcessor that runs engine.process_block per 128-sample
block. Loads its own WASM instance from main-thread-supplied bytes
(no fetch in worklet).
- playground/src/stores/ml-store.ts: wired stub methods to WasmIML
singleton; lazy initialize().
- playground/src/debug/probe.ts: window.__nisps now calls real WasmIML
via the store; lazy-init on first use.
Verified:
- bash scripts/build-wasm.sh succeeds (94 KB nisps.wasm)
- bun run typecheck OK
- bun run build OK (production bundle)
- vite dev server serves /nisps.{js,wasm} with COOP/COEP
Known limitation: WASM is fixed at one MLP shape. Multi-arch deferred —
documented in nisps/wasm/README.md.
2026-04-29 15:36:29 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
|
|
|
|
void nisps_ml_get_weights(void* ml, float* out) {
|
|
|
|
|
|
if (!ml || !out) return;
|
|
|
|
|
|
auto* h = static_cast<MLHandle*>(ml);
|
|
|
|
|
|
auto w = h->mlp.get_weights();
|
|
|
|
|
|
std::memcpy(out, w.data(), w.size() * sizeof(float));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
|
|
|
|
void nisps_ml_set_weights(void* ml, const float* in) {
|
|
|
|
|
|
if (!ml || !in) return;
|
|
|
|
|
|
auto* h = static_cast<MLHandle*>(ml);
|
feat(wasm)!: P2.2 — nisps_ml_create honours dims; runtime-shaped browser MLP + reshape
Operator-approved ABI change (P2 stop-point). The WASM MLP is now
MLPCore<DynamicStorage>:
- nisps_ml_create(input, output, hidden[3], n, seed) honours its args;
non-positive/null fall back to the historical 32→[10,14,18]→126, so
every pre-P2 caller (manifold, worker, parity harness) stays
bit-identical. Invalid/oversized dims (>4096) → null.
- NEW nisps_ml_reshape(ml, in, out, hidden, n, spread): fresh net at the
new dims, warm-started via nisps/ml/warm_start.hpp (overlapping region
copied; rest keeps spread init); feedback controller re-created (state
resets — reset-on-reshape modal is the front-end contract). Failure
leaves the old net untouched.
- nisps_ml_describe(ml, out): takes the handle; null reports defaults.
- FeedbackController got the same storage split: algorithms in
FeedbackControllerCore<FbStorage>; FixedFeedbackStorage keeps firmware/
tests source-identical via the old alias; DynamicFeedbackStorage (one
arena) sizes to the runtime net. Firmware .text unchanged (122692).
- MLHandle: per-instance scratch vectors; dropped the dead 2MB
batch_out_scratch.
- TS: types.ts decls (+_nisps_ml_reshape), wasm-iml re-describes the
created instance, worker carries a shape-contract note for P2.3.
Verified: ctest 4/4 incl. new warm-start grow/shrink test; reshape ABI
smoke (dims honoured, overlap survives, invalid rejected, outputs
bounded); parity PASS unchanged (2.4e-7); lint clean; manifold 9 unit +
20 e2e green; firmware .text 122692 (+0.30% vs pre-P2 baseline).
2026-07-14 03:38:06 +02:00
|
|
|
|
h->mlp.set_weights(std::span<const float>(in, h->mlp.weight_count()));
|
feat(playground/wasm): WASM bridge between C++ core and SolidJS playground (meml-tgm)
Stream 7 wires nisps/ml + nisps/engines into the playground via Emscripten.
Highlights:
- nisps/wasm/bindings.cpp: flat C API per architecture.md §6.2. Fixed-arch
MLP<2, 10, 14, 18, 126>; engine string→type dispatch table with NoOp
fallback.
- scripts/build-wasm.sh: emcc invocation, MODULARIZE=1, exports listed
explicitly; produces playground/public/nisps.{js,wasm}.
- playground/src/ml/wasm-iml.ts: main-thread MLP host (sync inference,
sync training, RL ops, weights I/O, layer stats, localStorage).
- playground/src/ml/wasm-worker.ts: disposable Web Worker for off-thread
async training, owns its own WASM instance.
- playground/src/ml/dataset.ts: Float32Array-backed FIFO with sample-weight
modes (uniform/global/local/combined). Port of legacy dataset.js.
- playground/src/audio/engine-host.ts: AudioContext + AudioWorkletNode
lifecycle, with start/stop/setEngine/setParams.
- playground/src/audio/worklet/nisps-processor.ts: WASM-loading
AudioWorkletProcessor that runs engine.process_block per 128-sample
block. Loads its own WASM instance from main-thread-supplied bytes
(no fetch in worklet).
- playground/src/stores/ml-store.ts: wired stub methods to WasmIML
singleton; lazy initialize().
- playground/src/debug/probe.ts: window.__nisps now calls real WasmIML
via the store; lazy-init on first use.
Verified:
- bash scripts/build-wasm.sh succeeds (94 KB nisps.wasm)
- bun run typecheck OK
- bun run build OK (production bundle)
- vite dev server serves /nisps.{js,wasm} with COOP/COEP
Known limitation: WASM is fixed at one MLP shape. Multi-arch deferred —
documented in nisps/wasm/README.md.
2026-04-29 15:36:29 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
|
|
|
|
void nisps_ml_draw_weights(void* ml, float spread) {
|
|
|
|
|
|
if (!ml) return;
|
|
|
|
|
|
auto* h = static_cast<MLHandle*>(ml);
|
|
|
|
|
|
h->mlp.draw_weights(spread);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-28 04:14:12 +02:00
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// ML feedback — the "Down Action" state machine (Avoid / RandomiseOutputs /
|
|
|
|
|
|
// RandomiseMlp). The controller decides WHAT transition happened (returns a
|
|
|
|
|
|
// FeedbackAction int); JS performs the side effect (store example, grow noise,
|
|
|
|
|
|
// train). See nisps/ml/feedback.hpp. Mode ints: 0=Avoid 1=RandOut 2=RandMlp.
|
|
|
|
|
|
// Action ints mirror nisps::ml::FeedbackAction.
|
|
|
|
|
|
//
|
|
|
|
|
|
// CALLER CONTRACT (commit ordering — important):
|
|
|
|
|
|
// On a "keep" (up) or drag-commit while exploring RandomiseMlp, the controller
|
|
|
|
|
|
// RESTORES the original net before returning. The output the user is hearing
|
|
|
|
|
|
// comes from the *temporary* (randomised) net, so you MUST capture the current
|
|
|
|
|
|
// output (nisps_ml_outputs / nisps_ml_feedback_static_output) BEFORE calling
|
|
|
|
|
|
// nisps_ml_feedback_up / _drag, then store THAT captured vector as the +1
|
|
|
|
|
|
// example. Reading the output AFTER the call yields the restored (wrong) net.
|
feat(wasm)!: P2.2 — nisps_ml_create honours dims; runtime-shaped browser MLP + reshape
Operator-approved ABI change (P2 stop-point). The WASM MLP is now
MLPCore<DynamicStorage>:
- nisps_ml_create(input, output, hidden[3], n, seed) honours its args;
non-positive/null fall back to the historical 32→[10,14,18]→126, so
every pre-P2 caller (manifold, worker, parity harness) stays
bit-identical. Invalid/oversized dims (>4096) → null.
- NEW nisps_ml_reshape(ml, in, out, hidden, n, spread): fresh net at the
new dims, warm-started via nisps/ml/warm_start.hpp (overlapping region
copied; rest keeps spread init); feedback controller re-created (state
resets — reset-on-reshape modal is the front-end contract). Failure
leaves the old net untouched.
- nisps_ml_describe(ml, out): takes the handle; null reports defaults.
- FeedbackController got the same storage split: algorithms in
FeedbackControllerCore<FbStorage>; FixedFeedbackStorage keeps firmware/
tests source-identical via the old alias; DynamicFeedbackStorage (one
arena) sizes to the runtime net. Firmware .text unchanged (122692).
- MLHandle: per-instance scratch vectors; dropped the dead 2MB
batch_out_scratch.
- TS: types.ts decls (+_nisps_ml_reshape), wasm-iml re-describes the
created instance, worker carries a shape-contract note for P2.3.
Verified: ctest 4/4 incl. new warm-start grow/shrink test; reshape ABI
smoke (dims honoured, overlap survives, invalid rejected, outputs
bounded); parity PASS unchanged (2.4e-7); lint clean; manifold 9 unit +
20 e2e green; firmware .text 122692 (+0.30% vs pre-P2 baseline).
2026-07-14 03:38:06 +02:00
|
|
|
|
// nisps_ml_feedback_down with current_out should pass the full n_out
|
2026-06-28 04:14:12 +02:00
|
|
|
|
// live vector (RandomiseOutputs freezes unfocused dims at those values).
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
|
|
|
|
void nisps_ml_feedback_set_mode(void* ml, int mode) {
|
|
|
|
|
|
if (!ml) return;
|
|
|
|
|
|
auto* h = static_cast<MLHandle*>(ml);
|
|
|
|
|
|
nisps::ml::FeedbackMode m = nisps::ml::FeedbackMode::Avoid;
|
|
|
|
|
|
if (mode == 1) m = nisps::ml::FeedbackMode::RandomiseOutputs;
|
|
|
|
|
|
else if (mode == 2) m = nisps::ml::FeedbackMode::RandomiseMlp;
|
|
|
|
|
|
else if (mode == 3) m = nisps::ml::FeedbackMode::ExploreAndPlace;
|
|
|
|
|
|
h->feedback.set_mode(m, h->mlp);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-25 15:16:37 +02:00
|
|
|
|
// Reset only the index-aligned feedback/exploration state. Same-capacity I/O
|
|
|
|
|
|
// edits remap weights and examples without reconstructing the network, but
|
|
|
|
|
|
// replay/scratch vectors have no stable parameter-identity contract.
|
|
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
|
|
|
|
void nisps_ml_feedback_reset(void* ml) {
|
|
|
|
|
|
if (!ml) return;
|
|
|
|
|
|
auto* h = static_cast<MLHandle*>(ml);
|
|
|
|
|
|
const auto feedback_mode = h->feedback.mode();
|
|
|
|
|
|
const auto avoid_style = h->feedback.avoid_style();
|
|
|
|
|
|
BrowserFeedback fb(h->seed64 ^ kFeedbackSalt, h->n_out(), h->mlp.weight_count(),
|
|
|
|
|
|
kFeedbackUndoDepth, h->n_in(), kFeedbackReplayCap);
|
|
|
|
|
|
if (!fb.valid()) return;
|
|
|
|
|
|
fb.set_avoid_style(avoid_style);
|
|
|
|
|
|
fb.set_mode(feedback_mode, h->mlp);
|
|
|
|
|
|
h->feedback = static_cast<BrowserFeedback&&>(fb);
|
|
|
|
|
|
h->feedback_static_scratch.assign(h->n_out(), 0.f);
|
|
|
|
|
|
h->mlp.reset_optimizer_state();
|
|
|
|
|
|
h->jolt.release();
|
|
|
|
|
|
h->ou.reset();
|
|
|
|
|
|
h->jolt_scratch.assign(h->mlp.weight_count(), 0.f);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-28 04:14:12 +02:00
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
|
|
|
|
int nisps_ml_feedback_get_mode(void* ml) {
|
|
|
|
|
|
if (!ml) return 0;
|
|
|
|
|
|
return static_cast<int>(static_cast<MLHandle*>(ml)->feedback.mode());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
|
|
|
|
int nisps_ml_feedback_exploring(void* ml) {
|
|
|
|
|
|
if (!ml) return 0;
|
|
|
|
|
|
return static_cast<MLHandle*>(ml)->feedback.exploring() ? 1 : 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
|
|
|
|
void nisps_ml_feedback_set_focus(void* ml, const uint8_t* mask, int n) {
|
|
|
|
|
|
if (!ml) return;
|
|
|
|
|
|
auto* h = static_cast<MLHandle*>(ml);
|
|
|
|
|
|
if (!mask || n <= 0) {
|
|
|
|
|
|
h->feedback.clear_focus_mask();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
h->feedback.set_focus_mask(
|
|
|
|
|
|
std::span<const std::uint8_t>(mask, static_cast<std::size_t>(n)));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat(wasm)!: P2.2 — nisps_ml_create honours dims; runtime-shaped browser MLP + reshape
Operator-approved ABI change (P2 stop-point). The WASM MLP is now
MLPCore<DynamicStorage>:
- nisps_ml_create(input, output, hidden[3], n, seed) honours its args;
non-positive/null fall back to the historical 32→[10,14,18]→126, so
every pre-P2 caller (manifold, worker, parity harness) stays
bit-identical. Invalid/oversized dims (>4096) → null.
- NEW nisps_ml_reshape(ml, in, out, hidden, n, spread): fresh net at the
new dims, warm-started via nisps/ml/warm_start.hpp (overlapping region
copied; rest keeps spread init); feedback controller re-created (state
resets — reset-on-reshape modal is the front-end contract). Failure
leaves the old net untouched.
- nisps_ml_describe(ml, out): takes the handle; null reports defaults.
- FeedbackController got the same storage split: algorithms in
FeedbackControllerCore<FbStorage>; FixedFeedbackStorage keeps firmware/
tests source-identical via the old alias; DynamicFeedbackStorage (one
arena) sizes to the runtime net. Firmware .text unchanged (122692).
- MLHandle: per-instance scratch vectors; dropped the dead 2MB
batch_out_scratch.
- TS: types.ts decls (+_nisps_ml_reshape), wasm-iml re-describes the
created instance, worker carries a shape-contract note for P2.3.
Verified: ctest 4/4 incl. new warm-start grow/shrink test; reshape ABI
smoke (dims honoured, overlap survives, invalid rejected, outputs
bounded); parity PASS unchanged (2.4e-7); lint clean; manifold 9 unit +
20 e2e green; firmware .text 122692 (+0.30% vs pre-P2 baseline).
2026-07-14 03:38:06 +02:00
|
|
|
|
// current_out = n_out floats the user is hearing (may be null).
|
2026-06-28 04:14:12 +02:00
|
|
|
|
// pin_mask may be null. Returns the FeedbackAction int.
|
|
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
|
|
|
|
int nisps_ml_feedback_down(void* ml, const float* current_out,
|
|
|
|
|
|
float speed, float spread, const uint8_t* pin_mask) {
|
|
|
|
|
|
if (!ml) return 0;
|
|
|
|
|
|
auto* h = static_cast<MLHandle*>(ml);
|
|
|
|
|
|
std::span<const float> out;
|
feat(wasm)!: P2.2 — nisps_ml_create honours dims; runtime-shaped browser MLP + reshape
Operator-approved ABI change (P2 stop-point). The WASM MLP is now
MLPCore<DynamicStorage>:
- nisps_ml_create(input, output, hidden[3], n, seed) honours its args;
non-positive/null fall back to the historical 32→[10,14,18]→126, so
every pre-P2 caller (manifold, worker, parity harness) stays
bit-identical. Invalid/oversized dims (>4096) → null.
- NEW nisps_ml_reshape(ml, in, out, hidden, n, spread): fresh net at the
new dims, warm-started via nisps/ml/warm_start.hpp (overlapping region
copied; rest keeps spread init); feedback controller re-created (state
resets — reset-on-reshape modal is the front-end contract). Failure
leaves the old net untouched.
- nisps_ml_describe(ml, out): takes the handle; null reports defaults.
- FeedbackController got the same storage split: algorithms in
FeedbackControllerCore<FbStorage>; FixedFeedbackStorage keeps firmware/
tests source-identical via the old alias; DynamicFeedbackStorage (one
arena) sizes to the runtime net. Firmware .text unchanged (122692).
- MLHandle: per-instance scratch vectors; dropped the dead 2MB
batch_out_scratch.
- TS: types.ts decls (+_nisps_ml_reshape), wasm-iml re-describes the
created instance, worker carries a shape-contract note for P2.3.
Verified: ctest 4/4 incl. new warm-start grow/shrink test; reshape ABI
smoke (dims honoured, overlap survives, invalid rejected, outputs
bounded); parity PASS unchanged (2.4e-7); lint clean; manifold 9 unit +
20 e2e green; firmware .text 122692 (+0.30% vs pre-P2 baseline).
2026-07-14 03:38:06 +02:00
|
|
|
|
if (current_out) out = std::span<const float>(current_out, h->n_out());
|
2026-06-28 04:14:12 +02:00
|
|
|
|
std::span<const std::uint8_t> mask;
|
feat(wasm)!: P2.2 — nisps_ml_create honours dims; runtime-shaped browser MLP + reshape
Operator-approved ABI change (P2 stop-point). The WASM MLP is now
MLPCore<DynamicStorage>:
- nisps_ml_create(input, output, hidden[3], n, seed) honours its args;
non-positive/null fall back to the historical 32→[10,14,18]→126, so
every pre-P2 caller (manifold, worker, parity harness) stays
bit-identical. Invalid/oversized dims (>4096) → null.
- NEW nisps_ml_reshape(ml, in, out, hidden, n, spread): fresh net at the
new dims, warm-started via nisps/ml/warm_start.hpp (overlapping region
copied; rest keeps spread init); feedback controller re-created (state
resets — reset-on-reshape modal is the front-end contract). Failure
leaves the old net untouched.
- nisps_ml_describe(ml, out): takes the handle; null reports defaults.
- FeedbackController got the same storage split: algorithms in
FeedbackControllerCore<FbStorage>; FixedFeedbackStorage keeps firmware/
tests source-identical via the old alias; DynamicFeedbackStorage (one
arena) sizes to the runtime net. Firmware .text unchanged (122692).
- MLHandle: per-instance scratch vectors; dropped the dead 2MB
batch_out_scratch.
- TS: types.ts decls (+_nisps_ml_reshape), wasm-iml re-describes the
created instance, worker carries a shape-contract note for P2.3.
Verified: ctest 4/4 incl. new warm-start grow/shrink test; reshape ABI
smoke (dims honoured, overlap survives, invalid rejected, outputs
bounded); parity PASS unchanged (2.4e-7); lint clean; manifold 9 unit +
20 e2e green; firmware .text 122692 (+0.30% vs pre-P2 baseline).
2026-07-14 03:38:06 +02:00
|
|
|
|
if (pin_mask) mask = std::span<const std::uint8_t>(pin_mask, h->n_out());
|
2026-06-28 04:14:12 +02:00
|
|
|
|
return static_cast<int>(h->feedback.on_down(h->mlp, out, speed, spread, mask));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
|
|
|
|
int nisps_ml_feedback_up(void* ml) {
|
|
|
|
|
|
if (!ml) return 0;
|
|
|
|
|
|
auto* h = static_cast<MLHandle*>(ml);
|
|
|
|
|
|
return static_cast<int>(h->feedback.on_up(h->mlp));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat(wasm)!: P2.2 — nisps_ml_create honours dims; runtime-shaped browser MLP + reshape
Operator-approved ABI change (P2 stop-point). The WASM MLP is now
MLPCore<DynamicStorage>:
- nisps_ml_create(input, output, hidden[3], n, seed) honours its args;
non-positive/null fall back to the historical 32→[10,14,18]→126, so
every pre-P2 caller (manifold, worker, parity harness) stays
bit-identical. Invalid/oversized dims (>4096) → null.
- NEW nisps_ml_reshape(ml, in, out, hidden, n, spread): fresh net at the
new dims, warm-started via nisps/ml/warm_start.hpp (overlapping region
copied; rest keeps spread init); feedback controller re-created (state
resets — reset-on-reshape modal is the front-end contract). Failure
leaves the old net untouched.
- nisps_ml_describe(ml, out): takes the handle; null reports defaults.
- FeedbackController got the same storage split: algorithms in
FeedbackControllerCore<FbStorage>; FixedFeedbackStorage keeps firmware/
tests source-identical via the old alias; DynamicFeedbackStorage (one
arena) sizes to the runtime net. Firmware .text unchanged (122692).
- MLHandle: per-instance scratch vectors; dropped the dead 2MB
batch_out_scratch.
- TS: types.ts decls (+_nisps_ml_reshape), wasm-iml re-describes the
created instance, worker carries a shape-contract note for P2.3.
Verified: ctest 4/4 incl. new warm-start grow/shrink test; reshape ABI
smoke (dims honoured, overlap survives, invalid rejected, outputs
bounded); parity PASS unchanged (2.4e-7); lint clean; manifold 9 unit +
20 e2e green; firmware .text 122692 (+0.30% vs pre-P2 baseline).
2026-07-14 03:38:06 +02:00
|
|
|
|
// If returns 1, `out` (n_out floats) holds the static bypass vector
|
2026-06-28 04:14:12 +02:00
|
|
|
|
// and the caller should NOT call nisps_ml_process(); if 0, run process().
|
|
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
|
|
|
|
int nisps_ml_feedback_static_output(void* ml, float* out) {
|
|
|
|
|
|
if (!ml || !out) return 0;
|
|
|
|
|
|
auto* h = static_cast<MLHandle*>(ml);
|
feat(wasm)!: P2.2 — nisps_ml_create honours dims; runtime-shaped browser MLP + reshape
Operator-approved ABI change (P2 stop-point). The WASM MLP is now
MLPCore<DynamicStorage>:
- nisps_ml_create(input, output, hidden[3], n, seed) honours its args;
non-positive/null fall back to the historical 32→[10,14,18]→126, so
every pre-P2 caller (manifold, worker, parity harness) stays
bit-identical. Invalid/oversized dims (>4096) → null.
- NEW nisps_ml_reshape(ml, in, out, hidden, n, spread): fresh net at the
new dims, warm-started via nisps/ml/warm_start.hpp (overlapping region
copied; rest keeps spread init); feedback controller re-created (state
resets — reset-on-reshape modal is the front-end contract). Failure
leaves the old net untouched.
- nisps_ml_describe(ml, out): takes the handle; null reports defaults.
- FeedbackController got the same storage split: algorithms in
FeedbackControllerCore<FbStorage>; FixedFeedbackStorage keeps firmware/
tests source-identical via the old alias; DynamicFeedbackStorage (one
arena) sizes to the runtime net. Firmware .text unchanged (122692).
- MLHandle: per-instance scratch vectors; dropped the dead 2MB
batch_out_scratch.
- TS: types.ts decls (+_nisps_ml_reshape), wasm-iml re-describes the
created instance, worker carries a shape-contract note for P2.3.
Verified: ctest 4/4 incl. new warm-start grow/shrink test; reshape ABI
smoke (dims honoured, overlap survives, invalid rejected, outputs
bounded); parity PASS unchanged (2.4e-7); lint clean; manifold 9 unit +
20 e2e green; firmware .text 122692 (+0.30% vs pre-P2 baseline).
2026-07-14 03:38:06 +02:00
|
|
|
|
const bool bypass = h->feedback.static_output(std::span<float>(
|
|
|
|
|
|
h->feedback_static_scratch.data(), h->feedback_static_scratch.size()));
|
2026-06-28 04:14:12 +02:00
|
|
|
|
if (bypass) {
|
|
|
|
|
|
std::memcpy(out, h->feedback_static_scratch.data(),
|
feat(wasm)!: P2.2 — nisps_ml_create honours dims; runtime-shaped browser MLP + reshape
Operator-approved ABI change (P2 stop-point). The WASM MLP is now
MLPCore<DynamicStorage>:
- nisps_ml_create(input, output, hidden[3], n, seed) honours its args;
non-positive/null fall back to the historical 32→[10,14,18]→126, so
every pre-P2 caller (manifold, worker, parity harness) stays
bit-identical. Invalid/oversized dims (>4096) → null.
- NEW nisps_ml_reshape(ml, in, out, hidden, n, spread): fresh net at the
new dims, warm-started via nisps/ml/warm_start.hpp (overlapping region
copied; rest keeps spread init); feedback controller re-created (state
resets — reset-on-reshape modal is the front-end contract). Failure
leaves the old net untouched.
- nisps_ml_describe(ml, out): takes the handle; null reports defaults.
- FeedbackController got the same storage split: algorithms in
FeedbackControllerCore<FbStorage>; FixedFeedbackStorage keeps firmware/
tests source-identical via the old alias; DynamicFeedbackStorage (one
arena) sizes to the runtime net. Firmware .text unchanged (122692).
- MLHandle: per-instance scratch vectors; dropped the dead 2MB
batch_out_scratch.
- TS: types.ts decls (+_nisps_ml_reshape), wasm-iml re-describes the
created instance, worker carries a shape-contract note for P2.3.
Verified: ctest 4/4 incl. new warm-start grow/shrink test; reshape ABI
smoke (dims honoured, overlap survives, invalid rejected, outputs
bounded); parity PASS unchanged (2.4e-7); lint clean; manifold 9 unit +
20 e2e green; firmware .text 122692 (+0.30% vs pre-P2 baseline).
2026-07-14 03:38:06 +02:00
|
|
|
|
h->n_out() * sizeof(float));
|
2026-06-28 04:14:12 +02:00
|
|
|
|
}
|
|
|
|
|
|
return bypass ? 1 : 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// ML feedback — ExploreAndPlace lifecycle (Idle → Exploring → Placing → Idle).
|
|
|
|
|
|
// Granular transitions so the SAME shared core drives both the browser (which
|
|
|
|
|
|
// also uses on_down/on_up via _down/_up) and firmware (which maps buttons to
|
|
|
|
|
|
// these directly). Set mode 3 (ExploreAndPlace) via nisps_ml_feedback_set_mode.
|
|
|
|
|
|
//
|
|
|
|
|
|
// CALLER CONTRACT (commit ordering): on _commit_place the controller restores
|
|
|
|
|
|
// the REAL net; the caller then reads nisps_ml_feedback_committed_output and
|
|
|
|
|
|
// adds it as the +1 example label at the chosen input, then trains.
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
// Idle→Exploring: snapshot the real net, randomise a scratchpad.
|
|
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
|
|
|
|
void nisps_ml_feedback_enter_explore(void* ml, float spread) {
|
|
|
|
|
|
if (!ml) return;
|
|
|
|
|
|
auto* h = static_cast<MLHandle*>(ml);
|
|
|
|
|
|
h->feedback.enter_explore(h->mlp, spread);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Exploring→Idle: restore the real net, discard scratchpad.
|
|
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
|
|
|
|
void nisps_ml_feedback_exit_explore(void* ml) {
|
|
|
|
|
|
if (!ml) return;
|
|
|
|
|
|
auto* h = static_cast<MLHandle*>(ml);
|
|
|
|
|
|
h->feedback.exit_explore(h->mlp);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Exploring scratchpad op: re-randomise.
|
|
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
|
|
|
|
void nisps_ml_feedback_reroll(void* ml, float spread) {
|
|
|
|
|
|
if (!ml) return;
|
|
|
|
|
|
auto* h = static_cast<MLHandle*>(ml);
|
|
|
|
|
|
h->feedback.reroll(h->mlp, spread);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Exploring scratchpad op: bounded nudge (amount = noise stddev, e.g. 0.05).
|
|
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
|
|
|
|
void nisps_ml_feedback_nudge(void* ml, float amount) {
|
|
|
|
|
|
if (!ml) return;
|
|
|
|
|
|
auto* h = static_cast<MLHandle*>(ml);
|
|
|
|
|
|
h->feedback.nudge(h->mlp, amount);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Exploring scratchpad op: undo last reroll/nudge.
|
|
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
|
|
|
|
void nisps_ml_feedback_undo(void* ml) {
|
|
|
|
|
|
if (!ml) return;
|
|
|
|
|
|
auto* h = static_cast<MLHandle*>(ml);
|
|
|
|
|
|
h->feedback.undo(h->mlp);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Exploring→Placing: freeze the scratchpad output at its CURRENT input.
|
|
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
|
|
|
|
void nisps_ml_feedback_like(void* ml) {
|
|
|
|
|
|
if (!ml) return;
|
|
|
|
|
|
auto* h = static_cast<MLHandle*>(ml);
|
|
|
|
|
|
h->feedback.begin_place(h->mlp);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Placing→Idle: restore the real net. Caller then reads committed_output and
|
|
|
|
|
|
// stores the +1 example at the chosen input.
|
|
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
|
|
|
|
void nisps_ml_feedback_commit_place(void* ml) {
|
|
|
|
|
|
if (!ml) return;
|
|
|
|
|
|
auto* h = static_cast<MLHandle*>(ml);
|
|
|
|
|
|
h->feedback.commit_place(h->mlp);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Placing→Exploring: back out of placing (no store).
|
|
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
|
|
|
|
void nisps_ml_feedback_cancel_place(void* ml) {
|
|
|
|
|
|
if (!ml) return;
|
|
|
|
|
|
auto* h = static_cast<MLHandle*>(ml);
|
|
|
|
|
|
h->feedback.cancel_place();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Scratchpad undo-ring depth currently available to pop.
|
|
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
|
|
|
|
int nisps_ml_feedback_undo_depth(void* ml) {
|
|
|
|
|
|
if (!ml) return 0;
|
|
|
|
|
|
return static_cast<int>(static_cast<MLHandle*>(ml)->feedback.undo_depth());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat(wasm)!: P2.2 — nisps_ml_create honours dims; runtime-shaped browser MLP + reshape
Operator-approved ABI change (P2 stop-point). The WASM MLP is now
MLPCore<DynamicStorage>:
- nisps_ml_create(input, output, hidden[3], n, seed) honours its args;
non-positive/null fall back to the historical 32→[10,14,18]→126, so
every pre-P2 caller (manifold, worker, parity harness) stays
bit-identical. Invalid/oversized dims (>4096) → null.
- NEW nisps_ml_reshape(ml, in, out, hidden, n, spread): fresh net at the
new dims, warm-started via nisps/ml/warm_start.hpp (overlapping region
copied; rest keeps spread init); feedback controller re-created (state
resets — reset-on-reshape modal is the front-end contract). Failure
leaves the old net untouched.
- nisps_ml_describe(ml, out): takes the handle; null reports defaults.
- FeedbackController got the same storage split: algorithms in
FeedbackControllerCore<FbStorage>; FixedFeedbackStorage keeps firmware/
tests source-identical via the old alias; DynamicFeedbackStorage (one
arena) sizes to the runtime net. Firmware .text unchanged (122692).
- MLHandle: per-instance scratch vectors; dropped the dead 2MB
batch_out_scratch.
- TS: types.ts decls (+_nisps_ml_reshape), wasm-iml re-describes the
created instance, worker carries a shape-contract note for P2.3.
Verified: ctest 4/4 incl. new warm-start grow/shrink test; reshape ABI
smoke (dims honoured, overlap survives, invalid rejected, outputs
bounded); parity PASS unchanged (2.4e-7); lint clean; manifold 9 unit +
20 e2e green; firmware .text 122692 (+0.30% vs pre-P2 baseline).
2026-07-14 03:38:06 +02:00
|
|
|
|
// Writes the committed/placed output vector (n_out floats) into `out`.
|
2026-06-28 04:14:12 +02:00
|
|
|
|
// Returns 1 if a vector was written (placing OR a fresh commit), else 0. Reads
|
|
|
|
|
|
// committed_output() (valid post-commit) falling back to placed_output() (while
|
|
|
|
|
|
// placing) so the caller can grab the label either before or after commit.
|
|
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
|
|
|
|
int nisps_ml_feedback_placed_output(void* ml, float* out) {
|
|
|
|
|
|
if (!ml || !out) return 0;
|
|
|
|
|
|
auto* h = static_cast<MLHandle*>(ml);
|
|
|
|
|
|
std::span<const float> v = h->feedback.committed_output();
|
|
|
|
|
|
if (v.empty()) v = h->feedback.placed_output();
|
|
|
|
|
|
if (v.empty()) return 0;
|
feat(wasm)!: P2.2 — nisps_ml_create honours dims; runtime-shaped browser MLP + reshape
Operator-approved ABI change (P2 stop-point). The WASM MLP is now
MLPCore<DynamicStorage>:
- nisps_ml_create(input, output, hidden[3], n, seed) honours its args;
non-positive/null fall back to the historical 32→[10,14,18]→126, so
every pre-P2 caller (manifold, worker, parity harness) stays
bit-identical. Invalid/oversized dims (>4096) → null.
- NEW nisps_ml_reshape(ml, in, out, hidden, n, spread): fresh net at the
new dims, warm-started via nisps/ml/warm_start.hpp (overlapping region
copied; rest keeps spread init); feedback controller re-created (state
resets — reset-on-reshape modal is the front-end contract). Failure
leaves the old net untouched.
- nisps_ml_describe(ml, out): takes the handle; null reports defaults.
- FeedbackController got the same storage split: algorithms in
FeedbackControllerCore<FbStorage>; FixedFeedbackStorage keeps firmware/
tests source-identical via the old alias; DynamicFeedbackStorage (one
arena) sizes to the runtime net. Firmware .text unchanged (122692).
- MLHandle: per-instance scratch vectors; dropped the dead 2MB
batch_out_scratch.
- TS: types.ts decls (+_nisps_ml_reshape), wasm-iml re-describes the
created instance, worker carries a shape-contract note for P2.3.
Verified: ctest 4/4 incl. new warm-start grow/shrink test; reshape ABI
smoke (dims honoured, overlap survives, invalid rejected, outputs
bounded); parity PASS unchanged (2.4e-7); lint clean; manifold 9 unit +
20 e2e green; firmware .text 122692 (+0.30% vs pre-P2 baseline).
2026-07-14 03:38:06 +02:00
|
|
|
|
const std::size_t n = (v.size() < h->n_out()) ? v.size() : h->n_out();
|
2026-06-28 04:14:12 +02:00
|
|
|
|
std::memcpy(out, v.data(), n * sizeof(float));
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat(ml)!: P3 core — geometric dislike in nisps/, jolt/OU + geo ABI
Geometric dislike (rl-feedback-design §2.1/§4; upstream InterfaceRL @
0a541cc ported verbatim, constants included):
- nisps/ml/replay.hpp: ReplayView over storage-owned buffers — deepen-or-
store negatives (dedup 0.05, clamp -16), k-NN positive centroid with
deterministic index tie-break + fixed accumulation order, proportional
decay (0.0025*max(|r|,1)) + eviction, order-preserving compaction.
- nisps/ml/geo_push.hpp: push-away target (pushStep clamp(|avgNeg|,.25,1)
*0.5, taper /(1+len), useRandom on len<=1e-4 via nisps::Rng — the single
deliberate divergence from libc rand()), negLRRatio 0.5-0.4*negFraction.
- mlp.hpp: train_targets(input, computed-target, lr, out_mask) — trains
toward computed targets (negative lr = cold-start train-away); solo/
focus gating zeroes masked derivs.
- feedback.hpp: AvoidStyle {Geometric (new default), Diffuse (legacy
move_weights, kept for A/B)}; dislike_geometric() collapses upstream's
press+optimise into one synchronous call; on_up in geometric Avoid
feeds the positive centroid; dislike-multiplier bookkeeping. Storage
gains replay buffers (Fixed: ReplayCap=32 firmware default ≈ +8KB SRAM;
Dynamic arena: cap 64).
- bindings: nisps_ml_feedback_{dislike_geometric,store_positive,
positive_count,negative_count,set_avoid_style} + P3.2 jolt/OU ABI:
nisps_ml_jolt_{press,step,release,active,lr_scale,tick_lr_ramp},
nisps_ml_explore_{intensity,get_intensity,apply} (OUNoise<4096>
over-provisioned; same code the firmware ModeBase runs).
- parity v4: Stage 6 scripted geometric session (2 likes → 2 dislikes,
f32-exact heard vectors via Math.fround) — 961 floats PASS at 2.4e-7.
- tests: test_mlp_geo_dislike.cpp (replay dedup/deepen/clamp, centroid
tie-break, push direction/taper/mask/clamp, cold-start inertness +
train-away, determinism, Diffuse legacy); legacy Avoid test pinned to
Diffuse per the ADR's deliberate-break note.
Firmware: PAFSynth .text/.data unchanged (geometric path not referenced
by current glue). NOTE: discovered pre-existing bug 10c3e55c — the
explore/place wiring is linker-GC'd out of the PAFSynth ELF (predates
this refactor; evidence in the ergo task).
2026-07-14 04:16:21 +02:00
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// ML feedback — geometric dislike (one-core-engine P3; rl-feedback-design
|
|
|
|
|
|
// §2.1). The Avoid mode's default realisation. current_out may be null (the
|
|
|
|
|
|
// MLP's live output is used — note the zero-derivative caveat: pass the
|
|
|
|
|
|
// HEARD post-pipeline vector for an audible push). lr <= 0 uses the
|
|
|
|
|
|
// controller default (1e-3, upstream).
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
|
|
|
|
int nisps_ml_feedback_dislike_geometric(void* ml, const float* current_out, float lr) {
|
|
|
|
|
|
if (!ml) return 0;
|
|
|
|
|
|
auto* h = static_cast<MLHandle*>(ml);
|
|
|
|
|
|
std::span<const float> out;
|
|
|
|
|
|
if (current_out) out = std::span<const float>(current_out, h->n_out());
|
|
|
|
|
|
const float use_lr = (lr > 0.f) ? lr : h->feedback.geo_lr();
|
|
|
|
|
|
return static_cast<int>(h->feedback.dislike_geometric(h->mlp, out, use_lr));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Store a positive (like) into the replay memory so the k-NN centroid sees
|
|
|
|
|
|
// it. current_out may be null (live output used). The caller still runs its
|
|
|
|
|
|
// usual addExample + train.
|
|
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
|
|
|
|
void nisps_ml_feedback_store_positive(void* ml, const float* current_out) {
|
|
|
|
|
|
if (!ml) return;
|
|
|
|
|
|
auto* h = static_cast<MLHandle*>(ml);
|
|
|
|
|
|
std::span<const float> out;
|
|
|
|
|
|
if (current_out) out = std::span<const float>(current_out, h->n_out());
|
|
|
|
|
|
h->feedback.store_positive(h->mlp, out);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
|
|
|
|
int nisps_ml_feedback_positive_count(void* ml) {
|
|
|
|
|
|
if (!ml) return 0;
|
|
|
|
|
|
return static_cast<int>(static_cast<MLHandle*>(ml)->feedback.positive_count());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
|
|
|
|
int nisps_ml_feedback_negative_count(void* ml) {
|
|
|
|
|
|
if (!ml) return 0;
|
|
|
|
|
|
return static_cast<int>(static_cast<MLHandle*>(ml)->feedback.negative_count());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Avoid sub-mode: 0 = Geometric (default), 1 = Diffuse (legacy move_weights,
|
|
|
|
|
|
// kept for A/B comparison).
|
|
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
|
|
|
|
void nisps_ml_feedback_set_avoid_style(void* ml, int style) {
|
|
|
|
|
|
if (!ml) return;
|
|
|
|
|
|
static_cast<MLHandle*>(ml)->feedback.set_avoid_style(
|
|
|
|
|
|
style == 1 ? nisps::ml::AvoidStyle::Diffuse : nisps::ml::AvoidStyle::Geometric);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// Jolt (held weight morph) + OU exploration noise (one-core-engine P3.2) —
|
|
|
|
|
|
// the same nisps/ml/{jolt,ou_noise}.hpp the firmware ModeBase runs.
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
|
|
|
|
void nisps_ml_jolt_press(void* ml) {
|
|
|
|
|
|
if (!ml) return;
|
|
|
|
|
|
auto* h = static_cast<MLHandle*>(ml);
|
|
|
|
|
|
h->jolt.press(h->mlp.weight_count());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// One ~200 Hz morph tick while held (no-op when inactive): reads the flat
|
|
|
|
|
|
// weights, glides the jolt-selected few, writes them back.
|
|
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
|
|
|
|
void nisps_ml_jolt_step(void* ml) {
|
|
|
|
|
|
if (!ml) return;
|
|
|
|
|
|
auto* h = static_cast<MLHandle*>(ml);
|
|
|
|
|
|
if (!h->jolt.active()) return;
|
|
|
|
|
|
auto w = h->mlp.get_weights();
|
|
|
|
|
|
for (std::size_t i = 0; i < w.size(); ++i) h->jolt_scratch[i] = w[i];
|
|
|
|
|
|
h->jolt.step(std::span<float>(h->jolt_scratch.data(), w.size()));
|
|
|
|
|
|
h->mlp.set_weights(std::span<const float>(h->jolt_scratch.data(), w.size()));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
|
|
|
|
void nisps_ml_jolt_release(void* ml) {
|
|
|
|
|
|
if (!ml) return;
|
|
|
|
|
|
static_cast<MLHandle*>(ml)->jolt.release();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
|
|
|
|
int nisps_ml_jolt_active(void* ml) {
|
|
|
|
|
|
if (!ml) return 0;
|
|
|
|
|
|
return static_cast<MLHandle*>(ml)->jolt.active() ? 1 : 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Exploration amount in [0,1]; 0 disables (inert — parity-safe).
|
|
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
|
|
|
|
void nisps_ml_explore_intensity(void* ml, float level) {
|
|
|
|
|
|
if (!ml) return;
|
|
|
|
|
|
static_cast<MLHandle*>(ml)->ou.set_intensity(level);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
|
|
|
|
float nisps_ml_explore_get_intensity(void* ml) {
|
|
|
|
|
|
if (!ml) return 0.f;
|
|
|
|
|
|
return static_cast<MLHandle*>(ml)->ou.intensity();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Advance the OU walk and add it (clamped to [0,1]) to `inout` (n floats,
|
|
|
|
|
|
// capped at the instance's n_out). No-op at intensity 0.
|
|
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
|
|
|
|
void nisps_ml_explore_apply(void* ml, float* inout, int n) {
|
|
|
|
|
|
if (!ml || !inout || n <= 0) return;
|
|
|
|
|
|
auto* h = static_cast<MLHandle*>(ml);
|
|
|
|
|
|
std::size_t count = static_cast<std::size_t>(n);
|
|
|
|
|
|
if (count > h->n_out()) count = h->n_out();
|
|
|
|
|
|
h->ou.apply(std::span<float>(inout, count));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat(playground/wasm): WASM bridge between C++ core and SolidJS playground (meml-tgm)
Stream 7 wires nisps/ml + nisps/engines into the playground via Emscripten.
Highlights:
- nisps/wasm/bindings.cpp: flat C API per architecture.md §6.2. Fixed-arch
MLP<2, 10, 14, 18, 126>; engine string→type dispatch table with NoOp
fallback.
- scripts/build-wasm.sh: emcc invocation, MODULARIZE=1, exports listed
explicitly; produces playground/public/nisps.{js,wasm}.
- playground/src/ml/wasm-iml.ts: main-thread MLP host (sync inference,
sync training, RL ops, weights I/O, layer stats, localStorage).
- playground/src/ml/wasm-worker.ts: disposable Web Worker for off-thread
async training, owns its own WASM instance.
- playground/src/ml/dataset.ts: Float32Array-backed FIFO with sample-weight
modes (uniform/global/local/combined). Port of legacy dataset.js.
- playground/src/audio/engine-host.ts: AudioContext + AudioWorkletNode
lifecycle, with start/stop/setEngine/setParams.
- playground/src/audio/worklet/nisps-processor.ts: WASM-loading
AudioWorkletProcessor that runs engine.process_block per 128-sample
block. Loads its own WASM instance from main-thread-supplied bytes
(no fetch in worklet).
- playground/src/stores/ml-store.ts: wired stub methods to WasmIML
singleton; lazy initialize().
- playground/src/debug/probe.ts: window.__nisps now calls real WasmIML
via the store; lazy-init on first use.
Verified:
- bash scripts/build-wasm.sh succeeds (94 KB nisps.wasm)
- bun run typecheck OK
- bun run build OK (production bundle)
- vite dev server serves /nisps.{js,wasm} with COOP/COEP
Known limitation: WASM is fixed at one MLP shape. Multi-arch deferred —
documented in nisps/wasm/README.md.
2026-04-29 15:36:29 +02:00
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
|
|
|
|
void nisps_ml_get_layer_stats(void* ml, float* out_stats) {
|
|
|
|
|
|
if (!ml || !out_stats) return;
|
|
|
|
|
|
auto* h = static_cast<MLHandle*>(ml);
|
feat(wasm)!: P2.2 — nisps_ml_create honours dims; runtime-shaped browser MLP + reshape
Operator-approved ABI change (P2 stop-point). The WASM MLP is now
MLPCore<DynamicStorage>:
- nisps_ml_create(input, output, hidden[3], n, seed) honours its args;
non-positive/null fall back to the historical 32→[10,14,18]→126, so
every pre-P2 caller (manifold, worker, parity harness) stays
bit-identical. Invalid/oversized dims (>4096) → null.
- NEW nisps_ml_reshape(ml, in, out, hidden, n, spread): fresh net at the
new dims, warm-started via nisps/ml/warm_start.hpp (overlapping region
copied; rest keeps spread init); feedback controller re-created (state
resets — reset-on-reshape modal is the front-end contract). Failure
leaves the old net untouched.
- nisps_ml_describe(ml, out): takes the handle; null reports defaults.
- FeedbackController got the same storage split: algorithms in
FeedbackControllerCore<FbStorage>; FixedFeedbackStorage keeps firmware/
tests source-identical via the old alias; DynamicFeedbackStorage (one
arena) sizes to the runtime net. Firmware .text unchanged (122692).
- MLHandle: per-instance scratch vectors; dropped the dead 2MB
batch_out_scratch.
- TS: types.ts decls (+_nisps_ml_reshape), wasm-iml re-describes the
created instance, worker carries a shape-contract note for P2.3.
Verified: ctest 4/4 incl. new warm-start grow/shrink test; reshape ABI
smoke (dims honoured, overlap survives, invalid rejected, outputs
bounded); parity PASS unchanged (2.4e-7); lint clean; manifold 9 unit +
20 e2e green; firmware .text 122692 (+0.30% vs pre-P2 baseline).
2026-07-14 03:38:06 +02:00
|
|
|
|
for (std::size_t i = 0; i < BrowserMLP::kNumLayers; ++i) {
|
feat(playground/wasm): WASM bridge between C++ core and SolidJS playground (meml-tgm)
Stream 7 wires nisps/ml + nisps/engines into the playground via Emscripten.
Highlights:
- nisps/wasm/bindings.cpp: flat C API per architecture.md §6.2. Fixed-arch
MLP<2, 10, 14, 18, 126>; engine string→type dispatch table with NoOp
fallback.
- scripts/build-wasm.sh: emcc invocation, MODULARIZE=1, exports listed
explicitly; produces playground/public/nisps.{js,wasm}.
- playground/src/ml/wasm-iml.ts: main-thread MLP host (sync inference,
sync training, RL ops, weights I/O, layer stats, localStorage).
- playground/src/ml/wasm-worker.ts: disposable Web Worker for off-thread
async training, owns its own WASM instance.
- playground/src/ml/dataset.ts: Float32Array-backed FIFO with sample-weight
modes (uniform/global/local/combined). Port of legacy dataset.js.
- playground/src/audio/engine-host.ts: AudioContext + AudioWorkletNode
lifecycle, with start/stop/setEngine/setParams.
- playground/src/audio/worklet/nisps-processor.ts: WASM-loading
AudioWorkletProcessor that runs engine.process_block per 128-sample
block. Loads its own WASM instance from main-thread-supplied bytes
(no fetch in worklet).
- playground/src/stores/ml-store.ts: wired stub methods to WasmIML
singleton; lazy initialize().
- playground/src/debug/probe.ts: window.__nisps now calls real WasmIML
via the store; lazy-init on first use.
Verified:
- bash scripts/build-wasm.sh succeeds (94 KB nisps.wasm)
- bun run typecheck OK
- bun run build OK (production bundle)
- vite dev server serves /nisps.{js,wasm} with COOP/COEP
Known limitation: WASM is fixed at one MLP shape. Multi-arch deferred —
documented in nisps/wasm/README.md.
2026-04-29 15:36:29 +02:00
|
|
|
|
const auto s = h->mlp.layer_stats(i);
|
|
|
|
|
|
out_stats[i * 4u + 0u] = s.mean_abs;
|
|
|
|
|
|
out_stats[i * 4u + 1u] = s.max_abs;
|
|
|
|
|
|
out_stats[i * 4u + 2u] = s.dead_frac;
|
|
|
|
|
|
out_stats[i * 4u + 3u] = s.saturating_frac;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
|
|
|
|
void nisps_ml_clear_examples(void* ml) {
|
|
|
|
|
|
if (!ml) return;
|
|
|
|
|
|
auto* h = static_cast<MLHandle*>(ml);
|
|
|
|
|
|
h->mlp.clear_examples();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
fix(ml): one named example capacity; train() and trainAsync() no longer diverge
Phase 2, S35. Two real defects from one root cause, both confirmed by trace
rather than taken from the audit:
1. Divergence. WasmIML built its TS Dataset mirror with a cap of 100 while
every addExample() ALSO pushed into the C++ FIFO ring, capped at 128. Since
train() reads the C++ ring and trainAsync() reads the TS mirror, past 100
examples the two trained on different datasets — silently.
2. Latent OOB read. nisps_ml_train sizes its sample-weight span by the C++
side's example_count() (up to 128), but wasm-iml.ts allocates that heap
buffer from the TS dataset's size (<=100). Once the ring exceeds the mirror,
the span reads past the end of the caller's allocation.
Fix: name the capacity ONCE as nisps::ml::kDefaultMaxExamples = 128, used by
FixedStorage's default template arg, DynamicStorage's default ctor arg, and the
MLP<> alias (which is the only real FixedStorage instantiation path and carried
its own independent 128 literal — the last copy of this dual truth). Expose it
through nisps_ml_describe and have the TS side read it instead of hardcoding.
Dataset's constructor default is removed entirely: a default was what invited
this bug class, and the sole call site now always supplies the describe() value.
ABI NOTE: this extends nisps_ml_describe from a 6-int to a 7-int descriptor.
nisps_ml_describe always writes 7 ints regardless of the caller's buffer, so
every call site had to grow in the same change or it would overflow the WASM
heap by 4 bytes per call. All five sites updated: three in wasm-iml.ts (init
defaults, init per-instance, reshape re-describe — the finding said there were
two), one in wasm-worker.ts, one in tests/cpp/parity_wasm.mjs. The parity
harness's expected-dims check now also pins the new max_examples slot.
Regression test: tests/cpp/test_mlp_storage_defaults.cpp — pins the two storage
policies to one constant, and drives MLPCore<DynamicStorage> exactly as
bindings.cpp does past the old TS cap, asserting it saturates at 128 and not at
100. Fail-before/pass-after confirmed by temporarily setting the constant to
100: 2 failures, named. Reverted: green.
Audit correction: the cited dataset.ts:81 is the FIFO eviction check; the
hardcoded default was at dataset.ts:45.
Gates: run-all-tests.sh ALL GREEN, parity PASS.
2026-07-21 13:22:38 +02:00
|
|
|
|
// Architecture introspection — writes [in, h1, h2, h3, out, n_layers,
|
|
|
|
|
|
// max_examples] into a caller-supplied int buffer. Always 7 ints. With a
|
|
|
|
|
|
// null handle it reports the DEFAULT shape (what create() yields for
|
|
|
|
|
|
// non-positive args); with a handle it reports that instance's actual
|
|
|
|
|
|
// runtime shape. max_examples is the example-store (dataset) ring-buffer
|
|
|
|
|
|
// capacity (nisps::ml::kDefaultMaxExamples, nisps/ml/storage.hpp) — the
|
|
|
|
|
|
// single source of truth the TS Dataset mirror sizes itself to instead of
|
|
|
|
|
|
// hardcoding a second, potentially-divergent number (S35).
|
feat(playground/wasm): WASM bridge between C++ core and SolidJS playground (meml-tgm)
Stream 7 wires nisps/ml + nisps/engines into the playground via Emscripten.
Highlights:
- nisps/wasm/bindings.cpp: flat C API per architecture.md §6.2. Fixed-arch
MLP<2, 10, 14, 18, 126>; engine string→type dispatch table with NoOp
fallback.
- scripts/build-wasm.sh: emcc invocation, MODULARIZE=1, exports listed
explicitly; produces playground/public/nisps.{js,wasm}.
- playground/src/ml/wasm-iml.ts: main-thread MLP host (sync inference,
sync training, RL ops, weights I/O, layer stats, localStorage).
- playground/src/ml/wasm-worker.ts: disposable Web Worker for off-thread
async training, owns its own WASM instance.
- playground/src/ml/dataset.ts: Float32Array-backed FIFO with sample-weight
modes (uniform/global/local/combined). Port of legacy dataset.js.
- playground/src/audio/engine-host.ts: AudioContext + AudioWorkletNode
lifecycle, with start/stop/setEngine/setParams.
- playground/src/audio/worklet/nisps-processor.ts: WASM-loading
AudioWorkletProcessor that runs engine.process_block per 128-sample
block. Loads its own WASM instance from main-thread-supplied bytes
(no fetch in worklet).
- playground/src/stores/ml-store.ts: wired stub methods to WasmIML
singleton; lazy initialize().
- playground/src/debug/probe.ts: window.__nisps now calls real WasmIML
via the store; lazy-init on first use.
Verified:
- bash scripts/build-wasm.sh succeeds (94 KB nisps.wasm)
- bun run typecheck OK
- bun run build OK (production bundle)
- vite dev server serves /nisps.{js,wasm} with COOP/COEP
Known limitation: WASM is fixed at one MLP shape. Multi-arch deferred —
documented in nisps/wasm/README.md.
2026-04-29 15:36:29 +02:00
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
feat(wasm)!: P2.2 — nisps_ml_create honours dims; runtime-shaped browser MLP + reshape
Operator-approved ABI change (P2 stop-point). The WASM MLP is now
MLPCore<DynamicStorage>:
- nisps_ml_create(input, output, hidden[3], n, seed) honours its args;
non-positive/null fall back to the historical 32→[10,14,18]→126, so
every pre-P2 caller (manifold, worker, parity harness) stays
bit-identical. Invalid/oversized dims (>4096) → null.
- NEW nisps_ml_reshape(ml, in, out, hidden, n, spread): fresh net at the
new dims, warm-started via nisps/ml/warm_start.hpp (overlapping region
copied; rest keeps spread init); feedback controller re-created (state
resets — reset-on-reshape modal is the front-end contract). Failure
leaves the old net untouched.
- nisps_ml_describe(ml, out): takes the handle; null reports defaults.
- FeedbackController got the same storage split: algorithms in
FeedbackControllerCore<FbStorage>; FixedFeedbackStorage keeps firmware/
tests source-identical via the old alias; DynamicFeedbackStorage (one
arena) sizes to the runtime net. Firmware .text unchanged (122692).
- MLHandle: per-instance scratch vectors; dropped the dead 2MB
batch_out_scratch.
- TS: types.ts decls (+_nisps_ml_reshape), wasm-iml re-describes the
created instance, worker carries a shape-contract note for P2.3.
Verified: ctest 4/4 incl. new warm-start grow/shrink test; reshape ABI
smoke (dims honoured, overlap survives, invalid rejected, outputs
bounded); parity PASS unchanged (2.4e-7); lint clean; manifold 9 unit +
20 e2e green; firmware .text 122692 (+0.30% vs pre-P2 baseline).
2026-07-14 03:38:06 +02:00
|
|
|
|
void nisps_ml_describe(void* ml, int* out_dims) {
|
feat(playground/wasm): WASM bridge between C++ core and SolidJS playground (meml-tgm)
Stream 7 wires nisps/ml + nisps/engines into the playground via Emscripten.
Highlights:
- nisps/wasm/bindings.cpp: flat C API per architecture.md §6.2. Fixed-arch
MLP<2, 10, 14, 18, 126>; engine string→type dispatch table with NoOp
fallback.
- scripts/build-wasm.sh: emcc invocation, MODULARIZE=1, exports listed
explicitly; produces playground/public/nisps.{js,wasm}.
- playground/src/ml/wasm-iml.ts: main-thread MLP host (sync inference,
sync training, RL ops, weights I/O, layer stats, localStorage).
- playground/src/ml/wasm-worker.ts: disposable Web Worker for off-thread
async training, owns its own WASM instance.
- playground/src/ml/dataset.ts: Float32Array-backed FIFO with sample-weight
modes (uniform/global/local/combined). Port of legacy dataset.js.
- playground/src/audio/engine-host.ts: AudioContext + AudioWorkletNode
lifecycle, with start/stop/setEngine/setParams.
- playground/src/audio/worklet/nisps-processor.ts: WASM-loading
AudioWorkletProcessor that runs engine.process_block per 128-sample
block. Loads its own WASM instance from main-thread-supplied bytes
(no fetch in worklet).
- playground/src/stores/ml-store.ts: wired stub methods to WasmIML
singleton; lazy initialize().
- playground/src/debug/probe.ts: window.__nisps now calls real WasmIML
via the store; lazy-init on first use.
Verified:
- bash scripts/build-wasm.sh succeeds (94 KB nisps.wasm)
- bun run typecheck OK
- bun run build OK (production bundle)
- vite dev server serves /nisps.{js,wasm} with COOP/COEP
Known limitation: WASM is fixed at one MLP shape. Multi-arch deferred —
documented in nisps/wasm/README.md.
2026-04-29 15:36:29 +02:00
|
|
|
|
if (!out_dims) return;
|
feat(wasm)!: P2.2 — nisps_ml_create honours dims; runtime-shaped browser MLP + reshape
Operator-approved ABI change (P2 stop-point). The WASM MLP is now
MLPCore<DynamicStorage>:
- nisps_ml_create(input, output, hidden[3], n, seed) honours its args;
non-positive/null fall back to the historical 32→[10,14,18]→126, so
every pre-P2 caller (manifold, worker, parity harness) stays
bit-identical. Invalid/oversized dims (>4096) → null.
- NEW nisps_ml_reshape(ml, in, out, hidden, n, spread): fresh net at the
new dims, warm-started via nisps/ml/warm_start.hpp (overlapping region
copied; rest keeps spread init); feedback controller re-created (state
resets — reset-on-reshape modal is the front-end contract). Failure
leaves the old net untouched.
- nisps_ml_describe(ml, out): takes the handle; null reports defaults.
- FeedbackController got the same storage split: algorithms in
FeedbackControllerCore<FbStorage>; FixedFeedbackStorage keeps firmware/
tests source-identical via the old alias; DynamicFeedbackStorage (one
arena) sizes to the runtime net. Firmware .text unchanged (122692).
- MLHandle: per-instance scratch vectors; dropped the dead 2MB
batch_out_scratch.
- TS: types.ts decls (+_nisps_ml_reshape), wasm-iml re-describes the
created instance, worker carries a shape-contract note for P2.3.
Verified: ctest 4/4 incl. new warm-start grow/shrink test; reshape ABI
smoke (dims honoured, overlap survives, invalid rejected, outputs
bounded); parity PASS unchanged (2.4e-7); lint clean; manifold 9 unit +
20 e2e green; firmware .text 122692 (+0.30% vs pre-P2 baseline).
2026-07-14 03:38:06 +02:00
|
|
|
|
if (!ml) {
|
|
|
|
|
|
out_dims[0] = static_cast<int>(kDefaultInputs);
|
|
|
|
|
|
out_dims[1] = static_cast<int>(kDefaultHidden[0]);
|
|
|
|
|
|
out_dims[2] = static_cast<int>(kDefaultHidden[1]);
|
|
|
|
|
|
out_dims[3] = static_cast<int>(kDefaultHidden[2]);
|
|
|
|
|
|
out_dims[4] = static_cast<int>(kDefaultOutputs);
|
|
|
|
|
|
out_dims[5] = static_cast<int>(BrowserMLP::kNumLayers);
|
fix(ml): one named example capacity; train() and trainAsync() no longer diverge
Phase 2, S35. Two real defects from one root cause, both confirmed by trace
rather than taken from the audit:
1. Divergence. WasmIML built its TS Dataset mirror with a cap of 100 while
every addExample() ALSO pushed into the C++ FIFO ring, capped at 128. Since
train() reads the C++ ring and trainAsync() reads the TS mirror, past 100
examples the two trained on different datasets — silently.
2. Latent OOB read. nisps_ml_train sizes its sample-weight span by the C++
side's example_count() (up to 128), but wasm-iml.ts allocates that heap
buffer from the TS dataset's size (<=100). Once the ring exceeds the mirror,
the span reads past the end of the caller's allocation.
Fix: name the capacity ONCE as nisps::ml::kDefaultMaxExamples = 128, used by
FixedStorage's default template arg, DynamicStorage's default ctor arg, and the
MLP<> alias (which is the only real FixedStorage instantiation path and carried
its own independent 128 literal — the last copy of this dual truth). Expose it
through nisps_ml_describe and have the TS side read it instead of hardcoding.
Dataset's constructor default is removed entirely: a default was what invited
this bug class, and the sole call site now always supplies the describe() value.
ABI NOTE: this extends nisps_ml_describe from a 6-int to a 7-int descriptor.
nisps_ml_describe always writes 7 ints regardless of the caller's buffer, so
every call site had to grow in the same change or it would overflow the WASM
heap by 4 bytes per call. All five sites updated: three in wasm-iml.ts (init
defaults, init per-instance, reshape re-describe — the finding said there were
two), one in wasm-worker.ts, one in tests/cpp/parity_wasm.mjs. The parity
harness's expected-dims check now also pins the new max_examples slot.
Regression test: tests/cpp/test_mlp_storage_defaults.cpp — pins the two storage
policies to one constant, and drives MLPCore<DynamicStorage> exactly as
bindings.cpp does past the old TS cap, asserting it saturates at 128 and not at
100. Fail-before/pass-after confirmed by temporarily setting the constant to
100: 2 failures, named. Reverted: green.
Audit correction: the cited dataset.ts:81 is the FIFO eviction check; the
hardcoded default was at dataset.ts:45.
Gates: run-all-tests.sh ALL GREEN, parity PASS.
2026-07-21 13:22:38 +02:00
|
|
|
|
out_dims[6] = static_cast<int>(nisps::ml::kDefaultMaxExamples);
|
feat(wasm)!: P2.2 — nisps_ml_create honours dims; runtime-shaped browser MLP + reshape
Operator-approved ABI change (P2 stop-point). The WASM MLP is now
MLPCore<DynamicStorage>:
- nisps_ml_create(input, output, hidden[3], n, seed) honours its args;
non-positive/null fall back to the historical 32→[10,14,18]→126, so
every pre-P2 caller (manifold, worker, parity harness) stays
bit-identical. Invalid/oversized dims (>4096) → null.
- NEW nisps_ml_reshape(ml, in, out, hidden, n, spread): fresh net at the
new dims, warm-started via nisps/ml/warm_start.hpp (overlapping region
copied; rest keeps spread init); feedback controller re-created (state
resets — reset-on-reshape modal is the front-end contract). Failure
leaves the old net untouched.
- nisps_ml_describe(ml, out): takes the handle; null reports defaults.
- FeedbackController got the same storage split: algorithms in
FeedbackControllerCore<FbStorage>; FixedFeedbackStorage keeps firmware/
tests source-identical via the old alias; DynamicFeedbackStorage (one
arena) sizes to the runtime net. Firmware .text unchanged (122692).
- MLHandle: per-instance scratch vectors; dropped the dead 2MB
batch_out_scratch.
- TS: types.ts decls (+_nisps_ml_reshape), wasm-iml re-describes the
created instance, worker carries a shape-contract note for P2.3.
Verified: ctest 4/4 incl. new warm-start grow/shrink test; reshape ABI
smoke (dims honoured, overlap survives, invalid rejected, outputs
bounded); parity PASS unchanged (2.4e-7); lint clean; manifold 9 unit +
20 e2e green; firmware .text 122692 (+0.30% vs pre-P2 baseline).
2026-07-14 03:38:06 +02:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
auto* h = static_cast<MLHandle*>(ml);
|
|
|
|
|
|
out_dims[0] = static_cast<int>(h->mlp.n_in());
|
|
|
|
|
|
out_dims[1] = static_cast<int>(h->mlp.fan_out(0u));
|
|
|
|
|
|
out_dims[2] = static_cast<int>(h->mlp.fan_out(1u));
|
|
|
|
|
|
out_dims[3] = static_cast<int>(h->mlp.fan_out(2u));
|
|
|
|
|
|
out_dims[4] = static_cast<int>(h->mlp.n_out());
|
|
|
|
|
|
out_dims[5] = static_cast<int>(BrowserMLP::kNumLayers);
|
fix(ml): one named example capacity; train() and trainAsync() no longer diverge
Phase 2, S35. Two real defects from one root cause, both confirmed by trace
rather than taken from the audit:
1. Divergence. WasmIML built its TS Dataset mirror with a cap of 100 while
every addExample() ALSO pushed into the C++ FIFO ring, capped at 128. Since
train() reads the C++ ring and trainAsync() reads the TS mirror, past 100
examples the two trained on different datasets — silently.
2. Latent OOB read. nisps_ml_train sizes its sample-weight span by the C++
side's example_count() (up to 128), but wasm-iml.ts allocates that heap
buffer from the TS dataset's size (<=100). Once the ring exceeds the mirror,
the span reads past the end of the caller's allocation.
Fix: name the capacity ONCE as nisps::ml::kDefaultMaxExamples = 128, used by
FixedStorage's default template arg, DynamicStorage's default ctor arg, and the
MLP<> alias (which is the only real FixedStorage instantiation path and carried
its own independent 128 literal — the last copy of this dual truth). Expose it
through nisps_ml_describe and have the TS side read it instead of hardcoding.
Dataset's constructor default is removed entirely: a default was what invited
this bug class, and the sole call site now always supplies the describe() value.
ABI NOTE: this extends nisps_ml_describe from a 6-int to a 7-int descriptor.
nisps_ml_describe always writes 7 ints regardless of the caller's buffer, so
every call site had to grow in the same change or it would overflow the WASM
heap by 4 bytes per call. All five sites updated: three in wasm-iml.ts (init
defaults, init per-instance, reshape re-describe — the finding said there were
two), one in wasm-worker.ts, one in tests/cpp/parity_wasm.mjs. The parity
harness's expected-dims check now also pins the new max_examples slot.
Regression test: tests/cpp/test_mlp_storage_defaults.cpp — pins the two storage
policies to one constant, and drives MLPCore<DynamicStorage> exactly as
bindings.cpp does past the old TS cap, asserting it saturates at 128 and not at
100. Fail-before/pass-after confirmed by temporarily setting the constant to
100: 2 failures, named. Reverted: green.
Audit correction: the cited dataset.ts:81 is the FIFO eviction check; the
hardcoded default was at dataset.ts:45.
Gates: run-all-tests.sh ALL GREEN, parity PASS.
2026-07-21 13:22:38 +02:00
|
|
|
|
out_dims[6] = static_cast<int>(h->mlp.max_examples());
|
feat(playground/wasm): WASM bridge between C++ core and SolidJS playground (meml-tgm)
Stream 7 wires nisps/ml + nisps/engines into the playground via Emscripten.
Highlights:
- nisps/wasm/bindings.cpp: flat C API per architecture.md §6.2. Fixed-arch
MLP<2, 10, 14, 18, 126>; engine string→type dispatch table with NoOp
fallback.
- scripts/build-wasm.sh: emcc invocation, MODULARIZE=1, exports listed
explicitly; produces playground/public/nisps.{js,wasm}.
- playground/src/ml/wasm-iml.ts: main-thread MLP host (sync inference,
sync training, RL ops, weights I/O, layer stats, localStorage).
- playground/src/ml/wasm-worker.ts: disposable Web Worker for off-thread
async training, owns its own WASM instance.
- playground/src/ml/dataset.ts: Float32Array-backed FIFO with sample-weight
modes (uniform/global/local/combined). Port of legacy dataset.js.
- playground/src/audio/engine-host.ts: AudioContext + AudioWorkletNode
lifecycle, with start/stop/setEngine/setParams.
- playground/src/audio/worklet/nisps-processor.ts: WASM-loading
AudioWorkletProcessor that runs engine.process_block per 128-sample
block. Loads its own WASM instance from main-thread-supplied bytes
(no fetch in worklet).
- playground/src/stores/ml-store.ts: wired stub methods to WasmIML
singleton; lazy initialize().
- playground/src/debug/probe.ts: window.__nisps now calls real WasmIML
via the store; lazy-init on first use.
Verified:
- bash scripts/build-wasm.sh succeeds (94 KB nisps.wasm)
- bun run typecheck OK
- bun run build OK (production bundle)
- vite dev server serves /nisps.{js,wasm} with COOP/COEP
Known limitation: WASM is fixed at one MLP shape. Multi-arch deferred —
documented in nisps/wasm/README.md.
2026-04-29 15:36:29 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
feat(pipeline)!: P4 core — input/output chains + curve catalog in nisps/
- nisps/pipeline/input_chain.hpp: faithful f32 port of the manifold input
pipeline (invert→deadzone→circular clamp→momentum-modulated zoom→centred
power→EMA→momentum update). Caller-supplied dt, internal accumulated
clock (no wall clock — deterministic; matches the P1 fixtures' clock
contract). Fixed-capacity velocity ring; serialisable state.
- nisps/pipeline/output_chain.hpp: curve→EMA→slew→freeze(+per-output mask)
chain, capacity-templated (browser cap 4096; firmware would use NOut).
- nisps/core/math.hpp: + centered_power(x, exponent) (both chains use it).
- bindings: nisps_pipeline_create/destroy, nisps_input_set_config(15-float
wire layout)/process/reset, nisps_output_set_config/set_freeze_mask/
process/reset, pipeline state save/load, nisps_curve_apply(+batch)
(ids 0-6 = Curve enum, 7 = centred power).
- parity v5 Stage 7: rational (transcendental-free) traces through both
chains (2 configs each) + full curve catalog — 1273 floats PASS, the
pipeline floats bit-identical native↔WASM.
- ctest test_pipeline.cpp: deadzone remap, circular clamp, zoom+freeze,
sticky anchor, frame-rate-independent EMA, momentum zoom-out/recovery,
state round-trip, slew, freeze gate/mask, reseed-on-length-change,
centred-power endpoints.
Part of one-core-engine-refactor P4; the manifold TS switch follows.
2026-07-18 11:52:53 +02:00
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// Pipelines (one-core-engine P4). Input chain: the 2-axis pad pipeline.
|
|
|
|
|
|
// Output chain: curve → EMA → slew → freeze over the routed vector. State
|
|
|
|
|
|
// lives C++-side per handle; the TS wrappers are thin.
|
|
|
|
|
|
//
|
|
|
|
|
|
// INPUT CONFIG WIRE LAYOUT (nisps_input_set_config, 15 floats — keep in
|
|
|
|
|
|
// lockstep with manifold/src/engine wrappers):
|
|
|
|
|
|
// [0] zoom [1] zoomX (0=null) [2] zoomY (0=null)
|
|
|
|
|
|
// [3] anchorX [4] anchorY [5] anchorMode (0 auto/1 sticky/2 centre)
|
|
|
|
|
|
// [6] deadzone [7] inputCurve [8] curveX (0=null)
|
|
|
|
|
|
// [9] curveY (0=null) [10] smoothing [11] momentumMode (0 off/1 gentle/2 strong)
|
|
|
|
|
|
// [12] velocityWindow (SECONDS) [13] invertX (!=0) [14] invertY (!=0)
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
|
|
|
|
void* nisps_pipeline_create(void) {
|
|
|
|
|
|
return static_cast<void*>(new PipelineHandle());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
|
|
|
|
void nisps_pipeline_destroy(void* p) {
|
|
|
|
|
|
if (!p) return;
|
|
|
|
|
|
delete static_cast<PipelineHandle*>(p);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
|
|
|
|
void nisps_input_set_config(void* p, const float* cfg, int n) {
|
|
|
|
|
|
if (!p || !cfg || n < 15) return;
|
|
|
|
|
|
auto* h = static_cast<PipelineHandle*>(p);
|
|
|
|
|
|
nisps::pipeline::InputChainConfig c;
|
|
|
|
|
|
c.zoom = cfg[0];
|
|
|
|
|
|
c.zoom_x = cfg[1];
|
|
|
|
|
|
c.zoom_y = cfg[2];
|
|
|
|
|
|
c.anchor_x = cfg[3];
|
|
|
|
|
|
c.anchor_y = cfg[4];
|
|
|
|
|
|
c.anchor_mode = static_cast<nisps::pipeline::AnchorMode>(
|
|
|
|
|
|
static_cast<int>(cfg[5]) == 1 ? 1 : (static_cast<int>(cfg[5]) == 2 ? 2 : 0));
|
|
|
|
|
|
c.deadzone = cfg[6];
|
|
|
|
|
|
c.input_curve = cfg[7];
|
|
|
|
|
|
c.curve_x = cfg[8];
|
|
|
|
|
|
c.curve_y = cfg[9];
|
|
|
|
|
|
c.smoothing = cfg[10];
|
|
|
|
|
|
c.momentum_mode = static_cast<nisps::pipeline::MomentumMode>(
|
|
|
|
|
|
static_cast<int>(cfg[11]) == 1 ? 1 : (static_cast<int>(cfg[11]) == 2 ? 2 : 0));
|
|
|
|
|
|
c.velocity_window_s = cfg[12];
|
|
|
|
|
|
c.invert_x = cfg[13] != 0.f;
|
|
|
|
|
|
c.invert_y = cfg[14] != 0.f;
|
|
|
|
|
|
h->input.set_config(c);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Returns 1 when the chain is frozen (both axes at the freeze threshold).
|
|
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
|
|
|
|
int nisps_input_process(void* p, float x, float y, float dt_s, float* out_xy) {
|
|
|
|
|
|
if (!p || !out_xy) return 0;
|
|
|
|
|
|
auto* h = static_cast<PipelineHandle*>(p);
|
|
|
|
|
|
const auto r = h->input.process(x, y, dt_s);
|
|
|
|
|
|
out_xy[0] = r.x;
|
|
|
|
|
|
out_xy[1] = r.y;
|
|
|
|
|
|
return r.frozen ? 1 : 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
|
|
|
|
void nisps_input_reset(void* p) {
|
|
|
|
|
|
if (!p) return;
|
|
|
|
|
|
static_cast<PipelineHandle*>(p)->input.reset();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
|
|
|
|
void nisps_output_set_config(void* p, float global_curve, float smoothing,
|
|
|
|
|
|
float slew_rate, int freeze) {
|
|
|
|
|
|
if (!p) return;
|
|
|
|
|
|
auto* h = static_cast<PipelineHandle*>(p);
|
|
|
|
|
|
nisps::pipeline::OutputChainConfig c;
|
|
|
|
|
|
c.global_curve = global_curve;
|
|
|
|
|
|
c.smoothing = smoothing;
|
|
|
|
|
|
c.slew_rate = slew_rate; // <= 0 ⇒ unlimited (the TS Infinity default)
|
|
|
|
|
|
c.freeze_output = freeze != 0;
|
|
|
|
|
|
h->output.set_config(c);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
|
|
|
|
void nisps_output_set_freeze_mask(void* p, const uint8_t* mask, int n) {
|
|
|
|
|
|
if (!p) return;
|
|
|
|
|
|
auto* h = static_cast<PipelineHandle*>(p);
|
|
|
|
|
|
if (!mask || n <= 0) {
|
|
|
|
|
|
h->output.clear_freeze_mask();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
h->output.set_freeze_mask(
|
|
|
|
|
|
std::span<const std::uint8_t>(mask, static_cast<std::size_t>(n)));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// In-place: processes the first n floats of `inout`.
|
|
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
|
|
|
|
void nisps_output_process(void* p, float* inout, int n, float dt_s) {
|
|
|
|
|
|
if (!p || !inout || n <= 0) return;
|
|
|
|
|
|
auto* h = static_cast<PipelineHandle*>(p);
|
|
|
|
|
|
const std::size_t count = static_cast<std::size_t>(n);
|
|
|
|
|
|
h->output.process(std::span<const float>(inout, count),
|
|
|
|
|
|
std::span<float>(inout, count), dt_s);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
|
|
|
|
void nisps_output_reset(void* p) {
|
|
|
|
|
|
if (!p) return;
|
|
|
|
|
|
static_cast<PipelineHandle*>(p)->output.reset();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// Curve catalog (one-core-engine P4): nisps/core/math.hpp is the single
|
|
|
|
|
|
// source of truth; the browser samples it instead of mirroring the maths.
|
|
|
|
|
|
// ids 0..6 = nisps::Curve (param ignored); id 7 = centred power (param =
|
|
|
|
|
|
// exponent). UI curve previews render by sampling the batch call.
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
|
|
|
|
float nisps_curve_apply(int id, float x, float param) {
|
|
|
|
|
|
if (id == 7) return nisps::centered_power(x, param);
|
|
|
|
|
|
if (id < 0 || id > 6) return x;
|
|
|
|
|
|
return nisps::apply_curve(static_cast<nisps::Curve>(id), nisps::clamp01(x));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
|
|
|
|
void nisps_curve_apply_batch(int id, const float* xs, float* out, int n, float param) {
|
|
|
|
|
|
if (!xs || !out || n <= 0) return;
|
|
|
|
|
|
for (int i = 0; i < n; ++i) {
|
|
|
|
|
|
out[i] = nisps_curve_apply(id, xs[i], param);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat(playground/wasm): WASM bridge between C++ core and SolidJS playground (meml-tgm)
Stream 7 wires nisps/ml + nisps/engines into the playground via Emscripten.
Highlights:
- nisps/wasm/bindings.cpp: flat C API per architecture.md §6.2. Fixed-arch
MLP<2, 10, 14, 18, 126>; engine string→type dispatch table with NoOp
fallback.
- scripts/build-wasm.sh: emcc invocation, MODULARIZE=1, exports listed
explicitly; produces playground/public/nisps.{js,wasm}.
- playground/src/ml/wasm-iml.ts: main-thread MLP host (sync inference,
sync training, RL ops, weights I/O, layer stats, localStorage).
- playground/src/ml/wasm-worker.ts: disposable Web Worker for off-thread
async training, owns its own WASM instance.
- playground/src/ml/dataset.ts: Float32Array-backed FIFO with sample-weight
modes (uniform/global/local/combined). Port of legacy dataset.js.
- playground/src/audio/engine-host.ts: AudioContext + AudioWorkletNode
lifecycle, with start/stop/setEngine/setParams.
- playground/src/audio/worklet/nisps-processor.ts: WASM-loading
AudioWorkletProcessor that runs engine.process_block per 128-sample
block. Loads its own WASM instance from main-thread-supplied bytes
(no fetch in worklet).
- playground/src/stores/ml-store.ts: wired stub methods to WasmIML
singleton; lazy initialize().
- playground/src/debug/probe.ts: window.__nisps now calls real WasmIML
via the store; lazy-init on first use.
Verified:
- bash scripts/build-wasm.sh succeeds (94 KB nisps.wasm)
- bun run typecheck OK
- bun run build OK (production bundle)
- vite dev server serves /nisps.{js,wasm} with COOP/COEP
Known limitation: WASM is fixed at one MLP shape. Multi-arch deferred —
documented in nisps/wasm/README.md.
2026-04-29 15:36:29 +02:00
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// Engine lifecycle
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
|
|
|
|
void* nisps_engine_create(const char* engine_id, float sample_rate) {
|
|
|
|
|
|
if (!engine_id) return nullptr;
|
|
|
|
|
|
auto* h = new EngineHandle(dispatch_create(engine_id, sample_rate));
|
|
|
|
|
|
return static_cast<void*>(h);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
|
|
|
|
void nisps_engine_destroy(void* engine) {
|
|
|
|
|
|
if (!engine) return;
|
|
|
|
|
|
auto* h = static_cast<EngineHandle*>(engine);
|
|
|
|
|
|
dispatch_destroy(*h);
|
|
|
|
|
|
delete h;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
|
|
|
|
void nisps_engine_set_params(void* engine, const float* params, int n_params) {
|
|
|
|
|
|
if (!engine || !params || n_params <= 0) return;
|
|
|
|
|
|
auto* h = static_cast<EngineHandle*>(engine);
|
|
|
|
|
|
dispatch_set_params(*h, std::span<const float>(params, static_cast<std::size_t>(n_params)));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
EMSCRIPTEN_KEEPALIVE
|
|
|
|
|
|
void nisps_engine_process_block(void* engine,
|
|
|
|
|
|
const float* in_l, const float* in_r,
|
|
|
|
|
|
float* out_l, float* out_r,
|
|
|
|
|
|
int n_samples) {
|
|
|
|
|
|
if (!engine || n_samples <= 0) return;
|
|
|
|
|
|
auto* h = static_cast<EngineHandle*>(engine);
|
|
|
|
|
|
dispatch_process_block(*h, in_l, in_r, out_l, out_r, n_samples);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
} // extern "C"
|