Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
101f631
Add File/FileReader JS polyfill for Babylon Native Playground
bkaradzic-microsoft May 16, 2026
803e5c2
Re-enable 19 GLTF/OBJ serializer tests now unblocked by File polyfill
bkaradzic-microsoft May 16, 2026
aa84042
Add DOM polyfills (TextEncoder, PointerEvent, AbortController, docume…
bkaradzic-microsoft May 16, 2026
067a8a4
Replace OpenGL ExternalTexture throw-stubs with inert default-value s…
bkaradzic-microsoft May 16, 2026
dfae89e
Add fetch() polyfill for Playground
bkaradzic-microsoft May 16, 2026
6744fb6
Add .env fallback for cubemap loads + re-enable 7 tests
bkaradzic-microsoft May 16, 2026
b094b58
Triage post-SPIRV-Cross-bump pixel-diff fallout
bkaradzic-microsoft May 16, 2026
10a6792
Update reason strings on excluded pixel-diff tests
bkaradzic-microsoft May 16, 2026
cd10aa9
Add ES2020+ -> ES2019 syntax repair polyfill for Chakra
bkaradzic-microsoft May 16, 2026
384a7c5
Drop BN-rendered reference image re-bakes from #1647 triage
bkaradzic-microsoft May 18, 2026
c5cb8c0
Android: link AbortController polyfill into BabylonNativeJNI
bkaradzic-microsoft May 18, 2026
44af2c0
config: keep GLTF Serializer KHR gpu instancing excluded on OpenGL
bkaradzic-microsoft May 18, 2026
03d7657
config: keep GLTF Serializer Camera, left-handed excluded on OpenGL
bkaradzic-microsoft May 18, 2026
9caa44d
config: exclude remaining 3 GLTF Serializer Camera variants on OpenGL
bkaradzic-microsoft May 18, 2026
dbaf6c0
Merge branch 'weekend/tpc-1652-externaltexture-c4702' into weekend/al…
bkaradzic-microsoft May 18, 2026
d0b3f1c
Merge branch 'weekend/tpc-1647-triage' into weekend/all-combined
bkaradzic-microsoft May 18, 2026
06349f1
Merge branch 'weekend/tpc-1599-subtle-pixel-diffs' into weekend/all-c…
bkaradzic-microsoft May 18, 2026
f1fa220
Merge branch 'weekend/tpc-1582-file-api' into weekend/all-combined
bkaradzic-microsoft May 18, 2026
20eef2b
Merge branch 'weekend/tpc-1579-dom-globals' into weekend/all-combined
bkaradzic-microsoft May 18, 2026
a293973
Merge branch 'weekend/tpc-1581-fetch-shim' into weekend/all-combined
bkaradzic-microsoft May 18, 2026
b99d736
Merge branch 'weekend/tpc-1577-cubemap-6-files' into weekend/all-comb…
bkaradzic-microsoft May 18, 2026
61da57f
Merge branch 'weekend/tpc-1575-chakra-parse' into weekend/all-combined
bkaradzic-microsoft May 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Apps/Playground/Android/BabylonNative/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ target_link_libraries(BabylonNativeJNI
PRIVATE EGL
PRIVATE log
PRIVATE -lz
PRIVATE AbortController
PRIVATE AndroidExtensions
PRIVATE AppRuntime
PRIVATE Blob
Expand Down
6 changes: 6 additions & 0 deletions Apps/Playground/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@ set(DEPENDENCIES
"../Dependencies/recast.js")

set(SCRIPTS
"Scripts/cube_texture_polyfill.js"
"Scripts/dom_polyfill.js"
"Scripts/es2019_transpile.js"
"Scripts/experience.js"
"Scripts/fetch_polyfill.js"
"Scripts/file_polyfill.js"
"Scripts/playground_runner.js"
"Scripts/validation_native.js"
"Scripts/config.json")
Expand Down Expand Up @@ -134,6 +139,7 @@ endif()
target_include_directories(Playground PRIVATE ".")

target_link_libraries(Playground
PRIVATE AbortController
PRIVATE AppRuntime
PRIVATE Blob
PRIVATE bx
Expand Down
166 changes: 60 additions & 106 deletions Apps/Playground/Scripts/config.json

Large diffs are not rendered by default.

113 changes: 113 additions & 0 deletions Apps/Playground/Scripts/cube_texture_polyfill.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// Cube texture fallback for BN's NativeEngine.
//
// BN's NativeEngine.createCubeTexture only natively handles .env single-file
// cubemaps and 6-file face arrays. Snippets that load .dds (or .ktx) cubemaps
// without an explicit forcedExtension or 6-file array throw "Cannot load
// cubemap because 6 files were not defined".
//
// Babylon's standard environment cubemaps are hosted both as <name>.dds and
// <name>.env at the same path on assets.babylonjs.com and
// playground.babylonjs.com. This polyfill detects the failure pre-condition
// and transparently retries with the .env URL. If the .env counterpart 404s,
// it falls through to the original (which throws), preserving existing
// behavior.

(function () {
"use strict";

if (typeof BABYLON === "undefined") {
return;
}
if (!BABYLON.NativeEngine || !BABYLON.NativeEngine.prototype) {
return;
}

var proto = BABYLON.NativeEngine.prototype;
if (proto.__cubeTexturePolyfillInstalled) {
return;
}
proto.__cubeTexturePolyfillInstalled = true;

var original = proto.createCubeTexture;
if (typeof original !== "function") {
return;
}

var FALLBACK_EXTS = [".dds", ".ktx", ".ktx2"];

function getExtension(url, forced) {
if (forced) {
return forced.toLowerCase();
}
var dot = url.lastIndexOf(".");
if (dot < 0) {
return "";
}
var ext = url.substring(dot).toLowerCase();
var q = ext.indexOf("?");
if (q >= 0) {
ext = ext.substring(0, q);
}
return ext;
}

function replaceExt(url, oldExt) {
return url.substring(0, url.length - oldExt.length) + ".env";
}

proto.createCubeTexture = function (rootUrl, scene, files, noMipmap, onLoad, onError, format, forcedExtension, createPolynomials, lodScale, lodOffset, fallback, loaderOptions, useSRGBBuffer, buffer) {
var ext = getExtension(rootUrl, forcedExtension);
var hasFiles = files && files.length === 6;
var canFallback = !buffer && !forcedExtension && !hasFiles && FALLBACK_EXTS.indexOf(ext) >= 0;

if (!canFallback) {
return original.apply(this, arguments);
}

var self = this;
var envUrl = replaceExt(rootUrl, ext);
var texture = fallback || new BABYLON.InternalTexture(self, 7 /* Cube */);
texture.isCube = true;
texture.url = rootUrl;

var settled = false;
var settle = function (action) {
if (settled) {
return;
}
settled = true;
try {
action();
} catch (e) {
if (onError) {
onError(e && e.message ? e.message : String(e), e);
}
}
};

var onEnvLoaded = function (data) {
settle(function () {
var buf = (data && data.byteLength !== undefined && !(data instanceof Uint8Array)) ? new Uint8Array(data, 0, data.byteLength) : data;
original.call(self, envUrl, scene, files, noMipmap, onLoad, onError, format, ".env", createPolynomials, lodScale || 0, lodOffset || 0, texture, loaderOptions, useSRGBBuffer || false, buf);
});
};

var onEnvFailed = function (request, exception) {
settle(function () {
original.call(self, rootUrl, scene, files, noMipmap, onLoad, onError, format, forcedExtension, createPolynomials, lodScale, lodOffset, texture, loaderOptions, useSRGBBuffer, buffer);
});
};

try {
self._loadFile(envUrl, onEnvLoaded, undefined, undefined, true, onEnvFailed);
} catch (e) {
onEnvFailed(null, e);
}

return texture;
};

if (typeof console !== "undefined" && console.log) {
console.log("[cube_texture_polyfill] NativeEngine.createCubeTexture patched with .env fallback");
}
})();
114 changes: 114 additions & 0 deletions Apps/Playground/Scripts/dom_polyfill.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// dom_polyfill.js
//
// Minimal browser/DOM globals for the Babylon Native Playground JS host.
// Provides TextEncoder and PointerEvent on runtimes (older Chakra) that do
// not ship them natively. AbortController is provided by a native polyfill.
// Each shim is self-detecting and no-ops if the symbol already exists.
//
// `document` is shimmed by validation_native.js once it has loaded the
// config, since the runner needs to inject test-specific behaviour into
// createElement.

(function () {
'use strict';

// Chakra has no `globalThis`; use the Function-constructor trick.
var g = (new Function('return this'))();

if (typeof g.TextEncoder === 'undefined') {
function TextEncoder() {}
Object.defineProperty(TextEncoder.prototype, 'encoding', {
get: function () { return 'utf-8'; },
configurable: true,
});
TextEncoder.prototype.encode = function (input) {
var str = input === undefined ? '' : String(input);
var bytes = [];
for (var i = 0; i < str.length; i++) {
var code = str.charCodeAt(i);
if (code >= 0xD800 && code <= 0xDBFF && i + 1 < str.length) {
var c2 = str.charCodeAt(i + 1);
if (c2 >= 0xDC00 && c2 <= 0xDFFF) {
code = 0x10000 + (((code & 0x3FF) << 10) | (c2 & 0x3FF));
i++;
}
}
if (code < 0x80) {
bytes.push(code);
} else if (code < 0x800) {
bytes.push(0xC0 | (code >> 6));
bytes.push(0x80 | (code & 0x3F));
} else if (code < 0x10000) {
bytes.push(0xE0 | (code >> 12));
bytes.push(0x80 | ((code >> 6) & 0x3F));
bytes.push(0x80 | (code & 0x3F));
} else {
bytes.push(0xF0 | (code >> 18));
bytes.push(0x80 | ((code >> 12) & 0x3F));
bytes.push(0x80 | ((code >> 6) & 0x3F));
bytes.push(0x80 | (code & 0x3F));
}
}
return new Uint8Array(bytes);
};
TextEncoder.prototype.encodeInto = function (input, dest) {
var arr = this.encode(input);
var n = Math.min(arr.length, dest.length);
for (var i = 0; i < n; i++) dest[i] = arr[i];
return { read: String(input).length, written: n };
};
g.TextEncoder = TextEncoder;
}

if (typeof g.PointerEvent === 'undefined') {
function PointerEvent(type, init) {
init = init || {};
this.type = String(type || '');
this.bubbles = !!init.bubbles;
this.cancelable = init.cancelable !== false;
this.composed = !!init.composed;
this.defaultPrevented = false;
this.target = init.target || null;
this.currentTarget = null;
this.timeStamp = Date.now();
this.pointerId = init.pointerId !== undefined ? init.pointerId : 0;
this.width = init.width !== undefined ? init.width : 1;
this.height = init.height !== undefined ? init.height : 1;
this.pressure = init.pressure !== undefined ? init.pressure : 0.5;
this.tangentialPressure = init.tangentialPressure || 0;
this.tiltX = init.tiltX || 0;
this.tiltY = init.tiltY || 0;
this.twist = init.twist || 0;
this.altitudeAngle = init.altitudeAngle || 0;
this.azimuthAngle = init.azimuthAngle || 0;
this.pointerType = init.pointerType || 'mouse';
this.isPrimary = init.isPrimary !== false;
this.clientX = init.clientX || 0;
this.clientY = init.clientY || 0;
this.offsetX = init.offsetX || 0;
this.offsetY = init.offsetY || 0;
this.pageX = init.pageX || 0;
this.pageY = init.pageY || 0;
this.screenX = init.screenX || 0;
this.screenY = init.screenY || 0;
this.movementX = init.movementX || 0;
this.movementY = init.movementY || 0;
this.button = init.button || 0;
this.buttons = init.buttons || 0;
this.relatedTarget = init.relatedTarget || null;
this.ctrlKey = !!init.ctrlKey;
this.shiftKey = !!init.shiftKey;
this.altKey = !!init.altKey;
this.metaKey = !!init.metaKey;
this.detail = init.detail || 0;
this.view = init.view || null;
}
PointerEvent.prototype.preventDefault = function () { this.defaultPrevented = true; };
PointerEvent.prototype.stopPropagation = function () {};
PointerEvent.prototype.stopImmediatePropagation = function () {};
PointerEvent.prototype.getModifierState = function () { return false; };
PointerEvent.prototype.getCoalescedEvents = function () { return []; };
PointerEvent.prototype.getPredictedEvents = function () { return []; };
g.PointerEvent = PointerEvent;
}
})();
114 changes: 114 additions & 0 deletions Apps/Playground/Scripts/es2019_transpile.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// es2019_transpile.js
//
// Lightweight regex-based ES2020+ -> ES2019 syntax repair for engines that
// lack optional chaining (?.), nullish coalescing (??), and numeric
// separators (1_000_000).
//
// This is a SYNTAX REPAIR, not a full transpile. It rewrites parse-time
// failing tokens to the closest legal ES2019 expressions so that the host
// engine accepts the code. Where native ES2020+ semantics differ from the
// rewritten form, the rewritten form runs the "happy path" -- assumes the
// target value is present. If the target is null at runtime the rewritten
// code throws a TypeError where native ?. would have short-circuited to
// undefined. This is an intentional trade-off: it produces valid syntax
// (the prerequisite for the test running at all) while preserving the
// common case behaviour. Tests that rely on the nullish-short-circuit
// semantics may surface runtime errors that were previously hidden by
// the parse failure -- a strict improvement for debuggability.
//
// Transforms applied:
// * 1_000_000 -> 1000000 (strip numeric separators)
// * a?.b -> a.b (optional chaining -> required chaining)
// * a?.[x] -> a[x]
// * a?.() -> a()
// * a ?? b -> (a != null ? a : b)
//
// Not handled (still cause parse failures on Chakra):
// * Logical assignment ||= &&= ??=
// * Class private fields #name
// * BigInt literals 1n
//
// String/regex/comment literals containing ? . sequences are not skipped,
// but in Babylon snippet code such occurrences are rare.
//
// On engines with native ES2020+ support (V8, JSC) this code is never
// invoked because the caller only retries with this transform after
// initial eval throws a SyntaxError.
//
// Public API: top-level global function __bnTranspileES2019(code) -> string.
// Chakra in Babylon Native does not define `self` or `globalThis`, so we
// install via a top-level `var` declaration which becomes a property of
// the script-global object across all our supported engines.
var __bnTranspileES2019 = (function () {
"use strict";

function stripNumericSeparators(code) {
// Decimal / float: 1_000_000, 1.234_5, 1_000e3
var out = code.replace(/\b(\d[\d_]*(?:\.[\d_]+)?(?:[eE][+\-]?[\d_]+)?)\b/g, function (m) {
return m.indexOf("_") === -1 ? m : m.replace(/_/g, "");
});
// Hex / octal / binary: 0xFFFF_FFFF, 0o7_5, 0b1010_0101
out = out.replace(/\b(0[xXoObB][0-9A-Fa-f_]+)\b/g, function (m) {
return m.indexOf("_") === -1 ? m : m.replace(/_/g, "");
});
return out;
}

function transformLogicalAssignment(code) {
// a ||= b -> (a || (a = b))
// a &&= b -> (a && (a = b))
// a ??= b -> (a != null ? a : (a = b))
// Only matches simple LHS (identifier, optional .prop / [idx] chain) to keep
// the rewrite syntactically safe; complex LHS expressions are left alone.
var lhs = "(?:[A-Za-z_$][\\w$]*)(?:\\.[A-Za-z_$][\\w$]*|\\[[^\\]\\n]*\\])*";
var rhs = "[^;\\n]+";
var prev = null, out = code, guard = 0;
while (prev !== out && guard++ < 10) {
prev = out;
out = out
.replace(new RegExp("(" + lhs + ")\\s*\\?\\?=\\s*(" + rhs + ")", "g"),
"$1 = ($1 != null ? $1 : ($2))")
.replace(new RegExp("(" + lhs + ")\\s*\\|\\|=\\s*(" + rhs + ")", "g"),
"$1 = ($1 || ($2))")
.replace(new RegExp("(" + lhs + ")\\s*&&=\\s*(" + rhs + ")", "g"),
"$1 = ($1 && ($2))");
}
return out;
}

function transformOptionalChaining(code) {
var prev = null, out = code, guard = 0;
while (prev !== out && guard++ < 50) {
prev = out;
out = out
.replace(/\?\.(?=\()/g, "")
.replace(/\?\.(?=\[)/g, "")
.replace(/\?\./g, ".");
}
return out;
}

function transformNullishCoalescing(code) {
var prev = null, out = code, guard = 0;
while (prev !== out && guard++ < 50) {
prev = out;
out = out.replace(
/(\b[A-Za-z_$][\w$.]*(?:\[[^\]]*\])?)\s*\?\?\s*(\b[A-Za-z_$][\w$.]*(?:\[[^\]]*\])?|"[^"]*"|'[^']*'|\d+(?:\.\d+)?|true|false|null|undefined)/g,
"($1 != null ? $1 : $2)"
);
}
return out;
}

function transpileES2019(code) {
if (typeof code !== "string") { return code; }
var out = code;
out = stripNumericSeparators(out);
out = transformLogicalAssignment(out);
out = transformOptionalChaining(out);
out = transformNullishCoalescing(out);
return out;
}

return transpileES2019;
})();
Loading
Loading