From 04d0cafaa06c5599dbf26038e749d4df6584170e Mon Sep 17 00:00:00 2001 From: William Candillon Date: Tue, 3 Nov 2026 11:10:30 +0100 Subject: [PATCH 1/3] =?UTF-8?q?fix(=F0=9F=8D=8F):=20don't=20compile=20want?= =?UTF-8?q?sExtendedDynamicRangeContent=20on=20tvOS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #606 --- apps/docs/content/api/canvas.mdx | 2 +- packages/webgpu/apple/MetalLayerColorSpace.mm | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/apps/docs/content/api/canvas.mdx b/apps/docs/content/api/canvas.mdx index 8e20ef7d5..e910c4359 100644 --- a/apps/docs/content/api/canvas.mdx +++ b/apps/docs/content/api/canvas.mdx @@ -141,7 +141,7 @@ To show the views behind the canvas, configure the context with `alphaMode: "pre ## HDR (extended dynamic range) -`context.configure({ toneMapping: { mode } })` says what happens to values above 1.0 on an `rgba16float` canvas: `"standard"` (the default) clamps them to the display's SDR range, `"extended"` lets them use the display's headroom, as in a browser. On iOS 16 and later the `CAMetalLayer` opts into extended dynamic range content for `"extended"`; the mode is accepted and has no effect on an 8-bit canvas, which cannot hold values above 1.0. Android currently ignores `toneMapping`. +`context.configure({ toneMapping: { mode } })` says what happens to values above 1.0 on an `rgba16float` canvas: `"standard"` (the default) clamps them to the display's SDR range, `"extended"` lets them use the display's headroom, as in a browser. On iOS 16 and later the `CAMetalLayer` opts into extended dynamic range content for `"extended"`; the mode is accepted and has no effect on an 8-bit canvas, which cannot hold values above 1.0. tvOS has no extended dynamic range for this layer, so `"extended"` behaves like `"standard"` there. Android currently ignores `toneMapping`. ### Android rendering options diff --git a/packages/webgpu/apple/MetalLayerColorSpace.mm b/packages/webgpu/apple/MetalLayerColorSpace.mm index b9682719d..a1c0a3c76 100644 --- a/packages/webgpu/apple/MetalLayerColorSpace.mm +++ b/packages/webgpu/apple/MetalLayerColorSpace.mm @@ -54,11 +54,18 @@ void applyCAMetalLayerColorSpace(void *nativeSurface, } const bool extendedRange = isFloat && toneMappingMode == CanvasToneMappingMode::Extended; - if (@available(iOS 16.0, tvOS 16.0, *)) { + // The property does not exist on tvOS (the SDK marks it unavailable, which + // @available cannot work around), so it is compiled only where it exists. + // On tvOS "extended" behaves like "standard". +#if TARGET_OS_IOS || TARGET_OS_OSX || TARGET_OS_VISION + if (@available(iOS 16.0, macOS 10.11, visionOS 1.0, *)) { if (metalLayer.wantsExtendedDynamicRangeContent != extendedRange) { metalLayer.wantsExtendedDynamicRangeContent = extendedRange; } } +#else + (void)extendedRange; +#endif // The change must be set synchronously so the first present already sees // it, and it must reach the render server. On a non-main thread (RN JS or // worklet runtime) the property lands in that thread's implicit From 4ecdc3a107448eba9da404e81bed17986a1de298 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Tue, 3 Nov 2026 11:13:16 +0100 Subject: [PATCH 2/3] =?UTF-8?q?fix(=F0=9F=8D=8F):=20read=20the=20video=20t?= =?UTF-8?q?rack=20with=20loadTracksWithMediaType=20on=20visionOS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit tracksWithMediaType: is unavailable on visionOS, so the package did not compile there. --- packages/webgpu/apple/AppleVideoPlayer.mm | 32 +++++++++++++++-------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/packages/webgpu/apple/AppleVideoPlayer.mm b/packages/webgpu/apple/AppleVideoPlayer.mm index 42764e5d7..bca4fd692 100644 --- a/packages/webgpu/apple/AppleVideoPlayer.mm +++ b/packages/webgpu/apple/AppleVideoPlayer.mm @@ -141,6 +141,15 @@ static int rotationFromTransform(CGAffineTransform t) { // The metadata loads asynchronously, so that a remote asset never blocks // the calling thread; the getters report 0 until it has. AVAsset *asset = item.asset; + void (^readVideoTrack)(AVAssetTrack *) = ^(AVAssetTrack *track) { + if (track) { + CGSize size = track.naturalSize; + state->width = static_cast(size.width); + state->height = static_cast(size.height); + state->rotation = rotationFromTransform(track.preferredTransform); + state->frameRate = track.nominalFrameRate; + } + }; #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" [asset @@ -155,17 +164,18 @@ static int rotationFromTransform(CGAffineTransform t) { } if ([asset statusOfValueForKey:@"tracks" error:nil] == AVKeyValueStatusLoaded) { - AVAssetTrack *track = - [[asset tracksWithMediaType:AVMediaTypeVideo] - firstObject]; - if (track) { - CGSize size = track.naturalSize; - state->width = static_cast(size.width); - state->height = static_cast(size.height); - state->rotation = - rotationFromTransform(track.preferredTransform); - state->frameRate = track.nominalFrameRate; - } +#if TARGET_OS_VISION + // tracksWithMediaType: is unavailable on visionOS. + [asset loadTracksWithMediaType:AVMediaTypeVideo + completionHandler:^( + NSArray *tracks, + NSError * /*error*/) { + readVideoTrack(tracks.firstObject); + }]; +#else + readVideoTrack( + [[asset tracksWithMediaType:AVMediaTypeVideo] firstObject]); +#endif } }]; #pragma clang diagnostic pop From fb3301d003153c039598de52fcdd1c6c5d10adbe Mon Sep 17 00:00:00 2001 From: William Candillon Date: Tue, 3 Nov 2026 11:13:16 +0100 Subject: [PATCH 3/3] =?UTF-8?q?ci(=F0=9F=8D=8F):=20syntax-check=20the=20Ap?= =?UTF-8?q?ple=20sources=20against=20every=20Apple=20SDK?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Runs on a hosted macos-latest runner without yarn install or pods: only the Dawn headers are downloaded. --- .github/workflows/apple-sdks.yml | 28 ++++++++++++ packages/webgpu/scripts/check-apple-sdks.sh | 48 +++++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 .github/workflows/apple-sdks.yml create mode 100755 packages/webgpu/scripts/check-apple-sdks.sh diff --git a/.github/workflows/apple-sdks.yml b/.github/workflows/apple-sdks.yml new file mode 100644 index 000000000..64acb7295 --- /dev/null +++ b/.github/workflows/apple-sdks.yml @@ -0,0 +1,28 @@ +name: apple-sdks +on: + push: + branches: + - main + pull_request: + paths: + - "packages/webgpu/apple/**" + - "packages/webgpu/cpp/**" + - "packages/webgpu/package.json" + - "packages/webgpu/scripts/check-apple-sdks.sh" + - ".github/workflows/apple-sdks.yml" +jobs: + apple-sdks: + # Syntax-check only: no yarn install, no pods, no simulator. Only the Dawn + # headers are downloaded (about 1 MB out of the package's tarball). + runs-on: macos-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + - name: Download Dawn headers + run: | + VERSION=$(node -p 'require("./packages/webgpu/package.json").dependencies["react-native-webgpu-dawn"]') + mkdir -p "$RUNNER_TEMP/dawn" + curl -sfL "https://registry.npmjs.org/react-native-webgpu-dawn/-/react-native-webgpu-dawn-$VERSION.tgz" \ + | tar xz -C "$RUNNER_TEMP/dawn" package/include + - name: Check Apple sources against every SDK + run: packages/webgpu/scripts/check-apple-sdks.sh "$RUNNER_TEMP/dawn/package/include" diff --git a/packages/webgpu/scripts/check-apple-sdks.sh b/packages/webgpu/scripts/check-apple-sdks.sh new file mode 100755 index 000000000..77fcb862e --- /dev/null +++ b/packages/webgpu/scripts/check-apple-sdks.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +# Syntax-checks the Apple sources that compile without React headers against +# every Apple SDK the package can be built for. An API that exists on iOS but +# is marked unavailable on another platform (wantsExtendedDynamicRangeContent +# on tvOS, #606) is a compile error that @available cannot hide, and nothing +# else in CI builds for tvOS or visionOS. +# +# Usage: check-apple-sdks.sh [dawn-include-dir] +# Defaults to the Dawn headers installed in node_modules. +set -euo pipefail + +cd "$(dirname "$0")/.." + +DAWN_INCLUDE="${1:-}" +if [ -z "$DAWN_INCLUDE" ]; then + DAWN_INCLUDE="$(dirname "$(node -p 'require.resolve("react-native-webgpu-dawn/package.json")')")/include" +fi + +# The other files under apple/ need React and codegen headers, which only +# exist after a pod install. +FILES=(apple/MetalLayerColorSpace.mm apple/AppleVideoPlayer.mm) + +# sdk:target pairs, using the podspec's minimum versions. +TARGETS=( + iphoneos:arm64-apple-ios15.1 + iphonesimulator:arm64-apple-ios15.1-simulator + macosx:arm64-apple-macos10.15 + xros:arm64-apple-xros1.0 + appletvos:arm64-apple-tvos15.1 + appletvsimulator:arm64-apple-tvos15.1-simulator +) + +status=0 +for pair in "${TARGETS[@]}"; do + sdk="${pair%%:*}" + target="${pair#*:}" + for file in "${FILES[@]}"; do + if xcrun --sdk "$sdk" clang++ -target "$target" -std=c++20 -fobjc-arc \ + -fsyntax-only -w -x objective-c++ -I "$DAWN_INCLUDE" -I cpp/rnwgpu \ + -I cpp -I apple "$file"; then + echo "✅ $sdk $file" + else + echo "❌ $sdk $file" + status=1 + fi + done +done +exit $status