From 5c7edb004671ff0e8437f06dae5e6b55a0ff6b33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Branimir=20Karad=C5=BEi=C4=87?= Date: Fri, 15 May 2026 21:27:59 -0700 Subject: [PATCH 1/5] Add DOM polyfills (TextEncoder, PointerEvent, AbortController, document.createEvent) Playground tests that use modern browser APIs hit ReferenceErrors on the Chakra-based BN runtime because TextEncoder and PointerEvent are not part of older Chakra's built-ins. Several serializer tests also exercise document.createEvent + element.dispatchEvent shapes that the existing minimal document polyfill in validation_native.js did not cover. - Apps/Playground/Scripts/dom_polyfill.js: new self-detecting JS polyfills for TextEncoder (UTF-8 encode + encodeInto) and PointerEvent (constructor with the MouseEvent + PointerEvent surface). - Apps/Playground/Shared/AppContext.cpp + CMakeLists.txt: wire in the native AbortController polyfill from JsRuntimeHost and load dom_polyfill.js before ammo.js. - Apps/Playground/Scripts/validation_native.js: extend the existing document shim with createEvent, dispatchEvent, addEventListener and a generic-element fallback for createElement so click-event-based serializer tests can run. - Apps/Playground/Scripts/config.json: re-enable "Serialize scene without materials" (idx 342), which now validates with 248 px diff. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Apps/Playground/CMakeLists.txt | 2 + Apps/Playground/Scripts/config.json | 2 - Apps/Playground/Scripts/dom_polyfill.js | 114 +++++++++++++++++++ Apps/Playground/Scripts/validation_native.js | 47 +++++++- Apps/Playground/Shared/AppContext.cpp | 4 + 5 files changed, 165 insertions(+), 4 deletions(-) create mode 100644 Apps/Playground/Scripts/dom_polyfill.js diff --git a/Apps/Playground/CMakeLists.txt b/Apps/Playground/CMakeLists.txt index 90ddb81c8..dcf54f906 100644 --- a/Apps/Playground/CMakeLists.txt +++ b/Apps/Playground/CMakeLists.txt @@ -13,6 +13,7 @@ set(DEPENDENCIES "../Dependencies/recast.js") set(SCRIPTS + "Scripts/dom_polyfill.js" "Scripts/experience.js" "Scripts/playground_runner.js" "Scripts/validation_native.js" @@ -134,6 +135,7 @@ endif() target_include_directories(Playground PRIVATE ".") target_link_libraries(Playground + PRIVATE AbortController PRIVATE AppRuntime PRIVATE Blob PRIVATE bx diff --git a/Apps/Playground/Scripts/config.json b/Apps/Playground/Scripts/config.json index e0022cf38..2915afaf4 100644 --- a/Apps/Playground/Scripts/config.json +++ b/Apps/Playground/Scripts/config.json @@ -2258,8 +2258,6 @@ { "title": "Serialize scene without materials", "playgroundId": "#PH4DEZ#1", - "excludeFromAutomaticTesting": true, - "reason": "Pixel comparison fails (more than 20% pixels differ)", "referenceImage": "serializeWithoutMaterials.png" }, { diff --git a/Apps/Playground/Scripts/dom_polyfill.js b/Apps/Playground/Scripts/dom_polyfill.js new file mode 100644 index 000000000..003ee0c74 --- /dev/null +++ b/Apps/Playground/Scripts/dom_polyfill.js @@ -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; + } +})(); diff --git a/Apps/Playground/Scripts/validation_native.js b/Apps/Playground/Scripts/validation_native.js index 686280208..130d9a122 100644 --- a/Apps/Playground/Scripts/validation_native.js +++ b/Apps/Playground/Scripts/validation_native.js @@ -525,9 +525,52 @@ if (type === "canvas") { return new OffscreenCanvas(64, 64); } - return {}; + // Generic element stub with a no-op dispatchEvent so serializer + // tests that simulate a click via createEvent/dispatchEvent run + // without throwing. + return { + style: {}, + addEventListener: function () { }, + removeEventListener: function () { }, + dispatchEvent: function () { return true; }, + appendChild: function (c) { return c; }, + removeChild: function (c) { return c; }, + setAttribute: function () { }, + getAttribute: function () { return null; }, + click: function () { }, + }; + }, + createEvent: function (type) { + var ev = { + type: '', + bubbles: false, + cancelable: false, + defaultPrevented: false, + target: null, + currentTarget: null, + timeStamp: Date.now(), + detail: null, + preventDefault: function () { ev.defaultPrevented = true; }, + stopPropagation: function () { }, + stopImmediatePropagation: function () { }, + }; + ev.initEvent = function (t, bubbles, cancelable) { + ev.type = String(t || ''); + ev.bubbles = !!bubbles; + ev.cancelable = !!cancelable; + }; + ev.initCustomEvent = function (t, bubbles, cancelable, detail) { + ev.initEvent(t, bubbles, cancelable); + ev.detail = detail; + }; + ev.initUIEvent = ev.initEvent; + ev.initMouseEvent = ev.initEvent; + return ev; }, - removeEventListener: function () { } + addEventListener: function () { }, + removeEventListener: function () { }, + dispatchEvent: function () { return true; }, + body: { appendChild: function (c) { return c; }, removeChild: function (c) { return c; } }, } const xhr = new XMLHttpRequest(); diff --git a/Apps/Playground/Shared/AppContext.cpp b/Apps/Playground/Shared/AppContext.cpp index 09d9a408a..8db2df3e6 100644 --- a/Apps/Playground/Shared/AppContext.cpp +++ b/Apps/Playground/Shared/AppContext.cpp @@ -17,6 +17,7 @@ #include #include +#include #include #include #include @@ -185,6 +186,8 @@ AppContext::AppContext( Babylon::Polyfills::Window::Initialize(env); + Babylon::Polyfills::AbortController::Initialize(env); + Babylon::Polyfills::TextDecoder::Initialize(env); Babylon::Polyfills::XMLHttpRequest::Initialize(env); @@ -213,6 +216,7 @@ AppContext::AppContext( }); m_scriptLoader.emplace(*m_runtime); + m_scriptLoader->LoadScript("app:///Scripts/dom_polyfill.js"); m_scriptLoader->LoadScript("app:///Scripts/ammo.js"); // Commenting out recast.js for now because v8jsi is incompatible with asm.js. // m_scriptLoader->LoadScript("app:///Scripts/recast.js"); From ddbc2c9e686b25c6d7614452c11de9485088fd24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Branimir=20Karad=C5=BEi=C4=87?= Date: Mon, 18 May 2026 08:14:05 -0700 Subject: [PATCH 2/5] Android: link AbortController polyfill into BabylonNativeJNI AppContext.cpp includes and calls Babylon::Polyfills::AbortController::Initialize(env), but the Android- specific CMakeLists at Apps/Playground/Android/BabylonNative did not add AbortController to BabylonNativeJNI's PRIVATE link libraries. The header is published only by the AbortController target, so Android CI failed with 'fatal error: Babylon/Polyfills/AbortController.h file not found'. Linux/macOS/Windows builds were fine because they use the main Apps/Playground/CMakeLists.txt which already links AbortController. Mirror the change there so Android picks up the include directory. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Apps/Playground/Android/BabylonNative/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/Apps/Playground/Android/BabylonNative/CMakeLists.txt b/Apps/Playground/Android/BabylonNative/CMakeLists.txt index 3f1b5ee15..0ab5ba294 100644 --- a/Apps/Playground/Android/BabylonNative/CMakeLists.txt +++ b/Apps/Playground/Android/BabylonNative/CMakeLists.txt @@ -25,6 +25,7 @@ target_link_libraries(BabylonNativeJNI PRIVATE EGL PRIVATE log PRIVATE -lz + PRIVATE AbortController PRIVATE AndroidExtensions PRIVATE AppRuntime PRIVATE Blob From d7936678b26417b506109ebca390c8f1e5c0e583 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Branimir=20Karad=C5=BEi=C4=87?= Date: Fri, 15 May 2026 22:18:46 -0700 Subject: [PATCH 3/5] Add fetch() polyfill for Playground Implements a minimal `fetch(input, init)` polyfill in Apps/Playground that wraps XMLHttpRequest. Provides a Response-like result with `.ok`, `.status`, `.statusText`, `.url`, `.text()`, `.arrayBuffer()`, `.json()`, `.blob()`, and a `headers` stub. Self-detecting: no-op if a global `fetch` already exists; bails if XMLHttpRequest is unavailable. Wired into the Playground SCRIPTS list and loaded by AppContext before `ammo.js` / `babylon.max.js` so playground snippets that use the modern `fetch` API can run on Babylon Native's host environments (Chakra and V8), which do not provide `fetch` by default. Babylon Native's `XMLHttpRequest` only dispatches events through `addEventListener` (no `onload`/`onerror` properties) and fires `loadend` rather than `load`; the polyfill listens on `loadend` and inspects `status` to decide resolve/reject. When `responseType` is `arraybuffer`, BN's `XHR.responseText` is empty, so `.text()` decodes the array buffer directly rather than reading `responseText`. --- Apps/Playground/CMakeLists.txt | 1 + Apps/Playground/Scripts/fetch_polyfill.js | 227 ++++++++++++++++++++++ Apps/Playground/Shared/AppContext.cpp | 1 + 3 files changed, 229 insertions(+) create mode 100644 Apps/Playground/Scripts/fetch_polyfill.js diff --git a/Apps/Playground/CMakeLists.txt b/Apps/Playground/CMakeLists.txt index dcf54f906..e32911e26 100644 --- a/Apps/Playground/CMakeLists.txt +++ b/Apps/Playground/CMakeLists.txt @@ -15,6 +15,7 @@ set(DEPENDENCIES set(SCRIPTS "Scripts/dom_polyfill.js" "Scripts/experience.js" + "Scripts/fetch_polyfill.js" "Scripts/playground_runner.js" "Scripts/validation_native.js" "Scripts/config.json") diff --git a/Apps/Playground/Scripts/fetch_polyfill.js b/Apps/Playground/Scripts/fetch_polyfill.js new file mode 100644 index 000000000..e6eaa257b --- /dev/null +++ b/Apps/Playground/Scripts/fetch_polyfill.js @@ -0,0 +1,227 @@ +// Minimal `fetch` polyfill for Babylon Native. +// +// Provides global `fetch(url, options)` returning a Promise that resolves to a +// Response-like object with `.ok`, `.status`, `.statusText`, `.url`, +// `.text()`, `.arrayBuffer()`, `.json()`, `.blob()`, and a `headers` +// stub (always empty). Internally wraps XMLHttpRequest so any URL scheme +// the host XHR can resolve (http:, https:, app:, data:, ...) works. +// +// Skipped if a `fetch` global is already defined. + +(function () { + var g = (new Function('return this'))(); + if (typeof g.fetch === 'function') { + return; + } + + var XHR = g.XMLHttpRequest; + if (typeof XHR !== 'function') { + // Nothing we can do without XHR. + return; + } + + function HeadersStub() {} + HeadersStub.prototype.get = function () { return null; }; + HeadersStub.prototype.has = function () { return false; }; + HeadersStub.prototype.forEach = function () {}; + + function arrayBufferToString(buf) { + var bytes = new Uint8Array(buf); + // Chunk to avoid call stack limits on large buffers. + var chunkSize = 0x8000; + var chars = []; + for (var i = 0; i < bytes.length; i += chunkSize) { + chars.push(String.fromCharCode.apply(null, bytes.subarray(i, i + chunkSize))); + } + return chars.join(''); + } + + function makeResponse(xhr, requestUrl, effectiveResponseType) { + var bodyUsed = false; + var cachedArrayBuffer = null; + var cachedText = null; + + function ensureNotConsumed() { + if (bodyUsed) { + throw new TypeError('Body has already been consumed.'); + } + bodyUsed = true; + } + + function getArrayBuffer() { + if (cachedArrayBuffer !== null) { + return cachedArrayBuffer; + } + var resp = xhr.response; + if (resp instanceof ArrayBuffer) { + cachedArrayBuffer = resp; + } else if (typeof resp === 'string') { + var len = resp.length; + var buf = new ArrayBuffer(len); + var view = new Uint8Array(buf); + for (var i = 0; i < len; i++) { + view[i] = resp.charCodeAt(i) & 0xFF; + } + cachedArrayBuffer = buf; + } else if (resp && resp.byteLength !== undefined) { + cachedArrayBuffer = resp; + } else { + cachedArrayBuffer = new ArrayBuffer(0); + } + return cachedArrayBuffer; + } + + function getText() { + if (cachedText !== null) { + return cachedText; + } + // Prefer responseText only when the request actually used the + // String response type; otherwise BN's XHR returns an empty + // string and we must decode the arraybuffer ourselves. + if (effectiveResponseType === 'text' && typeof xhr.responseText === 'string') { + cachedText = xhr.responseText; + } else { + cachedText = arrayBufferToString(getArrayBuffer()); + } + return cachedText; + } + + var status = xhr.status || 0; + return { + ok: status >= 200 && status < 300, + status: status, + statusText: xhr.statusText || '', + url: requestUrl, + redirected: false, + type: 'basic', + headers: new HeadersStub(), + get bodyUsed() { return bodyUsed; }, + arrayBuffer: function () { + ensureNotConsumed(); + return Promise.resolve(getArrayBuffer()); + }, + text: function () { + ensureNotConsumed(); + return Promise.resolve(getText()); + }, + json: function () { + ensureNotConsumed(); + try { + return Promise.resolve(JSON.parse(getText())); + } catch (e) { + return Promise.reject(e); + } + }, + blob: function () { + ensureNotConsumed(); + var buf = getArrayBuffer(); + if (typeof g.Blob === 'function') { + return Promise.resolve(new g.Blob([buf])); + } + return Promise.resolve(buf); + }, + clone: function () { + throw new TypeError('Response.clone() is not supported by this polyfill.'); + } + }; + } + + g.fetch = function (input, init) { + init = init || {}; + var url; + var method = (init.method || 'GET').toUpperCase(); + var body = init.body; + var headers = init.headers; + var responseType = init.responseType; + + if (typeof input === 'string') { + url = input; + } else if (input && typeof input.url === 'string') { + url = input.url; + method = (input.method || method).toUpperCase(); + body = input.body || body; + headers = input.headers || headers; + } else { + return Promise.reject(new TypeError('fetch: invalid input.')); + } + + return new Promise(function (resolve, reject) { + var xhr; + try { + xhr = new XHR(); + } catch (e) { + reject(e); + return; + } + + try { + xhr.open(method, url, true); + } catch (e) { + reject(e); + return; + } + + var effectiveResponseType = responseType === 'text' ? 'text' : 'arraybuffer'; + try { + xhr.responseType = effectiveResponseType; + } catch (e) {} + + if (headers) { + if (typeof headers.forEach === 'function') { + headers.forEach(function (value, name) { + try { xhr.setRequestHeader(name, value); } catch (e) {} + }); + } else { + for (var k in headers) { + if (Object.prototype.hasOwnProperty.call(headers, k)) { + try { xhr.setRequestHeader(k, headers[k]); } catch (e) {} + } + } + } + } + + // Babylon Native's XMLHttpRequest only dispatches via addEventListener + // and only fires 'readystatechange', 'loadend', and 'error' (no 'load'). + // Inspect status in loadend to decide success/failure. + var settled = false; + function settleFromLoadEnd() { + if (settled) { + return; + } + settled = true; + var status = xhr.status || 0; + if (status >= 200 && status < 300) { + resolve(makeResponse(xhr, url, effectiveResponseType)); + } else { + reject(new TypeError('fetch: request failed with status ' + status + ' for ' + url)); + } + } + function settleAsError() { + if (settled) { + return; + } + settled = true; + reject(new TypeError('Network request failed: ' + url)); + } + + if (typeof xhr.addEventListener === 'function') { + xhr.addEventListener('loadend', settleFromLoadEnd); + xhr.addEventListener('error', settleAsError); + } else { + xhr.onload = settleFromLoadEnd; + xhr.onloadend = settleFromLoadEnd; + xhr.onerror = settleAsError; + xhr.onabort = settleAsError; + } + + try { + xhr.send(body || null); + } catch (e) { + if (!settled) { + settled = true; + reject(e); + } + } + }); + }; +})(); diff --git a/Apps/Playground/Shared/AppContext.cpp b/Apps/Playground/Shared/AppContext.cpp index 8db2df3e6..adc3a748a 100644 --- a/Apps/Playground/Shared/AppContext.cpp +++ b/Apps/Playground/Shared/AppContext.cpp @@ -217,6 +217,7 @@ AppContext::AppContext( m_scriptLoader.emplace(*m_runtime); m_scriptLoader->LoadScript("app:///Scripts/dom_polyfill.js"); + m_scriptLoader->LoadScript("app:///Scripts/fetch_polyfill.js"); m_scriptLoader->LoadScript("app:///Scripts/ammo.js"); // Commenting out recast.js for now because v8jsi is incompatible with asm.js. // m_scriptLoader->LoadScript("app:///Scripts/recast.js"); From 8ddd8c8113da463496fb76c41202fbb61dc671c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Branimir=20Karad=C5=BEi=C4=87?= Date: Fri, 15 May 2026 22:43:54 -0700 Subject: [PATCH 4/5] Add .env fallback for cubemap loads + re-enable 7 tests Babylon Native's NativeEngine.createCubeTexture override only handles .env single-file cubemaps and 6-face arrays; .dds / .ktx / .ktx2 URLs fall through to a throw ("Cannot load cubemap because 6 files were not defined"). The loader-aware path used by the WebGL engine (texture loader registry lookup) is bypassed entirely. Add a JS-side polyfill that detects .dds / .ktx / .ktx2 single-URL cubemap loads and retries with the .env extension. Babylon's CI generates both .dds and .env from the same source HDR and uploads them to the same CDN path, so the swap is transparent for the Babylon-hosted environments these tests reference. On 404 (no .env counterpart exists) the polyfill re-invokes the original code path, preserving the existing throw semantics. This unblocks 7 tests that were excluded because the throw aborted the scene before any rendering could happen: idx 141 NMEGLTF idx 172 Anisotropic idx 173 Clear Coat idx 246 PBRMetallicRoughnessMaterial idx 247 PBRSpecularGlossinessMaterial idx 248 PBR idx 290 Prepass SSAO + depth of field Strip excludeFromAutomaticTesting + reason from those 7 entries in config.json. All 7 validate sub-threshold on Win32 V8 D3D11 Release without --include-excluded after the strip (pixel diff range 308..2638, well under the 2.5% threshold). --- Apps/Playground/CMakeLists.txt | 1 + Apps/Playground/Scripts/config.json | 14 --- .../Scripts/cube_texture_polyfill.js | 113 ++++++++++++++++++ Apps/Playground/Shared/AppContext.cpp | 1 + 4 files changed, 115 insertions(+), 14 deletions(-) create mode 100644 Apps/Playground/Scripts/cube_texture_polyfill.js diff --git a/Apps/Playground/CMakeLists.txt b/Apps/Playground/CMakeLists.txt index e32911e26..15603a057 100644 --- a/Apps/Playground/CMakeLists.txt +++ b/Apps/Playground/CMakeLists.txt @@ -13,6 +13,7 @@ set(DEPENDENCIES "../Dependencies/recast.js") set(SCRIPTS + "Scripts/cube_texture_polyfill.js" "Scripts/dom_polyfill.js" "Scripts/experience.js" "Scripts/fetch_polyfill.js" diff --git a/Apps/Playground/Scripts/config.json b/Apps/Playground/Scripts/config.json index 2915afaf4..8029716fd 100644 --- a/Apps/Playground/Scripts/config.json +++ b/Apps/Playground/Scripts/config.json @@ -858,8 +858,6 @@ { "title": "NMEGLTF", "playgroundId": "#WGZLGJ#10320", - "excludeFromAutomaticTesting": true, - "reason": "Pixel comparison fails (more than 20% pixels differ)", "referenceImage": "nmegltf.png" }, { @@ -1056,15 +1054,11 @@ { "title": "Anisotropic", "playgroundId": "#MAXCNU#1", - "excludeFromAutomaticTesting": true, - "reason": "Pixel comparison fails (more than 20% pixels differ)", "referenceImage": "anisotropic.png" }, { "title": "Clear Coat", "playgroundId": "#YACNQS#2", - "excludeFromAutomaticTesting": true, - "reason": "Pixel comparison fails (more than 20% pixels differ)", "referenceImage": "clearCoat.png" }, { @@ -1575,22 +1569,16 @@ { "title": "PBRMetallicRoughnessMaterial", "playgroundId": "#2FDQT5#13", - "excludeFromAutomaticTesting": true, - "reason": "Pixel comparison fails (more than 20% pixels differ)", "referenceImage": "PBRMetallicRoughnessMaterial.png" }, { "title": "PBRSpecularGlossinessMaterial", "playgroundId": "#Z1VL3V#4", - "excludeFromAutomaticTesting": true, - "reason": "Pixel comparison fails (more than 20% pixels differ)", "referenceImage": "PBRSpecularGlossinessMaterial.png" }, { "title": "PBR", "playgroundId": "#LCA0Q4#27", - "excludeFromAutomaticTesting": true, - "reason": "Pixel comparison fails (more than 20% pixels differ)", "referenceImage": "pbr.png" }, { @@ -1873,8 +1861,6 @@ "title": "Prepass SSAO + depth of field", "playgroundId": "#8F5HYV#72", "renderCount": 10, - "excludeFromAutomaticTesting": true, - "reason": "Pixel comparison fails (more than 20% pixels differ)", "referenceImage": "prepass-ssao-dof.png" }, { diff --git a/Apps/Playground/Scripts/cube_texture_polyfill.js b/Apps/Playground/Scripts/cube_texture_polyfill.js new file mode 100644 index 000000000..f8440cb85 --- /dev/null +++ b/Apps/Playground/Scripts/cube_texture_polyfill.js @@ -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 .dds and +// .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"); + } +})(); diff --git a/Apps/Playground/Shared/AppContext.cpp b/Apps/Playground/Shared/AppContext.cpp index adc3a748a..5e54295ca 100644 --- a/Apps/Playground/Shared/AppContext.cpp +++ b/Apps/Playground/Shared/AppContext.cpp @@ -222,6 +222,7 @@ AppContext::AppContext( // Commenting out recast.js for now because v8jsi is incompatible with asm.js. // m_scriptLoader->LoadScript("app:///Scripts/recast.js"); m_scriptLoader->LoadScript("app:///Scripts/babylon.max.js"); + m_scriptLoader->LoadScript("app:///Scripts/cube_texture_polyfill.js"); m_scriptLoader->LoadScript("app:///Scripts/babylonjs.loaders.js"); m_scriptLoader->LoadScript("app:///Scripts/babylonjs.materials.js"); m_scriptLoader->LoadScript("app:///Scripts/babylon.gui.js"); From c5a1a864d2db7866d4274ee2e8f1b2cc2fbb6b4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Branimir=20Karad=C5=BEi=C4=87?= Date: Fri, 15 May 2026 23:30:28 -0700 Subject: [PATCH 5/5] Add ES2020+ -> ES2019 syntax repair polyfill for Chakra Babylon Native's Chakra-based builds parse only up to ES2019; modern Babylon.js playground snippets routinely use optional chaining (?.), nullish coalescing (??), numeric separators (1_000_000), and logical assignment (||= &&= ??=), causing 26 visual tests to fail with SyntaxError at the eval() site in validation_native.js. Add a lightweight regex-based syntax-repair polyfill loaded before babylon.max.js. The polyfill exposes __bnTranspileES2019(code) as a top-level global var. validation_native.js's new evalWithFallback helper catches SyntaxError on the first eval and retries once with the repaired source. Engines that already accept the source (V8, JSC) never hit the retry path. The polyfill is syntax-only (parse-time fix), not semantic - ?. is rewritten to . so a null target throws TypeError at runtime instead of short-circuiting to undefined. This is a deliberate trade-off: parse failures are 100% blocking; runtime TypeErrors are diagnosable and at worst surface a different bug that was previously hidden. Strip excludeFromAutomaticTesting from idx 403 (custom-handling-of-materials-for-render-target-pass) which now passes end-to-end. Updated reasons for the other 25 entries to describe their post-polyfill state: 9 hit runtime errors after parse repair, 12 hit asset/module gaps, 4 hit ES2022 syntax not covered by the polyfill (e.g. private class fields). --- Apps/Playground/CMakeLists.txt | 1 + Apps/Playground/Scripts/config.json | 52 ++++----- Apps/Playground/Scripts/es2019_transpile.js | 114 +++++++++++++++++++ Apps/Playground/Scripts/validation_native.js | 24 +++- Apps/Playground/Shared/AppContext.cpp | 1 + 5 files changed, 163 insertions(+), 29 deletions(-) create mode 100644 Apps/Playground/Scripts/es2019_transpile.js diff --git a/Apps/Playground/CMakeLists.txt b/Apps/Playground/CMakeLists.txt index 15603a057..6122396e2 100644 --- a/Apps/Playground/CMakeLists.txt +++ b/Apps/Playground/CMakeLists.txt @@ -15,6 +15,7 @@ set(DEPENDENCIES set(SCRIPTS "Scripts/cube_texture_polyfill.js" "Scripts/dom_polyfill.js" + "Scripts/es2019_transpile.js" "Scripts/experience.js" "Scripts/fetch_polyfill.js" "Scripts/playground_runner.js" diff --git a/Apps/Playground/Scripts/config.json b/Apps/Playground/Scripts/config.json index 8029716fd..62a22d4bf 100644 --- a/Apps/Playground/Scripts/config.json +++ b/Apps/Playground/Scripts/config.json @@ -1465,7 +1465,7 @@ "playgroundId": "#8NTR5X#8", "replace": "//options//, roundtrip = true;", "excludeFromAutomaticTesting": true, - "reason": "Pixel comparison fails (more than 20% pixels differ)", + "reason": "Chakra parse fails on ES2022 syntax not handled by ES2019 polyfill (e.g. private class fields).", "referenceImage": "glTFSerializerTextureExport.png" }, { @@ -2212,7 +2212,7 @@ "title": "Material Plugin", "playgroundId": "#22HT5Z#10", "excludeFromAutomaticTesting": true, - "reason": "Pixel comparison fails (more than 20% pixels differ)", + "reason": "Chakra parse fails on ES2022 syntax not handled by ES2019 polyfill (e.g. private class fields).", "referenceImage": "materialPlugin.png" }, { @@ -2289,7 +2289,7 @@ "playgroundId": "#PCMH7A#2", "renderCount": 120, "excludeFromAutomaticTesting": true, - "reason": "Pixel comparison fails (more than 20% pixels differ)", + "reason": "Chakra parse fails on ES2022 syntax not handled by ES2019 polyfill (e.g. private class fields).", "referenceImage": "fluidBoxSphere.png" }, { @@ -2654,8 +2654,6 @@ "title": "custom-handling-of-materials-for-render-target-pass", "playgroundId": "#FIVL25#21", "renderCount": 60, - "excludeFromAutomaticTesting": true, - "reason": "Pixel comparison fails (more than 20% pixels differ)", "referenceImage": "custom-handling-of-materials-for-render-target-pass.png" }, { @@ -2745,7 +2743,7 @@ "renderCount": 2, "replace": "//options//, modelIndex = 2;", "excludeFromAutomaticTesting": true, - "reason": "Test crashes or hangs on Babylon Native", + "reason": "ES2020+ ?. parsed via ES2019 polyfill; runtime TypeError from .stop() on optional null field.", "referenceImage": "computeMaxExtents-BoxAnimated.png" }, { @@ -2754,7 +2752,7 @@ "renderCount": 2, "replace": "//options//, modelIndex = 4; animationIndex = 0;", "excludeFromAutomaticTesting": true, - "reason": "Test crashes or hangs on Babylon Native", + "reason": "ES2020+ ?. parsed via ES2019 polyfill; runtime TypeError from .stop() on optional null field.", "referenceImage": "computeMaxExtents-Fox0.png" }, { @@ -2763,7 +2761,7 @@ "renderCount": 2, "replace": "//options//, modelIndex = 4; animationIndex = 1;", "excludeFromAutomaticTesting": true, - "reason": "Test crashes or hangs on Babylon Native", + "reason": "ES2020+ ?. parsed via ES2019 polyfill; runtime TypeError from .stop() on optional null field.", "referenceImage": "computeMaxExtents-Fox1.png" }, { @@ -2772,7 +2770,7 @@ "renderCount": 2, "replace": "//options//, modelIndex = 5;", "excludeFromAutomaticTesting": true, - "reason": "Test crashes or hangs on Babylon Native", + "reason": "ES2020+ ?. parsed via ES2019 polyfill; runtime TypeError from .stop() on optional null field.", "referenceImage": "computeMaxExtents-MorphStressTest.png" }, { @@ -2883,7 +2881,7 @@ "title": "FrameGraph nrge custom rendering", "playgroundId": "#1QCA2M#35", "excludeFromAutomaticTesting": true, - "reason": "Pixel comparison fails (more than 20% pixels differ)", + "reason": "Parse repaired via ES2019 polyfill; runtime fails (asset/URL load issue in FrameGraph snippet).", "referenceImage": "FrameGraph-nrge-custom-rendering.png" }, { @@ -2897,14 +2895,14 @@ "title": "FrameGraph nrge glow layer", "playgroundId": "#IG8NRC#84", "excludeFromAutomaticTesting": true, - "reason": "Pixel comparison fails (more than 20% pixels differ)", + "reason": "Parse repaired via ES2019 polyfill; runtime fails: cubemap files not defined (asset gap).", "referenceImage": "FrameGraph-nrge-glow-layer.png" }, { "title": "FrameGraph nrge highlight layer", "playgroundId": "#QZYNMK#3", "excludeFromAutomaticTesting": true, - "reason": "Pixel comparison fails (more than 20% pixels differ)", + "reason": "Parse repaired via ES2019 polyfill; runtime fails (asset/URL load issue in FrameGraph snippet).", "referenceImage": "FrameGraph-nrge-highlight-layer.png" }, { @@ -2962,7 +2960,7 @@ "title": "FrameGraph nrge rig camera", "playgroundId": "#ATL1CS#19", "excludeFromAutomaticTesting": true, - "reason": "Pixel comparison fails (more than 20% pixels differ)", + "reason": "Parse repaired via ES2019 polyfill; runtime fails (asset/URL load issue in FrameGraph snippet).", "referenceImage": "FrameGraph-nrge-rig-camera.png" }, { @@ -2990,7 +2988,7 @@ "title": "FrameGraph nrge volumetric lighting", "playgroundId": "#3VH0AC#2", "excludeFromAutomaticTesting": true, - "reason": "Pixel comparison fails (more than 20% pixels differ)", + "reason": "Parse repaired via ES2019 polyfill; runtime hangs (FrameGraph volumetric lighting).", "referenceImage": "FrameGraph-nrge-volumetric-lighting.png" }, { @@ -3018,14 +3016,14 @@ "title": "FrameGraph nrge transmission", "playgroundId": "#ZNTBN2#10", "excludeFromAutomaticTesting": true, - "reason": "Pixel comparison fails (more than 20% pixels differ)", + "reason": "Chakra parse fails on ES2022 syntax not handled by ES2019 polyfill (e.g. private class fields).", "referenceImage": "FrameGraph-nrge-transmission.png" }, { "title": "FrameGraph nrge selection outline layer", "playgroundId": "#ADUC74#1", "excludeFromAutomaticTesting": true, - "reason": "Pixel comparison fails (more than 20% pixels differ)", + "reason": "Parse repaired via ES2019 polyfill; runtime fails (asset/URL load issue in FrameGraph snippet).", "referenceImage": "FrameGraph-nrge-selection-outline-layer.png" }, { @@ -3133,7 +3131,7 @@ "title": "FrameGraph volumetric lighting", "playgroundId": "#3VH0AC", "excludeFromAutomaticTesting": true, - "reason": "Test crashes or hangs on Babylon Native", + "reason": "Parse repaired via ES2019 polyfill; runtime TypeError (FrameGraph volumetric lighting).", "referenceImage": "FrameGraph-volumetric-lighting.png" }, { @@ -3154,14 +3152,14 @@ "title": "FrameGraph selection outline", "playgroundId": "#E1F0GP#4", "excludeFromAutomaticTesting": true, - "reason": "Pixel comparison fails (more than 20% pixels differ)", + "reason": "Parse repaired via ES2019 polyfill; runtime fails: Unknown error opening URL.", "referenceImage": "FrameGraph-selection-outline.png" }, { "title": "Render target texture with clustered lights", "playgroundId": "#1QCA2M#11", "excludeFromAutomaticTesting": true, - "reason": "Pixel comparison fails (more than 20% pixels differ)", + "reason": "Parse repaired via ES2019 polyfill; runtime fails: texture data size mismatch (clustered lighting gap).", "referenceImage": "Render-target-texture-with-clustered-lights.png" }, { @@ -3283,14 +3281,14 @@ "title": "Sponza Clustered Lighting", "playgroundId": "#CSCJO2#17", "excludeFromAutomaticTesting": true, - "reason": "Pixel comparison fails (more than 20% pixels differ)", + "reason": "Parse repaired via ES2019 polyfill; runtime fails: texture data size mismatch (clustered lighting gap).", "referenceImage": "sponza-clustered-lighting.png" }, { "title": "Sponza Clustered Lighting (2 viewports)", "playgroundId": "#CSCJO2#20", "excludeFromAutomaticTesting": true, - "reason": "Pixel comparison fails (more than 20% pixels differ)", + "reason": "Parse repaired via ES2019 polyfill; runtime fails: texture data size mismatch (clustered lighting gap).", "referenceImage": "sponza-clustered-lighting-viewports.png" }, { @@ -3379,28 +3377,28 @@ "title": "Atmosphere Day", "playgroundId": "#VO1Z0C#39", "excludeFromAutomaticTesting": true, - "reason": "Pixel comparison fails (more than 20% pixels differ)", + "reason": "Requires babylonjs-addons (ADDONS.Atmosphere); not shipped by Babylon Native.", "referenceImage": "atmosphere-day.png" }, { "title": "Atmosphere Day (Planet Origin)", "playgroundId": "#VO1Z0C#40", "excludeFromAutomaticTesting": true, - "reason": "Pixel comparison fails (more than 20% pixels differ)", + "reason": "Requires babylonjs-addons (ADDONS.Atmosphere); not shipped by Babylon Native.", "referenceImage": "atmosphere-day.png" }, { "title": "Atmosphere Day (Ray Marching)", "playgroundId": "#VO1Z0C#41", "excludeFromAutomaticTesting": true, - "reason": "Pixel comparison fails (more than 20% pixels differ)", + "reason": "Requires babylonjs-addons (ADDONS.Atmosphere); not shipped by Babylon Native.", "referenceImage": "atmosphere-day-ray-marching.png" }, { "title": "Atmosphere Sunset", "playgroundId": "#VO1Z0C#42", "excludeFromAutomaticTesting": true, - "reason": "Pixel comparison fails (more than 20% pixels differ)", + "reason": "Requires babylonjs-addons (ADDONS.Atmosphere); not shipped by Babylon Native.", "referenceImage": "atmosphere-sunset.png" }, { @@ -4063,7 +4061,7 @@ "renderCount": 180, "useLargeWorldRendering": true, "excludeFromAutomaticTesting": true, - "reason": "Test crashes or hangs on Babylon Native", + "reason": "Parse repaired via ES2019 polyfill; runtime fails: 'HK' (Havok) global not defined.", "referenceImage": "Havok-FloatingOrigin-Multi-Region.png" }, { @@ -4466,7 +4464,7 @@ "title": "Selection outline layer with instances", "playgroundId": "#UR9706#0", "excludeFromAutomaticTesting": true, - "reason": "Pixel comparison fails (more than 20% pixels differ)", + "reason": "Parse repaired via ES2019 polyfill; runtime fails (asset/URL load issue).", "referenceImage": "Selection-outline-layer-with-instances.png" }, { diff --git a/Apps/Playground/Scripts/es2019_transpile.js b/Apps/Playground/Scripts/es2019_transpile.js new file mode 100644 index 000000000..471811c7c --- /dev/null +++ b/Apps/Playground/Scripts/es2019_transpile.js @@ -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; +})(); diff --git a/Apps/Playground/Scripts/validation_native.js b/Apps/Playground/Scripts/validation_native.js index 130d9a122..6943d8b8c 100644 --- a/Apps/Playground/Scripts/validation_native.js +++ b/Apps/Playground/Scripts/validation_native.js @@ -45,6 +45,26 @@ done(false); } + // Run `eval(src)` directly. If the host engine throws a SyntaxError + // (e.g. Chakra rejecting ?. ?? or numeric separators) and the + // __bnTranspileES2019 helper is available, retry once with an ES2019 + // syntax-repaired version of the source. Re-throws on any other error. + function evalWithFallback(src, test) { + try { + return eval(src); + } catch (e) { + if (e instanceof SyntaxError && typeof __bnTranspileES2019 === "function") { + const repaired = __bnTranspileES2019(src); + if (repaired !== src) { + const title = test && test.title ? test.title : "(unknown)"; + console.log("Retrying '" + title + "' after ES2019 syntax repair (host engine lacks ES2020+ parse support)."); + return eval(repaired); + } + } + throw e; + } + } + // Per-run counters surfaced as a final summary line on exit. let ranCount = 0; let passedCount = 0; @@ -363,7 +383,7 @@ } } - currentScene = eval(code + "\r\ncreateScene(engine)"); + currentScene = evalWithFallback(code + "\r\ncreateScene(engine)", test); if (currentScene.then) { // Handle if createScene returns a promise @@ -434,7 +454,7 @@ } } - currentScene = eval(scriptToRun + test.functionToCall + "(engine)"); + currentScene = evalWithFallback(scriptToRun + test.functionToCall + "(engine)", test); processCurrentScene(test, renderImage, done, compareFunction); } catch (e) { diff --git a/Apps/Playground/Shared/AppContext.cpp b/Apps/Playground/Shared/AppContext.cpp index 5e54295ca..3c7b378ee 100644 --- a/Apps/Playground/Shared/AppContext.cpp +++ b/Apps/Playground/Shared/AppContext.cpp @@ -216,6 +216,7 @@ AppContext::AppContext( }); m_scriptLoader.emplace(*m_runtime); + m_scriptLoader->LoadScript("app:///Scripts/es2019_transpile.js"); m_scriptLoader->LoadScript("app:///Scripts/dom_polyfill.js"); m_scriptLoader->LoadScript("app:///Scripts/fetch_polyfill.js"); m_scriptLoader->LoadScript("app:///Scripts/ammo.js");