Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
20 changes: 19 additions & 1 deletion Apps/UnitTests/Source/Tests.JavaScript.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,24 @@ TEST(JavaScript, All)
device.StartRenderingCurrentFrame();
device.FinishRenderingCurrentFrame();

auto exitCode{exitCodePromise.get_future().get()};
// Pump frames while JS tests run — tests use RAF internally and
// SubmitCommands requires an active frame.
auto exitCodeFuture = exitCodePromise.get_future();
while (exitCodeFuture.wait_for(std::chrono::milliseconds(16)) != std::future_status::ready)
{
device.StartRenderingCurrentFrame();
device.FinishRenderingCurrentFrame();
}

// Keep the frame open during shutdown so any pending JS work
// (e.g., SubmitCommands acquiring a FrameCompletionScope) can complete.
device.StartRenderingCurrentFrame();

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
89 changes: 42 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,38 @@ namespace Babylon::Graphics
bgfx::TextureFormat::Enum Format{};
};

class UpdateToken final
// FrameCompletionScope is an RAII guard that keeps a frame "open" on the JS thread.
// While any scope is alive, FinishRenderingCurrentFrame() on the main thread will
// block — it cannot call bgfx::frame() until all scopes are destroyed.
//
// This prevents a race where the main thread submits a bgfx frame while the JS
// thread is still recording encoder commands (which would cause bgfx deadlocks
// or lost draw calls).
//
// Three usage patterns:
// 1. RAF scheduling: scope acquired on main thread during StartRenderingCurrentFrame,
// transferred to JS thread, released after RAF callbacks complete + one extra
// dispatch cycle (to cover GC-triggered resource destruction).
// 2. NativeEngine::GetEncoder(): scope acquired lazily when JS code uses the encoder
// outside RAF (e.g., async texture loads, LOD switches). Released on next dispatch.
// 3. Canvas::Flush(): stack-scoped for the duration of nanovg rendering.
//
// Construction blocks if m_frameBlocked is true (frame submission in progress).
// Destruction decrements counter and wakes main thread via condition variable.
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 +78,19 @@ 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.
// The scope must be held while JS frame callbacks are running.
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 SetRenderResetCallback(std::function<void()> callback);
Expand All @@ -113,7 +110,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();

// TODO: find a different way to get the texture info for frame capture
void AddTexture(bgfx::TextureHandle handle, uint16_t width, uint16_t height, bool hasMips, uint16_t numLayers, bgfx::TextureFormat::Enum format);
Expand All @@ -122,8 +119,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.

13 changes: 0 additions & 13 deletions Core/Graphics/Source/Device.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -66,19 +66,6 @@ namespace Babylon::Graphics
m_impl->DisableRendering();
}

DeviceUpdate Device::GetUpdate(const char* updateName)
{
auto& guarantor = m_impl->GetSafeTimespanGuarantor(updateName);
return {
[&guarantor] {
guarantor.Open();
},
[&guarantor](std::function<void()> callback) {
guarantor.CloseScheduler()(std::move(callback));
guarantor.RequestClose();
}};
}

void Device::StartRenderingCurrentFrame()
{
m_impl->StartRenderingCurrentFrame();
Expand Down
Loading
Loading