diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index 219ddedc10..3b3bc160cc 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -5,10 +5,6 @@ inputs: github_token: description: 'GitHub Token' required: false - download_skia: - description: 'Download Skia Binary Artifacts' - required: false - default: 'true' graphite: description: 'Use Graphite Skia Build' required: false @@ -32,13 +28,11 @@ runs: - name: Install dependencies run: yarn install --immutable shell: bash - env: - SKIP_SKIA_DOWNLOAD: ${{ inputs.download_skia == 'false' && '1' || '' }} - - name: Copy Skia headers (standard build) + - name: Install Skia (standard build) if: inputs.graphite != 'true' working-directory: packages/skia - run: yarn copy-skia-headers + run: yarn install-skia shell: bash - name: Install Skia Graphite diff --git a/.github/workflows/build-dawn.yml b/.github/workflows/build-dawn.yml index fd05ad5eb1..a4e2216c21 100644 --- a/.github/workflows/build-dawn.yml +++ b/.github/workflows/build-dawn.yml @@ -7,7 +7,7 @@ concurrency: cancel-in-progress: true env: - DAWN_BRANCH: chromium/7770 # Configure the Dawn branch to checkout + DAWN_BRANCH: chromium/7849 # Configure the Dawn branch to checkout jobs: prepare-release: @@ -19,7 +19,7 @@ jobs: release_name: ${{ steps.release_meta.outputs.release_name }} steps: - name: Checkout repository - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v6.0.3 - name: Determine Dawn branch metadata id: dawn_meta @@ -45,7 +45,7 @@ jobs: - name: Create GitHub release id: create_release - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: @@ -92,7 +92,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout main repository - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v6.0.3 - name: Checkout Dawn repository run: | @@ -100,7 +100,7 @@ jobs: - name: Setup Android NDK id: setup-ndk - uses: nttld/setup-ndk@v1 + uses: nttld/setup-ndk@ed92fe6cadad69be94a966a7ee3271275e62f779 # v1.6.0 with: ndk-version: r27d @@ -117,6 +117,7 @@ jobs: -DCMAKE_BUILD_TYPE=Release \ -DBUILD_SHARED_LIBS=OFF \ -DDAWN_BUILD_MONOLITHIC_LIBRARY=SHARED \ + -DDAWN_BUILD_PROTOBUF=OFF \ -DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON \ -DCMAKE_EXE_LINKER_FLAGS="-llog" \ -DCMAKE_SHARED_LINKER_FLAGS="-llog -Wl,-z,max-page-size=16384" @@ -175,7 +176,7 @@ jobs: runs-on: macos-latest-large steps: - name: Checkout main repository - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v6.0.3 - name: Checkout Dawn repository run: | @@ -183,7 +184,7 @@ jobs: # submodules: true - name: Setup Xcode - uses: maxim-lobanov/setup-xcode@v1 + uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1.7.0 with: xcode-version: latest-stable @@ -200,7 +201,8 @@ jobs: -C externals/dawn/.github/workflows/dawn-ci.cmake \ -DCMAKE_TOOLCHAIN_FILE=build-tools/apple.toolchain.cmake \ ${{ matrix.cmake_args }} \ - -DCMAKE_BUILD_TYPE=Release + -DCMAKE_BUILD_TYPE=Release \ + -DDAWN_BUILD_PROTOBUF=OFF ninja -C ${{ matrix.output_dir }} - name: Upload build artifacts uses: actions/upload-artifact@v7 @@ -214,7 +216,7 @@ jobs: needs: [prepare-release, mobile-android, mobile-apple] steps: - name: Checkout main repository - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v6.0.3 - name: Download all build artifacts uses: actions/download-artifact@v8 @@ -263,7 +265,7 @@ jobs: tar -czf dawn-headers-${TAG}.tar.gz dawn-headers - name: Upload to GitHub release - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: diff --git a/.github/workflows/build-npm.yml b/.github/workflows/build-npm.yml index 145d2c256e..f270e3fbb4 100644 --- a/.github/workflows/build-npm.yml +++ b/.github/workflows/build-npm.yml @@ -15,7 +15,7 @@ jobs: cancel-in-progress: true steps: - name: checkout - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 # v5.0.0 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v5.0.0 with: submodules: recursive @@ -38,7 +38,7 @@ jobs: NPM_CONFIG_PROVENANCE: true - name: Deploy to GitHub Pages - uses: peaceiris/actions-gh-pages@4f9cc6602d3f66b9c108549d475ec49e8ef4d45e # v4.0.0 + uses: peaceiris/actions-gh-pages@84c30a85c19949d7eee79c4ff27748b70285e453 # v4.1.0 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./apps/docs/build diff --git a/.github/workflows/build-skia-graphite.yml b/.github/workflows/build-skia-graphite.yml index 68fbc58bfb..dd0eefa203 100644 --- a/.github/workflows/build-skia-graphite.yml +++ b/.github/workflows/build-skia-graphite.yml @@ -15,11 +15,6 @@ on: required: false type: boolean default: false - skip_skia_download: - description: 'Skip downloading prebuilt Skia (always true for building from source)' - required: false - type: boolean - default: true jobs: prepare-release: runs-on: ubuntu-latest @@ -30,7 +25,7 @@ jobs: release_name: ${{ steps.release_meta.outputs.release_name }} steps: - name: Checkout repository - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 # v5.0.0 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v5.0.0 with: submodules: recursive fetch-depth: 0 @@ -85,7 +80,7 @@ jobs: - name: Create GitHub release if: ${{ github.event.inputs.dry_run != 'true' }} id: create_release - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: @@ -124,7 +119,7 @@ jobs: fail-fast: false steps: - name: checkout - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 # v5.0.0 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v5.0.0 with: submodules: recursive @@ -140,7 +135,7 @@ jobs: - name: Setup Android NDK id: setup-ndk if: startsWith(matrix.target, 'android') - uses: nttld/setup-ndk@afb4c9964b521afb97c864b7d40b11e6911bd410 # v1.5.0 + uses: nttld/setup-ndk@ed92fe6cadad69be94a966a7ee3271275e62f779 # v1.6.0 with: ndk-version: r27d @@ -149,12 +144,11 @@ jobs: run: echo "ANDROID_NDK=$ANDROID_HOME/ndk-bundle" >> $GITHUB_ENV - name: Setup Ninja - uses: seanmiddleditch/gha-setup-ninja@master + uses: seanmiddleditch/gha-setup-ninja@7e868db0f3406270dd46e1dac26c65f621456723 # master - name: Setup uses: ./.github/actions/setup with: - download_skia: ${{ github.event.inputs.skip_skia_download == 'true' && 'false' || 'true' }} github_token: ${{ secrets.GITHUB_TOKEN }} - name: Update depot_tools @@ -213,7 +207,7 @@ jobs: - name: Upload binaries to GitHub release - ${{ matrix.artifact_name }} if: ${{ github.event.inputs.dry_run != 'true' }} - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: @@ -258,7 +252,7 @@ jobs: - name: Upload Graphite Headers to GitHub release if: ${{ matrix.target == 'apple-ios' && github.event.inputs.dry_run != 'true' }} - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: diff --git a/.github/workflows/build-skia.yml b/.github/workflows/build-skia.yml index 0bac957ac8..5b0f372cd3 100644 --- a/.github/workflows/build-skia.yml +++ b/.github/workflows/build-skia.yml @@ -11,11 +11,6 @@ on: required: false type: boolean default: false - skip_skia_download: - description: 'Skip downloading prebuilt Skia (always true for building from source)' - required: false - type: boolean - default: true jobs: prepare-release: runs-on: ubuntu-latest @@ -26,7 +21,7 @@ jobs: release_name: ${{ steps.release_meta.outputs.release_name }} steps: - name: Checkout repository - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 # v5.0.0 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v5.0.0 with: submodules: recursive fetch-depth: 0 @@ -63,7 +58,7 @@ jobs: - name: Create GitHub release if: ${{ github.event.inputs.dry_run != 'true' }} id: create_release - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: @@ -103,24 +98,23 @@ jobs: fail-fast: false steps: - name: checkout - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 # v5.0.0 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v5.0.0 with: submodules: recursive - name: Setup Android NDK id: setup-ndk if: startsWith(matrix.target, 'android') - uses: nttld/setup-ndk@afb4c9964b521afb97c864b7d40b11e6911bd410 # v1.5.0 + uses: nttld/setup-ndk@ed92fe6cadad69be94a966a7ee3271275e62f779 # v1.6.0 with: ndk-version: r27c - name: Setup Ninja - uses: seanmiddleditch/gha-setup-ninja@master + uses: seanmiddleditch/gha-setup-ninja@7e868db0f3406270dd46e1dac26c65f621456723 # master - name: Setup uses: ./.github/actions/setup with: - download_skia: ${{ github.event.inputs.skip_skia_download == 'true' && 'false' || 'true' }} github_token: ${{ secrets.GITHUB_TOKEN }} - name: Update depot_tools @@ -164,7 +158,7 @@ jobs: - name: Upload binaries to GitHub release - ${{ matrix.artifact_name }} if: ${{ github.event.inputs.dry_run != 'true' }} - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: diff --git a/.github/workflows/ci-graphite.yml b/.github/workflows/ci-graphite.yml index bc5a0137fa..68add60d64 100644 --- a/.github/workflows/ci-graphite.yml +++ b/.github/workflows/ci-graphite.yml @@ -18,12 +18,13 @@ concurrency: jobs: build-android-graphite: + if: ${{ github.repository == 'shopify/react-native-skia' }} runs-on: ubuntu-latest env: TURBO_CACHE_DIR: .turbo/android steps: - name: Checkout - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 # v5.0.0 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v5.0.0 with: submodules: recursive @@ -34,7 +35,7 @@ jobs: graphite: true - name: Free disk space - uses: jlumbroso/free-disk-space@main + uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1 with: tool-cache: false android: false @@ -45,7 +46,7 @@ jobs: swap-storage: true - name: Cache turborepo for Android - uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: ${{ env.TURBO_CACHE_DIR }} key: ${{ runner.os }}-turborepo-android-${{ hashFiles('yarn.lock') }} @@ -59,7 +60,7 @@ jobs: java-version: '17' - name: Install NDK - uses: nttld/setup-ndk@afb4c9964b521afb97c864b7d40b11e6911bd410 # v1.5.0 + uses: nttld/setup-ndk@ed92fe6cadad69be94a966a7ee3271275e62f779 # v1.6.0 id: setup-ndk with: ndk-version: r27d @@ -76,7 +77,7 @@ jobs: echo "sdk.dir=$ANDROID_HOME" > $GITHUB_WORKSPACE/apps/example/android/local.properties - name: Cache Gradle - uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: | ~/.gradle/wrapper @@ -92,7 +93,7 @@ jobs: yarn turbo run build:android --concurrency 1 - name: Cache apk - uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 env: cache-name: cache-apk with: @@ -106,6 +107,7 @@ jobs: path: apps/example/android/app/build/outputs/apk/debug/app-debug.apk test-android-graphite: + if: ${{ github.repository == 'shopify/react-native-skia' }} needs: build-android-graphite runs-on: ubuntu-latest timeout-minutes: 60 @@ -113,7 +115,7 @@ jobs: TURBO_CACHE_DIR: .turbo/android steps: - name: checkout - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 # v5.0.0 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v5.0.0 with: submodules: recursive @@ -124,7 +126,7 @@ jobs: graphite: true - name: Setup Android SDK - uses: android-actions/setup-android@v4 + uses: android-actions/setup-android@40fd30fb8d7440372e1316f5d1809ec01dcd3699 # v4.0.1 - name: Install Android SDK tools run: | @@ -141,7 +143,7 @@ jobs: - name: Restore APK id: cache-apk - uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 + uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae with: path: apps/example/android/app/build/outputs/apk/debug/app-debug.apk key: apk-graphite-${{ github.sha }} @@ -151,7 +153,7 @@ jobs: run: E2E=true yarn start & - name: Run Android Emulator Tests - uses: reactivecircus/android-emulator-runner@v2 + uses: reactivecircus/android-emulator-runner@e89f39f1abbbd05b1113a29cf4db69e7540cae5a # v2.37.0 with: api-level: 30 arch: x86_64 @@ -168,7 +170,15 @@ jobs: adb shell monkey -p com.microsoft.reacttestapp 1 # Run tests - cd packages/skia && CI=true yarn test -i e2e/Path --testPathIgnorePatterns Paragraphs + # On Graphite we run the Path render tests plus the WebGPU specs. + # ArrayBuffer, ComputeShader, ErrorScope, ImageBitmapBounds and + # DawnToggles are pure surface.eval round-trips that assert on + # returned values. The external-texture specs (ExternalTexture, + # ImportExternalTexture, SharedTextureMemory) additionally exercise + # NativeBuffer import and GPU read-back; they self-validate via + # copyTextureToBuffer (not Skia image readPixels) and skip + # themselves where NativeBuffer/createImageBitmap are unavailable. + cd packages/skia && CI=true yarn test -i "e2e/(Path|ArrayBuffer|ComputeShader|ErrorScope|ImageBitmapBounds|DawnToggles|ExternalTexture|ImportExternalTexture|SharedTextureMemory)" --testPathIgnorePatterns Paragraphs - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v6.0.0 if: failure() @@ -183,12 +193,13 @@ jobs: name: tests-docs-screenshots build-test-ios-graphite: + if: ${{ github.repository == 'shopify/react-native-skia' }} runs-on: macos-latest-xlarge env: TURBO_CACHE_DIR: .turbo/ios steps: - name: Checkout - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 # v5.0.0 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v5.0.0 with: submodules: recursive @@ -199,7 +210,7 @@ jobs: graphite: true - name: Cache turborepo for iOS - uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: ${{ env.TURBO_CACHE_DIR }} key: ${{ runner.os }}-turborepo-ios-${{ hashFiles('yarn.lock') }} @@ -228,4 +239,12 @@ jobs: - name: Run e2e tests working-directory: packages/skia - run: CI=true E2E=true yarn test -i Paths + # On Graphite we run the Paths render test plus the WebGPU specs. + # ArrayBuffer, ComputeShader, ErrorScope, ImageBitmapBounds and + # DawnToggles are pure surface.eval round-trips that assert on returned + # values. The external-texture specs (ExternalTexture, + # ImportExternalTexture, SharedTextureMemory) additionally exercise + # NativeBuffer import and GPU read-back; they self-validate via + # copyTextureToBuffer (not Skia image readPixels) and skip themselves + # where NativeBuffer/createImageBitmap are unavailable. + run: CI=true E2E=true yarn test -i "e2e/(Paths|ArrayBuffer|ComputeShader|ErrorScope|ImageBitmapBounds|DawnToggles|ExternalTexture|ImportExternalTexture|SharedTextureMemory)" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fb2d86dd97..49d006f0f2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,10 +18,11 @@ concurrency: jobs: lint: + if: ${{ github.repository == 'shopify/react-native-skia' }} runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 # v5.0.0 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v5.0.0 with: submodules: recursive @@ -57,10 +58,11 @@ jobs: run: yarn tsc test: + if: ${{ github.repository == 'shopify/react-native-skia' }} runs-on: macos-latest steps: - name: Checkout - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 # v5.0.0 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v5.0.0 with: submodules: recursive @@ -73,12 +75,13 @@ jobs: run: yarn test build-library: + if: ${{ github.repository == 'shopify/react-native-skia' }} runs-on: ubuntu-latest outputs: run_id: ${{ github.run_id }} steps: - name: Checkout - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 # v5.0.0 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v5.0.0 with: submodules: recursive @@ -109,6 +112,7 @@ jobs: YARN_NPM_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} test-package-e2e: + if: ${{ github.repository == 'shopify/react-native-skia' }} needs: build-library uses: ./.github/workflows/test-skia-package.yml with: @@ -117,11 +121,23 @@ jobs: test_android: true test_web: true + test-package-e2e-static-frameworks: + if: ${{ github.repository == 'shopify/react-native-skia' }} + needs: build-library + uses: ./.github/workflows/test-skia-package.yml + with: + skia_version: artifact:${{ needs.build-library.outputs.run_id }} + test_ios: true + test_android: false + test_web: false + ios_use_frameworks: static + build-windows-library: + if: ${{ github.repository == 'shopify/react-native-skia' }} runs-on: windows-latest steps: - name: Checkout - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 # v5.0.0 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v5.0.0 with: submodules: recursive @@ -134,12 +150,13 @@ jobs: run: yarn build build-android: + if: ${{ github.repository == 'shopify/react-native-skia' }} runs-on: ubuntu-latest env: TURBO_CACHE_DIR: .turbo/android steps: - name: Checkout - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 # v5.0.0 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v5.0.0 with: submodules: recursive @@ -161,7 +178,7 @@ jobs: df -h - name: Cache turborepo for Android - uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: ${{ env.TURBO_CACHE_DIR }} key: ${{ runner.os }}-turborepo-android-${{ hashFiles('yarn.lock') }} @@ -175,7 +192,7 @@ jobs: java-version: '17' - name: Install NDK - uses: nttld/setup-ndk@afb4c9964b521afb97c864b7d40b11e6911bd410 # v1.5.0 + uses: nttld/setup-ndk@ed92fe6cadad69be94a966a7ee3271275e62f779 # v1.6.0 id: setup-ndk with: ndk-version: r27d @@ -192,7 +209,7 @@ jobs: echo "sdk.dir=$ANDROID_HOME" > $GITHUB_WORKSPACE/apps/example/android/local.properties - name: Cache Gradle - uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: | ~/.gradle/wrapper @@ -208,7 +225,7 @@ jobs: yarn turbo run build:android --concurrency 1 - name: Cache apk - uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 env: cache-name: cache-apk with: @@ -222,6 +239,7 @@ jobs: path: apps/example/android/app/build/outputs/apk/debug/app-debug.apk test-android: + if: ${{ github.repository == 'shopify/react-native-skia' }} needs: build-android runs-on: ubuntu-latest timeout-minutes: 60 @@ -229,7 +247,7 @@ jobs: TURBO_CACHE_DIR: .turbo/android steps: - name: checkout - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 # v5.0.0 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v5.0.0 with: submodules: recursive @@ -239,7 +257,7 @@ jobs: github_token: ${{ secrets.GITHUB_TOKEN }} - name: Setup Android SDK - uses: android-actions/setup-android@v4 + uses: android-actions/setup-android@40fd30fb8d7440372e1316f5d1809ec01dcd3699 # v4.0.1 - name: Install Android SDK tools run: | @@ -256,7 +274,7 @@ jobs: - name: Restore APK id: cache-apk - uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 + uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae with: path: apps/example/android/app/build/outputs/apk/debug/app-debug.apk key: apk-${{ github.sha }} @@ -266,7 +284,7 @@ jobs: run: E2E=true yarn start & - name: Run Android Emulator Tests - uses: reactivecircus/android-emulator-runner@v2 + uses: reactivecircus/android-emulator-runner@e89f39f1abbbd05b1113a29cf4db69e7540cae5a # v2.37.0 with: api-level: 30 arch: x86_64 @@ -298,12 +316,13 @@ jobs: name: tests-docs-screenshots build-test-ios: + if: ${{ github.repository == 'shopify/react-native-skia' }} runs-on: macos-latest-xlarge env: TURBO_CACHE_DIR: .turbo/ios steps: - name: Checkout - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 # v5.0.0 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v5.0.0 with: submodules: recursive @@ -313,7 +332,7 @@ jobs: github_token: ${{ secrets.GITHUB_TOKEN }} - name: Cache turborepo for iOS - uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: ${{ env.TURBO_CACHE_DIR }} key: ${{ runner.os }}-turborepo-ios-${{ hashFiles('yarn.lock') }} @@ -345,12 +364,13 @@ jobs: run: CI=true yarn e2e build-test-macos: + if: ${{ github.repository == 'shopify/react-native-skia' }} runs-on: macos-latest env: TURBO_CACHE_DIR: .turbo/macos steps: - name: Checkout - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 # v5.0.0 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v5.0.0 with: submodules: recursive @@ -360,7 +380,7 @@ jobs: github_token: ${{ secrets.GITHUB_TOKEN }} - name: Cache turborepo for macOS - uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: ${{ env.TURBO_CACHE_DIR }} key: ${{ runner.os }}-turborepo-macos-${{ hashFiles('yarn.lock') }} @@ -374,7 +394,7 @@ jobs: # runs-on: macos-latest # steps: # - name: Checkout - # uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 # v5.0.0 + # uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v5.0.0 # with: # submodules: recursive diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index df3b495096..e07b30b2e0 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 # v5.0.0 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v5.0.0 with: submodules: recursive @@ -26,7 +26,7 @@ jobs: yarn build - name: Deploy to GitHub Pages - uses: peaceiris/actions-gh-pages@4f9cc6602d3f66b9c108549d475ec49e8ef4d45e # v4.0.0 + uses: peaceiris/actions-gh-pages@84c30a85c19949d7eee79c4ff27748b70285e453 # v4.1.0 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./apps/docs/build diff --git a/.github/workflows/test-skia-package.yml b/.github/workflows/test-skia-package.yml index 07ea874ff4..0236bd1536 100644 --- a/.github/workflows/test-skia-package.yml +++ b/.github/workflows/test-skia-package.yml @@ -37,6 +37,11 @@ on: required: false type: boolean default: true + ios_use_frameworks: + description: 'iOS framework linkage: "dynamic" (default) or "static"' + required: false + type: string + default: 'dynamic' jobs: test-skia-ios: @@ -46,10 +51,10 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v6.0.3 - name: Setup Node.js - uses: actions/setup-node@v6.3.0 + uses: actions/setup-node@v6.4.0 with: node-version: '20' @@ -58,6 +63,14 @@ jobs: corepack enable yarn --version + - name: Select latest Xcode + run: | + LATEST_XCODE=$(ls -d /Applications/Xcode_*.app | sort -V | tail -n 1) + echo "Selecting: $LATEST_XCODE" + sudo xcode-select -s "$LATEST_XCODE/Contents/Developer" + xcodebuild -version + swift --version + - name: List available simulators run: xcrun simctl list devices @@ -68,7 +81,7 @@ jobs: pwd - name: Create Expo app with Skia template - uses: nick-fields/retry@v4 + uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4.0.0 with: timeout_minutes: 10 max_attempts: 3 @@ -111,12 +124,6 @@ jobs: echo "Installed React Native Skia version:" yarn list @shopify/react-native-skia --depth=0 - - name: Install compatible Worklets version - working-directory: /Users/runner/skia-test-app/my-app - run: | - echo "Installing react-native-worklets@^0.7.1 for Reanimated compatibility..." - yarn add react-native-worklets@^0.7.1 - - name: Install TypeScript dependencies working-directory: /Users/runner/skia-test-app/my-app run: | @@ -129,10 +136,24 @@ jobs: echo "Installing expo-dev-client..." yarn expo install expo-dev-client + - name: Enable static frameworks (iOS) + if: inputs.ios_use_frameworks == 'static' + working-directory: /Users/runner/skia-test-app/my-app + run: | + echo "Configuring expo-build-properties for useFrameworks: 'static'..." + yarn expo install expo-build-properties + node -e " + const fs = require('fs'); + const cfg = JSON.parse(fs.readFileSync('app.json', 'utf8')); + cfg.expo.plugins = cfg.expo.plugins || []; + cfg.expo.plugins.push(['expo-build-properties', { ios: { useFrameworks: 'static' } }]); + fs.writeFileSync('app.json', JSON.stringify(cfg, null, 2)); + " + - name: Prebuild native directories working-directory: /Users/runner/skia-test-app/my-app run: | - echo "Generating native iOS directory..." + echo "Generating native iOS directory (linkage: ${{ inputs.ios_use_frameworks }})..." yarn expo prebuild --platform ios - name: Install iOS dependencies @@ -188,6 +209,7 @@ jobs: echo "✓ Created Expo app with Skia template" echo "✓ Installed @shopify/react-native-skia@${{ inputs.skia_version }}" echo "✓ Installed expo-dev-client" + echo "✓ Framework linkage: ${{ inputs.ios_use_frameworks }}" echo "✓ Built and ran iOS app on ${{ inputs.simulator_device }}" echo "================================================================" @@ -198,10 +220,10 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v6.0.3 - name: Setup Node.js - uses: actions/setup-node@v6.3.0 + uses: actions/setup-node@v6.4.0 with: node-version: '20' @@ -231,7 +253,7 @@ jobs: pwd - name: Create Expo app with Skia template - uses: nick-fields/retry@v4 + uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4.0.0 with: timeout_minutes: 10 max_attempts: 3 @@ -274,12 +296,6 @@ jobs: echo "Installed React Native Skia version:" yarn list @shopify/react-native-skia --depth=0 - - name: Install compatible Worklets version - working-directory: /Users/runner/skia-test-app/my-app - run: | - echo "Installing react-native-worklets@^0.7.1 for Reanimated compatibility..." - yarn add react-native-worklets@^0.7.1 - - name: Install TypeScript dependencies working-directory: /Users/runner/skia-test-app/my-app run: | @@ -362,10 +378,10 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v6.0.2 + uses: actions/checkout@v6.0.3 - name: Setup Node.js - uses: actions/setup-node@v6.3.0 + uses: actions/setup-node@v6.4.0 with: node-version: '20' @@ -381,7 +397,7 @@ jobs: pwd - name: Create Expo app with Skia template - uses: nick-fields/retry@v4 + uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4.0.0 with: timeout_minutes: 10 max_attempts: 3 @@ -424,12 +440,6 @@ jobs: echo "Installed React Native Skia version:" yarn list @shopify/react-native-skia --depth=0 - - name: Install compatible Worklets version - working-directory: /Users/runner/skia-test-app/my-app - run: | - echo "Installing react-native-worklets@^0.7.1 for Reanimated compatibility..." - yarn add react-native-worklets@^0.7.1 - - name: Install TypeScript dependencies working-directory: /Users/runner/skia-test-app/my-app run: | diff --git a/.github/workflows/test-skia-release.yml b/.github/workflows/test-skia-release.yml index 712d1a9f36..75f3a1741c 100644 --- a/.github/workflows/test-skia-release.yml +++ b/.github/workflows/test-skia-release.yml @@ -38,6 +38,11 @@ on: required: false default: true type: boolean + ios_use_frameworks: + description: 'iOS framework linkage: "dynamic" (default) or "static"' + required: false + default: 'dynamic' + type: string jobs: test-skia: @@ -50,3 +55,4 @@ jobs: simulator_device: ${{ inputs.simulator_device }} android_api_level: ${{ inputs.android_api_level }} android_device: ${{ inputs.android_device }} + ios_use_frameworks: ${{ inputs.ios_use_frameworks }} diff --git a/.github/workflows/wcandillon-build-skia-graphite.yml b/.github/workflows/wcandillon-build-skia-graphite.yml new file mode 100644 index 0000000000..819245644b --- /dev/null +++ b/.github/workflows/wcandillon-build-skia-graphite.yml @@ -0,0 +1,282 @@ +# OWNER: wcandillon/react-native-skia +# RUNNER: self-hosted macOS. Shares the `wcandillon-self-hosted-macos-build` +# concurrency group with wcandillon-build-skia so the two never build in +# parallel on the single runner. The build matrix runs one target at a time +# (max-parallel: 1) for the same reason. +name: wcandillon - Build SKIA Graphite +on: + workflow_dispatch: + inputs: + skia_branch: + description: 'Skia branch to build (e.g., chrome/m144). Leave empty to use default submodule branch.' + required: false + default: '' + tag_suffix: + description: 'Optional suffix to append to tag (e.g., "a" for skia-graphite-m144a)' + required: false + default: '' + dry_run: + description: 'Test build only - skip GitHub release creation and upload' + required: false + type: boolean + default: false + +concurrency: + group: wcandillon-self-hosted-macos-build + cancel-in-progress: false + +jobs: + prepare-release: + if: ${{ github.repository == 'wcandillon/react-native-skia' }} + runs-on: ubuntu-latest + outputs: + skia_branch: ${{ steps.skia_meta.outputs.branch }} + skia_branch_slug: ${{ steps.skia_meta.outputs.branch_slug }} + tag_name: ${{ steps.release_meta.outputs.tag_name }} + release_name: ${{ steps.release_meta.outputs.release_name }} + steps: + - name: Checkout repository + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v5.0.0 + with: + submodules: recursive + fetch-depth: 0 + + - name: Checkout custom Skia branch + if: ${{ github.event.inputs.skia_branch != '' }} + working-directory: externals/skia + run: | + echo "Checking out Skia branch: ${{ github.event.inputs.skia_branch }}" + git fetch origin ${{ github.event.inputs.skia_branch }} + git checkout FETCH_HEAD + echo "Current Skia commit: $(git rev-parse HEAD)" + + - name: Determine Skia branch + id: skia_meta + run: | + set -eo pipefail + if [ ! -d "externals/skia" ]; then + echo "::error::externals/skia not found." + exit 1 + fi + # Use input branch if provided, otherwise detect from git + if [ -n "$INPUT_SKIA_BRANCH" ]; then + branch="$INPUT_SKIA_BRANCH" + else + branch=$(git -C externals/skia branch --show-current || true) + if [ -z "$branch" ]; then + branch=$(git -C externals/skia describe --all --exact-match 2>/dev/null || true) + fi + if [ -z "$branch" ]; then + branch=$(git -C externals/skia rev-parse --short HEAD) + fi + fi + # Strip remotes/origin/chrome/ or chrome/ prefix if present + branch=${branch#remotes/origin/chrome/} + branch=${branch#chrome/} + slug=${branch//\//-} + echo "branch=$branch" >> "$GITHUB_OUTPUT" + echo "branch_slug=$slug" >> "$GITHUB_OUTPUT" + env: + INPUT_SKIA_BRANCH: ${{ github.event.inputs.skia_branch }} + - name: Compute release metadata + id: release_meta + run: | + tag="skia-graphite-${SKIA_BRANCH_SLUG}${TAG_SUFFIX}" + echo "tag_name=$tag" >> "$GITHUB_OUTPUT" + echo "release_name=Skia Graphite ${SKIA_BRANCH}${TAG_SUFFIX}" >> "$GITHUB_OUTPUT" + env: + SKIA_BRANCH: ${{ steps.skia_meta.outputs.branch }} + SKIA_BRANCH_SLUG: ${{ steps.skia_meta.outputs.branch_slug }} + TAG_SUFFIX: ${{ github.event.inputs.tag_suffix }} + - name: Create GitHub release + if: ${{ github.event.inputs.dry_run != 'true' }} + id: create_release + uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ steps.release_meta.outputs.tag_name }} + name: ${{ steps.release_meta.outputs.release_name }} + body: "Skia Graphite prebuilt binaries for version ${{ steps.skia_meta.outputs.branch }}" + draft: false + prerelease: true + generate_release_notes: false + build: + if: ${{ github.repository == 'wcandillon/react-native-skia' }} + needs: prepare-release + runs-on: [self-hosted, macOS] + strategy: + max-parallel: 1 + matrix: + include: + - target: "apple-ios" + artifact_name: "skia-graphite-apple-ios-xcframeworks" + package_root: "./packages/skia/libs/ios" + - target: "apple-macos" + artifact_name: "skia-graphite-apple-macos-xcframeworks" + package_root: "./packages/skia/libs/macos" + # Note: No apple-tvos (not supported with Graphite) + # Note: No apple-maccatalyst (not available with Graphite) + - target: "android-arm" + artifact_name: "skia-graphite-android-arm" + package_root: "./externals/skia/out/android/arm" + - target: "android-arm64" + artifact_name: "skia-graphite-android-arm-64" + package_root: "./externals/skia/out/android/arm64" + - target: "android-x86" + artifact_name: "skia-graphite-android-arm-x86" + package_root: "./externals/skia/out/android/x86" + - target: "android-x64" + artifact_name: "skia-graphite-android-arm-x64" + package_root: "./externals/skia/out/android/x64" + fail-fast: false + steps: + - name: checkout + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v5.0.0 + with: + submodules: recursive + + - name: Checkout custom Skia branch + if: ${{ github.event.inputs.skia_branch != '' }} + working-directory: externals/skia + run: | + echo "Checking out Skia branch: ${{ github.event.inputs.skia_branch }}" + git fetch origin ${{ github.event.inputs.skia_branch }} + git checkout FETCH_HEAD + echo "Current Skia commit: $(git rev-parse HEAD)" + + - name: Setup Android NDK + id: setup-ndk + if: startsWith(matrix.target, 'android') + uses: nttld/setup-ndk@ed92fe6cadad69be94a966a7ee3271275e62f779 # v1.6.0 + with: + ndk-version: r27d + + - name: Set ANDROID_NDK + if: startsWith(matrix.target, 'android') + run: echo "ANDROID_NDK=$ANDROID_HOME/ndk-bundle" >> $GITHUB_ENV + + - name: Setup Ninja + uses: seanmiddleditch/gha-setup-ninja@7e868db0f3406270dd46e1dac26c65f621456723 # master + + - name: Setup + uses: ./.github/actions/setup + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + + - name: Update depot_tools + working-directory: ./externals/depot_tools + run: ./update_depot_tools + + - name: Apply Dawn patch + run: | + DAWN_GNI_FILE="./externals/skia/build_overrides/dawn.gni" + if [ -f "$DAWN_GNI_FILE" ]; then + echo "Applying patch to dawn.gni..." + # Remove the specified lines from the end of the file + sed -i.bak '/# PartitionAlloc is an optional dependency:/,$d' "$DAWN_GNI_FILE" + echo "Patch applied successfully." + echo "Modified dawn.gni content:" + cat "$DAWN_GNI_FILE" + else + echo "Warning: dawn.gni file not found at $DAWN_GNI_FILE" + fi + + - name: Build Skia with Graphite - ${{ matrix.target }} + working-directory: ./packages/skia + env: + ANDROID_NDK: ${{ steps.setup-ndk.outputs.ndk-path }} + GIT_SYNC_DEPS_SKIP_EMSDK: 'true' + ZERO_AR_DATE: 1 + SK_GRAPHITE: 1 + run: yarn build-skia ${{ matrix.target }} + + - name: Package binaries - ${{ matrix.artifact_name }} + id: package + run: | + set -eo pipefail + ROOT="${{ matrix.package_root }}" + BASENAME="$(basename "$ROOT")" + PARENT="$(dirname "$ROOT")" + TAG="${DISPATCH_TAG}" + if [ -z "$TAG" ]; then + TAG="${SKIA_BRANCH_SLUG}" + fi + if [ -z "$TAG" ]; then + TAG="${GITHUB_REF_NAME:-}" + fi + if [ -z "$TAG" ]; then + TAG="${GITHUB_SHA}" + fi + ARCHIVE="${{ matrix.artifact_name }}-${TAG}.tar.gz" + mkdir -p release-assets + tar -czf "release-assets/$ARCHIVE" -C "$PARENT" "$BASENAME" + echo "asset_path=release-assets/$ARCHIVE" >> "$GITHUB_OUTPUT" + echo "asset_name=$ARCHIVE" >> "$GITHUB_OUTPUT" + shell: bash + env: + DISPATCH_TAG: ${{ needs.prepare-release.outputs.tag_name }} + SKIA_BRANCH_SLUG: ${{ needs.prepare-release.outputs.skia_branch_slug }} + + - name: Upload binaries to GitHub release - ${{ matrix.artifact_name }} + if: ${{ github.event.inputs.dry_run != 'true' }} + uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ needs.prepare-release.outputs.tag_name }} + files: ${{ steps.package.outputs.asset_path }} + prerelease: true + + - name: Upload binaries as workflow artifact (dry run) - ${{ matrix.artifact_name }} + if: ${{ github.event.inputs.dry_run == 'true' }} + uses: actions/upload-artifact@v7 + with: + name: ${{ matrix.artifact_name }} + path: ${{ steps.package.outputs.asset_path }} + retention-days: 7 + + - name: Package Graphite Headers + if: matrix.target == 'apple-ios' + id: package_headers + run: | + set -eo pipefail + TAG="${DISPATCH_TAG}" + if [ -z "$TAG" ]; then + TAG="${SKIA_BRANCH_SLUG}" + fi + if [ -z "$TAG" ]; then + TAG="${GITHUB_REF_NAME:-}" + fi + if [ -z "$TAG" ]; then + TAG="${GITHUB_SHA}" + fi + ARCHIVE="skia-graphite-headers-${TAG}.tar.gz" + mkdir -p release-assets + tar -czf "release-assets/$ARCHIVE" \ + ./packages/skia/cpp/dawn/include \ + ./packages/skia/cpp/skia/src/gpu/graphite + echo "asset_path=release-assets/$ARCHIVE" >> "$GITHUB_OUTPUT" + echo "asset_name=$ARCHIVE" >> "$GITHUB_OUTPUT" + shell: bash + env: + DISPATCH_TAG: ${{ needs.prepare-release.outputs.tag_name }} + SKIA_BRANCH_SLUG: ${{ needs.prepare-release.outputs.skia_branch_slug }} + + - name: Upload Graphite Headers to GitHub release + if: ${{ matrix.target == 'apple-ios' && github.event.inputs.dry_run != 'true' }} + uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ needs.prepare-release.outputs.tag_name }} + files: ${{ steps.package_headers.outputs.asset_path }} + prerelease: true + + - name: Upload Graphite Headers as workflow artifact (dry run) + if: ${{ matrix.target == 'apple-ios' && github.event.inputs.dry_run == 'true' }} + uses: actions/upload-artifact@v7 + with: + name: skia-graphite-headers + path: ${{ steps.package_headers.outputs.asset_path }} + retention-days: 7 diff --git a/.github/workflows/wcandillon-build-skia.yml b/.github/workflows/wcandillon-build-skia.yml new file mode 100644 index 0000000000..4291236c5f --- /dev/null +++ b/.github/workflows/wcandillon-build-skia.yml @@ -0,0 +1,188 @@ +# OWNER: wcandillon/react-native-skia +# RUNNER: self-hosted macOS. Shares the `wcandillon-self-hosted-macos-build` +# concurrency group with wcandillon-build-skia-graphite so the two never +# build in parallel on the single runner. The build matrix runs one target +# at a time (max-parallel: 1) for the same reason. +name: wcandillon - Build SKIA +on: + workflow_dispatch: + inputs: + tag_suffix: + description: 'Optional suffix to append to tag (e.g., "a" for skia-m144a)' + required: false + default: '' + dry_run: + description: 'Test build only - skip GitHub release creation and upload' + required: false + type: boolean + default: false + +concurrency: + group: wcandillon-self-hosted-macos-build + cancel-in-progress: false + +jobs: + prepare-release: + if: ${{ github.repository == 'wcandillon/react-native-skia' }} + runs-on: ubuntu-latest + outputs: + skia_branch: ${{ steps.skia_meta.outputs.branch }} + skia_branch_slug: ${{ steps.skia_meta.outputs.branch_slug }} + tag_name: ${{ steps.release_meta.outputs.tag_name }} + release_name: ${{ steps.release_meta.outputs.release_name }} + steps: + - name: Checkout repository + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v5.0.0 + with: + submodules: recursive + fetch-depth: 0 + - name: Determine Skia branch + id: skia_meta + run: | + set -eo pipefail + if [ ! -d "externals/skia" ]; then + echo "::error::externals/skia not found." + exit 1 + fi + branch=$(git -C externals/skia branch --show-current || true) + if [ -z "$branch" ]; then + branch=$(git -C externals/skia describe --all --exact-match 2>/dev/null || true) + fi + if [ -z "$branch" ]; then + branch=$(git -C externals/skia rev-parse --short HEAD) + fi + # Strip remotes/origin/chrome/ prefix if present + branch=${branch#remotes/origin/chrome/} + slug=${branch//\//-} + echo "branch=$branch" >> "$GITHUB_OUTPUT" + echo "branch_slug=$slug" >> "$GITHUB_OUTPUT" + - name: Compute release metadata + id: release_meta + run: | + tag="skia-${SKIA_BRANCH_SLUG}${TAG_SUFFIX}" + echo "tag_name=$tag" >> "$GITHUB_OUTPUT" + echo "release_name=Skia ${SKIA_BRANCH}${TAG_SUFFIX}" >> "$GITHUB_OUTPUT" + env: + SKIA_BRANCH: ${{ steps.skia_meta.outputs.branch }} + SKIA_BRANCH_SLUG: ${{ steps.skia_meta.outputs.branch_slug }} + TAG_SUFFIX: ${{ github.event.inputs.tag_suffix }} + - name: Create GitHub release + if: ${{ github.event.inputs.dry_run != 'true' }} + id: create_release + uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ steps.release_meta.outputs.tag_name }} + name: ${{ steps.release_meta.outputs.release_name }} + body: "Skia prebuilt binaries for version ${{ steps.skia_meta.outputs.branch }}" + draft: false + prerelease: true + generate_release_notes: false + build: + if: ${{ github.repository == 'wcandillon/react-native-skia' }} + needs: prepare-release + runs-on: [self-hosted, macOS] + strategy: + max-parallel: 1 + matrix: + include: + - target: "apple-ios" + artifact_name: "skia-apple-ios-xcframeworks" + package_root: "./packages/skia/libs/ios" + - target: "apple-tvos" + artifact_name: "skia-apple-tvos-xcframeworks" + package_root: "./packages/skia/libs/tvos" + - target: "apple-macos" + artifact_name: "skia-apple-macos-xcframeworks" + package_root: "./packages/skia/libs/macos" + - target: "android-arm" + artifact_name: "skia-android-arm" + package_root: "./packages/skia/libs/android/armeabi-v7a" + - target: "android-arm64" + artifact_name: "skia-android-arm-64" + package_root: "./packages/skia/libs/android/arm64-v8a" + - target: "android-x86" + artifact_name: "skia-android-arm-x86" + package_root: "./packages/skia/libs/android/x86" + - target: "android-x64" + artifact_name: "skia-android-arm-x64" + package_root: "./packages/skia/libs/android/x86_64" + fail-fast: false + steps: + - name: checkout + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v5.0.0 + with: + submodules: recursive + + - name: Setup Android NDK + id: setup-ndk + if: startsWith(matrix.target, 'android') + uses: nttld/setup-ndk@ed92fe6cadad69be94a966a7ee3271275e62f779 # v1.6.0 + with: + ndk-version: r27c + + - name: Setup Ninja + uses: seanmiddleditch/gha-setup-ninja@7e868db0f3406270dd46e1dac26c65f621456723 # master + + - name: Setup + uses: ./.github/actions/setup + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + + - name: Update depot_tools + working-directory: ./externals/depot_tools + run: ./update_depot_tools + + - name: Build Skia - ${{ matrix.target }} + working-directory: ./packages/skia + env: + ANDROID_NDK: ${{ steps.setup-ndk.outputs.ndk-path }} + GIT_SYNC_DEPS_SKIP_EMSDK: 'true' + ZERO_AR_DATE: 1 + run: yarn build-skia ${{ matrix.target }} + + - name: Package binaries - ${{ matrix.artifact_name }} + id: package + run: | + set -eo pipefail + ROOT="${{ matrix.package_root }}" + BASENAME="$(basename "$ROOT")" + PARENT="$(dirname "$ROOT")" + TAG="${DISPATCH_TAG}" + if [ -z "$TAG" ]; then + TAG="${SKIA_BRANCH_SLUG}" + fi + if [ -z "$TAG" ]; then + TAG="${GITHUB_REF_NAME:-}" + fi + if [ -z "$TAG" ]; then + TAG="${GITHUB_SHA}" + fi + ARCHIVE="${{ matrix.artifact_name }}-${TAG}.tar.gz" + mkdir -p release-assets + tar -czf "release-assets/$ARCHIVE" -C "$PARENT" "$BASENAME" + echo "asset_path=release-assets/$ARCHIVE" >> "$GITHUB_OUTPUT" + echo "asset_name=$ARCHIVE" >> "$GITHUB_OUTPUT" + shell: bash + env: + DISPATCH_TAG: ${{ needs.prepare-release.outputs.tag_name }} + SKIA_BRANCH_SLUG: ${{ needs.prepare-release.outputs.skia_branch_slug }} + + - name: Upload binaries to GitHub release - ${{ matrix.artifact_name }} + if: ${{ github.event.inputs.dry_run != 'true' }} + uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ needs.prepare-release.outputs.tag_name }} + files: ${{ steps.package.outputs.asset_path }} + prerelease: true + + - name: Upload binaries as workflow artifact (dry run) - ${{ matrix.artifact_name }} + if: ${{ github.event.inputs.dry_run == 'true' }} + uses: actions/upload-artifact@v7 + with: + name: ${{ matrix.artifact_name }} + path: ${{ steps.package.outputs.asset_path }} + retention-days: 7 diff --git a/.github/workflows/wcandillon-ci-graphite.yml b/.github/workflows/wcandillon-ci-graphite.yml new file mode 100644 index 0000000000..d22e55ada1 --- /dev/null +++ b/.github/workflows/wcandillon-ci-graphite.yml @@ -0,0 +1,253 @@ +# OWNER: wcandillon/react-native-skia +# RUNNER: the iOS Graphite job runs on the self-hosted macOS runner; the +# Android Graphite jobs stay on GitHub-hosted Ubuntu runners. +name: wcandillon - CI Graphite +on: + push: + branches: + - main + - next + pull_request: + branches: + - main + - next + merge_group: + types: + - checks_requested + +concurrency: + group: wcandillon-${{ github.ref }}-graphite + cancel-in-progress: true + +jobs: + build-android-graphite: + if: ${{ github.repository == 'wcandillon/react-native-skia' }} + runs-on: ubuntu-latest + env: + TURBO_CACHE_DIR: .turbo/android + steps: + - name: Checkout + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v5.0.0 + with: + submodules: recursive + + - name: Setup + uses: ./.github/actions/setup + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + graphite: true + + - name: Free disk space + uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1 + with: + tool-cache: false + android: false + dotnet: true + haskell: true + large-packages: true + docker-images: true + swap-storage: true + + - name: Cache turborepo for Android + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: ${{ env.TURBO_CACHE_DIR }} + key: ${{ runner.os }}-turborepo-android-${{ hashFiles('yarn.lock') }} + restore-keys: | + ${{ runner.os }}-turborepo-android + + - name: Install JDK + uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0 + with: + distribution: 'zulu' + java-version: '17' + + - name: Install NDK + uses: nttld/setup-ndk@ed92fe6cadad69be94a966a7ee3271275e62f779 # v1.6.0 + id: setup-ndk + with: + ndk-version: r27d + + - name: Set ANDROID_NDK + run: echo "ANDROID_NDK=$ANDROID_HOME/ndk-bundle" >> $GITHUB_ENV + + - name: Finalize Android SDK + run: | + /bin/bash -c "yes | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --licenses > /dev/null" + + - name: Install Android SDK + run: | + echo "sdk.dir=$ANDROID_HOME" > $GITHUB_WORKSPACE/apps/example/android/local.properties + + - name: Cache Gradle + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: | + ~/.gradle/wrapper + ~/.gradle/caches + key: ${{ runner.os }}-gradle-${{ hashFiles('./apps/example/android/gradle/wrapper/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: Build example for Android + env: + JAVA_OPTS: "-XX:MaxHeapSize=6g" + run: | + yarn turbo run build:android --concurrency 1 + + - name: Cache apk + uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + env: + cache-name: cache-apk + with: + path: apps/example/android/app/build/outputs/apk/debug/app-debug.apk + key: apk-graphite-${{ github.sha }} + + - name: Upload Android APK artifact + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v6.0.0 + with: + name: android-example-apk-graphite + path: apps/example/android/app/build/outputs/apk/debug/app-debug.apk + + test-android-graphite: + if: ${{ github.repository == 'wcandillon/react-native-skia' }} + needs: build-android-graphite + runs-on: ubuntu-latest + timeout-minutes: 60 + env: + TURBO_CACHE_DIR: .turbo/android + steps: + - name: checkout + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v5.0.0 + with: + submodules: recursive + + - name: Setup + uses: ./.github/actions/setup + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + graphite: true + + - name: Setup Android SDK + uses: android-actions/setup-android@40fd30fb8d7440372e1316f5d1809ec01dcd3699 # v4.0.1 + + - name: Install Android SDK tools + run: | + echo "ANDROID_HOME=$ANDROID_HOME" >> $GITHUB_ENV + echo "$ANDROID_HOME/cmdline-tools/latest/bin" >> $GITHUB_PATH + echo "$ANDROID_HOME/platform-tools" >> $GITHUB_PATH + echo "$ANDROID_HOME/emulator" >> $GITHUB_PATH + + - name: Enable KVM group perms + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + + - name: Restore APK + id: cache-apk + uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae + with: + path: apps/example/android/app/build/outputs/apk/debug/app-debug.apk + key: apk-graphite-${{ github.sha }} + + - name: Start Package Manager + working-directory: apps/example/ + run: E2E=true yarn start & + + - name: Run Android Emulator Tests + uses: reactivecircus/android-emulator-runner@e89f39f1abbbd05b1113a29cf4db69e7540cae5a # v2.37.0 + with: + api-level: 30 + arch: x86_64 + profile: Nexus 5X + force-avd-creation: true + emulator-options: -no-snapshot-save -no-window -gpu swangle_indirect -noaudio -no-boot-anim + disable-animations: true + script: | + # Wait for Metro bundler + sleep 10 + + # Install and launch app + adb install -r apps/example/android/app/build/outputs/apk/debug/app-debug.apk + adb shell monkey -p com.microsoft.reacttestapp 1 + + # Run tests + # On Graphite we run the Path render tests plus the WebGPU specs. + # ArrayBuffer, ComputeShader, ErrorScope, ImageBitmapBounds and + # DawnToggles are pure surface.eval round-trips that assert on + # returned values. The external-texture specs (ExternalTexture, + # ImportExternalTexture, SharedTextureMemory) additionally exercise + # NativeBuffer import and GPU read-back; they self-validate via + # copyTextureToBuffer (not Skia image readPixels) and skip + # themselves where NativeBuffer/createImageBitmap are unavailable. + cd packages/skia && CI=true yarn test -i "e2e/(Path|ArrayBuffer|ComputeShader|ErrorScope|ImageBitmapBounds|DawnToggles|ExternalTexture|ImportExternalTexture|SharedTextureMemory)" --testPathIgnorePatterns Paragraphs + + - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v6.0.0 + if: failure() + with: + path: packages/skia/src/__tests__/snapshots/ + name: tests-snapshots-screenshots + + - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v6.0.0 + if: failure() + with: + path: apps/docs/static/ + name: tests-docs-screenshots + + build-test-ios-graphite: + if: ${{ github.repository == 'wcandillon/react-native-skia' }} + runs-on: [self-hosted, macOS] + env: + TURBO_CACHE_DIR: .turbo/ios + steps: + - name: Checkout + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v5.0.0 + with: + submodules: recursive + + - name: Setup + uses: ./.github/actions/setup + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + graphite: true + + - name: Cache turborepo for iOS + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: ${{ env.TURBO_CACHE_DIR }} + key: ${{ runner.os }}-turborepo-ios-${{ hashFiles('yarn.lock') }} + restore-keys: | + ${{ runner.os }}-turborepo-ios- + + - name: Check turborepo cache for iOS + run: | + TURBO_CACHE_STATUS=$(node -p "($(yarn turbo run build:ios --cache-dir=\"${{ env.TURBO_CACHE_DIR }}\" --dry=json)).tasks.find(t => t.task === 'build:ios').cache.status") + + if [[ $TURBO_CACHE_STATUS == "HIT" ]]; then + echo "turbo_cache_hit=1" >> $GITHUB_ENV + fi + + - name: Install CocoaPods + working-directory: apps/example/ios + run: pod install + + - name: Start Package Manager + working-directory: apps/example + run: E2E=true yarn start & + + - name: Build example for iOS + working-directory: apps/example + run: yarn ios --simulator 'iPhone 16 Pro' + + - name: Run e2e tests + working-directory: packages/skia + # On Graphite we run the Paths render test plus the WebGPU specs. + # ArrayBuffer, ComputeShader, ErrorScope, ImageBitmapBounds and + # DawnToggles are pure surface.eval round-trips that assert on returned + # values. The external-texture specs (ExternalTexture, + # ImportExternalTexture, SharedTextureMemory) additionally exercise + # NativeBuffer import and GPU read-back; they self-validate via + # copyTextureToBuffer (not Skia image readPixels) and skip themselves + # where NativeBuffer/createImageBitmap are unavailable. + run: CI=true E2E=true yarn test -i "e2e/(Paths|ArrayBuffer|ComputeShader|ErrorScope|ImageBitmapBounds|DawnToggles|ExternalTexture|ImportExternalTexture|SharedTextureMemory)" diff --git a/.github/workflows/wcandillon-ci.yml b/.github/workflows/wcandillon-ci.yml new file mode 100644 index 0000000000..6300bcb7c8 --- /dev/null +++ b/.github/workflows/wcandillon-ci.yml @@ -0,0 +1,346 @@ +# OWNER: wcandillon/react-native-skia +# RUNNER: macOS jobs (test, build-test-ios, build-test-macos) run on the +# self-hosted macOS runner; they serialize naturally since there is a single +# runner. Ubuntu jobs stay on GitHub-hosted runners. +# NOTE: Windows build and package e2e tests are intentionally NOT part of this +# automatic CI - they are dispatched manually (see dedicated workflows). +name: wcandillon - CI +on: + push: + branches: + - main + - next + pull_request: + branches: + - main + - next + merge_group: + types: + - checks_requested + +concurrency: + group: wcandillon-${{ github.ref }} + cancel-in-progress: true + +jobs: + lint: + if: ${{ github.repository == 'wcandillon/react-native-skia' }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v5.0.0 + with: + submodules: recursive + + - name: Setup + uses: ./.github/actions/setup + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + - run: pip install cpplint + - run: cpplint --linelength=230 --filter=-legal/copyright,-whitespace/indent,-whitespace/comments,-whitespace/ending_newline,-build/include_order,-runtime/references,-readability/todo,-whitespace/blank_line,-whitespace/todo,-runtime/int,-build/c++11,-whitespace/parens --exclude=package/cpp/skia --exclude=package/apple --exclude=package/android/build --exclude=package/node_modules --recursive package + + - name: Install clang-format + run: sudo apt-get update && sudo apt-get install -y clang-format-18 + + - name: Lint files + run: yarn lint + + - name: Typecheck files + run: yarn tsc + + test: + if: ${{ github.repository == 'wcandillon/react-native-skia' }} + runs-on: [self-hosted, macOS] + steps: + - name: Checkout + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v5.0.0 + with: + submodules: recursive + + - name: Setup + uses: ./.github/actions/setup + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + + - name: Test + run: yarn test + + build-library: + if: ${{ github.repository == 'wcandillon/react-native-skia' }} + runs-on: ubuntu-latest + outputs: + run_id: ${{ github.run_id }} + steps: + - name: Checkout + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v5.0.0 + with: + submodules: recursive + + - name: Setup + uses: ./.github/actions/setup + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + + - name: Build package + run: yarn build + + - name: Pack package + working-directory: packages/skia + run: yarn pack + + - name: Upload package artifact + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v6.0.0 + with: + name: package-tgz + path: packages/skia/package.tgz + + - name: Build dry run release + working-directory: packages/skia + run: | + npx semantic-release --dry-run + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + YARN_NPM_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + build-android: + if: ${{ github.repository == 'wcandillon/react-native-skia' }} + runs-on: ubuntu-latest + env: + TURBO_CACHE_DIR: .turbo/android + steps: + - name: Checkout + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v5.0.0 + with: + submodules: recursive + + - name: Setup + uses: ./.github/actions/setup + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + + - name: Free disk space + run: | + echo "Disk usage before cleanup:" + df -h + sudo rm -rf /usr/share/dotnet + sudo rm -rf /opt/ghc + sudo rm -rf /opt/hostedtoolcache/CodeQL + sudo docker image prune -af || true + sudo apt-get clean + echo "Disk usage after cleanup:" + df -h + + - name: Cache turborepo for Android + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: ${{ env.TURBO_CACHE_DIR }} + key: ${{ runner.os }}-turborepo-android-${{ hashFiles('yarn.lock') }} + restore-keys: | + ${{ runner.os }}-turborepo-android + + - name: Install JDK + uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0 + with: + distribution: 'zulu' + java-version: '17' + + - name: Install NDK + uses: nttld/setup-ndk@ed92fe6cadad69be94a966a7ee3271275e62f779 # v1.6.0 + id: setup-ndk + with: + ndk-version: r27d + + - name: Set ANDROID_NDK + run: echo "ANDROID_NDK=$ANDROID_HOME/ndk-bundle" >> $GITHUB_ENV + + - name: Finalize Android SDK + run: | + /bin/bash -c "yes | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --licenses > /dev/null" + + - name: Install Android SDK + run: | + echo "sdk.dir=$ANDROID_HOME" > $GITHUB_WORKSPACE/apps/example/android/local.properties + + - name: Cache Gradle + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: | + ~/.gradle/wrapper + ~/.gradle/caches + key: ${{ runner.os }}-gradle-${{ hashFiles('./apps/example/android/gradle/wrapper/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: Build example for Android + env: + JAVA_OPTS: "-XX:MaxHeapSize=6g" + run: | + yarn turbo run build:android --concurrency 1 + + - name: Cache apk + uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + env: + cache-name: cache-apk + with: + path: apps/example/android/app/build/outputs/apk/debug/app-debug.apk + key: apk-${{ github.sha }} + + - name: Upload Android APK artifact + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v6.0.0 + with: + name: android-example-apk + path: apps/example/android/app/build/outputs/apk/debug/app-debug.apk + + test-android: + if: ${{ github.repository == 'wcandillon/react-native-skia' }} + needs: build-android + runs-on: ubuntu-latest + timeout-minutes: 60 + env: + TURBO_CACHE_DIR: .turbo/android + steps: + - name: checkout + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v5.0.0 + with: + submodules: recursive + + - name: Setup + uses: ./.github/actions/setup + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup Android SDK + uses: android-actions/setup-android@40fd30fb8d7440372e1316f5d1809ec01dcd3699 # v4.0.1 + + - name: Install Android SDK tools + run: | + echo "ANDROID_HOME=$ANDROID_HOME" >> $GITHUB_ENV + echo "$ANDROID_HOME/cmdline-tools/latest/bin" >> $GITHUB_PATH + echo "$ANDROID_HOME/platform-tools" >> $GITHUB_PATH + echo "$ANDROID_HOME/emulator" >> $GITHUB_PATH + + - name: Enable KVM group perms + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + + - name: Restore APK + id: cache-apk + uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae + with: + path: apps/example/android/app/build/outputs/apk/debug/app-debug.apk + key: apk-${{ github.sha }} + + - name: Start Package Manager + working-directory: apps/example/ + run: E2E=true yarn start & + + - name: Run Android Emulator Tests + uses: reactivecircus/android-emulator-runner@e89f39f1abbbd05b1113a29cf4db69e7540cae5a # v2.37.0 + with: + api-level: 30 + arch: x86_64 + profile: Nexus 5X + force-avd-creation: true + emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim + disable-animations: true + script: | + # Wait for Metro bundler + sleep 10 + + # Install and launch app + adb install -r apps/example/android/app/build/outputs/apk/debug/app-debug.apk + adb shell monkey -p com.microsoft.reacttestapp 1 + + # Run tests + cd packages/skia && CI=true yarn e2e --testPathIgnorePatterns Paragraphs + + - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v6.0.0 + if: failure() + with: + path: packages/skia/src/__tests__/snapshots/ + name: tests-snapshots-screenshots + + - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v6.0.0 + if: failure() + with: + path: apps/docs/static/ + name: tests-docs-screenshots + + build-test-ios: + if: ${{ github.repository == 'wcandillon/react-native-skia' }} + runs-on: [self-hosted, macOS] + env: + TURBO_CACHE_DIR: .turbo/ios + steps: + - name: Checkout + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v5.0.0 + with: + submodules: recursive + + - name: Setup + uses: ./.github/actions/setup + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + + - name: Cache turborepo for iOS + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: ${{ env.TURBO_CACHE_DIR }} + key: ${{ runner.os }}-turborepo-ios-${{ hashFiles('yarn.lock') }} + restore-keys: | + ${{ runner.os }}-turborepo-ios- + + - name: Check turborepo cache for iOS + run: | + TURBO_CACHE_STATUS=$(node -p "($(yarn turbo run build:ios --cache-dir="${{ env.TURBO_CACHE_DIR }}" --dry=json)).tasks.find(t => t.task === 'build:ios').cache.status") + + if [[ $TURBO_CACHE_STATUS == "HIT" ]]; then + echo "turbo_cache_hit=1" >> $GITHUB_ENV + fi + + - name: Install CocoaPods + working-directory: apps/example/ios + run: pod install + + - name: Start Package Manager + working-directory: apps/example + run: E2E=true yarn start & + + - name: Build example for iOS + working-directory: apps/example + run: yarn ios --simulator 'iPhone 16 Pro' + + - name: Run e2e tests + working-directory: packages/skia + run: CI=true yarn e2e + + build-test-macos: + if: ${{ github.repository == 'wcandillon/react-native-skia' }} + runs-on: [self-hosted, macOS] + env: + TURBO_CACHE_DIR: .turbo/macos + steps: + - name: Checkout + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v5.0.0 + with: + submodules: recursive + + - name: Setup + uses: ./.github/actions/setup + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + + - name: Cache turborepo for macOS + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: ${{ env.TURBO_CACHE_DIR }} + key: ${{ runner.os }}-turborepo-macos-${{ hashFiles('yarn.lock') }} + restore-keys: | + ${{ runner.os }}-turborepo-macos- + + - name: Build example for macOS + run: yarn turbo run build:macos --concurrency 1 diff --git a/.github/workflows/wcandillon-test-package.yml b/.github/workflows/wcandillon-test-package.yml new file mode 100644 index 0000000000..aaf4b82e74 --- /dev/null +++ b/.github/workflows/wcandillon-test-package.yml @@ -0,0 +1,92 @@ +# OWNER: wcandillon/react-native-skia +# Manual-dispatch package e2e test: builds the package from the current branch, +# then installs the resulting .tgz into a fresh Expo app and runs it on +# iOS / Android / Web. Mirrors the test-package-e2e jobs that used to live in +# CI. The macOS test jobs run on GitHub-hosted runners (see +# wcandillon-test-skia-package.yml). +name: wcandillon - Test Package +on: + workflow_dispatch: + inputs: + test_ios: + description: 'Run iOS tests' + required: false + type: boolean + default: true + test_android: + description: 'Run Android tests' + required: false + type: boolean + default: true + test_web: + description: 'Run Web tests' + required: false + type: boolean + default: true + ios_use_frameworks: + description: 'iOS framework linkage' + required: false + type: choice + options: + - dynamic + - static + default: dynamic + simulator_device: + description: 'iOS Simulator device' + required: false + default: 'iPhone 16 Pro' + type: string + android_api_level: + description: 'Android API level' + required: false + default: '30' + type: string + android_device: + description: 'Android device profile' + required: false + default: 'Nexus 5X' + type: string + +jobs: + build-library: + if: ${{ github.repository == 'wcandillon/react-native-skia' }} + runs-on: ubuntu-latest + outputs: + run_id: ${{ github.run_id }} + steps: + - name: Checkout + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v5.0.0 + with: + submodules: recursive + + - name: Setup + uses: ./.github/actions/setup + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + + - name: Build package + run: yarn build + + - name: Pack package + working-directory: packages/skia + run: yarn pack + + - name: Upload package artifact + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v6.0.0 + with: + name: package-tgz + path: packages/skia/package.tgz + + test-package-e2e: + if: ${{ github.repository == 'wcandillon/react-native-skia' }} + needs: build-library + uses: ./.github/workflows/wcandillon-test-skia-package.yml + with: + skia_version: artifact:${{ needs.build-library.outputs.run_id }} + test_ios: ${{ inputs.test_ios }} + test_android: ${{ inputs.test_android }} + test_web: ${{ inputs.test_web }} + ios_use_frameworks: ${{ inputs.ios_use_frameworks }} + simulator_device: ${{ inputs.simulator_device }} + android_api_level: ${{ inputs.android_api_level }} + android_device: ${{ inputs.android_device }} diff --git a/.github/workflows/wcandillon-test-skia-package.yml b/.github/workflows/wcandillon-test-skia-package.yml new file mode 100644 index 0000000000..c844f6dbbd --- /dev/null +++ b/.github/workflows/wcandillon-test-skia-package.yml @@ -0,0 +1,500 @@ +# OWNER: wcandillon/react-native-skia +# Fork copy of test-skia-package.yml. Only difference: the package artifact is +# downloaded from the calling repository (github.repository) instead of being +# hardcoded to Shopify/react-native-skia. Jobs run on GitHub-hosted macOS. +name: wcandillon - Test Skia Package (Reusable) + +on: + workflow_call: + inputs: + skia_version: + description: 'Skia version to test (version number, "latest", URL to .tgz, or "artifact:RUN_ID")' + required: true + type: string + test_ios: + description: 'Run iOS tests' + required: false + type: boolean + default: true + test_android: + description: 'Run Android tests' + required: false + type: boolean + default: true + simulator_device: + description: 'iOS Simulator device' + required: false + default: 'iPhone 16 Pro' + type: string + android_api_level: + description: 'Android API level' + required: false + default: '30' + type: string + android_device: + description: 'Android device profile' + required: false + default: 'Nexus 5X' + type: string + test_web: + description: 'Run Web tests' + required: false + type: boolean + default: true + ios_use_frameworks: + description: 'iOS framework linkage: "dynamic" (default) or "static"' + required: false + type: string + default: 'dynamic' + +jobs: + test-skia-ios: + if: inputs.test_ios + runs-on: macos-latest-xlarge + timeout-minutes: 30 + + steps: + - name: Checkout repository + uses: actions/checkout@v6.0.3 + + - name: Setup Node.js + uses: actions/setup-node@v6.4.0 + with: + node-version: '20' + + - name: Setup Yarn + run: | + corepack enable + yarn --version + + - name: Select latest Xcode + run: | + LATEST_XCODE=$(ls -d /Applications/Xcode_*.app | sort -V | tail -n 1) + echo "Selecting: $LATEST_XCODE" + sudo xcode-select -s "$LATEST_XCODE/Contents/Developer" + xcodebuild -version + swift --version + + - name: List available simulators + run: xcrun simctl list devices + + - name: Create test directory + run: | + mkdir -p /Users/runner/skia-test-app + cd /Users/runner/skia-test-app + pwd + + - name: Create Expo app with Skia template + uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4.0.0 + with: + timeout_minutes: 10 + max_attempts: 3 + retry_wait_seconds: 120 + command: | + cd /Users/runner/skia-test-app + echo "Creating Expo app with Skia template..." + yarn create expo-app my-app -e with-skia + + - name: Download artifact package if specified + if: startsWith(inputs.skia_version, 'artifact:') + run: | + ARTIFACT_ID=$(echo "${{ inputs.skia_version }}" | sed 's/artifact://') + echo "Downloading artifact ID: $ARTIFACT_ID" + gh run download --repo ${{ github.repository }} $ARTIFACT_ID -n package-tgz -D /tmp + env: + GH_TOKEN: ${{ github.token }} + + - name: Install React Native Skia + working-directory: /Users/runner/skia-test-app/my-app + run: | + VERSION="${{ inputs.skia_version }}" + if [ "$VERSION" == "latest" ]; then + echo "Installing @shopify/react-native-skia@latest..." + yarn add @shopify/react-native-skia@latest + elif [[ "$VERSION" == artifact:* ]]; then + echo "Installing @shopify/react-native-skia from downloaded artifact..." + yarn add @shopify/react-native-skia@file:/tmp/package.tgz + elif [[ "$VERSION" == http* ]]; then + echo "Installing @shopify/react-native-skia from URL: $VERSION" + yarn add @shopify/react-native-skia@"$VERSION" + else + echo "Installing @shopify/react-native-skia@$VERSION..." + yarn add @shopify/react-native-skia@"$VERSION" + fi + + - name: Show installed Skia version + working-directory: /Users/runner/skia-test-app/my-app + run: | + echo "Installed React Native Skia version:" + yarn list @shopify/react-native-skia --depth=0 + + - name: Install TypeScript dependencies + working-directory: /Users/runner/skia-test-app/my-app + run: | + echo "Installing TypeScript dependencies..." + yarn expo install typescript @types/react + + - name: Install Expo Dev Client + working-directory: /Users/runner/skia-test-app/my-app + run: | + echo "Installing expo-dev-client..." + yarn expo install expo-dev-client + + - name: Enable static frameworks (iOS) + if: inputs.ios_use_frameworks == 'static' + working-directory: /Users/runner/skia-test-app/my-app + run: | + echo "Configuring expo-build-properties for useFrameworks: 'static'..." + yarn expo install expo-build-properties + node -e " + const fs = require('fs'); + const cfg = JSON.parse(fs.readFileSync('app.json', 'utf8')); + cfg.expo.plugins = cfg.expo.plugins || []; + cfg.expo.plugins.push(['expo-build-properties', { ios: { useFrameworks: 'static' } }]); + fs.writeFileSync('app.json', JSON.stringify(cfg, null, 2)); + " + + - name: Prebuild native directories + working-directory: /Users/runner/skia-test-app/my-app + run: | + echo "Generating native iOS directory (linkage: ${{ inputs.ios_use_frameworks }})..." + yarn expo prebuild --platform ios + + - name: Install iOS dependencies + working-directory: /Users/runner/skia-test-app/my-app + run: | + echo "Installing iOS dependencies..." + cd ios + pod install --repo-update + + - name: Start Metro bundler + working-directory: /Users/runner/skia-test-app/my-app + run: | + echo "Starting Metro bundler in background..." + yarn expo start --dev-client & + sleep 10 + + - name: Build and run iOS app + working-directory: /Users/runner/skia-test-app/my-app + run: | + echo "Building and running iOS app on simulator: ${{ inputs.simulator_device }}..." + yarn expo run:ios --device "${{ inputs.simulator_device }}" + + - name: Wait for app to launch + run: | + echo "Waiting for app to launch and stabilize..." + sleep 30 + + - name: Capture simulator screenshot + if: always() + run: | + SIMULATOR_ID=$(xcrun simctl list devices | grep "${{ inputs.simulator_device }}" | grep -E 'Booted' | head -n 1 | cut -d '(' -f 2 | cut -d ')' -f 1) + if [ -n "$SIMULATOR_ID" ]; then + echo "Capturing screenshot from simulator ID: $SIMULATOR_ID" + xcrun simctl io $SIMULATOR_ID screenshot /Users/runner/skia-test-screenshot-ios.png + else + echo "No booted simulator found" + fi + + - name: Upload screenshot + if: always() + uses: actions/upload-artifact@v7 + with: + name: ios-simulator-screenshot-${{ github.run_id }} + path: /Users/runner/skia-test-screenshot-ios.png + if-no-files-found: ignore + + - name: Print test summary + if: always() + run: | + echo "================================================================" + echo "iOS Test Summary:" + echo "================================================================" + echo "✓ Created Expo app with Skia template" + echo "✓ Installed @shopify/react-native-skia@${{ inputs.skia_version }}" + echo "✓ Installed expo-dev-client" + echo "✓ Framework linkage: ${{ inputs.ios_use_frameworks }}" + echo "✓ Built and ran iOS app on ${{ inputs.simulator_device }}" + echo "================================================================" + + test-skia-android: + if: inputs.test_android + runs-on: macos-latest-large + timeout-minutes: 60 + + steps: + - name: Checkout repository + uses: actions/checkout@v6.0.3 + + - name: Setup Node.js + uses: actions/setup-node@v6.4.0 + with: + node-version: '20' + + - name: Setup Yarn + run: | + corepack enable + yarn --version + + - name: Setup Java + uses: actions/setup-java@v5 + with: + distribution: 'zulu' + java-version: '17' + + - name: Download Android system image + run: $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager "system-images;android-${{ inputs.android_api_level }};default;x86_64" + + - name: Create Android emulator + run: | + echo "Creating Android emulator..." + $ANDROID_HOME/cmdline-tools/latest/bin/avdmanager create avd -n test_avd --device '${{ inputs.android_device }}' --package "system-images;android-${{ inputs.android_api_level }};default;x86_64" --sdcard 512M --force + + - name: Create test directory + run: | + mkdir -p /Users/runner/skia-test-app + cd /Users/runner/skia-test-app + pwd + + - name: Create Expo app with Skia template + uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4.0.0 + with: + timeout_minutes: 10 + max_attempts: 3 + retry_wait_seconds: 120 + command: | + cd /Users/runner/skia-test-app + echo "Creating Expo app with Skia template..." + yarn create expo-app my-app -e with-skia + + - name: Download artifact package if specified + if: startsWith(inputs.skia_version, 'artifact:') + run: | + ARTIFACT_ID=$(echo "${{ inputs.skia_version }}" | sed 's/artifact://') + echo "Downloading artifact ID: $ARTIFACT_ID" + gh run download --repo ${{ github.repository }} $ARTIFACT_ID -n package-tgz -D /tmp + env: + GH_TOKEN: ${{ github.token }} + + - name: Install React Native Skia + working-directory: /Users/runner/skia-test-app/my-app + run: | + VERSION="${{ inputs.skia_version }}" + if [ "$VERSION" == "latest" ]; then + echo "Installing @shopify/react-native-skia@latest..." + yarn add @shopify/react-native-skia@latest + elif [[ "$VERSION" == artifact:* ]]; then + echo "Installing @shopify/react-native-skia from downloaded artifact..." + yarn add @shopify/react-native-skia@file:/tmp/package.tgz + elif [[ "$VERSION" == http* ]]; then + echo "Installing @shopify/react-native-skia from URL: $VERSION" + yarn add @shopify/react-native-skia@"$VERSION" + else + echo "Installing @shopify/react-native-skia@$VERSION..." + yarn add @shopify/react-native-skia@"$VERSION" + fi + + - name: Show installed Skia version + working-directory: /Users/runner/skia-test-app/my-app + run: | + echo "Installed React Native Skia version:" + yarn list @shopify/react-native-skia --depth=0 + + - name: Install TypeScript dependencies + working-directory: /Users/runner/skia-test-app/my-app + run: | + echo "Installing TypeScript dependencies..." + yarn expo install typescript @types/react + + - name: Install Expo Dev Client + working-directory: /Users/runner/skia-test-app/my-app + run: | + echo "Installing expo-dev-client..." + yarn expo install expo-dev-client + + - name: Prebuild native directories + working-directory: /Users/runner/skia-test-app/my-app + run: | + echo "Generating native Android directory..." + yarn expo prebuild --platform android + + - name: Start Android emulator + run: | + echo "Starting Android emulator in background..." + $ANDROID_HOME/emulator/emulator -memory 4096 -avd test_avd -wipe-data -no-window -gpu angle_indirect -no-snapshot -noaudio -no-boot-anim & + + - name: Wait for emulator to boot + run: | + echo "Waiting for Android emulator to boot..." + adb wait-for-device shell 'while [[ -z $(getprop sys.boot_completed) ]]; do sleep 1; done;' + echo "Emulator is ready" + adb devices + timeout-minutes: 10 + + - name: Start Metro bundler + working-directory: /Users/runner/skia-test-app/my-app + run: | + echo "Starting Metro bundler in background..." + yarn expo start --dev-client & + sleep 10 + + - name: Build and run Android app + working-directory: /Users/runner/skia-test-app/my-app + run: | + echo "Building and running Android app on emulator..." + yarn expo run:android + + - name: Wait for app to launch + run: | + echo "Waiting for app to launch and stabilize..." + sleep 120 + + - name: Capture emulator screenshot + if: always() + run: | + echo "Capturing screenshot from Android emulator..." + adb exec-out screencap -p > /Users/runner/skia-test-screenshot-android.png || echo "Failed to capture screenshot" + + - name: Upload screenshot + if: always() + uses: actions/upload-artifact@v7 + with: + name: android-emulator-screenshot-${{ github.run_id }} + path: /Users/runner/skia-test-screenshot-android.png + if-no-files-found: ignore + + - name: Print test summary + if: always() + run: | + echo "================================================================" + echo "Android Test Summary:" + echo "================================================================" + echo "✓ Created Expo app with Skia template" + echo "✓ Installed @shopify/react-native-skia@${{ inputs.skia_version }}" + echo "✓ Installed expo-dev-client" + echo "✓ Built and ran Android app on API level ${{ inputs.android_api_level }}" + echo "================================================================" + + test-skia-web: + if: inputs.test_web + runs-on: macos-latest-large + timeout-minutes: 30 + + steps: + - name: Checkout repository + uses: actions/checkout@v6.0.3 + + - name: Setup Node.js + uses: actions/setup-node@v6.4.0 + with: + node-version: '20' + + - name: Setup Yarn + run: | + corepack enable + yarn --version + + - name: Create test directory + run: | + mkdir -p /Users/runner/skia-test-app + cd /Users/runner/skia-test-app + pwd + + - name: Create Expo app with Skia template + uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4.0.0 + with: + timeout_minutes: 10 + max_attempts: 3 + retry_wait_seconds: 120 + command: | + cd /Users/runner/skia-test-app + echo "Creating Expo app with Skia template..." + yarn create expo-app my-app -e with-skia + + - name: Download artifact package if specified + if: startsWith(inputs.skia_version, 'artifact:') + run: | + ARTIFACT_ID=$(echo "${{ inputs.skia_version }}" | sed 's/artifact://') + echo "Downloading artifact ID: $ARTIFACT_ID" + gh run download --repo ${{ github.repository }} $ARTIFACT_ID -n package-tgz -D /tmp + env: + GH_TOKEN: ${{ github.token }} + + - name: Install React Native Skia + working-directory: /Users/runner/skia-test-app/my-app + run: | + VERSION="${{ inputs.skia_version }}" + if [ "$VERSION" == "latest" ]; then + echo "Installing @shopify/react-native-skia@latest..." + yarn add @shopify/react-native-skia@latest + elif [[ "$VERSION" == artifact:* ]]; then + echo "Installing @shopify/react-native-skia from downloaded artifact..." + yarn add @shopify/react-native-skia@file:/tmp/package.tgz + elif [[ "$VERSION" == http* ]]; then + echo "Installing @shopify/react-native-skia from URL: $VERSION" + yarn add @shopify/react-native-skia@"$VERSION" + else + echo "Installing @shopify/react-native-skia@$VERSION..." + yarn add @shopify/react-native-skia@"$VERSION" + fi + + - name: Show installed Skia version + working-directory: /Users/runner/skia-test-app/my-app + run: | + echo "Installed React Native Skia version:" + yarn list @shopify/react-native-skia --depth=0 + + - name: Install TypeScript dependencies + working-directory: /Users/runner/skia-test-app/my-app + run: | + echo "Installing TypeScript dependencies..." + yarn expo install typescript @types/react + + - name: Install repo dependencies for Playwright + run: yarn install --immutable + + - name: Install Playwright dependencies + run: | + echo "Installing Playwright..." + yarn playwright install chromium + yarn playwright install-deps chromium + + - name: Start web server + working-directory: /Users/runner/skia-test-app/my-app + run: | + echo "Starting web server in background..." + yarn expo start --web & + sleep 30 + + - name: Wait for web app to be ready + run: | + echo "Waiting for web app to be accessible..." + timeout 60 bash -c 'until curl -s http://localhost:8081 > /dev/null; do sleep 2; done' || echo "Web server may not be fully ready" + echo "Waiting additional time for app to fully render..." + sleep 30 + + - name: Take screenshot + if: always() + run: | + echo "Taking screenshot of web app..." + yarn playwright screenshot http://localhost:8081 /Users/runner/skia-test-screenshot-web.png || echo "Failed to capture screenshot" + + - name: Upload screenshot + if: always() + uses: actions/upload-artifact@v7 + with: + name: web-screenshot-${{ github.run_id }} + path: /Users/runner/skia-test-screenshot-web.png + if-no-files-found: ignore + + - name: Print test summary + if: always() + run: | + echo "================================================================" + echo "Web Test Summary:" + echo "================================================================" + echo "✓ Created Expo app with Skia template" + echo "✓ Installed @shopify/react-native-skia@${{ inputs.skia_version }}" + echo "✓ Started web server" + echo "✓ Web app accessible at http://localhost:8081" + echo "================================================================" diff --git a/.github/workflows/wcandillon-test-windows.yml b/.github/workflows/wcandillon-test-windows.yml new file mode 100644 index 0000000000..ef9e44e0bd --- /dev/null +++ b/.github/workflows/wcandillon-test-windows.yml @@ -0,0 +1,24 @@ +# OWNER: wcandillon/react-native-skia +# Manual-dispatch Windows build check. Mirrors the build-windows-library job +# that used to live in CI. Runs on a GitHub-hosted windows-latest runner. +name: wcandillon - Test Windows +on: + workflow_dispatch: + +jobs: + build-windows-library: + if: ${{ github.repository == 'wcandillon/react-native-skia' }} + runs-on: windows-latest + steps: + - name: Checkout + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v5.0.0 + with: + submodules: recursive + + - name: Setup + uses: ./.github/actions/setup + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + + - name: Build package + run: yarn build diff --git a/.gitmodules b/.gitmodules index 561c25ba38..7ce029cf77 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,7 +1,7 @@ [submodule "externals/skia"] path = externals/skia url = https://chromium.googlesource.com/skia/ - branch = chrome/m147 + branch = chrome/m150 [submodule "externals/depot_tools"] path = externals/depot_tools url = https://chromium.googlesource.com/chromium/tools/depot_tools.git diff --git a/README.md b/README.md index 511999b489..b8bc67be5e 100644 --- a/README.md +++ b/README.md @@ -23,8 +23,15 @@ For detailed information on library development, building, testing, and contribu ## Graphite Skia has two backends: Ganesh and Graphite. Ganesh is the default backend. -If you want to experiment with Graphite, you can enable it by building Skia with `SK_GRAPHITE=1 yarn build-skia`. +Graphite is available as an experimental preview in the `@next` distribution channel: + +```sh +yarn add @shopify/react-native-skia@next +``` + +**This is highly experimental and not recommended for production use.** Skia Graphite requires Android API Level 26 or above. +If you prefer to build Skia yourself with Graphite support, you can do so with `SK_GRAPHITE=1 yarn build-skia`. React Native Skia automatically detects Graphite via a marker file included in the prebuilt libs. diff --git a/apps/docs/docs/getting-started/installation.md b/apps/docs/docs/getting-started/installation.md index ac0c08180d..3920306319 100644 --- a/apps/docs/docs/getting-started/installation.md +++ b/apps/docs/docs/getting-started/installation.md @@ -23,16 +23,7 @@ yarn add @shopify/react-native-skia npm install @shopify/react-native-skia ``` -This package uses a `postinstall` script to copy Skia prebuilt binaries into the correct location for the native build systems. Some package managers require you to explicitly allow this script to run: - -- **Bun**: Add `@shopify/react-native-skia` to `trustedDependencies` in your `package.json`: - ```json - { - "trustedDependencies": ["@shopify/react-native-skia"] - } - ``` -- **Yarn (Berry/v2+)**: Make sure `enableScripts` is not set to `false` in `.yarnrc.yml`. -- **npm/Yarn Classic**: The postinstall script runs automatically. +The Skia prebuilt binaries are delivered as regular npm dependencies (`react-native-skia-android` and `react-native-skia-apple-*`) and are resolved automatically by the native build systems (CocoaPods on iOS/macOS/tvOS, Gradle on Android). No `postinstall` script is required, so there is nothing to allow or configure — `trustedDependencies` (Bun) or `enableScripts` (Yarn Berry) settings are not needed. ## Using Expo @@ -132,6 +123,20 @@ The `jestEnv.js` will load CanvasKit for you and `jestEnv.js` mocks React Native You can also have a look at the [example app](https://github.com/Shopify/react-native-skia/tree/main/apps/example) to see how Jest tests are enabled there. +## Graphite (Experimental) + +Skia has two backends: Ganesh (default) and Graphite. An experimental preview of Graphite is available in the `@next` distribution channel: + +```sh +yarn add @shopify/react-native-skia@next +``` + +:::warning + +Graphite support is highly experimental. Skia Graphite requires Android API Level 26 or above. + +::: + ## Playground We have example projects you can play with [here](https://github.com/Shopify/react-native-skia/tree/main/apps). diff --git a/apps/docs/package.json b/apps/docs/package.json index 405f5564f3..f1d2cd5bc8 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -31,8 +31,8 @@ "react-dom": "19.0.0", "react-native": "0.83.1", "react-native-gesture-handler": "^2.24.0", - "react-native-reanimated": "^4.2.1", - "react-native-worklets": "^0.7.0", + "react-native-reanimated": "4.3.1", + "react-native-worklets": "0.8.3", "url-loader": "^4.1.1" }, "devDependencies": { diff --git a/apps/docs/static/img/lighting-image-filters/combined-lighting-fire-ice-ios-graphite.png b/apps/docs/static/img/lighting-image-filters/combined-lighting-fire-ice-ios-graphite.png new file mode 100644 index 0000000000..714926460c Binary files /dev/null and b/apps/docs/static/img/lighting-image-filters/combined-lighting-fire-ice-ios-graphite.png differ diff --git a/apps/docs/static/img/lighting-image-filters/point-lit-diffuse-ios-graphite.png b/apps/docs/static/img/lighting-image-filters/point-lit-diffuse-ios-graphite.png new file mode 100644 index 0000000000..c9f3c0a9d0 Binary files /dev/null and b/apps/docs/static/img/lighting-image-filters/point-lit-diffuse-ios-graphite.png differ diff --git a/apps/docs/static/img/lighting-image-filters/point-lit-specular-ios-graphite.png b/apps/docs/static/img/lighting-image-filters/point-lit-specular-ios-graphite.png new file mode 100644 index 0000000000..a7845d7745 Binary files /dev/null and b/apps/docs/static/img/lighting-image-filters/point-lit-specular-ios-graphite.png differ diff --git a/apps/docs/static/img/lighting-image-filters/spot-lit-diffuse-ios-graphite.png b/apps/docs/static/img/lighting-image-filters/spot-lit-diffuse-ios-graphite.png new file mode 100644 index 0000000000..a8435b1a8e Binary files /dev/null and b/apps/docs/static/img/lighting-image-filters/spot-lit-diffuse-ios-graphite.png differ diff --git a/apps/docs/static/img/lighting-image-filters/spot-lit-specular-ios-graphite.png b/apps/docs/static/img/lighting-image-filters/spot-lit-specular-ios-graphite.png new file mode 100644 index 0000000000..bd498f5831 Binary files /dev/null and b/apps/docs/static/img/lighting-image-filters/spot-lit-specular-ios-graphite.png differ diff --git a/apps/example/ios/Podfile b/apps/example/ios/Podfile index bb69c54efb..80473dfa75 100644 --- a/apps/example/ios/Podfile +++ b/apps/example/ios/Podfile @@ -6,4 +6,23 @@ require "#{ws_dir}/node_modules/react-native-test-app/test_app.rb" workspace 'example.xcworkspace' -use_test_app! :hermes_enabled => true, :new_arch_enabled => true +# Build the `fmt` pod as C++17. Xcode 26.4's Apple Clang 21 is stricter about +# consteval, and fmt's compile-time format-string checking (FMT_USE_CONSTEVAL, +# C++20-only) no longer satisfies the constant-expression rules, failing with +# "call to consteval function ... is not a constant expression". Since consteval +# didn't exist before C++20, compiling fmt as C++17 skips that path and falls +# back to runtime format-string validation. react-native-test-app forwards +# options[:post_install] to its own post_install hook. +fix_fmt_consteval = ->(installer) do + installer.pods_project.targets.each do |target| + next unless target.name == 'fmt' + + target.build_configurations.each do |config| + config.build_settings['CLANG_CXX_LANGUAGE_STANDARD'] = 'c++17' + end + end +end + +use_test_app! :hermes_enabled => true, + :new_arch_enabled => true, + :post_install => fix_fmt_consteval diff --git a/apps/example/ios/Podfile.lock b/apps/example/ios/Podfile.lock index 667c7f4b03..ab66d2e4c9 100644 --- a/apps/example/ios/Podfile.lock +++ b/apps/example/ios/Podfile.lock @@ -2628,7 +2628,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - RNReanimated (4.2.2): + - RNReanimated (4.3.1): - boost - DoubleConversion - fast_float @@ -2655,11 +2655,12 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNReanimated/reanimated (= 4.2.2) + - RNReanimated/apple (= 4.3.1) + - RNReanimated/common (= 4.3.1) - RNWorklets - SocketRocket - Yoga - - RNReanimated/reanimated (4.2.2): + - RNReanimated/apple (4.3.1): - boost - DoubleConversion - fast_float @@ -2686,11 +2687,10 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNReanimated/reanimated/apple (= 4.2.2) - RNWorklets - SocketRocket - Yoga - - RNReanimated/reanimated/apple (4.2.2): + - RNReanimated/common (4.3.1): - boost - DoubleConversion - fast_float @@ -2836,7 +2836,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - RNWorklets (0.7.4): + - RNWorklets (0.8.3): - boost - DoubleConversion - fast_float @@ -2863,10 +2863,11 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNWorklets/worklets (= 0.7.4) + - RNWorklets/apple (= 0.8.3) + - RNWorklets/common (= 0.8.3) - SocketRocket - Yoga - - RNWorklets/worklets (0.7.4): + - RNWorklets/apple (0.8.3): - boost - DoubleConversion - fast_float @@ -2893,10 +2894,9 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNWorklets/worklets/apple (= 0.7.4) - SocketRocket - Yoga - - RNWorklets/worklets/apple (0.7.4): + - RNWorklets/common (0.8.3): - boost - DoubleConversion - fast_float @@ -3207,7 +3207,7 @@ SPEC CHECKSUMS: FBLazyVector: 309703e71d3f2f1ed7dc7889d58309c9d77a95a4 fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd glog: 5683914934d5b6e4240e497e0f4a3b42d1854183 - hermes-engine: ad676c360175e5b8af471b8ce1389e6cf4f9e1ee + hermes-engine: 11b010917f5f15150b2c015abddef1573d2bb05d RCT-Folly: 846fda9475e61ec7bcbf8a3fe81edfcaeb090669 RCTDeprecation: a41bbdd9af30bf2e5715796b313e44ec43eefff1 RCTRequired: 7be34aabb0b77c3cefe644528df0fa0afad4e4d0 @@ -3245,7 +3245,7 @@ SPEC CHECKSUMS: React-Mapbuffer: 7b72a669e94662359dad4f42b5af005eb24b4e83 React-microtasksnativemodule: cdc02da075f2857803ed63f24f5f72fc40e094c0 react-native-safe-area-context: c00143b4823773bba23f2f19f85663ae89ceb460 - react-native-skia: 03e5c42eb263a22f9228d3cbfaa140fcaad08408 + react-native-skia: 3e1e8bfce008b86c45433f3ffc64bdd337b21953 React-NativeModulesApple: a2c3d2cbec893956a5b3e4060322db2984fff75b React-networking: 3f98bd96893a294376e7e03730947a08d474c380 React-oscompat: 80166b66da22e7af7fad94474e9997bd52d4c8c6 @@ -3283,13 +3283,13 @@ SPEC CHECKSUMS: ReactTestApp-DevSupport: ea18f446cff64b6c9a3e28788600c82ecf51bde6 ReactTestApp-Resources: 1bd9ff10e4c24f2ad87101a32023721ae923bccf RNGestureHandler: cd4be101cfa17ea6bbd438710caa02e286a84381 - RNReanimated: 8182ef6e805d04dce3618f1a843cb8d2f9ae23b4 + RNReanimated: c26dfcd831add485c2ed93de9d7bfb90b035eeaa RNScreens: 714e10b6b554f7dc7ad9f78dcf36dc8e3fc73415 RNSVG: 11354d28dd6cb71a59570b68c91ba6772a2d781d - RNWorklets: d9fe4a51421f69ce78bb415a7e0710c15fe61150 + RNWorklets: b89b501d37972e6419d6f87effe41d6d76157648 SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 Yoga: 5456bb010373068fc92221140921b09d126b116e -PODFILE CHECKSUM: 87506345285a0371afb28b9c3e6daaa999c214f3 +PODFILE CHECKSUM: dca89d921c9f2a2d3d405a5fca0bfb60b30b5022 COCOAPODS: 1.16.2 diff --git a/apps/example/package.json b/apps/example/package.json index b1ce2f4c08..28c01b0be7 100644 --- a/apps/example/package.json +++ b/apps/example/package.json @@ -35,16 +35,16 @@ "react-native": "0.83.1", "react-native-gesture-handler": "^2.24.0", "react-native-macos": "^0.81.1", - "react-native-reanimated": "^4.2.1", + "react-native-reanimated": "4.3.1", "react-native-safe-area-context": "^5.2.0", "react-native-screens": "^4.10.0", "react-native-svg": "patch:react-native-svg@npm%3A15.14.0#../../.yarn/patches/react-native-svg-npm-15.14.0-macos-uiimage-fix.patch", "react-native-web": "^0.21.2", "react-native-windows": "^0.75.0", - "react-native-worklets": "^0.7.0" + "react-native-worklets": "0.8.3" }, "devDependencies": { - "@babel/core": "^7.25.2", + "@babel/core": "^7.29.6", "@babel/plugin-proposal-explicit-resource-management": "^7.27.4", "@babel/preset-env": "^7.25.3", "@babel/runtime": "^7.25.0", diff --git a/apps/example/src/Examples/WebGPU/ImportExternalTexture.tsx b/apps/example/src/Examples/WebGPU/ImportExternalTexture.tsx new file mode 100644 index 0000000000..02d72404f7 --- /dev/null +++ b/apps/example/src/Examples/WebGPU/ImportExternalTexture.tsx @@ -0,0 +1,235 @@ +import React, { useEffect, useRef } from "react"; +import { StyleSheet, View, Text } from "react-native"; +import type { WebGPUCanvasRef, NativeBuffer } from "@shopify/react-native-skia"; +import { WebGPUCanvas, Skia } from "@shopify/react-native-skia"; + +// Demonstrates GPUDevice.importExternalTexture with a self-contained source: a +// platform native buffer (CVPixelBufferRef on iOS, AHardwareBuffer on Android) +// CPU-filled with a procedural test pattern via Skia.NativeBuffer.MakeTestBuffer +// — no camera or video decode involved. Each frame we import the buffer as a +// GPUExternalTexture and sample it. A GPUExternalTexture expires once the queue +// work that used it is submitted, so we re-import one every frame. +const TEXTURE_SIZE = 512; + +const SHADER = /* wgsl */ ` +struct VsOut { + @builtin(position) position: vec4f, + @location(0) uv: vec2f, +}; + +// Per-axis scale applied to UVs around the center so the canvas samples a +// sub-rectangle of the texture matching the canvas aspect ratio ('cover' fit). +struct Uniforms { uvScale: vec2f }; + +@group(0) @binding(0) var srcTex: texture_external; +@group(0) @binding(1) var srcSampler: sampler; +@group(0) @binding(2) var u: Uniforms; + +@vertex +fn vs_main(@builtin(vertex_index) vid: u32) -> VsOut { + // Full-screen triangle. + var positions = array( + vec2f(-1.0, -3.0), + vec2f(-1.0, 1.0), + vec2f( 3.0, 1.0), + ); + var uvs = array( + vec2f(0.0, 2.0), + vec2f(0.0, 0.0), + vec2f(2.0, 0.0), + ); + var out: VsOut; + out.position = vec4f(positions[vid], 0.0, 1.0); + out.uv = uvs[vid]; + return out; +} + +@fragment +fn fs_main(in: VsOut) -> @location(0) vec4f { + let uv = vec2f(0.5) + (in.uv - vec2f(0.5)) * u.uvScale; + // External textures must be sampled with textureSampleBaseClampToEdge. + let color = textureSampleBaseClampToEdge(srcTex, srcSampler, uv); + return vec4f(color.rgb, 1.0); +} +`; + +export function ImportExternalTexture() { + const canvasRef = useRef(null); + const animationRef = useRef(0); + const cleanupRef = useRef<(() => void) | null>(null); + + useEffect(() => { + let cancelled = false; + + const init = () => { + if (!canvasRef.current || typeof RNWebGPU === "undefined") { + return; + } + const ctx = canvasRef.current.getContext("webgpu"); + if (!ctx) { + return; + } + + const device = Skia.getDevice(); + const canvas = ctx.canvas as unknown as { width: number; height: number }; + const format = navigator.gpu.getPreferredCanvasFormat(); + ctx.configure({ device, format, alphaMode: "premultiplied" }); + + const module = device.createShaderModule({ code: SHADER }); + const pipeline = device.createRenderPipeline({ + layout: "auto", + vertex: { module, entryPoint: "vs_main" }, + fragment: { module, entryPoint: "fs_main", targets: [{ format }] }, + primitive: { topology: "triangle-list" }, + }); + const sampler = device.createSampler({ + magFilter: "linear", + minFilter: "linear", + }); + // vec2 padded to the 16-byte uniform alignment. + const uniformBuffer = device.createBuffer({ + size: 16, + usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, + }); + + // 'cover' fit: scale UVs around their center so the longer axis of the + // texture is cropped to match the canvas aspect ratio. + const computeUvScale = (texW: number, texH: number): [number, number] => { + if (!canvas.width || !canvas.height) { + return [1, 1]; + } + const canvasAR = canvas.width / canvas.height; + const texAR = texW / texH; + return texAR > canvasAR ? [canvasAR / texAR, 1] : [1, texAR / canvasAR]; + }; + + // A single static test-pattern buffer reused every frame. + const nativeBuffer: NativeBuffer = Skia.NativeBuffer.MakeTestBuffer( + TEXTURE_SIZE, + TEXTURE_SIZE + ); + let uniformsWritten = false; + + const render = () => { + if (cancelled) { + return; + } + + if (!uniformsWritten) { + const [sx, sy] = computeUvScale(TEXTURE_SIZE, TEXTURE_SIZE); + device.queue.writeBuffer( + uniformBuffer, + 0, + new Float32Array([sx, sy]) + ); + uniformsWritten = true; + } + + const encoder = device.createCommandEncoder(); + const pass = encoder.beginRenderPass({ + colorAttachments: [ + { + view: ctx.getCurrentTexture().createView(), + clearValue: { r: 0, g: 0, b: 0, a: 1 }, + loadOp: "clear", + storeOp: "store", + }, + ], + }); + + // Re-import the buffer each tick (external textures expire after submit). + let externalTexture: GPUExternalTexture | null = null; + try { + externalTexture = device.importExternalTexture({ + source: nativeBuffer, + label: "test-pattern", + }); + } catch (e) { + console.warn("[ImportExternalTexture] import failed:", e); + } + if (externalTexture) { + const bindGroup = device.createBindGroup({ + layout: pipeline.getBindGroupLayout(0), + entries: [ + { binding: 0, resource: externalTexture }, + { binding: 1, resource: sampler }, + { binding: 2, resource: { buffer: uniformBuffer } }, + ], + }); + pass.setPipeline(pipeline); + pass.setBindGroup(0, bindGroup); + pass.draw(3); + } + + pass.end(); + device.queue.submit([encoder.finish()]); + // End the access window now that the sampling work is submitted. + externalTexture?.destroy(); + ctx.present(); + animationRef.current = requestAnimationFrame(render); + }; + animationRef.current = requestAnimationFrame(render); + + cleanupRef.current = () => { + cancelAnimationFrame(animationRef.current); + Skia.NativeBuffer.Release(nativeBuffer); + uniformBuffer.destroy(); + }; + }; + + init(); + + return () => { + cancelled = true; + cleanupRef.current?.(); + }; + }, []); + + if (typeof RNWebGPU === "undefined") { + return ( + + + + External textures require SK_GRAPHITE to be enabled. + + + Build react-native-skia with Graphite support to use this feature. + + + + ); + } + + return ( + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: "#1a1a1a", + }, + canvas: { + flex: 1, + }, + messageContainer: { + flex: 1, + justifyContent: "center", + alignItems: "center", + padding: 20, + }, + message: { + color: "#fff", + fontSize: 18, + textAlign: "center", + marginBottom: 10, + }, + submessage: { + color: "#888", + fontSize: 14, + textAlign: "center", + }, +}); diff --git a/apps/example/src/Examples/WebGPU/List.tsx b/apps/example/src/Examples/WebGPU/List.tsx index ca4d1f9864..10c781d51d 100644 --- a/apps/example/src/Examples/WebGPU/List.tsx +++ b/apps/example/src/Examples/WebGPU/List.tsx @@ -21,6 +21,11 @@ export const examples = [ title: "Textured Cube", description: "Rotating 3D cube with texture mapping", }, + { + screen: "ImportExternalTexture", + title: "Import External Texture", + description: "Import a Skia NativeBuffer as a GPUExternalTexture", + }, ] as const; const styles = StyleSheet.create({ diff --git a/apps/example/src/Examples/WebGPU/Routes.ts b/apps/example/src/Examples/WebGPU/Routes.ts index 66cb2da81b..c4d91bec38 100644 --- a/apps/example/src/Examples/WebGPU/Routes.ts +++ b/apps/example/src/Examples/WebGPU/Routes.ts @@ -3,4 +3,5 @@ export type Routes = { Wireframes: undefined; Triangle: undefined; TexturedCube: undefined; + ImportExternalTexture: undefined; }; diff --git a/apps/example/src/Examples/WebGPU/index.tsx b/apps/example/src/Examples/WebGPU/index.tsx index 292bd85c23..2a95976e58 100644 --- a/apps/example/src/Examples/WebGPU/index.tsx +++ b/apps/example/src/Examples/WebGPU/index.tsx @@ -6,6 +6,7 @@ import { List } from "./List"; import { Triangle } from "./Triangle"; import { Wireframes } from "./Wireframes"; import { TexturedCube } from "./TexturedCube"; +import { ImportExternalTexture } from "./ImportExternalTexture"; const Stack = createNativeStackNavigator(); @@ -42,6 +43,13 @@ export const WebGPU = () => { title: "Textured Cube", }} /> + ); }; diff --git a/apps/example/src/Tests/Tests.tsx b/apps/example/src/Tests/Tests.tsx index fac5c7cf6c..892d5c0fe1 100644 --- a/apps/example/src/Tests/Tests.tsx +++ b/apps/example/src/Tests/Tests.tsx @@ -21,6 +21,15 @@ const scale = s / PixelRatio.get(); const size = 256 * scale; const timeToDraw = CI ? 1500 : 500; +// Report a failure back to the host as { $$error: message } so the matching +// test can `.rejects` instead of hanging on a missing reply, and so the throw +// is not surfaced as an unhandled exception that could break the socket for +// subsequent tests. +const errorEnvelope = (error: unknown) => + JSON.stringify({ + $$error: error instanceof Error ? error.message : String(error), + }); + interface TestsProps { assets: { [key: string]: any }; } @@ -38,22 +47,29 @@ export const Tests = ({ assets }: TestsProps) => { const handleMessage = (e: MessageEvent) => { const tree: any = JSON.parse(e.data); if (tree.code) { - client.send( - JSON.stringify( - eval( - `(function Main() { + try { + const result = eval( + `(function Main() { const __addDisposableResource = (e, o) => { return o; }; const __disposeResources = () => {}; return (${tree.code})(this.Skia, this.ctx, this.size, this.scale); })` - ).call({ - Skia, - ctx: parseProps(tree.ctx, assets), - size: size * PixelRatio.get(), - scale: s, - }) - ) - ); + ).call({ + Skia, + ctx: parseProps(tree.ctx, assets), + size: size * PixelRatio.get(), + scale: s, + }); + if (result instanceof Promise) { + result + .then((r) => client.send(JSON.stringify(r))) + .catch((error) => client.send(errorEnvelope(error))); + } else { + client.send(JSON.stringify(result)); + } + } catch (error) { + client.send(errorEnvelope(error)); + } } else if (typeof tree.screen === "string") { const Screen = Screens[tree.screen]; if (!Screen) { @@ -96,6 +112,7 @@ export const Tests = ({ assets }: TestsProps) => { }) .catch((e) => { console.error(e); + client.send(errorEnvelope(e)); }); } }, timeToDraw); @@ -109,10 +126,15 @@ export const Tests = ({ assets }: TestsProps) => { useEffect(() => { if (screen && client) { const it = setTimeout(async () => { - const image = await makeImageFromView(viewRef as RefObject); - if (image && client) { - const data = image.encodeToBytes(); - client.send(data); + try { + const image = await makeImageFromView(viewRef as RefObject); + if (image && client) { + const data = image.encodeToBytes(); + client.send(data); + } + } catch (e) { + console.error(e); + client.send(errorEnvelope(e)); } }, timeToDraw); return () => { diff --git a/apps/example/src/Tests/useClient.ts b/apps/example/src/Tests/useClient.ts index debb09d74e..52fcf86d0a 100644 --- a/apps/example/src/Tests/useClient.ts +++ b/apps/example/src/Tests/useClient.ts @@ -1,3 +1,4 @@ +import { Skia } from "@shopify/react-native-skia"; import { useEffect, useState } from "react"; import { Platform } from "react-native"; @@ -8,6 +9,17 @@ const HOST = OS === "android" ? ANDROID_WS_HOST : IOS_WS_HOST; const PORT = 4242; // eslint-disable-next-line @typescript-eslint/no-explicit-any const arch = (global as any)?.nativeFabricUIManager ? "fabric" : "paper"; +// Whether this build runs the Graphite backend, i.e. WebGPU bindings +// (navigator.gpu / Skia.getDevice()) are available. Reported to the test +// server so it can gate Graphite-only specs. Guarded in case an older runtime +// doesn't expose hasDevice(). +const graphite = (() => { + try { + return Skia.hasDevice?.() ?? false; + } catch { + return false; + } +})(); type UseClient = [client: WebSocket | null, hostname: string]; export const useClient = (): UseClient => { @@ -24,6 +36,7 @@ export const useClient = (): UseClient => { JSON.stringify({ OS, arch, + graphite, }) ); }; diff --git a/apps/example/tsconfig.json b/apps/example/tsconfig.json index e35cb74b63..3aff833c27 100644 --- a/apps/example/tsconfig.json +++ b/apps/example/tsconfig.json @@ -9,5 +9,8 @@ "esModuleInterop": true, "skipLibCheck": true, "baseUrl": ".", + "paths": { + "@shopify/react-native-skia": ["../../packages/skia/src/index.ts"] + } } } diff --git a/apps/remotion/package.json b/apps/remotion/package.json index acbed1c20c..1a49c9bfbb 100644 --- a/apps/remotion/package.json +++ b/apps/remotion/package.json @@ -14,7 +14,7 @@ "repository": {}, "license": "UNLICENSED", "dependencies": { - "@babel/core": "^7.20.2", + "@babel/core": "^7.29.6", "@babel/plugin-transform-flow-strip-types": "^7.25.9", "@babel/preset-env": "^7.20.2", "@babel/preset-typescript": "^7.18.6", diff --git a/apps/web-app/package.json b/apps/web-app/package.json index a8e95f96bd..a68779c972 100644 --- a/apps/web-app/package.json +++ b/apps/web-app/package.json @@ -16,8 +16,8 @@ "@types/react-dom": "^19.0.0", "react": "19.0.0", "react-dom": "19.0.0", - "react-native-reanimated": "^4.2.1", - "react-native-worklets": "^0.7.0", + "react-native-reanimated": "4.3.1", + "react-native-worklets": "0.8.3", "react-scripts": "5.0.1", "typescript": "^5.2.2", "web-vitals": "^2.1.0" diff --git a/externals/depot_tools b/externals/depot_tools index a6671ce6c7..f488573369 160000 --- a/externals/depot_tools +++ b/externals/depot_tools @@ -1 +1 @@ -Subproject commit a6671ce6c7086f856322fb3afa9ff41c99d76de5 +Subproject commit f488573369dfbbdd523177b93a369d4096d06e5d diff --git a/externals/skia b/externals/skia index 4502f88af9..9f330f1704 160000 --- a/externals/skia +++ b/externals/skia @@ -1 +1 @@ -Subproject commit 4502f88af90279ad2685528bd3cf7e90ab140f19 +Subproject commit 9f330f1704305686dafa9eeef11de77caa5314b1 diff --git a/package.json b/package.json index f0525584b7..9be177f75c 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,6 @@ "eas-cli": "18.4.0", "playwright": "1.59.0", "react-native-test-app": "^4.2.0", - "turbo": "^2.1.1" + "turbo": "^2.9.14" } } diff --git a/packages/skia/.babelrc b/packages/skia/.babelrc index d25ebe3bee..bccb2e74d7 100644 --- a/packages/skia/.babelrc +++ b/packages/skia/.babelrc @@ -28,7 +28,7 @@ ], "plugins": [ "@babel/plugin-transform-explicit-resource-management", - "@babel/plugin-proposal-class-properties", - "@babel/plugin-proposal-nullish-coalescing-operator" + "@babel/plugin-transform-class-properties", + "@babel/plugin-transform-nullish-coalescing-operator" ] } \ No newline at end of file diff --git a/packages/skia/CONTRIBUTING.md b/packages/skia/CONTRIBUTING.md index 8e9c333c53..b7efb5ee90 100644 --- a/packages/skia/CONTRIBUTING.md +++ b/packages/skia/CONTRIBUTING.md @@ -6,10 +6,20 @@ To develop react-native-skia, you can build the skia libraries on your computer. ### Using pre-built binaries -The Skia prebuilt binaries are installed as npm dependencies (`react-native-skia-android`, `react-native-skia-apple-*`). The native build systems (Gradle, CocoaPods) automatically resolve these packages. +The Skia prebuilt binaries are installed as npm dependencies (`react-native-skia-android`, `react-native-skia-apple-*`). The native build systems (Gradle, CocoaPods) automatically resolve these packages — there is no `postinstall` step. - Checkout submodules: `git submodule update --init --recursive` - Install dependencies: `yarn` +- Set up the standard build: `cd packages/skia && yarn install-skia` + +`yarn install-skia` copies the Skia headers needed to compile against the prebuilt binaries. The binaries themselves are not copied: Gradle reads them in place from `node_modules`, and the podspec copies them in at `pod install` time. + +#### Switching between the standard and Graphite builds + +- Standard (Ganesh) build: `yarn install-skia` +- [Graphite](https://skia.org/docs/user/graphite/) build: `yarn install-skia-graphite` (downloads the Graphite binaries into `libs/` and writes a `libs/.graphite` marker) + +Run `yarn install-skia` to switch back from Graphite to the standard build (it removes the `libs/.graphite` marker). After switching, run `pod install` again in the example app so CocoaPods picks up the matching frameworks. ### Building @@ -25,15 +35,99 @@ And then the _SDK Location_ section. It will show you the NDK path, or the optio - Build the Skia libraries: `yarn build-skia` (this can take a while) - Copy Skia headers: `yarn copy-skia-headers` -### Upgrading +### Upgrading Skia + +Upgrading to a new Skia milestone (for example `chrome/m147` to `chrome/m150`) is a multi-stage process: bump the submodule, build locally and fix the C++ API churn, test the example app against the freshly built binaries, publish the prebuilt binaries from CI, and finally release them through the binaries repo. The steps below use `m150` as the running example; substitute the milestone you are upgrading to. + +#### 1. Update the Skia submodule + +1. In `.gitmodules`, change the `externals/skia` submodule `branch` from `chrome/m147` to `chrome/m150`. +2. Fetch and checkout the new tip: + ```sh + cd externals/skia + git fetch origin chrome/m150 + git checkout FETCH_HEAD -- + cd ../.. + ``` + Confirm the submodule is on the new tip with `git -C externals/skia rev-parse HEAD`. (Once `.gitmodules` points at the new branch, `git submodule update --recursive --remote` also moves it.) + +#### 2. Build Skia and fix the C++ API churn + +Make sure `$ANDROID_NDK` and `$ANDROID_HOME` are set (see [Building](#building)). + +1. Bootstrap depot_tools once (otherwise `gn gen` fails with `python3_bin_reldir.txt not found`): + ```sh + cd externals/depot_tools && ./update_depot_tools && cd ../.. + ``` +2. Clean and build from `packages/skia`: + ```sh + cd packages/skia + yarn clean-skia + yarn build-skia # all platforms, or scope it: yarn build-skia apple-ios android + yarn copy-skia-headers # build-skia also runs this at the end + ``` + `build-skia` runs `tools/git-sync-deps` first, which fetches third-party deps from `*.googlesource.com`. This can fail with HTTP 429 ("Short term server-time rate limit exceeded"). Re-run it with backoff until it succeeds before retrying the build: + ```sh + cd externals/skia && PATH=../depot_tools/:$PATH python3 tools/git-sync-deps + ``` +3. Fix any C++ compilation errors coming from the new headers. Skia's public API churns between milestones (APIs that start returning `std::optional`, `SkPath` becoming `SkPathBuilder`, gradients moving to `SkGradient`/`SkShaders`, new required includes, and so on). The wrapper code in `cpp/**` is what needs updating; the previous Skia bump commit is a good reference for the kinds of changes to expect. + - If a vendored Skia source file starts including a header that is not copied yet (for example `src/base/SkAutoLocaleSetter.h`), add it to the copy list in `scripts/skia-configuration.ts` (`copyHeaders`) and re-run `yarn copy-skia-headers`. + - The vendored headers under `cpp/skia/**` are generated by `copy-skia-headers` and are gitignored, so only the script and the wrapper sources show up as changes. + +#### 3. Test the example app locally + +Important gotcha: in the standard (Ganesh) build the example app links against the prebuilt binaries from the npm packages (`react-native-skia-android`, `react-native-skia-apple-*`), not the libs you just built in `packages/skia/libs/`. Until those packages are republished (steps 4 to 6), the app compiles against the new `m150` headers but links the old binaries, which surfaces as link errors such as `undefined symbol: vtable for SkFontMgr`. Point the local build at your fresh binaries first: + +- Android: overwrite the static libs in the npm package with the ones you built: + ```sh + for abi in armeabi-v7a arm64-v8a x86 x86_64; do + cp packages/skia/libs/android/$abi/*.a node_modules/react-native-skia-android/libs/$abi/ + done + ``` +- iOS: `pod install` copies the npm package's xcframeworks into `libs/ios`, skipping only when `libs/ios/.version` matches the npm package version. After `yarn build-skia apple-ios` rewrites `libs/ios` with your build, stamp the marker with the current npm version so `pod install` leaves your binaries in place: + ```sh + printf "$(node -p "require('react-native-skia-apple-ios/package.json').version")" \ + > packages/skia/libs/ios/.version + cd apps/example/ios && pod install && cd - + ``` + +These `node_modules` and `.version` edits are throwaway; `yarn install` restores the published binaries. + +Then build both platforms: + +- iOS: + ```sh + cd apps/example/ios + xcodebuild -workspace example.xcworkspace -scheme example -sdk iphonesimulator \ + -configuration Debug -destination 'generic/platform=iOS Simulator' \ + build CODE_SIGNING_ALLOWED=NO + ``` +- Android: + ```sh + cd apps/example/android && ./gradlew :app:assembleDebug + ``` + +`yarn ios` / `yarn android` work too. Run the e2e tests (see [Testing](#testing)) to validate behavior, not just compilation. + +#### 4. Publish the prebuilt binaries (GitHub Actions) + +With the submodule bump merged (the workflows detect the Skia branch from the checked-in submodule), build and upload the prebuilt binaries from the Actions tab. Both workflows are `workflow_dispatch` only and share two inputs: + +- `tag_suffix`: appended to the tag (for example `a` produces `skia-m150a`) for re-spins of the same milestone. +- `dry_run`: build and upload as workflow artifacts only, skipping the GitHub release. Use this to validate the build before cutting a real release. + +Run them: + +- Standard (Ganesh): **Build SKIA** (`.github/workflows/build-skia.yml`). Builds apple-ios, apple-tvos, apple-macos and the four Android ABIs, creates a prerelease tagged `skia-m150`, and uploads one tarball per target. +- Graphite: **Build SKIA Graphite** (`.github/workflows/build-skia-graphite.yml`, `SK_GRAPHITE=1`). Builds iOS, macOS and Android (no tvOS/maccatalyst), tags `skia-graphite-m150`, and additionally uploads the Graphite headers tarball. It also accepts an optional `skia_branch` input to build a branch other than the submodule default. + +#### 5. Release the binaries through react-native-skia-binaries + +The npm packages this library consumes (`react-native-skia-android`, `react-native-skia-apple-ios`, `react-native-skia-apple-macos`, `react-native-skia-apple-tvos`, and the Graphite headers package) are produced from the release tarballs in [wcandillon/react-native-skia-binaries](https://github.com/wcandillon/react-native-skia-binaries). Update that repo to consume the new `skia-m150` and `skia-graphite-m150` release assets, bump the package versions, and publish them to npm. -If a new version of Skia is included in an upgrade of this library, you need to perform a few extra steps before continuing: +#### 6. Point the library at the new binaries -1. Update submodules: `git submodule update --recursive --remote` -2. Clean Skia: `yarn clean-skia` -3. Build Skia: `yarn build-skia` -4. Copy Skia Headers: `yarn copy-skia-headers` -5. Run pod install in the example project +Back in this repo, bump the prebuilt binary versions in `packages/skia/package.json` (`react-native-skia-android` and `react-native-skia-apple-*`) to the versions you just published, run `yarn`, and re-run `pod install` in the example app so it consumes the released binaries. Drop the throwaway `node_modules` and `libs/ios/.version` edits from step 3. ### Publishing diff --git a/packages/skia/android/CMakeLists.txt b/packages/skia/android/CMakeLists.txt index 1b085e6c14..0f4f24235c 100644 --- a/packages/skia/android/CMakeLists.txt +++ b/packages/skia/android/CMakeLists.txt @@ -4,7 +4,8 @@ cmake_minimum_required(VERSION 3.4.1) set (CMAKE_VERBOSE_MAKEFILE ON) set (CMAKE_CXX_STANDARD 20) -# SKIA_LIBS_PATH is passed from Gradle (pointing to libs/android/, populated by npm postinstall) +# SKIA_LIBS_PATH is passed from Gradle (pointing at the prebuilt Skia binaries, +# read straight from the react-native-skia-android npm package, or libs/android for Graphite) # Append the ABI to get the full path set (SKIA_LIBS_PATH "${SKIA_LIBS_PATH}/${ANDROID_ABI}") @@ -17,12 +18,12 @@ if(NOT EXISTS "${SKIA_LIBS_PATH}/libskia.a") message("│ │") message("│ Could not find libskia.a at: ${SKIA_LIBS_PATH} │") message("│ │") - message("│ Run the following command to install them: │") - message("│ npx install-skia │") + message("│ Make sure dependencies are installed: │") + message("│ yarn install (or npm install) │") message("│ │") message("└─────────────────────────────────────────────────────────────────────────────┘") message("") - message(FATAL_ERROR "Skia prebuilt binaries not found. Run `npx install-skia` to fix this.") + message(FATAL_ERROR "Skia prebuilt binaries not found. Run `yarn install` to fix this.") endif() # Import libskia @@ -134,12 +135,15 @@ if(SK_GRAPHITE) "${PROJECT_SOURCE_DIR}/../cpp/rnwgpu/api/GPUComputePipeline.cpp" "${PROJECT_SOURCE_DIR}/../cpp/rnwgpu/api/GPUDevice.cpp" "${PROJECT_SOURCE_DIR}/../cpp/rnwgpu/api/GPUDeviceLostInfo.cpp" + "${PROJECT_SOURCE_DIR}/../cpp/rnwgpu/api/GPUExternalTexture.cpp" "${PROJECT_SOURCE_DIR}/../cpp/rnwgpu/api/GPUQuerySet.cpp" "${PROJECT_SOURCE_DIR}/../cpp/rnwgpu/api/GPUQueue.cpp" "${PROJECT_SOURCE_DIR}/../cpp/rnwgpu/api/GPURenderBundleEncoder.cpp" "${PROJECT_SOURCE_DIR}/../cpp/rnwgpu/api/GPURenderPassEncoder.cpp" "${PROJECT_SOURCE_DIR}/../cpp/rnwgpu/api/GPURenderPipeline.cpp" "${PROJECT_SOURCE_DIR}/../cpp/rnwgpu/api/GPUShaderModule.cpp" + "${PROJECT_SOURCE_DIR}/../cpp/rnwgpu/api/GPUSharedFence.cpp" + "${PROJECT_SOURCE_DIR}/../cpp/rnwgpu/api/GPUSharedTextureMemory.cpp" "${PROJECT_SOURCE_DIR}/../cpp/rnwgpu/api/GPUSupportedLimits.cpp" "${PROJECT_SOURCE_DIR}/../cpp/rnwgpu/api/GPUTexture.cpp" diff --git a/packages/skia/android/build.gradle b/packages/skia/android/build.gradle index 437408bb12..ae3a4be040 100644 --- a/packages/skia/android/build.gradle +++ b/packages/skia/android/build.gradle @@ -58,10 +58,29 @@ static def findNodeModules(baseDir) { def nodeModules = findNodeModules(projectDir) -// Skia prebuilt libs are copied into libs/android/ by the npm postinstall script -// Graphite is detected via marker file created by install-skia-graphite +// Resolve a node package directory using Node's own module resolution, so we +// pick up the prebuilt Skia binaries straight from node_modules (no postinstall +// copy step needed). Mirrors `require.resolve(pkg/package.json)`. +static def resolveNodePackage(packageName, baseDir) { + def script = "process.stdout.write(require('path').dirname(require.resolve('${packageName}/package.json')))" + def proc = ["node", "-e", script].execute(null, baseDir) + proc.waitFor() + if (proc.exitValue() != 0) { + throw new GradleException( + "react-native-skia: Could not resolve the '${packageName}' package. " + + "Make sure dependencies are installed (yarn install / npm install).\n" + proc.err.text + ) + } + return proc.text.trim() +} + +// Graphite is detected via a marker file created by install-skia-graphite, which +// downloads its binaries directly into libs/. For the default (Ganesh) build the +// binaries live in the react-native-skia-android npm package and are read in place. def useGraphite = file("${projectDir}/../libs/.graphite").exists() -def skiaLibsPath = "${projectDir}/../libs/android" +def skiaLibsPath = useGraphite + ? "${projectDir}/../libs/android" + : "${resolveNodePackage('react-native-skia-android', projectDir)}/libs" logger.warn("react-native-skia: SK_GRAPHITE: ${useGraphite}") logger.warn("react-native-skia: Skia libs: ${skiaLibsPath}") diff --git a/packages/skia/android/cpp/rnskia-android/RNSkAndroidPlatformContext.h b/packages/skia/android/cpp/rnskia-android/RNSkAndroidPlatformContext.h index f9f8a1540d..cb98691276 100644 --- a/packages/skia/android/cpp/rnskia-android/RNSkAndroidPlatformContext.h +++ b/packages/skia/android/cpp/rnskia-android/RNSkAndroidPlatformContext.h @@ -3,6 +3,8 @@ #if __ANDROID_API__ >= 26 #include #endif +#include +#include #include #include #include @@ -120,6 +122,13 @@ class RNSkAndroidPlatformContext : public RNSkPlatformContext { uint64_t makeNativeBuffer(sk_sp image) override { #if __ANDROID_API__ >= 26 +#if defined(SK_GRAPHITE) + // A Graphite GPU texture can't be read with readPixels(nullptr); read it + // back to a raster image first or the buffer ends up uninitialized/black. + if (image && image->isTextureBacked()) { + image = DawnContext::getInstance().MakeRasterImage(image); + } +#endif auto bytesPerPixel = image->imageInfo().bytesPerPixel(); int bytesPerRow = image->width() * bytesPerPixel; auto buf = SkData::MakeUninitialized(image->width() * image->height() * @@ -177,6 +186,59 @@ class RNSkAndroidPlatformContext : public RNSkPlatformContext { #endif } + uint64_t makeTestNativeBuffer(int width, int height) override { +#if __ANDROID_API__ >= 26 + // Allocate an RGBA8 AHardwareBuffer and fill it with a procedural test + // pattern (RGB gradient + diagonal stripes), entirely on the CPU. We read + // the buffer's actual row stride after locking (the allocator may pad it), + // so the upload is correct regardless of width alignment. + AHardwareBuffer_Desc desc = {}; + desc.width = static_cast(width); + desc.height = static_cast(height); + desc.layers = 1; + desc.format = AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM; + desc.usage = AHARDWAREBUFFER_USAGE_CPU_WRITE_RARELY | + AHARDWAREBUFFER_USAGE_CPU_READ_RARELY | + AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE; + + AHardwareBuffer *buffer = nullptr; + if (AHardwareBuffer_allocate(&desc, &buffer) != 0) { + return 0; + } + + AHardwareBuffer_Desc allocated = {}; + AHardwareBuffer_describe(buffer, &allocated); + const size_t rowBytes = static_cast(allocated.stride) * 4; + + void *mappedBuffer = nullptr; + AHardwareBuffer_lock(buffer, AHARDWAREBUFFER_USAGE_CPU_WRITE_RARELY, -1, + nullptr, &mappedBuffer); + if (mappedBuffer == nullptr) { + AHardwareBuffer_release(buffer); + return 0; + } + + auto *base = static_cast(mappedBuffer); + for (int y = 0; y < height; ++y) { + uint8_t *row = base + y * rowBytes; + for (int x = 0; x < width; ++x) { + uint8_t r = static_cast((x * 255) / std::max(width - 1, 1)); + uint8_t g = static_cast((y * 255) / std::max(height - 1, 1)); + uint8_t b = static_cast(((x + y) & 0x20) ? 220 : 30); + row[x * 4 + 0] = r; // RGBA byte order + row[x * 4 + 1] = g; + row[x * 4 + 2] = b; + row[x * 4 + 3] = 0xFF; + } + } + + AHardwareBuffer_unlock(buffer, nullptr); + return reinterpret_cast(buffer); +#else + return 0; +#endif + } + #if !defined(SK_GRAPHITE) GrDirectContext *getDirectContext() override { return OpenGLContext::getInstance().getDirectContext(); diff --git a/packages/skia/android/src/paper/java/com/facebook/react/viewmanagers/SkiaWebGPUViewManagerDelegate.java b/packages/skia/android/src/paper/java/com/facebook/react/viewmanagers/SkiaWebGPUViewManagerDelegate.java new file mode 100644 index 0000000000..553618e68f --- /dev/null +++ b/packages/skia/android/src/paper/java/com/facebook/react/viewmanagers/SkiaWebGPUViewManagerDelegate.java @@ -0,0 +1,35 @@ +/** +* This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen). +* +* Do not edit this file as changes may cause incorrect behavior and will be lost +* once the code is regenerated. +* +* @generated by codegen project: GeneratePropsJavaDelegate.js +*/ + +package com.facebook.react.viewmanagers; + +import android.view.View; +import androidx.annotation.Nullable; +import com.facebook.react.uimanager.BaseViewManagerDelegate; +import com.facebook.react.uimanager.BaseViewManager; +import com.facebook.react.uimanager.LayoutShadowNode; + +public class SkiaWebGPUViewManagerDelegate & SkiaWebGPUViewManagerInterface> extends BaseViewManagerDelegate { + public SkiaWebGPUViewManagerDelegate(U viewManager) { + super(viewManager); + } + @Override + public void setProperty(T view, String propName, @Nullable Object value) { + switch (propName) { + case "contextId": + mViewManager.setContextId(view, value == null ? 0 : ((Double) value).intValue()); + break; + case "transparent": + mViewManager.setTransparent(view, value != null && (boolean) value); + break; + default: + super.setProperty(view, propName, value); + } + } +} diff --git a/packages/skia/android/src/paper/java/com/facebook/react/viewmanagers/SkiaWebGPUViewManagerInterface.java b/packages/skia/android/src/paper/java/com/facebook/react/viewmanagers/SkiaWebGPUViewManagerInterface.java new file mode 100644 index 0000000000..e905536180 --- /dev/null +++ b/packages/skia/android/src/paper/java/com/facebook/react/viewmanagers/SkiaWebGPUViewManagerInterface.java @@ -0,0 +1,17 @@ +/** +* This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen). +* +* Do not edit this file as changes may cause incorrect behavior and will be lost +* once the code is regenerated. +* +* @generated by codegen project: GeneratePropsJavaInterface.js +*/ + +package com.facebook.react.viewmanagers; + +import android.view.View; + +public interface SkiaWebGPUViewManagerInterface { + void setContextId(T view, int value); + void setTransparent(T view, boolean value); +} diff --git a/packages/skia/apple/RNSkApplePlatformContext.h b/packages/skia/apple/RNSkApplePlatformContext.h index f24823420c..9ee648093f 100644 --- a/packages/skia/apple/RNSkApplePlatformContext.h +++ b/packages/skia/apple/RNSkApplePlatformContext.h @@ -57,6 +57,8 @@ class RNSkApplePlatformContext : public RNSkPlatformContext { uint64_t makeNativeBuffer(sk_sp image) override; + uint64_t makeTestNativeBuffer(int width, int height) override; + void releaseNativeBuffer(uint64_t pointer) override; std::shared_ptr createVideo(const std::string &url) override; diff --git a/packages/skia/apple/RNSkApplePlatformContext.mm b/packages/skia/apple/RNSkApplePlatformContext.mm index 9f5fda81b3..b7800624ee 100644 --- a/packages/skia/apple/RNSkApplePlatformContext.mm +++ b/packages/skia/apple/RNSkApplePlatformContext.mm @@ -3,6 +3,7 @@ #import #include #import +#include #include #include #include @@ -82,6 +83,15 @@ } uint64_t RNSkApplePlatformContext::makeNativeBuffer(sk_sp image) { +#if defined(SK_GRAPHITE) + // A Graphite GPU texture can't be read with readPixels(nullptr) (and can't be + // drawn onto a raster surface) — both yield uninitialized/black pixels. Read + // it back to a raster image first. (JsiNativeBuffer calls the Ganesh-only + // SkImage::makeNonTextureImage(), which is a no-op on Graphite.) + if (image && image->isTextureBacked()) { + image = DawnContext::getInstance().MakeRasterImage(image); + } +#endif // 0. If Image is not in BGRA, convert to BGRA as only BGRA is supported. if (image->colorType() != kBGRA_8888_SkColorType) { const SkImageInfo bgraInfo = @@ -170,6 +180,67 @@ return reinterpret_cast(pixelBuffer); } +uint64_t RNSkApplePlatformContext::makeTestNativeBuffer(int width, int height) { + // Allocate a BGRA IOSurface and fill it with a procedural test pattern (RGB + // gradient + diagonal stripes), entirely on the CPU. No GPU / SkImage round + // trip, so this works the same on every backend. + const int bytesPerElement = 4; + const int pitch = width * bytesPerElement; + const int allocSize = width * height * bytesPerElement; + OSType pixelFormat = kCVPixelFormatType_32BGRA; + CFMutableDictionaryRef dict = CFDictionaryCreateMutable( + kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks, + &kCFTypeDictionaryValueCallBacks); + auto setInt = [&](CFStringRef key, int value) { + CFNumberRef num = + CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &value); + CFDictionarySetValue(dict, key, num); + CFRelease(num); + }; + setInt(kIOSurfaceWidth, width); + setInt(kIOSurfaceHeight, height); + setInt(kIOSurfaceBytesPerRow, pitch); + setInt(kIOSurfaceBytesPerElement, bytesPerElement); + setInt(kIOSurfacePixelFormat, static_cast(pixelFormat)); + setInt(kIOSurfaceAllocSize, allocSize); + IOSurfaceRef surface = IOSurfaceCreate(dict); + CFRelease(dict); + if (surface == nil) { + throw std::runtime_error("Failed to create " + std::to_string(width) + "x" + + std::to_string(height) + " test IOSurface!"); + } + + IOSurfaceLock(surface, 0, nil); + auto *base = static_cast(IOSurfaceGetBaseAddress(surface)); + const size_t rowBytes = IOSurfaceGetBytesPerRow(surface); + for (int y = 0; y < height; ++y) { + uint8_t *row = base + y * rowBytes; + for (int x = 0; x < width; ++x) { + uint8_t r = static_cast((x * 255) / std::max(width - 1, 1)); + uint8_t g = static_cast((y * 255) / std::max(height - 1, 1)); + uint8_t b = static_cast(((x + y) & 0x20) ? 220 : 30); + row[x * 4 + 0] = b; // BGRA byte order + row[x * 4 + 1] = g; + row[x * 4 + 2] = r; + row[x * 4 + 3] = 0xFF; + } + } + IOSurfaceUnlock(surface, 0, nil); + + CVPixelBufferRef pixelBuffer = nullptr; + CVReturn result = + CVPixelBufferCreateWithIOSurface(nil, surface, nil, &pixelBuffer); + // The CVPixelBuffer retains the IOSurface; drop our reference so the + // CVPixelBuffer is its sole owner (freed by releaseNativeBuffer). + CFRelease(surface); + if (result != kCVReturnSuccess) { + throw std::runtime_error("Failed to create CVPixelBuffer for test native " + "buffer! Return value: " + + std::to_string(result)); + } + return reinterpret_cast(pixelBuffer); +} + #if !defined(SK_GRAPHITE) GrDirectContext *RNSkApplePlatformContext::getDirectContext() { return MetalContext::getInstance().getDirectContext(); diff --git a/packages/skia/apple/RNWebGPUAppleNativeBuffer.mm b/packages/skia/apple/RNWebGPUAppleNativeBuffer.mm new file mode 100644 index 0000000000..a55a746a93 --- /dev/null +++ b/packages/skia/apple/RNWebGPUAppleNativeBuffer.mm @@ -0,0 +1,33 @@ +#ifdef SK_GRAPHITE + +#import + +#include "rnwgpu/api/AppleNativeBuffer.h" + +namespace rnwgpu { + +void *GetIOSurfaceFromNativeBuffer(void *cvPixelBuffer, uint32_t *outWidth, + uint32_t *outHeight) { + auto pixelBuffer = reinterpret_cast(cvPixelBuffer); + if (pixelBuffer == nullptr) { + if (outWidth != nullptr) { + *outWidth = 0; + } + if (outHeight != nullptr) { + *outHeight = 0; + } + return nullptr; + } + if (outWidth != nullptr) { + *outWidth = static_cast(CVPixelBufferGetWidth(pixelBuffer)); + } + if (outHeight != nullptr) { + *outHeight = static_cast(CVPixelBufferGetHeight(pixelBuffer)); + } + // The IOSurface is owned by the CVPixelBuffer; we don't retain it here. + return CVPixelBufferGetIOSurface(pixelBuffer); +} + +} // namespace rnwgpu + +#endif // SK_GRAPHITE diff --git a/packages/skia/cpp/api/JsiNativeBuffer.h b/packages/skia/cpp/api/JsiNativeBuffer.h index 283c4b8db7..480e8baca9 100644 --- a/packages/skia/cpp/api/JsiNativeBuffer.h +++ b/packages/skia/cpp/api/JsiNativeBuffer.h @@ -24,6 +24,13 @@ class JsiNativeBufferFactory : public JsiSkHostObject { return jsi::BigInt::fromUint64(runtime, pointer); } + JSI_HOST_FUNCTION(MakeTestBuffer) { + auto width = static_cast(arguments[0].asNumber()); + auto height = static_cast(arguments[1].asNumber()); + uint64_t pointer = getContext()->makeTestNativeBuffer(width, height); + return jsi::BigInt::fromUint64(runtime, pointer); + } + JSI_HOST_FUNCTION(Release) { jsi::BigInt pointer = arguments[0].asBigInt(runtime); @@ -34,7 +41,8 @@ class JsiNativeBufferFactory : public JsiSkHostObject { } JSI_EXPORT_FUNCTIONS(JSI_EXPORT_FUNC(JsiNativeBufferFactory, Release), - JSI_EXPORT_FUNC(JsiNativeBufferFactory, MakeFromImage)) + JSI_EXPORT_FUNC(JsiNativeBufferFactory, MakeFromImage), + JSI_EXPORT_FUNC(JsiNativeBufferFactory, MakeTestBuffer)) size_t getMemoryPressure() const override { return 1024; } diff --git a/packages/skia/cpp/api/JsiSkAnimatedImage.h b/packages/skia/cpp/api/JsiSkAnimatedImage.h index 25551b8e44..a7e47214e8 100644 --- a/packages/skia/cpp/api/JsiSkAnimatedImage.h +++ b/packages/skia/cpp/api/JsiSkAnimatedImage.h @@ -8,7 +8,7 @@ #include #include "JsiSkHostObjects.h" -#include "third_party/base64.h" +#include "api/third_party/base64.h" #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdocumentation" diff --git a/packages/skia/cpp/api/JsiSkAnimatedImageFactory.h b/packages/skia/cpp/api/JsiSkAnimatedImageFactory.h index 01f1422ce1..20222c3311 100644 --- a/packages/skia/cpp/api/JsiSkAnimatedImageFactory.h +++ b/packages/skia/cpp/api/JsiSkAnimatedImageFactory.h @@ -5,7 +5,7 @@ #include -#include "JsiPromises.h" +#include "jsi/JsiPromises.h" #include "JsiSkAnimatedImage.h" #include "JsiSkData.h" #include "JsiSkHostObjects.h" diff --git a/packages/skia/cpp/api/JsiSkApi.h b/packages/skia/cpp/api/JsiSkApi.h index f78c64ec77..e9d55c6e43 100644 --- a/packages/skia/cpp/api/JsiSkApi.h +++ b/packages/skia/cpp/api/JsiSkApi.h @@ -2,12 +2,12 @@ #include -#include "RNSkPlatformContext.h" +#include "rnskia/RNSkPlatformContext.h" #include "JsiSkHostObjects.h" #ifdef SK_GRAPHITE -#include "RNDawnContext.h" +#include "rnskia/RNDawnContext.h" #include "rnwgpu/api/GPUDevice.h" #include "rnwgpu/async/AsyncRunner.h" #endif @@ -61,7 +61,7 @@ #include "JsiSkiaContext.h" #include "JsiSkottieFactory.h" #include "JsiVideo.h" -#include "recorder/JsiRecorder.h" +#include "api/recorder/JsiRecorder.h" namespace RNSkia { diff --git a/packages/skia/cpp/api/JsiSkCanvas.h b/packages/skia/cpp/api/JsiSkCanvas.h index 98556b86e0..7cfa9ea2f8 100644 --- a/packages/skia/cpp/api/JsiSkCanvas.h +++ b/packages/skia/cpp/api/JsiSkCanvas.h @@ -19,7 +19,11 @@ #include "JsiSkTextBlob.h" #include "JsiSkVertices.h" -#include "RNSkTypedArray.h" +#include "utils/RNSkTypedArray.h" + +#if defined(SK_GRAPHITE) +#include "rnskia/RNDawnContext.h" +#endif #include @@ -644,6 +648,29 @@ class JsiSkCanvas : public JsiSkHostObject { .getArrayBuffer(runtime); auto bfrPtr = reinterpret_cast(buffer.data(runtime)); +#if defined(SK_GRAPHITE) + // Graphite records draws lazily and offers no synchronous GPU readback. If + // this canvas belongs to a surface, snap & submit its recording, snapshot + // it to a CPU raster image and read from that (mirroring makeImageSnapshot). + // A canvas without an owning surface (e.g. a picture-recording canvas) has + // no texture to read back, so fall through to the raster canvas read below. + if (_surface) { + // Snapshot first: makeImageSnapshot records a copy task into the recorder + // that must be submitted before the texture can be read back (this is the + // same ordering used by JsiSkSurface::makeImageSnapshot and RNSkView). + auto snapshot = _surface->makeImageSnapshot(); + if (auto *recorder = _surface->recorder()) { + DawnContext::getInstance().submitRecording(recorder->snap().get()); + } + auto raster = DawnContext::getInstance().MakeRasterImage(snapshot); + if (!raster || + !raster->readPixels(nullptr, *info, bfrPtr, bytesPerRow, srcX, srcY)) { + return jsi::Value::null(); + } + return dest; + } +#endif + if (!_canvas->readPixels(*info, bfrPtr, bytesPerRow, srcX, srcY)) { return jsi::Value::null(); } @@ -708,7 +735,13 @@ class JsiSkCanvas : public JsiSkHostObject { void setCanvas(SkCanvas *canvas) { _canvas = canvas; } SkCanvas *getCanvas() { return _canvas; } + // Optionally associate the canvas with its owning surface. This lets + // readPixels fall back to a surface snapshot on Graphite, which has no + // synchronous canvas readback. + void setSurface(sk_sp surface) { _surface = std::move(surface); } + private: SkCanvas *_canvas; + sk_sp _surface; }; } // namespace RNSkia diff --git a/packages/skia/cpp/api/JsiSkColor.h b/packages/skia/cpp/api/JsiSkColor.h index 75334fe08f..305a8b8bac 100644 --- a/packages/skia/cpp/api/JsiSkColor.h +++ b/packages/skia/cpp/api/JsiSkColor.h @@ -7,7 +7,7 @@ #include #include "JsiSkHostObjects.h" -#include "third_party/CSSColorParser.h" +#include "api/third_party/CSSColorParser.h" #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdocumentation" diff --git a/packages/skia/cpp/api/JsiSkDataFactory.h b/packages/skia/cpp/api/JsiSkDataFactory.h index 15d6046083..477992b1e8 100644 --- a/packages/skia/cpp/api/JsiSkDataFactory.h +++ b/packages/skia/cpp/api/JsiSkDataFactory.h @@ -5,9 +5,9 @@ #include -#include "JsiPromises.h" +#include "jsi/JsiPromises.h" #include "JsiSkData.h" -#include "third_party/base64.h" +#include "api/third_party/base64.h" namespace RNSkia { diff --git a/packages/skia/cpp/api/JsiSkFont.h b/packages/skia/cpp/api/JsiSkFont.h index baf3a8abd6..be387ed61d 100644 --- a/packages/skia/cpp/api/JsiSkFont.h +++ b/packages/skia/cpp/api/JsiSkFont.h @@ -6,7 +6,7 @@ #include #include "JsiSkHostObjects.h" -#include "RNSkLog.h" +#include "utils/RNSkLog.h" #include #include "JsiSkPaint.h" diff --git a/packages/skia/cpp/api/JsiSkFontMgr.h b/packages/skia/cpp/api/JsiSkFontMgr.h index aec7f266a5..4ce5837ea4 100644 --- a/packages/skia/cpp/api/JsiSkFontMgr.h +++ b/packages/skia/cpp/api/JsiSkFontMgr.h @@ -7,7 +7,7 @@ #include "JsiSkFontStyle.h" #include "JsiSkHostObjects.h" -#include "RNSkLog.h" +#include "utils/RNSkLog.h" #include #pragma clang diagnostic push diff --git a/packages/skia/cpp/api/JsiSkHostObjects.h b/packages/skia/cpp/api/JsiSkHostObjects.h index 756476ffe0..6d8f2c01e3 100644 --- a/packages/skia/cpp/api/JsiSkHostObjects.h +++ b/packages/skia/cpp/api/JsiSkHostObjects.h @@ -5,9 +5,9 @@ #include #include -#include "JsiHostObject.h" -#include "RNSkLog.h" -#include "RNSkPlatformContext.h" +#include "jsi/JsiHostObject.h" +#include "utils/RNSkLog.h" +#include "rnskia/RNSkPlatformContext.h" namespace RNSkia { diff --git a/packages/skia/cpp/api/JsiSkImage.h b/packages/skia/cpp/api/JsiSkImage.h index a269ddd499..377324b762 100644 --- a/packages/skia/cpp/api/JsiSkImage.h +++ b/packages/skia/cpp/api/JsiSkImage.h @@ -9,13 +9,13 @@ #include "JsiSkImageInfo.h" #include "JsiSkMatrix.h" #include "JsiSkShader.h" -#include "third_party/base64.h" +#include "api/third_party/base64.h" #include "JsiTextureInfo.h" -#include "RNSkTypedArray.h" +#include "utils/RNSkTypedArray.h" #if defined(SK_GRAPHITE) -#include "RNDawnContext.h" +#include "rnskia/RNDawnContext.h" #include "include/gpu/graphite/Context.h" #else #include "include/gpu/ganesh/GrDirectContext.h" @@ -277,9 +277,6 @@ class JsiSkImage : public JsiSkWrappingSkPtrHostObject { } JSI_HOST_FUNCTION(readPixels) { -#if defined(SK_GRAPHITE) - throw std::runtime_error("Not implemented yet"); -#else int srcX = 0; int srcY = 0; if (count > 0 && !arguments[0].isUndefined()) { @@ -313,13 +310,24 @@ class JsiSkImage : public JsiSkWrappingSkPtrHostObject { .getArrayBuffer(runtime); auto bfrPtr = reinterpret_cast(buffer.data(runtime)); +#if defined(SK_GRAPHITE) + // Graphite offers no synchronous GPU readback, so fall back to a CPU raster + // copy of the image (a no-op when the image is already raster) and read the + // pixels from that. This matches the Ganesh behaviour for non-texture and + // texture-backed images alike. + auto image = DawnContext::getInstance().MakeRasterImage(getObject()); + if (!image || + !image->readPixels(nullptr, info, bfrPtr, bytesPerRow, srcX, srcY)) { + return jsi::Value::null(); + } +#else auto grContext = getContext()->getDirectContext(); if (!getObject()->readPixels(grContext, info, bfrPtr, bytesPerRow, srcX, srcY)) { return jsi::Value::null(); } - return dest; #endif + return dest; } JSI_HOST_FUNCTION(makeNonTextureImage) { diff --git a/packages/skia/cpp/api/JsiSkImageFactory.h b/packages/skia/cpp/api/JsiSkImageFactory.h index 9661b69b46..e6382c749b 100644 --- a/packages/skia/cpp/api/JsiSkImageFactory.h +++ b/packages/skia/cpp/api/JsiSkImageFactory.h @@ -5,14 +5,14 @@ #include -#include "JsiPromises.h" +#include "jsi/JsiPromises.h" #include "JsiSkData.h" #include "JsiSkHostObjects.h" #include "JsiSkImage.h" #include "JsiSkImageInfo.h" #ifdef SK_GRAPHITE -#include "RNDawnContext.h" +#include "rnskia/RNDawnContext.h" #include "rnwgpu/api/GPUTexture.h" #endif diff --git a/packages/skia/cpp/api/JsiSkPath.h b/packages/skia/cpp/api/JsiSkPath.h index 5e3eeabc76..1b0b8d86b1 100644 --- a/packages/skia/cpp/api/JsiSkPath.h +++ b/packages/skia/cpp/api/JsiSkPath.h @@ -14,7 +14,7 @@ #include "JsiSkPoint.h" #include "JsiSkRRect.h" #include "JsiSkRect.h" -#include "RNSkLog.h" +#include "utils/RNSkLog.h" #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdocumentation" diff --git a/packages/skia/cpp/api/JsiSkPathFactory.h b/packages/skia/cpp/api/JsiSkPathFactory.h index 09cf7495f7..ec639d9186 100644 --- a/packages/skia/cpp/api/JsiSkPathFactory.h +++ b/packages/skia/cpp/api/JsiSkPathFactory.h @@ -17,7 +17,7 @@ #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdocumentation" -#include "RNSkLog.h" +#include "utils/RNSkLog.h" #include "include/core/SkPath.h" #include "include/core/SkPathBuilder.h" #include "include/core/SkPathUtils.h" diff --git a/packages/skia/cpp/api/JsiSkSkottie.h b/packages/skia/cpp/api/JsiSkSkottie.h index 73e97a2cee..d6c6933c4a 100644 --- a/packages/skia/cpp/api/JsiSkSkottie.h +++ b/packages/skia/cpp/api/JsiSkSkottie.h @@ -7,7 +7,7 @@ #include "JsiSkHostObjects.h" #include "JsiSkPoint.h" #include "JsiSkRect.h" -#include "third_party/SkottieUtils.h" +#include "api/third_party/SkottieUtils.h" #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdocumentation" diff --git a/packages/skia/cpp/api/JsiSkSurface.h b/packages/skia/cpp/api/JsiSkSurface.h index 343951d279..33e287c424 100644 --- a/packages/skia/cpp/api/JsiSkSurface.h +++ b/packages/skia/cpp/api/JsiSkSurface.h @@ -15,7 +15,7 @@ #include "JsiSkImage.h" #if defined(SK_GRAPHITE) -#include "RNDawnContext.h" +#include "rnskia/RNDawnContext.h" #endif #pragma clang diagnostic push @@ -72,8 +72,12 @@ class JsiSkSurface : public JsiSkWrappingSkPtrHostObject { } JSI_HOST_FUNCTION(getCanvas) { + auto surface = getObject(); auto canvas = - std::make_shared(getContext(), getObject()->getCanvas()); + std::make_shared(getContext(), surface->getCanvas()); + // Keep a reference to the owning surface so the canvas can read pixels back + // through a snapshot on Graphite (which lacks synchronous canvas readback). + canvas->setSurface(surface); return JSI_CREATE_HOST_OBJECT_WITH_MEMORY_PRESSURE(runtime, canvas, getContext()); } @@ -81,8 +85,12 @@ class JsiSkSurface : public JsiSkWrappingSkPtrHostObject { JSI_HOST_FUNCTION(flush) { auto surface = getObject(); #if defined(SK_GRAPHITE) - auto recording = surface->recorder()->snap(); - DawnContext::getInstance().submitRecording(recording.get()); + // A raster surface (e.g. Skia.Surface.Make) has no Graphite recorder; + // only Graphite-backed surfaces need to snap and submit a recording. + if (auto *recorder = surface->recorder()) { + auto recording = recorder->snap(); + DawnContext::getInstance().submitRecording(recording.get()); + } #else if (auto dContext = GrAsDirectContext(surface->recordingContext())) { dContext->flushAndSubmit(); @@ -102,8 +110,12 @@ class JsiSkSurface : public JsiSkWrappingSkPtrHostObject { image = surface->makeImageSnapshot(); } #if defined(SK_GRAPHITE) - auto recording = surface->recorder()->snap(); - DawnContext::getInstance().submitRecording(recording.get()); + // A raster surface (e.g. Skia.Surface.Make) has no Graphite recorder; its + // snapshot is already a valid CPU image, so skip the recording submit. + if (auto *recorder = surface->recorder()) { + auto recording = recorder->snap(); + DawnContext::getInstance().submitRecording(recording.get()); + } #endif if (count > 1 && arguments[1].isObject()) { auto jsiImage = diff --git a/packages/skia/cpp/api/JsiSkTypeface.h b/packages/skia/cpp/api/JsiSkTypeface.h index 3ab373b009..6e79c325c2 100644 --- a/packages/skia/cpp/api/JsiSkTypeface.h +++ b/packages/skia/cpp/api/JsiSkTypeface.h @@ -7,7 +7,7 @@ #include #include "JsiSkHostObjects.h" -#include "RNSkLog.h" +#include "utils/RNSkLog.h" #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdocumentation" diff --git a/packages/skia/cpp/api/JsiSkTypefaceFontProvider.h b/packages/skia/cpp/api/JsiSkTypefaceFontProvider.h index 1fa037e7b3..8d4ef8c3dd 100644 --- a/packages/skia/cpp/api/JsiSkTypefaceFontProvider.h +++ b/packages/skia/cpp/api/JsiSkTypefaceFontProvider.h @@ -9,7 +9,7 @@ #include "JsiSkHostObjects.h" #include "JsiSkTypeface.h" -#include "RNSkLog.h" +#include "utils/RNSkLog.h" #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdocumentation" diff --git a/packages/skia/cpp/api/JsiSkiaContext.h b/packages/skia/cpp/api/JsiSkiaContext.h index fbb4a653d6..7bf494f812 100644 --- a/packages/skia/cpp/api/JsiSkiaContext.h +++ b/packages/skia/cpp/api/JsiSkiaContext.h @@ -6,7 +6,7 @@ #include #include "JsiSkHostObjects.h" -#include "RNSkLog.h" +#include "utils/RNSkLog.h" #include #include "JsiSkPaint.h" @@ -14,7 +14,7 @@ #include "JsiSkRect.h" #include "JsiSkTypeface.h" -#include "RNWindowContext.h" +#include "rnskia/RNWindowContext.h" #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdocumentation" diff --git a/packages/skia/cpp/api/JsiTextureInfo.h b/packages/skia/cpp/api/JsiTextureInfo.h index 762e080c3d..5f0f053bbb 100644 --- a/packages/skia/cpp/api/JsiTextureInfo.h +++ b/packages/skia/cpp/api/JsiTextureInfo.h @@ -2,7 +2,7 @@ #include -#include "RNSkPlatformContext.h" +#include "rnskia/RNSkPlatformContext.h" namespace jsi = facebook::jsi; namespace react = facebook::react; diff --git a/packages/skia/cpp/api/JsiVideo.h b/packages/skia/cpp/api/JsiVideo.h index b5cae9308c..6205ba44a6 100644 --- a/packages/skia/cpp/api/JsiVideo.h +++ b/packages/skia/cpp/api/JsiVideo.h @@ -6,7 +6,7 @@ #include #include "JsiSkHostObjects.h" -#include "RNSkLog.h" +#include "utils/RNSkLog.h" #include #include "JsiSkPaint.h" @@ -14,7 +14,7 @@ #include "JsiSkRect.h" #include "JsiSkTypeface.h" -#include "RNSkVideo.h" +#include "rnskia/RNSkVideo.h" #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdocumentation" diff --git a/packages/skia/cpp/api/recorder/Convertor.h b/packages/skia/cpp/api/recorder/Convertor.h index efd4c61f2f..0e58c16e4e 100644 --- a/packages/skia/cpp/api/recorder/Convertor.h +++ b/packages/skia/cpp/api/recorder/Convertor.h @@ -16,7 +16,7 @@ #include #include "../CustomBlendModes.h" -#include "third_party/CSSColorParser.h" +#include "api/third_party/CSSColorParser.h" #include "DataTypes.h" diff --git a/packages/skia/cpp/api/recorder/Drawings.h b/packages/skia/cpp/api/recorder/Drawings.h index 681555ef91..c5ce24b14c 100644 --- a/packages/skia/cpp/api/recorder/Drawings.h +++ b/packages/skia/cpp/api/recorder/Drawings.h @@ -6,7 +6,7 @@ #include "Convertor.h" #include "DrawingCtx.h" #include "ImageFit.h" -#include "RNSkPlatformContext.h" +#include "rnskia/RNSkPlatformContext.h" #include "include/core/SkPathBuilder.h" #include "include/core/SkStrokeRec.h" diff --git a/packages/skia/cpp/api/recorder/JsiRecorder.h b/packages/skia/cpp/api/recorder/JsiRecorder.h index 46ca09ecd0..742eebacc9 100644 --- a/packages/skia/cpp/api/recorder/JsiRecorder.h +++ b/packages/skia/cpp/api/recorder/JsiRecorder.h @@ -5,13 +5,13 @@ #include #include -#include "JsiSkCanvas.h" -#include "JsiSkHostObjects.h" -#include "JsiSkPicture.h" +#include "api/JsiSkCanvas.h" +#include "api/JsiSkHostObjects.h" +#include "api/JsiSkPicture.h" #include "DrawingCtx.h" #include "RNRecorder.h" -#include "RNSkLog.h" +#include "utils/RNSkLog.h" #include diff --git a/packages/skia/cpp/api/recorder/RNRecorder.h b/packages/skia/cpp/api/recorder/RNRecorder.h index d96331f088..18a4ef7300 100644 --- a/packages/skia/cpp/api/recorder/RNRecorder.h +++ b/packages/skia/cpp/api/recorder/RNRecorder.h @@ -17,7 +17,7 @@ #include "ImageFilters.h" #include "Paint.h" #include "PathEffects.h" -#include "RNSkPlatformContext.h" +#include "rnskia/RNSkPlatformContext.h" #include "Shaders.h" namespace RNSkia { diff --git a/packages/skia/cpp/api/third_party/SkottieUtils.cpp b/packages/skia/cpp/api/third_party/SkottieUtils.cpp index dc763ac08b..98d8156e3f 100644 --- a/packages/skia/cpp/api/third_party/SkottieUtils.cpp +++ b/packages/skia/cpp/api/third_party/SkottieUtils.cpp @@ -5,7 +5,7 @@ * found in the LICENSE file. */ -#include "third_party/SkottieUtils.h" +#include "SkottieUtils.h" #include "include/core/SkData.h" #include "include/core/SkRect.h" diff --git a/packages/skia/cpp/api/third_party/base64.cpp b/packages/skia/cpp/api/third_party/base64.cpp index ac02ac2c91..a89403433a 100644 --- a/packages/skia/cpp/api/third_party/base64.cpp +++ b/packages/skia/cpp/api/third_party/base64.cpp @@ -4,7 +4,7 @@ * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ -#include "third_party/base64.h" +#include "base64.h" #include diff --git a/packages/skia/cpp/jsi/JsiHostObject.cpp b/packages/skia/cpp/jsi/JsiHostObject.cpp index ef1654d45b..4c4ee26f61 100644 --- a/packages/skia/cpp/jsi/JsiHostObject.cpp +++ b/packages/skia/cpp/jsi/JsiHostObject.cpp @@ -24,12 +24,6 @@ void JsiHostObject::set(jsi::Runtime &rt, const jsi::PropNameID &name, } } -jsi::Value eval(jsi::Runtime &runtime, const std::string &js) { - return runtime.global() - .getPropertyAsFunction(runtime, "eval") - .call(runtime, js); -} - jsi::Value JsiHostObject::get(jsi::Runtime &runtime, const jsi::PropNameID &name) { auto nameStr = name.utf8(runtime); @@ -86,7 +80,11 @@ jsi::Value JsiHostObject::get(jsi::Runtime &runtime, // Check for dispose symbol as last resort static const auto disposeSymbol = jsi::PropNameID::forSymbol( runtime, - eval(runtime, "Symbol.for('Symbol.dispose');").getSymbol(runtime)); + runtime.global() + .getPropertyAsObject(runtime, "Symbol") + .getPropertyAsFunction(runtime, "for") + .call(runtime, "Symbol.dispose") + .getSymbol(runtime)); if (jsi::PropNameID::compare(runtime, disposeSymbol, name)) { // Recursively call get with "dispose" string auto disposeName = jsi::PropNameID::forAscii(runtime, "dispose"); diff --git a/packages/skia/cpp/jsi/JsiPromises.h b/packages/skia/cpp/jsi/JsiPromises.h index aa3c07e82a..f2ef6cb089 100644 --- a/packages/skia/cpp/jsi/JsiPromises.h +++ b/packages/skia/cpp/jsi/JsiPromises.h @@ -6,7 +6,7 @@ #include -#include "third_party/base64.h" +#include "api/third_party/base64.h" namespace RNJsi { namespace jsi = facebook::jsi; diff --git a/packages/skia/cpp/jsi/ViewProperty.h b/packages/skia/cpp/jsi/ViewProperty.h index 25ae8c0f5b..a361086cfd 100644 --- a/packages/skia/cpp/jsi/ViewProperty.h +++ b/packages/skia/cpp/jsi/ViewProperty.h @@ -7,7 +7,7 @@ #include #include -#include "JsiSkPicture.h" +#include "api/JsiSkPicture.h" #include "RuntimeLifecycleMonitor.h" namespace RNJsi { diff --git a/packages/skia/cpp/rnskia/RNDawnContext.h b/packages/skia/cpp/rnskia/RNDawnContext.h index a12855e224..94b27cff8a 100644 --- a/packages/skia/cpp/rnskia/RNDawnContext.h +++ b/packages/skia/cpp/rnskia/RNDawnContext.h @@ -138,6 +138,15 @@ class DawnContext { wgpu::SharedTextureMemoryBeginAccessDescriptor beginAccessDesc; beginAccessDesc.initialized = true; beginAccessDesc.fenceCount = 0; +#if defined(__ANDROID__) + // Dawn's Vulkan backend requires the acquired VkImageLayout to be chained. + // UNDEFINED (= 0) on both ends is the canonical "no prior GPU producer" + // pattern (matches GPUSharedTextureMemory::beginAccess). + wgpu::SharedTextureMemoryVkImageLayoutBeginState vkBegin = {}; + vkBegin.oldLayout = 0; + vkBegin.newLayout = 0; + beginAccessDesc.nextInChain = &vkBegin; +#endif bool success = memory.BeginAccess(texture, &beginAccessDesc); if (success) { @@ -149,6 +158,10 @@ class DawnContext { [](void *context) { auto ctx = static_cast(context); wgpu::SharedTextureMemoryEndAccessState endState = {}; +#if defined(__ANDROID__) + wgpu::SharedTextureMemoryVkImageLayoutEndState vkEnd = {}; + endState.nextInChain = &vkEnd; +#endif ctx->sharedTextureMemory.EndAccess(ctx->texture, &endState); delete ctx; }, diff --git a/packages/skia/cpp/rnskia/RNDawnUtils.h b/packages/skia/cpp/rnskia/RNDawnUtils.h index 55534d81f0..dffc54a9ea 100644 --- a/packages/skia/cpp/rnskia/RNDawnUtils.h +++ b/packages/skia/cpp/rnskia/RNDawnUtils.h @@ -5,7 +5,7 @@ #include "dawn/dawn_proc.h" #include "dawn/native/DawnNative.h" -#include "RNSkLog.h" +#include "utils/RNSkLog.h" #include "include/core/SkColorType.h" #include "include/gpu/graphite/dawn/DawnBackendContext.h" @@ -206,12 +206,22 @@ createDawnBackendContext(dawn::native::Instance *instance) { wgpu::FeatureName::ImplicitDeviceSynchronization, #ifdef __APPLE__ wgpu::FeatureName::SharedTextureMemoryIOSurface, + // Required to call SharedTextureMemory::EndAccess on Metal (it exports a + // MTLSharedEvent fence). importExternalTexture / importSharedTextureMemory + // end the access window after submit; without this EndAccess errors with + // "Required feature (SharedFenceMTLSharedEvent) is missing". Safe here + // because we always queue.submit() before EndAccess (the secondary device + // omits it on purpose — its camera path doesn't commit first). + wgpu::FeatureName::SharedFenceMTLSharedEvent, wgpu::FeatureName::DawnMultiPlanarFormats, wgpu::FeatureName::MultiPlanarFormatP010, wgpu::FeatureName::MultiPlanarFormatP210, wgpu::FeatureName::MultiPlanarFormatExtendedUsages, #else wgpu::FeatureName::SharedTextureMemoryAHardwareBuffer, + // Vulkan equivalent of the above: EndAccess exports a sync-fd fence. + wgpu::FeatureName::SharedFenceSyncFD, + wgpu::FeatureName::SharedFenceVkSemaphoreOpaqueFD, #endif }; diff --git a/packages/skia/cpp/rnskia/RNSkJsiViewApi.h b/packages/skia/cpp/rnskia/RNSkJsiViewApi.h index 6a18a5b336..9bcf60ed68 100644 --- a/packages/skia/cpp/rnskia/RNSkJsiViewApi.h +++ b/packages/skia/cpp/rnskia/RNSkJsiViewApi.h @@ -9,11 +9,11 @@ #include #include -#include "JsiHostObject.h" +#include "jsi/JsiHostObject.h" #include "RNSkPictureView.h" #include "RNSkPlatformContext.h" #include "RNSkView.h" -#include "ViewProperty.h" +#include "jsi/ViewProperty.h" #include namespace RNSkia { diff --git a/packages/skia/cpp/rnskia/RNSkManager.cpp b/packages/skia/cpp/rnskia/RNSkManager.cpp index 60ac52777a..8eb939b1e6 100644 --- a/packages/skia/cpp/rnskia/RNSkManager.cpp +++ b/packages/skia/cpp/rnskia/RNSkManager.cpp @@ -5,22 +5,29 @@ #include -#include "JsiSkApi.h" +#include "api/JsiSkApi.h" #include "RNSkJsiViewApi.h" #include "RNSkView.h" -#include "RuntimeAwareCache.h" +#include "jsi/RuntimeAwareCache.h" #ifdef SK_GRAPHITE #include "RNDawnContext.h" +#include "rnwgpu/ArrayBuffer.h" #include "rnwgpu/api/GPU.h" #include "rnwgpu/api/GPUUncapturedErrorEvent.h" +#include "rnwgpu/api/ImageBitmap.h" #include "rnwgpu/api/RNWebGPU.h" #include "rnwgpu/api/descriptors/GPUBufferUsage.h" #include "rnwgpu/api/descriptors/GPUColorWrite.h" #include "rnwgpu/api/descriptors/GPUMapMode.h" #include "rnwgpu/api/descriptors/GPUShaderStage.h" #include "rnwgpu/api/descriptors/GPUTextureUsage.h" +#include "jsi2/Promise.h" + +#include "include/core/SkData.h" +#include "include/core/SkImage.h" +#include "include/core/SkImageInfo.h" #endif namespace RNSkia { @@ -114,6 +121,85 @@ void RNSkManager::installBindings() { auto rnWebGPU = std::make_shared(gpu, nullptr); _jsRuntime->global().setProperty( *_jsRuntime, "RNWebGPU", rnwgpu::RNWebGPU::create(*_jsRuntime, rnWebGPU)); + + // DRAFT — compile-unverified. Install the ImageBitmap constructor (so + // `instanceof ImageBitmap` works) and a global createImageBitmap() that + // accepts the non-standard encoded-BufferSource overload. + // + // The BufferSource is run through the shared rnwgpu::ArrayBuffer converter, + // which validates byteOffset/byteLength against the backing buffer and throws + // synchronously on a spoofed / out-of-bounds view — so createImageBitmap() + // rejects rather than reading out of bounds (see ArrayBufferBounds / + // ImageBitmapBounds specs). Decoding uses Skia's own codec; no platform image + // decoder is needed. + rnwgpu::ImageBitmap::installConstructor(*_jsRuntime); + _jsRuntime->global().setProperty( + *_jsRuntime, "createImageBitmap", + jsi::Function::createFromHostFunction( + *_jsRuntime, + jsi::PropNameID::forAscii(*_jsRuntime, "createImageBitmap"), 1, + [](jsi::Runtime &rt, const jsi::Value & /*thisVal*/, + const jsi::Value *args, size_t count) -> jsi::Value { + if (count < 1 || !args[0].isObject()) { + throw jsi::JSError( + rt, "createImageBitmap requires a BufferSource argument"); + } + // Only the encoded ArrayBuffer / ArrayBufferView overload is + // supported here. Anything else (Blob, ImageData, …) is rejected. + auto obj = args[0].getObject(rt); + bool isBufferSource = obj.isArrayBuffer(rt); + if (!isBufferSource && obj.hasProperty(rt, "buffer")) { + auto bufferProp = obj.getProperty(rt, "buffer"); + isBufferSource = + bufferProp.isObject() && + bufferProp.getObject(rt).isArrayBuffer(rt); + } + if (!isBufferSource) { + throw jsi::JSError(rt, "createImageBitmap: unsupported source " + "(expected an ArrayBuffer or TypedArray " + "of encoded image bytes)"); + } + // Validates bounds and THROWS synchronously on a spoofed view, so + // the bad pointer never reaches the copy below. + auto buffer = + rnwgpu::JSIConverter>:: + fromJSI(rt, args[0], false); + // Copy the encoded bytes off the JS-owned ArrayBuffer. + const uint8_t *bytes = buffer->data(); + std::vector encoded(bytes, bytes + buffer->size()); + + return rnwgpu::Promise::createPromise( + rt, [encoded = std::move(encoded)]( + jsi::Runtime &runtime, + std::shared_ptr promise) mutable { + auto skData = + SkData::MakeWithCopy(encoded.data(), encoded.size()); + auto image = SkImages::DeferredFromEncodedData(skData); + if (image == nullptr) { + promise->reject( + "createImageBitmap: failed to decode image data"); + return; + } + const int w = image->width(); + const int h = image->height(); + auto info = + SkImageInfo::Make(w, h, kRGBA_8888_SkColorType, + kUnpremul_SkAlphaType); + std::vector pixels(info.computeMinByteSize()); + // nullptr context: decode/read on the CPU (raster). + if (!image->readPixels(nullptr, info, pixels.data(), + info.minRowBytes(), 0, 0)) { + promise->reject( + "createImageBitmap: failed to read decoded pixels"); + return; + } + auto bitmap = std::make_shared( + std::move(pixels), static_cast(w), + static_cast(h)); + promise->resolve( + rnwgpu::ImageBitmap::create(runtime, bitmap)); + }); + })); #endif } } // namespace RNSkia diff --git a/packages/skia/cpp/rnskia/RNSkPictureView.h b/packages/skia/cpp/rnskia/RNSkPictureView.h index 0059ea1c7e..8cc91b690b 100644 --- a/packages/skia/cpp/rnskia/RNSkPictureView.h +++ b/packages/skia/cpp/rnskia/RNSkPictureView.h @@ -11,12 +11,12 @@ #include #include "RNSkView.h" -#include "ViewProperty.h" +#include "jsi/ViewProperty.h" -#include "JsiSkPicture.h" -#include "RNSkLog.h" +#include "api/JsiSkPicture.h" +#include "utils/RNSkLog.h" #include "RNSkPlatformContext.h" -#include "RNSkTimingInfo.h" +#include "utils/RNSkTimingInfo.h" #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdocumentation" diff --git a/packages/skia/cpp/rnskia/RNSkPlatformContext.h b/packages/skia/cpp/rnskia/RNSkPlatformContext.h index 596bb93f11..f74e190e23 100644 --- a/packages/skia/cpp/rnskia/RNSkPlatformContext.h +++ b/packages/skia/cpp/rnskia/RNSkPlatformContext.h @@ -141,6 +141,13 @@ class RNSkPlatformContext { virtual uint64_t makeNativeBuffer(sk_sp image) = 0; + // Allocate a platform native buffer (IOSurface on Apple, AHardwareBuffer on + // Android) of the given size filled with a procedural test pattern (RGB + // gradient + diagonal stripes), entirely on the CPU. Intended for examples + // and tests that need a buffer to feed into importExternalTexture without a + // camera/video source. Release it with releaseNativeBuffer(). + virtual uint64_t makeTestNativeBuffer(int width, int height) = 0; + virtual std::shared_ptr createVideo(const std::string &url) = 0; /** diff --git a/packages/skia/cpp/rnskia/RNSkView.h b/packages/skia/cpp/rnskia/RNSkView.h index 5181c713c7..ba92d75f1d 100644 --- a/packages/skia/cpp/rnskia/RNSkView.h +++ b/packages/skia/cpp/rnskia/RNSkView.h @@ -7,11 +7,11 @@ #include #include "RNSkPlatformContext.h" -#include "ViewProperty.h" +#include "jsi/ViewProperty.h" -#include "JsiSkImage.h" -#include "JsiSkPoint.h" -#include "JsiSkRect.h" +#include "api/JsiSkImage.h" +#include "api/JsiSkPoint.h" +#include "api/JsiSkRect.h" #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdocumentation" @@ -94,8 +94,11 @@ class RNSkOffscreenCanvasProvider : public RNSkCanvasProvider { image = _surface->makeImageSnapshot(); } #if defined(SK_GRAPHITE) - DawnContext::getInstance().submitRecording( - _surface->recorder()->snap().get()); + // Only Graphite-backed surfaces have a recorder to snap/submit; a raster + // surface's snapshot is already a valid CPU image. + if (auto *recorder = _surface->recorder()) { + DawnContext::getInstance().submitRecording(recorder->snap().get()); + } return DawnContext::getInstance().MakeRasterImage(image); #else auto grContext = _context->getDirectContext(); diff --git a/packages/skia/cpp/rnwgpu/ArrayBuffer.h b/packages/skia/cpp/rnwgpu/ArrayBuffer.h index 0cdc6098af..2f3d949887 100644 --- a/packages/skia/cpp/rnwgpu/ArrayBuffer.h +++ b/packages/skia/cpp/rnwgpu/ArrayBuffer.h @@ -1,6 +1,8 @@ #pragma once #include +#include +#include #include #include "jsi2/JSIConverter.h" @@ -47,16 +49,58 @@ template <> struct JSIConverter> { if (bufferProp.isObject() && bufferProp.getObject(runtime).isArrayBuffer(runtime)) { auto buff = bufferProp.getObject(runtime); - auto bytesPerElements = - obj.getProperty(runtime, "BYTES_PER_ELEMENT").asNumber(); auto arrayBuffer = buff.getArrayBuffer(runtime); - auto byteOffset = static_cast( - obj.getProperty(runtime, "byteOffset").asNumber()); - auto byteLength = static_cast( - obj.getProperty(runtime, "byteLength").asNumber()); + const size_t bufferSize = arrayBuffer.size(runtime); + + // byteOffset / byteLength are user-readable JS properties, not values + // the engine guarantees (unlike Dawn's node binding, which reads them + // off the engine's typed-array view). Read them as doubles so we can + // reject negative, non-integral, NaN/Inf, or oversized values before + // they wrap around when cast to size_t. + const double byteOffsetValue = + obj.getProperty(runtime, "byteOffset").asNumber(); + const double byteLengthValue = + obj.getProperty(runtime, "byteLength").asNumber(); + + auto isValidByteIndex = [](double value) { + return std::isfinite(value) && value >= 0.0 && + value <= static_cast(SIZE_MAX) && + std::floor(value) == value; + }; + if (!isValidByteIndex(byteOffsetValue) || + !isValidByteIndex(byteLengthValue)) { + throw std::runtime_error( + "ArrayBuffer::fromJSI: invalid byteOffset/byteLength"); + } + + const size_t byteOffset = static_cast(byteOffsetValue); + const size_t byteLength = static_cast(byteLengthValue); + + // Overflow-safe bounds check: byteOffset + byteLength <= bufferSize. + if (byteOffset > bufferSize || + byteLength > bufferSize - byteOffset) { + throw std::runtime_error( + "ArrayBuffer::fromJSI: view bounds [byteOffset, byteOffset + " + "byteLength) exceed the backing ArrayBuffer size"); + } + + // BYTES_PER_ELEMENT is absent on a DataView; default to 1. A spoofed + // object could report 0 (or a negative/NaN value), so clamp to a + // minimum of 1 to avoid a later division by zero in writeBuffer. + size_t bytesPerElements = 1; + if (obj.hasProperty(runtime, "BYTES_PER_ELEMENT")) { + auto bpe = obj.getProperty(runtime, "BYTES_PER_ELEMENT"); + if (bpe.isNumber()) { + const double value = bpe.asNumber(); + if (std::isfinite(value) && value >= 1.0) { + bytesPerElements = static_cast(value); + } + } + } + return std::make_shared( arrayBuffer.data(runtime) + byteOffset, byteLength, - static_cast(bytesPerElements)); + bytesPerElements); } } } diff --git a/packages/skia/cpp/rnwgpu/api/AppleNativeBuffer.h b/packages/skia/cpp/rnwgpu/api/AppleNativeBuffer.h new file mode 100644 index 0000000000..0393707503 --- /dev/null +++ b/packages/skia/cpp/rnwgpu/api/AppleNativeBuffer.h @@ -0,0 +1,22 @@ +#pragma once + +#if defined(__APPLE__) + +#include + +namespace rnwgpu { + +// Extract the backing IOSurface and dimensions from a CVPixelBufferRef pointer +// (the value Skia's NativeBuffer.MakeFromImage returns on Apple). The returned +// IOSurfaceRef is owned by the CVPixelBuffer; the caller must keep the +// CVPixelBuffer alive while the IOSurface is in use. Returns nullptr (and +// leaves the out-params at 0) if the buffer has no IOSurface. +// +// Defined in apple/RNWebGPUAppleNativeBuffer.mm (Objective-C++ so it can use +// CoreVideo, which isn't available from the cpp/ translation units). +void *GetIOSurfaceFromNativeBuffer(void *cvPixelBuffer, uint32_t *outWidth, + uint32_t *outHeight); + +} // namespace rnwgpu + +#endif // __APPLE__ diff --git a/packages/skia/cpp/rnwgpu/api/Convertors.h b/packages/skia/cpp/rnwgpu/api/Convertors.h index 4c23b3c2ed..33a6d692d6 100644 --- a/packages/skia/cpp/rnwgpu/api/Convertors.h +++ b/packages/skia/cpp/rnwgpu/api/Convertors.h @@ -252,13 +252,24 @@ class Convertor { [[nodiscard]] bool Convert(wgpu::BindGroupLayoutEntry &out, const GPUBindGroupLayoutEntry &in) { - return Convert(out.binding, in.binding) && - Convert(out.visibility, in.visibility) && - Convert(out.buffer, in.buffer) && Convert(out.sampler, in.sampler) && - Convert(out.texture, in.texture) && - Convert(out.storageTexture, in.storageTexture); - // no external textures here - //&& Convert(out.externalTexture, in.externalTexture); + out = {}; + if (!Convert(out.binding, in.binding) || + !Convert(out.visibility, in.visibility) || + !Convert(out.buffer, in.buffer) || !Convert(out.sampler, in.sampler) || + !Convert(out.texture, in.texture) || + !Convert(out.storageTexture, in.storageTexture)) { + return false; + } + if (in.externalTexture.has_value() && + in.externalTexture.value() != nullptr) { + // External texture layouts bind via a chained struct rather than a + // direct field on BindGroupLayoutEntry. The chained struct must outlive + // the BindGroupLayoutEntry until Device::CreateBindGroupLayout returns, + // so we allocate it on the Convertor's arena. + auto *chain = Allocate(); + out.nextInChain = chain; + } + return true; } [[nodiscard]] bool Convert(wgpu::BlendComponent &out, @@ -422,9 +433,11 @@ class Convertor { } [[nodiscard]] bool Convert(wgpu::ExternalTextureBindingLayout &out, - const GPUExternalTextureBindingLayout &in) { - // no external textures at the moment - return false; + const GPUExternalTextureBindingLayout & /*in*/) { + // ExternalTextureBindingLayout carries no fields of its own; its presence + // (as a chained struct) is what marks the entry as an external texture. + out = {}; + return true; } [[nodiscard]] bool Convert(wgpu::ConstantEntry &out, const std::string &key, @@ -729,7 +742,16 @@ class Convertor { out.buffer = buffer->get(); return true; } - // Not external textures at the moment + if (in.externalTexture != nullptr) { + // External textures bind via a chained struct rather than a direct field + // on BindGroupEntry. The chained struct must outlive the BindGroupEntry + // until Device::CreateBindGroup returns, so we allocate it on the + // Convertor's arena. + auto *chain = Allocate(); + chain->externalTexture = in.externalTexture->get(); + out.nextInChain = chain; + return true; + } return false; } diff --git a/packages/skia/cpp/rnwgpu/api/GPU.cpp b/packages/skia/cpp/rnwgpu/api/GPU.cpp index 9fa5ac8f63..258389ad98 100644 --- a/packages/skia/cpp/rnwgpu/api/GPU.cpp +++ b/packages/skia/cpp/rnwgpu/api/GPU.cpp @@ -136,9 +136,6 @@ std::unordered_set GPU::getWgslLanguageFeatures() { case wgpu::WGSLLanguageFeatureName::BufferView: name = "buffer_view"; break; - case wgpu::WGSLLanguageFeatureName::FilteringParameters: - name = "filtering_parameters"; - break; case wgpu::WGSLLanguageFeatureName::SwizzleAssignment: name = "swizzle_assignment"; break; diff --git a/packages/skia/cpp/rnwgpu/api/GPUAdapter.cpp b/packages/skia/cpp/rnwgpu/api/GPUAdapter.cpp index 195b143336..8d3610f9fa 100644 --- a/packages/skia/cpp/rnwgpu/api/GPUAdapter.cpp +++ b/packages/skia/cpp/rnwgpu/api/GPUAdapter.cpp @@ -109,13 +109,39 @@ async::AsyncTaskHandle GPUAdapter::requestDevice( deviceLostBinding, creationRuntime](const async::AsyncTaskHandle::ResolveFunction &resolve, const async::AsyncTaskHandle::RejectFunction &reject) { - (void)descriptor; + // Build a local mutable copy so we can chain Dawn's device toggles. + // The toggle name strings are owned by `descriptor` (captured above), + // and the const char* / DawnTogglesDescriptor locals live for the + // whole synchronous RequestDevice call below, which is when Dawn reads + // the chained struct. + wgpu::DeviceDescriptor deviceDesc = aDescriptor; + wgpu::DawnTogglesDescriptor toggles{}; + std::vector enabledToggles; + std::vector disabledToggles; + if (descriptor.has_value() && descriptor.value()->dawnToggles) { + const auto &dawnToggles = descriptor.value()->dawnToggles.value(); + if (dawnToggles->enabledToggles) { + for (const auto &t : dawnToggles->enabledToggles.value()) { + enabledToggles.push_back(t.c_str()); + } + toggles.enabledToggleCount = enabledToggles.size(); + toggles.enabledToggles = enabledToggles.data(); + } + if (dawnToggles->disabledToggles) { + for (const auto &t : dawnToggles->disabledToggles.value()) { + disabledToggles.push_back(t.c_str()); + } + toggles.disabledToggleCount = disabledToggles.size(); + toggles.disabledToggles = disabledToggles.data(); + } + deviceDesc.nextInChain = &toggles; + } _instance.RequestDevice( - &aDescriptor, wgpu::CallbackMode::AllowProcessEvents, + &deviceDesc, wgpu::CallbackMode::AllowProcessEvents, [asyncRunner = _async, resolve, reject, label, creationRuntime, deviceLostBinding](wgpu::RequestDeviceStatus status, wgpu::Device device, - wgpu::StringView message) mutable { + wgpu::StringView message) { if (message.length) { fprintf(stderr, "%s", message.data); } @@ -128,10 +154,13 @@ async::AsyncTaskHandle GPUAdapter::requestDevice( return; } + // SetLoggingCallback is a repeatable callback (no callback mode), + // which rejects capturing lambdas. Pass the runtime pointer + // through Dawn's userdata argument instead of capturing it. device.SetLoggingCallback( - [creationRuntime](wgpu::LoggingType type, - wgpu::StringView msg) { - if (creationRuntime == nullptr) { + [](wgpu::LoggingType type, wgpu::StringView msg, + jsi::Runtime *runtime) { + if (runtime == nullptr) { return; } const char *logLevel = ""; @@ -157,7 +186,8 @@ async::AsyncTaskHandle GPUAdapter::requestDevice( fprintf(stderr, "%s: %.*s\n", logLevel, static_cast(msg.length), msg.data); } - }); + }, + creationRuntime); auto deviceHost = std::make_shared(std::move(device), asyncRunner, label); diff --git a/packages/skia/cpp/rnwgpu/api/GPUBuffer.h b/packages/skia/cpp/rnwgpu/api/GPUBuffer.h index f8919f9469..2706a50490 100644 --- a/packages/skia/cpp/rnwgpu/api/GPUBuffer.h +++ b/packages/skia/cpp/rnwgpu/api/GPUBuffer.h @@ -14,7 +14,7 @@ #include "webgpu/webgpu_cpp.h" -#include "ArrayBuffer.h" +#include "rnwgpu/ArrayBuffer.h" namespace rnwgpu { diff --git a/packages/skia/cpp/rnwgpu/api/GPUDevice.cpp b/packages/skia/cpp/rnwgpu/api/GPUDevice.cpp index 19e2b1d977..436f1dd7ea 100644 --- a/packages/skia/cpp/rnwgpu/api/GPUDevice.cpp +++ b/packages/skia/cpp/rnwgpu/api/GPUDevice.cpp @@ -7,6 +7,7 @@ #include #include "Convertors.h" +#include "NativeBufferUtils.h" #include "jsi2/JSIConverter.h" #include "GPUFeatures.h" @@ -234,8 +235,83 @@ std::shared_ptr GPUDevice::createPipelineLayout( std::shared_ptr GPUDevice::importExternalTexture( std::shared_ptr descriptor) { - throw std::runtime_error( - "GPUDevice::importExternalTexture(): Not implemented"); + // The import / begin-access / descriptor-build logic, plus the matching + // EndAccess, all live on GPUExternalTexture so the begin/end lifecycle stays + // in one translation unit (see GPUExternalTexture.cpp). + return GPUExternalTexture::Create(_instance, std::move(descriptor)); +} + +std::shared_ptr GPUDevice::importSharedTextureMemory( + std::shared_ptr descriptor) { + if (!descriptor || descriptor->handle == 0) { + throw std::runtime_error( + "GPUDevice::importSharedTextureMemory(): handle must be a non-null " + "native buffer pointer (from Skia.NativeBuffer.MakeFromImage)"); + } + void *bufferPtr = + reinterpret_cast(static_cast(descriptor->handle)); + std::string label = descriptor->label.value_or(""); + + auto memory = importNativeBufferAsSharedTextureMemory( + _instance, bufferPtr, label, /*outWidth=*/nullptr, /*outHeight=*/nullptr); + if (memory == nullptr) { + throw std::runtime_error( + "GPUDevice::importSharedTextureMemory(): ImportSharedTextureMemory " + "returned null - is the 'shared-texture-memory-iosurface' (Apple) or " + "'shared-texture-memory-ahardware-buffer' (Android) feature enabled on " + "the device?"); + } + return std::make_shared(std::move(memory), + std::move(label)); +} + +std::shared_ptr GPUDevice::importSharedFence( + std::shared_ptr descriptor) { + if (!descriptor || descriptor->handle == nullptr) { + throw std::runtime_error("GPUDevice::importSharedFence(): handle must be a " + "non-null native handle"); + } + + wgpu::SharedFenceDescriptor desc{}; + std::string label = descriptor->label.value_or(""); + if (!label.empty()) { + desc.label = wgpu::StringView(label.c_str(), label.size()); + } + + // The chained platform descriptor must outlive the synchronous + // ImportSharedFence() below; declare them all and chain the matching one. + wgpu::SharedFenceMTLSharedEventDescriptor mtlDesc{}; + wgpu::SharedFenceSyncFDDescriptor syncFdDesc{}; + wgpu::SharedFenceVkSemaphoreOpaqueFDDescriptor vkFdDesc{}; + + const std::string &type = descriptor->type; + if (type == "mtl-shared-event") { + // handle is an id pointer. + mtlDesc.sharedEvent = descriptor->handle; + desc.nextInChain = &mtlDesc; + } else if (type == "sync-fd") { + // handle is an OS file descriptor. + syncFdDesc.handle = + static_cast(reinterpret_cast(descriptor->handle)); + desc.nextInChain = &syncFdDesc; + } else if (type == "vk-semaphore-opaque-fd") { + vkFdDesc.handle = + static_cast(reinterpret_cast(descriptor->handle)); + desc.nextInChain = &vkFdDesc; + } else { + throw std::runtime_error( + "GPUDevice::importSharedFence(): unsupported fence type '" + type + + "' (expected 'mtl-shared-event', 'sync-fd' or " + "'vk-semaphore-opaque-fd')"); + } + + auto fence = _instance.ImportSharedFence(&desc); + if (fence == nullptr) { + throw std::runtime_error( + "GPUDevice::importSharedFence(): ImportSharedFence returned null - is " + "the matching 'shared-fence-*' feature enabled on the device?"); + } + return std::make_shared(std::move(fence), std::move(label)); } async::AsyncTaskHandle GPUDevice::createComputePipelineAsync( @@ -262,7 +338,7 @@ async::AsyncTaskHandle GPUDevice::createComputePipelineAsync( &desc, wgpu::CallbackMode::AllowProcessEvents, [pipelineHolder, resolve, reject](wgpu::CreatePipelineAsyncStatus status, - wgpu::ComputePipeline pipeline, const char *msg) mutable { + wgpu::ComputePipeline pipeline, wgpu::StringView msg) { if (status == wgpu::CreatePipelineAsyncStatus::Success && pipeline) { pipelineHolder->_instance = pipeline; resolve([pipelineHolder](jsi::Runtime &runtime) mutable { @@ -271,7 +347,8 @@ async::AsyncTaskHandle GPUDevice::createComputePipelineAsync( }); } else { std::string error = - msg ? std::string(msg) : "Failed to create compute pipeline"; + msg.length ? std::string(msg.data, msg.length) + : "Failed to create compute pipeline"; reject(std::move(error)); } }); @@ -303,7 +380,7 @@ async::AsyncTaskHandle GPUDevice::createRenderPipelineAsync( &desc, wgpu::CallbackMode::AllowProcessEvents, [pipelineHolder, resolve, reject](wgpu::CreatePipelineAsyncStatus status, - wgpu::RenderPipeline pipeline, const char *msg) mutable { + wgpu::RenderPipeline pipeline, wgpu::StringView msg) { if (status == wgpu::CreatePipelineAsyncStatus::Success && pipeline) { pipelineHolder->_instance = pipeline; resolve([pipelineHolder](jsi::Runtime &runtime) mutable { @@ -312,7 +389,8 @@ async::AsyncTaskHandle GPUDevice::createRenderPipelineAsync( }); } else { std::string error = - msg ? std::string(msg) : "Failed to create render pipeline"; + msg.length ? std::string(msg.data, msg.length) + : "Failed to create render pipeline"; reject(std::move(error)); } }); diff --git a/packages/skia/cpp/rnwgpu/api/GPUDevice.h b/packages/skia/cpp/rnwgpu/api/GPUDevice.h index 4c10eee683..834f245ee7 100644 --- a/packages/skia/cpp/rnwgpu/api/GPUDevice.h +++ b/packages/skia/cpp/rnwgpu/api/GPUDevice.h @@ -37,6 +37,8 @@ #include "GPURenderPipeline.h" #include "GPUSampler.h" #include "GPUShaderModule.h" +#include "GPUSharedFence.h" +#include "GPUSharedTextureMemory.h" #include "GPUSupportedLimits.h" #include "GPUTexture.h" #include "descriptors/GPUBindGroupDescriptor.h" @@ -51,6 +53,8 @@ #include "descriptors/GPURenderPipelineDescriptor.h" #include "descriptors/GPUSamplerDescriptor.h" #include "descriptors/GPUShaderModuleDescriptor.h" +#include "descriptors/GPUSharedFenceDescriptor.h" +#include "descriptors/GPUSharedTextureMemoryDescriptor.h" #include "descriptors/GPUTextureDescriptor.h" namespace rnwgpu { @@ -120,6 +124,10 @@ class GPUDevice : public NativeObject { std::optional> descriptor); std::shared_ptr importExternalTexture( std::shared_ptr descriptor); + std::shared_ptr importSharedTextureMemory( + std::shared_ptr descriptor); + std::shared_ptr + importSharedFence(std::shared_ptr descriptor); std::shared_ptr createBindGroupLayout( std::shared_ptr descriptor); std::shared_ptr @@ -173,6 +181,10 @@ class GPUDevice : public NativeObject { &GPUDevice::createSampler); installMethod(runtime, prototype, "importExternalTexture", &GPUDevice::importExternalTexture); + installMethod(runtime, prototype, "importSharedTextureMemory", + &GPUDevice::importSharedTextureMemory); + installMethod(runtime, prototype, "importSharedFence", + &GPUDevice::importSharedFence); installMethod(runtime, prototype, "createBindGroupLayout", &GPUDevice::createBindGroupLayout); installMethod(runtime, prototype, "createPipelineLayout", diff --git a/packages/skia/cpp/rnwgpu/api/GPUExternalTexture.cpp b/packages/skia/cpp/rnwgpu/api/GPUExternalTexture.cpp new file mode 100644 index 0000000000..131b826f9a --- /dev/null +++ b/packages/skia/cpp/rnwgpu/api/GPUExternalTexture.cpp @@ -0,0 +1,139 @@ +#include "GPUExternalTexture.h" + +#include +#include +#include +#include + +#include "NativeBufferUtils.h" +#include "descriptors/GPUExternalTextureDescriptor.h" + +namespace rnwgpu { + +// Identity gamut (same primaries) as a 3x3 column-major matrix. +static const float kIdentityGamutMatrix[9] = { + 1.0f, 0.0f, 0.0f, // + 0.0f, 1.0f, 0.0f, // + 0.0f, 0.0f, 1.0f, // +}; + +// Identity transfer (y = x). The native buffers produced by Skia's NativeBuffer +// API are single-plane BGRA/RGBA already in the render target's color space, so +// no conversion is wanted. Dawn dereferences the transfer-function arrays +// unconditionally (ComputeExternalTextureParams), so these must be non-null. +static const float kIdentityTransferParams[7] = { + 1.0f, // G + 1.0f, // A + 0.0f, // B + 0.0f, // C + 0.0f, // D + 0.0f, // E + 0.0f, // F +}; + +// Map a rotation in degrees (0 / 90 / 180 / 270) to Dawn's enum. Anything that +// isn't a clean multiple of 90 snaps to the nearest quadrant; Dawn only +// supports those four steps for external textures. +static wgpu::ExternalTextureRotation toExternalTextureRotation(double degrees) { + int quadrant = static_cast(std::lround(degrees / 90.0)); + quadrant = ((quadrant % 4) + 4) % 4; + switch (quadrant) { + case 1: + return wgpu::ExternalTextureRotation::Rotate90Degrees; + case 2: + return wgpu::ExternalTextureRotation::Rotate180Degrees; + case 3: + return wgpu::ExternalTextureRotation::Rotate270Degrees; + default: + return wgpu::ExternalTextureRotation::Rotate0Degrees; + } +} + +std::shared_ptr GPUExternalTexture::Create( + wgpu::Device device, + std::shared_ptr descriptor) { + if (!descriptor || descriptor->source == 0) { + throw std::runtime_error( + "GPUExternalTexture::Create(): descriptor.source (a native buffer " + "pointer from Skia.NativeBuffer.MakeFromImage) is required"); + } + void *bufferPtr = + reinterpret_cast(static_cast(descriptor->source)); + std::string label = descriptor->label.value_or("external-texture"); + + // 1. Import the native buffer as SharedTextureMemory and read its dimensions. + uint32_t width = 0; + uint32_t height = 0; + wgpu::SharedTextureMemory memory = importNativeBufferAsSharedTextureMemory( + device, bufferPtr, label, &width, &height); + if (memory == nullptr) { + throw std::runtime_error( + "GPUExternalTexture::Create(): ImportSharedTextureMemory returned " + "null"); + } + + // 2. Create the texture from the surface (Dawn picks the single-plane + // BGRA/RGBA format). + auto texture = memory.CreateTexture(); + if (texture == nullptr) { + throw std::runtime_error( + "GPUExternalTexture::Create(): CreateTexture returned null"); + } + + // 3. Begin access. The matching EndAccess runs when the GPUExternalTexture is + // destroyed (explicitly via destroy() or at GC). + wgpu::SharedTextureMemoryBeginAccessDescriptor begin{}; + begin.initialized = true; + begin.concurrentRead = false; +#if defined(__ANDROID__) + // Dawn's Vulkan backend requires the acquired VkImageLayout to be chained. + // UNDEFINED (= 0) on both ends is the canonical "no prior GPU producer" + // pattern (matches GPUSharedTextureMemory::beginAccess). + wgpu::SharedTextureMemoryVkImageLayoutBeginState vkBegin{}; + vkBegin.oldLayout = 0; + vkBegin.newLayout = 0; + begin.nextInChain = &vkBegin; +#endif + if (!memory.BeginAccess(texture, &begin)) { + throw std::runtime_error( + "GPUExternalTexture::Create(): BeginAccess failed"); + } + + // 4. Single-plane view (the whole BGRA/RGBA surface). + auto plane0 = texture.CreateView(); + + // 5. Build the ExternalTextureDescriptor. The surface is already RGB in the + // target color space, so pass it through with identity transfer/gamut. + wgpu::ExternalTextureDescriptor extDesc{}; + if (!label.empty()) { + extDesc.label = wgpu::StringView(label.c_str(), label.size()); + } + extDesc.plane0 = plane0; + extDesc.gamutConversionMatrix = kIdentityGamutMatrix; + extDesc.srcTransferFunctionParameters = kIdentityTransferParams; + extDesc.dstTransferFunctionParameters = kIdentityTransferParams; + extDesc.cropOrigin = {0, 0}; + extDesc.cropSize = {width, height}; + extDesc.apparentSize = {width, height}; + extDesc.mirrored = descriptor->mirrored.value_or(false); + extDesc.rotation = + toExternalTextureRotation(descriptor->rotation.value_or(0)); + + auto external = device.CreateExternalTexture(&extDesc); + if (external == nullptr) { + wgpu::SharedTextureMemoryEndAccessState state{}; +#if defined(__ANDROID__) + wgpu::SharedTextureMemoryVkImageLayoutEndState vkEnd{}; + state.nextInChain = &vkEnd; +#endif + (void)memory.EndAccess(texture, &state); + throw std::runtime_error( + "GPUExternalTexture::Create(): CreateExternalTexture returned null"); + } + + return std::make_shared( + std::move(external), std::move(memory), std::move(texture), + std::move(label)); +} + +} // namespace rnwgpu diff --git a/packages/skia/cpp/rnwgpu/api/GPUExternalTexture.h b/packages/skia/cpp/rnwgpu/api/GPUExternalTexture.h index 56b8e08fc2..1e32d6db44 100644 --- a/packages/skia/cpp/rnwgpu/api/GPUExternalTexture.h +++ b/packages/skia/cpp/rnwgpu/api/GPUExternalTexture.h @@ -1,6 +1,8 @@ #pragma once +#include #include +#include #include "descriptors/Unions.h" @@ -12,16 +14,61 @@ namespace rnwgpu { namespace jsi = facebook::jsi; +struct GPUExternalTextureDescriptor; + class GPUExternalTexture : public NativeObject { public: static constexpr const char *CLASS_NAME = "GPUExternalTexture"; - explicit GPUExternalTexture(wgpu::ExternalTexture instance, std::string label) - : NativeObject(CLASS_NAME), _instance(instance), _label(label) {} + // Import a native buffer (via descriptor.source, a CVPixelBufferRef / + // AHardwareBuffer* from Skia's NativeBuffer API) as a GPUExternalTexture on + // `device`: imports the native surface as SharedTextureMemory, begins access, + // and wraps the resulting wgpu::ExternalTexture together with the resources + // whose lifetime it owns. The matching EndAccess runs in destroy() / the + // destructor. Defined in GPUExternalTexture.cpp. + static std::shared_ptr + Create(wgpu::Device device, + std::shared_ptr descriptor); + + // Construct from an already-built wgpu::ExternalTexture plus the underlying + // shared-memory resources we need to keep alive. The wrapper takes ownership + // of the SharedTextureMemory + Texture and calls EndAccess on destruction so + // the producer (the native buffer's surface) can reclaim it. + GPUExternalTexture(wgpu::ExternalTexture instance, + wgpu::SharedTextureMemory memory, wgpu::Texture texture, + std::string label) + : NativeObject(CLASS_NAME), _instance(std::move(instance)), + _memory(std::move(memory)), _texture(std::move(texture)), + _label(std::move(label)) {} + + ~GPUExternalTexture() override { destroy(); } public: std::string getBrand() { return CLASS_NAME; } + // End the shared-memory access window and release the underlying resources. + // Idempotent: safe to call more than once, and the destructor calls it as a + // garbage-collection fallback. Call it right after the queue.submit() that + // sampled this texture (never before): a GPUExternalTexture's access window + // is owned by this wrapper's lifetime, not by submit, so without an explicit + // destroy() the producer's surface stays claimed until GC runs. EndAccess is + // the designed post-submit call: Dawn keeps the texture alive for in-flight + // GPU work via the fences it returns. + void destroy() { + if (_memory && _texture) { + wgpu::SharedTextureMemoryEndAccessState state{}; +#if defined(__ANDROID__) + // Dawn's Vulkan backend requires the released VkImageLayout to be chained + // (matches BeginAccess in GPUExternalTexture::Create). + wgpu::SharedTextureMemoryVkImageLayoutEndState vkEnd{}; + state.nextInChain = &vkEnd; +#endif + (void)_memory.EndAccess(_texture, &state); + } + _texture = nullptr; + _memory = nullptr; + } + std::string getLabel() { return _label; } void setLabel(const std::string &label) { _label = label; @@ -33,12 +80,15 @@ class GPUExternalTexture : public NativeObject { installGetterSetter(runtime, prototype, "label", &GPUExternalTexture::getLabel, &GPUExternalTexture::setLabel); + installMethod(runtime, prototype, "destroy", &GPUExternalTexture::destroy); } inline const wgpu::ExternalTexture get() { return _instance; } private: wgpu::ExternalTexture _instance; + wgpu::SharedTextureMemory _memory; + wgpu::Texture _texture; std::string _label; }; diff --git a/packages/skia/cpp/rnwgpu/api/GPUQueue.cpp b/packages/skia/cpp/rnwgpu/api/GPUQueue.cpp index a46f334136..6d321be33d 100644 --- a/packages/skia/cpp/rnwgpu/api/GPUQueue.cpp +++ b/packages/skia/cpp/rnwgpu/api/GPUQueue.cpp @@ -1,5 +1,6 @@ #include "GPUQueue.h" +#include #include #include #include @@ -103,51 +104,55 @@ void GPUQueue::copyExternalImageToTexture( std::shared_ptr source, std::shared_ptr destination, std::shared_ptr size) { - // wgpu::TexelCopyTextureInfo dst{}; - // wgpu::TexelCopyBufferLayout layout{}; - // wgpu::Extent3D sz{}; - // Convertor conv; - // uint32_t bytesPerPixel = - // source->source->getSize() / - // (source->source->getWidth() * source->source->getHeight()); - // auto dataLayout = std::make_shared(GPUImageDataLayout{ - // std::optional{0.0}, - // std::optional{ - // static_cast(bytesPerPixel * source->source->getWidth())}, - // std::optional{static_cast(source->source->getHeight())}}); - // if (!conv(dst.aspect, destination->aspect) || - // !conv(dst.mipLevel, destination->mipLevel) || - // !conv(dst.origin, destination->origin) || - // !conv(dst.texture, destination->texture) || - // !conv(layout, dataLayout) || // - // !conv(sz, size)) { - // throw std::runtime_error("Invalid input for GPUQueue::writeTexture()"); - // } - // - // if (source->flipY.value_or(false)) { - // // Calculate the row size and total size - // uint32_t rowSize = bytesPerPixel * source->source->getWidth(); - // uint32_t totalSize = source->source->getSize(); - // - // // Create a new buffer for the flipped data - // std::vector flippedData(totalSize); - // - // // Flip the data vertically - // for (uint32_t row = 0; row < source->source->getHeight(); ++row) { - // std::memcpy(flippedData.data() + - // (source->source->getHeight() - 1 - row) * rowSize, - // static_cast(source->source->getData()) + - // row * rowSize, - // rowSize); - // } - // // Use the flipped data for writing to texture - // _instance.WriteTexture(&dst, flippedData.data(), totalSize, &layout, - // &sz); - // } else { - // - // _instance.WriteTexture(&dst, source->source->getData(), - // source->source->getSize(), &layout, &sz); - // } + if (!source || source->source == nullptr) { + throw std::runtime_error("GPUQueue::copyExternalImageToTexture(): " + "source.source (ImageBitmap) is required"); + } + // ImageBitmap holds decoded, row-major RGBA8 pixels. We upload them with + // WriteTexture (a CPU staging copy) rather than a GPU blit; that is enough + // for the common "decode an image, sample it" path and avoids needing a + // source GPUTexture. + const auto &pixels = source->source->data(); + auto width = static_cast(source->source->getWidth()); + auto height = static_cast(source->source->getHeight()); + if (pixels.empty() || width == 0 || height == 0) { + throw std::runtime_error("GPUQueue::copyExternalImageToTexture(): " + "ImageBitmap has no pixels (was it closed?)"); + } + auto bytesPerPixel = static_cast( + pixels.size() / (static_cast(width) * height)); + uint32_t rowSize = bytesPerPixel * width; + + wgpu::TexelCopyTextureInfo dst{}; + wgpu::TexelCopyBufferLayout layout{}; + wgpu::Extent3D sz{}; + Convertor conv; + auto dataLayout = std::make_shared( + GPUImageDataLayout{std::optional{0.0}, + std::optional{static_cast(rowSize)}, + std::optional{static_cast(height)}}); + if (!conv(dst.aspect, destination->aspect) || + !conv(dst.mipLevel, destination->mipLevel) || + !conv(dst.origin, destination->origin) || + !conv(dst.texture, destination->texture) || !conv(layout, dataLayout) || + !conv(sz, size)) { + throw std::runtime_error( + "Invalid input for GPUQueue::copyExternalImageToTexture()"); + } + + if (source->flipY.value_or(false)) { + // Flip rows so the image is uploaded bottom-up (matches the WebGPU + // copyExternalImageToTexture flipY semantics). + std::vector flippedData(pixels.size()); + for (uint32_t row = 0; row < height; ++row) { + std::memcpy(flippedData.data() + (height - 1 - row) * rowSize, + pixels.data() + row * rowSize, rowSize); + } + _instance.WriteTexture(&dst, flippedData.data(), flippedData.size(), + &layout, &sz); + } else { + _instance.WriteTexture(&dst, pixels.data(), pixels.size(), &layout, &sz); + } } void GPUQueue::writeTexture(std::shared_ptr destination, diff --git a/packages/skia/cpp/rnwgpu/api/GPUShaderModule.cpp b/packages/skia/cpp/rnwgpu/api/GPUShaderModule.cpp index 04a53e3a67..e6950bf104 100644 --- a/packages/skia/cpp/rnwgpu/api/GPUShaderModule.cpp +++ b/packages/skia/cpp/rnwgpu/api/GPUShaderModule.cpp @@ -18,7 +18,7 @@ async::AsyncTaskHandle GPUShaderModule::getCompilationInfo() { wgpu::CallbackMode::AllowProcessEvents, [result, resolve, reject](wgpu::CompilationInfoRequestStatus status, - const wgpu::CompilationInfo *compilationInfo) mutable { + const wgpu::CompilationInfo *compilationInfo) { if (status != wgpu::CompilationInfoRequestStatus::Success || compilationInfo == nullptr) { reject("Failed to get compilation info"); diff --git a/packages/skia/cpp/rnwgpu/api/GPUSharedFence.cpp b/packages/skia/cpp/rnwgpu/api/GPUSharedFence.cpp new file mode 100644 index 0000000000..7465612615 --- /dev/null +++ b/packages/skia/cpp/rnwgpu/api/GPUSharedFence.cpp @@ -0,0 +1,80 @@ +#include "GPUSharedFence.h" + +#include +#include + +#if defined(__ANDROID__) +#include +#endif + +namespace rnwgpu { + +namespace { + +// Kebab-case names matching the shared-fence-* feature strings (see Unions.h / +// GPUFeatures.h). +std::string sharedFenceTypeToString(wgpu::SharedFenceType type) { + switch (type) { + case wgpu::SharedFenceType::MTLSharedEvent: + return "mtl-shared-event"; + case wgpu::SharedFenceType::SyncFD: + return "sync-fd"; + case wgpu::SharedFenceType::VkSemaphoreOpaqueFD: + return "vk-semaphore-opaque-fd"; + case wgpu::SharedFenceType::VkSemaphoreZirconHandle: + return "vk-semaphore-zircon-handle"; + case wgpu::SharedFenceType::DXGISharedHandle: + return "dxgi-shared-handle"; + case wgpu::SharedFenceType::EGLSync: + return "egl-sync"; + default: + return ""; + } +} + +} // namespace + +jsi::Value GPUSharedFence::exportInfo(jsi::Runtime &runtime, const jsi::Value &, + const jsi::Value *, size_t) { + wgpu::SharedFenceExportInfo info{}; + uint64_t handle = 0; + +#if defined(__APPLE__) + // Apple: the handle is an id pointer. + wgpu::SharedFenceMTLSharedEventExportInfo mtlInfo{}; + info.nextInChain = &mtlInfo; + _instance.ExportInfo(&info); + handle = reinterpret_cast(mtlInfo.sharedEvent); +#elif defined(__ANDROID__) + // Android: the handle is an OS file descriptor (sync_fd). Dawn's ExportInfo + // returns a BORROWED fd: it is owned by the SharedFence and closed when the + // fence is destroyed. This exported handle is documented as caller-owned (the + // caller must close() it), so dup() it. Without the dup the same fd is closed + // twice (once by the caller and once by Dawn on fence destruction), tripping + // Android's fdsan (double-close abort). + wgpu::SharedFenceSyncFDExportInfo fdInfo{}; + info.nextInChain = &fdInfo; + _instance.ExportInfo(&info); + int exportedFd = fdInfo.handle >= 0 + ? ::fcntl(fdInfo.handle, F_DUPFD_CLOEXEC, 0) + : fdInfo.handle; + handle = static_cast(static_cast(exportedFd)); +#else + // react-native-skia only targets Apple (Metal) and Android (Vulkan). On any + // other platform there is no native handle convention to expose, so fail + // loudly rather than handing back a meaningless handle of 0. + throw jsi::JSError(runtime, + "GPUSharedFence::export(): unsupported platform (only " + "Apple/Metal and Android/Vulkan are supported)"); +#endif + + jsi::Object result(runtime); + result.setProperty( + runtime, "type", + jsi::String::createFromUtf8(runtime, sharedFenceTypeToString(info.type))); + result.setProperty(runtime, "handle", + jsi::BigInt::fromUint64(runtime, handle)); + return result; +} + +} // namespace rnwgpu diff --git a/packages/skia/cpp/rnwgpu/api/GPUSharedFence.h b/packages/skia/cpp/rnwgpu/api/GPUSharedFence.h new file mode 100644 index 0000000000..41a6e9e4f8 --- /dev/null +++ b/packages/skia/cpp/rnwgpu/api/GPUSharedFence.h @@ -0,0 +1,53 @@ +#pragma once + +#include +#include + +#include "jsi2/NativeObject.h" + +#include "webgpu/webgpu_cpp.h" + +namespace rnwgpu { + +namespace jsi = facebook::jsi; + +// Wraps a wgpu::SharedFence: a native GPU sync primitive (id on +// Apple, sync-fd / VkSemaphore on Android). +class GPUSharedFence : public NativeObject { +public: + static constexpr const char *CLASS_NAME = "GPUSharedFence"; + + explicit GPUSharedFence(wgpu::SharedFence instance, std::string label) + : NativeObject(CLASS_NAME), _instance(std::move(instance)), + _label(std::move(label)) {} + +public: + std::string getBrand() { return CLASS_NAME; } + + // export() -> { type, handle }: exposes the native handle (as a BigInt) so + // app code can wait on or signal the fence. The caller owns the returned + // handle (e.g. an exported sync-fd must be close()d). + jsi::Value exportInfo(jsi::Runtime &runtime, const jsi::Value &thisVal, + const jsi::Value *args, size_t count); + + std::string getLabel() { return _label; } + void setLabel(const std::string &label) { + _label = label; + _instance.SetLabel(_label.c_str()); + } + + static void definePrototype(jsi::Runtime &runtime, jsi::Object &prototype) { + installGetter(runtime, prototype, "__brand", &GPUSharedFence::getBrand); + installMethod(runtime, prototype, "export", &GPUSharedFence::exportInfo); + installGetterSetter(runtime, prototype, "label", &GPUSharedFence::getLabel, + &GPUSharedFence::setLabel); + } + + inline wgpu::SharedFence get() { return _instance; } + +private: + wgpu::SharedFence _instance; + std::string _label; +}; + +} // namespace rnwgpu diff --git a/packages/skia/cpp/rnwgpu/api/GPUSharedTextureMemory.cpp b/packages/skia/cpp/rnwgpu/api/GPUSharedTextureMemory.cpp new file mode 100644 index 0000000000..53cf66138b --- /dev/null +++ b/packages/skia/cpp/rnwgpu/api/GPUSharedTextureMemory.cpp @@ -0,0 +1,135 @@ +#include "GPUSharedTextureMemory.h" + +#include +#include +#include +#include +#include +#include + +#include "Convertors.h" + +namespace rnwgpu { + +std::shared_ptr GPUSharedTextureMemory::createTexture( + std::optional> descriptor) { + if (!descriptor.has_value() || descriptor.value() == nullptr) { + auto texture = _instance.CreateTexture(); + // The texture aliases the shared memory; it doesn't own GPU allocation, so + // it doesn't report memory pressure. + return std::make_shared(texture, "", false); + } + + wgpu::TextureDescriptor desc{}; + Convertor conv; + if (!conv(desc, descriptor.value())) { + throw std::runtime_error( + "GPUSharedTextureMemory::createTexture(): Error with " + "GPUTextureDescriptor"); + } + auto texture = _instance.CreateTexture(&desc); + return std::make_shared( + texture, descriptor.value()->label.value_or(""), false); +} + +void GPUSharedTextureMemory::beginAccess( + std::shared_ptr texture, bool initialized, + std::optional>> fences) { + if (!texture) { + throw std::runtime_error( + "GPUSharedTextureMemory::beginAccess(): texture is null"); + } + wgpu::SharedTextureMemoryBeginAccessDescriptor desc{}; + desc.initialized = initialized; + desc.concurrentRead = false; + + // Built in lockstep so fenceCount covers both arrays, and kept in locals so + // the raw pointers outlive the synchronous BeginAccess() below. + std::vector rawFences; + std::vector values; + if (fences.has_value()) { + for (const auto &state : *fences) { + if (state && state->fence) { + rawFences.push_back(state->fence->get()); + values.push_back(state->signaledValue); + } + } + } + if (!rawFences.empty()) { + desc.fenceCount = rawFences.size(); + desc.fences = rawFences.data(); + desc.signaledValues = values.data(); + } else { + desc.fenceCount = 0; + desc.fences = nullptr; + desc.signaledValues = nullptr; + } + +#if defined(__ANDROID__) + // Dawn's Vulkan backend (AHardwareBuffer) validates that the begin-access + // descriptor chains a SharedTextureMemoryVkImageLayoutBeginState specifying + // the VkImageLayout to acquire the image into. UNDEFINED (= 0) on both ends + // is the canonical "no prior GPU producer" pattern: Dawn performs an + // external-queue acquire from VK_QUEUE_FAMILY_EXTERNAL which preserves the + // AHB contents, then transitions to whatever layout the texture's actual + // usage requires. + wgpu::SharedTextureMemoryVkImageLayoutBeginState vkLayout{}; + vkLayout.oldLayout = 0; + vkLayout.newLayout = 0; + desc.nextInChain = &vkLayout; +#endif + + auto status = _instance.BeginAccess(texture->get(), &desc); + if (!status) { + throw std::runtime_error("GPUSharedTextureMemory::beginAccess() failed"); + } +} + +jsi::Value GPUSharedTextureMemory::endAccess(jsi::Runtime &runtime, + const jsi::Value &, + const jsi::Value *args, + size_t count) { + if (count < 1 || !args[0].isObject()) { + throw jsi::JSError( + runtime, "GPUSharedTextureMemory::endAccess(): expected (texture)"); + } + auto texture = GPUTexture::fromValue(runtime, args[0]); + + wgpu::SharedTextureMemoryEndAccessState state{}; + +#if defined(__ANDROID__) + // Dawn's Vulkan backend writes the released old/new VkImageLayouts back into + // a chained SharedTextureMemoryVkImageLayoutEndState; validation requires + // the chain even when the caller doesn't read the values. + wgpu::SharedTextureMemoryVkImageLayoutEndState vkLayout{}; + state.nextInChain = &vkLayout; +#endif + + auto status = _instance.EndAccess(texture->get(), &state); + if (!status) { + throw jsi::JSError(runtime, "GPUSharedTextureMemory::endAccess() failed"); + } + + // Copy each wgpu::SharedFence (ref-counted) into its own GPUSharedFence + // wrapper before `state` is destroyed. + jsi::Array fences(runtime, state.fenceCount); + for (size_t i = 0; i < state.fenceCount; i++) { + wgpu::SharedFence fence = state.fences[i]; + auto wrapper = std::make_shared(std::move(fence), ""); + jsi::Object entry(runtime); + entry.setProperty(runtime, "fence", + GPUSharedFence::create(runtime, std::move(wrapper))); + entry.setProperty( + runtime, "signaledValue", + jsi::BigInt::fromUint64(runtime, state.signaledValues[i])); + fences.setValueAtIndex(runtime, i, std::move(entry)); + } + + jsi::Object result(runtime); + result.setProperty(runtime, "initialized", + jsi::Value(static_cast(state.initialized))); + result.setProperty(runtime, "fences", std::move(fences)); + return result; +} + +} // namespace rnwgpu diff --git a/packages/skia/cpp/rnwgpu/api/GPUSharedTextureMemory.h b/packages/skia/cpp/rnwgpu/api/GPUSharedTextureMemory.h new file mode 100644 index 0000000000..b5db61f9c3 --- /dev/null +++ b/packages/skia/cpp/rnwgpu/api/GPUSharedTextureMemory.h @@ -0,0 +1,75 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "jsi2/NativeObject.h" + +#include "webgpu/webgpu_cpp.h" + +#include "GPUSharedFence.h" +#include "GPUTexture.h" +#include "descriptors/GPUSharedFenceState.h" +#include "descriptors/GPUTextureDescriptor.h" + +namespace rnwgpu { + +namespace jsi = facebook::jsi; + +class GPUSharedTextureMemory : public NativeObject { +public: + static constexpr const char *CLASS_NAME = "GPUSharedTextureMemory"; + + explicit GPUSharedTextureMemory(wgpu::SharedTextureMemory instance, + std::string label) + : NativeObject(CLASS_NAME), _instance(std::move(instance)), + _label(std::move(label)) {} + +public: + std::string getBrand() { return CLASS_NAME; } + + std::shared_ptr createTexture( + std::optional> descriptor); + + // Optional `fences` are wait fences: Dawn waits for each to reach its + // signaledValue before writing the surface. Throws on failure. + void beginAccess( + std::shared_ptr texture, bool initialized, + std::optional>> fences); + + // endAccess(texture) -> { initialized, fences: { fence, signaledValue }[] } + // Surfaces the fences Dawn produced for the access. Throws on failure. + jsi::Value endAccess(jsi::Runtime &runtime, const jsi::Value &thisVal, + const jsi::Value *args, size_t count); + + std::string getLabel() { return _label; } + void setLabel(const std::string &label) { + _label = label; + _instance.SetLabel(_label.c_str()); + } + + static void definePrototype(jsi::Runtime &runtime, jsi::Object &prototype) { + installGetter(runtime, prototype, "__brand", + &GPUSharedTextureMemory::getBrand); + installMethod(runtime, prototype, "createTexture", + &GPUSharedTextureMemory::createTexture); + installMethod(runtime, prototype, "beginAccess", + &GPUSharedTextureMemory::beginAccess); + installMethod(runtime, prototype, "endAccess", + &GPUSharedTextureMemory::endAccess); + installGetterSetter(runtime, prototype, "label", + &GPUSharedTextureMemory::getLabel, + &GPUSharedTextureMemory::setLabel); + } + + inline const wgpu::SharedTextureMemory get() { return _instance; } + +private: + wgpu::SharedTextureMemory _instance; + std::string _label; +}; + +} // namespace rnwgpu diff --git a/packages/skia/cpp/rnwgpu/api/ImageBitmap.h b/packages/skia/cpp/rnwgpu/api/ImageBitmap.h new file mode 100644 index 0000000000..556cad86ad --- /dev/null +++ b/packages/skia/cpp/rnwgpu/api/ImageBitmap.h @@ -0,0 +1,62 @@ +#pragma once + +#include +#include +#include +#include + +#include "jsi2/NativeObject.h" + +namespace rnwgpu { + +namespace jsi = facebook::jsi; + +// DRAFT — compile-unverified. Minimal ImageBitmap holding decoded, unpremul +// RGBA8 pixels plus its dimensions. Produced by the global createImageBitmap() +// binding (see RNSkManager.cpp). Decoding is done with Skia's own codec, so no +// platform-specific image decoder is required. +// +// FOLLOW-UP: to make an ImageBitmap usable as a copyExternalImageToTexture +// source, uncomment the `source` field + ImageBitmap converter in +// rnwgpu/api/descriptors/GPUImageCopyExternalImage.h and upload `data()` in +// GPUQueue::copyExternalImageToTexture. That GPU wiring is intentionally out of +// scope for this draft. +class ImageBitmap : public NativeObject { +public: + static constexpr const char *CLASS_NAME = "ImageBitmap"; + + ImageBitmap(std::vector data, size_t width, size_t height) + : NativeObject(CLASS_NAME), _data(std::move(data)), _width(width), + _height(height) {} + + size_t getWidth() { return _width; } + + size_t getHeight() { return _height; } + + // Per the spec, close() releases the bitmap's underlying pixels and zeroes + // its dimensions. Idempotent. + void close() { + _data.clear(); + _data.shrink_to_fit(); + _width = 0; + _height = 0; + } + + // Decoded, unpremultiplied RGBA8 pixels (row-major, width*height*4 bytes). + const std::vector &data() const { return _data; } + + static void definePrototype(jsi::Runtime &runtime, jsi::Object &prototype) { + installGetter(runtime, prototype, "width", &ImageBitmap::getWidth); + installGetter(runtime, prototype, "height", &ImageBitmap::getHeight); + installMethod(runtime, prototype, "close", &ImageBitmap::close); + } + + size_t getMemoryPressure() override { return _data.size(); } + +private: + std::vector _data; + size_t _width; + size_t _height; +}; + +} // namespace rnwgpu diff --git a/packages/skia/cpp/rnwgpu/api/NativeBufferUtils.h b/packages/skia/cpp/rnwgpu/api/NativeBufferUtils.h new file mode 100644 index 0000000000..e8443b79f9 --- /dev/null +++ b/packages/skia/cpp/rnwgpu/api/NativeBufferUtils.h @@ -0,0 +1,87 @@ +#pragma once + +#include +#include +#include + +#include "webgpu/webgpu_cpp.h" + +#if defined(__APPLE__) +#include "AppleNativeBuffer.h" +#elif defined(__ANDROID__) +#include +#endif + +namespace rnwgpu { + +// Import a Skia NativeBuffer pointer (a CVPixelBufferRef on Apple, an +// AHardwareBuffer* on Android, as returned by Skia.NativeBuffer.MakeFromImage / +// MakeTestBuffer) as a wgpu::SharedTextureMemory on `device`. When non-null, +// outWidth/outHeight receive the surface dimensions. Returns a null +// SharedTextureMemory if the import itself fails (the caller decides how to +// report that); throws std::runtime_error for pre-import failures (a buffer +// with no IOSurface, or an unsupported platform). +// +// The platform-specific chained descriptor only needs to outlive the +// ImportSharedTextureMemory call, so it lives entirely within this function. +// Shared by GPUDevice::importSharedTextureMemory and +// GPUExternalTexture::Create. +inline wgpu::SharedTextureMemory importNativeBufferAsSharedTextureMemory( + const wgpu::Device &device, void *bufferPtr, const std::string &label, + uint32_t *outWidth, uint32_t *outHeight) { + wgpu::SharedTextureMemoryDescriptor memDesc{}; + if (!label.empty()) { + memDesc.label = wgpu::StringView(label.c_str(), label.size()); + } + + uint32_t width = 0; + uint32_t height = 0; + wgpu::SharedTextureMemory memory; + +#if defined(__APPLE__) + // Skia's NativeBuffer is a CVPixelBufferRef; extract its backing IOSurface + // (and dimensions) in Objective-C++ since CoreVideo isn't available here. + void *ioSurface = GetIOSurfaceFromNativeBuffer(bufferPtr, &width, &height); + if (ioSurface == nullptr) { + throw std::runtime_error( + "importNativeBufferAsSharedTextureMemory(): native " + "buffer has no IOSurface"); + } + wgpu::SharedTextureMemoryIOSurfaceDescriptor platformDesc{}; + platformDesc.ioSurface = ioSurface; + // Default off: enabling it propagates StorageBinding into properties.usage, + // which then forces memory.createTexture() (no-descriptor form) to validate + // the format against storage capabilities. bgra8unorm (the standard + // CVPixelBuffer format) only supports storage when the device opts into the + // bgra8unorm-storage feature, so unconditionally setting this here breaks the + // common sample-only case. + platformDesc.allowStorageBinding = false; + memDesc.nextInChain = &platformDesc; + memory = device.ImportSharedTextureMemory(&memDesc); +#elif defined(__ANDROID__) + auto *ahb = reinterpret_cast(bufferPtr); + AHardwareBuffer_Desc ahbDesc = {}; + AHardwareBuffer_describe(ahb, &ahbDesc); + width = ahbDesc.width; + height = ahbDesc.height; + wgpu::SharedTextureMemoryAHardwareBufferDescriptor platformDesc{}; + platformDesc.handle = ahb; + memDesc.nextInChain = &platformDesc; + memory = device.ImportSharedTextureMemory(&memDesc); +#else + (void)device; + (void)bufferPtr; + throw std::runtime_error( + "importNativeBufferAsSharedTextureMemory(): unsupported platform"); +#endif + + if (outWidth != nullptr) { + *outWidth = width; + } + if (outHeight != nullptr) { + *outHeight = height; + } + return memory; +} + +} // namespace rnwgpu diff --git a/packages/skia/cpp/rnwgpu/api/descriptors/GPUBindGroupEntry.h b/packages/skia/cpp/rnwgpu/api/descriptors/GPUBindGroupEntry.h index aeadee0738..813389bd0d 100644 --- a/packages/skia/cpp/rnwgpu/api/descriptors/GPUBindGroupEntry.h +++ b/packages/skia/cpp/rnwgpu/api/descriptors/GPUBindGroupEntry.h @@ -20,7 +20,7 @@ struct GPUBindGroupEntry { std::shared_ptr sampler = nullptr; std::shared_ptr textureView = nullptr; std::shared_ptr buffer = nullptr; - // external textures + std::shared_ptr externalTexture = nullptr; }; } // namespace rnwgpu @@ -45,6 +45,9 @@ template <> struct JSIConverter> { } else if (obj.hasNativeState(runtime)) { result->textureView = obj.getNativeState(runtime); + } else if (obj.hasNativeState(runtime)) { + result->externalTexture = + obj.getNativeState(runtime); } else if (obj.hasNativeState(runtime)) { auto binding = std::make_shared(); binding->buffer = obj.getNativeState(runtime); diff --git a/packages/skia/cpp/rnwgpu/api/descriptors/GPUCanvasConfiguration.h b/packages/skia/cpp/rnwgpu/api/descriptors/GPUCanvasConfiguration.h index 9cccc66767..cb319b7d72 100644 --- a/packages/skia/cpp/rnwgpu/api/descriptors/GPUCanvasConfiguration.h +++ b/packages/skia/cpp/rnwgpu/api/descriptors/GPUCanvasConfiguration.h @@ -7,7 +7,7 @@ #include "jsi2/JSIConverter.h" -#include "GPUDevice.h" +#include "rnwgpu/api/GPUDevice.h" namespace jsi = facebook::jsi; diff --git a/packages/skia/cpp/rnwgpu/api/descriptors/GPUDawnTogglesDescriptor.h b/packages/skia/cpp/rnwgpu/api/descriptors/GPUDawnTogglesDescriptor.h new file mode 100644 index 0000000000..c31460664d --- /dev/null +++ b/packages/skia/cpp/rnwgpu/api/descriptors/GPUDawnTogglesDescriptor.h @@ -0,0 +1,56 @@ +#pragma once + +#include +#include +#include + +#include "webgpu/webgpu_cpp.h" + +#include "jsi2/JSIConverter.h" + +namespace jsi = facebook::jsi; + +namespace rnwgpu { + +// Non-standard, Dawn-only. Mirrors wgpu::DawnTogglesDescriptor field-for-field +// so the mapping to the native chained struct is 1:1. Chained onto the +// wgpu::DeviceDescriptor in GPUAdapter::requestDevice. +struct GPUDawnTogglesDescriptor { + std::optional> enabledToggles; // Iterable + std::optional> disabledToggles; // Iterable +}; + +} // namespace rnwgpu + +namespace rnwgpu { + +template <> +struct JSIConverter> { + static std::shared_ptr + fromJSI(jsi::Runtime &runtime, const jsi::Value &arg, bool outOfBounds) { + auto result = std::make_unique(); + if (!outOfBounds && arg.isObject()) { + auto value = arg.getObject(runtime); + if (value.hasProperty(runtime, "enabledToggles")) { + auto prop = value.getProperty(runtime, "enabledToggles"); + result->enabledToggles = + JSIConverter>>::fromJSI( + runtime, prop, false); + } + if (value.hasProperty(runtime, "disabledToggles")) { + auto prop = value.getProperty(runtime, "disabledToggles"); + result->disabledToggles = + JSIConverter>>::fromJSI( + runtime, prop, false); + } + } + return result; + } + static jsi::Value + toJSI(jsi::Runtime &runtime, + std::shared_ptr arg) { + throw std::runtime_error("Invalid GPUDawnTogglesDescriptor::toJSI()"); + } +}; + +} // namespace rnwgpu diff --git a/packages/skia/cpp/rnwgpu/api/descriptors/GPUDeviceDescriptor.h b/packages/skia/cpp/rnwgpu/api/descriptors/GPUDeviceDescriptor.h index e570e24e81..b1aebb603d 100644 --- a/packages/skia/cpp/rnwgpu/api/descriptors/GPUDeviceDescriptor.h +++ b/packages/skia/cpp/rnwgpu/api/descriptors/GPUDeviceDescriptor.h @@ -9,6 +9,7 @@ #include "jsi2/JSIConverter.h" +#include "GPUDawnTogglesDescriptor.h" #include "GPUQueueDescriptor.h" namespace jsi = facebook::jsi; @@ -23,6 +24,9 @@ struct GPUDeviceDescriptor { std::optional> defaultQueue; // GPUQueueDescriptor std::optional label; // string + // Non-standard Dawn-only device toggles, chained onto the wgpu::Device + // descriptor in GPUAdapter::requestDevice. + std::optional> dawnToggles; }; } // namespace rnwgpu @@ -86,6 +90,12 @@ template <> struct JSIConverter> { result->label = JSIConverter>::fromJSI( runtime, prop, false); } + if (value.hasProperty(runtime, "dawnToggles")) { + auto prop = value.getProperty(runtime, "dawnToggles"); + result->dawnToggles = JSIConverter>>::fromJSI(runtime, prop, + false); + } } return result; diff --git a/packages/skia/cpp/rnwgpu/api/descriptors/GPUExternalTextureDescriptor.h b/packages/skia/cpp/rnwgpu/api/descriptors/GPUExternalTextureDescriptor.h index 51da39afe9..05e30c3397 100644 --- a/packages/skia/cpp/rnwgpu/api/descriptors/GPUExternalTextureDescriptor.h +++ b/packages/skia/cpp/rnwgpu/api/descriptors/GPUExternalTextureDescriptor.h @@ -1,26 +1,36 @@ #pragma once +#include #include #include #include -#include #include "webgpu/webgpu_cpp.h" -#include "Convertors.h" - #include "jsi2/JSIConverter.h" namespace jsi = facebook::jsi; namespace rnwgpu { +// Mirror of GPUExternalTextureDescriptor from the WebGPU spec, adapted to +// Skia's binding. Skia has no VideoFrame; instead `source` is a native buffer +// pointer (the BigInt returned by Skia.NativeBuffer.MakeFromImage): a +// CVPixelBufferRef on Apple, an AHardwareBuffer* on Android. The pointer's +// lifetime is owned by the caller (Skia.NativeBuffer.Release); the imported +// texture must not outlive it. +// +// We don't expose colorSpace yet; the C++ side picks dst-sRGB and identity +// gamut, the right default for "render this BGRA frame to an sRGB framebuffer". +// +// `rotation` / `mirrored` are a non-spec extension baked into Dawn's sampling +// transform. `rotation` is in degrees and must be one of 0 / 90 / 180 / 270. struct GPUExternalTextureDescriptor { - // std::variant, - // std::shared_ptr> - // source; // | HTMLVideoElement | VideoFrame - // std::optional colorSpace; // PredefinedColorSpace - std::optional label; // string + // native buffer pointer (CVPixelBufferRef / AHardwareBuffer*) + uint64_t source = 0; + std::optional label; + std::optional rotation; + std::optional mirrored; }; } // namespace rnwgpu @@ -31,23 +41,21 @@ template <> struct JSIConverter> { static std::shared_ptr fromJSI(jsi::Runtime &runtime, const jsi::Value &arg, bool outOfBounds) { - auto result = std::make_unique(); + auto result = std::make_shared(); if (!outOfBounds && arg.isObject()) { auto value = arg.getObject(runtime); if (value.hasProperty(runtime, "source")) { auto prop = value.getProperty(runtime, "source"); - // result->source = JSIConverter< - // std::variant, - // std::shared_ptr>>::fromJSI(runtime, - // prop, - // false); - } - if (value.hasProperty(runtime, "colorSpace")) { - auto prop = value.getProperty(runtime, "colorSpace"); - if (!prop.isUndefined()) { - // result->colorSpace = - // JSIConverter>::fromJSI( - // runtime, prop, false); + // The native buffer pointer arrives as a BigInt (uintptr_t value). It + // must be a BigInt: a JS number can't represent a 64-bit pointer + // without truncation, so we reject it rather than corrupt the address. + if (prop.isBigInt()) { + result->source = prop.asBigInt(runtime).asUint64(runtime); + } else if (!prop.isUndefined() && !prop.isNull()) { + throw jsi::JSError( + runtime, "GPUExternalTextureDescriptor.source must be a " + "NativeBuffer (BigInt) from " + "Skia.NativeBuffer.MakeFromImage / MakeTestBuffer"); } } if (value.hasProperty(runtime, "label")) { @@ -57,13 +65,24 @@ struct JSIConverter> { runtime, prop, false); } } + if (value.hasProperty(runtime, "rotation")) { + auto prop = value.getProperty(runtime, "rotation"); + if (prop.isNumber()) { + result->rotation = prop.asNumber(); + } + } + if (value.hasProperty(runtime, "mirrored")) { + auto prop = value.getProperty(runtime, "mirrored"); + if (prop.isBool()) { + result->mirrored = prop.getBool(); + } + } } - return result; } static jsi::Value - toJSI(jsi::Runtime &runtime, - std::shared_ptr arg) { + toJSI(jsi::Runtime & /*runtime*/, + std::shared_ptr /*arg*/) { throw std::runtime_error("Invalid GPUExternalTextureDescriptor::toJSI()"); } }; diff --git a/packages/skia/cpp/rnwgpu/api/descriptors/GPUImageCopyExternalImage.h b/packages/skia/cpp/rnwgpu/api/descriptors/GPUImageCopyExternalImage.h index cc9396cf67..45e36176db 100644 --- a/packages/skia/cpp/rnwgpu/api/descriptors/GPUImageCopyExternalImage.h +++ b/packages/skia/cpp/rnwgpu/api/descriptors/GPUImageCopyExternalImage.h @@ -7,17 +7,18 @@ #include "webgpu/webgpu_cpp.h" -#include "Convertors.h" +#include "rnwgpu/api/Convertors.h" #include "jsi2/JSIConverter.h" -#include "GPUOrigin2D.h" +#include "rnwgpu/api/GPUOrigin2D.h" +#include "rnwgpu/api/ImageBitmap.h" namespace jsi = facebook::jsi; namespace rnwgpu { struct GPUImageCopyExternalImage { - // std::shared_ptr source; // GPUImageCopyExternalImageSource + std::shared_ptr source; // GPUImageCopyExternalImageSource std::optional> origin; // GPUOrigin2DStrict std::optional flipY; // boolean }; @@ -33,12 +34,11 @@ struct JSIConverter> { auto result = std::make_unique(); if (!outOfBounds && arg.isObject()) { auto obj = arg.getObject(runtime); - // if (obj.hasProperty(runtime, "source")) { - // auto prop = obj.getProperty(runtime, "source"); - // result->source = - // JSIConverter>::fromJSI( - // runtime, prop, false); - // } + if (obj.hasProperty(runtime, "source")) { + auto prop = obj.getProperty(runtime, "source"); + result->source = JSIConverter>::fromJSI( + runtime, prop, false); + } if (obj.hasProperty(runtime, "origin")) { auto prop = obj.getProperty(runtime, "origin"); result->origin = JSIConverter>::fromJSI( diff --git a/packages/skia/cpp/rnwgpu/api/descriptors/GPUImageCopyTexture.h b/packages/skia/cpp/rnwgpu/api/descriptors/GPUImageCopyTexture.h index eed89f4878..ff3472d0e0 100644 --- a/packages/skia/cpp/rnwgpu/api/descriptors/GPUImageCopyTexture.h +++ b/packages/skia/cpp/rnwgpu/api/descriptors/GPUImageCopyTexture.h @@ -6,7 +6,7 @@ #include "jsi2/JSIConverter.h" -#include "GPUOrigin3D.h" +#include "rnwgpu/api/GPUOrigin3D.h" #include "rnwgpu/api/GPUTexture.h" namespace jsi = facebook::jsi; diff --git a/packages/skia/cpp/rnwgpu/api/descriptors/GPUImageCopyTextureTagged.h b/packages/skia/cpp/rnwgpu/api/descriptors/GPUImageCopyTextureTagged.h index 56b578ac39..fa8191a963 100644 --- a/packages/skia/cpp/rnwgpu/api/descriptors/GPUImageCopyTextureTagged.h +++ b/packages/skia/cpp/rnwgpu/api/descriptors/GPUImageCopyTextureTagged.h @@ -6,8 +6,8 @@ #include "jsi2/JSIConverter.h" -#include "External.h" -#include "GPUOrigin3D.h" +#include "rnwgpu/api/External.h" +#include "rnwgpu/api/GPUOrigin3D.h" #include "rnwgpu/api/GPUTexture.h" namespace jsi = facebook::jsi; diff --git a/packages/skia/cpp/rnwgpu/api/descriptors/GPUSharedFenceDescriptor.h b/packages/skia/cpp/rnwgpu/api/descriptors/GPUSharedFenceDescriptor.h new file mode 100644 index 0000000000..2943a2f349 --- /dev/null +++ b/packages/skia/cpp/rnwgpu/api/descriptors/GPUSharedFenceDescriptor.h @@ -0,0 +1,58 @@ +#pragma once + +#include +#include +#include + +#include "webgpu/webgpu_cpp.h" + +#include "jsi2/JSIConverter.h" + +namespace jsi = facebook::jsi; + +namespace rnwgpu { + +// Descriptor for GPUDevice.importSharedFence. `handle` is the native handle +// (an id pointer on Apple, an OS file descriptor on Android), +// passed from JS as a BigInt. +struct GPUSharedFenceDescriptor { + std::string type; + void *handle = nullptr; + std::optional label; +}; + +} // namespace rnwgpu + +namespace rnwgpu { + +template <> +struct JSIConverter> { + static std::shared_ptr + fromJSI(jsi::Runtime &runtime, const jsi::Value &arg, bool outOfBounds) { + auto result = std::make_unique(); + if (!outOfBounds && arg.isObject()) { + auto value = arg.getObject(runtime); + if (value.hasProperty(runtime, "type")) { + result->type = + value.getProperty(runtime, "type").asString(runtime).utf8(runtime); + } + if (value.hasProperty(runtime, "handle")) { + auto prop = value.getProperty(runtime, "handle"); + result->handle = JSIConverter::fromJSI(runtime, prop, false); + } + if (value.hasProperty(runtime, "label")) { + auto prop = value.getProperty(runtime, "label"); + result->label = JSIConverter>::fromJSI( + runtime, prop, false); + } + } + return result; + } + static jsi::Value + toJSI(jsi::Runtime & /*runtime*/, + std::shared_ptr /*arg*/) { + throw std::runtime_error("Invalid GPUSharedFenceDescriptor::toJSI()"); + } +}; + +} // namespace rnwgpu diff --git a/packages/skia/cpp/rnwgpu/api/descriptors/GPUSharedFenceState.h b/packages/skia/cpp/rnwgpu/api/descriptors/GPUSharedFenceState.h new file mode 100644 index 0000000000..d82f0ccb53 --- /dev/null +++ b/packages/skia/cpp/rnwgpu/api/descriptors/GPUSharedFenceState.h @@ -0,0 +1,51 @@ +#pragma once + +#include +#include + +#include "webgpu/webgpu_cpp.h" + +#include "jsi2/JSIConverter.h" + +#include "rnwgpu/api/GPUSharedFence.h" + +namespace jsi = facebook::jsi; + +namespace rnwgpu { + +// A fence and the timeline value to wait for / signal at. +struct GPUSharedFenceState { + std::shared_ptr fence; + uint64_t signaledValue = 0; +}; + +} // namespace rnwgpu + +namespace rnwgpu { + +template <> struct JSIConverter> { + static std::shared_ptr + fromJSI(jsi::Runtime &runtime, const jsi::Value &arg, bool outOfBounds) { + auto result = std::make_unique(); + if (!outOfBounds && arg.isObject()) { + auto value = arg.getObject(runtime); + if (value.hasProperty(runtime, "fence")) { + auto prop = value.getProperty(runtime, "fence"); + result->fence = JSIConverter>::fromJSI( + runtime, prop, false); + } + if (value.hasProperty(runtime, "signaledValue")) { + auto prop = value.getProperty(runtime, "signaledValue"); + result->signaledValue = + JSIConverter::fromJSI(runtime, prop, false); + } + } + return result; + } + static jsi::Value toJSI(jsi::Runtime &runtime, + std::shared_ptr arg) { + throw std::runtime_error("Invalid GPUSharedFenceState::toJSI()"); + } +}; + +} // namespace rnwgpu diff --git a/packages/skia/cpp/rnwgpu/api/descriptors/GPUSharedTextureMemoryDescriptor.h b/packages/skia/cpp/rnwgpu/api/descriptors/GPUSharedTextureMemoryDescriptor.h new file mode 100644 index 0000000000..e5ce67ad31 --- /dev/null +++ b/packages/skia/cpp/rnwgpu/api/descriptors/GPUSharedTextureMemoryDescriptor.h @@ -0,0 +1,73 @@ +#pragma once + +#include +#include +#include +#include + +#include "webgpu/webgpu_cpp.h" + +#include "jsi2/JSIConverter.h" + +namespace jsi = facebook::jsi; + +namespace rnwgpu { + +// Descriptor for GPUDevice.importSharedTextureMemory. +// +// `handle` is a native buffer pointer as a uintptr_t (passed as a BigInt from +// JS), i.e. the value returned by Skia.NativeBuffer.MakeFromImage: +// - Apple platforms: CVPixelBufferRef (its backing IOSurface is imported) +// - Android: AHardwareBuffer* +// +// Lifetime: the caller is responsible for keeping the underlying object alive +// (via Skia.NativeBuffer.Release) for as long as this shared memory is in use. +struct GPUSharedTextureMemoryDescriptor { + uint64_t handle = 0; + std::optional label; +}; + +} // namespace rnwgpu + +namespace rnwgpu { + +template <> +struct JSIConverter> { + static std::shared_ptr + fromJSI(jsi::Runtime &runtime, const jsi::Value &arg, bool outOfBounds) { + auto result = std::make_shared(); + if (!outOfBounds && arg.isObject()) { + auto value = arg.getObject(runtime); + if (value.hasProperty(runtime, "handle")) { + auto prop = value.getProperty(runtime, "handle"); + // The native buffer pointer arrives as a BigInt (uintptr_t value). It + // must be a BigInt: a JS number can't represent a 64-bit pointer + // without truncation, so we reject it rather than corrupt the address. + if (prop.isBigInt()) { + result->handle = prop.asBigInt(runtime).asUint64(runtime); + } else if (!prop.isUndefined() && !prop.isNull()) { + throw jsi::JSError( + runtime, "GPUSharedTextureMemoryDescriptor.handle must be a " + "NativeBuffer (BigInt) from " + "Skia.NativeBuffer.MakeFromImage / MakeTestBuffer"); + } + } + if (value.hasProperty(runtime, "label")) { + auto prop = value.getProperty(runtime, "label"); + if (!prop.isUndefined()) { + result->label = JSIConverter>::fromJSI( + runtime, prop, false); + } + } + } + return result; + } + static jsi::Value + toJSI(jsi::Runtime & /*runtime*/, + std::shared_ptr /*arg*/) { + throw std::runtime_error( + "Invalid GPUSharedTextureMemoryDescriptor::toJSI()"); + } +}; + +} // namespace rnwgpu diff --git a/packages/skia/cpp/rnwgpu/api/descriptors/GPUTextureDescriptor.h b/packages/skia/cpp/rnwgpu/api/descriptors/GPUTextureDescriptor.h index c1ce354fb8..177ca0e876 100644 --- a/packages/skia/cpp/rnwgpu/api/descriptors/GPUTextureDescriptor.h +++ b/packages/skia/cpp/rnwgpu/api/descriptors/GPUTextureDescriptor.h @@ -8,7 +8,7 @@ #include "jsi2/JSIConverter.h" -#include "GPUExtent3D.h" +#include "rnwgpu/api/GPUExtent3D.h" namespace jsi = facebook::jsi; diff --git a/packages/skia/cpp/rnwgpu/api/descriptors/GPUUncapturedErrorEventInit.h b/packages/skia/cpp/rnwgpu/api/descriptors/GPUUncapturedErrorEventInit.h index 2847377463..329bd0f9c1 100644 --- a/packages/skia/cpp/rnwgpu/api/descriptors/GPUUncapturedErrorEventInit.h +++ b/packages/skia/cpp/rnwgpu/api/descriptors/GPUUncapturedErrorEventInit.h @@ -6,7 +6,7 @@ #include "jsi2/JSIConverter.h" -#include "GPUError.h" +#include "rnwgpu/api/GPUError.h" namespace jsi = facebook::jsi; diff --git a/packages/skia/package.json b/packages/skia/package.json index 444f9a9c46..4faa5baa4c 100644 --- a/packages/skia/package.json +++ b/packages/skia/package.json @@ -5,7 +5,6 @@ "provenance": true }, "bin": { - "install-skia": "scripts/install-libs.js", "setup-skia-web": "scripts/setup-canvaskit.js" }, "title": "React Native Skia", @@ -17,7 +16,6 @@ "types": "lib/typescript/index.d.ts", "files": [ "scripts/setup-canvaskit.js", - "scripts/install-libs.js", "src/**", "lib/**", "!**/__tests__/**", @@ -37,7 +35,6 @@ "dist/**" ], "scripts": { - "postinstall": "node scripts/install-libs.js", "lint": "eslint . --ext .ts,.tsx --max-warnings 0 --cache --fix", "tsc": "tsc --noEmit", "test": "jest", @@ -47,8 +44,8 @@ "clean-skia": "yarn rimraf ./libs && yarn rimraf ../../externals/skia/out", "build-skia": "tsx ./scripts/build-skia.ts", "copy-skia-headers": "tsx ./scripts/copy-skia-headers.ts", + "install-skia": "tsx ./scripts/install-skia.ts", "install-skia-graphite": "tsx ./scripts/install-skia-graphite.ts", - "install-skia": "node scripts/install-libs.js", "clang-format": "yarn clang-format-ios && yarn clang-format-android && yarn clang-format-common", "clang-format-ios": "find apple/ -iname '*.h' -o -iname '*.mm' -o -iname '*.cpp' | xargs clang-format -i", "clang-format-android": "find android/cpp/ -iname '*.h' -o -iname '*.m' -o -iname '*.cpp' | xargs clang-format -i", @@ -92,8 +89,13 @@ } }, "devDependencies": { - "@babel/plugin-proposal-nullish-coalescing-operator": "7.18.6", + "@babel/plugin-transform-class-properties": "^7.28.0", "@babel/plugin-transform-explicit-resource-management": "^7.28.0", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.28.0", + "@babel/preset-env": "^7.28.0", + "@babel/preset-flow": "^7.28.0", + "@babel/preset-react": "^7.28.0", + "@babel/preset-typescript": "^7.28.0", "@blazediff/core": "^1.4.0", "@semantic-release/commit-analyzer": "^13.0.0", "@semantic-release/exec": "^7.0.3", @@ -116,23 +118,23 @@ "react": "19.0.0", "react-native": "0.83.1", "react-native-builder-bob": "0.18.2", - "react-native-reanimated": "^4.2.1", - "react-native-worklets": "^0.7.0", + "react-native-reanimated": "4.3.1", + "react-native-worklets": "0.8.3", "rimraf": "3.0.2", "semantic-release": "^24.1.0", "semantic-release-yarn": "^3.0.2", - "tar": "^7.5.11", + "tar": "^7.5.16", "ts-jest": "29.4.3", "tsx": "^4.21.0", "typescript": "^5.2.2", - "ws": "8.18.0" + "ws": "8.21.0" }, "dependencies": { "canvaskit-wasm": "0.41.0", - "react-native-skia-android": "147.1.0", - "react-native-skia-apple-ios": "147.1.0", - "react-native-skia-apple-macos": "147.1.0", - "react-native-skia-apple-tvos": "147.1.0", + "react-native-skia-android": "150.0.0", + "react-native-skia-apple-ios": "150.0.0", + "react-native-skia-apple-macos": "150.0.0", + "react-native-skia-apple-tvos": "150.0.0", "react-reconciler": "0.31.0" }, "eslintIgnore": [ diff --git a/packages/skia/react-native-skia.podspec b/packages/skia/react-native-skia.podspec index 407320fcda..f075e62b33 100644 --- a/packages/skia/react-native-skia.podspec +++ b/packages/skia/react-native-skia.podspec @@ -1,6 +1,7 @@ # @shopify/react-native-skia.podspec require "json" +require "fileutils" package = JSON.parse(File.read(File.join(__dir__, "package.json"))) @@ -8,6 +9,55 @@ package = JSON.parse(File.read(File.join(__dir__, "package.json"))) use_graphite = File.exist?(File.join(__dir__, 'libs', '.graphite')) puts "-- SK_GRAPHITE: #{use_graphite ? 'ON' : 'OFF'} (detected via libs/.graphite marker file)" +# Resolve a node package directory using Node's own module resolution +# (mirrors `require.resolve(pkg/package.json)`). Returns nil if it can't be found. +# Defined as a lambda (not a `def`) because CocoaPods evaluates the podspec inside +# the `Pod` module, where top-level methods are not reachable at the call site. +resolve_node_package = lambda do |name, base_dir| + script = "process.stdout.write(require('path').dirname(require.resolve('#{name}/package.json')))" + dir = Dir.chdir(base_dir) { `node -e "#{script}" 2>/dev/null`.strip } + dir.empty? ? nil : dir +end + +# Copy the prebuilt xcframeworks from the Skia npm packages into libs/. +# +# This replaces what the old npm `postinstall` script used to do. We do it here, at +# `pod install` time, so we no longer rely on a lifecycle script. CocoaPods always +# re-evaluates the podspec for path-based pods, so this runs on every install; to keep +# it cache-friendly we stamp the copied package version into libs//.version +# and skip the copy when it already matches. On a version bump the frameworks are +# re-copied and CocoaPods picks up the change. This is best-effort: if `pod install` +# does not detect the change, a clean reinstall fixes it (acceptable until the upcoming +# Swift Package Manager migration). +install_apple_skia_libs = lambda do |base_dir| + { 'ios' => 'react-native-skia-apple-ios', + 'macos' => 'react-native-skia-apple-macos', + 'tvos' => 'react-native-skia-apple-tvos' }.each do |platform, pkg_name| + pkg_dir = resolve_node_package.call(pkg_name, base_dir) + next if pkg_dir.nil? + + src = File.join(pkg_dir, 'libs') + next unless Dir.exist?(src) && !Dir.glob(File.join(src, '*.xcframework')).empty? + + version = JSON.parse(File.read(File.join(pkg_dir, 'package.json')))['version'].to_s + dest = File.join(base_dir, 'libs', platform) + marker = File.join(dest, '.version') + + # Already up to date: leave the files untouched so CocoaPods keeps its cache. + next if File.exist?(marker) && File.read(marker).strip == version + + Pod::UI.puts "react-native-skia: installing #{platform} Skia frameworks (#{version})" + FileUtils.rm_rf(dest) + FileUtils.mkdir_p(dest) + Dir.glob(File.join(src, '*.xcframework')).each { |xcf| FileUtils.cp_r(xcf, dest) } + File.write(marker, version) + end +end + +# Graphite downloads its binaries directly into libs/; only the default build needs +# the npm packages copied in. +install_apple_skia_libs.call(__dir__) unless use_graphite + # Set preprocessor definitions based on GRAPHITE flag preprocessor_defs = use_graphite ? '$(inherited) SK_GRAPHITE=1 SK_IMAGE_READ_PIXELS_DISABLE_LEGACY_API=1 SK_DISABLE_LEGACY_SHAPER_FACTORY=1' : @@ -21,22 +71,24 @@ framework_names = ['libskia', 'libsvg', 'libskshaper', 'libskparagraph', # Add Dawn library for Graphite builds (contains dawn::native symbols) framework_names += ['libdawn_combined'] if use_graphite -# Verify that prebuilt binaries have been installed by the postinstall script +# Verify that the prebuilt binaries are available (copied in above, or downloaded by +# install-skia-graphite for Graphite builds). unless Dir.exist?(File.join(__dir__, 'libs', 'ios')) && Dir.exist?(File.join(__dir__, 'libs', 'macos')) Pod::UI.warn "#{'-' * 72}" Pod::UI.warn "react-native-skia: Skia prebuilt binaries not found in libs/!" Pod::UI.warn "" - Pod::UI.warn "Run the following command to install them:" - Pod::UI.warn " npx install-skia" + Pod::UI.warn "Make sure dependencies are installed (yarn install / npm install) so that" + Pod::UI.warn "the react-native-skia-apple-* packages are present, then run `pod install` again." Pod::UI.warn "#{'-' * 72}" - raise "react-native-skia: Skia prebuilt binaries not found. Run `npx install-skia` to fix this." + raise "react-native-skia: Skia prebuilt binaries not found. Run `yarn install` then `pod install` to fix this." end # Build platform-specific framework paths (relative to pod's libs directory) -# xcframeworks are copied into libs/ by the npm postinstall script (scripts/install-libs.js) +# xcframeworks are copied into libs/ by install_apple_skia_libs above (default build) +# or downloaded by install-skia-graphite (Graphite build). ios_frameworks = framework_names.map { |f| "libs/ios/#{f}.xcframework" } osx_frameworks = framework_names.map { |f| "libs/macos/#{f}.xcframework" } -# tvOS frameworks - check if libs/tvos/ exists (populated by postinstall before pod install runs) +# tvOS frameworks - check if libs/tvos/ exists (only populated for the default build) tvos_frameworks = if use_graphite || !Dir.exist?(File.join(__dir__, 'libs', 'tvos')) [] else @@ -65,7 +117,7 @@ Pod::Spec.new do |s| 'GCC_PREPROCESSOR_DEFINITIONS' => preprocessor_defs, 'CLANG_CXX_LANGUAGE_STANDARD' => 'c++17', 'DEFINES_MODULE' => 'YES', - "HEADER_SEARCH_PATHS" => '"$(PODS_TARGET_SRCROOT)/cpp/"/** "$(PODS_TARGET_SRCROOT)/cpp" "$(PODS_TARGET_SRCROOT)/cpp/jsi2" "$(PODS_TARGET_SRCROOT)/cpp/rnwgpu" "$(PODS_TARGET_SRCROOT)/cpp/rnwgpu/api" "$(PODS_TARGET_SRCROOT)/cpp/rnwgpu/api/descriptors" "$(PODS_TARGET_SRCROOT)/cpp/rnwgpu/async" "$(PODS_TARGET_SRCROOT)/cpp/dawn/include"' + "HEADER_SEARCH_PATHS" => '"$(PODS_TARGET_SRCROOT)/cpp"/** "$(PODS_TARGET_SRCROOT)/cpp" "$(PODS_TARGET_SRCROOT)/cpp/skia" "$(PODS_TARGET_SRCROOT)/cpp/dawn/include"' } s.frameworks = ['MetalKit', 'AVFoundation', 'AVKit', 'CoreMedia'] diff --git a/packages/skia/scripts/build-skia.ts b/packages/skia/scripts/build-skia.ts index 320b6bf97f..8761213e8a 100644 --- a/packages/skia/scripts/build-skia.ts +++ b/packages/skia/scripts/build-skia.ts @@ -310,6 +310,11 @@ const buildXCFramework = (platformName: ApplePlatformName) => { const arm64ePatchFile = path.join(__dirname, "dawn-arm64e-simulator.patch"); $(`cd ${SkiaSrc} && git apply ${arm64ePatchFile}`); + // Implement drawAtlas for the Graphite backend (removes the no-op override + // so the inherited drawVertices-based default lights up the API). + const drawAtlasPatchFile = path.join(__dirname, "graphite-drawatlas.patch"); + $(`cd ${SkiaSrc} && git apply ${drawAtlasPatchFile}`); + // Remove arm64e arch flags (not available on simulator) { const filePath = `${SkiaSrc}/gn/skia/BUILD.gn`; @@ -323,7 +328,10 @@ const buildXCFramework = (platformName: ApplePlatformName) => { ` }`, ` } else if (current_cpu == "x86") {`, ].join("\n"); - const replace = [` ]`, ` } else if (current_cpu == "x86") {`].join("\n"); + const replace = [ + ` ]`, + ` } else if (current_cpu == "x86") {`, + ].join("\n"); const content = fs.readFileSync(filePath, "utf-8"); if (!content.includes(search)) { throw new Error(`Patch target not found in ${filePath}`); @@ -348,7 +356,10 @@ const buildXCFramework = (platformName: ApplePlatformName) => { const content = fs.readFileSync(filePath, "utf-8"); fs.writeFileSync( filePath, - content.replace(/uint32\(bindingInfo\.binding\)/g, "uint32_t(bindingInfo.binding)") + content.replace( + /uint32\(bindingInfo\.binding\)/g, + "uint32_t(bindingInfo.binding)" + ) ); } // Add iOS support to Dawn cmake_utils.py diff --git a/packages/skia/scripts/graphite-drawatlas.patch b/packages/skia/scripts/graphite-drawatlas.patch new file mode 100644 index 0000000000..07eae3acc8 --- /dev/null +++ b/packages/skia/scripts/graphite-drawatlas.patch @@ -0,0 +1,57 @@ +../../packages/skia/scripts/graphite-drawatlas.patch From 4d25f90dae867435707631ce0e16d2f8899ae29d Mon Sep 17 00:00:00 2001 +From: Claude +Date: Fri, 19 Jun 2026 13:54:32 +0000 +Subject: [PATCH] Implement drawAtlas for the Graphite backend + +The Graphite Device overrode SkDevice::drawAtlas() with an empty no-op, +so SkCanvas::drawAtlas() silently drew nothing on Graphite. + +drawAtlas is definitionally "drawVertices with an image shader": the +canvas layer (SkCanvas::onDrawAtlas2) already installs the atlas image +as the paint's shader, and SkDevice's default drawAtlas() expands the +RSXform/tex/color spans into an SkVertices and forwards to drawVertices(). +Graphite already fully implements drawVertices() (per-vertex colors, +primitive blender, and shader-sourced texture coordinates), so removing +the no-op override lets the inherited implementation light the API up. + +This mirrors how Graphite already defers drawRegion and drawPatch to +their default drawVertices/drawPath/drawRect routing instead of +specializing them. + +Co-Authored-By: Claude Opus 4.8 +Claude-Session: https://claude.ai/code/session_01LDRiP7kX2Wa8pD7ewEuhFf +--- + src/gpu/graphite/Device.h | 8 +++----- + 1 file changed, 3 insertions(+), 5 deletions(-) + +diff --git a/src/gpu/graphite/Device.h b/src/gpu/graphite/Device.h +index c5669d2..8b5e9ad 100644 +--- a/src/gpu/graphite/Device.h ++++ b/src/gpu/graphite/Device.h +@@ -210,8 +210,8 @@ public: + void drawPath(const SkPath& path, const SkPaint&) override; + void drawDRRect(const SkRRect& outer, const SkRRect& inner, const SkPaint&) override; + +- // No need to specialize drawRegion or drawPatch as the default impls all route to drawPath, +- // drawRect, or drawVertices as desired. ++ // No need to specialize drawRegion, drawPatch, or drawAtlas as the default impls all route to ++ // drawPath, drawRect, or drawVertices as desired. + + void drawEdgeAAQuad(const SkRect& rect, const SkPoint clip[4], + SkCanvas::QuadAAFlags aaFlags, const SkColor4f& color, +@@ -234,11 +234,9 @@ public: + const SkSamplingOptions&, + const SkPaint&, + SkCanvas::SrcRectConstraint) override; +- // TODO: Implement these using per-edge AA quads and an inlined image shader program. ++ // TODO: Implement this using per-edge AA quads and an inlined image shader program. + void drawImageLattice(const SkImage*, const SkCanvas::Lattice&, + const SkRect& dst, SkFilterMode, const SkPaint&) override {} +- void drawAtlas(SkSpan, SkSpan, SkSpan, +- sk_sp, const SkPaint&) override {} + + void drawDrawable(SkCanvas*, SkDrawable*, const SkMatrix*) override {} + void drawMesh(const SkMesh&, sk_sp, const SkPaint&) override {} +-- +2.43.0 + diff --git a/packages/skia/scripts/install-libs.js b/packages/skia/scripts/install-libs.js deleted file mode 100755 index 3174606688..0000000000 --- a/packages/skia/scripts/install-libs.js +++ /dev/null @@ -1,133 +0,0 @@ -#!/usr/bin/env node -"use strict"; - -const path = require("path"); -const fs = require("fs"); - -const useGraphite = - process.env.SK_GRAPHITE === "1" || - (process.env.SK_GRAPHITE || "").toLowerCase() === "true"; -const prefix = useGraphite ? "react-native-skia-graphite" : "react-native-skia"; -const libsDir = path.join(__dirname, "..", "libs"); - -function copySync(src, dest, options) { - if (!src.includes("*")) { - return fs.cpSync(src, dest, options); - } - - const wildcardSplit = src.split("*"); - const basePath = wildcardSplit[0]; - const files = fs.readdirSync(basePath); - files - .filter((file) => file.endsWith(wildcardSplit[1])) - .forEach((file) => { - return fs.cpSync( - path.join(basePath, file), - path.join(dest, file), - options - ); - }); -} - -// --- Apple platforms --- - -let iosPackage, macosPackage; -try { - iosPackage = path.dirname( - require.resolve(prefix + "-apple-ios/package.json") - ); - macosPackage = path.dirname( - require.resolve(prefix + "-apple-macos/package.json") - ); -} catch (e) { - console.error( - "ERROR: Could not find " + - prefix + - "-apple-ios or " + - prefix + - "-apple-macos" - ); - console.error("Make sure you have run yarn install or npm install"); - process.exit(1); -} - -// Verify xcframeworks exist in the iOS package -const iosXcf = path.join(iosPackage, "libs"); -if ( - !fs.existsSync(iosXcf) || - !fs.readdirSync(iosXcf).some((f) => f.endsWith(".xcframework")) -) { - console.error( - "ERROR: Skia prebuilt binaries not found in " + prefix + "-apple-ios!" - ); - process.exit(1); -} - -console.log("-- Skia iOS package: " + iosPackage); -console.log("-- Skia macOS package: " + macosPackage); - -// Clean and copy Apple frameworks -fs.rmSync(path.join(libsDir, "ios"), { recursive: true, force: true }); -fs.rmSync(path.join(libsDir, "macos"), { recursive: true, force: true }); -fs.rmSync(path.join(libsDir, "tvos"), { recursive: true, force: true }); -fs.mkdirSync(path.join(libsDir, "ios"), { recursive: true }); -fs.mkdirSync(path.join(libsDir, "macos"), { recursive: true }); - -copySync(iosPackage + "/libs/*.xcframework", path.join(libsDir, "ios"), { - recursive: true, -}); -copySync(macosPackage + "/libs/*.xcframework", path.join(libsDir, "macos"), { - recursive: true, -}); - -// Handle tvOS (non-Graphite only) -if (!useGraphite) { - try { - const tvosPackage = path.dirname( - require.resolve(prefix + "-apple-tvos/package.json") - ); - console.log("-- Skia tvOS package: " + tvosPackage); - fs.mkdirSync(path.join(libsDir, "tvos"), { recursive: true }); - copySync(tvosPackage + "/libs/*.xcframework", path.join(libsDir, "tvos"), { - recursive: true, - }); - } catch (e) { - console.log("-- tvOS package not found, skipping"); - } -} - -console.log("-- Copied Apple xcframeworks to libs/"); - -// --- Android --- - -const androidPackageName = useGraphite - ? "react-native-skia-graphite-android" - : "react-native-skia-android"; - -let androidPackage; -try { - androidPackage = path.dirname( - require.resolve(androidPackageName + "/package.json") - ); -} catch (e) { - console.error("ERROR: Could not find " + androidPackageName); - console.error("Make sure you have run yarn install or npm install"); - process.exit(1); -} - -const androidSrcLibs = path.join(androidPackage, "libs"); -if (!fs.existsSync(androidSrcLibs)) { - console.error( - "ERROR: Skia prebuilt binaries not found in " + androidPackageName + "!" - ); - process.exit(1); -} - -console.log("-- Skia Android package: " + androidPackage); - -// Copy Android libs (per-ABI static libraries) -const androidDest = path.join(libsDir, "android"); -fs.rmSync(androidDest, { recursive: true, force: true }); -copySync(androidSrcLibs, androidDest, { recursive: true }); - -console.log("-- Copied Android libs to libs/android/"); diff --git a/packages/skia/scripts/install-skia-graphite.ts b/packages/skia/scripts/install-skia-graphite.ts index 353e3b503c..47ea4e7dbb 100644 --- a/packages/skia/scripts/install-skia-graphite.ts +++ b/packages/skia/scripts/install-skia-graphite.ts @@ -29,20 +29,20 @@ import { fileOps } from "./utils"; // Graphite configuration const GRAPHITE_CONFIG = { - version: "m147a", + version: "m150", checksums: { "android-armeabi-v7a": - "84bdd468bd50f5e1c32ea8809eba400f1da74893f4675b2822af26ead6d3acd7", + "c8a1f9d259599280b737497a914e6fcb1b47fbf6e59537cffe3e3ebbc3aa0394", "android-arm64-v8a": - "691865ca5fb750de65f904d163a6a2feff2b671268bb1c781e73d0db99c2e41f", + "21667587386ebbaba1b61926f81dc1562443eefec5d1489b9278084f18ebb8e4", "android-x86": - "7cc2993d68ef7cb50c542b04385239674c4991e519c1890742100cddddf624f1", + "0679a34612b98b5397180e027f7592026eb4af950b36e55bf7882ea86e2cb1e3", "android-x86_64": - "06d5f88bd12ead9134a0fd512dd54e95fbb936f78d29c507155106cb49c1f163", + "eec546cf240e76129e9e6d16e51c61acfd62b7c00d655b133eddfab890c3488b", "apple-ios-xcframeworks": - "55b188a6f604411ddccff96253268db0134acb6ec5751b196b7552c104d61d7c", + "0ad5434961b22a59541c0364be20a54c5cde599c1ffd4d6b653fefb19c2119fc", "apple-macos-xcframeworks": - "a2cf4591f2471f1b36dc2eae5d4e8c098243b738656dd9daa1ea57ee59cb0a63", + "e3861d45386309dfed851488293027f3026a35684007595071426f4e46bef023", }, } as const; diff --git a/packages/skia/scripts/install-skia.ts b/packages/skia/scripts/install-skia.ts new file mode 100644 index 0000000000..2b26fc8033 --- /dev/null +++ b/packages/skia/scripts/install-skia.ts @@ -0,0 +1,55 @@ +/** + * Set up the standard (Ganesh) Skia build for development. + * + * Unlike `install-skia-graphite`, this script does NOT download or copy any + * prebuilt binaries. The standard binaries ship in the `react-native-skia-android` + * and `react-native-skia-apple-*` npm packages and are resolved straight from + * node_modules by build.gradle (Android, read in place) and the podspec (Apple, + * copied in at `pod install`). This replaces the work the old npm `postinstall` + * script used to do. + * + * It only needs to: + * 1. Undo a previous Graphite install by removing the `libs/.graphite` marker + * and the Graphite-only `cpp/dawn` headers, so the native build systems + * switch back to the standard binaries. + * 2. Copy the Skia headers required to compile against the prebuilt libraries. + * + * It deliberately leaves the contents of `libs/` in place, since `yarn build-skia` + * writes locally-built binaries there. + */ +import { existsSync, rmSync } from "fs"; +import path from "path"; + +import { copyHeaders } from "./skia-configuration"; + +const packageRoot = path.resolve(__dirname, ".."); +const repoRoot = path.resolve(packageRoot, "..", ".."); + +// Headers are copied from the Skia submodule. If it isn't checked out, copyHeaders +// would silently copy nothing (copyRecursiveSync no-ops on a missing source) and +// the failure would only surface later as missing Skia headers during compilation. +// Fail loudly here instead, with an actionable message. +const skiaInclude = path.join(repoRoot, "externals", "skia", "include"); +if (!existsSync(skiaInclude)) { + console.error( + `\n❌ Skia submodule not found at ${skiaInclude}\n` + + " Headers are copied from the Skia submodule. Check it out first:\n" + + " git submodule update --init --recursive\n" + ); + process.exit(1); +} + +const graphiteMarker = path.join(packageRoot, "libs", ".graphite"); +if (existsSync(graphiteMarker)) { + console.log("-- Removing Graphite marker (switching to the standard build)"); + rmSync(graphiteMarker, { force: true }); + // Drop the Graphite-only Dawn headers so they don't linger in a standard build. + rmSync(path.join(packageRoot, "cpp", "dawn"), { recursive: true, force: true }); +} + +console.log("-- Copying Skia headers"); +copyHeaders(); + +console.log( + "✅ Standard Skia build ready. Prebuilt binaries are resolved from node_modules." +); diff --git a/packages/skia/scripts/skia-configuration.ts b/packages/skia/scripts/skia-configuration.ts index 4db46627ab..eb7a0377ca 100644 --- a/packages/skia/scripts/skia-configuration.ts +++ b/packages/skia/scripts/skia-configuration.ts @@ -657,6 +657,10 @@ export const copyHeaders = () => { "../../externals/skia/src/base/SkArenaAlloc.h", "./cpp/skia/src/base/SkArenaAlloc.h" ); + fileOps.cp( + "../../externals/skia/src/base/SkAutoLocaleSetter.h", + "./cpp/skia/src/base/SkAutoLocaleSetter.h" + ); // Copy skunicode fileOps.mkdir("./cpp/skia/modules/skunicode/include/"); diff --git a/packages/skia/src/__tests__/globalSetup.ts b/packages/skia/src/__tests__/globalSetup.ts index 7eaac9ec89..91029463c7 100644 --- a/packages/skia/src/__tests__/globalSetup.ts +++ b/packages/skia/src/__tests__/globalSetup.ts @@ -6,6 +6,9 @@ declare global { var testClient: WebSocket; var testOS: "ios" | "android" | "web" | "node" | "macos"; var testArch: "paper" | "fabric"; + // Whether the connected device is running the Graphite backend (Dawn/WebGPU). + // Older example-app builds don't send this field, so it defaults to false. + var testGraphite: boolean; } const isOS = ( @@ -32,7 +35,7 @@ const globalSetup = () => { global.testClient = client; client.once("message", (msg) => { const obj = JSON.parse(msg.toString("utf8")); - const { OS, arch } = obj; + const { OS, arch, graphite } = obj; if (!isOS(OS)) { throw new Error("Unknown testing platform: " + OS); } @@ -41,7 +44,10 @@ const globalSetup = () => { } global.testOS = OS; global.testArch = arch; - console.log(`${OS} device connected (${arch})`); + global.testGraphite = graphite === true; + console.log( + `${OS} device connected (${arch}, graphite: ${global.testGraphite})` + ); resolve(); }); }); diff --git a/packages/skia/src/__tests__/setup.ts b/packages/skia/src/__tests__/setup.ts index 23b5f4c28f..c67d6370cb 100644 --- a/packages/skia/src/__tests__/setup.ts +++ b/packages/skia/src/__tests__/setup.ts @@ -10,6 +10,12 @@ import type { SkSurface, SkImage } from "../skia/types"; export const E2E = process.env.E2E === "true"; export const CI = process.env.CI === "true"; export const WEB = process.env.WEB === "true"; +// Whether the connected device runs the Graphite (Dawn/WebGPU) backend. Set by +// globalSetup once the example app reports its backend, so it is available by +// the time spec files are collected. Used by checkImage to pick up a +// backend-specific (…-graphite.png) baseline when one exists. +export const GRAPHITE = + E2E && (global as { testGraphite?: boolean }).testGraphite === true; export const itFailsE2e = E2E ? it.failing : it; export const itSkipsCanvasKit = WEB || !E2E ? it.skip : it; export const itRunsE2eOnly = E2E && !WEB ? it : it.skip; @@ -57,7 +63,18 @@ export const checkImage = ( const options = { ...defaultCheckImageOptions, ...opts }; const { overwrite, threshold, mute, maxPixelDiff, shouldFail } = options; const png = image.encodeToBytes(); - const p = path.resolve(__dirname, relPath); + let p = path.resolve(__dirname, relPath); + // Some Skia APIs render differently on the Graphite (Dawn/WebGPU) backend + // than on Ganesh. When a backend-specific baseline (…-graphite.png) exists we + // compare against it; otherwise we fall back to the shared baseline. With + // `overwrite` we always target the Graphite-specific file so its baseline can + // be (re)generated without touching the Ganesh one. + if (GRAPHITE) { + const graphitePath = p.replace(/\.png$/, "-graphite.png"); + if (overwrite || fs.existsSync(graphitePath)) { + p = graphitePath; + } + } if (fs.existsSync(p) && !overwrite) { const ref = fs.readFileSync(p); const baseline = PNG.sync.read(ref); diff --git a/packages/skia/src/__tests__/snapshots/platform-buffer.png b/packages/skia/src/__tests__/snapshots/platform-buffer.png index 12426ed751..d932bd16a5 100644 Binary files a/packages/skia/src/__tests__/snapshots/platform-buffer.png and b/packages/skia/src/__tests__/snapshots/platform-buffer.png differ diff --git a/packages/skia/src/renderer/__tests__/e2e/ArrayBuffer.spec.tsx b/packages/skia/src/renderer/__tests__/e2e/ArrayBuffer.spec.tsx new file mode 100644 index 0000000000..afa4731cd2 --- /dev/null +++ b/packages/skia/src/renderer/__tests__/e2e/ArrayBuffer.spec.tsx @@ -0,0 +1,79 @@ +import { surface, itRunsWithGraphite } from "../setup"; + +// Ported from react-native-webgpu (ArrayBuffer.spec.ts). Round-trips data +// through GPU buffers (writeBuffer -> copyBufferToBuffer -> mapAsync). WebGPU is +// only available on Graphite (Dawn) builds. +describe("ArrayBuffer", () => { + itRunsWithGraphite("Array Buffer (1)", async () => { + const result = await surface.eval(() => { + return Array.from(new Uint8Array([1.0, 2.0, 3.0, 4.0])); + }); + expect(result.slice(0, 4)).toEqual([1, 2, 3, 4]); + }); + + itRunsWithGraphite("Array Buffer (2)", async () => { + const result = await surface.eval((Skia) => { + const device = Skia.getDevice(); + const data = new Float32Array([1.0, 2.0, 3.0, 4.0]); + const bufferSize = data.byteLength; // 4 32-bit floats + const sourceBuffer = device.createBuffer({ + size: bufferSize, + usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST, + }); + + // Create a buffer for reading + const readBuffer = device.createBuffer({ + size: bufferSize, + usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ, + }); + + // Write data to the source buffer + device.queue.writeBuffer(sourceBuffer, 0, data); + + // Copy data from source buffer to read buffer + const encoder = device.createCommandEncoder(); + encoder.copyBufferToBuffer(sourceBuffer, 0, readBuffer, 0, bufferSize); + device.queue.submit([encoder.finish()]); + return device.queue.onSubmittedWorkDone().then(() => { + // Map the read buffer for reading + return readBuffer.mapAsync(GPUMapMode.READ).then(() => { + const readData = new Float32Array(readBuffer.getMappedRange()); + return Array.from(readData); + }); + }); + }); + expect(result).toEqual([1, 2, 3, 4]); + }); + + itRunsWithGraphite("Array Buffer (3)", async () => { + const result = await surface.eval((Skia) => { + const device = Skia.getDevice(); + const data = new Float32Array([1.0, 2.0, 3.0, 4.0]); + const bufferSize = data.byteLength; // 4 32-bit floats + const sourceBuffer = device.createBuffer({ + size: bufferSize, + usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST, + }); + + // Create a buffer for reading + const readBuffer = device.createBuffer({ + size: bufferSize, + usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ, + }); + + // Write data to the source buffer + device.queue.writeBuffer(sourceBuffer, 0, data); + + // Copy data from source buffer to read buffer + const encoder = device.createCommandEncoder(); + encoder.copyBufferToBuffer(sourceBuffer, 0, readBuffer, 0, bufferSize); + device.queue.submit([encoder.finish()]); + // Map the read buffer for reading + return readBuffer.mapAsync(GPUMapMode.READ).then(() => { + const readData = new Float32Array(readBuffer.getMappedRange()); + return Array.from(readData); + }); + }); + expect(result).toEqual([1, 2, 3, 4]); + }); +}); diff --git a/packages/skia/src/renderer/__tests__/e2e/ArrayBufferBounds.spec.tsx b/packages/skia/src/renderer/__tests__/e2e/ArrayBufferBounds.spec.tsx new file mode 100644 index 0000000000..1093e6e97b --- /dev/null +++ b/packages/skia/src/renderer/__tests__/e2e/ArrayBufferBounds.spec.tsx @@ -0,0 +1,213 @@ +import { surface, itRunsWithGraphite } from "../setup"; + +// These tests exercise the conversion of JS BufferSources (TypedArrays / +// DataView / raw ArrayBuffer) into the native rnwgpu::ArrayBuffer in +// cpp/rnwgpu/ArrayBuffer.h. +// +// The native `fromJSI` reads `byteOffset` / `byteLength` / `BYTES_PER_ELEMENT` +// directly off the JS object and does `arrayBuffer.data(runtime) + byteOffset` +// with `byteLength` as the size. Those properties must be validated against the +// real size of the backing ArrayBuffer: a spoofed object (or a view over a +// detached / resized buffer) can otherwise produce an out-of-bounds pointer or +// length and trigger a heap OOB read/write when consumed by writeBuffer. +// +// Dawn's node binding avoids this because it reads ByteOffset()/ByteLength() +// from the engine's typed-array view object (Napi), whose invariants the JS +// engine guarantees. Since JSI exposes these only as user-readable properties, +// we must validate them ourselves: byteOffset + byteLength <= buffer size. +// +// WebGPU is only available on Graphite (Dawn) builds, so these are gated on +// the Graphite backend. +describe("ArrayBuffer bounds", () => { + // -- Correctness of the legitimate offset path ------------------------------- + + itRunsWithGraphite( + "writes a typed-array view that has a non-zero byteOffset", + async () => { + const result = await surface.eval((Skia) => { + const device = Skia.getDevice(); + // A Float32Array view starting at element 1 of a 4-element buffer. + const backing = new Float32Array([10, 20, 30, 40]); + const view = backing.subarray(1, 3); // [20, 30], byteOffset = 4 + const size = view.byteLength; // 8 bytes + + const src = device.createBuffer({ + size, + usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST, + }); + const readBuffer = device.createBuffer({ + size, + usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ, + }); + + device.queue.writeBuffer(src, 0, view); + const encoder = device.createCommandEncoder(); + encoder.copyBufferToBuffer(src, 0, readBuffer, 0, size); + device.queue.submit([encoder.finish()]); + return device.queue + .onSubmittedWorkDone() + .then(() => + readBuffer + .mapAsync(GPUMapMode.READ) + .then(() => + Array.from(new Float32Array(readBuffer.getMappedRange())) + ) + ); + }); + // Must read the SLICE, not the whole backing buffer. + expect(result).toEqual([20, 30]); + } + ); + + itRunsWithGraphite( + "respects dataOffset/size element arguments of writeBuffer", + async () => { + const result = await surface.eval((Skia) => { + const device = Skia.getDevice(); + const data = new Uint32Array([1, 2, 3, 4]); + const size = 2 * data.BYTES_PER_ELEMENT; // copy elements [1,2] + + const src = device.createBuffer({ + size, + usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST, + }); + const readBuffer = device.createBuffer({ + size, + usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ, + }); + + // dataOffset = 1 element, size = 2 elements -> [2, 3] + device.queue.writeBuffer(src, 0, data, 1, 2); + const encoder = device.createCommandEncoder(); + encoder.copyBufferToBuffer(src, 0, readBuffer, 0, size); + device.queue.submit([encoder.finish()]); + return device.queue + .onSubmittedWorkDone() + .then(() => + readBuffer + .mapAsync(GPUMapMode.READ) + .then(() => + Array.from(new Uint32Array(readBuffer.getMappedRange())) + ) + ); + }); + expect(result).toEqual([2, 3]); + } + ); + + itRunsWithGraphite( + "accepts a raw ArrayBuffer as the data source", + async () => { + const result = await surface.eval((Skia) => { + const device = Skia.getDevice(); + const ab = new Uint8Array([5, 6, 7, 8]).buffer; // raw ArrayBuffer + const size = ab.byteLength; + + const src = device.createBuffer({ + size, + usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST, + }); + const readBuffer = device.createBuffer({ + size, + usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ, + }); + + device.queue.writeBuffer(src, 0, ab); + const encoder = device.createCommandEncoder(); + encoder.copyBufferToBuffer(src, 0, readBuffer, 0, size); + device.queue.submit([encoder.finish()]); + return device.queue + .onSubmittedWorkDone() + .then(() => + readBuffer + .mapAsync(GPUMapMode.READ) + .then(() => + Array.from(new Uint8Array(readBuffer.getMappedRange())) + ) + ); + }); + expect(result).toEqual([5, 6, 7, 8]); + } + ); + + // -- Out-of-bounds rejection ------------------------------------------------- + // The following cases MUST throw rather than read/write out of bounds. They + // pass once fromJSI validates byteOffset + byteLength against the real buffer + // size. Without that fix they may corrupt memory / crash instead of throwing. + + itRunsWithGraphite( + "rejects a spoofed BufferSource whose byteLength exceeds the buffer", + async () => { + await expect( + surface.eval((Skia) => { + const device = Skia.getDevice(); + const realBuffer = new ArrayBuffer(4); + // Looks like a TypedArray to native code, but lies about its size. + const spoofed = { + buffer: realBuffer, + byteOffset: 0, + byteLength: 1 << 24, // 16 MB, far beyond the 4-byte backing store + BYTES_PER_ELEMENT: 1, + }; + const dst = device.createBuffer({ + size: 256, + usage: GPUBufferUsage.COPY_DST, + }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + device.queue.writeBuffer(dst, 0, spoofed as any); + return true; + }) + ).rejects.toBeDefined(); + } + ); + + itRunsWithGraphite( + "rejects a spoofed BufferSource whose byteOffset is past the end", + async () => { + await expect( + surface.eval((Skia) => { + const device = Skia.getDevice(); + const realBuffer = new ArrayBuffer(4); + const spoofed = { + buffer: realBuffer, + byteOffset: 1 << 24, + byteLength: 4, + BYTES_PER_ELEMENT: 1, + }; + const dst = device.createBuffer({ + size: 256, + usage: GPUBufferUsage.COPY_DST, + }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + device.queue.writeBuffer(dst, 0, spoofed as any); + return true; + }) + ).rejects.toBeDefined(); + } + ); + + itRunsWithGraphite( + "rejects a BufferSource with a negative byteOffset", + async () => { + await expect( + surface.eval((Skia) => { + const device = Skia.getDevice(); + const realBuffer = new ArrayBuffer(16); + const spoofed = { + buffer: realBuffer, + byteOffset: -8, // wraps to a huge size_t in native code + byteLength: 8, + BYTES_PER_ELEMENT: 1, + }; + const dst = device.createBuffer({ + size: 256, + usage: GPUBufferUsage.COPY_DST, + }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + device.queue.writeBuffer(dst, 0, spoofed as any); + return true; + }) + ).rejects.toBeDefined(); + } + ); +}); diff --git a/packages/skia/src/renderer/__tests__/e2e/ComputeShader.spec.tsx b/packages/skia/src/renderer/__tests__/e2e/ComputeShader.spec.tsx new file mode 100644 index 0000000000..7259627d68 --- /dev/null +++ b/packages/skia/src/renderer/__tests__/e2e/ComputeShader.spec.tsx @@ -0,0 +1,274 @@ +import { surface, itRunsWithGraphite } from "../setup"; + +// Ported from react-native-webgpu (ComputeShader.spec.ts). Runs a matrix +// multiplication on a compute pipeline and checks the GPU result against a JS +// reference. WebGPU is only available on Graphite (Dawn) builds. +const multiplyMatrices = (m1: number[], m2: number[]) => { + const rows1 = m1[0], + cols1 = m1[1]; + const cols2 = m2[1]; + const result: number[] = new Array(2 + rows1 * cols2); + result[0] = rows1; + result[1] = cols2; + + for (let i = 0; i < rows1; i++) { + for (let j = 0; j < cols2; j++) { + let sum = 0; + for (let k = 0; k < cols1; k++) { + sum += m1[2 + i * cols1 + k] * m2[2 + k * cols2 + j]; + } + result[2 + i * cols2 + j] = sum; + } + } + return result; +}; + +const makeMatrix = (rows: number, columns: number) => { + const m: number[] = new Array(rows * columns + 2); + m[0] = rows; + m[1] = columns; + for (let i = 2; i < m.length; i++) { + m[i] = Math.random(); + } + return m; +}; + +const SHADER = ` +struct Matrix { + size : vec2, + numbers: array, +} + +@group(0) @binding(0) var firstMatrix : Matrix; +@group(0) @binding(1) var secondMatrix : Matrix; +@group(0) @binding(2) var resultMatrix : Matrix; + +@compute @workgroup_size(8, 8) +fn main(@builtin(global_invocation_id) global_id : vec3) { + // Guard against out-of-bounds work group sizes + if (global_id.x >= u32(firstMatrix.size.x) || global_id.y >= u32(secondMatrix.size.y)) { + return; + } + + resultMatrix.size = vec2(firstMatrix.size.x, secondMatrix.size.y); + + let resultCell = vec2(global_id.x, global_id.y); + var result = 0.0; + for (var i = 0u; i < u32(firstMatrix.size.y); i = i + 1u) { + let a = i + resultCell.x * u32(firstMatrix.size.y); + let b = resultCell.y + i * u32(secondMatrix.size.y); + result = result + firstMatrix.numbers[a] * secondMatrix.numbers[b]; + } + + let index = resultCell.y + resultCell.x * u32(secondMatrix.size.y); + resultMatrix.numbers[index] = result; +} +`; + +describe("Compute Shader", () => { + itRunsWithGraphite("matrix multiplication", async () => { + const rows = 16; + const columns = 16; + const m1 = makeMatrix(rows, columns); + const m2 = makeMatrix(rows, columns); + + const result = await surface.eval( + (Skia, { firstMatrixRaw, secondMatrixRaw, rows1, columns1, shader }) => { + const device = Skia.getDevice(); + const firstMatrix = new Float32Array(firstMatrixRaw); + const secondMatrix = new Float32Array(secondMatrixRaw); + const gpuBufferFirstMatrix = device.createBuffer({ + mappedAtCreation: true, + size: firstMatrix.byteLength, + usage: GPUBufferUsage.STORAGE, + }); + new Float32Array(gpuBufferFirstMatrix.getMappedRange()).set( + firstMatrix + ); + gpuBufferFirstMatrix.unmap(); + + const gpuBufferSecondMatrix = device.createBuffer({ + mappedAtCreation: true, + size: secondMatrix.byteLength, + usage: GPUBufferUsage.STORAGE, + }); + new Float32Array(gpuBufferSecondMatrix.getMappedRange()).set( + secondMatrix + ); + gpuBufferSecondMatrix.unmap(); + + const resultMatrixBufferSize = + Float32Array.BYTES_PER_ELEMENT * + (2 + firstMatrix[0] * secondMatrix[1]); + const resultMatrixBuffer = device.createBuffer({ + size: resultMatrixBufferSize, + usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC, + }); + + const shaderModule = device.createShaderModule({ code: shader }); + const computePipeline = device.createComputePipeline({ + layout: "auto", + compute: { module: shaderModule, entryPoint: "main" }, + }); + + const bindGroup = device.createBindGroup({ + layout: computePipeline.getBindGroupLayout(0), + entries: [ + { binding: 0, resource: { buffer: gpuBufferFirstMatrix } }, + { binding: 1, resource: { buffer: gpuBufferSecondMatrix } }, + { binding: 2, resource: { buffer: resultMatrixBuffer } }, + ], + }); + + const commandEncoder = device.createCommandEncoder(); + const passEncoder = commandEncoder.beginComputePass(); + passEncoder.setPipeline(computePipeline); + passEncoder.setBindGroup(0, bindGroup); + passEncoder.dispatchWorkgroups( + Math.ceil(rows1 / 8), + Math.ceil(columns1 / 8) + ); + passEncoder.end(); + + const gpuReadBuffer = device.createBuffer({ + size: resultMatrixBufferSize, + usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ, + }); + commandEncoder.copyBufferToBuffer( + resultMatrixBuffer, + 0, + gpuReadBuffer, + 0, + resultMatrixBufferSize + ); + device.queue.submit([commandEncoder.finish()]); + + return gpuReadBuffer.mapAsync(GPUMapMode.READ).then(() => { + const r = Array.from( + new Float32Array(gpuReadBuffer.getMappedRange()) + ); + gpuReadBuffer.unmap(); + return r; + }); + }, + { + firstMatrixRaw: m1, + secondMatrixRaw: m2, + rows1: rows, + columns1: columns, + shader: SHADER, + } + ); + + expect(result.length).toBe(16 * 16 + 2); + expect(result.some((x) => x !== 0)).toBe(true); + const reference = multiplyMatrices(m1, m2); + for (let i = 0; i < result.length; i++) { + expect(result[i]).toBeCloseTo(reference[i], 5); + } + }); + + itRunsWithGraphite("async matrix multiplication", async () => { + const rows = 16; + const columns = 16; + const m1 = makeMatrix(rows, columns); + const m2 = makeMatrix(rows, columns); + + const result = await surface.eval( + (Skia, { firstMatrixRaw, secondMatrixRaw, rows1, columns1, shader }) => { + const device = Skia.getDevice(); + const firstMatrix = new Float32Array(firstMatrixRaw); + const secondMatrix = new Float32Array(secondMatrixRaw); + const gpuBufferFirstMatrix = device.createBuffer({ + mappedAtCreation: true, + size: firstMatrix.byteLength, + usage: GPUBufferUsage.STORAGE, + }); + new Float32Array(gpuBufferFirstMatrix.getMappedRange()).set( + firstMatrix + ); + gpuBufferFirstMatrix.unmap(); + + const gpuBufferSecondMatrix = device.createBuffer({ + mappedAtCreation: true, + size: secondMatrix.byteLength, + usage: GPUBufferUsage.STORAGE, + }); + new Float32Array(gpuBufferSecondMatrix.getMappedRange()).set( + secondMatrix + ); + gpuBufferSecondMatrix.unmap(); + + const resultMatrixBufferSize = + Float32Array.BYTES_PER_ELEMENT * + (2 + firstMatrix[0] * secondMatrix[1]); + const resultMatrixBuffer = device.createBuffer({ + size: resultMatrixBufferSize, + usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC, + }); + + const shaderModule = device.createShaderModule({ code: shader }); + return device + .createComputePipelineAsync({ + layout: "auto", + compute: { module: shaderModule, entryPoint: "main" }, + }) + .then((computePipeline) => { + const bindGroup = device.createBindGroup({ + layout: computePipeline.getBindGroupLayout(0), + entries: [ + { binding: 0, resource: { buffer: gpuBufferFirstMatrix } }, + { binding: 1, resource: { buffer: gpuBufferSecondMatrix } }, + { binding: 2, resource: { buffer: resultMatrixBuffer } }, + ], + }); + + const commandEncoder = device.createCommandEncoder(); + const passEncoder = commandEncoder.beginComputePass(); + passEncoder.setPipeline(computePipeline); + passEncoder.setBindGroup(0, bindGroup); + passEncoder.dispatchWorkgroups( + Math.ceil(rows1 / 8), + Math.ceil(columns1 / 8) + ); + passEncoder.end(); + + const gpuReadBuffer = device.createBuffer({ + size: resultMatrixBufferSize, + usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ, + }); + commandEncoder.copyBufferToBuffer( + resultMatrixBuffer, + 0, + gpuReadBuffer, + 0, + resultMatrixBufferSize + ); + device.queue.submit([commandEncoder.finish()]); + + return gpuReadBuffer.mapAsync(GPUMapMode.READ).then(() => { + const r = Array.from( + new Float32Array(gpuReadBuffer.getMappedRange()) + ); + gpuReadBuffer.unmap(); + return r; + }); + }); + }, + { + firstMatrixRaw: m1, + secondMatrixRaw: m2, + rows1: rows, + columns1: columns, + shader: SHADER, + } + ); + + expect(result.length).toBe(16 * 16 + 2); + expect(result.some((x) => x !== 0)).toBe(true); + const reference = multiplyMatrices(m1, m2); + for (let i = 0; i < result.length; i++) { + expect(result[i]).toBeCloseTo(reference[i], 5); + } + }); +}); diff --git a/packages/skia/src/renderer/__tests__/e2e/DawnToggles.spec.tsx b/packages/skia/src/renderer/__tests__/e2e/DawnToggles.spec.tsx new file mode 100644 index 0000000000..8ed415819a --- /dev/null +++ b/packages/skia/src/renderer/__tests__/e2e/DawnToggles.spec.tsx @@ -0,0 +1,111 @@ +import { surface, itRunsWithGraphite } from "../setup"; + +// Ported from react-native-webgpu (DawnToggles.spec.ts). Exercises the +// non-standard, Dawn-only `dawnToggles` field on GPUDeviceDescriptor, which is +// parsed in cpp/rnwgpu/api/descriptors/GPUDeviceDescriptor.h and chained onto +// the native wgpu::DeviceDescriptor in GPUAdapter::requestDevice. WebGPU is +// only available on Graphite (Dawn) builds, and dawnToggles is a Dawn +// extension, so these are gated on the Graphite backend. +describe("Dawn toggles", () => { + itRunsWithGraphite( + "requests a device with enabled and disabled dawnToggles", + async () => { + const result = await surface.eval(() => { + return navigator.gpu.requestAdapter().then((adapter) => + adapter! + .requestDevice({ + dawnToggles: { + enabledToggles: ["disable_symbol_renaming"], + disabledToggles: ["lazy_clear_resource_on_first_use"], + }, + }) + .then((device) => !!device) + ); + }); + expect(result).toBe(true); + } + ); + + itRunsWithGraphite( + "requests a device with no dawnToggles (unchanged behavior)", + async () => { + const result = await surface.eval(() => { + return navigator.gpu + .requestAdapter() + .then((adapter) => + adapter!.requestDevice().then((device) => !!device) + ); + }); + expect(result).toBe(true); + } + ); + + itRunsWithGraphite( + "ignores unknown toggle names without failing device creation", + async () => { + const result = await surface.eval(() => { + return navigator.gpu.requestAdapter().then((adapter) => + adapter! + .requestDevice({ + dawnToggles: { enabledToggles: ["this_toggle_does_not_exist"] }, + }) + .then((device) => !!device) + ); + }); + expect(result).toBe(true); + } + ); + + // The tests above only assert that requestDevice resolves, which is true + // whether or not the toggle is parsed and chained onto the device descriptor. + // This test instead activates a real toggle (skip_validation) and observes a + // behavioral difference, so it fails if dawnToggles ever stops being applied. + itRunsWithGraphite( + "applies skip_validation so a normally-invalid buffer creates without error", + async () => { + const result = await surface.eval(() => { + // MAP_READ may only be combined with COPY_DST and MAP_WRITE only with + // COPY_SRC, so MAP_READ | MAP_WRITE is a validation error by default. + // The buffer is never used on the GPU, so creating it with validation + // skipped is harmless on every backend. + const invalid = { + size: 16, + usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.MAP_WRITE, + }; + // A fresh adapter per device: an adapter only vends a single device. + return navigator.gpu + .requestAdapter() + .then((adapter) => adapter!.requestDevice()) + .then((control) => { + control.pushErrorScope("validation"); + control.createBuffer(invalid); + return control.popErrorScope(); + }) + .then((controlError) => + navigator.gpu + .requestAdapter() + .then((adapter) => + adapter!.requestDevice({ + dawnToggles: { enabledToggles: ["skip_validation"] }, + }) + ) + .then((toggled) => { + toggled.pushErrorScope("validation"); + toggled.createBuffer(invalid); + return toggled.popErrorScope(); + }) + .then((toggledError) => ({ + controlHadError: controlError !== null, + toggledHadError: toggledError !== null, + })) + ); + }); + // The operation is genuinely invalid on this build (guards against the + // test silently passing because the buffer became valid). + expect(result.controlHadError).toBe(true); + // skip_validation took effect, which is only possible if dawnToggles was + // parsed and chained onto the device descriptor. + expect(result.toggledHadError).toBe(false); + } + ); +}); diff --git a/packages/skia/src/renderer/__tests__/e2e/ErrorScope.spec.tsx b/packages/skia/src/renderer/__tests__/e2e/ErrorScope.spec.tsx new file mode 100644 index 0000000000..13fe26c4c1 --- /dev/null +++ b/packages/skia/src/renderer/__tests__/e2e/ErrorScope.spec.tsx @@ -0,0 +1,89 @@ +import { surface, itRunsWithGraphite } from "../setup"; + +// Ported from react-native-webgpu (ErrorScope.spec.ts). Exercises +// pushErrorScope/popErrorScope validation capture. WebGPU is only available on +// Graphite (Dawn) builds. +describe("Error Scope", () => { + itRunsWithGraphite( + "should capture validation error when creating sampler with invalid maxAnisotropy", + async () => { + const result = await surface.eval((Skia) => { + const device = Skia.getDevice(); + device.pushErrorScope("validation"); + device.createSampler({ + maxAnisotropy: 0, // Invalid, maxAnisotropy must be at least 1. + }); + return device.popErrorScope().then((error) => { + if (error) { + return { + hasError: true, + messageLength: error.message.length, + messageNotEmpty: error.message.length > 0, + }; + } + return { hasError: false, messageLength: 0, messageNotEmpty: false }; + }); + }); + + expect(result.hasError).toBe(true); + expect(result.messageNotEmpty).toBe(true); + expect(result.messageLength).toBeGreaterThan(0); + } + ); + + itRunsWithGraphite( + "should capture and return error messages from popErrorScope", + async () => { + const result = await surface.eval((Skia) => { + const device = Skia.getDevice(); + // Invalid WGSL shader with syntax error (missing closing parenthesis) + const invalidShaderWGSL = `@fragment + fn main() -> @location(0) vec4f { + return vec4(1.0, 0.0, 0.0, 1.0; + }`; + device.pushErrorScope("validation"); + device.createShaderModule({ code: invalidShaderWGSL }); + return device.popErrorScope().then((error) => { + if (error) { + return { + hasError: true, + messageLength: error.message.length, + messageNotEmpty: error.message.length > 0, + messageContainsExpected: + error.message.includes("expected") || + error.message.includes("error") || + error.message.includes("parsing"), + }; + } + return { + hasError: false, + messageLength: 0, + messageNotEmpty: false, + messageContainsExpected: false, + }; + }); + }); + expect(result.hasError).toBe(true); + expect(result.messageNotEmpty).toBe(true); + expect(result.messageLength).toBeGreaterThan(0); + expect(result.messageContainsExpected).toBe(true); + } + ); + + itRunsWithGraphite("should return null when no error occurs", async () => { + const result = await surface.eval((Skia) => { + const device = Skia.getDevice(); + const validShaderWGSL = `@fragment + fn main() -> @location(0) vec4f { + return vec4(1.0, 0.0, 0.0, 1.0); + }`; + device.pushErrorScope("validation"); + // This should not generate any errors + device.createShaderModule({ code: validShaderWGSL }); + return device.popErrorScope().then((error) => ({ + hasError: error !== null, + })); + }); + expect(result.hasError).toBe(false); + }); +}); diff --git a/packages/skia/src/renderer/__tests__/e2e/ExternalTexture.spec.tsx b/packages/skia/src/renderer/__tests__/e2e/ExternalTexture.spec.tsx new file mode 100644 index 0000000000..16013ba7fe --- /dev/null +++ b/packages/skia/src/renderer/__tests__/e2e/ExternalTexture.spec.tsx @@ -0,0 +1,234 @@ +import { surface, itRunsWithGraphite } from "../setup"; + +// Ported from react-native-webgpu (ExternalTexture.spec.ts), adapted to Skia. +// Exercises queue.copyExternalImageToTexture: decode an image via +// createImageBitmap, upload it into a GPUTexture, then either sample it through +// a textured quad or read the texture back directly. Self-validating via pixel +// readback (no snapshot assets). WebGPU is only available on Graphite (Dawn) +// builds; createImageBitmap is a native binding that may be unbound. + +const TEXTURED_QUAD_SHADER = /* wgsl */ ` + struct VsOut { + @builtin(position) position: vec4f, + @location(0) uv: vec2f, + }; + + @vertex fn vs(@builtin(vertex_index) vid: u32) -> VsOut { + var positions = array( + vec2f(-1.0, -3.0), + vec2f(-1.0, 1.0), + vec2f( 3.0, 1.0), + ); + var uvs = array( + vec2f(0.0, 2.0), + vec2f(0.0, 0.0), + vec2f(2.0, 0.0), + ); + var out: VsOut; + out.position = vec4f(positions[vid], 0.0, 1.0); + out.uv = uvs[vid]; + return out; + } + + @group(0) @binding(0) var srcSampler: sampler; + @group(0) @binding(1) var srcTex: texture_2d; + + @fragment fn fs(in: VsOut) -> @location(0) vec4f { + return textureSample(srcTex, srcSampler, in.uv); + } +`; + +const isCreateImageBitmapBound = () => + surface.eval(() => typeof createImageBitmap === "function"); + +describe("External Textures", () => { + itRunsWithGraphite( + "uploads an ImageBitmap with copyExternalImageToTexture and samples it", + async () => { + if (!(await isCreateImageBitmapBound())) { + console.warn("createImageBitmap is not bound — skipping"); + return; + } + const result = await surface.eval( + (Skia, { shader }) => { + const SIZE = 64; + // Encode a solid-blue image, then decode it as an ImageBitmap. + const offscreen = Skia.Surface.MakeOffscreen(SIZE, SIZE)!; + offscreen.getCanvas().drawColor(Skia.Color("blue")); + offscreen.flush(); + const png = offscreen.makeImageSnapshot().encodeToBytes(); + + const device = Skia.getDevice(); + return createImageBitmap( + png.buffer as unknown as ImageBitmapSource + ).then((bitmap) => { + const format: GPUTextureFormat = "rgba8unorm"; + const texture = device.createTexture({ + size: [bitmap.width, bitmap.height], + format, + usage: + GPUTextureUsage.TEXTURE_BINDING | + GPUTextureUsage.COPY_DST | + GPUTextureUsage.RENDER_ATTACHMENT, + }); + device.queue.copyExternalImageToTexture( + { source: bitmap }, + { texture }, + { width: bitmap.width, height: bitmap.height } + ); + + const module = device.createShaderModule({ code: shader }); + const pipeline = device.createRenderPipeline({ + layout: "auto", + vertex: { module, entryPoint: "vs" }, + fragment: { module, entryPoint: "fs", targets: [{ format }] }, + primitive: { topology: "triangle-list" }, + }); + const sampler = device.createSampler({ + magFilter: "linear", + minFilter: "linear", + }); + const bindGroup = device.createBindGroup({ + layout: pipeline.getBindGroupLayout(0), + entries: [ + { binding: 0, resource: sampler }, + { binding: 1, resource: texture.createView() }, + ], + }); + + const target = device.createTexture({ + size: [SIZE, SIZE], + format, + usage: + GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC, + }); + const bytesPerRow = SIZE * 4; + const readBuffer = device.createBuffer({ + size: bytesPerRow * SIZE, + usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ, + }); + + const encoder = device.createCommandEncoder(); + const pass = encoder.beginRenderPass({ + colorAttachments: [ + { + view: target.createView(), + clearValue: { r: 0, g: 0, b: 0, a: 1 }, + loadOp: "clear", + storeOp: "store", + }, + ], + }); + pass.setPipeline(pipeline); + pass.setBindGroup(0, bindGroup); + pass.draw(3); + pass.end(); + encoder.copyTextureToBuffer( + { texture: target }, + { buffer: readBuffer, bytesPerRow }, + { width: SIZE, height: SIZE } + ); + device.queue.submit([encoder.finish()]); + + return readBuffer.mapAsync(GPUMapMode.READ).then(() => { + const pixels = new Uint8Array( + readBuffer.getMappedRange().slice(0) + ); + readBuffer.unmap(); + bitmap.close(); + const offset = + (Math.floor(SIZE / 2) * SIZE + Math.floor(SIZE / 2)) * 4; + return [ + pixels[offset], + pixels[offset + 1], + pixels[offset + 2], + pixels[offset + 3], + ]; + }); + }); + }, + { shader: TEXTURED_QUAD_SHADER } + ); + const [r, g, b, a] = result; + // Sampled the solid-blue uploaded image. + expect(b).toBeGreaterThan(200); + expect(r).toBeLessThan(60); + expect(g).toBeLessThan(60); + expect(a).toBeGreaterThan(200); + } + ); + + itRunsWithGraphite( + "copyExternalImageToTexture flipY uploads rows bottom-up", + async () => { + if (!(await isCreateImageBitmapBound())) { + console.warn("createImageBitmap is not bound — skipping"); + return; + } + const result = await surface.eval((Skia) => { + const SIZE = 64; + // Top half red, bottom half blue. + const offscreen = Skia.Surface.MakeOffscreen(SIZE, SIZE)!; + const canvas = offscreen.getCanvas(); + const paint = Skia.Paint(); + paint.setColor(Skia.Color("red")); + canvas.drawRect(Skia.XYWHRect(0, 0, SIZE, SIZE / 2), paint); + paint.setColor(Skia.Color("blue")); + canvas.drawRect(Skia.XYWHRect(0, SIZE / 2, SIZE, SIZE / 2), paint); + offscreen.flush(); + const png = offscreen.makeImageSnapshot().encodeToBytes(); + + const device = Skia.getDevice(); + return createImageBitmap( + png.buffer as unknown as ImageBitmapSource + ).then((bitmap) => { + const format: GPUTextureFormat = "rgba8unorm"; + const texture = device.createTexture({ + size: [bitmap.width, bitmap.height], + format, + usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.COPY_SRC, + }); + // flipY: source row 0 (top, red) ends up at the bottom of the + // texture, so the texture's top row is the source's bottom (blue). + device.queue.copyExternalImageToTexture( + { source: bitmap, flipY: true }, + { texture }, + { width: bitmap.width, height: bitmap.height } + ); + + const bytesPerRow = SIZE * 4; + const readBuffer = device.createBuffer({ + size: bytesPerRow * SIZE, + usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ, + }); + const encoder = device.createCommandEncoder(); + encoder.copyTextureToBuffer( + { texture }, + { buffer: readBuffer, bytesPerRow }, + { width: SIZE, height: SIZE } + ); + device.queue.submit([encoder.finish()]); + + return readBuffer.mapAsync(GPUMapMode.READ).then(() => { + const pixels = new Uint8Array(readBuffer.getMappedRange().slice(0)); + readBuffer.unmap(); + bitmap.close(); + // Column center, top row (y=2) and bottom row (y=SIZE-2). + const cx = Math.floor(SIZE / 2); + const top = (2 * SIZE + cx) * 4; + const bottom = ((SIZE - 2) * SIZE + cx) * 4; + return { + top: [pixels[top], pixels[top + 1], pixels[top + 2]], + bottom: [pixels[bottom], pixels[bottom + 1], pixels[bottom + 2]], + }; + }); + }); + }); + // After flipY, the texture top row is blue and the bottom row is red. + expect(result.top[2]).toBeGreaterThan(200); // blue + expect(result.top[0]).toBeLessThan(60); + expect(result.bottom[0]).toBeGreaterThan(200); // red + expect(result.bottom[2]).toBeLessThan(60); + } + ); +}); diff --git a/packages/skia/src/renderer/__tests__/e2e/ImageBitmap.spec.tsx b/packages/skia/src/renderer/__tests__/e2e/ImageBitmap.spec.tsx new file mode 100644 index 0000000000..9c73214771 --- /dev/null +++ b/packages/skia/src/renderer/__tests__/e2e/ImageBitmap.spec.tsx @@ -0,0 +1,82 @@ +import { surface, itRunsWithGraphite, images, resolveFile } from "../setup"; + +// Ported from react-native-webgpu (PR #354). Covers ImageBitmap.close(), which +// releases the decoded pixels and zeroes width/height (idempotent). +// +// createImageBitmap is a native-only binding; until it is installed in +// RNSkManager each test self-skips (with a warning) rather than vacuously +// running. WebGPU is only available on Graphite (Dawn) builds. +const PNG_URI = "skia/__tests__/assets/skia_logo.png"; +const pngBytes = Array.from(resolveFile(PNG_URI)); + +const isCreateImageBitmapBound = () => + surface.eval(() => typeof createImageBitmap === "function"); + +describe("ImageBitmap", () => { + itRunsWithGraphite("close() zeroes width and height", async () => { + if (!(await isCreateImageBitmapBound())) { + console.warn("createImageBitmap is not bound — skipping"); + return; + } + const result = await surface.eval( + (_Skia, { pngData }) => { + const bytes = new Uint8Array(pngData); + // Non-standard ArrayBuffer overload (native binding), not in lib.dom. + return createImageBitmap( + bytes.buffer as unknown as ImageBitmapSource + ).then((bmp) => { + const before = { width: bmp.width, height: bmp.height }; + bmp.close(); + const after = { width: bmp.width, height: bmp.height }; + return { before, after }; + }); + }, + { pngData: pngBytes } + ); + expect(result.before.width).toBe(images.skiaLogoPng.width()); + expect(result.before.height).toBe(images.skiaLogoPng.height()); + expect(result.after.width).toBe(0); + expect(result.after.height).toBe(0); + }); + + itRunsWithGraphite("close() is idempotent", async () => { + if (!(await isCreateImageBitmapBound())) { + console.warn("createImageBitmap is not bound — skipping"); + return; + } + const result = await surface.eval( + (_Skia, { pngData }) => { + const bytes = new Uint8Array(pngData); + // Non-standard ArrayBuffer overload (native binding), not in lib.dom. + return createImageBitmap( + bytes.buffer as unknown as ImageBitmapSource + ).then((bmp) => { + bmp.close(); + bmp.close(); + return { width: bmp.width, height: bmp.height }; + }); + }, + { pngData: pngBytes } + ); + expect(result.width).toBe(0); + expect(result.height).toBe(0); + }); + + itRunsWithGraphite("exposes close as a function", async () => { + if (!(await isCreateImageBitmapBound())) { + console.warn("createImageBitmap is not bound — skipping"); + return; + } + const result = await surface.eval( + (_Skia, { pngData }) => { + const bytes = new Uint8Array(pngData); + // Non-standard ArrayBuffer overload (native binding), not in lib.dom. + return createImageBitmap( + bytes.buffer as unknown as ImageBitmapSource + ).then((bmp) => typeof bmp.close); + }, + { pngData: pngBytes } + ); + expect(result).toBe("function"); + }); +}); diff --git a/packages/skia/src/renderer/__tests__/e2e/ImageBitmapBounds.spec.tsx b/packages/skia/src/renderer/__tests__/e2e/ImageBitmapBounds.spec.tsx new file mode 100644 index 0000000000..6b0bc641ad --- /dev/null +++ b/packages/skia/src/renderer/__tests__/e2e/ImageBitmapBounds.spec.tsx @@ -0,0 +1,94 @@ +import { surface, itRunsWithGraphite } from "../setup"; + +// These tests exercise the same BufferSource bounds checking as +// ArrayBufferBounds.spec.tsx, but through the non-standard createImageBitmap +// overload that accepts an ArrayBuffer / TypedArray of encoded image bytes. +// +// That path must reuse the shared rnwgpu::ArrayBuffer converter +// (cpp/rnwgpu/ArrayBuffer.h), so a spoofed BufferSource that lies about +// byteOffset / byteLength is rejected rather than producing an out-of-bounds +// read when the bytes are copied. +// +// NOTE: createImageBitmap is a native-only (non-standard) binding. It is not +// bound in Skia's runtime yet, so each test self-skips (with a warning) until +// the binding is installed in RNSkManager. Once createImageBitmap is exposed, +// these run automatically on Graphite builds. They are skipped on web (the +// overload is native-only) via the Graphite gate. + +// Returns true when the connected device exposes a createImageBitmap binding. +// Until the native binding lands these specs are inert rather than vacuously +// passing (any rejection — including "createImageBitmap is not defined" — +// would otherwise satisfy `.rejects`). +const isCreateImageBitmapBound = () => + surface.eval(() => typeof createImageBitmap === "function"); + +describe("createImageBitmap bounds", () => { + itRunsWithGraphite( + "rejects a spoofed BufferSource whose byteLength exceeds the buffer", + async () => { + if (!(await isCreateImageBitmapBound())) { + console.warn("createImageBitmap is not bound — skipping"); + return; + } + await expect( + surface.eval(() => { + const realBuffer = new ArrayBuffer(4); + const spoofed = { + buffer: realBuffer, + byteOffset: 0, + byteLength: 1 << 24, // 16 MB, far beyond the 4-byte backing store + BYTES_PER_ELEMENT: 1, + }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return createImageBitmap(spoofed as any).then(() => true); + }) + ).rejects.toBeDefined(); + } + ); + + itRunsWithGraphite( + "rejects a spoofed BufferSource whose byteOffset is past the end", + async () => { + if (!(await isCreateImageBitmapBound())) { + console.warn("createImageBitmap is not bound — skipping"); + return; + } + await expect( + surface.eval(() => { + const realBuffer = new ArrayBuffer(4); + const spoofed = { + buffer: realBuffer, + byteOffset: 1 << 24, + byteLength: 4, + BYTES_PER_ELEMENT: 1, + }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return createImageBitmap(spoofed as any).then(() => true); + }) + ).rejects.toBeDefined(); + } + ); + + itRunsWithGraphite( + "rejects a BufferSource with a negative byteOffset", + async () => { + if (!(await isCreateImageBitmapBound())) { + console.warn("createImageBitmap is not bound — skipping"); + return; + } + await expect( + surface.eval(() => { + const realBuffer = new ArrayBuffer(16); + const spoofed = { + buffer: realBuffer, + byteOffset: -8, // wraps to a huge size_t in native code + byteLength: 8, + BYTES_PER_ELEMENT: 1, + }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return createImageBitmap(spoofed as any).then(() => true); + }) + ).rejects.toBeDefined(); + } + ); +}); diff --git a/packages/skia/src/renderer/__tests__/e2e/ImportExternalTexture.spec.tsx b/packages/skia/src/renderer/__tests__/e2e/ImportExternalTexture.spec.tsx new file mode 100644 index 0000000000..0e575eb6c5 --- /dev/null +++ b/packages/skia/src/renderer/__tests__/e2e/ImportExternalTexture.spec.tsx @@ -0,0 +1,325 @@ +import { surface, itRunsWithGraphite } from "../setup"; + +// Ported from react-native-webgpu (ImportExternalTexture.spec.ts), adapted to +// Skia's NativeBuffer API as the external-texture source (Skia has no +// VideoFrame). Draws a known solid color into an SkImage, wraps it as a native +// buffer (CVPixelBufferRef / AHardwareBuffer), imports it as a +// GPUExternalTexture, samples it through a render pass, reads the result back, +// and checks the sampled color matches. WebGPU is only available on Graphite +// (Dawn) builds. + +// NativeBuffer is iOS/Android only, and unavailable on the API-21 Fabric +// Android emulator (mirrors NativeBuffer.spec.tsx's shouldNativeBufferTestRun). +const supportsNativeBuffer = () => { + if (surface.OS !== "ios" && surface.OS !== "android") { + return false; + } + if (surface.arch === "fabric" && surface.OS === "android") { + return false; + } + return true; +}; + +describe("ImportExternalTexture", () => { + itRunsWithGraphite( + "imports a NativeBuffer as an external texture and samples it", + async () => { + if (!supportsNativeBuffer()) { + return; + } + const result = await surface.eval((Skia) => { + const SIZE = 64; + + // 1. Draw a solid red image and wrap it as a native buffer. + const offscreen = Skia.Surface.MakeOffscreen(SIZE, SIZE)!; + const canvas = offscreen.getCanvas(); + canvas.drawColor(Skia.Color("red")); + offscreen.flush(); + const image = offscreen.makeImageSnapshot(); + const nativeBuffer = Skia.NativeBuffer.MakeFromImage(image); + + const device = Skia.getDevice(); + try { + // 2. Import it. The GPUExternalTexture owns the shared-memory access + // window; there is no createTexture / beginAccess to manage. + // Skia's binding accepts the native buffer pointer as `source` (see + // the SkiaGPUExternalTextureDescriptor overload it adds to GPUDevice). + const externalTexture = device.importExternalTexture({ + source: nativeBuffer, + label: "test-frame", + }); + + // 3. Sample the external texture into a render target. + const module = device.createShaderModule({ + code: /* wgsl */ ` + struct VsOut { + @builtin(position) position: vec4f, + @location(0) uv: vec2f, + }; + + @vertex fn vs(@builtin(vertex_index) vid: u32) -> VsOut { + var positions = array( + vec2f(-1.0, -3.0), + vec2f(-1.0, 1.0), + vec2f( 3.0, 1.0), + ); + var uvs = array( + vec2f(0.0, 2.0), + vec2f(0.0, 0.0), + vec2f(2.0, 0.0), + ); + var out: VsOut; + out.position = vec4f(positions[vid], 0.0, 1.0); + out.uv = uvs[vid]; + return out; + } + + @group(0) @binding(0) var srcTex: texture_external; + @group(0) @binding(1) var srcSampler: sampler; + + @fragment fn fs(in: VsOut) -> @location(0) vec4f { + return textureSampleBaseClampToEdge(srcTex, srcSampler, in.uv); + } + `, + }); + const format: GPUTextureFormat = "rgba8unorm"; + const pipeline = device.createRenderPipeline({ + layout: "auto", + vertex: { module, entryPoint: "vs" }, + fragment: { module, entryPoint: "fs", targets: [{ format }] }, + primitive: { topology: "triangle-list" }, + }); + const sampler = device.createSampler({ + magFilter: "linear", + minFilter: "linear", + }); + const bindGroup = device.createBindGroup({ + layout: pipeline.getBindGroupLayout(0), + entries: [ + { binding: 0, resource: externalTexture }, + { binding: 1, resource: sampler }, + ], + }); + + const target = device.createTexture({ + size: [SIZE, SIZE], + format, + usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC, + }); + const bytesPerRow = SIZE * 4; // 256, already 256-byte aligned + const readBuffer = device.createBuffer({ + size: bytesPerRow * SIZE, + usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ, + }); + + const encoder = device.createCommandEncoder(); + const pass = encoder.beginRenderPass({ + colorAttachments: [ + { + view: target.createView(), + clearValue: { r: 0, g: 0, b: 0, a: 1 }, + loadOp: "clear", + storeOp: "store", + }, + ], + }); + pass.setPipeline(pipeline); + pass.setBindGroup(0, bindGroup); + pass.draw(3); + pass.end(); + encoder.copyTextureToBuffer( + { texture: target }, + { buffer: readBuffer, bytesPerRow }, + { width: SIZE, height: SIZE } + ); + device.queue.submit([encoder.finish()]); + // End the access window now that the sampling work is submitted. + externalTexture.destroy(); + + return readBuffer.mapAsync(GPUMapMode.READ).then(() => { + const pixels = new Uint8Array(readBuffer.getMappedRange().slice(0)); + readBuffer.unmap(); + // Release the native buffer: all GPU work referencing it is + // submitted and the pixels have been read back. + Skia.NativeBuffer.Release(nativeBuffer); + // Center pixel. + const offset = + (Math.floor(SIZE / 2) * SIZE + Math.floor(SIZE / 2)) * 4; + return [ + pixels[offset], + pixels[offset + 1], + pixels[offset + 2], + pixels[offset + 3], + ]; + }); + } catch (e) { + Skia.NativeBuffer.Release(nativeBuffer); + throw e; + } + }); + const [r, g, b, a] = result; + // Sampled the solid-red source: red high, green/blue low, opaque. + expect(r).toBeGreaterThan(200); + expect(g).toBeLessThan(60); + expect(b).toBeLessThan(60); + expect(a).toBeGreaterThan(200); + } + ); + + // A solid color can't catch a broken UV mapping (every texel is the same), so + // this samples a 4-quadrant image and checks all four distinct colors survive + // — a collapsed/degenerate transform would repeat or blend them. + itRunsWithGraphite( + "preserves the 2D layout of a non-uniform external texture", + async () => { + if (!supportsNativeBuffer()) { + return; + } + const result = await surface.eval((Skia) => { + const SIZE = 64; + const half = SIZE / 2; + // Four distinct quadrants: TL red, TR green, BL blue, BR yellow. + const offscreen = Skia.Surface.MakeOffscreen(SIZE, SIZE)!; + const canvas = offscreen.getCanvas(); + const paint = Skia.Paint(); + const quadrants: [string, number, number][] = [ + ["red", 0, 0], + ["lime", half, 0], + ["blue", 0, half], + ["yellow", half, half], + ]; + quadrants.forEach(([color, x, y]) => { + paint.setColor(Skia.Color(color)); + canvas.drawRect(Skia.XYWHRect(x, y, half, half), paint); + }); + offscreen.flush(); + const nativeBuffer = Skia.NativeBuffer.MakeFromImage( + offscreen.makeImageSnapshot() + ); + + const device = Skia.getDevice(); + try { + const externalTexture = device.importExternalTexture({ + source: nativeBuffer, + label: "grid-frame", + }); + const module = device.createShaderModule({ + code: /* wgsl */ ` + struct VsOut { + @builtin(position) position: vec4f, + @location(0) uv: vec2f, + }; + @vertex fn vs(@builtin(vertex_index) vid: u32) -> VsOut { + var positions = array( + vec2f(-1.0, -3.0), vec2f(-1.0, 1.0), vec2f(3.0, 1.0), + ); + var uvs = array( + vec2f(0.0, 2.0), vec2f(0.0, 0.0), vec2f(2.0, 0.0), + ); + var out: VsOut; + out.position = vec4f(positions[vid], 0.0, 1.0); + out.uv = uvs[vid]; + return out; + } + @group(0) @binding(0) var srcTex: texture_external; + @group(0) @binding(1) var srcSampler: sampler; + @fragment fn fs(in: VsOut) -> @location(0) vec4f { + return textureSampleBaseClampToEdge(srcTex, srcSampler, in.uv); + } + `, + }); + const format: GPUTextureFormat = "rgba8unorm"; + const pipeline = device.createRenderPipeline({ + layout: "auto", + vertex: { module, entryPoint: "vs" }, + fragment: { module, entryPoint: "fs", targets: [{ format }] }, + primitive: { topology: "triangle-list" }, + }); + // Nearest filtering so quadrant centers read back as pure colors. + const sampler = device.createSampler({}); + const bindGroup = device.createBindGroup({ + layout: pipeline.getBindGroupLayout(0), + entries: [ + { binding: 0, resource: externalTexture }, + { binding: 1, resource: sampler }, + ], + }); + + const target = device.createTexture({ + size: [SIZE, SIZE], + format, + usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC, + }); + const bytesPerRow = SIZE * 4; + const readBuffer = device.createBuffer({ + size: bytesPerRow * SIZE, + usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ, + }); + + const encoder = device.createCommandEncoder(); + const pass = encoder.beginRenderPass({ + colorAttachments: [ + { + view: target.createView(), + clearValue: { r: 0, g: 0, b: 0, a: 1 }, + loadOp: "clear", + storeOp: "store", + }, + ], + }); + pass.setPipeline(pipeline); + pass.setBindGroup(0, bindGroup); + pass.draw(3); + pass.end(); + encoder.copyTextureToBuffer( + { texture: target }, + { buffer: readBuffer, bytesPerRow }, + { width: SIZE, height: SIZE } + ); + device.queue.submit([encoder.finish()]); + externalTexture.destroy(); + + return readBuffer.mapAsync(GPUMapMode.READ).then(() => { + const pixels = new Uint8Array(readBuffer.getMappedRange().slice(0)); + readBuffer.unmap(); + Skia.NativeBuffer.Release(nativeBuffer); + // Sample the center of each rendered quadrant. + const q = SIZE / 4; + const at = (x: number, y: number) => { + const o = (y * SIZE + x) * 4; + return [pixels[o], pixels[o + 1], pixels[o + 2]]; + }; + return [at(q, q), at(3 * q, q), at(q, 3 * q), at(3 * q, 3 * q)]; + }); + } catch (e) { + Skia.NativeBuffer.Release(nativeBuffer); + throw e; + } + }); + // Classify each sampled quadrant by dominant channels. Orientation- + // agnostic: a correct 2D mapping yields the four distinct source colors + // (in some orientation); a collapse would repeat or blend them. + const classify = ([r, g, b]: number[]) => { + if (r > 180 && g > 180 && b < 80) { + return "yellow"; + } + if (r > 180 && g < 80 && b < 80) { + return "red"; + } + if (r < 80 && g > 180 && b < 80) { + return "green"; + } + if (r < 80 && g < 80 && b > 180) { + return "blue"; + } + return "other"; + }; + expect(result.map(classify).sort()).toEqual([ + "blue", + "green", + "red", + "yellow", + ]); + } + ); +}); diff --git a/packages/skia/src/renderer/__tests__/e2e/SharedTextureMemory.spec.tsx b/packages/skia/src/renderer/__tests__/e2e/SharedTextureMemory.spec.tsx new file mode 100644 index 0000000000..f963d488b1 --- /dev/null +++ b/packages/skia/src/renderer/__tests__/e2e/SharedTextureMemory.spec.tsx @@ -0,0 +1,305 @@ +import { surface, itRunsWithGraphite } from "../setup"; + +// Ported from react-native-webgpu (SharedTextureMemory.spec.ts), adapted to +// Skia's NativeBuffer API as the shared-memory source (Skia has no VideoFrame). +// Draws a known solid color into an SkImage, wraps it as a native buffer +// (CVPixelBufferRef / AHardwareBuffer), imports it via +// device.importSharedTextureMemory, creates a texture aliasing it, samples it +// through a textured quad, reads the result back, and checks the sampled color. +// WebGPU is only available on Graphite (Dawn) builds, and NativeBuffer only on +// iOS/Android (not on the API-21 Fabric Android emulator). + +const SHARED_TEXTURE_SHADER = /* wgsl */ ` + struct VsOut { + @builtin(position) position: vec4f, + @location(0) uv: vec2f, + }; + + @vertex fn vs(@builtin(vertex_index) vid: u32) -> VsOut { + var positions = array( + vec2f(-1.0, -3.0), + vec2f(-1.0, 1.0), + vec2f( 3.0, 1.0), + ); + var uvs = array( + vec2f(0.0, 2.0), + vec2f(0.0, 0.0), + vec2f(2.0, 0.0), + ); + var out: VsOut; + out.position = vec4f(positions[vid], 0.0, 1.0); + out.uv = uvs[vid]; + return out; + } + + @group(0) @binding(0) var srcTex: texture_2d; + @group(0) @binding(1) var srcSampler: sampler; + + @fragment fn fs(in: VsOut) -> @location(0) vec4f { + return textureSample(srcTex, srcSampler, in.uv); + } +`; + +// NativeBuffer is iOS/Android only, and unavailable on the API-21 Fabric +// Android emulator (mirrors NativeBuffer.spec.tsx's shouldNativeBufferTestRun). +const supportsNativeBuffer = () => { + if (surface.OS !== "ios" && surface.OS !== "android") { + return false; + } + if (surface.arch === "fabric" && surface.OS === "android") { + return false; + } + return true; +}; + +describe("SharedTextureMemory", () => { + itRunsWithGraphite( + "imports a NativeBuffer and samples it through a textured quad", + async () => { + if (!supportsNativeBuffer()) { + return; + } + const result = await surface.eval( + (Skia, { shader }) => { + const SIZE = 64; + const offscreen = Skia.Surface.MakeOffscreen(SIZE, SIZE)!; + offscreen.getCanvas().drawColor(Skia.Color("red")); + offscreen.flush(); + const nativeBuffer = Skia.NativeBuffer.MakeFromImage( + offscreen.makeImageSnapshot() + ); + + const device = Skia.getDevice(); + try { + // importSharedTextureMemory is a Skia extension to GPUDevice (see + // the types in src/skia/types/WebGPU.ts). + const memory = device.importSharedTextureMemory({ + handle: nativeBuffer, + label: "test-frame", + }); + const texture = memory.createTexture(); + // beginAccess returns void and throws on failure. + memory.beginAccess(texture, true); + + const module = device.createShaderModule({ code: shader }); + const format: GPUTextureFormat = "rgba8unorm"; + const pipeline = device.createRenderPipeline({ + layout: "auto", + vertex: { module, entryPoint: "vs" }, + fragment: { module, entryPoint: "fs", targets: [{ format }] }, + primitive: { topology: "triangle-list" }, + }); + const sampler = device.createSampler({ + magFilter: "linear", + minFilter: "linear", + }); + const bindGroup = device.createBindGroup({ + layout: pipeline.getBindGroupLayout(0), + entries: [ + { binding: 0, resource: texture.createView() }, + { binding: 1, resource: sampler }, + ], + }); + + const target = device.createTexture({ + size: [SIZE, SIZE], + format, + usage: + GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC, + }); + const bytesPerRow = SIZE * 4; // 256, already 256-byte aligned + const readBuffer = device.createBuffer({ + size: bytesPerRow * SIZE, + usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ, + }); + + const encoder = device.createCommandEncoder(); + const pass = encoder.beginRenderPass({ + colorAttachments: [ + { + view: target.createView(), + clearValue: { r: 0, g: 0, b: 0, a: 1 }, + loadOp: "clear", + storeOp: "store", + }, + ], + }); + pass.setPipeline(pipeline); + pass.setBindGroup(0, bindGroup); + pass.draw(3); + pass.end(); + encoder.copyTextureToBuffer( + { texture: target }, + { buffer: readBuffer, bytesPerRow }, + { width: SIZE, height: SIZE } + ); + device.queue.submit([encoder.finish()]); + + return readBuffer.mapAsync(GPUMapMode.READ).then(() => { + const pixels = new Uint8Array( + readBuffer.getMappedRange().slice(0) + ); + readBuffer.unmap(); + memory.endAccess(texture); + texture.destroy(); + Skia.NativeBuffer.Release(nativeBuffer); + const offset = + (Math.floor(SIZE / 2) * SIZE + Math.floor(SIZE / 2)) * 4; + return [ + pixels[offset], + pixels[offset + 1], + pixels[offset + 2], + pixels[offset + 3], + ]; + }); + } catch (e) { + Skia.NativeBuffer.Release(nativeBuffer); + throw e; + } + }, + { shader: SHARED_TEXTURE_SHADER } + ); + const [r, g, b, a] = result; + // Sampled the solid-red source. + expect(r).toBeGreaterThan(200); + expect(g).toBeLessThan(60); + expect(b).toBeLessThan(60); + expect(a).toBeGreaterThan(200); + } + ); + + // Same as above, but with an *explicit* bind group layout + // (createBindGroupLayout + createPipelineLayout) instead of layout: "auto". + // This exercises the native BindGroupLayoutEntry conversion path, which + // "auto" layouts bypass entirely. + itRunsWithGraphite( + "samples a shared texture through an explicit bind group layout", + async () => { + if (!supportsNativeBuffer()) { + return; + } + const result = await surface.eval( + (Skia, { shader }) => { + const SIZE = 64; + const offscreen = Skia.Surface.MakeOffscreen(SIZE, SIZE)!; + offscreen.getCanvas().drawColor(Skia.Color("lime")); + offscreen.flush(); + const nativeBuffer = Skia.NativeBuffer.MakeFromImage( + offscreen.makeImageSnapshot() + ); + + const device = Skia.getDevice(); + try { + // importSharedTextureMemory is a Skia extension to GPUDevice (see + // the types in src/skia/types/WebGPU.ts). + const memory = device.importSharedTextureMemory({ + handle: nativeBuffer, + label: "test-frame", + }); + const texture = memory.createTexture(); + // beginAccess returns void and throws on failure. + memory.beginAccess(texture, true); + + const module = device.createShaderModule({ code: shader }); + const format: GPUTextureFormat = "rgba8unorm"; + const bindGroupLayout = device.createBindGroupLayout({ + entries: [ + { + binding: 0, + visibility: GPUShaderStage.FRAGMENT, + texture: {}, + }, + { + binding: 1, + visibility: GPUShaderStage.FRAGMENT, + sampler: {}, + }, + ], + }); + const pipeline = device.createRenderPipeline({ + layout: device.createPipelineLayout({ + bindGroupLayouts: [bindGroupLayout], + }), + vertex: { module, entryPoint: "vs" }, + fragment: { module, entryPoint: "fs", targets: [{ format }] }, + primitive: { topology: "triangle-list" }, + }); + const sampler = device.createSampler({ + magFilter: "linear", + minFilter: "linear", + }); + const bindGroup = device.createBindGroup({ + layout: bindGroupLayout, + entries: [ + { binding: 0, resource: texture.createView() }, + { binding: 1, resource: sampler }, + ], + }); + + const target = device.createTexture({ + size: [SIZE, SIZE], + format, + usage: + GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC, + }); + const bytesPerRow = SIZE * 4; + const readBuffer = device.createBuffer({ + size: bytesPerRow * SIZE, + usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ, + }); + + const encoder = device.createCommandEncoder(); + const pass = encoder.beginRenderPass({ + colorAttachments: [ + { + view: target.createView(), + clearValue: { r: 0, g: 0, b: 0, a: 1 }, + loadOp: "clear", + storeOp: "store", + }, + ], + }); + pass.setPipeline(pipeline); + pass.setBindGroup(0, bindGroup); + pass.draw(3); + pass.end(); + encoder.copyTextureToBuffer( + { texture: target }, + { buffer: readBuffer, bytesPerRow }, + { width: SIZE, height: SIZE } + ); + device.queue.submit([encoder.finish()]); + + return readBuffer.mapAsync(GPUMapMode.READ).then(() => { + const pixels = new Uint8Array( + readBuffer.getMappedRange().slice(0) + ); + readBuffer.unmap(); + memory.endAccess(texture); + texture.destroy(); + Skia.NativeBuffer.Release(nativeBuffer); + const offset = + (Math.floor(SIZE / 2) * SIZE + Math.floor(SIZE / 2)) * 4; + return [ + pixels[offset], + pixels[offset + 1], + pixels[offset + 2], + pixels[offset + 3], + ]; + }); + } catch (e) { + Skia.NativeBuffer.Release(nativeBuffer); + throw e; + } + }, + { shader: SHARED_TEXTURE_SHADER } + ); + const [r, g, b, a] = result; + // Sampled the solid-green ("lime") source. + expect(g).toBeGreaterThan(200); + expect(r).toBeLessThan(60); + expect(b).toBeLessThan(60); + expect(a).toBeGreaterThan(200); + } + ); +}); diff --git a/packages/skia/src/renderer/__tests__/setup.tsx b/packages/skia/src/renderer/__tests__/setup.tsx index f80a9f672e..49c7b3fae9 100644 --- a/packages/skia/src/renderer/__tests__/setup.tsx +++ b/packages/skia/src/renderer/__tests__/setup.tsx @@ -27,6 +27,7 @@ declare global { var testServer: Server; var testClient: WebSocket; var testOS: TestOS; + var testGraphite: boolean; } export let surface: TestingSurface; const assets = new Map(); @@ -110,6 +111,47 @@ beforeAll(async () => { export const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); +// Error envelopes are JSON objects starting with "{" (0x7b). PNG payloads start +// with the 0x89 signature and base64/other JSON responses won't carry $$error, +// so we only attempt a parse when the buffer looks like a JSON object. +const parseErrorResponse = (raw: Buffer): string | null => { + if (raw[0] !== 0x7b) { + return null; + } + try { + const obj = JSON.parse(raw.toString()); + if (obj && typeof obj === "object" && "$$error" in obj) { + return String(obj.$$error); + } + } catch { + // Not a JSON object; treat as a normal payload. + } + return null; +}; + +// Registers a test that only runs against a device using the Graphite backend +// (Dawn/WebGPU). The backend is only known after the websocket handshake, so — +// like itSkipsOnWeb in react-native-webgpu — the guard is a runtime early +// return rather than it.skip at collection time. This also keeps the WebGPU +// specs inert in Node/Local mode and on Ganesh builds, where Skia.getDevice() +// would throw. +export const itRunsWithGraphite = ( + name: string, + fn: () => Promise, + timeout?: number +) => { + it( + name, + async () => { + if (!E2E || !surface.graphite) { + return; + } + await fn(); + }, + timeout + ); +}; + export const resolveFile = (uri: string) => fs.readFileSync(path.resolve(__dirname, `../../${uri}`)); @@ -378,6 +420,10 @@ interface TestingSurface { fontSize: number; OS: TestOS; arch: "paper" | "fabric"; + // True when the connected device runs the Graphite backend, i.e. the WebGPU + // API (navigator.gpu / Skia.getDevice()) is available. Always false in Node + // (LocalSurface) and on Ganesh builds. + graphite: boolean; } class LocalSurface implements TestingSurface { @@ -386,6 +432,7 @@ class LocalSurface implements TestingSurface { readonly fontSize = 32; readonly OS = "node"; readonly arch = "paper"; + readonly graphite = false; eval( fn: (Skia: Skia, ctx: Ctx) => R, @@ -433,6 +480,7 @@ class RemoteSurface implements TestingSurface { readonly fontSize = 32; readonly OS = global.testOS; readonly arch = global.testArch; + readonly graphite = global.testGraphite ?? false; eval( fn: (Skia: Skia, ctx: Ctx) => any, @@ -504,10 +552,32 @@ return surface.makeImageSnapshot().encodeToBase64(); body: string, json?: boolean ): Promise { - return new Promise((resolve) => { - this.client.once("message", (raw: Buffer) => { + // Guard against an eval that never replies (e.g. the device threw before it + // could post a result back). Without this the host would await forever and + // a single failing case would stall the whole serial suite. + const EVAL_TIMEOUT_MS = 30 * 1000; + return new Promise((resolve, reject) => { + const onMessage = (raw: Buffer) => { + clearTimeout(timeout); + // The device reports a thrown error as { $$error: message } (always + // JSON, even for image responses) so the matching test can `.rejects` + // instead of hanging or trying to decode an error string as a PNG. + const error = parseErrorResponse(raw); + if (error !== null) { + reject(new Error(error)); + return; + } resolve(json ? JSON.parse(raw.toString()) : this.decodeImage(raw)); - }); + }; + const timeout = setTimeout(() => { + this.client.off("message", onMessage); + reject( + new Error( + `eval timed out after ${EVAL_TIMEOUT_MS}ms without a response from the device` + ) + ); + }, EVAL_TIMEOUT_MS); + this.client.once("message", onMessage); this.client.send(body); }); } diff --git a/packages/skia/src/skia/types/NativeBuffer/NativeBufferFactory.ts b/packages/skia/src/skia/types/NativeBuffer/NativeBufferFactory.ts index c5a079298f..5a4f65abbf 100644 --- a/packages/skia/src/skia/types/NativeBuffer/NativeBufferFactory.ts +++ b/packages/skia/src/skia/types/NativeBuffer/NativeBufferFactory.ts @@ -40,7 +40,16 @@ export interface NativeBufferFactory { */ MakeFromImage: (image: SkImage) => NativeBuffer; /** - * Release a native buffer that was created with `MakeFromImage`. + * Create a native buffer of the given size filled with a procedural test + * pattern (RGB gradient + diagonal stripes), entirely on the CPU. Useful for + * examples and tests that need a buffer to feed into + * `GPUDevice.importExternalTexture` without a camera/video source. Release it + * with `Release`. + */ + MakeTestBuffer: (width: number, height: number) => NativeBuffer; + /** + * Release a native buffer that was created with `MakeFromImage` or + * `MakeTestBuffer`. */ Release: (nativeBuffer: NativeBuffer) => void; } diff --git a/packages/skia/src/skia/types/WebGPU.ts b/packages/skia/src/skia/types/WebGPU.ts new file mode 100644 index 0000000000..225f67de22 --- /dev/null +++ b/packages/skia/src/skia/types/WebGPU.ts @@ -0,0 +1,186 @@ +import type { NativeBuffer } from "./NativeBuffer"; + +// Skia's Graphite/Dawn backend extends the standard WebGPU API (typed by +// @webgpu/types) with a few Skia- and Dawn-specific entry points. These are +// only available on native (SK_GRAPHITE) builds, reachable through +// `Skia.getDevice()`. +// +// The exported interfaces below describe the descriptors and objects those +// entry points use; the `declare global` block augments the standard WebGPU +// interfaces so the new methods are typed without casting to `any`. + +/** + * Descriptor for {@link GPUDevice.importExternalTexture} when the source is a + * Skia NativeBuffer (Skia has no WebCodecs `VideoFrame`). + * + * `source` is the handle returned by `Skia.NativeBuffer.MakeFromImage`: a + * `CVPixelBufferRef` on Apple, an `AHardwareBuffer*` on Android. The caller + * owns its lifetime (release it with `Skia.NativeBuffer.Release`) and must keep + * it alive until the imported texture is destroyed. + */ +export interface SkiaGPUExternalTextureDescriptor extends GPUObjectDescriptorBase { + source: NativeBuffer; + /** Rotation applied while sampling, in degrees. One of 0 | 90 | 180 | 270. */ + rotation?: number; + /** Mirror horizontally while sampling. */ + mirrored?: boolean; +} + +/** + * Descriptor for {@link GPUDevice.importSharedTextureMemory}. `handle` is the + * NativeBuffer returned by `Skia.NativeBuffer.MakeFromImage` (see + * {@link SkiaGPUExternalTextureDescriptor} for the platform-specific types and + * lifetime rules). + */ +export interface GPUSharedTextureMemoryDescriptor extends GPUObjectDescriptorBase { + handle: NativeBuffer; +} + +/** + * The kind of native synchronization primitive a {@link GPUSharedFence} wraps, + * matching the `shared-fence-*` device feature names. Limited to the kinds + * react-native-skia targets (iOS/Metal and Android/Vulkan); `importSharedFence` + * accepts these and `export()` reports them. + */ +export type GPUSharedFenceType = + | "mtl-shared-event" + | "sync-fd" + | "vk-semaphore-opaque-fd"; + +/** + * Descriptor for {@link GPUDevice.importSharedFence}. + */ +export interface GPUSharedFenceDescriptor { + /** + * The fence kind to import. Must match a `shared-fence-*` feature enabled on + * the device. + */ + type: GPUSharedFenceType; + /** + * The raw native handle as a BigInt: an `id` pointer for + * `"mtl-shared-event"`, or an OS file descriptor for the `*-fd` kinds. + */ + handle: bigint; + label?: string; +} + +export interface GPUSharedFenceExportInfo { + type: GPUSharedFenceType; + /** + * An `id` pointer (Apple) or file descriptor (Android), as a + * BigInt. The caller takes ownership; e.g. an exported sync-fd must be closed + * once consumed. + */ + handle: bigint; +} + +/** + * A native GPU synchronization primitive shared across queues/APIs. Produced by + * {@link GPUSharedTextureMemory.endAccess}, consumed by + * {@link GPUSharedTextureMemory.beginAccess}, or imported from a consumer's + * fence with {@link GPUDevice.importSharedFence}. + */ +export interface GPUSharedFence { + readonly __brand: "GPUSharedFence"; + label: string; + export(): GPUSharedFenceExportInfo; +} + +/** A fence and the timeline value to wait for (0n for binary sync-fd fences). */ +export interface GPUSharedFenceState { + fence: GPUSharedFence; + signaledValue: bigint; +} + +/** + * The result of {@link GPUSharedTextureMemory.endAccess}: each fence is signaled + * at its `signaledValue` once Dawn's GPU work for the access completes. + */ +export interface GPUSharedTextureMemoryEndAccessState { + initialized: boolean; + fences: GPUSharedFenceState[]; +} + +/** + * Shared texture memory imported from a platform native buffer via + * {@link GPUDevice.importSharedTextureMemory}. Create a texture that aliases + * the memory, then bracket the GPU work that touches it with + * {@link GPUSharedTextureMemory.beginAccess} / {@link GPUSharedTextureMemory.endAccess}. + */ +export interface GPUSharedTextureMemory extends GPUObjectBase { + /** Create a texture that aliases the shared memory. */ + createTexture(descriptor?: GPUTextureDescriptor): GPUTexture; + /** + * Acquire the memory for GPU access. `initialized` marks whether the existing + * contents should be preserved (pass `true` for an already-rendered frame). + * Optional `fences` are wait fences: Dawn waits for each to reach its + * `signaledValue` before writing the surface. Throws if the access could not + * be acquired. + */ + beginAccess( + texture: GPUTexture, + initialized: boolean, + fences?: GPUSharedFenceState[] + ): void; + /** + * Release the memory after the GPU work that accessed it has been submitted, + * and return the fences Dawn produced for the access. Throws on failure. + */ + endAccess(texture: GPUTexture): GPUSharedTextureMemoryEndAccessState; +} + +/** + * Dawn-specific toggles, passed via {@link GPUDeviceDescriptor.dawnToggles} to + * `adapter.requestDevice`. This is a non-spec, Dawn-only extension; see Dawn's + * toggle list for valid names. + */ +export interface GPUDawnTogglesDescriptor { + enabledToggles?: string[]; + disabledToggles?: string[]; +} + +declare global { + interface GPUDeviceDescriptor { + /** Dawn-specific toggles (Skia/Graphite extension, non-spec). */ + dawnToggles?: GPUDawnTogglesDescriptor; + } + + interface GPUExternalTexture { + /** + * Skia extension: end the imported buffer's shared-memory access window and + * release the underlying resources. Call right after the `queue.submit()` + * that sampled this texture (never before). Idempotent, and also runs at + * garbage collection as a fallback. + */ + destroy(): void; + } + + interface GPUDevice { + /** + * Skia extension: import a NativeBuffer (from + * `Skia.NativeBuffer.MakeFromImage`) as a {@link GPUExternalTexture}, sampled + * with `texture_external` in WGSL. The returned texture owns the + * shared-memory access window; call `destroy()` on it after the sampling + * `queue.submit()`. + */ + importExternalTexture( + descriptor: SkiaGPUExternalTextureDescriptor + ): GPUExternalTexture; + /** + * Skia extension: import a NativeBuffer (from + * `Skia.NativeBuffer.MakeFromImage`) as {@link GPUSharedTextureMemory}, the + * lower-level path that lets you create an aliasing texture and manage the + * begin/end access window yourself. + */ + importSharedTextureMemory( + descriptor: GPUSharedTextureMemoryDescriptor + ): GPUSharedTextureMemory; + /** + * Skia extension: import a native synchronization primitive (an + * `id` on Apple, a sync-fd / VkSemaphore on Android) as a + * {@link GPUSharedFence}, e.g. to wait on a fence a consumer produced. The + * matching `shared-fence-*` feature must be enabled on the device. + */ + importSharedFence(descriptor: GPUSharedFenceDescriptor): GPUSharedFence; + } +} diff --git a/packages/skia/src/skia/types/index.ts b/packages/skia/src/skia/types/index.ts index 2d55392f42..be8fcf4f54 100644 --- a/packages/skia/src/skia/types/index.ts +++ b/packages/skia/src/skia/types/index.ts @@ -30,6 +30,7 @@ export * from "./Size"; export * from "./Paragraph"; export * from "./Matrix4"; export * from "./NativeBuffer"; +export * from "./WebGPU"; export * from "./Recorder"; export * from "./Video"; export * from "./Skottie"; diff --git a/packages/skia/src/skia/web/JsiSkNativeBufferFactory.ts b/packages/skia/src/skia/web/JsiSkNativeBufferFactory.ts index 55ff04ac6b..cd6da31645 100644 --- a/packages/skia/src/skia/web/JsiSkNativeBufferFactory.ts +++ b/packages/skia/src/skia/web/JsiSkNativeBufferFactory.ts @@ -25,6 +25,26 @@ export class JsiSkNativeBufferFactory return canvas; } + MakeTestBuffer(width: number, height: number): NativeBuffer { + const pixels = new Uint8ClampedArray(width * height * 4); + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + const i = (y * width + x) * 4; + pixels[i + 0] = Math.floor((x * 255) / Math.max(width - 1, 1)); + pixels[i + 1] = Math.floor((y * 255) / Math.max(height - 1, 1)); + pixels[i + 2] = (x + y) & 0x20 ? 220 : 30; + pixels[i + 3] = 0xff; + } + } + const canvas = new OffscreenCanvas(width, height); + const ctx = canvas.getContext("2d"); + if (!ctx) { + throw new Error("Failed to get 2d context from canvas"); + } + ctx.putImageData(new ImageData(pixels, width, height), 0, 0); + return canvas; + } + Release(_nativeBuffer: NativeBuffer) { // it's a noop on Web } diff --git a/packages/skia/src/skia/web/JsiSkPath.ts b/packages/skia/src/skia/web/JsiSkPath.ts index ccad962b7b..ef0f1a0309 100644 --- a/packages/skia/src/skia/web/JsiSkPath.ts +++ b/packages/skia/src/skia/web/JsiSkPath.ts @@ -447,7 +447,10 @@ export class JsiSkPath } simplify() { - warnDeprecatedPathMethod("simplify", "Use Skia.Path.Simplify(path) instead."); + warnDeprecatedPathMethod( + "simplify", + "Use Skia.Path.Simplify(path) instead." + ); const path = this.asPath(); const result = path.makeSimplified(); path.delete(); @@ -500,7 +503,10 @@ export class JsiSkPath } stroke(opts?: StrokeOpts) { - warnDeprecatedPathMethod("stroke", "Use Skia.Path.Stroke(path, opts) instead."); + warnDeprecatedPathMethod( + "stroke", + "Use Skia.Path.Stroke(path, opts) instead." + ); const path = this.asPath(); const result = path.makeStroked( opts === undefined diff --git a/yarn.lock b/yarn.lock index 5dc88d9545..4784100f40 100644 --- a/yarn.lock +++ b/yarn.lock @@ -219,7 +219,7 @@ __metadata: languageName: node linkType: hard -"@babel/code-frame@npm:^7.20.0": +"@babel/code-frame@npm:^7.20.0, @babel/code-frame@npm:^7.29.0": version: 7.29.0 resolution: "@babel/code-frame@npm:7.29.0" dependencies: @@ -230,6 +230,17 @@ __metadata: languageName: node linkType: hard +"@babel/code-frame@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/code-frame@npm:7.29.7" + dependencies: + "@babel/helper-validator-identifier": ^7.29.7 + js-tokens: ^4.0.0 + picocolors: ^1.1.1 + checksum: 21b12fe2356e36f6cc3cd8a3721f878bfeea80ce38356979a0518b47b3aafdcc0bd263da75ccc9d51c64d40b1b6df00e768ce2446acb0b7cbec0ae8f905663ad + languageName: node + linkType: hard + "@babel/code-frame@npm:~7.10.4": version: 7.10.4 resolution: "@babel/code-frame@npm:7.10.4" @@ -246,7 +257,14 @@ __metadata: languageName: node linkType: hard -"@babel/core@npm:^7.1.0, @babel/core@npm:^7.11.1, @babel/core@npm:^7.11.6, @babel/core@npm:^7.12.10, @babel/core@npm:^7.12.3, @babel/core@npm:^7.13.16, @babel/core@npm:^7.14.0, @babel/core@npm:^7.16.0, @babel/core@npm:^7.20.0, @babel/core@npm:^7.20.2, @babel/core@npm:^7.21.3, @babel/core@npm:^7.23.9, @babel/core@npm:^7.24.4, @babel/core@npm:^7.25.2, @babel/core@npm:^7.25.9, @babel/core@npm:^7.27.4, @babel/core@npm:^7.7.2, @babel/core@npm:^7.8.0": +"@babel/compat-data@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/compat-data@npm:7.29.7" + checksum: 4424f8fb72f61657c8e811bdf7e5c69af212a15fe362711162b2e00b82f0e0588fb8259ecd9a70c5490562bf51d408b0cb92f277ead71a2390ddddfe7283b35d + languageName: node + linkType: hard + +"@babel/core@npm:^7.1.0, @babel/core@npm:^7.11.1, @babel/core@npm:^7.11.6, @babel/core@npm:^7.12.10, @babel/core@npm:^7.12.3, @babel/core@npm:^7.13.16, @babel/core@npm:^7.14.0, @babel/core@npm:^7.16.0, @babel/core@npm:^7.20.0, @babel/core@npm:^7.21.3, @babel/core@npm:^7.23.9, @babel/core@npm:^7.24.4, @babel/core@npm:^7.25.2, @babel/core@npm:^7.25.9, @babel/core@npm:^7.27.4, @babel/core@npm:^7.7.2, @babel/core@npm:^7.8.0": version: 7.28.6 resolution: "@babel/core@npm:7.28.6" dependencies: @@ -269,6 +287,29 @@ __metadata: languageName: node linkType: hard +"@babel/core@npm:^7.29.6": + version: 7.29.6 + resolution: "@babel/core@npm:7.29.6" + dependencies: + "@babel/code-frame": ^7.29.0 + "@babel/generator": ^7.29.6 + "@babel/helper-compilation-targets": ^7.28.6 + "@babel/helper-module-transforms": ^7.28.6 + "@babel/helpers": ^7.29.2 + "@babel/parser": ^7.29.3 + "@babel/template": ^7.28.6 + "@babel/traverse": ^7.29.0 + "@babel/types": ^7.29.0 + "@jridgewell/remapping": ^2.3.5 + convert-source-map: ^2.0.0 + debug: ^4.1.0 + gensync: ^1.0.0-beta.2 + json5: ^2.2.3 + semver: ^6.3.1 + checksum: 94d510191c8dfc5745adce4a91dff24f8664925f5c9a8f8c9e6f458f0b3ab55af89a846cba3657b633c772716e69f220c59aa4c7d8caf02b322892aa18273462 + languageName: node + linkType: hard + "@babel/eslint-parser@npm:^7.16.3, @babel/eslint-parser@npm:^7.18.2, @babel/eslint-parser@npm:^7.25.1": version: 7.28.6 resolution: "@babel/eslint-parser@npm:7.28.6" @@ -296,6 +337,32 @@ __metadata: languageName: node linkType: hard +"@babel/generator@npm:^7.29.0": + version: 7.29.1 + resolution: "@babel/generator@npm:7.29.1" + dependencies: + "@babel/parser": ^7.29.0 + "@babel/types": ^7.29.0 + "@jridgewell/gen-mapping": ^0.3.12 + "@jridgewell/trace-mapping": ^0.3.28 + jsesc: ^3.0.2 + checksum: d8e6863b2d04f684e65ad72731049ac7d754d3a3d1a67cdfc20807b109ba3180ed90d7ccef58ce5d38ded2eaeb71983a76c711eecb9b6266118262378f6c7226 + languageName: node + linkType: hard + +"@babel/generator@npm:^7.29.6, @babel/generator@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/generator@npm:7.29.7" + dependencies: + "@babel/parser": ^7.29.7 + "@babel/types": ^7.29.7 + "@jridgewell/gen-mapping": ^0.3.12 + "@jridgewell/trace-mapping": ^0.3.28 + jsesc: ^3.0.2 + checksum: 6bb8f4dc0641dca19e81f5daab37ed1a1f5a78e4d702eb79a4275739f773975134e250090c182cf1eea35d9bd65e634b9d9babf66600ffdcf8ac62fa40f3b980 + languageName: node + linkType: hard + "@babel/helper-annotate-as-pure@npm:^7.18.6, @babel/helper-annotate-as-pure@npm:^7.27.1, @babel/helper-annotate-as-pure@npm:^7.27.3": version: 7.27.3 resolution: "@babel/helper-annotate-as-pure@npm:7.27.3" @@ -305,7 +372,16 @@ __metadata: languageName: node linkType: hard -"@babel/helper-compilation-targets@npm:^7.27.1, @babel/helper-compilation-targets@npm:^7.27.2, @babel/helper-compilation-targets@npm:^7.28.6": +"@babel/helper-annotate-as-pure@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/helper-annotate-as-pure@npm:7.29.7" + dependencies: + "@babel/types": ^7.29.7 + checksum: acd9e128de634a5144b5d622357d018fa616de45f64c74e42007c048dd15d0a0be213f4d5a2bf02307bdaddf053791b87900a99d183de828c08dc3b556329009 + languageName: node + linkType: hard + +"@babel/helper-compilation-targets@npm:^7.27.1, @babel/helper-compilation-targets@npm:^7.28.6": version: 7.28.6 resolution: "@babel/helper-compilation-targets@npm:7.28.6" dependencies: @@ -318,7 +394,20 @@ __metadata: languageName: node linkType: hard -"@babel/helper-create-class-features-plugin@npm:^7.18.6, @babel/helper-create-class-features-plugin@npm:^7.21.0, @babel/helper-create-class-features-plugin@npm:^7.27.1, @babel/helper-create-class-features-plugin@npm:^7.28.6": +"@babel/helper-compilation-targets@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/helper-compilation-targets@npm:7.29.7" + dependencies: + "@babel/compat-data": ^7.29.7 + "@babel/helper-validator-option": ^7.29.7 + browserslist: ^4.24.0 + lru-cache: ^5.1.1 + semver: ^6.3.1 + checksum: f60a943937f4eba0e671aa28551cb45569fd081c1e30a52ede167860475dc0417f3dbdf2a0fa3f086965595c7070aa76308da60cc0319860de05db4ed2a431f7 + languageName: node + linkType: hard + +"@babel/helper-create-class-features-plugin@npm:^7.18.6, @babel/helper-create-class-features-plugin@npm:^7.21.0, @babel/helper-create-class-features-plugin@npm:^7.28.6": version: 7.28.6 resolution: "@babel/helper-create-class-features-plugin@npm:7.28.6" dependencies: @@ -335,6 +424,23 @@ __metadata: languageName: node linkType: hard +"@babel/helper-create-class-features-plugin@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/helper-create-class-features-plugin@npm:7.29.7" + dependencies: + "@babel/helper-annotate-as-pure": ^7.29.7 + "@babel/helper-member-expression-to-functions": ^7.29.7 + "@babel/helper-optimise-call-expression": ^7.29.7 + "@babel/helper-replace-supers": ^7.29.7 + "@babel/helper-skip-transparent-expression-wrappers": ^7.29.7 + "@babel/traverse": ^7.29.7 + semver: ^6.3.1 + peerDependencies: + "@babel/core": ^7.0.0 + checksum: c954e4bfe423a277cdcbad64344637cf696ddcd80085fce5f284b02a0c700af0d2d7b61468a06d9e296e948de60ece138921b65564aec023a9d9594f5d9fe18d + languageName: node + linkType: hard + "@babel/helper-create-regexp-features-plugin@npm:^7.18.6, @babel/helper-create-regexp-features-plugin@npm:^7.27.1, @babel/helper-create-regexp-features-plugin@npm:^7.28.5": version: 7.28.5 resolution: "@babel/helper-create-regexp-features-plugin@npm:7.28.5" @@ -348,6 +454,19 @@ __metadata: languageName: node linkType: hard +"@babel/helper-create-regexp-features-plugin@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/helper-create-regexp-features-plugin@npm:7.29.7" + dependencies: + "@babel/helper-annotate-as-pure": ^7.29.7 + regexpu-core: ^6.3.1 + semver: ^6.3.1 + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 702a34db6c064a2c26675b717b3af88b8acaeb5341e2285792a873a67a16ec4cbe987ba72e055db198b2e03ead05600d8c9c0c1e7436708e95865bbfb3516026 + languageName: node + linkType: hard + "@babel/helper-define-polyfill-provider@npm:^0.6.5, @babel/helper-define-polyfill-provider@npm:^0.6.6": version: 0.6.6 resolution: "@babel/helper-define-polyfill-provider@npm:0.6.6" @@ -363,6 +482,21 @@ __metadata: languageName: node linkType: hard +"@babel/helper-define-polyfill-provider@npm:^0.6.8": + version: 0.6.8 + resolution: "@babel/helper-define-polyfill-provider@npm:0.6.8" + dependencies: + "@babel/helper-compilation-targets": ^7.28.6 + "@babel/helper-plugin-utils": ^7.28.6 + debug: ^4.4.3 + lodash.debounce: ^4.0.8 + resolve: ^1.22.11 + peerDependencies: + "@babel/core": ^7.4.0 || ^8.0.0-0 <8.0.0 + checksum: 39fef64ade79253836320c7826895d948ab5e8e21479cf29f5d6bb5284126693ca537b6ace9d9b7b515a8be66bd4a8a7d7687f9b25b7574a52dae7790fcd3a4e + languageName: node + linkType: hard + "@babel/helper-globals@npm:^7.28.0": version: 7.28.0 resolution: "@babel/helper-globals@npm:7.28.0" @@ -370,6 +504,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-globals@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/helper-globals@npm:7.29.7" + checksum: 6deaf9846a415c7f110ac153e8c3d81e3543c9b685aa9fd9a59b4235f402e2b760129d3df49466daea9162f0cf73ff98a0ec7f180448a3449ca14ae5e1117b42 + languageName: node + linkType: hard + "@babel/helper-member-expression-to-functions@npm:^7.28.5": version: 7.28.5 resolution: "@babel/helper-member-expression-to-functions@npm:7.28.5" @@ -380,6 +521,16 @@ __metadata: languageName: node linkType: hard +"@babel/helper-member-expression-to-functions@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/helper-member-expression-to-functions@npm:7.29.7" + dependencies: + "@babel/traverse": ^7.29.7 + "@babel/types": ^7.29.7 + checksum: 79d5f095b4bafadff3d1ec316d9f17ec85940fece957db62dd523b08e142da73c53180d1bce2fdc4d523e3889bb01b39bea70ad968b6e2f4dbdc66ebd1055b8a + languageName: node + linkType: hard + "@babel/helper-module-imports@npm:^7.10.4, @babel/helper-module-imports@npm:^7.27.1, @babel/helper-module-imports@npm:^7.28.6": version: 7.28.6 resolution: "@babel/helper-module-imports@npm:7.28.6" @@ -390,7 +541,17 @@ __metadata: languageName: node linkType: hard -"@babel/helper-module-transforms@npm:^7.27.1, @babel/helper-module-transforms@npm:^7.28.3, @babel/helper-module-transforms@npm:^7.28.6": +"@babel/helper-module-imports@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/helper-module-imports@npm:7.29.7" + dependencies: + "@babel/traverse": ^7.29.7 + "@babel/types": ^7.29.7 + checksum: ad5a768fc9c162620b7f5b7645c6c2efee1e6f9df432bb79651888661a7e4cd91dac7192f3ddcfd6de4778a089e9fb30fafae768156aa73a07eeca425f64e849 + languageName: node + linkType: hard + +"@babel/helper-module-transforms@npm:^7.27.1, @babel/helper-module-transforms@npm:^7.28.6": version: 7.28.6 resolution: "@babel/helper-module-transforms@npm:7.28.6" dependencies: @@ -403,6 +564,19 @@ __metadata: languageName: node linkType: hard +"@babel/helper-module-transforms@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/helper-module-transforms@npm:7.29.7" + dependencies: + "@babel/helper-module-imports": ^7.29.7 + "@babel/helper-validator-identifier": ^7.29.7 + "@babel/traverse": ^7.29.7 + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 484f6d02975d304f680d44e331d4b832d67e51917483985eab7b853664452b5a627366e7a9d421200ec772a123213a591e28b40636566afeac189411ba3f45a8 + languageName: node + linkType: hard + "@babel/helper-optimise-call-expression@npm:^7.27.1": version: 7.27.1 resolution: "@babel/helper-optimise-call-expression@npm:7.27.1" @@ -412,6 +586,15 @@ __metadata: languageName: node linkType: hard +"@babel/helper-optimise-call-expression@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/helper-optimise-call-expression@npm:7.29.7" + dependencies: + "@babel/types": ^7.29.7 + checksum: 6b477e01b403fd48349336cb1d94722bff4fa54af2841b5fa950c557b796f4ecc14724052252ed1362ccfc23d1c09c54dc03e182fea59d3dc5bd69f8c626ba25 + languageName: node + linkType: hard + "@babel/helper-plugin-utils@npm:^7.0.0, @babel/helper-plugin-utils@npm:^7.10.4, @babel/helper-plugin-utils@npm:^7.12.13, @babel/helper-plugin-utils@npm:^7.14.5, @babel/helper-plugin-utils@npm:^7.18.6, @babel/helper-plugin-utils@npm:^7.20.2, @babel/helper-plugin-utils@npm:^7.27.1, @babel/helper-plugin-utils@npm:^7.28.6, @babel/helper-plugin-utils@npm:^7.8.0": version: 7.28.6 resolution: "@babel/helper-plugin-utils@npm:7.28.6" @@ -419,6 +602,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-plugin-utils@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/helper-plugin-utils@npm:7.29.7" + checksum: b0a183abcc6670afa4861425fa428217d8ebadce062d5b43117919e8715f820080fd63bbfcf0e43c6e0e7d21a96b21f635c46dda80bdb0ce7e8a762ebee1d8d9 + languageName: node + linkType: hard + "@babel/helper-remap-async-to-generator@npm:^7.27.1": version: 7.27.1 resolution: "@babel/helper-remap-async-to-generator@npm:7.27.1" @@ -432,6 +622,19 @@ __metadata: languageName: node linkType: hard +"@babel/helper-remap-async-to-generator@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/helper-remap-async-to-generator@npm:7.29.7" + dependencies: + "@babel/helper-annotate-as-pure": ^7.29.7 + "@babel/helper-wrap-function": ^7.29.7 + "@babel/traverse": ^7.29.7 + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 98338ad6e34ebb4be2dc23f8d9199d28d6d8ac6a2ce8b90fe9efdf3595b39748321528d9f2540ec0586a6e45f7c84f5f623fbf980c5efa7fa9ba7ce837ea4b20 + languageName: node + linkType: hard + "@babel/helper-replace-supers@npm:^7.27.1, @babel/helper-replace-supers@npm:^7.28.6": version: 7.28.6 resolution: "@babel/helper-replace-supers@npm:7.28.6" @@ -445,6 +648,19 @@ __metadata: languageName: node linkType: hard +"@babel/helper-replace-supers@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/helper-replace-supers@npm:7.29.7" + dependencies: + "@babel/helper-member-expression-to-functions": ^7.29.7 + "@babel/helper-optimise-call-expression": ^7.29.7 + "@babel/traverse": ^7.29.7 + peerDependencies: + "@babel/core": ^7.0.0 + checksum: f7eb9a6b035d9d45250c880eb09605f95998998e828ec90759ed45764fe0abeee583419969de8b8dee551163dff914c9fc6ace90c1d819c56c23146f8df525db + languageName: node + linkType: hard + "@babel/helper-skip-transparent-expression-wrappers@npm:^7.20.0, @babel/helper-skip-transparent-expression-wrappers@npm:^7.27.1": version: 7.27.1 resolution: "@babel/helper-skip-transparent-expression-wrappers@npm:7.27.1" @@ -455,6 +671,16 @@ __metadata: languageName: node linkType: hard +"@babel/helper-skip-transparent-expression-wrappers@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/helper-skip-transparent-expression-wrappers@npm:7.29.7" + dependencies: + "@babel/traverse": ^7.29.7 + "@babel/types": ^7.29.7 + checksum: a5800bfcdca6cef7f6fe33ac02a0f05ff33da9746f97806553f249733f7ba8400290a17f3831d7faa5d91656f254ab749931f53c8a29f301d958d7dd00499637 + languageName: node + linkType: hard + "@babel/helper-string-parser@npm:^7.27.1": version: 7.27.1 resolution: "@babel/helper-string-parser@npm:7.27.1" @@ -462,6 +688,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-string-parser@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/helper-string-parser@npm:7.29.7" + checksum: 4c229d2c2296b6c94439e87ecddf3a93cee3ffd2d4cee0b4c28079275bed3de4e02cd943e4cb06036d1d9407549c4e3423006b61a46215c482fce902ee02bc0b + languageName: node + linkType: hard + "@babel/helper-validator-identifier@npm:^7.25.9, @babel/helper-validator-identifier@npm:^7.28.5": version: 7.28.5 resolution: "@babel/helper-validator-identifier@npm:7.28.5" @@ -469,6 +702,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-validator-identifier@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/helper-validator-identifier@npm:7.29.7" + checksum: cc7779e96fe9c9478c96ca4bf04fc338b95b501aa5abe78178307dea282d4a5dc23d443efb12dde8e8c5636d03dd00e443532e6ed7f15fa7977349af1f87ba4d + languageName: node + linkType: hard + "@babel/helper-validator-option@npm:^7.27.1": version: 7.27.1 resolution: "@babel/helper-validator-option@npm:7.27.1" @@ -476,6 +716,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-validator-option@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/helper-validator-option@npm:7.29.7" + checksum: aeb6aa966f59300d3cc2fea7c68e1dfd7ad011fc10e535c8e2b2de3094b27c859428dc7220f16420350f8b1cde99da120b673be04bcb0c2f37b56258c96bed58 + languageName: node + linkType: hard + "@babel/helper-wrap-function@npm:^7.27.1": version: 7.28.6 resolution: "@babel/helper-wrap-function@npm:7.28.6" @@ -487,6 +734,17 @@ __metadata: languageName: node linkType: hard +"@babel/helper-wrap-function@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/helper-wrap-function@npm:7.29.7" + dependencies: + "@babel/template": ^7.29.7 + "@babel/traverse": ^7.29.7 + "@babel/types": ^7.29.7 + checksum: 2e6dfca94a10a3672a6b0ff337c80f27d7c66f3ea110969e833529a2d5bfbb5e53ab40abf6fad4657b8f810fcab2e3d56998f8da89bfe82a58267c5e8bc06e9a + languageName: node + linkType: hard + "@babel/helpers@npm:^7.28.6": version: 7.28.6 resolution: "@babel/helpers@npm:7.28.6" @@ -497,6 +755,16 @@ __metadata: languageName: node linkType: hard +"@babel/helpers@npm:^7.29.2": + version: 7.29.7 + resolution: "@babel/helpers@npm:7.29.7" + dependencies: + "@babel/template": ^7.29.7 + "@babel/types": ^7.29.7 + checksum: b5c4ed0ce5983c5599cd01b3948444b77ba2fa47bf6282a82afdcbe45eb8cc5b7986194e3920ef2760f533c6a775504e6b00a66960c0a3bc52909920b8433908 + languageName: node + linkType: hard + "@babel/highlight@npm:^7.10.4, @babel/highlight@npm:^7.23.4": version: 7.25.9 resolution: "@babel/highlight@npm:7.25.9" @@ -529,6 +797,28 @@ __metadata: languageName: node linkType: hard +"@babel/parser@npm:^7.29.0": + version: 7.29.3 + resolution: "@babel/parser@npm:7.29.3" + dependencies: + "@babel/types": ^7.29.0 + bin: + parser: ./bin/babel-parser.js + checksum: 046f46996bf4053b6e29f8a7f420f9e0a2878593c1c9a9914a36faca23fc544a307c78a0101ba3ae98936ade68bdde686a83e1ab2b74c2ebb80dc4a9df48476d + languageName: node + linkType: hard + +"@babel/parser@npm:^7.29.3, @babel/parser@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/parser@npm:7.29.7" + dependencies: + "@babel/types": ^7.29.7 + bin: + parser: ./bin/babel-parser.js + checksum: 56f4c32a371004d3becd0a960a85a3bba4ec4df73d5e202aa3fe6473328b243162c6be9a510be1c12ed1825f5ce9ff1ea5cc357298631e8acf2e5b8da9f5a961 + languageName: node + linkType: hard + "@babel/plugin-bugfix-firefox-class-in-computed-class-key@npm:^7.28.5": version: 7.28.5 resolution: "@babel/plugin-bugfix-firefox-class-in-computed-class-key@npm:7.28.5" @@ -541,6 +831,18 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-bugfix-firefox-class-in-computed-class-key@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/plugin-bugfix-firefox-class-in-computed-class-key@npm:7.29.7" + dependencies: + "@babel/helper-plugin-utils": ^7.29.7 + "@babel/traverse": ^7.29.7 + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 83cca77d175f3e66460a004648c8a645da880d5a9db96b03abd433e05bc4357aec0418d17b05fbfecfa679438b8eb8d8adc71b8f4db86cfaf6e6b0373b39a05e + languageName: node + linkType: hard + "@babel/plugin-bugfix-safari-class-field-initializer-scope@npm:^7.27.1": version: 7.27.1 resolution: "@babel/plugin-bugfix-safari-class-field-initializer-scope@npm:7.27.1" @@ -552,6 +854,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-bugfix-safari-class-field-initializer-scope@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/plugin-bugfix-safari-class-field-initializer-scope@npm:7.29.7" + dependencies: + "@babel/helper-plugin-utils": ^7.29.7 + peerDependencies: + "@babel/core": ^7.0.0 + checksum: ad06de66ccfb19f0f04e6124d144f3fef72fa5191861b1d04bc32cab87ce43958810d9632eac5881ef991a78b33e68588e3d90e76a63d917bd5a7ff4c96618f8 + languageName: node + linkType: hard + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:^7.27.1": version: 7.27.1 resolution: "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:7.27.1" @@ -563,6 +876,29 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:7.29.7" + dependencies: + "@babel/helper-plugin-utils": ^7.29.7 + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 2189a2a648948107c59ad3bf028e5b71a85b28c840facfead769a33c5f63ae4ec0f147e6a2e664a91a506d4405f5a8972d2ca628f3b64d03edd7620d770761fb + languageName: node + linkType: hard + +"@babel/plugin-bugfix-safari-rest-destructuring-rhs-array@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/plugin-bugfix-safari-rest-destructuring-rhs-array@npm:7.29.7" + dependencies: + "@babel/helper-plugin-utils": ^7.29.7 + "@babel/helper-skip-transparent-expression-wrappers": ^7.29.7 + peerDependencies: + "@babel/core": ^7.0.0 + checksum: a9bd13c2600bdfcb5b35590e210bcb30f009fda9bd1556567757ea4fcb8ca247750331d6d10774d68127cae4e529bc79abd86a28fe5e2a1cc2cc00e75964ac52 + languageName: node + linkType: hard + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@npm:^7.27.1": version: 7.27.1 resolution: "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@npm:7.27.1" @@ -576,6 +912,19 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@npm:7.29.7" + dependencies: + "@babel/helper-plugin-utils": ^7.29.7 + "@babel/helper-skip-transparent-expression-wrappers": ^7.29.7 + "@babel/plugin-transform-optional-chaining": ^7.29.7 + peerDependencies: + "@babel/core": ^7.13.0 + checksum: 76a494ec4f52a127b0208c4574a6da36f6ff5f30484306921762db549fa7d9d9183c837759eb68c0c9e5a56013aa247e6e02e02263384d2a103240353ae0ceb6 + languageName: node + linkType: hard + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@npm:^7.28.6": version: 7.28.6 resolution: "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@npm:7.28.6" @@ -588,6 +937,18 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@npm:7.29.7" + dependencies: + "@babel/helper-plugin-utils": ^7.29.7 + "@babel/traverse": ^7.29.7 + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 7a58b4b32f4c04b86160dc7900612904e2b88d42c8b89d59a89d3c343f320ad096d73fb453a2e8bf8e97c9d4af3563043bc6d1e3def53c819812cb03f3f873c8 + languageName: node + linkType: hard + "@babel/plugin-proposal-class-properties@npm:^7.12.1, @babel/plugin-proposal-class-properties@npm:^7.13.0, @babel/plugin-proposal-class-properties@npm:^7.16.0": version: 7.18.6 resolution: "@babel/plugin-proposal-class-properties@npm:7.18.6" @@ -636,7 +997,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-proposal-nullish-coalescing-operator@npm:7.18.6, @babel/plugin-proposal-nullish-coalescing-operator@npm:^7.13.8, @babel/plugin-proposal-nullish-coalescing-operator@npm:^7.16.0": +"@babel/plugin-proposal-nullish-coalescing-operator@npm:^7.13.8, @babel/plugin-proposal-nullish-coalescing-operator@npm:^7.16.0": version: 7.18.6 resolution: "@babel/plugin-proposal-nullish-coalescing-operator@npm:7.18.6" dependencies: @@ -796,6 +1157,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-syntax-flow@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/plugin-syntax-flow@npm:7.29.7" + dependencies: + "@babel/helper-plugin-utils": ^7.29.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 3e03792bb20d4a0f2610df5d5af6c2ec8cbb5096a7576b24027eca60ac2b9e3a183d48255e4156fa94768322d82b13e77623f785ef556660de1c0efc5708e52a + languageName: node + linkType: hard + "@babel/plugin-syntax-import-assertions@npm:^7.28.6": version: 7.28.6 resolution: "@babel/plugin-syntax-import-assertions@npm:7.28.6" @@ -807,6 +1179,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-syntax-import-assertions@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/plugin-syntax-import-assertions@npm:7.29.7" + dependencies: + "@babel/helper-plugin-utils": ^7.29.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: a7f24858e7e833c2ee25779a355b5eff46e4bc93c98b06bda7ea0fd12faf99c4954e0f71074485512045920432790e0269aa562dd4d2085c3f8660ed1cfde8a1 + languageName: node + linkType: hard + "@babel/plugin-syntax-import-attributes@npm:^7.24.7, @babel/plugin-syntax-import-attributes@npm:^7.28.6": version: 7.28.6 resolution: "@babel/plugin-syntax-import-attributes@npm:7.28.6" @@ -818,6 +1201,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-syntax-import-attributes@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/plugin-syntax-import-attributes@npm:7.29.7" + dependencies: + "@babel/helper-plugin-utils": ^7.29.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 9f47345d09aae16b7ab52ecaf541cde3e3ae1e57e3eb2d4088e062b29dfbd67db55d42d529840557583d66121e2a98788df7a455401cc6d635c8b7700a02efc9 + languageName: node + linkType: hard + "@babel/plugin-syntax-import-meta@npm:^7.10.4": version: 7.10.4 resolution: "@babel/plugin-syntax-import-meta@npm:7.10.4" @@ -851,6 +1245,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-syntax-jsx@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/plugin-syntax-jsx@npm:7.29.7" + dependencies: + "@babel/helper-plugin-utils": ^7.29.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 84150d27c553a1d3d921354437f6725ca1d63b49514c25591bfcaaafa6ea4d6c10715b66fe7245e4ad7ab7c6cf4b6e1de7373defd3df00877ab12638170d7772 + languageName: node + linkType: hard + "@babel/plugin-syntax-logical-assignment-operators@npm:^7.10.4": version: 7.10.4 resolution: "@babel/plugin-syntax-logical-assignment-operators@npm:7.10.4" @@ -950,6 +1355,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-syntax-typescript@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/plugin-syntax-typescript@npm:7.29.7" + dependencies: + "@babel/helper-plugin-utils": ^7.29.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: ef454d2a7a6209dd4255361c072c94ab1293e7ad4b06e7e744d08bb308065d4d6544964eae9b2357c3b33d8d939f9e32d4aa95905bc464407cd8f7101dee4443 + languageName: node + linkType: hard + "@babel/plugin-syntax-unicode-sets-regex@npm:^7.18.6": version: 7.18.6 resolution: "@babel/plugin-syntax-unicode-sets-regex@npm:7.18.6" @@ -962,7 +1378,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-arrow-functions@npm:7.27.1, @babel/plugin-transform-arrow-functions@npm:^7.0.0, @babel/plugin-transform-arrow-functions@npm:^7.24.7, @babel/plugin-transform-arrow-functions@npm:^7.27.1": +"@babel/plugin-transform-arrow-functions@npm:^7.0.0, @babel/plugin-transform-arrow-functions@npm:^7.24.7, @babel/plugin-transform-arrow-functions@npm:^7.27.1": version: 7.27.1 resolution: "@babel/plugin-transform-arrow-functions@npm:7.27.1" dependencies: @@ -973,6 +1389,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-arrow-functions@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/plugin-transform-arrow-functions@npm:7.29.7" + dependencies: + "@babel/helper-plugin-utils": ^7.29.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 0037fd7563c7c91cddb8ce104e270bc260190d29c7a297df65e4306471010b4343366de13fab4602cf8ee6c672d3b313b34a01b62f27a333cad16908c83368d8 + languageName: node + linkType: hard + "@babel/plugin-transform-async-generator-functions@npm:^7.24.3, @babel/plugin-transform-async-generator-functions@npm:^7.25.4, @babel/plugin-transform-async-generator-functions@npm:^7.28.6": version: 7.28.6 resolution: "@babel/plugin-transform-async-generator-functions@npm:7.28.6" @@ -986,6 +1413,19 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-async-generator-functions@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/plugin-transform-async-generator-functions@npm:7.29.7" + dependencies: + "@babel/helper-plugin-utils": ^7.29.7 + "@babel/helper-remap-async-to-generator": ^7.29.7 + "@babel/traverse": ^7.29.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: d8239f43b4051b12cd7a5581367bd2b13ac26235da39d79f182999879f010124341ac500f480140b0961a62ec451a0a963b421c509f9c1a306c313f10fc60451 + languageName: node + linkType: hard + "@babel/plugin-transform-async-to-generator@npm:^7.20.0, @babel/plugin-transform-async-to-generator@npm:^7.24.7, @babel/plugin-transform-async-to-generator@npm:^7.28.6": version: 7.28.6 resolution: "@babel/plugin-transform-async-to-generator@npm:7.28.6" @@ -999,6 +1439,19 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-async-to-generator@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/plugin-transform-async-to-generator@npm:7.29.7" + dependencies: + "@babel/helper-module-imports": ^7.29.7 + "@babel/helper-plugin-utils": ^7.29.7 + "@babel/helper-remap-async-to-generator": ^7.29.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: e24db9c4c69121daab25883f9a96e6849fa664c78c6bbcfb77fe0a0c6ab29b81ba39e8497985367463d3a88deac3b5bbe15dd1c5d0e5dd492cfccf8efdd27452 + languageName: node + linkType: hard + "@babel/plugin-transform-block-scoped-functions@npm:^7.27.1": version: 7.27.1 resolution: "@babel/plugin-transform-block-scoped-functions@npm:7.27.1" @@ -1010,6 +1463,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-block-scoped-functions@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/plugin-transform-block-scoped-functions@npm:7.29.7" + dependencies: + "@babel/helper-plugin-utils": ^7.29.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 24f9712ade98061cd22088709b86b8e3e19ea416dbe5a69ad504fc3f8f2179af5bdeb32fcb9c24fc861bef515e41ae5ef72cd909e0e42bb0cf15838c4e737149 + languageName: node + linkType: hard + "@babel/plugin-transform-block-scoping@npm:^7.0.0, @babel/plugin-transform-block-scoping@npm:^7.25.0, @babel/plugin-transform-block-scoping@npm:^7.28.6": version: 7.28.6 resolution: "@babel/plugin-transform-block-scoping@npm:7.28.6" @@ -1021,15 +1485,14 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-class-properties@npm:7.27.1": - version: 7.27.1 - resolution: "@babel/plugin-transform-class-properties@npm:7.27.1" +"@babel/plugin-transform-block-scoping@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/plugin-transform-block-scoping@npm:7.29.7" dependencies: - "@babel/helper-create-class-features-plugin": ^7.27.1 - "@babel/helper-plugin-utils": ^7.27.1 + "@babel/helper-plugin-utils": ^7.29.7 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 475a6e5a9454912fe1bdc171941976ca10ea4e707675d671cdb5ce6b6761d84d1791ac61b6bca81a2e5f6430cb7b9d8e4b2392404110e69c28207a754e196294 + checksum: a17c02f8dfcaf2c26c0c323aaa8e3a1616f2de3ea0a4947e16789bb64cb0f177deff388eb1390c100c82d7e6fecd4d583646bb9305fb3df3ca824b1bdbebdc8e languageName: node linkType: hard @@ -1045,6 +1508,18 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-class-properties@npm:^7.27.1, @babel/plugin-transform-class-properties@npm:^7.28.0, @babel/plugin-transform-class-properties@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/plugin-transform-class-properties@npm:7.29.7" + dependencies: + "@babel/helper-create-class-features-plugin": ^7.29.7 + "@babel/helper-plugin-utils": ^7.29.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 0cc5e7a882e29eead360f02ef79f6b2ec3b3813213b1513d8fdaa931d1d1361fccc92fbacc9b399e42495953d9d6fc722f283b5f3aa272fe016a0b5fe1e6a130 + languageName: node + linkType: hard + "@babel/plugin-transform-class-static-block@npm:^7.28.6": version: 7.28.6 resolution: "@babel/plugin-transform-class-static-block@npm:7.28.6" @@ -1057,19 +1532,15 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-classes@npm:7.28.4": - version: 7.28.4 - resolution: "@babel/plugin-transform-classes@npm:7.28.4" +"@babel/plugin-transform-class-static-block@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/plugin-transform-class-static-block@npm:7.29.7" dependencies: - "@babel/helper-annotate-as-pure": ^7.27.3 - "@babel/helper-compilation-targets": ^7.27.2 - "@babel/helper-globals": ^7.28.0 - "@babel/helper-plugin-utils": ^7.27.1 - "@babel/helper-replace-supers": ^7.27.1 - "@babel/traverse": ^7.28.4 + "@babel/helper-create-class-features-plugin": ^7.29.7 + "@babel/helper-plugin-utils": ^7.29.7 peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: f412e00c86584a9094cc0a2f3dd181b8108a4dced477d609c5406beddd5bf79d05a7ea74db508dc4dcb37172f042d5ef98d3d6311ade61c7ea6fbbbb70f5ec29 + "@babel/core": ^7.12.0 + checksum: fe94eb94d8417de753a26f06a3c50780cdfc3f07e10bcd09dde2fe61dcd9a2714b83b1b2d36733328a3ad09b02e84fa06197f757644ffbcca1673e87c64ecb76 languageName: node linkType: hard @@ -1089,6 +1560,22 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-classes@npm:^7.28.4, @babel/plugin-transform-classes@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/plugin-transform-classes@npm:7.29.7" + dependencies: + "@babel/helper-annotate-as-pure": ^7.29.7 + "@babel/helper-compilation-targets": ^7.29.7 + "@babel/helper-globals": ^7.29.7 + "@babel/helper-plugin-utils": ^7.29.7 + "@babel/helper-replace-supers": ^7.29.7 + "@babel/traverse": ^7.29.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: cc45ff5b5b063339131cd701266af90506db8640e56494de0c78f882da6317f057951bf6507c5b48a0278cf5dad21691baf2af05e24b8c26b0725d677088edbf + languageName: node + linkType: hard + "@babel/plugin-transform-computed-properties@npm:^7.0.0, @babel/plugin-transform-computed-properties@npm:^7.24.7, @babel/plugin-transform-computed-properties@npm:^7.28.6": version: 7.28.6 resolution: "@babel/plugin-transform-computed-properties@npm:7.28.6" @@ -1101,6 +1588,18 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-computed-properties@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/plugin-transform-computed-properties@npm:7.29.7" + dependencies: + "@babel/helper-plugin-utils": ^7.29.7 + "@babel/template": ^7.29.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: ad945a48e6826c3b34f825e780ab33bf91e18c7924dd263e44bef8d8a6350636305b22af8f00793d3767482004651f4bfb9fed0c92f05c01b99ae80d79956e67 + languageName: node + linkType: hard + "@babel/plugin-transform-destructuring@npm:^7.20.0, @babel/plugin-transform-destructuring@npm:^7.24.8, @babel/plugin-transform-destructuring@npm:^7.27.3, @babel/plugin-transform-destructuring@npm:^7.28.5": version: 7.28.5 resolution: "@babel/plugin-transform-destructuring@npm:7.28.5" @@ -1113,6 +1612,18 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-destructuring@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/plugin-transform-destructuring@npm:7.29.7" + dependencies: + "@babel/helper-plugin-utils": ^7.29.7 + "@babel/traverse": ^7.29.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 5f56c030beec2ae1640909eb5213fc655f0062046b28699d049cc8b00fb7d9aa1c5ba1dc1c7ee41c8bae97be778066f1f9ea2ecd8fff872a1e8f10fd3addf18c + languageName: node + linkType: hard + "@babel/plugin-transform-dotall-regex@npm:^7.28.6": version: 7.28.6 resolution: "@babel/plugin-transform-dotall-regex@npm:7.28.6" @@ -1125,6 +1636,18 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-dotall-regex@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/plugin-transform-dotall-regex@npm:7.29.7" + dependencies: + "@babel/helper-create-regexp-features-plugin": ^7.29.7 + "@babel/helper-plugin-utils": ^7.29.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 537df0fb915a420df715a2f4da16ab6c08ce2370521edef9a8a59af23a533314109f6abd836c5e127c8cea4a46bc4a05692670cf7ad64868c286075fa2d7848c + languageName: node + linkType: hard + "@babel/plugin-transform-duplicate-keys@npm:^7.27.1": version: 7.27.1 resolution: "@babel/plugin-transform-duplicate-keys@npm:7.27.1" @@ -1136,6 +1659,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-duplicate-keys@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/plugin-transform-duplicate-keys@npm:7.29.7" + dependencies: + "@babel/helper-plugin-utils": ^7.29.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 39bf312a798792e361d2afa9e900eb7d7fd853239a776912bafc5bb3799886374d54a0882d21517296086abdd3e5458405f6f3a7da5dcb493bc86c5a32576bab + languageName: node + linkType: hard + "@babel/plugin-transform-duplicate-named-capturing-groups-regex@npm:^7.28.6": version: 7.28.6 resolution: "@babel/plugin-transform-duplicate-named-capturing-groups-regex@npm:7.28.6" @@ -1148,6 +1682,18 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-duplicate-named-capturing-groups-regex@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/plugin-transform-duplicate-named-capturing-groups-regex@npm:7.29.7" + dependencies: + "@babel/helper-create-regexp-features-plugin": ^7.29.7 + "@babel/helper-plugin-utils": ^7.29.7 + peerDependencies: + "@babel/core": ^7.0.0 + checksum: fa7fcdbede10dbc06fe4f861e90286f7f599d3f3c43e69445e63c3f48422fd34db0b0dd9bbebccd1dbd04a50fca4ab22fd82d28e7bc8fe9b6d5790c9e694d380 + languageName: node + linkType: hard + "@babel/plugin-transform-dynamic-import@npm:^7.27.1": version: 7.27.1 resolution: "@babel/plugin-transform-dynamic-import@npm:7.27.1" @@ -1159,6 +1705,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-dynamic-import@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/plugin-transform-dynamic-import@npm:7.29.7" + dependencies: + "@babel/helper-plugin-utils": ^7.29.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 58c035e6b9103c225f3d181671d9b3dec140351c3ecf12bc3a66b8653be41161f20135ada00a703244c2bfc9dd57b62fc54f77f2f6fe43df6c598358f377e6fa + languageName: node + linkType: hard + "@babel/plugin-transform-explicit-resource-management@npm:^7.28.0, @babel/plugin-transform-explicit-resource-management@npm:^7.28.6": version: 7.28.6 resolution: "@babel/plugin-transform-explicit-resource-management@npm:7.28.6" @@ -1171,6 +1728,18 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-explicit-resource-management@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/plugin-transform-explicit-resource-management@npm:7.29.7" + dependencies: + "@babel/helper-plugin-utils": ^7.29.7 + "@babel/plugin-transform-destructuring": ^7.29.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 28ff30f46d97b91ba5f57ae63499a489fa1ec3dcc427ec851c2aacb34106b573c58eba4be000bc4c28223ba767cfadc008cdfa9f833ed12996e2f014e0a06373 + languageName: node + linkType: hard + "@babel/plugin-transform-exponentiation-operator@npm:^7.28.6": version: 7.28.6 resolution: "@babel/plugin-transform-exponentiation-operator@npm:7.28.6" @@ -1182,6 +1751,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-exponentiation-operator@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/plugin-transform-exponentiation-operator@npm:7.29.7" + dependencies: + "@babel/helper-plugin-utils": ^7.29.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: bf0366d77d318d4b6eae6880217e3fdfcb6e5f7913f658583de9537fd4fca1f05f9ac4083d8f946a84eaefec068cbba948be90f4a39484c85bc69082def3162d + languageName: node + linkType: hard + "@babel/plugin-transform-export-namespace-from@npm:^7.27.1": version: 7.27.1 resolution: "@babel/plugin-transform-export-namespace-from@npm:7.27.1" @@ -1193,6 +1773,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-export-namespace-from@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/plugin-transform-export-namespace-from@npm:7.29.7" + dependencies: + "@babel/helper-plugin-utils": ^7.29.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: d157d62b144d1626b801e557dcede914db33e78f3f4230f487e5709c21efd40648f7f3a47a44a7fce694ad9cd117d2ac3ed796da0102275fd02b4fdb317d906b + languageName: node + linkType: hard + "@babel/plugin-transform-flow-strip-types@npm:^7.16.0, @babel/plugin-transform-flow-strip-types@npm:^7.20.0, @babel/plugin-transform-flow-strip-types@npm:^7.25.2, @babel/plugin-transform-flow-strip-types@npm:^7.25.9, @babel/plugin-transform-flow-strip-types@npm:^7.27.1": version: 7.27.1 resolution: "@babel/plugin-transform-flow-strip-types@npm:7.27.1" @@ -1205,6 +1796,18 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-flow-strip-types@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/plugin-transform-flow-strip-types@npm:7.29.7" + dependencies: + "@babel/helper-plugin-utils": ^7.29.7 + "@babel/plugin-syntax-flow": ^7.29.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: bc6e94f5398bb512b1a1d7a2bfb36dff9dd7d5150a3644e2ff4f361512b6be3c6a164828263fedbbc9eaa0015617b6c4e0e347117a0ae02acc184215cc27a6c2 + languageName: node + linkType: hard + "@babel/plugin-transform-for-of@npm:^7.0.0, @babel/plugin-transform-for-of@npm:^7.24.7, @babel/plugin-transform-for-of@npm:^7.27.1": version: 7.27.1 resolution: "@babel/plugin-transform-for-of@npm:7.27.1" @@ -1217,6 +1820,18 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-for-of@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/plugin-transform-for-of@npm:7.29.7" + dependencies: + "@babel/helper-plugin-utils": ^7.29.7 + "@babel/helper-skip-transparent-expression-wrappers": ^7.29.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 7641d5eb4ef268df2f7d8736691a565646f42c55fdc2c86a65ed027276415a19ac6cafab2fc386621894c5d3202baacc9f222424bda37fa2ab9a20d7fec921f9 + languageName: node + linkType: hard + "@babel/plugin-transform-function-name@npm:^7.0.0, @babel/plugin-transform-function-name@npm:^7.25.1, @babel/plugin-transform-function-name@npm:^7.27.1": version: 7.27.1 resolution: "@babel/plugin-transform-function-name@npm:7.27.1" @@ -1230,6 +1845,19 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-function-name@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/plugin-transform-function-name@npm:7.29.7" + dependencies: + "@babel/helper-compilation-targets": ^7.29.7 + "@babel/helper-plugin-utils": ^7.29.7 + "@babel/traverse": ^7.29.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 87a6329442ef8085fbc160e659f64562f0f5b63be65403b8bbb8e18c67dd7d03b82fb2e1371fdde734e2f05b13a72f88ed8d8cb03807150063954b9604d082cf + languageName: node + linkType: hard + "@babel/plugin-transform-json-strings@npm:^7.28.6": version: 7.28.6 resolution: "@babel/plugin-transform-json-strings@npm:7.28.6" @@ -1241,6 +1869,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-json-strings@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/plugin-transform-json-strings@npm:7.29.7" + dependencies: + "@babel/helper-plugin-utils": ^7.29.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 3af97e144e0d986522f01803ffa0f90741530d1610952ac6a92511c356ecbf5616092ab1d21401d25bc7cb8ac90d73c2c7ff35e41766df2708c29d7e588cfa7f + languageName: node + linkType: hard + "@babel/plugin-transform-literals@npm:^7.0.0, @babel/plugin-transform-literals@npm:^7.25.2, @babel/plugin-transform-literals@npm:^7.27.1": version: 7.27.1 resolution: "@babel/plugin-transform-literals@npm:7.27.1" @@ -1252,6 +1891,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-literals@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/plugin-transform-literals@npm:7.29.7" + dependencies: + "@babel/helper-plugin-utils": ^7.29.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: aadb2b3fe85186c274a07d5486aeef9496ce374e534fbc7b54f77985c75513422d9acec4c532f67b027e939644d93a69c00505b8909e259184c3ee5c5c62c46b + languageName: node + linkType: hard + "@babel/plugin-transform-logical-assignment-operators@npm:^7.24.1, @babel/plugin-transform-logical-assignment-operators@npm:^7.24.7, @babel/plugin-transform-logical-assignment-operators@npm:^7.28.6": version: 7.28.6 resolution: "@babel/plugin-transform-logical-assignment-operators@npm:7.28.6" @@ -1263,6 +1913,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-logical-assignment-operators@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/plugin-transform-logical-assignment-operators@npm:7.29.7" + dependencies: + "@babel/helper-plugin-utils": ^7.29.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 374d83dfbb2de5e339d2966487c0f0d766cd67422addbd0172104ff2788b60317858172a7ab2cb6ee7d5bffad72ce07120fec009a2ef3b35551013fce4908686 + languageName: node + linkType: hard + "@babel/plugin-transform-member-expression-literals@npm:^7.27.1": version: 7.27.1 resolution: "@babel/plugin-transform-member-expression-literals@npm:7.27.1" @@ -1274,6 +1935,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-member-expression-literals@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/plugin-transform-member-expression-literals@npm:7.29.7" + dependencies: + "@babel/helper-plugin-utils": ^7.29.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 698cbc9500e8caea1d36b48248112d60e023cea9d0772ac1ecf1639b38b662d9352043747dbe617fe1f6f17709bc17601534f7bf2f22c791fb86f7e2ab43e39f + languageName: node + linkType: hard + "@babel/plugin-transform-modules-amd@npm:^7.27.1": version: 7.27.1 resolution: "@babel/plugin-transform-modules-amd@npm:7.27.1" @@ -1286,6 +1958,18 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-modules-amd@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/plugin-transform-modules-amd@npm:7.29.7" + dependencies: + "@babel/helper-module-transforms": ^7.29.7 + "@babel/helper-plugin-utils": ^7.29.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 6fce7d329230e3be0389b1135b6ffa1224c3f060294409137c5bb7f26d81ac47b6d91651deaef93b17f5e6f99e5ca7edb80b6b855b562b65ad00b3d7f717da40 + languageName: node + linkType: hard + "@babel/plugin-transform-modules-commonjs@npm:^7.0.0, @babel/plugin-transform-modules-commonjs@npm:^7.13.8, @babel/plugin-transform-modules-commonjs@npm:^7.24.8, @babel/plugin-transform-modules-commonjs@npm:^7.27.1, @babel/plugin-transform-modules-commonjs@npm:^7.28.6": version: 7.28.6 resolution: "@babel/plugin-transform-modules-commonjs@npm:7.28.6" @@ -1298,17 +1982,43 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-modules-commonjs@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/plugin-transform-modules-commonjs@npm:7.29.7" + dependencies: + "@babel/helper-module-transforms": ^7.29.7 + "@babel/helper-plugin-utils": ^7.29.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: e9d8102deca4c2f06507a8d071ca0e161f01cec0e108151142edb39995bd830e312d55d21fac0ceb390ac79a1fee5fb768b719413b8fc5adda5f06ecd8845cd6 + languageName: node + linkType: hard + "@babel/plugin-transform-modules-systemjs@npm:^7.28.5": - version: 7.28.5 - resolution: "@babel/plugin-transform-modules-systemjs@npm:7.28.5" + version: 7.29.4 + resolution: "@babel/plugin-transform-modules-systemjs@npm:7.29.4" dependencies: - "@babel/helper-module-transforms": ^7.28.3 - "@babel/helper-plugin-utils": ^7.27.1 + "@babel/helper-module-transforms": ^7.28.6 + "@babel/helper-plugin-utils": ^7.28.6 "@babel/helper-validator-identifier": ^7.28.5 - "@babel/traverse": ^7.28.5 + "@babel/traverse": ^7.29.0 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: d9cbb30669077756048af990a08ad1ba149785c336024affa49848dc4ffc5948bfaaf52d90bbec711a1f320e19e2c60182dbeff40d81cc5b9a09a87919abe07d + languageName: node + linkType: hard + +"@babel/plugin-transform-modules-systemjs@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/plugin-transform-modules-systemjs@npm:7.29.7" + dependencies: + "@babel/helper-module-transforms": ^7.29.7 + "@babel/helper-plugin-utils": ^7.29.7 + "@babel/helper-validator-identifier": ^7.29.7 + "@babel/traverse": ^7.29.7 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 646748dcf968c107fedfbff38aa37f7a9ebf2ccdf51fd9f578c6cd323371db36bbc5fe0d995544db168f39be9bca32a85fbf3bfff4742d2bed22e21c2847fa46 + checksum: 185d26d1f20fabdb120cb0bacdb2d245a8fbe628778cc2cd096a804c1169e21ae555e482b76fcc04a585edd70d9758a456794ae4d69cd41e638e10f620d65058 languageName: node linkType: hard @@ -1324,6 +2034,18 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-modules-umd@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/plugin-transform-modules-umd@npm:7.29.7" + dependencies: + "@babel/helper-module-transforms": ^7.29.7 + "@babel/helper-plugin-utils": ^7.29.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 589f5c30a906587d039e2f3bf8267652be272d93d3056667b479f450318c91e55d1674631239ac25e0e9591f4abb7ff225bb0fc8af58e5bfb703d3385ec02082 + languageName: node + linkType: hard + "@babel/plugin-transform-named-capturing-groups-regex@npm:^7.0.0, @babel/plugin-transform-named-capturing-groups-regex@npm:^7.24.7, @babel/plugin-transform-named-capturing-groups-regex@npm:^7.27.1": version: 7.27.1 resolution: "@babel/plugin-transform-named-capturing-groups-regex@npm:7.27.1" @@ -1336,6 +2058,18 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-named-capturing-groups-regex@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/plugin-transform-named-capturing-groups-regex@npm:7.29.7" + dependencies: + "@babel/helper-create-regexp-features-plugin": ^7.29.7 + "@babel/helper-plugin-utils": ^7.29.7 + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 3a481be133e9ca5d25570b5ed62daae323a51663bacf30fed0d1980e912047ebd34d6326533182db625fc0bbd086d1a23ed68ebb6108e7a68a8650c228afc084 + languageName: node + linkType: hard + "@babel/plugin-transform-new-target@npm:^7.27.1": version: 7.27.1 resolution: "@babel/plugin-transform-new-target@npm:7.27.1" @@ -1347,14 +2081,14 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-nullish-coalescing-operator@npm:7.27.1": - version: 7.27.1 - resolution: "@babel/plugin-transform-nullish-coalescing-operator@npm:7.27.1" +"@babel/plugin-transform-new-target@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/plugin-transform-new-target@npm:7.29.7" dependencies: - "@babel/helper-plugin-utils": ^7.27.1 + "@babel/helper-plugin-utils": ^7.29.7 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 1c6b3730748782d2178cc30f5cc37be7d7666148260f3f2dfc43999908bdd319bdfebaaf19cf04ac1f9dee0f7081093d3fa730cda5ae1b34bcd73ce406a78be7 + checksum: 35b4c295348d17c9c00cf772663a6e705fd48f5a9a5378d7b548ab4cb71d5f183ca9446d2a86c77f16b037cfe64c621c00e93219aefab43bfd39d8dd2c8b56bb languageName: node linkType: hard @@ -1369,6 +2103,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-nullish-coalescing-operator@npm:^7.27.1, @babel/plugin-transform-nullish-coalescing-operator@npm:^7.28.0, @babel/plugin-transform-nullish-coalescing-operator@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/plugin-transform-nullish-coalescing-operator@npm:7.29.7" + dependencies: + "@babel/helper-plugin-utils": ^7.29.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: f486e737ddcec3a88e2e0dc004c28e15156dd255f3b2d944903f3d75666257c96ee7ebf57d80d2cf42eda2bee497db8134a26d657ddc3defcc34b9583ecbd119 + languageName: node + linkType: hard + "@babel/plugin-transform-numeric-separator@npm:^7.24.1, @babel/plugin-transform-numeric-separator@npm:^7.24.7, @babel/plugin-transform-numeric-separator@npm:^7.28.6": version: 7.28.6 resolution: "@babel/plugin-transform-numeric-separator@npm:7.28.6" @@ -1380,6 +2125,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-numeric-separator@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/plugin-transform-numeric-separator@npm:7.29.7" + dependencies: + "@babel/helper-plugin-utils": ^7.29.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 6177df9e1a8a190bf4a360f8cbcfa614b70ededba61201bd0a38929770b6d00a8a54a19f8fda82cf60688d9bab938427a9fe5dae0a9e8cd8ed79c88a3b2dbcd7 + languageName: node + linkType: hard + "@babel/plugin-transform-object-rest-spread@npm:^7.24.5, @babel/plugin-transform-object-rest-spread@npm:^7.24.7, @babel/plugin-transform-object-rest-spread@npm:^7.28.6": version: 7.28.6 resolution: "@babel/plugin-transform-object-rest-spread@npm:7.28.6" @@ -1395,6 +2151,21 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-object-rest-spread@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/plugin-transform-object-rest-spread@npm:7.29.7" + dependencies: + "@babel/helper-compilation-targets": ^7.29.7 + "@babel/helper-plugin-utils": ^7.29.7 + "@babel/plugin-transform-destructuring": ^7.29.7 + "@babel/plugin-transform-parameters": ^7.29.7 + "@babel/traverse": ^7.29.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: e09bcc8800cf374962ab98bce5ccceb900266278a3a1e4a68391abec440fdf7371c8059f9091f407a1b167f3acf624c34c9b92188674fdcb7797e3f02509216b + languageName: node + linkType: hard + "@babel/plugin-transform-object-super@npm:^7.27.1": version: 7.27.1 resolution: "@babel/plugin-transform-object-super@npm:7.27.1" @@ -1407,6 +2178,18 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-object-super@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/plugin-transform-object-super@npm:7.29.7" + dependencies: + "@babel/helper-plugin-utils": ^7.29.7 + "@babel/helper-replace-supers": ^7.29.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 65ed8719563572c4917f19b32c476c9a9062fccf89bfd44a418bb734cd866034d66049499cace58e2bb4589da779983f534078bd989333f36268b9da08d4b33c + languageName: node + linkType: hard + "@babel/plugin-transform-optional-catch-binding@npm:^7.24.1, @babel/plugin-transform-optional-catch-binding@npm:^7.24.7, @babel/plugin-transform-optional-catch-binding@npm:^7.28.6": version: 7.28.6 resolution: "@babel/plugin-transform-optional-catch-binding@npm:7.28.6" @@ -1418,15 +2201,14 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-optional-chaining@npm:7.27.1": - version: 7.27.1 - resolution: "@babel/plugin-transform-optional-chaining@npm:7.27.1" +"@babel/plugin-transform-optional-catch-binding@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/plugin-transform-optional-catch-binding@npm:7.29.7" dependencies: - "@babel/helper-plugin-utils": ^7.27.1 - "@babel/helper-skip-transparent-expression-wrappers": ^7.27.1 + "@babel/helper-plugin-utils": ^7.29.7 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: c4428d31f182d724db6f10575669aad3dbccceb0dea26aa9071fa89f11b3456278da3097fcc78937639a13c105a82cd452dc0218ce51abdbcf7626a013b928a5 + checksum: 4b6e41e1dc5dbd02cfe0b96214130cca5fd3bd879551fc82188bb3d9a2782af9bab50f2140af9ff946a8ee23b9478ee42810641fb99aa3e033884d7c6103d138 languageName: node linkType: hard @@ -1442,6 +2224,18 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-optional-chaining@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/plugin-transform-optional-chaining@npm:7.29.7" + dependencies: + "@babel/helper-plugin-utils": ^7.29.7 + "@babel/helper-skip-transparent-expression-wrappers": ^7.29.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 6255d259bb0143f7938278ebb382aba22311c260919d3f08476509c76335a111b479b3ad7363e86de064f87af85ca4d7f03f08195f0646c525ea70fb587c0a2d + languageName: node + linkType: hard + "@babel/plugin-transform-parameters@npm:^7.0.0, @babel/plugin-transform-parameters@npm:^7.24.7, @babel/plugin-transform-parameters@npm:^7.27.7": version: 7.27.7 resolution: "@babel/plugin-transform-parameters@npm:7.27.7" @@ -1453,6 +2247,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-parameters@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/plugin-transform-parameters@npm:7.29.7" + dependencies: + "@babel/helper-plugin-utils": ^7.29.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 615cdd72dc2d08051e6d4e7f0539661e40029257a047ce15c5da5f4394a372a53f6f6d6aece72ad15ccf0590e448c9a742f8576564321c9dcc16262ec6197c9b + languageName: node + linkType: hard + "@babel/plugin-transform-private-methods@npm:^7.22.5, @babel/plugin-transform-private-methods@npm:^7.24.7, @babel/plugin-transform-private-methods@npm:^7.28.6": version: 7.28.6 resolution: "@babel/plugin-transform-private-methods@npm:7.28.6" @@ -1465,6 +2270,18 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-private-methods@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/plugin-transform-private-methods@npm:7.29.7" + dependencies: + "@babel/helper-create-class-features-plugin": ^7.29.7 + "@babel/helper-plugin-utils": ^7.29.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 5055af2b86a95acbf6bd1a14256edf439ba4f214707f6aa9f6a29d1168c882e5709853c4ff225f55575f88d3a58effbed952d25c5cd70f93785106541f992cdd + languageName: node + linkType: hard + "@babel/plugin-transform-private-property-in-object@npm:^7.22.11, @babel/plugin-transform-private-property-in-object@npm:^7.24.7, @babel/plugin-transform-private-property-in-object@npm:^7.28.6": version: 7.28.6 resolution: "@babel/plugin-transform-private-property-in-object@npm:7.28.6" @@ -1478,6 +2295,19 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-private-property-in-object@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/plugin-transform-private-property-in-object@npm:7.29.7" + dependencies: + "@babel/helper-annotate-as-pure": ^7.29.7 + "@babel/helper-create-class-features-plugin": ^7.29.7 + "@babel/helper-plugin-utils": ^7.29.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 068ff9e35e103b269c707d16f50384646295a6613c5abfdebea24f0430bb18ea4ef40a299e1dcc915bb22f65e2217c3428c20bcd4a3fe5f8ac60ec212835646d + languageName: node + linkType: hard + "@babel/plugin-transform-property-literals@npm:^7.27.1": version: 7.27.1 resolution: "@babel/plugin-transform-property-literals@npm:7.27.1" @@ -1489,6 +2319,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-property-literals@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/plugin-transform-property-literals@npm:7.29.7" + dependencies: + "@babel/helper-plugin-utils": ^7.29.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 7e7a557df8feb50cd6913158c6d12df0e8a9da58e3f73e7cbfc08af399055b03e77a22b12c73d5bf6bb49239617d6189ccb6021ab03f0225bc54f9c74a880b62 + languageName: node + linkType: hard + "@babel/plugin-transform-react-constant-elements@npm:^7.12.1, @babel/plugin-transform-react-constant-elements@npm:^7.21.3": version: 7.27.1 resolution: "@babel/plugin-transform-react-constant-elements@npm:7.27.1" @@ -1511,6 +2352,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-react-display-name@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/plugin-transform-react-display-name@npm:7.29.7" + dependencies: + "@babel/helper-plugin-utils": ^7.29.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: ded95cce1816f800db43e8f4e1f7fbb928091bf036438617b8ca7e9ce776079606045a0ca482904bfeff801c4fc726de633153843c441ef31980b8c41ace04c9 + languageName: node + linkType: hard + "@babel/plugin-transform-react-jsx-development@npm:^7.27.1": version: 7.27.1 resolution: "@babel/plugin-transform-react-jsx-development@npm:7.27.1" @@ -1522,6 +2374,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-react-jsx-development@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/plugin-transform-react-jsx-development@npm:7.29.7" + dependencies: + "@babel/plugin-transform-react-jsx": ^7.29.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: c535bb5ee09e07839a422f7a8e55849cd30525af57021888eceb84d33391290f6250207319bb4fbb4d4cbdcdb894b2a2b963f0769a3e95536370159b4b505855 + languageName: node + linkType: hard + "@babel/plugin-transform-react-jsx-self@npm:^7.0.0, @babel/plugin-transform-react-jsx-self@npm:^7.24.7": version: 7.27.1 resolution: "@babel/plugin-transform-react-jsx-self@npm:7.27.1" @@ -1559,6 +2422,21 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-react-jsx@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/plugin-transform-react-jsx@npm:7.29.7" + dependencies: + "@babel/helper-annotate-as-pure": ^7.29.7 + "@babel/helper-module-imports": ^7.29.7 + "@babel/helper-plugin-utils": ^7.29.7 + "@babel/plugin-syntax-jsx": ^7.29.7 + "@babel/types": ^7.29.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: d50e5d6f12051c688280b118fc0cdc49f617f7c1f2c41b25733b606aa9a14d2dc84bc544163d115226b9d2cde9f147f49568350b9d100cb47988e5e76cf495c7 + languageName: node + linkType: hard + "@babel/plugin-transform-react-pure-annotations@npm:^7.27.1": version: 7.27.1 resolution: "@babel/plugin-transform-react-pure-annotations@npm:7.27.1" @@ -1571,6 +2449,18 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-react-pure-annotations@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/plugin-transform-react-pure-annotations@npm:7.29.7" + dependencies: + "@babel/helper-annotate-as-pure": ^7.29.7 + "@babel/helper-plugin-utils": ^7.29.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 7b6bc9e9db06f2c40685b4f0a043af17a98a8bee833831aa28f70c89876dc649fdd682ae572445143b53fe091258964107bae9d3583480eb1c4f1d9c22780b38 + languageName: node + linkType: hard + "@babel/plugin-transform-regenerator@npm:^7.20.0, @babel/plugin-transform-regenerator@npm:^7.24.7, @babel/plugin-transform-regenerator@npm:^7.28.6": version: 7.28.6 resolution: "@babel/plugin-transform-regenerator@npm:7.28.6" @@ -1582,6 +2472,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-regenerator@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/plugin-transform-regenerator@npm:7.29.7" + dependencies: + "@babel/helper-plugin-utils": ^7.29.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 1a136712ba92693402d7fafb96624d1a833e888cd59029a84ed24d8ff083304a3f1d101d8d5013d0400229041c8a1e4ddf08027268f3c876ea226e86734acd25 + languageName: node + linkType: hard + "@babel/plugin-transform-regexp-modifiers@npm:^7.28.6": version: 7.28.6 resolution: "@babel/plugin-transform-regexp-modifiers@npm:7.28.6" @@ -1594,6 +2495,18 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-regexp-modifiers@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/plugin-transform-regexp-modifiers@npm:7.29.7" + dependencies: + "@babel/helper-create-regexp-features-plugin": ^7.29.7 + "@babel/helper-plugin-utils": ^7.29.7 + peerDependencies: + "@babel/core": ^7.0.0 + checksum: bfbc6b898ace99b8412eb6e902b90856188c692e69672d42d48c5e22a5bf9b0d15d35424fda8501bfb06ba0504acc5f1b9e13eacaba232aa58af74472d6068dc + languageName: node + linkType: hard + "@babel/plugin-transform-reserved-words@npm:^7.27.1": version: 7.27.1 resolution: "@babel/plugin-transform-reserved-words@npm:7.27.1" @@ -1605,6 +2518,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-reserved-words@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/plugin-transform-reserved-words@npm:7.29.7" + dependencies: + "@babel/helper-plugin-utils": ^7.29.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: ffd7f86ddce36c6ded2dd9218f81be8cbae35b1ed01100ac0a98623bd0a76c059188b70953ab5accae8f8430451e8ed4779d533ac5e35c523f5004a981208b7c + languageName: node + linkType: hard + "@babel/plugin-transform-runtime@npm:^7.0.0, @babel/plugin-transform-runtime@npm:^7.16.4, @babel/plugin-transform-runtime@npm:^7.24.7, @babel/plugin-transform-runtime@npm:^7.25.9": version: 7.28.5 resolution: "@babel/plugin-transform-runtime@npm:7.28.5" @@ -1621,7 +2545,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-shorthand-properties@npm:7.27.1, @babel/plugin-transform-shorthand-properties@npm:^7.0.0, @babel/plugin-transform-shorthand-properties@npm:^7.24.7, @babel/plugin-transform-shorthand-properties@npm:^7.27.1": +"@babel/plugin-transform-shorthand-properties@npm:^7.0.0, @babel/plugin-transform-shorthand-properties@npm:^7.24.7, @babel/plugin-transform-shorthand-properties@npm:^7.27.1": version: 7.27.1 resolution: "@babel/plugin-transform-shorthand-properties@npm:7.27.1" dependencies: @@ -1632,6 +2556,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-shorthand-properties@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/plugin-transform-shorthand-properties@npm:7.29.7" + dependencies: + "@babel/helper-plugin-utils": ^7.29.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: c57ef27853f334a6147da9aa00f8a8f4c3a1c217eb2efa73cba2e118edda10754fa23cec2c0c7f7408279ad28fef92c1f663dfec137a7503813331569c3e02f9 + languageName: node + linkType: hard + "@babel/plugin-transform-spread@npm:^7.0.0, @babel/plugin-transform-spread@npm:^7.24.7, @babel/plugin-transform-spread@npm:^7.28.6": version: 7.28.6 resolution: "@babel/plugin-transform-spread@npm:7.28.6" @@ -1644,6 +2579,18 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-spread@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/plugin-transform-spread@npm:7.29.7" + dependencies: + "@babel/helper-plugin-utils": ^7.29.7 + "@babel/helper-skip-transparent-expression-wrappers": ^7.29.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: eb25f24d9a5cac163fea91b5872ff2ddb892083624284a6e8a1fa8dfc4c4c6f6230f3b478405eb846df895a3f96631ac19e8c169af33012aafaa144e6192d5d3 + languageName: node + linkType: hard + "@babel/plugin-transform-sticky-regex@npm:^7.0.0, @babel/plugin-transform-sticky-regex@npm:^7.24.7, @babel/plugin-transform-sticky-regex@npm:^7.27.1": version: 7.27.1 resolution: "@babel/plugin-transform-sticky-regex@npm:7.27.1" @@ -1655,7 +2602,18 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-template-literals@npm:7.27.1, @babel/plugin-transform-template-literals@npm:^7.27.1": +"@babel/plugin-transform-sticky-regex@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/plugin-transform-sticky-regex@npm:7.29.7" + dependencies: + "@babel/helper-plugin-utils": ^7.29.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 16b570c0270a59c2a29b2118c00e463882e9dda49b51a17dde3ae6b2886995d49f4bf2161a4c743ad1bca39d2aeb3ac963400e095682e105c7f751e6a2156c28 + languageName: node + linkType: hard + +"@babel/plugin-transform-template-literals@npm:^7.27.1": version: 7.27.1 resolution: "@babel/plugin-transform-template-literals@npm:7.27.1" dependencies: @@ -1666,6 +2624,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-template-literals@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/plugin-transform-template-literals@npm:7.29.7" + dependencies: + "@babel/helper-plugin-utils": ^7.29.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: d1014dab020f0f802089de17bba82d929eda6ac87fde5f58fb9763885b8d645ce63fc1df97055c06a12d95a8788334a93b559fcaf1da6d7777a191e5f9c5646e + languageName: node + linkType: hard + "@babel/plugin-transform-typeof-symbol@npm:^7.27.1": version: 7.27.1 resolution: "@babel/plugin-transform-typeof-symbol@npm:7.27.1" @@ -1677,7 +2646,18 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-typescript@npm:^7.25.2, @babel/plugin-transform-typescript@npm:^7.27.1, @babel/plugin-transform-typescript@npm:^7.28.5, @babel/plugin-transform-typescript@npm:^7.5.0": +"@babel/plugin-transform-typeof-symbol@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/plugin-transform-typeof-symbol@npm:7.29.7" + dependencies: + "@babel/helper-plugin-utils": ^7.29.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 90c2308f97c4e3773b7d2010b962ce6122bd9e1163f0caf7bb45819342f547a5a41d36c73d9a828ad89ce2072e2613ea867d28ad59eb440d9e22196438437d02 + languageName: node + linkType: hard + +"@babel/plugin-transform-typescript@npm:^7.25.2, @babel/plugin-transform-typescript@npm:^7.28.5, @babel/plugin-transform-typescript@npm:^7.5.0": version: 7.28.6 resolution: "@babel/plugin-transform-typescript@npm:7.28.6" dependencies: @@ -1692,6 +2672,21 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-typescript@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/plugin-transform-typescript@npm:7.29.7" + dependencies: + "@babel/helper-annotate-as-pure": ^7.29.7 + "@babel/helper-create-class-features-plugin": ^7.29.7 + "@babel/helper-plugin-utils": ^7.29.7 + "@babel/helper-skip-transparent-expression-wrappers": ^7.29.7 + "@babel/plugin-syntax-typescript": ^7.29.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: e95bce53fa2add836eec5ef5221e260cfa4ab889a146f7ba5e29cbd42bfe3183cb94e40b49bfb0d14a75f233982723903d3efad0f528b835ce771e38bd365440 + languageName: node + linkType: hard + "@babel/plugin-transform-unicode-escapes@npm:^7.27.1": version: 7.27.1 resolution: "@babel/plugin-transform-unicode-escapes@npm:7.27.1" @@ -1703,6 +2698,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-unicode-escapes@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/plugin-transform-unicode-escapes@npm:7.29.7" + dependencies: + "@babel/helper-plugin-utils": ^7.29.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: cf337bf93a2e76aa82acb60d8a8a567225744cf7f42ffa4dde4c177aef54250f4067db38b8ab2c26f20cf2a589822242891ba8b91739a705f618bf6a4eeca7ff + languageName: node + linkType: hard + "@babel/plugin-transform-unicode-property-regex@npm:^7.28.6": version: 7.28.6 resolution: "@babel/plugin-transform-unicode-property-regex@npm:7.28.6" @@ -1715,7 +2721,19 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-unicode-regex@npm:7.27.1, @babel/plugin-transform-unicode-regex@npm:^7.0.0, @babel/plugin-transform-unicode-regex@npm:^7.24.7, @babel/plugin-transform-unicode-regex@npm:^7.27.1": +"@babel/plugin-transform-unicode-property-regex@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/plugin-transform-unicode-property-regex@npm:7.29.7" + dependencies: + "@babel/helper-create-regexp-features-plugin": ^7.29.7 + "@babel/helper-plugin-utils": ^7.29.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 03ee0b5d27eee08ba71bc09e919eb648094123985b7adc7dc875e4172100596a47ab68337abf6814cbd3140c6552cf1044135eb0ec1ec78345e1270e5e6aabba + languageName: node + linkType: hard + +"@babel/plugin-transform-unicode-regex@npm:^7.0.0, @babel/plugin-transform-unicode-regex@npm:^7.24.7, @babel/plugin-transform-unicode-regex@npm:^7.27.1": version: 7.27.1 resolution: "@babel/plugin-transform-unicode-regex@npm:7.27.1" dependencies: @@ -1727,6 +2745,18 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-unicode-regex@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/plugin-transform-unicode-regex@npm:7.29.7" + dependencies: + "@babel/helper-create-regexp-features-plugin": ^7.29.7 + "@babel/helper-plugin-utils": ^7.29.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 1ade0672ae5bbbf2ec1ea0a8de1b5d804ae414283215620097ab21cf7f05dae8916f5b0548a18c6f080ec17135018f5edd2d38f8fa9ca052af570cab5c712786 + languageName: node + linkType: hard + "@babel/plugin-transform-unicode-sets-regex@npm:^7.28.6": version: 7.28.6 resolution: "@babel/plugin-transform-unicode-sets-regex@npm:7.28.6" @@ -1739,6 +2769,18 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-unicode-sets-regex@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/plugin-transform-unicode-sets-regex@npm:7.29.7" + dependencies: + "@babel/helper-create-regexp-features-plugin": ^7.29.7 + "@babel/helper-plugin-utils": ^7.29.7 + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 680c22e2a63bffbf6798b0c2f599b06beb7ea4d29ee37a56c9192014143d396c479b12783d9c343e107fa491aeeb65b1ca774fd76825ffb306eef7fb5e7b4b2c + languageName: node + linkType: hard + "@babel/preset-env@npm:^7.11.0, @babel/preset-env@npm:^7.12.1, @babel/preset-env@npm:^7.12.11, @babel/preset-env@npm:^7.16.4, @babel/preset-env@npm:^7.20.2, @babel/preset-env@npm:^7.25.3, @babel/preset-env@npm:^7.25.9": version: 7.28.6 resolution: "@babel/preset-env@npm:7.28.6" @@ -1819,6 +2861,87 @@ __metadata: languageName: node linkType: hard +"@babel/preset-env@npm:^7.28.0": + version: 7.29.7 + resolution: "@babel/preset-env@npm:7.29.7" + dependencies: + "@babel/compat-data": ^7.29.7 + "@babel/helper-compilation-targets": ^7.29.7 + "@babel/helper-plugin-utils": ^7.29.7 + "@babel/helper-validator-option": ^7.29.7 + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": ^7.29.7 + "@babel/plugin-bugfix-safari-class-field-initializer-scope": ^7.29.7 + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": ^7.29.7 + "@babel/plugin-bugfix-safari-rest-destructuring-rhs-array": ^7.29.7 + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": ^7.29.7 + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": ^7.29.7 + "@babel/plugin-proposal-private-property-in-object": 7.21.0-placeholder-for-preset-env.2 + "@babel/plugin-syntax-import-assertions": ^7.29.7 + "@babel/plugin-syntax-import-attributes": ^7.29.7 + "@babel/plugin-syntax-unicode-sets-regex": ^7.18.6 + "@babel/plugin-transform-arrow-functions": ^7.29.7 + "@babel/plugin-transform-async-generator-functions": ^7.29.7 + "@babel/plugin-transform-async-to-generator": ^7.29.7 + "@babel/plugin-transform-block-scoped-functions": ^7.29.7 + "@babel/plugin-transform-block-scoping": ^7.29.7 + "@babel/plugin-transform-class-properties": ^7.29.7 + "@babel/plugin-transform-class-static-block": ^7.29.7 + "@babel/plugin-transform-classes": ^7.29.7 + "@babel/plugin-transform-computed-properties": ^7.29.7 + "@babel/plugin-transform-destructuring": ^7.29.7 + "@babel/plugin-transform-dotall-regex": ^7.29.7 + "@babel/plugin-transform-duplicate-keys": ^7.29.7 + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": ^7.29.7 + "@babel/plugin-transform-dynamic-import": ^7.29.7 + "@babel/plugin-transform-explicit-resource-management": ^7.29.7 + "@babel/plugin-transform-exponentiation-operator": ^7.29.7 + "@babel/plugin-transform-export-namespace-from": ^7.29.7 + "@babel/plugin-transform-for-of": ^7.29.7 + "@babel/plugin-transform-function-name": ^7.29.7 + "@babel/plugin-transform-json-strings": ^7.29.7 + "@babel/plugin-transform-literals": ^7.29.7 + "@babel/plugin-transform-logical-assignment-operators": ^7.29.7 + "@babel/plugin-transform-member-expression-literals": ^7.29.7 + "@babel/plugin-transform-modules-amd": ^7.29.7 + "@babel/plugin-transform-modules-commonjs": ^7.29.7 + "@babel/plugin-transform-modules-systemjs": ^7.29.7 + "@babel/plugin-transform-modules-umd": ^7.29.7 + "@babel/plugin-transform-named-capturing-groups-regex": ^7.29.7 + "@babel/plugin-transform-new-target": ^7.29.7 + "@babel/plugin-transform-nullish-coalescing-operator": ^7.29.7 + "@babel/plugin-transform-numeric-separator": ^7.29.7 + "@babel/plugin-transform-object-rest-spread": ^7.29.7 + "@babel/plugin-transform-object-super": ^7.29.7 + "@babel/plugin-transform-optional-catch-binding": ^7.29.7 + "@babel/plugin-transform-optional-chaining": ^7.29.7 + "@babel/plugin-transform-parameters": ^7.29.7 + "@babel/plugin-transform-private-methods": ^7.29.7 + "@babel/plugin-transform-private-property-in-object": ^7.29.7 + "@babel/plugin-transform-property-literals": ^7.29.7 + "@babel/plugin-transform-regenerator": ^7.29.7 + "@babel/plugin-transform-regexp-modifiers": ^7.29.7 + "@babel/plugin-transform-reserved-words": ^7.29.7 + "@babel/plugin-transform-shorthand-properties": ^7.29.7 + "@babel/plugin-transform-spread": ^7.29.7 + "@babel/plugin-transform-sticky-regex": ^7.29.7 + "@babel/plugin-transform-template-literals": ^7.29.7 + "@babel/plugin-transform-typeof-symbol": ^7.29.7 + "@babel/plugin-transform-unicode-escapes": ^7.29.7 + "@babel/plugin-transform-unicode-property-regex": ^7.29.7 + "@babel/plugin-transform-unicode-regex": ^7.29.7 + "@babel/plugin-transform-unicode-sets-regex": ^7.29.7 + "@babel/preset-modules": 0.1.6-no-external-plugins + babel-plugin-polyfill-corejs2: ^0.4.15 + babel-plugin-polyfill-corejs3: ^0.14.0 + babel-plugin-polyfill-regenerator: ^0.6.6 + core-js-compat: ^3.48.0 + semver: ^6.3.1 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 36deae2ea4329e8490fc4507e6747e07c4cff9df1f1397aa99fae16e03a5195f9c7a3494f571ddfb5002bbc0e18832a8cc711831d686896312fa127823f7fedc + languageName: node + linkType: hard + "@babel/preset-flow@npm:^7.12.1, @babel/preset-flow@npm:^7.13.13, @babel/preset-flow@npm:^7.25.9": version: 7.27.1 resolution: "@babel/preset-flow@npm:7.27.1" @@ -1832,6 +2955,19 @@ __metadata: languageName: node linkType: hard +"@babel/preset-flow@npm:^7.28.0": + version: 7.29.7 + resolution: "@babel/preset-flow@npm:7.29.7" + dependencies: + "@babel/helper-plugin-utils": ^7.29.7 + "@babel/helper-validator-option": ^7.29.7 + "@babel/plugin-transform-flow-strip-types": ^7.29.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 562fe8494d7e8e3a894cb9b1213acdc418b42d54f0fe316e97192bb235c8dfe7cde58d351057b9f7896bdc5ff42b605863c00cf6df952d5789cf3d865ab723ba + languageName: node + linkType: hard + "@babel/preset-modules@npm:0.1.6-no-external-plugins": version: 0.1.6-no-external-plugins resolution: "@babel/preset-modules@npm:0.1.6-no-external-plugins" @@ -1861,18 +2997,19 @@ __metadata: languageName: node linkType: hard -"@babel/preset-typescript@npm:7.27.1": - version: 7.27.1 - resolution: "@babel/preset-typescript@npm:7.27.1" +"@babel/preset-react@npm:^7.28.0": + version: 7.29.7 + resolution: "@babel/preset-react@npm:7.29.7" dependencies: - "@babel/helper-plugin-utils": ^7.27.1 - "@babel/helper-validator-option": ^7.27.1 - "@babel/plugin-syntax-jsx": ^7.27.1 - "@babel/plugin-transform-modules-commonjs": ^7.27.1 - "@babel/plugin-transform-typescript": ^7.27.1 + "@babel/helper-plugin-utils": ^7.29.7 + "@babel/helper-validator-option": ^7.29.7 + "@babel/plugin-transform-react-display-name": ^7.29.7 + "@babel/plugin-transform-react-jsx": ^7.29.7 + "@babel/plugin-transform-react-jsx-development": ^7.29.7 + "@babel/plugin-transform-react-pure-annotations": ^7.29.7 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 38020f1b23e88ec4fbffd5737da455d8939244bddfb48a2516aef93fb5947bd9163fb807ce6eff3e43fa5ffe9113aa131305fef0fb5053998410bbfcfe6ce0ec + checksum: ec9d4d418df3825674ef807020b90a93d6bf6b786a723a59e673c3a32e31927b2f8a50071a17a50fff632f41eda8933e4196974130f2a5cdeb1a040fb2b62b76 languageName: node linkType: hard @@ -1891,6 +3028,21 @@ __metadata: languageName: node linkType: hard +"@babel/preset-typescript@npm:^7.27.1, @babel/preset-typescript@npm:^7.28.0": + version: 7.29.7 + resolution: "@babel/preset-typescript@npm:7.29.7" + dependencies: + "@babel/helper-plugin-utils": ^7.29.7 + "@babel/helper-validator-option": ^7.29.7 + "@babel/plugin-syntax-jsx": ^7.29.7 + "@babel/plugin-transform-modules-commonjs": ^7.29.7 + "@babel/plugin-transform-typescript": ^7.29.7 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: f2f58cbbbdb84f6b27e20c7835fbd1e2474e7e6075c97a6609c606139bc782c995f0c5eb5b64f5b613997f8a463f3b4cea10cd1f0d706a66bf1aa41fea666725 + languageName: node + linkType: hard + "@babel/register@npm:^7.13.16": version: 7.28.6 resolution: "@babel/register@npm:7.28.6" @@ -1933,7 +3085,18 @@ __metadata: languageName: node linkType: hard -"@babel/traverse--for-generate-function-map@npm:@babel/traverse@^7.25.3, @babel/traverse@npm:^7.20.0, @babel/traverse@npm:^7.25.3, @babel/traverse@npm:^7.25.9, @babel/traverse@npm:^7.27.1, @babel/traverse@npm:^7.28.4, @babel/traverse@npm:^7.28.5, @babel/traverse@npm:^7.28.6, @babel/traverse@npm:^7.7.2": +"@babel/template@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/template@npm:7.29.7" + dependencies: + "@babel/code-frame": ^7.29.7 + "@babel/parser": ^7.29.7 + "@babel/types": ^7.29.7 + checksum: 521eb6a1fd4735074ca8dac0d70810860a80edf3bf78105851571993cd13701a2041987e7398ccc9376eb6235ea1258bf494ccaccf3b67fa98dbe954154a2e93 + languageName: node + linkType: hard + +"@babel/traverse--for-generate-function-map@npm:@babel/traverse@^7.25.3, @babel/traverse@npm:^7.20.0, @babel/traverse@npm:^7.25.3, @babel/traverse@npm:^7.25.9, @babel/traverse@npm:^7.27.1, @babel/traverse@npm:^7.28.5, @babel/traverse@npm:^7.28.6, @babel/traverse@npm:^7.7.2": version: 7.28.6 resolution: "@babel/traverse@npm:7.28.6" dependencies: @@ -1948,6 +3111,36 @@ __metadata: languageName: node linkType: hard +"@babel/traverse@npm:^7.29.0": + version: 7.29.0 + resolution: "@babel/traverse@npm:7.29.0" + dependencies: + "@babel/code-frame": ^7.29.0 + "@babel/generator": ^7.29.0 + "@babel/helper-globals": ^7.28.0 + "@babel/parser": ^7.29.0 + "@babel/template": ^7.28.6 + "@babel/types": ^7.29.0 + debug: ^4.3.1 + checksum: fbb5085aa525b5d4ecd9fe2f5885d88413fff6ad9c0fac244c37f96069b6d3af9ce825750cd16af1d97d26fa3d354b38dbbdb5f31430e0d99ed89660ab65430e + languageName: node + linkType: hard + +"@babel/traverse@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/traverse@npm:7.29.7" + dependencies: + "@babel/code-frame": ^7.29.7 + "@babel/generator": ^7.29.7 + "@babel/helper-globals": ^7.29.7 + "@babel/parser": ^7.29.7 + "@babel/template": ^7.29.7 + "@babel/types": ^7.29.7 + debug: ^4.3.1 + checksum: 6c4508fd2a308a6a41fbf40bd2590bccfdc3903de51c640a928c49e810220b9e27323a083cda604d44a27449b57265a701b549de01f479611390863734b4fd38 + languageName: node + linkType: hard + "@babel/types@npm:^7.0.0, @babel/types@npm:^7.12.6, @babel/types@npm:^7.20.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.21.3, @babel/types@npm:^7.25.2, @babel/types@npm:^7.27.1, @babel/types@npm:^7.27.3, @babel/types@npm:^7.28.2, @babel/types@npm:^7.28.5, @babel/types@npm:^7.28.6, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4, @babel/types@npm:^7.8.3": version: 7.28.6 resolution: "@babel/types@npm:7.28.6" @@ -1958,6 +3151,26 @@ __metadata: languageName: node linkType: hard +"@babel/types@npm:^7.29.0": + version: 7.29.0 + resolution: "@babel/types@npm:7.29.0" + dependencies: + "@babel/helper-string-parser": ^7.27.1 + "@babel/helper-validator-identifier": ^7.28.5 + checksum: 83f190438e94c22b2574aaeef7501830311ef266eaabfb06523409f64e2fe855e522951607085d71cad286719adef14e1ba37b671f334a7cd25b0f8506a01e0b + languageName: node + linkType: hard + +"@babel/types@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/types@npm:7.29.7" + dependencies: + "@babel/helper-string-parser": ^7.29.7 + "@babel/helper-validator-identifier": ^7.29.7 + checksum: 71c46837d22c5c63a5ed571f3b68b4261ecabfc3d4be7251b336ccbd26bc52752ae68ae6d16d24ea512311b78b6f54efdfb00dde87a81a4dc3d19e4a45f05b20 + languageName: node + linkType: hard + "@bcoe/v8-coverage@npm:^0.2.3": version: 0.2.3 resolution: "@bcoe/v8-coverage@npm:0.2.3" @@ -6015,9 +7228,9 @@ __metadata: linkType: hard "@nevware21/ts-utils@npm:>= 0.10.4 < 2.x, @nevware21/ts-utils@npm:>= 0.11.8 < 2.x, @nevware21/ts-utils@npm:>= 0.12.2 < 2.x, @nevware21/ts-utils@npm:>= 0.9.4 < 2.x": - version: 0.12.5 - resolution: "@nevware21/ts-utils@npm:0.12.5" - checksum: 342d762fe6e546ddc08a7a8521a85c6181f1858b247eb6cce3303639f074b59c166a4b867beceecf81a7ce3a28335a2fc3d841ec4a9743b5ab5565ebf3b193f2 + version: 0.14.0 + resolution: "@nevware21/ts-utils@npm:0.14.0" + checksum: 28b2277dcf1af4888a732ca066c8cb16ea62aa41601abd4e7afb6082abb262bef4a4d453e1457224f32e22cb8342c8282f6f623f2a9924bcfd97fbb5bdfec296 languageName: node linkType: hard @@ -8462,8 +9675,13 @@ __metadata: version: 0.0.0-use.local resolution: "@shopify/react-native-skia@workspace:packages/skia" dependencies: - "@babel/plugin-proposal-nullish-coalescing-operator": 7.18.6 + "@babel/plugin-transform-class-properties": ^7.28.0 "@babel/plugin-transform-explicit-resource-management": ^7.28.0 + "@babel/plugin-transform-nullish-coalescing-operator": ^7.28.0 + "@babel/preset-env": ^7.28.0 + "@babel/preset-flow": ^7.28.0 + "@babel/preset-react": ^7.28.0 + "@babel/preset-typescript": ^7.28.0 "@blazediff/core": ^1.4.0 "@semantic-release/commit-analyzer": ^13.0.0 "@semantic-release/exec": ^7.0.3 @@ -8487,21 +9705,21 @@ __metadata: react: 19.0.0 react-native: 0.83.1 react-native-builder-bob: 0.18.2 - react-native-reanimated: ^4.2.1 - react-native-skia-android: 147.1.0 - react-native-skia-apple-ios: 147.1.0 - react-native-skia-apple-macos: 147.1.0 - react-native-skia-apple-tvos: 147.1.0 - react-native-worklets: ^0.7.0 + react-native-reanimated: 4.3.1 + react-native-skia-android: 150.0.0 + react-native-skia-apple-ios: 150.0.0 + react-native-skia-apple-macos: 150.0.0 + react-native-skia-apple-tvos: 150.0.0 + react-native-worklets: 0.8.3 react-reconciler: 0.31.0 rimraf: 3.0.2 semantic-release: ^24.1.0 semantic-release-yarn: ^3.0.2 - tar: ^7.5.11 + tar: ^7.5.16 ts-jest: 29.4.3 tsx: ^4.21.0 typescript: ^5.2.2 - ws: 8.18.0 + ws: 8.21.0 peerDependencies: react: ">=19.0" react-native: ">=0.78" @@ -8512,7 +9730,6 @@ __metadata: react-native-reanimated: optional: true bin: - install-skia: scripts/install-libs.js setup-skia-web: scripts/setup-canvaskit.js languageName: unknown linkType: soft @@ -9108,6 +10325,48 @@ __metadata: languageName: node linkType: hard +"@turbo/darwin-64@npm:2.9.14": + version: 2.9.14 + resolution: "@turbo/darwin-64@npm:2.9.14" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@turbo/darwin-arm64@npm:2.9.14": + version: 2.9.14 + resolution: "@turbo/darwin-arm64@npm:2.9.14" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@turbo/linux-64@npm:2.9.14": + version: 2.9.14 + resolution: "@turbo/linux-64@npm:2.9.14" + conditions: os=linux & cpu=x64 + languageName: node + linkType: hard + +"@turbo/linux-arm64@npm:2.9.14": + version: 2.9.14 + resolution: "@turbo/linux-arm64@npm:2.9.14" + conditions: os=linux & cpu=arm64 + languageName: node + linkType: hard + +"@turbo/windows-64@npm:2.9.14": + version: 2.9.14 + resolution: "@turbo/windows-64@npm:2.9.14" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"@turbo/windows-arm64@npm:2.9.14": + version: 2.9.14 + resolution: "@turbo/windows-arm64@npm:2.9.14" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + "@tybys/wasm-util@npm:^0.10.0": version: 0.10.1 resolution: "@tybys/wasm-util@npm:0.10.1" @@ -11610,6 +12869,19 @@ __metadata: languageName: node linkType: hard +"babel-plugin-polyfill-corejs2@npm:^0.4.15": + version: 0.4.17 + resolution: "babel-plugin-polyfill-corejs2@npm:0.4.17" + dependencies: + "@babel/compat-data": ^7.28.6 + "@babel/helper-define-polyfill-provider": ^0.6.8 + semver: ^6.3.1 + peerDependencies: + "@babel/core": ^7.4.0 || ^8.0.0-0 <8.0.0 + checksum: 945f80f413706831b665322690c655f3782ca6fd8c1fbcccaf449d976ebe6151677fb9331442c72e85eae9a05d5e6633be4e15f75d3e788762d825d31f2964ce + languageName: node + linkType: hard + "babel-plugin-polyfill-corejs3@npm:^0.13.0": version: 0.13.0 resolution: "babel-plugin-polyfill-corejs3@npm:0.13.0" @@ -11622,6 +12894,18 @@ __metadata: languageName: node linkType: hard +"babel-plugin-polyfill-corejs3@npm:^0.14.0": + version: 0.14.2 + resolution: "babel-plugin-polyfill-corejs3@npm:0.14.2" + dependencies: + "@babel/helper-define-polyfill-provider": ^0.6.8 + core-js-compat: ^3.48.0 + peerDependencies: + "@babel/core": ^7.4.0 || ^8.0.0-0 <8.0.0 + checksum: 4bcaf4da658aaeb7a6534e6b65a6a45539c5f53bec596fefd0b44eebd249e5db8bbf239a421ceaff5933a0a7eee11e45791e4f4e04886cdf47bb1d4b1a8015aa + languageName: node + linkType: hard + "babel-plugin-polyfill-regenerator@npm:^0.6.5": version: 0.6.6 resolution: "babel-plugin-polyfill-regenerator@npm:0.6.6" @@ -11633,6 +12917,17 @@ __metadata: languageName: node linkType: hard +"babel-plugin-polyfill-regenerator@npm:^0.6.6": + version: 0.6.8 + resolution: "babel-plugin-polyfill-regenerator@npm:0.6.8" + dependencies: + "@babel/helper-define-polyfill-provider": ^0.6.8 + peerDependencies: + "@babel/core": ^7.4.0 || ^8.0.0-0 <8.0.0 + checksum: 974464353d6f974e97673385aff616a913c0b76039eab8c5317a2d07c661e080f3dcc213e86f3eae40010172a27ab793cda7a290a8a899716f9a22df9b1d92d2 + languageName: node + linkType: hard + "babel-plugin-syntax-hermes-parser@npm:0.29.1": version: 0.29.1 resolution: "babel-plugin-syntax-hermes-parser@npm:0.29.1" @@ -13509,13 +14804,6 @@ __metadata: languageName: node linkType: hard -"convert-source-map@npm:2.0.0, convert-source-map@npm:^2.0.0": - version: 2.0.0 - resolution: "convert-source-map@npm:2.0.0" - checksum: 63ae9933be5a2b8d4509daca5124e20c14d023c820258e484e32dc324d34c2754e71297c94a05784064ad27615037ef677e3f0c00469fb55f409d2bb21261035 - languageName: node - linkType: hard - "convert-source-map@npm:^1.4.0, convert-source-map@npm:^1.6.0, convert-source-map@npm:^1.7.0": version: 1.9.0 resolution: "convert-source-map@npm:1.9.0" @@ -13523,6 +14811,13 @@ __metadata: languageName: node linkType: hard +"convert-source-map@npm:^2.0.0": + version: 2.0.0 + resolution: "convert-source-map@npm:2.0.0" + checksum: 63ae9933be5a2b8d4509daca5124e20c14d023c820258e484e32dc324d34c2754e71297c94a05784064ad27615037ef677e3f0c00469fb55f409d2bb21261035 + languageName: node + linkType: hard + "cookie-signature@npm:~1.0.6": version: 1.0.7 resolution: "cookie-signature@npm:1.0.7" @@ -13562,6 +14857,15 @@ __metadata: languageName: node linkType: hard +"core-js-compat@npm:^3.48.0": + version: 3.49.0 + resolution: "core-js-compat@npm:3.49.0" + dependencies: + browserslist: ^4.28.1 + checksum: 21afa75a64b30810f4cc61e90758346e8df6bd20dd8da5afe08fc041b5fb766cf7c41c9cbc63f8fb96bef4e4a2a90eb6f2d7bbd20ac53b8ff23a58bc87e40231 + languageName: node + linkType: hard + "core-js-pure@npm:^3.23.3, core-js-pure@npm:^3.43.0": version: 3.48.0 resolution: "core-js-pure@npm:3.48.0" @@ -14819,8 +16123,8 @@ __metadata: react-dom: 19.0.0 react-native: 0.83.1 react-native-gesture-handler: ^2.24.0 - react-native-reanimated: ^4.2.1 - react-native-worklets: ^0.7.0 + react-native-reanimated: 4.3.1 + react-native-worklets: 0.8.3 ts-morph: ^25.0.1 ts-node: ^10.9.2 twoslash-cli: 1.3.24 @@ -16734,7 +18038,7 @@ __metadata: version: 0.0.0-use.local resolution: "example@workspace:apps/example" dependencies: - "@babel/core": ^7.25.2 + "@babel/core": ^7.29.6 "@babel/plugin-proposal-explicit-resource-management": ^7.27.4 "@babel/preset-env": ^7.25.3 "@babel/runtime": ^7.25.0 @@ -16769,14 +18073,14 @@ __metadata: react-native: 0.83.1 react-native-gesture-handler: ^2.24.0 react-native-macos: ^0.81.1 - react-native-reanimated: ^4.2.1 + react-native-reanimated: 4.3.1 react-native-safe-area-context: ^5.2.0 react-native-screens: ^4.10.0 react-native-svg: "patch:react-native-svg@npm%3A15.14.0#../../.yarn/patches/react-native-svg-npm-15.14.0-macos-uiimage-fix.patch" react-native-test-app: 4.4.7 react-native-web: ^0.21.2 react-native-windows: ^0.75.0 - react-native-worklets: ^0.7.0 + react-native-worklets: 0.8.3 react-test-renderer: 19.0.0 typescript: ^5.2.2 languageName: unknown @@ -17136,9 +18440,9 @@ __metadata: linkType: hard "fast-uri@npm:^3.0.1": - version: 3.1.0 - resolution: "fast-uri@npm:3.1.0" - checksum: daab0efd3548cc53d0db38ecc764d125773f8bd70c34552ff21abdc6530f26fa4cb1771f944222ca5e61a0a1a85d01a104848ff88c61736de445d97bd616ea7e + version: 3.1.2 + resolution: "fast-uri@npm:3.1.2" + checksum: 73a6e1b04e6fcf7a09ed93316e72d643ef177d26969973784690708612141f2c2f74657120bab75bf5bbc26bfd535a32c90a8c3bc50aca50584cf01f98815afe languageName: node linkType: hard @@ -17570,12 +18874,12 @@ __metadata: linkType: hard "follow-redirects@npm:^1.0.0": - version: 1.15.11 - resolution: "follow-redirects@npm:1.15.11" + version: 1.16.0 + resolution: "follow-redirects@npm:1.16.0" peerDependenciesMeta: debug: optional: true - checksum: 20bf55e9504f59e6cc3743ba27edb2ebf41edea1baab34799408f2c050f73f0c612728db21c691276296d2795ea8a812dc532a98e8793619fcab91abe06d017f + checksum: e90dce4607b1f6b8b9883287f912585573c19088209ad82341d550a795b4ba514522b73b1b340cf618279df27975cd46504d09149be60291ba6767384c1fd8f8 languageName: node linkType: hard @@ -17637,15 +18941,15 @@ __metadata: linkType: hard "form-data@npm:^3.0.0": - version: 3.0.4 - resolution: "form-data@npm:3.0.4" + version: 3.0.5 + resolution: "form-data@npm:3.0.5" dependencies: asynckit: ^0.4.0 combined-stream: ^1.0.8 es-set-tostringtag: ^2.1.0 - hasown: ^2.0.2 + hasown: ^2.0.4 mime-types: ^2.1.35 - checksum: 989005f575b9a14a30144df1745ef60c64cf901e648ae198bf63e5caeaf8dacf595a85dfd56f90a845eceb14fe1bea58b3845e8171337a4cf72781fa19867efc + checksum: 03b753d26cd7da91bafbdf40078cb9750b91f204377a228c94b538fcc35ff7df476f1f8c0d2d8acfb7e4cbfb45894ac945ff2ba40908d842cf0d7123d583cee4 languageName: node linkType: hard @@ -18536,6 +19840,15 @@ __metadata: languageName: node linkType: hard +"hasown@npm:^2.0.4": + version: 2.0.4 + resolution: "hasown@npm:2.0.4" + dependencies: + function-bind: ^1.1.2 + checksum: 4bd8f916b629e06324853593ffbdd45e200022952a85ad0c967f3bd4c2e4c7e1f9a9766fbe6186f60bd394e0afc73e719730caa1da15cd9bd832b7cdf53fd26c + languageName: node + linkType: hard + "hast-util-from-parse5@npm:^8.0.0": version: 8.0.3 resolution: "hast-util-from-parse5@npm:8.0.3" @@ -19579,9 +20892,9 @@ __metadata: linkType: hard "ip-address@npm:^10.0.1": - version: 10.1.0 - resolution: "ip-address@npm:10.1.0" - checksum: 76b1abcdf52a32e2e05ca1f202f3a8ab8547e5651a9233781b330271bd7f1a741067748d71c4cbb9d9906d9f1fa69e7ddc8b4a11130db4534fdab0e908c84e0d + version: 10.2.0 + resolution: "ip-address@npm:10.2.0" + checksum: 3ffba04dc4cdaf81ed2ed6edc47eee1494bb97550ef73f1918ca28405d175c03efa416b8337e868123b08c2cc677e3a07c5ce03eda3b1aeb2741c149bd37ddf9 languageName: node linkType: hard @@ -22473,12 +23786,12 @@ __metadata: linkType: hard "launch-editor@npm:^2.6.0, launch-editor@npm:^2.6.1": - version: 2.12.0 - resolution: "launch-editor@npm:2.12.0" + version: 2.14.1 + resolution: "launch-editor@npm:2.14.1" dependencies: picocolors: ^1.1.1 - shell-quote: ^1.8.3 - checksum: b1aa1b92ef4e720d1edd7f80affb90b2fa1cc2c41641cf80158940698c18a4b6a67e2a7cb060547712e858f0ec1a7c8c39f605e0eb299f516a6184f4e680ffc8 + shell-quote: ^1.8.4 + checksum: 232ea8a80146e7fec6c8ece7ebb600d58a82e9938f36111194980188d276935f424754b18e37d5b098f596d30580def723b231e093c27c3bcd703418278afc4c languageName: node linkType: hard @@ -29084,11 +30397,11 @@ __metadata: linkType: hard "qs@npm:^6.12.3, qs@npm:~6.14.0": - version: 6.14.1 - resolution: "qs@npm:6.14.1" + version: 6.14.2 + resolution: "qs@npm:6.14.2" dependencies: side-channel: ^1.1.0 - checksum: 7fffab0344fd75bfb6b8c94b8ba17f3d3e823d25b615900f68b473c3a078e497de8eaa08f709eaaa170eedfcee50638a7159b98abef7d8c89c2ede79291522f2 + checksum: e613d0b8d02cec33c20d1a6015ec2a5db614bf3dd2ffd9bde08bc34a76419213f291c91fb00519a3d8a74e4727f565b350f8394f9d381bc64e6da663d9e031d4 languageName: node linkType: hard @@ -29471,13 +30784,13 @@ __metadata: languageName: node linkType: hard -"react-native-is-edge-to-edge@npm:1.2.1": - version: 1.2.1 - resolution: "react-native-is-edge-to-edge@npm:1.2.1" +"react-native-is-edge-to-edge@npm:^1.3.1": + version: 1.3.1 + resolution: "react-native-is-edge-to-edge@npm:1.3.1" peerDependencies: react: "*" react-native: "*" - checksum: 8fb6d8ab7b953c7d7cec8c987cef24f1c5348a293a85cb49c7c53b54ef110c0ca746736ae730e297603c8c76020df912e93915fb17518c4f2f91143757177aba + checksum: dc82d54e0bf8f89208a538bb0d14e4891af6efae27ed5b7b21be683a72c38c5219ab9be1ea9bd40aa1c905d481174e649d0b71aeceaa9946e6c707f251568282 languageName: node linkType: hard @@ -29533,17 +30846,17 @@ __metadata: languageName: node linkType: hard -"react-native-reanimated@npm:^4.2.1": - version: 4.2.2 - resolution: "react-native-reanimated@npm:4.2.2" +"react-native-reanimated@npm:4.3.1": + version: 4.3.1 + resolution: "react-native-reanimated@npm:4.3.1" dependencies: - react-native-is-edge-to-edge: 1.2.1 - semver: 7.7.3 + react-native-is-edge-to-edge: ^1.3.1 + semver: ^7.7.3 peerDependencies: react: "*" - react-native: "*" - react-native-worklets: ">=0.7.0" - checksum: 2497901472996aece1e350ffa02233ad0f9f7a16e389a201246474fae872f3ec9834a0b7c3801241d5844b9f7dbf0309d868728b3b35555de5d1836b3f527644 + react-native: 0.81 - 0.85 + react-native-worklets: 0.8.x + checksum: 2649672f72dbd52aa612e5c9bcf8ab2dc256d614ed3cd21cd44ba97b26eefd30e0be629f5aec6ba209ccc07aad46831866b69f6bf51fb9708d49431bd608507a languageName: node linkType: hard @@ -29570,31 +30883,31 @@ __metadata: languageName: node linkType: hard -"react-native-skia-android@npm:147.1.0": - version: 147.1.0 - resolution: "react-native-skia-android@npm:147.1.0" - checksum: 1aca3e311db852a7cc3adce31169cf8dda3e68b3812453b86296d0c27e577438ac4344cd26d25c09329faff470bcdd04c16da547904164677e435ac770c00bc3 +"react-native-skia-android@npm:150.0.0": + version: 150.0.0 + resolution: "react-native-skia-android@npm:150.0.0" + checksum: e33eecf360925bc0402c1d6b0db5845f2a38d19244f9c128b7c208abb7dc2c07158cdfcd0c998c31271075ce8ad8a3b1bed329cd010a9df9a3ae58a752e8fded languageName: node linkType: hard -"react-native-skia-apple-ios@npm:147.1.0": - version: 147.1.0 - resolution: "react-native-skia-apple-ios@npm:147.1.0" - checksum: 704d86ebb038b472cc294267893bee1cc733695515dbe87d43edddbc89ccbef9f6a49d0dfe374849230637af304746f341ddce1c5c85e95f3d6d6b6d3520ce6e +"react-native-skia-apple-ios@npm:150.0.0": + version: 150.0.0 + resolution: "react-native-skia-apple-ios@npm:150.0.0" + checksum: d96bfdf9b5bd8eb038b29b4949f591acd9064b00e8d4229f8932b8a9e7bbcf206c6e57a32f1ad0ba6b58ff033388a6dbe059f73d9eb91686afb0f8a3742e5392 languageName: node linkType: hard -"react-native-skia-apple-macos@npm:147.1.0": - version: 147.1.0 - resolution: "react-native-skia-apple-macos@npm:147.1.0" - checksum: 4a6f3e1bcae8950de0cd94f9b5958f91c61e2ac41c178d66df2a933451be16bd4aa06191d70317dd07a4e095b775341532d2f7aefa424a8b4f01bf6fed88a503 +"react-native-skia-apple-macos@npm:150.0.0": + version: 150.0.0 + resolution: "react-native-skia-apple-macos@npm:150.0.0" + checksum: 0963df1f238a03a6390e9c11eb3cc662a81db5ad2daee77eb7555f35f4ccf06ac32e94c37d42704a33bf8b09d1ebb31193361a5183662df29b9e28f10aa6937d languageName: node linkType: hard -"react-native-skia-apple-tvos@npm:147.1.0": - version: 147.1.0 - resolution: "react-native-skia-apple-tvos@npm:147.1.0" - checksum: 5af05fe914e641f2488ac719aa1763ae2850efaea785b12febdf67ee7594c7b340d2d2cd36870ceb056a249d72b40d34500c5da11dba9fdef26a5652c4ff04f8 +"react-native-skia-apple-tvos@npm:150.0.0": + version: 150.0.0 + resolution: "react-native-skia-apple-tvos@npm:150.0.0" + checksum: c96428599c75febf5f0ea9a9282d28dd100be1ddc5953cbd6afc845379ef24de2fd3e8f3643a16c1d864b395ce225f01e35621e613e88edcb5b0b9ba78011a3e languageName: node linkType: hard @@ -29605,7 +30918,7 @@ __metadata: eas-cli: 18.4.0 playwright: 1.59.0 react-native-test-app: ^4.2.0 - turbo: ^2.1.1 + turbo: ^2.9.14 languageName: unknown linkType: soft @@ -29805,26 +31118,27 @@ __metadata: languageName: node linkType: hard -"react-native-worklets@npm:^0.7.0": - version: 0.7.4 - resolution: "react-native-worklets@npm:0.7.4" - dependencies: - "@babel/plugin-transform-arrow-functions": 7.27.1 - "@babel/plugin-transform-class-properties": 7.27.1 - "@babel/plugin-transform-classes": 7.28.4 - "@babel/plugin-transform-nullish-coalescing-operator": 7.27.1 - "@babel/plugin-transform-optional-chaining": 7.27.1 - "@babel/plugin-transform-shorthand-properties": 7.27.1 - "@babel/plugin-transform-template-literals": 7.27.1 - "@babel/plugin-transform-unicode-regex": 7.27.1 - "@babel/preset-typescript": 7.27.1 - convert-source-map: 2.0.0 - semver: 7.7.3 +"react-native-worklets@npm:0.8.3": + version: 0.8.3 + resolution: "react-native-worklets@npm:0.8.3" + dependencies: + "@babel/plugin-transform-arrow-functions": ^7.27.1 + "@babel/plugin-transform-class-properties": ^7.27.1 + "@babel/plugin-transform-classes": ^7.28.4 + "@babel/plugin-transform-nullish-coalescing-operator": ^7.27.1 + "@babel/plugin-transform-optional-chaining": ^7.27.1 + "@babel/plugin-transform-shorthand-properties": ^7.27.1 + "@babel/plugin-transform-template-literals": ^7.27.1 + "@babel/plugin-transform-unicode-regex": ^7.27.1 + "@babel/preset-typescript": ^7.27.1 + convert-source-map: ^2.0.0 + semver: ^7.7.3 peerDependencies: "@babel/core": "*" + "@react-native/metro-config": "*" react: "*" - react-native: "*" - checksum: eb92247e208c6ee641dd639319628213aa2f22249b2fb09d5b9b0ea8a13e7bbb906eabc9dfebfe00404eec5d3e233378cc4991b9a43565338a4f083a68b7bd5f + react-native: 0.81 - 0.85 + checksum: a44e50fc0ec765612df3f6b6104444d28e94634f3642b689df2638c5c9187ddadd7069953bb9d9a68ca253422e6630db36ffcfee80eea5f71f73cc99539307fd languageName: node linkType: hard @@ -30669,7 +31983,7 @@ __metadata: version: 0.0.0-use.local resolution: "remotion-skia@workspace:apps/remotion" dependencies: - "@babel/core": ^7.20.2 + "@babel/core": ^7.29.6 "@babel/plugin-transform-flow-strip-types": ^7.25.9 "@babel/preset-env": ^7.20.2 "@babel/preset-flow": ^7.25.9 @@ -31532,15 +32846,6 @@ __metadata: languageName: node linkType: hard -"semver@npm:7.7.3, semver@npm:^7.1.1, semver@npm:^7.1.2, semver@npm:^7.1.3, semver@npm:^7.3.2, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.3.8, semver@npm:^7.5.2, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.0, semver@npm:^7.7.1, semver@npm:^7.7.2, semver@npm:^7.7.3": - version: 7.7.3 - resolution: "semver@npm:7.7.3" - bin: - semver: bin/semver.js - checksum: f013a3ee4607857bcd3503b6ac1d80165f7f8ea94f5d55e2d3e33df82fce487aa3313b987abf9b39e0793c83c9fc67b76c36c067625141a9f6f704ae0ea18db2 - languageName: node - linkType: hard - "semver@npm:^5.5.0, semver@npm:^5.6.0": version: 5.7.2 resolution: "semver@npm:5.7.2" @@ -31559,6 +32864,15 @@ __metadata: languageName: node linkType: hard +"semver@npm:^7.1.1, semver@npm:^7.1.2, semver@npm:^7.1.3, semver@npm:^7.3.2, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.3.8, semver@npm:^7.5.2, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.0, semver@npm:^7.7.1, semver@npm:^7.7.2, semver@npm:^7.7.3": + version: 7.7.3 + resolution: "semver@npm:7.7.3" + bin: + semver: bin/semver.js + checksum: f013a3ee4607857bcd3503b6ac1d80165f7f8ea94f5d55e2d3e33df82fce487aa3313b987abf9b39e0793c83c9fc67b76c36c067625141a9f6f704ae0ea18db2 + languageName: node + linkType: hard + "semver@npm:^7.6.2": version: 7.7.4 resolution: "semver@npm:7.7.4" @@ -31880,10 +33194,10 @@ __metadata: languageName: node linkType: hard -"shell-quote@npm:^1.6.1, shell-quote@npm:^1.7.3, shell-quote@npm:^1.8.3": - version: 1.8.3 - resolution: "shell-quote@npm:1.8.3" - checksum: 550dd84e677f8915eb013d43689c80bb114860649ec5298eb978f40b8f3d4bc4ccb072b82c094eb3548dc587144bb3965a8676f0d685c1cf4c40b5dc27166242 +"shell-quote@npm:^1.6.1, shell-quote@npm:^1.7.3, shell-quote@npm:^1.8.4": + version: 1.8.4 + resolution: "shell-quote@npm:1.8.4" + checksum: 082dc836baa8ade01144ee3068af487ea45ba570ea6ab13a5eddc11ab16a976b8857b51ef2caf7dc9a1e173ff0aea685b8f78b4f6f5a0a1ef24c7b17c51350e2 languageName: node linkType: hard @@ -33318,16 +34632,16 @@ __metadata: languageName: node linkType: hard -"tar@npm:^7.5.11": - version: 7.5.11 - resolution: "tar@npm:7.5.11" +"tar@npm:^7.5.16": + version: 7.5.16 + resolution: "tar@npm:7.5.16" dependencies: "@isaacs/fs-minipass": ^4.0.0 chownr: ^3.0.0 minipass: ^7.1.2 minizlib: ^3.1.0 yallist: ^5.0.0 - checksum: 7f6785a85dd571b88985e493ec86f692962cbfa7b4017961fddfd2241e0ff3bcd89ed347f4c02b5433aa22b30cca5566e8711543df054fda8fd12425f505378f + checksum: 9b7f886f5ce8681a7430f80b9b377bfa498e6feb957b9afe6507db08e59d309f8546b7f76a0c2e47bdb54da4602575a5c7519e287fe94de8302e635032fc94f1 languageName: node linkType: hard @@ -33982,74 +35296,32 @@ __metadata: languageName: node linkType: hard -"turbo-darwin-64@npm:2.8.0": - version: 2.8.0 - resolution: "turbo-darwin-64@npm:2.8.0" - conditions: os=darwin & cpu=x64 - languageName: node - linkType: hard - -"turbo-darwin-arm64@npm:2.8.0": - version: 2.8.0 - resolution: "turbo-darwin-arm64@npm:2.8.0" - conditions: os=darwin & cpu=arm64 - languageName: node - linkType: hard - -"turbo-linux-64@npm:2.8.0": - version: 2.8.0 - resolution: "turbo-linux-64@npm:2.8.0" - conditions: os=linux & cpu=x64 - languageName: node - linkType: hard - -"turbo-linux-arm64@npm:2.8.0": - version: 2.8.0 - resolution: "turbo-linux-arm64@npm:2.8.0" - conditions: os=linux & cpu=arm64 - languageName: node - linkType: hard - -"turbo-windows-64@npm:2.8.0": - version: 2.8.0 - resolution: "turbo-windows-64@npm:2.8.0" - conditions: os=win32 & cpu=x64 - languageName: node - linkType: hard - -"turbo-windows-arm64@npm:2.8.0": - version: 2.8.0 - resolution: "turbo-windows-arm64@npm:2.8.0" - conditions: os=win32 & cpu=arm64 - languageName: node - linkType: hard - -"turbo@npm:^2.1.1": - version: 2.8.0 - resolution: "turbo@npm:2.8.0" - dependencies: - turbo-darwin-64: 2.8.0 - turbo-darwin-arm64: 2.8.0 - turbo-linux-64: 2.8.0 - turbo-linux-arm64: 2.8.0 - turbo-windows-64: 2.8.0 - turbo-windows-arm64: 2.8.0 +"turbo@npm:^2.9.14": + version: 2.9.14 + resolution: "turbo@npm:2.9.14" + dependencies: + "@turbo/darwin-64": 2.9.14 + "@turbo/darwin-arm64": 2.9.14 + "@turbo/linux-64": 2.9.14 + "@turbo/linux-arm64": 2.9.14 + "@turbo/windows-64": 2.9.14 + "@turbo/windows-arm64": 2.9.14 dependenciesMeta: - turbo-darwin-64: + "@turbo/darwin-64": optional: true - turbo-darwin-arm64: + "@turbo/darwin-arm64": optional: true - turbo-linux-64: + "@turbo/linux-64": optional: true - turbo-linux-arm64: + "@turbo/linux-arm64": optional: true - turbo-windows-64: + "@turbo/windows-64": optional: true - turbo-windows-arm64: + "@turbo/windows-arm64": optional: true bin: turbo: bin/turbo - checksum: 0880b6a4e769d9fa489775c6008a02be4effcee5ff045775049fa3d1dc27aa4aa6811e084a3d89a3129c0ad9f6b001849f3fd5fcaa277d93c7d677c4c8cd2263 + checksum: 8cc51fa1b82e6b3603c164a38aa1e99a6d1f27b1bedc8a47190fb56d1a5e69b5bae6c38df95b7fc99ca139ac73e3f6a0e1df150cbd629476a7128269307a2099 languageName: node linkType: hard @@ -34955,11 +36227,11 @@ __metadata: linkType: hard "uuid@npm:^11.0.0": - version: 11.1.0 - resolution: "uuid@npm:11.1.0" + version: 11.1.1 + resolution: "uuid@npm:11.1.1" bin: uuid: dist/esm/bin/uuid - checksum: 840f19758543c4631e58a29439e51b5b669d5f34b4dd2700b6a1d15c5708c7a6e0c3e2c8c4a2eae761a3a7caa7e9884d00c86c02622ba91137bd3deade6b4b4a + checksum: 1c1ea6ffc9fd02a343d77151734af65644ebf5bac733b59bdab85859cf793cf03ac8d60909fa19dcd2272e154b20734c464b667afd8bdac02b4da5e206079469 languageName: node linkType: hard @@ -35230,9 +36502,9 @@ __metadata: path-browserify: ^1.0.1 react: 19.0.0 react-dom: 19.0.0 - react-native-reanimated: ^4.2.1 + react-native-reanimated: 4.3.1 react-native-web: ^0.21.1 - react-native-worklets: ^0.7.0 + react-native-worklets: 0.8.3 react-scripts: 5.0.1 typescript: ^5.2.2 web-vitals: ^2.1.0 @@ -35392,8 +36664,8 @@ __metadata: linkType: hard "webpack-dev-server@npm:^5.2.2": - version: 5.2.3 - resolution: "webpack-dev-server@npm:5.2.3" + version: 5.2.4 + resolution: "webpack-dev-server@npm:5.2.4" dependencies: "@types/bonjour": ^3.5.13 "@types/connect-history-api-fallback": ^1.5.4 @@ -35432,7 +36704,7 @@ __metadata: optional: true bin: webpack-dev-server: bin/webpack-dev-server.js - checksum: 2b7f2096529945f578e86966d9a0994b0a87b82e3bb3a7cd1ccea2fb81aa95724473a88c9b2033ae3d2f799d13330342948ea77638029d10c8c0c746aac545f7 + checksum: 4e1b13dc170b9964e579e5703dd56e97c2fbb6b101586cfda56f58f1ea0327ea9fcb9e4f36ff6264ee02ab69298697df87bd9617716d9c8379d2a6dc22bb103f languageName: node linkType: hard @@ -36140,9 +37412,9 @@ __metadata: languageName: node linkType: hard -"ws@npm:8.18.0": - version: 8.18.0 - resolution: "ws@npm:8.18.0" +"ws@npm:8.21.0": + version: 8.21.0 + resolution: "ws@npm:8.21.0" peerDependencies: bufferutil: ^4.0.1 utf-8-validate: ">=5.0.2" @@ -36151,7 +37423,7 @@ __metadata: optional: true utf-8-validate: optional: true - checksum: 91d4d35bc99ff6df483bdf029b9ea4bfd7af1f16fc91231a96777a63d263e1eabf486e13a2353970efc534f9faa43bdbf9ee76525af22f4752cbc5ebda333975 + checksum: 83ff89ae011bc5c3c5605a45a0d50e12589143c7500ca4de83a8d43b3cd26e71f422cb3206fd1a9e6d541d666eeb66255c30d095d62d413b3c7afe5d2c5cb928 languageName: node linkType: hard