Skip to content

Commit a0c842a

Browse files
authored
chore(🗿): improve async WebGPU model (#3897)
1 parent d3d2e82 commit a0c842a

28 files changed

Lines changed: 675 additions & 421 deletions

‎packages/skia/android/CMakeLists.txt‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,9 +120,8 @@ if(SK_GRAPHITE)
120120
"${PROJECT_SOURCE_DIR}/../cpp/jsi2/Promise.cpp"
121121

122122
# WebGPU async system
123-
"${PROJECT_SOURCE_DIR}/../cpp/rnwgpu/async/AsyncRunner.cpp"
124123
"${PROJECT_SOURCE_DIR}/../cpp/rnwgpu/async/AsyncTaskHandle.cpp"
125-
"${PROJECT_SOURCE_DIR}/../cpp/rnwgpu/async/JSIMicrotaskDispatcher.cpp"
124+
"${PROJECT_SOURCE_DIR}/../cpp/rnwgpu/async/RuntimeContext.cpp"
126125

127126
# WebGPU API
128127
"${PROJECT_SOURCE_DIR}/../cpp/rnwgpu/api/GPU.cpp"

‎packages/skia/cpp/api/JsiSkApi.h‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
#ifdef SK_GRAPHITE
1010
#include "rnskia/RNDawnContext.h"
1111
#include "rnwgpu/api/GPUDevice.h"
12-
#include "rnwgpu/async/AsyncRunner.h"
12+
#include "rnwgpu/async/RuntimeContext.h"
1313
#endif
1414

1515
#include "JsiNativeBuffer.h"
@@ -164,12 +164,12 @@ class JsiSkApi : public JsiSkHostObject {
164164
"getDevice", JSI_HOST_FUNCTION_LAMBDA {
165165
#ifdef SK_GRAPHITE
166166
auto &dawnContext = DawnContext::getInstance();
167-
auto asyncRunner = rnwgpu::async::AsyncRunner::get(runtime);
168-
if (!asyncRunner) {
169-
throw jsi::JSError(runtime, "AsyncRunner not initialized");
170-
}
167+
// Per-runtime context: async ops on this device resolve on the calling
168+
// runtime's own thread (via its ProcessEvents pump).
169+
auto context = rnwgpu::async::RuntimeContext::getOrCreate(
170+
runtime, dawnContext.getWGPUInstance());
171171
auto device = std::make_shared<rnwgpu::GPUDevice>(
172-
dawnContext.getWGPUDevice(), asyncRunner, "Skia Device");
172+
dawnContext.getWGPUDevice(), context, "Skia Device");
173173
return rnwgpu::GPUDevice::create(runtime, device);
174174
#else
175175
throw jsi::JSError(runtime,

‎packages/skia/cpp/jsi2/NativeObject.h‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,29 @@ class NativeObject : public jsi::NativeState,
432432
prototype.setProperty(runtime, name, func);
433433
}
434434

435+
/**
436+
* Install a method whose native implementation needs the calling jsi::Runtime
437+
* as its first parameter. Used by entry points that must act per-runtime
438+
* (e.g. GPU::requestAdapter, which creates a per-runtime RuntimeContext).
439+
*/
440+
template <typename ReturnType, typename... Args>
441+
static void
442+
installMethodWithRuntime(jsi::Runtime &runtime, jsi::Object &prototype,
443+
const char *name,
444+
ReturnType (Derived::*method)(jsi::Runtime &,
445+
Args...)) {
446+
auto func = jsi::Function::createFromHostFunction(
447+
runtime, jsi::PropNameID::forUtf8(runtime, name), sizeof...(Args),
448+
[method](jsi::Runtime &rt, const jsi::Value &thisVal,
449+
const jsi::Value *args, size_t count) -> jsi::Value {
450+
auto native = Derived::fromValue(rt, thisVal);
451+
return callMethodWithRuntime(native.get(), method, rt, args,
452+
std::index_sequence_for<Args...>{},
453+
count);
454+
});
455+
prototype.setProperty(runtime, name, func);
456+
}
457+
435458
/**
436459
* Install a getter on the prototype.
437460
*/
@@ -567,6 +590,22 @@ class NativeObject : public jsi::NativeState,
567590
}
568591

569592
private:
593+
// Helper to call a method that takes the calling jsi::Runtime as its first
594+
// parameter, with JSI argument conversion for the rest and JSI conversion of
595+
// the result.
596+
template <typename ReturnType, typename... Args, size_t... Is>
597+
static jsi::Value
598+
callMethodWithRuntime(Derived *obj,
599+
ReturnType (Derived::*method)(jsi::Runtime &, Args...),
600+
jsi::Runtime &runtime, const jsi::Value *args,
601+
std::index_sequence<Is...>, size_t count) {
602+
ReturnType result = (obj->*method)(
603+
runtime, rnwgpu::JSIConverter<std::decay_t<Args>>::fromJSI(
604+
runtime, args[Is], Is >= count)...);
605+
return rnwgpu::JSIConverter<std::decay_t<ReturnType>>::toJSI(
606+
runtime, std::move(result));
607+
}
608+
570609
// Helper to call a method with JSI argument conversion
571610
template <typename ReturnType, typename... Args, size_t... Is>
572611
static jsi::Value callMethod(Derived *obj,

‎packages/skia/cpp/rnskia/RNSkManager.cpp‎

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
#include "rnwgpu/api/descriptors/GPUMapMode.h"
2424
#include "rnwgpu/api/descriptors/GPUShaderStage.h"
2525
#include "rnwgpu/api/descriptors/GPUTextureUsage.h"
26+
#include "rnwgpu/api/WebGPUConstants.h"
27+
#include "rnwgpu/async/RuntimeContext.h"
2628
#include "jsi2/Promise.h"
2729

2830
#include "include/core/SkData.h"
@@ -82,6 +84,12 @@ void RNSkManager::installBindings() {
8284
jsi::Object::createFromHostObject(*_jsRuntime, _viewApi));
8385

8486
#ifdef SK_GRAPHITE
87+
// Register the main runtime + its CallInvoker so spontaneous events
88+
// (device.lost / uncapturederror) on main-runtime devices can be delivered to
89+
// the JS thread without the ProcessEvents pump. Worklet-runtime devices have
90+
// no invoker (best-effort; see the RuntimeContext "Threading model" doc).
91+
rnwgpu::async::RuntimeContext::registerMainRuntime(_jsRuntime, _jsCallInvoker);
92+
8593
// Install WebGPU constructors
8694
rnwgpu::GPU::installConstructor(*_jsRuntime);
8795
rnwgpu::GPUUncapturedErrorEvent::installConstructor(*_jsRuntime);
@@ -104,18 +112,26 @@ void RNSkManager::installBindings() {
104112
std::move(navigator));
105113
}
106114

107-
// Install WebGPU constant objects as plain JS objects
108-
_jsRuntime->global().setProperty(*_jsRuntime, "GPUBufferUsage",
109-
rnwgpu::GPUBufferUsage::create(*_jsRuntime));
110-
_jsRuntime->global().setProperty(*_jsRuntime, "GPUColorWrite",
111-
rnwgpu::GPUColorWrite::create(*_jsRuntime));
112-
_jsRuntime->global().setProperty(*_jsRuntime, "GPUMapMode",
113-
rnwgpu::GPUMapMode::create(*_jsRuntime));
114-
_jsRuntime->global().setProperty(*_jsRuntime, "GPUShaderStage",
115-
rnwgpu::GPUShaderStage::create(*_jsRuntime));
115+
// Install WebGPU constant objects as plain JS objects on the main runtime.
116+
rnwgpu::installWebGPUConstants(*_jsRuntime);
117+
118+
// Install a global `installWebGPU()` host function so worklet runtimes can get
119+
// the same constants. A host function captured into a worklet is serialized as
120+
// a SerializableHostFunction and re-created on the worklet runtime, so the body
121+
// runs there (its `rt` is the worklet runtime) and installs the constants on
122+
// that runtime. The constants come from the native wgpu::*Usage enums, so the
123+
// values stay a single source of truth across every runtime. Calling it on a
124+
// runtime that already has the globals is a safe, idempotent no-op.
116125
_jsRuntime->global().setProperty(
117-
*_jsRuntime, "GPUTextureUsage",
118-
rnwgpu::GPUTextureUsage::create(*_jsRuntime));
126+
*_jsRuntime, "installWebGPU",
127+
jsi::Function::createFromHostFunction(
128+
*_jsRuntime, jsi::PropNameID::forAscii(*_jsRuntime, "installWebGPU"),
129+
0,
130+
[](jsi::Runtime &rt, const jsi::Value & /*thisVal*/,
131+
const jsi::Value * /*args*/, size_t /*count*/) -> jsi::Value {
132+
rnwgpu::installWebGPUConstants(rt);
133+
return jsi::Value::undefined();
134+
}));
119135

120136
// Install RNWebGPU global object for WebGPU Canvas support
121137
auto rnWebGPU = std::make_shared<rnwgpu::RNWebGPU>(gpu, nullptr);

‎packages/skia/cpp/rnwgpu/SurfaceRegistry.h‎

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@
77

88
#include "webgpu/webgpu_cpp.h"
99

10+
#ifdef __APPLE__
11+
namespace dawn::native::metal {
12+
void WaitForCommandsToBeScheduled(WGPUDevice device);
13+
} // namespace dawn::native::metal
14+
#endif
15+
1016
namespace rnwgpu {
1117

1218
struct NativeInfo {
@@ -112,13 +118,39 @@ class SurfaceInfo {
112118
height = newHeight;
113119
}
114120

115-
void present() {
121+
// Present the current surface texture. Called synchronously from the thread
122+
// that did getCurrentTexture / submit (via GPUCanvasContext::present), so it
123+
// preserves Dawn surface thread-affinity. No-op when offscreen / unconfigured
124+
// (no surface).
125+
void presentFrame() {
126+
#ifdef __APPLE__
127+
// Ensure command buffers are scheduled before presenting. Read the device
128+
// under a shared lock, then wait without holding it (the wait can block).
129+
// The device may be reconfigured between the two locks; that is safe because
130+
// present() is called on the rendering thread right after submit(), the wait
131+
// just flushes that thread's already-submitted work, and the Present() below
132+
// re-checks `surface` under the unique lock before touching it.
133+
wgpu::Device device;
134+
{
135+
std::shared_lock<std::shared_mutex> lock(_mutex);
136+
device = config.device;
137+
}
138+
if (device) {
139+
dawn::native::metal::WaitForCommandsToBeScheduled(device.Get());
140+
}
141+
#endif
116142
std::unique_lock<std::shared_mutex> lock(_mutex);
117143
if (surface) {
118144
surface.Present();
119145
}
120146
}
121147

148+
// True when an on-screen wgpu::Surface is attached (vs offscreen texture).
149+
bool hasSurface() {
150+
std::shared_lock<std::shared_mutex> lock(_mutex);
151+
return surface != nullptr;
152+
}
153+
122154
wgpu::Texture getCurrentTexture() {
123155
std::shared_lock<std::shared_mutex> lock(_mutex);
124156
if (surface) {

‎packages/skia/cpp/rnwgpu/api/GPU.cpp‎

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,15 @@
99

1010
#include "Convertors.h"
1111
#include "jsi2/JSIConverter.h"
12-
#include "rnwgpu/async/JSIMicrotaskDispatcher.h"
12+
#include "rnwgpu/async/RuntimeContext.h"
1313

1414
namespace rnwgpu {
1515

16-
GPU::GPU(jsi::Runtime &runtime, wgpu::Instance instance)
17-
: NativeObject(CLASS_NAME), _instance(instance) {
18-
auto dispatcher = std::make_shared<async::JSIMicrotaskDispatcher>(runtime);
19-
_async = async::AsyncRunner::getOrCreate(runtime, _instance, dispatcher);
20-
}
16+
GPU::GPU(jsi::Runtime & /*runtime*/, wgpu::Instance instance)
17+
: NativeObject(CLASS_NAME), _instance(instance) {}
2118

2219
async::AsyncTaskHandle GPU::requestAdapter(
20+
jsi::Runtime &runtime,
2321
std::optional<std::shared_ptr<GPURequestAdapterOptions>> options) {
2422
wgpu::RequestAdapterOptions aOptions;
2523
Convertor conv;
@@ -32,21 +30,26 @@ async::AsyncTaskHandle GPU::requestAdapter(
3230
constexpr auto kDefaultBackendType = wgpu::BackendType::Vulkan;
3331
#endif
3432
aOptions.backendType = kDefaultBackendType;
35-
return _async->postTask(
36-
[this, aOptions](const async::AsyncTaskHandle::ResolveFunction &resolve,
37-
const async::AsyncTaskHandle::RejectFunction &reject) {
33+
34+
// Per-runtime context: async ops requested on this runtime resolve on this
35+
// runtime's own thread (via its ProcessEvents pump).
36+
auto context = async::RuntimeContext::getOrCreate(runtime, _instance);
37+
return context->postTask(
38+
[this, aOptions,
39+
context](const async::AsyncTaskHandle::ResolveFunction &resolve,
40+
const async::AsyncTaskHandle::RejectFunction &reject) {
3841
_instance.RequestAdapter(
3942
&aOptions, wgpu::CallbackMode::AllowProcessEvents,
40-
[asyncRunner = _async, resolve,
43+
[context, resolve,
4144
reject](wgpu::RequestAdapterStatus status, wgpu::Adapter adapter,
4245
wgpu::StringView message) {
4346
if (message.length) {
4447
fprintf(stderr, "%s", message.data);
4548
}
4649

4750
if (status == wgpu::RequestAdapterStatus::Success && adapter) {
48-
auto adapterHost = std::make_shared<GPUAdapter>(
49-
std::move(adapter), asyncRunner);
51+
auto adapterHost =
52+
std::make_shared<GPUAdapter>(std::move(adapter), context);
5053
auto result =
5154
std::variant<std::nullptr_t, std::shared_ptr<GPUAdapter>>(
5255
adapterHost);

‎packages/skia/cpp/rnwgpu/api/GPU.h‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@
99

1010
#include "jsi2/NativeObject.h"
1111

12-
#include "rnwgpu/async/AsyncRunner.h"
1312
#include "rnwgpu/async/AsyncTaskHandle.h"
13+
#include "rnwgpu/async/RuntimeContext.h"
1414

1515
#include "webgpu/webgpu_cpp.h"
1616

@@ -32,15 +32,19 @@ class GPU : public NativeObject<GPU> {
3232
public:
3333
std::string getBrand() { return CLASS_NAME; }
3434

35+
// requestAdapter needs the calling runtime so each runtime gets its own
36+
// RuntimeContext (and ProcessEvents pump on its own thread).
3537
async::AsyncTaskHandle requestAdapter(
38+
jsi::Runtime &runtime,
3639
std::optional<std::shared_ptr<GPURequestAdapterOptions>> options);
3740
wgpu::TextureFormat getPreferredCanvasFormat();
3841

3942
std::unordered_set<std::string> getWgslLanguageFeatures();
4043

4144
static void definePrototype(jsi::Runtime &runtime, jsi::Object &prototype) {
4245
installGetter(runtime, prototype, "__brand", &GPU::getBrand);
43-
installMethod(runtime, prototype, "requestAdapter", &GPU::requestAdapter);
46+
installMethodWithRuntime(runtime, prototype, "requestAdapter",
47+
&GPU::requestAdapter);
4448
installMethod(runtime, prototype, "getPreferredCanvasFormat",
4549
&GPU::getPreferredCanvasFormat);
4650
installGetter(runtime, prototype, "wgslLanguageFeatures",
@@ -51,7 +55,6 @@ class GPU : public NativeObject<GPU> {
5155

5256
private:
5357
wgpu::Instance _instance;
54-
std::shared_ptr<async::AsyncRunner> _async;
5558
};
5659

5760
} // namespace rnwgpu

‎packages/skia/cpp/rnwgpu/api/GPUAdapter.cpp‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ async::AsyncTaskHandle GPUAdapter::requestDevice(
138138
}
139139
_instance.RequestDevice(
140140
&deviceDesc, wgpu::CallbackMode::AllowProcessEvents,
141-
[asyncRunner = _async, resolve, reject, label, creationRuntime,
141+
[context = _async, resolve, reject, label, creationRuntime,
142142
deviceLostBinding](wgpu::RequestDeviceStatus status,
143143
wgpu::Device device,
144144
wgpu::StringView message) {
@@ -190,7 +190,7 @@ async::AsyncTaskHandle GPUAdapter::requestDevice(
190190
creationRuntime);
191191

192192
auto deviceHost = std::make_shared<GPUDevice>(std::move(device),
193-
asyncRunner, label);
193+
context, label);
194194
*deviceLostBinding = deviceHost;
195195
resolve([deviceHost = std::move(deviceHost)](
196196
jsi::Runtime &runtime) mutable {

‎packages/skia/cpp/rnwgpu/api/GPUAdapter.h‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88

99
#include "jsi2/NativeObject.h"
1010

11-
#include "rnwgpu/async/AsyncRunner.h"
1211
#include "rnwgpu/async/AsyncTaskHandle.h"
12+
#include "rnwgpu/async/RuntimeContext.h"
1313

1414
#include "webgpu/webgpu_cpp.h"
1515

@@ -27,7 +27,7 @@ class GPUAdapter : public NativeObject<GPUAdapter> {
2727
static constexpr const char *CLASS_NAME = "GPUAdapter";
2828

2929
explicit GPUAdapter(wgpu::Adapter instance,
30-
std::shared_ptr<async::AsyncRunner> async)
30+
std::shared_ptr<async::RuntimeContext> async)
3131
: NativeObject(CLASS_NAME), _instance(instance), _async(async) {}
3232

3333
public:
@@ -53,7 +53,7 @@ class GPUAdapter : public NativeObject<GPUAdapter> {
5353

5454
private:
5555
wgpu::Adapter _instance;
56-
std::shared_ptr<async::AsyncRunner> _async;
56+
std::shared_ptr<async::RuntimeContext> _async;
5757
};
5858

5959
} // namespace rnwgpu

‎packages/skia/cpp/rnwgpu/api/GPUBuffer.h‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@
99

1010
#include "jsi2/NativeObject.h"
1111

12-
#include "rnwgpu/async/AsyncRunner.h"
1312
#include "rnwgpu/async/AsyncTaskHandle.h"
13+
#include "rnwgpu/async/RuntimeContext.h"
1414

1515
#include "webgpu/webgpu_cpp.h"
1616

@@ -25,7 +25,7 @@ class GPUBuffer : public NativeObject<GPUBuffer> {
2525
static constexpr const char *CLASS_NAME = "GPUBuffer";
2626

2727
explicit GPUBuffer(wgpu::Buffer instance,
28-
std::shared_ptr<async::AsyncRunner> async,
28+
std::shared_ptr<async::RuntimeContext> async,
2929
std::string label)
3030
: NativeObject(CLASS_NAME), _instance(instance), _async(async),
3131
_label(label) {}
@@ -71,7 +71,7 @@ class GPUBuffer : public NativeObject<GPUBuffer> {
7171

7272
private:
7373
wgpu::Buffer _instance;
74-
std::shared_ptr<async::AsyncRunner> _async;
74+
std::shared_ptr<async::RuntimeContext> _async;
7575
std::string _label;
7676
struct Mapping {
7777
uint64_t start;

0 commit comments

Comments
 (0)