diff --git a/manifold/public/nisps.js b/manifold/public/nisps.js index 54335cd..0c7ad29 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={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_feedback_dislike_geometric=Module["_nisps_ml_feedback_dislike_geometric"]=(a0,a1,a2)=>(_nisps_ml_feedback_dislike_geometric=Module["_nisps_ml_feedback_dislike_geometric"]=wasmExports["P"])(a0,a1,a2);var _nisps_ml_feedback_store_positive=Module["_nisps_ml_feedback_store_positive"]=(a0,a1)=>(_nisps_ml_feedback_store_positive=Module["_nisps_ml_feedback_store_positive"]=wasmExports["Q"])(a0,a1);var _nisps_ml_feedback_positive_count=Module["_nisps_ml_feedback_positive_count"]=a0=>(_nisps_ml_feedback_positive_count=Module["_nisps_ml_feedback_positive_count"]=wasmExports["R"])(a0);var _nisps_ml_feedback_negative_count=Module["_nisps_ml_feedback_negative_count"]=a0=>(_nisps_ml_feedback_negative_count=Module["_nisps_ml_feedback_negative_count"]=wasmExports["S"])(a0);var _nisps_ml_feedback_set_avoid_style=Module["_nisps_ml_feedback_set_avoid_style"]=(a0,a1)=>(_nisps_ml_feedback_set_avoid_style=Module["_nisps_ml_feedback_set_avoid_style"]=wasmExports["T"])(a0,a1);var _nisps_ml_jolt_press=Module["_nisps_ml_jolt_press"]=a0=>(_nisps_ml_jolt_press=Module["_nisps_ml_jolt_press"]=wasmExports["U"])(a0);var _nisps_ml_jolt_step=Module["_nisps_ml_jolt_step"]=a0=>(_nisps_ml_jolt_step=Module["_nisps_ml_jolt_step"]=wasmExports["V"])(a0);var _nisps_ml_jolt_release=Module["_nisps_ml_jolt_release"]=a0=>(_nisps_ml_jolt_release=Module["_nisps_ml_jolt_release"]=wasmExports["W"])(a0);var _nisps_ml_jolt_active=Module["_nisps_ml_jolt_active"]=a0=>(_nisps_ml_jolt_active=Module["_nisps_ml_jolt_active"]=wasmExports["X"])(a0);var _nisps_ml_jolt_lr_scale=Module["_nisps_ml_jolt_lr_scale"]=a0=>(_nisps_ml_jolt_lr_scale=Module["_nisps_ml_jolt_lr_scale"]=wasmExports["Y"])(a0);var _nisps_ml_jolt_tick_lr_ramp=Module["_nisps_ml_jolt_tick_lr_ramp"]=a0=>(_nisps_ml_jolt_tick_lr_ramp=Module["_nisps_ml_jolt_tick_lr_ramp"]=wasmExports["Z"])(a0);var _nisps_ml_explore_intensity=Module["_nisps_ml_explore_intensity"]=(a0,a1)=>(_nisps_ml_explore_intensity=Module["_nisps_ml_explore_intensity"]=wasmExports["_"])(a0,a1);var _nisps_ml_explore_get_intensity=Module["_nisps_ml_explore_get_intensity"]=a0=>(_nisps_ml_explore_get_intensity=Module["_nisps_ml_explore_get_intensity"]=wasmExports["$"])(a0);var _nisps_ml_explore_apply=Module["_nisps_ml_explore_apply"]=(a0,a1,a2)=>(_nisps_ml_explore_apply=Module["_nisps_ml_explore_apply"]=wasmExports["aa"])(a0,a1,a2);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["ba"])(a0,a1);var _nisps_ml_example_count=Module["_nisps_ml_example_count"]=a0=>(_nisps_ml_example_count=Module["_nisps_ml_example_count"]=wasmExports["ca"])(a0);var _nisps_ml_clear_examples=Module["_nisps_ml_clear_examples"]=a0=>(_nisps_ml_clear_examples=Module["_nisps_ml_clear_examples"]=wasmExports["da"])(a0);var _nisps_ml_reset=Module["_nisps_ml_reset"]=a0=>(_nisps_ml_reset=Module["_nisps_ml_reset"]=wasmExports["ea"])(a0);var _nisps_ml_describe=Module["_nisps_ml_describe"]=(a0,a1)=>(_nisps_ml_describe=Module["_nisps_ml_describe"]=wasmExports["fa"])(a0,a1);var _nisps_engine_create=Module["_nisps_engine_create"]=(a0,a1)=>(_nisps_engine_create=Module["_nisps_engine_create"]=wasmExports["ga"])(a0,a1);var _nisps_engine_destroy=Module["_nisps_engine_destroy"]=a0=>(_nisps_engine_destroy=Module["_nisps_engine_destroy"]=wasmExports["ha"])(a0);var _nisps_engine_set_params=Module["_nisps_engine_set_params"]=(a0,a1,a2)=>(_nisps_engine_set_params=Module["_nisps_engine_set_params"]=wasmExports["ia"])(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["ja"])(a0,a1,a2,a3,a4,a5);var _malloc=Module["_malloc"]=a0=>(_malloc=Module["_malloc"]=wasmExports["la"])(a0);var _free=Module["_free"]=a0=>(_free=Module["_free"]=wasmExports["ma"])(a0);var __emscripten_stack_restore=a0=>(__emscripten_stack_restore=wasmExports["na"])(a0);var __emscripten_stack_alloc=a0=>(__emscripten_stack_alloc=wasmExports["oa"])(a0);var _emscripten_stack_get_current=()=>(_emscripten_stack_get_current=wasmExports["pa"])();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_feedback_dislike_geometric=Module["_nisps_ml_feedback_dislike_geometric"]=(a0,a1,a2)=>(_nisps_ml_feedback_dislike_geometric=Module["_nisps_ml_feedback_dislike_geometric"]=wasmExports["P"])(a0,a1,a2);var _nisps_ml_feedback_store_positive=Module["_nisps_ml_feedback_store_positive"]=(a0,a1)=>(_nisps_ml_feedback_store_positive=Module["_nisps_ml_feedback_store_positive"]=wasmExports["Q"])(a0,a1);var _nisps_ml_feedback_positive_count=Module["_nisps_ml_feedback_positive_count"]=a0=>(_nisps_ml_feedback_positive_count=Module["_nisps_ml_feedback_positive_count"]=wasmExports["R"])(a0);var _nisps_ml_feedback_negative_count=Module["_nisps_ml_feedback_negative_count"]=a0=>(_nisps_ml_feedback_negative_count=Module["_nisps_ml_feedback_negative_count"]=wasmExports["S"])(a0);var _nisps_ml_feedback_set_avoid_style=Module["_nisps_ml_feedback_set_avoid_style"]=(a0,a1)=>(_nisps_ml_feedback_set_avoid_style=Module["_nisps_ml_feedback_set_avoid_style"]=wasmExports["T"])(a0,a1);var _nisps_ml_jolt_press=Module["_nisps_ml_jolt_press"]=a0=>(_nisps_ml_jolt_press=Module["_nisps_ml_jolt_press"]=wasmExports["U"])(a0);var _nisps_ml_jolt_step=Module["_nisps_ml_jolt_step"]=a0=>(_nisps_ml_jolt_step=Module["_nisps_ml_jolt_step"]=wasmExports["V"])(a0);var _nisps_ml_jolt_release=Module["_nisps_ml_jolt_release"]=a0=>(_nisps_ml_jolt_release=Module["_nisps_ml_jolt_release"]=wasmExports["W"])(a0);var _nisps_ml_jolt_active=Module["_nisps_ml_jolt_active"]=a0=>(_nisps_ml_jolt_active=Module["_nisps_ml_jolt_active"]=wasmExports["X"])(a0);var _nisps_ml_jolt_lr_scale=Module["_nisps_ml_jolt_lr_scale"]=a0=>(_nisps_ml_jolt_lr_scale=Module["_nisps_ml_jolt_lr_scale"]=wasmExports["Y"])(a0);var _nisps_ml_jolt_tick_lr_ramp=Module["_nisps_ml_jolt_tick_lr_ramp"]=a0=>(_nisps_ml_jolt_tick_lr_ramp=Module["_nisps_ml_jolt_tick_lr_ramp"]=wasmExports["Z"])(a0);var _nisps_ml_explore_intensity=Module["_nisps_ml_explore_intensity"]=(a0,a1)=>(_nisps_ml_explore_intensity=Module["_nisps_ml_explore_intensity"]=wasmExports["_"])(a0,a1);var _nisps_ml_explore_get_intensity=Module["_nisps_ml_explore_get_intensity"]=a0=>(_nisps_ml_explore_get_intensity=Module["_nisps_ml_explore_get_intensity"]=wasmExports["$"])(a0);var _nisps_ml_explore_apply=Module["_nisps_ml_explore_apply"]=(a0,a1,a2)=>(_nisps_ml_explore_apply=Module["_nisps_ml_explore_apply"]=wasmExports["aa"])(a0,a1,a2);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["ba"])(a0,a1);var _nisps_ml_example_count=Module["_nisps_ml_example_count"]=a0=>(_nisps_ml_example_count=Module["_nisps_ml_example_count"]=wasmExports["ca"])(a0);var _nisps_ml_clear_examples=Module["_nisps_ml_clear_examples"]=a0=>(_nisps_ml_clear_examples=Module["_nisps_ml_clear_examples"]=wasmExports["da"])(a0);var _nisps_ml_reset=Module["_nisps_ml_reset"]=a0=>(_nisps_ml_reset=Module["_nisps_ml_reset"]=wasmExports["ea"])(a0);var _nisps_ml_describe=Module["_nisps_ml_describe"]=(a0,a1)=>(_nisps_ml_describe=Module["_nisps_ml_describe"]=wasmExports["fa"])(a0,a1);var _nisps_pipeline_create=Module["_nisps_pipeline_create"]=()=>(_nisps_pipeline_create=Module["_nisps_pipeline_create"]=wasmExports["ga"])();var _nisps_pipeline_destroy=Module["_nisps_pipeline_destroy"]=a0=>(_nisps_pipeline_destroy=Module["_nisps_pipeline_destroy"]=wasmExports["ha"])(a0);var _nisps_input_set_config=Module["_nisps_input_set_config"]=(a0,a1,a2)=>(_nisps_input_set_config=Module["_nisps_input_set_config"]=wasmExports["ia"])(a0,a1,a2);var _nisps_input_process=Module["_nisps_input_process"]=(a0,a1,a2,a3,a4)=>(_nisps_input_process=Module["_nisps_input_process"]=wasmExports["ja"])(a0,a1,a2,a3,a4);var _nisps_input_reset=Module["_nisps_input_reset"]=a0=>(_nisps_input_reset=Module["_nisps_input_reset"]=wasmExports["ka"])(a0);var _nisps_output_set_config=Module["_nisps_output_set_config"]=(a0,a1,a2,a3,a4)=>(_nisps_output_set_config=Module["_nisps_output_set_config"]=wasmExports["la"])(a0,a1,a2,a3,a4);var _nisps_output_set_freeze_mask=Module["_nisps_output_set_freeze_mask"]=(a0,a1,a2)=>(_nisps_output_set_freeze_mask=Module["_nisps_output_set_freeze_mask"]=wasmExports["ma"])(a0,a1,a2);var _nisps_output_process=Module["_nisps_output_process"]=(a0,a1,a2,a3)=>(_nisps_output_process=Module["_nisps_output_process"]=wasmExports["na"])(a0,a1,a2,a3);var _nisps_output_reset=Module["_nisps_output_reset"]=a0=>(_nisps_output_reset=Module["_nisps_output_reset"]=wasmExports["oa"])(a0);var _nisps_pipeline_state_size=Module["_nisps_pipeline_state_size"]=a0=>(_nisps_pipeline_state_size=Module["_nisps_pipeline_state_size"]=wasmExports["pa"])(a0);var _nisps_pipeline_save_state=Module["_nisps_pipeline_save_state"]=(a0,a1)=>(_nisps_pipeline_save_state=Module["_nisps_pipeline_save_state"]=wasmExports["qa"])(a0,a1);var _nisps_pipeline_load_state=Module["_nisps_pipeline_load_state"]=(a0,a1,a2)=>(_nisps_pipeline_load_state=Module["_nisps_pipeline_load_state"]=wasmExports["ra"])(a0,a1,a2);var _nisps_curve_apply=Module["_nisps_curve_apply"]=(a0,a1,a2)=>(_nisps_curve_apply=Module["_nisps_curve_apply"]=wasmExports["sa"])(a0,a1,a2);var _nisps_curve_apply_batch=Module["_nisps_curve_apply_batch"]=(a0,a1,a2,a3,a4)=>(_nisps_curve_apply_batch=Module["_nisps_curve_apply_batch"]=wasmExports["ta"])(a0,a1,a2,a3,a4);var _nisps_engine_create=Module["_nisps_engine_create"]=(a0,a1)=>(_nisps_engine_create=Module["_nisps_engine_create"]=wasmExports["ua"])(a0,a1);var _nisps_engine_destroy=Module["_nisps_engine_destroy"]=a0=>(_nisps_engine_destroy=Module["_nisps_engine_destroy"]=wasmExports["va"])(a0);var _nisps_engine_set_params=Module["_nisps_engine_set_params"]=(a0,a1,a2)=>(_nisps_engine_set_params=Module["_nisps_engine_set_params"]=wasmExports["wa"])(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["xa"])(a0,a1,a2,a3,a4,a5);var _malloc=Module["_malloc"]=a0=>(_malloc=Module["_malloc"]=wasmExports["za"])(a0);var _free=Module["_free"]=a0=>(_free=Module["_free"]=wasmExports["Aa"])(a0);var __emscripten_stack_restore=a0=>(__emscripten_stack_restore=wasmExports["Ba"])(a0);var __emscripten_stack_alloc=a0=>(__emscripten_stack_alloc=wasmExports["Ca"])(a0);var _emscripten_stack_get_current=()=>(_emscripten_stack_get_current=wasmExports["Da"])();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 f15c4f3..89b6424 100755 Binary files a/manifold/public/nisps.wasm and b/manifold/public/nisps.wasm differ diff --git a/nisps/CMakeLists.txt b/nisps/CMakeLists.txt index b22d424..82037de 100644 --- a/nisps/CMakeLists.txt +++ b/nisps/CMakeLists.txt @@ -62,6 +62,7 @@ if(NOT EMSCRIPTEN) ${NISPS_TEST_DIR}/test_mlp_feedback.cpp ${NISPS_TEST_DIR}/test_mlp_geo_dislike.cpp ${NISPS_TEST_DIR}/test_mlp_serialize.cpp + ${NISPS_TEST_DIR}/test_pipeline.cpp ) target_link_libraries(nisps_core_tests PRIVATE nisps_core) diff --git a/nisps/core/math.hpp b/nisps/core/math.hpp index cc6c4a3..fe8f607 100644 --- a/nisps/core/math.hpp +++ b/nisps/core/math.hpp @@ -105,4 +105,21 @@ inline float apply_curve(Curve c, float x) noexcept { return x; // unreachable, silences -Wreturn-type } +// Centred power curve — pivots around 0.5 instead of 0 (from the legacy +// input/output pipelines; both nisps/pipeline chains use it): +// exponent < 1 → push toward the extremes +// exponent = 1 → identity +// exponent > 1 → pull toward the centre +// Parameterised, so it lives beside the Curve enum rather than inside it +// (schema param curves don't carry a parameter). +inline float centered_power(float x, float exponent) noexcept { + if (exponent == 1.f) return clamp01(x); + const float offset = x - 0.5f; + const float sign = (offset < 0.f) ? -1.f : 1.f; + // Range [-0.5, 0.5] → [-1, 1] for the power op, then halve back. + const float shaped = + sign * std::pow(std::fabs(offset) * 2.f, exponent) * 0.5f; + return clamp01(shaped + 0.5f); +} + } // namespace nisps diff --git a/nisps/pipeline/input_chain.hpp b/nisps/pipeline/input_chain.hpp new file mode 100644 index 0000000..bbf4981 --- /dev/null +++ b/nisps/pipeline/input_chain.hpp @@ -0,0 +1,273 @@ +// nisps/pipeline/input_chain.hpp — the 2-axis input-processing chain +// (one-core-engine-refactor P4). Faithful C++ port of the retired +// manifold/src/engine/input-pipeline.ts (itself a bit-for-bit port of the +// legacy js/ui/input-pipeline.js), which is the behaviour contract pinned by +// manifold/tests/fixtures/input-pipeline-golden.json. +// +// Stages (in order), each axis in [0,1]: +// 0. Invert (per-axis flip) +// 1. Deadzone (suppress jitter near centre, remap live zone to [0,1]) +// 2. Circular clamp (constrain to unit disk centred at 0.5,0.5) +// 3. Zoom (narrow window around anchor, modulated by momentum) +// 4. Centred power curve (per-axis exponent) +// 5. EMA smoothing (frame-rate-independent) +// 6. Momentum-as-zoom update (consumed next frame) +// +// TIME MODEL: the caller passes dt in SECONDS per call; the chain accumulates +// its own clock for the momentum velocity window (the TS original read +// performance.now() — the fixtures pin the equivalent clock contract). No +// wall clock in core: fully deterministic. +// +// PERF CONTRACT: no heap, no virtual dispatch, `.f` literals, fixed-capacity +// velocity ring. Control-rate (per pointer event / per control tick), not the +// audio ISR. + +#pragma once + +#include +#include +#include +#include + +#include "../core/math.hpp" +#include "../core/perf.hpp" + +namespace nisps::pipeline { + +inline constexpr float kZoomMin = 0.01f; +inline constexpr float kZoomMax = 1.0f; +inline constexpr float kFreezeThreshold = kZoomMin; +inline constexpr float kReferenceDt = 1.f / 60.f; +inline constexpr float kVelocityWindowDefaultS = 0.150f; + +// Sentinel for "null" per-axis overrides (valid zooms are [0.01, 1], valid +// curves [0.2, 5] — zero is outside both ranges). +inline constexpr float kUnsetOverride = 0.f; + +enum class AnchorMode : std::uint8_t { Auto = 0, Sticky = 1, Center = 2 }; +enum class MomentumMode : std::uint8_t { Off = 0, Gentle = 1, Strong = 2 }; + +struct InputChainConfig { + float zoom = 1.0f; // [0.01, 1] + float zoom_x = kUnsetOverride; // 0 ⇒ use zoom + float zoom_y = kUnsetOverride; + float anchor_x = 0.5f; + float anchor_y = 0.5f; + AnchorMode anchor_mode = AnchorMode::Center; + float deadzone = 0.f; // [0, 0.4] + float input_curve = 1.0f; // [0.2, 5] + float curve_x = kUnsetOverride; // 0 ⇒ use input_curve + float curve_y = kUnsetOverride; + float smoothing = 0.f; // [0, 0.95] + MomentumMode momentum_mode = MomentumMode::Off; + float velocity_window_s = kVelocityWindowDefaultS; + bool invert_x = false; + bool invert_y = false; +}; + +struct InputChainResult { + float x; + float y; + bool frozen; +}; + +class InputChain { + public: + // Velocity-history capacity. The TS original kept an unbounded window- + // trimmed list; at real pointer rates (≤240 Hz) a 150 ms window holds + // ≤36 entries. When full, the oldest entry is dropped (it would be the + // first trimmed anyway). + static constexpr std::size_t kHistoryCap = 64u; + + InputChain() noexcept = default; + + void set_config(const InputChainConfig& c) noexcept { cfg_ = c; } + const InputChainConfig& config() const noexcept { return cfg_; } + + void reset() noexcept { + smoothed_x_ = 0.5f; + smoothed_y_ = 0.5f; + momentum_multiplier_ = 1.f; + frozen_ = false; + now_s_ = 0.f; + hist_count_ = 0u; + hist_head_ = 0u; + } + + // Serialisable per-instance state (persistence): [smoothed_x, smoothed_y, + // momentum_multiplier]. The velocity history is transient by design. + static constexpr std::size_t state_size() noexcept { return 3u; } + void save_state(std::span out) const noexcept { + if (out.size() < state_size()) return; + out[0] = smoothed_x_; + out[1] = smoothed_y_; + out[2] = momentum_multiplier_; + } + void load_state(std::span in) noexcept { + if (in.size() < state_size()) return; + smoothed_x_ = in[0]; + smoothed_y_ = in[1]; + momentum_multiplier_ = in[2]; + hist_count_ = 0u; + hist_head_ = 0u; + } + + // Process one raw 2D sample. `dt_s` = seconds since the previous call + // (clamped at 0; 0 falls back to the 1/60 reference inside smoothing). + InputChainResult process(float raw_x, float raw_y, float dt_s) noexcept { + const float safe_dt = (dt_s > 0.f) ? dt_s : 0.f; + now_s_ += safe_dt; + + const float base_zoom_x = (cfg_.zoom_x != kUnsetOverride) ? cfg_.zoom_x : cfg_.zoom; + const float base_zoom_y = (cfg_.zoom_y != kUnsetOverride) ? cfg_.zoom_y : cfg_.zoom; + + const bool frozen_x = base_zoom_x <= kFreezeThreshold; + const bool frozen_y = base_zoom_y <= kFreezeThreshold; + if (frozen_x && frozen_y) { + frozen_ = true; + return {smoothed_x_, smoothed_y_, true}; + } + + // 0. Invert + float x = cfg_.invert_x ? (1.f - raw_x) : raw_x; + float y = cfg_.invert_y ? (1.f - raw_y) : raw_y; + + // 1. Deadzone + x = apply_deadzone_(x, cfg_.deadzone); + y = apply_deadzone_(y, cfg_.deadzone); + + // 2. Circular clamp to the unit disk centred at (0.5, 0.5) + { + const float cx = x - 0.5f; + const float cy = y - 0.5f; + const float dist = std::sqrt(cx * cx + cy * cy); + if (dist > 0.5f && dist > 1e-12f) { + const float scale = 0.5f / dist; + x = 0.5f + cx * scale; + y = 0.5f + cy * scale; + } + } + + // 3. Zoom around the anchor (with momentum modulation) + const float anchor_x = (cfg_.anchor_mode == AnchorMode::Center) ? 0.5f : cfg_.anchor_x; + const float anchor_y = (cfg_.anchor_mode == AnchorMode::Center) ? 0.5f : cfg_.anchor_y; + const float eff_zoom_x = frozen_x + ? kFreezeThreshold + : nisps::clamp(base_zoom_x * momentum_multiplier_, kZoomMin, kZoomMax); + const float eff_zoom_y = frozen_y + ? kFreezeThreshold + : nisps::clamp(base_zoom_y * momentum_multiplier_, kZoomMin, kZoomMax); + x = frozen_x ? smoothed_x_ : apply_zoom_(x, anchor_x, eff_zoom_x); + y = frozen_y ? smoothed_y_ : apply_zoom_(y, anchor_y, eff_zoom_y); + + // 4. Centred power curve + const float curve_x = (cfg_.curve_x != kUnsetOverride) ? cfg_.curve_x : cfg_.input_curve; + const float curve_y = (cfg_.curve_y != kUnsetOverride) ? cfg_.curve_y : cfg_.input_curve; + if (!frozen_x) x = nisps::centered_power(x, curve_x); + if (!frozen_y) y = nisps::centered_power(y, curve_y); + + // 5. EMA smoothing + if (!frozen_x) smoothed_x_ = ema_smooth_(smoothed_x_, x, cfg_.smoothing, safe_dt); + if (!frozen_y) smoothed_y_ = ema_smooth_(smoothed_y_, y, cfg_.smoothing, safe_dt); + + // 6. Update momentum-zoom for the next frame (uses the RAW sample, + // pre-pipeline, like the TS original). + update_momentum_(raw_x, raw_y, safe_dt); + + frozen_ = false; + return {smoothed_x_, smoothed_y_, false}; + } + + bool frozen() const noexcept { return frozen_; } + float momentum_multiplier() const noexcept { return momentum_multiplier_; } + + private: + static float apply_deadzone_(float input, float deadzone) noexcept { + if (deadzone <= 0.f) return input; + const float offset = input - 0.5f; + const float abs_off = std::fabs(offset); + const float half_dz = deadzone * 0.5f; + if (abs_off <= half_dz) return 0.5f; + const float sign = (offset < 0.f) ? -1.f : 1.f; + const float remapped = ((abs_off - half_dz) / (0.5f - half_dz)) * 0.5f; + return 0.5f + sign * remapped; + } + + static float apply_zoom_(float input, float anchor, float zoom_level) noexcept { + return nisps::clamp(anchor + (input - 0.5f) * zoom_level, 0.f, 1.f); + } + + static float ema_smooth_(float prev, float raw, float smoothing, float dt) noexcept { + if (smoothing <= 0.f) return raw; + const float effective_dt = (dt > 0.f) ? dt : kReferenceDt; + const float alpha = 1.f - smoothing; + const float alpha_eff = 1.f - std::pow(1.f - alpha, effective_dt / kReferenceDt); + return prev + alpha_eff * (raw - prev); + } + + void update_momentum_(float raw_x, float raw_y, float dt) noexcept { + float factor, min_mul, max_mul; + switch (cfg_.momentum_mode) { + case MomentumMode::Gentle: factor = 0.6f; min_mul = 0.3f; max_mul = 1.0f; break; + case MomentumMode::Strong: factor = 1.5f; min_mul = 0.15f; max_mul = 1.0f; break; + case MomentumMode::Off: + default: + momentum_multiplier_ = 1.f; + hist_count_ = 0u; + hist_head_ = 0u; + return; + } + + // Trim entries older than the window, then append (bounded ring). + const float window = cfg_.velocity_window_s; + while (hist_count_ > 0u) { + const HistEntry& oldest = hist_[hist_head_]; + if (now_s_ - oldest.t <= window) break; + hist_head_ = (hist_head_ + 1u) % kHistoryCap; + --hist_count_; + } + if (hist_count_ == kHistoryCap) { + hist_head_ = (hist_head_ + 1u) % kHistoryCap; + --hist_count_; + } + hist_[(hist_head_ + hist_count_) % kHistoryCap] = {raw_x, raw_y, now_s_}; + ++hist_count_; + + if (hist_count_ < 2u) { + momentum_multiplier_ = 1.f; + return; + } + const HistEntry& a = hist_[hist_head_]; + const HistEntry& b = hist_[(hist_head_ + hist_count_ - 1u) % kHistoryCap]; + const float dt_hist = b.t - a.t; + if (dt_hist <= 0.f) return; // keep the previous multiplier + const float dx = b.x - a.x; + const float dy = b.y - a.y; + const float dist = std::sqrt(dx * dx + dy * dy); + const float speed = dist / dt_hist; // [0,1]-space units per second + const float norm_speed = nisps::clamp(speed * factor, 0.f, 1.f); + // Higher speed → smaller multiplier (zoom out faster movements). + const float target = max_mul - (max_mul - min_mul) * norm_speed; + // Smooth toward the target so the zoom doesn't jitter. + const float smooth_coeff = nisps::clamp(dt * 6.f, 0.f, 1.f); + momentum_multiplier_ += smooth_coeff * (target - momentum_multiplier_); + } + + struct HistEntry { + float x; + float y; + float t; + }; + + InputChainConfig cfg_{}; + float smoothed_x_ = 0.5f; + float smoothed_y_ = 0.5f; + float momentum_multiplier_ = 1.f; + bool frozen_ = false; + float now_s_ = 0.f; + HistEntry hist_[kHistoryCap]{}; + std::size_t hist_head_ = 0u; + std::size_t hist_count_ = 0u; +}; + +} // namespace nisps::pipeline diff --git a/nisps/pipeline/output_chain.hpp b/nisps/pipeline/output_chain.hpp new file mode 100644 index 0000000..8dfeb79 --- /dev/null +++ b/nisps/pipeline/output_chain.hpp @@ -0,0 +1,162 @@ +// nisps/pipeline/output_chain.hpp — the per-output processing chain +// (one-core-engine-refactor P4). Faithful C++ port of the retired +// manifold/src/engine/output-pipeline.ts, the behaviour contract pinned by +// manifold/tests/fixtures/output-pipeline-golden.json. +// +// Stages (in order) for each output: +// 1. Global power curve (raw^exponent, exponent in [0.2, 5.0]) +// 2. Per-output EMA smoothing (frame-rate-independent) +// 3. Slew-rate limiting (max change per second per output) +// 4. Freeze gate (global) and per-output freeze mask +// +// PERF CONTRACT: no heap — capacity is a template parameter (browser +// bindings instantiate a large cap; firmware would pick its mode's NOut). +// Control-rate. `.f` literals, no virtual dispatch. + +#pragma once + +#include +#include +#include +#include + +#include "../core/math.hpp" +#include "../core/perf.hpp" + +namespace nisps::pipeline { + +inline constexpr float kOutputReferenceDt = 1.f / 60.f; +// Slew sentinel: any value <= 0 means "unlimited" (the TS Infinity default). +inline constexpr float kSlewUnlimited = 0.f; + +struct OutputChainConfig { + float global_curve = 1.0f; // [0.2, 5]; 1 = linear + float smoothing = 0.f; // [0, 0.95] + float slew_rate = kSlewUnlimited; // change/sec; <= 0 ⇒ unlimited + bool freeze_output = false; // global freeze gate +}; + +template +class OutputChain { + public: + static constexpr std::size_t kMaxOutputs = NMax; + + OutputChain() noexcept = default; + + void set_config(const OutputChainConfig& c) noexcept { cfg_ = c; } + const OutputChainConfig& config() const noexcept { return cfg_; } + + // Per-output freeze mask (1 = frozen). Empty span clears the mask. + void set_freeze_mask(std::span mask) noexcept { + mask_count_ = (mask.size() < NMax) ? mask.size() : NMax; + for (std::size_t i = 0; i < mask_count_; ++i) freeze_mask_[i] = mask[i]; + } + void clear_freeze_mask() noexcept { mask_count_ = 0u; } + + void reset() noexcept { + seeded_count_ = 0u; + } + + // Serialisable state: [count, prev..., smoothed...]. + std::size_t state_size() const noexcept { return 1u + 2u * seeded_count_; } + void save_state(std::span out) const noexcept { + if (out.size() < state_size()) return; + out[0] = static_cast(seeded_count_); + for (std::size_t i = 0; i < seeded_count_; ++i) { + out[1u + i] = prev_[i]; + out[1u + seeded_count_ + i] = smoothed_[i]; + } + } + void load_state(std::span in) noexcept { + if (in.empty()) return; + std::size_t n = static_cast(in[0]); + if (n > NMax) n = NMax; + if (in.size() < 1u + 2u * n) return; + seeded_count_ = n; + for (std::size_t i = 0; i < n; ++i) { + prev_[i] = in[1u + i]; + smoothed_[i] = in[1u + n + i]; + } + } + + // Process `raw` (n ≤ NMax) into `out` (may alias `raw`). `dt_s` = seconds + // since the previous call. + void process(std::span raw, std::span out, float dt_s) noexcept { + std::size_t n = raw.size(); + if (n > NMax) n = NMax; + if (out.size() < n) return; + const float dt = (dt_s > 0.f) ? dt_s : 0.f; + + // (Re)seed prev/smoothed from raw on first call or length change — + // matches the TS null/length-mismatch reseed. + if (seeded_count_ != n) { + for (std::size_t i = 0; i < n; ++i) { + const float r = nisps::clamp01(raw[i]); + prev_[i] = r; + smoothed_[i] = r; + } + seeded_count_ = n; + } + + if (cfg_.freeze_output) { + // Output frozen: hold prior values. + for (std::size_t i = 0; i < n; ++i) out[i] = prev_[i]; + return; + } + + const float exp = cfg_.global_curve; + const bool slew_on = cfg_.slew_rate > 0.f; + const float max_delta = slew_on ? cfg_.slew_rate * dt : 0.f; + + for (std::size_t i = 0; i < n; ++i) { + const float r = nisps::clamp01(raw[i]); + const float curved = (exp == 1.0f) ? r : std::pow(r, exp); + + // Per-output freeze + if (i < mask_count_ && freeze_mask_[i] != 0u) { + out[i] = prev_[i]; + continue; + } + + // Stage 2: EMA smoothing + float value = ema_smooth_(smoothed_[i], curved, cfg_.smoothing, dt); + smoothed_[i] = value; + + // Stage 3: slew-rate limit + if (slew_on) { + const float delta = value - prev_[i]; + if (std::fabs(delta) > max_delta) { + const float sign = (delta < 0.f) ? -1.f : 1.f; + value = prev_[i] + sign * max_delta; + } + } + + out[i] = nisps::clamp01(value); + } + + // Update prev for the next call (frozen dims hold their prev). + for (std::size_t i = 0; i < n; ++i) { + if (i < mask_count_ && freeze_mask_[i] != 0u) continue; + prev_[i] = out[i]; + } + } + + private: + static float ema_smooth_(float prev, float raw, float smoothing, float dt) noexcept { + if (smoothing <= 0.f) return raw; + const float effective_dt = (dt > 0.f) ? dt : kOutputReferenceDt; + const float alpha = 1.f - smoothing; + const float alpha_eff = + 1.f - std::pow(1.f - alpha, effective_dt / kOutputReferenceDt); + return prev + alpha_eff * (raw - prev); + } + + OutputChainConfig cfg_{}; + std::uint8_t freeze_mask_[NMax]{}; + std::size_t mask_count_ = 0u; + float prev_[NMax]{}; + float smoothed_[NMax]{}; + std::size_t seeded_count_ = 0u; +}; + +} // namespace nisps::pipeline diff --git a/nisps/wasm/bindings.cpp b/nisps/wasm/bindings.cpp index 7bea365..691e2e6 100644 --- a/nisps/wasm/bindings.cpp +++ b/nisps/wasm/bindings.cpp @@ -62,6 +62,7 @@ #include "../engines/xiasri.hpp" // ML. +#include "../core/math.hpp" #include "../core/types.hpp" #include "../ml/dynamic_storage.hpp" #include "../ml/feedback.hpp" @@ -71,6 +72,10 @@ #include "../ml/stats.hpp" #include "../ml/warm_start.hpp" +// Pipelines (one-core-engine P4). +#include "../pipeline/input_chain.hpp" +#include "../pipeline/output_chain.hpp" + namespace { // --------------------------------------------------------------------------- @@ -179,6 +184,17 @@ struct MLHandle { std::size_t n_out() const noexcept { return mlp.n_out(); } }; +// --------------------------------------------------------------------------- +// Pipeline side (one-core-engine P4): the input/output processing chains, +// state C++-side per handle. The output chain is capacity-templated; the +// browser instantiates the kMaxDim cap. +// --------------------------------------------------------------------------- + +struct PipelineHandle { + nisps::pipeline::InputChain input; + nisps::pipeline::OutputChain output; +}; + // --------------------------------------------------------------------------- // Engine side // --------------------------------------------------------------------------- @@ -946,6 +962,164 @@ void nisps_ml_describe(void* ml, int* out_dims) { out_dims[5] = static_cast(BrowserMLP::kNumLayers); } +// --------------------------------------------------------------------------- +// Pipelines (one-core-engine P4). Input chain: the 2-axis pad pipeline. +// Output chain: curve → EMA → slew → freeze over the routed vector. State +// lives C++-side per handle; the TS wrappers are thin. +// +// INPUT CONFIG WIRE LAYOUT (nisps_input_set_config, 15 floats — keep in +// lockstep with manifold/src/engine wrappers): +// [0] zoom [1] zoomX (0=null) [2] zoomY (0=null) +// [3] anchorX [4] anchorY [5] anchorMode (0 auto/1 sticky/2 centre) +// [6] deadzone [7] inputCurve [8] curveX (0=null) +// [9] curveY (0=null) [10] smoothing [11] momentumMode (0 off/1 gentle/2 strong) +// [12] velocityWindow (SECONDS) [13] invertX (!=0) [14] invertY (!=0) +// --------------------------------------------------------------------------- + +EMSCRIPTEN_KEEPALIVE +void* nisps_pipeline_create(void) { + return static_cast(new PipelineHandle()); +} + +EMSCRIPTEN_KEEPALIVE +void nisps_pipeline_destroy(void* p) { + if (!p) return; + delete static_cast(p); +} + +EMSCRIPTEN_KEEPALIVE +void nisps_input_set_config(void* p, const float* cfg, int n) { + if (!p || !cfg || n < 15) return; + auto* h = static_cast(p); + nisps::pipeline::InputChainConfig c; + c.zoom = cfg[0]; + c.zoom_x = cfg[1]; + c.zoom_y = cfg[2]; + c.anchor_x = cfg[3]; + c.anchor_y = cfg[4]; + c.anchor_mode = static_cast( + static_cast(cfg[5]) == 1 ? 1 : (static_cast(cfg[5]) == 2 ? 2 : 0)); + c.deadzone = cfg[6]; + c.input_curve = cfg[7]; + c.curve_x = cfg[8]; + c.curve_y = cfg[9]; + c.smoothing = cfg[10]; + c.momentum_mode = static_cast( + static_cast(cfg[11]) == 1 ? 1 : (static_cast(cfg[11]) == 2 ? 2 : 0)); + c.velocity_window_s = cfg[12]; + c.invert_x = cfg[13] != 0.f; + c.invert_y = cfg[14] != 0.f; + h->input.set_config(c); +} + +// Returns 1 when the chain is frozen (both axes at the freeze threshold). +EMSCRIPTEN_KEEPALIVE +int nisps_input_process(void* p, float x, float y, float dt_s, float* out_xy) { + if (!p || !out_xy) return 0; + auto* h = static_cast(p); + const auto r = h->input.process(x, y, dt_s); + out_xy[0] = r.x; + out_xy[1] = r.y; + return r.frozen ? 1 : 0; +} + +EMSCRIPTEN_KEEPALIVE +void nisps_input_reset(void* p) { + if (!p) return; + static_cast(p)->input.reset(); +} + +EMSCRIPTEN_KEEPALIVE +void nisps_output_set_config(void* p, float global_curve, float smoothing, + float slew_rate, int freeze) { + if (!p) return; + auto* h = static_cast(p); + nisps::pipeline::OutputChainConfig c; + c.global_curve = global_curve; + c.smoothing = smoothing; + c.slew_rate = slew_rate; // <= 0 ⇒ unlimited (the TS Infinity default) + c.freeze_output = freeze != 0; + h->output.set_config(c); +} + +EMSCRIPTEN_KEEPALIVE +void nisps_output_set_freeze_mask(void* p, const uint8_t* mask, int n) { + if (!p) return; + auto* h = static_cast(p); + if (!mask || n <= 0) { + h->output.clear_freeze_mask(); + return; + } + h->output.set_freeze_mask( + std::span(mask, static_cast(n))); +} + +// In-place: processes the first n floats of `inout`. +EMSCRIPTEN_KEEPALIVE +void nisps_output_process(void* p, float* inout, int n, float dt_s) { + if (!p || !inout || n <= 0) return; + auto* h = static_cast(p); + const std::size_t count = static_cast(n); + h->output.process(std::span(inout, count), + std::span(inout, count), dt_s); +} + +EMSCRIPTEN_KEEPALIVE +void nisps_output_reset(void* p) { + if (!p) return; + static_cast(p)->output.reset(); +} + +// Persistence: [input state (3)] + [output state (1 + 2*count)]. +EMSCRIPTEN_KEEPALIVE +int nisps_pipeline_state_size(void* p) { + if (!p) return 0; + auto* h = static_cast(p); + return static_cast(h->input.state_size() + h->output.state_size()); +} + +EMSCRIPTEN_KEEPALIVE +void nisps_pipeline_save_state(void* p, float* out) { + if (!p || !out) return; + auto* h = static_cast(p); + const std::size_t in_n = h->input.state_size(); + h->input.save_state(std::span(out, in_n)); + h->output.save_state(std::span(out + in_n, h->output.state_size())); +} + +EMSCRIPTEN_KEEPALIVE +void nisps_pipeline_load_state(void* p, const float* in, int n) { + if (!p || !in || n <= 0) return; + auto* h = static_cast(p); + const std::size_t in_n = h->input.state_size(); + const std::size_t total = static_cast(n); + if (total < in_n) return; + h->input.load_state(std::span(in, in_n)); + h->output.load_state(std::span(in + in_n, total - in_n)); +} + +// --------------------------------------------------------------------------- +// Curve catalog (one-core-engine P4): nisps/core/math.hpp is the single +// source of truth; the browser samples it instead of mirroring the maths. +// ids 0..6 = nisps::Curve (param ignored); id 7 = centred power (param = +// exponent). UI curve previews render by sampling the batch call. +// --------------------------------------------------------------------------- + +EMSCRIPTEN_KEEPALIVE +float nisps_curve_apply(int id, float x, float param) { + if (id == 7) return nisps::centered_power(x, param); + if (id < 0 || id > 6) return x; + return nisps::apply_curve(static_cast(id), nisps::clamp01(x)); +} + +EMSCRIPTEN_KEEPALIVE +void nisps_curve_apply_batch(int id, const float* xs, float* out, int n, float param) { + if (!xs || !out || n <= 0) return; + for (int i = 0; i < n; ++i) { + out[i] = nisps_curve_apply(id, xs[i], param); + } +} + // --------------------------------------------------------------------------- // Engine lifecycle // --------------------------------------------------------------------------- diff --git a/scripts/build-wasm.sh b/scripts/build-wasm.sh index 80d9d29..89e2239 100755 --- a/scripts/build-wasm.sh +++ b/scripts/build-wasm.sh @@ -53,6 +53,12 @@ EXPORTED_FUNCS='[ "_nisps_ml_jolt_press","_nisps_ml_jolt_step","_nisps_ml_jolt_release", "_nisps_ml_jolt_active","_nisps_ml_jolt_lr_scale","_nisps_ml_jolt_tick_lr_ramp", "_nisps_ml_explore_intensity","_nisps_ml_explore_get_intensity","_nisps_ml_explore_apply", + "_nisps_pipeline_create","_nisps_pipeline_destroy", + "_nisps_input_set_config","_nisps_input_process","_nisps_input_reset", + "_nisps_output_set_config","_nisps_output_set_freeze_mask", + "_nisps_output_process","_nisps_output_reset", + "_nisps_pipeline_state_size","_nisps_pipeline_save_state","_nisps_pipeline_load_state", + "_nisps_curve_apply","_nisps_curve_apply_batch", "_nisps_ml_get_layer_stats","_nisps_ml_describe", "_nisps_engine_create","_nisps_engine_destroy", "_nisps_engine_set_params","_nisps_engine_process_block" diff --git a/tests/cpp/parity_check.cpp b/tests/cpp/parity_check.cpp index 2b93e85..d13ba88 100644 --- a/tests/cpp/parity_check.cpp +++ b/tests/cpp/parity_check.cpp @@ -55,10 +55,13 @@ #include #include +#include "../../nisps/core/math.hpp" #include "../../nisps/engines/channel_strip.hpp" #include "../../nisps/engines/paf_synth.hpp" #include "../../nisps/ml/feedback.hpp" #include "../../nisps/ml/mlp.hpp" +#include "../../nisps/pipeline/input_chain.hpp" +#include "../../nisps/pipeline/output_chain.hpp" namespace { @@ -83,7 +86,7 @@ constexpr std::array kProbeIdx = { }; constexpr std::uint32_t kMagic = 0x5450524Eu; // 'NPRT' -constexpr std::uint32_t kVersion = 4u; // v4 adds stage 6 (geometric dislike) +constexpr std::uint32_t kVersion = 5u; // v5 adds stage 7 (pipelines + curves) // Must match the salt in nisps/wasm/bindings.cpp MLHandle so the controller's // static-output RNG stream is identical native ↔ WASM. @@ -324,6 +327,85 @@ int main(int argc, char** argv) { } } + // ---- Stage 7: pipelines + curves (one-core-engine P4) ---- + // Deterministic rational traces (no transcendentals crossing the JS/C++ + // boundary) through the input chain (2 configs), the output chain + // (2 configs), and the curve catalog. parity_wasm.mjs drives the same + // sequences through the C ABI. + { + // -- input chain: default config, then a loaded config -- + auto run_input = [&payload](const nisps::pipeline::InputChainConfig& cfg) { + nisps::pipeline::InputChain ch; + ch.set_config(cfg); + const float dt = 1.f / 120.f; + for (int i = 0; i < 120; ++i) { + const float x = static_cast((i * 37) % 97) / 96.f; + const float y = static_cast((i * 53 + 11) % 89) / 88.f; + const auto r = ch.process(x, y, dt); + if (i % 10 == 9) { + payload.push_back(r.x); + payload.push_back(r.y); + } + } + }; + run_input(nisps::pipeline::InputChainConfig{}); + { + nisps::pipeline::InputChainConfig c; + c.zoom = 0.7f; + c.deadzone = 0.1f; + c.input_curve = 1.8f; + c.smoothing = 0.6f; + c.momentum_mode = nisps::pipeline::MomentumMode::Strong; + c.invert_x = true; + run_input(c); + } + + // -- output chain: default, then loaded config with a freeze mask -- + auto run_output = [&payload](const nisps::pipeline::OutputChainConfig& cfg, + bool with_mask) { + nisps::pipeline::OutputChain<16u> ch; + ch.set_config(cfg); + if (with_mask) { + std::uint8_t mask[16u]; + for (std::size_t j = 0; j < 16u; ++j) { + mask[j] = (j % 2u == 0u) ? 1u : 0u; + } + ch.set_freeze_mask(mask); + } + const float dt = 1.f / 60.f; + float vec[16u]; + float out[16u]; + for (int i = 0; i < 60; ++i) { + for (std::size_t j = 0; j < 16u; ++j) { + vec[j] = static_cast((i * 13 + static_cast(j) * 29) % 101) / 100.f; + } + ch.process(std::span(vec), std::span(out), dt); + if (i % 15 == 14) { + for (std::size_t j = 0; j < 16u; ++j) payload.push_back(out[j]); + } + } + }; + run_output(nisps::pipeline::OutputChainConfig{}, false); + { + nisps::pipeline::OutputChainConfig c; + c.global_curve = 2.2f; + c.smoothing = 0.5f; + c.slew_rate = 2.0f; + run_output(c, true); + } + + // -- curve catalog: ids 0..6 (enum) + centred power (param 1.7) -- + for (int id = 0; id <= 7; ++id) { + for (int i = 0; i <= 16; ++i) { + const float x = static_cast(i) / 16.f; + const float v = (id == 7) + ? nisps::centered_power(x, 1.7f) + : nisps::apply_curve(static_cast(id), x); + payload.push_back(v); + } + } + } + // ---- Sanity: every value finite ---- for (std::size_t i = 0; i < payload.size(); ++i) { if (!std::isfinite(payload[i])) { diff --git a/tests/cpp/parity_diff.mjs b/tests/cpp/parity_diff.mjs index b843a47..885f2af 100644 --- a/tests/cpp/parity_diff.mjs +++ b/tests/cpp/parity_diff.mjs @@ -16,7 +16,7 @@ import { readFile } from 'node:fs/promises'; const MAGIC = 0x5450524e; -const VERSION = 4; // v4 adds stage 6 (geometric dislike) +const VERSION = 5; // v5 adds stage 7 (pipelines + curves) const DEFAULT_TOL = 1e-5; // Layout for context in error messages — must match parity_check.cpp / parity_wasm.mjs. diff --git a/tests/cpp/parity_wasm.mjs b/tests/cpp/parity_wasm.mjs index b0cc675..91dd4a3 100644 --- a/tests/cpp/parity_wasm.mjs +++ b/tests/cpp/parity_wasm.mjs @@ -38,7 +38,7 @@ const __dirname = dirname(__filename); const repoRoot = resolve(__dirname, '..', '..'); const MAGIC = 0x5450524e; // 'NPRT' -const VERSION = 4; // v4 adds stage 6 (geometric dislike) +const VERSION = 5; // v5 adds stage 7 (pipelines + curves) const SEED = 42 >>> 0; const INPUT_X = 0.25; @@ -116,6 +116,15 @@ function bind(Module) { feedbackNegativeCount: cwrap('nisps_ml_feedback_negative_count', 'number', ['number']), describe: cwrap('nisps_ml_describe', null, ['number','number']), + pipelineCreate: cwrap('nisps_pipeline_create', 'number', []), + pipelineDestroy: cwrap('nisps_pipeline_destroy', null, ['number']), + inputSetConfig: cwrap('nisps_input_set_config', null, ['number','number','number']), + inputProcess: cwrap('nisps_input_process', 'number', ['number','number','number','number','number']), + outputSetConfig: cwrap('nisps_output_set_config', null, ['number','number','number','number','number']), + outputSetFreezeMask: cwrap('nisps_output_set_freeze_mask', null, ['number','number','number']), + outputProcess: cwrap('nisps_output_process', null, ['number','number','number','number']), + curveApply: cwrap('nisps_curve_apply', 'number', ['number','number','number']), + engineCreate: cwrap('nisps_engine_create', 'number', ['string','number']), engineDestroy: cwrap('nisps_engine_destroy', null, ['number']), engineSetParams: cwrap('nisps_engine_set_params', null, ['number','number','number']), @@ -379,6 +388,78 @@ async function main() { api.destroy(ml); + // --- Stage 7: pipelines + curves (one-core-engine P4) --- + // Mirrors parity_check.cpp stage 7: rational traces (bit-exact across the + // JS/C++ boundary) through the input chain, output chain, and curve + // catalog via the C ABI. + const pipelineFloats = []; + { + const outXY = api.malloc(8); + const cfgBuf = api.malloc(15 * 4); + + // Input-config wire layout (see bindings.cpp): [zoom, zoomX, zoomY, + // anchorX, anchorY, anchorMode, deadzone, inputCurve, curveX, curveY, + // smoothing, momentumMode, velocityWindowS, invertX, invertY] + const runInput = (cfg) => { + const p = api.pipelineCreate(); + new Float32Array(api.HEAPF32.buffer, cfgBuf, 15).set(cfg); + api.inputSetConfig(p, cfgBuf, 15); + const dt = Math.fround(1 / 120); + for (let i = 0; i < 120; i++) { + const x = Math.fround(((i * 37) % 97) / 96); + const y = Math.fround(((i * 53 + 11) % 89) / 88); + api.inputProcess(p, x, y, dt, outXY); + if (i % 10 === 9) { + const v = new Float32Array(api.HEAPF32.buffer, outXY, 2); + pipelineFloats.push(v[0], v[1]); + } + } + api.pipelineDestroy(p); + }; + const defIn = [1, 0, 0, 0.5, 0.5, 2, 0, 1, 0, 0, 0, 0, 0.15, 0, 0]; + runInput(defIn); + runInput([0.7, 0, 0, 0.5, 0.5, 2, 0.1, 1.8, 0, 0, 0.6, 2, 0.15, 1, 0]); + + const N16 = 16; + const vecBuf = api.malloc(N16 * 4); + const maskBuf = api.malloc(N16); + const runOutput = (curve, smoothing, slew, withMask) => { + const p = api.pipelineCreate(); + api.outputSetConfig(p, curve, smoothing, slew, 0); + if (withMask) { + const m = new Uint8Array(api.HEAPF32.buffer, maskBuf, N16); + for (let j = 0; j < N16; j++) m[j] = j % 2 === 0 ? 1 : 0; + api.outputSetFreezeMask(p, maskBuf, N16); + } + const dt = Math.fround(1 / 60); + for (let i = 0; i < 60; i++) { + const vec = new Float32Array(api.HEAPF32.buffer, vecBuf, N16); + for (let j = 0; j < N16; j++) { + vec[j] = Math.fround(((i * 13 + j * 29) % 101) / 100); + } + api.outputProcess(p, vecBuf, N16, dt); + if (i % 15 === 14) { + const out = new Float32Array(api.HEAPF32.buffer, vecBuf, N16); + for (let j = 0; j < N16; j++) pipelineFloats.push(out[j]); + } + } + api.pipelineDestroy(p); + }; + runOutput(1, 0, 0, false); // defaults (slew 0 = unlimited) + runOutput(2.2, 0.5, 2.0, true); + + for (let id = 0; id <= 7; id++) { + for (let i = 0; i <= 16; i++) { + pipelineFloats.push(api.curveApply(id, Math.fround(i / 16), 1.7)); + } + } + + api.free(outXY); + api.free(cfgBuf); + api.free(vecBuf); + api.free(maskBuf); + } + // --- Build payload, write blob --- const payload = []; for (const v of outsStage1) payload.push(v); @@ -388,6 +469,7 @@ async function main() { payload.push(pafL, pafR); payload.push(csL, csR); for (const v of feedbackFloats) payload.push(v); + for (const v of pipelineFloats) payload.push(v); // Sanity: all finite. for (let i = 0; i < payload.length; ++i) { diff --git a/tests/cpp/test_pipeline.cpp b/tests/cpp/test_pipeline.cpp new file mode 100644 index 0000000..faaa776 --- /dev/null +++ b/tests/cpp/test_pipeline.cpp @@ -0,0 +1,238 @@ +// tests/cpp/test_pipeline.cpp — nisps/pipeline input+output chains +// (one-core-engine P4). Unit semantics; the fixture regression against the +// pre-migration TS goldens runs browser-side (manifold pipeline-golden test +// flipped onto the WASM build), and native↔WASM agreement is parity stage 7. + +#include +#include +#include +#include + +#include "../../nisps/core/math.hpp" +#include "../../nisps/pipeline/input_chain.hpp" +#include "../../nisps/pipeline/output_chain.hpp" +#include "test_helpers.hpp" + +namespace { + +using nisps::pipeline::AnchorMode; +using nisps::pipeline::InputChain; +using nisps::pipeline::InputChainConfig; +using nisps::pipeline::MomentumMode; +using nisps::pipeline::OutputChain; +using nisps::pipeline::OutputChainConfig; + +constexpr float kDt = 1.f / 120.f; + +} // namespace + +// -- input chain ---------------------------------------------------------------- + +NISPS_TEST(input_chain_identity_default) { + InputChain ch; + const auto r = ch.process(0.3f, 0.8f, kDt); + NISPS_EXPECT(!r.frozen); + NISPS_EXPECT_NEAR(r.x, 0.3f, 1e-6); + NISPS_EXPECT_NEAR(r.y, 0.8f, 1e-6); +} + +NISPS_TEST(input_chain_deadzone_remap) { + InputChain ch; + InputChainConfig c; + c.deadzone = 0.2f; // half-dz 0.1 around 0.5 + ch.set_config(c); + // Inside the deadzone → pinned to centre. + NISPS_EXPECT_NEAR(ch.process(0.55f, 0.5f, kDt).x, 0.5f, 1e-6); + // Live-zone endpoints preserved. + NISPS_EXPECT_NEAR(ch.process(1.f, 0.5f, kDt).x, 1.f, 1e-6); + NISPS_EXPECT_NEAR(ch.process(0.f, 0.5f, kDt).x, 0.f, 1e-6); + // Just outside the deadzone remaps continuously from centre. + const float just = ch.process(0.6f + 1e-3f, 0.5f, kDt).x; + NISPS_EXPECT(just > 0.5f && just < 0.52f); +} + +NISPS_TEST(input_chain_circular_clamp) { + InputChain ch; + // A corner (1,1) is outside the unit disk around (0.5,0.5) → clamped to + // the rim, direction preserved. + const auto r = ch.process(1.f, 1.f, kDt); + const float cx = r.x - 0.5f; + const float cy = r.y - 0.5f; + NISPS_EXPECT_NEAR(std::sqrt(cx * cx + cy * cy), 0.5f, 1e-5); + NISPS_EXPECT_NEAR(cx, cy, 1e-6); +} + +NISPS_TEST(input_chain_zoom_and_freeze) { + InputChain ch; + InputChainConfig c; + c.zoom = 0.5f; // half window around the centre anchor + ch.set_config(c); + NISPS_EXPECT_NEAR(ch.process(1.f, 0.5f, kDt).x, 0.75f, 1e-6); + NISPS_EXPECT_NEAR(ch.process(0.f, 0.5f, kDt).x, 0.25f, 1e-6); + + // Zoom at the freeze threshold: holds the last smoothed value. + ch.process(0.75f, 0.25f, kDt); // establish state (0.625, 0.375) + c.zoom = 0.01f; + ch.set_config(c); + const auto frozen = ch.process(0.f, 1.f, kDt); + NISPS_EXPECT(frozen.frozen); + NISPS_EXPECT_NEAR(frozen.x, 0.625f, 1e-5); + NISPS_EXPECT_NEAR(frozen.y, 0.375f, 1e-5); +} + +NISPS_TEST(input_chain_sticky_anchor) { + InputChain ch; + InputChainConfig c; + c.zoom = 0.2f; + c.anchor_mode = AnchorMode::Sticky; + c.anchor_x = 0.8f; + c.anchor_y = 0.2f; + ch.set_config(c); + const auto r = ch.process(0.5f, 0.5f, kDt); // centred stick → exactly the anchor + NISPS_EXPECT_NEAR(r.x, 0.8f, 1e-6); + NISPS_EXPECT_NEAR(r.y, 0.2f, 1e-6); +} + +NISPS_TEST(input_chain_ema_frame_rate_independent) { + // Same wall-time travel at different tick rates converges to ~the same + // place (the alpha_eff dt-compensation). + auto run = [](float dt, int steps) { + InputChain ch; + InputChainConfig c; + c.smoothing = 0.8f; + ch.set_config(c); + float x = 0.f; + for (int i = 0; i < steps; ++i) x = ch.process(1.f, 0.5f, dt).x; + return x; + }; + const float at60 = run(1.f / 60.f, 60); // 1 s + const float at240 = run(1.f / 240.f, 240); // 1 s + NISPS_EXPECT_NEAR(at60, at240, 5e-3); + NISPS_EXPECT(at60 > 0.9f); +} + +NISPS_TEST(input_chain_momentum_zooms_out_on_speed) { + InputChain ch; + InputChainConfig c; + c.momentum_mode = MomentumMode::Strong; + ch.set_config(c); + // Sweep fast across the full range: multiplier should drop below 1. + float x = 0.f; + for (int i = 0; i <= 24; ++i) { + x = static_cast(i) / 24.f; + ch.process(x, 0.5f, kDt); + } + NISPS_EXPECT(ch.momentum_multiplier() < 0.9f); + // Dwell: multiplier recovers toward 1. + for (int i = 0; i < 240; ++i) ch.process(1.f, 0.5f, kDt); + NISPS_EXPECT(ch.momentum_multiplier() > 0.95f); +} + +NISPS_TEST(input_chain_state_round_trip) { + InputChain a; + InputChainConfig c; + c.smoothing = 0.5f; + a.set_config(c); + for (int i = 0; i < 10; ++i) a.process(0.9f, 0.1f, kDt); + + std::array blob{}; + a.save_state(blob); + + InputChain b; + b.set_config(c); + b.load_state(blob); + const auto ra = a.process(0.9f, 0.1f, kDt); + const auto rb = b.process(0.9f, 0.1f, kDt); + NISPS_EXPECT(ra.x == rb.x); + NISPS_EXPECT(ra.y == rb.y); +} + +// -- output chain --------------------------------------------------------------- + +NISPS_TEST(output_chain_identity_default) { + OutputChain<8u> ch; + const float raw[4] = {0.1f, 0.5f, 0.9f, 1.2f}; + float out[4]; + ch.process(std::span(raw), std::span(out), kDt); + NISPS_EXPECT_NEAR(out[0], 0.1f, 1e-6); + NISPS_EXPECT_NEAR(out[3], 1.0f, 1e-6); // clamped +} + +NISPS_TEST(output_chain_global_curve) { + OutputChain<8u> ch; + OutputChainConfig c; + c.global_curve = 2.0f; + ch.set_config(c); + const float raw[2] = {0.5f, 0.9f}; + float out[2]; + ch.process(std::span(raw), std::span(out), kDt); + NISPS_EXPECT_NEAR(out[0], 0.25f, 1e-6); + NISPS_EXPECT_NEAR(out[1], 0.81f, 1e-5); +} + +NISPS_TEST(output_chain_slew_limits_change) { + OutputChain<4u> ch; + OutputChainConfig c; + c.slew_rate = 1.0f; // one full unit per second + ch.set_config(c); + const float step0[1] = {0.f}; + const float step1[1] = {1.f}; + float out[1]; + ch.process(std::span(step0), std::span(out), kDt); // seed at 0 + ch.process(std::span(step1), std::span(out), kDt); + NISPS_EXPECT_NEAR(out[0], kDt, 1e-6); // limited to slew*dt + ch.process(std::span(step1), std::span(out), kDt); + NISPS_EXPECT_NEAR(out[0], 2.f * kDt, 1e-6); +} + +NISPS_TEST(output_chain_freeze_gate_and_mask) { + OutputChain<4u> ch; + const float a[2] = {0.2f, 0.8f}; + float out[2]; + ch.process(std::span(a), std::span(out), kDt); + + // Global freeze holds prior values. + OutputChainConfig c; + c.freeze_output = true; + ch.set_config(c); + const float b[2] = {0.9f, 0.1f}; + ch.process(std::span(b), std::span(out), kDt); + NISPS_EXPECT_NEAR(out[0], 0.2f, 1e-6); + NISPS_EXPECT_NEAR(out[1], 0.8f, 1e-6); + + // Per-output mask freezes only masked dims. + c.freeze_output = false; + ch.set_config(c); + const std::uint8_t mask[2] = {1u, 0u}; + ch.set_freeze_mask(mask); + ch.process(std::span(b), std::span(out), kDt); + NISPS_EXPECT_NEAR(out[0], 0.2f, 1e-6); // frozen + NISPS_EXPECT_NEAR(out[1], 0.1f, 1e-6); // live +} + +NISPS_TEST(output_chain_reseed_on_length_change) { + OutputChain<8u> ch; + const float a4[4] = {0.1f, 0.2f, 0.3f, 0.4f}; + float out4[4]; + ch.process(std::span(a4), std::span(out4), kDt); + // Shorter vector reseeds rather than reusing stale state. + OutputChainConfig c; + c.slew_rate = 0.001f; // would clamp hard if prev were stale + ch.set_config(c); + const float a2[2] = {0.9f, 0.9f}; + float out2[2]; + ch.process(std::span(a2), std::span(out2), kDt); + NISPS_EXPECT_NEAR(out2[0], 0.9f, 1e-6); // seeded fresh from raw +} + +// -- curve catalog -------------------------------------------------------------- + +NISPS_TEST(centered_power_pivots_at_half) { + NISPS_EXPECT_NEAR(nisps::centered_power(0.5f, 3.f), 0.5f, 1e-7); + NISPS_EXPECT_NEAR(nisps::centered_power(0.f, 1.f), 0.f, 1e-7); + NISPS_EXPECT_NEAR(nisps::centered_power(1.f, 2.f), 1.f, 1e-6); + // exponent > 1 pulls toward the centre. + NISPS_EXPECT(nisps::centered_power(0.75f, 2.f) < 0.75f); + // exponent < 1 pushes toward the extremes. + NISPS_EXPECT(nisps::centered_power(0.75f, 0.5f) > 0.75f); +}