From b6819fd26ffbc4e80caaeede9bf3c493b1c6fa53 Mon Sep 17 00:00:00 2001 From: monkey-w1n5t0n Date: Tue, 14 Jul 2026 03:38:06 +0200 Subject: [PATCH] =?UTF-8?q?feat(wasm)!:=20P2.2=20=E2=80=94=20nisps=5Fml=5F?= =?UTF-8?q?create=20honours=20dims;=20runtime-shaped=20browser=20MLP=20+?= =?UTF-8?q?=20reshape?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Operator-approved ABI change (P2 stop-point). The WASM MLP is now MLPCore: - 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; 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). --- docs/specs/plans/one-core-engine-refactor.md | 10 +- manifold/public/nisps.js | 2 +- manifold/public/nisps.wasm | Bin 103104 -> 111206 bytes manifold/src/engine/types.ts | 8 +- manifold/src/engine/wasm-iml.ts | 33 +-- manifold/src/engine/wasm-worker.ts | 5 + nisps/ml/dynamic_storage.hpp | 85 ++++++ nisps/ml/feedback.hpp | 296 ++++++++++++------- nisps/ml/warm_start.hpp | 65 ++++ nisps/wasm/bindings.cpp | 274 +++++++++++------ scripts/build-wasm.sh | 2 +- tests/cpp/parity_wasm.mjs | 7 +- tests/cpp/test_mlp_storage_parity.cpp | 71 +++++ 13 files changed, 640 insertions(+), 218 deletions(-) create mode 100644 nisps/ml/warm_start.hpp diff --git a/docs/specs/plans/one-core-engine-refactor.md b/docs/specs/plans/one-core-engine-refactor.md index 0db5678..88cd167 100644 --- a/docs/specs/plans/one-core-engine-refactor.md +++ b/docs/specs/plans/one-core-engine-refactor.md @@ -110,8 +110,14 @@ Each phase ends green on its test gate and is independently landable. File phase - `DynamicStorage` (`nisps/ml/dynamic_storage.hpp`) — sizes at construction, single arena allocation, nothing per-call. `#error`s under `NISPS_TARGET_EMBEDDED`; sole `lint-cpp.sh` heap-allowlist entry, with a lint check that fails if the guard is ever removed. -- `nisps_ml_create(input, output, hidden[])` honours its arguments. Reshape = new instance + warm-start - copy of overlapping weights (the BUILD-PLAN warm-start idea, now runtime). +- ✅ (landed 2026-07-14, operator-approved) `nisps_ml_create(input, output, hidden[])` honours its + arguments (non-positive/null → the historical 32→[10,14,18]→126 defaults, keeping pre-P2 callers + bit-identical). `nisps_ml_reshape` = new instance + warm-start copy of overlapping weights + (`nisps/ml/warm_start.hpp`); feedback controller re-created (state resets — front-end modal). + `FeedbackController` got the same storage split (`FeedbackControllerCore`, fixed alias for + firmware/tests, `DynamicFeedbackStorage` for the browser). `nisps_ml_describe` now takes the handle + (null → default shape). Verified: reshape ABI smoke (dims honoured, overlap survives, invalid dims + rejected), warm-start ctest (grow+shrink), parity PASS unchanged, firmware `.text` unchanged. - Manifold drops input clamping/phantom-channel handling; XIASRI/sound-analysis multi-input modes become browser-viable. - **Gate:** parity — fixed and dynamic storage produce bit-identical outputs for identical shapes/seeds diff --git a/manifold/public/nisps.js b/manifold/public/nisps.js index 9c76fe9..7188327 100644 --- a/manifold/public/nisps.js +++ b/manifold/public/nisps.js @@ -6,7 +6,7 @@ var createNispsModule = (() => { function(moduleArg = {}) { var moduleRtn; -var Module=moduleArg;var readyPromiseResolve,readyPromiseReject;var readyPromise=new Promise((resolve,reject)=>{readyPromiseResolve=resolve;readyPromiseReject=reject});var ENVIRONMENT_IS_WEB=typeof window=="object";var ENVIRONMENT_IS_WORKER=typeof importScripts=="function";var ENVIRONMENT_IS_NODE=typeof process=="object"&&typeof process.versions=="object"&&typeof process.versions.node=="string"&&process.type!="renderer";var moduleOverrides=Object.assign({},Module);var arguments_=[];var thisProgram="./this.program";var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){if(ENVIRONMENT_IS_WORKER){scriptDirectory=self.location.href}else if(typeof document!="undefined"&&document.currentScript){scriptDirectory=document.currentScript.src}if(_scriptName){scriptDirectory=_scriptName}if(scriptDirectory.startsWith("blob:")){scriptDirectory=""}else{scriptDirectory=scriptDirectory.substr(0,scriptDirectory.replace(/[?#].*/,"").lastIndexOf("/")+1)}{if(ENVIRONMENT_IS_WORKER){readBinary=url=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.responseType="arraybuffer";xhr.send(null);return new Uint8Array(xhr.response)}}readAsync=url=>fetch(url,{credentials:"same-origin"}).then(response=>{if(response.ok){return response.arrayBuffer()}return Promise.reject(new Error(response.status+" : "+response.url))})}}else{}var out=Module["print"]||console.log.bind(console);var err=Module["printErr"]||console.error.bind(console);Object.assign(Module,moduleOverrides);moduleOverrides=null;if(Module["arguments"])arguments_=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];var wasmBinary=Module["wasmBinary"];var wasmMemory;var ABORT=false;var HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAPF64;function updateMemoryViews(){var b=wasmMemory.buffer;Module["HEAP8"]=HEAP8=new Int8Array(b);Module["HEAP16"]=HEAP16=new Int16Array(b);Module["HEAPU8"]=HEAPU8=new Uint8Array(b);Module["HEAPU16"]=HEAPU16=new Uint16Array(b);Module["HEAP32"]=HEAP32=new Int32Array(b);Module["HEAPU32"]=HEAPU32=new Uint32Array(b);Module["HEAPF32"]=HEAPF32=new Float32Array(b);Module["HEAPF64"]=HEAPF64=new Float64Array(b)}var __ATPRERUN__=[];var __ATINIT__=[];var __ATPOSTRUN__=[];var runtimeInitialized=false;function preRun(){var preRuns=Module["preRun"];if(preRuns){if(typeof preRuns=="function")preRuns=[preRuns];preRuns.forEach(addOnPreRun)}callRuntimeCallbacks(__ATPRERUN__)}function initRuntime(){runtimeInitialized=true;callRuntimeCallbacks(__ATINIT__)}function postRun(){var postRuns=Module["postRun"];if(postRuns){if(typeof postRuns=="function")postRuns=[postRuns];postRuns.forEach(addOnPostRun)}callRuntimeCallbacks(__ATPOSTRUN__)}function addOnPreRun(cb){__ATPRERUN__.unshift(cb)}function addOnInit(cb){__ATINIT__.unshift(cb)}function addOnPostRun(cb){__ATPOSTRUN__.unshift(cb)}var runDependencies=0;var runDependencyWatcher=null;var dependenciesFulfilled=null;function addRunDependency(id){runDependencies++;Module["monitorRunDependencies"]?.(runDependencies)}function removeRunDependency(id){runDependencies--;Module["monitorRunDependencies"]?.(runDependencies);if(runDependencies==0){if(runDependencyWatcher!==null){clearInterval(runDependencyWatcher);runDependencyWatcher=null}if(dependenciesFulfilled){var callback=dependenciesFulfilled;dependenciesFulfilled=null;callback()}}}function abort(what){Module["onAbort"]?.(what);what="Aborted("+what+")";err(what);ABORT=true;what+=". Build with -sASSERTIONS for more info.";var e=new WebAssembly.RuntimeError(what);readyPromiseReject(e);throw e}var dataURIPrefix="data:application/octet-stream;base64,";var isDataURI=filename=>filename.startsWith(dataURIPrefix);function findWasmBinary(){var f="nisps.wasm";if(!isDataURI(f)){return locateFile(f)}return f}var wasmBinaryFile;function getBinarySync(file){if(file==wasmBinaryFile&&wasmBinary){return new Uint8Array(wasmBinary)}if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}function getBinaryPromise(binaryFile){if(!wasmBinary){return readAsync(binaryFile).then(response=>new Uint8Array(response),()=>getBinarySync(binaryFile))}return Promise.resolve().then(()=>getBinarySync(binaryFile))}function instantiateArrayBuffer(binaryFile,imports,receiver){return getBinaryPromise(binaryFile).then(binary=>WebAssembly.instantiate(binary,imports)).then(receiver,reason=>{err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)})}function instantiateAsync(binary,binaryFile,imports,callback){if(!binary&&typeof WebAssembly.instantiateStreaming=="function"&&!isDataURI(binaryFile)&&typeof fetch=="function"){return fetch(binaryFile,{credentials:"same-origin"}).then(response=>{var result=WebAssembly.instantiateStreaming(response,imports);return result.then(callback,function(reason){err(`wasm streaming compile failed: ${reason}`);err("falling back to ArrayBuffer instantiation");return instantiateArrayBuffer(binaryFile,imports,callback)})})}return instantiateArrayBuffer(binaryFile,imports,callback)}function getWasmImports(){return{a:wasmImports}}function createWasm(){var info=getWasmImports();function receiveInstance(instance,module){wasmExports=instance.exports;wasmMemory=wasmExports["d"];updateMemoryViews();addOnInit(wasmExports["e"]);removeRunDependency("wasm-instantiate");return wasmExports}addRunDependency("wasm-instantiate");function receiveInstantiationResult(result){receiveInstance(result["instance"])}if(Module["instantiateWasm"]){try{return Module["instantiateWasm"](info,receiveInstance)}catch(e){err(`Module.instantiateWasm callback failed with error: ${e}`);readyPromiseReject(e)}}wasmBinaryFile??=findWasmBinary();instantiateAsync(wasmBinary,wasmBinaryFile,info,receiveInstantiationResult).catch(readyPromiseReject);return{}}var callRuntimeCallbacks=callbacks=>{callbacks.forEach(f=>f(Module))};var noExitRuntime=Module["noExitRuntime"]||true;var stackRestore=val=>__emscripten_stack_restore(val);var stackSave=()=>_emscripten_stack_get_current();var __abort_js=()=>{abort("")};var __emscripten_memcpy_js=(dest,src,num)=>HEAPU8.copyWithin(dest,src,src+num);var getHeapMax=()=>2147483648;var alignMemory=(size,alignment)=>Math.ceil(size/alignment)*alignment;var growMemory=size=>{var b=wasmMemory.buffer;var pages=(size-b.byteLength+65535)/65536|0;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){}};var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignMemory(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}return false};var getCFunc=ident=>{var func=Module["_"+ident];return func};var writeArrayToMemory=(array,buffer)=>{HEAP8.set(array,buffer)};var lengthBytesUTF8=str=>{var len=0;for(var i=0;i=55296&&c<=57343){len+=4;++i}else{len+=3}}return len};var stringToUTF8Array=(str,heap,outIdx,maxBytesToWrite)=>{if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i=55296&&u<=57343){var u1=str.charCodeAt(++i);u=65536+((u&1023)<<10)|u1&1023}if(u<=127){if(outIdx>=endIdx)break;heap[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++]=192|u>>6;heap[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++]=224|u>>12;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;heap[outIdx++]=240|u>>18;heap[outIdx++]=128|u>>12&63;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}}heap[outIdx]=0;return outIdx-startIdx};var stringToUTF8=(str,outPtr,maxBytesToWrite)=>stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite);var stackAlloc=sz=>__emscripten_stack_alloc(sz);var stringToUTF8OnStack=str=>{var size=lengthBytesUTF8(str)+1;var ret=stackAlloc(size);stringToUTF8(str,ret,size);return ret};var UTF8Decoder=typeof TextDecoder!="undefined"?new TextDecoder:undefined;var UTF8ArrayToString=(heapOrArray,idx=0,maxBytesToRead=NaN)=>{var endIdx=idx+maxBytesToRead;var endPtr=idx;while(heapOrArray[endPtr]&&!(endPtr>=endIdx))++endPtr;if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.subarray(idx,endPtr))}var str="";while(idx>10,56320|ch&1023)}}return str};var UTF8ToString=(ptr,maxBytesToRead)=>ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead):"";var ccall=(ident,returnType,argTypes,args,opts)=>{var toC={string:str=>{var ret=0;if(str!==null&&str!==undefined&&str!==0){ret=stringToUTF8OnStack(str)}return ret},array:arr=>{var ret=stackAlloc(arr.length);writeArrayToMemory(arr,ret);return ret}};function convertReturnValue(ret){if(returnType==="string"){return UTF8ToString(ret)}if(returnType==="boolean")return Boolean(ret);return ret}var func=getCFunc(ident);var cArgs=[];var stack=0;if(args){for(var i=0;i{var numericArgs=!argTypes||argTypes.every(type=>type==="number"||type==="boolean");var numericRet=returnType!=="string";if(numericRet&&numericArgs&&!opts){return getCFunc(ident)}return(...args)=>ccall(ident,returnType,argTypes,args,opts)};var wasmImports={a:__abort_js,c:__emscripten_memcpy_js,b:_emscripten_resize_heap};var wasmExports=createWasm();var ___wasm_call_ctors=()=>(___wasm_call_ctors=wasmExports["e"])();var _nisps_ml_create=Module["_nisps_ml_create"]=(a0,a1,a2,a3,a4)=>(_nisps_ml_create=Module["_nisps_ml_create"]=wasmExports["f"])(a0,a1,a2,a3,a4);var _nisps_ml_destroy=Module["_nisps_ml_destroy"]=a0=>(_nisps_ml_destroy=Module["_nisps_ml_destroy"]=wasmExports["g"])(a0);var _nisps_ml_set_input=Module["_nisps_ml_set_input"]=(a0,a1,a2)=>(_nisps_ml_set_input=Module["_nisps_ml_set_input"]=wasmExports["h"])(a0,a1,a2);var _nisps_ml_process=Module["_nisps_ml_process"]=a0=>(_nisps_ml_process=Module["_nisps_ml_process"]=wasmExports["i"])(a0);var _nisps_ml_outputs=Module["_nisps_ml_outputs"]=a0=>(_nisps_ml_outputs=Module["_nisps_ml_outputs"]=wasmExports["j"])(a0);var _nisps_ml_infer_batch=Module["_nisps_ml_infer_batch"]=(a0,a1,a2,a3)=>(_nisps_ml_infer_batch=Module["_nisps_ml_infer_batch"]=wasmExports["k"])(a0,a1,a2,a3);var _nisps_ml_add_example=Module["_nisps_ml_add_example"]=(a0,a1,a2)=>(_nisps_ml_add_example=Module["_nisps_ml_add_example"]=wasmExports["l"])(a0,a1,a2);var _nisps_ml_train=Module["_nisps_ml_train"]=(a0,a1,a2,a3,a4)=>(_nisps_ml_train=Module["_nisps_ml_train"]=wasmExports["m"])(a0,a1,a2,a3,a4);var _nisps_ml_eval_loss=Module["_nisps_ml_eval_loss"]=a0=>(_nisps_ml_eval_loss=Module["_nisps_ml_eval_loss"]=wasmExports["n"])(a0);var _nisps_ml_weight_count=Module["_nisps_ml_weight_count"]=a0=>(_nisps_ml_weight_count=Module["_nisps_ml_weight_count"]=wasmExports["o"])(a0);var _nisps_ml_get_weights=Module["_nisps_ml_get_weights"]=(a0,a1)=>(_nisps_ml_get_weights=Module["_nisps_ml_get_weights"]=wasmExports["p"])(a0,a1);var _nisps_ml_set_weights=Module["_nisps_ml_set_weights"]=(a0,a1)=>(_nisps_ml_set_weights=Module["_nisps_ml_set_weights"]=wasmExports["q"])(a0,a1);var _nisps_ml_draw_weights=Module["_nisps_ml_draw_weights"]=(a0,a1)=>(_nisps_ml_draw_weights=Module["_nisps_ml_draw_weights"]=wasmExports["r"])(a0,a1);var _nisps_ml_move_weights=Module["_nisps_ml_move_weights"]=(a0,a1,a2,a3)=>(_nisps_ml_move_weights=Module["_nisps_ml_move_weights"]=wasmExports["s"])(a0,a1,a2,a3);var _nisps_ml_feedback_set_mode=Module["_nisps_ml_feedback_set_mode"]=(a0,a1)=>(_nisps_ml_feedback_set_mode=Module["_nisps_ml_feedback_set_mode"]=wasmExports["t"])(a0,a1);var _nisps_ml_feedback_get_mode=Module["_nisps_ml_feedback_get_mode"]=a0=>(_nisps_ml_feedback_get_mode=Module["_nisps_ml_feedback_get_mode"]=wasmExports["u"])(a0);var _nisps_ml_feedback_exploring=Module["_nisps_ml_feedback_exploring"]=a0=>(_nisps_ml_feedback_exploring=Module["_nisps_ml_feedback_exploring"]=wasmExports["v"])(a0);var _nisps_ml_feedback_learning_paused=Module["_nisps_ml_feedback_learning_paused"]=a0=>(_nisps_ml_feedback_learning_paused=Module["_nisps_ml_feedback_learning_paused"]=wasmExports["w"])(a0);var _nisps_ml_feedback_set_focus=Module["_nisps_ml_feedback_set_focus"]=(a0,a1,a2)=>(_nisps_ml_feedback_set_focus=Module["_nisps_ml_feedback_set_focus"]=wasmExports["x"])(a0,a1,a2);var _nisps_ml_feedback_down=Module["_nisps_ml_feedback_down"]=(a0,a1,a2,a3,a4)=>(_nisps_ml_feedback_down=Module["_nisps_ml_feedback_down"]=wasmExports["y"])(a0,a1,a2,a3,a4);var _nisps_ml_feedback_up=Module["_nisps_ml_feedback_up"]=a0=>(_nisps_ml_feedback_up=Module["_nisps_ml_feedback_up"]=wasmExports["z"])(a0);var _nisps_ml_feedback_drag=Module["_nisps_ml_feedback_drag"]=a0=>(_nisps_ml_feedback_drag=Module["_nisps_ml_feedback_drag"]=wasmExports["A"])(a0);var _nisps_ml_feedback_static_output=Module["_nisps_ml_feedback_static_output"]=(a0,a1)=>(_nisps_ml_feedback_static_output=Module["_nisps_ml_feedback_static_output"]=wasmExports["B"])(a0,a1);var _nisps_ml_feedback_enter_explore=Module["_nisps_ml_feedback_enter_explore"]=(a0,a1)=>(_nisps_ml_feedback_enter_explore=Module["_nisps_ml_feedback_enter_explore"]=wasmExports["C"])(a0,a1);var _nisps_ml_feedback_exit_explore=Module["_nisps_ml_feedback_exit_explore"]=a0=>(_nisps_ml_feedback_exit_explore=Module["_nisps_ml_feedback_exit_explore"]=wasmExports["D"])(a0);var _nisps_ml_feedback_reroll=Module["_nisps_ml_feedback_reroll"]=(a0,a1)=>(_nisps_ml_feedback_reroll=Module["_nisps_ml_feedback_reroll"]=wasmExports["E"])(a0,a1);var _nisps_ml_feedback_nudge=Module["_nisps_ml_feedback_nudge"]=(a0,a1)=>(_nisps_ml_feedback_nudge=Module["_nisps_ml_feedback_nudge"]=wasmExports["F"])(a0,a1);var _nisps_ml_feedback_undo=Module["_nisps_ml_feedback_undo"]=a0=>(_nisps_ml_feedback_undo=Module["_nisps_ml_feedback_undo"]=wasmExports["G"])(a0);var _nisps_ml_feedback_like=Module["_nisps_ml_feedback_like"]=a0=>(_nisps_ml_feedback_like=Module["_nisps_ml_feedback_like"]=wasmExports["H"])(a0);var _nisps_ml_feedback_commit_place=Module["_nisps_ml_feedback_commit_place"]=a0=>(_nisps_ml_feedback_commit_place=Module["_nisps_ml_feedback_commit_place"]=wasmExports["I"])(a0);var _nisps_ml_feedback_cancel_place=Module["_nisps_ml_feedback_cancel_place"]=a0=>(_nisps_ml_feedback_cancel_place=Module["_nisps_ml_feedback_cancel_place"]=wasmExports["J"])(a0);var _nisps_ml_feedback_placing=Module["_nisps_ml_feedback_placing"]=a0=>(_nisps_ml_feedback_placing=Module["_nisps_ml_feedback_placing"]=wasmExports["K"])(a0);var _nisps_ml_feedback_state=Module["_nisps_ml_feedback_state"]=a0=>(_nisps_ml_feedback_state=Module["_nisps_ml_feedback_state"]=wasmExports["L"])(a0);var _nisps_ml_feedback_undo_depth=Module["_nisps_ml_feedback_undo_depth"]=a0=>(_nisps_ml_feedback_undo_depth=Module["_nisps_ml_feedback_undo_depth"]=wasmExports["M"])(a0);var _nisps_ml_feedback_placed_output=Module["_nisps_ml_feedback_placed_output"]=(a0,a1)=>(_nisps_ml_feedback_placed_output=Module["_nisps_ml_feedback_placed_output"]=wasmExports["N"])(a0,a1);var _nisps_ml_get_layer_stats=Module["_nisps_ml_get_layer_stats"]=(a0,a1)=>(_nisps_ml_get_layer_stats=Module["_nisps_ml_get_layer_stats"]=wasmExports["O"])(a0,a1);var _nisps_ml_example_count=Module["_nisps_ml_example_count"]=a0=>(_nisps_ml_example_count=Module["_nisps_ml_example_count"]=wasmExports["P"])(a0);var _nisps_ml_clear_examples=Module["_nisps_ml_clear_examples"]=a0=>(_nisps_ml_clear_examples=Module["_nisps_ml_clear_examples"]=wasmExports["Q"])(a0);var _nisps_ml_reset=Module["_nisps_ml_reset"]=a0=>(_nisps_ml_reset=Module["_nisps_ml_reset"]=wasmExports["R"])(a0);var _nisps_ml_describe=Module["_nisps_ml_describe"]=a0=>(_nisps_ml_describe=Module["_nisps_ml_describe"]=wasmExports["S"])(a0);var _nisps_engine_create=Module["_nisps_engine_create"]=(a0,a1)=>(_nisps_engine_create=Module["_nisps_engine_create"]=wasmExports["T"])(a0,a1);var _nisps_engine_destroy=Module["_nisps_engine_destroy"]=a0=>(_nisps_engine_destroy=Module["_nisps_engine_destroy"]=wasmExports["U"])(a0);var _nisps_engine_set_params=Module["_nisps_engine_set_params"]=(a0,a1,a2)=>(_nisps_engine_set_params=Module["_nisps_engine_set_params"]=wasmExports["V"])(a0,a1,a2);var _nisps_engine_process_block=Module["_nisps_engine_process_block"]=(a0,a1,a2,a3,a4,a5)=>(_nisps_engine_process_block=Module["_nisps_engine_process_block"]=wasmExports["W"])(a0,a1,a2,a3,a4,a5);var _malloc=Module["_malloc"]=a0=>(_malloc=Module["_malloc"]=wasmExports["Y"])(a0);var _free=Module["_free"]=a0=>(_free=Module["_free"]=wasmExports["Z"])(a0);var __emscripten_stack_restore=a0=>(__emscripten_stack_restore=wasmExports["_"])(a0);var __emscripten_stack_alloc=a0=>(__emscripten_stack_alloc=wasmExports["$"])(a0);var _emscripten_stack_get_current=()=>(_emscripten_stack_get_current=wasmExports["aa"])();Module["ccall"]=ccall;Module["cwrap"]=cwrap;var calledRun;var calledPrerun;dependenciesFulfilled=function runCaller(){if(!calledRun)run();if(!calledRun)dependenciesFulfilled=runCaller};function run(){if(runDependencies>0){return}if(!calledPrerun){calledPrerun=1;preRun();if(runDependencies>0){return}}function doRun(){if(calledRun)return;calledRun=1;Module["calledRun"]=1;if(ABORT)return;initRuntime();readyPromiseResolve(Module);Module["onRuntimeInitialized"]?.();postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout(()=>{setTimeout(()=>Module["setStatus"](""),1);doRun()},1)}else{doRun()}}if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].pop()()}}run();moduleRtn=readyPromise; +var Module=moduleArg;var readyPromiseResolve,readyPromiseReject;var readyPromise=new Promise((resolve,reject)=>{readyPromiseResolve=resolve;readyPromiseReject=reject});var ENVIRONMENT_IS_WEB=typeof window=="object";var ENVIRONMENT_IS_WORKER=typeof importScripts=="function";var ENVIRONMENT_IS_NODE=typeof process=="object"&&typeof process.versions=="object"&&typeof process.versions.node=="string"&&process.type!="renderer";var moduleOverrides=Object.assign({},Module);var arguments_=[];var thisProgram="./this.program";var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){if(ENVIRONMENT_IS_WORKER){scriptDirectory=self.location.href}else if(typeof document!="undefined"&&document.currentScript){scriptDirectory=document.currentScript.src}if(_scriptName){scriptDirectory=_scriptName}if(scriptDirectory.startsWith("blob:")){scriptDirectory=""}else{scriptDirectory=scriptDirectory.substr(0,scriptDirectory.replace(/[?#].*/,"").lastIndexOf("/")+1)}{if(ENVIRONMENT_IS_WORKER){readBinary=url=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.responseType="arraybuffer";xhr.send(null);return new Uint8Array(xhr.response)}}readAsync=url=>fetch(url,{credentials:"same-origin"}).then(response=>{if(response.ok){return response.arrayBuffer()}return Promise.reject(new Error(response.status+" : "+response.url))})}}else{}var out=Module["print"]||console.log.bind(console);var err=Module["printErr"]||console.error.bind(console);Object.assign(Module,moduleOverrides);moduleOverrides=null;if(Module["arguments"])arguments_=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];var wasmBinary=Module["wasmBinary"];var wasmMemory;var ABORT=false;var HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAPF64;function updateMemoryViews(){var b=wasmMemory.buffer;Module["HEAP8"]=HEAP8=new Int8Array(b);Module["HEAP16"]=HEAP16=new Int16Array(b);Module["HEAPU8"]=HEAPU8=new Uint8Array(b);Module["HEAPU16"]=HEAPU16=new Uint16Array(b);Module["HEAP32"]=HEAP32=new Int32Array(b);Module["HEAPU32"]=HEAPU32=new Uint32Array(b);Module["HEAPF32"]=HEAPF32=new Float32Array(b);Module["HEAPF64"]=HEAPF64=new Float64Array(b)}var __ATPRERUN__=[];var __ATINIT__=[];var __ATPOSTRUN__=[];var runtimeInitialized=false;function preRun(){var preRuns=Module["preRun"];if(preRuns){if(typeof preRuns=="function")preRuns=[preRuns];preRuns.forEach(addOnPreRun)}callRuntimeCallbacks(__ATPRERUN__)}function initRuntime(){runtimeInitialized=true;callRuntimeCallbacks(__ATINIT__)}function postRun(){var postRuns=Module["postRun"];if(postRuns){if(typeof postRuns=="function")postRuns=[postRuns];postRuns.forEach(addOnPostRun)}callRuntimeCallbacks(__ATPOSTRUN__)}function addOnPreRun(cb){__ATPRERUN__.unshift(cb)}function addOnInit(cb){__ATINIT__.unshift(cb)}function addOnPostRun(cb){__ATPOSTRUN__.unshift(cb)}var runDependencies=0;var runDependencyWatcher=null;var dependenciesFulfilled=null;function addRunDependency(id){runDependencies++;Module["monitorRunDependencies"]?.(runDependencies)}function removeRunDependency(id){runDependencies--;Module["monitorRunDependencies"]?.(runDependencies);if(runDependencies==0){if(runDependencyWatcher!==null){clearInterval(runDependencyWatcher);runDependencyWatcher=null}if(dependenciesFulfilled){var callback=dependenciesFulfilled;dependenciesFulfilled=null;callback()}}}function abort(what){Module["onAbort"]?.(what);what="Aborted("+what+")";err(what);ABORT=true;what+=". Build with -sASSERTIONS for more info.";var e=new WebAssembly.RuntimeError(what);readyPromiseReject(e);throw e}var dataURIPrefix="data:application/octet-stream;base64,";var isDataURI=filename=>filename.startsWith(dataURIPrefix);function findWasmBinary(){var f="nisps.wasm";if(!isDataURI(f)){return locateFile(f)}return f}var wasmBinaryFile;function getBinarySync(file){if(file==wasmBinaryFile&&wasmBinary){return new Uint8Array(wasmBinary)}if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}function getBinaryPromise(binaryFile){if(!wasmBinary){return readAsync(binaryFile).then(response=>new Uint8Array(response),()=>getBinarySync(binaryFile))}return Promise.resolve().then(()=>getBinarySync(binaryFile))}function instantiateArrayBuffer(binaryFile,imports,receiver){return getBinaryPromise(binaryFile).then(binary=>WebAssembly.instantiate(binary,imports)).then(receiver,reason=>{err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)})}function instantiateAsync(binary,binaryFile,imports,callback){if(!binary&&typeof WebAssembly.instantiateStreaming=="function"&&!isDataURI(binaryFile)&&typeof fetch=="function"){return fetch(binaryFile,{credentials:"same-origin"}).then(response=>{var result=WebAssembly.instantiateStreaming(response,imports);return result.then(callback,function(reason){err(`wasm streaming compile failed: ${reason}`);err("falling back to ArrayBuffer instantiation");return instantiateArrayBuffer(binaryFile,imports,callback)})})}return instantiateArrayBuffer(binaryFile,imports,callback)}function getWasmImports(){return{a:wasmImports}}function createWasm(){var info=getWasmImports();function receiveInstance(instance,module){wasmExports=instance.exports;wasmMemory=wasmExports["d"];updateMemoryViews();addOnInit(wasmExports["e"]);removeRunDependency("wasm-instantiate");return wasmExports}addRunDependency("wasm-instantiate");function receiveInstantiationResult(result){receiveInstance(result["instance"])}if(Module["instantiateWasm"]){try{return Module["instantiateWasm"](info,receiveInstance)}catch(e){err(`Module.instantiateWasm callback failed with error: ${e}`);readyPromiseReject(e)}}wasmBinaryFile??=findWasmBinary();instantiateAsync(wasmBinary,wasmBinaryFile,info,receiveInstantiationResult).catch(readyPromiseReject);return{}}var callRuntimeCallbacks=callbacks=>{callbacks.forEach(f=>f(Module))};var noExitRuntime=Module["noExitRuntime"]||true;var stackRestore=val=>__emscripten_stack_restore(val);var stackSave=()=>_emscripten_stack_get_current();var __abort_js=()=>{abort("")};var __emscripten_memcpy_js=(dest,src,num)=>HEAPU8.copyWithin(dest,src,src+num);var getHeapMax=()=>2147483648;var alignMemory=(size,alignment)=>Math.ceil(size/alignment)*alignment;var growMemory=size=>{var b=wasmMemory.buffer;var pages=(size-b.byteLength+65535)/65536|0;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){}};var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignMemory(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}return false};var getCFunc=ident=>{var func=Module["_"+ident];return func};var writeArrayToMemory=(array,buffer)=>{HEAP8.set(array,buffer)};var lengthBytesUTF8=str=>{var len=0;for(var i=0;i=55296&&c<=57343){len+=4;++i}else{len+=3}}return len};var stringToUTF8Array=(str,heap,outIdx,maxBytesToWrite)=>{if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i=55296&&u<=57343){var u1=str.charCodeAt(++i);u=65536+((u&1023)<<10)|u1&1023}if(u<=127){if(outIdx>=endIdx)break;heap[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++]=192|u>>6;heap[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++]=224|u>>12;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;heap[outIdx++]=240|u>>18;heap[outIdx++]=128|u>>12&63;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}}heap[outIdx]=0;return outIdx-startIdx};var stringToUTF8=(str,outPtr,maxBytesToWrite)=>stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite);var stackAlloc=sz=>__emscripten_stack_alloc(sz);var stringToUTF8OnStack=str=>{var size=lengthBytesUTF8(str)+1;var ret=stackAlloc(size);stringToUTF8(str,ret,size);return ret};var UTF8Decoder=typeof TextDecoder!="undefined"?new TextDecoder:undefined;var UTF8ArrayToString=(heapOrArray,idx=0,maxBytesToRead=NaN)=>{var endIdx=idx+maxBytesToRead;var endPtr=idx;while(heapOrArray[endPtr]&&!(endPtr>=endIdx))++endPtr;if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.subarray(idx,endPtr))}var str="";while(idx>10,56320|ch&1023)}}return str};var UTF8ToString=(ptr,maxBytesToRead)=>ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead):"";var ccall=(ident,returnType,argTypes,args,opts)=>{var toC={string:str=>{var ret=0;if(str!==null&&str!==undefined&&str!==0){ret=stringToUTF8OnStack(str)}return ret},array:arr=>{var ret=stackAlloc(arr.length);writeArrayToMemory(arr,ret);return ret}};function convertReturnValue(ret){if(returnType==="string"){return UTF8ToString(ret)}if(returnType==="boolean")return Boolean(ret);return ret}var func=getCFunc(ident);var cArgs=[];var stack=0;if(args){for(var i=0;i{var numericArgs=!argTypes||argTypes.every(type=>type==="number"||type==="boolean");var numericRet=returnType!=="string";if(numericRet&&numericArgs&&!opts){return getCFunc(ident)}return(...args)=>ccall(ident,returnType,argTypes,args,opts)};var wasmImports={c:__abort_js,b:__emscripten_memcpy_js,a:_emscripten_resize_heap};var wasmExports=createWasm();var ___wasm_call_ctors=()=>(___wasm_call_ctors=wasmExports["e"])();var _nisps_ml_create=Module["_nisps_ml_create"]=(a0,a1,a2,a3,a4)=>(_nisps_ml_create=Module["_nisps_ml_create"]=wasmExports["f"])(a0,a1,a2,a3,a4);var _nisps_ml_reshape=Module["_nisps_ml_reshape"]=(a0,a1,a2,a3,a4,a5)=>(_nisps_ml_reshape=Module["_nisps_ml_reshape"]=wasmExports["g"])(a0,a1,a2,a3,a4,a5);var _nisps_ml_destroy=Module["_nisps_ml_destroy"]=a0=>(_nisps_ml_destroy=Module["_nisps_ml_destroy"]=wasmExports["h"])(a0);var _nisps_ml_set_input=Module["_nisps_ml_set_input"]=(a0,a1,a2)=>(_nisps_ml_set_input=Module["_nisps_ml_set_input"]=wasmExports["i"])(a0,a1,a2);var _nisps_ml_process=Module["_nisps_ml_process"]=a0=>(_nisps_ml_process=Module["_nisps_ml_process"]=wasmExports["j"])(a0);var _nisps_ml_outputs=Module["_nisps_ml_outputs"]=a0=>(_nisps_ml_outputs=Module["_nisps_ml_outputs"]=wasmExports["k"])(a0);var _nisps_ml_infer_batch=Module["_nisps_ml_infer_batch"]=(a0,a1,a2,a3)=>(_nisps_ml_infer_batch=Module["_nisps_ml_infer_batch"]=wasmExports["l"])(a0,a1,a2,a3);var _nisps_ml_add_example=Module["_nisps_ml_add_example"]=(a0,a1,a2)=>(_nisps_ml_add_example=Module["_nisps_ml_add_example"]=wasmExports["m"])(a0,a1,a2);var _nisps_ml_train=Module["_nisps_ml_train"]=(a0,a1,a2,a3,a4)=>(_nisps_ml_train=Module["_nisps_ml_train"]=wasmExports["n"])(a0,a1,a2,a3,a4);var _nisps_ml_eval_loss=Module["_nisps_ml_eval_loss"]=a0=>(_nisps_ml_eval_loss=Module["_nisps_ml_eval_loss"]=wasmExports["o"])(a0);var _nisps_ml_weight_count=Module["_nisps_ml_weight_count"]=a0=>(_nisps_ml_weight_count=Module["_nisps_ml_weight_count"]=wasmExports["p"])(a0);var _nisps_ml_get_weights=Module["_nisps_ml_get_weights"]=(a0,a1)=>(_nisps_ml_get_weights=Module["_nisps_ml_get_weights"]=wasmExports["q"])(a0,a1);var _nisps_ml_set_weights=Module["_nisps_ml_set_weights"]=(a0,a1)=>(_nisps_ml_set_weights=Module["_nisps_ml_set_weights"]=wasmExports["r"])(a0,a1);var _nisps_ml_draw_weights=Module["_nisps_ml_draw_weights"]=(a0,a1)=>(_nisps_ml_draw_weights=Module["_nisps_ml_draw_weights"]=wasmExports["s"])(a0,a1);var _nisps_ml_move_weights=Module["_nisps_ml_move_weights"]=(a0,a1,a2,a3)=>(_nisps_ml_move_weights=Module["_nisps_ml_move_weights"]=wasmExports["t"])(a0,a1,a2,a3);var _nisps_ml_feedback_set_mode=Module["_nisps_ml_feedback_set_mode"]=(a0,a1)=>(_nisps_ml_feedback_set_mode=Module["_nisps_ml_feedback_set_mode"]=wasmExports["u"])(a0,a1);var _nisps_ml_feedback_get_mode=Module["_nisps_ml_feedback_get_mode"]=a0=>(_nisps_ml_feedback_get_mode=Module["_nisps_ml_feedback_get_mode"]=wasmExports["v"])(a0);var _nisps_ml_feedback_exploring=Module["_nisps_ml_feedback_exploring"]=a0=>(_nisps_ml_feedback_exploring=Module["_nisps_ml_feedback_exploring"]=wasmExports["w"])(a0);var _nisps_ml_feedback_learning_paused=Module["_nisps_ml_feedback_learning_paused"]=a0=>(_nisps_ml_feedback_learning_paused=Module["_nisps_ml_feedback_learning_paused"]=wasmExports["x"])(a0);var _nisps_ml_feedback_set_focus=Module["_nisps_ml_feedback_set_focus"]=(a0,a1,a2)=>(_nisps_ml_feedback_set_focus=Module["_nisps_ml_feedback_set_focus"]=wasmExports["y"])(a0,a1,a2);var _nisps_ml_feedback_down=Module["_nisps_ml_feedback_down"]=(a0,a1,a2,a3,a4)=>(_nisps_ml_feedback_down=Module["_nisps_ml_feedback_down"]=wasmExports["z"])(a0,a1,a2,a3,a4);var _nisps_ml_feedback_up=Module["_nisps_ml_feedback_up"]=a0=>(_nisps_ml_feedback_up=Module["_nisps_ml_feedback_up"]=wasmExports["A"])(a0);var _nisps_ml_feedback_drag=Module["_nisps_ml_feedback_drag"]=a0=>(_nisps_ml_feedback_drag=Module["_nisps_ml_feedback_drag"]=wasmExports["B"])(a0);var _nisps_ml_feedback_static_output=Module["_nisps_ml_feedback_static_output"]=(a0,a1)=>(_nisps_ml_feedback_static_output=Module["_nisps_ml_feedback_static_output"]=wasmExports["C"])(a0,a1);var _nisps_ml_feedback_enter_explore=Module["_nisps_ml_feedback_enter_explore"]=(a0,a1)=>(_nisps_ml_feedback_enter_explore=Module["_nisps_ml_feedback_enter_explore"]=wasmExports["D"])(a0,a1);var _nisps_ml_feedback_exit_explore=Module["_nisps_ml_feedback_exit_explore"]=a0=>(_nisps_ml_feedback_exit_explore=Module["_nisps_ml_feedback_exit_explore"]=wasmExports["E"])(a0);var _nisps_ml_feedback_reroll=Module["_nisps_ml_feedback_reroll"]=(a0,a1)=>(_nisps_ml_feedback_reroll=Module["_nisps_ml_feedback_reroll"]=wasmExports["F"])(a0,a1);var _nisps_ml_feedback_nudge=Module["_nisps_ml_feedback_nudge"]=(a0,a1)=>(_nisps_ml_feedback_nudge=Module["_nisps_ml_feedback_nudge"]=wasmExports["G"])(a0,a1);var _nisps_ml_feedback_undo=Module["_nisps_ml_feedback_undo"]=a0=>(_nisps_ml_feedback_undo=Module["_nisps_ml_feedback_undo"]=wasmExports["H"])(a0);var _nisps_ml_feedback_like=Module["_nisps_ml_feedback_like"]=a0=>(_nisps_ml_feedback_like=Module["_nisps_ml_feedback_like"]=wasmExports["I"])(a0);var _nisps_ml_feedback_commit_place=Module["_nisps_ml_feedback_commit_place"]=a0=>(_nisps_ml_feedback_commit_place=Module["_nisps_ml_feedback_commit_place"]=wasmExports["J"])(a0);var _nisps_ml_feedback_cancel_place=Module["_nisps_ml_feedback_cancel_place"]=a0=>(_nisps_ml_feedback_cancel_place=Module["_nisps_ml_feedback_cancel_place"]=wasmExports["K"])(a0);var _nisps_ml_feedback_placing=Module["_nisps_ml_feedback_placing"]=a0=>(_nisps_ml_feedback_placing=Module["_nisps_ml_feedback_placing"]=wasmExports["L"])(a0);var _nisps_ml_feedback_state=Module["_nisps_ml_feedback_state"]=a0=>(_nisps_ml_feedback_state=Module["_nisps_ml_feedback_state"]=wasmExports["M"])(a0);var _nisps_ml_feedback_undo_depth=Module["_nisps_ml_feedback_undo_depth"]=a0=>(_nisps_ml_feedback_undo_depth=Module["_nisps_ml_feedback_undo_depth"]=wasmExports["N"])(a0);var _nisps_ml_feedback_placed_output=Module["_nisps_ml_feedback_placed_output"]=(a0,a1)=>(_nisps_ml_feedback_placed_output=Module["_nisps_ml_feedback_placed_output"]=wasmExports["O"])(a0,a1);var _nisps_ml_get_layer_stats=Module["_nisps_ml_get_layer_stats"]=(a0,a1)=>(_nisps_ml_get_layer_stats=Module["_nisps_ml_get_layer_stats"]=wasmExports["P"])(a0,a1);var _nisps_ml_example_count=Module["_nisps_ml_example_count"]=a0=>(_nisps_ml_example_count=Module["_nisps_ml_example_count"]=wasmExports["Q"])(a0);var _nisps_ml_clear_examples=Module["_nisps_ml_clear_examples"]=a0=>(_nisps_ml_clear_examples=Module["_nisps_ml_clear_examples"]=wasmExports["R"])(a0);var _nisps_ml_reset=Module["_nisps_ml_reset"]=a0=>(_nisps_ml_reset=Module["_nisps_ml_reset"]=wasmExports["S"])(a0);var _nisps_ml_describe=Module["_nisps_ml_describe"]=(a0,a1)=>(_nisps_ml_describe=Module["_nisps_ml_describe"]=wasmExports["T"])(a0,a1);var _nisps_engine_create=Module["_nisps_engine_create"]=(a0,a1)=>(_nisps_engine_create=Module["_nisps_engine_create"]=wasmExports["U"])(a0,a1);var _nisps_engine_destroy=Module["_nisps_engine_destroy"]=a0=>(_nisps_engine_destroy=Module["_nisps_engine_destroy"]=wasmExports["V"])(a0);var _nisps_engine_set_params=Module["_nisps_engine_set_params"]=(a0,a1,a2)=>(_nisps_engine_set_params=Module["_nisps_engine_set_params"]=wasmExports["W"])(a0,a1,a2);var _nisps_engine_process_block=Module["_nisps_engine_process_block"]=(a0,a1,a2,a3,a4,a5)=>(_nisps_engine_process_block=Module["_nisps_engine_process_block"]=wasmExports["X"])(a0,a1,a2,a3,a4,a5);var _malloc=Module["_malloc"]=a0=>(_malloc=Module["_malloc"]=wasmExports["Z"])(a0);var _free=Module["_free"]=a0=>(_free=Module["_free"]=wasmExports["_"])(a0);var __emscripten_stack_restore=a0=>(__emscripten_stack_restore=wasmExports["$"])(a0);var __emscripten_stack_alloc=a0=>(__emscripten_stack_alloc=wasmExports["aa"])(a0);var _emscripten_stack_get_current=()=>(_emscripten_stack_get_current=wasmExports["ba"])();Module["ccall"]=ccall;Module["cwrap"]=cwrap;var calledRun;var calledPrerun;dependenciesFulfilled=function runCaller(){if(!calledRun)run();if(!calledRun)dependenciesFulfilled=runCaller};function run(){if(runDependencies>0){return}if(!calledPrerun){calledPrerun=1;preRun();if(runDependencies>0){return}}function doRun(){if(calledRun)return;calledRun=1;Module["calledRun"]=1;if(ABORT)return;initRuntime();readyPromiseResolve(Module);Module["onRuntimeInitialized"]?.();postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout(()=>{setTimeout(()=>Module["setStatus"](""),1);doRun()},1)}else{doRun()}}if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].pop()()}}run();moduleRtn=readyPromise; return moduleRtn; diff --git a/manifold/public/nisps.wasm b/manifold/public/nisps.wasm index 10a4b0cf0fffc784110ce5629477fa7a1d63e5af..a9a19e427ff3a877e534299397eb534222c849b9 100755 GIT binary patch delta 37282 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%0ATni{>*lzPA(Yv)Lj@ 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 delta 29437 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 diff --git a/manifold/src/engine/types.ts b/manifold/src/engine/types.ts index 29e9d83..6055ced 100644 --- a/manifold/src/engine/types.ts +++ b/manifold/src/engine/types.ts @@ -28,9 +28,14 @@ export interface NispsModule { _free(ptr: number): void; // ML lifecycle. Seed is uint32_t (not 64-bit) — see bindings.cpp file comment. + // Since one-core-engine P2 the dims are HONOURED (runtime-shaped MLP); + // non-positive/null args fall back to the compiled defaults (32→[10,14,18]→126). _nisps_ml_create(input_size: number, output_size: number, hidden_ptr: number, n_hidden: number, seed: number): number; _nisps_ml_destroy(ml: number): void; _nisps_ml_reset(ml: number): void; + // Reshape = new net at the new dims, warm-started with the overlapping + // weights; feedback state resets. Returns 1 on success (0 = no change). + _nisps_ml_reshape(ml: number, input_size: number, output_size: number, hidden_ptr: number, n_hidden: number, spread: number): number; // ML inference. _nisps_ml_set_input(ml: number, idx: number, v: number): void; @@ -54,7 +59,8 @@ export interface NispsModule { _nisps_ml_draw_weights(ml: number, spread: number): void; _nisps_ml_move_weights(ml: number, speed: number, spread: number, mask_ptr: number): void; _nisps_ml_get_layer_stats(ml: number, out_ptr: number): void; - _nisps_ml_describe(out_ptr: number): void; + // Null ml reports the DEFAULT shape; a handle reports its runtime shape. + _nisps_ml_describe(ml: number, out_ptr: number): void; // ML feedback — the "Down Action" state machine (Avoid / RandomiseOutputs / // RandomiseMlp). Mode ints: 0=Avoid 1=RandomiseOutputs 2=RandomiseMlp. diff --git a/manifold/src/engine/wasm-iml.ts b/manifold/src/engine/wasm-iml.ts index 612add7..9e20755 100644 --- a/manifold/src/engine/wasm-iml.ts +++ b/manifold/src/engine/wasm-iml.ts @@ -164,8 +164,20 @@ export class WasmIML { locateFile: (path: string) => (path.endsWith('.wasm') ? assetUrl('nisps.wasm') : path), }); + // Default shape (null handle). Since one-core-engine P2 the MLP is + // runtime-shaped: create() honours requested dims; we pass the caller's + // sizes (falling back to the defaults) and re-describe the instance. this.describePtr = this.module._malloc(6 * 4); - this.module._nisps_ml_describe(this.describePtr); + this.module._nisps_ml_describe(0, this.describePtr); + const defaults = new Int32Array(this.module.HEAP32.buffer, this.describePtr, 6); + const wantedIn = opts.inputSize ?? defaults[0]; + const wantedOut = opts.outputSize ?? defaults[4]; + + const seed = (opts.seed ?? (Date.now() >>> 0)) >>> 0; + this.mlHandle = this.module._nisps_ml_create(wantedIn, wantedOut, 0, 0, seed); + if (!this.mlHandle) throw new Error('[wasm-iml] nisps_ml_create returned null'); + + this.module._nisps_ml_describe(this.mlHandle, this.describePtr); const dims = new Int32Array(this.module.HEAP32.buffer, this.describePtr, 6); this.arch_ = { inputSize: dims[0], @@ -174,25 +186,6 @@ export class WasmIML { numLayers: dims[5], }; - const wantedIn = opts.inputSize ?? this.arch_.inputSize; - const wantedOut = opts.outputSize ?? this.arch_.outputSize; - if (wantedIn !== this.arch_.inputSize || wantedOut !== this.arch_.outputSize) { - console.warn( - `[wasm-iml] requested ${wantedIn}->${wantedOut} but WASM build is fixed at ` + - `${this.arch_.inputSize}->${this.arch_.outputSize}; extras are ignored.`, - ); - } - - const seed = (opts.seed ?? (Date.now() >>> 0)) >>> 0; - this.mlHandle = this.module._nisps_ml_create( - this.arch_.inputSize, - this.arch_.outputSize, - 0, - 0, - seed, - ); - if (!this.mlHandle) throw new Error('[wasm-iml] nisps_ml_create returned null'); - this.weightCount_ = this.module._nisps_ml_weight_count(this.mlHandle); this.featuresBuf = new HeapBuffer(this.module, this.arch_.inputSize); diff --git a/manifold/src/engine/wasm-worker.ts b/manifold/src/engine/wasm-worker.ts index f2ca35f..b8ee549 100644 --- a/manifold/src/engine/wasm-worker.ts +++ b/manifold/src/engine/wasm-worker.ts @@ -194,6 +194,11 @@ if (isWorker) { mod = await factory({ locateFile: (path: string) => (path.endsWith('.wasm') ? assetUrl('nisps.wasm') : path), }); + // Default shape (0,0 → 32→[10,14,18]→126). The worker's net MUST match + // the main thread's shape — weights are exchanged as flat vectors. When + // the main thread creates/reshapes with non-default dims (one-core-engine + // P2.3+), the init/train messages must carry those dims and this call + // must pass them through. mlHandle = mod._nisps_ml_create(0, 0, 0, 0, seed >>> 0); weightCount = mod._nisps_ml_weight_count(mlHandle); } diff --git a/nisps/ml/dynamic_storage.hpp b/nisps/ml/dynamic_storage.hpp index 451f274..9d92e3e 100644 --- a/nisps/ml/dynamic_storage.hpp +++ b/nisps/ml/dynamic_storage.hpp @@ -23,6 +23,7 @@ #endif #include +#include #include #include @@ -191,4 +192,88 @@ class DynamicStorage { std::size_t total_ = 0u; }; +// --------------------------------------------------------------------------- +// Runtime-sized feedback-controller storage (see nisps/ml/feedback.hpp for +// the surface contract). One arena allocation at construction; nothing +// per-call. The focus mask lives in a byte region carved from the same +// arena (aliased through the float arena's tail, kept byte-aligned by +// allocating whole floats for it). +// --------------------------------------------------------------------------- +class DynamicFeedbackStorage { + public: + DynamicFeedbackStorage(std::size_t n_out, + std::size_t n_weights, + std::size_t undo_depth = 4u) noexcept + : n_out_(n_out), n_weights_(n_weights), undo_cap_(undo_depth) { + if (n_out == 0u || n_weights == 0u || undo_depth == 0u) return; + // float regions: static_out, placed_out, snapshot, scratch, undo ring + // byte region: focus mask (n_out bytes, rounded up to whole floats) + const std::size_t focus_floats = (n_out + sizeof(float) - 1u) / sizeof(float); + const std::size_t total = n_out * 2u // static_out + placed_out + + n_weights * 2u // snapshot + scratch + + n_weights * undo_depth // undo ring + + focus_floats; + arena_ = new (std::nothrow) float[total](); + if (!arena_) return; + off_placed_ = n_out_; + off_snap_ = off_placed_ + n_out_; + off_scratch_ = off_snap_ + n_weights_; + off_undo_ = off_scratch_ + n_weights_; + off_focus_ = off_undo_ + n_weights_ * undo_cap_; + } + + ~DynamicFeedbackStorage() { delete[] arena_; } + + DynamicFeedbackStorage(const DynamicFeedbackStorage&) = delete; + DynamicFeedbackStorage& operator=(const DynamicFeedbackStorage&) = delete; + DynamicFeedbackStorage(DynamicFeedbackStorage&& o) noexcept { move_from_(o); } + DynamicFeedbackStorage& operator=(DynamicFeedbackStorage&& o) noexcept { + if (this != &o) { + delete[] arena_; + move_from_(o); + } + return *this; + } + + bool valid() const noexcept { return arena_ != nullptr; } + + std::size_t n_out() const noexcept { return n_out_; } + std::size_t n_weights() const noexcept { return n_weights_; } + std::size_t undo_cap() const noexcept { return undo_cap_; } + + std::span static_out() noexcept { return {arena_, n_out_}; } + std::span static_out() const noexcept { return {arena_, n_out_}; } + std::span placed_out() noexcept { return {arena_ + off_placed_, n_out_}; } + std::span placed_out() const noexcept { return {arena_ + off_placed_, n_out_}; } + std::span snapshot() noexcept { return {arena_ + off_snap_, n_weights_}; } + std::span snapshot() const noexcept { return {arena_ + off_snap_, n_weights_}; } + std::span scratch_buf() noexcept { return {arena_ + off_scratch_, n_weights_}; } + std::span undo_slot(std::size_t i) noexcept { + return {arena_ + off_undo_ + i * n_weights_, n_weights_}; + } + std::span undo_slot(std::size_t i) const noexcept { + return {arena_ + off_undo_ + i * n_weights_, n_weights_}; + } + std::span focus() noexcept { + return {reinterpret_cast(arena_ + off_focus_), n_out_}; + } + std::span focus() const noexcept { + return {reinterpret_cast(arena_ + off_focus_), n_out_}; + } + + private: + void move_from_(DynamicFeedbackStorage& o) noexcept { + n_out_ = o.n_out_; n_weights_ = o.n_weights_; undo_cap_ = o.undo_cap_; + off_placed_ = o.off_placed_; off_snap_ = o.off_snap_; + off_scratch_ = o.off_scratch_; off_undo_ = o.off_undo_; off_focus_ = o.off_focus_; + arena_ = o.arena_; + o.arena_ = nullptr; + } + + std::size_t n_out_ = 0u, n_weights_ = 0u, undo_cap_ = 0u; + std::size_t off_placed_ = 0u, off_snap_ = 0u, off_scratch_ = 0u, + off_undo_ = 0u, off_focus_ = 0u; + float* arena_ = nullptr; +}; + } // namespace nisps::ml diff --git a/nisps/ml/feedback.hpp b/nisps/ml/feedback.hpp index f665d1b..ae11dab 100644 --- a/nisps/ml/feedback.hpp +++ b/nisps/ml/feedback.hpp @@ -21,12 +21,21 @@ // then restores the original net (the kept example then // trains the original net toward the audition). // -// Design: header-only class template over the concrete MLP type. The controller -// does NOT own the MLP — every mutating method takes `MLP_T&`. It owns only the -// exploration state, all fixed-size (no heap), with its OWN per-instance Rng so +// STORAGE POLICY (one-core-engine-refactor P2): like MLPCore, the controller +// algorithms are written once in `FeedbackControllerCore` against a +// storage surface. Two models: +// * `FixedFeedbackStorage` — std::array, zero +// heap. The classic `FeedbackController` alias derives +// the sizes from the fixed MLP type; firmware + tests compile unchanged. +// * `DynamicFeedbackStorage` (nisps/ml/dynamic_storage.hpp) — sizes at +// construction for the runtime-shaped browser MLP. Non-embedded only. +// +// Design: the controller does NOT own the MLP — every mutating method takes +// the MLP by reference (method-level template, so fixed and dynamic MLPs both +// work). It owns only the exploration state, with its OWN per-instance Rng so // re-rolling outputs is deterministic and never perturbs the MLP's RNG stream. -// Honours the RP2350 perf contract: no heap, no virtual dispatch, deterministic -// per-instance RNG. +// Honours the RP2350 perf contract in the fixed model: no heap, no virtual +// dispatch, deterministic per-instance RNG. // // The C++/JS boundary: the controller decides *what transition happened* // (returns a FeedbackAction); the caller decides *what to persist* (add example, @@ -40,6 +49,7 @@ #include #include +#include "../core/perf.hpp" #include "../core/rng.hpp" namespace nisps::ml { @@ -59,7 +69,7 @@ enum class FeedbackMode : std::uint8_t { // Exploring — real net snapshotted aside; a random SCRATCHPAD net is live and // the user auditions it (reroll / nudge / undo). NEVER trained. // Placing — the user liked the current scratchpad sound; its output vector -// is FROZEN in placed_out_ and held while they choose WHERE to +// is FROZEN in placed_out and held while they choose WHERE to // place it. The caller drives inference at the chosen input but // the audition stays the frozen vector. enum class ExploreState : std::uint8_t { @@ -84,28 +94,68 @@ enum class FeedbackAction : std::uint8_t { ScratchReroll = 8, // scratchpad re-randomised (Exploring); pure audition, no store. ScratchNudge = 9, // scratchpad nudged (bounded perturb, Exploring); undoable. ScratchUndo = 10, // last reroll/nudge undone (Exploring). - BeginPlace = 11, // Exploring→Placing; placed_out_ captured + frozen (no store yet). + BeginPlace = 11, // Exploring→Placing; placed_out captured + frozen (no store yet). CommitPlace = 12, // Placing→Idle; real net restored. CALLER adds +1 (input→placed_output) + trains. CancelPlace = 13, // Placing→Exploring; backed out of placing (no store). }; +// --------------------------------------------------------------------------- +// Fixed feedback storage — std::array, zero heap. Sizes are compile-time. // UndoDepth = number of scratchpad ops (reroll/nudge) that can be undone in -// ExploreAndPlace. The undo ring is a fixed std::array of weight snapshots -// (no heap); each slot is kWeights floats. WASM uses depth 4, firmware 2 (per -// rl-feedback-design §2.2 — SRAM budget). Default 4 (the WASM depth). -template -class FeedbackController { +// ExploreAndPlace; each undo slot is NWeights floats. WASM historically used +// depth 4, firmware 2 (per rl-feedback-design §2.2 — SRAM budget). +// --------------------------------------------------------------------------- +template +class FixedFeedbackStorage { public: - static constexpr std::size_t kNOut = MLP_T::kOutput; - static constexpr std::size_t kWeights = MLP_T::weight_count(); + static constexpr std::size_t kNOut = NOut; + static constexpr std::size_t kWeights = NWeights; static constexpr std::size_t kUndoDepth = UndoDepth; - explicit FeedbackController(std::uint64_t seed) noexcept : rng_(seed) {} + static constexpr std::size_t n_out() noexcept { return NOut; } + static constexpr std::size_t n_weights() noexcept { return NWeights; } + static constexpr std::size_t undo_cap() noexcept { return UndoDepth; } + + NISPS_FORCE_INLINE std::span static_out() noexcept { return static_out_; } + NISPS_FORCE_INLINE std::span static_out() const noexcept { return static_out_; } + NISPS_FORCE_INLINE std::span snapshot() noexcept { return snapshot_; } + NISPS_FORCE_INLINE std::span snapshot() const noexcept { return snapshot_; } + NISPS_FORCE_INLINE std::span focus() noexcept { return focus_; } + NISPS_FORCE_INLINE std::span focus() const noexcept { return focus_; } + NISPS_FORCE_INLINE std::span placed_out() noexcept { return placed_out_; } + NISPS_FORCE_INLINE std::span placed_out() const noexcept { return placed_out_; } + NISPS_FORCE_INLINE std::span scratch_buf() noexcept { return scratch_buf_; } + NISPS_FORCE_INLINE std::span undo_slot(std::size_t i) noexcept { return undo_ring_[i]; } + NISPS_FORCE_INLINE std::span undo_slot(std::size_t i) const noexcept { + return undo_ring_[i]; + } + + private: + std::array static_out_{}; + std::array snapshot_{}; + std::array focus_{}; + std::array placed_out_{}; + std::array, UndoDepth> undo_ring_{}; + std::array scratch_buf_{}; +}; + +// --------------------------------------------------------------------------- +// The controller algorithms, written once against the feedback storage +// surface: n_out(), n_weights(), undo_cap(), static_out(), snapshot(), +// focus(), placed_out(), scratch_buf(), undo_slot(i). +// --------------------------------------------------------------------------- +template +class FeedbackControllerCore : public FbStorage { + public: + template + explicit FeedbackControllerCore(std::uint64_t seed, StorageArgs&&... storage_args) noexcept + : FbStorage(static_cast(storage_args)...), rng_(seed) {} // ---- mode --------------------------------------------------------------- // Switching mode mid-exploration cleanly tears down: restores the net (in // RandomiseMlp) and resumes learning, so we never strand a randomised net. - void set_mode(FeedbackMode m, MLP_T& mlp) noexcept { + template + void set_mode(FeedbackMode m, M& mlp) noexcept { if (explore_active_) abort_explore(mlp); if (ep_state_ != ExploreState::Idle) abort_explore_place(mlp); mode_ = m; @@ -126,33 +176,35 @@ class FeedbackController { bool placing() const noexcept { return ep_state_ == ExploreState::Placing; } // True while a REPOSITION hold is active (grab→move→drop). Distinguishes a // reposition (real net never set aside) from an Explore→Place (scratchpad + - // snapshot). Both sit in ExploreState::Placing and both hold placed_out_ via + // snapshot). Both sit in ExploreState::Placing and both hold placed_out via // static_output(); only commit/teardown differ (reposition does NOT restore // weights — there is nothing to restore). bool repositioning() const noexcept { return reposition_; } - // Depth of the scratchpad undo ring currently available to pop (0..UndoDepth). + // Depth of the scratchpad undo ring currently available to pop (0..undo_cap). std::size_t undo_depth() const noexcept { return undo_count_; } // The output vector frozen at like()/begin-place time. Valid only while // placing(); empty span otherwise. The caller adds this as the +1 example // label at commit (input → placed_output). std::span placed_output() const noexcept { if (ep_state_ != ExploreState::Placing) return {}; - return std::span(placed_out_.data(), kNOut); + return this->placed_out(); } // ---- focus mask: 1 byte per output; 0 == frozen (unfocused). Copied into a // fixed buffer (no heap, no dangling span). Empty ⇒ all outputs active. void set_focus_mask(std::span mask) noexcept { - focus_count_ = (mask.size() < kNOut) ? mask.size() : kNOut; - for (std::size_t i = 0; i < focus_count_; ++i) focus_[i] = mask[i]; + auto focus = this->focus(); + focus_count_ = (mask.size() < focus.size()) ? mask.size() : focus.size(); + for (std::size_t i = 0; i < focus_count_; ++i) focus[i] = mask[i]; } void clear_focus_mask() noexcept { focus_count_ = 0; } // ---- press handlers ----------------------------------------------------- // `current_out` is the live (post-pipeline) output the user is hearing - // (kNOut floats). `pin_mask` may be empty. Returns the FeedbackAction the + // (n_out floats). `pin_mask` may be empty. Returns the FeedbackAction the // caller must act on. - FeedbackAction on_down(MLP_T& mlp, std::span current_out, + template + FeedbackAction on_down(M& mlp, std::span current_out, float speed, float spread, std::span pin_mask) noexcept { switch (mode_) { @@ -197,7 +249,8 @@ class FeedbackController { // Up = thumbs-up / "keep". While exploring it commits: the CALLER must have // captured the heard output BEFORE calling this (on_up restores the original // net in RandomiseMlp), then stores it as a +1 example at the current input. - FeedbackAction on_up(MLP_T& mlp) noexcept { + template + FeedbackAction on_up(M& mlp) noexcept { if (mode_ == FeedbackMode::ExploreAndPlace) { // SOFTWARE DEFAULT POLICY (browser): up begins place from // Exploring (freeze the heard output), then commits from Placing @@ -225,7 +278,8 @@ class FeedbackController { // Drag-store (joystick freeze→reposition→release). In RandomiseMlp this is // the "reposition-commit": the caller has already stored the +1 at the new // input; we just restore the original net and end exploration. - FeedbackAction on_drag(MLP_T& mlp) noexcept { + template + FeedbackAction on_drag(M& mlp) noexcept { if (explore_active_ && mode_ == FeedbackMode::RandomiseMlp) { restore_after_explore(mlp); return FeedbackAction::Restore; @@ -235,18 +289,21 @@ class FeedbackController { // Inference hook: fills `out` with the held static vector and returns true // when RandomiseOutputs is bypassing the MLP; else returns false (the caller - // should run mlp.process() normally). `out` should hold at least kNOut. + // should run mlp.process() normally). `out` should hold at least n_out. bool static_output(std::span out) const noexcept { + const std::size_t n_out = this->n_out(); // ExploreAndPlace: while PLACING, the audition is the frozen vector the // user liked, held steady as they aim at a location. if (mode_ == FeedbackMode::ExploreAndPlace && ep_state_ == ExploreState::Placing) { - const std::size_t n = (out.size() < kNOut) ? out.size() : kNOut; - for (std::size_t i = 0; i < n; ++i) out[i] = placed_out_[i]; + const auto placed = this->placed_out(); + const std::size_t n = (out.size() < n_out) ? out.size() : n_out; + for (std::size_t i = 0; i < n; ++i) out[i] = placed[i]; return true; } if (!(mode_ == FeedbackMode::RandomiseOutputs && explore_active_)) return false; - const std::size_t n = (out.size() < kNOut) ? out.size() : kNOut; - for (std::size_t i = 0; i < n; ++i) out[i] = static_out_[i]; + const auto held = this->static_out(); + const std::size_t n = (out.size() < n_out) ? out.size() : n_out; + for (std::size_t i = 0; i < n; ++i) out[i] = held[i]; return true; } @@ -264,11 +321,11 @@ class FeedbackController { // Idle→Exploring. Snapshot the real (trained) net aside, randomise a // scratchpad net the user auditions. No-op if not Idle. - void enter_explore(MLP_T& mlp, float spread) noexcept { + template + void enter_explore(M& mlp, float spread) noexcept { if (mode_ != FeedbackMode::ExploreAndPlace) return; if (ep_state_ != ExploreState::Idle) return; - auto w = mlp.get_weights(); // flat snapshot (size == kWeights) - for (std::size_t i = 0; i < kWeights; ++i) snapshot_[i] = w[i]; + take_snapshot(mlp); learning_paused_ = true; ep_state_ = ExploreState::Exploring; undo_count_ = 0u; @@ -278,78 +335,85 @@ class FeedbackController { // Exploring→Idle. Restore the real net, discard the scratchpad. No example // stored. (The hardware "enter/exit explore toggle" off-path.) - void exit_explore(MLP_T& mlp) noexcept { + template + void exit_explore(M& mlp) noexcept { if (mode_ != FeedbackMode::ExploreAndPlace) return; if (ep_state_ == ExploreState::Idle) return; restore_real_net(mlp); } // Exploring scratchpad op: re-randomise the scratchpad. Undoable. - void reroll(MLP_T& mlp, float spread) noexcept { + template + void reroll(M& mlp, float spread) noexcept { if (!can_scratch_op()) return; push_undo(mlp); mlp.draw_weights(spread); } // Exploring scratchpad op: small bounded perturbation of the scratchpad via - // move_weights on the controller's OWN Rng-free path — move_weights uses the - // MLP's Rng, so to keep the controller's Rng stream out of the MLP stream we - // draw the perturbation here and apply it. Undoable. `amount` is the noise - // stddev (small, e.g. 0.05). - void nudge(MLP_T& mlp, float amount) noexcept { + // the controller's OWN Rng (move_weights uses the MLP's Rng; to keep the + // controller's Rng stream out of the MLP stream we draw the perturbation + // here and apply it). Undoable. `amount` is the noise stddev (e.g. 0.05). + template + void nudge(M& mlp, float amount) noexcept { if (!can_scratch_op()) return; push_undo(mlp); + auto scratch = this->scratch_buf(); + const std::size_t n_weights = this->n_weights(); auto w = mlp.get_weights(); - for (std::size_t i = 0; i < kWeights; ++i) { - scratch_buf_[i] = w[i] + rng_.next_float_gaussian(amount); + for (std::size_t i = 0; i < n_weights; ++i) { + scratch[i] = w[i] + rng_.next_float_gaussian(amount); } - mlp.set_weights(std::span(scratch_buf_.data(), kWeights)); + mlp.set_weights(std::span(scratch.data(), n_weights)); } // Exploring scratchpad op: undo the last reroll/nudge (bounded ring). - void undo(MLP_T& mlp) noexcept { + template + void undo(M& mlp) noexcept { if (!can_scratch_op()) return; if (undo_count_ == 0u) return; - undo_head_ = (undo_head_ + kUndoDepth - 1u) % kUndoDepth; + const std::size_t cap = this->undo_cap(); + undo_head_ = (undo_head_ + cap - 1u) % cap; --undo_count_; - mlp.set_weights(std::span(undo_ring_[undo_head_].data(), kWeights)); + const auto slot = this->undo_slot(undo_head_); + mlp.set_weights(std::span(slot.data(), this->n_weights())); } // Exploring→Placing. Capture + FREEZE the current scratchpad output the user // is auditioning. The caller MUST have run mlp.process() at the audition // input first; pass that output here. While placing, static_output() holds // this vector and the caller chooses WHERE to place it. - void begin_place(MLP_T& mlp, std::span current_out) noexcept { + template + void begin_place(M& mlp, std::span current_out) noexcept { + (void)mlp; if (mode_ != FeedbackMode::ExploreAndPlace) return; if (ep_state_ != ExploreState::Exploring) return; - const std::size_t n = (current_out.size() < kNOut) ? current_out.size() : kNOut; - for (std::size_t i = 0; i < n; ++i) placed_out_[i] = current_out[i]; + capture_placed(current_out); ep_state_ = ExploreState::Placing; } // Convenience: freeze the scratchpad's output at its CURRENT input (runs the // forward pass on the live scratchpad net). Equivalent to process()+capture. - void begin_place(MLP_T& mlp) noexcept { + template + void begin_place(M& mlp) noexcept { if (mode_ != FeedbackMode::ExploreAndPlace) return; if (ep_state_ != ExploreState::Exploring) return; mlp.process(); - const auto outs = mlp.outputs(); - const std::size_t n = (outs.size() < kNOut) ? outs.size() : kNOut; - for (std::size_t i = 0; i < n; ++i) placed_out_[i] = outs[i]; + capture_placed(mlp.outputs()); ep_state_ = ExploreState::Placing; } // Placing→Idle. Restore the real net. The CALLER then adds a +1 example at - // (chosen input → placed_output()) and trains. Returns the placed output so - // the caller can read it after the restore (it survives the restore). - void commit_place(MLP_T& mlp) noexcept { + // (chosen input → placed_output()) and trains. + template + void commit_place(M& mlp) noexcept { if (mode_ != FeedbackMode::ExploreAndPlace) return; if (ep_state_ != ExploreState::Placing) return; - // Restore the real net but KEEP placed_out_ valid for the caller until + // Restore the real net but KEEP placed_out valid for the caller until // it transitions to Idle; expose via a separate accessor that does not // gate on Placing. - mlp.set_weights(std::span(snapshot_.data(), kWeights)); - last_placed_valid_ = true; // placed_out_ holds the just-committed vector + restore_snapshot(mlp); + last_placed_valid_ = true; // placed_out holds the just-committed vector learning_paused_ = false; ep_state_ = ExploreState::Idle; undo_count_ = 0u; @@ -360,7 +424,7 @@ class FeedbackController { // AFTER commit_place has restored the real net. std::span committed_output() const noexcept { if (!last_placed_valid_) return {}; - return std::span(placed_out_.data(), kNOut); + return this->placed_out(); } // Placing→Exploring. Back out of placing without storing; resume auditioning @@ -393,8 +457,7 @@ class FeedbackController { void begin_reposition(std::span current_out) noexcept { if (mode_ != FeedbackMode::ExploreAndPlace) return; if (ep_state_ != ExploreState::Idle) return; - const std::size_t n = (current_out.size() < kNOut) ? current_out.size() : kNOut; - for (std::size_t i = 0; i < n; ++i) placed_out_[i] = current_out[i]; + capture_placed(current_out); reposition_ = true; learning_paused_ = true; last_placed_valid_ = false; @@ -403,13 +466,12 @@ class FeedbackController { // Convenience: capture the trained net's output at its CURRENT input // (process + capture). Equivalent to begin_reposition(mlp.outputs()). - void begin_reposition(MLP_T& mlp) noexcept { + template + void begin_reposition(M& mlp) noexcept { if (mode_ != FeedbackMode::ExploreAndPlace) return; if (ep_state_ != ExploreState::Idle) return; mlp.process(); - const auto outs = mlp.outputs(); - const std::size_t n = (outs.size() < kNOut) ? outs.size() : kNOut; - for (std::size_t i = 0; i < n; ++i) placed_out_[i] = outs[i]; + capture_placed(mlp.outputs()); reposition_ = true; learning_paused_ = true; last_placed_valid_ = false; @@ -429,48 +491,74 @@ class FeedbackController { } private: + void capture_placed(std::span src) noexcept { + auto placed = this->placed_out(); + const std::size_t n = (src.size() < placed.size()) ? src.size() : placed.size(); + for (std::size_t i = 0; i < n; ++i) placed[i] = src[i]; + } + + template + void take_snapshot(M& mlp) noexcept { + auto snap = this->snapshot(); + auto w = mlp.get_weights(); // flat snapshot (size == n_weights) + const std::size_t n = this->n_weights(); + for (std::size_t i = 0; i < n; ++i) snap[i] = w[i]; + } + + template + void restore_snapshot(M& mlp) noexcept { + const auto snap = this->snapshot(); + mlp.set_weights(std::span(snap.data(), this->n_weights())); + } + void enter_randomise_outputs(std::span seed_out) noexcept { explore_active_ = true; learning_paused_ = true; // Seed every dim with the live output the user is hearing, so unfocused // (frozen) dims hold that value through the exploration — matching the // firmware `staticRandomOut_ = action; _roll_static_outputs();`. The - // CALLER CONTRACT is to pass the full kNOut live output. Any dims beyond + // CALLER CONTRACT is to pass the full n_out live output. Any dims beyond // a short seed keep their previous static value (we have no live value // to freeze them to); they are only observable if a focus mask freezes // a dim the short seed did not cover — an out-of-contract corner. - const std::size_t n = (seed_out.size() < kNOut) ? seed_out.size() : kNOut; - for (std::size_t i = 0; i < n; ++i) static_out_[i] = seed_out[i]; + auto held = this->static_out(); + const std::size_t n = (seed_out.size() < held.size()) ? seed_out.size() : held.size(); + for (std::size_t i = 0; i < n; ++i) held[i] = seed_out[i]; roll_static_outputs(); } void roll_static_outputs() noexcept { - for (std::size_t i = 0; i < kNOut; ++i) { - const bool active = (focus_count_ == 0u) || (i < focus_count_ && focus_[i] != 0u); - if (active) static_out_[i] = rng_.next_float_uniform(); // [0, 1) + auto held = this->static_out(); + const auto focus = this->focus(); + for (std::size_t i = 0; i < held.size(); ++i) { + const bool active = (focus_count_ == 0u) || (i < focus_count_ && focus[i] != 0u); + if (active) held[i] = rng_.next_float_uniform(); // [0, 1) // inactive dims keep their seeded entry value } } - void enter_randomise_mlp(MLP_T& mlp, float spread) noexcept { + template + void enter_randomise_mlp(M& mlp, float spread) noexcept { explore_active_ = true; learning_paused_ = true; - auto w = mlp.get_weights(); // flat snapshot (size == kWeights) - for (std::size_t i = 0; i < kWeights; ++i) snapshot_[i] = w[i]; + take_snapshot(mlp); mlp.draw_weights(spread); // randomise the live net } - void restore_after_explore(MLP_T& mlp) noexcept { + template + void restore_after_explore(M& mlp) noexcept { if (mode_ == FeedbackMode::RandomiseMlp) { - mlp.set_weights(std::span(snapshot_.data(), kWeights)); + restore_snapshot(mlp); } learning_paused_ = false; explore_active_ = false; } // Cancel and abort share restore semantics; the caller stores nothing. - void cancel_explore(MLP_T& mlp) noexcept { restore_after_explore(mlp); } - void abort_explore(MLP_T& mlp) noexcept { restore_after_explore(mlp); } + template + void cancel_explore(M& mlp) noexcept { restore_after_explore(mlp); } + template + void abort_explore(M& mlp) noexcept { restore_after_explore(mlp); } // ---- ExploreAndPlace helpers -------------------------------------------- bool can_scratch_op() const noexcept { @@ -480,28 +568,34 @@ class FeedbackController { // Push the CURRENT scratchpad weights onto the bounded undo ring before a // mutating op, so undo() restores the pre-op candidate. - void push_undo(MLP_T& mlp) noexcept { + template + void push_undo(M& mlp) noexcept { + auto slot = this->undo_slot(undo_head_); auto w = mlp.get_weights(); - for (std::size_t i = 0; i < kWeights; ++i) undo_ring_[undo_head_][i] = w[i]; - undo_head_ = (undo_head_ + 1u) % kUndoDepth; - if (undo_count_ < kUndoDepth) ++undo_count_; + const std::size_t n = this->n_weights(); + for (std::size_t i = 0; i < n; ++i) slot[i] = w[i]; + const std::size_t cap = this->undo_cap(); + undo_head_ = (undo_head_ + 1u) % cap; + if (undo_count_ < cap) ++undo_count_; } // Restore the set-aside real net and return to Idle. Shared by exit_explore // and abort_explore_place. No example stored. - void restore_real_net(MLP_T& mlp) noexcept { - mlp.set_weights(std::span(snapshot_.data(), kWeights)); + template + void restore_real_net(M& mlp) noexcept { + restore_snapshot(mlp); learning_paused_ = false; ep_state_ = ExploreState::Idle; undo_count_ = 0u; last_placed_valid_ = false; } - void abort_explore_place(MLP_T& mlp) noexcept { + template + void abort_explore_place(M& mlp) noexcept { if (ep_state_ == ExploreState::Idle) return; if (reposition_) { // A reposition never set the real net aside, so there is nothing to - // restore — clearing snapshot_ into the net here would CLOBBER the + // restore — clearing snapshot into the net here would CLOBBER the // live trained weights. Just drop the hold. reposition_ = false; learning_paused_ = false; @@ -516,22 +610,22 @@ class FeedbackController { FeedbackMode mode_ = FeedbackMode::Avoid; bool explore_active_ = false; bool learning_paused_ = false; - std::array static_out_{}; - std::array snapshot_{}; - std::array focus_{}; - std::size_t focus_count_ = 0; // 0 ⇒ all active + std::size_t focus_count_ = 0; // 0 ⇒ all active - // ---- ExploreAndPlace state (all fixed-size, no heap) -------------------- - ExploreState ep_state_ = ExploreState::Idle; - std::array placed_out_{}; // frozen audition/carried vector - bool last_placed_valid_ = false; - bool reposition_ = false; // grab→move→drop hold; net NOT set aside - std::array, kUndoDepth> undo_ring_{}; // bounded undo - std::size_t undo_head_ = 0u; // next write slot - std::size_t undo_count_ = 0u; // valid entries (0..kUndoDepth) - std::array scratch_buf_{}; // nudge scratch (no heap) + // ---- ExploreAndPlace state ---------------------------------------------- + ExploreState ep_state_ = ExploreState::Idle; + bool last_placed_valid_ = false; + bool reposition_ = false; // grab→move→drop hold; net NOT set aside + std::size_t undo_head_ = 0u; // next write slot + std::size_t undo_count_ = 0u; // valid entries (0..undo_cap) - Rng rng_; + Rng rng_; }; +// The classic fixed-size controller over a compile-time MLP type — the +// firmware model and the default for tests. Sizes derive from the MLP. +template +using FeedbackController = FeedbackControllerCore< + FixedFeedbackStorage>; + } // namespace nisps::ml diff --git a/nisps/ml/warm_start.hpp b/nisps/ml/warm_start.hpp new file mode 100644 index 0000000..842edf8 --- /dev/null +++ b/nisps/ml/warm_start.hpp @@ -0,0 +1,65 @@ +// nisps/ml/warm_start.hpp — copy overlapping weights between two MLPs of +// (possibly) different shapes. +// +// Used by the runtime-reshape path (one-core-engine-refactor P2): reshape = +// construct a NEW instance at the new dimensions, then warm-start it by +// copying every weight/bias whose (layer, node, input) coordinate exists in +// BOTH shapes. Weights outside the overlap keep the destination's fresh +// initialisation. Deterministic, allocation-free, works across storage +// policies (fixed→dynamic, dynamic→dynamic, fixed→fixed). +// +// Row-major layout per layer: w[node * fan_in + j]. The overlap is the +// top-left submatrix min(fan_out) × min(fan_in) plus the bias prefix +// min(fan_out). + +#pragma once + +#include +#include + +#include "../core/perf.hpp" + +namespace nisps::ml { + +namespace detail { + +template +NISPS_FORCE_INLINE void warm_start_copy_layer(DstMLP& dst, const SrcMLP& src) noexcept { + const std::size_t src_in = src.template fan_in_l(); + const std::size_t src_out = src.template fan_out_l(); + const std::size_t dst_in = dst.template fan_in_l(); + const std::size_t dst_out = dst.template fan_out_l(); + const std::size_t n_in = (src_in < dst_in) ? src_in : dst_in; + const std::size_t n_out = (src_out < dst_out) ? src_out : dst_out; + + std::span sw = src.template weights_l(); + std::span dw = dst.template weights_l(); + for (std::size_t node = 0; node < n_out; ++node) { + const std::size_t src_row = node * src_in; + const std::size_t dst_row = node * dst_in; + for (std::size_t j = 0; j < n_in; ++j) { + dw[dst_row + j] = sw[src_row + j]; + } + } + + std::span sb = src.template biases_l(); + std::span db = dst.template biases_l(); + for (std::size_t node = 0; node < n_out; ++node) { + db[node] = sb[node]; + } +} + +} // namespace detail + +// Copy the overlapping region of every layer from `src` into `dst`. Both +// must expose the MLP storage surface (fan_in_l/fan_out_l/weights_l/ +// biases_l) — i.e. any MLPCore instantiation. +template +inline void warm_start_copy(DstMLP& dst, const SrcMLP& src) noexcept { + detail::warm_start_copy_layer<0u>(dst, src); + detail::warm_start_copy_layer<1u>(dst, src); + detail::warm_start_copy_layer<2u>(dst, src); + detail::warm_start_copy_layer<3u>(dst, src); +} + +} // namespace nisps::ml diff --git a/nisps/wasm/bindings.cpp b/nisps/wasm/bindings.cpp index fdfa3f3..f5dea44 100644 --- a/nisps/wasm/bindings.cpp +++ b/nisps/wasm/bindings.cpp @@ -1,31 +1,26 @@ -// nisps/wasm/bindings.cpp — flat C API exported to the SolidJS playground. +// nisps/wasm/bindings.cpp — flat C API exported to the Manifold browser app. // // Two consumers per build: -// 1. Main-thread WasmIML (playground/src/ml/wasm-iml.ts) — ML calls. -// 2. AudioWorklet processor (playground/src/audio/worklet/...) — engine +// 1. Main-thread WasmIML (manifold/src/engine/wasm-iml.ts) — ML calls. +// 2. AudioWorklet processor (manifold/src/engine/worklet/...) — engine // calls. (Each instance owns its own WASM module instance.) // -// ARCHITECTURE (input dim is OVER-PROVISIONED for mix-and-match inputs) +// ARCHITECTURE (runtime-shaped since one-core-engine-refactor P2) // --------------------------------------------------------------------- -// The C++ MLP class is templated on layer sizes (architecture.md §4.1, §6.2). -// We instantiate ONE concrete configuration here: +// The browser MLP is `MLPCore`: `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). // -// using DefaultMLP = nisps::ml::MLP<32, 10, 14, 18, 126>; -// -// The 32-input dimension is the MAX number of composed input axes the manifold -// front-end can feed (matches MAX_AXES in manifold/src/inputs/input-layer.ts). -// The mix-and-match input layer (Internal XY pad + Game Controller + MIDI) gives -// each active axis its OWN dedicated input slot — NO mean-blending — and feeds -// the remaining (unused) slots a constant 0. The "active input dimension count" -// is a front-end concept: a 2-axis pad uses slots 0–1, a 4-axis pad+stick uses -// 0–3, etc. Because slot assignment is stable and unused slots are held at 0, -// the net behaves as an N-input net where N = active axes; changing N is a -// reshape, after which the front-end resets the weights (recreate-from-scratch, -// behind a confirm modal). 126 outputs cover C15 + any current schema. -// -// `nisps_ml_create()` accepts caller-supplied input_size/output_size/hidden[] -// but only validates them against these compile-time defaults — extra -// inputs/outputs are clipped at the boundary and hidden overrides are ignored. +// 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). // // WIRE FORMAT FOR WEIGHTS // ----------------------- @@ -68,9 +63,11 @@ // ML. #include "../core/types.hpp" +#include "../ml/dynamic_storage.hpp" #include "../ml/feedback.hpp" #include "../ml/mlp.hpp" #include "../ml/stats.hpp" +#include "../ml/warm_start.hpp" namespace { @@ -78,45 +75,89 @@ namespace { // ML side // --------------------------------------------------------------------------- -// Compile-time default architecture. See header comment. -// -// Choice rationale: -// * 2 inputs — playground virtual joystick (X, Y). +// 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). // * [10, 14, 18] hidden — covers the largest schema layouts in -// `schemas/modes/*.json` (channel_strip variants, verb_fx, breakor, -// elysiamorf, memlcelium). +// `schemas/modes/*.json`. // * 126 outputs — enough for the C15 mode and any current schema. -// -// The MLP also has dataset slots, loss history etc. — see mlp.hpp. -using DefaultMLP = nisps::ml::MLP<32u, 10u, 14u, 18u, 126u>; +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::size_t kDefaultInputs = DefaultMLP::kInput; -constexpr std::size_t kDefaultOutputs = DefaultMLP::kOutput; +constexpr std::uint64_t kFeedbackSalt = 0xFEEDBACC0DEull; + +using BrowserMLP = nisps::ml::MLPCore; +using BrowserFeedback = + nisps::ml::FeedbackControllerCore; + +constexpr std::size_t kFeedbackUndoDepth = 4u; + +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(input_size); + if (output_size > 0) d.n_out = static_cast(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(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; +} // We allocate the MLP on the heap (one-off — not the audio path) and return // the opaque pointer to JS. struct MLHandle { - DefaultMLP mlp; + std::uint64_t seed64; + BrowserMLP mlp; // "Down Action" negative-feedback controller (Avoid/RandomiseOutputs/ - // RandomiseMlp). Seeded off the MLP seed XOR a salt so its static-output - // RNG stream is independent of the MLP's inference/move RNG. - nisps::ml::FeedbackController feedback; - // Buffers used to bridge JS → C++: - std::array input_scratch{}; - std::array output_scratch{}; + // 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 output_scratch; // Stats buffer fed back to JS via get_layer_stats. - std::array stats_scratch{}; + std::array stats_scratch{}; // Static-output buffer for the RandomiseOutputs bypass path. - std::array feedback_static_scratch{}; - // Used by infer_batch with arbitrary N — must exceed any reasonable - // request from the heatmap. 256x256 = 65536 max points → too many in - // practice. We cap batch size at 4096 here; callers must split larger - // requests. + std::vector feedback_static_scratch; + // infer_batch cap; callers must split larger requests. static constexpr std::size_t kMaxBatch = 4096u; - std::array batch_out_scratch{}; - explicit MLHandle(std::uint64_t seed) noexcept - : mlp(seed), feedback(seed ^ 0xFEEDBACC0DEull) {} + MLHandle(std::uint64_t seed, const MlDims& d) noexcept + : seed64(seed), + mlp(seed, d.n_in, std::span(d.hidden, 3u), d.n_out), + feedback(seed ^ kFeedbackSalt, d.n_out, mlp.weight_count(), kFeedbackUndoDepth), + output_scratch(d.n_out, 0.f), + feedback_static_scratch(d.n_out, 0.f) {} + + 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(); } }; // --------------------------------------------------------------------------- @@ -290,24 +331,57 @@ extern "C" { EMSCRIPTEN_KEEPALIVE void* nisps_ml_create(int input_size, int output_size, - const int* /*hidden*/, int /*n_hidden*/, + const int* hidden, int n_hidden, uint32_t seed) { - // We accept and ignore caller-supplied dimensions if they don't match the - // compile-time default. See file header. + // Dimensions are HONOURED (runtime-shaped MLP); non-positive/missing args + // fall back to the historical defaults. See file header. // // 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 - // playground doesn't need them, and avoiding BigInt at the boundary + // front-end doesn't need them, and avoiding BigInt at the boundary // simplifies both wasm-iml.ts and wasm-worker.ts. - (void)input_size; - (void)output_size; + const MlDims d = sanitise_dims(input_size, output_size, hidden, n_hidden); + if (!d.ok) return nullptr; const std::uint64_t s64 = static_cast(seed) ^ (static_cast(seed) << 32); - auto* h = new MLHandle(s64); + auto* h = new MLHandle(s64, d); + if (!h->valid()) { + delete h; + return nullptr; + } return static_cast(h); } +// 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(ml); + const MlDims d = sanitise_dims(input_size, output_size, hidden, n_hidden); + if (!d.ok) return 0; + + BrowserMLP fresh(h->seed64, d.n_in, std::span(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(), + kFeedbackUndoDepth); + if (!fb.valid()) return 0; + + h->mlp = static_cast(fresh); + h->feedback = static_cast(fb); + h->output_scratch.assign(d.n_out, 0.f); + h->feedback_static_scratch.assign(d.n_out, 0.f); + return 1; +} + EMSCRIPTEN_KEEPALIVE void nisps_ml_destroy(void* ml) { if (!ml) return; @@ -323,7 +397,7 @@ void nisps_ml_set_input(void* ml, int idx, float v) { if (!ml) return; auto* h = static_cast(ml); if (idx < 0) return; - if (static_cast(idx) >= kDefaultInputs) return; + if (static_cast(idx) >= h->n_in()) return; h->mlp.set_input(static_cast(idx), v); } @@ -333,7 +407,8 @@ void nisps_ml_process(void* ml) { auto* h = static_cast(ml); h->mlp.process(); auto outs = h->mlp.outputs(); - for (std::size_t i = 0; i < kDefaultOutputs; ++i) h->output_scratch[i] = outs[i]; + 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]; } EMSCRIPTEN_KEEPALIVE @@ -347,18 +422,20 @@ 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(ml); + const std::size_t n_in = h->n_in(); + const std::size_t n_out = h->n_out(); const std::size_t n = static_cast(n_points); if (n > MLHandle::kMaxBatch) { - // Caller exceeded the scratch buffer. Process what we can. + // Caller exceeded the batch cap. Process what we can. const std::size_t safe_n = MLHandle::kMaxBatch; h->mlp.infer_batch( - std::span(points, safe_n * kDefaultInputs), - std::span(out, safe_n * kDefaultOutputs)); + std::span(points, safe_n * n_in), + std::span(out, safe_n * n_out)); return; } h->mlp.infer_batch( - std::span(points, n * kDefaultInputs), - std::span(out, n * kDefaultOutputs)); + std::span(points, n * n_in), + std::span(out, n * n_out)); } // --------------------------------------------------------------------------- @@ -370,8 +447,8 @@ void nisps_ml_add_example(void* ml, const float* features, const float* labels) if (!ml || !features || !labels) return; auto* h = static_cast(ml); h->mlp.add_example( - std::span(features, kDefaultInputs), - std::span(labels, kDefaultOutputs)); + std::span(features, h->n_in()), + std::span(labels, h->n_out())); } EMSCRIPTEN_KEEPALIVE @@ -400,8 +477,15 @@ float nisps_ml_eval_loss(void* ml) { EMSCRIPTEN_KEEPALIVE int nisps_ml_weight_count(void* ml) { - (void)ml; - return static_cast(DefaultMLP::weight_count()); + if (!ml) { + // Handle-less callers get the default shape's count. + const MlDims d = sanitise_dims(0, 0, nullptr, 0); + return static_cast( + 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(static_cast(ml)->mlp.weight_count()); } EMSCRIPTEN_KEEPALIVE @@ -416,7 +500,7 @@ EMSCRIPTEN_KEEPALIVE void nisps_ml_set_weights(void* ml, const float* in) { if (!ml || !in) return; auto* h = static_cast(ml); - h->mlp.set_weights(std::span(in, DefaultMLP::weight_count())); + h->mlp.set_weights(std::span(in, h->mlp.weight_count())); } EMSCRIPTEN_KEEPALIVE @@ -433,7 +517,7 @@ void nisps_ml_move_weights(void* ml, float speed, float spread, auto* h = static_cast(ml); std::span mask; if (output_pin_mask) { - mask = std::span(output_pin_mask, kDefaultOutputs); + mask = std::span(output_pin_mask, h->n_out()); } h->mlp.move_weights(speed, spread, mask); } @@ -452,7 +536,7 @@ void nisps_ml_move_weights(void* ml, float speed, float spread, // 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. -// nisps_ml_feedback_down with current_out should pass the full kDefaultOutputs +// nisps_ml_feedback_down with current_out should pass the full n_out // live vector (RandomiseOutputs freezes unfocused dims at those values). // --------------------------------------------------------------------------- @@ -497,7 +581,7 @@ void nisps_ml_feedback_set_focus(void* ml, const uint8_t* mask, int n) { std::span(mask, static_cast(n))); } -// current_out = kDefaultOutputs floats the user is hearing (may be null). +// current_out = n_out floats the user is hearing (may be null). // pin_mask may be null. Returns the FeedbackAction int. EMSCRIPTEN_KEEPALIVE int nisps_ml_feedback_down(void* ml, const float* current_out, @@ -505,9 +589,9 @@ int nisps_ml_feedback_down(void* ml, const float* current_out, if (!ml) return 0; auto* h = static_cast(ml); std::span out; - if (current_out) out = std::span(current_out, kDefaultOutputs); + if (current_out) out = std::span(current_out, h->n_out()); std::span mask; - if (pin_mask) mask = std::span(pin_mask, kDefaultOutputs); + if (pin_mask) mask = std::span(pin_mask, h->n_out()); return static_cast(h->feedback.on_down(h->mlp, out, speed, spread, mask)); } @@ -525,17 +609,17 @@ int nisps_ml_feedback_drag(void* ml) { return static_cast(h->feedback.on_drag(h->mlp)); } -// If returns 1, `out` (kDefaultOutputs floats) holds the static bypass vector +// If returns 1, `out` (n_out floats) holds the static bypass vector // 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(ml); - const bool bypass = - h->feedback.static_output(std::span(h->feedback_static_scratch)); + const bool bypass = h->feedback.static_output(std::span( + h->feedback_static_scratch.data(), h->feedback_static_scratch.size())); if (bypass) { std::memcpy(out, h->feedback_static_scratch.data(), - kDefaultOutputs * sizeof(float)); + h->n_out() * sizeof(float)); } return bypass ? 1 : 0; } @@ -637,7 +721,7 @@ int nisps_ml_feedback_undo_depth(void* ml) { return static_cast(static_cast(ml)->feedback.undo_depth()); } -// Writes the committed/placed output vector (kDefaultOutputs floats) into `out`. +// Writes the committed/placed output vector (n_out floats) into `out`. // 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. @@ -648,7 +732,7 @@ int nisps_ml_feedback_placed_output(void* ml, float* out) { std::span v = h->feedback.committed_output(); if (v.empty()) v = h->feedback.placed_output(); if (v.empty()) return 0; - const std::size_t n = (v.size() < kDefaultOutputs) ? v.size() : kDefaultOutputs; + const std::size_t n = (v.size() < h->n_out()) ? v.size() : h->n_out(); std::memcpy(out, v.data(), n * sizeof(float)); return 1; } @@ -657,7 +741,7 @@ EMSCRIPTEN_KEEPALIVE void nisps_ml_get_layer_stats(void* ml, float* out_stats) { if (!ml || !out_stats) return; auto* h = static_cast(ml); - for (std::size_t i = 0; i < DefaultMLP::kNumLayers; ++i) { + for (std::size_t i = 0; i < BrowserMLP::kNumLayers; ++i) { const auto s = h->mlp.layer_stats(i); out_stats[i * 4u + 0u] = s.mean_abs; out_stats[i * 4u + 1u] = s.max_abs; @@ -689,17 +773,29 @@ void nisps_ml_reset(void* ml) { h->mlp.reset(); } -// Architecture introspection — returns 4-int packed [in, h1, h2, h3, out, n_layers]. -// Kept simple: writes into a caller-supplied int buffer. Always 6 ints. +// Architecture introspection — writes [in, h1, h2, h3, out, n_layers] into a +// caller-supplied int buffer. Always 6 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. EMSCRIPTEN_KEEPALIVE -void nisps_ml_describe(int* out_dims) { +void nisps_ml_describe(void* ml, int* out_dims) { if (!out_dims) return; - out_dims[0] = static_cast(DefaultMLP::kInput); - out_dims[1] = static_cast(DefaultMLP::kHidden1); - out_dims[2] = static_cast(DefaultMLP::kHidden2); - out_dims[3] = static_cast(DefaultMLP::kHidden3); - out_dims[4] = static_cast(DefaultMLP::kOutput); - out_dims[5] = static_cast(DefaultMLP::kNumLayers); + if (!ml) { + out_dims[0] = static_cast(kDefaultInputs); + out_dims[1] = static_cast(kDefaultHidden[0]); + out_dims[2] = static_cast(kDefaultHidden[1]); + out_dims[3] = static_cast(kDefaultHidden[2]); + out_dims[4] = static_cast(kDefaultOutputs); + out_dims[5] = static_cast(BrowserMLP::kNumLayers); + return; + } + auto* h = static_cast(ml); + out_dims[0] = static_cast(h->mlp.n_in()); + out_dims[1] = static_cast(h->mlp.fan_out(0u)); + out_dims[2] = static_cast(h->mlp.fan_out(1u)); + out_dims[3] = static_cast(h->mlp.fan_out(2u)); + out_dims[4] = static_cast(h->mlp.n_out()); + out_dims[5] = static_cast(BrowserMLP::kNumLayers); } // --------------------------------------------------------------------------- diff --git a/scripts/build-wasm.sh b/scripts/build-wasm.sh index 0c5a334..47311a7 100755 --- a/scripts/build-wasm.sh +++ b/scripts/build-wasm.sh @@ -32,7 +32,7 @@ mkdir -p "$OUT" # function is missing. EXPORTED_FUNCS='[ "_malloc","_free", - "_nisps_ml_create","_nisps_ml_destroy","_nisps_ml_reset", + "_nisps_ml_create","_nisps_ml_destroy","_nisps_ml_reset","_nisps_ml_reshape", "_nisps_ml_set_input","_nisps_ml_process","_nisps_ml_outputs","_nisps_ml_infer_batch", "_nisps_ml_add_example","_nisps_ml_train","_nisps_ml_eval_loss", "_nisps_ml_clear_examples","_nisps_ml_example_count", diff --git a/tests/cpp/parity_wasm.mjs b/tests/cpp/parity_wasm.mjs index b8e75da..6ae21b2 100644 --- a/tests/cpp/parity_wasm.mjs +++ b/tests/cpp/parity_wasm.mjs @@ -112,7 +112,7 @@ function bind(Module) { feedbackLike: cwrap('nisps_ml_feedback_like', null, ['number']), feedbackCommitPlace: cwrap('nisps_ml_feedback_commit_place', null, ['number']), feedbackPlacedOutput: cwrap('nisps_ml_feedback_placed_output', 'number', ['number','number']), - describe: cwrap('nisps_ml_describe', null, ['number']), + describe: cwrap('nisps_ml_describe', null, ['number','number']), engineCreate: cwrap('nisps_engine_create', 'number', ['string','number']), engineDestroy: cwrap('nisps_engine_destroy', null, ['number']), @@ -186,9 +186,10 @@ async function main() { const Module = await loadWasm(); const api = bind(Module); - // Verify dimensions match the native side. + // Verify dimensions match the native side. A null handle reports the + // DEFAULT shape (what create() yields for non-positive args). const dimsBuf = api.malloc(6 * 4); - api.describe(dimsBuf); + api.describe(0, dimsBuf); const dims = new Int32Array(Module.HEAP32.buffer, dimsBuf, 6).slice(); api.free(dimsBuf); // Expect: [32, 10, 14, 18, 126, 4] (32-input max for mix-and-match) diff --git a/tests/cpp/test_mlp_storage_parity.cpp b/tests/cpp/test_mlp_storage_parity.cpp index d7f980e..c2ede32 100644 --- a/tests/cpp/test_mlp_storage_parity.cpp +++ b/tests/cpp/test_mlp_storage_parity.cpp @@ -12,6 +12,7 @@ #include "../../nisps/ml/dynamic_storage.hpp" #include "../../nisps/ml/mlp.hpp" +#include "../../nisps/ml/warm_start.hpp" #include "test_helpers.hpp" namespace { @@ -143,6 +144,76 @@ NISPS_TEST(mlp_dynamic_storage_invalid_dims_inert) { NISPS_EXPECT(bad.eval_loss() == 0.f); } +// warm_start_copy preserves the overlapping weight region across a reshape +// (grow AND shrink), and leaves the destination's fresh init outside it. +NISPS_TEST(mlp_warm_start_copy_overlap) { + // Source: 3→[10,14,18]→7 with a recognisable weight pattern. + FixedMLP src(kSeed); + { + const auto wf = src.get_weights(); + std::vector w(wf.begin(), wf.end()); + for (std::size_t i = 0; i < w.size(); ++i) { + w[i] = 0.001f * static_cast(i % 997u); + } + src.set_weights(w); + } + + // Grow: 5 inputs, 9 outputs (same hidden). Overlap = src's full matrix + // region per layer. + const std::size_t hidden[3] = {kH1, kH2, kH3}; + DynamicMLP grown(kSeed ^ 0x9E3779B9u, 5u, std::span(hidden), 9u); + NISPS_ASSERT(grown.valid()); + nisps::ml::warm_start_copy(grown, src); + + // Layer 0 rows: node < kH1, j < kIn must match; j >= kIn keeps fresh init. + { + auto sw = src.weights_l<0u>(); + auto gw = grown.weights_l<0u>(); + bool overlap_ok = true; + for (std::size_t node = 0; node < kH1 && overlap_ok; ++node) { + for (std::size_t j = 0; j < kIn; ++j) { + if (sw[node * kIn + j] != gw[node * 5u + j]) { overlap_ok = false; break; } + } + } + NISPS_EXPECT(overlap_ok); + } + // Final layer: node < kOut biases match; nodes kOut..8 keep fresh init. + { + auto sb = src.biases_l<3u>(); + auto gb = grown.biases_l<3u>(); + bool bias_ok = true; + for (std::size_t node = 0; node < kOut; ++node) { + if (sb[node] != gb[node]) { bias_ok = false; break; } + } + NISPS_EXPECT(bias_ok); + } + + // Shrink: 2 inputs, 4 outputs. Every dst weight must come from src. + DynamicMLP shrunk(kSeed ^ 0x51ED270Bu, 2u, std::span(hidden), 4u); + NISPS_ASSERT(shrunk.valid()); + nisps::ml::warm_start_copy(shrunk, src); + { + auto sw = src.weights_l<0u>(); + auto dw = shrunk.weights_l<0u>(); + bool ok = true; + for (std::size_t node = 0; node < kH1 && ok; ++node) { + for (std::size_t j = 0; j < 2u; ++j) { + if (sw[node * kIn + j] != dw[node * 2u + j]) { ok = false; break; } + } + } + NISPS_EXPECT(ok); + auto sw3 = src.weights_l<3u>(); + auto dw3 = shrunk.weights_l<3u>(); + ok = true; + for (std::size_t node = 0; node < 4u && ok; ++node) { + for (std::size_t j = 0; j < kH3; ++j) { + if (sw3[node * kH3 + j] != dw3[node * kH3 + j]) { ok = false; break; } + } + } + NISPS_EXPECT(ok); + } +} + // Moved-from dynamic instances stay inert; moved-to keeps working. NISPS_TEST(mlp_dynamic_storage_move_semantics) { DynamicMLP a = make_dynamic(kSeed);