From a73f0c04d2ed72b5c408db05bc532c62319e4733 Mon Sep 17 00:00:00 2001 From: Cedric Guillemet <1312968+CedricGuillemet@users.noreply.github.com> Date: Fri, 3 Jul 2026 14:08:45 +0200 Subject: [PATCH 1/8] quickJS --- .github/workflows/build-macos.yml | 5 +++++ .github/workflows/build-win32.yml | 3 +++ .github/workflows/ci.yml | 32 +++++++++++++++++++++++++++++++ CMakeLists.txt | 4 ++-- 4 files changed, 42 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-macos.yml b/.github/workflows/build-macos.yml index 558c63fc3..c9618fa0b 100644 --- a/.github/workflows/build-macos.yml +++ b/.github/workflows/build-macos.yml @@ -19,6 +19,10 @@ on: required: false type: string default: macos-latest + js-engine: + required: false + type: string + default: '' env: UBSAN_OPTIONS: halt_on_error=1:print_stacktrace=1:symbolize=1 @@ -41,6 +45,7 @@ jobs: - name: Generate macOS solution run: | cmake -G "${{ inputs.generator }}" -B build/macOS \ + ${{ inputs.js-engine != '' && format('-D NAPI_JAVASCRIPT_ENGINE={0}', inputs.js-engine) || '' }} \ -D BABYLON_DEBUG_TRACE=ON \ -D ENABLE_SANITIZERS=${{ inputs.enable-sanitizers && 'ON' || 'OFF' }} \ -D BABYLON_NATIVE_TESTS_USE_NOOP_METAL_DEVICE=ON diff --git a/.github/workflows/build-win32.yml b/.github/workflows/build-win32.yml index 2f58771d7..b73bd85bb 100644 --- a/.github/workflows/build-win32.yml +++ b/.github/workflows/build-win32.yml @@ -47,6 +47,9 @@ jobs: elif [ "${{ inputs.napi-type }}" = "Hermes" ]; then SUFFIX="_Hermes" JS_DEFINE="-DNAPI_JAVASCRIPT_ENGINE=Hermes -DHERMES_ALLOW_BOOST_CONTEXT=0" + elif [ "${{ inputs.napi-type }}" = "QuickJS" ]; then + SUFFIX="_QuickJS" + JS_DEFINE="-DNAPI_JAVASCRIPT_ENGINE=QuickJS" fi echo "suffix=$SUFFIX" >> "$GITHUB_OUTPUT" echo "js_define=$JS_DEFINE" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f8eb537b1..cc104e8ec 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,6 +42,13 @@ jobs: xcode-version: "26.4" runs-on: macos-26 + MacOS_QuickJS: + uses: ./.github/workflows/build-macos.yml + with: + xcode-version: "26.4" + runs-on: macos-26 + js-engine: QuickJS + # ── Win32───────────────────────────────────────────────────── Win32_x64_D3D11: uses: ./.github/workflows/build-win32.yml @@ -66,6 +73,12 @@ jobs: platform: x64 napi-type: Hermes + Win32_x64_QuickJS_D3D11: + uses: ./.github/workflows/build-win32.yml + with: + platform: x64 + napi-type: QuickJS + Win32_x64_D3D11_Sanitizers: uses: ./.github/workflows/build-win32.yml with: @@ -120,6 +133,13 @@ jobs: cxx: g++ js-engine: JavaScriptCore + Ubuntu_Clang_QuickJS: + uses: ./.github/workflows/build-linux.yml + with: + cc: clang + cxx: clang++ + js-engine: QuickJS + # ── Android ─────────────────────────────────────────────────── Android_Ubuntu_JSC: uses: ./.github/workflows/build-android.yml @@ -139,6 +159,12 @@ jobs: runs-on: ubuntu-latest js-engine: Hermes + Android_Ubuntu_QuickJS: + uses: ./.github/workflows/build-android.yml + with: + runs-on: ubuntu-latest + js-engine: QuickJS + Android_MacOS_JSC: uses: ./.github/workflows/build-android.yml with: @@ -151,6 +177,12 @@ jobs: runs-on: macos-26 js-engine: V8 + Android_MacOS_QuickJS: + uses: ./.github/workflows/build-android.yml + with: + runs-on: macos-26 + js-engine: QuickJS + # ── Installation Tests ──────────────────────────────────────── iOS_Installation: uses: ./.github/workflows/test-install-ios.yml diff --git a/CMakeLists.txt b/CMakeLists.txt index 1f5c3894d..a429dbc15 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -53,8 +53,8 @@ FetchContent_Declare(ios-cmake GIT_TAG 4.5.0 EXCLUDE_FROM_ALL) FetchContent_Declare(JsRuntimeHost - GIT_REPOSITORY https://github.com/BabylonJS/JsRuntimeHost.git - GIT_TAG f07d99119887131a89a3580ada9c2a8dbc7782f7) + GIT_REPOSITORY https://github.com/CedricGuillemet/JsRuntimeHost.git + GIT_TAG e30c746cf9c997d6b19d4ac70fa1e0a33dbe1f6e) # quickjs branch (BabylonJS/JsRuntimeHost#132) FetchContent_Declare(metal-cpp GIT_REPOSITORY https://github.com/bkaradzic/metal-cpp.git GIT_TAG metal-cpp_26 From ef45f072e05be2a2c7e829476ec992b2390fdc7a Mon Sep 17 00:00:00 2001 From: Cedric Guillemet <1312968+CedricGuillemet@users.noreply.github.com> Date: Fri, 3 Jul 2026 14:21:56 +0200 Subject: [PATCH 2/8] be more specific on api and jsengine --- .github/workflows/build-macos.yml | 2 +- .github/workflows/ci.yml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-macos.yml b/.github/workflows/build-macos.yml index c9618fa0b..8437034c5 100644 --- a/.github/workflows/build-macos.yml +++ b/.github/workflows/build-macos.yml @@ -79,7 +79,7 @@ jobs: if: failure() uses: actions/upload-artifact@v6 with: - name: macos-${{ inputs.generator }}${{ inputs.enable-sanitizers && '-sanitizer' || '' }}-core-dumps + name: macos-${{ inputs.generator }}-${{ inputs.js-engine != '' && inputs.js-engine || 'default' }}${{ inputs.enable-sanitizers && '-sanitizer' || '' }}-core-dumps path: | build/macOS/Apps/UnitTests/RelWithDebInfo/UnitTests build/macOS/Apps/UnitTests/RelWithDebInfo/*.core.* diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cc104e8ec..e11ed8fd5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -78,6 +78,7 @@ jobs: with: platform: x64 napi-type: QuickJS + graphics-api: D3D11 Win32_x64_D3D11_Sanitizers: uses: ./.github/workflows/build-win32.yml From 691b4e4b70a5837d6cbd273f8bc5c7e1fbdc82b1 Mon Sep 17 00:00:00 2001 From: Cedric Guillemet <1312968+CedricGuillemet@users.noreply.github.com> Date: Fri, 3 Jul 2026 14:43:35 +0200 Subject: [PATCH 3/8] missing Apple import --- Apps/ModuleLoadTest/Source/App.Apple.mm | 2 ++ Apps/UnitTests/Source/App.Apple.mm | 2 ++ 2 files changed, 4 insertions(+) diff --git a/Apps/ModuleLoadTest/Source/App.Apple.mm b/Apps/ModuleLoadTest/Source/App.Apple.mm index 18dcdb577..2fa2af541 100644 --- a/Apps/ModuleLoadTest/Source/App.Apple.mm +++ b/Apps/ModuleLoadTest/Source/App.Apple.mm @@ -4,6 +4,8 @@ #include #include +#import + #import #include diff --git a/Apps/UnitTests/Source/App.Apple.mm b/Apps/UnitTests/Source/App.Apple.mm index f16b80428..44e5449ec 100644 --- a/Apps/UnitTests/Source/App.Apple.mm +++ b/Apps/UnitTests/Source/App.Apple.mm @@ -1,6 +1,8 @@ #include "App.h" #include +#import + #include std::filesystem::path GetExecutableDirectory() From 61d4e2accc9148b96ed78c9e9dad334b3d35c6c8 Mon Sep 17 00:00:00 2001 From: Cedric Guillemet <1312968+CedricGuillemet@users.noreply.github.com> Date: Mon, 6 Jul 2026 09:45:53 +0200 Subject: [PATCH 4/8] release stack pressure --- Apps/Playground/Scripts/validation_native.js | 62 +++++++++++++++----- 1 file changed, 46 insertions(+), 16 deletions(-) diff --git a/Apps/Playground/Scripts/validation_native.js b/Apps/Playground/Scripts/validation_native.js index e691600ad..4e4276ac3 100644 --- a/Apps/Playground/Scripts/validation_native.js +++ b/Apps/Playground/Scripts/validation_native.js @@ -371,21 +371,36 @@ } } - currentScene = eval(code + "\r\ncreateScene(engine)"); - - if (currentScene.then) { - // Handle if createScene returns a promise - currentScene.then(function (scene) { - currentScene = scene; - processCurrentScene(test, referenceImage, done, compareFunction); - }).catch(function (e) { - console.error(e); + const pgCode = code + "\r\ncreateScene(engine)"; + // Defer scene construction to a fresh macrotask so + // eval()/createScene() run at a shallow native-stack + // depth instead of nested inside the native snippet + // load callback. Deep scenes otherwise pile onto the + // native XHR dispatch frames and can overflow engines + // with a small C stack (e.g. QuickJS). + setTimeout(function () { + try { + currentScene = eval(pgCode); + + if (currentScene.then) { + // Handle if createScene returns a promise + currentScene.then(function (scene) { + currentScene = scene; + processCurrentScene(test, referenceImage, done, compareFunction); + }).catch(function (e) { + console.error(e); + failTest(done); + }); + } else { + // Handle if createScene returns a scene + processCurrentScene(test, referenceImage, done, compareFunction); + } + } + catch (e) { + console.error("Failed to evaluate playground snippet " + test.playgroundId + ": " + e); failTest(done); - }); - } else { - // Handle if createScene returns a scene - processCurrentScene(test, referenceImage, done, compareFunction); - } + } + }, 0); } catch (e) { console.error("Failed to evaluate playground snippet " + test.playgroundId + ": " + e); @@ -442,8 +457,23 @@ } } - currentScene = eval(scriptToRun + test.functionToCall + "(engine)"); - processCurrentScene(test, renderImage, done, compareFunction); + const scriptCode = scriptToRun + test.functionToCall + "(engine)"; + // Defer scene construction to a fresh macrotask so + // eval()/() run at a shallow native-stack + // depth instead of nested inside the native XHR + // completion callback. Deep scenes otherwise pile onto + // the native XHR dispatch frames and can overflow engines + // with a small C stack (e.g. QuickJS). + setTimeout(function () { + try { + currentScene = eval(scriptCode); + processCurrentScene(test, renderImage, done, compareFunction); + } + catch (e) { + console.error(e); + failTest(done); + } + }, 0); } catch (e) { console.error(e); From e64b18879a43f9b3d558216d37ee2c21c0bc7f87 Mon Sep 17 00:00:00 2001 From: Cedric Guillemet <1312968+CedricGuillemet@users.noreply.github.com> Date: Mon, 6 Jul 2026 10:58:46 +0200 Subject: [PATCH 5/8] test with removed problematic scene --- Apps/Playground/Scripts/config.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Apps/Playground/Scripts/config.json b/Apps/Playground/Scripts/config.json index 3b7389558..018ebacf2 100644 --- a/Apps/Playground/Scripts/config.json +++ b/Apps/Playground/Scripts/config.json @@ -16,7 +16,9 @@ { "title": "Native Canvas", "playgroundId": "#TKVFSA#8", - "referenceImage": "native-canvas.png" + "referenceImage": "native-canvas.png", + "excludeFromAutomaticTesting": true, + "reason": "Crashes on QuickJS: use-after-free during async-function resume after awaiting _native.Canvas.loadTTFAsync (QuickJS Node-API deferred-promise path)" }, { "title": "EXR Loader", From 7d884310ad19ad94ab900f3da6701803ddc5bd54 Mon Sep 17 00:00:00 2001 From: Cedric Guillemet <1312968+CedricGuillemet@users.noreply.github.com> Date: Mon, 6 Jul 2026 15:01:48 +0200 Subject: [PATCH 6/8] dangling pointer --- Apps/Playground/Scripts/config.json | 4 +--- Polyfills/Canvas/Source/Gradient.cpp | 9 +++++---- Polyfills/Canvas/Source/ImageData.cpp | 4 +++- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/Apps/Playground/Scripts/config.json b/Apps/Playground/Scripts/config.json index 018ebacf2..3b7389558 100644 --- a/Apps/Playground/Scripts/config.json +++ b/Apps/Playground/Scripts/config.json @@ -16,9 +16,7 @@ { "title": "Native Canvas", "playgroundId": "#TKVFSA#8", - "referenceImage": "native-canvas.png", - "excludeFromAutomaticTesting": true, - "reason": "Crashes on QuickJS: use-after-free during async-function resume after awaiting _native.Canvas.loadTTFAsync (QuickJS Node-API deferred-promise path)" + "referenceImage": "native-canvas.png" }, { "title": "EXR Loader", diff --git a/Polyfills/Canvas/Source/Gradient.cpp b/Polyfills/Canvas/Source/Gradient.cpp index 31249859f..c5e09246c 100644 --- a/Polyfills/Canvas/Source/Gradient.cpp +++ b/Polyfills/Canvas/Source/Gradient.cpp @@ -88,8 +88,10 @@ namespace Babylon::Polyfills::Internal Napi::Object CanvasGradient::CreateLinear(Napi::Env env, const std::shared_ptr& context, float x0, float y0, float x1, float y1) { - Napi::HandleScope scope{ env }; - + // NOTE: Do not open a Napi::HandleScope here. The gradient object created below is + // returned to the caller, and a plain (non-escapable) HandleScope would release the + // handle on close. Under the reference-counted QuickJS Node-API port that frees the + // object's only reference, yielding a dangling value (typeof "unknown", no prototype). auto func = JsRuntime::NativeObject::GetFromJavaScript(env).Get(JS_CANVAS_GRADIENT_CONSTRUCTOR_NAME).As(); auto gradientValue = func.New({ Napi::Value::From(env, x0), Napi::Value::From(env, y0), Napi::Value::From(env, x1), Napi::Value::From(env, y1) }); CanvasGradient::Unwrap(gradientValue)->context = context; @@ -98,8 +100,7 @@ namespace Babylon::Polyfills::Internal Napi::Object CanvasGradient::CreateRadial(Napi::Env env, const std::shared_ptr& context, float x0, float y0, float r0, float x1, float y1, float r1) { - Napi::HandleScope scope{ env }; - + // See CreateLinear: no HandleScope here so the returned gradient handle is not freed on scope close. auto func = JsRuntime::NativeObject::GetFromJavaScript(env).Get(JS_CANVAS_GRADIENT_CONSTRUCTOR_NAME).As(); auto gradientValue = func.New({ Napi::Value::From(env, x0), Napi::Value::From(env, y0), Napi::Value::From(env, x1), Napi::Value::From(env, y1), Napi::Value::From(env, r0), Napi::Value::From(env, r1) }); CanvasGradient::Unwrap(gradientValue)->context = context; diff --git a/Polyfills/Canvas/Source/ImageData.cpp b/Polyfills/Canvas/Source/ImageData.cpp index 7011000bb..16180d5ad 100644 --- a/Polyfills/Canvas/Source/ImageData.cpp +++ b/Polyfills/Canvas/Source/ImageData.cpp @@ -21,7 +21,9 @@ namespace Babylon::Polyfills::Internal Napi::Value ImageData::CreateInstance(Napi::Env env, Context* context, uint32_t width, uint32_t height) { - Napi::HandleScope scope{env}; + // No Napi::HandleScope here: the object created by func.New() is returned to the caller. + // A plain HandleScope would free the handle on close, which under the reference-counted + // QuickJS Node-API port leaves the caller with a dangling value. Napi::Function func = DefineClass( env, JS_IMAGEDATA_CONSTRUCTOR_NAME, From 1a26706cacfffd2af2990d0826bca1c860cbd602 Mon Sep 17 00:00:00 2001 From: Cedric Guillemet <1312968+CedricGuillemet@users.noreply.github.com> Date: Wed, 8 Jul 2026 11:57:03 +0200 Subject: [PATCH 7/8] up arcana --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index a429dbc15..606e10028 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -23,7 +23,7 @@ FetchContent_Declare(AndroidExtensions EXCLUDE_FROM_ALL) FetchContent_Declare(arcana.cpp GIT_REPOSITORY https://github.com/microsoft/arcana.cpp.git - GIT_TAG b9bf9d85fce37d5fc9dbfc4a4dc5e1531bee215a + GIT_TAG fe41b17dedea47b5dead98f0271ca284e94ac6a8 EXCLUDE_FROM_ALL) FetchContent_Declare(arcore-android-sdk GIT_REPOSITORY https://github.com/google-ar/arcore-android-sdk.git @@ -54,7 +54,7 @@ FetchContent_Declare(ios-cmake EXCLUDE_FROM_ALL) FetchContent_Declare(JsRuntimeHost GIT_REPOSITORY https://github.com/CedricGuillemet/JsRuntimeHost.git - GIT_TAG e30c746cf9c997d6b19d4ac70fa1e0a33dbe1f6e) # quickjs branch (BabylonJS/JsRuntimeHost#132) + GIT_TAG 0e34a58a2e0a986efbf7b70434b61739c451afb7) # quickjs branch (BabylonJS/JsRuntimeHost#132) FetchContent_Declare(metal-cpp GIT_REPOSITORY https://github.com/bkaradzic/metal-cpp.git GIT_TAG metal-cpp_26 From 797dbe0846ed527f54c8d8d4f1eb70445922cd80 Mon Sep 17 00:00:00 2001 From: Cedric Guillemet <1312968+CedricGuillemet@users.noreply.github.com> Date: Fri, 10 Jul 2026 09:57:55 +0200 Subject: [PATCH 8/8] JSRuntimeHost commit id --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 606e10028..60ad46a4a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -53,8 +53,8 @@ FetchContent_Declare(ios-cmake GIT_TAG 4.5.0 EXCLUDE_FROM_ALL) FetchContent_Declare(JsRuntimeHost - GIT_REPOSITORY https://github.com/CedricGuillemet/JsRuntimeHost.git - GIT_TAG 0e34a58a2e0a986efbf7b70434b61739c451afb7) # quickjs branch (BabylonJS/JsRuntimeHost#132) + GIT_REPOSITORY https://github.com/BabylonJS/JsRuntimeHost.git + GIT_TAG 8cd142951018de07c89c23f726fb7dc9c8374599) FetchContent_Declare(metal-cpp GIT_REPOSITORY https://github.com/bkaradzic/metal-cpp.git GIT_TAG metal-cpp_26