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 10a4b0c..a9a19e4 100755 Binary files a/manifold/public/nisps.wasm and b/manifold/public/nisps.wasm differ 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);