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/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/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 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 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