Bug: unsafe-eval UBO sync polyfill has wrong arity — custom UniformGroup UBOs upload all-zero under strict CSP (blank WebGPU rendering)
PixiJS version
8.19.0 (also confirmed by inspection at current dev HEAD)
Link to minimal reproduction
Runnable: https://gist.githack.com/gtran-07/d97fbb8803804438d5873870ce3e85f2/raw/index.html
(source: https://gist.github.com/gtran-07/d97fbb8803804438d5873870ce3e85f2 — a single self-contained index.html, pixi 8.19.0 from jsdelivr)
Steps to reproduce
- Open the reproduction link. It imports
lib/unsafe-eval/ubo/generateUboSyncPolyfill.mjs(the exact modulepixi.js/unsafe-evalinstalls as_generateUboSync) and builds a sync function for a minimal uniform set (alpha: f32,count: i32). - The page then calls the generated sync function exactly as
UboSystem.syncUniformGroupcalls it — with 4 arguments(uniforms, data, dataInt32, offset)— and prints the resulting buffer. - Observe the two red FAIL lines:
- the 4-arg call leaves the buffer all-zero (
data[0] = 0, expected1); - the i32 uniform is written through the Float32 view (
dataInt32[1] = 1088421888— the IEEE-754 bits of7.0— expected7).
- the 4-arg call leaves the buffer all-zero (
- A control call using the polyfill's own 3-arg shape
(uniforms, data, offset)writes correctly, showing the polyfill internals are fine — only the signature disagrees with the caller.
Real-world trigger: any app under a strict CSP (script-src 'self', no 'unsafe-eval') — where pixi.js/unsafe-eval is mandatory — using a custom UniformGroup on the WebGPU renderer. Every custom UBO uploads all-zero, so custom-shader content renders blank (we hit this in production: our entire custom WGSL layer disappeared on the WebGPU backend, while native Graphics in the same frame rendered fine).
What is expected?
The polyfill-generated sync function should have the same signature as the eval build — new Function("uv", "data", "dataInt32", "offset", …) — so the numeric offset is applied and uniform values reach the buffer. Custom UniformGroups should upload identically with and without unsafe-eval.
What is actually happening?
generateUboSyncPolyfill (both …WGSL and …STD40 variants) returns a 3-parameter closure (uniforms, data, offset):
https://github.com/pixijs/pixijs/blob/dev/src/unsafe-eval/ubo/generateUboSyncPolyfill.ts
…but the caller UboSystem.syncUniformGroup invokes it with 4 arguments — syncFunction(uniformGroup.uniforms, data, dataInt32, offset) (and the declared type of syncFunction is 4-arg too):
https://github.com/pixijs/pixijs/blob/dev/src/rendering/renderers/shared/shader/UboSystem.ts
So the polyfill's offset parameter receives the dataInt32 typed array and the real numeric offset is silently dropped. Every write then computes data[dataInt32 + elementOffset] — the typed array coerces to a garbage string key — so writes land as expando properties and the underlying buffer stays all-zero.
Bonus latent bug found while root-causing: in src/unsafe-eval/ubo/uboSyncFunctions.ts, the i32 single-value setter writes through data (the Float32 view) instead of dataInt32:
i32: (_name, data, offset, _uv, v) => { data[offset] = v; },
The eval build writes i32 values through dataInt32, so under the polyfill a shader-side i32 reads the float bit pattern instead of the integer. (Masked today because the signature bug zeroes everything first.) A fix should extend UboUploadFunction to receive dataInt32 so integer types can write the correct view.
System Info
System:
OS: Windows 11 10.0.26200
CPU: (20) x64 13th Gen Intel(R) Core(TM) i7-13700H
Memory: 5.72 GB / 31.69 GB
Browsers:
Chrome: 150.0.7871.115
Edge: Chromium (150.0.4078.48)
(Bug is platform-independent — a pure JS arity mismatch; we hit it in Electron/Chromium on the WebGPU backend.)
Any additional comments?
Suggested fix: have generateUboSyncPolyfill return (uniforms, data, dataInt32, offset) => … and thread dataInt32 through the per-uniform upload functions; fix the i32 setter (and any other integer types) to write through dataInt32.
Workaround we ship today: a side-effect module imported right after pixi.js/unsafe-eval that wraps GpuUboSystem._generateUboSync / GlUboSystem._generateUboSync and, when the produced sync has arity 3, adapts the 4-arg call by forwarding the real numeric offset into the third slot (arity-guarded so it no-ops once the signature is fixed upstream).
Source: pixijs/pixijs