diff --git a/Apps/UnitTests/Shared/Shared.cpp b/Apps/UnitTests/Shared/Shared.cpp index ad1bb8b67..91937ccdc 100644 --- a/Apps/UnitTests/Shared/Shared.cpp +++ b/Apps/UnitTests/Shared/Shared.cpp @@ -98,43 +98,6 @@ TEST(JavaScript, All) EXPECT_EQ(exitCode, 0); } -/* -This test does a serie of initialization and shutdowns. -It needs the shutdown PR to be merged before running properly. -TEST(NativeAPI, LifeCycle) -{ - for (int cycle = 0; cycle < 20; cycle++) - { - Babylon::Graphics::Device device{deviceConfig}; - std::optional nativeCanvas; - - Babylon::AppRuntime runtime{}; - runtime.Dispatch([&device, &nativeCanvas](Napi::Env env) { - device.AddToJavaScript(env); - - Babylon::Polyfills::XMLHttpRequest::Initialize(env); - Babylon::Polyfills::Console::Initialize(env, [](const char* message, auto) { - printf("%s", message); - fflush(stdout); - }); - Babylon::Polyfills::Window::Initialize(env); - nativeCanvas.emplace(Babylon::Polyfills::Canvas::Initialize(env)); - Babylon::Plugins::NativeEngine::Initialize(env); - }); - - Babylon::ScriptLoader loader{runtime}; - loader.LoadScript("app:///Scripts/babylon.max.js"); - loader.LoadScript("app:///Scripts/babylonjs.materials.js"); - - for (int frame = 0; frame < 10; frame++) - { - device.StartRenderingCurrentFrame(); - device.FinishRenderingCurrentFrame(); - } - } -} -*/ - TEST(Performance, Spheres) { // create a bunch of sphere, does the rendering for a number of frames, log time it took @@ -212,6 +175,70 @@ TEST(Performance, Spheres) std::cout.flush(); } +TEST(Shutdown, AsyncShaderCompilation) +{ + std::promise exitCodePromise; + Babylon::Graphics::Device device{ deviceConfig }; + auto update{ device.GetUpdate("update") }; + std::optional nativeCanvas; + Babylon::AppRuntime runtime{}; + + // OpenGL/Linux needs to render 1 frame to set bgfx caps. More infos here : https://github.com/BabylonJS/BabylonNative/issues/1163 + device.StartRenderingCurrentFrame(); + update.Start(); + update.Finish(); + device.FinishRenderingCurrentFrame(); + + runtime.Dispatch([&device, &nativeCanvas, &exitCodePromise](Napi::Env env) { + device.AddToJavaScript(env); + Babylon::Polyfills::XMLHttpRequest::Initialize(env); + Babylon::Polyfills::Console::Initialize(env, [](const char* message, auto) { + std::cout << message << std::endl; + std::cout.flush(); + }); + Babylon::Polyfills::Window::Initialize(env); + nativeCanvas.emplace(Babylon::Polyfills::Canvas::Initialize(env)); + Babylon::Plugins::NativeEngine::Initialize(env); + auto setExitCodeCallback = Napi::Function::New( + env, [&exitCodePromise](const Napi::CallbackInfo& info) { + Napi::Env env = info.Env(); + exitCodePromise.set_value(info[0].As().Int32Value()); + }, "setExitCode"); + env.Global().Set("setExitCode", setExitCodeCallback); + }); + Babylon::ScriptLoader loader{ runtime }; + loader.LoadScript("app:///Scripts/babylon.max.js"); + loader.LoadScript("app:///Scripts/babylonjs.materials.js"); + loader.Eval(R"( + function CreateBoxAsync(scene) { + BABYLON.Mesh.CreateBox("box1", 0.2, scene); + return Promise.resolve(); + } + var engine = new BABYLON.NativeEngine(); + var scene = new BABYLON.Scene(engine); + CreateBoxAsync(scene).then(function () { + var disc = BABYLON.Mesh.CreateDisc("disc", 3, 60, scene); + scene.createDefaultCamera(true, true, true); + scene.activeCamera.alpha += Math.PI; + scene.createDefaultLight(true); + var mainMaterial = new BABYLON.PBRMaterial("main", scene); + disc.material = mainMaterial; + engine.runRenderLoop(function () { + scene.render(); + }); + console.log("Ready to shutdown."); + setExitCode(1); + }, function (ex) { + console.log(ex.message, ex.stack); + }); + )", "script"); + auto exitCode{ exitCodePromise.get_future().get() }; + device.StartRenderingCurrentFrame(); + update.Start(); + update.Finish(); + device.FinishRenderingCurrentFrame(); +} + int RunTests(const Babylon::Graphics::Configuration& config) { deviceConfig = config; diff --git a/CMakeLists.txt b/CMakeLists.txt index b1cd1915c..5f10fc027 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -36,8 +36,8 @@ FetchContent_Declare(ios-cmake GIT_REPOSITORY https://github.com/leetal/ios-cmake.git GIT_TAG 4.4.1) FetchContent_Declare(JsRuntimeHost - GIT_REPOSITORY https://github.com/BabylonJS/JsRuntimeHost.git - GIT_TAG 9ccb9477a97501525f3f9ca0e00e6971934037ec) + GIT_REPOSITORY https://github.com/cedricguillemet/JsRuntimeHost.git + GIT_TAG 05105aed423084a3581ad6ea8cfaa3b2c2d1641f) FetchContent_Declare(OpenXR-MixedReality GIT_REPOSITORY https://github.com/microsoft/OpenXR-MixedReality.git GIT_TAG 67424511525b96a36847f2a96d689d99e5879503) diff --git a/Plugins/NativeCamera/Source/CameraDevice.h b/Plugins/NativeCamera/Source/CameraDevice.h index d644623b6..2d5798042 100644 --- a/Plugins/NativeCamera/Source/CameraDevice.h +++ b/Plugins/NativeCamera/Source/CameraDevice.h @@ -7,6 +7,16 @@ #include #include "Capability.h" +#ifdef _MSC_VER +#define DISABLE_UNREACHABLE_CODE_WARNINGS \ + __pragma(warning(push)) \ + __pragma(warning(disable : 4702)) +#define ENABLE_UNREACHABLE_CODE_WARNINGS __pragma(warning(pop)) +#else +#define DISABLE_UNREACHABLE_CODE_WARNINGS +#define ENABLE_UNREACHABLE_CODE_WARNINGS +#endif + namespace Babylon::Plugins { enum class RedEyeReduction diff --git a/Plugins/NativeCamera/Source/ImageCapture.cpp b/Plugins/NativeCamera/Source/ImageCapture.cpp index 4e7431afb..b2655c9e1 100644 --- a/Plugins/NativeCamera/Source/ImageCapture.cpp +++ b/Plugins/NativeCamera/Source/ImageCapture.cpp @@ -4,16 +4,6 @@ #include #include -#ifdef _MSC_VER - #define DISABLE_UNREACHABLE_CODE_WARNINGS \ - __pragma(warning(push)) \ - __pragma(warning(disable : 4702)) - #define ENABLE_UNREACHABLE_CODE_WARNINGS __pragma(warning(pop)) -#else - #define DISABLE_UNREACHABLE_CODE_WARNINGS - #define ENABLE_UNREACHABLE_CODE_WARNINGS -#endif - namespace Babylon::Plugins::Internal { namespace @@ -169,7 +159,7 @@ DISABLE_UNREACHABLE_CODE_WARNINGS bytes.resize(result.size()); std::memcpy(bytes.data(), result.data(), result.size()); return bytes; - }).then(m_runtimeScheduler, arcana::cancellation::none(), [env, deferred](const arcana::expected, std::exception_ptr>& result) { + }).then(m_runtimeScheduler.Get(), arcana::cancellation::none(), [env, deferred](const arcana::expected, std::exception_ptr>& result) { if (result.has_error()) { deferred.Reject(Napi::Error::New(env, result.error()).Value()); diff --git a/Plugins/NativeCamera/Source/MediaDevices.cpp b/Plugins/NativeCamera/Source/MediaDevices.cpp index 7469061b5..4f8b2e4b5 100644 --- a/Plugins/NativeCamera/Source/MediaDevices.cpp +++ b/Plugins/NativeCamera/Source/MediaDevices.cpp @@ -37,9 +37,8 @@ namespace Babylon::Plugins::Internal videoConstraints = constraints.Get("video").As(); } } - - 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) { +DISABLE_UNREACHABLE_CODE_WARNINGS + 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()); @@ -48,7 +47,7 @@ namespace Babylon::Plugins::Internal deferred.Resolve(result.value()); }); - +ENABLE_UNREACHABLE_CODE_WARNINGS return std::move(promise); } diff --git a/Plugins/NativeCamera/Source/MediaStream.cpp b/Plugins/NativeCamera/Source/MediaStream.cpp index 583a1ce8d..3742aa07f 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,18 @@ 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)]() {}); } + m_runtimeScheduler.Rundown(); } Napi::Value MediaStream::GetVideoTracks(const Napi::CallbackInfo& info) @@ -120,7 +125,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(), arcana::cancellation::none(), [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 74593a74c..0f639facf 100644 --- a/Plugins/NativeCamera/Source/MediaStream.h +++ b/Plugins/NativeCamera/Source/MediaStream.h @@ -53,5 +53,6 @@ namespace Babylon::Plugins JsRuntimeScheduler m_runtimeScheduler; Napi::ObjectReference m_currentConstraints{}; + arcana::cancellation_source m_cancellationSource; }; } diff --git a/Plugins/NativeCamera/Source/NativeVideo.cpp b/Plugins/NativeCamera/Source/NativeVideo.cpp index 5198f357d..731baeecd 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 dbb46caf9..b46cff684 100644 --- a/Plugins/NativeEngine/Source/NativeEngine.cpp +++ b/Plugins/NativeEngine/Source/NativeEngine.cpp @@ -710,14 +710,13 @@ namespace Babylon NativeEngine::NativeEngine(const Napi::CallbackInfo& info, JsRuntime& runtime) : Napi::ObjectWrap{info} - , m_cancellationSource{std::make_shared()} , m_runtime{runtime} , m_deviceContext{Graphics::DeviceContext::GetFromJavaScript(info.Env())} , m_update{m_deviceContext.GetUpdate("update")} , m_runtimeScheduler{runtime} , m_defaultFrameBuffer{m_deviceContext, BGFX_INVALID_HANDLE, 0, 0, true, true, true} , m_boundFrameBuffer{&m_defaultFrameBuffer} - , m_boundFrameBufferNeedsRebinding{m_deviceContext, *m_cancellationSource, true} + , m_boundFrameBufferNeedsRebinding{m_deviceContext, m_cancellationSource, true} { // Set features supported by the NativeEngine from Babylon.js. if (!info[0].IsUndefined()) @@ -730,13 +729,15 @@ namespace Babylon NativeEngine::~NativeEngine() { Dispose(); + // Wait for async operations to complete. + m_runtimeScheduler.Rundown(); } void NativeEngine::Dispose() { m_deviceContext.SetRenderResetCallback(nullptr); - m_cancellationSource->cancel(); + m_cancellationSource.cancel(); } void NativeEngine::Dispose(const Napi::CallbackInfo& /*info*/) @@ -1029,16 +1030,15 @@ namespace Babylon ProgramData* program = new ProgramData{m_deviceContext}; 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]() -> std::unique_ptr { return CreateProgramInternal(vertexSource, fragmentSource); }) - .then(m_runtimeScheduler, *m_cancellationSource, + .then(m_runtimeScheduler.Get(), 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) { + 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()}); @@ -1389,13 +1389,13 @@ 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, - [dataSpan, generateMips, invertY, srgb, texture, cancellationSource{m_cancellationSource}]() { + arcana::make_task(arcana::threadpool_scheduler, m_cancellationSource, + [dataSpan, generateMips, invertY, srgb, texture]() { bimg::ImageContainer* image{ParseImage(Graphics::DeviceContext::GetDefaultAllocator(), dataSpan)}; image = PrepareImage(Graphics::DeviceContext::GetDefaultAllocator(), 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) { + .then(m_runtimeScheduler.Get(), m_cancellationSource, [dataRef{Napi::Persistent(data)}, onSuccessRef{Napi::Persistent(onSuccess)}, onErrorRef{Napi::Persistent(onError)}](arcana::expected result) { if (result.has_error()) { onErrorRef.Call({}); @@ -1412,12 +1412,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]() { + 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 = m_update.GetUpdateToken().GetEncoder(); GetBoundFrameBuffer(*encoder).Blit(*encoder, textureDestination->Handle(), 0, 0, textureSource->Handle()); - }).then(arcana::inline_scheduler, *m_cancellationSource, [this, cancellationSource{m_cancellationSource}](const arcana::expected& result) { - if (!cancellationSource->cancelled() && result.has_error()) + }).then(arcana::inline_scheduler, m_cancellationSource, [this](const arcana::expected& result) { + if (result.has_error()) { Napi::Error::New(Env(), result.error()).ThrowAsJavaScriptException(); } @@ -1508,7 +1508,7 @@ 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, [dataSpan, invertY, generateMips, srgb]() { + tasks[face] = arcana::make_task(arcana::threadpool_scheduler, m_cancellationSource, [dataSpan, invertY, generateMips, srgb]() { bimg::ImageContainer* image{ParseImage(Graphics::DeviceContext::GetDefaultAllocator(), dataSpan)}; image = PrepareImage(Graphics::DeviceContext::GetDefaultAllocator(), image, invertY, srgb, generateMips); return image; @@ -1516,10 +1516,10 @@ namespace Babylon } 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, [dataRefs{std::move(dataRefs)}, onSuccessRef{Napi::Persistent(onSuccess)}, onErrorRef{Napi::Persistent(onError)}](arcana::expected result) { if (result.has_error()) { onErrorRef.Call({}); @@ -1551,7 +1551,7 @@ 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, [dataSpan, invertY, srgb]() { + tasks[(face * numMips) + mip] = arcana::make_task(arcana::threadpool_scheduler, m_cancellationSource, [dataSpan, invertY, srgb]() { bimg::ImageContainer* image{ParseImage(Graphics::DeviceContext::GetDefaultAllocator(), dataSpan)}; image = PrepareImage(Graphics::DeviceContext::GetDefaultAllocator(), image, invertY, srgb, false); return image; @@ -1560,10 +1560,10 @@ namespace Babylon } 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, [dataRefs{std::move(dataRefs)}, onSuccessRef{Napi::Persistent(onSuccess)}, onErrorRef{Napi::Persistent(onError)}](arcana::expected result) { if (result.has_error()) { onErrorRef.Call({}); @@ -1744,7 +1744,7 @@ namespace Babylon // Read the source texture. m_deviceContext.ReadTextureAsync(sourceTextureHandle, textureBuffer, mipLevel) - .then(arcana::inline_scheduler, *m_cancellationSource, [textureBuffer{std::move(textureBuffer)}, sourceTextureInfo, targetTextureInfo]() mutable { + .then(arcana::inline_scheduler, m_cancellationSource, [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) { @@ -1767,7 +1767,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()); @@ -1777,7 +1777,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; @@ -1785,10 +1785,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); } @@ -2348,8 +2348,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"}; @@ -2358,8 +2358,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 0bba88d64..28294a3a7 100644 --- a/Plugins/NativeEngine/Source/NativeEngine.h +++ b/Plugins/NativeEngine/Source/NativeEngine.h @@ -228,7 +228,7 @@ namespace Babylon Graphics::UpdateToken& GetUpdateToken(); Graphics::FrameBuffer& GetBoundFrameBuffer(bgfx::Encoder& encoder); - std::shared_ptr m_cancellationSource{}; + arcana::cancellation_source m_cancellationSource{}; ShaderCompiler m_shaderCompiler{}; diff --git a/Plugins/NativeInput/Source/Shared/NativeInput.cpp b/Plugins/NativeInput/Source/Shared/NativeInput.cpp index 23f004dd6..c122275af 100644 --- a/Plugins/NativeInput/Source/Shared/NativeInput.cpp +++ b/Plugins/NativeInput/Source/Shared/NativeInput.cpp @@ -89,7 +89,7 @@ namespace Babylon::Plugins } NativeInput::Impl::Impl(Napi::Env env) - : m_runtimeScheduler{JsRuntime::GetFromJavaScript(env)} + : m_runtime{JsRuntime::GetFromJavaScript(env)} { NativeInput::Impl::DeviceInputSystem::Initialize(env); @@ -152,7 +152,7 @@ namespace Babylon::Plugins void NativeInput::Impl::PointerDown(uint32_t pointerId, uint32_t buttonIndex, int32_t x, int32_t y, DeviceType deviceType) { - m_runtimeScheduler([pointerId, buttonIndex, x, y, deviceType, this]() { + m_runtime.Dispatch([pointerId, buttonIndex, x, y, deviceType, this](auto) { const uint32_t inputIndex{GetPointerButtonInputIndex(buttonIndex)}; std::vector& deviceInputs{ GetOrCreateInputMap(deviceType, pointerId, @@ -177,7 +177,7 @@ namespace Babylon::Plugins void NativeInput::Impl::PointerUp(uint32_t pointerId, uint32_t buttonIndex, int32_t x, int32_t y, DeviceType deviceType) { - m_runtimeScheduler([pointerId, buttonIndex, x, y, deviceType, this]() { + m_runtime.Dispatch([pointerId, buttonIndex, x, y, deviceType, this](auto) { const uint32_t inputIndex{GetPointerButtonInputIndex(buttonIndex)}; std::vector& deviceInputs{ GetOrCreateInputMap(deviceType, pointerId, @@ -211,7 +211,7 @@ namespace Babylon::Plugins void NativeInput::Impl::PointerMove(uint32_t pointerId, int32_t x, int32_t y, DeviceType deviceType) { - m_runtimeScheduler([pointerId, x, y, deviceType, this]() { + m_runtime.Dispatch([pointerId, x, y, deviceType, this](auto) { std::vector& deviceInputs{ GetOrCreateInputMap(deviceType, pointerId, { @@ -251,7 +251,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/NativeXrImpl.cpp b/Plugins/NativeXr/Source/NativeXrImpl.cpp index ab99d7514..846b0b20d 100644 --- a/Plugins/NativeXr/Source/NativeXrImpl.cpp +++ b/Plugins/NativeXr/Source/NativeXrImpl.cpp @@ -165,7 +165,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(); @@ -202,7 +202,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()) { @@ -275,7 +275,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); diff --git a/Plugins/NativeXr/Source/XR.h b/Plugins/NativeXr/Source/XR.h index 3214e4006..a15061bae 100644 --- a/Plugins/NativeXr/Source/XR.h +++ b/Plugins/NativeXr/Source/XR.h @@ -81,7 +81,7 @@ namespace Babylon // Fire off the IsSessionSupported task. xr::System::IsSessionSupportedAsync(sessionType) - .then(m_runtimeScheduler, + .then(m_runtimeScheduler.Get(), arcana::cancellation::none(), [deferred, env = info.Env()](bool result) { deferred.Resolve(Napi::Boolean::New(env, result)); diff --git a/Plugins/NativeXr/Source/XRSession.cpp b/Plugins/NativeXr/Source/XRSession.cpp index ce5b486a9..9bc5fa0db 100644 --- a/Plugins/NativeXr/Source/XRSession.cpp +++ b/Plugins/NativeXr/Source/XRSession.cpp @@ -279,7 +279,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()) { @@ -673,7 +673,7 @@ namespace Babylon Napi::Value XRSession::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()) { diff --git a/Polyfills/Canvas/Source/Canvas.cpp b/Polyfills/Canvas/Source/Canvas.cpp index 03d9eb02f..270088f19 100644 --- a/Polyfills/Canvas/Source/Canvas.cpp +++ b/Polyfills/Canvas/Source/Canvas.cpp @@ -22,14 +22,16 @@ 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), InstanceMethod("remove", &NativeCanvas::Remove), - StaticMethod("parseColor", &NativeCanvas::ParseColor)}); + StaticMethod("parseColor", &NativeCanvas::ParseColor) + }); JsRuntime::NativeObject::GetFromJavaScript(env).Set(JS_CONSTRUCTOR_NAME, func); } @@ -58,21 +60,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 6a6931250..57db931a1 100644 --- a/Polyfills/Canvas/Source/Context.cpp +++ b/Polyfills/Canvas/Source/Context.cpp @@ -98,9 +98,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())} { @@ -113,7 +112,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&) @@ -128,6 +129,7 @@ namespace Babylon::Polyfills::Internal void Context::Dispose() { + m_cancellationSource.cancel(); if (m_nvg) { for (auto& image : m_nvgImageIndices) @@ -158,7 +160,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&) @@ -171,7 +173,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&) @@ -184,7 +186,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&) @@ -192,29 +194,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; } @@ -243,7 +245,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) @@ -251,14 +253,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) @@ -266,19 +268,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) @@ -290,7 +292,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*/) @@ -314,13 +316,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) @@ -329,7 +331,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) @@ -338,7 +340,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) @@ -349,7 +351,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) @@ -376,27 +378,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(); @@ -414,8 +416,8 @@ namespace Babylon::Polyfills::Internal nvgEndFrame(m_nvg); frameBuffer.Unbind(*encoder); m_dirty = false; - }).then(arcana::inline_scheduler, *m_cancellationSource, [this, cancellationSource{m_cancellationSource}](const arcana::expected& result) { - if (!cancellationSource->cancelled() && result.has_error()) + }).then(arcana::inline_scheduler, m_cancellationSource, [this](const arcana::expected& result) { + if (result.has_error()) { Napi::Error::New(Env(), result.error()).ThrowAsJavaScriptException(); } @@ -437,7 +439,7 @@ namespace Babylon::Polyfills::Internal const auto endAngle = static_cast(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) @@ -474,7 +476,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) { @@ -493,7 +495,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) { @@ -518,7 +520,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 89dd3da86..c70c8f252 100644 --- a/Polyfills/Canvas/Source/Image.cpp +++ b/Polyfills/Canvas/Source/Image.cpp @@ -42,13 +42,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() @@ -58,7 +60,7 @@ namespace Babylon::Polyfills::Internal bimg::imageFree(m_imageContainer); m_imageContainer = nullptr; } - m_cancellationSource->cancel(); + m_cancellationSource.cancel(); } Napi::Value NativeCanvasImage::GetWidth(const Napi::CallbackInfo&) @@ -126,7 +128,7 @@ namespace Babylon::Polyfills::Internal const auto pos = text.find(base64); if (pos != std::string::npos) { - arcana::make_task(m_runtimeScheduler, *m_cancellationSource, [env{info.Env()}, this, text{std::move(text)}, pos]() { + arcana::make_task(m_runtimeScheduler.Get(), m_cancellationSource, [env{info.Env()}, this, text{std::move(text)}, pos]() { std::vector base64Buffer; bn::decode_b64(text.begin() + pos + base64.length(), text.end(), std::back_inserter(base64Buffer)); gsl::span buffer = {reinterpret_cast(base64Buffer.data()), base64Buffer.size()}; @@ -143,10 +145,10 @@ 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, cancellationSource{m_cancellationSource}, request{std::move(request)}, text](arcana::expected result) { + 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())); + HandleLoadImageError(Napi::Error::New(Env(), result.error())); return; } @@ -155,13 +157,13 @@ namespace Babylon::Polyfills::Internal auto buffer{request.ResponseBuffer()}; if (buffer.data() == nullptr || buffer.size_bytes() == 0) { - HandleLoadImageError(Napi::Error::New(env, "Image with provided source returned empty response or invalid base64.")); + HandleLoadImageError(Napi::Error::New(Env(), "Image with provided source returned empty response or invalid base64.")); return; } if (!SetBuffer(buffer)) { - HandleLoadImageError(Napi::Error::New(env, "Unable to decode image with provided source URL.")); + HandleLoadImageError(Napi::Error::New(Env(), "Unable to decode image with provided source URL.")); } }); } diff --git a/Polyfills/Canvas/Source/Image.h b/Polyfills/Canvas/Source/Image.h index 5cd62aed0..21b62b922 100644 --- a/Polyfills/Canvas/Source/Image.h +++ b/Polyfills/Canvas/Source/Image.h @@ -47,7 +47,7 @@ namespace Babylon::Polyfills::Internal JsRuntimeScheduler m_runtimeScheduler; Napi::FunctionReference m_onloadHandlerRef; Napi::FunctionReference m_onerrorHandlerRef; - std::shared_ptr m_cancellationSource{}; + arcana::cancellation_source m_cancellationSource{}; bimg::ImageContainer* m_imageContainer{}; }; } diff --git a/Polyfills/Window/Source/TimeoutDispatcher.cpp b/Polyfills/Window/Source/TimeoutDispatcher.cpp index e4cca0f0a..672f94f4c 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,14 +23,16 @@ 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(); + + m_runtimeScheduler.Rundown(); } TimeoutDispatcher::TimeoutId TimeoutDispatcher::Dispatch(std::shared_ptr function, std::chrono::milliseconds delay) @@ -60,7 +42,7 @@ namespace Babylon::Polyfills::Internal delay = std::chrono::milliseconds{0}; } - std::unique_lock lk{m_mutex}; + std::unique_lock lock{m_mutex}; const auto id = NextTimeoutId(); const auto earliestTime = m_timeMap.empty() ? TimePoint::max() : m_timeMap.cbegin()->second->time; @@ -70,7 +52,7 @@ namespace Babylon::Polyfills::Internal if (time <= earliestTime) { - m_runtime.Dispatch([this](Napi::Env) { + m_runtimeScheduler.Get()([this]() { m_condVariable.notify_one(); }); } @@ -80,7 +62,7 @@ namespace Babylon::Polyfills::Internal void TimeoutDispatcher::Clear(TimeoutId id) { - std::unique_lock lk{m_mutex}; + std::unique_lock lock{m_mutex}; const auto itId = m_idMap.find(id); if (itId != m_idMap.end()) { @@ -122,9 +104,9 @@ namespace Babylon::Polyfills::Internal void TimeoutDispatcher::ThreadFunction() { - while (!m_shutdown) + while (!m_cancellationSource.cancelled()) { - std::unique_lock lk{m_mutex}; + std::unique_lock lock{m_mutex}; TimePoint nextTimePoint{}; while (!m_timeMap.empty()) @@ -135,7 +117,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 +129,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 +140,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..383817664 100644 --- a/Polyfills/Window/Source/TimeoutDispatcher.h +++ b/Polyfills/Window/Source/TimeoutDispatcher.h @@ -1,6 +1,6 @@ #pragma once -#include +#include #include #include @@ -16,7 +16,6 @@ namespace Babylon::Polyfills::Internal class TimeoutDispatcher { using TimeoutId = int32_t; - struct Timeout; public: TimeoutDispatcher(Babylon::JsRuntime& runtime); @@ -28,17 +27,37 @@ 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; + Babylon::JsRuntimeScheduler m_runtimeScheduler; 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 02de9586e..a048dbf38 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..4690d2cbf 100644 --- a/Polyfills/Window/Source/Window.h +++ b/Polyfills/Window/Source/Window.h @@ -2,10 +2,6 @@ #include "TimeoutDispatcher.h" -#include - -#include - namespace Babylon::Polyfills::Internal { class Window : public Napi::ObjectWrap @@ -19,8 +15,7 @@ namespace Babylon::Polyfills::Internal Window(const Napi::CallbackInfo& info); private: - JsRuntime& m_runtime; - std::optional m_timeoutDispatcher; + TimeoutDispatcher m_timeoutDispatcher; static Napi::Value SetTimeout(const Napi::CallbackInfo& info); static void ClearTimeout(const Napi::CallbackInfo& info);