Skip to content

Commit dc29a57

Browse files
committed
🔧
1 parent c547ae8 commit dc29a57

11 files changed

Lines changed: 394 additions & 35 deletions

File tree

apps/example/android/gradle.properties

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64
3636
newArchEnabled=true
3737
hermesEnabled=true
3838

39+
# react-native-wgpu requires API 26+ (AHardwareBuffer for shared texture memory,
40+
# and Dawn's Android backend itself).
41+
react.minSdkVersion=26
42+
3943
# Use this property to enable support to the new architecture.
4044
# This will allow you to use TurboModules and the Fabric render in
4145
# your application. You should enable this flag either if you want

apps/example/ios/Podfile.lock

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1865,7 +1865,7 @@ PODS:
18651865
- ReactCommon/turbomodule/core
18661866
- SocketRocket
18671867
- Yoga
1868-
- react-native-wgpu (0.5.11):
1868+
- react-native-wgpu (0.5.12):
18691869
- boost
18701870
- DoubleConversion
18711871
- fast_float
@@ -2460,7 +2460,7 @@ PODS:
24602460
- ReactCommon/turbomodule/core
24612461
- SocketRocket
24622462
- Yoga
2463-
- RNReanimated (4.2.1):
2463+
- RNReanimated (4.3.1):
24642464
- boost
24652465
- DoubleConversion
24662466
- fast_float
@@ -2487,11 +2487,12 @@ PODS:
24872487
- ReactCodegen
24882488
- ReactCommon/turbomodule/bridging
24892489
- ReactCommon/turbomodule/core
2490-
- RNReanimated/reanimated (= 4.2.1)
2490+
- RNReanimated/apple (= 4.3.1)
2491+
- RNReanimated/common (= 4.3.1)
24912492
- RNWorklets
24922493
- SocketRocket
24932494
- Yoga
2494-
- RNReanimated/reanimated (4.2.1):
2495+
- RNReanimated/apple (4.3.1):
24952496
- boost
24962497
- DoubleConversion
24972498
- fast_float
@@ -2518,11 +2519,10 @@ PODS:
25182519
- ReactCodegen
25192520
- ReactCommon/turbomodule/bridging
25202521
- ReactCommon/turbomodule/core
2521-
- RNReanimated/reanimated/apple (= 4.2.1)
25222522
- RNWorklets
25232523
- SocketRocket
25242524
- Yoga
2525-
- RNReanimated/reanimated/apple (4.2.1):
2525+
- RNReanimated/common (4.3.1):
25262526
- boost
25272527
- DoubleConversion
25282528
- fast_float
@@ -2552,7 +2552,7 @@ PODS:
25522552
- RNWorklets
25532553
- SocketRocket
25542554
- Yoga
2555-
- RNWorklets (0.7.2):
2555+
- RNWorklets (0.8.3):
25562556
- boost
25572557
- DoubleConversion
25582558
- fast_float
@@ -2579,10 +2579,11 @@ PODS:
25792579
- ReactCodegen
25802580
- ReactCommon/turbomodule/bridging
25812581
- ReactCommon/turbomodule/core
2582-
- RNWorklets/worklets (= 0.7.2)
2582+
- RNWorklets/apple (= 0.8.3)
2583+
- RNWorklets/common (= 0.8.3)
25832584
- SocketRocket
25842585
- Yoga
2585-
- RNWorklets/worklets (0.7.2):
2586+
- RNWorklets/apple (0.8.3):
25862587
- boost
25872588
- DoubleConversion
25882589
- fast_float
@@ -2609,10 +2610,9 @@ PODS:
26092610
- ReactCodegen
26102611
- ReactCommon/turbomodule/bridging
26112612
- ReactCommon/turbomodule/core
2612-
- RNWorklets/worklets/apple (= 0.7.2)
26132613
- SocketRocket
26142614
- Yoga
2615-
- RNWorklets/worklets/apple (0.7.2):
2615+
- RNWorklets/common (0.8.3):
26162616
- boost
26172617
- DoubleConversion
26182618
- fast_float
@@ -2938,7 +2938,7 @@ SPEC CHECKSUMS:
29382938
React-microtasksnativemodule: 75b6604b667d297292345302cc5bfb6b6aeccc1b
29392939
react-native-safe-area-context: c00143b4823773bba23f2f19f85663ae89ceb460
29402940
react-native-skia: fc73e9bdc46ebb420a98c9c2be29fee80f565e79
2941-
react-native-wgpu: 990ed34b0216cb4980a52344ec959cb8899d2845
2941+
react-native-wgpu: 274ffec11ee3a082260d9f3d1fb54030a5ca0873
29422942
React-NativeModulesApple: 879fbdc5dcff7136abceb7880fe8a2022a1bd7c3
29432943
React-oscompat: 93b5535ea7f7dff46aaee4f78309a70979bdde9d
29442944
React-perflogger: 5536d2df3d18fe0920263466f7b46a56351c0510
@@ -2973,8 +2973,8 @@ SPEC CHECKSUMS:
29732973
ReactTestApp-DevSupport: 9b7bbba5e8fed998e763809171d9906a1375f9d3
29742974
ReactTestApp-Resources: 1bd9ff10e4c24f2ad87101a32023721ae923bccf
29752975
RNGestureHandler: e37bdb684df1ac17c7e1d8f71a3311b2793c186b
2976-
RNReanimated: 464375ff2caa801358547c44eca894ff0bf68e74
2977-
RNWorklets: ee58e869ea579800ec5f2f1cb6ae195fd3537546
2976+
RNReanimated: 9d012d4031abc9df896f8a82f9928eb2b9eae417
2977+
RNWorklets: 0da2552f9ff5d17506918a692304110cfebb9f0a
29782978
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
29792979
Yoga: a3ed390a19db0459bd6839823a6ac6d9c6db198d
29802980

apps/example/src/SharedTextureMemory/SharedTextureMemory.tsx

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -109,14 +109,45 @@ export const SharedTextureMemory = () => {
109109
alphaMode: "premultiplied",
110110
});
111111

112-
// 1. Open the video and start playback. AVPlayer accepts local file paths
113-
// as well as http(s):// URLs and keeps the IOSurface pool up to date
114-
// in the background. For a fully offline demo, swap this URL for
115-
// RNWebGPU.writeTestVideoFile() which generates a tiny mp4 on disk.
116-
const VIDEO_URL =
117-
"https://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/1080/Big_Buck_Bunny_1080_10s_5MB.mp4";
118-
const player = RNWebGPU.createVideoPlayer(VIDEO_URL);
119-
player.play();
112+
// Pick a frame source per platform. On iOS we use AVPlayer to stream a
113+
// real video; on Android we don't have a video pipeline yet, so we fall
114+
// back to a single synthetic IOSurface/AHardwareBuffer frame produced by
115+
// RNWebGPU.createTestVideoFrame. The rAF loop below treats a null return
116+
// from copyLatestFrame() as "keep showing the previous frame", which means
117+
// a one-shot source renders correctly without any other change.
118+
interface FrameSource {
119+
copyLatestFrame(): VideoFrame | null;
120+
release(): void;
121+
}
122+
let source: FrameSource;
123+
if (Platform.OS === "ios") {
124+
const VIDEO_URL =
125+
"https://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/1080/Big_Buck_Bunny_1080_10s_5MB.mp4";
126+
const player = RNWebGPU.createVideoPlayer(VIDEO_URL);
127+
player.play();
128+
source = {
129+
copyLatestFrame: () => source.copyLatestFrame(),
130+
release: () => player.release(),
131+
};
132+
} else {
133+
let pending: VideoFrame | null = RNWebGPU.createTestVideoFrame(
134+
1024,
135+
1024,
136+
);
137+
source = {
138+
copyLatestFrame: () => {
139+
const f = pending;
140+
pending = null;
141+
return f;
142+
},
143+
release: () => {
144+
if (pending) {
145+
pending.release();
146+
pending = null;
147+
}
148+
},
149+
};
150+
}
120151

121152
const module = device.createShaderModule({ code: SHADER });
122153
const pipeline = device.createRenderPipeline({
@@ -216,7 +247,7 @@ export const SharedTextureMemory = () => {
216247
const render = () => {
217248
// Pull the latest frame from the player. Null means "no new frame since
218249
// we last asked", in which case we keep using the existing one.
219-
const newFrame = player.copyLatestFrame();
250+
const newFrame = source.copyLatestFrame();
220251
if (newFrame) {
221252
const next = bindFrame(newFrame);
222253
if (next) {
@@ -258,7 +289,7 @@ export const SharedTextureMemory = () => {
258289
releaseBound(current);
259290
current = null;
260291
}
261-
player.release();
292+
source.release();
262293
};
263294
}, [device, adapter, ref]);
264295

apps/example/src/Tests.tsx

Lines changed: 80 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React, { useEffect, useState } from "react";
2-
import { Dimensions, Text, View, Image } from "react-native";
3-
import { GPUOffscreenCanvas, useDevice } from "react-native-wgpu";
2+
import { Dimensions, Platform, Text, View, Image } from "react-native";
3+
import { GPUOffscreenCanvas } from "react-native-wgpu";
44
import { mat4, vec3, mat3 } from "wgpu-matrix";
55

66
import { useClient } from "./useClient";
@@ -14,9 +14,72 @@ export const CI = process.env.CI === "true";
1414
const { width } = Dimensions.get("window");
1515
const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
1616

17+
// Platform-specific Dawn features that back importSharedTextureMemory. The
18+
// fence feature is paired with the memory feature because Dawn validates it
19+
// is enabled before letting BeginAccess/EndAccess succeed on the matching
20+
// backend, even when callers pass zero fences.
21+
const sharedTextureMemoryFeatures = (): GPUFeatureName[] => {
22+
if (Platform.OS === "ios") {
23+
return [
24+
"shared-texture-memory-iosurface",
25+
"shared-fence-mtl-shared-event",
26+
] as unknown as GPUFeatureName[];
27+
}
28+
if (Platform.OS === "android") {
29+
return [
30+
"shared-texture-memory-ahardware-buffer",
31+
"shared-fence-vk-semaphore-sync-fd",
32+
] as unknown as GPUFeatureName[];
33+
}
34+
return [];
35+
};
36+
const OPTIONAL_SHARED_TEXTURE_MEMORY_FEATURES = sharedTextureMemoryFeatures();
37+
1738
export const Tests = ({ assets: { di3D, saturn, moon } }: AssetProps) => {
1839
const [texture, setTexture] = useState<GPUTexture | null>(null);
19-
const { adapter, device } = useDevice();
40+
const [adapter, setAdapter] = useState<GPUAdapter | null>(null);
41+
const [device, setDevice] = useState<GPUDevice | null>(null);
42+
const [setupError, setSetupError] = useState<string | null>(null);
43+
useEffect(() => {
44+
let cancelled = false;
45+
(async () => {
46+
try {
47+
const a = await navigator.gpu.requestAdapter();
48+
if (!a) {
49+
throw new Error("No appropriate GPUAdapter found.");
50+
}
51+
const requiredFeatures = OPTIONAL_SHARED_TEXTURE_MEMORY_FEATURES.filter(
52+
(f) => a.features.has(f),
53+
);
54+
console.log(
55+
`[Tests] adapter features (shared-*): ${[...a.features]
56+
.filter((f) => f.toString().startsWith("shared-"))
57+
.join(", ")}`,
58+
);
59+
console.log(
60+
`[Tests] requestDevice with requiredFeatures: ${JSON.stringify(requiredFeatures)}`,
61+
);
62+
const d = await a.requestDevice({ requiredFeatures });
63+
if (!d) {
64+
throw new Error("No appropriate GPUDevice found.");
65+
}
66+
if (cancelled) {
67+
return;
68+
}
69+
setAdapter(a);
70+
setDevice(d);
71+
} catch (e) {
72+
const msg = e instanceof Error ? e.message : String(e);
73+
console.warn(`[Tests] device setup failed: ${msg}`);
74+
if (!cancelled) {
75+
setSetupError(msg);
76+
}
77+
}
78+
})();
79+
return () => {
80+
cancelled = true;
81+
};
82+
}, []);
2083
const [client, hostname] = useClient();
2184
useEffect(() => {
2285
if (client !== null && adapter !== null && device !== null) {
@@ -78,8 +141,21 @@ export const Tests = ({ assets: { di3D, saturn, moon } }: AssetProps) => {
78141
}
79142
return;
80143
}, [adapter, client, device, di3D, moon, saturn]);
144+
if (setupError) {
145+
return (
146+
<View style={{ flex: 1, padding: 16, backgroundColor: "white" }}>
147+
<Text style={{ color: "red" }}>
148+
🔴 Test device setup failed: {setupError}
149+
</Text>
150+
</View>
151+
);
152+
}
81153
if (!device) {
82-
return null;
154+
return (
155+
<View style={{ flex: 1, padding: 16, backgroundColor: "white" }}>
156+
<Text style={{ color: "black" }}>⏳ Initializing GPU device…</Text>
157+
</View>
158+
);
83159
}
84160
return (
85161
<View style={{ flex: 1, backgroundColor: "white" }}>

packages/webgpu/android/cpp/AndroidPlatformContext.h

Lines changed: 64 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
#pragma once
22

33
#include <android/bitmap.h>
4+
#include <android/hardware_buffer.h>
45
#include <jni.h>
56

7+
#include <algorithm>
68
#include <functional>
79
#include <memory>
810
#include <string>
@@ -213,11 +215,68 @@ class AndroidPlatformContext : public PlatformContext {
213215
"device.importSharedTextureMemory.");
214216
}
215217

216-
VideoFrameHandle createTestVideoFrame(uint32_t /*width*/,
217-
uint32_t /*height*/) override {
218-
// TODO: implement using AHardwareBuffer_allocate (Android API 26+).
219-
throw std::runtime_error(
220-
"createTestVideoFrame is not yet implemented on Android.");
218+
VideoFrameHandle createTestVideoFrame(uint32_t width,
219+
uint32_t height) override {
220+
// Dawn's Android backend already requires API 26+, so AHardwareBuffer_*
221+
// symbols are guaranteed to be available at link time on supported builds.
222+
AHardwareBuffer_Desc desc = {};
223+
desc.width = width;
224+
desc.height = height;
225+
desc.layers = 1;
226+
desc.format = AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM;
227+
desc.usage = AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE |
228+
AHARDWAREBUFFER_USAGE_CPU_WRITE_RARELY |
229+
AHARDWAREBUFFER_USAGE_CPU_READ_RARELY;
230+
231+
AHardwareBuffer *buffer = nullptr;
232+
int err = AHardwareBuffer_allocate(&desc, &buffer);
233+
if (err != 0 || !buffer) {
234+
throw std::runtime_error(
235+
"createTestVideoFrame: AHardwareBuffer_allocate failed (" +
236+
std::to_string(err) + ")");
237+
}
238+
239+
AHardwareBuffer_Desc actualDesc = {};
240+
AHardwareBuffer_describe(buffer, &actualDesc);
241+
const uint32_t stridePixels = actualDesc.stride;
242+
243+
void *vaddr = nullptr;
244+
int rc = AHardwareBuffer_lock(buffer, AHARDWAREBUFFER_USAGE_CPU_WRITE_RARELY,
245+
-1, nullptr, &vaddr);
246+
if (rc != 0 || !vaddr) {
247+
AHardwareBuffer_release(buffer);
248+
throw std::runtime_error(
249+
"createTestVideoFrame: AHardwareBuffer_lock failed (" +
250+
std::to_string(rc) + ")");
251+
}
252+
253+
// Same procedural pattern as ApplePlatformContext::createTestVideoFrame so
254+
// the shared snapshot matches on both platforms. Apple writes BGRA bytes
255+
// (CVPixelBuffer is kCVPixelFormatType_32BGRA, sampled as bgra8unorm);
256+
// here we write RGBA bytes (AHardwareBuffer is R8G8B8A8_UNORM, sampled as
257+
// rgba8unorm). The fragment shader sees the same logical (r, g, b, a).
258+
uint8_t *base = static_cast<uint8_t *>(vaddr);
259+
const size_t rowBytes = stridePixels * 4;
260+
for (uint32_t y = 0; y < height; ++y) {
261+
uint8_t *row = base + y * rowBytes;
262+
for (uint32_t x = 0; x < width; ++x) {
263+
uint8_t r = static_cast<uint8_t>((x * 255) / std::max(width - 1, 1u));
264+
uint8_t g = static_cast<uint8_t>((y * 255) / std::max(height - 1, 1u));
265+
uint8_t b = static_cast<uint8_t>(((x + y) & 0x20) ? 220 : 30);
266+
row[x * 4 + 0] = r;
267+
row[x * 4 + 1] = g;
268+
row[x * 4 + 2] = b;
269+
row[x * 4 + 3] = 0xFF;
270+
}
271+
}
272+
AHardwareBuffer_unlock(buffer, nullptr);
273+
274+
VideoFrameHandle handle;
275+
handle.handle = static_cast<void *>(buffer);
276+
handle.width = width;
277+
handle.height = height;
278+
handle.deleter = [buffer]() { AHardwareBuffer_release(buffer); };
279+
return handle;
221280
}
222281

223282
std::unique_ptr<IVideoPlayer>
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
Wgpu_kotlinVersion=1.7.0
2-
Wgpu_minSdkVersion=21
2+
Wgpu_minSdkVersion=26
33
Wgpu_targetSdkVersion=31
44
Wgpu_compileSdkVersion=31
55
Wgpu_ndkversion=21.4.7075529

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,13 @@ std::shared_ptr<GPUSharedTextureMemory> GPUDevice::importSharedTextureMemory(
254254
#if defined(__APPLE__)
255255
wgpu::SharedTextureMemoryIOSurfaceDescriptor platformDesc{};
256256
platformDesc.ioSurface = descriptor->handle;
257-
platformDesc.allowStorageBinding = true;
257+
// Default off: enabling it propagates StorageBinding into properties.usage,
258+
// which then forces memory.createTexture() (no-descriptor form) to validate
259+
// the format against storage capabilities. bgra8unorm (the standard
260+
// CVPixelBuffer format) only supports storage when the device opts into the
261+
// bgra8unorm-storage feature, so unconditionally setting this here breaks
262+
// the common sample-only case.
263+
platformDesc.allowStorageBinding = false;
258264
desc.nextInChain = &platformDesc;
259265
#elif defined(__ANDROID__)
260266
wgpu::SharedTextureMemoryAHardwareBufferDescriptor platformDesc{};

0 commit comments

Comments
 (0)