diff --git a/website/index.html b/website/index.html index 40199fe8499..58a1ac01071 100644 --- a/website/index.html +++ b/website/index.html @@ -40,6 +40,7 @@
  • Backends
  • Partners
  • GitHub
  • +
  • Playground
  • + +
    +
    +

    ExecuTorch Playground

    +

    Try ExecuTorch hands-on

    +

    Interactive experiments that run ExecuTorch right where you are: no install and no server. Pick a playground to start.

    +
    + + +
    +
    +
    + WebGPU + Experimental +
    +

    Run PyTorch models locally in your browser

    +

    Chat with a language model, transcribe your voice, segment objects, and see depth through your camera. Real ExecuTorch models running on your GPU with the WebGPU backend.

    +
      +

      Private by design: models run on your GPU, and nothing you type, say, or show leaves the tab.

      + Open the WebGPU playground +
      +
      + +
      +

      More playgrounds are on the way

      +

      What would you like to try next? Tell us on GitHub or in the PyTorch Edge Discord.

      +
      +
      + + + + diff --git a/website/playground/webgpu/README.md b/website/playground/webgpu/README.md new file mode 100644 index 00000000000..9e907b556e1 --- /dev/null +++ b/website/playground/webgpu/README.md @@ -0,0 +1,85 @@ +# ExecuTorch WebGPU Playground + +The playground for executorch.ai, served at `/playground/webgpu/`. Visitors run real +ExecuTorch models in their own browser on WebGPU: nothing to install, and +prompts, photos, and audio never leave the tab. + +Zero dependencies and no build step: plain ES modules, one design system, hash +routing. The folder drops into the site as-is; the homepage links to it from the +nav ("Playground") and a call-to-action band under the hero. + +## Run it locally + +```bash +node scripts/serve.mjs # http://localhost:5173/playground/webgpu/ (+ LAN URLs for the no-WebGPU fallback) +node --test "tests/*.test.mjs" # licensing gate, internal-reference scan, runtime, loader, wiring +node scripts/check-wiring.mjs # static wiring check on its own +``` + +### Local-assets dev mode (real models before hosting is cleared) + +```bash +node scripts/serve.mjs --dev-assets --dev-config +``` + +The server binds loopback only and adds two routes the deployed site never has: +`/__dev__/config.json` (model id → an `assets` block shaped like the one in +`models.js`, with `baseUrl` relative to ``) and `/__dev_assets__/*` (files +from ``). A dev entry takes precedence over the hosted one, so a new export +can be tried before it is published. Pages only look for the config when +they are themselves on a loopback host. Shipped model entries keep +`baseUrl: null` and `license.status: 'todo'`, so a deployed copy shows +"not available in this build" until a model is cleared. + +To try it on a laptop GPU from a remote dev machine, forward the port +(`ssh -L 5173:localhost:5173 `) and open `http://localhost:5173/playground/webgpu/`; +`localhost` is a secure context, so WebGPU is available. + +## What runs + +Every tile's badge comes from the `compute` field in `src/config/catalog.js`. + +| Demo | Badge | What runs | +|---|---|---| +| On-device Chat | ExecuTorch | Qwen 3 0.6B, Qwen 2.5 0.5B, Llama 3.2 1B (4-bit `.pte`) with batched prefill, KV cache reused across turns, and JS sampling. Voice input through Whisper. | +| Segment Anything | ExecuTorch | SAM 2 Hiera Tiny: encode once, decode a mask per click or box. | +| VLM Q&A | ExecuTorch | SmolVLM-500M on a photo or camera snapshot. | +| Depth, Object Detection | ExecuTorch | Depth Anything V2 Small, YOLO26s, on photos or the live camera. | +| Background Removal, Super-Resolution | ExecuTorch | MODNet (photo or live camera), Real-ESRGAN ×4. | +| Speech to Text | ExecuTorch | Whisper base on the microphone or an audio file. | + +## Model licensing gate + +All checkpoints are referenced from one module, `src/config/models.js`. +`resolveWeights()` in `src/loader/resolve.js` returns remote weights only when +the license is `cleared` **and** an HTTPS URL is set; `tests/licensing.test.mjs` +asserts both halves. To enable a model after review: set +`license.status: 'cleared'`, record the decision, and set `assets.baseUrl` (a +commit-pinned URL). The download path (`src/loader/fetch-progress.js`) streams +with byte progress and caches in Cache Storage. + +## Runtime layer + +- `runtime/`: the ExecuTorch Wasm runtime with its WebGPU backend, shared by + every demo. `runtime/BUILD_INFO.md` says how it was built and when to rebuild it. +- `src/runtime/executorch.js`: starts that runtime once (one WebGPU device), + loads `.pte` programs, and runs one call into it at a time. +- `src/runtime/sources.js`: where a model's files come from (cleared remote URL, + or the loopback dev config). +- `src/runtime/llm.js`: ExecuTorch LLM driver (batched prefill, decode, sampling). +- `src/runtime/forward.js`, `whisper.js`, `sam.js`: the other model drivers. +- `src/runtime/emscripten.js`: loads an Emscripten runtime script and its `.wasm`. +- `src/runtime/vlm.js`: the unlisted VLM demo, which still targets its own + runtime build. + +## Shipping to executorch.ai + +1. Settle model licensing and host the cleared `.pte` files at commit-pinned + URLs with CORS; fill in `models.js`. The runtime ships with the site + (`runtime/`); serve `.wasm` as `application/wasm`. +2. Copy `site/` changes upstream: `playground/` (hub + `webgpu/`), `examples/` (redirect), the homepage nav item and CTA, + and the `style.css` additions. `scripts/` and `tests/` can stay or be excluded. +3. HTTPS is required (WebGPU needs a secure context). Asset names are not + content-hashed, so keep HTML/JS/CSS on a short cache or revalidation policy. +4. The runtime is single-threaded, so cross-origin isolation (COOP/COEP) is + not required. diff --git a/website/playground/webgpu/assets/executorch-chip-logo.svg b/website/playground/webgpu/assets/executorch-chip-logo.svg new file mode 100644 index 00000000000..11e5ed60956 --- /dev/null +++ b/website/playground/webgpu/assets/executorch-chip-logo.svg @@ -0,0 +1,205 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/website/playground/webgpu/assets/huggingface-logo.svg b/website/playground/webgpu/assets/huggingface-logo.svg new file mode 100644 index 00000000000..43c5d3c0c97 --- /dev/null +++ b/website/playground/webgpu/assets/huggingface-logo.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + diff --git a/website/playground/webgpu/assets/samples/CREDITS.md b/website/playground/webgpu/assets/samples/CREDITS.md new file mode 100644 index 00000000000..e763a67f46e --- /dev/null +++ b/website/playground/webgpu/assets/samples/CREDITS.md @@ -0,0 +1,18 @@ +# Sample photos + +Each sample photo is public domain, CC0 or CC BY 2.0 (attribution below). Files were resized and re-encoded, and their metadata was removed. + +| File | Title | Author | Source | License | +|---|---|---|---|---| +| `astronaut.jpg` | Astronaut Eileen Collins (scikit-image 'astronaut' sample) | NASA | https://scikit-image.org/docs/stable/api/skimage.data.html#skimage.data.astronaut | [Public domain (work of the U.S. federal government, NASA)](https://www.usa.gov/government-copyright) | +| `butterfly.jpg` | Monarch butterfly on butterfly bush | Becky Dill (iNaturalist user gillydilly) | https://www.inaturalist.org/photos/43428386 | [CC0 1.0](https://creativecommons.org/publicdomain/zero/1.0/) | +| `cat.jpg` | Domestic cat under a parked car | Marina Potapova (iNaturalist user windof) | https://www.inaturalist.org/photos/124476421 | [CC0 1.0](https://creativecommons.org/publicdomain/zero/1.0/) | +| `chihuahuas.jpg` | Two small dogs held in a person's arms | kheintzman (iNaturalist user kheintzman) | https://www.inaturalist.org/photos/59051932 | [CC0 1.0](https://creativecommons.org/publicdomain/zero/1.0/) | +| `city.jpg` | Columbus Tower | Sentinel Building at Night | [Nan Palmero](https://www.flickr.com/people/nanpalmero/) | https://www.flickr.com/photos/nanpalmero/17092878238 (via Open Images Dataset) | [CC BY 2.0](https://creativecommons.org/licenses/by/2.0/) | +| `esa-astronaut.jpg` | ESA Astronaut Alexander Gerst | [DLR German Aerospace Center](https://www.flickr.com/people/dlr_de/) | https://www.flickr.com/photos/dlr_de/21377675248 (via Open Images Dataset) | [CC BY 2.0](https://creativecommons.org/licenses/by/2.0/) | +| `giraffes.jpg` | Masai Giraffe - Masai Mara National Reserve, Kenya. 04.08.2008 | [JULIAN MASON](https://www.flickr.com/people/meandmyshadow/) | https://www.flickr.com/photos/meandmyshadow/3520928208 (via Open Images Dataset) | [CC BY 2.0](https://creativecommons.org/licenses/by/2.0/) | +| `husky.jpg` | Husky face close-up | Nonbinary-Naturalist (iNaturalist user nonbinary-naturalist) | https://www.inaturalist.org/photos/466838529 | [CC0 1.0](https://creativecommons.org/publicdomain/zero/1.0/) | +| `receipt.jpg` | Receipt for mail to be delivered on March 24, 1970 | Albany Letter Express (issuer); National Postal Museum | https://postalmuseum.si.edu/object/npm_2010.2023.13 | [CC0 1.0](https://creativecommons.org/publicdomain/zero/1.0/) | +| `skier.jpg` | Heli Ski in Chile | [Alex Grechman](https://www.flickr.com/people/grechman/) | https://www.flickr.com/photos/grechman/2790755992 (via Open Images Dataset) | [CC BY 2.0](https://creativecommons.org/licenses/by/2.0/) | +| `street.jpg` | New York City | [FaceMePLS](https://www.flickr.com/people/faceme/) | https://www.flickr.com/photos/faceme/9580869905 (via Open Images Dataset) | [CC BY 2.0](https://creativecommons.org/licenses/by/2.0/) | +| `table.jpg` | Family Reunion | [Kurman Communications, Inc.](https://www.flickr.com/people/kurmanphotos/) | https://www.flickr.com/photos/kurmanphotos/22097532635 (via Open Images Dataset) | [CC BY 2.0](https://creativecommons.org/licenses/by/2.0/) | diff --git a/website/playground/webgpu/assets/samples/astronaut.jpg b/website/playground/webgpu/assets/samples/astronaut.jpg new file mode 100644 index 00000000000..4818b96ccca Binary files /dev/null and b/website/playground/webgpu/assets/samples/astronaut.jpg differ diff --git a/website/playground/webgpu/assets/samples/butterfly.jpg b/website/playground/webgpu/assets/samples/butterfly.jpg new file mode 100644 index 00000000000..164dfe58686 Binary files /dev/null and b/website/playground/webgpu/assets/samples/butterfly.jpg differ diff --git a/website/playground/webgpu/assets/samples/cat.jpg b/website/playground/webgpu/assets/samples/cat.jpg new file mode 100644 index 00000000000..129a6167a57 Binary files /dev/null and b/website/playground/webgpu/assets/samples/cat.jpg differ diff --git a/website/playground/webgpu/assets/samples/chihuahuas.jpg b/website/playground/webgpu/assets/samples/chihuahuas.jpg new file mode 100644 index 00000000000..5b968e152c9 Binary files /dev/null and b/website/playground/webgpu/assets/samples/chihuahuas.jpg differ diff --git a/website/playground/webgpu/assets/samples/city.jpg b/website/playground/webgpu/assets/samples/city.jpg new file mode 100644 index 00000000000..2cc212981af Binary files /dev/null and b/website/playground/webgpu/assets/samples/city.jpg differ diff --git a/website/playground/webgpu/assets/samples/esa-astronaut.jpg b/website/playground/webgpu/assets/samples/esa-astronaut.jpg new file mode 100644 index 00000000000..dfa89a52df9 Binary files /dev/null and b/website/playground/webgpu/assets/samples/esa-astronaut.jpg differ diff --git a/website/playground/webgpu/assets/samples/giraffes.jpg b/website/playground/webgpu/assets/samples/giraffes.jpg new file mode 100644 index 00000000000..7310b8f3090 Binary files /dev/null and b/website/playground/webgpu/assets/samples/giraffes.jpg differ diff --git a/website/playground/webgpu/assets/samples/husky.jpg b/website/playground/webgpu/assets/samples/husky.jpg new file mode 100644 index 00000000000..f67775cc5c4 Binary files /dev/null and b/website/playground/webgpu/assets/samples/husky.jpg differ diff --git a/website/playground/webgpu/assets/samples/receipt.jpg b/website/playground/webgpu/assets/samples/receipt.jpg new file mode 100644 index 00000000000..d41f56908c3 Binary files /dev/null and b/website/playground/webgpu/assets/samples/receipt.jpg differ diff --git a/website/playground/webgpu/assets/samples/skier.jpg b/website/playground/webgpu/assets/samples/skier.jpg new file mode 100644 index 00000000000..427a5e2bbe3 Binary files /dev/null and b/website/playground/webgpu/assets/samples/skier.jpg differ diff --git a/website/playground/webgpu/assets/samples/street.jpg b/website/playground/webgpu/assets/samples/street.jpg new file mode 100644 index 00000000000..65f769c8622 Binary files /dev/null and b/website/playground/webgpu/assets/samples/street.jpg differ diff --git a/website/playground/webgpu/assets/samples/table.jpg b/website/playground/webgpu/assets/samples/table.jpg new file mode 100644 index 00000000000..70d0203c267 Binary files /dev/null and b/website/playground/webgpu/assets/samples/table.jpg differ diff --git a/website/playground/webgpu/index.html b/website/playground/webgpu/index.html new file mode 100644 index 00000000000..d27e69f28c6 --- /dev/null +++ b/website/playground/webgpu/index.html @@ -0,0 +1,53 @@ + + + + + + WebGPU · ExecuTorch Playground + + + + + + + + + + + + + +
      + +
      +
      + +
      + + + diff --git a/website/playground/webgpu/package.json b/website/playground/webgpu/package.json new file mode 100644 index 00000000000..6c4e5bc688d --- /dev/null +++ b/website/playground/webgpu/package.json @@ -0,0 +1,12 @@ +{ + "name": "executorch-webgpu-examples", + "private": true, + "version": "0.1.0", + "description": "Prototype of the public ExecuTorch WebGPU examples playground. Zero runtime dependencies; static files only.", + "type": "module", + "scripts": { + "serve": "node scripts/serve.mjs", + "check": "node scripts/check-wiring.mjs", + "test": "node --test \"tests/*.test.mjs\"" + } +} diff --git a/website/playground/webgpu/runtime/BUILD_INFO.md b/website/playground/webgpu/runtime/BUILD_INFO.md new file mode 100644 index 00000000000..5838c3e032b --- /dev/null +++ b/website/playground/webgpu/runtime/BUILD_INFO.md @@ -0,0 +1,93 @@ +# ExecuTorch Wasm + WebGPU runtime + +`executorch_wasm_lib.js` and `executorch_wasm_lib.wasm` are the ExecuTorch Wasm +bindings (`extension/wasm`) with the WebGPU backend, linked into one web bundle. +The `.js` loader ships in this folder. The `.wasm` is hosted on Hugging Face +(`executorch-community/ExecuTorch-WebGPU-Runtime`, pinned in `src/config/runtime.js`), +downloaded and checked against the sha256 below like a model file. +Every demo in the playground shares this one runtime (`src/runtime/executorch.js`). +The JS file defines `createExecuTorch()`. After `await et.initWebGPU()`, a `.pte` +loads with `et.Module.load(path)` and `loadMethod('forward')`, and runs with an +async `forward([tensors])`. + +## Source + +Built from the ExecuTorch source tree of 2026-10-16 (`version.txt` 1.6.0) with the two +changes that make WebGPU run in the Wasm build, both merged upstream: + +1. pytorch/executorch#23938: the `EXECUTORCH_BUILD_WASM_WEBGPU` option, which links the + WebGPU backend into `extension/wasm`, and the `initWebGPU()` binding. +2. pytorch/executorch#23914: `WebGPUGraph::propagate_resize` resizes every operand before + its consumer in one pass, so a multi-token LLM prefill works. + +A build from ExecuTorch main that contains both, with the commands below, should give the +same files. The emdawnwebgpu override (`EXECUTORCH_WEBGPU_EMDAWNWEBGPU_PORT`) was only +needed because the build machine could not reach GitHub; without it, Emscripten downloads +the same emdawnwebgpu version itself. + +## Toolchain + +- Emscripten 4.0.19, official release build `8b01e2ec3f33e6b94842096d7312ce4ef5f33f6c`. +- emdawnwebgpu v20251002.162335, the version Emscripten 4.0.19 pins. It was rebuilt + from Dawn source at `01940842b667a7812d0e4ca0ef4367fbec294241`, using Dawn's + own packaging recipe. +- CMake 3.31, Ninja 1.13. The configure step needs a Python with `torch` + (for `torchgen`). + +## Build + +A small consumer project links the bindings into a distributable bundle, as +described under "Building for Web" in `extension/wasm/README.md`: + +```cmake +cmake_minimum_required(VERSION 3.29) +project(executorch_web) +add_subdirectory(${ET_SRC} executorch) +add_executable(executorch_wasm_lib) +target_link_libraries(executorch_wasm_lib PRIVATE executorch_wasm) +target_link_options(executorch_wasm_lib PRIVATE + -sMODULARIZE=1 -sEXPORT_NAME=createExecuTorch -sENVIRONMENT=web,worker + -sFORCE_FILESYSTEM=1 -sEXPORTED_RUNTIME_METHODS=FS -sINITIAL_MEMORY=64MB + -sEXPORT_EXCEPTION_HANDLING_HELPERS=1) +``` + +```bash +emcmake cmake -S -B build -G Ninja -DCMAKE_BUILD_TYPE=Release \ + -DET_SRC= \ + -DEXECUTORCH_BUILD_WASM=ON -DEXECUTORCH_BUILD_WEBGPU=ON -DEXECUTORCH_BUILD_WASM_WEBGPU=ON \ + -DEXECUTORCH_BUILD_EXTENSION_DATA_LOADER=ON -DEXECUTORCH_BUILD_EXTENSION_FLAT_TENSOR=ON \ + -DEXECUTORCH_BUILD_EXTENSION_MODULE=ON -DEXECUTORCH_BUILD_EXTENSION_NAMED_DATA_MAP=ON \ + -DEXECUTORCH_BUILD_EXTENSION_TENSOR=ON \ + -DEXECUTORCH_ENABLE_LOGGING=ON -DEXECUTORCH_LOG_LEVEL=Info \ + -DEXECUTORCH_WEBGPU_EMDAWNWEBGPU_PORT=/emdawnwebgpu.port.py \ + "-DCMAKE_C_FLAGS=-ffile-prefix-map==executorch" \ + "-DCMAKE_CXX_FLAGS=-ffile-prefix-map==executorch" \ + -DPYTHON_EXECUTABLE= +cmake --build build --target executorch_wasm_lib +``` + +The WebGPU Wasm option itself adds `-sASYNCIFY -sASYNCIFY_STACK_SIZE=65536 +-sALLOW_MEMORY_GROWTH=1 -sMAXIMUM_MEMORY=4GB -fexceptions`. The full portable +kernel library is linked (no selective build). Logging is at Info, so a failed +load or run prints the reason to the console. `-ffile-prefix-map` keeps +build-machine paths out of the binary. + +## Files + +| File | Bytes | sha256 | +|---|---:|---| +| `executorch_wasm_lib.js` | 165,877 | `8a631b1537927f8f2ef184d9f1b343061f47df0a32ddb8f5190d74e80d40618e` | +| `executorch_wasm_lib.wasm` | 6,562,188 | `35a9087d0b6cd86c279e7111adfd54a58156bd993928b20f63348f1043652eb4` | + +## Checked with this build + +Headless Chromium 141, WebGPU on SwiftShader (no `shader-f16`), with the +playground's models compared against ExecuTorch CPU references: + +- Depth Anything V2 Small, MODNet, Real-ESRGAN, D-FINE S: max abs difference + between 0 and 4.6e-3. +- SAM 2.1 Hiera Tiny: mask IoU 1.000 on a click, a second click, and a box prompt. + +The playground's own end-to-end checks, through the real UI, cover Qwen3 0.6B, +Llama 3.2 1B, SAM 2.1, Depth Anything V2, D-FINE S and Whisper base. Real GPUs +and the `shader-f16` kernel paths have not been tested with this build. diff --git a/website/playground/webgpu/runtime/executorch_wasm_lib.js b/website/playground/webgpu/runtime/executorch_wasm_lib.js new file mode 100644 index 00000000000..c86950d8bc8 --- /dev/null +++ b/website/playground/webgpu/runtime/executorch_wasm_lib.js @@ -0,0 +1,2 @@ +var createExecuTorch=(()=>{var _scriptName=globalThis.document?.currentScript?.src;return async function(moduleArg={}){var moduleRtn;var Module=moduleArg;var ENVIRONMENT_IS_WEB=!!globalThis.window;var ENVIRONMENT_IS_WORKER=!!globalThis.WorkerGlobalScope;var ENVIRONMENT_IS_NODE=globalThis.process?.versions?.node&&globalThis.process?.type!="renderer";var arguments_=[];var thisProgram="./this.program";var quit_=(status,toThrow)=>{throw toThrow};if(ENVIRONMENT_IS_WORKER){_scriptName=self.location.href}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){try{scriptDirectory=new URL(".",_scriptName).href}catch{}{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=async url=>{var response=await fetch(url,{credentials:"same-origin"});if(response.ok){return response.arrayBuffer()}throw new Error(response.status+" : "+response.url)}}}else{}var out=console.log.bind(console);var err=console.error.bind(console);var wasmBinary;var ABORT=false;var EXITSTATUS;function assert(condition,text){if(!condition){abort(text)}}var readyPromiseResolve,readyPromiseReject;var HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAPF64;var HEAP64,HEAPU64;var runtimeInitialized=false;function updateMemoryViews(){var b=wasmMemory.buffer;HEAP8=new Int8Array(b);HEAP16=new Int16Array(b);HEAPU8=new Uint8Array(b);HEAPU16=new Uint16Array(b);HEAP32=new Int32Array(b);HEAPU32=new Uint32Array(b);HEAPF32=new Float32Array(b);HEAPF64=new Float64Array(b);HEAP64=new BigInt64Array(b);HEAPU64=new BigUint64Array(b)}function preRun(){if(Module["preRun"]){if(typeof Module["preRun"]=="function")Module["preRun"]=[Module["preRun"]];while(Module["preRun"].length){addOnPreRun(Module["preRun"].shift())}}callRuntimeCallbacks(onPreRuns)}function initRuntime(){runtimeInitialized=true;if(!Module["noFSInit"]&&!FS.initialized)FS.init();TTY.init();wasmExports["ub"]();FS.ignorePermissions=false}function postRun(){if(Module["postRun"]){if(typeof Module["postRun"]=="function")Module["postRun"]=[Module["postRun"]];while(Module["postRun"].length){addOnPostRun(Module["postRun"].shift())}}callRuntimeCallbacks(onPostRuns)}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 wasmBinaryFile;function findWasmBinary(){return locateFile("executorch_wasm_lib.wasm")}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"}async function getWasmBinary(binaryFile){if(!wasmBinary){try{var response=await readAsync(binaryFile);return new Uint8Array(response)}catch{}}return getBinarySync(binaryFile)}async function instantiateArrayBuffer(binaryFile,imports){try{var binary=await getWasmBinary(binaryFile);var instance=await WebAssembly.instantiate(binary,imports);return instance}catch(reason){err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)}}async function instantiateAsync(binary,binaryFile,imports){if(!binary){try{var response=fetch(binaryFile,{credentials:"same-origin"});var instantiationResult=await WebAssembly.instantiateStreaming(response,imports);return instantiationResult}catch(reason){err(`wasm streaming compile failed: ${reason}`);err("falling back to ArrayBuffer instantiation")}}return instantiateArrayBuffer(binaryFile,imports)}function getWasmImports(){var imports={a:wasmImports};return imports}async function createWasm(){function receiveInstance(instance,module){wasmExports=instance.exports;wasmExports=Asyncify.instrumentWasmExports(wasmExports);wasmExports=applySignatureConversions(wasmExports);assignWasmExports(wasmExports);updateMemoryViews();return wasmExports}function receiveInstantiationResult(result){return receiveInstance(result["instance"])}var info=getWasmImports();if(Module["instantiateWasm"]){return new Promise((resolve,reject)=>{Module["instantiateWasm"](info,(inst,mod)=>{resolve(receiveInstance(inst,mod))})})}wasmBinaryFile??=findWasmBinary();var result=await instantiateAsync(wasmBinary,wasmBinaryFile,info);var exports=receiveInstantiationResult(result);return exports}class ExitStatus{name="ExitStatus";constructor(status){this.message=`Program terminated with exit(${status})`;this.status=status}}var callRuntimeCallbacks=callbacks=>{while(callbacks.length>0){callbacks.shift()(Module)}};var onPostRuns=[];var addOnPostRun=cb=>onPostRuns.push(cb);var onPreRuns=[];var addOnPreRun=cb=>onPreRuns.push(cb);var dynCalls={};var dynCallLegacy=(sig,ptr,args)=>{sig=sig.replace(/p/g,"i");var f=dynCalls[sig];return f(ptr,...args)};var dynCall=(sig,ptr,args=[],promising=false)=>{var rtn=dynCallLegacy(sig,ptr,args);function convert(rtn){return sig[0]=="p"?rtn>>>0:rtn}return convert(rtn)};var noExitRuntime=true;var stackRestore=val=>__emscripten_stack_restore(val);var stackSave=()=>_emscripten_stack_get_current();var exceptionCaught=[];var uncaughtExceptionCount=0;var INT53_MAX=9007199254740992;var INT53_MIN=-9007199254740992;var bigintToI53Checked=num=>numINT53_MAX?NaN:Number(num);function ___cxa_begin_catch(ptr){ptr>>>=0;var info=new ExceptionInfo(ptr);if(!info.get_caught()){info.set_caught(true);uncaughtExceptionCount--}info.set_rethrown(false);exceptionCaught.push(info);___cxa_increment_exception_refcount(ptr);return ___cxa_get_exception_ptr(ptr)}var exceptionLast=0;var ___cxa_end_catch=()=>{_setThrew(0,0);var info=exceptionCaught.pop();___cxa_decrement_exception_refcount(info.excPtr);exceptionLast=0};class ExceptionInfo{constructor(excPtr){this.excPtr=excPtr;this.ptr=excPtr-24}set_type(type){HEAPU32[this.ptr+4>>>2>>>0]=type}get_type(){return HEAPU32[this.ptr+4>>>2>>>0]}set_destructor(destructor){HEAPU32[this.ptr+8>>>2>>>0]=destructor}get_destructor(){return HEAPU32[this.ptr+8>>>2>>>0]}set_caught(caught){caught=caught?1:0;HEAP8[this.ptr+12>>>0]=caught}get_caught(){return HEAP8[this.ptr+12>>>0]!=0}set_rethrown(rethrown){rethrown=rethrown?1:0;HEAP8[this.ptr+13>>>0]=rethrown}get_rethrown(){return HEAP8[this.ptr+13>>>0]!=0}init(type,destructor){this.set_adjusted_ptr(0);this.set_type(type);this.set_destructor(destructor)}set_adjusted_ptr(adjustedPtr){HEAPU32[this.ptr+16>>>2>>>0]=adjustedPtr}get_adjusted_ptr(){return HEAPU32[this.ptr+16>>>2>>>0]}}var setTempRet0=val=>__emscripten_tempret_set(val);var findMatchingCatch=args=>{var thrown=exceptionLast;if(!thrown){setTempRet0(0);return 0}var info=new ExceptionInfo(thrown);info.set_adjusted_ptr(thrown);var thrownType=info.get_type();if(!thrownType){setTempRet0(0);return thrown}for(var caughtType of args){if(caughtType===0||caughtType===thrownType){break}var adjusted_ptr_addr=info.ptr+16;if(___cxa_can_catch(caughtType,thrownType,adjusted_ptr_addr)){setTempRet0(caughtType);return thrown}}setTempRet0(thrownType);return thrown};function ___cxa_find_matching_catch_2(){return findMatchingCatch([])}function ___cxa_find_matching_catch_3(arg0){arg0>>>=0;return findMatchingCatch([arg0])}var ___cxa_rethrow=()=>{var info=exceptionCaught.pop();if(!info){abort("no exception to throw")}var ptr=info.excPtr;if(!info.get_rethrown()){exceptionCaught.push(info);info.set_rethrown(true);info.set_caught(false);uncaughtExceptionCount++}exceptionLast=ptr;throw exceptionLast};function ___cxa_throw(ptr,type,destructor){ptr>>>=0;type>>>=0;destructor>>>=0;var info=new ExceptionInfo(ptr);info.init(type,destructor);exceptionLast=ptr;uncaughtExceptionCount++;throw exceptionLast}function ___resumeException(ptr){ptr>>>=0;if(!exceptionLast){exceptionLast=ptr}throw exceptionLast}var PATH={isAbs:path=>path.charAt(0)==="/",splitPath:filename=>{var splitPathRe=/^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/;return splitPathRe.exec(filename).slice(1)},normalizeArray:(parts,allowAboveRoot)=>{var up=0;for(var i=parts.length-1;i>=0;i--){var last=parts[i];if(last==="."){parts.splice(i,1)}else if(last===".."){parts.splice(i,1);up++}else if(up){parts.splice(i,1);up--}}if(allowAboveRoot){for(;up;up--){parts.unshift("..")}}return parts},normalize:path=>{var isAbsolute=PATH.isAbs(path),trailingSlash=path.slice(-1)==="/";path=PATH.normalizeArray(path.split("/").filter(p=>!!p),!isAbsolute).join("/");if(!path&&!isAbsolute){path="."}if(path&&trailingSlash){path+="/"}return(isAbsolute?"/":"")+path},dirname:path=>{var result=PATH.splitPath(path),root=result[0],dir=result[1];if(!root&&!dir){return"."}if(dir){dir=dir.slice(0,-1)}return root+dir},basename:path=>path&&path.match(/([^\/]+|\/)\/*$/)[1],join:(...paths)=>PATH.normalize(paths.join("/")),join2:(l,r)=>PATH.normalize(l+"/"+r)};var initRandomFill=()=>view=>crypto.getRandomValues(view);var randomFill=view=>{(randomFill=initRandomFill())(view)};var PATH_FS={resolve:(...args)=>{var resolvedPath="",resolvedAbsolute=false;for(var i=args.length-1;i>=-1&&!resolvedAbsolute;i--){var path=i>=0?args[i]:FS.cwd();if(typeof path!="string"){throw new TypeError("Arguments to path.resolve must be strings")}else if(!path){return""}resolvedPath=path+"/"+resolvedPath;resolvedAbsolute=PATH.isAbs(path)}resolvedPath=PATH.normalizeArray(resolvedPath.split("/").filter(p=>!!p),!resolvedAbsolute).join("/");return(resolvedAbsolute?"/":"")+resolvedPath||"."},relative:(from,to)=>{from=PATH_FS.resolve(from).slice(1);to=PATH_FS.resolve(to).slice(1);function trim(arr){var start=0;for(;start=0;end--){if(arr[end]!=="")break}if(start>end)return[];return arr.slice(start,end-start+1)}var fromParts=trim(from.split("/"));var toParts=trim(to.split("/"));var length=Math.min(fromParts.length,toParts.length);var samePartsLength=length;for(var i=0;i{var maxIdx=idx+maxBytesToRead;if(ignoreNul)return maxIdx;while(heapOrArray[idx]&&!(idx>=maxIdx))++idx;return idx};var UTF8ArrayToString=(heapOrArray,idx=0,maxBytesToRead,ignoreNul)=>{idx>>>=0;var endPtr=findStringEnd(heapOrArray,idx,maxBytesToRead,ignoreNul);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 FS_stdin_getChar_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)=>{outIdx>>>=0;if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i=endIdx)break;heap[outIdx++>>>0]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++>>>0]=192|u>>6;heap[outIdx++>>>0]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++>>>0]=224|u>>12;heap[outIdx++>>>0]=128|u>>6&63;heap[outIdx++>>>0]=128|u&63}else{if(outIdx+3>=endIdx)break;heap[outIdx++>>>0]=240|u>>18;heap[outIdx++>>>0]=128|u>>12&63;heap[outIdx++>>>0]=128|u>>6&63;heap[outIdx++>>>0]=128|u&63;i++}}heap[outIdx>>>0]=0;return outIdx-startIdx};var intArrayFromString=(stringy,dontAddNull,length)=>{var len=length>0?length:lengthBytesUTF8(stringy)+1;var u8array=new Array(len);var numBytesWritten=stringToUTF8Array(stringy,u8array,0,u8array.length);if(dontAddNull)u8array.length=numBytesWritten;return u8array};var FS_stdin_getChar=()=>{if(!FS_stdin_getChar_buffer.length){var result=null;if(globalThis.window?.prompt){result=window.prompt("Input: ");if(result!==null){result+="\n"}}else{}if(!result){return null}FS_stdin_getChar_buffer=intArrayFromString(result,true)}return FS_stdin_getChar_buffer.shift()};var TTY={ttys:[],init(){},shutdown(){},register(dev,ops){TTY.ttys[dev]={input:[],output:[],ops};FS.registerDevice(dev,TTY.stream_ops)},stream_ops:{open(stream){var tty=TTY.ttys[stream.node.rdev];if(!tty){throw new FS.ErrnoError(43)}stream.tty=tty;stream.seekable=false},close(stream){stream.tty.ops.fsync(stream.tty)},fsync(stream){stream.tty.ops.fsync(stream.tty)},read(stream,buffer,offset,length,pos){if(!stream.tty||!stream.tty.ops.get_char){throw new FS.ErrnoError(60)}var bytesRead=0;for(var i=0;i0){out(UTF8ArrayToString(tty.output));tty.output=[]}},ioctl_tcgets(tty){return{c_iflag:25856,c_oflag:5,c_cflag:191,c_lflag:35387,c_cc:[3,28,127,21,4,0,1,0,17,19,26,0,18,15,23,22,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}},ioctl_tcsets(tty,optional_actions,data){return 0},ioctl_tiocgwinsz(tty){return[24,80]}},default_tty1_ops:{put_char(tty,val){if(val===null||val===10){err(UTF8ArrayToString(tty.output));tty.output=[]}else{if(val!=0)tty.output.push(val)}},fsync(tty){if(tty.output?.length>0){err(UTF8ArrayToString(tty.output));tty.output=[]}}}};var zeroMemory=(ptr,size)=>HEAPU8.fill(0,ptr,ptr+size);var alignMemory=(size,alignment)=>Math.ceil(size/alignment)*alignment;var mmapAlloc=size=>{size=alignMemory(size,65536);var ptr=_emscripten_builtin_memalign(65536,size);if(ptr)zeroMemory(ptr,size);return ptr};var MEMFS={ops_table:null,mount(mount){return MEMFS.createNode(null,"/",16895,0)},createNode(parent,name,mode,dev){if(FS.isBlkdev(mode)||FS.isFIFO(mode)){throw new FS.ErrnoError(63)}MEMFS.ops_table||={dir:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr,lookup:MEMFS.node_ops.lookup,mknod:MEMFS.node_ops.mknod,rename:MEMFS.node_ops.rename,unlink:MEMFS.node_ops.unlink,rmdir:MEMFS.node_ops.rmdir,readdir:MEMFS.node_ops.readdir,symlink:MEMFS.node_ops.symlink},stream:{llseek:MEMFS.stream_ops.llseek}},file:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr},stream:{llseek:MEMFS.stream_ops.llseek,read:MEMFS.stream_ops.read,write:MEMFS.stream_ops.write,mmap:MEMFS.stream_ops.mmap,msync:MEMFS.stream_ops.msync}},link:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr,readlink:MEMFS.node_ops.readlink},stream:{}},chrdev:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr},stream:FS.chrdev_stream_ops}};var node=FS.createNode(parent,name,mode,dev);if(FS.isDir(node.mode)){node.node_ops=MEMFS.ops_table.dir.node;node.stream_ops=MEMFS.ops_table.dir.stream;node.contents={}}else if(FS.isFile(node.mode)){node.node_ops=MEMFS.ops_table.file.node;node.stream_ops=MEMFS.ops_table.file.stream;node.usedBytes=0;node.contents=null}else if(FS.isLink(node.mode)){node.node_ops=MEMFS.ops_table.link.node;node.stream_ops=MEMFS.ops_table.link.stream}else if(FS.isChrdev(node.mode)){node.node_ops=MEMFS.ops_table.chrdev.node;node.stream_ops=MEMFS.ops_table.chrdev.stream}node.atime=node.mtime=node.ctime=Date.now();if(parent){parent.contents[name]=node;parent.atime=parent.mtime=parent.ctime=node.atime}return node},getFileDataAsTypedArray(node){if(!node.contents)return new Uint8Array(0);if(node.contents.subarray)return node.contents.subarray(0,node.usedBytes);return new Uint8Array(node.contents)},expandFileStorage(node,newCapacity){var prevCapacity=node.contents?node.contents.length:0;if(prevCapacity>=newCapacity)return;var CAPACITY_DOUBLING_MAX=1024*1024;newCapacity=Math.max(newCapacity,prevCapacity*(prevCapacity>>0);if(prevCapacity!=0)newCapacity=Math.max(newCapacity,256);var oldContents=node.contents;node.contents=new Uint8Array(newCapacity);if(node.usedBytes>0)node.contents.set(oldContents.subarray(0,node.usedBytes),0)},resizeFileStorage(node,newSize){if(node.usedBytes==newSize)return;if(newSize==0){node.contents=null;node.usedBytes=0}else{var oldContents=node.contents;node.contents=new Uint8Array(newSize);if(oldContents){node.contents.set(oldContents.subarray(0,Math.min(newSize,node.usedBytes)))}node.usedBytes=newSize}},node_ops:{getattr(node){var attr={};attr.dev=FS.isChrdev(node.mode)?node.id:1;attr.ino=node.id;attr.mode=node.mode;attr.nlink=1;attr.uid=0;attr.gid=0;attr.rdev=node.rdev;if(FS.isDir(node.mode)){attr.size=4096}else if(FS.isFile(node.mode)){attr.size=node.usedBytes}else if(FS.isLink(node.mode)){attr.size=node.link.length}else{attr.size=0}attr.atime=new Date(node.atime);attr.mtime=new Date(node.mtime);attr.ctime=new Date(node.ctime);attr.blksize=4096;attr.blocks=Math.ceil(attr.size/attr.blksize);return attr},setattr(node,attr){for(const key of["mode","atime","mtime","ctime"]){if(attr[key]!=null){node[key]=attr[key]}}if(attr.size!==undefined){MEMFS.resizeFileStorage(node,attr.size)}},lookup(parent,name){if(!MEMFS.doesNotExistError){MEMFS.doesNotExistError=new FS.ErrnoError(44);MEMFS.doesNotExistError.stack=""}throw MEMFS.doesNotExistError},mknod(parent,name,mode,dev){return MEMFS.createNode(parent,name,mode,dev)},rename(old_node,new_dir,new_name){var new_node;try{new_node=FS.lookupNode(new_dir,new_name)}catch(e){}if(new_node){if(FS.isDir(old_node.mode)){for(var i in new_node.contents){throw new FS.ErrnoError(55)}}FS.hashRemoveNode(new_node)}delete old_node.parent.contents[old_node.name];new_dir.contents[new_name]=old_node;old_node.name=new_name;new_dir.ctime=new_dir.mtime=old_node.parent.ctime=old_node.parent.mtime=Date.now()},unlink(parent,name){delete parent.contents[name];parent.ctime=parent.mtime=Date.now()},rmdir(parent,name){var node=FS.lookupNode(parent,name);for(var i in node.contents){throw new FS.ErrnoError(55)}delete parent.contents[name];parent.ctime=parent.mtime=Date.now()},readdir(node){return[".","..",...Object.keys(node.contents)]},symlink(parent,newname,oldpath){var node=MEMFS.createNode(parent,newname,511|40960,0);node.link=oldpath;return node},readlink(node){if(!FS.isLink(node.mode)){throw new FS.ErrnoError(28)}return node.link}},stream_ops:{read(stream,buffer,offset,length,position){var contents=stream.node.contents;if(position>=stream.node.usedBytes)return 0;var size=Math.min(stream.node.usedBytes-position,length);if(size>8&&contents.subarray){buffer.set(contents.subarray(position,position+size),offset)}else{for(var i=0;i0||position+length>>0)}}return{ptr,allocated}},msync(stream,buffer,offset,length,mmapFlags){MEMFS.stream_ops.write(stream,buffer,0,length,offset,false);return 0}}};var FS_modeStringToFlags=str=>{var flagModes={r:0,"r+":2,w:512|64|1,"w+":512|64|2,a:1024|64|1,"a+":1024|64|2};var flags=flagModes[str];if(typeof flags=="undefined"){throw new Error(`Unknown file open mode: ${str}`)}return flags};var FS_getMode=(canRead,canWrite)=>{var mode=0;if(canRead)mode|=292|73;if(canWrite)mode|=146;return mode};var asyncLoad=async url=>{var arrayBuffer=await readAsync(url);return new Uint8Array(arrayBuffer)};var FS_createDataFile=(...args)=>FS.createDataFile(...args);var getUniqueRunDependency=id=>id;var runDependencies=0;var dependenciesFulfilled=null;var removeRunDependency=id=>{runDependencies--;Module["monitorRunDependencies"]?.(runDependencies);if(runDependencies==0){if(dependenciesFulfilled){var callback=dependenciesFulfilled;dependenciesFulfilled=null;callback()}}};var addRunDependency=id=>{runDependencies++;Module["monitorRunDependencies"]?.(runDependencies)};var preloadPlugins=[];var FS_handledByPreloadPlugin=async(byteArray,fullname)=>{if(typeof Browser!="undefined")Browser.init();for(var plugin of preloadPlugins){if(plugin["canHandle"](fullname)){return plugin["handle"](byteArray,fullname)}}return byteArray};var FS_preloadFile=async(parent,name,url,canRead,canWrite,dontCreateFile,canOwn,preFinish)=>{var fullname=name?PATH_FS.resolve(PATH.join2(parent,name)):parent;var dep=getUniqueRunDependency(`cp ${fullname}`);addRunDependency(dep);try{var byteArray=url;if(typeof url=="string"){byteArray=await asyncLoad(url)}byteArray=await FS_handledByPreloadPlugin(byteArray,fullname);preFinish?.();if(!dontCreateFile){FS_createDataFile(parent,name,byteArray,canRead,canWrite,canOwn)}}finally{removeRunDependency(dep)}};var FS_createPreloadedFile=(parent,name,url,canRead,canWrite,onload,onerror,dontCreateFile,canOwn,preFinish)=>{FS_preloadFile(parent,name,url,canRead,canWrite,dontCreateFile,canOwn,preFinish).then(onload).catch(onerror)};var FS={root:null,mounts:[],devices:{},streams:[],nextInode:1,nameTable:null,currentPath:"/",initialized:false,ignorePermissions:true,filesystems:null,syncFSRequests:0,readFiles:{},ErrnoError:class{name="ErrnoError";constructor(errno){this.errno=errno}},FSStream:class{shared={};get object(){return this.node}set object(val){this.node=val}get isRead(){return(this.flags&2097155)!==1}get isWrite(){return(this.flags&2097155)!==0}get isAppend(){return this.flags&1024}get flags(){return this.shared.flags}set flags(val){this.shared.flags=val}get position(){return this.shared.position}set position(val){this.shared.position=val}},FSNode:class{node_ops={};stream_ops={};readMode=292|73;writeMode=146;mounted=null;constructor(parent,name,mode,rdev){if(!parent){parent=this}this.parent=parent;this.mount=parent.mount;this.id=FS.nextInode++;this.name=name;this.mode=mode;this.rdev=rdev;this.atime=this.mtime=this.ctime=Date.now()}get read(){return(this.mode&this.readMode)===this.readMode}set read(val){val?this.mode|=this.readMode:this.mode&=~this.readMode}get write(){return(this.mode&this.writeMode)===this.writeMode}set write(val){val?this.mode|=this.writeMode:this.mode&=~this.writeMode}get isFolder(){return FS.isDir(this.mode)}get isDevice(){return FS.isChrdev(this.mode)}},lookupPath(path,opts={}){if(!path){throw new FS.ErrnoError(44)}opts.follow_mount??=true;if(!PATH.isAbs(path)){path=FS.cwd()+"/"+path}linkloop:for(var nlinks=0;nlinks<40;nlinks++){var parts=path.split("/").filter(p=>!!p);var current=FS.root;var current_path="/";for(var i=0;i>>0)%FS.nameTable.length},hashAddNode(node){var hash=FS.hashName(node.parent.id,node.name);node.name_next=FS.nameTable[hash];FS.nameTable[hash]=node},hashRemoveNode(node){var hash=FS.hashName(node.parent.id,node.name);if(FS.nameTable[hash]===node){FS.nameTable[hash]=node.name_next}else{var current=FS.nameTable[hash];while(current){if(current.name_next===node){current.name_next=node.name_next;break}current=current.name_next}}},lookupNode(parent,name){var errCode=FS.mayLookup(parent);if(errCode){throw new FS.ErrnoError(errCode)}var hash=FS.hashName(parent.id,name);for(var node=FS.nameTable[hash];node;node=node.name_next){var nodeName=node.name;if(node.parent.id===parent.id&&nodeName===name){return node}}return FS.lookup(parent,name)},createNode(parent,name,mode,rdev){var node=new FS.FSNode(parent,name,mode,rdev);FS.hashAddNode(node);return node},destroyNode(node){FS.hashRemoveNode(node)},isRoot(node){return node===node.parent},isMountpoint(node){return!!node.mounted},isFile(mode){return(mode&61440)===32768},isDir(mode){return(mode&61440)===16384},isLink(mode){return(mode&61440)===40960},isChrdev(mode){return(mode&61440)===8192},isBlkdev(mode){return(mode&61440)===24576},isFIFO(mode){return(mode&61440)===4096},isSocket(mode){return(mode&49152)===49152},flagsToPermissionString(flag){var perms=["r","w","rw"][flag&3];if(flag&512){perms+="w"}return perms},nodePermissions(node,perms){if(FS.ignorePermissions){return 0}if(perms.includes("r")&&!(node.mode&292)){return 2}else if(perms.includes("w")&&!(node.mode&146)){return 2}else if(perms.includes("x")&&!(node.mode&73)){return 2}return 0},mayLookup(dir){if(!FS.isDir(dir.mode))return 54;var errCode=FS.nodePermissions(dir,"x");if(errCode)return errCode;if(!dir.node_ops.lookup)return 2;return 0},mayCreate(dir,name){if(!FS.isDir(dir.mode)){return 54}try{var node=FS.lookupNode(dir,name);return 20}catch(e){}return FS.nodePermissions(dir,"wx")},mayDelete(dir,name,isdir){var node;try{node=FS.lookupNode(dir,name)}catch(e){return e.errno}var errCode=FS.nodePermissions(dir,"wx");if(errCode){return errCode}if(isdir){if(!FS.isDir(node.mode)){return 54}if(FS.isRoot(node)||FS.getPath(node)===FS.cwd()){return 10}}else{if(FS.isDir(node.mode)){return 31}}return 0},mayOpen(node,flags){if(!node){return 44}if(FS.isLink(node.mode)){return 32}else if(FS.isDir(node.mode)){if(FS.flagsToPermissionString(flags)!=="r"||flags&(512|64)){return 31}}return FS.nodePermissions(node,FS.flagsToPermissionString(flags))},checkOpExists(op,err){if(!op){throw new FS.ErrnoError(err)}return op},MAX_OPEN_FDS:4096,nextfd(){for(var fd=0;fd<=FS.MAX_OPEN_FDS;fd++){if(!FS.streams[fd]){return fd}}throw new FS.ErrnoError(33)},getStreamChecked(fd){var stream=FS.getStream(fd);if(!stream){throw new FS.ErrnoError(8)}return stream},getStream:fd=>FS.streams[fd],createStream(stream,fd=-1){stream=Object.assign(new FS.FSStream,stream);if(fd==-1){fd=FS.nextfd()}stream.fd=fd;FS.streams[fd]=stream;return stream},closeStream(fd){FS.streams[fd]=null},dupStream(origStream,fd=-1){var stream=FS.createStream(origStream,fd);stream.stream_ops?.dup?.(stream);return stream},doSetAttr(stream,node,attr){var setattr=stream?.stream_ops.setattr;var arg=setattr?stream:node;setattr??=node.node_ops.setattr;FS.checkOpExists(setattr,63);setattr(arg,attr)},chrdev_stream_ops:{open(stream){var device=FS.getDevice(stream.node.rdev);stream.stream_ops=device.stream_ops;stream.stream_ops.open?.(stream)},llseek(){throw new FS.ErrnoError(70)}},major:dev=>dev>>8,minor:dev=>dev&255,makedev:(ma,mi)=>ma<<8|mi,registerDevice(dev,ops){FS.devices[dev]={stream_ops:ops}},getDevice:dev=>FS.devices[dev],getMounts(mount){var mounts=[];var check=[mount];while(check.length){var m=check.pop();mounts.push(m);check.push(...m.mounts)}return mounts},syncfs(populate,callback){if(typeof populate=="function"){callback=populate;populate=false}FS.syncFSRequests++;if(FS.syncFSRequests>1){err(`warning: ${FS.syncFSRequests} FS.syncfs operations in flight at once, probably just doing extra work`)}var mounts=FS.getMounts(FS.root.mount);var completed=0;function doCallback(errCode){FS.syncFSRequests--;return callback(errCode)}function done(errCode){if(errCode){if(!done.errored){done.errored=true;return doCallback(errCode)}return}if(++completed>=mounts.length){doCallback(null)}}for(var mount of mounts){if(mount.type.syncfs){mount.type.syncfs(mount,populate,done)}else{done(null)}}},mount(type,opts,mountpoint){var root=mountpoint==="/";var pseudo=!mountpoint;var node;if(root&&FS.root){throw new FS.ErrnoError(10)}else if(!root&&!pseudo){var lookup=FS.lookupPath(mountpoint,{follow_mount:false});mountpoint=lookup.path;node=lookup.node;if(FS.isMountpoint(node)){throw new FS.ErrnoError(10)}if(!FS.isDir(node.mode)){throw new FS.ErrnoError(54)}}var mount={type,opts,mountpoint,mounts:[]};var mountRoot=type.mount(mount);mountRoot.mount=mount;mount.root=mountRoot;if(root){FS.root=mountRoot}else if(node){node.mounted=mount;if(node.mount){node.mount.mounts.push(mount)}}return mountRoot},unmount(mountpoint){var lookup=FS.lookupPath(mountpoint,{follow_mount:false});if(!FS.isMountpoint(lookup.node)){throw new FS.ErrnoError(28)}var node=lookup.node;var mount=node.mounted;var mounts=FS.getMounts(mount);for(var[hash,current]of Object.entries(FS.nameTable)){while(current){var next=current.name_next;if(mounts.includes(current.mount)){FS.destroyNode(current)}current=next}}node.mounted=null;var idx=node.mount.mounts.indexOf(mount);node.mount.mounts.splice(idx,1)},lookup(parent,name){return parent.node_ops.lookup(parent,name)},mknod(path,mode,dev){var lookup=FS.lookupPath(path,{parent:true});var parent=lookup.node;var name=PATH.basename(path);if(!name){throw new FS.ErrnoError(28)}if(name==="."||name===".."){throw new FS.ErrnoError(20)}var errCode=FS.mayCreate(parent,name);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.mknod){throw new FS.ErrnoError(63)}return parent.node_ops.mknod(parent,name,mode,dev)},statfs(path){return FS.statfsNode(FS.lookupPath(path,{follow:true}).node)},statfsStream(stream){return FS.statfsNode(stream.node)},statfsNode(node){var rtn={bsize:4096,frsize:4096,blocks:1e6,bfree:5e5,bavail:5e5,files:FS.nextInode,ffree:FS.nextInode-1,fsid:42,flags:2,namelen:255};if(node.node_ops.statfs){Object.assign(rtn,node.node_ops.statfs(node.mount.opts.root))}return rtn},create(path,mode=438){mode&=4095;mode|=32768;return FS.mknod(path,mode,0)},mkdir(path,mode=511){mode&=511|512;mode|=16384;return FS.mknod(path,mode,0)},mkdirTree(path,mode){var dirs=path.split("/");var d="";for(var dir of dirs){if(!dir)continue;if(d||PATH.isAbs(path))d+="/";d+=dir;try{FS.mkdir(d,mode)}catch(e){if(e.errno!=20)throw e}}},mkdev(path,mode,dev){if(typeof dev=="undefined"){dev=mode;mode=438}mode|=8192;return FS.mknod(path,mode,dev)},symlink(oldpath,newpath){if(!PATH_FS.resolve(oldpath)){throw new FS.ErrnoError(44)}var lookup=FS.lookupPath(newpath,{parent:true});var parent=lookup.node;if(!parent){throw new FS.ErrnoError(44)}var newname=PATH.basename(newpath);var errCode=FS.mayCreate(parent,newname);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.symlink){throw new FS.ErrnoError(63)}return parent.node_ops.symlink(parent,newname,oldpath)},rename(old_path,new_path){var old_dirname=PATH.dirname(old_path);var new_dirname=PATH.dirname(new_path);var old_name=PATH.basename(old_path);var new_name=PATH.basename(new_path);var lookup,old_dir,new_dir;lookup=FS.lookupPath(old_path,{parent:true});old_dir=lookup.node;lookup=FS.lookupPath(new_path,{parent:true});new_dir=lookup.node;if(!old_dir||!new_dir)throw new FS.ErrnoError(44);if(old_dir.mount!==new_dir.mount){throw new FS.ErrnoError(75)}var old_node=FS.lookupNode(old_dir,old_name);var relative=PATH_FS.relative(old_path,new_dirname);if(relative.charAt(0)!=="."){throw new FS.ErrnoError(28)}relative=PATH_FS.relative(new_path,old_dirname);if(relative.charAt(0)!=="."){throw new FS.ErrnoError(55)}var new_node;try{new_node=FS.lookupNode(new_dir,new_name)}catch(e){}if(old_node===new_node){return}var isdir=FS.isDir(old_node.mode);var errCode=FS.mayDelete(old_dir,old_name,isdir);if(errCode){throw new FS.ErrnoError(errCode)}errCode=new_node?FS.mayDelete(new_dir,new_name,isdir):FS.mayCreate(new_dir,new_name);if(errCode){throw new FS.ErrnoError(errCode)}if(!old_dir.node_ops.rename){throw new FS.ErrnoError(63)}if(FS.isMountpoint(old_node)||new_node&&FS.isMountpoint(new_node)){throw new FS.ErrnoError(10)}if(new_dir!==old_dir){errCode=FS.nodePermissions(old_dir,"w");if(errCode){throw new FS.ErrnoError(errCode)}}FS.hashRemoveNode(old_node);try{old_dir.node_ops.rename(old_node,new_dir,new_name);old_node.parent=new_dir}catch(e){throw e}finally{FS.hashAddNode(old_node)}},rmdir(path){var lookup=FS.lookupPath(path,{parent:true});var parent=lookup.node;var name=PATH.basename(path);var node=FS.lookupNode(parent,name);var errCode=FS.mayDelete(parent,name,true);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.rmdir){throw new FS.ErrnoError(63)}if(FS.isMountpoint(node)){throw new FS.ErrnoError(10)}parent.node_ops.rmdir(parent,name);FS.destroyNode(node)},readdir(path){var lookup=FS.lookupPath(path,{follow:true});var node=lookup.node;var readdir=FS.checkOpExists(node.node_ops.readdir,54);return readdir(node)},unlink(path){var lookup=FS.lookupPath(path,{parent:true});var parent=lookup.node;if(!parent){throw new FS.ErrnoError(44)}var name=PATH.basename(path);var node=FS.lookupNode(parent,name);var errCode=FS.mayDelete(parent,name,false);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.unlink){throw new FS.ErrnoError(63)}if(FS.isMountpoint(node)){throw new FS.ErrnoError(10)}parent.node_ops.unlink(parent,name);FS.destroyNode(node)},readlink(path){var lookup=FS.lookupPath(path);var link=lookup.node;if(!link){throw new FS.ErrnoError(44)}if(!link.node_ops.readlink){throw new FS.ErrnoError(28)}return link.node_ops.readlink(link)},stat(path,dontFollow){var lookup=FS.lookupPath(path,{follow:!dontFollow});var node=lookup.node;var getattr=FS.checkOpExists(node.node_ops.getattr,63);return getattr(node)},fstat(fd){var stream=FS.getStreamChecked(fd);var node=stream.node;var getattr=stream.stream_ops.getattr;var arg=getattr?stream:node;getattr??=node.node_ops.getattr;FS.checkOpExists(getattr,63);return getattr(arg)},lstat(path){return FS.stat(path,true)},doChmod(stream,node,mode,dontFollow){FS.doSetAttr(stream,node,{mode:mode&4095|node.mode&~4095,ctime:Date.now(),dontFollow})},chmod(path,mode,dontFollow){var node;if(typeof path=="string"){var lookup=FS.lookupPath(path,{follow:!dontFollow});node=lookup.node}else{node=path}FS.doChmod(null,node,mode,dontFollow)},lchmod(path,mode){FS.chmod(path,mode,true)},fchmod(fd,mode){var stream=FS.getStreamChecked(fd);FS.doChmod(stream,stream.node,mode,false)},doChown(stream,node,dontFollow){FS.doSetAttr(stream,node,{timestamp:Date.now(),dontFollow})},chown(path,uid,gid,dontFollow){var node;if(typeof path=="string"){var lookup=FS.lookupPath(path,{follow:!dontFollow});node=lookup.node}else{node=path}FS.doChown(null,node,dontFollow)},lchown(path,uid,gid){FS.chown(path,uid,gid,true)},fchown(fd,uid,gid){var stream=FS.getStreamChecked(fd);FS.doChown(stream,stream.node,false)},doTruncate(stream,node,len){if(FS.isDir(node.mode)){throw new FS.ErrnoError(31)}if(!FS.isFile(node.mode)){throw new FS.ErrnoError(28)}var errCode=FS.nodePermissions(node,"w");if(errCode){throw new FS.ErrnoError(errCode)}FS.doSetAttr(stream,node,{size:len,timestamp:Date.now()})},truncate(path,len){if(len<0){throw new FS.ErrnoError(28)}var node;if(typeof path=="string"){var lookup=FS.lookupPath(path,{follow:true});node=lookup.node}else{node=path}FS.doTruncate(null,node,len)},ftruncate(fd,len){var stream=FS.getStreamChecked(fd);if(len<0||(stream.flags&2097155)===0){throw new FS.ErrnoError(28)}FS.doTruncate(stream,stream.node,len)},utime(path,atime,mtime){var lookup=FS.lookupPath(path,{follow:true});var node=lookup.node;var setattr=FS.checkOpExists(node.node_ops.setattr,63);setattr(node,{atime,mtime})},open(path,flags,mode=438){if(path===""){throw new FS.ErrnoError(44)}flags=typeof flags=="string"?FS_modeStringToFlags(flags):flags;if(flags&64){mode=mode&4095|32768}else{mode=0}var node;var isDirPath;if(typeof path=="object"){node=path}else{isDirPath=path.endsWith("/");var lookup=FS.lookupPath(path,{follow:!(flags&131072),noent_okay:true});node=lookup.node;path=lookup.path}var created=false;if(flags&64){if(node){if(flags&128){throw new FS.ErrnoError(20)}}else if(isDirPath){throw new FS.ErrnoError(31)}else{node=FS.mknod(path,mode|511,0);created=true}}if(!node){throw new FS.ErrnoError(44)}if(FS.isChrdev(node.mode)){flags&=~512}if(flags&65536&&!FS.isDir(node.mode)){throw new FS.ErrnoError(54)}if(!created){var errCode=FS.mayOpen(node,flags);if(errCode){throw new FS.ErrnoError(errCode)}}if(flags&512&&!created){FS.truncate(node,0)}flags&=~(128|512|131072);var stream=FS.createStream({node,path:FS.getPath(node),flags,seekable:true,position:0,stream_ops:node.stream_ops,ungotten:[],error:false});if(stream.stream_ops.open){stream.stream_ops.open(stream)}if(created){FS.chmod(node,mode&511)}if(Module["logReadFiles"]&&!(flags&1)){if(!(path in FS.readFiles)){FS.readFiles[path]=1}}return stream},close(stream){if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if(stream.getdents)stream.getdents=null;try{if(stream.stream_ops.close){stream.stream_ops.close(stream)}}catch(e){throw e}finally{FS.closeStream(stream.fd)}stream.fd=null},isClosed(stream){return stream.fd===null},llseek(stream,offset,whence){if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if(!stream.seekable||!stream.stream_ops.llseek){throw new FS.ErrnoError(70)}if(whence!=0&&whence!=1&&whence!=2){throw new FS.ErrnoError(28)}stream.position=stream.stream_ops.llseek(stream,offset,whence);stream.ungotten=[];return stream.position},read(stream,buffer,offset,length,position){if(length<0||position<0){throw new FS.ErrnoError(28)}if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if((stream.flags&2097155)===1){throw new FS.ErrnoError(8)}if(FS.isDir(stream.node.mode)){throw new FS.ErrnoError(31)}if(!stream.stream_ops.read){throw new FS.ErrnoError(28)}var seeking=typeof position!="undefined";if(!seeking){position=stream.position}else if(!stream.seekable){throw new FS.ErrnoError(70)}var bytesRead=stream.stream_ops.read(stream,buffer,offset,length,position);if(!seeking)stream.position+=bytesRead;return bytesRead},write(stream,buffer,offset,length,position,canOwn){if(length<0||position<0){throw new FS.ErrnoError(28)}if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if((stream.flags&2097155)===0){throw new FS.ErrnoError(8)}if(FS.isDir(stream.node.mode)){throw new FS.ErrnoError(31)}if(!stream.stream_ops.write){throw new FS.ErrnoError(28)}if(stream.seekable&&stream.flags&1024){FS.llseek(stream,0,2)}var seeking=typeof position!="undefined";if(!seeking){position=stream.position}else if(!stream.seekable){throw new FS.ErrnoError(70)}var bytesWritten=stream.stream_ops.write(stream,buffer,offset,length,position,canOwn);if(!seeking)stream.position+=bytesWritten;return bytesWritten},mmap(stream,length,position,prot,flags){if((prot&2)!==0&&(flags&2)===0&&(stream.flags&2097155)!==2){throw new FS.ErrnoError(2)}if((stream.flags&2097155)===1){throw new FS.ErrnoError(2)}if(!stream.stream_ops.mmap){throw new FS.ErrnoError(43)}if(!length){throw new FS.ErrnoError(28)}return stream.stream_ops.mmap(stream,length,position,prot,flags)},msync(stream,buffer,offset,length,mmapFlags){if(!stream.stream_ops.msync){return 0}return stream.stream_ops.msync(stream,buffer,offset,length,mmapFlags)},ioctl(stream,cmd,arg){if(!stream.stream_ops.ioctl){throw new FS.ErrnoError(59)}return stream.stream_ops.ioctl(stream,cmd,arg)},readFile(path,opts={}){opts.flags=opts.flags||0;opts.encoding=opts.encoding||"binary";if(opts.encoding!=="utf8"&&opts.encoding!=="binary"){abort(`Invalid encoding type "${opts.encoding}"`)}var stream=FS.open(path,opts.flags);var stat=FS.stat(path);var length=stat.size;var buf=new Uint8Array(length);FS.read(stream,buf,0,length,0);if(opts.encoding==="utf8"){buf=UTF8ArrayToString(buf)}FS.close(stream);return buf},writeFile(path,data,opts={}){opts.flags=opts.flags||577;var stream=FS.open(path,opts.flags,opts.mode);if(typeof data=="string"){data=new Uint8Array(intArrayFromString(data,true))}if(ArrayBuffer.isView(data)){FS.write(stream,data,0,data.byteLength,undefined,opts.canOwn)}else{abort("Unsupported data type")}FS.close(stream)},cwd:()=>FS.currentPath,chdir(path){var lookup=FS.lookupPath(path,{follow:true});if(lookup.node===null){throw new FS.ErrnoError(44)}if(!FS.isDir(lookup.node.mode)){throw new FS.ErrnoError(54)}var errCode=FS.nodePermissions(lookup.node,"x");if(errCode){throw new FS.ErrnoError(errCode)}FS.currentPath=lookup.path},createDefaultDirectories(){FS.mkdir("/tmp");FS.mkdir("/home");FS.mkdir("/home/web_user")},createDefaultDevices(){FS.mkdir("/dev");FS.registerDevice(FS.makedev(1,3),{read:()=>0,write:(stream,buffer,offset,length,pos)=>length,llseek:()=>0});FS.mkdev("/dev/null",FS.makedev(1,3));TTY.register(FS.makedev(5,0),TTY.default_tty_ops);TTY.register(FS.makedev(6,0),TTY.default_tty1_ops);FS.mkdev("/dev/tty",FS.makedev(5,0));FS.mkdev("/dev/tty1",FS.makedev(6,0));var randomBuffer=new Uint8Array(1024),randomLeft=0;var randomByte=()=>{if(randomLeft===0){randomFill(randomBuffer);randomLeft=randomBuffer.byteLength}return randomBuffer[--randomLeft]};FS.createDevice("/dev","random",randomByte);FS.createDevice("/dev","urandom",randomByte);FS.mkdir("/dev/shm");FS.mkdir("/dev/shm/tmp")},createSpecialDirectories(){FS.mkdir("/proc");var proc_self=FS.mkdir("/proc/self");FS.mkdir("/proc/self/fd");FS.mount({mount(){var node=FS.createNode(proc_self,"fd",16895,73);node.stream_ops={llseek:MEMFS.stream_ops.llseek};node.node_ops={lookup(parent,name){var fd=+name;var stream=FS.getStreamChecked(fd);var ret={parent:null,mount:{mountpoint:"fake"},node_ops:{readlink:()=>stream.path},id:fd+1};ret.parent=ret;return ret},readdir(){return Array.from(FS.streams.entries()).filter(([k,v])=>v).map(([k,v])=>k.toString())}};return node}},{},"/proc/self/fd")},createStandardStreams(input,output,error){if(input){FS.createDevice("/dev","stdin",input)}else{FS.symlink("/dev/tty","/dev/stdin")}if(output){FS.createDevice("/dev","stdout",null,output)}else{FS.symlink("/dev/tty","/dev/stdout")}if(error){FS.createDevice("/dev","stderr",null,error)}else{FS.symlink("/dev/tty1","/dev/stderr")}var stdin=FS.open("/dev/stdin",0);var stdout=FS.open("/dev/stdout",1);var stderr=FS.open("/dev/stderr",1)},staticInit(){FS.nameTable=new Array(4096);FS.mount(MEMFS,{},"/");FS.createDefaultDirectories();FS.createDefaultDevices();FS.createSpecialDirectories();FS.filesystems={MEMFS}},init(input,output,error){FS.initialized=true;input??=Module["stdin"];output??=Module["stdout"];error??=Module["stderr"];FS.createStandardStreams(input,output,error)},quit(){FS.initialized=false;for(var stream of FS.streams){if(stream){FS.close(stream)}}},findObject(path,dontResolveLastLink){var ret=FS.analyzePath(path,dontResolveLastLink);if(!ret.exists){return null}return ret.object},analyzePath(path,dontResolveLastLink){try{var lookup=FS.lookupPath(path,{follow:!dontResolveLastLink});path=lookup.path}catch(e){}var ret={isRoot:false,exists:false,error:0,name:null,path:null,object:null,parentExists:false,parentPath:null,parentObject:null};try{var lookup=FS.lookupPath(path,{parent:true});ret.parentExists=true;ret.parentPath=lookup.path;ret.parentObject=lookup.node;ret.name=PATH.basename(path);lookup=FS.lookupPath(path,{follow:!dontResolveLastLink});ret.exists=true;ret.path=lookup.path;ret.object=lookup.node;ret.name=lookup.node.name;ret.isRoot=lookup.path==="/"}catch(e){ret.error=e.errno}return ret},createPath(parent,path,canRead,canWrite){parent=typeof parent=="string"?parent:FS.getPath(parent);var parts=path.split("/").reverse();while(parts.length){var part=parts.pop();if(!part)continue;var current=PATH.join2(parent,part);try{FS.mkdir(current)}catch(e){if(e.errno!=20)throw e}parent=current}return current},createFile(parent,name,properties,canRead,canWrite){var path=PATH.join2(typeof parent=="string"?parent:FS.getPath(parent),name);var mode=FS_getMode(canRead,canWrite);return FS.create(path,mode)},createDataFile(parent,name,data,canRead,canWrite,canOwn){var path=name;if(parent){parent=typeof parent=="string"?parent:FS.getPath(parent);path=name?PATH.join2(parent,name):parent}var mode=FS_getMode(canRead,canWrite);var node=FS.create(path,mode);if(data){if(typeof data=="string"){var arr=new Array(data.length);for(var i=0,len=data.length;ithis.length-1||idx<0){return undefined}var chunkOffset=idx%this.chunkSize;var chunkNum=idx/this.chunkSize|0;return this.getter(chunkNum)[chunkOffset]}setDataGetter(getter){this.getter=getter}cacheLength(){var xhr=new XMLHttpRequest;xhr.open("HEAD",url,false);xhr.send(null);if(!(xhr.status>=200&&xhr.status<300||xhr.status===304))abort("Couldn't load "+url+". Status: "+xhr.status);var datalength=Number(xhr.getResponseHeader("Content-length"));var header;var hasByteServing=(header=xhr.getResponseHeader("Accept-Ranges"))&&header==="bytes";var usesGzip=(header=xhr.getResponseHeader("Content-Encoding"))&&header==="gzip";var chunkSize=1024*1024;if(!hasByteServing)chunkSize=datalength;var doXHR=(from,to)=>{if(from>to)abort("invalid range ("+from+", "+to+") or no bytes requested!");if(to>datalength-1)abort("only "+datalength+" bytes available! programmer error!");var xhr=new XMLHttpRequest;xhr.open("GET",url,false);if(datalength!==chunkSize)xhr.setRequestHeader("Range","bytes="+from+"-"+to);xhr.responseType="arraybuffer";if(xhr.overrideMimeType){xhr.overrideMimeType("text/plain; charset=x-user-defined")}xhr.send(null);if(!(xhr.status>=200&&xhr.status<300||xhr.status===304))abort("Couldn't load "+url+". Status: "+xhr.status);if(xhr.response!==undefined){return new Uint8Array(xhr.response||[])}return intArrayFromString(xhr.responseText||"",true)};var lazyArray=this;lazyArray.setDataGetter(chunkNum=>{var start=chunkNum*chunkSize;var end=(chunkNum+1)*chunkSize-1;end=Math.min(end,datalength-1);if(typeof lazyArray.chunks[chunkNum]=="undefined"){lazyArray.chunks[chunkNum]=doXHR(start,end)}if(typeof lazyArray.chunks[chunkNum]=="undefined")abort("doXHR failed!");return lazyArray.chunks[chunkNum]});if(usesGzip||!datalength){chunkSize=datalength=1;datalength=this.getter(0).length;chunkSize=datalength;out("LazyFiles on gzip forces download of the whole file when length is accessed")}this._length=datalength;this._chunkSize=chunkSize;this.lengthKnown=true}get length(){if(!this.lengthKnown){this.cacheLength()}return this._length}get chunkSize(){if(!this.lengthKnown){this.cacheLength()}return this._chunkSize}}if(globalThis.XMLHttpRequest){if(!ENVIRONMENT_IS_WORKER)abort("Cannot do synchronous binary XHRs outside webworkers in modern browsers. Use --embed-file or --preload-file in emcc");var lazyArray=new LazyUint8Array;var properties={isDevice:false,contents:lazyArray}}else{var properties={isDevice:false,url}}var node=FS.createFile(parent,name,properties,canRead,canWrite);if(properties.contents){node.contents=properties.contents}else if(properties.url){node.contents=null;node.url=properties.url}Object.defineProperties(node,{usedBytes:{get:function(){return this.contents.length}}});var stream_ops={};for(const[key,fn]of Object.entries(node.stream_ops)){stream_ops[key]=(...args)=>{FS.forceLoadFile(node);return fn(...args)}}function writeChunks(stream,buffer,offset,length,position){var contents=stream.node.contents;if(position>=contents.length)return 0;var size=Math.min(contents.length-position,length);if(contents.slice){for(var i=0;i{FS.forceLoadFile(node);return writeChunks(stream,buffer,offset,length,position)};stream_ops.mmap=(stream,length,position,prot,flags)=>{FS.forceLoadFile(node);var ptr=mmapAlloc(length);if(!ptr){throw new FS.ErrnoError(48)}writeChunks(stream,HEAP8,ptr,length,position);return{ptr,allocated:true}};node.stream_ops=stream_ops;return node}};var UTF8ToString=(ptr,maxBytesToRead,ignoreNul)=>{ptr>>>=0;return ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead,ignoreNul):""};var SYSCALLS={DEFAULT_POLLMASK:5,calculateAt(dirfd,path,allowEmpty){if(PATH.isAbs(path)){return path}var dir;if(dirfd===-100){dir=FS.cwd()}else{var dirstream=SYSCALLS.getStreamFromFD(dirfd);dir=dirstream.path}if(path.length==0){if(!allowEmpty){throw new FS.ErrnoError(44)}return dir}return dir+"/"+path},writeStat(buf,stat){HEAPU32[buf>>>2>>>0]=stat.dev;HEAPU32[buf+4>>>2>>>0]=stat.mode;HEAPU32[buf+8>>>2>>>0]=stat.nlink;HEAPU32[buf+12>>>2>>>0]=stat.uid;HEAPU32[buf+16>>>2>>>0]=stat.gid;HEAPU32[buf+20>>>2>>>0]=stat.rdev;HEAP64[buf+24>>>3>>>0]=BigInt(stat.size);HEAP32[buf+32>>>2>>>0]=4096;HEAP32[buf+36>>>2>>>0]=stat.blocks;var atime=stat.atime.getTime();var mtime=stat.mtime.getTime();var ctime=stat.ctime.getTime();HEAP64[buf+40>>>3>>>0]=BigInt(Math.floor(atime/1e3));HEAPU32[buf+48>>>2>>>0]=atime%1e3*1e3*1e3;HEAP64[buf+56>>>3>>>0]=BigInt(Math.floor(mtime/1e3));HEAPU32[buf+64>>>2>>>0]=mtime%1e3*1e3*1e3;HEAP64[buf+72>>>3>>>0]=BigInt(Math.floor(ctime/1e3));HEAPU32[buf+80>>>2>>>0]=ctime%1e3*1e3*1e3;HEAP64[buf+88>>>3>>>0]=BigInt(stat.ino);return 0},writeStatFs(buf,stats){HEAPU32[buf+4>>>2>>>0]=stats.bsize;HEAPU32[buf+60>>>2>>>0]=stats.bsize;HEAP64[buf+8>>>3>>>0]=BigInt(stats.blocks);HEAP64[buf+16>>>3>>>0]=BigInt(stats.bfree);HEAP64[buf+24>>>3>>>0]=BigInt(stats.bavail);HEAP64[buf+32>>>3>>>0]=BigInt(stats.files);HEAP64[buf+40>>>3>>>0]=BigInt(stats.ffree);HEAPU32[buf+48>>>2>>>0]=stats.fsid;HEAPU32[buf+64>>>2>>>0]=stats.flags;HEAPU32[buf+56>>>2>>>0]=stats.namelen},doMsync(addr,stream,len,flags,offset){if(!FS.isFile(stream.node.mode)){throw new FS.ErrnoError(43)}if(flags&2){return 0}var buffer=HEAPU8.slice(addr,addr+len);FS.msync(stream,buffer,offset,len,flags)},getStreamFromFD(fd){var stream=FS.getStreamChecked(fd);return stream},varargs:undefined,getStr(ptr){var ret=UTF8ToString(ptr);return ret}};function ___syscall_fstat64(fd,buf){buf>>>=0;try{return SYSCALLS.writeStat(buf,FS.fstat(fd))}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_newfstatat(dirfd,path,buf,flags){path>>>=0;buf>>>=0;try{path=SYSCALLS.getStr(path);var nofollow=flags&256;var allowEmpty=flags&4096;flags=flags&~6400;path=SYSCALLS.calculateAt(dirfd,path,allowEmpty);return SYSCALLS.writeStat(buf,nofollow?FS.lstat(path):FS.stat(path))}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}var syscallGetVarargI=()=>{var ret=HEAP32[+SYSCALLS.varargs>>>2>>>0];SYSCALLS.varargs+=4;return ret};function ___syscall_openat(dirfd,path,flags,varargs){path>>>=0;varargs>>>=0;SYSCALLS.varargs=varargs;try{path=SYSCALLS.getStr(path);path=SYSCALLS.calculateAt(dirfd,path);var mode=varargs?syscallGetVarargI():0;return FS.open(path,flags,mode).fd}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}var __abort_js=()=>abort("");var structRegistrations={};var runDestructors=destructors=>{while(destructors.length){var ptr=destructors.pop();var del=destructors.pop();del(ptr)}};function readPointer(pointer){return this.fromWireType(HEAPU32[pointer>>>2>>>0])}var awaitingDependencies={};var registeredTypes={};var typeDependencies={};var InternalError=class InternalError extends Error{constructor(message){super(message);this.name="InternalError"}};var throwInternalError=message=>{throw new InternalError(message)};var whenDependentTypesAreResolved=(myTypes,dependentTypes,getTypeConverters)=>{myTypes.forEach(type=>typeDependencies[type]=dependentTypes);function onComplete(typeConverters){var myTypeConverters=getTypeConverters(typeConverters);if(myTypeConverters.length!==myTypes.length){throwInternalError("Mismatched type converter count")}for(var i=0;i{typeConverters[i]=registeredTypes[dt];++registered;if(registered===unregisteredTypes.length){onComplete(typeConverters)}})}}if(0===unregisteredTypes.length){onComplete(typeConverters)}};var __embind_finalize_value_object=function(structType){structType>>>=0;var reg=structRegistrations[structType];delete structRegistrations[structType];var rawConstructor=reg.rawConstructor;var rawDestructor=reg.rawDestructor;var fieldRecords=reg.fields;var fieldTypes=fieldRecords.map(field=>field.getterReturnType).concat(fieldRecords.map(field=>field.setterArgumentType));whenDependentTypesAreResolved([structType],fieldTypes,fieldTypes=>{var fields={};for(var[i,field]of fieldRecords.entries()){const getterReturnType=fieldTypes[i];const getter=field.getter;const getterContext=field.getterContext;const setterArgumentType=fieldTypes[i+fieldRecords.length];const setter=field.setter;const setterContext=field.setterContext;fields[field.fieldName]={read:ptr=>getterReturnType.fromWireType(getter(getterContext,ptr)),write:(ptr,o)=>{var destructors=[];setter(setterContext,ptr,setterArgumentType.toWireType(destructors,o));runDestructors(destructors)},optional:getterReturnType.optional}}return[{name:reg.name,fromWireType:ptr=>{var rv={};for(var i in fields){rv[i]=fields[i].read(ptr)}rawDestructor(ptr);return rv},toWireType:(destructors,o)=>{for(var fieldName in fields){if(!(fieldName in o)&&!fields[fieldName].optional){throw new TypeError(`Missing field: "${fieldName}"`)}}var ptr=rawConstructor();for(fieldName in fields){fields[fieldName].write(ptr,o[fieldName])}if(destructors!==null){destructors.push(rawDestructor,ptr)}return ptr},readValueFromPointer:readPointer,destructorFunction:rawDestructor}]})};var AsciiToString=ptr=>{ptr>>>=0;var str="";while(1){var ch=HEAPU8[ptr++>>>0];if(!ch)return str;str+=String.fromCharCode(ch)}};var BindingError=class BindingError extends Error{constructor(message){super(message);this.name="BindingError"}};var throwBindingError=message=>{throw new BindingError(message)};function sharedRegisterType(rawType,registeredInstance,options={}){var name=registeredInstance.name;if(!rawType){throwBindingError(`type "${name}" must have a positive integer typeid pointer`)}if(registeredTypes.hasOwnProperty(rawType)){if(options.ignoreDuplicateRegistrations){return}else{throwBindingError(`Cannot register type '${name}' twice`)}}registeredTypes[rawType]=registeredInstance;delete typeDependencies[rawType];if(awaitingDependencies.hasOwnProperty(rawType)){var callbacks=awaitingDependencies[rawType];delete awaitingDependencies[rawType];callbacks.forEach(cb=>cb())}}function registerType(rawType,registeredInstance,options={}){return sharedRegisterType(rawType,registeredInstance,options)}var integerReadValueFromPointer=(name,width,signed)=>{switch(width){case 1:return signed?pointer=>HEAP8[pointer>>>0]:pointer=>HEAPU8[pointer>>>0];case 2:return signed?pointer=>HEAP16[pointer>>>1>>>0]:pointer=>HEAPU16[pointer>>>1>>>0];case 4:return signed?pointer=>HEAP32[pointer>>>2>>>0]:pointer=>HEAPU32[pointer>>>2>>>0];case 8:return signed?pointer=>HEAP64[pointer>>>3>>>0]:pointer=>HEAPU64[pointer>>>3>>>0];default:throw new TypeError(`invalid integer width (${width}): ${name}`)}};var __embind_register_bigint=function(primitiveType,name,size,minRange,maxRange){primitiveType>>>=0;name>>>=0;size>>>=0;name=AsciiToString(name);const isUnsignedType=minRange===0n;let fromWireType=value=>value;if(isUnsignedType){const bitSize=size*8;fromWireType=value=>BigInt.asUintN(bitSize,value);maxRange=fromWireType(maxRange)}registerType(primitiveType,{name,fromWireType,toWireType:(destructors,value)=>{if(typeof value=="number"){value=BigInt(value)}return value},readValueFromPointer:integerReadValueFromPointer(name,size,!isUnsignedType),destructorFunction:null})};function __embind_register_bool(rawType,name,trueValue,falseValue){rawType>>>=0;name>>>=0;name=AsciiToString(name);registerType(rawType,{name,fromWireType:function(wt){return!!wt},toWireType:function(destructors,o){return o?trueValue:falseValue},readValueFromPointer:function(pointer){return this.fromWireType(HEAPU8[pointer>>>0])},destructorFunction:null})}var shallowCopyInternalPointer=o=>({count:o.count,deleteScheduled:o.deleteScheduled,preservePointerOnDelete:o.preservePointerOnDelete,ptr:o.ptr,ptrType:o.ptrType,smartPtr:o.smartPtr,smartPtrType:o.smartPtrType});var throwInstanceAlreadyDeleted=obj=>{function getInstanceTypeName(handle){return handle.$$.ptrType.registeredClass.name}throwBindingError(getInstanceTypeName(obj)+" instance already deleted")};var finalizationRegistry=false;var detachFinalizer=handle=>{};var runDestructor=$$=>{if($$.smartPtr){$$.smartPtrType.rawDestructor($$.smartPtr)}else{$$.ptrType.registeredClass.rawDestructor($$.ptr)}};var releaseClassHandle=$$=>{$$.count.value-=1;var toDelete=0===$$.count.value;if(toDelete){runDestructor($$)}};var attachFinalizer=handle=>{if(!globalThis.FinalizationRegistry){attachFinalizer=handle=>handle;return handle}finalizationRegistry=new FinalizationRegistry(info=>{releaseClassHandle(info.$$)});attachFinalizer=handle=>{var $$=handle.$$;var hasSmartPtr=!!$$.smartPtr;if(hasSmartPtr){var info={$$};finalizationRegistry.register(handle,info,handle)}return handle};detachFinalizer=handle=>finalizationRegistry.unregister(handle);return attachFinalizer(handle)};var deletionQueue=[];var flushPendingDeletes=()=>{while(deletionQueue.length){var obj=deletionQueue.pop();obj.$$.deleteScheduled=false;obj["delete"]()}};var delayFunction;var init_ClassHandle=()=>{let proto=ClassHandle.prototype;Object.assign(proto,{isAliasOf(other){if(!(this instanceof ClassHandle)){return false}if(!(other instanceof ClassHandle)){return false}var leftClass=this.$$.ptrType.registeredClass;var left=this.$$.ptr;other.$$=other.$$;var rightClass=other.$$.ptrType.registeredClass;var right=other.$$.ptr;while(leftClass.baseClass){left=leftClass.upcast(left);leftClass=leftClass.baseClass}while(rightClass.baseClass){right=rightClass.upcast(right);rightClass=rightClass.baseClass}return leftClass===rightClass&&left===right},clone(){if(!this.$$.ptr){throwInstanceAlreadyDeleted(this)}if(this.$$.preservePointerOnDelete){this.$$.count.value+=1;return this}else{var clone=attachFinalizer(Object.create(Object.getPrototypeOf(this),{$$:{value:shallowCopyInternalPointer(this.$$)}}));clone.$$.count.value+=1;clone.$$.deleteScheduled=false;return clone}},delete(){if(!this.$$.ptr){throwInstanceAlreadyDeleted(this)}if(this.$$.deleteScheduled&&!this.$$.preservePointerOnDelete){throwBindingError("Object already scheduled for deletion")}detachFinalizer(this);releaseClassHandle(this.$$);if(!this.$$.preservePointerOnDelete){this.$$.smartPtr=undefined;this.$$.ptr=undefined}},isDeleted(){return!this.$$.ptr},deleteLater(){if(!this.$$.ptr){throwInstanceAlreadyDeleted(this)}if(this.$$.deleteScheduled&&!this.$$.preservePointerOnDelete){throwBindingError("Object already scheduled for deletion")}deletionQueue.push(this);if(deletionQueue.length===1&&delayFunction){delayFunction(flushPendingDeletes)}this.$$.deleteScheduled=true;return this}});const symbolDispose=Symbol.dispose;if(symbolDispose){proto[symbolDispose]=proto["delete"]}};function ClassHandle(){}var createNamedFunction=(name,func)=>Object.defineProperty(func,"name",{value:name});var registeredPointers={};var ensureOverloadTable=(proto,methodName,humanName)=>{if(undefined===proto[methodName].overloadTable){var prevFunc=proto[methodName];proto[methodName]=function(...args){if(!proto[methodName].overloadTable.hasOwnProperty(args.length)){throwBindingError(`Function '${humanName}' called with an invalid number of arguments (${args.length}) - expects one of (${proto[methodName].overloadTable})!`)}return proto[methodName].overloadTable[args.length].apply(this,args)};proto[methodName].overloadTable=[];proto[methodName].overloadTable[prevFunc.argCount]=prevFunc}};var exposePublicSymbol=(name,value,numArguments)=>{if(Module.hasOwnProperty(name)){if(undefined===numArguments||undefined!==Module[name].overloadTable&&undefined!==Module[name].overloadTable[numArguments]){throwBindingError(`Cannot register public name '${name}' twice`)}ensureOverloadTable(Module,name,name);if(Module[name].overloadTable.hasOwnProperty(numArguments)){throwBindingError(`Cannot register multiple overloads of a function with the same number of arguments (${numArguments})!`)}Module[name].overloadTable[numArguments]=value}else{Module[name]=value;Module[name].argCount=numArguments}};var char_0=48;var char_9=57;var makeLegalFunctionName=name=>{name=name.replace(/[^a-zA-Z0-9_]/g,"$");var f=name.charCodeAt(0);if(f>=char_0&&f<=char_9){return`_${name}`}return name};function RegisteredClass(name,constructor,instancePrototype,rawDestructor,baseClass,getActualType,upcast,downcast){this.name=name;this.constructor=constructor;this.instancePrototype=instancePrototype;this.rawDestructor=rawDestructor;this.baseClass=baseClass;this.getActualType=getActualType;this.upcast=upcast;this.downcast=downcast;this.pureVirtualFunctions=[]}var upcastPointer=(ptr,ptrClass,desiredClass)=>{while(ptrClass!==desiredClass){if(!ptrClass.upcast){throwBindingError(`Expected null or instance of ${desiredClass.name}, got an instance of ${ptrClass.name}`)}ptr=ptrClass.upcast(ptr);ptrClass=ptrClass.baseClass}return ptr};var embindRepr=v=>{if(v===null){return"null"}var t=typeof v;if(t==="object"||t==="array"||t==="function"){return v.toString()}else{return""+v}};function constNoSmartPtrRawPointerToWireType(destructors,handle){if(handle===null){if(this.isReference){throwBindingError(`null is not a valid ${this.name}`)}return 0}if(!handle.$$){throwBindingError(`Cannot pass "${embindRepr(handle)}" as a ${this.name}`)}if(!handle.$$.ptr){throwBindingError(`Cannot pass deleted object as a pointer of type ${this.name}`)}var handleClass=handle.$$.ptrType.registeredClass;var ptr=upcastPointer(handle.$$.ptr,handleClass,this.registeredClass);return ptr}function genericPointerToWireType(destructors,handle){var ptr;if(handle===null){if(this.isReference){throwBindingError(`null is not a valid ${this.name}`)}if(this.isSmartPointer){ptr=this.rawConstructor();if(destructors!==null){destructors.push(this.rawDestructor,ptr)}return ptr}else{return 0}}if(!handle||!handle.$$){throwBindingError(`Cannot pass "${embindRepr(handle)}" as a ${this.name}`)}if(!handle.$$.ptr){throwBindingError(`Cannot pass deleted object as a pointer of type ${this.name}`)}if(!this.isConst&&handle.$$.ptrType.isConst){throwBindingError(`Cannot convert argument of type ${handle.$$.smartPtrType?handle.$$.smartPtrType.name:handle.$$.ptrType.name} to parameter type ${this.name}`)}var handleClass=handle.$$.ptrType.registeredClass;ptr=upcastPointer(handle.$$.ptr,handleClass,this.registeredClass);if(this.isSmartPointer){if(undefined===handle.$$.smartPtr){throwBindingError("Passing raw pointer to smart pointer is illegal")}switch(this.sharingPolicy){case 0:if(handle.$$.smartPtrType===this){ptr=handle.$$.smartPtr}else{throwBindingError(`Cannot convert argument of type ${handle.$$.smartPtrType?handle.$$.smartPtrType.name:handle.$$.ptrType.name} to parameter type ${this.name}`)}break;case 1:ptr=handle.$$.smartPtr;break;case 2:if(handle.$$.smartPtrType===this){ptr=handle.$$.smartPtr}else{var clonedHandle=handle["clone"]();ptr=this.rawShare(ptr,Emval.toHandle(()=>clonedHandle["delete"]()));if(destructors!==null){destructors.push(this.rawDestructor,ptr)}}break;default:throwBindingError("Unsupporting sharing policy")}}return ptr}function nonConstNoSmartPtrRawPointerToWireType(destructors,handle){if(handle===null){if(this.isReference){throwBindingError(`null is not a valid ${this.name}`)}return 0}if(!handle.$$){throwBindingError(`Cannot pass "${embindRepr(handle)}" as a ${this.name}`)}if(!handle.$$.ptr){throwBindingError(`Cannot pass deleted object as a pointer of type ${this.name}`)}if(handle.$$.ptrType.isConst){throwBindingError(`Cannot convert argument of type ${handle.$$.ptrType.name} to parameter type ${this.name}`)}var handleClass=handle.$$.ptrType.registeredClass;var ptr=upcastPointer(handle.$$.ptr,handleClass,this.registeredClass);return ptr}var downcastPointer=(ptr,ptrClass,desiredClass)=>{if(ptrClass===desiredClass){return ptr}if(undefined===desiredClass.baseClass){return null}var rv=downcastPointer(ptr,ptrClass,desiredClass.baseClass);if(rv===null){return null}return desiredClass.downcast(rv)};var registeredInstances={};var getBasestPointer=(class_,ptr)=>{if(ptr===undefined){throwBindingError("ptr should not be undefined")}while(class_.baseClass){ptr=class_.upcast(ptr);class_=class_.baseClass}return ptr};var getInheritedInstance=(class_,ptr)=>{ptr=getBasestPointer(class_,ptr);return registeredInstances[ptr]};var makeClassHandle=(prototype,record)=>{if(!record.ptrType||!record.ptr){throwInternalError("makeClassHandle requires ptr and ptrType")}var hasSmartPtrType=!!record.smartPtrType;var hasSmartPtr=!!record.smartPtr;if(hasSmartPtrType!==hasSmartPtr){throwInternalError("Both smartPtrType and smartPtr must be specified")}record.count={value:1};return attachFinalizer(Object.create(prototype,{$$:{value:record,writable:true}}))};function RegisteredPointer_fromWireType(ptr){var rawPointer=this.getPointee(ptr);if(!rawPointer){this.destructor(ptr);return null}var registeredInstance=getInheritedInstance(this.registeredClass,rawPointer);if(undefined!==registeredInstance){if(0===registeredInstance.$$.count.value){registeredInstance.$$.ptr=rawPointer;registeredInstance.$$.smartPtr=ptr;return registeredInstance["clone"]()}else{var rv=registeredInstance["clone"]();this.destructor(ptr);return rv}}function makeDefaultHandle(){if(this.isSmartPointer){return makeClassHandle(this.registeredClass.instancePrototype,{ptrType:this.pointeeType,ptr:rawPointer,smartPtrType:this,smartPtr:ptr})}else{return makeClassHandle(this.registeredClass.instancePrototype,{ptrType:this,ptr})}}var actualType=this.registeredClass.getActualType(rawPointer);var registeredPointerRecord=registeredPointers[actualType];if(!registeredPointerRecord){return makeDefaultHandle.call(this)}var toType;if(this.isConst){toType=registeredPointerRecord.constPointerType}else{toType=registeredPointerRecord.pointerType}var dp=downcastPointer(rawPointer,this.registeredClass,toType.registeredClass);if(dp===null){return makeDefaultHandle.call(this)}if(this.isSmartPointer){return makeClassHandle(toType.registeredClass.instancePrototype,{ptrType:toType,ptr:dp,smartPtrType:this,smartPtr:ptr})}else{return makeClassHandle(toType.registeredClass.instancePrototype,{ptrType:toType,ptr:dp})}}var init_RegisteredPointer=()=>{Object.assign(RegisteredPointer.prototype,{getPointee(ptr){if(this.rawGetPointee){ptr=this.rawGetPointee(ptr)}return ptr},destructor(ptr){this.rawDestructor?.(ptr)},readValueFromPointer:readPointer,fromWireType:RegisteredPointer_fromWireType})};function RegisteredPointer(name,registeredClass,isReference,isConst,isSmartPointer,pointeeType,sharingPolicy,rawGetPointee,rawConstructor,rawShare,rawDestructor){this.name=name;this.registeredClass=registeredClass;this.isReference=isReference;this.isConst=isConst;this.isSmartPointer=isSmartPointer;this.pointeeType=pointeeType;this.sharingPolicy=sharingPolicy;this.rawGetPointee=rawGetPointee;this.rawConstructor=rawConstructor;this.rawShare=rawShare;this.rawDestructor=rawDestructor;if(!isSmartPointer&®isteredClass.baseClass===undefined){if(isConst){this.toWireType=constNoSmartPtrRawPointerToWireType;this.destructorFunction=null}else{this.toWireType=nonConstNoSmartPtrRawPointerToWireType;this.destructorFunction=null}}else{this.toWireType=genericPointerToWireType}}var replacePublicSymbol=(name,value,numArguments)=>{if(!Module.hasOwnProperty(name)){throwInternalError("Replacing nonexistent public symbol")}if(undefined!==Module[name].overloadTable&&undefined!==numArguments){Module[name].overloadTable[numArguments]=value}else{Module[name]=value;Module[name].argCount=numArguments}};var getDynCaller=(sig,ptr,promising=false)=>(...args)=>dynCall(sig,ptr,args,promising);var embind__requireFunction=(signature,rawFunction,isAsync=false)=>{signature=AsciiToString(signature);function makeDynCaller(){return getDynCaller(signature,rawFunction)}var fp=makeDynCaller();if(typeof fp!="function"){throwBindingError(`unknown function pointer with signature ${signature}: ${rawFunction}`)}return fp};class UnboundTypeError extends Error{}var getTypeName=type=>{var ptr=___getTypeName(type);var rv=AsciiToString(ptr);_free(ptr);return rv};var throwUnboundTypeError=(message,types)=>{var unboundTypes=[];var seen={};function visit(type){if(seen[type]){return}if(registeredTypes[type]){return}if(typeDependencies[type]){typeDependencies[type].forEach(visit);return}unboundTypes.push(type);seen[type]=true}types.forEach(visit);throw new UnboundTypeError(`${message}: `+unboundTypes.map(getTypeName).join([", "]))};function __embind_register_class(rawType,rawPointerType,rawConstPointerType,baseClassRawType,getActualTypeSignature,getActualType,upcastSignature,upcast,downcastSignature,downcast,name,destructorSignature,rawDestructor){rawType>>>=0;rawPointerType>>>=0;rawConstPointerType>>>=0;baseClassRawType>>>=0;getActualTypeSignature>>>=0;getActualType>>>=0;upcastSignature>>>=0;upcast>>>=0;downcastSignature>>>=0;downcast>>>=0;name>>>=0;destructorSignature>>>=0;rawDestructor>>>=0;name=AsciiToString(name);getActualType=embind__requireFunction(getActualTypeSignature,getActualType);upcast&&=embind__requireFunction(upcastSignature,upcast);downcast&&=embind__requireFunction(downcastSignature,downcast);rawDestructor=embind__requireFunction(destructorSignature,rawDestructor);var legalFunctionName=makeLegalFunctionName(name);exposePublicSymbol(legalFunctionName,function(){throwUnboundTypeError(`Cannot construct ${name} due to unbound types`,[baseClassRawType])});whenDependentTypesAreResolved([rawType,rawPointerType,rawConstPointerType],baseClassRawType?[baseClassRawType]:[],base=>{base=base[0];var baseClass;var basePrototype;if(baseClassRawType){baseClass=base.registeredClass;basePrototype=baseClass.instancePrototype}else{basePrototype=ClassHandle.prototype}var constructor=createNamedFunction(name,function(...args){if(Object.getPrototypeOf(this)!==instancePrototype){throw new BindingError(`Use 'new' to construct ${name}`)}if(undefined===registeredClass.constructor_body){throw new BindingError(`${name} has no accessible constructor`)}var body=registeredClass.constructor_body[args.length];if(undefined===body){throw new BindingError(`Tried to invoke ctor of ${name} with invalid number of parameters (${args.length}) - expected (${Object.keys(registeredClass.constructor_body).toString()}) parameters instead!`)}return body.apply(this,args)});var instancePrototype=Object.create(basePrototype,{constructor:{value:constructor}});constructor.prototype=instancePrototype;var registeredClass=new RegisteredClass(name,constructor,instancePrototype,rawDestructor,baseClass,getActualType,upcast,downcast);if(registeredClass.baseClass){registeredClass.baseClass.__derivedClasses??=[];registeredClass.baseClass.__derivedClasses.push(registeredClass)}var referenceConverter=new RegisteredPointer(name,registeredClass,true,false,false);var pointerConverter=new RegisteredPointer(name+"*",registeredClass,false,false,false);var constPointerConverter=new RegisteredPointer(name+" const*",registeredClass,false,true,false);registeredPointers[rawType]={pointerType:pointerConverter,constPointerType:constPointerConverter};replacePublicSymbol(legalFunctionName,constructor);return[referenceConverter,pointerConverter,constPointerConverter]})}function usesDestructorStack(argTypes){for(var i=1;i{try{return func()}catch(e){abort(e)}};var handleException=e=>{if(e instanceof ExitStatus||e=="unwind"){return EXITSTATUS}quit_(1,e)};var runtimeKeepaliveCounter=0;var keepRuntimeAlive=()=>noExitRuntime||runtimeKeepaliveCounter>0;var _proc_exit=code=>{EXITSTATUS=code;if(!keepRuntimeAlive()){Module["onExit"]?.(code);ABORT=true}quit_(code,new ExitStatus(code))};var exitJS=(status,implicit)=>{EXITSTATUS=status;_proc_exit(status)};var _exit=exitJS;var maybeExit=()=>{if(!keepRuntimeAlive()){try{_exit(EXITSTATUS)}catch(e){handleException(e)}}};var callUserCallback=func=>{if(ABORT){return}try{func();maybeExit()}catch(e){handleException(e)}};var Asyncify={instrumentWasmImports(imports){var importPattern=/^(invoke_.*|__asyncjs__.*)$/;for(let[x,original]of Object.entries(imports)){if(typeof original=="function"){let isAsyncifyImport=original.isAsync||importPattern.test(x)}}},instrumentFunction(original){var wrapper=(...args)=>{Asyncify.exportCallStack.push(original);try{return original(...args)}finally{if(!ABORT){var top=Asyncify.exportCallStack.pop();Asyncify.maybeStopUnwind()}}};Asyncify.funcWrappers.set(original,wrapper);return wrapper},instrumentWasmExports(exports){var ret={};for(let[x,original]of Object.entries(exports)){if(typeof original=="function"){var wrapper=Asyncify.instrumentFunction(original);ret[x]=wrapper}else{ret[x]=original}}return ret},State:{Normal:0,Unwinding:1,Rewinding:2,Disabled:3},state:0,StackSize:65536,currData:null,handleSleepReturnValue:0,exportCallStack:[],callstackFuncToId:new Map,callStackIdToFunc:new Map,funcWrappers:new Map,callStackId:0,asyncPromiseHandlers:null,sleepCallbacks:[],getCallStackId(func){if(!Asyncify.callstackFuncToId.has(func)){var id=Asyncify.callStackId++;Asyncify.callstackFuncToId.set(func,id);Asyncify.callStackIdToFunc.set(id,func)}return Asyncify.callstackFuncToId.get(func)},maybeStopUnwind(){if(Asyncify.currData&&Asyncify.state===Asyncify.State.Unwinding&&Asyncify.exportCallStack.length===0){Asyncify.state=Asyncify.State.Normal;runAndAbortIfError(_asyncify_stop_unwind);if(typeof Fibers!="undefined"){Fibers.trampoline()}}},whenDone(){return new Promise((resolve,reject)=>{Asyncify.asyncPromiseHandlers={resolve,reject}})},allocateData(){var ptr=_malloc(12+Asyncify.StackSize);Asyncify.setDataHeader(ptr,ptr+12,Asyncify.StackSize);Asyncify.setDataRewindFunc(ptr);return ptr},setDataHeader(ptr,stack,stackSize){HEAPU32[ptr>>>2>>>0]=stack;HEAPU32[ptr+4>>>2>>>0]=stack+stackSize},setDataRewindFunc(ptr){var bottomOfCallStack=Asyncify.exportCallStack[0];var rewindId=Asyncify.getCallStackId(bottomOfCallStack);HEAP32[ptr+8>>>2>>>0]=rewindId},getDataRewindFunc(ptr){var id=HEAP32[ptr+8>>>2>>>0];var func=Asyncify.callStackIdToFunc.get(id);return func},doRewind(ptr){var original=Asyncify.getDataRewindFunc(ptr);var func=Asyncify.funcWrappers.get(original);return func()},handleSleep(startAsync){if(ABORT)return;if(Asyncify.state===Asyncify.State.Normal){var reachedCallback=false;var reachedAfterCallback=false;startAsync((handleSleepReturnValue=0)=>{if(ABORT)return;Asyncify.handleSleepReturnValue=handleSleepReturnValue;reachedCallback=true;if(!reachedAfterCallback){return}Asyncify.state=Asyncify.State.Rewinding;runAndAbortIfError(()=>_asyncify_start_rewind(Asyncify.currData));if(typeof MainLoop!="undefined"&&MainLoop.func){MainLoop.resume()}var asyncWasmReturnValue,isError=false;try{asyncWasmReturnValue=Asyncify.doRewind(Asyncify.currData)}catch(err){asyncWasmReturnValue=err;isError=true}var handled=false;if(!Asyncify.currData){var asyncPromiseHandlers=Asyncify.asyncPromiseHandlers;if(asyncPromiseHandlers){Asyncify.asyncPromiseHandlers=null;(isError?asyncPromiseHandlers.reject:asyncPromiseHandlers.resolve)(asyncWasmReturnValue);handled=true}}if(isError&&!handled){throw asyncWasmReturnValue}});reachedAfterCallback=true;if(!reachedCallback){Asyncify.state=Asyncify.State.Unwinding;Asyncify.currData=Asyncify.allocateData();if(typeof MainLoop!="undefined"&&MainLoop.func){MainLoop.pause()}runAndAbortIfError(()=>_asyncify_start_unwind(Asyncify.currData))}}else if(Asyncify.state===Asyncify.State.Rewinding){Asyncify.state=Asyncify.State.Normal;runAndAbortIfError(_asyncify_stop_rewind);_free(Asyncify.currData);Asyncify.currData=null;Asyncify.sleepCallbacks.forEach(callUserCallback)}else{abort(`invalid state: ${Asyncify.state}`)}return Asyncify.handleSleepReturnValue},handleAsync:startAsync=>Asyncify.handleSleep(wakeUp=>{startAsync().then(wakeUp)})};function craftInvokerFunction(humanName,argTypes,classType,cppInvokerFunc,cppTargetFunc,isAsync){var argCount=argTypes.length;if(argCount<2){throwBindingError("argTypes array size mismatch! Must at least get return value and 'this' types!")}var isClassMethodFunc=argTypes[1]!==null&&classType!==null;var needsDestructorStack=usesDestructorStack(argTypes);var returns=!argTypes[0].isVoid;var retType=argTypes[0];var instType=argTypes[1];var closureArgs=[humanName,throwBindingError,cppInvokerFunc,cppTargetFunc,runDestructors,retType.fromWireType.bind(retType),instType?.toWireType.bind(instType)];for(var i=2;i{var array=[];for(var i=0;i>>2>>>0])}return array};var getFunctionName=signature=>{signature=signature.trim();const argsIndex=signature.indexOf("(");if(argsIndex===-1)return signature;return signature.slice(0,argsIndex)};var __embind_register_class_class_function=function(rawClassType,methodName,argCount,rawArgTypesAddr,invokerSignature,rawInvoker,fn,isAsync,isNonnullReturn){rawClassType>>>=0;methodName>>>=0;rawArgTypesAddr>>>=0;invokerSignature>>>=0;rawInvoker>>>=0;fn>>>=0;var rawArgTypes=heap32VectorToArray(argCount,rawArgTypesAddr);methodName=AsciiToString(methodName);methodName=getFunctionName(methodName);rawInvoker=embind__requireFunction(invokerSignature,rawInvoker,isAsync);whenDependentTypesAreResolved([],[rawClassType],classType=>{classType=classType[0];var humanName=`${classType.name}.${methodName}`;function unboundTypesHandler(){throwUnboundTypeError(`Cannot call ${humanName} due to unbound types`,rawArgTypes)}if(methodName.startsWith("@@")){methodName=Symbol[methodName.substring(2)]}var proto=classType.registeredClass.constructor;if(undefined===proto[methodName]){unboundTypesHandler.argCount=argCount-1;proto[methodName]=unboundTypesHandler}else{ensureOverloadTable(proto,methodName,humanName);proto[methodName].overloadTable[argCount-1]=unboundTypesHandler}whenDependentTypesAreResolved([],rawArgTypes,argTypes=>{var invokerArgsArray=[argTypes[0],null].concat(argTypes.slice(1));var func=craftInvokerFunction(humanName,invokerArgsArray,null,rawInvoker,fn,isAsync);if(undefined===proto[methodName].overloadTable){func.argCount=argCount-1;proto[methodName]=func}else{proto[methodName].overloadTable[argCount-1]=func}if(classType.registeredClass.__derivedClasses){for(const derivedClass of classType.registeredClass.__derivedClasses){if(!derivedClass.constructor.hasOwnProperty(methodName)){derivedClass.constructor[methodName]=func}}}return[]});return[]})};var __embind_register_class_function=function(rawClassType,methodName,argCount,rawArgTypesAddr,invokerSignature,rawInvoker,context,isPureVirtual,isAsync,isNonnullReturn){rawClassType>>>=0;methodName>>>=0;rawArgTypesAddr>>>=0;invokerSignature>>>=0;rawInvoker>>>=0;context>>>=0;var rawArgTypes=heap32VectorToArray(argCount,rawArgTypesAddr);methodName=AsciiToString(methodName);methodName=getFunctionName(methodName);rawInvoker=embind__requireFunction(invokerSignature,rawInvoker,isAsync);whenDependentTypesAreResolved([],[rawClassType],classType=>{classType=classType[0];var humanName=`${classType.name}.${methodName}`;if(methodName.startsWith("@@")){methodName=Symbol[methodName.substring(2)]}if(isPureVirtual){classType.registeredClass.pureVirtualFunctions.push(methodName)}function unboundTypesHandler(){throwUnboundTypeError(`Cannot call ${humanName} due to unbound types`,rawArgTypes)}var proto=classType.registeredClass.instancePrototype;var method=proto[methodName];if(undefined===method||undefined===method.overloadTable&&method.className!==classType.name&&method.argCount===argCount-2){unboundTypesHandler.argCount=argCount-2;unboundTypesHandler.className=classType.name;proto[methodName]=unboundTypesHandler}else{ensureOverloadTable(proto,methodName,humanName);proto[methodName].overloadTable[argCount-2]=unboundTypesHandler}whenDependentTypesAreResolved([],rawArgTypes,argTypes=>{var memberFunction=craftInvokerFunction(humanName,argTypes,classType,rawInvoker,context,isAsync);if(undefined===proto[methodName].overloadTable){memberFunction.argCount=argCount-2;proto[methodName]=memberFunction}else{proto[methodName].overloadTable[argCount-2]=memberFunction}return[]});return[]})};var validateThis=(this_,classType,humanName)=>{if(!(this_ instanceof Object)){throwBindingError(`${humanName} with invalid "this": ${this_}`)}if(!(this_ instanceof classType.registeredClass.constructor)){throwBindingError(`${humanName} incompatible with "this" of type ${this_.constructor.name}`)}if(!this_.$$.ptr){throwBindingError(`cannot call emscripten binding method ${humanName} on deleted object`)}return upcastPointer(this_.$$.ptr,this_.$$.ptrType.registeredClass,classType.registeredClass)};var __embind_register_class_property=function(classType,fieldName,getterReturnType,getterSignature,getter,getterContext,setterArgumentType,setterSignature,setter,setterContext){classType>>>=0;fieldName>>>=0;getterReturnType>>>=0;getterSignature>>>=0;getter>>>=0;getterContext>>>=0;setterArgumentType>>>=0;setterSignature>>>=0;setter>>>=0;setterContext>>>=0;fieldName=AsciiToString(fieldName);getter=embind__requireFunction(getterSignature,getter);whenDependentTypesAreResolved([],[classType],classType=>{classType=classType[0];var humanName=`${classType.name}.${fieldName}`;var desc={get(){throwUnboundTypeError(`Cannot access ${humanName} due to unbound types`,[getterReturnType,setterArgumentType])},enumerable:true,configurable:true};if(setter){desc.set=()=>throwUnboundTypeError(`Cannot access ${humanName} due to unbound types`,[getterReturnType,setterArgumentType])}else{desc.set=v=>throwBindingError(humanName+" is a read-only property")}Object.defineProperty(classType.registeredClass.instancePrototype,fieldName,desc);whenDependentTypesAreResolved([],setter?[getterReturnType,setterArgumentType]:[getterReturnType],types=>{var getterReturnType=types[0];var desc={get(){var ptr=validateThis(this,classType,humanName+" getter");return getterReturnType.fromWireType(getter(getterContext,ptr))},enumerable:true};if(setter){setter=embind__requireFunction(setterSignature,setter);var setterArgumentType=types[1];desc.set=function(v){var ptr=validateThis(this,classType,humanName+" setter");var destructors=[];setter(setterContext,ptr,setterArgumentType.toWireType(destructors,v));runDestructors(destructors)}}Object.defineProperty(classType.registeredClass.instancePrototype,fieldName,desc);return[]});return[]})};var emval_freelist=[];var emval_handles=[0,1,,1,null,1,true,1,false,1];function __emval_decref(handle){handle>>>=0;if(handle>9&&0===--emval_handles[handle+1]){emval_handles[handle]=undefined;emval_freelist.push(handle)}}var Emval={toValue:handle=>{if(!handle){throwBindingError(`Cannot use deleted val. handle = ${handle}`)}return emval_handles[handle]},toHandle:value=>{switch(value){case undefined:return 2;case null:return 4;case true:return 6;case false:return 8;default:{const handle=emval_freelist.pop()||emval_handles.length;emval_handles[handle]=value;emval_handles[handle+1]=1;return handle}}}};var EmValType={name:"emscripten::val",fromWireType:handle=>{var rv=Emval.toValue(handle);__emval_decref(handle);return rv},toWireType:(destructors,value)=>Emval.toHandle(value),readValueFromPointer:readPointer,destructorFunction:null};function __embind_register_emval(rawType){rawType>>>=0;return registerType(rawType,EmValType)}var enumReadValueFromPointer=(name,width,signed)=>{switch(width){case 1:return signed?function(pointer){return this.fromWireType(HEAP8[pointer>>>0])}:function(pointer){return this.fromWireType(HEAPU8[pointer>>>0])};case 2:return signed?function(pointer){return this.fromWireType(HEAP16[pointer>>>1>>>0])}:function(pointer){return this.fromWireType(HEAPU16[pointer>>>1>>>0])};case 4:return signed?function(pointer){return this.fromWireType(HEAP32[pointer>>>2>>>0])}:function(pointer){return this.fromWireType(HEAPU32[pointer>>>2>>>0])};default:throw new TypeError(`invalid integer width (${width}): ${name}`)}};function __embind_register_enum(rawType,name,size,isSigned){rawType>>>=0;name>>>=0;size>>>=0;name=AsciiToString(name);function ctor(){}ctor.values={};registerType(rawType,{name,constructor:ctor,fromWireType:function(c){return this.constructor.values[c]},toWireType:(destructors,c)=>c.value,readValueFromPointer:enumReadValueFromPointer(name,size,isSigned),destructorFunction:null});exposePublicSymbol(name,ctor)}var requireRegisteredType=(rawType,humanName)=>{var impl=registeredTypes[rawType];if(undefined===impl){throwBindingError(`${humanName} has unknown type ${getTypeName(rawType)}`)}return impl};function __embind_register_enum_value(rawEnumType,name,enumValue){rawEnumType>>>=0;name>>>=0;var enumType=requireRegisteredType(rawEnumType,"enum");name=AsciiToString(name);var Enum=enumType.constructor;var Value=Object.create(enumType.constructor.prototype,{value:{value:enumValue},constructor:{value:createNamedFunction(`${enumType.name}_${name}`,function(){})}});Enum.values[enumValue]=Value;Enum[name]=Value}var floatReadValueFromPointer=(name,width)=>{switch(width){case 4:return function(pointer){return this.fromWireType(HEAPF32[pointer>>>2>>>0])};case 8:return function(pointer){return this.fromWireType(HEAPF64[pointer>>>3>>>0])};default:throw new TypeError(`invalid float width (${width}): ${name}`)}};var __embind_register_float=function(rawType,name,size){rawType>>>=0;name>>>=0;size>>>=0;name=AsciiToString(name);registerType(rawType,{name,fromWireType:value=>value,toWireType:(destructors,value)=>value,readValueFromPointer:floatReadValueFromPointer(name,size),destructorFunction:null})};function __embind_register_function(name,argCount,rawArgTypesAddr,signature,rawInvoker,fn,isAsync,isNonnullReturn){name>>>=0;rawArgTypesAddr>>>=0;signature>>>=0;rawInvoker>>>=0;fn>>>=0;var argTypes=heap32VectorToArray(argCount,rawArgTypesAddr);name=AsciiToString(name);name=getFunctionName(name);rawInvoker=embind__requireFunction(signature,rawInvoker,isAsync);exposePublicSymbol(name,function(){throwUnboundTypeError(`Cannot call ${name} due to unbound types`,argTypes)},argCount-1);whenDependentTypesAreResolved([],argTypes,argTypes=>{var invokerArgsArray=[argTypes[0],null].concat(argTypes.slice(1));replacePublicSymbol(name,craftInvokerFunction(name,invokerArgsArray,null,rawInvoker,fn,isAsync),argCount-1);return[]})}var __embind_register_integer=function(primitiveType,name,size,minRange,maxRange){primitiveType>>>=0;name>>>=0;size>>>=0;name=AsciiToString(name);const isUnsignedType=minRange===0;let fromWireType=value=>value;if(isUnsignedType){var bitshift=32-8*size;fromWireType=value=>value<>>bitshift;maxRange=fromWireType(maxRange)}registerType(primitiveType,{name,fromWireType,toWireType:(destructors,value)=>value,readValueFromPointer:integerReadValueFromPointer(name,size,minRange!==0),destructorFunction:null})};function __embind_register_memory_view(rawType,dataTypeIndex,name){rawType>>>=0;name>>>=0;var typeMapping=[Int8Array,Uint8Array,Int16Array,Uint16Array,Int32Array,Uint32Array,Float32Array,Float64Array,BigInt64Array,BigUint64Array];var TA=typeMapping[dataTypeIndex];function decodeMemoryView(handle){var size=HEAPU32[handle>>>2>>>0];var data=HEAPU32[handle+4>>>2>>>0];return new TA(HEAP8.buffer,data,size)}name=AsciiToString(name);registerType(rawType,{name,fromWireType:decodeMemoryView,readValueFromPointer:decodeMemoryView},{ignoreDuplicateRegistrations:true})}var stringToUTF8=(str,outPtr,maxBytesToWrite)=>stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite);function __embind_register_std_string(rawType,name){rawType>>>=0;name>>>=0;name=AsciiToString(name);var stdStringIsUTF8=true;registerType(rawType,{name,fromWireType(value){var length=HEAPU32[value>>>2>>>0];var payload=value+4;var str;if(stdStringIsUTF8){str=UTF8ToString(payload,length,true)}else{str="";for(var i=0;i>>0])}}_free(value);return str},toWireType(destructors,value){if(value instanceof ArrayBuffer){value=new Uint8Array(value)}var length;var valueIsOfTypeString=typeof value=="string";if(!(valueIsOfTypeString||ArrayBuffer.isView(value)&&value.BYTES_PER_ELEMENT==1)){throwBindingError("Cannot pass non-string to std::string")}if(stdStringIsUTF8&&valueIsOfTypeString){length=lengthBytesUTF8(value)}else{length=value.length}var base=_malloc(4+length+1);var ptr=base+4;HEAPU32[base>>>2>>>0]=length;if(valueIsOfTypeString){if(stdStringIsUTF8){stringToUTF8(value,ptr,length+1)}else{for(var i=0;i255){_free(base);throwBindingError("String has UTF-16 code units that do not fit in 8 bits")}HEAPU8[ptr+i>>>0]=charCode}}}else{HEAPU8.set(value,ptr>>>0)}if(destructors!==null){destructors.push(_free,base)}return base},readValueFromPointer:readPointer,destructorFunction(ptr){_free(ptr)}})}var UTF16Decoder=globalThis.TextDecoder?new TextDecoder("utf-16le"):undefined;var UTF16ToString=(ptr,maxBytesToRead,ignoreNul)=>{var idx=ptr>>>1;var endIdx=findStringEnd(HEAPU16,idx,maxBytesToRead/2,ignoreNul);if(endIdx-idx>16&&UTF16Decoder)return UTF16Decoder.decode(HEAPU16.subarray(idx>>>0,endIdx>>>0));var str="";for(var i=idx;i>>0];str+=String.fromCharCode(codeUnit)}return str};var stringToUTF16=(str,outPtr,maxBytesToWrite)=>{maxBytesToWrite??=2147483647;if(maxBytesToWrite<2)return 0;maxBytesToWrite-=2;var startPtr=outPtr;var numCharsToWrite=maxBytesToWrite>>1>>>0]=codeUnit;outPtr+=2}HEAP16[outPtr>>>1>>>0]=0;return outPtr-startPtr};var lengthBytesUTF16=str=>str.length*2;var UTF32ToString=(ptr,maxBytesToRead,ignoreNul)=>{var str="";var startIdx=ptr>>>2;for(var i=0;!(i>=maxBytesToRead/4);i++){var utf32=HEAPU32[startIdx+i>>>0];if(!utf32&&!ignoreNul)break;str+=String.fromCodePoint(utf32)}return str};var stringToUTF32=(str,outPtr,maxBytesToWrite)=>{outPtr>>>=0;maxBytesToWrite??=2147483647;if(maxBytesToWrite<4)return 0;var startPtr=outPtr;var endPtr=startPtr+maxBytesToWrite-4;for(var i=0;i65535){i++}HEAP32[outPtr>>>2>>>0]=codePoint;outPtr+=4;if(outPtr+4>endPtr)break}HEAP32[outPtr>>>2>>>0]=0;return outPtr-startPtr};var lengthBytesUTF32=str=>{var len=0;for(var i=0;i65535){i++}len+=4}return len};function __embind_register_std_wstring(rawType,charSize,name){rawType>>>=0;charSize>>>=0;name>>>=0;name=AsciiToString(name);var decodeString,encodeString,lengthBytesUTF;if(charSize===2){decodeString=UTF16ToString;encodeString=stringToUTF16;lengthBytesUTF=lengthBytesUTF16}else{decodeString=UTF32ToString;encodeString=stringToUTF32;lengthBytesUTF=lengthBytesUTF32}registerType(rawType,{name,fromWireType:value=>{var length=HEAPU32[value>>>2>>>0];var str=decodeString(value+4,length*charSize,true);_free(value);return str},toWireType:(destructors,value)=>{if(!(typeof value=="string")){throwBindingError(`Cannot pass non-string to C++ string type ${name}`)}var length=lengthBytesUTF(value);var ptr=_malloc(4+length+charSize);HEAPU32[ptr>>>2>>>0]=length/charSize;encodeString(value,ptr+4,length+charSize);if(destructors!==null){destructors.push(_free,ptr)}return ptr},readValueFromPointer:readPointer,destructorFunction(ptr){_free(ptr)}})}function __embind_register_value_object(rawType,name,constructorSignature,rawConstructor,destructorSignature,rawDestructor){rawType>>>=0;name>>>=0;constructorSignature>>>=0;rawConstructor>>>=0;destructorSignature>>>=0;rawDestructor>>>=0;structRegistrations[rawType]={name:AsciiToString(name),rawConstructor:embind__requireFunction(constructorSignature,rawConstructor),rawDestructor:embind__requireFunction(destructorSignature,rawDestructor),fields:[]}}function __embind_register_value_object_field(structType,fieldName,getterReturnType,getterSignature,getter,getterContext,setterArgumentType,setterSignature,setter,setterContext){structType>>>=0;fieldName>>>=0;getterReturnType>>>=0;getterSignature>>>=0;getter>>>=0;getterContext>>>=0;setterArgumentType>>>=0;setterSignature>>>=0;setter>>>=0;setterContext>>>=0;structRegistrations[structType].fields.push({fieldName:AsciiToString(fieldName),getterReturnType,getter:embind__requireFunction(getterSignature,getter),getterContext,setterArgumentType,setter:embind__requireFunction(setterSignature,setter),setterContext})}var __embind_register_void=function(rawType,name){rawType>>>=0;name>>>=0;name=AsciiToString(name);registerType(rawType,{isVoid:true,name,fromWireType:()=>undefined,toWireType:(destructors,o)=>undefined})};function __emval_array_to_memory_view(dst,src){dst>>>=0;src>>>=0;dst=Emval.toValue(dst);src=Emval.toValue(src);dst.set(src)}var emval_methodCallers=[];var emval_addMethodCaller=caller=>{var id=emval_methodCallers.length;emval_methodCallers.push(caller);return id};var emval_lookupTypes=(argCount,argTypes)=>{var a=new Array(argCount);for(var i=0;i>>2>>>0],`parameter ${i}`)}return a};var emval_returnValue=(toReturnWire,destructorsRef,handle)=>{var destructors=[];var result=toReturnWire(destructors,handle);if(destructors.length){HEAPU32[destructorsRef>>>2>>>0]=Emval.toHandle(destructors)}return result};var emval_symbols={};var getStringOrSymbol=address=>{var symbol=emval_symbols[address];if(symbol===undefined){return AsciiToString(address)}return symbol};var __emval_create_invoker=function(argCount,argTypesPtr,kind){argTypesPtr>>>=0;var GenericWireTypeSize=8;var[retType,...argTypes]=emval_lookupTypes(argCount,argTypesPtr);var toReturnWire=retType.toWireType.bind(retType);var argFromPtr=argTypes.map(type=>type.readValueFromPointer.bind(type));argCount--;var captures={toValue:Emval.toValue};var args=argFromPtr.map((argFromPtr,i)=>{var captureName=`argFromPtr${i}`;captures[captureName]=argFromPtr;return`${captureName}(args${i?"+"+i*GenericWireTypeSize:""})`});var functionBody;switch(kind){case 0:functionBody="toValue(handle)";break;case 2:functionBody="new (toValue(handle))";break;case 3:functionBody="";break;case 1:captures["getStringOrSymbol"]=getStringOrSymbol;functionBody="toValue(handle)[getStringOrSymbol(methodName)]";break}functionBody+=`(${args})`;if(!retType.isVoid){captures["toReturnWire"]=toReturnWire;captures["emval_returnValue"]=emval_returnValue;functionBody=`return emval_returnValue(toReturnWire, destructorsRef, ${functionBody})`}functionBody=`return function (handle, methodName, destructorsRef, args) {\n ${functionBody}\n }`;var invokerFunction=new Function(Object.keys(captures),functionBody)(...Object.values(captures));var functionName=`methodCaller<(${argTypes.map(t=>t.name)}) => ${retType.name}>`;return emval_addMethodCaller(createNamedFunction(functionName,invokerFunction))};function __emval_get_global(name){name>>>=0;if(!name){return Emval.toHandle(globalThis)}name=getStringOrSymbol(name);return Emval.toHandle(globalThis[name])}function __emval_get_module_property(name){name>>>=0;name=getStringOrSymbol(name);return Emval.toHandle(Module[name])}function __emval_get_property(handle,key){handle>>>=0;key>>>=0;handle=Emval.toValue(handle);key=Emval.toValue(key);return Emval.toHandle(handle[key])}function __emval_incref(handle){handle>>>=0;if(handle>9){emval_handles[handle+1]+=1}}function __emval_instanceof(object,constructor){object>>>=0;constructor>>>=0;object=Emval.toValue(object);constructor=Emval.toValue(constructor);return object instanceof constructor}function __emval_invoke(caller,handle,methodName,destructorsRef,args){caller>>>=0;handle>>>=0;methodName>>>=0;destructorsRef>>>=0;args>>>=0;return emval_methodCallers[caller](handle,methodName,destructorsRef,args)}var __emval_invoke_i64=__emval_invoke;function __emval_is_number(handle){handle>>>=0;handle=Emval.toValue(handle);return typeof handle=="number"}function __emval_is_string(handle){handle>>>=0;handle=Emval.toValue(handle);return typeof handle=="string"}function __emval_new_array(){return Emval.toHandle([])}function __emval_new_cstring(v){v>>>=0;return Emval.toHandle(getStringOrSymbol(v))}function __emval_new_u8string(v){v>>>=0;return Emval.toHandle(UTF8ToString(v))}function __emval_run_destructors(handle){handle>>>=0;var destructors=Emval.toValue(handle);runDestructors(destructors);__emval_decref(handle)}function __emval_set_property(handle,key,value){handle>>>=0;key>>>=0;value>>>=0;handle=Emval.toValue(handle);key=Emval.toValue(key);value=Emval.toValue(value);handle[key]=value}function __emval_typeof(handle){handle>>>=0;handle=Emval.toValue(handle);return Emval.toHandle(typeof handle)}function __mmap_js(len,prot,flags,fd,offset,allocated,addr){len>>>=0;offset=bigintToI53Checked(offset);allocated>>>=0;addr>>>=0;try{var stream=SYSCALLS.getStreamFromFD(fd);var res=FS.mmap(stream,len,offset,prot,flags);var ptr=res.ptr;HEAP32[allocated>>>2>>>0]=res.allocated;HEAPU32[addr>>>2>>>0]=ptr;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function __munmap_js(addr,len,prot,flags,fd,offset){addr>>>=0;len>>>=0;offset=bigintToI53Checked(offset);try{var stream=SYSCALLS.getStreamFromFD(fd);if(prot&2){SYSCALLS.doMsync(addr,stream,len,flags,offset)}}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}var _emscripten_get_now=()=>performance.now();var _emscripten_date_now=()=>Date.now();var nowIsMonotonic=1;var checkWasiClock=clock_id=>clock_id>=0&&clock_id<=3;function _clock_time_get(clk_id,ignored_precision,ptime){ignored_precision=bigintToI53Checked(ignored_precision);ptime>>>=0;if(!checkWasiClock(clk_id)){return 28}var now;if(clk_id===0){now=_emscripten_date_now()}else if(nowIsMonotonic){now=_emscripten_get_now()}else{return 52}var nsec=Math.round(now*1e3*1e3);HEAP64[ptime>>>3>>>0]=BigInt(nsec);return 0}var readEmAsmArgsArray=[];var readEmAsmArgs=(sigPtr,buf)=>{readEmAsmArgsArray.length=0;var ch;while(ch=HEAPU8[sigPtr++>>>0]){var wide=ch!=105;wide&=ch!=112;buf+=wide&&buf%8?4:0;readEmAsmArgsArray.push(ch==112?HEAPU32[buf>>>2>>>0]:ch==106?HEAP64[buf>>>3>>>0]:ch==105?HEAP32[buf>>>2>>>0]:HEAPF64[buf>>>3>>>0]);buf+=wide?8:4}return readEmAsmArgsArray};var runEmAsmFunction=(code,sigPtr,argbuf)=>{var args=readEmAsmArgs(sigPtr,argbuf);return ASM_CONSTS[code](...args)};function _emscripten_asm_const_int(code,sigPtr,argbuf){code>>>=0;sigPtr>>>=0;argbuf>>>=0;return runEmAsmFunction(code,sigPtr,argbuf)}var getHeapMax=()=>4294901760;function _emscripten_get_heap_max(){return getHeapMax()}var _emscripten_has_asyncify=()=>1;var growMemory=size=>{var oldHeapSize=wasmMemory.buffer.byteLength;var pages=(size-oldHeapSize+65535)/65536|0;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){}};function _emscripten_resize_heap(requestedSize){requestedSize>>>=0;var oldSize=HEAPU8.length;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 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 stringToNewUTF8=str=>{var size=lengthBytesUTF8(str)+1;var ret=_malloc(size);if(ret)stringToUTF8(str,ret,size);return ret};var WebGPU={Internals:{jsObjects:[],jsObjectInsert:(ptr,jsObject)=>{ptr>>>=0;WebGPU.Internals.jsObjects[ptr]=jsObject},bufferOnUnmaps:[],futures:[],futureInsert:(futureId,promise)=>{WebGPU.Internals.futures[futureId]=new Promise(resolve=>promise.finally(()=>resolve(futureId)))}},getJsObject:ptr=>{if(!ptr)return undefined;ptr>>>=0;return WebGPU.Internals.jsObjects[ptr]},importJsAdapter:(obj,parentPtr=0)=>{var ptr=_emwgpuCreateAdapter(parentPtr);WebGPU.Internals.jsObjects[ptr]=obj;return ptr},importJsBindGroup:(obj,parentPtr=0)=>{var ptr=_emwgpuCreateBindGroup(parentPtr);WebGPU.Internals.jsObjects[ptr]=obj;return ptr},importJsBindGroupLayout:(obj,parentPtr=0)=>{var ptr=_emwgpuCreateBindGroupLayout(parentPtr);WebGPU.Internals.jsObjects[ptr]=obj;return ptr},importJsBuffer:(buffer,parentPtr=0)=>{assert(buffer.mapState!="pending");var mapState=buffer.mapState=="mapped"?3:1;var bufferPtr=_emwgpuCreateBuffer(parentPtr,mapState);WebGPU.Internals.jsObjectInsert(bufferPtr,buffer);if(buffer.mapState=="mapped"){WebGPU.Internals.bufferOnUnmaps[bufferPtr]=[]}return bufferPtr},importJsCommandBuffer:(obj,parentPtr=0)=>{var ptr=_emwgpuCreateCommandBuffer(parentPtr);WebGPU.Internals.jsObjects[ptr]=obj;return ptr},importJsCommandEncoder:(obj,parentPtr=0)=>{var ptr=_emwgpuCreateCommandEncoder(parentPtr);WebGPU.Internals.jsObjects[ptr]=obj;return ptr},importJsComputePassEncoder:(obj,parentPtr=0)=>{var ptr=_emwgpuCreateComputePassEncoder(parentPtr);WebGPU.Internals.jsObjects[ptr]=obj;return ptr},importJsComputePipeline:(obj,parentPtr=0)=>{var ptr=_emwgpuCreateComputePipeline(parentPtr);WebGPU.Internals.jsObjects[ptr]=obj;return ptr},importJsDevice:(device,parentPtr=0)=>{var queuePtr=_emwgpuCreateQueue(parentPtr);var devicePtr=_emwgpuCreateDevice(parentPtr,queuePtr);WebGPU.Internals.jsObjectInsert(queuePtr,device.queue);WebGPU.Internals.jsObjectInsert(devicePtr,device);return devicePtr},importJsPipelineLayout:(obj,parentPtr=0)=>{var ptr=_emwgpuCreatePipelineLayout(parentPtr);WebGPU.Internals.jsObjects[ptr]=obj;return ptr},importJsQuerySet:(obj,parentPtr=0)=>{var ptr=_emwgpuCreateQuerySet(parentPtr);WebGPU.Internals.jsObjects[ptr]=obj;return ptr},importJsQueue:(obj,parentPtr=0)=>{var ptr=_emwgpuCreateQueue(parentPtr);WebGPU.Internals.jsObjects[ptr]=obj;return ptr},importJsRenderBundle:(obj,parentPtr=0)=>{var ptr=_emwgpuCreateRenderBundle(parentPtr);WebGPU.Internals.jsObjects[ptr]=obj;return ptr},importJsRenderBundleEncoder:(obj,parentPtr=0)=>{var ptr=_emwgpuCreateRenderBundleEncoder(parentPtr);WebGPU.Internals.jsObjects[ptr]=obj;return ptr},importJsRenderPassEncoder:(obj,parentPtr=0)=>{var ptr=_emwgpuCreateRenderPassEncoder(parentPtr);WebGPU.Internals.jsObjects[ptr]=obj;return ptr},importJsRenderPipeline:(obj,parentPtr=0)=>{var ptr=_emwgpuCreateRenderPipeline(parentPtr);WebGPU.Internals.jsObjects[ptr]=obj;return ptr},importJsSampler:(obj,parentPtr=0)=>{var ptr=_emwgpuCreateSampler(parentPtr);WebGPU.Internals.jsObjects[ptr]=obj;return ptr},importJsShaderModule:(obj,parentPtr=0)=>{var ptr=_emwgpuCreateShaderModule(parentPtr);WebGPU.Internals.jsObjects[ptr]=obj;return ptr},importJsSurface:(obj,parentPtr=0)=>{var ptr=_emwgpuCreateSurface(parentPtr);WebGPU.Internals.jsObjects[ptr]=obj;return ptr},importJsTexture:(obj,parentPtr=0)=>{var ptr=_emwgpuCreateTexture(parentPtr);WebGPU.Internals.jsObjects[ptr]=obj;return ptr},importJsTextureView:(obj,parentPtr=0)=>{var ptr=_emwgpuCreateTextureView(parentPtr);WebGPU.Internals.jsObjects[ptr]=obj;return ptr},errorCallback:(callback,type,message,userdata)=>{var sp=stackSave();var messagePtr=stringToUTF8OnStack(message);((a1,a2,a3)=>dynCall_viii(callback,a1,a2,a3))(type,messagePtr,userdata);stackRestore(sp)},setStringView:(ptr,data,length)=>{HEAPU32[ptr>>>2>>>0]=data;HEAPU32[ptr+4>>>2>>>0]=length},makeStringFromStringView:stringViewPtr=>{var ptr=HEAPU32[stringViewPtr>>>2>>>0];var length=HEAPU32[stringViewPtr+4>>>2>>>0];return UTF8ToString(ptr,length)},makeStringFromOptionalStringView:stringViewPtr=>{var ptr=HEAPU32[stringViewPtr>>>2>>>0];var length=HEAPU32[stringViewPtr+4>>>2>>>0];if(!ptr){if(length===0){return""}return undefined}return UTF8ToString(ptr,length)},makeColor:ptr=>({r:HEAPF64[ptr>>>3>>>0],g:HEAPF64[ptr+8>>>3>>>0],b:HEAPF64[ptr+16>>>3>>>0],a:HEAPF64[ptr+24>>>3>>>0]}),makeExtent3D:ptr=>({width:HEAPU32[ptr>>>2>>>0],height:HEAPU32[ptr+4>>>2>>>0],depthOrArrayLayers:HEAPU32[ptr+8>>>2>>>0]}),makeOrigin3D:ptr=>({x:HEAPU32[ptr>>>2>>>0],y:HEAPU32[ptr+4>>>2>>>0],z:HEAPU32[ptr+8>>>2>>>0]}),makeTexelCopyTextureInfo:ptr=>({texture:WebGPU.getJsObject(HEAPU32[ptr>>>2>>>0]),mipLevel:HEAPU32[ptr+4>>>2>>>0],origin:WebGPU.makeOrigin3D(ptr+8),aspect:WebGPU.TextureAspect[HEAPU32[ptr+20>>>2>>>0]]}),makeTexelCopyBufferLayout:ptr=>{var bytesPerRow=HEAPU32[ptr+8>>>2>>>0];var rowsPerImage=HEAPU32[ptr+12>>>2>>>0];return{offset:HEAPU32[ptr+4>>>2>>>0]*4294967296+HEAPU32[ptr>>>2>>>0],bytesPerRow:bytesPerRow===4294967295?undefined:bytesPerRow,rowsPerImage:rowsPerImage===4294967295?undefined:rowsPerImage}},makeTexelCopyBufferInfo:ptr=>{var layoutPtr=ptr+0;var bufferCopyView=WebGPU.makeTexelCopyBufferLayout(layoutPtr);bufferCopyView["buffer"]=WebGPU.getJsObject(HEAPU32[ptr+16>>>2>>>0]);return bufferCopyView},makePassTimestampWrites:ptr=>{if(ptr===0)return undefined;return{querySet:WebGPU.getJsObject(HEAPU32[ptr+4>>>2>>>0]),beginningOfPassWriteIndex:HEAPU32[ptr+8>>>2>>>0],endOfPassWriteIndex:HEAPU32[ptr+12>>>2>>>0]}},makePipelineConstants:(constantCount,constantsPtr)=>{if(!constantCount)return;var constants={};for(var i=0;i>>3>>>0]}return constants},makePipelineLayout:layoutPtr=>{if(!layoutPtr)return"auto";return WebGPU.getJsObject(layoutPtr)},makeComputeState:ptr=>{if(!ptr)return undefined;var desc={module:WebGPU.getJsObject(HEAPU32[ptr+4>>>2>>>0]),constants:WebGPU.makePipelineConstants(HEAPU32[ptr+16>>>2>>>0],HEAPU32[ptr+20>>>2>>>0]),entryPoint:WebGPU.makeStringFromOptionalStringView(ptr+8)};return desc},makeComputePipelineDesc:descriptor=>{var desc={label:WebGPU.makeStringFromOptionalStringView(descriptor+4),layout:WebGPU.makePipelineLayout(HEAPU32[descriptor+12>>>2>>>0]),compute:WebGPU.makeComputeState(descriptor+16)};return desc},makeRenderPipelineDesc:descriptor=>{function makePrimitiveState(psPtr){if(!psPtr)return undefined;return{topology:WebGPU.PrimitiveTopology[HEAPU32[psPtr+4>>>2>>>0]],stripIndexFormat:WebGPU.IndexFormat[HEAPU32[psPtr+8>>>2>>>0]],frontFace:WebGPU.FrontFace[HEAPU32[psPtr+12>>>2>>>0]],cullMode:WebGPU.CullMode[HEAPU32[psPtr+16>>>2>>>0]],unclippedDepth:!!HEAPU32[psPtr+20>>>2>>>0]}}function makeBlendComponent(bdPtr){if(!bdPtr)return undefined;return{operation:WebGPU.BlendOperation[HEAPU32[bdPtr>>>2>>>0]],srcFactor:WebGPU.BlendFactor[HEAPU32[bdPtr+4>>>2>>>0]],dstFactor:WebGPU.BlendFactor[HEAPU32[bdPtr+8>>>2>>>0]]}}function makeBlendState(bsPtr){if(!bsPtr)return undefined;return{alpha:makeBlendComponent(bsPtr+12),color:makeBlendComponent(bsPtr+0)}}function makeColorState(csPtr){var formatInt=HEAPU32[csPtr+4>>>2>>>0];return formatInt===0?undefined:{format:WebGPU.TextureFormat[formatInt],blend:makeBlendState(HEAPU32[csPtr+8>>>2>>>0]),writeMask:HEAPU32[csPtr+16>>>2>>>0]}}function makeColorStates(count,csArrayPtr){var states=[];for(var i=0;i>>2>>>0]],failOp:WebGPU.StencilOperation[HEAPU32[ssfPtr+4>>>2>>>0]],depthFailOp:WebGPU.StencilOperation[HEAPU32[ssfPtr+8>>>2>>>0]],passOp:WebGPU.StencilOperation[HEAPU32[ssfPtr+12>>>2>>>0]]}}function makeDepthStencilState(dssPtr){if(!dssPtr)return undefined;return{format:WebGPU.TextureFormat[HEAPU32[dssPtr+4>>>2>>>0]],depthWriteEnabled:!!HEAPU32[dssPtr+8>>>2>>>0],depthCompare:WebGPU.CompareFunction[HEAPU32[dssPtr+12>>>2>>>0]],stencilFront:makeStencilStateFace(dssPtr+16),stencilBack:makeStencilStateFace(dssPtr+32),stencilReadMask:HEAPU32[dssPtr+48>>>2>>>0],stencilWriteMask:HEAPU32[dssPtr+52>>>2>>>0],depthBias:HEAP32[dssPtr+56>>>2>>>0],depthBiasSlopeScale:HEAPF32[dssPtr+60>>>2>>>0],depthBiasClamp:HEAPF32[dssPtr+64>>>2>>>0]}}function makeVertexAttribute(vaPtr){return{format:WebGPU.VertexFormat[HEAPU32[vaPtr+4>>>2>>>0]],offset:HEAPU32[vaPtr+4+8>>>2>>>0]*4294967296+HEAPU32[vaPtr+8>>>2>>>0],shaderLocation:HEAPU32[vaPtr+16>>>2>>>0]}}function makeVertexAttributes(count,vaArrayPtr){var vas=[];for(var i=0;i>>2>>>0];var attributeCountInt=HEAPU32[vbPtr+16>>>2>>>0];if(stepModeInt===0&&attributeCountInt===0){return null}return{arrayStride:HEAPU32[vbPtr+4+8>>>2>>>0]*4294967296+HEAPU32[vbPtr+8>>>2>>>0],stepMode:WebGPU.VertexStepMode[stepModeInt],attributes:makeVertexAttributes(attributeCountInt,HEAPU32[vbPtr+20>>>2>>>0])}}function makeVertexBuffers(count,vbArrayPtr){if(!count)return undefined;var vbs=[];for(var i=0;i>>2>>>0]),constants:WebGPU.makePipelineConstants(HEAPU32[viPtr+16>>>2>>>0],HEAPU32[viPtr+20>>>2>>>0]),buffers:makeVertexBuffers(HEAPU32[viPtr+24>>>2>>>0],HEAPU32[viPtr+28>>>2>>>0]),entryPoint:WebGPU.makeStringFromOptionalStringView(viPtr+8)};return desc}function makeMultisampleState(msPtr){if(!msPtr)return undefined;return{count:HEAPU32[msPtr+4>>>2>>>0],mask:HEAPU32[msPtr+8>>>2>>>0],alphaToCoverageEnabled:!!HEAPU32[msPtr+12>>>2>>>0]}}function makeFragmentState(fsPtr){if(!fsPtr)return undefined;var desc={module:WebGPU.getJsObject(HEAPU32[fsPtr+4>>>2>>>0]),constants:WebGPU.makePipelineConstants(HEAPU32[fsPtr+16>>>2>>>0],HEAPU32[fsPtr+20>>>2>>>0]),targets:makeColorStates(HEAPU32[fsPtr+24>>>2>>>0],HEAPU32[fsPtr+28>>>2>>>0]),entryPoint:WebGPU.makeStringFromOptionalStringView(fsPtr+8)};return desc}var desc={label:WebGPU.makeStringFromOptionalStringView(descriptor+4),layout:WebGPU.makePipelineLayout(HEAPU32[descriptor+12>>>2>>>0]),vertex:makeVertexState(descriptor+16),primitive:makePrimitiveState(descriptor+48),depthStencil:makeDepthStencilState(HEAPU32[descriptor+72>>>2>>>0]),multisample:makeMultisampleState(descriptor+76),fragment:makeFragmentState(HEAPU32[descriptor+92>>>2>>>0])};return desc},fillLimitStruct:(limits,limitsOutPtr)=>{function setLimitValueU32(name,limitOffset){var limitValue=limits[name];HEAP32[limitsOutPtr+limitOffset>>>2>>>0]=limitValue}function setLimitValueU64(name,limitOffset){var limitValue=limits[name];HEAP64[limitsOutPtr+limitOffset>>>3>>>0]=BigInt(limitValue)}setLimitValueU32("maxTextureDimension1D",4);setLimitValueU32("maxTextureDimension2D",8);setLimitValueU32("maxTextureDimension3D",12);setLimitValueU32("maxTextureArrayLayers",16);setLimitValueU32("maxBindGroups",20);setLimitValueU32("maxBindGroupsPlusVertexBuffers",24);setLimitValueU32("maxBindingsPerBindGroup",28);setLimitValueU32("maxDynamicUniformBuffersPerPipelineLayout",32);setLimitValueU32("maxDynamicStorageBuffersPerPipelineLayout",36);setLimitValueU32("maxSampledTexturesPerShaderStage",40);setLimitValueU32("maxSamplersPerShaderStage",44);setLimitValueU32("maxStorageBuffersPerShaderStage",48);setLimitValueU32("maxStorageTexturesPerShaderStage",52);setLimitValueU32("maxUniformBuffersPerShaderStage",56);setLimitValueU32("minUniformBufferOffsetAlignment",80);setLimitValueU32("minStorageBufferOffsetAlignment",84);setLimitValueU64("maxUniformBufferBindingSize",64);setLimitValueU64("maxStorageBufferBindingSize",72);setLimitValueU32("maxVertexBuffers",88);setLimitValueU64("maxBufferSize",96);setLimitValueU32("maxVertexAttributes",104);setLimitValueU32("maxVertexBufferArrayStride",108);setLimitValueU32("maxInterStageShaderVariables",112);setLimitValueU32("maxColorAttachments",116);setLimitValueU32("maxColorAttachmentBytesPerSample",120);setLimitValueU32("maxComputeWorkgroupStorageSize",124);setLimitValueU32("maxComputeInvocationsPerWorkgroup",128);setLimitValueU32("maxComputeWorkgroupSizeX",132);setLimitValueU32("maxComputeWorkgroupSizeY",136);setLimitValueU32("maxComputeWorkgroupSizeZ",140);setLimitValueU32("maxComputeWorkgroupsPerDimension",144);if(limits.maxImmediateSize!==undefined){setLimitValueU32("maxImmediateSize",148)}},fillAdapterInfoStruct:(info,infoStruct)=>{HEAP32[infoStruct+52>>>2>>>0]=info.subgroupMinSize;HEAP32[infoStruct+56>>>2>>>0]=info.subgroupMaxSize;var strs=info.vendor+info.architecture+info.device+info.description;var strPtr=stringToNewUTF8(strs);var vendorLen=lengthBytesUTF8(info.vendor);WebGPU.setStringView(infoStruct+4,strPtr,vendorLen);strPtr+=vendorLen;var architectureLen=lengthBytesUTF8(info.architecture);WebGPU.setStringView(infoStruct+12,strPtr,architectureLen);strPtr+=architectureLen;var deviceLen=lengthBytesUTF8(info.device);WebGPU.setStringView(infoStruct+20,strPtr,deviceLen);strPtr+=deviceLen;var descriptionLen=lengthBytesUTF8(info.description);WebGPU.setStringView(infoStruct+28,strPtr,descriptionLen);strPtr+=descriptionLen;HEAP32[infoStruct+36>>>2>>>0]=2;var adapterType=info.isFallbackAdapter?3:4;HEAP32[infoStruct+40>>>2>>>0]=adapterType;HEAP32[infoStruct+44>>>2>>>0]=0;HEAP32[infoStruct+48>>>2>>>0]=0},AddressMode:[,"clamp-to-edge","repeat","mirror-repeat"],BlendFactor:[,"zero","one","src","one-minus-src","src-alpha","one-minus-src-alpha","dst","one-minus-dst","dst-alpha","one-minus-dst-alpha","src-alpha-saturated","constant","one-minus-constant","src1","one-minus-src1","src1alpha","one-minus-src1alpha"],BlendOperation:[,"add","subtract","reverse-subtract","min","max"],BufferBindingType:["binding-not-used",,"uniform","storage","read-only-storage"],BufferMapState:[,"unmapped","pending","mapped"],CompareFunction:[,"never","less","equal","less-equal","greater","not-equal","greater-equal","always"],CompilationInfoRequestStatus:[,"success","callback-cancelled"],CompositeAlphaMode:[,"opaque","premultiplied","unpremultiplied","inherit"],CullMode:[,"none","front","back"],ErrorFilter:[,"validation","out-of-memory","internal"],FeatureLevel:[,"compatibility","core"],FeatureName:{1:"core-features-and-limits",2:"depth-clip-control",3:"depth32float-stencil8",4:"texture-compression-bc",5:"texture-compression-bc-sliced-3d",6:"texture-compression-etc2",7:"texture-compression-astc",8:"texture-compression-astc-sliced-3d",9:"timestamp-query",10:"indirect-first-instance",11:"shader-f16",12:"rg11b10ufloat-renderable",13:"bgra8unorm-storage",14:"float32-filterable",15:"float32-blendable",16:"clip-distances",17:"dual-source-blending",18:"subgroups",19:"texture-formats-tier1",20:"texture-formats-tier2",21:"primitive-index",327692:"chromium-experimental-unorm16-texture-formats",327693:"chromium-experimental-snorm16-texture-formats",327732:"chromium-experimental-multi-draw-indirect"},FilterMode:[,"nearest","linear"],FrontFace:[,"ccw","cw"],IndexFormat:[,"uint16","uint32"],InstanceFeatureName:[,"timed-wait-any","shader-source-spirv","multiple-devices-per-adapter"],LoadOp:[,"load","clear"],MipmapFilterMode:[,"nearest","linear"],OptionalBool:["false","true"],PowerPreference:[,"low-power","high-performance"],PredefinedColorSpace:[,"srgb","display-p3"],PrimitiveTopology:[,"point-list","line-list","line-strip","triangle-list","triangle-strip"],QueryType:[,"occlusion","timestamp"],SamplerBindingType:["binding-not-used",,"filtering","non-filtering","comparison"],Status:[,"success","error"],StencilOperation:[,"keep","zero","replace","invert","increment-clamp","decrement-clamp","increment-wrap","decrement-wrap"],StorageTextureAccess:["binding-not-used",,"write-only","read-only","read-write"],StoreOp:[,"store","discard"],SurfaceGetCurrentTextureStatus:[,"success-optimal","success-suboptimal","timeout","outdated","lost","error"],TextureAspect:[,"all","stencil-only","depth-only"],TextureDimension:[,"1d","2d","3d"],TextureFormat:[,"r8unorm","r8snorm","r8uint","r8sint","r16unorm","r16snorm","r16uint","r16sint","r16float","rg8unorm","rg8snorm","rg8uint","rg8sint","r32float","r32uint","r32sint","rg16unorm","rg16snorm","rg16uint","rg16sint","rg16float","rgba8unorm","rgba8unorm-srgb","rgba8snorm","rgba8uint","rgba8sint","bgra8unorm","bgra8unorm-srgb","rgb10a2uint","rgb10a2unorm","rg11b10ufloat","rgb9e5ufloat","rg32float","rg32uint","rg32sint","rgba16unorm","rgba16snorm","rgba16uint","rgba16sint","rgba16float","rgba32float","rgba32uint","rgba32sint","stencil8","depth16unorm","depth24plus","depth24plus-stencil8","depth32float","depth32float-stencil8","bc1-rgba-unorm","bc1-rgba-unorm-srgb","bc2-rgba-unorm","bc2-rgba-unorm-srgb","bc3-rgba-unorm","bc3-rgba-unorm-srgb","bc4-r-unorm","bc4-r-snorm","bc5-rg-unorm","bc5-rg-snorm","bc6h-rgb-ufloat","bc6h-rgb-float","bc7-rgba-unorm","bc7-rgba-unorm-srgb","etc2-rgb8unorm","etc2-rgb8unorm-srgb","etc2-rgb8a1unorm","etc2-rgb8a1unorm-srgb","etc2-rgba8unorm","etc2-rgba8unorm-srgb","eac-r11unorm","eac-r11snorm","eac-rg11unorm","eac-rg11snorm","astc-4x4-unorm","astc-4x4-unorm-srgb","astc-5x4-unorm","astc-5x4-unorm-srgb","astc-5x5-unorm","astc-5x5-unorm-srgb","astc-6x5-unorm","astc-6x5-unorm-srgb","astc-6x6-unorm","astc-6x6-unorm-srgb","astc-8x5-unorm","astc-8x5-unorm-srgb","astc-8x6-unorm","astc-8x6-unorm-srgb","astc-8x8-unorm","astc-8x8-unorm-srgb","astc-10x5-unorm","astc-10x5-unorm-srgb","astc-10x6-unorm","astc-10x6-unorm-srgb","astc-10x8-unorm","astc-10x8-unorm-srgb","astc-10x10-unorm","astc-10x10-unorm-srgb","astc-12x10-unorm","astc-12x10-unorm-srgb","astc-12x12-unorm","astc-12x12-unorm-srgb"],TextureSampleType:["binding-not-used",,"float","unfilterable-float","depth","sint","uint"],TextureViewDimension:[,"1d","2d","2d-array","cube","cube-array","3d"],ToneMappingMode:[,"standard","extended"],VertexFormat:[,"uint8","uint8x2","uint8x4","sint8","sint8x2","sint8x4","unorm8","unorm8x2","unorm8x4","snorm8","snorm8x2","snorm8x4","uint16","uint16x2","uint16x4","sint16","sint16x2","sint16x4","unorm16","unorm16x2","unorm16x4","snorm16","snorm16x2","snorm16x4","float16","float16x2","float16x4","float32","float32x2","float32x3","float32x4","uint32","uint32x2","uint32x3","uint32x4","sint32","sint32x2","sint32x3","sint32x4","unorm10-10-10-2","unorm8x4-bgra"],VertexStepMode:[,"vertex","instance"],WGSLLanguageFeatureName:[,"readonly_and_readwrite_storage_textures","packed_4x8_integer_dot_product","unrestricted_pointer_parameters","pointer_composite_access"]};var emwgpuStringToInt_DeviceLostReason={undefined:1,unknown:1,destroyed:2};function _emwgpuAdapterRequestDevice(adapterPtr,futureId,deviceLostFutureId,devicePtr,queuePtr,descriptor){adapterPtr>>>=0;futureId=bigintToI53Checked(futureId);deviceLostFutureId=bigintToI53Checked(deviceLostFutureId);devicePtr>>>=0;queuePtr>>>=0;descriptor>>>=0;var adapter=WebGPU.getJsObject(adapterPtr);var desc={};if(descriptor){var requiredFeatureCount=HEAPU32[descriptor+12>>>2>>>0];if(requiredFeatureCount){var requiredFeaturesPtr=HEAPU32[descriptor+16>>>2>>>0];desc["requiredFeatures"]=Array.from(HEAPU32.subarray(requiredFeaturesPtr>>>2>>>0,requiredFeaturesPtr+requiredFeatureCount*4>>>2>>>0),feature=>WebGPU.FeatureName[feature])}var limitsPtr=HEAPU32[descriptor+20>>>2>>>0];if(limitsPtr){var requiredLimits={};function setLimitU32IfDefined(name,limitOffset,ignoreIfZero=false){var ptr=limitsPtr+limitOffset;var value=HEAPU32[ptr>>>2>>>0];if(value!=4294967295&&(!ignoreIfZero||value!=0)){requiredLimits[name]=value}}function setLimitU64IfDefined(name,limitOffset){var ptr=limitsPtr+limitOffset;var limitPart1=HEAPU32[ptr>>>2>>>0];var limitPart2=HEAPU32[ptr+4>>>2>>>0];if(limitPart1!=4294967295||limitPart2!=4294967295){requiredLimits[name]=HEAPU32[ptr+4>>>2>>>0]*4294967296+HEAPU32[ptr>>>2>>>0]}}setLimitU32IfDefined("maxTextureDimension1D",4);setLimitU32IfDefined("maxTextureDimension2D",8);setLimitU32IfDefined("maxTextureDimension3D",12);setLimitU32IfDefined("maxTextureArrayLayers",16);setLimitU32IfDefined("maxBindGroups",20);setLimitU32IfDefined("maxBindGroupsPlusVertexBuffers",24);setLimitU32IfDefined("maxDynamicUniformBuffersPerPipelineLayout",32);setLimitU32IfDefined("maxDynamicStorageBuffersPerPipelineLayout",36);setLimitU32IfDefined("maxSampledTexturesPerShaderStage",40);setLimitU32IfDefined("maxSamplersPerShaderStage",44);setLimitU32IfDefined("maxStorageBuffersPerShaderStage",48);setLimitU32IfDefined("maxStorageTexturesPerShaderStage",52);setLimitU32IfDefined("maxUniformBuffersPerShaderStage",56);setLimitU32IfDefined("minUniformBufferOffsetAlignment",80);setLimitU32IfDefined("minStorageBufferOffsetAlignment",84);setLimitU64IfDefined("maxUniformBufferBindingSize",64);setLimitU64IfDefined("maxStorageBufferBindingSize",72);setLimitU32IfDefined("maxVertexBuffers",88);setLimitU64IfDefined("maxBufferSize",96);setLimitU32IfDefined("maxVertexAttributes",104);setLimitU32IfDefined("maxVertexBufferArrayStride",108);setLimitU32IfDefined("maxInterStageShaderVariables",112);setLimitU32IfDefined("maxColorAttachments",116);setLimitU32IfDefined("maxColorAttachmentBytesPerSample",120);setLimitU32IfDefined("maxComputeWorkgroupStorageSize",124);setLimitU32IfDefined("maxComputeInvocationsPerWorkgroup",128);setLimitU32IfDefined("maxComputeWorkgroupSizeX",132);setLimitU32IfDefined("maxComputeWorkgroupSizeY",136);setLimitU32IfDefined("maxComputeWorkgroupSizeZ",140);setLimitU32IfDefined("maxComputeWorkgroupsPerDimension",144);setLimitU32IfDefined("maxImmediateSize",148,true);desc["requiredLimits"]=requiredLimits}var defaultQueuePtr=HEAPU32[descriptor+24>>>2>>>0];if(defaultQueuePtr){var defaultQueueDesc={label:WebGPU.makeStringFromOptionalStringView(defaultQueuePtr+4)};desc["defaultQueue"]=defaultQueueDesc}desc["label"]=WebGPU.makeStringFromOptionalStringView(descriptor+4)}WebGPU.Internals.futureInsert(futureId,adapter.requestDevice(desc).then(device=>{callUserCallback(()=>{WebGPU.Internals.jsObjectInsert(queuePtr,device.queue);WebGPU.Internals.jsObjectInsert(devicePtr,device);WebGPU.Internals.futureInsert(deviceLostFutureId,device.lost.then(info=>{callUserCallback(()=>{device.onuncapturederror=ev=>{};var sp=stackSave();var messagePtr=stringToUTF8OnStack(info.message);_emwgpuOnDeviceLostCompleted(deviceLostFutureId,emwgpuStringToInt_DeviceLostReason[info.reason],messagePtr);stackRestore(sp)})}));device.onuncapturederror=ev=>{var type=5;if(ev.error instanceof GPUValidationError)type=2;else if(ev.error instanceof GPUOutOfMemoryError)type=3;else if(ev.error instanceof GPUInternalError)type=4;var sp=stackSave();var messagePtr=stringToUTF8OnStack(ev.error.message);_emwgpuOnUncapturedError(devicePtr,type,messagePtr);stackRestore(sp)};_emwgpuOnRequestDeviceCompleted(futureId,1,devicePtr,0)})},ex=>{callUserCallback(()=>{var sp=stackSave();var messagePtr=stringToUTF8OnStack(ex.message);_emwgpuOnRequestDeviceCompleted(futureId,3,devicePtr,messagePtr);if(deviceLostFutureId){_emwgpuOnDeviceLostCompleted(deviceLostFutureId,4,messagePtr)}stackRestore(sp)})}))}var warnOnce=text=>{warnOnce.shown||={};if(!warnOnce.shown[text]){warnOnce.shown[text]=1;err(text)}};function _emwgpuBufferGetConstMappedRange(bufferPtr,offset,size){bufferPtr>>>=0;offset>>>=0;size>>>=0;var buffer=WebGPU.getJsObject(bufferPtr);if(size===0)warnOnce("getMappedRange size=0 no longer means WGPU_WHOLE_MAP_SIZE");if(size==4294967295)size=undefined;var mapped;try{mapped=buffer.getMappedRange(offset,size)}catch(ex){return 0}var data=_memalign(16,mapped.byteLength);HEAPU8.set(new Uint8Array(mapped),data>>>0);WebGPU.Internals.bufferOnUnmaps[bufferPtr].push(()=>_free(data));return data}function _emwgpuBufferGetMappedRange(bufferPtr,offset,size){bufferPtr>>>=0;offset>>>=0;size>>>=0;var buffer=WebGPU.getJsObject(bufferPtr);if(size===0)warnOnce("getMappedRange size=0 no longer means WGPU_WHOLE_MAP_SIZE");if(size==4294967295)size=undefined;var mapped;try{mapped=buffer.getMappedRange(offset,size)}catch(ex){return 0}var data=_memalign(16,mapped.byteLength);HEAPU8.fill(0,data,mapped.byteLength);WebGPU.Internals.bufferOnUnmaps[bufferPtr].push(()=>{new Uint8Array(mapped).set(HEAPU8.subarray(data>>>0,data+mapped.byteLength>>>0));_free(data)});return data}var _emwgpuBufferMapAsync=function(bufferPtr,futureId,mode,offset,size){bufferPtr>>>=0;futureId=bigintToI53Checked(futureId);mode=bigintToI53Checked(mode);offset>>>=0;size>>>=0;var buffer=WebGPU.getJsObject(bufferPtr);WebGPU.Internals.bufferOnUnmaps[bufferPtr]=[];if(size==4294967295)size=undefined;WebGPU.Internals.futureInsert(futureId,buffer.mapAsync(mode,offset,size).then(()=>{callUserCallback(()=>{_emwgpuOnMapAsyncCompleted(futureId,1,0)})},ex=>{callUserCallback(()=>{var sp=stackSave();var messagePtr=stringToUTF8OnStack(ex.message);var status=ex.name==="AbortError"?4:ex.name==="OperationError"?3:0;_emwgpuOnMapAsyncCompleted(futureId,status,messagePtr);delete WebGPU.Internals.bufferOnUnmaps[bufferPtr]})}))};function _emwgpuBufferUnmap(bufferPtr){bufferPtr>>>=0;var buffer=WebGPU.getJsObject(bufferPtr);var onUnmap=WebGPU.Internals.bufferOnUnmaps[bufferPtr];if(!onUnmap){return}for(var i=0;i>>=0;delete WebGPU.Internals.jsObjects[ptr]}function _emwgpuDeviceCreateBuffer(devicePtr,descriptor,bufferPtr){devicePtr>>>=0;descriptor>>>=0;bufferPtr>>>=0;var mappedAtCreation=!!HEAPU32[descriptor+32>>>2>>>0];var desc={label:WebGPU.makeStringFromOptionalStringView(descriptor+4),usage:HEAPU32[descriptor+16>>>2>>>0],size:HEAPU32[descriptor+4+24>>>2>>>0]*4294967296+HEAPU32[descriptor+24>>>2>>>0],mappedAtCreation};var device=WebGPU.getJsObject(devicePtr);var buffer;try{buffer=device.createBuffer(desc)}catch(ex){return false}WebGPU.Internals.jsObjectInsert(bufferPtr,buffer);if(mappedAtCreation){WebGPU.Internals.bufferOnUnmaps[bufferPtr]=[]}return true}function _emwgpuDeviceCreateShaderModule(devicePtr,descriptor,shaderModulePtr){devicePtr>>>=0;descriptor>>>=0;shaderModulePtr>>>=0;var nextInChainPtr=HEAPU32[descriptor>>>2>>>0];var sType=HEAPU32[nextInChainPtr+4>>>2>>>0];var desc={label:WebGPU.makeStringFromOptionalStringView(descriptor+4),code:""};switch(sType){case 2:{desc["code"]=WebGPU.makeStringFromStringView(nextInChainPtr+8);break}}var device=WebGPU.getJsObject(devicePtr);WebGPU.Internals.jsObjectInsert(shaderModulePtr,device.createShaderModule(desc))}var _emwgpuDeviceDestroy=devicePtr=>{const device=WebGPU.getJsObject(devicePtr);device.onuncapturederror=null;device.destroy()};function _emwgpuInstanceRequestAdapter(instancePtr,futureId,options,adapterPtr){instancePtr>>>=0;futureId=bigintToI53Checked(futureId);options>>>=0;adapterPtr>>>=0;var opts;if(options){var featureLevel=HEAPU32[options+4>>>2>>>0];opts={featureLevel:WebGPU.FeatureLevel[featureLevel],powerPreference:WebGPU.PowerPreference[HEAPU32[options+8>>>2>>>0]],forceFallbackAdapter:!!HEAPU32[options+12>>>2>>>0]};var nextInChainPtr=HEAPU32[options>>>2>>>0];if(nextInChainPtr!==0){var sType=HEAPU32[nextInChainPtr+4>>>2>>>0];var webxrOptions=nextInChainPtr;opts.xrCompatible=!!HEAPU32[webxrOptions+8>>>2>>>0]}}if(!("gpu"in navigator)){var sp=stackSave();var messagePtr=stringToUTF8OnStack("WebGPU not available on this browser (navigator.gpu is not available)");_emwgpuOnRequestAdapterCompleted(futureId,3,adapterPtr,messagePtr);stackRestore(sp);return}WebGPU.Internals.futureInsert(futureId,navigator.gpu.requestAdapter(opts).then(adapter=>{callUserCallback(()=>{if(adapter){WebGPU.Internals.jsObjectInsert(adapterPtr,adapter);_emwgpuOnRequestAdapterCompleted(futureId,1,adapterPtr,0)}else{var sp=stackSave();var messagePtr=stringToUTF8OnStack("WebGPU not available on this browser (requestAdapter returned null)");_emwgpuOnRequestAdapterCompleted(futureId,3,adapterPtr,messagePtr);stackRestore(sp)}})},ex=>{callUserCallback(()=>{var sp=stackSave();var messagePtr=stringToUTF8OnStack(ex.message);_emwgpuOnRequestAdapterCompleted(futureId,4,adapterPtr,messagePtr);stackRestore(sp)})}))}var _emwgpuWaitAny=function(futurePtr,futureCount,timeoutMSPtr){futurePtr>>>=0;futureCount>>>=0;timeoutMSPtr>>>=0;return Asyncify.handleAsync(async()=>{var promises=[];if(timeoutMSPtr){var timeoutMS=HEAP32[timeoutMSPtr>>>2>>>0];promises.length=futureCount+1;promises[futureCount]=new Promise(resolve=>setTimeout(resolve,timeoutMS,0))}else{promises.length=futureCount}for(var i=0;i>>2>>>0]*4294967296+HEAPU32[futurePtr+i*8>>>2>>>0];if(!(futureId in WebGPU.Internals.futures)){return futureId}promises[i]=WebGPU.Internals.futures[futureId]}const firstResolvedFuture=await Promise.race(promises);delete WebGPU.Internals.futures[firstResolvedFuture];return firstResolvedFuture})};_emwgpuWaitAny.isAsync=true;function _fd_close(fd){try{var stream=SYSCALLS.getStreamFromFD(fd);FS.close(stream);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var doReadv=(stream,iov,iovcnt,offset)=>{var ret=0;for(var i=0;i>>2>>>0];var len=HEAPU32[iov+4>>>2>>>0];iov+=8;var curr=FS.read(stream,HEAP8,ptr,len,offset);if(curr<0)return-1;ret+=curr;if(curr>>=0;iovcnt>>>=0;offset=bigintToI53Checked(offset);pnum>>>=0;try{if(isNaN(offset))return 61;var stream=SYSCALLS.getStreamFromFD(fd);var num=doReadv(stream,iov,iovcnt,offset);HEAPU32[pnum>>>2>>>0]=num;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}function _fd_seek(fd,offset,whence,newOffset){offset=bigintToI53Checked(offset);newOffset>>>=0;try{if(isNaN(offset))return 61;var stream=SYSCALLS.getStreamFromFD(fd);FS.llseek(stream,offset,whence);HEAP64[newOffset>>>3>>>0]=BigInt(stream.position);if(stream.getdents&&offset===0&&whence===0)stream.getdents=null;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var doWritev=(stream,iov,iovcnt,offset)=>{var ret=0;for(var i=0;i>>2>>>0];var len=HEAPU32[iov+4>>>2>>>0];iov+=8;var curr=FS.write(stream,HEAP8,ptr,len,offset);if(curr<0)return-1;ret+=curr;if(curr>>=0;iovcnt>>>=0;pnum>>>=0;try{var stream=SYSCALLS.getStreamFromFD(fd);var num=doWritev(stream,iov,iovcnt);HEAPU32[pnum>>>2>>>0]=num;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}function _llvm_eh_typeid_for(type){type>>>=0;return type}function _random_get(buffer,size){buffer>>>=0;size>>>=0;try{randomFill(HEAPU8.subarray(buffer>>>0,buffer+size>>>0));return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}function _wgpuAdapterGetLimits(adapterPtr,limitsOutPtr){adapterPtr>>>=0;limitsOutPtr>>>=0;var adapter=WebGPU.getJsObject(adapterPtr);WebGPU.fillLimitStruct(adapter.limits,limitsOutPtr);return 1}function _wgpuAdapterHasFeature(adapterPtr,featureEnumValue){adapterPtr>>>=0;var adapter=WebGPU.getJsObject(adapterPtr);return adapter.features.has(WebGPU.FeatureName[featureEnumValue])}var _wgpuBufferGetSize=function(bufferPtr){bufferPtr>>>=0;var ret=(()=>{var buffer=WebGPU.getJsObject(bufferPtr);return buffer.size})();return BigInt(ret)};function _wgpuCommandEncoderBeginComputePass(encoderPtr,descriptor){encoderPtr>>>=0;descriptor>>>=0;var desc;if(descriptor){desc={label:WebGPU.makeStringFromOptionalStringView(descriptor+4),timestampWrites:WebGPU.makePassTimestampWrites(HEAPU32[descriptor+12>>>2>>>0])}}var commandEncoder=WebGPU.getJsObject(encoderPtr);var ptr=_emwgpuCreateComputePassEncoder(0);WebGPU.Internals.jsObjectInsert(ptr,commandEncoder.beginComputePass(desc));return ptr}function _wgpuCommandEncoderCopyBufferToBuffer(encoderPtr,srcPtr,srcOffset,dstPtr,dstOffset,size){encoderPtr>>>=0;srcPtr>>>=0;srcOffset=bigintToI53Checked(srcOffset);dstPtr>>>=0;dstOffset=bigintToI53Checked(dstOffset);size=bigintToI53Checked(size);var commandEncoder=WebGPU.getJsObject(encoderPtr);var src=WebGPU.getJsObject(srcPtr);var dst=WebGPU.getJsObject(dstPtr);commandEncoder.copyBufferToBuffer(src,srcOffset,dst,dstOffset,size)}function _wgpuCommandEncoderFinish(encoderPtr,descriptor){encoderPtr>>>=0;descriptor>>>=0;var commandEncoder=WebGPU.getJsObject(encoderPtr);var ptr=_emwgpuCreateCommandBuffer(0);WebGPU.Internals.jsObjectInsert(ptr,commandEncoder.finish());return ptr}function _wgpuComputePassEncoderDispatchWorkgroups(passPtr,x,y,z){passPtr>>>=0;var pass=WebGPU.getJsObject(passPtr);pass.dispatchWorkgroups(x,y,z)}function _wgpuComputePassEncoderEnd(passPtr){passPtr>>>=0;var pass=WebGPU.getJsObject(passPtr);pass.end()}function _wgpuComputePassEncoderSetBindGroup(passPtr,groupIndex,groupPtr,dynamicOffsetCount,dynamicOffsetsPtr){passPtr>>>=0;groupPtr>>>=0;dynamicOffsetCount>>>=0;dynamicOffsetsPtr>>>=0;var pass=WebGPU.getJsObject(passPtr);var group=WebGPU.getJsObject(groupPtr);if(dynamicOffsetCount==0){pass.setBindGroup(groupIndex,group)}else{pass.setBindGroup(groupIndex,group,HEAPU32,dynamicOffsetsPtr>>>2,dynamicOffsetCount)}}function _wgpuComputePassEncoderSetPipeline(passPtr,pipelinePtr){passPtr>>>=0;pipelinePtr>>>=0;var pass=WebGPU.getJsObject(passPtr);var pipeline=WebGPU.getJsObject(pipelinePtr);pass.setPipeline(pipeline)}function _wgpuComputePipelineGetBindGroupLayout(pipelinePtr,groupIndex){pipelinePtr>>>=0;var pipeline=WebGPU.getJsObject(pipelinePtr);var ptr=_emwgpuCreateBindGroupLayout(0);WebGPU.Internals.jsObjectInsert(ptr,pipeline.getBindGroupLayout(groupIndex));return ptr}var readI53FromI64=ptr=>HEAPU32[ptr>>>2>>>0]+HEAP32[ptr+4>>>2>>>0]*4294967296;function _wgpuDeviceCreateBindGroup(devicePtr,descriptor){devicePtr>>>=0;descriptor>>>=0;function makeEntry(entryPtr){var bufferPtr=HEAPU32[entryPtr+8>>>2>>>0];var samplerPtr=HEAPU32[entryPtr+32>>>2>>>0];var textureViewPtr=HEAPU32[entryPtr+36>>>2>>>0];var binding=HEAPU32[entryPtr+4>>>2>>>0];if(bufferPtr){var size=readI53FromI64(entryPtr+24);if(size==-1)size=undefined;return{binding,resource:{buffer:WebGPU.getJsObject(bufferPtr),offset:HEAPU32[entryPtr+4+16>>>2>>>0]*4294967296+HEAPU32[entryPtr+16>>>2>>>0],size}}}else if(samplerPtr){return{binding,resource:WebGPU.getJsObject(samplerPtr)}}else{return{binding,resource:WebGPU.getJsObject(textureViewPtr)}}}function makeEntries(count,entriesPtrs){var entries=[];for(var i=0;i>>2>>>0]),entries:makeEntries(HEAPU32[descriptor+16>>>2>>>0],HEAPU32[descriptor+20>>>2>>>0])};var device=WebGPU.getJsObject(devicePtr);var ptr=_emwgpuCreateBindGroup(0);WebGPU.Internals.jsObjectInsert(ptr,device.createBindGroup(desc));return ptr}function _wgpuDeviceCreateBindGroupLayout(devicePtr,descriptor){devicePtr>>>=0;descriptor>>>=0;function makeBufferEntry(entryPtr){var typeInt=HEAPU32[entryPtr+4>>>2>>>0];if(!typeInt)return undefined;return{type:WebGPU.BufferBindingType[typeInt],hasDynamicOffset:!!HEAPU32[entryPtr+8>>>2>>>0],minBindingSize:HEAPU32[entryPtr+4+16>>>2>>>0]*4294967296+HEAPU32[entryPtr+16>>>2>>>0]}}function makeSamplerEntry(entryPtr){var typeInt=HEAPU32[entryPtr+4>>>2>>>0];if(!typeInt)return undefined;return{type:WebGPU.SamplerBindingType[typeInt]}}function makeTextureEntry(entryPtr){var sampleTypeInt=HEAPU32[entryPtr+4>>>2>>>0];if(!sampleTypeInt)return undefined;return{sampleType:WebGPU.TextureSampleType[sampleTypeInt],viewDimension:WebGPU.TextureViewDimension[HEAPU32[entryPtr+8>>>2>>>0]],multisampled:!!HEAPU32[entryPtr+12>>>2>>>0]}}function makeStorageTextureEntry(entryPtr){var accessInt=HEAPU32[entryPtr+4>>>2>>>0];if(!accessInt)return undefined;return{access:WebGPU.StorageTextureAccess[accessInt],format:WebGPU.TextureFormat[HEAPU32[entryPtr+8>>>2>>>0]],viewDimension:WebGPU.TextureViewDimension[HEAPU32[entryPtr+12>>>2>>>0]]}}function makeEntry(entryPtr){return{binding:HEAPU32[entryPtr+4>>>2>>>0],visibility:HEAPU32[entryPtr+8>>>2>>>0],buffer:makeBufferEntry(entryPtr+24),sampler:makeSamplerEntry(entryPtr+48),texture:makeTextureEntry(entryPtr+56),storageTexture:makeStorageTextureEntry(entryPtr+72)}}function makeEntries(count,entriesPtrs){var entries=[];for(var i=0;i>>2>>>0],HEAPU32[descriptor+16>>>2>>>0])};var device=WebGPU.getJsObject(devicePtr);var ptr=_emwgpuCreateBindGroupLayout(0);WebGPU.Internals.jsObjectInsert(ptr,device.createBindGroupLayout(desc));return ptr}function _wgpuDeviceCreateCommandEncoder(devicePtr,descriptor){devicePtr>>>=0;descriptor>>>=0;var desc;if(descriptor){desc={label:WebGPU.makeStringFromOptionalStringView(descriptor+4)}}var device=WebGPU.getJsObject(devicePtr);var ptr=_emwgpuCreateCommandEncoder(0);WebGPU.Internals.jsObjectInsert(ptr,device.createCommandEncoder(desc));return ptr}function _wgpuDeviceCreateComputePipeline(devicePtr,descriptor){devicePtr>>>=0;descriptor>>>=0;var desc=WebGPU.makeComputePipelineDesc(descriptor);var device=WebGPU.getJsObject(devicePtr);var ptr=_emwgpuCreateComputePipeline(0);WebGPU.Internals.jsObjectInsert(ptr,device.createComputePipeline(desc));return ptr}function _wgpuDeviceCreatePipelineLayout(devicePtr,descriptor){devicePtr>>>=0;descriptor>>>=0;var bglCount=HEAPU32[descriptor+12>>>2>>>0];var bglPtr=HEAPU32[descriptor+16>>>2>>>0];var bgls=[];for(var i=0;i>>2>>>0]))}var desc={label:WebGPU.makeStringFromOptionalStringView(descriptor+4),bindGroupLayouts:bgls};var device=WebGPU.getJsObject(devicePtr);var ptr=_emwgpuCreatePipelineLayout(0);WebGPU.Internals.jsObjectInsert(ptr,device.createPipelineLayout(desc));return ptr}function _wgpuDeviceGetLimits(devicePtr,limitsOutPtr){devicePtr>>>=0;limitsOutPtr>>>=0;var device=WebGPU.getJsObject(devicePtr);WebGPU.fillLimitStruct(device.limits,limitsOutPtr);return 1}var _wgpuQueueSubmit=function(queuePtr,commandCount,commands){queuePtr>>>=0;commandCount>>>=0;commands>>>=0;var queue=WebGPU.getJsObject(queuePtr);var cmds=Array.from(HEAP32.subarray(commands>>>2>>>0,commands+commandCount*4>>>2>>>0),id=>WebGPU.getJsObject(id));queue.submit(cmds)};function _wgpuQueueWriteBuffer(queuePtr,bufferPtr,bufferOffset,data,size){queuePtr>>>=0;bufferPtr>>>=0;bufferOffset=bigintToI53Checked(bufferOffset);data>>>=0;size>>>=0;var queue=WebGPU.getJsObject(queuePtr);var buffer=WebGPU.getJsObject(bufferPtr);var subarray=HEAPU8.subarray(data>>>0,data+size>>>0);queue.writeBuffer(buffer,bufferOffset,subarray,0,size)}var FS_createPath=(...args)=>FS.createPath(...args);var FS_unlink=(...args)=>FS.unlink(...args);var FS_createLazyFile=(...args)=>FS.createLazyFile(...args);var FS_createDevice=(...args)=>FS.createDevice(...args);var incrementExceptionRefcount=ptr=>___cxa_increment_exception_refcount(ptr);var decrementExceptionRefcount=ptr=>___cxa_decrement_exception_refcount(ptr);var getExceptionMessageCommon=ptr=>{var sp=stackSave();var type_addr_addr=stackAlloc(4);var message_addr_addr=stackAlloc(4);___get_exception_message(ptr,type_addr_addr,message_addr_addr);var type_addr=HEAPU32[type_addr_addr>>>2>>>0];var message_addr=HEAPU32[message_addr_addr>>>2>>>0];var type=UTF8ToString(type_addr);_free(type_addr);var message;if(message_addr){message=UTF8ToString(message_addr);_free(message_addr)}stackRestore(sp);return[type,message]};var getExceptionMessage=ptr=>getExceptionMessageCommon(ptr);FS.createPreloadedFile=FS_createPreloadedFile;FS.preloadFile=FS_preloadFile;FS.staticInit();init_ClassHandle();init_RegisteredPointer();{if(Module["noExitRuntime"])noExitRuntime=Module["noExitRuntime"];if(Module["preloadPlugins"])preloadPlugins=Module["preloadPlugins"];if(Module["print"])out=Module["print"];if(Module["printErr"])err=Module["printErr"];if(Module["wasmBinary"])wasmBinary=Module["wasmBinary"];if(Module["arguments"])arguments_=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].shift()()}}}Module["addRunDependency"]=addRunDependency;Module["removeRunDependency"]=removeRunDependency;Module["FS_preloadFile"]=FS_preloadFile;Module["FS_unlink"]=FS_unlink;Module["FS_createPath"]=FS_createPath;Module["FS_createDevice"]=FS_createDevice;Module["FS"]=FS;Module["FS_createDataFile"]=FS_createDataFile;Module["FS_createLazyFile"]=FS_createLazyFile;Module["incrementExceptionRefcount"]=incrementExceptionRefcount;Module["decrementExceptionRefcount"]=decrementExceptionRefcount;Module["getExceptionMessage"]=getExceptionMessage;var ASM_CONSTS={504824:$0=>{throw new Error(UTF8ToString($0))},504858:$0=>{throw new Error(UTF8ToString($0))},504892:$0=>{throw new TypeError(UTF8ToString($0))},504930:$0=>{throw new TypeError(UTF8ToString($0))},504968:$0=>{throw new TypeError(UTF8ToString($0))},505006:$0=>{throw new Error(UTF8ToString($0))},505040:$0=>{throw new Error(UTF8ToString($0))},505074:$0=>{throw new Error(UTF8ToString($0))},505108:$0=>{throw new Error(UTF8ToString($0))},505142:$0=>{throw new Error(UTF8ToString($0))},505176:$0=>{throw new Error(UTF8ToString($0))},505210:$0=>{throw new Error(UTF8ToString($0))},505244:$0=>{throw new Error(UTF8ToString($0))},505278:$0=>{throw new TypeError(UTF8ToString($0))},505316:$0=>{throw new Error(UTF8ToString($0))},505350:$0=>{throw new Error(UTF8ToString($0))},505384:$0=>{throw new TypeError(UTF8ToString($0))},505422:$0=>{throw new TypeError(UTF8ToString($0))},505460:$0=>{throw new Error(UTF8ToString($0))},505494:$0=>{throw new TypeError(UTF8ToString($0))},505532:$0=>{throw new TypeError(UTF8ToString($0))},505570:$0=>{throw new TypeError(UTF8ToString($0))},505608:$0=>{throw new TypeError(UTF8ToString($0))},505646:$0=>{throw new TypeError(UTF8ToString($0))},505684:$0=>{throw new TypeError(UTF8ToString($0))},505722:$0=>{throw new Error(UTF8ToString($0))},505756:$0=>{throw new Error(UTF8ToString($0))},505790:$0=>{throw new Error(UTF8ToString($0))},505824:$0=>{throw new Error(UTF8ToString($0))},505858:$0=>{throw new Error(UTF8ToString($0))},505892:$0=>{throw new Error(UTF8ToString($0))},505926:$0=>{throw new Error(UTF8ToString($0))},505960:$0=>{throw new Error(UTF8ToString($0))},505994:$0=>{throw new Error(UTF8ToString($0))},506028:$0=>{throw new Error(UTF8ToString($0))},506062:$0=>{throw new Error(UTF8ToString($0))},506096:$0=>{throw new TypeError(UTF8ToString($0))},506134:$0=>{throw new TypeError(UTF8ToString($0))},506172:$0=>{throw new RangeError(UTF8ToString($0))},506211:$0=>{throw new RangeError(UTF8ToString($0))},506250:$0=>{throw new RangeError(UTF8ToString($0))},506289:$0=>{throw new RangeError(UTF8ToString($0))},506328:$0=>{throw new Error(UTF8ToString($0))},506362:$0=>{throw new Error(UTF8ToString($0))},506396:$0=>{throw new Error(UTF8ToString($0))},506430:$0=>{throw new TypeError(UTF8ToString($0))},506468:$0=>{throw new TypeError(UTF8ToString($0))}};var _malloc,___getTypeName,_free,_emwgpuCreateBindGroup,_emwgpuCreateBindGroupLayout,_emwgpuCreateCommandBuffer,_emwgpuCreateCommandEncoder,_emwgpuCreateComputePassEncoder,_emwgpuCreateComputePipeline,_emwgpuCreatePipelineLayout,_emwgpuCreateQuerySet,_emwgpuCreateRenderBundle,_emwgpuCreateRenderBundleEncoder,_emwgpuCreateRenderPassEncoder,_emwgpuCreateRenderPipeline,_emwgpuCreateSampler,_emwgpuCreateSurface,_emwgpuCreateTexture,_emwgpuCreateTextureView,_emwgpuCreateAdapter,_emwgpuCreateBuffer,_emwgpuCreateDevice,_emwgpuCreateQueue,_emwgpuCreateShaderModule,_emwgpuOnDeviceLostCompleted,_emwgpuOnMapAsyncCompleted,_emwgpuOnRequestAdapterCompleted,_emwgpuOnRequestDeviceCompleted,_emwgpuOnUncapturedError,_emscripten_builtin_memalign,_memalign,_setThrew,__emscripten_tempret_set,__emscripten_stack_restore,__emscripten_stack_alloc,_emscripten_stack_get_current,___cxa_decrement_exception_refcount,___cxa_increment_exception_refcount,___get_exception_message,___cxa_can_catch,___cxa_get_exception_ptr,dynCall_viiiiii,dynCall_j,dynCall_vijiiiii,dynCall_v,dynCall_ii,dynCall_viii,dynCall_vii,dynCall_vi,dynCall_iii,dynCall_viiiii,dynCall_iiiii,dynCall_iiii,dynCall_viiii,dynCall_viiiiiii,dynCall_i,dynCall_viijii,dynCall_jijiii,dynCall_iiiij,dynCall_ji,dynCall_jiii,dynCall_iiiiii,dynCall_viijiii,dynCall_iji,dynCall_iiiiiiiiii,dynCall_viiiijiiii,dynCall_vij,dynCall_viiiiiijii,dynCall_iiiiiii,dynCall_viiiiiiiiii,dynCall_iiiiiiii,dynCall_diiiii,dynCall_fiii,dynCall_jiiiii,dynCall_viiiiiiiii,dynCall_viijji,dynCall_iijiii,dynCall_ff,dynCall_dd,dynCall_vji,dynCall_vfi,dynCall_vdi,dynCall_fi,dynCall_di,dynCall_id,dynCall_if,dynCall_jjjj,dynCall_vjiiiiii,dynCall_viji,dynCall_jiji,dynCall_iidiiii,dynCall_vijj,_asyncify_start_unwind,_asyncify_stop_unwind,_asyncify_start_rewind,_asyncify_stop_rewind,memory,__indirect_function_table,wasmMemory,wasmTable;function assignWasmExports(wasmExports){_malloc=wasmExports["wb"];___getTypeName=wasmExports["xb"];_free=wasmExports["yb"];_emwgpuCreateBindGroup=wasmExports["zb"];_emwgpuCreateBindGroupLayout=wasmExports["Ab"];_emwgpuCreateCommandBuffer=wasmExports["Bb"];_emwgpuCreateCommandEncoder=wasmExports["Cb"];_emwgpuCreateComputePassEncoder=wasmExports["Db"];_emwgpuCreateComputePipeline=wasmExports["Eb"];_emwgpuCreatePipelineLayout=wasmExports["Fb"];_emwgpuCreateQuerySet=wasmExports["Gb"];_emwgpuCreateRenderBundle=wasmExports["Hb"];_emwgpuCreateRenderBundleEncoder=wasmExports["Ib"];_emwgpuCreateRenderPassEncoder=wasmExports["Jb"];_emwgpuCreateRenderPipeline=wasmExports["Kb"];_emwgpuCreateSampler=wasmExports["Lb"];_emwgpuCreateSurface=wasmExports["Mb"];_emwgpuCreateTexture=wasmExports["Nb"];_emwgpuCreateTextureView=wasmExports["Ob"];_emwgpuCreateAdapter=wasmExports["Pb"];_emwgpuCreateBuffer=wasmExports["Qb"];_emwgpuCreateDevice=wasmExports["Rb"];_emwgpuCreateQueue=wasmExports["Sb"];_emwgpuCreateShaderModule=wasmExports["Tb"];_emwgpuOnDeviceLostCompleted=wasmExports["Ub"];_emwgpuOnMapAsyncCompleted=wasmExports["Vb"];_emwgpuOnRequestAdapterCompleted=wasmExports["Wb"];_emwgpuOnRequestDeviceCompleted=wasmExports["Xb"];_emwgpuOnUncapturedError=wasmExports["Yb"];_emscripten_builtin_memalign=wasmExports["Zb"];_memalign=wasmExports["_b"];_setThrew=wasmExports["$b"];__emscripten_tempret_set=wasmExports["ac"];__emscripten_stack_restore=wasmExports["bc"];__emscripten_stack_alloc=wasmExports["cc"];_emscripten_stack_get_current=wasmExports["dc"];___cxa_decrement_exception_refcount=wasmExports["ec"];___cxa_increment_exception_refcount=wasmExports["fc"];___get_exception_message=wasmExports["gc"];___cxa_can_catch=wasmExports["hc"];___cxa_get_exception_ptr=wasmExports["ic"];dynCall_viiiiii=dynCalls["viiiiii"]=wasmExports["jc"];dynCall_j=dynCalls["j"]=wasmExports["kc"];dynCall_vijiiiii=dynCalls["vijiiiii"]=wasmExports["lc"];dynCall_v=dynCalls["v"]=wasmExports["mc"];dynCall_ii=dynCalls["ii"]=wasmExports["nc"];dynCall_viii=dynCalls["viii"]=wasmExports["oc"];dynCall_vii=dynCalls["vii"]=wasmExports["pc"];dynCall_vi=dynCalls["vi"]=wasmExports["qc"];dynCall_iii=dynCalls["iii"]=wasmExports["rc"];dynCall_viiiii=dynCalls["viiiii"]=wasmExports["sc"];dynCall_iiiii=dynCalls["iiiii"]=wasmExports["tc"];dynCall_iiii=dynCalls["iiii"]=wasmExports["uc"];dynCall_viiii=dynCalls["viiii"]=wasmExports["vc"];dynCall_viiiiiii=dynCalls["viiiiiii"]=wasmExports["wc"];dynCall_i=dynCalls["i"]=wasmExports["xc"];dynCall_viijii=dynCalls["viijii"]=wasmExports["yc"];dynCall_jijiii=dynCalls["jijiii"]=wasmExports["zc"];dynCall_iiiij=dynCalls["iiiij"]=wasmExports["Ac"];dynCall_ji=dynCalls["ji"]=wasmExports["Bc"];dynCall_jiii=dynCalls["jiii"]=wasmExports["Cc"];dynCall_iiiiii=dynCalls["iiiiii"]=wasmExports["Dc"];dynCall_viijiii=dynCalls["viijiii"]=wasmExports["Ec"];dynCall_iji=dynCalls["iji"]=wasmExports["Fc"];dynCall_iiiiiiiiii=dynCalls["iiiiiiiiii"]=wasmExports["Gc"];dynCall_viiiijiiii=dynCalls["viiiijiiii"]=wasmExports["Hc"];dynCall_vij=dynCalls["vij"]=wasmExports["Ic"];dynCall_viiiiiijii=dynCalls["viiiiiijii"]=wasmExports["Jc"];dynCall_iiiiiii=dynCalls["iiiiiii"]=wasmExports["Kc"];dynCall_viiiiiiiiii=dynCalls["viiiiiiiiii"]=wasmExports["Lc"];dynCall_iiiiiiii=dynCalls["iiiiiiii"]=wasmExports["Mc"];dynCall_diiiii=dynCalls["diiiii"]=wasmExports["Nc"];dynCall_fiii=dynCalls["fiii"]=wasmExports["Oc"];dynCall_jiiiii=dynCalls["jiiiii"]=wasmExports["Pc"];dynCall_viiiiiiiii=dynCalls["viiiiiiiii"]=wasmExports["Qc"];dynCall_viijji=dynCalls["viijji"]=wasmExports["Rc"];dynCall_iijiii=dynCalls["iijiii"]=wasmExports["Sc"];dynCall_ff=dynCalls["ff"]=wasmExports["Tc"];dynCall_dd=dynCalls["dd"]=wasmExports["Uc"];dynCall_vji=dynCalls["vji"]=wasmExports["Vc"];dynCall_vfi=dynCalls["vfi"]=wasmExports["Wc"];dynCall_vdi=dynCalls["vdi"]=wasmExports["Xc"];dynCall_fi=dynCalls["fi"]=wasmExports["Yc"];dynCall_di=dynCalls["di"]=wasmExports["Zc"];dynCall_id=dynCalls["id"]=wasmExports["_c"];dynCall_if=dynCalls["if"]=wasmExports["$c"];dynCall_jjjj=dynCalls["jjjj"]=wasmExports["ad"];dynCall_vjiiiiii=dynCalls["vjiiiiii"]=wasmExports["bd"];dynCall_viji=dynCalls["viji"]=wasmExports["cd"];dynCall_jiji=dynCalls["jiji"]=wasmExports["dd"];dynCall_iidiiii=dynCalls["iidiiii"]=wasmExports["ed"];dynCall_vijj=dynCalls["vijj"]=wasmExports["fd"];_asyncify_start_unwind=wasmExports["gd"];_asyncify_stop_unwind=wasmExports["hd"];_asyncify_start_rewind=wasmExports["id"];_asyncify_stop_rewind=wasmExports["jd"];memory=wasmMemory=wasmExports["tb"];__indirect_function_table=wasmTable=wasmExports["vb"]}var wasmImports={s:___cxa_begin_catch,ma:___cxa_end_catch,a:___cxa_find_matching_catch_2,h:___cxa_find_matching_catch_3,Va:___cxa_rethrow,g:___cxa_throw,d:___resumeException,gb:___syscall_fstat64,fb:___syscall_newfstatat,cb:___syscall_openat,hb:__abort_js,Ma:__embind_finalize_value_object,ga:__embind_register_bigint,Aa:__embind_register_bool,na:__embind_register_class,C:__embind_register_class_class_function,O:__embind_register_class_function,aa:__embind_register_class_property,ya:__embind_register_emval,oa:__embind_register_enum,F:__embind_register_enum_value,fa:__embind_register_float,Oa:__embind_register_function,J:__embind_register_integer,x:__embind_register_memory_view,za:__embind_register_std_string,Z:__embind_register_std_wstring,la:__embind_register_value_object,Na:__embind_register_value_object_field,Ba:__embind_register_void,Ca:__emval_array_to_memory_view,z:__emval_create_invoker,Ka:__emval_decref,W:__emval_get_global,A:__emval_get_module_property,La:__emval_get_property,H:__emval_incref,Ia:__emval_instanceof,E:__emval_invoke,ha:__emval_invoke_i64,ja:__emval_is_number,Ja:__emval_is_string,_:__emval_new_array,M:__emval_new_cstring,Ga:__emval_new_u8string,Fa:__emval_run_destructors,Da:__emval_set_property,$:__emval_typeof,db:__mmap_js,eb:__munmap_js,Za:_clock_time_get,o:_emscripten_asm_const_int,$a:_emscripten_get_heap_max,mb:_emscripten_has_asyncify,_a:_emscripten_resize_heap,lb:_emwgpuAdapterRequestDevice,sb:_emwgpuBufferGetConstMappedRange,rb:_emwgpuBufferGetMappedRange,qb:_emwgpuBufferMapAsync,pb:_emwgpuBufferUnmap,r:_emwgpuDelete,kb:_emwgpuDeviceCreateBuffer,jb:_emwgpuDeviceCreateShaderModule,ob:_emwgpuDeviceDestroy,ib:_emwgpuInstanceRequestAdapter,nb:_emwgpuWaitAny,xa:_fd_close,bb:_fd_pread,ab:_fd_seek,wa:_fd_write,v:invoke_diiiii,ia:invoke_fiii,D:invoke_i,f:invoke_ii,b:invoke_iii,m:invoke_iiii,G:invoke_iiiii,y:invoke_iiiiii,Ha:invoke_iiiiiii,ka:invoke_iiiiiiii,qa:invoke_iiiiiiiiii,ba:invoke_iiiij,Qa:invoke_iji,t:invoke_j,K:invoke_ji,X:invoke_jiii,Ea:invoke_jiiiii,Ua:invoke_jijiii,j:invoke_v,c:invoke_vi,i:invoke_vii,e:invoke_viii,n:invoke_viiii,u:invoke_viiiii,L:invoke_viiiiii,l:invoke_viiiiiii,N:invoke_viiiiiiiii,B:invoke_viiiiiiiiii,pa:invoke_viiiiiijii,P:invoke_viiiijiiii,q:invoke_viijii,Ra:invoke_viijiii,Pa:invoke_vij,S:invoke_viji,w:invoke_vijiiiii,T:invoke_vijj,Y:_llvm_eh_typeid_for,Ya:_random_get,Ta:_wgpuAdapterGetLimits,Sa:_wgpuAdapterHasFeature,Xa:_wgpuBufferGetSize,va:_wgpuCommandEncoderBeginComputePass,I:_wgpuCommandEncoderCopyBufferToBuffer,da:_wgpuCommandEncoderFinish,sa:_wgpuComputePassEncoderDispatchWorkgroups,ra:_wgpuComputePassEncoderEnd,ta:_wgpuComputePassEncoderSetBindGroup,ua:_wgpuComputePassEncoderSetPipeline,Wa:_wgpuComputePipelineGetBindGroupLayout,U:_wgpuDeviceCreateBindGroup,R:_wgpuDeviceCreateBindGroupLayout,ea:_wgpuDeviceCreateCommandEncoder,V:_wgpuDeviceCreateComputePipeline,Q:_wgpuDeviceCreatePipelineLayout,k:_wgpuDeviceGetLimits,ca:_wgpuQueueSubmit,p:_wgpuQueueWriteBuffer};function invoke_viiiiii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{dynCall_viiiiii(index,a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_j(index){var sp=stackSave();try{return dynCall_j(index)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0);return 0n}}function invoke_vijiiiii(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{dynCall_vijiiiii(index,a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_v(index){var sp=stackSave();try{dynCall_v(index)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_ii(index,a1){var sp=stackSave();try{return dynCall_ii(index,a1)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viii(index,a1,a2,a3){var sp=stackSave();try{dynCall_viii(index,a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vii(index,a1,a2){var sp=stackSave();try{dynCall_vii(index,a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vi(index,a1){var sp=stackSave();try{dynCall_vi(index,a1)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iii(index,a1,a2){var sp=stackSave();try{return dynCall_iii(index,a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiii(index,a1,a2,a3){var sp=stackSave();try{return dynCall_iiii(index,a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{dynCall_viiiii(index,a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiii(index,a1,a2,a3,a4){var sp=stackSave();try{dynCall_viiii(index,a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vijj(index,a1,a2,a3){var sp=stackSave();try{dynCall_vijj(index,a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viji(index,a1,a2,a3){var sp=stackSave();try{dynCall_viji(index,a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiii(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{dynCall_viiiiiii(index,a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_i(index){var sp=stackSave();try{return dynCall_i(index)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viijii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{dynCall_viijii(index,a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_jijiii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return dynCall_jijiii(index,a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0);return 0n}}function invoke_iiiij(index,a1,a2,a3,a4){var sp=stackSave();try{return dynCall_iiiij(index,a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_ji(index,a1){var sp=stackSave();try{return dynCall_ji(index,a1)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0);return 0n}}function invoke_jiii(index,a1,a2,a3){var sp=stackSave();try{return dynCall_jiii(index,a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0);return 0n}}function invoke_iiiiii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return dynCall_iiiiii(index,a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiii(index,a1,a2,a3,a4){var sp=stackSave();try{return dynCall_iiiii(index,a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viijiii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{dynCall_viijiii(index,a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iji(index,a1,a2){var sp=stackSave();try{return dynCall_iji(index,a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9){var sp=stackSave();try{return dynCall_iiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiijiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9){var sp=stackSave();try{dynCall_viiiijiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vij(index,a1,a2){var sp=stackSave();try{dynCall_vij(index,a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiijii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9){var sp=stackSave();try{dynCall_viiiiiijii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10){var sp=stackSave();try{dynCall_viiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiii(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{return dynCall_iiiiiiii(index,a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{return dynCall_iiiiiii(index,a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_diiiii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return dynCall_diiiii(index,a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fiii(index,a1,a2,a3){var sp=stackSave();try{return dynCall_fiii(index,a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_jiiiii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return dynCall_jiiiii(index,a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0);return 0n}}function invoke_viiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9){var sp=stackSave();try{dynCall_viiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function applySignatureConversions(wasmExports){wasmExports=Object.assign({},wasmExports);var makeWrapper_pp=f=>a0=>f(a0)>>>0;var makeWrapper_ppp=f=>(a0,a1)=>f(a0,a1)>>>0;var makeWrapper_p=f=>()=>f()>>>0;wasmExports["wb"]=makeWrapper_pp(wasmExports["wb"]);wasmExports["xb"]=makeWrapper_pp(wasmExports["xb"]);wasmExports["Zb"]=makeWrapper_ppp(wasmExports["Zb"]);wasmExports["_b"]=makeWrapper_ppp(wasmExports["_b"]);wasmExports["cc"]=makeWrapper_pp(wasmExports["cc"]);wasmExports["dc"]=makeWrapper_p(wasmExports["dc"]);wasmExports["ic"]=makeWrapper_pp(wasmExports["ic"]);return wasmExports}function run(){if(runDependencies>0){dependenciesFulfilled=run;return}preRun();if(runDependencies>0){dependenciesFulfilled=run;return}function doRun(){Module["calledRun"]=true;if(ABORT)return;initRuntime();readyPromiseResolve?.(Module);Module["onRuntimeInitialized"]?.();postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout(()=>{setTimeout(()=>Module["setStatus"](""),1);doRun()},1)}else{doRun()}}var wasmExports;wasmExports=await (createWasm());run();if(runtimeInitialized){moduleRtn=Module}else{moduleRtn=new Promise((resolve,reject)=>{readyPromiseResolve=resolve;readyPromiseReject=reject})} +;return moduleRtn}})();if(typeof exports==="object"&&typeof module==="object"){module.exports=createExecuTorch;module.exports.default=createExecuTorch}else if(typeof define==="function"&&define["amd"])define([],()=>createExecuTorch); diff --git a/website/playground/webgpu/scripts/check-wiring.mjs b/website/playground/webgpu/scripts/check-wiring.mjs new file mode 100644 index 00000000000..ce7caba7018 --- /dev/null +++ b/website/playground/webgpu/scripts/check-wiring.mjs @@ -0,0 +1,168 @@ +// Static wiring check. Two classes of bug are decidable from source and easy to +// miss in review: an import that names something the target never exports (it +// takes down the whole module graph at load), and a class applied in JS that the +// stylesheet never styles (it renders unstyled while looking fine in the diff). +import { existsSync, readdirSync, readFileSync } from 'node:fs'; +import { dirname, join, relative, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const ENTRY = join(ROOT, 'src/main.js'); +const STYLESHEETS = readdirSync(join(ROOT, 'styles')).filter((f) => f.endsWith('.css')).map((f) => join(ROOT, 'styles', f)); +const HTML = [join(ROOT, 'index.html')]; + +// Modules that are intentionally not imported by the app yet, with the reason. +const UNREFERENCED_OK = { + 'src/loader/fetch-progress.js': 'download path for cleared models; no model resolves to a URL yet', +}; + +const stripComments = (src) => src.replace(/\/\*[\s\S]*?\*\//g, '').replace(/(^|[^:'"`\\])\/\/.*$/gm, '$1'); + +function parseImports(src) { + const out = []; + const re = /import\s+(?:([\s\S]*?)\s+from\s+)?['"]([^'"]+)['"]/g; + for (const m of src.matchAll(re)) { + const clause = m[1]?.trim() ?? ''; + const entry = { spec: m[2], named: [], namespace: null }; + const ns = clause.match(/\*\s+as\s+(\w+)/); + if (ns) entry.namespace = ns[1]; + const braces = clause.match(/\{([\s\S]*)\}/); + if (braces) { + for (const part of braces[1].split(',').map((s) => s.trim()).filter(Boolean)) { + const [name, alias] = part.split(/\s+as\s+/); + entry.named.push({ name: name.trim(), local: (alias ?? name).trim() }); + } + } + out.push(entry); + } + return out; +} + +function parseExports(src) { + const names = new Set(); + for (const m of src.matchAll(/export\s+(?:async\s+)?(?:function\*?|const|let|var|class)\s+(\w+)/g)) names.add(m[1]); + for (const m of src.matchAll(/export\s*\{([^}]*)\}/g)) { + for (const part of m[1].split(',').map((s) => s.trim()).filter(Boolean)) { + const pieces = part.split(/\s+as\s+/); + names.add((pieces[1] ?? pieces[0]).trim()); + } + } + return names; +} + +function walk(entry) { + const problems = []; + const seen = new Map(); + const queue = [entry]; + while (queue.length) { + const file = queue.shift(); + if (seen.has(file)) continue; + const src = stripComments(readFileSync(file, 'utf8')); + seen.set(file, src); + for (const imp of parseImports(src)) { + if (!imp.spec.startsWith('.')) continue; + const target = resolve(dirname(file), imp.spec); + const rel = relative(ROOT, file); + if (!existsSync(target)) { + problems.push(`${rel}: imports missing file ${imp.spec}`); + continue; + } + const exports = parseExports(stripComments(readFileSync(target, 'utf8'))); + for (const { name } of imp.named) { + if (!exports.has(name)) problems.push(`${rel}: imports { ${name} } but ${relative(ROOT, target)} does not export it`); + } + if (imp.namespace) { + for (const m of src.matchAll(new RegExp(`\\b${imp.namespace}\\.(\\w+)`, 'g'))) { + if (!exports.has(m[1])) problems.push(`${rel}: uses ${imp.namespace}.${m[1]} but ${relative(ROOT, target)} does not export it`); + } + } + queue.push(target); + } + } + return { problems, modules: seen }; +} + +function listFiles(dir, ext) { + return readdirSync(dir, { withFileTypes: true }).flatMap((d) => { + const p = join(dir, d.name); + return d.isDirectory() ? listFiles(p, ext) : p.endsWith(ext) ? [p] : []; + }); +} + +const quoted = /'([^'\\]*)'|"([^"\\]*)"|`([^`\\]*)`/g; + +// Returns { static: Set, prefixes: Set } for classes applied in JS/HTML. +function appliedClasses(sources) { + const found = new Map(); + const prefixes = new Map(); + const add = (cls, where) => { + if (!cls || /[^\w-]/.test(cls)) return; + if (!found.has(cls)) found.set(cls, where); + }; + const addValue = (value, where) => { + for (const token of value.split(/\s+/)) { + if (!token) continue; + const dyn = token.indexOf('${'); + if (dyn === 0) continue; + if (dyn > 0) prefixes.set(token.slice(0, dyn), where); + else add(token, where); + } + }; + // In `cond ? 'a' : 'b'`, only the branches are classes; strings in the condition are not. + const branches = (expr) => (expr.includes('?') ? expr.slice(expr.indexOf('?') + 1) : expr); + for (const [file, src] of sources) { + const where = relative(ROOT, file); + for (const m of src.matchAll(/\bclass:\s*([^,}\n]+)/g)) { + for (const q of branches(m[1]).matchAll(quoted)) addValue(q[1] ?? q[2] ?? q[3], where); + } + for (const m of src.matchAll(/classList\.(?:add|remove|toggle|contains)\(([^)]*)\)/g)) { + const firstArg = m[1].split(/,(?=\s*[^'"`\s])/)[0]; + for (const q of branches(firstArg).matchAll(quoted)) addValue(q[1] ?? q[2] ?? q[3], where); + } + for (const m of src.matchAll(/class="([^"]*)"/g)) addValue(m[1], where); + } + return { found, prefixes }; +} + +function definedClasses(cssFiles) { + const set = new Set(); + for (const f of cssFiles) { + const css = readFileSync(f, 'utf8') + .replace(/\/\*[\s\S]*?\*\//g, '') + .replace(/url\([^)]*\)/g, '') + .replace(/"[^"]*"/g, ''); + for (const m of css.matchAll(/\.(-?[_a-zA-Z][\w-]*)/g)) set.add(m[1]); + } + return set; +} + +export function checkWiring() { + const { problems, modules } = walk(ENTRY); + + for (const file of listFiles(join(ROOT, 'src'), '.js')) { + const rel = relative(ROOT, file); + if (!modules.has(file) && !UNREFERENCED_OK[rel]) problems.push(`${rel}: not reachable from src/main.js (dead module or missing import)`); + } + + const sources = [...modules].concat(HTML.map((f) => [f, readFileSync(f, 'utf8')])); + const { found, prefixes } = appliedClasses(sources); + const defined = definedClasses(STYLESHEETS); + for (const [cls, where] of found) { + if (!defined.has(cls)) problems.push(`${where}: applies class "${cls}" but no stylesheet rule mentions .${cls}`); + } + for (const [prefix, where] of prefixes) { + if (![...defined].some((c) => c.startsWith(prefix))) problems.push(`${where}: builds classes "${prefix}…" but no stylesheet rule starts with .${prefix}`); + } + return { problems, modules: modules.size, classes: found.size }; +} + +if (process.argv[1] === fileURLToPath(import.meta.url)) { + const { problems, modules, classes } = checkWiring(); + console.log(`checked ${modules} modules and ${classes} applied classes`); + for (const p of problems) console.log(` ✗ ${p}`); + if (problems.length) { + console.log(`${problems.length} wiring problem(s)`); + process.exit(1); + } + console.log('wiring OK'); +} diff --git a/website/playground/webgpu/scripts/serve.mjs b/website/playground/webgpu/scripts/serve.mjs new file mode 100644 index 00000000000..fbd87a95ef3 --- /dev/null +++ b/website/playground/webgpu/scripts/serve.mjs @@ -0,0 +1,170 @@ +// Zero-dependency static server for local review. Binds every interface so the +// no-WebGPU fallback can be exercised over a LAN IP (an insecure context). +// +// Local-assets dev mode (`--dev-assets --dev-config `) additionally +// serves model files from a directory outside this tree, so real models can be +// tried before any checkpoint is cleared for hosting. It binds loopback only and +// is never part of the deployed site: production has no /__dev__ routes. +// `--share --tls-cert --tls-key ` adds an HTTPS listener on every +// interface (port --share-port, default 5174) for an authenticated HTTPS proxy in +// front of this machine, and marks pages served there so they use the dev config +// on that proxy's hostname too. Plain HTTP stays on loopback. +import { createReadStream } from 'node:fs'; +import { readdir, readFile, stat } from 'node:fs/promises'; +import { createServer } from 'node:http'; +import { createServer as createTlsServer } from 'node:https'; +import { networkInterfaces } from 'node:os'; +import { extname, join, normalize, resolve, sep } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const ROOT = resolve(fileURLToPath(new URL('..', import.meta.url))); +const SITE_ROOT = resolve(ROOT, '..', '..'); +const args = parseArgs(process.argv.slice(2)); +const PORT = Number(args.port ?? process.env.PORT) || 5173; +const DEV_ASSETS = args['dev-assets'] ? resolve(args['dev-assets']) : null; +const DEV_CONFIG = args['dev-config'] ? resolve(args['dev-config']) : null; +const DEV = Boolean(DEV_ASSETS && DEV_CONFIG); +const SHARE = DEV && args.share === 'true'; +const SHARE_PORT = Number(args['share-port']) || 5174; +const HOST = DEV ? '127.0.0.1' : '0.0.0.0'; +const SHARE_MARK = ''; +if (SHARE && !(args['tls-cert'] && args['tls-key'])) { + console.error('--share needs --tls-cert and --tls-key: the proxy only talks to HTTPS backends'); + process.exit(1); +} + +const TYPES = { + '.html': 'text/html; charset=utf-8', + '.js': 'text/javascript; charset=utf-8', + '.mjs': 'text/javascript; charset=utf-8', + '.css': 'text/css; charset=utf-8', + '.svg': 'image/svg+xml', + '.json': 'application/json', + '.png': 'image/png', + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.webp': 'image/webp', + '.ico': 'image/x-icon', + '.wasm': 'application/wasm', + '.txt': 'text/plain; charset=utf-8', + '.wav': 'audio/wav', + '.mp3': 'audio/mpeg', + '.mp4': 'video/mp4', + '.webm': 'video/webm', +}; + +function parseArgs(argv) { + const out = {}; + for (let i = 0; i < argv.length; i++) { + const m = /^--([\w-]+)(?:=(.*))?$/.exec(argv[i]); + if (!m) continue; + out[m[1]] = m[2] ?? (argv[i + 1] && !argv[i + 1].startsWith('--') ? argv[++i] : 'true'); + } + return out; +} + +// --dev-config is a JSON file, or a directory of JSON fragments merged by key. +async function readDevConfig() { + const files = (await stat(DEV_CONFIG)).isDirectory() + ? (await readdir(DEV_CONFIG)).filter((f) => f.endsWith('.json')).sort().map((f) => join(DEV_CONFIG, f)) + : [DEV_CONFIG]; + const merged = { models: {}, shared: {} }; + for (const f of files) { + const c = JSON.parse(await readFile(f, 'utf8')); + Object.assign(merged.models, c.models); + Object.assign(merged.shared, c.shared); + } + return merged; +} + +function inside(base, path) { + const file = normalize(join(base, path)); + return file === base || file.startsWith(base + sep) ? file : null; +} + +async function sendFile(req, res, file, cacheControl, shared) { + let info = await stat(file); + if (info.isDirectory()) { + file = join(file, 'index.html'); + info = await stat(file); + } + if (shared && extname(file) === '.html') { + const html = (await readFile(file, 'utf8')).replace('', `\n ${SHARE_MARK}`); + res.writeHead(200, { 'content-type': TYPES['.html'], 'cache-control': cacheControl }); + return res.end(req.method === 'HEAD' ? undefined : html); + } + res.writeHead(200, { + 'content-type': TYPES[extname(file).toLowerCase()] ?? 'application/octet-stream', + 'content-length': String(info.size), + 'cache-control': cacheControl, + }); + if (req.method === 'HEAD') return res.end(); + createReadStream(file).pipe(res); +} + +function requestPath(url) { + try { + return decodeURIComponent(new URL(url, 'http://localhost/').pathname); + } catch { + return null; + } +} + +const handler = (shared) => async (req, res) => { + const path = requestPath(req.url); + if (path == null) return res.writeHead(400).end(); + try { + if (DEV && path === '/__dev__/config.json') { + const body = JSON.stringify({ ...(await readDevConfig()), base: '/__dev_assets__/' }); + res.writeHead(200, { 'content-type': 'application/json', 'cache-control': 'no-store' }); + return res.end(body); + } + if (DEV && path.startsWith('/__dev_assets__/')) { + const file = inside(DEV_ASSETS, path.slice('/__dev_assets__/'.length)); + if (!file) return res.writeHead(403).end(); + return await sendFile(req, res, file, 'no-cache', shared); + } + // The whole site is served as deployed; / opens the playground hub. + if (path === '/') { + res.writeHead(302, { location: '/playground/' }); + return res.end(); + } + const file = inside(SITE_ROOT, path); + if (!file) return res.writeHead(403).end(); + await sendFile(req, res, file, 'no-store', shared); + } catch { + if (!res.headersSent) res.writeHead(404, { 'content-type': 'text/plain' }).end('not found'); + } +}; + +if (SHARE) { + const readTls = async () => ({ cert: await readFile(args['tls-cert']), key: await readFile(args['tls-key']) }); + const tlsServer = createTlsServer(await readTls(), handler(true)); + // Host certificates rotate under a running server; pick up the new pair hourly. + setInterval(async () => { + try { + tlsServer.setSecureContext(await readTls()); + } catch (err) { + console.error(`could not reload TLS certificate: ${err.message}`); + } + }, 60 * 60 * 1000).unref(); + tlsServer.listen(SHARE_PORT, '::', () => { + console.log(`shared over HTTPS on port ${SHARE_PORT} (all interfaces) for an authenticated proxy`); + }); +} + +// A malformed request must never take the server down for everyone else. +process.on('uncaughtException', (err) => console.error(`request error: ${err.message}`)); + +createServer(handler(false)).listen(PORT, HOST, () => { + console.log(`secure context http://localhost:${PORT}/playground/ (site home: http://localhost:${PORT}/index.html)`); + if (DEV) { + console.log('local-assets dev mode: loopback only; model files are served from outside the site tree'); + return; + } + for (const addrs of Object.values(networkInterfaces())) { + for (const a of addrs ?? []) { + if (a.family === 'IPv4' && !a.internal) console.log(`insecure (LAN) http://${a.address}:${PORT}/playground/`); + } + } +}); diff --git a/website/playground/webgpu/src/audio/decode.js b/website/playground/webgpu/src/audio/decode.js new file mode 100644 index 00000000000..42faee6ff78 --- /dev/null +++ b/website/playground/webgpu/src/audio/decode.js @@ -0,0 +1,22 @@ +// Decodes an uploaded audio file in the browser's own decoder, straight to +// 16 kHz (decodeAudioData resamples to the context's rate). +import { downmix, SPEECH_RATE } from './pcm.js'; + +export const MAX_FILE_SECONDS = 600; + +export async function decodeAudioFile(fileOrBuffer) { + const bytes = fileOrBuffer instanceof ArrayBuffer ? fileOrBuffer : await fileOrBuffer.arrayBuffer(); + const Ctx = globalThis.OfflineAudioContext ?? globalThis.webkitOfflineAudioContext; + if (!Ctx) throw new Error('This browser cannot decode audio files.'); + let buffer; + try { + buffer = await new Ctx(1, 1, SPEECH_RATE).decodeAudioData(bytes); + } catch { + throw new Error('This file could not be decoded. Try WAV, MP3, M4A, OGG or WebM audio.'); + } + if (buffer.duration > MAX_FILE_SECONDS) { + throw new Error(`This clip is ${Math.round(buffer.duration / 60)} minutes long; the demo transcribes up to ${MAX_FILE_SECONDS / 60} minutes.`); + } + const channels = Array.from({ length: buffer.numberOfChannels }, (_, i) => buffer.getChannelData(i)); + return downmix(channels); +} diff --git a/website/playground/webgpu/src/audio/mic.js b/website/playground/webgpu/src/audio/mic.js new file mode 100644 index 00000000000..a8beffdd791 --- /dev/null +++ b/website/playground/webgpu/src/audio/mic.js @@ -0,0 +1,154 @@ +// Microphone capture to raw PCM (no MediaRecorder, so nothing is encoded and +// decoded again), resampled to 16 kHz mono when stopped. +import { concat, resample, SPEECH_RATE } from './pcm.js'; + +export class MicError extends Error { + // kind: 'unsupported' | 'insecure' | 'denied' | 'no-device' | 'busy' | 'failed' + constructor(kind, message) { + super(message); + this.name = 'MicError'; + this.kind = kind; + } +} + +function toMicError(err) { + switch (err?.name) { + case 'NotAllowedError': + case 'SecurityError': + return new MicError('denied', 'Microphone access was blocked. Allow it in your browser’s site settings, or upload a file instead.'); + case 'NotFoundError': + case 'OverconstrainedError': + return new MicError('no-device', 'No microphone was found. Connect one, or upload a file instead.'); + case 'NotReadableError': + case 'AbortError': + return new MicError('busy', 'The microphone is in use by another app or could not be started.'); + default: + return new MicError('failed', `Could not start the microphone: ${err?.message ?? err}`); + } +} + +// Batches 128-frame render quanta into ~40 ms messages. +const TAP_SOURCE = ` +class Tap extends AudioWorkletProcessor { + constructor() { super(); this.buf = new Float32Array(2048); this.n = 0; } + process(inputs) { + const ch = inputs[0] && inputs[0][0]; + if (ch) { + if (this.n + ch.length > this.buf.length) { this.port.postMessage(this.buf.slice(0, this.n)); this.n = 0; } + this.buf.set(ch, this.n); this.n += ch.length; + } + return true; + } +} +registerProcessor('pcm-tap', Tap); +`; + +async function tapNode(ctx, onChunk) { + if (ctx.audioWorklet && typeof AudioWorkletNode === 'function') { + const url = URL.createObjectURL(new Blob([TAP_SOURCE], { type: 'text/javascript' })); + try { + await ctx.audioWorklet.addModule(url); + } finally { + URL.revokeObjectURL(url); + } + const node = new AudioWorkletNode(ctx, 'pcm-tap', { numberOfInputs: 1, numberOfOutputs: 1, outputChannelCount: [1] }); + node.port.onmessage = (e) => onChunk(e.data); + return node; + } + const node = ctx.createScriptProcessor(2048, 1, 1); + node.onaudioprocess = (e) => onChunk(Float32Array.from(e.inputBuffer.getChannelData(0))); + return node; +} + +export function micSupport() { + if (typeof window !== 'undefined' && !window.isSecureContext) return 'insecure'; + if (!globalThis.navigator?.mediaDevices?.getUserMedia) return 'unsupported'; + return 'ok'; +} + +/** + * Starts recording from the default microphone. + * onLevel({ rms, peak, seconds }) fires ~25×/s. Recording stops by itself at + * `maxSeconds` (onAutoStop is then called; stop() still returns the audio). + * Returns { stop(): Promise (16 kHz mono), cancel(), seconds }. + * Throws MicError. + */ +export async function recordMic({ onLevel, maxSeconds = 300, onAutoStop } = {}) { + const support = micSupport(); + if (support === 'insecure') throw new MicError('insecure', 'The microphone needs a secure page (https or localhost).'); + if (support === 'unsupported') throw new MicError('unsupported', 'This browser does not expose a microphone API.'); + + let stream; + try { + stream = await navigator.mediaDevices.getUserMedia({ + audio: { channelCount: 1, echoCancellation: true, noiseSuppression: true, autoGainControl: true }, + }); + } catch (err) { + throw toMicError(err); + } + + const ctx = new AudioContext(); + const chunks = []; + let frames = 0; + let capturing = true; + let release = null; + const done = new Promise((r) => (release = r)); + + const teardown = async () => { + capturing = false; + for (const t of stream.getTracks()) t.stop(); + try { + await ctx.close(); + } catch { + // Already closed. + } + release(); + }; + + try { + const node = await tapNode(ctx, (chunk) => { + if (!capturing) return; + chunks.push(chunk); + frames += chunk.length; + let s = 0; + let p = 0; + for (let i = 0; i < chunk.length; i++) { + const x = chunk[i]; + s += x * x; + if (Math.abs(x) > p) p = Math.abs(x); + } + const seconds = frames / ctx.sampleRate; + onLevel?.({ rms: Math.sqrt(s / chunk.length), peak: p, seconds }); + if (seconds >= maxSeconds) { + capturing = false; + onAutoStop?.(); + } + }); + ctx.createMediaStreamSource(stream).connect(node); + // The node writes no output; the connection only keeps it scheduled. + node.connect(ctx.destination); + await ctx.resume(); + } catch (err) { + await teardown(); + throw new MicError('failed', `Could not start audio capture: ${err.message}`); + } + + let stopped = null; + return { + get seconds() { + return frames / ctx.sampleRate; + }, + stop() { + stopped ??= (async () => { + const rate = ctx.sampleRate; + await teardown(); + return resample(concat(chunks), rate, SPEECH_RATE); + })(); + return stopped; + }, + cancel() { + if (!stopped) stopped = teardown().then(() => new Float32Array(0)); + return done; + }, + }; +} diff --git a/website/playground/webgpu/src/audio/pcm.js b/website/playground/webgpu/src/audio/pcm.js new file mode 100644 index 00000000000..928e68b2a8c --- /dev/null +++ b/website/playground/webgpu/src/audio/pcm.js @@ -0,0 +1,127 @@ +// Pure PCM helpers (no DOM), shared by the mic, file decoding and the +// recognizer. Everything downstream of capture is 16 kHz mono Float32. +export const SPEECH_RATE = 16000; + +// Band-limited resampling: Hann-windowed sinc, normalised to unity DC gain. +// When downsampling the cutoff tracks the target Nyquist so nothing aliases. +export function resample(input, fromRate, toRate = SPEECH_RATE) { + if (fromRate === toRate) return Float32Array.from(input); + const ratio = fromRate / toRate; + const out = new Float32Array(Math.floor(input.length / ratio)); + const cutoff = Math.min(1, 1 / ratio) * 0.94; + const half = Math.ceil(8 * Math.max(1, ratio)); + const last = input.length - 1; + for (let i = 0; i < out.length; i++) { + const center = i * ratio; + const lo = Math.max(0, Math.ceil(center - half)); + const hi = Math.min(last, Math.floor(center + half)); + let acc = 0; + let wsum = 0; + for (let j = lo; j <= hi; j++) { + const x = j - center; + const t = Math.PI * cutoff * x; + const k = (x === 0 ? 1 : Math.sin(t) / t) * (0.5 + 0.5 * Math.cos((Math.PI * x) / half)); + acc += input[j] * k; + wsum += k; + } + out[i] = wsum ? acc / wsum : 0; + } + return out; +} + +export function downmix(channels) { + if (channels.length === 1) return Float32Array.from(channels[0]); + const out = new Float32Array(channels[0].length); + for (const ch of channels) for (let i = 0; i < out.length; i++) out[i] += ch[i]; + for (let i = 0; i < out.length; i++) out[i] /= channels.length; + return out; +} + +export function concat(chunks) { + const out = new Float32Array(chunks.reduce((n, c) => n + c.length, 0)); + let o = 0; + for (const c of chunks) { + out.set(c, o); + o += c.length; + } + return out; +} + +export function rms(samples, start = 0, end = samples.length) { + let s = 0; + for (let i = start; i < end; i++) s += samples[i] * samples[i]; + return end > start ? Math.sqrt(s / (end - start)) : 0; +} + +// Splits audio into windows of at most `maxSec`, cutting at the quietest +// `frameMs` frame in the last stretch of each window so words stay whole. +export function planWindows(samples, { rate = SPEECH_RATE, maxSec = 15, minFrac = 0.6, frameMs = 40 } = {}) { + const length = samples.length; + const max = Math.round(maxSec * rate); + const min = Math.round(max * minFrac); + const frame = Math.max(1, Math.round((frameMs / 1000) * rate)); + const out = []; + let pos = 0; + while (length - pos > max) { + let cut = pos + max; + let best = Infinity; + for (let f = pos + min; f + frame <= pos + max; f += frame) { + const e = rms(samples, f, f + frame); + if (e < best) { + best = e; + cut = f + (frame >> 1); + } + } + out.push({ start: pos, end: cut }); + pos = cut; + } + if (length > pos) out.push({ start: pos, end: length }); + return out; +} + +// Peak envelope in `buckets` columns, for drawing a waveform. +export function peaks(samples, buckets) { + const out = new Float32Array(buckets); + const step = samples.length / buckets; + for (let b = 0; b < buckets; b++) { + const s = Math.floor(b * step); + const e = Math.max(s + 1, Math.floor((b + 1) * step)); + let p = 0; + for (let i = s; i < e && i < samples.length; i++) p = Math.max(p, Math.abs(samples[i])); + out[b] = p; + } + return out; +} + +// 16-bit PCM WAV, for playing a clip back. +export function encodeWav(samples, rate = SPEECH_RATE) { + const buf = new ArrayBuffer(44 + samples.length * 2); + const v = new DataView(buf); + const str = (o, s) => [...s].forEach((c, i) => v.setUint8(o + i, c.charCodeAt(0))); + str(0, 'RIFF'); + v.setUint32(4, 36 + samples.length * 2, true); + str(8, 'WAVE'); + str(12, 'fmt '); + v.setUint32(16, 16, true); + v.setUint16(20, 1, true); + v.setUint16(22, 1, true); + v.setUint32(24, rate, true); + v.setUint32(28, rate * 2, true); + v.setUint16(32, 2, true); + v.setUint16(34, 16, true); + str(36, 'data'); + v.setUint32(40, samples.length * 2, true); + for (let i = 0; i < samples.length; i++) { + const x = Math.max(-1, Math.min(1, samples[i])); + v.setInt16(44 + i * 2, x < 0 ? x * 0x8000 : x * 0x7fff, true); + } + return new Uint8Array(buf); +} + +export function formatClock(seconds, { tenths = false } = {}) { + const s = Math.max(0, seconds); + const m = Math.floor(s / 60); + const rest = s - m * 60; + const sec = tenths ? rest.toFixed(1).padStart(4, '0') : String(Math.floor(rest)).padStart(2, '0'); + return `${String(m).padStart(2, '0')}:${sec}`; +} diff --git a/website/playground/webgpu/src/audio/waveform.js b/website/playground/webgpu/src/audio/waveform.js new file mode 100644 index 00000000000..a4e8162f23f --- /dev/null +++ b/website/playground/webgpu/src/audio/waveform.js @@ -0,0 +1,108 @@ +// Canvas waveform: a scrolling level meter while recording, or the envelope of +// a finished clip. Colours come from the theme tokens at draw time. +import { peaks } from './pcm.js'; + +const BAR = 3; +const GAP = 3; + +export function createWaveform(canvas) { + const levels = []; + let clip = null; + let progress = -1; + let raf = 0; + + const size = () => { + const dpr = Math.min(2, globalThis.devicePixelRatio || 1); + const w = Math.max(1, Math.round(canvas.clientWidth * dpr)); + const hgt = Math.max(1, Math.round(canvas.clientHeight * dpr)); + if (canvas.width !== w || canvas.height !== hgt) { + canvas.width = w; + canvas.height = hgt; + } + return dpr; + }; + + const colors = () => { + const cs = getComputedStyle(canvas); + return { live: cs.getPropertyValue('--accent').trim() || '#e2502a', idle: cs.getPropertyValue('--text-3').trim() || '#888' }; + }; + + function draw() { + raf = 0; + const dpr = size(); + const ctx = canvas.getContext('2d'); + const { width: W, height: H } = canvas; + ctx.clearRect(0, 0, W, H); + const c = colors(); + const step = (BAR + GAP) * dpr; + const n = Math.max(1, Math.floor(W / step)); + const mid = H / 2; + const bar = (i, amp, color, alpha = 1) => { + const hh = Math.max(BAR * dpr, amp * (H - 4 * dpr)); + ctx.globalAlpha = alpha; + ctx.fillStyle = color; + ctx.beginPath(); + ctx.roundRect(i * step + GAP * dpr * 0.5, mid - hh / 2, BAR * dpr, hh, BAR * dpr); + ctx.fill(); + }; + if (clip) { + const env = peaks(clip, n); + let max = 0; + for (const p of env) max = Math.max(max, p); + const scale = max > 0 ? 1 / max : 0; + for (let i = 0; i < n; i++) { + const done = progress >= 0 && i / n <= progress; + bar(i, Math.sqrt(env[i] * scale), done ? c.live : c.idle, done ? 1 : 0.55); + } + ctx.globalAlpha = 1; + return; + } + const first = Math.max(0, levels.length - n); + const offset = n - (levels.length - first); + for (let i = 0; i < n; i++) { + const k = i - offset; + if (k < 0) bar(i, 0, c.idle, 0.35); + else bar(i, Math.min(1, Math.sqrt(levels[first + k]) * 1.6), c.live); + } + ctx.globalAlpha = 1; + } + + const schedule = () => { + if (!raf) raf = requestAnimationFrame(draw); + }; + const ro = typeof ResizeObserver === 'function' ? new ResizeObserver(schedule) : null; + ro?.observe(canvas); + const themeWatch = typeof MutationObserver === 'function' ? new MutationObserver(schedule) : null; + themeWatch?.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] }); + schedule(); + + return { + push(level) { + clip = null; + levels.push(level); + if (levels.length > 2000) levels.splice(0, 1000); + schedule(); + }, + showClip(samples) { + clip = samples; + progress = -1; + levels.length = 0; + schedule(); + }, + setProgress(f) { + progress = f; + schedule(); + }, + clear() { + clip = null; + levels.length = 0; + schedule(); + }, + redraw: schedule, + dispose() { + ro?.disconnect(); + themeWatch?.disconnect(); + cancelAnimationFrame(raf); + }, + }; +} diff --git a/website/playground/webgpu/src/config/catalog.js b/website/playground/webgpu/src/config/catalog.js new file mode 100644 index 00000000000..76ed9e59e36 --- /dev/null +++ b/website/playground/webgpu/src/config/catalog.js @@ -0,0 +1,150 @@ +// The catalog table. Tiles, routes, and compute badges are all generated from +// this one list, so a badge can never disagree with what a demo actually does. +import { getModel, isPublished } from './models.js'; + +export const COMPUTE = Object.freeze({ + EXECUTORCH: 'executorch', +}); + +export const COMPUTE_BADGES = Object.freeze({ + [COMPUTE.EXECUTORCH]: { + label: 'ExecuTorch · real model', + short: 'ExecuTorch', + description: 'A real ExecuTorch .pte runs on the WebGPU backend in this tab. Nothing is sent to a server.', + }, +}); + +export const CATEGORIES = [ + { id: 'language', label: 'Language', blurb: 'Chat with a model running on your GPU.' }, + { id: 'vision', label: 'Vision', blurb: 'Pixels in, structure out. Works on your camera, live.' }, + { id: 'generative', label: 'Generative', blurb: 'Images made and remade in the tab.' }, + { id: 'audio', label: 'Audio', blurb: 'Speech to text, without a server.' }, +]; + +export const DEMOS = [ + { + id: 'chat', + category: 'language', + title: 'On-device Chat', + blurb: 'Chat with a real language model running entirely on your GPU. Talk to it, too.', + meta: { label: 'Models', value: 'Qwen 3 0.6B, Llama 3.2 1B' }, + models: ['qwen3-0.6b', 'llama-3.2-1b-instruct'], + compute: COMPUTE.EXECUTORCH, + view: 'chat', + art: 'chat', + }, + { + id: 'effects', + category: 'vision', + title: 'Camera Effects', + blurb: 'Your camera, six ways at once: portrait blur, pixelated or swapped backgrounds, neon scanlines, color pop.', + meta: { label: 'Model', value: 'MODNet person segmentation · live camera' }, + models: ['modnet'], + compute: COMPUTE.EXECUTORCH, + view: 'effects', + art: 'effects', + featured: true, + }, + { + id: 'segment', + category: 'vision', + title: 'Segment Anything', + blurb: 'Click any object and get a pixel-accurate mask back in milliseconds.', + meta: { label: 'Model', value: 'SAM 2.1 Hiera Tiny' }, + models: ['sam2-hiera-tiny'], + compute: COMPUTE.EXECUTORCH, + view: 'segment', + art: 'segment', + }, + { + id: 'vlm', + unlisted: 'The public WebGPU runtime has no 8-bit linear kernel yet, and the 4-bit export loses too much answer quality.', + category: 'vision', + title: 'VLM Q&A', + blurb: 'Show it a picture and ask about it. The image never leaves your device.', + meta: { label: 'Model', value: 'SmolVLM-500M · photo or camera' }, + models: ['smolvlm-500m-instruct'], + compute: COMPUTE.EXECUTORCH, + view: 'vlm', + art: 'vlm', + }, + { + id: 'depth', + category: 'vision', + title: 'Depth Estimation', + blurb: 'See the third dimension: a depth map for any photo, or your camera in real time.', + meta: { label: 'Model', value: 'Depth Anything V2 Small · photo or camera' }, + models: ['depth-anything-v2-small'], + compute: COMPUTE.EXECUTORCH, + view: 'vision', + task: 'depth', + art: 'depth', + }, + { + id: 'detect', + category: 'vision', + title: 'Object Detection', + blurb: 'Boxes and labels for 80 everyday objects, on a photo or a live camera feed.', + meta: { label: 'Model', value: 'D-FINE S · photo, video, or camera' }, + models: ['dfine-small'], + compute: COMPUTE.EXECUTORCH, + view: 'vision', + task: 'detect', + art: 'detect', + }, + { + id: 'bgremove', + category: 'generative', + title: 'Background Removal', + blurb: 'Cut people out of a photo, or swap your camera background, without a green screen.', + meta: { label: 'Model', value: 'MODNet · photo or camera' }, + models: ['modnet'], + compute: COMPUTE.EXECUTORCH, + view: 'vision', + task: 'bgremove', + art: 'bgremove', + }, + { + id: 'superres', + category: 'generative', + title: 'Super-Resolution', + blurb: 'Upscale a small image 4× and recover detail, with a before-and-after slider.', + meta: { label: 'Model', value: 'Real-ESRGAN ×4' }, + models: ['real-esrgan'], + compute: COMPUTE.EXECUTORCH, + view: 'vision', + task: 'superres', + art: 'superres', + }, + { + id: 'speech-to-text', + category: 'audio', + title: 'Speech to Text', + blurb: 'Record your voice or drop in an audio file and get a transcript, decoded entirely in the tab.', + meta: { label: 'Model', value: 'Whisper base' }, + models: ['whisper-base'], + compute: COMPUTE.EXECUTORCH, + view: 'stt', + art: 'stt', + }, +]; + +// Visitors see a demo only when it is listed and every model it needs is published. +export const LISTED_DEMOS = DEMOS.filter((d) => !d.unlisted && d.models.every((id) => isPublished(getModel(id)))); +export const LISTED_CATEGORIES = CATEGORIES.filter((c) => LISTED_DEMOS.some((d) => d.category === c.id)); + +const BY_ID = new Map(LISTED_DEMOS.map((d) => [d.id, d])); + +export function getDemo(id) { + return BY_ID.get(id) ?? null; +} + +export function computeBadge(demo) { + const badge = COMPUTE_BADGES[demo.compute]; + if (!badge) throw new Error(`Demo ${demo.id} has unknown compute kind: ${demo.compute}`); + return badge; +} + +export function demosIn(categoryId) { + return LISTED_DEMOS.filter((d) => d.category === categoryId); +} diff --git a/website/playground/webgpu/src/config/models.js b/website/playground/webgpu/src/config/models.js new file mode 100644 index 00000000000..0f3bb7d5b23 --- /dev/null +++ b/website/playground/webgpu/src/config/models.js @@ -0,0 +1,468 @@ +// The ONE place model checkpoints are referenced. +// +// Cleared entries point at their Hugging Face repo pinned to one commit, with a +// sha256 per file; the rest ship with `assets.baseUrl: null` and `license.status: 'todo'`. +// The resolver (src/loader/resolve.js) refuses remote weights unless BOTH the +// license is cleared AND a URL is populated, so filling in a URL alone never +// implies legal review happened. To enable a model after review, set +// `license.status: 'cleared'`, record the decision in `license.reviewNote`, +// and point `assets.baseUrl` at the approved repo. + +export const LICENSE_STATUS = Object.freeze({ + TODO: 'todo', + CLEARED: 'cleared', + BLOCKED: 'blocked', +}); + +export const MODELS = [ + { + id: 'llama-3.2-1b-instruct', + name: 'Llama 3.2 1B Instruct', + shortName: 'Llama 3.2 1B', + task: 'text-generation', + tagline: 'Larger · Built with Llama', + approxBytes: 744e6, + approxBytesIsEstimate: false, + arch: { layers: 16, dModel: 2048, ffn: 8192, heads: 32, kvHeads: 8, vocab: 128256 }, + assets: { + "baseUrl": "https://huggingface.co/executorch-community/Llama-3.2-1B-Instruct-ExecuTorch-WebGPU/resolve/f2b116fc0b746ec8ba8adb93258b9e8b439cefa7/", // @lint-ignore (base URL, files resolve under it) + "files": [ + "llama3_2_1b_instruct_q4gsw.pte", + "tokenizer/config.json", + "tokenizer/generation_config.json", + "tokenizer/special_tokens_map.json", + "tokenizer/tokenizer.json", + "tokenizer/tokenizer_config.json" + ], + "sha256": { + "llama3_2_1b_instruct_q4gsw.pte": "49de6eec3a54f1f45a785152375edafec07cb0fd879a142100340fd2765c40a7", + "tokenizer/config.json": "2febf68cea25bf4611be02b7536f2488a5ba523bb1134986e3610152abe74fdb", + "tokenizer/generation_config.json": "88effbb63300dbbc7390143fbbdd9d9fa50587b37e8bfd16c8c90d4970a74a36", + "tokenizer/special_tokens_map.json": "6f38c73729248f6c127296386e3cdde96e254636cc58b4169d3fd32328d9a8ec", + "tokenizer/tokenizer.json": "79e3e522635f3171300913bb421464a87de6222182a0570b9b2ccba2a964b2b4", + "tokenizer/tokenizer_config.json": "9823dcfdc1121869029da45192238e85cf44f0b232a6d9dc20e4fe6f4242a14e" + }, + "spec": { + "pte": "llama3_2_1b_instruct_q4gsw.pte", + "pteBytes": 734868736, + "tokenizer": "tokenizer", + "chat": "template", + "prefillMax": 512, + "maxCtx": 8960, + "eos": [ + 128001, + 128008, + 128009 + ] + } + }, + credit: { by: 'Meta', url: 'https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct', paper: 'https://arxiv.org/abs/2407.21783' }, + attribution: 'Built with Llama', + license: { + status: LICENSE_STATUS.CLEARED, + reviewNote: "Approved for public redistribution (legal/OSS approval confirmed by the project owner, 2026-10-14).", + upstream: 'Llama 3.2 Community License + Acceptable Use Policy', + provenance: + 'Upstream: meta-llama/Llama-3.2-1B-Instruct (gated on Hugging Face). Derivatives carry attribution and naming requirements. ', + }, + }, + { + id: 'qwen3-0.6b', + name: 'Qwen 3 0.6B', + shortName: 'Qwen 3 0.6B', + task: 'text-generation', + tagline: 'Fastest · 100+ languages', + approxBytes: 416e6, + approxBytesIsEstimate: false, + arch: { layers: 28, dModel: 1024, ffn: 3072, heads: 16, kvHeads: 8, vocab: 151936 }, + assets: { + "baseUrl": "https://huggingface.co/executorch-community/Qwen3-0.6B-ExecuTorch-WebGPU/resolve/65fa8ec0b8996b4a0157cdb89ea766b000b1134a/", // @lint-ignore (base URL, files resolve under it) + "files": [ + "qwen3_0_6b_q4gsw.pte", + "tokenizer/config.json", + "tokenizer/generation_config.json", + "tokenizer/merges.txt", + "tokenizer/tokenizer.json", + "tokenizer/tokenizer_config.json", + "tokenizer/vocab.json" + ], + "sha256": { + "qwen3_0_6b_q4gsw.pte": "057af5734a6ce17cbf19598bcc0e972aed2401d1522eed41249675c1ab372ce6", + "tokenizer/config.json": "660db3b73d788119c04535e48cf9be5f55bc3100841a718637ae695b442f27dd", + "tokenizer/generation_config.json": "2325da0f15bb848e018c5ae071b7943332e9f871d6b60e2ed22ca97d4cb993d2", + "tokenizer/merges.txt": "8831e4f1a044471340f7c0a83d7bd71306a5b867e95fd870f74d0c5308a904d5", + "tokenizer/tokenizer.json": "aeb13307a71acd8fe81861d94ad54ab689df773318809eed3cbe794b4492dae4", + "tokenizer/tokenizer_config.json": "d5d09f07b48c3086c508b30d1c9114bd1189145b74e982a265350c923acd8101", + "tokenizer/vocab.json": "ca10d7e9fb3ed18575dd1e277a2579c16d108e32f27439684afa0e10b1440910" + }, + "spec": { + "pte": "qwen3_0_6b_q4gsw.pte", + "pteBytes": 400532224, + "tokenizer": "tokenizer", + "chat": "chatml-nothink", + "prefillMax": 512, + "maxCtx": 8960, + "eos": [ + 151645, + 151643 + ] + } + }, + credit: { by: 'Qwen Team, Alibaba Cloud', url: 'https://huggingface.co/Qwen/Qwen3-0.6B', paper: 'https://arxiv.org/abs/2505.09388' }, + license: { + status: LICENSE_STATUS.CLEARED, + reviewNote: "Approved for public redistribution (legal/OSS approval confirmed by the project owner, 2026-10-14).", + upstream: 'Apache-2.0', + provenance: 'Upstream: Qwen/Qwen3-0.6B.', + }, + }, + { + id: 'sam2-hiera-tiny', + name: 'SAM 2.1 Hiera Tiny', + shortName: 'SAM 2.1', + task: 'segmentation', + approxBytes: 134e6, + approxBytesIsEstimate: false, + assets: { + "baseUrl": "https://huggingface.co/executorch-community/SAM2-Hiera-Tiny-ExecuTorch-WebGPU/resolve/1c8d8006fa21e77a7c3be6b4cf0a1ed54c1e0469/", // @lint-ignore (base URL, files resolve under it) + "files": [ + "prompt_box_br.bin", + "prompt_box_tl.bin", + "prompt_dense.bin", + "prompt_gaussian.bin", + "prompt_img_pos.bin", + "prompt_not_a_point.bin", + "prompt_point_embed_bg.bin", + "prompt_point_embed_fg.bin", + "sam2_decoder.pte", + "sam2_encoder.pte" + ], + "sha256": { + "prompt_box_br.bin": "a0df40a29af4a9b65d390ae46913d4170762a306fc8061ea87f1d8f4a4a1b0d5", + "prompt_box_tl.bin": "96c208c44e95f7044bc480951be257c415cfa9c91db907e4575ae3c92d09e6b7", + "prompt_dense.bin": "52cece57b549dc81439c1156a07a25ff02db8cb81329e8eefb68c90457a884f3", + "prompt_gaussian.bin": "c37a6a9361886286fc3e868dd279dfa4703919e3fee65c07a07e5a75e980d84f", + "prompt_img_pos.bin": "89dc4182ee4ac6a366dbfd0e9b825e4d7b6e88ded91b2ab06604eb5139e1cc2c", + "prompt_not_a_point.bin": "19771c067b922d9d652e6f3dd1f6e2976c68abb8e6fd9db571255ae37687ea50", + "prompt_point_embed_bg.bin": "91c432d10fcef8e32ce561eb4820beb6a772a61a3a937d7c2af8597061759cfa", + "prompt_point_embed_fg.bin": "2e037dab5bee002d94bdb12d3141ab952cfe2315f0c9feb53a7c78a7e656ed33", + "sam2_decoder.pte": "57b91034acc71be2ce1835ea546a174295daee1046e40db0743e0238ef11e278", + "sam2_encoder.pte": "30f9fb469b779558b6b5c7c3eb995c50e1213ffbe082e31430c929ba2b3ffe20" + }, + "spec": { + "encoder": "sam2_encoder.pte", + "encoderBytes": 115248512, + "decoder": "sam2_decoder.pte", + "decoderBytes": 16949252, + "prompts": { + "gaussian": "prompt_gaussian.bin", + "pointPositive": "prompt_point_embed_fg.bin", + "pointNegative": "prompt_point_embed_bg.bin", + "notAPoint": "prompt_not_a_point.bin", + "boxTopLeft": "prompt_box_tl.bin", + "boxBottomRight": "prompt_box_br.bin", + "imagePositional": "prompt_img_pos.bin", + "dense": "prompt_dense.bin" + }, + "promptTokens": 3, + "imageSize": 512, + "hidden": 256, + "peDim": 128, + "mean": [ + 0.485, + 0.456, + 0.406 + ], + "std": [ + 0.229, + 0.224, + 0.225 + ] + } + }, + credit: { by: 'Meta FAIR', url: 'https://huggingface.co/facebook/sam2.1-hiera-tiny', paper: 'https://arxiv.org/abs/2408.00714' }, + license: { + status: LICENSE_STATUS.CLEARED, + reviewNote: "Approved for public redistribution (legal/OSS approval confirmed by the project owner, 2026-10-14).", + upstream: 'Apache-2.0', + provenance: 'Upstream: facebook/sam2.1-hiera-tiny.', + }, + }, + { + id: 'smolvlm-500m-instruct', + name: 'SmolVLM 500M Instruct', + shortName: 'SmolVLM-500M', + task: 'vision-language', + approxBytes: 934e6, + approxBytesIsEstimate: false, + assets: { + "baseUrl": "https://huggingface.co/executorch-community/SmolVLM-500M-Instruct-ExecuTorch-WebGPU/resolve/2764d1a89e23ca1d2503e6f6e211341317594a74/", // @lint-ignore (base URL, files resolve under it) + "files": [ + "embed_tokens.i8.bin", + "embed_tokens.scale.f32.bin", + "tokenizer/special_tokens_map.json", + "tokenizer/tokenizer.json", + "tokenizer/tokenizer_config.json", + "vlm_decoder.pte", + "vlm_meta.json", + "vlm_prefill.pte", + "vlm_vision_q8.pte" + ], + "sha256": { + "embed_tokens.i8.bin": "d3ea47247be0b0703ca1246335cb3bca5f68a4068e177475388e287d9eb5f385", + "embed_tokens.scale.f32.bin": "adefdadaae81d572c6094b6826e850f263939d716b74da03484dc20c0769562a", + "tokenizer/special_tokens_map.json": "aa0ff906077086dfa9734a7f97f68c825877a48f9468807be65504495cdeef09", + "tokenizer/tokenizer.json": "5ece781dc8d2b2f3e2f289ca0ae50b17cfc27dd27bfe7971bb8241e0b964331a", + "tokenizer/tokenizer_config.json": "36c6fd44d07d10fd8180ee6b46dcccf69fb7c06753968ff0d7e17b8bfe17b777", + "vlm_decoder.pte": "d92697a8c32eeaa1d73603cf608d658cf297fdf41ed81384ec29b7025e78e996", + "vlm_meta.json": "b36fcf7ef7da8c2ace5271aa5da5ffaa5f10a779db2095cb69f5eadfc9972ae4", + "vlm_prefill.pte": "1e8671eb77ae2f60c1eb80073454230bfaeb1eb22cee36dd9b028d8e291a3e3f", + "vlm_vision_q8.pte": "5f09d6028d9bb945ce135d4be9ad499c1081f13d46655d2d1288c207f536ee4e" + }, + "spec": { + "files": { + "vision": { + "path": "vlm_vision_q8.pte", + "bytes": 108885888 + }, + "decoder": { + "path": "vlm_decoder.pte", + "bytes": 388664832 + }, + "prefill": { + "path": "vlm_prefill.pte", + "bytes": 388664320 + }, + "embed": { + "path": "embed_tokens.i8.bin", + "bytes": 47308800 + }, + "embedScale": { + "path": "embed_tokens.scale.f32.bin", + "bytes": 197120 + } + }, + "meta": "vlm_meta.json", + "tokenizer": "tokenizer", + "prefillMax": 83, + "maxCtx": 512 + } + }, + credit: { by: 'Hugging Face', url: 'https://huggingface.co/HuggingFaceTB/SmolVLM-500M-Instruct', paper: 'https://arxiv.org/abs/2504.05299' }, + license: { + status: LICENSE_STATUS.CLEARED, + reviewNote: "Approved for public redistribution (legal/OSS approval confirmed by the project owner, 2026-10-14).", + upstream: 'Apache-2.0', + provenance: + 'Upstream: HuggingFaceTB/SmolVLM-500M-Instruct (built on a SigLIP vision encoder and a SmolLM2 decoder; check both components). ', + }, + }, + { + id: 'whisper-base', + name: 'Whisper base', + shortName: 'Whisper base', + task: 'speech-recognition', + approxBytes: 241e6, + approxBytesIsEstimate: false, + assets: { + "baseUrl": "https://huggingface.co/executorch-community/Whisper-Base-ExecuTorch-WebGPU/resolve/58b7dbb27117def05bca7cbd8af082554c58bb5a/", // @lint-ignore (base URL, files resolve under it) + "files": [ + "vocab.json", + "whisper_cross_kv.pte", + "whisper_decode_stream.pte", + "whisper_embed.i8.bin", + "whisper_embed.scale.f32.bin", + "whisper_encoder.pte", + "whisper_lmhead.pte", + "whisper_mel.pte", + "whisper_pos_embed.f32.bin" + ], + "sha256": { + "vocab.json": "8f680bba319e01a653d2e8a5dbc17a9157179e0576e6ce74ce0c06356c6e24f9", + "whisper_cross_kv.pte": "6e8078924168c21a863ff662e9ccba55ed86bb3826dabd28fea77010e3151012", + "whisper_decode_stream.pte": "bc2f33ad7aa164059e819247365e2e0a75a3f1a2973024e4fdc67a254d7faa8d", + "whisper_embed.i8.bin": "bc7f8d8336ad935eb8b6374036478d92d679eae156a1d4188d5cc016b5f2dda2", + "whisper_embed.scale.f32.bin": "1f58851911611b9686851c586365df2fcaaa0727957c05bd163d7c3ccc231f4d", + "whisper_encoder.pte": "841e7702ba8fa73594f0b4e1400230d6b686af958628ead6b22919e9a3e27c69", + "whisper_lmhead.pte": "68577fa240d29923058fd72b94db583f3ff171302ad12834c2b610e04c559f8e", + "whisper_mel.pte": "7cae31829487958454c5a14aecfed0845f6320dd480c24883861fa39b07de497", + "whisper_pos_embed.f32.bin": "88fa1cdbf2b06f86b0ecb7be0fccfc39e906502986572b8cf5319c250e857169" + }, + "spec": { + "files": { + "mel": "whisper_mel.pte", + "encoder": "whisper_encoder.pte", + "crossKv": "whisper_cross_kv.pte", + "lmHead": "whisper_lmhead.pte", + "decodeStream": "whisper_decode_stream.pte", + "embed": "whisper_embed.i8.bin", + "embedScale": "whisper_embed.scale.f32.bin", + "posEmbed": "whisper_pos_embed.f32.bin", + "vocab": "vocab.json" + }, + "bytes": { + "mel": 710976, + "encoder": 82420996, + "crossKv": 12610560, + "lmHead": 27184000, + "decodeStream": 89270912, + "embed": 26554880, + "embedScale": 207460, + "posEmbed": 917504, + "vocab": 835550 + } + } + }, + credit: { by: 'OpenAI', url: 'https://huggingface.co/openai/whisper-base', paper: 'https://arxiv.org/abs/2212.04356' }, + license: { + status: LICENSE_STATUS.CLEARED, + reviewNote: "Approved for public redistribution (legal/OSS approval confirmed by the project owner, 2026-10-14).", + upstream: 'Apache-2.0 (weights, per the upstream model card; the code is MIT)', + provenance: 'Upstream: openai/whisper-base.', + }, + }, + { + id: 'depth-anything-v2-small', + name: 'Depth Anything V2 Small', + shortName: 'Depth Anything V2-S', + task: 'depth-estimation', + approxBytes: 97e6, + approxBytesIsEstimate: false, + assets: { + "baseUrl": "https://huggingface.co/executorch-community/Depth-Anything-V2-Small-ExecuTorch-WebGPU/resolve/aeba0bbf167caf9007c88b480e8e78fb6e276ccf/", // @lint-ignore (base URL, files resolve under it) + "files": [ + "depth_v2s_webgpu.pte" + ], + "sha256": { + "depth_v2s_webgpu.pte": "285602ed3a34a26a4d9d36355ce978587ed555e68b6b5fa8a567c1155a3b5160" + }, + "spec": { + "pte": "depth_v2s_webgpu.pte", + "pteBytes": 97307268, + "inputSize": [ + 224, + 224 + ] + } + }, + credit: { by: 'Lihe Yang et al. (HKU and TikTok)', url: 'https://huggingface.co/depth-anything/Depth-Anything-V2-Small-hf', paper: 'https://arxiv.org/abs/2406.09414' }, + license: { + status: LICENSE_STATUS.CLEARED, + reviewNote: "Approved for public redistribution (legal/OSS approval confirmed by the project owner, 2026-10-14).", + upstream: 'Apache-2.0 (Small only; Base and Large are CC BY-NC 4.0)', + provenance: 'Upstream: depth-anything/Depth-Anything-V2-Small.', + }, + }, + { + id: 'modnet', + name: 'MODNet portrait matting', + shortName: 'MODNet', + task: 'matting', + approxBytes: 26e6, + approxBytesIsEstimate: false, + assets: { + "baseUrl": "https://huggingface.co/executorch-community/MODNet-ExecuTorch-WebGPU/resolve/fd4bfe709c151e686bf1de95dcd782238d4f65dd/", // @lint-ignore (base URL, files resolve under it) + "files": [ + "modnet_webgpu.pte" + ], + "sha256": { + "modnet_webgpu.pte": "0c2ff92e26087e98b4b2b61497d48d278be2474cc3cfabbc16f58da7eb5ad6c4" + }, + "spec": { + "pte": "modnet_webgpu.pte", + "pteBytes": 26109828, + "inputSize": [ + 256, + 256 + ] + } + }, + credit: { by: 'Zhanghan Ke et al.', url: 'https://github.com/ZHKKKe/MODNet', paper: 'https://arxiv.org/abs/2011.11961' }, + license: { + status: LICENSE_STATUS.CLEARED, + reviewNote: "Approved for public redistribution (legal/OSS approval confirmed by the project owner, 2026-10-14).", + upstream: 'Apache-2.0', + provenance: 'Upstream: ZHKKKe/MODNet.', + }, + }, + { + id: 'real-esrgan', + name: 'Real-ESRGAN ×4 (general v3)', + shortName: 'Real-ESRGAN', + task: 'super-resolution', + approxBytes: 4.9e6, + approxBytesIsEstimate: false, + assets: { + "baseUrl": "https://huggingface.co/executorch-community/Real-ESRGAN-General-x4v3-ExecuTorch-WebGPU/resolve/9cd66d389085ebf6cf65532551d0cac128134007/", // @lint-ignore (base URL, files resolve under it) + "files": [ + "realesr_x4v3_webgpu.pte" + ], + "sha256": { + "realesr_x4v3_webgpu.pte": "a4c40616767829cf6d3dab2c272b147764db1e2a01d610b2a4178e9153a66e5d" + }, + "spec": { + "name": "Real-ESRGAN ×4 (general v3)", + "shortName": "Real-ESRGAN ×4 v3", + "pte": "realesr_x4v3_webgpu.pte", + "pteBytes": 4913472, + "inputSize": [ + 64, + 64 + ], + "scale": 4 + } + }, + credit: { by: 'Xintao Wang et al. (Tencent ARC)', url: 'https://github.com/xinntao/Real-ESRGAN', paper: 'https://arxiv.org/abs/2107.10833' }, + license: { + status: LICENSE_STATUS.CLEARED, + reviewNote: "Approved for public redistribution (legal/OSS approval confirmed by the project owner, 2026-10-14).", + upstream: 'BSD-3-Clause', + provenance: 'Upstream: xinntao/Real-ESRGAN, realesr-general-x4v3 weights.', + }, + }, + { + id: 'dfine-small', + name: 'D-FINE small object detection', + shortName: 'D-FINE S', + task: 'object-detection', + approxBytes: 42e6, + approxBytesIsEstimate: false, + assets: { + "baseUrl": "https://huggingface.co/executorch-community/D-FINE-Small-COCO-ExecuTorch-WebGPU/resolve/c5a149dfd9886a16649c2f9c2148b78cbbc06396/", // @lint-ignore (base URL, files resolve under it) + "files": [ + "dfine_s_coco_webgpu.pte" + ], + "sha256": { + "dfine_s_coco_webgpu.pte": "96cc65f615dc2c79d1da80ef075b831fac8e39e8dcb58af9aff5bab154a4c4d6" + }, + "spec": { + "pte": "dfine_s_coco_webgpu.pte", + "pteBytes": 42260612, + "inputSize": [ + 640, + 640 + ] + } + }, + credit: { by: 'Yansong Peng et al. (USTC)', url: 'https://github.com/Peterande/D-FINE', paper: 'https://arxiv.org/abs/2410.13842' }, + license: { + status: LICENSE_STATUS.CLEARED, + reviewNote: "Approved for public redistribution (approved and published by the project owner, 2026-10-15).", + upstream: 'Apache-2.0', + provenance: 'Upstream: Peterande/D-FINE, D-FINE-S COCO weights via the ustc-community/dfine-small-coco Hugging Face port, verified bit-identical to the official dfine_s_coco.pth.', + }, + }, +]; + +// Published: license cleared and hosted, so visitors can download it. +export function isPublished(model) { + return model.license.status === LICENSE_STATUS.CLEARED && Boolean(model.assets?.baseUrl); +} + +const BY_ID = new Map(MODELS.map((m) => [m.id, m])); + +export function getModel(id) { + const model = BY_ID.get(id); + if (!model) throw new Error(`Unknown model id: ${id}`); + return model; +} diff --git a/website/playground/webgpu/src/config/photo-credits.js b/website/playground/webgpu/src/config/photo-credits.js new file mode 100644 index 00000000000..de19b374a8d --- /dev/null +++ b/website/playground/webgpu/src/config/photo-credits.js @@ -0,0 +1,118 @@ +// Sample photos and their credits. Every photo is public domain, CC0 or CC BY 2.0; files were +// resized, re-encoded and stripped of metadata. +export const PHOTO_CREDITS = [ + { + "file": "astronaut.jpg", + "title": "Astronaut Eileen Collins (scikit-image 'astronaut' sample)", + "author": "NASA", + "authorUrl": "", + "source": "https://scikit-image.org/docs/stable/api/skimage.data.html#skimage.data.astronaut", + "license": "Public domain (work of the U.S. federal government, NASA)", + "licenseUrl": "https://www.usa.gov/government-copyright" + }, + { + "file": "butterfly.jpg", + "title": "Monarch butterfly on butterfly bush", + "author": "Becky Dill (iNaturalist user gillydilly)", + "authorUrl": "", + "source": "https://www.inaturalist.org/photos/43428386", + "license": "CC0 1.0", + "licenseUrl": "https://creativecommons.org/publicdomain/zero/1.0/" + }, + { + "file": "cat.jpg", + "title": "Domestic cat under a parked car", + "author": "Marina Potapova (iNaturalist user windof)", + "authorUrl": "", + "source": "https://www.inaturalist.org/photos/124476421", + "license": "CC0 1.0", + "licenseUrl": "https://creativecommons.org/publicdomain/zero/1.0/" + }, + { + "file": "chihuahuas.jpg", + "title": "Two small dogs held in a person's arms", + "author": "kheintzman (iNaturalist user kheintzman)", + "authorUrl": "", + "source": "https://www.inaturalist.org/photos/59051932", + "license": "CC0 1.0", + "licenseUrl": "https://creativecommons.org/publicdomain/zero/1.0/" + }, + { + "file": "city.jpg", + "title": "Columbus Tower | Sentinel Building at Night", + "author": "Nan Palmero", + "authorUrl": "https://www.flickr.com/people/nanpalmero/", + "source": "https://www.flickr.com/photos/nanpalmero/17092878238", + "license": "CC BY 2.0", + "licenseUrl": "https://creativecommons.org/licenses/by/2.0/", + "via": "Open Images Dataset" + }, + { + "file": "esa-astronaut.jpg", + "title": "ESA Astronaut Alexander Gerst", + "author": "DLR German Aerospace Center", + "authorUrl": "https://www.flickr.com/people/dlr_de/", + "source": "https://www.flickr.com/photos/dlr_de/21377675248", + "license": "CC BY 2.0", + "licenseUrl": "https://creativecommons.org/licenses/by/2.0/", + "via": "Open Images Dataset" + }, + { + "file": "giraffes.jpg", + "title": "Masai Giraffe - Masai Mara National Reserve, Kenya. 04.08.2008", + "author": "JULIAN MASON", + "authorUrl": "https://www.flickr.com/people/meandmyshadow/", + "source": "https://www.flickr.com/photos/meandmyshadow/3520928208", + "license": "CC BY 2.0", + "licenseUrl": "https://creativecommons.org/licenses/by/2.0/", + "via": "Open Images Dataset" + }, + { + "file": "husky.jpg", + "title": "Husky face close-up", + "author": "Nonbinary-Naturalist (iNaturalist user nonbinary-naturalist)", + "authorUrl": "", + "source": "https://www.inaturalist.org/photos/466838529", + "license": "CC0 1.0", + "licenseUrl": "https://creativecommons.org/publicdomain/zero/1.0/" + }, + { + "file": "receipt.jpg", + "title": "Receipt for mail to be delivered on March 24, 1970", + "author": "Albany Letter Express (issuer); National Postal Museum", + "authorUrl": "", + "source": "https://postalmuseum.si.edu/object/npm_2010.2023.13", + "license": "CC0 1.0", + "licenseUrl": "https://creativecommons.org/publicdomain/zero/1.0/" + }, + { + "file": "skier.jpg", + "title": "Heli Ski in Chile", + "author": "Alex Grechman", + "authorUrl": "https://www.flickr.com/people/grechman/", + "source": "https://www.flickr.com/photos/grechman/2790755992", + "license": "CC BY 2.0", + "licenseUrl": "https://creativecommons.org/licenses/by/2.0/", + "via": "Open Images Dataset" + }, + { + "file": "street.jpg", + "title": "New York City", + "author": "FaceMePLS", + "authorUrl": "https://www.flickr.com/people/faceme/", + "source": "https://www.flickr.com/photos/faceme/9580869905", + "license": "CC BY 2.0", + "licenseUrl": "https://creativecommons.org/licenses/by/2.0/", + "via": "Open Images Dataset" + }, + { + "file": "table.jpg", + "title": "Family Reunion", + "author": "Kurman Communications, Inc.", + "authorUrl": "https://www.flickr.com/people/kurmanphotos/", + "source": "https://www.flickr.com/photos/kurmanphotos/22097532635", + "license": "CC BY 2.0", + "licenseUrl": "https://creativecommons.org/licenses/by/2.0/", + "via": "Open Images Dataset" + } +]; diff --git a/website/playground/webgpu/src/config/runtime.js b/website/playground/webgpu/src/config/runtime.js new file mode 100644 index 00000000000..69a4faf07e2 --- /dev/null +++ b/website/playground/webgpu/src/config/runtime.js @@ -0,0 +1,17 @@ +// Files hosted in the ExecuTorch WebGPU Runtime repo on Hugging Face, pinned to one +// commit and checked against their SHA-256: the runtime's .wasm +// (runtime/executorch_wasm_lib.js is the loader from the same build; see +// runtime/BUILD_INFO.md) and the tokenizer library. +export const RUNTIME_WASM = Object.freeze({ + url: 'https://huggingface.co/executorch-community/ExecuTorch-WebGPU-Runtime/resolve/48a27434e9f3096f100d79a5d1e8437a124ca654/executorch_wasm_lib.wasm', + sha256: '35a9087d0b6cd86c279e7111adfd54a58156bd993928b20f63348f1043652eb4', + bytes: 6562188, +}); + +// @huggingface/transformers 3.0.2, unmodified from npm (Apache-2.0); used for +// tokenizers and chat templates only. +export const TOKENIZER_LIB = Object.freeze({ + url: 'https://huggingface.co/executorch-community/ExecuTorch-WebGPU-Runtime/resolve/48a27434e9f3096f100d79a5d1e8437a124ca654/vendor/transformers/transformers.min.js', + sha256: '495b03b846b315baab6ff2e2f048085e45684b910799e36c4d7f2b4be00cff72', + bytes: 748496, +}); diff --git a/website/playground/webgpu/src/config/shared.js b/website/playground/webgpu/src/config/shared.js new file mode 100644 index 00000000000..56249367a6d --- /dev/null +++ b/website/playground/webgpu/src/config/shared.js @@ -0,0 +1,18 @@ +// Sample media, served from this site (sources and licenses in config/photo-credits.js, +// also assets/samples/CREDITS.md), and the pinned tokenizer library. +import { TOKENIZER_LIB } from './runtime.js'; +const at = (path) => new URL(`../../${path}`, import.meta.url).href; +const sample = (file, label) => ({ path: at(`assets/samples/${file}`), label }); + +export const SHARED = Object.freeze({ + tokenizerLib: TOKENIZER_LIB, + visionSamples: { + depth: [sample('city.jpg', 'City at night'), sample('giraffes.jpg', 'Giraffes'), sample('husky.jpg', 'Husky'), sample('astronaut.jpg', 'Astronaut')], + detect: [sample('street.jpg', 'Street'), sample('table.jpg', 'Table'), sample('cat.jpg', 'Cat')], + bgremove: [sample('astronaut.jpg', 'Astronaut'), sample('esa-astronaut.jpg', 'ESA astronaut'), sample('skier.jpg', 'Skier')], + superres: [sample('husky.jpg', 'Husky'), sample('butterfly.jpg', 'Butterfly'), sample('chihuahuas.jpg', 'Chihuahuas'), sample('giraffes.jpg', 'Giraffes')], + effects: [sample('astronaut.jpg', 'Astronaut'), sample('esa-astronaut.jpg', 'ESA astronaut'), sample('skier.jpg', 'Skier')], + }, + segmentSamples: [sample('chihuahuas.jpg', 'Chihuahuas'), sample('city.jpg', 'City at night'), sample('street.jpg', 'Street'), sample('astronaut.jpg', 'Astronaut')], + vlmSamples: [sample('astronaut.jpg', 'Astronaut'), sample('receipt.jpg', 'Receipt'), sample('city.jpg', 'City street'), sample('husky.jpg', 'Husky')], +}); diff --git a/website/playground/webgpu/src/gpu/adapter.js b/website/playground/webgpu/src/gpu/adapter.js new file mode 100644 index 00000000000..ab7142e463b --- /dev/null +++ b/website/playground/webgpu/src/gpu/adapter.js @@ -0,0 +1,118 @@ +// WebGPU detection without UA sniffing: ask for an adapter and report what the +// browser says. Failure modes are kept distinct because each has a different remedy. + +export const GPU_FAILURE = Object.freeze({ + INSECURE_CONTEXT: 'insecure-context', + NO_API: 'no-api', + NO_ADAPTER: 'no-adapter', + DEVICE_FAILED: 'device-failed', + DEVICE_LOST: 'device-lost', +}); + +export const FAILURE_COPY = { + [GPU_FAILURE.INSECURE_CONTEXT]: { + chip: 'No WebGPU · insecure page', + title: 'This page isn’t in a secure context', + body: 'Browsers only expose WebGPU on HTTPS pages and on localhost. This copy was opened over plain HTTP, so the GPU is hidden from it.', + remedies: ['Open the HTTPS version of this page.', 'If you are serving it yourself, use HTTPS or localhost.'], + }, + [GPU_FAILURE.NO_API]: { + chip: 'No WebGPU in this browser', + title: 'Your browser doesn’t offer WebGPU yet', + body: 'navigator.gpu is missing, so this browser (or this browser configuration) has no WebGPU implementation turned on.', + remedies: [ + 'Use a current Chrome or Edge on desktop or Android, or Safari 26 or later.', + 'Firefox ships WebGPU on some platforms. Check that it is enabled in your version.', + 'Enterprise policies can switch WebGPU off. Ask your administrator if it should be on.', + ], + }, + [GPU_FAILURE.NO_ADAPTER]: { + chip: 'No WebGPU adapter', + title: 'WebGPU is here, but no GPU was offered', + body: 'The API exists, but requestAdapter() returned nothing. That usually means the GPU driver is blocklisted, or this is a virtual machine or remote desktop without GPU passthrough.', + remedies: [ + 'Update your graphics driver and restart the browser.', + 'Turn on hardware acceleration in your browser settings.', + 'On a VM or remote desktop, try a machine with direct GPU access.', + ], + }, + [GPU_FAILURE.DEVICE_FAILED]: { + chip: 'GPU device unavailable', + title: 'Found a GPU, but couldn’t open it', + body: 'An adapter was offered, but creating a device on it failed. This often clears up after the GPU process restarts.', + remedies: ['Close other GPU-heavy tabs and reload.', 'Restart the browser.', 'Update your graphics driver.'], + }, + [GPU_FAILURE.DEVICE_LOST]: { + chip: 'GPU connection lost', + title: 'The GPU connection was lost', + body: 'The browser reset the GPU device. This can happen after a driver crash, a long-running shader, or when the system reclaims GPU memory.', + remedies: ['Reconnect below. Loaded demos will need to load again.'], + }, +}; + +export async function readAdapterInfo(adapter) { + let info = adapter.info; + if (!info && typeof adapter.requestAdapterInfo === 'function') { + try { + info = await adapter.requestAdapterInfo(); + } catch { + info = null; + } + } + const field = (k) => (info && typeof info[k] === 'string' ? info[k].trim() : ''); + return { + vendor: field('vendor'), + architecture: field('architecture'), + device: field('device'), + description: field('description'), + isFallbackAdapter: Boolean(info?.isFallbackAdapter ?? adapter.isFallbackAdapter), + }; +} + +// Browsers mask these strings to different degrees; blanks are normal. +export function adapterLabel(info) { + const primary = [info.vendor, info.architecture].filter(Boolean).join(' '); + const label = primary || info.description || info.device || 'WebGPU adapter (details hidden by browser)'; + return info.isFallbackAdapter ? `${label} · software` : label; +} + +export async function detectGPU({ powerPreference = 'high-performance' } = {}) { + if (!globalThis.isSecureContext) return { ok: false, reason: GPU_FAILURE.INSECURE_CONTEXT }; + if (!('gpu' in navigator) || !navigator.gpu) return { ok: false, reason: GPU_FAILURE.NO_API }; + + let adapter = null; + try { + adapter = await navigator.gpu.requestAdapter({ powerPreference }); + } catch (err) { + return { ok: false, reason: GPU_FAILURE.NO_ADAPTER, detail: String(err?.message ?? err) }; + } + if (!adapter) return { ok: false, reason: GPU_FAILURE.NO_ADAPTER }; + + const info = await readAdapterInfo(adapter); + let device; + try { + device = await adapter.requestDevice({ label: 'executorch-webgpu-examples' }); + } catch (err) { + return { ok: false, reason: GPU_FAILURE.DEVICE_FAILED, info, detail: String(err?.message ?? err) }; + } + return { + ok: true, + adapter, + device, + info, + label: adapterLabel(info), + features: [...adapter.features].sort(), + limits: pickLimits(device.limits), + }; +} + +function pickLimits(limits) { + const keys = [ + 'maxBufferSize', + 'maxStorageBufferBindingSize', + 'maxComputeWorkgroupStorageSize', + 'maxComputeInvocationsPerWorkgroup', + 'maxComputeWorkgroupsPerDimension', + ]; + return Object.fromEntries(keys.map((k) => [k, limits[k]])); +} diff --git a/website/playground/webgpu/src/gpu/context.js b/website/playground/webgpu/src/gpu/context.js new file mode 100644 index 00000000000..dfd77ce9082 --- /dev/null +++ b/website/playground/webgpu/src/gpu/context.js @@ -0,0 +1,74 @@ +import { createStore } from '../lib/store.js'; +import { adapterLabel, detectGPU, GPU_FAILURE } from './adapter.js'; + +// status: 'probing' | 'ready' | 'unavailable' | 'lost' +export const gpuState = createStore({ status: 'probing', reason: null, label: '', info: null, device: null }); + +// Shader compile messages, validation errors, and self-test results all land +// here so the UI can surface them instead of burying them in the console. +export const diagnostics = createStore({ items: [] }); + +let nextId = 1; + +export function reportDiagnostic(item) { + const entry = { id: nextId++, time: Date.now(), ...item }; + diagnostics.set({ items: [...diagnostics.get().items, entry] }); + const log = item.level === 'error' ? console.error : item.level === 'warning' ? console.warn : console.info; + log(`[webgpu:${item.source}] ${item.title}${item.detail ? `\n${item.detail}` : ''}`); + return entry; +} + +export function dismissDiagnostic(id) { + diagnostics.set({ items: diagnostics.get().items.filter((d) => d.id !== id) }); +} + +export async function initGPU() { + gpuState.set({ status: 'probing', reason: null, device: null }); + const result = await detectGPU(); + if (!result.ok) { + gpuState.set({ + status: 'unavailable', + reason: result.reason, + detail: result.detail ?? '', + info: result.info ?? null, + label: result.info ? adapterLabel(result.info) : '', + device: null, + }); + return gpuState.get(); + } + + const { device } = result; + device.addEventListener('uncapturederror', (event) => { + reportDiagnostic({ + level: 'error', + source: 'validation', + title: 'WebGPU reported an error', + detail: event.error?.message ?? String(event.error), + }); + }); + device.lost.then((lost) => { + if (gpuState.get().device !== device) return; + // reason 'destroyed' means we tore it down on purpose. + if (lost.reason === 'destroyed') return; + gpuState.set({ status: 'lost', reason: GPU_FAILURE.DEVICE_LOST, detail: lost.message, device: null }); + reportDiagnostic({ level: 'error', source: 'device', title: 'GPU device lost', detail: lost.message }); + }); + + gpuState.set({ + status: 'ready', + reason: null, + detail: '', + device, + info: result.info, + label: result.label, + features: result.features, + limits: result.limits, + }); + return gpuState.get(); +} + +export function requireDevice() { + const { device, status } = gpuState.get(); + if (!device) throw new Error(`WebGPU device not available (status: ${status})`); + return device; +} diff --git a/website/playground/webgpu/src/lib/dom.js b/website/playground/webgpu/src/lib/dom.js new file mode 100644 index 00000000000..a2cf5e3f1db --- /dev/null +++ b/website/playground/webgpu/src/lib/dom.js @@ -0,0 +1,48 @@ +// Minimal element builder. Classes are always passed as `class: '...'` string +// literals so scripts/check-wiring.mjs can verify every one has a CSS rule. +export function h(tag, props, ...children) { + const el = document.createElement(tag); + if (props) { + for (const [key, value] of Object.entries(props)) { + if (value == null || value === false) continue; + if (key === 'class') el.className = value; + else if (key === 'html') el.innerHTML = value; + else if (key === 'dataset') Object.assign(el.dataset, value); + else if (key === 'style' && typeof value === 'object') Object.assign(el.style, value); + else if (key.startsWith('on') && typeof value === 'function') el.addEventListener(key.slice(2).toLowerCase(), value); + else if (key === 'value') el.value = value; + else if (value === true) el.setAttribute(key, ''); + else el.setAttribute(key, String(value)); + } + } + appendChildren(el, children); + return el; +} + +export function appendChildren(el, children) { + for (const child of children.flat(Infinity)) { + if (child == null || child === false) continue; + el.append(child instanceof Node ? child : document.createTextNode(String(child))); + } + return el; +} + +// replaceChildren() would stringify null/false into text; this skips them like h(). +export function replace(el, ...children) { + el.replaceChildren(); + return appendChildren(el, children); +} + +export function svg(markup) { + const tpl = document.createElement('template'); + tpl.innerHTML = markup.trim(); + return tpl.content.firstElementChild; +} + +let uidCounter = 0; +// SVG gradient/pattern ids are document-global; suffix them per instance so +// several cards on screen never cross-wire their fills. +export function uid(prefix = 'u') { + uidCounter += 1; + return `${prefix}${uidCounter}`; +} diff --git a/website/playground/webgpu/src/lib/format.js b/website/playground/webgpu/src/lib/format.js new file mode 100644 index 00000000000..0ac94244e55 --- /dev/null +++ b/website/playground/webgpu/src/lib/format.js @@ -0,0 +1,26 @@ +export function formatBytes(bytes, { approx = false } = {}) { + if (bytes == null || !Number.isFinite(bytes)) return '—'; + const prefix = approx ? '~' : ''; + if (bytes >= 1e9) return `${prefix}${(bytes / 1e9).toFixed(bytes >= 1e10 ? 0 : 1)} GB`; + if (bytes >= 1e6) return `${prefix}${Math.round(bytes / 1e6)} MB`; + if (bytes >= 1e3) return `${prefix}${Math.round(bytes / 1e3)} KB`; + return `${prefix}${bytes} B`; +} + +export function formatMs(ms) { + if (ms == null || !Number.isFinite(ms)) return '—'; + if (ms >= 10000) return `${(ms / 1000).toFixed(1)} s`; + if (ms >= 1000) return `${(ms / 1000).toFixed(2)} s`; + if (ms >= 100) return `${Math.round(ms)} ms`; + return `${ms.toFixed(1)} ms`; +} + +export function formatRate(value, unit) { + if (value == null || !Number.isFinite(value)) return '—'; + const digits = value >= 100 ? 0 : value >= 10 ? 1 : 2; + return `${value.toFixed(digits)} ${unit}`; +} + +export function formatInt(n) { + return Number(n).toLocaleString('en-US'); +} diff --git a/website/playground/webgpu/src/lib/markdown.js b/website/playground/webgpu/src/lib/markdown.js new file mode 100644 index 00000000000..45b4f0f07ff --- /dev/null +++ b/website/playground/webgpu/src/lib/markdown.js @@ -0,0 +1,58 @@ +// A small Markdown subset for model replies: fenced code, inline code, bold, +// italics, headings, lists, and paragraphs. Input is HTML-escaped first and +// links are never generated, so model output cannot inject markup. +const escape = (s) => s.replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c]); + +function inline(s) { + return s + .replace(/`([^`\n]+)`/g, '$1') + .replace(/\*\*([^*\n]+)\*\*/g, '$1') + .replace(/(^|[^*])\*([^*\n]+)\*(?!\*)/g, '$1$2'); +} + +export function renderMarkdown(src) { + const out = []; + const parts = src.split(/^```[^\n]*\n?/m); + parts.forEach((part, i) => { + if (i % 2 === 1) { + // An unterminated fence while streaming still renders as code. + out.push(`
      ${escape(part.replace(/\n$/, ''))}
      `); + return; + } + let list = null; + let para = []; + const flushPara = () => { + if (para.length) out.push(`

      ${inline(escape(para.join('\n'))).replace(/\n/g, '
      ')}

      `); + para = []; + }; + const flushList = () => { + if (list) out.push(`<${list.tag}>${list.items.map((x) => `
    • ${inline(escape(x))}
    • `).join('')}`); + list = null; + }; + for (const line of part.split('\n')) { + const bullet = /^\s*[-*•]\s+(.*)$/.exec(line); + const numbered = /^\s*\d+[.)]\s+(.*)$/.exec(line); + const heading = /^(#{1,4})\s+(.*)$/.exec(line); + if (bullet || numbered) { + flushPara(); + const tag = bullet ? 'ul' : 'ol'; + if (list?.tag !== tag) flushList(); + list ??= { tag, items: [] }; + list.items.push((bullet ?? numbered)[1]); + } else if (heading) { + flushPara(); + flushList(); + out.push(`

      ${inline(escape(heading[2]))}

      `); + } else if (!line.trim()) { + flushPara(); + flushList(); + } else { + flushList(); + para.push(line); + } + } + flushPara(); + flushList(); + }); + return out.join(''); +} diff --git a/website/playground/webgpu/src/lib/prng.js b/website/playground/webgpu/src/lib/prng.js new file mode 100644 index 00000000000..c8546e0e7dc --- /dev/null +++ b/website/playground/webgpu/src/lib/prng.js @@ -0,0 +1,47 @@ +// Deterministic pseudo-random helpers shared by the CPU references, the +// tokenizer's quick-fill text, and the benchmarks. + +export function hashString(str) { + let h = 0x811c9dc5; + for (let i = 0; i < str.length; i++) { + h ^= str.charCodeAt(i); + h = Math.imul(h, 0x01000193); + } + return h >>> 0; +} + +export function mulberry32(seed) { + let a = seed >>> 0; + return function next() { + a = (a + 0x6d2b79f5) >>> 0; + let t = a; + t = Math.imul(t ^ (t >>> 15), t | 1); + t ^= t + Math.imul(t ^ (t >>> 7), t | 61); + return ((t ^ (t >>> 14)) >>> 0) / 4294967296; + }; +} + +export function randomFloats(n, seed, scale = 1) { + const rng = mulberry32(seed); + const out = new Float32Array(n); + for (let i = 0; i < n; i++) out[i] = (rng() * 2 - 1) * scale; + return out; +} + +// Mirrors `pcg` in the WGSL init kernel bit-for-bit, so GPU-initialised +// parameters can be checked against the CPU. +export function pcg(v) { + const state = (Math.imul(v >>> 0, 747796405) + 2891336453) >>> 0; + const word = Math.imul(((state >>> ((state >>> 28) + 4)) ^ state) >>> 0, 277803737) >>> 0; + return ((word >>> 22) ^ word) >>> 0; +} + +// Sum of three uniforms: mean 0, variance 1. A cheap stand-in for a Gaussian. +export function initValue(index, seed, scale) { + const s = pcg(seed); + const i3 = Math.imul(index >>> 0, 3) >>> 0; + const u0 = pcg((i3 + s) >>> 0) / 4294967296; + const u1 = pcg((i3 + 1 + s) >>> 0) / 4294967296; + const u2 = pcg((i3 + 2 + s) >>> 0) / 4294967296; + return Math.fround((u0 + u1 + u2 - 1.5) * 2 * scale); +} diff --git a/website/playground/webgpu/src/lib/store.js b/website/playground/webgpu/src/lib/store.js new file mode 100644 index 00000000000..6438c7928b6 --- /dev/null +++ b/website/playground/webgpu/src/lib/store.js @@ -0,0 +1,16 @@ +export function createStore(initial) { + let state = initial; + const listeners = new Set(); + return { + get: () => state, + set(patch) { + state = { ...state, ...patch }; + for (const fn of listeners) fn(state); + }, + subscribe(fn) { + listeners.add(fn); + fn(state); + return () => listeners.delete(fn); + }, + }; +} diff --git a/website/playground/webgpu/src/lib/theme.js b/website/playground/webgpu/src/lib/theme.js new file mode 100644 index 00000000000..84ee5b15630 --- /dev/null +++ b/website/playground/webgpu/src/lib/theme.js @@ -0,0 +1,34 @@ +import { createStore } from './store.js'; + +// Follows the OS until the visitor picks a side, then persists their choice. +const KEY = 'etwg-theme'; +const media = matchMedia('(prefers-color-scheme: dark)'); + +function readSaved() { + try { + const v = localStorage.getItem(KEY); + return v === 'dark' || v === 'light' ? v : null; + } catch { + return null; + } +} + +export const theme = createStore({ value: readSaved() ?? (media.matches ? 'dark' : 'light'), explicit: readSaved() !== null }); + +theme.subscribe(({ value }) => { + document.documentElement.dataset.theme = value; +}); + +media.addEventListener('change', (e) => { + if (!theme.get().explicit) theme.set({ value: e.matches ? 'dark' : 'light' }); +}); + +export function toggleTheme() { + const value = theme.get().value === 'dark' ? 'light' : 'dark'; + try { + localStorage.setItem(KEY, value); + } catch { + // Storage can be unavailable (private mode); the choice still applies for this visit. + } + theme.set({ value, explicit: true }); +} diff --git a/website/playground/webgpu/src/loader/fetch-progress.js b/website/playground/webgpu/src/loader/fetch-progress.js new file mode 100644 index 00000000000..4cd6b9d7d3e --- /dev/null +++ b/website/playground/webgpu/src/loader/fetch-progress.js @@ -0,0 +1,100 @@ +// Streaming fetch with byte-level progress and a Cache Storage tier, so each +// model file downloads at most once per device. Every hosted model file, and the +// runtime's .wasm, takes this path. + +export const MODEL_CACHE = 'executorch-webgpu-models-v1'; + +// Expected SHA-256 per URL, registered by the model source for hosted files. +// A download that doesn't match is rejected before it is cached or used. +const expected = new Map(); + +export function expectSha256(url, hex) { + expected.set(url, hex.toLowerCase()); +} + +async function verify(url, bytes) { + const want = expected.get(url); + if (!want) return; + if (!globalThis.crypto?.subtle) { + throw new Error(`Cannot verify ${url}: this page is not a secure context (HTTPS or localhost)`); + } + const digest = new Uint8Array(await crypto.subtle.digest('SHA-256', bytes)); + const got = Array.from(digest, (b) => b.toString(16).padStart(2, '0')).join(''); + if (got !== want) throw new Error(`Checksum mismatch for ${url}: the download is corrupt or was changed`); +} + +export async function fetchWithProgress(url, { onProgress, signal, expectedBytes = 0, deps = {} } = {}) { + const fetchImpl = deps.fetch ?? globalThis.fetch; + const cacheStorage = deps.caches ?? globalThis.caches; + const cache = cacheStorage ? await cacheStorage.open(MODEL_CACHE) : null; + + const hit = cache ? await cache.match(url) : undefined; + if (hit) { + const bytes = new Uint8Array(await hit.arrayBuffer()); + onProgress?.({ loaded: bytes.byteLength, total: bytes.byteLength, fromCache: true }); + return bytes; + } + + const response = await fetchImpl(url, { signal }); + if (!response.ok) throw new Error(`Download failed: HTTP ${response.status} for ${url}`); + + // Content-Length is the compressed size when the server applies Content-Encoding, + // so it is only a hint; an overrun switches the bar to indeterminate. + const headerBytes = Number(response.headers.get('content-length')) || 0; + let total = headerBytes || expectedBytes; + const reader = response.body.getReader(); + const chunks = []; + let loaded = 0; + for (;;) { + const { done, value } = await reader.read(); + if (done) break; + chunks.push(value); + loaded += value.byteLength; + if (total && loaded > total) total = 0; + onProgress?.({ loaded, total, fromCache: false }); + } + + const bytes = new Uint8Array(loaded); + let offset = 0; + for (const c of chunks) { + bytes.set(c, offset); + offset += c.byteLength; + } + onProgress?.({ loaded, total: loaded, fromCache: false }); + await verify(url, bytes); + + if (cache) { + // Caching is best-effort: a full quota or a private window must not fail a + // download that already succeeded. + try { + await cache.put( + url, + new Response(bytes, { + headers: { + 'content-type': response.headers.get('content-type') ?? 'application/octet-stream', + 'content-length': String(loaded), + }, + }), + ); + } catch (err) { + console.warn(`Could not cache ${url}: ${err.message}`); + } + } + return bytes; +} + +export async function isCached(url, deps = {}) { + const cacheStorage = deps.caches ?? globalThis.caches; + if (!cacheStorage) return false; + const cache = await cacheStorage.open(MODEL_CACHE); + return Boolean(await cache.match(url)); +} + +export async function evictFromCache(urls, deps = {}) { + const cacheStorage = deps.caches ?? globalThis.caches; + if (!cacheStorage) return 0; + const cache = await cacheStorage.open(MODEL_CACHE); + let removed = 0; + for (const url of urls) if (await cache.delete(url)) removed++; + return removed; +} diff --git a/website/playground/webgpu/src/loader/import-verified.js b/website/playground/webgpu/src/loader/import-verified.js new file mode 100644 index 00000000000..ef6b5aecb7b --- /dev/null +++ b/website/playground/webgpu/src/loader/import-verified.js @@ -0,0 +1,27 @@ +// Imports an ES module from a pinned URL after checking its SHA-256. The bytes go +// through the same verified, cached download as model files, then load from a +// blob: URL, so the module's MIME type doesn't depend on the host. A blob: URL +// can't be the base of a relative URL, so the module's import.meta.url is +// pointed back at the URL it was downloaded from. +import { expectSha256, fetchWithProgress } from './fetch-progress.js'; + +const modules = new Map(); + +export function importVerified({ url, sha256, bytes }) { + if (!modules.has(url)) { + const loading = (async () => { + expectSha256(url, sha256); + const code = await fetchWithProgress(url, { expectedBytes: bytes }); + const source = new TextDecoder().decode(code).replaceAll('import.meta.url', JSON.stringify(url)); + const blobUrl = URL.createObjectURL(new Blob([source], { type: 'text/javascript' })); + try { + return await import(blobUrl); + } finally { + URL.revokeObjectURL(blobUrl); + } + })(); + loading.catch(() => modules.delete(url)); + modules.set(url, loading); + } + return modules.get(url); +} diff --git a/website/playground/webgpu/src/loader/resolve.js b/website/playground/webgpu/src/loader/resolve.js new file mode 100644 index 00000000000..94b3e8ce834 --- /dev/null +++ b/website/playground/webgpu/src/loader/resolve.js @@ -0,0 +1,23 @@ +import { LICENSE_STATUS } from '../config/models.js'; + +export const WEIGHT_SOURCE = Object.freeze({ REMOTE: 'remote', UNAVAILABLE: 'unavailable' }); + +// Remote weights require BOTH a cleared license AND an https URL. Either alone +// is treated as "not available" so a stray URL can never imply legal review. +export function resolveWeights(model) { + const reasons = []; + if (model?.license?.status !== LICENSE_STATUS.CLEARED) { + reasons.push({ code: 'license-not-cleared', text: `License review status: ${model?.license?.status ?? 'missing'}` }); + } + const baseUrl = model?.assets?.baseUrl; + if (typeof baseUrl !== 'string' || !/^https:\/\//.test(baseUrl)) { + reasons.push({ code: 'no-asset-url', text: 'No approved download location configured' }); + } + if (reasons.length) return { source: WEIGHT_SOURCE.UNAVAILABLE, reasons }; + const root = baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`; + return { + source: WEIGHT_SOURCE.REMOTE, + reasons: [], + files: model.assets.files.map((name) => ({ name, url: new URL(name, root).href })), + }; +} diff --git a/website/playground/webgpu/src/main.js b/website/playground/webgpu/src/main.js new file mode 100644 index 00000000000..988ca2fe1cc --- /dev/null +++ b/website/playground/webgpu/src/main.js @@ -0,0 +1,107 @@ +import { getDemo } from './config/catalog.js'; +import { initGPU } from './gpu/context.js'; +import { h, replace } from './lib/dom.js'; +import './lib/theme.js'; +import { mountCatalog } from './ui/catalog.js'; +import { mountChat } from './ui/demo-chat.js'; +import { mountEffects } from './ui/demo-effects.js'; +import { mountSegment } from './ui/demo-segment.js'; +import { mountStt } from './ui/demo-stt.js'; +import { mountVision } from './ui/demo-vision.js'; +import { mountVlm } from './ui/demo-vlm.js'; +import { mountDiagnosticsBanner } from './ui/diagnostics-banner.js'; +import { icon } from './ui/icons.js'; +import { mountModels } from './ui/models-page.js'; +import { createSidebar } from './ui/sidebar.js'; + +const DEMO_VIEWS = { + chat: mountChat, + vision: mountVision, + stt: mountStt, + vlm: mountVlm, + segment: mountSegment, + effects: mountEffects, +}; + +// Hash routing keeps the site a folder of static files that works under any +// subpath (e.g. /examples/) without server rewrites. +function parseRoute() { + const parts = location.hash.replace(/^#\/?/, '').split('/').filter(Boolean); + if (parts[0] === 'demo' && parts[1]) return { name: 'demo', id: parts[1], nav: 'demo' }; + if (parts[0] === 'models') return { name: 'models', nav: 'models' }; + if (parts[0] === 'examples' && parts[1]) return { name: 'catalog', category: parts[1], nav: `examples/${parts[1]}` }; + return { name: 'catalog', nav: 'examples' }; +} + +const COLLAPSE_KEY = 'etwg-sidebar-collapsed'; +const app = document.getElementById('app'); +const main = h('main', { class: 'main', id: 'main', tabindex: '-1' }); +const banner = h('div', { class: 'diag-stack', 'aria-live': 'assertive' }); +const view = h('div', { class: 'view' }); + +function setCollapsed(collapsed) { + app.classList.toggle('app--collapsed', collapsed); + try { + localStorage.setItem(COLLAPSE_KEY, collapsed ? '1' : '0'); + } catch { + // Non-persistent is fine. + } +} + +const sidebar = createSidebar({ + onToggleCollapse: () => setCollapsed(!app.classList.contains('app--collapsed')), + onNavigate: () => app.classList.remove('app--drawer-open'), +}); +const mobileBar = h( + 'div', + { class: 'mobilebar' }, + h('button', { class: 'icon-btn', type: 'button', 'aria-label': 'Open navigation', onclick: () => app.classList.add('app--drawer-open') }, icon('menu', 20)), + h('span', { class: 'mobilebar__title' }, 'WebGPU Playground'), +); +const scrim = h('div', { class: 'scrim', onclick: () => app.classList.remove('app--drawer-open') }); + +main.append(mobileBar, banner, view); +replace(app, sidebar, scrim, main); +try { + if (localStorage.getItem(COLLAPSE_KEY) === '1') app.classList.add('app--collapsed'); +} catch { + // Storage unavailable: start expanded. +} +mountDiagnosticsBanner(banner); + +let cleanup = null; +let lastKey = ''; +function render() { + const route = parseRoute(); + const key = route.name === 'demo' ? `demo/${route.id}` : route.name; + sidebar.setActive(route); + if (key === lastKey && route.name === 'catalog') { + if (route.category) view.querySelector(`#band-${route.category}`)?.scrollIntoView({ behavior: 'smooth', block: 'start' }); + else window.scrollTo({ top: 0, behavior: 'smooth' }); + return; + } + lastKey = key; + cleanup?.(); + cleanup = null; + window.scrollTo({ top: 0 }); + + if (route.name === 'demo') { + const demo = getDemo(route.id); + if (!demo) { + replace(view, h('div', { class: 'page' }, h('h1', { class: 'page-title' }, 'Not found'), h('p', null, h('a', { href: '#/examples' }, 'Back to Examples')))); + return; + } + document.title = `${demo.title} · ExecuTorch Playground`; + cleanup = DEMO_VIEWS[demo.view](view, demo); + } else if (route.name === 'models') { + document.title = 'Models · ExecuTorch Playground'; + cleanup = mountModels(view); + } else { + document.title = 'WebGPU · ExecuTorch Playground'; + cleanup = mountCatalog(view, { category: route.category }); + } +} + +window.addEventListener('hashchange', render); +render(); +initGPU(); diff --git a/website/playground/webgpu/src/runtime/emscripten.js b/website/playground/webgpu/src/runtime/emscripten.js new file mode 100644 index 00000000000..ecca1fbd65e --- /dev/null +++ b/website/playground/webgpu/src/runtime/emscripten.js @@ -0,0 +1,49 @@ +// Loads an Emscripten MODULARIZE runtime (a classic script defining a global +// factory) and instantiates it, with its .wasm passed in as bytes or resolved +// next to the script. +const scripts = new Map(); + +export function loadScript(url) { + if (!scripts.has(url)) { + scripts.set( + url, + new Promise((resolve, reject) => { + const el = document.createElement('script'); + el.src = url; + el.async = true; + el.onload = () => resolve(); + el.onerror = () => { + scripts.delete(url); + reject(new Error(`Could not load runtime script ${url}`)); + }; + document.head.append(el); + }), + ); + } + return scripts.get(url); +} + +// Each runtime script declares its factory with `var`, so two builds that share a +// factory name overwrite each other. Capture the factory right after its own +// script loads, and give every build a unique script URL. +const factories = new Map(); + +export async function instantiate(scriptUrl, factoryName, { wasmUrl, wasmBinary, onLog } = {}) { + if (!factories.has(scriptUrl)) { + await loadScript(scriptUrl); + const factory = globalThis[factoryName]; + if (typeof factory !== 'function') throw new Error(`Runtime ${scriptUrl} did not define ${factoryName}()`); + factories.set(scriptUrl, factory); + } + const dir = scriptUrl.slice(0, scriptUrl.lastIndexOf('/') + 1); + return factories.get(scriptUrl)({ + ...(wasmBinary ? { wasmBinary } : {}), + locateFile: (p) => (p.endsWith('.wasm') && wasmUrl ? wasmUrl : dir + p), + print: (m) => onLog?.('log', m), + printErr: (m) => onLog?.('err', m), + }); +} + +export function hasExport(module, name) { + return typeof module[`_${name}`] === 'function'; +} diff --git a/website/playground/webgpu/src/runtime/executorch.js b/website/playground/webgpu/src/runtime/executorch.js new file mode 100644 index 00000000000..701f878e71d --- /dev/null +++ b/website/playground/webgpu/src/runtime/executorch.js @@ -0,0 +1,143 @@ +// The ExecuTorch Wasm runtime with its WebGPU backend (built from the public +// ExecuTorch sources; see runtime/BUILD_INFO.md), shared by every model on the +// page: one instance, one WebGPU device, one heap. The loader script ships in +// runtime/; its .wasm downloads from Hugging Face like a model file. +// +// Calls into the runtime can suspend (Asyncify) while the GPU works, and a +// second call must not enter it meanwhile, so every use goes through +// withRuntime(), which runs one caller at a time. +import { RUNTIME_WASM } from '../config/runtime.js'; +import { expectSha256, fetchWithProgress } from '../loader/fetch-progress.js'; +import { instantiate } from './emscripten.js'; + +const SCRIPT = new URL('../../runtime/executorch_wasm_lib.js', import.meta.url).href; +const logs = []; // the last 50 runtime log lines, as { n, text } +let logged = 0; +let ready = null; +let queue = Promise.resolve(); + +function start() { + ready ??= (async () => { + if (!globalThis.navigator?.gpu) throw new Error('WebGPU is not available in this browser.'); + expectSha256(RUNTIME_WASM.url, RUNTIME_WASM.sha256); + const wasmBinary = await fetchWithProgress(RUNTIME_WASM.url, { expectedBytes: RUNTIME_WASM.bytes }); + const et = await instantiate(SCRIPT, 'createExecuTorch', { + wasmBinary, + onLog: (kind, text) => { + logs.push({ n: ++logged, text }); + if (logs.length > 50) logs.shift(); + console.debug(`[ExecuTorch] ${text}`); + }, + }); + try { + await et.initWebGPU(); + } catch (err) { + throw new Error(`No WebGPU device available to the ExecuTorch runtime: ${err.message ?? err}`); + } + return et; + })(); + ready.catch(() => (ready = null)); + return ready; +} + +// Starts the runtime (once) without queueing behind other callers. +export function startRuntime() { + return start().then(() => {}); +} + +// Runs fn(et) with exclusive use of the runtime. A failure carries the first +// error the runtime logged meanwhile, which usually names the failing op. +export function withRuntime(fn) { + const run = queue.then(async () => { + const et = await start(); + const seen = logged; + try { + return await fn(et); + } catch (err) { + const fresh = logs.filter((l) => l.n > seen).map((l) => l.text); + const detail = fresh.find((t) => t.startsWith('E ')) ?? fresh.at(-1) ?? ''; + const message = describe(et, err); + throw new Error(detail && !message.includes(detail) ? `${message} (${detail})` : message); + } + }); + queue = run.catch(() => {}); + return run; +} + +// The runtime throws Error objects for its own checks, but C++ exceptions and +// file system errors arrive as other objects. +function describe(et, err) { + if (err instanceof Error && err.message) return err.message; + try { + const [type, message] = et.getExceptionMessage(err); + return message ? `${type}: ${message}` : type; + } catch { + return err?.message ?? (err?.errno != null ? `file system error ${err.errno}` : String(err)); + } +} + +// Wasm-side tensors. Pass these to forward() and delete them when done; the +// runtime copies `data` in, so the typed array can be reused straight away. +export const f32 = (et, shape, data) => et.Tensor.fromArray(shape, data, et.ScalarType.Float); +export const i64 = (et, shape, values) => et.Tensor.fromArray(shape, BigInt64Array.from(values, BigInt), et.ScalarType.Long); + +// Copies every output out of the runtime and frees it. Output memory belongs +// to the program and is overwritten by its next run. +export function readOutputs(outputs) { + const out = outputs.map((t) => ({ data: t.data.slice(), shape: Array.from(t.sizes) })); + for (const t of outputs) t.delete(); + return out; +} + +// One .pte program with its `forward` method loaded on the GPU. +export class Program { + static #files = 0; + + // Must run inside withRuntime(). The runtime takes ownership of `bytes`. + static async load(et, bytes, name) { + const path = `/program-${++Program.#files}.pte`; + let module = null; + try { + et.FS.writeFile(path, bytes, { canOwn: true }); + module = et.Module.load(path); + await module.loadMethod('forward'); + } catch (err) { + module?.delete(); + throw new Error(`The runtime could not load the ${name}: ${describe(et, err)}`); + } finally { + // The loader keeps its own handle; the file only has to outlive loadMethod. + try { + et.FS.unlink(path); + } catch { + // Never written. + } + } + return new Program(et, module, name); + } + + constructor(et, module, name) { + this.et = et; + this.module = module; + this.name = name; + } + + // Shape of each tensor input, from the program itself. + inputShapes() { + return this.module.getMethodMeta('forward').inputTensorMeta.map((m) => (m ? Array.from(m.sizes) : null)); + } + + // Must run inside withRuntime(). Inputs are runtime tensors; the caller owns + // and deletes them. Returns the runtime's output tensors (see readOutputs). + async forward(inputs) { + try { + return await this.module.forward(inputs); + } catch (err) { + throw new Error(`The ${this.name} failed to run: ${describe(this.et, err)}`); + } + } + + delete() { + this.module?.delete(); + this.module = null; + } +} diff --git a/website/playground/webgpu/src/runtime/forward.js b/website/playground/webgpu/src/runtime/forward.js new file mode 100644 index 00000000000..fcc444e431c --- /dev/null +++ b/website/playground/webgpu/src/runtime/forward.js @@ -0,0 +1,85 @@ +// A single-graph ExecuTorch model on WebGPU, run through the shared runtime: +// set inputs, call forward, read back every output. +import { fetchWithProgress } from '../loader/fetch-progress.js'; +import { f32, Program, readOutputs, startRuntime, withRuntime } from './executorch.js'; + +export class ForwardModel { + // onProgress receives {stage: 'runtime' | 'weights' | 'gpu', fraction, loaded?, total?, fromCache?} + // Models loaded this session, by program URL, so revisiting a demo (or another + // demo that uses the same model) skips loading entirely. + static #loaded = new Map(); + + static isLoaded(source) { + return Boolean(source?.spec && ForwardModel.#loaded.get(source.url(source.spec.pte))?.alive); + } + + static async load(source, { onProgress = () => {}, signal } = {}) { + const { spec, url } = source; + const key = url(spec.pte); + const existing = ForwardModel.#loaded.get(key); + if (existing?.alive) { + for (const stage of ['runtime', 'weights', 'gpu']) onProgress({ stage, fraction: 1, fromCache: true }); + return existing; + } + const abortCheck = () => { + if (signal?.aborted) throw new DOMException('Aborted', 'AbortError'); + }; + onProgress({ stage: 'runtime', fraction: 0 }); + const [, bytes] = await Promise.all([ + startRuntime().then(() => onProgress({ stage: 'runtime', fraction: 1 })), + fetchWithProgress(key, { + signal, + expectedBytes: spec.pteBytes, + onProgress: (p) => onProgress({ stage: 'weights', fraction: p.total ? p.loaded / p.total : 0, ...p }), + }), + ]); + abortCheck(); + onProgress({ stage: 'gpu', fraction: 0 }); + const program = await withRuntime((et) => Program.load(et, bytes, 'model')); + const model = new ForwardModel(program); + if (signal?.aborted) model.dispose(); + abortCheck(); + ForwardModel.#loaded.set(key, model); + onProgress({ stage: 'gpu', fraction: 1 }); + return model; + } + + constructor(program) { + this.program = program; + this.lastMs = null; + } + + // inputs: [{data: Float32Array, shape: number[]}] in the program's argument order. + // -> [{data: Float32Array, shape: number[]}] for every output. + run(inputs) { + return withRuntime(async (et) => { + if (!this.program) throw new Error('This model was unloaded'); + const t0 = performance.now(); + const tensors = inputs.map((t) => f32(et, t.shape, t.data)); + const t1 = performance.now(); + let outputs; + try { + outputs = await this.program.forward(tensors); + } finally { + for (const t of tensors) t.delete(); + } + const t2 = performance.now(); + const result = readOutputs(outputs); + const t3 = performance.now(); + this.lastMs = t3 - t0; + // Where one run's time went, for the on-page timing breakdown. + this.lastTiming = { upload: t1 - t0, forward: t2 - t1, readback: t3 - t2 }; + return result; + }); + } + + get alive() { + return Boolean(this.program); + } + + dispose() { + const program = this.program; + this.program = null; + if (program) withRuntime(() => program.delete()); + } +} diff --git a/website/playground/webgpu/src/runtime/llm.js b/website/playground/webgpu/src/runtime/llm.js new file mode 100644 index 00000000000..f6d10122a7f --- /dev/null +++ b/website/playground/webgpu/src/runtime/llm.js @@ -0,0 +1,250 @@ +// A real ExecuTorch LLM on WebGPU: one .pte with an in-graph KV cache, driven +// token by token from JS through the shared runtime. +// +// The program's forward takes `tokens` [1, n] and `input_pos` [1] and returns +// the logits of the last token. Prefill feeds the prompt in chunks of up to +// `prefillMax` tokens; decode feeds one token at a time and samples in JS. +import { fetchWithProgress } from '../loader/fetch-progress.js'; +import { i64, Program, startRuntime, withRuntime } from './executorch.js'; +import { importVerified } from '../loader/import-verified.js'; + +async function loadTokenizer(lib, rootUrl, dir) { + const tj = await importVerified(lib); + tj.env.allowLocalModels = true; + tj.env.allowRemoteModels = false; + tj.env.useBrowserCache = false; + tj.env.localModelPath = rootUrl; + return tj.AutoTokenizer.from_pretrained(dir.replace(/\/$/, '')); +} + +export class ExecuTorchLLM { + // stages reported through onProgress: runtime, weights (bytes), tokenizer, gpu + static async load(source, { onProgress = () => {}, signal } = {}) { + const { spec, url } = source; + onProgress({ stage: 'runtime', fraction: 0 }); + const [, pte, tokenizer] = await Promise.all([ + startRuntime().then(() => onProgress({ stage: 'runtime', fraction: 1 })), + fetchWithProgress(url(spec.pte), { + signal, + expectedBytes: spec.pteBytes, + onProgress: (p) => onProgress({ stage: 'weights', fraction: p.total ? p.loaded / p.total : 0, ...p }), + }), + loadTokenizer(source.shared.tokenizerLib, url(''), spec.tokenizer).then((t) => { + onProgress({ stage: 'tokenizer', fraction: 1 }); + return t; + }), + ]); + if (signal?.aborted) throw new DOMException('Aborted', 'AbortError'); + + onProgress({ stage: 'gpu', fraction: 0 }); + const program = await withRuntime((et) => Program.load(et, pte, 'model')); + onProgress({ stage: 'gpu', fraction: 1 }); + return new ExecuTorchLLM(program, tokenizer, spec); + } + + constructor(program, tokenizer, spec) { + this.program = program; + this.tokenizer = tokenizer; + this.spec = spec; + this.maxCtx = spec.maxCtx; + this.prefillMax = spec.prefillMax || 1; + this.eos = new Set(spec.eos); + this.committed = []; + this.busy = Promise.resolve(); + } + + get pos() { + return this.committed.length; + } + + encodeChat(messages) { + const chat = this.spec.chat ?? 'template'; + let ids = null; + if (chat === 'template') { + try { + const enc = this.tokenizer.apply_chat_template(messages, { add_generation_prompt: true, tokenize: true, return_tensor: false }); + ids = Array.isArray(enc) ? enc.flat(Infinity).map(Number) : null; + } catch { + ids = null; + } + } + if (!ids?.length) { + // ChatML, the format of the Qwen family. "nothink" pre-closes Qwen 3's + // reasoning block, which is what its template does with thinking disabled. + let s = ''; + for (const m of messages) s += `<|im_start|>${m.role}\n${m.content}<|im_end|>\n`; + s += '<|im_start|>assistant\n'; + if (chat === 'chatml-nothink') s += '\n\n\n\n'; + ids = Array.from(this.tokenizer.encode(s, { add_special_tokens: false })).map(Number); + } + return ids; + } + + countTokens(text) { + return this.tokenizer.encode(text, { add_special_tokens: false }).length; + } + + reset() { + this.committed = []; + } + + // Feeds tokens at positions [pos, pos + tokens.length) and returns the logits + // that follow the last one. + #forward(tokens, pos) { + return withRuntime(async (et) => { + if (!this.program) throw new Error('This model was unloaded'); + const inputs = [i64(et, [1, tokens.length], tokens), i64(et, [1], [pos])]; + let outputs; + try { + outputs = await this.program.forward(inputs); + } finally { + for (const t of inputs) t.delete(); + } + const logits = outputs[0].data.slice(); + for (const t of outputs) t.delete(); + return logits; + }); + } + + // Fills KV positions [from, to) and returns the logits after the last token. + async #prefill(ids, from, to, onChunk) { + let logits = null; + for (let off = from; off < to; ) { + const count = Math.min(this.prefillMax, to - off); + logits = await this.#forward(ids.slice(off, off + count), off); + off += count; + onChunk?.(off - from, to - from); + } + return logits; + } + + // Frees the program on the GPU once any running generation has finished. + dispose() { + this.busy = this.busy.then(() => + withRuntime(() => { + this.program?.delete(); + this.program = null; + }), + ); + } + + // Streams a reply to `messages`. The KV cache persists across turns, so only the + // part of the prompt that differs from what is already resident is prefilled. + async generate(messages, { maxNewTokens = 512, sampling = {}, signal, onPrefill, onText } = {}) { + const run = this.busy.then(() => this.#generate(messages, { maxNewTokens, sampling, signal, onPrefill, onText })); + this.busy = run.catch(() => {}); + return run; + } + + async #generate(messages, { maxNewTokens, sampling, signal, onPrefill, onText }) { + const ids = this.encodeChat(messages); + if (ids.length + 16 > this.maxCtx) throw new Error(`The conversation (${ids.length} tokens) no longer fits the ${this.maxCtx}-token context.`); + const budget = Math.min(maxNewTokens, this.maxCtx - ids.length - 1); + + let keep = 0; + while (keep < this.committed.length && keep < ids.length - 1 && this.committed[keep] === ids[keep]) keep++; + const t0 = performance.now(); + this.committed = ids.slice(0, keep); + let logits = await this.#prefill(ids, keep, ids.length, onPrefill); + this.committed = ids.slice(); + + const sampler = makeSampler(sampling); + const out = []; + let text = ''; + let ttft = 0; + let stop = 'length'; + // `committed` always lists exactly the tokens resident in the KV cache; the last + // sampled token is only fed back when another one is wanted. + const decodeStart = { t: 0 }; + for (let n = 0; n < budget; n++) { + const next = sampler(logits, this.committed); + if (next < 0) throw new Error('The model produced no usable logits (all values were non-finite).'); + if (n === 0) { + ttft = performance.now() - t0; + decodeStart.t = performance.now(); + } + if (this.eos.has(next)) { + stop = 'eos'; + break; + } + out.push(next); + const full = this.tokenizer.decode(out, { skip_special_tokens: true }); + if (full.length > text.length && !full.endsWith('�')) { + onText?.(full.slice(text.length), full); + text = full; + } + if (signal?.aborted) { + stop = 'aborted'; + break; + } + if (n + 1 < budget) { + logits = await this.#forward([next], this.committed.length); + this.committed.push(next); + } + } + const final = this.tokenizer.decode(out, { skip_special_tokens: true }); + if (final.length > text.length) onText?.(final.slice(text.length), final); + const decodeMs = performance.now() - decodeStart.t; + return { + text: final, + stop, + promptTokens: ids.length, + prefilledTokens: ids.length - keep, + newTokens: out.length, + ttftMs: ttft, + decodeTps: out.length > 1 ? (out.length - 1) / (decodeMs / 1000) : null, + }; + } +} + +// Temperature + top-k + top-p with a repetition penalty over recent context. +export function makeSampler({ temperature = 0.7, topK = 40, topP = 0.9, repetitionPenalty = 1.1, window = 96 } = {}) { + return (logits, context) => { + if (repetitionPenalty !== 1) { + const seen = new Set(context.slice(-window)); + for (const t of seen) { + if (t < logits.length) logits[t] = logits[t] > 0 ? logits[t] / repetitionPenalty : logits[t] * repetitionPenalty; + } + } + if (temperature <= 0) { + let best = 0; + for (let i = 1; i < logits.length; i++) if (logits[i] > logits[best]) best = i; + return best; + } + const k = Math.min(topK, logits.length); + const top = []; + for (let i = 0; i < logits.length; i++) { + const v = logits[i]; + if (!Number.isFinite(v)) continue; + if (top.length < k) { + top.push([v, i]); + if (top.length === k) top.sort((a, b) => a[0] - b[0]); + } else if (v > top[0][0]) { + top[0] = [v, i]; + for (let j = 0; j + 1 < k && top[j][0] > top[j + 1][0]; j++) [top[j], top[j + 1]] = [top[j + 1], top[j]]; + } + } + if (!top.length) return -1; + top.sort((a, b) => b[0] - a[0]); + const max = top[0][0]; + let sum = 0; + const probs = top.map(([v]) => { + const p = Math.exp((v - max) / temperature); + sum += p; + return p; + }); + let cut = 0; + let acc = 0; + while (cut < probs.length) { + acc += probs[cut] / sum; + cut++; + if (acc >= topP) break; + } + let r = Math.random() * acc * sum; + for (let i = 0; i < cut; i++) { + r -= probs[i]; + if (r <= 0) return top[i][1]; + } + return top[cut - 1][1]; + }; +} diff --git a/website/playground/webgpu/src/runtime/model-storage.js b/website/playground/webgpu/src/runtime/model-storage.js new file mode 100644 index 00000000000..ae4eb10f306 --- /dev/null +++ b/website/playground/webgpu/src/runtime/model-storage.js @@ -0,0 +1,59 @@ +// What each model has stored on this device, and removing it. Model files live +// in the browser's Cache Storage (written by fetchWithProgress), keyed by URL. +import { evictFromCache, MODEL_CACHE } from '../loader/fetch-progress.js'; +import { resolveModelSource } from './sources.js'; + +const FILE = /\.(pte|ptd|bin|json|txt|model|gguf|onnx|wasm)$/i; + +// Runtime specs nest file paths in different shapes (a `pte` string, a `files` +// map, a list of `{ path }`), so collect every string that looks like a file. +function filePaths(value, out = new Set()) { + if (typeof value === 'string') { + if (FILE.test(value)) out.add(value); + } else if (Array.isArray(value)) { + for (const v of value) filePaths(v, out); + } else if (value && typeof value === 'object') { + // The runtime build is shared by several models and isn't part of any one of them. + for (const [k, v] of Object.entries(value)) if (k !== 'runtime') filePaths(v, out); + } + return out; +} + +export async function modelFileUrls(model) { + const source = await resolveModelSource(model); + if (source.kind === 'remote' || source.kind === 'dev') return [...filePaths(source.spec)].map((p) => source.url(p)); + return []; +} + +// -> { urls: string[] (the ones actually cached), bytes } +export async function storedFiles(model) { + if (!globalThis.caches) return { urls: [], bytes: 0 }; + const cache = await caches.open(MODEL_CACHE); + const urls = []; + let bytes = 0; + for (const url of await modelFileUrls(model)) { + const hit = await cache.match(url); + if (!hit) continue; + urls.push(url); + bytes += Number(hit.headers.get('content-length')) || 0; + } + return { urls, bytes }; +} + +export async function deleteStoredModel(model) { + const { urls, bytes } = await storedFiles(model); + await evictFromCache(urls); + return { removed: urls.length, bytes }; +} + +const WEIGHTS = /\.(pte|ptd|bin)$/i; + +// True when every weight file the model needs is already in this browser. +export async function isOnDevice(model) { + if (!globalThis.caches) return false; + const urls = (await modelFileUrls(model)).filter((u) => WEIGHTS.test(new URL(u).pathname)); + if (!urls.length) return false; + const cache = await caches.open(MODEL_CACHE); + for (const url of urls) if (!(await cache.match(url))) return false; + return true; +} diff --git a/website/playground/webgpu/src/runtime/sam.js b/website/playground/webgpu/src/runtime/sam.js new file mode 100644 index 00000000000..74117bde8a8 --- /dev/null +++ b/website/playground/webgpu/src/runtime/sam.js @@ -0,0 +1,462 @@ +// Segment Anything 2 on WebGPU: two .pte programs on the shared runtime. +// +// The heavy image encoder runs once per image, and its outputs stay in the +// runtime as decoder inputs. Every prompt after that runs only the small mask +// decoder, which is what makes clicking feel instant. The prompt encoder is +// tiny, so it runs here in JS: each point becomes a random-Fourier positional +// encoding plus a learned embedding for its kind (positive, negative, box +// corner, padding). +import { fetchWithProgress } from '../loader/fetch-progress.js'; +import { f32, Program, readOutputs, startRuntime, withRuntime } from './executorch.js'; + +export const LABEL = { NEGATIVE: 0, POSITIVE: 1 }; +const STABILITY_OFFSET = 1; + +export class SegmentAnything { + // stages reported through onProgress: runtime, weights (bytes), prompts, gpu + static async load(source, { onProgress = () => {}, signal } = {}) { + const { spec, url } = source; + const aborted = () => { + if (signal?.aborted) throw new DOMException('Aborted', 'AbortError'); + }; + + onProgress({ stage: 'runtime', fraction: 0 }); + const expected = (spec.encoderBytes ?? 0) + (spec.decoderBytes ?? 0); + const got = { encoder: 0, decoder: 0 }; + const report = (key) => (p) => { + got[key] = p.loaded; + const loaded = got.encoder + got.decoder; + onProgress({ stage: 'weights', fraction: expected ? Math.min(1, loaded / expected) : 0, loaded, total: expected, fromCache: p.fromCache }); + }; + const floats = (path) => fetchWithProgress(url(path), { signal }).then((b) => new Float32Array(b.buffer, b.byteOffset, b.byteLength / 4)); + const p = spec.prompts; + const [, encoderBytes, decoderBytes, tables] = await Promise.all([ + startRuntime().then(() => onProgress({ stage: 'runtime', fraction: 1 })), + fetchWithProgress(url(spec.encoder), { signal, expectedBytes: spec.encoderBytes, onProgress: report('encoder') }), + fetchWithProgress(url(spec.decoder), { signal, expectedBytes: spec.decoderBytes, onProgress: report('decoder') }), + Promise.all([p.gaussian, p.pointNegative, p.pointPositive, p.boxTopLeft, p.boxBottomRight, p.notAPoint, p.imagePositional, p.dense].map(floats)).then((t) => { + onProgress({ stage: 'prompts', fraction: 1 }); + return t; + }), + ]); + aborted(); + + onProgress({ stage: 'gpu', fraction: 0 }); + const [gaussian, negative, positive, boxTL, boxBR, notAPoint, imagePos, dense] = tables; + const size = spec.imageSize ?? 512; + const engine = await withRuntime(async (et) => { + const encoder = await Program.load(et, encoderBytes, 'image encoder'); + onProgress({ stage: 'gpu', fraction: 0.5 }); + let decoder; + try { + decoder = await Program.load(et, decoderBytes, 'mask decoder'); + } catch (err) { + encoder.delete(); + throw err; + } + // The decoder's inputs: image embeddings, positional grid, sparse prompt + // [1, 1, tokens, hidden], dense prompt, then two high-resolution features. + // The grid and the no-mask dense embedding are the same for every prompt. + const shapes = decoder.inputShapes(); + const constants = { imagePos: f32(et, shapes[1], imagePos), dense: f32(et, shapes[3], dense) }; + return new SegmentAnything(encoder, decoder, constants, spec, { size, tokens: shapes[2][2] }, { gaussian, embeds: [negative, positive, boxTL, boxBR], notAPoint }); + }); + onProgress({ stage: 'gpu', fraction: 1 }); + if (signal?.aborted) engine.dispose(); + aborted(); + return engine; + } + + constructor(encoder, decoder, constants, spec, { size, tokens }, prompt) { + this.encoder = encoder; + this.decoder = decoder; + this.constants = constants; + this.size = size; + this.hidden = spec.hidden ?? 256; + this.mean = spec.mean ?? [0.485, 0.456, 0.406]; + this.std = spec.std ?? [0.229, 0.224, 0.225]; + this.prompt = prompt; + this.image = null; + this.features = null; + this.cache = new Map(); + // How many prompt tokens one decode takes; richer prompts are composed from + // several decodes (see planPrompt). + this.fixedTokens = tokens; + this.decodeCount = 0; + this.decodeMs = null; + this.busy = Promise.resolve(); + this.pixels = new Float32Array(3 * size * size); + const canvas = typeof OffscreenCanvas === 'function' ? new OffscreenCanvas(size, size) : Object.assign(document.createElement('canvas'), { width: size, height: size }); + this.ctx = canvas.getContext('2d', { willReadFrequently: true }); + } + + // One call into the runtime at a time; a failed call must not wedge the queue. + #serial(fn) { + const run = this.busy.then(fn); + this.busy = run.catch(() => {}); + return run; + } + + // Encodes an image (anything drawImage accepts). Prompts are in this image's + // pixel coordinates until the next setImage. + setImage(img, { width, height } = {}) { + const w = width ?? img.naturalWidth ?? img.videoWidth ?? img.width; + const hgt = height ?? img.naturalHeight ?? img.videoHeight ?? img.height; + return this.#serial(async () => { + this.image = null; + this.cache = new Map(); + this.#preprocess(img); + const t0 = performance.now(); + await withRuntime(async (et) => { + this.#freeFeatures(); + const input = f32(et, [1, 3, this.size, this.size], this.pixels); + let outputs; + try { + outputs = await this.encoder.forward([input]); + } finally { + input.delete(); + } + // Copied out first: creating a tensor can grow the heap under a view. + const [emb, hr0, hr1] = readOutputs(outputs); + this.features = { emb: f32(et, emb.shape, emb.data), hr0: f32(et, hr0.shape, hr0.data), hr1: f32(et, hr1.shape, hr1.data) }; + }); + const ms = performance.now() - t0; + this.image = { width: w, height: hgt }; + return { ms }; + }); + } + + #freeFeatures() { + if (!this.features) return; + for (const t of Object.values(this.features)) t.delete(); + this.features = null; + } + + #preprocess(img) { + const S = this.size; + const ctx = this.ctx; + ctx.imageSmoothingEnabled = true; + ctx.imageSmoothingQuality = 'high'; + // SAM 2 resizes to a square without keeping aspect; masks map back the same way. + ctx.drawImage(img, 0, 0, S, S); + const rgba = ctx.getImageData(0, 0, S, S).data; + const plane = S * S; + const px = this.pixels; + const [m0, m1, m2] = this.mean; + const [s0, s1, s2] = this.std; + for (let i = 0; i < plane; i++) { + px[i] = (rgba[i * 4] / 255 - m0) / s0; + px[plane + i] = (rgba[i * 4 + 1] / 255 - m1) / s1; + px[2 * plane + i] = (rgba[i * 4 + 2] / 255 - m2) / s2; + } + } + + // points: [{x, y, label}] with label 1 (include) or 0 (exclude); box: {x0, y0, x1, y1} + // in image pixels. `detail` picks among the decoder's granularities: 'auto', or + // 0..n-1 from smallest to largest. Returns combined logits at decoder resolution; + // `mask` upsamples them to the image size on first access. + segment({ points = [], box = null, detail = 'auto' } = {}) { + return this.#serial(async () => { + if (!this.image) throw new Error('Set an image before segmenting'); + const t0 = performance.now(); + const before = this.decodeCount; + const units = planPrompt({ points, box, fixedTokens: this.fixedTokens }); + const decoded = []; + for (const unit of units) { + const keep = []; + const cut = []; + for (const t of unit.keep) keep.push(await this.#decode(t)); + for (const t of unit.cut) cut.push(await this.#decode(t)); + decoded.push({ keep, cut }); + } + const result = composeMasks(decoded, { detail, ...this.image }); + result.ms = performance.now() - t0; + result.decodes = this.decodeCount - before; + return result; + }); + } + + // Decodes one prompt (a list of tokens), memoised per image so undo and + // re-picking a granularity never rerun the decoder. + async #decode(tokens) { + const key = JSON.stringify(tokens); + const hit = this.cache.get(key); + if (hit) return hit; + if (tokens.length !== this.fixedTokens) throw new Error(`The mask decoder takes ${this.fixedTokens} prompt tokens, not ${tokens.length}`); + const { width, height } = this.image; + const data = encodePromptTokens(tokens, { width, height, size: this.size, hidden: this.hidden, ...this.prompt }); + const t0 = performance.now(); + const [masks] = await withRuntime(async (et) => { + const { emb, hr0, hr1 } = this.features; + const sparse = f32(et, [1, 1, tokens.length, this.hidden], data); + try { + return readOutputs(await this.decoder.forward([emb, this.constants.imagePos, sparse, this.constants.dense, hr0, hr1])); + } finally { + sparse.delete(); + } + }); + this.decodeMs = performance.now() - t0; + this.decodeCount++; + // masks [1, n, h, w]: mask logits for mask tokens 1..n (part, object, group). + const [n, lh, lw] = masks.shape.slice(-3); + const out = { all: masks.data, n, lw, lh, candidates: scoreMasks(masks.data, n, lw * lh) }; + this.cache.set(key, out); + return out; + } + + dispose() { + this.busy.finally(() => + withRuntime(() => { + this.#freeFeatures(); + for (const t of Object.values(this.constants)) t.delete(); + this.encoder.delete(); + this.decoder.delete(); + }), + ); + } +} + +const tok = (kind, x = 0, y = 0) => ({ kind, x: Math.round(x * 10) / 10, y: Math.round(y * 10) / 10 }); + +// Turns a prompt into decoder calls. SAM 2's native prompt is every point and +// box corner, then one padding token ("not a point"). A decoder that takes that +// many tokens (or any number) gets exactly that, padded to its size. A smaller +// fixed decoder gets a composition of two-token prompts instead: +// - each include point is decoded with each exclude point as its partner (no +// excludes: with padding), and the pair masks are intersected; +// - a box is decoded on its own, and each exclude point cuts out the smallest +// confident segment under it; +// - the object is the union of all of the above. +export function planPrompt({ points = [], box = null, fixedTokens = null }) { + const pos = points.filter((p) => p.label !== LABEL.NEGATIVE).map((p) => tok('pos', p.x, p.y)); + const neg = points.filter((p) => p.label === LABEL.NEGATIVE).map((p) => tok('neg', p.x, p.y)); + const corners = box + ? [tok('tl', Math.min(box.x0, box.x1), Math.min(box.y0, box.y1)), tok('br', Math.max(box.x0, box.x1), Math.max(box.y0, box.y1))] + : []; + if (!pos.length && !box) return []; + const fill = (tokens) => (fixedTokens == null ? tokens : [...tokens, ...Array(Math.max(0, fixedTokens - tokens.length)).fill(tok('pad'))]); + const native = [...pos, ...neg, ...corners, tok('pad')]; + if (fixedTokens == null || native.length <= fixedTokens) return [{ keep: [fill(native)], cut: [] }]; + const units = pos.map((p) => ({ keep: neg.length ? neg.map((n) => fill([p, n])) : [fill([p, tok('pad')])], cut: [] })); + if (box) units.push({ keep: [fill(corners)], cut: neg.map((n) => fill([tok('pos', n.x, n.y), tok('pad')])) }); + return units; +} + +// Combines decoded units in logit space (union = max, intersection = min, +// cut = min with the negated cut), which keeps a smooth zero crossing for the +// outline instead of a jagged boolean edge. +export function composeMasks(units, { detail = 'auto', width, height }) { + const first = units[0]?.keep[0]; + const lw = first?.lw ?? 1; + const lh = first?.lh ?? 1; + const plane = lw * lh; + const logits = new Float32Array(plane).fill(-20); + const scores = []; + for (const u of units) { + const acc = new Float32Array(plane).fill(Infinity); + for (const d of u.keep) { + const c = pickMask(d.candidates, detail); + scores.push(d.candidates[c].stability); + const src = d.all.subarray(c * plane, (c + 1) * plane); + for (let i = 0; i < plane; i++) if (src[i] < acc[i]) acc[i] = src[i]; + } + for (const d of u.cut) { + const c = smallestConfident(d.candidates); + const src = d.all.subarray(c * plane, (c + 1) * plane); + for (let i = 0; i < plane; i++) if (-src[i] < acc[i]) acc[i] = -src[i]; + } + for (let i = 0; i < plane; i++) if (acc[i] > logits[i]) logits[i] = acc[i]; + } + let on = 0; + for (let i = 0; i < plane; i++) if (logits[i] > 0) on++; + let mask = null; + return { + logits, + logitsWidth: lw, + logitsHeight: lh, + granularities: first?.n ?? 0, + score: scores.length ? Math.min(...scores) : 0, + area: on / plane, + width, + height, + get mask() { + mask ??= upsampleMask(logits, lw, lh, width, height); + return mask; + }, + }; +} + +function pickMask(candidates, detail) { + if (detail === 'auto' || detail == null) return bestMask(candidates); + const bySize = [...candidates].sort((a, b) => a.area - b.area); + return bySize[Math.max(0, Math.min(bySize.length - 1, detail))].index; +} + +function smallestConfident(candidates) { + const ok = candidates.filter((c) => c.area > 0.0005 && c.stability > 0.6).sort((a, b) => a.area - b.area); + return (ok[0] ?? [...candidates].sort((a, b) => a.area - b.area)[0]).index; +} + +const KIND = { neg: 0, pos: 1, tl: 2, br: 3 }; + +// Builds the decoder's sparse prompt tensor [1, 1, tokens, hidden]. Coordinates are +// mapped into the square encoder frame and encoded as [sin(2πcG), cos(2πcG)] plus +// the learned embedding for the token's kind; padding is "not a point" alone. +export function encodePromptTokens(tokens, { width, height, size, hidden, gaussian, embeds, notAPoint }) { + const D = hidden / 2; + const data = new Float32Array(tokens.length * hidden); + tokens.forEach((t, row) => { + const o = row * hidden; + if (t.kind === 'pad') { + data.set(notAPoint.subarray(0, hidden), o); + return; + } + const embed = embeds[KIND[t.kind]]; + const cx = (2 * (t.x * (size / width) + 0.5)) / size - 1; + const cy = (2 * (t.y * (size / height) + 0.5)) / size - 1; + for (let k = 0; k < D; k++) { + const a = 2 * Math.PI * (cx * gaussian[k] + cy * gaussian[D + k]); + data[o + k] = Math.sin(a) + embed[k]; + data[o + D + k] = Math.cos(a) + embed[D + k]; + } + }); + return data; +} + +// Stability = IoU between the mask thresholded at +1 and at -1: a mask whose +// boundary barely moves when the threshold does is a confident one. +export function scoreMasks(all, n, plane) { + const out = []; + for (let c = 0; c < n; c++) { + let pos = 0; + let hi = 0; + let lo = 0; + const off = c * plane; + for (let i = 0; i < plane; i++) { + const v = all[off + i]; + if (v > 0) pos++; + if (v > STABILITY_OFFSET) hi++; + if (v > -STABILITY_OFFSET) lo++; + } + out.push({ index: c, area: pos / plane, stability: lo ? hi / lo : 0 }); + } + return out; +} + +// The decoder returns several granularities (part, object, group). Its IoU head +// often disagrees with upstream SAM 2 at this input size, so prefer stable masks +// and break ties toward the whole object rather than a sliver. Near-empty and +// near-full masks lose. +export function bestMask(candidates) { + let best = 0; + let bestScore = -Infinity; + for (const c of candidates) { + if (c.area < 0.0005 || c.area > 0.97) continue; + const s = c.stability + Math.min(c.area, 0.5); + if (s > bestScore) { + bestScore = s; + best = c.index; + } + } + return best; +} + +// Bilinear upsample of mask logits (cell-centred, like align_corners=false), then +// threshold at 0. Returns 0/1 per pixel. +export function upsampleMask(logits, lw, lh, width, height) { + const out = new Uint8Array(width * height); + const sx = lw / width; + const sy = lh / height; + for (let y = 0; y < height; y++) { + const fy = Math.min(lh - 1, Math.max(0, (y + 0.5) * sy - 0.5)); + const y0 = Math.floor(fy); + const y1 = Math.min(lh - 1, y0 + 1); + const ty = fy - y0; + for (let x = 0; x < width; x++) { + const fx = Math.min(lw - 1, Math.max(0, (x + 0.5) * sx - 0.5)); + const x0 = Math.floor(fx); + const x1 = Math.min(lw - 1, x0 + 1); + const tx = fx - x0; + const top = logits[y0 * lw + x0] * (1 - tx) + logits[y0 * lw + x1] * tx; + const bot = logits[y1 * lw + x0] * (1 - tx) + logits[y1 * lw + x1] * tx; + out[y * width + x] = top * (1 - ty) + bot * ty > 0 ? 1 : 0; + } + } + return out; +} + +// Sub-pixel outlines of the zero level set, as closed polylines in logit-grid +// coordinates (cell centres at integers). Marching squares over a grid padded +// with background, so every contour closes; filled with the even-odd rule, the +// loops reproduce holes too. +export function maskContours(logits, w, h) { + const W = w + 2; + const H = h + 2; + const v = (x, y) => (x <= 0 || y <= 0 || x > w || y > h ? -1 : logits[(y - 1) * w + (x - 1)]); + // Edge ids: horizontal edge (x,y)-(x+1,y) -> 2*(y*W+x); vertical (x,y)-(x,y+1) -> +1. + const point = new Map(); + const cross = (id, ax, ay, bx, by) => { + if (!point.has(id)) { + const a = v(ax, ay); + const b = v(bx, by); + const t = a === b ? 0.5 : a / (a - b); + point.set(id, [ax + (bx - ax) * t - 1, ay + (by - ay) * t - 1]); + } + return id; + }; + const links = new Map(); + const link = (a, b) => { + (links.get(a) ?? links.set(a, []).get(a)).push(b); + (links.get(b) ?? links.set(b, []).get(b)).push(a); + }; + for (let y = 0; y < H - 1; y++) { + for (let x = 0; x < W - 1; x++) { + const tl = v(x, y) > 0; + const tr = v(x + 1, y) > 0; + const br = v(x + 1, y + 1) > 0; + const bl = v(x, y + 1) > 0; + const code = (tl ? 8 : 0) | (tr ? 4 : 0) | (br ? 2 : 0) | (bl ? 1 : 0); + if (code === 0 || code === 15) continue; + const top = () => cross(2 * (y * W + x), x, y, x + 1, y); + const bottom = () => cross(2 * ((y + 1) * W + x), x, y + 1, x + 1, y + 1); + const left = () => cross(2 * (y * W + x) + 1, x, y, x, y + 1); + const right = () => cross(2 * (y * W + x + 1) + 1, x + 1, y, x + 1, y + 1); + switch (code) { + case 1: case 14: link(left(), bottom()); break; + case 2: case 13: link(bottom(), right()); break; + case 3: case 12: link(left(), right()); break; + case 4: case 11: link(top(), right()); break; + case 6: case 9: link(top(), bottom()); break; + case 7: case 8: link(left(), top()); break; + case 5: case 10: { + const centre = (v(x, y) + v(x + 1, y) + v(x + 1, y + 1) + v(x, y + 1)) / 4 > 0; + if ((code === 5) === centre) { + link(left(), top()); + link(bottom(), right()); + } else { + link(left(), bottom()); + link(top(), right()); + } + break; + } + } + } + } + const loops = []; + const seen = new Set(); + for (const start of links.keys()) { + if (seen.has(start)) continue; + const loop = []; + let prev = -1; + let cur = start; + while (cur !== undefined && !seen.has(cur)) { + seen.add(cur); + loop.push(point.get(cur)); + const next = links.get(cur).find((n) => n !== prev && !seen.has(n)); + prev = cur; + cur = next; + } + if (loop.length >= 3) loops.push(loop); + } + return loops; +} diff --git a/website/playground/webgpu/src/runtime/sources.js b/website/playground/webgpu/src/runtime/sources.js new file mode 100644 index 00000000000..16e935f833c --- /dev/null +++ b/website/playground/webgpu/src/runtime/sources.js @@ -0,0 +1,52 @@ +// Where a real ExecuTorch model's files come from. +// +// Shipped builds resolve weights only through resolveWeights() (license cleared +// AND an https URL). For local review there is a second source: the dev server's +// /__dev__/config.json, which maps model ids to an `assets` block shaped like the +// one in models.js, with `baseUrl` relative to the directory the dev server +// serves from outside the site. A dev entry wins over the hosted one, so a new +// export can be tried before it is published. Production hosts have no such +// route, and the lookup is skipped unless the page is on a loopback host or the +// dev server marked it. Either way the runtime itself is this site's runtime/. +import { expectSha256 } from '../loader/fetch-progress.js'; +import { resolveWeights, WEIGHT_SOURCE } from '../loader/resolve.js'; +import { SHARED } from '../config/shared.js'; + +const LOOPBACK = new Set(['localhost', '127.0.0.1', '[::1]']); +let devConfig = null; + +export function getDevConfig() { + if (!devConfig) { + const page = typeof document !== 'undefined'; + const marked = page && document.querySelector('meta[name="executorch-dev-assets"]'); + devConfig = page && (LOOPBACK.has(location.hostname) || marked) + ? fetch('/__dev__/config.json', { cache: 'no-store' }) + .then((r) => (r.ok ? r.json() : null)) + .catch(() => null) + : Promise.resolve(null); + } + return devConfig; +} + +// -> { kind, spec, url(path), shared, repoUrl? } for an `assets` block +// ({ baseUrl, spec, sha256 }) whose baseUrl is resolved against `base`. +// Registers each file's sha256 so a download that doesn't match is rejected. +export function assetsSource(kind, { baseUrl, spec, sha256 = {} }, base) { + const root = new URL(baseUrl ?? '', base).href; + const dir = root.endsWith('/') ? root : `${root}/`; + const url = (path) => new URL(path, dir).href; + for (const [path, hex] of Object.entries(sha256)) expectSha256(url(path), hex); + const repoUrl = kind === 'remote' && dir.includes('/resolve/') ? dir.split('/resolve/')[0] : null; + return { kind, spec, url, shared: SHARED, repoUrl }; +} + +// -> { kind: 'dev' | 'remote', spec, url(path), shared, repoUrl } | { kind: 'unavailable', reasons } +export async function resolveModelSource(model) { + const dev = await getDevConfig(); + const local = dev?.models?.[model.id]; + if (local?.spec) return assetsSource('dev', local, new URL(dev.base, location.origin)); + const weights = resolveWeights(model); + if (weights.source !== WEIGHT_SOURCE.REMOTE) return { kind: 'unavailable', reasons: weights.reasons }; + if (!model.assets.spec) return { kind: 'unavailable', reasons: [{ code: 'no-spec', text: 'This model has no ExecuTorch program description yet' }] }; + return assetsSource('remote', model.assets); +} diff --git a/website/playground/webgpu/src/runtime/vlm.js b/website/playground/webgpu/src/runtime/vlm.js new file mode 100644 index 00000000000..c31050ceff8 --- /dev/null +++ b/website/playground/webgpu/src/runtime/vlm.js @@ -0,0 +1,347 @@ +// SmolVLM on WebGPU: an Emscripten runtime holding three ExecuTorch programs +// (vision tower, single-token decoder, batched prefill) driven from JS. +// +// image -> 512x512 RGB, normalized to [-1, 1], CHW -> et_run_vision -> 64 x 960 image embeddings +// prompt ids -> rows of the int8 token-embedding table, dequantized on the host, +// with the 64 image rows spliced in where the placeholders sit +// et_prefill_batch fills the KV cache for the whole prompt in one pass (from +// position 0, at most `prefillMax` rows) and returns the first token; +// et_prefill_step_embed / et_step_embed continue one row at a time from any position. +// +// Text embedding happens on the host because the decoder programs take +// embeddings, not token ids. The KV cache survives between questions, so a +// follow-up only prefills the new turn. +import { fetchWithProgress } from '../loader/fetch-progress.js'; +import { hasExport, instantiate } from './emscripten.js'; +import { SHARED } from '../config/shared.js'; +import { importVerified } from '../loader/import-verified.js'; + +const FAKE_IMAGE = 49189; // +const GLOBAL_IMAGE = 49152; // +const END_OF_UTTERANCE = 49279; +// "<|im_start|>User:" and "\nAssistant:" as the chat template tokenizes them. +const USER_OPEN = [1, 11126, 42]; +const ASSISTANT_OPEN = [END_OF_UTTERANCE, 198, 9519, 9531, 42]; + +async function loadTokenizer(lib, rootUrl, dir) { + const tj = await importVerified(lib); + tj.env.allowLocalModels = true; + tj.env.allowRemoteModels = false; + tj.env.useBrowserCache = false; + tj.env.localModelPath = rootUrl; + return tj.AutoTokenizer.from_pretrained(dir.replace(/\/$/, '')); +} + +const aborted = () => new DOMException('Aborted', 'AbortError'); + +// RGBA bytes -> planar RGB floats, rescaled and normalized as the model's image processor does. +export function normalizeRGBA(rgba, plane, { rescale, mean, std }) { + const px = new Float32Array(3 * plane); + const k = rescale / std; + const b = mean / std; + for (let p = 0; p < plane; p++) { + px[p] = rgba[p * 4] * k - b; + px[plane + p] = rgba[p * 4 + 1] * k - b; + px[2 * plane + p] = rgba[p * 4 + 2] * k - b; + } + return px; +} + +// "<|im_start|>User:{question}\nAssistant:", where +// expands to the wrapped run of placeholders the image embeddings replace. +export function firstTurnIds(meta, questionIds) { + const image = [FAKE_IMAGE, GLOBAL_IMAGE, ...Array(meta.n_image_tokens).fill(meta.image_token_id), FAKE_IMAGE]; + return { ids: [...USER_OPEN, ...image, ...questionIds, ...ASSISTANT_OPEN], imageStart: USER_OPEN.length + 2 }; +} + +// "\nUser: {question}\nAssistant:" after a previous answer. +export function followUpIds(pendingToken, userTurnIds) { + return [...(pendingToken == null ? [] : [pendingToken]), END_OF_UTTERANCE, ...userTurnIds, ...ASSISTANT_OPEN]; +} + +export class VisionLanguageModel { + // stages reported through onProgress: runtime, download (bytes over all files), gpu + static async load(source, { onProgress = () => {}, signal } = {}) { + const { spec, url } = source; + const logs = []; + onProgress({ stage: 'runtime', fraction: 0 }); + const module = await instantiate(url(spec.runtime.script), spec.runtime.factory, { + wasmUrl: url(spec.runtime.wasm), + onLog: (kind, m) => (logs.push(m), console.debug(`[ExecuTorch] ${m}`)), + }); + if (!module.ccall('et_init', 'number', [], [])) throw new Error('No WebGPU device available to the ExecuTorch runtime'); + onProgress({ stage: 'runtime', fraction: 1 }); + + const files = Object.entries(spec.files); + const total = files.reduce((s, [, f]) => s + (f.bytes ?? 0), 0); + const loaded = Object.fromEntries(files.map(([k]) => [k, 0])); + let fromCache = true; + const report = () => { + const got = Object.values(loaded).reduce((a, b) => a + b, 0); + onProgress({ stage: 'download', fraction: total ? Math.min(1, got / total) : 0, loaded: got, total, fromCache }); + }; + const libUrl = SHARED.tokenizerLib; + if (!libUrl) throw new Error('No tokenizer library is configured for this build'); + const [blobs, meta, tokenizer] = await Promise.all([ + Promise.all( + files.map(([key, f]) => + fetchWithProgress(url(f.path), { + signal, + expectedBytes: f.bytes, + onProgress: (p) => { + loaded[key] = p.loaded; + fromCache &&= p.fromCache; + report(); + }, + }).then((bytes) => [key, bytes]), + ), + ).then(Object.fromEntries), + fetch(url(spec.meta), { signal }).then((r) => { + if (!r.ok) throw new Error(`Could not read the model description (HTTP ${r.status})`); + return r.json(); + }), + loadTokenizer(libUrl, url(''), spec.tokenizer), + ]); + if (signal?.aborted) throw aborted(); + + onProgress({ stage: 'gpu', fraction: 0 }); + for (const [fn, types, values] of spec.configure ?? []) { + if (hasExport(module, fn)) module.ccall(fn, null, types, values); + } + const programs = [ + ['vision', 'et_load_vision', [], []], + ['decoder', 'et_load_decoder', [], []], + ['prefill', 'et_load_prefill', ['number'], [spec.prefillMax]], + ]; + let hasPrefill = false; + for (const [i, [key, fn, extraTypes, extraArgs]] of programs.entries()) { + if (!blobs[key] || !hasExport(module, fn)) continue; + const path = `/${key}.pte`; + module.FS.writeFile(path, blobs[key], { canOwn: true }); + delete blobs[key]; + const ok = await module.ccall(fn, 'number', ['string', ...extraTypes], [path, ...extraArgs], { async: true }); + try { + module.FS.unlink(path); + } catch { + // The runtime may already have taken ownership of the file. + } + if (key === 'prefill') hasPrefill = ok === 1; + else if (!ok) throw new Error(`The runtime could not load the ${key} program${logs.length ? `: ${logs.at(-1)}` : ''}`); + onProgress({ stage: 'gpu', fraction: (i + 1) / programs.length }); + if (signal?.aborted) throw aborted(); + } + return new VisionLanguageModel(module, tokenizer, spec, meta, { + embed: new Int8Array(blobs.embed.buffer, blobs.embed.byteOffset, blobs.embed.byteLength), + scale: new Float32Array(blobs.embedScale.buffer.slice(blobs.embedScale.byteOffset, blobs.embedScale.byteOffset + blobs.embedScale.byteLength)), + hasPrefill, + }); + } + + constructor(module, tokenizer, spec, meta, { embed, scale, hasPrefill }) { + this.m = module; + this.tokenizer = tokenizer; + this.hidden = meta.hidden; + this.size = meta.image_size; + this.meta = meta; + this.embed = embed; + this.scale = scale; + this.hasPrefill = hasPrefill; + this.prefillMax = hasPrefill ? spec.prefillMax : 0; + this.maxCtx = spec.maxCtx ?? 512; + this.eos = new Set(meta.eos_token_ids); + this.imageTokens = meta.n_image_tokens; + this.imageStart = firstTurnIds(meta, []).imageStart; + + const plane = this.size * this.size; + this.pixPtr = module._malloc(3 * plane * 4); + this.rowPtr = module._malloc(this.hidden * 4); + this.batchPtr = this.prefillMax ? module._malloc(this.prefillMax * this.hidden * 4) : 0; + this.canvas = document.createElement('canvas'); + this.canvas.width = this.canvas.height = this.size; + this.ctx = this.canvas.getContext('2d', { willReadFrequently: true }); + + this.pixels = null; + this.imageEmbeds = null; + this.pos = 0; + this.pending = null; + this.busy = Promise.resolve(); + } + + #enqueue(fn) { + const run = this.busy.then(fn); + this.busy = run.catch(() => {}); + return run; + } + + // Accepts anything drawImage does (ImageBitmap, canvas, ,