From 6465f4d8aa2ffc36745802ad07422f03c64db5a0 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Thu, 8 Oct 2026 09:51:55 +0000 Subject: [PATCH 1/3] =?UTF-8?q?chore(=F0=9F=90=99):=20probe=20Metal=20on?= =?UTF-8?q?=20a=20GitHub-hosted=20macOS=20runner?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Experiment for moving the iOS e2e job off the self-hosted Mac: a workflow on macos-latest that creates the default Metal device, runs a compute kernel and a render pass, and checks the results, on the host and inside a booted iOS simulator. --- .github/runner/metal-probe.swift | 132 ++++++++++++++++++++++++++++++ .github/workflows/metal-probe.yml | 49 +++++++++++ 2 files changed, 181 insertions(+) create mode 100644 .github/runner/metal-probe.swift create mode 100644 .github/workflows/metal-probe.yml diff --git a/.github/runner/metal-probe.swift b/.github/runner/metal-probe.swift new file mode 100644 index 000000000..ce8241567 --- /dev/null +++ b/.github/runner/metal-probe.swift @@ -0,0 +1,132 @@ +// Probe whether Metal is usable where this runs: on a GitHub-hosted macOS +// runner (macOS VM) and inside the iOS simulator on that runner. It creates +// the default device, runs a compute kernel and a render pass, and checks the +// results on the CPU. Exit code 0 means Metal works end to end. +import Foundation +import Metal + +func fail(_ message: String) -> Never { + print("FAIL: \(message)") + exit(1) +} + +guard let device = MTLCreateSystemDefaultDevice() else { + fail("MTLCreateSystemDefaultDevice() returned nil: no Metal device") +} + +print("device: \(device.name)") +#if os(macOS) +print("all devices: \(MTLCopyAllDevices().map { $0.name })") +print("lowPower: \(device.isLowPower) headless: \(device.isHeadless) removable: \(device.isRemovable)") +#endif +print("families: apple7=\(device.supportsFamily(.apple7)) apple8=\(device.supportsFamily(.apple8)) metal3=\(device.supportsFamily(.metal3)) common3=\(device.supportsFamily(.common3))") +print("maxThreadsPerThreadgroup: \(device.maxThreadsPerThreadgroup)") +print("maxBufferLength: \(device.maxBufferLength)") +print("argumentBuffersSupport tier: \(device.argumentBuffersSupport.rawValue)") +print("unifiedMemory: \(device.hasUnifiedMemory)") + +let source = """ +#include +using namespace metal; + +kernel void square(device const float* in [[buffer(0)]], + device float* out [[buffer(1)]], + uint id [[thread_position_in_grid]]) { + out[id] = in[id] * in[id]; +} + +struct VSOut { + float4 position [[position]]; +}; + +vertex VSOut vs(uint vid [[vertex_id]]) { + // One triangle that covers the whole clip space. + float2 p[3] = { float2(-1, -1), float2(3, -1), float2(-1, 3) }; + VSOut o; + o.position = float4(p[vid], 0, 1); + return o; +} + +fragment float4 fs() { + return float4(0, 1, 0, 1); +} +""" + +do { + let library = try device.makeLibrary(source: source, options: nil) + guard let queue = device.makeCommandQueue() else { fail("no command queue") } + + // Compute + guard let squareFn = library.makeFunction(name: "square") else { fail("no kernel") } + let pipeline = try device.makeComputePipelineState(function: squareFn) + let count = 4096 + var input = (0..> $GITHUB_ENV + xcrun simctl boot "$UDID" + xcrun simctl bootstatus "$UDID" -b + + - name: Metal in the iOS simulator + run: | + SDK=$(xcrun -sdk iphonesimulator --show-sdk-path) + SDK_VERSION=$(xcrun -sdk iphonesimulator --show-sdk-version) + xcrun -sdk iphonesimulator swiftc \ + -target "arm64-apple-ios${SDK_VERSION}-simulator" -sdk "$SDK" \ + .github/runner/metal-probe.swift -o "$RUNNER_TEMP/metal-probe-sim" + xcrun simctl spawn "$UDID" "$RUNNER_TEMP/metal-probe-sim" From 8aaf91b600c07795058a94c3310a78c66efc4a96 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Thu, 8 Oct 2026 09:53:07 +0000 Subject: [PATCH 2/3] =?UTF-8?q?chore(=F0=9F=90=99):=20pick=20a=20valid=20i?= =?UTF-8?q?Phone=20device=20type=20for=20the=20Metal=20probe=20simulator?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous selection landed on an iPod touch, which the iOS 26.5 runtime refuses. Use the same runtime and device type selection as the e2e job in ci.yml. --- .github/workflows/metal-probe.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/metal-probe.yml b/.github/workflows/metal-probe.yml index 12947c928..86b668cd7 100644 --- a/.github/workflows/metal-probe.yml +++ b/.github/workflows/metal-probe.yml @@ -29,10 +29,14 @@ jobs: - name: Boot an iOS simulator run: | + # Same selection as the e2e job in ci.yml: newest iOS runtime and the + # newest "iPhone N Pro" device type. RUNTIME=$(xcrun simctl list runtimes available --json | jq -r ' - .runtimes | map(select(.platform == "iOS")) | sort_by(.version) | last | .identifier') + .runtimes | map(select(.platform == "iOS")) + | sort_by(.version | split(".") | map(tonumber)) | last | .identifier') DEVICE_TYPE=$(xcrun simctl list devicetypes --json | jq -r ' - .devicetypes | map(select(.productFamily == "iPhone")) | last | .identifier') + .devicetypes | map(select(.name | test("^iPhone [0-9]+ Pro$"))) + | sort_by(.name | capture("^iPhone (?[0-9]+) Pro$").n | tonumber) | last | .identifier') echo "runtime=$RUNTIME device=$DEVICE_TYPE" UDID=$(xcrun simctl create metal-probe "$DEVICE_TYPE" "$RUNTIME") echo "UDID=$UDID" >> $GITHUB_ENV From fb5ff431403baa46a1b5db8491f91d563ffb0303 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Thu, 8 Oct 2026 09:59:35 +0000 Subject: [PATCH 3/3] =?UTF-8?q?chore(=F0=9F=90=99):=20run=20the=20iOS=20e2?= =?UTF-8?q?e=20job=20on=20a=20GitHub-hosted=20Mac=20as=20an=20experiment?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both Metal probes passed on macos-latest (host and iOS simulator), so try the actual e2e job there: same steps as the self-hosted build job, only runs-on differs. --- .github/workflows/metal-probe.yml | 78 +++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/.github/workflows/metal-probe.yml b/.github/workflows/metal-probe.yml index 86b668cd7..dedafe9f5 100644 --- a/.github/workflows/metal-probe.yml +++ b/.github/workflows/metal-probe.yml @@ -51,3 +51,81 @@ jobs: -target "arm64-apple-ios${SDK_VERSION}-simulator" -sdk "$SDK" \ .github/runner/metal-probe.swift -o "$RUNNER_TEMP/metal-probe-sim" xcrun simctl spawn "$UDID" "$RUNNER_TEMP/metal-probe-sim" + + # Second half of the experiment: the iOS e2e job from ci.yml, unchanged + # except for runs-on, on a fresh GitHub-hosted Mac. Lint, docs and the + # release dry run are left out: they are not what the self-hosted machine + # was assumed to be needed for. + e2e-hosted: + runs-on: macos-latest + timeout-minutes: 60 + env: + METRO_PORT: 8083 + E2E_PORT: 4244 + IOS_SIMULATOR: webgpu-ci + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup + uses: ./.github/actions/setup + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + + - name: Build packages + run: yarn turbo run build --filter='./packages/*' + + - name: Install CocoaPods + working-directory: apps/example/ios + run: pod install + + - name: Start Package Manager + working-directory: apps/example + run: CI=true yarn start --port $METRO_PORT --reset-cache & + + - name: Build example for iOS + working-directory: apps/example/ios + run: | + which xcpretty >/dev/null || sudo gem install xcpretty + set -o pipefail && xcodebuild \ + -workspace example.xcworkspace \ + -scheme Example \ + -configuration Debug \ + -sdk iphonesimulator \ + -destination "generic/platform=iOS Simulator" \ + -derivedDataPath build \ + COMPILER_INDEX_STORE_ENABLE=NO \ + DISABLE_MANUAL_TARGET_ORDER_BUILD_WARNING=YES \ + RCT_METRO_PORT=$METRO_PORT \ + -disableAutomaticPackageResolution \ + build | xcpretty + + - name: Prepare iOS simulator + run: | + RUNTIME=$(xcrun simctl list runtimes available --json | jq -r ' + .runtimes | map(select(.platform == "iOS")) + | sort_by(.version | split(".") | map(tonumber)) | last | .identifier') + DEVICE_TYPE=$(xcrun simctl list devicetypes --json | jq -r ' + .devicetypes | map(select(.name | test("^iPhone [0-9]+ Pro$"))) + | sort_by(.name | capture("^iPhone (?[0-9]+) Pro$").n | tonumber) | last | .identifier') + echo "Creating $IOS_SIMULATOR ($DEVICE_TYPE, $RUNTIME)" + UDID=$(xcrun simctl create "$IOS_SIMULATOR" "$DEVICE_TYPE" "$RUNTIME") + echo "IOS_SIMULATOR_UDID=$UDID" >> $GITHUB_ENV + xcrun simctl boot "$UDID" + xcrun simctl bootstatus "$UDID" -b + + - name: Install and launch app on Simulator + run: | + xcrun simctl install "$IOS_SIMULATOR_UDID" apps/example/ios/build/Build/Products/Debug-iphonesimulator/ReactTestApp.app + xcrun simctl launch "$IOS_SIMULATOR_UDID" com.microsoft.ReactTestApp + + - name: Run e2e tests + working-directory: packages/webgpu + run: yarn test + + - name: Upload test snapshots + if: always() + uses: actions/upload-artifact@v4 + with: + name: webgpu-test-snapshots-hosted + path: packages/webgpu/src/__tests__/snapshots/