Skip to content

Commit 026c2f2

Browse files
committed
🔧
1 parent bb74056 commit 026c2f2

3 files changed

Lines changed: 118 additions & 14 deletions

File tree

apps/example/src/VisionCamera/VisionCamera.tsx

Lines changed: 101 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React, { useEffect } from "react";
22
import {
33
Linking,
44
PixelRatio,
5+
Platform,
56
StyleSheet,
67
Text,
78
TouchableOpacity,
@@ -10,7 +11,6 @@ import {
1011
import {
1112
Canvas,
1213
useCanvasRef,
13-
useDevice,
1414
type NativeCanvas,
1515
type RNCanvasContext,
1616
} from "react-native-wgpu";
@@ -84,6 +84,15 @@ const REQUIRED_FEATURES: GPUFeatureName[] = [
8484
"dawn-multi-planar-formats" as GPUFeatureName,
8585
];
8686

87+
// Android-only feature, gates Dawn's "wrap a YCbCr AHB as a GPUExternalTexture
88+
// with implicit SamplerYcbcrConversion" path. Without it our native
89+
// `importExternalTexture` flow on Android can't produce a usable external
90+
// texture from a camera frame. We probe the adapter for it and surface a
91+
// clear error if the device's Vulkan driver doesn't advertise it (e.g. some
92+
// Android-Desktop / Chromebook configurations).
93+
const OPAQUE_YCBCR_EXT =
94+
"opaque-ycbcr-android-for-external-texture" as GPUFeatureName;
95+
8796
const ABERRATION_STRENGTH = 0.006;
8897

8998
export const VisionCamera = () => {
@@ -114,9 +123,69 @@ export const VisionCamera = () => {
114123

115124
const CameraView = () => {
116125
const ref = useCanvasRef();
117-
const { device, adapter } = useDevice(undefined, {
118-
requiredFeatures: REQUIRED_FEATURES,
119-
});
126+
const [gpu, setGpu] = React.useState<{
127+
adapter: GPUAdapter;
128+
device: GPUDevice;
129+
} | null>(null);
130+
const [deviceError, setDeviceError] = React.useState<string | null>(null);
131+
React.useEffect(() => {
132+
let cancelled = false;
133+
(async () => {
134+
try {
135+
const adapter = await navigator.gpu.requestAdapter();
136+
if (!adapter) {
137+
throw new Error("requestAdapter returned null");
138+
}
139+
const adapterFeatures = [...adapter.features].sort();
140+
console.log(
141+
"[VisionCamera] adapter features (" +
142+
adapterFeatures.length +
143+
"): " +
144+
adapterFeatures.join(", "),
145+
);
146+
const hasOpaqueYCbCrExt =
147+
Platform.OS !== "android" || adapter.features.has(OPAQUE_YCBCR_EXT);
148+
if (Platform.OS === "android" && !hasOpaqueYCbCrExt) {
149+
throw new Error(
150+
"This Android device's Vulkan driver doesn't advertise " +
151+
"opaque-ycbcr-android-for-external-texture. Camera-frame import " +
152+
"as a GPUExternalTexture isn't supported here. (This is a " +
153+
"device/driver limitation, not a code issue.)",
154+
);
155+
}
156+
const featuresToRequest: GPUFeatureName[] = [
157+
...REQUIRED_FEATURES,
158+
...(Platform.OS === "android" ? [OPAQUE_YCBCR_EXT] : []),
159+
];
160+
console.log(
161+
"[VisionCamera] requesting device with features: " +
162+
featuresToRequest.join(", "),
163+
);
164+
const device = await adapter.requestDevice({
165+
requiredFeatures: featuresToRequest,
166+
});
167+
if (cancelled) {
168+
return;
169+
}
170+
console.log(
171+
"[VisionCamera] device created, features: " +
172+
[...device.features].sort().join(", "),
173+
);
174+
setGpu({ adapter, device });
175+
} catch (e) {
176+
if (cancelled) {
177+
return;
178+
}
179+
console.warn("[VisionCamera] device creation failed: " + String(e));
180+
setDeviceError(String(e));
181+
}
182+
})();
183+
return () => {
184+
cancelled = true;
185+
};
186+
}, []);
187+
const device = gpu?.device ?? null;
188+
const adapter = gpu?.adapter ?? null;
120189
const devices = useCameraDevices();
121190
// Pick back camera if available, otherwise front, otherwise anything. The
122191
// iOS simulator returns an empty list since there are no cameras, in which
@@ -274,10 +343,18 @@ const CameraView = () => {
274343
new Float32Array([sx, sy, ABERRATION_STRENGTH, 0]),
275344
);
276345

277-
const externalTex = device.importExternalTexture({
278-
source: videoFrame,
279-
label: "camera-frame",
280-
});
346+
let externalTex;
347+
try {
348+
externalTex = device.importExternalTexture({
349+
source: videoFrame,
350+
label: "camera-frame",
351+
});
352+
} catch (e) {
353+
console.warn(
354+
"[VisionCamera] importExternalTexture threw: " + String(e),
355+
);
356+
throw e;
357+
}
281358
const bindGroup = device.createBindGroup({
282359
layout: pipeline.getBindGroupLayout(0),
283360
entries: [
@@ -323,13 +400,29 @@ const CameraView = () => {
323400
outputs: [frameOutput],
324401
});
325402

403+
if (deviceError) {
404+
return (
405+
<View style={styles.errorContainer}>
406+
<Text style={styles.errorText}>
407+
Device creation failed: {deviceError}
408+
</Text>
409+
</View>
410+
);
411+
}
326412
if (error) {
327413
return (
328414
<View style={styles.errorContainer}>
329415
<Text style={styles.errorText}>{error}</Text>
330416
</View>
331417
);
332418
}
419+
if (!device) {
420+
return (
421+
<View style={styles.errorContainer}>
422+
<Text style={styles.errorText}>Waiting for GPU device...</Text>
423+
</View>
424+
);
425+
}
333426
if (cameraDevice == null) {
334427
return (
335428
<View style={styles.errorContainer}>

packages/webgpu/cpp/rnwgpu/api/GPU.cpp

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,22 @@ GPU::GPU(jsi::Runtime &runtime) : NativeObject(CLASS_NAME) {
2020

2121
wgpu::InstanceLimits limits{.timedWaitAnyMaxCount = 64};
2222
instanceDesc.requiredLimits = &limits;
23+
24+
// Expose Dawn's experimental adapter features. Several features needed by
25+
// our Android external-texture path (YCbCrVulkanSamplers, StaticSamplers,
26+
// and eventually OpaqueYCbCrAndroidForExternalTexture once it's wired up in
27+
// the Vulkan backend) are tagged Experimental in Dawn's feature table and
28+
// are otherwise hidden from adapter.features. The toggle only unhides
29+
// features; application code still has to list each one in
30+
// requiredFeatures.
31+
static const char *const kEnabledToggles[] = {
32+
"expose_wgsl_experimental_features",
33+
};
34+
wgpu::DawnTogglesDescriptor toggles;
35+
toggles.enabledToggleCount = std::size(kEnabledToggles);
36+
toggles.enabledToggles = kEnabledToggles;
37+
instanceDesc.nextInChain = &toggles;
38+
2339
_instance = wgpu::CreateInstance(&instanceDesc);
2440

2541
auto dispatcher = std::make_shared<async::JSIMicrotaskDispatcher>(runtime);

packages/webgpu/cpp/rnwgpu/api/RnFeatures.h

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,8 @@ inline std::vector<wgpu::FeatureName> rnSharedTextureMemoryBackingFeatures() {
2424
return {wgpu::FeatureName::SharedTextureMemoryIOSurface,
2525
wgpu::FeatureName::SharedFenceMTLSharedEvent};
2626
#elif defined(__ANDROID__)
27-
// OpaqueYCbCrAndroidForExternalTexture is the Vulkan-side equivalent of what
28-
// we get "for free" through IOSurface biplanar textures on Metal: it lets
29-
// CreateExternalTexture wrap an AHB-backed YCbCr texture and have sampling
30-
// route through a SamplerYcbcrConversion implicitly.
3127
return {wgpu::FeatureName::SharedTextureMemoryAHardwareBuffer,
32-
wgpu::FeatureName::SharedFenceSyncFD,
33-
wgpu::FeatureName::OpaqueYCbCrAndroidForExternalTexture};
28+
wgpu::FeatureName::SharedFenceSyncFD};
3429
#else
3530
return {};
3631
#endif

0 commit comments

Comments
 (0)