#12117·pixijs

Bug: unsafe-eval UBO sync polyfill has wrong arity — custom UniformGroup UBOs upload all-zero under strict CSP (blank WebGPU rendering)

Author: gtran-07Created Jul 10, 2026Updated Jul 10, 2026
Labelsenv: webgpupriority: highstatus: needs triagescope: 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

  1. Open the reproduction link. It imports lib/unsafe-eval/ubo/generateUboSyncPolyfill.mjs (the exact module pixi.js/unsafe-eval installs as _generateUboSync) and builds a sync function for a minimal uniform set (alpha: f32, count: i32).
  2. The page then calls the generated sync function exactly as UboSystem.syncUniformGroup calls it — with 4 arguments (uniforms, data, dataInt32, offset) — and prints the resulting buffer.
  3. Observe the two red FAIL lines:
    • the 4-arg call leaves the buffer all-zero (data[0] = 0, expected 1);
    • the i32 uniform is written through the Float32 view (dataInt32[1] = 1088421888 — the IEEE-754 bits of 7.0 — expected 7).
  4. 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 argumentssyncFunction(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).