From 0e64f84ff5b9ed4e3ec84ed87174df05c88218ce Mon Sep 17 00:00:00 2001 From: Gary Hsu Date: Thu, 30 Jul 2026 11:02:12 -0700 Subject: [PATCH 1/5] Size bgfx render-item arrays dynamically instead of reserving the maximum bgfx sizes its per-frame render-item arrays (sort keys, RenderItem, RenderBind) from Init::Limits::numDrawCalls. That defaults to BGFX_CONFIG_MAX_DRAW_CALLS, 65535, and is allocated up front for every Frame object, so a consumer that draws a handful of quads still reserves tens of megabytes. bgfx gained the ability to grow and shrink those arrays on demand, but it is opt-in: the defaults remain numDrawCalls = BGFX_CONFIG_MAX_DRAW_CALLS and numDrawCallPeakFrames = 0 (disabled), so simply picking up the newer bgfx changes nothing. Opt in by starting empty and letting bgfx grow up to the same BGFX_CONFIG_MAX_DRAW_CALLS ceiling, in BGFX_CONFIG_DRAW_CALL_BLOCK-sized steps. The headroom is unchanged for scenes that need it; it is just no longer paid for when unused. Lowering BGFX_CONFIG_MAX_DRAW_CALLS instead would also shrink the allocation, but it lowers the ceiling with it. Submissions past the limit are dropped rather than queued -- Frame::m_maxDrawCalls <= renderItemIdx discards the draw and counts it in m_numDropped -- so geometry disappears with nothing raised to say so. Growing on demand keeps the ceiling and the memory. Measured in a video-compositing embedder rendering 49 tiles: peak usage is 50 draw calls, and this change accounts for roughly 73 MB of a 111 MB reduction in process memory. No change to rendering output; UnitTests pass (17/17). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 714b4495-258e-4645-abf7-26c17bc29d5b --- Core/Graphics/Source/DeviceImpl.cpp | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/Core/Graphics/Source/DeviceImpl.cpp b/Core/Graphics/Source/DeviceImpl.cpp index 3b449cd9d5..480a7ad87f 100644 --- a/Core/Graphics/Source/DeviceImpl.cpp +++ b/Core/Graphics/Source/DeviceImpl.cpp @@ -40,6 +40,25 @@ namespace Babylon::Graphics auto& init = m_state.Bgfx.InitState; init.callback = &m_bgfxCallback; + // bgfx sizes its per-frame render-item arrays (sort keys, RenderItem, + // RenderBind) from limits.numDrawCalls, and by default that is + // BGFX_CONFIG_MAX_DRAW_CALLS (65535) allocated up front, for every Frame + // object. That is tens of megabytes reserved regardless of how much a + // consumer actually draws. + // + // Start empty instead and let bgfx grow the arrays on demand, up to the + // same BGFX_CONFIG_MAX_DRAW_CALLS ceiling, so the headroom is still there + // for scenes that need it but is not paid for when it is unused. Growth is + // in BGFX_CONFIG_DRAW_CALL_BLOCK-sized steps, and once grown the arrays are + // never shrunk below one such block. + // + // numDrawCallPeakFrames is how many frames a high-water mark must go + // unmatched before the arrays are shrunk again. It must be non-zero, or + // bgfx skips the resizing entirely and the arrays stay fixed at whatever + // numDrawCalls asked for. + init.limits.numDrawCalls = 0; + init.limits.numDrawCallPeakFrames = 60; + // Use the noop renderer if the configuration has no window and no size. if (config.Window == WindowT{} && config.Width == 0 && config.Height == 0) { From cb9589370ee431fea067ef78ea8271fd6f5e5c64 Mon Sep 17 00:00:00 2001 From: Gary Hsu Date: Thu, 30 Jul 2026 16:34:05 -0700 Subject: [PATCH 2/5] Correct the numDrawCalls = 0 comment bgfx::init clamps limits.numDrawCalls up to kDrawCallBlock (BGFX_CONFIG_DRAW_CALL_BLOCK, 1024 by default): init.limits.numDrawCalls = alignDrawCalls( bx::max(init.limits.numDrawCalls, kDrawCallBlock) ); so 0 asks for one block, not for an empty allocation. Frame::create -> allocArrays applies no floor of its own, which is what the previous comment described, but the clamp happens a layer above it in bgfx::init. The behaviour and the memory saving are unchanged: the reserved slot count still drops from BGFX_CONFIG_MAX_DRAW_CALLS to one block. Only the comment was wrong. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 714b4495-258e-4645-abf7-26c17bc29d5b --- Core/Graphics/Source/DeviceImpl.cpp | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Core/Graphics/Source/DeviceImpl.cpp b/Core/Graphics/Source/DeviceImpl.cpp index 480a7ad87f..bd7639507c 100644 --- a/Core/Graphics/Source/DeviceImpl.cpp +++ b/Core/Graphics/Source/DeviceImpl.cpp @@ -46,11 +46,12 @@ namespace Babylon::Graphics // object. That is tens of megabytes reserved regardless of how much a // consumer actually draws. // - // Start empty instead and let bgfx grow the arrays on demand, up to the - // same BGFX_CONFIG_MAX_DRAW_CALLS ceiling, so the headroom is still there - // for scenes that need it but is not paid for when it is unused. Growth is - // in BGFX_CONFIG_DRAW_CALL_BLOCK-sized steps, and once grown the arrays are - // never shrunk below one such block. + // Ask for the smallest bgfx allows instead and let it grow the arrays on + // demand, up to the same BGFX_CONFIG_MAX_DRAW_CALLS ceiling, so the headroom + // is still there for scenes that need it but is not paid for when it is + // unused. bgfx::init clamps numDrawCalls up to BGFX_CONFIG_DRAW_CALL_BLOCK, + // so 0 here means one block, not nothing. Growth and shrink are both in + // block-sized steps, and the arrays are never shrunk below one block. // // numDrawCallPeakFrames is how many frames a high-water mark must go // unmatched before the arrays are shrunk again. It must be non-zero, or From 89b3ebb0941c46b2b2ca6236e8d05bdb357571b6 Mon Sep 17 00:00:00 2001 From: Gary Hsu Date: Thu, 30 Jul 2026 16:59:46 -0700 Subject: [PATCH 3/5] Start at 4096 draw calls and make the starting capacity configurable Requesting the smallest capacity bgfx allows meant any scene crossing one BGFX_CONFIG_DRAW_CALL_BLOCK (1024) lost the draws past the boundary for a frame, because bgfx grows reactively: the frame that overflows discards the excess and only the next frame sees the larger arrays. Babylon Native never dropped draws before this change, so that was a regression. Start at 4096 instead, which no realistic scene begins above, and expose the value as Configuration::InitialDrawCallCapacity so an embedder that knows its own ceiling can go lower. Paired with an override of BGFX_CONFIG_DRAW_CALL_BLOCK that lets a small embedder reserve well under a block. Reserved slots still drop from BGFX_CONFIG_MAX_DRAW_CALLS (65535) to 4096, so the bulk of the saving is unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 714b4495-258e-4645-abf7-26c17bc29d5b --- .../Include/Shared/Babylon/Graphics/Device.h | 8 ++++++++ Core/Graphics/Source/DeviceImpl.cpp | 20 ++++++++++++------- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/Core/Graphics/Include/Shared/Babylon/Graphics/Device.h b/Core/Graphics/Include/Shared/Babylon/Graphics/Device.h index 2066e33a81..6e2c2e849e 100644 --- a/Core/Graphics/Include/Shared/Babylon/Graphics/Device.h +++ b/Core/Graphics/Include/Shared/Babylon/Graphics/Device.h @@ -56,6 +56,14 @@ namespace Babylon::Graphics // Format to use when creating the depth/stencil texture for the back buffer. // Specify DepthStencilFormat::None to not create a depth/stencil texture. DepthStencilFormat BackBufferDepthStencilFormat{DepthStencilFormat::Depth24Stencil8}; + + // Number of draw call slots bgfx reserves per frame up front. bgfx grows this + // on demand up to BGFX_CONFIG_MAX_DRAW_CALLS, so this is a starting point, not + // a cap. The frame that first exceeds it drops the draws past it, so lower it + // only when the scene's draw call ceiling is known. bgfx rounds the value up to + // a multiple of BGFX_CONFIG_DRAW_CALL_BLOCK (1024 by default), which is also + // the smallest capacity it will ever use. + uint32_t InitialDrawCallCapacity{4096}; }; class DeviceImpl; diff --git a/Core/Graphics/Source/DeviceImpl.cpp b/Core/Graphics/Source/DeviceImpl.cpp index bd7639507c..1d995d09ff 100644 --- a/Core/Graphics/Source/DeviceImpl.cpp +++ b/Core/Graphics/Source/DeviceImpl.cpp @@ -46,18 +46,24 @@ namespace Babylon::Graphics // object. That is tens of megabytes reserved regardless of how much a // consumer actually draws. // - // Ask for the smallest bgfx allows instead and let it grow the arrays on - // demand, up to the same BGFX_CONFIG_MAX_DRAW_CALLS ceiling, so the headroom - // is still there for scenes that need it but is not paid for when it is - // unused. bgfx::init clamps numDrawCalls up to BGFX_CONFIG_DRAW_CALL_BLOCK, - // so 0 here means one block, not nothing. Growth and shrink are both in - // block-sized steps, and the arrays are never shrunk below one block. + // Ask for a much smaller starting capacity instead and let bgfx grow the + // arrays on demand, up to the same BGFX_CONFIG_MAX_DRAW_CALLS ceiling, so the + // headroom is still there for scenes that need it but is not paid for when it + // is unused. Growth and shrink are both in BGFX_CONFIG_DRAW_CALL_BLOCK steps, + // and the arrays are never shrunk below one block. + // + // Growth is reactive: the frame that first exceeds the current capacity drops + // the draws past it and only the next frame sees the larger arrays. 4096 is + // high enough that no realistic Babylon Native scene starts above it, so the + // growth path is effectively a safety net rather than something a scene hits + // on its way up. An embedder that knows its own ceiling can go lower by + // overriding BGFX_CONFIG_DRAW_CALL_BLOCK and InitialDrawCallCapacity. // // numDrawCallPeakFrames is how many frames a high-water mark must go // unmatched before the arrays are shrunk again. It must be non-zero, or // bgfx skips the resizing entirely and the arrays stay fixed at whatever // numDrawCalls asked for. - init.limits.numDrawCalls = 0; + init.limits.numDrawCalls = config.InitialDrawCallCapacity; init.limits.numDrawCallPeakFrames = 60; // Use the noop renderer if the configuration has no window and no size. From 81d6a59df70025ccce605e178cbab3ed9f85ef0d Mon Sep 17 00:00:00 2001 From: Gary Hsu Date: Thu, 30 Jul 2026 17:28:37 -0700 Subject: [PATCH 4/5] Use BGFX_CONFIG_DRAW_CALL_BLOCK as the reserved-capacity knob Replaces the Configuration::InitialDrawCallCapacity field added in the previous commit. bgfx already has a compile-time setting for this: numDrawCalls = 0 asks for exactly one BGFX_CONFIG_DRAW_CALL_BLOCK, and the block is also the growth, shrink and floor granularity, so it is the only value a consumer needs to set. A runtime field alongside it was a second way to say the same thing. Set the block to 4096 for Babylon Native. Growth is reactive -- the frame that first exceeds capacity drops the draws past it -- so leaving the block at bgfx's default of 1024 would have cost a frame of geometry on any scene crossing 1024 draws. A consumer that knows its own ceiling can override the block down. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 714b4495-258e-4645-abf7-26c17bc29d5b --- .../Include/Shared/Babylon/Graphics/Device.h | 8 ------- Core/Graphics/Source/DeviceImpl.cpp | 23 ++++++++++--------- Dependencies/CMakeLists.txt | 1 + 3 files changed, 13 insertions(+), 19 deletions(-) diff --git a/Core/Graphics/Include/Shared/Babylon/Graphics/Device.h b/Core/Graphics/Include/Shared/Babylon/Graphics/Device.h index 6e2c2e849e..2066e33a81 100644 --- a/Core/Graphics/Include/Shared/Babylon/Graphics/Device.h +++ b/Core/Graphics/Include/Shared/Babylon/Graphics/Device.h @@ -56,14 +56,6 @@ namespace Babylon::Graphics // Format to use when creating the depth/stencil texture for the back buffer. // Specify DepthStencilFormat::None to not create a depth/stencil texture. DepthStencilFormat BackBufferDepthStencilFormat{DepthStencilFormat::Depth24Stencil8}; - - // Number of draw call slots bgfx reserves per frame up front. bgfx grows this - // on demand up to BGFX_CONFIG_MAX_DRAW_CALLS, so this is a starting point, not - // a cap. The frame that first exceeds it drops the draws past it, so lower it - // only when the scene's draw call ceiling is known. bgfx rounds the value up to - // a multiple of BGFX_CONFIG_DRAW_CALL_BLOCK (1024 by default), which is also - // the smallest capacity it will ever use. - uint32_t InitialDrawCallCapacity{4096}; }; class DeviceImpl; diff --git a/Core/Graphics/Source/DeviceImpl.cpp b/Core/Graphics/Source/DeviceImpl.cpp index 1d995d09ff..b0d79d2b3d 100644 --- a/Core/Graphics/Source/DeviceImpl.cpp +++ b/Core/Graphics/Source/DeviceImpl.cpp @@ -46,24 +46,25 @@ namespace Babylon::Graphics // object. That is tens of megabytes reserved regardless of how much a // consumer actually draws. // - // Ask for a much smaller starting capacity instead and let bgfx grow the - // arrays on demand, up to the same BGFX_CONFIG_MAX_DRAW_CALLS ceiling, so the - // headroom is still there for scenes that need it but is not paid for when it - // is unused. Growth and shrink are both in BGFX_CONFIG_DRAW_CALL_BLOCK steps, - // and the arrays are never shrunk below one block. + // Ask for the smallest capacity bgfx allows instead and let it grow the arrays + // on demand, up to the same BGFX_CONFIG_MAX_DRAW_CALLS ceiling, so the headroom + // is still there for scenes that need it but is not paid for when it is unused. + // bgfx clamps numDrawCalls up to BGFX_CONFIG_DRAW_CALL_BLOCK, so 0 asks for one + // block, not for nothing. Growth and shrink are both in block-sized steps, and + // the arrays are never shrunk below one block, which makes the block the single + // knob for how much a consumer reserves. Babylon Native sets it to 4096 in + // Dependencies/CMakeLists.txt. // // Growth is reactive: the frame that first exceeds the current capacity drops - // the draws past it and only the next frame sees the larger arrays. 4096 is - // high enough that no realistic Babylon Native scene starts above it, so the - // growth path is effectively a safety net rather than something a scene hits - // on its way up. An embedder that knows its own ceiling can go lower by - // overriding BGFX_CONFIG_DRAW_CALL_BLOCK and InitialDrawCallCapacity. + // the draws past it and only the next frame sees the larger arrays. That is why + // the block is not left at bgfx's default of 1024 -- a consumer should reserve + // above its own ceiling and treat growth as a safety net. // // numDrawCallPeakFrames is how many frames a high-water mark must go // unmatched before the arrays are shrunk again. It must be non-zero, or // bgfx skips the resizing entirely and the arrays stay fixed at whatever // numDrawCalls asked for. - init.limits.numDrawCalls = config.InitialDrawCallCapacity; + init.limits.numDrawCalls = 0; init.limits.numDrawCallPeakFrames = 60; // Use the noop renderer if the configuration has no window and no size. diff --git a/Dependencies/CMakeLists.txt b/Dependencies/CMakeLists.txt index 9f7a5a6f4c..c6b2b599e5 100644 --- a/Dependencies/CMakeLists.txt +++ b/Dependencies/CMakeLists.txt @@ -46,6 +46,7 @@ FetchContent_MakeAvailable_With_Message(bgfx.cmake) target_compile_definitions(bgfx PRIVATE BGFX_CONFIG_DEBUG_ANNOTATION=0) target_compile_definitions(bgfx PRIVATE BGFX_CONFIG_DEFAULT_MAX_ENCODERS=2) +target_compile_definitions(bgfx PRIVATE BGFX_CONFIG_DRAW_CALL_BLOCK=4096) target_compile_definitions(bgfx PRIVATE BGFX_CONFIG_MAX_VERTEX_STREAMS=18) target_compile_definitions(bgfx PRIVATE BGFX_CONFIG_MIN_RESOURCE_COMMAND_BUFFER_SIZE=16) target_compile_definitions(bgfx PRIVATE BGFX_CONFIG_MIN_UNIFORM_BUFFER_SIZE=4096) From be3b4bf01ac01b8865b22efa3c98019f6a904132 Mon Sep 17 00:00:00 2001 From: Gary Hsu Date: Thu, 30 Jul 2026 17:39:14 -0700 Subject: [PATCH 5/5] Trim the numDrawCalls comment Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 714b4495-258e-4645-abf7-26c17bc29d5b --- Core/Graphics/Source/DeviceImpl.cpp | 31 +++++++++-------------------- 1 file changed, 9 insertions(+), 22 deletions(-) diff --git a/Core/Graphics/Source/DeviceImpl.cpp b/Core/Graphics/Source/DeviceImpl.cpp index b0d79d2b3d..4ebfec0bc5 100644 --- a/Core/Graphics/Source/DeviceImpl.cpp +++ b/Core/Graphics/Source/DeviceImpl.cpp @@ -40,30 +40,17 @@ namespace Babylon::Graphics auto& init = m_state.Bgfx.InitState; init.callback = &m_bgfxCallback; - // bgfx sizes its per-frame render-item arrays (sort keys, RenderItem, - // RenderBind) from limits.numDrawCalls, and by default that is - // BGFX_CONFIG_MAX_DRAW_CALLS (65535) allocated up front, for every Frame - // object. That is tens of megabytes reserved regardless of how much a - // consumer actually draws. + // bgfx reserves limits.numDrawCalls render-item slots (sort keys, RenderItem, + // RenderBind) up front for every Frame, and defaults to + // BGFX_CONFIG_MAX_DRAW_CALLS (65535) -- tens of megabytes. // - // Ask for the smallest capacity bgfx allows instead and let it grow the arrays - // on demand, up to the same BGFX_CONFIG_MAX_DRAW_CALLS ceiling, so the headroom - // is still there for scenes that need it but is not paid for when it is unused. - // bgfx clamps numDrawCalls up to BGFX_CONFIG_DRAW_CALL_BLOCK, so 0 asks for one - // block, not for nothing. Growth and shrink are both in block-sized steps, and - // the arrays are never shrunk below one block, which makes the block the single - // knob for how much a consumer reserves. Babylon Native sets it to 4096 in - // Dependencies/CMakeLists.txt. + // 0 asks for a single BGFX_CONFIG_DRAW_CALL_BLOCK instead (4096, set in + // Dependencies/CMakeLists.txt), and bgfx resizes in block steps up to that same + // ceiling. Growth is reactive -- the frame that overflows drops the draws past + // capacity -- so the block should sit above the expected peak. // - // Growth is reactive: the frame that first exceeds the current capacity drops - // the draws past it and only the next frame sees the larger arrays. That is why - // the block is not left at bgfx's default of 1024 -- a consumer should reserve - // above its own ceiling and treat growth as a safety net. - // - // numDrawCallPeakFrames is how many frames a high-water mark must go - // unmatched before the arrays are shrunk again. It must be non-zero, or - // bgfx skips the resizing entirely and the arrays stay fixed at whatever - // numDrawCalls asked for. + // numDrawCallPeakFrames is the shrink delay in frames, ~1s at 60fps. Resizing + // is disabled entirely at 0. init.limits.numDrawCalls = 0; init.limits.numDrawCallPeakFrames = 60;