From 6264dea12cf75a885bf1ce5a274b0adbc434ab0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Branimir=20Karad=C5=BEi=C4=87?= Date: Mon, 1 Jun 2026 09:00:47 -0700 Subject: [PATCH 1/3] Playground: link AbortController polyfill + extend document shim - Initialize AbortController polyfill in AppContext alongside the other JsRuntimeHost polyfills and link it into the Win32 / Android Playground binaries (Android JNI link fix for BabylonNativeJNI). - Extend the validation_native.js `document` shim with `createEvent` and `dispatchEvent` so playgrounds that synthesize DOM events stop tripping `document.dispatchEvent is not a function`. - Re-enable the one playground (serialization round-trip) that was previously quarantined for that error. Note on review history: an earlier revision of this PR also introduced native `TextEncoder` and `PointerEvent` polyfills directly under `Polyfills/`. Per review feedback those have been split off: - `TextEncoder` belongs alongside `TextDecoder` in JsRuntimeHost (WHATWG Encoding Standard); proposed there in BabylonJS/JsRuntimeHost#171 and will be wired into Playground in a follow-up once that lands. - `PointerEvent` was dropped pending an offline discussion about whether BabylonNative should polyfill DOM input types at all when `DeviceInputSystem` already exists. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Android/BabylonNative/CMakeLists.txt | 1 + Apps/Playground/CMakeLists.txt | 1 + Apps/Playground/Scripts/config.json | 2 - Apps/Playground/Scripts/validation_native.js | 47 ++++++++++++++++++- Apps/Playground/Shared/AppContext.cpp | 3 ++ 5 files changed, 50 insertions(+), 4 deletions(-) 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 diff --git a/Apps/Playground/CMakeLists.txt b/Apps/Playground/CMakeLists.txt index 90ddb81c8..a7fefc137 100644 --- a/Apps/Playground/CMakeLists.txt +++ b/Apps/Playground/CMakeLists.txt @@ -134,6 +134,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/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..a963f2087 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); From 5e2fec841ff00423a5c036e0c0beb1c0648b8e04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Branimir=20Karad=C5=BEi=C4=87?= Date: Mon, 1 Jun 2026 12:51:29 -0700 Subject: [PATCH 2/3] Playground: wire TextEncoder polyfill from JsRuntimeHost JsRuntimeHost #171 added a TextEncoder polyfill alongside the existing TextDecoder one (per @bghgary's review feedback on this PR, where the polyfill originally lived in BN itself). Bump the JsRuntimeHost pin and link / initialize TextEncoder the same way TextDecoder is linked / initialized. Pin bump: 1e9b17e4 (Bump UrlLib pin) -> 81b01d2e (Add TextEncoder polyfill) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Apps/Playground/Android/BabylonNative/CMakeLists.txt | 1 + Apps/Playground/CMakeLists.txt | 1 + Apps/Playground/Shared/AppContext.cpp | 3 +++ 3 files changed, 5 insertions(+) diff --git a/Apps/Playground/Android/BabylonNative/CMakeLists.txt b/Apps/Playground/Android/BabylonNative/CMakeLists.txt index 0ab5ba294..00229f72a 100644 --- a/Apps/Playground/Android/BabylonNative/CMakeLists.txt +++ b/Apps/Playground/Android/BabylonNative/CMakeLists.txt @@ -46,5 +46,6 @@ target_link_libraries(BabylonNativeJNI PRIVATE ShaderCache PRIVATE TestUtils PRIVATE TextDecoder + PRIVATE TextEncoder PRIVATE Window PRIVATE XMLHttpRequest) diff --git a/Apps/Playground/CMakeLists.txt b/Apps/Playground/CMakeLists.txt index a7fefc137..975aa2a6f 100644 --- a/Apps/Playground/CMakeLists.txt +++ b/Apps/Playground/CMakeLists.txt @@ -155,6 +155,7 @@ target_link_libraries(Playground PRIVATE ShaderCache PRIVATE TestUtils PRIVATE TextDecoder + PRIVATE TextEncoder PRIVATE Window PRIVATE XMLHttpRequest ${ADDITIONAL_LIBRARIES} diff --git a/Apps/Playground/Shared/AppContext.cpp b/Apps/Playground/Shared/AppContext.cpp index a963f2087..0e44588b1 100644 --- a/Apps/Playground/Shared/AppContext.cpp +++ b/Apps/Playground/Shared/AppContext.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include #include @@ -190,6 +191,8 @@ AppContext::AppContext( Babylon::Polyfills::TextDecoder::Initialize(env); + Babylon::Polyfills::TextEncoder::Initialize(env); + Babylon::Polyfills::XMLHttpRequest::Initialize(env); m_canvas.emplace(Babylon::Polyfills::Canvas::Initialize(env)); From 82e555a393e14aeea77ad035623542f7cccdfa0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Branimir=20Karad=C5=BEi=C4=87?= Date: Thu, 11 Jun 2026 10:52:46 -0700 Subject: [PATCH 3/3] Playground: round-trip serialization in-memory instead of DOM shim Per review, BabylonNative should not fake DOM APIs so DOM-dependent content can run. The "Serialize scene without materials" playground (#PH4DEZ) only used document (createElement('a') + createEvent + dispatchEvent) to trigger a file download with no visual effect, which is why it needed the expanded document shim. - #PH4DEZ now does a real in-memory round-trip (serialize -> reload via SceneLoader.ImportMeshAsync("data:" + json) -> render the deserialized scene), modeled on the glTF serializer #KU72PX. This validates serialization correctness with no DOM dependency. The browser-only download is kept but guarded on document.createEvent. config.json repointed #PH4DEZ#1 -> #PH4DEZ#4. (Babylon.js visual suite to repoint to the same revision.) - Reverted the validation_native.js document-shim expansion (createEvent / initEvent / element-stub dispatchEvent / document-level dispatchEvent / addEventListener / body) back to the original minimal shim. Verified locally on Win32 D3D11: the test renders the deserialized scene and passes pixel comparison (248 px diff, well within tolerance). --- Apps/Playground/Scripts/config.json | 2 +- Apps/Playground/Scripts/validation_native.js | 47 +------------------- 2 files changed, 3 insertions(+), 46 deletions(-) diff --git a/Apps/Playground/Scripts/config.json b/Apps/Playground/Scripts/config.json index 2915afaf4..1bf99ce26 100644 --- a/Apps/Playground/Scripts/config.json +++ b/Apps/Playground/Scripts/config.json @@ -2257,7 +2257,7 @@ }, { "title": "Serialize scene without materials", - "playgroundId": "#PH4DEZ#1", + "playgroundId": "#PH4DEZ#4", "referenceImage": "serializeWithoutMaterials.png" }, { diff --git a/Apps/Playground/Scripts/validation_native.js b/Apps/Playground/Scripts/validation_native.js index 130d9a122..686280208 100644 --- a/Apps/Playground/Scripts/validation_native.js +++ b/Apps/Playground/Scripts/validation_native.js @@ -525,52 +525,9 @@ if (type === "canvas") { return new OffscreenCanvas(64, 64); } - // 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; + return {}; }, - addEventListener: function () { }, - removeEventListener: function () { }, - dispatchEvent: function () { return true; }, - body: { appendChild: function (c) { return c; }, removeChild: function (c) { return c; } }, + removeEventListener: function () { } } const xhr = new XMLHttpRequest();