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..[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 + 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" + + # 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/