From 80efb8c78ab94eeecb9ab8ea90e23f896fda4ad5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Branimir=20Karad=C5=BEi=C4=87?= Date: Fri, 5 Jun 2026 11:24:14 -0700 Subject: [PATCH 1/3] NativeEngine: support standalone sampleable depth/stencil textures Babylon's NativeEngine._createDepthStencilTexture requests a standalone, sampleable depth/stencil texture by calling createFrameBuffer with a freshly-created (and therefore uninitialized) color texture whose bgfx handle is still kInvalidHandle. CreateFrameBuffer only guarded against a null texture, so it attached the invalid handle as a color target. bgfx framebuffer validation then rejected the attachment ("Invalid texture attachment") and threw, aborting the entire headless Playground sweep. Detect this request (non-null texture with an invalid bgfx handle) and: - skip attaching the invalid color handle (depth-only framebuffer), - allocate the depth attachment as a readable BGFX_TEXTURE_RT so it can be sampled, and - alias the framebuffer's depth attachment back into the caller-supplied texture (ownsHandle=false; the framebuffer owns the handle) so Babylon can sample it (e.g. fluid rendering's depth copy). This removes the whole-sweep abort and makes the depth/stencil texture sampleable per Babylon's contract. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Plugins/NativeEngine/Source/NativeEngine.cpp | 26 ++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/Plugins/NativeEngine/Source/NativeEngine.cpp b/Plugins/NativeEngine/Source/NativeEngine.cpp index 864031710..ceef4b034 100644 --- a/Plugins/NativeEngine/Source/NativeEngine.cpp +++ b/Plugins/NativeEngine/Source/NativeEngine.cpp @@ -1856,7 +1856,13 @@ namespace Babylon std::array attachments{}; uint8_t numAttachments = 0; - if (texture != nullptr) + // Babylon's NativeEngine._createDepthStencilTexture asks for a standalone, *sampleable* depth/stencil + // texture by passing a freshly created (and therefore uninitialized) color texture: its bgfx handle is + // still kInvalidHandle. Detect that here so we (a) don't attach the invalid handle as a color target and + // (b) create a readable depth attachment and alias it back into the supplied texture so it can be sampled. + const bool requestDepthStencilTexture = (texture != nullptr && !bgfx::isValid(texture->Handle())); + + if (texture != nullptr && bgfx::isValid(texture->Handle())) { const bgfx::Caps* caps = bgfx::getCaps(); // bgfx validation now asserts when trying to use BGFX_RESOLVE_AUTO_GEN_MIPS with a texture that doesn't have the BGFX_CAPS_FORMAT_TEXTURE_MIP_AUTOGEN flag, @@ -1869,6 +1875,8 @@ namespace Babylon bgfx::TextureHandle depthStencilTextureHandle = BGFX_INVALID_HANDLE; int8_t depthStencilAttachmentIndex = -1; + bgfx::TextureFormat::Enum depthStencilTextureFormat = bgfx::TextureFormat::Unknown; + uint64_t depthStencilTextureFlags = 0; if (generateStencilBuffer || generateDepth) { if (generateStencilBuffer && !generateDepth) @@ -1876,7 +1884,9 @@ namespace Babylon JsConsoleLogger::LogWarn(info.Env(), "Stencil without depth is not supported, assuming depth and stencil"); } - auto flags = BGFX_TEXTURE_RT_WRITE_ONLY | RenderTargetSamplesToBgfxMsaaFlag(samples); + // A standalone depth/stencil texture must be readable; render-target-only depth attachments stay + // write-only (cheaper, and the resolve path below relies on it). + auto flags = (requestDepthStencilTexture ? BGFX_TEXTURE_RT : BGFX_TEXTURE_RT_WRITE_ONLY) | RenderTargetSamplesToBgfxMsaaFlag(samples); #ifdef ANDROID // On Android with Mali GPU (Oppo Find x5 lite, Google Pixel 8, Samsung Galaxy Tab Active 3, ...) // D32 depth buffer gives glitches. Everything is fine with D24S8. @@ -1888,6 +1898,8 @@ namespace Babylon #endif assert(bgfx::isTextureValid(0, false, 1, depthStencilFormat, flags)); depthStencilTextureHandle = bgfx::createTexture2D(width, height, false, 1, depthStencilFormat, flags); + depthStencilTextureFormat = depthStencilFormat; + depthStencilTextureFlags = flags; // bgfx doesn't add flag D3D11_RESOURCE_MISC_GENERATE_MIPS for depth textures (missing that flag will crash D3D with resolving) // And not sure it makes sense to generate mipmaps from a depth buffer with exponential values. @@ -1909,6 +1921,16 @@ namespace Babylon } Graphics::FrameBuffer* frameBuffer = new Graphics::FrameBuffer(m_deviceContext, frameBufferHandle, width, height, false, generateDepth, generateStencilBuffer, depthStencilAttachmentIndex); + + // For a standalone depth/stencil texture request, alias the framebuffer's readable depth attachment back + // into the caller-supplied texture so Babylon can sample it (e.g. fluid rendering's depth copy). The + // framebuffer owns the handle (its destructor destroys it), so the texture must not own it. + if (requestDepthStencilTexture && depthStencilAttachmentIndex >= 0) + { + texture->Attach(bgfx::getTexture(frameBufferHandle, static_cast(depthStencilAttachmentIndex)), + false, width, height, false, 1, depthStencilTextureFormat, depthStencilTextureFlags); + } + return Napi::Pointer::Create(info.Env(), frameBuffer, Napi::NapiPointerDeleter(frameBuffer)); } From a9ce9659bcb1a9c5ac3b61ac0923c6d202716428 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Branimir=20Karad=C5=BEi=C4=87?= Date: Fri, 5 Jun 2026 11:49:52 -0700 Subject: [PATCH 2/3] NativeEngine: report depth present when stencil-without-depth allocates depth/stencil When generateStencilBuffer is requested without generateDepth, CreateFrameBuffer still allocates a combined D24S8 depth/stencil attachment, but the FrameBuffer was constructed with hasDepth=generateDepth (false). That made HasDepth() return false, so Clear/DrawInternal skipped depth clear and Z-writes against a depth buffer that actually exists. Report hasDepth as (generateDepth || generateStencilBuffer). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Plugins/NativeEngine/Source/NativeEngine.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Plugins/NativeEngine/Source/NativeEngine.cpp b/Plugins/NativeEngine/Source/NativeEngine.cpp index ceef4b034..b311d2f1f 100644 --- a/Plugins/NativeEngine/Source/NativeEngine.cpp +++ b/Plugins/NativeEngine/Source/NativeEngine.cpp @@ -1920,7 +1920,11 @@ namespace Babylon throw Napi::Error::New(info.Env(), "Failed to create frame buffer"); } - Graphics::FrameBuffer* frameBuffer = new Graphics::FrameBuffer(m_deviceContext, frameBufferHandle, width, height, false, generateDepth, generateStencilBuffer, depthStencilAttachmentIndex); + // Stencil-without-depth still allocates a combined depth/stencil attachment above, so the framebuffer + // genuinely has depth in that case too. Report hasDepth accordingly, otherwise Clear/DrawInternal would + // skip depth clear and Z-writes against a depth buffer that actually exists. + const bool hasDepthAttachment = generateDepth || generateStencilBuffer; + Graphics::FrameBuffer* frameBuffer = new Graphics::FrameBuffer(m_deviceContext, frameBufferHandle, width, height, false, hasDepthAttachment, generateStencilBuffer, depthStencilAttachmentIndex); // For a standalone depth/stencil texture request, alias the framebuffer's readable depth attachment back // into the caller-supplied texture so Babylon can sample it (e.g. fluid rendering's depth copy). The From 5088a3cc52a88a95a32562384e3cd3c4a47fb632 Mon Sep 17 00:00:00 2001 From: Branimir Karadzic Date: Mon, 6 Jul 2026 08:14:27 -0700 Subject: [PATCH 3/3] Bump babylonjs to 9.15.0 to pick up merged native-engine fixes The re-enabled validation tests depend on native-engine fixes that landed in babylonjs 9.14.0+ (depthCullingState.depthTest, cube render targets, updateTextureData / engine.name). The Apps lockfile was pinned to 9.9.1, so 'npm ci' installed an engine without those fixes and the tests crashed on the old WebGL-only paths. Bump to 9.15.0 (protocol-compatible: PROTOCOL_VERSION 9). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Apps/package-lock.json | 70 +++++++++++++++++++++--------------------- Apps/package.json | 14 ++++----- 2 files changed, 42 insertions(+), 42 deletions(-) diff --git a/Apps/package-lock.json b/Apps/package-lock.json index 2156eb656..d8adf7009 100644 --- a/Apps/package-lock.json +++ b/Apps/package-lock.json @@ -11,13 +11,13 @@ "UnitTests/JavaScript" ], "dependencies": { - "babylonjs": "^9.3.4", - "babylonjs-addons": "^9.3.4", - "babylonjs-gltf2interface": "^9.3.4", - "babylonjs-gui": "^9.3.4", - "babylonjs-loaders": "^9.3.4", - "babylonjs-materials": "^9.3.4", - "babylonjs-serializers": "^9.3.4", + "babylonjs": "^9.15.0", + "babylonjs-addons": "^9.15.0", + "babylonjs-gltf2interface": "^9.15.0", + "babylonjs-gui": "^9.15.0", + "babylonjs-loaders": "^9.15.0", + "babylonjs-materials": "^9.15.0", + "babylonjs-serializers": "^9.15.0", "jsc-android": "^241213.1.0", "v8-android": "^7.8.2" } @@ -2596,63 +2596,63 @@ } }, "node_modules/babylonjs": { - "version": "9.9.1", - "resolved": "https://registry.npmjs.org/babylonjs/-/babylonjs-9.9.1.tgz", - "integrity": "sha512-zMH8r0l/il9PJhy2+uS+/EyeLBy/ElZg0lKLj2d+ZrUarOanjMISaACw0HUx7C1EgYoXUFSMQLYcpXguleh8GA==", + "version": "9.15.0", + "resolved": "https://registry.npmjs.org/babylonjs/-/babylonjs-9.15.0.tgz", + "integrity": "sha512-IJQhrxsxxj4KCg4aSsB5chLidzAfbghr8rnzo6sRLFin6ipfeayCxg7MevjaMzqdVmc/BOMU6sj49nSNrBq+yQ==", "hasInstallScript": true, "license": "Apache-2.0" }, "node_modules/babylonjs-addons": { - "version": "9.9.1", - "resolved": "https://registry.npmjs.org/babylonjs-addons/-/babylonjs-addons-9.9.1.tgz", - "integrity": "sha512-Hi9QZqeanqG+4VAAmJmHJh7JKf1bc/Y5Ml+wuv+W+3f/dKltZpy/pii/QN4vhYRyrU0n+S8OJc+kVeaUUFjHEg==", + "version": "9.15.0", + "resolved": "https://registry.npmjs.org/babylonjs-addons/-/babylonjs-addons-9.15.0.tgz", + "integrity": "sha512-7vxm0ffFXWHCZWDlYYleifb6mg1iSH7nNo+sA9706z7QIfpNnLS9+9cTYadWobvhURwaD/DHAKnm7Z8ckq9YAA==", "license": "Apache-2.0", "dependencies": { - "babylonjs": "9.9.1" + "babylonjs": "9.15.0" } }, "node_modules/babylonjs-gltf2interface": { - "version": "9.9.1", - "resolved": "https://registry.npmjs.org/babylonjs-gltf2interface/-/babylonjs-gltf2interface-9.9.1.tgz", - "integrity": "sha512-qHE7pASEWbRORU+NEPSSL9fyVh6gBNqC1ugV94YL9Dz0HnLcH8YNcxdi6Pf+oXgFHRrXWZiJATn37HynsF7/0g==", + "version": "9.15.0", + "resolved": "https://registry.npmjs.org/babylonjs-gltf2interface/-/babylonjs-gltf2interface-9.15.0.tgz", + "integrity": "sha512-Yr/WvOvnsZFN2LoyNU/ZZbT+lXMdNA9OiZzE4RibI1tnYxQjpPKT6X1cxQ/ACJPVzKwudxzJxQ/f/Tdba4TLDg==", "license": "Apache-2.0" }, "node_modules/babylonjs-gui": { - "version": "9.9.1", - "resolved": "https://registry.npmjs.org/babylonjs-gui/-/babylonjs-gui-9.9.1.tgz", - "integrity": "sha512-w6HSvuHSqLYUOuxvko+gkjAQMvatf7gnOPufLRslm+P4tC+5KMEd0hyC+1vHCJuQmZXXkoc/GnozzFeaqllHZw==", + "version": "9.15.0", + "resolved": "https://registry.npmjs.org/babylonjs-gui/-/babylonjs-gui-9.15.0.tgz", + "integrity": "sha512-klF2ywhA040JMQ3Z5XaGkN4S0GIOshtnvtUd8kbYqlLYEu5i6/y/zvB8V3O4geslzV0kWcBe2mIthlr7M7p4og==", "license": "Apache-2.0", "dependencies": { - "babylonjs": "9.9.1" + "babylonjs": "9.15.0" } }, "node_modules/babylonjs-loaders": { - "version": "9.9.1", - "resolved": "https://registry.npmjs.org/babylonjs-loaders/-/babylonjs-loaders-9.9.1.tgz", - "integrity": "sha512-bFSe4TQTi7aA7RsWmuZHQLX4mBMoza5D+AFRD2tVrizGyGCfQJALKnKze7Z3OetFyeo72838GXJAXmyiOWcxAA==", + "version": "9.15.0", + "resolved": "https://registry.npmjs.org/babylonjs-loaders/-/babylonjs-loaders-9.15.0.tgz", + "integrity": "sha512-k5Kg9wmuy0n4ZAWu9woFk3C1yEwSvQHRv0Ct2GOQtuRvIHUIyRcW4KEKOsW0GOiApSjXh9nQcabdJTnUo8IBNw==", "license": "Apache-2.0", "dependencies": { - "babylonjs": "9.9.1", - "babylonjs-gltf2interface": "9.9.1" + "babylonjs": "9.15.0", + "babylonjs-gltf2interface": "9.15.0" } }, "node_modules/babylonjs-materials": { - "version": "9.9.1", - "resolved": "https://registry.npmjs.org/babylonjs-materials/-/babylonjs-materials-9.9.1.tgz", - "integrity": "sha512-2ai5hk59dzHRUN28/5/Nijknn+IungSuoBsrHrNZE2T/yIT+/T/IrU0ebm53nRYliv6NG0IaxSCfnh6euZUj7A==", + "version": "9.15.0", + "resolved": "https://registry.npmjs.org/babylonjs-materials/-/babylonjs-materials-9.15.0.tgz", + "integrity": "sha512-E9C5EB0nZJrGvT7Mb38gypAnzi1q8vr/Gi+fTeqRQilIU8mdk+YzD5unfKOROEv8arRFCXGWrtBqkIN8k2PFxA==", "license": "Apache-2.0", "dependencies": { - "babylonjs": "9.9.1" + "babylonjs": "9.15.0" } }, "node_modules/babylonjs-serializers": { - "version": "9.9.1", - "resolved": "https://registry.npmjs.org/babylonjs-serializers/-/babylonjs-serializers-9.9.1.tgz", - "integrity": "sha512-nZJPplzHprwJhp4JVLgB3dEpJR2HvEElPIDvOoAFf1IcaKwcDja2tZTGXIbARdWlTX547pN0qN/vEd6dYLrnDA==", + "version": "9.15.0", + "resolved": "https://registry.npmjs.org/babylonjs-serializers/-/babylonjs-serializers-9.15.0.tgz", + "integrity": "sha512-iLYwPzZrUr2SnwHcsXjHJHYdJIVC+boW6z/olE0mE5GvqmfKXTtqFr9wRGrsq85BarhHQkfVlfQdFwhFq/JG4w==", "license": "Apache-2.0", "dependencies": { - "babylonjs": "9.9.1", - "babylonjs-gltf2interface": "9.9.1" + "babylonjs": "9.15.0", + "babylonjs-gltf2interface": "9.15.0" } }, "node_modules/balanced-match": { diff --git a/Apps/package.json b/Apps/package.json index f6a7e31e7..632285ede 100644 --- a/Apps/package.json +++ b/Apps/package.json @@ -9,13 +9,13 @@ "getNightly": "node scripts/getNightly.js" }, "dependencies": { - "babylonjs": "^9.3.4", - "babylonjs-addons": "^9.3.4", - "babylonjs-gltf2interface": "^9.3.4", - "babylonjs-gui": "^9.3.4", - "babylonjs-loaders": "^9.3.4", - "babylonjs-materials": "^9.3.4", - "babylonjs-serializers": "^9.3.4", + "babylonjs": "^9.15.0", + "babylonjs-addons": "^9.15.0", + "babylonjs-gltf2interface": "^9.15.0", + "babylonjs-gui": "^9.15.0", + "babylonjs-loaders": "^9.15.0", + "babylonjs-materials": "^9.15.0", + "babylonjs-serializers": "^9.15.0", "jsc-android": "^241213.1.0", "v8-android": "^7.8.2" }