Skip to content

Add File/Blob/FileReader polyfill for Playground (re-enables 19 GLTF tests)#1706

Open
bkaradzic-microsoft wants to merge 6 commits into
BabylonJS:masterfrom
bkaradzic-microsoft:weekend/tpc-1582-file-api
Open

Add File/Blob/FileReader polyfill for Playground (re-enables 19 GLTF tests)#1706
bkaradzic-microsoft wants to merge 6 commits into
BabylonJS:masterfrom
bkaradzic-microsoft:weekend/tpc-1582-file-api

Conversation

@bkaradzic-microsoft
Copy link
Copy Markdown
Contributor

@bkaradzic-microsoft bkaradzic-microsoft commented May 18, 2026

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:

  • New Apps/Playground/Scripts/file_polyfill.js - JS shim wrapping native Blob with File semantics (name, lastModified) plus a Blob.arrayBuffer()-backed FileReader (readAsArrayBuffer/Text/DataURL/BinaryString + event surface).
  • Apps/Playground/CMakeLists.txt - adds file_polyfill.js to SCRIPTS list.
  • Apps/Playground/Shared/AppContext.cpp - loads file_polyfill.js before ammo.js.
  • Apps/Playground/Scripts/config.json - re-enables 19 affected tests; adds excludedGraphicsApis: ["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).

Note: the original split included an 8th PR (#1709, ES2020+ -> ES2019 syntax-repair polyfill for Chakra). It was closed in favour of investigating @babel/standalone properly (#1711).

Recommended landing order

Tier 1 - parallel-reviewable, no source conflicts:

  1. Fix ExternalTexture_OpenGL throw-stubs to avoid MSVC C4702 under /WX #1703 - ExternalTexture C4702 build fix
  2. Document accurate root cause for post-#1695 pixel-diff fallouts #1704 - config.json reason rewrites (5 entries)
  3. Document accurate root cause for 17 subtle pixel-diff tests #1705 - config.json reason rewrites (17 entries)

Tier 2 - sequential, each touches Apps/Playground/CMakeLists.txt SCRIPTS list + Apps/Playground/Shared/AppContext.cpp LoadScript order; rebase the next branch after the previous merges:

  1. Add File/Blob/FileReader polyfill for Playground (re-enables 19 GLTF tests) #1706 - File/Blob/FileReader polyfill (largest test impact: 19 re-enables)
  2. Add fetch() polyfill over XMLHttpRequest for Playground #1707 - fetch polyfill
  3. Add DOM globals polyfill + native AbortController for Playground #1708 - DOM globals + native AbortController + Android CMake link
  4. Add cubemap auto-expand polyfill for Playground (re-enables 7 PBR tests) #1710 - Cubemap auto-expand polyfill (loaded after babylon.max.js)

Reference policy reminder

Reference PNGs across all 7 PRs come from Babylon.js; never re-baked by BN. Combined diff: 0 PNGs.

bkaradzic-microsoft and others added 5 commits May 15, 2026 20:58
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>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.js implementing File (as a decorated native Blob) and a Promise-backed FileReader.
  • 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.

Comment on lines +127 to +135
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");
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +160 to +163
Promise.resolve(bufPromise).then(function (buf) {
if (mode === "arraybuffer") {
reader.result = buf;
} else if (mode === "text") {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +209 to +218
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");
});
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +232 to +241
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");
};
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 () {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +164 to +174
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 */ }
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread Apps/Playground/Scripts/config.json Outdated
"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'",
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Copy link
Copy Markdown
Member

@ryantrem ryantrem left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we not just add File support to the actual native Blob polyfill?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants