Skip to content

Commit cf63d0a

Browse files
committed
🔧
1 parent fa10f69 commit cf63d0a

13 files changed

Lines changed: 511 additions & 55 deletions

File tree

apps/example/src/SharedTextureMemory/SharedTextureMemory.tsx

Lines changed: 106 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import {
44
Canvas,
55
useCanvasRef,
66
useDevice,
7+
type GPUSharedTextureMemory,
78
type NativeCanvas,
9+
type VideoFrame,
810
} from "react-native-wgpu";
911

1012
const SHADER = /* wgsl */ `
@@ -41,37 +43,44 @@ fn fs_main(in: VsOut) -> @location(0) vec4f {
4143
}
4244
`;
4345

44-
const REQUIRED_FEATURE =
46+
// On Metal, EndAccess on an IOSurface-backed SharedTextureMemory always
47+
// produces an MTLSharedEvent fence (so the producer can wait on the GPU). Even
48+
// though we don't currently expose the fence to JS, Dawn validates that the
49+
// fence feature is enabled before letting EndAccess succeed. Android has the
50+
// equivalent pairing with sync fds.
51+
const REQUIRED_FEATURES =
4552
Platform.OS === "ios"
46-
? "shared-texture-memory-iosurface"
47-
: "shared-texture-memory-ahardware-buffer";
53+
? ["shared-texture-memory-iosurface", "shared-fence-mtl-shared-event"]
54+
: [
55+
"shared-texture-memory-ahardware-buffer",
56+
"shared-fence-vk-semaphore-sync-fd",
57+
];
4858

4959
export const SharedTextureMemory = () => {
5060
const ref = useCanvasRef();
5161
const [error, setError] = useState<string | null>(null);
5262
const rafRef = useRef<number | null>(null);
5363

54-
// Request the shared-memory feature when constructing the device so the
55-
// shared-texture-memory* extension is enabled.
5664
const { device, adapter } = useDevice(undefined, {
5765
// Cast: GPUFeatureName in @webgpu/types doesn't include the Dawn-specific
58-
// extension name yet, but Dawn accepts it.
59-
requiredFeatures: [REQUIRED_FEATURE as GPUFeatureName],
66+
// extension names yet, but Dawn accepts them.
67+
requiredFeatures: REQUIRED_FEATURES as unknown as GPUFeatureName[],
6068
});
6169

6270
useEffect(() => {
6371
if (!device) {
6472
return;
6573
}
66-
if (!device.features.has(REQUIRED_FEATURE)) {
74+
const missing = REQUIRED_FEATURES.filter((f) => !device.features.has(f));
75+
if (missing.length > 0) {
6776
setError(
68-
`Device is missing the '${REQUIRED_FEATURE}' feature (adapter supports: ${
77+
`Device is missing required features [${missing.join(", ")}]. Adapter supports: ${
6978
adapter
7079
? [...adapter.features]
7180
.filter((f) => f.toString().startsWith("shared-"))
7281
.join(", ") || "none"
7382
: "n/a"
74-
})`,
83+
}`,
7584
);
7685
return;
7786
}
@@ -90,38 +99,14 @@ export const SharedTextureMemory = () => {
9099
alphaMode: "premultiplied",
91100
});
92101

93-
// 1. Acquire a native, GPU-shareable surface. In production this would
94-
// come from a camera frame processor or video decoder. The test helper
95-
// synthesizes a 256x256 RGB-gradient pattern in an IOSurface.
96-
const frame = RNWebGPU.createTestVideoFrame(256, 256);
97-
98-
// 2. Import the raw native handle into a SharedTextureMemory.
99-
const sharedMemory = device.importSharedTextureMemory({
100-
handle: frame.handle,
101-
label: "video-frame-shared-memory",
102-
});
103-
104-
// 3. Create a regular GPUTexture that aliases the surface's pixels.
105-
// No descriptor needed: the format/size are inferred from the surface.
106-
const texture = sharedMemory.createTexture();
107-
108-
// 4. beginAccess declares that we're about to read or write the texture on
109-
// the GPU timeline. `initialized: true` means "the surface already has
110-
// meaningful pixels", which is correct for an incoming video frame.
111-
//
112-
// Because this example owns a *static* IOSurface (no external producer
113-
// is writing new pixels between frames), we keep one access window open
114-
// for the lifetime of the texture and call endAccess only on unmount.
115-
//
116-
// For a live camera or video feed, you'd instead wrap each frame:
117-
// beginAccess(tex, true) -> submit -> endAccess(tex)
118-
// around every render to hand ownership back to the producer. That's
119-
// also where fence support (not yet wired through this binding) becomes
120-
// important to avoid races with the producer.
121-
if (!sharedMemory.beginAccess(texture, true)) {
122-
setError("beginAccess() failed");
123-
return;
124-
}
102+
// 1. Open the video and start playback. AVPlayer accepts local file paths
103+
// as well as http(s):// URLs and keeps the IOSurface pool up to date
104+
// in the background. For a fully offline demo, swap this URL for
105+
// RNWebGPU.writeTestVideoFile() which generates a tiny mp4 on disk.
106+
const VIDEO_URL =
107+
"https://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/1080/Big_Buck_Bunny_1080_10s_5MB.mp4";
108+
const player = RNWebGPU.createVideoPlayer(VIDEO_URL);
109+
player.play();
125110

126111
const module = device.createShaderModule({ code: SHADER });
127112
const pipeline = device.createRenderPipeline({
@@ -138,15 +123,77 @@ export const SharedTextureMemory = () => {
138123
magFilter: "linear",
139124
minFilter: "linear",
140125
});
141-
const bindGroup = device.createBindGroup({
142-
layout: pipeline.getBindGroupLayout(0),
143-
entries: [
144-
{ binding: 0, resource: texture.createView() },
145-
{ binding: 1, resource: sampler },
146-
],
147-
});
126+
127+
// We hold the *current* frame across rAF ticks so that when the video
128+
// hasn't produced a new frame yet (between decoder timestamps), we keep
129+
// rendering the last one rather than dropping to a black screen.
130+
//
131+
// For each new IOSurface we:
132+
// - create a SharedTextureMemory + texture + bindGroup
133+
// - beginAccess(initialized: true) to declare "the producer has written
134+
// these pixels and we're now sampling them"
135+
// - sample in the shader
136+
// - endAccess to hand ownership back to the producer
137+
//
138+
// We close out the previous frame's access window first. In a fence-aware
139+
// build we'd plumb an AVPlayer fence through beginAccess/endAccess; for the
140+
// demo we rely on AVPlayer recycling its IOSurface pool, which is safe as
141+
// long as we end-access before letting the player reclaim the buffer.
142+
type Bound = {
143+
frame: VideoFrame;
144+
memory: GPUSharedTextureMemory;
145+
texture: GPUTexture;
146+
bindGroup: GPUBindGroup;
147+
};
148+
let current: Bound | null = null;
149+
150+
const bindFrame = (frame: VideoFrame): Bound | null => {
151+
try {
152+
const memory = device.importSharedTextureMemory({
153+
handle: frame.handle,
154+
label: "video-frame",
155+
});
156+
const texture = memory.createTexture();
157+
if (!memory.beginAccess(texture, true)) {
158+
texture.destroy();
159+
frame.release();
160+
return null;
161+
}
162+
const bindGroup = device.createBindGroup({
163+
layout: pipeline.getBindGroupLayout(0),
164+
entries: [
165+
{ binding: 0, resource: texture.createView() },
166+
{ binding: 1, resource: sampler },
167+
],
168+
});
169+
return { frame, memory, texture, bindGroup };
170+
} catch (e) {
171+
console.warn("[SharedTextureMemory] bindFrame failed:", e);
172+
frame.release();
173+
return null;
174+
}
175+
};
176+
177+
const releaseBound = (b: Bound) => {
178+
b.memory.endAccess(b.texture);
179+
b.texture.destroy();
180+
b.frame.release();
181+
};
148182

149183
const render = () => {
184+
// Pull the latest frame from the player. Null means "no new frame since
185+
// we last asked", in which case we keep using the existing one.
186+
const newFrame = player.copyLatestFrame();
187+
if (newFrame) {
188+
const next = bindFrame(newFrame);
189+
if (next) {
190+
if (current) {
191+
releaseBound(current);
192+
}
193+
current = next;
194+
}
195+
}
196+
150197
const encoder = device.createCommandEncoder();
151198
const pass = encoder.beginRenderPass({
152199
colorAttachments: [
@@ -158,9 +205,11 @@ export const SharedTextureMemory = () => {
158205
},
159206
],
160207
});
161-
pass.setPipeline(pipeline);
162-
pass.setBindGroup(0, bindGroup);
163-
pass.draw(3);
208+
if (current) {
209+
pass.setPipeline(pipeline);
210+
pass.setBindGroup(0, current.bindGroup);
211+
pass.draw(3);
212+
}
164213
pass.end();
165214
device.queue.submit([encoder.finish()]);
166215
context.present();
@@ -172,9 +221,11 @@ export const SharedTextureMemory = () => {
172221
if (rafRef.current !== null) {
173222
cancelAnimationFrame(rafRef.current);
174223
}
175-
sharedMemory.endAccess(texture);
176-
texture.destroy();
177-
frame.release();
224+
if (current) {
225+
releaseBound(current);
226+
current = null;
227+
}
228+
player.release();
178229
};
179230
}, [device, adapter, ref]);
180231

packages/webgpu/android/cpp/AndroidPlatformContext.h

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,19 @@ class AndroidPlatformContext : public PlatformContext {
219219
throw std::runtime_error(
220220
"createTestVideoFrame is not yet implemented on Android.");
221221
}
222+
223+
std::unique_ptr<IVideoPlayer>
224+
createVideoPlayer(const std::string & /*path*/) override {
225+
// TODO: implement using MediaCodec -> ImageReader (AHardwareBuffer mode).
226+
throw std::runtime_error(
227+
"createVideoPlayer is not yet implemented on Android.");
228+
}
229+
230+
std::string writeTestVideoFile() override {
231+
// TODO: implement using MediaCodec (H.264 encoder) or MediaMuxer.
232+
throw std::runtime_error(
233+
"writeTestVideoFile is not yet implemented on Android.");
234+
}
222235
};
223236

224237
} // namespace rnwgpu

packages/webgpu/apple/ApplePlatformContext.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ class ApplePlatformContext : public PlatformContext {
3131

3232
VideoFrameHandle createTestVideoFrame(uint32_t width,
3333
uint32_t height) override;
34+
35+
std::unique_ptr<IVideoPlayer>
36+
createVideoPlayer(const std::string &path) override;
37+
38+
std::string writeTestVideoFile() override;
3439
};
3540

3641
} // namespace rnwgpu

packages/webgpu/apple/ApplePlatformContext.mm

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
#import <React/RCTBridge+Private.h>
99
#import <ReactCommon/RCTTurboModule.h>
1010

11+
#include "AppleVideoPlayer.h"
12+
1113
#include "RNWebGPUManager.h"
1214
#include "WebGPUModule.h"
1315

@@ -234,6 +236,15 @@ void checkIfUsingSimulatorWithAPIValidation() {
234236
return handle;
235237
}
236238

239+
std::unique_ptr<IVideoPlayer>
240+
ApplePlatformContext::createVideoPlayer(const std::string &path) {
241+
return createAppleVideoPlayer(path);
242+
}
243+
244+
std::string ApplePlatformContext::writeTestVideoFile() {
245+
return writeAppleTestVideoFile();
246+
}
247+
237248
VideoFrameHandle
238249
ApplePlatformContext::createTestVideoFrame(uint32_t width, uint32_t height) {
239250
NSDictionary *attrs = @{
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
#pragma once
2+
3+
#include "PlatformContext.h"
4+
5+
#include <memory>
6+
#include <string>
7+
8+
namespace rnwgpu {
9+
10+
// Factory: creates a new IVideoPlayer backed by AVPlayer +
11+
// AVPlayerItemVideoOutput.
12+
std::unique_ptr<IVideoPlayer>
13+
createAppleVideoPlayer(const std::string &path);
14+
15+
// Generate a small procedurally-animated test video and write it to a
16+
// temporary file. Returns the absolute path. Used by the SharedTextureMemory
17+
// example so it doesn't need a bundled .mp4.
18+
std::string writeAppleTestVideoFile();
19+
20+
} // namespace rnwgpu

0 commit comments

Comments
 (0)