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 1553ce29a..1d86a2a8a 100644 --- a/Apps/Playground/Scripts/validation_native.js +++ b/Apps/Playground/Scripts/validation_native.js @@ -93,6 +93,81 @@ return getExclusionReason(t); } + function shouldUseRenderReadinessPump(test) { + if (test && typeof test.renderReadinessPump === "boolean") { + return test.renderReadinessPump; + } + return globalThis.__nativeValidationRenderReadinessPump === true; + } + + function formatReadinessError(error) { + if (error && error.stack) { + return error.stack; + } + if (error && error.message) { + return error.message; + } + return String(error); + } + + function startSceneReadinessRenderPump(scene) { + let stopped = false; + let frameCount = 0; + let errorCount = 0; + let firstError = null; + let lastError = null; + + const pump = function () { + let nextPumpDelay = 100; + 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") { + nextPumpDelay = 16; + // Readiness renders are only for material/effect compilation. + // Preserve the screenshot test's animation frame count. + scene.render(true, true); + frameCount++; + } + } catch (e) { + errorCount++; + if (errorCount === 1) { + firstError = e; + console.warn("Readiness render pump encountered a transient error; continuing to poll scene readiness:\n" + + formatReadinessError(e)); + } + lastError = e; + } + + setTimeout(pump, nextPumpDelay); + }; + + setTimeout(pump, 0); + return { + stop: function () { + stopped = true; + }, + getFrameCount: function () { + return frameCount; + }, + getErrorCount: function () { + return errorCount; + }, + getFirstError: function () { + return firstError; + }, + getLastError: function () { + return lastError; + } + }; + } + function logRunSummary() { console.log("Run complete. ran=" + ranCount + " passed=" + passedCount + @@ -263,11 +338,33 @@ let stopped = false; let pendingScreenshot = null; 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; } + stopReadinessPump(false); evaluated = true; evaluateScreenshot(test, screenshot, renderImage, done, compareFunction); }; @@ -283,12 +380,34 @@ // never-ready scene into a fast test failure instead of a silent hang. currentScene.onReadyTimeoutDuration = 10 * 60 * 1000; currentScene.onReadyTimeoutObservable.addOnce(function () { + let readinessErrorDetails = ""; + 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); + } + } + evaluated = true; + stopped = 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); + } + currentScene.executeWhenReady(function () { + if (evaluated) { + return; + } + 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; } @@ -328,6 +447,9 @@ } } catch (e) { + stopReadinessPump(false); + evaluated = true; + stopped = true; console.error(e); failTest(done); } 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 # --------------------------------------------------