diff --git a/apps/docs/content/api/gpu-device-extensions.mdx b/apps/docs/content/api/gpu-device-extensions.mdx index 71f1783da..a788e1425 100644 --- a/apps/docs/content/api/gpu-device-extensions.mdx +++ b/apps/docs/content/api/gpu-device-extensions.mdx @@ -161,6 +161,8 @@ const device = await adapter.requestDevice({ On Android, the `ignore_imported_ahardwarebuffer_vulkan_image_size` toggle is enabled by default, so native buffers import on every driver. Pass it in `disabledToggles` to turn it off. +On iOS and tvOS, the `metal_suspend_submissions_in_background` toggle is enabled by default, so GPU work submitted while the app is in the background is held rather than refused (see [Background and foreground](/docs/getting-started/native-api#background-and-foreground)). Pass it in `disabledToggles` to turn it off. + ## implicitDeviceSynchronization By default, `requestDevice()` enables Dawn's [`implicit-device-synchronization`](https://dawn.googlesource.com/dawn/+/refs/heads/main/docs/dawn/features/implicit_device_synchronization.md) feature, which lets a device created on the JS thread be used from the UI thread or a dedicated worklet runtime (see [Thread safety](/docs/integrations/worklets#thread-safety)). diff --git a/apps/docs/content/docs/getting-started/native-api.mdx b/apps/docs/content/docs/getting-started/native-api.mdx index 52a1f63ea..61054cf5d 100644 --- a/apps/docs/content/docs/getting-started/native-api.mdx +++ b/apps/docs/content/docs/getting-started/native-api.mdx @@ -37,6 +37,17 @@ Dawn's implicit device synchronization, enabled by default, lets a device be use See [Worklets](/docs/integrations/worklets) for the full integration guide. +## Background and foreground + +iOS refuses GPU work from an app in the background, including work still queued when the app got there, and Dawn loses the device over a refused command buffer. +React Native WebGPU holds that work instead, with Dawn's `metal_suspend_submissions_in_background` toggle, enabled on every device it creates on iOS and tvOS: when the app goes to the background, what was submitted is committed before the app is suspended, and from then on every submission is encoded but not committed, in order, until the app comes back to the foreground. +Presenting a canvas does nothing meanwhile; the canvas catches up on return. +Nothing changes for your code: a held `onSubmittedWorkDone()` or `mapAsync()` promise settles once the app is back. +A render loop should still pause while the app is in the background (React Native's `AppState`), if only to spare the battery: what it submits meanwhile is kept in memory until the app returns. +Android and macOS do not refuse GPU work from a background app, so nothing is held there. +The hold needs `react-native-webgpu-dawn` 157.1.0 or later; an older Dawn drops the toggle, and the device is lost over the first refused command buffer (its `lost` promise then resolves with the reason `unknown`). +Pass the toggle in `disabledToggles` (see [dawnToggles](/docs/api/gpu-device-extensions#dawntoggles)) to opt out. + ## Loading images This module provides a `createImageBitmap` function you can use with `copyExternalImageToTexture`. diff --git a/package.json b/package.json index e973c4835..883e5ea05 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "resolutions": { "@types/react": "^18.2.44", "@webgpu/types": "^0.1.42", + "react-native-webgpu-dawn": "157.1.0", "@react-three/fiber@^9.4.0": "patch:@react-three/fiber@npm%3A9.4.0#./.yarn/patches/@react-three-fiber-npm-9.4.0-77fe8ee9bd.patch" }, "packageManager": "yarn@3.6.1", diff --git a/packages/webgpu/CONTRIBUTING.md b/packages/webgpu/CONTRIBUTING.md index b0ba79b77..aa9a21588 100644 --- a/packages/webgpu/CONTRIBUTING.md +++ b/packages/webgpu/CONTRIBUTING.md @@ -93,7 +93,10 @@ sibling in `node_modules`, or this monorepo's root) and links its headers, so no header search path is needed for them. A Graphite build of react-native-skia installed alongside must link the same Dawn, which the manifest checks at evaluation time: a react-native-skia that depends on -`react-native-webgpu-dawn` must pin the same version, and the 3.0.x line, which +`react-native-webgpu-dawn` must resolve the same version (the copy nested under +it, else the one next to it, as its podspec resolves it; a workspace that +resolves the package to one version for both passes while react-native-skia's +own pin lags behind a Dawn bump), and the 3.0.x line, which vendors its own Dawn, must record this package's `dawn` tag at `react-native-skia-graphite-apple-ios/libs/.dawn-version` (hoisted or nested under `react-native-skia/node_modules`, also checked for macOS and for the v2 diff --git a/packages/webgpu/Package.swift b/packages/webgpu/Package.swift index c3506c0ef..1793b97bb 100644 --- a/packages/webgpu/Package.swift +++ b/packages/webgpu/Package.swift @@ -56,21 +56,33 @@ let dawnVersion = // changing either package's Dawn reset the package caches (File > Packages > // Reset Package Caches) if this does not re-run. // -// A react-native-skia that depends on react-native-webgpu-dawn itself must pin -// the same version as this package. Older Graphite builds (react-native-skia -// 3.0.x) vendor their own Dawn and record its release tag at -// libs/.dawn-version in their binary packages; this package's `dawn` field +// A react-native-skia that depends on react-native-webgpu-dawn itself must +// resolve the same version as this package: the copy nested under it when the +// package manager could not hoist it, else the one next to it (Node's +// resolution, which react-native-skia's podspec compares too). A workspace +// that resolves the package to one version for both (yarn `resolutions`, npm +// `overrides`) passes while react-native-skia's own pin lags behind a Dawn +// bump: the app then contains that one Dawn. Older Graphite builds +// (react-native-skia 3.0.x) vendor their own Dawn and record its release tag +// at libs/.dawn-version in their binary packages; this package's `dawn` field // names the release that Dawn is interchangeable with. if let skiaPath = nodePackage("react-native-skia"), let skiaPackage = readJSON("\(skiaPath)/package.json"), - let skiaDawn = (skiaPackage["dependencies"] as? [String: String])?["react-native-webgpu-dawn"] + let skiaDawnPin = (skiaPackage["dependencies"] as? [String: String])?["react-native-webgpu-dawn"] { + let skiaDawnPath = [ + "\(skiaPath)/node_modules/react-native-webgpu-dawn", + "\(skiaPath)/../react-native-webgpu-dawn", + ].first { FileManager.default.fileExists(atPath: "\($0)/package.json") } + let skiaDawn = + skiaDawnPath.flatMap { readJSON("\($0)/package.json")?["version"] as? String } ?? skiaDawnPin if skiaDawn != dawnVersion { fatalError( """ react-native-webgpu: Dawn version mismatch. This package links \ - react-native-webgpu-dawn \(dawnVersion) but react-native-skia depends on \ - \(skiaDawn). Align the two packages so the app contains exactly one Dawn. + react-native-webgpu-dawn \(dawnVersion) but react-native-skia resolves \ + \(skiaDawn) (it depends on \(skiaDawnPin)). Align the two packages so the \ + app contains exactly one Dawn. """) } } else { diff --git a/packages/webgpu/cpp/rnwgpu/api/GPUAdapter.cpp b/packages/webgpu/cpp/rnwgpu/api/GPUAdapter.cpp index f2df9398d..97154b24a 100644 --- a/packages/webgpu/cpp/rnwgpu/api/GPUAdapter.cpp +++ b/packages/webgpu/cpp/rnwgpu/api/GPUAdapter.cpp @@ -271,6 +271,32 @@ async::AsyncTaskHandle GPUAdapter::requestDevice( enabledToggles.push_back(name); } } +#endif +#if defined(__APPLE__) + // iOS refuses GPU work from an app in the background, and Dawn loses + // the device over a refused command buffer. With this toggle Dawn + // holds the commits while the app is in the background, from + // UIApplication's own notifications, and commits them when it comes + // back: nothing is lost, nothing is refused, and a promise on the + // held work settles once the app is back (the same toggle Skia + // requests on the device it shares, see react-native-skia's + // DawnUtils::requestDevice). Dawn applies it on iOS and tvOS only, and + // a Dawn without it (react-native-webgpu-dawn before 157.1.0) drops + // the name. The caller opts out through disabledToggles. + { + static const char *const kName = + "metal_suspend_submissions_in_background"; + const auto named = [](const char *t) { + return std::strcmp(t, kName) == 0; + }; + const bool explicitlyDisabled = std::any_of( + disabledToggles.begin(), disabledToggles.end(), named); + const bool alreadyEnabled = + std::any_of(enabledToggles.begin(), enabledToggles.end(), named); + if (!explicitlyDisabled && !alreadyEnabled) { + enabledToggles.push_back(kName); + } + } #endif if (!enabledToggles.empty() || !disabledToggles.empty()) { toggles.enabledToggleCount = enabledToggles.size(); diff --git a/packages/webgpu/package.json b/packages/webgpu/package.json index 9d0e47cd3..7a0a3c3f4 100644 --- a/packages/webgpu/package.json +++ b/packages/webgpu/package.json @@ -111,7 +111,7 @@ "yargs": "^17.7.2" }, "dependencies": { - "react-native-webgpu-dawn": "157.0.0" + "react-native-webgpu-dawn": "157.1.0" }, "peerDependencies": { "react": "*", diff --git a/yarn.lock b/yarn.lock index 4e03cd522..1da273877 100644 --- a/yarn.lock +++ b/yarn.lock @@ -20880,10 +20880,10 @@ __metadata: languageName: node linkType: hard -"react-native-webgpu-dawn@npm:157.0.0": - version: 157.0.0 - resolution: "react-native-webgpu-dawn@npm:157.0.0" - checksum: 2fbebac05e083a4d0ecd258e459ddb6a41c1755f1720b9361a43af31c37d8e67aac60904f2731535b52e9fb19dfb0ced05d3011861635786a449e8a3835cbb20 +"react-native-webgpu-dawn@npm:157.1.0": + version: 157.1.0 + resolution: "react-native-webgpu-dawn@npm:157.1.0" + checksum: c938c4d0c1f1b5bc9eb8b8f2f3580df1babb7e02151262a3ff1cd4dfe4954c7ee6e56408403e715730eed5b00c33189095639f990befe9ad759632c82605ed1a languageName: node linkType: hard @@ -20948,7 +20948,7 @@ __metadata: react-native-builder-bob: ^0.23.2 react-native-reanimated: 4.6.0 react-native-web: ^0.21.2 - react-native-webgpu-dawn: 157.0.0 + react-native-webgpu-dawn: 157.1.0 react-native-worklets: 0.12.2 rimraf: ^5.0.7 seedrandom: ^3.0.5