diff --git a/.agents/skills/cg-perf/SKILL.md b/.agents/skills/cg-perf/SKILL.md index 373426237e..a9f15822c3 100644 --- a/.agents/skills/cg-perf/SKILL.md +++ b/.agents/skills/cg-perf/SKILL.md @@ -33,34 +33,64 @@ Before touching any code, build context by reading these sources in order: read whichever documents match the problem at hand. 4. **Read existing benchmarks** — look in `crates/grida-canvas/benches/` to understand what is already measured and how. -5. **Read `crates/grida-dev/src/main.rs`** — the `bench` subcommand shows - how GPU benchmarks work with real `.grida` scene files. +5. **Read `crates/grida-dev/src/main.rs`** — the `bench` and `bench-report` + subcommands show how GPU benchmarks work with real `.grida` scene files. + `bench-report` is the bulk mode that outputs JSON across all fixtures. Use `grep` and `glob` to discover the current state of code rather than relying on hardcoded paths. File locations shift as the engine evolves. ### Key discovery queries -| What you need | How to find it | -|---|---| -| The renderer entry point | `grep "struct Renderer" --include="*.rs"` in `crates/grida-canvas/src/` | -| Camera change classification | `grep "enum CameraChangeKind" --include="*.rs"` | -| How compositor cache works | `grep "struct LayerImage" --include="*.rs"` and read the containing module | -| Promotion heuristics | `grep "fn should_promote" --include="*.rs"` | -| Where zoom invalidation happens | `grep "zoom_changed\|mark_all_stale\|invalidate_all" --include="*.rs"` in `src/runtime/` | -| Frame pipeline flow | Search for `fn queue\|fn flush\|fn draw\|fn frame` in the renderer file | -| Benchmark fixture scenes | `--list-scenes` flag on `grida-dev bench` | +| What you need | How to find it | +| ----------------------------------------- | --------------------------------------------------------------------------------------------------- | +| The renderer entry point | `grep "struct Renderer" --include="*.rs"` in `crates/grida-canvas/src/` | +| Camera change classification | `grep "enum CameraChangeKind" --include="*.rs"` | +| How compositor cache works | `grep "struct LayerImage" --include="*.rs"` and read the containing module | +| Promotion heuristics | `grep "fn should_promote" --include="*.rs"` | +| Where zoom invalidation happens | `grep "zoom_changed\|mark_all_stale\|invalidate_all" --include="*.rs"` in `src/runtime/` | +| Frame pipeline flow | Search for `fn queue\|fn flush\|fn draw\|fn frame` in the renderer file | +| Benchmark fixture scenes | `--list-scenes` flag on `grida-dev bench`, or run `bench-report` on a directory | | Config toggles (compositing, atlas, etc.) | `grep "set_layer_compositing\|set_compositor_atlas\|set_interaction_render_scale" --include="*.rs"` | -| Existing `.plan.md` proposals | `glob "docs/wg/feat-2d/*.plan.md"` | +| Existing `.plan.md` proposals | `glob "docs/wg/feat-2d/*.plan.md"` | --- -## The Two Benchmark Systems +## The Benchmark Systems -There are two complementary benchmarks. **Always use both** — they -measure different things and a change that helps one can hurt the other. +There are three complementary benchmarks. The bulk report is the +recommended starting point; the single-scene bench and Criterion +provide deeper investigation when needed. -### 1. GPU benchmark (`grida-dev bench`) +### 1. Bulk benchmark report (`grida-dev bench-report`) + +Runs all scenes in all `.grida` files and outputs a compact **JSON +report**. Use this to establish baselines, detect regressions across +the full fixture set, and identify which scenes/stages are slowest. + +```sh +# All fixtures — recommended first step +cargo run -p grida-dev --release -- bench-report ./fixtures/ --frames 100 --output baseline.json + +# Single file +cargo run -p grida-dev --release -- bench-report ./fixtures/test-grida/bench.grida --frames 200 + +# Local fixtures for broader coverage +cargo run -p grida-dev --release -- bench-report ./fixtures/local/ --frames 100 --output baseline-local.json +``` + +The JSON report contains per-scene results with: + +- `nodes`, `effects_nodes` — scene complexity +- `pan.{avg_us, fps, p50_us, p95_us, p99_us}` — pan performance +- `pan.{draw_us, mid_flush_us, compositor_us, flush_us}` — per-stage breakdown +- `zoom.{avg_us, fps, p50_us, p95_us, p99_us}` — zoom performance +- `errors[]` — files that failed to load + +Progress goes to stderr, JSON to stdout (or `--output path`). This +keeps the JSON clean for programmatic consumption. + +### 2. Single-scene GPU benchmark (`grida-dev bench`) Runs real scene data on the actual GPU backend (Metal/GL). This is the ground truth for "does the user experience improve?" @@ -105,13 +135,15 @@ of scenes, configs, and operations. The naming convention is ### When to use which -| Question | Use | -|---|---| -| Is the optimization visible to users? | GPU bench | -| Is the algorithm itself faster? | Criterion | -| Is there a statistical regression? | Criterion (has CI) | -| What's the real frame time with GPU overhead? | GPU bench | -| Does a config toggle actually help? | Both (compare configs) | +| Question | Use | +| --------------------------------------------- | -------------------------------- | +| What's slow across all fixtures? | Bulk report (`bench-report`) | +| Baseline before/after a change? | Bulk report (save JSON, compare) | +| Detailed investigation of one scene? | Single-scene GPU bench | +| Is the algorithm itself faster? | Criterion | +| Is there a statistical regression? | Criterion (has CI) | +| What's the real frame time with GPU overhead? | Single-scene GPU bench | +| Does a config toggle actually help? | Both GPU benchmarks + Criterion | --- @@ -121,8 +153,14 @@ of scenes, configs, and operations. The naming convention is ### Step 1: Baseline -Run both benchmarks on the relevant scenes BEFORE any changes. Copy -the output somewhere — you'll compare against it. +Run the bulk benchmark report BEFORE any changes. Save the JSON output +so you can compare against it after the change. + +```sh +cargo run -p grida-dev --release -- bench-report ./fixtures/ --frames 100 --output baseline.json +``` + +For algorithmic changes, also run Criterion to get statistical baselines. ### Step 2: Implement @@ -140,18 +178,24 @@ Run the same benchmarks AFTER the change. Compare the numbers. ### Step 4: Regression check +Re-run the bulk benchmark report and compare against `baseline.json`. + +```sh +cargo run -p grida-dev --release -- bench-report ./fixtures/ --frames 100 --output after.json +``` + A zoom optimization must not regress pan. An effects optimization must -not regress non-effects scenes. Always run at least one scene from a -different category than the one you optimized. +not regress non-effects scenes. The bulk report covers all scenes +automatically — compare the full set, not just the target. ### Step 5: Accept or iterate -| Criterion | Required? | -|---|---| -| Target operation meets the fps goal | Yes | -| Non-target operations within 5% of baseline | Yes | -| All `cargo test -p cg` tests pass | Yes | -| No new clippy warnings from changed files | Yes | +| Criterion | Required? | +| ------------------------------------------- | --------- | +| Target operation meets the fps goal | Yes | +| Non-target operations within 5% of baseline | Yes | +| All `cargo test -p cg` tests pass | Yes | +| No new clippy warnings from changed files | Yes | --- @@ -263,6 +307,7 @@ section. Read it as a living catalog — items are added as new strategies are designed. The document is organized by category: + - Transform & Geometry (items 1-3) - Rendering Pipeline (items 4-14) - Pan-Only Optimization (items 15-20) @@ -313,7 +358,7 @@ Criterion runs on a CPU raster backend. It's excellent for measuring algorithmic cost and detecting regressions in pipeline logic. But it tells you nothing about GPU texture switching, GPU flush latency, or Metal/GL driver behavior. Don't conclude "compositing doesn't help" -from Criterion results — it doesn't help on *raster*, which is expected. +from Criterion results — it doesn't help on _raster_, which is expected. ### Timing overhead in budgeted loops diff --git a/.agents/skills/research/SKILL.md b/.agents/skills/research/SKILL.md index 45548f9cd9..31bdf7affd 100644 --- a/.agents/skills/research/SKILL.md +++ b/.agents/skills/research/SKILL.md @@ -79,6 +79,8 @@ Known citations: ## Reference Repositories +**Local clones (optional):** If `~/Documents/GitHub/` exists, it may contain default-style clones (sibling dirs named by repo, e.g. `skia`). Prefer searching there before cloning or using only the web. + ### Graphics & Rendering | Repo | Lang | When to reference | Key paths | diff --git a/AGENTS.md b/AGENTS.md index 7f47b95ce0..c8eec13dee 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -239,3 +239,18 @@ pnpm typecheck # run test (only packages and editor) pnpm turbo test --filter='./packages/*' --filter=editor ``` + +## Worktree + +This project supports git worktrees. When working in a fresh worktree, run the following setup: + +```sh +# 1. Initialize git submodules (e.g. emsdk for WASM builds) +git submodule update --init + +# 2. Install node dependencies +pnpm install +``` + +- **Cargo / Rust** works out of the box — the `target/` directory is resolved via relative paths and shared across worktrees. +- **Rustup targets** (e.g. `wasm32-unknown-emscripten`) are installed globally and do not need per-worktree setup. 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 881da00daf..a5f30da961 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["Mg"]();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,_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_json,_pointer_move,_redraw,_resize_surface,_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,_set_debug,_set_default_fallback_fonts,_set_main_camera_transform,_set_verbose,_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["Ng"];_add_font=Module["_add_font"]=wasmExports["Pg"];_add_image=Module["_add_image"]=wasmExports["Qg"];_add_image_with_rid=Module["_add_image_with_rid"]=wasmExports["Rg"];_allocate=Module["_allocate"]=wasmExports["Sg"];_apply_scene_transactions=Module["_apply_scene_transactions"]=wasmExports["Tg"];_command=Module["_command"]=wasmExports["Ug"];_deallocate=Module["_deallocate"]=wasmExports["Vg"];_destroy=Module["_destroy"]=wasmExports["Wg"];_devtools_rendering_set_show_fps_meter=Module["_devtools_rendering_set_show_fps_meter"]=wasmExports["Xg"];_devtools_rendering_set_show_hit_testing=Module["_devtools_rendering_set_show_hit_testing"]=wasmExports["Yg"];_devtools_rendering_set_show_ruler=Module["_devtools_rendering_set_show_ruler"]=wasmExports["Zg"];_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["$g"];_export_node_as=Module["_export_node_as"]=wasmExports["ah"];_get_default_fallback_fonts=Module["_get_default_fallback_fonts"]=wasmExports["bh"];_get_image_bytes=Module["_get_image_bytes"]=wasmExports["ch"];_get_image_size=Module["_get_image_size"]=wasmExports["dh"];_get_node_absolute_bounding_box=Module["_get_node_absolute_bounding_box"]=wasmExports["eh"];_get_node_id_from_point=Module["_get_node_id_from_point"]=wasmExports["fh"];_get_node_ids_from_envelope=Module["_get_node_ids_from_envelope"]=wasmExports["gh"];_get_node_ids_from_point=Module["_get_node_ids_from_point"]=wasmExports["hh"];_grida_fonts_analyze_family=Module["_grida_fonts_analyze_family"]=wasmExports["ih"];_grida_fonts_free=Module["_grida_fonts_free"]=wasmExports["jh"];_grida_fonts_parse_font=Module["_grida_fonts_parse_font"]=wasmExports["kh"];_grida_markdown_to_html=Module["_grida_markdown_to_html"]=wasmExports["lh"];_grida_svg_optimize=Module["_grida_svg_optimize"]=wasmExports["mh"];_grida_svg_to_document=Module["_grida_svg_to_document"]=wasmExports["nh"];_has_missing_fonts=Module["_has_missing_fonts"]=wasmExports["oh"];_highlight_strokes=Module["_highlight_strokes"]=wasmExports["ph"];_init=Module["_init"]=wasmExports["qh"];_init_with_backend=Module["_init_with_backend"]=wasmExports["rh"];_list_available_fonts=Module["_list_available_fonts"]=wasmExports["sh"];_list_missing_fonts=Module["_list_missing_fonts"]=wasmExports["th"];_load_benchmark_scene=Module["_load_benchmark_scene"]=wasmExports["uh"];_load_dummy_scene=Module["_load_dummy_scene"]=wasmExports["vh"];_load_scene_json=Module["_load_scene_json"]=wasmExports["wh"];_pointer_move=Module["_pointer_move"]=wasmExports["xh"];_redraw=Module["_redraw"]=wasmExports["yh"];_resize_surface=Module["_resize_surface"]=wasmExports["zh"];_runtime_renderer_set_layer_compositing=Module["_runtime_renderer_set_layer_compositing"]=wasmExports["Ah"];_runtime_renderer_set_outline_mode=Module["_runtime_renderer_set_outline_mode"]=wasmExports["Bh"];_runtime_renderer_set_pixel_preview_scale=Module["_runtime_renderer_set_pixel_preview_scale"]=wasmExports["Ch"];_runtime_renderer_set_pixel_preview_stable=Module["_runtime_renderer_set_pixel_preview_stable"]=wasmExports["Dh"];_runtime_renderer_set_render_policy_flags=Module["_runtime_renderer_set_render_policy_flags"]=wasmExports["Eh"];_set_debug=Module["_set_debug"]=wasmExports["Fh"];_set_default_fallback_fonts=Module["_set_default_fallback_fonts"]=wasmExports["Gh"];_set_main_camera_transform=Module["_set_main_camera_transform"]=wasmExports["Hh"];_set_verbose=Module["_set_verbose"]=wasmExports["Ih"];_text_edit_command=Module["_text_edit_command"]=wasmExports["Jh"];_text_edit_enter=Module["_text_edit_enter"]=wasmExports["Kh"];_text_edit_exit=Module["_text_edit_exit"]=wasmExports["Lh"];_text_edit_get_caret_rect=Module["_text_edit_get_caret_rect"]=wasmExports["Mh"];_text_edit_get_selected_html=Module["_text_edit_get_selected_html"]=wasmExports["Nh"];_text_edit_get_selected_text=Module["_text_edit_get_selected_text"]=wasmExports["Oh"];_text_edit_get_selection_rects=Module["_text_edit_get_selection_rects"]=wasmExports["Ph"];_text_edit_get_text=Module["_text_edit_get_text"]=wasmExports["Qh"];_text_edit_ime_cancel=Module["_text_edit_ime_cancel"]=wasmExports["Rh"];_text_edit_ime_commit=Module["_text_edit_ime_commit"]=wasmExports["Sh"];_text_edit_ime_set_preedit=Module["_text_edit_ime_set_preedit"]=wasmExports["Th"];_text_edit_is_active=Module["_text_edit_is_active"]=wasmExports["Uh"];_text_edit_paste_html=Module["_text_edit_paste_html"]=wasmExports["Vh"];_text_edit_paste_text=Module["_text_edit_paste_text"]=wasmExports["Wh"];_text_edit_pointer_down=Module["_text_edit_pointer_down"]=wasmExports["Xh"];_text_edit_pointer_move=Module["_text_edit_pointer_move"]=wasmExports["Yh"];_text_edit_pointer_up=Module["_text_edit_pointer_up"]=wasmExports["Zh"];_text_edit_redo=Module["_text_edit_redo"]=wasmExports["_h"];_text_edit_set_color=Module["_text_edit_set_color"]=wasmExports["$h"];_text_edit_set_font_family=Module["_text_edit_set_font_family"]=wasmExports["ai"];_text_edit_set_font_size=Module["_text_edit_set_font_size"]=wasmExports["bi"];_text_edit_tick=Module["_text_edit_tick"]=wasmExports["ci"];_text_edit_toggle_bold=Module["_text_edit_toggle_bold"]=wasmExports["di"];_text_edit_toggle_italic=Module["_text_edit_toggle_italic"]=wasmExports["ei"];_text_edit_toggle_strikethrough=Module["_text_edit_toggle_strikethrough"]=wasmExports["fi"];_text_edit_toggle_underline=Module["_text_edit_toggle_underline"]=wasmExports["gi"];_text_edit_undo=Module["_text_edit_undo"]=wasmExports["hi"];_tick=Module["_tick"]=wasmExports["ii"];_to_vector_network=Module["_to_vector_network"]=wasmExports["ji"];_toggle_debug=Module["_toggle_debug"]=wasmExports["ki"];_main=Module["_main"]=wasmExports["li"];_emscripten_builtin_memalign=wasmExports["mi"];_setThrew=wasmExports["ni"];__emscripten_tempret_set=wasmExports["oi"];__emscripten_stack_restore=wasmExports["pi"];__emscripten_stack_alloc=wasmExports["qi"];_emscripten_stack_get_current=wasmExports["ri"];___cxa_decrement_exception_refcount=wasmExports["si"];___cxa_increment_exception_refcount=wasmExports["ti"];___cxa_can_catch=wasmExports["ui"];___cxa_get_exception_ptr=wasmExports["vi"];memory=wasmMemory=wasmExports["Lg"];__indirect_function_table=wasmTable=wasmExports["Og"]}var wasmImports={G:___cxa_begin_catch,N:___cxa_end_catch,a:___cxa_find_matching_catch_2,n:___cxa_find_matching_catch_3,fa:___cxa_find_matching_catch_4,Aa:___cxa_rethrow,H:___cxa_throw,db:___cxa_uncaught_exceptions,e:___resumeException,Da:___syscall_fcntl64,vb:___syscall_fstat64,rb:___syscall_getcwd,wb:___syscall_ioctl,sb:___syscall_lstat64,tb:___syscall_newfstatat,Ea:___syscall_openat,ub:___syscall_stat64,zb:__abort_js,fb:__emscripten_throw_longjmp,mb:__gmtime_js,kb:__mmap_js,lb:__munmap_js,Ab:__tzset_js,yb:_clock_time_get,xb:_emscripten_date_now,hb:_emscripten_get_heap_max,Af:_emscripten_glActiveTexture,Bf:_emscripten_glAttachShader,de:_emscripten_glBeginQuery,Zd:_emscripten_glBeginQueryEXT,Fc:_emscripten_glBeginTransformFeedback,Cf:_emscripten_glBindAttribLocation,Df:_emscripten_glBindBuffer,Cc:_emscripten_glBindBufferBase,Dc:_emscripten_glBindBufferRange,Be:_emscripten_glBindFramebuffer,Ce:_emscripten_glBindRenderbuffer,je:_emscripten_glBindSampler,Ef:_emscripten_glBindTexture,Sb:_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,Pf:_emscripten_glCompressedTexImage2D,Rc:_emscripten_glCompressedTexImage3D,Qf:_emscripten_glCompressedTexSubImage2D,Qc:_emscripten_glCompressedTexSubImage3D,ue:_emscripten_glCopyBufferSubData,Gd:_emscripten_glCopyTexImage2D,Rf:_emscripten_glCopyTexSubImage2D,Sc:_emscripten_glCopyTexSubImage3D,Sf:_emscripten_glCreateProgram,Tf:_emscripten_glCreateShader,Uf:_emscripten_glCullFace,Vf:_emscripten_glDeleteBuffers,Ee:_emscripten_glDeleteFramebuffers,Wf:_emscripten_glDeleteProgram,ee:_emscripten_glDeleteQueries,_d:_emscripten_glDeleteQueriesEXT,Fe:_emscripten_glDeleteRenderbuffers,ke:_emscripten_glDeleteSamplers,Xf:_emscripten_glDeleteShader,te:_emscripten_glDeleteSync,Zf:_emscripten_glDeleteTextures,Rb:_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,Ec:_emscripten_glEndTransformFeedback,pe:_emscripten_glFenceSync,fg:_emscripten_glFinish,gg:_emscripten_glFlush,Ge:_emscripten_glFramebufferRenderbuffer,He:_emscripten_glFramebufferTexture2D,Ic:_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,Qb:_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,sc:_emscripten_glGetFragDataLocation,ye:_emscripten_glGetFramebufferAttachmentParameteriv,Yb:_emscripten_glGetInteger64i_v,_b:_emscripten_glGetInteger64v,Gc:_emscripten_glGetIntegeri_v,ng:_emscripten_glGetIntegerv,Ib:_emscripten_glGetInternalformativ,Mb:_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,Ac:_emscripten_glGetTransformFeedbackVarying,cc:_emscripten_glGetUniformBlockIndex,ec:_emscripten_glGetUniformIndices,tg:_emscripten_glGetUniformLocation,ud:_emscripten_glGetUniformfv,td:_emscripten_glGetUniformiv,tc:_emscripten_glGetUniformuiv,zc:_emscripten_glGetVertexAttribIiv,yc:_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,Pc:_emscripten_glIsQuery,Qd:_emscripten_glIsQueryEXT,kd:_emscripten_glIsRenderbuffer,Wb:_emscripten_glIsSampler,jd:_emscripten_glIsShader,qe:_emscripten_glIsSync,ug:_emscripten_glIsTexture,Pb:_emscripten_glIsTransformFeedback,Hc:_emscripten_glIsVertexArray,Od:_emscripten_glIsVertexArrayOES,vg:_emscripten_glLineWidth,wg:_emscripten_glLinkProgram,Oe:_emscripten_glMultiDrawArraysInstancedBaseInstanceWEBGL,Pe:_emscripten_glMultiDrawElementsInstancedBaseVertexBaseInstanceWEBGL,Ob:_emscripten_glPauseTransformFeedback,xg:_emscripten_glPixelStorei,Zc:_emscripten_glPolygonModeWEBGL,id:_emscripten_glPolygonOffset,$c:_emscripten_glPolygonOffsetClampEXT,Lb:_emscripten_glProgramBinary,Kb:_emscripten_glProgramParameteri,Yd:_emscripten_glQueryCounterEXT,Re:_emscripten_glReadBuffer,yg:_emscripten_glReadPixels,hd:_emscripten_glReleaseShaderCompiler,Ae:_emscripten_glRenderbufferStorage,we:_emscripten_glRenderbufferStorageMultisample,Nb:_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,Ka:_emscripten_glTexParameteriv,Le:_emscripten_glTexStorage2D,Jb:_emscripten_glTexStorage3D,La:_emscripten_glTexSubImage2D,Tc:_emscripten_glTexSubImage3D,Bc:_emscripten_glTransformFeedbackVaryings,Ma:_emscripten_glUniform1f,Na:_emscripten_glUniform1fv,wf:_emscripten_glUniform1i,xf:_emscripten_glUniform1iv,rc:_emscripten_glUniform1ui,nc:_emscripten_glUniform1uiv,yf:_emscripten_glUniform2f,zf:_emscripten_glUniform2fv,vf:_emscripten_glUniform2i,uf:_emscripten_glUniform2iv,qc:_emscripten_glUniform2ui,mc:_emscripten_glUniform2uiv,tf:_emscripten_glUniform3f,sf:_emscripten_glUniform3fv,rf:_emscripten_glUniform3i,qf:_emscripten_glUniform3iv,pc:_emscripten_glUniform3ui,lc:_emscripten_glUniform3uiv,pf:_emscripten_glUniform4f,of:_emscripten_glUniform4fv,bf:_emscripten_glUniform4i,cf:_emscripten_glUniform4iv,oc:_emscripten_glUniform4ui,jc:_emscripten_glUniform4uiv,$b:_emscripten_glUniformBlockBinding,df:_emscripten_glUniformMatrix2fv,Oc:_emscripten_glUniformMatrix2x3fv,Mc:_emscripten_glUniformMatrix2x4fv,ef:_emscripten_glUniformMatrix3fv,Nc:_emscripten_glUniformMatrix3x2fv,Kc:_emscripten_glUniformMatrix3x4fv,ff:_emscripten_glUniformMatrix4fv,Lc:_emscripten_glUniformMatrix4x2fv,Jc:_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,xc:_emscripten_glVertexAttribI4i,vc:_emscripten_glVertexAttribI4iv,wc:_emscripten_glVertexAttribI4ui,uc:_emscripten_glVertexAttribI4uiv,Ne:_emscripten_glVertexAttribIPointer,mf:_emscripten_glVertexAttribPointer,nf:_emscripten_glViewport,re:_emscripten_glWaitSync,Ya:_emscripten_request_animation_frame_loop,gb:_emscripten_resize_heap,ob:_environ_get,pb:_environ_sizes_get,Qa:_exit,la:_fd_close,jb:_fd_pread,Ca:_fd_read,nb:_fd_seek,ja:_fd_write,Oa:_glGetIntegerv,oa:_glGetString,Pa:_glGetStringi,Rd:invoke_dd,Sd:invoke_dddd,xa:invoke_diii,Ta:invoke_fdiiii,Sa:invoke_fdiiiii,Ra:invoke_fii,za:invoke_fiii,s:invoke_fiiidi,U:invoke_fiiif,t:invoke_fiiiidi,r:invoke_i,j:invoke_ii,D:invoke_iif,ib:invoke_iiffi,qa:invoke_iiffiii,g:invoke_iii,Ha:invoke_iiiffii,sa:invoke_iiifi,f:invoke_iiii,S:invoke_iiiiff,l:invoke_iiiii,cb:invoke_iiiiid,z:invoke_iiiiii,x:invoke_iiiiiii,E:invoke_iiiiiiii,q:invoke_iiiiiiiii,pa:invoke_iiiiiiiiii,ca:invoke_iiiiiiiiiiii,na:invoke_iiiiiiiiiiiifiii,ta:invoke_iiijj,eb:invoke_j,W:invoke_ji,Z:invoke_jiii,da:invoke_jiiii,J:invoke_jjji,k:invoke_v,Yf:invoke_vff,b:invoke_vi,P:invoke_vid,R:invoke_vif,u:invoke_viff,C:invoke_viffff,Y:invoke_vifffff,Ua:invoke_viffffff,B:invoke_viffi,ga:invoke_viffiiiiiii,c:invoke_vii,Xa:invoke_viidii,O:invoke_viif,F:invoke_viiff,_:invoke_viifi,ya:invoke_viififii,w:invoke_viifiiifi,d:invoke_viii,I:invoke_viiif,A:invoke_viiiffi,K:invoke_viiiffiffii,L:invoke_viiififiiiiiiiiiiii,i:invoke_viiii,Wa:invoke_viiiidididii,ka:invoke_viiiif,Fa:invoke_viiiiff,va:invoke_viiiiffi,Ba:invoke_viiiifi,h:invoke_viiiii,Hb:invoke_viiiiif,Ga:invoke_viiiiiff,Va:invoke_viiiiiffiii,ab:invoke_viiiiifi,m:invoke_viiiiii,p:invoke_viiiiiii,V:invoke_viiiiiiii,X:invoke_viiiiiiiii,M:invoke_viiiiiiiiii,ra:invoke_viiiiiiiiiii,ba:invoke_viiiiiiiiiiiiiii,Ja:invoke_viiiiiji,bb:invoke_viiiijjiiiiff,Q:invoke_viiij,y:invoke_viiijii,T:invoke_viij,o:invoke_viiji,ia:invoke_viijiffi,aa:invoke_viijii,$a:invoke_viijiii,$:invoke_viijiiiif,Ia:invoke_viijiiiii,ha:invoke_viji,Za:invoke_vijififi,v:invoke_vijii,wa:invoke_vijiifi,_a:invoke_vijiififi,ua:invoke_vijiii,ea:invoke_vijjjj,kc:invoke_vjii,ma:_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_iiijj(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_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_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_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["Lg"]();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,_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_json,_pointer_move,_redraw,_resize_surface,_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,_set_debug,_set_default_fallback_fonts,_set_main_camera_transform,_set_verbose,_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["Mg"];_add_font=Module["_add_font"]=wasmExports["Og"];_add_image=Module["_add_image"]=wasmExports["Pg"];_add_image_with_rid=Module["_add_image_with_rid"]=wasmExports["Qg"];_allocate=Module["_allocate"]=wasmExports["Rg"];_apply_scene_transactions=Module["_apply_scene_transactions"]=wasmExports["Sg"];_command=Module["_command"]=wasmExports["Tg"];_deallocate=Module["_deallocate"]=wasmExports["Ug"];_destroy=Module["_destroy"]=wasmExports["Vg"];_devtools_rendering_set_show_fps_meter=Module["_devtools_rendering_set_show_fps_meter"]=wasmExports["Wg"];_devtools_rendering_set_show_hit_testing=Module["_devtools_rendering_set_show_hit_testing"]=wasmExports["Xg"];_devtools_rendering_set_show_ruler=Module["_devtools_rendering_set_show_ruler"]=wasmExports["Yg"];_devtools_rendering_set_show_stats=Module["_devtools_rendering_set_show_stats"]=wasmExports["Zg"];_devtools_rendering_set_show_tiles=Module["_devtools_rendering_set_show_tiles"]=wasmExports["_g"];_export_node_as=Module["_export_node_as"]=wasmExports["$g"];_get_default_fallback_fonts=Module["_get_default_fallback_fonts"]=wasmExports["ah"];_get_image_bytes=Module["_get_image_bytes"]=wasmExports["bh"];_get_image_size=Module["_get_image_size"]=wasmExports["ch"];_get_node_absolute_bounding_box=Module["_get_node_absolute_bounding_box"]=wasmExports["dh"];_get_node_id_from_point=Module["_get_node_id_from_point"]=wasmExports["eh"];_get_node_ids_from_envelope=Module["_get_node_ids_from_envelope"]=wasmExports["fh"];_get_node_ids_from_point=Module["_get_node_ids_from_point"]=wasmExports["gh"];_grida_fonts_analyze_family=Module["_grida_fonts_analyze_family"]=wasmExports["hh"];_grida_fonts_free=Module["_grida_fonts_free"]=wasmExports["ih"];_grida_fonts_parse_font=Module["_grida_fonts_parse_font"]=wasmExports["jh"];_grida_markdown_to_html=Module["_grida_markdown_to_html"]=wasmExports["kh"];_grida_svg_optimize=Module["_grida_svg_optimize"]=wasmExports["lh"];_grida_svg_to_document=Module["_grida_svg_to_document"]=wasmExports["mh"];_has_missing_fonts=Module["_has_missing_fonts"]=wasmExports["nh"];_highlight_strokes=Module["_highlight_strokes"]=wasmExports["oh"];_init=Module["_init"]=wasmExports["ph"];_init_with_backend=Module["_init_with_backend"]=wasmExports["qh"];_list_available_fonts=Module["_list_available_fonts"]=wasmExports["rh"];_list_missing_fonts=Module["_list_missing_fonts"]=wasmExports["sh"];_load_benchmark_scene=Module["_load_benchmark_scene"]=wasmExports["th"];_load_dummy_scene=Module["_load_dummy_scene"]=wasmExports["uh"];_load_scene_json=Module["_load_scene_json"]=wasmExports["vh"];_pointer_move=Module["_pointer_move"]=wasmExports["wh"];_redraw=Module["_redraw"]=wasmExports["xh"];_resize_surface=Module["_resize_surface"]=wasmExports["yh"];_runtime_renderer_set_layer_compositing=Module["_runtime_renderer_set_layer_compositing"]=wasmExports["zh"];_runtime_renderer_set_outline_mode=Module["_runtime_renderer_set_outline_mode"]=wasmExports["Ah"];_runtime_renderer_set_pixel_preview_scale=Module["_runtime_renderer_set_pixel_preview_scale"]=wasmExports["Bh"];_runtime_renderer_set_pixel_preview_stable=Module["_runtime_renderer_set_pixel_preview_stable"]=wasmExports["Ch"];_runtime_renderer_set_render_policy_flags=Module["_runtime_renderer_set_render_policy_flags"]=wasmExports["Dh"];_set_debug=Module["_set_debug"]=wasmExports["Eh"];_set_default_fallback_fonts=Module["_set_default_fallback_fonts"]=wasmExports["Fh"];_set_main_camera_transform=Module["_set_main_camera_transform"]=wasmExports["Gh"];_set_verbose=Module["_set_verbose"]=wasmExports["Hh"];_text_edit_command=Module["_text_edit_command"]=wasmExports["Ih"];_text_edit_enter=Module["_text_edit_enter"]=wasmExports["Jh"];_text_edit_exit=Module["_text_edit_exit"]=wasmExports["Kh"];_text_edit_get_caret_rect=Module["_text_edit_get_caret_rect"]=wasmExports["Lh"];_text_edit_get_selected_html=Module["_text_edit_get_selected_html"]=wasmExports["Mh"];_text_edit_get_selected_text=Module["_text_edit_get_selected_text"]=wasmExports["Nh"];_text_edit_get_selection_rects=Module["_text_edit_get_selection_rects"]=wasmExports["Oh"];_text_edit_get_text=Module["_text_edit_get_text"]=wasmExports["Ph"];_text_edit_ime_cancel=Module["_text_edit_ime_cancel"]=wasmExports["Qh"];_text_edit_ime_commit=Module["_text_edit_ime_commit"]=wasmExports["Rh"];_text_edit_ime_set_preedit=Module["_text_edit_ime_set_preedit"]=wasmExports["Sh"];_text_edit_is_active=Module["_text_edit_is_active"]=wasmExports["Th"];_text_edit_paste_html=Module["_text_edit_paste_html"]=wasmExports["Uh"];_text_edit_paste_text=Module["_text_edit_paste_text"]=wasmExports["Vh"];_text_edit_pointer_down=Module["_text_edit_pointer_down"]=wasmExports["Wh"];_text_edit_pointer_move=Module["_text_edit_pointer_move"]=wasmExports["Xh"];_text_edit_pointer_up=Module["_text_edit_pointer_up"]=wasmExports["Yh"];_text_edit_redo=Module["_text_edit_redo"]=wasmExports["Zh"];_text_edit_set_color=Module["_text_edit_set_color"]=wasmExports["_h"];_text_edit_set_font_family=Module["_text_edit_set_font_family"]=wasmExports["$h"];_text_edit_set_font_size=Module["_text_edit_set_font_size"]=wasmExports["ai"];_text_edit_tick=Module["_text_edit_tick"]=wasmExports["bi"];_text_edit_toggle_bold=Module["_text_edit_toggle_bold"]=wasmExports["ci"];_text_edit_toggle_italic=Module["_text_edit_toggle_italic"]=wasmExports["di"];_text_edit_toggle_strikethrough=Module["_text_edit_toggle_strikethrough"]=wasmExports["ei"];_text_edit_toggle_underline=Module["_text_edit_toggle_underline"]=wasmExports["fi"];_text_edit_undo=Module["_text_edit_undo"]=wasmExports["gi"];_tick=Module["_tick"]=wasmExports["hi"];_to_vector_network=Module["_to_vector_network"]=wasmExports["ii"];_toggle_debug=Module["_toggle_debug"]=wasmExports["ji"];_main=Module["_main"]=wasmExports["ki"];_emscripten_builtin_memalign=wasmExports["li"];_setThrew=wasmExports["mi"];__emscripten_tempret_set=wasmExports["ni"];__emscripten_stack_restore=wasmExports["oi"];__emscripten_stack_alloc=wasmExports["pi"];_emscripten_stack_get_current=wasmExports["qi"];___cxa_decrement_exception_refcount=wasmExports["ri"];___cxa_increment_exception_refcount=wasmExports["si"];___cxa_can_catch=wasmExports["ti"];___cxa_get_exception_ptr=wasmExports["ui"];memory=wasmMemory=wasmExports["Kg"];__indirect_function_table=wasmTable=wasmExports["Ng"]}var wasmImports={G:___cxa_begin_catch,N:___cxa_end_catch,a:___cxa_find_matching_catch_2,n:___cxa_find_matching_catch_3,ea:___cxa_find_matching_catch_4,za:___cxa_rethrow,H:___cxa_throw,eb:___cxa_uncaught_exceptions,e:___resumeException,Ca:___syscall_fcntl64,vb:___syscall_fstat64,rb:___syscall_getcwd,wb:___syscall_ioctl,sb:___syscall_lstat64,tb:___syscall_newfstatat,Da:___syscall_openat,ub:___syscall_stat64,Ab:__abort_js,gb:__emscripten_throw_longjmp,mb:__gmtime_js,kb:__mmap_js,lb:__munmap_js,Bb:__tzset_js,zb:_clock_time_get,yb:_emscripten_date_now,ib:_emscripten_get_heap_max,Af:_emscripten_glActiveTexture,Bf:_emscripten_glAttachShader,de:_emscripten_glBeginQuery,Zd:_emscripten_glBeginQueryEXT,Fc:_emscripten_glBeginTransformFeedback,Cf:_emscripten_glBindAttribLocation,Df:_emscripten_glBindBuffer,Cc:_emscripten_glBindBufferBase,Dc:_emscripten_glBindBufferRange,Be:_emscripten_glBindFramebuffer,Ce:_emscripten_glBindRenderbuffer,je:_emscripten_glBindSampler,Ef:_emscripten_glBindTexture,Sb:_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,Pf:_emscripten_glCompressedTexImage2D,Rc:_emscripten_glCompressedTexImage3D,Qf:_emscripten_glCompressedTexSubImage2D,Qc:_emscripten_glCompressedTexSubImage3D,ue:_emscripten_glCopyBufferSubData,Gd:_emscripten_glCopyTexImage2D,Rf:_emscripten_glCopyTexSubImage2D,Sc:_emscripten_glCopyTexSubImage3D,Sf:_emscripten_glCreateProgram,Tf:_emscripten_glCreateShader,Uf:_emscripten_glCullFace,Vf:_emscripten_glDeleteBuffers,Ee:_emscripten_glDeleteFramebuffers,Wf:_emscripten_glDeleteProgram,ee:_emscripten_glDeleteQueries,_d:_emscripten_glDeleteQueriesEXT,Fe:_emscripten_glDeleteRenderbuffers,ke:_emscripten_glDeleteSamplers,Yf:_emscripten_glDeleteShader,te:_emscripten_glDeleteSync,Zf:_emscripten_glDeleteTextures,Rb:_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,Eb:_emscripten_glDrawArraysInstancedARB,Se:_emscripten_glDrawArraysInstancedBaseInstanceWEBGL,Xc:_emscripten_glDrawArraysInstancedEXT,Fb:_emscripten_glDrawArraysInstancedNV,Qe:_emscripten_glDrawBuffers,Vc:_emscripten_glDrawBuffersEXT,Nd:_emscripten_glDrawBuffersWEBGL,cg:_emscripten_glDrawElements,We:_emscripten_glDrawElementsInstanced,Ld:_emscripten_glDrawElementsInstancedANGLE,Cb:_emscripten_glDrawElementsInstancedARB,Te:_emscripten_glDrawElementsInstancedBaseVertexBaseInstanceWEBGL,Db:_emscripten_glDrawElementsInstancedEXT,Wc:_emscripten_glDrawElementsInstancedNV,Ke:_emscripten_glDrawRangeElements,dg:_emscripten_glEnable,eg:_emscripten_glEnableVertexAttribArray,fe:_emscripten_glEndQuery,$d:_emscripten_glEndQueryEXT,Ec:_emscripten_glEndTransformFeedback,pe:_emscripten_glFenceSync,fg:_emscripten_glFinish,gg:_emscripten_glFlush,Ge:_emscripten_glFramebufferRenderbuffer,He:_emscripten_glFramebufferTexture2D,Ic:_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,Qb:_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,sc:_emscripten_glGetFragDataLocation,ye:_emscripten_glGetFramebufferAttachmentParameteriv,Yb:_emscripten_glGetInteger64i_v,_b:_emscripten_glGetInteger64v,Gc:_emscripten_glGetIntegeri_v,ng:_emscripten_glGetIntegerv,Ib:_emscripten_glGetInternalformativ,Mb:_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,Ac:_emscripten_glGetTransformFeedbackVarying,cc:_emscripten_glGetUniformBlockIndex,ec:_emscripten_glGetUniformIndices,tg:_emscripten_glGetUniformLocation,ud:_emscripten_glGetUniformfv,td:_emscripten_glGetUniformiv,tc:_emscripten_glGetUniformuiv,zc:_emscripten_glGetVertexAttribIiv,yc:_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,Pc:_emscripten_glIsQuery,Qd:_emscripten_glIsQueryEXT,kd:_emscripten_glIsRenderbuffer,Wb:_emscripten_glIsSampler,jd:_emscripten_glIsShader,qe:_emscripten_glIsSync,ug:_emscripten_glIsTexture,Pb:_emscripten_glIsTransformFeedback,Hc:_emscripten_glIsVertexArray,Od:_emscripten_glIsVertexArrayOES,vg:_emscripten_glLineWidth,wg:_emscripten_glLinkProgram,Oe:_emscripten_glMultiDrawArraysInstancedBaseInstanceWEBGL,Pe:_emscripten_glMultiDrawElementsInstancedBaseVertexBaseInstanceWEBGL,Ob:_emscripten_glPauseTransformFeedback,xg:_emscripten_glPixelStorei,Zc:_emscripten_glPolygonModeWEBGL,id:_emscripten_glPolygonOffset,$c:_emscripten_glPolygonOffsetClampEXT,Lb:_emscripten_glProgramBinary,Kb:_emscripten_glProgramParameteri,Yd:_emscripten_glQueryCounterEXT,Re:_emscripten_glReadBuffer,yg:_emscripten_glReadPixels,hd:_emscripten_glReleaseShaderCompiler,Ae:_emscripten_glRenderbufferStorage,we:_emscripten_glRenderbufferStorageMultisample,Nb:_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,Ka:_emscripten_glTexParameteri,La:_emscripten_glTexParameteriv,Le:_emscripten_glTexStorage2D,Jb:_emscripten_glTexStorage3D,Ma:_emscripten_glTexSubImage2D,Tc:_emscripten_glTexSubImage3D,Bc:_emscripten_glTransformFeedbackVaryings,Na:_emscripten_glUniform1f,Oa:_emscripten_glUniform1fv,wf:_emscripten_glUniform1i,xf:_emscripten_glUniform1iv,rc:_emscripten_glUniform1ui,nc:_emscripten_glUniform1uiv,yf:_emscripten_glUniform2f,zf:_emscripten_glUniform2fv,vf:_emscripten_glUniform2i,uf:_emscripten_glUniform2iv,qc:_emscripten_glUniform2ui,mc:_emscripten_glUniform2uiv,tf:_emscripten_glUniform3f,sf:_emscripten_glUniform3fv,rf:_emscripten_glUniform3i,qf:_emscripten_glUniform3iv,pc:_emscripten_glUniform3ui,lc:_emscripten_glUniform3uiv,pf:_emscripten_glUniform4f,of:_emscripten_glUniform4fv,bf:_emscripten_glUniform4i,cf:_emscripten_glUniform4iv,oc:_emscripten_glUniform4ui,kc:_emscripten_glUniform4uiv,$b:_emscripten_glUniformBlockBinding,df:_emscripten_glUniformMatrix2fv,Oc:_emscripten_glUniformMatrix2x3fv,Mc:_emscripten_glUniformMatrix2x4fv,ef:_emscripten_glUniformMatrix3fv,Nc:_emscripten_glUniformMatrix3x2fv,Kc:_emscripten_glUniformMatrix3x4fv,ff:_emscripten_glUniformMatrix4fv,Lc:_emscripten_glUniformMatrix4x2fv,Jc:_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,Gb:_emscripten_glVertexAttribDivisorARB,Yc:_emscripten_glVertexAttribDivisorEXT,Hb:_emscripten_glVertexAttribDivisorNV,xc:_emscripten_glVertexAttribI4i,vc:_emscripten_glVertexAttribI4iv,wc:_emscripten_glVertexAttribI4ui,uc:_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,Ba:_fd_read,nb:_fd_seek,ka:_fd_write,Pa:_glGetIntegerv,oa:_glGetString,Qa:_glGetStringi,Rd:invoke_dd,Sd:invoke_dddd,xa:invoke_diii,Ua:invoke_fdiiii,Ta:invoke_fdiiiii,Sa:invoke_fii,ya:invoke_fiii,s:invoke_fiiidi,T:invoke_fiiif,t:invoke_fiiiidi,r:invoke_i,j:invoke_ii,D:invoke_iif,cb:invoke_iiffi,qa:invoke_iiffiii,g:invoke_iii,Ha:invoke_iiiffii,sa:invoke_iiifi,f:invoke_iiii,S:invoke_iiiiff,l:invoke_iiiii,db:invoke_iiiiid,z:invoke_iiiiii,x:invoke_iiiiiii,E:invoke_iiiiiiii,q:invoke_iiiiiiiii,pa:invoke_iiiiiiiiii,ba:invoke_iiiiiiiiiiii,na:invoke_iiiiiiiiiiiifiii,fb:invoke_j,V:invoke_ji,Y:invoke_jiii,ca:invoke_jiiii,J:invoke_jjji,k:invoke_v,Xf:invoke_vff,b:invoke_vi,P:invoke_vid,R:invoke_vif,u:invoke_viff,C:invoke_viffff,X:invoke_vifffff,Va:invoke_viffffff,B:invoke_viffi,ga:invoke_viffiiiiiii,c:invoke_vii,Ya:invoke_viidii,O:invoke_viif,F:invoke_viiff,Z:invoke_viifi,wa:invoke_viififii,w:invoke_viifiiifi,d:invoke_viii,I:invoke_viiif,Ea:invoke_viiiff,A:invoke_viiiffi,K:invoke_viiiffiffii,L:invoke_viiififiiiiiiiiiiii,i:invoke_viiii,Xa:invoke_viiiidididii,ja:invoke_viiiif,Fa:invoke_viiiiff,ua:invoke_viiiiffi,Aa:invoke_viiiifi,h:invoke_viiiii,xb:invoke_viiiiif,Ga:invoke_viiiiiff,Wa:invoke_viiiiiffiii,ab:invoke_viiiiifi,m:invoke_viiiiii,p:invoke_viiiiiii,U:invoke_viiiiiiii,W:invoke_viiiiiiiii,M:invoke_viiiiiiiiii,ra:invoke_viiiiiiiiiii,aa:invoke_viiiiiiiiiiiiiii,Ja:invoke_viiiiiji,bb:invoke_viiiijjiiiiff,Q:invoke_viiij,y:invoke_viiijii,fa:invoke_viij,o:invoke_viiji,ia:invoke_viijiffi,$:invoke_viijii,$a:invoke_viijiii,_:invoke_viijiiiif,Ia:invoke_viijiiiii,ha:invoke_viji,v:invoke_vijii,va:invoke_vijiifi,_a:invoke_vijiififi,ta:invoke_vijiii,da:invoke_vijjjj,jc:invoke_vjii,ma:_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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_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})} ;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 9f9a8b2f78..edba8b1a17 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:4ed602485bf7e5786945a4c0298c194d1c17bccb54591a2e5d61b7f8259888af -size 13005460 +oid sha256:ad2ce4fc2943bd989b1fdc5584818a7edfa3563450b78e7e50cedc69be2174a4 +size 13015470 diff --git a/crates/grida-canvas-wasm/package.json b/crates/grida-canvas-wasm/package.json index e08d57d52e..80a9e07152 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.3", + "version": "0.91.0-canary.5", "private": false, "description": "WASM bindings for Grida Canvas", "keywords": [ diff --git a/crates/grida-canvas/benches/bench_rectangles.rs b/crates/grida-canvas/benches/bench_rectangles.rs index 855db59bfa..01ac8b5cbc 100644 --- a/crates/grida-canvas/benches/bench_rectangles.rs +++ b/crates/grida-canvas/benches/bench_rectangles.rs @@ -6,7 +6,33 @@ use cg::runtime::scene::{Backend, Renderer}; use criterion::{black_box, criterion_group, criterion_main, Criterion}; use math2::transform::AffineTransform; +struct RectConfig { + opacity: f32, + blend_mode: LayerBlendMode, + with_effects: bool, +} + +impl Default for RectConfig { + fn default() -> Self { + Self { + opacity: 1.0, + blend_mode: LayerBlendMode::default(), + with_effects: false, + } + } +} + fn create_rectangles(count: usize, with_effects: bool) -> Scene { + create_rectangles_cfg( + count, + RectConfig { + with_effects, + ..Default::default() + }, + ) +} + +fn create_rectangles_cfg(count: usize, cfg: RectConfig) -> Scene { let mut graph = SceneGraph::new(); // Create rectangles @@ -14,8 +40,8 @@ fn create_rectangles(count: usize, with_effects: bool) -> Scene { .map(|_i| { Node::Rectangle(RectangleNodeRec { active: true, - opacity: 1.0, - blend_mode: LayerBlendMode::default(), + opacity: cfg.opacity, + blend_mode: cfg.blend_mode, mask: None, transform: AffineTransform::identity(), size: Size { @@ -34,7 +60,7 @@ fn create_rectangles(count: usize, with_effects: bool) -> Scene { stroke_dash_array: None, }, stroke_width: 1.0.into(), - effects: if with_effects { + effects: if cfg.with_effects { LayerEffects::from_array(vec![FilterEffect::DropShadow(FeShadow { dx: 2.0, dy: 2.0, @@ -229,5 +255,259 @@ fn bench_rectangles(c: &mut Criterion) { group.finish(); } -criterion_group!(benches, bench_rectangles); +/// Benchmarks specifically targeting the save_layer opacity folding optimization. +/// +/// Semi-transparent nodes (opacity < 1.0) with a non-PassThrough blend mode +/// previously required **two** save_layers: one for blend isolation, one for +/// opacity. With the opacity folding optimization, effectless semi-transparent +/// nodes now merge opacity into the blend save_layer, eliminating one GPU +/// surface allocation per node. +fn bench_opacity_folding(c: &mut Criterion) { + let width = 1000; + let height = 1000; + + let mut group = c.benchmark_group("opacity_folding"); + group.sample_size(100); + group.measurement_time(std::time::Duration::from_secs(10)); + + // --- 1K nodes --- + + // Baseline: opaque, no save_layer needed + group.bench_function("1k_opaque_passthrough", |b| { + b.iter(|| { + let mut renderer = Renderer::new( + Backend::new_from_raster(width, height), + None, + Camera2D::new(Size { + width: width as f32, + height: height as f32, + }), + ); + let scene = create_rectangles_cfg( + black_box(1_000), + RectConfig { + opacity: 1.0, + blend_mode: LayerBlendMode::default(), + with_effects: false, + }, + ); + renderer.load_scene(scene); + renderer.queue_unstable(); + renderer.flush(); + renderer.free(); + }) + }); + + // Optimized path: opacity folded into blend save_layer (1 save_layer) + group.bench_function("1k_semitransparent_normal", |b| { + b.iter(|| { + let mut renderer = Renderer::new( + Backend::new_from_raster(width, height), + None, + Camera2D::new(Size { + width: width as f32, + height: height as f32, + }), + ); + let scene = create_rectangles_cfg( + black_box(1_000), + RectConfig { + opacity: 0.8, + blend_mode: LayerBlendMode::Blend(BlendMode::Normal), + with_effects: false, + }, + ); + renderer.load_scene(scene); + renderer.queue_unstable(); + renderer.flush(); + renderer.free(); + }) + }); + + // Cannot fold: effects need separate opacity isolation (2+ save_layers) + group.bench_function("1k_semitransparent_normal_effects", |b| { + b.iter(|| { + let mut renderer = Renderer::new( + Backend::new_from_raster(width, height), + None, + Camera2D::new(Size { + width: width as f32, + height: height as f32, + }), + ); + let scene = create_rectangles_cfg( + black_box(1_000), + RectConfig { + opacity: 0.8, + blend_mode: LayerBlendMode::Blend(BlendMode::Normal), + with_effects: true, + }, + ); + renderer.load_scene(scene); + renderer.queue_unstable(); + renderer.flush(); + renderer.free(); + }) + }); + + // Opaque + Normal blend — previously wasted a save_layer, now zero overhead + group.bench_function("1k_opaque_normal", |b| { + b.iter(|| { + let mut renderer = Renderer::new( + Backend::new_from_raster(width, height), + None, + Camera2D::new(Size { + width: width as f32, + height: height as f32, + }), + ); + let scene = create_rectangles_cfg( + black_box(1_000), + RectConfig { + opacity: 1.0, + blend_mode: LayerBlendMode::Blend(BlendMode::Normal), + with_effects: false, + }, + ); + renderer.load_scene(scene); + renderer.queue_unstable(); + renderer.flush(); + renderer.free(); + }) + }); + + // --- 10K nodes --- + + group.bench_function("10k_opaque_passthrough", |b| { + b.iter(|| { + let mut renderer = Renderer::new( + Backend::new_from_raster(width, height), + None, + Camera2D::new(Size { + width: width as f32, + height: height as f32, + }), + ); + let scene = create_rectangles_cfg( + black_box(10_000), + RectConfig { + opacity: 1.0, + blend_mode: LayerBlendMode::default(), + with_effects: false, + }, + ); + renderer.load_scene(scene); + renderer.queue_unstable(); + renderer.flush(); + renderer.free(); + }) + }); + + // Opaque + Normal blend at 10k — should match opaque_passthrough now + group.bench_function("10k_opaque_normal", |b| { + b.iter(|| { + let mut renderer = Renderer::new( + Backend::new_from_raster(width, height), + None, + Camera2D::new(Size { + width: width as f32, + height: height as f32, + }), + ); + let scene = create_rectangles_cfg( + black_box(10_000), + RectConfig { + opacity: 1.0, + blend_mode: LayerBlendMode::Blend(BlendMode::Normal), + with_effects: false, + }, + ); + renderer.load_scene(scene); + renderer.queue_unstable(); + renderer.flush(); + renderer.free(); + }) + }); + + group.bench_function("10k_semitransparent_normal", |b| { + b.iter(|| { + let mut renderer = Renderer::new( + Backend::new_from_raster(width, height), + None, + Camera2D::new(Size { + width: width as f32, + height: height as f32, + }), + ); + let scene = create_rectangles_cfg( + black_box(10_000), + RectConfig { + opacity: 0.8, + blend_mode: LayerBlendMode::Blend(BlendMode::Normal), + with_effects: false, + }, + ); + renderer.load_scene(scene); + renderer.queue_unstable(); + renderer.flush(); + renderer.free(); + }) + }); + + group.bench_function("10k_semitransparent_normal_effects", |b| { + b.iter(|| { + let mut renderer = Renderer::new( + Backend::new_from_raster(width, height), + None, + Camera2D::new(Size { + width: width as f32, + height: height as f32, + }), + ); + let scene = create_rectangles_cfg( + black_box(10_000), + RectConfig { + opacity: 0.8, + blend_mode: LayerBlendMode::Blend(BlendMode::Normal), + with_effects: true, + }, + ); + renderer.load_scene(scene); + renderer.queue_unstable(); + renderer.flush(); + renderer.free(); + }) + }); + + // --- PassThrough + opacity (save_layer_alpha path) --- + + group.bench_function("10k_semitransparent_passthrough", |b| { + b.iter(|| { + let mut renderer = Renderer::new( + Backend::new_from_raster(width, height), + None, + Camera2D::new(Size { + width: width as f32, + height: height as f32, + }), + ); + let scene = create_rectangles_cfg( + black_box(10_000), + RectConfig { + opacity: 0.8, + blend_mode: LayerBlendMode::default(), // PassThrough + with_effects: false, + }, + ); + renderer.load_scene(scene); + renderer.queue_unstable(); + renderer.flush(); + renderer.free(); + }) + }); + + group.finish(); +} + +criterion_group!(benches, bench_rectangles, bench_opacity_folding); criterion_main!(benches); diff --git a/crates/grida-canvas/examples/tool_gen_bench_fixture.rs b/crates/grida-canvas/examples/tool_gen_bench_fixture.rs index 61dcb215ee..aaea740ab7 100644 --- a/crates/grida-canvas/examples/tool_gen_bench_fixture.rs +++ b/crates/grida-canvas/examples/tool_gen_bench_fixture.rs @@ -95,15 +95,23 @@ fn scene_mixed_heavy() -> Scene { flat_scene("bench-mixed-heavy", nodes) } -/// Wide container with 10 000 child rectangles. +/// Single wide container with 10 000 absolutely positioned child rectangles. +/// +/// Children use `rect_absolute` so Taffy places them on the explicit grid (same pattern as +/// `bench-blur-container`). Without `LayoutPositioning::Absolute`, `LayoutMode::Normal` would +/// stack them in block flow and ignore transform-based cell coordinates. +/// +/// `clip: true` on the container exercises clipping against a tight bounds rect that matches +/// the grid extent (last row/column edges, no extra slack gap). fn scene_wide_container() -> Scene { let child_count: usize = 10_000; let cols: usize = 100; let cell = 20.0_f32; let gap = 2.0_f32; + let rows = child_count.div_ceil(cols); - let container_w = cols as f32 * (cell + gap); - let container_h = child_count.div_ceil(cols) as f32 * (cell + gap); + let container_w = cols as f32 * cell + (cols - 1).max(0) as f32 * gap; + let container_h = rows as f32 * cell + (rows - 1).max(0) as f32 * gap; let container = Node::Container(ContainerNodeRec { active: true, @@ -151,7 +159,7 @@ fn scene_wide_container() -> Scene { let r = ((i * 13) % 256) as u8; let g = ((i * 7) % 256) as u8; let b = ((i * 3) % 256) as u8; - pairs.push((id, rect(x, y, cell, cell, solid(r, g, b, 255)))); + pairs.push((id, rect_absolute(x, y, cell, cell, solid(r, g, b, 255)))); children_ids.push(id); } @@ -271,6 +279,91 @@ fn scene_text_heavy() -> Scene { flat_scene("bench-text-heavy", nodes) } +/// Text spans with stroke (3 000 nodes). +fn scene_text_stroke_heavy() -> Scene { + let count = 3000; + let cols = 20; + let row_h = 28.0_f32; + let col_w = 200.0_f32; + + let mut nodes = Vec::with_capacity(count); + for i in 0..count { + let col = i % cols; + let row = i / cols; + let x = col as f32 * col_w; + let y = row as f32 * row_h; + let weight = if i % 3 == 0 { 700 } else { 400 }; + let size = 12.0 + (i % 5) as f32 * 2.0; + let stroke_w = 1.0 + (i % 3) as f32; + nodes.push(Node::TextSpan(TextSpanNodeRec { + active: true, + transform: AffineTransform::new(x, y, 0.0), + width: None, + height: None, + layout_child: None, + text: format!("Stroke text #{i}"), + text_style: { + let mut ts = TextStyleRec::from_font("Inter", size); + ts.font_weight = FontWeight(weight); + ts + }, + text_align: TextAlign::Left, + text_align_vertical: TextAlignVertical::Top, + max_lines: None, + ellipsis: None, + fills: Paints::new(vec![solid(30, 30, 40, 255)]), + strokes: Paints::new(vec![solid(255, 140, 0, 255)]), + stroke_width: stroke_w, + stroke_align: StrokeAlign::Center, + opacity: 1.0, + blend_mode: LayerBlendMode::PassThrough, + mask: None, + effects: LayerEffects::default(), + })); + } + flat_scene("bench-text-stroke-heavy", nodes) +} + +/// Filled rectangles with uniform stroke (2 000 nodes). +fn scene_stroke_rect_grid() -> Scene { + let cols = 50; + let rows = 40; + let cell = 40.0_f32; + let gap = 8.0_f32; + + let mut nodes = Vec::with_capacity(cols * rows); + for row in 0..rows { + for col in 0..cols { + let x = col as f32 * (cell + gap); + let y = row as f32 * (cell + gap); + let r = ((col * 7) % 256) as u8; + let g = ((row * 11) % 256) as u8; + let b = (((col + row) * 5) % 256) as u8; + let sw = 1.0 + (col % 4) as f32; + nodes.push(Node::Rectangle(RectangleNodeRec { + active: true, + opacity: 1.0, + blend_mode: LayerBlendMode::PassThrough, + mask: None, + transform: AffineTransform::from_box_center(x, y, cell, cell, 0.0), + size: Size { + width: cell, + height: cell, + }, + corner_radius: RectangularCornerRadius::default(), + corner_smoothing: CornerSmoothing(0.0), + fills: Paints::new(vec![solid(r, g, b, 230)]), + strokes: Paints::new(vec![solid(20, 20, 30, 255)]), + stroke_style: StrokeStyle::default(), + stroke_width: StrokeWidth::Uniform(sw), + effects: LayerEffects::default(), + layout_child: None, + })); + } + } + flat_scene("bench-stroke-rect-grid", nodes) +} + /// Drop-shadow rectangles (2 000 nodes). /// Each has a drop shadow — the primary target for layer compositing cache. fn scene_shadow_grid() -> Scene { @@ -291,7 +384,7 @@ fn scene_shadow_grid() -> Scene { dx: 4.0, dy: 4.0, blur: 8.0, - spread: 0.0, + spread: 4.0, color: CGColor::from_rgba(0, 0, 0, 80), active: true, }); @@ -386,84 +479,6 @@ fn scene_mixed_effects() -> Scene { flat_scene("bench-mixed-effects", nodes) } -/// Container with drop shadow wrapping 2000 plain child rects. -/// This exercises the render surface optimization: the shadow is applied -/// ONCE to the composited subtree, not 2000 times. -/// -/// Compare with `scene_shadow_grid` where each rect has its own shadow. -fn scene_shadow_container() -> Scene { - let child_count: usize = 2000; - let cols: usize = 50; - let cell = 40.0_f32; - let gap = 20.0_f32; - - let container_w = cols as f32 * (cell + gap); - let container_h = child_count.div_ceil(cols) as f32 * (cell + gap); - - let shadow_effects = LayerEffects::new().drop_shadow(FeShadow { - dx: 4.0, - dy: 4.0, - blur: 8.0, - spread: 0.0, - color: CGColor::from_rgba(0, 0, 0, 80), - active: true, - }); - - let container = Node::Container(ContainerNodeRec { - active: true, - opacity: 1.0, - blend_mode: LayerBlendMode::PassThrough, - mask: None, - rotation: 0.0, - position: LayoutPositioningBasis::Inset(EdgeInsets { - top: 0.0, - right: 0.0, - bottom: 0.0, - left: 0.0, - }), - layout_container: LayoutContainerStyle::default(), - layout_dimensions: LayoutDimensionStyle { - layout_target_width: Some(container_w), - layout_target_height: Some(container_h), - layout_min_width: None, - layout_max_width: None, - layout_min_height: None, - layout_max_height: None, - layout_target_aspect_ratio: None, - }, - layout_child: None, - corner_radius: RectangularCornerRadius::default(), - corner_smoothing: CornerSmoothing(0.0), - fills: Paints::new(vec![solid(245, 245, 245, 255)]), - strokes: Paints::new(vec![]), - stroke_style: StrokeStyle::default(), - stroke_width: StrokeWidth::None, - effects: shadow_effects, - clip: false, - }); - - let container_id: u64 = 1; - let mut pairs: Vec<(u64, Node)> = vec![(container_id, container)]; - let mut children_ids: Vec = Vec::with_capacity(child_count); - - for i in 0..child_count { - let id = (i + 2) as u64; - let col = i % cols; - let row = i / cols; - let x = col as f32 * (cell + gap); - let y = row as f32 * (cell + gap); - let r = ((col * 7) % 256) as u8; - let g = ((row * 11) % 256) as u8; - let b = (((col + row) * 5) % 256) as u8; - pairs.push((id, rect_absolute(x, y, cell, cell, solid(r, g, b, 255)))); - children_ids.push(id); - } - - let mut links = HashMap::new(); - links.insert(container_id, children_ids); - build_scene("bench-shadow-container", None, pairs, links, vec![container_id]) -} - /// Container with layer blur wrapping 1000 plain child rects. /// The blur is applied once to the composited subtree via render surface. fn scene_blur_container() -> Scene { @@ -623,11 +638,11 @@ fn scene_blur_children_in_container() -> Scene { ) } -/// Progressive-blurred rectangles (1 000 nodes). +/// Progressive-blurred rectangles (~300 nodes). /// Each has a layer progressive blur with varying gradient directions. fn scene_progressive_blur_grid() -> Scene { - let cols = 40; - let rows = 25; + let cols = 20; + let rows = 15; let cell = 50.0_f32; let gap = 10.0_f32; @@ -652,7 +667,7 @@ fn scene_progressive_blur_grid() -> Scene { start: Alignment(-sx, -sy), end: Alignment(sx, sy), radius: 0.0, - radius2: 8.0 + (col % 8) as f32 * 2.0, + radius2: 6.0 + (col % 4) as f32 * 2.0, }), }), ..LayerEffects::default() @@ -663,6 +678,204 @@ fn scene_progressive_blur_grid() -> Scene { flat_scene("bench-progressive-blur-grid", nodes) } +/// High-contrast vertical stripes under semi-transparent panels with backdrop blur. +/// Stripes span the full grid so blur visibly smears fine detail; roots list stripes first. +fn scene_backdrop_blur_grid() -> Scene { + let cols = 20; + let rows = 15; + let cell = 52.0_f32; + let gap = 8.0_f32; + + let grid_w = cols as f32 * cell + (cols - 1).max(0) as f32 * gap; + let grid_h = rows as f32 * cell + (rows - 1).max(0) as f32 * gap; + let stripe_w = 14.0_f32; + let n_stripes = ((grid_w / stripe_w).ceil() as usize).max(1); + + let mut pairs: Vec<(u64, Node)> = Vec::with_capacity(n_stripes + cols * rows); + let mut roots: Vec = Vec::with_capacity(n_stripes + cols * rows); + let mut id: u64 = 1; + + for i in 0..n_stripes { + let left = i as f32 * stripe_w; + if left >= grid_w { + break; + } + let w = stripe_w.min(grid_w - left); + let cx = left + w * 0.5; + let cy = grid_h * 0.5; + // Slate / rose stripes — high contrast so backdrop blur is obvious. + let fill = if i % 2 == 0 { + solid(15, 23, 42, 255) + } else { + solid(244, 63, 94, 255) + }; + pairs.push((id, rect(cx, cy, w, grid_h, fill))); + roots.push(id); + id += 1; + } + + for row in 0..rows { + for col in 0..cols { + let x = col as f32 * (cell + gap); + let y = row as f32 * (cell + gap); + let blur = 4.0 + (col % 5) as f32; + let effects = LayerEffects::new().backdrop_blur(blur); + pairs.push(( + id, + rect_with_effects( + x, + y, + cell, + cell, + solid(255, 255, 255, 72), + effects, + ), + )); + roots.push(id); + id += 1; + } + } + + build_scene( + "bench-backdrop-blur-grid", + None, + pairs, + HashMap::new(), + roots, + ) +} + +/// Rectangles with procedural noise overlay (500 nodes). +fn scene_noise_grid() -> Scene { + let cols = 25; + let rows = 20; + let cell = 44.0_f32; + let gap = 8.0_f32; + + let mut nodes = Vec::with_capacity(cols * rows); + for row in 0..rows { + for col in 0..cols { + let x = col as f32 * (cell + gap); + let y = row as f32 * (cell + gap); + let r = ((col * 9) % 256) as u8; + let g = ((row * 13) % 256) as u8; + let b = 140; + let effects = LayerEffects { + noises: vec![FeNoiseEffect { + active: true, + noise_size: 1.2 + (row % 3) as f32 * 0.4, + density: 0.25 + (col % 5) as f32 * 0.08, + num_octaves: 3, + seed: (col * 31 + row * 17) as f32, + coloring: NoiseEffectColors::Mono { + color: CGColor::from_rgba(255, 255, 255, 40), + }, + blend_mode: BlendMode::Normal, + }], + ..LayerEffects::default() + }; + nodes.push(rect_with_effects(x, y, cell, cell, solid(r, g, b, 255), effects)); + } + } + flat_scene("bench-noise-grid", nodes) +} + +/// Liquid glass on a zebra stripe field, composed like `fixtures/l0_effects_glass.rs` / L0 scene +/// "L0 Effects Glass": white base, black vertical stripes (even indices), then inset rounded glass +/// with empty fills and `FeLiquidGlass` matching the golden reference. +fn scene_glass_grid() -> Scene { + let cols = 15; + let rows = 10; + let cell = 64.0_f32; + let gap = 12.0_f32; + // Same stripe pitch as L0 (`l0_effects_glass.rs`). + let stripe_w = 10.0_f32; + // Inset glass like L0's padding around the 300px panel (40 / 300 ≈ 13%). + let inset = (cell * (40.0 / 300.0)).clamp(6.0, 14.0); + + let n_stripes_per_cell = (cell / stripe_w).ceil() as i32; + let nodes_per_cell = 1 + (n_stripes_per_cell as usize + 1) / 2 + 1; + let mut pairs: Vec<(u64, Node)> = Vec::with_capacity(cols * rows * nodes_per_cell); + let mut roots: Vec = Vec::with_capacity(cols * rows * nodes_per_cell); + let mut id: u64 = 1; + + for row in 0..rows { + for col in 0..cols { + let x = col as f32 * (cell + gap); + let y = row as f32 * (cell + gap); + + // 1) White tile (shows through stripe gaps). + pairs.push((id, rect(x, y, cell, cell, solid(255, 255, 255, 255)))); + roots.push(id); + id += 1; + + // 2) Black vertical stripes on even indices only (odd columns stay white). + for i in 0..n_stripes_per_cell { + if i % 2 != 0 { + continue; + } + let left = i as f32 * stripe_w; + if left >= cell { + break; + } + let w = stripe_w.min(cell - left); + pairs.push(( + id, + rect(x + left, y, w, cell, solid(0, 0, 0, 255)), + )); + roots.push(id); + id += 1; + } + + // 3) Glass: no fill/stroke, rounded rect, L0-style liquid glass (scaled depth for tile). + let gw = cell - 2.0 * inset; + let gh = cell - 2.0 * inset; + let gx = x + inset; + let gy = y + inset; + let corner = (gw * (60.0 / 300.0)).clamp(6.0, 18.0); + let depth = (100.0 * (gw / 300.0)).clamp(24.0, 100.0); + + pairs.push(( + id, + Node::Rectangle(RectangleNodeRec { + active: true, + opacity: 1.0, + blend_mode: LayerBlendMode::PassThrough, + mask: None, + transform: AffineTransform::from_box_center(gx, gy, gw, gh, 0.0), + size: Size { + width: gw, + height: gh, + }, + corner_radius: RectangularCornerRadius::circular(corner), + corner_smoothing: CornerSmoothing(0.0), + fills: Paints::new(vec![]), + strokes: Paints::new(vec![]), + stroke_style: StrokeStyle::default(), + stroke_width: StrokeWidth::None, + effects: LayerEffects { + glass: Some(FeLiquidGlass { + active: true, + light_intensity: 0.7, + light_angle: 45.0, + refraction: 1.0, + depth, + blur_radius: 0.0, + dispersion: 1.0, + }), + ..LayerEffects::default() + }, + layout_child: None, + }), + )); + roots.push(id); + id += 1; + } + } + + build_scene("bench-glass-grid", None, pairs, HashMap::new(), roots) +} + /// Opacity grid: 5 000 rects with fill only, varying opacity (0.1–0.9). /// Exercises the save_layer path for per-node opacity. fn scene_opacity_fill_only() -> Scene { @@ -725,13 +938,17 @@ fn main() { ("bench-deep-nesting", scene_deep_nesting()), ("bench-rotated-rects", scene_rotated_rects()), ("bench-text-heavy", scene_text_heavy()), + ("bench-text-stroke-heavy", scene_text_stroke_heavy()), + ("bench-stroke-rect-grid", scene_stroke_rect_grid()), ("bench-shadow-grid", scene_shadow_grid()), ("bench-blur-grid", scene_blur_grid()), ("bench-mixed-effects", scene_mixed_effects()), - ("bench-shadow-container", scene_shadow_container()), ("bench-blur-container", scene_blur_container()), ("bench-blur-children-in-container", scene_blur_children_in_container()), ("bench-progressive-blur-grid", scene_progressive_blur_grid()), + ("bench-backdrop-blur-grid", scene_backdrop_blur_grid()), + ("bench-noise-grid", scene_noise_grid()), + ("bench-glass-grid", scene_glass_grid()), ("bench-opacity-fill-only", scene_opacity_fill_only()), ("bench-opacity-fill-stroke", scene_opacity_fill_stroke()), ]; diff --git a/crates/grida-canvas/src/cache/compositor/promotion.rs b/crates/grida-canvas/src/cache/compositor/promotion.rs index c08e1a60a6..38cc919ee4 100644 --- a/crates/grida-canvas/src/cache/compositor/promotion.rs +++ b/crates/grida-canvas/src/cache/compositor/promotion.rs @@ -81,6 +81,16 @@ pub fn should_promote( PromotionStatus::Promoted } +/// Returns true if the node has any effects that make it a candidate for +/// compositor promotion (shadows, layer blur, noise). +/// +/// This is a cheap check on struct fields (no HashMap lookups). Use it as +/// an early filter before the full `should_promote` evaluation to skip +/// nodes that will never be promoted. +pub fn has_promotable_effects(layer: &PainterPictureLayer) -> bool { + has_expensive_effects(layer) +} + /// Returns true if the node has effects that are expensive to repaint /// (shadows, layer blur, noise). fn has_expensive_effects(layer: &PainterPictureLayer) -> bool { diff --git a/crates/grida-canvas/src/node/schema.rs b/crates/grida-canvas/src/node/schema.rs index d8f691dd48..ef90ce01ee 100644 --- a/crates/grida-canvas/src/node/schema.rs +++ b/crates/grida-canvas/src/node/schema.rs @@ -30,6 +30,18 @@ impl LayerEffects { Self::default() } + /// Returns true when there are no effects at all (no shadows, blur, + /// backdrop blur, glass, or noise). Used for fast-path dispatch + /// to skip the effects pipeline entirely for simple nodes. + #[inline] + pub fn is_empty(&self) -> bool { + self.blur.is_none() + && self.backdrop_blur.is_none() + && self.glass.is_none() + && self.shadows.is_empty() + && self.noises.is_empty() + } + /// Set layer blur effect pub fn blur(mut self, blur: impl Into) -> Self { self.blur = Some(FeLayerBlur::from(blur.into())); @@ -90,6 +102,35 @@ impl LayerEffects { self } + /// Returns true if opacity must be isolated in a separate save_layer + /// because effects (shadows, blur, glass, backdrop blur) render outside + /// the opacity wrapper and should appear at full alpha. + /// + /// When false, opacity can be safely folded into a parent save_layer + /// or the paint alpha, eliminating a GPU surface allocation. + #[inline] + pub fn needs_opacity_isolation(&self) -> bool { + // Drop/inner shadows render outside opacity — they should appear at + // full opacity even when the shape content is semi-transparent. + if self.shadows.iter().any(|s| s.active()) { + return true; + } + // Layer blur wraps everything including content — opacity inside + // blur vs outside blur produces different results. + if self.blur.as_ref().is_some_and(|b| b.active) { + return true; + } + // Backdrop blur and glass read from content behind the node + // and render outside the opacity wrapper. + if self.backdrop_blur.as_ref().is_some_and(|b| b.active) { + return true; + } + if self.glass.as_ref().is_some_and(|g| g.active) { + return true; + } + false + } + /// Returns true if this layer has any active effects that are expensive /// to paint (shadows, blurs, noise, glass). Simple fill/stroke-only /// nodes return false. diff --git a/crates/grida-canvas/src/painter/effects_noise.rs b/crates/grida-canvas/src/painter/effects_noise.rs index 5b41bbd3fa..cb8cae8676 100644 --- a/crates/grida-canvas/src/painter/effects_noise.rs +++ b/crates/grida-canvas/src/painter/effects_noise.rs @@ -50,7 +50,6 @@ pub fn render_noise_effect(effect: &FeNoiseEffect, canvas: &sk::Canvas, shape: & // Apply blend mode directly to paint, matching SVG feMerge behavior let blend_mode: sk::BlendMode = effect.blend_mode.into(); let mut p = Paint::default(); - let path = shape.to_path(); match &effect.coloring { NoiseEffectColors::Mono { color } => { @@ -78,7 +77,7 @@ pub fn render_noise_effect(effect: &FeNoiseEffect, canvas: &sk::Canvas, shape: & p.set_shader(shader); p.set_blend_mode(blend_mode); p.set_anti_alias(true); - canvas.draw_path(&path, &p); + shape.draw_on_canvas(canvas, &p); } NoiseEffectColors::Duo { color1, color2 } => { // SVG filter pipeline for Duo (USES TWO DISTINCT NON-OVERLAPPING PATTERNS): @@ -117,7 +116,7 @@ pub fn render_noise_effect(effect: &FeNoiseEffect, canvas: &sk::Canvas, shape: & p.set_shader(shader1); p.set_blend_mode(blend_mode); p.set_anti_alias(true); - canvas.draw_path(&path, &p); + shape.draw_on_canvas(canvas, &p); // Draw color2 pattern (upper alpha range) on top let color2_sk: sk::Color = (*color2).into(); @@ -126,7 +125,7 @@ pub fn render_noise_effect(effect: &FeNoiseEffect, canvas: &sk::Canvas, shape: & p.set_shader(shader2); p.set_blend_mode(blend_mode); p.set_anti_alias(true); - canvas.draw_path(&path, &p); + shape.draw_on_canvas(canvas, &p); } NoiseEffectColors::Multi { opacity } => { // SVG filter pipeline for Multi: @@ -153,7 +152,7 @@ pub fn render_noise_effect(effect: &FeNoiseEffect, canvas: &sk::Canvas, shape: & p.set_alpha(alpha); p.set_blend_mode(blend_mode); p.set_anti_alias(true); - canvas.draw_path(&path, &p); + shape.draw_on_canvas(canvas, &p); } } } diff --git a/crates/grida-canvas/src/painter/geometry.rs b/crates/grida-canvas/src/painter/geometry.rs index 32e0bef03d..1c4971c0df 100644 --- a/crates/grida-canvas/src/painter/geometry.rs +++ b/crates/grida-canvas/src/painter/geometry.rs @@ -133,6 +133,47 @@ impl PainterShape { } } + /// Draw the shape directly on the canvas using the most efficient Skia + /// primitive for the shape type. + /// + /// For simple shapes (rect, rrect, oval), this avoids creating an + /// intermediate `Path` object and uses Skia's specialized GPU draw calls + /// (`draw_rect`, `draw_rrect`, `draw_oval`) which have lower overhead + /// than `draw_path`. + #[inline] + pub fn draw_on_canvas(&self, canvas: &skia_safe::Canvas, paint: &skia_safe::Paint) { + if let Some(rect) = self.rect_shape { + canvas.draw_rect(rect, paint); + } else if let Some(rrect) = &self.rrect { + canvas.draw_rrect(rrect, paint); + } else if let Some(oval) = &self.oval { + canvas.draw_oval(oval, paint); + } else if let Some(existing_path) = &self.path { + canvas.draw_path(existing_path, paint); + } else { + canvas.draw_rect(self.rect, paint); + } + } + + /// Clip the canvas to this shape using the most efficient Skia primitive. + /// + /// For rect/rrect shapes, uses `clip_rect`/`clip_rrect` which are faster + /// than `clip_path`. Falls back to `clip_path` for ovals and complex paths. + #[inline] + pub fn clip_on_canvas(&self, canvas: &skia_safe::Canvas) { + if let Some(rect) = self.rect_shape { + canvas.clip_rect(rect, None, true); + } else if let Some(rrect) = &self.rrect { + canvas.clip_rrect(rrect, None, true); + } else if let Some(oval) = &self.oval { + canvas.clip_path(&Path::oval(oval, None), None, true); + } else if let Some(existing_path) = &self.path { + canvas.clip_path(existing_path, None, true); + } else { + canvas.clip_rect(self.rect, None, true); + } + } + pub fn is_closed(&self) -> bool { if let Some(path) = &self.path { path.is_last_contour_closed() diff --git a/crates/grida-canvas/src/painter/layer.rs b/crates/grida-canvas/src/painter/layer.rs index 515ae80a6a..5ca20eb33e 100644 --- a/crates/grida-canvas/src/painter/layer.rs +++ b/crates/grida-canvas/src/painter/layer.rs @@ -196,6 +196,30 @@ pub struct PainterPictureShapeLayer { pub marker_end_shape: StrokeMarkerPreset, /// Stroke width needed for decoration sizing. pub stroke_width: f32, + /// Whether the stroke geometry overlaps the fill area (Inside or Center). + /// When true AND both fills and strokes are present, the paint-alpha opacity + /// folding fast path cannot be used because applying opacity independently + /// to fill and stroke paints produces a visible compositing artifact in the + /// overlap region (double-blending, up to 64 channel diff). + /// + /// Per the SVG/CSS spec (and Chromium's implementation), node-level opacity + /// requires group isolation (save_layer): fill+stroke are drawn at full + /// opacity into an offscreen surface, then composited at the node's opacity. + /// Only Outside strokes have zero geometric overlap and can safely use + /// per-paint-alpha. + /// + /// See `docs/wg/feat-2d/stroke-fill-opacity.md`. + pub stroke_overlaps_fill: bool, + /// Pre-computed fill path with stroke region subtracted (PathOp::Difference). + /// + /// When stroke overlaps fill (Inside/Center) and both fills and strokes are + /// present, drawing this path instead of the full fill path eliminates the + /// overlap region. This allows per-paint-alpha opacity folding (zero GPU + /// surfaces) while producing output identical to `save_layer_alpha`. + /// + /// `None` when no overlap exists, or when PathOp fails on degenerate geometry. + /// In the `None` + overlap case, the painter falls back to `save_layer_alpha`. + pub non_overlapping_fill_path: Option, } #[derive(Debug, Clone)] @@ -420,6 +444,30 @@ impl LayerList { self.layers.len() } + /// Compute a fill path with the stroke region subtracted (PathOp::Difference). + /// + /// Returns `Some(path)` when stroke overlaps fill and both are present. + /// The resulting path, drawn with per-paint-alpha opacity, produces output + /// identical to `save_layer_alpha` group isolation — zero overlap, zero + /// GPU surface allocations. + /// + /// Returns `None` if no overlap, fills/strokes are empty, or PathOp fails + /// (degenerate geometry). The painter falls back to `save_layer_alpha`. + fn compute_non_overlapping_fill_path( + shape: &PainterShape, + stroke_path: Option<&skia_safe::Path>, + stroke_overlaps_fill: bool, + fills: &Paints, + strokes: &Paints, + ) -> Option { + if !stroke_overlaps_fill || fills.is_empty() || strokes.is_empty() { + return None; + } + stroke_path.and_then(|sp| { + skia_safe::op(&shape.to_path(), sp, skia_safe::PathOp::Difference) + }) + } + fn flatten_node( id: &NodeId, graph: &SceneGraph, @@ -508,6 +556,13 @@ impl LayerList { (opacity, n.blend_mode) }; + let fills = Self::filter_visible_paints(&n.fills); + 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, + ); + let layer = PainterPictureLayer::Shape(PainterPictureShapeLayer { base: PainterPictureLayerBase { id: id.clone(), @@ -519,12 +574,14 @@ impl LayerList { }, shape, effects: own_effects, - strokes: Self::filter_visible_paints(&n.strokes), - fills: Self::filter_visible_paints(&n.fills), + strokes, + fills, stroke_path, marker_start_shape: StrokeMarkerPreset::None, marker_end_shape: StrokeMarkerPreset::None, stroke_width: 0.0, + stroke_overlaps_fill, + non_overlapping_fill_path, }); out.push(LayerEntry { id: id.clone(), @@ -599,6 +656,13 @@ impl LayerList { } else { None }; + let fills = Self::filter_visible_paints(&n.fills); + 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, + ); + let layer = PainterPictureLayer::Shape(PainterPictureShapeLayer { base: PainterPictureLayerBase { id: id.clone(), @@ -610,12 +674,14 @@ impl LayerList { }, shape, effects: Self::filter_active_effects(&n.effects), - strokes: Self::filter_visible_paints(&n.strokes), - fills: Self::filter_visible_paints(&n.fills), + strokes, + fills, stroke_path, marker_start_shape: StrokeMarkerPreset::None, marker_end_shape: StrokeMarkerPreset::None, stroke_width: 0.0, + stroke_overlaps_fill, + non_overlapping_fill_path, }); out.push(LayerEntry { id: id.clone(), @@ -652,6 +718,13 @@ impl LayerList { &n.size, &shape, ); + let fills = Self::filter_visible_paints(&n.fills); + 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, + ); + let layer = PainterPictureLayer::Shape(PainterPictureShapeLayer { base: PainterPictureLayerBase { id: id.clone(), @@ -663,12 +736,14 @@ impl LayerList { }, shape, effects: Self::filter_active_effects(&n.effects), - strokes: Self::filter_visible_paints(&n.strokes), - fills: Self::filter_visible_paints(&n.fills), + strokes, + fills, stroke_path, marker_start_shape: StrokeMarkerPreset::None, marker_end_shape: StrokeMarkerPreset::None, stroke_width: 0.0, + stroke_overlaps_fill, + non_overlapping_fill_path, }); out.push(LayerEntry { id: id.clone(), @@ -699,6 +774,13 @@ impl LayerList { } else { None }; + let fills = Self::filter_visible_paints(&n.fills); + 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, + ); + let layer = PainterPictureLayer::Shape(PainterPictureShapeLayer { base: PainterPictureLayerBase { id: id.clone(), @@ -710,12 +792,14 @@ impl LayerList { }, shape, effects: Self::filter_active_effects(&n.effects), - strokes: Self::filter_visible_paints(&n.strokes), - fills: Self::filter_visible_paints(&n.fills), + strokes, + fills, stroke_path, marker_start_shape: StrokeMarkerPreset::None, marker_end_shape: StrokeMarkerPreset::None, stroke_width: 0.0, + stroke_overlaps_fill, + non_overlapping_fill_path, }); out.push(LayerEntry { id: id.clone(), @@ -746,6 +830,13 @@ impl LayerList { } else { None }; + let fills = Self::filter_visible_paints(&n.fills); + 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, + ); + let layer = PainterPictureLayer::Shape(PainterPictureShapeLayer { base: PainterPictureLayerBase { id: id.clone(), @@ -757,12 +848,14 @@ impl LayerList { }, shape, effects: Self::filter_active_effects(&n.effects), - strokes: Self::filter_visible_paints(&n.strokes), - fills: Self::filter_visible_paints(&n.fills), + strokes, + fills, stroke_path, marker_start_shape: StrokeMarkerPreset::None, marker_end_shape: StrokeMarkerPreset::None, stroke_width: 0.0, + stroke_overlaps_fill, + non_overlapping_fill_path, }); out.push(LayerEntry { id: id.clone(), @@ -793,6 +886,13 @@ impl LayerList { } else { None }; + let fills = Self::filter_visible_paints(&n.fills); + 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, + ); + let layer = PainterPictureLayer::Shape(PainterPictureShapeLayer { base: PainterPictureLayerBase { id: id.clone(), @@ -804,12 +904,14 @@ impl LayerList { }, shape, effects: Self::filter_active_effects(&n.effects), - strokes: Self::filter_visible_paints(&n.strokes), - fills: Self::filter_visible_paints(&n.fills), + strokes, + fills, stroke_path, marker_start_shape: StrokeMarkerPreset::None, marker_end_shape: StrokeMarkerPreset::None, stroke_width: 0.0, + stroke_overlaps_fill, + non_overlapping_fill_path, }); out.push(LayerEntry { id: id.clone(), @@ -840,6 +942,13 @@ impl LayerList { } else { None }; + let fills = Self::filter_visible_paints(&n.fills); + 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, + ); + let layer = PainterPictureLayer::Shape(PainterPictureShapeLayer { base: PainterPictureLayerBase { id: id.clone(), @@ -851,12 +960,14 @@ impl LayerList { }, shape, effects: Self::filter_active_effects(&n.effects), - strokes: Self::filter_visible_paints(&n.strokes), - fills: Self::filter_visible_paints(&n.fills), + strokes, + fills, stroke_path, marker_start_shape: StrokeMarkerPreset::None, marker_end_shape: StrokeMarkerPreset::None, stroke_width: 0.0, + stroke_overlaps_fill, + non_overlapping_fill_path, }); out.push(LayerEntry { id: id.clone(), @@ -927,6 +1038,8 @@ impl LayerList { marker_start_shape: n.marker_start_shape, marker_end_shape: n.marker_end_shape, stroke_width: n.stroke_width, + stroke_overlaps_fill: true, + non_overlapping_fill_path: None, }); out.push(LayerEntry { id: id.clone(), @@ -1021,6 +1134,13 @@ impl LayerList { } else { None }; + let fills = Self::filter_visible_paints(&n.fills); + 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, + ); + let layer = PainterPictureLayer::Shape(PainterPictureShapeLayer { base: PainterPictureLayerBase { id: id.clone(), @@ -1032,12 +1152,14 @@ impl LayerList { }, shape, effects: Self::filter_active_effects(&n.effects), - strokes: Self::filter_visible_paints(&n.strokes), - fills: Self::filter_visible_paints(&n.fills), + strokes, + fills, stroke_path, marker_start_shape: StrokeMarkerPreset::None, marker_end_shape: StrokeMarkerPreset::None, stroke_width: 0.0, + stroke_overlaps_fill, + non_overlapping_fill_path, }); out.push(LayerEntry { id: id.clone(), @@ -1108,6 +1230,15 @@ impl LayerList { } else { None }; + let fills = Self::filter_visible_paints(&Paints::new([Paint::Image( + n.fill.clone(), + )])); + 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, + ); + let layer = PainterPictureLayer::Shape(PainterPictureShapeLayer { base: PainterPictureLayerBase { id: id.clone(), @@ -1119,14 +1250,14 @@ impl LayerList { }, shape, effects: Self::filter_active_effects(&n.effects), - strokes: Self::filter_visible_paints(&n.strokes), - fills: Self::filter_visible_paints(&Paints::new([Paint::Image( - n.fill.clone(), - )])), + strokes, + fills, stroke_path, marker_start_shape: StrokeMarkerPreset::None, marker_end_shape: StrokeMarkerPreset::None, stroke_width: 0.0, + stroke_overlaps_fill, + non_overlapping_fill_path, }); out.push(LayerEntry { id: id.clone(), @@ -1160,6 +1291,8 @@ impl LayerList { marker_start_shape: StrokeMarkerPreset::None, marker_end_shape: StrokeMarkerPreset::None, stroke_width: 0.0, + stroke_overlaps_fill: false, + non_overlapping_fill_path: None, }); out.push(LayerEntry { id: id.clone(), diff --git a/crates/grida-canvas/src/painter/paint.rs b/crates/grida-canvas/src/painter/paint.rs index b0a8e636b2..ca5caea68d 100644 --- a/crates/grida-canvas/src/painter/paint.rs +++ b/crates/grida-canvas/src/painter/paint.rs @@ -29,6 +29,21 @@ pub fn sk_paint_stack( size: (f32, f32), images: &ImageRepository, ) -> Option { + // Fast path: single solid fill — set color directly on the paint, + // avoiding shader object allocation and giving Skia's GPU backend + // a simpler code path (no shader program dispatch). + if paints.len() == 1 { + if let Paint::Solid(solid) = &paints[0] { + let CGColor { r, g, b, a } = solid.color; + let final_alpha = (a as f32 * solid.opacity()).round() as u8; + let mut paint = skia_safe::Paint::default(); + paint.set_anti_alias(true); + paint.set_color(Color::from_argb(final_alpha, r, g, b)); + paint.set_blend_mode(solid.blend_mode.into()); + return Some(paint); + } + } + // Paint stacking semantics: // - `paints` is ordered bottom → top (the last entry is visually top-most). // - Skia's `shaders::blend(mode, dst, src)` interprets the first shader as the @@ -72,6 +87,19 @@ pub fn sk_paint_stack_without_images( paints: &[Paint], size: (f32, f32), ) -> Option { + // Fast path: single solid fill — direct color, no shader allocation. + if paints.len() == 1 { + if let Paint::Solid(solid) = &paints[0] { + let CGColor { r, g, b, a } = solid.color; + let final_alpha = (a as f32 * solid.opacity()).round() as u8; + let mut paint = skia_safe::Paint::default(); + paint.set_anti_alias(true); + paint.set_color(Color::from_argb(final_alpha, r, g, b)); + paint.set_blend_mode(solid.blend_mode.into()); + return Some(paint); + } + } + // Same ordering rules as `sk_paint_stack` (bottom → top). let mut iter = paints.iter(); let first = iter.next()?; @@ -89,8 +117,6 @@ pub fn sk_paint_stack_without_images( // Apply the base paint's blend mode at the paint level so the first // fill can blend with the canvas/background, matching editor semantics. paint.set_blend_mode(base_blend_mode.into()); - - // Don't set blend mode - defaults to SrcOver, and blending is already handled in shader composition Some(paint) } diff --git a/crates/grida-canvas/src/painter/painter.rs b/crates/grida-canvas/src/painter/painter.rs index 4c107e5672..eb49ce9e62 100644 --- a/crates/grida-canvas/src/painter/painter.rs +++ b/crates/grida-canvas/src/painter/painter.rs @@ -21,9 +21,29 @@ use skia_safe::{ canvas::SaveLayerRec, textlayout, Matrix, Paint as SkPaint, Path, PathBuilder, Point, Rect, Shader, }; -use std::cell::RefCell; +use std::cell::{Cell, RefCell}; +use std::collections::HashMap; use std::rc::Rc; +/// Pre-extracted blit data for a single promoted (compositor-cached) node. +/// +/// Built before the draw pass so that the Painter can blit promoted nodes +/// inline at their correct z-position in the render command tree, instead +/// of batching all promoted blits before live draws (which breaks z-order +/// when a live parent covers its promoted children). +pub struct PromotedBlit { + /// The cached image to blit (either individual texture or atlas snapshot). + pub image: Rc, + /// Source rectangle within the image (full image for individual, sub-rect for atlas). + pub src_rect: Rect, + /// Destination rectangle in world coordinates (the node's render bounds). + pub dst_rect: Rect, + /// Opacity to apply when blitting. + pub opacity: f32, + /// Blend mode to apply when blitting. + pub blend_mode: skia_safe::BlendMode, +} + /// A painter that handles all drawing operations for nodes, /// with proper effect ordering and a layer‐blur/backdrop‐blur pipeline. pub struct Painter<'a> { @@ -32,12 +52,13 @@ pub struct Painter<'a> { images: &'a ImageRepository, path_cache: RefCell, scene_cache: Option<&'a SceneCache>, - cache_hits: RefCell, + cache_hits: Cell, policy: RenderPolicy, variant_key: u64, - /// Node IDs that are handled by the compositor and should be skipped - /// during live drawing to avoid double-rendering. - promoted_skip: Option<&'a std::collections::HashSet>, + /// 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>, } impl<'a> Painter<'a> { @@ -82,18 +103,18 @@ impl<'a> Painter<'a> { images, path_cache: RefCell::new(VectorPathCache::new()), scene_cache: Some(scene_cache), // Store reference to scene cache - cache_hits: RefCell::new(0), + cache_hits: Cell::new(0), policy, variant_key, - promoted_skip: None, + promoted_blits: None, } } - /// Set the promoted-skip set. Nodes in this set will be skipped during - /// `draw_render_commands` because they are blitted from the compositor - /// cache instead. - pub fn with_promoted_skip(mut self, skip: &'a std::collections::HashSet) -> Self { - self.promoted_skip = Some(skip); + /// 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 { + self.promoted_blits = Some(blits); self } @@ -210,11 +231,15 @@ impl<'a> Painter<'a> { canvas.restore(); } - /// If opacity < 1.0, wrap drawing in a save_layer_alpha; else draw directly. - pub fn with_opacity(&self, opacity: f32, f: F) { + /// If opacity < 1.0, wrap drawing in a bounded save_layer_alpha; else draw directly. + /// + /// Providing tight bounds limits the offscreen GPU buffer to the node's + /// actual extent instead of the full canvas (~100x smaller). See item 12 + /// in `docs/wg/feat-2d/optimization.md`. + pub fn with_opacity(&self, opacity: f32, bounds: Option<&Rect>, f: F) { let canvas = self.canvas; if opacity < 1.0 { - canvas.save_layer_alpha(None, (opacity * 255.0) as u32); + canvas.save_layer_alpha(bounds.copied(), (opacity * 255.0) as u32); f(); canvas.restore(); } else { @@ -231,6 +256,12 @@ impl<'a> Painter<'a> { /// - shape: Shape with bounds in LOCAL coordinates (0,0 based) /// - effects: Layer effects for drop shadow expansion /// - transform: Transform matrix to convert local bounds to world coordinates + /// - stroke_path: Optional stroke path for bounds expansion (Center/Outside strokes) + /// + /// When a stroke path extends beyond `shape.rect` (Center or Outside + /// alignment), the save_layer bounds must include the stroke geometry + /// to avoid clipping. Without this, `save_layer_alpha` would clip the + /// outer portion of Center/Outside strokes. /// /// # TODO: Move to Geometry Stage /// @@ -240,14 +271,26 @@ impl<'a> Painter<'a> { /// redundant calculations during rendering. The geometry cache already computes `absolute_render_bounds` /// which includes effect expansion - blend mode isolation bounds could be added as a separate field /// or computed alongside render bounds. - fn compute_blend_mode_bounds( + fn compute_blend_mode_bounds_with_stroke( shape: &PainterShape, effects: &LayerEffects, transform: &[[f32; 3]; 2], + stroke_path: Option<&skia_safe::Path>, ) -> Rect { // Start with local bounds (0,0 based) let mut local_bounds = shape.rect; + // Expand for stroke path that extends beyond fill bounds + if let Some(path) = stroke_path { + let stroke_bounds = path.bounds(); + local_bounds = Rect::from_ltrb( + local_bounds.left().min(stroke_bounds.left()), + local_bounds.top().min(stroke_bounds.top()), + local_bounds.right().max(stroke_bounds.right()), + local_bounds.bottom().max(stroke_bounds.bottom()), + ); + } + // Expand for drop shadows in local space (drawn inside blend mode isolation) for shadow in &effects.shadows { if let FilterShadowEffect::DropShadow(ds) = shadow { @@ -303,10 +346,16 @@ impl<'a> Painter<'a> { ) } - /// If blend mode is not PassThrough, wrap drawing in a bounds-optimized save_layer. + /// If blend mode is not PassThrough or Normal, wrap drawing in a bounds-optimized save_layer. /// /// Performance: Uses bounds-based save_layer to limit offscreen buffer size (~100x smaller). - /// Spec compliance: Preserves isolation semantics - all blend modes (including Normal) are isolated. + /// + /// **Normal blend mode fast path:** For leaf nodes (Shape, Text, Vector), + /// `Blend(Normal)` is mathematically equivalent to `PassThrough` because + /// SrcOver compositing is associative — drawing fills/strokes into an + /// offscreen then blitting with SrcOver produces identical results to + /// drawing directly. This method is only called for leaf nodes (not + /// containers), so we skip the save_layer entirely for Normal blend. /// /// Bounds safety: Includes drop shadows which extend beyond base shape bounds. /// @@ -317,21 +366,29 @@ impl<'a> Painter<'a> { shape: &PainterShape, effects: &LayerEffects, transform: &[[f32; 3]; 2], + stroke_path: Option<&skia_safe::Path>, f: F, ) { match layer_blend_mode { - LayerBlendMode::PassThrough => { - // No isolation - draw directly (fast path) + LayerBlendMode::PassThrough | LayerBlendMode::Blend(BlendMode::Normal) => { + // No isolation needed — draw directly. + // Normal (SrcOver) on a leaf node is equivalent to PassThrough + // because there are no children whose blend modes could interact + // with content beneath the node. f(); } LayerBlendMode::Blend(blend_mode) => { - // Compute safe bounds in world coordinates (shape.rect is in local space) - let bounds = Self::compute_blend_mode_bounds(shape, effects, transform); + // Non-Normal blend modes (Multiply, Screen, etc.) need isolation. + let bounds = Self::compute_blend_mode_bounds_with_stroke( + shape, + effects, + transform, + stroke_path, + ); let mut paint = SkPaint::default(); paint.set_blend_mode(blend_mode.into()); - // Use bounds-based save_layer (much smaller than full canvas) let layer_rec = SaveLayerRec::default().bounds(&bounds).paint(&paint); self.canvas.save_layer(&layer_rec); @@ -341,23 +398,74 @@ impl<'a> Painter<'a> { } } + /// Combined blend mode + opacity isolation in a single save_layer. + /// + /// When a node has no effects that require separate opacity isolation + /// (no shadows, blur, glass, or backdrop blur), we can merge the + /// opacity into the blend mode save_layer paint, eliminating one + /// GPU surface allocation per node. + /// + /// For PassThrough and Normal blend modes on leaf nodes, falls back to + /// save_layer_alpha with bounds optimization (no blend isolation needed). + fn with_blendmode_and_opacity( + &self, + layer_blend_mode: LayerBlendMode, + opacity: f32, + shape: &PainterShape, + effects: &LayerEffects, + transform: &[[f32; 3]; 2], + stroke_path: Option<&skia_safe::Path>, + f: F, + ) { + match layer_blend_mode { + LayerBlendMode::PassThrough | LayerBlendMode::Blend(BlendMode::Normal) => { + // Normal (SrcOver) on a leaf node needs no blend isolation. + // Just apply opacity via save_layer_alpha if needed. + if opacity < 1.0 { + let bounds = Self::compute_blend_mode_bounds_with_stroke( + shape, + effects, + transform, + stroke_path, + ); + self.canvas + .save_layer_alpha(bounds, (opacity * 255.0) as u32); + f(); + self.canvas.restore(); + } else { + f(); + } + } + LayerBlendMode::Blend(blend_mode) => { + // Non-Normal blend modes need isolation. + let bounds = Self::compute_blend_mode_bounds_with_stroke( + shape, + effects, + transform, + stroke_path, + ); + + let mut paint = SkPaint::default(); + paint.set_blend_mode(blend_mode.into()); + // Merge opacity into the blend paint alpha — single GPU surface + // instead of nested save_layer(blend) + save_layer_alpha(opacity). + if opacity < 1.0 { + paint.set_alpha_f(opacity); + } + + let layer_rec = SaveLayerRec::default().bounds(&bounds).paint(&paint); + self.canvas.save_layer(&layer_rec); + f(); + self.canvas.restore(); + } + } + } + /// Helper method to apply clipping to a region with optional corner radius pub fn with_clip(&self, shape: &PainterShape, f: F) { let canvas = self.canvas; canvas.save(); - - // Try to use the most efficient clipping method based on shape type - if let Some(rect) = shape.rect_shape { - // Simple rectangle - use clip_rect (fastest) - canvas.clip_rect(rect, None, true); - } else if let Some(rrect) = &shape.rrect { - // Rounded rectangle - use clip_rrect (faster than path) - canvas.clip_rrect(rrect, None, true); - } else { - // Complex shape - fall back to path clipping - canvas.clip_path(&shape.to_path(), None, true); - } - + shape.clip_on_canvas(canvas); f(); canvas.restore(); } @@ -630,7 +738,7 @@ impl<'a> Painter<'a> { if let Some(filter) = image_filter { // 1) Clip to the shape canvas.save(); - canvas.clip_path(&shape.to_path(), None, true); + shape.clip_on_canvas(canvas); // 2) Use a SaveLayerRec with a backdrop filter so that everything behind is blurred let layer_rec = SaveLayerRec::default() @@ -687,13 +795,7 @@ impl<'a> Painter<'a> { canvas.translate((bounds.x(), bounds.y())); // Clip using the most efficient method based on shape type - if let Some(rect) = shape.rect_shape { - canvas.clip_rect(rect, None, true); - } else if let Some(rrect) = &shape.rrect { - canvas.clip_rrect(rrect, None, true); - } else { - canvas.clip_path(&shape.to_path(), None, true); - } + shape.clip_on_canvas(canvas); // SaveLayer with backdrop captures background and applies filter // Use bounds relative to translated origin (0,0 based after translation) @@ -750,7 +852,132 @@ impl<'a> Painter<'a> { (shape.rect.width(), shape.rect.height()), self.images, ) { - self.canvas.draw_path(&shape.to_path(), &paint); + shape.draw_on_canvas(self.canvas, &paint); + } + } + + /// Draw fills at pre-translated coordinates, avoiding canvas save/concat/restore. + /// + /// For pure-translation transforms, this pre-applies the translation to shape + /// coordinates and draws directly. Reduces the SkPicture from 4 commands + /// (save + concat + draw + restore) to 1 command (draw) per node. + #[inline] + fn draw_fills_translated( + &self, + shape: &PainterShape, + fills: &[Paint], + tx: f32, + ty: f32, + ) { + if fills.is_empty() { + return; + } + if let Some(paint) = paint::sk_paint_stack( + fills, + (shape.rect.width(), shape.rect.height()), + self.images, + ) { + self.draw_shape_at_offset(shape, &paint, tx, ty); + } + } + + /// Draw fills at pre-translated coordinates with opacity baked into paint alpha. + #[inline] + fn draw_fills_translated_with_opacity( + &self, + shape: &PainterShape, + fills: &[Paint], + opacity: f32, + tx: f32, + ty: f32, + ) { + if fills.is_empty() { + return; + } + if let Some(mut paint) = paint::sk_paint_stack( + fills, + (shape.rect.width(), shape.rect.height()), + self.images, + ) { + paint.set_alpha_f(paint.alpha_f() * opacity); + self.draw_shape_at_offset(shape, &paint, tx, ty); + } + } + + /// Draw a shape at an offset without canvas save/concat/restore. + /// + /// For rect, rrect, and oval shapes, translates coordinates directly. + /// For path shapes, falls back to save/translate/draw/restore. + #[inline] + fn draw_shape_at_offset( + &self, + shape: &PainterShape, + paint: &SkPaint, + tx: f32, + ty: f32, + ) { + if tx == 0.0 && ty == 0.0 { + shape.draw_on_canvas(self.canvas, paint); + } else if let Some(rect) = shape.rect_shape { + self.canvas.draw_rect(rect.with_offset((tx, ty)), paint); + } else if let Some(rrect) = &shape.rrect { + self.canvas + .draw_rrect(rrect.with_offset((tx, ty)), paint); + } else if let Some(oval) = &shape.oval { + self.canvas.draw_oval(oval.with_offset((tx, ty)), paint); + } else { + // Path: use save/translate/draw/restore + self.canvas.save(); + self.canvas.translate((tx, ty)); + shape.draw_on_canvas(self.canvas, paint); + self.canvas.restore(); + } + } + + /// Draw fills with layer opacity baked into the paint alpha. + /// + /// Eliminates the save_layer_alpha GPU surface allocation for fills-only + /// leaf nodes. The opacity is multiplied into the paint's alpha channel, + /// producing identical results to wrapping in save_layer_alpha when there + /// is only a single draw call (no strokes overlapping fills). + #[inline] + fn draw_fills_with_opacity(&self, shape: &PainterShape, fills: &[Paint], opacity: f32) { + if fills.is_empty() { + return; + } + if let Some(mut paint) = paint::sk_paint_stack( + fills, + (shape.rect.width(), shape.rect.height()), + self.images, + ) { + paint.set_alpha_f(paint.alpha_f() * opacity); + shape.draw_on_canvas(self.canvas, &paint); + } + } + + /// Draw fills using a custom path (instead of `shape.to_path()`) with opacity. + /// + /// Used for the non-overlapping fill path optimization: when stroke overlaps + /// fill (Inside/Center), we draw fills using a path with the stroke region + /// subtracted (PathOp::Difference). This eliminates the overlap, allowing + /// per-paint-alpha opacity folding without double-blending artifacts. + fn draw_path_fills_with_opacity( + &self, + path: &skia_safe::Path, + shape: &PainterShape, + fills: &[Paint], + opacity: f32, + ) { + if fills.is_empty() { + return; + } + if let Some(mut paint) = paint::sk_paint_stack( + fills, + (shape.rect.width(), shape.rect.height()), + self.images, + ) { + paint.set_alpha_f(paint.alpha_f() * opacity); + self.canvas.draw_path(path, &paint); } } @@ -798,6 +1025,28 @@ impl<'a> Painter<'a> { } } + /// Draw stroke path with layer opacity baked into the paint alpha. + #[inline] + fn draw_stroke_path_with_opacity( + &self, + shape: &PainterShape, + stroke_path: &skia_safe::Path, + strokes: &[Paint], + opacity: f32, + ) { + if strokes.is_empty() { + return; + } + if let Some(mut paint) = paint::sk_paint_stack( + strokes, + (shape.rect.width(), shape.rect.height()), + self.images, + ) { + paint.set_alpha_f(paint.alpha_f() * opacity); + self.canvas.draw_path(stroke_path, &paint); + } + } + /// Draw stroke decoration markers at the start/end endpoints of a path. pub fn draw_stroke_decorations( &self, @@ -1025,124 +1274,396 @@ impl<'a> Painter<'a> { fn draw_layer_standard(&self, layer: &PainterPictureLayer) { match layer { PainterPictureLayer::Shape(shape_layer) => { - self.with_blendmode( + let effects = &shape_layer.effects; + let opacity = shape_layer.base.opacity; + + // Trivial-node fast path: opacity=1.0, no effects, Normal/PassThrough blend. + // Skips the entire effects pipeline, blend wrapper, opacity wrapper, + // and all associated condition checks / closure creation. + // This is the most common case for simple fill/stroke nodes. + if opacity >= 1.0 + && effects.is_empty() + && matches!( + shape_layer.base.blend_mode, + LayerBlendMode::PassThrough | LayerBlendMode::Blend(BlendMode::Normal) + ) + { + let m = &shape_layer.base.transform.matrix; + + // Translate-fold fast path: for pure-translation transforms with + // no clip path and no strokes, pre-apply the translation to shape + // coordinates instead of save/concat(matrix)/restore. This reduces + // the recorded SkPicture from 4 commands to 1 per node. + // + // Only applies to fills-only nodes: when strokes are present, the + // shared save/concat/restore wrapping both fills and strokes is + // cheaper than splitting into separate draw + save/translate/restore. + if shape_layer.base.clip_path.is_none() + && shape_layer.stroke_path.is_none() + && m[0][0] == 1.0 + && m[1][0] == 0.0 + && m[0][1] == 0.0 + && m[1][1] == 1.0 + { + let tx = m[0][2]; + let ty = m[1][2]; + if self.policy.render_fills() { + self.draw_fills_translated( + &shape_layer.shape, + &shape_layer.fills, + tx, + ty, + ); + } + return; + } + + self.with_transform(&shape_layer.base.transform.matrix, || { + let shape = &shape_layer.shape; + let clip_path = &shape_layer.base.clip_path; + self.with_optional_clip_path(clip_path.as_ref(), || { + if self.policy.render_fills() { + self.draw_fills(shape, &shape_layer.fills); + } + if self.policy.render_strokes() { + if let Some(path) = &shape_layer.stroke_path { + self.draw_stroke_path(shape, path, &shape_layer.strokes); + } + } + }); + }); + return; + } + + let can_fold_opacity = opacity < 1.0 && !effects.needs_opacity_isolation(); + + // Paint-alpha fast path: for simple leaf nodes with + // PassThrough/Normal blend and no effects, fold opacity directly + // into each paint's alpha — zero GPU surface allocations. + // + // This eliminates save_layer_alpha, which is the #1 GPU bottleneck + // for semi-transparent scenes during panning. + // + // Per the SVG/CSS spec (and Chromium's implementation), node-level + // opacity requires group isolation: fill+stroke are drawn at full + // opacity into an offscreen surface, then composited at the node's + // opacity. Per-paint alpha is only spec-correct when there is no + // geometric overlap between fill and stroke. + // + // When stroke overlaps fill (Inside/Center), we use a pre-computed + // non-overlapping fill path (fill minus stroke via PathOp::Difference) + // to eliminate the overlap at zero GPU cost. If that path is + // unavailable (PathOp failed), we fall back to save_layer_alpha + // with bounds expanded to include the stroke path. + // See docs/wg/feat-2d/stroke-fill-opacity.md + let has_noises = !shape_layer.fills.is_empty() && !effects.noises.is_empty(); + let is_simple_blend = matches!( shape_layer.base.blend_mode, - &shape_layer.shape, - &shape_layer.effects, - &shape_layer.base.transform.matrix, - || { - self.with_transform(&shape_layer.base.transform.matrix, || { - let shape = &shape_layer.shape; - let effect_ref = &shape_layer.effects; - let clip_path = &shape_layer.base.clip_path; - let draw_content = || { - self.with_opacity(shape_layer.base.opacity, || { - // 1. Fills - if self.policy.render_fills() { - self.draw_fills(shape, &shape_layer.fills); + LayerBlendMode::PassThrough | LayerBlendMode::Blend(BlendMode::Normal) + ); + let has_stroke_fill_overlap = shape_layer.stroke_overlaps_fill + && !shape_layer.fills.is_empty() + && !shape_layer.strokes.is_empty(); + let has_non_overlapping_fill = shape_layer.non_overlapping_fill_path.is_some(); + let can_fold_into_paint = can_fold_opacity + && is_simple_blend + && !has_noises + && !effects.has_expensive_effects() + && (!has_stroke_fill_overlap || has_non_overlapping_fill); + + if can_fold_into_paint { + // Zero save_layers: opacity folded into paint alpha. + let m = &shape_layer.base.transform.matrix; + + // Translate-fold: skip save/concat/restore for pure translations. + // Fall through to the normal path when stroke markers are + // present — draw_stroke_decorations requires the full + // transform context. + let has_markers = shape_layer.marker_start_shape.has_marker() + || shape_layer.marker_end_shape.has_marker(); + if shape_layer.base.clip_path.is_none() + && shape_layer.non_overlapping_fill_path.is_none() + && !has_markers + && m[0][0] == 1.0 + && m[1][0] == 0.0 + && m[0][1] == 0.0 + && m[1][1] == 1.0 + { + let tx = m[0][2]; + let ty = m[1][2]; + if self.policy.render_fills() { + self.draw_fills_translated_with_opacity( + &shape_layer.shape, + &shape_layer.fills, + opacity, + tx, + ty, + ); + } + if self.policy.render_strokes() { + if let Some(path) = &shape_layer.stroke_path { + if tx == 0.0 && ty == 0.0 { + self.draw_stroke_path_with_opacity( + &shape_layer.shape, + path, + &shape_layer.strokes, + opacity, + ); + } else { + self.canvas.save(); + self.canvas.translate((tx, ty)); + self.draw_stroke_path_with_opacity( + &shape_layer.shape, + path, + &shape_layer.strokes, + opacity, + ); + self.canvas.restore(); + } + } + } + return; + } - // 2. Noise (only if fills are visible) - if !shape_layer.fills.is_empty() - && !effect_ref.noises.is_empty() - { - self.draw_noise_effects(shape, &effect_ref.noises); - } - } + self.with_transform(&shape_layer.base.transform.matrix, || { + let shape = &shape_layer.shape; + let clip_path = &shape_layer.base.clip_path; + self.with_optional_clip_path(clip_path.as_ref(), || { + if self.policy.render_fills() { + // When stroke overlaps fill, use the pre-computed + // non-overlapping fill path to avoid double-blending. + if let Some(ref nof_path) = shape_layer.non_overlapping_fill_path { + self.draw_path_fills_with_opacity( + nof_path, + shape, + &shape_layer.fills, + opacity, + ); + } else { + self.draw_fills_with_opacity( + shape, + &shape_layer.fills, + opacity, + ); + } + } + if self.policy.render_strokes() { + if let Some(path) = &shape_layer.stroke_path { + self.draw_stroke_path_with_opacity( + shape, + path, + &shape_layer.strokes, + opacity, + ); + } + self.draw_stroke_decorations( + shape, + &shape_layer.strokes, + shape_layer.stroke_width, + shape_layer.marker_start_shape, + shape_layer.marker_end_shape, + ); + } + }); + }); + return; + } - // 3. Strokes - if self.policy.render_strokes() { - if let Some(path) = &shape_layer.stroke_path { - self.draw_stroke_path( - shape, - path, - &shape_layer.strokes, - ); - } + let blend_wrapper = |f: &dyn Fn()| { + if can_fold_opacity { + // Merged path: single save_layer with blend + opacity + self.with_blendmode_and_opacity( + shape_layer.base.blend_mode, + opacity, + &shape_layer.shape, + effects, + &shape_layer.base.transform.matrix, + shape_layer.stroke_path.as_ref(), + f, + ); + } else { + // Standard path: separate blend + opacity save_layers + self.with_blendmode( + shape_layer.base.blend_mode, + &shape_layer.shape, + effects, + &shape_layer.base.transform.matrix, + shape_layer.stroke_path.as_ref(), + f, + ); + } + }; - // 4. Stroke decorations (markers at endpoints) - self.draw_stroke_decorations( + blend_wrapper(&|| { + self.with_transform(&shape_layer.base.transform.matrix, || { + let shape = &shape_layer.shape; + let effect_ref = &shape_layer.effects; + let clip_path = &shape_layer.base.clip_path; + let draw_content = || { + let inner_draw = || { + // 1. Fills + if self.policy.render_fills() { + self.draw_fills(shape, &shape_layer.fills); + + // 2. Noise (only if fills are visible) + if !shape_layer.fills.is_empty() + && !effect_ref.noises.is_empty() + { + self.draw_noise_effects(shape, &effect_ref.noises); + } + } + + // 3. Strokes + if self.policy.render_strokes() { + if let Some(path) = &shape_layer.stroke_path { + self.draw_stroke_path( shape, + path, &shape_layer.strokes, - shape_layer.stroke_width, - shape_layer.marker_start_shape, - shape_layer.marker_end_shape, ); } - }); + + // 4. Stroke decorations (markers at endpoints) + self.draw_stroke_decorations( + shape, + &shape_layer.strokes, + shape_layer.stroke_width, + shape_layer.marker_start_shape, + shape_layer.marker_end_shape, + ); + } }; - self.with_optional_clip_path(clip_path.as_ref(), || { - self.draw_shape_with_effects(effect_ref, shape, draw_content); - }); + if can_fold_opacity { + inner_draw(); + } else { + // Compute tight local bounds for the opacity + // save_layer: shape rect expanded by stroke path. + let mut local_bounds = shape.rect; + if let Some(sp) = &shape_layer.stroke_path { + let sb = sp.bounds(); + local_bounds = Rect::from_ltrb( + local_bounds.left().min(sb.left()), + local_bounds.top().min(sb.top()), + local_bounds.right().max(sb.right()), + local_bounds.bottom().max(sb.bottom()), + ); + } + self.with_opacity( + opacity, + Some(&local_bounds), + inner_draw, + ); + } + }; + self.with_optional_clip_path(clip_path.as_ref(), || { + self.draw_shape_with_effects(effect_ref, shape, draw_content); }); - }, - ); + }); + }); } PainterPictureLayer::Text(text_layer) => { - self.with_blendmode( - text_layer.base.blend_mode, - &text_layer.shape, - &text_layer.effects, - &text_layer.base.transform.matrix, - || { - self.with_transform(&text_layer.base.transform.matrix, || { - let effects = &text_layer.effects; - let clip_path = &text_layer.base.clip_path; - - let paragraph = self.cached_paragraph( - &text_layer.base.id, - &text_layer.text, - &text_layer.width, - &text_layer.max_lines, - &text_layer.ellipsis, - &text_layer.fills, - &text_layer.text_align, - &text_layer.text_align_vertical, - &text_layer.text_style, - ); - let layout_size = { - let para = paragraph.borrow(); - (para.max_width(), para.height()) - }; - - let layout_height = layout_size.1; - let container_height = text_layer.height.unwrap_or(layout_height); - let y_offset = match text_layer.height { - Some(h) => match text_layer.text_align_vertical { - TextAlignVertical::Top => 0.0, - TextAlignVertical::Center => (h - layout_height) / 2.0, - TextAlignVertical::Bottom => h - layout_height, - }, - None => 0.0, - }; + let text_effects = &text_layer.effects; + let text_opacity = text_layer.base.opacity; + let text_can_fold = text_opacity < 1.0 && !text_effects.needs_opacity_isolation(); + + let text_blend_wrapper = |f: &dyn Fn()| { + if text_can_fold { + self.with_blendmode_and_opacity( + text_layer.base.blend_mode, + text_opacity, + &text_layer.shape, + text_effects, + &text_layer.base.transform.matrix, + None, + f, + ); + } else { + self.with_blendmode( + text_layer.base.blend_mode, + &text_layer.shape, + text_effects, + &text_layer.base.transform.matrix, + None, + f, + ); + } + }; - let draw_content = || { - self.with_opacity(text_layer.base.opacity, || { - // Allow stroke-only text: paragraph paint may be empty, but we can still draw strokes. - if text_layer.fills.is_empty() - && (text_layer.strokes.is_empty() - || text_layer.stroke_width <= 0.0) - { - return; - } - self.draw_text_paragraph( - ¶graph, - if self.policy.render_strokes() { - &text_layer.strokes - } else { - &[] - }, - if self.policy.render_strokes() { - text_layer.stroke_width - } else { - 0.0 - }, - &text_layer.stroke_align, - (layout_size.0, container_height), - y_offset, - self.policy.render_fills(), - ); - }); + text_blend_wrapper(&|| { + self.with_transform(&text_layer.base.transform.matrix, || { + let effects = &text_layer.effects; + let clip_path = &text_layer.base.clip_path; + + let paragraph = self.cached_paragraph( + &text_layer.base.id, + &text_layer.text, + &text_layer.width, + &text_layer.max_lines, + &text_layer.ellipsis, + &text_layer.fills, + &text_layer.text_align, + &text_layer.text_align_vertical, + &text_layer.text_style, + ); + let layout_size = { + let para = paragraph.borrow(); + (para.max_width(), para.height()) + }; + + let layout_height = layout_size.1; + let container_height = text_layer.height.unwrap_or(layout_height); + let y_offset = match text_layer.height { + Some(h) => match text_layer.text_align_vertical { + TextAlignVertical::Top => 0.0, + TextAlignVertical::Center => (h - layout_height) / 2.0, + TextAlignVertical::Bottom => h - layout_height, + }, + None => 0.0, + }; + + let draw_content = || { + let inner_text_draw = || { + // Allow stroke-only text: paragraph paint may be empty, but we can still draw strokes. + if text_layer.fills.is_empty() + && (text_layer.strokes.is_empty() + || text_layer.stroke_width <= 0.0) + { + return; + } + self.draw_text_paragraph( + ¶graph, + if self.policy.render_strokes() { + &text_layer.strokes + } else { + &[] + }, + if self.policy.render_strokes() { + text_layer.stroke_width + } else { + 0.0 + }, + &text_layer.stroke_align, + (layout_size.0, container_height), + y_offset, + self.policy.render_fills(), + ); }; + if text_can_fold { + inner_text_draw(); + } else { + let text_bounds = Rect::from_xywh( + 0.0, + y_offset, + layout_size.0, + container_height, + ); + self.with_opacity( + text_opacity, + Some(&text_bounds), + inner_text_draw, + ); + } + }; let apply_effects = || { if let Some(blur) = &effects.backdrop_blur { @@ -1182,26 +1703,47 @@ impl<'a> Painter<'a> { } }; - self.with_optional_clip_path(clip_path.as_ref(), || { - draw_with_effects(); - }); + self.with_optional_clip_path(clip_path.as_ref(), || { + draw_with_effects(); }); - }, - ); + }); + }); } PainterPictureLayer::Vector(vector_layer) => { - self.with_blendmode( - vector_layer.base.blend_mode, - &vector_layer.shape, - &vector_layer.effects, - &vector_layer.base.transform.matrix, - || { - self.with_transform(&vector_layer.base.transform.matrix, || { - let shape = &vector_layer.shape; - let effect_ref = &vector_layer.effects; - let clip_path = &vector_layer.base.clip_path; - let draw_content = || { - self.with_opacity(vector_layer.base.opacity, || { + let vec_effects = &vector_layer.effects; + let vec_opacity = vector_layer.base.opacity; + let vec_can_fold = vec_opacity < 1.0 && !vec_effects.needs_opacity_isolation(); + + let vec_blend_wrapper = |f: &dyn Fn()| { + if vec_can_fold { + self.with_blendmode_and_opacity( + vector_layer.base.blend_mode, + vec_opacity, + &vector_layer.shape, + vec_effects, + &vector_layer.base.transform.matrix, + None, + f, + ); + } else { + self.with_blendmode( + vector_layer.base.blend_mode, + &vector_layer.shape, + vec_effects, + &vector_layer.base.transform.matrix, + None, + f, + ); + } + }; + + vec_blend_wrapper(&|| { + self.with_transform(&vector_layer.base.transform.matrix, || { + let shape = &vector_layer.shape; + let effect_ref = &vector_layer.effects; + let clip_path = &vector_layer.base.clip_path; + let draw_content = || { + let inner_vec_draw = || { // Use VNPainter for vector network rendering let vn_painter = crate::vectornetwork::vn_painter::VNPainter::new_with_images( @@ -1329,15 +1871,23 @@ impl<'a> Painter<'a> { } } } - } - }); + } }; - self.with_optional_clip_path(clip_path.as_ref(), || { - self.draw_shape_with_effects(effect_ref, shape, draw_content); - }); + if vec_can_fold { + inner_vec_draw(); + } else { + self.with_opacity( + vec_opacity, + Some(&shape.rect), + inner_vec_draw, + ); + } + }; + self.with_optional_clip_path(clip_path.as_ref(), || { + self.draw_shape_with_effects(effect_ref, shape, draw_content); }); - }, - ); + }); + }); } } } @@ -1490,9 +2040,26 @@ impl<'a> Painter<'a> { for command in commands { match command { PainterRenderCommand::Draw(layer) => { - // Skip nodes that are drawn via the compositor cache. - if let Some(skip) = self.promoted_skip { - if skip.contains(layer.id()) { + // Blit promoted nodes from the compositor cache inline at + // their correct z-position. This preserves z-order when a + // live parent (e.g. Container with fills) has promoted + // children (e.g. nodes with blur/shadow effects). + if let Some(blits) = self.promoted_blits { + if let Some(blit) = blits.get(layer.id()) { + let mut paint = SkPaint::default(); + if blit.opacity < 1.0 { + paint.set_alpha_f(blit.opacity); + } + paint.set_blend_mode(blit.blend_mode); + self.canvas.draw_image_rect( + &*blit.image, + Some(( + &blit.src_rect, + skia_safe::canvas::SrcRectConstraint::Fast, + )), + blit.dst_rect, + &paint, + ); continue; } } @@ -1502,7 +2069,7 @@ impl<'a> Painter<'a> { scene_cache.get_node_picture_variant(layer.id(), self.variant_key) { self.canvas.draw_picture(pic, None, None); - *self.cache_hits.borrow_mut() += 1; + self.cache_hits.set(self.cache_hits.get() + 1); continue; } } @@ -1857,7 +2424,7 @@ impl<'a> Painter<'a> { if let Some(pic) = scene_cache.get_node_picture_variant(&entry.id, self.variant_key) { self.canvas.draw_picture(pic, None, None); - *self.cache_hits.borrow_mut() += 1; + self.cache_hits.set(self.cache_hits.get() + 1); continue; } } @@ -1866,6 +2433,6 @@ impl<'a> Painter<'a> { } pub fn cache_picture_hits(&self) -> usize { - *self.cache_hits.borrow() + self.cache_hits.get() } } diff --git a/crates/grida-canvas/src/painter/painter_debug_node.rs b/crates/grida-canvas/src/painter/painter_debug_node.rs index 4b5a6eb528..547aae59b9 100644 --- a/crates/grida-canvas/src/painter/painter_debug_node.rs +++ b/crates/grida-canvas/src/painter/painter_debug_node.rs @@ -36,12 +36,13 @@ impl<'a> NodePainter<'a> { let identity_transform = math2::transform::AffineTransform::identity().matrix; self.painter .draw_shape_with_effects(&node.effects, &shape, || { - self.painter.with_opacity(node.opacity, || { + self.painter.with_opacity(node.opacity, None, || { self.painter.with_blendmode( node.blend_mode, &shape, &node.effects, &identity_transform, + None, || { self.painter.draw_fills(&shape, &node.fills); let stroke_width = node.render_bounds_stroke_width(); @@ -72,12 +73,13 @@ impl<'a> NodePainter<'a> { self.painter .draw_shape_with_effects(&node.effects, &shape, || { - self.painter.with_opacity(node.opacity, || { + self.painter.with_opacity(node.opacity, None, || { self.painter.with_blendmode( node.blend_mode, &shape, &node.effects, &identity_transform, + None, || { // Use the single image fill directly - aligns with web development patterns // where elements have one image source @@ -115,12 +117,13 @@ impl<'a> NodePainter<'a> { let identity_transform = math2::transform::AffineTransform::identity().matrix; self.painter .draw_shape_with_effects(&node.effects, &shape, || { - self.painter.with_opacity(node.opacity, || { + self.painter.with_opacity(node.opacity, None, || { self.painter.with_blendmode( node.blend_mode, &shape, &node.effects, &identity_transform, + None, || { self.painter.draw_fills(&shape, &node.fills); self.painter.draw_strokes( @@ -146,12 +149,13 @@ impl<'a> NodePainter<'a> { let node_enum = Node::Line(node.clone()); let shape = build_shape(&node_enum, &DUMMY_BOUNDS); - self.painter.with_opacity(node.opacity, || { + self.painter.with_opacity(node.opacity, None, || { self.painter.with_blendmode( node.blend_mode, &shape, &node.effects, &node.transform.matrix, + None, || { self.painter.draw_strokes( &shape, @@ -187,12 +191,13 @@ impl<'a> NodePainter<'a> { let identity_transform = math2::transform::AffineTransform::identity().matrix; self.painter .draw_shape_with_effects(&node.effects, &shape, || { - self.painter.with_opacity(node.opacity, || { + self.painter.with_opacity(node.opacity, None, || { self.painter.with_blendmode( node.blend_mode, &shape, &node.effects, &identity_transform, + None, || { if !node.fills.is_empty() { self.painter.draw_fills(&shape, &node.fills); @@ -234,12 +239,13 @@ impl<'a> NodePainter<'a> { let identity_transform = math2::transform::AffineTransform::identity().matrix; self.painter .draw_shape_with_effects(&node.effects, &shape, || { - self.painter.with_opacity(node.opacity, || { + self.painter.with_opacity(node.opacity, None, || { self.painter.with_blendmode( node.blend_mode, &shape, &node.effects, &identity_transform, + None, || { if !node.fills.is_empty() { self.painter.draw_fills(&shape, &node.fills); @@ -272,12 +278,13 @@ impl<'a> NodePainter<'a> { let identity_transform = math2::transform::AffineTransform::identity().matrix; self.painter .draw_shape_with_effects(&node.effects, &shape, || { - self.painter.with_opacity(node.opacity, || { + self.painter.with_opacity(node.opacity, None, || { self.painter.with_blendmode( node.blend_mode, &shape, &node.effects, &identity_transform, + None, || { self.painter.draw_fills(&shape, &node.fills); self.painter.draw_strokes( @@ -357,12 +364,13 @@ impl<'a> NodePainter<'a> { let shape = build_shape(&node_enum, &DUMMY_BOUNDS); // In debug rendering, transform is already applied, so bounds should be in local space (identity transform) let identity_transform = math2::transform::AffineTransform::identity().matrix; - self.painter.with_opacity(node.opacity, || { + self.painter.with_opacity(node.opacity, None, || { self.painter.with_blendmode( node.blend_mode, &shape, &node.effects, &identity_transform, + None, || { self.painter.draw_text_span( &dummy_id, @@ -403,7 +411,7 @@ impl<'a> NodePainter<'a> { active: true, }); - self.painter.with_opacity(node.opacity, || { + self.painter.with_opacity(node.opacity, None, || { self.painter.draw_fills(&shape, std::slice::from_ref(&fill)); self.painter.draw_strokes( &shape, @@ -428,7 +436,7 @@ impl<'a> NodePainter<'a> { cache: &GeometryCache, ) { self.painter.with_transform_option(&node.transform, || { - self.painter.with_opacity(node.opacity, || { + self.painter.with_opacity(node.opacity, None, || { if let Some(children) = graph.get_children(id) { for child_id in children { if let Ok(child) = graph.get_node(child_id) { @@ -453,12 +461,13 @@ impl<'a> NodePainter<'a> { let identity_transform = math2::transform::AffineTransform::identity().matrix; self.painter .draw_shape_with_effects(&node.effects, &shape, || { - self.painter.with_opacity(node.opacity, || { + self.painter.with_opacity(node.opacity, None, || { self.painter.with_blendmode( node.blend_mode, &shape, &node.effects, &identity_transform, + None, || { if !node.fills.is_empty() { self.painter.draw_fills(&shape, &node.fills); @@ -533,7 +542,7 @@ impl<'a> NodePainter<'a> { .expect("Geometry must exist - pipeline bug"); self.painter.with_transform(&local_transform.matrix, || { - self.painter.with_opacity(n.opacity, || { + self.painter.with_opacity(n.opacity, None, || { // Geometry guaranteed to exist - no Option let bounds = cache .get_world_bounds(id) @@ -551,6 +560,7 @@ impl<'a> NodePainter<'a> { &shape, &n.effects, &identity_transform, + None, || { // Paint fills first self.painter.draw_fills(&shape, &n.fills); diff --git a/crates/grida-canvas/src/painter/shadow.rs b/crates/grida-canvas/src/painter/shadow.rs index 928842ccb0..cd3e860e27 100644 --- a/crates/grida-canvas/src/painter/shadow.rs +++ b/crates/grida-canvas/src/painter/shadow.rs @@ -39,14 +39,13 @@ pub fn drop_shadow_image_filter(shadow: &FeShadow) -> sk::ImageFilter { /// Draw a drop shadow behind the given shape on the provided canvas. pub fn draw_drop_shadow(canvas: &sk::Canvas, shape: &PainterShape, shadow: &FeShadow) { let color: sk::Color = shadow.color.into(); - let path = shape.to_path(); let mut paint = Paint::default(); let filter = drop_shadow_image_filter(shadow); paint.set_color(color); paint.set_image_filter(filter); paint.set_anti_alias(true); - canvas.draw_path(&path, &paint); + shape.draw_on_canvas(canvas, &paint); } pub fn inner_shadow_image_filter(shadow: &FeShadow) -> sk::ImageFilter { @@ -96,8 +95,7 @@ pub fn draw_inner_shadow(canvas: &sk::Canvas, shape: &PainterShape, shadow: &FeS shadow_paint.set_anti_alias(true); canvas.save(); - let path = shape.to_path(); - canvas.clip_path(&path, None, true); - canvas.draw_path(&path, &shadow_paint); + shape.clip_on_canvas(canvas); + shape.draw_on_canvas(canvas, &shadow_paint); canvas.restore(); } diff --git a/crates/grida-canvas/src/runtime/camera.rs b/crates/grida-canvas/src/runtime/camera.rs index 01410ff5f0..f00de1b614 100644 --- a/crates/grida-canvas/src/runtime/camera.rs +++ b/crates/grida-canvas/src/runtime/camera.rs @@ -1,5 +1,5 @@ use crate::node::schema::Size; -use math2::{quantize, rect, rect::Rectangle, transform::AffineTransform, vector2}; +use math2::{quantize, rect::Rectangle, transform::AffineTransform, vector2}; /// Classifies what changed between two consecutive camera states. /// @@ -27,20 +27,21 @@ pub enum CameraChangeKind { ZoomOut, /// Both translation and zoom changed (e.g. pinch gesture, scroll-wheel /// zoom at cursor which adjusts translation to keep the focal point fixed). - PanAndZoom, + /// The boolean indicates zoom direction: `true` = zoom-in, `false` = zoom-out. + PanAndZoom(bool), } impl CameraChangeKind { /// Returns `true` when zoom (scale) changed between frames. #[inline] pub fn zoom_changed(self) -> bool { - matches!(self, Self::ZoomIn | Self::ZoomOut | Self::PanAndZoom) + matches!(self, Self::ZoomIn | Self::ZoomOut | Self::PanAndZoom(_)) } /// Returns `true` when translation changed between frames. #[inline] pub fn pan_changed(self) -> bool { - matches!(self, Self::PanOnly | Self::PanAndZoom) + matches!(self, Self::PanOnly | Self::PanAndZoom(_)) } /// Returns `true` when any camera property changed. @@ -48,6 +49,19 @@ impl CameraChangeKind { pub fn any_changed(self) -> bool { !matches!(self, Self::None) } + + /// Short human-readable label for debug overlays and stats strings. + #[inline] + pub fn label(self) -> &'static str { + match self { + Self::None => "none", + Self::PanOnly => "pan", + Self::ZoomIn => "zoom-in", + Self::ZoomOut => "zoom-out", + Self::PanAndZoom(true) => "pan+zoom-in", + Self::PanAndZoom(false) => "pan+zoom-out", + } + } } /// A 2D camera that defines how world-space content is projected onto the screen. @@ -80,8 +94,26 @@ pub struct Camera2D { /// Maximum allowed zoom value pub max_zoom: f32, - /// The last time the camera was changed. - pub t: std::time::Instant, + /// The last time the camera was changed (host-time, milliseconds). + /// Uses `f64` instead of `std::time::Instant` to work correctly on + /// WASM where inter-frame scheduling must use host-provided time. + pub t: f64, + + // ── Cached derived values ────────────────────────────────────── + // Avoids recomputing sqrt, inverse, atan2, sin_cos on every call. + // Invalidated automatically whenever the transform changes. + + /// Cached zoom value (= 1 / scale_x). Updated on every zoom mutation. + /// Eliminates the `sqrt(a² + b²)` in `get_scale_x()` on every access. + cached_zoom: f32, + + /// Cached view matrix. Invalidated on any transform or size change. + /// Eliminates repeated `inverse()` + `compose()` per frame. + cached_view_matrix: Option, + + /// Cached inverse of the view matrix (= world_from_screen transform). + /// Computed alongside `cached_view_matrix`. + cached_view_matrix_inv: Option, } impl Camera2D { @@ -115,9 +147,12 @@ impl Camera2D { }, min_zoom: f32::MIN, max_zoom: f32::MAX, - t: std::time::Instant::now(), + t: 0.0, prev_transform: transform, prev_quantized_camera_transform: None, + cached_zoom: 1.0, + cached_view_matrix: None, + cached_view_matrix_inv: None, } } @@ -131,7 +166,10 @@ impl Camera2D { min_zoom, max_zoom, prev_quantized_camera_transform: None, - t: std::time::Instant::now(), + t: 0.0, + cached_zoom: 1.0, + cached_view_matrix: None, + cached_view_matrix_inv: None, }; c.set_zoom(1.0); c @@ -144,6 +182,10 @@ impl Camera2D { /// Sync the camera cache whenever the camera is changed. /// Returns true if the camera was changed. fn after_change(&mut self) -> bool { + // Invalidate cached derived values. + self.cached_view_matrix = None; + self.cached_view_matrix_inv = None; + let quantized = self.quantized_transform(); let changed = match self.prev_quantized_camera_transform { Some(prev) => prev != quantized, @@ -151,8 +193,10 @@ impl Camera2D { }; if changed { self.prev_quantized_camera_transform = Some(quantized); - - self.t = std::time::Instant::now(); + // Note: `t` is no longer set here. The application should call + // `camera.set_time(clock.now())` after mutating the camera, so + // that inter-frame scheduling uses host-provided time (required + // for correct WASM behavior). } changed } @@ -175,9 +219,28 @@ impl Camera2D { let zoom = zoom.clamp(self.min_zoom, self.max_zoom); let tx = self.transform.x(); let ty = self.transform.y(); - let (s, c) = self.transform.rotation().sin_cos(); + // Extract sin/cos directly from the matrix elements rather than going + // through atan2 → sin_cos. The current matrix is [[c*s, -s*s, tx], [s*s, c*s, ty]] + // where s = old_scale. We normalize by old_scale to recover (cos, sin). + let old_scale = 1.0 / self.cached_zoom; + let (sin, cos) = if old_scale > f32::EPSILON { + let inv_scale = 1.0 / old_scale; + ( + self.transform.matrix[1][0] * inv_scale, + self.transform.matrix[0][0] * inv_scale, + ) + } else { + // Fallback: degenerate scale, use atan2 + self.transform.rotation().sin_cos() + }; let scale = 1.0 / zoom; - self.transform.matrix = [[c * scale, -s * scale, tx], [s * scale, c * scale, ty]]; + self.transform.matrix = + [[cos * scale, -sin * scale, tx], [sin * scale, cos * scale, ty]]; + self.cached_zoom = zoom; + // Invalidate cached inverse so callers (e.g. set_zoom_at → + // screen_to_canvas_point) compute a fresh projection from the + // updated transform rather than using a stale cached value. + self.cached_view_matrix_inv = None; } /// Set zoom factor (1 = 100%). Preserves rotation & translation. @@ -199,20 +262,32 @@ impl Camera2D { } /// Get current zoom (1/scale). + /// + /// Returns the cached zoom value, avoiding the `sqrt(a² + b²)` that + /// `get_scale_x()` would require on every call. + #[inline] pub fn get_zoom(&self) -> f32 { - 1.0 / self.transform.get_scale_x() + self.cached_zoom } /// Returns the camera transform quantized to the nearest visible pixel. pub fn quantized_transform(&self) -> AffineTransform { - let zoom = self.get_zoom(); + let zoom = self.cached_zoom; let quant_zoom = quantize(zoom, Self::ZOOM_STEP); - // translate world-space camera position into screen space using - // the quantized zoom factor. This ensures snapping occurs in - // pixel space regardless of the zoom level or rotation. - let angle = self.transform.rotation(); - let (sin, cos) = angle.sin_cos(); + // Extract cos/sin directly from the matrix rather than going through + // atan2 → sin_cos. The matrix stores [[cos*scale, -sin*scale, ...], [sin*scale, cos*scale, ...]] + // where scale = 1/zoom. + let scale = 1.0 / zoom; + let (sin, cos) = if scale > f32::EPSILON { + let inv_scale = 1.0 / scale; + ( + self.transform.matrix[1][0] * inv_scale, + self.transform.matrix[0][0] * inv_scale, + ) + } else { + (0.0, 1.0) + }; let tx = self.transform.x(); let ty = self.transform.y(); @@ -232,15 +307,54 @@ impl Camera2D { } /// View matrix = center-screen translation × inverse(world→camera). + /// + /// Result is cached and reused until the next camera mutation. + #[inline] pub fn view_matrix(&self) -> AffineTransform { - let inv = self - .transform - .clone() - .inverse() - .unwrap_or_else(AffineTransform::identity); - let mut t = AffineTransform::identity(); - t.translate(self.size.width * 0.5, self.size.height * 0.5); - t.compose(&inv) + if let Some(cached) = self.cached_view_matrix { + return cached; + } + self.compute_view_matrix() + } + + /// Compute the view matrix without consulting the cache. + fn compute_view_matrix(&self) -> AffineTransform { + // For a camera transform [[a,c,tx],[b,d,ty]], the inverse is: + // det = a*d - b*c + // [[d/det, -c/det, -(d*tx - c*ty)/det], + // [-b/det, a/det, (b*tx - a*ty)/det]] + // Then compose with translate(w/2, h/2). + // + // We inline the inverse + compose to avoid intermediate allocations + // and reduce FLOPs. + let [[a, c, tx], [b, d, ty]] = self.transform.matrix; + let det = a * d - b * c; + if det.abs() < f32::EPSILON { + return AffineTransform::identity(); + } + let inv_det = 1.0 / det; + let ai = d * inv_det; + let bi = -b * inv_det; + let ci = -c * inv_det; + let di = a * inv_det; + let txi = -(ai * tx + ci * ty); + let tyi = -(bi * tx + di * ty); + + let hw = self.size.width * 0.5; + let hh = self.size.height * 0.5; + + // compose: translate(hw, hh) × inverse + // Result translation = hw + txi, hh + tyi + // (translate only affects tx/ty columns) + AffineTransform { + matrix: [[ai, ci, hw + txi], [bi, di, hh + tyi]], + } + } + + /// Set the host-time timestamp (ms) for this camera change. + /// Should be called by the application after mutating the camera. + pub fn set_time(&mut self, now: f64) { + self.t = now; } pub fn get_size(&self) -> &Size { @@ -252,28 +366,83 @@ impl Camera2D { self.after_change(); } + /// Populate the view-matrix cache so subsequent calls to `view_matrix()`, + /// `rect()`, and `screen_to_canvas_point()` within this frame are free. + /// + /// Call once at the start of each frame (before `draw()` / `frame()`). + pub fn warm_cache(&mut self) { + if self.cached_view_matrix.is_none() { + let vm = self.compute_view_matrix(); + self.cached_view_matrix = Some(vm); + self.cached_view_matrix_inv = vm.inverse(); + } + } + /// World‐space rect currently visible. + /// + /// Computes the visible rectangle directly from the camera transform, + /// avoiding the double matrix inversion that the old implementation + /// performed (`view_matrix()` → `inverse()` → transform 4 corners). + /// + /// The inverse of `view_matrix()` is simply + /// `camera_transform × translate(-w/2, -h/2)`, which we compute inline. pub fn rect(&self) -> Rectangle { - let vp = Rectangle { - x: 0.0, - y: 0.0, - width: self.size.width, - height: self.size.height, - }; - let inv = self - .view_matrix() - .inverse() - .unwrap_or_else(AffineTransform::identity); - rect::transform(vp, &inv) + // inverse(view_matrix) = transform × translate(-hw, -hh) + // For point (sx, sy) in screen space: + // wx = a*(sx - hw) + c*(sy - hh) + tx + // wy = b*(sx - hw) + d*(sy - hh) + ty + let [[a, c, tx], [b, d, ty]] = self.transform.matrix; + let hw = self.size.width * 0.5; + let hh = self.size.height * 0.5; + + // Transform viewport corners (0,0), (w,0), (0,h), (w,h) + let cx0 = 0.0 - hw; // = -hw + let cy0 = 0.0 - hh; // = -hh + let cx1 = self.size.width - hw; // = hw + let cy1 = self.size.height - hh; // = hh + + // Top-left: (cx0, cy0) + let x0 = a * cx0 + c * cy0 + tx; + let y0 = b * cx0 + d * cy0 + ty; + // Top-right: (cx1, cy0) + let x1 = a * cx1 + c * cy0 + tx; + let y1 = b * cx1 + d * cy0 + ty; + // Bottom-left: (cx0, cy1) + let x2 = a * cx0 + c * cy1 + tx; + let y2 = b * cx0 + d * cy1 + ty; + // Bottom-right: (cx1, cy1) + let x3 = a * cx1 + c * cy1 + tx; + let y3 = b * cx1 + d * cy1 + ty; + + let min_x = x0.min(x1).min(x2).min(x3); + let min_y = y0.min(y1).min(y2).min(y3); + let max_x = x0.max(x1).max(x2).max(x3); + let max_y = y0.max(y1).max(y2).max(y3); + + Rectangle { + x: min_x, + y: min_y, + width: max_x - min_x, + height: max_y - min_y, + } } /// Converts a screen-space point to canvas coordinates using the inverse view matrix. + /// + /// Uses the cached inverse when available, otherwise computes inline from + /// the camera transform (avoiding a full `view_matrix()` + `inverse()` pair). pub fn screen_to_canvas_point(&self, screen: vector2::Vector2) -> vector2::Vector2 { - let inv = self - .view_matrix() - .inverse() - .unwrap_or_else(AffineTransform::identity); - vector2::transform(screen, &inv) + if let Some(ref inv) = self.cached_view_matrix_inv { + return vector2::transform(screen, inv); + } + // Inline: inverse(view_matrix) applied to a single point. + // inverse(view_matrix) = transform × translate(-hw, -hh) + let [[a, c, tx], [b, d, ty]] = self.transform.matrix; + let hw = self.size.width * 0.5; + let hh = self.size.height * 0.5; + let sx = screen[0] - hw; + let sy = screen[1] - hh; + [a * sx + c * sy + tx, b * sx + d * sy + ty] } /// Get the current transform. @@ -292,7 +461,19 @@ impl Camera2D { } pub fn has_zoom_changed(&self) -> bool { - self.get_zoom() != 1.0 / self.prev_transform.get_scale_x() + // Compare scale components of prev and current transforms directly. + // translate() only modifies [0][2] and [1][2], so the scale entries + // ([0][0], [0][1], [1][0], [1][1]) are bitwise identical after a + // pan-only change. + // + // Previous implementation compared prev matrix scale against + // cached_zoom, which diverged after repeated _set_zoom calls due + // to floating-point drift in the sin/cos extraction (cos²+sin² ≠ 1). + let prev_a = self.prev_transform.matrix[0][0]; + let prev_b = self.prev_transform.matrix[1][0]; + let cur_a = self.transform.matrix[0][0]; + let cur_b = self.transform.matrix[1][0]; + prev_a != cur_a || prev_b != cur_b } /// Returns `true` when translation changed between the current and previous @@ -303,6 +484,17 @@ impl Camera2D { cx != px || cy != py } + /// Consume the pending camera change, resetting `prev_transform` to match + /// the current `transform`. After this call, [`change_kind`](Self::change_kind) + /// will return [`CameraChangeKind::None`] until the next mutation. + /// + /// This must be called once per frame **after** the change has been + /// processed (plan built, compositor invalidated, etc.) so that subsequent + /// frames don't see stale deltas. + pub fn consume_change(&mut self) { + self.prev_transform = self.transform.clone(); + } + /// Classify the camera change that occurred between `prev_transform` and /// the current `transform`. /// @@ -316,16 +508,18 @@ impl Camera2D { match (pan_changed, zoom_changed) { (false, false) => CameraChangeKind::None, (true, false) => CameraChangeKind::PanOnly, - (false, true) => { + (false, true) | (true, true) => { let current_zoom = self.get_zoom(); let prev_zoom = 1.0 / self.prev_transform.get_scale_x(); - if current_zoom > prev_zoom { + let zooming_in = current_zoom > prev_zoom; + if pan_changed { + CameraChangeKind::PanAndZoom(zooming_in) + } else if zooming_in { CameraChangeKind::ZoomIn } else { CameraChangeKind::ZoomOut } } - (true, true) => CameraChangeKind::PanAndZoom, } } @@ -343,6 +537,8 @@ impl Camera2D { pub fn set_transform(&mut self, transform: AffineTransform) -> bool { self.before_change(); self.transform = transform; + // Recompute cached zoom from the new transform's scale. + self.cached_zoom = 1.0 / self.transform.get_scale_x(); self.after_change() } } @@ -455,7 +651,7 @@ mod tests { // translation to keep the anchor point fixed on screen. // Using [75, 75] ensures the translation compensation is non-zero. camera.set_zoom_at(2.0, [75.0, 75.0]); - assert_eq!(camera.change_kind(), CameraChangeKind::PanAndZoom); + assert_eq!(camera.change_kind(), CameraChangeKind::PanAndZoom(true)); assert!(camera.change_kind().pan_changed()); assert!(camera.change_kind().zoom_changed()); } @@ -471,4 +667,192 @@ mod tests { assert!((dx - 5.0).abs() < f32::EPSILON); assert!((dy - (-3.0)).abs() < f32::EPSILON); } + + // --------------------------------------------------------------- + // Sequences that simulate real gesture interactions. + // Each step mirrors what the app does: mutate camera → change_kind + // → consume_change (the queue+flush cycle). + // --------------------------------------------------------------- + + /// Helper: simulate one queue+flush cycle on the legacy path. + /// Returns the CameraChangeKind that `queue()` would see. + fn sim_queue(camera: &mut Camera2D) -> CameraChangeKind { + let kind = camera.change_kind(); + camera.consume_change(); + kind + } + + #[test] + fn sequence_pan_zoom_in_pan() { + // pan → zoom-in → pan (user reports: correct, shows "pan") + let mut c = Camera2D::new(Size { width: 100.0, height: 100.0 }); + + c.translate(5.0, 0.0); + assert_eq!(sim_queue(&mut c), CameraChangeKind::PanOnly); + + c.set_zoom(2.0); + assert!(sim_queue(&mut c).zoom_changed()); + + c.translate(5.0, 0.0); + assert_eq!(sim_queue(&mut c), CameraChangeKind::PanOnly, + "after zoom-in then pan, should be PanOnly"); + } + + #[test] + fn sequence_zoom_in_zoom_out_pan() { + // zoom-in → zoom-out → pan (user reports: stuck at pan+zoom) + let mut c = Camera2D::new(Size { width: 100.0, height: 100.0 }); + + c.set_zoom(2.0); + let k = sim_queue(&mut c); + assert!(k.zoom_changed(), "zoom-in: {:?}", k); + + c.set_zoom(0.5); + let k = sim_queue(&mut c); + assert!(k.zoom_changed(), "zoom-out: {:?}", k); + + c.translate(5.0, 0.0); + assert_eq!(sim_queue(&mut c), CameraChangeKind::PanOnly, + "after zoom-in + zoom-out + pan, should be PanOnly"); + } + + #[test] + fn sequence_set_zoom_at_center_then_pan() { + // set_zoom_at at viewport center → no translation shift → ZoomIn + let mut c = Camera2D::new(Size { width: 100.0, height: 100.0 }); + + c.set_zoom_at(2.0, [50.0, 50.0]); + let k = sim_queue(&mut c); + assert_eq!(k, CameraChangeKind::ZoomIn, + "zoom at center should be ZoomIn (no pan compensation)"); + + c.translate(5.0, 0.0); + assert_eq!(sim_queue(&mut c), CameraChangeKind::PanOnly, + "after zoom-at-center + pan, should be PanOnly"); + } + + #[test] + fn sequence_set_zoom_at_off_center_then_pan() { + // set_zoom_at off-center → translation shifts → PanAndZoom + let mut c = Camera2D::new(Size { width: 100.0, height: 100.0 }); + + c.set_zoom_at(2.0, [75.0, 75.0]); + let k = sim_queue(&mut c); + assert!(matches!(k, CameraChangeKind::PanAndZoom(_)), + "zoom at off-center should be PanAndZoom, got: {:?}", k); + + c.translate(5.0, 0.0); + assert_eq!(sim_queue(&mut c), CameraChangeKind::PanOnly, + "after off-center pinch + pan, should be PanOnly"); + } + + #[test] + fn sequence_set_zoom_at_in_out_then_pan() { + // Pinch zoom in → pinch zoom out → pan + let mut c = Camera2D::new(Size { width: 100.0, height: 100.0 }); + + c.set_zoom_at(2.0, [50.0, 50.0]); + let _ = sim_queue(&mut c); + + c.set_zoom_at(0.5, [50.0, 50.0]); + let _ = sim_queue(&mut c); + + c.translate(5.0, 0.0); + assert_eq!(sim_queue(&mut c), CameraChangeKind::PanOnly, + "after pinch-in + pinch-out + pan, should be PanOnly"); + } + + #[test] + fn sequence_zoom_out_is_not_pan_and_zoom() { + // Pure set_zoom (no focal point) should be ZoomOut, not PanAndZoom + let mut c = Camera2D::new(Size { width: 100.0, height: 100.0 }); + + c.set_zoom(0.5); + assert_eq!(sim_queue(&mut c), CameraChangeKind::ZoomOut, + "set_zoom(0.5) should be ZoomOut, not PanAndZoom"); + } + + #[test] + fn sequence_set_zoom_at_out_is_pan_and_zoom() { + // set_zoom_at always adjusts translation to keep focal point fixed, + // so it inherently produces PanAndZoom. This is correct behavior. + let mut c = Camera2D::new(Size { width: 100.0, height: 100.0 }); + + c.set_zoom_at(0.5, [75.0, 75.0]); + assert!(matches!(sim_queue(&mut c), CameraChangeKind::PanAndZoom(_)), + "set_zoom_at always changes both zoom and translation"); + } + + #[test] + fn sequence_consume_then_no_change() { + // After consume, change_kind should return None + let mut c = Camera2D::new(Size { width: 100.0, height: 100.0 }); + + c.translate(5.0, 0.0); + assert_eq!(sim_queue(&mut c), CameraChangeKind::PanOnly); + + // No mutation — should be None + assert_eq!(sim_queue(&mut c), CameraChangeKind::None, + "after consume with no mutation, should be None"); + } + + #[test] + fn sequence_multiple_translates_before_queue() { + // Multiple translates before a single queue — should still be PanOnly + let mut c = Camera2D::new(Size { width: 100.0, height: 100.0 }); + + c.translate(5.0, 0.0); + c.translate(3.0, 0.0); + c.translate(-2.0, 0.0); + // Each translate calls before_change, so only the last delta is visible + assert_eq!(sim_queue(&mut c), CameraChangeKind::PanOnly); + } + + #[test] + fn sequence_zoom_then_translate_before_queue() { + // Zoom then translate in same "frame" (before queue) — translate's + // before_change overwrites the zoom baseline, so only pan is visible + let mut c = Camera2D::new(Size { width: 100.0, height: 100.0 }); + + c.set_zoom(2.0); + c.translate(5.0, 0.0); + // translate's before_change saved post-zoom state; only pan delta visible + assert_eq!(sim_queue(&mut c), CameraChangeKind::PanOnly, + "zoom + translate before queue: translate's before_change hides the zoom"); + } + + #[test] + fn sequence_interleaved_zoom_pan_with_queue() { + // Interleaved zoom and pan WITH queue between each — simulates + // macOS sending PinchGesture + MouseWheel events, each triggering queue() + let mut c = Camera2D::new(Size { width: 100.0, height: 100.0 }); + + // Pinch event → zoom (at center → no translation change) + c.set_zoom_at(1.5, [50.0, 50.0]); + let k1 = sim_queue(&mut c); + assert_eq!(k1, CameraChangeKind::ZoomIn); + + // Scroll event → pan (after consumed zoom) + c.translate(5.0, 0.0); + let k2 = sim_queue(&mut c); + assert_eq!(k2, CameraChangeKind::PanOnly, + "translate after consumed zoom should be PanOnly"); + } + + #[test] + fn sequence_rapid_zoom_at_oscillation_then_pan() { + // Simulate rapid pinch in/out (oscillating zoom) then pure pan + let mut c = Camera2D::new(Size { width: 100.0, height: 100.0 }); + + for i in 0..20 { + let z = if i % 2 == 0 { 1.5 } else { 0.8 }; + c.set_zoom_at(z, [50.0, 50.0]); + let _ = sim_queue(&mut c); + } + + // Now pure pan + c.translate(10.0, 0.0); + assert_eq!(sim_queue(&mut c), CameraChangeKind::PanOnly, + "after rapid zoom oscillation, pan should be PanOnly"); + } } diff --git a/crates/grida-canvas/src/runtime/frame_loop.rs b/crates/grida-canvas/src/runtime/frame_loop.rs new file mode 100644 index 0000000000..dd2455f567 --- /dev/null +++ b/crates/grida-canvas/src/runtime/frame_loop.rs @@ -0,0 +1,265 @@ +/// Unified frame lifecycle controller. +/// +/// `FrameLoop` is the single owner of "should we render, and at what quality?" +/// It is **source-agnostic**: it does not know *what* changed (camera, scene, +/// config, etc.) — only *that* something changed and *when*. +/// +/// # Design +/// +/// The host calls one function per frame. The engine decides whether to produce +/// pixels. No callbacks from engine to host. No platform-specific notification +/// mechanism inside the rendering core. +/// +/// Three operations: +/// - [`invalidate`](FrameLoop::invalidate) — something changed, O(1) +/// - [`poll`](FrameLoop::poll) — should we render this tick? +/// - [`complete`](FrameLoop::complete) — mark frame as rendered +/// +/// All times are host-provided `f64` milliseconds (e.g. from +/// `performance.now()` or `requestAnimationFrame`). No `std::time::Instant`. +#[derive(Debug)] +pub struct FrameLoop { + /// Last rendered frame number. + prev_frame: u64, + /// Next pending frame number. + next_frame: u64, + /// Host-time (ms) of last invalidation. + last_change_time: f64, + /// 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, +} + +/// What quality of frame to render. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FrameQuality { + /// Fast, reduced-quality frame during active interaction. + Unstable, + /// Full-quality frame after interaction settles. + Stable, +} + +impl Default for FrameLoop { + fn default() -> Self { + Self::new() + } +} + +impl FrameLoop { + /// Default debounce delay (milliseconds) before a stable frame fires. + pub const DEFAULT_STABLE_DELAY_MS: f64 = 50.0; + + /// Create a new `FrameLoop` with the default stable delay. + pub fn new() -> Self { + Self::with_stable_delay(Self::DEFAULT_STABLE_DELAY_MS) + } + + /// Create a new `FrameLoop` with a custom stable delay. + pub fn with_stable_delay(stable_delay_ms: f64) -> Self { + Self { + prev_frame: 0, + next_frame: 0, + last_change_time: 0.0, + stable_delay_ms, + needs_stable: false, + } + } + + /// Something changed. Bumps `next_frame`, records timestamp, sets + /// `needs_stable = true`. O(1), no plan building. + pub fn invalidate(&mut self, now: f64) { + self.next_frame = self.next_frame.wrapping_add(1); + self.last_change_time = now; + self.needs_stable = true; + } + + /// Called once per host frame. Returns: + /// - `None` — idle, nothing to render + /// - `Some(Unstable)` — change within debounce window, render fast + /// - `Some(Stable)` — debounce expired and stable still owed, render full quality + pub fn poll(&self, now: f64) -> Option { + // New frame pending (change since last render)? + let has_pending = self.next_frame != self.prev_frame; + + if has_pending { + // Always render pending frames as unstable first. + return Some(FrameQuality::Unstable); + } + + // 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 { + return Some(FrameQuality::Stable); + } + } + + None + } + + /// Mark frame as rendered. If `Stable`, clears `needs_stable`. + /// If `Unstable`, keeps `needs_stable = true` so `poll()` will + /// return `Stable` once the debounce expires. + pub fn complete(&mut self, quality: FrameQuality) { + self.prev_frame = self.next_frame; + match quality { + FrameQuality::Stable => { + self.needs_stable = false; + } + FrameQuality::Unstable => { + // needs_stable stays true — poll() will fire Stable later + } + } + } + + /// Whether the frame loop is completely idle (no pending work). + pub fn is_idle(&self) -> bool { + self.next_frame == self.prev_frame && !self.needs_stable + } + + /// Current frame number. + pub fn current_frame(&self) -> u64 { + self.next_frame + } + + /// Last rendered frame number. + pub fn last_rendered_frame(&self) -> u64 { + self.prev_frame + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn idle_by_default() { + let fl = FrameLoop::new(); + assert!(fl.is_idle()); + assert_eq!(fl.poll(0.0), None); + assert_eq!(fl.poll(1000.0), None); + } + + #[test] + fn invalidate_triggers_unstable() { + let mut fl = FrameLoop::new(); + fl.invalidate(100.0); + assert!(!fl.is_idle()); + assert_eq!(fl.poll(100.0), Some(FrameQuality::Unstable)); + } + + #[test] + fn complete_unstable_then_stable_after_delay() { + let mut fl = FrameLoop::new(); + fl.invalidate(100.0); + + // First poll: unstable + assert_eq!(fl.poll(100.0), Some(FrameQuality::Unstable)); + fl.complete(FrameQuality::Unstable); + + // Immediately after: no pending, but stable not yet due + assert_eq!(fl.poll(110.0), None); + + // After debounce: stable fires + assert_eq!(fl.poll(200.0), Some(FrameQuality::Stable)); + fl.complete(FrameQuality::Stable); + + // Now idle + assert!(fl.is_idle()); + assert_eq!(fl.poll(300.0), None); + } + + #[test] + fn rapid_invalidations_coalesce() { + let mut fl = FrameLoop::new(); + + // 10 invalidations in rapid succession + for i in 0..10 { + fl.invalidate(100.0 + i as f64); + } + + // Only one unstable frame needed + assert_eq!(fl.poll(109.0), Some(FrameQuality::Unstable)); + fl.complete(FrameQuality::Unstable); + + // Stable fires after the *last* invalidation's debounce + assert_eq!(fl.poll(150.0), None); // 109 + 50 = 159 not yet + assert_eq!(fl.poll(159.0), Some(FrameQuality::Stable)); + fl.complete(FrameQuality::Stable); + + assert!(fl.is_idle()); + } + + #[test] + fn invalidate_during_stable_wait_restarts_debounce() { + let mut fl = FrameLoop::new(); + fl.invalidate(100.0); + + assert_eq!(fl.poll(100.0), Some(FrameQuality::Unstable)); + fl.complete(FrameQuality::Unstable); + + // Waiting for stable at 150ms... + assert_eq!(fl.poll(140.0), None); + + // New invalidation at 145ms resets the debounce + fl.invalidate(145.0); + assert_eq!(fl.poll(145.0), Some(FrameQuality::Unstable)); + fl.complete(FrameQuality::Unstable); + + // 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)); + fl.complete(FrameQuality::Stable); + + assert!(fl.is_idle()); + } + + #[test] + fn custom_stable_delay() { + let mut fl = FrameLoop::with_stable_delay(200.0); + fl.invalidate(0.0); + + assert_eq!(fl.poll(0.0), Some(FrameQuality::Unstable)); + fl.complete(FrameQuality::Unstable); + + assert_eq!(fl.poll(100.0), None); + assert_eq!(fl.poll(199.0), None); + assert_eq!(fl.poll(200.0), Some(FrameQuality::Stable)); + } + + #[test] + fn complete_stable_without_unstable() { + // Edge case: if we directly call complete(Stable), needs_stable clears + let mut fl = FrameLoop::new(); + fl.invalidate(0.0); + + // Skip unstable, go straight to stable (e.g. scene load) + assert_eq!(fl.poll(0.0), Some(FrameQuality::Unstable)); + // But the caller renders it as stable quality + fl.complete(FrameQuality::Stable); + + // Should be idle — stable was delivered + assert!(fl.is_idle()); + } + + #[test] + fn frame_numbers_increment() { + let mut fl = FrameLoop::new(); + assert_eq!(fl.current_frame(), 0); + assert_eq!(fl.last_rendered_frame(), 0); + + fl.invalidate(0.0); + assert_eq!(fl.current_frame(), 1); + assert_eq!(fl.last_rendered_frame(), 0); + + fl.complete(FrameQuality::Unstable); + assert_eq!(fl.last_rendered_frame(), 1); + + fl.invalidate(10.0); + fl.invalidate(20.0); + assert_eq!(fl.current_frame(), 3); + } +} diff --git a/crates/grida-canvas/src/runtime/mod.rs b/crates/grida-canvas/src/runtime/mod.rs index 8d47534731..a7b2900a7a 100644 --- a/crates/grida-canvas/src/runtime/mod.rs +++ b/crates/grida-canvas/src/runtime/mod.rs @@ -3,6 +3,7 @@ pub mod config; pub mod counter; pub mod effect_tree; pub mod font_repository; +pub mod frame_loop; pub mod image_repository; pub mod pixel_preview; pub mod render_policy; diff --git a/crates/grida-canvas/src/runtime/scene.rs b/crates/grida-canvas/src/runtime/scene.rs index 6e1287137b..a66b43939f 100644 --- a/crates/grida-canvas/src/runtime/scene.rs +++ b/crates/grida-canvas/src/runtime/scene.rs @@ -20,13 +20,36 @@ use crate::{ use math2::{self, rect}; use skia_safe::{ - surfaces, Canvas, FilterMode, Image, MipmapMode, Paint as SkPaint, Picture, PictureRecorder, - Rect, SamplingOptions, Surface, + surfaces, Canvas, Color, FilterMode, Image, MipmapMode, Paint as SkPaint, Picture, + PictureRecorder, Rect, SamplingOptions, Surface, }; use std::collections::HashSet; use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; +// --------------------------------------------------------------------------- +// Renderer tuning constants +// --------------------------------------------------------------------------- + +/// Zoom scale bucket ratio for the compositor raster cache. +/// +/// A cached node image is reused (GPU-stretched) as long as the current zoom +/// stays within this factor of the zoom at which the image was rasterized. +/// Once the drift exceeds this ratio in either direction the node is +/// re-rasterized (subject to the per-frame time budget on interactive frames, +/// or immediately on stable frames). +/// +/// Set to 2.0 to match Chromium's `kMaxScaleRatioDuringPinch`: tiles are +/// reused until the zoom drifts more than 2× from the cached scale, then +/// re-rasterization is triggered. +/// +/// Raising this value reduces re-rasterization frequency (smoother +/// interaction, blurrier stretched content). Lowering it increases sharpness +/// at the cost of more frequent re-rasters. +const RASTER_ZOOM_RATIO: f32 = 2.0; + +// --------------------------------------------------------------------------- + fn normalize_image_id(id: &str) -> String { if id.starts_with("res://") || id.starts_with("system://") { id.to_string() @@ -102,6 +125,11 @@ pub struct FramePlan { pub promoted: Vec, /// regions with their intersecting indices (live-drawn nodes only) pub regions: Vec<(rect::Rectangle, Vec)>, + /// Visible layer indices with promotable effects (shadows, blur, noise). + /// Pre-filtered from the R-tree query so the compositor iterates only + /// nodes that may need promotion, avoiding a redundant R-tree query + /// and the per-node `has_promotable_effects` check. + pub compositor_indices: Vec, pub display_list_duration: Duration, pub display_list_size_estimated: usize, } @@ -196,6 +224,26 @@ impl RendererWindowContext { /// Nodes larger than this in either axis are skipped for compositing. const COMPOSITOR_SURFACE_SIZE: i32 = 4096; +/// Maximum screen-space offset (in pixels) before the pan image cache is +/// invalidated and a full re-draw is triggered. Beyond this threshold the +/// exposed strips are too large for the blit-only fast path to look acceptable. +const PAN_IMAGE_CACHE_MAX_OFFSET: f32 = 200.0; + +/// Cached GPU snapshot of the composited frame for pan-only fast path. +/// +/// During pan-only camera changes (the most common interaction), the scene +/// graph is static and zoom is constant — only the viewport translation +/// changes. Instead of re-drawing every visible node each frame, we capture +/// the composited frame as a GPU texture and blit it at the new camera +/// offset. This replaces O(N) draw commands with a single texture blit. +struct PanImageCache { + /// GPU texture snapshot of the composited frame. + image: Image, + /// View matrix translation components at capture time. + origin_tx: f32, + origin_ty: f32, +} + pub struct Renderer { pub backend: Backend, pub scene: Option, @@ -230,6 +278,9 @@ pub struct Renderer { downscale_surface: Option, /// Dimensions of the cached downscale surface (to detect size changes). downscale_dims: (i32, i32), + /// Cached composited frame for pan-only fast path. + /// See [`PanImageCache`] for details. + pan_image_cache: Option, } impl Renderer { @@ -257,11 +308,25 @@ impl Renderer { policy: RenderPolicy, ) { let variant_key = policy.variant_key(); - // Prefill picture cache for visible layers so Painter can reuse pictures even with masks + // Prefill picture cache for visible layers so Painter can reuse pictures even with masks. + // Fast path: skip clone + recording when the picture is already cached (common case + // on cache-warm frames). The clone of LayerEntry is expensive because it deep-copies + // fills, strokes, effects, paths, etc. for (_region, indices) in &plan.regions { for idx in indices { - if let Some(entry) = self.scene_cache.layers.layers.get(*idx).cloned() { + if let Some(entry) = self.scene_cache.layers.layers.get(*idx) { let id = entry.id; + // Check cache before cloning — avoids expensive deep clone on cache hits. + if self + .scene_cache + .picture + .get_node_picture_variant(&id, variant_key) + .is_some() + { + continue; + } + // Cache miss — clone and record. + let entry = entry.clone(); let _ = self.with_recording_cached_with_policy( &id, variant_key, @@ -275,6 +340,74 @@ impl Renderer { } } + /// Pre-extract blit data for all promoted nodes. + /// + /// Iterates through the promoted node list and extracts the image, + /// source rect, destination rect, opacity, and blend mode for each node. + /// The resulting map is passed to the Painter so it can blit promoted + /// nodes inline at their correct z-position in the render command tree. + fn build_promoted_blits( + &mut self, + plan: &FramePlan, + ) -> ( + std::collections::HashMap, + usize, + ) { + let mut blits = std::collections::HashMap::new(); + let mut cache_hits = 0usize; + + for id in &plan.promoted { + if let Some(layer_img) = self.scene_cache.compositor.get(id) { + let b = &layer_img.local_bounds; + let dst_rect = Rect::from_xywh(b.x, b.y, b.width, b.height); + let opacity = layer_img.opacity; + let cg_blend: crate::cg::types::BlendMode = layer_img.blend_mode.into(); + let sk_blend: skia_safe::BlendMode = cg_blend.into(); + + let blit = if layer_img.is_atlas_backed() { + // Atlas path: same-texture sub-rect blit. + if let Some((atlas_image, src_rect)) = + self.compositor_atlas.get_image_and_src_rect(id) + { + Some(crate::painter::PromotedBlit { + image: std::rc::Rc::new(atlas_image.clone()), + src_rect, + dst_rect, + opacity, + blend_mode: sk_blend, + }) + } else { + None + } + } else if let Some(img) = layer_img.individual_image() { + // Individual texture path. + let src_rect = Rect::new( + 0.0, + 0.0, + img.width() as f32, + img.height() as f32, + ); + Some(crate::painter::PromotedBlit { + image: std::rc::Rc::clone(img), + src_rect, + dst_rect, + opacity, + blend_mode: sk_blend, + }) + } else { + None + }; + + if let Some(blit) = blit { + blits.insert(*id, blit); + cache_hits += 1; + } + } + } + + (blits, cache_hits) + } + #[inline] fn draw_layers_with_scene_cache(&mut self, canvas: &Canvas, plan: &FramePlan) -> usize { self.draw_layers_with_scene_cache_skip(canvas, plan, None) @@ -284,7 +417,7 @@ impl Renderer { &mut self, canvas: &Canvas, plan: &FramePlan, - promoted_skip: Option<&std::collections::HashSet>, + promoted_blits: Option<&std::collections::HashMap>, ) -> usize { // Select effect quality based on frame stability. // Unstable (interactive) frames use reduced effects for performance. @@ -304,8 +437,8 @@ impl Renderer { &self.scene_cache, policy, ); - let painter = if let Some(skip) = promoted_skip { - painter.with_promoted_skip(skip) + let painter = if let Some(blits) = promoted_blits { + painter.with_promoted_blits(blits) } else { painter }; @@ -371,6 +504,7 @@ impl Renderer { ), downscale_surface: None, downscale_dims: (0, 0), + pan_image_cache: None, } } @@ -555,6 +689,7 @@ impl Renderer { // re-captured as individual textures on the next frame. self.scene_cache.compositor.invalidate_all(); self.compositor_atlas.clear(); + self.pan_image_cache = None; } } @@ -640,37 +775,106 @@ impl Renderer { /// Render the queued frame if any and return the completed statistics. /// Intended to be called by the host when a redraw request is received. + /// + /// NOTE: camera `consume_change()` is NOT called here. The caller + /// (`app.redraw()` or `app.frame()`) is responsible for consuming + /// after rendering. Consuming here would eat the change before + /// `frame()` sees it on the web path, where both `redraw()` (called + /// by JS) and `frame()` (called by RAF) may run. pub fn flush(&mut self) -> FrameFlushResult { if !self.fc.has_pending() { return FrameFlushResult::NoPending; } - let Some(frame) = self.plan.take() else { + let Some(plan) = self.plan.take() else { return FrameFlushResult::NoFrame; }; - let start = Instant::now(); - - let Some(scene_ptr) = self.scene.as_ref().map(|s| s as *const Scene) else { + if self.scene.is_none() { return FrameFlushResult::NoScene; - }; + } + + let stats = self.render_frame(plan); + self.fc.flush(); + self.plan = None; + + FrameFlushResult::OK(stats) + } + + /// Core rendering logic shared by `flush()` and `flush_with_plan()`. + /// + /// Assumes `self.scene` is `Some`. Panics otherwise. + fn render_frame(&mut self, plan: FramePlan) -> FrameFlushStats { + let start = Instant::now(); + + let scene_ptr = self.scene.as_ref().unwrap() as *const Scene; let surface = unsafe { &mut *self.backend.get_surface() }; let scene = unsafe { &*scene_ptr }; let width = surface.width() as f32; let height = surface.height() as f32; + // --- Pan image cache fast path --- + // On pan-only frames with a valid cached composited frame and small + // offset, blit the cached GPU texture instead of re-drawing all + // nodes. This replaces O(N) draw commands + GPU rasterization with + // a single texture blit. + if plan.camera_change == CameraChangeKind::PanOnly && self.backend.is_gpu() { + if let Some(ref cache) = self.pan_image_cache { + let vm = self.camera.view_matrix(); + let dx = vm.matrix[0][2] - cache.origin_tx; + let dy = vm.matrix[1][2] - cache.origin_ty; + + if dx.abs() <= PAN_IMAGE_CACHE_MAX_OFFSET + && dy.abs() <= PAN_IMAGE_CACHE_MAX_OFFSET + { + let canvas = surface.canvas(); + if let Some(bg) = scene.background_color { + canvas.clear(Color::from(bg)); + } else { + canvas.clear(Color::TRANSPARENT); + } + canvas.draw_image(&cache.image, (dx, dy), None); + + let mid_flush_start = Instant::now(); + Self::gpu_flush(surface); + let mid_flush_duration = mid_flush_start.elapsed(); + let frame_duration = start.elapsed(); + + return FrameFlushStats { + frame: plan, + draw: DrawResult { + painter_duration: Duration::ZERO, + cache_picture_used: 0, + cache_picture_size: self.scene_cache.picture.len(), + cache_geometry_size: self.scene_cache.geometry.len(), + layer_image_cache_size: 0, + layer_image_cache_hits: 0, + layer_image_cache_bytes: 0, + live_draw_count: 0, + }, + frame_duration, + flush_duration: Duration::ZERO, + total_duration: frame_duration, + compositor_duration: Duration::ZERO, + mid_flush_duration, + }; + } + // Offset too large — fall through to full re-draw below. + } + } + + // --- Full draw path --- + // Reuse or create a downscaled offscreen for interaction rendering. let interaction_scale = self.config.interaction_render_scale; - let use_downscale = !frame.stable - && interaction_scale > 0.0 - && interaction_scale < 1.0; + let use_downscale = + !plan.stable && interaction_scale > 0.0 && interaction_scale < 1.0; if use_downscale { let sw = (width * interaction_scale).ceil() as i32; let sh = (height * interaction_scale).ceil() as i32; if sw > 0 && sh > 0 && (sw, sh) != self.downscale_dims { - // Size changed — recreate the surface. let info = skia_safe::ImageInfo::new_n32_premul((sw, sh), None); self.downscale_surface = surface.new_surface(&info); self.downscale_dims = (sw, sh); @@ -688,7 +892,7 @@ impl Renderer { let mut canvas = surface.canvas(); let draw = self.draw( &mut canvas, - &frame, + &plan, scene.background_color, width, height, @@ -700,37 +904,36 @@ impl Renderer { self.downscale_surface = ds_taken; } - // Layer compositing cache: capture (or re-capture) node images. - // - // GPU-only: offscreen surfaces share the GL context so cached - // SkImages live in VRAM. On a CPU/raster backend the extra copy - // would be strictly slower than direct painting, so we skip it. - // - // Runs on every frame — the method itself is cheap when all entries - // are already cached and clean. Rasterisation only happens for nodes - // that are new or dirty (zoom change, content edit, etc.). - // Mid-frame flush: isolate draw vs compositor GPU work. + // Mid-frame GPU flush: isolate draw vs compositor GPU work. let mid_flush_start = Instant::now(); - if let Some(mut gr_context) = surface.recording_context() { - if let Some(mut direct_context) = gr_context.as_direct_context() { - direct_context.flush_and_submit(); - } - } + Self::gpu_flush(surface); let mid_flush_duration = mid_flush_start.elapsed(); + // Capture composited frame for pan image cache. + // After mid_flush the surface has the complete rendered scene. + // Only capture on non-stable frames that didn't change zoom — zoom + // frames invalidate the cache on the next queue(), so capturing + // them wastes the snapshot. + if self.backend.is_gpu() && !plan.stable && !plan.camera_change.zoom_changed() { + let vm = self.camera.view_matrix(); + let image = surface.image_snapshot(); + self.pan_image_cache = Some(PanImageCache { + image, + origin_tx: vm.matrix[0][2], + origin_ty: vm.matrix[1][2], + }); + } + + // Compositor update (GPU-only). let compositor_start = Instant::now(); if self.backend.is_gpu() { - let effective_layer_compositing = self.config.layer_compositing + let effective = self.config.layer_compositing && self.config.render_policy.allows_layer_compositing(); - if effective_layer_compositing { - if frame.stable { - // Stable frame: re-rasterize all stale entries at the - // final zoom density without a time budget. - self.update_compositor_stable(surface); + if effective { + if plan.stable { + self.update_compositor_stable(surface, &plan.compositor_indices); } else { - // Unstable frame: budgeted re-rasterization. Stale - // entries stay GPU-stretched until their turn comes. - self.update_compositor(surface); + self.update_compositor(surface, &plan.compositor_indices); } } } @@ -738,28 +941,30 @@ impl Renderer { let frame_duration = start.elapsed(); + // Final GPU flush. let flush_start = Instant::now(); - if let Some(mut gr_context) = surface.recording_context() { - if let Some(mut direct_context) = gr_context.as_direct_context() { - direct_context.flush_and_submit(); - } - } + Self::gpu_flush(surface); let flush_duration = flush_start.elapsed(); - let stats = FrameFlushStats { - frame, + FrameFlushStats { + frame: plan, draw, frame_duration, flush_duration, total_duration: frame_duration + flush_duration, compositor_duration, mid_flush_duration, - }; - - self.fc.flush(); - self.plan = None; + } + } - FrameFlushResult::OK(stats) + /// Submit pending GPU work on the surface's direct context (if any). + #[inline] + fn gpu_flush(surface: &mut Surface) { + if let Some(mut gr_context) = surface.recording_context() { + if let Some(mut direct_context) = gr_context.as_direct_context() { + direct_context.flush_and_submit(); + } + } } /// Invoke the request redraw callback. @@ -785,6 +990,7 @@ impl Renderer { self.scene = Some(scene); self.scene_cache = cache::scene::SceneCache::new(); + self.pan_image_cache = None; if let Some(scene) = self.scene.as_ref() { let requested = collect_scene_font_families(scene); self.fonts.set_requested_families(requested.into_iter()); @@ -839,6 +1045,13 @@ impl Renderer { self.scene_cache.compositor.mark_all_stale(); } + // Invalidate pan image cache when zoom changes or on stable frames. + // 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 { + self.pan_image_cache = None; + } + // Always compute the latest frame plan so that a subsequent flush uses up-to-date state, // even if a previous frame is already pending. let rect = Some(self.camera.rect()); @@ -866,12 +1079,51 @@ impl Renderer { self.queue(true); } + /// Mark compositor entries as stale on zoom change. + /// + /// Called by the application's `frame()` method when the camera zoom + /// changed. Separated from `queue()` so that the new `FrameLoop`-based + /// path can prepare the renderer without building a full plan eagerly. + pub fn invalidate_compositor_on_zoom(&mut self) { + if self.config.layer_compositing { + self.scene_cache.compositor.mark_all_stale(); + } + } + + /// Build a frame plan without queuing it on the renderer. + /// + /// Used by the application's unified `frame()` entry point, which + /// handles frame scheduling through `FrameLoop` rather than the + /// legacy `FrameCounter`-based path. + pub fn build_frame_plan( + &self, + bounds: rect::Rectangle, + zoom: f32, + stable: bool, + camera_change: CameraChangeKind, + ) -> FramePlan { + self.frame(bounds, zoom, stable, camera_change) + } + + /// Flush a caller-provided plan: draw + GPU submit + compositor update. + /// + /// Returns `Some(stats)` on success, `None` if no scene is loaded. + /// Unlike the legacy `flush()`, this does NOT consult `FrameCounter` + /// — the caller (via `FrameLoop`) already decided to render. + pub fn flush_with_plan(&mut self, plan: FramePlan) -> Option { + if self.scene.is_none() { + return None; + } + Some(self.render_frame(plan)) + } + /// Clear the cached scene picture. pub fn invalidate_cache(&mut self) { self.scene_cache.invalidate(); // Also invalidate all compositor layer images so they re-rasterize. self.scene_cache.compositor.invalidate_all(); self.compositor_atlas.clear(); + self.pan_image_cache = None; } /// Rebuild scene caches after scene geometry has changed. @@ -1011,40 +1263,56 @@ impl Renderer { ) -> FramePlan { let __start = Instant::now(); - let painter_region = vec![bounds]; - let effective_layer_compositing = self.config.layer_compositing && self.config.render_policy.allows_layer_compositing(); let mut promoted_ids: Vec = Vec::new(); let mut regions: Vec<(rect::Rectangle, Vec)> = Vec::new(); - for rect in painter_region { - let mut indices = self.scene_cache.intersects(rect); - - // TODO: sort is expensive — consider incremental visible-set - // update (item 19) for pan-only frames where the entering/exiting - // sets are tiny. - indices.sort(); - - if effective_layer_compositing { - // Separate promoted (cached) nodes from live-drawn nodes. - let mut live_indices = Vec::new(); - for &idx in &indices { - if let Some(entry) = self.scene_cache.layers.layers.get(idx) { - if self.scene_cache.compositor.peek(&entry.id).is_some() { - promoted_ids.push(entry.id); - } else { - live_indices.push(idx); - } + // Query the R-tree once for all visible layer indices. + let mut indices = self.scene_cache.intersects(bounds); + + // TODO: sort is expensive — consider incremental visible-set + // update (item 19) for pan-only frames where the entering/exiting + // sets are tiny. + indices.sort(); + + // Pre-filter compositor-relevant indices during the same pass. + // Nodes without expensive effects (the vast majority) are skipped + // by the compositor anyway. Filtering here avoids a redundant + // R-tree query in update_compositor_inner AND reduces the compositor + // loop to only promotable nodes. + let mut compositor_indices = Vec::new(); + + if effective_layer_compositing { + // Separate promoted (cached) nodes from live-drawn nodes. + let mut live_indices = Vec::new(); + for &idx in &indices { + if let Some(entry) = self.scene_cache.layers.layers.get(idx) { + if crate::cache::compositor::promotion::has_promotable_effects(&entry.layer) { + compositor_indices.push(idx); + } + if self.scene_cache.compositor.peek(&entry.id).is_some() { + promoted_ids.push(entry.id); + } else { + live_indices.push(idx); } } - if !live_indices.is_empty() { - regions.push((rect, live_indices)); + } + if !live_indices.is_empty() { + regions.push((bounds, live_indices)); + } + } else { + // No compositing: still collect compositor-relevant indices + // for the compositor update pass. + for &idx in &indices { + if let Some(entry) = self.scene_cache.layers.layers.get(idx) { + if crate::cache::compositor::promotion::has_promotable_effects(&entry.layer) { + compositor_indices.push(idx); + } } - } else { - regions.push((rect, indices)); } + regions.push((bounds, indices)); } let ll_len = regions.iter().map(|(_, indices)| indices.len()).sum(); @@ -1056,6 +1324,7 @@ impl Renderer { camera_change, promoted: promoted_ids, regions, + compositor_indices, display_list_duration: __ll_duration, display_list_size_estimated: ll_len, } @@ -1170,8 +1439,6 @@ impl Renderer { // Fallback: draw at full resolution below. } - canvas.clear(skia_safe::Color::TRANSPARENT); - Self::clear_and_paint_background(canvas, background_color, width, height); canvas.save(); @@ -1179,76 +1446,20 @@ impl Renderer { // Apply camera transform canvas.concat(&sk::sk_matrix(self.camera.view_matrix().matrix)); - // Draw promoted nodes from the layer compositing cache. - // Each cached image is blitted at its world-space render bounds. - // The camera view matrix (already applied to canvas) handles zoom. - // Opacity and blend mode are stored in LayerImage at capture time. - // - // Atlas-backed nodes use same-texture sub-rect draws (batch-friendly, - // eliminates GPU texture switching). Individual-backed nodes use - // per-node texture blits as fallback. - let mut layer_image_cache_hits = 0usize; - let promoted_set: std::collections::HashSet = - plan.promoted.iter().copied().collect(); - if !plan.promoted.is_empty() { - for id in &plan.promoted { - if let Some(layer_img) = self.scene_cache.compositor.get(id) { - let b = &layer_img.local_bounds; - let dst = Rect::from_xywh(b.x, b.y, b.width, b.height); - let mut paint = SkPaint::default(); - if layer_img.opacity < 1.0 { - paint.set_alpha_f(layer_img.opacity); - } - let cg_blend: crate::cg::types::BlendMode = layer_img.blend_mode.into(); - let sk_blend: skia_safe::BlendMode = cg_blend.into(); - paint.set_blend_mode(sk_blend); - - if layer_img.is_atlas_backed() { - // Atlas path: same-texture sub-rect blit. - if let Some((atlas_image, src_rect)) = - self.compositor_atlas.get_image_and_src_rect(id) - { - canvas.draw_image_rect( - atlas_image, - Some((&src_rect, skia_safe::canvas::SrcRectConstraint::Fast)), - dst, - &paint, - ); - } - } else if let Some(img) = layer_img.individual_image() { - // Individual texture path. - let src = Rect::new( - 0.0, - 0.0, - img.width() as f32, - img.height() as f32, - ); - canvas.draw_image_rect( - img, - Some((&src, skia_safe::canvas::SrcRectConstraint::Fast)), - dst, - &paint, - ); - } - layer_image_cache_hits += 1; - } - } - } + // Build promoted blit map: pre-extract image data for compositor- + // cached nodes. The Painter will blit these inline at their correct + // z-position in the render command tree, preserving proper z-order + // when a live parent (e.g. Container with fills) has promoted children. + let (promoted_blits, layer_image_cache_hits) = self.build_promoted_blits(plan); - // Draw live (non-promoted) layers via the Painter. - // Promoted nodes are skipped — they were already blitted above. - // Skip entirely when all visible nodes are promoted — no live work needed. - let has_live_work = plan.regions.iter().any(|(_, indices)| !indices.is_empty()); - let promoted_skip = if promoted_set.is_empty() { + // Draw all layers via the Painter with promoted nodes blitted inline. + let promoted_blits_ref = if promoted_blits.is_empty() { None } else { - Some(&promoted_set) - }; - let cache_picture_used = if has_live_work { - self.draw_layers_with_scene_cache_skip(canvas, plan, promoted_skip) - } else { - 0 + Some(&promoted_blits) }; + let cache_picture_used = + self.draw_layers_with_scene_cache_skip(canvas, plan, promoted_blits_ref); let __painter_duration = __before_paint.elapsed(); @@ -1301,59 +1512,15 @@ impl Renderer { off_canvas.scale((scale, scale)); off_canvas.concat(&sk::sk_matrix(self.camera.view_matrix().matrix)); - // Blit promoted (compositor-cached) nodes. - let mut layer_image_cache_hits = 0usize; - let promoted_set: std::collections::HashSet = - plan.promoted.iter().copied().collect(); - for id in &plan.promoted { - if let Some(layer_img) = self.scene_cache.compositor.get(id) { - let b = &layer_img.local_bounds; - let dst = Rect::from_xywh(b.x, b.y, b.width, b.height); - let mut paint = SkPaint::default(); - if layer_img.opacity < 1.0 { - paint.set_alpha_f(layer_img.opacity); - } - let cg_blend: crate::cg::types::BlendMode = layer_img.blend_mode.into(); - let sk_blend: skia_safe::BlendMode = cg_blend.into(); - paint.set_blend_mode(sk_blend); - - if layer_img.is_atlas_backed() { - if let Some((atlas_image, src_rect)) = - self.compositor_atlas.get_image_and_src_rect(id) - { - off_canvas.draw_image_rect( - atlas_image, - Some((&src_rect, skia_safe::canvas::SrcRectConstraint::Fast)), - dst, - &paint, - ); - } - } else if let Some(img) = layer_img.individual_image() { - let src = Rect::new(0.0, 0.0, img.width() as f32, img.height() as f32); - off_canvas.draw_image_rect( - img, - Some((&src, skia_safe::canvas::SrcRectConstraint::Fast)), - dst, - &paint, - ); - } - layer_image_cache_hits += 1; - } - } - - // Draw live (non-promoted) layers at reduced resolution. - // Promoted nodes are skipped — they were already blitted above. - let has_live_work = plan.regions.iter().any(|(_, indices)| !indices.is_empty()); - let promoted_skip = if promoted_set.is_empty() { + // Build promoted blit map and draw all layers with inline blitting. + let (promoted_blits, layer_image_cache_hits) = self.build_promoted_blits(plan); + let promoted_blits_ref = if promoted_blits.is_empty() { None } else { - Some(&promoted_set) - }; - let cache_picture_used = if has_live_work { - self.draw_layers_with_scene_cache_skip(off_canvas, plan, promoted_skip) - } else { - 0 + Some(&promoted_blits) }; + let cache_picture_used = + self.draw_layers_with_scene_cache_skip(off_canvas, plan, promoted_blits_ref); off_canvas.restore(); @@ -1486,14 +1653,22 @@ impl Renderer { /// during unstable (interactive) frames, and without limit on stable /// (settled) frames. Stale entries (zoom mismatch) are blitted with /// GPU texture stretching until re-rasterized. - fn update_compositor(&mut self, parent_surface: &mut Surface) { - self.update_compositor_inner(parent_surface, false); + fn update_compositor( + &mut self, + parent_surface: &mut Surface, + visible_indices: &[usize], + ) { + self.update_compositor_inner(parent_surface, false, visible_indices); } /// Variant called on stable frames — no time budget, all stale entries /// are re-rasterized to achieve full quality at the final zoom. - fn update_compositor_stable(&mut self, parent_surface: &mut Surface) { - self.update_compositor_inner(parent_surface, true); + fn update_compositor_stable( + &mut self, + parent_surface: &mut Surface, + visible_indices: &[usize], + ) { + self.update_compositor_inner(parent_surface, true, visible_indices); } /// Core compositor update logic. @@ -1502,7 +1677,12 @@ impl Renderer { /// re-rasterized without a time budget. When false (unstable/interactive /// frame), re-rasterization is capped to `ZOOM_RERASTER_BUDGET` to keep /// frame times low. - fn update_compositor_inner(&mut self, parent_surface: &mut Surface, force_all: bool) { + fn update_compositor_inner( + &mut self, + parent_surface: &mut Surface, + force_all: bool, + visible_indices: &[usize], + ) { use crate::cache::compositor::promotion; let zoom = self.camera.get_zoom(); @@ -1521,11 +1701,8 @@ impl Renderer { } } - // Only process layers visible in the current viewport. - let viewport_rect = self.camera.rect(); - let visible_indices = self.scene_cache.intersects(viewport_rect); - let visible_set: std::collections::HashSet = - visible_indices.into_iter().collect(); + // Visible indices are pre-computed by the frame plan's R-tree query + // and passed in to avoid a redundant spatial query each frame. // Time budget for stale re-rasterization during interactive frames. // 8ms leaves headroom within a 16ms frame budget (60fps target). @@ -1533,18 +1710,17 @@ impl Renderer { std::time::Duration::from_micros(8000); let budget_start = std::time::Instant::now(); - // Zoom scale bucket ratio — only re-rasterize when the zoom drift - // exceeds this threshold. Within the bucket, the GPU stretch is - // visually acceptable. - const RASTER_ZOOM_RATIO: f32 = 1.5; - - for (idx, entry) in self.scene_cache.layers.layers.iter().enumerate() { - if !visible_set.contains(&idx) { + for &idx in visible_indices { + let Some(entry) = self.scene_cache.layers.layers.get(idx) else { continue; - } + }; let id = entry.id; + // Note: visible_indices is pre-filtered by the frame plan to only + // include nodes with promotable effects (has_promotable_effects). + // No need to re-check here. + // Decide whether this node needs (re-)rasterization. // // State | Unstable frame | Stable frame (force_all) diff --git a/crates/grida-canvas/src/sys/mod.rs b/crates/grida-canvas/src/sys/mod.rs index 984425ce7c..b8789e03f6 100644 --- a/crates/grida-canvas/src/sys/mod.rs +++ b/crates/grida-canvas/src/sys/mod.rs @@ -1,4 +1,2 @@ pub mod clock; -pub mod scheduler; -pub mod timeout; pub mod timer; diff --git a/crates/grida-canvas/src/sys/scheduler.rs b/crates/grida-canvas/src/sys/scheduler.rs deleted file mode 100644 index 07da9bdbce..0000000000 --- a/crates/grida-canvas/src/sys/scheduler.rs +++ /dev/null @@ -1,113 +0,0 @@ -#[cfg(not(target_arch = "wasm32"))] -use std::collections::VecDeque; -#[cfg(not(target_arch = "wasm32"))] -use std::time::Instant; -use std::time::Duration; - -/// A module that controls frame pacing using target and max FPS limits, -/// while maintaining frame duration statistics for FPS estimation. -/// In WASM, the pacing logic is a no-op and the browser controls timing. -pub struct FrameScheduler { - #[cfg(not(target_arch = "wasm32"))] - last_frame_time: Instant, - target_frame_time: Duration, - max_frame_time: Option, - #[cfg(not(target_arch = "wasm32"))] - frame_durations: VecDeque, - #[cfg(not(target_arch = "wasm32"))] - max_samples: usize, -} - -impl FrameScheduler { - /// Creates a new scheduler with a given target FPS and rolling sample size. - pub fn new(target_fps: u32) -> Self { - Self { - #[cfg(not(target_arch = "wasm32"))] - last_frame_time: Instant::now(), - target_frame_time: Duration::from_micros(1_000_000 / target_fps as u64), - max_frame_time: None, - #[cfg(not(target_arch = "wasm32"))] - frame_durations: VecDeque::with_capacity(60), - #[cfg(not(target_arch = "wasm32"))] - max_samples: 60, - } - } - - /// Sets a maximum FPS cap to prevent over-drawing on high-refresh displays. - pub fn with_max_fps(mut self, max_fps: u32) -> Self { - self.max_frame_time = Some(Duration::from_micros(1_000_000 / max_fps as u64)); - self - } - - /// Disable frame pacing entirely (uncapped FPS). - /// Useful for benchmarking raw render throughput. - pub fn with_no_limit(mut self) -> Self { - self.target_frame_time = Duration::ZERO; - self.max_frame_time = None; - self - } - - /// Records the most recent frame duration for smoothing. - #[cfg(not(target_arch = "wasm32"))] - fn record_frame_duration(&mut self, duration: Duration) { - if self.frame_durations.len() == self.max_samples { - self.frame_durations.pop_front(); - } - self.frame_durations.push_back(duration); - } - - /// Returns the average FPS based on the last N recorded frames. - #[cfg(not(target_arch = "wasm32"))] - pub fn average_fps(&self) -> f32 { - if self.frame_durations.is_empty() { - return 0.0; - } - - let total: Duration = self.frame_durations.iter().copied().sum(); - let avg = total / self.frame_durations.len() as u32; - 1_000_000.0 / avg.as_micros() as f32 - } - - /// In WASM, the browser controls frame rate; no frame duration tracking. - #[cfg(target_arch = "wasm32")] - pub fn average_fps(&self) -> f32 { - 0.0 - } - - /// No-op in WASM; browser controls frame rate via rAF. - #[cfg(target_arch = "wasm32")] - pub fn sleep_to_maintain_fps(&mut self) { - // no-op - } - - /// For native platforms, enforces frame pacing and tracks durations. - #[cfg(not(target_arch = "wasm32"))] - pub fn sleep_to_maintain_fps(&mut self) { - let now = Instant::now(); - let elapsed = now.duration_since(self.last_frame_time); - - let target = match self.max_frame_time { - Some(max_time) => self.target_frame_time.max(max_time), - None => self.target_frame_time, - }; - - if elapsed < target { - std::thread::sleep(target - elapsed); - } - - let end = Instant::now(); - let frame_duration = end.duration_since(self.last_frame_time); - self.record_frame_duration(frame_duration); - self.last_frame_time = end; - } - - /// Returns the configured target frame time. - pub fn get_target_frame_time(&self) -> Duration { - self.target_frame_time - } - - /// Returns the configured maximum frame time, if any. - pub fn get_max_frame_time(&self) -> Option { - self.max_frame_time - } -} diff --git a/crates/grida-canvas/src/sys/timeout.rs b/crates/grida-canvas/src/sys/timeout.rs deleted file mode 100644 index 914ecf1c9c..0000000000 --- a/crates/grida-canvas/src/sys/timeout.rs +++ /dev/null @@ -1,63 +0,0 @@ -use std::time::{Duration, Instant}; - -/// A one-shot timeout timer driven by an external clock. -/// -/// `Timeout` represents a countdown timer that expires after a specified duration, -/// using externally supplied time (e.g. from a centralized tick-based clock). -/// It does not track time itself and must be ticked manually via the caller. -/// -/// # Example -/// ```rust -/// use std::time::{Duration, Instant}; -/// use cg::sys::timeout::Timeout; -/// -/// let mut timeout = Timeout::new(); -/// let now = Instant::now(); -/// -/// // Start a 1-second timeout -/// timeout.start(Duration::from_secs(1), now); -/// -/// // Later in your loop or tick function -/// let later = now + Duration::from_millis(1200); -/// if timeout.is_expired(later) { -/// println!("Timeout expired!"); -/// } -/// ``` -pub struct Timeout { - deadline: Option, -} - -impl Timeout { - /// Creates a new inactive timeout. - /// - /// To activate, call [`start`] with a duration and the current time. - pub fn new() -> Self { - Self { deadline: None } - } - - /// Starts or restarts the timeout with the given duration, - /// using the provided `now` time as the starting point. - pub fn start(&mut self, duration: Duration, now: Instant) { - self.deadline = Some(now + duration); - } - - /// Returns `true` if the timeout has expired at the given `now` time. - /// - /// Returns `false` if the timeout is inactive or not yet expired. - pub fn is_expired(&self, now: Instant) -> bool { - match self.deadline { - Some(deadline) => now >= deadline, - None => false, - } - } - - /// Clears the timeout, making it inactive. - pub fn clear(&mut self) { - self.deadline = None; - } - - /// Returns `true` if the timeout is currently active. - pub fn is_active(&self) -> bool { - self.deadline.is_some() - } -} diff --git a/crates/grida-canvas/src/window/application.rs b/crates/grida-canvas/src/window/application.rs index 55b37bcac5..af59490eaf 100644 --- a/crates/grida-canvas/src/window/application.rs +++ b/crates/grida-canvas/src/window/application.rs @@ -10,8 +10,8 @@ use crate::node::schema::*; use crate::resources::{FontMessage, ImageMessage}; use crate::runtime::camera::Camera2D; use crate::runtime::scene::{Backend, FrameFlushResult, Renderer}; +use crate::runtime::frame_loop::{FrameLoop, FrameQuality}; use crate::sys::clock; -use crate::sys::scheduler; use crate::sys::timer::TimerMgr; use crate::text; use crate::vectornetwork::VectorNetwork; @@ -152,7 +152,6 @@ pub struct UnknownTargetApplication { pub(crate) clock: clock::EventLoopClock, pub(crate) timer: TimerMgr, pub(crate) clipboard: Clipboard, - pub(crate) scheduler: scheduler::FrameScheduler, pub(crate) request_redraw: crate::runtime::scene::RequestRedrawCallback, pub(crate) renderer: Renderer, pub(crate) state: super::state::AnySurfaceState, @@ -175,10 +174,8 @@ pub struct UnknownTargetApplication { pub(crate) devtools_rendering_show_stats: bool, pub(crate) devtools_rendering_show_hit_overlay: bool, pub(crate) devtools_rendering_show_ruler: bool, - pub(crate) queue_stable_debounce_millis: u64, - - /// timer id for debouncing stable frame queues - queue_stable_timer: Option, + /// Unified frame lifecycle controller. + frame_loop: FrameLoop, /// Bidirectional mapping between user string IDs and internal u64 IDs /// Maintained across scene loads to enable API calls with string IDs @@ -592,7 +589,7 @@ impl UnknownTargetApplication { state: super::state::AnySurfaceState, backend: Backend, camera: Camera2D, - target_fps: u32, + _target_fps: u32, #[cfg(not(target_arch = "wasm32"))] image_rx: mpsc::UnboundedReceiver, #[cfg(not(target_arch = "wasm32"))] font_rx: mpsc::UnboundedReceiver, request_redraw: Option, @@ -621,7 +618,6 @@ impl UnknownTargetApplication { image_rx, #[cfg(not(target_arch = "wasm32"))] font_rx, - scheduler: scheduler::FrameScheduler::new(target_fps).with_max_fps(target_fps), last_frame_time: std::time::Instant::now(), last_stats: None, devtools_selection: None, @@ -633,8 +629,7 @@ impl UnknownTargetApplication { devtools_rendering_show_hit_overlay: debug, devtools_rendering_show_ruler: debug, timer: TimerMgr::new(), - queue_stable_timer: None, - queue_stable_debounce_millis: 50, + frame_loop: FrameLoop::new(), id_mapping: std::collections::HashMap::new(), id_mapping_reverse: std::collections::HashMap::new(), running: true, @@ -767,27 +762,98 @@ impl UnknownTargetApplication { } fn queue(&mut self) { + // Invalidate the frame loop — it will handle unstable/stable + // scheduling automatically via poll()/complete(). + let now = self.clock.now(); + self.frame_loop.invalidate(now); + + // Legacy path: also eager-queue an unstable frame on the renderer + // so that the existing `redraw()` flow continues to work when + // hosts call the old `redraw()` entry point. self.renderer.queue_unstable(); + } + + /// Unified frame entry point. + /// + /// Called once per host frame (e.g. from RAF on WASM, RedrawRequested on + /// native). The engine decides whether to produce pixels and at what + /// quality. Returns `true` if a frame was rendered. + pub fn frame(&mut self, time: f64) -> bool { + // 1. Advance host clock + self.clock.tick(time); - if let Some(id) = self.queue_stable_timer.take() { - self.timer.cancel(id); + // 2. Fire non-frame timers (text blink, etc.) + // Timer callbacks may call invalidate() on the frame loop — that's + // fine, invalidate() just sets flags. + self.timer.tick(self.clock.now()); + + // Drive the text-edit clock from the host's wall time. + #[cfg(target_arch = "wasm32")] + grida_text_edit::time::Instant::set_micros((time * 1000.0) as u64); + + // 3. Process async resources (native only) + #[cfg(not(target_arch = "wasm32"))] + { + self.process_image_queue(); + self.process_font_queue(); } - let renderer_ptr: *mut Renderer = &mut self.renderer; - self.queue_stable_timer = Some(self.timer.set_timeout( - std::time::Duration::from_millis(self.queue_stable_debounce_millis), - move || unsafe { - (*renderer_ptr).queue_stable(); - }, - )); + // 4. Poll frame loop — should we render? + let now = self.clock.now(); + let quality = match self.frame_loop.poll(now) { + Some(q) => q, + None => return false, // idle + }; + + // 5. Build plan + render + let __frame_start = std::time::Instant::now(); - // TODO: can't use debounce - let's try this later - // self.debounce( - // std::time::Duration::from_millis(100), - // || self.renderer.queue_stable(), - // false, - // true, - // ); + // Prepare camera change for the renderer + let camera_change = self.renderer.camera.change_kind(); + if camera_change.zoom_changed() { + self.renderer.invalidate_compositor_on_zoom(); + } + + // Promote to stable quality when the camera didn't change — there is + // no reason to render at reduced resolution for non-camera + // invalidations (hit-test highlight, scene edits, etc.). + let stable = quality == FrameQuality::Stable || !camera_change.any_changed(); + + // 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(); + + // Build frame plan lazily + let rect = self.renderer.camera.rect(); + let zoom = self.renderer.camera.get_zoom(); + let plan = self.renderer.build_frame_plan(rect, zoom, stable, camera_change); + + // Consume the camera change so the next frame sees None + // (unless a new mutation occurs before then). + self.renderer.camera.consume_change(); + + // Flush (draw + GPU submit) + let stats = self.renderer.flush_with_plan(plan); + + // 6. Stats bookkeeping — update *before* the overlay so the overlay + // always shows the current frame's data (not the previous frame's). + // This matters for the stable frame: it's the last frame before idle, + // so the overlay text it paints is what the user sees until the next + // interaction. + let __render_time = __frame_start.elapsed(); + if let Some(ref stats) = stats { + self.update_stats(stats, __render_time); + } + + // 7. Draw devtools overlays (uses the just-updated last_stats) + let _overlay_time = self.draw_and_flush_devtools_overlay(); + + // 8. Complete frame in the loop + self.frame_loop.complete(quality); + + self.last_frame_time = __frame_start; + + true } #[cfg(not(target_arch = "wasm32"))] @@ -967,47 +1033,21 @@ impl UnknownTargetApplication { } }; - let overlay_time = self.draw_and_flush_devtools_overlay(); + // Consume the camera change so the next change_kind() returns None + // (unless a new mutation occurs). This must happen here (not in + // renderer.flush()) because on the web path both redraw() and frame() + // may run: consuming in flush() would eat the change before frame() + // sees it. Completing the frame loop prevents frame() from rendering + // a redundant second frame for the same invalidation. + self.renderer.camera.consume_change(); + self.frame_loop.complete(crate::runtime::frame_loop::FrameQuality::Unstable); - let __sleep_start = std::time::Instant::now(); - self.scheduler.sleep_to_maintain_fps(); - let __sleep_time = __sleep_start.elapsed(); + // Build stats string BEFORE the overlay so the overlay shows the + // current frame's data, not the previous frame's. + let __total_frame_time_pre = __frame_start.elapsed(); + self.update_stats(&stats, __total_frame_time_pre); - let __total_frame_time = __frame_start.elapsed(); - let camera_label = match stats.frame.camera_change { - crate::runtime::camera::CameraChangeKind::None => "none", - crate::runtime::camera::CameraChangeKind::PanOnly => "pan", - crate::runtime::camera::CameraChangeKind::ZoomIn => "zoom-in", - crate::runtime::camera::CameraChangeKind::ZoomOut => "zoom-out", - crate::runtime::camera::CameraChangeKind::PanAndZoom => "pan+zoom", - }; - let stat_string = format!( - "fps*: {:.0} | t: {:.2}ms | cam: {} | render: {:.1}ms | flush: {:.1}ms | overlays: {:.1}ms | frame: {:.1}ms | list: {:.1}ms ({:?}) | draw: {:.1}ms | $:pic: {:?} ({:?} use) | $:geo: {:?} | comp: {:?} ({:?} hit, {:.1}KB) | live: {:?} | res: {} | img: {} | fnt: {}", - 1.0 / __total_frame_time.as_secs_f64(), - __total_frame_time.as_secs_f64() * 1000.0, - camera_label, - stats.total_duration.as_secs_f64() * 1000.0, - stats.flush_duration.as_secs_f64() * 1000.0, - overlay_time.as_secs_f64() * 1000.0, - stats.frame_duration.as_secs_f64() * 1000.0, - stats.frame.display_list_duration.as_secs_f64() * 1000.0, - stats.frame.display_list_size_estimated, - stats.draw.painter_duration.as_secs_f64() * 1000.0, - stats.draw.cache_picture_size, - stats.draw.cache_picture_used, - stats.draw.cache_geometry_size, - stats.draw.layer_image_cache_size, - stats.draw.layer_image_cache_hits, - stats.draw.layer_image_cache_bytes as f64 / 1024.0, - stats.draw.live_draw_count, - self.renderer.resources.len(), - self.renderer.images.len(), - self.renderer.fonts.len(), - ); - - self.verbose(&stat_string); - - self.last_stats = Some(stat_string); + let _overlay_time = self.draw_and_flush_devtools_overlay(); self.last_frame_time = __frame_start; } @@ -1021,7 +1061,7 @@ impl UnknownTargetApplication { let surface = self.state.surface_mut(); let canvas = surface.canvas(); if self.devtools_rendering_show_fps { - fps_overlay::FpsMeter::draw(&canvas, self.scheduler.average_fps()); + fps_overlay::FpsMeter::draw(&canvas, self.clock.hz() as f32); } if self.devtools_rendering_show_stats { if let Some(s) = self.last_stats.as_deref() { @@ -1074,6 +1114,38 @@ impl UnknownTargetApplication { overlay_flush_time + overlay_draw_time } + /// Format and store the frame stats string for the devtools overlay. + fn update_stats( + &mut self, + stats: &crate::runtime::scene::FrameFlushStats, + wall_time: std::time::Duration, + ) { + let s = format!( + "fps*: {:.0} | t: {:.2}ms | cam: {} | render: {:.1}ms | flush: {:.1}ms | frame: {:.1}ms | list: {:.1}ms ({:?}) | draw: {:.1}ms | $:pic: {:?} ({:?} use) | $:geo: {:?} | comp: {:?} ({:?} hit, {:.1}KB) | live: {:?} | res: {} | img: {} | fnt: {}", + 1.0 / wall_time.as_secs_f64(), + wall_time.as_secs_f64() * 1000.0, + stats.frame.camera_change.label(), + stats.total_duration.as_secs_f64() * 1000.0, + stats.flush_duration.as_secs_f64() * 1000.0, + stats.frame_duration.as_secs_f64() * 1000.0, + stats.frame.display_list_duration.as_secs_f64() * 1000.0, + stats.frame.display_list_size_estimated, + stats.draw.painter_duration.as_secs_f64() * 1000.0, + stats.draw.cache_picture_size, + stats.draw.cache_picture_used, + stats.draw.cache_geometry_size, + stats.draw.layer_image_cache_size, + stats.draw.layer_image_cache_hits, + stats.draw.layer_image_cache_bytes as f64 / 1024.0, + stats.draw.live_draw_count, + self.renderer.resources.len(), + self.renderer.images.len(), + self.renderer.fonts.len(), + ); + self.verbose(&s); + self.last_stats = Some(s); + } + /// Update the cursor position and run a debounced hit test. #[allow(dead_code)] pub fn pointer_move(&mut self, x: f32, y: f32) { diff --git a/crates/grida-canvas/src/window/application_emscripten.rs b/crates/grida-canvas/src/window/application_emscripten.rs index 71eabc5b43..88eaac52d9 100644 --- a/crates/grida-canvas/src/window/application_emscripten.rs +++ b/crates/grida-canvas/src/window/application_emscripten.rs @@ -97,7 +97,11 @@ unsafe extern "C" fn request_animation_frame_callback_unknown_target( return false; } - app.tick(time); + // Use the unified frame() entry point — this drives the clock, + // timers, and the FrameLoop (poll → flush → complete) in one call. + // This fixes the web-host stable-frame bug: after pan/zoom stops, + // FrameLoop::poll() returns Stable once the debounce expires. + app.frame(time); } true } diff --git a/crates/grida-canvas/tests/camera_change_kind.rs b/crates/grida-canvas/tests/camera_change_kind.rs new file mode 100644 index 0000000000..96fdd2509b --- /dev/null +++ b/crates/grida-canvas/tests/camera_change_kind.rs @@ -0,0 +1,610 @@ +//! Tests for CameraChangeKind classification through the full Renderer +//! queue/flush cycle. +//! +//! These reproduce real gesture sequences: pan, pinch-zoom, cmd+scroll zoom, +//! and transitions between them. Each test calls the same methods the app +//! layer calls (camera.translate, camera.set_zoom, camera.set_zoom_at, +//! renderer.queue_unstable, renderer.flush) so that any state-management +//! bugs surface here, not only in manual testing with a trackpad. + +use cg::cg::prelude::*; +use cg::node::factory::NodeFactory; +use cg::node::scene_graph::{Parent, SceneGraph}; +use cg::node::schema::*; +use cg::runtime::camera::{Camera2D, CameraChangeKind}; +use cg::runtime::scene::{Backend, FrameFlushResult, FrameFlushStats, Renderer}; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +fn create_grid() -> Scene { + let nf = NodeFactory::new(); + let mut graph = SceneGraph::new(); + for y in 0..10u32 { + for x in 0..10u32 { + let mut rect = nf.create_rectangle_node(); + rect.transform = + math2::transform::AffineTransform::new(x as f32 * 30.0, y as f32 * 30.0, 0.0); + rect.size = Size { + width: 20.0, + height: 20.0, + }; + rect.set_fill(Paint::Solid(SolidPaint::RED)); + graph.append_child(Node::Rectangle(rect), Parent::Root); + } + } + Scene { + name: "grid".into(), + graph, + background_color: None, + } +} + +fn make_renderer() -> Renderer { + let mut r = Renderer::new( + Backend::new_from_raster(200, 200), + None, + Camera2D::new(Size { + width: 200.0, + height: 200.0, + }), + ); + r.load_scene(create_grid()); + // load_scene queues a stable frame; flush it so we start clean. + let _ = r.flush(); + r +} + +/// Queue an unstable frame and flush, returning the stats. +/// This mirrors the app's: camera.mutate() → queue_unstable() → flush() → +/// consume_change(). The consume is the app's responsibility (not the +/// renderer's) so we do it here to match real behavior. +fn queue_flush(r: &mut Renderer) -> FrameFlushStats { + r.queue_unstable(); + let result = match r.flush() { + FrameFlushResult::OK(s) => s, + other => panic!( + "expected OK, got {:?}", + match other { + FrameFlushResult::NoPending => "NoPending", + FrameFlushResult::NoFrame => "NoFrame", + FrameFlushResult::NoScene => "NoScene", + _ => "OK", + } + ), + }; + r.camera.consume_change(); + result +} + +fn cam_label(kind: CameraChangeKind) -> &'static str { + match kind { + CameraChangeKind::None => "none", + CameraChangeKind::PanOnly => "pan", + CameraChangeKind::ZoomIn => "zoom-in", + CameraChangeKind::ZoomOut => "zoom-out", + CameraChangeKind::PanAndZoom(true) => "pan+zoom-in", + CameraChangeKind::PanAndZoom(false) => "pan+zoom-out", + } +} + +/// Simulate what the app's command(Pan) does: divide by zoom, translate, queue. +fn app_pan(r: &mut Renderer, tx: f32, ty: f32) { + let zoom = r.camera.get_zoom(); + r.camera.translate(tx * (1.0 / zoom), ty * (1.0 / zoom)); + r.queue_unstable(); +} + +/// Simulate what the app's command(ZoomDelta) does: multiply zoom, set_zoom_at. +fn app_zoom_delta(r: &mut Renderer, delta: f32, cursor: [f32; 2]) { + let current_zoom = r.camera.get_zoom(); + let zoom_factor = 1.0 + delta; + if zoom_factor.is_finite() && zoom_factor > 0.0 { + r.camera.set_zoom_at(current_zoom * zoom_factor, cursor); + } + r.queue_unstable(); +} + +/// Simulate the app's redraw() → flush() → consume_change(). +fn app_redraw(r: &mut Renderer) -> Option { + match r.flush() { + FrameFlushResult::OK(s) => { + r.camera.consume_change(); + Some(s) + } + _ => None, + } +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +#[test] +fn pan_only_reports_pan() { + let mut r = make_renderer(); + r.camera.translate(5.0, 0.0); + let s = queue_flush(&mut r); + assert_eq!(s.frame.camera_change, CameraChangeKind::PanOnly); +} + +#[test] +fn set_zoom_reports_zoom_in() { + let mut r = make_renderer(); + r.camera.set_zoom(2.0); + let s = queue_flush(&mut r); + assert_eq!(s.frame.camera_change, CameraChangeKind::ZoomIn); +} + +#[test] +fn set_zoom_reports_zoom_out() { + let mut r = make_renderer(); + r.camera.set_zoom(0.5); + let s = queue_flush(&mut r); + assert_eq!(s.frame.camera_change, CameraChangeKind::ZoomOut); +} + +#[test] +fn set_zoom_at_center_reports_zoom_not_pan() { + // Zooming at viewport center produces no translation shift. + let mut r = make_renderer(); + r.camera.set_zoom_at(2.0, [100.0, 100.0]); + let s = queue_flush(&mut r); + assert!( + !s.frame.camera_change.pan_changed(), + "zoom at center should NOT report pan, got: {}", + cam_label(s.frame.camera_change) + ); + assert!(s.frame.camera_change.zoom_changed()); +} + +#[test] +fn set_zoom_at_off_center_reports_pan_and_zoom() { + // Zooming off-center adjusts translation to keep the focal point fixed. + let mut r = make_renderer(); + r.camera.set_zoom_at(2.0, [150.0, 150.0]); + let s = queue_flush(&mut r); + assert!( + matches!(s.frame.camera_change, CameraChangeKind::PanAndZoom(_)), + "zoom at off-center should be PanAndZoom, got: {:?}", + s.frame.camera_change + ); +} + +// --- Transition sequences --- + +#[test] +fn pan_then_zoom_then_pan() { + // User reports: "pan → zoom-in → pan shows pan" — correct. + let mut r = make_renderer(); + + r.camera.translate(5.0, 0.0); + let s = queue_flush(&mut r); + assert_eq!(s.frame.camera_change, CameraChangeKind::PanOnly, "step 1: pan"); + + r.camera.set_zoom(2.0); + let s = queue_flush(&mut r); + assert_eq!(s.frame.camera_change, CameraChangeKind::ZoomIn, "step 2: zoom-in"); + + r.camera.translate(5.0, 0.0); + let s = queue_flush(&mut r); + assert_eq!(s.frame.camera_change, CameraChangeKind::PanOnly, "step 3: pan after zoom"); +} + +#[test] +fn zoom_in_zoom_out_then_pan() { + // User reports: "zoom in+out → stuck at pan+zoom when panning" + let mut r = make_renderer(); + + r.camera.set_zoom(2.0); + let s = queue_flush(&mut r); + assert!(s.frame.camera_change.zoom_changed(), "step 1: zoom-in"); + + r.camera.set_zoom(0.5); + let s = queue_flush(&mut r); + assert!(s.frame.camera_change.zoom_changed(), "step 2: zoom-out"); + + r.camera.translate(5.0, 0.0); + let s = queue_flush(&mut r); + assert_eq!( + s.frame.camera_change, + CameraChangeKind::PanOnly, + "step 3: pan after zoom-in+out should be PanOnly, got: {}", + cam_label(s.frame.camera_change) + ); +} + +#[test] +fn pinch_zoom_in_out_then_pan() { + // Pinch zoom (set_zoom_at) in+out then pan. + // set_zoom_at off-center produces PanAndZoom. After gesture ends, + // pure pan must report PanOnly. + let mut r = make_renderer(); + + r.camera.set_zoom_at(2.0, [150.0, 150.0]); + let s = queue_flush(&mut r); + assert!(matches!(s.frame.camera_change, CameraChangeKind::PanAndZoom(_)), "step 1: pinch-in, got: {:?}", s.frame.camera_change); + + r.camera.set_zoom_at(0.5, [150.0, 150.0]); + let s = queue_flush(&mut r); + assert!(s.frame.camera_change.zoom_changed(), "step 2: pinch-out"); + + r.camera.translate(5.0, 0.0); + let s = queue_flush(&mut r); + assert_eq!( + s.frame.camera_change, + CameraChangeKind::PanOnly, + "step 3: pan after pinch in+out should be PanOnly, got: {}", + cam_label(s.frame.camera_change) + ); +} + +#[test] +fn rapid_pinch_oscillation_then_pan() { + // Rapid pinch in/out for 20 frames, then pure pan. + let mut r = make_renderer(); + + for i in 0..20 { + let z = if i % 2 == 0 { 1.5 } else { 0.8 }; + r.camera.set_zoom_at(z, [120.0, 120.0]); + let _ = queue_flush(&mut r); + } + + r.camera.translate(10.0, 0.0); + let s = queue_flush(&mut r); + assert_eq!( + s.frame.camera_change, + CameraChangeKind::PanOnly, + "pan after rapid pinch oscillation should be PanOnly, got: {}", + cam_label(s.frame.camera_change) + ); +} + +#[test] +fn no_change_after_flush_reports_none() { + // After flush, queuing again without mutation should produce None. + // (In practice, the app wouldn't queue without a mutation, but this + // verifies consume_change works.) + let mut r = make_renderer(); + + r.camera.translate(5.0, 0.0); + let s = queue_flush(&mut r); + assert_eq!(s.frame.camera_change, CameraChangeKind::PanOnly); + + // Queue again without any camera change. + let s = queue_flush(&mut r); + assert_eq!( + s.frame.camera_change, + CameraChangeKind::None, + "no mutation after flush should be None, got: {}", + cam_label(s.frame.camera_change) + ); +} + +#[test] +fn steady_after_zoom_reports_none() { + // User reports: "when steady after zoom, shows zoom-out instead of none" + let mut r = make_renderer(); + + r.camera.set_zoom(0.5); + let s = queue_flush(&mut r); + assert_eq!(s.frame.camera_change, CameraChangeKind::ZoomOut); + + // Steady — queue again without mutation. + let s = queue_flush(&mut r); + assert_eq!( + s.frame.camera_change, + CameraChangeKind::None, + "steady after zoom should be None, got: {}", + cam_label(s.frame.camera_change) + ); +} + +#[test] +fn steady_after_pinch_reports_none() { + // Steady after pinch gesture. + let mut r = make_renderer(); + + r.camera.set_zoom_at(2.0, [150.0, 150.0]); + let s = queue_flush(&mut r); + assert!(s.frame.camera_change.zoom_changed()); + + // Steady — queue again without mutation. + let s = queue_flush(&mut r); + assert_eq!( + s.frame.camera_change, + CameraChangeKind::None, + "steady after pinch should be None, got: {}", + cam_label(s.frame.camera_change) + ); +} + +#[test] +fn cmd_scroll_zoom_out_reports_zoom_out() { + // Cmd+scroll uses set_zoom (no focal point), should report ZoomOut. + let mut r = make_renderer(); + + // Simulate multiple cmd+scroll zoom-out steps + for _ in 0..5 { + let z = r.camera.get_zoom(); + r.camera.set_zoom(z * (1.0 - 0.01)); // zoom_factor slightly < 1 + let s = queue_flush(&mut r); + assert_eq!( + s.frame.camera_change, + CameraChangeKind::ZoomOut, + "cmd+scroll zoom-out should be ZoomOut, got: {}", + cam_label(s.frame.camera_change) + ); + } +} + +#[test] +fn pinch_zoom_out_reports_zoom_changed() { + // Pinch zoom-out uses set_zoom_at. Off-center: PanAndZoom is correct + // because focal-point compensation IS a translation. At center: ZoomOut. + let mut r = make_renderer(); + + // Off-center pinch zoom-out + r.camera.set_zoom_at(0.5, [150.0, 150.0]); + let s = queue_flush(&mut r); + assert!( + s.frame.camera_change.zoom_changed(), + "pinch zoom-out should have zoom_changed, got: {}", + cam_label(s.frame.camera_change) + ); + // Note: PanAndZoom is correct here — the translation shift is real. +} + +#[test] +fn multiple_queue_without_flush_uses_last() { + // If queue_unstable is called multiple times before flush, the last + // plan wins. This simulates multiple events between redraws. + let mut r = make_renderer(); + + r.camera.translate(5.0, 0.0); + r.queue_unstable(); // plan 1: PanOnly + + r.camera.set_zoom(2.0); + r.queue_unstable(); // plan 2: ZoomIn (overwrites plan 1) + + let s = match r.flush() { + FrameFlushResult::OK(s) => s, + _ => panic!("expected OK"), + }; + r.camera.consume_change(); + assert!( + s.frame.camera_change.zoom_changed(), + "last queue wins: should show zoom, got: {}", + cam_label(s.frame.camera_change) + ); +} + +// --------------------------------------------------------------------------- +// App-level simulation tests (exact app command flow) +// --------------------------------------------------------------------------- + +#[test] +fn app_pan_only_never_zoom() { + // "if pan only, and never zoom, all works perfectly fine" + let mut r = make_renderer(); + + for _ in 0..10 { + app_pan(&mut r, 4.0, 2.0); + let s = app_redraw(&mut r).expect("should render"); + assert_eq!( + s.frame.camera_change, + CameraChangeKind::PanOnly, + "pure pan should always be PanOnly, got: {}", + cam_label(s.frame.camera_change) + ); + } +} + +#[test] +fn app_steady_after_zoom_shows_none() { + // "when steady, instead of `none` we see `zoom-out`" + let mut r = make_renderer(); + let cursor = [100.0, 100.0]; + + // Zoom out + app_zoom_delta(&mut r, -0.01, cursor); + let s = app_redraw(&mut r).expect("should render"); + assert!(s.frame.camera_change.zoom_changed(), + "zoom frame should show zoom, got: {}", cam_label(s.frame.camera_change)); + + // Steady: no mutation, but queue+flush again + r.queue_unstable(); + let s = app_redraw(&mut r).expect("should render"); + assert_eq!( + s.frame.camera_change, + CameraChangeKind::None, + "steady after zoom should be None, got: {}", + cam_label(s.frame.camera_change) + ); +} + +#[test] +fn app_zoom_out_via_pinch_classification() { + // "when zoom out, we almost always see pan+zoom" + // Pinch zoom uses set_zoom_at which adjusts translation for focal point. + // At center: no translation → ZoomOut. Off-center: PanAndZoom. + let mut r = make_renderer(); + + // Pinch at center + app_zoom_delta(&mut r, -0.01, [100.0, 100.0]); + let s = app_redraw(&mut r).expect("should render"); + assert_eq!(s.frame.camera_change, CameraChangeKind::ZoomOut, + "pinch at center should be ZoomOut, got: {}", cam_label(s.frame.camera_change)); + + // Pinch at off-center (typical real-world) + app_zoom_delta(&mut r, -0.01, [150.0, 150.0]); + let s = app_redraw(&mut r).expect("should render"); + // PanAndZoom is CORRECT here — the focal-point translation is real. + assert!(matches!(s.frame.camera_change, CameraChangeKind::PanAndZoom(_)), + "pinch at off-center should be PanAndZoom, got: {}", cam_label(s.frame.camera_change)); +} + +#[test] +fn app_pinch_zoom_in_out_then_pan() { + // "when we did both zoom in and out, it stucks at pan+zoom, and never hits pan" + let mut r = make_renderer(); + let cursor = [120.0, 120.0]; + + // Simulate 10 pinch-in events + for _ in 0..10 { + app_zoom_delta(&mut r, 0.01, cursor); + let _ = app_redraw(&mut r); + } + + // Simulate 10 pinch-out events + for _ in 0..10 { + app_zoom_delta(&mut r, -0.01, cursor); + let _ = app_redraw(&mut r); + } + + // Now pure pan + for i in 0..5 { + app_pan(&mut r, 4.0, 0.0); + let s = app_redraw(&mut r).expect("should render"); + assert_eq!( + s.frame.camera_change, + CameraChangeKind::PanOnly, + "pan frame {} after pinch in+out should be PanOnly, got: {}", + i, cam_label(s.frame.camera_change) + ); + } +} + +#[test] +fn app_cmd_scroll_zoom_then_settle() { + // "when zoom (in or out) via cmd+scroll, it will at least settle, to zoom-out" + // cmd+scroll uses set_zoom (no focal point) → ZoomIn/ZoomOut (no pan component) + let mut r = make_renderer(); + + // Simulate cmd+scroll zoom out (set_zoom directly, not set_zoom_at) + for _ in 0..5 { + let z = r.camera.get_zoom(); + r.camera.set_zoom(z * 0.99); + r.queue_unstable(); + let s = app_redraw(&mut r).expect("should render"); + assert_eq!(s.frame.camera_change, CameraChangeKind::ZoomOut, + "cmd+scroll zoom-out should be ZoomOut, got: {}", cam_label(s.frame.camera_change)); + } +} + +#[test] +fn app_pinch_never_settles_repro() { + // "when zoom via pinch, it never settles, always showing pan+zoom" + // After pinch ends, the next redraw without mutation should show None. + let mut r = make_renderer(); + let cursor = [120.0, 120.0]; + + // Pinch zoom (multiple events) + for _ in 0..5 { + app_zoom_delta(&mut r, 0.02, cursor); + let _ = app_redraw(&mut r); + } + + // Gesture ends. Simulate "idle" redraw. + r.queue_unstable(); + let s = app_redraw(&mut r).expect("should render"); + assert_eq!( + s.frame.camera_change, + CameraChangeKind::None, + "after pinch gesture ends, idle frame should be None, got: {}", + cam_label(s.frame.camera_change) + ); +} + +#[test] +fn app_first_pan_after_zoom_shows_pan_not_zoom() { + // "show zoom-out on first frame, when even not zooming out (first frame when panning)" + let mut r = make_renderer(); + let cursor = [120.0, 120.0]; + + // Zoom out via pinch + for _ in 0..5 { + app_zoom_delta(&mut r, -0.02, cursor); + let _ = app_redraw(&mut r); + } + + // First pan after zoom + app_pan(&mut r, 4.0, 0.0); + let s = app_redraw(&mut r).expect("should render"); + assert_eq!( + s.frame.camera_change, + CameraChangeKind::PanOnly, + "first pan after zoom should be PanOnly, got: {}", + cam_label(s.frame.camera_change) + ); +} + +#[test] +fn app_float_drift_after_many_pinch_zoom_at() { + // Reproduces the exact bug from debug.log: after many set_zoom_at calls, + // the sin/cos extraction accumulates floating-point drift so that + // has_zoom_changed() falsely returns true on subsequent pan-only frames. + // + // The sequence: zoom-out (many steps) → zoom-in (many steps) → pan. + // The pan frames must report PanOnly, not PanAndZoom. + let mut r = make_renderer(); + + // Phase 1: pinch zoom out (20 steps, like lines 117-154 in debug.log) + for _ in 0..20 { + app_zoom_delta(&mut r, -0.02, [120.0, 80.0]); + let _ = app_redraw(&mut r); + } + + // Phase 2: pinch zoom in (30 steps, like lines 155-212) + for _ in 0..30 { + app_zoom_delta(&mut r, 0.015, [120.0, 80.0]); + let _ = app_redraw(&mut r); + } + + // Phase 3: pan only (like lines 213+ where zoom=constant but was PanAndZoom) + for i in 0..20 { + app_pan(&mut r, 4.0, 2.0); + let s = app_redraw(&mut r).expect("should render"); + assert_eq!( + s.frame.camera_change, + CameraChangeKind::PanOnly, + "pan frame {} after 50 pinch-zoom steps must be PanOnly (float drift bug), got: {}", + i, + cam_label(s.frame.camera_change) + ); + } +} + +// --------------------------------------------------------------------------- +// Original unit-level tests +// --------------------------------------------------------------------------- + +#[test] +fn interleaved_pinch_and_scroll_events() { + // Simulates macOS sending PinchGesture + MouseWheel in the same + // frame, each triggering queue_unstable. + let mut r = make_renderer(); + + // Pinch event + r.camera.set_zoom_at(1.5, [120.0, 120.0]); + r.queue_unstable(); + + // Scroll event in same frame + r.camera.translate(5.0, 0.0); + r.queue_unstable(); // overwrites the plan + + let s = match r.flush() { + FrameFlushResult::OK(s) => s, + _ => panic!("expected OK"), + }; + r.camera.consume_change(); + // translate's before_change saved the post-zoom state; the delta is + // only the translation. So the last plan should be PanOnly. + assert_eq!( + s.frame.camera_change, + CameraChangeKind::PanOnly, + "last event was translate → PanOnly, got: {}", + cam_label(s.frame.camera_change) + ); +} diff --git a/crates/grida-canvas/tests/compositor_effects.rs b/crates/grida-canvas/tests/compositor_effects.rs new file mode 100644 index 0000000000..afa4516c31 --- /dev/null +++ b/crates/grida-canvas/tests/compositor_effects.rs @@ -0,0 +1,1218 @@ +//! Pixel-comparison tests verifying that compositor-cached rendering of nodes +//! with effects (blur, shadows) matches live rendering. +//! +//! The compositor rasterizes promoted nodes into offscreen textures, then blits +//! them. This test ensures the rasterization path produces identical output to +//! the live (direct draw) path for nodes with effects. + +use cg::cache::geometry::GeometryCache; +use cg::cache::scene::SceneCache; +use cg::cg::prelude::*; +use cg::node::{ + factory::NodeFactory, + scene_graph::{Parent, SceneGraph}, + schema::*, +}; +use cg::painter::{Painter, PromotedBlit}; +use cg::resources::ByteStore; +use cg::runtime::font_repository::FontRepository; +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}; + +/// Create a raster surface of the given size. +fn make_surface(w: i32, h: i32) -> Surface { + surfaces::raster_n32_premul((w, h)).expect("Failed to create surface") +} + +/// Read RGBA pixels from a surface. +fn surface_to_rgba(surface: &mut Surface, w: i32, h: i32) -> Vec<[u8; 4]> { + let img = surface.image_snapshot(); + let info = img.image_info(); + let row_bytes = info.min_row_bytes(); + let mut raw = vec![0u8; row_bytes * h as usize]; + img.read_pixels( + &info, + &mut raw, + row_bytes, + skia_safe::IPoint::new(0, 0), + skia_safe::image::CachingHint::Allow, + ); + let pixel_count = (w * h) as usize; + let mut rgba = Vec::with_capacity(pixel_count); + for i in 0..pixel_count { + let off = i * 4; + rgba.push([raw[off + 2], raw[off + 1], raw[off], raw[off + 3]]); + } + rgba +} + +fn make_store() -> Arc> { + Arc::new(Mutex::new(ByteStore::new())) +} + +/// Draw a layer live into a surface (no compositor, direct painting). +fn draw_layer_live( + layer: &cg::painter::layer::PainterPictureLayer, + scene_cache: &SceneCache, + w: i32, + h: i32, +) -> Vec<[u8; 4]> { + let mut surface = make_surface(w, h); + let canvas = surface.canvas(); + canvas.clear(skia_safe::Color::TRANSPARENT); + + let store = make_store(); + let fonts = FontRepository::new(store.clone()); + let images = ImageRepository::new(store); + let policy = RenderPolicy::STANDARD; + + let painter = Painter::new_with_scene_cache(canvas, &fonts, &images, scene_cache, policy); + painter.draw_layer(layer); + + surface_to_rgba(&mut surface, w, h) +} + +/// Simulate compositor rasterization: draw a layer into an offscreen surface +/// using the same setup as update_compositor_inner(), then blit to a final surface. +fn draw_layer_composited( + layer: &cg::painter::layer::PainterPictureLayer, + render_bounds: &Rectangle, + scene_cache: &SceneCache, + w: i32, + h: i32, + zoom: f32, +) -> Vec<[u8; 4]> { + let pixel_width = (render_bounds.width * zoom).ceil() as i32; + let pixel_height = (render_bounds.height * zoom).ceil() as i32; + + // Step 1: Rasterize into offscreen (same as compositor path) + let mut offscreen = make_surface(pixel_width.max(1), pixel_height.max(1)); + { + let off_canvas = offscreen.canvas(); + off_canvas.clear(skia_safe::Color::TRANSPARENT); + // Same transform as update_compositor_inner: scale(zoom) * translate(-render_bounds.origin) + off_canvas.scale((zoom, zoom)); + off_canvas.translate((-render_bounds.x, -render_bounds.y)); + + let store = make_store(); + let fonts = FontRepository::new(store.clone()); + let images = ImageRepository::new(store); + let policy = RenderPolicy::STANDARD; + + let painter = + Painter::new_with_scene_cache(off_canvas, &fonts, &images, scene_cache, policy); + painter.draw_layer(layer); + } + + // Step 2: Blit the offscreen texture to the final surface (same as draw() blit path) + let mut final_surface = make_surface(w, h); + let final_canvas = final_surface.canvas(); + final_canvas.clear(skia_safe::Color::TRANSPARENT); + + let dst = Rect::from_xywh( + render_bounds.x, + render_bounds.y, + render_bounds.width, + render_bounds.height, + ); + let img = offscreen.image_snapshot(); + let src = Rect::new(0.0, 0.0, img.width() as f32, img.height() as f32); + let paint = SkPaint::default(); + final_canvas.draw_image_rect( + &img, + Some((&src, skia_safe::canvas::SrcRectConstraint::Fast)), + dst, + &paint, + ); + + surface_to_rgba(&mut final_surface, w, h) +} + +/// Check if any non-transparent pixel exists in the image. +fn has_visible_pixels(pixels: &[[u8; 4]]) -> bool { + pixels.iter().any(|p| p[3] > 0) +} + +/// Count non-transparent pixels. +fn count_visible_pixels(pixels: &[[u8; 4]]) -> usize { + pixels.iter().filter(|p| p[3] > 0).count() +} + +/// Count pixels with any channel difference > threshold. +fn count_differing_pixels(a: &[[u8; 4]], b: &[[u8; 4]], threshold: u8) -> usize { + a.iter() + .zip(b.iter()) + .filter(|(pa, pb)| { + (0..4).any(|c| (pa[c] as i16 - pb[c] as i16).unsigned_abs() > threshold as u16) + }) + .count() +} + +fn build_scene_cache(scene: &Scene) -> SceneCache { + let store = make_store(); + let fonts = FontRepository::new(store); + let mut scene_cache = SceneCache::new(); + scene_cache.geometry = GeometryCache::from_scene(scene, &fonts); + scene_cache.update_layers(scene); + scene_cache +} + +// ───────────────────────────────────────────────────────────────────────────── +// Tests +// ───────────────────────────────────────────────────────────────────────────── + +/// Test that a rectangle with a layer blur renders identically +/// via live drawing and compositor (offscreen) rasterization. +#[test] +fn compositor_preserves_layer_blur() { + let w = 200; + let h = 200; + let nf = NodeFactory::new(); + let mut graph = SceneGraph::new(); + + let mut rect = nf.create_rectangle_node(); + rect.size = Size { + width: 80.0, + height: 60.0, + }; + rect.transform = math2::transform::AffineTransform::new(60.0, 70.0, 0.0); + rect.effects = LayerEffects::new().blur(5.0f32); + rect.set_fill(Paint::Solid(SolidPaint { + color: CGColor::from_rgba(255, 0, 0, 255), + blend_mode: BlendMode::Normal, + active: true, + })); + + let rect_id = graph.append_child(Node::Rectangle(rect), Parent::Root); + + let scene = Scene { + name: "blur_test".into(), + graph, + background_color: None, + }; + + let scene_cache = build_scene_cache(&scene); + + let layer_entry = scene_cache + .layers + .layers + .iter() + .find(|e| e.id == rect_id) + .expect("Layer entry not found"); + + let render_bounds = scene_cache + .geometry + .get_render_bounds(&rect_id) + .expect("Render bounds not found"); + + // Live rendering + let live_pixels = draw_layer_live(&layer_entry.layer, &scene_cache, w, h); + + // Compositor rendering at zoom=1 + let comp_pixels = + draw_layer_composited(&layer_entry.layer, &render_bounds, &scene_cache, w, h, 1.0); + + // Both should have visible pixels + assert!( + has_visible_pixels(&live_pixels), + "Live rendering produced no visible pixels" + ); + assert!( + has_visible_pixels(&comp_pixels), + "Compositor rendering produced no visible pixels — blur effect was lost" + ); + + let live_visible = count_visible_pixels(&live_pixels); + let comp_visible = count_visible_pixels(&comp_pixels); + + // Compositor should produce at least 70% of visible pixels + assert!( + comp_visible >= live_visible * 70 / 100, + "Compositor lost too many visible pixels: live={live_visible}, comp={comp_visible}" + ); + + // Pixel comparison: allow minor differences + let diff_count = count_differing_pixels(&live_pixels, &comp_pixels, 10); + let total_pixels = (w * h) as usize; + let diff_pct = diff_count as f64 / total_pixels as f64 * 100.0; + assert!( + diff_pct < 5.0, + "Too many differing pixels: {diff_count} ({diff_pct:.1}%)" + ); +} + +/// Test that a rectangle with a drop shadow renders identically. +#[test] +fn compositor_preserves_drop_shadow() { + let w = 250; + let h = 250; + let nf = NodeFactory::new(); + let mut graph = SceneGraph::new(); + + let mut rect = nf.create_rectangle_node(); + rect.size = Size { + width: 80.0, + height: 60.0, + }; + rect.transform = math2::transform::AffineTransform::new(80.0, 90.0, 0.0); + rect.effects = LayerEffects::from_array(vec![FilterEffect::DropShadow(FeShadow { + dx: 5.0, + dy: 5.0, + blur: 8.0, + spread: 0.0, + color: CGColor::from_rgba(0, 0, 0, 200), + active: true, + })]); + rect.set_fill(Paint::Solid(SolidPaint { + color: CGColor::from_rgba(0, 128, 255, 255), + blend_mode: BlendMode::Normal, + active: true, + })); + + let rect_id = graph.append_child(Node::Rectangle(rect), Parent::Root); + + let scene = Scene { + name: "shadow_test".into(), + graph, + background_color: None, + }; + + let scene_cache = build_scene_cache(&scene); + + let layer_entry = scene_cache + .layers + .layers + .iter() + .find(|e| e.id == rect_id) + .expect("Layer entry not found"); + + let render_bounds = scene_cache + .geometry + .get_render_bounds(&rect_id) + .expect("Render bounds not found"); + + let live_pixels = draw_layer_live(&layer_entry.layer, &scene_cache, w, h); + let comp_pixels = + draw_layer_composited(&layer_entry.layer, &render_bounds, &scene_cache, w, h, 1.0); + + assert!( + has_visible_pixels(&live_pixels), + "Live rendering produced no visible pixels" + ); + assert!( + has_visible_pixels(&comp_pixels), + "Compositor rendering produced no visible pixels — drop shadow was lost" + ); + + let live_visible = count_visible_pixels(&live_pixels); + let comp_visible = count_visible_pixels(&comp_pixels); + assert!( + comp_visible >= live_visible * 70 / 100, + "Compositor lost too many visible pixels: live={live_visible}, comp={comp_visible}" + ); + + let diff_count = count_differing_pixels(&live_pixels, &comp_pixels, 10); + let total_pixels = (w * h) as usize; + let diff_pct = diff_count as f64 / total_pixels as f64 * 100.0; + assert!( + diff_pct < 5.0, + "Too many differing pixels: {diff_count} ({diff_pct:.1}%)" + ); +} + +/// Test that a rectangle with effects INSIDE a container renders correctly +/// when compositor-cached. This is the specific scenario from the bug report. +#[test] +fn compositor_preserves_effects_inside_container() { + let w = 300; + let h = 300; + let nf = NodeFactory::new(); + let mut graph = SceneGraph::new(); + + // Create a container + let container = nf.create_container_node(); + let container_id = graph.append_child(Node::Container(container), Parent::Root); + + // Create a rectangle child with blur inside the container + let mut rect = nf.create_rectangle_node(); + rect.size = Size { + width: 100.0, + height: 80.0, + }; + rect.transform = math2::transform::AffineTransform::new(50.0, 60.0, 0.0); + rect.effects = LayerEffects::new().blur(8.0f32); + rect.set_fill(Paint::Solid(SolidPaint { + color: CGColor::from_rgba(255, 50, 50, 255), + blend_mode: BlendMode::Normal, + active: true, + })); + + let rect_id = graph.append_child(Node::Rectangle(rect), Parent::NodeId(container_id)); + + let scene = Scene { + name: "container_blur_test".into(), + graph, + background_color: None, + }; + + let scene_cache = build_scene_cache(&scene); + + // Find the child rectangle's layer entry + let child_layer_entry = scene_cache + .layers + .layers + .iter() + .find(|e| e.id == rect_id) + .expect("Child layer entry not found"); + + let render_bounds = scene_cache + .geometry + .get_render_bounds(&rect_id) + .expect("Render bounds not found"); + + // Verify render_bounds includes blur expansion + let world_bounds = scene_cache + .geometry + .get_world_bounds(&rect_id) + .expect("World bounds not found"); + assert!( + render_bounds.width > world_bounds.width, + "render_bounds should be wider than world_bounds (blur expansion)" + ); + assert!( + render_bounds.height > world_bounds.height, + "render_bounds should be taller than world_bounds (blur expansion)" + ); + + let live_pixels = draw_layer_live(&child_layer_entry.layer, &scene_cache, w, h); + let comp_pixels = draw_layer_composited( + &child_layer_entry.layer, + &render_bounds, + &scene_cache, + w, + h, + 1.0, + ); + + assert!( + has_visible_pixels(&live_pixels), + "Live rendering produced no visible pixels" + ); + assert!( + has_visible_pixels(&comp_pixels), + "Compositor rendering produced no visible pixels — effects were lost inside container" + ); + + let live_visible = count_visible_pixels(&live_pixels); + let comp_visible = count_visible_pixels(&comp_pixels); + assert!( + comp_visible >= live_visible * 70 / 100, + "Compositor lost too many visible pixels inside container: live={live_visible}, comp={comp_visible}" + ); +} + +/// Test that compositor rasterization at zoom=2 still preserves effects. +#[test] +fn compositor_preserves_blur_at_zoom_2() { + let w = 300; + let h = 300; + let nf = NodeFactory::new(); + let mut graph = SceneGraph::new(); + + let mut rect = nf.create_rectangle_node(); + rect.size = Size { + width: 60.0, + height: 40.0, + }; + rect.transform = math2::transform::AffineTransform::new(40.0, 50.0, 0.0); + rect.effects = LayerEffects::new().blur(6.0f32); + rect.set_fill(Paint::Solid(SolidPaint { + color: CGColor::from_rgba(0, 200, 100, 255), + blend_mode: BlendMode::Normal, + active: true, + })); + + let rect_id = graph.append_child(Node::Rectangle(rect), Parent::Root); + + let scene = Scene { + name: "zoom_blur_test".into(), + graph, + background_color: None, + }; + + let scene_cache = build_scene_cache(&scene); + + let layer_entry = scene_cache + .layers + .layers + .iter() + .find(|e| e.id == rect_id) + .expect("Layer entry not found"); + + let render_bounds = scene_cache + .geometry + .get_render_bounds(&rect_id) + .expect("Render bounds not found"); + + let comp_pixels = + draw_layer_composited(&layer_entry.layer, &render_bounds, &scene_cache, w, h, 2.0); + + assert!( + has_visible_pixels(&comp_pixels), + "Compositor at zoom=2 produced no visible pixels" + ); +} + +/// Verify that a child's render_bounds includes its blur expansion, +/// while the container's render_bounds only reflects its own bounds. +#[test] +fn container_render_bounds_vs_child_render_bounds() { + let nf = NodeFactory::new(); + let mut graph = SceneGraph::new(); + + let mut container = nf.create_container_node(); + container.layout_dimensions.layout_target_width = Some(200.0); + container.layout_dimensions.layout_target_height = Some(150.0); + let container_id = graph.append_child(Node::Container(container), Parent::Root); + + let mut rect = nf.create_rectangle_node(); + rect.size = Size { + width: 50.0, + height: 40.0, + }; + rect.transform = math2::transform::AffineTransform::new(10.0, 10.0, 0.0); + rect.effects = LayerEffects::new().blur(10.0f32); + + let rect_id = graph.append_child(Node::Rectangle(rect), Parent::NodeId(container_id)); + + let scene = Scene { + name: "bounds_test".into(), + graph, + background_color: None, + }; + + let store = make_store(); + let fonts = FontRepository::new(store); + let cache = GeometryCache::from_scene(&scene, &fonts); + + let container_rb = cache.get_render_bounds(&container_id).unwrap(); + let child_rb = cache.get_render_bounds(&rect_id).unwrap(); + + // Child render_bounds should include blur expansion (10 * 3 = 30 on each side) + assert!( + child_rb.width > 50.0, + "Child render_bounds should include blur expansion" + ); + + let expected_expansion = 10.0 * 3.0; + let expected_child_width = 50.0 + 2.0 * expected_expansion; + assert!( + (child_rb.width - expected_child_width).abs() < 1.0, + "Child render_bounds width should be ~{expected_child_width}, got {}", + child_rb.width + ); + + // Container's render_bounds based on its own bounds (current behavior) + assert_eq!( + container_rb.width, 200.0, + "Container render_bounds should match its own bounds" + ); +} + +/// Regression test: Z-order interleaving of promoted blits and live draws. +/// +/// When a Container (not promoted, drawn live) has a child with blur effects +/// (promoted, compositor-cached), the child must be visible ABOVE the +/// container's background fill. Previously, all promoted nodes were blitted +/// in a batch BEFORE live nodes were drawn, causing the Container's +/// background to cover its promoted children. +/// +/// This test simulates the full draw path: builds a LayerList with both a +/// container (live) and its child (promoted via blur), then draws using +/// `draw_layer_list` with `promoted_blits` to verify the child is visible. +#[test] +fn z_order_promoted_child_visible_above_container() { + let w = 300; + let h = 300; + let nf = NodeFactory::new(); + let mut graph = SceneGraph::new(); + + // Container with solid fill (white) — drawn live, NOT promoted + let mut container = nf.create_container_node(); + container.layout_dimensions.layout_target_width = Some(200.0); + container.layout_dimensions.layout_target_height = Some(150.0); + // Ensure the container has a solid white fill + container.fills = Paints::new(vec![Paint::Solid(SolidPaint { + color: CGColor::from_rgba(255, 255, 255, 255), + blend_mode: BlendMode::Normal, + active: true, + })]); + let container_id = graph.append_child(Node::Container(container), Parent::Root); + + // Child rectangle with blur effect (red) — will be "promoted" + let mut rect = nf.create_rectangle_node(); + rect.size = Size { + width: 100.0, + height: 80.0, + }; + rect.transform = math2::transform::AffineTransform::new(50.0, 35.0, 0.0); + rect.effects = LayerEffects::new().blur(5.0f32); + rect.set_fill(Paint::Solid(SolidPaint { + color: CGColor::from_rgba(255, 0, 0, 255), + blend_mode: BlendMode::Normal, + active: true, + })); + + let rect_id = graph.append_child(Node::Rectangle(rect), Parent::NodeId(container_id)); + + let scene = Scene { + name: "z_order_test".into(), + graph, + background_color: None, + }; + + let scene_cache = build_scene_cache(&scene); + + let render_bounds = scene_cache + .geometry + .get_render_bounds(&rect_id) + .expect("Render bounds not found"); + + // Step 1: Draw the child into an offscreen surface (simulating compositor) + let zoom = 1.0f32; + let pixel_width = (render_bounds.width * zoom).ceil() as i32; + let pixel_height = (render_bounds.height * zoom).ceil() as i32; + + let mut offscreen = make_surface(pixel_width.max(1), pixel_height.max(1)); + { + let child_layer_entry = scene_cache + .layers + .layers + .iter() + .find(|e| e.id == rect_id) + .expect("Child layer entry not found"); + + let off_canvas = offscreen.canvas(); + off_canvas.clear(skia_safe::Color::TRANSPARENT); + off_canvas.scale((zoom, zoom)); + off_canvas.translate((-render_bounds.x, -render_bounds.y)); + + let store = make_store(); + let fonts = FontRepository::new(store.clone()); + let images = ImageRepository::new(store); + let policy = RenderPolicy::STANDARD; + + let painter = + Painter::new_with_scene_cache(off_canvas, &fonts, &images, &scene_cache, policy); + painter.draw_layer(&child_layer_entry.layer); + } + + let offscreen_image = offscreen.image_snapshot(); + + // Step 2: Build the promoted_blits map + let mut promoted_blits: HashMap = HashMap::new(); + let src_rect = Rect::new( + 0.0, + 0.0, + offscreen_image.width() as f32, + offscreen_image.height() as f32, + ); + let dst_rect = Rect::from_xywh( + render_bounds.x, + render_bounds.y, + render_bounds.width, + render_bounds.height, + ); + promoted_blits.insert( + rect_id, + PromotedBlit { + image: Rc::new(offscreen_image), + src_rect, + dst_rect, + opacity: 1.0, + blend_mode: skia_safe::BlendMode::SrcOver, + }, + ); + + // Step 3: Draw the full layer list with promoted_blits + let mut surface = make_surface(w, h); + let canvas = surface.canvas(); + canvas.clear(skia_safe::Color::WHITE); + + let store = make_store(); + let fonts = FontRepository::new(store.clone()); + let images = ImageRepository::new(store); + let policy = RenderPolicy::STANDARD; + + let painter = Painter::new_with_scene_cache(canvas, &fonts, &images, &scene_cache, policy) + .with_promoted_blits(&promoted_blits); + painter.draw_layer_list(&scene_cache.layers); + + // Step 4: Verify the child rectangle is visible (red pixels exist) + let pixels = surface_to_rgba(&mut surface, w, h); + + // Check for non-white pixels in the area where the child should be (50..150, 35..115). + // The child has a colored fill (red in RGBA), which should be visually distinct + // from the container's white background. We check for pixels that are NOT white. + // + // Note: We avoid checking specific channel values due to platform-dependent + // channel ordering (BGRA vs RGBA in Skia's kN32 format). + let mut child_pixel_count = 0; + for y in 45..105 { + for x in 60..140 { + let idx = y * w as usize + x; + let p = pixels[idx]; + // Check for non-white, opaque pixels — any channel < 200 means not white + let is_white = p[0] > 240 && p[1] > 240 && p[2] > 240; + if !is_white && p[3] > 200 { + child_pixel_count += 1; + } + } + } + + // The inner area is 80x60 = 4800 pixels. With blur the center should + // still have clearly colored pixels. We expect at least 25% of the + // interior to be clearly non-white. + assert!( + child_pixel_count > 1000, + "Promoted child NOT visible above container — z-order bug! \ + Non-white pixels in child inner area: {child_pixel_count} (expected > 1000). \ + Container background is covering the promoted child." + ); + + // Also verify that white pixels exist in the container area outside the child + // (proving the container background was drawn) + let mut white_pixel_count = 0; + for y in 0..35 { + for x in 0..200 { + let idx = y * w as usize + x; + let p = pixels[idx]; + if p[0] > 240 && p[1] > 240 && p[2] > 240 && p[3] > 200 { + white_pixel_count += 1; + } + } + } + + assert!( + white_pixel_count > 1000, + "Container background not drawn: white pixels = {white_pixel_count}" + ); +} + +/// Pixel-level correctness test for the opacity folding optimization. +/// +/// Verifies that rendering a semi-transparent rectangle (opacity=0.5) with +/// `Blend(Normal)` and no effects (the "folded" save_layer path) produces +/// pixel values that match the expected alpha-blending math. +/// +/// On a white background, a red rectangle at 50% opacity should produce: +/// R = 255*0.5 + 255*0.5 = 255 +/// G = 0*0.5 + 255*0.5 = 127–128 +/// B = 0*0.5 + 255*0.5 = 127–128 +/// +/// Additionally, we render a second scene with `opacity=1.0` and +/// `PassThrough` using a 50% alpha fill color to produce equivalent output +/// WITHOUT the folding path, and verify both produce matching pixels. +#[test] +fn opacity_folding_pixel_accuracy() { + let w = 100i32; + let h = 100i32; + + // Helper: build a scene with a single rectangle and render via Painter. + let render_rect = |effects: LayerEffects| -> Vec<[u8; 4]> { + let nf = NodeFactory::new(); + let mut graph = SceneGraph::new(); + + let mut rect = nf.create_rectangle_node(); + rect.size = Size { + width: 60.0, + height: 60.0, + }; + rect.transform = math2::transform::AffineTransform::new(20.0, 20.0, 0.0); + rect.opacity = 0.5; + rect.blend_mode = LayerBlendMode::Blend(BlendMode::Normal); + rect.effects = effects; + rect.set_fill(Paint::Solid(SolidPaint { + color: CGColor::from_rgba(255, 0, 0, 255), + blend_mode: BlendMode::Normal, + active: true, + })); + + graph.append_child(Node::Rectangle(rect), Parent::Root); + + let scene = Scene { + name: "test".into(), + graph, + background_color: None, + }; + + let scene_cache = build_scene_cache(&scene); + + let mut surface = make_surface(w, h); + let canvas = surface.canvas(); + canvas.clear(skia_safe::Color::WHITE); + + let store = make_store(); + let fonts = FontRepository::new(store.clone()); + let images = ImageRepository::new(store); + let policy = RenderPolicy::STANDARD; + + let painter = + Painter::new_with_scene_cache(canvas, &fonts, &images, &scene_cache, policy); + painter.draw_layer_list(&scene_cache.layers); + + surface_to_rgba(&mut surface, w, h) + }; + + // ── Scene A: folded path (opacity=0.5, Blend(Normal), NO effects) ── + // This exercises with_blendmode_and_opacity — 1 save_layer + let pixels_a = render_rect(LayerEffects::default()); + + // ── Scene B: unfolded path (opacity=0.5, Blend(Normal), WITH invisible shadow) ── + // The active shadow forces needs_opacity_isolation() → true, + // so the unfolded 2-save_layer path runs. The shadow itself is + // fully transparent (alpha=0) and zero-size, so it doesn't affect pixels. + let unfolded_effects = LayerEffects { + shadows: vec![FilterShadowEffect::DropShadow(FeShadow { + dx: 0.0, + dy: 0.0, + blur: 0.0, + spread: 0.0, + color: CGColor::from_rgba(0, 0, 0, 0), // fully transparent + active: true, // but active → forces unfolded path + })], + ..LayerEffects::default() + }; + let pixels_b = render_rect(unfolded_effects); + + // ── Verification 1: Check pixel math for Scene A ── + // Sample a pixel well inside the rectangle (center: 50, 50) + let center = pixels_a[50 * w as usize + 50]; + // On a white bg, red@50% opacity should produce a tinted pixel. + // Exact channel order depends on platform (kN32 may be BGRA or RGBA), + // and surface_to_rgba applies a fixed swizzle. Rather than assuming + // channel identity, verify: + // - At least one channel is high (~255) — the red or white contribution + // - At least one channel is in the mid range (~127) — the blended channel + // - Alpha is fully opaque + let channels = [center[0], center[1], center[2]]; + let has_high = channels.iter().any(|&c| c > 200); + let has_mid = channels.iter().any(|&c| c > 100 && c < 160); + assert!( + has_high, + "Expected at least one high channel (>200) in center pixel: {:?}", + center + ); + assert!( + has_mid, + "Expected at least one mid-range channel (100-160) in center pixel: {:?}", + center + ); + assert!( + center[3] > 250, + "Center alpha should be fully opaque after compositing: {}", + center[3] + ); + + // Verify the center is NOT white (the rect is visible) + let is_white = channels.iter().all(|&c| c > 240); + assert!( + !is_white, + "Center pixel should not be white — rect must be visible: {:?}", + center + ); + + // ── Verification 2: Compare Scene A vs Scene B pixel-by-pixel ── + // Allow small rounding difference (opacity 0.5 vs alpha 128/255 ≈ 0.502) + let tolerance = 3u8; + let mut max_diff = 0u8; + let mut diff_count = 0usize; + + for (i, (a, b)) in pixels_a.iter().zip(pixels_b.iter()).enumerate() { + for ch in 0..4 { + let d = (a[ch] as i16 - b[ch] as i16).unsigned_abs() as u8; + if d > max_diff { + max_diff = d; + } + if d > tolerance { + diff_count += 1; + if diff_count <= 5 { + let x = i % w as usize; + let y = i / w as usize; + eprintln!( + "Pixel diff at ({x},{y}) ch{ch}: folded={} reference={} diff={d}", + a[ch], b[ch] + ); + } + } + } + } + + assert_eq!( + diff_count, 0, + "Opacity folding produced different pixels vs reference! \ + {diff_count} channel values differ by more than {tolerance}. \ + Max diff = {max_diff}." + ); + + // ── Verification 3: Background pixels outside rect are pure white ── + let corner = pixels_a[5 * w as usize + 5]; // (5, 5) — well outside rect + assert!( + corner[0] > 250 && corner[1] > 250 && corner[2] > 250, + "Background should be white: {:?}", + corner + ); +} + +/// Pixel-level correctness test for Normal blend mode save_layer elimination. +/// +/// Verifies that `Blend(Normal)` on a leaf node (no save_layer, fast path) +/// produces identical pixels to `PassThrough` (also no save_layer). +/// Both should be equivalent since Normal = SrcOver and SrcOver is +/// associative for leaf nodes with no children. +/// +/// Tests multiple configurations: +/// 1. Opaque rect (opacity=1.0) — both paths skip save_layer entirely +/// 2. Semi-transparent rect (opacity=0.5) — exercises save_layer_alpha path +/// 3. Multiple fills — verifies fill stacking is identical +#[test] +fn normal_blend_elimination_pixel_accuracy() { + let w = 100i32; + let h = 100i32; + + // Helper: render a rect with specified blend mode and opacity + let render = |blend: LayerBlendMode, opacity: f32, fills: Vec| -> Vec<[u8; 4]> { + let nf = NodeFactory::new(); + let mut graph = SceneGraph::new(); + + let mut rect = nf.create_rectangle_node(); + rect.size = Size { + width: 60.0, + height: 60.0, + }; + rect.transform = math2::transform::AffineTransform::new(20.0, 20.0, 0.0); + rect.opacity = opacity; + rect.blend_mode = blend; + rect.effects = LayerEffects::default(); + rect.fills = Paints::new(fills); + + graph.append_child(Node::Rectangle(rect), Parent::Root); + + let scene = Scene { + name: "test".into(), + graph, + background_color: None, + }; + + let scene_cache = build_scene_cache(&scene); + let mut surface = make_surface(w, h); + surface.canvas().clear(skia_safe::Color::WHITE); + + let store = make_store(); + let fonts = FontRepository::new(store.clone()); + let images = ImageRepository::new(store); + let policy = RenderPolicy::STANDARD; + + let painter = + Painter::new_with_scene_cache(surface.canvas(), &fonts, &images, &scene_cache, policy); + painter.draw_layer_list(&scene_cache.layers); + + surface_to_rgba(&mut surface, w, h) + }; + + let compare = |label: &str, a: &[[u8; 4]], b: &[[u8; 4]]| { + let mut max_diff = 0u8; + let mut diff_count = 0usize; + for (i, (pa, pb)) in a.iter().zip(b.iter()).enumerate() { + for ch in 0..4 { + let d = (pa[ch] as i16 - pb[ch] as i16).unsigned_abs() as u8; + if d > max_diff { + max_diff = d; + } + if d > 0 { + diff_count += 1; + if diff_count <= 3 { + let x = i % w as usize; + let y = i / w as usize; + eprintln!( + "[{label}] diff at ({x},{y}) ch{ch}: normal={} passthrough={} diff={d}", + pa[ch], pb[ch] + ); + } + } + } + } + assert_eq!( + diff_count, 0, + "[{label}] Normal blend produced different pixels vs PassThrough! \ + {diff_count} values differ. Max diff = {max_diff}." + ); + }; + + let red_fill = Paint::Solid(SolidPaint { + color: CGColor::from_rgba(255, 0, 0, 255), + blend_mode: BlendMode::Normal, + active: true, + }); + + // Test 1: Opaque single fill + let normal_opaque = render( + LayerBlendMode::Blend(BlendMode::Normal), + 1.0, + vec![red_fill.clone()], + ); + let passthrough_opaque = render(LayerBlendMode::default(), 1.0, vec![red_fill.clone()]); + compare("opaque_single_fill", &normal_opaque, &passthrough_opaque); + + // Test 2: Semi-transparent single fill + let normal_semi = render( + LayerBlendMode::Blend(BlendMode::Normal), + 0.5, + vec![red_fill.clone()], + ); + let passthrough_semi = render(LayerBlendMode::default(), 0.5, vec![red_fill.clone()]); + compare("semitransparent_single_fill", &normal_semi, &passthrough_semi); + + // Test 3: Multiple overlapping fills (red + semi-transparent blue) + let blue_semi = Paint::Solid(SolidPaint { + color: CGColor::from_rgba(0, 0, 255, 128), + blend_mode: BlendMode::Normal, + active: true, + }); + let multi_fills = vec![red_fill.clone(), blue_semi]; + let normal_multi = render( + LayerBlendMode::Blend(BlendMode::Normal), + 1.0, + multi_fills.clone(), + ); + let passthrough_multi = render(LayerBlendMode::default(), 1.0, multi_fills); + compare("opaque_multi_fill", &normal_multi, &passthrough_multi); + + // Test 4: Semi-transparent with multiple fills (paint-alpha fast path) + // This exercises draw_fills_with_opacity (the new zero-save_layer path) + let multi_fills_2 = vec![red_fill.clone()]; + let paint_alpha_path = render(LayerBlendMode::default(), 0.5, multi_fills_2.clone()); + let normal_alpha_path = render( + LayerBlendMode::Blend(BlendMode::Normal), + 0.5, + multi_fills_2, + ); + compare( + "semitransparent_paint_alpha", + &paint_alpha_path, + &normal_alpha_path, + ); +} + +/// Pixel-level accuracy test for the paint-alpha folding optimization with +/// **both fills and strokes** at sub-unit opacity. +/// +/// The paint-alpha fast path applies opacity independently to the fill paint +/// and the stroke paint. For Inside strokes, the stroke geometry overlaps +/// the fill area, so applying opacity independently yields a slightly +/// different composite result compared to `save_layer_alpha` (which groups +/// fill + stroke into one surface and then alpha-blends the whole surface). +/// +/// Specifically, in the overlap region: +/// - **save_layer_alpha**: fill and stroke compose opaquely on the offscreen +/// surface, then the entire surface is blitted at `opacity`. The overlap +/// region shows only the topmost paint (stroke) at `opacity`. +/// - **paint-alpha folding**: fill is drawn at `opacity`, then stroke is drawn +/// at `opacity` on top. The overlap region blends both, which can show +/// a slight "fill bleed-through" artifact. +/// +/// For Outside strokes, there is NO geometric overlap between fill and stroke, +/// so the paint-alpha path should match save_layer_alpha exactly. +/// +/// This test: +/// 1. Outside strokes: verifies 0 pixel difference (exact match expected). +/// 2. Inside strokes: measures the overlap artifact and asserts it is minimal. +/// 3. Center strokes: measures the partial overlap artifact. +#[test] +fn fill_stroke_opacity_pixel_accuracy() { + let w = 120i32; + let h = 120i32; + + // Helper: render a rect with fill + stroke at given opacity and stroke_align. + // `force_unfolded` = true adds an invisible active shadow to force the + // save_layer_alpha reference path (2+ save_layers, no paint-alpha folding). + let render = |stroke_align: StrokeAlign, opacity: f32, force_unfolded: bool| -> Vec<[u8; 4]> { + let nf = NodeFactory::new(); + let mut graph = SceneGraph::new(); + + let mut rect = nf.create_rectangle_node(); + rect.size = Size { + width: 60.0, + height: 60.0, + }; + rect.transform = math2::transform::AffineTransform::new(30.0, 30.0, 0.0); + rect.opacity = opacity; + rect.blend_mode = LayerBlendMode::Blend(BlendMode::Normal); + rect.set_fill(Paint::Solid(SolidPaint { + color: CGColor::from_rgba(255, 0, 0, 255), // red fill + blend_mode: BlendMode::Normal, + active: true, + })); + rect.strokes = Paints::new(vec![Paint::Solid(SolidPaint { + color: CGColor::from_rgba(0, 0, 255, 255), // blue stroke + blend_mode: BlendMode::Normal, + active: true, + })]); + rect.stroke_width = StrokeWidth::Uniform(8.0); + rect.stroke_style = StrokeStyle { + stroke_align, + stroke_cap: StrokeCap::default(), + stroke_join: StrokeJoin::default(), + stroke_miter_limit: StrokeMiterLimit::default(), + stroke_dash_array: None, + }; + + if force_unfolded { + // Invisible active shadow: forces needs_opacity_isolation() → true, + // which prevents can_fold_into_paint, falling back to save_layer_alpha. + rect.effects = LayerEffects { + shadows: vec![FilterShadowEffect::DropShadow(FeShadow { + dx: 0.0, + dy: 0.0, + blur: 0.0, + spread: 0.0, + color: CGColor::from_rgba(0, 0, 0, 0), // fully transparent + active: true, + })], + ..LayerEffects::default() + }; + } else { + rect.effects = LayerEffects::default(); + } + + graph.append_child(Node::Rectangle(rect), Parent::Root); + + let scene = Scene { + name: "fill_stroke_test".into(), + graph, + background_color: None, + }; + + let scene_cache = build_scene_cache(&scene); + let mut surface = make_surface(w, h); + surface.canvas().clear(skia_safe::Color::WHITE); + + let store = make_store(); + let fonts = FontRepository::new(store.clone()); + let images = ImageRepository::new(store); + let policy = RenderPolicy::STANDARD; + + let painter = + Painter::new_with_scene_cache(surface.canvas(), &fonts, &images, &scene_cache, policy); + painter.draw_layer_list(&scene_cache.layers); + + surface_to_rgba(&mut surface, w, h) + }; + + let compare_with_report = + |label: &str, folded: &[[u8; 4]], reference: &[[u8; 4]], max_allowed_diff_pixels: usize| { + let mut max_diff = 0u8; + let mut diff_count = 0usize; + let mut diff_pixel_count = 0usize; + + for (i, (a, b)) in folded.iter().zip(reference.iter()).enumerate() { + let mut pixel_differs = false; + for ch in 0..4 { + let d = (a[ch] as i16 - b[ch] as i16).unsigned_abs() as u8; + if d > max_diff { + max_diff = d; + } + if d > 0 { + diff_count += 1; + pixel_differs = true; + } + } + if pixel_differs { + diff_pixel_count += 1; + if diff_pixel_count <= 3 { + let x = i % w as usize; + let y = i / w as usize; + eprintln!( + "[{label}] diff pixel at ({x},{y}): folded={:?} reference={:?}", + a, b + ); + } + } + } + + eprintln!( + "[{label}] Summary: {diff_pixel_count} differing pixels, \ + {diff_count} channel diffs, max_diff={max_diff}" + ); + + assert!( + diff_pixel_count <= max_allowed_diff_pixels, + "[{label}] Too many differing pixels: {diff_pixel_count} \ + (max allowed: {max_allowed_diff_pixels}). Max channel diff = {max_diff}." + ); + + // Even for allowed diffs, the max channel difference should be bounded. + // For Inside/Center strokes the overlap region can have up to ~64 diff + // due to independent alpha compositing vs grouped alpha compositing. + if diff_pixel_count > 0 { + assert!( + max_diff <= 80, + "[{label}] Channel difference too large: max_diff={max_diff} (allowed ≤ 80). \ + This suggests a rendering bug, not just an overlap artifact." + ); + } + }; + + // ── Test 1: Outside strokes — NO overlap, should be exact match ── + let outside_folded = render(StrokeAlign::Outside, 0.5, false); + let outside_reference = render(StrokeAlign::Outside, 0.5, true); + compare_with_report( + "outside_stroke_opacity", + &outside_folded, + &outside_reference, + 0, // exact match expected + ); + + // ── Test 2: Inside strokes — exact match via save_layer_alpha fallback ── + let inside_folded = render(StrokeAlign::Inside, 0.5, false); + let inside_reference = render(StrokeAlign::Inside, 0.5, true); + // Inside strokes fully overlap the fill. The painter detects this via + // stroke_overlaps_fill and falls back to save_layer_alpha (1 GPU surface) + // instead of paint-alpha folding, ensuring pixel-perfect output. + compare_with_report( + "inside_stroke_opacity", + &inside_folded, + &inside_reference, + 0, // exact match: save_layer_alpha handles inside strokes correctly + ); + + // ── Test 3: Center strokes — exact match via save_layer_alpha fallback ── + let center_folded = render(StrokeAlign::Center, 0.5, false); + let center_reference = render(StrokeAlign::Center, 0.5, true); + // Center strokes partially overlap the fill (inner half). The painter + // detects this via stroke_overlaps_fill and falls back to save_layer_alpha. + // The save_layer bounds are expanded to include the stroke path geometry + // (via compute_blend_mode_bounds_with_stroke), so the outer half of + // Center strokes is not clipped. Per SVG/CSS spec and Chromium behavior. + compare_with_report( + "center_stroke_opacity", + ¢er_folded, + ¢er_reference, + 0, // exact match: save_layer_alpha with expanded bounds handles center strokes + ); + + // ── Test 4: Verify visible pixels exist (sanity) ── + assert!( + has_visible_pixels(&outside_folded), + "Outside stroke folded path has no visible pixels" + ); + assert!( + has_visible_pixels(&inside_folded), + "Inside stroke folded path has no visible pixels" + ); +} diff --git a/crates/grida-canvas/tests/translate_fold_accuracy.rs b/crates/grida-canvas/tests/translate_fold_accuracy.rs new file mode 100644 index 0000000000..a61bfd0781 --- /dev/null +++ b/crates/grida-canvas/tests/translate_fold_accuracy.rs @@ -0,0 +1,370 @@ +//! Pixel-accuracy tests for the translate-fold optimization. +//! +//! Verifies that pre-applying translation to shape coordinates (the fast path) +//! produces identical pixels to the canonical save/concat(matrix)/restore path. +//! +//! The optimization folds pure-translation transforms directly into shape +//! coordinates, eliminating canvas.save(), canvas.concat(), and canvas.restore(). +//! These tests verify pixel-identical output at the Skia canvas level. + +use skia_safe::{surfaces, Paint, Path, Point, Rect, RRect, Surface}; + +const W: i32 = 400; +const H: i32 = 400; + +fn make_surface() -> Surface { + surfaces::raster_n32_premul((W, H)).expect("surface") +} + +/// Read RGBA pixels from a surface. +fn surface_to_rgba(surface: &mut Surface) -> Vec<[u8; 4]> { + let img = surface.image_snapshot(); + let info = img.image_info(); + let row_bytes = info.min_row_bytes(); + let mut raw = vec![0u8; row_bytes * H as usize]; + img.read_pixels( + &info, + &mut raw, + row_bytes, + skia_safe::IPoint::new(0, 0), + skia_safe::image::CachingHint::Allow, + ); + let pixel_count = (W * H) as usize; + let mut rgba = Vec::with_capacity(pixel_count); + for i in 0..pixel_count { + let off = i * 4; + // Skia N32 premul is BGRA on little-endian → RGBA + rgba.push([raw[off + 2], raw[off + 1], raw[off], raw[off + 3]]); + } + rgba +} + +/// Assert pixel-identical output (0 diff tolerance). +fn assert_pixels_identical(name: &str, a: &[[u8; 4]], b: &[[u8; 4]]) { + assert_eq!(a.len(), b.len(), "{name}: pixel count mismatch"); + let mut diff_count = 0; + let mut max_diff = 0u8; + for (pa, pb) in a.iter().zip(b.iter()) { + for c in 0..4 { + let d = (pa[c] as i16 - pb[c] as i16).unsigned_abs() as u8; + if d > 0 { + diff_count += 1; + max_diff = max_diff.max(d); + } + } + } + assert_eq!( + diff_count, 0, + "{name}: {diff_count} channel diffs found (max diff: {max_diff})" + ); +} + +// ── Rect tests ────────────────────────────────────────────────────────── + +/// Test: draw_rect via save/concat/restore vs draw_rect with pre-translated coords. +#[test] +fn translate_fold_rect_pixel_identical() { + let local_rect = Rect::from_xywh(0.0, 0.0, 50.0, 35.0); + let offsets: &[(f32, f32)] = &[ + (10.0, 20.0), + (100.5, 50.5), // sub-pixel + (0.0, 0.0), // identity + (200.0, 300.0), // large offset + (0.3, 0.7), // fractional + (123.456, 78.9), // non-round + ]; + + for &(tx, ty) in offsets { + let mut paint = Paint::default(); + paint.set_anti_alias(true); + paint.set_color(skia_safe::Color::from_argb(200, 255, 0, 0)); + + // Path A: canonical save/concat/restore + let mut sa = make_surface(); + { + let canvas = sa.canvas(); + canvas.clear(skia_safe::Color::WHITE); + canvas.save(); + let m = skia_safe::Matrix::translate((tx, ty)); + canvas.concat(&m); + canvas.draw_rect(local_rect, &paint); + canvas.restore(); + } + let pixels_a = surface_to_rgba(&mut sa); + + // Path B: pre-translated rect (the optimization) + let mut sb = make_surface(); + { + let canvas = sb.canvas(); + canvas.clear(skia_safe::Color::WHITE); + canvas.draw_rect(local_rect.with_offset((tx, ty)), &paint); + } + let pixels_b = surface_to_rgba(&mut sb); + + assert_pixels_identical(&format!("rect({tx},{ty})"), &pixels_a, &pixels_b); + } +} + +// ── RRect tests ────────────────────────────────────────────────────────── + +/// Test: draw_rrect via save/concat/restore vs draw_rrect with with_offset. +#[test] +fn translate_fold_rrect_pixel_identical() { + let local_rrect = RRect::new_rect_xy(Rect::from_xywh(0.0, 0.0, 60.0, 45.0), 8.0, 8.0); + let offsets: &[(f32, f32)] = &[ + (15.0, 25.0), + (80.5, 40.5), + (0.0, 0.0), + (150.0, 200.0), + (0.25, 0.75), + ]; + + for &(tx, ty) in offsets { + let mut paint = Paint::default(); + paint.set_anti_alias(true); + paint.set_color(skia_safe::Color::from_argb(180, 0, 128, 255)); + + // Path A: save/concat/restore + let mut sa = make_surface(); + { + let canvas = sa.canvas(); + canvas.clear(skia_safe::Color::WHITE); + canvas.save(); + canvas.concat(&skia_safe::Matrix::translate((tx, ty))); + canvas.draw_rrect(local_rrect, &paint); + canvas.restore(); + } + let pixels_a = surface_to_rgba(&mut sa); + + // Path B: with_offset (the optimization) + let mut sb = make_surface(); + { + let canvas = sb.canvas(); + canvas.clear(skia_safe::Color::WHITE); + canvas.draw_rrect(local_rrect.with_offset((tx, ty)), &paint); + } + let pixels_b = surface_to_rgba(&mut sb); + + assert_pixels_identical(&format!("rrect({tx},{ty})"), &pixels_a, &pixels_b); + } +} + +// ── Oval tests ────────────────────────────────────────────────────────── + +/// Test: draw_oval via save/concat/restore vs draw_oval with pre-translated rect. +#[test] +fn translate_fold_oval_pixel_identical() { + let local_oval = Rect::from_xywh(0.0, 0.0, 50.0, 35.0); + let offsets: &[(f32, f32)] = &[ + (20.0, 30.0), + (90.5, 60.5), + (0.0, 0.0), + (180.0, 250.0), + (0.1, 0.9), + ]; + + for &(tx, ty) in offsets { + let mut paint = Paint::default(); + paint.set_anti_alias(true); + paint.set_color(skia_safe::Color::from_argb(220, 0, 200, 100)); + + // Path A: save/concat/restore + let mut sa = make_surface(); + { + let canvas = sa.canvas(); + canvas.clear(skia_safe::Color::WHITE); + canvas.save(); + canvas.concat(&skia_safe::Matrix::translate((tx, ty))); + canvas.draw_oval(local_oval, &paint); + canvas.restore(); + } + let pixels_a = surface_to_rgba(&mut sa); + + // Path B: translated oval rect (the optimization) + let mut sb = make_surface(); + { + let canvas = sb.canvas(); + canvas.clear(skia_safe::Color::WHITE); + canvas.draw_oval(local_oval.with_offset((tx, ty)), &paint); + } + let pixels_b = surface_to_rgba(&mut sb); + + assert_pixels_identical(&format!("oval({tx},{ty})"), &pixels_a, &pixels_b); + } +} + +// ── Opacity tests ─────────────────────────────────────────────────────── + +/// Test: translating a semi-transparent rect preserves opacity blending. +#[test] +fn translate_fold_opacity_pixel_identical() { + let local_rect = Rect::from_xywh(0.0, 0.0, 60.0, 40.0); + let offsets: &[(f32, f32)] = &[ + (30.0, 40.0), + (100.5, 70.5), + (0.0, 0.0), + ]; + let opacities: &[f32] = &[0.5, 0.3, 0.8, 0.1, 1.0]; + + for &(tx, ty) in offsets { + for &opacity in opacities { + let mut paint = Paint::default(); + paint.set_anti_alias(true); + // Bake opacity into paint alpha (this is what per-paint-alpha does) + paint.set_color(skia_safe::Color::from_argb( + (255.0 * opacity) as u8, + 200, + 50, + 50, + )); + + // Path A: save/concat/restore + let mut sa = make_surface(); + { + let canvas = sa.canvas(); + canvas.clear(skia_safe::Color::WHITE); + canvas.save(); + canvas.concat(&skia_safe::Matrix::translate((tx, ty))); + canvas.draw_rect(local_rect, &paint); + canvas.restore(); + } + let pixels_a = surface_to_rgba(&mut sa); + + // Path B: pre-translated + let mut sb = make_surface(); + { + let canvas = sb.canvas(); + canvas.clear(skia_safe::Color::WHITE); + canvas.draw_rect(local_rect.with_offset((tx, ty)), &paint); + } + let pixels_b = surface_to_rgba(&mut sb); + + assert_pixels_identical( + &format!("opacity({tx},{ty},a={opacity})"), + &pixels_a, + &pixels_b, + ); + } + } +} + +// ── Multi-shape scene test ────────────────────────────────────────────── + +/// Test: render multiple shapes at different positions to verify that +/// translate-fold produces identical composited output. +#[test] +fn translate_fold_multi_shape_scene_pixel_identical() { + struct ShapeDef { + x: f32, + y: f32, + w: f32, + h: f32, + color: skia_safe::Color, + } + + let shapes = vec![ + ShapeDef { x: 10.0, y: 10.0, w: 40.0, h: 30.0, color: skia_safe::Color::from_argb(255, 255, 0, 0) }, + ShapeDef { x: 60.0, y: 20.0, w: 50.0, h: 40.0, color: skia_safe::Color::from_argb(200, 0, 255, 0) }, + ShapeDef { x: 120.5, y: 80.5, w: 35.0, h: 25.0, color: skia_safe::Color::from_argb(180, 0, 0, 255) }, + ShapeDef { x: 200.0, y: 150.0, w: 60.0, h: 50.0, color: skia_safe::Color::from_argb(150, 255, 255, 0) }, + ShapeDef { x: 50.0, y: 250.0, w: 80.0, h: 20.0, color: skia_safe::Color::from_argb(255, 128, 0, 255) }, + ShapeDef { x: 300.0, y: 10.0, w: 45.0, h: 45.0, color: skia_safe::Color::from_argb(200, 0, 200, 200) }, + ]; + + // Path A: each shape drawn via save/concat/restore + let mut sa = make_surface(); + { + let canvas = sa.canvas(); + canvas.clear(skia_safe::Color::WHITE); + for s in &shapes { + let local_rect = Rect::from_xywh(0.0, 0.0, s.w, s.h); + let mut paint = Paint::default(); + paint.set_anti_alias(true); + paint.set_color(s.color); + canvas.save(); + canvas.concat(&skia_safe::Matrix::translate((s.x, s.y))); + canvas.draw_rect(local_rect, &paint); + canvas.restore(); + } + } + let pixels_a = surface_to_rgba(&mut sa); + + // Path B: each shape drawn with pre-translated coords + let mut sb = make_surface(); + { + let canvas = sb.canvas(); + canvas.clear(skia_safe::Color::WHITE); + for s in &shapes { + let translated_rect = Rect::from_xywh(s.x, s.y, s.w, s.h); + let mut paint = Paint::default(); + paint.set_anti_alias(true); + paint.set_color(s.color); + canvas.draw_rect(translated_rect, &paint); + } + } + let pixels_b = surface_to_rgba(&mut sb); + + assert_pixels_identical("multi-shape-scene", &pixels_a, &pixels_b); +} + +// ── Path fallback test ────────────────────────────────────────────────── + +/// Test: for path shapes, the optimization falls back to save/translate/restore. +/// Verify this still matches save/concat(full_matrix)/restore. +#[test] +fn translate_fold_path_fallback_pixel_identical() { + let path = Path::polygon( + &[ + Point::new(0.0, 0.0), + Point::new(30.0, 0.0), + Point::new(30.0, 20.0), + Point::new(15.0, 30.0), + Point::new(0.0, 20.0), + ], + true, + None, + None, + ); + + let offsets: &[(f32, f32)] = &[ + (50.0, 50.0), + (150.5, 100.5), + (0.0, 0.0), + ]; + + for &(tx, ty) in offsets { + let mut paint = Paint::default(); + paint.set_anti_alias(true); + paint.set_color(skia_safe::Color::from_argb(255, 200, 100, 50)); + + // Path A: save/concat(translate_matrix)/restore + let mut sa = make_surface(); + { + let canvas = sa.canvas(); + canvas.clear(skia_safe::Color::WHITE); + canvas.save(); + canvas.concat(&skia_safe::Matrix::translate((tx, ty))); + canvas.draw_path(&path, &paint); + canvas.restore(); + } + let pixels_a = surface_to_rgba(&mut sa); + + // Path B: save/translate/draw/restore (the optimization's fallback) + let mut sb = make_surface(); + { + let canvas = sb.canvas(); + canvas.clear(skia_safe::Color::WHITE); + if tx == 0.0 && ty == 0.0 { + canvas.draw_path(&path, &paint); + } else { + canvas.save(); + canvas.translate((tx, ty)); + canvas.draw_path(&path, &paint); + canvas.restore(); + } + } + let pixels_b = surface_to_rgba(&mut sb); + + assert_pixels_identical(&format!("path({tx},{ty})"), &pixels_a, &pixels_b); + } +} diff --git a/crates/grida-dev/examples/bench_cache_picture.rs b/crates/grida-dev/examples/bench_cache_picture.rs index f8532c2e8c..696410457b 100644 --- a/crates/grida-dev/examples/bench_cache_picture.rs +++ b/crates/grida-dev/examples/bench_cache_picture.rs @@ -1,4 +1,4 @@ -use cg::sys::scheduler::FrameScheduler; +use std::collections::VecDeque; use glutin::{ config::{ConfigTemplateBuilder, GlConfig}, context::{ContextApi, ContextAttributesBuilder, PossiblyCurrentContext}, @@ -221,8 +221,7 @@ fn main() { let mut last_fps_time = Instant::now(); let mut last_frame_time = Instant::now(); - // Create frame scheduler with 120 FPS target and 144 FPS max - let mut scheduler = FrameScheduler::new(120).with_max_fps(144); + let mut fps_samples: VecDeque = VecDeque::with_capacity(60); // Enable pre-present notification for better frame timing window.pre_present_notify(); @@ -269,18 +268,26 @@ fn main() { eprintln!("Error swapping buffers: {:?}", e); } - // Frame timing and pacing + // Frame timing let frame_time = now.duration_since(last_frame_time); - scheduler.sleep_to_maintain_fps(); + if fps_samples.len() == 60 { fps_samples.pop_front(); } + fps_samples.push_back(frame_time); last_frame_time = now; // FPS calculation frame_count += 1; if now.duration_since(last_fps_time).as_secs_f32() >= 1.0 { + let avg_fps = if fps_samples.is_empty() { + 0.0 + } else { + let total: std::time::Duration = fps_samples.iter().copied().sum(); + let avg = total / fps_samples.len() as u32; + 1_000_000.0 / avg.as_micros() as f64 + }; println!( - "FPS: {} (Target: {:.1}, Frame Time: {:.2}ms)", + "FPS: {} (Avg: {:.1}, Frame Time: {:.2}ms)", frame_count, - scheduler.average_fps(), + avg_fps, frame_time.as_secs_f32() * 1000.0 ); frame_count = 0; diff --git a/crates/grida-dev/examples/sys_winit_raf.rs b/crates/grida-dev/examples/sys_winit_raf.rs index 07b5d8f153..b20dabfb97 100644 --- a/crates/grida-dev/examples/sys_winit_raf.rs +++ b/crates/grida-dev/examples/sys_winit_raf.rs @@ -1,4 +1,3 @@ -use cg::sys::scheduler; use std::time::{Duration, Instant}; use std::time::{SystemTime, UNIX_EPOCH}; use winit::{ @@ -21,7 +20,6 @@ struct App { window: Window, frame_count: u32, start_time: Instant, - scheduler: scheduler::FrameScheduler, } impl App { @@ -50,8 +48,6 @@ impl ApplicationHandler for App { winit::event::WindowEvent::RedrawRequested => { self.render(); // Simulate some frame rendering work - self.scheduler.sleep_to_maintain_fps(); // Apply pacing (no-op on wasm) - self.frame_count += 1; // Log FPS every second @@ -84,12 +80,10 @@ fn main() { let now = Instant::now(); - // Initialize application with both a target and max FPS let mut app = App { window, frame_count: 0, start_time: now, - scheduler: scheduler::FrameScheduler::new(u32::MAX).with_max_fps(u32::MAX), }; // Start the app's event loop diff --git a/crates/grida-dev/src/bench/args.rs b/crates/grida-dev/src/bench/args.rs new file mode 100644 index 0000000000..0a61c9ee1f --- /dev/null +++ b/crates/grida-dev/src/bench/args.rs @@ -0,0 +1,43 @@ +use clap::Args; + +#[derive(Args, Debug)] +pub struct BenchArgs { + /// Path to a `.grida` file (optional; uses synthetic grid if omitted). + pub path: Option, + /// Grid dimension when no file is given (renders N x N rectangles). + #[arg(long = "size", default_value_t = 100)] + pub size: u32, + /// Scene index to benchmark (0-based). Use --list-scenes to see available. + #[arg(long = "scene", default_value_t = 0)] + pub scene_index: usize, + /// List available scene names and exit. + #[arg(long = "list-scenes", default_value_t = false)] + pub list_scenes: bool, + /// Number of pan frames to measure. + #[arg(long = "frames", default_value_t = 200)] + pub frames: u32, + /// Viewport width. + #[arg(long = "width", default_value_t = 1000)] + pub width: i32, + /// Viewport height. + #[arg(long = "height", default_value_t = 1000)] + pub height: i32, +} + +#[derive(Args, Debug)] +pub struct BenchReportArgs { + /// Path to a `.grida` file or a directory (recursively finds `*.grida` files). + pub path: String, + /// Number of frames per benchmark pass (pan and zoom each). + #[arg(long = "frames", default_value_t = 100)] + pub frames: u32, + /// Viewport width. + #[arg(long = "width", default_value_t = 1000)] + pub width: i32, + /// Viewport height. + #[arg(long = "height", default_value_t = 1000)] + pub height: i32, + /// Output file path for the JSON report (stdout if omitted). + #[arg(long = "output")] + pub output: Option, +} diff --git a/crates/grida-dev/src/bench/mod.rs b/crates/grida-dev/src/bench/mod.rs new file mode 100644 index 0000000000..540edd51ac --- /dev/null +++ b/crates/grida-dev/src/bench/mod.rs @@ -0,0 +1,6 @@ +pub mod args; +pub mod report; +pub mod runner; + +pub use args::{BenchArgs, BenchReportArgs}; +pub use runner::{run_bench, run_bench_report}; diff --git a/crates/grida-dev/src/bench/report.rs b/crates/grida-dev/src/bench/report.rs new file mode 100644 index 0000000000..edad9c8f05 --- /dev/null +++ b/crates/grida-dev/src/bench/report.rs @@ -0,0 +1,53 @@ +#[derive(serde::Serialize)] +pub struct BenchReportOutput { + pub meta: BenchReportMeta, + pub results: Vec, + pub errors: Vec, +} + +#[derive(serde::Serialize)] +pub struct BenchReportMeta { + pub frames: u32, + pub viewport: [i32; 2], + pub files_count: usize, + pub scenes_count: usize, +} + +#[derive(serde::Serialize)] +pub struct SceneBenchResult { + pub file: String, + pub scene: String, + pub scene_index: usize, + pub nodes: usize, + pub effects_nodes: usize, + pub pan: PanStats, + pub zoom: ZoomStats, +} + +#[derive(serde::Serialize)] +pub struct PanStats { + pub avg_us: u64, + pub fps: f64, + pub p50_us: u64, + pub p95_us: u64, + pub p99_us: u64, + pub draw_us: u64, + pub mid_flush_us: u64, + pub compositor_us: u64, + pub flush_us: u64, +} + +#[derive(serde::Serialize)] +pub struct ZoomStats { + pub avg_us: u64, + pub fps: f64, + pub p50_us: u64, + pub p95_us: u64, + pub p99_us: u64, +} + +#[derive(serde::Serialize)] +pub struct BenchError { + pub file: String, + pub error: String, +} diff --git a/crates/grida-dev/src/bench/runner.rs b/crates/grida-dev/src/bench/runner.rs new file mode 100644 index 0000000000..ebe5ade2d0 --- /dev/null +++ b/crates/grida-dev/src/bench/runner.rs @@ -0,0 +1,389 @@ +use super::args::{BenchArgs, BenchReportArgs}; +use super::report::*; +use anyhow::{anyhow, Result}; +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::scene::FrameFlushResult; +use cg::window::headless::HeadlessGpu; +use math2::transform::AffineTransform; +use std::path::{Path, PathBuf}; +use std::time::Instant; + +// --------------------------------------------------------------------------- +// Shared helpers +// --------------------------------------------------------------------------- + +fn count_effects_nodes(renderer: &cg::runtime::scene::Renderer) -> usize { + renderer + .scene + .as_ref() + .map(|s| { + s.graph + .nodes_iter() + .filter(|(_, node)| match node { + Node::Rectangle(r) => r.effects.has_expensive_effects(), + Node::Ellipse(e) => e.effects.has_expensive_effects(), + _ => false, + }) + .count() + }) + .unwrap_or(0) +} + +fn warmup(renderer: &mut cg::runtime::scene::Renderer) { + renderer.queue_stable(); + let _ = renderer.flush(); + for _ in 0..10 { + renderer.camera.translate(1.0, 0.0); + renderer.queue_unstable(); + let _ = renderer.flush(); + } +} + +fn run_pan_pass(renderer: &mut cg::runtime::scene::Renderer, frames: u32) -> PanStats { + let pan_start = Instant::now(); + let mut frame_times = Vec::with_capacity(frames as usize); + let mut draw_us_acc = Vec::with_capacity(frames as usize); + let mut mid_flush_us_acc = Vec::with_capacity(frames as usize); + let mut compositor_us_acc = Vec::with_capacity(frames as usize); + let mut flush_us_acc = Vec::with_capacity(frames as usize); + + for i in 0..frames { + let dx = if i % 2 == 0 { 5.0 } else { -5.0 }; + renderer.camera.translate(dx, 0.0); + renderer.queue_unstable(); + let t = Instant::now(); + if let FrameFlushResult::OK(stats) = renderer.flush() { + frame_times.push(t.elapsed().as_micros() as u64); + 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); + } + } + let pan_wall = pan_start.elapsed(); + + if frame_times.is_empty() { + return PanStats { + avg_us: 0, fps: 0.0, p50_us: 0, p95_us: 0, p99_us: 0, + draw_us: 0, mid_flush_us: 0, compositor_us: 0, flush_us: 0, + }; + } + + frame_times.sort(); + let n = frame_times.len(); + let avg = pan_wall.as_micros() as u64 / n as u64; + PanStats { + avg_us: avg, + fps: 1_000_000.0 / avg as f64, + p50_us: frame_times[n / 2], + p95_us: frame_times[n * 95 / 100], + p99_us: frame_times[n * 99 / 100], + draw_us: draw_us_acc.iter().sum::() / n as u64, + mid_flush_us: mid_flush_us_acc.iter().sum::() / n as u64, + compositor_us: compositor_us_acc.iter().sum::() / n as u64, + flush_us: flush_us_acc.iter().sum::() / n as u64, + } +} + +fn run_zoom_pass(renderer: &mut cg::runtime::scene::Renderer, frames: u32) -> ZoomStats { + renderer.camera.set_zoom(1.0); + let zoom_start = Instant::now(); + let mut zoom_times = Vec::with_capacity(frames as usize); + let mut z = 1.0f32; + let mut zdir = 1; + + for _ in 0..frames { + z += zdir as f32 * 0.02; + if z > 2.0 || z < 0.5 { + zdir = -zdir; + } + renderer.camera.set_zoom(z); + renderer.queue_unstable(); + let t = Instant::now(); + if let FrameFlushResult::OK(_) = renderer.flush() { + zoom_times.push(t.elapsed().as_micros() as u64); + } + } + let zoom_wall = zoom_start.elapsed(); + + if zoom_times.is_empty() { + return ZoomStats { avg_us: 0, fps: 0.0, p50_us: 0, p95_us: 0, p99_us: 0 }; + } + + zoom_times.sort(); + let n = zoom_times.len(); + let avg = zoom_wall.as_micros() as u64 / n as u64; + ZoomStats { + avg_us: avg, + fps: 1_000_000.0 / avg as f64, + p50_us: zoom_times[n / 2], + p95_us: zoom_times[n * 95 / 100], + p99_us: zoom_times[n * 99 / 100], + } +} + +fn collect_grida_files(path: &Path) -> Vec { + if path.is_file() { + return vec![path.to_path_buf()]; + } + let mut files = Vec::new(); + fn walk(dir: &Path, out: &mut Vec) { + let entries = match std::fs::read_dir(dir) { + Ok(e) => e, + Err(_) => return, + }; + for entry in entries.flatten() { + let p = entry.path(); + if p.is_dir() { + walk(&p, out); + } else if p.extension().map(|e| e == "grida").unwrap_or(false) { + out.push(p); + } + } + } + walk(path, &mut files); + files.sort(); + files +} + +fn build_benchmark_scene(grid: u32) -> Scene { + let nf = NodeFactory::new(); + let mut graph = SceneGraph::new(); + let grid = grid.max(1); + let size = 18.0f32; + let spacing = 6.0f32; + + for y in 0..grid { + for x in 0..grid { + let mut rect = nf.create_rectangle_node(); + rect.transform = AffineTransform::new( + 40.0 + x as f32 * (size + spacing), + 40.0 + y as f32 * (size + spacing), + 0.0, + ); + rect.size = Size { + width: size, + height: size, + }; + rect.fills = Paints::new([Paint::Solid(SolidPaint { + color: CGColor::from_rgb(((x * 11) % 255) as u8, ((y * 7) % 255) as u8, 210), + blend_mode: BlendMode::default(), + active: true, + })]); + graph.append_child(Node::Rectangle(rect), Parent::Root); + } + } + + Scene { + name: format!("Benchmark {}x{}", grid, grid), + graph, + background_color: Some(CGColor::from_rgb(250, 250, 250)), + } +} + +// --------------------------------------------------------------------------- +// Single-scene bench (human-readable output) +// --------------------------------------------------------------------------- + +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 { + vec![build_benchmark_scene(args.size)] + }; + + if args.list_scenes { + println!("Available scenes ({}):", scenes.len()); + for (i, s) in scenes.iter().enumerate() { + println!(" [{}] {} ({} nodes)", i, s.name, s.graph.node_count()); + } + return Ok(()); + } + + if args.scene_index >= scenes.len() { + return Err(anyhow!( + "scene index {} out of range (0..{}). Use --list-scenes.", + args.scene_index, + scenes.len() + )); + } + + 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}"))?; + gpu.print_gl_info(); + + let mut renderer = gpu.create_renderer(); + renderer.load_scene(scene); + renderer.fit_camera_to_scene(); + + let cam_rect = renderer.camera.rect(); + println!("Loaded scene: {} nodes", node_count); + println!( + "Camera: zoom={:.4} viewport=({:.0}x{:.0})", + renderer.camera.get_zoom(), + cam_rect.width, + cam_rect.height, + ); + println!( + "Viewport: {}x{}, frames: {}\n", + args.width, args.height, args.frames + ); + + warmup(&mut renderer); + + let effects_count = count_effects_nodes(&renderer); + let comp_stats = renderer.get_cache().compositor.stats(); + println!( + "Nodes with effects: {} Compositor: {} promoted, {:.1} KB", + effects_count, + comp_stats.promoted_count, + comp_stats.memory_bytes as f64 / 1024.0, + ); + + // --- Pan --- + println!("=== Pan benchmark ({} frames) ===", args.frames); + let pan = run_pan_pass(&mut renderer, args.frames); + println!(" avg: {:>7} us ({:>6.1} fps)", pan.avg_us, pan.fps); + println!( + " p50: {:>7} us p95: {:>7} us p99: {:>7} us", + pan.p50_us, pan.p95_us, pan.p99_us + ); + println!( + " draw: {} us mid_flush(draw GPU): {} us compositor: {} us end_flush: {} us", + pan.draw_us, pan.mid_flush_us, pan.compositor_us, pan.flush_us + ); + + // --- Zoom --- + println!("\n=== Zoom benchmark ({} frames) ===", args.frames); + let zoom = run_zoom_pass(&mut renderer, args.frames); + println!( + " avg: {:>7} us ({:>6.1} fps) p50: {:>7} us p95: {:>7} us", + zoom.avg_us, zoom.fps, zoom.p50_us, zoom.p95_us + ); + + drop(renderer); + println!("\nDone."); + Ok(()) +} + +// --------------------------------------------------------------------------- +// Bulk bench-report (JSON output) +// --------------------------------------------------------------------------- + +pub async fn run_bench_report( + args: BenchReportArgs, + load_scenes: impl AsyncSceneLoader, +) -> Result<()> { + let input_path = Path::new(&args.path); + if !input_path.exists() { + return Err(anyhow!("path not found: {}", args.path)); + } + + let files = collect_grida_files(input_path); + if files.is_empty() { + return Err(anyhow!("no .grida files found in {}", args.path)); + } + + eprintln!( + "bench-report: {} files, {} frames/pass, {}x{} viewport", + files.len(), args.frames, args.width, args.height + ); + + let mut results = Vec::new(); + let mut errors = Vec::new(); + + for (fi, file_path) in files.iter().enumerate() { + let file_str = file_path.to_string_lossy().to_string(); + eprintln!("[{}/{}] {}", fi + 1, files.len(), file_str); + + let scenes = match load_scenes.load(&file_str).await { + Ok(s) => s, + Err(e) => { + errors.push(BenchError { + file: file_str, + error: format!("{e}"), + }); + continue; + } + }; + + for (si, scene) in scenes.into_iter().enumerate() { + let node_count = scene.graph.node_count(); + let scene_name = scene.name.clone(); + eprintln!(" scene[{}] \"{}\" ({} nodes)", si, scene_name, node_count); + + let mut gpu = match HeadlessGpu::new(args.width, args.height) { + Ok(g) => g, + Err(e) => { + errors.push(BenchError { + file: file_str.clone(), + error: format!("GPU init failed for scene {si}: {e}"), + }); + continue; + } + }; + + let mut renderer = gpu.create_renderer(); + renderer.load_scene(scene); + renderer.fit_camera_to_scene(); + warmup(&mut renderer); + + let effects_count = count_effects_nodes(&renderer); + let pan = run_pan_pass(&mut renderer, args.frames); + let zoom = run_zoom_pass(&mut renderer, args.frames); + + drop(renderer); + + results.push(SceneBenchResult { + file: file_str.clone(), + scene: scene_name, + scene_index: si, + nodes: node_count, + effects_nodes: effects_count, + pan, + zoom, + }); + } + } + + let report = BenchReportOutput { + meta: BenchReportMeta { + frames: args.frames, + viewport: [args.width, args.height], + files_count: files.len(), + scenes_count: results.len(), + }, + results, + errors, + }; + + let json = serde_json::to_string_pretty(&report)?; + + if let Some(ref out_path) = args.output { + std::fs::write(out_path, &json)?; + eprintln!("report written to {out_path}"); + } else { + println!("{json}"); + } + + eprintln!("bench-report done."); + Ok(()) +} + +// --------------------------------------------------------------------------- +// Scene loading trait — decouples bench module from main.rs file loading +// --------------------------------------------------------------------------- + +#[allow(async_fn_in_trait)] +pub trait AsyncSceneLoader { + async fn load(&self, source: &str) -> Result>; +} diff --git a/crates/grida-dev/src/main.rs b/crates/grida-dev/src/main.rs index efca43e046..4d0cf5c29c 100644 --- a/crates/grida-dev/src/main.rs +++ b/crates/grida-dev/src/main.rs @@ -1,20 +1,15 @@ -use anyhow::{anyhow, Context, Result}; -use cg::cg::prelude::*; -use cg::cg::types::ResourceRef; -use cg::node::factory::NodeFactory; -use cg::node::scene_graph::{Parent, SceneGraph}; -use cg::node::schema::{Node, Scene, Size}; +use anyhow::{Context, Result}; +use cg::node::schema::Scene; use cg::resources::{load_scene_images, ImageMessage}; use cg::svg::pack; use cg::window::application::{HostEvent, HostEventCallback}; -use clap::{Args, Parser, Subcommand}; +use clap::{Parser, Subcommand}; use futures::channel::mpsc; use grida_dev::platform::native_demo::run_demo_window_with_drop; +mod bench; mod grida_file; mod reftest; use image::image_dimensions; -use math2::transform::AffineTransform; -use std::fs; use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; use tokio::fs as async_fs; @@ -42,51 +37,48 @@ struct Cli { enum Command { /// Headless GPU benchmark — no window, prints per-frame stats. /// Accepts either a `.grida` file or `--size N` for a synthetic grid. - Bench(BenchArgs), + Bench(bench::BenchArgs), /// Run SVG reftests against W3C SVG 1.1 Test Suite. Reftest(reftest::ReftestArgs), /// Convert SVG files to `.grida` for cross-boundary codec testing. /// Output goes to `fixtures/test-svg/.generated/`. SvgToGrida(SvgToGridaArgs), -} - -#[derive(Args, Debug)] -struct BenchArgs { - /// Path to a `.grida` file (optional; uses synthetic grid if omitted). - path: Option, - /// Grid dimension when no file is given (renders N x N rectangles). - #[arg(long = "size", default_value_t = 100)] - size: u32, - /// Scene index to benchmark (0-based). Use --list-scenes to see available. - #[arg(long = "scene", default_value_t = 0)] - scene_index: usize, - /// List available scene names and exit. - #[arg(long = "list-scenes", default_value_t = false)] - list_scenes: bool, - /// Number of pan frames to measure. - #[arg(long = "frames", default_value_t = 200)] - frames: u32, - /// Viewport width. - #[arg(long = "width", default_value_t = 1000)] - width: i32, - /// Viewport height. - #[arg(long = "height", default_value_t = 1000)] - height: i32, + /// Bulk benchmark — runs all scenes in all `.grida` files, outputs a compact JSON report. + /// Accepts a single `.grida` file or a directory (recursively finds `*.grida` files). + BenchReport(bench::BenchReportArgs), } #[tokio::main] async fn main() -> Result<()> { let cli = Cli::parse(); + let loader = FileSceneLoader; match cli.command { - Some(Command::Bench(args)) => run_bench(args).await?, + Some(Command::Bench(args)) => bench::run_bench(args, loader).await?, Some(Command::Reftest(args)) => reftest::run(args).await?, Some(Command::SvgToGrida(args)) => run_svg_to_grida(args), + Some(Command::BenchReport(args)) => bench::run_bench_report(args, loader).await?, None => run_interactive(cli.file).await?, } Ok(()) } -#[derive(Args, Debug)] +// --------------------------------------------------------------------------- +// Scene loader — bridges main.rs file I/O into the bench module +// --------------------------------------------------------------------------- + +struct FileSceneLoader; + +impl bench::runner::AsyncSceneLoader for FileSceneLoader { + async fn load(&self, source: &str) -> Result> { + load_scenes_from_source(source).await + } +} + +// --------------------------------------------------------------------------- +// SVG-to-Grida converter +// --------------------------------------------------------------------------- + +#[derive(clap::Args, Debug)] struct SvgToGridaArgs { /// Input directory containing SVG files. Defaults to `fixtures/test-svg/L0`. path: Option, @@ -103,7 +95,6 @@ struct SvgToGridaArgs { fn run_svg_to_grida(args: SvgToGridaArgs) { use cg::io::io_svg::svg_to_grida_bytes; - use std::path::{Path, PathBuf}; let input_dir = PathBuf::from( args.path @@ -181,252 +172,11 @@ fn run_svg_to_grida(args: SvgToGridaArgs) { ); } -async fn run_bench(args: BenchArgs) -> Result<()> { - use cg::runtime::scene::FrameFlushResult; - use cg::window::headless::HeadlessGpu; - use std::time::Instant; - - let scenes = if let Some(ref path) = args.path { - load_scenes_from_source(path).await? - } else { - vec![build_benchmark_scene(args.size)] - }; - - if args.list_scenes { - println!("Available scenes ({}):", scenes.len()); - for (i, s) in scenes.iter().enumerate() { - println!(" [{}] {} ({} nodes)", i, s.name, s.graph.node_count()); - } - return Ok(()); - } - - if args.scene_index >= scenes.len() { - return Err(anyhow!( - "scene index {} out of range (0..{}). Use --list-scenes.", - args.scene_index, - scenes.len() - )); - } - - 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}"))?; - gpu.print_gl_info(); - - let mut renderer = gpu.create_renderer(); - renderer.load_scene(scene); - - // Fit camera so all content is visible — same as windowed demo. - renderer.fit_camera_to_scene(); - - let cam_rect = renderer.camera.rect(); - println!("Loaded scene: {} nodes", node_count); - println!( - "Camera: zoom={:.4} viewport=({:.0}x{:.0})", - renderer.camera.get_zoom(), - cam_rect.width, - cam_rect.height, - ); - println!( - "Viewport: {}x{}, frames: {}\n", - args.width, args.height, args.frames - ); - - // Warm up: stable frame first (populates compositor cache), then - // unstable pan frames to fill picture/geometry caches. - renderer.queue_stable(); - let _ = renderer.flush(); - for _ in 0..10 { - renderer.camera.translate(1.0, 0.0); - renderer.queue_unstable(); - let _ = renderer.flush(); - } - - // Count nodes with effects for diagnostics. - let effects_count = renderer - .scene - .as_ref() - .map(|s| { - s.graph - .nodes_iter() - .filter(|(_, node)| match node { - cg::node::schema::Node::Rectangle(r) => r.effects.has_expensive_effects(), - cg::node::schema::Node::Ellipse(e) => e.effects.has_expensive_effects(), - _ => false, - }) - .count() - }) - .unwrap_or(0); - - let comp_stats = renderer.get_cache().compositor.stats(); - println!( - "Nodes with effects: {} Compositor: {} promoted, {:.1} KB", - effects_count, - comp_stats.promoted_count, - comp_stats.memory_bytes as f64 / 1024.0, - ); - - // --- Pan benchmark --- - println!("=== Pan benchmark ({} frames) ===", args.frames); - let pan_start = Instant::now(); - let mut frame_times = Vec::with_capacity(args.frames as usize); - let mut internal_render_us = Vec::with_capacity(args.frames as usize); - let mut internal_flush_us = Vec::with_capacity(args.frames as usize); - let mut internal_draw_us = Vec::with_capacity(args.frames as usize); - let mut total_dl = 0usize; - let mut total_live = 0usize; - let mut total_comp_hits = 0usize; - let mut internal_compositor_us = Vec::with_capacity(args.frames as usize); - let mut internal_mid_flush_us = Vec::with_capacity(args.frames as usize); - - for i in 0..args.frames { - let dx = if i % 2 == 0 { 5.0 } else { -5.0 }; - renderer.camera.translate(dx, 0.0); - renderer.queue_unstable(); - let frame_start = Instant::now(); - if let FrameFlushResult::OK(stats) = renderer.flush() { - let ft = frame_start.elapsed(); - frame_times.push(ft); - internal_render_us.push(stats.total_duration.as_micros() as u64); - internal_flush_us.push(stats.flush_duration.as_micros() as u64); - internal_draw_us.push(stats.draw.painter_duration.as_micros() as u64); - internal_compositor_us.push(stats.compositor_duration.as_micros() as u64); - internal_mid_flush_us.push(stats.mid_flush_duration.as_micros() as u64); - total_dl += stats.frame.display_list_size_estimated; - total_live += stats.draw.live_draw_count; - total_comp_hits += stats.draw.layer_image_cache_hits; - } - } - let pan_wall = pan_start.elapsed(); - - if frame_times.is_empty() { - return Err(anyhow!( - "no benchmark samples collected, cannot compute summary" - )); - } - - frame_times.sort(); - let n = frame_times.len(); - let p50 = frame_times[n / 2]; - let p95 = frame_times[n * 95 / 100]; - let p99 = frame_times[n * 99 / 100]; - let avg = pan_wall / n as u32; - let fps = 1_000_000.0 / avg.as_micros() as f64; - - let _avg_render = internal_render_us.iter().sum::() / n as u64; - let avg_flush = internal_flush_us.iter().sum::() / n as u64; - let avg_draw = internal_draw_us.iter().sum::() / n as u64; - let avg_compositor = internal_compositor_us.iter().sum::() / n as u64; - let avg_mid_flush = internal_mid_flush_us.iter().sum::() / n as u64; - - println!( - " avg: {:>7} us ({:>6.1} fps)", - avg.as_micros(), - fps - ); - println!( - " p50: {:>7} us p95: {:>7} us p99: {:>7} us", - p50.as_micros(), - p95.as_micros(), - p99.as_micros() - ); - println!( - " draw: {} us mid_flush(draw GPU): {} us compositor: {} us end_flush: {} us", - avg_draw, avg_mid_flush, avg_compositor, avg_flush - ); - println!( - " dl: {} live: {} comp_hits: {} wall: {:.1} ms", - total_dl / n, - total_live / n, - total_comp_hits / n, - pan_wall.as_secs_f64() * 1000.0 - ); - - // --- Zoom benchmark --- - println!("\n=== Zoom benchmark ({} frames) ===", args.frames); - renderer.camera.set_zoom(1.0); - let zoom_start = Instant::now(); - let mut zoom_times = Vec::with_capacity(args.frames as usize); - let mut z = 1.0f32; - let mut zdir = 1; - - for _ in 0..args.frames { - z += zdir as f32 * 0.02; - if z > 2.0 || z < 0.5 { - zdir = -zdir; - } - renderer.camera.set_zoom(z); - renderer.queue_unstable(); - let frame_start = Instant::now(); - if let FrameFlushResult::OK(_) = renderer.flush() { - zoom_times.push(frame_start.elapsed()); - } - } - let zoom_wall = zoom_start.elapsed(); - - zoom_times.sort(); - let zn = zoom_times.len(); - let zp50 = zoom_times[zn / 2]; - let zp95 = zoom_times[zn * 95 / 100]; - let zavg = zoom_wall / zn as u32; - let zfps = 1_000_000.0 / zavg.as_micros() as f64; - - println!( - " avg: {:>7.1} us ({:>6.1} fps) p50: {:>7.1} us p95: {:>7.1} us wall: {:.1} ms", - zavg.as_micros(), - zfps, - zp50.as_micros(), - zp95.as_micros(), - zoom_wall.as_secs_f64() * 1000.0 - ); - - drop(renderer); - println!("\nDone."); - Ok(()) -} - -async fn run_interactive(file: Option) -> Result<()> { - // Load initial scenes from the CLI argument (file path or URL), if given. - let initial_scenes = if let Some(ref source) = file { - load_scenes_from_source(source).await? - } else { - vec![build_empty_scene()] - }; - - let first = initial_scenes - .first() - .cloned() - .expect("at least one scene"); - - let (drop_tx, drop_rx) = unbounded_channel::(); - let (scenes_tx, scenes_rx) = unbounded_channel::>(); - let drop_rx = Arc::new(Mutex::new(Some(drop_rx))); - - // Seed the scenes channel with the initial set so the window picks them - // up on the first tick (enables PageUp/PageDown for multi-scene files). - if initial_scenes.len() > 1 { - let _ = scenes_tx.send(initial_scenes); - } - - run_demo_window_with_drop( - first, - move |_renderer, tx, _font_tx, proxy| { - 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); - }, - drop_tx, - scenes_rx, - ) - .await; - - Ok(()) -} +// --------------------------------------------------------------------------- +// Scene loading helpers (shared by interactive mode and FileSceneLoader) +// --------------------------------------------------------------------------- async fn load_scenes_from_source(source: &str) -> Result> { - // If it looks like a local file with a known extension, route by type. if !is_url(source) { let path = Path::new(source); if let Some(ext) = path.extension().and_then(|e| e.to_str()) { @@ -435,7 +185,7 @@ async fn load_scenes_from_source(source: &str) -> Result> { "png" | "jpg" | "jpeg" | "webp" => { return scene_from_raster_path(path).map(|s| vec![s]) } - _ => {} // fall through to grida/json decoding + _ => {} } } } @@ -459,46 +209,17 @@ async fn read_source_bytes(source: &str) -> Result> { } } -fn build_benchmark_scene(grid: u32) -> Scene { - let nf = NodeFactory::new(); - let mut graph = SceneGraph::new(); - let grid = grid.max(1); - let size = 18.0f32; - let spacing = 6.0f32; - - for y in 0..grid { - for x in 0..grid { - let mut rect = nf.create_rectangle_node(); - rect.transform = AffineTransform::new( - 40.0 + x as f32 * (size + spacing), - 40.0 + y as f32 * (size + spacing), - 0.0, - ); - rect.size = Size { - width: size, - height: size, - }; - rect.fills = Paints::new([Paint::Solid(SolidPaint { - color: CGColor::from_rgb(((x * 11) % 255) as u8, ((y * 7) % 255) as u8, 210), - blend_mode: BlendMode::default(), - active: true, - })]); - graph.append_child(Node::Rectangle(rect), Parent::Root); - } - } - - Scene { - name: format!("Benchmark {}x{}", grid, grid), - graph, - background_color: Some(CGColor::from_rgb(250, 250, 250)), - } -} - fn is_url(path: &str) -> bool { path.starts_with("http://") || path.starts_with("https://") } +// --------------------------------------------------------------------------- +// Interactive windowed mode +// --------------------------------------------------------------------------- + fn build_empty_scene() -> Scene { + use cg::cg::prelude::CGColor; + use cg::node::scene_graph::SceneGraph; Scene { name: "Drop a file to begin".to_string(), graph: SceneGraph::new(), @@ -506,30 +227,47 @@ fn build_empty_scene() -> Scene { } } -async fn load_master_scenes_from_path(path: &Path) -> Result> { - let ext = path - .extension() - .and_then(|e| e.to_str()) - .map(|s| s.to_ascii_lowercase()) - .ok_or_else(|| anyhow!("Dropped file has no extension: {}", path.display()))?; +async fn run_interactive(file: Option) -> Result<()> { + let initial_scenes = if let Some(ref source) = file { + load_scenes_from_source(source).await? + } else { + vec![build_empty_scene()] + }; - 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]), - other => Err(anyhow!( - "Unsupported dropped file type ({}): {}", - other, - path.display() - )), + let first = initial_scenes + .first() + .cloned() + .expect("at least one scene"); + + let (drop_tx, drop_rx) = unbounded_channel::(); + let (scenes_tx, scenes_rx) = unbounded_channel::>(); + let drop_rx = Arc::new(Mutex::new(Some(drop_rx))); + + if initial_scenes.len() > 1 { + let _ = scenes_tx.send(initial_scenes); } + + run_demo_window_with_drop( + first, + move |_renderer, tx, _font_tx, proxy| { + 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); + }, + drop_tx, + scenes_rx, + ) + .await; + + Ok(()) } fn scene_from_svg_path(path: &Path) -> Result { + use cg::cg::prelude::CGColor; let svg_source = - fs::read_to_string(path).with_context(|| format!("failed to read {}", path.display()))?; + std::fs::read_to_string(path).with_context(|| format!("failed to read {}", path.display()))?; let graph = pack::from_svg_str(&svg_source) - .map_err(|err| anyhow!("failed to convert SVG {}: {err}", path.display()))?; + .map_err(|err| anyhow::anyhow!("failed to convert SVG {}: {err}", path.display()))?; Ok(Scene { name: path @@ -542,6 +280,11 @@ fn scene_from_svg_path(path: &Path) -> Result { } fn scene_from_raster_path(path: &Path) -> Result { + use cg::cg::prelude::CGColor; + use cg::cg::types::ResourceRef; + use cg::node::factory::NodeFactory; + use cg::node::scene_graph::{Parent, SceneGraph}; + use cg::node::schema::{Node, Size}; let (width, height) = image_dimensions(path) .with_context(|| format!("failed to read image dimensions {}", path.display()))?; let mut graph = SceneGraph::new(); @@ -566,6 +309,25 @@ fn scene_from_raster_path(path: &Path) -> Result { }) } +async fn load_master_scenes_from_path(path: &Path) -> Result> { + let ext = path + .extension() + .and_then(|e| e.to_str()) + .map(|s| s.to_ascii_lowercase()) + .ok_or_else(|| anyhow::anyhow!("Dropped file has no extension: {}", path.display()))?; + + 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]), + other => Err(anyhow::anyhow!( + "Unsupported dropped file type ({}): {}", + other, + path.display() + )), + } +} + fn start_master_drop_task( mut drop_rx: UnboundedReceiver, image_tx: mpsc::UnboundedSender, @@ -582,7 +344,6 @@ fn start_master_drop_task( continue; } - // Load images for all scenes in the background. for scene in scenes_for_loader { let tx_clone = image_tx.clone(); let proxy_clone = proxy.clone(); diff --git a/crates/grida-dev/src/platform/native_application.rs b/crates/grida-dev/src/platform/native_application.rs index 3584ca82f9..8f43603fec 100644 --- a/crates/grida-dev/src/platform/native_application.rs +++ b/crates/grida-dev/src/platform/native_application.rs @@ -37,9 +37,18 @@ fn handle_window_event( }, .. } => handle_key_pressed(key, modifiers), - WindowEvent::PinchGesture { delta, .. } => ApplicationCommand::ZoomDelta { - delta: *delta as f32, - }, + WindowEvent::PinchGesture { delta, .. } => { + // Deadzone: ignore tiny pinch deltas that macOS trackpads + // generate incidentally during two-finger scroll. Without this, + // every pan gesture registers as PanAndZoom, defeating pan-only + // optimizations (pan image cache, etc.). + let d = *delta as f32; + if d.abs() < 0.002 { + ApplicationCommand::None + } else { + ApplicationCommand::ZoomDelta { delta: d } + } + } WindowEvent::MouseWheel { delta, .. } => { if modifiers.super_key() || modifiers.control_key() { // Cmd+scroll (macOS) or Ctrl+scroll → zoom, same as pinch @@ -101,6 +110,9 @@ pub struct NativeApplication { pub(crate) modifiers: winit::keyboard::ModifiersState, file_drop_tx: Option>, fit_scene_on_load: 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, /// All scenes loaded from the file (for PageUp/PageDown switching). pub(crate) scenes: Vec, /// Index of the currently displayed scene in `scenes`. @@ -183,6 +195,7 @@ impl NativeApplication { modifiers: winit::keyboard::ModifiersState::default(), file_drop_tx, fit_scene_on_load, + settle_countdown: 0, scenes: Vec::new(), scene_index: 0, image_tx: None, @@ -299,6 +312,12 @@ impl NativeApplicationHandler for NativeApplication { _ => { let is_copy_png = matches!(cmd, ApplicationCommand::TryCopyAsPNG); let ok = self.app.command(cmd); + if ok { + // Schedule a settle redraw ~50ms after the last interaction + // so the overlay shows "none" when the gesture ends. + // The 240Hz tick decrements the countdown (~12 ticks ≈ 50ms). + self.settle_countdown = 12; + } if ok && is_copy_png { use std::io::Write; @@ -341,6 +360,17 @@ impl NativeApplicationHandler for NativeApplication { } } self.app.tick_with_current_time(); + + // Settle frame: after the last interaction, request one more + // redraw so the overlay shows "none" and the renderer + // can capture a clean pan-image-cache snapshot. + if self.settle_countdown > 0 { + self.settle_countdown -= 1; + if self.settle_countdown == 0 { + self.app.renderer_mut().queue_unstable(); + self.window.request_redraw(); + } + } } HostEvent::RedrawRequest => self.window.request_redraw(), HostEvent::FontLoaded(_f) => { diff --git a/docs/wg/feat-2d/optimization.md b/docs/wg/feat-2d/optimization.md index b2d60f518e..c8511dadc1 100644 --- a/docs/wg/feat-2d/optimization.md +++ b/docs/wg/feat-2d/optimization.md @@ -165,46 +165,47 @@ Related: per-paint alpha changes the blend input, producing different compositing results than whole-node alpha. - ### When per-paint alpha is technically wrong but acceptable + ### Previously: heuristic-based tolerance (superseded) - - **Fills + stroke at high opacity (>0.8)** — the double-blend at - the stroke/fill boundary is `opacity^2` vs `opacity`. At 0.9, - that's 0.81 vs 0.9 — a 10% difference in a thin band. Typically - imperceptible. - - **Fills + stroke with thin stroke (1-2px)** — the overlap band - is sub-pixel to 2px. Hard to see at any opacity. + Before the non-overlapping fill path optimization, a heuristic allowed + per-paint alpha for thin strokes or high opacity. This has been + **replaced** by the exact PathOp::Difference approach, which eliminates + the overlap entirely and produces pixel-correct results at all opacities + and stroke widths. See `docs/wg/feat-2d/stroke-fill-opacity.md`. ### Decision rule for implementation ```text can_use_per_paint_alpha(node): - if node has noise effects → NO (always wrong) - if node has fills AND strokes → MAYBE (see below) - if node has only fills → YES - if node has only strokes → YES - - fill+stroke heuristic: - if stroke_width <= 2.0 → YES (overlap band too thin to see) - if opacity >= 0.85 → YES (double-blend delta < 13%) - otherwise → NO (use save_layer) + if node has noise effects → NO (always wrong) + if node has expensive effects → NO (shadows/blur need isolation) + if non-Normal blend mode → NO (need blend isolation) + if node has only fills → YES (one draw call, no overlap) + if node has only strokes → YES (one draw call, no overlap) + if node has fill + stroke: + if stroke_align == Outside → YES (no geometric overlap) + if non_overlapping_fill_path → YES (overlap eliminated by PathOp) + otherwise → NO (PathOp failed, use save_layer) ``` + See `docs/wg/feat-2d/stroke-fill-opacity.md` for the full spec. + ### Current state in codebase - `with_opacity()` in `painter.rs:202` uses **unbounded** - `save_layer_alpha(None, ...)` for ALL nodes with `opacity < 1.0`. - The per-paint alpha optimization is **not implemented**. Even when - `save_layer` is needed, the missing bounds make it worse (allocates - a full-canvas offscreen instead of a tight rect). + **Implemented.** Per-paint alpha is used for all qualifying nodes: + fills-only, strokes-only, and fill+stroke with non-overlapping fill + paths. `with_opacity()` now passes tight local bounds when + `save_layer_alpha` is still required (effects needing opacity + isolation). See `docs/wg/feat-2d/stroke-fill-opacity.md`. ### Implementation priority - 1. **Fills-only or strokes-only nodes** — safe, no heuristic needed, - large speedup. Majority of typical document nodes. - 2. **Pass bounds to remaining `save_layer` calls** — even when - `save_layer` is still needed, bounded is cheaper than unbounded. - 3. **Fill+stroke with heuristic** — optional, requires threshold - tuning, smaller win (most of the cost is already in tier 1). + 1. ~~**Fills-only or strokes-only nodes**~~ — **done**. Safe, no heuristic + needed, large speedup. Majority of typical document nodes. + 2. ~~**Pass bounds to remaining `save_layer` calls**~~ — **done**. + `with_opacity()` now computes `shape.rect ∪ stroke_path.bounds()`. + 3. ~~**Fill+stroke with heuristic**~~ — **done**. Replaced by + non-overlapping fill path (PathOp::Difference). See stroke-fill-opacity.md. 7. **Per-Node Image Cache (for Expensive Effect Nodes)** @@ -257,6 +258,82 @@ Related: Benchmark source: `crates/grida-canvas/examples/skia_bench/skia_bench_atlas.rs` +7c. **Compositor Early-Exit for Non-Promotable Nodes** + + The compositor update loop iterates over all visible nodes each frame to + decide which nodes should be promoted to cached GPU textures. For nodes + without expensive effects (simple fill/stroke), this loop performs + unnecessary HashMap lookups (`compositor.peek`, `geometry.get_render_bounds`) + and `should_promote` calls — only to conclude the node is not promotable. + + **The optimization:** check `has_promotable_effects()` (a cheap struct field + check on the LayerEffects fields) before any HashMap lookups. Nodes without + shadows, blur, noise, or glass skip the entire compositor evaluation. + + **Measured impact (Apple M2 Pro, GPU benchmark):** + + | Scene | Compositor before | Compositor after | Delta | + | ------------------------------ | ----------------- | ---------------- | ------- | + | flat grid (10K rects, pan) | 941 µs | 134 µs | -85.8% | + | stroke rect grid (2K, pan) | 122 µs | 18 µs | -85.2% | + | opacity fill (5K, pan) | 346 µs | 51 µs | -85.3% | + | opacity fill+stroke (5K, pan) | 428 µs | 74 µs | -82.7% | + | shadow grid (2K promoted, pan) | 38 µs | 38 µs | 0% | + + Total frame time improvement: + + | Scene | Pan avg before | Pan avg after | Delta | + | ---------------------------- | -------------- | ------------- | ------- | + | flat grid (10K rects) | 7310 µs | 6084 µs | -16.8% | + | opacity fill (5K) | 3320 µs | 2956 µs | -11.0% | + | opacity fill+stroke (5K) | 6214 µs | 5767 µs | -7.2% | + | shadow grid (2K promoted) | 1233 µs | 1224 µs | 0% | + + Scenes with promoted nodes (shadow grid) are unaffected — all their visible + nodes have effects and still go through the full compositor path. + +7d. **Pre-Filtered Compositor Indices (Eliminate Redundant R-Tree Query)** + + The compositor update previously performed its own R-tree spatial query + each frame to find visible nodes, duplicating the same query already done + by the frame plan builder. Additionally, it iterated ALL visible nodes + just to check `has_promotable_effects()` — which returns false for the + vast majority of nodes in typical scenes. + + **The optimization:** the frame plan now pre-filters visible indices to + only those with promotable effects (`compositor_indices`) during its + existing iteration pass. The compositor receives this pre-filtered slice, + eliminating both the redundant R-tree query and the per-node promotability + check. + + For scenes without effects (the common case), `compositor_indices` is + empty and the compositor loop body never executes — zero work. + + **Measured impact (Apple M2 Pro, GPU benchmark):** + + | Scene | Compositor before | Compositor after | Delta | + | ------------------------------ | ----------------- | ---------------- | ------- | + | flat grid (10K rects, pan) | 134 µs | 0 µs | -100% | + | stroke rect grid (2K, pan) | 18 µs | 0 µs | -100% | + | opacity fill (5K, pan) | 51 µs | 0 µs | -100% | + | shadow grid (2K promoted, pan) | 34 µs | 33 µs | -3% | + + **Criterion (CPU raster, statistically rigorous):** + + | Scene | Change | p-value | + | -------------------------------------- | --------- | ------- | + | simple_baseline/pan | -2.18% | < 0.01 | + | simple_baseline/zoom | -1.85% | < 0.01 | + | heavy_compositing/pan | -5.41% | < 0.01 | + | heavy_compositing/zoom | -4.63% | < 0.01 | + | heavy_compositing/pinch_zoom | -4.85% | < 0.01 | + | heavy_compositing/pan_after_zoom | -4.79% | < 0.01 | + | heavy_compositing/rapid_zoom_steps | -4.94% | < 0.01 | + + Also includes: `RefCell` → `Cell` for the picture cache + hit counter (eliminates runtime borrow checking overhead in the hot draw + loop), and removal of a redundant `canvas.clear(TRANSPARENT)` call. + 8. **Dirty & Re-Cache Strategy** - Nodes marked dirty trigger re-recording of their `SkPicture`. - Render surfaces containing dirty children are re-composited. @@ -307,6 +384,109 @@ Related: - Reuse transforms and paints. - Precompute common values like DPI × Zoom × ViewMatrix. +11b. **Specialized Primitive Draw Calls (Avoid Intermediate Path Creation)** + + `PainterShape` discriminates between rect, rrect, oval, and path. Instead + of always calling `shape.to_path()` followed by `canvas.draw_path()`, use + Skia's specialized draw calls (`draw_rect`, `draw_rrect`, `draw_oval`) + that bypass path construction and use optimized GPU pipelines. + + Similarly, clipping uses `clip_rect`/`clip_rrect` instead of converting to + a path and calling `clip_path`. + + The `draw_on_canvas()` and `clip_on_canvas()` methods on `PainterShape` + dispatch to the optimal Skia primitive based on shape type. + + **Measured impact (Apple M2 Pro, GPU benchmark):** + + | Scene | Before | After | Delta | + | ---------------------------- | ----------- | ----------- | ------ | + | flat grid (10K rects, pan) | 11802 µs | 10717 µs | -9.2% | + | stroke rect grid (2K, pan) | 4015 µs | 3654 µs | -9.0% | + | opacity fill (5K, pan) | 13910 µs | 13073 µs | -6.0% | + + **Criterion (CPU raster, statistically rigorous):** + + | Scene | Change | p-value | + | ---------------------------- | --------- | ------- | + | simple_baseline/pan | -10.3% | < 0.01 | + | simple_baseline/pan_zoomed_in| -20.7% | < 0.01 | + | heavy_compositing/pan | -11.6% | < 0.01 | + + The improvement is purely CPU-side: eliminated `Path::rect()`/`Path::rrect()` + allocation on every fill draw call. At 10000 visible nodes, this saves ~1ms + of CPU time per frame. + + Applied to: `draw_fills`, `draw_fills_with_opacity`, `draw_drop_shadow`, + `draw_inner_shadow`, `render_noise_effect`, `with_clip`, `draw_backdrop_blur`, + `draw_glass_effect`. + +11c. **Direct Color Paint for Single Solid Fills** + + `sk_paint_stack` creates a `SkColorShader` and attaches it to the paint + even for the most common case: a single solid fill. The GPU backend + dispatches a shader program for any paint with a shader, even a trivial + color shader. Setting the color directly on the paint via + `paint.set_color()` gives Skia a simpler GPU code path. + + The fast path fires when `paints.len() == 1` and the paint is + `Paint::Solid`. All other cases (gradients, images, multi-paint stacks) + fall through to the existing shader-blending path. + + Applied to both `sk_paint_stack` and `sk_paint_stack_without_images`. + + **Measured impact (Apple M2 Pro, GPU benchmark):** + + | Scene | Before | After | Delta | + | ---------------------------- | ----------- | ----------- | ------- | + | flat grid (10K rects, pan) | 10885 µs | 9223 µs | -15.3% | + | opacity fill (5K, pan) | 13906 µs | 4296 µs | -69.1% | + | opacity fill+stroke (5K) | 16416 µs | 7462 µs | -54.6% | + + The opacity scene improvements are amplified by the combined effect of + this optimization, per-paint-alpha opacity folding (item 6b), and + specialized primitive draw calls (item 11b). For fill-only nodes, + per-paint-alpha eliminates `save_layer` entirely; for fill+stroke + nodes with overlap, the non-overlapping fill path (PathOp::Difference) + achieves the same. Direct color paint further reduces per-draw overhead + by avoiding shader program dispatch for solid colors. + +11d. **Translate-Fold for Pure-Translation Transforms** + + For the most common node type — fills-only with a pure-translation + transform (no rotation, scale, or skew) — the painter folds the + translation directly into the shape coordinates and draws with a single + Skia call. This eliminates `canvas.save()`, `canvas.concat(matrix)`, and + `canvas.restore()`, reducing the recorded SkPicture from 4 commands to 1 + per qualifying node. + + The optimization fires when: + - Transform is pure translation (`[[1,0,tx],[0,1,ty]]`) + - No clip path + - No stroke path (fills only) + - Trivial fast path conditions (opacity=1.0, no effects, Normal blend) + + Also applied to the per-paint-alpha opacity path for fills-only nodes + with opacity < 1.0. + + For rect shapes, coordinates are offset directly. For rrect and oval + shapes, `with_offset()` is used. Path shapes fall back to + `save/translate/draw/restore`. + + **Measured impact (Apple M2 Pro, GPU benchmark):** + + | Scene | Before | After | Delta | + | ---------------------------- | ----------- | ----------- | ------- | + | flat grid (10K rects, pan) | 6882 µs | 5455 µs | -20.7% | + | flat grid (10K rects, draw) | 5538 µs | 4228 µs | -23.7% | + | opacity fill (5K, pan) | 2896 µs | 2529 µs | -12.7% | + | opacity fill (5K, draw) | 2311 µs | 1899 µs | -17.8% | + | stroke rect (2K, pan) | 2115 µs | ~2250 µs | ~0% (noise) | + | shadow grid (2K promoted) | 1196 µs | ~1218 µs | ~0% (noise) | + + Scenes with strokes or effects are unaffected — all their nodes bypass + the translate-fold path and use the existing `save/concat/restore`. + 12. **Tight Bounds for `save_layer` Operations** - First: avoid `save_layer` entirely when possible (see item 6b). - When required: always provide explicit bounds. @@ -318,6 +498,12 @@ Related: - Each `save_layer` has a fixed ~57-60 µs overhead (measured). At scale, this dominates frame time. + **Current status:** `with_opacity()` now passes tight local bounds + (`shape.rect ∪ stroke_path.bounds()`) to `save_layer_alpha`. Previously + it passed `None` (unbounded, full-canvas offscreen). Blend mode isolation + (`with_blendmode`, `with_blendmode_and_opacity`) already used bounded + `save_layer` via `compute_blend_mode_bounds_with_stroke()`. + 13. **Text & Path Caching** - Cache laid-out paragraphs keyed by content hash + font generation. - Cache parsed SVG paths keyed by content hash. @@ -336,20 +522,20 @@ Not all effects can be cached in isolation. The critical distinction: **Self-contained** (safe to cache): -| Effect | Notes | -| ------------------------------------ | ------------------------------------------------------------------------------------------- | -| Fills (solid, gradient, image) | Pure paint operations | -| Strokes (all variants) | Computed from path + stroke params | -| Drop shadows | Extends bounds — cached image must include expansion | -| Inner shadows | Clipped to shape; operates on own content only | -| Noise effects | Blends with fills within same surface | -| Layer blur | `save_layer` with image filter — reads own buffer only | -| Opacity (fills-only or strokes-only) | Per-paint alpha — no `save_layer` needed (item 6b) | -| Opacity (fills + noise) | Requires `save_layer` — noise compositing is wrong without isolation | -| Opacity (fills + stroke) | `save_layer` for correctness; per-paint acceptable if thin stroke or high opacity (item 6b) | -| Opacity (2+ overlapping children) | Requires `save_layer` via render surface (item 6) | -| Clip paths | Restricts visible area | -| Mask groups | Self-contained, cached as a unit | +| Effect | Notes | +| ------------------------------------ | ------------------------------------------------------------------------------------------------------------------- | +| Fills (solid, gradient, image) | Pure paint operations | +| Strokes (all variants) | Computed from path + stroke params | +| Drop shadows | Extends bounds — cached image must include expansion | +| Inner shadows | Clipped to shape; operates on own content only | +| Noise effects | Blends with fills within same surface | +| Layer blur | `save_layer` with image filter — reads own buffer only | +| Opacity (fills-only or strokes-only) | Per-paint alpha — no `save_layer` needed (item 6b) | +| Opacity (fills + noise) | Requires `save_layer` — noise compositing is wrong without isolation | +| Opacity (fills + stroke) | Per-paint alpha via non-overlapping fill path (PathOp::Difference); `save_layer` fallback if PathOp fails (item 6b) | +| Opacity (2+ overlapping children) | Requires `save_layer` via render surface (item 6) | +| Clip paths | Restricts visible area | +| Mask groups | Self-contained, cached as a unit | **Context-dependent** (must draw live): @@ -385,10 +571,12 @@ zoom stays constant. This unlocks optimizations impossible when scale changes. Computed once per frame, threaded through the pipeline so every stage can take the cheapest path. - **Frame cost hierarchy:** `None < ZoomIn < PanOnly < ZoomOut < PanAndZoom`. - Zoom-in is cheaper than pan because no new spatial content enters the - viewport — only pixel density changes. Zoom-out is more expensive than - pan because new content appears in all four directions simultaneously. + **Frame cost hierarchy:** `None < PanOnly < ZoomIn < ZoomOut < PanAndZoom`. + With the pan image cache (item 16b), PanOnly is now the cheapest + non-idle frame — a single GPU texture blit (~20-200 µs regardless of + scene complexity). ZoomIn is next cheapest (cached content is a spatial + superset). ZoomOut is more expensive than zoom-in because new content + appears at all edges. PanAndZoom combines both costs. 16. **Cached Content Reuse on Pan** @@ -399,6 +587,78 @@ zoom stays constant. This unlocks optimizations impossible when scale changes. - Only live (non-cached) nodes need actual draw calls. - No re-rasterization, no geometry recomputation. +16b. **Pan Image Cache (Whole-Frame GPU Texture Blit)** + + The most aggressive pan optimization: capture the fully composited + frame as a GPU texture snapshot (`SkImage`) after the first draw, then + on subsequent pan-only frames, blit this single texture at the camera + offset instead of re-drawing all visible nodes. + + This replaces the entire draw pipeline (CPU iteration over N nodes, + per-node SkPicture replay, GPU rasterization of all draw commands) + with a single `draw_image` call — one texture blit. + + **How it works:** + 1. After the draw phase and mid-flush GPU submit, `surface.image_snapshot()` + captures the composited frame as a GPU-resident `SkImage` (copy-on-write, + near-zero cost). + 2. The view matrix translation at capture time is stored alongside. + 3. On the next pan-only frame, the screen-space offset is computed from + the difference in view matrix translations. + 4. The canvas is cleared (background color), and the cached image is + blitted at the computed offset. A single GPU flush processes the blit. + 5. The draw phase, per-node picture replay, compositor update, and second + GPU flush are all skipped entirely. + + **Cache invalidation:** + - Zoom change (pixel density changes) + - Scene mutation (load_scene, invalidate_cache) + - Stable frame (settle after interaction — full-quality re-draw) + - Offset exceeds threshold (200px — exposed strips too large) + - Config changes (compositor atlas toggle, render policy) + + **Limitations & visual tradeoff:** + - **Exposed strips:** viewport edges show background color instead of + scene content when the camera moves past the cached image boundary. + For typical per-frame deltas (5-20px), the strips are narrow (~1-2% + of viewport). After the gesture settles, the stable frame re-draws + at full quality. This is the same tradeoff browsers make during + scroll (checkerboard/blank tiles filled asynchronously). + - **Cache is not refreshed on the fast path.** During sustained pan, + the cache builds up offset until it exceeds the 200px threshold, + then one full-cost frame re-draws and re-captures. In practice this + means ~4-20 fast frames per 1 slow frame, depending on pan velocity. + - GPU-only: the snapshot is a GPU texture; raster backends fall through + to the normal draw path. + - Zoom frames skip capture to avoid unnecessary copy-on-write overhead + (the cache would be invalidated immediately on the next frame anyway). + + **Measured impact (Apple M2 Pro, GPU benchmark, 100 frames):** + + Note: benchmark numbers below represent **cache-hit frames** — the + cache is primed during warmup, so all 100 measurement frames take the + fast path. Real-world pan mixes cache-hit (~20-200 µs) with periodic + cache-miss frames (same cost as "before"). The effective speedup + during a sustained pan gesture depends on how often the cache refreshes. + + | Scene | Pan before | Pan after (cache hit) | Cache hit cost | + | ---------------------------- | ----------- | --------------------- | -------------- | + | bench-backdrop-blur-grid | 45,471 µs | 24 µs | 1,895x | + | [WWW] Design (10.5K nodes) | 37,808 µs | 55 µs | 687x | + | bench-glass-grid | 29,132 µs | 29 µs | 1,005x | + | Materials (6.5K nodes) | 12,794 µs | 23 µs | 556x | + | bench-flat-grid (69K nodes) | 6,469 µs | 200 µs | 32x | + | bench-shadow-grid (promoted) | 1,534 µs | 90 µs | 17x | + | Icons (4.9K vectors) | 1,763 µs | 30 µs | 59x | + The heaviest GPU-bound scenes (backdrop blur, glass, complex vectors) + saw the largest gains because the cache eliminates the GPU rasterization + that dominated their frame time (`mid_flush`). + + **Chromium parallel:** Chromium's compositor tiles serve a similar role — + existing tiles are translated during scroll without re-rasterization. + Our approach is simpler (one full-frame texture vs. a tile grid) but + achieves the same effect for the common case of small pan deltas. + 17. **Incremental Visible-Set Update** Track the previous frame's visible set. On pan, compute only entering diff --git a/docs/wg/feat-2d/stroke-fill-opacity.md b/docs/wg/feat-2d/stroke-fill-opacity.md new file mode 100644 index 0000000000..2bf33afa15 --- /dev/null +++ b/docs/wg/feat-2d/stroke-fill-opacity.md @@ -0,0 +1,227 @@ +--- +title: "Stroke-Fill Opacity Compositing" +--- + +# Stroke-Fill Opacity Compositing + +How node-level opacity interacts with fill and stroke paint when they +overlap. Defines the correct compositing behavior and the per-paint-alpha +optimization with its validity conditions. + +Related: + +- [Rendering Optimization Strategies](./optimization.md) — item 6b +- Chromium source: `third_party/blink/renderer/core/paint/svg_shape_painter.cc` +- Chromium source: `third_party/blink/renderer/core/paint/svg_object_painter.cc` +- SVG 2 Specification, Chapter 3: Rendering Model + +--- + +## Two Kinds of Opacity + +There are two distinct opacity mechanisms, operating at different levels: + +| Property | Scope | Compositing | Isolation? | +| ---------------------------------------------------- | ------------------------- | --------------------------------------------------------- | ---------- | +| **Node opacity** (`opacity`) | Entire element as a group | `save_layer(opacity)` → draw fill+stroke at 1.0 → restore | **Yes** | +| **Paint opacity** (`fill-opacity`, `stroke-opacity`) | Individual paint only | Baked into `paint.alpha` | **No** | + +This matches the SVG/CSS specification and Chromium's implementation. + +### Node opacity (group isolation) + +When a node has `opacity < 1.0`, the entire element — fill, stroke, +markers, effects — is rendered into an **offscreen surface at full +opacity**, then the surface is composited onto the canvas at the +specified opacity. + +``` +save_layer_alpha(bounds, opacity) ← offscreen surface + draw_fill(color, fill_opacity=1.0) + draw_stroke(color, stroke_opacity=1.0) +restore ← composite at `opacity` +``` + +This ensures fill and stroke compose correctly in the overlap region. +The overlap shows only the topmost paint (stroke), blended once at +`opacity` against the background. No double-blending. + +### Paint opacity (per-paint alpha) + +`fill-opacity` and `stroke-opacity` are multiplied directly into the +paint color's alpha channel. No offscreen surface. Each paint is drawn +independently: + +``` +draw_fill(color * fill_opacity) +draw_stroke(color * stroke_opacity) +``` + +In the overlap region, the fill shows through at `fill_opacity`, then +the stroke composites on top at `stroke_opacity`. This is intentional — +paint-level opacity controls the individual paint's transparency, not +the element as a group. + +--- + +## Chromium Implementation Reference + +Chromium's SVG renderer handles this with two separate code paths: + +### 1. `opacity` → Effect paint property → `SaveLayerAlphaOp` + +When `style.Opacity() != 1.0f`, an **Effect paint property node** is +created in the paint property tree: + +``` +// paint_property_tree_builder.cc:1630 +if (style.Opacity() != 1.0f) + return true; // needs Effect node + +// paint_property_tree_builder.cc:1784 +state.opacity = style.Opacity(); +``` + +During rasterization, this Effect node becomes a `SaveLayerAlphaOp`: + +``` +// paint_chunks_to_cc_layer.cc:697 +save_layer_id = push(effect.Opacity()); +``` + +All fill/stroke draws happen INSIDE this save_layer. + +### 2. `fill-opacity` / `stroke-opacity` → paint color alpha + +In `SVGObjectPainter::PreparePaint()`: + +``` +// svg_object_painter.cc:142 +const float alpha = + apply_to_fill ? style.FillOpacity() : style.StrokeOpacity(); + +// svg_object_painter.cc:176 +flag_color.SetAlpha(flag_color.Alpha() * alpha); +flags.setColor(flag_color.toSkColor4f()); +``` + +No offscreen surface. Alpha is baked directly into the paint. + +### 3. Combined: `opacity=0.5` on element with fill+stroke + +Rendering order: + +1. `SaveLayerAlpha(0.5)` — start offscreen buffer +2. Draw fill (red, at full opacity) +3. Draw stroke (blue, at full opacity) +4. `Restore` — composite offscreen buffer at 50% opacity + +The overlap region shows **only** the stroke color at 50% opacity +against the background. No fill bleed-through. + +--- + +## The Per-Paint-Alpha Optimization + +`save_layer` is the most expensive non-filter Skia GPU operation +(~57-60 µs per call, measured). For nodes with `opacity < 1.0`, we +can sometimes avoid it by folding the opacity into each paint's alpha. + +### When per-paint-alpha is spec-correct (zero visual difference) + +Per-paint-alpha produces identical output to save_layer when the +node's draw calls do NOT overlap on the canvas: + +- **Fills only** (no stroke) — one `draw_path`, zero overlap +- **Strokes only** (no fill) — one `draw_path`, zero overlap +- **Fill + Outside stroke** — stroke geometry is entirely outside the + fill area. Zero geometric overlap. Exact match. + +### When per-paint-alpha is spec-incorrect (visible artifact) + +When fill and stroke geometrically overlap, applying opacity +independently produces **double-blending** in the overlap region: + +- **Fill + Inside stroke** — stroke fully overlaps the fill. The + overlap region (entire stroke band) shows fill bleed-through. + Max channel difference: ~64/255 at opacity=0.5. +- **Fill + Center stroke** — inner half of stroke overlaps the fill. + Same artifact in the overlap band, but narrower. + +The artifact magnitude scales with `stroke_width * (1 - opacity)`. + +### Non-overlapping fill path optimization + +Instead of falling back to `save_layer_alpha` for Inside/Center strokes, +we eliminate the overlap at layer construction time by computing: + +``` +non_overlapping_fill = fill_path.op(stroke_path, PathOp::Difference) +``` + +This subtracts the stroke region from the fill path. Drawing the +non-overlapping fill + original stroke with per-paint-alpha produces +output identical to `save_layer_alpha` — zero GPU surfaces, zero +artifacts. + +`PathOp::Difference` is a CPU-side Skia boolean path operation (~5-15 µs +for simple shapes, ~20-40 µs for complex paths). This is consistently +cheaper than `save_layer_alpha` (~57-60 µs GPU surface allocation) and +the cost is paid once at layer construction, amortized across frames. + +### Decision rule + +``` +can_use_per_paint_alpha(node): + if node has noise effects → NO (always wrong, see optimization.md) + if node has only fills → YES (one draw call, no overlap) + if node has only strokes → YES (one draw call, no overlap) + if node has fill + stroke: + if stroke_align == Outside → YES (no geometric overlap) + if stroke_align == Inside: + if non_overlapping_fill → YES (overlap eliminated by PathOp) + else → NO (PathOp failed, use save_layer) + if stroke_align == Center: + if non_overlapping_fill → YES (overlap eliminated by PathOp) + else → NO (PathOp failed, use save_layer) +``` + +When `save_layer` is required (PathOp fallback), always provide **tight +bounds** that include the stroke expansion (not just `shape.rect`). + +### Save-layer bounds computation (fallback path) + +The `save_layer` bounds must encompass all drawing that happens inside +it. For a node with strokes, this includes the stroke path which may +extend beyond `shape.rect`: + +``` +bounds = shape.rect +if has_stroke_path: + bounds = bounds.union(stroke_path.bounds()) +// also expand for shadow/blur effects +``` + +Without correct bounds, `save_layer` clips content outside +`shape.rect`, cutting off Outside/Center stroke geometry. + +--- + +## Implementation Status + +| Stroke Align | Overlap? | Strategy | Pixel-correct? | +| ------------ | -------- | --------------------------------------------- | -------------- | +| Outside | None | Per-paint-alpha (fast) | ✅ Exact match | +| Inside | Full | Non-overlapping fill + per-paint-alpha (fast) | ✅ Exact match | +| Center | Partial | Non-overlapping fill + per-paint-alpha (fast) | ✅ Exact match | +| Fills only | N/A | Per-paint-alpha (fast) | ✅ Exact match | +| Strokes only | N/A | Per-paint-alpha (fast) | ✅ Exact match | + +The `stroke_overlaps_fill` flag on `PainterPictureShapeLayer` controls +overlap detection. The `non_overlapping_fill_path` field stores the +pre-computed fill path with stroke region subtracted. Both are set at +layer construction time. + +If `PathOp::Difference` fails (degenerate geometry), the painter falls +back to `save_layer_alpha` with bounds expanded via +`compute_blend_mode_bounds_with_stroke()`. diff --git a/fixtures/test-grida/bench.grida b/fixtures/test-grida/bench.grida index 3c4cbaff57..1b7b35375c 100644 Binary files a/fixtures/test-grida/bench.grida and b/fixtures/test-grida/bench.grida differ