From 268329fbc450229884ded62ff222ec25311374e0 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/2] 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 e7065541d63545c4d5ab8dbcc017de5ef3e18c1a 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/2] 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