Skip to content

Commit fa10f69

Browse files
committed
Initial implementation
1 parent d188e19 commit fa10f69

21 files changed

Lines changed: 766 additions & 0 deletions

apps/example/src/App.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import { Reanimated } from "./Reanimated";
3636
import { AsyncStarvation } from "./Diagnostics/AsyncStarvation";
3737
import { DeviceLostHang } from "./Diagnostics/DeviceLostHang";
3838
import { StorageBufferVertices } from "./StorageBufferVertices";
39+
import { SharedTextureMemory } from "./SharedTextureMemory";
3940

4041
// The two lines below are needed by three.js
4142
import "fast-text-encoding";
@@ -97,6 +98,10 @@ function App() {
9798
name="StorageBufferVertices"
9899
component={StorageBufferVertices}
99100
/>
101+
<Stack.Screen
102+
name="SharedTextureMemory"
103+
component={SharedTextureMemory}
104+
/>
100105
</Stack.Navigator>
101106
</NavigationContainer>
102107
</GestureHandlerRootView>

apps/example/src/Home.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,10 @@ export const examples = [
127127
screen: "StorageBufferVertices",
128128
title: "💾 Storage Buffer Vertices",
129129
},
130+
{
131+
screen: "SharedTextureMemory",
132+
title: "🎞️ Shared Texture Memory",
133+
},
130134
];
131135

132136
const styles = StyleSheet.create({

apps/example/src/Route.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,5 @@ export type Routes = {
2929
AsyncStarvation: undefined;
3030
DeviceLostHang: undefined;
3131
StorageBufferVertices: undefined;
32+
SharedTextureMemory: undefined;
3233
};
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
import React, { useEffect, useRef, useState } from "react";
2+
import { PixelRatio, Platform, StyleSheet, Text, View } from "react-native";
3+
import {
4+
Canvas,
5+
useCanvasRef,
6+
useDevice,
7+
type NativeCanvas,
8+
} from "react-native-wgpu";
9+
10+
const SHADER = /* wgsl */ `
11+
struct VsOut {
12+
@builtin(position) position: vec4f,
13+
@location(0) uv: vec2f,
14+
};
15+
16+
@vertex
17+
fn vs_main(@builtin(vertex_index) vid: u32) -> VsOut {
18+
// Full-screen triangle.
19+
var positions = array<vec2f, 3>(
20+
vec2f(-1.0, -3.0),
21+
vec2f(-1.0, 1.0),
22+
vec2f( 3.0, 1.0),
23+
);
24+
var uvs = array<vec2f, 3>(
25+
vec2f(0.0, 2.0),
26+
vec2f(0.0, 0.0),
27+
vec2f(2.0, 0.0),
28+
);
29+
var out: VsOut;
30+
out.position = vec4f(positions[vid], 0.0, 1.0);
31+
out.uv = uvs[vid];
32+
return out;
33+
}
34+
35+
@group(0) @binding(0) var srcTex: texture_2d<f32>;
36+
@group(0) @binding(1) var srcSampler: sampler;
37+
38+
@fragment
39+
fn fs_main(in: VsOut) -> @location(0) vec4f {
40+
return textureSample(srcTex, srcSampler, in.uv);
41+
}
42+
`;
43+
44+
const REQUIRED_FEATURE =
45+
Platform.OS === "ios"
46+
? "shared-texture-memory-iosurface"
47+
: "shared-texture-memory-ahardware-buffer";
48+
49+
export const SharedTextureMemory = () => {
50+
const ref = useCanvasRef();
51+
const [error, setError] = useState<string | null>(null);
52+
const rafRef = useRef<number | null>(null);
53+
54+
// Request the shared-memory feature when constructing the device so the
55+
// shared-texture-memory* extension is enabled.
56+
const { device, adapter } = useDevice(undefined, {
57+
// 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],
60+
});
61+
62+
useEffect(() => {
63+
if (!device) {
64+
return;
65+
}
66+
if (!device.features.has(REQUIRED_FEATURE)) {
67+
setError(
68+
`Device is missing the '${REQUIRED_FEATURE}' feature (adapter supports: ${
69+
adapter
70+
? [...adapter.features]
71+
.filter((f) => f.toString().startsWith("shared-"))
72+
.join(", ") || "none"
73+
: "n/a"
74+
})`,
75+
);
76+
return;
77+
}
78+
79+
const context = ref.current?.getContext("webgpu");
80+
if (!context) {
81+
return;
82+
}
83+
const canvas = context.canvas as unknown as NativeCanvas;
84+
canvas.width = canvas.clientWidth * PixelRatio.get();
85+
canvas.height = canvas.clientHeight * PixelRatio.get();
86+
const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
87+
context.configure({
88+
device,
89+
format: presentationFormat,
90+
alphaMode: "premultiplied",
91+
});
92+
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+
}
125+
126+
const module = device.createShaderModule({ code: SHADER });
127+
const pipeline = device.createRenderPipeline({
128+
layout: "auto",
129+
vertex: { module, entryPoint: "vs_main" },
130+
fragment: {
131+
module,
132+
entryPoint: "fs_main",
133+
targets: [{ format: presentationFormat }],
134+
},
135+
primitive: { topology: "triangle-list" },
136+
});
137+
const sampler = device.createSampler({
138+
magFilter: "linear",
139+
minFilter: "linear",
140+
});
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+
});
148+
149+
const render = () => {
150+
const encoder = device.createCommandEncoder();
151+
const pass = encoder.beginRenderPass({
152+
colorAttachments: [
153+
{
154+
view: context.getCurrentTexture().createView(),
155+
clearValue: { r: 0, g: 0, b: 0, a: 1 },
156+
loadOp: "clear",
157+
storeOp: "store",
158+
},
159+
],
160+
});
161+
pass.setPipeline(pipeline);
162+
pass.setBindGroup(0, bindGroup);
163+
pass.draw(3);
164+
pass.end();
165+
device.queue.submit([encoder.finish()]);
166+
context.present();
167+
rafRef.current = requestAnimationFrame(render);
168+
};
169+
rafRef.current = requestAnimationFrame(render);
170+
171+
return () => {
172+
if (rafRef.current !== null) {
173+
cancelAnimationFrame(rafRef.current);
174+
}
175+
sharedMemory.endAccess(texture);
176+
texture.destroy();
177+
frame.release();
178+
};
179+
}, [device, adapter, ref]);
180+
181+
if (error) {
182+
return (
183+
<View style={styles.errorContainer}>
184+
<Text style={styles.errorText}>{error}</Text>
185+
</View>
186+
);
187+
}
188+
return (
189+
<View style={{ flex: 1 }}>
190+
<Canvas ref={ref} style={{ flex: 1 }} />
191+
</View>
192+
);
193+
};
194+
195+
const styles = StyleSheet.create({
196+
errorContainer: { flex: 1, padding: 16, justifyContent: "center" },
197+
errorText: { color: "red", fontSize: 14 },
198+
});
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from "./SharedTextureMemory";

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/GPUSharedTextureMemory.cpp
4041
../cpp/rnwgpu/api/GPURenderBundleEncoder.cpp
4142
../cpp/rnwgpu/api/GPURenderPassEncoder.cpp
4243
../cpp/rnwgpu/api/GPURenderPipeline.cpp

packages/webgpu/android/cpp/AndroidPlatformContext.h

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,23 @@ class AndroidPlatformContext : public PlatformContext {
202202
}
203203
}).detach();
204204
}
205+
206+
VideoFrameHandle loadVideoFrame(const std::string & /*path*/) override {
207+
// TODO: implement using MediaExtractor + MediaCodec to decode the first
208+
// frame into an AHardwareBuffer-backed Image (Android API 26+).
209+
throw std::runtime_error(
210+
"loadVideoFrame is not yet implemented on Android. Pass an "
211+
"AHardwareBuffer pointer obtained elsewhere (e.g. from "
212+
"react-native-vision-camera) directly to "
213+
"device.importSharedTextureMemory.");
214+
}
215+
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.");
221+
}
205222
};
206223

207224
} // namespace rnwgpu

packages/webgpu/apple/ApplePlatformContext.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ class ApplePlatformContext : public PlatformContext {
2626
void createImageBitmapFromDataAsync(
2727
std::span<const uint8_t> data, std::function<void(ImageData)> onSuccess,
2828
std::function<void(std::string)> onError) override;
29+
30+
VideoFrameHandle loadVideoFrame(const std::string &path) override;
31+
32+
VideoFrameHandle createTestVideoFrame(uint32_t width,
33+
uint32_t height) override;
2934
};
3035

3136
} // namespace rnwgpu

0 commit comments

Comments
 (0)