From 6d6071e287e67b4574113049b72fc38d8d2430bb 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 1/2] 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 90ddb81c8..0fdd1288e 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/experience.js" "Scripts/playground_runner.js" "Scripts/validation_native.js" diff --git a/Apps/Playground/Scripts/config.json b/Apps/Playground/Scripts/config.json index e0022cf38..12891d9e0 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 09d9a408a..57a526f25 100644 --- a/Apps/Playground/Shared/AppContext.cpp +++ b/Apps/Playground/Shared/AppContext.cpp @@ -217,6 +217,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 60a8e0a68a41d46c48719d30f07d448e7fad9863 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Branimir=20Karad=C5=BEi=C4=87?= Date: Wed, 27 May 2026 16:20:28 -0700 Subject: [PATCH 2/2] PR #1710: replace cube_texture_polyfill.js with native CubeTexture polyfill Per review: this polyfill should be available to every Babylon Native consumer using Chakra, not just the Playground app. Move it from a Playground-only LoadScript() of cube_texture_polyfill.js into a proper C++ polyfill library under Polyfills/ that any embedder can link. The polyfill patches BABYLON.NativeEngine.prototype.createCubeTexture to transparently retry single-URL .dds / .ktx / .ktx2 cubemap loads (which BN does not currently handle) as their .env counterpart that Babylon's CDN co-hosts at the same path. On 404 it falls through to the original (which throws), preserving existing error semantics. Implementation pattern follows the existing ErrorCause / StringTrim native polyfills: - New Polyfills/CubeTexture/ library. - Public header Babylon/Polyfills/CubeTexture.h declares void BABYLON_API Initialize(Napi::Env env). - Source/CubeTexture.cpp embeds the polyfill IIFE verbatim from the prior cube_texture_polyfill.js (same semantics, same idempotency marker __cubeTexturePolyfillInstalled) and dispatches it via the free function Napi::Eval(env, source, url). Napi::Eval is declared by every engine-specific across both N-API trees (Chakra / V8 / JavaScriptCore in Core/Node-API/Include/Engine// and JSI in Core/Node-API-JSI/Include/napi/) so the same source builds uniformly across all backends; no __has_include switch needed for this entry point. Wiring: - Top-level CMakeLists.txt: add BABYLON_NATIVE_POLYFILL_CUBETEXTURE option (default ON). - Polyfills/CMakeLists.txt: guarded add_subdirectory(CubeTexture). - Apps/Playground/CMakeLists.txt and Apps/Playground/Android/BabylonNative/CMakeLists.txt: PRIVATE link CubeTexture into the Playground / BabylonNativeJNI executables so AppContext.cpp can use the published header on every platform. - Apps/Playground/Shared/AppContext.cpp: include the new header and replace LoadScript("app:///Scripts/cube_texture_polyfill.js"); with Dispatch([](Napi::Env env) { Babylon::Polyfills::CubeTexture::Initialize(env); }); scheduled right after LoadScript("babylon.max.js"). ScriptLoader's Dispatch and LoadScript queue onto the same ordered task chain (see jsruntimehost ScriptLoader::Impl), so Initialize is guaranteed to run after babylon.max.js finishes evaluating - same timing the prior LoadScript provided. Delete Apps/Playground/Scripts/cube_texture_polyfill.js (no longer shipped). Verified locally on Chakra build (build/win32 RelWithDebInfo, headless) - all 7 cubemap-affected tests re-enabled by this PR pass: Clear Coat diff=519 px PBR diff=482 px (12 sub-tests skipped, expected) Anisotropic diff=308 px PBRMetallicRoughnessMaterial diff=482 px NMEGLTF diff=631 px PBRSpecularGlossinessMaterial diff=522 px Console log confirms the polyfill ran: [CubeTexture polyfill] NativeEngine.createCubeTexture patched with .env fallback Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Android/BabylonNative/CMakeLists.txt | 1 + Apps/Playground/CMakeLists.txt | 2 +- Apps/Playground/Shared/AppContext.cpp | 9 +++- CMakeLists.txt | 1 + Polyfills/CMakeLists.txt | 4 ++ Polyfills/CubeTexture/CMakeLists.txt | 16 ++++++ .../Include/Babylon/Polyfills/CubeTexture.h | 31 +++++++++++ .../CubeTexture/Source/CubeTexture.cpp | 53 +++++++++++++------ 8 files changed, 99 insertions(+), 18 deletions(-) create mode 100644 Polyfills/CubeTexture/CMakeLists.txt create mode 100644 Polyfills/CubeTexture/Include/Babylon/Polyfills/CubeTexture.h rename Apps/Playground/Scripts/cube_texture_polyfill.js => Polyfills/CubeTexture/Source/CubeTexture.cpp (65%) diff --git a/Apps/Playground/Android/BabylonNative/CMakeLists.txt b/Apps/Playground/Android/BabylonNative/CMakeLists.txt index 3f1b5ee15..439fd2c5d 100644 --- a/Apps/Playground/Android/BabylonNative/CMakeLists.txt +++ b/Apps/Playground/Android/BabylonNative/CMakeLists.txt @@ -30,6 +30,7 @@ target_link_libraries(BabylonNativeJNI PRIVATE Blob PRIVATE Canvas PRIVATE Console + PRIVATE CubeTexture PRIVATE File PRIVATE GraphicsDevice PRIVATE NativeCamera diff --git a/Apps/Playground/CMakeLists.txt b/Apps/Playground/CMakeLists.txt index 0fdd1288e..da1b3c862 100644 --- a/Apps/Playground/CMakeLists.txt +++ b/Apps/Playground/CMakeLists.txt @@ -13,7 +13,6 @@ set(DEPENDENCIES "../Dependencies/recast.js") set(SCRIPTS - "Scripts/cube_texture_polyfill.js" "Scripts/experience.js" "Scripts/playground_runner.js" "Scripts/validation_native.js" @@ -140,6 +139,7 @@ target_link_libraries(Playground PRIVATE bx PRIVATE Canvas PRIVATE Console + PRIVATE CubeTexture PRIVATE ExternalTexture PRIVATE File PRIVATE GraphicsDevice diff --git a/Apps/Playground/Shared/AppContext.cpp b/Apps/Playground/Shared/AppContext.cpp index 57a526f25..04542eb6e 100644 --- a/Apps/Playground/Shared/AppContext.cpp +++ b/Apps/Playground/Shared/AppContext.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -217,7 +218,13 @@ 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"); + // CubeTexture polyfill must run AFTER babylon.max.js is evaluated because + // it patches BABYLON.NativeEngine.prototype.createCubeTexture. The + // ScriptLoader's Dispatch is ordered against LoadScript on the same task + // chain, so this is guaranteed to run after babylon.max.js completes. + m_scriptLoader->Dispatch([](Napi::Env env) { + Babylon::Polyfills::CubeTexture::Initialize(env); + }); m_scriptLoader->LoadScript("app:///Scripts/babylonjs.loaders.js"); m_scriptLoader->LoadScript("app:///Scripts/babylonjs.materials.js"); m_scriptLoader->LoadScript("app:///Scripts/babylon.gui.js"); diff --git a/CMakeLists.txt b/CMakeLists.txt index a544a8d52..04bf6734a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -142,6 +142,7 @@ option(BABYLON_NATIVE_PLUGIN_TESTUTILS "Include Babylon Native Plugin TestUtils. # Polyfills option(BABYLON_NATIVE_POLYFILL_WINDOW "Include Babylon Native Polyfill Window." ON) option(BABYLON_NATIVE_POLYFILL_CANVAS "Include Babylon Native Polyfill Canvas." ON) +option(BABYLON_NATIVE_POLYFILL_CUBETEXTURE "Include Babylon Native Polyfill CubeTexture (.dds/.ktx/.ktx2 -> .env fallback for NativeEngine.createCubeTexture)." ON) # Sanitizers option(ENABLE_SANITIZERS "Enable AddressSanitizer and UBSan" OFF) diff --git a/Polyfills/CMakeLists.txt b/Polyfills/CMakeLists.txt index 16c1b767f..75ebd0cd9 100644 --- a/Polyfills/CMakeLists.txt +++ b/Polyfills/CMakeLists.txt @@ -5,3 +5,7 @@ endif() if(BABYLON_NATIVE_POLYFILL_CANVAS) add_subdirectory(Canvas) endif() + +if(BABYLON_NATIVE_POLYFILL_CUBETEXTURE) + add_subdirectory(CubeTexture) +endif() diff --git a/Polyfills/CubeTexture/CMakeLists.txt b/Polyfills/CubeTexture/CMakeLists.txt new file mode 100644 index 000000000..a5c614047 --- /dev/null +++ b/Polyfills/CubeTexture/CMakeLists.txt @@ -0,0 +1,16 @@ +set(SOURCES + "Include/Babylon/Polyfills/CubeTexture.h" + "Source/CubeTexture.cpp") + +add_library(CubeTexture ${SOURCES}) +warnings_as_errors(CubeTexture) + +target_include_directories(CubeTexture + PUBLIC "Include") + +target_link_libraries(CubeTexture + PUBLIC napi + PUBLIC Foundation) + +set_property(TARGET CubeTexture PROPERTY FOLDER Polyfills) +source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${SOURCES}) diff --git a/Polyfills/CubeTexture/Include/Babylon/Polyfills/CubeTexture.h b/Polyfills/CubeTexture/Include/Babylon/Polyfills/CubeTexture.h new file mode 100644 index 000000000..c88f1406a --- /dev/null +++ b/Polyfills/CubeTexture/Include/Babylon/Polyfills/CubeTexture.h @@ -0,0 +1,31 @@ +#pragma once + +#include +#include + +namespace Babylon::Polyfills::CubeTexture +{ + // Patches BABYLON.NativeEngine.prototype.createCubeTexture to transparently + // retry .dds / .ktx / .ktx2 single-URL cubemap loads as .env (a format BN + // does support). Babylon's CDN co-hosts both, so the swap is invisible to + // consumer JS. Falls back to the original (which throws "Cannot load + // cubemap because 6 files were not defined") on 404 - preserving existing + // error semantics. + // + // Call Initialize AFTER babylon.max.js has been evaluated; the patch + // requires BABYLON.NativeEngine.prototype to exist. When using the + // jsruntimehost ScriptLoader, the simplest pattern is: + // + // scriptLoader.LoadScript("app:///Scripts/babylon.max.js"); + // scriptLoader.Dispatch([](Napi::Env env) { + // Babylon::Polyfills::CubeTexture::Initialize(env); + // }); + // + // The Dispatch queues onto the same ordered task chain as LoadScript, so + // Initialize is guaranteed to run after babylon.max.js finishes evaluating. + // + // This is a tactical bridge until Plugins/NativeEngine wires a proper + // loader registry path for cubemap formats. Safe to call multiple times; + // the patch is idempotent. + void BABYLON_API Initialize(Napi::Env env); +} diff --git a/Apps/Playground/Scripts/cube_texture_polyfill.js b/Polyfills/CubeTexture/Source/CubeTexture.cpp similarity index 65% rename from Apps/Playground/Scripts/cube_texture_polyfill.js rename to Polyfills/CubeTexture/Source/CubeTexture.cpp index f8440cb85..b090d465b 100644 --- a/Apps/Playground/Scripts/cube_texture_polyfill.js +++ b/Polyfills/CubeTexture/Source/CubeTexture.cpp @@ -1,17 +1,22 @@ -// 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. - +#include + +#include + +namespace Babylon::Polyfills::CubeTexture +{ + namespace + { + // Embedded polyfill source. Kept verbatim from the original Playground + // cube_texture_polyfill.js (PR #1710 v0) so behaviour stays identical: + // - Patches BABYLON.NativeEngine.prototype.createCubeTexture. + // - Triggers only when the URL ends in .dds / .ktx / .ktx2 and no + // forcedExtension, 6-file array, or raw buffer was supplied. + // - Retries via NativeEngine._loadFile to fetch the .env counterpart; + // on 404 falls back to the original (which throws), preserving the + // existing failure mode. + // - Idempotent via the __cubeTexturePolyfillInstalled marker on the + // prototype. + constexpr const char* JS_SOURCE = R"javascript( (function () { "use strict"; @@ -92,7 +97,7 @@ }); }; - var onEnvFailed = function (request, exception) { + 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); }); @@ -108,6 +113,22 @@ }; if (typeof console !== "undefined" && console.log) { - console.log("[cube_texture_polyfill] NativeEngine.createCubeTexture patched with .env fallback"); + console.log("[CubeTexture polyfill] NativeEngine.createCubeTexture patched with .env fallback"); } })(); +)javascript"; + + constexpr const char* JS_SOURCE_URL = "babylon-native://polyfills/CubeTexture.js"; + } + + void Initialize(Napi::Env env) + { + // The free function Napi::Eval(env, source, url) is declared by every + // engine-specific across both N-API trees (Chakra, V8, + // JavaScriptCore in Core/Node-API/Include/Engine//, and JSI in + // Core/Node-API-JSI/Include/napi/). Using it uniformly avoids the + // Shared-only env.RunScript() which is missing from the JSI tree. + Napi::HandleScope scope{env}; + Napi::Eval(env, JS_SOURCE, JS_SOURCE_URL); + } +}