diff --git a/.agents/skills/cg-perf/SKILL.md b/.agents/skills/cg-perf/SKILL.md index fafa832ada..c5c791d540 100644 --- a/.agents/skills/cg-perf/SKILL.md +++ b/.agents/skills/cg-perf/SKILL.md @@ -131,6 +131,7 @@ reports `min/p50/p95/p99/MAX` plus per-stage breakdown and settle cost. | `zoom` | slow/fast × around-fit/high | Zoom oscillation at different levels | | `pan_with_settle` | slow/fast × fit/zoomed | Pan with settle frames interleaved every 12 frames | | `realtime` | fast/slow × fit/zoomed | **Real-time event loop simulation** with sleep, 240Hz tick thread, and settle countdown matching the native viewer | +| `frameloop` | 16/50/80/120/200/300/500ms interval | **Real FrameLoop path** — the only bench that captures stable-frame jank during panning (see below) | | `resize` | alternating viewport sizes | `--resize` flag. Measures `resize()` + `redraw()` cost per cycle (layout rebuild + cache invalidation + repaint) | The `realtime` scenarios use actual `thread::sleep()` between frames @@ -138,6 +139,16 @@ and simulate the native viewer's 240Hz tick thread + settle countdown. These produce frame timings that match what users actually see, including settle-induced frame drops at their natural frequency. +The `frameloop` scenarios go through the actual `FrameLoop.poll()` / +`complete()` path — the same code path as `Application::frame()`. All +other pan/zoom scenarios bypass `FrameLoop` and call `queue_unstable()` +directly, which means they never produce stable frames mid-interaction. +The `frameloop` scenarios sweep scroll intervals from 16ms (fast flick) +to 500ms (discrete clicks) and reveal how `FrameLoop`'s stable-frame +decisions affect the frame time distribution at each speed. Use these +when investigating panning jank, adaptive timing, or pan/zoom image +cache behavior. + **Choosing scenes:** Use `--list-scenes` to see what's available. Pick scenes that stress the subsystem you're optimizing. For effects/caching work, look for scenes with high promoted-node counts. For culling work, @@ -174,6 +185,7 @@ of scenes, configs, and operations. The naming convention is | Does a config toggle actually help? | Both GPU benchmarks + Criterion | | Does it match what users see in the app? | `realtime` scenarios (sleep + settle simulation) | | Are there frame drops during gestures? | Check `p99` and `MAX` in scenario stats | +| Is slow panning janky (stable frame spikes)? | `frameloop` scenarios (real FrameLoop path) | | Is resize janky? | Single-scene GPU bench with `--resize` | --- @@ -447,9 +459,19 @@ Back-to-back frame benchmarks (no sleep between frames) can produce misleadingly fast numbers because they never trigger settle frames. The native viewer's 240Hz tick thread fires `queue_stable()` ~50ms after the last interaction, clearing image caches. Use the `realtime` -scenario type to simulate this timing and produce numbers that match -what users actually see. Always check `p99` and `MAX` — not just -`p50` — to catch settle-induced spikes. +or `frameloop` scenario types to produce numbers that match what users +actually see. Always check `p99` and `MAX` — not just `p50` — to +catch settle-induced spikes. + +### Most benchmarks bypass FrameLoop + +All pan/zoom/circle/zigzag scenarios call `queue_unstable()` directly +— they never go through `FrameLoop.poll()`. This means they never +produce stable frames mid-interaction and cannot capture the jank +pattern where a stable frame interrupts slow panning. Only the +`frameloop` scenarios use the real `FrameLoop` decision path. When +investigating panning smoothness or adaptive timing, always use the +`frameloop` scenarios. ### Stable frames must recapture caches @@ -473,3 +495,77 @@ absolute-positioned documents. thousands of cheap entries, the timing checks themselves can become significant. Use `elapsed()` checks at reasonable intervals, not every iteration. + +### `Instant::now()` is broken on emscripten + +Under emscripten, `Instant::now()` is effectively constant, so durations +collapse to zero. Use `crate::sys::perf_now()` for timing: it maps to +`emscripten_get_now()` (`performance.now()`) on WASM and `Instant` on native. + +### WASM/native ratios are stage-dependent + +WASM overhead is not a single multiplier. Roughly: simple compute is ~2-3x, +HashMap-heavy traversals can be 10-35x, and after Vec-indexing hot paths, +data-structure-bound stages drop to ~1-2x while compute-heavy stages stay +~5-15x+. Measure per stage. + +### Data structures matter much more in WASM + +Large `HashMap`s (100K+ entries) may be fine on native but can be extremely +slow in WASM due to linear memory and weaker cache behavior. Prefer dense +Vec-indexed storage (`DenseNodeMap`) for hot paths. See `cache/fast_hash.rs`. + +### Native profiles can mis-rank WASM bottlenecks + +Native profiling finds stage costs, but not WASM amplification. Example: +native highlighted layers, while WASM was dominated by geometry because +per-node `HashMap` costs were amplified. Confirm priorities with WASM data. + +--- + +## WASM Performance + +WASM is the primary shipping target. Native benchmarks show the algorithmic +ceiling; WASM benchmarks show delivered performance. + +See `docs/wg/feat-2d/wasm-benchmarking.md` for the full strategy and +lessons learned. Key points: + +### Measurement inside WASM + +`load_scene` emits per-stage timing via `eprintln!` + `sys::perf_now()`. +Read the `[load_scene]` line in browser console (stderr) for +fonts/layout/geometry/effects/layers. This is the primary `load_scene` +WASM measurement path today. + +### Three-layer benchmarking model + +1. **Native** (`load-bench`, Criterion): algorithmic ceiling + profiling +2. **WASM-on-Node**: real WASM in headless/CI — **implemented** +3. **Browser**: full pipeline (JS encode + WASM load + GPU render) + +WASM-on-Node benchmark: + +```sh +# Build WASM first +just --justfile crates/grida-canvas-wasm/justfile build + +# Run benchmark (requires fixtures/local/perf/local/yrr-main.grida for 136k test) +cd crates/grida-canvas-wasm && npx vitest run __test__/bench-load-scene.test.ts +``` + +WASM-on-Node results closely match browser WASM timings, confirming it as +a valid benchmarking layer for compute-heavy stages. + +### Known WASM-specific issues + +- **GPU-only paths** can fail only on WASM (native runs CPU backend). + `blit_content_cache` and overlay-only fast path both had WASM-only bugs. +- **Large enum access** is the dominant WASM bottleneck. The `Node` enum + (15 variants, each hundreds of bytes) causes cache-unfriendly memory access + that WASM amplifies to 30×+ native cost. Fix: Struct-of-Arrays (SoA) — + see `docs/wg/feat-2d/wasm-load-scene-optimization.md`. +- **Deep recursion** (`build_recursive`, `flatten_node`) is costlier in WASM + due to stack-frame overhead in linear memory. +- **JS↔WASM boundary** is small for bulk calls (`switch_scene`), but JS-side + FlatBuffers encoding is still ~10% of pipeline cost. diff --git a/.ref/figma/fig2kiwi.ts b/.ref/figma/fig2kiwi.ts index fcd0f69cec..bec0275240 100755 --- a/.ref/figma/fig2kiwi.ts +++ b/.ref/figma/fig2kiwi.ts @@ -54,8 +54,12 @@ import { // --- Constants --- +// Kiwi archive preludes: the first 8 bytes of the file (see FigmaArchiveParser.parseArchive). +// Each variant uses a different fixed 8-byte ASCII magic string; FigJam's ends with a literal +// period — it is not a typo and must match bytes on disk. const FIG_KIWI_PRELUDE = "fig-kiwi"; const FIGJAM_KIWI_PRELUDE = "fig-jam."; +const FIGDECK_KIWI_PRELUDE = "fig-deck"; const ZIP_SIGNATURE = [0x50, 0x4b, 0x03, 0x04]; // --- Archive Parser (duplicated from main source) --- @@ -94,7 +98,11 @@ class FigmaArchiveParser { const preludeData = parser.read(FIG_KIWI_PRELUDE.length); const prelude = String.fromCharCode.apply(String, Array.from(preludeData)); - if (prelude !== FIG_KIWI_PRELUDE && prelude !== FIGJAM_KIWI_PRELUDE) { + if ( + prelude !== FIG_KIWI_PRELUDE && + prelude !== FIGJAM_KIWI_PRELUDE && + prelude !== FIGDECK_KIWI_PRELUDE + ) { throw new Error(`Unexpected prelude: "${prelude}"`); } @@ -138,8 +146,12 @@ function readFigFile(data: Uint8Array) { String, Array.from(fileData.slice(0, 8)) ); - return prelude === FIG_KIWI_PRELUDE || prelude === FIGJAM_KIWI_PRELUDE; - }) || keys.find((k) => k.endsWith(".fig")); + return ( + prelude === FIG_KIWI_PRELUDE || + prelude === FIGJAM_KIWI_PRELUDE || + prelude === FIGDECK_KIWI_PRELUDE + ); + }) || keys.find((k) => k.endsWith(".fig") || k.endsWith(".deck")); if (!mainFile) { throw new Error( diff --git a/Cargo.lock b/Cargo.lock index 61edb15e4e..808b7c0ca4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -495,6 +495,7 @@ dependencies = [ "rendiff", "reqwest", "rstar", + "rustc-hash", "seahash", "serde", "serde_json", diff --git a/crates/grida-canvas-wasm/lib/__test__/bench-load-scene.test.ts b/crates/grida-canvas-wasm/lib/__test__/bench-load-scene.test.ts new file mode 100644 index 0000000000..3ca202eca1 --- /dev/null +++ b/crates/grida-canvas-wasm/lib/__test__/bench-load-scene.test.ts @@ -0,0 +1,151 @@ +// @vitest-environment node +// +// WASM-on-Node benchmark for load_scene pipeline. +// +// Measures real WASM execution of the scene loading stages: +// 1. loadSceneGrida — FBS decode + SceneGraph construction +// 2. switchScene — layout + geometry + effects + layers +// +// When `perf` feature is enabled on the cg crate, the Rust side emits +// per-stage timing via eprintln! ([load_scene] line). +// This test measures JS-side wall time for comparison. +// +// Usage: +// pnpm test bench-load-scene +// pnpm vitest run bench-load-scene --reporter=verbose +// +// To benchmark a .grida file, place it in: +// lib/__test__/fixtures/local/ +// All .grida files in that directory will be auto-discovered. + +import { readFileSync, existsSync, readdirSync } from "node:fs"; +import { resolve } from "node:path"; +import { beforeAll, describe, expect, it } from "vitest"; +import { Scene } from "../modules/canvas"; + +/** Directory for local (gitignored) benchmark fixtures. */ +const LOCAL_FIXTURES_DIR = resolve(__dirname, "fixtures/local"); + +let module: any; + +beforeAll(async () => { + const pkg = require("../../dist/index.js") as { + default: (opts?: unknown) => Promise; + }; + const factory = await pkg.default(); + module = factory.module; +}, 30_000); + +function createRasterScene(width = 1000, height = 1000): Scene { + const appptr = module._init_with_backend( + 1, // BACKEND_ID.Raster + width, + height, + 1, // useEmbeddedFonts = true + 0 // configFlags + ); + return new Scene(module, appptr); +} + +/** + * Discover .grida files from the local fixtures directory. + */ +function discoverGridaFixtures(): { name: string; path: string }[] { + if (!existsSync(LOCAL_FIXTURES_DIR)) { + return []; + } + return readdirSync(LOCAL_FIXTURES_DIR) + .filter((f) => f.endsWith(".grida")) + .sort() + .map((f) => ({ name: f, path: resolve(LOCAL_FIXTURES_DIR, f) })); +} + +describe("bench: load_scene (WASM-on-Node)", () => { + it("grida1 JSON (rectangle)", async () => { + const scene = createRasterScene(); + const doc = readFileSync( + resolve(process.cwd(), "example/rectangle.grida1"), + "utf8" + ); + + const t0 = performance.now(); + scene.loadScene(doc); + const elapsed = performance.now() - t0; + + console.log(`[wasm-bench] rectangle.grida1: ${elapsed.toFixed(0)}ms`); + expect(elapsed).toBeLessThan(5_000); + scene.dispose(); + }); + + it("synthetic 100x100 grid (10k nodes)", async () => { + const scene = createRasterScene(); + + const t0 = performance.now(); + scene.loadBenchmarkScene(100, 100); + const elapsed = performance.now() - t0; + + console.log( + `[wasm-bench] synthetic 100x100: ${elapsed.toFixed(0)}ms (10k nodes)` + ); + expect(elapsed).toBeLessThan(30_000); + scene.dispose(); + }, 60_000); + + it("synthetic 200x200 grid (40k nodes)", async () => { + const scene = createRasterScene(); + + const t0 = performance.now(); + scene.loadBenchmarkScene(200, 200); + const elapsed = performance.now() - t0; + + console.log( + `[wasm-bench] synthetic 200x200: ${elapsed.toFixed(0)}ms (40k nodes)` + ); + expect(elapsed).toBeLessThan(60_000); + scene.dispose(); + }, 120_000); + + // Auto-discovered .grida fixtures from fixtures/local/ + const fixtures = discoverGridaFixtures(); + + for (const fx of fixtures) { + it(`grida binary: ${fx.name}`, async () => { + const data = new Uint8Array(readFileSync(fx.path)); + const scene = createRasterScene(); + + // Phase 1: FBS decode + const t0 = performance.now(); + scene.loadSceneGrida(data); + const tLoad = performance.now(); + + // Phase 2: switch to the first scene + const sceneIds = scene.loadedSceneIds(); + expect(sceneIds.length).toBeGreaterThan(0); + const firstSceneId = sceneIds[0]; + + scene.switchScene(firstSceneId); + const tSwitch = performance.now(); + + const loadMs = tLoad - t0; + const switchMs = tSwitch - tLoad; + const totalMs = tSwitch - t0; + + console.log( + `[wasm-bench] ${fx.name} (scene=${firstSceneId}): ` + + `load=${loadMs.toFixed(0)}ms switch=${switchMs.toFixed(0)}ms total=${totalMs.toFixed(0)}ms` + ); + + expect(totalMs).toBeLessThan(120_000); + scene.dispose(); + }, 120_000); + } + + if (fixtures.length === 0) { + it("no .grida fixtures found (skipped)", () => { + console.log( + "[wasm-bench] No .grida fixtures in lib/__test__/fixtures/local/. " + + "Place .grida files there to benchmark real scenes." + ); + }); + } +}); diff --git a/crates/grida-canvas-wasm/lib/bin/grida-canvas-wasm.js b/crates/grida-canvas-wasm/lib/bin/grida-canvas-wasm.js index 4094ee5487..05337f71df 100644 --- a/crates/grida-canvas-wasm/lib/bin/grida-canvas-wasm.js +++ b/crates/grida-canvas-wasm/lib/bin/grida-canvas-wasm.js @@ -1,2 +1,2 @@ -var createGridaCanvas=(()=>{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(typeof __filename!="undefined"){_scriptName=__filename}else{}var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_NODE){var fs=require("node:fs");scriptDirectory=__dirname+"/";readBinary=filename=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename);return ret};readAsync=async(filename,binary=true)=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename,binary?undefined:"utf8");return ret};if(process.argv.length>1){thisProgram=process.argv[1].replace(/\\/g,"/")}arguments_=process.argv.slice(2);quit_=(status,toThrow)=>{process.exitCode=status;throw toThrow}}else if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){try{scriptDirectory=new URL(".",_scriptName).href}catch{}{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;var isFileURI=filename=>filename.startsWith("file://");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);Module["HEAPU8"]=HEAPU8=new Uint8Array(b);Module["HEAPU16"]=HEAPU16=new Uint16Array(b);Module["HEAP32"]=HEAP32=new Int32Array(b);Module["HEAPU32"]=HEAPU32=new Uint32Array(b);Module["HEAPF32"]=HEAPF32=new Float32Array(b);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["Ng"]();FS.ignorePermissions=false}function preMain(){}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("grida_canvas_wasm.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&&!ENVIRONMENT_IS_NODE){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;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 noExitRuntime=true;var stackRestore=val=>__emscripten_stack_restore(val);var stackSave=()=>_emscripten_stack_get_current();var exceptionCaught=[];var uncaughtExceptionCount=0;var ___cxa_begin_catch=ptr=>{var info=new ExceptionInfo(ptr);if(!info.get_caught()){info.set_caught(true);uncaughtExceptionCount--}info.set_rethrown(false);exceptionCaught.push(info);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]=type}get_type(){return HEAPU32[this.ptr+4>>2]}set_destructor(destructor){HEAPU32[this.ptr+8>>2]=destructor}get_destructor(){return HEAPU32[this.ptr+8>>2]}set_caught(caught){caught=caught?1:0;HEAP8[this.ptr+12]=caught}get_caught(){return HEAP8[this.ptr+12]!=0}set_rethrown(rethrown){rethrown=rethrown?1:0;HEAP8[this.ptr+13]=rethrown}get_rethrown(){return HEAP8[this.ptr+13]!=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]=adjustedPtr}get_adjusted_ptr(){return HEAPU32[this.ptr+16>>2]}}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};var ___cxa_find_matching_catch_2=()=>findMatchingCatch([]);var ___cxa_find_matching_catch_3=arg0=>findMatchingCatch([arg0]);var ___cxa_find_matching_catch_4=(arg0,arg1)=>findMatchingCatch([arg0,arg1]);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++}___cxa_increment_exception_refcount(ptr);exceptionLast=ptr;throw exceptionLast};var ___cxa_throw=(ptr,type,destructor)=>{var info=new ExceptionInfo(ptr);info.init(type,destructor);___cxa_increment_exception_refcount(ptr);exceptionLast=ptr;uncaughtExceptionCount++;throw exceptionLast};var ___cxa_uncaught_exceptions=()=>uncaughtExceptionCount;var ___resumeException=ptr=>{if(!exceptionLast){exceptionLast=ptr}throw exceptionLast};var syscallGetVarargI=()=>{var ret=HEAP32[+SYSCALLS.varargs>>2];SYSCALLS.varargs+=4;return ret};var syscallGetVarargP=syscallGetVarargI;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=()=>{if(ENVIRONMENT_IS_NODE){var nodeCrypto=require("node:crypto");return view=>nodeCrypto.randomFillSync(view)}return 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)=>{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)=>{if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i=endIdx)break;heap[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++]=192|u>>6;heap[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++]=224|u>>12;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;heap[outIdx++]=240|u>>18;heap[outIdx++]=128|u>>12&63;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63;i++}}heap[outIdx]=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(ENVIRONMENT_IS_NODE){var BUFSIZE=256;var buf=Buffer.alloc(BUFSIZE);var bytesRead=0;var fd=process.stdin.fd;try{bytesRead=fs.readSync(fd,buf,0,BUFSIZE)}catch(e){if(e.toString().includes("EOF"))bytesRead=0;else throw e}if(bytesRead>0){result=buf.slice(0,bytesRead).toString("utf-8")}}else 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{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?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead,ignoreNul):"";var SYSCALLS={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]=stat.dev;HEAPU32[buf+4>>2]=stat.mode;HEAPU32[buf+8>>2]=stat.nlink;HEAPU32[buf+12>>2]=stat.uid;HEAPU32[buf+16>>2]=stat.gid;HEAPU32[buf+20>>2]=stat.rdev;HEAP64[buf+24>>3]=BigInt(stat.size);HEAP32[buf+32>>2]=4096;HEAP32[buf+36>>2]=stat.blocks;var atime=stat.atime.getTime();var mtime=stat.mtime.getTime();var ctime=stat.ctime.getTime();HEAP64[buf+40>>3]=BigInt(Math.floor(atime/1e3));HEAPU32[buf+48>>2]=atime%1e3*1e3*1e3;HEAP64[buf+56>>3]=BigInt(Math.floor(mtime/1e3));HEAPU32[buf+64>>2]=mtime%1e3*1e3*1e3;HEAP64[buf+72>>3]=BigInt(Math.floor(ctime/1e3));HEAPU32[buf+80>>2]=ctime%1e3*1e3*1e3;HEAP64[buf+88>>3]=BigInt(stat.ino);return 0},writeStatFs(buf,stats){HEAPU32[buf+4>>2]=stats.bsize;HEAPU32[buf+60>>2]=stats.bsize;HEAP64[buf+8>>3]=BigInt(stats.blocks);HEAP64[buf+16>>3]=BigInt(stats.bfree);HEAP64[buf+24>>3]=BigInt(stats.bavail);HEAP64[buf+32>>3]=BigInt(stats.files);HEAP64[buf+40>>3]=BigInt(stats.ffree);HEAPU32[buf+48>>2]=stats.fsid;HEAPU32[buf+64>>2]=stats.flags;HEAPU32[buf+56>>2]=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_fcntl64(fd,cmd,varargs){SYSCALLS.varargs=varargs;try{var stream=SYSCALLS.getStreamFromFD(fd);switch(cmd){case 0:{var arg=syscallGetVarargI();if(arg<0){return-28}while(FS.streams[arg]){arg++}var newStream;newStream=FS.dupStream(stream,arg);return newStream.fd}case 1:case 2:return 0;case 3:return stream.flags;case 4:{var arg=syscallGetVarargI();stream.flags|=arg;return 0}case 12:{var arg=syscallGetVarargP();var offset=0;HEAP16[arg+offset>>1]=2;return 0}case 13:case 14:return 0}return-28}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_fstat64(fd,buf){try{return SYSCALLS.writeStat(buf,FS.fstat(fd))}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}var stringToUTF8=(str,outPtr,maxBytesToWrite)=>stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite);function ___syscall_getcwd(buf,size){try{if(size===0)return-28;var cwd=FS.cwd();var cwdLengthInBytes=lengthBytesUTF8(cwd)+1;if(size>2]=termios.c_iflag||0;HEAP32[argp+4>>2]=termios.c_oflag||0;HEAP32[argp+8>>2]=termios.c_cflag||0;HEAP32[argp+12>>2]=termios.c_lflag||0;for(var i=0;i<32;i++){HEAP8[argp+i+17]=termios.c_cc[i]||0}return 0}return 0}case 21510:case 21511:case 21512:{if(!stream.tty)return-59;return 0}case 21506:case 21507:case 21508:{if(!stream.tty)return-59;if(stream.tty.ops.ioctl_tcsets){var argp=syscallGetVarargP();var c_iflag=HEAP32[argp>>2];var c_oflag=HEAP32[argp+4>>2];var c_cflag=HEAP32[argp+8>>2];var c_lflag=HEAP32[argp+12>>2];var c_cc=[];for(var i=0;i<32;i++){c_cc.push(HEAP8[argp+i+17])}return stream.tty.ops.ioctl_tcsets(stream.tty,op,{c_iflag,c_oflag,c_cflag,c_lflag,c_cc})}return 0}case 21519:{if(!stream.tty)return-59;var argp=syscallGetVarargP();HEAP32[argp>>2]=0;return 0}case 21520:{if(!stream.tty)return-59;return-28}case 21537:case 21531:{var argp=syscallGetVarargP();return FS.ioctl(stream,op,argp)}case 21523:{if(!stream.tty)return-59;if(stream.tty.ops.ioctl_tiocgwinsz){var winsize=stream.tty.ops.ioctl_tiocgwinsz(stream.tty);var argp=syscallGetVarargP();HEAP16[argp>>1]=winsize[0];HEAP16[argp+2>>1]=winsize[1]}return 0}case 21524:{if(!stream.tty)return-59;return 0}case 21515:{if(!stream.tty)return-59;return 0}default:return-28}}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_lstat64(path,buf){try{path=SYSCALLS.getStr(path);return SYSCALLS.writeStat(buf,FS.lstat(path))}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_newfstatat(dirfd,path,buf,flags){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}}function ___syscall_openat(dirfd,path,flags,varargs){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}}function ___syscall_stat64(path,buf){try{path=SYSCALLS.getStr(path);return SYSCALLS.writeStat(buf,FS.stat(path))}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}var __abort_js=()=>abort("");var __emscripten_throw_longjmp=()=>{throw Infinity};var INT53_MAX=9007199254740992;var INT53_MIN=-9007199254740992;var bigintToI53Checked=num=>numINT53_MAX?NaN:Number(num);function __gmtime_js(time,tmPtr){time=bigintToI53Checked(time);var date=new Date(time*1e3);HEAP32[tmPtr>>2]=date.getUTCSeconds();HEAP32[tmPtr+4>>2]=date.getUTCMinutes();HEAP32[tmPtr+8>>2]=date.getUTCHours();HEAP32[tmPtr+12>>2]=date.getUTCDate();HEAP32[tmPtr+16>>2]=date.getUTCMonth();HEAP32[tmPtr+20>>2]=date.getUTCFullYear()-1900;HEAP32[tmPtr+24>>2]=date.getUTCDay();var start=Date.UTC(date.getUTCFullYear(),0,1,0,0,0,0);var yday=(date.getTime()-start)/(1e3*60*60*24)|0;HEAP32[tmPtr+28>>2]=yday}function __mmap_js(len,prot,flags,fd,offset,allocated,addr){offset=bigintToI53Checked(offset);try{var stream=SYSCALLS.getStreamFromFD(fd);var res=FS.mmap(stream,len,offset,prot,flags);var ptr=res.ptr;HEAP32[allocated>>2]=res.allocated;HEAPU32[addr>>2]=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){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 __tzset_js=(timezone,daylight,std_name,dst_name)=>{var currentYear=(new Date).getFullYear();var winter=new Date(currentYear,0,1);var summer=new Date(currentYear,6,1);var winterOffset=winter.getTimezoneOffset();var summerOffset=summer.getTimezoneOffset();var stdTimezoneOffset=Math.max(winterOffset,summerOffset);HEAPU32[timezone>>2]=stdTimezoneOffset*60;HEAP32[daylight>>2]=Number(winterOffset!=summerOffset);var extractZone=timezoneOffset=>{var sign=timezoneOffset>=0?"-":"+";var absOffset=Math.abs(timezoneOffset);var hours=String(Math.floor(absOffset/60)).padStart(2,"0");var minutes=String(absOffset%60).padStart(2,"0");return`UTC${sign}${hours}${minutes}`};var winterName=extractZone(winterOffset);var summerName=extractZone(summerOffset);if(summerOffsetperformance.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);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]=BigInt(nsec);return 0}var getHeapMax=()=>2147483648;var _emscripten_get_heap_max=()=>getHeapMax();var GLctx;var webgl_enable_ANGLE_instanced_arrays=ctx=>{var ext=ctx.getExtension("ANGLE_instanced_arrays");if(ext){ctx["vertexAttribDivisor"]=(index,divisor)=>ext["vertexAttribDivisorANGLE"](index,divisor);ctx["drawArraysInstanced"]=(mode,first,count,primcount)=>ext["drawArraysInstancedANGLE"](mode,first,count,primcount);ctx["drawElementsInstanced"]=(mode,count,type,indices,primcount)=>ext["drawElementsInstancedANGLE"](mode,count,type,indices,primcount);return 1}};var webgl_enable_OES_vertex_array_object=ctx=>{var ext=ctx.getExtension("OES_vertex_array_object");if(ext){ctx["createVertexArray"]=()=>ext["createVertexArrayOES"]();ctx["deleteVertexArray"]=vao=>ext["deleteVertexArrayOES"](vao);ctx["bindVertexArray"]=vao=>ext["bindVertexArrayOES"](vao);ctx["isVertexArray"]=vao=>ext["isVertexArrayOES"](vao);return 1}};var webgl_enable_WEBGL_draw_buffers=ctx=>{var ext=ctx.getExtension("WEBGL_draw_buffers");if(ext){ctx["drawBuffers"]=(n,bufs)=>ext["drawBuffersWEBGL"](n,bufs);return 1}};var webgl_enable_WEBGL_draw_instanced_base_vertex_base_instance=ctx=>!!(ctx.dibvbi=ctx.getExtension("WEBGL_draw_instanced_base_vertex_base_instance"));var webgl_enable_WEBGL_multi_draw_instanced_base_vertex_base_instance=ctx=>!!(ctx.mdibvbi=ctx.getExtension("WEBGL_multi_draw_instanced_base_vertex_base_instance"));var webgl_enable_EXT_polygon_offset_clamp=ctx=>!!(ctx.extPolygonOffsetClamp=ctx.getExtension("EXT_polygon_offset_clamp"));var webgl_enable_EXT_clip_control=ctx=>!!(ctx.extClipControl=ctx.getExtension("EXT_clip_control"));var webgl_enable_WEBGL_polygon_mode=ctx=>!!(ctx.webglPolygonMode=ctx.getExtension("WEBGL_polygon_mode"));var webgl_enable_WEBGL_multi_draw=ctx=>!!(ctx.multiDrawWebgl=ctx.getExtension("WEBGL_multi_draw"));var getEmscriptenSupportedExtensions=ctx=>{var supportedExtensions=["ANGLE_instanced_arrays","EXT_blend_minmax","EXT_disjoint_timer_query","EXT_frag_depth","EXT_shader_texture_lod","EXT_sRGB","OES_element_index_uint","OES_fbo_render_mipmap","OES_standard_derivatives","OES_texture_float","OES_texture_half_float","OES_texture_half_float_linear","OES_vertex_array_object","WEBGL_color_buffer_float","WEBGL_depth_texture","WEBGL_draw_buffers","EXT_color_buffer_float","EXT_conservative_depth","EXT_disjoint_timer_query_webgl2","EXT_texture_norm16","NV_shader_noperspective_interpolation","WEBGL_clip_cull_distance","EXT_clip_control","EXT_color_buffer_half_float","EXT_depth_clamp","EXT_float_blend","EXT_polygon_offset_clamp","EXT_texture_compression_bptc","EXT_texture_compression_rgtc","EXT_texture_filter_anisotropic","KHR_parallel_shader_compile","OES_texture_float_linear","WEBGL_blend_func_extended","WEBGL_compressed_texture_astc","WEBGL_compressed_texture_etc","WEBGL_compressed_texture_etc1","WEBGL_compressed_texture_s3tc","WEBGL_compressed_texture_s3tc_srgb","WEBGL_debug_renderer_info","WEBGL_debug_shaders","WEBGL_lose_context","WEBGL_multi_draw","WEBGL_polygon_mode"];return(ctx.getSupportedExtensions()||[]).filter(ext=>supportedExtensions.includes(ext))};var GL={counter:1,buffers:[],programs:[],framebuffers:[],renderbuffers:[],textures:[],shaders:[],vaos:[],contexts:[],offscreenCanvases:{},queries:[],samplers:[],transformFeedbacks:[],syncs:[],stringCache:{},stringiCache:{},unpackAlignment:4,unpackRowLength:0,recordError:errorCode=>{if(!GL.lastError){GL.lastError=errorCode}},getNewId:table=>{var ret=GL.counter++;for(var i=table.length;i{for(var i=0;i>2]=id}},getSource:(shader,count,string,length)=>{var source="";for(var i=0;i>2]:undefined;source+=UTF8ToString(HEAPU32[string+i*4>>2],len)}return source},createContext:(canvas,webGLContextAttributes)=>{if(!canvas.getContextSafariWebGL2Fixed){canvas.getContextSafariWebGL2Fixed=canvas.getContext;function fixedGetContext(ver,attrs){var gl=canvas.getContextSafariWebGL2Fixed(ver,attrs);return ver=="webgl"==gl instanceof WebGLRenderingContext?gl:null}canvas.getContext=fixedGetContext}var ctx=webGLContextAttributes.majorVersion>1?canvas.getContext("webgl2",webGLContextAttributes):canvas.getContext("webgl",webGLContextAttributes);if(!ctx)return 0;var handle=GL.registerContext(ctx,webGLContextAttributes);return handle},registerContext:(ctx,webGLContextAttributes)=>{var handle=GL.getNewId(GL.contexts);var context={handle,attributes:webGLContextAttributes,version:webGLContextAttributes.majorVersion,GLctx:ctx};if(ctx.canvas)ctx.canvas.GLctxObject=context;GL.contexts[handle]=context;if(typeof webGLContextAttributes.enableExtensionsByDefault=="undefined"||webGLContextAttributes.enableExtensionsByDefault){GL.initExtensions(context)}return handle},makeContextCurrent:contextHandle=>{GL.currentContext=GL.contexts[contextHandle];Module["ctx"]=GLctx=GL.currentContext?.GLctx;return!(contextHandle&&!GLctx)},getContext:contextHandle=>GL.contexts[contextHandle],deleteContext:contextHandle=>{if(GL.currentContext===GL.contexts[contextHandle]){GL.currentContext=null}if(typeof JSEvents=="object"){JSEvents.removeAllHandlersOnTarget(GL.contexts[contextHandle].GLctx.canvas)}if(GL.contexts[contextHandle]?.GLctx.canvas){GL.contexts[contextHandle].GLctx.canvas.GLctxObject=undefined}GL.contexts[contextHandle]=null},initExtensions:context=>{context||=GL.currentContext;if(context.initExtensionsDone)return;context.initExtensionsDone=true;var GLctx=context.GLctx;webgl_enable_WEBGL_multi_draw(GLctx);webgl_enable_EXT_polygon_offset_clamp(GLctx);webgl_enable_EXT_clip_control(GLctx);webgl_enable_WEBGL_polygon_mode(GLctx);webgl_enable_ANGLE_instanced_arrays(GLctx);webgl_enable_OES_vertex_array_object(GLctx);webgl_enable_WEBGL_draw_buffers(GLctx);webgl_enable_WEBGL_draw_instanced_base_vertex_base_instance(GLctx);webgl_enable_WEBGL_multi_draw_instanced_base_vertex_base_instance(GLctx);if(context.version>=2){GLctx.disjointTimerQueryExt=GLctx.getExtension("EXT_disjoint_timer_query_webgl2")}if(context.version<2||!GLctx.disjointTimerQueryExt){GLctx.disjointTimerQueryExt=GLctx.getExtension("EXT_disjoint_timer_query")}for(var ext of getEmscriptenSupportedExtensions(GLctx)){if(!ext.includes("lose_context")&&!ext.includes("debug")){GLctx.getExtension(ext)}}}};var _emscripten_glActiveTexture=x0=>GLctx.activeTexture(x0);var _emscripten_glAttachShader=(program,shader)=>{GLctx.attachShader(GL.programs[program],GL.shaders[shader])};var _emscripten_glBeginQuery=(target,id)=>{GLctx.beginQuery(target,GL.queries[id])};var _emscripten_glBeginQueryEXT=(target,id)=>{GLctx.disjointTimerQueryExt["beginQueryEXT"](target,GL.queries[id])};var _emscripten_glBeginTransformFeedback=x0=>GLctx.beginTransformFeedback(x0);var _emscripten_glBindAttribLocation=(program,index,name)=>{GLctx.bindAttribLocation(GL.programs[program],index,UTF8ToString(name))};var _emscripten_glBindBuffer=(target,buffer)=>{if(target==35051){GLctx.currentPixelPackBufferBinding=buffer}else if(target==35052){GLctx.currentPixelUnpackBufferBinding=buffer}GLctx.bindBuffer(target,GL.buffers[buffer])};var _emscripten_glBindBufferBase=(target,index,buffer)=>{GLctx.bindBufferBase(target,index,GL.buffers[buffer])};var _emscripten_glBindBufferRange=(target,index,buffer,offset,ptrsize)=>{GLctx.bindBufferRange(target,index,GL.buffers[buffer],offset,ptrsize)};var _emscripten_glBindFramebuffer=(target,framebuffer)=>{GLctx.bindFramebuffer(target,GL.framebuffers[framebuffer])};var _emscripten_glBindRenderbuffer=(target,renderbuffer)=>{GLctx.bindRenderbuffer(target,GL.renderbuffers[renderbuffer])};var _emscripten_glBindSampler=(unit,sampler)=>{GLctx.bindSampler(unit,GL.samplers[sampler])};var _emscripten_glBindTexture=(target,texture)=>{GLctx.bindTexture(target,GL.textures[texture])};var _emscripten_glBindTransformFeedback=(target,id)=>{GLctx.bindTransformFeedback(target,GL.transformFeedbacks[id])};var _emscripten_glBindVertexArray=vao=>{GLctx.bindVertexArray(GL.vaos[vao])};var _glBindVertexArray=_emscripten_glBindVertexArray;var _emscripten_glBindVertexArrayOES=_glBindVertexArray;var _emscripten_glBlendColor=(x0,x1,x2,x3)=>GLctx.blendColor(x0,x1,x2,x3);var _emscripten_glBlendEquation=x0=>GLctx.blendEquation(x0);var _emscripten_glBlendEquationSeparate=(x0,x1)=>GLctx.blendEquationSeparate(x0,x1);var _emscripten_glBlendFunc=(x0,x1)=>GLctx.blendFunc(x0,x1);var _emscripten_glBlendFuncSeparate=(x0,x1,x2,x3)=>GLctx.blendFuncSeparate(x0,x1,x2,x3);var _emscripten_glBlitFramebuffer=(x0,x1,x2,x3,x4,x5,x6,x7,x8,x9)=>GLctx.blitFramebuffer(x0,x1,x2,x3,x4,x5,x6,x7,x8,x9);var _emscripten_glBufferData=(target,size,data,usage)=>{if(GL.currentContext.version>=2){if(data&&size){GLctx.bufferData(target,HEAPU8,usage,data,size)}else{GLctx.bufferData(target,size,usage)}return}GLctx.bufferData(target,data?HEAPU8.subarray(data,data+size):size,usage)};var _emscripten_glBufferSubData=(target,offset,size,data)=>{if(GL.currentContext.version>=2){size&&GLctx.bufferSubData(target,offset,HEAPU8,data,size);return}GLctx.bufferSubData(target,offset,HEAPU8.subarray(data,data+size))};var _emscripten_glCheckFramebufferStatus=x0=>GLctx.checkFramebufferStatus(x0);var _emscripten_glClear=x0=>GLctx.clear(x0);var _emscripten_glClearBufferfi=(x0,x1,x2,x3)=>GLctx.clearBufferfi(x0,x1,x2,x3);var _emscripten_glClearBufferfv=(buffer,drawbuffer,value)=>{GLctx.clearBufferfv(buffer,drawbuffer,HEAPF32,value>>2)};var _emscripten_glClearBufferiv=(buffer,drawbuffer,value)=>{GLctx.clearBufferiv(buffer,drawbuffer,HEAP32,value>>2)};var _emscripten_glClearBufferuiv=(buffer,drawbuffer,value)=>{GLctx.clearBufferuiv(buffer,drawbuffer,HEAPU32,value>>2)};var _emscripten_glClearColor=(x0,x1,x2,x3)=>GLctx.clearColor(x0,x1,x2,x3);var _emscripten_glClearDepthf=x0=>GLctx.clearDepth(x0);var _emscripten_glClearStencil=x0=>GLctx.clearStencil(x0);var _emscripten_glClientWaitSync=(sync,flags,timeout)=>{timeout=Number(timeout);return GLctx.clientWaitSync(GL.syncs[sync],flags,timeout)};var _emscripten_glClipControlEXT=(origin,depth)=>{GLctx.extClipControl["clipControlEXT"](origin,depth)};var _emscripten_glColorMask=(red,green,blue,alpha)=>{GLctx.colorMask(!!red,!!green,!!blue,!!alpha)};var _emscripten_glCompileShader=shader=>{GLctx.compileShader(GL.shaders[shader])};var _emscripten_glCompressedTexImage2D=(target,level,internalFormat,width,height,border,imageSize,data)=>{if(GL.currentContext.version>=2){if(GLctx.currentPixelUnpackBufferBinding||!imageSize){GLctx.compressedTexImage2D(target,level,internalFormat,width,height,border,imageSize,data);return}GLctx.compressedTexImage2D(target,level,internalFormat,width,height,border,HEAPU8,data,imageSize);return}GLctx.compressedTexImage2D(target,level,internalFormat,width,height,border,HEAPU8.subarray(data,data+imageSize))};var _emscripten_glCompressedTexImage3D=(target,level,internalFormat,width,height,depth,border,imageSize,data)=>{if(GLctx.currentPixelUnpackBufferBinding){GLctx.compressedTexImage3D(target,level,internalFormat,width,height,depth,border,imageSize,data)}else{GLctx.compressedTexImage3D(target,level,internalFormat,width,height,depth,border,HEAPU8,data,imageSize)}};var _emscripten_glCompressedTexSubImage2D=(target,level,xoffset,yoffset,width,height,format,imageSize,data)=>{if(GL.currentContext.version>=2){if(GLctx.currentPixelUnpackBufferBinding||!imageSize){GLctx.compressedTexSubImage2D(target,level,xoffset,yoffset,width,height,format,imageSize,data);return}GLctx.compressedTexSubImage2D(target,level,xoffset,yoffset,width,height,format,HEAPU8,data,imageSize);return}GLctx.compressedTexSubImage2D(target,level,xoffset,yoffset,width,height,format,HEAPU8.subarray(data,data+imageSize))};var _emscripten_glCompressedTexSubImage3D=(target,level,xoffset,yoffset,zoffset,width,height,depth,format,imageSize,data)=>{if(GLctx.currentPixelUnpackBufferBinding){GLctx.compressedTexSubImage3D(target,level,xoffset,yoffset,zoffset,width,height,depth,format,imageSize,data)}else{GLctx.compressedTexSubImage3D(target,level,xoffset,yoffset,zoffset,width,height,depth,format,HEAPU8,data,imageSize)}};var _emscripten_glCopyBufferSubData=(x0,x1,x2,x3,x4)=>GLctx.copyBufferSubData(x0,x1,x2,x3,x4);var _emscripten_glCopyTexImage2D=(x0,x1,x2,x3,x4,x5,x6,x7)=>GLctx.copyTexImage2D(x0,x1,x2,x3,x4,x5,x6,x7);var _emscripten_glCopyTexSubImage2D=(x0,x1,x2,x3,x4,x5,x6,x7)=>GLctx.copyTexSubImage2D(x0,x1,x2,x3,x4,x5,x6,x7);var _emscripten_glCopyTexSubImage3D=(x0,x1,x2,x3,x4,x5,x6,x7,x8)=>GLctx.copyTexSubImage3D(x0,x1,x2,x3,x4,x5,x6,x7,x8);var _emscripten_glCreateProgram=()=>{var id=GL.getNewId(GL.programs);var program=GLctx.createProgram();program.name=id;program.maxUniformLength=program.maxAttributeLength=program.maxUniformBlockNameLength=0;program.uniformIdCounter=1;GL.programs[id]=program;return id};var _emscripten_glCreateShader=shaderType=>{var id=GL.getNewId(GL.shaders);GL.shaders[id]=GLctx.createShader(shaderType);return id};var _emscripten_glCullFace=x0=>GLctx.cullFace(x0);var _emscripten_glDeleteBuffers=(n,buffers)=>{for(var i=0;i>2];var buffer=GL.buffers[id];if(!buffer)continue;GLctx.deleteBuffer(buffer);buffer.name=0;GL.buffers[id]=null;if(id==GLctx.currentPixelPackBufferBinding)GLctx.currentPixelPackBufferBinding=0;if(id==GLctx.currentPixelUnpackBufferBinding)GLctx.currentPixelUnpackBufferBinding=0}};var _emscripten_glDeleteFramebuffers=(n,framebuffers)=>{for(var i=0;i>2];var framebuffer=GL.framebuffers[id];if(!framebuffer)continue;GLctx.deleteFramebuffer(framebuffer);framebuffer.name=0;GL.framebuffers[id]=null}};var _emscripten_glDeleteProgram=id=>{if(!id)return;var program=GL.programs[id];if(!program){GL.recordError(1281);return}GLctx.deleteProgram(program);program.name=0;GL.programs[id]=null};var _emscripten_glDeleteQueries=(n,ids)=>{for(var i=0;i>2];var query=GL.queries[id];if(!query)continue;GLctx.deleteQuery(query);GL.queries[id]=null}};var _emscripten_glDeleteQueriesEXT=(n,ids)=>{for(var i=0;i>2];var query=GL.queries[id];if(!query)continue;GLctx.disjointTimerQueryExt["deleteQueryEXT"](query);GL.queries[id]=null}};var _emscripten_glDeleteRenderbuffers=(n,renderbuffers)=>{for(var i=0;i>2];var renderbuffer=GL.renderbuffers[id];if(!renderbuffer)continue;GLctx.deleteRenderbuffer(renderbuffer);renderbuffer.name=0;GL.renderbuffers[id]=null}};var _emscripten_glDeleteSamplers=(n,samplers)=>{for(var i=0;i>2];var sampler=GL.samplers[id];if(!sampler)continue;GLctx.deleteSampler(sampler);sampler.name=0;GL.samplers[id]=null}};var _emscripten_glDeleteShader=id=>{if(!id)return;var shader=GL.shaders[id];if(!shader){GL.recordError(1281);return}GLctx.deleteShader(shader);GL.shaders[id]=null};var _emscripten_glDeleteSync=id=>{if(!id)return;var sync=GL.syncs[id];if(!sync){GL.recordError(1281);return}GLctx.deleteSync(sync);sync.name=0;GL.syncs[id]=null};var _emscripten_glDeleteTextures=(n,textures)=>{for(var i=0;i>2];var texture=GL.textures[id];if(!texture)continue;GLctx.deleteTexture(texture);texture.name=0;GL.textures[id]=null}};var _emscripten_glDeleteTransformFeedbacks=(n,ids)=>{for(var i=0;i>2];var transformFeedback=GL.transformFeedbacks[id];if(!transformFeedback)continue;GLctx.deleteTransformFeedback(transformFeedback);transformFeedback.name=0;GL.transformFeedbacks[id]=null}};var _emscripten_glDeleteVertexArrays=(n,vaos)=>{for(var i=0;i>2];GLctx.deleteVertexArray(GL.vaos[id]);GL.vaos[id]=null}};var _glDeleteVertexArrays=_emscripten_glDeleteVertexArrays;var _emscripten_glDeleteVertexArraysOES=_glDeleteVertexArrays;var _emscripten_glDepthFunc=x0=>GLctx.depthFunc(x0);var _emscripten_glDepthMask=flag=>{GLctx.depthMask(!!flag)};var _emscripten_glDepthRangef=(x0,x1)=>GLctx.depthRange(x0,x1);var _emscripten_glDetachShader=(program,shader)=>{GLctx.detachShader(GL.programs[program],GL.shaders[shader])};var _emscripten_glDisable=x0=>GLctx.disable(x0);var _emscripten_glDisableVertexAttribArray=index=>{GLctx.disableVertexAttribArray(index)};var _emscripten_glDrawArrays=(mode,first,count)=>{GLctx.drawArrays(mode,first,count)};var _emscripten_glDrawArraysInstanced=(mode,first,count,primcount)=>{GLctx.drawArraysInstanced(mode,first,count,primcount)};var _glDrawArraysInstanced=_emscripten_glDrawArraysInstanced;var _emscripten_glDrawArraysInstancedANGLE=_glDrawArraysInstanced;var _emscripten_glDrawArraysInstancedARB=_glDrawArraysInstanced;var _emscripten_glDrawArraysInstancedBaseInstanceWEBGL=(mode,first,count,instanceCount,baseInstance)=>{GLctx.dibvbi["drawArraysInstancedBaseInstanceWEBGL"](mode,first,count,instanceCount,baseInstance)};var _emscripten_glDrawArraysInstancedEXT=_glDrawArraysInstanced;var _emscripten_glDrawArraysInstancedNV=_glDrawArraysInstanced;var tempFixedLengthArray=[];var _emscripten_glDrawBuffers=(n,bufs)=>{var bufArray=tempFixedLengthArray[n];for(var i=0;i>2]}GLctx.drawBuffers(bufArray)};var _glDrawBuffers=_emscripten_glDrawBuffers;var _emscripten_glDrawBuffersEXT=_glDrawBuffers;var _emscripten_glDrawBuffersWEBGL=_glDrawBuffers;var _emscripten_glDrawElements=(mode,count,type,indices)=>{GLctx.drawElements(mode,count,type,indices)};var _emscripten_glDrawElementsInstanced=(mode,count,type,indices,primcount)=>{GLctx.drawElementsInstanced(mode,count,type,indices,primcount)};var _glDrawElementsInstanced=_emscripten_glDrawElementsInstanced;var _emscripten_glDrawElementsInstancedANGLE=_glDrawElementsInstanced;var _emscripten_glDrawElementsInstancedARB=_glDrawElementsInstanced;var _emscripten_glDrawElementsInstancedBaseVertexBaseInstanceWEBGL=(mode,count,type,offset,instanceCount,baseVertex,baseinstance)=>{GLctx.dibvbi["drawElementsInstancedBaseVertexBaseInstanceWEBGL"](mode,count,type,offset,instanceCount,baseVertex,baseinstance)};var _emscripten_glDrawElementsInstancedEXT=_glDrawElementsInstanced;var _emscripten_glDrawElementsInstancedNV=_glDrawElementsInstanced;var _glDrawElements=_emscripten_glDrawElements;var _emscripten_glDrawRangeElements=(mode,start,end,count,type,indices)=>{_glDrawElements(mode,count,type,indices)};var _emscripten_glEnable=x0=>GLctx.enable(x0);var _emscripten_glEnableVertexAttribArray=index=>{GLctx.enableVertexAttribArray(index)};var _emscripten_glEndQuery=x0=>GLctx.endQuery(x0);var _emscripten_glEndQueryEXT=target=>{GLctx.disjointTimerQueryExt["endQueryEXT"](target)};var _emscripten_glEndTransformFeedback=()=>GLctx.endTransformFeedback();var _emscripten_glFenceSync=(condition,flags)=>{var sync=GLctx.fenceSync(condition,flags);if(sync){var id=GL.getNewId(GL.syncs);sync.name=id;GL.syncs[id]=sync;return id}return 0};var _emscripten_glFinish=()=>GLctx.finish();var _emscripten_glFlush=()=>GLctx.flush();var _emscripten_glFramebufferRenderbuffer=(target,attachment,renderbuffertarget,renderbuffer)=>{GLctx.framebufferRenderbuffer(target,attachment,renderbuffertarget,GL.renderbuffers[renderbuffer])};var _emscripten_glFramebufferTexture2D=(target,attachment,textarget,texture,level)=>{GLctx.framebufferTexture2D(target,attachment,textarget,GL.textures[texture],level)};var _emscripten_glFramebufferTextureLayer=(target,attachment,texture,level,layer)=>{GLctx.framebufferTextureLayer(target,attachment,GL.textures[texture],level,layer)};var _emscripten_glFrontFace=x0=>GLctx.frontFace(x0);var _emscripten_glGenBuffers=(n,buffers)=>{GL.genObject(n,buffers,"createBuffer",GL.buffers)};var _emscripten_glGenFramebuffers=(n,ids)=>{GL.genObject(n,ids,"createFramebuffer",GL.framebuffers)};var _emscripten_glGenQueries=(n,ids)=>{GL.genObject(n,ids,"createQuery",GL.queries)};var _emscripten_glGenQueriesEXT=(n,ids)=>{for(var i=0;i>2]=0;return}var id=GL.getNewId(GL.queries);query.name=id;GL.queries[id]=query;HEAP32[ids+i*4>>2]=id}};var _emscripten_glGenRenderbuffers=(n,renderbuffers)=>{GL.genObject(n,renderbuffers,"createRenderbuffer",GL.renderbuffers)};var _emscripten_glGenSamplers=(n,samplers)=>{GL.genObject(n,samplers,"createSampler",GL.samplers)};var _emscripten_glGenTextures=(n,textures)=>{GL.genObject(n,textures,"createTexture",GL.textures)};var _emscripten_glGenTransformFeedbacks=(n,ids)=>{GL.genObject(n,ids,"createTransformFeedback",GL.transformFeedbacks)};var _emscripten_glGenVertexArrays=(n,arrays)=>{GL.genObject(n,arrays,"createVertexArray",GL.vaos)};var _glGenVertexArrays=_emscripten_glGenVertexArrays;var _emscripten_glGenVertexArraysOES=_glGenVertexArrays;var _emscripten_glGenerateMipmap=x0=>GLctx.generateMipmap(x0);var __glGetActiveAttribOrUniform=(funcName,program,index,bufSize,length,size,type,name)=>{program=GL.programs[program];var info=GLctx[funcName](program,index);if(info){var numBytesWrittenExclNull=name&&stringToUTF8(info.name,name,bufSize);if(length)HEAP32[length>>2]=numBytesWrittenExclNull;if(size)HEAP32[size>>2]=info.size;if(type)HEAP32[type>>2]=info.type}};var _emscripten_glGetActiveAttrib=(program,index,bufSize,length,size,type,name)=>__glGetActiveAttribOrUniform("getActiveAttrib",program,index,bufSize,length,size,type,name);var _emscripten_glGetActiveUniform=(program,index,bufSize,length,size,type,name)=>__glGetActiveAttribOrUniform("getActiveUniform",program,index,bufSize,length,size,type,name);var _emscripten_glGetActiveUniformBlockName=(program,uniformBlockIndex,bufSize,length,uniformBlockName)=>{program=GL.programs[program];var result=GLctx.getActiveUniformBlockName(program,uniformBlockIndex);if(!result)return;if(uniformBlockName&&bufSize>0){var numBytesWrittenExclNull=stringToUTF8(result,uniformBlockName,bufSize);if(length)HEAP32[length>>2]=numBytesWrittenExclNull}else{if(length)HEAP32[length>>2]=0}};var _emscripten_glGetActiveUniformBlockiv=(program,uniformBlockIndex,pname,params)=>{if(!params){GL.recordError(1281);return}program=GL.programs[program];if(pname==35393){var name=GLctx.getActiveUniformBlockName(program,uniformBlockIndex);HEAP32[params>>2]=name.length+1;return}var result=GLctx.getActiveUniformBlockParameter(program,uniformBlockIndex,pname);if(result===null)return;if(pname==35395){for(var i=0;i>2]=result[i]}}else{HEAP32[params>>2]=result}};var _emscripten_glGetActiveUniformsiv=(program,uniformCount,uniformIndices,pname,params)=>{if(!params){GL.recordError(1281);return}if(uniformCount>0&&uniformIndices==0){GL.recordError(1281);return}program=GL.programs[program];var ids=[];for(var i=0;i>2])}var result=GLctx.getActiveUniforms(program,ids,pname);if(!result)return;var len=result.length;for(var i=0;i>2]=result[i]}};var _emscripten_glGetAttachedShaders=(program,maxCount,count,shaders)=>{var result=GLctx.getAttachedShaders(GL.programs[program]);var len=result.length;if(len>maxCount){len=maxCount}HEAP32[count>>2]=len;for(var i=0;i>2]=id}};var _emscripten_glGetAttribLocation=(program,name)=>GLctx.getAttribLocation(GL.programs[program],UTF8ToString(name));var writeI53ToI64=(ptr,num)=>{HEAPU32[ptr>>2]=num;var lower=HEAPU32[ptr>>2];HEAPU32[ptr+4>>2]=(num-lower)/4294967296};var webglGetExtensions=()=>{var exts=getEmscriptenSupportedExtensions(GLctx);exts=exts.concat(exts.map(e=>"GL_"+e));return exts};var emscriptenWebGLGet=(name_,p,type)=>{if(!p){GL.recordError(1281);return}var ret=undefined;switch(name_){case 36346:ret=1;break;case 36344:if(type!=0&&type!=1){GL.recordError(1280)}return;case 34814:case 36345:ret=0;break;case 34466:var formats=GLctx.getParameter(34467);ret=formats?formats.length:0;break;case 33309:if(GL.currentContext.version<2){GL.recordError(1282);return}ret=webglGetExtensions().length;break;case 33307:case 33308:if(GL.currentContext.version<2){GL.recordError(1280);return}ret=name_==33307?3:0;break}if(ret===undefined){var result=GLctx.getParameter(name_);switch(typeof result){case"number":ret=result;break;case"boolean":ret=result?1:0;break;case"string":GL.recordError(1280);return;case"object":if(result===null){switch(name_){case 34964:case 35725:case 34965:case 36006:case 36007:case 32873:case 34229:case 36662:case 36663:case 35053:case 35055:case 36010:case 35097:case 35869:case 32874:case 36389:case 35983:case 35368:case 34068:{ret=0;break}default:{GL.recordError(1280);return}}}else if(result instanceof Float32Array||result instanceof Uint32Array||result instanceof Int32Array||result instanceof Array){for(var i=0;i>2]=result[i];break;case 2:HEAPF32[p+i*4>>2]=result[i];break;case 4:HEAP8[p+i]=result[i]?1:0;break}}return}else{try{ret=result.name|0}catch(e){GL.recordError(1280);err(`GL_INVALID_ENUM in glGet${type}v: Unknown object returned from WebGL getParameter(${name_})! (error: ${e})`);return}}break;default:GL.recordError(1280);err(`GL_INVALID_ENUM in glGet${type}v: Native code calling glGet${type}v(${name_}) and it returns ${result} of type ${typeof result}!`);return}}switch(type){case 1:writeI53ToI64(p,ret);break;case 0:HEAP32[p>>2]=ret;break;case 2:HEAPF32[p>>2]=ret;break;case 4:HEAP8[p]=ret?1:0;break}};var _emscripten_glGetBooleanv=(name_,p)=>emscriptenWebGLGet(name_,p,4);var _emscripten_glGetBufferParameteri64v=(target,value,data)=>{if(!data){GL.recordError(1281);return}writeI53ToI64(data,GLctx.getBufferParameter(target,value))};var _emscripten_glGetBufferParameteriv=(target,value,data)=>{if(!data){GL.recordError(1281);return}HEAP32[data>>2]=GLctx.getBufferParameter(target,value)};var _emscripten_glGetError=()=>{var error=GLctx.getError()||GL.lastError;GL.lastError=0;return error};var _emscripten_glGetFloatv=(name_,p)=>emscriptenWebGLGet(name_,p,2);var _emscripten_glGetFragDataLocation=(program,name)=>GLctx.getFragDataLocation(GL.programs[program],UTF8ToString(name));var _emscripten_glGetFramebufferAttachmentParameteriv=(target,attachment,pname,params)=>{var result=GLctx.getFramebufferAttachmentParameter(target,attachment,pname);if(result instanceof WebGLRenderbuffer||result instanceof WebGLTexture){result=result.name|0}HEAP32[params>>2]=result};var emscriptenWebGLGetIndexed=(target,index,data,type)=>{if(!data){GL.recordError(1281);return}var result=GLctx.getIndexedParameter(target,index);var ret;switch(typeof result){case"boolean":ret=result?1:0;break;case"number":ret=result;break;case"object":if(result===null){switch(target){case 35983:case 35368:ret=0;break;default:{GL.recordError(1280);return}}}else if(result instanceof WebGLBuffer){ret=result.name|0}else{GL.recordError(1280);return}break;default:GL.recordError(1280);return}switch(type){case 1:writeI53ToI64(data,ret);break;case 0:HEAP32[data>>2]=ret;break;case 2:HEAPF32[data>>2]=ret;break;case 4:HEAP8[data]=ret?1:0;break;default:abort("internal emscriptenWebGLGetIndexed() error, bad type: "+type)}};var _emscripten_glGetInteger64i_v=(target,index,data)=>emscriptenWebGLGetIndexed(target,index,data,1);var _emscripten_glGetInteger64v=(name_,p)=>{emscriptenWebGLGet(name_,p,1)};var _emscripten_glGetIntegeri_v=(target,index,data)=>emscriptenWebGLGetIndexed(target,index,data,0);var _emscripten_glGetIntegerv=(name_,p)=>emscriptenWebGLGet(name_,p,0);var _emscripten_glGetInternalformativ=(target,internalformat,pname,bufSize,params)=>{if(bufSize<0){GL.recordError(1281);return}if(!params){GL.recordError(1281);return}var ret=GLctx.getInternalformatParameter(target,internalformat,pname);if(ret===null)return;for(var i=0;i>2]=ret[i]}};var _emscripten_glGetProgramBinary=(program,bufSize,length,binaryFormat,binary)=>{GL.recordError(1282)};var _emscripten_glGetProgramInfoLog=(program,maxLength,length,infoLog)=>{var log=GLctx.getProgramInfoLog(GL.programs[program]);if(log===null)log="(unknown error)";var numBytesWrittenExclNull=maxLength>0&&infoLog?stringToUTF8(log,infoLog,maxLength):0;if(length)HEAP32[length>>2]=numBytesWrittenExclNull};var _emscripten_glGetProgramiv=(program,pname,p)=>{if(!p){GL.recordError(1281);return}if(program>=GL.counter){GL.recordError(1281);return}program=GL.programs[program];if(pname==35716){var log=GLctx.getProgramInfoLog(program);if(log===null)log="(unknown error)";HEAP32[p>>2]=log.length+1}else if(pname==35719){if(!program.maxUniformLength){var numActiveUniforms=GLctx.getProgramParameter(program,35718);for(var i=0;i>2]=program.maxUniformLength}else if(pname==35722){if(!program.maxAttributeLength){var numActiveAttributes=GLctx.getProgramParameter(program,35721);for(var i=0;i>2]=program.maxAttributeLength}else if(pname==35381){if(!program.maxUniformBlockNameLength){var numActiveUniformBlocks=GLctx.getProgramParameter(program,35382);for(var i=0;i>2]=program.maxUniformBlockNameLength}else{HEAP32[p>>2]=GLctx.getProgramParameter(program,pname)}};var _emscripten_glGetQueryObjecti64vEXT=(id,pname,params)=>{if(!params){GL.recordError(1281);return}var query=GL.queries[id];var param;if(GL.currentContext.version<2){param=GLctx.disjointTimerQueryExt["getQueryObjectEXT"](query,pname)}else{param=GLctx.getQueryParameter(query,pname)}var ret;if(typeof param=="boolean"){ret=param?1:0}else{ret=param}writeI53ToI64(params,ret)};var _emscripten_glGetQueryObjectivEXT=(id,pname,params)=>{if(!params){GL.recordError(1281);return}var query=GL.queries[id];var param=GLctx.disjointTimerQueryExt["getQueryObjectEXT"](query,pname);var ret;if(typeof param=="boolean"){ret=param?1:0}else{ret=param}HEAP32[params>>2]=ret};var _glGetQueryObjecti64vEXT=_emscripten_glGetQueryObjecti64vEXT;var _emscripten_glGetQueryObjectui64vEXT=_glGetQueryObjecti64vEXT;var _emscripten_glGetQueryObjectuiv=(id,pname,params)=>{if(!params){GL.recordError(1281);return}var query=GL.queries[id];var param=GLctx.getQueryParameter(query,pname);var ret;if(typeof param=="boolean"){ret=param?1:0}else{ret=param}HEAP32[params>>2]=ret};var _glGetQueryObjectivEXT=_emscripten_glGetQueryObjectivEXT;var _emscripten_glGetQueryObjectuivEXT=_glGetQueryObjectivEXT;var _emscripten_glGetQueryiv=(target,pname,params)=>{if(!params){GL.recordError(1281);return}HEAP32[params>>2]=GLctx.getQuery(target,pname)};var _emscripten_glGetQueryivEXT=(target,pname,params)=>{if(!params){GL.recordError(1281);return}HEAP32[params>>2]=GLctx.disjointTimerQueryExt["getQueryEXT"](target,pname)};var _emscripten_glGetRenderbufferParameteriv=(target,pname,params)=>{if(!params){GL.recordError(1281);return}HEAP32[params>>2]=GLctx.getRenderbufferParameter(target,pname)};var _emscripten_glGetSamplerParameterfv=(sampler,pname,params)=>{if(!params){GL.recordError(1281);return}HEAPF32[params>>2]=GLctx.getSamplerParameter(GL.samplers[sampler],pname)};var _emscripten_glGetSamplerParameteriv=(sampler,pname,params)=>{if(!params){GL.recordError(1281);return}HEAP32[params>>2]=GLctx.getSamplerParameter(GL.samplers[sampler],pname)};var _emscripten_glGetShaderInfoLog=(shader,maxLength,length,infoLog)=>{var log=GLctx.getShaderInfoLog(GL.shaders[shader]);if(log===null)log="(unknown error)";var numBytesWrittenExclNull=maxLength>0&&infoLog?stringToUTF8(log,infoLog,maxLength):0;if(length)HEAP32[length>>2]=numBytesWrittenExclNull};var _emscripten_glGetShaderPrecisionFormat=(shaderType,precisionType,range,precision)=>{var result=GLctx.getShaderPrecisionFormat(shaderType,precisionType);HEAP32[range>>2]=result.rangeMin;HEAP32[range+4>>2]=result.rangeMax;HEAP32[precision>>2]=result.precision};var _emscripten_glGetShaderSource=(shader,bufSize,length,source)=>{var result=GLctx.getShaderSource(GL.shaders[shader]);if(!result)return;var numBytesWrittenExclNull=bufSize>0&&source?stringToUTF8(result,source,bufSize):0;if(length)HEAP32[length>>2]=numBytesWrittenExclNull};var _emscripten_glGetShaderiv=(shader,pname,p)=>{if(!p){GL.recordError(1281);return}if(pname==35716){var log=GLctx.getShaderInfoLog(GL.shaders[shader]);if(log===null)log="(unknown error)";var logLength=log?log.length+1:0;HEAP32[p>>2]=logLength}else if(pname==35720){var source=GLctx.getShaderSource(GL.shaders[shader]);var sourceLength=source?source.length+1:0;HEAP32[p>>2]=sourceLength}else{HEAP32[p>>2]=GLctx.getShaderParameter(GL.shaders[shader],pname)}};var stringToNewUTF8=str=>{var size=lengthBytesUTF8(str)+1;var ret=_malloc(size);if(ret)stringToUTF8(str,ret,size);return ret};var _emscripten_glGetString=name_=>{var ret=GL.stringCache[name_];if(!ret){switch(name_){case 7939:ret=stringToNewUTF8(webglGetExtensions().join(" "));break;case 7936:case 7937:case 37445:case 37446:var s=GLctx.getParameter(name_);if(!s){GL.recordError(1280)}ret=s?stringToNewUTF8(s):0;break;case 7938:var webGLVersion=GLctx.getParameter(7938);var glVersion=`OpenGL ES 2.0 (${webGLVersion})`;if(GL.currentContext.version>=2)glVersion=`OpenGL ES 3.0 (${webGLVersion})`;ret=stringToNewUTF8(glVersion);break;case 35724:var glslVersion=GLctx.getParameter(35724);var ver_re=/^WebGL GLSL ES ([0-9]\.[0-9][0-9]?)(?:$| .*)/;var ver_num=glslVersion.match(ver_re);if(ver_num!==null){if(ver_num[1].length==3)ver_num[1]=ver_num[1]+"0";glslVersion=`OpenGL ES GLSL ES ${ver_num[1]} (${glslVersion})`}ret=stringToNewUTF8(glslVersion);break;default:GL.recordError(1280)}GL.stringCache[name_]=ret}return ret};var _emscripten_glGetStringi=(name,index)=>{if(GL.currentContext.version<2){GL.recordError(1282);return 0}var stringiCache=GL.stringiCache[name];if(stringiCache){if(index<0||index>=stringiCache.length){GL.recordError(1281);return 0}return stringiCache[index]}switch(name){case 7939:var exts=webglGetExtensions().map(stringToNewUTF8);stringiCache=GL.stringiCache[name]=exts;if(index<0||index>=stringiCache.length){GL.recordError(1281);return 0}return stringiCache[index];default:GL.recordError(1280);return 0}};var _emscripten_glGetSynciv=(sync,pname,bufSize,length,values)=>{if(bufSize<0){GL.recordError(1281);return}if(!values){GL.recordError(1281);return}var ret=GLctx.getSyncParameter(GL.syncs[sync],pname);if(ret!==null){HEAP32[values>>2]=ret;if(length)HEAP32[length>>2]=1}};var _emscripten_glGetTexParameterfv=(target,pname,params)=>{if(!params){GL.recordError(1281);return}HEAPF32[params>>2]=GLctx.getTexParameter(target,pname)};var _emscripten_glGetTexParameteriv=(target,pname,params)=>{if(!params){GL.recordError(1281);return}HEAP32[params>>2]=GLctx.getTexParameter(target,pname)};var _emscripten_glGetTransformFeedbackVarying=(program,index,bufSize,length,size,type,name)=>{program=GL.programs[program];var info=GLctx.getTransformFeedbackVarying(program,index);if(!info)return;if(name&&bufSize>0){var numBytesWrittenExclNull=stringToUTF8(info.name,name,bufSize);if(length)HEAP32[length>>2]=numBytesWrittenExclNull}else{if(length)HEAP32[length>>2]=0}if(size)HEAP32[size>>2]=info.size;if(type)HEAP32[type>>2]=info.type};var _emscripten_glGetUniformBlockIndex=(program,uniformBlockName)=>GLctx.getUniformBlockIndex(GL.programs[program],UTF8ToString(uniformBlockName));var _emscripten_glGetUniformIndices=(program,uniformCount,uniformNames,uniformIndices)=>{if(!uniformIndices){GL.recordError(1281);return}if(uniformCount>0&&(uniformNames==0||uniformIndices==0)){GL.recordError(1281);return}program=GL.programs[program];var names=[];for(var i=0;i>2]));var result=GLctx.getUniformIndices(program,names);if(!result)return;var len=result.length;for(var i=0;i>2]=result[i]}};var jstoi_q=str=>parseInt(str);var webglGetLeftBracePos=name=>name.slice(-1)=="]"&&name.lastIndexOf("[");var webglPrepareUniformLocationsBeforeFirstUse=program=>{var uniformLocsById=program.uniformLocsById,uniformSizeAndIdsByName=program.uniformSizeAndIdsByName,i,j;if(!uniformLocsById){program.uniformLocsById=uniformLocsById={};program.uniformArrayNamesById={};var numActiveUniforms=GLctx.getProgramParameter(program,35718);for(i=0;i0?nm.slice(0,lb):nm;var id=program.uniformIdCounter;program.uniformIdCounter+=sz;uniformSizeAndIdsByName[arrayName]=[sz,id];for(j=0;j{name=UTF8ToString(name);if(program=GL.programs[program]){webglPrepareUniformLocationsBeforeFirstUse(program);var uniformLocsById=program.uniformLocsById;var arrayIndex=0;var uniformBaseName=name;var leftBrace=webglGetLeftBracePos(name);if(leftBrace>0){arrayIndex=jstoi_q(name.slice(leftBrace+1))>>>0;uniformBaseName=name.slice(0,leftBrace)}var sizeAndId=program.uniformSizeAndIdsByName[uniformBaseName];if(sizeAndId&&arrayIndex{var p=GLctx.currentProgram;if(p){var webglLoc=p.uniformLocsById[location];if(typeof webglLoc=="number"){p.uniformLocsById[location]=webglLoc=GLctx.getUniformLocation(p,p.uniformArrayNamesById[location]+(webglLoc>0?`[${webglLoc}]`:""))}return webglLoc}else{GL.recordError(1282)}};var emscriptenWebGLGetUniform=(program,location,params,type)=>{if(!params){GL.recordError(1281);return}program=GL.programs[program];webglPrepareUniformLocationsBeforeFirstUse(program);var data=GLctx.getUniform(program,webglGetUniformLocation(location));if(typeof data=="number"||typeof data=="boolean"){switch(type){case 0:HEAP32[params>>2]=data;break;case 2:HEAPF32[params>>2]=data;break}}else{for(var i=0;i>2]=data[i];break;case 2:HEAPF32[params+i*4>>2]=data[i];break}}}};var _emscripten_glGetUniformfv=(program,location,params)=>{emscriptenWebGLGetUniform(program,location,params,2)};var _emscripten_glGetUniformiv=(program,location,params)=>{emscriptenWebGLGetUniform(program,location,params,0)};var _emscripten_glGetUniformuiv=(program,location,params)=>emscriptenWebGLGetUniform(program,location,params,0);var emscriptenWebGLGetVertexAttrib=(index,pname,params,type)=>{if(!params){GL.recordError(1281);return}var data=GLctx.getVertexAttrib(index,pname);if(pname==34975){HEAP32[params>>2]=data&&data["name"]}else if(typeof data=="number"||typeof data=="boolean"){switch(type){case 0:HEAP32[params>>2]=data;break;case 2:HEAPF32[params>>2]=data;break;case 5:HEAP32[params>>2]=Math.fround(data);break}}else{for(var i=0;i>2]=data[i];break;case 2:HEAPF32[params+i*4>>2]=data[i];break;case 5:HEAP32[params+i*4>>2]=Math.fround(data[i]);break}}}};var _emscripten_glGetVertexAttribIiv=(index,pname,params)=>{emscriptenWebGLGetVertexAttrib(index,pname,params,0)};var _glGetVertexAttribIiv=_emscripten_glGetVertexAttribIiv;var _emscripten_glGetVertexAttribIuiv=_glGetVertexAttribIiv;var _emscripten_glGetVertexAttribPointerv=(index,pname,pointer)=>{if(!pointer){GL.recordError(1281);return}HEAP32[pointer>>2]=GLctx.getVertexAttribOffset(index,pname)};var _emscripten_glGetVertexAttribfv=(index,pname,params)=>{emscriptenWebGLGetVertexAttrib(index,pname,params,2)};var _emscripten_glGetVertexAttribiv=(index,pname,params)=>{emscriptenWebGLGetVertexAttrib(index,pname,params,5)};var _emscripten_glHint=(x0,x1)=>GLctx.hint(x0,x1);var _emscripten_glInvalidateFramebuffer=(target,numAttachments,attachments)=>{var list=tempFixedLengthArray[numAttachments];for(var i=0;i>2]}GLctx.invalidateFramebuffer(target,list)};var _emscripten_glInvalidateSubFramebuffer=(target,numAttachments,attachments,x,y,width,height)=>{var list=tempFixedLengthArray[numAttachments];for(var i=0;i>2]}GLctx.invalidateSubFramebuffer(target,list,x,y,width,height)};var _emscripten_glIsBuffer=buffer=>{var b=GL.buffers[buffer];if(!b)return 0;return GLctx.isBuffer(b)};var _emscripten_glIsEnabled=x0=>GLctx.isEnabled(x0);var _emscripten_glIsFramebuffer=framebuffer=>{var fb=GL.framebuffers[framebuffer];if(!fb)return 0;return GLctx.isFramebuffer(fb)};var _emscripten_glIsProgram=program=>{program=GL.programs[program];if(!program)return 0;return GLctx.isProgram(program)};var _emscripten_glIsQuery=id=>{var query=GL.queries[id];if(!query)return 0;return GLctx.isQuery(query)};var _emscripten_glIsQueryEXT=id=>{var query=GL.queries[id];if(!query)return 0;return GLctx.disjointTimerQueryExt["isQueryEXT"](query)};var _emscripten_glIsRenderbuffer=renderbuffer=>{var rb=GL.renderbuffers[renderbuffer];if(!rb)return 0;return GLctx.isRenderbuffer(rb)};var _emscripten_glIsSampler=id=>{var sampler=GL.samplers[id];if(!sampler)return 0;return GLctx.isSampler(sampler)};var _emscripten_glIsShader=shader=>{var s=GL.shaders[shader];if(!s)return 0;return GLctx.isShader(s)};var _emscripten_glIsSync=sync=>GLctx.isSync(GL.syncs[sync]);var _emscripten_glIsTexture=id=>{var texture=GL.textures[id];if(!texture)return 0;return GLctx.isTexture(texture)};var _emscripten_glIsTransformFeedback=id=>GLctx.isTransformFeedback(GL.transformFeedbacks[id]);var _emscripten_glIsVertexArray=array=>{var vao=GL.vaos[array];if(!vao)return 0;return GLctx.isVertexArray(vao)};var _glIsVertexArray=_emscripten_glIsVertexArray;var _emscripten_glIsVertexArrayOES=_glIsVertexArray;var _emscripten_glLineWidth=x0=>GLctx.lineWidth(x0);var _emscripten_glLinkProgram=program=>{program=GL.programs[program];GLctx.linkProgram(program);program.uniformLocsById=0;program.uniformSizeAndIdsByName={}};var _emscripten_glMultiDrawArraysInstancedBaseInstanceWEBGL=(mode,firsts,counts,instanceCounts,baseInstances,drawCount)=>{GLctx.mdibvbi["multiDrawArraysInstancedBaseInstanceWEBGL"](mode,HEAP32,firsts>>2,HEAP32,counts>>2,HEAP32,instanceCounts>>2,HEAPU32,baseInstances>>2,drawCount)};var _emscripten_glMultiDrawElementsInstancedBaseVertexBaseInstanceWEBGL=(mode,counts,type,offsets,instanceCounts,baseVertices,baseInstances,drawCount)=>{GLctx.mdibvbi["multiDrawElementsInstancedBaseVertexBaseInstanceWEBGL"](mode,HEAP32,counts>>2,type,HEAP32,offsets>>2,HEAP32,instanceCounts>>2,HEAP32,baseVertices>>2,HEAPU32,baseInstances>>2,drawCount)};var _emscripten_glPauseTransformFeedback=()=>GLctx.pauseTransformFeedback();var _emscripten_glPixelStorei=(pname,param)=>{if(pname==3317){GL.unpackAlignment=param}else if(pname==3314){GL.unpackRowLength=param}GLctx.pixelStorei(pname,param)};var _emscripten_glPolygonModeWEBGL=(face,mode)=>{GLctx.webglPolygonMode["polygonModeWEBGL"](face,mode)};var _emscripten_glPolygonOffset=(x0,x1)=>GLctx.polygonOffset(x0,x1);var _emscripten_glPolygonOffsetClampEXT=(factor,units,clamp)=>{GLctx.extPolygonOffsetClamp["polygonOffsetClampEXT"](factor,units,clamp)};var _emscripten_glProgramBinary=(program,binaryFormat,binary,length)=>{GL.recordError(1280)};var _emscripten_glProgramParameteri=(program,pname,value)=>{GL.recordError(1280)};var _emscripten_glQueryCounterEXT=(id,target)=>{GLctx.disjointTimerQueryExt["queryCounterEXT"](GL.queries[id],target)};var _emscripten_glReadBuffer=x0=>GLctx.readBuffer(x0);var computeUnpackAlignedImageSize=(width,height,sizePerPixel)=>{function roundedToNextMultipleOf(x,y){return x+y-1&-y}var plainRowSize=(GL.unpackRowLength||width)*sizePerPixel;var alignedRowSize=roundedToNextMultipleOf(plainRowSize,GL.unpackAlignment);return height*alignedRowSize};var colorChannelsInGlTextureFormat=format=>{var colorChannels={5:3,6:4,8:2,29502:3,29504:4,26917:2,26918:2,29846:3,29847:4};return colorChannels[format-6402]||1};var heapObjectForWebGLType=type=>{type-=5120;if(type==0)return HEAP8;if(type==1)return HEAPU8;if(type==2)return HEAP16;if(type==4)return HEAP32;if(type==6)return HEAPF32;if(type==5||type==28922||type==28520||type==30779||type==30782)return HEAPU32;return HEAPU16};var toTypedArrayIndex=(pointer,heap)=>pointer>>>31-Math.clz32(heap.BYTES_PER_ELEMENT);var emscriptenWebGLGetTexPixelData=(type,format,width,height,pixels,internalFormat)=>{var heap=heapObjectForWebGLType(type);var sizePerPixel=colorChannelsInGlTextureFormat(format)*heap.BYTES_PER_ELEMENT;var bytes=computeUnpackAlignedImageSize(width,height,sizePerPixel);return heap.subarray(toTypedArrayIndex(pixels,heap),toTypedArrayIndex(pixels+bytes,heap))};var _emscripten_glReadPixels=(x,y,width,height,format,type,pixels)=>{if(GL.currentContext.version>=2){if(GLctx.currentPixelPackBufferBinding){GLctx.readPixels(x,y,width,height,format,type,pixels);return}var heap=heapObjectForWebGLType(type);var target=toTypedArrayIndex(pixels,heap);GLctx.readPixels(x,y,width,height,format,type,heap,target);return}var pixelData=emscriptenWebGLGetTexPixelData(type,format,width,height,pixels,format);if(!pixelData){GL.recordError(1280);return}GLctx.readPixels(x,y,width,height,format,type,pixelData)};var _emscripten_glReleaseShaderCompiler=()=>{};var _emscripten_glRenderbufferStorage=(x0,x1,x2,x3)=>GLctx.renderbufferStorage(x0,x1,x2,x3);var _emscripten_glRenderbufferStorageMultisample=(x0,x1,x2,x3,x4)=>GLctx.renderbufferStorageMultisample(x0,x1,x2,x3,x4);var _emscripten_glResumeTransformFeedback=()=>GLctx.resumeTransformFeedback();var _emscripten_glSampleCoverage=(value,invert)=>{GLctx.sampleCoverage(value,!!invert)};var _emscripten_glSamplerParameterf=(sampler,pname,param)=>{GLctx.samplerParameterf(GL.samplers[sampler],pname,param)};var _emscripten_glSamplerParameterfv=(sampler,pname,params)=>{var param=HEAPF32[params>>2];GLctx.samplerParameterf(GL.samplers[sampler],pname,param)};var _emscripten_glSamplerParameteri=(sampler,pname,param)=>{GLctx.samplerParameteri(GL.samplers[sampler],pname,param)};var _emscripten_glSamplerParameteriv=(sampler,pname,params)=>{var param=HEAP32[params>>2];GLctx.samplerParameteri(GL.samplers[sampler],pname,param)};var _emscripten_glScissor=(x0,x1,x2,x3)=>GLctx.scissor(x0,x1,x2,x3);var _emscripten_glShaderBinary=(count,shaders,binaryformat,binary,length)=>{GL.recordError(1280)};var _emscripten_glShaderSource=(shader,count,string,length)=>{var source=GL.getSource(shader,count,string,length);GLctx.shaderSource(GL.shaders[shader],source)};var _emscripten_glStencilFunc=(x0,x1,x2)=>GLctx.stencilFunc(x0,x1,x2);var _emscripten_glStencilFuncSeparate=(x0,x1,x2,x3)=>GLctx.stencilFuncSeparate(x0,x1,x2,x3);var _emscripten_glStencilMask=x0=>GLctx.stencilMask(x0);var _emscripten_glStencilMaskSeparate=(x0,x1)=>GLctx.stencilMaskSeparate(x0,x1);var _emscripten_glStencilOp=(x0,x1,x2)=>GLctx.stencilOp(x0,x1,x2);var _emscripten_glStencilOpSeparate=(x0,x1,x2,x3)=>GLctx.stencilOpSeparate(x0,x1,x2,x3);var _emscripten_glTexImage2D=(target,level,internalFormat,width,height,border,format,type,pixels)=>{if(GL.currentContext.version>=2){if(GLctx.currentPixelUnpackBufferBinding){GLctx.texImage2D(target,level,internalFormat,width,height,border,format,type,pixels);return}if(pixels){var heap=heapObjectForWebGLType(type);var index=toTypedArrayIndex(pixels,heap);GLctx.texImage2D(target,level,internalFormat,width,height,border,format,type,heap,index);return}}var pixelData=pixels?emscriptenWebGLGetTexPixelData(type,format,width,height,pixels,internalFormat):null;GLctx.texImage2D(target,level,internalFormat,width,height,border,format,type,pixelData)};var _emscripten_glTexImage3D=(target,level,internalFormat,width,height,depth,border,format,type,pixels)=>{if(GLctx.currentPixelUnpackBufferBinding){GLctx.texImage3D(target,level,internalFormat,width,height,depth,border,format,type,pixels)}else if(pixels){var heap=heapObjectForWebGLType(type);GLctx.texImage3D(target,level,internalFormat,width,height,depth,border,format,type,heap,toTypedArrayIndex(pixels,heap))}else{GLctx.texImage3D(target,level,internalFormat,width,height,depth,border,format,type,null)}};var _emscripten_glTexParameterf=(x0,x1,x2)=>GLctx.texParameterf(x0,x1,x2);var _emscripten_glTexParameterfv=(target,pname,params)=>{var param=HEAPF32[params>>2];GLctx.texParameterf(target,pname,param)};var _emscripten_glTexParameteri=(x0,x1,x2)=>GLctx.texParameteri(x0,x1,x2);var _emscripten_glTexParameteriv=(target,pname,params)=>{var param=HEAP32[params>>2];GLctx.texParameteri(target,pname,param)};var _emscripten_glTexStorage2D=(x0,x1,x2,x3,x4)=>GLctx.texStorage2D(x0,x1,x2,x3,x4);var _emscripten_glTexStorage3D=(x0,x1,x2,x3,x4,x5)=>GLctx.texStorage3D(x0,x1,x2,x3,x4,x5);var _emscripten_glTexSubImage2D=(target,level,xoffset,yoffset,width,height,format,type,pixels)=>{if(GL.currentContext.version>=2){if(GLctx.currentPixelUnpackBufferBinding){GLctx.texSubImage2D(target,level,xoffset,yoffset,width,height,format,type,pixels);return}if(pixels){var heap=heapObjectForWebGLType(type);GLctx.texSubImage2D(target,level,xoffset,yoffset,width,height,format,type,heap,toTypedArrayIndex(pixels,heap));return}}var pixelData=pixels?emscriptenWebGLGetTexPixelData(type,format,width,height,pixels,0):null;GLctx.texSubImage2D(target,level,xoffset,yoffset,width,height,format,type,pixelData)};var _emscripten_glTexSubImage3D=(target,level,xoffset,yoffset,zoffset,width,height,depth,format,type,pixels)=>{if(GLctx.currentPixelUnpackBufferBinding){GLctx.texSubImage3D(target,level,xoffset,yoffset,zoffset,width,height,depth,format,type,pixels)}else if(pixels){var heap=heapObjectForWebGLType(type);GLctx.texSubImage3D(target,level,xoffset,yoffset,zoffset,width,height,depth,format,type,heap,toTypedArrayIndex(pixels,heap))}else{GLctx.texSubImage3D(target,level,xoffset,yoffset,zoffset,width,height,depth,format,type,null)}};var _emscripten_glTransformFeedbackVaryings=(program,count,varyings,bufferMode)=>{program=GL.programs[program];var vars=[];for(var i=0;i>2]));GLctx.transformFeedbackVaryings(program,vars,bufferMode)};var _emscripten_glUniform1f=(location,v0)=>{GLctx.uniform1f(webglGetUniformLocation(location),v0)};var miniTempWebGLFloatBuffers=[];var _emscripten_glUniform1fv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform1fv(webglGetUniformLocation(location),HEAPF32,value>>2,count);return}if(count<=288){var view=miniTempWebGLFloatBuffers[count];for(var i=0;i>2]}}else{var view=HEAPF32.subarray(value>>2,value+count*4>>2)}GLctx.uniform1fv(webglGetUniformLocation(location),view)};var _emscripten_glUniform1i=(location,v0)=>{GLctx.uniform1i(webglGetUniformLocation(location),v0)};var miniTempWebGLIntBuffers=[];var _emscripten_glUniform1iv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform1iv(webglGetUniformLocation(location),HEAP32,value>>2,count);return}if(count<=288){var view=miniTempWebGLIntBuffers[count];for(var i=0;i>2]}}else{var view=HEAP32.subarray(value>>2,value+count*4>>2)}GLctx.uniform1iv(webglGetUniformLocation(location),view)};var _emscripten_glUniform1ui=(location,v0)=>{GLctx.uniform1ui(webglGetUniformLocation(location),v0)};var _emscripten_glUniform1uiv=(location,count,value)=>{count&&GLctx.uniform1uiv(webglGetUniformLocation(location),HEAPU32,value>>2,count)};var _emscripten_glUniform2f=(location,v0,v1)=>{GLctx.uniform2f(webglGetUniformLocation(location),v0,v1)};var _emscripten_glUniform2fv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform2fv(webglGetUniformLocation(location),HEAPF32,value>>2,count*2);return}if(count<=144){count*=2;var view=miniTempWebGLFloatBuffers[count];for(var i=0;i>2];view[i+1]=HEAPF32[value+(4*i+4)>>2]}}else{var view=HEAPF32.subarray(value>>2,value+count*8>>2)}GLctx.uniform2fv(webglGetUniformLocation(location),view)};var _emscripten_glUniform2i=(location,v0,v1)=>{GLctx.uniform2i(webglGetUniformLocation(location),v0,v1)};var _emscripten_glUniform2iv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform2iv(webglGetUniformLocation(location),HEAP32,value>>2,count*2);return}if(count<=144){count*=2;var view=miniTempWebGLIntBuffers[count];for(var i=0;i>2];view[i+1]=HEAP32[value+(4*i+4)>>2]}}else{var view=HEAP32.subarray(value>>2,value+count*8>>2)}GLctx.uniform2iv(webglGetUniformLocation(location),view)};var _emscripten_glUniform2ui=(location,v0,v1)=>{GLctx.uniform2ui(webglGetUniformLocation(location),v0,v1)};var _emscripten_glUniform2uiv=(location,count,value)=>{count&&GLctx.uniform2uiv(webglGetUniformLocation(location),HEAPU32,value>>2,count*2)};var _emscripten_glUniform3f=(location,v0,v1,v2)=>{GLctx.uniform3f(webglGetUniformLocation(location),v0,v1,v2)};var _emscripten_glUniform3fv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform3fv(webglGetUniformLocation(location),HEAPF32,value>>2,count*3);return}if(count<=96){count*=3;var view=miniTempWebGLFloatBuffers[count];for(var i=0;i>2];view[i+1]=HEAPF32[value+(4*i+4)>>2];view[i+2]=HEAPF32[value+(4*i+8)>>2]}}else{var view=HEAPF32.subarray(value>>2,value+count*12>>2)}GLctx.uniform3fv(webglGetUniformLocation(location),view)};var _emscripten_glUniform3i=(location,v0,v1,v2)=>{GLctx.uniform3i(webglGetUniformLocation(location),v0,v1,v2)};var _emscripten_glUniform3iv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform3iv(webglGetUniformLocation(location),HEAP32,value>>2,count*3);return}if(count<=96){count*=3;var view=miniTempWebGLIntBuffers[count];for(var i=0;i>2];view[i+1]=HEAP32[value+(4*i+4)>>2];view[i+2]=HEAP32[value+(4*i+8)>>2]}}else{var view=HEAP32.subarray(value>>2,value+count*12>>2)}GLctx.uniform3iv(webglGetUniformLocation(location),view)};var _emscripten_glUniform3ui=(location,v0,v1,v2)=>{GLctx.uniform3ui(webglGetUniformLocation(location),v0,v1,v2)};var _emscripten_glUniform3uiv=(location,count,value)=>{count&&GLctx.uniform3uiv(webglGetUniformLocation(location),HEAPU32,value>>2,count*3)};var _emscripten_glUniform4f=(location,v0,v1,v2,v3)=>{GLctx.uniform4f(webglGetUniformLocation(location),v0,v1,v2,v3)};var _emscripten_glUniform4fv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform4fv(webglGetUniformLocation(location),HEAPF32,value>>2,count*4);return}if(count<=72){var view=miniTempWebGLFloatBuffers[4*count];var heap=HEAPF32;value=value>>2;count*=4;for(var i=0;i>2,value+count*16>>2)}GLctx.uniform4fv(webglGetUniformLocation(location),view)};var _emscripten_glUniform4i=(location,v0,v1,v2,v3)=>{GLctx.uniform4i(webglGetUniformLocation(location),v0,v1,v2,v3)};var _emscripten_glUniform4iv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform4iv(webglGetUniformLocation(location),HEAP32,value>>2,count*4);return}if(count<=72){count*=4;var view=miniTempWebGLIntBuffers[count];for(var i=0;i>2];view[i+1]=HEAP32[value+(4*i+4)>>2];view[i+2]=HEAP32[value+(4*i+8)>>2];view[i+3]=HEAP32[value+(4*i+12)>>2]}}else{var view=HEAP32.subarray(value>>2,value+count*16>>2)}GLctx.uniform4iv(webglGetUniformLocation(location),view)};var _emscripten_glUniform4ui=(location,v0,v1,v2,v3)=>{GLctx.uniform4ui(webglGetUniformLocation(location),v0,v1,v2,v3)};var _emscripten_glUniform4uiv=(location,count,value)=>{count&&GLctx.uniform4uiv(webglGetUniformLocation(location),HEAPU32,value>>2,count*4)};var _emscripten_glUniformBlockBinding=(program,uniformBlockIndex,uniformBlockBinding)=>{program=GL.programs[program];GLctx.uniformBlockBinding(program,uniformBlockIndex,uniformBlockBinding)};var _emscripten_glUniformMatrix2fv=(location,count,transpose,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniformMatrix2fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*4);return}if(count<=72){count*=4;var view=miniTempWebGLFloatBuffers[count];for(var i=0;i>2];view[i+1]=HEAPF32[value+(4*i+4)>>2];view[i+2]=HEAPF32[value+(4*i+8)>>2];view[i+3]=HEAPF32[value+(4*i+12)>>2]}}else{var view=HEAPF32.subarray(value>>2,value+count*16>>2)}GLctx.uniformMatrix2fv(webglGetUniformLocation(location),!!transpose,view)};var _emscripten_glUniformMatrix2x3fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix2x3fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*6)};var _emscripten_glUniformMatrix2x4fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix2x4fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*8)};var _emscripten_glUniformMatrix3fv=(location,count,transpose,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniformMatrix3fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*9);return}if(count<=32){count*=9;var view=miniTempWebGLFloatBuffers[count];for(var i=0;i>2];view[i+1]=HEAPF32[value+(4*i+4)>>2];view[i+2]=HEAPF32[value+(4*i+8)>>2];view[i+3]=HEAPF32[value+(4*i+12)>>2];view[i+4]=HEAPF32[value+(4*i+16)>>2];view[i+5]=HEAPF32[value+(4*i+20)>>2];view[i+6]=HEAPF32[value+(4*i+24)>>2];view[i+7]=HEAPF32[value+(4*i+28)>>2];view[i+8]=HEAPF32[value+(4*i+32)>>2]}}else{var view=HEAPF32.subarray(value>>2,value+count*36>>2)}GLctx.uniformMatrix3fv(webglGetUniformLocation(location),!!transpose,view)};var _emscripten_glUniformMatrix3x2fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix3x2fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*6)};var _emscripten_glUniformMatrix3x4fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix3x4fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*12)};var _emscripten_glUniformMatrix4fv=(location,count,transpose,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniformMatrix4fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*16);return}if(count<=18){var view=miniTempWebGLFloatBuffers[16*count];var heap=HEAPF32;value=value>>2;count*=16;for(var i=0;i>2,value+count*64>>2)}GLctx.uniformMatrix4fv(webglGetUniformLocation(location),!!transpose,view)};var _emscripten_glUniformMatrix4x2fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix4x2fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*8)};var _emscripten_glUniformMatrix4x3fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix4x3fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*12)};var _emscripten_glUseProgram=program=>{program=GL.programs[program];GLctx.useProgram(program);GLctx.currentProgram=program};var _emscripten_glValidateProgram=program=>{GLctx.validateProgram(GL.programs[program])};var _emscripten_glVertexAttrib1f=(x0,x1)=>GLctx.vertexAttrib1f(x0,x1);var _emscripten_glVertexAttrib1fv=(index,v)=>{GLctx.vertexAttrib1f(index,HEAPF32[v>>2])};var _emscripten_glVertexAttrib2f=(x0,x1,x2)=>GLctx.vertexAttrib2f(x0,x1,x2);var _emscripten_glVertexAttrib2fv=(index,v)=>{GLctx.vertexAttrib2f(index,HEAPF32[v>>2],HEAPF32[v+4>>2])};var _emscripten_glVertexAttrib3f=(x0,x1,x2,x3)=>GLctx.vertexAttrib3f(x0,x1,x2,x3);var _emscripten_glVertexAttrib3fv=(index,v)=>{GLctx.vertexAttrib3f(index,HEAPF32[v>>2],HEAPF32[v+4>>2],HEAPF32[v+8>>2])};var _emscripten_glVertexAttrib4f=(x0,x1,x2,x3,x4)=>GLctx.vertexAttrib4f(x0,x1,x2,x3,x4);var _emscripten_glVertexAttrib4fv=(index,v)=>{GLctx.vertexAttrib4f(index,HEAPF32[v>>2],HEAPF32[v+4>>2],HEAPF32[v+8>>2],HEAPF32[v+12>>2])};var _emscripten_glVertexAttribDivisor=(index,divisor)=>{GLctx.vertexAttribDivisor(index,divisor)};var _glVertexAttribDivisor=_emscripten_glVertexAttribDivisor;var _emscripten_glVertexAttribDivisorANGLE=_glVertexAttribDivisor;var _emscripten_glVertexAttribDivisorARB=_glVertexAttribDivisor;var _emscripten_glVertexAttribDivisorEXT=_glVertexAttribDivisor;var _emscripten_glVertexAttribDivisorNV=_glVertexAttribDivisor;var _emscripten_glVertexAttribI4i=(x0,x1,x2,x3,x4)=>GLctx.vertexAttribI4i(x0,x1,x2,x3,x4);var _emscripten_glVertexAttribI4iv=(index,v)=>{GLctx.vertexAttribI4i(index,HEAP32[v>>2],HEAP32[v+4>>2],HEAP32[v+8>>2],HEAP32[v+12>>2])};var _emscripten_glVertexAttribI4ui=(x0,x1,x2,x3,x4)=>GLctx.vertexAttribI4ui(x0,x1,x2,x3,x4);var _emscripten_glVertexAttribI4uiv=(index,v)=>{GLctx.vertexAttribI4ui(index,HEAPU32[v>>2],HEAPU32[v+4>>2],HEAPU32[v+8>>2],HEAPU32[v+12>>2])};var _emscripten_glVertexAttribIPointer=(index,size,type,stride,ptr)=>{GLctx.vertexAttribIPointer(index,size,type,stride,ptr)};var _emscripten_glVertexAttribPointer=(index,size,type,normalized,stride,ptr)=>{GLctx.vertexAttribPointer(index,size,type,!!normalized,stride,ptr)};var _emscripten_glViewport=(x0,x1,x2,x3)=>GLctx.viewport(x0,x1,x2,x3);var _emscripten_glWaitSync=(sync,flags,timeout)=>{timeout=Number(timeout);GLctx.waitSync(GL.syncs[sync],flags,timeout)};var wasmTableMirror=[];var getWasmTableEntry=funcPtr=>{var func=wasmTableMirror[funcPtr];if(!func){wasmTableMirror[funcPtr]=func=wasmTable.get(funcPtr)}return func};var _emscripten_request_animation_frame_loop=(cb,userData)=>{function tick(timeStamp){if(getWasmTableEntry(cb)(timeStamp,userData)){requestAnimationFrame(tick)}}return requestAnimationFrame(tick)};var growMemory=size=>{var oldHeapSize=wasmMemory.buffer.byteLength;var pages=(size-oldHeapSize+65535)/65536|0;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){}};var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignMemory(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}return false};var ENV={};var getExecutableName=()=>thisProgram||"./this.program";var getEnvStrings=()=>{if(!getEnvStrings.strings){var lang=(globalThis.navigator?.language??"C").replace("-","_")+".UTF-8";var env={USER:"web_user",LOGNAME:"web_user",PATH:"/",PWD:"/",HOME:"/home/web_user",LANG:lang,_:getExecutableName()};for(var x in ENV){if(ENV[x]===undefined)delete env[x];else env[x]=ENV[x]}var strings=[];for(var x in env){strings.push(`${x}=${env[x]}`)}getEnvStrings.strings=strings}return getEnvStrings.strings};var _environ_get=(__environ,environ_buf)=>{var bufSize=0;var envp=0;for(var string of getEnvStrings()){var ptr=environ_buf+bufSize;HEAPU32[__environ+envp>>2]=ptr;bufSize+=stringToUTF8(string,ptr,Infinity)+1;envp+=4}return 0};var _environ_sizes_get=(penviron_count,penviron_buf_size)=>{var strings=getEnvStrings();HEAPU32[penviron_count>>2]=strings.length;var bufSize=0;for(var string of strings){bufSize+=lengthBytesUTF8(string)+1}HEAPU32[penviron_buf_size>>2]=bufSize;return 0};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;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];var len=HEAPU32[iov+4>>2];iov+=8;var curr=FS.read(stream,HEAP8,ptr,len,offset);if(curr<0)return-1;ret+=curr;if(curr>2]=num;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}function _fd_read(fd,iov,iovcnt,pnum){try{var stream=SYSCALLS.getStreamFromFD(fd);var num=doReadv(stream,iov,iovcnt);HEAPU32[pnum>>2]=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);try{if(isNaN(offset))return 61;var stream=SYSCALLS.getStreamFromFD(fd);FS.llseek(stream,offset,whence);HEAP64[newOffset>>3]=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];var len=HEAPU32[iov+4>>2];iov+=8;var curr=FS.write(stream,HEAP8,ptr,len,offset);if(curr<0)return-1;ret+=curr;if(curr>2]=num;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var _glGetIntegerv=_emscripten_glGetIntegerv;var _glGetString=_emscripten_glGetString;var _glGetStringi=_emscripten_glGetStringi;var _llvm_eh_typeid_for=type=>type;function _random_get(buffer,size){try{randomFill(HEAPU8.subarray(buffer,buffer+size));return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var handleException=e=>{if(e instanceof ExitStatus||e=="unwind"){return EXITSTATUS}quit_(1,e)};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};FS.createPreloadedFile=FS_createPreloadedFile;FS.preloadFile=FS_preloadFile;FS.staticInit();for(let i=0;i<32;++i)tempFixedLengthArray.push(new Array(i));var miniTempWebGLFloatBuffersStorage=new Float32Array(288);for(var i=0;i<=288;++i){miniTempWebGLFloatBuffers[i]=miniTempWebGLFloatBuffersStorage.subarray(0,i)}var miniTempWebGLIntBuffersStorage=new Int32Array(288);for(var i=0;i<=288;++i){miniTempWebGLIntBuffers[i]=miniTempWebGLIntBuffersStorage.subarray(0,i)}{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["UTF8ToString"]=UTF8ToString;Module["stringToUTF8"]=stringToUTF8;Module["lengthBytesUTF8"]=lengthBytesUTF8;Module["GL"]=GL;var _malloc,_add_font,_add_image,_add_image_with_rid,_allocate,_apply_scene_transactions,_command,_deallocate,_destroy,_devtools_rendering_set_show_fps_meter,_devtools_rendering_set_show_hit_testing,_devtools_rendering_set_show_ruler,_devtools_rendering_set_show_stats,_devtools_rendering_set_show_tiles,_drain_missing_images,_export_node_as,_get_default_fallback_fonts,_get_image_bytes,_get_image_size,_get_node_absolute_bounding_box,_get_node_id_from_point,_get_node_ids_from_envelope,_get_node_ids_from_point,_grida_fonts_analyze_family,_grida_fonts_free,_grida_fonts_parse_font,_grida_markdown_to_html,_grida_svg_optimize,_grida_svg_to_document,_has_missing_fonts,_highlight_strokes,_init,_init_with_backend,_list_available_fonts,_list_missing_fonts,_load_benchmark_scene,_load_dummy_scene,_load_scene_grida,_load_scene_grida1,_pointer_move,_redraw,_resize_surface,_resolve_image,_runtime_renderer_set_layer_compositing,_runtime_renderer_set_outline_mode,_runtime_renderer_set_pixel_preview_scale,_runtime_renderer_set_pixel_preview_stable,_runtime_renderer_set_render_policy_flags,_runtime_renderer_set_skip_layout,_set_debug,_set_default_fallback_fonts,_set_main_camera_transform,_set_surface_overlay_config,_set_verbose,_surface_get_cursor,_surface_get_hovered_node,_surface_get_selected_nodes,_surface_pointer_down,_surface_pointer_move,_surface_pointer_up,_surface_set_selection,_switch_scene,_text_edit_command,_text_edit_enter,_text_edit_exit,_text_edit_get_caret_rect,_text_edit_get_selected_html,_text_edit_get_selected_text,_text_edit_get_selection_rects,_text_edit_get_text,_text_edit_ime_cancel,_text_edit_ime_commit,_text_edit_ime_set_preedit,_text_edit_is_active,_text_edit_paste_html,_text_edit_paste_text,_text_edit_pointer_down,_text_edit_pointer_move,_text_edit_pointer_up,_text_edit_redo,_text_edit_set_color,_text_edit_set_font_family,_text_edit_set_font_size,_text_edit_tick,_text_edit_toggle_bold,_text_edit_toggle_italic,_text_edit_toggle_strikethrough,_text_edit_toggle_underline,_text_edit_undo,_tick,_to_vector_network,_toggle_debug,_main,_emscripten_builtin_memalign,_setThrew,__emscripten_tempret_set,__emscripten_stack_restore,__emscripten_stack_alloc,_emscripten_stack_get_current,___cxa_decrement_exception_refcount,___cxa_increment_exception_refcount,___cxa_can_catch,___cxa_get_exception_ptr,memory,__indirect_function_table,wasmMemory,wasmTable;function assignWasmExports(wasmExports){_malloc=wasmExports["Og"];_add_font=Module["_add_font"]=wasmExports["Qg"];_add_image=Module["_add_image"]=wasmExports["Rg"];_add_image_with_rid=Module["_add_image_with_rid"]=wasmExports["Sg"];_allocate=Module["_allocate"]=wasmExports["Tg"];_apply_scene_transactions=Module["_apply_scene_transactions"]=wasmExports["Ug"];_command=Module["_command"]=wasmExports["Vg"];_deallocate=Module["_deallocate"]=wasmExports["Wg"];_destroy=Module["_destroy"]=wasmExports["Xg"];_devtools_rendering_set_show_fps_meter=Module["_devtools_rendering_set_show_fps_meter"]=wasmExports["Yg"];_devtools_rendering_set_show_hit_testing=Module["_devtools_rendering_set_show_hit_testing"]=wasmExports["Zg"];_devtools_rendering_set_show_ruler=Module["_devtools_rendering_set_show_ruler"]=wasmExports["_g"];_devtools_rendering_set_show_stats=Module["_devtools_rendering_set_show_stats"]=wasmExports["$g"];_devtools_rendering_set_show_tiles=Module["_devtools_rendering_set_show_tiles"]=wasmExports["ah"];_drain_missing_images=Module["_drain_missing_images"]=wasmExports["bh"];_export_node_as=Module["_export_node_as"]=wasmExports["ch"];_get_default_fallback_fonts=Module["_get_default_fallback_fonts"]=wasmExports["dh"];_get_image_bytes=Module["_get_image_bytes"]=wasmExports["eh"];_get_image_size=Module["_get_image_size"]=wasmExports["fh"];_get_node_absolute_bounding_box=Module["_get_node_absolute_bounding_box"]=wasmExports["gh"];_get_node_id_from_point=Module["_get_node_id_from_point"]=wasmExports["hh"];_get_node_ids_from_envelope=Module["_get_node_ids_from_envelope"]=wasmExports["ih"];_get_node_ids_from_point=Module["_get_node_ids_from_point"]=wasmExports["jh"];_grida_fonts_analyze_family=Module["_grida_fonts_analyze_family"]=wasmExports["kh"];_grida_fonts_free=Module["_grida_fonts_free"]=wasmExports["lh"];_grida_fonts_parse_font=Module["_grida_fonts_parse_font"]=wasmExports["mh"];_grida_markdown_to_html=Module["_grida_markdown_to_html"]=wasmExports["nh"];_grida_svg_optimize=Module["_grida_svg_optimize"]=wasmExports["oh"];_grida_svg_to_document=Module["_grida_svg_to_document"]=wasmExports["ph"];_has_missing_fonts=Module["_has_missing_fonts"]=wasmExports["qh"];_highlight_strokes=Module["_highlight_strokes"]=wasmExports["rh"];_init=Module["_init"]=wasmExports["sh"];_init_with_backend=Module["_init_with_backend"]=wasmExports["th"];_list_available_fonts=Module["_list_available_fonts"]=wasmExports["uh"];_list_missing_fonts=Module["_list_missing_fonts"]=wasmExports["vh"];_load_benchmark_scene=Module["_load_benchmark_scene"]=wasmExports["wh"];_load_dummy_scene=Module["_load_dummy_scene"]=wasmExports["xh"];_load_scene_grida=Module["_load_scene_grida"]=wasmExports["yh"];_load_scene_grida1=Module["_load_scene_grida1"]=wasmExports["zh"];_pointer_move=Module["_pointer_move"]=wasmExports["Ah"];_redraw=Module["_redraw"]=wasmExports["Bh"];_resize_surface=Module["_resize_surface"]=wasmExports["Ch"];_resolve_image=Module["_resolve_image"]=wasmExports["Dh"];_runtime_renderer_set_layer_compositing=Module["_runtime_renderer_set_layer_compositing"]=wasmExports["Eh"];_runtime_renderer_set_outline_mode=Module["_runtime_renderer_set_outline_mode"]=wasmExports["Fh"];_runtime_renderer_set_pixel_preview_scale=Module["_runtime_renderer_set_pixel_preview_scale"]=wasmExports["Gh"];_runtime_renderer_set_pixel_preview_stable=Module["_runtime_renderer_set_pixel_preview_stable"]=wasmExports["Hh"];_runtime_renderer_set_render_policy_flags=Module["_runtime_renderer_set_render_policy_flags"]=wasmExports["Ih"];_runtime_renderer_set_skip_layout=Module["_runtime_renderer_set_skip_layout"]=wasmExports["Jh"];_set_debug=Module["_set_debug"]=wasmExports["Kh"];_set_default_fallback_fonts=Module["_set_default_fallback_fonts"]=wasmExports["Lh"];_set_main_camera_transform=Module["_set_main_camera_transform"]=wasmExports["Mh"];_set_surface_overlay_config=Module["_set_surface_overlay_config"]=wasmExports["Nh"];_set_verbose=Module["_set_verbose"]=wasmExports["Oh"];_surface_get_cursor=Module["_surface_get_cursor"]=wasmExports["Ph"];_surface_get_hovered_node=Module["_surface_get_hovered_node"]=wasmExports["Qh"];_surface_get_selected_nodes=Module["_surface_get_selected_nodes"]=wasmExports["Rh"];_surface_pointer_down=Module["_surface_pointer_down"]=wasmExports["Sh"];_surface_pointer_move=Module["_surface_pointer_move"]=wasmExports["Th"];_surface_pointer_up=Module["_surface_pointer_up"]=wasmExports["Uh"];_surface_set_selection=Module["_surface_set_selection"]=wasmExports["Vh"];_switch_scene=Module["_switch_scene"]=wasmExports["Wh"];_text_edit_command=Module["_text_edit_command"]=wasmExports["Xh"];_text_edit_enter=Module["_text_edit_enter"]=wasmExports["Yh"];_text_edit_exit=Module["_text_edit_exit"]=wasmExports["Zh"];_text_edit_get_caret_rect=Module["_text_edit_get_caret_rect"]=wasmExports["_h"];_text_edit_get_selected_html=Module["_text_edit_get_selected_html"]=wasmExports["$h"];_text_edit_get_selected_text=Module["_text_edit_get_selected_text"]=wasmExports["ai"];_text_edit_get_selection_rects=Module["_text_edit_get_selection_rects"]=wasmExports["bi"];_text_edit_get_text=Module["_text_edit_get_text"]=wasmExports["ci"];_text_edit_ime_cancel=Module["_text_edit_ime_cancel"]=wasmExports["di"];_text_edit_ime_commit=Module["_text_edit_ime_commit"]=wasmExports["ei"];_text_edit_ime_set_preedit=Module["_text_edit_ime_set_preedit"]=wasmExports["fi"];_text_edit_is_active=Module["_text_edit_is_active"]=wasmExports["gi"];_text_edit_paste_html=Module["_text_edit_paste_html"]=wasmExports["hi"];_text_edit_paste_text=Module["_text_edit_paste_text"]=wasmExports["ii"];_text_edit_pointer_down=Module["_text_edit_pointer_down"]=wasmExports["ji"];_text_edit_pointer_move=Module["_text_edit_pointer_move"]=wasmExports["ki"];_text_edit_pointer_up=Module["_text_edit_pointer_up"]=wasmExports["li"];_text_edit_redo=Module["_text_edit_redo"]=wasmExports["mi"];_text_edit_set_color=Module["_text_edit_set_color"]=wasmExports["ni"];_text_edit_set_font_family=Module["_text_edit_set_font_family"]=wasmExports["oi"];_text_edit_set_font_size=Module["_text_edit_set_font_size"]=wasmExports["pi"];_text_edit_tick=Module["_text_edit_tick"]=wasmExports["qi"];_text_edit_toggle_bold=Module["_text_edit_toggle_bold"]=wasmExports["ri"];_text_edit_toggle_italic=Module["_text_edit_toggle_italic"]=wasmExports["si"];_text_edit_toggle_strikethrough=Module["_text_edit_toggle_strikethrough"]=wasmExports["ti"];_text_edit_toggle_underline=Module["_text_edit_toggle_underline"]=wasmExports["ui"];_text_edit_undo=Module["_text_edit_undo"]=wasmExports["vi"];_tick=Module["_tick"]=wasmExports["wi"];_to_vector_network=Module["_to_vector_network"]=wasmExports["xi"];_toggle_debug=Module["_toggle_debug"]=wasmExports["yi"];_main=Module["_main"]=wasmExports["zi"];_emscripten_builtin_memalign=wasmExports["Ai"];_setThrew=wasmExports["Bi"];__emscripten_tempret_set=wasmExports["Ci"];__emscripten_stack_restore=wasmExports["Di"];__emscripten_stack_alloc=wasmExports["Ei"];_emscripten_stack_get_current=wasmExports["Fi"];___cxa_decrement_exception_refcount=wasmExports["Gi"];___cxa_increment_exception_refcount=wasmExports["Hi"];___cxa_can_catch=wasmExports["Ii"];___cxa_get_exception_ptr=wasmExports["Ji"];memory=wasmMemory=wasmExports["Mg"];__indirect_function_table=wasmTable=wasmExports["Pg"]}var wasmImports={F:___cxa_begin_catch,N:___cxa_end_catch,a:___cxa_find_matching_catch_2,o:___cxa_find_matching_catch_3,ga:___cxa_find_matching_catch_4,Ca:___cxa_rethrow,H:___cxa_throw,eb:___cxa_uncaught_exceptions,e:___resumeException,Fa:___syscall_fcntl64,vb:___syscall_fstat64,rb:___syscall_getcwd,wb:___syscall_ioctl,sb:___syscall_lstat64,tb:___syscall_newfstatat,Ga:___syscall_openat,ub:___syscall_stat64,zb:__abort_js,gb:__emscripten_throw_longjmp,mb:__gmtime_js,kb:__mmap_js,lb:__munmap_js,Ab:__tzset_js,yb:_clock_time_get,xb:_emscripten_date_now,ib:_emscripten_get_heap_max,Af:_emscripten_glActiveTexture,Bf:_emscripten_glAttachShader,de:_emscripten_glBeginQuery,Zd:_emscripten_glBeginQueryEXT,Ec:_emscripten_glBeginTransformFeedback,Cf:_emscripten_glBindAttribLocation,Df:_emscripten_glBindBuffer,Bc:_emscripten_glBindBufferBase,Cc:_emscripten_glBindBufferRange,Be:_emscripten_glBindFramebuffer,Ce:_emscripten_glBindRenderbuffer,je:_emscripten_glBindSampler,Ef:_emscripten_glBindTexture,Rb:_emscripten_glBindTransformFeedback,Xe:_emscripten_glBindVertexArray,_e:_emscripten_glBindVertexArrayOES,Ff:_emscripten_glBlendColor,Gf:_emscripten_glBlendEquation,Jd:_emscripten_glBlendEquationSeparate,Hf:_emscripten_glBlendFunc,Id:_emscripten_glBlendFuncSeparate,ve:_emscripten_glBlitFramebuffer,If:_emscripten_glBufferData,Jf:_emscripten_glBufferSubData,De:_emscripten_glCheckFramebufferStatus,Kf:_emscripten_glClear,fc:_emscripten_glClearBufferfi,gc:_emscripten_glClearBufferfv,ic:_emscripten_glClearBufferiv,hc:_emscripten_glClearBufferuiv,Lf:_emscripten_glClearColor,Hd:_emscripten_glClearDepthf,Mf:_emscripten_glClearStencil,se:_emscripten_glClientWaitSync,_c:_emscripten_glClipControlEXT,Nf:_emscripten_glColorMask,Of:_emscripten_glCompileShader,Qf:_emscripten_glCompressedTexImage2D,Rc:_emscripten_glCompressedTexImage3D,Rf:_emscripten_glCompressedTexSubImage2D,Qc:_emscripten_glCompressedTexSubImage3D,ue:_emscripten_glCopyBufferSubData,Gd:_emscripten_glCopyTexImage2D,Sf:_emscripten_glCopyTexSubImage2D,Sc:_emscripten_glCopyTexSubImage3D,Tf:_emscripten_glCreateProgram,Uf:_emscripten_glCreateShader,Vf:_emscripten_glCullFace,Wf:_emscripten_glDeleteBuffers,Ee:_emscripten_glDeleteFramebuffers,Xf:_emscripten_glDeleteProgram,ee:_emscripten_glDeleteQueries,_d:_emscripten_glDeleteQueriesEXT,Fe:_emscripten_glDeleteRenderbuffers,ke:_emscripten_glDeleteSamplers,Yf:_emscripten_glDeleteShader,te:_emscripten_glDeleteSync,Zf:_emscripten_glDeleteTextures,Qb:_emscripten_glDeleteTransformFeedbacks,Ye:_emscripten_glDeleteVertexArrays,$e:_emscripten_glDeleteVertexArraysOES,Fd:_emscripten_glDepthFunc,_f:_emscripten_glDepthMask,Ed:_emscripten_glDepthRangef,Dd:_emscripten_glDetachShader,$f:_emscripten_glDisable,ag:_emscripten_glDisableVertexAttribArray,bg:_emscripten_glDrawArrays,Ve:_emscripten_glDrawArraysInstanced,Md:_emscripten_glDrawArraysInstancedANGLE,Db:_emscripten_glDrawArraysInstancedARB,Se:_emscripten_glDrawArraysInstancedBaseInstanceWEBGL,Xc:_emscripten_glDrawArraysInstancedEXT,Eb:_emscripten_glDrawArraysInstancedNV,Qe:_emscripten_glDrawBuffers,Vc:_emscripten_glDrawBuffersEXT,Nd:_emscripten_glDrawBuffersWEBGL,cg:_emscripten_glDrawElements,We:_emscripten_glDrawElementsInstanced,Ld:_emscripten_glDrawElementsInstancedANGLE,Bb:_emscripten_glDrawElementsInstancedARB,Te:_emscripten_glDrawElementsInstancedBaseVertexBaseInstanceWEBGL,Cb:_emscripten_glDrawElementsInstancedEXT,Wc:_emscripten_glDrawElementsInstancedNV,Ke:_emscripten_glDrawRangeElements,dg:_emscripten_glEnable,eg:_emscripten_glEnableVertexAttribArray,fe:_emscripten_glEndQuery,$d:_emscripten_glEndQueryEXT,Dc:_emscripten_glEndTransformFeedback,pe:_emscripten_glFenceSync,fg:_emscripten_glFinish,gg:_emscripten_glFlush,Ge:_emscripten_glFramebufferRenderbuffer,He:_emscripten_glFramebufferTexture2D,Hc:_emscripten_glFramebufferTextureLayer,hg:_emscripten_glFrontFace,ig:_emscripten_glGenBuffers,Ie:_emscripten_glGenFramebuffers,ge:_emscripten_glGenQueries,ae:_emscripten_glGenQueriesEXT,Je:_emscripten_glGenRenderbuffers,le:_emscripten_glGenSamplers,jg:_emscripten_glGenTextures,Pb:_emscripten_glGenTransformFeedbacks,Ue:_emscripten_glGenVertexArrays,af:_emscripten_glGenVertexArraysOES,xe:_emscripten_glGenerateMipmap,Cd:_emscripten_glGetActiveAttrib,Bd:_emscripten_glGetActiveUniform,ac:_emscripten_glGetActiveUniformBlockName,bc:_emscripten_glGetActiveUniformBlockiv,dc:_emscripten_glGetActiveUniformsiv,Ad:_emscripten_glGetAttachedShaders,zd:_emscripten_glGetAttribLocation,yd:_emscripten_glGetBooleanv,Xb:_emscripten_glGetBufferParameteri64v,kg:_emscripten_glGetBufferParameteriv,lg:_emscripten_glGetError,mg:_emscripten_glGetFloatv,rc:_emscripten_glGetFragDataLocation,ye:_emscripten_glGetFramebufferAttachmentParameteriv,Yb:_emscripten_glGetInteger64i_v,_b:_emscripten_glGetInteger64v,Fc:_emscripten_glGetIntegeri_v,ng:_emscripten_glGetIntegerv,Hb:_emscripten_glGetInternalformativ,Lb:_emscripten_glGetProgramBinary,og:_emscripten_glGetProgramInfoLog,pg:_emscripten_glGetProgramiv,Wd:_emscripten_glGetQueryObjecti64vEXT,Pd:_emscripten_glGetQueryObjectivEXT,Xd:_emscripten_glGetQueryObjectui64vEXT,he:_emscripten_glGetQueryObjectuiv,be:_emscripten_glGetQueryObjectuivEXT,ie:_emscripten_glGetQueryiv,ce:_emscripten_glGetQueryivEXT,ze:_emscripten_glGetRenderbufferParameteriv,Tb:_emscripten_glGetSamplerParameterfv,Ub:_emscripten_glGetSamplerParameteriv,qg:_emscripten_glGetShaderInfoLog,Td:_emscripten_glGetShaderPrecisionFormat,xd:_emscripten_glGetShaderSource,rg:_emscripten_glGetShaderiv,sg:_emscripten_glGetString,Ze:_emscripten_glGetStringi,Zb:_emscripten_glGetSynciv,wd:_emscripten_glGetTexParameterfv,vd:_emscripten_glGetTexParameteriv,zc:_emscripten_glGetTransformFeedbackVarying,cc:_emscripten_glGetUniformBlockIndex,ec:_emscripten_glGetUniformIndices,tg:_emscripten_glGetUniformLocation,ud:_emscripten_glGetUniformfv,td:_emscripten_glGetUniformiv,sc:_emscripten_glGetUniformuiv,yc:_emscripten_glGetVertexAttribIiv,xc:_emscripten_glGetVertexAttribIuiv,qd:_emscripten_glGetVertexAttribPointerv,sd:_emscripten_glGetVertexAttribfv,rd:_emscripten_glGetVertexAttribiv,pd:_emscripten_glHint,Ud:_emscripten_glInvalidateFramebuffer,Vd:_emscripten_glInvalidateSubFramebuffer,od:_emscripten_glIsBuffer,nd:_emscripten_glIsEnabled,md:_emscripten_glIsFramebuffer,ld:_emscripten_glIsProgram,Oc:_emscripten_glIsQuery,Qd:_emscripten_glIsQueryEXT,kd:_emscripten_glIsRenderbuffer,Wb:_emscripten_glIsSampler,jd:_emscripten_glIsShader,qe:_emscripten_glIsSync,ug:_emscripten_glIsTexture,Ob:_emscripten_glIsTransformFeedback,Gc:_emscripten_glIsVertexArray,Od:_emscripten_glIsVertexArrayOES,vg:_emscripten_glLineWidth,wg:_emscripten_glLinkProgram,Oe:_emscripten_glMultiDrawArraysInstancedBaseInstanceWEBGL,Pe:_emscripten_glMultiDrawElementsInstancedBaseVertexBaseInstanceWEBGL,Nb:_emscripten_glPauseTransformFeedback,xg:_emscripten_glPixelStorei,Zc:_emscripten_glPolygonModeWEBGL,id:_emscripten_glPolygonOffset,$c:_emscripten_glPolygonOffsetClampEXT,Kb:_emscripten_glProgramBinary,Jb:_emscripten_glProgramParameteri,Yd:_emscripten_glQueryCounterEXT,Re:_emscripten_glReadBuffer,yg:_emscripten_glReadPixels,hd:_emscripten_glReleaseShaderCompiler,Ae:_emscripten_glRenderbufferStorage,we:_emscripten_glRenderbufferStorageMultisample,Mb:_emscripten_glResumeTransformFeedback,gd:_emscripten_glSampleCoverage,me:_emscripten_glSamplerParameterf,Vb:_emscripten_glSamplerParameterfv,ne:_emscripten_glSamplerParameteri,oe:_emscripten_glSamplerParameteriv,zg:_emscripten_glScissor,fd:_emscripten_glShaderBinary,Ag:_emscripten_glShaderSource,Bg:_emscripten_glStencilFunc,Cg:_emscripten_glStencilFuncSeparate,Dg:_emscripten_glStencilMask,Eg:_emscripten_glStencilMaskSeparate,Fg:_emscripten_glStencilOp,Gg:_emscripten_glStencilOpSeparate,Hg:_emscripten_glTexImage2D,Uc:_emscripten_glTexImage3D,Ig:_emscripten_glTexParameterf,Jg:_emscripten_glTexParameterfv,Kg:_emscripten_glTexParameteri,Lg:_emscripten_glTexParameteriv,Le:_emscripten_glTexStorage2D,Ib:_emscripten_glTexStorage3D,Ma:_emscripten_glTexSubImage2D,Tc:_emscripten_glTexSubImage3D,Ac:_emscripten_glTransformFeedbackVaryings,Na:_emscripten_glUniform1f,Oa:_emscripten_glUniform1fv,wf:_emscripten_glUniform1i,xf:_emscripten_glUniform1iv,qc:_emscripten_glUniform1ui,mc:_emscripten_glUniform1uiv,yf:_emscripten_glUniform2f,zf:_emscripten_glUniform2fv,vf:_emscripten_glUniform2i,uf:_emscripten_glUniform2iv,pc:_emscripten_glUniform2ui,lc:_emscripten_glUniform2uiv,tf:_emscripten_glUniform3f,sf:_emscripten_glUniform3fv,rf:_emscripten_glUniform3i,qf:_emscripten_glUniform3iv,oc:_emscripten_glUniform3ui,kc:_emscripten_glUniform3uiv,pf:_emscripten_glUniform4f,of:_emscripten_glUniform4fv,bf:_emscripten_glUniform4i,cf:_emscripten_glUniform4iv,nc:_emscripten_glUniform4ui,jc:_emscripten_glUniform4uiv,$b:_emscripten_glUniformBlockBinding,df:_emscripten_glUniformMatrix2fv,Nc:_emscripten_glUniformMatrix2x3fv,Lc:_emscripten_glUniformMatrix2x4fv,ef:_emscripten_glUniformMatrix3fv,Mc:_emscripten_glUniformMatrix3x2fv,Jc:_emscripten_glUniformMatrix3x4fv,ff:_emscripten_glUniformMatrix4fv,Kc:_emscripten_glUniformMatrix4x2fv,Ic:_emscripten_glUniformMatrix4x3fv,gf:_emscripten_glUseProgram,ed:_emscripten_glValidateProgram,hf:_emscripten_glVertexAttrib1f,dd:_emscripten_glVertexAttrib1fv,cd:_emscripten_glVertexAttrib2f,jf:_emscripten_glVertexAttrib2fv,bd:_emscripten_glVertexAttrib3f,kf:_emscripten_glVertexAttrib3fv,ad:_emscripten_glVertexAttrib4f,lf:_emscripten_glVertexAttrib4fv,Me:_emscripten_glVertexAttribDivisor,Kd:_emscripten_glVertexAttribDivisorANGLE,Fb:_emscripten_glVertexAttribDivisorARB,Yc:_emscripten_glVertexAttribDivisorEXT,Gb:_emscripten_glVertexAttribDivisorNV,wc:_emscripten_glVertexAttribI4i,uc:_emscripten_glVertexAttribI4iv,vc:_emscripten_glVertexAttribI4ui,tc:_emscripten_glVertexAttribI4uiv,Ne:_emscripten_glVertexAttribIPointer,mf:_emscripten_glVertexAttribPointer,nf:_emscripten_glViewport,re:_emscripten_glWaitSync,Za:_emscripten_request_animation_frame_loop,hb:_emscripten_resize_heap,ob:_environ_get,pb:_environ_sizes_get,Ra:_exit,la:_fd_close,jb:_fd_pread,Ea:_fd_read,nb:_fd_seek,ka:_fd_write,Pa:_glGetIntegerv,pa:_glGetString,Qa:_glGetStringi,Rd:invoke_dd,Sd:invoke_dddd,Aa:invoke_diii,Ua:invoke_fdiiii,Ta:invoke_fdiiiii,Sa:invoke_fii,Ba:invoke_fiii,s:invoke_fiiidi,V:invoke_fiiif,t:invoke_fiiiidi,r:invoke_i,j:invoke_ii,G:invoke_iif,$a:invoke_iiffi,ra:invoke_iiffiii,f:invoke_iii,Ja:invoke_iiiffii,ta:invoke_iiifi,g:invoke_iiii,T:invoke_iiiiff,l:invoke_iiiii,db:invoke_iiiiid,A:invoke_iiiiii,y:invoke_iiiiiii,E:invoke_iiiiiiii,q:invoke_iiiiiiiii,qa:invoke_iiiiiiiiii,ca:invoke_iiiiiiiiiiii,oa:invoke_iiiiiiiiiiiifiii,R:invoke_iij,fb:invoke_j,ha:invoke_ji,_:invoke_jiii,da:invoke_jiiii,J:invoke_jjji,k:invoke_v,Pf:invoke_vff,b:invoke_vi,P:invoke_vid,S:invoke_vif,u:invoke_viff,D:invoke_viffff,Z:invoke_vifffff,Va:invoke_viffffff,C:invoke_viffi,ia:invoke_viffiiiiiii,c:invoke_vii,Ya:invoke_viidii,O:invoke_viif,v:invoke_viiff,$:invoke_viifi,va:invoke_viififii,x:invoke_viifiiifi,d:invoke_viii,I:invoke_viiif,xa:invoke_viiiff,B:invoke_viiiffi,K:invoke_viiiffiffii,L:invoke_viiififiiiiiiiiiiii,i:invoke_viiii,Xa:invoke_viiiidididii,ja:invoke_viiiif,ya:invoke_viiiiff,Da:invoke_viiiiffi,wa:invoke_viiiifi,h:invoke_viiiii,ab:invoke_viiiiif,Ia:invoke_viiiiiff,Wa:invoke_viiiiiffiii,cb:invoke_viiiiifi,m:invoke_viiiiii,p:invoke_viiiiiii,W:invoke_viiiiiiii,Y:invoke_viiiiiiiii,M:invoke_viiiiiiiiii,sa:invoke_viiiiiiiiiii,ba:invoke_viiiiiiiiiiiiiii,La:invoke_viiiiiji,Sb:invoke_viiiijjiiiiff,Q:invoke_viiij,z:invoke_viiijii,X:invoke_viij,n:invoke_viiji,ma:invoke_viijiffi,fa:invoke_viijii,bb:invoke_viijiii,aa:invoke_viijiiiif,Ka:invoke_viijiiiii,ua:invoke_viijj,U:invoke_viji,w:invoke_vijii,Ha:invoke_vijiifi,_a:invoke_vijiififi,za:invoke_vijiii,ea:invoke_vijjjj,Pc:invoke_vjii,na:_llvm_eh_typeid_for,qb:_random_get};function invoke_vi(index,a1){var sp=stackSave();try{getWasmTableEntry(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 getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vii(index,a1,a2){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_ii(index,a1){var sp=stackSave();try{return getWasmTableEntry(index)(a1)}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 getWasmTableEntry(index)(a1,a2,a3)}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{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viii(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(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{getWasmTableEntry(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 getWasmTableEntry(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 getWasmTableEntry(index)(a1)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0);return 0n}}function invoke_v(index){var sp=stackSave();try{getWasmTableEntry(index)()}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiij(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vff(index,a1,a2){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viij(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiji(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiff(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiji(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}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 getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viijiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vid(index,a1,a2){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viji(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiif(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiff(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiiiiiiifiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vif(index,a1,a2){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viffi(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vjii(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiffii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiff(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viijiffi(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}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{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiijjiiiiff(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vijii(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vijiifi(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiffi(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viifiiifi(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiifi(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vijiii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}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{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}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{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiififiiiiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viijiiiif(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiffiffii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10){var sp=stackSave();try{getWasmTableEntry(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_viiiiff(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiff(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiffi(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiif(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiif(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiifi(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiffi(index,a1,a2,a3,a4){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viififii(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vijiififi(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iij(index,a1,a2){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viijj(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_jjji(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0);return 0n}}function invoke_iiifi(index,a1,a2,a3,a4){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iif(index,a1,a2){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viifi(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viif(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_jiii(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(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 getWasmTableEntry(index)(a1,a2,a3,a4,a5)}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 getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiffiii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viff(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vifffff(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fiiidi(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viidii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiidididii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fiiiidi(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiffiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10){var sp=stackSave();try{getWasmTableEntry(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_viiijii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viffff(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viffffff(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9){var sp=stackSave();try{getWasmTableEntry(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_fiiif(index,a1,a2,a3,a4){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fdiiii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fdiiiii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fii(index,a1,a2){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viffiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10){var sp=stackSave();try{getWasmTableEntry(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_viiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10){var sp=stackSave();try{getWasmTableEntry(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_iiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9){var sp=stackSave();try{return getWasmTableEntry(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_dddd(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_dd(index,a1){var sp=stackSave();try{return getWasmTableEntry(index)(a1)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vijjjj(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_j(index){var sp=stackSave();try{return getWasmTableEntry(index)()}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0);return 0n}}function invoke_iiiiid(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_jiiii(index,a1,a2,a3,a4){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0);return 0n}}function invoke_fiii(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_diii(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_i(index){var sp=stackSave();try{return getWasmTableEntry(index)()}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function callMain(args=[]){var entryFunction=_main;args.unshift(thisProgram);var argc=args.length;var argv=stackAlloc((argc+1)*4);var argv_ptr=argv;for(var arg of args){HEAPU32[argv_ptr>>2]=stringToUTF8OnStack(arg);argv_ptr+=4}HEAPU32[argv_ptr>>2]=0;try{var ret=entryFunction(argc,argv);exitJS(ret,true);return ret}catch(e){return handleException(e)}}function run(args=arguments_){if(runDependencies>0){dependenciesFulfilled=run;return}preRun();if(runDependencies>0){dependenciesFulfilled=run;return}function doRun(){Module["calledRun"]=true;if(ABORT)return;initRuntime();preMain();readyPromiseResolve?.(Module);Module["onRuntimeInitialized"]?.();var noInitialRun=Module["noInitialRun"]||false;if(!noInitialRun)callMain(args);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})} +var createGridaCanvas=(()=>{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(typeof __filename!="undefined"){_scriptName=__filename}else{}var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_NODE){var fs=require("node:fs");scriptDirectory=__dirname+"/";readBinary=filename=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename);return ret};readAsync=async(filename,binary=true)=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename,binary?undefined:"utf8");return ret};if(process.argv.length>1){thisProgram=process.argv[1].replace(/\\/g,"/")}arguments_=process.argv.slice(2);quit_=(status,toThrow)=>{process.exitCode=status;throw toThrow}}else if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){try{scriptDirectory=new URL(".",_scriptName).href}catch{}{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;var isFileURI=filename=>filename.startsWith("file://");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);Module["HEAPU8"]=HEAPU8=new Uint8Array(b);Module["HEAPU16"]=HEAPU16=new Uint16Array(b);Module["HEAP32"]=HEAP32=new Int32Array(b);Module["HEAPU32"]=HEAPU32=new Uint32Array(b);Module["HEAPF32"]=HEAPF32=new Float32Array(b);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["Og"]();FS.ignorePermissions=false}function preMain(){}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("grida_canvas_wasm.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&&!ENVIRONMENT_IS_NODE){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;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 noExitRuntime=true;var stackRestore=val=>__emscripten_stack_restore(val);var stackSave=()=>_emscripten_stack_get_current();var exceptionCaught=[];var uncaughtExceptionCount=0;var ___cxa_begin_catch=ptr=>{var info=new ExceptionInfo(ptr);if(!info.get_caught()){info.set_caught(true);uncaughtExceptionCount--}info.set_rethrown(false);exceptionCaught.push(info);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]=type}get_type(){return HEAPU32[this.ptr+4>>2]}set_destructor(destructor){HEAPU32[this.ptr+8>>2]=destructor}get_destructor(){return HEAPU32[this.ptr+8>>2]}set_caught(caught){caught=caught?1:0;HEAP8[this.ptr+12]=caught}get_caught(){return HEAP8[this.ptr+12]!=0}set_rethrown(rethrown){rethrown=rethrown?1:0;HEAP8[this.ptr+13]=rethrown}get_rethrown(){return HEAP8[this.ptr+13]!=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]=adjustedPtr}get_adjusted_ptr(){return HEAPU32[this.ptr+16>>2]}}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};var ___cxa_find_matching_catch_2=()=>findMatchingCatch([]);var ___cxa_find_matching_catch_3=arg0=>findMatchingCatch([arg0]);var ___cxa_find_matching_catch_4=(arg0,arg1)=>findMatchingCatch([arg0,arg1]);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++}___cxa_increment_exception_refcount(ptr);exceptionLast=ptr;throw exceptionLast};var ___cxa_throw=(ptr,type,destructor)=>{var info=new ExceptionInfo(ptr);info.init(type,destructor);___cxa_increment_exception_refcount(ptr);exceptionLast=ptr;uncaughtExceptionCount++;throw exceptionLast};var ___cxa_uncaught_exceptions=()=>uncaughtExceptionCount;var ___resumeException=ptr=>{if(!exceptionLast){exceptionLast=ptr}throw exceptionLast};var syscallGetVarargI=()=>{var ret=HEAP32[+SYSCALLS.varargs>>2];SYSCALLS.varargs+=4;return ret};var syscallGetVarargP=syscallGetVarargI;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=()=>{if(ENVIRONMENT_IS_NODE){var nodeCrypto=require("node:crypto");return view=>nodeCrypto.randomFillSync(view)}return 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)=>{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)=>{if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i=endIdx)break;heap[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++]=192|u>>6;heap[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++]=224|u>>12;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;heap[outIdx++]=240|u>>18;heap[outIdx++]=128|u>>12&63;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63;i++}}heap[outIdx]=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(ENVIRONMENT_IS_NODE){var BUFSIZE=256;var buf=Buffer.alloc(BUFSIZE);var bytesRead=0;var fd=process.stdin.fd;try{bytesRead=fs.readSync(fd,buf,0,BUFSIZE)}catch(e){if(e.toString().includes("EOF"))bytesRead=0;else throw e}if(bytesRead>0){result=buf.slice(0,bytesRead).toString("utf-8")}}else 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{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?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead,ignoreNul):"";var SYSCALLS={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]=stat.dev;HEAPU32[buf+4>>2]=stat.mode;HEAPU32[buf+8>>2]=stat.nlink;HEAPU32[buf+12>>2]=stat.uid;HEAPU32[buf+16>>2]=stat.gid;HEAPU32[buf+20>>2]=stat.rdev;HEAP64[buf+24>>3]=BigInt(stat.size);HEAP32[buf+32>>2]=4096;HEAP32[buf+36>>2]=stat.blocks;var atime=stat.atime.getTime();var mtime=stat.mtime.getTime();var ctime=stat.ctime.getTime();HEAP64[buf+40>>3]=BigInt(Math.floor(atime/1e3));HEAPU32[buf+48>>2]=atime%1e3*1e3*1e3;HEAP64[buf+56>>3]=BigInt(Math.floor(mtime/1e3));HEAPU32[buf+64>>2]=mtime%1e3*1e3*1e3;HEAP64[buf+72>>3]=BigInt(Math.floor(ctime/1e3));HEAPU32[buf+80>>2]=ctime%1e3*1e3*1e3;HEAP64[buf+88>>3]=BigInt(stat.ino);return 0},writeStatFs(buf,stats){HEAPU32[buf+4>>2]=stats.bsize;HEAPU32[buf+60>>2]=stats.bsize;HEAP64[buf+8>>3]=BigInt(stats.blocks);HEAP64[buf+16>>3]=BigInt(stats.bfree);HEAP64[buf+24>>3]=BigInt(stats.bavail);HEAP64[buf+32>>3]=BigInt(stats.files);HEAP64[buf+40>>3]=BigInt(stats.ffree);HEAPU32[buf+48>>2]=stats.fsid;HEAPU32[buf+64>>2]=stats.flags;HEAPU32[buf+56>>2]=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_fcntl64(fd,cmd,varargs){SYSCALLS.varargs=varargs;try{var stream=SYSCALLS.getStreamFromFD(fd);switch(cmd){case 0:{var arg=syscallGetVarargI();if(arg<0){return-28}while(FS.streams[arg]){arg++}var newStream;newStream=FS.dupStream(stream,arg);return newStream.fd}case 1:case 2:return 0;case 3:return stream.flags;case 4:{var arg=syscallGetVarargI();stream.flags|=arg;return 0}case 12:{var arg=syscallGetVarargP();var offset=0;HEAP16[arg+offset>>1]=2;return 0}case 13:case 14:return 0}return-28}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_fstat64(fd,buf){try{return SYSCALLS.writeStat(buf,FS.fstat(fd))}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}var stringToUTF8=(str,outPtr,maxBytesToWrite)=>stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite);function ___syscall_getcwd(buf,size){try{if(size===0)return-28;var cwd=FS.cwd();var cwdLengthInBytes=lengthBytesUTF8(cwd)+1;if(size>2]=termios.c_iflag||0;HEAP32[argp+4>>2]=termios.c_oflag||0;HEAP32[argp+8>>2]=termios.c_cflag||0;HEAP32[argp+12>>2]=termios.c_lflag||0;for(var i=0;i<32;i++){HEAP8[argp+i+17]=termios.c_cc[i]||0}return 0}return 0}case 21510:case 21511:case 21512:{if(!stream.tty)return-59;return 0}case 21506:case 21507:case 21508:{if(!stream.tty)return-59;if(stream.tty.ops.ioctl_tcsets){var argp=syscallGetVarargP();var c_iflag=HEAP32[argp>>2];var c_oflag=HEAP32[argp+4>>2];var c_cflag=HEAP32[argp+8>>2];var c_lflag=HEAP32[argp+12>>2];var c_cc=[];for(var i=0;i<32;i++){c_cc.push(HEAP8[argp+i+17])}return stream.tty.ops.ioctl_tcsets(stream.tty,op,{c_iflag,c_oflag,c_cflag,c_lflag,c_cc})}return 0}case 21519:{if(!stream.tty)return-59;var argp=syscallGetVarargP();HEAP32[argp>>2]=0;return 0}case 21520:{if(!stream.tty)return-59;return-28}case 21537:case 21531:{var argp=syscallGetVarargP();return FS.ioctl(stream,op,argp)}case 21523:{if(!stream.tty)return-59;if(stream.tty.ops.ioctl_tiocgwinsz){var winsize=stream.tty.ops.ioctl_tiocgwinsz(stream.tty);var argp=syscallGetVarargP();HEAP16[argp>>1]=winsize[0];HEAP16[argp+2>>1]=winsize[1]}return 0}case 21524:{if(!stream.tty)return-59;return 0}case 21515:{if(!stream.tty)return-59;return 0}default:return-28}}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_lstat64(path,buf){try{path=SYSCALLS.getStr(path);return SYSCALLS.writeStat(buf,FS.lstat(path))}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_newfstatat(dirfd,path,buf,flags){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}}function ___syscall_openat(dirfd,path,flags,varargs){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}}function ___syscall_stat64(path,buf){try{path=SYSCALLS.getStr(path);return SYSCALLS.writeStat(buf,FS.stat(path))}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}var __abort_js=()=>abort("");var __emscripten_throw_longjmp=()=>{throw Infinity};var INT53_MAX=9007199254740992;var INT53_MIN=-9007199254740992;var bigintToI53Checked=num=>numINT53_MAX?NaN:Number(num);function __gmtime_js(time,tmPtr){time=bigintToI53Checked(time);var date=new Date(time*1e3);HEAP32[tmPtr>>2]=date.getUTCSeconds();HEAP32[tmPtr+4>>2]=date.getUTCMinutes();HEAP32[tmPtr+8>>2]=date.getUTCHours();HEAP32[tmPtr+12>>2]=date.getUTCDate();HEAP32[tmPtr+16>>2]=date.getUTCMonth();HEAP32[tmPtr+20>>2]=date.getUTCFullYear()-1900;HEAP32[tmPtr+24>>2]=date.getUTCDay();var start=Date.UTC(date.getUTCFullYear(),0,1,0,0,0,0);var yday=(date.getTime()-start)/(1e3*60*60*24)|0;HEAP32[tmPtr+28>>2]=yday}function __mmap_js(len,prot,flags,fd,offset,allocated,addr){offset=bigintToI53Checked(offset);try{var stream=SYSCALLS.getStreamFromFD(fd);var res=FS.mmap(stream,len,offset,prot,flags);var ptr=res.ptr;HEAP32[allocated>>2]=res.allocated;HEAPU32[addr>>2]=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){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 __tzset_js=(timezone,daylight,std_name,dst_name)=>{var currentYear=(new Date).getFullYear();var winter=new Date(currentYear,0,1);var summer=new Date(currentYear,6,1);var winterOffset=winter.getTimezoneOffset();var summerOffset=summer.getTimezoneOffset();var stdTimezoneOffset=Math.max(winterOffset,summerOffset);HEAPU32[timezone>>2]=stdTimezoneOffset*60;HEAP32[daylight>>2]=Number(winterOffset!=summerOffset);var extractZone=timezoneOffset=>{var sign=timezoneOffset>=0?"-":"+";var absOffset=Math.abs(timezoneOffset);var hours=String(Math.floor(absOffset/60)).padStart(2,"0");var minutes=String(absOffset%60).padStart(2,"0");return`UTC${sign}${hours}${minutes}`};var winterName=extractZone(winterOffset);var summerName=extractZone(summerOffset);if(summerOffsetperformance.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);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]=BigInt(nsec);return 0}var getHeapMax=()=>2147483648;var _emscripten_get_heap_max=()=>getHeapMax();var GLctx;var webgl_enable_ANGLE_instanced_arrays=ctx=>{var ext=ctx.getExtension("ANGLE_instanced_arrays");if(ext){ctx["vertexAttribDivisor"]=(index,divisor)=>ext["vertexAttribDivisorANGLE"](index,divisor);ctx["drawArraysInstanced"]=(mode,first,count,primcount)=>ext["drawArraysInstancedANGLE"](mode,first,count,primcount);ctx["drawElementsInstanced"]=(mode,count,type,indices,primcount)=>ext["drawElementsInstancedANGLE"](mode,count,type,indices,primcount);return 1}};var webgl_enable_OES_vertex_array_object=ctx=>{var ext=ctx.getExtension("OES_vertex_array_object");if(ext){ctx["createVertexArray"]=()=>ext["createVertexArrayOES"]();ctx["deleteVertexArray"]=vao=>ext["deleteVertexArrayOES"](vao);ctx["bindVertexArray"]=vao=>ext["bindVertexArrayOES"](vao);ctx["isVertexArray"]=vao=>ext["isVertexArrayOES"](vao);return 1}};var webgl_enable_WEBGL_draw_buffers=ctx=>{var ext=ctx.getExtension("WEBGL_draw_buffers");if(ext){ctx["drawBuffers"]=(n,bufs)=>ext["drawBuffersWEBGL"](n,bufs);return 1}};var webgl_enable_WEBGL_draw_instanced_base_vertex_base_instance=ctx=>!!(ctx.dibvbi=ctx.getExtension("WEBGL_draw_instanced_base_vertex_base_instance"));var webgl_enable_WEBGL_multi_draw_instanced_base_vertex_base_instance=ctx=>!!(ctx.mdibvbi=ctx.getExtension("WEBGL_multi_draw_instanced_base_vertex_base_instance"));var webgl_enable_EXT_polygon_offset_clamp=ctx=>!!(ctx.extPolygonOffsetClamp=ctx.getExtension("EXT_polygon_offset_clamp"));var webgl_enable_EXT_clip_control=ctx=>!!(ctx.extClipControl=ctx.getExtension("EXT_clip_control"));var webgl_enable_WEBGL_polygon_mode=ctx=>!!(ctx.webglPolygonMode=ctx.getExtension("WEBGL_polygon_mode"));var webgl_enable_WEBGL_multi_draw=ctx=>!!(ctx.multiDrawWebgl=ctx.getExtension("WEBGL_multi_draw"));var getEmscriptenSupportedExtensions=ctx=>{var supportedExtensions=["ANGLE_instanced_arrays","EXT_blend_minmax","EXT_disjoint_timer_query","EXT_frag_depth","EXT_shader_texture_lod","EXT_sRGB","OES_element_index_uint","OES_fbo_render_mipmap","OES_standard_derivatives","OES_texture_float","OES_texture_half_float","OES_texture_half_float_linear","OES_vertex_array_object","WEBGL_color_buffer_float","WEBGL_depth_texture","WEBGL_draw_buffers","EXT_color_buffer_float","EXT_conservative_depth","EXT_disjoint_timer_query_webgl2","EXT_texture_norm16","NV_shader_noperspective_interpolation","WEBGL_clip_cull_distance","EXT_clip_control","EXT_color_buffer_half_float","EXT_depth_clamp","EXT_float_blend","EXT_polygon_offset_clamp","EXT_texture_compression_bptc","EXT_texture_compression_rgtc","EXT_texture_filter_anisotropic","KHR_parallel_shader_compile","OES_texture_float_linear","WEBGL_blend_func_extended","WEBGL_compressed_texture_astc","WEBGL_compressed_texture_etc","WEBGL_compressed_texture_etc1","WEBGL_compressed_texture_s3tc","WEBGL_compressed_texture_s3tc_srgb","WEBGL_debug_renderer_info","WEBGL_debug_shaders","WEBGL_lose_context","WEBGL_multi_draw","WEBGL_polygon_mode"];return(ctx.getSupportedExtensions()||[]).filter(ext=>supportedExtensions.includes(ext))};var GL={counter:1,buffers:[],programs:[],framebuffers:[],renderbuffers:[],textures:[],shaders:[],vaos:[],contexts:[],offscreenCanvases:{},queries:[],samplers:[],transformFeedbacks:[],syncs:[],stringCache:{},stringiCache:{},unpackAlignment:4,unpackRowLength:0,recordError:errorCode=>{if(!GL.lastError){GL.lastError=errorCode}},getNewId:table=>{var ret=GL.counter++;for(var i=table.length;i{for(var i=0;i>2]=id}},getSource:(shader,count,string,length)=>{var source="";for(var i=0;i>2]:undefined;source+=UTF8ToString(HEAPU32[string+i*4>>2],len)}return source},createContext:(canvas,webGLContextAttributes)=>{if(!canvas.getContextSafariWebGL2Fixed){canvas.getContextSafariWebGL2Fixed=canvas.getContext;function fixedGetContext(ver,attrs){var gl=canvas.getContextSafariWebGL2Fixed(ver,attrs);return ver=="webgl"==gl instanceof WebGLRenderingContext?gl:null}canvas.getContext=fixedGetContext}var ctx=webGLContextAttributes.majorVersion>1?canvas.getContext("webgl2",webGLContextAttributes):canvas.getContext("webgl",webGLContextAttributes);if(!ctx)return 0;var handle=GL.registerContext(ctx,webGLContextAttributes);return handle},registerContext:(ctx,webGLContextAttributes)=>{var handle=GL.getNewId(GL.contexts);var context={handle,attributes:webGLContextAttributes,version:webGLContextAttributes.majorVersion,GLctx:ctx};if(ctx.canvas)ctx.canvas.GLctxObject=context;GL.contexts[handle]=context;if(typeof webGLContextAttributes.enableExtensionsByDefault=="undefined"||webGLContextAttributes.enableExtensionsByDefault){GL.initExtensions(context)}return handle},makeContextCurrent:contextHandle=>{GL.currentContext=GL.contexts[contextHandle];Module["ctx"]=GLctx=GL.currentContext?.GLctx;return!(contextHandle&&!GLctx)},getContext:contextHandle=>GL.contexts[contextHandle],deleteContext:contextHandle=>{if(GL.currentContext===GL.contexts[contextHandle]){GL.currentContext=null}if(typeof JSEvents=="object"){JSEvents.removeAllHandlersOnTarget(GL.contexts[contextHandle].GLctx.canvas)}if(GL.contexts[contextHandle]?.GLctx.canvas){GL.contexts[contextHandle].GLctx.canvas.GLctxObject=undefined}GL.contexts[contextHandle]=null},initExtensions:context=>{context||=GL.currentContext;if(context.initExtensionsDone)return;context.initExtensionsDone=true;var GLctx=context.GLctx;webgl_enable_WEBGL_multi_draw(GLctx);webgl_enable_EXT_polygon_offset_clamp(GLctx);webgl_enable_EXT_clip_control(GLctx);webgl_enable_WEBGL_polygon_mode(GLctx);webgl_enable_ANGLE_instanced_arrays(GLctx);webgl_enable_OES_vertex_array_object(GLctx);webgl_enable_WEBGL_draw_buffers(GLctx);webgl_enable_WEBGL_draw_instanced_base_vertex_base_instance(GLctx);webgl_enable_WEBGL_multi_draw_instanced_base_vertex_base_instance(GLctx);if(context.version>=2){GLctx.disjointTimerQueryExt=GLctx.getExtension("EXT_disjoint_timer_query_webgl2")}if(context.version<2||!GLctx.disjointTimerQueryExt){GLctx.disjointTimerQueryExt=GLctx.getExtension("EXT_disjoint_timer_query")}for(var ext of getEmscriptenSupportedExtensions(GLctx)){if(!ext.includes("lose_context")&&!ext.includes("debug")){GLctx.getExtension(ext)}}}};var _emscripten_glActiveTexture=x0=>GLctx.activeTexture(x0);var _emscripten_glAttachShader=(program,shader)=>{GLctx.attachShader(GL.programs[program],GL.shaders[shader])};var _emscripten_glBeginQuery=(target,id)=>{GLctx.beginQuery(target,GL.queries[id])};var _emscripten_glBeginQueryEXT=(target,id)=>{GLctx.disjointTimerQueryExt["beginQueryEXT"](target,GL.queries[id])};var _emscripten_glBeginTransformFeedback=x0=>GLctx.beginTransformFeedback(x0);var _emscripten_glBindAttribLocation=(program,index,name)=>{GLctx.bindAttribLocation(GL.programs[program],index,UTF8ToString(name))};var _emscripten_glBindBuffer=(target,buffer)=>{if(target==35051){GLctx.currentPixelPackBufferBinding=buffer}else if(target==35052){GLctx.currentPixelUnpackBufferBinding=buffer}GLctx.bindBuffer(target,GL.buffers[buffer])};var _emscripten_glBindBufferBase=(target,index,buffer)=>{GLctx.bindBufferBase(target,index,GL.buffers[buffer])};var _emscripten_glBindBufferRange=(target,index,buffer,offset,ptrsize)=>{GLctx.bindBufferRange(target,index,GL.buffers[buffer],offset,ptrsize)};var _emscripten_glBindFramebuffer=(target,framebuffer)=>{GLctx.bindFramebuffer(target,GL.framebuffers[framebuffer])};var _emscripten_glBindRenderbuffer=(target,renderbuffer)=>{GLctx.bindRenderbuffer(target,GL.renderbuffers[renderbuffer])};var _emscripten_glBindSampler=(unit,sampler)=>{GLctx.bindSampler(unit,GL.samplers[sampler])};var _emscripten_glBindTexture=(target,texture)=>{GLctx.bindTexture(target,GL.textures[texture])};var _emscripten_glBindTransformFeedback=(target,id)=>{GLctx.bindTransformFeedback(target,GL.transformFeedbacks[id])};var _emscripten_glBindVertexArray=vao=>{GLctx.bindVertexArray(GL.vaos[vao])};var _glBindVertexArray=_emscripten_glBindVertexArray;var _emscripten_glBindVertexArrayOES=_glBindVertexArray;var _emscripten_glBlendColor=(x0,x1,x2,x3)=>GLctx.blendColor(x0,x1,x2,x3);var _emscripten_glBlendEquation=x0=>GLctx.blendEquation(x0);var _emscripten_glBlendEquationSeparate=(x0,x1)=>GLctx.blendEquationSeparate(x0,x1);var _emscripten_glBlendFunc=(x0,x1)=>GLctx.blendFunc(x0,x1);var _emscripten_glBlendFuncSeparate=(x0,x1,x2,x3)=>GLctx.blendFuncSeparate(x0,x1,x2,x3);var _emscripten_glBlitFramebuffer=(x0,x1,x2,x3,x4,x5,x6,x7,x8,x9)=>GLctx.blitFramebuffer(x0,x1,x2,x3,x4,x5,x6,x7,x8,x9);var _emscripten_glBufferData=(target,size,data,usage)=>{if(GL.currentContext.version>=2){if(data&&size){GLctx.bufferData(target,HEAPU8,usage,data,size)}else{GLctx.bufferData(target,size,usage)}return}GLctx.bufferData(target,data?HEAPU8.subarray(data,data+size):size,usage)};var _emscripten_glBufferSubData=(target,offset,size,data)=>{if(GL.currentContext.version>=2){size&&GLctx.bufferSubData(target,offset,HEAPU8,data,size);return}GLctx.bufferSubData(target,offset,HEAPU8.subarray(data,data+size))};var _emscripten_glCheckFramebufferStatus=x0=>GLctx.checkFramebufferStatus(x0);var _emscripten_glClear=x0=>GLctx.clear(x0);var _emscripten_glClearBufferfi=(x0,x1,x2,x3)=>GLctx.clearBufferfi(x0,x1,x2,x3);var _emscripten_glClearBufferfv=(buffer,drawbuffer,value)=>{GLctx.clearBufferfv(buffer,drawbuffer,HEAPF32,value>>2)};var _emscripten_glClearBufferiv=(buffer,drawbuffer,value)=>{GLctx.clearBufferiv(buffer,drawbuffer,HEAP32,value>>2)};var _emscripten_glClearBufferuiv=(buffer,drawbuffer,value)=>{GLctx.clearBufferuiv(buffer,drawbuffer,HEAPU32,value>>2)};var _emscripten_glClearColor=(x0,x1,x2,x3)=>GLctx.clearColor(x0,x1,x2,x3);var _emscripten_glClearDepthf=x0=>GLctx.clearDepth(x0);var _emscripten_glClearStencil=x0=>GLctx.clearStencil(x0);var _emscripten_glClientWaitSync=(sync,flags,timeout)=>{timeout=Number(timeout);return GLctx.clientWaitSync(GL.syncs[sync],flags,timeout)};var _emscripten_glClipControlEXT=(origin,depth)=>{GLctx.extClipControl["clipControlEXT"](origin,depth)};var _emscripten_glColorMask=(red,green,blue,alpha)=>{GLctx.colorMask(!!red,!!green,!!blue,!!alpha)};var _emscripten_glCompileShader=shader=>{GLctx.compileShader(GL.shaders[shader])};var _emscripten_glCompressedTexImage2D=(target,level,internalFormat,width,height,border,imageSize,data)=>{if(GL.currentContext.version>=2){if(GLctx.currentPixelUnpackBufferBinding||!imageSize){GLctx.compressedTexImage2D(target,level,internalFormat,width,height,border,imageSize,data);return}GLctx.compressedTexImage2D(target,level,internalFormat,width,height,border,HEAPU8,data,imageSize);return}GLctx.compressedTexImage2D(target,level,internalFormat,width,height,border,HEAPU8.subarray(data,data+imageSize))};var _emscripten_glCompressedTexImage3D=(target,level,internalFormat,width,height,depth,border,imageSize,data)=>{if(GLctx.currentPixelUnpackBufferBinding){GLctx.compressedTexImage3D(target,level,internalFormat,width,height,depth,border,imageSize,data)}else{GLctx.compressedTexImage3D(target,level,internalFormat,width,height,depth,border,HEAPU8,data,imageSize)}};var _emscripten_glCompressedTexSubImage2D=(target,level,xoffset,yoffset,width,height,format,imageSize,data)=>{if(GL.currentContext.version>=2){if(GLctx.currentPixelUnpackBufferBinding||!imageSize){GLctx.compressedTexSubImage2D(target,level,xoffset,yoffset,width,height,format,imageSize,data);return}GLctx.compressedTexSubImage2D(target,level,xoffset,yoffset,width,height,format,HEAPU8,data,imageSize);return}GLctx.compressedTexSubImage2D(target,level,xoffset,yoffset,width,height,format,HEAPU8.subarray(data,data+imageSize))};var _emscripten_glCompressedTexSubImage3D=(target,level,xoffset,yoffset,zoffset,width,height,depth,format,imageSize,data)=>{if(GLctx.currentPixelUnpackBufferBinding){GLctx.compressedTexSubImage3D(target,level,xoffset,yoffset,zoffset,width,height,depth,format,imageSize,data)}else{GLctx.compressedTexSubImage3D(target,level,xoffset,yoffset,zoffset,width,height,depth,format,HEAPU8,data,imageSize)}};var _emscripten_glCopyBufferSubData=(x0,x1,x2,x3,x4)=>GLctx.copyBufferSubData(x0,x1,x2,x3,x4);var _emscripten_glCopyTexImage2D=(x0,x1,x2,x3,x4,x5,x6,x7)=>GLctx.copyTexImage2D(x0,x1,x2,x3,x4,x5,x6,x7);var _emscripten_glCopyTexSubImage2D=(x0,x1,x2,x3,x4,x5,x6,x7)=>GLctx.copyTexSubImage2D(x0,x1,x2,x3,x4,x5,x6,x7);var _emscripten_glCopyTexSubImage3D=(x0,x1,x2,x3,x4,x5,x6,x7,x8)=>GLctx.copyTexSubImage3D(x0,x1,x2,x3,x4,x5,x6,x7,x8);var _emscripten_glCreateProgram=()=>{var id=GL.getNewId(GL.programs);var program=GLctx.createProgram();program.name=id;program.maxUniformLength=program.maxAttributeLength=program.maxUniformBlockNameLength=0;program.uniformIdCounter=1;GL.programs[id]=program;return id};var _emscripten_glCreateShader=shaderType=>{var id=GL.getNewId(GL.shaders);GL.shaders[id]=GLctx.createShader(shaderType);return id};var _emscripten_glCullFace=x0=>GLctx.cullFace(x0);var _emscripten_glDeleteBuffers=(n,buffers)=>{for(var i=0;i>2];var buffer=GL.buffers[id];if(!buffer)continue;GLctx.deleteBuffer(buffer);buffer.name=0;GL.buffers[id]=null;if(id==GLctx.currentPixelPackBufferBinding)GLctx.currentPixelPackBufferBinding=0;if(id==GLctx.currentPixelUnpackBufferBinding)GLctx.currentPixelUnpackBufferBinding=0}};var _emscripten_glDeleteFramebuffers=(n,framebuffers)=>{for(var i=0;i>2];var framebuffer=GL.framebuffers[id];if(!framebuffer)continue;GLctx.deleteFramebuffer(framebuffer);framebuffer.name=0;GL.framebuffers[id]=null}};var _emscripten_glDeleteProgram=id=>{if(!id)return;var program=GL.programs[id];if(!program){GL.recordError(1281);return}GLctx.deleteProgram(program);program.name=0;GL.programs[id]=null};var _emscripten_glDeleteQueries=(n,ids)=>{for(var i=0;i>2];var query=GL.queries[id];if(!query)continue;GLctx.deleteQuery(query);GL.queries[id]=null}};var _emscripten_glDeleteQueriesEXT=(n,ids)=>{for(var i=0;i>2];var query=GL.queries[id];if(!query)continue;GLctx.disjointTimerQueryExt["deleteQueryEXT"](query);GL.queries[id]=null}};var _emscripten_glDeleteRenderbuffers=(n,renderbuffers)=>{for(var i=0;i>2];var renderbuffer=GL.renderbuffers[id];if(!renderbuffer)continue;GLctx.deleteRenderbuffer(renderbuffer);renderbuffer.name=0;GL.renderbuffers[id]=null}};var _emscripten_glDeleteSamplers=(n,samplers)=>{for(var i=0;i>2];var sampler=GL.samplers[id];if(!sampler)continue;GLctx.deleteSampler(sampler);sampler.name=0;GL.samplers[id]=null}};var _emscripten_glDeleteShader=id=>{if(!id)return;var shader=GL.shaders[id];if(!shader){GL.recordError(1281);return}GLctx.deleteShader(shader);GL.shaders[id]=null};var _emscripten_glDeleteSync=id=>{if(!id)return;var sync=GL.syncs[id];if(!sync){GL.recordError(1281);return}GLctx.deleteSync(sync);sync.name=0;GL.syncs[id]=null};var _emscripten_glDeleteTextures=(n,textures)=>{for(var i=0;i>2];var texture=GL.textures[id];if(!texture)continue;GLctx.deleteTexture(texture);texture.name=0;GL.textures[id]=null}};var _emscripten_glDeleteTransformFeedbacks=(n,ids)=>{for(var i=0;i>2];var transformFeedback=GL.transformFeedbacks[id];if(!transformFeedback)continue;GLctx.deleteTransformFeedback(transformFeedback);transformFeedback.name=0;GL.transformFeedbacks[id]=null}};var _emscripten_glDeleteVertexArrays=(n,vaos)=>{for(var i=0;i>2];GLctx.deleteVertexArray(GL.vaos[id]);GL.vaos[id]=null}};var _glDeleteVertexArrays=_emscripten_glDeleteVertexArrays;var _emscripten_glDeleteVertexArraysOES=_glDeleteVertexArrays;var _emscripten_glDepthFunc=x0=>GLctx.depthFunc(x0);var _emscripten_glDepthMask=flag=>{GLctx.depthMask(!!flag)};var _emscripten_glDepthRangef=(x0,x1)=>GLctx.depthRange(x0,x1);var _emscripten_glDetachShader=(program,shader)=>{GLctx.detachShader(GL.programs[program],GL.shaders[shader])};var _emscripten_glDisable=x0=>GLctx.disable(x0);var _emscripten_glDisableVertexAttribArray=index=>{GLctx.disableVertexAttribArray(index)};var _emscripten_glDrawArrays=(mode,first,count)=>{GLctx.drawArrays(mode,first,count)};var _emscripten_glDrawArraysInstanced=(mode,first,count,primcount)=>{GLctx.drawArraysInstanced(mode,first,count,primcount)};var _glDrawArraysInstanced=_emscripten_glDrawArraysInstanced;var _emscripten_glDrawArraysInstancedANGLE=_glDrawArraysInstanced;var _emscripten_glDrawArraysInstancedARB=_glDrawArraysInstanced;var _emscripten_glDrawArraysInstancedBaseInstanceWEBGL=(mode,first,count,instanceCount,baseInstance)=>{GLctx.dibvbi["drawArraysInstancedBaseInstanceWEBGL"](mode,first,count,instanceCount,baseInstance)};var _emscripten_glDrawArraysInstancedEXT=_glDrawArraysInstanced;var _emscripten_glDrawArraysInstancedNV=_glDrawArraysInstanced;var tempFixedLengthArray=[];var _emscripten_glDrawBuffers=(n,bufs)=>{var bufArray=tempFixedLengthArray[n];for(var i=0;i>2]}GLctx.drawBuffers(bufArray)};var _glDrawBuffers=_emscripten_glDrawBuffers;var _emscripten_glDrawBuffersEXT=_glDrawBuffers;var _emscripten_glDrawBuffersWEBGL=_glDrawBuffers;var _emscripten_glDrawElements=(mode,count,type,indices)=>{GLctx.drawElements(mode,count,type,indices)};var _emscripten_glDrawElementsInstanced=(mode,count,type,indices,primcount)=>{GLctx.drawElementsInstanced(mode,count,type,indices,primcount)};var _glDrawElementsInstanced=_emscripten_glDrawElementsInstanced;var _emscripten_glDrawElementsInstancedANGLE=_glDrawElementsInstanced;var _emscripten_glDrawElementsInstancedARB=_glDrawElementsInstanced;var _emscripten_glDrawElementsInstancedBaseVertexBaseInstanceWEBGL=(mode,count,type,offset,instanceCount,baseVertex,baseinstance)=>{GLctx.dibvbi["drawElementsInstancedBaseVertexBaseInstanceWEBGL"](mode,count,type,offset,instanceCount,baseVertex,baseinstance)};var _emscripten_glDrawElementsInstancedEXT=_glDrawElementsInstanced;var _emscripten_glDrawElementsInstancedNV=_glDrawElementsInstanced;var _glDrawElements=_emscripten_glDrawElements;var _emscripten_glDrawRangeElements=(mode,start,end,count,type,indices)=>{_glDrawElements(mode,count,type,indices)};var _emscripten_glEnable=x0=>GLctx.enable(x0);var _emscripten_glEnableVertexAttribArray=index=>{GLctx.enableVertexAttribArray(index)};var _emscripten_glEndQuery=x0=>GLctx.endQuery(x0);var _emscripten_glEndQueryEXT=target=>{GLctx.disjointTimerQueryExt["endQueryEXT"](target)};var _emscripten_glEndTransformFeedback=()=>GLctx.endTransformFeedback();var _emscripten_glFenceSync=(condition,flags)=>{var sync=GLctx.fenceSync(condition,flags);if(sync){var id=GL.getNewId(GL.syncs);sync.name=id;GL.syncs[id]=sync;return id}return 0};var _emscripten_glFinish=()=>GLctx.finish();var _emscripten_glFlush=()=>GLctx.flush();var _emscripten_glFramebufferRenderbuffer=(target,attachment,renderbuffertarget,renderbuffer)=>{GLctx.framebufferRenderbuffer(target,attachment,renderbuffertarget,GL.renderbuffers[renderbuffer])};var _emscripten_glFramebufferTexture2D=(target,attachment,textarget,texture,level)=>{GLctx.framebufferTexture2D(target,attachment,textarget,GL.textures[texture],level)};var _emscripten_glFramebufferTextureLayer=(target,attachment,texture,level,layer)=>{GLctx.framebufferTextureLayer(target,attachment,GL.textures[texture],level,layer)};var _emscripten_glFrontFace=x0=>GLctx.frontFace(x0);var _emscripten_glGenBuffers=(n,buffers)=>{GL.genObject(n,buffers,"createBuffer",GL.buffers)};var _emscripten_glGenFramebuffers=(n,ids)=>{GL.genObject(n,ids,"createFramebuffer",GL.framebuffers)};var _emscripten_glGenQueries=(n,ids)=>{GL.genObject(n,ids,"createQuery",GL.queries)};var _emscripten_glGenQueriesEXT=(n,ids)=>{for(var i=0;i>2]=0;return}var id=GL.getNewId(GL.queries);query.name=id;GL.queries[id]=query;HEAP32[ids+i*4>>2]=id}};var _emscripten_glGenRenderbuffers=(n,renderbuffers)=>{GL.genObject(n,renderbuffers,"createRenderbuffer",GL.renderbuffers)};var _emscripten_glGenSamplers=(n,samplers)=>{GL.genObject(n,samplers,"createSampler",GL.samplers)};var _emscripten_glGenTextures=(n,textures)=>{GL.genObject(n,textures,"createTexture",GL.textures)};var _emscripten_glGenTransformFeedbacks=(n,ids)=>{GL.genObject(n,ids,"createTransformFeedback",GL.transformFeedbacks)};var _emscripten_glGenVertexArrays=(n,arrays)=>{GL.genObject(n,arrays,"createVertexArray",GL.vaos)};var _glGenVertexArrays=_emscripten_glGenVertexArrays;var _emscripten_glGenVertexArraysOES=_glGenVertexArrays;var _emscripten_glGenerateMipmap=x0=>GLctx.generateMipmap(x0);var __glGetActiveAttribOrUniform=(funcName,program,index,bufSize,length,size,type,name)=>{program=GL.programs[program];var info=GLctx[funcName](program,index);if(info){var numBytesWrittenExclNull=name&&stringToUTF8(info.name,name,bufSize);if(length)HEAP32[length>>2]=numBytesWrittenExclNull;if(size)HEAP32[size>>2]=info.size;if(type)HEAP32[type>>2]=info.type}};var _emscripten_glGetActiveAttrib=(program,index,bufSize,length,size,type,name)=>__glGetActiveAttribOrUniform("getActiveAttrib",program,index,bufSize,length,size,type,name);var _emscripten_glGetActiveUniform=(program,index,bufSize,length,size,type,name)=>__glGetActiveAttribOrUniform("getActiveUniform",program,index,bufSize,length,size,type,name);var _emscripten_glGetActiveUniformBlockName=(program,uniformBlockIndex,bufSize,length,uniformBlockName)=>{program=GL.programs[program];var result=GLctx.getActiveUniformBlockName(program,uniformBlockIndex);if(!result)return;if(uniformBlockName&&bufSize>0){var numBytesWrittenExclNull=stringToUTF8(result,uniformBlockName,bufSize);if(length)HEAP32[length>>2]=numBytesWrittenExclNull}else{if(length)HEAP32[length>>2]=0}};var _emscripten_glGetActiveUniformBlockiv=(program,uniformBlockIndex,pname,params)=>{if(!params){GL.recordError(1281);return}program=GL.programs[program];if(pname==35393){var name=GLctx.getActiveUniformBlockName(program,uniformBlockIndex);HEAP32[params>>2]=name.length+1;return}var result=GLctx.getActiveUniformBlockParameter(program,uniformBlockIndex,pname);if(result===null)return;if(pname==35395){for(var i=0;i>2]=result[i]}}else{HEAP32[params>>2]=result}};var _emscripten_glGetActiveUniformsiv=(program,uniformCount,uniformIndices,pname,params)=>{if(!params){GL.recordError(1281);return}if(uniformCount>0&&uniformIndices==0){GL.recordError(1281);return}program=GL.programs[program];var ids=[];for(var i=0;i>2])}var result=GLctx.getActiveUniforms(program,ids,pname);if(!result)return;var len=result.length;for(var i=0;i>2]=result[i]}};var _emscripten_glGetAttachedShaders=(program,maxCount,count,shaders)=>{var result=GLctx.getAttachedShaders(GL.programs[program]);var len=result.length;if(len>maxCount){len=maxCount}HEAP32[count>>2]=len;for(var i=0;i>2]=id}};var _emscripten_glGetAttribLocation=(program,name)=>GLctx.getAttribLocation(GL.programs[program],UTF8ToString(name));var writeI53ToI64=(ptr,num)=>{HEAPU32[ptr>>2]=num;var lower=HEAPU32[ptr>>2];HEAPU32[ptr+4>>2]=(num-lower)/4294967296};var webglGetExtensions=()=>{var exts=getEmscriptenSupportedExtensions(GLctx);exts=exts.concat(exts.map(e=>"GL_"+e));return exts};var emscriptenWebGLGet=(name_,p,type)=>{if(!p){GL.recordError(1281);return}var ret=undefined;switch(name_){case 36346:ret=1;break;case 36344:if(type!=0&&type!=1){GL.recordError(1280)}return;case 34814:case 36345:ret=0;break;case 34466:var formats=GLctx.getParameter(34467);ret=formats?formats.length:0;break;case 33309:if(GL.currentContext.version<2){GL.recordError(1282);return}ret=webglGetExtensions().length;break;case 33307:case 33308:if(GL.currentContext.version<2){GL.recordError(1280);return}ret=name_==33307?3:0;break}if(ret===undefined){var result=GLctx.getParameter(name_);switch(typeof result){case"number":ret=result;break;case"boolean":ret=result?1:0;break;case"string":GL.recordError(1280);return;case"object":if(result===null){switch(name_){case 34964:case 35725:case 34965:case 36006:case 36007:case 32873:case 34229:case 36662:case 36663:case 35053:case 35055:case 36010:case 35097:case 35869:case 32874:case 36389:case 35983:case 35368:case 34068:{ret=0;break}default:{GL.recordError(1280);return}}}else if(result instanceof Float32Array||result instanceof Uint32Array||result instanceof Int32Array||result instanceof Array){for(var i=0;i>2]=result[i];break;case 2:HEAPF32[p+i*4>>2]=result[i];break;case 4:HEAP8[p+i]=result[i]?1:0;break}}return}else{try{ret=result.name|0}catch(e){GL.recordError(1280);err(`GL_INVALID_ENUM in glGet${type}v: Unknown object returned from WebGL getParameter(${name_})! (error: ${e})`);return}}break;default:GL.recordError(1280);err(`GL_INVALID_ENUM in glGet${type}v: Native code calling glGet${type}v(${name_}) and it returns ${result} of type ${typeof result}!`);return}}switch(type){case 1:writeI53ToI64(p,ret);break;case 0:HEAP32[p>>2]=ret;break;case 2:HEAPF32[p>>2]=ret;break;case 4:HEAP8[p]=ret?1:0;break}};var _emscripten_glGetBooleanv=(name_,p)=>emscriptenWebGLGet(name_,p,4);var _emscripten_glGetBufferParameteri64v=(target,value,data)=>{if(!data){GL.recordError(1281);return}writeI53ToI64(data,GLctx.getBufferParameter(target,value))};var _emscripten_glGetBufferParameteriv=(target,value,data)=>{if(!data){GL.recordError(1281);return}HEAP32[data>>2]=GLctx.getBufferParameter(target,value)};var _emscripten_glGetError=()=>{var error=GLctx.getError()||GL.lastError;GL.lastError=0;return error};var _emscripten_glGetFloatv=(name_,p)=>emscriptenWebGLGet(name_,p,2);var _emscripten_glGetFragDataLocation=(program,name)=>GLctx.getFragDataLocation(GL.programs[program],UTF8ToString(name));var _emscripten_glGetFramebufferAttachmentParameteriv=(target,attachment,pname,params)=>{var result=GLctx.getFramebufferAttachmentParameter(target,attachment,pname);if(result instanceof WebGLRenderbuffer||result instanceof WebGLTexture){result=result.name|0}HEAP32[params>>2]=result};var emscriptenWebGLGetIndexed=(target,index,data,type)=>{if(!data){GL.recordError(1281);return}var result=GLctx.getIndexedParameter(target,index);var ret;switch(typeof result){case"boolean":ret=result?1:0;break;case"number":ret=result;break;case"object":if(result===null){switch(target){case 35983:case 35368:ret=0;break;default:{GL.recordError(1280);return}}}else if(result instanceof WebGLBuffer){ret=result.name|0}else{GL.recordError(1280);return}break;default:GL.recordError(1280);return}switch(type){case 1:writeI53ToI64(data,ret);break;case 0:HEAP32[data>>2]=ret;break;case 2:HEAPF32[data>>2]=ret;break;case 4:HEAP8[data]=ret?1:0;break;default:abort("internal emscriptenWebGLGetIndexed() error, bad type: "+type)}};var _emscripten_glGetInteger64i_v=(target,index,data)=>emscriptenWebGLGetIndexed(target,index,data,1);var _emscripten_glGetInteger64v=(name_,p)=>{emscriptenWebGLGet(name_,p,1)};var _emscripten_glGetIntegeri_v=(target,index,data)=>emscriptenWebGLGetIndexed(target,index,data,0);var _emscripten_glGetIntegerv=(name_,p)=>emscriptenWebGLGet(name_,p,0);var _emscripten_glGetInternalformativ=(target,internalformat,pname,bufSize,params)=>{if(bufSize<0){GL.recordError(1281);return}if(!params){GL.recordError(1281);return}var ret=GLctx.getInternalformatParameter(target,internalformat,pname);if(ret===null)return;for(var i=0;i>2]=ret[i]}};var _emscripten_glGetProgramBinary=(program,bufSize,length,binaryFormat,binary)=>{GL.recordError(1282)};var _emscripten_glGetProgramInfoLog=(program,maxLength,length,infoLog)=>{var log=GLctx.getProgramInfoLog(GL.programs[program]);if(log===null)log="(unknown error)";var numBytesWrittenExclNull=maxLength>0&&infoLog?stringToUTF8(log,infoLog,maxLength):0;if(length)HEAP32[length>>2]=numBytesWrittenExclNull};var _emscripten_glGetProgramiv=(program,pname,p)=>{if(!p){GL.recordError(1281);return}if(program>=GL.counter){GL.recordError(1281);return}program=GL.programs[program];if(pname==35716){var log=GLctx.getProgramInfoLog(program);if(log===null)log="(unknown error)";HEAP32[p>>2]=log.length+1}else if(pname==35719){if(!program.maxUniformLength){var numActiveUniforms=GLctx.getProgramParameter(program,35718);for(var i=0;i>2]=program.maxUniformLength}else if(pname==35722){if(!program.maxAttributeLength){var numActiveAttributes=GLctx.getProgramParameter(program,35721);for(var i=0;i>2]=program.maxAttributeLength}else if(pname==35381){if(!program.maxUniformBlockNameLength){var numActiveUniformBlocks=GLctx.getProgramParameter(program,35382);for(var i=0;i>2]=program.maxUniformBlockNameLength}else{HEAP32[p>>2]=GLctx.getProgramParameter(program,pname)}};var _emscripten_glGetQueryObjecti64vEXT=(id,pname,params)=>{if(!params){GL.recordError(1281);return}var query=GL.queries[id];var param;if(GL.currentContext.version<2){param=GLctx.disjointTimerQueryExt["getQueryObjectEXT"](query,pname)}else{param=GLctx.getQueryParameter(query,pname)}var ret;if(typeof param=="boolean"){ret=param?1:0}else{ret=param}writeI53ToI64(params,ret)};var _emscripten_glGetQueryObjectivEXT=(id,pname,params)=>{if(!params){GL.recordError(1281);return}var query=GL.queries[id];var param=GLctx.disjointTimerQueryExt["getQueryObjectEXT"](query,pname);var ret;if(typeof param=="boolean"){ret=param?1:0}else{ret=param}HEAP32[params>>2]=ret};var _glGetQueryObjecti64vEXT=_emscripten_glGetQueryObjecti64vEXT;var _emscripten_glGetQueryObjectui64vEXT=_glGetQueryObjecti64vEXT;var _emscripten_glGetQueryObjectuiv=(id,pname,params)=>{if(!params){GL.recordError(1281);return}var query=GL.queries[id];var param=GLctx.getQueryParameter(query,pname);var ret;if(typeof param=="boolean"){ret=param?1:0}else{ret=param}HEAP32[params>>2]=ret};var _glGetQueryObjectivEXT=_emscripten_glGetQueryObjectivEXT;var _emscripten_glGetQueryObjectuivEXT=_glGetQueryObjectivEXT;var _emscripten_glGetQueryiv=(target,pname,params)=>{if(!params){GL.recordError(1281);return}HEAP32[params>>2]=GLctx.getQuery(target,pname)};var _emscripten_glGetQueryivEXT=(target,pname,params)=>{if(!params){GL.recordError(1281);return}HEAP32[params>>2]=GLctx.disjointTimerQueryExt["getQueryEXT"](target,pname)};var _emscripten_glGetRenderbufferParameteriv=(target,pname,params)=>{if(!params){GL.recordError(1281);return}HEAP32[params>>2]=GLctx.getRenderbufferParameter(target,pname)};var _emscripten_glGetSamplerParameterfv=(sampler,pname,params)=>{if(!params){GL.recordError(1281);return}HEAPF32[params>>2]=GLctx.getSamplerParameter(GL.samplers[sampler],pname)};var _emscripten_glGetSamplerParameteriv=(sampler,pname,params)=>{if(!params){GL.recordError(1281);return}HEAP32[params>>2]=GLctx.getSamplerParameter(GL.samplers[sampler],pname)};var _emscripten_glGetShaderInfoLog=(shader,maxLength,length,infoLog)=>{var log=GLctx.getShaderInfoLog(GL.shaders[shader]);if(log===null)log="(unknown error)";var numBytesWrittenExclNull=maxLength>0&&infoLog?stringToUTF8(log,infoLog,maxLength):0;if(length)HEAP32[length>>2]=numBytesWrittenExclNull};var _emscripten_glGetShaderPrecisionFormat=(shaderType,precisionType,range,precision)=>{var result=GLctx.getShaderPrecisionFormat(shaderType,precisionType);HEAP32[range>>2]=result.rangeMin;HEAP32[range+4>>2]=result.rangeMax;HEAP32[precision>>2]=result.precision};var _emscripten_glGetShaderSource=(shader,bufSize,length,source)=>{var result=GLctx.getShaderSource(GL.shaders[shader]);if(!result)return;var numBytesWrittenExclNull=bufSize>0&&source?stringToUTF8(result,source,bufSize):0;if(length)HEAP32[length>>2]=numBytesWrittenExclNull};var _emscripten_glGetShaderiv=(shader,pname,p)=>{if(!p){GL.recordError(1281);return}if(pname==35716){var log=GLctx.getShaderInfoLog(GL.shaders[shader]);if(log===null)log="(unknown error)";var logLength=log?log.length+1:0;HEAP32[p>>2]=logLength}else if(pname==35720){var source=GLctx.getShaderSource(GL.shaders[shader]);var sourceLength=source?source.length+1:0;HEAP32[p>>2]=sourceLength}else{HEAP32[p>>2]=GLctx.getShaderParameter(GL.shaders[shader],pname)}};var stringToNewUTF8=str=>{var size=lengthBytesUTF8(str)+1;var ret=_malloc(size);if(ret)stringToUTF8(str,ret,size);return ret};var _emscripten_glGetString=name_=>{var ret=GL.stringCache[name_];if(!ret){switch(name_){case 7939:ret=stringToNewUTF8(webglGetExtensions().join(" "));break;case 7936:case 7937:case 37445:case 37446:var s=GLctx.getParameter(name_);if(!s){GL.recordError(1280)}ret=s?stringToNewUTF8(s):0;break;case 7938:var webGLVersion=GLctx.getParameter(7938);var glVersion=`OpenGL ES 2.0 (${webGLVersion})`;if(GL.currentContext.version>=2)glVersion=`OpenGL ES 3.0 (${webGLVersion})`;ret=stringToNewUTF8(glVersion);break;case 35724:var glslVersion=GLctx.getParameter(35724);var ver_re=/^WebGL GLSL ES ([0-9]\.[0-9][0-9]?)(?:$| .*)/;var ver_num=glslVersion.match(ver_re);if(ver_num!==null){if(ver_num[1].length==3)ver_num[1]=ver_num[1]+"0";glslVersion=`OpenGL ES GLSL ES ${ver_num[1]} (${glslVersion})`}ret=stringToNewUTF8(glslVersion);break;default:GL.recordError(1280)}GL.stringCache[name_]=ret}return ret};var _emscripten_glGetStringi=(name,index)=>{if(GL.currentContext.version<2){GL.recordError(1282);return 0}var stringiCache=GL.stringiCache[name];if(stringiCache){if(index<0||index>=stringiCache.length){GL.recordError(1281);return 0}return stringiCache[index]}switch(name){case 7939:var exts=webglGetExtensions().map(stringToNewUTF8);stringiCache=GL.stringiCache[name]=exts;if(index<0||index>=stringiCache.length){GL.recordError(1281);return 0}return stringiCache[index];default:GL.recordError(1280);return 0}};var _emscripten_glGetSynciv=(sync,pname,bufSize,length,values)=>{if(bufSize<0){GL.recordError(1281);return}if(!values){GL.recordError(1281);return}var ret=GLctx.getSyncParameter(GL.syncs[sync],pname);if(ret!==null){HEAP32[values>>2]=ret;if(length)HEAP32[length>>2]=1}};var _emscripten_glGetTexParameterfv=(target,pname,params)=>{if(!params){GL.recordError(1281);return}HEAPF32[params>>2]=GLctx.getTexParameter(target,pname)};var _emscripten_glGetTexParameteriv=(target,pname,params)=>{if(!params){GL.recordError(1281);return}HEAP32[params>>2]=GLctx.getTexParameter(target,pname)};var _emscripten_glGetTransformFeedbackVarying=(program,index,bufSize,length,size,type,name)=>{program=GL.programs[program];var info=GLctx.getTransformFeedbackVarying(program,index);if(!info)return;if(name&&bufSize>0){var numBytesWrittenExclNull=stringToUTF8(info.name,name,bufSize);if(length)HEAP32[length>>2]=numBytesWrittenExclNull}else{if(length)HEAP32[length>>2]=0}if(size)HEAP32[size>>2]=info.size;if(type)HEAP32[type>>2]=info.type};var _emscripten_glGetUniformBlockIndex=(program,uniformBlockName)=>GLctx.getUniformBlockIndex(GL.programs[program],UTF8ToString(uniformBlockName));var _emscripten_glGetUniformIndices=(program,uniformCount,uniformNames,uniformIndices)=>{if(!uniformIndices){GL.recordError(1281);return}if(uniformCount>0&&(uniformNames==0||uniformIndices==0)){GL.recordError(1281);return}program=GL.programs[program];var names=[];for(var i=0;i>2]));var result=GLctx.getUniformIndices(program,names);if(!result)return;var len=result.length;for(var i=0;i>2]=result[i]}};var jstoi_q=str=>parseInt(str);var webglGetLeftBracePos=name=>name.slice(-1)=="]"&&name.lastIndexOf("[");var webglPrepareUniformLocationsBeforeFirstUse=program=>{var uniformLocsById=program.uniformLocsById,uniformSizeAndIdsByName=program.uniformSizeAndIdsByName,i,j;if(!uniformLocsById){program.uniformLocsById=uniformLocsById={};program.uniformArrayNamesById={};var numActiveUniforms=GLctx.getProgramParameter(program,35718);for(i=0;i0?nm.slice(0,lb):nm;var id=program.uniformIdCounter;program.uniformIdCounter+=sz;uniformSizeAndIdsByName[arrayName]=[sz,id];for(j=0;j{name=UTF8ToString(name);if(program=GL.programs[program]){webglPrepareUniformLocationsBeforeFirstUse(program);var uniformLocsById=program.uniformLocsById;var arrayIndex=0;var uniformBaseName=name;var leftBrace=webglGetLeftBracePos(name);if(leftBrace>0){arrayIndex=jstoi_q(name.slice(leftBrace+1))>>>0;uniformBaseName=name.slice(0,leftBrace)}var sizeAndId=program.uniformSizeAndIdsByName[uniformBaseName];if(sizeAndId&&arrayIndex{var p=GLctx.currentProgram;if(p){var webglLoc=p.uniformLocsById[location];if(typeof webglLoc=="number"){p.uniformLocsById[location]=webglLoc=GLctx.getUniformLocation(p,p.uniformArrayNamesById[location]+(webglLoc>0?`[${webglLoc}]`:""))}return webglLoc}else{GL.recordError(1282)}};var emscriptenWebGLGetUniform=(program,location,params,type)=>{if(!params){GL.recordError(1281);return}program=GL.programs[program];webglPrepareUniformLocationsBeforeFirstUse(program);var data=GLctx.getUniform(program,webglGetUniformLocation(location));if(typeof data=="number"||typeof data=="boolean"){switch(type){case 0:HEAP32[params>>2]=data;break;case 2:HEAPF32[params>>2]=data;break}}else{for(var i=0;i>2]=data[i];break;case 2:HEAPF32[params+i*4>>2]=data[i];break}}}};var _emscripten_glGetUniformfv=(program,location,params)=>{emscriptenWebGLGetUniform(program,location,params,2)};var _emscripten_glGetUniformiv=(program,location,params)=>{emscriptenWebGLGetUniform(program,location,params,0)};var _emscripten_glGetUniformuiv=(program,location,params)=>emscriptenWebGLGetUniform(program,location,params,0);var emscriptenWebGLGetVertexAttrib=(index,pname,params,type)=>{if(!params){GL.recordError(1281);return}var data=GLctx.getVertexAttrib(index,pname);if(pname==34975){HEAP32[params>>2]=data&&data["name"]}else if(typeof data=="number"||typeof data=="boolean"){switch(type){case 0:HEAP32[params>>2]=data;break;case 2:HEAPF32[params>>2]=data;break;case 5:HEAP32[params>>2]=Math.fround(data);break}}else{for(var i=0;i>2]=data[i];break;case 2:HEAPF32[params+i*4>>2]=data[i];break;case 5:HEAP32[params+i*4>>2]=Math.fround(data[i]);break}}}};var _emscripten_glGetVertexAttribIiv=(index,pname,params)=>{emscriptenWebGLGetVertexAttrib(index,pname,params,0)};var _glGetVertexAttribIiv=_emscripten_glGetVertexAttribIiv;var _emscripten_glGetVertexAttribIuiv=_glGetVertexAttribIiv;var _emscripten_glGetVertexAttribPointerv=(index,pname,pointer)=>{if(!pointer){GL.recordError(1281);return}HEAP32[pointer>>2]=GLctx.getVertexAttribOffset(index,pname)};var _emscripten_glGetVertexAttribfv=(index,pname,params)=>{emscriptenWebGLGetVertexAttrib(index,pname,params,2)};var _emscripten_glGetVertexAttribiv=(index,pname,params)=>{emscriptenWebGLGetVertexAttrib(index,pname,params,5)};var _emscripten_glHint=(x0,x1)=>GLctx.hint(x0,x1);var _emscripten_glInvalidateFramebuffer=(target,numAttachments,attachments)=>{var list=tempFixedLengthArray[numAttachments];for(var i=0;i>2]}GLctx.invalidateFramebuffer(target,list)};var _emscripten_glInvalidateSubFramebuffer=(target,numAttachments,attachments,x,y,width,height)=>{var list=tempFixedLengthArray[numAttachments];for(var i=0;i>2]}GLctx.invalidateSubFramebuffer(target,list,x,y,width,height)};var _emscripten_glIsBuffer=buffer=>{var b=GL.buffers[buffer];if(!b)return 0;return GLctx.isBuffer(b)};var _emscripten_glIsEnabled=x0=>GLctx.isEnabled(x0);var _emscripten_glIsFramebuffer=framebuffer=>{var fb=GL.framebuffers[framebuffer];if(!fb)return 0;return GLctx.isFramebuffer(fb)};var _emscripten_glIsProgram=program=>{program=GL.programs[program];if(!program)return 0;return GLctx.isProgram(program)};var _emscripten_glIsQuery=id=>{var query=GL.queries[id];if(!query)return 0;return GLctx.isQuery(query)};var _emscripten_glIsQueryEXT=id=>{var query=GL.queries[id];if(!query)return 0;return GLctx.disjointTimerQueryExt["isQueryEXT"](query)};var _emscripten_glIsRenderbuffer=renderbuffer=>{var rb=GL.renderbuffers[renderbuffer];if(!rb)return 0;return GLctx.isRenderbuffer(rb)};var _emscripten_glIsSampler=id=>{var sampler=GL.samplers[id];if(!sampler)return 0;return GLctx.isSampler(sampler)};var _emscripten_glIsShader=shader=>{var s=GL.shaders[shader];if(!s)return 0;return GLctx.isShader(s)};var _emscripten_glIsSync=sync=>GLctx.isSync(GL.syncs[sync]);var _emscripten_glIsTexture=id=>{var texture=GL.textures[id];if(!texture)return 0;return GLctx.isTexture(texture)};var _emscripten_glIsTransformFeedback=id=>GLctx.isTransformFeedback(GL.transformFeedbacks[id]);var _emscripten_glIsVertexArray=array=>{var vao=GL.vaos[array];if(!vao)return 0;return GLctx.isVertexArray(vao)};var _glIsVertexArray=_emscripten_glIsVertexArray;var _emscripten_glIsVertexArrayOES=_glIsVertexArray;var _emscripten_glLineWidth=x0=>GLctx.lineWidth(x0);var _emscripten_glLinkProgram=program=>{program=GL.programs[program];GLctx.linkProgram(program);program.uniformLocsById=0;program.uniformSizeAndIdsByName={}};var _emscripten_glMultiDrawArraysInstancedBaseInstanceWEBGL=(mode,firsts,counts,instanceCounts,baseInstances,drawCount)=>{GLctx.mdibvbi["multiDrawArraysInstancedBaseInstanceWEBGL"](mode,HEAP32,firsts>>2,HEAP32,counts>>2,HEAP32,instanceCounts>>2,HEAPU32,baseInstances>>2,drawCount)};var _emscripten_glMultiDrawElementsInstancedBaseVertexBaseInstanceWEBGL=(mode,counts,type,offsets,instanceCounts,baseVertices,baseInstances,drawCount)=>{GLctx.mdibvbi["multiDrawElementsInstancedBaseVertexBaseInstanceWEBGL"](mode,HEAP32,counts>>2,type,HEAP32,offsets>>2,HEAP32,instanceCounts>>2,HEAP32,baseVertices>>2,HEAPU32,baseInstances>>2,drawCount)};var _emscripten_glPauseTransformFeedback=()=>GLctx.pauseTransformFeedback();var _emscripten_glPixelStorei=(pname,param)=>{if(pname==3317){GL.unpackAlignment=param}else if(pname==3314){GL.unpackRowLength=param}GLctx.pixelStorei(pname,param)};var _emscripten_glPolygonModeWEBGL=(face,mode)=>{GLctx.webglPolygonMode["polygonModeWEBGL"](face,mode)};var _emscripten_glPolygonOffset=(x0,x1)=>GLctx.polygonOffset(x0,x1);var _emscripten_glPolygonOffsetClampEXT=(factor,units,clamp)=>{GLctx.extPolygonOffsetClamp["polygonOffsetClampEXT"](factor,units,clamp)};var _emscripten_glProgramBinary=(program,binaryFormat,binary,length)=>{GL.recordError(1280)};var _emscripten_glProgramParameteri=(program,pname,value)=>{GL.recordError(1280)};var _emscripten_glQueryCounterEXT=(id,target)=>{GLctx.disjointTimerQueryExt["queryCounterEXT"](GL.queries[id],target)};var _emscripten_glReadBuffer=x0=>GLctx.readBuffer(x0);var computeUnpackAlignedImageSize=(width,height,sizePerPixel)=>{function roundedToNextMultipleOf(x,y){return x+y-1&-y}var plainRowSize=(GL.unpackRowLength||width)*sizePerPixel;var alignedRowSize=roundedToNextMultipleOf(plainRowSize,GL.unpackAlignment);return height*alignedRowSize};var colorChannelsInGlTextureFormat=format=>{var colorChannels={5:3,6:4,8:2,29502:3,29504:4,26917:2,26918:2,29846:3,29847:4};return colorChannels[format-6402]||1};var heapObjectForWebGLType=type=>{type-=5120;if(type==0)return HEAP8;if(type==1)return HEAPU8;if(type==2)return HEAP16;if(type==4)return HEAP32;if(type==6)return HEAPF32;if(type==5||type==28922||type==28520||type==30779||type==30782)return HEAPU32;return HEAPU16};var toTypedArrayIndex=(pointer,heap)=>pointer>>>31-Math.clz32(heap.BYTES_PER_ELEMENT);var emscriptenWebGLGetTexPixelData=(type,format,width,height,pixels,internalFormat)=>{var heap=heapObjectForWebGLType(type);var sizePerPixel=colorChannelsInGlTextureFormat(format)*heap.BYTES_PER_ELEMENT;var bytes=computeUnpackAlignedImageSize(width,height,sizePerPixel);return heap.subarray(toTypedArrayIndex(pixels,heap),toTypedArrayIndex(pixels+bytes,heap))};var _emscripten_glReadPixels=(x,y,width,height,format,type,pixels)=>{if(GL.currentContext.version>=2){if(GLctx.currentPixelPackBufferBinding){GLctx.readPixels(x,y,width,height,format,type,pixels);return}var heap=heapObjectForWebGLType(type);var target=toTypedArrayIndex(pixels,heap);GLctx.readPixels(x,y,width,height,format,type,heap,target);return}var pixelData=emscriptenWebGLGetTexPixelData(type,format,width,height,pixels,format);if(!pixelData){GL.recordError(1280);return}GLctx.readPixels(x,y,width,height,format,type,pixelData)};var _emscripten_glReleaseShaderCompiler=()=>{};var _emscripten_glRenderbufferStorage=(x0,x1,x2,x3)=>GLctx.renderbufferStorage(x0,x1,x2,x3);var _emscripten_glRenderbufferStorageMultisample=(x0,x1,x2,x3,x4)=>GLctx.renderbufferStorageMultisample(x0,x1,x2,x3,x4);var _emscripten_glResumeTransformFeedback=()=>GLctx.resumeTransformFeedback();var _emscripten_glSampleCoverage=(value,invert)=>{GLctx.sampleCoverage(value,!!invert)};var _emscripten_glSamplerParameterf=(sampler,pname,param)=>{GLctx.samplerParameterf(GL.samplers[sampler],pname,param)};var _emscripten_glSamplerParameterfv=(sampler,pname,params)=>{var param=HEAPF32[params>>2];GLctx.samplerParameterf(GL.samplers[sampler],pname,param)};var _emscripten_glSamplerParameteri=(sampler,pname,param)=>{GLctx.samplerParameteri(GL.samplers[sampler],pname,param)};var _emscripten_glSamplerParameteriv=(sampler,pname,params)=>{var param=HEAP32[params>>2];GLctx.samplerParameteri(GL.samplers[sampler],pname,param)};var _emscripten_glScissor=(x0,x1,x2,x3)=>GLctx.scissor(x0,x1,x2,x3);var _emscripten_glShaderBinary=(count,shaders,binaryformat,binary,length)=>{GL.recordError(1280)};var _emscripten_glShaderSource=(shader,count,string,length)=>{var source=GL.getSource(shader,count,string,length);GLctx.shaderSource(GL.shaders[shader],source)};var _emscripten_glStencilFunc=(x0,x1,x2)=>GLctx.stencilFunc(x0,x1,x2);var _emscripten_glStencilFuncSeparate=(x0,x1,x2,x3)=>GLctx.stencilFuncSeparate(x0,x1,x2,x3);var _emscripten_glStencilMask=x0=>GLctx.stencilMask(x0);var _emscripten_glStencilMaskSeparate=(x0,x1)=>GLctx.stencilMaskSeparate(x0,x1);var _emscripten_glStencilOp=(x0,x1,x2)=>GLctx.stencilOp(x0,x1,x2);var _emscripten_glStencilOpSeparate=(x0,x1,x2,x3)=>GLctx.stencilOpSeparate(x0,x1,x2,x3);var _emscripten_glTexImage2D=(target,level,internalFormat,width,height,border,format,type,pixels)=>{if(GL.currentContext.version>=2){if(GLctx.currentPixelUnpackBufferBinding){GLctx.texImage2D(target,level,internalFormat,width,height,border,format,type,pixels);return}if(pixels){var heap=heapObjectForWebGLType(type);var index=toTypedArrayIndex(pixels,heap);GLctx.texImage2D(target,level,internalFormat,width,height,border,format,type,heap,index);return}}var pixelData=pixels?emscriptenWebGLGetTexPixelData(type,format,width,height,pixels,internalFormat):null;GLctx.texImage2D(target,level,internalFormat,width,height,border,format,type,pixelData)};var _emscripten_glTexImage3D=(target,level,internalFormat,width,height,depth,border,format,type,pixels)=>{if(GLctx.currentPixelUnpackBufferBinding){GLctx.texImage3D(target,level,internalFormat,width,height,depth,border,format,type,pixels)}else if(pixels){var heap=heapObjectForWebGLType(type);GLctx.texImage3D(target,level,internalFormat,width,height,depth,border,format,type,heap,toTypedArrayIndex(pixels,heap))}else{GLctx.texImage3D(target,level,internalFormat,width,height,depth,border,format,type,null)}};var _emscripten_glTexParameterf=(x0,x1,x2)=>GLctx.texParameterf(x0,x1,x2);var _emscripten_glTexParameterfv=(target,pname,params)=>{var param=HEAPF32[params>>2];GLctx.texParameterf(target,pname,param)};var _emscripten_glTexParameteri=(x0,x1,x2)=>GLctx.texParameteri(x0,x1,x2);var _emscripten_glTexParameteriv=(target,pname,params)=>{var param=HEAP32[params>>2];GLctx.texParameteri(target,pname,param)};var _emscripten_glTexStorage2D=(x0,x1,x2,x3,x4)=>GLctx.texStorage2D(x0,x1,x2,x3,x4);var _emscripten_glTexStorage3D=(x0,x1,x2,x3,x4,x5)=>GLctx.texStorage3D(x0,x1,x2,x3,x4,x5);var _emscripten_glTexSubImage2D=(target,level,xoffset,yoffset,width,height,format,type,pixels)=>{if(GL.currentContext.version>=2){if(GLctx.currentPixelUnpackBufferBinding){GLctx.texSubImage2D(target,level,xoffset,yoffset,width,height,format,type,pixels);return}if(pixels){var heap=heapObjectForWebGLType(type);GLctx.texSubImage2D(target,level,xoffset,yoffset,width,height,format,type,heap,toTypedArrayIndex(pixels,heap));return}}var pixelData=pixels?emscriptenWebGLGetTexPixelData(type,format,width,height,pixels,0):null;GLctx.texSubImage2D(target,level,xoffset,yoffset,width,height,format,type,pixelData)};var _emscripten_glTexSubImage3D=(target,level,xoffset,yoffset,zoffset,width,height,depth,format,type,pixels)=>{if(GLctx.currentPixelUnpackBufferBinding){GLctx.texSubImage3D(target,level,xoffset,yoffset,zoffset,width,height,depth,format,type,pixels)}else if(pixels){var heap=heapObjectForWebGLType(type);GLctx.texSubImage3D(target,level,xoffset,yoffset,zoffset,width,height,depth,format,type,heap,toTypedArrayIndex(pixels,heap))}else{GLctx.texSubImage3D(target,level,xoffset,yoffset,zoffset,width,height,depth,format,type,null)}};var _emscripten_glTransformFeedbackVaryings=(program,count,varyings,bufferMode)=>{program=GL.programs[program];var vars=[];for(var i=0;i>2]));GLctx.transformFeedbackVaryings(program,vars,bufferMode)};var _emscripten_glUniform1f=(location,v0)=>{GLctx.uniform1f(webglGetUniformLocation(location),v0)};var miniTempWebGLFloatBuffers=[];var _emscripten_glUniform1fv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform1fv(webglGetUniformLocation(location),HEAPF32,value>>2,count);return}if(count<=288){var view=miniTempWebGLFloatBuffers[count];for(var i=0;i>2]}}else{var view=HEAPF32.subarray(value>>2,value+count*4>>2)}GLctx.uniform1fv(webglGetUniformLocation(location),view)};var _emscripten_glUniform1i=(location,v0)=>{GLctx.uniform1i(webglGetUniformLocation(location),v0)};var miniTempWebGLIntBuffers=[];var _emscripten_glUniform1iv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform1iv(webglGetUniformLocation(location),HEAP32,value>>2,count);return}if(count<=288){var view=miniTempWebGLIntBuffers[count];for(var i=0;i>2]}}else{var view=HEAP32.subarray(value>>2,value+count*4>>2)}GLctx.uniform1iv(webglGetUniformLocation(location),view)};var _emscripten_glUniform1ui=(location,v0)=>{GLctx.uniform1ui(webglGetUniformLocation(location),v0)};var _emscripten_glUniform1uiv=(location,count,value)=>{count&&GLctx.uniform1uiv(webglGetUniformLocation(location),HEAPU32,value>>2,count)};var _emscripten_glUniform2f=(location,v0,v1)=>{GLctx.uniform2f(webglGetUniformLocation(location),v0,v1)};var _emscripten_glUniform2fv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform2fv(webglGetUniformLocation(location),HEAPF32,value>>2,count*2);return}if(count<=144){count*=2;var view=miniTempWebGLFloatBuffers[count];for(var i=0;i>2];view[i+1]=HEAPF32[value+(4*i+4)>>2]}}else{var view=HEAPF32.subarray(value>>2,value+count*8>>2)}GLctx.uniform2fv(webglGetUniformLocation(location),view)};var _emscripten_glUniform2i=(location,v0,v1)=>{GLctx.uniform2i(webglGetUniformLocation(location),v0,v1)};var _emscripten_glUniform2iv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform2iv(webglGetUniformLocation(location),HEAP32,value>>2,count*2);return}if(count<=144){count*=2;var view=miniTempWebGLIntBuffers[count];for(var i=0;i>2];view[i+1]=HEAP32[value+(4*i+4)>>2]}}else{var view=HEAP32.subarray(value>>2,value+count*8>>2)}GLctx.uniform2iv(webglGetUniformLocation(location),view)};var _emscripten_glUniform2ui=(location,v0,v1)=>{GLctx.uniform2ui(webglGetUniformLocation(location),v0,v1)};var _emscripten_glUniform2uiv=(location,count,value)=>{count&&GLctx.uniform2uiv(webglGetUniformLocation(location),HEAPU32,value>>2,count*2)};var _emscripten_glUniform3f=(location,v0,v1,v2)=>{GLctx.uniform3f(webglGetUniformLocation(location),v0,v1,v2)};var _emscripten_glUniform3fv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform3fv(webglGetUniformLocation(location),HEAPF32,value>>2,count*3);return}if(count<=96){count*=3;var view=miniTempWebGLFloatBuffers[count];for(var i=0;i>2];view[i+1]=HEAPF32[value+(4*i+4)>>2];view[i+2]=HEAPF32[value+(4*i+8)>>2]}}else{var view=HEAPF32.subarray(value>>2,value+count*12>>2)}GLctx.uniform3fv(webglGetUniformLocation(location),view)};var _emscripten_glUniform3i=(location,v0,v1,v2)=>{GLctx.uniform3i(webglGetUniformLocation(location),v0,v1,v2)};var _emscripten_glUniform3iv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform3iv(webglGetUniformLocation(location),HEAP32,value>>2,count*3);return}if(count<=96){count*=3;var view=miniTempWebGLIntBuffers[count];for(var i=0;i>2];view[i+1]=HEAP32[value+(4*i+4)>>2];view[i+2]=HEAP32[value+(4*i+8)>>2]}}else{var view=HEAP32.subarray(value>>2,value+count*12>>2)}GLctx.uniform3iv(webglGetUniformLocation(location),view)};var _emscripten_glUniform3ui=(location,v0,v1,v2)=>{GLctx.uniform3ui(webglGetUniformLocation(location),v0,v1,v2)};var _emscripten_glUniform3uiv=(location,count,value)=>{count&&GLctx.uniform3uiv(webglGetUniformLocation(location),HEAPU32,value>>2,count*3)};var _emscripten_glUniform4f=(location,v0,v1,v2,v3)=>{GLctx.uniform4f(webglGetUniformLocation(location),v0,v1,v2,v3)};var _emscripten_glUniform4fv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform4fv(webglGetUniformLocation(location),HEAPF32,value>>2,count*4);return}if(count<=72){var view=miniTempWebGLFloatBuffers[4*count];var heap=HEAPF32;value=value>>2;count*=4;for(var i=0;i>2,value+count*16>>2)}GLctx.uniform4fv(webglGetUniformLocation(location),view)};var _emscripten_glUniform4i=(location,v0,v1,v2,v3)=>{GLctx.uniform4i(webglGetUniformLocation(location),v0,v1,v2,v3)};var _emscripten_glUniform4iv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform4iv(webglGetUniformLocation(location),HEAP32,value>>2,count*4);return}if(count<=72){count*=4;var view=miniTempWebGLIntBuffers[count];for(var i=0;i>2];view[i+1]=HEAP32[value+(4*i+4)>>2];view[i+2]=HEAP32[value+(4*i+8)>>2];view[i+3]=HEAP32[value+(4*i+12)>>2]}}else{var view=HEAP32.subarray(value>>2,value+count*16>>2)}GLctx.uniform4iv(webglGetUniformLocation(location),view)};var _emscripten_glUniform4ui=(location,v0,v1,v2,v3)=>{GLctx.uniform4ui(webglGetUniformLocation(location),v0,v1,v2,v3)};var _emscripten_glUniform4uiv=(location,count,value)=>{count&&GLctx.uniform4uiv(webglGetUniformLocation(location),HEAPU32,value>>2,count*4)};var _emscripten_glUniformBlockBinding=(program,uniformBlockIndex,uniformBlockBinding)=>{program=GL.programs[program];GLctx.uniformBlockBinding(program,uniformBlockIndex,uniformBlockBinding)};var _emscripten_glUniformMatrix2fv=(location,count,transpose,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniformMatrix2fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*4);return}if(count<=72){count*=4;var view=miniTempWebGLFloatBuffers[count];for(var i=0;i>2];view[i+1]=HEAPF32[value+(4*i+4)>>2];view[i+2]=HEAPF32[value+(4*i+8)>>2];view[i+3]=HEAPF32[value+(4*i+12)>>2]}}else{var view=HEAPF32.subarray(value>>2,value+count*16>>2)}GLctx.uniformMatrix2fv(webglGetUniformLocation(location),!!transpose,view)};var _emscripten_glUniformMatrix2x3fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix2x3fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*6)};var _emscripten_glUniformMatrix2x4fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix2x4fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*8)};var _emscripten_glUniformMatrix3fv=(location,count,transpose,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniformMatrix3fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*9);return}if(count<=32){count*=9;var view=miniTempWebGLFloatBuffers[count];for(var i=0;i>2];view[i+1]=HEAPF32[value+(4*i+4)>>2];view[i+2]=HEAPF32[value+(4*i+8)>>2];view[i+3]=HEAPF32[value+(4*i+12)>>2];view[i+4]=HEAPF32[value+(4*i+16)>>2];view[i+5]=HEAPF32[value+(4*i+20)>>2];view[i+6]=HEAPF32[value+(4*i+24)>>2];view[i+7]=HEAPF32[value+(4*i+28)>>2];view[i+8]=HEAPF32[value+(4*i+32)>>2]}}else{var view=HEAPF32.subarray(value>>2,value+count*36>>2)}GLctx.uniformMatrix3fv(webglGetUniformLocation(location),!!transpose,view)};var _emscripten_glUniformMatrix3x2fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix3x2fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*6)};var _emscripten_glUniformMatrix3x4fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix3x4fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*12)};var _emscripten_glUniformMatrix4fv=(location,count,transpose,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniformMatrix4fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*16);return}if(count<=18){var view=miniTempWebGLFloatBuffers[16*count];var heap=HEAPF32;value=value>>2;count*=16;for(var i=0;i>2,value+count*64>>2)}GLctx.uniformMatrix4fv(webglGetUniformLocation(location),!!transpose,view)};var _emscripten_glUniformMatrix4x2fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix4x2fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*8)};var _emscripten_glUniformMatrix4x3fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix4x3fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*12)};var _emscripten_glUseProgram=program=>{program=GL.programs[program];GLctx.useProgram(program);GLctx.currentProgram=program};var _emscripten_glValidateProgram=program=>{GLctx.validateProgram(GL.programs[program])};var _emscripten_glVertexAttrib1f=(x0,x1)=>GLctx.vertexAttrib1f(x0,x1);var _emscripten_glVertexAttrib1fv=(index,v)=>{GLctx.vertexAttrib1f(index,HEAPF32[v>>2])};var _emscripten_glVertexAttrib2f=(x0,x1,x2)=>GLctx.vertexAttrib2f(x0,x1,x2);var _emscripten_glVertexAttrib2fv=(index,v)=>{GLctx.vertexAttrib2f(index,HEAPF32[v>>2],HEAPF32[v+4>>2])};var _emscripten_glVertexAttrib3f=(x0,x1,x2,x3)=>GLctx.vertexAttrib3f(x0,x1,x2,x3);var _emscripten_glVertexAttrib3fv=(index,v)=>{GLctx.vertexAttrib3f(index,HEAPF32[v>>2],HEAPF32[v+4>>2],HEAPF32[v+8>>2])};var _emscripten_glVertexAttrib4f=(x0,x1,x2,x3,x4)=>GLctx.vertexAttrib4f(x0,x1,x2,x3,x4);var _emscripten_glVertexAttrib4fv=(index,v)=>{GLctx.vertexAttrib4f(index,HEAPF32[v>>2],HEAPF32[v+4>>2],HEAPF32[v+8>>2],HEAPF32[v+12>>2])};var _emscripten_glVertexAttribDivisor=(index,divisor)=>{GLctx.vertexAttribDivisor(index,divisor)};var _glVertexAttribDivisor=_emscripten_glVertexAttribDivisor;var _emscripten_glVertexAttribDivisorANGLE=_glVertexAttribDivisor;var _emscripten_glVertexAttribDivisorARB=_glVertexAttribDivisor;var _emscripten_glVertexAttribDivisorEXT=_glVertexAttribDivisor;var _emscripten_glVertexAttribDivisorNV=_glVertexAttribDivisor;var _emscripten_glVertexAttribI4i=(x0,x1,x2,x3,x4)=>GLctx.vertexAttribI4i(x0,x1,x2,x3,x4);var _emscripten_glVertexAttribI4iv=(index,v)=>{GLctx.vertexAttribI4i(index,HEAP32[v>>2],HEAP32[v+4>>2],HEAP32[v+8>>2],HEAP32[v+12>>2])};var _emscripten_glVertexAttribI4ui=(x0,x1,x2,x3,x4)=>GLctx.vertexAttribI4ui(x0,x1,x2,x3,x4);var _emscripten_glVertexAttribI4uiv=(index,v)=>{GLctx.vertexAttribI4ui(index,HEAPU32[v>>2],HEAPU32[v+4>>2],HEAPU32[v+8>>2],HEAPU32[v+12>>2])};var _emscripten_glVertexAttribIPointer=(index,size,type,stride,ptr)=>{GLctx.vertexAttribIPointer(index,size,type,stride,ptr)};var _emscripten_glVertexAttribPointer=(index,size,type,normalized,stride,ptr)=>{GLctx.vertexAttribPointer(index,size,type,!!normalized,stride,ptr)};var _emscripten_glViewport=(x0,x1,x2,x3)=>GLctx.viewport(x0,x1,x2,x3);var _emscripten_glWaitSync=(sync,flags,timeout)=>{timeout=Number(timeout);GLctx.waitSync(GL.syncs[sync],flags,timeout)};var wasmTableMirror=[];var getWasmTableEntry=funcPtr=>{var func=wasmTableMirror[funcPtr];if(!func){wasmTableMirror[funcPtr]=func=wasmTable.get(funcPtr)}return func};var _emscripten_request_animation_frame_loop=(cb,userData)=>{function tick(timeStamp){if(getWasmTableEntry(cb)(timeStamp,userData)){requestAnimationFrame(tick)}}return requestAnimationFrame(tick)};var growMemory=size=>{var oldHeapSize=wasmMemory.buffer.byteLength;var pages=(size-oldHeapSize+65535)/65536|0;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){}};var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignMemory(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}return false};var ENV={};var getExecutableName=()=>thisProgram||"./this.program";var getEnvStrings=()=>{if(!getEnvStrings.strings){var lang=(globalThis.navigator?.language??"C").replace("-","_")+".UTF-8";var env={USER:"web_user",LOGNAME:"web_user",PATH:"/",PWD:"/",HOME:"/home/web_user",LANG:lang,_:getExecutableName()};for(var x in ENV){if(ENV[x]===undefined)delete env[x];else env[x]=ENV[x]}var strings=[];for(var x in env){strings.push(`${x}=${env[x]}`)}getEnvStrings.strings=strings}return getEnvStrings.strings};var _environ_get=(__environ,environ_buf)=>{var bufSize=0;var envp=0;for(var string of getEnvStrings()){var ptr=environ_buf+bufSize;HEAPU32[__environ+envp>>2]=ptr;bufSize+=stringToUTF8(string,ptr,Infinity)+1;envp+=4}return 0};var _environ_sizes_get=(penviron_count,penviron_buf_size)=>{var strings=getEnvStrings();HEAPU32[penviron_count>>2]=strings.length;var bufSize=0;for(var string of strings){bufSize+=lengthBytesUTF8(string)+1}HEAPU32[penviron_buf_size>>2]=bufSize;return 0};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;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];var len=HEAPU32[iov+4>>2];iov+=8;var curr=FS.read(stream,HEAP8,ptr,len,offset);if(curr<0)return-1;ret+=curr;if(curr>2]=num;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}function _fd_read(fd,iov,iovcnt,pnum){try{var stream=SYSCALLS.getStreamFromFD(fd);var num=doReadv(stream,iov,iovcnt);HEAPU32[pnum>>2]=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);try{if(isNaN(offset))return 61;var stream=SYSCALLS.getStreamFromFD(fd);FS.llseek(stream,offset,whence);HEAP64[newOffset>>3]=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];var len=HEAPU32[iov+4>>2];iov+=8;var curr=FS.write(stream,HEAP8,ptr,len,offset);if(curr<0)return-1;ret+=curr;if(curr>2]=num;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var _glGetIntegerv=_emscripten_glGetIntegerv;var _glGetString=_emscripten_glGetString;var _glGetStringi=_emscripten_glGetStringi;var _llvm_eh_typeid_for=type=>type;function _random_get(buffer,size){try{randomFill(HEAPU8.subarray(buffer,buffer+size));return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var handleException=e=>{if(e instanceof ExitStatus||e=="unwind"){return EXITSTATUS}quit_(1,e)};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};FS.createPreloadedFile=FS_createPreloadedFile;FS.preloadFile=FS_preloadFile;FS.staticInit();for(let i=0;i<32;++i)tempFixedLengthArray.push(new Array(i));var miniTempWebGLFloatBuffersStorage=new Float32Array(288);for(var i=0;i<=288;++i){miniTempWebGLFloatBuffers[i]=miniTempWebGLFloatBuffersStorage.subarray(0,i)}var miniTempWebGLIntBuffersStorage=new Int32Array(288);for(var i=0;i<=288;++i){miniTempWebGLIntBuffers[i]=miniTempWebGLIntBuffersStorage.subarray(0,i)}{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["UTF8ToString"]=UTF8ToString;Module["stringToUTF8"]=stringToUTF8;Module["lengthBytesUTF8"]=lengthBytesUTF8;Module["GL"]=GL;var _malloc,_add_font,_add_image,_add_image_with_rid,_allocate,_apply_scene_transactions,_command,_deallocate,_destroy,_devtools_rendering_set_show_fps_meter,_devtools_rendering_set_show_hit_testing,_devtools_rendering_set_show_ruler,_devtools_rendering_set_show_stats,_devtools_rendering_set_show_tiles,_drain_missing_images,_export_node_as,_get_default_fallback_fonts,_get_image_bytes,_get_image_size,_get_node_absolute_bounding_box,_get_node_id_from_point,_get_node_ids_from_envelope,_get_node_ids_from_point,_grida_fonts_analyze_family,_grida_fonts_free,_grida_fonts_parse_font,_grida_markdown_to_html,_grida_svg_optimize,_grida_svg_to_document,_has_missing_fonts,_highlight_strokes,_init,_init_with_backend,_list_available_fonts,_list_missing_fonts,_load_benchmark_scene,_load_dummy_scene,_load_scene_grida,_load_scene_grida1,_loaded_scene_ids,_pointer_move,_redraw,_resize_surface,_resolve_image,_runtime_renderer_set_layer_compositing,_runtime_renderer_set_outline_mode,_runtime_renderer_set_pixel_preview_scale,_runtime_renderer_set_pixel_preview_stable,_runtime_renderer_set_render_policy_flags,_runtime_renderer_set_skip_layout,_set_debug,_set_default_fallback_fonts,_set_main_camera_transform,_set_surface_overlay_config,_set_verbose,_surface_get_cursor,_surface_get_hovered_node,_surface_get_selected_nodes,_surface_pointer_down,_surface_pointer_move,_surface_pointer_up,_surface_set_selection,_switch_scene,_text_edit_command,_text_edit_enter,_text_edit_exit,_text_edit_get_caret_rect,_text_edit_get_selected_html,_text_edit_get_selected_text,_text_edit_get_selection_rects,_text_edit_get_text,_text_edit_ime_cancel,_text_edit_ime_commit,_text_edit_ime_set_preedit,_text_edit_is_active,_text_edit_paste_html,_text_edit_paste_text,_text_edit_pointer_down,_text_edit_pointer_move,_text_edit_pointer_up,_text_edit_redo,_text_edit_set_color,_text_edit_set_font_family,_text_edit_set_font_size,_text_edit_tick,_text_edit_toggle_bold,_text_edit_toggle_italic,_text_edit_toggle_strikethrough,_text_edit_toggle_underline,_text_edit_undo,_tick,_to_vector_network,_toggle_debug,_main,_emscripten_builtin_memalign,_setThrew,__emscripten_tempret_set,__emscripten_stack_restore,__emscripten_stack_alloc,_emscripten_stack_get_current,___cxa_decrement_exception_refcount,___cxa_increment_exception_refcount,___cxa_can_catch,___cxa_get_exception_ptr,memory,__indirect_function_table,wasmMemory,wasmTable;function assignWasmExports(wasmExports){_malloc=wasmExports["Pg"];_add_font=Module["_add_font"]=wasmExports["Rg"];_add_image=Module["_add_image"]=wasmExports["Sg"];_add_image_with_rid=Module["_add_image_with_rid"]=wasmExports["Tg"];_allocate=Module["_allocate"]=wasmExports["Ug"];_apply_scene_transactions=Module["_apply_scene_transactions"]=wasmExports["Vg"];_command=Module["_command"]=wasmExports["Wg"];_deallocate=Module["_deallocate"]=wasmExports["Xg"];_destroy=Module["_destroy"]=wasmExports["Yg"];_devtools_rendering_set_show_fps_meter=Module["_devtools_rendering_set_show_fps_meter"]=wasmExports["Zg"];_devtools_rendering_set_show_hit_testing=Module["_devtools_rendering_set_show_hit_testing"]=wasmExports["_g"];_devtools_rendering_set_show_ruler=Module["_devtools_rendering_set_show_ruler"]=wasmExports["$g"];_devtools_rendering_set_show_stats=Module["_devtools_rendering_set_show_stats"]=wasmExports["ah"];_devtools_rendering_set_show_tiles=Module["_devtools_rendering_set_show_tiles"]=wasmExports["bh"];_drain_missing_images=Module["_drain_missing_images"]=wasmExports["ch"];_export_node_as=Module["_export_node_as"]=wasmExports["dh"];_get_default_fallback_fonts=Module["_get_default_fallback_fonts"]=wasmExports["eh"];_get_image_bytes=Module["_get_image_bytes"]=wasmExports["fh"];_get_image_size=Module["_get_image_size"]=wasmExports["gh"];_get_node_absolute_bounding_box=Module["_get_node_absolute_bounding_box"]=wasmExports["hh"];_get_node_id_from_point=Module["_get_node_id_from_point"]=wasmExports["ih"];_get_node_ids_from_envelope=Module["_get_node_ids_from_envelope"]=wasmExports["jh"];_get_node_ids_from_point=Module["_get_node_ids_from_point"]=wasmExports["kh"];_grida_fonts_analyze_family=Module["_grida_fonts_analyze_family"]=wasmExports["lh"];_grida_fonts_free=Module["_grida_fonts_free"]=wasmExports["mh"];_grida_fonts_parse_font=Module["_grida_fonts_parse_font"]=wasmExports["nh"];_grida_markdown_to_html=Module["_grida_markdown_to_html"]=wasmExports["oh"];_grida_svg_optimize=Module["_grida_svg_optimize"]=wasmExports["ph"];_grida_svg_to_document=Module["_grida_svg_to_document"]=wasmExports["qh"];_has_missing_fonts=Module["_has_missing_fonts"]=wasmExports["rh"];_highlight_strokes=Module["_highlight_strokes"]=wasmExports["sh"];_init=Module["_init"]=wasmExports["th"];_init_with_backend=Module["_init_with_backend"]=wasmExports["uh"];_list_available_fonts=Module["_list_available_fonts"]=wasmExports["vh"];_list_missing_fonts=Module["_list_missing_fonts"]=wasmExports["wh"];_load_benchmark_scene=Module["_load_benchmark_scene"]=wasmExports["xh"];_load_dummy_scene=Module["_load_dummy_scene"]=wasmExports["yh"];_load_scene_grida=Module["_load_scene_grida"]=wasmExports["zh"];_load_scene_grida1=Module["_load_scene_grida1"]=wasmExports["Ah"];_loaded_scene_ids=Module["_loaded_scene_ids"]=wasmExports["Bh"];_pointer_move=Module["_pointer_move"]=wasmExports["Ch"];_redraw=Module["_redraw"]=wasmExports["Dh"];_resize_surface=Module["_resize_surface"]=wasmExports["Eh"];_resolve_image=Module["_resolve_image"]=wasmExports["Fh"];_runtime_renderer_set_layer_compositing=Module["_runtime_renderer_set_layer_compositing"]=wasmExports["Gh"];_runtime_renderer_set_outline_mode=Module["_runtime_renderer_set_outline_mode"]=wasmExports["Hh"];_runtime_renderer_set_pixel_preview_scale=Module["_runtime_renderer_set_pixel_preview_scale"]=wasmExports["Ih"];_runtime_renderer_set_pixel_preview_stable=Module["_runtime_renderer_set_pixel_preview_stable"]=wasmExports["Jh"];_runtime_renderer_set_render_policy_flags=Module["_runtime_renderer_set_render_policy_flags"]=wasmExports["Kh"];_runtime_renderer_set_skip_layout=Module["_runtime_renderer_set_skip_layout"]=wasmExports["Lh"];_set_debug=Module["_set_debug"]=wasmExports["Mh"];_set_default_fallback_fonts=Module["_set_default_fallback_fonts"]=wasmExports["Nh"];_set_main_camera_transform=Module["_set_main_camera_transform"]=wasmExports["Oh"];_set_surface_overlay_config=Module["_set_surface_overlay_config"]=wasmExports["Ph"];_set_verbose=Module["_set_verbose"]=wasmExports["Qh"];_surface_get_cursor=Module["_surface_get_cursor"]=wasmExports["Rh"];_surface_get_hovered_node=Module["_surface_get_hovered_node"]=wasmExports["Sh"];_surface_get_selected_nodes=Module["_surface_get_selected_nodes"]=wasmExports["Th"];_surface_pointer_down=Module["_surface_pointer_down"]=wasmExports["Uh"];_surface_pointer_move=Module["_surface_pointer_move"]=wasmExports["Vh"];_surface_pointer_up=Module["_surface_pointer_up"]=wasmExports["Wh"];_surface_set_selection=Module["_surface_set_selection"]=wasmExports["Xh"];_switch_scene=Module["_switch_scene"]=wasmExports["Yh"];_text_edit_command=Module["_text_edit_command"]=wasmExports["Zh"];_text_edit_enter=Module["_text_edit_enter"]=wasmExports["_h"];_text_edit_exit=Module["_text_edit_exit"]=wasmExports["$h"];_text_edit_get_caret_rect=Module["_text_edit_get_caret_rect"]=wasmExports["ai"];_text_edit_get_selected_html=Module["_text_edit_get_selected_html"]=wasmExports["bi"];_text_edit_get_selected_text=Module["_text_edit_get_selected_text"]=wasmExports["ci"];_text_edit_get_selection_rects=Module["_text_edit_get_selection_rects"]=wasmExports["di"];_text_edit_get_text=Module["_text_edit_get_text"]=wasmExports["ei"];_text_edit_ime_cancel=Module["_text_edit_ime_cancel"]=wasmExports["fi"];_text_edit_ime_commit=Module["_text_edit_ime_commit"]=wasmExports["gi"];_text_edit_ime_set_preedit=Module["_text_edit_ime_set_preedit"]=wasmExports["hi"];_text_edit_is_active=Module["_text_edit_is_active"]=wasmExports["ii"];_text_edit_paste_html=Module["_text_edit_paste_html"]=wasmExports["ji"];_text_edit_paste_text=Module["_text_edit_paste_text"]=wasmExports["ki"];_text_edit_pointer_down=Module["_text_edit_pointer_down"]=wasmExports["li"];_text_edit_pointer_move=Module["_text_edit_pointer_move"]=wasmExports["mi"];_text_edit_pointer_up=Module["_text_edit_pointer_up"]=wasmExports["ni"];_text_edit_redo=Module["_text_edit_redo"]=wasmExports["oi"];_text_edit_set_color=Module["_text_edit_set_color"]=wasmExports["pi"];_text_edit_set_font_family=Module["_text_edit_set_font_family"]=wasmExports["qi"];_text_edit_set_font_size=Module["_text_edit_set_font_size"]=wasmExports["ri"];_text_edit_tick=Module["_text_edit_tick"]=wasmExports["si"];_text_edit_toggle_bold=Module["_text_edit_toggle_bold"]=wasmExports["ti"];_text_edit_toggle_italic=Module["_text_edit_toggle_italic"]=wasmExports["ui"];_text_edit_toggle_strikethrough=Module["_text_edit_toggle_strikethrough"]=wasmExports["vi"];_text_edit_toggle_underline=Module["_text_edit_toggle_underline"]=wasmExports["wi"];_text_edit_undo=Module["_text_edit_undo"]=wasmExports["xi"];_tick=Module["_tick"]=wasmExports["yi"];_to_vector_network=Module["_to_vector_network"]=wasmExports["zi"];_toggle_debug=Module["_toggle_debug"]=wasmExports["Ai"];_main=Module["_main"]=wasmExports["Bi"];_emscripten_builtin_memalign=wasmExports["Ci"];_setThrew=wasmExports["Di"];__emscripten_tempret_set=wasmExports["Ei"];__emscripten_stack_restore=wasmExports["Fi"];__emscripten_stack_alloc=wasmExports["Gi"];_emscripten_stack_get_current=wasmExports["Hi"];___cxa_decrement_exception_refcount=wasmExports["Ii"];___cxa_increment_exception_refcount=wasmExports["Ji"];___cxa_can_catch=wasmExports["Ki"];___cxa_get_exception_ptr=wasmExports["Li"];memory=wasmMemory=wasmExports["Ng"];__indirect_function_table=wasmTable=wasmExports["Qg"]}var wasmImports={G:___cxa_begin_catch,O:___cxa_end_catch,a:___cxa_find_matching_catch_2,n:___cxa_find_matching_catch_3,ia:___cxa_find_matching_catch_4,Da:___cxa_rethrow,I:___cxa_throw,cb:___cxa_uncaught_exceptions,e:___resumeException,Ga:___syscall_fcntl64,ub:___syscall_fstat64,pb:___syscall_getcwd,vb:___syscall_ioctl,qb:___syscall_lstat64,rb:___syscall_newfstatat,Ha:___syscall_openat,sb:___syscall_stat64,yb:__abort_js,eb:__emscripten_throw_longjmp,kb:__gmtime_js,ib:__mmap_js,jb:__munmap_js,Ab:__tzset_js,xb:_clock_time_get,wb:_emscripten_date_now,gb:_emscripten_get_heap_max,zf:_emscripten_glActiveTexture,Af:_emscripten_glAttachShader,ce:_emscripten_glBeginQuery,Yd:_emscripten_glBeginQueryEXT,Dc:_emscripten_glBeginTransformFeedback,Bf:_emscripten_glBindAttribLocation,Cf:_emscripten_glBindBuffer,Ac:_emscripten_glBindBufferBase,Bc:_emscripten_glBindBufferRange,Ae:_emscripten_glBindFramebuffer,Be:_emscripten_glBindRenderbuffer,ie:_emscripten_glBindSampler,Df:_emscripten_glBindTexture,Rb:_emscripten_glBindTransformFeedback,We:_emscripten_glBindVertexArray,Ze:_emscripten_glBindVertexArrayOES,Ef:_emscripten_glBlendColor,Ff:_emscripten_glBlendEquation,Id:_emscripten_glBlendEquationSeparate,Gf:_emscripten_glBlendFunc,Hd:_emscripten_glBlendFuncSeparate,ue:_emscripten_glBlitFramebuffer,Hf:_emscripten_glBufferData,If:_emscripten_glBufferSubData,Ce:_emscripten_glCheckFramebufferStatus,Jf:_emscripten_glClear,ec:_emscripten_glClearBufferfi,fc:_emscripten_glClearBufferfv,hc:_emscripten_glClearBufferiv,gc:_emscripten_glClearBufferuiv,Kf:_emscripten_glClearColor,Gd:_emscripten_glClearDepthf,Lf:_emscripten_glClearStencil,re:_emscripten_glClientWaitSync,Zc:_emscripten_glClipControlEXT,Mf:_emscripten_glColorMask,Nf:_emscripten_glCompileShader,Of:_emscripten_glCompressedTexImage2D,Qc:_emscripten_glCompressedTexImage3D,Pf:_emscripten_glCompressedTexSubImage2D,Pc:_emscripten_glCompressedTexSubImage3D,te:_emscripten_glCopyBufferSubData,Fd:_emscripten_glCopyTexImage2D,Rf:_emscripten_glCopyTexSubImage2D,Rc:_emscripten_glCopyTexSubImage3D,Sf:_emscripten_glCreateProgram,Tf:_emscripten_glCreateShader,Uf:_emscripten_glCullFace,Vf:_emscripten_glDeleteBuffers,De:_emscripten_glDeleteFramebuffers,Wf:_emscripten_glDeleteProgram,de:_emscripten_glDeleteQueries,Zd:_emscripten_glDeleteQueriesEXT,Ee:_emscripten_glDeleteRenderbuffers,je:_emscripten_glDeleteSamplers,Xf:_emscripten_glDeleteShader,se:_emscripten_glDeleteSync,Yf:_emscripten_glDeleteTextures,Qb:_emscripten_glDeleteTransformFeedbacks,Xe:_emscripten_glDeleteVertexArrays,_e:_emscripten_glDeleteVertexArraysOES,Ed:_emscripten_glDepthFunc,Zf:_emscripten_glDepthMask,Dd:_emscripten_glDepthRangef,Cd:_emscripten_glDetachShader,_f:_emscripten_glDisable,$f:_emscripten_glDisableVertexAttribArray,ag:_emscripten_glDrawArrays,Ue:_emscripten_glDrawArraysInstanced,Ld:_emscripten_glDrawArraysInstancedANGLE,Db:_emscripten_glDrawArraysInstancedARB,Re:_emscripten_glDrawArraysInstancedBaseInstanceWEBGL,Wc:_emscripten_glDrawArraysInstancedEXT,Eb:_emscripten_glDrawArraysInstancedNV,Pe:_emscripten_glDrawBuffers,Uc:_emscripten_glDrawBuffersEXT,Md:_emscripten_glDrawBuffersWEBGL,bg:_emscripten_glDrawElements,Ve:_emscripten_glDrawElementsInstanced,Kd:_emscripten_glDrawElementsInstancedANGLE,Bb:_emscripten_glDrawElementsInstancedARB,Se:_emscripten_glDrawElementsInstancedBaseVertexBaseInstanceWEBGL,Cb:_emscripten_glDrawElementsInstancedEXT,Vc:_emscripten_glDrawElementsInstancedNV,Je:_emscripten_glDrawRangeElements,cg:_emscripten_glEnable,dg:_emscripten_glEnableVertexAttribArray,ee:_emscripten_glEndQuery,_d:_emscripten_glEndQueryEXT,Cc:_emscripten_glEndTransformFeedback,oe:_emscripten_glFenceSync,eg:_emscripten_glFinish,fg:_emscripten_glFlush,Fe:_emscripten_glFramebufferRenderbuffer,Ge:_emscripten_glFramebufferTexture2D,Hc:_emscripten_glFramebufferTextureLayer,gg:_emscripten_glFrontFace,hg:_emscripten_glGenBuffers,He:_emscripten_glGenFramebuffers,fe:_emscripten_glGenQueries,$d:_emscripten_glGenQueriesEXT,Ie:_emscripten_glGenRenderbuffers,ke:_emscripten_glGenSamplers,ig:_emscripten_glGenTextures,Pb:_emscripten_glGenTransformFeedbacks,Te:_emscripten_glGenVertexArrays,$e:_emscripten_glGenVertexArraysOES,we:_emscripten_glGenerateMipmap,Bd:_emscripten_glGetActiveAttrib,Ad:_emscripten_glGetActiveUniform,$b:_emscripten_glGetActiveUniformBlockName,ac:_emscripten_glGetActiveUniformBlockiv,cc:_emscripten_glGetActiveUniformsiv,zd:_emscripten_glGetAttachedShaders,yd:_emscripten_glGetAttribLocation,xd:_emscripten_glGetBooleanv,Wb:_emscripten_glGetBufferParameteri64v,jg:_emscripten_glGetBufferParameteriv,kg:_emscripten_glGetError,lg:_emscripten_glGetFloatv,qc:_emscripten_glGetFragDataLocation,xe:_emscripten_glGetFramebufferAttachmentParameteriv,Xb:_emscripten_glGetInteger64i_v,Zb:_emscripten_glGetInteger64v,Ec:_emscripten_glGetIntegeri_v,mg:_emscripten_glGetIntegerv,Hb:_emscripten_glGetInternalformativ,Lb:_emscripten_glGetProgramBinary,ng:_emscripten_glGetProgramInfoLog,og:_emscripten_glGetProgramiv,Vd:_emscripten_glGetQueryObjecti64vEXT,Od:_emscripten_glGetQueryObjectivEXT,Wd:_emscripten_glGetQueryObjectui64vEXT,ge:_emscripten_glGetQueryObjectuiv,ae:_emscripten_glGetQueryObjectuivEXT,he:_emscripten_glGetQueryiv,be:_emscripten_glGetQueryivEXT,ye:_emscripten_glGetRenderbufferParameteriv,Sb:_emscripten_glGetSamplerParameterfv,Tb:_emscripten_glGetSamplerParameteriv,pg:_emscripten_glGetShaderInfoLog,Sd:_emscripten_glGetShaderPrecisionFormat,wd:_emscripten_glGetShaderSource,qg:_emscripten_glGetShaderiv,rg:_emscripten_glGetString,Ye:_emscripten_glGetStringi,Yb:_emscripten_glGetSynciv,vd:_emscripten_glGetTexParameterfv,ud:_emscripten_glGetTexParameteriv,yc:_emscripten_glGetTransformFeedbackVarying,bc:_emscripten_glGetUniformBlockIndex,dc:_emscripten_glGetUniformIndices,sg:_emscripten_glGetUniformLocation,td:_emscripten_glGetUniformfv,sd:_emscripten_glGetUniformiv,rc:_emscripten_glGetUniformuiv,xc:_emscripten_glGetVertexAttribIiv,wc:_emscripten_glGetVertexAttribIuiv,pd:_emscripten_glGetVertexAttribPointerv,rd:_emscripten_glGetVertexAttribfv,qd:_emscripten_glGetVertexAttribiv,od:_emscripten_glHint,Td:_emscripten_glInvalidateFramebuffer,Ud:_emscripten_glInvalidateSubFramebuffer,nd:_emscripten_glIsBuffer,md:_emscripten_glIsEnabled,ld:_emscripten_glIsFramebuffer,kd:_emscripten_glIsProgram,Oc:_emscripten_glIsQuery,Pd:_emscripten_glIsQueryEXT,jd:_emscripten_glIsRenderbuffer,Vb:_emscripten_glIsSampler,id:_emscripten_glIsShader,pe:_emscripten_glIsSync,tg:_emscripten_glIsTexture,Ob:_emscripten_glIsTransformFeedback,Fc:_emscripten_glIsVertexArray,Nd:_emscripten_glIsVertexArrayOES,ug:_emscripten_glLineWidth,vg:_emscripten_glLinkProgram,Ne:_emscripten_glMultiDrawArraysInstancedBaseInstanceWEBGL,Oe:_emscripten_glMultiDrawElementsInstancedBaseVertexBaseInstanceWEBGL,Nb:_emscripten_glPauseTransformFeedback,wg:_emscripten_glPixelStorei,Yc:_emscripten_glPolygonModeWEBGL,hd:_emscripten_glPolygonOffset,_c:_emscripten_glPolygonOffsetClampEXT,Kb:_emscripten_glProgramBinary,Jb:_emscripten_glProgramParameteri,Xd:_emscripten_glQueryCounterEXT,Qe:_emscripten_glReadBuffer,xg:_emscripten_glReadPixels,gd:_emscripten_glReleaseShaderCompiler,ze:_emscripten_glRenderbufferStorage,ve:_emscripten_glRenderbufferStorageMultisample,Mb:_emscripten_glResumeTransformFeedback,fd:_emscripten_glSampleCoverage,le:_emscripten_glSamplerParameterf,Ub:_emscripten_glSamplerParameterfv,me:_emscripten_glSamplerParameteri,ne:_emscripten_glSamplerParameteriv,yg:_emscripten_glScissor,ed:_emscripten_glShaderBinary,zg:_emscripten_glShaderSource,Ag:_emscripten_glStencilFunc,Bg:_emscripten_glStencilFuncSeparate,Cg:_emscripten_glStencilMask,Dg:_emscripten_glStencilMaskSeparate,Eg:_emscripten_glStencilOp,Fg:_emscripten_glStencilOpSeparate,Gg:_emscripten_glTexImage2D,Tc:_emscripten_glTexImage3D,Hg:_emscripten_glTexParameterf,Ig:_emscripten_glTexParameterfv,Jg:_emscripten_glTexParameteri,Kg:_emscripten_glTexParameteriv,Ke:_emscripten_glTexStorage2D,Ib:_emscripten_glTexStorage3D,Lg:_emscripten_glTexSubImage2D,Sc:_emscripten_glTexSubImage3D,zc:_emscripten_glTransformFeedbackVaryings,Mg:_emscripten_glUniform1f,Na:_emscripten_glUniform1fv,vf:_emscripten_glUniform1i,wf:_emscripten_glUniform1iv,pc:_emscripten_glUniform1ui,lc:_emscripten_glUniform1uiv,xf:_emscripten_glUniform2f,yf:_emscripten_glUniform2fv,uf:_emscripten_glUniform2i,tf:_emscripten_glUniform2iv,oc:_emscripten_glUniform2ui,kc:_emscripten_glUniform2uiv,sf:_emscripten_glUniform3f,rf:_emscripten_glUniform3fv,qf:_emscripten_glUniform3i,pf:_emscripten_glUniform3iv,nc:_emscripten_glUniform3ui,jc:_emscripten_glUniform3uiv,of:_emscripten_glUniform4f,nf:_emscripten_glUniform4fv,af:_emscripten_glUniform4i,bf:_emscripten_glUniform4iv,mc:_emscripten_glUniform4ui,ic:_emscripten_glUniform4uiv,_b:_emscripten_glUniformBlockBinding,cf:_emscripten_glUniformMatrix2fv,Nc:_emscripten_glUniformMatrix2x3fv,Lc:_emscripten_glUniformMatrix2x4fv,df:_emscripten_glUniformMatrix3fv,Mc:_emscripten_glUniformMatrix3x2fv,Jc:_emscripten_glUniformMatrix3x4fv,ef:_emscripten_glUniformMatrix4fv,Kc:_emscripten_glUniformMatrix4x2fv,Ic:_emscripten_glUniformMatrix4x3fv,ff:_emscripten_glUseProgram,dd:_emscripten_glValidateProgram,gf:_emscripten_glVertexAttrib1f,cd:_emscripten_glVertexAttrib1fv,bd:_emscripten_glVertexAttrib2f,hf:_emscripten_glVertexAttrib2fv,ad:_emscripten_glVertexAttrib3f,jf:_emscripten_glVertexAttrib3fv,$c:_emscripten_glVertexAttrib4f,kf:_emscripten_glVertexAttrib4fv,Le:_emscripten_glVertexAttribDivisor,Jd:_emscripten_glVertexAttribDivisorANGLE,Fb:_emscripten_glVertexAttribDivisorARB,Xc:_emscripten_glVertexAttribDivisorEXT,Gb:_emscripten_glVertexAttribDivisorNV,vc:_emscripten_glVertexAttribI4i,tc:_emscripten_glVertexAttribI4iv,uc:_emscripten_glVertexAttribI4ui,sc:_emscripten_glVertexAttribI4uiv,Me:_emscripten_glVertexAttribIPointer,lf:_emscripten_glVertexAttribPointer,mf:_emscripten_glViewport,qe:_emscripten_glWaitSync,Ya:_emscripten_request_animation_frame_loop,fb:_emscripten_resize_heap,mb:_environ_get,nb:_environ_sizes_get,Qa:_exit,na:_fd_close,hb:_fd_pread,Fa:_fd_read,lb:_fd_seek,ma:_fd_write,Oa:_glGetIntegerv,qa:_glGetString,Pa:_glGetStringi,Qd:invoke_dd,Rd:invoke_dddd,Ba:invoke_diii,Ta:invoke_fdiiii,Sa:invoke_fdiiiii,Ra:invoke_fii,Ca:invoke_fiii,t:invoke_fiiidi,U:invoke_fiiif,u:invoke_fiiiidi,s:invoke_i,j:invoke_ii,H:invoke_iif,$a:invoke_iiffi,sa:invoke_iiffiii,f:invoke_iii,ua:invoke_iiifi,g:invoke_iiii,T:invoke_iiiiff,l:invoke_iiiii,bb:invoke_iiiiid,z:invoke_iiiiii,A:invoke_iiiiiii,F:invoke_iiiiiiii,q:invoke_iiiiiiiii,ra:invoke_iiiiiiiiii,ea:invoke_iiiiiiiiiiii,pa:invoke_iiiiiiiiiiiifiii,W:invoke_iij,db:invoke_j,ja:invoke_ji,r:invoke_jiii,fa:invoke_jiiii,Z:invoke_jiijj,K:invoke_jjji,k:invoke_v,Qf:invoke_vff,b:invoke_vi,Q:invoke_vid,S:invoke_vif,v:invoke_viff,E:invoke_viffff,aa:invoke_vifffff,Ua:invoke_viffffff,D:invoke_viffi,ka:invoke_viffiiiiiii,c:invoke_vii,Xa:invoke_viidii,P:invoke_viif,x:invoke_viiff,Ka:invoke_viiffii,ba:invoke_viifi,va:invoke_viififii,y:invoke_viifiiifi,d:invoke_viii,J:invoke_viiif,xa:invoke_viiiff,C:invoke_viiiffi,L:invoke_viiiffiffii,M:invoke_viiififiiiiiiiiiiii,i:invoke_viiii,Wa:invoke_viiiidididii,X:invoke_viiiif,ya:invoke_viiiiff,Aa:invoke_viiiiffi,wa:invoke_viiiifi,h:invoke_viiiii,Va:invoke_viiiiiffiii,ab:invoke_viiiiifi,m:invoke_viiiiii,Ja:invoke_viiiiiiff,p:invoke_viiiiiii,V:invoke_viiiiiiii,$:invoke_viiiiiiiii,N:invoke_viiiiiiiiii,zb:invoke_viiiiiiiiiifii,ta:invoke_viiiiiiiiiii,da:invoke_viiiiiiiiiiiiiii,Ma:invoke_viiiiiiji,R:invoke_viiij,B:invoke_viiijii,_:invoke_viij,o:invoke_viiji,Ia:invoke_viijiffi,ha:invoke_viijii,ca:invoke_viijiiiif,La:invoke_viijiiiii,la:invoke_viijj,Y:invoke_viji,Za:invoke_vijififi,w:invoke_vijii,Ea:invoke_vijiifi,_a:invoke_vijiififi,za:invoke_vijiii,tb:invoke_vijijjiii,ga:invoke_vijjjj,Gc:invoke_vjii,oa:_llvm_eh_typeid_for,ob:_random_get};function invoke_vi(index,a1){var sp=stackSave();try{getWasmTableEntry(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 getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vii(index,a1,a2){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_ii(index,a1){var sp=stackSave();try{return getWasmTableEntry(index)(a1)}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 getWasmTableEntry(index)(a1,a2,a3)}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{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viii(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(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{getWasmTableEntry(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 getWasmTableEntry(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 getWasmTableEntry(index)(a1)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0);return 0n}}function invoke_v(index){var sp=stackSave();try{getWasmTableEntry(index)()}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiij(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vff(index,a1,a2){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viij(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiji(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiff(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_jiijj(index,a1,a2,a3,a4){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0);return 0n}}function invoke_iiiiiii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viijiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiji(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vid(index,a1,a2){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viji(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiif(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiff(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiiiiiiifiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vif(index,a1,a2){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viffi(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vjii(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiffii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiff(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vijii(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viijiffi(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}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{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiiiifii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vijijjiii(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vijiifi(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viifiiifi(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiffi(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiifi(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vijiii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_i(index){var sp=stackSave();try{return getWasmTableEntry(index)()}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{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiififiiiiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viijiiiif(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiffiffii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10){var sp=stackSave();try{getWasmTableEntry(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_viiiiff(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiif(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiff(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiffi(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiifi(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiffi(index,a1,a2,a3,a4){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viififii(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_jiii(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0);return 0n}}function invoke_vijiififi(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vijififi(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iij(index,a1,a2){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viijj(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_jjji(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0);return 0n}}function invoke_iiifi(index,a1,a2,a3,a4){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iif(index,a1,a2){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viifi(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viif(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3)}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 getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiffiii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viff(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vifffff(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fiiidi(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viidii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiidididii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fiiiidi(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiffiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10){var sp=stackSave();try{getWasmTableEntry(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_viiijii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viffff(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viffffff(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9){var sp=stackSave();try{getWasmTableEntry(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_fiiif(index,a1,a2,a3,a4){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fdiiii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fdiiiii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fii(index,a1,a2){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viffiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10){var sp=stackSave();try{getWasmTableEntry(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_viiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10){var sp=stackSave();try{getWasmTableEntry(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_iiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9){var sp=stackSave();try{return getWasmTableEntry(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_dddd(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_dd(index,a1){var sp=stackSave();try{return getWasmTableEntry(index)(a1)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vijjjj(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_j(index){var sp=stackSave();try{return getWasmTableEntry(index)()}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0);return 0n}}function invoke_iiiiid(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_jiiii(index,a1,a2,a3,a4){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0);return 0n}}function invoke_fiii(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_diii(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function callMain(args=[]){var entryFunction=_main;args.unshift(thisProgram);var argc=args.length;var argv=stackAlloc((argc+1)*4);var argv_ptr=argv;for(var arg of args){HEAPU32[argv_ptr>>2]=stringToUTF8OnStack(arg);argv_ptr+=4}HEAPU32[argv_ptr>>2]=0;try{var ret=entryFunction(argc,argv);exitJS(ret,true);return ret}catch(e){return handleException(e)}}function run(args=arguments_){if(runDependencies>0){dependenciesFulfilled=run;return}preRun();if(runDependencies>0){dependenciesFulfilled=run;return}function doRun(){Module["calledRun"]=true;if(ABORT)return;initRuntime();preMain();readyPromiseResolve?.(Module);Module["onRuntimeInitialized"]?.();var noInitialRun=Module["noInitialRun"]||false;if(!noInitialRun)callMain(args);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=createGridaCanvas;module.exports.default=createGridaCanvas}else if(typeof define==="function"&&define["amd"])define([],()=>createGridaCanvas); diff --git a/crates/grida-canvas-wasm/lib/bin/grida_canvas_wasm.wasm b/crates/grida-canvas-wasm/lib/bin/grida_canvas_wasm.wasm index 33ce6f67f6..1cce264521 100755 --- a/crates/grida-canvas-wasm/lib/bin/grida_canvas_wasm.wasm +++ b/crates/grida-canvas-wasm/lib/bin/grida_canvas_wasm.wasm @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:64dc52d48723cf9eff216754c3cb024ea27ef810bd016d21d560e42cd82e951b -size 13197254 +oid sha256:fce3f7c0ac424db1867c19a5c58f76fc1c567b0dddcb9048aff2f8e653d74a2b +size 13245926 diff --git a/crates/grida-canvas-wasm/lib/modules/canvas-bindings.d.ts b/crates/grida-canvas-wasm/lib/modules/canvas-bindings.d.ts index 03cea3272d..1023d08417 100644 --- a/crates/grida-canvas-wasm/lib/modules/canvas-bindings.d.ts +++ b/crates/grida-canvas-wasm/lib/modules/canvas-bindings.d.ts @@ -51,6 +51,7 @@ declare namespace canvas { ptr: number, len: number ): void; + _loaded_scene_ids(state: GridaCanvasApplicationPtr): Ptr; _drain_missing_images(state: GridaCanvasApplicationPtr): Ptr; _resolve_image( state: GridaCanvasApplicationPtr, diff --git a/crates/grida-canvas-wasm/lib/modules/canvas.ts b/crates/grida-canvas-wasm/lib/modules/canvas.ts index 38a625b71a..2a66266076 100644 --- a/crates/grida-canvas-wasm/lib/modules/canvas.ts +++ b/crates/grida-canvas-wasm/lib/modules/canvas.ts @@ -195,6 +195,19 @@ export class Scene { this._free_string(ptr, len); } + /** + * Return the IDs of all scenes decoded by the last `loadSceneGrida` call. + */ + loadedSceneIds(): string[] { + this._assertAlive(); + const outptr = this.module._loaded_scene_ids(this.appptr); + if (outptr === 0) { + return []; + } + const str = ffi.readLenPrefixedString(this.module, outptr); + return JSON.parse(str) as string[]; + } + /** * Returns image refs that were needed during the last render but not found. * Only returns refs not yet reported in a previous call. diff --git a/crates/grida-canvas-wasm/lib/modules/ffi.ts b/crates/grida-canvas-wasm/lib/modules/ffi.ts index 987a188db4..b977d75d84 100644 --- a/crates/grida-canvas-wasm/lib/modules/ffi.ts +++ b/crates/grida-canvas-wasm/lib/modules/ffi.ts @@ -29,6 +29,8 @@ export namespace ffi { ): [ptr: number, len: number] { const len = bytes.length; const ptr = module._allocate(len); + // Re-read HEAPU8 after _allocate — if WASM memory grew during + // allocation, the old Uint8Array view is detached. module.HEAPU8.set(bytes, ptr); return [ptr, len]; } diff --git a/crates/grida-canvas-wasm/package.json b/crates/grida-canvas-wasm/package.json index db08cf0fff..908d9b0f49 100644 --- a/crates/grida-canvas-wasm/package.json +++ b/crates/grida-canvas-wasm/package.json @@ -1,6 +1,6 @@ { "name": "@grida/canvas-wasm", - "version": "0.91.0-canary.12", + "version": "0.91.0-canary.13", "private": false, "description": "WASM bindings for Grida Canvas", "keywords": [ @@ -23,7 +23,7 @@ "build": "tsup", "dev": "tsup --watch", "prepack": "just build", - "prepublishOnly": "[ $(du -sk lib 2>/dev/null | cut -f1) -lt 15360 ]", + "prepublishOnly": "[ $(du -sk dist 2>/dev/null | cut -f1) -lt 15360 ]", "serve": "serve -p 4020", "test": "vitest run", "typecheck": "tsc --noEmit" diff --git a/crates/grida-canvas-wasm/src/wasm_application.rs b/crates/grida-canvas-wasm/src/wasm_application.rs index 3b1a73fd6e..f5dd969f86 100644 --- a/crates/grida-canvas-wasm/src/wasm_application.rs +++ b/crates/grida-canvas-wasm/src/wasm_application.rs @@ -180,6 +180,22 @@ pub unsafe extern "C" fn switch_scene( } } +#[no_mangle] +/// js::_loaded_scene_ids +/// Returns a len-prefixed JSON array of scene ID strings, or null if empty. +pub unsafe extern "C" fn loaded_scene_ids(app: *mut UnknownTargetApplication) -> *const u8 { + if let Some(app) = app.as_ref() { + let ids = app.loaded_scene_ids(); + if ids.is_empty() { + return std::ptr::null(); + } + if let Ok(json) = serde_json::to_string(&ids) { + return alloc_len_prefixed(json.as_bytes()); + } + } + std::ptr::null() +} + #[no_mangle] /// js::_drain_missing_images /// Returns a len-prefixed JSON array of missing image ref strings, or null if empty. diff --git a/crates/grida-canvas/Cargo.toml b/crates/grida-canvas/Cargo.toml index 732ccf6d7b..4fa3bdfbfb 100644 --- a/crates/grida-canvas/Cargo.toml +++ b/crates/grida-canvas/Cargo.toml @@ -33,6 +33,8 @@ rstar = "0.12" # core resource hashing seahash = "4.1.0" +# fast non-cryptographic hasher for hot-path HashMaps +rustc-hash = "2" # layout engine taffy = "0.9.2" # svg parsing @@ -63,6 +65,7 @@ tokio = { version = "1", features = ["macros", "rt-multi-thread", "time"] } [features] default = [] web = [] +perf = [] native-clock-tick = [] native-gl-context = ["dep:glutin", "dep:raw-window-handle"] diff --git a/crates/grida-canvas/examples/tool_io_grida.rs b/crates/grida-canvas/examples/tool_io_grida.rs index f2dc30aeec..7afe666f49 100644 --- a/crates/grida-canvas/examples/tool_io_grida.rs +++ b/crates/grida-canvas/examples/tool_io_grida.rs @@ -296,7 +296,7 @@ fn print_scene_stats(scene: &Scene, id_map: &HashMap, verbose: b // Node type breakdown let mut type_counts: BTreeMap<&str, usize> = BTreeMap::new(); for (node_id, node) in graph.nodes_iter() { - if !reachable.contains(node_id) { + if !reachable.contains(&node_id) { continue; } let label = classify_node(node); @@ -371,19 +371,19 @@ fn run_layout_check(scene: &Scene, id_map: &HashMap) -> bool { let mut missing: Vec<(NodeId, String, Vec)> = Vec::new(); for (node_id, node) in graph.nodes_iter() { - if !reachable.contains(node_id) { + if !reachable.contains(&node_id) { continue; } if let Node::Container(_) = node { - if layout_result.get(node_id).is_none() { + if layout_result.get(&node_id).is_none() { let string_id = id_map - .get(node_id) + .get(&node_id) .cloned() .unwrap_or_else(|| format!("{:?}", node_id)); - let mut ancestor_ids: Vec = graph.ancestors(node_id).unwrap_or_default(); + let mut ancestor_ids: Vec = graph.ancestors(&node_id).unwrap_or_default(); ancestor_ids.reverse(); - ancestor_ids.push(*node_id); + ancestor_ids.push(node_id); let path: Vec = ancestor_ids .iter() .map(|id| { @@ -394,7 +394,7 @@ fn run_layout_check(scene: &Scene, id_map: &HashMap) -> bool { }) .collect(); - missing.push((*node_id, string_id, path)); + missing.push((node_id, string_id, path)); } } } diff --git a/crates/grida-canvas/src/cache/atlas/atlas.rs b/crates/grida-canvas/src/cache/atlas/atlas.rs index cbc5968cbd..21b2826978 100644 --- a/crates/grida-canvas/src/cache/atlas/atlas.rs +++ b/crates/grida-canvas/src/cache/atlas/atlas.rs @@ -12,7 +12,7 @@ use super::packing::{ShelfPacker, Slot, SlotId}; use crate::node::schema::NodeId; use skia_safe::{Canvas, Image, Rect, Surface}; -use std::collections::HashMap; +use crate::cache::fast_hash::{new_node_id_map, NodeIdHashMap}; /// A single atlas page. /// @@ -26,9 +26,9 @@ pub struct AtlasPage { /// Shelf packer managing slot allocation. packer: ShelfPacker, /// Map from slot ID to the node that occupies it. - slot_to_node: HashMap, + slot_to_node: NodeIdHashMap, /// Map from node ID to its allocated slot. - node_to_slot: HashMap, + node_to_slot: NodeIdHashMap, /// Whether the surface has been modified since the last snapshot. dirty: bool, /// Page index (for multi-page atlas sets). @@ -70,8 +70,8 @@ impl AtlasPage { surface, image: None, packer: ShelfPacker::new(w, h), - slot_to_node: HashMap::new(), - node_to_slot: HashMap::new(), + slot_to_node: new_node_id_map(), + node_to_slot: new_node_id_map(), dirty: false, page_index, } diff --git a/crates/grida-canvas/src/cache/atlas/atlas_set.rs b/crates/grida-canvas/src/cache/atlas/atlas_set.rs index 84ae1342e1..46b03579f0 100644 --- a/crates/grida-canvas/src/cache/atlas/atlas_set.rs +++ b/crates/grida-canvas/src/cache/atlas/atlas_set.rs @@ -7,7 +7,7 @@ use super::atlas::{AtlasAllocation, AtlasPage}; use crate::node::schema::NodeId; use skia_safe::{Image, Surface}; -use std::collections::HashMap; +use crate::cache::fast_hash::{new_node_id_map, NodeIdHashMap}; /// Configuration for an atlas set. #[derive(Debug, Clone, Copy)] @@ -54,7 +54,7 @@ pub struct AtlasSet { config: AtlasSetConfig, pages: Vec, /// Map from node ID to the page index it's allocated on. - node_page: HashMap, + node_page: NodeIdHashMap, } impl AtlasSet { @@ -65,7 +65,7 @@ impl AtlasSet { Self { config, pages: Vec::new(), - node_page: HashMap::new(), + node_page: new_node_id_map(), } } diff --git a/crates/grida-canvas/src/cache/compositor/cache.rs b/crates/grida-canvas/src/cache/compositor/cache.rs index a45578e20c..2cd83a2878 100644 --- a/crates/grida-canvas/src/cache/compositor/cache.rs +++ b/crates/grida-canvas/src/cache/compositor/cache.rs @@ -8,7 +8,7 @@ use crate::cg::prelude::LayerBlendMode; use crate::node::schema::NodeId; use math2::rect::Rectangle; use skia_safe::Image; -use std::collections::HashMap; +use crate::cache::fast_hash::{new_node_id_map, NodeIdHashMap}; use std::rc::Rc; /// Where a promoted node's cached pixels live. @@ -108,7 +108,7 @@ pub struct LayerImageCacheStats { #[derive(Debug, Clone)] pub struct LayerImageCache { /// Promoted node entries, keyed by node ID. - images: HashMap, + images: NodeIdHashMap, /// Maximum memory budget in bytes (default: 128 MB). /// Only individual (non-atlas) images count against this budget. memory_budget: usize, @@ -131,7 +131,7 @@ impl LayerImageCache { /// Create a new layer image cache with the given memory budget. pub fn new(memory_budget: usize) -> Self { Self { - images: HashMap::new(), + images: new_node_id_map(), memory_budget, memory_used: 0, frame_counter: 0, diff --git a/crates/grida-canvas/src/cache/fast_hash.rs b/crates/grida-canvas/src/cache/fast_hash.rs new file mode 100644 index 0000000000..d68c1061c1 --- /dev/null +++ b/crates/grida-canvas/src/cache/fast_hash.rs @@ -0,0 +1,277 @@ +//! Fast hasher for u64-keyed HashMaps in the rendering hot path. +//! +//! The default `HashMap` uses SipHash-1-3, which provides DoS resistance +//! at ~25ns per hash. For trusted-input rendering caches keyed by `NodeId` +//! (u64), we can use a much faster multiplicative hash (~3ns) since there +//! is no untrusted input to defend against. +//! +//! This is the same approach as `rustc-hash` (FxHash): multiply by a large +//! odd constant to scatter bits, then use the result directly as the hash. + +use std::collections::HashMap; +use std::hash::{BuildHasher, Hasher}; + +/// A fast hasher for integer keys. +/// +/// Uses a single multiply to distribute bits. Suitable for u64 keys +/// (NodeId) and (u64, u64) tuple keys used in the picture/geometry/ +/// compositor caches. +#[derive(Default)] +pub struct NodeIdHasher { + hash: u64, +} + +impl Hasher for NodeIdHasher { + #[inline] + fn write(&mut self, bytes: &[u8]) { + // For arbitrary byte sequences, use a simple FNV-like combine. + for &b in bytes { + self.hash = self.hash.wrapping_mul(0x100000001b3).wrapping_add(b as u64); + } + } + + #[inline] + fn write_u64(&mut self, i: u64) { + // FxHash: XOR-fold then multiply by a large odd constant. + // This is the primary fast path for NodeId (u64) keys. + self.hash = self.hash ^ i; + self.hash = self.hash.wrapping_mul(0x517cc1b727220a95); + } + + #[inline] + fn finish(&self) -> u64 { + self.hash + } +} + +/// BuildHasher that produces `NodeIdHasher` instances. +#[derive(Clone, Default)] +pub struct NodeIdBuildHasher; + +impl BuildHasher for NodeIdBuildHasher { + type Hasher = NodeIdHasher; + + #[inline] + fn build_hasher(&self) -> NodeIdHasher { + NodeIdHasher::default() + } +} + +/// A HashMap using the fast NodeId hasher. +/// +/// Use this for caches keyed by `NodeId` (u64) or `(NodeId, u64)` tuples +/// where keys come from trusted internal sources (no DoS risk). +pub type NodeIdHashMap = HashMap; + +/// Create a new empty NodeIdHashMap. +#[inline] +pub fn new_node_id_map() -> NodeIdHashMap { + HashMap::with_hasher(NodeIdBuildHasher) +} + +/// Create a new NodeIdHashMap with the specified capacity. +#[inline] +pub fn new_node_id_map_with_capacity(capacity: usize) -> NodeIdHashMap { + HashMap::with_capacity_and_hasher(capacity, NodeIdBuildHasher) +} + +// --------------------------------------------------------------------------- +// DenseNodeMap — Vec-indexed storage for sequential NodeId keys +// --------------------------------------------------------------------------- + +use crate::node::id::NodeId; + +/// A dense, Vec-backed map keyed by `NodeId` (sequential u64 starting at 0). +/// +/// Replaces `HashMap` in hot paths where NodeIds are generated by +/// a counter. Provides O(1) access with zero hashing and excellent cache +/// locality — critical for WASM where HashMap overhead is 10–30× native. +#[derive(Debug, Clone)] +pub struct DenseNodeMap { + slots: Vec>, + len: usize, +} + +impl DenseNodeMap { + #[inline] + pub fn new() -> Self { + Self { + slots: Vec::new(), + len: 0, + } + } + + #[inline] + pub fn with_capacity(capacity: usize) -> Self { + let mut slots = Vec::with_capacity(capacity); + slots.resize_with(capacity, || None); + Self { slots, len: 0 } + } + + #[inline] + pub fn insert(&mut self, id: NodeId, value: V) -> Option { + let idx = id as usize; + if idx >= self.slots.len() { + self.slots.resize_with(idx + 1, || None); + } + let old = self.slots[idx].take(); + self.slots[idx] = Some(value); + if old.is_none() { + self.len += 1; + } + old + } + + #[inline] + pub fn get(&self, id: &NodeId) -> Option<&V> { + self.slots.get(*id as usize).and_then(|s| s.as_ref()) + } + + #[inline] + pub fn get_mut(&mut self, id: &NodeId) -> Option<&mut V> { + self.slots.get_mut(*id as usize).and_then(|s| s.as_mut()) + } + + #[inline] + pub fn remove(&mut self, id: &NodeId) -> Option { + let idx = *id as usize; + if idx < self.slots.len() { + let old = self.slots[idx].take(); + if old.is_some() { + self.len -= 1; + } + old + } else { + None + } + } + + #[inline] + pub fn contains_key(&self, id: &NodeId) -> bool { + self.get(id).is_some() + } + + #[inline] + pub fn len(&self) -> usize { + self.len + } + + #[inline] + pub fn is_empty(&self) -> bool { + self.len == 0 + } + + pub fn clear(&mut self) { + for slot in self.slots.iter_mut() { + *slot = None; + } + self.len = 0; + } + + /// Pre-allocate storage so that IDs up to `capacity - 1` can be inserted + /// without reallocation. + pub fn reserve(&mut self, capacity: usize) { + if capacity > self.slots.len() { + self.slots.resize_with(capacity, || None); + } + } + + /// Iterate over occupied entries as `(NodeId, &V)`. + pub fn iter(&self) -> DenseNodeMapIter<'_, V> { + DenseNodeMapIter { + inner: self.slots.iter().enumerate(), + } + } + + /// Iterate over occupied entries as `(NodeId, &mut V)`. + pub fn iter_mut(&mut self) -> DenseNodeMapIterMut<'_, V> { + DenseNodeMapIterMut { + inner: self.slots.iter_mut().enumerate(), + } + } + + /// Retain only entries satisfying a predicate. + pub fn retain bool>(&mut self, mut f: F) { + for (idx, slot) in self.slots.iter_mut().enumerate() { + if let Some(ref mut v) = slot { + if !f(idx as NodeId, v) { + *slot = None; + self.len -= 1; + } + } + } + } + + /// Get the allocated capacity (max NodeId + 1 that fits without realloc). + #[inline] + pub fn capacity(&self) -> usize { + self.slots.len() + } +} + +impl Default for DenseNodeMap { + fn default() -> Self { + Self::new() + } +} + +/// Iterator over `(NodeId, &V)` pairs in a `DenseNodeMap`. +pub struct DenseNodeMapIter<'a, V> { + inner: std::iter::Enumerate>>, +} + +impl<'a, V> Iterator for DenseNodeMapIter<'a, V> { + type Item = (NodeId, &'a V); + + #[inline] + fn next(&mut self) -> Option { + loop { + match self.inner.next() { + Some((idx, Some(v))) => return Some((idx as NodeId, v)), + Some((_, None)) => continue, + None => return None, + } + } + } +} + +/// Iterator over `(NodeId, &mut V)` pairs in a `DenseNodeMap`. +pub struct DenseNodeMapIterMut<'a, V> { + inner: std::iter::Enumerate>>, +} + +impl<'a, V> Iterator for DenseNodeMapIterMut<'a, V> { + type Item = (NodeId, &'a mut V); + + #[inline] + fn next(&mut self) -> Option { + loop { + match self.inner.next() { + Some((idx, Some(v))) => return Some((idx as NodeId, v)), + Some((_, None)) => continue, + None => return None, + } + } + } +} + +impl<'a, V> IntoIterator for &'a DenseNodeMap { + type Item = (NodeId, &'a V); + type IntoIter = DenseNodeMapIter<'a, V>; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +impl FromIterator<(NodeId, V)> for DenseNodeMap { + fn from_iter>(iter: T) -> Self { + let iter = iter.into_iter(); + let (lower, _) = iter.size_hint(); + let mut map = Self::with_capacity(lower); + for (id, v) in iter { + map.insert(id, v); + } + map + } +} diff --git a/crates/grida-canvas/src/cache/geometry.rs b/crates/grida-canvas/src/cache/geometry.rs index 30d1453aa8..9dabf8db63 100644 --- a/crates/grida-canvas/src/cache/geometry.rs +++ b/crates/grida-canvas/src/cache/geometry.rs @@ -9,16 +9,44 @@ //! - Consumes LayoutResult as immutable input from LayoutEngine //! - Missing layout for Inset nodes is a PANIC (LayoutEngine bug) //! - Missing geometry entry when accessed is a PANIC (GeometryCache bug) +//! +//! ## Property Split Optimization +//! +//! The `SceneGraph` stores compact `NodeGeoData` (~48 bytes/node) alongside +//! the full `Node` enum. This module resolves layout into a `GeoInput` struct +//! by reading only from `NodeGeoData` + `LayoutResult`, then runs the DFS on +//! that — never touching the full `Node` enum (~500+ bytes/node). +//! Working set for 136k nodes: ~7.6 MB instead of ~65 MB. +use crate::cache::fast_hash::DenseNodeMap; use crate::cache::paragraph::ParagraphCache; -use crate::cg::prelude::*; -use crate::node::scene_graph::SceneGraph; -use crate::node::schema::{LayerEffects, Node, NodeGeometryMixin, NodeId, NodeRectMixin, Scene}; +use crate::node::scene_graph::{GeoNodeKind, NodeGeoData, RenderBoundsInflation, SceneGraph}; +use crate::node::schema::{Node, NodeId, Scene}; use crate::runtime::font_repository::FontRepository; use math2::rect; use math2::rect::Rectangle; use math2::transform::AffineTransform; -use std::collections::HashMap; + +// --------------------------------------------------------------------------- +// GeoInput — layout-resolved per-node geometry for the DFS +// --------------------------------------------------------------------------- + +/// Layout-resolved per-node data for the geometry DFS. +/// +/// Built from `NodeGeoData` (schema-level) + `LayoutResult` (layout-level). +/// The DFS reads only this — compact and `Copy`, ~48 bytes. +#[derive(Debug, Clone, Copy)] +struct GeoInput { + transform: AffineTransform, + width: f32, + height: f32, + kind: GeoNodeKind, + render_bounds_inflation: RenderBoundsInflation, +} + +// --------------------------------------------------------------------------- +// GeometryEntry — public output (unchanged) +// --------------------------------------------------------------------------- /// Geometry data used for layout, culling, and rendering. /// @@ -45,20 +73,19 @@ pub struct GeometryEntry { pub dirty_bounds: bool, } -/// Context passed during geometry building -struct GeometryBuildContext { - viewport_size: crate::node::schema::Size, -} +// --------------------------------------------------------------------------- +// GeometryCache — public API (unchanged) +// --------------------------------------------------------------------------- #[derive(Debug, Clone)] pub struct GeometryCache { - entries: HashMap, + entries: DenseNodeMap, } impl GeometryCache { pub fn new() -> Self { Self { - entries: HashMap::new(), + entries: DenseNodeMap::new(), } } @@ -85,74 +112,111 @@ impl GeometryCache { layout_result: Option<&crate::layout::cache::LayoutResult>, viewport_size: crate::node::schema::Size, ) -> Self { - let mut cache = Self::new(); - let root_world = AffineTransform::identity(); - let context = GeometryBuildContext { viewport_size }; - - for child in scene.graph.roots() { - Self::build_recursive( - &child, - &scene.graph, - &root_world, - None, - &mut cache, + let graph = &scene.graph; + let schema_geo = graph.geo_data(); + + #[cfg(feature = "perf")] + let _t_resolve_start = crate::sys::perf_now(); + + // ── Layout resolution pass ── + // Resolve layout-dependent fields (position, size) from the compact + // NodeGeoData + LayoutResult. Text measurement for nodes without + // layout falls back to accessing the Node enum (rare path). + // + // This pass iterates over NodeGeoData (~48 bytes/node) instead of + // Node (~500+ bytes/node). + let mut is_layout_container = DenseNodeMap::with_capacity(graph.node_count()); + for (id, geo) in schema_geo.iter() { + let is_container = matches!( + geo.kind, + GeoNodeKind::Container | GeoNodeKind::InitialContainer + ); + is_layout_container.insert(id, is_container); + } + + // Build parent map from the graph's link structure. + let mut parent_map: DenseNodeMap = DenseNodeMap::with_capacity(graph.node_count()); + for (id, _) in schema_geo.iter() { + if let Some(children) = graph.get_children(&id) { + for child_id in children { + parent_map.insert(*child_id, id); + } + } + } + + let mut geo_inputs = DenseNodeMap::with_capacity(graph.node_count()); + + for (id, geo) in schema_geo.iter() { + let parent_id = parent_map.get(&id).copied(); + let resolved = resolve_layout( + &id, + geo, + parent_id, + layout_result, + &is_layout_container, + graph, paragraph_cache, fonts, - layout_result, - &context, + viewport_size, ); + geo_inputs.insert(id, resolved); } - cache - } - /// Check if a node's parent is a layout container (Container or ICB). - /// Only layout containers provide meaningful layout results; Group and - /// BooleanOperation parents produce synthetic fallbacks. - fn is_layout_container_parent( - parent_id: &Option, - graph: &SceneGraph, - ) -> bool { - parent_id - .as_ref() - .and_then(|pid| graph.get_node(pid).ok()) - .map(|parent_node| { - matches!(parent_node, Node::Container(_) | Node::InitialContainer(_)) - }) - .unwrap_or(false) + #[cfg(feature = "perf")] + let _t_resolve_end = crate::sys::perf_now(); + + // ── DFS pass ── + let mut cache = Self { + entries: DenseNodeMap::with_capacity(graph.node_count()), + }; + let root_world = AffineTransform::identity(); + + for child in graph.roots() { + Self::build_recursive(child, &root_world, None, &mut cache, graph, &geo_inputs); + } + + #[cfg(feature = "perf")] + { + let _t_dfs_end = crate::sys::perf_now(); + eprintln!( + "[geometry] resolve={:.0}ms dfs={:.0}ms total={:.0}ms", + _t_resolve_end - _t_resolve_start, + _t_dfs_end - _t_resolve_end, + _t_dfs_end - _t_resolve_start, + ); + } + + cache } + /// DFS that operates on layout-resolved `GeoInput` data. fn build_recursive( id: &NodeId, - graph: &SceneGraph, parent_world: &AffineTransform, parent_id: Option, cache: &mut GeometryCache, - paragraph_cache: &mut ParagraphCache, - fonts: &FontRepository, - layout_result: Option<&crate::layout::cache::LayoutResult>, - context: &GeometryBuildContext, + graph: &SceneGraph, + geo_inputs: &DenseNodeMap, ) -> Rectangle { - let node = graph - .get_node(id) - .expect(&format!("node not found in geometry cache {id:?}")); + let geo = geo_inputs + .get(id) + .expect("GeoInput not found — resolve pass missed a node"); - match node { - Node::Group(n) => { - let world_transform = parent_world.compose(&n.transform.unwrap_or_default()); + match geo.kind { + GeoNodeKind::Group => { + let world_transform = parent_world.compose(&geo.transform); let mut union_bounds: Option = None; let mut union_render_bounds: Option = None; + if let Some(children) = graph.get_children(id) { for child_id in children { let child_bounds = Self::build_recursive( child_id, - graph, &world_transform, - Some(id.clone()), + Some(*id), cache, - paragraph_cache, - fonts, - layout_result, - context, + graph, + geo_inputs, ); union_bounds = match union_bounds { Some(b) => Some(rect::union(&[b, child_bounds])), @@ -167,7 +231,7 @@ impl GeometryCache { } } - let world_bounds = union_bounds.unwrap_or_else(|| Rectangle { + let world_bounds = union_bounds.unwrap_or(Rectangle { x: 0.0, y: 0.0, width: 0.0, @@ -188,85 +252,73 @@ impl GeometryCache { let render_bounds = union_render_bounds.unwrap_or(world_bounds); let entry = GeometryEntry { - transform: n.transform.unwrap_or_default(), + transform: geo.transform, absolute_transform: world_transform, bounding_box: local_bounds, absolute_bounding_box: world_bounds, absolute_render_bounds: render_bounds, - parent: parent_id.clone(), + parent: parent_id, dirty_transform: false, dirty_bounds: false, }; - cache.entries.insert(id.clone(), entry.clone()); - entry.absolute_bounding_box + let bounds = entry.absolute_bounding_box; + cache.entries.insert(*id, entry); + bounds } - Node::InitialContainer(_n) => { - // ICB fills viewport - size from context - // Layout was already computed by LayoutEngine - let size = context.viewport_size; - - let local_transform = AffineTransform::identity(); - let world_transform = parent_world.compose(&local_transform); + GeoNodeKind::InitialContainer => { + let world_transform = parent_world.compose(&geo.transform); let local_bounds = Rectangle { x: 0.0, y: 0.0, - width: size.width, - height: size.height, + width: geo.width, + height: geo.height, }; - - // Build children geometries (may use computed layouts from LayoutEngine) let mut union_world_bounds = transform_rect(&local_bounds, &world_transform); if let Some(children) = graph.get_children(id) { for child_id in children { let child_bounds = Self::build_recursive( child_id, - graph, &world_transform, - Some(id.clone()), + Some(*id), cache, - paragraph_cache, - fonts, - layout_result, - context, + graph, + geo_inputs, ); union_world_bounds = rect::union(&[union_world_bounds, child_bounds]); } } - let render_bounds = union_world_bounds; // ICB has no effects - let entry = GeometryEntry { - transform: local_transform, + transform: geo.transform, absolute_transform: world_transform, bounding_box: local_bounds, absolute_bounding_box: union_world_bounds, - absolute_render_bounds: render_bounds, + absolute_render_bounds: union_world_bounds, parent: parent_id, dirty_transform: false, dirty_bounds: false, }; - cache.entries.insert(id.clone(), entry); + cache.entries.insert(*id, entry); union_world_bounds } - Node::BooleanOperation(n) => { - let world_transform = parent_world.compose(&n.transform.unwrap_or_default()); + + GeoNodeKind::BooleanOperation => { + let world_transform = parent_world.compose(&geo.transform); let mut union_bounds: Option = None; + if let Some(children) = graph.get_children(id) { for child_id in children { let child_bounds = Self::build_recursive( child_id, - graph, &world_transform, - Some(id.clone()), + Some(*id), cache, - paragraph_cache, - fonts, - layout_result, - context, + graph, + geo_inputs, ); union_bounds = match union_bounds { Some(b) => Some(rect::union(&[b, child_bounds])), @@ -275,7 +327,7 @@ impl GeometryCache { } } - let world_bounds = union_bounds.unwrap_or_else(|| Rectangle { + let world_bounds = union_bounds.unwrap_or(Rectangle { x: 0.0, y: 0.0, width: 0.0, @@ -292,264 +344,117 @@ impl GeometryCache { height: 0.0, } }; - - let render_bounds = compute_render_bounds_from_style( - world_bounds, - n.stroke_width.value_or_zero(), - n.stroke_style.stroke_align, - &n.effects, - ); + let render_bounds = inflate_rect_sides(world_bounds, &geo.render_bounds_inflation); let entry = GeometryEntry { - transform: n.transform.unwrap_or_default(), + transform: geo.transform, absolute_transform: world_transform, bounding_box: local_bounds, absolute_bounding_box: world_bounds, absolute_render_bounds: render_bounds, - parent: parent_id.clone(), + parent: parent_id, dirty_transform: false, dirty_bounds: false, }; - cache.entries.insert(id.clone(), entry.clone()); - entry.absolute_bounding_box + let bounds = entry.absolute_bounding_box; + cache.entries.insert(*id, entry); + bounds } - Node::Container(n) => { - // All containers use computed layout (roots have position corrected by LayoutEngine) - let (x, y, width, height) = if let Some(result) = layout_result { - // Layout engine is active: use computed layout - let computed = result - .get(id) - .expect("Container must have layout result when layout engine is used"); - (computed.x, computed.y, computed.width, computed.height) - } else { - // No layout engine: use schema directly (backward compatibility) - ( - n.position.x().unwrap_or(0.0), - n.position.y().unwrap_or(0.0), - n.layout_dimensions.layout_target_width.unwrap_or(0.0), - n.layout_dimensions.layout_target_height.unwrap_or(0.0), - ) - }; - let local_transform = AffineTransform::new(x, y, n.rotation); + GeoNodeKind::Container => { let local_bounds = Rectangle { x: 0.0, y: 0.0, - width, - height, + width: geo.width, + height: geo.height, }; - let world_transform = parent_world.compose(&local_transform); + let world_transform = parent_world.compose(&geo.transform); let world_bounds = transform_rect(&local_bounds, &world_transform); let mut union_world_bounds = world_bounds; - let render_bounds = if let Some(rect_stroke) = n.rectangular_stroke_width() { - compute_render_bounds_with_rectangular_stroke( - world_bounds, - &rect_stroke, - n.stroke_style.stroke_align, - &n.effects, - ) - } else { - compute_render_bounds_from_style( - world_bounds, - n.render_bounds_stroke_width(), - n.stroke_style.stroke_align, - &n.effects, - ) - }; + let render_bounds = inflate_rect_sides(world_bounds, &geo.render_bounds_inflation); if let Some(children) = graph.get_children(id) { for child_id in children { let child_bounds = Self::build_recursive( child_id, - graph, &world_transform, - Some(id.clone()), + Some(*id), cache, - paragraph_cache, - fonts, - layout_result, - context, + graph, + geo_inputs, ); union_world_bounds = rect::union(&[union_world_bounds, child_bounds]); } } let entry = GeometryEntry { - transform: local_transform, + transform: geo.transform, absolute_transform: world_transform, bounding_box: local_bounds, absolute_bounding_box: world_bounds, absolute_render_bounds: render_bounds, - parent: parent_id.clone(), + parent: parent_id, dirty_transform: false, dirty_bounds: false, }; - cache.entries.insert(id.clone(), entry.clone()); - + cache.entries.insert(*id, entry); union_world_bounds } - Node::TextSpan(n) => { - // Resolve layout position/size (if available) and measure text consistently with layout width - let layout = layout_result.and_then(|r| r.get(id)); - let width_for_measure = layout.map(|l| l.width).or(n.width); - - let measurements = paragraph_cache.measure( - &n.text, - &n.text_style, - &n.text_align, - &n.max_lines, - &n.ellipsis, - width_for_measure, - fonts, - Some(id), - ); - - const MIN_SIZE_DIRTY_HACK: f32 = 1.0; - - let parent_is_layout_container = - Self::is_layout_container_parent(&parent_id, graph); - - let (local_transform, width, height) = if parent_is_layout_container { - let width = layout - .map(|l| l.width) - .unwrap_or_else(|| measurements.max_width) - .max(MIN_SIZE_DIRTY_HACK); - let height = layout - .map(|l| l.height) - .unwrap_or_else(|| n.height.unwrap_or(measurements.height)) - .max(MIN_SIZE_DIRTY_HACK); - let (x, y) = if let Some(l) = layout { - (l.x, l.y) - } else { - (n.transform.x(), n.transform.y()) - }; - (AffineTransform::new(x, y, n.transform.rotation()), width, height) - } else { - let width = layout - .map(|l| l.width) - .unwrap_or_else(|| measurements.max_width) - .max(MIN_SIZE_DIRTY_HACK); - let height = layout - .map(|l| l.height) - .unwrap_or_else(|| n.height.unwrap_or(measurements.height)) - .max(MIN_SIZE_DIRTY_HACK); - (n.transform, width, height) - }; + + GeoNodeKind::TextSpan => { let local_bounds = Rectangle { x: 0.0, y: 0.0, - width, - height, + width: geo.width, + height: geo.height, }; - let world_transform = parent_world.compose(&local_transform); + let world_transform = parent_world.compose(&geo.transform); let world_bounds = transform_rect(&local_bounds, &world_transform); - let render_bounds = compute_render_bounds(node, world_bounds); + let render_bounds = inflate_rect_sides(world_bounds, &geo.render_bounds_inflation); let entry = GeometryEntry { - transform: local_transform, + transform: geo.transform, absolute_transform: world_transform, bounding_box: local_bounds, absolute_bounding_box: world_bounds, absolute_render_bounds: render_bounds, - parent: parent_id.clone(), + parent: parent_id, dirty_transform: false, dirty_bounds: false, }; - cache.entries.insert(id.clone(), entry.clone()); - - local_bounds + let bounds = world_bounds; + cache.entries.insert(*id, entry); + bounds } - _ => { - // Leaf nodes - check layout result first, fallback to schema transform - let (rec_transform, schema_width, schema_height) = match node { - Node::Rectangle(n) => (n.transform, n.size.width, n.size.height), - Node::Ellipse(n) => (n.transform, n.size.width, n.size.height), - Node::Image(n) => (n.transform, n.size.width, n.size.height), - Node::RegularPolygon(n) => (n.transform, n.size.width, n.size.height), - Node::RegularStarPolygon(n) => (n.transform, n.size.width, n.size.height), - Node::Line(n) => (n.transform, n.size.width, 0.0), - Node::Polygon(n) => { - let rect = n.rect(); - (n.transform, rect.width, rect.height) - } - Node::Path(n) => { - let rect = n.rect(); - (n.transform, rect.width, rect.height) - } - Node::Vector(n) => { - let rect = n.network.bounds(); - (n.transform, rect.width, rect.height) - } - Node::Error(n) => (n.transform, n.size.width, n.size.height), - // V2/special nodes handled above - _ => unreachable!("Has dedicated case above"), - }; - - // Check if this node's parent is a layout container (Container - // or ICB). Only those parents provide meaningful layout results - // (computed flex/block positions). Nodes under Group or - let parent_is_layout_container = - Self::is_layout_container_parent(&parent_id, graph); - - let (local_transform, width, height) = if parent_is_layout_container { - // Parent is a layout container: use layout result for - // position/size (flex/absolute layout), with rotation - // from the schema transform. - let (x, y, width, height) = - if let Some(result) = layout_result.and_then(|r| r.get(id)) { - (result.x, result.y, result.width, result.height) - } else { - ( - rec_transform.x(), - rec_transform.y(), - schema_width, - schema_height, - ) - }; - (AffineTransform::new(x, y, rec_transform.rotation()), width, height) - } else { - // Parent is NOT a layout container (Group, - // BooleanOperation, or root): use the full schema - // transform. This preserves scale, skew, and arbitrary - // matrix entries that don't fit the (x, y, rotation) - // decomposition. - let width = layout_result - .and_then(|r| r.get(id)) - .map(|l| l.width) - .unwrap_or(schema_width); - let height = layout_result - .and_then(|r| r.get(id)) - .map(|l| l.height) - .unwrap_or(schema_height); - (rec_transform, width, height) - }; + GeoNodeKind::Leaf => { let local_bounds = Rectangle { x: 0.0, y: 0.0, - width, - height, + width: geo.width, + height: geo.height, }; - let world_transform = parent_world.compose(&local_transform); + let world_transform = parent_world.compose(&geo.transform); let world_bounds = transform_rect(&local_bounds, &world_transform); - let render_bounds = compute_render_bounds(node, world_bounds); + let render_bounds = inflate_rect_sides(world_bounds, &geo.render_bounds_inflation); let entry = GeometryEntry { - transform: local_transform, + transform: geo.transform, absolute_transform: world_transform, bounding_box: local_bounds, absolute_bounding_box: world_bounds, absolute_render_bounds: render_bounds, - parent: parent_id.clone(), + parent: parent_id, dirty_transform: false, dirty_bounds: false, }; - cache.entries.insert(id.clone(), entry.clone()); - entry.absolute_bounding_box + let bounds = entry.absolute_bounding_box; + cache.entries.insert(*id, entry); + bounds } } } @@ -566,14 +471,12 @@ impl GeometryCache { self.entries.get(id).map(|e| e.absolute_bounding_box) } - /// Return expanded render bounds for a node if available. pub fn get_render_bounds(&self, id: &NodeId) -> Option { self.entries.get(id).map(|e| e.absolute_render_bounds) } - /// Return the parent NodeId for a given node if available. pub fn get_parent(&self, id: &NodeId) -> Option { - self.entries.get(id).and_then(|e| e.parent.clone()) + self.entries.get(id).and_then(|e| e.parent) } pub fn len(&self) -> usize { @@ -584,258 +487,200 @@ impl GeometryCache { self.entries.contains_key(id) } - /// filter by node id and its entry data pub fn filter(&self, filter: impl Fn(&NodeId, &GeometryEntry) -> bool) -> Self { Self { entries: self .entries .iter() .filter(|(id, entry)| filter(id, entry)) - .map(|(id, entry)| (id.clone(), entry.clone())) + .map(|(id, entry)| (id, entry.clone())) .collect(), } } } -fn transform_rect(rect: &Rectangle, t: &AffineTransform) -> Rectangle { - rect::transform(*rect, t) -} +// --------------------------------------------------------------------------- +// Layout resolution — NodeGeoData + LayoutResult → GeoInput +// --------------------------------------------------------------------------- -fn inflate_rect(rect: Rectangle, delta: f32) -> Rectangle { - if delta <= 0.0 { - return rect; - } - Rectangle { - x: rect.x - delta, - y: rect.y - delta, - width: rect.width + 2.0 * delta, - height: rect.height + 2.0 * delta, - } -} - -fn stroke_outset(align: StrokeAlign, width: f32) -> f32 { - match align { - StrokeAlign::Inside => 0.0, - StrokeAlign::Center => width / 2.0, - StrokeAlign::Outside => width, - } -} - -fn compute_render_bounds_from_effects(bounds: Rectangle, effects: &LayerEffects) -> Rectangle { - let mut bounds = bounds; - if let Some(blur) = &effects.blur { - bounds = match &blur.blur { - FeBlur::Gaussian(gaussian) => { - // Use 3x sigma for 99.7% Gaussian coverage - inflate_rect(bounds, gaussian.radius * 3.0) - } - FeBlur::Progressive(progressive) => { - // Use the maximum of both radii for bounds calculation - // to handle both increasing and decreasing blur gradients - // Multiply by 3.0 for proper 3-sigma Gaussian coverage - let max_radius = progressive.radius.max(progressive.radius2); - inflate_rect(bounds, max_radius * 3.0) - } - }; - } - for shadow in &effects.shadows { - bounds = compute_render_bounds_from_effect(bounds, &shadow.clone().into()); - } - bounds -} - -fn compute_render_bounds_from_effect(bounds: Rectangle, effect: &FilterEffect) -> Rectangle { - match effect { - FilterEffect::LiquidGlass(glass) => inflate_rect(bounds, glass.blur_radius * 3.0), - FilterEffect::LayerBlur(blur) => match &blur.blur { - FeBlur::Gaussian(gaussian) => inflate_rect(bounds, gaussian.radius * 3.0), - FeBlur::Progressive(progressive) => { - let max_radius = progressive.radius.max(progressive.radius2); - inflate_rect(bounds, max_radius * 3.0) - } +/// Resolve layout-dependent fields from `NodeGeoData` + `LayoutResult`. +/// +/// For most nodes this is a lightweight copy from the pre-extracted data +/// with layout overrides for position/size. Only text spans without layout +/// results fall back to accessing the full `Node` for text measurement. +#[allow(clippy::too_many_arguments)] +fn resolve_layout( + id: &NodeId, + geo: &NodeGeoData, + parent_id: Option, + layout_result: Option<&crate::layout::cache::LayoutResult>, + is_layout_container: &DenseNodeMap, + graph: &SceneGraph, + paragraph_cache: &mut ParagraphCache, + fonts: &FontRepository, + viewport_size: crate::node::schema::Size, +) -> GeoInput { + match geo.kind { + GeoNodeKind::Group | GeoNodeKind::BooleanOperation => GeoInput { + transform: geo.schema_transform, + width: geo.schema_width, + height: geo.schema_height, + kind: geo.kind, + render_bounds_inflation: geo.render_bounds_inflation, }, - FilterEffect::BackdropBlur(blur) => match &blur.blur { - FeBlur::Gaussian(gaussian) => inflate_rect(bounds, gaussian.radius * 3.0), - FeBlur::Progressive(progressive) => { - let max_radius = progressive.radius.max(progressive.radius2); - inflate_rect(bounds, max_radius * 3.0) - } + GeoNodeKind::InitialContainer => GeoInput { + transform: geo.schema_transform, + width: viewport_size.width, + height: viewport_size.height, + kind: geo.kind, + render_bounds_inflation: geo.render_bounds_inflation, }, - FilterEffect::DropShadow(shadow) => { - // Apply spread by inflating the bounds, then offset and blur - let mut rect = if shadow.spread != 0.0 { - inflate_rect(bounds, shadow.spread) + GeoNodeKind::Container => { + let (x, y, width, height) = + if let Some(computed) = layout_result.and_then(|r| r.get(id)) { + (computed.x, computed.y, computed.width, computed.height) + } else { + // Fallback to schema data when layout result is missing. + // This happens for orphan nodes not reachable from scene roots, + // or when layout is skipped entirely (layout_result == None). + ( + geo.schema_transform.x(), + geo.schema_transform.y(), + geo.schema_width, + geo.schema_height, + ) + }; + GeoInput { + transform: AffineTransform::new(x, y, geo.rotation), + width, + height, + kind: geo.kind, + render_bounds_inflation: geo.render_bounds_inflation, + } + } + GeoNodeKind::TextSpan => { + let layout = layout_result.and_then(|r| r.get(id)); + const MIN_SIZE_DIRTY_HACK: f32 = 1.0; + + let parent_is_layout_container = parent_id + .as_ref() + .and_then(|pid| is_layout_container.get(pid).copied()) + .unwrap_or(false); + + let (local_transform, width, height) = if let Some(l) = layout { + let width = l.width.max(MIN_SIZE_DIRTY_HACK); + let height = l.height.max(MIN_SIZE_DIRTY_HACK); + let transform = if parent_is_layout_container { + AffineTransform::new(l.x, l.y, geo.schema_transform.rotation()) + } else { + geo.schema_transform + }; + (transform, width, height) } else { - bounds + // Fallback: text measurement via paragraph cache. + // This requires accessing the Node for text content. + // Only happens when layout_result is None (rare path). + if let Ok(Node::TextSpan(n)) = graph.get_node(id) { + let measurements = paragraph_cache.measure( + &n.text, + &n.text_style, + &n.text_align, + &n.max_lines, + &n.ellipsis, + n.width, + fonts, + Some(id), + ); + let width = measurements.max_width.max(MIN_SIZE_DIRTY_HACK); + let height = n + .height + .unwrap_or(measurements.height) + .max(MIN_SIZE_DIRTY_HACK); + (geo.schema_transform, width, height) + } else { + // Shouldn't happen; schema_width/height as fallback + ( + geo.schema_transform, + geo.schema_width.max(MIN_SIZE_DIRTY_HACK), + geo.schema_height.max(MIN_SIZE_DIRTY_HACK), + ) + } }; - rect.x += shadow.dx; - rect.y += shadow.dy; - // Use 3x sigma for proper Gaussian blur coverage - inflate_rect(rect, shadow.blur * 3.0) - } - // no inflation - FilterEffect::Noise(_) => bounds, - FilterEffect::InnerShadow(_) => bounds, - } -} - -fn compute_render_bounds_from_style( - world_bounds: Rectangle, - stroke_width: f32, - stroke_align: StrokeAlign, - effects: &LayerEffects, -) -> Rectangle { - let mut bounds = inflate_rect(world_bounds, stroke_outset(stroke_align, stroke_width)); - - bounds = compute_render_bounds_from_effects(bounds, effects); - bounds -} - -/// Computes render bounds for nodes with per-side stroke widths. -/// -/// Handles all three stroke alignments: -/// - **Center**: Inflate by half-widths (stroke extends inward and outward) -/// - **Inside**: No inflation (stroke is entirely inside node bounds) -/// - **Outside**: Inflate by full-widths (stroke extends entirely outward) -fn compute_render_bounds_with_rectangular_stroke( - world_bounds: Rectangle, - rect_stroke: &RectangularStrokeWidth, - stroke_align: StrokeAlign, - effects: &LayerEffects, -) -> Rectangle { - let mut bounds = world_bounds; - - // Inflate based on stroke alignment - match stroke_align { - StrokeAlign::Center => { - // Center: inflate by half the stroke width on each side - bounds = rect::inflate( - bounds, - rect::Sides { - top: rect_stroke.stroke_top_width / 2.0, - right: rect_stroke.stroke_right_width / 2.0, - bottom: rect_stroke.stroke_bottom_width / 2.0, - left: rect_stroke.stroke_left_width / 2.0, - }, - ); - } - StrokeAlign::Inside => { - // Inside: no inflation - stroke is entirely inside the node bounds - // bounds remain unchanged + GeoInput { + transform: local_transform, + width, + height, + kind: geo.kind, + render_bounds_inflation: geo.render_bounds_inflation, + } } - StrokeAlign::Outside => { - // Outside: inflate by full stroke width on each side - bounds = rect::inflate( - bounds, - rect::Sides { - top: rect_stroke.stroke_top_width, - right: rect_stroke.stroke_right_width, - bottom: rect_stroke.stroke_bottom_width, - left: rect_stroke.stroke_left_width, - }, - ); + GeoNodeKind::Leaf => { + let parent_is_layout_container = parent_id + .as_ref() + .and_then(|pid| is_layout_container.get(pid).copied()) + .unwrap_or(false); + + let (local_transform, width, height) = if parent_is_layout_container { + let (x, y, width, height) = + if let Some(result) = layout_result.and_then(|r| r.get(id)) { + (result.x, result.y, result.width, result.height) + } else { + ( + geo.schema_transform.x(), + geo.schema_transform.y(), + geo.schema_width, + geo.schema_height, + ) + }; + ( + AffineTransform::new(x, y, geo.schema_transform.rotation()), + width, + height, + ) + } else { + let width = layout_result + .and_then(|r| r.get(id)) + .map(|l| l.width) + .unwrap_or(geo.schema_width); + let height = layout_result + .and_then(|r| r.get(id)) + .map(|l| l.height) + .unwrap_or(geo.schema_height); + (geo.schema_transform, width, height) + }; + + GeoInput { + transform: local_transform, + width, + height, + kind: geo.kind, + render_bounds_inflation: geo.render_bounds_inflation, + } } } +} - bounds = compute_render_bounds_from_effects(bounds, effects); +// --------------------------------------------------------------------------- +// Helper functions +// --------------------------------------------------------------------------- - bounds +fn transform_rect(rect: &Rectangle, t: &AffineTransform) -> Rectangle { + rect::transform(*rect, t) } -fn compute_render_bounds(node: &Node, world_bounds: Rectangle) -> Rectangle { - match node { - Node::Rectangle(n) => { - // Check if this node has per-side stroke widths - if let Some(rect_stroke) = n.rectangular_stroke_width() { - compute_render_bounds_with_rectangular_stroke( - world_bounds, - &rect_stroke, - n.stroke_style.stroke_align, - &n.effects, - ) - } else { - compute_render_bounds_from_style( - world_bounds, - n.render_bounds_stroke_width(), - n.stroke_style.stroke_align, - &n.effects, - ) - } - } - Node::Ellipse(n) => compute_render_bounds_from_style( - world_bounds, - n.render_bounds_stroke_width(), - n.stroke_style.stroke_align, - &n.effects, - ), - Node::Polygon(n) => compute_render_bounds_from_style( - world_bounds, - n.render_bounds_stroke_width(), - n.stroke_style.stroke_align, - &n.effects, - ), - Node::RegularPolygon(n) => compute_render_bounds_from_style( - world_bounds, - n.render_bounds_stroke_width(), - n.stroke_style.stroke_align, - &n.effects, - ), - Node::RegularStarPolygon(n) => compute_render_bounds_from_style( - world_bounds, - n.render_bounds_stroke_width(), - n.stroke_style.stroke_align, - &n.effects, - ), - Node::Path(n) => compute_render_bounds_from_style( - world_bounds, - n.stroke_width.value_or_zero(), - n.stroke_style.stroke_align, - &n.effects, - ), - Node::Vector(n) => compute_render_bounds_from_style( - world_bounds, - n.stroke_width, - n.get_stroke_align(), - &n.effects, - ), - Node::Image(n) => compute_render_bounds_from_style( - world_bounds, - n.render_bounds_stroke_width(), - n.stroke_style.stroke_align, - &n.effects, - ), - Node::Line(n) => compute_render_bounds_from_style( - world_bounds, - n.stroke_width, - n.get_stroke_align(), - &n.effects, - ), - Node::TextSpan(n) => compute_render_bounds_from_style( - world_bounds, - n.stroke_width, - n.stroke_align, - &LayerEffects::default(), - ), - Node::Container(n) => { - // Check if this node has per-side stroke widths - if let Some(rect_stroke) = n.rectangular_stroke_width() { - compute_render_bounds_with_rectangular_stroke( - world_bounds, - &rect_stroke, - n.stroke_style.stroke_align, - &n.effects, - ) - } else { - compute_render_bounds_from_style( - world_bounds, - n.render_bounds_stroke_width(), - n.stroke_style.stroke_align, - &n.effects, - ) - } - } - Node::Error(_) => world_bounds, - Node::Group(_) | Node::BooleanOperation(_) | Node::InitialContainer(_) => world_bounds, +/// Inflate a rectangle by pre-computed per-side values. +fn inflate_rect_sides(rect: Rectangle, inf: &RenderBoundsInflation) -> Rectangle { + if inf.is_zero() { + return rect; } + rect::inflate( + rect, + rect::Sides { + top: inf.top, + right: inf.right, + bottom: inf.bottom, + left: inf.left, + }, + ) } diff --git a/crates/grida-canvas/src/cache/mod.rs b/crates/grida-canvas/src/cache/mod.rs index 33130963d1..4b95930162 100644 --- a/crates/grida-canvas/src/cache/mod.rs +++ b/crates/grida-canvas/src/cache/mod.rs @@ -1,5 +1,6 @@ pub mod atlas; pub mod compositor; +pub mod fast_hash; pub mod geometry; pub mod paragraph; pub mod picture; diff --git a/crates/grida-canvas/src/cache/paragraph.rs b/crates/grida-canvas/src/cache/paragraph.rs index d2a4bf3874..bcbd1a450a 100644 --- a/crates/grida-canvas/src/cache/paragraph.rs +++ b/crates/grida-canvas/src/cache/paragraph.rs @@ -78,6 +78,7 @@ use crate::text::text_style::textstyle; use skia_safe::textlayout; use std::cell::RefCell; use std::collections::hash_map::DefaultHasher; +use crate::cache::fast_hash::DenseNodeMap; use std::collections::HashMap; use std::hash::{Hash, Hasher}; use std::rc::Rc; @@ -152,8 +153,9 @@ pub struct ParagraphCacheEntry { pub font_generation: usize, /// The cached Skia paragraph object pub paragraph: Rc>, - // TODO: Add width-based caching in the future - // For now, we just store the paragraph and compute measurements on demand + /// Cached measurements for the last layout width — avoids re-calling + /// Skia `paragraph.layout()` on every access when the width hasn't changed. + pub cached_measurements: Option<(Option, LayoutMeasurements)>, } /// Accumulated statistics from `measure()` calls — for benchmarking only. @@ -167,8 +169,8 @@ pub struct ParagraphMeasureStats { #[derive(Default, Debug, Clone)] pub struct ParagraphCache { - // ID-based cache for text nodes (primary usage) - entries_measurement_by_id: HashMap, + // ID-based cache for text nodes (primary usage) — Vec-backed for O(1) access + entries_measurement_by_id: DenseNodeMap, // Shape-key-based cache for flexible usage (not currently used) entries_measurement_by_shapekey_unstable: HashMap, /// Benchmark statistics — zero-cost when not read. @@ -181,7 +183,7 @@ pub struct ParagraphCache { impl ParagraphCache { pub fn new() -> Self { Self { - entries_measurement_by_id: HashMap::new(), + entries_measurement_by_id: DenseNodeMap::new(), entries_measurement_by_shapekey_unstable: HashMap::new(), stats: ParagraphMeasureStats::default(), skip_text_measure: false, @@ -236,23 +238,37 @@ impl ParagraphCache { // Check if we have a cached paragraph if let Some(node_id) = id { // Use ID-based cache - if let Some(entry) = self.entries_measurement_by_id.get(node_id) { + if let Some(entry) = self.entries_measurement_by_id.get_mut(node_id) { if entry.font_generation == fonts_gen { self.stats.cache_hits += 1; - // Use the cached paragraph and compute measurements + // Fast path: return cached measurements if width matches + if let Some((cached_w, ref measurements)) = entry.cached_measurements { + if cached_w == width { + return measurements.clone(); + } + } + // Width changed: re-layout and cache let paragraph_rc = entry.paragraph.clone(); - return Self::compute_measurements(paragraph_rc, width); + let m = Self::compute_measurements(paragraph_rc, width); + entry.cached_measurements = Some((width, m.clone())); + return m; } } } else { // Use shape-key-based cache let hash = Self::shape_key(text, style, align, max_lines); - if let Some(entry) = self.entries_measurement_by_shapekey_unstable.get(&hash) { + if let Some(entry) = self.entries_measurement_by_shapekey_unstable.get_mut(&hash) { if entry.font_generation == fonts_gen { self.stats.cache_hits += 1; - // Use the cached paragraph and compute measurements + if let Some((cached_w, ref measurements)) = entry.cached_measurements { + if cached_w == width { + return measurements.clone(); + } + } let paragraph_rc = entry.paragraph.clone(); - return Self::compute_measurements(paragraph_rc, width); + let m = Self::compute_measurements(paragraph_rc, width); + entry.cached_measurements = Some((width, m.clone())); + return m; } } } @@ -292,10 +308,14 @@ impl ParagraphCache { // Store the paragraph for future use let paragraph_rc = Rc::new(RefCell::new(paragraph)); + // Compute measurements and cache them with the entry + let measurements = Self::compute_measurements(paragraph_rc.clone(), width); + let entry = ParagraphCacheEntry { hash: Self::shape_key(text, style, align, max_lines), font_generation: fonts_gen, - paragraph: paragraph_rc.clone(), + paragraph: paragraph_rc, + cached_measurements: Some((width, measurements.clone())), }; // Store in the appropriate cache @@ -307,8 +327,7 @@ impl ParagraphCache { .insert(entry.hash, entry); } - // Compute and return the measurements - Self::compute_measurements(paragraph_rc, width) + measurements } /// Helper method to compute measurements for a given paragraph and width diff --git a/crates/grida-canvas/src/cache/picture.rs b/crates/grida-canvas/src/cache/picture.rs index 6384202690..23e1941084 100644 --- a/crates/grida-canvas/src/cache/picture.rs +++ b/crates/grida-canvas/src/cache/picture.rs @@ -1,6 +1,6 @@ +use crate::cache::fast_hash::{new_node_id_map, NodeIdHashMap}; use crate::node::schema::NodeId; use skia_safe::Picture; -use std::collections::HashMap; /// Configuration for how the scene should be cached. /// @@ -22,17 +22,17 @@ impl Default for PictureCacheStrategy { pub struct PictureCache { strategy: PictureCacheStrategy, /// Fast-path store for the default render variant (variant key = 0). - default_store: HashMap, + default_store: NodeIdHashMap, /// Store for non-default render variants (variant key != 0). - variant_store: HashMap<(NodeId, u64), Picture>, + variant_store: NodeIdHashMap<(NodeId, u64), Picture>, } impl PictureCache { pub fn new() -> Self { Self { strategy: PictureCacheStrategy::default(), - default_store: HashMap::new(), - variant_store: HashMap::new(), + default_store: new_node_id_map(), + variant_store: new_node_id_map(), } } diff --git a/crates/grida-canvas/src/cache/scene.rs b/crates/grida-canvas/src/cache/scene.rs index c9bdda7cad..a4070d31d1 100644 --- a/crates/grida-canvas/src/cache/scene.rs +++ b/crates/grida-canvas/src/cache/scene.rs @@ -98,13 +98,24 @@ impl SceneCache { self.layers .layers .sort_by_key(|entry| entry.layer.z_index()); - self.layer_index = RTree::new(); - for (i, entry) in self.layers.layers.iter().enumerate() { - if let Some(rb) = self.geometry.get_render_bounds(&entry.id) { - let bounds = AABB::from_corners([rb.x, rb.y], [rb.x + rb.width, rb.y + rb.height]); - self.layer_index.insert(IndexedLayer { index: i, bounds }); - } - } + let items: Vec = self + .layers + .layers + .iter() + .enumerate() + .filter_map(|(i, entry)| { + self.geometry.get_render_bounds(&entry.id).map(|rb| { + IndexedLayer { + index: i, + bounds: AABB::from_corners( + [rb.x, rb.y], + [rb.x + rb.width, rb.y + rb.height], + ), + } + }) + }) + .collect(); + self.layer_index = RTree::bulk_load(items); } /// Access the geometry cache. @@ -191,6 +202,17 @@ impl SceneCache { .collect() } + /// Return the bounding envelope of all scene content in the R-tree. + /// + /// O(1) — reads the cached root node envelope. Returns `None` when + /// the scene is empty (no layers indexed). + pub fn scene_envelope(&self) -> Option> { + if self.layer_index.size() == 0 { + return None; + } + Some(self.layer_index.root().envelope()) + } + /// Query painter layer indices whose bounds contain the given point. pub fn intersects_point(&self, point: Vector2) -> Vec { let env = AABB::from_point([point[0], point[1]]); diff --git a/crates/grida-canvas/src/cache/vector_path.rs b/crates/grida-canvas/src/cache/vector_path.rs index 1f09c49966..ff32b989c7 100644 --- a/crates/grida-canvas/src/cache/vector_path.rs +++ b/crates/grida-canvas/src/cache/vector_path.rs @@ -1,7 +1,7 @@ +use crate::cache::fast_hash::{new_node_id_map, NodeIdHashMap}; use crate::node::schema::NodeId; use skia_safe::Path; use std::collections::hash_map::DefaultHasher; -use std::collections::HashMap; use std::hash::{Hash, Hasher}; use std::rc::Rc; @@ -13,13 +13,13 @@ pub struct VectorPathCacheEntry { #[derive(Default, Clone, Debug)] pub struct VectorPathCache { - entries: HashMap, + entries: NodeIdHashMap, } impl VectorPathCache { pub fn new() -> Self { Self { - entries: HashMap::new(), + entries: new_node_id_map(), } } diff --git a/crates/grida-canvas/src/cg/types.rs b/crates/grida-canvas/src/cg/types.rs index 6ae4b93426..f2f561eb95 100644 --- a/crates/grida-canvas/src/cg/types.rs +++ b/crates/grida-canvas/src/cg/types.rs @@ -2110,7 +2110,6 @@ impl FromIterator for Paints { // Support for array literals - much more ergonomic than vec![] impl From<[Paint; N]> for Paints { fn from(value: [Paint; N]) -> Self { - // Most efficient: direct construction without intermediate allocations Paints { paints: value.to_vec(), } @@ -2120,7 +2119,6 @@ impl From<[Paint; N]> for Paints { // Support for single Paint conversion impl From for Paints { fn from(value: Paint) -> Self { - // More efficient: avoid the intermediate Vec allocation Paints { paints: vec![value], } diff --git a/crates/grida-canvas/src/hittest/hit_tester.rs b/crates/grida-canvas/src/hittest/hit_tester.rs index 3f28afa84d..3c0a0b2483 100644 --- a/crates/grida-canvas/src/hittest/hit_tester.rs +++ b/crates/grida-canvas/src/hittest/hit_tester.rs @@ -278,4 +278,69 @@ impl<'a> HitTester<'a> { } out } + + /// Returns the shallowest nodes whose bounding boxes intersect `rect`. + /// + /// Like [`intersects`], but when a node and one of its descendants + /// both match, only the ancestor is returned. The result contains no + /// node whose ancestor is also in the result. + /// + /// Layers are processed in ascending z-index order (parents before + /// children in the paint tree). Once a node is accepted, all of its + /// descendants are skipped with a single parent lookup each, making + /// the pruning effectively O(K) amortized. + pub fn intersects_topmost(&self, rect: &Rectangle) -> Vec { + let mut indices = self.cache.intersects(*rect); + if indices.is_empty() { + return Vec::new(); + } + indices.sort(); + + // Set of selected node IDs — used for O(1) ancestor lookups. + let mut selected_set = + std::collections::HashSet::with_capacity(indices.len().min(256)); + let mut out = Vec::with_capacity(indices.len().min(256)); + + let center_point = [rect.x + rect.width / 2.0, rect.y + rect.height / 2.0]; + + // Ascending z-index order: parents appear before their children. + for idx in indices { + let entry = &self.cache.layers.layers[idx]; + let id = &entry.id; + + // Quick check: does any ancestor already cover this node? + if self.has_selected_ancestor(id, &selected_set) { + continue; + } + + // Bounds + clip check (same as `intersects`) + if let Some(bounds) = self.cache.geometry.get_world_bounds(id) { + if rect::intersects(&bounds, rect) { + if self.is_point_within_parent_clip_bounds(id, center_point) { + selected_set.insert(*id); + out.push(*id); + } + } + } + } + out + } + + /// Walk up the parent chain and return `true` if any ancestor is in + /// `selected`. Stops as soon as a match is found — amortized O(1) + /// when parents are typically selected before their children. + fn has_selected_ancestor( + &self, + id: &NodeId, + selected: &std::collections::HashSet, + ) -> bool { + let mut current = self.cache.geometry.get_parent(id); + while let Some(parent) = current { + if selected.contains(&parent) { + return true; + } + current = self.cache.geometry.get_parent(&parent); + } + false + } } diff --git a/crates/grida-canvas/src/io/io_grida_fbs.rs b/crates/grida-canvas/src/io/io_grida_fbs.rs index 4c68a73a8c..b9119acb35 100644 --- a/crates/grida-canvas/src/io/io_grida_fbs.rs +++ b/crates/grida-canvas/src/io/io_grida_fbs.rs @@ -17,6 +17,7 @@ use std::collections::HashMap; use math2::{box_fit::BoxFit, transform::AffineTransform}; +use rustc_hash::FxHashMap; /// Schema version emitted by the Rust FlatBuffers writer. /// @@ -34,19 +35,18 @@ use crate::cg::{ stroke_dasharray::StrokeDashArray, stroke_width::{RectangularStrokeWidth, SingularStrokeWidth, StrokeWidth}, tilemode::TileMode, - varwidth, types::{ Axis, BlendMode, BooleanPathOperation, CGPoint, ContainerClipFlag, CornerSmoothing, CrossAxisAlignment, DiamondGradientPaint, EdgeInsets, FontFeature, FontOpticalSizing, FontVariation, FontWeight, GradientStop, ImageFilters, ImagePaint, ImagePaintFit, - ImageRepeat, ImageTile, - LayerBlendMode, LayerMaskType, LayoutGap, LayoutMode, LayoutPositioning, LayoutWrap, - LinearGradientPaint, MainAxisAlignment, Paint, Paints, RadialGradientPaint, - RectangularCornerRadius, ResourceRef, SolidPaint, StrokeAlign, StrokeCap, StrokeJoin, - StrokeMarkerPreset, StrokeMiterLimit, SweepGradientPaint, TextAlign, TextAlignVertical, - TextDecorationLine, TextDecorationRec, TextDecorationStyle, TextLetterSpacing, - TextLineHeight, TextStyleRec, TextTransform, TextWordSpacing, + ImageRepeat, ImageTile, LayerBlendMode, LayerMaskType, LayoutGap, LayoutMode, + LayoutPositioning, LayoutWrap, LinearGradientPaint, MainAxisAlignment, Paint, Paints, + RadialGradientPaint, RectangularCornerRadius, ResourceRef, SolidPaint, StrokeAlign, + StrokeCap, StrokeJoin, StrokeMarkerPreset, StrokeMiterLimit, SweepGradientPaint, TextAlign, + TextAlignVertical, TextDecorationLine, TextDecorationRec, TextDecorationStyle, + TextLetterSpacing, TextLineHeight, TextStyleRec, TextTransform, TextWordSpacing, }, + varwidth, }; use crate::node::{ id::NodeIdGenerator, @@ -54,9 +54,9 @@ use crate::node::{ schema::{ BooleanPathOperationNodeRec, ContainerNodeRec, EllipseNodeRec, GroupNodeRec, InitialContainerNodeRec, LayerEffects, LayoutChildStyle, LayoutContainerStyle, - LayoutDimensionStyle, LayoutPositioningBasis, LineNodeRec, Node, RectangleNodeRec, - RegularPolygonNodeRec, RegularStarPolygonNodeRec, Scene, Size, StrokeStyle, - PathNodeRec, TextSpanNodeRec, VectorNodeRec, + LayoutDimensionStyle, LayoutPositioningBasis, LineNodeRec, Node, PathNodeRec, + RectangleNodeRec, RegularPolygonNodeRec, RegularStarPolygonNodeRec, Scene, Size, + StrokeStyle, TextSpanNodeRec, VectorNodeRec, }, }; use crate::vectornetwork::{ @@ -146,14 +146,16 @@ fn decode_layer_common(sys: &fbs::SystemNodeTrait<'_>, layer: &fbs::LayerTrait<' fn decode_shape_layout(layer: &fbs::LayerTrait<'_>, cos_sin: (f32, f32)) -> ShapeLayout { let layout = layer.layout(); let (x, y) = layout.as_ref().map(decode_layout_xy).unwrap_or((0.0, 0.0)); - let (w, h) = layout.as_ref().map(decode_dimensions).unwrap_or((None, None)); + let (w, h) = layout + .as_ref() + .map(decode_dimensions) + .unwrap_or((None, None)); let size = Size { width: w.unwrap_or(0.0), height: h.unwrap_or(0.0), }; let (cos, sin) = cos_sin; - let transform = - AffineTransform::from_box_center_raw(x, y, size.width, size.height, cos, sin); + let transform = AffineTransform::from_box_center_raw(x, y, size.width, size.height, cos, sin); ShapeLayout { x, y, @@ -232,10 +234,18 @@ fn decode_all_inner(bytes: &[u8]) -> Result { } // ── 2. Decode all node slots ───────────────────────────────────────────── + // + // The decode pipeline is structured to minimize heap allocations: + // + // Phase 2a: Decode FBS → NodeEntry (one String per ID, no cloning) + // Phase 2b: Assign NodeId in the same loop → build string_to_internal_id + // Phase 3: Build children_by_parent using NodeId keys (no String cloning) + // Phase 4: Single-pass consume node_entries → node_pairs + node_names + position_map struct NodeEntry { - id: String, - parent: Option<(String, String)>, // (parent_id, fractional-index position) + internal_id: crate::node::id::NodeId, + parent_internal: Option, + position: Option, node: Node, name: Option, } @@ -247,24 +257,53 @@ fn decode_all_inner(bytes: &[u8]) -> Result { background_color: Option, } - let mut node_entries: Vec = Vec::new(); - let mut scene_metas: HashMap = HashMap::new(); + // Pre-allocate based on estimated node count. + let estimated_count = document.nodes().map(|v| v.len()).unwrap_or(0); + let mut string_to_internal_id: FxHashMap = + FxHashMap::with_capacity_and_hasher(estimated_count, Default::default()); + let mut id_generator = NodeIdGenerator::new(); + let mut node_entries: Vec = Vec::with_capacity(estimated_count); + let mut scene_metas: FxHashMap = + FxHashMap::default(); + + // Helper: get or assign an internal NodeId for a string ID. + // Uses the shared id_generator and string_to_internal_id map. + let mut get_or_assign_id = |s: String| -> crate::node::id::NodeId { + *string_to_internal_id + .entry(s) + .or_insert_with(|| id_generator.next()) + }; - /// Helper macro: every layer-bearing node type follows the same pattern of - /// extracting `sys`, `layer`, `id`, `parent` from the slot, then calling a - /// decoder. This macro eliminates that boilerplate and makes it impossible - /// to forget any step. + /// Helper macro: decode a layer-bearing node. Assigns internal IDs + /// immediately during the hot loop — no deferred string cloning needed. macro_rules! decode_layer_node { ($slot:expr, $accessor:ident, $decode_fn:expr) => { if let Some(typed) = $slot.$accessor() { let sys = typed.node(); let layer = typed.layer(); - let id = sys.id().id().to_owned(); + let string_id = sys.id().id().to_owned(); let name = sys.name().map(|s| s.to_owned()); - let parent = decode_parent_ref(&layer); + let (parent_internal, position) = { + let parent_ref = layer.parent(); + let parent_str = parent_ref.parent_id().id(); + if parent_str.is_empty() { + (None, None) + } else { + let parent_nid = get_or_assign_id(parent_str.to_owned()); + let pos = parent_ref.position().unwrap_or("").to_owned(); + (Some(parent_nid), Some(pos)) + } + }; + let internal_id = get_or_assign_id(string_id); let lc = decode_layer_common(&sys, &layer); let node = $decode_fn(&lc, &layer, &typed); - node_entries.push(NodeEntry { id, parent, node, name }); + node_entries.push(NodeEntry { + internal_id, + parent_internal, + position, + node, + name, + }); } }; } @@ -278,9 +317,8 @@ fn decode_all_inner(bytes: &[u8]) -> Result { let sys = sn.node(); let id = sys.id().id().to_owned(); let name = sys.name().unwrap_or("").to_owned(); - let bg = sn - .scene_background_color() - .map(decode_rgba32f_to_cg_color); + let bg = sn.scene_background_color().map(decode_rgba32f_to_cg_color); + get_or_assign_id(id.clone()); scene_metas.insert( id.clone(), SceneMeta { @@ -331,97 +369,126 @@ fn decode_all_inner(bytes: &[u8]) -> Result { } } - // ── 3. Build ID mapping (string → internal NodeId) ─────────────────────── - let mut string_to_internal_id: HashMap = HashMap::new(); - let mut id_generator = NodeIdGenerator::new(); - + // ── 3. Build children_by_parent using NodeId keys (no String cloning) ──── + // + // Previously this was HashMap> which cloned + // every node ID and position string. Now uses u64 NodeId keys directly. + let mut children_by_parent: FxHashMap< + crate::node::id::NodeId, + Vec<(crate::node::id::NodeId, &str)>, + > = FxHashMap::with_capacity_and_hasher(estimated_count / 4, Default::default()); for e in &node_entries { - string_to_internal_id - .entry(e.id.clone()) - .or_insert_with(|| id_generator.next()); - } - for (sid, _) in &scene_metas { - string_to_internal_id - .entry(sid.clone()) - .or_insert_with(|| id_generator.next()); - } - - let get_id = |s: &String| string_to_internal_id.get(s).copied(); - - // ── 4. Build children_by_parent (sorted by fractional index) ───────────── - let mut children_by_parent: HashMap> = HashMap::new(); - for e in &node_entries { - if let Some((parent_id, position)) = &e.parent { + if let Some(parent_nid) = e.parent_internal { + let pos = e.position.as_deref().unwrap_or(""); children_by_parent - .entry(parent_id.clone()) + .entry(parent_nid) .or_default() - .push((e.id.clone(), position.clone())); + .push((e.internal_id, pos)); } } for children in children_by_parent.values_mut() { children.sort_by(|a, b| a.1.cmp(&b.1)); } - let node_pairs: Vec<_> = node_entries - .iter() - .filter_map(|e| Some((get_id(&e.id)?, e.node.clone()))) - .collect(); - - let node_names: Vec<_> = node_entries - .iter() - .filter_map(|e| { - let id = get_id(&e.id)?; - let name = e.name.clone()?; - Some((id, name)) - }) - .collect(); - + // Build internal_links from children_by_parent BEFORE consuming + // node_entries, since children_by_parent borrows &str from node_entries. let internal_links: HashMap<_, Vec<_>> = children_by_parent - .iter() - .filter_map(|(parent_str, children)| { - let parent_internal = get_id(parent_str)?; - let child_internals: Vec<_> = children - .iter() - .filter_map(|(child_str, _)| get_id(child_str)) + .into_iter() + .map(|(parent_nid, children)| { + let child_ids: Vec<_> = children + .into_iter() + .map(|(child_nid, _)| child_nid) .collect(); - if child_internals.is_empty() { - None - } else { - Some((parent_internal, child_internals)) - } + (parent_nid, child_ids) }) .collect(); + // ── 4. Single-pass consume: node_pairs + node_names + position_map ─────── + // + // Moves nodes out of node_entries — no cloning of Node enums. + let mut node_pairs: Vec<(crate::node::id::NodeId, Node)> = + Vec::with_capacity(node_entries.len()); + let mut node_names: Vec<(crate::node::id::NodeId, String)> = Vec::new(); + let mut position_map: FxHashMap = + FxHashMap::with_capacity_and_hasher(node_entries.len(), Default::default()); + + for e in node_entries.into_iter() { + if let Some(pos) = e.position { + position_map.insert(e.internal_id, pos); + } + if let Some(name) = e.name { + node_names.push((e.internal_id, name)); + } + node_pairs.push((e.internal_id, e.node)); + } + // ── 5. Produce one Scene per listed scene id ────────────────────────────── + // + // For each scene we need a SceneGraph built from the full node set. + // The last scene consumes node_pairs/internal_links by move to avoid + // a deep clone of all Node enums. Earlier scenes (multi-scene files) + // must clone. Single-scene files (the common case) get zero clones. + // + // TODO: Each SceneGraph currently receives ALL document nodes, not just + // the nodes reachable from that scene's roots. This causes unnecessary + // extraction (geo_data, layer_core) and storage for orphan nodes. + // Future: compute reachability per scene from `internal_links` + roots, + // then pass only reachable nodes to `new_from_snapshot`. let mut scenes: Vec = Vec::new(); - let iter: Box> = if !scene_ids_ordered.is_empty() { - Box::new(scene_ids_ordered.iter()) + // Helper: resolve scene root NodeIds from the scene string ID. + let get_scene_roots = |scene_str: &str| -> Vec { + let scene_nid = string_to_internal_id.get(scene_str).copied(); + scene_nid + .and_then(|nid| internal_links.get(&nid)) + .cloned() + .unwrap_or_default() + }; + + let scene_id_strs: Vec<&String> = if !scene_ids_ordered.is_empty() { + scene_ids_ordered.iter().collect() } else { - Box::new(scene_metas.keys()) + scene_metas.keys().collect() }; + let scene_count = scene_id_strs.len(); + + // Handle all scenes except the last one (if multi-scene) with clones. + if scene_count > 1 { + for scene_id_str in &scene_id_strs[..scene_count - 1] { + let meta = scene_metas.get(*scene_id_str); + let name = meta.map(|m| m.name.clone()).unwrap_or_default(); + let background_color = meta.and_then(|m| m.background_color); + + let roots_internal = get_scene_roots(scene_id_str); - for scene_id_str in iter { - let meta = scene_metas.get(scene_id_str); + let mut graph = SceneGraph::new_from_snapshot( + node_pairs.clone(), + internal_links.clone(), + roots_internal, + ); + + for (id, name) in &node_names { + graph.set_name(*id, name.clone()); + } + + scenes.push(Scene { + name, + graph, + background_color, + }); + } + } + + // Handle the last (or only) scene by consuming node_pairs — zero clones. + if let Some(scene_id_str) = scene_id_strs.last() { + let meta = scene_metas.get(*scene_id_str); let name = meta.map(|m| m.name.clone()).unwrap_or_default(); let background_color = meta.and_then(|m| m.background_color); - let roots_strings = children_by_parent - .get(scene_id_str) - .cloned() - .unwrap_or_default(); - let roots_internal: Vec<_> = roots_strings - .iter() - .filter_map(|(child_str, _)| get_id(child_str)) - .collect(); + let roots_internal = get_scene_roots(scene_id_str); - let mut graph = SceneGraph::new_from_snapshot( - node_pairs.clone(), - internal_links.clone(), - roots_internal, - ); + let mut graph = SceneGraph::new_from_snapshot(node_pairs, internal_links, roots_internal); - // Preserve node display names for (id, name) in &node_names { graph.set_name(*id, name.clone()); } @@ -439,22 +506,11 @@ fn decode_all_inner(bytes: &[u8]) -> Result { .map(|(s, &nid)| (nid, s.clone())) .collect(); - // Build the position map (internal NodeId → original position string) - // so the encoder can preserve child ordering exactly. - let mut position_map: HashMap = HashMap::new(); - for e in &node_entries { - if let Some((_parent_id, position)) = &e.parent { - if let Some(&nid) = string_to_internal_id.get(&e.id) { - position_map.insert(nid, position.clone()); - } - } - } - Ok(DecodeResult { scenes, id_map, scene_ids: scene_ids_ordered, - position_map, + position_map: position_map.into_iter().collect(), }) } @@ -462,15 +518,8 @@ fn decode_all_inner(bytes: &[u8]) -> Result { // Hierarchy helpers // ───────────────────────────────────────────────────────────────────────────── -fn decode_parent_ref(layer: &fbs::LayerTrait<'_>) -> Option<(String, String)> { - let parent_ref = layer.parent(); - let parent_id = parent_ref.parent_id().id().to_owned(); - if parent_id.is_empty() { - return None; - } - let position = parent_ref.position().unwrap_or("").to_owned(); - Some((parent_id, position)) -} +// decode_parent_ref removed — parent decoding is now inline in the +// decode_layer_node! macro using get_or_assign_id for zero-clone IDs. // ───────────────────────────────────────────────────────────────────────────── // Color helpers @@ -665,15 +714,18 @@ fn decode_paint_item(item: &fbs::PaintStackItem<'_>) -> Option { fit, opacity: ip.opacity(), blend_mode: decode_blend_mode(ip.blend_mode()), - filters: ip.filters().map(|f| ImageFilters { - exposure: f.exposure(), - contrast: f.contrast(), - saturation: f.saturation(), - temperature: f.temperature(), - tint: f.tint(), - highlights: f.highlights(), - shadows: f.shadows(), - }).unwrap_or_default(), + filters: ip + .filters() + .map(|f| ImageFilters { + exposure: f.exposure(), + contrast: f.contrast(), + saturation: f.saturation(), + temperature: f.temperature(), + tint: f.tint(), + highlights: f.highlights(), + shadows: f.shadows(), + }) + .unwrap_or_default(), })) } _ => None, @@ -864,26 +916,62 @@ fn decode_fe_shadow(s: &fbs::FeShadow<'_>) -> FeShadow { dy: s.dy(), blur: s.blur(), spread: s.spread(), - color: s.color().map(decode_rgba32f_to_cg_color).unwrap_or(CGColor { r: 0, g: 0, b: 0, a: 64 }), + color: s + .color() + .map(decode_rgba32f_to_cg_color) + .unwrap_or(CGColor { + r: 0, + g: 0, + b: 0, + a: 64, + }), active: s.active(), } } fn decode_fe_noise(ne: &fbs::FeNoiseEffect<'_>) -> FeNoiseEffect { - const DEFAULT_MONO: CGColor = CGColor { r: 0, g: 0, b: 0, a: 64 }; - let default_mono = || NoiseEffectColors::Mono { color: DEFAULT_MONO }; + const DEFAULT_MONO: CGColor = CGColor { + r: 0, + g: 0, + b: 0, + a: 64, + }; + let default_mono = || NoiseEffectColors::Mono { + color: DEFAULT_MONO, + }; let coloring = ne .coloring() .map(|c| match c.kind() { fbs::NoiseEffectColorsKind::Mono => NoiseEffectColors::Mono { - color: c.mono_color().map(decode_rgba32f_to_cg_color).unwrap_or(DEFAULT_MONO), + color: c + .mono_color() + .map(decode_rgba32f_to_cg_color) + .unwrap_or(DEFAULT_MONO), }, fbs::NoiseEffectColorsKind::Duo => NoiseEffectColors::Duo { - color1: c.duo_color1().map(decode_rgba32f_to_cg_color).unwrap_or(CGColor { r: 0, g: 0, b: 0, a: 128 }), - color2: c.duo_color2().map(decode_rgba32f_to_cg_color).unwrap_or(CGColor { r: 255, g: 255, b: 255, a: 128 }), + color1: c + .duo_color1() + .map(decode_rgba32f_to_cg_color) + .unwrap_or(CGColor { + r: 0, + g: 0, + b: 0, + a: 128, + }), + color2: c + .duo_color2() + .map(decode_rgba32f_to_cg_color) + .unwrap_or(CGColor { + r: 255, + g: 255, + b: 255, + a: 128, + }), + }, + fbs::NoiseEffectColorsKind::Multi => NoiseEffectColors::Multi { + opacity: c.multi_opacity(), }, - fbs::NoiseEffectColorsKind::Multi => NoiseEffectColors::Multi { opacity: c.multi_opacity() }, _ => default_mono(), }) .unwrap_or_else(default_mono); @@ -1022,7 +1110,9 @@ fn decode_rectangular_stroke_geometry( // Corner radius / smoothing // ───────────────────────────────────────────────────────────────────────────── -fn decode_corner_radius(cr: Option>) -> RectangularCornerRadius { +fn decode_corner_radius( + cr: Option>, +) -> RectangularCornerRadius { use crate::cg::types::Radius; match cr.and_then(|t| t.rectangular_corner_radius()) { Some(rcr) => RectangularCornerRadius { @@ -1245,7 +1335,14 @@ fn decode_vector_network(vnd: Option>) -> VectorNetwo let vertices: Vec<(f32, f32)> = vnd .vertices() - .map(|v| (0..v.len()).map(|i| { let p = v.get(i); (p.x(), p.y()) }).collect()) + .map(|v| { + (0..v.len()) + .map(|i| { + let p = v.get(i); + (p.x(), p.y()) + }) + .collect() + }) .unwrap_or_default(); let segments: Vec = vnd @@ -1347,9 +1444,18 @@ fn decode_container_node( cn: &fbs::ContainerNode<'_>, ) -> Node { let layout = layer.layout(); - let position = layout.as_ref().map(decode_layout_position).unwrap_or_default(); - let layout_container = layout.as_ref().map(decode_layout_container_style).unwrap_or_default(); - let layout_dimensions = layout.as_ref().map(decode_layout_dimension_style).unwrap_or_default(); + let position = layout + .as_ref() + .map(decode_layout_position) + .unwrap_or_default(); + let layout_container = layout + .as_ref() + .map(decode_layout_container_style) + .unwrap_or_default(); + let layout_dimensions = layout + .as_ref() + .map(decode_layout_dimension_style) + .unwrap_or_default(); let (stroke_style, stroke_width) = decode_rectangular_stroke_geometry(cn.stroke_geometry()); Node::Container(ContainerNodeRec { @@ -1379,14 +1485,21 @@ fn decode_initial_container_node( _icn: &fbs::InitialContainerNode<'_>, ) -> Node { let layout = layer.layout(); - let lcs = layout.as_ref().map(decode_layout_container_style).unwrap_or_default(); + let lcs = layout + .as_ref() + .map(decode_layout_container_style) + .unwrap_or_default(); Node::InitialContainer(InitialContainerNodeRec { active: lc.active, layout_mode: lcs.layout_mode, layout_direction: lcs.layout_direction, layout_wrap: lcs.layout_wrap.unwrap_or(LayoutWrap::NoWrap), - layout_main_axis_alignment: lcs.layout_main_axis_alignment.unwrap_or(MainAxisAlignment::Start), - layout_cross_axis_alignment: lcs.layout_cross_axis_alignment.unwrap_or(CrossAxisAlignment::Start), + layout_main_axis_alignment: lcs + .layout_main_axis_alignment + .unwrap_or(MainAxisAlignment::Start), + layout_cross_axis_alignment: lcs + .layout_cross_axis_alignment + .unwrap_or(CrossAxisAlignment::Start), padding: lcs.layout_padding.unwrap_or_default(), layout_gap: lcs.layout_gap.unwrap_or(LayoutGap { main_axis_gap: 0.0, @@ -1418,7 +1531,12 @@ fn decode_basic_shape_node( } } else { let r = Radius::circular(bsn.corner_radius()); - RectangularCornerRadius { tl: r, tr: r, bl: r, br: r } + RectangularCornerRadius { + tl: r, + tr: r, + bl: r, + br: r, + } } }; let stroke_style = decode_stroke_style_from_fbs(bsn.stroke_style()); @@ -1444,11 +1562,17 @@ fn decode_basic_shape_node( let bottom = rsw.stroke_bottom_width(); let left = rsw.stroke_left_width(); if top == right && right == bottom && bottom == left { - if top == 0.0 { StrokeWidth::None } else { StrokeWidth::Uniform(top) } + if top == 0.0 { + StrokeWidth::None + } else { + StrokeWidth::Uniform(top) + } } else { StrokeWidth::Rectangular(RectangularStrokeWidth { - stroke_top_width: top, stroke_right_width: right, - stroke_bottom_width: bottom, stroke_left_width: left, + stroke_top_width: top, + stroke_right_width: right, + stroke_bottom_width: bottom, + stroke_left_width: left, }) } } else if stroke_width_f32 == 0.0 { @@ -1493,7 +1617,11 @@ fn decode_basic_shape_node( angle, corner_radius: { let cr = bsn.corner_radius(); - if cr == 0.0 { None } else { Some(cr) } + if cr == 0.0 { + None + } else { + Some(cr) + } }, effects: lc.effects.clone(), layout_child: lc.layout_child.clone(), @@ -1585,7 +1713,11 @@ fn decode_vector_node( mask: lc.mask, transform: sl.transform, network: decode_vector_network(vn.vector_network_data()), - corner_radius: vn.corner_radius().and_then(|cr| cr.corner_radius()).map(|r| r.rx()).unwrap_or(0.0), + corner_radius: vn + .corner_radius() + .and_then(|cr| cr.corner_radius()) + .map(|r| r.rx()) + .unwrap_or(0.0), fills: decode_paints_vec(vn.fill_paints()), strokes: decode_paints_vec(vn.stroke_paints()), stroke_width: stroke_width_f32, @@ -1602,11 +1734,7 @@ fn decode_vector_node( }) } -fn decode_path_node( - lc: &LayerCommon, - layer: &fbs::LayerTrait<'_>, - pn: &fbs::PathNode<'_>, -) -> Node { +fn decode_path_node(lc: &LayerCommon, layer: &fbs::LayerTrait<'_>, pn: &fbs::PathNode<'_>) -> Node { let sl = decode_shape_layout(layer, lc.rotation_cos_sin); let (stroke_style, stroke_width_f32, _stroke_width_profile) = decode_stroke_geometry_trait(pn.stroke_geometry()); @@ -1627,11 +1755,7 @@ fn decode_path_node( }) } -fn decode_line_node( - lc: &LayerCommon, - layer: &fbs::LayerTrait<'_>, - ln: &fbs::LineNode<'_>, -) -> Node { +fn decode_line_node(lc: &LayerCommon, layer: &fbs::LayerTrait<'_>, ln: &fbs::LineNode<'_>) -> Node { let sl = decode_shape_layout(layer, lc.rotation_cos_sin); let sg = ln.stroke_geometry(); let stroke_width = sg.as_ref().map(|s| s.stroke_width()).unwrap_or(0.0); @@ -1645,14 +1769,11 @@ fn decode_line_node( .and_then(|s| s.stroke_style()) .map(|ss| StrokeMiterLimit(ss.stroke_miter_limit())) .unwrap_or_default(); - let stroke_dash_array = sg - .as_ref() - .and_then(|s| s.stroke_style()) - .and_then(|ss| { - ss.stroke_dash_array() - .filter(|v| v.len() > 0) - .map(|v| StrokeDashArray((0..v.len()).map(|i| v.get(i)).collect())) - }); + let stroke_dash_array = sg.as_ref().and_then(|s| s.stroke_style()).and_then(|ss| { + ss.stroke_dash_array() + .filter(|v| v.len() > 0) + .map(|v| StrokeDashArray((0..v.len()).map(|i| v.get(i)).collect())) + }); // Lines use translation + rotation (no center-origin) and height=0. // Use raw cos/sin to avoid lossy degree conversion. @@ -1752,50 +1873,72 @@ fn decode_text_span_node( .unwrap_or(FontOpticalSizing::Auto); rec.text_decoration = ts.text_decoration().map(|td| TextDecorationRec { text_decoration_line: decode_text_decoration_line(td.text_decoration_line()), - text_decoration_color: td.text_decoration_color().map(|c| decode_rgba32f_to_cg_color(c)), - text_decoration_style: Some(decode_text_decoration_style(td.text_decoration_style())), + text_decoration_color: td + .text_decoration_color() + .map(|c| decode_rgba32f_to_cg_color(c)), + text_decoration_style: Some(decode_text_decoration_style( + td.text_decoration_style(), + )), text_decoration_skip_ink: Some(td.text_decoration_skip_ink()), text_decoration_thickness: { let t = td.text_decoration_thickness(); - if t == 0.0 { None } else { Some(t) } + if t == 0.0 { + None + } else { + Some(t) + } }, }); - rec.letter_spacing = ts.letter_spacing().map(|td| { - match td.kind() { - fbs::TextDimensionKind::Factor => TextLetterSpacing::Factor(td.value().unwrap_or(0.0)), + rec.letter_spacing = ts + .letter_spacing() + .map(|td| match td.kind() { + fbs::TextDimensionKind::Factor => { + TextLetterSpacing::Factor(td.value().unwrap_or(0.0)) + } _ => TextLetterSpacing::Fixed(td.value().unwrap_or(0.0)), - } - }).unwrap_or_default(); - rec.word_spacing = ts.word_spacing().map(|td| { - match td.kind() { - fbs::TextDimensionKind::Factor => TextWordSpacing::Factor(td.value().unwrap_or(0.0)), + }) + .unwrap_or_default(); + rec.word_spacing = ts + .word_spacing() + .map(|td| match td.kind() { + fbs::TextDimensionKind::Factor => { + TextWordSpacing::Factor(td.value().unwrap_or(0.0)) + } _ => TextWordSpacing::Fixed(td.value().unwrap_or(0.0)), - } - }).unwrap_or_default(); - rec.line_height = ts.line_height().map(|td| { - match td.kind() { + }) + .unwrap_or_default(); + rec.line_height = ts + .line_height() + .map(|td| match td.kind() { fbs::TextDimensionKind::Normal => TextLineHeight::Normal, - fbs::TextDimensionKind::Factor => TextLineHeight::Factor(td.value().unwrap_or(1.0)), + fbs::TextDimensionKind::Factor => { + TextLineHeight::Factor(td.value().unwrap_or(1.0)) + } _ => TextLineHeight::Fixed(td.value().unwrap_or(0.0)), - } - }).unwrap_or_default(); + }) + .unwrap_or_default(); rec.font_features = ts.font_features().map(|ff| { - (0..ff.len()).filter_map(|i| { - let f = ff.get(i); - f.open_type_feature_tag().map(|tag| FontFeature { - tag: String::from_utf8_lossy(&[tag.a(), tag.b(), tag.c(), tag.d()]).into_owned(), - value: f.open_type_feature_value(), + (0..ff.len()) + .filter_map(|i| { + let f = ff.get(i); + f.open_type_feature_tag().map(|tag| FontFeature { + tag: String::from_utf8_lossy(&[tag.a(), tag.b(), tag.c(), tag.d()]) + .into_owned(), + value: f.open_type_feature_value(), + }) }) - }).collect() + .collect() }); rec.font_variations = ts.font_variations().map(|fv| { - (0..fv.len()).map(|i| { - let v = fv.get(i); - FontVariation { - axis: v.variation_axis().to_owned(), - value: v.variation_value(), - } - }).collect() + (0..fv.len()) + .map(|i| { + let v = fv.get(i); + FontVariation { + axis: v.variation_axis().to_owned(), + value: v.variation_value(), + } + }) + .collect() }); rec }) @@ -1841,8 +1984,17 @@ fn decode_text_span_node( text_style, text_align, text_align_vertical, - max_lines: props.as_ref().map(|p| p.max_lines()).and_then(|v| if v == 0 { None } else { Some(v as usize) }), - ellipsis: props.as_ref().and_then(|p| p.ellipsis()).map(|s| s.to_owned()), + max_lines: props.as_ref().map(|p| p.max_lines()).and_then(|v| { + if v == 0 { + None + } else { + Some(v as usize) + } + }), + ellipsis: props + .as_ref() + .and_then(|p| p.ellipsis()) + .map(|s| s.to_owned()), fills: fill_paints, strokes: stroke_paints, stroke_width, @@ -1906,7 +2058,9 @@ pub fn encode( let scene_id_str = fbb.create_string(scene_id); let scene_nid = fbs::NodeIdentifier::create( &mut fbb, - &fbs::NodeIdentifierArgs { id: Some(scene_id_str) }, + &fbs::NodeIdentifierArgs { + id: Some(scene_id_str), + }, ); let scenes_vec = fbb.create_vector(&[scene_nid]); @@ -1924,7 +2078,9 @@ pub fn encode( // ── 5. Build GridaFile root ───────────────────────────────────────────── let root = fbs::GridaFile::create( &mut fbb, - &fbs::GridaFileArgs { document: Some(doc) }, + &fbs::GridaFileArgs { + document: Some(doc), + }, ); fbb.finish(root, Some("GRID")); @@ -1990,7 +2146,9 @@ pub fn encode_multi( ); let root = fbs::GridaFile::create( &mut fbb, - &fbs::GridaFileArgs { document: Some(doc) }, + &fbs::GridaFileArgs { + document: Some(doc), + }, ); fbb.finish(root, Some("GRID")); fbb.finished_data().to_vec() @@ -2085,11 +2243,14 @@ fn encode_scene_node<'a, A: flatbuffers::Allocator + 'a>( let bg = scene.background_color.map(|c| encode_color_to_rgba32f(&c)); - let sn = fbs::SceneNode::create(fbb, &fbs::SceneNodeArgs { - node: Some(sys), - scene_background_color: bg.as_ref(), - ..Default::default() - }); + let sn = fbs::SceneNode::create( + fbb, + &fbs::SceneNodeArgs { + node: Some(sys), + scene_background_color: bg.as_ref(), + ..Default::default() + }, + ); make_node_slot(fbb, fbs::Node::SceneNode, sn.as_union_value()) } @@ -2215,8 +2376,12 @@ fn encode_layer_trait<'a, A: flatbuffers::Allocator + 'a>( ) -> flatbuffers::WIPOffset> { // Parent reference let parent_id_str = fbb.create_string(input.parent_id); - let parent_nid = - fbs::NodeIdentifier::create(fbb, &fbs::NodeIdentifierArgs { id: Some(parent_id_str) }); + let parent_nid = fbs::NodeIdentifier::create( + fbb, + &fbs::NodeIdentifierArgs { + id: Some(parent_id_str), + }, + ); let pos_str = fbb.create_string(input.position); let parent_ref = fbs::ParentReference::create( fbb, @@ -2306,7 +2471,10 @@ fn encode_layer_blend_mode(lbm: LayerBlendMode) -> fbs::LayerBlendMode { fn encode_mask_type<'a, A: flatbuffers::Allocator + 'a>( fbb: &mut flatbuffers::FlatBufferBuilder<'a, A>, mask: Option, -) -> (fbs::LayerMaskType, Option>) { +) -> ( + fbs::LayerMaskType, + Option>, +) { match mask { None => (fbs::LayerMaskType::NONE, None), Some(LayerMaskType::Image(imt)) => { @@ -2326,10 +2494,7 @@ fn encode_mask_type<'a, A: flatbuffers::Allocator + 'a>( ) } Some(LayerMaskType::Geometry) => { - let table = fbs::LayerMaskTypeGeometry::create( - fbb, - &fbs::LayerMaskTypeGeometryArgs {}, - ); + let table = fbs::LayerMaskTypeGeometry::create(fbb, &fbs::LayerMaskTypeGeometryArgs {}); ( fbs::LayerMaskType::LayerMaskTypeGeometry, Some(table.as_union_value()), @@ -2345,7 +2510,11 @@ fn encode_mask_type<'a, A: flatbuffers::Allocator + 'a>( fn encode_paints<'a, A: flatbuffers::Allocator + 'a>( fbb: &mut flatbuffers::FlatBufferBuilder<'a, A>, paints: &Paints, -) -> Option>>>> { +) -> Option< + flatbuffers::WIPOffset< + flatbuffers::Vector<'a, flatbuffers::ForwardsUOffset>>, + >, +> { if paints.is_empty() { return None; } @@ -2364,118 +2533,166 @@ fn encode_paint_item<'a, A: flatbuffers::Allocator + 'a>( match paint { Paint::Solid(sp) => { let color = encode_color_to_rgba32f(&sp.color); - let solid = fbs::SolidPaint::create(fbb, &fbs::SolidPaintArgs { - active: sp.active, - color: Some(&color), - blend_mode: encode_blend_mode(sp.blend_mode), - }); - Some(fbs::PaintStackItem::create(fbb, &fbs::PaintStackItemArgs { - paint_type: fbs::Paint::SolidPaint, - paint: Some(solid.as_union_value()), - })) + let solid = fbs::SolidPaint::create( + fbb, + &fbs::SolidPaintArgs { + active: sp.active, + color: Some(&color), + blend_mode: encode_blend_mode(sp.blend_mode), + }, + ); + Some(fbs::PaintStackItem::create( + fbb, + &fbs::PaintStackItemArgs { + paint_type: fbs::Paint::SolidPaint, + paint: Some(solid.as_union_value()), + }, + )) } Paint::LinearGradient(lg) => { let stops = encode_gradient_stops(fbb, &lg.stops); let xy1 = fbs::Alignment::new(lg.xy1.0, lg.xy1.1); let xy2 = fbs::Alignment::new(lg.xy2.0, lg.xy2.1); let transform = encode_affine_to_cg_transform(&lg.transform); - let lgp = fbs::LinearGradientPaint::create(fbb, &fbs::LinearGradientPaintArgs { - active: lg.active, - xy1: Some(&xy1), - xy2: Some(&xy2), - stops: Some(stops), - opacity: lg.opacity, - blend_mode: encode_blend_mode(lg.blend_mode), - transform: Some(&transform), - tile_mode: encode_tile_mode(lg.tile_mode), - }); - Some(fbs::PaintStackItem::create(fbb, &fbs::PaintStackItemArgs { - paint_type: fbs::Paint::LinearGradientPaint, - paint: Some(lgp.as_union_value()), - })) + let lgp = fbs::LinearGradientPaint::create( + fbb, + &fbs::LinearGradientPaintArgs { + active: lg.active, + xy1: Some(&xy1), + xy2: Some(&xy2), + stops: Some(stops), + opacity: lg.opacity, + blend_mode: encode_blend_mode(lg.blend_mode), + transform: Some(&transform), + tile_mode: encode_tile_mode(lg.tile_mode), + }, + ); + Some(fbs::PaintStackItem::create( + fbb, + &fbs::PaintStackItemArgs { + paint_type: fbs::Paint::LinearGradientPaint, + paint: Some(lgp.as_union_value()), + }, + )) } Paint::RadialGradient(rg) => { let stops = encode_gradient_stops(fbb, &rg.stops); let transform = encode_affine_to_cg_transform(&rg.transform); - let rgp = fbs::RadialGradientPaint::create(fbb, &fbs::RadialGradientPaintArgs { - active: rg.active, - stops: Some(stops), - opacity: rg.opacity, - blend_mode: encode_blend_mode(rg.blend_mode), - transform: Some(&transform), - tile_mode: encode_tile_mode(rg.tile_mode), - }); - Some(fbs::PaintStackItem::create(fbb, &fbs::PaintStackItemArgs { - paint_type: fbs::Paint::RadialGradientPaint, - paint: Some(rgp.as_union_value()), - })) + let rgp = fbs::RadialGradientPaint::create( + fbb, + &fbs::RadialGradientPaintArgs { + active: rg.active, + stops: Some(stops), + opacity: rg.opacity, + blend_mode: encode_blend_mode(rg.blend_mode), + transform: Some(&transform), + tile_mode: encode_tile_mode(rg.tile_mode), + }, + ); + Some(fbs::PaintStackItem::create( + fbb, + &fbs::PaintStackItemArgs { + paint_type: fbs::Paint::RadialGradientPaint, + paint: Some(rgp.as_union_value()), + }, + )) } Paint::SweepGradient(sg) => { let stops = encode_gradient_stops(fbb, &sg.stops); let transform = encode_affine_to_cg_transform(&sg.transform); - let sgp = fbs::SweepGradientPaint::create(fbb, &fbs::SweepGradientPaintArgs { - active: sg.active, - stops: Some(stops), - opacity: sg.opacity, - blend_mode: encode_blend_mode(sg.blend_mode), - transform: Some(&transform), - }); - Some(fbs::PaintStackItem::create(fbb, &fbs::PaintStackItemArgs { - paint_type: fbs::Paint::SweepGradientPaint, - paint: Some(sgp.as_union_value()), - })) + let sgp = fbs::SweepGradientPaint::create( + fbb, + &fbs::SweepGradientPaintArgs { + active: sg.active, + stops: Some(stops), + opacity: sg.opacity, + blend_mode: encode_blend_mode(sg.blend_mode), + transform: Some(&transform), + }, + ); + Some(fbs::PaintStackItem::create( + fbb, + &fbs::PaintStackItemArgs { + paint_type: fbs::Paint::SweepGradientPaint, + paint: Some(sgp.as_union_value()), + }, + )) } Paint::Image(ip) => { let image_ref_offset = match &ip.image { ResourceRef::HASH(h) => { let hash_str = fbb.create_string(h); - let href = fbs::ResourceRefHASH::create(fbb, &fbs::ResourceRefHASHArgs { hash: Some(hash_str) }); + let href = fbs::ResourceRefHASH::create( + fbb, + &fbs::ResourceRefHASHArgs { + hash: Some(hash_str), + }, + ); (fbs::ResourceRef::ResourceRefHASH, href.as_union_value()) } ResourceRef::RID(r) => { let rid_str = fbb.create_string(r); - let rref = fbs::ResourceRefRID::create(fbb, &fbs::ResourceRefRIDArgs { rid: Some(rid_str) }); + let rref = fbs::ResourceRefRID::create( + fbb, + &fbs::ResourceRefRIDArgs { rid: Some(rid_str) }, + ); (fbs::ResourceRef::ResourceRefRID, rref.as_union_value()) } }; let alignment = fbs::Alignment::new(ip.alignement.0, ip.alignement.1); let (fit_type, fit_value) = encode_image_paint_fit(fbb, &ip.fit); let fbs_filters = fbs::ImageFilters::new( - ip.filters.exposure, ip.filters.contrast, ip.filters.saturation, - ip.filters.temperature, ip.filters.tint, ip.filters.highlights, + ip.filters.exposure, + ip.filters.contrast, + ip.filters.saturation, + ip.filters.temperature, + ip.filters.tint, + ip.filters.highlights, ip.filters.shadows, ); - let ip_offset = fbs::ImagePaint::create(fbb, &fbs::ImagePaintArgs { - active: ip.active, - image_type: image_ref_offset.0, - image: Some(image_ref_offset.1), - quarter_turns: ip.quarter_turns, - alignement: Some(&alignment), - fit_type, - fit: Some(fit_value), - opacity: ip.opacity, - blend_mode: encode_blend_mode(ip.blend_mode), - filters: Some(&fbs_filters), - }); - Some(fbs::PaintStackItem::create(fbb, &fbs::PaintStackItemArgs { - paint_type: fbs::Paint::ImagePaint, - paint: Some(ip_offset.as_union_value()), - })) + let ip_offset = fbs::ImagePaint::create( + fbb, + &fbs::ImagePaintArgs { + active: ip.active, + image_type: image_ref_offset.0, + image: Some(image_ref_offset.1), + quarter_turns: ip.quarter_turns, + alignement: Some(&alignment), + fit_type, + fit: Some(fit_value), + opacity: ip.opacity, + blend_mode: encode_blend_mode(ip.blend_mode), + filters: Some(&fbs_filters), + }, + ); + Some(fbs::PaintStackItem::create( + fbb, + &fbs::PaintStackItemArgs { + paint_type: fbs::Paint::ImagePaint, + paint: Some(ip_offset.as_union_value()), + }, + )) } Paint::DiamondGradient(dg) => { let stops = encode_gradient_stops(fbb, &dg.stops); let transform = encode_affine_to_cg_transform(&dg.transform); - let dgp = fbs::DiamondGradientPaint::create(fbb, &fbs::DiamondGradientPaintArgs { - active: dg.active, - stops: Some(stops), - opacity: dg.opacity, - blend_mode: encode_blend_mode(dg.blend_mode), - transform: Some(&transform), - }); - Some(fbs::PaintStackItem::create(fbb, &fbs::PaintStackItemArgs { - paint_type: fbs::Paint::DiamondGradientPaint, - paint: Some(dgp.as_union_value()), - })) + let dgp = fbs::DiamondGradientPaint::create( + fbb, + &fbs::DiamondGradientPaintArgs { + active: dg.active, + stops: Some(stops), + opacity: dg.opacity, + blend_mode: encode_blend_mode(dg.blend_mode), + transform: Some(&transform), + }, + ); + Some(fbs::PaintStackItem::create( + fbb, + &fbs::PaintStackItemArgs { + paint_type: fbs::Paint::DiamondGradientPaint, + paint: Some(dgp.as_union_value()), + }, + )) } } } @@ -2483,16 +2700,32 @@ fn encode_paint_item<'a, A: flatbuffers::Allocator + 'a>( fn encode_image_paint_fit<'a, A: flatbuffers::Allocator + 'a>( fbb: &mut flatbuffers::FlatBufferBuilder<'a, A>, fit: &ImagePaintFit, -) -> (fbs::ImagePaintFit, flatbuffers::WIPOffset) { +) -> ( + fbs::ImagePaintFit, + flatbuffers::WIPOffset, +) { match fit { ImagePaintFit::Fit(box_fit) => { - let f = fbs::ImagePaintFitFit::create(fbb, &fbs::ImagePaintFitFitArgs { box_fit: encode_box_fit(*box_fit) }); + let f = fbs::ImagePaintFitFit::create( + fbb, + &fbs::ImagePaintFitFitArgs { + box_fit: encode_box_fit(*box_fit), + }, + ); (fbs::ImagePaintFit::ImagePaintFitFit, f.as_union_value()) } ImagePaintFit::Transform(t) => { let ct = encode_affine_to_cg_transform(t); - let f = fbs::ImagePaintFitTransform::create(fbb, &fbs::ImagePaintFitTransformArgs { transform: Some(&ct) }); - (fbs::ImagePaintFit::ImagePaintFitTransform, f.as_union_value()) + let f = fbs::ImagePaintFitTransform::create( + fbb, + &fbs::ImagePaintFitTransformArgs { + transform: Some(&ct), + }, + ); + ( + fbs::ImagePaintFit::ImagePaintFitTransform, + f.as_union_value(), + ) } ImagePaintFit::Tile(tile) => { let fbs_repeat = match tile.repeat { @@ -2501,7 +2734,12 @@ fn encode_image_paint_fit<'a, A: flatbuffers::Allocator + 'a>( crate::cg::types::ImageRepeat::Repeat => fbs::ImageRepeat::Repeat, }; let fbs_tile = fbs::ImageTile::new(tile.scale, fbs_repeat); - let f = fbs::ImagePaintFitTile::create(fbb, &fbs::ImagePaintFitTileArgs { tile: Some(&fbs_tile) }); + let f = fbs::ImagePaintFitTile::create( + fbb, + &fbs::ImagePaintFitTileArgs { + tile: Some(&fbs_tile), + }, + ); (fbs::ImagePaintFit::ImagePaintFitTile, f.as_union_value()) } } @@ -2542,15 +2780,20 @@ fn encode_text_dimension_from_letter_spacing<'a, A: flatbuffers::Allocator + 'a> ) -> Option>> { let (kind, value) = match ls { TextLetterSpacing::Fixed(v) => { - if *v == 0.0 { return None; } + if *v == 0.0 { + return None; + } (fbs::TextDimensionKind::Fixed, *v) } TextLetterSpacing::Factor(v) => (fbs::TextDimensionKind::Factor, *v), }; - Some(fbs::TextDimension::create(fbb, &fbs::TextDimensionArgs { - kind, - value: Some(value), - })) + Some(fbs::TextDimension::create( + fbb, + &fbs::TextDimensionArgs { + kind, + value: Some(value), + }, + )) } fn encode_text_dimension_from_word_spacing<'a, A: flatbuffers::Allocator + 'a>( @@ -2559,15 +2802,20 @@ fn encode_text_dimension_from_word_spacing<'a, A: flatbuffers::Allocator + 'a>( ) -> Option>> { let (kind, value) = match ws { TextWordSpacing::Fixed(v) => { - if *v == 0.0 { return None; } + if *v == 0.0 { + return None; + } (fbs::TextDimensionKind::Fixed, *v) } TextWordSpacing::Factor(v) => (fbs::TextDimensionKind::Factor, *v), }; - Some(fbs::TextDimension::create(fbb, &fbs::TextDimensionArgs { - kind, - value: Some(value), - })) + Some(fbs::TextDimension::create( + fbb, + &fbs::TextDimensionArgs { + kind, + value: Some(value), + }, + )) } fn encode_text_dimension_from_line_height<'a, A: flatbuffers::Allocator + 'a>( @@ -2576,14 +2824,20 @@ fn encode_text_dimension_from_line_height<'a, A: flatbuffers::Allocator + 'a>( ) -> Option>> { match lh { TextLineHeight::Normal => None, - TextLineHeight::Fixed(v) => Some(fbs::TextDimension::create(fbb, &fbs::TextDimensionArgs { - kind: fbs::TextDimensionKind::Fixed, - value: Some(*v), - })), - TextLineHeight::Factor(v) => Some(fbs::TextDimension::create(fbb, &fbs::TextDimensionArgs { - kind: fbs::TextDimensionKind::Factor, - value: Some(*v), - })), + TextLineHeight::Fixed(v) => Some(fbs::TextDimension::create( + fbb, + &fbs::TextDimensionArgs { + kind: fbs::TextDimensionKind::Fixed, + value: Some(*v), + }, + )), + TextLineHeight::Factor(v) => Some(fbs::TextDimension::create( + fbb, + &fbs::TextDimensionArgs { + kind: fbs::TextDimensionKind::Factor, + value: Some(*v), + }, + )), } } @@ -2595,7 +2849,10 @@ fn encode_text_dimension_from_line_height<'a, A: flatbuffers::Allocator + 'a>( fn encode_fe_blur<'a, A: flatbuffers::Allocator + 'a>( fbb: &mut flatbuffers::FlatBufferBuilder<'a, A>, blur: &FeBlur, -) -> (fbs::FeBlur, flatbuffers::WIPOffset) { +) -> ( + fbs::FeBlur, + flatbuffers::WIPOffset, +) { match blur { FeBlur::Gaussian(g) => { let offset = @@ -2634,55 +2891,75 @@ fn encode_layer_effects<'a, A: flatbuffers::Allocator + 'a>( let blur_offset = effects.blur.as_ref().map(|lb| { let (blur_type, blur_union) = encode_fe_blur(fbb, &lb.blur); - fbs::FeLayerBlur::create(fbb, &fbs::FeLayerBlurArgs { - active: lb.active, - blur_type, - blur: Some(blur_union), - }) + fbs::FeLayerBlur::create( + fbb, + &fbs::FeLayerBlurArgs { + active: lb.active, + blur_type, + blur: Some(blur_union), + }, + ) }); let backdrop_blur_offset = effects.backdrop_blur.as_ref().map(|bb| { let (blur_type, blur_union) = encode_fe_blur(fbb, &bb.blur); - fbs::FeBackdropBlur::create(fbb, &fbs::FeBackdropBlurArgs { - active: bb.active, - blur_type, - blur: Some(blur_union), - }) + fbs::FeBackdropBlur::create( + fbb, + &fbs::FeBackdropBlurArgs { + active: bb.active, + blur_type, + blur: Some(blur_union), + }, + ) }); let shadows_offset = if effects.shadows.is_empty() { None } else { - let shadow_items: Vec<_> = effects.shadows.iter().map(|s| encode_filter_shadow_effect(fbb, s)).collect(); + let shadow_items: Vec<_> = effects + .shadows + .iter() + .map(|s| encode_filter_shadow_effect(fbb, s)) + .collect(); Some(fbb.create_vector(&shadow_items)) }; let glass_offset = effects.glass.as_ref().map(|lg| { - fbs::FeLiquidGlass::create(fbb, &fbs::FeLiquidGlassArgs { - active: lg.active, - light_intensity: lg.light_intensity, - light_angle: lg.light_angle, - refraction: lg.refraction, - depth: lg.depth, - dispersion: lg.dispersion, - blur_radius: lg.blur_radius, - }) + fbs::FeLiquidGlass::create( + fbb, + &fbs::FeLiquidGlassArgs { + active: lg.active, + light_intensity: lg.light_intensity, + light_angle: lg.light_angle, + refraction: lg.refraction, + depth: lg.depth, + dispersion: lg.dispersion, + blur_radius: lg.blur_radius, + }, + ) }); let noises_offset = if effects.noises.is_empty() { None } else { - let noise_items: Vec<_> = effects.noises.iter().map(|n| encode_fe_noise_effect(fbb, n)).collect(); + let noise_items: Vec<_> = effects + .noises + .iter() + .map(|n| encode_fe_noise_effect(fbb, n)) + .collect(); Some(fbb.create_vector(&noise_items)) }; - Some(fbs::LayerEffects::create(fbb, &fbs::LayerEffectsArgs { - fe_blur: blur_offset, - fe_backdrop_blur: backdrop_blur_offset, - fe_shadows: shadows_offset, - fe_glass: glass_offset, - fe_noises: noises_offset, - })) + Some(fbs::LayerEffects::create( + fbb, + &fbs::LayerEffectsArgs { + fe_blur: blur_offset, + fe_backdrop_blur: backdrop_blur_offset, + fe_shadows: shadows_offset, + fe_glass: glass_offset, + fe_noises: noises_offset, + }, + )) } fn encode_filter_shadow_effect<'a, A: flatbuffers::Allocator + 'a>( @@ -2694,13 +2971,24 @@ fn encode_filter_shadow_effect<'a, A: flatbuffers::Allocator + 'a>( FilterShadowEffect::InnerShadow(s) => (fbs::FilterShadowEffectKind::InnerShadow, s), }; let color = encode_color_to_rgba32f(&shadow.color); - let shadow_offset = fbs::FeShadow::create(fbb, &fbs::FeShadowArgs { - active: shadow.active, dx: shadow.dx, dy: shadow.dy, - blur: shadow.blur, spread: shadow.spread, color: Some(&color), - }); - fbs::FilterShadowEffect::create(fbb, &fbs::FilterShadowEffectArgs { - kind, shadow: Some(shadow_offset), - }) + let shadow_offset = fbs::FeShadow::create( + fbb, + &fbs::FeShadowArgs { + active: shadow.active, + dx: shadow.dx, + dy: shadow.dy, + blur: shadow.blur, + spread: shadow.spread, + color: Some(&color), + }, + ); + fbs::FilterShadowEffect::create( + fbb, + &fbs::FilterShadowEffectArgs { + kind, + shadow: Some(shadow_offset), + }, + ) } fn encode_fe_noise_effect<'a, A: flatbuffers::Allocator + 'a>( @@ -2708,11 +2996,18 @@ fn encode_fe_noise_effect<'a, A: flatbuffers::Allocator + 'a>( noise: &FeNoiseEffect, ) -> flatbuffers::WIPOffset> { let coloring = encode_noise_colors(fbb, &noise.coloring); - fbs::FeNoiseEffect::create(fbb, &fbs::FeNoiseEffectArgs { - active: noise.active, noise_size: noise.noise_size, density: noise.density, - num_octaves: noise.num_octaves, seed: noise.seed, coloring: Some(coloring), - blend_mode: encode_blend_mode(noise.blend_mode), - }) + fbs::FeNoiseEffect::create( + fbb, + &fbs::FeNoiseEffectArgs { + active: noise.active, + noise_size: noise.noise_size, + density: noise.density, + num_octaves: noise.num_octaves, + seed: noise.seed, + coloring: Some(coloring), + blend_mode: encode_blend_mode(noise.blend_mode), + }, + ) } fn encode_noise_colors<'a, A: flatbuffers::Allocator + 'a>( @@ -2722,22 +3017,36 @@ fn encode_noise_colors<'a, A: flatbuffers::Allocator + 'a>( match colors { NoiseEffectColors::Mono { color } => { let c = encode_color_to_rgba32f(color); - fbs::NoiseEffectColors::create(fbb, &fbs::NoiseEffectColorsArgs { - kind: fbs::NoiseEffectColorsKind::Mono, mono_color: Some(&c), ..Default::default() - }) + fbs::NoiseEffectColors::create( + fbb, + &fbs::NoiseEffectColorsArgs { + kind: fbs::NoiseEffectColorsKind::Mono, + mono_color: Some(&c), + ..Default::default() + }, + ) } NoiseEffectColors::Duo { color1, color2 } => { let c1 = encode_color_to_rgba32f(color1); let c2 = encode_color_to_rgba32f(color2); - fbs::NoiseEffectColors::create(fbb, &fbs::NoiseEffectColorsArgs { - kind: fbs::NoiseEffectColorsKind::Duo, duo_color1: Some(&c1), duo_color2: Some(&c2), ..Default::default() - }) - } - NoiseEffectColors::Multi { opacity } => { - fbs::NoiseEffectColors::create(fbb, &fbs::NoiseEffectColorsArgs { - kind: fbs::NoiseEffectColorsKind::Multi, multi_opacity: *opacity, ..Default::default() - }) + fbs::NoiseEffectColors::create( + fbb, + &fbs::NoiseEffectColorsArgs { + kind: fbs::NoiseEffectColorsKind::Duo, + duo_color1: Some(&c1), + duo_color2: Some(&c2), + ..Default::default() + }, + ) } + NoiseEffectColors::Multi { opacity } => fbs::NoiseEffectColors::create( + fbb, + &fbs::NoiseEffectColorsArgs { + kind: fbs::NoiseEffectColorsKind::Multi, + multi_opacity: *opacity, + ..Default::default() + }, + ), } } @@ -2933,9 +3242,10 @@ fn encode_container_layout<'a, A: flatbuffers::Allocator + 'a>( // Container style — use FBS `::None` sentinels for unset optional enum fields // so the decoder can distinguish "not set" from "explicitly set to default". - let padding = container_style.layout_padding.as_ref().map(|p| { - fbs::EdgeInsets::new(p.top, p.right, p.bottom, p.left) - }); + let padding = container_style + .layout_padding + .as_ref() + .map(|p| fbs::EdgeInsets::new(p.top, p.right, p.bottom, p.left)); let lc_fbs = Some(fbs::LayoutContainerStyle::create( fbb, &fbs::LayoutContainerStyleArgs { @@ -2976,7 +3286,12 @@ fn encode_container_layout<'a, A: flatbuffers::Allocator + 'a>( }, )); - let dims = encode_dimensions_with_aspect(fbb, dimensions.layout_target_width, dimensions.layout_target_height, dimensions.layout_target_aspect_ratio); + let dims = encode_dimensions_with_aspect( + fbb, + dimensions.layout_target_width, + dimensions.layout_target_height, + dimensions.layout_target_aspect_ratio, + ); let child = encode_layout_child_style(fbb, layout_child); fbs::LayoutStyle::create( fbb, @@ -3021,10 +3336,7 @@ fn encode_stroke_geometry<'a, A: flatbuffers::Allocator + 'a>( .stops .iter() .map(|s| { - fbs::VariableWidthStop::create( - fbb, - &fbs::VariableWidthStopArgs { u: s.u, r: s.r }, - ) + fbs::VariableWidthStop::create(fbb, &fbs::VariableWidthStopArgs { u: s.u, r: s.r }) }) .collect(); let stops_vec = fbb.create_vector(&stop_offsets); @@ -3103,12 +3415,18 @@ fn encode_container_node<'a, A: flatbuffers::Allocator + 'a>( let fill_offsets = encode_paints(fbb, &r.fills); let stroke_offsets = encode_paints(fbb, &r.strokes); - let cn = fbs::ContainerNode::create(fbb, &fbs::ContainerNodeArgs { - node: Some(sys), layer: Some(layer), - corner_radius: Some(cr_trait), stroke_geometry: Some(sg), - fill_paints: fill_offsets, stroke_paints: stroke_offsets, - clips_content: r.clip, - }); + let cn = fbs::ContainerNode::create( + fbb, + &fbs::ContainerNodeArgs { + node: Some(sys), + layer: Some(layer), + corner_radius: Some(cr_trait), + stroke_geometry: Some(sg), + fill_paints: fill_offsets, + stroke_paints: stroke_offsets, + clips_content: r.clip, + }, + ); make_node_slot(fbb, fbs::Node::ContainerNode, cn.as_union_value()) } @@ -3153,9 +3471,13 @@ fn encode_initial_container_node<'a, A: flatbuffers::Allocator + 'a>( }, ); - let icn = fbs::InitialContainerNode::create(fbb, &fbs::InitialContainerNodeArgs { - node: Some(sys), layer: Some(layer), - }); + let icn = fbs::InitialContainerNode::create( + fbb, + &fbs::InitialContainerNodeArgs { + node: Some(sys), + layer: Some(layer), + }, + ); make_node_slot(fbb, fbs::Node::InitialContainerNode, icn.as_union_value()) } @@ -3173,7 +3495,10 @@ fn encode_group_node<'a, A: flatbuffers::Allocator + 'a>( // Groups carry the full affine transform (scale, skew, rotation) — not // just rotation like shape nodes. Encode the full matrix so SVG-imported // transforms (e.g. scale(0.8, 1.2)) survive the roundtrip. - let plt = r.transform.as_ref().map(|t| encode_affine_to_cg_transform(t)); + let plt = r + .transform + .as_ref() + .map(|t| encode_affine_to_cg_transform(t)); let sys = encode_system_node_trait(fbb, node_id, "", r.active, false); let layout = encode_shape_layout(fbb, x, y, None, None, &None); @@ -3192,9 +3517,13 @@ fn encode_group_node<'a, A: flatbuffers::Allocator + 'a>( }, ); - let gn = fbs::GroupNode::create(fbb, &fbs::GroupNodeArgs { - node: Some(sys), layer: Some(layer), - }); + let gn = fbs::GroupNode::create( + fbb, + &fbs::GroupNodeArgs { + node: Some(sys), + layer: Some(layer), + }, + ); make_node_slot(fbb, fbs::Node::GroupNode, gn.as_union_value()) } @@ -3219,20 +3548,52 @@ fn encode_basic_shape_node<'a, A: flatbuffers::Allocator + 'a>( let (active, opacity, blend_mode, mask, effects, transform, size, fills, strokes, layout_child) = match &fields { BasicShapeFields::Rectangle(r) => ( - r.active, r.opacity, r.blend_mode, r.mask, &r.effects, &r.transform, &r.size, - &r.fills, &r.strokes, &r.layout_child, + r.active, + r.opacity, + r.blend_mode, + r.mask, + &r.effects, + &r.transform, + &r.size, + &r.fills, + &r.strokes, + &r.layout_child, ), BasicShapeFields::Ellipse(e) => ( - e.active, e.opacity, e.blend_mode, e.mask, &e.effects, &e.transform, &e.size, - &e.fills, &e.strokes, &e.layout_child, + e.active, + e.opacity, + e.blend_mode, + e.mask, + &e.effects, + &e.transform, + &e.size, + &e.fills, + &e.strokes, + &e.layout_child, ), BasicShapeFields::RegularPolygon(p) => ( - p.active, p.opacity, p.blend_mode, p.mask, &p.effects, &p.transform, &p.size, - &p.fills, &p.strokes, &p.layout_child, + p.active, + p.opacity, + p.blend_mode, + p.mask, + &p.effects, + &p.transform, + &p.size, + &p.fills, + &p.strokes, + &p.layout_child, ), BasicShapeFields::RegularStarPolygon(s) => ( - s.active, s.opacity, s.blend_mode, s.mask, &s.effects, &s.transform, &s.size, - &s.fills, &s.strokes, &s.layout_child, + s.active, + s.opacity, + s.blend_mode, + s.mask, + &s.effects, + &s.transform, + &s.size, + &s.fills, + &s.strokes, + &s.layout_child, ), }; @@ -3241,14 +3602,7 @@ fn encode_basic_shape_node<'a, A: flatbuffers::Allocator + 'a>( let plt = affine_to_rotation_transform(transform); let sys = encode_system_node_trait(fbb, node_id, "", active, false); - let layout = encode_shape_layout( - fbb, - x, - y, - Some(size.width), - Some(size.height), - layout_child, - ); + let layout = encode_shape_layout(fbb, x, y, Some(size.width), Some(size.height), layout_child); let layer = encode_layer_trait( fbb, &LayerTraitInput { @@ -3292,7 +3646,8 @@ fn encode_basic_shape_node<'a, A: flatbuffers::Allocator + 'a>( // Shape descriptor let shape_offset = match &fields { BasicShapeFields::Rectangle(_) => { - fbs::CanonicalShapeRectangular::create(fbb, &fbs::CanonicalShapeRectangularArgs {}).as_union_value() + fbs::CanonicalShapeRectangular::create(fbb, &fbs::CanonicalShapeRectangularArgs {}) + .as_union_value() } BasicShapeFields::Ellipse(e) => { let inner = e.inner_radius.unwrap_or(0.0); @@ -3315,17 +3670,21 @@ fn encode_basic_shape_node<'a, A: flatbuffers::Allocator + 'a>( ) .as_union_value() } - BasicShapeFields::RegularPolygon(p) => { - fbs::CanonicalShapeRegularPolygon::create(fbb, &fbs::CanonicalShapeRegularPolygonArgs { + BasicShapeFields::RegularPolygon(p) => fbs::CanonicalShapeRegularPolygon::create( + fbb, + &fbs::CanonicalShapeRegularPolygonArgs { point_count: p.point_count as u32, - }).as_union_value() - } - BasicShapeFields::RegularStarPolygon(s) => { - fbs::CanonicalShapeRegularStarPolygon::create(fbb, &fbs::CanonicalShapeRegularStarPolygonArgs { + }, + ) + .as_union_value(), + BasicShapeFields::RegularStarPolygon(s) => fbs::CanonicalShapeRegularStarPolygon::create( + fbb, + &fbs::CanonicalShapeRegularStarPolygonArgs { point_count: s.point_count as u32, inner_radius_ratio: s.inner_radius, - }).as_union_value() - } + }, + ) + .as_union_value(), }; // Corner smoothing (only meaningful for rectangles) @@ -3340,17 +3699,25 @@ fn encode_basic_shape_node<'a, A: flatbuffers::Allocator + 'a>( _ => None, }; - let bsn = fbs::BasicShapeNode::create(fbb, &fbs::BasicShapeNodeArgs { - node: Some(sys), layer: Some(layer), - type_: node_type, shape_type, shape: Some(shape_offset), - corner_radius: scalar_cr, fill_paints: fill_offsets, - stroke_style: Some(stroke_style_offset), stroke_width: stroke_width_f32, - rectangular_corner_radius: rect_cr.as_ref(), - stroke_paints: stroke_offsets, - corner_smoothing, - rectangular_stroke_width: rect_sw.as_ref(), - ..Default::default() - }); + let bsn = fbs::BasicShapeNode::create( + fbb, + &fbs::BasicShapeNodeArgs { + node: Some(sys), + layer: Some(layer), + type_: node_type, + shape_type, + shape: Some(shape_offset), + corner_radius: scalar_cr, + fill_paints: fill_offsets, + stroke_style: Some(stroke_style_offset), + stroke_width: stroke_width_f32, + rectangular_corner_radius: rect_cr.as_ref(), + stroke_paints: stroke_offsets, + corner_smoothing, + rectangular_stroke_width: rect_sw.as_ref(), + ..Default::default() + }, + ); make_node_slot(fbb, fbs::Node::BasicShapeNode, bsn.as_union_value()) } @@ -3383,22 +3750,32 @@ fn encode_line_node<'a, A: flatbuffers::Allocator + 'a>( }, ); - let sg = encode_stroke_geometry(fbb, &StrokeStyle { - stroke_align: r._data_stroke_align, - stroke_cap: r.stroke_cap, - stroke_join: StrokeJoin::Miter, - stroke_miter_limit: r.stroke_miter_limit, - stroke_dash_array: r.stroke_dash_array.clone(), - }, r.stroke_width, None); + let sg = encode_stroke_geometry( + fbb, + &StrokeStyle { + stroke_align: r._data_stroke_align, + stroke_cap: r.stroke_cap, + stroke_join: StrokeJoin::Miter, + stroke_miter_limit: r.stroke_miter_limit, + stroke_dash_array: r.stroke_dash_array.clone(), + }, + r.stroke_width, + None, + ); let stroke_offsets = encode_paints(fbb, &r.strokes); - let ln = fbs::LineNode::create(fbb, &fbs::LineNodeArgs { - node: Some(sys), layer: Some(layer), stroke_geometry: Some(sg), - stroke_paints: stroke_offsets, - marker_start_shape: encode_stroke_marker(r.marker_start_shape), - marker_end_shape: encode_stroke_marker(r.marker_end_shape), - }); + let ln = fbs::LineNode::create( + fbb, + &fbs::LineNodeArgs { + node: Some(sys), + layer: Some(layer), + stroke_geometry: Some(sg), + stroke_paints: stroke_offsets, + marker_start_shape: encode_stroke_marker(r.marker_start_shape), + marker_end_shape: encode_stroke_marker(r.marker_end_shape), + }, + ); make_node_slot(fbb, fbs::Node::LineNode, ln.as_union_value()) } @@ -3432,25 +3809,36 @@ fn encode_vector_node<'a, A: flatbuffers::Allocator + 'a>( // Vector network let vn = encode_vector_network(fbb, &r.network); - let sg = encode_stroke_geometry(fbb, &StrokeStyle { - stroke_align: r.stroke_align, - stroke_cap: r.stroke_cap, - stroke_join: r.stroke_join, - stroke_miter_limit: r.stroke_miter_limit, - stroke_dash_array: r.stroke_dash_array.clone(), - }, r.stroke_width, r.stroke_width_profile.as_ref()); + let sg = encode_stroke_geometry( + fbb, + &StrokeStyle { + stroke_align: r.stroke_align, + stroke_cap: r.stroke_cap, + stroke_join: r.stroke_join, + stroke_miter_limit: r.stroke_miter_limit, + stroke_dash_array: r.stroke_dash_array.clone(), + }, + r.stroke_width, + r.stroke_width_profile.as_ref(), + ); let fill_offsets = encode_paints(fbb, &r.fills); let stroke_offsets = encode_paints(fbb, &r.strokes); - let vn_node = fbs::VectorNode::create(fbb, &fbs::VectorNodeArgs { - node: Some(sys), layer: Some(layer), stroke_geometry: Some(sg), - stroke_paints: stroke_offsets, fill_paints: fill_offsets, - vector_network_data: vn, - marker_start_shape: encode_stroke_marker(r.marker_start_shape), - marker_end_shape: encode_stroke_marker(r.marker_end_shape), - ..Default::default() - }); + let vn_node = fbs::VectorNode::create( + fbb, + &fbs::VectorNodeArgs { + node: Some(sys), + layer: Some(layer), + stroke_geometry: Some(sg), + stroke_paints: stroke_offsets, + fill_paints: fill_offsets, + vector_network_data: vn, + marker_start_shape: encode_stroke_marker(r.marker_start_shape), + marker_end_shape: encode_stroke_marker(r.marker_end_shape), + ..Default::default() + }, + ); make_node_slot(fbb, fbs::Node::VectorNode, vn_node.as_union_value()) } @@ -3467,31 +3855,37 @@ fn encode_path_node<'a, A: flatbuffers::Allocator + 'a>( let sys = encode_system_node_trait(fbb, node_id, "", r.active, false); let layout = encode_shape_layout(fbb, x, y, None, None, &r.layout_child); - let layer = encode_layer_trait(fbb, &LayerTraitInput { - parent_id, - position, - opacity: r.opacity, - blend_mode: r.blend_mode, - mask: r.mask, - effects: &r.effects, - post_layout_transform: plt, - layout: Some(layout), - }); + let layer = encode_layer_trait( + fbb, + &LayerTraitInput { + parent_id, + position, + opacity: r.opacity, + blend_mode: r.blend_mode, + mask: r.mask, + effects: &r.effects, + post_layout_transform: plt, + layout: Some(layout), + }, + ); let sg = encode_stroke_geometry(fbb, &r.stroke_style, r.stroke_width.value_or_zero(), None); let fill_offsets = encode_paints(fbb, &r.fills); let stroke_offsets = encode_paints(fbb, &r.strokes); let data_offset = fbb.create_string(&r.data); - let pn = fbs::PathNode::create(fbb, &fbs::PathNodeArgs { - node: Some(sys), - layer: Some(layer), - stroke_geometry: Some(sg), - fill_paints: fill_offsets, - stroke_paints: stroke_offsets, - data: Some(data_offset), - fill_rule: fbs::FillRule::NonZero, - }); + let pn = fbs::PathNode::create( + fbb, + &fbs::PathNodeArgs { + node: Some(sys), + layer: Some(layer), + stroke_geometry: Some(sg), + fill_paints: fill_offsets, + stroke_paints: stroke_offsets, + data: Some(data_offset), + fill_rule: fbs::FillRule::NonZero, + }, + ); make_node_slot(fbb, fbs::Node::PathNode, pn.as_union_value()) } @@ -3527,92 +3921,136 @@ fn encode_text_span_node<'a, A: flatbuffers::Allocator + 'a>( let font_family_str = fbb.create_string(&ts.font_family); let font_weight = fbs::FontWeight::new(ts.font_weight.0); let fbs_font_optical_sizing = match ts.font_optical_sizing { - FontOpticalSizing::Auto => fbs::FontOpticalSizing::new(fbs::FontOpticalSizingKind::Auto, 0.0), - FontOpticalSizing::None => fbs::FontOpticalSizing::new(fbs::FontOpticalSizingKind::None, 0.0), - FontOpticalSizing::Fixed(v) => fbs::FontOpticalSizing::new(fbs::FontOpticalSizingKind::Fixed, v), + FontOpticalSizing::Auto => { + fbs::FontOpticalSizing::new(fbs::FontOpticalSizingKind::Auto, 0.0) + } + FontOpticalSizing::None => { + fbs::FontOpticalSizing::new(fbs::FontOpticalSizingKind::None, 0.0) + } + FontOpticalSizing::Fixed(v) => { + fbs::FontOpticalSizing::new(fbs::FontOpticalSizingKind::Fixed, v) + } }; let text_decoration_offset = ts.text_decoration.as_ref().map(|td| { - let color = td.text_decoration_color.as_ref().map(|c| encode_color_to_rgba32f(c)); - fbs::TextDecorationRec::create(fbb, &fbs::TextDecorationRecArgs { - text_decoration_line: encode_text_decoration_line(td.text_decoration_line), - text_decoration_style: encode_text_decoration_style(td.text_decoration_style.unwrap_or_default()), - text_decoration_skip_ink: td.text_decoration_skip_ink.unwrap_or(true), - text_decoration_thickness: td.text_decoration_thickness.unwrap_or(0.0), - text_decoration_color: color.as_ref(), - }) + let color = td + .text_decoration_color + .as_ref() + .map(|c| encode_color_to_rgba32f(c)); + fbs::TextDecorationRec::create( + fbb, + &fbs::TextDecorationRecArgs { + text_decoration_line: encode_text_decoration_line(td.text_decoration_line), + text_decoration_style: encode_text_decoration_style( + td.text_decoration_style.unwrap_or_default(), + ), + text_decoration_skip_ink: td.text_decoration_skip_ink.unwrap_or(true), + text_decoration_thickness: td.text_decoration_thickness.unwrap_or(0.0), + text_decoration_color: color.as_ref(), + }, + ) }); let letter_spacing_offset = encode_text_dimension_from_letter_spacing(fbb, &ts.letter_spacing); let word_spacing_offset = encode_text_dimension_from_word_spacing(fbb, &ts.word_spacing); let line_height_offset = encode_text_dimension_from_line_height(fbb, &ts.line_height); let font_features_offset = ts.font_features.as_ref().map(|features| { - let items: Vec<_> = features.iter().map(|f| { - let bytes = f.tag.as_bytes(); - let tag = fbs::OpenTypeFeatureTag::new( - *bytes.first().unwrap_or(&0), - *bytes.get(1).unwrap_or(&0), - *bytes.get(2).unwrap_or(&0), - *bytes.get(3).unwrap_or(&0), - ); - fbs::FontFeature::create(fbb, &fbs::FontFeatureArgs { - open_type_feature_tag: Some(&tag), - open_type_feature_value: f.value, + let items: Vec<_> = features + .iter() + .map(|f| { + let bytes = f.tag.as_bytes(); + let tag = fbs::OpenTypeFeatureTag::new( + *bytes.first().unwrap_or(&0), + *bytes.get(1).unwrap_or(&0), + *bytes.get(2).unwrap_or(&0), + *bytes.get(3).unwrap_or(&0), + ); + fbs::FontFeature::create( + fbb, + &fbs::FontFeatureArgs { + open_type_feature_tag: Some(&tag), + open_type_feature_value: f.value, + }, + ) }) - }).collect(); + .collect(); fbb.create_vector(&items) }); let font_variations_offset = ts.font_variations.as_ref().map(|variations| { - let items: Vec<_> = variations.iter().map(|v| { - let axis_str = fbb.create_string(&v.axis); - fbs::FontVariation::create(fbb, &fbs::FontVariationArgs { - variation_axis: Some(axis_str), - variation_value: v.value, + let items: Vec<_> = variations + .iter() + .map(|v| { + let axis_str = fbb.create_string(&v.axis); + fbs::FontVariation::create( + fbb, + &fbs::FontVariationArgs { + variation_axis: Some(axis_str), + variation_value: v.value, + }, + ) }) - }).collect(); + .collect(); fbb.create_vector(&items) }); - let text_style = fbs::TextStyleRec::create(fbb, &fbs::TextStyleRecArgs { - font_family: Some(font_family_str), - font_size: ts.font_size, - font_weight: Some(&font_weight), - font_style_italic: ts.font_style_italic, - font_kerning: ts.font_kerning, - font_width: ts.font_width.unwrap_or(0.0), - font_optical_sizing: Some(&fbs_font_optical_sizing), - text_transform: encode_text_transform(ts.text_transform), - text_decoration: text_decoration_offset, - letter_spacing: letter_spacing_offset, - word_spacing: word_spacing_offset, - line_height: line_height_offset, - font_features: font_features_offset, - font_variations: font_variations_offset, - }); + let text_style = fbs::TextStyleRec::create( + fbb, + &fbs::TextStyleRecArgs { + font_family: Some(font_family_str), + font_size: ts.font_size, + font_weight: Some(&font_weight), + font_style_italic: ts.font_style_italic, + font_kerning: ts.font_kerning, + font_width: ts.font_width.unwrap_or(0.0), + font_optical_sizing: Some(&fbs_font_optical_sizing), + text_transform: encode_text_transform(ts.text_transform), + text_decoration: text_decoration_offset, + letter_spacing: letter_spacing_offset, + word_spacing: word_spacing_offset, + line_height: line_height_offset, + font_features: font_features_offset, + font_variations: font_variations_offset, + }, + ); let text_str = fbb.create_string(&r.text); let fill_offsets = encode_paints(fbb, &r.fills); let stroke_offsets = encode_paints(fbb, &r.strokes); - let sg = encode_stroke_geometry(fbb, &StrokeStyle { - stroke_align: r.stroke_align, - stroke_cap: StrokeCap::Butt, - stroke_join: StrokeJoin::Miter, - stroke_miter_limit: StrokeMiterLimit::default(), - stroke_dash_array: None, - }, r.stroke_width, None); + let sg = encode_stroke_geometry( + fbb, + &StrokeStyle { + stroke_align: r.stroke_align, + stroke_cap: StrokeCap::Butt, + stroke_join: StrokeJoin::Miter, + stroke_miter_limit: StrokeMiterLimit::default(), + stroke_dash_array: None, + }, + r.stroke_width, + None, + ); let ellipsis_offset = r.ellipsis.as_ref().map(|s| fbb.create_string(s)); - let props = fbs::TextSpanNodeProperties::create(fbb, &fbs::TextSpanNodePropertiesArgs { - text: Some(text_str), text_style: Some(text_style), - text_align: encode_text_align(r.text_align), - text_align_vertical: encode_text_align_vertical(r.text_align_vertical), - fill_paints: fill_offsets, stroke_paints: stroke_offsets, - stroke_geometry: Some(sg), - max_lines: r.max_lines.map(|v| v as u32).unwrap_or(0), - ellipsis: ellipsis_offset, - }); + let props = fbs::TextSpanNodeProperties::create( + fbb, + &fbs::TextSpanNodePropertiesArgs { + text: Some(text_str), + text_style: Some(text_style), + text_align: encode_text_align(r.text_align), + text_align_vertical: encode_text_align_vertical(r.text_align_vertical), + fill_paints: fill_offsets, + stroke_paints: stroke_offsets, + stroke_geometry: Some(sg), + max_lines: r.max_lines.map(|v| v as u32).unwrap_or(0), + ellipsis: ellipsis_offset, + }, + ); - let tn = fbs::TextSpanNode::create(fbb, &fbs::TextSpanNodeArgs { - node: Some(sys), layer: Some(layer), properties: Some(props), - }); + let tn = fbs::TextSpanNode::create( + fbb, + &fbs::TextSpanNodeArgs { + node: Some(sys), + layer: Some(layer), + properties: Some(props), + }, + ); make_node_slot(fbb, fbs::Node::TextSpanNode, tn.as_union_value()) } @@ -3745,10 +4183,7 @@ fn encode_vector_network_region<'a, A: flatbuffers::Allocator + 'a>( crate::cg::types::FillRule::NonZero => fbs::FillRule::NonZero, }; - let fills_offset = region - .fills - .as_ref() - .and_then(|p| encode_paints(fbb, p)); + let fills_offset = region.fills.as_ref().and_then(|p| encode_paints(fbb, p)); fbs::VectorNetworkRegion::create( fbb, @@ -3764,9 +4199,7 @@ fn encode_vector_network_region<'a, A: flatbuffers::Allocator + 'a>( // Geometry helpers for encoding // ───────────────────────────────────────────────────────────────────────────── -fn encode_rectangular_corner_radius( - cr: &RectangularCornerRadius, -) -> fbs::RectangularCornerRadius { +fn encode_rectangular_corner_radius(cr: &RectangularCornerRadius) -> fbs::RectangularCornerRadius { let tl = fbs::CGRadius::new(cr.tl.rx, cr.tl.ry); let tr = fbs::CGRadius::new(cr.tr.rx, cr.tr.ry); let bl = fbs::CGRadius::new(cr.bl.rx, cr.bl.ry); diff --git a/crates/grida-canvas/src/layout/cache.rs b/crates/grida-canvas/src/layout/cache.rs index a78709fedf..e8e941b1c7 100644 --- a/crates/grida-canvas/src/layout/cache.rs +++ b/crates/grida-canvas/src/layout/cache.rs @@ -1,20 +1,20 @@ +use crate::cache::fast_hash::DenseNodeMap; use crate::layout::ComputedLayout; use crate::node::schema::NodeId; -use std::collections::HashMap; /// Immutable layout computation result /// /// Maps NodeId to computed position/size. Represents the output of a layout /// computation phase. Cached between frames for performance and change detection. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone)] pub struct LayoutResult { - layouts: HashMap, + layouts: DenseNodeMap, } impl LayoutResult { pub fn new() -> Self { Self { - layouts: HashMap::new(), + layouts: DenseNodeMap::new(), } } @@ -38,7 +38,7 @@ impl LayoutResult { self.layouts.clear(); } - pub fn iter(&self) -> impl Iterator { + pub fn iter(&self) -> impl Iterator { self.layouts.iter() } diff --git a/crates/grida-canvas/src/layout/engine.rs b/crates/grida-canvas/src/layout/engine.rs index 35343aa5cb..b2ccade15d 100644 --- a/crates/grida-canvas/src/layout/engine.rs +++ b/crates/grida-canvas/src/layout/engine.rs @@ -62,7 +62,7 @@ use crate::layout::cache::LayoutResult; use crate::layout::tree::{LayoutTree, TextMeasureContext, TextMeasureProvider}; use crate::layout::ComputedLayout; use crate::node::scene_graph::SceneGraph; -use crate::node::schema::{Node, NodeId, NodeRectMixin, Size}; +use crate::node::schema::{Node, NodeId, NodeRectMixin, NodeTypeTag, Size}; use taffy::prelude::*; /// Layout engine for the scene graph. @@ -143,7 +143,9 @@ impl LayoutEngine { // (Group, BoolOp) get manual layout results from schema data. // Children of non-Taffy parents get schema-position correction // via parent-type check (no extra bookkeeping needed). - self.extract_all_layouts(root_id, graph); + // Text nodes not in Taffy are measured on-the-fly if they lack + // explicit height in the schema. + self.extract_all_layouts(root_id, graph, &mut text_measure); } &self.result @@ -172,21 +174,24 @@ impl LayoutEngine { } /// Recursively extract schema positions/sizes for a node and its children. + /// + /// Uses `NodeGeoData` (~48 bytes) instead of full `Node` (~500+ bytes) to + /// read schema positions and sizes — avoids 136K full-Node reads. fn extract_schema_only_recursive( &mut self, id: &NodeId, graph: &crate::node::scene_graph::SceneGraph, ) { - if let Ok(node) = graph.get_node(id) { - let (x, y) = Self::get_schema_position(node); - let (width, height) = Self::get_schema_size(node); + if let Some(geo) = graph.geo_data().get(id) { + let x = geo.schema_transform.x(); + let y = geo.schema_transform.y(); self.result.insert( *id, ComputedLayout { x, y, - width, - height, + width: geo.schema_width, + height: geo.schema_height, }, ); } @@ -278,15 +283,16 @@ impl LayoutEngine { } } - /// Check if a node type participates in Taffy layout. + /// Check if a node type participates in Taffy layout (using NodeTypeTag). /// /// Virtual grouping nodes (Group, BooleanOperation) are excluded — they /// have no intrinsic size, don't constrain children, and their bounds are /// derived from children. Their children form independent Taffy subtrees. - fn is_layout_node(node: &Node) -> bool { - !matches!(node, Node::Group(_) | Node::BooleanOperation(_)) + fn is_layout_node_tag(tag: NodeTypeTag) -> bool { + !matches!(tag, NodeTypeTag::Group | NodeTypeTag::BooleanOperation) } + /// Recursively build Taffy tree for a node and its descendants. /// /// Virtual grouping nodes (Group, BooleanOperation) are skipped from the @@ -300,11 +306,13 @@ impl LayoutEngine { viewport_size: Size, extra_roots: &mut Vec, ) -> Option { - let node = graph.get_node(node_id).ok()?; + // Fast-path: use compact layer_core (~16 bytes) for is_layout_node + // and is_flex_container checks before touching the full Node (~500+ bytes). + let lc = graph.get_layer_core(node_id)?; // Virtual grouping nodes don't participate in Taffy — skip them but // recurse into their children to discover Taffy-capable subtrees. - if !Self::is_layout_node(node) { + if !Self::is_layout_node_tag(lc.node_type) { if let Some(children) = graph.get_children(node_id) { for child_id in children { if let Some(taffy_id) = @@ -317,6 +325,9 @@ impl LayoutEngine { return None; } + // Only access full Node for nodes that participate in Taffy layout. + let node = graph.get_node(node_id).ok()?; + // Get style for this node (universal mapping) let mut style = crate::layout::into_taffy::node_to_taffy_style(node, graph, node_id); @@ -324,7 +335,7 @@ impl LayoutEngine { // extract_all_layouts() post-processes to apply schema positions // Special handling for root ICB nodes - use viewport size - if let Node::InitialContainer(_) = node { + if lc.node_type == NodeTypeTag::InitialContainer { style.size = taffy::Size { width: Dimension::length(viewport_size.width), height: Dimension::length(viewport_size.height), @@ -336,7 +347,20 @@ impl LayoutEngine { if let Some(children) = children { if !children.is_empty() { - // Build children recursively, filtering out those that shouldn't participate + // For non-flex containers (LayoutMode::Normal), children are + // positioned via schema coordinates, not flex layout. Create + // this container as a Taffy leaf — it only needs its own size + // for its parent's flex computation. Children will be handled + // by extract_all_layouts using schema positions directly. + // + // This is the critical optimization for Figma imports: most + // containers use Normal mode, so we skip building Taffy nodes + // for their entire subtrees (~90%+ of nodes). + if !lc.is_flex { + return self.tree.new_leaf(*node_id, style).ok(); + } + + // Flex containers: build children as Taffy children let taffy_children: Vec = children .iter() .filter_map(|child_id| { @@ -377,48 +401,105 @@ impl LayoutEngine { /// We override root positions with their schema positions so multiple /// artboards/nodes can be positioned anywhere in the viewport. /// - /// **Virtual grouping nodes** (Group, BooleanOperation) are not in the - /// Taffy tree — they get manual layout results from their schema. - /// Their children become independent Taffy subtree roots (computed at 0,0), - /// so we detect this via parent-type check and apply schema positions. - /// No extra bookkeeping is needed; this handles arbitrary nesting depth. - fn extract_all_layouts(&mut self, id: &NodeId, graph: &SceneGraph) { + /// **Non-layout nodes** (Group, BooleanOperation) are not in the Taffy + /// tree — they get manual layout results from their schema. + /// **Non-flex containers** (LayoutMode::Normal) ARE in Taffy (as leaves + /// for their parent's flex computation) but their children are independent + /// subtrees with schema positions. + /// In both cases, children become independent Taffy subtree roots + /// (computed at 0,0), so we detect this via parent-type check and apply + /// schema positions. No extra bookkeeping is needed. + fn extract_all_layouts( + &mut self, + id: &NodeId, + graph: &SceneGraph, + text_measure: &mut Option>, + ) { if let Some(layout) = self.tree.get_layout(id) { let mut computed = ComputedLayout::from(layout); - // Taffy roots are computed at (0,0). Two cases need schema-position + // Taffy roots are computed at (0,0). Three cases need schema-position // correction: // 1. Graph roots — top-level nodes on the infinite canvas - // 2. Children of non-layout parents (Group/BoolOp) — these are - // independent Taffy subtree roots, also computed at (0,0) + // 2. Children of non-layout parents (Group/BoolOp) — independent + // Taffy subtree roots, also computed at (0,0) + // 3. Children of non-flex containers (LayoutMode::Normal) — these + // are also extra_roots with schema positions + // + // Uses layer_core (~16 bytes) instead of full Node (~500+ bytes) + // for parent type checks. let needs_schema_position = graph.is_root(id) || graph .get_parent(id) - .and_then(|pid| graph.get_node(&pid).ok()) - .is_some_and(|parent| !Self::is_layout_node(parent)); + .and_then(|pid| graph.get_layer_core(&pid)) + .is_some_and(|parent_lc| { + !Self::is_layout_node_tag(parent_lc.node_type) || !parent_lc.is_flex + }); if needs_schema_position { - if let Ok(node) = graph.get_node(id) { - let (schema_x, schema_y) = Self::get_schema_position(node); - computed.x = schema_x; - computed.y = schema_y; + // Use geo_data (~48 bytes) for schema position instead of full Node. + if let Some(geo) = graph.geo_data().get(id) { + computed.x = geo.schema_transform.x(); + computed.y = geo.schema_transform.y(); } } self.result.insert(*id, computed); } else { - // Node not in Taffy tree (virtual grouping node) — use schema - if let Ok(node) = graph.get_node(id) { - let (x, y) = Self::get_schema_position(node); - let (width, height) = Self::get_schema_size(node); + // Node not in Taffy tree — use schema positions/sizes from geo_data. + // For text nodes with missing dimensions, access full Node for measurement. + let lc = graph.get_layer_core(id); + let is_text = lc.map(|c| c.node_type == NodeTypeTag::TextSpan).unwrap_or(false); + if is_text { + // TextSpan: may need on-the-fly measurement — access full Node. + if let Ok(node) = graph.get_node(id) { + let (x, y) = Self::get_schema_position(node); + let (mut width, mut height) = Self::get_schema_size(node); + + if let Node::TextSpan(n) = node { + if n.width.is_none() || n.height.is_none() { + if let Some(ref mut provider) = text_measure { + let width_constraint = n.width; + let measurements = provider.paragraph_cache.measure( + &n.text, + &n.text_style, + &n.text_align, + &n.max_lines, + &n.ellipsis, + width_constraint, + provider.fonts, + Some(id), + ); + if n.width.is_none() { + width = measurements.max_width; + } + if n.height.is_none() { + height = measurements.height; + } + } + } + } + + self.result.insert( + *id, + ComputedLayout { + x, + y, + width, + height, + }, + ); + } + } else if let Some(geo) = graph.geo_data().get(id) { + // Non-text: use geo_data (~48 bytes) instead of full Node (~500+ bytes). self.result.insert( *id, ComputedLayout { - x, - y, - width, - height, + x: geo.schema_transform.x(), + y: geo.schema_transform.y(), + width: geo.schema_width, + height: geo.schema_height, }, ); } @@ -427,7 +508,7 @@ impl LayoutEngine { // Recurse for children if let Some(children) = graph.get_children(id) { for child_id in children { - self.extract_all_layouts(child_id, graph); + self.extract_all_layouts(child_id, graph, text_measure); } } } diff --git a/crates/grida-canvas/src/layout/tree.rs b/crates/grida-canvas/src/layout/tree.rs index a79bb839c2..a2e8475e4c 100644 --- a/crates/grida-canvas/src/layout/tree.rs +++ b/crates/grida-canvas/src/layout/tree.rs @@ -1,7 +1,9 @@ +use crate::cache::fast_hash::DenseNodeMap; use crate::cache::paragraph::ParagraphCache; use crate::cg::types::{TextAlign, TextStyleRec}; use crate::node::schema::NodeId; use crate::runtime::font_repository::FontRepository; +#[cfg(test)] use std::collections::HashMap; use taffy::prelude::*; @@ -38,8 +40,9 @@ pub(crate) struct LayoutTree { /// Taffy tree for layout computation taffy: TaffyTree, /// Map from our SceneGraph NodeId to Taffy's NodeId - scene_to_taffy: HashMap, - /// Reverse map from Taffy NodeId to SceneGraph NodeId + scene_to_taffy: DenseNodeMap, + /// Reverse map from Taffy NodeId to SceneGraph NodeId (test-only) + #[cfg(test)] taffy_to_scene: HashMap, } @@ -47,7 +50,8 @@ impl LayoutTree { pub(crate) fn new() -> Self { Self { taffy: TaffyTree::new(), - scene_to_taffy: HashMap::new(), + scene_to_taffy: DenseNodeMap::new(), + #[cfg(test)] taffy_to_scene: HashMap::new(), } } @@ -64,6 +68,7 @@ impl LayoutTree { if self.scene_to_taffy.capacity() < capacity { self.taffy = TaffyTree::with_capacity(capacity); self.scene_to_taffy.reserve(capacity); + #[cfg(test)] self.taffy_to_scene.reserve(capacity); } } @@ -77,17 +82,17 @@ impl LayoutTree { style: Style, ) -> Result { let taffy_id = self.taffy.new_leaf(style)?; - - // Clean up any existing mapping for this scene_node_id - if let Some(old_taffy_id) = self.scene_to_taffy.insert(scene_node_id, taffy_id) { + #[cfg(test)] + if let Some(old_taffy_id) = self.scene_to_taffy.get(&scene_node_id).copied() { self.taffy_to_scene.remove(&old_taffy_id); } - - // Clean up any existing mapping for this taffy_id - if let Some(old_scene_id) = self.taffy_to_scene.insert(taffy_id, scene_node_id) { - self.scene_to_taffy.remove(&old_scene_id); + self.scene_to_taffy.insert(scene_node_id, taffy_id); + #[cfg(test)] + { + if let Some(old_scene_id) = self.taffy_to_scene.insert(taffy_id, scene_node_id) { + self.scene_to_taffy.remove(&old_scene_id); + } } - Ok(taffy_id) } @@ -101,15 +106,17 @@ impl LayoutTree { let taffy_id = self .taffy .new_leaf_with_context(style, LayoutNodeContext::Text(context))?; - - if let Some(old_taffy_id) = self.scene_to_taffy.insert(scene_node_id, taffy_id) { + #[cfg(test)] + if let Some(old_taffy_id) = self.scene_to_taffy.get(&scene_node_id).copied() { self.taffy_to_scene.remove(&old_taffy_id); } - - if let Some(old_scene_id) = self.taffy_to_scene.insert(taffy_id, scene_node_id) { - self.scene_to_taffy.remove(&old_scene_id); + self.scene_to_taffy.insert(scene_node_id, taffy_id); + #[cfg(test)] + { + if let Some(old_scene_id) = self.taffy_to_scene.insert(taffy_id, scene_node_id) { + self.scene_to_taffy.remove(&old_scene_id); + } } - Ok(taffy_id) } @@ -123,17 +130,17 @@ impl LayoutTree { children: &[taffy::NodeId], ) -> Result { let taffy_id = self.taffy.new_with_children(style, children)?; - - // Clean up any existing mapping for this scene_node_id - if let Some(old_taffy_id) = self.scene_to_taffy.insert(scene_node_id, taffy_id) { + #[cfg(test)] + if let Some(old_taffy_id) = self.scene_to_taffy.get(&scene_node_id).copied() { self.taffy_to_scene.remove(&old_taffy_id); } - - // Clean up any existing mapping for this taffy_id - if let Some(old_scene_id) = self.taffy_to_scene.insert(taffy_id, scene_node_id) { - self.scene_to_taffy.remove(&old_scene_id); + self.scene_to_taffy.insert(scene_node_id, taffy_id); + #[cfg(test)] + { + if let Some(old_scene_id) = self.taffy_to_scene.insert(taffy_id, scene_node_id) { + self.scene_to_taffy.remove(&old_scene_id); + } } - Ok(taffy_id) } @@ -213,6 +220,7 @@ impl LayoutTree { pub(crate) fn clear(&mut self) { self.taffy.clear(); self.scene_to_taffy.clear(); + #[cfg(test)] self.taffy_to_scene.clear(); } diff --git a/crates/grida-canvas/src/node/repository.rs b/crates/grida-canvas/src/node/repository.rs index 57f4c90abd..ffbcc202c3 100644 --- a/crates/grida-canvas/src/node/repository.rs +++ b/crates/grida-canvas/src/node/repository.rs @@ -1,11 +1,11 @@ +use crate::cache::fast_hash::DenseNodeMap; use crate::node::schema::{Node, NodeId}; -use std::collections::HashMap; /// A repository for managing nodes with automatic ID indexing. #[derive(Debug, Clone)] pub struct NodeRepository { /// The map of all nodes indexed by their IDs - nodes: HashMap, + nodes: DenseNodeMap, /// ID generator for auto-assigning IDs id_generator: crate::node::id::NodeIdGenerator, } @@ -14,7 +14,7 @@ impl NodeRepository { /// Creates a new empty node repository pub fn new() -> Self { Self { - nodes: HashMap::new(), + nodes: DenseNodeMap::new(), id_generator: crate::node::id::NodeIdGenerator::new(), } } @@ -49,7 +49,7 @@ impl NodeRepository { } /// Returns an iterator over all nodes in the repository - pub fn iter(&self) -> impl Iterator { + pub fn iter(&self) -> impl Iterator { self.nodes.iter() } @@ -64,15 +64,16 @@ impl NodeRepository { } pub fn filter(&self, filter: impl Fn(&Node) -> bool) -> Self { - NodeRepository { - nodes: self - .nodes - .iter() - .filter(|(_, node)| filter(node)) - .map(|(k, v)| (k.clone(), v.clone())) - .collect(), + let mut new_repo = NodeRepository { + nodes: DenseNodeMap::new(), id_generator: self.id_generator.clone(), + }; + for (id, node) in self.nodes.iter() { + if filter(node) { + new_repo.nodes.insert(id, node.clone()); + } } + new_repo } } diff --git a/crates/grida-canvas/src/node/scene_graph.rs b/crates/grida-canvas/src/node/scene_graph.rs index 28d245e7c0..38468c541f 100644 --- a/crates/grida-canvas/src/node/scene_graph.rs +++ b/crates/grida-canvas/src/node/scene_graph.rs @@ -1,5 +1,10 @@ use super::repository::NodeRepository; -use super::schema::{Node, NodeId}; +use super::schema::{ + extract_layer_core, Node, NodeGeometryMixin, NodeId, NodeLayerCore, NodeRectMixin, +}; +use crate::cache::fast_hash::DenseNodeMap; +use crate::cg::prelude::*; +use math2::transform::AffineTransform; use std::collections::HashMap; /// Parent reference in the scene graph @@ -45,12 +50,386 @@ impl std::error::Error for SceneGraphError {} pub type SceneGraphResult = Result; +// --------------------------------------------------------------------------- +// NodeGeoData — compact, schema-level geometry data per node +// --------------------------------------------------------------------------- + +/// Classifies how a node participates in geometry computation. +#[derive(Debug, Clone, Copy, PartialEq)] +#[repr(u8)] +pub enum GeoNodeKind { + Group, + InitialContainer, + Container, + BooleanOperation, + TextSpan, + Leaf, +} + +/// Pre-computed render bounds inflation. +/// +/// Stores per-side pixel expansion from stroke + effects, computed at +/// construction time. `Copy`, no heap allocation, 16 bytes total. +/// The geometry cache inflates world bounds by these values at DFS time. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct RenderBoundsInflation { + pub top: f32, + pub right: f32, + pub bottom: f32, + pub left: f32, +} + +impl RenderBoundsInflation { + pub const ZERO: Self = Self { + top: 0.0, + right: 0.0, + bottom: 0.0, + left: 0.0, + }; + + /// Uniform inflation on all sides. + pub fn uniform(delta: f32) -> Self { + Self { + top: delta, + right: delta, + bottom: delta, + left: delta, + } + } + + /// Expand this inflation to include another inflation (per-side max). + pub fn expand(&self, other: &Self) -> Self { + Self { + top: self.top.max(other.top), + right: self.right.max(other.right), + bottom: self.bottom.max(other.bottom), + left: self.left.max(other.left), + } + } + + /// Whether this inflation is all zeros. + pub fn is_zero(&self) -> bool { + self.top == 0.0 && self.right == 0.0 && self.bottom == 0.0 && self.left == 0.0 + } +} + +/// Compact, schema-level geometry data extracted from a `Node` at construction +/// time. Stored in a parallel `DenseNodeMap` on the `SceneGraph` so that the +/// geometry cache never needs to iterate over the full `Node` enum. +/// +/// Layout-dependent fields (final width/height/position) are resolved by the +/// geometry cache using this data + `LayoutResult`. +/// +/// This struct is `Copy` — no heap allocations, ~48 bytes total. +#[derive(Debug, Clone, Copy)] +pub struct NodeGeoData { + /// The node's transform as stored in the schema. + /// + /// For Container nodes, this is `AffineTransform::new(fallback_x, fallback_y, rotation)`. + /// The geometry cache may override x/y from layout results. + pub schema_transform: AffineTransform, + /// Schema width (from size, rect(), or network.bounds()). + pub schema_width: f32, + /// Schema height. + pub schema_height: f32, + /// What kind of node (determines DFS behavior). + pub kind: GeoNodeKind, + /// Pre-computed per-side render bounds inflation from stroke + effects. + pub render_bounds_inflation: RenderBoundsInflation, + /// Container rotation (needed to reconstruct transform from layout x/y). + /// Only meaningful for Container nodes; 0.0 for others. + pub rotation: f32, +} + +/// Extract schema-level geometry data from a `Node`. +/// +/// This is a pure function of the Node — no layout results, no font metrics, +/// no paragraph cache. Called once per node during SceneGraph construction. +/// All values are `Copy` — zero heap allocations. +pub fn extract_geo_data(node: &Node) -> NodeGeoData { + match node { + Node::Group(n) => NodeGeoData { + schema_transform: n.transform.unwrap_or_default(), + schema_width: 0.0, + schema_height: 0.0, + kind: GeoNodeKind::Group, + render_bounds_inflation: RenderBoundsInflation::ZERO, // union of children + rotation: 0.0, + }, + Node::InitialContainer(_) => NodeGeoData { + schema_transform: AffineTransform::identity(), + schema_width: 0.0, + schema_height: 0.0, + kind: GeoNodeKind::InitialContainer, + render_bounds_inflation: RenderBoundsInflation::ZERO, + rotation: 0.0, + }, + Node::BooleanOperation(n) => NodeGeoData { + schema_transform: n.transform.unwrap_or_default(), + schema_width: 0.0, + schema_height: 0.0, + kind: GeoNodeKind::BooleanOperation, + render_bounds_inflation: compute_inflation_uniform( + n.stroke_width.value_or_zero(), + n.stroke_style.stroke_align, + &n.effects, + ), + rotation: 0.0, + }, + Node::Container(n) => { + let fallback_x = n.position.x().unwrap_or(0.0); + let fallback_y = n.position.y().unwrap_or(0.0); + let schema_transform = AffineTransform::new(fallback_x, fallback_y, n.rotation); + + let render_bounds_inflation = if let Some(rect_stroke) = n.rectangular_stroke_width() { + compute_inflation_rectangular(&rect_stroke, n.stroke_style.stroke_align, &n.effects) + } else { + compute_inflation_uniform( + n.render_bounds_stroke_width(), + n.stroke_style.stroke_align, + &n.effects, + ) + }; + + NodeGeoData { + schema_transform, + schema_width: n.layout_dimensions.layout_target_width.unwrap_or(0.0), + schema_height: n.layout_dimensions.layout_target_height.unwrap_or(0.0), + kind: GeoNodeKind::Container, + render_bounds_inflation, + rotation: n.rotation, + } + } + Node::TextSpan(n) => NodeGeoData { + schema_transform: n.transform, + schema_width: n.width.unwrap_or(0.0), + schema_height: n.height.unwrap_or(0.0), + kind: GeoNodeKind::TextSpan, + render_bounds_inflation: compute_inflation_uniform( + n.stroke_width, + n.stroke_align, + &n.effects, + ), + rotation: 0.0, + }, + _ => { + // Leaf nodes: Rectangle, Ellipse, Image, RegularPolygon, + // RegularStarPolygon, Line, Polygon, Path, Vector, Error. + let (schema_transform, schema_width, schema_height) = match node { + Node::Rectangle(n) => (n.transform, n.size.width, n.size.height), + Node::Ellipse(n) => (n.transform, n.size.width, n.size.height), + Node::Image(n) => (n.transform, n.size.width, n.size.height), + Node::RegularPolygon(n) => (n.transform, n.size.width, n.size.height), + Node::RegularStarPolygon(n) => (n.transform, n.size.width, n.size.height), + Node::Line(n) => (n.transform, n.size.width, 0.0), + Node::Polygon(n) => { + let rect = n.rect(); + (n.transform, rect.width, rect.height) + } + Node::Path(n) => { + let rect = n.rect(); + (n.transform, rect.width, rect.height) + } + Node::Vector(n) => { + let rect = n.network.bounds(); + (n.transform, rect.width, rect.height) + } + Node::Error(n) => (n.transform, n.size.width, n.size.height), + _ => unreachable!("Non-leaf variants handled above"), + }; + + let render_bounds_inflation = extract_leaf_inflation(node); + + NodeGeoData { + schema_transform, + schema_width, + schema_height, + kind: GeoNodeKind::Leaf, + render_bounds_inflation, + rotation: 0.0, + } + } + } +} + +// --------------------------------------------------------------------------- +// Render bounds inflation computation — pure scalars, no heap allocations +// --------------------------------------------------------------------------- + +/// Stroke outset for a given alignment. +fn stroke_outset(align: StrokeAlign, width: f32) -> f32 { + match align { + StrokeAlign::Inside => 0.0, + StrokeAlign::Center => width / 2.0, + StrokeAlign::Outside => width, + } +} + +/// Compute per-side inflation from a uniform stroke + effects. +fn compute_inflation_uniform( + stroke_width: f32, + stroke_align: StrokeAlign, + effects: &super::schema::LayerEffects, +) -> RenderBoundsInflation { + let stroke_delta = stroke_outset(stroke_align, stroke_width); + let base = RenderBoundsInflation::uniform(stroke_delta); + expand_inflation_with_effects(base, effects) +} + +/// Compute per-side inflation from a rectangular (per-side) stroke + effects. +fn compute_inflation_rectangular( + rect_stroke: &RectangularStrokeWidth, + stroke_align: StrokeAlign, + effects: &super::schema::LayerEffects, +) -> RenderBoundsInflation { + let base = match stroke_align { + StrokeAlign::Center => RenderBoundsInflation { + top: rect_stroke.stroke_top_width / 2.0, + right: rect_stroke.stroke_right_width / 2.0, + bottom: rect_stroke.stroke_bottom_width / 2.0, + left: rect_stroke.stroke_left_width / 2.0, + }, + StrokeAlign::Inside => RenderBoundsInflation::ZERO, + StrokeAlign::Outside => RenderBoundsInflation { + top: rect_stroke.stroke_top_width, + right: rect_stroke.stroke_right_width, + bottom: rect_stroke.stroke_bottom_width, + left: rect_stroke.stroke_left_width, + }, + }; + expand_inflation_with_effects(base, effects) +} + +/// Expand a base inflation with the effect-induced expansion. +/// +/// Effects are additive: blur expands uniformly, drop shadows expand +/// asymmetrically (offset + blur + spread). +fn expand_inflation_with_effects( + base: RenderBoundsInflation, + effects: &super::schema::LayerEffects, +) -> RenderBoundsInflation { + let mut result = base; + + if let Some(blur_effect) = &effects.blur { + let radius = match &blur_effect.blur { + crate::cg::prelude::FeBlur::Gaussian(g) => g.radius * 3.0, + crate::cg::prelude::FeBlur::Progressive(p) => p.radius.max(p.radius2) * 3.0, + }; + result = result.expand(&RenderBoundsInflation::uniform(radius)); + } + + for shadow in &effects.shadows { + let effect: crate::cg::prelude::FilterEffect = shadow.clone().into(); + let shadow_inflation = compute_effect_inflation(&effect); + result = result.expand(&shadow_inflation); + } + + result +} + +/// Compute per-side inflation from a single filter effect. +fn compute_effect_inflation(effect: &crate::cg::prelude::FilterEffect) -> RenderBoundsInflation { + use crate::cg::prelude::FilterEffect; + match effect { + FilterEffect::LiquidGlass(glass) => RenderBoundsInflation::uniform(glass.blur_radius * 3.0), + FilterEffect::LayerBlur(blur) => { + let r = match &blur.blur { + crate::cg::prelude::FeBlur::Gaussian(g) => g.radius * 3.0, + crate::cg::prelude::FeBlur::Progressive(p) => p.radius.max(p.radius2) * 3.0, + }; + RenderBoundsInflation::uniform(r) + } + FilterEffect::BackdropBlur(blur) => { + let r = match &blur.blur { + crate::cg::prelude::FeBlur::Gaussian(g) => g.radius * 3.0, + crate::cg::prelude::FeBlur::Progressive(p) => p.radius.max(p.radius2) * 3.0, + }; + RenderBoundsInflation::uniform(r) + } + FilterEffect::DropShadow(shadow) => { + // Shadow creates a shifted, blurred copy of the shape. + // The per-side inflation from the original bounds is: + // side = max(0, blur*3 + spread ± offset) + // where the sign of the offset depends on direction. + let blur_r = shadow.blur * 3.0; + let spread = shadow.spread.max(0.0); + let base = blur_r + spread; + RenderBoundsInflation { + top: (base - shadow.dy).max(0.0), + right: (base + shadow.dx).max(0.0), + bottom: (base + shadow.dy).max(0.0), + left: (base - shadow.dx).max(0.0), + } + } + FilterEffect::Noise(_) | FilterEffect::InnerShadow(_) => RenderBoundsInflation::ZERO, + } +} + +/// Extract render bounds inflation for leaf nodes. +fn extract_leaf_inflation(node: &Node) -> RenderBoundsInflation { + match node { + Node::Rectangle(n) => { + if let Some(rect_stroke) = n.rectangular_stroke_width() { + compute_inflation_rectangular(&rect_stroke, n.stroke_style.stroke_align, &n.effects) + } else { + compute_inflation_uniform( + n.render_bounds_stroke_width(), + n.stroke_style.stroke_align, + &n.effects, + ) + } + } + Node::Ellipse(n) => compute_inflation_uniform( + n.render_bounds_stroke_width(), + n.stroke_style.stroke_align, + &n.effects, + ), + Node::Polygon(n) => compute_inflation_uniform( + n.render_bounds_stroke_width(), + n.stroke_style.stroke_align, + &n.effects, + ), + Node::RegularPolygon(n) => compute_inflation_uniform( + n.render_bounds_stroke_width(), + n.stroke_style.stroke_align, + &n.effects, + ), + Node::RegularStarPolygon(n) => compute_inflation_uniform( + n.render_bounds_stroke_width(), + n.stroke_style.stroke_align, + &n.effects, + ), + Node::Path(n) => compute_inflation_uniform( + n.stroke_width.value_or_zero(), + n.stroke_style.stroke_align, + &n.effects, + ), + Node::Vector(n) => { + compute_inflation_uniform(n.stroke_width, n.get_stroke_align(), &n.effects) + } + Node::Image(n) => compute_inflation_uniform( + n.render_bounds_stroke_width(), + n.stroke_style.stroke_align, + &n.effects, + ), + Node::Line(n) => { + compute_inflation_uniform(n.stroke_width, n.get_stroke_align(), &n.effects) + } + _ => RenderBoundsInflation::ZERO, + } +} + /// A scene graph that manages both the tree structure and node data. /// /// The SceneGraph maintains: /// - Root node IDs (direct children of the scene) /// - An adjacency list (parent->children) for the tree structure /// - A node repository for storing actual node data +/// - A parallel `geo_data` map with compact, schema-level geometry data +/// +/// The `geo_data` map is populated at construction time from the `Node` data. +/// It enables the geometry cache to compute transforms and bounds without +/// iterating over the full `Node` enum (critical for WASM performance). /// /// This provides a centralized, efficient way to manage scene hierarchy /// separate from node attributes. @@ -59,11 +438,28 @@ pub struct SceneGraph { /// Root node IDs - direct children of the scene roots: Vec, /// Parent to children adjacency list - links: HashMap>, + links: DenseNodeMap>, /// Node data repository nodes: NodeRepository, /// Optional display names for nodes (from the source file). names: HashMap, + /// Compact, schema-level geometry data per node. + /// + /// Populated at construction time from `Node` data. The geometry cache + /// reads this instead of iterating over the full `Node` enum. + geo_data: DenseNodeMap, + /// Compact, layer-relevant data per node. + /// + /// Populated at construction time from `Node` data. The effect tree and + /// layers DFS read this instead of iterating over the full `Node` enum + /// for visibility / opacity / blend mode checks. + layer_core: DenseNodeMap, + /// Whether the scene contains any flex layout containers. + /// + /// When `false`, the layout engine can skip Taffy entirely and use + /// schema positions/sizes directly — saving ~1,500ms for 136K-node + /// Figma imports where all containers use `LayoutMode::Normal`. + has_flex: bool, } impl SceneGraph { @@ -71,9 +467,12 @@ impl SceneGraph { pub fn new() -> Self { Self { roots: Vec::new(), - links: HashMap::new(), + links: DenseNodeMap::new(), nodes: NodeRepository::new(), names: HashMap::new(), + geo_data: DenseNodeMap::new(), + layer_core: DenseNodeMap::new(), + has_flex: false, } } @@ -86,6 +485,18 @@ impl SceneGraph { /// * `nodes` - Iterator of nodes to add to the repository /// * `links` - HashMap of parent->children relationships /// * `roots` - Root node IDs (direct children of the scene) + // TODO: Currently `new_from_snapshot` receives ALL nodes from the + // document (across all scenes) but only one scene's roots. This means + // geo_data, layer_core, and the node repository contain orphan nodes + // not reachable from the current scene's roots. Downstream passes + // (geometry, layout, layers, effects) already scope their work to + // root-reachable nodes, but the extraction loop and storage still pay + // O(total_nodes) instead of O(scene_nodes). In a multi-scene document + // this is wasted work and memory. + // + // Future: either filter `node_pairs` to only scene-reachable nodes + // before calling this, or accept a reachability set so extraction and + // insertion can be bounded to the current scene. pub fn new_from_snapshot( node_pairs: impl IntoIterator, links: HashMap>, @@ -93,13 +504,66 @@ impl SceneGraph { ) -> Self { let mut graph = Self::new(); - // Add all nodes to the repository with their explicit IDs + // Add all nodes to the repository with their explicit IDs, + // extracting compact geo data and layer core at the same time. + // Also detect whether any flex containers exist (for layout skip optimization). + let mut has_flex = false; for (id, node) in node_pairs { + graph.geo_data.insert(id, extract_geo_data(&node)); + let lc = extract_layer_core(&node); + if lc.is_flex { + has_flex = true; + } + graph.layer_core.insert(id, lc); graph.nodes.insert_with_id(id, node); } + graph.has_flex = has_flex; + + Self::finish_snapshot(graph, links, roots) + } - // Set up all links - graph.links = links; + /// Like [`new_from_snapshot`] but accepts pre-extracted `geo_data` and + /// `layer_core` maps, avoiding a second iteration over all nodes. + /// + /// Used by the FBS decoder which extracts compact data in its own + /// consume loop (Phase 4) while the Node fields are cache-hot. + pub fn new_from_snapshot_preextracted( + node_pairs: impl IntoIterator, + geo_data: impl IntoIterator, + layer_core: impl IntoIterator, + has_flex: bool, + links: HashMap>, + roots: Vec, + ) -> Self { + let mut graph = Self::new(); + + // Insert nodes without re-extracting — already done by caller. + for (id, node) in node_pairs { + graph.nodes.insert_with_id(id, node); + } + for (id, geo) in geo_data { + graph.geo_data.insert(id, geo); + } + for (id, lc) in layer_core { + graph.layer_core.insert(id, lc); + } + graph.has_flex = has_flex; + + Self::finish_snapshot(graph, links, roots) + } + + /// Shared tail for snapshot constructors: set links and roots. + fn finish_snapshot( + mut graph: Self, + links: HashMap>, + roots: Vec, + ) -> Self { + // Convert HashMap links to DenseNodeMap + let mut dense_links = DenseNodeMap::new(); + for (id, children) in links { + dense_links.insert(id, children); + } + graph.links = dense_links; // Set roots graph.roots = roots; @@ -115,17 +579,25 @@ impl SceneGraph { /// /// Returns the node's ID. pub fn append_child(&mut self, node: Node, parent: Parent) -> NodeId { + let geo = extract_geo_data(&node); + let lc = extract_layer_core(&node); + if lc.is_flex { + self.has_flex = true; + } let id = self.nodes.insert(node); + self.geo_data.insert(id, geo); + self.layer_core.insert(id, lc); match parent { Parent::Root => { self.roots.push(id.clone()); } Parent::NodeId(parent_id) => { - self.links - .entry(parent_id) - .or_insert_with(Vec::new) - .push(id.clone()); + if let Some(children) = self.links.get_mut(&parent_id) { + children.push(id.clone()); + } else { + self.links.insert(parent_id, vec![id.clone()]); + } } } @@ -201,7 +673,7 @@ impl SceneGraph { } /// Iterate over all parent->children pairs - pub fn iter(&self) -> impl Iterator)> { + pub fn iter(&self) -> impl Iterator)> { self.links.iter() } @@ -220,7 +692,7 @@ impl SceneGraph { pub fn get_parent(&self, id: &NodeId) -> Option { for (parent_id, children) in &self.links { if children.contains(id) { - return Some(*parent_id); + return Some(parent_id); } } None @@ -256,6 +728,7 @@ impl SceneGraph { /// Remove a node from the repository and return it pub fn remove_node(&mut self, id: &NodeId) -> SceneGraphResult { + self.geo_data.remove(id); self.nodes .remove(id) .ok_or_else(|| SceneGraphError::NodeNotFound(id.clone())) @@ -280,10 +753,37 @@ impl SceneGraph { /// /// The iteration order is not guaranteed; callers should use `roots()` + /// `get_children()` if they need tree order. - pub fn nodes_iter(&self) -> impl Iterator { + pub fn nodes_iter(&self) -> impl Iterator { self.nodes.iter() } + /// Access the compact, schema-level geometry data map. + /// + /// This map is populated at construction time and contains only the fields + /// needed for geometry computation (~48 bytes/node instead of ~500+). + /// The geometry cache reads this instead of iterating over the full Node enum. + pub fn geo_data(&self) -> &DenseNodeMap { + &self.geo_data + } + + /// Access the compact layer-core data map. + pub fn layer_core(&self) -> &DenseNodeMap { + &self.layer_core + } + + /// Get layer-core data for a single node. + pub fn get_layer_core(&self, id: &NodeId) -> Option<&NodeLayerCore> { + self.layer_core.get(id) + } + + /// Whether the scene contains any flex layout containers. + /// + /// When `false`, all containers use `LayoutMode::Normal` (absolute + /// positioning) and the layout engine can skip Taffy entirely. + pub fn has_flex(&self) -> bool { + self.has_flex + } + // ------------------------------------------------------------------------- // Tree Traversal Methods // ------------------------------------------------------------------------- diff --git a/crates/grida-canvas/src/node/schema.rs b/crates/grida-canvas/src/node/schema.rs index 78000f9ee5..325735922e 100644 --- a/crates/grida-canvas/src/node/schema.rs +++ b/crates/grida-canvas/src/node/schema.rs @@ -894,6 +894,216 @@ impl Default for LayoutDimensionStyle { } } +/// Discriminant tag for the [`Node`] enum — lets hot loops dispatch on node +/// type without touching the full 500+ byte `Node` variant. +/// +/// Used by [`NodeLayerCore`] and performance-critical DFS paths. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[repr(u8)] +pub enum NodeTypeTag { + InitialContainer, + Container, + Error, + Group, + Rectangle, + Ellipse, + Polygon, + RegularPolygon, + RegularStarPolygon, + Line, + TextSpan, + Path, + Vector, + BooleanOperation, + Image, +} + +/// Compact, layer-relevant data extracted from a `Node` at construction time. +/// +/// Stored in a parallel `DenseNodeMap` on `SceneGraph` so that the layers DFS +/// and effect tree never need to iterate over the full `Node` enum for basic +/// visibility / dispatch checks. +/// +/// This struct is `Copy` — no heap allocations, ~16 bytes total. +#[derive(Debug, Clone, Copy)] +pub struct NodeLayerCore { + /// Whether this node is visible. + pub active: bool, + /// Node opacity (0.0–1.0). + pub opacity: f32, + /// Blend mode. + pub blend_mode: LayerBlendMode, + /// Mask type (if any). + pub mask: Option, + /// Whether this container clips its descendants. + pub clips_content: bool, + /// Whether the node has any non-empty effects (quick check). + pub has_effects: bool, + /// Node type discriminant for dispatch. + pub node_type: NodeTypeTag, + /// Whether this node is a flex layout container (Container with LayoutMode::Flex + /// or InitialContainer). Used by the layout engine to skip Taffy for normal containers. + pub is_flex: bool, +} + +/// Extract compact layer-core data from a `Node`. +/// +/// Called once per node during `SceneGraph` construction. All values are `Copy`. +pub fn extract_layer_core(node: &Node) -> NodeLayerCore { + match node { + Node::InitialContainer(n) => NodeLayerCore { + active: n.active, + opacity: 1.0, + blend_mode: LayerBlendMode::PassThrough, + mask: None, + clips_content: false, + has_effects: false, + node_type: NodeTypeTag::InitialContainer, + is_flex: true, // ICB always uses flex layout + }, + Node::Container(n) => NodeLayerCore { + active: n.active, + opacity: n.opacity, + blend_mode: n.blend_mode, + mask: n.mask, + clips_content: n.clip, + has_effects: !n.effects.is_empty(), + node_type: NodeTypeTag::Container, + is_flex: n.layout_container.layout_mode == crate::cg::types::LayoutMode::Flex, + }, + Node::Error(n) => NodeLayerCore { + active: n.active, + opacity: n.opacity, + blend_mode: LayerBlendMode::PassThrough, + mask: None, + clips_content: false, + has_effects: false, + node_type: NodeTypeTag::Error, + is_flex: false, + }, + Node::Group(n) => NodeLayerCore { + active: n.active, + opacity: n.opacity, + blend_mode: n.blend_mode, + mask: n.mask, + clips_content: false, + has_effects: false, + node_type: NodeTypeTag::Group, + is_flex: false, + }, + Node::Rectangle(n) => NodeLayerCore { + active: n.active, + opacity: n.opacity, + blend_mode: n.blend_mode, + mask: n.mask, + clips_content: false, + has_effects: !n.effects.is_empty(), + node_type: NodeTypeTag::Rectangle, + is_flex: false, + }, + Node::Ellipse(n) => NodeLayerCore { + active: n.active, + opacity: n.opacity, + blend_mode: n.blend_mode, + mask: n.mask, + clips_content: false, + has_effects: !n.effects.is_empty(), + node_type: NodeTypeTag::Ellipse, + is_flex: false, + }, + Node::Polygon(n) => NodeLayerCore { + active: n.active, + opacity: n.opacity, + blend_mode: n.blend_mode, + mask: n.mask, + clips_content: false, + has_effects: !n.effects.is_empty(), + node_type: NodeTypeTag::Polygon, + is_flex: false, + }, + Node::RegularPolygon(n) => NodeLayerCore { + active: n.active, + opacity: n.opacity, + blend_mode: n.blend_mode, + mask: n.mask, + clips_content: false, + has_effects: !n.effects.is_empty(), + node_type: NodeTypeTag::RegularPolygon, + is_flex: false, + }, + Node::RegularStarPolygon(n) => NodeLayerCore { + active: n.active, + opacity: n.opacity, + blend_mode: n.blend_mode, + mask: n.mask, + clips_content: false, + has_effects: !n.effects.is_empty(), + node_type: NodeTypeTag::RegularStarPolygon, + is_flex: false, + }, + Node::Line(n) => NodeLayerCore { + active: n.active, + opacity: n.opacity, + blend_mode: n.blend_mode, + mask: n.mask, + clips_content: false, + has_effects: !n.effects.is_empty(), + node_type: NodeTypeTag::Line, + is_flex: false, + }, + Node::TextSpan(n) => NodeLayerCore { + active: n.active, + opacity: n.opacity, + blend_mode: n.blend_mode, + mask: n.mask, + clips_content: false, + has_effects: !n.effects.is_empty(), + node_type: NodeTypeTag::TextSpan, + is_flex: false, + }, + Node::Path(n) => NodeLayerCore { + active: n.active, + opacity: n.opacity, + blend_mode: n.blend_mode, + mask: n.mask, + clips_content: false, + has_effects: !n.effects.is_empty(), + node_type: NodeTypeTag::Path, + is_flex: false, + }, + Node::Vector(n) => NodeLayerCore { + active: n.active, + opacity: n.opacity, + blend_mode: n.blend_mode, + mask: n.mask, + clips_content: false, + has_effects: !n.effects.is_empty(), + node_type: NodeTypeTag::Vector, + is_flex: false, + }, + Node::BooleanOperation(n) => NodeLayerCore { + active: n.active, + opacity: n.opacity, + blend_mode: n.blend_mode, + mask: n.mask, + clips_content: false, + has_effects: !n.effects.is_empty(), + node_type: NodeTypeTag::BooleanOperation, + is_flex: false, + }, + Node::Image(n) => NodeLayerCore { + active: n.active, + opacity: n.opacity, + blend_mode: n.blend_mode, + mask: n.mask, + clips_content: false, + has_effects: !n.effects.is_empty(), + node_type: NodeTypeTag::Image, + is_flex: false, + }, + } +} + #[derive(Debug, Clone)] pub enum Node { InitialContainer(InitialContainerNodeRec), diff --git a/crates/grida-canvas/src/os/emscripten.rs b/crates/grida-canvas/src/os/emscripten.rs index 8caba22b6e..dbf4564c55 100644 --- a/crates/grida-canvas/src/os/emscripten.rs +++ b/crates/grida-canvas/src/os/emscripten.rs @@ -1,4 +1,5 @@ #![cfg(target_os = "emscripten")] +#![allow(non_camel_case_types)] // // emscripten bindings @@ -6,6 +7,9 @@ // - https://github.com/ALEX11BR/emscripten-functions/tree/main/emscripten-functions-sys // +pub type em_callback_func = ::std::option::Option; +pub type em_arg_callback_func = + ::std::option::Option; unsafe extern "C" { pub fn emscripten_GetProcAddress( name: *const ::std::os::raw::c_char, @@ -25,6 +29,18 @@ unsafe extern "C" { pub fn emscripten_cancel_animation_frame(request_animation_frame_id: ::std::os::raw::c_int); } +extern "C" { + pub fn emscripten_get_now() -> f64; +} + +extern "C" { + pub fn emscripten_random() -> f32; +} + +extern "C" { + pub fn emscripten_get_device_pixel_ratio() -> f64; +} + unsafe extern "C" { pub fn emscripten_request_animation_frame_loop( cb: ::std::option::Option< @@ -33,3 +49,40 @@ unsafe extern "C" { user_data: *mut ::std::os::raw::c_void, ); } + +extern "C" { + pub fn emscripten_set_main_loop( + func: em_callback_func, + fps: ::std::os::raw::c_int, + simulate_infinite_loop: bool, + ); +} +extern "C" { + pub fn emscripten_set_main_loop_timing( + mode: ::std::os::raw::c_int, + value: ::std::os::raw::c_int, + ) -> ::std::os::raw::c_int; +} +extern "C" { + pub fn emscripten_get_main_loop_timing( + mode: *mut ::std::os::raw::c_int, + value: *mut ::std::os::raw::c_int, + ); +} +extern "C" { + pub fn emscripten_set_main_loop_arg( + func: em_arg_callback_func, + arg: *mut ::std::os::raw::c_void, + fps: ::std::os::raw::c_int, + simulate_infinite_loop: bool, + ); +} +extern "C" { + pub fn emscripten_pause_main_loop(); +} +extern "C" { + pub fn emscripten_resume_main_loop(); +} +extern "C" { + pub fn emscripten_cancel_main_loop(); +} diff --git a/crates/grida-canvas/src/painter/layer.rs b/crates/grida-canvas/src/painter/layer.rs index 8f12933b97..fc987645d7 100644 --- a/crates/grida-canvas/src/painter/layer.rs +++ b/crates/grida-canvas/src/painter/layer.rs @@ -459,8 +459,17 @@ impl LayerList { stroke_overlaps_fill: bool, fills: &Paints, strokes: &Paints, + node_opacity: f32, ) -> Option { - if !stroke_overlaps_fill || fills.is_empty() || strokes.is_empty() { + // The non-overlapping fill path is only needed when the node's own + // opacity < 1.0. At full opacity, stroke/fill overlap is invisible — + // no compositing artifact, no need for the expensive PathOp::Difference. + // Parent opacity is handled at the parent level via save_layer_alpha. + if node_opacity >= 1.0 + || !stroke_overlaps_fill + || fills.is_empty() + || strokes.is_empty() + { return None; } stroke_path.and_then(|sp| { @@ -475,14 +484,18 @@ impl LayerList { parent_opacity: f32, out: &mut Vec, ) -> FlattenResult { + // Fast-path: check active from compact layer_core (~16 bytes) + // before touching the full Node enum (~500+ bytes). + if let Some(lc) = graph.get_layer_core(id) { + if !lc.active { + return FlattenResult::default(); + } + } + let Ok(node) = graph.get_node(id) else { return FlattenResult::default(); }; - if !node.active() { - return FlattenResult::default(); - } - let transform = scene_cache .geometry() .get_world_transform(id) @@ -560,7 +573,7 @@ impl LayerList { let strokes = Self::filter_visible_paints(&n.strokes); let stroke_overlaps_fill = !matches!(n.stroke_style.stroke_align, StrokeAlign::Outside); let non_overlapping_fill_path = Self::compute_non_overlapping_fill_path( - &shape, stroke_path.as_ref(), stroke_overlaps_fill, &fills, &strokes, + &shape, stroke_path.as_ref(), stroke_overlaps_fill, &fills, &strokes, n.opacity, ); let layer = PainterPictureLayer::Shape(PainterPictureShapeLayer { @@ -670,7 +683,7 @@ impl LayerList { let strokes = Self::filter_visible_paints(&n.strokes); let stroke_overlaps_fill = !matches!(n.stroke_style.stroke_align, StrokeAlign::Outside); let non_overlapping_fill_path = Self::compute_non_overlapping_fill_path( - &shape, stroke_path.as_ref(), stroke_overlaps_fill, &fills, &strokes, + &shape, stroke_path.as_ref(), stroke_overlaps_fill, &fills, &strokes, n.opacity, ); let layer = PainterPictureLayer::Shape(PainterPictureShapeLayer { @@ -732,7 +745,7 @@ impl LayerList { let strokes = Self::filter_visible_paints(&n.strokes); let stroke_overlaps_fill = !matches!(n.stroke_style.stroke_align, StrokeAlign::Outside); let non_overlapping_fill_path = Self::compute_non_overlapping_fill_path( - &shape, stroke_path.as_ref(), stroke_overlaps_fill, &fills, &strokes, + &shape, stroke_path.as_ref(), stroke_overlaps_fill, &fills, &strokes, n.opacity, ); let layer = PainterPictureLayer::Shape(PainterPictureShapeLayer { @@ -788,7 +801,7 @@ impl LayerList { let strokes = Self::filter_visible_paints(&n.strokes); let stroke_overlaps_fill = !matches!(n.stroke_style.stroke_align, StrokeAlign::Outside); let non_overlapping_fill_path = Self::compute_non_overlapping_fill_path( - &shape, stroke_path.as_ref(), stroke_overlaps_fill, &fills, &strokes, + &shape, stroke_path.as_ref(), stroke_overlaps_fill, &fills, &strokes, n.opacity, ); let layer = PainterPictureLayer::Shape(PainterPictureShapeLayer { @@ -844,7 +857,7 @@ impl LayerList { let strokes = Self::filter_visible_paints(&n.strokes); let stroke_overlaps_fill = !matches!(n.stroke_style.stroke_align, StrokeAlign::Outside); let non_overlapping_fill_path = Self::compute_non_overlapping_fill_path( - &shape, stroke_path.as_ref(), stroke_overlaps_fill, &fills, &strokes, + &shape, stroke_path.as_ref(), stroke_overlaps_fill, &fills, &strokes, n.opacity, ); let layer = PainterPictureLayer::Shape(PainterPictureShapeLayer { @@ -900,7 +913,7 @@ impl LayerList { let strokes = Self::filter_visible_paints(&n.strokes); let stroke_overlaps_fill = !matches!(n.stroke_style.stroke_align, StrokeAlign::Outside); let non_overlapping_fill_path = Self::compute_non_overlapping_fill_path( - &shape, stroke_path.as_ref(), stroke_overlaps_fill, &fills, &strokes, + &shape, stroke_path.as_ref(), stroke_overlaps_fill, &fills, &strokes, n.opacity, ); let layer = PainterPictureLayer::Shape(PainterPictureShapeLayer { @@ -956,7 +969,7 @@ impl LayerList { let strokes = Self::filter_visible_paints(&n.strokes); let stroke_overlaps_fill = !matches!(n.stroke_style.stroke_align, StrokeAlign::Outside); let non_overlapping_fill_path = Self::compute_non_overlapping_fill_path( - &shape, stroke_path.as_ref(), stroke_overlaps_fill, &fills, &strokes, + &shape, stroke_path.as_ref(), stroke_overlaps_fill, &fills, &strokes, n.opacity, ); let layer = PainterPictureLayer::Shape(PainterPictureShapeLayer { @@ -1148,7 +1161,7 @@ impl LayerList { let strokes = Self::filter_visible_paints(&n.strokes); let stroke_overlaps_fill = !matches!(n.stroke_style.stroke_align, StrokeAlign::Outside); let non_overlapping_fill_path = Self::compute_non_overlapping_fill_path( - &shape, stroke_path.as_ref(), stroke_overlaps_fill, &fills, &strokes, + &shape, stroke_path.as_ref(), stroke_overlaps_fill, &fills, &strokes, n.opacity, ); let layer = PainterPictureLayer::Shape(PainterPictureShapeLayer { @@ -1246,7 +1259,7 @@ impl LayerList { let strokes = Self::filter_visible_paints(&n.strokes); let stroke_overlaps_fill = !matches!(n.stroke_style.stroke_align, StrokeAlign::Outside); let non_overlapping_fill_path = Self::compute_non_overlapping_fill_path( - &shape, stroke_path.as_ref(), stroke_overlaps_fill, &fills, &strokes, + &shape, stroke_path.as_ref(), stroke_overlaps_fill, &fills, &strokes, n.opacity, ); let layer = PainterPictureLayer::Shape(PainterPictureShapeLayer { diff --git a/crates/grida-canvas/src/painter/painter.rs b/crates/grida-canvas/src/painter/painter.rs index f4d84aac89..284d4caf6a 100644 --- a/crates/grida-canvas/src/painter/painter.rs +++ b/crates/grida-canvas/src/painter/painter.rs @@ -21,8 +21,8 @@ use skia_safe::{ canvas::SaveLayerRec, textlayout, Matrix, Paint as SkPaint, Path, PathBuilder, Point, Rect, Shader, }; +use crate::cache::fast_hash::NodeIdHashMap; use std::cell::{Cell, RefCell}; -use std::collections::HashMap; use std::rc::Rc; /// Pre-extracted blit data for a single promoted (compositor-cached) node. @@ -58,7 +58,7 @@ pub struct Painter<'a> { /// Pre-extracted blit data for promoted (compositor-cached) nodes. /// When present, promoted nodes are blitted inline at their correct /// z-position instead of being skipped. - promoted_blits: Option<&'a HashMap>, + promoted_blits: Option<&'a NodeIdHashMap>, } impl<'a> Painter<'a> { @@ -113,7 +113,7 @@ impl<'a> Painter<'a> { /// Set the promoted blit map. Nodes in this map will be blitted from /// their pre-extracted compositor cache data at the correct z-position /// in the render command tree, instead of being re-drawn live. - pub fn with_promoted_blits(mut self, blits: &'a HashMap) -> Self { + pub fn with_promoted_blits(mut self, blits: &'a NodeIdHashMap) -> Self { self.promoted_blits = Some(blits); self } diff --git a/crates/grida-canvas/src/resources/mod.rs b/crates/grida-canvas/src/resources/mod.rs index 5d2162ae4b..8247d53a07 100644 --- a/crates/grida-canvas/src/resources/mod.rs +++ b/crates/grida-canvas/src/resources/mod.rs @@ -118,28 +118,93 @@ pub struct FontMessage { pub data: Vec, } -/// Extract all image URLs from a scene. -pub fn extract_image_urls(scene: &Scene) -> Vec { - // FIXME: this should either iterate the fills / strokes (all paints) rather then iterating the nodes. - the below implementation is legacy. +/// Collect the URL string from a [`ResourceRef`]. +#[allow(dead_code)] +fn resource_ref_url(r: &ResourceRef) -> &str { + match r { + ResourceRef::RID(s) | ResourceRef::HASH(s) => s, + } +} + +/// Push any image URLs found in a paint slice. +#[allow(dead_code)] +fn collect_image_urls_from_paints(paints: &[Paint], out: &mut Vec) { + for paint in paints { + if let Paint::Image(img) = paint { + let url = resource_ref_url(&img.image); + if !url.is_empty() { + out.push(url.to_owned()); + } + } + } +} + +/// Extract all image URLs from a scene by inspecting every node's fills, +/// strokes, and dedicated image references. +// TODO: consider a dedicated paints store or iterator so this doesn't need +// to match every node variant individually. +#[allow(dead_code)] +fn extract_image_urls(scene: &Scene) -> Vec { + use crate::node::schema::Node; let mut urls = Vec::new(); for (id, _) in scene.graph.iter() { - if let Ok(n) = scene.graph.get_node(id) { - if let crate::node::schema::Node::Rectangle(rect) = n { - for fill in &rect.fills { - if let Paint::Image(img) = fill { - match &img.image { - ResourceRef::RID(r) | ResourceRef::HASH(r) => urls.push(r.clone()), - } - } - } - for stroke in &rect.strokes { - if let Paint::Image(img) = stroke { - match &img.image { - ResourceRef::RID(r) | ResourceRef::HASH(r) => urls.push(r.clone()), - } - } + let Ok(node) = scene.graph.get_node(&id) else { + continue; + }; + + match node { + Node::Image(n) => { + let url = resource_ref_url(&n.fill.image); + if !url.is_empty() { + urls.push(url.to_owned()); } } + Node::Rectangle(n) => { + collect_image_urls_from_paints(&n.fills, &mut urls); + collect_image_urls_from_paints(&n.strokes, &mut urls); + } + Node::Ellipse(n) => { + collect_image_urls_from_paints(&n.fills, &mut urls); + collect_image_urls_from_paints(&n.strokes, &mut urls); + } + Node::Container(n) => { + collect_image_urls_from_paints(&n.fills, &mut urls); + collect_image_urls_from_paints(&n.strokes, &mut urls); + } + Node::Vector(n) => { + collect_image_urls_from_paints(&n.fills, &mut urls); + collect_image_urls_from_paints(&n.strokes, &mut urls); + } + Node::Polygon(n) => { + collect_image_urls_from_paints(&n.fills, &mut urls); + collect_image_urls_from_paints(&n.strokes, &mut urls); + } + Node::RegularPolygon(n) => { + collect_image_urls_from_paints(&n.fills, &mut urls); + collect_image_urls_from_paints(&n.strokes, &mut urls); + } + Node::RegularStarPolygon(n) => { + collect_image_urls_from_paints(&n.fills, &mut urls); + collect_image_urls_from_paints(&n.strokes, &mut urls); + } + Node::Path(n) => { + collect_image_urls_from_paints(&n.fills, &mut urls); + collect_image_urls_from_paints(&n.strokes, &mut urls); + } + Node::BooleanOperation(n) => { + collect_image_urls_from_paints(&n.fills, &mut urls); + collect_image_urls_from_paints(&n.strokes, &mut urls); + } + Node::TextSpan(n) => { + collect_image_urls_from_paints(&n.fills, &mut urls); + collect_image_urls_from_paints(&n.strokes, &mut urls); + } + Node::Line(n) => { + // LineNodeRec has strokes only, no fills. + collect_image_urls_from_paints(&n.strokes, &mut urls); + } + // Group, InitialContainer, and Error nodes have no paint data. + Node::Group(_) | Node::InitialContainer(_) | Node::Error(_) => {} } } urls diff --git a/crates/grida-canvas/src/runtime/effect_tree.rs b/crates/grida-canvas/src/runtime/effect_tree.rs index 3ea05b2195..e73f9d98f7 100644 --- a/crates/grida-canvas/src/runtime/effect_tree.rs +++ b/crates/grida-canvas/src/runtime/effect_tree.rs @@ -39,11 +39,11 @@ //! } //! ``` +use crate::cache::fast_hash::{new_node_id_map, NodeIdHashMap}; use crate::cg::types::LayerBlendMode; use crate::node::id::NodeId; use crate::node::scene_graph::SceneGraph; -use crate::node::schema::{Node, NodeTrait}; -use std::collections::HashMap; +use crate::node::schema::NodeLayerCore; /// Why a node needs a render surface. /// @@ -110,7 +110,7 @@ impl EffectNode { pub struct EffectTree { /// Map from NodeId to its EffectNode data. /// Only contains nodes that need render surfaces. - nodes: HashMap, + nodes: NodeIdHashMap, /// Total number of render surfaces (== nodes.len()). surface_count: usize, /// Summary statistics for diagnostics. @@ -143,7 +143,7 @@ impl EffectTree { /// Create an empty effect tree (no render surfaces). pub fn empty() -> Self { Self { - nodes: HashMap::new(), + nodes: new_node_id_map(), surface_count: 0, stats: EffectTreeStats::default(), } @@ -155,7 +155,7 @@ impl EffectTree { /// surfaces. This is a full rebuild — no incremental state is carried /// over. pub fn build(graph: &SceneGraph) -> Self { - let mut nodes = HashMap::new(); + let mut nodes = new_node_id_map(); let mut stats = EffectTreeStats::default(); for root_id in graph.roots() { @@ -173,49 +173,45 @@ impl EffectTree { } /// Recursive visitor that checks each node for render surface triggers. + /// + /// Uses `layer_core` (compact ~16-byte struct) for all visibility, opacity, + /// blend mode, and mask checks — never touches the full 500+ byte `Node` + /// enum unless the node has effects that need detail inspection. fn visit( graph: &SceneGraph, id: &NodeId, - nodes: &mut HashMap, + nodes: &mut NodeIdHashMap, stats: &mut EffectTreeStats, ) { stats.nodes_visited += 1; - let node = match graph.get_node(id) { - Ok(n) => n, - Err(_) => return, + // Read from compact layer_core (~16 bytes) instead of full Node (~500 bytes). + let lc = match graph.get_layer_core(id) { + Some(lc) => *lc, + None => return, }; // Skip inactive nodes — they produce no visual output. - if !node.active() { + if !lc.active { return; } - let all_children: Vec = graph - .get_children(id) - .cloned() - .unwrap_or_default(); + let all_children = graph.get_children(id); + let all_children_slice = all_children.map(|c| c.as_slice()).unwrap_or(&[]); - // Filter to only active children — inactive nodes should not - // trigger render surface reasons (mask, shadow promotion, etc.). - let children: Vec = all_children + // Count visible children using layer_core (no full Node touch). + let layer_core_map = graph.layer_core(); + let visible_child_count = all_children_slice .iter() - .filter(|cid| { - graph - .get_node(cid) - .map(|n| n.active()) - .unwrap_or(false) - }) - .copied() - .collect(); - - let visible_child_count = children.len(); + .filter(|cid| layer_core_map.get(cid).map(|c| c.active).unwrap_or(false)) + .count(); - // Collect render surface reasons for this node. - let reasons = Self::classify(node, visible_child_count, &children, graph); + // Collect render surface reasons using layer_core for fast checks. + // Only access full Node when effects detail is needed. + let reasons = + Self::classify_from_core(id, &lc, visible_child_count, all_children_slice, graph); if !reasons.is_empty() { - // Update per-reason stats. for reason in &reasons { match reason { RenderSurfaceReason::Opacity => stats.by_reason.opacity += 1, @@ -227,12 +223,19 @@ impl EffectTree { } } + // Only allocate the children Vec for nodes that actually need a surface. + let active_children: Vec = all_children_slice + .iter() + .filter(|cid| layer_core_map.get(cid).map(|c| c.active).unwrap_or(false)) + .copied() + .collect(); + nodes.insert( *id, EffectNode { id: *id, reasons, - children: children.clone(), + children: active_children, visible_child_count, }, ); @@ -240,16 +243,19 @@ impl EffectTree { // Recurse into all children (including inactive ones, which will // early-return in visit) so the full tree is traversed. - for child_id in &all_children { + for child_id in all_children_slice { Self::visit(graph, child_id, nodes, stats); } } - /// Classify a node: determine which render surface reasons apply. + /// Classify a node using `NodeLayerCore` for fast-path checks. /// - /// Returns an empty vec if the node doesn't need a render surface. - fn classify( - node: &Node, + /// Only accesses the full `Node` when the node has effects (blur/shadows) + /// that need detail inspection. For the majority of nodes (no effects), + /// this never touches the 500+ byte Node enum. + fn classify_from_core( + id: &NodeId, + lc: &NodeLayerCore, visible_child_count: usize, children: &[NodeId], graph: &SceneGraph, @@ -257,66 +263,51 @@ impl EffectTree { let mut reasons = Vec::new(); // --- Opacity isolation --- - // Only needed when a container-like node has opacity < 1.0 AND - // has 2+ visible children. A single child (or leaf) can have - // opacity applied directly without isolation. - if node.opacity() < 1.0 && visible_child_count >= 2 { + if lc.opacity < 1.0 && visible_child_count >= 2 { reasons.push(RenderSurfaceReason::Opacity); } // --- Blend mode isolation --- - // Non-PassThrough blend modes require the subtree to be drawn into - // an offscreen before blending with the backdrop. Only applies to - // container-like nodes with children (leaf blend modes are handled - // by the painter directly). - if node.blend_mode() != LayerBlendMode::PassThrough && visible_child_count >= 1 { + if lc.blend_mode != LayerBlendMode::PassThrough && visible_child_count >= 1 { reasons.push(RenderSurfaceReason::BlendMode); } // --- Effects that benefit from render surfaces --- - if let Some(effects) = node.effects() { - // Layer blur: applies to entire subtree content. - // Only creates a surface when there are children — leaf nodes - // have their blur applied directly by the painter. - if effects.blur.as_ref().is_some_and(|b| b.active) && visible_child_count >= 1 { - reasons.push(RenderSurfaceReason::LayerBlur); - } - - // Shadows: a render surface lets us compute the shadow filter - // once for the entire subtree instead of per-child. - // Only for container-like nodes — leaf shadows are per-node. - if !effects.shadows.is_empty() && visible_child_count >= 1 { - reasons.push(RenderSurfaceReason::Shadow); + // Only access full Node when has_effects is true (minority of nodes). + if lc.has_effects && visible_child_count >= 1 { + if let Ok(node) = graph.get_node(id) { + if let Some(effects) = node.effects() { + if effects.blur.as_ref().is_some_and(|b| b.active) { + reasons.push(RenderSurfaceReason::LayerBlur); + } + if effects.shadows.iter().any(|s| s.active()) { + reasons.push(RenderSurfaceReason::Shadow); + } + } } - - // Note: backdrop_blur and liquid_glass are context-dependent - // (they read from content behind the node). They are handled - // per-node by the painter and do NOT create render surfaces. } // --- Clip --- - // Container clip=true requires clipping descendants to the - // container's shape. This is a render surface trigger. - if node.clips_content() && visible_child_count >= 1 { + if lc.clips_content && visible_child_count >= 1 { reasons.push(RenderSurfaceReason::Clip); } // --- Mask groups --- - // If any child is a mask node, this parent needs a render surface - // to implement the mask compositing (DstIn blend). - if Self::has_mask_children(children, graph) { + // Check children's mask from layer_core (no full Node needed). + if Self::has_mask_children_from_core(children, graph) { reasons.push(RenderSurfaceReason::Mask); } reasons } - /// Check if any of the given children are mask nodes. - fn has_mask_children(children: &[NodeId], graph: &SceneGraph) -> bool { + /// Check if any of the given children are active mask nodes using layer_core. + fn has_mask_children_from_core(children: &[NodeId], graph: &SceneGraph) -> bool { + let layer_core_map = graph.layer_core(); children.iter().any(|cid| { - graph - .get_node(cid) - .map(|n| n.mask().is_some()) + layer_core_map + .get(cid) + .map(|c| c.active && c.mask.is_some()) .unwrap_or(false) }) } @@ -494,10 +485,7 @@ mod tests { crate::node::scene_graph::Parent::Root, ); for _ in 0..5 { - graph.append_child( - rect_node(), - crate::node::scene_graph::Parent::NodeId(root), - ); + graph.append_child(rect_node(), crate::node::scene_graph::Parent::NodeId(root)); } let tree = EffectTree::build(&graph); @@ -788,20 +776,17 @@ mod tests { ); // Two children under each - graph.append_child( - rect_node(), - crate::node::scene_graph::Parent::NodeId(root), - ); - graph.append_child( - rect_node(), - crate::node::scene_graph::Parent::NodeId(inner), - ); + graph.append_child(rect_node(), crate::node::scene_graph::Parent::NodeId(root)); + graph.append_child(rect_node(), crate::node::scene_graph::Parent::NodeId(inner)); let tree = EffectTree::build(&graph); // Root needs surface for opacity (2 visible children: inner + rect). assert!(tree.needs_surface(&root)); - assert!(tree.get(&root).unwrap().has_reason(RenderSurfaceReason::Opacity)); + assert!(tree + .get(&root) + .unwrap() + .has_reason(RenderSurfaceReason::Opacity)); // Inner needs surface for clip (1 visible child). assert!(tree.needs_surface(&inner)); diff --git a/crates/grida-canvas/src/runtime/frame_loop.rs b/crates/grida-canvas/src/runtime/frame_loop.rs index dd2455f567..30305ca475 100644 --- a/crates/grida-canvas/src/runtime/frame_loop.rs +++ b/crates/grida-canvas/src/runtime/frame_loop.rs @@ -25,10 +25,19 @@ pub struct FrameLoop { next_frame: u64, /// Host-time (ms) of last invalidation. last_change_time: f64, - /// Debounce threshold in milliseconds before stable frame fires. + /// Base debounce threshold in milliseconds before stable frame fires. stable_delay_ms: f64, /// True after an unstable render, until a stable render completes. needs_stable: bool, + /// Smoothed input interval (EMA, milliseconds) for adaptive stable delay. + /// + /// During slow trackpad scrolling, events arrive at 60-120ms intervals — + /// longer than the base 50ms stable delay. Without adaptation, every gap + /// triggers a stable frame that nukes the pan cache and forces a full + /// redraw. By tracking the input cadence, `poll()` extends the effective + /// delay to `max(base, cadence * CADENCE_MULTIPLIER)`, ensuring stable + /// frames only fire when the user truly stops interacting. + input_cadence_ms: f64, } /// What quality of frame to render. @@ -50,6 +59,21 @@ impl FrameLoop { /// Default debounce delay (milliseconds) before a stable frame fires. pub const DEFAULT_STABLE_DELAY_MS: f64 = 50.0; + /// Multiplier applied to the smoothed input cadence. + /// + /// Effective stable delay = max(base_delay, cadence × multiplier). + /// At 2.5×, an 80ms trackpad cadence yields a 200ms effective delay, + /// comfortably above the 80ms inter-event gap. + const CADENCE_MULTIPLIER: f64 = 2.5; + + /// Intervals longer than this (ms) are treated as session breaks and + /// do not update the cadence EMA. + const CADENCE_MAX_INTERVAL: f64 = 500.0; + + /// EMA smoothing factor for input cadence (0 < α ≤ 1). + /// Lower values = more smoothing, slower to react. + const CADENCE_ALPHA: f64 = 0.3; + /// Create a new `FrameLoop` with the default stable delay. pub fn new() -> Self { Self::with_stable_delay(Self::DEFAULT_STABLE_DELAY_MS) @@ -60,20 +84,50 @@ impl FrameLoop { Self { prev_frame: 0, next_frame: 0, - last_change_time: 0.0, + last_change_time: f64::NAN, // NAN = no prior invalidation stable_delay_ms, needs_stable: false, + input_cadence_ms: 0.0, } } - /// Something changed. Bumps `next_frame`, records timestamp, sets - /// `needs_stable = true`. O(1), no plan building. + /// Something changed. Bumps `next_frame`, records timestamp, updates + /// the input cadence EMA, and sets `needs_stable = true`. O(1). pub fn invalidate(&mut self, now: f64) { + // Update cadence EMA from the interval since the last invalidation. + if self.last_change_time.is_finite() { + let interval = now - self.last_change_time; + if interval > 0.0 && interval < Self::CADENCE_MAX_INTERVAL { + if self.input_cadence_ms == 0.0 { + // First sample — seed the EMA. + self.input_cadence_ms = interval; + } else { + self.input_cadence_ms = self.input_cadence_ms * (1.0 - Self::CADENCE_ALPHA) + + interval * Self::CADENCE_ALPHA; + } + } else if interval >= Self::CADENCE_MAX_INTERVAL { + // Session break — reset cadence for the new interaction. + self.input_cadence_ms = 0.0; + } + } + self.next_frame = self.next_frame.wrapping_add(1); self.last_change_time = now; self.needs_stable = true; } + /// Effective stable delay, accounting for input cadence. + /// + /// Returns `max(base_delay, cadence × CADENCE_MULTIPLIER)`. + fn effective_stable_delay(&self) -> f64 { + if self.input_cadence_ms > 0.0 { + self.stable_delay_ms + .max(self.input_cadence_ms * Self::CADENCE_MULTIPLIER) + } else { + self.stable_delay_ms + } + } + /// Called once per host frame. Returns: /// - `None` — idle, nothing to render /// - `Some(Unstable)` — change within debounce window, render fast @@ -90,7 +144,7 @@ impl FrameLoop { // No new frame pending, but do we owe a stable frame? if self.needs_stable { let elapsed = now - self.last_change_time; - if elapsed >= self.stable_delay_ms { + if elapsed >= self.effective_stable_delay() { return Some(FrameQuality::Stable); } } @@ -98,7 +152,8 @@ impl FrameLoop { None } - /// Mark frame as rendered. If `Stable`, clears `needs_stable`. + /// Mark frame as rendered. If `Stable`, clears `needs_stable` and + /// resets the input cadence for the next interaction session. /// If `Unstable`, keeps `needs_stable = true` so `poll()` will /// return `Stable` once the debounce expires. pub fn complete(&mut self, quality: FrameQuality) { @@ -106,6 +161,10 @@ impl FrameLoop { match quality { FrameQuality::Stable => { self.needs_stable = false; + // Do NOT reset cadence or last_change_time here. + // The user may still be interacting (slow trackpad). + // Cadence resets naturally in invalidate() when the + // interval exceeds CADENCE_MAX_INTERVAL (session break). } FrameQuality::Unstable => { // needs_stable stays true — poll() will fire Stable later @@ -202,7 +261,8 @@ mod tests { // Waiting for stable at 150ms... assert_eq!(fl.poll(140.0), None); - // New invalidation at 145ms resets the debounce + // New invalidation at 145ms resets the debounce. + // Cadence becomes 45ms → effective delay = max(50, 45×2.5) = 112.5ms. fl.invalidate(145.0); assert_eq!(fl.poll(145.0), Some(FrameQuality::Unstable)); fl.complete(FrameQuality::Unstable); @@ -210,8 +270,11 @@ mod tests { // Old debounce (150ms) shouldn't fire stable assert_eq!(fl.poll(155.0), None); - // New debounce (145 + 50 = 195ms) fires stable - assert_eq!(fl.poll(195.0), Some(FrameQuality::Stable)); + // Base debounce (195ms) not enough — cadence pushes it to 257.5ms + assert_eq!(fl.poll(195.0), None); + + // Adaptive debounce: 145 + 112.5 = 257.5ms + assert_eq!(fl.poll(258.0), Some(FrameQuality::Stable)); fl.complete(FrameQuality::Stable); assert!(fl.is_idle()); @@ -262,4 +325,115 @@ mod tests { fl.invalidate(20.0); assert_eq!(fl.current_frame(), 3); } + + #[test] + fn adaptive_delay_slow_cadence() { + // Simulate slow trackpad scrolling at 80ms intervals. + // The adaptive delay should prevent stable frames from firing + // between scroll events. + let mut fl = FrameLoop::new(); + + // First scroll event + fl.invalidate(0.0); + assert_eq!(fl.poll(0.0), Some(FrameQuality::Unstable)); + fl.complete(FrameQuality::Unstable); + + // Second scroll event at 80ms — cadence seeds to 80ms. + // Effective delay = max(50, 80×2.5) = 200ms. + fl.invalidate(80.0); + assert_eq!(fl.poll(80.0), Some(FrameQuality::Unstable)); + fl.complete(FrameQuality::Unstable); + + // 50ms after last event — base delay expired but adaptive delay + // keeps us idle. No stable frame intrusion! + assert_eq!(fl.poll(130.0), None); + + // Third scroll event at 160ms — cadence stays ~80ms. + fl.invalidate(160.0); + assert_eq!(fl.poll(160.0), Some(FrameQuality::Unstable)); + fl.complete(FrameQuality::Unstable); + + // Still no stable frame between events + assert_eq!(fl.poll(210.0), None); + + // User stops scrolling. Stable fires after adaptive delay. + // Cadence ≈ 80ms, effective delay ≈ 200ms. + // Last event at 160ms → stable at ~360ms. + assert_eq!(fl.poll(350.0), None); + assert_eq!(fl.poll(361.0), Some(FrameQuality::Stable)); + fl.complete(FrameQuality::Stable); + + assert!(fl.is_idle()); + } + + #[test] + fn cadence_persists_across_stable() { + let mut fl = FrameLoop::new(); + + // Build up a slow cadence (80ms) + fl.invalidate(0.0); + fl.complete(FrameQuality::Unstable); + fl.invalidate(80.0); + fl.complete(FrameQuality::Unstable); + + // Let stable fire — cadence persists (interaction might continue). + assert_eq!(fl.poll(280.0), Some(FrameQuality::Stable)); + fl.complete(FrameQuality::Stable); + + // Interaction continues soon after. The interval 300-80=220ms + // blends with the old cadence 80ms → EMA ≈ 122ms. + // Effective delay = max(50, 122×2.5) = 305ms. + fl.invalidate(300.0); + assert_eq!(fl.poll(300.0), Some(FrameQuality::Unstable)); + fl.complete(FrameQuality::Unstable); + + // 50ms later: base delay expired but cadence keeps us idle + assert_eq!(fl.poll(350.0), None); + // 200ms later: still within 305ms effective delay + assert_eq!(fl.poll(500.0), None); + // After effective delay: 300 + 305 = 605ms + assert_eq!(fl.poll(606.0), Some(FrameQuality::Stable)); + fl.complete(FrameQuality::Stable); + } + + #[test] + fn cadence_resets_on_session_break() { + let mut fl = FrameLoop::new(); + + // Build up cadence + fl.invalidate(0.0); + fl.complete(FrameQuality::Unstable); + fl.invalidate(80.0); + fl.complete(FrameQuality::Unstable); + + // Let stable fire + assert_eq!(fl.poll(280.0), Some(FrameQuality::Stable)); + fl.complete(FrameQuality::Stable); + + // Long pause (>500ms) — session break resets cadence. + fl.invalidate(900.0); // 900 - 80 = 820ms > 500ms → cadence resets to 0 + assert_eq!(fl.poll(900.0), Some(FrameQuality::Unstable)); + fl.complete(FrameQuality::Unstable); + + // Now cadence=0, so base delay (50ms) applies. + assert_eq!(fl.poll(950.0), Some(FrameQuality::Stable)); + } + + #[test] + fn adaptive_delay_fast_cadence_uses_base() { + // Fast input (16ms, typical 60fps mouse) — cadence × 2.5 = 40ms < base 50ms. + // The base delay should be used. + let mut fl = FrameLoop::new(); + + fl.invalidate(0.0); + fl.complete(FrameQuality::Unstable); + fl.invalidate(16.0); + fl.complete(FrameQuality::Unstable); + fl.invalidate(32.0); + fl.complete(FrameQuality::Unstable); + + // Base delay applies: 32 + 50 = 82ms + assert_eq!(fl.poll(81.0), None); + assert_eq!(fl.poll(82.0), Some(FrameQuality::Stable)); + } } diff --git a/crates/grida-canvas/src/runtime/scene.rs b/crates/grida-canvas/src/runtime/scene.rs index 1449246499..4e097e6d7d 100644 --- a/crates/grida-canvas/src/runtime/scene.rs +++ b/crates/grida-canvas/src/runtime/scene.rs @@ -413,10 +413,10 @@ impl Renderer { &mut self, plan: &FramePlan, ) -> ( - std::collections::HashMap, + crate::cache::fast_hash::NodeIdHashMap, usize, ) { - let mut blits = std::collections::HashMap::new(); + let mut blits = crate::cache::fast_hash::new_node_id_map(); let mut cache_hits = 0usize; for id in &plan.promoted { @@ -475,7 +475,9 @@ impl Renderer { &mut self, canvas: &Canvas, plan: &FramePlan, - promoted_blits: Option<&std::collections::HashMap>, + promoted_blits: Option< + &crate::cache::fast_hash::NodeIdHashMap, + >, ) -> usize { // Select effect quality based on frame stability. // Unstable (interactive) frames use reduced effects for performance. @@ -910,7 +912,7 @@ impl Renderer { // // When the offset exceeds the viewport (no overlap with cached frame), // we fall through to a full redraw which captures a new snapshot. - if camera_change == CameraChangeKind::PanOnly && self.backend.is_gpu() { + if !stable && !camera_change.zoom_changed() && self.backend.is_gpu() { if let Some(ref cache) = self.pan_image_cache { let width = surface.width() as f32; let height = surface.height() as f32; @@ -1336,6 +1338,9 @@ impl Renderer { /// Load a scene into the renderer. Caching will be performed lazily during /// rendering based on the configured caching strategy. pub fn load_scene(&mut self, scene: Scene) { + #[cfg(feature = "perf")] + let _t0 = crate::sys::perf_now(); + self.scene = Some(scene); self.scene_cache = cache::scene::SceneCache::new(); @@ -1343,12 +1348,22 @@ impl Renderer { self.zoom_image_cache = None; self.images.clear_missing_tracking(); if let Some(scene) = self.scene.as_ref() { + #[cfg(feature = "perf")] + let _t_fonts_start = crate::sys::perf_now(); let requested = collect_scene_font_families(scene); self.fonts.set_requested_families(requested.into_iter()); + #[cfg(feature = "perf")] + let _t_fonts = crate::sys::perf_now(); let viewport_size = self.window_context.viewport_size; // 1. Compute layout phase + // + // NOTE: We cannot auto-skip Taffy based on has_flex() alone because + // compute_schema_only() skips text measurement — text nodes with + // height=None would get height=0 and become invisible. A future + // optimization could use a hybrid path that skips flex computation + // but still measures text via the paragraph cache. if self.config.skip_layout { // Fast path: derive layout directly from schema positions/sizes. // Skips Taffy tree construction, flexbox computation, and text @@ -1365,6 +1380,8 @@ impl Renderer { }), ); } + #[cfg(feature = "perf")] + let _t_layout = crate::sys::perf_now(); // 2. Build geometry with layout results let layout_result = self.layout_engine.result(); @@ -1374,12 +1391,30 @@ impl Renderer { layout_result, viewport_size, ); + #[cfg(feature = "perf")] + let _t_geometry = crate::sys::perf_now(); // 3. Build effect tree (identifies render surface boundaries) self.scene_cache.update_effect_tree(scene); + #[cfg(feature = "perf")] + let _t_effects = crate::sys::perf_now(); // 4. Build layers self.scene_cache.update_layers(scene); + + #[cfg(feature = "perf")] + { + let _t_layers = crate::sys::perf_now(); + eprintln!( + "[load_scene] fonts={:.0}ms layout={:.0}ms geometry={:.0}ms effects={:.0}ms layers={:.0}ms total={:.0}ms", + _t_fonts - _t_fonts_start, + _t_layout - _t_fonts, + _t_geometry - _t_layout, + _t_effects - _t_geometry, + _t_layers - _t_effects, + _t_layers - _t0, + ); + } } // Record SCENE_LOAD so apply_changes() knows to clear picture/paragraph/ // path/compositor caches on the next frame. The scene_cache was already @@ -1407,10 +1442,13 @@ impl Renderer { self.scene_cache.compositor.mark_all_stale(); } - // Invalidate pan image cache when zoom changes or on stable frames. + // Invalidate pan image cache on zoom changes only. // Zoom changes alter the pixel content (different scale/density). - // Stable frames should produce a full-quality render, not a cached blit. - if camera_change.zoom_changed() || stable { + // Stable frames do NOT nuke the pan cache — the cache is valid for + // pan-only and no-change scenarios. The render path recaptures it + // from the full-quality draw anyway, so the next unstable frame + // always has a fresh cache to blit from. + if camera_change.zoom_changed() { self.pan_image_cache = None; } @@ -1431,8 +1469,10 @@ impl Renderer { let can_defer = !stable && self.backend.is_gpu() && ( + // No content or camera change — overlay-only (marquee, hover) + (!camera_change.any_changed() && self.pan_image_cache.is_some()) // Pan cache will likely hit - (camera_change == CameraChangeKind::PanOnly && self.pan_image_cache.is_some()) + || (camera_change == CameraChangeKind::PanOnly && self.pan_image_cache.is_some()) // Zoom cache will likely hit || (camera_change.zoom_changed() && self.zoom_image_cache.is_some()) ); @@ -1508,6 +1548,50 @@ impl Renderer { Some(self.render_frame(plan)) } + /// Restore cached content for overlay-only frames. + /// + /// Blits the pan image cache at (0,0) onto the backend surface, + /// restoring the content layer without the previous overlay pixels. + /// Returns `true` if the blit succeeded, `false` if no cache exists. + /// + /// Used when neither scene data nor the camera changed — the content + /// is identical, so we skip the expensive frame-plan + draw and just + /// repaint the overlay on top of cached content. + pub fn blit_content_cache(&mut self) -> bool { + if !self.backend.is_gpu() { + return false; + } + let cache = match self.pan_image_cache.as_ref() { + Some(c) => c, + None => return false, + }; + // The pan cache image was captured at (origin_tx, origin_ty). + // If the camera has since moved (e.g. pan-only fast-path frames + // that don't recapture), we must offset the blit — otherwise the + // settle frame "reverts" to the old camera position. + let vm = self.camera.view_matrix(); + let dx = vm.matrix[0][2] - cache.origin_tx; + let dy = vm.matrix[1][2] - cache.origin_ty; + + let surface = unsafe { &mut *self.backend.get_surface() }; + let canvas = surface.canvas(); + if dx != 0.0 || dy != 0.0 { + // Offset blit — need to clear first (exposed edges). + if let Some(scene) = self.scene.as_ref() { + if let Some(bg) = scene.background_color { + canvas.clear(skia_safe::Color::from(bg)); + } else { + canvas.clear(skia_safe::Color::TRANSPARENT); + } + } else { + canvas.clear(skia_safe::Color::TRANSPARENT); + } + } + canvas.draw_image(&cache.image, (dx, dy), None); + Self::gpu_flush(surface); + true + } + /// Clear the cached scene picture. /// /// **Prefer [`mark_changed`] + [`apply_changes`]** for new code. @@ -1552,12 +1636,17 @@ impl Renderer { /// /// The `camera_change` parameter is folded in so that zoom/pan /// invalidation lives in the same dispatch table. - pub fn apply_changes(&mut self, camera_change: CameraChangeKind, stable: bool) { + /// Returns `true` when content actually needs re-rendering (data or + /// camera changed). Returns `false` for overlay-only frames (e.g. + /// marquee drag, hover highlight) where the caller can skip the + /// expensive frame-plan + draw and just blit the cached content. + pub fn apply_changes(&mut self, camera_change: CameraChangeKind, stable: bool) -> bool { let cs = self.changes.take(); let flags = cs.flags(); // Fast path: nothing changed (pure camera move handled below). let has_data_changes = !flags.is_empty(); + let content_changed = has_data_changes || camera_change.any_changed(); // ----- Layout ----- // Scene load handles its own layout in load_scene(); skip here. @@ -1619,8 +1708,19 @@ impl Renderer { // ----- Viewport snapshot caches (pan/zoom image caches) ----- // These are the ONLY caches that truly depend on viewport dimensions. - let invalidate_pan = has_data_changes || camera_change.zoom_changed() || stable; - let invalidate_zoom = has_data_changes || stable; + // + // Pan cache: invalidate on data changes or zoom changes. Stable + // frames do NOT nuke the pan cache — during slow panning, a stable + // frame firing between scroll events would destroy the cache and + // force an expensive full redraw on the next unstable frame. The + // stable frame's render path recaptures the pan cache anyway. + let invalidate_pan = has_data_changes || camera_change.zoom_changed(); + // Zoom cache: invalidate when content changed OR when a stable frame + // follows a camera change (full-quality recapture needed). Overlay- + // only frames (no data + no camera change) must NOT nuke the zoom + // cache — the content is identical, and destroying the cache forces + // an expensive full draw on the next real zoom interaction. + let invalidate_zoom = has_data_changes || (stable && camera_change.any_changed()); if invalidate_pan { self.pan_image_cache = None; @@ -1628,6 +1728,8 @@ impl Renderer { if invalidate_zoom { self.zoom_image_cache = None; } + + content_changed } /// Check whether the current scene has layout that depends on viewport size. diff --git a/crates/grida-canvas/src/surface/state.rs b/crates/grida-canvas/src/surface/state.rs index 64e0705be9..d1e0b361ad 100644 --- a/crates/grida-canvas/src/surface/state.rs +++ b/crates/grida-canvas/src/surface/state.rs @@ -94,7 +94,7 @@ impl SurfaceState { SurfaceEvent::PointerMove { canvas_point, screen_point, - } => self.handle_pointer_move(canvas_point, screen_point, hit_tester, hierarchy, ui_hit_regions), + } => self.handle_pointer_move(canvas_point, screen_point, hit_tester, ui_hit_regions), SurfaceEvent::PointerDown { canvas_point, @@ -156,7 +156,6 @@ impl SurfaceState { canvas_point: math2::vector2::Vector2, screen_point: math2::vector2::Vector2, hit_tester: &HitTester, - hierarchy: &impl Hierarchy, ui_hit_regions: &HitRegions, ) -> SurfaceResponse { let mut response = SurfaceResponse::none(); @@ -206,11 +205,12 @@ impl SurfaceState { current_canvas: canvas_point, }; - // Compute tentative selection from marquee rectangle + // Compute tentative selection from marquee rectangle. + // Use intersects_topmost to get only the shallowest + // matching ancestors — avoids the separate O(K*D) prune. let rect = marquee_rect(anchor_canvas, canvas_point); - let hits = hit_tester.intersects(&rect); + let hits = hit_tester.intersects_topmost(&rect); self.selection.set(hits); - self.prune_selection(hierarchy); response.selection_changed = true; response.needs_redraw = true; diff --git a/crates/grida-canvas/src/sys/mod.rs b/crates/grida-canvas/src/sys/mod.rs index b8789e03f6..3c67951d0f 100644 --- a/crates/grida-canvas/src/sys/mod.rs +++ b/crates/grida-canvas/src/sys/mod.rs @@ -1,2 +1,17 @@ pub mod clock; pub mod timer; + +/// High-resolution timestamp in milliseconds. +/// Uses `emscripten_get_now()` on WASM, `Instant` on native. +#[cfg(target_os = "emscripten")] +pub fn perf_now() -> f64 { + unsafe { crate::os::emscripten::emscripten_get_now() } +} + +#[cfg(not(target_os = "emscripten"))] +pub fn perf_now() -> f64 { + use std::time::Instant; + static START: std::sync::OnceLock = std::sync::OnceLock::new(); + let start = START.get_or_init(Instant::now); + start.elapsed().as_secs_f64() * 1000.0 +} diff --git a/crates/grida-canvas/src/window/application.rs b/crates/grida-canvas/src/window/application.rs index 87f3b2214c..c6f99e6530 100644 --- a/crates/grida-canvas/src/window/application.rs +++ b/crates/grida-canvas/src/window/application.rs @@ -132,6 +132,9 @@ pub trait ApplicationApi { /// Only works after `load_scene_grida` has decoded a multi-scene document. fn switch_scene(&mut self, scene_id: &str); + /// Return the IDs of all scenes decoded by the last `load_scene_grida` call. + fn loaded_scene_ids(&self) -> Vec; + /// Apply a batch of scene transactions represented as JSON Patch operations. fn apply_document_transactions( &mut self, @@ -557,11 +560,15 @@ impl ApplicationApi for UnknownTargetApplication { fn runtime_renderer_set_pixel_preview_scale(&mut self, scale: u8) { self.renderer.set_pixel_preview_scale(scale); + self.renderer + .mark_changed(crate::runtime::changes::ChangeFlags::CONFIG); self.queue(); } fn runtime_renderer_set_pixel_preview_stable(&mut self, stable: bool) { self.renderer.set_pixel_preview_strategy_stable(stable); + self.renderer + .mark_changed(crate::runtime::changes::ChangeFlags::CONFIG); self.queue(); } @@ -571,6 +578,8 @@ impl ApplicationApi for UnknownTargetApplication { ) { let policy = crate::runtime::render_policy::RenderPolicy::from_flags(flags); self.renderer.set_render_policy(policy); + self.renderer + .mark_changed(crate::runtime::changes::ChangeFlags::CONFIG); self.queue(); } @@ -620,8 +629,8 @@ impl ApplicationApi for UnknownTargetApplication { } fn load_scene_grida(&mut self, bytes: &[u8]) { - use crate::io::io_grida_fbs; - match io_grida_fbs::decode_with_id_map(bytes) { + use crate::io::io_grida_file; + match io_grida_file::decode_with_id_map(bytes) { Ok(result) => { // Build id mappings from the decode result let mut string_to_internal = std::collections::HashMap::new(); @@ -651,8 +660,9 @@ impl ApplicationApi for UnknownTargetApplication { } fn switch_scene(&mut self, scene_id: &str) { - if let Some((_, scene)) = self.loaded_scenes.iter().find(|(id, _)| id == scene_id) { - self.renderer.load_scene(scene.clone()); + if let Some(pos) = self.loaded_scenes.iter().position(|(id, _)| id == scene_id) { + let (_, scene) = self.loaded_scenes[pos].clone(); + self.renderer.load_scene(scene); self.queue(); } else { eprintln!( @@ -666,6 +676,13 @@ impl ApplicationApi for UnknownTargetApplication { } } + fn loaded_scene_ids(&self) -> Vec { + self.loaded_scenes + .iter() + .map(|(id, _)| id.clone()) + .collect() + } + fn apply_document_transactions( &mut self, transactions: Vec>, @@ -1133,12 +1150,32 @@ impl UnknownTargetApplication { // (viewport resize, font/image loads, text edits, config changes) // and camera state in one place. This replaces the ad-hoc // invalidate_compositor_on_zoom() call and all per-site cache nuking. - self.renderer.apply_changes(camera_change, stable); + let content_changed = self.renderer.apply_changes(camera_change, stable); // Warm the camera cache once per frame so view_matrix(), rect(), and // screen_to_canvas_point() are essentially free for the rest of this frame. self.renderer.camera.warm_cache(); + // 5a. Overlay-only fast path + // + // When neither scene data nor the camera changed (e.g. marquee drag, + // hover highlight, selection change), the content layer is identical + // to the previous frame. Restore it from the pan image cache and + // skip the expensive frame-plan build + full draw. The overlay is + // still re-drawn below so marquee/selection visuals update correctly. + if !content_changed && !camera_change.any_changed() && self.renderer.blit_content_cache() { + // Consume the camera change (no-op here, but keeps the contract). + self.renderer.camera.consume_change(); + + // Draw devtools overlays on top of the restored content. + let _overlay_time = self.draw_and_flush_devtools_overlay(); + + // Complete frame in the loop. + self.frame_loop.complete(quality); + self.last_frame_time = __frame_start; + return true; + } + // Build frame plan lazily let rect = self.renderer.camera.rect(); let zoom = self.renderer.camera.get_zoom(); diff --git a/crates/grida-canvas/src/window/application_emscripten.rs b/crates/grida-canvas/src/window/application_emscripten.rs index ca1f564574..237f0f1d6b 100644 --- a/crates/grida-canvas/src/window/application_emscripten.rs +++ b/crates/grida-canvas/src/window/application_emscripten.rs @@ -296,6 +296,10 @@ impl ApplicationApi for EmscriptenApplication { self.base.switch_scene(scene_id); } + fn loaded_scene_ids(&self) -> Vec { + self.base.loaded_scene_ids() + } + fn apply_document_transactions( &mut self, transactions: Vec>, diff --git a/crates/grida-canvas/tests/compositor_effects.rs b/crates/grida-canvas/tests/compositor_effects.rs index afa4516c31..9840f5ff3a 100644 --- a/crates/grida-canvas/tests/compositor_effects.rs +++ b/crates/grida-canvas/tests/compositor_effects.rs @@ -20,7 +20,6 @@ use cg::runtime::image_repository::ImageRepository; use cg::runtime::render_policy::RenderPolicy; use math2::rect::Rectangle; use skia_safe::{surfaces, Paint as SkPaint, Rect, Surface}; -use std::collections::HashMap; use std::rc::Rc; use std::sync::{Arc, Mutex}; @@ -615,7 +614,7 @@ fn z_order_promoted_child_visible_above_container() { let offscreen_image = offscreen.image_snapshot(); // Step 2: Build the promoted_blits map - let mut promoted_blits: HashMap = HashMap::new(); + let mut promoted_blits: cg::cache::fast_hash::NodeIdHashMap = cg::cache::fast_hash::new_node_id_map(); let src_rect = Rect::new( 0.0, 0.0, diff --git a/crates/grida-dev/Cargo.toml b/crates/grida-dev/Cargo.toml index b456362939..97edadd8fa 100644 --- a/crates/grida-dev/Cargo.toml +++ b/crates/grida-dev/Cargo.toml @@ -9,6 +9,7 @@ description = "Native winit playground for grida-canvas scenes." [dependencies] cg = { path = "../grida-canvas", features = [ "web", + "perf", "native-clock-tick", "native-gl-context", ] } diff --git a/crates/grida-dev/src/bench/load_bench.rs b/crates/grida-dev/src/bench/load_bench.rs index c03f9a7560..9c6fea15c6 100644 --- a/crates/grida-dev/src/bench/load_bench.rs +++ b/crates/grida-dev/src/bench/load_bench.rs @@ -187,7 +187,7 @@ fn layout_diff(scene: &Scene, width: i32, height: i32, threshold: f32) { let mut diffs = Vec::new(); let eps = threshold; for (id, full) in result_full.iter() { - if let Some(schema) = result_schema.get(id) { + if let Some(schema) = result_schema.get(&id) { let dx = (full.x - schema.x).abs(); let dy = (full.y - schema.y).abs(); let dw = (full.width - schema.width).abs(); @@ -195,14 +195,14 @@ fn layout_diff(scene: &Scene, width: i32, height: i32, threshold: f32) { if dx > eps || dy > eps || dw > eps || dh > eps { let node_type = scene .graph - .get_node(id) + .get_node(&id) .map(|n| format!("{:?}", std::mem::discriminant(n))) .unwrap_or_else(|_| "?".to_string()); // Get node type name cleanly let type_name = scene .graph - .get_node(id) + .get_node(&id) .map(|n| match n { cg::node::schema::Node::Container(_) => "Container", cg::node::schema::Node::Rectangle(_) => "Rectangle", @@ -226,7 +226,7 @@ fn layout_diff(scene: &Scene, width: i32, height: i32, threshold: f32) { // Check if parent is a container (to understand context) let parent_info = scene .graph - .get_parent(id) + .get_parent(&id) .and_then(|pid| { scene.graph.get_node(&pid).ok().map(|p| match p { cg::node::schema::Node::Container(_) => "Container", @@ -239,7 +239,7 @@ fn layout_diff(scene: &Scene, width: i32, height: i32, threshold: f32) { .unwrap_or("root"); diffs.push(( - *id, + id, type_name, parent_info, *full, @@ -258,7 +258,7 @@ fn layout_diff(scene: &Scene, width: i32, height: i32, threshold: f32) { // Also check for nodes in schema but not in full for (id, _) in result_schema.iter() { - if result_full.get(id).is_none() { + if result_full.get(&id).is_none() { eprintln!( " WARN: node {:?} in schema result but missing from full result", id diff --git a/crates/grida-dev/src/bench/runner.rs b/crates/grida-dev/src/bench/runner.rs index 1dc4cd129f..51b57ac5d5 100644 --- a/crates/grida-dev/src/bench/runner.rs +++ b/crates/grida-dev/src/bench/runner.rs @@ -5,6 +5,7 @@ use cg::cg::prelude::*; use cg::node::factory::NodeFactory; use cg::node::scene_graph::{Parent, SceneGraph}; use cg::node::schema::{Node, Scene, Size}; +use cg::runtime::frame_loop::{FrameLoop, FrameQuality}; use cg::runtime::scene::FrameFlushResult; use cg::window::headless::HeadlessGpu; use math2::transform::AffineTransform; @@ -44,7 +45,10 @@ fn warmup(renderer: &mut cg::runtime::scene::Renderer) { /// Measure a single frame including queue + flush. /// Returns (total_us, queue_us, draw_us, mid_flush_us, compositor_us, flush_us). -fn measure_frame(renderer: &mut cg::runtime::scene::Renderer, stable: bool) -> Option<(u64, u64, u64, u64, u64, u64)> { +fn measure_frame( + renderer: &mut cg::runtime::scene::Renderer, + stable: bool, +) -> Option<(u64, u64, u64, u64, u64, u64)> { let t0 = Instant::now(); if stable { renderer.queue_stable(); @@ -75,11 +79,7 @@ fn measure_frame(renderer: &mut cg::runtime::scene::Renderer, stable: bool) -> O /// Uses CONTINUOUS panning (one direction, then reverses) to trigger cache misses /// and expose frame drop outliers during area discovery/culling. /// Measures queue + flush per frame. Ends with a settle (stable) frame. -fn run_pan_pass_at( - renderer: &mut cg::runtime::scene::Renderer, - frames: u32, - dx: f32, -) -> PassStats { +fn run_pan_pass_at(renderer: &mut cg::runtime::scene::Renderer, frames: u32, dx: f32) -> PassStats { let wall_start = Instant::now(); let mut frame_times = Vec::with_capacity(frames as usize); let mut queue_us_acc = Vec::with_capacity(frames as usize); @@ -111,9 +111,14 @@ fn run_pan_pass_at( let settle_us = measure_settle(renderer); compute_pass_stats( - &frame_times, &queue_us_acc, &draw_us_acc, - &mid_flush_us_acc, &compositor_us_acc, &flush_us_acc, - wall, settle_us, + &frame_times, + &queue_us_acc, + &draw_us_acc, + &mid_flush_us_acc, + &compositor_us_acc, + &flush_us_acc, + wall, + settle_us, ) } @@ -178,9 +183,14 @@ fn run_zoom_pass_at( let settle_us = measure_settle(renderer); compute_pass_stats( - &frame_times, &queue_us_acc, &draw_us_acc, - &mid_flush_us_acc, &compositor_us_acc, &flush_us_acc, - wall, settle_us, + &frame_times, + &queue_us_acc, + &draw_us_acc, + &mid_flush_us_acc, + &compositor_us_acc, + &flush_us_acc, + wall, + settle_us, ) } @@ -299,8 +309,15 @@ fn compute_resize_stats( ) -> ResizePassStats { if total.is_empty() { return ResizePassStats { - avg_us: 0, min_us: 0, p50_us: 0, p95_us: 0, max_us: 0, - rebuild_us: 0, invalidate_us: 0, flush_us: 0, wall, + avg_us: 0, + min_us: 0, + p50_us: 0, + p95_us: 0, + max_us: 0, + rebuild_us: 0, + invalidate_us: 0, + flush_us: 0, + wall, }; } let mut sorted = total.to_vec(); @@ -331,8 +348,18 @@ fn compute_pass_stats( ) -> PassStats { if frame_times.is_empty() { return PassStats { - avg_us: 0, fps: 0.0, min_us: 0, p50_us: 0, p95_us: 0, p99_us: 0, max_us: 0, - queue_us: 0, draw_us: 0, mid_flush_us: 0, compositor_us: 0, flush_us: 0, + avg_us: 0, + fps: 0.0, + min_us: 0, + p50_us: 0, + p95_us: 0, + p99_us: 0, + max_us: 0, + queue_us: 0, + draw_us: 0, + mid_flush_us: 0, + compositor_us: 0, + flush_us: 0, settle_us: 0, }; } @@ -434,9 +461,14 @@ fn run_zigzag_pan_pass( let settle_us = measure_settle(renderer); compute_pass_stats( - &frame_times, &queue_us_acc, &draw_us_acc, - &mid_flush_us_acc, &compositor_us_acc, &flush_us_acc, - wall, settle_us, + &frame_times, + &queue_us_acc, + &draw_us_acc, + &mid_flush_us_acc, + &compositor_us_acc, + &flush_us_acc, + wall, + settle_us, ) } @@ -487,9 +519,14 @@ fn run_circle_pan_pass( let settle_us = measure_settle(renderer); compute_pass_stats( - &frame_times, &queue_us_acc, &draw_us_acc, - &mid_flush_us_acc, &compositor_us_acc, &flush_us_acc, - wall, settle_us, + &frame_times, + &queue_us_acc, + &draw_us_acc, + &mid_flush_us_acc, + &compositor_us_acc, + &flush_us_acc, + wall, + settle_us, ) } @@ -592,9 +629,153 @@ fn run_realtime_pan_pass( }; compute_pass_stats( - &frame_times, &queue_us_acc, &draw_us_acc, - &mid_flush_us_acc, &compositor_us_acc, &flush_us_acc, - wall, avg_settle, + &frame_times, + &queue_us_acc, + &draw_us_acc, + &mid_flush_us_acc, + &compositor_us_acc, + &flush_us_acc, + wall, + avg_settle, + ) +} + +/// Real-time FrameLoop pan pass. +/// +/// Reproduces **exactly** what happens in `Application::frame()` during +/// panning — same FrameLoop, same apply_changes/build_plan/flush_with_plan +/// path, same GPU backend, with real `thread::sleep()` between ticks so +/// the GPU pipeline sees realistic idle gaps. +/// +/// This is the only benchmark that captures the actual user-facing +/// bottleneck: stable frames interrupting pan interactions. +/// +/// # How it works +/// +/// Runs a 60fps RAF loop (real 16ms sleeps). Scroll events inject camera +/// translations at `scroll_interval_ms` intervals. `FrameLoop` decides +/// whether each tick produces a frame and at what quality. The output +/// is the same `PassStats` as other passes, but the frame time +/// distribution reflects the real interaction — including jank spikes +/// from stable frames. +fn run_frameloop_pan_pass( + renderer: &mut cg::runtime::scene::Renderer, + scroll_interval_ms: f64, + dx: f32, + duration_ms: f64, +) -> PassStats { + let raf_interval_us: u64 = 16_000; // 60fps host cadence + let t_origin = Instant::now(); + + let mut frame_loop = FrameLoop::new(); + + let mut frame_times = Vec::new(); + let mut queue_us_acc = Vec::new(); + let mut draw_us_acc = Vec::new(); + let mut mid_flush_us_acc = Vec::new(); + let mut compositor_us_acc = Vec::new(); + let mut flush_us_acc = Vec::new(); + let mut stable_count = 0u32; + let mut unstable_count = 0u32; + + let mut next_scroll_ms = 0.0f64; + let mut scroll_events_fired = 0u32; + let mut pan_direction = 1.0f32; + + loop { + // Real wall time since start → this is what FrameLoop sees. + let now_ms = t_origin.elapsed().as_secs_f64() * 1000.0; + if now_ms >= duration_ms { + break; + } + + // --- Inject scroll event if due --- + if now_ms >= next_scroll_ms { + let zoom = renderer.camera.get_zoom(); + renderer.camera.translate(dx * pan_direction / zoom, 0.0); + frame_loop.invalidate(now_ms); + next_scroll_ms += scroll_interval_ms; + scroll_events_fired += 1; + + if scroll_events_fired % 25 == 0 { + pan_direction = -pan_direction; + } + } + + // --- Application::frame() equivalent --- + // Steps 4-8 from Application::frame(), using real wall time. + if let Some(quality) = frame_loop.poll(now_ms) { + // Step 5: camera change + stable promotion + let camera_change = renderer.camera.change_kind(); + let stable = quality == FrameQuality::Stable || !camera_change.any_changed(); + + // Step: apply_changes (central invalidation dispatch) + renderer.apply_changes(camera_change, stable); + + // Step: warm camera cache + renderer.camera.warm_cache(); + + // Step: build frame plan + let rect = renderer.camera.rect(); + let zoom = renderer.camera.get_zoom(); + let plan = renderer.build_frame_plan(rect, zoom, stable, camera_change); + + // Step: consume camera change + renderer.camera.consume_change(); + + // Step: flush (draw + GPU submit) — MEASURED + let t0 = Instant::now(); + let stats_opt = renderer.flush_with_plan(plan); + let wall_time = t0.elapsed().as_micros() as u64; + + // Step: complete frame + frame_loop.complete(quality); + + if quality == FrameQuality::Stable { + stable_count += 1; + } else { + unstable_count += 1; + } + + if let Some(stats) = stats_opt { + frame_times.push(wall_time); + queue_us_acc.push(0); + draw_us_acc.push(stats.draw.painter_duration.as_micros() as u64); + mid_flush_us_acc.push(stats.mid_flush_duration.as_micros() as u64); + compositor_us_acc.push(stats.compositor_duration.as_micros() as u64); + flush_us_acc.push(stats.flush_duration.as_micros() as u64); + } + } + + // --- Real sleep to next RAF tick --- + let elapsed_us = t_origin.elapsed().as_micros() as u64; + let next_tick_us = (elapsed_us / raf_interval_us + 1) * raf_interval_us; + let sleep_us = next_tick_us.saturating_sub(t_origin.elapsed().as_micros() as u64); + if sleep_us > 500 { + std::thread::sleep(std::time::Duration::from_micros(sleep_us)); + } + } + + let wall = t_origin.elapsed(); + + eprintln!( + " [frameloop] scroll every {scroll_interval_ms:.0}ms | \ + {scroll_events_fired} events | \ + {} frames ({unstable_count} unstable, {stable_count} stable) | \ + wall: {:.0}ms", + frame_times.len(), + wall.as_millis(), + ); + + compute_pass_stats( + &frame_times, + &queue_us_acc, + &draw_us_acc, + &mid_flush_us_acc, + &compositor_us_acc, + &flush_us_acc, + wall, + 0, // settle is implicit — stable frames are in frame_times ) } @@ -612,8 +793,10 @@ fn run_realtime_diagnostic( eprintln!( "\n=== REALTIME DIAGNOSTIC: scroll every {scroll_interval_ms:.0}ms, settle after {settle_ticks} ticks ===", ); - eprintln!("{:>8} {:>6} {:>8} {:>8} {:>8}", - "time_ms", "event", "total_us", "draw_us", "note"); + eprintln!( + "{:>8} {:>6} {:>8} {:>8} {:>8}", + "time_ms", "event", "total_us", "draw_us", "note" + ); let mut clock: f64 = 0.0; let mut next_scroll = scroll_interval_ms; @@ -634,7 +817,10 @@ fn run_realtime_diagnostic( if settle_countdown == 0 { if let Some((total, _q, d, _mf, _c, _f)) = measure_frame(renderer, true) { let marker = if total > 1000 { " <<<" } else { "" }; - eprintln!("{:>8.1} {:>6} {:>8} {:>8} settle{marker}", clock, "SETTLE", total, d); + eprintln!( + "{:>8.1} {:>6} {:>8} {:>8} settle{marker}", + clock, "SETTLE", total, d + ); } } } @@ -647,7 +833,10 @@ fn run_realtime_diagnostic( if let Some((total, _q, d, _mf, _c, _f)) = measure_frame(renderer, false) { let note = if d > 0 { "full draw" } else { "cache hit" }; let marker = if total > 1000 { " <<<" } else { "" }; - eprintln!("{:>8.1} {:>6} {:>8} {:>8} {note}{marker}", clock, "scroll", total, d); + eprintln!( + "{:>8.1} {:>6} {:>8} {:>8} {note}{marker}", + clock, "scroll", total, d + ); } next_scroll += scroll_interval_ms; } @@ -662,7 +851,8 @@ fn run_pan_with_settle_pass( settle_interval: u32, ) -> PassStats { let wall_start = Instant::now(); - let mut frame_times = Vec::with_capacity(frames as usize + frames as usize / settle_interval as usize); + let mut frame_times = + Vec::with_capacity(frames as usize + frames as usize / settle_interval as usize); let mut queue_us_acc = Vec::new(); let mut draw_us_acc = Vec::new(); let mut mid_flush_us_acc = Vec::new(); @@ -713,9 +903,14 @@ fn run_pan_with_settle_pass( }; compute_pass_stats( - &frame_times, &queue_us_acc, &draw_us_acc, - &mid_flush_us_acc, &compositor_us_acc, &flush_us_acc, - wall, avg_settle, + &frame_times, + &queue_us_acc, + &draw_us_acc, + &mid_flush_us_acc, + &compositor_us_acc, + &flush_us_acc, + wall, + avg_settle, ) } @@ -730,8 +925,10 @@ fn run_pan_settle_diagnostic( settle_interval: u32, ) { eprintln!("\n=== PAN+SETTLE DIAGNOSTIC: dx={dx}, settle every {settle_interval} frames ==="); - eprintln!("{:>5} {:>4} {:>8} {:>8} {:>8} {:>8}", - "frame", "type", "total_us", "queue_us", "draw_us", "list"); + eprintln!( + "{:>5} {:>4} {:>8} {:>8} {:>8} {:>8}", + "frame", "type", "total_us", "queue_us", "draw_us", "list" + ); let mut since_settle = 0u32; for i in 0..frames { @@ -740,8 +937,10 @@ fn run_pan_settle_diagnostic( if let Some((total, q, d, _mf, _c, _f)) = measure_frame(renderer, false) { let list = 0; // Not available from measure_frame let marker = if total > 1000 { " <<<" } else { "" }; - eprintln!("{:>5} {:>4} {:>8} {:>8} {:>8} {:>8}{marker}", - i, "pan", total, q, d, list); + eprintln!( + "{:>5} {:>4} {:>8} {:>8} {:>8} {:>8}{marker}", + i, "pan", total, q, d, list + ); } since_settle += 1; @@ -749,8 +948,10 @@ fn run_pan_settle_diagnostic( since_settle = 0; if let Some((total, q, d, _mf, _c, _f)) = measure_frame(renderer, true) { let marker = if total > 1000 { " <<<" } else { "" }; - eprintln!("{:>5} {:>4} {:>8} {:>8} {:>8} {:>8}{marker}", - i, "STTL", total, q, d, 0); + eprintln!( + "{:>5} {:>4} {:>8} {:>8} {:>8} {:>8}{marker}", + i, "STTL", total, q, d, 0 + ); } } } @@ -761,14 +962,12 @@ fn run_pan_settle_diagnostic( /// Shows exactly where frame drops occur during the transition from /// "all nodes visible" to "some nodes culled". #[allow(dead_code)] -fn run_pan_diagnostic( - renderer: &mut cg::runtime::scene::Renderer, - frames: u32, - dx: f32, -) { +fn run_pan_diagnostic(renderer: &mut cg::runtime::scene::Renderer, frames: u32, dx: f32) { eprintln!("\n=== PAN DIAGNOSTIC: dx={dx}, {} frames ===", frames); - eprintln!("{:>5} {:>8} {:>8} {:>8} {:>8} {:>8} {:>8}", - "frame", "total_us", "queue_us", "draw_us", "mflush", "comp_us", "flush_us"); + eprintln!( + "{:>5} {:>8} {:>8} {:>8} {:>8} {:>8} {:>8}", + "frame", "total_us", "queue_us", "draw_us", "mflush", "comp_us", "flush_us" + ); for i in 0..frames { renderer.camera.translate(dx, 0.0); @@ -832,17 +1031,53 @@ fn standard_scenarios(fit_zoom: f32) -> (Vec, Vec) { let fit_hi = fit_zoom * 2.0; let pan_scenarios = vec![ - PanScenario { name: "pan_slow_fit", dx: 2.0, zoom: fit_zoom }, - PanScenario { name: "pan_fast_fit", dx: 50.0, zoom: fit_zoom }, - PanScenario { name: "pan_slow_zoomed", dx: 2.0, zoom: zoomed_in }, - PanScenario { name: "pan_fast_zoomed", dx: 50.0, zoom: zoomed_in }, + PanScenario { + name: "pan_slow_fit", + dx: 2.0, + zoom: fit_zoom, + }, + PanScenario { + name: "pan_fast_fit", + dx: 50.0, + zoom: fit_zoom, + }, + PanScenario { + name: "pan_slow_zoomed", + dx: 2.0, + zoom: zoomed_in, + }, + PanScenario { + name: "pan_fast_zoomed", + dx: 50.0, + zoom: zoomed_in, + }, ]; let zoom_scenarios = vec![ - ZoomScenario { name: "zoom_slow_around_fit", step: 0.005, z_min: fit_lo, z_max: fit_hi }, - ZoomScenario { name: "zoom_fast_around_fit", step: 0.05, z_min: fit_lo, z_max: fit_hi }, - ZoomScenario { name: "zoom_slow_high", step: 0.01, z_min: zoomed_in * 0.5, z_max: zoomed_in }, - ZoomScenario { name: "zoom_fast_high", step: 0.1, z_min: zoomed_in * 0.5, z_max: zoomed_in }, + ZoomScenario { + name: "zoom_slow_around_fit", + step: 0.005, + z_min: fit_lo, + z_max: fit_hi, + }, + ZoomScenario { + name: "zoom_fast_around_fit", + step: 0.05, + z_min: fit_lo, + z_max: fit_hi, + }, + ZoomScenario { + name: "zoom_slow_high", + step: 0.01, + z_min: zoomed_in * 0.5, + z_max: zoomed_in, + }, + ZoomScenario { + name: "zoom_fast_high", + step: 0.1, + z_min: zoomed_in * 0.5, + z_max: zoomed_in, + }, ]; (pan_scenarios, zoom_scenarios) @@ -892,10 +1127,26 @@ fn run_scenarios( let zoomed_in_c = (fit_zoom * 4.0).min(10.0); let circle_scenarios = vec![ - CirclePanScenario { name: "circle_small_fit", radius: 200.0, zoom: fit_zoom }, - CirclePanScenario { name: "circle_large_fit", radius: 2000.0, zoom: fit_zoom }, - CirclePanScenario { name: "circle_small_zoomed", radius: 200.0, zoom: zoomed_in_c }, - CirclePanScenario { name: "circle_large_zoomed", radius: 2000.0, zoom: zoomed_in_c }, + CirclePanScenario { + name: "circle_small_fit", + radius: 200.0, + zoom: fit_zoom, + }, + CirclePanScenario { + name: "circle_large_fit", + radius: 2000.0, + zoom: fit_zoom, + }, + CirclePanScenario { + name: "circle_small_zoomed", + radius: 200.0, + zoom: zoomed_in_c, + }, + CirclePanScenario { + name: "circle_large_zoomed", + radius: 2000.0, + zoom: zoomed_in_c, + }, ]; for cs in &circle_scenarios { @@ -939,22 +1190,38 @@ fn run_scenarios( let zigzag_scenarios = vec![ // Fast zigzag: continuous diagonal sweeps, no pauses ZigzagScenario { - name: "zigzag_fast_fit", dx: 30.0, dy: 5.0, - segment_frames: 20, pause_frames: 0, zoom: fit_zoom, + name: "zigzag_fast_fit", + dx: 30.0, + dy: 5.0, + segment_frames: 20, + pause_frames: 0, + zoom: fit_zoom, }, ZigzagScenario { - name: "zigzag_fast_zoomed", dx: 30.0, dy: 5.0, - segment_frames: 20, pause_frames: 0, zoom: zoomed_in_z, + name: "zigzag_fast_zoomed", + dx: 30.0, + dy: 5.0, + segment_frames: 20, + pause_frames: 0, + zoom: zoomed_in_z, }, // Slow zigzag: zig, stop (settle fires), zag, stop (settle fires) // pause_frames=3 simulates ~3 settle frames during the "reading" pause ZigzagScenario { - name: "zigzag_slow_fit", dx: 10.0, dy: 3.0, - segment_frames: 15, pause_frames: 3, zoom: fit_zoom, + name: "zigzag_slow_fit", + dx: 10.0, + dy: 3.0, + segment_frames: 15, + pause_frames: 3, + zoom: fit_zoom, }, ZigzagScenario { - name: "zigzag_slow_zoomed", dx: 10.0, dy: 3.0, - segment_frames: 15, pause_frames: 3, zoom: zoomed_in_z, + name: "zigzag_slow_zoomed", + dx: 10.0, + dy: 3.0, + segment_frames: 15, + pause_frames: 3, + zoom: zoomed_in_z, }, ]; @@ -969,7 +1236,12 @@ fn run_scenarios( } let stats = run_zigzag_pan_pass( - renderer, frames, zz.dx, zz.dy, zz.segment_frames, zz.pause_frames, + renderer, + frames, + zz.dx, + zz.dy, + zz.segment_frames, + zz.pause_frames, ); results.push(ScenarioResult { name: zz.name.to_string(), @@ -1010,10 +1282,30 @@ fn run_scenarios( let zoomed_in_s = (fit_zoom * 4.0).min(10.0); let settle_scenarios = vec![ - SettlePanScenario { name: "pan_settle_slow_fit", dx: 2.0, zoom: fit_zoom, settle_interval: 12 }, - SettlePanScenario { name: "pan_settle_fast_fit", dx: 50.0, zoom: fit_zoom, settle_interval: 12 }, - SettlePanScenario { name: "pan_settle_slow_zoomed", dx: 2.0, zoom: zoomed_in_s, settle_interval: 12 }, - SettlePanScenario { name: "pan_settle_fast_zoomed", dx: 50.0, zoom: zoomed_in_s, settle_interval: 12 }, + SettlePanScenario { + name: "pan_settle_slow_fit", + dx: 2.0, + zoom: fit_zoom, + settle_interval: 12, + }, + SettlePanScenario { + name: "pan_settle_fast_fit", + dx: 50.0, + zoom: fit_zoom, + settle_interval: 12, + }, + SettlePanScenario { + name: "pan_settle_slow_zoomed", + dx: 2.0, + zoom: zoomed_in_s, + settle_interval: 12, + }, + SettlePanScenario { + name: "pan_settle_fast_zoomed", + dx: 50.0, + zoom: zoomed_in_s, + settle_interval: 12, + }, ]; for ss in &settle_scenarios { @@ -1055,20 +1347,36 @@ fn run_scenarios( let zoomed_in_rt = (fit_zoom * 4.0).min(10.0); let realtime_scenarios = vec![ RealtimeScenario { - name: "rt_pan_fast_fit", scroll_interval_ms: 8.0, - dx: 2.0, dy: 0.0, zoom: fit_zoom, duration_ms: 2000.0, + name: "rt_pan_fast_fit", + scroll_interval_ms: 8.0, + dx: 2.0, + dy: 0.0, + zoom: fit_zoom, + duration_ms: 2000.0, }, RealtimeScenario { - name: "rt_pan_slow_fit", scroll_interval_ms: 100.0, - dx: 5.0, dy: 0.0, zoom: fit_zoom, duration_ms: 2000.0, + name: "rt_pan_slow_fit", + scroll_interval_ms: 100.0, + dx: 5.0, + dy: 0.0, + zoom: fit_zoom, + duration_ms: 2000.0, }, RealtimeScenario { - name: "rt_pan_fast_zoomed", scroll_interval_ms: 8.0, - dx: 2.0, dy: 0.0, zoom: zoomed_in_rt, duration_ms: 2000.0, + name: "rt_pan_fast_zoomed", + scroll_interval_ms: 8.0, + dx: 2.0, + dy: 0.0, + zoom: zoomed_in_rt, + duration_ms: 2000.0, }, RealtimeScenario { - name: "rt_pan_slow_zoomed", scroll_interval_ms: 100.0, - dx: 5.0, dy: 0.0, zoom: zoomed_in_rt, duration_ms: 2000.0, + name: "rt_pan_slow_zoomed", + scroll_interval_ms: 100.0, + dx: 5.0, + dy: 0.0, + zoom: zoomed_in_rt, + duration_ms: 2000.0, }, ]; @@ -1079,8 +1387,12 @@ fn run_scenarios( warmup(renderer); let stats = run_realtime_pan_pass( - renderer, rt.scroll_interval_ms, - rt.dx, rt.dy, rt.duration_ms, 12, + renderer, + rt.scroll_interval_ms, + rt.dx, + rt.dy, + rt.duration_ms, + 12, ); results.push(ScenarioResult { name: rt.name.to_string(), @@ -1095,6 +1407,99 @@ fn run_scenarios( }); } + // FrameLoop-based pan scenarios: the real FrameLoop decision path. + // Unlike all other scenarios, these go through FrameLoop.poll() which + // decides Stable vs Unstable based on adaptive delay. This captures: + // - Pan image cache hit rate (GPU-only: unstable pan = cache blit) + // - Stable frame intrusion frequency (adaptive delay prevents these) + // - Compositor budget impact on stable frame cost + struct FrameLoopScenario { + name: &'static str, + scroll_interval_ms: f64, + dx: f32, + zoom: f32, + } + + // Sweep across a range of scroll intervals to find the jank threshold. + // Real trackpad scroll events range from ~8ms (fast flick) to ~200ms+ + // (very slow, deliberate single-finger scroll). + let frameloop_scenarios = vec![ + // Continuous fast pan — baseline, no stable frames should fire + FrameLoopScenario { + name: "fl_16ms", + scroll_interval_ms: 16.0, + dx: 5.0, + zoom: fit_zoom, + }, + // Moderate pan — gaps start approaching old 50ms debounce + FrameLoopScenario { + name: "fl_50ms", + scroll_interval_ms: 50.0, + dx: 3.0, + zoom: fit_zoom, + }, + // Slow pan — exceeds old 50ms debounce, adaptive should extend + FrameLoopScenario { + name: "fl_80ms", + scroll_interval_ms: 80.0, + dx: 3.0, + zoom: fit_zoom, + }, + // Slower — common slow trackpad scroll speed + FrameLoopScenario { + name: "fl_120ms", + scroll_interval_ms: 120.0, + dx: 2.0, + zoom: fit_zoom, + }, + // Very slow — deliberate, careful scrolling + FrameLoopScenario { + name: "fl_200ms", + scroll_interval_ms: 200.0, + dx: 1.0, + zoom: fit_zoom, + }, + // Ultra slow — near the edge of "interaction session" detection + FrameLoopScenario { + name: "fl_300ms", + scroll_interval_ms: 300.0, + dx: 1.0, + zoom: fit_zoom, + }, + // Discrete clicks — clearly separate events, stable should fire between + FrameLoopScenario { + name: "fl_500ms", + scroll_interval_ms: 500.0, + dx: 1.0, + zoom: fit_zoom, + }, + ]; + + for fl_s in &frameloop_scenarios { + renderer.camera.set_zoom(fl_s.zoom); + renderer.queue_stable(); + let _ = renderer.flush(); + warmup(renderer); + + let stats = run_frameloop_pan_pass( + renderer, + fl_s.scroll_interval_ms, + fl_s.dx, + 2000.0, // 2 second session + ); + results.push(ScenarioResult { + name: fl_s.name.to_string(), + kind: "frameloop".to_string(), + params: ScenarioParams { + speed: Some(fl_s.dx), + zoom: Some(fl_s.zoom), + zoom_min: None, + zoom_max: None, + }, + stats, + }); + } + results } @@ -1165,10 +1570,7 @@ fn build_benchmark_scene(grid: u32) -> Scene { // Single-scene bench (human-readable output) // --------------------------------------------------------------------------- -pub async fn run_bench( - args: BenchArgs, - load_scenes: impl AsyncSceneLoader, -) -> Result<()> { +pub async fn run_bench(args: BenchArgs, load_scenes: impl AsyncSceneLoader) -> Result<()> { let scenes = if let Some(ref path) = args.path { load_scenes.load(path).await? } else { @@ -1194,8 +1596,8 @@ pub async fn run_bench( let scene = scenes.into_iter().nth(args.scene_index).unwrap(); let node_count = scene.graph.node_count(); - let mut gpu = HeadlessGpu::new(args.width, args.height) - .map_err(|e| anyhow!("GPU init failed: {e}"))?; + let mut gpu = + HeadlessGpu::new(args.width, args.height).map_err(|e| anyhow!("GPU init failed: {e}"))?; gpu.print_gl_info(); let mut renderer = gpu.create_renderer(); @@ -1207,9 +1609,7 @@ pub async fn run_bench( println!("Loaded scene: {} nodes", node_count); println!( "Camera: zoom={:.4} viewport=({:.0}x{:.0})", - fit_zoom, - cam_rect.width, - cam_rect.height, + fit_zoom, cam_rect.width, cam_rect.height, ); println!( "Viewport: {}x{}, frames: {}\n", @@ -1321,7 +1721,10 @@ pub async fn run_bench_report( eprintln!( "bench-report: {} files, {} frames/pass, {}x{} viewport", - files.len(), args.frames, args.width, args.height + files.len(), + args.frames, + args.width, + args.height ); let mut results = Vec::new(); diff --git a/crates/grida-dev/src/main.rs b/crates/grida-dev/src/main.rs index 6ee9dee2b5..b92b12daca 100644 --- a/crates/grida-dev/src/main.rs +++ b/crates/grida-dev/src/main.rs @@ -184,8 +184,10 @@ async fn load_scenes_from_source(source: &str) -> Result> { if let Some(ext) = path.extension().and_then(|e| e.to_str()) { match ext.to_ascii_lowercase().as_str() { "svg" => return scene_from_svg_path(path).map(|s| vec![s]), - "png" | "jpg" | "jpeg" | "webp" => { - return scene_from_raster_path(path).map(|s| vec![s]) + // Raster images should be loaded via load_raster() by the caller + // so bytes can be registered with the renderer. + ext if is_raster_ext(ext) => { + return load_raster(path).map(|r| vec![r.scene]) } _ => {} } @@ -230,8 +232,26 @@ fn build_empty_scene() -> Scene { } async fn run_interactive(file: Option) -> Result<()> { + // Load initial scene(s). For raster images we also capture the raw bytes + // so we can register them with the renderer once it's ready. + let mut initial_image: Option = None; let initial_scenes = if let Some(ref source) = file { - load_scenes_from_source(source).await? + let path = Path::new(source); + let ext = path + .extension() + .and_then(|e| e.to_str()) + .map(|s| s.to_ascii_lowercase()) + .unwrap_or_default(); + if !is_url(source) && is_raster_ext(&ext) { + let raster = load_raster(path)?; + initial_image = Some(ImageMessage { + src: raster.rid, + data: raster.bytes, + }); + vec![raster.scene] + } else { + load_scenes_from_source(source).await? + } } else { vec![build_empty_scene()] }; @@ -249,6 +269,12 @@ async fn run_interactive(file: Option) -> Result<()> { run_demo_window_with_drop( first, move |_renderer, tx, _font_tx, proxy| { + // Register initial raster image bytes if present. + if let Some(msg) = initial_image { + let _ = tx.unbounded_send(msg.clone()); + let _ = proxy.send_event(HostEvent::ImageLoaded(msg)); + } + let mut guard = drop_rx.lock().expect("drop rx mutex poisoned"); let drop_rx = guard.take().expect("drop receiver already taken"); start_master_drop_task(drop_rx, tx.clone(), proxy.clone(), scenes_tx); @@ -278,36 +304,60 @@ fn scene_from_svg_path(path: &Path) -> Result { }) } -fn scene_from_raster_path(path: &Path) -> Result { +/// Result of loading a raster image: the scene plus the raw bytes and RID +/// so the caller can register the image with the renderer directly. +struct RasterScene { + scene: Scene, + /// Raw image bytes read from disk. + bytes: Vec, + /// The `res://images/{hash}` RID used by the node's fill. + rid: String, +} + +fn load_raster(path: &Path) -> Result { use cg::cg::prelude::CGColor; - use cg::cg::types::ResourceRef; + use cg::cg::types::{Paints, ResourceRef}; use cg::node::factory::NodeFactory; use cg::node::scene_graph::{Parent, SceneGraph}; use cg::node::schema::{Node, Size}; + use cg::resources::hash_bytes; + + let bytes = std::fs::read(path) + .with_context(|| format!("failed to read image file {}", path.display()))?; let (width, height) = image_dimensions(path) .with_context(|| format!("failed to read image dimensions {}", path.display()))?; - let mut graph = SceneGraph::new(); - let nf = NodeFactory::new(); - let mut image_node = nf.create_image_node(); - image_node.size = Size { - width: width as f32, - height: height as f32, - }; - image_node.image = ResourceRef::RID(path.to_string_lossy().into_owned()); + let rid = format!("res://images/{:016x}", hash_bytes(&bytes)); + let ref_ = ResourceRef::RID(rid.clone()); - graph.append_child(Node::Image(image_node), Parent::Root); + let nf = NodeFactory::new(); + let mut node = nf.create_image_node(); + node.size = Size { width: width as f32, height: height as f32 }; + node.image = ref_.clone(); + node.fill.image = ref_; + node.strokes = Paints::default(); - Ok(Scene { - name: path - .file_stem() - .map(|n| n.to_string_lossy().into_owned()) - .unwrap_or_else(|| "Image".to_string()), - graph, - background_color: Some(CGColor::from_u32(0xF8F8F8FF)), + let mut graph = SceneGraph::new(); + graph.append_child(Node::Image(node), Parent::Root); + + Ok(RasterScene { + scene: Scene { + name: path + .file_stem() + .map(|n| n.to_string_lossy().into_owned()) + .unwrap_or_else(|| "Image".to_string()), + graph, + background_color: Some(CGColor::from_u32(0xF8F8F8FF)), + }, + bytes, + rid, }) } +fn is_raster_ext(ext: &str) -> bool { + matches!(ext, "png" | "jpg" | "jpeg" | "webp") +} + async fn load_master_scenes_from_path(path: &Path) -> Result> { let ext = path .extension() @@ -318,7 +368,7 @@ async fn load_master_scenes_from_path(path: &Path) -> Result> { match ext.as_str() { "grida" | "grida1" => load_scenes_from_source(&path.to_string_lossy()).await, "svg" => scene_from_svg_path(path).map(|s| vec![s]), - "png" | "jpg" | "jpeg" | "webp" => scene_from_raster_path(path).map(|s| vec![s]), + // Raster images are handled separately in start_master_drop_task. other => Err(anyhow::anyhow!( "Unsupported dropped file type ({}): {}", other, @@ -335,6 +385,37 @@ fn start_master_drop_task( ) { tokio::spawn(async move { while let Some(path) = drop_rx.recv().await { + let ext = path + .extension() + .and_then(|e| e.to_str()) + .map(|s| s.to_ascii_lowercase()) + .unwrap_or_default(); + + // Raster images: read bytes, register image, then send scene. + // No need to go through load_scene_images / extract_image_urls. + if is_raster_ext(&ext) { + match load_raster(&path) { + Ok(raster) => { + // Send image bytes so the renderer registers them. + let msg = ImageMessage { + src: raster.rid, + data: raster.bytes, + }; + let _ = image_tx.unbounded_send(msg.clone()); + let _ = proxy.send_event(HostEvent::ImageLoaded(msg)); + + if scenes_tx.send(vec![raster.scene]).is_err() { + eprintln!("failed to send scene to window"); + } + } + Err(err) => { + eprintln!("Failed to load dropped image {}: {err}", path.display()) + } + } + continue; + } + + // Non-raster files: load scenes, then resolve any embedded image refs. match load_master_scenes_from_path(&path).await { Ok(scenes) => { let scenes_for_loader = scenes.clone(); diff --git a/crates/grida-dev/src/platform/native_application.rs b/crates/grida-dev/src/platform/native_application.rs index fa7a6b62bd..bc5692855f 100644 --- a/crates/grida-dev/src/platform/native_application.rs +++ b/crates/grida-dev/src/platform/native_application.rs @@ -112,6 +112,10 @@ pub struct NativeApplication { pub(crate) modifiers: winit::keyboard::ModifiersState, file_drop_tx: Option>, fit_scene_on_load: bool, + /// Set to `true` after `CloseRequested` to prevent event processing on + /// a partially-torn-down application (the tick thread may still deliver + /// events between `event_loop.exit()` and actual termination). + exiting: bool, /// When >0, the next N ticks should request a redraw to produce a /// settle frame (showing "none" after a gesture ends). settle_countdown: u8, @@ -203,6 +207,7 @@ impl NativeApplication { modifiers: winit::keyboard::ModifiersState::default(), file_drop_tx, fit_scene_on_load, + exiting: false, settle_countdown: 0, scenes: Vec::new(), scene_index: 0, @@ -237,8 +242,13 @@ impl NativeApplicationHandler for NativeApplication { } if let WindowEvent::CloseRequested = &event { - self.app.renderer_mut().free(); + self.exiting = true; event_loop.exit(); + return; + } + + if self.exiting { + return; } if let WindowEvent::Resized(size) = &event { @@ -377,6 +387,9 @@ impl NativeApplicationHandler for NativeApplication { } fn user_event(&mut self, _event_loop: &winit::event_loop::ActiveEventLoop, event: HostEvent) { + if self.exiting { + return; + } match event { HostEvent::Tick => { // Poll for new scenes from the drop task. diff --git a/crates/grida-dev/src/platform/native_demo.rs b/crates/grida-dev/src/platform/native_demo.rs index a2584abd35..17b0a8047c 100644 --- a/crates/grida-dev/src/platform/native_demo.rs +++ b/crates/grida-dev/src/platform/native_demo.rs @@ -6,6 +6,7 @@ use cg::window::application::{ApplicationApi, HostEvent, HostEventCallback}; use futures::channel::mpsc; use std::path::PathBuf; use std::sync::Arc; +use std::time::Instant; use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; #[allow(dead_code)] @@ -74,8 +75,9 @@ async fn run_demo_window_core_multi( { let width = 1080; let height = 1080; + let startup_started_at = Instant::now(); - println!("🚀 Starting demo window..."); + println!("[demo] starting demo window"); let (tx, rx) = mpsc::unbounded(); let (font_tx, font_rx) = mpsc::unbounded(); @@ -92,15 +94,23 @@ async fn run_demo_window_core_multi( file_drop_tx.is_some(), scenes_rx, ); + println!( + "[demo] native application initialized in {:?}", + startup_started_at.elapsed() + ); let proxy = el.create_proxy(); let surface_ptr = app.app.surface_mut_ptr(); app.app.set_renderer_backend(Backend::GL(surface_ptr)); - println!("📸 Initializing image loader..."); - println!("🔄 Starting to load scene images in background..."); + println!( + "[demo] initializing image loader at {:?}", + startup_started_at.elapsed() + ); + println!("[demo] loading scene images in background"); let scene_clone = scene.clone(); let tx_clone = tx.clone(); + let image_load_started_at = Instant::now(); let event_cb: HostEventCallback = { let proxy_clone = proxy.clone(); Arc::new(move |event: HostEvent| { @@ -115,7 +125,10 @@ async fn run_demo_window_core_multi( let event_cb = event_cb.clone(); futures::executor::block_on(async move { load_scene_images(&scene_clone, tx_clone, event_cb).await; - println!("✅ Scene images loading completed in background"); + println!( + "[demo] scene images loaded in {:?}", + image_load_started_at.elapsed() + ); }); }); @@ -140,7 +153,10 @@ async fn run_demo_window_core_multi( app.app.devtools_rendering_set_show_ruler(true); app.app.devtools_rendering_set_show_tiles(false); - println!("🎭 Starting event loop..."); + println!( + "[demo] entering event loop after {:?}", + startup_started_at.elapsed() + ); if let Err(e) = el.run_app(&mut app) { eprintln!("Event loop error: {:?}", e); } diff --git a/crates/grida-dev/src/platform/winit.rs b/crates/grida-dev/src/platform/winit.rs index 449cbc42b2..64cdde3c43 100644 --- a/crates/grida-dev/src/platform/winit.rs +++ b/crates/grida-dev/src/platform/winit.rs @@ -11,7 +11,7 @@ use glutin_winit::DisplayBuilder; #[allow(deprecated)] use raw_window_handle::HasRawWindowHandle; use skia_safe::gpu; -use std::{ffi::CString, num::NonZeroU32}; +use std::{ffi::CString, num::NonZeroU32, time::Instant}; use winit::{ dpi::LogicalSize, event_loop::EventLoop, @@ -28,7 +28,8 @@ pub(crate) struct WinitResult { } pub(crate) fn winit_window(width: i32, height: i32) -> WinitResult { - println!("🔄 Window process started with PID: {}", std::process::id()); + let setup_started_at = Instant::now(); + println!("[winit] process started (pid={})", std::process::id()); let el = EventLoop::::with_user_event().build().unwrap(); @@ -54,7 +55,11 @@ pub(crate) fn winit_window(width: i32, height: i32) -> WinitResult { best }) .expect("failed to build window"); - println!("Picked a config with {} samples", gl_config.num_samples()); + println!( + "[winit] picked GL config: samples={} (elapsed={:?})", + gl_config.num_samples(), + setup_started_at.elapsed() + ); let window = window.expect("Could not create window with OpenGL context"); #[allow(deprecated)] let raw_window_handle = window @@ -148,6 +153,10 @@ pub(crate) fn winit_window(width: i32, height: i32) -> WinitResult { .expect("Could not create skia surface"); let state = SurfaceState::from_parts(gr_context, fb_info, surface); + println!( + "[winit] GL/Skia surface initialized in {:?}", + setup_started_at.elapsed() + ); WinitResult { state, diff --git a/docs/wg/feat-2d/optimization.md b/docs/wg/feat-2d/optimization.md index 4e4a573a93..84b841fbc4 100644 --- a/docs/wg/feat-2d/optimization.md +++ b/docs/wg/feat-2d/optimization.md @@ -983,6 +983,87 @@ Chromium uses 64 MB default with soft/hard limits. --- +## Slow-Pan Smoothness (FrameLoop) + +Slow trackpad panning (80-120ms between scroll events) was laggier than +fast panning — the fixed 50ms stable delay caused stable frames to fire +between every pair of scroll events, nuking the pan image cache and forcing +expensive full redraws. + +45. **Adaptive Stable Delay** ✅ IMPLEMENTED + + `FrameLoop` tracks input cadence via exponential moving average and + extends the effective stable delay to `max(base_delay, cadence × 2.5)`. + During 80ms trackpad scrolling, the delay becomes ~200ms. Stable frames + only fire when the user truly stops interacting. + + Cadence resets on session breaks (>500ms gap between events). Cadence + persists across stable frames (the user may still be scrolling slowly). + + **Measured impact (synthetic 200-node scene, `fl_80ms` scenario):** + + | Metric | Before | After | Improvement | + | ----------------- | -------- | ------ | ----------- | + | Stable intrusions | 25 | 1 | -96% | + | p50 frame time | 3,025 µs | 163 µs | 18.6× | + + Implementation: `FrameLoop` in `runtime/frame_loop.rs`. + +46. **Pan Cache Preservation on Stable Frames** ✅ IMPLEMENTED + + Stable frames no longer invalidate `pan_image_cache`. Previously, + `apply_changes()` and `queue()` nuked the pan cache when `stable=true`, + forcing the next unstable frame to do a full redraw. Since the stable + frame's render path recaptures the cache from every full-quality draw + anyway, the next unstable frame always has a fresh cache to blit from. + + Implementation: removed `|| stable` from `invalidate_pan` condition in + both `apply_changes()` and `queue()` in `runtime/scene.rs`. + +47. **Fully-Visible Stable Frame Fast Path** (reverted — correctness issues) + + When the viewport fully contains all scene content (`scene_envelope()` + containment check, O(1) via R-tree root node), the stable frame's full + draw path is redundant — the pan image cache already has the correct + pixels. The idea: blit from pan cache instead of doing an O(N) redraw. + + **Why it was reverted:** Three correctness bugs in succession: + 1. Blitting at `(0,0)` instead of the correct `(dx, dy)` offset caused + content to jump to the wrong position after panning stopped. + 2. Blitting at `(dx, dy)` clips content at viewport edges — the stable + frame never filled in the exposed strips, leaving permanent culling + artifacts at max zoom-out. + 3. Requiring `dx == dy == 0` and skipping the blit entirely (assuming + GPU surface persistence) caused stale back-buffer content to + accumulate — double-buffered GPU surfaces don't preserve content + across swaps. + + **The idea is valid but needs a different approach:** + - The `scene_envelope()` utility (O(1) R-tree root AABB) is kept in + `cache/scene.rs` for future use. + - A correct implementation would need either: (a) always blit the pan + cache at `(0,0)` after verifying the cache was captured at the + current camera position (not just any position), or (b) use a + `last_had_data_changes` flag that is reliably set in BOTH the + `frame()` and legacy `redraw()` code paths. + - The legacy `redraw()` path does not call `apply_changes()`, so any + flag set there is stale. Migrating all hosts to `frame()` would + eliminate this dual-path problem. + - The `queue()` stable promotion (non-camera events → stable quality) + interacts badly with clamped zoom at min/max zoom limits — the zoom + doesn't actually change, so `camera_change == None`, causing + unintended stable promotion that nukes the zoom cache and forces a + ~100ms full redraw. + + **Key files for future implementation:** + - `runtime/scene.rs` — `render_frame_with_plan_state()`, between the + pan-only cache check and the zoom cache check + - `cache/scene.rs` — `scene_envelope()` (already implemented) + - `runtime/scene.rs` — `apply_changes()` for `last_had_data_changes` + - `window/application.rs` — `frame()` vs `redraw()` dual-path issue + +--- + This list is designed to evolve the renderer from single-threaded mode to scalable, GPU-friendly real-time performance. Items are ordered roughly by implementation priority within each section. diff --git a/docs/wg/feat-2d/wasm-benchmarking.md b/docs/wg/feat-2d/wasm-benchmarking.md new file mode 100644 index 0000000000..55d3869a9d --- /dev/null +++ b/docs/wg/feat-2d/wasm-benchmarking.md @@ -0,0 +1,336 @@ +--- +title: "WASM Benchmarking Strategy" +format: md +tags: + - internal + - wg + - canvas + - performance + - wasm + - benchmarking +--- + +# WASM Benchmarking Strategy + +## Why Real WASM Benchmarking Matters + +When evaluating performance for a system that ships as WebAssembly, it is not +enough to benchmark only the native implementation. Native benchmarks are still +valuable, but they do not fully represent the runtime characteristics of WASM. + +WebAssembly introduces a different execution environment, different code +generation, different memory behavior, and often a different host integration +model. As a result, performance measured in native Rust cannot be assumed to +transfer directly to WASM. Even when the core algorithm is identical, the +compiled output and runtime constraints are not. + +Real WASM benchmarking is necessary whenever performance decisions are intended +to reflect the actual delivered runtime. + +## Why Native-Only Benchmarking Is Not Sufficient + +Native benchmarks are useful for understanding the upper bound of performance +and for comparing algorithmic choices in an idealized environment. They are +especially helpful for isolating pure compute costs and for detecting +regressions in the Rust implementation itself. + +However, native benchmarks do not capture several important WASM-specific +effects: + +- Code generation differences between native and WASM targets +- Linear memory behavior in WASM +- JS-WASM boundary costs +- Host runtime differences (V8/SpiderMonkey vs native CPU) +- Constraints specific to the WASM execution model (no SIMD by default, + limited stack, single-threaded) + +A native benchmark may correctly show that one version is faster than another, +while still failing to predict the actual magnitude of the improvement in WASM. + +## WASM-on-Node: Real WASM Without the Browser + +Although browser execution is the final target, benchmarking WASM does not +strictly require a browser in all cases. + +A WASM module can also run in Node.js. For compute-heavy workloads, this +provides a practical way to measure real WASM execution without introducing +browser automation, rendering setup, or UI-related noise. + +WASM-on-Node still measures: + +- The WASM target itself (same compiled `.wasm` binary) +- WASM code generation (same emscripten output) +- WASM memory behavior (same linear memory model) +- Much of the same runtime structure as deployed WASM (V8 WASM engine) + +It is not a native proxy pretending to be WASM. It is real WASM, just hosted +outside the browser. + +### What WASM-on-Node Is Good For + +WASM-on-Node is a strong option for benchmarks that are mostly internal to the +engine and do not depend heavily on browser APIs: + +- Scene loading and construction +- Layout computation +- Geometry processing +- Effect tree building +- Layer flattening and sorting +- Serialization and deserialization (FlatBuffers decode) +- Hit testing +- Pure compute kernels + +For these categories, WASM-on-Node provides meaningful performance +measurements and regression tracking. It is especially useful in CI, where +running a browser-based benchmark suite is heavier, more fragile, and harder +to keep deterministic. + +### What WASM-on-Node Cannot Fully Represent + +The browser environment adds behaviors and constraints that Node does not +reproduce exactly: + +- Browser event loop behavior and frame timing +- Worker scheduling characteristics +- Rendering pipeline costs (GPU flush, surface snapshot) +- DOM or canvas interaction +- Browser-specific memory pressure and GC behavior +- Browser-specific host API overhead + +WASM-on-Node should not be treated as a complete substitute for browser +benchmarking when the workload depends on browser integration. + +## Three-Layer Benchmarking Model + +A practical approach treats benchmarking as three separate layers: + +### Layer 1: Native benchmark + +Measures the implementation ceiling and isolates core algorithmic cost. + +Tools: `cargo run -p grida-dev --release -- load-bench`, Criterion benches +in `crates/grida-canvas/benches/`. + +Best for: Algorithmic comparisons, regression detection, profiling with +native tools (perf, Instruments, samply). + +### Layer 2: WASM-on-Node benchmark + +Measures real WASM execution in a simple, repeatable, automation-friendly +environment. + +Tools: Node.js script that loads the emscripten `.js` + `.wasm` module, +calls C-ABI functions, measures with `performance.now()`. + +Best for: Regression tracking, validating whether an optimization helps in +the WASM target, CI integration, comparing WASM/native ratios. + +### Layer 3: Browser benchmark + +The final reference for workloads that depend on browser-specific behavior +or APIs. + +Tools: Debug page at `/embed/v1/debug` with console timing, manual +stopwatch for end-to-end, browser DevTools profiling. + +Best for: Full pipeline validation (JS encode + WASM load + GPU render), +real-world user-facing latency measurement. + +## What We Can Safely Learn From WASM-on-Node + +WASM-on-Node is often very good for: + +- Tracking regressions over time +- Comparing alternative implementations +- Measuring scaling trends (how cost grows with node count) +- Validating whether an optimization helps in the WASM target at all + +Relative changes are more trustworthy than absolute numbers. If one +implementation is consistently 20% faster in WASM-on-Node, that is a +meaningful signal even if browser timings differ in absolute terms. + +## Caveats + +- Native Rust and WASM should not be assumed to have a fixed conversion + ratio. A workload that is close to native speed in one case may diverge + much more in another. +- WASM-on-Node and WASM-in-browser should not be assumed to be numerically + identical. They may show the same trend while differing in total runtime. +- Benchmarks that frequently cross the JS boundary may behave very + differently from benchmarks that remain almost entirely inside WASM. +- Any benchmark involving rendering, canvas, GPU, event loops, or browser + worker coordination should still be validated in the browser. + +## Lessons Learned (from load_scene optimization, 2026-03) + +### The WASM/native ratio is not constant across operations + +Different operations within the same codebase can have wildly different +WASM/native ratios. In the `load_scene` pipeline, we observed: + +- Simple compute (font collection, effect tree): ~2-3x WASM overhead +- HashMap-heavy traversal: 8-35x WASM overhead +- After replacing HashMap with Vec-indexed storage: overhead dropped to + 1-2x for data-structure-dominated stages, but stayed 5-30x for + compute-heavy stages + +The ratio depends on the nature of the work, not just the volume. Assuming +a single multiplier (e.g. "WASM is 3x slower") leads to incorrect +predictions. + +### Data structure choice matters far more in WASM than native + +HashMap with 136k entries showed acceptable performance on native (hidden +by hardware prefetch, out-of-order execution, and large caches). The same +HashMap in WASM was catastrophically slow because WASM's linear memory +model, smaller effective caches, and more in-order execution expose every +cache miss. + +The fix — replacing `HashMap` with `Vec>` indexed by +sequential `NodeId` — had a modest effect on native (~18% improvement) but +a dramatic effect on WASM, cutting some stages by 50% or more. This +confirms that data structure choices optimized for native may be poor +choices for WASM, and vice versa. + +### `std::time::Instant` does not work in emscripten WASM + +`Instant::now()` returns a constant value (effectively zero) under +emscripten. Any timing code that uses `Instant` will silently produce +meaningless results in WASM. + +The solution is `emscripten_get_now()` (bound as a C extern), which maps +to `performance.now()` and provides millisecond-resolution timing. We +wrapped this in `sys::perf_now()` which dispatches to `emscripten_get_now` +on WASM and `Instant`-based timing on native, so the same instrumentation +code works on both targets. + +### Native benchmarks can verify coverage but not WASM cost + +Before instrumenting WASM, we first confirmed that the native load-bench +covered the same code path as the WASM `switch_scene` C-ABI call. This +was important: if the benchmark had been missing a stage, the WASM +measurement would have been unexplainable. + +The native benchmark correctly identified all five stages (fonts, layout, +geometry, effects, layers) and their relative costs. What it could not +predict was which stages would blow up in WASM. The native profile showed +layers as the dominant cost (45%); in WASM, geometry was dominant (40%) +due to per-node HashMap amplification that native hardware masked. + +### Per-stage timing inside WASM is essential + +Without sub-stage timing, a 10-second WASM call is opaque. With +`emscripten_get_now()` instrumented around each stage, we immediately +identified that geometry and layout were the primary targets, not layers +(which native benchmarks had suggested). + +The pattern of adding `perf_now()` calls around major phases and +`eprintln!` for output (which appears in the browser console via stderr) +is lightweight and should be the first step in any WASM performance +investigation. + +### GPU-only code paths create WASM-specific bugs + +Two rendering bugs were found that only manifested in WASM because native +uses a CPU backend: + +1. `blit_content_cache()` drew a stale pan cache at (0,0) instead of the + correct offset — invisible on CPU backend because `is_gpu()` returned + false and the function short-circuited. +2. The overlay-only fast path intercepted stable frames, preventing + full-quality re-rendering — again only triggered on GPU backend. + +This reinforces that WASM testing is not just about performance. The GPU +backend (WebGL via emscripten) exercises code paths that native CPU +rendering never touches. + +### The JS-WASM boundary overhead is small for bulk operations + +For `load_scene`, the JS side contributes string allocation and a single +C-ABI call. The actual boundary cost (allocate string in WASM memory, +call `_switch_scene`, free string) is negligible compared to the work +inside WASM. For bulk operations, the boundary is not the bottleneck. + +However, the JS-side FlatBuffers encoding (serializing the scene graph +into a binary buffer before passing to WASM) is a non-trivial cost — +roughly 10% of the total pipeline. This work happens entirely in JS and +is invisible to Rust-side benchmarks. + +### Large enum access is pathologically slow in WASM + +After eliminating all data structure overhead (HashMap → DenseNodeMap), the +geometry DFS still showed 33× WASM/native ratio. The remaining bottleneck is +the `Node` enum itself — 15 variants, each a large struct. Accessing +`graph.get_node(id)` fetches a reference into `Vec>` where each +slot is the size of the largest variant (likely 500+ bytes). + +For 136k nodes, the DFS touches ~65MB of node data, most of which is +irrelevant to geometry (paints, text content, vector networks). Native +hardware mitigates this with prefetching and out-of-order execution. WASM's +linear memory model and bounds-checked loads make this 30× slower. + +The fix is a Struct-of-Arrays (SoA) approach: extract only the +geometry-relevant fields (transform, size, kind, ~48 bytes) into a compact +dense array, then run the DFS on that. This is documented in +`wasm-load-scene-optimization.md`. + +### Optimization priorities differ between native and WASM + +On native, the priority order for `load_scene` was: +layers > geometry > layout > fonts > effects. + +On WASM, after the same optimizations, the priority order was: +geometry > layout > layers > fonts > effects. + +An optimization strategy based purely on native profiling would have +targeted layers first. The actual highest-impact target in WASM was +geometry, due to HashMap amplification that native did not expose. + +## Implementation Plan: WASM-on-Node Benchmark Harness + +### Architecture + +The WASM-on-Node benchmark harness reuses the same emscripten-compiled +`.js` + `.wasm` module that ships to the browser. It loads the module in +Node.js, calls the C-ABI functions directly, and measures with +`performance.now()`. + +``` +Node.js script + |-- load grida-canvas-wasm.js (emscripten glue) + |-- instantiate grida_canvas_wasm.wasm + |-- call C-ABI: _create_app, _load_scene_grida, _switch_scene + |-- measure each call with performance.now() +``` + +### Scope + +The harness measures the `load_scene` pipeline — the same stages measured +by the native `load-bench`: + +1. FBS decode (`_load_scene_grida`) +2. Scene switch / layout+geometry+effects+layers (`_switch_scene`) + +GPU rendering is not available in Node (no WebGL context), so render-path +benchmarks are out of scope for this layer. + +### Key Differences from Browser + +- No GPU backend — the WASM module should be configured with a stub or + CPU-only backend for benchmarking load_scene (which does not render) +- No RAF loop — calls are synchronous +- No JS editor state — only the WASM module is exercised + +### Internal Timing + +The WASM module emits per-stage timing via `eprintln!` using +`sys::perf_now()` (which calls `emscripten_get_now()`). The Node harness +captures stderr and parses the `[load_scene]` line to extract per-stage +breakdowns without any additional instrumentation. + +### Location + +- WASM-on-Node bench: `crates/grida-canvas-wasm/lib/__test__/bench-load-scene.test.ts` +- Run: `cd crates/grida-canvas-wasm && npx vitest run __test__/bench-load-scene.test.ts` +- Reuses build artifacts from `crates/grida-canvas-wasm/lib/bin/` +- Requires fixture: `fixtures/local/perf/local/yrr-main.grida` (136k-node scene) diff --git a/docs/wg/feat-2d/wasm-load-scene-optimization.md b/docs/wg/feat-2d/wasm-load-scene-optimization.md new file mode 100644 index 0000000000..db1a202aa0 --- /dev/null +++ b/docs/wg/feat-2d/wasm-load-scene-optimization.md @@ -0,0 +1,261 @@ +--- +format: md +--- + +# WASM `load_scene` Optimization Plan + +## Status: In Progress + +## Problem + +`Renderer::load_scene()` for a 136k-node Figma-imported scene (yrr-main.grida) takes ~10s in WASM vs ~800ms native — a 13× overhead far beyond the normal 2-3× WASM/native ratio. + +## Current Measurements (WASM-on-Node) + +| Stage | Native (ms) | WASM (ms) | Ratio | Notes | +| --------- | ----------- | ---------- | --------- | -------------------------------- | +| fonts | 5 | 7 | 1.4× | Healthy | +| layout | 239 | 4,272 | 17.9× | Taffy tree + flex compute | +| geometry | 121 | 4,017 | 33× | DFS transform/bounds propagation | +| effects | 3 | 5 | 1.7× | Healthy | +| layers | 427 | 2,182 | 5.1× | Flatten + RTree | +| **total** | **796** | **10,484** | **13.2×** | | + +Stages with healthy ratios (fonts, effects) confirm that simple per-node work runs at 1.5-2× in WASM. The pathological ratios in geometry/layout/layers indicate something structurally cache-unfriendly. + +## Root Cause Analysis + +### What we ruled out + +These were investigated and either fixed or confirmed not the bottleneck: + +1. **HashMap overhead** — Replaced with `DenseNodeMap` (Vec-backed). Fixed fonts/effects ratios but geometry/layout/layers unchanged. +2. **Function parameter count** — Reduced `build_recursive` from 9 params to 5 via context struct. No measurable change. +3. **Redundant text measurement** — Geometry was calling `paragraph_cache.measure()` for all 27k text spans even when layout results existed. Fixed (skip when layout provides dimensions). Helped native slightly, no WASM change. +4. **RTree sequential insert** — Replaced `RTree::new()` + N inserts with `RTree::bulk_load()`. Marginal improvement. +5. **`taffy_to_scene` HashMap** — Was written to on every node insert but only read in `#[cfg(test)]`. Gated behind `#[cfg(test)]`. No measurable change. + +### What IS the bottleneck + +The `Node` enum has 15 variants, each containing a full `*NodeRec` struct (transform, paints, effects, text content, vector networks, etc.). During the geometry DFS, every `graph.get_node(id)` fetches a reference into a `Vec>` where each slot is the size of the largest variant — likely 500+ bytes. + +For 136k nodes, the DFS touches ~65MB of node data, most of which is irrelevant to geometry (paints, text content, etc.). In WASM's linear memory model, this cache-unfriendly access pattern is amplified: + +- **Native**: L1/L2 cache prefetching partially hides latency → 0.9μs/node +- **WASM**: Linear memory accesses compile to bounds-checked loads, no hardware prefetch → 29μs/node (33×) + +The layout stage (17.9×) has a similar problem inside Taffy's `SlotMap` internals, plus the cost of building a parallel Taffy tree from our scene graph. + +The layers stage (5.1×) is a DFS that also accesses the full `Node` enum plus geometry cache per node, though it's less pathological since it reads from the already-built geometry cache (dense, cache-friendly). + +## Recommended Fix: Targeted SoA for Geometry Phase + +### Concept + +Extract a compact, geometry-only representation from the scene graph once (O(n) scan), then run the DFS on that instead of the full `Node` enum. + +```rust +/// Compact per-node data for geometry computation. +/// ~48 bytes vs hundreds for the full Node enum. +#[derive(Clone, Copy)] +struct GeoInput { + transform: AffineTransform, // 28 bytes (6 f32 + rotation) + width: f32, // from layout result or schema + height: f32, // from layout result or schema + kind: GeoNodeKind, // 1 byte enum + render_bounds_inflation: f32, // pre-computed from effects/stroke +} + +#[repr(u8)] +enum GeoNodeKind { + Group, + InitialContainer, + Container, + BooleanOperation, + TextSpan, + Leaf, +} +``` + +### Implementation Steps + +1. **Define `GeoInput` and `GeoNodeKind`** in `cache/geometry.rs` + +2. **Add extraction pass** in `from_scene_with_layout`: + + ```rust + // O(n) scan: extract geometry-relevant data from Node enum + let mut geo_inputs = DenseNodeMap::with_capacity(graph.node_count()); + for (id, node) in graph.nodes_iter() { + let layout = layout_result.and_then(|r| r.get(&id)); + geo_inputs.insert(id, GeoInput::from_node(node, layout, &ctx)); + } + ``` + +3. **Rewrite `build_recursive`** to operate on `&GeoInput` instead of `&Node`: + - `graph.get_node(id)` → `geo_inputs.get(id)` (44 bytes, cache-friendly) + - Match on `GeoNodeKind` (1-byte discriminant) instead of `Node` (large enum) + - Children still come from `graph.get_children(id)` (unchanged) + +4. **Handle text measurement**: For `GeoNodeKind::TextSpan` without layout results, store the measured (width, height) in `GeoInput` during the extraction pass. This moves text measurement out of the DFS entirely. + +5. **Handle render bounds**: Pre-compute the effect/stroke inflation in `GeoInput::from_node` so the DFS only needs `world_bounds.inflate(inflation)`. + +### Expected Impact + +- **Geometry**: The DFS now touches ~6.5MB (48 bytes × 136k) instead of ~65MB. Should bring WASM ratio from 33× closer to 3-5×. +- **Native**: Also benefits from better cache locality, potentially 2-3× faster. +- **Layers**: Can follow the same pattern later (extract a `LayerInput` struct). +- **Layout**: Harder — Taffy's internal data structures are the bottleneck. Consider profiling Taffy separately. + +### What this does NOT change + +- Taffy layout computation (still uses Taffy's own data structures) +- Layer flattening (still reads full Node enum for paint info — separate optimization) +- Scene graph structure (Node enum stays as-is for all other uses) + +### Risks + +- **Render bounds accuracy**: Pre-computing inflation requires careful handling of per-side stroke widths and multiple effect types. The extraction pass must match the current `compute_render_bounds_*` logic exactly. +- **Text measurement in extraction**: Moving text measurement before the DFS means we measure all text nodes, even inactive ones. Add an `active` check. +- **Maintenance**: Two representations of the same data. Document clearly that `GeoInput` is a cache, not a source of truth. + +## Benchmark Infrastructure + +WASM-on-Node benchmarks are in `crates/grida-canvas-wasm/lib/__test__/bench-load-scene.test.ts`. Run with: + +```sh +cd crates/grida-canvas-wasm +npx vitest run __test__/bench-load-scene.test.ts +``` + +Native benchmarks: + +```sh +cargo run -p grida-dev --release -- load-bench fixtures/local/perf/local/yrr-main.grida --iterations 3 +``` + +Build WASM (from repo root): + +```sh +just --justfile crates/grida-canvas-wasm/justfile build +``` + +## Files to Modify + +| File | Change | +| -------------------------------------------- | ------------------------------------------------------ | +| `crates/grida-canvas/src/cache/geometry.rs` | Add `GeoInput`, extraction pass, rewrite DFS | +| `crates/grida-canvas/src/cache/paragraph.rs` | No change (already optimized with measurement caching) | +| `crates/grida-canvas/src/cache/scene.rs` | No change | +| `crates/grida-canvas/src/layout/tree.rs` | No change | + +## Validation + +1. `cargo test -p cg` — all 330 tests must pass +2. `cargo check -p cg -p grida-canvas-wasm -p grida-dev` — all crates compile +3. Native benchmark: should not regress (target: `<800ms`) +4. WASM-on-Node benchmark: geometry stage should drop from ~4s to `<1s` +5. Visual: load yrr-main in browser debug embed, verify text renders correctly and pan/zoom/settle work + +--- + +## Results: GeoInput SoA Extraction (Implemented) + +### What was implemented + +`GeoInput` struct + `GeoNodeKind` enum + `RenderBoundsInfo` enum in +`cache/geometry.rs`. The geometry cache now runs in two phases: + +1. **O(n) extraction pass** — iterates `graph.nodes_iter()`, extracts + `GeoInput` (~56 bytes) per node into a `DenseNodeMap`. Text + measurement happens here. +2. **DFS pass** — operates on `DenseNodeMap` only. Never touches + the full `Node` enum. + +All 330 tests pass. No API changes. Single file modified. + +### Benchmark Results (yrr-main.grida, 136K nodes) + +**Native (release, 3-iteration average):** + +| Stage | Before (ms) | After (ms) | Delta | +| ---------------- | ----------- | ---------- | ----------- | +| geometry total | 121 | 130 | +7% (noise) | +| geometry extract | — | 121 | (new) | +| geometry DFS | ~121 | 7 | **-94%** | +| total | 796 | 706 | -11% | + +**WASM-on-Node (release):** + +| Stage | Before (ms) | After (ms) | Delta | +| ---------------- | ----------- | ---------- | ------------ | +| geometry total | 4,017 | 4,668 | +16% (noise) | +| geometry extract | — | 4,661 | (new) | +| geometry DFS | ~4,017 | **5** | **-99.9%** | +| total | 10,484 | 12,500 | — | + +### Analysis + +The DFS optimization worked exactly as designed — **DFS dropped from +~4,000ms to 5ms in WASM** (800x improvement, 0.7x native ratio). Once +data is compact, WASM operates at near-native speed. + +However, the total geometry time is unchanged because the **extraction +pass inherits the same bottleneck**: it iterates `graph.nodes_iter()` +which yields `&Node` references into `Vec>` where each slot +is 500+ bytes. The sequential scan still touches ~65 MB of cold data in +WASM linear memory. + +The cost shifted from DFS to extraction. The root cause is confirmed: +**any iteration over the full `Node` enum is fundamentally cache-unfriendly +in WASM**, regardless of whether it's a DFS or a sequential scan. + +### Conclusion + +SoA extraction within geometry.rs is a dead end for total geometry time. +The extraction pass itself is the bottleneck, and it must touch the `Node` +enum. To eliminate this cost, the split must happen **upstream** — at scene +graph construction time — so that geometry-relevant data is never stored +inside the monolithic `Node` enum in the first place. + +See [docs/wg/research/chromium/node-data-layout.md](../../research/chromium/node-data-layout.md) +for research on Chromium's property tree architecture, which solves +exactly this problem by storing properties in separate flat arrays indexed +by integer IDs. + +## Next Steps: Property Split at Scene Graph Level + +The GeoInput experiment proved the hypothesis: **compact data = fast WASM**. +The remaining question is where to split: + +### Option A: Split at SceneGraph construction + +Populate `DenseNodeMap`, `DenseNodeMap`, etc. +during FBS decode / JSON parse. The `Node` enum remains for painter and +export but hot loops (geometry, layers, effects) use the split maps. + +- Pro: Incremental migration, no format changes +- Con: Dual storage during transition + +### Option B: Reshape the FBS schema + +Store geometry-relevant fields in a separate FBS table. Decode directly +into split maps without materializing the full `Node`. + +- Pro: Minimal memory (no dual storage), aligned end-to-end +- Con: Format migration, breaks existing .grida files + +### Option C: Full ECS + +Replace `Node` enum with entity-component storage (e.g., archetype-based). + +- Pro: Maximum flexibility for future component shapes +- Con: Highest complexity, archetype migration overhead for common editing + operations, tree traversal requires indirection + +**Recommendation: Option A** (split at SceneGraph) as the incremental path, +with Option B as the long-term goal once the split maps stabilize. + +See [docs/wg/research/chromium/node-data-layout.md](../../research/chromium/node-data-layout.md) +for the full analysis including ECS tradeoffs and mutation considerations. diff --git a/docs/wg/feat-fig/index.md b/docs/wg/feat-fig/index.md index 7d30c3de84..1f7647e3c6 100644 --- a/docs/wg/feat-fig/index.md +++ b/docs/wg/feat-fig/index.md @@ -6,7 +6,6 @@ tags: - wg - figma - import - --- # .fig Format - `io-figma` @@ -53,7 +52,7 @@ The following capabilities are explicitly **not** in scope: The `.fig` file format uses the Kiwi binary encoding protocol. A typical `.fig` file consists of: -1. **Header** - File prelude (`"fig-kiwi"` or `"fig-jam."`) and version number +1. **Header** - An 8-byte ASCII prelude (magic string) identifying the container, then a little-endian version `u32`. Known preludes are all exactly eight bytes: `"fig-kiwi"` (Figma design files), `"fig-jam."` (FigJam — the trailing `.` is part of the format, not punctuation), and `"fig-deck"` (deck files). 2. **Chunk 1** - Compressed Kiwi schema definition 3. **Chunk 2+** - Compressed scene data encoded using the schema @@ -80,7 +79,6 @@ This is why we maintain the [fig.kiwi schema file](https://github.com/gridaco/gr ### Implementation Strategy 1. **For `.fig` file import:** - - Read schema from chunk 1 - Use embedded schema to decode remaining chunks - More robust to schema changes @@ -106,6 +104,22 @@ See [Kiwi Schema Glossary](./glossary/fig.kiwi.md) for detailed schema documenta - [Figma Inside — .fig file analysis by easylogic](https://easylogic.medium.com/7252bef141da) - [fig2json@GitHub / rust](https://github.com/kreako/fig2json) +## Figma Deck (Slides) Import + +Figma's REST API does **not** expose Deck/Slides node types. They exist only in the `.fig` Kiwi binary format with the `"fig-deck"` prelude. The Kiwi schema defines `SLIDE`, `SLIDE_GRID`, `SLIDE_ROW`, and `INTERACTIVE_SLIDE_ELEMENT` node types. + +Grida handles these through the `iofigma.__ir` intermediate representation: + +| Kiwi type | IR type | Grida output | +| ------------------------------------ | -------------- | ------------ | +| `SLIDE`, `INTERACTIVE_SLIDE_ELEMENT` | `X_SLIDE` | container | +| `SLIDE_GRID` | `X_SLIDE_GRID` | container | +| `SLIDE_ROW` | `X_SLIDE_ROW` | container | + +All three IR types are structurally equivalent to `figrest.FrameNode` (children, fills, clips, layout) and convert to Grida containers — the same output as frames. `X_SLIDE` carries an optional `slideMetadata` bag (speaker notes, skip flag, slide numbering) for future use. + +Full slide semantics (presenter mode, slide indices in UI, theme maps) are out of scope; this mapping ensures FigDeck files produce a complete, visually usable scene graph without dropping slide subtrees. + ## See Also - [WG:SVG](../feat-svg) diff --git a/docs/wg/research/chromium/index.md b/docs/wg/research/chromium/index.md index 8b98700f29..9015f5fa79 100644 --- a/docs/wg/research/chromium/index.md +++ b/docs/wg/research/chromium/index.md @@ -6,7 +6,6 @@ tags: - chromium - rendering - compositing - --- # Chromium Compositor Research @@ -36,6 +35,7 @@ material when designing rendering systems that face similar problems. | [resolution-scaling-during-interaction.md](./resolution-scaling-during-interaction.md) | Source-level: pinch-zoom raster scale, stale-tile reuse, CoverageIterator | | [pinch-zoom-deep-dive.md](./pinch-zoom-deep-dive.md) | Pinch-zoom: GPU tile stretching, anchor point, settle/refine, data flow | | [effect-optimizations.md](./effect-optimizations.md) | Effect optimization: filter demotion, render pass bypass, damage tracking | +| [node-data-layout.md](./node-data-layout.md) | Node data layout: DOM RareData, compositor property trees, ECS comparison | | [svg-pattern.md](./svg-pattern.md) | SVG `` paint server semantics, Chromium/resvg/Skia comparison | ## Source locations diff --git a/docs/wg/research/chromium/node-data-layout.md b/docs/wg/research/chromium/node-data-layout.md new file mode 100644 index 0000000000..8e3da6ac2a --- /dev/null +++ b/docs/wg/research/chromium/node-data-layout.md @@ -0,0 +1,338 @@ +--- +title: "Chromium Node Data Layout for Rendering" +format: md +tags: + - internal + - research + - chromium + - rendering + - performance + - data-layout +--- + +# Chromium Node Data Layout for Rendering + +How Chromium stores and accesses per-node data across its rendering +pipeline, with focus on data layout strategies that determine cache +locality and iteration cost during compositing and property propagation. + +See [property-trees.md](./property-trees.md) for the full property tree +structure. This document focuses on **why** the data is split that way +and how the pattern extends to DOM/SVG storage and mutation. + +--- + +## Three Storage Tiers + +Chromium uses three distinct tiers of per-node storage, each optimized +for different access patterns: + +### Tier 1: DOM Objects (Blink) — Monolithic with RareData + +DOM nodes (`Element`, `SVGElement`, `LayoutObject`) are heap-allocated +objects. Each contains all properties for that node type. To manage size, +Blink factors rarely-used properties into lazily-allocated `RareData` +objects: + +``` +SVGElement (always allocated): + class_name_: Member + svg_rare_data_: Member // null until needed + + inherited Element fields (~100+ bytes) + +SVGElementRareData (allocated on demand): + animated_sms_style_properties_ + presentation_attribute_style_ + ... +``` + +The same pattern appears in `LayoutObject` (`LayoutObjectRareData`) and +`LayerImpl` (`RareProperties`). + +Source: `third_party/blink/renderer/core/svg/svg_element.h`, +`third_party/blink/renderer/core/layout/layout_object.h` + +**Key insight:** Blink tolerates monolithic objects at the DOM layer +because DOM operations are infrequent relative to compositor-driven +rendering. The performance-critical path is in the compositor, which +uses a different layout. + +### Tier 2: Compositor Layers — Thin Index Carriers + +Each compositor layer (`LayerImpl`) stores minimal data plus four integer +indices into the property trees: + +``` +LayerImpl (~100 bytes hot data): + bounds_: gfx::Size // 8 bytes + offset_to_transform_parent_: gfx::Vector2dF // 8 bytes + transform_tree_index_: int // 4 bytes + effect_tree_index_: int // 4 bytes + clip_tree_index_: int // 4 bytes + scroll_tree_index_: int // 4 bytes + draw_properties_: DrawProperties // computed cache + element_id_: ElementId // 16 bytes + + bitfields (~4 bytes) + rare_properties_: unique_ptr // cold, heap-allocated +``` + +A layer does not own its transform, effect, or clip data. It references +shared property tree nodes. Multiple sibling layers with the same +transform parent share a single `TransformNode`. + +Source: `cc/layers/layer_impl.h` + +### Tier 3: Property Trees — SoA by Domain + +Properties are stored in four flat `std::vector` arrays, one per +domain: + +| Array | Element Type | Approx Size/Element | What Iterates It | +| ----------------------- | --------------- | ------------------- | ----------------------- | +| `TransformTree::nodes_` | `TransformNode` | ~200 bytes | `UpdateAllTransforms()` | +| `EffectTree::nodes_` | `EffectNode` | ~120 bytes | `ComputeEffects()` | +| `ClipTree::nodes_` | `ClipNode` | ~80 bytes | `ComputeClips()` | +| `ScrollTree::nodes_` | `ScrollNode` | ~60 bytes | Scroll handling | + +Plus a parallel cache vector for computed results: + +| Array | Element Type | Approx Size/Element | Purpose | +| ----------------------------- | ------------------------- | ------------------- | -------------------------- | +| `TransformTree::cached_data_` | `TransformCachedNodeData` | ~136 bytes | `to_screen`, `from_screen` | + +Each rendering pipeline step walks **one** property tree contiguously. +`UpdateAllTransforms()` reads `TransformNode.local`/`to_parent` and +writes `TransformCachedNodeData.to_screen` — both are sequential vector +accesses. This is cache-friendly: the working set is one input vector + +one output vector. + +Source: `cc/trees/property_tree.h`, `cc/trees/transform_node.h` + +--- + +## Why This Layout Works + +### Transform Propagation + +Before property trees, Chromium stored all properties on layers and walked +the layer tree to propagate transforms. The `CalculateDrawProperties()` +function was one of the largest performance bottlenecks because each layer +had 50+ fields but transform propagation only needed 3-4. + +After the property tree refactor: + +| Metric | Layer-Walk (old) | Property Tree (current) | +| ----------------------- | ---------------------------- | -------------------------- | +| Data per node | ~500 bytes (full layer) | ~200 bytes (TransformNode) | +| Working set (1K layers) | ~500 KB | ~200 KB | +| Cache lines touched | ~8 per node | ~3 per node | +| Other properties loaded | All (paints, clips, effects) | None | + +The key: **separation by access pattern**. Transform propagation never +touches effect data. Effect computation never touches clip data. Each +stage loads only what it needs. + +### Shared Nodes + +Property trees have **fewer nodes** than the layer tree. Common case: + +- 1000 layers might reference only 200 transform nodes (sibling groups + share parents) +- An opacity change on a container creates one `EffectNode` referenced by + all descendant layers, not N copies + +This sharing reduces both storage and propagation cost. + +--- + +## Mutation and Incremental Update + +Property trees are **persistent across frames** and support efficient +single-node mutation. + +### Mutation Flow + +1. **Mutate**: Write to the property node field + set dirty flag + + ``` + TransformNode: + needs_local_transform_update: bool // dirty flag + transform_changed_: bool // change tracking + damage_reasons_: DamageReasonSet // why it changed + ``` + +2. **Propagate**: Next frame, `UpdateAllTransforms()` walks the flat + vector top-down. For each node: + - If `needs_local_transform_update`: recompute `to_parent` from + `local`, `origin`, `scroll_offset`, `post_translation` + - Always recompute `to_screen = parent.to_screen * to_parent` (cached) + - If `transform_changed_`: propagate change flag to descendants for + damage tracking + +3. **Damage**: Changed flags feed into `DamageTracker` which determines + which render surfaces need redraw. + +### Compositor-Thread Animations + +For animated properties (transform, opacity), Chromium avoids the main +thread entirely: + +``` +Main Thread → commit → Pending Tree → activation → Active Tree + ↑ + MutatorHost drives + animations directly +``` + +The compositor thread mutates `TransformNode.local` and +`EffectNode.opacity` directly on the active tree. Scroll offsets are +similarly dual-tracked via `SyncedScrollOffsetMap` (main-thread value + +impl-thread value). + +### Single-Node Mutation Cost + +| Operation | Cost | Notes | +| ------------------------- | --------------- | --------------------------- | +| Set transform on one node | O(1) | Write field + set dirty bit | +| Propagate transforms | O(tree_size) | Sequential vector walk | +| Re-propagate only subtree | Not implemented | Chromium walks full tree | +| Add/remove property node | O(1) amortized | Vector push/pop | + +Chromium does not implement subtree-scoped propagation because web page +property trees are typically small (100-500 nodes). For scenes with +significantly larger property trees (tens of thousands of nodes), +subtree-scoped propagation would be a worthwhile extension. + +Source: `cc/trees/transform_node.h` (lines 26-183), +`cc/trees/property_tree.h` (`UpdateAllTransforms`) + +--- + +## Blink SVG: Where Monolithic Storage Hurts + +SVG elements store all properties on the DOM object. Each `SVGElement` +inherits from `Element` (which inherits from `Node`) and adds SVG-specific +data. An SVG `` carries: + +- Transform (presentation attribute or CSS) +- Geometry (`x`, `y`, `width`, `height`, `rx`, `ry`) +- Paint (fill, stroke, opacity) +- Effects (filter, clip-path, mask) +- Layout state + +During SVG rendering, Blink resolves styles and paints for each element, +touching all fields even when only a subset is needed. Blink mitigates +this via: + +1. **Style sharing**: Resolved styles are shared between elements with + identical computed values (`ComputedStyle` is reference-counted) +2. **Paint invalidation**: Only elements with changed properties are + re-painted (invalidation rect tracking) +3. **Hardware acceleration**: SVG elements with `will-change: transform` + or CSS animations are promoted to compositor layers, which then use + the property tree architecture + +For SVG without compositor promotion, Blink does pay the monolithic-object +cost. This is a known performance issue for complex SVG content. + +Source: `third_party/blink/renderer/core/svg/svg_element.h`, +`third_party/blink/renderer/core/layout/svg/` + +--- + +## Comparison: ECS vs Property Trees + +Game engines (Bevy, Unity DOTS) use Entity-Component-System (ECS) as an +alternative data layout strategy. Both ECS and property trees achieve +SoA-style access, but with different tradeoffs. + +| Aspect | Property Trees (Chromium) | ECS (Bevy) | +| --------------------- | --------------------------------------- | ------------------------------------------------- | +| Storage | Flat `Vec` per property domain | Archetype tables (SoA within archetype) | +| Access | Direct integer index into vector | Query over matching archetypes | +| Hierarchy | `parent_id` field in each node | `ChildOf` component + `Children` | +| Node sharing | Siblings share property nodes | No sharing; each entity owns components | +| Mutation | Write field + dirty flag | Write component (change detection) | +| Adding properties | Insert into the relevant vector | Archetype migration (entity moves between tables) | +| Removing properties | Remove from the relevant vector | Archetype migration | +| Transform propagation | Sequential top-down vector walk | Parallel DFS with work-stealing | +| Sparse data | Dense vector (unused slots waste space) | Sparse: only entities with component are stored | +| Complexity | Low (flat arrays + indices) | High (archetype bookkeeping, query resolution) | + +### ECS Archetype Migration + +When a component is added or removed from an entity in ECS, the entity +must migrate between archetype tables (because the storage layout +changes). This involves copying all component data to the new table. +For example, adding a drop shadow to a shape would trigger archetype +migration (moving the entity from `[Transform, Style, Geometry]` to +`[Transform, Style, Geometry, Effects]`). + +Property trees avoid this: adding an effect to a node creates an +`EffectNode` in the effect tree and sets `effect_tree_index_` on the +layer. No data movement for other properties. + +### Suitability + +Property trees are better suited for rendering engines with: + +- Stable component shapes (most nodes have the same set of properties) +- Tree-structured hierarchical propagation +- Frequent single-property mutations (animation, interaction) +- Need for property sharing between nodes + +ECS is better suited for: + +- Highly heterogeneous entities (wildly different component sets) +- Flat iteration over specific component combinations +- Dynamic component addition/removal as a core operation + +For scene graphs that resemble a design tool or document renderer — +stable node types, tree-structured transforms, frequent interactive +edits — the property tree model is a better fit. + +--- + +## Key Takeaways + +1. **Split by access pattern, not by identity.** Transform propagation + should only touch transform data. Effect computation should only touch + effect data. Storing all properties in one object forces every pipeline + stage to load irrelevant data. + +2. **Flat contiguous arrays.** Property trees store each domain in a + dense `std::vector` with O(1) index access. Sequential top-down + walks get full benefit of hardware prefetching. + +3. **Shared property nodes reduce tree size.** Sibling layers with the + same transform parent share a single `TransformNode`. The property + tree is often 5-10x smaller than the layer tree. + +4. **Persistent trees with dirty flags.** Trees are not rebuilt from + scratch each frame. Single-node mutation is O(1) (write + dirty bit), + propagation is O(tree_size) via sequential vector walk. + +5. **Monolithic objects are tolerated only where iteration is rare.** + Blink's DOM objects are monolithic because style resolution and paint + are per-element operations with invalidation. The compositor, which + must walk all layers every frame, uses split property trees. + +6. **RareData factoring is a partial mitigation.** Lazily-allocated + cold-data objects reduce the base object size but do not help with + iteration cost — the hot-data portion is still interleaved with + pointers and padding in the base object. + +--- + +## Source Files Referenced + +- `third_party/blink/renderer/core/svg/svg_element.h` +- `third_party/blink/renderer/core/layout/layout_object.h` +- `cc/layers/layer_impl.h` +- `cc/trees/property_tree.h` +- `cc/trees/transform_node.h` +- `cc/trees/effect_node.h` +- `cc/trees/clip_node.h` +- `cc/trees/scroll_node.h` +- `cc/trees/draw_property_utils.h` +- `cc/trees/draw_property_utils.cc` diff --git a/editor/app/(embed)/embed/v1/debug/page.tsx b/editor/app/(embed)/embed/v1/debug/page.tsx index ead34b490f..479e968b74 100644 --- a/editor/app/(embed)/embed/v1/debug/page.tsx +++ b/editor/app/(embed)/embed/v1/debug/page.tsx @@ -210,7 +210,7 @@ export default function EmbedDebugPage() { { const f = e.target.files?.[0]; diff --git a/editor/grida-canvas-react-starter-kit/starterkit-import/from-figma.tsx b/editor/grida-canvas-react-starter-kit/starterkit-import/from-figma.tsx index ac0976e2ce..e22ffbe890 100644 --- a/editor/grida-canvas-react-starter-kit/starterkit-import/from-figma.tsx +++ b/editor/grida-canvas-react-starter-kit/starterkit-import/from-figma.tsx @@ -178,7 +178,8 @@ function FigFileImportTab({ const parseRunIdRef = useRef(0); const validateFile = (file: File) => { - return file.name.toLowerCase().endsWith(".fig"); + const name = file.name.toLowerCase(); + return name.endsWith(".fig") || name.endsWith(".deck"); }; const handleParse = useCallback(async (file: File, runId: number) => { @@ -258,7 +259,7 @@ function FigFileImportTab({ setParsed(result); setStep("confirm"); } catch (error) { - toast.error("Failed to parse .fig file"); + toast.error("Failed to parse file"); console.error(error); if (!isStale()) { // Mark failure to prevent repeated attempts for the same file @@ -340,7 +341,7 @@ function FigFileImportTab({ [Import Figma] to import .fig files"); continue; } diff --git a/editor/grida-canvas/editor.ts b/editor/grida-canvas/editor.ts index 1627e2312b..ced9b4d101 100644 --- a/editor/grida-canvas/editor.ts +++ b/editor/grida-canvas/editor.ts @@ -3214,10 +3214,17 @@ export class Editor sceneId?: string ) => { const t0 = __DEV__ ? performance.now() : 0; + let tLoad = 0; + let encodeMs = 0; + let loadMs = 0; try { const bytes = io.GRID.encode(document); + const tEncode = __DEV__ ? performance.now() : 0; surface.loadSceneGrida(bytes); + tLoad = __DEV__ ? performance.now() : 0; + encodeMs = tEncode - t0; + loadMs = tLoad - tEncode; } catch { // Fallback to JSON if FlatBuffers encoding fails (e.g. unsupported node types) const p = JSON.stringify({ @@ -3225,6 +3232,9 @@ export class Editor document, }); surface.loadScene(p); + tLoad = __DEV__ ? performance.now() : 0; + encodeMs = 0; + loadMs = tLoad - t0; } // loadSceneGrida only decodes and stores scenes. @@ -3236,12 +3246,17 @@ export class Editor surface.switchScene(targetScene); } + const tSwitch = __DEV__ ? performance.now() : 0; + surface.redraw(); if (__DEV__) { + const tRedraw = performance.now(); console.log( `[syncDocument] ${Object.keys(document.nodes).length} nodes, ` + - `scene=${targetScene ?? "(none)"} in ${(performance.now() - t0).toFixed(0)}ms` + `scene=${targetScene ?? "(none)"} in ${(tRedraw - t0).toFixed(0)}ms` + + ` (encode=${encodeMs.toFixed(0)}ms load=${loadMs.toFixed(0)}ms` + + ` switch=${(tSwitch - tLoad).toFixed(0)}ms redraw=${(tRedraw - tSwitch).toFixed(0)}ms)` ); } }; diff --git a/editor/scaffolds/embed/use-refig-editor.ts b/editor/scaffolds/embed/use-refig-editor.ts index b889558dce..a5c2f1e2f8 100644 --- a/editor/scaffolds/embed/use-refig-editor.ts +++ b/editor/scaffolds/embed/use-refig-editor.ts @@ -6,11 +6,13 @@ import { io } from "@grida/io"; import { editor } from "@/grida-canvas"; import { useEditor, useEditorState } from "@/grida-canvas-react"; import { distro } from "@/grida-canvas-hosted/distro"; +import type iofigma from "@grida/io-figma"; function validateExt(name: string) { const l = name.toLowerCase(); return ( l.endsWith(".fig") || + l.endsWith(".deck") || l.endsWith(".json") || l.endsWith(".json.gz") || l.endsWith(".zip") @@ -44,31 +46,20 @@ async function decompressGzip(buf: ArrayBuffer): Promise { * Renderer configuration for the refig embed canvas. * * These flags are applied to the WASM renderer after mount, before any - * document is loaded. Values are intentionally narrow literals today — - * widen to `boolean` once the feature graduates from "always-on". + * document is loaded. */ -interface RefigRenderConfig { +interface RefigRenderConfig extends Pick< + iofigma.restful.factory.FactoryContext, + "prefer_fixed_text_sizing" +> { /** - * Skip the Taffy flexbox layout engine during scene loading. - * - * Figma imports use absolute positioning — running layout on 100k+ - * nodes is the dominant cold-start cost (~25 s in WASM). Setting - * this to `true` derives layout from schema positions instead. + * Skip the flexbox layout engine during scene loading. */ - cg_skip_layout: true; - /** - * Bake Figma's absoluteBoundingBox dimensions into TEXT nodes instead - * of relying on layout-time text measurement. - * - * Paired with `cg_skip_layout` — without this, text nodes get 0×0 - * sizes because the layout engine (which would measure them) is skipped. - */ - prefer_fixed_text_sizing: true; + cg_skip_layout: boolean; } const REFIG_RENDER_CONFIG: RefigRenderConfig = { - cg_skip_layout: true, - prefer_fixed_text_sizing: true, + cg_skip_layout: false, }; export function useRefigEditor() { diff --git a/fixtures/.gitattributes b/fixtures/.gitattributes index 0eb23d95a0..b243b4a503 100644 --- a/fixtures/.gitattributes +++ b/fixtures/.gitattributes @@ -1 +1,2 @@ -*.fig filter=lfs diff=lfs merge=lfs -text \ No newline at end of file +*.fig filter=lfs diff=lfs merge=lfs -text +*.deck filter=lfs diff=lfs merge=lfs -text \ No newline at end of file diff --git a/fixtures/test-fig/deck/light.deck b/fixtures/test-fig/deck/light.deck new file mode 100644 index 0000000000..b29966bb65 --- /dev/null +++ b/fixtures/test-fig/deck/light.deck @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f230717e027ea0e0e4dd06a9dd4743457ccb42bf0005ebbaa5cfd1b6b1379414 +size 1191797 diff --git a/packages/grida-canvas-io-figma/__bench__/fig2grida.bench.ts b/packages/grida-canvas-io-figma/__bench__/fig2grida.bench.ts index 5b9a8494a9..348d564a5f 100644 --- a/packages/grida-canvas-io-figma/__bench__/fig2grida.bench.ts +++ b/packages/grida-canvas-io-figma/__bench__/fig2grida.bench.ts @@ -35,9 +35,7 @@ if (!HAS_FIXTURE) { const FIXTURE_GZ = HAS_FIXTURE ? readFileSync(FIXTURE_PATH) : null; -const decompressedBytes = FIXTURE_GZ - ? gunzipSync(FIXTURE_GZ) - : null; +const decompressedBytes = FIXTURE_GZ ? gunzipSync(FIXTURE_GZ) : null; const jsonString = decompressedBytes ? new TextDecoder().decode(decompressedBytes) : null; @@ -50,7 +48,6 @@ if (FIXTURE_GZ && decompressedBytes && jsonString && preConverted) { console.log( `[fixture] gz=${(FIXTURE_GZ.byteLength / 1024 / 1024).toFixed(1)}MB, ` + `decompressed=${(decompressedBytes.byteLength / 1024 / 1024).toFixed(1)}MB, ` + - `jsonString=${(jsonString.length / 1024 / 1024).toFixed(1)}MB, ` + `nodes=${nodeCount}` ); } @@ -60,22 +57,6 @@ if (FIXTURE_GZ && decompressedBytes && jsonString && preConverted) { // --------------------------------------------------------------------------- describe.skipIf(!HAS_FIXTURE)("fig2grida pipeline", () => { - bench( - "stage: gzip decompress", - () => { - gunzipSync(FIXTURE_GZ!); - }, - { iterations: 5, warmupIterations: 1 } - ); - - bench( - "stage: JSON.parse", - () => { - JSON.parse(jsonString!); - }, - { iterations: 5, warmupIterations: 1 } - ); - bench( "stage: restJsonToGridaDocument (convert + merge)", () => { @@ -92,14 +73,6 @@ describe.skipIf(!HAS_FIXTURE)("fig2grida pipeline", () => { { iterations: 3, warmupIterations: 1 } ); - bench( - "stage: io.archive.pack (level 6, default)", - () => { - io.archive.pack(preConverted!.document, preConverted!.assets); - }, - { iterations: 3, warmupIterations: 1 } - ); - bench( "stage: io.archive.pack (level 0, store)", () => { @@ -116,10 +89,10 @@ describe.skipIf(!HAS_FIXTURE)("fig2grida pipeline", () => { }); // --------------------------------------------------------------------------- -// Benchmarks — io.archive.pack sub-stages +// Benchmarks — encode sub-stages // --------------------------------------------------------------------------- -describe.skipIf(!HAS_FIXTURE)("io.archive.pack sub-stages", () => { +describe.skipIf(!HAS_FIXTURE)("encode sub-stages", () => { const docForFb = { ...preConverted!.document, images: {}, @@ -127,60 +100,85 @@ describe.skipIf(!HAS_FIXTURE)("io.archive.pack sub-stages", () => { }; bench( - "sub: toFlatbuffer", + "toFlatbuffer (default)", () => { format.document.encode.toFlatbuffer(docForFb); }, { iterations: 3, warmupIterations: 1 } ); - const { - images: _images, - bitmaps: _bitmaps, - ...persistedDocument - } = preConverted!.document; - const snapshotPayload = { - version: "1.0", - document: persistedDocument, - }; - bench( - "sub: JSON.stringify (snapshot)", + "toFlatbuffer (skipSort)", () => { - JSON.stringify(snapshotPayload); + format.document.encode.toFlatbuffer(docForFb, undefined, { + skipSort: true, + }); }, - { iterations: 5, warmupIterations: 1 } + { iterations: 3, warmupIterations: 1 } ); const fbBytes = format.document.encode.toFlatbuffer(docForFb); - const snapshotJson = JSON.stringify(snapshotPayload); - - const files: Record = { - "manifest.json": strToU8( - JSON.stringify({ document_file: "document.grida", version: "1.0" }) - ), - "document.grida": fbBytes, - "document.grida1": strToU8(snapshotJson), - }; bench( - "sub: zipSync (level 6)", + "zipSync (level 0)", () => { - zipSync(files); + zipSync( + { + "manifest.json": strToU8( + JSON.stringify({ + document_file: "document.grida", + version: "1.0", + }) + ), + "document.grida": fbBytes, + }, + { level: 0 } + ); }, { iterations: 5, warmupIterations: 1 } ); bench( - "sub: zipSync (level 0)", + "zipSync (level 6)", () => { - zipSync(files, { level: 0 }); + zipSync( + { + "manifest.json": strToU8( + JSON.stringify({ + document_file: "document.grida", + version: "1.0", + }) + ), + "document.grida": fbBytes, + }, + { level: 6 } + ); }, - { iterations: 5, warmupIterations: 1 } + { iterations: 3, warmupIterations: 1 } ); console.log( - `[pack sizes] flatbuffer=${(fbBytes.byteLength / 1024 / 1024).toFixed(1)}MB, ` + - `snapshot=${(snapshotJson.length / 1024 / 1024).toFixed(1)}MB` + `[encode sizes] flatbuffer=${(fbBytes.byteLength / 1024 / 1024).toFixed(1)}MB` + ); +}); + +// --------------------------------------------------------------------------- +// Benchmarks — decode (round-trip: encode once, then measure decode) +// --------------------------------------------------------------------------- + +describe.skipIf(!HAS_FIXTURE)("decode", () => { + const docForFb = { + ...preConverted!.document, + images: {}, + bitmaps: {}, + }; + const fbBytes = format.document.encode.toFlatbuffer(docForFb); + + bench( + "fromFlatbuffer", + () => { + format.document.decode.fromFlatbuffer(fbBytes); + }, + { iterations: 5, warmupIterations: 1 } ); }); diff --git a/packages/grida-canvas-io-figma/__tests__/iofigma.kiwi.fig.test.ts b/packages/grida-canvas-io-figma/__tests__/iofigma.kiwi.fig.test.ts index 790ed656a5..cbcab80028 100644 --- a/packages/grida-canvas-io-figma/__tests__/iofigma.kiwi.fig.test.ts +++ b/packages/grida-canvas-io-figma/__tests__/iofigma.kiwi.fig.test.ts @@ -297,4 +297,84 @@ describe("FigImporter", () => { expect(sortedPages.length).toBe(figFile.pages.length); }); }); + + describe("FigDeck (.deck) import", () => { + const deckFixture = + __dirname + "/../../../fixtures/test-fig/deck/light.deck"; + + it("should parse deck file and produce 42 slides", () => { + const data = readFileSync(deckFixture); + const figFile = FigImporter.parseFile(data); + + expect(figFile.pages.length).toBe(1); + + // Count X_SLIDE nodes in the converted tree + function countByType(node: any, type: string): number { + let count = node.type === type ? 1 : 0; + if ("children" in node && Array.isArray(node.children)) { + node.children.forEach((child: any) => { + count += countByType(child, type); + }); + } + return count; + } + + const page = figFile.pages[0]; + let slideCount = 0; + page.rootNodes.forEach((root: any) => { + slideCount += countByType(root, "X_SLIDE"); + }); + + expect(slideCount).toBe(42); + }); + + it("should preserve complete slide hierarchy (no dropped subtrees)", () => { + const data = readFileSync(deckFixture); + const figData = readFigFile(data); + const nodeChanges = figData.message.nodeChanges || []; + + const canvas = nodeChanges.find( + (nc) => nc.type === "CANVAS" && !nc.internalOnly + ); + if (!canvas?.guid) return; + + const canvasGuidStr = iofigma.kiwi.guid(canvas.guid); + const rawCount = countKiwiDescendants(canvasGuidStr, nodeChanges); + + // Count nodes with types that are intentionally unsupported (e.g. SHAPE_WITH_TEXT) + const unsupportedTypes = new Set(["SHAPE_WITH_TEXT"]); + const unsupportedCount = nodeChanges.filter( + (nc) => nc.type && unsupportedTypes.has(nc.type) + ).length; + + const figFile = FigImporter.parseFile(data); + const page = figFile.pages[0]; + + let processedCount = 0; + page.rootNodes.forEach((rootNode: any) => { + processedCount += countNodes(rootNode); + }); + + // All supported nodes must be preserved — only unsupported FigJam-crossover types may be absent + expect(processedCount).toBe(rawCount - unsupportedCount); + }); + + it("should convert deck to Grida document without errors", () => { + const data = readFileSync(deckFixture); + const figFile = FigImporter.parseFile(data); + const { document: packedDoc } = FigImporter.convertPageToScene( + figFile.pages[0], + { gradient_id_generator: () => "test-id" } + ); + + expect(packedDoc.nodes).toBeDefined(); + expect(packedDoc.scene).toBeDefined(); + expect(packedDoc.scene.children_refs.length).toBeGreaterThan(0); + + // All root IDs must exist in nodes + packedDoc.scene.children_refs.forEach((rootId: string) => { + expect(packedDoc.nodes[rootId]).toBeDefined(); + }); + }); + }); }); diff --git a/packages/grida-canvas-io-figma/fig-kiwi/index.ts b/packages/grida-canvas-io-figma/fig-kiwi/index.ts index ec602cae54..44f79daa0c 100644 --- a/packages/grida-canvas-io-figma/fig-kiwi/index.ts +++ b/packages/grida-canvas-io-figma/fig-kiwi/index.ts @@ -54,10 +54,21 @@ export { compileSchema, prettyPrintSchema } from "kiwi-schema"; export type Header = { prelude: string; version: number }; +// First 8 bytes of a Kiwi .fig archive. Each product uses a fixed ASCII magic string; +// FigJam's is `fig-jam.` including the trailing dot (on-wire / on-disk format). const FIG_KIWI_PRELUDE = "fig-kiwi"; const FIGJAM_KIWI_PRELUDE = "fig-jam."; +const FIGDECK_KIWI_PRELUDE = "fig-deck"; const FIG_KIWI_VERSION = 15; +function isKiwiArchivePrelude(prelude: string): boolean { + return ( + prelude === FIG_KIWI_PRELUDE || + prelude === FIGJAM_KIWI_PRELUDE || + prelude === FIGDECK_KIWI_PRELUDE + ); +} + const HTML_MARKERS = { metaStart: "", @@ -128,7 +139,7 @@ export class FigmaArchiveParser { // @ts-ignore: charCode check const prelude = String.fromCharCode.apply(String, preludeData); - if (prelude !== FIG_KIWI_PRELUDE && prelude !== FIGJAM_KIWI_PRELUDE) { + if (!isKiwiArchivePrelude(prelude)) { throw new Error(`Unexpected prelude: "${prelude}"`); } @@ -273,8 +284,8 @@ export function readFigFile(data: Uint8Array): ParsedFigmaArchive { // Check prelude // @ts-ignore: charCode check const prelude = String.fromCharCode.apply(String, fileData.slice(0, 8)); - return prelude === FIG_KIWI_PRELUDE || prelude === FIGJAM_KIWI_PRELUDE; - }) || keys.find((k) => k.endsWith(".fig")); + return isKiwiArchivePrelude(prelude); + }) || keys.find((k) => k.endsWith(".fig") || k.endsWith(".deck")); if (!mainFile) { throw new Error( @@ -360,7 +371,9 @@ export async function readFigFileFromStream( } const total = buffer.reduce((s, c) => s + c.length, 0); if (total < 4) { - throw new Error("readFigFileFromStream: stream too short to detect format"); + throw new Error( + "readFigFileFromStream: stream too short to detect format" + ); } const merged = new Uint8Array(total); let off = 0; @@ -381,8 +394,8 @@ export async function readFigFileFromStream( String, Array.from(fileData.slice(0, 8)) ); - return prelude === FIG_KIWI_PRELUDE || prelude === FIGJAM_KIWI_PRELUDE; - }) || keys.find((k) => k.endsWith(".fig")); + return isKiwiArchivePrelude(prelude); + }) || keys.find((k) => k.endsWith(".fig") || k.endsWith(".deck")); if (!mainFile) { throw new Error( diff --git a/packages/grida-canvas-io-figma/fig2grida-core.ts b/packages/grida-canvas-io-figma/fig2grida-core.ts index cb7137a2bc..57f2060c64 100644 --- a/packages/grida-canvas-io-figma/fig2grida-core.ts +++ b/packages/grida-canvas-io-figma/fig2grida-core.ts @@ -109,106 +109,105 @@ function tryReadRestArchiveZip( // Shared merge logic // --------------------------------------------------------------------------- -interface PageResult { - name: string; - result: iofigma.restful.factory.FigmaImportResult; -} - interface MergedDocument { document: grida.program.document.Document; imageRecord: Record; - imageRefsUsed: string[]; + imageRefsUsed: Set; pageNames: string[]; + /** Non-scene node count, accumulated during conversion. */ + nodeCount: number; } /** - * Merge per-page conversion results into a single multi-scene Document and - * collect referenced image bytes. + * Build a multi-scene Document by converting canvases directly into shared + * buffers, avoiding per-root / per-page Object.assign merge passes. */ -function mergePages( - pageResults: PageResult[], +function buildMergedDocument( + canvases: Array<{ + name: string; + children: Array>; + backgroundColor?: { r: number; g: number; b: number; a: number }; + }>, + context: iofigma.restful.factory.FactoryContext, imageProvider: (ref: string) => Uint8Array | undefined ): MergedDocument { - const allImageRefsUsed = new Set(); - const mergedNodes: Record = {}; - const mergedLinks: Record = {}; - const mergedImages: Record = {}; - const mergedBitmaps: Record = {}; - const mergedProperties: Record = {}; + // Single shared buffers — factory.document() writes directly here. + const sharedNodes: Record = {}; + const sharedLinks: Record = {}; + const sharedImageRefsUsed = new Set(); + const sharedFigmaIdMap = new Map(); + const scenesRef: string[] = []; + const pageNames: string[] = []; + let nodeCount = 0; + + const sharedContext: iofigma.restful.factory.FactoryContext = { + ...context, + _shared_nodes: sharedNodes, + _shared_links: sharedLinks, + _shared_image_refs_used: sharedImageRefsUsed, + _shared_figma_id_map: sharedFigmaIdMap, + }; - for (const { name, result } of pageResults) { - const packed = result.document; + for (const canvas of canvases) { + const background_color = canvas.backgroundColor + ? kolor.colorformats.newRGBA32F( + canvas.backgroundColor.r, + canvas.backgroundColor.g, + canvas.backgroundColor.b, + canvas.backgroundColor.a + ) + : undefined; + + const sceneId = makeIdGenerator("scene")(); + const childrenRefs: string[] = []; + + // Count nodes before this page so we can compute delta + const nodesBefore = Object.keys(sharedNodes).length; + + for (const rootNode of canvas.children) { + const result = iofigma.restful.factory.document( + rootNode as any, + {}, + sharedContext + ); + // factory.document() already wrote nodes/links/imageRefs into shared + // buffers. We only need the scene's children_refs from the result. + childrenRefs.push(...result.document.scene.children_refs); + } - const sceneId = - packed.scene.id === "tmp" ? makeIdGenerator("scene")() : packed.scene.id; + nodeCount += Object.keys(sharedNodes).length - nodesBefore; const sceneNode: grida.program.nodes.SceneNode = { type: "scene", id: sceneId, - name: name, + name: canvas.name, active: true, locked: false, - constraints: packed.scene.constraints, - guides: packed.scene.guides, - edges: packed.scene.edges, - background_color: packed.scene.background_color, + constraints: { children: "multiple" }, + guides: [], + edges: [], + background_color, }; - mergedNodes[sceneId] = sceneNode; - mergedLinks[sceneId] = packed.scene.children_refs; + (sharedNodes as any)[sceneId] = sceneNode; + sharedLinks[sceneId] = childrenRefs; scenesRef.push(sceneId); - - Object.assign(mergedNodes, packed.nodes); - for (const [key, value] of Object.entries(packed.links)) { - if (key !== sceneId) { - mergedLinks[key] = value; - } - } - Object.assign(mergedImages, packed.images); - Object.assign(mergedBitmaps, packed.bitmaps); - Object.assign(mergedProperties, packed.properties); - - for (const ref of result.imageRefsUsed) { - allImageRefsUsed.add(ref); - } - } - - // Prune orphan nodes - const reachable = new Set(scenesRef); - const queue = [...scenesRef]; - while (queue.length > 0) { - const id = queue.pop()!; - const children = mergedLinks[id]; - if (children) { - for (const childId of children) { - if (!reachable.has(childId)) { - reachable.add(childId); - queue.push(childId); - } - } - } - } - for (const id of Object.keys(mergedNodes)) { - if (!reachable.has(id)) { - delete mergedNodes[id]; - delete mergedLinks[id]; - } + pageNames.push(canvas.name); } const document: grida.program.document.Document = { - nodes: mergedNodes, - links: mergedLinks, - images: mergedImages, - bitmaps: mergedBitmaps, - properties: mergedProperties, + nodes: sharedNodes, + links: sharedLinks, + images: {}, + bitmaps: {}, + properties: {}, scenes_ref: scenesRef, entry_scene_id: scenesRef[0], }; - const imageRefsUsed = Array.from(allImageRefsUsed); const imageRecord: Record = {}; - for (const ref of imageRefsUsed) { + for (const ref of sharedImageRefsUsed) { const imageBytes = imageProvider(ref); if (imageBytes) { imageRecord[ref] = imageBytes; @@ -218,8 +217,9 @@ function mergePages( return { document, imageRecord, - imageRefsUsed, - pageNames: pageResults.map((p) => p.name), + imageRefsUsed: sharedImageRefsUsed, + pageNames, + nodeCount, }; } @@ -229,16 +229,13 @@ function packMergedDocument(merged: MergedDocument): Fig2GridaResult { merged.imageRecord, undefined, undefined, - { level: 0 } + { level: 0, snapshot: false, skip_sort: true } ); - const nodeCount = Object.keys(merged.document.nodes).filter( - (id) => merged.document.nodes[id]?.type !== "scene" - ).length; return { bytes: archiveBytes, pageNames: merged.pageNames, - nodeCount, + nodeCount: merged.nodeCount, imageCount: Object.keys(merged.imageRecord).length, }; } @@ -310,6 +307,88 @@ export function fig2grida( // .fig bytes path // --------------------------------------------------------------------------- +interface FigPageResult { + name: string; + result: iofigma.restful.factory.FigmaImportResult; +} + +/** + * Merge per-page results from the .fig/Kiwi path into a single Document. + * (The REST path uses buildMergedDocument with shared buffers instead.) + */ +function mergeFigPages( + pageResults: FigPageResult[], + imageProvider: (ref: string) => Uint8Array | undefined +): MergedDocument { + const allImageRefsUsed = new Set(); + const mergedNodes: Record = {}; + const mergedLinks: Record = {}; + const scenesRef: string[] = []; + let nodeCount = 0; + + for (const { name, result } of pageResults) { + const packed = result.document; + + const sceneId = + packed.scene.id === "tmp" ? makeIdGenerator("scene")() : packed.scene.id; + + const sceneNode: grida.program.nodes.SceneNode = { + type: "scene", + id: sceneId, + name: name, + active: true, + locked: false, + constraints: packed.scene.constraints, + guides: packed.scene.guides, + edges: packed.scene.edges, + background_color: packed.scene.background_color, + }; + + mergedNodes[sceneId] = sceneNode; + mergedLinks[sceneId] = packed.scene.children_refs; + scenesRef.push(sceneId); + + const pageSizeBefore = Object.keys(mergedNodes).length; + Object.assign(mergedNodes, packed.nodes); + for (const [key, value] of Object.entries(packed.links)) { + if (key !== sceneId) { + mergedLinks[key] = value; + } + } + nodeCount += Object.keys(mergedNodes).length - pageSizeBefore; + + for (const ref of result.imageRefsUsed) { + allImageRefsUsed.add(ref); + } + } + + const document: grida.program.document.Document = { + nodes: mergedNodes, + links: mergedLinks, + images: {}, + bitmaps: {}, + properties: {}, + scenes_ref: scenesRef, + entry_scene_id: scenesRef[0], + }; + + const imageRecord: Record = {}; + for (const ref of allImageRefsUsed) { + const imageBytes = imageProvider(ref); + if (imageBytes) { + imageRecord[ref] = imageBytes; + } + } + + return { + document, + imageRecord, + imageRefsUsed: allImageRefsUsed, + pageNames: pageResults.map((p) => p.name), + nodeCount, + }; +} + function fig2gridaFromFigBytes( input: Uint8Array, options?: Fig2GridaOptions @@ -327,7 +406,7 @@ function fig2gridaFromFigBytes( .map((i) => pages[i]); } - const pageResults: PageResult[] = []; + const pageResults: FigPageResult[] = []; for (const page of pages) { const placeholderForMissing = options?.placeholder_for_missing_images !== false; @@ -344,7 +423,7 @@ function fig2gridaFromFigBytes( } return packMergedDocument( - mergePages(pageResults, (ref) => extractedImages.get(ref)) + mergeFigPages(pageResults, (ref) => extractedImages.get(ref)) ); } @@ -452,7 +531,7 @@ function restJsonToMergedDocument( preserveFigmaIds?: boolean, preferFixedTextSizing?: boolean ): MergedDocument { - const canvases = extractCanvases(json); + const rawCanvases = extractCanvases(json); // A single shared context across all pages prevents ID collisions when // merging nodes from different canvases. @@ -472,17 +551,15 @@ function restJsonToMergedDocument( }), }; - const pageResults: PageResult[] = canvases.map((canvas) => ({ - name: canvas.name ?? "Page", - result: convertRootsToPackedScene( - canvas.name ?? "Page", - canvas.children ?? [], - canvas.backgroundColor, - context - ), + const canvases = rawCanvases.map((c) => ({ + name: c.name ?? "Page", + children: c.children ?? [], + backgroundColor: c.backgroundColor, })); - return mergePages(pageResults, (ref) => (images ? images[ref] : undefined)); + return buildMergedDocument(canvases, context, (ref) => + images ? images[ref] : undefined + ); } function fig2gridaFromRestJson( @@ -503,91 +580,6 @@ function fig2gridaFromRestJson( ); } -// --------------------------------------------------------------------------- -// Shared per-page conversion: root nodes → FigmaImportResult -// --------------------------------------------------------------------------- - -/** - * Convert an array of root nodes into a single packed scene document. - * Used by both the REST JSON path and the .fig Kiwi path (via - * `iofigma.kiwi.convertPageToScene`). - */ -function convertRootsToPackedScene( - name: string, - rootNodes: Array>, - backgroundColor: { r: number; g: number; b: number; a: number } | undefined, - context: iofigma.restful.factory.FactoryContext -): iofigma.restful.factory.FigmaImportResult { - const background_color = backgroundColor - ? kolor.colorformats.newRGBA32F( - backgroundColor.r, - backgroundColor.g, - backgroundColor.b, - backgroundColor.a - ) - : undefined; - - if (rootNodes.length === 0) { - return { - document: emptyPackedScene(name, background_color), - imageRefsUsed: [], - }; - } - - const individualResults = rootNodes.map((rootNode) => - iofigma.restful.factory.document(rootNode as any, {}, context) - ); - - const imageRefsUsed = new Set(); - for (const r of individualResults) { - for (const ref of r.imageRefsUsed) imageRefsUsed.add(ref); - } - - let packed: grida.program.document.IPackedSceneDocument; - if (individualResults.length === 1) { - packed = individualResults[0].document; - packed.scene.background_color = background_color; - } else { - packed = emptyPackedScene(name, background_color); - for (const { document: d } of individualResults) { - Object.assign(packed.nodes, d.nodes); - Object.assign(packed.links, d.links); - Object.assign(packed.images, d.images); - Object.assign(packed.bitmaps, d.bitmaps); - Object.assign(packed.properties, d.properties); - packed.scene.children_refs.push(...d.scene.children_refs); - } - } - - return { - document: packed, - imageRefsUsed: Array.from(imageRefsUsed), - }; -} - -function emptyPackedScene( - name: string, - background_color: ReturnType | undefined -): grida.program.document.IPackedSceneDocument { - return { - nodes: {}, - links: {}, - images: {}, - bitmaps: {}, - properties: {}, - scene: { - type: "scene", - id: "tmp", - name, - children_refs: [], - guides: [], - edges: [], - constraints: { children: "multiple" }, - background_color, - }, - }; -} - // --------------------------------------------------------------------------- // restJsonToGridaDocument — returns in-memory Document (no .grida packing) // --------------------------------------------------------------------------- @@ -635,6 +627,6 @@ export function restJsonToGridaDocument( return { document: merged.document, assets: merged.imageRecord, - imageRefsUsed: merged.imageRefsUsed, + imageRefsUsed: Array.from(merged.imageRefsUsed), }; } diff --git a/packages/grida-canvas-io-figma/fig2grida.ts b/packages/grida-canvas-io-figma/fig2grida.ts index 5d08f50197..0a639ef041 100644 --- a/packages/grida-canvas-io-figma/fig2grida.ts +++ b/packages/grida-canvas-io-figma/fig2grida.ts @@ -6,6 +6,7 @@ * * Supported input formats: * .fig Figma native binary (Kiwi/ZIP) + * .deck Figma Deck/Slides binary (same format as .fig) * .json Figma REST API JSON response * .json.gz Gzip-compressed REST API JSON * .zip REST API archive ZIP (contains document.json + images) @@ -33,7 +34,7 @@ function printHelp(): void { console.log(` fig2grida — Convert Figma files to .grida archives -Supported inputs: .fig, .json, .json.gz, .zip +Supported inputs: .fig, .deck, .json, .json.gz, .zip Usage: fig2grida [output.grida] @@ -50,6 +51,7 @@ Options: Examples: fig2grida design.fig + fig2grida presentation.deck fig2grida api-response.json.gz output.grida fig2grida design.fig --pages 0,2 --verbose fig2grida design.fig --info @@ -175,14 +177,16 @@ function main(): void { const inputPath = resolve(args.input); const lower = inputPath.toLowerCase(); - // Reject .fig-only flags for REST-format inputs - if (!lower.endsWith(".fig")) { + const isFigLike = lower.endsWith(".fig") || lower.endsWith(".deck"); + + // Reject .fig/.deck-only flags for REST-format inputs + if (!isFigLike) { if (args.info) { - console.error("--info is only supported for .fig input."); + console.error("--info is only supported for .fig/.deck input."); process.exit(1); } if (args.pages) { - console.error("--pages is currently only supported for .fig input."); + console.error("--pages is currently only supported for .fig/.deck input."); process.exit(1); } } @@ -195,7 +199,7 @@ function main(): void { // Strip known extensions to derive the default output name const stripExt = (name: string): string => { - for (const ext of [".json.gz", ".json", ".fig", ".zip"]) { + for (const ext of [".json.gz", ".json", ".fig", ".deck", ".zip"]) { if (name.toLowerCase().endsWith(ext)) { return name.slice(0, -ext.length); } diff --git a/packages/grida-canvas-io-figma/lib.ts b/packages/grida-canvas-io-figma/lib.ts index 1fe363226c..014f338a41 100644 --- a/packages/grida-canvas-io-figma/lib.ts +++ b/packages/grida-canvas-io-figma/lib.ts @@ -195,6 +195,45 @@ export namespace iofigma { pointCount: number; innerRadius: number; }; + + /** + * Slide node from Figma Deck (.fig with fig-deck prelude). + * + * - rest-api-spec - Not supported (Figma REST API has no Deck/Slides types) + * - kiwi-spec - SLIDE, INTERACTIVE_SLIDE_ELEMENT + * + * Structurally equivalent to a FrameNode (children, fills, clips, layout). + * INTERACTIVE_SLIDE_ELEMENT is also mapped here since it behaves as a + * frame-like interactive element within a slide. + */ + export type SlideNodeIR = Omit & { + type: "X_SLIDE"; + slideMetadata?: { + speakerNotes?: string; + isSkipped?: boolean; + slideNumber?: string; + }; + }; + + /** + * Slide grid container from Figma Deck. + * + * - rest-api-spec - Not supported + * - kiwi-spec - SLIDE_GRID + */ + export type SlideGridNodeIR = Omit & { + type: "X_SLIDE_GRID"; + }; + + /** + * Slide row container from Figma Deck. + * + * - rest-api-spec - Not supported + * - kiwi-spec - SLIDE_ROW + */ + export type SlideRowNodeIR = Omit & { + type: "X_SLIDE_ROW"; + }; } export namespace restful { @@ -473,6 +512,30 @@ export namespace iofigma { * @default false */ prefer_fixed_text_sizing?: boolean; + + // -- Shared buffers (performance) -- + // When provided, factory.document() writes directly into these + // pre-allocated collections instead of creating its own, eliminating + // the Object.assign merge passes in the caller. + + /** + * Shared nodes dictionary. When set, `factory.document()` inserts + * converted nodes here instead of allocating a local `nodes` object. + */ + _shared_nodes?: Record; + /** + * Shared links (parent→children) dictionary. + */ + _shared_links?: Record; + /** + * Shared mutable set for collecting image refs used across all roots. + */ + _shared_image_refs_used?: Set; + /** + * Shared Figma-ID → Grida-ID map. Avoids per-root Map allocations + * when converting many root nodes. + */ + _shared_figma_id_map?: Map; }; function toGradientPaint(paint: figrest.GradientPaint) { @@ -1008,26 +1071,35 @@ export namespace iofigma { | figrest.BooleanOperationNode | figrest.InstanceNode | figrest.FrameNode - | figrest.GroupNode; + | figrest.GroupNode + | __ir.SlideNodeIR + | __ir.SlideGridNodeIR + | __ir.SlideRowNodeIR; type InputNode = | (figrest.SubcanvasNode & Partial<__ir.HasLayoutTraitIR>) | __ir.VectorNodeRestInput | __ir.VectorNodeWithVectorNetworkDataPresent | __ir.StarNodeWithPointsDataPresent - | __ir.RegularPolygonNodeWithPointsDataPresent; + | __ir.RegularPolygonNodeWithPointsDataPresent + | __ir.SlideNodeIR + | __ir.SlideGridNodeIR + | __ir.SlideRowNodeIR; export function document( node: InputNode, images: { [key: string]: string }, context: FactoryContext ): FigmaImportResult { - const nodes: Record = {}; - const graph: Record = {}; - const imageRefsUsed = new Set(); + const nodes: Record = + context._shared_nodes ?? {}; + const graph: Record = context._shared_links ?? {}; + const imageRefsUsed: Set = + context._shared_image_refs_used ?? new Set(); // Map from Figma ID (ephemeral) to Grida ID (final) - const figma_id_to_grida_id = new Map(); + const figma_id_to_grida_id = + context._shared_figma_id_map ?? new Map(); // ID generator function - use provided generator or fallback let counter = 0; @@ -1465,7 +1537,11 @@ export namespace iofigma { return { document: packed, - imageRefsUsed: Array.from(imageRefsUsed), + // When shared buffers are in use, the caller reads imageRefsUsed + // from the shared Set directly — return empty to avoid copying. + imageRefsUsed: context._shared_image_refs_used + ? [] + : Array.from(imageRefsUsed), }; } @@ -1518,7 +1594,11 @@ export namespace iofigma { case "FRAME": // Fallback: treat COMPONENT_SET as FRAME for rendering. Grida does not yet // support component semantics; proper variant/swap support to be added later. - case "COMPONENT_SET": { + case "COMPONENT_SET": + // Slide IR types (Figma Deck) — structurally identical to frames + case "X_SLIDE": + case "X_SLIDE_GRID": + case "X_SLIDE_ROW": { return { id: gridaId, ...base_node_trait(node), @@ -2601,6 +2681,75 @@ export namespace iofigma { } satisfies figrest.FrameNode; } + /** + * Convert Kiwi SLIDE / INTERACTIVE_SLIDE_ELEMENT to X_SLIDE IR. + * Reuses the same trait pipeline as frame(). + */ + function slide( + nc: figkiwi.NodeChange + ): __ir.SlideNodeIR | undefined { + if (!nc.guid || !nc.name || !nc.size) return undefined; + return { + ...kiwi_is_layer_trait(nc, "FRAME"), + ...kiwi_blend_opacity_trait(nc), + ...kiwi_layout_trait(nc), + ...kiwi_geometry_trait(nc), + ...kiwi_corner_trait(nc), + ...kiwi_frame_clip_trait(nc), + ...kiwi_children_trait(), + ...kiwi_effects_trait(nc), + ...kiwi_has_export_settings_trait(nc), + type: "X_SLIDE", + slideMetadata: { + speakerNotes: nc.slideSpeakerNotes ?? undefined, + isSkipped: nc.isSkippedSlide ?? undefined, + slideNumber: nc.slideNumber ?? undefined, + }, + } as __ir.SlideNodeIR; + } + + /** + * Convert Kiwi SLIDE_GRID to X_SLIDE_GRID IR. + */ + function slideGrid( + nc: figkiwi.NodeChange + ): __ir.SlideGridNodeIR | undefined { + if (!nc.guid || !nc.name || !nc.size) return undefined; + return { + ...kiwi_is_layer_trait(nc, "FRAME"), + ...kiwi_blend_opacity_trait(nc), + ...kiwi_layout_trait(nc), + ...kiwi_geometry_trait(nc), + ...kiwi_corner_trait(nc), + ...kiwi_frame_clip_trait(nc), + ...kiwi_children_trait(), + ...kiwi_effects_trait(nc), + ...kiwi_has_export_settings_trait(nc), + type: "X_SLIDE_GRID", + } as __ir.SlideGridNodeIR; + } + + /** + * Convert Kiwi SLIDE_ROW to X_SLIDE_ROW IR. + */ + function slideRow( + nc: figkiwi.NodeChange + ): __ir.SlideRowNodeIR | undefined { + if (!nc.guid || !nc.name || !nc.size) return undefined; + return { + ...kiwi_is_layer_trait(nc, "FRAME"), + ...kiwi_blend_opacity_trait(nc), + ...kiwi_layout_trait(nc), + ...kiwi_geometry_trait(nc), + ...kiwi_corner_trait(nc), + ...kiwi_frame_clip_trait(nc), + ...kiwi_children_trait(), + ...kiwi_effects_trait(nc), + ...kiwi_has_export_settings_trait(nc), + type: "X_SLIDE_ROW", + } as __ir.SlideRowNodeIR; + } + /** * Convert NodeChange to SECTION node */ @@ -2964,6 +3113,9 @@ export namespace iofigma { | __ir.VectorNodeWithVectorNetworkDataPresent | __ir.StarNodeWithPointsDataPresent | __ir.RegularPolygonNodeWithPointsDataPresent + | __ir.SlideNodeIR + | __ir.SlideGridNodeIR + | __ir.SlideRowNodeIR | undefined { if (!nodeChange.type) return undefined; @@ -2995,6 +3147,13 @@ export namespace iofigma { return star(nodeChange); case "BOOLEAN_OPERATION": return booleanOperation(nodeChange); + case "SLIDE": + case "INTERACTIVE_SLIDE_ELEMENT": + return slide(nodeChange); + case "SLIDE_GRID": + return slideGrid(nodeChange); + case "SLIDE_ROW": + return slideRow(nodeChange); default: return undefined; } diff --git a/packages/grida-canvas-io/format.ts b/packages/grida-canvas-io/format.ts index a4b0495cad..4695f3f21f 100644 --- a/packages/grida-canvas-io/format.ts +++ b/packages/grida-canvas-io/format.ts @@ -5,7 +5,7 @@ import * as fbs from "@grida/format"; import { unionToPaint, unionToNode, unionToFeBlur } from "@grida/format"; import type { vn } from "@grida/schema"; import * as flatbuffers from "flatbuffers"; -import { generateNKeysBetween } from "@grida/sequence"; +// generateNKeysBetween removed — replaced by zero-padded integers for position encoding. type Builder = flatbuffers.Builder; @@ -439,7 +439,9 @@ export namespace format { builder: Builder, id: string ): flatbuffers.Offset { - const idOffset = builder.createString(id); + const idOffset = id + ? builder.createSharedString(id) + : builder.createString(id); return fbs.NodeIdentifier.createNodeIdentifier(builder, idOffset); } @@ -452,7 +454,7 @@ export namespace format { position: string ): flatbuffers.Offset { const parentIdOffset = structs.nodeIdentifier(builder, parentId); - const positionOffset = builder.createString(position); + const positionOffset = builder.createSharedString(position); fbs.ParentReference.startParentReference(builder); fbs.ParentReference.addParentId(builder, parentIdOffset); fbs.ParentReference.addPosition(builder, positionOffset); @@ -935,7 +937,7 @@ export namespace format { node: grida.program.nodes.Node ): flatbuffers.Offset { const idOffset = structs.nodeIdentifier(builder, node.id); - const nameOffset = builder.createString(node.name ?? ""); + const nameOffset = builder.createSharedString(node.name ?? ""); fbs.SystemNodeTrait.startSystemNodeTrait(builder); fbs.SystemNodeTrait.addId(builder, idOffset); @@ -1062,35 +1064,36 @@ export namespace format { } // 7. Post-layout transform (rotation as transform matrix) - // Convert rotation (degrees) to a rotation transform matrix + // Only compute trig and emit the transform when rotation is non-zero. const nodeWithRotation = node as grida.program.nodes.Node & Partial>; const rotationDegrees = nodeWithRotation.rotation ?? 0; - const rotationRad = (rotationDegrees * Math.PI) / 180; - const cos = Math.cos(rotationRad); - const sin = Math.sin(rotationRad); + if (rotationDegrees !== 0) { + const rotationRad = (rotationDegrees * Math.PI) / 180; + const cos = Math.cos(rotationRad); + const sin = Math.sin(rotationRad); - // Pure rotation matrix: [cos, -sin, 0], [sin, cos, 0] - const postLayoutTransformOffset = structs.cgTransform2D( - builder, - cos, // m00 - -sin, // m01 - 0, // m02 - sin, // m10 - cos, // m11 - 0 // m12 - ); - fbs.LayerTrait.addPostLayoutTransform( - builder, - postLayoutTransformOffset - ); + const postLayoutTransformOffset = structs.cgTransform2D( + builder, + cos, + -sin, + 0, + sin, + cos, + 0 + ); + fbs.LayerTrait.addPostLayoutTransform( + builder, + postLayoutTransformOffset + ); - // 8. Post-layout transform origin (default to center: 0, 0 in Alignment coordinates) - const transformOriginOffset = structs.alignment(builder, 0, 0); - fbs.LayerTrait.addPostLayoutTransformOrigin( - builder, - transformOriginOffset - ); + // 8. Post-layout transform origin (only needed when rotation is set) + const transformOriginOffset = structs.alignment(builder, 0, 0); + fbs.LayerTrait.addPostLayoutTransformOrigin( + builder, + transformOriginOffset + ); + } return fbs.LayerTrait.endLayerTrait(builder); } @@ -1332,10 +1335,6 @@ export namespace format { fbs.BasicShapeNodeType.Rectangle; // Helper to create StrokeStyle - const dashArrayOffset = fbs.StrokeStyle.createStrokeDashArrayVector( - builder, - [] - ); fbs.StrokeStyle.startStrokeStyle(builder); fbs.StrokeStyle.addStrokeCap( builder, @@ -1347,7 +1346,7 @@ export namespace format { ); fbs.StrokeStyle.addStrokeAlign(builder, fbs.StrokeAlign.Inside); fbs.StrokeStyle.addStrokeMiterLimit(builder, 4.0); - fbs.StrokeStyle.addStrokeDashArray(builder, dashArrayOffset); + // Skip empty dash array vector — decoder reads null as no dashes const strokeStyleOffset = fbs.StrokeStyle.endStrokeStyle(builder); // Encode paints as PaintStackItem arrays @@ -1364,15 +1363,8 @@ export namespace format { fbs.BasicShapeNode.createStrokePaintsVector ); - // Create VariableWidthProfile (empty for now - nodes don't have this in TS model) - const emptyStopsOffset = fbs.VariableWidthProfile.createStopsVector( - builder, - [] - ); - fbs.VariableWidthProfile.startVariableWidthProfile(builder); - fbs.VariableWidthProfile.addStops(builder, emptyStopsOffset); - const strokeWidthProfileOffset = - fbs.VariableWidthProfile.endVariableWidthProfile(builder); + // Skip VariableWidthProfile — nodes don't have this in TS model. + // Decoder reads null when the field is absent. // Encode corner_radius and rectangular properties // For rectangle, use rectangular_corner_radius; for others, use corner_radius @@ -1409,10 +1401,7 @@ export namespace format { builder, shapeNode.stroke_width ?? 0 ); - fbs.BasicShapeNode.addStrokeWidthProfile( - builder, - strokeWidthProfileOffset - ); + // strokeWidthProfile omitted — not in TS model, decoder reads null. // Create structs inline (must be done while table is being built) const rectangularCornerRadiusOffsetInline = shapeNode.type === "rectangle" @@ -2182,7 +2171,7 @@ export namespace format { ) => flatbuffers.Offset ): flatbuffers.Offset { if (!paints || paints.length === 0) { - return createVector(builder, []); + return 0; // No vector — decoder reads null/empty } const stackItemOffsets: flatbuffers.Offset[] = []; @@ -4516,52 +4505,67 @@ export namespace format { * @param document - The TS IR document to encode * @returns Uint8Array containing the FlatBuffers binary data */ + export interface ToFlatbufferOptions { + /** + * When true, skip sorting node IDs for deterministic output. + * Safe for write-once pipelines (e.g. fig2grida) where the + * consumer does not depend on node order in the FlatBuffer vector. + * @default false + */ + skipSort?: boolean; + } + export function toFlatbuffer( document: grida.program.document.Document, - schemaVersion: string = grida.program.document.SCHEMA_VERSION + schemaVersion: string = grida.program.document.SCHEMA_VERSION, + options?: ToFlatbufferOptions ): Uint8Array { - const builder = new flatbuffers.Builder(1024); + // Collect node IDs once — reused for pre-sizing and iteration. + const nodeIds = Object.keys(document.nodes || {}); + const nodeCount = nodeIds.length; + + // Pre-size the builder to reduce doubling + copy during growth. + const initialSize = Math.max(nodeCount * 256, 256 * 1024); + const builder = new flatbuffers.Builder(initialSize); // Build schema version const schemaVersionOffset = builder.createString(schemaVersion); - // Build parent reference map: for each node, find its parent and generate position - // First, build a reverse map: childId -> parentId - const childToParentMap = new Map(); + // Build parent→children map from links for position generation. const parentToChildrenMap = new Map(); if (document.links) { for (const [parentId, children] of Object.entries(document.links)) { if (children && children.length > 0) { parentToChildrenMap.set(parentId, children); - for (const childId of children) { - childToParentMap.set(childId, parentId); - } } } } - // Generate position strings for each parent's children + // Generate position strings for each parent's children. + // We use zero-padded integers ("000000", "000001", …) which are + // trivially lexicographically sortable and nearly free to generate, + // replacing the costly fractional-index algorithm. const nodeToParentRef = new Map< string, { parentId: string; position: string } >(); for (const [parentId, children] of parentToChildrenMap.entries()) { if (children.length === 0) continue; - // Generate position strings for all children - const positions = generateNKeysBetween(null, null, children.length); + const pad = Math.max(6, String(children.length - 1).length); for (let i = 0; i < children.length; i++) { nodeToParentRef.set(children[i]!, { parentId, - position: positions[i]!, + position: String(i).padStart(pad, "0"), }); } } // Encode nodes array (TS nodes map -> flat list) - const nodeIds = Object.keys(document.nodes || {}); - // Deterministic ordering: sort by string id - nodeIds.sort(); + // Deterministic ordering: sort by string id (skippable for perf) + if (!options?.skipSort) { + nodeIds.sort(); + } const nodeSlotOffsets: flatbuffers.Offset[] = []; for (const nodeId of nodeIds) { diff --git a/packages/grida-canvas-io/index.ts b/packages/grida-canvas-io/index.ts index a38545446e..8435e077df 100644 --- a/packages/grida-canvas-io/index.ts +++ b/packages/grida-canvas-io/index.ts @@ -939,6 +939,20 @@ export namespace io { * immediately consumed (e.g. fig2grida → io.load round-trip). */ level?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9; + /** + * When false, skip generating the `document.grida1` JSON snapshot. + * The snapshot is a migration/fallback format; pipelines that only + * need the FlatBuffers document (e.g. fig2grida) can disable it + * to avoid a costly `JSON.stringify` over the entire node tree. + * @default true + */ + snapshot?: boolean; + /** + * When true, skip deterministic sorting of node IDs in the FlatBuffer. + * Safe for write-once pipelines where node order is irrelevant. + * @default false + */ + skip_sort?: boolean; } export function pack( @@ -959,19 +973,24 @@ export namespace io { images: {}, bitmaps: {}, }, - schemaVersion + schemaVersion, + { skipSort: options?.skip_sort } ); // Generate document.grida1 (JSON snapshot) from document (for migration purposes) - const { - images: _images, - bitmaps: _bitmaps, - ...persistedDocument - } = document; - const snapshotJson = io.snapshot.stringify({ - version: schemaVersion, - document: persistedDocument, - }); + const includeSnapshot = options?.snapshot !== false; + let snapshotJson: string | undefined; + if (includeSnapshot) { + const { + images: _images, + bitmaps: _bitmaps, + ...persistedDocument + } = document; + snapshotJson = io.snapshot.stringify({ + version: schemaVersion, + document: persistedDocument, + }); + } const manifest: Manifest = { document_file: "document.grida", @@ -995,7 +1014,9 @@ export namespace io { const files: Record = { "manifest.json": strToU8(JSON.stringify(manifest)), "document.grida": fbBytes, - "document.grida1": strToU8(snapshotJson), + ...(snapshotJson && { + "document.grida1": strToU8(snapshotJson), + }), ...(images && Object.keys(images).length > 0 && { "images/": new Uint8Array() }), // Ensure folder exists }; diff --git a/packages/grida-canvas-sdk-render-figma/cli.ts b/packages/grida-canvas-sdk-render-figma/cli.ts index f27f6b91ed..a984a93e37 100644 --- a/packages/grida-canvas-sdk-render-figma/cli.ts +++ b/packages/grida-canvas-sdk-render-figma/cli.ts @@ -207,7 +207,9 @@ async function runExportAll( fontsDir?: string, skipDefaultFonts?: boolean ): Promise { - const isFig = documentPath.toLowerCase().endsWith(".fig"); + const isFig = + documentPath.toLowerCase().endsWith(".fig") || + documentPath.toLowerCase().endsWith(".deck"); let document: FigmaDocument; let items: ExportItem[]; let rendererOptions: { @@ -319,7 +321,9 @@ async function runSingleNode( JSON.parse(readFileSync(documentPath, "utf8")) ); } else { - const isFig = documentPath.toLowerCase().endsWith(".fig"); + const isFig = + documentPath.toLowerCase().endsWith(".fig") || + documentPath.toLowerCase().endsWith(".deck"); const useStreaming = isFig && statSync(documentPath).size >= LARGE_FILE_THRESHOLD; if (useStreaming) {