From 936c946e19e8bbfd9d8f2383c96dd7e52de8bf93 Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Tue, 16 Jun 2026 09:59:41 +0200 Subject: [PATCH 1/4] Preserve animation frames during readiness renders Render screenshot readiness-pump frames with animations ignored so material and effect readiness can advance without consuming the configured validation frame count. The GLTF serializer skinning tests are already enabled on current upstream master, so this keeps them compatible with readiness-driven validation. --- Apps/Playground/Scripts/validation_native.js | 91 ++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/Apps/Playground/Scripts/validation_native.js b/Apps/Playground/Scripts/validation_native.js index 0b75478a8..c0878f6de 100644 --- a/Apps/Playground/Scripts/validation_native.js +++ b/Apps/Playground/Scripts/validation_native.js @@ -72,6 +72,58 @@ return getExclusionReason(t); } + function shouldUseRenderReadinessPump(test) { + if (test && test.renderReadinessPump === false) { + return false; + } + if (globalThis.__nativeValidationRenderReadinessPump === false) { + return false; + } + return true; + } + + function startSceneReadinessRenderPump(scene, onFatalError) { + let stopped = false; + let frameCount = 0; + + const pump = function () { + if (stopped) { + return; + } + if (!scene || scene.isDisposed === true || (typeof scene.isDisposed === "function" && scene.isDisposed())) { + stopped = true; + return; + } + + try { + if (scene.activeCamera && typeof scene.render === "function") { + // Readiness renders are only for material/effect compilation. + // Preserve the screenshot test's animation frame count. + scene.render(true, true); + frameCount++; + } + } catch (e) { + stopped = true; + if (typeof onFatalError === "function") { + onFatalError(e); + } + return; + } + + setTimeout(pump, 16); + }; + + setTimeout(pump, 0); + return { + stop: function () { + stopped = true; + }, + getFrameCount: function () { + return frameCount; + } + }; + } + function logRunSummary() { console.log("Run complete. ran=" + ranCount + " passed=" + passedCount + @@ -235,11 +287,16 @@ let stopped = false; let pendingScreenshot = null; let evaluated = false; + let readinessPump = null; const runEvaluation = function (screenshot) { if (evaluated) { return; } + if (readinessPump !== null) { + readinessPump.stop(); + readinessPump = null; + } evaluated = true; evaluateScreenshot(test, screenshot, renderImage, done, compareFunction); }; @@ -255,12 +312,40 @@ // never-ready scene into a fast test failure instead of a silent hang. currentScene.onReadyTimeoutDuration = 10 * 60 * 1000; currentScene.onReadyTimeoutObservable.addOnce(function () { + if (readinessPump !== null) { + readinessPump.stop(); + readinessPump = null; + } + evaluated = true; console.error("Scene '" + (test.title || "?") + "' did not become ready within " + (currentScene.onReadyTimeoutDuration / 1000) + "s."); failTest(done); }); + if (shouldUseRenderReadinessPump(test)) { + readinessPump = startSceneReadinessRenderPump(currentScene, function (error) { + if (evaluated) { + return; + } + evaluated = true; + stopped = true; + console.error("Readiness render pump failed: " + (error && error.message ? error.message : String(error))); + failTest(done); + }); + } + currentScene.executeWhenReady(function () { + if (evaluated) { + return; + } + if (readinessPump !== null) { + const readinessFrameCount = readinessPump.getFrameCount(); + readinessPump.stop(); + readinessPump = null; + if (readinessFrameCount > 0) { + console.log("Readiness render pump rendered " + readinessFrameCount + " frame(s) before validation frame counting for " + (test.title || "(unnamed)") + "."); + } + } if (currentScene.activeCamera && currentScene.activeCamera.useAutoRotationBehavior) { currentScene.activeCamera.useAutoRotationBehavior = false; } @@ -300,6 +385,12 @@ } } catch (e) { + if (readinessPump !== null) { + readinessPump.stop(); + readinessPump = null; + } + evaluated = true; + stopped = true; console.error(e); failTest(done); } From 4fe9b5df2a2a78e6d13925484fffd59da497e9ed Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Fri, 17 Jul 2026 14:19:38 -0700 Subject: [PATCH 2/4] Make screenshot readiness rendering opt-in Limit speculative pre-ready renders to validation cases that need shader and material compilation progress without advancing their configured animation frames. This avoids changing particles, procedural textures, and render-observer state in the rest of the screenshot catalog.\n\nKeep readiness-phase render exceptions visible while polling continues. Record the first and last failures with their stacks, report recovered exception counts, and include retained diagnostics when a scene times out; the normal post-ready validation render remains a hard failure. --- Apps/Playground/Scripts/config.json | 4 ++ Apps/Playground/Scripts/validation_native.js | 68 ++++++++++++++------ 2 files changed, 52 insertions(+), 20 deletions(-) diff --git a/Apps/Playground/Scripts/config.json b/Apps/Playground/Scripts/config.json index f6d345f30..b005578d5 100644 --- a/Apps/Playground/Scripts/config.json +++ b/Apps/Playground/Scripts/config.json @@ -793,6 +793,7 @@ "title": "Gaussian Splatting Loading", "playgroundId": "#CID4NN#204", "renderCount": 15, + "renderReadinessPump": true, "referenceImage": "gsplat-loading.png" }, { @@ -808,6 +809,7 @@ { "title": "needDepthPrePass", "playgroundId": "#7A66KI#1", + "renderReadinessPump": true, "referenceImage": "needDepthPrePass.png" }, { @@ -1348,11 +1350,13 @@ { "title": "GLTF Serializer Skinning and Animation", "playgroundId": "#DMZBX1#1", + "renderReadinessPump": true, "referenceImage": "gltfSerializerSkinningAndAnimation.png" }, { "title": "GLTF Serializer Skinning and Animation (Right Handed)", "playgroundId": "#DMZBX1#2", + "renderReadinessPump": true, "referenceImage": "gltfSerializerSkinningAndAnimation.png" }, { diff --git a/Apps/Playground/Scripts/validation_native.js b/Apps/Playground/Scripts/validation_native.js index 5377d022e..7104f4793 100644 --- a/Apps/Playground/Scripts/validation_native.js +++ b/Apps/Playground/Scripts/validation_native.js @@ -94,18 +94,28 @@ } function shouldUseRenderReadinessPump(test) { - if (test && test.renderReadinessPump === false) { - return false; + if (test && typeof test.renderReadinessPump === "boolean") { + return test.renderReadinessPump; } - if (globalThis.__nativeValidationRenderReadinessPump === false) { - return false; + return globalThis.__nativeValidationRenderReadinessPump === true; + } + + function formatReadinessError(error) { + if (error && error.stack) { + return error.stack; } - return true; + if (error && error.message) { + return error.message; + } + return String(error); } - function startSceneReadinessRenderPump(scene, onFatalError) { + function startSceneReadinessRenderPump(scene) { let stopped = false; let frameCount = 0; + let errorCount = 0; + let firstError = null; + let lastError = null; const pump = function () { if (stopped) { @@ -124,11 +134,13 @@ frameCount++; } } catch (e) { - stopped = true; - if (typeof onFatalError === "function") { - onFatalError(e); + errorCount++; + if (errorCount === 1) { + firstError = e; + console.warn("Readiness render pump encountered a transient error; continuing to poll scene readiness:\n" + + formatReadinessError(e)); } - return; + lastError = e; } setTimeout(pump, 16); @@ -141,6 +153,15 @@ }, getFrameCount: function () { return frameCount; + }, + getErrorCount: function () { + return errorCount; + }, + getFirstError: function () { + return firstError; + }, + getLastError: function () { + return lastError; } }; } @@ -340,26 +361,29 @@ // never-ready scene into a fast test failure instead of a silent hang. currentScene.onReadyTimeoutDuration = 10 * 60 * 1000; currentScene.onReadyTimeoutObservable.addOnce(function () { + let readinessErrorDetails = ""; if (readinessPump !== null) { + const readinessErrorCount = readinessPump.getErrorCount(); + if (readinessErrorCount > 0) { + const firstReadinessError = readinessPump.getFirstError(); + const lastReadinessError = readinessPump.getLastError(); + readinessErrorDetails = "\nReadiness render pump observed " + readinessErrorCount + " exception(s).\nFirst error:\n" + + formatReadinessError(firstReadinessError); + if (lastReadinessError !== firstReadinessError) { + readinessErrorDetails += "\nLast error:\n" + formatReadinessError(lastReadinessError); + } + } readinessPump.stop(); readinessPump = null; } evaluated = true; console.error("Scene '" + (test.title || "?") + "' did not become ready within " + - (currentScene.onReadyTimeoutDuration / 1000) + "s."); + (currentScene.onReadyTimeoutDuration / 1000) + "s." + readinessErrorDetails); failTest(done); }); if (shouldUseRenderReadinessPump(test)) { - readinessPump = startSceneReadinessRenderPump(currentScene, function (error) { - if (evaluated) { - return; - } - evaluated = true; - stopped = true; - console.error("Readiness render pump failed: " + (error && error.message ? error.message : String(error))); - failTest(done); - }); + readinessPump = startSceneReadinessRenderPump(currentScene); } currentScene.executeWhenReady(function () { @@ -368,11 +392,15 @@ } if (readinessPump !== null) { const readinessFrameCount = readinessPump.getFrameCount(); + const readinessErrorCount = readinessPump.getErrorCount(); readinessPump.stop(); readinessPump = null; if (readinessFrameCount > 0) { console.log("Readiness render pump rendered " + readinessFrameCount + " frame(s) before validation frame counting for " + (test.title || "(unnamed)") + "."); } + if (readinessErrorCount > 0) { + console.warn("Readiness render pump recovered from " + readinessErrorCount + " transient exception(s) before " + (test.title || "(unnamed)") + " became ready."); + } } if (currentScene.activeCamera && currentScene.activeCamera.useAutoRotationBehavior) { currentScene.activeCamera.useAutoRotationBehavior = false; From f16f0ca7635d27958874a9e34da502b40d0a56b7 Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Fri, 17 Jul 2026 14:50:09 -0700 Subject: [PATCH 3/4] Use a coherent system SDK for Apple builds Default top-level macOS builds to the selected Xcode SDK instead of allowing Command Line Tools headers to mix with Xcode frameworks. Mark the JavaScriptCore framework search path as a system framework through napi so JsRuntimeHost's pedantic warnings-as-errors policy ignores SDK implementation diagnostics without suppressing project warnings.\n\nThis fixes Xcode 26.5 nullability, invalid UTF-8, and related framework-header failures while leaving explicit cross-compilation toolchains and Apple mobile platforms unchanged. --- CMakeLists.txt | 12 ++++++++++++ Dependencies/CMakeLists.txt | 10 ++++++++++ 2 files changed, 22 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index d37aac35e..9e3370fea 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -91,6 +91,18 @@ elseif(IOS) set(DEPLOYMENT_TARGET "13" CACHE STRING "") endif() +if(CMAKE_HOST_APPLE + AND CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR + AND NOT CMAKE_TOOLCHAIN_FILE + AND NOT IOS + AND NOT VISIONOS + AND NOT CMAKE_OSX_SYSROOT) + # Use one coherent Xcode SDK for headers and frameworks. Leaving this empty + # can mix Command Line Tools headers with frameworks from the selected + # Xcode, which turns SDK-only diagnostics into warnings-as-errors. + set(CMAKE_OSX_SYSROOT macosx CACHE STRING "macOS SDK" FORCE) +endif() + project(BabylonNative) set_property(GLOBAL PROPERTY USE_FOLDERS ON) diff --git a/Dependencies/CMakeLists.txt b/Dependencies/CMakeLists.txt index 49b360250..8db2a2d69 100644 --- a/Dependencies/CMakeLists.txt +++ b/Dependencies/CMakeLists.txt @@ -191,6 +191,16 @@ endif() # -------------------------------------------------- FetchContent_MakeAvailable_With_Message(JsRuntimeHost) +if(APPLE AND NAPI_JAVASCRIPT_ENGINE STREQUAL "JavaScriptCore" AND TARGET napi) + # find_library adds the SDK framework directory with -F, which does not + # classify framework headers as system headers. JsRuntimeHost uses + # -pedantic -Werror, so make that classification explicit for napi and all + # consumers while preserving warnings-as-errors for project sources. + get_filename_component(_JAVASCRIPTCORE_FRAMEWORKS_DIR "${JAVASCRIPTCORE_LIBRARY}" DIRECTORY) + target_compile_options(napi PUBLIC "SHELL:-iframework ${_JAVASCRIPTCORE_FRAMEWORKS_DIR}") + unset(_JAVASCRIPTCORE_FRAMEWORKS_DIR) +endif() + # -------------------------------------------------- # metal-cpp # -------------------------------------------------- From 7e6562337d638e811ae0290ecd36e624915a9fa8 Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Fri, 17 Jul 2026 14:53:10 -0700 Subject: [PATCH 4/4] Harden screenshot readiness pump cleanup Centralize readiness pump shutdown and summary collection so every completion and failure path stops the timer consistently. Mark timeout completion as stopped and avoid 60 Hz polling before an active camera is available while retaining render-rate polling once shader preparation can make progress. --- Apps/Playground/Scripts/validation_native.js | 67 ++++++++++---------- 1 file changed, 35 insertions(+), 32 deletions(-) diff --git a/Apps/Playground/Scripts/validation_native.js b/Apps/Playground/Scripts/validation_native.js index 7104f4793..1d86a2a8a 100644 --- a/Apps/Playground/Scripts/validation_native.js +++ b/Apps/Playground/Scripts/validation_native.js @@ -118,6 +118,7 @@ let lastError = null; const pump = function () { + let nextPumpDelay = 100; if (stopped) { return; } @@ -128,6 +129,7 @@ try { if (scene.activeCamera && typeof scene.render === "function") { + nextPumpDelay = 16; // Readiness renders are only for material/effect compilation. // Preserve the screenshot test's animation frame count. scene.render(true, true); @@ -143,7 +145,7 @@ lastError = e; } - setTimeout(pump, 16); + setTimeout(pump, nextPumpDelay); }; setTimeout(pump, 0); @@ -338,14 +340,31 @@ let evaluated = false; let readinessPump = null; + const stopReadinessPump = function (logFrameCount) { + if (readinessPump === null) { + return null; + } + + const pump = readinessPump; + readinessPump = null; + const summary = { + frameCount: pump.getFrameCount(), + errorCount: pump.getErrorCount(), + firstError: pump.getFirstError(), + lastError: pump.getLastError() + }; + pump.stop(); + if (logFrameCount && summary.frameCount > 0) { + console.log("Readiness render pump rendered " + summary.frameCount + " frame(s) before validation frame counting for " + (test.title || "(unnamed)") + "."); + } + return summary; + }; + const runEvaluation = function (screenshot) { if (evaluated) { return; } - if (readinessPump !== null) { - readinessPump.stop(); - readinessPump = null; - } + stopReadinessPump(false); evaluated = true; evaluateScreenshot(test, screenshot, renderImage, done, compareFunction); }; @@ -362,21 +381,16 @@ currentScene.onReadyTimeoutDuration = 10 * 60 * 1000; currentScene.onReadyTimeoutObservable.addOnce(function () { let readinessErrorDetails = ""; - if (readinessPump !== null) { - const readinessErrorCount = readinessPump.getErrorCount(); - if (readinessErrorCount > 0) { - const firstReadinessError = readinessPump.getFirstError(); - const lastReadinessError = readinessPump.getLastError(); - readinessErrorDetails = "\nReadiness render pump observed " + readinessErrorCount + " exception(s).\nFirst error:\n" + - formatReadinessError(firstReadinessError); - if (lastReadinessError !== firstReadinessError) { - readinessErrorDetails += "\nLast error:\n" + formatReadinessError(lastReadinessError); - } + const readinessSummary = stopReadinessPump(false); + if (readinessSummary !== null && readinessSummary.errorCount > 0) { + readinessErrorDetails = "\nReadiness render pump observed " + readinessSummary.errorCount + " exception(s).\nFirst error:\n" + + formatReadinessError(readinessSummary.firstError); + if (readinessSummary.lastError !== readinessSummary.firstError) { + readinessErrorDetails += "\nLast error:\n" + formatReadinessError(readinessSummary.lastError); } - readinessPump.stop(); - readinessPump = null; } evaluated = true; + stopped = true; console.error("Scene '" + (test.title || "?") + "' did not become ready within " + (currentScene.onReadyTimeoutDuration / 1000) + "s." + readinessErrorDetails); failTest(done); @@ -390,17 +404,9 @@ if (evaluated) { return; } - if (readinessPump !== null) { - const readinessFrameCount = readinessPump.getFrameCount(); - const readinessErrorCount = readinessPump.getErrorCount(); - readinessPump.stop(); - readinessPump = null; - if (readinessFrameCount > 0) { - console.log("Readiness render pump rendered " + readinessFrameCount + " frame(s) before validation frame counting for " + (test.title || "(unnamed)") + "."); - } - if (readinessErrorCount > 0) { - console.warn("Readiness render pump recovered from " + readinessErrorCount + " transient exception(s) before " + (test.title || "(unnamed)") + " became ready."); - } + const readinessSummary = stopReadinessPump(true); + if (readinessSummary !== null && readinessSummary.errorCount > 0) { + console.warn("Readiness render pump recovered from " + readinessSummary.errorCount + " transient exception(s) before " + (test.title || "(unnamed)") + " became ready."); } if (currentScene.activeCamera && currentScene.activeCamera.useAutoRotationBehavior) { currentScene.activeCamera.useAutoRotationBehavior = false; @@ -441,10 +447,7 @@ } } catch (e) { - if (readinessPump !== null) { - readinessPump.stop(); - readinessPump = null; - } + stopReadinessPump(false); evaluated = true; stopped = true; console.error(e);