Add File/Blob/FileReader polyfill for Playground (re-enables 19 GLTF tests)#1706
Add File/Blob/FileReader polyfill for Playground (re-enables 19 GLTF tests)#1706bkaradzic-microsoft wants to merge 6 commits into
Conversation
BabylonNative's native JsRuntimeHost ships a Blob C++ ObjectWrap but no File constructor or FileReader. Several Babylon.js serializer round-trip tests (GLTF/OBJ export then re-load) use new File([blob], 'scene.glb') and rely on the loader's FilesInputStore path that internally invokes FileReader.readAsArrayBuffer. Without these the tests fail with ReferenceError: 'File' is not defined / 'FileReader' is not defined. Adds a self-detecting JS polyfill that: - no-ops on runtimes that already expose File/FileReader (V8, JSC may) - no-ops when the native Blob polyfill is missing - decorates a native Blob with name/lastModified to produce a File (deliberately without setPrototypeOf - pivoting the napi ObjectWrap prototype strips its bound instance methods on Chakra) - implements FileReader on top of Blob.arrayBuffer() with readAsArrayBuffer, readAsText, readAsDataURL, readAsBinaryString and the standard event surface (onload/onerror/onloadend, addEventListener/removeEventListener) Hooked into Playground via SCRIPTS in Apps/Playground/CMakeLists.txt and loaded from AppContext.cpp before ammo.js / babylon.max.js. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Re-enables tests at indices 208-216, 219-226, 275, 276 in Apps/Playground/Scripts/config.json (GLTF Serializer round-trip variants and OBJ Stanford Bunny round-trip in both handednesses), which were previously excluded with "Pixel comparison fails (more than 20% pixels differ)" or "File API not available" reasons. The actual root cause was the missing File/FileReader globals, now addressed in the previous commit. All 19 tests verified passing via headless Playground runner without --include-excluded. Updates reasons for tests that still fail with the polyfill present: - idx 217 (Draco mesh compression): Draco loader uses 'utf8' encoding name which Babylon Native's TextDecoder polyfill rejects (only 'utf-8' OK) - idx 277, 278 (glTF to OBJ round trip): hangs on Win32 Chakra D3D11 Remaining still-excluded entries (218, 227-230, 420, 421) keep their existing pixel-diff reasons; they are unrelated to the File polyfill. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Linux JSC and GCC CI runners (OpenGL backend) hit a 3.4% pixel diff (8097/240000 px) on this test that does not appear on the D3D11 backend. The test was re-enabled by the file-api branch because it passes on Win32 D3D11, but the OpenGL backend has a separate rendering issue that is out of scope for the File API polyfill work. Mark the test excludedGraphicsApis=[OpenGL] so it still runs on D3D11/D3D12/Metal/ Vulkan and stays skipped on OpenGL. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Same pattern as KHR gpu instancing: the Camera serializer roundtrip test diverges on the OpenGL backend (Linux JSC and GCC CI runners, ~21% pixel diff with the rendered clear color showing through where the geometry should be) but passes on D3D11. Exclude on OpenGL while we figure out the backend-specific issue. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CI continues to surface the same OpenGL-only rendering issue on every test using playgroundId #O0M0J9#25 (the Camera serializer roundtrip): - left-handed (already excluded last round) - right-handed (failed this round, ~50K px diff) - left-handed, round trip twice - right-handed, round trip twice All share the same scene and codepath. Mark the remaining 3 with excludedGraphicsApis=[OpenGL] preemptively so we stop iterating one test at a time on this same failure mode. Tests still run on D3D11/ D3D12/Metal/Vulkan. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adds a JS-side File / FileReader polyfill to Babylon Native’s Playground so GLTF serializer/loader round-trip tests can run under the BN JS runtime again.
Changes:
- Introduces
file_polyfill.jsimplementingFile(as a decorated nativeBlob) and a Promise-backedFileReader. - Loads the polyfill early in Playground startup and includes it in the CMake scripts bundle.
- Re-enables previously excluded serializer-related tests and refines exclusions/reasons for known backend issues.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 7 comments.
| File | Description |
|---|---|
| Apps/Playground/Shared/AppContext.cpp | Loads the new File API polyfill before other scripts. |
| Apps/Playground/Scripts/file_polyfill.js | Adds minimal File + FileReader implementations on top of the native Blob. |
| Apps/Playground/Scripts/config.json | Re-enables many tests; adds targeted OpenGL exclusions and updates a few “reason” strings. |
| Apps/Playground/CMakeLists.txt | Ensures the new polyfill script is packaged with existing Playground scripts. |
| function startRead(reader, blob, mode) { | ||
| if (reader.readyState === LOADING) { | ||
| throw new Error("FileReader: read already in progress"); | ||
| } | ||
| reader.readyState = LOADING; | ||
| reader.result = null; | ||
| reader.error = null; | ||
|
|
||
| dispatch(reader, "loadstart"); |
There was a problem hiding this comment.
Addressed in 3598579. FileReader now uses a monotonic _readId token: startRead() bumps it and captures the value in a closure, the resolve/reject continuations early-return if reader._readId has moved on (or readyState is no longer LOADING), and abort() bumps the token explicitly so a late-resolving arrayBuffer() promise can no longer clobber state or dispatch a phantom load after a user-initiated abort.
| Promise.resolve(bufPromise).then(function (buf) { | ||
| if (mode === "arraybuffer") { | ||
| reader.result = buf; | ||
| } else if (mode === "text") { |
There was a problem hiding this comment.
Addressed in 3598579. FileReader now uses a monotonic _readId token: startRead() bumps it and captures the value in a closure, the resolve/reject continuations early-return if reader._readId has moved on (or readyState is no longer LOADING), and abort() bumps the token explicitly so a late-resolving arrayBuffer() promise can no longer clobber state or dispatch a phantom load after a user-initiated abort.
| reader.readyState = DONE; | ||
| dispatch(reader, "load"); | ||
| dispatch(reader, "loadend"); | ||
| }, function (err) { | ||
| reader.error = err || new Error("FileReader: unknown error"); | ||
| reader.readyState = DONE; | ||
| dispatch(reader, "error"); | ||
| dispatch(reader, "loadend"); | ||
| }); | ||
| } |
There was a problem hiding this comment.
Addressed in 3598579. FileReader now uses a monotonic _readId token: startRead() bumps it and captures the value in a closure, the resolve/reject continuations early-return if reader._readId has moved on (or readyState is no longer LOADING), and abort() bumps the token explicitly so a late-resolving arrayBuffer() promise can no longer clobber state or dispatch a phantom load after a user-initiated abort.
| FileReader.prototype.abort = function () { | ||
| if (this.readyState !== LOADING) { | ||
| return; | ||
| } | ||
| this.readyState = DONE; | ||
| this.result = null; | ||
| this.error = new Error("FileReader aborted"); | ||
| dispatch(this, "abort"); | ||
| dispatch(this, "loadend"); | ||
| }; |
There was a problem hiding this comment.
Addressed in 3598579. FileReader now uses a monotonic _readId token: startRead() bumps it and captures the value in a closure, the resolve/reject continuations early-return if reader._readId has moved on (or readyState is no longer LOADING), and abort() bumps the token explicitly so a late-resolving arrayBuffer() promise can no longer clobber state or dispatch a phantom load after a user-initiated abort.
| } | ||
| } | ||
| }; | ||
| FileReader.prototype.dispatchEvent = function () { |
There was a problem hiding this comment.
Addressed in 3598579. dispatchEvent(event) now routes through the existing internal dispatch() helper based on event.type, so on* handlers and addEventListener listeners actually fire. The caller's event payload is intentionally not surfaced (the helper constructs its own payload from reader.result), which is the same best-effort contract used by the internal callers - sufficient for duck-typed FileReader consumers, and strictly better than the previous silent no-op.
| var bytes = new Uint8Array(buf); | ||
| var s = ""; | ||
| for (var i = 0; i < bytes.length; i++) { | ||
| s += String.fromCharCode(bytes[i]); | ||
| } | ||
| // Best-effort UTF-8 decode if TextDecoder is available. | ||
| if (typeof TextDecoder !== "undefined") { | ||
| try { | ||
| s = new TextDecoder("utf-8").decode(buf); | ||
| } catch (e) { /* fall back to latin-1 string above */ } | ||
| } |
There was a problem hiding this comment.
Addressed in 3598579. readAsText now tries TextDecoder first (single-shot, native-speed) and only falls back to a manual byte-to-char path when TextDecoder is unavailable or throws. The fallback uses chunked String.fromCharCode.apply + Array.join (same chunking pattern as the dataurl path), avoiding the O(n^2) s += c loop entirely.
| "playgroundId": "#F8BF8N#3", | ||
| "excludeFromAutomaticTesting": true, | ||
| "reason": "Pixel comparison fails (more than 20% pixels differ)", | ||
| "reason": "Draco loader passes 'utf8' to TextDecoder which only accepts 'utf-8'", |
There was a problem hiding this comment.
Addressed in 3598579. Reworded to scope the limitation correctly: "Draco loader passes 'utf8' label; BN TextDecoder only accepts 'utf-8' (WHATWG label normalization not implemented)". The issue is BN-specific (our TextDecoder doesn't normalize labels), not Web TextDecoder in general.
FileReader changes: - abort() now invalidates the in-flight read via a monotonic `_readId` token; the .then continuation in startRead() captures the token and early-returns if it has moved on, so a late-resolving arrayBuffer() promise can no longer clobber state or dispatch a phantom `load` after a user-initiated abort. - readAsText now prefers TextDecoder first and falls back to a chunked String.fromCharCode.apply + Array.join path. The previous code built a byte-by-byte `s += c` string and then often discarded it; for multi-MB GLTF JSON this was the most expensive path in the polyfill. - dispatchEvent(event) routes through the existing internal dispatch() helper so on* handlers and addEventListener listeners actually fire for the event type, instead of returning true and dropping the event. Config change: - Rewrite the Draco/utf8 reason text to scope the limitation correctly: it is BN's TextDecoder (no WHATWG label normalization) that rejects `utf8`, not Web TextDecoder in general. Refs PR BabylonJS#1706. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
ryantrem
left a comment
There was a problem hiding this comment.
Should we not just add File support to the actual native Blob polyfill?
Adds a JS polyfill for the File API that allows GLTF serializer + loader playgrounds to run under BN. Largest test-impact PR of the weekend series (19 re-enables).
Changes:
Apps/Playground/Scripts/file_polyfill.js- JS shim wrapping nativeBlobwithFilesemantics (name,lastModified) plus aBlob.arrayBuffer()-backedFileReader(readAsArrayBuffer/Text/DataURL/BinaryString+ event surface).Apps/Playground/CMakeLists.txt- addsfile_polyfill.jsto SCRIPTS list.Apps/Playground/Shared/AppContext.cpp- loadsfile_polyfill.jsbeforeammo.js.Apps/Playground/Scripts/config.json- re-enables 19 affected tests; addsexcludedGraphicsApis: ["OpenGL"]to 5 GLTF Serializer tests (KHR gpu instancing + 4 Camera variants) for OpenGL-specific backend issues unrelated to this polyfill.Verified: 19 of 28 affected tests pass without
--include-excluded. Remaining 9 fail with separate root causes (Draco TextDecoder utf8/utf-8 naming, pixel-diffs unrelated to File API, 2 hangs on Chakra D3D11) - tracked separately.Landing context
This PR is one of 7 splits from the proven CI-green combined preview in draft PR #1702 (see #1702 for the full intended end-state and verified CI run 26044922430).
Recommended landing order
Tier 1 - parallel-reviewable, no source conflicts:
reasonrewrites (5 entries)reasonrewrites (17 entries)Tier 2 - sequential, each touches
Apps/Playground/CMakeLists.txtSCRIPTS list +Apps/Playground/Shared/AppContext.cppLoadScript order; rebase the next branch after the previous merges:Reference policy reminder
Reference PNGs across all 7 PRs come from Babylon.js; never re-baked by BN. Combined diff: 0 PNGs.