Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions Apps/Playground/Android/BabylonNative/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ target_link_libraries(BabylonNativeJNI
PRIVATE Blob
PRIVATE Canvas
PRIVATE Console
PRIVATE ErrorCause
PRIVATE GraphicsDevice
PRIVATE NativeCamera
PRIVATE NativeCapture
Expand All @@ -44,5 +45,6 @@ target_link_libraries(BabylonNativeJNI
PRIVATE ShaderCache
PRIVATE TestUtils
PRIVATE TextDecoder
PRIVATE StringTrim
PRIVATE Window
PRIVATE XMLHttpRequest)
3 changes: 3 additions & 0 deletions Apps/Playground/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
FILE(GLOB REFERENCE_IMAGES RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}" "ReferenceImages/*")

set(BABYLON_SCRIPTS
"../node_modules/babylonjs-addons/babylonjs.addons.js"
"../node_modules/babylonjs-loaders/babylonjs.loaders.js"
"../node_modules/babylonjs/babylon.max.js"
"../node_modules/babylonjs-materials/babylonjs.materials.js"
Expand Down Expand Up @@ -139,6 +140,7 @@ target_link_libraries(Playground
PRIVATE bx
PRIVATE Canvas
PRIVATE Console
PRIVATE ErrorCause
PRIVATE ExternalTexture
PRIVATE GraphicsDevice
PRIVATE NativeCamera
Expand All @@ -151,6 +153,7 @@ target_link_libraries(Playground
PRIVATE Performance
PRIVATE ScriptLoader
PRIVATE ShaderCache
PRIVATE StringTrim
PRIVATE TestUtils
PRIVATE TextDecoder
PRIVATE Window
Expand Down
12 changes: 12 additions & 0 deletions Apps/Playground/Shared/AppContext.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@
#include <Babylon/Polyfills/Blob.h>
#include <Babylon/Polyfills/Canvas.h>
#include <Babylon/Polyfills/Console.h>
#include <Babylon/Polyfills/ErrorCause.h>
#include <Babylon/Polyfills/Performance.h>
#include <Babylon/Polyfills/StringTrim.h>
#include <Babylon/Polyfills/TextDecoder.h>
#include <Babylon/Polyfills/Window.h>
#include <Babylon/Polyfills/XMLHttpRequest.h>
Expand Down Expand Up @@ -157,6 +159,15 @@ AppContext::AppContext(

Babylon::Polyfills::Blob::Initialize(env);

// Chakra compatibility shims must run before any other JS so that
// Babylon.js shader-compile error rethrows surface their real message
// (ES2022 Error options bag) and ShaderProcessor / NodeMaterial code
// paths that call String.prototype.trimEnd (ES2019) do not throw on
// Chakra-based BN runtimes. Both are no-ops on engines that already
// implement the relevant spec.
Babylon::Polyfills::ErrorCause::Initialize(env);
Babylon::Polyfills::StringTrim::Initialize(env);

Babylon::Polyfills::Console::Initialize(env, [env, debugLog](const char* message, Babylon::Polyfills::Console::LogLevel logLevel) {
std::ostringstream ss{};
ss << "[" << GetLogLevelString(logLevel) << "] " << message;
Expand Down Expand Up @@ -215,6 +226,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/babylonjs.addons.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");
Expand Down
10 changes: 10 additions & 0 deletions Apps/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Apps/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
},
"dependencies": {
"babylonjs": "^9.3.4",
"babylonjs-addons": "^9.3.4",
Comment thread
bkaradzic-microsoft marked this conversation as resolved.
"babylonjs-gltf2interface": "^9.3.4",
"babylonjs-gui": "^9.3.4",
"babylonjs-loaders": "^9.3.4",
Expand Down
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,8 @@ 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_ERRORCAUSE "Include Babylon Native Polyfill ErrorCause (ES2022 Error options)." ON)
option(BABYLON_NATIVE_POLYFILL_STRINGTRIM "Include Babylon Native Polyfill StringTrim (ES2019 trimStart/trimEnd)." ON)

# Sanitizers
option(ENABLE_SANITIZERS "Enable AddressSanitizer and UBSan" OFF)
Expand Down
8 changes: 8 additions & 0 deletions Polyfills/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,11 @@ endif()
if(BABYLON_NATIVE_POLYFILL_CANVAS)
add_subdirectory(Canvas)
endif()

if(BABYLON_NATIVE_POLYFILL_ERRORCAUSE)
add_subdirectory(ErrorCause)
endif()

if(BABYLON_NATIVE_POLYFILL_STRINGTRIM)
add_subdirectory(StringTrim)
endif()
15 changes: 15 additions & 0 deletions Polyfills/ErrorCause/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
set(SOURCES
"Include/Babylon/Polyfills/ErrorCause.h"
"Source/ErrorCause.cpp")

add_library(ErrorCause ${SOURCES})
warnings_as_errors(ErrorCause)

target_include_directories(ErrorCause PUBLIC "Include")

target_link_libraries(ErrorCause
PUBLIC napi
PUBLIC Foundation)

set_property(TARGET ErrorCause PROPERTY FOLDER Polyfills)
source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${SOURCES})
24 changes: 24 additions & 0 deletions Polyfills/ErrorCause/Include/Babylon/Polyfills/ErrorCause.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#pragma once

#include <napi/env.h>
#include <Babylon/Api.h>

namespace Babylon::Polyfills::ErrorCause
{
// ES2022 Error(message, options) polyfill.
//
// Chakra's built-in Error constructor predates ES2022 and treats its second
// argument as part of the message: `new Error("hello", { cause: 42 })`
// produces an Error whose `message` is `"[object Object]"` and whose `cause`
// is undefined. That swallows every Babylon.js shader-compile error message
// that uses the modern `throw new Error("SHADER ERROR\n" + msg, { cause: e })`
// pattern, leaving only `Error: [object Object]` in stdout, hiding every
// glslang / SPIRV-Cross / DXC diagnostic.
//
// Initialize() replaces the global Error constructor (and its standard
// subclasses) with a thin wrapper that forwards `message` correctly and
// attaches `cause` from the options bag. The shim is a no-op on engines
// that already implement the ES2022 signature (V8, modern JavaScriptCore),
// so it is safe to call on every backend.
void BABYLON_API Initialize(Napi::Env env);
}
132 changes: 132 additions & 0 deletions Polyfills/ErrorCause/Source/ErrorCause.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
#include <Babylon/Polyfills/ErrorCause.h>
Copy link
Copy Markdown
Contributor

@bghgary bghgary May 29, 2026

Choose a reason for hiding this comment

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

Drop (sorry — flipping my earlier ask). Either run Playground validation against an ES-compliant engine (Win32_x64_V8_D3D11 exists in CI), or bundle babel-standalone in the Playground to transpile snippets at load if we want to keep Chakra coverage. Chakra consumers use Babel themselves. If error surfacing through Node-API is degraded on Chakra, fix in JsRuntimeHost.


#include <napi/napi.h>

// The N-API JSI variant uses a different entry point for script evaluation
// (`Napi::Eval(env, source, url)` via <napi/env.h>) than the shared N-API
// variant (`env.RunScript(source, url)`). Pick the right one at compile time
// via __has_include so the same source builds against both.
#if defined(__has_include) && __has_include(<napi/env.h>)
#include <napi/env.h>
#define BABYLON_POLYFILL_USE_NAPI_JSI_EVAL 1
#endif

namespace
{
// The polyfill is expressed as a self-contained JS IIFE so that it can be
// injected into any host engine via napi_run_script. Implementing the same
// semantics directly through the N-API C++ surface is possible but would
// require manual replication of `new.target` preservation and
// Reflect.construct semantics for each of the seven standard Error
// subclasses; the JS form is shorter, easier to review, and identical to
// what V8 implements natively.
constexpr const char* kPolyfillScript = R"JS((function () {
var globalObj =
(typeof globalThis !== "undefined") ? globalThis :
(typeof self !== "undefined") ? self :
(typeof global !== "undefined") ? global :
(new Function("return this;"))();

// If the host already implements ES2022 Error options, do nothing.
try {
var probe = new Error("probe", { cause: 42 });
if (probe.message === "probe" && probe.cause === 42) {
return;
}
} catch (_) {
// fall through to patching
}

function patchCtor(name) {
var Orig = globalObj[name];
if (typeof Orig !== "function") {
return;
}

var hasReflectConstruct =
(typeof Reflect !== "undefined") &&
(typeof Reflect.construct === "function");

function Patched(message, options) {
var m;
if (typeof message === "string") {
m = message;
} else if (typeof message === "undefined") {
m = undefined;
} else {
m = String(message);
}

var args = (m === undefined) ? [] : [m];
// new.target preservation: when invoked via `super(...)` from a
// subclass we want the new instance's prototype chain to point at
// the subclass, not at Orig. Reflect.construct does that for us.
var newTarget = (typeof new.target !== "undefined") ? new.target : Patched;
var err = hasReflectConstruct
? Reflect.construct(Orig, args, newTarget)
: (m === undefined ? new Orig() : new Orig(m));

if (options !== null && typeof options === "object" && "cause" in options) {
Object.defineProperty(err, "cause", {
value: options.cause,
writable: true,
configurable: true,
enumerable: false
});
}
return err;
}

Patched.prototype = Orig.prototype;
Comment thread
bkaradzic-microsoft marked this conversation as resolved.
// Repoint the shared prototype's `constructor` at Patched so
// `(new Error()).constructor === Error` holds after we reassign
// the global. `instanceof` already works because the prototype
// chain is unchanged.
try {
Object.defineProperty(Orig.prototype, "constructor", {
value: Patched,
writable: true,
configurable: true
});
} catch (_) { /* frozen prototype on some engines, harmless */ }
// Inherit Orig's statics (captureStackTrace, stackTraceLimit, any
// future additions) without having to enumerate them.
try {
Object.setPrototypeOf(Patched, Orig);
} catch (_) { /* engines that disallow proto mutation, harmless */ }
// Preserve the original name so stack traces still show "Error: ...".
try {
Object.defineProperty(Patched, "name", { value: name, configurable: true });
} catch (_) { /* non-writable on some engines, harmless */ }

globalObj[name] = Patched;
}

var ctorNames = [
"Error",
"TypeError",
"RangeError",
"SyntaxError",
"ReferenceError",
"URIError",
"EvalError"
];
for (var i = 0; i < ctorNames.length; i++) {
patchCtor(ctorNames[i]);
}
})();
)JS";
}

namespace Babylon::Polyfills::ErrorCause
{
void BABYLON_API Initialize(Napi::Env env)
{
Napi::HandleScope scope{env};
#if defined(BABYLON_POLYFILL_USE_NAPI_JSI_EVAL)
Napi::Eval(env, kPolyfillScript, "Babylon.Polyfills.ErrorCause");
#else
env.RunScript(kPolyfillScript, "Babylon.Polyfills.ErrorCause");
#endif
}
}
15 changes: 15 additions & 0 deletions Polyfills/StringTrim/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
set(SOURCES
"Include/Babylon/Polyfills/StringTrim.h"
"Source/StringTrim.cpp")

add_library(StringTrim ${SOURCES})
warnings_as_errors(StringTrim)

target_include_directories(StringTrim PUBLIC "Include")

target_link_libraries(StringTrim
PUBLIC napi
PUBLIC Foundation)

set_property(TARGET StringTrim PROPERTY FOLDER Polyfills)
source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${SOURCES})
20 changes: 20 additions & 0 deletions Polyfills/StringTrim/Include/Babylon/Polyfills/StringTrim.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#pragma once

#include <napi/env.h>
#include <Babylon/Api.h>

namespace Babylon::Polyfills::StringTrim
{
// ES2019 String.prototype.trimStart / trimEnd polyfill.
//
// Chakra predates ES2019 and only implements the older trimLeft / trimRight
// names. Babylon.js's ShaderProcessor and NodeMaterial code paths use the
// newer trimEnd name, so on Chakra-based BN backends those paths fail with
// `TypeError: Object doesn't support property or method 'trimEnd'`.
//
// Initialize() aliases trimStart -> trimLeft and trimEnd -> trimRight when
// the new names are missing. Both old and new names are now standard
// aliases in the spec, so engines that already provide trimEnd/trimStart
// (V8, modern JavaScriptCore) are not touched.
void BABYLON_API Initialize(Napi::Env env);
}
48 changes: 48 additions & 0 deletions Polyfills/StringTrim/Source/StringTrim.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#include <Babylon/Polyfills/StringTrim.h>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Drop (sorry — same as ErrorCause). Switching Playground validation off Chakra makes this unnecessary.


#include <napi/napi.h>

// See ErrorCause.cpp for the rationale behind this compile-time switch
// between the JSI and shared N-API entry points for script evaluation.
#if defined(__has_include) && __has_include(<napi/env.h>)
#include <napi/env.h>
#define BABYLON_POLYFILL_USE_NAPI_JSI_EVAL 1
#endif

namespace
{
// Implemented as an injected JS IIFE for symmetry with the ErrorCause
// polyfill and to keep the shim verbatim-identical to what runs in browser
// ES2019 environments.
constexpr const char* kPolyfillScript = R"JS((function () {
var sp = String.prototype;
if (typeof sp.trimEnd !== "function" && typeof sp.trimRight === "function") {
Object.defineProperty(sp, "trimEnd", {
value: sp.trimRight,
writable: true,
configurable: true
});
}
if (typeof sp.trimStart !== "function" && typeof sp.trimLeft === "function") {
Object.defineProperty(sp, "trimStart", {
value: sp.trimLeft,
writable: true,
configurable: true
});
}
})();
)JS";
}

namespace Babylon::Polyfills::StringTrim
{
void BABYLON_API Initialize(Napi::Env env)
{
Napi::HandleScope scope{env};
#if defined(BABYLON_POLYFILL_USE_NAPI_JSI_EVAL)
Napi::Eval(env, kPolyfillScript, "Babylon.Polyfills.StringTrim");
#else
env.RunScript(kPolyfillScript, "Babylon.Polyfills.StringTrim");
#endif
}
}
Loading