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", diff --git a/Dependencies/CMakeLists.txt b/Dependencies/CMakeLists.txt index 8aa72e1c94..a45cfb8d21 100644 --- a/Dependencies/CMakeLists.txt +++ b/Dependencies/CMakeLists.txt @@ -53,6 +53,17 @@ 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. 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() +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..a921c9c34f 100644 --- a/Polyfills/Canvas/Source/Canvas.cpp +++ b/Polyfills/Canvas/Source/Canvas.cpp @@ -176,7 +176,19 @@ 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 (!bgfx::isValid(handle)) + { + // 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); m_dirty = false; diff --git a/Polyfills/Canvas/Source/Context.cpp b/Polyfills/Canvas/Source/Context.cpp index f3ac207fe2..7864635934 100644 --- a/Polyfills/Canvas/Source/Context.cpp +++ b/Polyfills/Canvas/Source/Context.cpp @@ -661,52 +661,63 @@ namespace Babylon::Polyfills::Internal } } - void Context::Flush(const Napi::CallbackInfo&) + void Context::Flush(const Napi::CallbackInfo& info) { EnsureFontsLoaded(); - 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) + try { - 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(); + // 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); + 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); + } } - 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()) + catch (const std::exception& ex) { - // sanity check no unreleased buffers - assert(buffer.isAvailable == true); + throw Napi::Error::New(info.Env(), ex.what()); } } diff --git a/Polyfills/Canvas/Source/FrameBufferPool.cpp b/Polyfills/Canvas/Source/FrameBufferPool.cpp index 63eb5d98a5..d8ea6f720b 100644 --- a/Polyfills/Canvas/Source/FrameBufferPool.cpp +++ b/Polyfills/Canvas/Source/FrameBufferPool.cpp @@ -67,6 +67,19 @@ 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 (!bgfx::isValid(TextBuffer)) + { + // 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)"}; + } + FrameBuffer = new Graphics::FrameBuffer(*m_graphicsContext, TextBuffer, m_width, m_height, false, false, false); m_available++; mPoolBuffers.push_back({FrameBuffer, true});