diff --git a/Apps/UnitTests/CMakeLists.txt b/Apps/UnitTests/CMakeLists.txt index bce196c5f..5c303c1fc 100644 --- a/Apps/UnitTests/CMakeLists.txt +++ b/Apps/UnitTests/CMakeLists.txt @@ -43,6 +43,7 @@ set(SOURCES "Source/Tests.JavaScript.cpp" "Source/Tests.NativeEngine.InstanceData.cpp" "Source/Tests.NativeEngine.ImageFormats.cpp" + "Source/Tests.NativeEngine.DepthTextures.cpp" "Source/Tests.NativeEngine.Teardown.cpp" "Source/Tests.ShaderCache.cpp" "Source/Tests.ShaderCompilation.cpp" diff --git a/Apps/UnitTests/Source/Tests.NativeEngine.DepthTextures.cpp b/Apps/UnitTests/Source/Tests.NativeEngine.DepthTextures.cpp new file mode 100644 index 000000000..a8ffae256 --- /dev/null +++ b/Apps/UnitTests/Source/Tests.NativeEngine.DepthTextures.cpp @@ -0,0 +1,111 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +extern Babylon::Graphics::Configuration g_deviceConfig; + +TEST(NativeEngineDepthTextures, ExposesReadableDepthAndPreservesFramebufferOwnership) +{ + Babylon::Graphics::Device device{g_deviceConfig}; + device.StartRenderingCurrentFrame(); + Babylon::AppRuntime runtime{}; + std::promise completed; + auto future = completed.get_future(); + runtime.Dispatch([&](Napi::Env env) { + std::string error; + try + { + device.AddToJavaScript(env); + Babylon::Polyfills::Console::Initialize(env, [](const char* message, auto) { + std::cout << message << std::endl; + }); + Babylon::Plugins::NativeEngine::Initialize(env); + auto& context = Babylon::Graphics::DeviceContext::GetFromJavaScript(env); + auto frameScope = context.AcquireFrameCompletionScope(); + auto engine = env.Global().Get("_native").As().Get("Engine").As().New({}); + const auto createFrameBuffer = [&](Napi::Value texture, bool stencil, bool depth, uint32_t samples = 1) { + return engine.Get("createFrameBuffer").As().Call(engine, { + texture, Napi::Number::New(env, 16), Napi::Number::New(env, 16), + Napi::Boolean::New(env, stencil), Napi::Boolean::New(env, depth), Napi::Number::New(env, samples)}); + }; + const std::array renderTargetFlags{ + BGFX_TEXTURE_RT, BGFX_TEXTURE_RT_MSAA_X2, BGFX_TEXTURE_RT_MSAA_X4}; + for (size_t index = 0; index < renderTargetFlags.size(); ++index) + { + const uint32_t samples = 1u << index; + const uint64_t samplingFlag = index == 0 ? BGFX_TEXTURE_NONE : BGFX_TEXTURE_MSAA_SAMPLE; + const uint64_t flags = renderTargetFlags[index] | samplingFlag; + SCOPED_TRACE(samples); + for (const bool stencil : {false, true}) + { + SCOPED_TRACE(stencil); + if (index != 0 && + !bgfx::isTextureValid(0, false, 1, bgfx::TextureFormat::D24S8, flags) && + (stencil || (!bgfx::isTextureValid(0, false, 1, bgfx::TextureFormat::D32F, flags) && + !bgfx::isTextureValid(0, false, 1, bgfx::TextureFormat::D24, flags)))) + { + std::cout << "Skipping unsupported readable depth MSAA: samples=" << samples + << ", stencil=" << stencil << std::endl; + continue; + } + for (const bool depth : {false, true}) + { + auto value = engine.Get("createTexture").As().Call(engine, {}); + auto* texture = value.As>().Get(); + if (!stencil && !depth) + { + EXPECT_THROW(createFrameBuffer(value, stencil, depth, samples), Napi::Error); + continue; + } + auto frameBufferValue = createFrameBuffer(value, stencil, depth, samples); + auto* frameBuffer = frameBufferValue.As>().Get(); + EXPECT_TRUE(texture->IsValid()); + EXPECT_EQ(texture->Width(), 16u); + EXPECT_EQ(texture->Height(), 16u); + EXPECT_EQ(texture->Flags() & BGFX_TEXTURE_RT_WRITE_ONLY, 0u); + EXPECT_EQ(texture->Flags() & BGFX_TEXTURE_MSAA_SAMPLE, samplingFlag); + EXPECT_EQ(texture->Flags() & BGFX_TEXTURE_RT_MSAA_MASK, renderTargetFlags[index]); + EXPECT_EQ(texture->Handle().idx, bgfx::getTexture(frameBuffer->Handle(), 0).idx); + EXPECT_TRUE(frameBuffer->HasDepth()); + EXPECT_EQ(frameBuffer->HasStencil(), stencil); + texture->Dispose(); + // Disposing the non-owning alias must leave the framebuffer usable. + frameBuffer->Clear(*context.GetActiveEncoder(), + BGFX_CLEAR_DEPTH | (stencil ? BGFX_CLEAR_STENCIL : 0), 0, 0.25f, 0); + frameBuffer->Dispose(); + } + } + } + auto frameBufferValue = createFrameBuffer(env.Null(), false, true); + auto* frameBuffer = frameBufferValue.As>().Get(); + EXPECT_TRUE(frameBuffer->HasDepth()); + EXPECT_FALSE(frameBuffer->HasStencil()); + frameBuffer->Dispose(); + engine.Get("dispose").As().Call(engine, {}); + } + catch (const std::exception& ex) + { + error = ex.what(); + } + completed.set_value(std::move(error)); + }); + while (future.wait_for(std::chrono::milliseconds{16}) != std::future_status::ready) + { + device.FinishRenderingCurrentFrame(); + device.StartRenderingCurrentFrame(); + } + EXPECT_EQ(future.get(), ""); + device.FinishRenderingCurrentFrame(); +} diff --git a/Plugins/NativeEngine/Source/NativeEngine.cpp b/Plugins/NativeEngine/Source/NativeEngine.cpp index 736d7a907..b250ad93c 100644 --- a/Plugins/NativeEngine/Source/NativeEngine.cpp +++ b/Plugins/NativeEngine/Source/NativeEngine.cpp @@ -2467,10 +2467,16 @@ namespace Babylon const uint32_t samples = info[5].IsUndefined() ? 1 : info[5].As().Uint32Value(); // A single render target is just the zero-or-one color attachment case of the shared implementation. + const bool requestDepthStencilTexture = texture != nullptr && !texture->IsValid(); + if (requestDepthStencilTexture && !generateDepth && !generateStencilBuffer) + { + throw Napi::Error::New(info.Env(), "An uninitialized texture requires a depth/stencil attachment"); + } Graphics::Texture* const colorTextures[]{texture}; - const gsl::span colorAttachments{colorTextures, texture != nullptr ? 1u : 0u}; + const gsl::span colorAttachments{colorTextures, texture != nullptr && !requestDepthStencilTexture ? 1u : 0u}; - return CreateFrameBufferImpl(info.Env(), colorAttachments, width, height, generateStencilBuffer, generateDepth, samples); + return CreateFrameBufferImpl(info.Env(), colorAttachments, width, height, generateStencilBuffer, generateDepth, samples, + requestDepthStencilTexture ? texture : nullptr); } Napi::Value NativeEngine::CreateMultiFrameBuffer(const Napi::CallbackInfo& info) @@ -2498,7 +2504,7 @@ namespace Babylon return CreateFrameBufferImpl(info.Env(), gsl::span{colorTextures.data(), colorCount}, width, height, generateStencilBuffer, generateDepth, samples); } - Napi::Value NativeEngine::CreateFrameBufferImpl(Napi::Env env, gsl::span colorTextures, uint16_t width, uint16_t height, bool generateStencilBuffer, bool generateDepth, uint32_t samples) + Napi::Value NativeEngine::CreateFrameBufferImpl(Napi::Env env, gsl::span colorTextures, uint16_t width, uint16_t height, bool generateStencilBuffer, bool generateDepth, uint32_t samples, Graphics::Texture* depthStencilTexture) { const bgfx::Caps* caps = bgfx::getCaps(); const uint32_t colorCount = static_cast(colorTextures.size()); @@ -2527,6 +2533,8 @@ namespace Babylon bgfx::TextureHandle depthStencilTextureHandle = BGFX_INVALID_HANDLE; int8_t depthStencilAttachmentIndex = -1; + bgfx::TextureFormat::Enum depthStencilTextureFormat = bgfx::TextureFormat::Unknown; + uint64_t depthStencilTextureFlags = BGFX_TEXTURE_NONE; if (generateStencilBuffer || generateDepth) { if (generateStencilBuffer && !generateDepth) @@ -2534,7 +2542,13 @@ namespace Babylon JsConsoleLogger::LogWarn(env, "Stencil without depth is not supported, assuming depth and stencil"); } - auto flags = BGFX_TEXTURE_RT_WRITE_ONLY | RenderTargetSamplesToBgfxMsaaFlag(samples); + const auto msaaFlag = RenderTargetSamplesToBgfxMsaaFlag(samples); + auto flags = BGFX_TEXTURE_RT_WRITE_ONLY | msaaFlag; + if (depthStencilTexture != nullptr) + { + // A standalone texture must be readable. Multisampled depth is sampled directly, not resolved. + flags = msaaFlag == BGFX_TEXTURE_NONE ? BGFX_TEXTURE_RT : msaaFlag | BGFX_TEXTURE_MSAA_SAMPLE; + } // Pick a depth(/stencil) format the active renderer actually supports as an RT. // Plain D32 is not a valid D3D11 depth RT (bgfx maps it to R24G8 with no DSV), and @@ -2561,6 +2575,8 @@ namespace Babylon throw Napi::Error::New(env, "No supported depth/stencil texture format for frame buffer"); } 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. @@ -2581,7 +2597,13 @@ namespace Babylon throw Napi::Error::New(env, "Failed to create frame buffer"); } - Graphics::FrameBuffer* frameBuffer = new Graphics::FrameBuffer(m_deviceContext, frameBufferHandle, width, height, false, generateDepth, generateStencilBuffer, depthStencilAttachmentIndex); + const bool hasDepthAttachment = generateDepth || generateStencilBuffer; + Graphics::FrameBuffer* frameBuffer = new Graphics::FrameBuffer(m_deviceContext, frameBufferHandle, width, height, false, hasDepthAttachment, generateStencilBuffer, depthStencilAttachmentIndex); + if (depthStencilTexture != nullptr) + { + // The framebuffer owns its depth attachment; expose a non-owning texture for sampling. + depthStencilTexture->Attach(depthStencilTextureHandle, width, height, false, 1, depthStencilTextureFormat, depthStencilTextureFlags); + } return Napi::Pointer::Create(env, frameBuffer, Napi::NapiPointerDeleter(frameBuffer)); } diff --git a/Plugins/NativeEngine/Source/NativeEngine.h b/Plugins/NativeEngine/Source/NativeEngine.h index 36af48a8a..35c6ba8da 100644 --- a/Plugins/NativeEngine/Source/NativeEngine.h +++ b/Plugins/NativeEngine/Source/NativeEngine.h @@ -123,7 +123,7 @@ namespace Babylon Napi::Value ReadTexture(const Napi::CallbackInfo& info); Napi::Value CreateFrameBuffer(const Napi::CallbackInfo& info); Napi::Value CreateMultiFrameBuffer(const Napi::CallbackInfo& info); - Napi::Value CreateFrameBufferImpl(Napi::Env env, gsl::span colorTextures, uint16_t width, uint16_t height, bool generateStencilBuffer, bool generateDepth, uint32_t samples); + Napi::Value CreateFrameBufferImpl(Napi::Env env, gsl::span colorTextures, uint16_t width, uint16_t height, bool generateStencilBuffer, bool generateDepth, uint32_t samples, Graphics::Texture* depthStencilTexture = nullptr); void DeleteFrameBuffer(NativeDataStream::Reader& data); void BindFrameBuffer(NativeDataStream::Reader& data); void UnbindFrameBuffer(NativeDataStream::Reader& data);