diff --git a/packages/webgpu/android/CMakeLists.txt b/packages/webgpu/android/CMakeLists.txt index 289372551..3ce4a4e27 100644 --- a/packages/webgpu/android/CMakeLists.txt +++ b/packages/webgpu/android/CMakeLists.txt @@ -50,9 +50,10 @@ add_library(${PACKAGE_NAME} SHARED ../cpp/jsi/Promise.cpp ../cpp/jsi/RuntimeLifecycleMonitor.cpp ../cpp/jsi/RuntimeAwareCache.cpp - ../cpp/rnwgpu/async/AsyncRunner.cpp + ../cpp/rnwgpu/async/RuntimeContext.cpp ../cpp/rnwgpu/async/AsyncTaskHandle.cpp - ../cpp/rnwgpu/async/JSIMicrotaskDispatcher.cpp + ../cpp/rnwgpu/async/CallInvokerScheduler.cpp + ../cpp/rnwgpu/async/GpuEventLoop.cpp ) target_include_directories( diff --git a/packages/webgpu/cpp/rnwgpu/RNWebGPUManager.cpp b/packages/webgpu/cpp/rnwgpu/RNWebGPUManager.cpp index f874c14e6..475125e4f 100644 --- a/packages/webgpu/cpp/rnwgpu/RNWebGPUManager.cpp +++ b/packages/webgpu/cpp/rnwgpu/RNWebGPUManager.cpp @@ -31,8 +31,8 @@ #include "GPURenderPassEncoder.h" #include "GPURenderPipeline.h" #include "GPUSampler.h" -#include "GPUSharedTextureMemory.h" #include "GPUShaderModule.h" +#include "GPUSharedTextureMemory.h" #include "GPUSupportedLimits.h" #include "GPUTexture.h" #include "GPUTextureView.h" @@ -63,7 +63,7 @@ RNWebGPUManager::RNWebGPUManager( // Register main runtime for RuntimeAwareCache BaseRuntimeAwareCache::setMainJsRuntime(_jsRuntime); - auto gpu = std::make_shared(*_jsRuntime); + auto gpu = std::make_shared(*_jsRuntime, _jsCallInvoker); auto rnWebGPU = std::make_shared(gpu, _platformContext, _jsCallInvoker); _gpu = gpu->get(); diff --git a/packages/webgpu/cpp/rnwgpu/api/GPU.cpp b/packages/webgpu/cpp/rnwgpu/api/GPU.cpp index 11530f4da..36434e0ee 100644 --- a/packages/webgpu/cpp/rnwgpu/api/GPU.cpp +++ b/packages/webgpu/cpp/rnwgpu/api/GPU.cpp @@ -9,11 +9,14 @@ #include "Convertors.h" #include "JSIConverter.h" -#include "rnwgpu/async/JSIMicrotaskDispatcher.h" +#include "rnwgpu/async/CallInvokerScheduler.h" +#include "rnwgpu/async/GpuEventLoop.h" namespace rnwgpu { -GPU::GPU(jsi::Runtime &runtime) : NativeObject(CLASS_NAME) { +GPU::GPU(jsi::Runtime &runtime, + std::shared_ptr callInvoker) + : NativeObject(CLASS_NAME) { static const auto kTimedWaitAny = wgpu::InstanceFeatureName::TimedWaitAny; wgpu::InstanceDescriptor instanceDesc{.requiredFeatureCount = 1, .requiredFeatures = &kTimedWaitAny}; @@ -49,8 +52,11 @@ GPU::GPU(jsi::Runtime &runtime) : NativeObject(CLASS_NAME) { _instance = wgpu::CreateInstance(&instanceDesc); - auto dispatcher = std::make_shared(runtime); - _async = async::AsyncRunner::getOrCreate(runtime, _instance, dispatcher); + auto scheduler = + std::make_shared(std::move(callInvoker)); + auto eventLoop = std::make_shared(_instance); + _async = async::RuntimeContext::getOrCreate(runtime, std::move(scheduler), + std::move(eventLoop)); } async::AsyncTaskHandle GPU::requestAdapter( @@ -68,10 +74,11 @@ async::AsyncTaskHandle GPU::requestAdapter( aOptions.backendType = kDefaultBackendType; return _async->postTask( [this, aOptions](const async::AsyncTaskHandle::ResolveFunction &resolve, - const async::AsyncTaskHandle::RejectFunction &reject) { - _instance.RequestAdapter( - &aOptions, wgpu::CallbackMode::AllowProcessEvents, - [asyncRunner = _async, resolve, + const async::AsyncTaskHandle::RejectFunction &reject) + -> wgpu::Future { + return _instance.RequestAdapter( + &aOptions, wgpu::CallbackMode::WaitAnyOnly, + [context = _async, resolve, reject](wgpu::RequestAdapterStatus status, wgpu::Adapter adapter, wgpu::StringView message) { if (message.length) { @@ -79,8 +86,8 @@ async::AsyncTaskHandle GPU::requestAdapter( } if (status == wgpu::RequestAdapterStatus::Success && adapter) { - auto adapterHost = std::make_shared( - std::move(adapter), asyncRunner); + auto adapterHost = + std::make_shared(std::move(adapter), context); auto result = std::variant>( adapterHost); diff --git a/packages/webgpu/cpp/rnwgpu/api/GPU.h b/packages/webgpu/cpp/rnwgpu/api/GPU.h index f6bb4ede3..e7dc15caf 100644 --- a/packages/webgpu/cpp/rnwgpu/api/GPU.h +++ b/packages/webgpu/cpp/rnwgpu/api/GPU.h @@ -9,8 +9,8 @@ #include "NativeObject.h" -#include "rnwgpu/async/AsyncRunner.h" #include "rnwgpu/async/AsyncTaskHandle.h" +#include "rnwgpu/async/RuntimeContext.h" #include "webgpu/webgpu_cpp.h" @@ -19,6 +19,10 @@ #include +namespace facebook::react { +class CallInvoker; +} // namespace facebook::react + namespace rnwgpu { namespace jsi = facebook::jsi; @@ -27,7 +31,8 @@ class GPU : public NativeObject { public: static constexpr const char *CLASS_NAME = "GPU"; - explicit GPU(jsi::Runtime &runtime); + GPU(jsi::Runtime &runtime, + std::shared_ptr callInvoker); public: std::string getBrand() { return CLASS_NAME; } @@ -48,11 +53,10 @@ class GPU : public NativeObject { } inline const wgpu::Instance get() { return _instance; } - inline std::shared_ptr getAsyncRunner() { return _async; } private: wgpu::Instance _instance; - std::shared_ptr _async; + std::shared_ptr _async; }; } // namespace rnwgpu diff --git a/packages/webgpu/cpp/rnwgpu/api/GPUAdapter.cpp b/packages/webgpu/cpp/rnwgpu/api/GPUAdapter.cpp index 085b582dc..130b00622 100644 --- a/packages/webgpu/cpp/rnwgpu/api/GPUAdapter.cpp +++ b/packages/webgpu/cpp/rnwgpu/api/GPUAdapter.cpp @@ -134,7 +134,8 @@ async::AsyncTaskHandle GPUAdapter::requestDevice( [this, aDescriptor, descriptor, label = std::move(label), deviceLostBinding, creationRuntime](const async::AsyncTaskHandle::ResolveFunction &resolve, - const async::AsyncTaskHandle::RejectFunction &reject) { + const async::AsyncTaskHandle::RejectFunction &reject) + -> wgpu::Future { // Build a local mutable copy so we can chain Dawn's device toggles. // The toggle name strings are owned by `descriptor` (captured above), // and the const char* / DawnTogglesDescriptor locals live for the @@ -162,12 +163,11 @@ async::AsyncTaskHandle GPUAdapter::requestDevice( } deviceDesc.nextInChain = &toggles; } - _instance.RequestDevice( - &deviceDesc, wgpu::CallbackMode::AllowProcessEvents, - [asyncRunner = _async, resolve, reject, label, creationRuntime, + return _instance.RequestDevice( + &deviceDesc, wgpu::CallbackMode::WaitAnyOnly, + [context = _async, resolve, reject, label, creationRuntime, deviceLostBinding](wgpu::RequestDeviceStatus status, - wgpu::Device device, - wgpu::StringView message) { + wgpu::Device device, wgpu::StringView message) { if (message.length) { fprintf(stderr, "%s", message.data); } @@ -191,14 +191,12 @@ async::AsyncTaskHandle GPUAdapter::requestDevice( case wgpu::LoggingType::Warning: logLevel = "Warning"; Logger::warnToJavascriptConsole( - *creationRuntime, - std::string(msg.data, msg.length)); + *creationRuntime, std::string(msg.data, msg.length)); break; case wgpu::LoggingType::Error: logLevel = "Error"; Logger::errorToJavascriptConsole( - *creationRuntime, - std::string(msg.data, msg.length)); + *creationRuntime, std::string(msg.data, msg.length)); break; case wgpu::LoggingType::Verbose: logLevel = "Verbose"; @@ -216,7 +214,7 @@ async::AsyncTaskHandle GPUAdapter::requestDevice( creationRuntime); auto deviceHost = std::make_shared(std::move(device), - asyncRunner, label); + context, label); *deviceLostBinding = deviceHost; // Register the device in the static registry so the uncaptured diff --git a/packages/webgpu/cpp/rnwgpu/api/GPUAdapter.h b/packages/webgpu/cpp/rnwgpu/api/GPUAdapter.h index 66acdc2f7..7f399f0a7 100644 --- a/packages/webgpu/cpp/rnwgpu/api/GPUAdapter.h +++ b/packages/webgpu/cpp/rnwgpu/api/GPUAdapter.h @@ -8,8 +8,8 @@ #include "NativeObject.h" -#include "rnwgpu/async/AsyncRunner.h" #include "rnwgpu/async/AsyncTaskHandle.h" +#include "rnwgpu/async/RuntimeContext.h" #include "webgpu/webgpu_cpp.h" @@ -27,7 +27,7 @@ class GPUAdapter : public NativeObject { static constexpr const char *CLASS_NAME = "GPUAdapter"; explicit GPUAdapter(wgpu::Adapter instance, - std::shared_ptr async) + std::shared_ptr async) : NativeObject(CLASS_NAME), _instance(instance), _async(async) {} public: @@ -53,7 +53,7 @@ class GPUAdapter : public NativeObject { private: wgpu::Adapter _instance; - std::shared_ptr _async; + std::shared_ptr _async; }; } // namespace rnwgpu diff --git a/packages/webgpu/cpp/rnwgpu/api/GPUBuffer.cpp b/packages/webgpu/cpp/rnwgpu/api/GPUBuffer.cpp index 4d6012621..a53d97940 100644 --- a/packages/webgpu/cpp/rnwgpu/api/GPUBuffer.cpp +++ b/packages/webgpu/cpp/rnwgpu/api/GPUBuffer.cpp @@ -54,31 +54,31 @@ async::AsyncTaskHandle GPUBuffer::mapAsync(uint64_t modeIn, return _async->postTask( [bufferHandle, mode, resolvedOffset, rangeSize](const async::AsyncTaskHandle::ResolveFunction &resolve, - const async::AsyncTaskHandle::RejectFunction &reject) { - bufferHandle.MapAsync(mode, resolvedOffset, rangeSize, - wgpu::CallbackMode::AllowProcessEvents, - [resolve, reject](wgpu::MapAsyncStatus status, - wgpu::StringView message) { - switch (status) { - case wgpu::MapAsyncStatus::Success: - resolve(nullptr); - break; - case wgpu::MapAsyncStatus::CallbackCancelled: - reject("MapAsyncStatus::CallbackCancelled"); - break; - case wgpu::MapAsyncStatus::Error: - reject("MapAsyncStatus::Error"); - break; - case wgpu::MapAsyncStatus::Aborted: - reject("MapAsyncStatus::Aborted"); - break; - default: - reject( - "MapAsyncStatus: " + - std::to_string(static_cast(status))); - break; - } - }); + const async::AsyncTaskHandle::RejectFunction &reject) + -> wgpu::Future { + return bufferHandle.MapAsync( + mode, resolvedOffset, rangeSize, wgpu::CallbackMode::WaitAnyOnly, + [resolve, reject](wgpu::MapAsyncStatus status, + wgpu::StringView message) { + switch (status) { + case wgpu::MapAsyncStatus::Success: + resolve(nullptr); + break; + case wgpu::MapAsyncStatus::CallbackCancelled: + reject("MapAsyncStatus::CallbackCancelled"); + break; + case wgpu::MapAsyncStatus::Error: + reject("MapAsyncStatus::Error"); + break; + case wgpu::MapAsyncStatus::Aborted: + reject("MapAsyncStatus::Aborted"); + break; + default: + reject("MapAsyncStatus: " + + std::to_string(static_cast(status))); + break; + } + }); }); } diff --git a/packages/webgpu/cpp/rnwgpu/api/GPUBuffer.h b/packages/webgpu/cpp/rnwgpu/api/GPUBuffer.h index edfc8e41b..036b5af4b 100644 --- a/packages/webgpu/cpp/rnwgpu/api/GPUBuffer.h +++ b/packages/webgpu/cpp/rnwgpu/api/GPUBuffer.h @@ -9,8 +9,8 @@ #include "NativeObject.h" -#include "rnwgpu/async/AsyncRunner.h" #include "rnwgpu/async/AsyncTaskHandle.h" +#include "rnwgpu/async/RuntimeContext.h" #include "webgpu/webgpu_cpp.h" @@ -25,7 +25,7 @@ class GPUBuffer : public NativeObject { static constexpr const char *CLASS_NAME = "GPUBuffer"; explicit GPUBuffer(wgpu::Buffer instance, - std::shared_ptr async, + std::shared_ptr async, std::string label) : NativeObject(CLASS_NAME), _instance(instance), _async(async), _label(label) {} @@ -71,7 +71,7 @@ class GPUBuffer : public NativeObject { private: wgpu::Buffer _instance; - std::shared_ptr _async; + std::shared_ptr _async; std::string _label; struct Mapping { uint64_t start; diff --git a/packages/webgpu/cpp/rnwgpu/api/GPUDevice.cpp b/packages/webgpu/cpp/rnwgpu/api/GPUDevice.cpp index 4d6c92ffa..1729cc856 100644 --- a/packages/webgpu/cpp/rnwgpu/api/GPUDevice.cpp +++ b/packages/webgpu/cpp/rnwgpu/api/GPUDevice.cpp @@ -19,23 +19,33 @@ namespace rnwgpu { void GPUDevice::notifyDeviceLost(wgpu::DeviceLostReason reason, std::string message) { - if (_lostSettled) { - return; - } + std::optional resolveToCall; + std::shared_ptr info; + { + std::lock_guard lock(_lostMutex); + if (_lostSettled) { + return; + } - _lostSettled = true; - _lostInfo = std::make_shared(reason, std::move(message)); + _lostSettled = true; + _lostInfo = std::make_shared(reason, std::move(message)); + info = _lostInfo; + + if (_lostResolve.has_value()) { + resolveToCall = std::move(*_lostResolve); + _lostResolve.reset(); + } - if (_lostResolve.has_value()) { - auto resolve = std::move(*_lostResolve); - _lostResolve.reset(); - resolve([info = _lostInfo](jsi::Runtime &runtime) mutable { + _lostHandle.reset(); + } + + // Settle outside the lock: resolve() only enqueues onto the JS thread. + if (resolveToCall.has_value()) { + (*resolveToCall)([info](jsi::Runtime &runtime) mutable { return JSIConverter>::toJSI(runtime, info); }); } - - _lostHandle.reset(); } void GPUDevice::forceLossForTesting() { @@ -304,10 +314,10 @@ async::AsyncTaskHandle GPUDevice::createComputePipelineAsync( const async::AsyncTaskHandle::ResolveFunction &resolve, const async::AsyncTaskHandle::RejectFunction - &reject) { + &reject) -> wgpu::Future { (void)descriptor; - device.CreateComputePipelineAsync( - &desc, wgpu::CallbackMode::AllowProcessEvents, + return device.CreateComputePipelineAsync( + &desc, wgpu::CallbackMode::WaitAnyOnly, [pipelineHolder, resolve, reject](wgpu::CreatePipelineAsyncStatus status, wgpu::ComputePipeline pipeline, wgpu::StringView msg) { @@ -318,9 +328,9 @@ async::AsyncTaskHandle GPUDevice::createComputePipelineAsync( runtime, pipelineHolder); }); } else { - std::string error = - msg.length ? std::string(msg.data, msg.length) - : "Failed to create compute pipeline"; + std::string error = msg.length + ? std::string(msg.data, msg.length) + : "Failed to create compute pipeline"; reject(std::move(error)); } }); @@ -346,10 +356,10 @@ async::AsyncTaskHandle GPUDevice::createRenderPipelineAsync( const async::AsyncTaskHandle::ResolveFunction &resolve, const async::AsyncTaskHandle::RejectFunction - &reject) { + &reject) -> wgpu::Future { (void)descriptor; - device.CreateRenderPipelineAsync( - &desc, wgpu::CallbackMode::AllowProcessEvents, + return device.CreateRenderPipelineAsync( + &desc, wgpu::CallbackMode::WaitAnyOnly, [pipelineHolder, resolve, reject](wgpu::CreatePipelineAsyncStatus status, wgpu::RenderPipeline pipeline, wgpu::StringView msg) { @@ -360,9 +370,8 @@ async::AsyncTaskHandle GPUDevice::createRenderPipelineAsync( runtime, pipelineHolder); }); } else { - std::string error = - msg.length ? std::string(msg.data, msg.length) - : "Failed to create render pipeline"; + std::string error = msg.length ? std::string(msg.data, msg.length) + : "Failed to create render pipeline"; reject(std::move(error)); } }); @@ -379,9 +388,9 @@ async::AsyncTaskHandle GPUDevice::popErrorScope() { return _async->postTask([device](const async::AsyncTaskHandle::ResolveFunction &resolve, const async::AsyncTaskHandle::RejectFunction - &reject) { - device.PopErrorScope( - wgpu::CallbackMode::AllowProcessEvents, + &reject) -> wgpu::Future { + return device.PopErrorScope( + wgpu::CallbackMode::WaitAnyOnly, [resolve, reject](wgpu::PopErrorScopeStatus status, wgpu::ErrorType type, wgpu::StringView message) { if (status == wgpu::PopErrorScopeStatus::Error || @@ -449,6 +458,11 @@ std::unordered_set GPUDevice::getFeatures() { } async::AsyncTaskHandle GPUDevice::getLost() { + // Held across the whole body: the postTask callback below runs synchronously + // on this (JS) thread and touches the same _lost* fields, so it must not + // re-lock. notifyDeviceLost() takes the same lock from its (possibly worker) + // thread. + std::lock_guard lock(_lostMutex); if (_lostHandle.has_value()) { return *_lostHandle; } @@ -457,29 +471,33 @@ async::AsyncTaskHandle GPUDevice::getLost() { return _async->postTask( [info = _lostInfo]( const async::AsyncTaskHandle::ResolveFunction &resolve, - const async::AsyncTaskHandle::RejectFunction & /*reject*/) { + const async::AsyncTaskHandle::RejectFunction & /*reject*/) + -> wgpu::Future { resolve([info](jsi::Runtime &runtime) mutable { return JSIConverter>::toJSI( runtime, info); }); - }, - false); + // No Dawn event to wait on: resolved synchronously. + return wgpu::Future{}; + }); } auto handle = _async->postTask( [this](const async::AsyncTaskHandle::ResolveFunction &resolve, - const async::AsyncTaskHandle::RejectFunction & /*reject*/) { + const async::AsyncTaskHandle::RejectFunction & /*reject*/) + -> wgpu::Future { if (_lostSettled && _lostInfo) { resolve([info = _lostInfo](jsi::Runtime &runtime) mutable { return JSIConverter>::toJSI( runtime, info); }); - return; + return wgpu::Future{}; } + // Resolved later from notifyDeviceLost(); no Dawn event to wait on. _lostResolve = resolve; - }, - false); + return wgpu::Future{}; + }); _lostHandle = handle; return handle; diff --git a/packages/webgpu/cpp/rnwgpu/api/GPUDevice.h b/packages/webgpu/cpp/rnwgpu/api/GPUDevice.h index 2ab1ddd14..facbed161 100644 --- a/packages/webgpu/cpp/rnwgpu/api/GPUDevice.h +++ b/packages/webgpu/cpp/rnwgpu/api/GPUDevice.h @@ -15,8 +15,8 @@ #include "NativeObject.h" -#include "rnwgpu/async/AsyncRunner.h" #include "rnwgpu/async/AsyncTaskHandle.h" +#include "rnwgpu/async/RuntimeContext.h" #include "webgpu/webgpu_cpp.h" @@ -45,10 +45,10 @@ #include "GPURenderPipelineDescriptor.h" #include "GPUSampler.h" #include "GPUSamplerDescriptor.h" -#include "GPUSharedTextureMemory.h" -#include "GPUSharedTextureMemoryDescriptor.h" #include "GPUShaderModule.h" #include "GPUShaderModuleDescriptor.h" +#include "GPUSharedTextureMemory.h" +#include "GPUSharedTextureMemoryDescriptor.h" #include "GPUSupportedLimits.h" #include "GPUTexture.h" #include "GPUTextureDescriptor.h" @@ -63,7 +63,7 @@ class GPUDevice : public NativeObject { static constexpr const char *CLASS_NAME = "GPUDevice"; explicit GPUDevice(wgpu::Device instance, - std::shared_ptr async, + std::shared_ptr async, std::string label) : NativeObject(CLASS_NAME), _instance(instance), _async(async), _label(label) {} @@ -249,8 +249,12 @@ class GPUDevice : public NativeObject { friend class GPUAdapter; wgpu::Device _instance; - std::shared_ptr _async; + std::shared_ptr _async; std::string _label; + // Guards the device-lost state below. notifyDeviceLost() may run on a + // GpuEventLoop worker thread (the device-lost callback is Spontaneous), while + // getLost() runs on the JS thread, so these fields need synchronization. + std::mutex _lostMutex; std::optional _lostHandle; std::shared_ptr _lostInfo; bool _lostSettled = false; diff --git a/packages/webgpu/cpp/rnwgpu/api/GPUQueue.cpp b/packages/webgpu/cpp/rnwgpu/api/GPUQueue.cpp index d3c0d65af..9b3365d69 100644 --- a/packages/webgpu/cpp/rnwgpu/api/GPUQueue.cpp +++ b/packages/webgpu/cpp/rnwgpu/api/GPUQueue.cpp @@ -82,9 +82,10 @@ async::AsyncTaskHandle GPUQueue::onSubmittedWorkDone() { auto queue = _instance; return _async->postTask( [queue](const async::AsyncTaskHandle::ResolveFunction &resolve, - const async::AsyncTaskHandle::RejectFunction &reject) { - queue.OnSubmittedWorkDone( - wgpu::CallbackMode::AllowProcessEvents, + const async::AsyncTaskHandle::RejectFunction &reject) + -> wgpu::Future { + return queue.OnSubmittedWorkDone( + wgpu::CallbackMode::WaitAnyOnly, [resolve, reject](wgpu::QueueWorkDoneStatus status, wgpu::StringView message) { if (status == wgpu::QueueWorkDoneStatus::Success) { diff --git a/packages/webgpu/cpp/rnwgpu/api/GPUQueue.h b/packages/webgpu/cpp/rnwgpu/api/GPUQueue.h index be824e781..f322392b7 100644 --- a/packages/webgpu/cpp/rnwgpu/api/GPUQueue.h +++ b/packages/webgpu/cpp/rnwgpu/api/GPUQueue.h @@ -8,8 +8,8 @@ #include "NativeObject.h" -#include "rnwgpu/async/AsyncRunner.h" #include "rnwgpu/async/AsyncTaskHandle.h" +#include "rnwgpu/async/RuntimeContext.h" #include "webgpu/webgpu_cpp.h" @@ -28,7 +28,7 @@ class GPUQueue : public NativeObject { static constexpr const char *CLASS_NAME = "GPUQueue"; explicit GPUQueue(wgpu::Queue instance, - std::shared_ptr async, + std::shared_ptr async, std::string label) : NativeObject(CLASS_NAME), _instance(instance), _async(async), _label(label) {} @@ -74,7 +74,7 @@ class GPUQueue : public NativeObject { private: wgpu::Queue _instance; - std::shared_ptr _async; + std::shared_ptr _async; std::string _label; }; diff --git a/packages/webgpu/cpp/rnwgpu/api/GPUShaderModule.cpp b/packages/webgpu/cpp/rnwgpu/api/GPUShaderModule.cpp index 113dc407c..5ac6d3634 100644 --- a/packages/webgpu/cpp/rnwgpu/api/GPUShaderModule.cpp +++ b/packages/webgpu/cpp/rnwgpu/api/GPUShaderModule.cpp @@ -12,10 +12,11 @@ async::AsyncTaskHandle GPUShaderModule::getCompilationInfo() { return _async->postTask( [module](const async::AsyncTaskHandle::ResolveFunction &resolve, - const async::AsyncTaskHandle::RejectFunction &reject) { + const async::AsyncTaskHandle::RejectFunction &reject) + -> wgpu::Future { auto result = std::make_shared(); - module.GetCompilationInfo( - wgpu::CallbackMode::AllowProcessEvents, + return module.GetCompilationInfo( + wgpu::CallbackMode::WaitAnyOnly, [result, resolve, reject](wgpu::CompilationInfoRequestStatus status, const wgpu::CompilationInfo *compilationInfo) { diff --git a/packages/webgpu/cpp/rnwgpu/api/GPUShaderModule.h b/packages/webgpu/cpp/rnwgpu/api/GPUShaderModule.h index ab8561090..0e59edf01 100644 --- a/packages/webgpu/cpp/rnwgpu/api/GPUShaderModule.h +++ b/packages/webgpu/cpp/rnwgpu/api/GPUShaderModule.h @@ -7,8 +7,8 @@ #include "NativeObject.h" -#include "rnwgpu/async/AsyncRunner.h" #include "rnwgpu/async/AsyncTaskHandle.h" +#include "rnwgpu/async/RuntimeContext.h" #include "webgpu/webgpu_cpp.h" @@ -23,7 +23,7 @@ class GPUShaderModule : public NativeObject { static constexpr const char *CLASS_NAME = "GPUShaderModule"; explicit GPUShaderModule(wgpu::ShaderModule instance, - std::shared_ptr async, + std::shared_ptr async, std::string label) : NativeObject(CLASS_NAME), _instance(instance), _async(async), _label(label) {} @@ -59,7 +59,7 @@ class GPUShaderModule : public NativeObject { private: wgpu::ShaderModule _instance; - std::shared_ptr _async; + std::shared_ptr _async; std::string _label; }; diff --git a/packages/webgpu/cpp/rnwgpu/async/AsyncDispatcher.h b/packages/webgpu/cpp/rnwgpu/async/AsyncDispatcher.h deleted file mode 100644 index 0ec176824..000000000 --- a/packages/webgpu/cpp/rnwgpu/async/AsyncDispatcher.h +++ /dev/null @@ -1,28 +0,0 @@ -#pragma once - -#include -#include - -#include - -namespace rnwgpu::async { - -namespace jsi = facebook::jsi; - -/** - * Abstract dispatcher used by the AsyncRunner to enqueue work back onto the - * JavaScript thread. - */ -class AsyncDispatcher { -public: - using Work = std::function; - - virtual ~AsyncDispatcher() = default; - - /** - * Enqueue a unit of work that will be executed on the JavaScript thread. - */ - virtual void post(Work work) = 0; -}; - -} // namespace rnwgpu::async diff --git a/packages/webgpu/cpp/rnwgpu/async/AsyncRunner.cpp b/packages/webgpu/cpp/rnwgpu/async/AsyncRunner.cpp deleted file mode 100644 index 94bbae230..000000000 --- a/packages/webgpu/cpp/rnwgpu/async/AsyncRunner.cpp +++ /dev/null @@ -1,215 +0,0 @@ -#include "AsyncRunner.h" - -#include -#include -#include - -#include "AsyncTaskHandle.h" -#include "WGPULogger.h" - -namespace rnwgpu::async { - -namespace { -struct RuntimeData { - std::shared_ptr runner; -}; -constexpr const char *TAG = "AsyncRunner"; -} // namespace - -AsyncRunner::AsyncRunner(wgpu::Instance instance, - std::shared_ptr dispatcher) - : _instance(std::move(instance)), _dispatcher(std::move(dispatcher)), - _pendingTasks(0), _pumpTasks(0), _tickScheduled(false), - _lastTickTimeNs(0) { - if (!_dispatcher) { - throw std::runtime_error("AsyncRunner requires a valid dispatcher."); - } - Logger::logToConsole("[%s] Created runner (dispatcher=%p)", TAG, - _dispatcher.get()); -} - -std::shared_ptr AsyncRunner::get(jsi::Runtime &runtime) { - auto data = runtime.getRuntimeData(runtimeDataUUID()); - if (!data) { - return nullptr; - } - auto stored = std::static_pointer_cast(data); - return stored->runner; -} - -std::shared_ptr -AsyncRunner::getOrCreate(jsi::Runtime &runtime, wgpu::Instance instance, - std::shared_ptr dispatcher) { - auto existing = get(runtime); - if (existing) { - return existing; - } - - auto runner = - std::make_shared(std::move(instance), std::move(dispatcher)); - auto data = std::make_shared(); - data->runner = runner; - runtime.setRuntimeData(runtimeDataUUID(), data); - return runner; -} - -AsyncTaskHandle AsyncRunner::postTask(const TaskCallback &callback, - bool keepPumping) { - auto handle = AsyncTaskHandle::create(shared_from_this(), keepPumping); - if (!handle.valid()) { - throw std::runtime_error("Failed to create AsyncTaskHandle."); - } - - _pendingTasks.fetch_add(1, std::memory_order_acq_rel); - if (keepPumping) { - _pumpTasks.fetch_add(1, std::memory_order_acq_rel); - } - requestTick(); - - Logger::logToConsole( - "[%s] postTask (keepPumping=%s, pending=%zu, pumping=%zu)", TAG, - keepPumping ? "true" : "false", - _pendingTasks.load(std::memory_order_acquire), - _pumpTasks.load(std::memory_order_acquire)); - - auto resolve = handle.createResolveFunction(); - auto reject = handle.createRejectFunction(); - - try { - callback(resolve, reject); - } catch (const std::exception &exception) { - reject(exception.what()); - } catch (...) { - reject("Unknown native error in AsyncRunner::postTask."); - } - - return handle; -} - -void AsyncRunner::requestTick() { - bool expected = false; - if (!_tickScheduled.compare_exchange_strong(expected, true, - std::memory_order_acq_rel)) { - return; - } - - auto self = shared_from_this(); - _dispatcher->post([self](jsi::Runtime &runtime) { - auto tickCallback = jsi::Function::createFromHostFunction( - runtime, jsi::PropNameID::forAscii(runtime, "AsyncRunnerTick"), 0, - [self](jsi::Runtime &runtime, const jsi::Value & /*thisValue*/, - const jsi::Value * /*args*/, size_t /*count*/) -> jsi::Value { - self->tick(runtime); - return jsi::Value::undefined(); - }); - -#if defined(ANDROID) || defined(__ANDROID__) - auto global = runtime.global(); - auto setImmediateValue = global.getProperty(runtime, "setImmediate"); - constexpr auto kMinTickInterval = std::chrono::milliseconds(4); - const int64_t nowNs = - std::chrono::duration_cast( - std::chrono::steady_clock::now().time_since_epoch()) - .count(); - const int64_t lastNs = - self->_lastTickTimeNs.load(std::memory_order_acquire); - int delayMs = 0; - if (lastNs > 0) { - const int64_t elapsedNs = nowNs - lastNs; - const int64_t minIntervalNs = kMinTickInterval.count() * 1000000LL; - if (elapsedNs < minIntervalNs) { - const int64_t remainingNs = minIntervalNs - elapsedNs; - delayMs = static_cast((remainingNs + 999999) / 1000000); - } - } - - auto tryScheduleTimeout = [&](int ms) { - auto setTimeoutValue = global.getProperty(runtime, "setTimeout"); - if (!setTimeoutValue.isObject()) { - return false; - } - auto setTimeoutObj = setTimeoutValue.asObject(runtime); - if (!setTimeoutObj.isFunction(runtime)) { - return false; - } - Logger::logToConsole("[%s] requestTick scheduled via setTimeout(%d)", TAG, - ms); - auto setTimeoutFn = setTimeoutObj.asFunction(runtime); - jsi::Value callbackArg(runtime, tickCallback); - jsi::Value delayArg(static_cast(ms)); - setTimeoutFn.call(runtime, callbackArg, delayArg); - return true; - }; - - if (delayMs > 0) { - if (tryScheduleTimeout(delayMs)) { - return; - } - // If setTimeout unavailable fall through to immediate scheduling. - } - - if (setImmediateValue.isObject()) { - auto setImmediateObj = setImmediateValue.asObject(runtime); - if (setImmediateObj.isFunction(runtime)) { - Logger::logToConsole("[%s] requestTick scheduled via setImmediate", - TAG); - auto setImmediateFn = setImmediateObj.asFunction(runtime); - jsi::Value callbackArg(runtime, tickCallback); - setImmediateFn.call(runtime, callbackArg); - return; - } - } - - int timeoutDelayMs = delayMs > 0 ? delayMs : 0; - if (tryScheduleTimeout(timeoutDelayMs)) { - return; - } - - Logger::logToConsole("[%s] requestTick scheduled via microtask fallback", - TAG); - runtime.queueMicrotask(std::move(tickCallback)); -#else - Logger::logToConsole("[%s] requestTick scheduled microtask (non-Android)", - TAG); - runtime.queueMicrotask(std::move(tickCallback)); -#endif - }); -} - -void AsyncRunner::tick(jsi::Runtime & /*runtime*/) { - _tickScheduled.store(false, std::memory_order_release); - _instance.ProcessEvents(); - const auto nowNs = std::chrono::duration_cast( - std::chrono::steady_clock::now().time_since_epoch()) - .count(); - _lastTickTimeNs.store(nowNs, std::memory_order_release); - Logger::logToConsole("[%s] tick processed events (pending=%zu, pumping=%zu)", - TAG, _pendingTasks.load(std::memory_order_acquire), - _pumpTasks.load(std::memory_order_acquire)); - if (_pumpTasks.load(std::memory_order_acquire) > 0) { - requestTick(); - } -} - -void AsyncRunner::onTaskSettled(bool keepPumping) { - _pendingTasks.fetch_sub(1, std::memory_order_acq_rel); - if (keepPumping) { - _pumpTasks.fetch_sub(1, std::memory_order_acq_rel); - } - Logger::logToConsole( - "[%s] onTaskSettled (keepPumping=%s, pending=%zu, pumping=%zu)", TAG, - keepPumping ? "true" : "false", - _pendingTasks.load(std::memory_order_acquire), - _pumpTasks.load(std::memory_order_acquire)); -} - -std::shared_ptr AsyncRunner::dispatcher() const { - return _dispatcher; -} - -jsi::UUID AsyncRunner::runtimeDataUUID() { - static const auto uuid = jsi::UUID(); - return uuid; -} - -} // namespace rnwgpu::async diff --git a/packages/webgpu/cpp/rnwgpu/async/AsyncRunner.h b/packages/webgpu/cpp/rnwgpu/async/AsyncRunner.h deleted file mode 100644 index f81101d10..000000000 --- a/packages/webgpu/cpp/rnwgpu/async/AsyncRunner.h +++ /dev/null @@ -1,53 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -#include - -#include "AsyncDispatcher.h" -#include "AsyncTaskHandle.h" - -#include "webgpu/webgpu_cpp.h" - -namespace jsi = facebook::jsi; - -namespace rnwgpu::async { - -class AsyncRunner : public std::enable_shared_from_this { -public: - using TaskCallback = - std::function; - - AsyncRunner(wgpu::Instance instance, - std::shared_ptr dispatcher); - - static std::shared_ptr get(jsi::Runtime &runtime); - static std::shared_ptr - getOrCreate(jsi::Runtime &runtime, wgpu::Instance instance, - std::shared_ptr dispatcher); - - AsyncTaskHandle postTask(const TaskCallback &callback, - bool keepPumping = true); - - void requestTick(); - void tick(jsi::Runtime &runtime); - void onTaskSettled(bool keepPumping); - - std::shared_ptr dispatcher() const; - -private: - static jsi::UUID runtimeDataUUID(); - - wgpu::Instance _instance; - std::shared_ptr _dispatcher; - std::atomic _pendingTasks; - std::atomic _pumpTasks; - std::atomic _tickScheduled; - std::atomic _lastTickTimeNs; -}; - -} // namespace rnwgpu::async diff --git a/packages/webgpu/cpp/rnwgpu/async/AsyncTaskHandle.cpp b/packages/webgpu/cpp/rnwgpu/async/AsyncTaskHandle.cpp index 6b262005a..c0876c1e3 100644 --- a/packages/webgpu/cpp/rnwgpu/async/AsyncTaskHandle.cpp +++ b/packages/webgpu/cpp/rnwgpu/async/AsyncTaskHandle.cpp @@ -1,20 +1,19 @@ #include "AsyncTaskHandle.h" +#include #include #include #include "Promise.h" -#include "AsyncRunner.h" - namespace rnwgpu::async { using Action = std::function; struct AsyncTaskHandle::State : public std::enable_shared_from_this { - State(std::shared_ptr runner, bool keepPumping) - : runner(std::move(runner)), keepPumping(keepPumping) {} + explicit State(std::shared_ptr scheduler) + : scheduler(std::move(scheduler)) {} void settle(Action action); void attachPromise(const std::shared_ptr &promise); @@ -26,12 +25,11 @@ struct AsyncTaskHandle::State std::shared_ptr currentPromise(); std::mutex mutex; - std::weak_ptr runner; + std::shared_ptr scheduler; std::shared_ptr promise; std::optional pendingAction; bool settled = false; std::shared_ptr keepAlive; - bool keepPumping; }; // MARK: - State helpers @@ -77,26 +75,18 @@ void AsyncTaskHandle::State::attachPromise( } void AsyncTaskHandle::State::schedule(Action action) { - auto runnerRef = runner.lock(); - if (!runnerRef) { + if (!scheduler) { return; } auto promiseRef = currentPromise(); if (!promiseRef) { - runnerRef->onTaskSettled(keepPumping); - return; - } - - auto dispatcherRef = runnerRef->dispatcher(); - if (!dispatcherRef) { - runnerRef->onTaskSettled(keepPumping); return; } - dispatcherRef->post([self = shared_from_this(), action = std::move(action), - runnerRef, promiseRef](jsi::Runtime &runtime) mutable { - runnerRef->onTaskSettled(self->keepPumping); + scheduler->scheduleOnJS([self = shared_from_this(), + action = std::move(action), + promiseRef](jsi::Runtime &runtime) mutable { action(runtime, *promiseRef); std::lock_guard lock(self->mutex); self->keepAlive.reset(); @@ -149,9 +139,8 @@ AsyncTaskHandle::AsyncTaskHandle(std::shared_ptr state) bool AsyncTaskHandle::valid() const { return _state != nullptr; } AsyncTaskHandle -AsyncTaskHandle::create(const std::shared_ptr &runner, - bool keepPumping) { - auto state = std::make_shared(runner, keepPumping); +AsyncTaskHandle::create(const std::shared_ptr &scheduler) { + auto state = std::make_shared(scheduler); state->keepAlive = state; return AsyncTaskHandle(std::move(state)); } diff --git a/packages/webgpu/cpp/rnwgpu/async/AsyncTaskHandle.h b/packages/webgpu/cpp/rnwgpu/async/AsyncTaskHandle.h index cb6c7a2a4..e3a224563 100644 --- a/packages/webgpu/cpp/rnwgpu/async/AsyncTaskHandle.h +++ b/packages/webgpu/cpp/rnwgpu/async/AsyncTaskHandle.h @@ -8,7 +8,7 @@ #include -#include "AsyncDispatcher.h" +#include "RuntimeScheduler.h" namespace rnwgpu { class Promise; @@ -16,11 +16,13 @@ class Promise; namespace rnwgpu::async { -class AsyncRunner; - /** * Represents a pending asynchronous WebGPU operation that can be converted into * a JavaScript Promise. + * + * The native callback (resolve/reject) may be invoked from any thread (e.g. a + * GpuEventLoop worker); the actual Promise settlement is marshalled onto the + * owning runtime's JS thread via a RuntimeScheduler. */ class AsyncTaskHandle { public: @@ -34,7 +36,7 @@ class AsyncTaskHandle { AsyncTaskHandle(); /** - * Internal constructor used by AsyncRunner. + * Internal constructor used by RuntimeContext. */ explicit AsyncTaskHandle(std::shared_ptr state); @@ -45,8 +47,8 @@ class AsyncTaskHandle { void attachPromise(const std::shared_ptr &promise) const; - static AsyncTaskHandle create(const std::shared_ptr &runner, - bool keepPumping); + static AsyncTaskHandle + create(const std::shared_ptr &scheduler); private: std::shared_ptr _state; diff --git a/packages/webgpu/cpp/rnwgpu/async/CallInvokerScheduler.cpp b/packages/webgpu/cpp/rnwgpu/async/CallInvokerScheduler.cpp new file mode 100644 index 000000000..2ef72f407 --- /dev/null +++ b/packages/webgpu/cpp/rnwgpu/async/CallInvokerScheduler.cpp @@ -0,0 +1,21 @@ +#include "CallInvokerScheduler.h" + +#include +#include + +namespace rnwgpu::async { + +CallInvokerScheduler::CallInvokerScheduler( + std::shared_ptr invoker) + : _invoker(std::move(invoker)) {} + +void CallInvokerScheduler::scheduleOnJS( + std::function job) { + if (!_invoker || !job) { + return; + } + _invoker->invokeAsync( + [job = std::move(job)](jsi::Runtime &runtime) { job(runtime); }); +} + +} // namespace rnwgpu::async diff --git a/packages/webgpu/cpp/rnwgpu/async/CallInvokerScheduler.h b/packages/webgpu/cpp/rnwgpu/async/CallInvokerScheduler.h new file mode 100644 index 000000000..cbb6a9174 --- /dev/null +++ b/packages/webgpu/cpp/rnwgpu/async/CallInvokerScheduler.h @@ -0,0 +1,32 @@ +#pragma once + +#include +#include + +#include +#include + +#include "RuntimeScheduler.h" + +namespace rnwgpu::async { + +namespace jsi = facebook::jsi; +namespace react = facebook::react; + +/** + * RuntimeScheduler for the main React Native JS runtime, backed by + * react::CallInvoker::invokeAsync. invokeAsync is safe to call from any thread + * and delivers the work on the JS thread with the runtime, which is exactly the + * contract RuntimeScheduler requires. + */ +class CallInvokerScheduler final : public RuntimeScheduler { +public: + explicit CallInvokerScheduler(std::shared_ptr invoker); + + void scheduleOnJS(std::function job) override; + +private: + std::shared_ptr _invoker; +}; + +} // namespace rnwgpu::async diff --git a/packages/webgpu/cpp/rnwgpu/async/GpuEventLoop.cpp b/packages/webgpu/cpp/rnwgpu/async/GpuEventLoop.cpp new file mode 100644 index 000000000..2bd643b39 --- /dev/null +++ b/packages/webgpu/cpp/rnwgpu/async/GpuEventLoop.cpp @@ -0,0 +1,113 @@ +#include "GpuEventLoop.h" + +#include +#include +#include +#include + +#include "WGPULogger.h" + +namespace rnwgpu::async { + +namespace { +constexpr const char *TAG = "GpuEventLoop"; + +std::size_t computeMaxWorkers() { + unsigned int hw = std::thread::hardware_concurrency(); + if (hw == 0) { + hw = 4; + } + // A small bounded pool: enough to overlap the handful of async GPU ops that + // are realistically in flight at once, without spawning unbounded threads. + return std::max(2, std::min(8, hw)); +} +} // namespace + +GpuEventLoop::GpuEventLoop(wgpu::Instance instance) + : _state(std::make_shared(std::move(instance))) { + _state->maxWorkers = computeMaxWorkers(); + Logger::logToConsole("[%s] Created (maxWorkers=%zu)", TAG, + _state->maxWorkers); +} + +GpuEventLoop::~GpuEventLoop() { + { + std::lock_guard lock(_state->mutex); + _state->running.store(false, std::memory_order_release); + } + // Wake idle workers so they can observe !running and exit. Workers that are + // currently blocked in WaitAny keep the shared State (and its wgpu::Instance + // ref) alive until their future completes, then exit; we intentionally do not + // join here to avoid blocking teardown on in-flight GPU work. + _state->cv.notify_all(); +} + +void GpuEventLoop::addFuture(wgpu::Future future) { + if (future.id == 0) { + // No event to wait on (deferred/immediate resolution). The callback path + // settles the promise without involving the event loop. + return; + } + + std::lock_guard lock(_state->mutex); + if (!_state->running.load(std::memory_order_acquire)) { + return; + } + + _state->queue.push(future); + + // Grow the pool if every worker is busy and we are still under the cap; + // otherwise wake an idle worker. A freshly spawned worker picks the job up + // via the queue-non-empty predicate, so it needs no separate notify. + if (_state->idleWorkers == 0 && _state->totalWorkers < _state->maxWorkers) { + _state->totalWorkers++; + std::thread(&GpuEventLoop::worker, _state).detach(); + Logger::logToConsole("[%s] grew pool to %zu worker(s)", TAG, + _state->totalWorkers); + } else { + _state->cv.notify_one(); + } +} + +void GpuEventLoop::worker(std::shared_ptr state) { + for (;;) { + wgpu::Future future{}; + { + std::unique_lock lock(state->mutex); + state->idleWorkers++; + state->cv.wait(lock, [&state] { + return !state->running.load(std::memory_order_acquire) || + !state->queue.empty(); + }); + state->idleWorkers--; + + if (state->queue.empty()) { + // Only happens when shutting down. + state->totalWorkers--; + return; + } + + future = state->queue.front(); + state->queue.pop(); + } + + // Single-future wait: always a legal single-source WaitAny. Blocks with no + // CPU cost until the GPU work completes, at which point Dawn invokes the + // future's callback on this thread (it then marshals back to the owning + // runtime via its RuntimeScheduler). + auto status = state->instance.WaitAny(future, UINT64_MAX); + if (status != wgpu::WaitStatus::Success) { + // With an infinite timeout on a single future this is not expected. If it + // happens, Dawn did not invoke the future's callback, so the associated + // JS Promise will never settle. Log it so the otherwise-silent hang is at + // least observable. + Logger::logToConsole( + "[%s] WaitAny returned non-success status %u for future %llu; its " + "Promise will not settle.", + TAG, static_cast(status), + static_cast(future.id)); + } + } +} + +} // namespace rnwgpu::async diff --git a/packages/webgpu/cpp/rnwgpu/async/GpuEventLoop.h b/packages/webgpu/cpp/rnwgpu/async/GpuEventLoop.h new file mode 100644 index 000000000..07e90cd98 --- /dev/null +++ b/packages/webgpu/cpp/rnwgpu/async/GpuEventLoop.h @@ -0,0 +1,70 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "webgpu/webgpu_cpp.h" + +namespace rnwgpu::async { + +/** + * Background, event-driven driver for Dawn async operations. Replaces the old + * JS-thread ProcessEvents polling loop. + * + * Each pending wgpu::Future (registered with CallbackMode::WaitAnyOnly) is + * handed to addFuture() and waited on by a worker thread via + * `instance.WaitAny(future, UINT64_MAX)`. The wait is genuinely event-driven + * (zero idle CPU) and resolves the instant the GPU work completes, at which + * point Dawn fires the future's callback on the worker thread. That callback is + * responsible for marshalling back to the owning runtime's JS thread (via a + * RuntimeScheduler) to settle the JS Promise. + * + * Threading model (validated in Phase 0, spike 2): each WaitAny call waits on a + * *single* future, which is always a legal single-source wait. Multiple workers + * may block in WaitAny on the same instance concurrently; Dawn's EventManager + * is designed for this. + * + * The worker pool grows lazily up to a small cap as concurrent work demands, + * and threads are reused. Shared state is held behind a shared_ptr so detached + * workers (and the wgpu::Instance ref they need) outlive this object safely. + */ +class GpuEventLoop { +public: + explicit GpuEventLoop(wgpu::Instance instance); + ~GpuEventLoop(); + + GpuEventLoop(const GpuEventLoop &) = delete; + GpuEventLoop &operator=(const GpuEventLoop &) = delete; + + /** + * Wait for `future` to complete on a background thread. A future with id == 0 + * (no event to wait on, e.g. a deferred/immediate resolution) is ignored. + * Thread-safe. + */ + void addFuture(wgpu::Future future); + +private: + struct State { + explicit State(wgpu::Instance instance) : instance(std::move(instance)) {} + + wgpu::Instance instance; + std::mutex mutex; + std::condition_variable cv; + std::queue queue; + std::atomic_bool running{true}; + std::size_t idleWorkers = 0; + std::size_t totalWorkers = 0; + std::size_t maxWorkers = 1; + }; + + static void worker(std::shared_ptr state); + + std::shared_ptr _state; +}; + +} // namespace rnwgpu::async diff --git a/packages/webgpu/cpp/rnwgpu/async/JSIMicrotaskDispatcher.cpp b/packages/webgpu/cpp/rnwgpu/async/JSIMicrotaskDispatcher.cpp deleted file mode 100644 index 6231a833c..000000000 --- a/packages/webgpu/cpp/rnwgpu/async/JSIMicrotaskDispatcher.cpp +++ /dev/null @@ -1,23 +0,0 @@ -#include "JSIMicrotaskDispatcher.h" - -#include - -namespace rnwgpu::async { - -JSIMicrotaskDispatcher::JSIMicrotaskDispatcher(jsi::Runtime &runtime) - : _runtime(runtime) {} - -void JSIMicrotaskDispatcher::post(Work work) { - auto microtask = jsi::Function::createFromHostFunction( - _runtime, jsi::PropNameID::forAscii(_runtime, "AsyncMicrotask"), 0, - [work = std::move(work)]( - jsi::Runtime &runtime, const jsi::Value & /*thisValue*/, - const jsi::Value * /*args*/, size_t /*count*/) -> jsi::Value { - work(runtime); - return jsi::Value::undefined(); - }); - - _runtime.queueMicrotask(std::move(microtask)); -} - -} // namespace rnwgpu::async diff --git a/packages/webgpu/cpp/rnwgpu/async/JSIMicrotaskDispatcher.h b/packages/webgpu/cpp/rnwgpu/async/JSIMicrotaskDispatcher.h deleted file mode 100644 index bae208c5d..000000000 --- a/packages/webgpu/cpp/rnwgpu/async/JSIMicrotaskDispatcher.h +++ /dev/null @@ -1,22 +0,0 @@ -#pragma once - -#include "AsyncDispatcher.h" - -namespace rnwgpu::async { - -/** - * Dispatcher implementation backed by `jsi::Runtime::queueMicrotask`. - */ -class JSIMicrotaskDispatcher final - : public AsyncDispatcher, - public std::enable_shared_from_this { -public: - explicit JSIMicrotaskDispatcher(jsi::Runtime &runtime); - - void post(Work work) override; - -private: - jsi::Runtime &_runtime; -}; - -} // namespace rnwgpu::async diff --git a/packages/webgpu/cpp/rnwgpu/async/RuntimeContext.cpp b/packages/webgpu/cpp/rnwgpu/async/RuntimeContext.cpp new file mode 100644 index 000000000..f297ae6b0 --- /dev/null +++ b/packages/webgpu/cpp/rnwgpu/async/RuntimeContext.cpp @@ -0,0 +1,92 @@ +#include "RuntimeContext.h" + +#include +#include +#include + +#include "AsyncTaskHandle.h" +#include "WGPULogger.h" + +namespace rnwgpu::async { + +namespace { +struct RuntimeData { + std::shared_ptr runner; +}; +constexpr const char *TAG = "RuntimeContext"; +} // namespace + +RuntimeContext::RuntimeContext(std::shared_ptr scheduler, + std::shared_ptr eventLoop) + : _scheduler(std::move(scheduler)), _eventLoop(std::move(eventLoop)) { + if (!_scheduler) { + throw std::runtime_error( + "RuntimeContext requires a valid RuntimeScheduler."); + } + if (!_eventLoop) { + throw std::runtime_error("RuntimeContext requires a valid GpuEventLoop."); + } + Logger::logToConsole("[%s] Created runner (scheduler=%p, eventLoop=%p)", TAG, + _scheduler.get(), _eventLoop.get()); +} + +std::shared_ptr RuntimeContext::get(jsi::Runtime &runtime) { + auto data = runtime.getRuntimeData(runtimeDataUUID()); + if (!data) { + return nullptr; + } + auto stored = std::static_pointer_cast(data); + return stored->runner; +} + +std::shared_ptr +RuntimeContext::getOrCreate(jsi::Runtime &runtime, + std::shared_ptr scheduler, + std::shared_ptr eventLoop) { + auto existing = get(runtime); + if (existing) { + return existing; + } + + auto runner = std::make_shared(std::move(scheduler), + std::move(eventLoop)); + auto data = std::make_shared(); + data->runner = runner; + runtime.setRuntimeData(runtimeDataUUID(), data); + return runner; +} + +AsyncTaskHandle RuntimeContext::postTask(const TaskCallback &callback) { + auto handle = AsyncTaskHandle::create(_scheduler); + if (!handle.valid()) { + throw std::runtime_error("Failed to create AsyncTaskHandle."); + } + + auto resolve = handle.createResolveFunction(); + auto reject = handle.createRejectFunction(); + + wgpu::Future future{}; + try { + future = callback(resolve, reject); + } catch (const std::exception &exception) { + reject(exception.what()); + return handle; + } catch (...) { + reject("Unknown native error in RuntimeContext::postTask."); + return handle; + } + + _eventLoop->addFuture(future); + return handle; +} + +std::shared_ptr RuntimeContext::scheduler() const { + return _scheduler; +} + +jsi::UUID RuntimeContext::runtimeDataUUID() { + static const auto uuid = jsi::UUID(); + return uuid; +} + +} // namespace rnwgpu::async diff --git a/packages/webgpu/cpp/rnwgpu/async/RuntimeContext.h b/packages/webgpu/cpp/rnwgpu/async/RuntimeContext.h new file mode 100644 index 000000000..a7a5d46f4 --- /dev/null +++ b/packages/webgpu/cpp/rnwgpu/async/RuntimeContext.h @@ -0,0 +1,57 @@ +#pragma once + +#include +#include + +#include + +#include "AsyncTaskHandle.h" +#include "GpuEventLoop.h" +#include "RuntimeScheduler.h" + +#include "webgpu/webgpu_cpp.h" + +namespace jsi = facebook::jsi; + +namespace rnwgpu::async { + +/** + * Per-runtime coordinator for asynchronous WebGPU operations. + * + * Bundles the runtime's RuntimeScheduler (how to settle Promises back on the + * owning JS thread) with the GpuEventLoop (how to wait on Dawn futures off the + * JS thread). This replaces the previous ProcessEvents polling design: there is + * no tick loop and no idle CPU usage. + * + * A task callback registers a Dawn async op with CallbackMode::WaitAnyOnly and + * returns the resulting wgpu::Future, which is handed to the GpuEventLoop. A + * returned future with id == 0 means "no event to wait on" (deferred/immediate + * resolution, e.g. GPUDevice::getLost). + */ +class RuntimeContext : public std::enable_shared_from_this { +public: + using TaskCallback = + std::function; + + RuntimeContext(std::shared_ptr scheduler, + std::shared_ptr eventLoop); + + static std::shared_ptr get(jsi::Runtime &runtime); + static std::shared_ptr + getOrCreate(jsi::Runtime &runtime, + std::shared_ptr scheduler, + std::shared_ptr eventLoop); + + AsyncTaskHandle postTask(const TaskCallback &callback); + + std::shared_ptr scheduler() const; + +private: + static jsi::UUID runtimeDataUUID(); + + std::shared_ptr _scheduler; + std::shared_ptr _eventLoop; +}; + +} // namespace rnwgpu::async diff --git a/packages/webgpu/cpp/rnwgpu/async/RuntimeScheduler.h b/packages/webgpu/cpp/rnwgpu/async/RuntimeScheduler.h new file mode 100644 index 000000000..926b494c3 --- /dev/null +++ b/packages/webgpu/cpp/rnwgpu/async/RuntimeScheduler.h @@ -0,0 +1,31 @@ +#pragma once + +#include + +#include + +namespace rnwgpu::async { + +namespace jsi = facebook::jsi; + +/** + * Thread-safe "post this job onto a specific runtime's JS thread". + * + * Replaces the old AsyncDispatcher / JSIMicrotaskDispatcher, whose + * queueMicrotask-based dispatch was only safe to call from the runtime's own + * thread. A RuntimeScheduler can be called from any thread (e.g. the + * GpuEventLoop background threads) and guarantees the job runs on the owning + * runtime's JS thread. + */ +class RuntimeScheduler { +public: + virtual ~RuntimeScheduler() = default; + + /** + * Schedule `job` to run on this runtime's JS thread. Callable from any + * thread. Jobs are delivered in FIFO order relative to one another. + */ + virtual void scheduleOnJS(std::function job) = 0; +}; + +} // namespace rnwgpu::async