Description
The prebuilt examples/jsm/libs/basis/basis_transcoder.js is an embind build with dynamic execution enabled: during embind registration it compiles its invoker functions with the Function constructor. (Grepping the file for new Function
finds nothing as the call is indirect, newFunc(Function, args) inside craftInvokerFunction.)
Under a CSP that allows WebAssembly but not JS eval, e.g. script-src 'self' 'wasm-unsafe-eval', every transcoder worker throws:
EvalError: Evaluating a string as JavaScript violates the following Content Security Policy directive because 'unsafe-eval' is not an allowed source of script: "script-src 'self' 'wasm-unsafe-eval'"
once per worker (4× with the default workerLimit), and every KTX2 load fails. The only workaround today is allowing full 'unsafe-eval'. KTX2Loader itself is fine under strict CSP (its worker is covered by worker-src blob:), and the bundled DRACO decoder is fine too; only the Basis transcoder is affected.
Rebuilding the transcoder with -sDYNAMIC_EXECUTION=0 makes embind use plain closures instead, with the same API. I rebuilt from basis_universal v1_50_0_2 with the upstream webgl/transcoder/CMakeLists.txt flags plus:
-sDYNAMIC_EXECUTION=0 - the fix; no eval-class constructs remain in the output;
-sEXPORTED_RUNTIME_METHODS=HEAP8 - needed with current Emscripten (tested 6.0.8): basis_wrappers.cpp reads Module.HEAP8 via val::module_property, which newer Emscripten no longer exports by default. The current bundled build still carries an explicit Module["HEAP8"]=HEAP8 from its older toolchain.
-sINCOMING_MODULE_JS_API=<default list>,wasmBinary: also needed with current Emscripten: the default list dropped wasmBinary, so without this the build ignores the wasmBinary that KTX2Loader passes into the module and instead tries to fetch basis_transcoder.wasm relative to its own URL — a blob: URL inside KTX2Loader's worker, so every worker dies with "Failed to parse URL from basis_transcoder.wasm". (Note the setting replaces the default list, so the defaults — onRuntimeInitialized included — must be repeated alongside wasmBinary.) A Node smoke test does not catch this: Node's fallback finds the .wasm on disk next to the .js. Test in a worker, or with the .js isolated from the .wasm.
Tested against KTX2Loader's call sequence (initializeBasis → new KTX2File → startTranscoding → transcodeImage) on ETC1S, UASTC, zstd-supercompressed and HDR .ktx2 files: the output is byte-identical to the current bundled build's, at every mip level. Verified in a browser behind script-src 'self' 'wasm-unsafe-eval', loading the transcoder through KTX2Loader's actual blob-worker mechanism; we run this build in production.
Two notes for a drop-in replacement:
- The current build exports the full
HEAP* family plus some embind helpers on Module; with current Emscripten only what's listed in -sEXPORTED_RUNTIME_METHODS is exported. KTX2Loader needs none of them (only the wasm-side wrapper reads HEAP8), but anyone using the file directly might — exporting the old set keeps it fully compatible.
- Performance (Node 24, x64, median of 40 alternating runs, all mips of a 1500×939 texture): ETC1S transcoding is identical; UASTC+zstd is ~8% slower with my build. The embind call overhead itself is lower with
DYNAMIC_EXECUTION=0 in my measurements, so the UASTC delta looks like wasm codegen from the newer toolchain, not the flag — worth rebenchmarking in whatever pipeline normally produces these files.
Reproduction steps
- Serve a page using
KTX2Loader with the response header
Content-Security-Policy: script-src 'self' 'wasm-unsafe-eval'; worker-src 'self' blob:
- Load any
.ktx2 texture.
- Each worker throws
EvalError; the load fails.
Code
const loader = new KTX2Loader().setTranscoderPath("jsm/libs/basis/").detectSupport(renderer);
loader.load("textures/ktx2/2d_etc1s.ktx2", console.log, undefined, console.error);
Live example
Not applicable as this needs a CSP response header, which jsfiddle/codepen can't set. Reproducible locally with any static server that sends the header above.
Screenshots
No response
Version
r185 & dev
Device
Desktop
Browser
Chrome
OS
Windows
Description
The prebuilt
examples/jsm/libs/basis/basis_transcoder.jsis an embind build with dynamic execution enabled: during embind registration it compiles its invoker functions with theFunctionconstructor. (Grepping the file fornew Functionfinds nothing as the call is indirect,
newFunc(Function, args)insidecraftInvokerFunction.)Under a CSP that allows WebAssembly but not JS eval, e.g.
script-src 'self' 'wasm-unsafe-eval', every transcoder worker throws:once per worker (4× with the default
workerLimit), and every KTX2 load fails. The only workaround today is allowing full'unsafe-eval'.KTX2Loaderitself is fine under strict CSP (its worker is covered byworker-src blob:), and the bundled DRACO decoder is fine too; only the Basis transcoder is affected.Rebuilding the transcoder with
-sDYNAMIC_EXECUTION=0makes embind use plain closures instead, with the same API. I rebuilt frombasis_universalv1_50_0_2with the upstreamwebgl/transcoder/CMakeLists.txtflags plus:-sDYNAMIC_EXECUTION=0- the fix; no eval-class constructs remain in the output;-sEXPORTED_RUNTIME_METHODS=HEAP8- needed with current Emscripten (tested 6.0.8):basis_wrappers.cppreadsModule.HEAP8viaval::module_property, which newer Emscripten no longer exports by default. The current bundled build still carries an explicitModule["HEAP8"]=HEAP8from its older toolchain.-sINCOMING_MODULE_JS_API=<default list>,wasmBinary: also needed with current Emscripten: the default list droppedwasmBinary, so without this the build ignores thewasmBinarythatKTX2Loaderpasses into the module and instead tries to fetchbasis_transcoder.wasmrelative to its own URL — ablob:URL inside KTX2Loader's worker, so every worker dies with "Failed to parse URL from basis_transcoder.wasm". (Note the setting replaces the default list, so the defaults —onRuntimeInitializedincluded — must be repeated alongsidewasmBinary.) A Node smoke test does not catch this: Node's fallback finds the.wasmon disk next to the.js. Test in a worker, or with the.jsisolated from the.wasm.Tested against
KTX2Loader's call sequence (initializeBasis→new KTX2File→startTranscoding→transcodeImage) on ETC1S, UASTC, zstd-supercompressed and HDR.ktx2files: the output is byte-identical to the current bundled build's, at every mip level. Verified in a browser behind script-src 'self' 'wasm-unsafe-eval', loading the transcoder through KTX2Loader's actual blob-worker mechanism; we run this build in production.Two notes for a drop-in replacement:
HEAP*family plus some embind helpers onModule; with current Emscripten only what's listed in-sEXPORTED_RUNTIME_METHODSis exported.KTX2Loaderneeds none of them (only the wasm-side wrapper readsHEAP8), but anyone using the file directly might — exporting the old set keeps it fully compatible.DYNAMIC_EXECUTION=0in my measurements, so the UASTC delta looks like wasm codegen from the newer toolchain, not the flag — worth rebenchmarking in whatever pipeline normally produces these files.Reproduction steps
KTX2Loaderwith the response headerContent-Security-Policy: script-src 'self' 'wasm-unsafe-eval'; worker-src 'self' blob:.ktx2texture.EvalError; the load fails.Code
Live example
Not applicable as this needs a CSP response header, which jsfiddle/codepen can't set. Reproducible locally with any static server that sends the header above.
Screenshots
No response
Version
r185 & dev
Device
Desktop
Browser
Chrome
OS
Windows