Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
ca85709
Reworked threading model.
bkaradzic Mar 25, 2026
9daf790
Added first frame started check.
bkaradzic Apr 6, 2026
9fa86ff
Fixed ordering.
bkaradzic Apr 8, 2026
55712c8
Capture just POD values.
bkaradzic Apr 8, 2026
b80caa4
Removed unused captures.
bkaradzic Apr 8, 2026
a2b1679
Fix null encoder crash in Canvas::Flush by adding defensive check aft…
bkaradzic Apr 8, 2026
05e682b
Fix LoadCubeTexture*.
bkaradzic Mar 12, 2026
212eb4a
Fix null encoder crash in SubmitCommands by using stack-scoped FrameC…
bkaradzic Apr 8, 2026
ed656eb
Always acquire FrameCompletionScope in SubmitCommands to prevent enco…
bkaradzic Apr 8, 2026
8cb30d5
Revert "Fix LoadCubeTexture*."
bkaradzic Apr 8, 2026
3d491d8
Pump frames in JavaScript unit test to prevent deadlock from always-a…
bkaradzic Apr 8, 2026
bf32f2d
Fix unit test shutdown race by using 16ms frame interval instead of s…
bkaradzic Apr 8, 2026
eb86445
Keep frame open during unit test shutdown to prevent JS thread deadlo…
bkaradzic Apr 9, 2026
d5560b6
Fix PrecompiledShaderTest deadlock by keeping frame open during start…
bkaradzic Apr 9, 2026
c73534c
Revert ReadTexture to inline blit with FrameCompletionScope — BeforeR…
bkaradzic Apr 9, 2026
935bf4a
Discard encoder state before Canvas Flush to prevent NativeEngine sta…
bkaradzic Apr 9, 2026
e97bfdc
Add FrameCompletionScope to ReadTexture — called during init and from…
bkaradzic Apr 9, 2026
282a9a6
Apps/UnitTests: hold the frame open across the JS test pump
bghgary Apr 29, 2026
ede3565
Merge origin/master into rework-thread-model-pump-fix
bghgary Apr 29, 2026
7f98c7c
NativeEngine: hold FrameCompletionScope across the JS-thread frame
bghgary May 4, 2026
bb16582
NativeEngine: fix Linux gcc build of SubmitCommands scope capture, re…
bghgary May 4, 2026
606369a
NativeEngine: dispatch the SubmitCommands frame scope at the top
bghgary May 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions Apps/HeadlessScreenshotApp/Win32/App.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -159,9 +159,17 @@ int main()
deviceUpdate.Finish();
device.FinishRenderingCurrentFrame();

// Reopen the gate so JS can continue running (startup may issue bgfx commands).
device.StartRenderingCurrentFrame();
deviceUpdate.Start();

// Wait for `startup` to finish.
startup.get_future().wait();

// Close the frame opened above.
deviceUpdate.Finish();
device.FinishRenderingCurrentFrame();

struct Asset
{
const char* Name;
Expand Down
8 changes: 8 additions & 0 deletions Apps/PrecompiledShaderTest/Source/App.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -166,9 +166,17 @@ int RunApp(
deviceUpdate.Finish();
device.FinishRenderingCurrentFrame();

// Reopen the gate so JS can continue running (startup may issue bgfx commands).
device.StartRenderingCurrentFrame();
deviceUpdate.Start();

// Wait for `startup` to finish.
startup.get_future().wait();

// Close the frame opened above.
deviceUpdate.Finish();
device.FinishRenderingCurrentFrame();

// Start a new frame for rendering the scene.
device.StartRenderingCurrentFrame();
deviceUpdate.Start();
Expand Down
8 changes: 8 additions & 0 deletions Apps/StyleTransferApp/Win32/App.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -362,9 +362,17 @@ int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
g_update->Finish();
g_device->FinishRenderingCurrentFrame();

// Reopen the gate so JS can continue running (startup may issue bgfx commands).
g_device->StartRenderingCurrentFrame();
g_update->Start();

// Wait for `startup` to finish.
startup.get_future().wait();

// Close the frame opened above.
g_update->Finish();
g_device->FinishRenderingCurrentFrame();

// --------------------------- Rendering loop -------------------------

HACCEL hAccelTable = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDC_PLAYGROUNDWIN32));
Expand Down
26 changes: 23 additions & 3 deletions Apps/UnitTests/Source/Tests.JavaScript.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,13 @@ TEST(JavaScript, All)

Babylon::Graphics::Device device{g_deviceConfig};

// Start rendering a frame to unblock the JavaScript from queuing graphics
// commands. The frame is held open through script load and the test pump
// (which only ticks bgfx via Finish; Start) so the JS thread can submit
// at any time without racing the gate. A final Finish closes it after
// runtime teardown.
device.StartRenderingCurrentFrame();

std::optional<Babylon::Polyfills::Canvas> nativeCanvas;

Babylon::AppRuntime::Options options{};
Expand Down Expand Up @@ -87,9 +94,22 @@ TEST(JavaScript, All)
loader.LoadScript("app:///Assets/babylonjs.materials.js");
loader.LoadScript("app:///Assets/tests.javaScript.all.js");

device.StartRenderingCurrentFrame();
device.FinishRenderingCurrentFrame();
// Pump frames while JS tests run — tests use RAF internally and
// SubmitCommands requires an active frame. The frame was opened
// immediately after device creation; the loop just ticks bgfx
// (Finish; Start) once per iteration so commands can advance.
auto exitCodeFuture = exitCodePromise.get_future();
while (exitCodeFuture.wait_for(std::chrono::milliseconds(16)) != std::future_status::ready)
{
device.FinishRenderingCurrentFrame();
device.StartRenderingCurrentFrame();
}

auto exitCode{exitCodePromise.get_future().get()};
auto exitCode = exitCodeFuture.get();
EXPECT_EQ(exitCode, 0);

// Runtime destructor joins the JS thread; must happen before Finish.
nativeCanvas.reset();

device.FinishRenderingCurrentFrame();
}
2 changes: 0 additions & 2 deletions Core/Graphics/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ set(SOURCES
"InternalInclude/Babylon/Graphics/continuation_scheduler.h"
"InternalInclude/Babylon/Graphics/FrameBuffer.h"
"InternalInclude/Babylon/Graphics/DeviceContext.h"
"InternalInclude/Babylon/Graphics/SafeTimespanGuarantor.h"
"InternalInclude/Babylon/Graphics/Texture.h"
"Source/BgfxCallback.cpp"
"Source/FrameBuffer.cpp"
Expand All @@ -16,7 +15,6 @@ set(SOURCES
"Source/DeviceImpl.h"
"Source/DeviceImpl_${BABYLON_NATIVE_PLATFORM}.${BABYLON_NATIVE_PLATFORM_IMPL_EXT}"
"Source/DeviceImpl_${GRAPHICS_API}.${BABYLON_NATIVE_PLATFORM_IMPL_EXT}"
"Source/SafeTimespanGuarantor.cpp"
"Source/Texture.cpp")

add_library(Graphics ${SOURCES})
Expand Down
38 changes: 8 additions & 30 deletions Core/Graphics/Include/Shared/Babylon/Graphics/Device.h
Original file line number Diff line number Diff line change
Expand Up @@ -58,45 +58,23 @@ namespace Babylon::Graphics
DepthStencilFormat BackBufferDepthStencilFormat{DepthStencilFormat::Depth24Stencil8};
};

class Device;
class DeviceImpl;

// Deprecated: DeviceUpdate is a no-op compatibility shim. Frame synchronization
// is now handled by FrameCompletionScope inside StartRenderingCurrentFrame/
// FinishRenderingCurrentFrame. This class will be removed in a future PR.
class DeviceUpdate
{
public:
void Start()
{
m_start();
}
void Start() {}
void Finish() {}

void RequestFinish(std::function<void()> onFinishCallback)
{
m_requestFinish(std::move(onFinishCallback));
}

void Finish()
{
std::promise<void> promise{};
auto future = promise.get_future();
RequestFinish([&promise] { promise.set_value(); });
future.wait();
}

private:
friend class Device;

template<typename StartCallableT, typename RequestEndCallableT>
DeviceUpdate(StartCallableT&& start, RequestEndCallableT&& requestEnd)
: m_start{std::forward<StartCallableT>(start)}
, m_requestFinish{std::forward<RequestEndCallableT>(requestEnd)}
{
onFinishCallback();
}

std::function<void()> m_start{};
std::function<void(std::function<void()>)> m_requestFinish{};
};

class DeviceImpl;

class Device
{
public:
Expand Down Expand Up @@ -128,7 +106,7 @@ namespace Babylon::Graphics
void EnableRendering();
void DisableRendering();

DeviceUpdate GetUpdate(const char* updateName);
DeviceUpdate GetUpdate(const char* /*updateName*/) { return {}; }

void StartRenderingCurrentFrame();
void FinishRenderingCurrentFrame();
Expand Down
96 changes: 49 additions & 47 deletions Core/Graphics/InternalInclude/Babylon/Graphics/DeviceContext.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
#include "BgfxCallback.h"
#include <bx/allocator.h>
#include "continuation_scheduler.h"
#include "SafeTimespanGuarantor.h"

#include <arcana/threading/task.h>

#include <napi/env.h>

Expand All @@ -15,7 +16,6 @@

namespace Babylon::Graphics
{
class Update;
class DeviceContext;
class DeviceImpl;

Expand All @@ -29,53 +29,41 @@ namespace Babylon::Graphics
bgfx::TextureFormat::Enum Format{};
};

class UpdateToken final
// FrameCompletionScope is an RAII guard that prevents the render thread from
// closing a bgfx frame while JS-thread work is still in flight. While any
// scope is alive, FinishRenderingCurrentFrame() blocks before bgfx::frame()
// — so all encoder commands recorded while a scope was held land in the
// same bgfx frame, never split across two.
//
// Acquisition blocks if the gate is closed (m_frameBlocked == true), so a
// JS thread that picks up work between frames waits for the next Start
// before proceeding.
//
// Two scoping patterns are used:
// 1. JS-frame scoped: capture the scope into an m_runtime.Dispatch lambda
// so it survives until the JS-thread queue services the next
// continuation. Use this when subsequent work in the same JS task may
// also touch bgfx (chained submitCommands, RAF callbacks, scene
// cleanup, etc.). NativeEngine::SubmitCommands and
// ScheduleRequestAnimationFrameCallbacks both do this.
// 2. Block scoped: hold the scope on the stack across a single self-
// contained bgfx phase. Used for one-shot operations like
// Canvas::Flush() called outside an active frame, and
// ReadTextureAsync.
class FrameCompletionScope final
{
public:
UpdateToken(const UpdateToken& other) = delete;
UpdateToken& operator=(const UpdateToken& other) = delete;

UpdateToken(UpdateToken&&) noexcept = default;

// The move assignment of `SafeTimespanGuarantor::SafetyGuarantee` is marked as delete.
// See https://github.com/Microsoft/GSL/issues/705.
//UpdateToken& operator=(UpdateToken&& other) = delete;
FrameCompletionScope(const FrameCompletionScope&) = delete;
FrameCompletionScope& operator=(const FrameCompletionScope&) = delete;
FrameCompletionScope& operator=(FrameCompletionScope&&) = delete;

bgfx::Encoder* GetEncoder();

private:
friend class Update;

UpdateToken(DeviceContext&, SafeTimespanGuarantor&);

DeviceContext& m_context;
SafeTimespanGuarantor::SafetyGuarantee m_guarantee;
};

class Update
{
public:
continuation_scheduler<>& Scheduler()
{
return m_safeTimespanGuarantor.OpenScheduler();
}

UpdateToken GetUpdateToken()
{
return {m_context, m_safeTimespanGuarantor};
}
FrameCompletionScope(FrameCompletionScope&&) noexcept;
~FrameCompletionScope();

private:
friend class DeviceContext;

Update(SafeTimespanGuarantor& safeTimespanGuarantor, DeviceContext& context)
: m_safeTimespanGuarantor{safeTimespanGuarantor}
, m_context{context}
{
}

SafeTimespanGuarantor& m_safeTimespanGuarantor;
DeviceContext& m_context;
FrameCompletionScope(DeviceImpl&);
DeviceImpl* m_impl;
};

class DeviceContext
Expand All @@ -93,7 +81,23 @@ namespace Babylon::Graphics
continuation_scheduler<>& BeforeRenderScheduler();
continuation_scheduler<>& AfterRenderScheduler();

Update GetUpdate(const char* updateName);
// Scheduler that fires when StartRenderingCurrentFrame ticks the frame start dispatcher.
// Use this to schedule work (e.g., requestAnimationFrame callbacks) that should run each frame.
continuation_scheduler<>& FrameStartScheduler();

// Acquire a scope that prevents FinishRenderingCurrentFrame from
// completing until the scope is destroyed. JS-thread callers that
// need coverage across the whole current JS task should capture the
// scope into an m_runtime.Dispatch lambda; callers needing only a
// single phase can hold it stack-scoped. See the class comment on
// FrameCompletionScope above for the two patterns.
FrameCompletionScope AcquireFrameCompletionScope();

// Active encoder for the current frame. Managed by DeviceImpl in
// StartRenderingCurrentFrame/FinishRenderingCurrentFrame.
// Used by NativeEngine, Canvas, and NativeXr.
void SetActiveEncoder(bgfx::Encoder* encoder);
bgfx::Encoder* GetActiveEncoder();

void RequestScreenShot(std::function<void(std::vector<uint8_t>)> callback);
void RequestCaptureNextFrame();
Expand All @@ -114,7 +118,7 @@ namespace Babylon::Graphics
using CaptureCallbackTicketT = arcana::ticketed_collection<std::function<void(const BgfxCallback::CaptureData&)>>::ticket;
CaptureCallbackTicketT AddCaptureCallback(std::function<void(const BgfxCallback::CaptureData&)> callback);

bgfx::ViewId AcquireNewViewId(bgfx::Encoder&);
bgfx::ViewId AcquireNewViewId();
bgfx::ViewId PeekNextViewId() const;

// TODO: find a different way to get the texture info for frame capture
Expand All @@ -124,8 +128,6 @@ namespace Babylon::Graphics
static bx::AllocatorI& GetDefaultAllocator() { return m_allocator; }

private:
friend UpdateToken;

DeviceImpl& m_graphicsImpl;

std::unordered_map<uint16_t, TextureInfo> m_textureHandleToInfo{};
Expand Down
10 changes: 5 additions & 5 deletions Core/Graphics/InternalInclude/Babylon/Graphics/FrameBuffer.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,12 @@ namespace Babylon::Graphics
uint16_t Height() const;
bool DefaultBackBuffer() const;

void Bind(bgfx::Encoder& encoder);
void Unbind(bgfx::Encoder& encoder);
void Bind();
void Unbind();

void Clear(bgfx::Encoder& encoder, uint16_t flags, uint32_t rgba, float depth, uint8_t stencil);
void SetViewPort(bgfx::Encoder& encoder, float x, float y, float width, float height);
void SetScissor(bgfx::Encoder& encoder, float x, float y, float width, float height);
void SetViewPort(float x, float y, float width, float height);
void SetScissor(float x, float y, float width, float height);
void Submit(bgfx::Encoder& encoder, bgfx::ProgramHandle programHandle, uint8_t flags);
void SetStencil(bgfx::Encoder& encoder, uint32_t stencilState);
void Blit(bgfx::Encoder& encoder, bgfx::TextureHandle dst, uint16_t dstX, uint16_t dstY, bgfx::TextureHandle src, uint16_t srcX = 0, uint16_t srcY = 0, uint16_t width = UINT16_MAX, uint16_t height = UINT16_MAX);
Expand All @@ -48,7 +48,7 @@ namespace Babylon::Graphics

private:
Rect GetBgfxScissor(float x, float y, float width, float height) const;
void SetBgfxViewPortAndScissor(bgfx::Encoder& encoder, const Rect& viewPort, const Rect& scissor);
void SetBgfxViewPortAndScissor(const Rect& viewPort, const Rect& scissor);

DeviceContext& m_deviceContext;
const uintptr_t m_deviceID{};
Expand Down

This file was deleted.

Loading