From 43dce920fa43f2d37c0da43300d78eb551989fd4 Mon Sep 17 00:00:00 2001 From: Gary Hsu Date: Wed, 28 Sep 2022 17:47:32 -0700 Subject: [PATCH 01/21] WIP: Fixes for shutting down during async operations --- Core/AppRuntime/Source/AppRuntime.cpp | 7 + Core/AppRuntime/Source/WorkQueue.cpp | 12 +- Core/AppRuntime/Source/WorkQueue.h | 6 + .../Babylon/Graphics/DeviceContext.h | 7 +- Core/Graphics/Source/DeviceContext.cpp | 4 +- Core/Graphics/Source/DeviceImpl.cpp | 11 +- Core/Graphics/Source/DeviceImpl.h | 2 +- Core/JsRuntime/Include/Babylon/JsRuntime.h | 25 +- .../Include/Babylon/JsRuntimeScheduler.h | 122 ++++++++- Core/JsRuntime/Source/JsRuntime.cpp | 14 + Plugins/NativeCamera/Source/NativeVideo.cpp | 5 +- Plugins/NativeCamera/Source/NativeVideo.h | 5 +- Plugins/NativeEngine/Source/NativeEngine.cpp | 246 +++++++++--------- Plugins/NativeEngine/Source/NativeEngine.h | 18 +- .../NativeInput/Source/Shared/NativeInput.cpp | 25 +- .../NativeInput/Source/Shared/NativeInput.h | 2 +- Plugins/NativeXr/Source/NativeXr.cpp | 24 +- Polyfills/Canvas/Source/Canvas.cpp | 19 +- Polyfills/Canvas/Source/Context.cpp | 21 +- Polyfills/Canvas/Source/Context.h | 4 +- Polyfills/Canvas/Source/Image.cpp | 16 +- Polyfills/Canvas/Source/Image.h | 7 +- Polyfills/Window/Source/Window.cpp | 19 +- Polyfills/Window/Source/Window.h | 13 +- .../XMLHttpRequest/Source/XMLHttpRequest.cpp | 7 +- 25 files changed, 414 insertions(+), 227 deletions(-) diff --git a/Core/AppRuntime/Source/AppRuntime.cpp b/Core/AppRuntime/Source/AppRuntime.cpp index 3a099ad89..c49ed905e 100644 --- a/Core/AppRuntime/Source/AppRuntime.cpp +++ b/Core/AppRuntime/Source/AppRuntime.cpp @@ -19,6 +19,13 @@ namespace Babylon AppRuntime::~AppRuntime() { + // Notify the JsRuntime on the JavaScript thread that the JavaScript + // runtime shutdown sequence has begun. The JsRuntimeScheduler will + // use this signal to gracefully cancel asynchronous operations. + Dispatch([](Napi::Env env) + { + JsRuntime::NotifyDisposing(JsRuntime::GetFromJavaScript(env)); + }); } void AppRuntime::Run(Napi::Env env) diff --git a/Core/AppRuntime/Source/WorkQueue.cpp b/Core/AppRuntime/Source/WorkQueue.cpp index 4c1c5f21a..a20cb8665 100644 --- a/Core/AppRuntime/Source/WorkQueue.cpp +++ b/Core/AppRuntime/Source/WorkQueue.cpp @@ -14,8 +14,12 @@ namespace Babylon Resume(); } - m_cancelSource.cancel(); - m_dispatcher.cancelled(); + // Dispatch a cancel to signal the Run function to gracefully end. + // It must be dispatched and not canceled directly to ensure that + // existing work is executed and executed in the correct order. + m_dispatcher([this]() { + m_cancelSource.cancel(); + }); m_thread.join(); } @@ -44,6 +48,8 @@ namespace Babylon m_dispatcher.blocking_tick(m_cancelSource); } - m_dispatcher.clear(); + // There should not be any outstanding work during the shutdown sequence + // which should be the only way exit the while loop above. + assert(m_dispatcher.empty()); } } diff --git a/Core/AppRuntime/Source/WorkQueue.h b/Core/AppRuntime/Source/WorkQueue.h index 2c2a7c5de..dd9ba0fbe 100644 --- a/Core/AppRuntime/Source/WorkQueue.h +++ b/Core/AppRuntime/Source/WorkQueue.h @@ -19,6 +19,12 @@ namespace Babylon template void Append(CallableT callable) { + if (m_cancelSource.cancelled()) + { + // There is likely a coding error if this exception is thrown. + throw std::runtime_error{"Cannot append to the work queue after it is canceled"}; + } + // Manual dispatcher queueing requires a copyable CallableT, we use a shared pointer trick to make a // copyable callable if necessary. if constexpr (std::is_copy_constructible::value) diff --git a/Core/Graphics/InternalInclude/Babylon/Graphics/DeviceContext.h b/Core/Graphics/InternalInclude/Babylon/Graphics/DeviceContext.h index 4ec40572c..3d3ee855a 100644 --- a/Core/Graphics/InternalInclude/Babylon/Graphics/DeviceContext.h +++ b/Core/Graphics/InternalInclude/Babylon/Graphics/DeviceContext.h @@ -6,6 +6,7 @@ #include +#include #include #include @@ -94,7 +95,7 @@ namespace Babylon::Graphics Update GetUpdate(const char* updateName); - void RequestScreenShot(std::function)> callback); + arcana::task, std::exception_ptr> RequestScreenShotAsync(); arcana::task ReadTextureAsync(bgfx::TextureHandle handle, gsl::span data, uint8_t mipLevel = 0); @@ -114,6 +115,8 @@ namespace Babylon::Graphics void RemoveTexture(bgfx::TextureHandle handle); TextureInfo GetTextureInfo(bgfx::TextureHandle handle); + bx::AllocatorI& Allocator() { return m_allocator; } + private: friend UpdateToken; @@ -121,5 +124,7 @@ namespace Babylon::Graphics std::unordered_map m_textureHandleToInfo{}; std::mutex m_textureHandleToInfoMutex{}; + + bx::DefaultAllocator m_allocator{}; }; } diff --git a/Core/Graphics/Source/DeviceContext.cpp b/Core/Graphics/Source/DeviceContext.cpp index aa981bc33..36d9b9d5f 100644 --- a/Core/Graphics/Source/DeviceContext.cpp +++ b/Core/Graphics/Source/DeviceContext.cpp @@ -56,9 +56,9 @@ namespace Babylon::Graphics return {m_graphicsImpl.GetSafeTimespanGuarantor(updateName), *this}; } - void DeviceContext::RequestScreenShot(std::function)> callback) + arcana::task, std::exception_ptr> DeviceContext::RequestScreenShotAsync() { - return m_graphicsImpl.RequestScreenShot(std::move(callback)); + return m_graphicsImpl.RequestScreenShotAsync(); } arcana::task DeviceContext::ReadTextureAsync(bgfx::TextureHandle handle, gsl::span data, uint8_t mipLevel) diff --git a/Core/Graphics/Source/DeviceImpl.cpp b/Core/Graphics/Source/DeviceImpl.cpp index d9b54d607..ccf88247e 100644 --- a/Core/Graphics/Source/DeviceImpl.cpp +++ b/Core/Graphics/Source/DeviceImpl.cpp @@ -271,9 +271,16 @@ namespace Babylon::Graphics m_bgfxCallback.SetDiagnosticOutput(std::move(diagnosticOutput)); } - void DeviceImpl::RequestScreenShot(std::function)> callback) + arcana::task, std::exception_ptr> DeviceImpl::RequestScreenShotAsync() { - m_screenShotCallbacks.push(std::move(callback)); + arcana::task_completion_source, std::exception_ptr> taskCompletionSource{}; + + m_screenShotCallbacks.push([taskCompletionSource](std::vector bytes) mutable + { + taskCompletionSource.complete(std::move(bytes)); + }); + + return taskCompletionSource.as_task(); } arcana::task DeviceImpl::ReadTextureAsync(bgfx::TextureHandle handle, gsl::span data, uint8_t mipLevel) diff --git a/Core/Graphics/Source/DeviceImpl.h b/Core/Graphics/Source/DeviceImpl.h index 60c826dbd..5decfeae3 100644 --- a/Core/Graphics/Source/DeviceImpl.h +++ b/Core/Graphics/Source/DeviceImpl.h @@ -66,7 +66,7 @@ namespace Babylon::Graphics Update GetUpdate(const char* updateName); - void RequestScreenShot(std::function)> callback); + arcana::task, std::exception_ptr> RequestScreenShotAsync(); arcana::task ReadTextureAsync(bgfx::TextureHandle handle, gsl::span data, uint8_t mipLevel); diff --git a/Core/JsRuntime/Include/Babylon/JsRuntime.h b/Core/JsRuntime/Include/Babylon/JsRuntime.h index 31300b6bb..9c9460490 100644 --- a/Core/JsRuntime/Include/Babylon/JsRuntime.h +++ b/Core/JsRuntime/Include/Babylon/JsRuntime.h @@ -4,6 +4,7 @@ #include #include +#include namespace Babylon { @@ -22,31 +23,41 @@ namespace Babylon } }; - struct InternalState; - friend struct InternalState; + JsRuntime(const JsRuntime&) = delete; + JsRuntime& operator=(const JsRuntime&) = delete; // Any JavaScript errors that occur will bubble up as a Napi::Error C++ exception. // JsRuntime expects the provided dispatch function to handle this exception, // such as with a try/catch and logging the exception message. using DispatchFunctionT = std::function)>; + // Creates the JsRuntime object owned by the JavaScript environment. // Note: It is the contract of JsRuntime that its dispatch function must be usable // at the moment of construction. JsRuntime cannot be built with dispatch function // that captures a reference to a not-yet-completed object that will be completed // later -- an instance of an inheriting type, for example. The dispatch function // must be safely callable as soon as it is passed to the JsRuntime constructor. static JsRuntime& CreateForJavaScript(Napi::Env, DispatchFunctionT); + + // Gets the JsRuntime from the given N-API environment. static JsRuntime& GetFromJavaScript(Napi::Env); - void Dispatch(std::function); - protected: - JsRuntime(const JsRuntime&) = delete; - JsRuntime& operator=(const JsRuntime&) = delete; + // Notifies the JsRuntime that the JavaScript environment will begin shutting down. + // Calling this function will signal callbacks registered with RegisterDisposing. + static void NotifyDisposing(JsRuntime&); + + // Registers a callback for when the JavaScript environment will begin shutting down. + static void RegisterDisposing(JsRuntime&, std::function); + + // Dispatches work onto the JavaScript thread and provides access to the N-API + // environment. + void Dispatch(std::function); private: JsRuntime(Napi::Env, DispatchFunctionT); - DispatchFunctionT m_dispatchFunction{}; std::mutex m_mutex{}; + DispatchFunctionT m_dispatchFunction{}; + std::vector> m_disposingCallbacks{}; }; } diff --git a/Core/JsRuntime/Include/Babylon/JsRuntimeScheduler.h b/Core/JsRuntime/Include/Babylon/JsRuntimeScheduler.h index 4ae848e1f..a3ad476ec 100644 --- a/Core/JsRuntime/Include/Babylon/JsRuntimeScheduler.h +++ b/Core/JsRuntime/Include/Babylon/JsRuntimeScheduler.h @@ -1,30 +1,130 @@ #pragma once #include "JsRuntime.h" +#include +#include +#include namespace Babylon { - /** - * Scheduler that invokes continuations via JsRuntime::Dispatch. - * Intended to be consumed by arcana.cpp tasks. - */ + // This class encapsulates a coding pattern for invoking continuations on the JavaScript thread while properly + // handling garbage collection and shutdown scenarios. This class provides and manages the schedulers intended + // for a N-API object to use with arcana tasks. It is different than the typical scheduler as this class itself + // is not a scheduler directly, but instead hands out scheduler via its `Get()` function. It provides special + // handling for when the JsRuntime begins shutting down, i.e., when JsRuntime::NotifyDisposing is called: + // 1. The destructor blocks if there are outstanding schedulers not yet invoked on the JavaScript thread. + // 2. Once the JsRuntime begins shutting down, all schedulers will reroute its dispatch calls from the + // JsRuntime to a separate dispatcher owned by the JsRuntimeScheduler itself. This class will then be able + // to pump this dispatcher in its destructor to prevent deadlocks. + // + // The typical pattern for an arcana task will look something like this: + // + // class MyClass + // { + // public: + // void MyFunction(const Napi::CallbackInfo& info); + // + // private: + // arcana::cancellation_source m_cancellationSource; + // + // // Put this last so that it gets destructed first. + // JsRuntimeScheduler m_runtimeScheduler; + // }; + // + // void MyClass::MyFunction(const Napi::CallbackInfo& info) + // { + // const auto callback{info[0].As()}; + // + // arcana::make_task(arcana::threadpool_scheduler, m_cancellationSource, []() + // { + // // do some asynchronous work + // }).then(m_runtimeScheduler.Get(), m_cancelSource, [thisRef = Napi::Persistent(info.This()), callback = Napi::Persistent(callback)]() { + // { + // callback.Call({}); + // }); + // } + // + // **IMPORTANT**: + // 1. To prevent continuations from accessing destructed objects, declare the JsRuntimeScheduler at the end of + // the N-API class. The destructor of the JsRuntimeScheduler will call `Rundown()` which will block until + // all of its schedulers are invoked. If this is not possible, call `Rundown()` manually in the destructor + // of the N-API class. + // 2. The last continuation that accesses members of the N-API object, including the cancellation associated with + // the continuation must capture a persistent reference to the N-API object itself to prevent GC from collecting + // the N-API object during the asynchronous operation. class JsRuntimeScheduler { public: explicit JsRuntimeScheduler(JsRuntime& runtime) - : m_runtime{runtime} + : m_runtime{&runtime} + , m_scheduler{*this} { + JsRuntime::RegisterDisposing(*m_runtime, [this]() + { + m_runtime = nullptr; + }); } - template - void operator()(CallableT&& callable) const + ~JsRuntimeScheduler() { - m_runtime.Dispatch([callable{std::forward(callable)}](Napi::Env){ - callable(); - }); + Rundown(); + } + + // Wait until all of the schedulers are invoked. + void Rundown() + { + while (m_count > 0) + { + m_disposingDispatcher.blocking_tick(arcana::cancellation::none()); + } + } + + // Get a scheduler to invoke continuations on the JavaScript thread. + const auto& Get() + { + ++m_count; + return m_scheduler; } private: - JsRuntime& m_runtime; + class SchedulerImpl + { + public: + explicit SchedulerImpl(JsRuntimeScheduler& parent) : m_parent{parent} + { + } + + template + void operator()(CallableT&& callable) const + { + m_parent.Dispatch(callable); + } + + private: + JsRuntimeScheduler& m_parent; + }; + + template + void Dispatch(CallableT&& callable) + { + if (m_runtime != nullptr) + { + m_runtime->Dispatch([callable{std::forward(callable)}](Napi::Env) + { + callable(); + }); + } + else + { + m_disposingDispatcher(callable); + } + + --m_count; + } + + JsRuntime* m_runtime; + SchedulerImpl m_scheduler; + std::atomic m_count{0}; + arcana::manual_dispatcher<128> m_disposingDispatcher{}; }; } diff --git a/Core/JsRuntime/Source/JsRuntime.cpp b/Core/JsRuntime/Source/JsRuntime.cpp index 0b12d2e73..f9dedf94b 100644 --- a/Core/JsRuntime/Source/JsRuntime.cpp +++ b/Core/JsRuntime/Source/JsRuntime.cpp @@ -40,6 +40,20 @@ namespace Babylon .Data(); } + void JsRuntime::NotifyDisposing(JsRuntime& runtime) + { + auto callbacks = std::move(runtime.m_disposingCallbacks); + for (const auto& callback : callbacks) + { + callback(); + } + } + + void JsRuntime::RegisterDisposing(JsRuntime& runtime, std::function callback) + { + runtime.m_disposingCallbacks.push_back(std::move(callback)); + } + void JsRuntime::Dispatch(std::function function) { std::scoped_lock lock{m_mutex}; diff --git a/Plugins/NativeCamera/Source/NativeVideo.cpp b/Plugins/NativeCamera/Source/NativeVideo.cpp index 108fc87be..634ac257a 100644 --- a/Plugins/NativeCamera/Source/NativeVideo.cpp +++ b/Plugins/NativeCamera/Source/NativeVideo.cpp @@ -139,7 +139,10 @@ namespace Babylon::Plugins if (!m_IsPlaying) { m_IsPlaying = true; - NativeCameraImpl->Open(m_maxWidth, m_maxHeight, m_frontCamera).then(m_runtimeScheduler, arcana::cancellation::none(), [this, env, deferred](const arcana::expected& result) { + + NativeCameraImpl->Open(m_maxWidth, m_maxHeight, m_frontCamera) + .then(m_runtimeScheduler.Get(), arcana::cancellation::none(), [this, env, deferred](const arcana::expected& result) + { if (result.has_error()) { deferred.Reject(Napi::Error::New(env, result.error()).Value()); diff --git a/Plugins/NativeCamera/Source/NativeVideo.h b/Plugins/NativeCamera/Source/NativeVideo.h index 193c0a07b..9e14c4f61 100644 --- a/Plugins/NativeCamera/Source/NativeVideo.h +++ b/Plugins/NativeCamera/Source/NativeVideo.h @@ -35,8 +35,6 @@ namespace Babylon::Plugins Napi::Value GetReadyState(const Napi::CallbackInfo& info); Napi::Value GetHaveCurrentData(const Napi::CallbackInfo& info); - JsRuntimeScheduler m_runtimeScheduler; - std::unordered_map> m_eventHandlerRefs{}; uint32_t m_maxWidth{}; uint32_t m_maxHeight{}; @@ -48,6 +46,9 @@ namespace Babylon::Plugins bool m_IsPlaying{}; + // Put this last so that it gets destructed first. + JsRuntimeScheduler m_runtimeScheduler; + static inline std::shared_ptr NativeCameraImpl{}; }; } diff --git a/Plugins/NativeEngine/Source/NativeEngine.cpp b/Plugins/NativeEngine/Source/NativeEngine.cpp index 48d863cc3..da6ef2e2c 100644 --- a/Plugins/NativeEngine/Source/NativeEngine.cpp +++ b/Plugins/NativeEngine/Source/NativeEngine.cpp @@ -501,14 +501,12 @@ namespace Babylon NativeEngine::NativeEngine(const Napi::CallbackInfo& info, JsRuntime& runtime) : Napi::ObjectWrap{info} - , m_cancellationSource{std::make_shared()} - , m_runtime{runtime} - , m_graphicsContext{Graphics::DeviceContext::GetFromJavaScript(info.Env())} - , m_update{m_graphicsContext.GetUpdate("update")} , m_runtimeScheduler{runtime} - , m_defaultFrameBuffer{m_graphicsContext, BGFX_INVALID_HANDLE, 0, 0, true, true, true} + , m_deviceContext{Graphics::DeviceContext::GetFromJavaScript(info.Env())} + , m_update{m_deviceContext.GetUpdate("update")} + , m_defaultFrameBuffer{m_deviceContext, BGFX_INVALID_HANDLE, 0, 0, true, true, true} , m_boundFrameBuffer{&m_defaultFrameBuffer} - , m_boundFrameBufferNeedsRebinding{m_graphicsContext, *m_cancellationSource, true} + , m_boundFrameBufferNeedsRebinding{m_deviceContext, m_cancellationSource, true} { } @@ -519,10 +517,10 @@ namespace Babylon void NativeEngine::Dispose() { - m_cancellationSource->cancel(); + m_cancellationSource.cancel(); } - void NativeEngine::Dispose(const Napi::CallbackInfo& /*info*/) + void NativeEngine::Dispose(const Napi::CallbackInfo&) { Dispose(); } @@ -1046,42 +1044,46 @@ namespace Babylon const auto dataSpan = gsl::make_span(static_cast(data.ArrayBuffer().Data()) + data.ByteOffset(), data.ByteLength()); - arcana::make_task(arcana::threadpool_scheduler, *m_cancellationSource, - [this, dataSpan, generateMips, invertY, srgb, texture, cancellationSource{m_cancellationSource}]() { - bimg::ImageContainer* image{ParseImage(m_allocator, dataSpan)}; - image = PrepareImage(m_allocator, image, invertY, srgb, generateMips); - LoadTextureFromImage(texture, image, srgb); - }) - .then(m_runtimeScheduler, *m_cancellationSource, [dataRef{Napi::Persistent(data)}, onSuccessRef{Napi::Persistent(onSuccess)}, onErrorRef{Napi::Persistent(onError)}, cancellationSource{m_cancellationSource}](arcana::expected result) { - if (result.has_error()) - { - onErrorRef.Call({}); - } - else - { - onSuccessRef.Call({}); - } - }); + arcana::make_task(arcana::threadpool_scheduler, m_cancellationSource, + [this, dataSpan, generateMips, invertY, srgb, texture]() + { + bimg::ImageContainer* image{ParseImage(m_deviceContext.Allocator(), dataSpan)}; + image = PrepareImage(m_deviceContext.Allocator(), image, invertY, srgb, generateMips); + LoadTextureFromImage(texture, image, srgb); + }).then(m_runtimeScheduler.Get(), m_cancellationSource, + [thisRef = Napi::Persistent(info.This()), dataRef = Napi::Persistent(data), onSuccessRef = Napi::Persistent(onSuccess), onErrorRef = Napi::Persistent(onError)](arcana::expected result) + { + if (result.has_error()) + { + onErrorRef.Call({}); + } + else + { + onSuccessRef.Call({}); + } + }); } - void NativeEngine::CopyTexture(const Napi::CallbackInfo& info) + void NativeEngine::CopyTexture(const Napi::CallbackInfo& /*info*/) { - const auto textureDestination = info[0].As>().Get(); - const auto textureSource = info[1].As>().Get(); + //const auto textureDestination = info[0].As>().Get(); + //const auto textureSource = info[1].As>().Get(); - arcana::make_task(m_update.Scheduler(), *m_cancellationSource, [this, textureDestination, textureSource, cancellationSource = m_cancellationSource]() - { - return arcana::make_task(m_runtimeScheduler, *m_cancellationSource, [this, textureDestination, textureSource, updateToken = m_update.GetUpdateToken(), cancellationSource = m_cancellationSource]() - { - bgfx::Encoder* encoder = m_update.GetUpdateToken().GetEncoder(); - GetBoundFrameBuffer(*encoder).Blit(*encoder, textureDestination->Handle(), 0, 0, textureSource->Handle()); - }).then(arcana::inline_scheduler, *m_cancellationSource, [this, cancellationSource{ m_cancellationSource }](const arcana::expected& result) { - if (!cancellationSource->cancelled() && result.has_error()) - { - Napi::Error::New(Env(), result.error()).ThrowAsJavaScriptException(); - } - }); - }); + // TODO: this code makes no sense + //arcana::make_task(m_graphicsUpdate.Scheduler(), m_cancellationSource, [this, textureDestination, textureSource]() + //{ + // return arcana::make_task(m_runtimeScheduler.Get(), m_cancellationSource, [this, textureDestination, textureSource, updateToken = m_graphicsUpdate.GetUpdateToken()]() mutable + // { + // bgfx::Encoder* encoder = updateToken.GetEncoder(); + // GetBoundFrameBuffer(*encoder).Blit(*encoder, textureDestination->Handle(), 0, 0, textureSource->Handle()); + // }).then(arcana::inline_scheduler, m_cancellationSource, [this, thisRef = Napi::Persistent(info.Env())](const arcana::expected& result) + // { + // if (result.has_error()) + // { + // Napi::Error::New(Env(), result.error()).ThrowAsJavaScriptException(); + // } + // }); + //}); } void NativeEngine::LoadRawTexture(const Napi::CallbackInfo& info) @@ -1100,8 +1102,8 @@ namespace Babylon throw Napi::Error::New(Env(), "The data size does not match width, height, and format"); } - bimg::ImageContainer* image{bimg::imageAlloc(&m_allocator, format, width, height, 1, 1, false, false, bytes)}; - image = PrepareImage(m_allocator, image, invertY, false, generateMips); + bimg::ImageContainer* image{bimg::imageAlloc(&m_deviceContext.Allocator(), format, width, height, 1, 1, false, false, bytes)}; + image = PrepareImage(m_deviceContext.Allocator(), image, invertY, false, generateMips); LoadTextureFromImage(texture, image, false); } @@ -1167,18 +1169,18 @@ namespace Babylon const auto typedArray{data[face].As()}; const auto dataSpan{gsl::make_span(static_cast(typedArray.ArrayBuffer().Data()) + typedArray.ByteOffset(), typedArray.ByteLength())}; dataRefs[face] = Napi::Persistent(typedArray); - tasks[face] = arcana::make_task(arcana::threadpool_scheduler, *m_cancellationSource, [this, dataSpan, invertY, generateMips, srgb]() { - bimg::ImageContainer* image{ParseImage(m_allocator, dataSpan)}; - image = PrepareImage(m_allocator, image, invertY, srgb, generateMips); + tasks[face] = arcana::make_task(arcana::threadpool_scheduler, m_cancellationSource, [this, dataSpan, invertY, generateMips, srgb]() { + bimg::ImageContainer* image{ParseImage(m_deviceContext.Allocator(), dataSpan)}; + image = PrepareImage(m_deviceContext.Allocator(), image, invertY, srgb, generateMips); return image; }); } arcana::when_all(gsl::make_span(tasks)) - .then(arcana::inline_scheduler, *m_cancellationSource, [texture, srgb, cancellationSource{m_cancellationSource}](std::vector images) { + .then(arcana::inline_scheduler, m_cancellationSource, [texture, srgb](std::vector images) { LoadCubeTextureFromImages(texture, images, srgb); }) - .then(m_runtimeScheduler, *m_cancellationSource, [dataRefs{std::move(dataRefs)}, onSuccessRef{Napi::Persistent(onSuccess)}, onErrorRef{Napi::Persistent(onError)}, cancellationSource{m_cancellationSource}](arcana::expected result) { + .then(m_runtimeScheduler.Get(), m_cancellationSource, [thisRef = Napi::Persistent(info.This()), dataRefs = std::move(dataRefs), onSuccessRef = Napi::Persistent(onSuccess), onErrorRef = Napi::Persistent(onError)](arcana::expected result) { if (result.has_error()) { onErrorRef.Call({}); @@ -1210,19 +1212,19 @@ namespace Babylon const auto typedArray = faceData[face].As(); const auto dataSpan = gsl::make_span(static_cast(typedArray.ArrayBuffer().Data()) + typedArray.ByteOffset(), typedArray.ByteLength()); dataRefs[(face * numMips) + mip] = Napi::Persistent(typedArray); - tasks[(face * numMips) + mip] = arcana::make_task(arcana::threadpool_scheduler, *m_cancellationSource, [this, dataSpan, invertY, srgb]() { - bimg::ImageContainer* image{ParseImage(m_allocator, dataSpan)}; - image = PrepareImage(m_allocator, image, invertY, srgb, false); + tasks[(face * numMips) + mip] = arcana::make_task(arcana::threadpool_scheduler, m_cancellationSource, [this, dataSpan, invertY, srgb]() { + bimg::ImageContainer* image{ParseImage(m_deviceContext.Allocator(), dataSpan)}; + image = PrepareImage(m_deviceContext.Allocator(), image, invertY, srgb, false); return image; }); } } arcana::when_all(gsl::make_span(tasks)) - .then(arcana::inline_scheduler, *m_cancellationSource, [texture, srgb, cancellationSource{m_cancellationSource}](std::vector images) { + .then(arcana::inline_scheduler, m_cancellationSource, [texture, srgb](std::vector images) { LoadCubeTextureFromImages(texture, images, srgb); }) - .then(m_runtimeScheduler, *m_cancellationSource, [dataRefs{std::move(dataRefs)}, onSuccessRef{Napi::Persistent(onSuccess)}, onErrorRef{Napi::Persistent(onError)}, cancellationSource{m_cancellationSource}](arcana::expected result) { + .then(m_runtimeScheduler.Get(), m_cancellationSource, [thisRef = Napi::Persistent(info.This()), dataRefs = std::move(dataRefs), onSuccessRef = Napi::Persistent(onSuccess), onErrorRef = Napi::Persistent(onError)](arcana::expected result) { if (result.has_error()) { onErrorRef.Call({}); @@ -1314,7 +1316,7 @@ namespace Babylon void NativeEngine::DeleteTexture(const Napi::CallbackInfo& info) { Graphics::Texture* texture = info[0].As>().Get(); - m_graphicsContext.RemoveTexture(texture->Handle()); + m_deviceContext.RemoveTexture(texture->Handle()); texture->Dispose(); } @@ -1373,7 +1375,7 @@ namespace Babylon if (x != 0 || y != 0 || width != (texture->Width() >> mipLevel) || height != (texture->Height() >> mipLevel) || (texture->Flags() & BGFX_TEXTURE_READ_BACK) == 0) { const bgfx::TextureHandle blitTextureHandle{bgfx::createTexture2D(width, height, /*hasMips*/ false, /*numLayers*/ 1, sourceTextureFormat, BGFX_TEXTURE_BLIT_DST | BGFX_TEXTURE_READ_BACK)}; - bgfx::Encoder* encoder{GetUpdateToken().GetEncoder()}; + bgfx::Encoder* encoder = GetUpdateToken().GetEncoder(); encoder->blit(static_cast(bgfx::getCaps()->limits.maxViews - 1), blitTextureHandle, /*dstMip*/ 0, /*dstX*/ 0, /*dstY*/ 0, /*dstZ*/ 0, sourceTextureHandle, mipLevel, x, y, /*srcZ*/ 0, width, height, /*depth*/ 0); sourceTextureHandle = blitTextureHandle; @@ -1387,12 +1389,12 @@ namespace Babylon std::vector textureBuffer(sourceTextureInfo.storageSize); // Read the source texture. - m_graphicsContext.ReadTextureAsync(sourceTextureHandle, textureBuffer, mipLevel).then(arcana::inline_scheduler, *m_cancellationSource, [this, textureBuffer{std::move(textureBuffer)}, sourceTextureInfo, targetTextureInfo]() mutable { + m_deviceContext.ReadTextureAsync(sourceTextureHandle, textureBuffer, mipLevel).then(arcana::inline_scheduler, m_cancellationSource, [this, textureBuffer{std::move(textureBuffer)}, sourceTextureInfo, targetTextureInfo]() mutable { // If the source texture format does not match the target texture format, convert it. if (targetTextureInfo.format != sourceTextureInfo.format) { std::vector convertedTextureBuffer(targetTextureInfo.storageSize); - if (!bimg::imageConvert(&m_allocator, convertedTextureBuffer.data(), bimg::TextureFormat::Enum(targetTextureInfo.format), textureBuffer.data(), bimg::TextureFormat::Enum(sourceTextureInfo.format), sourceTextureInfo.width, sourceTextureInfo.height, /*depth*/ 1)) + if (!bimg::imageConvert(&m_deviceContext.Allocator(), convertedTextureBuffer.data(), bimg::TextureFormat::Enum(targetTextureInfo.format), textureBuffer.data(), bimg::TextureFormat::Enum(sourceTextureInfo.format), sourceTextureInfo.width, sourceTextureInfo.height, /*depth*/ 1)) { throw std::runtime_error{"Texture conversion to RBGA8 failed."}; } @@ -1409,7 +1411,7 @@ namespace Babylon } return textureBuffer; - }).then(m_runtimeScheduler, *m_cancellationSource, [this, bufferRef{Napi::Persistent(buffer)}, bufferOffset, deferred, tempTexture, sourceTextureHandle](std::vector textureBuffer) mutable { + }).then(m_runtimeScheduler.Get(), m_cancellationSource, [this, bufferRef{Napi::Persistent(buffer)}, bufferOffset, deferred, tempTexture, sourceTextureHandle](std::vector textureBuffer) mutable { // Double check the destination buffer length. This is redundant with prior checks, but we'll be extra sure before the memcpy. assert(bufferRef.Value().ByteLength() - bufferOffset >= textureBuffer.size()); @@ -1419,17 +1421,17 @@ namespace Babylon // Dispose of the texture handle before resolving the promise. // TODO: Handle properly handle stale handles after BGFX shutdown - if (tempTexture && !m_cancellationSource->cancelled()) + if (tempTexture && !m_cancellationSource.cancelled()) { bgfx::destroy(sourceTextureHandle); tempTexture = false; } deferred.Resolve(bufferRef.Value()); - }).then(m_runtimeScheduler, arcana::cancellation::none(), [this, env, deferred, tempTexture, sourceTextureHandle](const arcana::expected& result) { + }).then(m_runtimeScheduler.Get(), arcana::cancellation::none(), [this, env, deferred, tempTexture, sourceTextureHandle](const arcana::expected& result) { // Dispose of the texture handle if not yet disposed. // TODO: Handle properly handle stale handles after BGFX shutdown - if (tempTexture && !m_cancellationSource->cancelled()) + if (tempTexture && !m_cancellationSource.cancelled()) { bgfx::destroy(sourceTextureHandle); } @@ -1486,9 +1488,9 @@ namespace Babylon bgfx::FrameBufferHandle frameBufferHandle = bgfx::createFrameBuffer(numAttachments, attachments.data(), true); assert(bgfx::isValid(frameBufferHandle)); - m_graphicsContext.AddTexture(texture->Handle(), width, height, generateMips, 1, format); + m_deviceContext.AddTexture(texture->Handle(), width, height, generateMips, 1, format); - Graphics::FrameBuffer* frameBuffer = new Graphics::FrameBuffer(m_graphicsContext, frameBufferHandle, width, height, false, generateDepth, generateStencilBuffer); + Graphics::FrameBuffer* frameBuffer = new Graphics::FrameBuffer(m_deviceContext, frameBufferHandle, width, height, false, generateDepth, generateStencilBuffer); return Napi::Pointer::Create(info.Env(), frameBuffer, Napi::NapiPointerDeleter(frameBuffer)); } @@ -1500,7 +1502,7 @@ namespace Babylon void NativeEngine::BindFrameBuffer(NativeDataStream::Reader& data) { - auto encoder = GetUpdateToken().GetEncoder(); + bgfx::Encoder* encoder = GetUpdateToken().GetEncoder(); Graphics::FrameBuffer* frameBuffer = data.ReadPointer(); m_boundFrameBuffer->Unbind(*encoder); @@ -1525,7 +1527,7 @@ namespace Babylon void NativeEngine::DrawIndexed(NativeDataStream::Reader& data) { - bgfx::Encoder* encoder{GetUpdateToken().GetEncoder()}; + bgfx::Encoder* encoder = GetUpdateToken().GetEncoder(); const uint32_t fillMode = data.ReadUint32(); const uint32_t indexStart = data.ReadUint32(); @@ -1542,7 +1544,7 @@ namespace Babylon void NativeEngine::Draw(NativeDataStream::Reader& data) { - bgfx::Encoder* encoder{GetUpdateToken().GetEncoder()}; + bgfx::Encoder* encoder = GetUpdateToken().GetEncoder(); const uint32_t fillMode = data.ReadUint32(); const uint32_t verticesStart = data.ReadUint32(); @@ -1558,7 +1560,7 @@ namespace Babylon void NativeEngine::Clear(NativeDataStream::Reader& data) { - bgfx::Encoder* encoder{GetUpdateToken().GetEncoder()}; + bgfx::Encoder* encoder = GetUpdateToken().GetEncoder(); uint16_t flags{0}; uint32_t rgba{0x000000ff}; @@ -1597,19 +1599,46 @@ namespace Babylon GetBoundFrameBuffer(*encoder).Clear(*encoder, flags, rgba, depth, stencil); } + void NativeEngine::SetStencil(NativeDataStream::Reader& data) + { + const uint32_t writeMask{data.ReadUint32()}; + const uint32_t stencilOpFail{data.ReadUint32()}; + const uint32_t depthOpFail{data.ReadUint32()}; + const uint32_t depthOpPass{data.ReadUint32()}; + const uint32_t func{data.ReadUint32()}; + const uint32_t ref{data.ReadUint32()}; + + m_stencilState = BGFX_STENCIL_FUNC_RMASK(0xFF); // always 0xFF + m_stencilState |= stencilOpFail; + m_stencilState |= depthOpFail; + // bgfx write mask is always 0xFF, to not change stencil value when writemask is 0 + // its value is kept unchanged. + // https://github.com/bkaradzic/bgfx/blob/2c21f68998595fa388e25cb6527e82254d0e9bff/src/renderer_d3d11.cpp#L2874 + if (writeMask == 0) + { + m_stencilState |= BGFX_STENCIL_OP_PASS_Z_KEEP; + } + else + { + m_stencilState |= depthOpPass; + } + m_stencilState |= func; + m_stencilState |= BGFX_STENCIL_FUNC_REF(ref); + } + Napi::Value NativeEngine::GetRenderWidth(const Napi::CallbackInfo& info) { - return Napi::Value::From(info.Env(), m_graphicsContext.GetWidth()); + return Napi::Value::From(info.Env(), m_deviceContext.GetWidth()); } Napi::Value NativeEngine::GetRenderHeight(const Napi::CallbackInfo& info) { - return Napi::Value::From(info.Env(), m_graphicsContext.GetHeight()); + return Napi::Value::From(info.Env(), m_deviceContext.GetHeight()); } void NativeEngine::SetViewPort(const Napi::CallbackInfo& info) { - bgfx::Encoder* encoder{GetUpdateToken().GetEncoder()}; + bgfx::Encoder* encoder = GetUpdateToken().GetEncoder(); const auto x = info[0].As().FloatValue(); const auto y = info[1].As().FloatValue(); @@ -1622,13 +1651,13 @@ namespace Babylon Napi::Value NativeEngine::GetHardwareScalingLevel(const Napi::CallbackInfo& info) { - return Napi::Value::From(info.Env(), m_graphicsContext.GetHardwareScalingLevel()); + return Napi::Value::From(info.Env(), m_deviceContext.GetHardwareScalingLevel()); } void NativeEngine::SetHardwareScalingLevel(const Napi::CallbackInfo& info) { const auto level = info[0].As().FloatValue(); - m_graphicsContext.SetHardwareScalingLevel(level); + m_deviceContext.SetHardwareScalingLevel(level); } Napi::Value NativeEngine::CreateImageBitmap(const Napi::CallbackInfo& info) @@ -1647,7 +1676,7 @@ namespace Babylon throw Napi::Error::New(env, "CreateImageBitmap array buffer is empty."); } - image = ParseImage(m_allocator, gsl::make_span(static_cast(data.Data()), data.ByteLength())); + image = ParseImage(m_deviceContext.Allocator(), gsl::make_span(static_cast(data.Data()), data.ByteLength())); allocatedImage = true; } else if (info[0].IsObject()) @@ -1703,7 +1732,7 @@ namespace Babylon const Napi::Env env{info.Env()}; - bimg::ImageContainer* image = bimg::imageAlloc(&m_allocator, format, static_cast(width), static_cast(height), 1, 1, false, false, data.Data()); + bimg::ImageContainer* image = bimg::imageAlloc(&m_deviceContext.Allocator(), format, static_cast(width), static_cast(height), 1, 1, false, false, data.Data()); if (image == nullptr) { throw Napi::Error::New(env, "Unable to allocate image for ResizeImageBitmap."); @@ -1715,7 +1744,7 @@ namespace Babylon { image->m_format = bimg::TextureFormat::A8; } - bimg::ImageContainer* rgba = bimg::imageConvert(&m_allocator, bimg::TextureFormat::RGBA8, *image, false); + bimg::ImageContainer* rgba = bimg::imageConvert(&m_deviceContext.Allocator(), bimg::TextureFormat::RGBA8, *image, false); if (rgba == nullptr) { throw Napi::Error::New(env, "Unable to convert image to RGBA pixel format for ResizeImageBitmap."); @@ -1738,47 +1767,6 @@ namespace Babylon return Napi::Value::From(env, outputData); } - void NativeEngine::GetFrameBufferData(const Napi::CallbackInfo& info) - { - const auto callback{info[0].As()}; - - auto callbackPtr{std::make_shared(Napi::Persistent(callback))}; - m_graphicsContext.RequestScreenShot([this, callbackPtr{std::move(callbackPtr)}](std::vector array) { - m_runtime.Dispatch([callbackPtr{std::move(callbackPtr)}, array{std::move(array)}](Napi::Env env) { - auto arrayBuffer{Napi::ArrayBuffer::New(env, const_cast(array.data()), array.size())}; - auto typedArray{Napi::Uint8Array::New(env, array.size(), arrayBuffer, 0)}; - callbackPtr->Value().Call({typedArray}); - }); - }); - } - - void NativeEngine::SetStencil(NativeDataStream::Reader& data) - { - const uint32_t writeMask{data.ReadUint32()}; - const uint32_t stencilOpFail{data.ReadUint32()}; - const uint32_t depthOpFail{data.ReadUint32()}; - const uint32_t depthOpPass{data.ReadUint32()}; - const uint32_t func{data.ReadUint32()}; - const uint32_t ref{data.ReadUint32()}; - - m_stencilState = BGFX_STENCIL_FUNC_RMASK(0xFF); // always 0xFF - m_stencilState |= stencilOpFail; - m_stencilState |= depthOpFail; - // bgfx write mask is always 0xFF, to not change stencil value when writemask is 0 - // its value is kept unchanged. - // https://github.com/bkaradzic/bgfx/blob/2c21f68998595fa388e25cb6527e82254d0e9bff/src/renderer_d3d11.cpp#L2874 - if (writeMask == 0) - { - m_stencilState |= BGFX_STENCIL_OP_PASS_Z_KEEP; - } - else - { - m_stencilState |= depthOpPass; - } - m_stencilState |= func; - m_stencilState |= BGFX_STENCIL_FUNC_REF(ref); - } - void NativeEngine::SetCommandDataStream(const Napi::CallbackInfo& info) { // TODO: This should be moved to the constructor once multi-update is available. @@ -1802,6 +1790,18 @@ namespace Babylon } } + void NativeEngine::GetFrameBufferData(const Napi::CallbackInfo& info) + { + const auto callback{info[0].As()}; + + m_deviceContext.RequestScreenShotAsync() + .then(m_runtimeScheduler.Get(), m_cancellationSource, [thisRef = Napi::Persistent(info.This()), callback = Napi::Persistent(callback)](std::vector bytes) { + auto arrayBuffer{Napi::ArrayBuffer::New(thisRef.Env(), const_cast(bytes.data()), bytes.size())}; + auto typedArray{Napi::Uint8Array::New(thisRef.Env(), bytes.size(), arrayBuffer, 0)}; + callback.Call({typedArray}); + }); + } + void NativeEngine::DrawInternal(bgfx::Encoder* encoder, uint32_t fillMode) { uint64_t fillModeState{0}; // indexed triangle list @@ -1874,7 +1874,10 @@ namespace Babylon if (!m_updateToken) { m_updateToken.emplace(m_update.GetUpdateToken()); - m_runtime.Dispatch([this](auto) { + + // TODO: is there an issue with this? + m_runtimeScheduler.Get()([this]() + { m_updateToken.reset(); }); } @@ -1888,7 +1891,8 @@ namespace Babylon { m_boundFrameBuffer = &m_defaultFrameBuffer; m_defaultFrameBuffer.Bind(encoder); - } else if (m_boundFrameBufferNeedsRebinding.Get(encoder)) + } + else if (m_boundFrameBufferNeedsRebinding.Get(encoder)) { m_boundFrameBuffer->Unbind(encoder); m_boundFrameBuffer->Bind(encoder); @@ -1907,8 +1911,8 @@ namespace Babylon m_requestAnimationFrameCallbacksScheduled = true; - arcana::make_task(m_update.Scheduler(), *m_cancellationSource, [this, cancellationSource{m_cancellationSource}]() { - return arcana::make_task(m_runtimeScheduler, *m_cancellationSource, [this, updateToken{m_update.GetUpdateToken()}, cancellationSource{m_cancellationSource}]() { + arcana::make_task(m_update.Scheduler(), m_cancellationSource, [this]() { + return arcana::make_task(m_runtimeScheduler.Get(), m_cancellationSource, [this, updateToken = m_update.GetUpdateToken()]() { m_requestAnimationFrameCallbacksScheduled = false; arcana::trace_region scheduleRegion{"NativeEngine::ScheduleRequestAnimationFrameCallbacks invoke JS callbacks"}; @@ -1917,8 +1921,8 @@ namespace Babylon { callback.Value().Call({}); } - }).then(arcana::inline_scheduler, *m_cancellationSource, [this, cancellationSource{m_cancellationSource}](const arcana::expected& result) { - if (!cancellationSource->cancelled() && result.has_error()) + }).then(arcana::inline_scheduler, m_cancellationSource, [this](const arcana::expected& result) { + if (result.has_error()) { Napi::Error::New(Env(), result.error()).ThrowAsJavaScriptException(); } diff --git a/Plugins/NativeEngine/Source/NativeEngine.h b/Plugins/NativeEngine/Source/NativeEngine.h index 1bc064b76..e1537e4ee 100644 --- a/Plugins/NativeEngine/Source/NativeEngine.h +++ b/Plugins/NativeEngine/Source/NativeEngine.h @@ -171,6 +171,7 @@ namespace Babylon void DrawIndexed(NativeDataStream::Reader& data); void Draw(NativeDataStream::Reader& data); void Clear(NativeDataStream::Reader& data); + void SetStencil(NativeDataStream::Reader& data); Napi::Value GetRenderWidth(const Napi::CallbackInfo& info); Napi::Value GetRenderHeight(const Napi::CallbackInfo& info); void SetViewPort(const Napi::CallbackInfo& info); @@ -178,35 +179,29 @@ namespace Babylon void SetHardwareScalingLevel(const Napi::CallbackInfo& info); Napi::Value CreateImageBitmap(const Napi::CallbackInfo& info); Napi::Value ResizeImageBitmap(const Napi::CallbackInfo& info); - void GetFrameBufferData(const Napi::CallbackInfo& info); - void SetStencil(NativeDataStream::Reader& data); void SetCommandDataStream(const Napi::CallbackInfo& info); void SubmitCommands(const Napi::CallbackInfo& info); - void DrawInternal(bgfx::Encoder* encoder, uint32_t fillMode); + void GetFrameBufferData(const Napi::CallbackInfo& info); + void DrawInternal(bgfx::Encoder* encoder, uint32_t fillMode); std::string ProcessShaderCoordinates(const std::string& vertexSource); - Graphics::UpdateToken& GetUpdateToken(); Graphics::FrameBuffer& GetBoundFrameBuffer(bgfx::Encoder& encoder); - std::shared_ptr m_cancellationSource{}; + arcana::cancellation_source m_cancellationSource{}; ShaderCompiler m_shaderCompiler{}; ProgramData* m_currentProgram{nullptr}; - JsRuntime& m_runtime; - Graphics::DeviceContext& m_graphicsContext; + Graphics::DeviceContext& m_deviceContext; Graphics::Update m_update; - JsRuntimeScheduler m_runtimeScheduler; - std::optional m_updateToken{}; void ScheduleRequestAnimationFrameCallbacks(); bool m_requestAnimationFrameCallbacksScheduled{}; - bx::DefaultAllocator m_allocator{}; uint64_t m_engineState{BGFX_STATE_DEFAULT}; uint32_t m_stencilState{BGFX_STENCIL_TEST_ALWAYS | BGFX_STENCIL_FUNC_REF(0) | BGFX_STENCIL_FUNC_RMASK(0xFF) | BGFX_STENCIL_OP_FAIL_S_KEEP | BGFX_STENCIL_OP_FAIL_Z_KEEP | BGFX_STENCIL_OP_PASS_Z_REPLACE}; @@ -237,5 +232,8 @@ namespace Babylon // TODO: This should be changed to a non-owning ref once multi-update is available. NativeDataStream* m_commandStream{}; + + // Put this last so that it gets destructed first. + JsRuntimeScheduler m_runtimeScheduler; }; } diff --git a/Plugins/NativeInput/Source/Shared/NativeInput.cpp b/Plugins/NativeInput/Source/Shared/NativeInput.cpp index db547cd77..2d0af58e6 100644 --- a/Plugins/NativeInput/Source/Shared/NativeInput.cpp +++ b/Plugins/NativeInput/Source/Shared/NativeInput.cpp @@ -89,7 +89,7 @@ namespace Babylon::Plugins } NativeInput::Impl::Impl(Napi::Env env) - : m_runtimeScheduler{JsRuntime::GetFromJavaScript(env)} + : m_runtime{JsRuntime::GetFromJavaScript(env)} { NativeInput::Impl::DeviceInputSystem::Initialize(env); @@ -152,7 +152,7 @@ namespace Babylon::Plugins void NativeInput::Impl::PointerDown(uint32_t pointerId, uint32_t buttonIndex, int32_t x, int32_t y, DeviceType deviceType) { - m_runtimeScheduler([pointerId, buttonIndex, x, y, deviceType, this]() { + m_runtime.Dispatch([pointerId, buttonIndex, x, y, deviceType, this](auto) { const uint32_t inputIndex{ GetPointerButtonInputIndex(buttonIndex) }; std::vector& deviceInputs{ GetOrCreateInputMap(deviceType, pointerId, { inputIndex, @@ -175,7 +175,7 @@ namespace Babylon::Plugins void NativeInput::Impl::PointerUp(uint32_t pointerId, uint32_t buttonIndex, int32_t x, int32_t y, DeviceType deviceType) { - m_runtimeScheduler([pointerId, buttonIndex, x, y, deviceType, this]() { + m_runtime.Dispatch([pointerId, buttonIndex, x, y, deviceType, this](auto) { const uint32_t inputIndex{ GetPointerButtonInputIndex(buttonIndex) }; std::vector& deviceInputs{ GetOrCreateInputMap(deviceType, pointerId, { inputIndex, @@ -207,7 +207,7 @@ namespace Babylon::Plugins void NativeInput::Impl::PointerMove(uint32_t pointerId, int32_t x, int32_t y, DeviceType deviceType) { - m_runtimeScheduler([pointerId, x, y, deviceType, this]() { + m_runtime.Dispatch([pointerId, x, y, deviceType, this](auto) { std::vector& deviceInputs{GetOrCreateInputMap(deviceType, pointerId, { POINTER_X_INPUT_INDEX, POINTER_Y_INPUT_INDEX, @@ -240,21 +240,20 @@ namespace Babylon::Plugins SetInputState(deviceType, pointerId, POINTER_DELTA_HORIZONTAL_INDEX, 0, deviceInputs, false); SetInputState(deviceType, pointerId, POINTER_DELTA_VERTICAL_INDEX, 0, deviceInputs, false); m_eventDispatcher.tick(arcana::cancellation::none()); - }); } void NativeInput::Impl::PointerScroll(uint32_t pointerId, uint32_t scrollAxis, int32_t scrollValue, DeviceType deviceType) { - m_runtimeScheduler([pointerId, scrollAxis, scrollValue, deviceType, this]() - { - std::vector& deviceInputs{GetOrCreateInputMap(deviceType, pointerId, {scrollAxis})}; - SetInputState(deviceType, pointerId, scrollAxis, scrollValue, deviceInputs, true); + m_runtime.Dispatch([pointerId, scrollAxis, scrollValue, deviceType, this](auto) + { + std::vector& deviceInputs{GetOrCreateInputMap(deviceType, pointerId, {scrollAxis})}; + SetInputState(deviceType, pointerId, scrollAxis, scrollValue, deviceInputs, true); - m_eventDispatcher.tick(arcana::cancellation::none()); - SetInputState(deviceType, pointerId, scrollAxis, 0, deviceInputs, true); - m_eventDispatcher.tick(arcana::cancellation::none()); - }); + m_eventDispatcher.tick(arcana::cancellation::none()); + SetInputState(deviceType, pointerId, scrollAxis, 0, deviceInputs, true); + m_eventDispatcher.tick(arcana::cancellation::none()); + }); } NativeInput::Impl::DeviceStatusChangedCallbackTicket NativeInput::Impl::AddDeviceConnectedCallback(NativeInput::Impl::DeviceStatusChangedCallback&& callback) diff --git a/Plugins/NativeInput/Source/Shared/NativeInput.h b/Plugins/NativeInput/Source/Shared/NativeInput.h index e4cdc8078..8a754d716 100644 --- a/Plugins/NativeInput/Source/Shared/NativeInput.h +++ b/Plugins/NativeInput/Source/Shared/NativeInput.h @@ -67,7 +67,7 @@ namespace Babylon::Plugins void RemoveInputMap(DeviceType deviceType, int32_t deviceSlot); void SetInputState(DeviceType deviceType, int32_t deviceSlot, uint32_t inputIndex, int32_t inputState, std::vector& deviceInputs, bool raiseEvents); - JsRuntimeScheduler m_runtimeScheduler; + JsRuntime& m_runtime; std::unordered_map, InputMapKeyHash> m_inputs{}; arcana::weak_table m_deviceConnectedCallbacks{}; arcana::weak_table m_deviceDisconnectedCallbacks{}; diff --git a/Plugins/NativeXr/Source/NativeXr.cpp b/Plugins/NativeXr/Source/NativeXr.cpp index 7f74d0e82..b4b2b535a 100644 --- a/Plugins/NativeXr/Source/NativeXr.cpp +++ b/Plugins/NativeXr/Source/NativeXr.cpp @@ -412,7 +412,6 @@ namespace Babylon private: Napi::Env m_env; - JsRuntimeScheduler m_runtimeScheduler; std::mutex m_sessionStateChangedCallbackMutex{}; std::function m_sessionStateChangedCallback{}; void* m_windowPtr{}; @@ -461,6 +460,8 @@ namespace Babylon void EndFrame(); void NotifySessionStateChanged(bool isSessionActive); + + JsRuntimeScheduler m_runtimeScheduler; }; NativeXr::Impl::Impl(Napi::Env env) @@ -584,8 +585,8 @@ namespace Babylon m_sessionState->FrameTask = arcana::make_task(m_sessionState->Update.Scheduler(), m_sessionState->CancellationSource, [this, thisRef{shared_from_this()}] { BeginFrame(); - return arcana::make_task(m_runtimeScheduler, m_sessionState->CancellationSource, [this, updateToken{m_sessionState->Update.GetUpdateToken()}, thisRef{shared_from_this()}]() - { + return arcana::make_task(m_runtimeScheduler.Get(), m_sessionState->CancellationSource, [this, updateToken{m_sessionState->Update.GetUpdateToken()}, thisRef{shared_from_this()}]() + { m_sessionState->FrameScheduled = false; BeginUpdate(); @@ -622,7 +623,7 @@ namespace Babylon bool shouldEndSession{}; bool shouldRestartSession{}; m_sessionState->Frame = m_sessionState->Session->GetNextFrame(shouldEndSession, shouldRestartSession, [this](void* texturePointer) { - return arcana::make_task(m_runtimeScheduler, arcana::cancellation::none(), [this, texturePointer]() { + return arcana::make_task(m_runtimeScheduler.Get(), arcana::cancellation::none(), [this, texturePointer]() { const auto itViewConfig{m_sessionState->TextureToViewConfigurationMap.find(texturePointer)}; if (itViewConfig != m_sessionState->TextureToViewConfigurationMap.end()) { @@ -695,7 +696,7 @@ namespace Babylon arcana::make_task(m_sessionState->GraphicsContext.AfterRenderScheduler(), arcana::cancellation::none(), [colorTexture, depthTexture, &viewConfig]() { bgfx::overrideInternal(colorTexture, reinterpret_cast(viewConfig.ColorTexturePointer)); bgfx::overrideInternal(depthTexture, reinterpret_cast(viewConfig.DepthTexturePointer)); - }).then(m_runtimeScheduler, m_sessionState->CancellationSource, [this, thisRef{shared_from_this()}, colorTexture, depthTexture, requiresAppClear, &viewConfig]() { + }).then(m_runtimeScheduler.Get(), m_sessionState->CancellationSource, [this, thisRef{shared_from_this()}, colorTexture, depthTexture, requiresAppClear, &viewConfig]() { const auto eyeCount = std::max(static_cast(1), static_cast(viewConfig.ViewTextureSize.Depth)); // TODO (rgerd): Remove old framebuffers from resource table? viewConfig.FrameBuffers.resize(eyeCount); @@ -2721,7 +2722,7 @@ namespace Babylon auto deferred{Napi::Promise::Deferred::New(info.Env())}; session.m_xr->BeginSessionAsync() - .then(session.m_runtimeScheduler, arcana::cancellation::none(), + .then(session.m_runtimeScheduler.Get(), arcana::cancellation::none(), [deferred, jsSession{std::move(jsSession)}, env{info.Env()}](const arcana::expected& result) { if (result.has_error()) { @@ -3164,7 +3165,7 @@ namespace Babylon Napi::Value End(const Napi::CallbackInfo& info) { auto deferred{Napi::Promise::Deferred::New(info.Env())}; - m_xr->EndSessionAsync().then(m_runtimeScheduler, arcana::cancellation::none(), + m_xr->EndSessionAsync().then(m_runtimeScheduler.Get(), arcana::cancellation::none(), [this, deferred](const arcana::expected& result) { if (result.has_error()) { @@ -3431,11 +3432,10 @@ namespace Babylon // Fire off the IsSessionSupported task. xr::System::IsSessionSupportedAsync(sessionType) - .then(m_runtimeScheduler, - arcana::cancellation::none(), - [deferred, env = info.Env()](bool result) { - deferred.Resolve(Napi::Boolean::New(env, result)); - }); + .then(m_runtimeScheduler.Get(), arcana::cancellation::none(), [deferred, thisRef = Napi::Persistent(info.This())](bool result) + { + deferred.Resolve(Napi::Boolean::New(thisRef.Env(), result)); + }); return deferred.Promise(); } diff --git a/Polyfills/Canvas/Source/Canvas.cpp b/Polyfills/Canvas/Source/Canvas.cpp index d21cc8107..99542c468 100644 --- a/Polyfills/Canvas/Source/Canvas.cpp +++ b/Polyfills/Canvas/Source/Canvas.cpp @@ -54,18 +54,23 @@ namespace Babylon::Polyfills::Internal Napi::Value NativeCanvas::LoadTTFAsync(const Napi::CallbackInfo& info) { + const auto fontName = info[0].As().Utf8Value(); const auto buffer = info[1].As(); std::vector fontBuffer(buffer.ByteLength()); memcpy(fontBuffer.data(), (uint8_t*)buffer.Data(), buffer.ByteLength()); - auto& graphicsContext{Graphics::DeviceContext::GetFromJavaScript(info.Env())}; - auto update = graphicsContext.GetUpdate("update"); - std::shared_ptr runtimeScheduler{ std::make_shared(JsRuntime::GetFromJavaScript(info.Env())) }; - auto deferred{Napi::Promise::Deferred::New(info.Env())}; - arcana::make_task(update.Scheduler(), arcana::cancellation::none(), [fontName{ info[0].As().Utf8Value() }, fontData{ std::move(fontBuffer) }]() { + auto& runtime = JsRuntime::GetFromJavaScript(info.Env()); + auto deferred = Napi::Promise::Deferred::New(info.Env()); + + auto& deviceContext = Graphics::DeviceContext::GetFromJavaScript(info.Env()); + auto update = deviceContext.GetUpdate("update"); + arcana::make_task(update.Scheduler(), arcana::cancellation::none(), [fontName = std::move(fontName), fontData = std::move(fontBuffer), &runtime, deferred]() + { fontsInfos[fontName] = fontData; - }).then(*runtimeScheduler, arcana::cancellation::none(), [runtimeScheduler /*Keep reference alive*/, env{ info.Env() }, deferred]() { - deferred.Resolve(env.Undefined()); + runtime.Dispatch([deferred](Napi::Env env) + { + deferred.Resolve(env.Undefined()); + }); }); return deferred.Promise(); diff --git a/Polyfills/Canvas/Source/Context.cpp b/Polyfills/Canvas/Source/Context.cpp index b7d8f0415..53c7a9530 100644 --- a/Polyfills/Canvas/Source/Context.cpp +++ b/Polyfills/Canvas/Source/Context.cpp @@ -90,9 +90,8 @@ namespace Babylon::Polyfills::Internal : Napi::ObjectWrap{info} , m_canvas{info[0].As>().Data()} , m_nvg{nvgCreate(1)} - , m_graphicsContext{m_canvas->GetGraphicsContext()} - , m_update{m_graphicsContext.GetUpdate("update")} - , m_cancellationSource{std::make_shared()} + , m_deviceContext{m_canvas->GetGraphicsContext()} + , m_update{m_deviceContext.GetUpdate("update")} , m_runtimeScheduler{Babylon::JsRuntime::GetFromJavaScript(info.Env())} , Polyfills::Canvas::Impl::MonitoredResource{Polyfills::Canvas::Impl::GetFromJavaScript(info.Env())} { @@ -105,7 +104,6 @@ namespace Babylon::Polyfills::Internal Context::~Context() { Dispose(); - m_cancellationSource->cancel(); } void Context::Dispose(const Napi::CallbackInfo&) @@ -120,12 +118,15 @@ namespace Babylon::Polyfills::Internal void Context::Dispose() { + m_cancellationSource.cancel(); + if (m_nvg) { - for(auto& image : m_nvgImageIndices) + for (auto& image : m_nvgImageIndices) { nvgDeleteImage(m_nvg, image.second); } + nvgDelete(m_nvg); m_nvg = nullptr; } @@ -364,8 +365,10 @@ namespace Babylon::Polyfills::Internal // Unlike other systems where it's cleared. bool needClear = m_canvas->UpdateRenderTarget(); - arcana::make_task(m_update.Scheduler(), *m_cancellationSource, [this, needClear, cancellationSource{ m_cancellationSource }]() { - return arcana::make_task(m_runtimeScheduler, *m_cancellationSource, [this, needClear, updateToken{ m_update.GetUpdateToken() }, cancellationSource{ m_cancellationSource }]() { + // TODO: capture thisRef + + arcana::make_task(m_update.Scheduler(), m_cancellationSource, [this, needClear]() { + return arcana::make_task(m_runtimeScheduler.Get(), m_cancellationSource, [this, needClear, updateToken = m_update.GetUpdateToken()]() { // JS Thread Graphics::FrameBuffer& frameBuffer = m_canvas->GetFrameBuffer(); bgfx::Encoder* encoder = m_update.GetUpdateToken().GetEncoder(); @@ -383,8 +386,8 @@ namespace Babylon::Polyfills::Internal nvgEndFrame(m_nvg); frameBuffer.Unbind(*encoder); m_dirty = false; - }).then(arcana::inline_scheduler, *m_cancellationSource, [this, cancellationSource{ m_cancellationSource }](const arcana::expected& result) { - if (!cancellationSource->cancelled() && result.has_error()) + }).then(arcana::inline_scheduler, m_cancellationSource, [this](const arcana::expected& result) { + if (result.has_error()) { Napi::Error::New(Env(), result.error()).ThrowAsJavaScriptException(); } diff --git a/Polyfills/Canvas/Source/Context.h b/Polyfills/Canvas/Source/Context.h index b740595e2..8fd053ab3 100644 --- a/Polyfills/Canvas/Source/Context.h +++ b/Polyfills/Canvas/Source/Context.h @@ -87,7 +87,7 @@ namespace Babylon::Polyfills::Internal std::map m_fonts; int m_currentFontId{ -1 }; - Graphics::DeviceContext& m_graphicsContext; + Graphics::DeviceContext& m_deviceContext; Graphics::Update m_update; bool m_dirty{}; @@ -97,7 +97,7 @@ namespace Babylon::Polyfills::Internal float left, top, width, height; } m_rectangleClipping{}; - std::shared_ptr m_cancellationSource{}; + arcana::cancellation_source m_cancellationSource{}; JsRuntimeScheduler m_runtimeScheduler; std::unordered_map m_nvgImageIndices; diff --git a/Polyfills/Canvas/Source/Image.cpp b/Polyfills/Canvas/Source/Image.cpp index eb4d75750..2b3869f11 100644 --- a/Polyfills/Canvas/Source/Image.cpp +++ b/Polyfills/Canvas/Source/Image.cpp @@ -41,7 +41,6 @@ namespace Babylon::Polyfills::Internal NativeCanvasImage::NativeCanvasImage(const Napi::CallbackInfo& info) : Napi::ObjectWrap{info} , m_runtimeScheduler{JsRuntime::GetFromJavaScript(info.Env())} - , m_cancellationSource{std::make_shared()} { } @@ -57,7 +56,8 @@ namespace Babylon::Polyfills::Internal bimg::imageFree(m_imageContainer); m_imageContainer = nullptr; } - m_cancellationSource->cancel(); + + m_cancellationSource.cancel(); } Napi::Value NativeCanvasImage::GetWidth(const Napi::CallbackInfo&) @@ -103,10 +103,12 @@ namespace Babylon::Polyfills::Internal UrlLib::UrlRequest request{}; request.Open(UrlLib::UrlMethod::Get, text); request.ResponseType(UrlLib::UrlResponseType::Buffer); - request.SendAsync().then(m_runtimeScheduler, *m_cancellationSource, [env{info.Env()}, this, request{std::move(request)}](arcana::expected result) { + request.SendAsync() + .then(m_runtimeScheduler.Get(), m_cancellationSource, [this, thisRef = Napi::Persistent(info.This()), request = std::move(request)](arcana::expected result) + { if (result.has_error()) { - HandleLoadImageError(Napi::Error::New(env, result.error())); + HandleLoadImageError(Napi::Error::New(thisRef.Env(), result.error())); return; } @@ -115,14 +117,14 @@ namespace Babylon::Polyfills::Internal auto buffer{request.ResponseBuffer()}; if (buffer.data() == nullptr || buffer.size_bytes() == 0) { - HandleLoadImageError(Napi::Error::New(env, "Image with provided source returned empty response.")); + HandleLoadImageError(Napi::Error::New(thisRef.Env(), "Image with provided source returned empty response.")); return; } - m_imageContainer = bimg::imageParse(&m_allocator, buffer.data(), static_cast(buffer.size_bytes())); + m_imageContainer = bimg::imageParse(&Graphics::DeviceContext::GetFromJavaScript(thisRef.Env()).Allocator(), buffer.data(), static_cast(buffer.size_bytes())); if (m_imageContainer == nullptr) { - HandleLoadImageError(Napi::Error::New(env, "Unable to decode image with provided src.")); + HandleLoadImageError(Napi::Error::New(thisRef.Env(), "Unable to decode image with provided src.")); return; } diff --git a/Polyfills/Canvas/Source/Image.h b/Polyfills/Canvas/Source/Image.h index a4b41adb0..0c840ea70 100644 --- a/Polyfills/Canvas/Source/Image.h +++ b/Polyfills/Canvas/Source/Image.h @@ -43,11 +43,12 @@ namespace Babylon::Polyfills::Internal std::string m_src{}; - JsRuntimeScheduler m_runtimeScheduler; Napi::FunctionReference m_onloadHandlerRef; Napi::FunctionReference m_onerrorHandlerRef; - std::shared_ptr m_cancellationSource{}; - bx::DefaultAllocator m_allocator{}; + arcana::cancellation_source m_cancellationSource{}; bimg::ImageContainer* m_imageContainer{}; + + // Put this last so that it gets destructed first. + JsRuntimeScheduler m_runtimeScheduler; }; } diff --git a/Polyfills/Window/Source/Window.cpp b/Polyfills/Window/Source/Window.cpp index 542eeef1f..607b2cb5c 100644 --- a/Polyfills/Window/Source/Window.cpp +++ b/Polyfills/Window/Source/Window.cpp @@ -71,10 +71,15 @@ namespace Babylon::Polyfills::Internal Window::Window(const Napi::CallbackInfo& info) : Napi::ObjectWrap{info} - , m_runtime{JsRuntime::GetFromJavaScript(info.Env())} + , m_runtimeScheduler{JsRuntime::GetFromJavaScript(info.Env())} { } + Window::~Window() + { + m_cancelSource.cancel(); + } + void Window::SetTimeout(const Napi::CallbackInfo& info) { auto function = Napi::Persistent(info[0].As()); @@ -82,7 +87,7 @@ namespace Babylon::Polyfills::Internal auto& window = *static_cast(info.Data()); - window.RecursiveWaitOrCall(std::make_shared(std::move(function)), std::chrono::system_clock::now() + milliseconds); + window.RecursiveWaitOrCall(Napi::Persistent(info.This()), std::move(function), std::chrono::system_clock::now() + milliseconds); } Napi::Value Window::DecodeBase64(const Napi::CallbackInfo& info) @@ -104,17 +109,19 @@ namespace Babylon::Polyfills::Internal } void Window::RecursiveWaitOrCall( - std::shared_ptr function, + Napi::Reference thisRef, + Napi::FunctionReference function, std::chrono::system_clock::time_point whenToRun) { - m_runtime.Dispatch([this, function = std::move(function), whenToRun](Napi::Env) { + arcana::make_task(m_runtimeScheduler.Get(), m_cancelSource, [this, thisRef = std::move(thisRef), function = std::move(function), whenToRun]() mutable + { if (std::chrono::system_clock::now() >= whenToRun) { - function->Call({}); + function.Call({}); } else { - RecursiveWaitOrCall(std::move(function), whenToRun); + RecursiveWaitOrCall(std::move(thisRef), std::move(function), whenToRun); } }); } diff --git a/Polyfills/Window/Source/Window.h b/Polyfills/Window/Source/Window.h index 1ad234be4..ecccc192e 100644 --- a/Polyfills/Window/Source/Window.h +++ b/Polyfills/Window/Source/Window.h @@ -1,6 +1,6 @@ #pragma once -#include +#include namespace Babylon::Polyfills::Internal { @@ -13,15 +13,20 @@ namespace Babylon::Polyfills::Internal static Window& GetFromJavaScript(Napi::Env); Window(const Napi::CallbackInfo& info); - private: - JsRuntime& m_runtime; + ~Window(); + private: static void SetTimeout(const Napi::CallbackInfo& info); static Napi::Value DecodeBase64(const Napi::CallbackInfo& info); static void AddEventListener(const Napi::CallbackInfo& info); static void RemoveEventListener(const Napi::CallbackInfo& info); static Napi::Value GetDevicePixelRatio(const Napi::CallbackInfo& info); - void RecursiveWaitOrCall(std::shared_ptr function, std::chrono::system_clock::time_point whenToRun); + void RecursiveWaitOrCall(Napi::Reference thisRef, Napi::FunctionReference function, std::chrono::system_clock::time_point whenToRun); + + arcana::cancellation_source m_cancelSource; + + // Put this last so that it gets destructed first. + JsRuntimeScheduler m_runtimeScheduler; }; } diff --git a/Polyfills/XMLHttpRequest/Source/XMLHttpRequest.cpp b/Polyfills/XMLHttpRequest/Source/XMLHttpRequest.cpp index 3c20242b2..22cc404fc 100644 --- a/Polyfills/XMLHttpRequest/Source/XMLHttpRequest.cpp +++ b/Polyfills/XMLHttpRequest/Source/XMLHttpRequest.cpp @@ -233,10 +233,13 @@ namespace Babylon::Polyfills::Internal throw Napi::Error::New(info.Env(), "XMLHttpRequest must be opened before it can be sent"); return; } - m_request.SendAsync().then(m_runtimeScheduler, arcana::cancellation::none(), [env{info.Env()}, this](arcana::expected result) { + + m_request.SendAsync() + .then(m_runtimeScheduler.Get(), arcana::cancellation::none(), [this, thisRef = Napi::Persistent(info.This())](arcana::expected result) + { if (result.has_error()) { - Napi::Error::New(env, result.error()).ThrowAsJavaScriptException(); + Napi::Error::New(thisRef.Env(), result.error()).ThrowAsJavaScriptException(); return; } From 925d243ea74d4a855529c504addd4785c5cded62 Mon Sep 17 00:00:00 2001 From: Gary Hsu Date: Thu, 29 Sep 2022 14:00:27 -0700 Subject: [PATCH 02/21] Change to explicitly call Rundown --- Core/AppRuntime/Source/WorkQueue.cpp | 6 +- Core/AppRuntime/Source/WorkQueue.h | 4 +- .../Include/Babylon/JsRuntimeScheduler.h | 56 +++---- Plugins/NativeCamera/Source/NativeVideo.cpp | 22 ++- Plugins/NativeCamera/Source/NativeVideo.h | 8 +- Plugins/NativeEngine/Source/NativeEngine.cpp | 3 + Plugins/NativeEngine/Source/NativeEngine.h | 4 +- Polyfills/Canvas/Source/Context.cpp | 79 +++++----- Polyfills/Canvas/Source/Context.h | 4 +- Polyfills/Canvas/Source/Image.cpp | 6 +- Polyfills/Canvas/Source/Image.h | 3 +- Polyfills/Window/Source/Window.cpp | 6 +- Polyfills/Window/Source/Window.h | 7 +- .../XMLHttpRequest/Source/XMLHttpRequest.cpp | 141 ++++++++++-------- .../XMLHttpRequest/Source/XMLHttpRequest.h | 5 +- 15 files changed, 197 insertions(+), 157 deletions(-) diff --git a/Core/AppRuntime/Source/WorkQueue.cpp b/Core/AppRuntime/Source/WorkQueue.cpp index a20cb8665..2fa9bbc62 100644 --- a/Core/AppRuntime/Source/WorkQueue.cpp +++ b/Core/AppRuntime/Source/WorkQueue.cpp @@ -18,7 +18,7 @@ namespace Babylon // It must be dispatched and not canceled directly to ensure that // existing work is executed and executed in the correct order. m_dispatcher([this]() { - m_cancelSource.cancel(); + m_cancellationSource.cancel(); }); m_thread.join(); @@ -43,9 +43,9 @@ namespace Babylon m_env = std::make_optional(env); m_dispatcher.set_affinity(std::this_thread::get_id()); - while (!m_cancelSource.cancelled()) + while (!m_cancellationSource.cancelled()) { - m_dispatcher.blocking_tick(m_cancelSource); + m_dispatcher.blocking_tick(m_cancellationSource); } // There should not be any outstanding work during the shutdown sequence diff --git a/Core/AppRuntime/Source/WorkQueue.h b/Core/AppRuntime/Source/WorkQueue.h index dd9ba0fbe..914f6dbce 100644 --- a/Core/AppRuntime/Source/WorkQueue.h +++ b/Core/AppRuntime/Source/WorkQueue.h @@ -19,7 +19,7 @@ namespace Babylon template void Append(CallableT callable) { - if (m_cancelSource.cancelled()) + if (m_cancellationSource.cancelled()) { // There is likely a coding error if this exception is thrown. throw std::runtime_error{"Cannot append to the work queue after it is canceled"}; @@ -54,7 +54,7 @@ namespace Babylon std::optional> m_suspensionLock{}; - arcana::cancellation_source m_cancelSource{}; + arcana::cancellation_source m_cancellationSource{}; arcana::manual_dispatcher<128> m_dispatcher{}; std::thread m_thread; diff --git a/Core/JsRuntime/Include/Babylon/JsRuntimeScheduler.h b/Core/JsRuntime/Include/Babylon/JsRuntimeScheduler.h index a3ad476ec..8b41fd023 100644 --- a/Core/JsRuntime/Include/Babylon/JsRuntimeScheduler.h +++ b/Core/JsRuntime/Include/Babylon/JsRuntimeScheduler.h @@ -11,47 +11,48 @@ namespace Babylon // handling garbage collection and shutdown scenarios. This class provides and manages the schedulers intended // for a N-API object to use with arcana tasks. It is different than the typical scheduler as this class itself // is not a scheduler directly, but instead hands out scheduler via its `Get()` function. It provides special - // handling for when the JsRuntime begins shutting down, i.e., when JsRuntime::NotifyDisposing is called: - // 1. The destructor blocks if there are outstanding schedulers not yet invoked on the JavaScript thread. + // handling for when the JsRuntime begins shutting down, i.e., when JsRuntime::NotifyDisposing is called. + // 1. Calling `Rundown` blocks execution until all outstanding schedulers are invoked on the JavaScript thread. // 2. Once the JsRuntime begins shutting down, all schedulers will reroute its dispatch calls from the // JsRuntime to a separate dispatcher owned by the JsRuntimeScheduler itself. This class will then be able // to pump this dispatcher in its destructor to prevent deadlocks. // // The typical pattern for an arcana task will look something like this: - // // class MyClass // { // public: - // void MyFunction(const Napi::CallbackInfo& info); + // ~MyClass() + // { + // m_cancellationSource.cancel(); + // + // // Wait for asynchronous operations to complete. + // m_runtimeScheduler.Rundown(); + // } + // + // void MyFunction(const Napi::CallbackInfo& info) + // { + // const auto callback{info[0].As()}; + // + // arcana::make_task(arcana::threadpool_scheduler, m_cancellationSource, []() { + // // do some asynchronous work + // }).then(m_runtimeScheduler.Get(), m_cancellationSource, [thisRef = Napi::Persistent(info.This()), callback = Napi::Persistent(callback)]() { + // callback.Call({}); + // }); + // } // // private: // arcana::cancellation_source m_cancellationSource; - // - // // Put this last so that it gets destructed first. // JsRuntimeScheduler m_runtimeScheduler; // }; // - // void MyClass::MyFunction(const Napi::CallbackInfo& info) - // { - // const auto callback{info[0].As()}; - // - // arcana::make_task(arcana::threadpool_scheduler, m_cancellationSource, []() - // { - // // do some asynchronous work - // }).then(m_runtimeScheduler.Get(), m_cancelSource, [thisRef = Napi::Persistent(info.This()), callback = Napi::Persistent(callback)]() { - // { - // callback.Call({}); - // }); - // } - // // **IMPORTANT**: - // 1. To prevent continuations from accessing destructed objects, declare the JsRuntimeScheduler at the end of - // the N-API class. The destructor of the JsRuntimeScheduler will call `Rundown()` which will block until - // all of its schedulers are invoked. If this is not possible, call `Rundown()` manually in the destructor - // of the N-API class. + // 1. To prevent continuations from accessing freed memory, the destructor of the N-API class is expected to call + // `Rundown()` which blocks execution until all of its schedulers are invoked. Failing to do so will result in + // an exception if there are outstanding schedulers not yet invoked. // 2. The last continuation that accesses members of the N-API object, including the cancellation associated with - // the continuation must capture a persistent reference to the N-API object itself to prevent GC from collecting - // the N-API object during the asynchronous operation. + // the continuation, must capture a persistent reference to the N-API object itself to prevent the GC from + // collecting the N-API object during the asynchronous operation. Failing to do so will result in a hang + // when `Rundown()` is called in the N-API class destructor. class JsRuntimeScheduler { public: @@ -67,7 +68,10 @@ namespace Babylon ~JsRuntimeScheduler() { - Rundown(); + if (m_count > 0) + { + throw std::runtime_error{"Schedulers for the JavaScript thread are not yet invoked"}; + } } // Wait until all of the schedulers are invoked. diff --git a/Plugins/NativeCamera/Source/NativeVideo.cpp b/Plugins/NativeCamera/Source/NativeVideo.cpp index 634ac257a..a9fef60de 100644 --- a/Plugins/NativeCamera/Source/NativeVideo.cpp +++ b/Plugins/NativeCamera/Source/NativeVideo.cpp @@ -43,6 +43,14 @@ namespace Babylon::Plugins { } + NativeVideo::~NativeVideo() + { + m_cancellationSource.cancel(); + + // Wait for async operations to complete. + m_runtimeScheduler.Rundown(); + } + Napi::Value NativeVideo::GetVideoWidth(const Napi::CallbackInfo& /*info*/) { return Napi::Value::From(Env(), m_width); @@ -133,34 +141,34 @@ namespace Babylon::Plugins Napi::Value NativeVideo::Play(const Napi::CallbackInfo& info) { - auto env{info.Env()}; - auto deferred{Napi::Promise::Deferred::New(env)}; + auto deferred = Napi::Promise::Deferred::New(info.Env()); if (!m_IsPlaying) { m_IsPlaying = true; NativeCameraImpl->Open(m_maxWidth, m_maxHeight, m_frontCamera) - .then(m_runtimeScheduler.Get(), arcana::cancellation::none(), [this, env, deferred](const arcana::expected& result) + .then(m_runtimeScheduler.Get(), m_cancellationSource, + [this, thisRef = Napi::Persistent(info.This()), deferred](const arcana::expected& result) { if (result.has_error()) { - deferred.Reject(Napi::Error::New(env, result.error()).Value()); + deferred.Reject(Napi::Error::New(thisRef.Env(), result.error()).Value()); } else { - auto cameraDimensions{result.value()}; + auto cameraDimensions = result.value(); this->m_width = cameraDimensions.width; this->m_height = cameraDimensions.height; this->m_isReady = true; - deferred.Resolve(env.Undefined()); + deferred.Resolve(thisRef.Env().Undefined()); RaiseEvent("playing"); } }); } else { - deferred.Resolve(env.Undefined()); + deferred.Resolve(info.Env().Undefined()); } return deferred.Promise(); diff --git a/Plugins/NativeCamera/Source/NativeVideo.h b/Plugins/NativeCamera/Source/NativeVideo.h index 9e14c4f61..2a9d622cd 100644 --- a/Plugins/NativeCamera/Source/NativeVideo.h +++ b/Plugins/NativeCamera/Source/NativeVideo.h @@ -17,7 +17,7 @@ namespace Babylon::Plugins static void Initialize(Napi::Env& env, std::shared_ptr nativeCameraImpl); static Napi::Object New(const Napi::CallbackInfo& info, uint32_t width, uint32_t height, bool frontCamera); NativeVideo(const Napi::CallbackInfo& info); - ~NativeVideo() = default; + ~NativeVideo(); void UpdateTexture(bgfx::TextureHandle textureHandle); @@ -35,6 +35,9 @@ namespace Babylon::Plugins Napi::Value GetReadyState(const Napi::CallbackInfo& info); Napi::Value GetHaveCurrentData(const Napi::CallbackInfo& info); + arcana::cancellation_source m_cancellationSource; + JsRuntimeScheduler m_runtimeScheduler; + std::unordered_map> m_eventHandlerRefs{}; uint32_t m_maxWidth{}; uint32_t m_maxHeight{}; @@ -46,9 +49,6 @@ namespace Babylon::Plugins bool m_IsPlaying{}; - // Put this last so that it gets destructed first. - JsRuntimeScheduler m_runtimeScheduler; - static inline std::shared_ptr NativeCameraImpl{}; }; } diff --git a/Plugins/NativeEngine/Source/NativeEngine.cpp b/Plugins/NativeEngine/Source/NativeEngine.cpp index da6ef2e2c..257d2a65d 100644 --- a/Plugins/NativeEngine/Source/NativeEngine.cpp +++ b/Plugins/NativeEngine/Source/NativeEngine.cpp @@ -513,6 +513,9 @@ namespace Babylon NativeEngine::~NativeEngine() { Dispose(); + + // Wait for async operations to complete. + m_runtimeScheduler.Rundown(); } void NativeEngine::Dispose() diff --git a/Plugins/NativeEngine/Source/NativeEngine.h b/Plugins/NativeEngine/Source/NativeEngine.h index e1537e4ee..f832833fa 100644 --- a/Plugins/NativeEngine/Source/NativeEngine.h +++ b/Plugins/NativeEngine/Source/NativeEngine.h @@ -189,6 +189,7 @@ namespace Babylon Graphics::FrameBuffer& GetBoundFrameBuffer(bgfx::Encoder& encoder); arcana::cancellation_source m_cancellationSource{}; + JsRuntimeScheduler m_runtimeScheduler; ShaderCompiler m_shaderCompiler{}; @@ -232,8 +233,5 @@ namespace Babylon // TODO: This should be changed to a non-owning ref once multi-update is available. NativeDataStream* m_commandStream{}; - - // Put this last so that it gets destructed first. - JsRuntimeScheduler m_runtimeScheduler; }; } diff --git a/Polyfills/Canvas/Source/Context.cpp b/Polyfills/Canvas/Source/Context.cpp index 53c7a9530..9223836e2 100644 --- a/Polyfills/Canvas/Source/Context.cpp +++ b/Polyfills/Canvas/Source/Context.cpp @@ -104,6 +104,9 @@ namespace Babylon::Polyfills::Internal Context::~Context() { Dispose(); + + // Wait for async operations to complete. + m_runtimeScheduler.Rundown(); } void Context::Dispose(const Napi::CallbackInfo&) @@ -145,7 +148,7 @@ namespace Babylon::Polyfills::Internal const auto color = StringToColor(info.Env(), m_fillStyle); nvgFillColor(m_nvg, color); nvgFill(m_nvg); - SetDirty(); + SetDirty(info.This()); } Napi::Value Context::GetFillStyle(const Napi::CallbackInfo&) @@ -158,7 +161,7 @@ namespace Babylon::Polyfills::Internal m_fillStyle = value.As().Utf8Value(); const auto color = StringToColor(info.Env(), m_fillStyle); nvgFillColor(m_nvg, color); - SetDirty(); + SetDirty(info.This()); } Napi::Value Context::GetStrokeStyle(const Napi::CallbackInfo&) @@ -171,37 +174,37 @@ namespace Babylon::Polyfills::Internal m_strokeStyle = value.As().Utf8Value(); auto color = StringToColor(info.Env(), m_strokeStyle); nvgStrokeColor(m_nvg, color); - SetDirty(); + SetDirty(info.This()); } - Napi::Value Context::GetLineWidth(const Napi::CallbackInfo& ) + Napi::Value Context::GetLineWidth(const Napi::CallbackInfo& info) { return Napi::Value::From(Env(), m_lineWidth); } - void Context::SetLineWidth(const Napi::CallbackInfo&, const Napi::Value& value) + void Context::SetLineWidth(const Napi::CallbackInfo& info, const Napi::Value& value) { m_lineWidth = value.As().FloatValue(); nvgStrokeWidth(m_nvg, m_lineWidth); - SetDirty(); + SetDirty(info.This()); } - void Context::Fill(const Napi::CallbackInfo&) + void Context::Fill(const Napi::CallbackInfo& info) { nvgFill(m_nvg); - SetDirty(); + SetDirty(info.This()); } - void Context::Save(const Napi::CallbackInfo&) + void Context::Save(const Napi::CallbackInfo& info) { nvgSave(m_nvg); - SetDirty(); + SetDirty(info.This()); } - void Context::Restore(const Napi::CallbackInfo&) + void Context::Restore(const Napi::CallbackInfo& info) { nvgRestore(m_nvg); - SetDirty(); + SetDirty(info.This()); } void Context::ClearRect(const Napi::CallbackInfo& info) @@ -219,7 +222,7 @@ namespace Babylon::Polyfills::Internal nvgFillColor(m_nvg, TRANSPARENT_BLACK); nvgFill(m_nvg); nvgRestore(m_nvg); - SetDirty(); + SetDirty(info.This()); } void Context::Translate(const Napi::CallbackInfo& info) @@ -227,14 +230,14 @@ namespace Babylon::Polyfills::Internal const auto x = info[0].As().FloatValue(); const auto y = info[1].As().FloatValue(); nvgTranslate(m_nvg, x, y); - SetDirty(); + SetDirty(info.This()); } void Context::Rotate(const Napi::CallbackInfo& info) { const auto angle = info[0].As().FloatValue(); nvgRotate(m_nvg, nvgDegToRad(angle)); - SetDirty(); + SetDirty(info.This()); } void Context::Scale(const Napi::CallbackInfo& info) @@ -242,19 +245,19 @@ namespace Babylon::Polyfills::Internal const auto x = info[0].As().FloatValue(); const auto y = info[1].As().FloatValue(); nvgScale(m_nvg, x, y); - SetDirty(); + SetDirty(info.This()); } - void Context::BeginPath(const Napi::CallbackInfo&) + void Context::BeginPath(const Napi::CallbackInfo& info) { nvgBeginPath(m_nvg); - SetDirty(); + SetDirty(info.This()); } - void Context::ClosePath(const Napi::CallbackInfo&) + void Context::ClosePath(const Napi::CallbackInfo& info) { nvgClosePath(m_nvg); - SetDirty(); + SetDirty(info.This()); } void Context::Rect(const Napi::CallbackInfo& info) @@ -266,7 +269,7 @@ namespace Babylon::Polyfills::Internal nvgRect(m_nvg, left, top, width, height); m_rectangleClipping = {left, top, width, height}; - SetDirty(); + SetDirty(info.This()); } void Context::Clip(const Napi::CallbackInfo& /*info*/) @@ -284,13 +287,13 @@ namespace Babylon::Polyfills::Internal nvgRect(m_nvg, left, top, width, height); nvgStroke(m_nvg); - SetDirty(); + SetDirty(info.This()); } - void Context::Stroke(const Napi::CallbackInfo&) + void Context::Stroke(const Napi::CallbackInfo& info) { nvgStroke(m_nvg); - SetDirty(); + SetDirty(info.This()); } void Context::MoveTo(const Napi::CallbackInfo& info) @@ -299,7 +302,7 @@ namespace Babylon::Polyfills::Internal const auto y = info[1].As().FloatValue(); nvgMoveTo(m_nvg, x, y); - SetDirty(); + SetDirty(info.This()); } void Context::LineTo(const Napi::CallbackInfo& info) @@ -308,7 +311,7 @@ namespace Babylon::Polyfills::Internal const auto y = info[1].As().FloatValue(); nvgLineTo(m_nvg, x, y); - SetDirty(); + SetDirty(info.This()); } void Context::QuadraticCurveTo(const Napi::CallbackInfo& info) @@ -319,7 +322,7 @@ namespace Babylon::Polyfills::Internal const auto y = info[3].As().FloatValue(); nvgBezierTo(m_nvg, cx, cy, cx, cy, x, y); - SetDirty(); + SetDirty(info.This()); } Napi::Value Context::MeasureText(const Napi::CallbackInfo& info) @@ -346,28 +349,26 @@ namespace Babylon::Polyfills::Internal } nvgText(m_nvg, x, y, text.c_str(), nullptr); - SetDirty(); + SetDirty(info.This()); } } - void Context::SetDirty() + void Context::SetDirty(Napi::Value thisVal) { if (!m_dirty) { m_dirty = true; - DeferredFlushFrame(); + DeferredFlushFrame(std::move(thisVal)); } } - void Context::DeferredFlushFrame() + void Context::DeferredFlushFrame(Napi::Value thisVal) { // on some systems (Ubuntu), the framebuffer contains garbage. // Unlike other systems where it's cleared. bool needClear = m_canvas->UpdateRenderTarget(); - // TODO: capture thisRef - - arcana::make_task(m_update.Scheduler(), m_cancellationSource, [this, needClear]() { + arcana::make_task(m_update.Scheduler(), m_cancellationSource, [this, thisRef = Napi::Persistent(thisVal), needClear]() mutable { return arcana::make_task(m_runtimeScheduler.Get(), m_cancellationSource, [this, needClear, updateToken = m_update.GetUpdateToken()]() { // JS Thread Graphics::FrameBuffer& frameBuffer = m_canvas->GetFrameBuffer(); @@ -386,7 +387,7 @@ namespace Babylon::Polyfills::Internal nvgEndFrame(m_nvg); frameBuffer.Unbind(*encoder); m_dirty = false; - }).then(arcana::inline_scheduler, m_cancellationSource, [this](const arcana::expected& result) { + }).then(arcana::inline_scheduler, m_cancellationSource, [this, thisRef = std::move(thisRef)](const arcana::expected& result) { if (result.has_error()) { Napi::Error::New(Env(), result.error()).ThrowAsJavaScriptException(); @@ -409,7 +410,7 @@ namespace Babylon::Polyfills::Internal const double endAngle = info[4].As().DoubleValue(); const NVGwinding winding = (info.Length() == 6 && info[5].As()) ? NVGwinding::NVG_CCW : NVGwinding::NVG_CW; nvgArc(m_nvg, x, y, radius, startAngle, endAngle, winding); - SetDirty(); + SetDirty(info.This()); } void Context::DrawImage(const Napi::CallbackInfo& info) @@ -441,7 +442,7 @@ namespace Babylon::Polyfills::Internal nvgRect(m_nvg, dx, dy, width, height); nvgFillPaint(m_nvg, imagePaint); nvgFill(m_nvg); - SetDirty(); + SetDirty(info.This()); } else if (info.Length() == 5) { @@ -455,7 +456,7 @@ namespace Babylon::Polyfills::Internal nvgRect(m_nvg, dx, dy, dWidth, dHeight); nvgFillPaint(m_nvg, imagePaint); nvgFill(m_nvg); - SetDirty(); + SetDirty(info.This()); } else if (info.Length() == 9) { @@ -475,7 +476,7 @@ namespace Babylon::Polyfills::Internal nvgRect(m_nvg, dx, dy, dWidth, dHeight); nvgFillPaint(m_nvg, imagePaint); nvgFill(m_nvg); - SetDirty(); + SetDirty(info.This()); } else { diff --git a/Polyfills/Canvas/Source/Context.h b/Polyfills/Canvas/Source/Context.h index 8fd053ab3..e12f8f968 100644 --- a/Polyfills/Canvas/Source/Context.h +++ b/Polyfills/Canvas/Source/Context.h @@ -72,8 +72,8 @@ namespace Babylon::Polyfills::Internal Napi::Value GetCanvas(const Napi::CallbackInfo&); void Dispose(const Napi::CallbackInfo&); void Dispose(); - void SetDirty(); - void DeferredFlushFrame(); + void SetDirty(Napi::Value thisVal); + void DeferredFlushFrame(Napi::Value thisVal); NativeCanvas* m_canvas; NVGcontext* m_nvg; diff --git a/Polyfills/Canvas/Source/Image.cpp b/Polyfills/Canvas/Source/Image.cpp index 2b3869f11..3d37d2227 100644 --- a/Polyfills/Canvas/Source/Image.cpp +++ b/Polyfills/Canvas/Source/Image.cpp @@ -47,6 +47,9 @@ namespace Babylon::Polyfills::Internal NativeCanvasImage::~NativeCanvasImage() { Dispose(); + + // Wait for async operations to complete. + m_runtimeScheduler.Rundown(); } void NativeCanvasImage::Dispose() @@ -104,7 +107,8 @@ namespace Babylon::Polyfills::Internal request.Open(UrlLib::UrlMethod::Get, text); request.ResponseType(UrlLib::UrlResponseType::Buffer); request.SendAsync() - .then(m_runtimeScheduler.Get(), m_cancellationSource, [this, thisRef = Napi::Persistent(info.This()), request = std::move(request)](arcana::expected result) + .then(m_runtimeScheduler.Get(), m_cancellationSource, + [this, thisRef = Napi::Persistent(info.This()), request](arcana::expected result) { if (result.has_error()) { diff --git a/Polyfills/Canvas/Source/Image.h b/Polyfills/Canvas/Source/Image.h index 0c840ea70..5f1dff729 100644 --- a/Polyfills/Canvas/Source/Image.h +++ b/Polyfills/Canvas/Source/Image.h @@ -45,10 +45,9 @@ namespace Babylon::Polyfills::Internal Napi::FunctionReference m_onloadHandlerRef; Napi::FunctionReference m_onerrorHandlerRef; - arcana::cancellation_source m_cancellationSource{}; bimg::ImageContainer* m_imageContainer{}; - // Put this last so that it gets destructed first. + arcana::cancellation_source m_cancellationSource{}; JsRuntimeScheduler m_runtimeScheduler; }; } diff --git a/Polyfills/Window/Source/Window.cpp b/Polyfills/Window/Source/Window.cpp index 607b2cb5c..486f5bb8f 100644 --- a/Polyfills/Window/Source/Window.cpp +++ b/Polyfills/Window/Source/Window.cpp @@ -78,6 +78,9 @@ namespace Babylon::Polyfills::Internal Window::~Window() { m_cancelSource.cancel(); + + // Wait for async operations to complete. + m_runtimeScheduler.Rundown(); } void Window::SetTimeout(const Napi::CallbackInfo& info) @@ -113,7 +116,8 @@ namespace Babylon::Polyfills::Internal Napi::FunctionReference function, std::chrono::system_clock::time_point whenToRun) { - arcana::make_task(m_runtimeScheduler.Get(), m_cancelSource, [this, thisRef = std::move(thisRef), function = std::move(function), whenToRun]() mutable + arcana::make_task(m_runtimeScheduler.Get(), m_cancelSource, + [this, thisRef = std::move(thisRef), function = std::move(function), whenToRun = std::move(whenToRun)]() mutable { if (std::chrono::system_clock::now() >= whenToRun) { diff --git a/Polyfills/Window/Source/Window.h b/Polyfills/Window/Source/Window.h index ecccc192e..0aaa1e299 100644 --- a/Polyfills/Window/Source/Window.h +++ b/Polyfills/Window/Source/Window.h @@ -22,11 +22,12 @@ namespace Babylon::Polyfills::Internal static void RemoveEventListener(const Napi::CallbackInfo& info); static Napi::Value GetDevicePixelRatio(const Napi::CallbackInfo& info); - void RecursiveWaitOrCall(Napi::Reference thisRef, Napi::FunctionReference function, std::chrono::system_clock::time_point whenToRun); + void RecursiveWaitOrCall( + Napi::Reference thisRef, + Napi::FunctionReference function, + std::chrono::system_clock::time_point whenToRun); arcana::cancellation_source m_cancelSource; - - // Put this last so that it gets destructed first. JsRuntimeScheduler m_runtimeScheduler; }; } diff --git a/Polyfills/XMLHttpRequest/Source/XMLHttpRequest.cpp b/Polyfills/XMLHttpRequest/Source/XMLHttpRequest.cpp index 22cc404fc..60ee21d64 100644 --- a/Polyfills/XMLHttpRequest/Source/XMLHttpRequest.cpp +++ b/Polyfills/XMLHttpRequest/Source/XMLHttpRequest.cpp @@ -3,82 +3,82 @@ #include #include -bool IsHexChar(const char& c) +namespace { - return ((c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f') || (c >= '0' && c <= '9')); -} - -std::string EncodePercent(const std::string& input) -{ - std::ostringstream encoded; - for (auto i = input.begin(), e = input.end(); i != e; ++i) + namespace ResponseType { - encoded << *i; - if (*i == '%') + constexpr const char* Text = "text"; + constexpr const char* ArrayBuffer = "arraybuffer"; + + UrlLib::UrlResponseType StringToEnum(const std::string& value) + { + if (value == Text) + return UrlLib::UrlResponseType::String; + if (value == ArrayBuffer) + return UrlLib::UrlResponseType::Buffer; + + throw std::runtime_error{"Unsupported response type: " + value}; + } + + const char* EnumToString(UrlLib::UrlResponseType value) { - if (std::distance(i, e) >= 2 && !(IsHexChar(*(i + 1)) && IsHexChar(*(i + 2)))) + switch (value) { - // If a percent character is not followed by two hex characters, we should encode it - encoded << "25"; + case UrlLib::UrlResponseType::String: + return Text; + case UrlLib::UrlResponseType::Buffer: + return ArrayBuffer; } + + throw std::runtime_error{"Invalid response type"}; } } - return encoded.str(); -} -namespace Babylon::Polyfills::Internal -{ - namespace + namespace MethodType { - namespace ResponseType - { - constexpr const char* Text = "text"; - constexpr const char* ArrayBuffer = "arraybuffer"; + constexpr const char* Get = "GET"; - UrlLib::UrlResponseType StringToEnum(const std::string& value) - { - if (value == Text) - return UrlLib::UrlResponseType::String; - if (value == ArrayBuffer) - return UrlLib::UrlResponseType::Buffer; + UrlLib::UrlMethod StringToEnum(const std::string& value) + { + if (value == Get) + return UrlLib::UrlMethod::Get; - throw std::runtime_error{"Unsupported response type: " + value}; - } + throw std::runtime_error{"Unsupported url method: " + value}; + } + } - const char* EnumToString(UrlLib::UrlResponseType value) - { - switch (value) - { - case UrlLib::UrlResponseType::String: - return Text; - case UrlLib::UrlResponseType::Buffer: - return ArrayBuffer; - } + namespace EventType + { + constexpr const char* ReadyStateChange = "readystatechange"; + constexpr const char* LoadEnd = "loadend"; + } - throw std::runtime_error{"Invalid response type"}; - } - } + bool IsHexChar(const char& c) + { + return ((c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f') || (c >= '0' && c <= '9')); + } - namespace MethodType + std::string EncodePercent(const std::string& input) + { + std::ostringstream encoded; + for (auto i = input.begin(), e = input.end(); i != e; ++i) { - constexpr const char* Get = "GET"; - - UrlLib::UrlMethod StringToEnum(const std::string& value) + encoded << *i; + if (*i == '%') { - if (value == Get) - return UrlLib::UrlMethod::Get; - - throw std::runtime_error{"Unsupported url method: " + value}; + if (std::distance(i, e) >= 2 && !(IsHexChar(*(i + 1)) && IsHexChar(*(i + 2)))) + { + // If a percent character is not followed by two hex characters, we should encode it + encoded << "25"; + } } } - - namespace EventType - { - constexpr const char* ReadyStateChange = "readystatechange"; - constexpr const char* LoadEnd = "loadend"; - } + return encoded.str(); } +} +namespace Babylon::Polyfills::Internal +{ void XMLHttpRequest::Initialize(Napi::Env env) { Napi::HandleScope scope{env}; @@ -121,6 +121,15 @@ namespace Babylon::Polyfills::Internal { } + XMLHttpRequest::~XMLHttpRequest() + { + m_request.Abort(); + m_cancellationSource.cancel(); + + // Wait for async operations to complete. + m_runtimeScheduler.Rundown(); + } + Napi::Value XMLHttpRequest::GetReadyState(const Napi::CallbackInfo&) { return Napi::Value::From(Env(), arcana::underlying_cast(m_readyState)); @@ -198,21 +207,26 @@ namespace Babylon::Polyfills::Internal void XMLHttpRequest::Abort(const Napi::CallbackInfo&) { m_request.Abort(); + m_cancellationSource.cancel(); } void XMLHttpRequest::Open(const Napi::CallbackInfo& info) { try { - // printfs for debugging CI, will be removed - auto inputURL{info[1].As()}; + auto method = info[0].As().Utf8Value(); + auto url = info[1].As().Utf8Value(); + // If the input URL contains any true % characters, encode them as %25 - auto encodedPercentURL{Napi::String::New(info.Env(), EncodePercent(inputURL.Utf8Value()))}; + auto encodedPercentURL = Napi::String::New(info.Env(), EncodePercent(url)); + // Decode the input URL to get a completely unencoded URL - auto decodedURL{info.Env().Global().Get("decodeURI").As().Call({encodedPercentURL})}; + auto decodedURL = info.Env().Global().Get("decodeURI").As().Call({encodedPercentURL}); + // Re-encode the URL to make sure that every illegal character is encoded - auto finalURL{info.Env().Global().Get("encodeURI").As().Call({decodedURL}).As()}; - m_request.Open(MethodType::StringToEnum(info[0].As().Utf8Value()), finalURL.Utf8Value()); + auto finalURL = info.Env().Global().Get("encodeURI").As().Call({decodedURL}).As().Utf8Value(); + + m_request.Open(MethodType::StringToEnum(method), finalURL); SetReadyState(ReadyState::Opened); } catch (const std::exception& e) @@ -235,7 +249,8 @@ namespace Babylon::Polyfills::Internal } m_request.SendAsync() - .then(m_runtimeScheduler.Get(), arcana::cancellation::none(), [this, thisRef = Napi::Persistent(info.This())](arcana::expected result) + .then(m_runtimeScheduler.Get(), m_cancellationSource, + [this, thisRef = Napi::Persistent(info.This())](arcana::expected result) { if (result.has_error()) { diff --git a/Polyfills/XMLHttpRequest/Source/XMLHttpRequest.h b/Polyfills/XMLHttpRequest/Source/XMLHttpRequest.h index 5f94c7717..5b88d18e9 100644 --- a/Polyfills/XMLHttpRequest/Source/XMLHttpRequest.h +++ b/Polyfills/XMLHttpRequest/Source/XMLHttpRequest.h @@ -15,6 +15,7 @@ namespace Babylon::Polyfills::Internal static void Initialize(Napi::Env env); explicit XMLHttpRequest(const Napi::CallbackInfo& info); + ~XMLHttpRequest(); private: enum class ReadyState @@ -41,8 +42,10 @@ namespace Babylon::Polyfills::Internal void RaiseEvent(const char* eventType); UrlLib::UrlRequest m_request{}; - JsRuntimeScheduler m_runtimeScheduler; ReadyState m_readyState{ReadyState::Unsent}; std::unordered_map> m_eventHandlerRefs; + + arcana::cancellation_source m_cancellationSource; + JsRuntimeScheduler m_runtimeScheduler; }; } From c99915305414c27a255d40afb9c7ff952adb403f Mon Sep 17 00:00:00 2001 From: Gary Hsu Date: Fri, 30 Sep 2022 13:11:05 -0700 Subject: [PATCH 03/21] Cannot throw in destructors --- Core/JsRuntime/Include/Babylon/JsRuntimeScheduler.h | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/Core/JsRuntime/Include/Babylon/JsRuntimeScheduler.h b/Core/JsRuntime/Include/Babylon/JsRuntimeScheduler.h index 8b41fd023..1ea5a275a 100644 --- a/Core/JsRuntime/Include/Babylon/JsRuntimeScheduler.h +++ b/Core/JsRuntime/Include/Babylon/JsRuntimeScheduler.h @@ -48,7 +48,7 @@ namespace Babylon // **IMPORTANT**: // 1. To prevent continuations from accessing freed memory, the destructor of the N-API class is expected to call // `Rundown()` which blocks execution until all of its schedulers are invoked. Failing to do so will result in - // an exception if there are outstanding schedulers not yet invoked. + // an assert if there are outstanding schedulers not yet invoked. // 2. The last continuation that accesses members of the N-API object, including the cancellation associated with // the continuation, must capture a persistent reference to the N-API object itself to prevent the GC from // collecting the N-API object during the asynchronous operation. Failing to do so will result in a hang @@ -68,10 +68,7 @@ namespace Babylon ~JsRuntimeScheduler() { - if (m_count > 0) - { - throw std::runtime_error{"Schedulers for the JavaScript thread are not yet invoked"}; - } + assert(m_count == 0 && "Schedulers for the JavaScript thread are not yet invoked"); } // Wait until all of the schedulers are invoked. From 9d1e098dafe17929488b4f2206b884335bed1b15 Mon Sep 17 00:00:00 2001 From: Gary Hsu Date: Wed, 26 Oct 2022 14:24:28 -0700 Subject: [PATCH 04/21] Fix merge issues --- Polyfills/Window/Source/TimeoutDispatcher.cpp | 8 ++++---- Polyfills/Window/Source/TimeoutDispatcher.h | 6 +++--- Polyfills/Window/Source/Window.cpp | 7 +++---- Polyfills/Window/Source/Window.h | 2 +- 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/Polyfills/Window/Source/TimeoutDispatcher.cpp b/Polyfills/Window/Source/TimeoutDispatcher.cpp index e84266d78..654976db9 100644 --- a/Polyfills/Window/Source/TimeoutDispatcher.cpp +++ b/Polyfills/Window/Source/TimeoutDispatcher.cpp @@ -34,8 +34,8 @@ namespace Babylon::Polyfills::Internal Timeout(Timeout&&) = delete; }; - TimeoutDispatcher::TimeoutDispatcher(Babylon::JsRuntime& runtime) - : m_runtime{runtime} + TimeoutDispatcher::TimeoutDispatcher(Babylon::JsRuntimeScheduler& runtimeScheduler) + : m_runtimeScheduler{runtimeScheduler} , m_thread{std::thread{&TimeoutDispatcher::ThreadFunction, this}} { } @@ -71,7 +71,7 @@ namespace Babylon::Polyfills::Internal if (time <= earliestTime) { - m_runtime.Dispatch([this](Napi::Env) { + m_runtimeScheduler.Get()([this]() { m_condVariable.notify_one(); }); } @@ -158,7 +158,7 @@ namespace Babylon::Polyfills::Internal { if (function) { - m_runtime.Dispatch([function = std::move(function)](Napi::Env) + m_runtimeScheduler.Get()([function = std::move(function)]() { function->Call({}); }); } } diff --git a/Polyfills/Window/Source/TimeoutDispatcher.h b/Polyfills/Window/Source/TimeoutDispatcher.h index 9be2dbd5b..1911c0c53 100644 --- a/Polyfills/Window/Source/TimeoutDispatcher.h +++ b/Polyfills/Window/Source/TimeoutDispatcher.h @@ -1,6 +1,6 @@ #pragma once -#include +#include #include #include @@ -19,7 +19,7 @@ namespace Babylon::Polyfills::Internal struct Timeout; public: - TimeoutDispatcher(Babylon::JsRuntime& runtime); + TimeoutDispatcher(Babylon::JsRuntimeScheduler& runtimeScheduler); ~TimeoutDispatcher(); TimeoutId Dispatch(std::shared_ptr function, std::chrono::milliseconds delay); @@ -32,7 +32,7 @@ namespace Babylon::Polyfills::Internal void ThreadFunction(); void CallFunction(std::shared_ptr function); - Babylon::JsRuntime& m_runtime; + Babylon::JsRuntimeScheduler& m_runtimeScheduler; std::mutex m_mutex{}; std::condition_variable m_condVariable{}; TimeoutId m_lastTimeoutId{0}; diff --git a/Polyfills/Window/Source/Window.cpp b/Polyfills/Window/Source/Window.cpp index 5ca67d58e..8279a67a1 100644 --- a/Polyfills/Window/Source/Window.cpp +++ b/Polyfills/Window/Source/Window.cpp @@ -72,7 +72,7 @@ namespace Babylon::Polyfills::Internal Window::Window(const Napi::CallbackInfo& info) : Napi::ObjectWrap{info} , m_runtimeScheduler{JsRuntime::GetFromJavaScript(info.Env())} - , m_timeoutDispatcher{m_runtime} + , m_timeoutDispatcher{m_runtimeScheduler} { } @@ -91,10 +91,9 @@ namespace Babylon::Polyfills::Internal ? std::make_shared(Napi::Persistent(info[0].As())) : std::shared_ptr{}; - window.RecursiveWaitOrCall(Napi::Persistent(info.This()), std::move(function), std::chrono::system_clock::now() + milliseconds); auto delay = std::chrono::milliseconds{info[1].ToNumber().Int32Value()}; - return Napi::Value::From(info.Env(), window.m_timeoutDispatcher->Dispatch(function, delay)); + return Napi::Value::From(info.Env(), window.m_timeoutDispatcher.Dispatch(function, delay)); } void Window::ClearTimeout(const Napi::CallbackInfo& info) @@ -104,7 +103,7 @@ namespace Babylon::Polyfills::Internal { auto timeoutId = arg.As().Int32Value(); auto& window = *static_cast(info.Data()); - window.m_timeoutDispatcher->Clear(timeoutId); + window.m_timeoutDispatcher.Clear(timeoutId); } } diff --git a/Polyfills/Window/Source/Window.h b/Polyfills/Window/Source/Window.h index 4dda834c1..0fcca3db2 100644 --- a/Polyfills/Window/Source/Window.h +++ b/Polyfills/Window/Source/Window.h @@ -27,6 +27,6 @@ namespace Babylon::Polyfills::Internal arcana::cancellation_source m_cancelSource; JsRuntimeScheduler m_runtimeScheduler; - std::optional m_timeoutDispatcher; + TimeoutDispatcher m_timeoutDispatcher; }; } From 124def435283b462f8cf71a7036bec63a5d2b247 Mon Sep 17 00:00:00 2001 From: Gary Hsu Date: Thu, 27 Oct 2022 10:36:38 -0700 Subject: [PATCH 05/21] Merge with new timeout code with work queue fixes --- Core/AppRuntime/Source/AppRuntime.cpp | 9 +++-- Core/AppRuntime/Source/WorkQueue.cpp | 6 ++- Core/AppRuntime/Source/WorkQueue.h | 4 +- Core/JsRuntime/Include/Babylon/JsRuntime.h | 12 +++++- .../Include/Babylon/JsRuntimeScheduler.h | 27 ++++++++++--- Core/JsRuntime/Source/JsRuntime.cpp | 21 ++++++++-- Plugins/NativeEngine/Source/NativeEngine.cpp | 39 +++++++++---------- Polyfills/Window/Source/TimeoutDispatcher.cpp | 30 ++++++++------ Polyfills/Window/Source/TimeoutDispatcher.h | 15 +++---- Polyfills/Window/Source/Window.cpp | 11 +----- Polyfills/Window/Source/Window.h | 3 -- 11 files changed, 106 insertions(+), 71 deletions(-) diff --git a/Core/AppRuntime/Source/AppRuntime.cpp b/Core/AppRuntime/Source/AppRuntime.cpp index c49ed905e..3702d4b6a 100644 --- a/Core/AppRuntime/Source/AppRuntime.cpp +++ b/Core/AppRuntime/Source/AppRuntime.cpp @@ -12,7 +12,8 @@ namespace Babylon : m_workQueue{std::make_unique([this] { RunPlatformTier(); })} , m_unhandledExceptionHandler{unhandledExceptionHandler} { - Dispatch([this](Napi::Env env) { + Dispatch([this](Napi::Env env) + { JsRuntime::CreateForJavaScript(env, [this](auto func) { Dispatch(std::move(func)); }); }); } @@ -45,8 +46,10 @@ namespace Babylon void AppRuntime::Dispatch(Dispatchable func) { - m_workQueue->Append([this, func{std::move(func)}](Napi::Env env) mutable { - Execute([this, env, func{std::move(func)}]() mutable { + m_workQueue->Append([this, func{std::move(func)}](Napi::Env env) mutable + { + Execute([this, env, func{std::move(func)}]() mutable + { try { func(env); diff --git a/Core/AppRuntime/Source/WorkQueue.cpp b/Core/AppRuntime/Source/WorkQueue.cpp index 2fa9bbc62..b4d065d4c 100644 --- a/Core/AppRuntime/Source/WorkQueue.cpp +++ b/Core/AppRuntime/Source/WorkQueue.cpp @@ -48,8 +48,10 @@ namespace Babylon m_dispatcher.blocking_tick(m_cancellationSource); } - // There should not be any outstanding work during the shutdown sequence - // which should be the only way exit the while loop above. + // Drain the queue to complete work dispatched after cancellation. + m_dispatcher.tick(arcana::cancellation::none()); + + // There should no longer be any outstanding work once the queue is drained. assert(m_dispatcher.empty()); } } diff --git a/Core/AppRuntime/Source/WorkQueue.h b/Core/AppRuntime/Source/WorkQueue.h index 914f6dbce..4e404bcff 100644 --- a/Core/AppRuntime/Source/WorkQueue.h +++ b/Core/AppRuntime/Source/WorkQueue.h @@ -22,7 +22,7 @@ namespace Babylon if (m_cancellationSource.cancelled()) { // There is likely a coding error if this exception is thrown. - throw std::runtime_error{"Cannot append to the work queue after it is canceled"}; + throw std::runtime_error{"Cannot append to the work queue while shutting down"}; } // Manual dispatcher queueing requires a copyable CallableT, we use a shared pointer trick to make a @@ -57,6 +57,6 @@ namespace Babylon arcana::cancellation_source m_cancellationSource{}; arcana::manual_dispatcher<128> m_dispatcher{}; - std::thread m_thread; + std::thread m_thread{}; }; } diff --git a/Core/JsRuntime/Include/Babylon/JsRuntime.h b/Core/JsRuntime/Include/Babylon/JsRuntime.h index 9c9460490..1c6b4cd1f 100644 --- a/Core/JsRuntime/Include/Babylon/JsRuntime.h +++ b/Core/JsRuntime/Include/Babylon/JsRuntime.h @@ -42,12 +42,20 @@ namespace Babylon // Gets the JsRuntime from the given N-API environment. static JsRuntime& GetFromJavaScript(Napi::Env); + struct IDisposingCallback + { + virtual void Disposing() = 0; + }; + // Notifies the JsRuntime that the JavaScript environment will begin shutting down. // Calling this function will signal callbacks registered with RegisterDisposing. static void NotifyDisposing(JsRuntime&); // Registers a callback for when the JavaScript environment will begin shutting down. - static void RegisterDisposing(JsRuntime&, std::function); + static void RegisterDisposing(JsRuntime&, IDisposingCallback*); + + // Unregisters a callback for when the JavaScript environment will begin shutting down. + static void UnregisterDisposing(JsRuntime&, IDisposingCallback*); // Dispatches work onto the JavaScript thread and provides access to the N-API // environment. @@ -58,6 +66,6 @@ namespace Babylon std::mutex m_mutex{}; DispatchFunctionT m_dispatchFunction{}; - std::vector> m_disposingCallbacks{}; + std::vector m_disposingCallbacks{}; }; } diff --git a/Core/JsRuntime/Include/Babylon/JsRuntimeScheduler.h b/Core/JsRuntime/Include/Babylon/JsRuntimeScheduler.h index 1ea5a275a..267746d25 100644 --- a/Core/JsRuntime/Include/Babylon/JsRuntimeScheduler.h +++ b/Core/JsRuntime/Include/Babylon/JsRuntimeScheduler.h @@ -53,22 +53,26 @@ namespace Babylon // the continuation, must capture a persistent reference to the N-API object itself to prevent the GC from // collecting the N-API object during the asynchronous operation. Failing to do so will result in a hang // when `Rundown()` is called in the N-API class destructor. - class JsRuntimeScheduler + class JsRuntimeScheduler : public JsRuntime::IDisposingCallback { public: explicit JsRuntimeScheduler(JsRuntime& runtime) : m_runtime{&runtime} , m_scheduler{*this} { - JsRuntime::RegisterDisposing(*m_runtime, [this]() - { - m_runtime = nullptr; - }); + std::scoped_lock lock{m_mutex}; + JsRuntime::RegisterDisposing(*m_runtime, this); } ~JsRuntimeScheduler() { assert(m_count == 0 && "Schedulers for the JavaScript thread are not yet invoked"); + + std::scoped_lock lock{m_mutex}; + if (m_runtime != nullptr) + { + JsRuntime::UnregisterDisposing(*m_runtime, this); + } } // Wait until all of the schedulers are invoked. @@ -88,6 +92,12 @@ namespace Babylon } private: + void Disposing() override + { + std::scoped_lock lock{m_mutex}; + m_runtime = nullptr; + } + class SchedulerImpl { public: @@ -108,6 +118,8 @@ namespace Babylon template void Dispatch(CallableT&& callable) { + std::scoped_lock lock{m_mutex}; + if (m_runtime != nullptr) { m_runtime->Dispatch([callable{std::forward(callable)}](Napi::Env) @@ -123,7 +135,10 @@ namespace Babylon --m_count; } - JsRuntime* m_runtime; + mutable std::mutex m_mutex{}; + JsRuntime* m_runtime{}; + std::function m_disposingCallback{}; + SchedulerImpl m_scheduler; std::atomic m_count{0}; arcana::manual_dispatcher<128> m_disposingDispatcher{}; diff --git a/Core/JsRuntime/Source/JsRuntime.cpp b/Core/JsRuntime/Source/JsRuntime.cpp index f9dedf94b..0a2c9d570 100644 --- a/Core/JsRuntime/Source/JsRuntime.cpp +++ b/Core/JsRuntime/Source/JsRuntime.cpp @@ -1,4 +1,5 @@ #include "JsRuntime.h" +#include namespace Babylon { @@ -43,15 +44,27 @@ namespace Babylon void JsRuntime::NotifyDisposing(JsRuntime& runtime) { auto callbacks = std::move(runtime.m_disposingCallbacks); - for (const auto& callback : callbacks) + for (auto* callback : callbacks) { - callback(); + callback->Disposing(); } } - void JsRuntime::RegisterDisposing(JsRuntime& runtime, std::function callback) + void JsRuntime::RegisterDisposing(JsRuntime& runtime, IDisposingCallback* callback) { - runtime.m_disposingCallbacks.push_back(std::move(callback)); + auto& callbacks = runtime.m_disposingCallbacks; + assert(std::find(callbacks.begin(), callbacks.end(), callback) == callbacks.end()); + callbacks.push_back(callback); + } + + void JsRuntime::UnregisterDisposing(JsRuntime& runtime, IDisposingCallback* callback) + { + auto& callbacks = runtime.m_disposingCallbacks; + auto it = std::find(callbacks.begin(), callbacks.end(), callback); + if (it != callbacks.end()) + { + callbacks.erase(it); + } } void JsRuntime::Dispatch(std::function function) diff --git a/Plugins/NativeEngine/Source/NativeEngine.cpp b/Plugins/NativeEngine/Source/NativeEngine.cpp index 63fa28cd4..ebbe7a35f 100644 --- a/Plugins/NativeEngine/Source/NativeEngine.cpp +++ b/Plugins/NativeEngine/Source/NativeEngine.cpp @@ -1067,26 +1067,25 @@ namespace Babylon }); } - void NativeEngine::CopyTexture(const Napi::CallbackInfo& /*info*/) - { - //const auto textureDestination = info[0].As>().Get(); - //const auto textureSource = info[1].As>().Get(); - - // TODO: this code makes no sense - //arcana::make_task(m_graphicsUpdate.Scheduler(), m_cancellationSource, [this, textureDestination, textureSource]() - //{ - // return arcana::make_task(m_runtimeScheduler.Get(), m_cancellationSource, [this, textureDestination, textureSource, updateToken = m_graphicsUpdate.GetUpdateToken()]() mutable - // { - // bgfx::Encoder* encoder = updateToken.GetEncoder(); - // GetBoundFrameBuffer(*encoder).Blit(*encoder, textureDestination->Handle(), 0, 0, textureSource->Handle()); - // }).then(arcana::inline_scheduler, m_cancellationSource, [this, thisRef = Napi::Persistent(info.Env())](const arcana::expected& result) - // { - // if (result.has_error()) - // { - // Napi::Error::New(Env(), result.error()).ThrowAsJavaScriptException(); - // } - // }); - //}); + void NativeEngine::CopyTexture(const Napi::CallbackInfo& info) + { + const auto textureDestination = info[0].As>().Get(); + const auto textureSource = info[1].As>().Get(); + + arcana::make_task(m_update.Scheduler(), m_cancellationSource, [this, textureDestination, textureSource]() mutable + { + return arcana::make_task(m_runtimeScheduler.Get(), m_cancellationSource, [this, textureDestination, textureSource, updateToken = m_update.GetUpdateToken()]() mutable + { + bgfx::Encoder* encoder = updateToken.GetEncoder(); + GetBoundFrameBuffer(*encoder).Blit(*encoder, textureDestination->Handle(), 0, 0, textureSource->Handle()); + }).then(arcana::inline_scheduler, m_cancellationSource, [this](const arcana::expected& result) + { + if (result.has_error()) + { + Napi::Error::New(Env(), result.error()).ThrowAsJavaScriptException(); + } + }); + }); } void NativeEngine::LoadRawTexture(const Napi::CallbackInfo& info) diff --git a/Polyfills/Window/Source/TimeoutDispatcher.cpp b/Polyfills/Window/Source/TimeoutDispatcher.cpp index 654976db9..deb4b437c 100644 --- a/Polyfills/Window/Source/TimeoutDispatcher.cpp +++ b/Polyfills/Window/Source/TimeoutDispatcher.cpp @@ -34,8 +34,8 @@ namespace Babylon::Polyfills::Internal Timeout(Timeout&&) = delete; }; - TimeoutDispatcher::TimeoutDispatcher(Babylon::JsRuntimeScheduler& runtimeScheduler) - : m_runtimeScheduler{runtimeScheduler} + TimeoutDispatcher::TimeoutDispatcher(Babylon::JsRuntime& runtime) + : m_runtimeScheduler{runtime} , m_thread{std::thread{&TimeoutDispatcher::ThreadFunction, this}} { } @@ -43,14 +43,17 @@ namespace Babylon::Polyfills::Internal TimeoutDispatcher::~TimeoutDispatcher() { { - std::unique_lock lk{m_mutex}; + std::unique_lock lock{m_mutex}; m_idMap.clear(); m_timeMap.clear(); } - m_shutdown = true; + m_cancellationSource.cancel(); m_condVariable.notify_one(); m_thread.join(); + + // Wait for async operations to complete. + m_runtimeScheduler.Rundown(); } TimeoutDispatcher::TimeoutId TimeoutDispatcher::Dispatch(std::shared_ptr function, std::chrono::milliseconds delay) @@ -71,7 +74,8 @@ namespace Babylon::Polyfills::Internal if (time <= earliestTime) { - m_runtimeScheduler.Get()([this]() { + m_runtimeScheduler.Get()([this]() + { m_condVariable.notify_one(); }); } @@ -123,11 +127,11 @@ namespace Babylon::Polyfills::Internal void TimeoutDispatcher::ThreadFunction() { - while (!m_shutdown) + while (!m_cancellationSource.cancelled()) { - std::unique_lock lk{m_mutex}; - TimePoint nextTimePoint{}; + std::unique_lock lock{m_mutex}; + TimePoint nextTimePoint{}; while (!m_timeMap.empty()) { nextTimePoint = m_timeMap.begin()->second->time; @@ -136,7 +140,7 @@ namespace Babylon::Polyfills::Internal break; } - m_condVariable.wait_until(lk, nextTimePoint); + m_condVariable.wait_until(lock, nextTimePoint); } while (!m_timeMap.empty() && m_timeMap.begin()->second->time == nextTimePoint) @@ -147,9 +151,9 @@ namespace Babylon::Polyfills::Internal m_idMap.erase(timeout->id); } - while (!m_shutdown && m_timeMap.empty()) + while (!m_cancellationSource.cancelled() && m_timeMap.empty()) { - m_condVariable.wait(lk); + m_condVariable.wait(lock); } } } @@ -159,7 +163,9 @@ namespace Babylon::Polyfills::Internal if (function) { m_runtimeScheduler.Get()([function = std::move(function)]() - { function->Call({}); }); + { + function->Call({}); + }); } } } diff --git a/Polyfills/Window/Source/TimeoutDispatcher.h b/Polyfills/Window/Source/TimeoutDispatcher.h index 1911c0c53..e20509cb2 100644 --- a/Polyfills/Window/Source/TimeoutDispatcher.h +++ b/Polyfills/Window/Source/TimeoutDispatcher.h @@ -19,7 +19,7 @@ namespace Babylon::Polyfills::Internal struct Timeout; public: - TimeoutDispatcher(Babylon::JsRuntimeScheduler& runtimeScheduler); + TimeoutDispatcher(Babylon::JsRuntime& runtime); ~TimeoutDispatcher(); TimeoutId Dispatch(std::shared_ptr function, std::chrono::milliseconds delay); @@ -32,13 +32,14 @@ namespace Babylon::Polyfills::Internal void ThreadFunction(); void CallFunction(std::shared_ptr function); - Babylon::JsRuntimeScheduler& m_runtimeScheduler; - std::mutex m_mutex{}; + Babylon::JsRuntimeScheduler m_runtimeScheduler; + + mutable std::mutex m_mutex{}; std::condition_variable m_condVariable{}; TimeoutId m_lastTimeoutId{0}; - std::unordered_map> m_idMap; - std::multimap m_timeMap; - std::atomic m_shutdown{false}; - std::thread m_thread; + std::unordered_map> m_idMap{}; + std::multimap m_timeMap{}; + arcana::cancellation_source m_cancellationSource{}; + std::thread m_thread{}; }; } diff --git a/Polyfills/Window/Source/Window.cpp b/Polyfills/Window/Source/Window.cpp index 8279a67a1..1b779a052 100644 --- a/Polyfills/Window/Source/Window.cpp +++ b/Polyfills/Window/Source/Window.cpp @@ -71,19 +71,10 @@ namespace Babylon::Polyfills::Internal Window::Window(const Napi::CallbackInfo& info) : Napi::ObjectWrap{info} - , m_runtimeScheduler{JsRuntime::GetFromJavaScript(info.Env())} - , m_timeoutDispatcher{m_runtimeScheduler} + , m_timeoutDispatcher{JsRuntime::GetFromJavaScript(info.Env())} { } - Window::~Window() - { - m_cancelSource.cancel(); - - // Wait for async operations to complete. - m_runtimeScheduler.Rundown(); - } - Napi::Value Window::SetTimeout(const Napi::CallbackInfo& info) { auto& window = *static_cast(info.Data()); diff --git a/Polyfills/Window/Source/Window.h b/Polyfills/Window/Source/Window.h index 0fcca3db2..45b3acf54 100644 --- a/Polyfills/Window/Source/Window.h +++ b/Polyfills/Window/Source/Window.h @@ -15,7 +15,6 @@ namespace Babylon::Polyfills::Internal static Window& GetFromJavaScript(Napi::Env); Window(const Napi::CallbackInfo& info); - ~Window(); private: static Napi::Value SetTimeout(const Napi::CallbackInfo& info); @@ -25,8 +24,6 @@ namespace Babylon::Polyfills::Internal static void RemoveEventListener(const Napi::CallbackInfo& info); static Napi::Value GetDevicePixelRatio(const Napi::CallbackInfo& info); - arcana::cancellation_source m_cancelSource; - JsRuntimeScheduler m_runtimeScheduler; TimeoutDispatcher m_timeoutDispatcher; }; } From 309c4d972329e64b0c5811713bc0c9fe94670c9d Mon Sep 17 00:00:00 2001 From: Gary Hsu Date: Thu, 27 Oct 2022 17:16:42 -0700 Subject: [PATCH 06/21] Fix Canvas --- Polyfills/Canvas/Source/Canvas.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Polyfills/Canvas/Source/Canvas.cpp b/Polyfills/Canvas/Source/Canvas.cpp index 99542c468..69cdda0b1 100644 --- a/Polyfills/Canvas/Source/Canvas.cpp +++ b/Polyfills/Canvas/Source/Canvas.cpp @@ -54,26 +54,27 @@ namespace Babylon::Polyfills::Internal Napi::Value NativeCanvas::LoadTTFAsync(const Napi::CallbackInfo& info) { - const auto fontName = info[0].As().Utf8Value(); + auto fontName = info[0].As().Utf8Value(); const auto buffer = info[1].As(); std::vector fontBuffer(buffer.ByteLength()); memcpy(fontBuffer.data(), (uint8_t*)buffer.Data(), buffer.ByteLength()); auto& runtime = JsRuntime::GetFromJavaScript(info.Env()); auto deferred = Napi::Promise::Deferred::New(info.Env()); + auto promise = deferred.Promise(); auto& deviceContext = Graphics::DeviceContext::GetFromJavaScript(info.Env()); auto update = deviceContext.GetUpdate("update"); - arcana::make_task(update.Scheduler(), arcana::cancellation::none(), [fontName = std::move(fontName), fontData = std::move(fontBuffer), &runtime, deferred]() + arcana::make_task(update.Scheduler(), arcana::cancellation::none(), [fontName = std::move(fontName), fontData = std::move(fontBuffer), &runtime, deferred = std::move(deferred)]() mutable { fontsInfos[fontName] = fontData; - runtime.Dispatch([deferred](Napi::Env env) + runtime.Dispatch([deferred = std::move(deferred)](Napi::Env env) { deferred.Resolve(env.Undefined()); }); }); - return deferred.Promise(); + return promise; } Napi::Value NativeCanvas::GetContext(const Napi::CallbackInfo& info) From ad88d30bd748272155e6f14b092db37d58e6a806 Mon Sep 17 00:00:00 2001 From: Gary Hsu Date: Fri, 28 Oct 2022 09:41:53 -0700 Subject: [PATCH 07/21] Fix Android build --- Polyfills/Window/Source/TimeoutDispatcher.cpp | 24 ++---------------- Polyfills/Window/Source/TimeoutDispatcher.h | 25 ++++++++++++++++--- Polyfills/Window/Source/Window.h | 1 - 3 files changed, 24 insertions(+), 26 deletions(-) diff --git a/Polyfills/Window/Source/TimeoutDispatcher.cpp b/Polyfills/Window/Source/TimeoutDispatcher.cpp index deb4b437c..fb4bfd863 100644 --- a/Polyfills/Window/Source/TimeoutDispatcher.cpp +++ b/Polyfills/Window/Source/TimeoutDispatcher.cpp @@ -14,26 +14,6 @@ namespace Babylon::Polyfills::Internal } } - struct TimeoutDispatcher::Timeout - { - TimeoutId id; - - // Make this non-shared when JsRuntime::Dispatch supports it. - std::shared_ptr function; - - TimePoint time; - - Timeout(TimeoutId id, std::shared_ptr function, TimePoint time) - : id{id} - , function{std::move(function)} - , time{time} - { - } - - Timeout(const Timeout&) = delete; - Timeout(Timeout&&) = delete; - }; - TimeoutDispatcher::TimeoutDispatcher(Babylon::JsRuntime& runtime) : m_runtimeScheduler{runtime} , m_thread{std::thread{&TimeoutDispatcher::ThreadFunction, this}} @@ -56,7 +36,7 @@ namespace Babylon::Polyfills::Internal m_runtimeScheduler.Rundown(); } - TimeoutDispatcher::TimeoutId TimeoutDispatcher::Dispatch(std::shared_ptr function, std::chrono::milliseconds delay) + TimeoutId TimeoutDispatcher::Dispatch(std::shared_ptr function, std::chrono::milliseconds delay) { if (delay.count() < 0) { @@ -107,7 +87,7 @@ namespace Babylon::Polyfills::Internal } } - TimeoutDispatcher::TimeoutId TimeoutDispatcher::NextTimeoutId() + TimeoutId TimeoutDispatcher::NextTimeoutId() { while (true) { diff --git a/Polyfills/Window/Source/TimeoutDispatcher.h b/Polyfills/Window/Source/TimeoutDispatcher.h index e20509cb2..c92a4da90 100644 --- a/Polyfills/Window/Source/TimeoutDispatcher.h +++ b/Polyfills/Window/Source/TimeoutDispatcher.h @@ -13,11 +13,10 @@ namespace Babylon::Polyfills::Internal { + using TimeoutId = int32_t; + class TimeoutDispatcher { - using TimeoutId = int32_t; - struct Timeout; - public: TimeoutDispatcher(Babylon::JsRuntime& runtime); ~TimeoutDispatcher(); @@ -28,6 +27,26 @@ namespace Babylon::Polyfills::Internal private: using TimePoint = std::chrono::time_point; + struct Timeout + { + TimeoutId id; + + // Make this non-shared when JsRuntime::Dispatch supports it. + std::shared_ptr function; + + TimePoint time; + + Timeout(TimeoutId id, std::shared_ptr function, TimePoint time) + : id{id} + , function{std::move(function)} + , time{time} + { + } + + Timeout(const Timeout&) = delete; + Timeout(Timeout&&) = delete; + }; + TimeoutId NextTimeoutId(); void ThreadFunction(); void CallFunction(std::shared_ptr function); diff --git a/Polyfills/Window/Source/Window.h b/Polyfills/Window/Source/Window.h index 45b3acf54..1690c0e70 100644 --- a/Polyfills/Window/Source/Window.h +++ b/Polyfills/Window/Source/Window.h @@ -2,7 +2,6 @@ #include #include "TimeoutDispatcher.h" -#include namespace Babylon::Polyfills::Internal { From 9322360331548ec51077a987c07924c9eb746f81 Mon Sep 17 00:00:00 2001 From: Gary Hsu Date: Tue, 4 Apr 2023 16:32:12 -0700 Subject: [PATCH 08/21] Update arcana.cpp --- Dependencies/arcana.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dependencies/arcana.cpp b/Dependencies/arcana.cpp index 10d167ffe..e9f2fb8bd 160000 --- a/Dependencies/arcana.cpp +++ b/Dependencies/arcana.cpp @@ -1 +1 @@ -Subproject commit 10d167ffe0f86b2ddaccd93fd78ff2240cbb0c49 +Subproject commit e9f2fb8bddd3eba0928ee4254dbe1c341e7bda97 From b4a3c7321db90adb719bcdb661f38e8da64270d7 Mon Sep 17 00:00:00 2001 From: Gary Hsu Date: Tue, 4 Apr 2023 18:00:39 -0700 Subject: [PATCH 09/21] Temp fixes for MediaStream --- Plugins/NativeCamera/Source/MediaDevices.cpp | 3 +-- Plugins/NativeCamera/Source/MediaStream.cpp | 18 +++++++++++------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/Plugins/NativeCamera/Source/MediaDevices.cpp b/Plugins/NativeCamera/Source/MediaDevices.cpp index ef631d7c4..096b74897 100644 --- a/Plugins/NativeCamera/Source/MediaDevices.cpp +++ b/Plugins/NativeCamera/Source/MediaDevices.cpp @@ -38,8 +38,7 @@ namespace Babylon::Plugins::Internal } } - auto runtimeScheduler{std::make_unique(JsRuntime::GetFromJavaScript(env))}; - MediaStream::NewAsync(env, videoConstraints).then(runtimeScheduler->Get(), arcana::cancellation::none(), [runtimeScheduler = std::move(runtimeScheduler), env, deferred](const arcana::expected& result) { + MediaStream::NewAsync(env, videoConstraints).then(arcana::inline_scheduler, arcana::cancellation::none(), [env, deferred](const arcana::expected& result) { if (result.has_error()) { deferred.Reject(Napi::Error::New(env, result.error()).Value()); diff --git a/Plugins/NativeCamera/Source/MediaStream.cpp b/Plugins/NativeCamera/Source/MediaStream.cpp index 0ccb62438..f7ea75756 100644 --- a/Plugins/NativeCamera/Source/MediaStream.cpp +++ b/Plugins/NativeCamera/Source/MediaStream.cpp @@ -58,13 +58,17 @@ namespace Babylon::Plugins MediaStream::~MediaStream() { - if (m_cameraDevice != nullptr) - { - // The cameraDevice should be destroyed on the JS thread as it may need to access main thread resources - // move ownership of the cameraDevice to a lambda and dispatch it with the runtimeScheduler so the destructor - // is called from that thread. - m_runtimeScheduler.Get()([cameraDevice = std::move(m_cameraDevice)]() {}); - } + // TODO: Is this still necessary? +// if (m_cameraDevice != nullptr) +// { +// // The cameraDevice should be destroyed on the JS thread as it may need to access main thread resources +// // move ownership of the cameraDevice to a lambda and dispatch it with the runtimeScheduler so the destructor +// // is called from that thread. +// m_runtimeScheduler.Get()([cameraDevice = std::move(m_cameraDevice)]() {}); +// } + + // Wait for async operations to complete. + m_runtimeScheduler.Rundown(); } Napi::Value MediaStream::GetVideoTracks(const Napi::CallbackInfo& info) From 9305ca8185afdb3569e0e22833bf133994828649 Mon Sep 17 00:00:00 2001 From: Gary Hsu Date: Wed, 19 Apr 2023 13:58:10 -0700 Subject: [PATCH 10/21] Add missing std::forward calls for perfect forwarding If the shared_ptr is copied, the final release may end up on a different thread. Adding std::forward will ensure that the lambda is moved and not copied which will prevent multiple threads from owning the shared_ptr. The shared_ptr is being used as a hack anyways. The correct fix is to not use shared_ptr and make Dispatch use the Dispatchable class from AppRuntime or if we can use C++23 someday, use std::move_only_function. --- Core/JsRuntime/Include/Babylon/JsRuntimeScheduler.h | 4 ++-- Polyfills/Window/Source/TimeoutDispatcher.cpp | 6 ++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/Core/JsRuntime/Include/Babylon/JsRuntimeScheduler.h b/Core/JsRuntime/Include/Babylon/JsRuntimeScheduler.h index 267746d25..b1131c531 100644 --- a/Core/JsRuntime/Include/Babylon/JsRuntimeScheduler.h +++ b/Core/JsRuntime/Include/Babylon/JsRuntimeScheduler.h @@ -108,7 +108,7 @@ namespace Babylon template void operator()(CallableT&& callable) const { - m_parent.Dispatch(callable); + m_parent.Dispatch(std::forward(callable)); } private: @@ -129,7 +129,7 @@ namespace Babylon } else { - m_disposingDispatcher(callable); + m_disposingDispatcher(std::forward(callable)); } --m_count; diff --git a/Polyfills/Window/Source/TimeoutDispatcher.cpp b/Polyfills/Window/Source/TimeoutDispatcher.cpp index 425fd46a8..524cd835a 100644 --- a/Polyfills/Window/Source/TimeoutDispatcher.cpp +++ b/Polyfills/Window/Source/TimeoutDispatcher.cpp @@ -53,8 +53,7 @@ namespace Babylon::Polyfills::Internal if (time <= earliestTime) { - m_runtimeScheduler.Get()([this]() - { + m_runtimeScheduler.Get()([this]() { m_condVariable.notify_one(); }); } @@ -142,8 +141,7 @@ namespace Babylon::Polyfills::Internal { if (function) { - m_runtimeScheduler.Get()([function = std::move(function)]() - { + m_runtimeScheduler.Get()([function = std::move(function)]() { function->Call({}); }); } From df246e20e76df7c3e2968afabea87861c34834f5 Mon Sep 17 00:00:00 2001 From: Gary Hsu Date: Fri, 28 Apr 2023 11:04:32 -0700 Subject: [PATCH 11/21] Work queue shutdown fixes --- Core/AppRuntime/Source/WorkQueue.cpp | 8 +++++- Core/AppRuntime/Source/WorkQueue.h | 41 ++++++++++++++++++---------- 2 files changed, 33 insertions(+), 16 deletions(-) diff --git a/Core/AppRuntime/Source/WorkQueue.cpp b/Core/AppRuntime/Source/WorkQueue.cpp index b4d065d4c..eb525bbcc 100644 --- a/Core/AppRuntime/Source/WorkQueue.cpp +++ b/Core/AppRuntime/Source/WorkQueue.cpp @@ -40,9 +40,10 @@ namespace Babylon void WorkQueue::Run(Napi::Env env) { - m_env = std::make_optional(env); m_dispatcher.set_affinity(std::this_thread::get_id()); + m_env = std::make_optional(env); + while (!m_cancellationSource.cancelled()) { m_dispatcher.blocking_tick(m_cancellationSource); @@ -53,5 +54,10 @@ namespace Babylon // There should no longer be any outstanding work once the queue is drained. assert(m_dispatcher.empty()); + + // Clear the shutdown queue to make sure the callables are destroyed on this thread. + m_shutdownQueue.clear(); + + m_env.reset(); } } diff --git a/Core/AppRuntime/Source/WorkQueue.h b/Core/AppRuntime/Source/WorkQueue.h index dbd68da71..3795cfff8 100644 --- a/Core/AppRuntime/Source/WorkQueue.h +++ b/Core/AppRuntime/Source/WorkQueue.h @@ -19,21 +19,33 @@ namespace Babylon template void Append(CallableT callable) { - if (m_cancellationSource.cancelled()) - { - // There is likely a coding error if this exception is thrown. - throw std::runtime_error{"Cannot append to the work queue while shutting down"}; - } - // Manual dispatcher queueing requires a copyable CallableT, we use a shared pointer trick to make a // copyable callable if necessary. if constexpr (std::is_copy_constructible::value) { - m_dispatcher.queue([this, callable = std::move(callable)]() { Invoke(callable); }); + if (m_cancellationSource.cancelled()) + { + m_shutdownQueue.push([callable = std::move(callable)] {}); + } + else + { + m_dispatcher.queue([this, callable = std::move(callable)]() { + callable(m_env.value()); + }); + } } else { - m_dispatcher.queue([this, callablePtr = std::make_shared(std::move(callable))]() { Invoke(*callablePtr); }); + if (m_cancellationSource.cancelled()) + { + m_shutdownQueue.push([callablePtr = std::make_shared(std::move(callable))] {}); + } + else + { + m_dispatcher.queue([this, callablePtr = std::make_shared(std::move(callable))]() { + (*callablePtr)(m_env.value()); + }); + } } } @@ -42,18 +54,17 @@ namespace Babylon void Run(Napi::Env); private: - template - void Invoke(CallableT& callable) - { - callable(m_env.value()); - } - std::optional m_env{}; std::optional> m_suspensionLock{}; arcana::cancellation_source m_cancellationSource{}; - arcana::manual_dispatcher<128> m_dispatcher{}; + + using DispatcherT = arcana::manual_dispatcher<128>; + DispatcherT m_dispatcher{}; + + // Put the callables in a separate queue during shutdown to ensure the callables are destroyed on the right thread. + arcana::blocking_concurrent_queue m_shutdownQueue{}; std::thread m_thread{}; }; From 7350715cf41ed88ce3234708afe97047fbac32ae Mon Sep 17 00:00:00 2001 From: Gary Hsu Date: Fri, 28 Apr 2023 11:05:05 -0700 Subject: [PATCH 12/21] Miscellaneous Windows AppRuntime fixes --- Core/AppRuntime/Source/AppRuntime_Chakra.cpp | 21 +++++------------- Core/AppRuntime/Source/AppRuntime_Win32.cpp | 23 ++++++-------------- Core/AppRuntime/Source/AppRuntime_WinRT.cpp | 5 ++++- 3 files changed, 17 insertions(+), 32 deletions(-) diff --git a/Core/AppRuntime/Source/AppRuntime_Chakra.cpp b/Core/AppRuntime/Source/AppRuntime_Chakra.cpp index 04d248224..2148401e1 100644 --- a/Core/AppRuntime/Source/AppRuntime_Chakra.cpp +++ b/Core/AppRuntime/Source/AppRuntime_Chakra.cpp @@ -20,30 +20,21 @@ namespace Babylon void AppRuntime::RunEnvironmentTier(const char*) { - using DispatchFunction = std::function)>; - DispatchFunction dispatchFunction{ - [this](std::function action) { - Dispatch([action = std::move(action)](Napi::Env) { - action(); - }); - }}; - JsRuntimeHandle jsRuntime; ThrowIfFailed(JsCreateRuntime(JsRuntimeAttributeNone, nullptr, &jsRuntime)); JsContextRef context; ThrowIfFailed(JsCreateContext(jsRuntime, &context)); ThrowIfFailed(JsSetCurrentContext(context)); ThrowIfFailed(JsSetPromiseContinuationCallback([](JsValueRef task, void* callbackState) { + auto* pThis = reinterpret_cast(callbackState); ThrowIfFailed(JsAddRef(task, nullptr)); - auto* dispatch = reinterpret_cast(callbackState); - dispatch->operator()([task]() { - JsValueRef undefined; - ThrowIfFailed(JsGetUndefinedValue(&undefined)); - ThrowIfFailed(JsCallFunction(task, &undefined, 1, nullptr)); + pThis->Dispatch([task](auto) { + JsValueRef global; + ThrowIfFailed(JsGetGlobalObject(&global)); + ThrowIfFailed(JsCallFunction(task, &global, 1, nullptr)); ThrowIfFailed(JsRelease(task, nullptr)); }); - }, - &dispatchFunction)); + }, this)); ThrowIfFailed(JsProjectWinRTNamespace(L"Windows")); #if defined(_DEBUG) diff --git a/Core/AppRuntime/Source/AppRuntime_Win32.cpp b/Core/AppRuntime/Source/AppRuntime_Win32.cpp index baf89300d..b17c5f6a1 100644 --- a/Core/AppRuntime/Source/AppRuntime_Win32.cpp +++ b/Core/AppRuntime/Source/AppRuntime_Win32.cpp @@ -1,7 +1,7 @@ #include "AppRuntime.h" -#include -#include +#include +#include #include #include @@ -10,23 +10,14 @@ namespace Babylon { - namespace - { - constexpr size_t FILENAME_BUFFER_SIZE = 1024; - } - void AppRuntime::RunPlatformTier() { - HRESULT hr = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); - assert(SUCCEEDED(hr)); - _CRT_UNUSED(hr); - auto coInitScopeGuard = gsl::finally([] { CoUninitialize(); }); + winrt::check_hresult(Windows::Foundation::Initialize(RO_INIT_MULTITHREADED)); + + char executablePath[1024]; + winrt::check_win32(GetModuleFileNameA(nullptr, executablePath, ARRAYSIZE(executablePath))); - char filename[FILENAME_BUFFER_SIZE]; - auto result = GetModuleFileNameA(nullptr, filename, static_cast(std::size(filename))); - assert(result != 0); - (void)result; - RunEnvironmentTier(filename); + RunEnvironmentTier(executablePath); } void AppRuntime::DefaultUnhandledExceptionHandler(const std::exception& error) diff --git a/Core/AppRuntime/Source/AppRuntime_WinRT.cpp b/Core/AppRuntime/Source/AppRuntime_WinRT.cpp index 2a1a3f586..b98a9fe28 100644 --- a/Core/AppRuntime/Source/AppRuntime_WinRT.cpp +++ b/Core/AppRuntime/Source/AppRuntime_WinRT.cpp @@ -1,6 +1,7 @@ #include "AppRuntime.h" -#include +#include +#include #include #include @@ -9,6 +10,8 @@ namespace Babylon { void AppRuntime::RunPlatformTier() { + winrt::check_hresult(Windows::Foundation::Initialize(RO_INIT_MULTITHREADED)); + RunEnvironmentTier(); } From 77dc8f35e8af651b6efe9a452bea0d80f87b1cee Mon Sep 17 00:00:00 2001 From: Gary Hsu Date: Fri, 28 Apr 2023 15:13:57 -0700 Subject: [PATCH 13/21] Fix typo in AppRuntime for Win32 --- Core/AppRuntime/Source/AppRuntime_Win32.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Core/AppRuntime/Source/AppRuntime_Win32.cpp b/Core/AppRuntime/Source/AppRuntime_Win32.cpp index b17c5f6a1..5fcee5698 100644 --- a/Core/AppRuntime/Source/AppRuntime_Win32.cpp +++ b/Core/AppRuntime/Source/AppRuntime_Win32.cpp @@ -15,7 +15,7 @@ namespace Babylon winrt::check_hresult(Windows::Foundation::Initialize(RO_INIT_MULTITHREADED)); char executablePath[1024]; - winrt::check_win32(GetModuleFileNameA(nullptr, executablePath, ARRAYSIZE(executablePath))); + winrt::check_bool(GetModuleFileNameA(nullptr, executablePath, ARRAYSIZE(executablePath)) != 0); RunEnvironmentTier(executablePath); } From def047ce49090c82e8916a087774f175651e3c8c Mon Sep 17 00:00:00 2001 From: Gary Hsu Date: Tue, 2 May 2023 13:59:58 -0700 Subject: [PATCH 14/21] Better fix for work queue shutdown issue --- Core/AppRuntime/Source/WorkQueue.cpp | 16 +++++-------- Core/AppRuntime/Source/WorkQueue.h | 36 ++++++---------------------- Dependencies/arcana.cpp | 2 +- 3 files changed, 14 insertions(+), 40 deletions(-) diff --git a/Core/AppRuntime/Source/WorkQueue.cpp b/Core/AppRuntime/Source/WorkQueue.cpp index eb525bbcc..9b3b2cc00 100644 --- a/Core/AppRuntime/Source/WorkQueue.cpp +++ b/Core/AppRuntime/Source/WorkQueue.cpp @@ -40,23 +40,19 @@ namespace Babylon void WorkQueue::Run(Napi::Env env) { - m_dispatcher.set_affinity(std::this_thread::get_id()); - m_env = std::make_optional(env); + m_dispatcher.set_affinity(std::this_thread::get_id()); + while (!m_cancellationSource.cancelled()) { m_dispatcher.blocking_tick(m_cancellationSource); } - // Drain the queue to complete work dispatched after cancellation. - m_dispatcher.tick(arcana::cancellation::none()); - - // There should no longer be any outstanding work once the queue is drained. - assert(m_dispatcher.empty()); - - // Clear the shutdown queue to make sure the callables are destroyed on this thread. - m_shutdownQueue.clear(); + // The dispatcher can be non-empty if something is dispatched after cancellation. + // For example, Chakra's JsSetPromiseContinuationCallback may potentially dispatch + // a continuation after cancellation. + m_dispatcher.clear(); m_env.reset(); } diff --git a/Core/AppRuntime/Source/WorkQueue.h b/Core/AppRuntime/Source/WorkQueue.h index 3795cfff8..a731c1ee9 100644 --- a/Core/AppRuntime/Source/WorkQueue.h +++ b/Core/AppRuntime/Source/WorkQueue.h @@ -23,29 +23,15 @@ namespace Babylon // copyable callable if necessary. if constexpr (std::is_copy_constructible::value) { - if (m_cancellationSource.cancelled()) - { - m_shutdownQueue.push([callable = std::move(callable)] {}); - } - else - { - m_dispatcher.queue([this, callable = std::move(callable)]() { - callable(m_env.value()); - }); - } + m_dispatcher.queue([this, callable = std::move(callable)]() { + callable(m_env.value()); + }); } else { - if (m_cancellationSource.cancelled()) - { - m_shutdownQueue.push([callablePtr = std::make_shared(std::move(callable))] {}); - } - else - { - m_dispatcher.queue([this, callablePtr = std::make_shared(std::move(callable))]() { - (*callablePtr)(m_env.value()); - }); - } + m_dispatcher.queue([this, callablePtr = std::make_shared(std::move(callable))]() { + (*callablePtr)(m_env.value()); + }); } } @@ -55,17 +41,9 @@ namespace Babylon private: std::optional m_env{}; - std::optional> m_suspensionLock{}; - arcana::cancellation_source m_cancellationSource{}; - - using DispatcherT = arcana::manual_dispatcher<128>; - DispatcherT m_dispatcher{}; - - // Put the callables in a separate queue during shutdown to ensure the callables are destroyed on the right thread. - arcana::blocking_concurrent_queue m_shutdownQueue{}; - + arcana::manual_dispatcher<128> m_dispatcher{}; std::thread m_thread{}; }; } diff --git a/Dependencies/arcana.cpp b/Dependencies/arcana.cpp index e9f2fb8bd..10d167ffe 160000 --- a/Dependencies/arcana.cpp +++ b/Dependencies/arcana.cpp @@ -1 +1 @@ -Subproject commit e9f2fb8bddd3eba0928ee4254dbe1c341e7bda97 +Subproject commit 10d167ffe0f86b2ddaccd93fd78ff2240cbb0c49 From 0ae6cf441ccb042e49eeedffdd0d49f4b832ede1 Mon Sep 17 00:00:00 2001 From: Gary Hsu Date: Thu, 4 May 2023 16:17:40 -0700 Subject: [PATCH 15/21] Fix build issues from merge --- Core/Graphics/Source/DeviceImpl.cpp | 11 ++++++++-- Core/Graphics/Source/DeviceImpl.h | 2 -- Dependencies/arcana.cpp | 2 +- Plugins/NativeCamera/Source/MediaStream.cpp | 24 ++++++++++++--------- Plugins/NativeCamera/Source/MediaStream.h | 1 + 5 files changed, 25 insertions(+), 15 deletions(-) diff --git a/Core/Graphics/Source/DeviceImpl.cpp b/Core/Graphics/Source/DeviceImpl.cpp index 3dad574bb..c866ab88b 100644 --- a/Core/Graphics/Source/DeviceImpl.cpp +++ b/Core/Graphics/Source/DeviceImpl.cpp @@ -300,9 +300,16 @@ namespace Babylon::Graphics return m_afterRenderDispatcher.scheduler(); } - void DeviceImpl::RequestScreenShot(std::function)> callback) + arcana::task, std::exception_ptr> DeviceImpl::RequestScreenShotAsync() { - m_screenShotCallbacks.push(std::move(callback)); + arcana::task_completion_source, std::exception_ptr> taskCompletionSource{}; + + m_screenShotCallbacks.push([taskCompletionSource](std::vector bytes) mutable + { + taskCompletionSource.complete(std::move(bytes)); + }); + + return taskCompletionSource.as_task(); } arcana::task DeviceImpl::ReadTextureAsync(bgfx::TextureHandle handle, gsl::span data, uint8_t mipLevel) diff --git a/Core/Graphics/Source/DeviceImpl.h b/Core/Graphics/Source/DeviceImpl.h index 41338c609..f0d101eb5 100644 --- a/Core/Graphics/Source/DeviceImpl.h +++ b/Core/Graphics/Source/DeviceImpl.h @@ -78,8 +78,6 @@ namespace Babylon::Graphics continuation_scheduler<>& BeforeRenderScheduler(); continuation_scheduler<>& AfterRenderScheduler(); - Update GetUpdate(const char* updateName); - arcana::task, std::exception_ptr> RequestScreenShotAsync(); arcana::task ReadTextureAsync(bgfx::TextureHandle handle, gsl::span data, uint8_t mipLevel); diff --git a/Dependencies/arcana.cpp b/Dependencies/arcana.cpp index e9f2fb8bd..10d167ffe 160000 --- a/Dependencies/arcana.cpp +++ b/Dependencies/arcana.cpp @@ -1 +1 @@ -Subproject commit e9f2fb8bddd3eba0928ee4254dbe1c341e7bda97 +Subproject commit 10d167ffe0f86b2ddaccd93fd78ff2240cbb0c49 diff --git a/Plugins/NativeCamera/Source/MediaStream.cpp b/Plugins/NativeCamera/Source/MediaStream.cpp index dc07c20e8..f9040bd7e 100644 --- a/Plugins/NativeCamera/Source/MediaStream.cpp +++ b/Plugins/NativeCamera/Source/MediaStream.cpp @@ -16,7 +16,7 @@ namespace Babylon::Plugins auto mediaStreamObject{Napi::Persistent(GetConstructor(env).New({}))}; auto mediaStream{MediaStream::Unwrap(mediaStreamObject.Value())}; - return mediaStream->ApplyInitialConstraintsAsync(constraints).then(mediaStream->m_runtimeScheduler, arcana::cancellation::none(), [mediaStreamObject{std::move(mediaStreamObject)}]() { + return mediaStream->ApplyInitialConstraintsAsync(constraints).then(arcana::inline_scheduler, arcana::cancellation::none(), [mediaStreamObject{std::move(mediaStreamObject)}]() { return mediaStreamObject.Value(); }); } @@ -58,14 +58,18 @@ namespace Babylon::Plugins MediaStream::~MediaStream() { - // TODO: Is this still necessary? -// if (m_cameraDevice != nullptr) -// { -// // The cameraDevice should be destroyed on the JS thread as it may need to access main thread resources -// // move ownership of the cameraDevice to a lambda and dispatch it with the runtimeScheduler so the destructor -// // is called from that thread. -// m_runtimeScheduler.Get()([cameraDevice = std::move(m_cameraDevice)]() {}); -// } + m_cancellationSource.cancel(); + + // HACK: This is a hack to make sure the camera device is destroyed on the JS thread. + // The napi-jsi adapter currently calls the destructors of JS objects possibly on the wrong thread. + // Once this is fixed, this hack will no longer be needed. + if (m_cameraDevice != nullptr) + { + // The cameraDevice should be destroyed on the JS thread as it may need to access main thread resources + // move ownership of the cameraDevice to a lambda and dispatch it with the runtimeScheduler so the destructor + // is called from that thread. + m_runtimeScheduler.Get()([cameraDevice = std::move(m_cameraDevice)]() {}); + } // Wait for async operations to complete. m_runtimeScheduler.Rundown(); @@ -124,7 +128,7 @@ namespace Babylon::Plugins // Create a persistent ref to the constraints object so it isn't destructed during our async work auto constraintsRef{Napi::Persistent(constraints)}; - return m_cameraDevice->OpenAsync(bestCamera.value().second).then(m_runtimeScheduler.Get(), arcana::cancellation::none(), [this, constraintsRef{std::move(constraintsRef)}](CameraDevice::CameraDimensions cameraDimensions) { + return m_cameraDevice->OpenAsync(bestCamera.value().second).then(m_runtimeScheduler.Get(), m_cancellationSource, [this, constraintsRef{std::move(constraintsRef)}](CameraDevice::CameraDimensions cameraDimensions) { this->Width = cameraDimensions.width; this->Height = cameraDimensions.height; diff --git a/Plugins/NativeCamera/Source/MediaStream.h b/Plugins/NativeCamera/Source/MediaStream.h index a55ec4cf1..e505314a1 100644 --- a/Plugins/NativeCamera/Source/MediaStream.h +++ b/Plugins/NativeCamera/Source/MediaStream.h @@ -46,6 +46,7 @@ namespace Babylon::Plugins // Capture CameraDevice in a shared_ptr because the iOS implementation relies on the `shared_from_this` syntax for async work std::shared_ptr m_cameraDevice{}; JsRuntimeScheduler m_runtimeScheduler; + arcana::cancellation_source m_cancellationSource; Napi::ObjectReference m_currentConstraints{}; }; From 71efaf422b4bd95705612f83adcd96675deb2484 Mon Sep 17 00:00:00 2001 From: Gary Hsu Date: Mon, 8 May 2023 17:32:18 -0700 Subject: [PATCH 16/21] Update arcana.cpp to include continuation fix --- Dependencies/arcana.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dependencies/arcana.cpp b/Dependencies/arcana.cpp index 10d167ffe..74b539f47 160000 --- a/Dependencies/arcana.cpp +++ b/Dependencies/arcana.cpp @@ -1 +1 @@ -Subproject commit 10d167ffe0f86b2ddaccd93fd78ff2240cbb0c49 +Subproject commit 74b539f474a7a85c2be8f7b3b8ac8303ce176cb4 From a0db41999261a5c594fcb9c89048289da9dcae38 Mon Sep 17 00:00:00 2001 From: Gary Hsu Date: Mon, 8 May 2023 17:45:40 -0700 Subject: [PATCH 17/21] Minor style fixes --- Plugins/NativeEngine/Source/NativeEngine.cpp | 1 - .../NativeInput/Source/Shared/NativeInput.cpp | 34 +++------- Polyfills/Canvas/Source/Image.cpp | 67 +++++++++---------- 3 files changed, 42 insertions(+), 60 deletions(-) diff --git a/Plugins/NativeEngine/Source/NativeEngine.cpp b/Plugins/NativeEngine/Source/NativeEngine.cpp index 3bac1dab2..9658dc1bd 100644 --- a/Plugins/NativeEngine/Source/NativeEngine.cpp +++ b/Plugins/NativeEngine/Source/NativeEngine.cpp @@ -1933,7 +1933,6 @@ namespace Babylon { m_updateToken.emplace(m_update.GetUpdateToken()); - // TODO: is there an issue with this? m_runtimeScheduler.Get()([this]() { m_updateToken.reset(); }); diff --git a/Plugins/NativeInput/Source/Shared/NativeInput.cpp b/Plugins/NativeInput/Source/Shared/NativeInput.cpp index 400f6363a..63427894c 100644 --- a/Plugins/NativeInput/Source/Shared/NativeInput.cpp +++ b/Plugins/NativeInput/Source/Shared/NativeInput.cpp @@ -153,14 +153,9 @@ namespace Babylon::Plugins void NativeInput::Impl::PointerDown(uint32_t pointerId, uint32_t buttonIndex, int32_t x, int32_t y, DeviceType deviceType) { m_runtime.Dispatch([pointerId, buttonIndex, x, y, deviceType, this](auto) { - const uint32_t inputIndex{ GetPointerButtonInputIndex(buttonIndex) }; - std::vector& deviceInputs{ GetOrCreateInputMap(deviceType, pointerId, { - inputIndex, - POINTER_X_INPUT_INDEX, - POINTER_Y_INPUT_INDEX, - POINTER_DELTA_HORIZONTAL_INDEX, - POINTER_DELTA_VERTICAL_INDEX - })}; + const uint32_t inputIndex{GetPointerButtonInputIndex(buttonIndex)}; + std::vector& deviceInputs{GetOrCreateInputMap(deviceType, pointerId, + {inputIndex, POINTER_X_INPUT_INDEX, POINTER_Y_INPUT_INDEX, POINTER_DELTA_HORIZONTAL_INDEX, POINTER_DELTA_VERTICAL_INDEX})}; // Record the x/y, but don't raise associated events (this matches the behavior in the browser). SetInputState(deviceType, pointerId, POINTER_DELTA_HORIZONTAL_INDEX, 0, deviceInputs, false); @@ -176,14 +171,9 @@ namespace Babylon::Plugins void NativeInput::Impl::PointerUp(uint32_t pointerId, uint32_t buttonIndex, int32_t x, int32_t y, DeviceType deviceType) { m_runtime.Dispatch([pointerId, buttonIndex, x, y, deviceType, this](auto) { - const uint32_t inputIndex{ GetPointerButtonInputIndex(buttonIndex) }; - std::vector& deviceInputs{ GetOrCreateInputMap(deviceType, pointerId, { - inputIndex, - POINTER_X_INPUT_INDEX, - POINTER_Y_INPUT_INDEX, - POINTER_DELTA_HORIZONTAL_INDEX, - POINTER_DELTA_VERTICAL_INDEX - })}; + const uint32_t inputIndex{GetPointerButtonInputIndex(buttonIndex)}; + std::vector& deviceInputs{GetOrCreateInputMap(deviceType, pointerId, + {inputIndex, POINTER_X_INPUT_INDEX, POINTER_Y_INPUT_INDEX, POINTER_DELTA_HORIZONTAL_INDEX, POINTER_DELTA_VERTICAL_INDEX})}; // Record the x/y, but don't raise associated events (this matches the behavior in the browser). SetInputState(deviceType, pointerId, POINTER_DELTA_HORIZONTAL_INDEX, 0, deviceInputs, false); @@ -208,13 +198,8 @@ namespace Babylon::Plugins void NativeInput::Impl::PointerMove(uint32_t pointerId, int32_t x, int32_t y, DeviceType deviceType) { m_runtime.Dispatch([pointerId, x, y, deviceType, this](auto) { - std::vector& deviceInputs{GetOrCreateInputMap(deviceType, pointerId, { - POINTER_X_INPUT_INDEX, - POINTER_Y_INPUT_INDEX, - POINTER_DELTA_HORIZONTAL_INDEX, - POINTER_DELTA_VERTICAL_INDEX, - POINTER_MOVE_INDEX - })}; + std::vector& deviceInputs{GetOrCreateInputMap(deviceType, pointerId, + {POINTER_X_INPUT_INDEX, POINTER_Y_INPUT_INDEX, POINTER_DELTA_HORIZONTAL_INDEX, POINTER_DELTA_VERTICAL_INDEX, POINTER_MOVE_INDEX})}; int32_t deltaX = 0; int32_t deltaY = 0; @@ -245,8 +230,7 @@ namespace Babylon::Plugins void NativeInput::Impl::PointerScroll(uint32_t pointerId, uint32_t scrollAxis, int32_t scrollValue, DeviceType deviceType) { - m_runtime.Dispatch([pointerId, scrollAxis, scrollValue, deviceType, this](auto) - { + m_runtime.Dispatch([pointerId, scrollAxis, scrollValue, deviceType, this](auto) { std::vector& deviceInputs{GetOrCreateInputMap(deviceType, pointerId, {scrollAxis})}; SetInputState(deviceType, pointerId, scrollAxis, scrollValue, deviceInputs, true); diff --git a/Polyfills/Canvas/Source/Image.cpp b/Polyfills/Canvas/Source/Image.cpp index 0bc9a1c0c..f182b0f73 100644 --- a/Polyfills/Canvas/Source/Image.cpp +++ b/Polyfills/Canvas/Source/Image.cpp @@ -108,40 +108,39 @@ namespace Babylon::Polyfills::Internal request.ResponseType(UrlLib::UrlResponseType::Buffer); request.SendAsync() .then(m_runtimeScheduler.Get(), m_cancellationSource, - [this, thisRef = Napi::Persistent(info.This()), request](arcana::expected result) - { - if (result.has_error()) - { - HandleLoadImageError(Napi::Error::New(thisRef.Env(), result.error())); - return; - } - - Dispose(); - - auto buffer = request.ResponseBuffer(); - if (buffer.data() == nullptr || buffer.size_bytes() == 0) - { - HandleLoadImageError(Napi::Error::New(thisRef.Env(), "Image with provided source returned empty response.")); - return; - } - - bx::AllocatorI* allocator = &Graphics::DeviceContext::GetFromJavaScript(thisRef.Env()).Allocator(); - m_imageContainer = bimg::imageParse(allocator, buffer.data(), static_cast(buffer.size_bytes()), bimg::TextureFormat::RGBA8); - - if (m_imageContainer == nullptr) - { - HandleLoadImageError(Napi::Error::New(thisRef.Env(), "Unable to decode image with provided src.")); - return; - } - - m_width = m_imageContainer->m_width; - m_height = m_imageContainer->m_height; - - if (!m_onloadHandlerRef.IsEmpty()) - { - m_onloadHandlerRef.Call({}); - } - }); + [this, thisRef = Napi::Persistent(info.This()), request](arcana::expected result) { + if (result.has_error()) + { + HandleLoadImageError(Napi::Error::New(Env(), result.error())); + return; + } + + Dispose(); + + auto buffer = request.ResponseBuffer(); + if (buffer.data() == nullptr || buffer.size_bytes() == 0) + { + HandleLoadImageError(Napi::Error::New(Env(), "Image with provided source returned empty response.")); + return; + } + + bx::AllocatorI* allocator = &Graphics::DeviceContext::GetFromJavaScript(thisRef.Env()).Allocator(); + m_imageContainer = bimg::imageParse(allocator, buffer.data(), static_cast(buffer.size_bytes()), bimg::TextureFormat::RGBA8); + + if (m_imageContainer == nullptr) + { + HandleLoadImageError(Napi::Error::New(Env(), "Unable to decode image with provided src.")); + return; + } + + m_width = m_imageContainer->m_width; + m_height = m_imageContainer->m_height; + + if (!m_onloadHandlerRef.IsEmpty()) + { + m_onloadHandlerRef.Call({}); + } + }); } void NativeCanvasImage::SetOnload(const Napi::CallbackInfo&, const Napi::Value& value) From 6909f55720afdc97a9c823320fc6494c1ee582a3 Mon Sep 17 00:00:00 2001 From: Gary Hsu Date: Mon, 8 May 2023 17:46:20 -0700 Subject: [PATCH 18/21] Update comment --- Core/JsRuntime/InternalInclude/Babylon/JsRuntimeScheduler.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Core/JsRuntime/InternalInclude/Babylon/JsRuntimeScheduler.h b/Core/JsRuntime/InternalInclude/Babylon/JsRuntimeScheduler.h index 6aeccece6..83ec9a936 100644 --- a/Core/JsRuntime/InternalInclude/Babylon/JsRuntimeScheduler.h +++ b/Core/JsRuntime/InternalInclude/Babylon/JsRuntimeScheduler.h @@ -18,7 +18,7 @@ namespace Babylon // to pump this dispatcher in its destructor to prevent deadlocks. // // The typical pattern for an arcana task will look something like this: - // class MyClass + // class MyClass : public ObjectWrap // { // public: // ~MyClass() From e9d229d56991052b757dd6b6a2324c5b5863dc51 Mon Sep 17 00:00:00 2001 From: Gary Hsu Date: Mon, 8 May 2023 17:46:57 -0700 Subject: [PATCH 19/21] Update comment 2 --- Core/JsRuntime/InternalInclude/Babylon/JsRuntimeScheduler.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Core/JsRuntime/InternalInclude/Babylon/JsRuntimeScheduler.h b/Core/JsRuntime/InternalInclude/Babylon/JsRuntimeScheduler.h index 83ec9a936..2f322c72f 100644 --- a/Core/JsRuntime/InternalInclude/Babylon/JsRuntimeScheduler.h +++ b/Core/JsRuntime/InternalInclude/Babylon/JsRuntimeScheduler.h @@ -18,7 +18,7 @@ namespace Babylon // to pump this dispatcher in its destructor to prevent deadlocks. // // The typical pattern for an arcana task will look something like this: - // class MyClass : public ObjectWrap + // class MyClass : public Napi::ObjectWrap // { // public: // ~MyClass() From 423ed5e214c761bd7cc453ce225df7891d363355 Mon Sep 17 00:00:00 2001 From: Gary Hsu Date: Mon, 8 May 2023 17:54:02 -0700 Subject: [PATCH 20/21] More style fixes --- Core/AppRuntime/Source/AppRuntime.cpp | 12 +-- Core/AppRuntime/Source/AppRuntime_Chakra.cpp | 19 ++-- Core/Graphics/Source/DeviceImpl.cpp | 3 +- .../Babylon/JsRuntimeScheduler.h | 10 +-- Plugins/NativeEngine/Source/NativeEngine.cpp | 9 +- Plugins/NativeXr/Source/NativeXr.cpp | 89 +++++++++---------- Polyfills/Canvas/Source/Canvas.cpp | 12 +-- Polyfills/Canvas/Source/Context.cpp | 2 +- .../XMLHttpRequest/Source/XMLHttpRequest.cpp | 29 +++--- 9 files changed, 88 insertions(+), 97 deletions(-) diff --git a/Core/AppRuntime/Source/AppRuntime.cpp b/Core/AppRuntime/Source/AppRuntime.cpp index ed893c647..0e6833a2c 100644 --- a/Core/AppRuntime/Source/AppRuntime.cpp +++ b/Core/AppRuntime/Source/AppRuntime.cpp @@ -13,8 +13,7 @@ namespace Babylon : m_workQueue{std::make_unique([this] { RunPlatformTier(); })} , m_unhandledExceptionHandler{unhandledExceptionHandler} { - Dispatch([this](Napi::Env env) - { + Dispatch([this](Napi::Env env) { JsRuntime::CreateForJavaScript(env, [this](auto func) { Dispatch(std::move(func)); }); }); } @@ -24,8 +23,7 @@ namespace Babylon // Notify the JsRuntime on the JavaScript thread that the JavaScript // runtime shutdown sequence has begun. The JsRuntimeScheduler will // use this signal to gracefully cancel asynchronous operations. - Dispatch([](Napi::Env env) - { + Dispatch([](Napi::Env env) { JsRuntime::NotifyDisposing(JsRuntime::GetFromJavaScript(env)); }); } @@ -47,10 +45,8 @@ namespace Babylon void AppRuntime::Dispatch(Dispatchable func) { - m_workQueue->Append([this, func{std::move(func)}](Napi::Env env) mutable - { - Execute([this, env, func{std::move(func)}]() mutable - { + m_workQueue->Append([this, func{std::move(func)}](Napi::Env env) mutable { + Execute([this, env, func{std::move(func)}]() mutable { try { func(env); diff --git a/Core/AppRuntime/Source/AppRuntime_Chakra.cpp b/Core/AppRuntime/Source/AppRuntime_Chakra.cpp index 8267031f2..d8200afc6 100644 --- a/Core/AppRuntime/Source/AppRuntime_Chakra.cpp +++ b/Core/AppRuntime/Source/AppRuntime_Chakra.cpp @@ -29,15 +29,16 @@ namespace Babylon ThrowIfFailed(JsSetCurrentContext(context)); ThrowIfFailed(JsSetPromiseContinuationCallback( [](JsValueRef task, void* callbackState) { - auto* pThis = reinterpret_cast(callbackState); - ThrowIfFailed(JsAddRef(task, nullptr)); - pThis->Dispatch([task](auto) { - JsValueRef global; - ThrowIfFailed(JsGetGlobalObject(&global)); - ThrowIfFailed(JsCallFunction(task, &global, 1, nullptr)); - ThrowIfFailed(JsRelease(task, nullptr)); - }); - }, this)); + auto* pThis = reinterpret_cast(callbackState); + ThrowIfFailed(JsAddRef(task, nullptr)); + pThis->Dispatch([task](auto) { + JsValueRef global; + ThrowIfFailed(JsGetGlobalObject(&global)); + ThrowIfFailed(JsCallFunction(task, &global, 1, nullptr)); + ThrowIfFailed(JsRelease(task, nullptr)); + }); + }, + this)); ThrowIfFailed(JsProjectWinRTNamespace(L"Windows")); #if defined(_DEBUG) diff --git a/Core/Graphics/Source/DeviceImpl.cpp b/Core/Graphics/Source/DeviceImpl.cpp index c866ab88b..8885c837a 100644 --- a/Core/Graphics/Source/DeviceImpl.cpp +++ b/Core/Graphics/Source/DeviceImpl.cpp @@ -304,8 +304,7 @@ namespace Babylon::Graphics { arcana::task_completion_source, std::exception_ptr> taskCompletionSource{}; - m_screenShotCallbacks.push([taskCompletionSource](std::vector bytes) mutable - { + m_screenShotCallbacks.push([taskCompletionSource](std::vector bytes) mutable { taskCompletionSource.complete(std::move(bytes)); }); diff --git a/Core/JsRuntime/InternalInclude/Babylon/JsRuntimeScheduler.h b/Core/JsRuntime/InternalInclude/Babylon/JsRuntimeScheduler.h index 2f322c72f..db7b210db 100644 --- a/Core/JsRuntime/InternalInclude/Babylon/JsRuntimeScheduler.h +++ b/Core/JsRuntime/InternalInclude/Babylon/JsRuntimeScheduler.h @@ -28,11 +28,11 @@ namespace Babylon // // Wait for asynchronous operations to complete. // m_runtimeScheduler.Rundown(); // } - // + // // void MyFunction(const Napi::CallbackInfo& info) // { // const auto callback{info[0].As()}; - // + // // arcana::make_task(arcana::threadpool_scheduler, m_cancellationSource, []() { // // do some asynchronous work // }).then(m_runtimeScheduler.Get(), m_cancellationSource, [thisRef = Napi::Persistent(info.This()), callback = Napi::Persistent(callback)]() { @@ -101,7 +101,8 @@ namespace Babylon class SchedulerImpl { public: - explicit SchedulerImpl(JsRuntimeScheduler& parent) : m_parent{parent} + explicit SchedulerImpl(JsRuntimeScheduler& parent) + : m_parent{parent} { } @@ -122,8 +123,7 @@ namespace Babylon if (m_runtime != nullptr) { - m_runtime->Dispatch([callable{std::forward(callable)}](Napi::Env) - { + m_runtime->Dispatch([callable{std::forward(callable)}](Napi::Env) { callable(); }); } diff --git a/Plugins/NativeEngine/Source/NativeEngine.cpp b/Plugins/NativeEngine/Source/NativeEngine.cpp index 9658dc1bd..24129a282 100644 --- a/Plugins/NativeEngine/Source/NativeEngine.cpp +++ b/Plugins/NativeEngine/Source/NativeEngine.cpp @@ -1132,14 +1132,11 @@ namespace Babylon const auto textureDestination = info[0].As>().Get(); const auto textureSource = info[1].As>().Get(); - arcana::make_task(m_update.Scheduler(), m_cancellationSource, [this, textureDestination, textureSource]() mutable - { - return arcana::make_task(m_runtimeScheduler.Get(), m_cancellationSource, [this, textureDestination, textureSource, updateToken = m_update.GetUpdateToken()]() mutable - { + arcana::make_task(m_update.Scheduler(), m_cancellationSource, [this, textureDestination, textureSource]() mutable { + return arcana::make_task(m_runtimeScheduler.Get(), m_cancellationSource, [this, textureDestination, textureSource, updateToken = m_update.GetUpdateToken()]() mutable { bgfx::Encoder* encoder = updateToken.GetEncoder(); GetBoundFrameBuffer(*encoder).Blit(*encoder, textureDestination->Handle(), 0, 0, textureSource->Handle()); - }).then(arcana::inline_scheduler, m_cancellationSource, [this](const arcana::expected& result) - { + }).then(arcana::inline_scheduler, m_cancellationSource, [this](const arcana::expected& result) { if (result.has_error()) { Napi::Error::New(Env(), result.error()).ThrowAsJavaScriptException(); diff --git a/Plugins/NativeXr/Source/NativeXr.cpp b/Plugins/NativeXr/Source/NativeXr.cpp index b90bf834b..7b50f1ba6 100644 --- a/Plugins/NativeXr/Source/NativeXr.cpp +++ b/Plugins/NativeXr/Source/NativeXr.cpp @@ -696,47 +696,47 @@ namespace Babylon bgfx::overrideInternal(colorTexture, reinterpret_cast(viewConfig.ColorTexturePointer)); bgfx::overrideInternal(depthTexture, reinterpret_cast(viewConfig.DepthTexturePointer)); }).then(m_runtimeScheduler.Get(), m_sessionState->CancellationSource, [this, thisRef{shared_from_this()}, colorTexture, depthTexture, requiresAppClear, &viewConfig]() { - const auto eyeCount = std::max(static_cast(1), static_cast(viewConfig.ViewTextureSize.Depth)); - // TODO (rgerd): Remove old framebuffers from resource table? - viewConfig.FrameBuffers.resize(eyeCount); - for (uint16_t eyeIdx = 0; eyeIdx < eyeCount; eyeIdx++) - { - std::array attachments{}; - attachments[0].init(colorTexture, bgfx::Access::Write, eyeIdx); - attachments[1].init(depthTexture, bgfx::Access::Write, eyeIdx); - - auto frameBufferHandle = bgfx::createFrameBuffer(static_cast(attachments.size()), attachments.data(), false); - - const auto frameBufferPtr = new Graphics::FrameBuffer( - m_sessionState->GraphicsContext, - frameBufferHandle, - static_cast(viewConfig.ViewTextureSize.Width), - static_cast(viewConfig.ViewTextureSize.Height), - true, - true, - true); - - auto& frameBuffer = *frameBufferPtr; - - // WebXR, at least in its current implementation, specifies an implicit default clear to black. - // https://immersive-web.github.io/webxr/#xrwebgllayer-interface - frameBuffer.Clear(*m_sessionState->Update.GetUpdateToken().GetEncoder(), BGFX_CLEAR_COLOR | BGFX_CLEAR_DEPTH | BGFX_CLEAR_STENCIL, 0, 1.0f, 0); - - viewConfig.FrameBuffers[eyeIdx] = frameBufferPtr; - - auto jsWidth{Napi::Value::From(m_env, viewConfig.ViewTextureSize.Width)}; - auto jsHeight{Napi::Value::From(m_env, viewConfig.ViewTextureSize.Height)}; - auto jsFrameBuffer{Napi::Pointer::Create(m_env, frameBufferPtr, Napi::NapiPointerDeleter(frameBufferPtr))}; - viewConfig.JsTextures[frameBufferPtr] = Napi::Persistent(m_sessionState->CreateRenderTexture.Call({jsWidth, jsHeight, jsFrameBuffer}).As()); - // OpenXR doesn't pre-clear textures, and so we need to make sure the render target gets cleared before rendering the scene. - // ARCore and ARKit effectively pre-clear by pre-compositing the camera feed. - if (requiresAppClear) - { - viewConfig.JsTextures[frameBufferPtr].Set("skipInitialClear", false); - } - } - viewConfig.Initialized = true; - }).then(arcana::inline_scheduler, m_sessionState->CancellationSource, [env{m_env}](const arcana::expected& result) { + const auto eyeCount = std::max(static_cast(1), static_cast(viewConfig.ViewTextureSize.Depth)); + // TODO (rgerd): Remove old framebuffers from resource table? + viewConfig.FrameBuffers.resize(eyeCount); + for (uint16_t eyeIdx = 0; eyeIdx < eyeCount; eyeIdx++) + { + std::array attachments{}; + attachments[0].init(colorTexture, bgfx::Access::Write, eyeIdx); + attachments[1].init(depthTexture, bgfx::Access::Write, eyeIdx); + + auto frameBufferHandle = bgfx::createFrameBuffer(static_cast(attachments.size()), attachments.data(), false); + + const auto frameBufferPtr = new Graphics::FrameBuffer( + m_sessionState->GraphicsContext, + frameBufferHandle, + static_cast(viewConfig.ViewTextureSize.Width), + static_cast(viewConfig.ViewTextureSize.Height), + true, + true, + true); + + auto& frameBuffer = *frameBufferPtr; + + // WebXR, at least in its current implementation, specifies an implicit default clear to black. + // https://immersive-web.github.io/webxr/#xrwebgllayer-interface + frameBuffer.Clear(*m_sessionState->Update.GetUpdateToken().GetEncoder(), BGFX_CLEAR_COLOR | BGFX_CLEAR_DEPTH | BGFX_CLEAR_STENCIL, 0, 1.0f, 0); + + viewConfig.FrameBuffers[eyeIdx] = frameBufferPtr; + + auto jsWidth{Napi::Value::From(m_env, viewConfig.ViewTextureSize.Width)}; + auto jsHeight{Napi::Value::From(m_env, viewConfig.ViewTextureSize.Height)}; + auto jsFrameBuffer{Napi::Pointer::Create(m_env, frameBufferPtr, Napi::NapiPointerDeleter(frameBufferPtr))}; + viewConfig.JsTextures[frameBufferPtr] = Napi::Persistent(m_sessionState->CreateRenderTexture.Call({jsWidth, jsHeight, jsFrameBuffer}).As()); + // OpenXR doesn't pre-clear textures, and so we need to make sure the render target gets cleared before rendering the scene. + // ARCore and ARKit effectively pre-clear by pre-compositing the camera feed. + if (requiresAppClear) + { + viewConfig.JsTextures[frameBufferPtr].Set("skipInitialClear", false); + } + } + viewConfig.Initialized = true; + }).then(arcana::inline_scheduler, m_sessionState->CancellationSource, [env{m_env}](const arcana::expected& result) { if (result.has_error()) { Napi::Error::New(env, result.error()).ThrowAsJavaScriptException(); @@ -3427,10 +3427,9 @@ namespace Babylon // Fire off the IsSessionSupported task. xr::System::IsSessionSupportedAsync(sessionType) - .then(m_runtimeScheduler.Get(), arcana::cancellation::none(), [deferred, thisRef = Napi::Persistent(info.This())](bool result) - { - deferred.Resolve(Napi::Boolean::New(thisRef.Env(), result)); - }); + .then(m_runtimeScheduler.Get(), arcana::cancellation::none(), [deferred, thisRef = Napi::Persistent(info.This())](bool result) { + deferred.Resolve(Napi::Boolean::New(thisRef.Env(), result)); + }); return deferred.Promise(); } diff --git a/Polyfills/Canvas/Source/Canvas.cpp b/Polyfills/Canvas/Source/Canvas.cpp index 7267929d3..71c9f0272 100644 --- a/Polyfills/Canvas/Source/Canvas.cpp +++ b/Polyfills/Canvas/Source/Canvas.cpp @@ -22,13 +22,15 @@ namespace Babylon::Polyfills::Internal Napi::Function func = DefineClass( env, JS_CONSTRUCTOR_NAME, - {StaticMethod("loadTTFAsync", &NativeCanvas::LoadTTFAsync), + { + StaticMethod("loadTTFAsync", &NativeCanvas::LoadTTFAsync), InstanceAccessor("width", &NativeCanvas::GetWidth, &NativeCanvas::SetWidth), InstanceAccessor("height", &NativeCanvas::GetHeight, &NativeCanvas::SetHeight), InstanceMethod("getContext", &NativeCanvas::GetContext), InstanceMethod("getCanvasTexture", &NativeCanvas::GetCanvasTexture), InstanceMethod("dispose", &NativeCanvas::Dispose), - StaticMethod("parseColor", &NativeCanvas::ParseColor)}); + StaticMethod("parseColor", &NativeCanvas::ParseColor), + }); JsRuntime::NativeObject::GetFromJavaScript(env).Set(JS_CONSTRUCTOR_NAME, func); } @@ -63,11 +65,9 @@ namespace Babylon::Polyfills::Internal auto& deviceContext = Graphics::DeviceContext::GetFromJavaScript(info.Env()); auto update = deviceContext.GetUpdate("update"); - arcana::make_task(update.Scheduler(), arcana::cancellation::none(), [fontName = std::move(fontName), fontData = std::move(fontBuffer), &runtime, deferred = std::move(deferred)]() mutable - { + arcana::make_task(update.Scheduler(), arcana::cancellation::none(), [fontName = std::move(fontName), fontData = std::move(fontBuffer), &runtime, deferred = std::move(deferred)]() mutable { fontsInfos[fontName] = fontData; - runtime.Dispatch([deferred = std::move(deferred)](Napi::Env env) - { + runtime.Dispatch([deferred = std::move(deferred)](Napi::Env env) { deferred.Resolve(env.Undefined()); }); }); diff --git a/Polyfills/Canvas/Source/Context.cpp b/Polyfills/Canvas/Source/Context.cpp index b98144805..c7b4a419c 100644 --- a/Polyfills/Canvas/Source/Context.cpp +++ b/Polyfills/Canvas/Source/Context.cpp @@ -471,7 +471,7 @@ namespace Babylon::Polyfills::Internal nvgFillPaint(m_nvg, imagePaint); nvgFill(m_nvg); SetDirty(info.This()); - } + } else if (info.Length() == 5) { const auto dx = info[1].As().Int32Value(); diff --git a/Polyfills/XMLHttpRequest/Source/XMLHttpRequest.cpp b/Polyfills/XMLHttpRequest/Source/XMLHttpRequest.cpp index fcca5a127..9c3dd488d 100644 --- a/Polyfills/XMLHttpRequest/Source/XMLHttpRequest.cpp +++ b/Polyfills/XMLHttpRequest/Source/XMLHttpRequest.cpp @@ -224,21 +224,20 @@ namespace Babylon::Polyfills::Internal m_request.SendAsync() .then(m_runtimeScheduler.Get(), m_cancellationSource, - [this, thisRef = Napi::Persistent(info.This())](arcana::expected result) - { - if (result.has_error()) - { - Napi::Error::New(thisRef.Env(), result.error()).ThrowAsJavaScriptException(); - return; - } - - SetReadyState(ReadyState::Done); - RaiseEvent(EventType::LoadEnd); - - // Assume the XMLHttpRequest will only be used for a single request and clear the event handlers. - // Single use seems to be the standard pattern, and we need to release our strong refs to event handlers. - m_eventHandlerRefs.clear(); - }); + [this, thisRef = Napi::Persistent(info.This())](arcana::expected result) { + if (result.has_error()) + { + Napi::Error::New(thisRef.Env(), result.error()).ThrowAsJavaScriptException(); + return; + } + + SetReadyState(ReadyState::Done); + RaiseEvent(EventType::LoadEnd); + + // Assume the XMLHttpRequest will only be used for a single request and clear the event handlers. + // Single use seems to be the standard pattern, and we need to release our strong refs to event handlers. + m_eventHandlerRefs.clear(); + }); } void XMLHttpRequest::SetReadyState(ReadyState readyState) From 36a466163a74580165cb91d067ab62ad5d0110be Mon Sep 17 00:00:00 2001 From: Gary Hsu Date: Tue, 23 May 2023 18:16:36 -0700 Subject: [PATCH 21/21] Merge AppRuntime and WorkQueue to fix race condition --- Core/AppRuntime/CMakeLists.txt | 8 +- Core/AppRuntime/Include/Babylon/AppRuntime.h | 32 ++----- Core/AppRuntime/Source/AppRuntime.cpp | 51 +++-------- Core/AppRuntime/Source/AppRuntimeImpl.cpp | 88 +++++++++++++++++++ Core/AppRuntime/Source/AppRuntimeImpl.h | 68 ++++++++++++++ ...Android.cpp => AppRuntimeImpl_Android.cpp} | 8 +- ...e_Chakra.cpp => AppRuntimeImpl_Chakra.cpp} | 26 +++--- ...Runtime_JSI.cpp => AppRuntimeImpl_JSI.cpp} | 15 ++-- ....cpp => AppRuntimeImpl_JavaScriptCore.cpp} | 4 +- .../AppRuntime/Source/AppRuntimeImpl_Unix.cpp | 21 +++++ ...ppRuntime_V8.cpp => AppRuntimeImpl_V8.cpp} | 4 +- ...ime_Win32.cpp => AppRuntimeImpl_Win32.cpp} | 8 +- ...ime_WinRT.cpp => AppRuntimeImpl_WinRT.cpp} | 8 +- ...ppRuntime_iOS.mm => AppRuntimeImpl_iOS.mm} | 8 +- ...ntime_macOS.mm => AppRuntimeImpl_macOS.mm} | 8 +- Core/AppRuntime/Source/AppRuntime_Unix.cpp | 22 ----- Core/AppRuntime/Source/WorkQueue.cpp | 59 ------------- Core/AppRuntime/Source/WorkQueue.h | 49 ----------- 18 files changed, 243 insertions(+), 244 deletions(-) create mode 100644 Core/AppRuntime/Source/AppRuntimeImpl.cpp create mode 100644 Core/AppRuntime/Source/AppRuntimeImpl.h rename Core/AppRuntime/Source/{AppRuntime_Android.cpp => AppRuntimeImpl_Android.cpp} (61%) rename Core/AppRuntime/Source/{AppRuntime_Chakra.cpp => AppRuntimeImpl_Chakra.cpp} (66%) rename Core/AppRuntime/Source/{AppRuntime_JSI.cpp => AppRuntimeImpl_JSI.cpp} (76%) rename Core/AppRuntime/Source/{AppRuntime_JavaScriptCore.cpp => AppRuntimeImpl_JavaScriptCore.cpp} (83%) create mode 100644 Core/AppRuntime/Source/AppRuntimeImpl_Unix.cpp rename Core/AppRuntime/Source/{AppRuntime_V8.cpp => AppRuntimeImpl_V8.cpp} (96%) rename Core/AppRuntime/Source/{AppRuntime_Win32.cpp => AppRuntimeImpl_Win32.cpp} (73%) rename Core/AppRuntime/Source/{AppRuntime_WinRT.cpp => AppRuntimeImpl_WinRT.cpp} (65%) rename Core/AppRuntime/Source/{AppRuntime_iOS.mm => AppRuntimeImpl_iOS.mm} (54%) rename Core/AppRuntime/Source/{AppRuntime_macOS.mm => AppRuntimeImpl_macOS.mm} (54%) delete mode 100644 Core/AppRuntime/Source/AppRuntime_Unix.cpp delete mode 100644 Core/AppRuntime/Source/WorkQueue.cpp delete mode 100644 Core/AppRuntime/Source/WorkQueue.h diff --git a/Core/AppRuntime/CMakeLists.txt b/Core/AppRuntime/CMakeLists.txt index 400783488..b7e24b38e 100644 --- a/Core/AppRuntime/CMakeLists.txt +++ b/Core/AppRuntime/CMakeLists.txt @@ -2,10 +2,10 @@ set(SOURCES "Include/Babylon/Dispatchable.h" "Include/Babylon/AppRuntime.h" "Source/AppRuntime.cpp" - "Source/AppRuntime_${NAPI_JAVASCRIPT_ENGINE}.cpp" - "Source/AppRuntime_${BABYLON_NATIVE_PLATFORM}.${BABYLON_NATIVE_PLATFORM_IMPL_EXT}" - "Source/WorkQueue.cpp" - "Source/WorkQueue.h") + "Source/AppRuntimeImpl.h" + "Source/AppRuntimeImpl.cpp" + "Source/AppRuntimeImpl_${NAPI_JAVASCRIPT_ENGINE}.cpp" + "Source/AppRuntimeImpl_${BABYLON_NATIVE_PLATFORM}.${BABYLON_NATIVE_PLATFORM_IMPL_EXT}") add_library(AppRuntime ${SOURCES}) warnings_as_errors(AppRuntime) diff --git a/Core/AppRuntime/Include/Babylon/AppRuntime.h b/Core/AppRuntime/Include/Babylon/AppRuntime.h index 70ddd5921..ab0a9f000 100644 --- a/Core/AppRuntime/Include/Babylon/AppRuntime.h +++ b/Core/AppRuntime/Include/Babylon/AppRuntime.h @@ -1,7 +1,8 @@ #pragma once +#include + #include "Dispatchable.h" -#include #include #include @@ -9,7 +10,7 @@ namespace Babylon { - class WorkQueue; + class AppRuntimeImpl; class AppRuntime final { @@ -18,33 +19,16 @@ namespace Babylon AppRuntime(std::function unhandledExceptionHandler); ~AppRuntime(); + // Move semantics + AppRuntime(AppRuntime&&) noexcept; + AppRuntime& operator=(AppRuntime&&) noexcept; + void Suspend(); void Resume(); void Dispatch(Dispatchable callback); private: - // These three methods are the mechanism by which platform- and JavaScript-specific - // code can be "injected" into the execution of the JavaScript thread. These three - // functions are implemented in separate files, thus allowing implementations to be - // mixed and matched by the build system based on the platform and JavaScript engine - // being targeted, without resorting to virtuality. An important nuance of these - // functions is that they are all intended to call each other: RunPlatformTier MUST - // call RunEnvironmentTier, which MUST create the initial Napi::Env and pass it to - // Run. This arrangement allows not only for an arbitrary assemblage of platforms, - // but it also allows us to respect the requirement by certain platforms (notably V8) - // that certain program state be allocated and stored only on the stack. - void RunPlatformTier(); - void RunEnvironmentTier(const char* executablePath = "."); - void Run(Napi::Env); - - // This method is called from Dispatch to allow platform-specific code to add - // extra logic around the invocation of a dispatched callback. - void Execute(Dispatchable callback); - - static void DefaultUnhandledExceptionHandler(const std::exception& error); - - std::unique_ptr m_workQueue{}; - std::function m_unhandledExceptionHandler{}; + std::unique_ptr m_impl; }; } diff --git a/Core/AppRuntime/Source/AppRuntime.cpp b/Core/AppRuntime/Source/AppRuntime.cpp index 0e6833a2c..ca0ea96f5 100644 --- a/Core/AppRuntime/Source/AppRuntime.cpp +++ b/Core/AppRuntime/Source/AppRuntime.cpp @@ -1,65 +1,36 @@ #include "AppRuntime.h" -#include "WorkQueue.h" -#include +#include "AppRuntimeImpl.h" namespace Babylon { AppRuntime::AppRuntime() - : AppRuntime{DefaultUnhandledExceptionHandler} + : m_impl{std::make_unique()} { } AppRuntime::AppRuntime(std::function unhandledExceptionHandler) - : m_workQueue{std::make_unique([this] { RunPlatformTier(); })} - , m_unhandledExceptionHandler{unhandledExceptionHandler} + : m_impl{std::make_unique(unhandledExceptionHandler)} { - Dispatch([this](Napi::Env env) { - JsRuntime::CreateForJavaScript(env, [this](auto func) { Dispatch(std::move(func)); }); - }); } - AppRuntime::~AppRuntime() - { - // Notify the JsRuntime on the JavaScript thread that the JavaScript - // runtime shutdown sequence has begun. The JsRuntimeScheduler will - // use this signal to gracefully cancel asynchronous operations. - Dispatch([](Napi::Env env) { - JsRuntime::NotifyDisposing(JsRuntime::GetFromJavaScript(env)); - }); - } + AppRuntime::~AppRuntime() = default; - void AppRuntime::Run(Napi::Env env) - { - m_workQueue->Run(env); - } + // Move semantics + AppRuntime::AppRuntime(AppRuntime&&) noexcept = default; + AppRuntime& AppRuntime::operator=(AppRuntime&&) noexcept = default; void AppRuntime::Suspend() { - m_workQueue->Suspend(); + m_impl->Suspend(); } void AppRuntime::Resume() { - m_workQueue->Resume(); + m_impl->Resume(); } - void AppRuntime::Dispatch(Dispatchable func) + void AppRuntime::Dispatch(Dispatchable callback) { - m_workQueue->Append([this, func{std::move(func)}](Napi::Env env) mutable { - Execute([this, env, func{std::move(func)}]() mutable { - try - { - func(env); - } - catch (const std::exception& error) - { - m_unhandledExceptionHandler(error); - } - catch (...) - { - std::abort(); - } - }); - }); + m_impl->Dispatch(std::move(callback)); } } diff --git a/Core/AppRuntime/Source/AppRuntimeImpl.cpp b/Core/AppRuntime/Source/AppRuntimeImpl.cpp new file mode 100644 index 000000000..9e28faf1e --- /dev/null +++ b/Core/AppRuntime/Source/AppRuntimeImpl.cpp @@ -0,0 +1,88 @@ +#include "AppRuntimeImpl.h" +#include + +namespace Babylon +{ + AppRuntimeImpl::AppRuntimeImpl(std::function unhandledExceptionHandler) + : m_unhandledExceptionHandler{std::move(unhandledExceptionHandler)} + , m_thread{[this] { RunPlatformTier(); }} + { + Dispatch([this](Napi::Env env) { + JsRuntime::CreateForJavaScript(env, [this](auto func) { Dispatch(std::move(func)); }); + }); + } + + AppRuntimeImpl::~AppRuntimeImpl() + { + if (m_suspensionLock.has_value()) + { + m_suspensionLock.reset(); + } + + Dispatch([this](Napi::Env env) { + // Notify the JsRuntime on the JavaScript thread that the JavaScript runtime shutdown sequence has + // begun. The JsRuntimeScheduler will use this signal to gracefully cancel asynchronous operations. + JsRuntime::NotifyDisposing(JsRuntime::GetFromJavaScript(env)); + + // Cancel on the JavaScript thread to signal the Run function to gracefully end. It must be + // dispatched and not canceled directly to ensure that existing work is executed and executed in + // the correct order. + m_cancellationSource.cancel(); + }); + + m_thread.join(); + } + + void AppRuntimeImpl::Suspend() + { + auto suspensionMutex = std::make_shared(); + m_suspensionLock.emplace(*suspensionMutex); + Append([suspensionMutex = std::move(suspensionMutex)](Napi::Env) { + std::scoped_lock lock{*suspensionMutex}; + }); + } + + void AppRuntimeImpl::Resume() + { + m_suspensionLock.reset(); + } + + void AppRuntimeImpl::Dispatch(Dispatchable func) + { + Append([this, func{std::move(func)}](Napi::Env env) mutable { + Execute([this, env, func{std::move(func)}]() mutable { + try + { + func(env); + } + catch (const std::exception& error) + { + m_unhandledExceptionHandler(error); + } + catch (...) + { + std::abort(); + } + }); + }); + } + + void AppRuntimeImpl::Run(Napi::Env env) + { + m_env = std::make_optional(env); + + m_dispatcher.set_affinity(std::this_thread::get_id()); + + while (!m_cancellationSource.cancelled()) + { + m_dispatcher.blocking_tick(m_cancellationSource); + } + + // The dispatcher can be non-empty if something is dispatched after cancellation. + // For example, Chakra's JsSetPromiseContinuationCallback may potentially dispatch + // a continuation after cancellation. + m_dispatcher.clear(); + + m_env.reset(); + } +} diff --git a/Core/AppRuntime/Source/AppRuntimeImpl.h b/Core/AppRuntime/Source/AppRuntimeImpl.h new file mode 100644 index 000000000..a3bc12104 --- /dev/null +++ b/Core/AppRuntime/Source/AppRuntimeImpl.h @@ -0,0 +1,68 @@ +#pragma once + +#include "AppRuntime.h" +#include +#include +#include + +namespace Babylon +{ + class AppRuntimeImpl + { + public: + AppRuntimeImpl(std::function unhandledExceptionHandler = DefaultUnhandledExceptionHandler); + ~AppRuntimeImpl(); + + void Suspend(); + void Resume(); + + void Dispatch(Dispatchable func); + + private: + static void DefaultUnhandledExceptionHandler(const std::exception& error); + + template + void Append(CallableT callable) + { + // Manual dispatcher queueing requires a copyable CallableT, we use a shared pointer trick to make a + // copyable callable if necessary. + if constexpr (std::is_copy_constructible::value) + { + m_dispatcher.queue([this, callable = std::move(callable)]() { + callable(m_env.value()); + }); + } + else + { + m_dispatcher.queue([this, callablePtr = std::make_shared(std::move(callable))]() { + (*callablePtr)(m_env.value()); + }); + } + } + + // These three methods are the mechanism by which platform- and JavaScript-specific + // code can be "injected" into the execution of the JavaScript thread. These three + // functions are implemented in separate files, thus allowing implementations to be + // mixed and matched by the build system based on the platform and JavaScript engine + // being targeted, without resorting to virtuality. An important nuance of these + // functions is that they are all intended to call each other: RunPlatformTier MUST + // call RunEnvironmentTier, which MUST create the initial Napi::Env and pass it to + // Run. This arrangement allows not only for an arbitrary assemblage of platforms, + // but it also allows us to respect the requirement by certain platforms (notably V8) + // that certain program state be allocated and stored only on the stack. + void RunPlatformTier(); + void RunEnvironmentTier(const char* executablePath = "."); + void Run(Napi::Env); + + // This method is called from Dispatch to allow platform-specific code to add + // extra logic around the invocation of a dispatched callback. + void Execute(Dispatchable callback); + + std::function m_unhandledExceptionHandler{}; + std::optional m_env{}; + std::optional> m_suspensionLock{}; + arcana::cancellation_source m_cancellationSource{}; + arcana::manual_dispatcher<128> m_dispatcher{}; + std::thread m_thread{}; + }; +} diff --git a/Core/AppRuntime/Source/AppRuntime_Android.cpp b/Core/AppRuntime/Source/AppRuntimeImpl_Android.cpp similarity index 61% rename from Core/AppRuntime/Source/AppRuntime_Android.cpp rename to Core/AppRuntime/Source/AppRuntimeImpl_Android.cpp index a87db5681..6a1092555 100644 --- a/Core/AppRuntime/Source/AppRuntime_Android.cpp +++ b/Core/AppRuntime/Source/AppRuntimeImpl_Android.cpp @@ -1,23 +1,23 @@ -#include "AppRuntime.h" +#include "AppRuntimeImpl.h" #include #include #include namespace Babylon { - void AppRuntime::RunPlatformTier() + void AppRuntimeImpl::RunPlatformTier() { RunEnvironmentTier(); } - void AppRuntime::DefaultUnhandledExceptionHandler(const std::exception& error) + void AppRuntimeImpl::DefaultUnhandledExceptionHandler(const std::exception& error) { std::stringstream ss{}; ss << "Uncaught Error: " << error.what() << std::endl; __android_log_write(ANDROID_LOG_ERROR, "BabylonNative", ss.str().data()); } - void AppRuntime::Execute(Dispatchable callback) + void AppRuntimeImpl::Execute(Dispatchable callback) { callback(); } diff --git a/Core/AppRuntime/Source/AppRuntime_Chakra.cpp b/Core/AppRuntime/Source/AppRuntimeImpl_Chakra.cpp similarity index 66% rename from Core/AppRuntime/Source/AppRuntime_Chakra.cpp rename to Core/AppRuntime/Source/AppRuntimeImpl_Chakra.cpp index d8200afc6..6d829e763 100644 --- a/Core/AppRuntime/Source/AppRuntime_Chakra.cpp +++ b/Core/AppRuntime/Source/AppRuntimeImpl_Chakra.cpp @@ -1,4 +1,4 @@ -#include "AppRuntime.h" +#include "AppRuntimeImpl.h" #include @@ -20,25 +20,23 @@ namespace Babylon } } - void AppRuntime::RunEnvironmentTier(const char*) + void AppRuntimeImpl::RunEnvironmentTier(const char*) { JsRuntimeHandle jsRuntime; ThrowIfFailed(JsCreateRuntime(JsRuntimeAttributeNone, nullptr, &jsRuntime)); JsContextRef context; ThrowIfFailed(JsCreateContext(jsRuntime, &context)); ThrowIfFailed(JsSetCurrentContext(context)); - ThrowIfFailed(JsSetPromiseContinuationCallback( - [](JsValueRef task, void* callbackState) { - auto* pThis = reinterpret_cast(callbackState); - ThrowIfFailed(JsAddRef(task, nullptr)); - pThis->Dispatch([task](auto) { - JsValueRef global; - ThrowIfFailed(JsGetGlobalObject(&global)); - ThrowIfFailed(JsCallFunction(task, &global, 1, nullptr)); - ThrowIfFailed(JsRelease(task, nullptr)); - }); - }, - this)); + ThrowIfFailed(JsSetPromiseContinuationCallback([](JsValueRef task, void* callbackState) { + auto* pThis = reinterpret_cast(callbackState); + ThrowIfFailed(JsAddRef(task, nullptr)); + pThis->Dispatch([task](auto) { + JsValueRef global; + ThrowIfFailed(JsGetGlobalObject(&global)); + ThrowIfFailed(JsCallFunction(task, &global, 1, nullptr)); + ThrowIfFailed(JsRelease(task, nullptr)); + }); + }, this)); ThrowIfFailed(JsProjectWinRTNamespace(L"Windows")); #if defined(_DEBUG) diff --git a/Core/AppRuntime/Source/AppRuntime_JSI.cpp b/Core/AppRuntime/Source/AppRuntimeImpl_JSI.cpp similarity index 76% rename from Core/AppRuntime/Source/AppRuntime_JSI.cpp rename to Core/AppRuntime/Source/AppRuntimeImpl_JSI.cpp index 2b94fcd19..34e880d40 100644 --- a/Core/AppRuntime/Source/AppRuntime_JSI.cpp +++ b/Core/AppRuntime/Source/AppRuntimeImpl_JSI.cpp @@ -1,5 +1,4 @@ -#include "AppRuntime.h" -#include "WorkQueue.h" +#include "AppRuntimeImpl.h" #include @@ -11,15 +10,15 @@ namespace class TaskRunnerAdapter : public v8runtime::JSITaskRunner { public: - TaskRunnerAdapter(Babylon::WorkQueue& workQueue) - : m_workQueue(workQueue) + TaskRunnerAdapter(Babylon::AppRuntimeImpl& appRuntimeImpl) + : m_appRuntimeImpl(appRuntimeImpl) { } void postTask(std::unique_ptr task) override { std::shared_ptr shared_task(task.release()); - m_workQueue.Append([shared_task2 = std::move(shared_task)](Napi::Env) { + m_appRuntimeImpl.Dispatch([shared_task2 = std::move(shared_task)](Napi::Env) { shared_task2->run(); }); } @@ -28,17 +27,17 @@ namespace TaskRunnerAdapter(const TaskRunnerAdapter&) = delete; TaskRunnerAdapter& operator=(const TaskRunnerAdapter&) = delete; - Babylon::WorkQueue& m_workQueue; + Babylon::AppRuntimeImpl& m_appRuntimeImpl; }; } namespace Babylon { - void AppRuntime::RunEnvironmentTier(const char*) + void AppRuntimeImpl::RunEnvironmentTier(const char*) { v8runtime::V8RuntimeArgs args{}; args.inspectorPort = 5643; - args.foreground_task_runner = std::make_shared(*m_workQueue); + args.foreground_task_runner = std::make_shared(*this); const auto runtime{v8runtime::makeV8Runtime(std::move(args))}; const auto env{Napi::Attach(*runtime)}; diff --git a/Core/AppRuntime/Source/AppRuntime_JavaScriptCore.cpp b/Core/AppRuntime/Source/AppRuntimeImpl_JavaScriptCore.cpp similarity index 83% rename from Core/AppRuntime/Source/AppRuntime_JavaScriptCore.cpp rename to Core/AppRuntime/Source/AppRuntimeImpl_JavaScriptCore.cpp index bcc224d29..48f42ca18 100644 --- a/Core/AppRuntime/Source/AppRuntime_JavaScriptCore.cpp +++ b/Core/AppRuntime/Source/AppRuntimeImpl_JavaScriptCore.cpp @@ -1,11 +1,11 @@ -#include "AppRuntime.h" +#include "AppRuntimeImpl.h" #include #include namespace Babylon { - void AppRuntime::RunEnvironmentTier(const char*) + void AppRuntimeImpl::RunEnvironmentTier(const char*) { auto globalContext = JSGlobalContextCreateInGroup(nullptr, nullptr); Napi::Env env = Napi::Attach(globalContext); diff --git a/Core/AppRuntime/Source/AppRuntimeImpl_Unix.cpp b/Core/AppRuntime/Source/AppRuntimeImpl_Unix.cpp new file mode 100644 index 000000000..fcc67b22f --- /dev/null +++ b/Core/AppRuntime/Source/AppRuntimeImpl_Unix.cpp @@ -0,0 +1,21 @@ +#include "AppRuntimeImpl.h" +#include +#include + +namespace Babylon +{ + void AppRuntimeImpl::RunPlatformTier() + { + RunEnvironmentTier(); + } + + void AppRuntimeImpl::DefaultUnhandledExceptionHandler(const std::exception& error) + { + std::cerr << "Uncaught Error: " << error.what() << std::endl; + } + + void AppRuntimeImpl::Execute(Dispatchable callback) + { + callback(); + } +} diff --git a/Core/AppRuntime/Source/AppRuntime_V8.cpp b/Core/AppRuntime/Source/AppRuntimeImpl_V8.cpp similarity index 96% rename from Core/AppRuntime/Source/AppRuntime_V8.cpp rename to Core/AppRuntime/Source/AppRuntimeImpl_V8.cpp index a08abdeff..445d220e9 100644 --- a/Core/AppRuntime/Source/AppRuntime_V8.cpp +++ b/Core/AppRuntime/Source/AppRuntimeImpl_V8.cpp @@ -1,4 +1,4 @@ -#include "AppRuntime.h" +#include "AppRuntimeImpl.h" #include @@ -64,7 +64,7 @@ namespace Babylon std::unique_ptr Module::s_module; } - void AppRuntime::RunEnvironmentTier(const char* executablePath) + void AppRuntimeImpl::RunEnvironmentTier(const char* executablePath) { // Create the isolate. Module::Initialize(executablePath); diff --git a/Core/AppRuntime/Source/AppRuntime_Win32.cpp b/Core/AppRuntime/Source/AppRuntimeImpl_Win32.cpp similarity index 73% rename from Core/AppRuntime/Source/AppRuntime_Win32.cpp rename to Core/AppRuntime/Source/AppRuntimeImpl_Win32.cpp index 5fcee5698..753de97d9 100644 --- a/Core/AppRuntime/Source/AppRuntime_Win32.cpp +++ b/Core/AppRuntime/Source/AppRuntimeImpl_Win32.cpp @@ -1,4 +1,4 @@ -#include "AppRuntime.h" +#include "AppRuntimeImpl.h" #include #include @@ -10,7 +10,7 @@ namespace Babylon { - void AppRuntime::RunPlatformTier() + void AppRuntimeImpl::RunPlatformTier() { winrt::check_hresult(Windows::Foundation::Initialize(RO_INIT_MULTITHREADED)); @@ -20,14 +20,14 @@ namespace Babylon RunEnvironmentTier(executablePath); } - void AppRuntime::DefaultUnhandledExceptionHandler(const std::exception& error) + void AppRuntimeImpl::DefaultUnhandledExceptionHandler(const std::exception& error) { std::stringstream ss{}; ss << "Uncaught Error: " << error.what() << std::endl; OutputDebugStringA(ss.str().data()); } - void AppRuntime::Execute(Dispatchable callback) + void AppRuntimeImpl::Execute(Dispatchable callback) { callback(); } diff --git a/Core/AppRuntime/Source/AppRuntime_WinRT.cpp b/Core/AppRuntime/Source/AppRuntimeImpl_WinRT.cpp similarity index 65% rename from Core/AppRuntime/Source/AppRuntime_WinRT.cpp rename to Core/AppRuntime/Source/AppRuntimeImpl_WinRT.cpp index b98a9fe28..266d85c9f 100644 --- a/Core/AppRuntime/Source/AppRuntime_WinRT.cpp +++ b/Core/AppRuntime/Source/AppRuntimeImpl_WinRT.cpp @@ -1,4 +1,4 @@ -#include "AppRuntime.h" +#include "AppRuntimeImpl.h" #include #include @@ -8,21 +8,21 @@ namespace Babylon { - void AppRuntime::RunPlatformTier() + void AppRuntimeImpl::RunPlatformTier() { winrt::check_hresult(Windows::Foundation::Initialize(RO_INIT_MULTITHREADED)); RunEnvironmentTier(); } - void AppRuntime::DefaultUnhandledExceptionHandler(const std::exception& error) + void AppRuntimeImpl::DefaultUnhandledExceptionHandler(const std::exception& error) { std::stringstream ss{}; ss << "Uncaught Error: " << error.what() << std::endl; OutputDebugStringA(ss.str().data()); } - void AppRuntime::Execute(Dispatchable callback) + void AppRuntimeImpl::Execute(Dispatchable callback) { callback(); } diff --git a/Core/AppRuntime/Source/AppRuntime_iOS.mm b/Core/AppRuntime/Source/AppRuntimeImpl_iOS.mm similarity index 54% rename from Core/AppRuntime/Source/AppRuntime_iOS.mm rename to Core/AppRuntime/Source/AppRuntimeImpl_iOS.mm index e1528cf8e..49f764b61 100644 --- a/Core/AppRuntime/Source/AppRuntime_iOS.mm +++ b/Core/AppRuntime/Source/AppRuntimeImpl_iOS.mm @@ -1,21 +1,21 @@ -#include "AppRuntime.h" +#include "AppRuntimeImpl.h" #include #import namespace Babylon { - void AppRuntime::RunPlatformTier() + void AppRuntimeImpl::RunPlatformTier() { RunEnvironmentTier(); } - void AppRuntime::DefaultUnhandledExceptionHandler(const std::exception& error) + void AppRuntimeImpl::DefaultUnhandledExceptionHandler(const std::exception& error) { NSLog(@"Uncaught Error: %s", error.what()); } - void AppRuntime::Execute(Dispatchable callback) + void AppRuntimeImpl::Execute(Dispatchable callback) { @autoreleasepool { diff --git a/Core/AppRuntime/Source/AppRuntime_macOS.mm b/Core/AppRuntime/Source/AppRuntimeImpl_macOS.mm similarity index 54% rename from Core/AppRuntime/Source/AppRuntime_macOS.mm rename to Core/AppRuntime/Source/AppRuntimeImpl_macOS.mm index e1528cf8e..49f764b61 100644 --- a/Core/AppRuntime/Source/AppRuntime_macOS.mm +++ b/Core/AppRuntime/Source/AppRuntimeImpl_macOS.mm @@ -1,21 +1,21 @@ -#include "AppRuntime.h" +#include "AppRuntimeImpl.h" #include #import namespace Babylon { - void AppRuntime::RunPlatformTier() + void AppRuntimeImpl::RunPlatformTier() { RunEnvironmentTier(); } - void AppRuntime::DefaultUnhandledExceptionHandler(const std::exception& error) + void AppRuntimeImpl::DefaultUnhandledExceptionHandler(const std::exception& error) { NSLog(@"Uncaught Error: %s", error.what()); } - void AppRuntime::Execute(Dispatchable callback) + void AppRuntimeImpl::Execute(Dispatchable callback) { @autoreleasepool { diff --git a/Core/AppRuntime/Source/AppRuntime_Unix.cpp b/Core/AppRuntime/Source/AppRuntime_Unix.cpp deleted file mode 100644 index 69585c398..000000000 --- a/Core/AppRuntime/Source/AppRuntime_Unix.cpp +++ /dev/null @@ -1,22 +0,0 @@ -#include "WorkQueue.h" -#include "AppRuntime.h" -#include -#include - -namespace Babylon -{ - void AppRuntime::RunPlatformTier() - { - RunEnvironmentTier(); - } - - void AppRuntime::DefaultUnhandledExceptionHandler(const std::exception& error) - { - std::cerr << "Uncaught Error: " << error.what() << std::endl; - } - - void AppRuntime::Execute(Dispatchable callback) - { - callback(); - } -} diff --git a/Core/AppRuntime/Source/WorkQueue.cpp b/Core/AppRuntime/Source/WorkQueue.cpp deleted file mode 100644 index 9b3b2cc00..000000000 --- a/Core/AppRuntime/Source/WorkQueue.cpp +++ /dev/null @@ -1,59 +0,0 @@ -#include "WorkQueue.h" - -namespace Babylon -{ - WorkQueue::WorkQueue(std::function threadProcedure) - : m_thread{std::move(threadProcedure)} - { - } - - WorkQueue::~WorkQueue() - { - if (m_suspensionLock.has_value()) - { - Resume(); - } - - // Dispatch a cancel to signal the Run function to gracefully end. - // It must be dispatched and not canceled directly to ensure that - // existing work is executed and executed in the correct order. - m_dispatcher([this]() { - m_cancellationSource.cancel(); - }); - - m_thread.join(); - } - - void WorkQueue::Suspend() - { - auto suspensionMutex = std::make_shared(); - m_suspensionLock.emplace(*suspensionMutex); - Append([suspensionMutex{std::move(suspensionMutex)}](Napi::Env) { - std::scoped_lock lock{*suspensionMutex}; - }); - } - - void WorkQueue::Resume() - { - m_suspensionLock.reset(); - } - - void WorkQueue::Run(Napi::Env env) - { - m_env = std::make_optional(env); - - m_dispatcher.set_affinity(std::this_thread::get_id()); - - while (!m_cancellationSource.cancelled()) - { - m_dispatcher.blocking_tick(m_cancellationSource); - } - - // The dispatcher can be non-empty if something is dispatched after cancellation. - // For example, Chakra's JsSetPromiseContinuationCallback may potentially dispatch - // a continuation after cancellation. - m_dispatcher.clear(); - - m_env.reset(); - } -} diff --git a/Core/AppRuntime/Source/WorkQueue.h b/Core/AppRuntime/Source/WorkQueue.h deleted file mode 100644 index a731c1ee9..000000000 --- a/Core/AppRuntime/Source/WorkQueue.h +++ /dev/null @@ -1,49 +0,0 @@ -#pragma once - -#include -#include -#include - -#include -#include -#include - -namespace Babylon -{ - class WorkQueue - { - public: - WorkQueue(std::function threadProcedure); - ~WorkQueue(); - - template - void Append(CallableT callable) - { - // Manual dispatcher queueing requires a copyable CallableT, we use a shared pointer trick to make a - // copyable callable if necessary. - if constexpr (std::is_copy_constructible::value) - { - m_dispatcher.queue([this, callable = std::move(callable)]() { - callable(m_env.value()); - }); - } - else - { - m_dispatcher.queue([this, callablePtr = std::make_shared(std::move(callable))]() { - (*callablePtr)(m_env.value()); - }); - } - } - - void Suspend(); - void Resume(); - void Run(Napi::Env); - - private: - std::optional m_env{}; - std::optional> m_suspensionLock{}; - arcana::cancellation_source m_cancellationSource{}; - arcana::manual_dispatcher<128> m_dispatcher{}; - std::thread m_thread{}; - }; -}