Skip to content

Commit b368a9a

Browse files
feat(🚧): add fence for shared texture memory (#375)
--------- Co-authored-by: William Candillon <wcandillon@gmail.com>
1 parent 6a26fa9 commit b368a9a

15 files changed

Lines changed: 656 additions & 42 deletions

‎apps/example/src/SharedTextureMemory/SharedTextureMemory.tsx‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -202,11 +202,7 @@ export const SharedTextureMemory = () => {
202202
label: "video-frame",
203203
});
204204
const texture = memory.createTexture();
205-
if (!memory.beginAccess(texture, true)) {
206-
texture.destroy();
207-
frame.release();
208-
return null;
209-
}
205+
memory.beginAccess(texture, true);
210206
const uniformBuffer = device.createBuffer({
211207
size: 16, // vec2<f32> padded to 16-byte uniform alignment
212208
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
@@ -230,7 +226,11 @@ export const SharedTextureMemory = () => {
230226
};
231227

232228
const releaseBound = (b: Bound) => {
233-
b.memory.endAccess(b.texture);
229+
try {
230+
b.memory.endAccess(b.texture);
231+
} catch (e) {
232+
console.warn("[SharedTextureMemory] endAccess failed:", e);
233+
}
234234
b.texture.destroy();
235235
b.uniformBuffer.destroy();
236236
b.frame.release();

‎packages/webgpu/android/CMakeLists.txt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ add_library(${PACKAGE_NAME} SHARED
3737
../cpp/rnwgpu/api/GPUCommandEncoder.cpp
3838
../cpp/rnwgpu/api/GPUQuerySet.cpp
3939
../cpp/rnwgpu/api/GPUTexture.cpp
40+
../cpp/rnwgpu/api/GPUSharedFence.cpp
4041
../cpp/rnwgpu/api/GPUSharedTextureMemory.cpp
4142
../cpp/rnwgpu/api/GPUExternalTexture.cpp
4243
../cpp/rnwgpu/api/GPURenderBundleEncoder.cpp

‎packages/webgpu/cpp/rnwgpu/RNWebGPUManager.cpp‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
#include "GPURenderPassEncoder.h"
3232
#include "GPURenderPipeline.h"
3333
#include "GPUSampler.h"
34+
#include "GPUSharedFence.h"
3435
#include "GPUSharedTextureMemory.h"
3536
#include "GPUShaderModule.h"
3637
#include "GPUSupportedLimits.h"
@@ -106,6 +107,7 @@ RNWebGPUManager::RNWebGPUManager(
106107
GPURenderPassEncoder::installConstructor(*_jsRuntime);
107108
GPURenderPipeline::installConstructor(*_jsRuntime);
108109
GPUSampler::installConstructor(*_jsRuntime);
110+
GPUSharedFence::installConstructor(*_jsRuntime);
109111
GPUSharedTextureMemory::installConstructor(*_jsRuntime);
110112
GPUShaderModule::installConstructor(*_jsRuntime);
111113
GPUSupportedLimits::installConstructor(*_jsRuntime);

‎packages/webgpu/cpp/rnwgpu/api/GPUDevice.cpp‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,55 @@ std::shared_ptr<GPUSharedTextureMemory> GPUDevice::importSharedTextureMemory(
286286
std::move(label));
287287
}
288288

289+
std::shared_ptr<GPUSharedFence> GPUDevice::importSharedFence(
290+
std::shared_ptr<GPUSharedFenceDescriptor> descriptor) {
291+
if (!descriptor || descriptor->handle == nullptr) {
292+
throw std::runtime_error("GPUDevice::importSharedFence(): handle must be a "
293+
"non-null native handle");
294+
}
295+
296+
wgpu::SharedFenceDescriptor desc{};
297+
std::string label = descriptor->label.value_or("");
298+
if (!label.empty()) {
299+
desc.label = wgpu::StringView(label.c_str(), label.size());
300+
}
301+
302+
// The chained platform descriptor must outlive the synchronous
303+
// ImportSharedFence() below; declare them all and chain the matching one.
304+
wgpu::SharedFenceMTLSharedEventDescriptor mtlDesc{};
305+
wgpu::SharedFenceSyncFDDescriptor syncFdDesc{};
306+
wgpu::SharedFenceVkSemaphoreOpaqueFDDescriptor vkFdDesc{};
307+
308+
const std::string &type = descriptor->type;
309+
if (type == "mtl-shared-event") {
310+
// handle is an id<MTLSharedEvent> pointer.
311+
mtlDesc.sharedEvent = descriptor->handle;
312+
desc.nextInChain = &mtlDesc;
313+
} else if (type == "sync-fd") {
314+
// handle is an OS file descriptor.
315+
syncFdDesc.handle =
316+
static_cast<int>(reinterpret_cast<uintptr_t>(descriptor->handle));
317+
desc.nextInChain = &syncFdDesc;
318+
} else if (type == "vk-semaphore-opaque-fd") {
319+
vkFdDesc.handle =
320+
static_cast<int>(reinterpret_cast<uintptr_t>(descriptor->handle));
321+
desc.nextInChain = &vkFdDesc;
322+
} else {
323+
throw std::runtime_error(
324+
"GPUDevice::importSharedFence(): unsupported fence type '" + type +
325+
"' (expected 'mtl-shared-event', 'sync-fd' or "
326+
"'vk-semaphore-opaque-fd')");
327+
}
328+
329+
auto fence = _instance.ImportSharedFence(&desc);
330+
if (fence == nullptr) {
331+
throw std::runtime_error(
332+
"GPUDevice::importSharedFence(): ImportSharedFence returned null - is "
333+
"the matching 'shared-fence-*' feature enabled on the device?");
334+
}
335+
return std::make_shared<GPUSharedFence>(std::move(fence), std::move(label));
336+
}
337+
289338
async::AsyncTaskHandle GPUDevice::createComputePipelineAsync(
290339
std::shared_ptr<GPUComputePipelineDescriptor> descriptor) {
291340
wgpu::ComputePipelineDescriptor desc{};

‎packages/webgpu/cpp/rnwgpu/api/GPUDevice.h‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
#include "GPURenderPipelineDescriptor.h"
4646
#include "GPUSampler.h"
4747
#include "GPUSamplerDescriptor.h"
48+
#include "GPUSharedFenceDescriptor.h"
4849
#include "GPUSharedTextureMemory.h"
4950
#include "GPUSharedTextureMemoryDescriptor.h"
5051
#include "GPUShaderModule.h"
@@ -120,6 +121,8 @@ class GPUDevice : public NativeObject<GPUDevice> {
120121
std::shared_ptr<GPUExternalTextureDescriptor> descriptor);
121122
std::shared_ptr<GPUSharedTextureMemory> importSharedTextureMemory(
122123
std::shared_ptr<GPUSharedTextureMemoryDescriptor> descriptor);
124+
std::shared_ptr<GPUSharedFence> importSharedFence(
125+
std::shared_ptr<GPUSharedFenceDescriptor> descriptor);
123126
std::shared_ptr<GPUBindGroupLayout> createBindGroupLayout(
124127
std::shared_ptr<GPUBindGroupLayoutDescriptor> descriptor);
125128
std::shared_ptr<GPUPipelineLayout>
@@ -175,6 +178,8 @@ class GPUDevice : public NativeObject<GPUDevice> {
175178
&GPUDevice::importExternalTexture);
176179
installMethod(runtime, prototype, "importSharedTextureMemory",
177180
&GPUDevice::importSharedTextureMemory);
181+
installMethod(runtime, prototype, "importSharedFence",
182+
&GPUDevice::importSharedFence);
178183
installMethod(runtime, prototype, "createBindGroupLayout",
179184
&GPUDevice::createBindGroupLayout);
180185
installMethod(runtime, prototype, "createPipelineLayout",
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
#include "GPUSharedFence.h"
2+
3+
#include <cstdint>
4+
#include <string>
5+
6+
#if defined(__ANDROID__)
7+
#include <fcntl.h>
8+
#endif
9+
10+
namespace rnwgpu {
11+
12+
namespace {
13+
14+
// Kebab-case names matching the shared-fence-* feature strings (see Unions.h /
15+
// GPUFeatures.h).
16+
std::string sharedFenceTypeToString(wgpu::SharedFenceType type) {
17+
switch (type) {
18+
case wgpu::SharedFenceType::MTLSharedEvent:
19+
return "mtl-shared-event";
20+
case wgpu::SharedFenceType::SyncFD:
21+
return "sync-fd";
22+
case wgpu::SharedFenceType::VkSemaphoreOpaqueFD:
23+
return "vk-semaphore-opaque-fd";
24+
case wgpu::SharedFenceType::VkSemaphoreZirconHandle:
25+
return "vk-semaphore-zircon-handle";
26+
case wgpu::SharedFenceType::DXGISharedHandle:
27+
return "dxgi-shared-handle";
28+
case wgpu::SharedFenceType::EGLSync:
29+
return "egl-sync";
30+
default:
31+
return "";
32+
}
33+
}
34+
35+
} // namespace
36+
37+
jsi::Value GPUSharedFence::exportInfo(jsi::Runtime &runtime, const jsi::Value &,
38+
const jsi::Value *, size_t) {
39+
wgpu::SharedFenceExportInfo info{};
40+
uint64_t handle = 0;
41+
42+
#if defined(__APPLE__)
43+
// Apple: the handle is an id<MTLSharedEvent> pointer.
44+
wgpu::SharedFenceMTLSharedEventExportInfo mtlInfo{};
45+
info.nextInChain = &mtlInfo;
46+
_instance.ExportInfo(&info);
47+
handle = reinterpret_cast<uint64_t>(mtlInfo.sharedEvent);
48+
#elif defined(__ANDROID__)
49+
// Android: the handle is an OS file descriptor (sync_fd). Dawn's ExportInfo returns a BORROWED fd — it is
50+
// owned by the SharedFence and closed when the fence is destroyed. This exported handle is documented as
51+
// caller-owned (the caller must close() it), so dup() it. Without the dup the same fd is closed twice —
52+
// once by the caller and once by Dawn on fence destruction — tripping Android's fdsan (double-close abort).
53+
wgpu::SharedFenceSyncFDExportInfo fdInfo{};
54+
info.nextInChain = &fdInfo;
55+
_instance.ExportInfo(&info);
56+
int exportedFd =
57+
fdInfo.handle >= 0 ? ::fcntl(fdInfo.handle, F_DUPFD_CLOEXEC, 0) : fdInfo.handle;
58+
handle = static_cast<uint64_t>(static_cast<uint32_t>(exportedFd));
59+
#else
60+
// react-native-webgpu only targets Apple (Metal) and Android (Vulkan). On any
61+
// other platform there is no native handle convention to expose, so fail loudly
62+
// rather than handing back a meaningless handle of 0.
63+
throw jsi::JSError(runtime,
64+
"GPUSharedFence::export(): unsupported platform (only "
65+
"Apple/Metal and Android/Vulkan are supported)");
66+
#endif
67+
68+
jsi::Object result(runtime);
69+
result.setProperty(
70+
runtime, "type",
71+
jsi::String::createFromUtf8(runtime, sharedFenceTypeToString(info.type)));
72+
result.setProperty(runtime, "handle",
73+
jsi::BigInt::fromUint64(runtime, handle));
74+
return result;
75+
}
76+
77+
} // namespace rnwgpu
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
#pragma once
2+
3+
#include <memory>
4+
#include <string>
5+
6+
#include "NativeObject.h"
7+
8+
#include "webgpu/webgpu_cpp.h"
9+
10+
namespace rnwgpu {
11+
12+
namespace jsi = facebook::jsi;
13+
14+
// Wraps a wgpu::SharedFence: a native GPU sync primitive (id<MTLSharedEvent> on
15+
// Apple, sync-fd / VkSemaphore on Android).
16+
class GPUSharedFence : public NativeObject<GPUSharedFence> {
17+
public:
18+
static constexpr const char *CLASS_NAME = "GPUSharedFence";
19+
20+
explicit GPUSharedFence(wgpu::SharedFence instance, std::string label)
21+
: NativeObject(CLASS_NAME), _instance(std::move(instance)),
22+
_label(std::move(label)) {}
23+
24+
public:
25+
std::string getBrand() { return CLASS_NAME; }
26+
27+
// export() -> { type, handle }: exposes the native handle (as a BigInt) so
28+
// app code can wait on or signal the fence. The caller owns the returned
29+
// handle (e.g. an exported sync-fd must be close()d).
30+
jsi::Value exportInfo(jsi::Runtime &runtime, const jsi::Value &thisVal,
31+
const jsi::Value *args, size_t count);
32+
33+
std::string getLabel() { return _label; }
34+
void setLabel(const std::string &label) {
35+
_label = label;
36+
_instance.SetLabel(_label.c_str());
37+
}
38+
39+
static void definePrototype(jsi::Runtime &runtime, jsi::Object &prototype) {
40+
installGetter(runtime, prototype, "__brand", &GPUSharedFence::getBrand);
41+
installMethod(runtime, prototype, "export", &GPUSharedFence::exportInfo);
42+
installGetterSetter(runtime, prototype, "label", &GPUSharedFence::getLabel,
43+
&GPUSharedFence::setLabel);
44+
}
45+
46+
inline wgpu::SharedFence get() { return _instance; }
47+
48+
private:
49+
wgpu::SharedFence _instance;
50+
std::string _label;
51+
};
52+
53+
} // namespace rnwgpu

‎packages/webgpu/cpp/rnwgpu/api/GPUSharedTextureMemory.cpp‎

Lines changed: 60 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,18 +27,38 @@ std::shared_ptr<GPUTexture> GPUSharedTextureMemory::createTexture(
2727
descriptor.value()->label.value_or(""));
2828
}
2929

30-
bool GPUSharedTextureMemory::beginAccess(std::shared_ptr<GPUTexture> texture,
31-
bool initialized) {
30+
void GPUSharedTextureMemory::beginAccess(
31+
std::shared_ptr<GPUTexture> texture, bool initialized,
32+
std::optional<std::vector<std::shared_ptr<GPUSharedFenceState>>> fences) {
3233
if (!texture) {
3334
throw std::runtime_error(
3435
"GPUSharedTextureMemory::beginAccess(): texture is null");
3536
}
3637
wgpu::SharedTextureMemoryBeginAccessDescriptor desc{};
3738
desc.initialized = initialized;
3839
desc.concurrentRead = false;
39-
desc.fenceCount = 0;
40-
desc.fences = nullptr;
41-
desc.signaledValues = nullptr;
40+
41+
// Built in lockstep so fenceCount covers both arrays, and kept in locals so
42+
// the raw pointers outlive the synchronous BeginAccess() below.
43+
std::vector<wgpu::SharedFence> rawFences;
44+
std::vector<uint64_t> values;
45+
if (fences.has_value()) {
46+
for (const auto &state : *fences) {
47+
if (state && state->fence) {
48+
rawFences.push_back(state->fence->get());
49+
values.push_back(state->signaledValue);
50+
}
51+
}
52+
}
53+
if (!rawFences.empty()) {
54+
desc.fenceCount = rawFences.size();
55+
desc.fences = rawFences.data();
56+
desc.signaledValues = values.data();
57+
} else {
58+
desc.fenceCount = 0;
59+
desc.fences = nullptr;
60+
desc.signaledValues = nullptr;
61+
}
4262

4363
#if defined(__ANDROID__)
4464
// Dawn's Vulkan backend (AHardwareBuffer) validates that the begin-access
@@ -55,14 +75,21 @@ bool GPUSharedTextureMemory::beginAccess(std::shared_ptr<GPUTexture> texture,
5575
#endif
5676

5777
auto status = _instance.BeginAccess(texture->get(), &desc);
58-
return static_cast<bool>(status);
78+
if (!status) {
79+
throw std::runtime_error("GPUSharedTextureMemory::beginAccess() failed");
80+
}
5981
}
6082

61-
bool GPUSharedTextureMemory::endAccess(std::shared_ptr<GPUTexture> texture) {
62-
if (!texture) {
63-
throw std::runtime_error(
64-
"GPUSharedTextureMemory::endAccess(): texture is null");
83+
jsi::Value GPUSharedTextureMemory::endAccess(jsi::Runtime &runtime,
84+
const jsi::Value &,
85+
const jsi::Value *args,
86+
size_t count) {
87+
if (count < 1 || !args[0].isObject()) {
88+
throw jsi::JSError(
89+
runtime, "GPUSharedTextureMemory::endAccess(): expected (texture)");
6590
}
91+
auto texture = GPUTexture::fromValue(runtime, args[0]);
92+
6693
wgpu::SharedTextureMemoryEndAccessState state{};
6794

6895
#if defined(__ANDROID__)
@@ -74,7 +101,29 @@ bool GPUSharedTextureMemory::endAccess(std::shared_ptr<GPUTexture> texture) {
74101
#endif
75102

76103
auto status = _instance.EndAccess(texture->get(), &state);
77-
return static_cast<bool>(status);
104+
if (!status) {
105+
throw jsi::JSError(runtime, "GPUSharedTextureMemory::endAccess() failed");
106+
}
107+
108+
// Copy each wgpu::SharedFence (ref-counted) into its own GPUSharedFence
109+
// wrapper before `state` is destroyed.
110+
jsi::Array fences(runtime, state.fenceCount);
111+
for (size_t i = 0; i < state.fenceCount; i++) {
112+
wgpu::SharedFence fence = state.fences[i];
113+
auto wrapper = std::make_shared<GPUSharedFence>(std::move(fence), "");
114+
jsi::Object entry(runtime);
115+
entry.setProperty(runtime, "fence",
116+
GPUSharedFence::create(runtime, std::move(wrapper)));
117+
entry.setProperty(runtime, "signaledValue",
118+
jsi::BigInt::fromUint64(runtime, state.signaledValues[i]));
119+
fences.setValueAtIndex(runtime, i, std::move(entry));
120+
}
121+
122+
jsi::Object result(runtime);
123+
result.setProperty(runtime, "initialized",
124+
jsi::Value(static_cast<bool>(state.initialized)));
125+
result.setProperty(runtime, "fences", std::move(fences));
126+
return result;
78127
}
79128

80129
} // namespace rnwgpu

0 commit comments

Comments
 (0)