From 330d2d32a00c65543635069bd2d6a307920f4790 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Branimir=20Karad=C5=BEi=C4=87?= Date: Thu, 21 May 2026 13:55:27 -0700 Subject: [PATCH 1/3] Survive Canvas framebuffer pool exhaustion instead of aborting. The Canvas polyfill creates a fresh bgfx framebuffer per JS Canvas instance and per text-rendering operation. Under V8's GC pacing, finalizers (which release the framebuffers) only run in batches, so a long playground sweep can outpace the default 128-slot bgfx pool. The first allocation past the cliff returned BGFX_INVALID_HANDLE, the bare assert() at Canvas.cpp tripped (Debug) or the invalid handle silently bled into Graphics::FrameBuffer/bgfx-internal arrays (Release, later AV at bgfx_p.h:5526). The whole run aborted regardless of which test happened to be unlucky. Three changes: - Dependencies/CMakeLists.txt now propagates BGFX_CONFIG_MAX_FRAME_BUFFERS to the bgfx target as a compile definition, defaulting to 512 (4x the bgfx default) and honoring any -D override the way CI workflows already pass it. Previously the CI -D was a no-op. - Canvas.cpp UpdateRenderTarget() now throws std::runtime_error when createFrameBuffer returns kInvalidHandle and frees the two textures it just allocated. Context::Flush wraps the call and rethrows as Napi::Error so the offending test fails cleanly without taking the rest of the sweep down. - FrameBufferPool::Add gets the same treatment for symmetry. The pool bump alone is expected to remove the cascade in practice; the throw paths are defense-in-depth so future regressions surface as a catchable JS error instead of a silent process kill. Refs BabylonJS/BabylonNative cascade-victim discussion (113 playground tests previously excluded with reasons like "Suspected to corrupt state causing later D3D11 cascade crash"). --- Dependencies/CMakeLists.txt | 9 +++++++++ Polyfills/Canvas/Source/Canvas.cpp | 8 +++++++- Polyfills/Canvas/Source/Context.cpp | 15 +++++++++++++-- Polyfills/Canvas/Source/FrameBufferPool.cpp | 7 +++++++ 4 files changed, 36 insertions(+), 3 deletions(-) diff --git a/Dependencies/CMakeLists.txt b/Dependencies/CMakeLists.txt index 8aa72e1c94..d0d7326d8a 100644 --- a/Dependencies/CMakeLists.txt +++ b/Dependencies/CMakeLists.txt @@ -53,6 +53,15 @@ target_compile_definitions(bgfx PRIVATE BGFX_CONFIG_UNIFORM_BUFFER_RESIZE_THRESH target_compile_definitions(bgfx PRIVATE BGFX_CONFIG_UNIFORM_BUFFER_RESIZE_INCREMENT_SIZE=1024) target_compile_definitions(bgfx PUBLIC BGFX_PLATFORM_SUPPORTS_WGSL=0) +# Canvas plugin allocates one bgfx framebuffer per JS Canvas object and per +# text-rendering operation; combined with V8 GC pacing this can exceed the +# default 128 limit during long playground sweeps. CI workflows pass +# -DBGFX_CONFIG_MAX_FRAME_BUFFERS as a CMake variable, so honour it here. +if(NOT BGFX_CONFIG_MAX_FRAME_BUFFERS OR BGFX_CONFIG_MAX_FRAME_BUFFERS LESS 512) + set(BGFX_CONFIG_MAX_FRAME_BUFFERS 512) +endif() +target_compile_definitions(bgfx PRIVATE BGFX_CONFIG_MAX_FRAME_BUFFERS=${BGFX_CONFIG_MAX_FRAME_BUFFERS}) + # Temporary disable uniform debug. target_compile_definitions(bgfx PRIVATE BGFX_CONFIG_DEBUG_UNIFORM=0) diff --git a/Polyfills/Canvas/Source/Canvas.cpp b/Polyfills/Canvas/Source/Canvas.cpp index 458173a38f..9fc114fd07 100644 --- a/Polyfills/Canvas/Source/Canvas.cpp +++ b/Polyfills/Canvas/Source/Canvas.cpp @@ -176,7 +176,13 @@ namespace Babylon::Polyfills::Internal attachments[0].init(textures[0], bgfx::Access::Write, 0, 1, 0, colorResolve); attachments[1].init(textures[1], bgfx::Access::Write, 0, 1, 0, BGFX_RESOLVE_NONE); auto handle = bgfx::createFrameBuffer(static_cast(attachments.size()), attachments.data(), true); - assert(handle.idx != bgfx::kInvalidHandle); + if (handle.idx == bgfx::kInvalidHandle) + { + // Free the textures we just allocated so they don't leak alongside the failed FB. + bgfx::destroy(textures[0]); + bgfx::destroy(textures[1]); + throw std::runtime_error{"bgfx::createFrameBuffer returned invalid handle (framebuffer pool exhausted; raise BGFX_CONFIG_MAX_FRAME_BUFFERS or audit Canvas/Context lifetime)"}; + } m_frameBuffer = std::make_unique(m_graphicsContext, handle, m_width, m_height, false, false, false); m_dirty = false; diff --git a/Polyfills/Canvas/Source/Context.cpp b/Polyfills/Canvas/Source/Context.cpp index f3ac207fe2..cff29af2f6 100644 --- a/Polyfills/Canvas/Source/Context.cpp +++ b/Polyfills/Canvas/Source/Context.cpp @@ -661,11 +661,22 @@ namespace Babylon::Polyfills::Internal } } - void Context::Flush(const Napi::CallbackInfo&) + void Context::Flush(const Napi::CallbackInfo& info) { EnsureFontsLoaded(); - bool needClear = m_canvas->UpdateRenderTarget(); + bool needClear = false; + try + { + needClear = m_canvas->UpdateRenderTarget(); + } + catch (const std::exception& ex) + { + // Convert C++ failures (e.g. bgfx framebuffer pool exhaustion) into a + // catchable JS error so the offending test fails cleanly instead of + // aborting the whole sweep. + throw Napi::Error::New(info.Env(), ex.what()); + } Graphics::FrameBuffer& frameBuffer = m_canvas->GetFrameBuffer(); diff --git a/Polyfills/Canvas/Source/FrameBufferPool.cpp b/Polyfills/Canvas/Source/FrameBufferPool.cpp index 63eb5d98a5..e4d814161d 100644 --- a/Polyfills/Canvas/Source/FrameBufferPool.cpp +++ b/Polyfills/Canvas/Source/FrameBufferPool.cpp @@ -67,6 +67,13 @@ namespace Babylon::Polyfills attachments[1].init(textures[1], bgfx::Access::Write, 0, 1, 0, BGFX_RESOLVE_NONE); TextBuffer = bgfx::createFrameBuffer(static_cast(attachments.size()), attachments.data(), true); + if (TextBuffer.idx == bgfx::kInvalidHandle) + { + bgfx::destroy(textures[0]); + bgfx::destroy(textures[1]); + throw std::runtime_error{"FrameBufferPool::Add: bgfx::createFrameBuffer returned invalid handle (pool exhausted)"}; + } + FrameBuffer = new Graphics::FrameBuffer(*m_graphicsContext, TextBuffer, m_width, m_height, false, false, false); m_available++; mPoolBuffers.push_back({FrameBuffer, true}); From dc48972d4f88584c426d5f5b09deba89c246729f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Branimir=20Karad=C5=BEi=C4=87?= Date: Thu, 4 Jun 2026 10:46:10 -0700 Subject: [PATCH 2/3] Re-enable 25 crash-cascade victim tests fixed by Canvas FB-pool fix These tests were excluded as Win32 D3D11 ACCESS_VIOLATION / STATUS_BREAKPOINT cascade victims caused by Canvas framebuffer pool exhaustion. The FB-pool fix in this PR stops that crash. Each of these 25 re-enables was verified to cause zero downstream test regressions in a full headless sweep on BOTH the D3D11 and ANGLE/OpenGL backends (185 ran / 185 passed, 0 failures each). The remaining cascade-victim re-enables are deferred to a follow-up PR: a single test, cube-with-holes-using-stencil-buffer (#CW5PRI#11), was identified as a render-state corruptor that leaves the stencil buffer dirty and causes a ~38-test downstream pixel cascade on both backends; it needs its own fix before its neighbours can be safely re-enabled. --- Apps/Playground/Scripts/config.json | 98 +++++++---------------------- 1 file changed, 24 insertions(+), 74 deletions(-) diff --git a/Apps/Playground/Scripts/config.json b/Apps/Playground/Scripts/config.json index ae53495b6d..b41b60366a 100644 --- a/Apps/Playground/Scripts/config.json +++ b/Apps/Playground/Scripts/config.json @@ -486,40 +486,30 @@ "title": "GLTF Node NegativeScale (0)", "playgroundId": "#DS8AA7#27", "replace": "__folder__, Node_NegativeScale, __page__, 0, __disableSRGBBuffers__, 0", - "referenceImage": "gltfNodeNegativeScale0.png", - "excludeFromAutomaticTesting": true, - "reason": "Disabled to land sync PR; V8 cascade victim that crashes Win32 V8 D3D11 with ACCESS_VIOLATION (-1073741819). Will be re-enabled in follow-up fix PR." + "referenceImage": "gltfNodeNegativeScale0.png" }, { "title": "GLTF Node NegativeScale (1)", "playgroundId": "#DS8AA7#27", "replace": "__folder__, Node_NegativeScale, __page__, 1, __disableSRGBBuffers__, 0", - "referenceImage": "gltfNodeNegativeScale1.png", - "excludeFromAutomaticTesting": true, - "reason": "Disabled to land sync PR; V8 cascade victim that crashes Win32 V8 D3D11 with ACCESS_VIOLATION (-1073741819). Will be re-enabled in follow-up fix PR." + "referenceImage": "gltfNodeNegativeScale1.png" }, { "title": "GLTF Texture Sampler (0)", "playgroundId": "#DS8AA7#27", "replace": "__folder__, Texture_Sampler, __page__, 0, __disableSRGBBuffers__, 0", - "referenceImage": "gltfTextureSampler0.png", - "excludeFromAutomaticTesting": true, - "reason": "Disabled to land sync PR; V8 cascade victim that crashes Win32 V8 D3D11 with ACCESS_VIOLATION (-1073741819). Will be re-enabled in follow-up fix PR." + "referenceImage": "gltfTextureSampler0.png" }, { "title": "GLTF Texture Sampler (1)", "playgroundId": "#DS8AA7#27", "replace": "__folder__, Texture_Sampler, __page__, 1, __disableSRGBBuffers__, 0", - "referenceImage": "gltfTextureSampler1.png", - "excludeFromAutomaticTesting": true, - "reason": "Disabled to land sync PR; V8 cascade victim that crashes Win32 V8 D3D11 with ACCESS_VIOLATION (-1073741819). Will be re-enabled in follow-up fix PR." + "referenceImage": "gltfTextureSampler1.png" }, { "title": "GLTF Alien", "playgroundId": "#XN37SR#5", - "referenceImage": "gltfAlien.png", - "excludeFromAutomaticTesting": true, - "reason": "Disabled to land sync PR; V8 cascade victim that crashes Win32 V8 D3D11 with ACCESS_VIOLATION (-1073741819). Will be re-enabled in follow-up fix PR." + "referenceImage": "gltfAlien.png" }, { "title": "Interleaved buffers test", @@ -622,8 +612,6 @@ { "title": "NPE - Angle align", "playgroundId": "#H5RP91", - "excludeFromAutomaticTesting": true, - "reason": "Test crashes or hangs on Babylon Native", "referenceImage": "npe-angle-align.png" }, { @@ -657,9 +645,7 @@ { "title": "RH billboard2", "playgroundId": "#QDVYS4#4", - "referenceImage": "rh-billboard2.png", - "excludeFromAutomaticTesting": true, - "reason": "Disabled to land sync PR; V8 cascade victim that crashes Win32 V8 D3D11 with ACCESS_VIOLATION (-1073741819). Will be re-enabled in follow-up fix PR." + "referenceImage": "rh-billboard2.png" }, { "title": "MSDF", @@ -789,9 +775,7 @@ { "title": "Lattice", "playgroundId": "#MDVD75#19", - "referenceImage": "lattice.png", - "excludeFromAutomaticTesting": true, - "reason": "Disabled to land sync PR; V8 cascade victim that crashes Win32 V8 D3D11 with ACCESS_VIOLATION (-1073741819). Will be re-enabled in follow-up fix PR." + "referenceImage": "lattice.png" }, { "title": "HAL Lattice", @@ -803,38 +787,28 @@ { "title": "NME Loop Block", "playgroundId": "#D8AK3Z#115", - "referenceImage": "nme-loop.png", - "excludeFromAutomaticTesting": true, - "reason": "Test crashes on Win32 V8 D3D11 (access violation)" + "referenceImage": "nme-loop.png" }, { "title": "Gaussian Splatting Loading", "playgroundId": "#CID4NN#204", "renderCount": 15, - "referenceImage": "gsplat-loading.png", - "excludeFromAutomaticTesting": true, - "reason": "Test crashes on Win32 D3D11 / Sanitizers / V8 D3D11" + "referenceImage": "gsplat-loading.png" }, { "title": "Simple refraction", "playgroundId": "#22KZUW#652", - "referenceImage": "simple-refraction.png", - "excludeFromAutomaticTesting": true, - "reason": "Test crashes on Win32 V8 D3D11 (access violation)" + "referenceImage": "simple-refraction.png" }, { "title": "Detail map", "playgroundId": "#F8FDYT#2", - "referenceImage": "detail-map.png", - "excludeFromAutomaticTesting": true, - "reason": "Test crashes on Win32 D3D11 / Sanitizers (STATUS_BREAKPOINT)" + "referenceImage": "detail-map.png" }, { "title": "needDepthPrePass", "playgroundId": "#7A66KI#1", - "referenceImage": "needDepthPrePass.png", - "excludeFromAutomaticTesting": true, - "reason": "Test crashes on Win32 V8 D3D11 (access violation)" + "referenceImage": "needDepthPrePass.png" }, { "title": "Decal map PP", @@ -847,16 +821,12 @@ { "title": "Convolution Post Process", "playgroundId": "#FBH4J7#281", - "referenceImage": "convolution.png", - "excludeFromAutomaticTesting": true, - "reason": "Test crashes on Win32 D3D11 / Sanitizers (STATUS_BREAKPOINT)" + "referenceImage": "convolution.png" }, { "title": "Volumetric Light Scattering Post Process with Skeleton", "playgroundId": "#5E318S#6", - "referenceImage": "volumetricLightScatteringSkeleton.png", - "excludeFromAutomaticTesting": true, - "reason": "Triggers ACCESS_VIOLATION cascade on Win32 V8 D3D11 -- the next test always crashes regardless of which one it is. Likely heap corruption." + "referenceImage": "volumetricLightScatteringSkeleton.png" }, { "title": "Volumetric Light Scattering Post Process with Morph Targets", @@ -902,9 +872,7 @@ { "title": "Trailmesh tapered and untapered", "playgroundId": "#XGGSWJ#4", - "referenceImage": "trailMesh.png", - "excludeFromAutomaticTesting": true, - "reason": "Test crashes on Win32 V8 D3D11 (access violation cascade)" + "referenceImage": "trailMesh.png" }, { "title": "GUI Input Text Area with Placeholder", @@ -916,16 +884,12 @@ { "title": "Ground Projection", "playgroundId": "#XG08YC#0", - "referenceImage": "groundProjection.png", - "excludeFromAutomaticTesting": true, - "reason": "Test crashes on Win32 V8 D3D11 (access violation cascade)" + "referenceImage": "groundProjection.png" }, { "title": "Node geometry", "playgroundId": "#WGZLGJ#9152", - "referenceImage": "nodeGeometry.png", - "excludeFromAutomaticTesting": true, - "reason": "Test crashes on Win32 V8 D3D11 (access violation cascade)" + "referenceImage": "nodeGeometry.png" }, { "title": "Node Geometry - Building generator", @@ -965,30 +929,22 @@ { "title": "Negative scaling with instances", "playgroundId": "#Z3YS9T#0", - "referenceImage": "negative-scaling-with-instances.png", - "excludeFromAutomaticTesting": true, - "reason": "Test crashes on Win32 V8 D3D11 (access violation cascade)" + "referenceImage": "negative-scaling-with-instances.png" }, { "title": "Iridescence", "playgroundId": "#2FDQT5#1505", - "referenceImage": "iridescence.png", - "excludeFromAutomaticTesting": true, - "reason": "Test crashes on Win32 V8 D3D11 (access violation cascade)" + "referenceImage": "iridescence.png" }, { "title": "Merge Meshes", "playgroundId": "#484RHA#0", - "referenceImage": "mergemeshes.png", - "excludeFromAutomaticTesting": true, - "reason": "Test crashes on Win32 V8 D3D11 (access violation cascade)" + "referenceImage": "mergemeshes.png" }, { "title": "Sprites", "playgroundId": "#ZX8DJ3#1", - "referenceImage": "Sprites.png", - "excludeFromAutomaticTesting": true, - "reason": "Disabled to land sync PR; V8 cascade victim that crashes Win32 V8 D3D11 with ACCESS_VIOLATION (-1073741819). Will be re-enabled in follow-up fix PR." + "referenceImage": "Sprites.png" }, { "title": "Procedural texture with NME", @@ -1034,9 +990,7 @@ { "title": "Node material 3", "playgroundId": "#LWGVT0#2", - "referenceImage": "node-material3.png", - "excludeFromAutomaticTesting": true, - "reason": "Newly added test crashes Win32 V8 D3D11 with ACCESS_VIOLATION (-1073741819)." + "referenceImage": "node-material3.png" }, { "title": "Node material 4", @@ -1048,9 +1002,7 @@ { "title": "Node material 5", "playgroundId": "#V8VH7B#0", - "referenceImage": "node-material5.png", - "excludeFromAutomaticTesting": true, - "reason": "Newly added test crashes Win32 V8 D3D11 with ACCESS_VIOLATION (-1073741819)." + "referenceImage": "node-material5.png" }, { "title": "Node material 6", @@ -1170,9 +1122,7 @@ { "title": "Color Grading", "playgroundId": "#8EDB5N#2", - "referenceImage": "colorGrading.png", - "excludeFromAutomaticTesting": true, - "reason": "Newly added test crashes Win32 V8 D3D11 with ACCESS_VIOLATION (-1073741819)." + "referenceImage": "colorGrading.png" }, { "title": "Clip planes", From 4cd33dfaca87329d80a01023221619ff6580da55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Branimir=20Karad=C5=BEi=C4=87?= Date: Thu, 4 Jun 2026 12:34:42 -0700 Subject: [PATCH 3/3] Canvas: harden FB-pool exhaustion error paths (review feedback) - Guard cleanup destroy() calls with bgfx::isValid so a failed createTexture2D doesn't trigger a destroy-on-invalid-handle assert (Canvas.cpp, FrameBufferPool.cpp). - Widen Context::Flush try/catch to cover the whole flush so pool-exhaustion thrown from FrameBufferPool::Acquire() during nvgEndFrame() is also converted to a catchable JS error instead of aborting the whole sweep. - Clarify the CMake comment to document that BGFX_CONFIG_MAX_FRAME_BUFFERS is clamped to a floor of 512 (CI passes 256, which is intentionally raised). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Dependencies/CMakeLists.txt | 6 +- Polyfills/Canvas/Source/Canvas.cpp | 14 ++- Polyfills/Canvas/Source/Context.cpp | 94 ++++++++++----------- Polyfills/Canvas/Source/FrameBufferPool.cpp | 12 ++- 4 files changed, 70 insertions(+), 56 deletions(-) diff --git a/Dependencies/CMakeLists.txt b/Dependencies/CMakeLists.txt index d0d7326d8a..a45cfb8d21 100644 --- a/Dependencies/CMakeLists.txt +++ b/Dependencies/CMakeLists.txt @@ -55,8 +55,10 @@ target_compile_definitions(bgfx PUBLIC BGFX_PLATFORM_SUPPORTS_WGSL=0) # Canvas plugin allocates one bgfx framebuffer per JS Canvas object and per # text-rendering operation; combined with V8 GC pacing this can exceed the -# default 128 limit during long playground sweeps. CI workflows pass -# -DBGFX_CONFIG_MAX_FRAME_BUFFERS as a CMake variable, so honour it here. +# default 128 limit during long playground sweeps. We enforce a floor of 512: +# CI workflows pass -DBGFX_CONFIG_MAX_FRAME_BUFFERS, and any value below 512 +# (including a smaller CI override) is clamped up so the pool never regresses +# below the size the Canvas sweeps need. if(NOT BGFX_CONFIG_MAX_FRAME_BUFFERS OR BGFX_CONFIG_MAX_FRAME_BUFFERS LESS 512) set(BGFX_CONFIG_MAX_FRAME_BUFFERS 512) endif() diff --git a/Polyfills/Canvas/Source/Canvas.cpp b/Polyfills/Canvas/Source/Canvas.cpp index 9fc114fd07..a921c9c34f 100644 --- a/Polyfills/Canvas/Source/Canvas.cpp +++ b/Polyfills/Canvas/Source/Canvas.cpp @@ -176,11 +176,17 @@ namespace Babylon::Polyfills::Internal attachments[0].init(textures[0], bgfx::Access::Write, 0, 1, 0, colorResolve); attachments[1].init(textures[1], bgfx::Access::Write, 0, 1, 0, BGFX_RESOLVE_NONE); auto handle = bgfx::createFrameBuffer(static_cast(attachments.size()), attachments.data(), true); - if (handle.idx == bgfx::kInvalidHandle) + if (!bgfx::isValid(handle)) { - // Free the textures we just allocated so they don't leak alongside the failed FB. - bgfx::destroy(textures[0]); - bgfx::destroy(textures[1]); + // Free any textures we managed to allocate so they don't leak alongside the failed FB. + // Guard with isValid: a failed createTexture2D returns an invalid handle that bgfx::destroy would assert on. + for (auto texture : textures) + { + if (bgfx::isValid(texture)) + { + bgfx::destroy(texture); + } + } throw std::runtime_error{"bgfx::createFrameBuffer returned invalid handle (framebuffer pool exhausted; raise BGFX_CONFIG_MAX_FRAME_BUFFERS or audit Canvas/Context lifetime)"}; } m_frameBuffer = std::make_unique(m_graphicsContext, handle, m_width, m_height, false, false, false); diff --git a/Polyfills/Canvas/Source/Context.cpp b/Polyfills/Canvas/Source/Context.cpp index cff29af2f6..7864635934 100644 --- a/Polyfills/Canvas/Source/Context.cpp +++ b/Polyfills/Canvas/Source/Context.cpp @@ -665,60 +665,60 @@ namespace Babylon::Polyfills::Internal { EnsureFontsLoaded(); - bool needClear = false; try { - needClear = m_canvas->UpdateRenderTarget(); + // The entire flush is wrapped: bgfx framebuffer pool exhaustion can throw both from + // UpdateRenderTarget() and from FrameBufferPool::Acquire() reached via nvgEndFrame() + // below. Converting any such C++ failure into a catchable JS error lets the offending + // test fail cleanly instead of aborting the whole sweep. + const bool needClear = m_canvas->UpdateRenderTarget(); + + Graphics::FrameBuffer& frameBuffer = m_canvas->GetFrameBuffer(); + + auto updateToken{m_update.GetUpdateToken()}; + bgfx::Encoder* encoder = updateToken.GetEncoder(); + frameBuffer.Bind(*encoder); + if (needClear) + { + frameBuffer.Clear(*encoder, BGFX_CLEAR_COLOR | BGFX_CLEAR_DEPTH | BGFX_CLEAR_STENCIL, 0, 1.f, 0); + } + frameBuffer.SetViewPort(*encoder, 0.f, 0.f, 1.f, 1.f); + const auto width = m_canvas->GetWidth(); + const auto height = m_canvas->GetHeight(); + + for (auto& buffer : m_canvas->m_frameBufferPool.GetPoolBuffers()) + { + // sanity check no buffers should have been acquired yet + assert(buffer.isAvailable == true); + } + std::function acquire = [this, encoder]() -> Babylon::Graphics::FrameBuffer* { + Babylon::Graphics::FrameBuffer *frameBuffer = this->m_canvas->m_frameBufferPool.Acquire(); + frameBuffer->Bind(*encoder); + return frameBuffer; + }; + std::function release = [this, encoder](Babylon::Graphics::FrameBuffer* frameBuffer) -> void { + // clear framebuffer when released + frameBuffer->Clear(*encoder, BGFX_CLEAR_COLOR | BGFX_CLEAR_DEPTH | BGFX_CLEAR_STENCIL, 0, 1.f, 0); + this->m_canvas->m_frameBufferPool.Release(frameBuffer); + frameBuffer->Unbind(*encoder); + }; + + nvgBeginFrame(*m_nvg, float(width), float(height), 1.0f); + nvgSetFrameBufferAndEncoder(*m_nvg, frameBuffer, encoder); + nvgSetFrameBufferPool(*m_nvg, { acquire, release }); + nvgEndFrame(*m_nvg); + frameBuffer.Unbind(*encoder); + + for (auto& buffer : m_canvas->m_frameBufferPool.GetPoolBuffers()) + { + // sanity check no unreleased buffers + assert(buffer.isAvailable == true); + } } catch (const std::exception& ex) { - // Convert C++ failures (e.g. bgfx framebuffer pool exhaustion) into a - // catchable JS error so the offending test fails cleanly instead of - // aborting the whole sweep. throw Napi::Error::New(info.Env(), ex.what()); } - - Graphics::FrameBuffer& frameBuffer = m_canvas->GetFrameBuffer(); - - auto updateToken{m_update.GetUpdateToken()}; - bgfx::Encoder* encoder = updateToken.GetEncoder(); - frameBuffer.Bind(*encoder); - if (needClear) - { - frameBuffer.Clear(*encoder, BGFX_CLEAR_COLOR | BGFX_CLEAR_DEPTH | BGFX_CLEAR_STENCIL, 0, 1.f, 0); - } - frameBuffer.SetViewPort(*encoder, 0.f, 0.f, 1.f, 1.f); - const auto width = m_canvas->GetWidth(); - const auto height = m_canvas->GetHeight(); - - for (auto& buffer : m_canvas->m_frameBufferPool.GetPoolBuffers()) - { - // sanity check no buffers should have been acquired yet - assert(buffer.isAvailable == true); - } - std::function acquire = [this, encoder]() -> Babylon::Graphics::FrameBuffer* { - Babylon::Graphics::FrameBuffer *frameBuffer = this->m_canvas->m_frameBufferPool.Acquire(); - frameBuffer->Bind(*encoder); - return frameBuffer; - }; - std::function release = [this, encoder](Babylon::Graphics::FrameBuffer* frameBuffer) -> void { - // clear framebuffer when released - frameBuffer->Clear(*encoder, BGFX_CLEAR_COLOR | BGFX_CLEAR_DEPTH | BGFX_CLEAR_STENCIL, 0, 1.f, 0); - this->m_canvas->m_frameBufferPool.Release(frameBuffer); - frameBuffer->Unbind(*encoder); - }; - - nvgBeginFrame(*m_nvg, float(width), float(height), 1.0f); - nvgSetFrameBufferAndEncoder(*m_nvg, frameBuffer, encoder); - nvgSetFrameBufferPool(*m_nvg, { acquire, release }); - nvgEndFrame(*m_nvg); - frameBuffer.Unbind(*encoder); - - for (auto& buffer : m_canvas->m_frameBufferPool.GetPoolBuffers()) - { - // sanity check no unreleased buffers - assert(buffer.isAvailable == true); - } } void Context::PutImageData(const Napi::CallbackInfo&) diff --git a/Polyfills/Canvas/Source/FrameBufferPool.cpp b/Polyfills/Canvas/Source/FrameBufferPool.cpp index e4d814161d..d8ea6f720b 100644 --- a/Polyfills/Canvas/Source/FrameBufferPool.cpp +++ b/Polyfills/Canvas/Source/FrameBufferPool.cpp @@ -67,10 +67,16 @@ namespace Babylon::Polyfills attachments[1].init(textures[1], bgfx::Access::Write, 0, 1, 0, BGFX_RESOLVE_NONE); TextBuffer = bgfx::createFrameBuffer(static_cast(attachments.size()), attachments.data(), true); - if (TextBuffer.idx == bgfx::kInvalidHandle) + if (!bgfx::isValid(TextBuffer)) { - bgfx::destroy(textures[0]); - bgfx::destroy(textures[1]); + // Guard with isValid: a failed createTexture2D returns an invalid handle that bgfx::destroy would assert on. + for (auto texture : textures) + { + if (bgfx::isValid(texture)) + { + bgfx::destroy(texture); + } + } throw std::runtime_error{"FrameBufferPool::Add: bgfx::createFrameBuffer returned invalid handle (pool exhausted)"}; }