feat(🤖): correct per-buffer YUV conversion for Android camera frames#387
Open
wcandillon wants to merge 1 commit into
Open
feat(🤖): correct per-buffer YUV conversion for Android camera frames#387wcandillon wants to merge 1 commit into
wcandillon wants to merge 1 commit into
Conversation
Three improvements to the Android camera-frame path, found while reviewing the VisionCamera integration: 1. Expose GPUExternalTexture.yuvToRgbMatrix (non-spec extension). Dawn samples Android external-format (opaque YCbCr) buffers through a Vulkan conversion hard-coded to RGB_IDENTITY (SamplerVk.cpp, see crbug.com/497675620), so shaders receive raw [Y, Cb, Cr] and previously had to guess the conversion. The example hard-coded BT.709 narrow-range, but Android camera streams are usually BT.601, and range varies by device, which skews colors. The driver reports the correct model and range per buffer (suggestedYcbcrModel/Range); Dawn captures them at import time and we now read them back via the shared memory's AHardwareBuffer properties and derive the exact 3x4 conversion matrix. On iOS and for RGBA surfaces the matrix is the identity passthrough, so shaders can apply it unconditionally. The VisionCamera example now uploads the matrix as uniforms instead of hard-coding coefficients. 2. Fail fast on CPU-only AHardwareBuffers. Dawn only grants TextureBinding when the AHB was allocated with AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE; without it, import fails deep inside Dawn with an opaque validation error. wrapNativeBuffer now checks the usage bits and throws an actionable error (pointing at vision-camera's pixelFormat: 'native'). 3. Remove the unreachable "defined biplanar format" branch on Android. Dawn maps every YUV AHB format (Y8Cb8Cr8_420, P010, vendor formats) to OpaqueYCbCrAndroid (AHBFunctions.cpp), so the R8BG8Biplanar420Unorm plane-splitting path and its BT.709 matrix could never run. Also folds the Android both-axes flip in the example into the rotation passed to importExternalTexture (a double mirror is a 180° rotation), removing the per-fragment flip branches from the WGSL prelude. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Three improvements to the Android camera-frame path, found while reviewing the VisionCamera integration. Companion PR on the producer side: mrousavy/react-native-vision-camera#4023 (GPU-sampleable YUV buffers).
1.
GPUExternalTexture.yuvToRgbMatrix(non-spec extension): per-buffer correct color conversionDawn samples Android external-format (opaque YCbCr) buffers through a Vulkan
SamplerYcbcrConversionhard-coded toRGB_IDENTITY+ full range (SamplerVk.cpp::GetYCbCrForTextureView, crbug.com/497675620), sotextureSampleBaseClampToEdgereturns raw[Y, Cb, Cr]and the shader must convert. The example hard-coded BT.709 narrow-range coefficients, but Android camera streams are usually BT.601, and range varies by device/driver, so colors were subtly wrong (hue shift + washed-out or crushed levels depending on the device).The driver reports the truth per buffer (
suggestedYcbcrModel/suggestedYcbcrRange); Dawn captures it at import time. We now read it back throughSharedTextureMemoryAHardwareBufferProperties.yCbCrInfoand derive the exact 3x4[Y, Cb, Cr, 1] -> R'G'B'matrix (601/709/2020, full/narrow). On iOS (Dawn converts NV12 in the sampler transform) and for RGBA surfaces the property is the identity passthrough, so shaders can apply it unconditionally:The VisionCamera example now uploads the matrix as three
vec4funiform rows instead of hard-coding coefficients in WGSL.2. Fail fast on CPU-only AHardwareBuffers
Dawn derives WebGPU usages from the AHB usage bits (
AHBFunctions.cpp) and only grantsTextureBindingforAHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE. CameraX's default ImageReaders allocate CPU-only buffers, and the resulting failure was an opaque validation error deep inside Dawn.wrapNativeBuffernow checks the usage bits and throws an actionable error pointing at vision-camera'spixelFormat: 'native'.3. Remove the unreachable biplanar branch on Android
Dawn maps every YUV AHB format (
Y8Cb8Cr8_420,P010, vendor formats) toOpaqueYCbCrAndroid(AHBFunctions.cpp::FormatFromAHardwareBufferFormat), so theR8BG8Biplanar420Unormplane-splitting path and itskBT709LimitedToRgbmatrix could never run. Deleted, with the reasoning documented at the call site.Bonus: example cleanup
The Android both-axes flip in the example (
CAMERA_FLIP_X/CAMERA_FLIP_Y) is exactly a 180° rotation, now folded into therotationpassed toimportExternalTexture; the WGSL prelude loses its per-fragment flip branches and platform conditionals.Testing
yarn tscandyarn lintclean inpackages/webgpuandapps/example.GPUExternalTexture.cppsyntax-checked for both branches (Apple clang and NDK r27aarch64-linux-android29).Longer term, the cleaner fix is upstream: Dawn honoring
suggestedYcbcrModel/Rangein its static sampler would let us deletecameraDecodeentirely and also fix chroma siting/filtering. We already carry a Dawn patch, so that's a viable follow-up.🤖 Generated with Claude Code