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 42bf8fce3..ca0ea96f5 100644 --- a/Core/AppRuntime/Source/AppRuntime.cpp +++ b/Core/AppRuntime/Source/AppRuntime.cpp @@ -1,59 +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() - { - } + 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 55% rename from Core/AppRuntime/Source/AppRuntime_Chakra.cpp rename to Core/AppRuntime/Source/AppRuntimeImpl_Chakra.cpp index 4484070c0..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,33 +20,23 @@ namespace Babylon } } - void AppRuntime::RunEnvironmentTier(const char*) + void AppRuntimeImpl::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) { - ThrowIfFailed(JsAddRef(task, nullptr)); - auto* dispatch = reinterpret_cast(callbackState); - dispatch->operator()([task]() { - JsValueRef undefined; - ThrowIfFailed(JsGetUndefinedValue(&undefined)); - ThrowIfFailed(JsCallFunction(task, &undefined, 1, nullptr)); - ThrowIfFailed(JsRelease(task, nullptr)); - }); - }, - &dispatchFunction)); + 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/AppRuntimeImpl_Win32.cpp b/Core/AppRuntime/Source/AppRuntimeImpl_Win32.cpp new file mode 100644 index 000000000..753de97d9 --- /dev/null +++ b/Core/AppRuntime/Source/AppRuntimeImpl_Win32.cpp @@ -0,0 +1,34 @@ +#include "AppRuntimeImpl.h" + +#include +#include + +#include +#include +#include +#include + +namespace Babylon +{ + void AppRuntimeImpl::RunPlatformTier() + { + winrt::check_hresult(Windows::Foundation::Initialize(RO_INIT_MULTITHREADED)); + + char executablePath[1024]; + winrt::check_bool(GetModuleFileNameA(nullptr, executablePath, ARRAYSIZE(executablePath)) != 0); + + RunEnvironmentTier(executablePath); + } + + void AppRuntimeImpl::DefaultUnhandledExceptionHandler(const std::exception& error) + { + std::stringstream ss{}; + ss << "Uncaught Error: " << error.what() << std::endl; + OutputDebugStringA(ss.str().data()); + } + + void AppRuntimeImpl::Execute(Dispatchable callback) + { + callback(); + } +} diff --git a/Core/AppRuntime/Source/AppRuntimeImpl_WinRT.cpp b/Core/AppRuntime/Source/AppRuntimeImpl_WinRT.cpp new file mode 100644 index 000000000..266d85c9f --- /dev/null +++ b/Core/AppRuntime/Source/AppRuntimeImpl_WinRT.cpp @@ -0,0 +1,29 @@ +#include "AppRuntimeImpl.h" + +#include +#include + +#include +#include + +namespace Babylon +{ + void AppRuntimeImpl::RunPlatformTier() + { + winrt::check_hresult(Windows::Foundation::Initialize(RO_INIT_MULTITHREADED)); + + RunEnvironmentTier(); + } + + void AppRuntimeImpl::DefaultUnhandledExceptionHandler(const std::exception& error) + { + std::stringstream ss{}; + ss << "Uncaught Error: " << error.what() << std::endl; + OutputDebugStringA(ss.str().data()); + } + + void AppRuntimeImpl::Execute(Dispatchable callback) + { + callback(); + } +} diff --git a/Core/AppRuntime/Source/AppRuntime_macOS.mm b/Core/AppRuntime/Source/AppRuntimeImpl_iOS.mm similarity index 54% rename from Core/AppRuntime/Source/AppRuntime_macOS.mm rename to Core/AppRuntime/Source/AppRuntimeImpl_iOS.mm index e1528cf8e..49f764b61 100644 --- a/Core/AppRuntime/Source/AppRuntime_macOS.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_iOS.mm b/Core/AppRuntime/Source/AppRuntimeImpl_macOS.mm similarity index 54% rename from Core/AppRuntime/Source/AppRuntime_iOS.mm rename to Core/AppRuntime/Source/AppRuntimeImpl_macOS.mm index e1528cf8e..49f764b61 100644 --- a/Core/AppRuntime/Source/AppRuntime_iOS.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/AppRuntime_Win32.cpp b/Core/AppRuntime/Source/AppRuntime_Win32.cpp deleted file mode 100644 index baf89300d..000000000 --- a/Core/AppRuntime/Source/AppRuntime_Win32.cpp +++ /dev/null @@ -1,43 +0,0 @@ -#include "AppRuntime.h" - -#include -#include - -#include -#include -#include -#include - -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(); }); - - char filename[FILENAME_BUFFER_SIZE]; - auto result = GetModuleFileNameA(nullptr, filename, static_cast(std::size(filename))); - assert(result != 0); - (void)result; - RunEnvironmentTier(filename); - } - - void AppRuntime::DefaultUnhandledExceptionHandler(const std::exception& error) - { - std::stringstream ss{}; - ss << "Uncaught Error: " << error.what() << std::endl; - OutputDebugStringA(ss.str().data()); - } - - void AppRuntime::Execute(Dispatchable callback) - { - callback(); - } -} diff --git a/Core/AppRuntime/Source/AppRuntime_WinRT.cpp b/Core/AppRuntime/Source/AppRuntime_WinRT.cpp deleted file mode 100644 index 2a1a3f586..000000000 --- a/Core/AppRuntime/Source/AppRuntime_WinRT.cpp +++ /dev/null @@ -1,26 +0,0 @@ -#include "AppRuntime.h" - -#include - -#include -#include - -namespace Babylon -{ - void AppRuntime::RunPlatformTier() - { - RunEnvironmentTier(); - } - - void AppRuntime::DefaultUnhandledExceptionHandler(const std::exception& error) - { - std::stringstream ss{}; - ss << "Uncaught Error: " << error.what() << std::endl; - OutputDebugStringA(ss.str().data()); - } - - 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 4c1c5f21a..000000000 --- a/Core/AppRuntime/Source/WorkQueue.cpp +++ /dev/null @@ -1,49 +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(); - } - - m_cancelSource.cancel(); - m_dispatcher.cancelled(); - - 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_cancelSource.cancelled()) - { - m_dispatcher.blocking_tick(m_cancelSource); - } - - m_dispatcher.clear(); - } -} diff --git a/Core/AppRuntime/Source/WorkQueue.h b/Core/AppRuntime/Source/WorkQueue.h deleted file mode 100644 index a9b8fada8..000000000 --- a/Core/AppRuntime/Source/WorkQueue.h +++ /dev/null @@ -1,54 +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)]() { Invoke(callable); }); - } - else - { - m_dispatcher.queue([this, callablePtr = std::make_shared(std::move(callable))]() { Invoke(*callablePtr); }); - } - } - - void Suspend(); - void Resume(); - 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_cancelSource{}; - arcana::manual_dispatcher<128> m_dispatcher{}; - - std::thread m_thread; - }; -} diff --git a/Core/Graphics/InternalInclude/Babylon/Graphics/DeviceContext.h b/Core/Graphics/InternalInclude/Babylon/Graphics/DeviceContext.h index 90c6c928e..33f46dbb6 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); @@ -115,6 +116,8 @@ namespace Babylon::Graphics void RemoveTexture(bgfx::TextureHandle handle); TextureInfo GetTextureInfo(bgfx::TextureHandle handle); + bx::AllocatorI& Allocator() { return m_allocator; } + private: friend UpdateToken; @@ -122,5 +125,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 3dad574bb..8885c837a 100644 --- a/Core/Graphics/Source/DeviceImpl.cpp +++ b/Core/Graphics/Source/DeviceImpl.cpp @@ -300,9 +300,15 @@ 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 7455a1575..f0d101eb5 100644 --- a/Core/Graphics/Source/DeviceImpl.h +++ b/Core/Graphics/Source/DeviceImpl.h @@ -78,7 +78,7 @@ namespace Babylon::Graphics continuation_scheduler<>& BeforeRenderScheduler(); continuation_scheduler<>& AfterRenderScheduler(); - 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 afa288454..804a351de 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,30 +23,48 @@ 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; + 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&, 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. + void Dispatch(std::function); private: JsRuntime(Napi::Env, DispatchFunctionT); DispatchFunctionT m_dispatchFunction{}; + std::vector m_disposingCallbacks{}; }; } diff --git a/Core/JsRuntime/InternalInclude/Babylon/JsRuntimeScheduler.h b/Core/JsRuntime/InternalInclude/Babylon/JsRuntimeScheduler.h index 4f1929203..db7b210db 100644 --- a/Core/JsRuntime/InternalInclude/Babylon/JsRuntimeScheduler.h +++ b/Core/JsRuntime/InternalInclude/Babylon/JsRuntimeScheduler.h @@ -1,30 +1,146 @@ #pragma once #include +#include +#include +#include namespace Babylon { - /** - * Scheduler that invokes continuations via JsRuntime::Dispatch. - * Intended to be consumed by arcana.cpp tasks. - */ - class JsRuntimeScheduler + // 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. 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 Napi::ObjectWrap + // { + // public: + // ~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; + // JsRuntimeScheduler m_runtimeScheduler; + // }; + // + // **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 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 + // when `Rundown()` is called in the N-API class destructor. + class JsRuntimeScheduler : public JsRuntime::IDisposingCallback { public: explicit JsRuntimeScheduler(JsRuntime& runtime) - : m_runtime{runtime} + : m_runtime{&runtime} + , m_scheduler{*this} { + std::scoped_lock lock{m_mutex}; + JsRuntime::RegisterDisposing(*m_runtime, this); } - template - void operator()(CallableT&& callable) const + ~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. + void Rundown() { - m_runtime.Dispatch([callable{std::forward(callable)}](Napi::Env) { - callable(); - }); + 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; + void Disposing() override + { + std::scoped_lock lock{m_mutex}; + m_runtime = nullptr; + } + + class SchedulerImpl + { + public: + explicit SchedulerImpl(JsRuntimeScheduler& parent) + : m_parent{parent} + { + } + + template + void operator()(CallableT&& callable) const + { + m_parent.Dispatch(std::forward(callable)); + } + + private: + JsRuntimeScheduler& m_parent; + }; + + template + void Dispatch(CallableT&& callable) + { + std::scoped_lock lock{m_mutex}; + + if (m_runtime != nullptr) + { + m_runtime->Dispatch([callable{std::forward(callable)}](Napi::Env) { + callable(); + }); + } + else + { + m_disposingDispatcher(std::forward(callable)); + } + + --m_count; + } + + 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 9e8f4c1a6..bc2b5e636 100644 --- a/Core/JsRuntime/Source/JsRuntime.cpp +++ b/Core/JsRuntime/Source/JsRuntime.cpp @@ -1,4 +1,5 @@ #include +#include namespace Babylon { @@ -40,6 +41,32 @@ namespace Babylon .Data(); } + void JsRuntime::NotifyDisposing(JsRuntime& runtime) + { + auto callbacks = std::move(runtime.m_disposingCallbacks); + for (auto* callback : callbacks) + { + callback->Disposing(); + } + } + + void JsRuntime::RegisterDisposing(JsRuntime& runtime, IDisposingCallback* 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) { m_dispatchFunction([function = std::move(function)](Napi::Env env) { diff --git a/Dependencies/arcana.cpp b/Dependencies/arcana.cpp index e9f2fb8bd..74b539f47 160000 --- a/Dependencies/arcana.cpp +++ b/Dependencies/arcana.cpp @@ -1 +1 @@ -Subproject commit e9f2fb8bddd3eba0928ee4254dbe1c341e7bda97 +Subproject commit 74b539f474a7a85c2be8f7b3b8ac8303ce176cb4 diff --git a/Plugins/NativeCamera/Source/MediaDevices.cpp b/Plugins/NativeCamera/Source/MediaDevices.cpp index 7469061b5..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, 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 7f44e05eb..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,13 +58,21 @@ namespace Babylon::Plugins MediaStream::~MediaStream() { + 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([cameraDevice = std::move(m_cameraDevice)]() {}); + 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) @@ -120,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, 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{}; }; diff --git a/Plugins/NativeCamera/Source/NativeVideo.cpp b/Plugins/NativeCamera/Source/NativeVideo.cpp index 5198f357d..d292e16d1 100644 --- a/Plugins/NativeCamera/Source/NativeVideo.cpp +++ b/Plugins/NativeCamera/Source/NativeVideo.cpp @@ -34,9 +34,18 @@ namespace Babylon::Plugins NativeVideo::NativeVideo(const Napi::CallbackInfo& info) : Napi::ObjectWrap{info} + , m_runtimeScheduler{JsRuntime::GetFromJavaScript(info.Env())} { } + NativeVideo::~NativeVideo() + { + m_cancellationSource.cancel(); + + // Wait for async operations to complete. + m_runtimeScheduler.Rundown(); + } + Napi::Value NativeVideo::GetVideoWidth(const Napi::CallbackInfo& /*info*/) { if (!m_streamObject.Value().IsNull() && !m_streamObject.Value().IsUndefined()) @@ -141,8 +150,7 @@ 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_streamObject.Value().IsNull() && !m_streamObject.Value().IsUndefined()) { @@ -150,7 +158,7 @@ namespace Babylon::Plugins RaiseEvent("playing"); } - 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 ab8e27ca8..fa8735652 100644 --- a/Plugins/NativeCamera/Source/NativeVideo.h +++ b/Plugins/NativeCamera/Source/NativeVideo.h @@ -18,7 +18,7 @@ namespace Babylon::Plugins static void Initialize(Napi::Env& env); static Napi::Object New(const Napi::CallbackInfo& info); NativeVideo(const Napi::CallbackInfo& info); - ~NativeVideo() = default; + ~NativeVideo(); void UpdateTexture(bgfx::TextureHandle textureHandle); @@ -37,6 +37,9 @@ namespace Babylon::Plugins Napi::Value GetSrcObject(const Napi::CallbackInfo& info); void SetSrcObject(const Napi::CallbackInfo& info, const Napi::Value& value); + arcana::cancellation_source m_cancellationSource; + JsRuntimeScheduler m_runtimeScheduler; + std::unordered_map> m_eventHandlerRefs{}; bool m_isReady{false}; diff --git a/Plugins/NativeEngine/Source/NativeEngine.cpp b/Plugins/NativeEngine/Source/NativeEngine.cpp index de7746031..24129a282 100644 --- a/Plugins/NativeEngine/Source/NativeEngine.cpp +++ b/Plugins/NativeEngine/Source/NativeEngine.cpp @@ -502,28 +502,29 @@ 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} { } NativeEngine::~NativeEngine() { Dispose(); + + // Wait for async operations to complete. + m_runtimeScheduler.Rundown(); } void NativeEngine::Dispose() { - m_cancellationSource->cancel(); + m_cancellationSource.cancel(); } - void NativeEngine::Dispose(const Napi::CallbackInfo& /*info*/) + void NativeEngine::Dispose(const Napi::CallbackInfo&) { Dispose(); } @@ -751,16 +752,13 @@ namespace Babylon ProgramData* program = new ProgramData{}; Napi::Value jsProgram = Napi::Pointer::Create(info.Env(), program, Napi::NapiPointerDeleter(program)); - arcana::make_task(arcana::threadpool_scheduler, *m_cancellationSource, - [this, vertexSource, fragmentSource, cancellationSource{m_cancellationSource}]() -> std::unique_ptr { + arcana::make_task(arcana::threadpool_scheduler, m_cancellationSource, + [this, vertexSource, fragmentSource]() { return CreateProgramInternal(vertexSource, fragmentSource); }) - .then(m_runtimeScheduler, *m_cancellationSource, - [program, - jsProgramRef{Napi::Persistent(jsProgram)}, - onSuccessRef{Napi::Persistent(onSuccess)}, - onErrorRef{Napi::Persistent(onError)}, - cancellationSource{m_cancellationSource}](const arcana::expected, std::exception_ptr>& result) { + .then(m_runtimeScheduler.Get(), m_cancellationSource, + [thisRef = Napi::Persistent(info.This()), program, jsProgramRef = Napi::Persistent(jsProgram), + onSuccessRef = Napi::Persistent(onSuccess), onErrorRef = Napi::Persistent(onError)](const arcana::expected, std::exception_ptr>& result) { if (result.has_error()) { onErrorRef.Call({Napi::Error::New(onErrorRef.Env(), result.error()).Value()}); @@ -1110,22 +1108,23 @@ 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); + 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, *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({}); - } - }); + .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) @@ -1133,12 +1132,12 @@ 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, 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(); + 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, 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(); } @@ -1162,8 +1161,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); } @@ -1229,18 +1228,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({}); @@ -1272,19 +1271,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({}); @@ -1376,7 +1375,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(); } @@ -1435,7 +1434,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; @@ -1449,13 +1448,13 @@ 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."}; } @@ -1473,7 +1472,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()); @@ -1483,7 +1482,7 @@ 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; @@ -1491,10 +1490,10 @@ namespace Babylon 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); } @@ -1546,7 +1545,7 @@ namespace Babylon bgfx::FrameBufferHandle frameBufferHandle = bgfx::createFrameBuffer(numAttachments, attachments.data(), true); assert(bgfx::isValid(frameBufferHandle)); - 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)); } @@ -1558,7 +1557,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); @@ -1583,7 +1582,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(); @@ -1600,7 +1599,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(); @@ -1616,7 +1615,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}; @@ -1657,23 +1656,23 @@ namespace Babylon Napi::Value NativeEngine::GetRenderWidth(const Napi::CallbackInfo& info) { - return Napi::Value::From(info.Env(), std::floor(m_graphicsContext.GetWidth() / m_graphicsContext.GetHardwareScalingLevel())); + return Napi::Value::From(info.Env(), std::floor(m_deviceContext.GetWidth() / m_deviceContext.GetHardwareScalingLevel())); } Napi::Value NativeEngine::GetRenderHeight(const Napi::CallbackInfo& info) { - return Napi::Value::From(info.Env(), std::floor(m_graphicsContext.GetHeight() / m_graphicsContext.GetHardwareScalingLevel())); + return Napi::Value::From(info.Env(), std::floor(m_deviceContext.GetHeight() / m_deviceContext.GetHardwareScalingLevel())); } 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) @@ -1692,7 +1691,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()) @@ -1748,7 +1747,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."); @@ -1760,7 +1759,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."); @@ -1783,20 +1782,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()}; @@ -1860,6 +1845,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 @@ -1932,7 +1929,8 @@ namespace Babylon if (!m_updateToken) { m_updateToken.emplace(m_update.GetUpdateToken()); - m_runtime.Dispatch([this](auto) { + + m_runtimeScheduler.Get()([this]() { m_updateToken.reset(); }); } @@ -1966,8 +1964,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"}; @@ -1976,8 +1974,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 a814fd647..5b689e742 100644 --- a/Plugins/NativeEngine/Source/NativeEngine.h +++ b/Plugins/NativeEngine/Source/NativeEngine.h @@ -203,31 +203,27 @@ namespace Babylon void SetViewPort(NativeDataStream::Reader& data); void SetCommandDataStream(const Napi::CallbackInfo& info); void SubmitCommands(const Napi::CallbackInfo& info); - void DrawInternal(bgfx::Encoder* encoder, uint32_t fillMode); + 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{}; + JsRuntimeScheduler m_runtimeScheduler; 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}; diff --git a/Plugins/NativeInput/Source/Shared/NativeInput.cpp b/Plugins/NativeInput/Source/Shared/NativeInput.cpp index 026efb85b..63427894c 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,17 +152,10 @@ 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, - POINTER_X_INPUT_INDEX, - POINTER_Y_INPUT_INDEX, - POINTER_DELTA_HORIZONTAL_INDEX, - POINTER_DELTA_VERTICAL_INDEX, - })}; + 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); @@ -177,17 +170,10 @@ 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, - POINTER_X_INPUT_INDEX, - POINTER_Y_INPUT_INDEX, - POINTER_DELTA_HORIZONTAL_INDEX, - POINTER_DELTA_VERTICAL_INDEX, - })}; + 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); @@ -211,16 +197,9 @@ 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]() { - 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, - })}; + 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})}; int32_t deltaX = 0; int32_t deltaY = 0; @@ -251,7 +230,7 @@ namespace Babylon::Plugins void NativeInput::Impl::PointerScroll(uint32_t pointerId, uint32_t scrollAxis, int32_t scrollValue, DeviceType deviceType) { - m_runtimeScheduler([pointerId, scrollAxis, scrollValue, deviceType, this]() { + 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/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 b732fbfda..7b50f1ba6 100644 --- a/Plugins/NativeXr/Source/NativeXr.cpp +++ b/Plugins/NativeXr/Source/NativeXr.cpp @@ -410,7 +410,6 @@ namespace Babylon private: Napi::Env m_env; - JsRuntimeScheduler m_runtimeScheduler; std::mutex m_sessionStateChangedCallbackMutex{}; std::function m_sessionStateChangedCallback{}; void* m_windowPtr{}; @@ -459,6 +458,8 @@ namespace Babylon void EndFrame(); void NotifySessionStateChanged(bool isSessionActive); + + JsRuntimeScheduler m_runtimeScheduler; }; NativeXr::Impl::Impl(Napi::Env env) @@ -584,7 +585,7 @@ 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(); @@ -621,7 +622,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()) { @@ -694,7 +695,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); @@ -2716,7 +2717,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()) { @@ -3160,7 +3161,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()) { @@ -3426,11 +3427,9 @@ 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 df8ed9836..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); } @@ -52,21 +54,25 @@ namespace Babylon::Polyfills::Internal Napi::Value NativeCanvas::LoadTTFAsync(const Napi::CallbackInfo& info) { + 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 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 = std::move(deferred)]() mutable { fontsInfos[fontName] = fontData; - }).then(*runtimeScheduler, arcana::cancellation::none(), [runtimeScheduler /*Keep reference alive*/, env{info.Env()}, deferred]() { - deferred.Resolve(env.Undefined()); + 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) diff --git a/Polyfills/Canvas/Source/Context.cpp b/Polyfills/Canvas/Source/Context.cpp index e47bc5e7d..c7b4a419c 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,9 @@ namespace Babylon::Polyfills::Internal Context::~Context() { Dispose(); - m_cancellationSource->cancel(); + + // Wait for async operations to complete. + m_runtimeScheduler.Rundown(); } void Context::Dispose(const Napi::CallbackInfo&) @@ -120,12 +121,15 @@ namespace Babylon::Polyfills::Internal void Context::Dispose() { + m_cancellationSource.cancel(); + if (m_nvg) { for (auto& image : m_nvgImageIndices) { nvgDeleteImage(m_nvg, image.second); } + nvgDelete(m_nvg); m_nvg = nullptr; } @@ -150,7 +154,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&) @@ -163,7 +167,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&) @@ -176,7 +180,7 @@ 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&) @@ -184,29 +188,29 @@ namespace Babylon::Polyfills::Internal 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()); m_isClipped = false; } @@ -235,7 +239,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) @@ -243,14 +247,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) @@ -258,19 +262,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) @@ -282,7 +286,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*/) @@ -306,13 +310,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) @@ -321,7 +325,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) @@ -330,7 +334,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) @@ -341,7 +345,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) @@ -368,27 +372,27 @@ 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(); - 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}]() { + 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(); bgfx::Encoder* encoder = m_update.GetUpdateToken().GetEncoder(); @@ -406,8 +410,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, thisRef = std::move(thisRef)](const arcana::expected& result) { + if (result.has_error()) { Napi::Error::New(Env(), result.error()).ThrowAsJavaScriptException(); } @@ -429,7 +433,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) @@ -466,7 +470,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) { @@ -485,7 +489,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) { @@ -510,7 +514,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 7692e82a7..edfa4f40b 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; @@ -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{}; @@ -98,7 +98,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 72858bfda..f182b0f73 100644 --- a/Polyfills/Canvas/Source/Image.cpp +++ b/Polyfills/Canvas/Source/Image.cpp @@ -41,13 +41,15 @@ namespace Babylon::Polyfills::Internal NativeCanvasImage::NativeCanvasImage(const Napi::CallbackInfo& info) : Napi::ObjectWrap{info} , m_runtimeScheduler{JsRuntime::GetFromJavaScript(info.Env())} - , m_cancellationSource{std::make_shared()} { } NativeCanvasImage::~NativeCanvasImage() { Dispose(); + + // Wait for async operations to complete. + m_runtimeScheduler.Rundown(); } void NativeCanvasImage::Dispose() @@ -57,7 +59,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,38 +106,41 @@ 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) { - 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; - } - - m_imageContainer = bimg::imageParse(&m_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({}); - } - }); + 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(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) diff --git a/Polyfills/Canvas/Source/Image.h b/Polyfills/Canvas/Source/Image.h index a4b41adb0..5f1dff729 100644 --- a/Polyfills/Canvas/Source/Image.h +++ b/Polyfills/Canvas/Source/Image.h @@ -43,11 +43,11 @@ 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{}; bimg::ImageContainer* m_imageContainer{}; + + arcana::cancellation_source m_cancellationSource{}; + JsRuntimeScheduler m_runtimeScheduler; }; } diff --git a/Polyfills/Window/Source/TimeoutDispatcher.cpp b/Polyfills/Window/Source/TimeoutDispatcher.cpp index e4cca0f0a..524cd835a 100644 --- a/Polyfills/Window/Source/TimeoutDispatcher.cpp +++ b/Polyfills/Window/Source/TimeoutDispatcher.cpp @@ -14,28 +14,8 @@ 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_runtime{runtime} + : m_runtimeScheduler{runtime} , m_thread{std::thread{&TimeoutDispatcher::ThreadFunction, this}} { } @@ -43,17 +23,20 @@ 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) + TimeoutId TimeoutDispatcher::Dispatch(std::shared_ptr function, std::chrono::milliseconds delay) { if (delay.count() < 0) { @@ -70,7 +53,7 @@ namespace Babylon::Polyfills::Internal if (time <= earliestTime) { - m_runtime.Dispatch([this](Napi::Env) { + m_runtimeScheduler.Get()([this]() { m_condVariable.notify_one(); }); } @@ -102,7 +85,7 @@ namespace Babylon::Polyfills::Internal } } - TimeoutDispatcher::TimeoutId TimeoutDispatcher::NextTimeoutId() + TimeoutId TimeoutDispatcher::NextTimeoutId() { while (true) { @@ -122,11 +105,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; @@ -135,7 +118,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 +130,9 @@ namespace Babylon::Polyfills::Internal CallFunction(std::move(function)); } - while (!m_shutdown && m_timeMap.empty()) + while (!m_cancellationSource.cancelled() && m_timeMap.empty()) { - m_condVariable.wait(lk); + m_condVariable.wait(lock); } } } @@ -158,7 +141,9 @@ namespace Babylon::Polyfills::Internal { if (function) { - m_runtime.Dispatch([function = std::move(function)](Napi::Env) { function->Call({}); }); + 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..c92a4da90 100644 --- a/Polyfills/Window/Source/TimeoutDispatcher.h +++ b/Polyfills/Window/Source/TimeoutDispatcher.h @@ -1,6 +1,6 @@ #pragma once -#include +#include #include #include @@ -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,17 +27,38 @@ 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); - Babylon::JsRuntime& m_runtime; - 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 76788b225..73cf998dd 100644 --- a/Polyfills/Window/Source/Window.cpp +++ b/Polyfills/Window/Source/Window.cpp @@ -72,8 +72,7 @@ namespace Babylon::Polyfills::Internal Window::Window(const Napi::CallbackInfo& info) : Napi::ObjectWrap{info} - , m_runtime{JsRuntime::GetFromJavaScript(info.Env())} - , m_timeoutDispatcher{m_runtime} + , m_timeoutDispatcher{JsRuntime::GetFromJavaScript(info.Env())} { } @@ -84,7 +83,7 @@ namespace Babylon::Polyfills::Internal 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) @@ -94,7 +93,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 03f7dd572..1690c0e70 100644 --- a/Polyfills/Window/Source/Window.h +++ b/Polyfills/Window/Source/Window.h @@ -1,11 +1,8 @@ #pragma once +#include #include "TimeoutDispatcher.h" -#include - -#include - namespace Babylon::Polyfills::Internal { class Window : public Napi::ObjectWrap @@ -19,14 +16,13 @@ namespace Babylon::Polyfills::Internal Window(const Napi::CallbackInfo& info); private: - JsRuntime& m_runtime; - std::optional m_timeoutDispatcher; - static Napi::Value SetTimeout(const Napi::CallbackInfo& info); static void ClearTimeout(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); + + TimeoutDispatcher m_timeoutDispatcher; }; } diff --git a/Polyfills/XMLHttpRequest/Source/XMLHttpRequest.cpp b/Polyfills/XMLHttpRequest/Source/XMLHttpRequest.cpp index 972d8cf5d..9c3dd488d 100644 --- a/Polyfills/XMLHttpRequest/Source/XMLHttpRequest.cpp +++ b/Polyfills/XMLHttpRequest/Source/XMLHttpRequest.cpp @@ -3,59 +3,59 @@ #include #include -namespace Babylon::Polyfills::Internal +namespace { - namespace + namespace ResponseType { - namespace ResponseType - { - constexpr const char* Text = "text"; - constexpr const char* ArrayBuffer = "arraybuffer"; + 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) - { - switch (value) - { - case UrlLib::UrlResponseType::String: - return Text; - case UrlLib::UrlResponseType::Buffer: - return 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{"Invalid response type"}; - } + throw std::runtime_error{"Unsupported response type: " + value}; } - namespace MethodType + const char* EnumToString(UrlLib::UrlResponseType value) { - constexpr const char* Get = "GET"; - - UrlLib::UrlMethod StringToEnum(const std::string& value) + switch (value) { - if (value == Get) - return UrlLib::UrlMethod::Get; - - throw std::runtime_error{"Unsupported url method: " + value}; + case UrlLib::UrlResponseType::String: + return Text; + case UrlLib::UrlResponseType::Buffer: + return ArrayBuffer; } + + throw std::runtime_error{"Invalid response type"}; } + } + + namespace MethodType + { + constexpr const char* Get = "GET"; - namespace EventType + UrlLib::UrlMethod StringToEnum(const std::string& value) { - constexpr const char* ReadyStateChange = "readystatechange"; - constexpr const char* LoadEnd = "loadend"; + if (value == Get) + return UrlLib::UrlMethod::Get; + + throw std::runtime_error{"Unsupported url method: " + value}; } } + namespace EventType + { + constexpr const char* ReadyStateChange = "readystatechange"; + constexpr const char* LoadEnd = "loadend"; + } +} + +namespace Babylon::Polyfills::Internal +{ void XMLHttpRequest::Initialize(Napi::Env env) { Napi::HandleScope scope{env}; @@ -99,6 +99,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)); @@ -183,6 +192,7 @@ namespace Babylon::Polyfills::Internal void XMLHttpRequest::Abort(const Napi::CallbackInfo&) { m_request.Abort(); + m_cancellationSource.cancel(); } void XMLHttpRequest::Open(const Napi::CallbackInfo& info) @@ -212,20 +222,22 @@ namespace Babylon::Polyfills::Internal throw Napi::Error::New(info.Env(), "XMLHttpRequest must be opened before it can be sent"); } - m_request.SendAsync().then(m_runtimeScheduler, arcana::cancellation::none(), [env{info.Env()}, this](arcana::expected result) { - if (result.has_error()) - { - Napi::Error::New(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(); - }); + 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(); + }); } void XMLHttpRequest::SetReadyState(ReadyState readyState) diff --git a/Polyfills/XMLHttpRequest/Source/XMLHttpRequest.h b/Polyfills/XMLHttpRequest/Source/XMLHttpRequest.h index c2e495a1f..1e66906bd 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 @@ -42,8 +43,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; }; }