Open
Conversation
Contributor
There was a problem hiding this comment.
Pull request overview
This PR reworks the graphics threading/synchronization model by replacing per-thread encoders and the SafeTimespan/UpdateToken mechanism with a single per-frame bgfx encoder owned by DeviceImpl and a new FrameCompletionScope gate/counter for cross-thread frame completion synchronization.
Changes:
- Removed
SafeTimespanGuarantor,UpdateToken/Update, and per-thread encoder management; introduced a single frame encoder lifecycle inDeviceImpl. - Added
FrameCompletionScope+ frame-start/before-render/after-render scheduling to coordinate JS-thread work withbgfx::frame(). - Updated NativeEngine/NativeXR/Canvas/FrameBuffer APIs to use
GetActiveEncoder()and internal encoder acquisition patterns instead of passing encoders through call chains.
Reviewed changes
Copilot reviewed 21 out of 21 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| Core/Graphics/Source/DeviceImpl.h | Adds single-frame encoder pointer and frame gate (mutex/CV + pending scope count). |
| Core/Graphics/Source/DeviceImpl.cpp | Implements frame open/close sequencing, scope blocking, and frame-start dispatcher tick. |
| Core/Graphics/InternalInclude/Babylon/Graphics/DeviceContext.h | Replaces Update/UpdateToken with FrameCompletionScope, adds frame-start scheduler + encoder accessors. |
| Core/Graphics/Source/DeviceContext.cpp | Implements FrameCompletionScope RAII and forwards new scheduling/encoder methods. |
| Core/Graphics/Include/Shared/Babylon/Graphics/Device.h | Converts DeviceUpdate into a no-op compatibility shim and inlines GetUpdate. |
| Core/Graphics/Source/Device.cpp | Removes old GetUpdate implementation. |
| Core/Graphics/InternalInclude/Babylon/Graphics/FrameBuffer.h | Removes encoder parameters from bind/unbind/viewport/scissor APIs. |
| Core/Graphics/Source/FrameBuffer.cpp | Updates FrameBuffer internals to acquire view IDs without needing an encoder parameter. |
| Plugins/NativeEngine/Source/NativeEngine.h | Switches to active-encoder access + outside-frame scope holding; adds begin/end frame no-ops. |
| Plugins/NativeEngine/Source/NativeEngine.cpp | Updates command submission and RAF scheduling to use frame-start scheduling + scopes; updates ReadTexture blit scheduling. |
| Plugins/NativeEngine/Source/PerFrameValue.h | Adjusts API to remove encoder parameters (but currently has a type-signature issue). |
| Plugins/NativeXr/Source/NativeXrImpl.h | Removes stored Update member from XR session state. |
| Plugins/NativeXr/Source/NativeXrImpl.cpp | Migrates XR scheduling to frame-start + scopes; updates encoder usage. |
| Polyfills/Canvas/Source/Context.h | Removes stored Update member. |
| Polyfills/Canvas/Source/Context.cpp | Uses GetActiveEncoder() and acquires a FrameCompletionScope when flushing outside the frame cycle. |
| Polyfills/Canvas/Source/nanovg/nanovg_babylon.cpp | Updates FrameBuffer binding calls to new signature (no encoder param). |
| Apps/HeadlessScreenshotApp/Win32/App.cpp | Wraps startup wait with Start/Finish frame calls to keep gate open during startup. |
| Apps/StyleTransferApp/Win32/App.cpp | Same startup framing change as HeadlessScreenshotApp. |
| Core/Graphics/CMakeLists.txt | Removes SafeTimespanGuarantor sources from build. |
Comments suppressed due to low confidence (1)
Plugins/NativeEngine/Source/PerFrameValue.h:33
PerFrameValueis templated onT, butSetcurrently takes aboolparameter. This makes the template misleading and will break or silently convert ifPerFrameValueis ever instantiated with a non-bool type. ChangeSetto acceptT(e.g.,const T&orT) to match the class template parameter.
T Get() const
{
return m_value;
}
void Set(bool value)
{
m_value = value;
if (!m_isResetScheduled)
{
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…er FrameCompletionScope acquisition.
…ompletionScope instead of deferred member release.
This reverts commit 0d315bb.
…cquired FrameCompletionScope in SubmitCommands.
…ck on FrameCompletionScope.
…enderScheduler fires one frame late when called outside RAF.
…te leaking into nanovg rendering.
… getFrameBufferData outside RAF.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What Changed
The old model used per-thread encoders (
GetEncoderForThread/EndEncoders)and UpdateToken (wrapping
SafeTimespanGuarantor::SafetyGuarantee) for encoderaccess. The new model replaces all of this with a single frame encoder owned
by
DeviceImpland FrameCompletionScope for synchronization.SafeTimespanGuarantor,UpdateToken, andUpdateclasses are fully removed.DeviceUpdateremains as a no-op shim inDevice.h— its removal is aseparate PR because it is an API breaking change.
Summary of Changes
Core/Graphics — Removed
SafeTimespanGuarantor.h/SafeTimespanGuarantor.cpp— deletedUpdateToken,Updateclasses — removed fromDeviceContext.hGetSafeTimespanGuarantor()— removed fromDeviceImplGetEncoderForThread(),EndEncoders()— removed fromDeviceImplm_threadIdToEncoder,m_updateSafeTimespans— removed fromDeviceImplDevice::GetUpdate()— replaced with inline no-op inDevice.hDeviceUpdateclass — replaced with no-op shim (Start/Finish/RequestFinish do nothing)bgfx::Encoder¶meter — removed fromAcquireNewViewId,FrameBuffermethodsCore/Graphics — Added
m_frameEncoder— singlebgfx::Encoder*inDeviceImpl, acquired inStartRenderingCurrentFrame, ended inFinishRenderingCurrentFrameFrameCompletionScope— RAII counter that preventsFinishRenderingCurrentFramefrom proceeding while held
m_frameBlocked+m_pendingFrameScopes+ mutex/CV — blocking gate mechanismm_frameStartDispatcher— ticked inStartRenderingCurrentFramefor RAF schedulingm_firstFrameStarted— flag to distinguish first-call no-op from lifecycle errorsin
FinishRenderingCurrentFrameSetActiveEncoder()/GetActiveEncoder()— on DeviceImpl, forwarded by DeviceContextFrameStartScheduler(),AcquireFrameCompletionScope()— on DeviceContextCore/Graphics — Modified
StartRenderingCurrentFrame(): acquires encoder, opens gate, ticks frame startdispatcher
FinishRenderingCurrentFrame(): waits for scopes==0, closes gate, ticks beforeRenderdispatcher, ends encoder, calls
Frame(). Returns early on first call before anyStarthas been called; throws on subsequent out-of-order calls.FrameBuffermethods: get encoder internally viaGetActiveEncoder()instead ofparameter
Plugins/NativeEngine
GetUpdateToken().GetEncoder()withGetEncoder()→GetActiveEncoder()+ assertBeginFrame()/EndFrame()— no-ops (encoder lifecycle is in DeviceImpl)SubmitCommands()always acquires a stack-scopedFrameCompletionScope. Whenwithin a RAF callback the frame is already open and acquisition returns immediately.
When outside (e.g., XHR callback), it blocks until
StartRenderingCurrentFrameprovides the encoder. The scope lives on the stack — no deferred release, no member
variable, no risk of mid-execution release via microtask draining.
ReadTexture(): acquires its ownFrameCompletionScopeto ensure the encoderis available. When a blit is needed, executes it inline using
GetEncoder().Both blit and
ReadTextureAsyncland in the same frame. The scope keeps theframe open for the entire operation.
GetEncoder()includesassert(encoder != nullptr)— all call-site asserts removedm_update,m_updateTokenLoadTextureFromImage/LoadCubeTextureFromImages: cachedm_numMipsbeforeloop to avoid reading from potentially freed image (from PR Fixed race condition when passing data to bgfx. Issue #1398 #1628)
Plugins/NativeXr
Update.Scheduler()→FrameStartScheduler()Update.GetUpdateToken()→AcquireFrameCompletionScope()GetUpdateToken().GetEncoder()→GetActiveEncoder()Updatemember fromSessionStatePolyfills/Canvas
m_update.GetUpdateToken().GetEncoder()→GetActiveEncoder()Flush(): acquiresFrameCompletionScopewhen encoder is null (outside frame),plus defensive null check after acquisition — returns early if encoder still null
(shutdown edge case). Discards encoder state before nanovg rendering to prevent
NativeEngine state from leaking into Canvas rendering (old model had separate
per-thread encoder with clean state).
m_updatememberApps
Start/Finishframe aroundstartup wait to keep encoder available while JS runs
while waiting for JS test completion. Keep frame open during shutdown so JS
thread can complete pending work.
bgfx Fix (applied to build deps)
m_encoderBeginLockinbgfx::frame()to prevent deadlock withbgfx::destroy()Architecture
Single Encoder Lifecycle
Encoder Access
Per-Frame Sequence Diagram
ReadTexture
ReadTextureacquires its ownFrameCompletionScope— it can be called duringRAF, from
getFrameBufferDatacallbacks, or during initialization:SubmitCommands Scope Acquisition
SubmitCommandsalways acquires a stack-scopedFrameCompletionScope:This eliminates:
drainMicrotasks)m_outsideFrameScopemember variable and deferred release patternCanvas Flush
Synchronization Rules
Single encoder, shared by all consumers — DeviceImpl acquires the encoder in
StartRenderingCurrentFrameand all consumers read it viaGetActiveEncoder().Encoder acquired before gate opens —
bgfx::begin(true)is called BEFOREm_frameBlockedis set tofalse. Mutex provides memory ordering guarantee.Encoder ended after gate closes —
bgfx::end()is called AFTER allFrameCompletionScopes are released andm_frameBlockedis set totrue.SubmitCommands always holds a scope — Prevents encoder from being ended
mid-command-processing, regardless of microtask draining or reentrancy.
Apps must keep frame open when waiting for JS — Any main thread wait for JS
work must be bracketed by
Start/Finish. OtherwiseSubmitCommandsblocksforever waiting for the gate to open.
ReadTexture holds its own scope — Acquires
FrameCompletionScopeto ensureencoder is available for blit and
ReadTextureAsync. Both land in the same frame.Works during RAF, from
getFrameBufferDatacallbacks, and during initialization.Files Changed (vs 413cb49)
Core/Graphics/Source/DeviceImpl.hCore/Graphics/Source/DeviceImpl.cppCore/Graphics/InternalInclude/.../DeviceContext.hCore/Graphics/Source/DeviceContext.cppCore/Graphics/Include/.../Device.hCore/Graphics/Source/Device.cppCore/Graphics/InternalInclude/.../FrameBuffer.hCore/Graphics/Source/FrameBuffer.cppCore/Graphics/InternalInclude/.../SafeTimespanGuarantor.hCore/Graphics/Source/SafeTimespanGuarantor.cppPlugins/NativeEngine/Source/NativeEngine.hPlugins/NativeEngine/Source/NativeEngine.cppPlugins/NativeXr/Source/NativeXrImpl.h/cppPolyfills/Canvas/Source/Context.h/cppApps/HeadlessScreenshotApp/Win32/App.cppApps/StyleTransferApp/Win32/App.cppApps/UnitTests/Source/Tests.JavaScript.cppbgfx/src/bgfx.cpp+bgfx_p.hWhat Remains for Separate PR
Remove DeviceUpdate —
DeviceUpdateis a no-op shim retained because itsremoval is an API breaking change (apps call
GetUpdate/Start/Finish).All other changes in this branch are non-breaking. Removing DeviceUpdate will
be a separate PR that updates all app call sites.