react-native-webgpu 0.10.1 → 0.10.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/Package.swift CHANGED
@@ -1,54 +1,27 @@
1
- // swift-tools-version: 5.9
1
+ // swift-tools-version: 6.0
2
2
  // Generated by scripts/generate-package-swift.ts — do not edit by hand.
3
3
  // Re-run that script after a new Dawn release to update the URL/checksum below.
4
- import Foundation
5
- import PackageDescription
6
-
7
- // Integration model: a local Swift package next to a CocoaPods install.
8
4
  //
9
- // This manifest targets React Native 0.81+ (the package's peer range), where
10
- // React Native's own headers and the RNWgpuViewSpec codegen output only exist
11
- // inside the consuming app's CocoaPods install. Add this package as a local
12
- // Swift Package dependency in place of the react-native-webgpu pod (export
13
- // RNWGPU_USE_SPM=1 for `pod install`) and keep running `pod install` for
14
- // everything else. The header search paths below point into that Pods
15
- // directory. They can only be expressed as `unsafeFlags`, and SwiftPM rejects
16
- // unsafeFlags in anything but a path dependency, so this package can only be
17
- // consumed locally, never by URL and version.
5
+ // SwiftPM manifest for react-native-webgpu. iOS only.
6
+ // Additive: the CocoaPods podspec remains the supported default.
18
7
  //
19
- // React Native 0.87 added SwiftPM autolinking. There, a dependency shipping
20
- // its own Package.swift is treated as self-managed and referenced directly,
21
- // and it consumes React Native's headers as SwiftPM products (ReactHeaders,
22
- // ReactNativeHeaders, ReactNativeDependenciesHeaders, plus ReactAppHeaders
23
- // from the app's generated codegen package) with no CocoaPods at all. This
24
- // manifest does not implement that model yet: on a 0.87+ app that has opted
25
- // into SwiftPM autolinking it is picked up as self-managed but cannot find
26
- // React Native's headers. Keep CocoaPods for React Native on those apps.
8
+ // React Native 0.87+ references a library that ships its own Package.swift
9
+ // through a symlink at <app>/ios/build/generated/autolinking/libs/<name>, and
10
+ // resolves the relative paths below against that symlink. They are the same
11
+ // for every standard app, because the symlink location is. Earlier React
12
+ // Native releases have no SwiftPM autolinking: keep CocoaPods there.
27
13
 
28
- // Absolute location of this package. Xcode may hand over a symlink (React
29
- // Native 0.87's autolinker does), so resolve it before deriving sibling paths.
14
+ import Foundation
15
+ import PackageDescription
16
+
17
+ // Absolute location of this package. Xcode passes the autolinking symlink as
18
+ // the package directory, so resolve it before deriving sibling paths.
30
19
  let packageRoot = URL(fileURLWithPath: Context.packageDirectory)
31
20
  .resolvingSymlinksInPath().path
32
21
  let sibling = { (relative: String) -> String in
33
22
  URL(fileURLWithPath: "\(packageRoot)/\(relative)").standardized.path
34
23
  }
35
24
 
36
- // Where the consuming app's Pods live. PODS_ROOT is exported by Xcode.app for
37
- // a real build, but never by plain `xcodebuild`, and never while Xcode indexes
38
- // the manifest before a first build. SwiftPM also caches manifest evaluations,
39
- // so an evaluation that ran without it would otherwise be cached with an empty
40
- // include list and surface later as header-not-found errors. Fall back to
41
- // where Pods sit relative to this package for every standard app layout, so
42
- // the result is the same with or without the environment variable.
43
- let podsRootCandidates = [
44
- sibling("../../ios/Pods"), // consumer: <app>/node_modules/react-native-webgpu
45
- sibling("../../apps/spm-example/ios/Pods"), // this monorepo
46
- ]
47
- let podsRoot =
48
- ProcessInfo.processInfo.environment["PODS_ROOT"]
49
- ?? podsRootCandidates.first { FileManager.default.fileExists(atPath: $0) }
50
- ?? podsRootCandidates[0]
51
-
52
25
  // Skia Graphite and WebGPU must link the exact same Dawn, or the pair links
53
26
  // fine and crashes at runtime. react-native-skia's podspec enforces this at
54
27
  // `pod install` time by comparing its libs/.dawn-version against this
@@ -79,57 +52,6 @@ for skia in skiaCandidates {
79
52
  break
80
53
  }
81
54
 
82
- // Static-library Pods layout. With `use_frameworks!` CocoaPods keeps headers
83
- // inside each framework instead and none of these paths exist; the podspec
84
- // refuses RNWGPU_USE_SPM in that mode.
85
- let publicHeaders = "\(podsRoot)/Headers/Public"
86
- let privateHeaders = "\(podsRoot)/Headers/Private"
87
- let headerSearchPaths = [
88
- publicHeaders,
89
- // Yoga's headers are CocoaPods-private (apps aren't meant to use Yoga's
90
- // C++ API directly), but React-Fabric's view props headers include them
91
- // transitively.
92
- "\(privateHeaders)/Yoga",
93
- "\(publicHeaders)/React-Core",
94
- "\(publicHeaders)/React-jsi",
95
- "\(publicHeaders)/React-callinvoker",
96
- "\(publicHeaders)/React-NativeModulesApple",
97
- "\(publicHeaders)/React-RCTFabric",
98
- "\(publicHeaders)/ReactCodegen",
99
- "\(publicHeaders)/RCT-Folly",
100
- "\(publicHeaders)/RCTRequired",
101
- "\(publicHeaders)/RCTTypeSafety",
102
- "\(publicHeaders)/FBLazyVector",
103
- "\(publicHeaders)/ReactCommon",
104
- "\(publicHeaders)/React-Fabric",
105
- "\(publicHeaders)/React-graphics",
106
- "\(publicHeaders)/React-utils",
107
- "\(publicHeaders)/React-featureflags",
108
- "\(publicHeaders)/React-rendererdebug",
109
- "\(publicHeaders)/React-debug",
110
- "\(publicHeaders)/React-timing",
111
- "\(publicHeaders)/React-runtimescheduler",
112
- "\(publicHeaders)/React-rendererconsistency",
113
- "\(publicHeaders)/React-performancetimeline",
114
- "\(publicHeaders)/React-runtimeexecutor",
115
- "\(publicHeaders)/React-cxxreact",
116
- "\(publicHeaders)/hermes-engine",
117
- "\(publicHeaders)/glog",
118
- "\(publicHeaders)/DoubleConversion",
119
- "\(publicHeaders)/fmt",
120
- "\(publicHeaders)/fast_float",
121
- ]
122
- // The only unsafe flags left: .headerSearchPath cannot escape the target
123
- // path, and these live in the app's Pods directory.
124
- let cFamilyIncludeFlags = headerSearchPaths.map { "-I\($0)" }
125
- let follyDefines: [(name: String, value: String?)] = [
126
- ("FOLLY_NO_CONFIG", nil),
127
- ("FOLLY_MOBILE", "1"),
128
- ("FOLLY_USE_LIBCPP", "1"),
129
- ("FOLLY_CFG_NO_COROUTINES", "1"),
130
- ("FOLLY_HAVE_CLOCK_GETTIME", "1"),
131
- ]
132
-
133
55
  // Unlike a native Xcode target, SwiftPM doesn't generate an implicit
134
56
  // per-target header map, so quoted includes only resolve within the
135
57
  // including file's own directory. The sources use bare cross-directory
@@ -153,10 +75,19 @@ let internalHeaderDirs = [
153
75
  ]
154
76
 
155
77
  let package = Package(
156
- name: "react-native-webgpu",
157
- platforms: [.iOS("15.1"), .macOS("11.0")],
78
+ name: "ReactNativeWebGPU",
79
+ // SwiftPM refuses to link a product whose floor is above the depending
80
+ // target's. React Native's generated Autolinked aggregate hardcodes iOS 15.0
81
+ // (react-native#58379 derives it from the app instead), so this must not
82
+ // exceed 15.0; a lower floor is always accepted.
83
+ platforms: [.iOS(.v15)],
158
84
  products: [
159
- .library(name: "react-native-webgpu", targets: ["react-native-webgpu"])
85
+ // Autolinking looks this name up verbatim; react-native.config.js pins it.
86
+ .library(name: "ReactNativeWebGPU", targets: ["ReactNativeWebGPU"])
87
+ ],
88
+ dependencies: [
89
+ .package(name: "React-GeneratedCode", path: "../../../ios"),
90
+ .package(name: "ReactNative", path: "../../../../xcframeworks"),
160
91
  ],
161
92
  targets: [
162
93
  // Dawn's compiled WebGPU implementation. The xcframework embeds the
@@ -168,18 +99,41 @@ let package = Package(
168
99
  checksum: "896575ffbc99610198a83d061f8c5a2789139b7f16c669fbf3a9f7a7ac9be123"
169
100
  ),
170
101
  .target(
171
- name: "react-native-webgpu",
172
- dependencies: ["WebGPUDawn"],
102
+ name: "ReactNativeWebGPU",
103
+ dependencies: [
104
+ .product(name: "ReactHeaders", package: "ReactNative"),
105
+ .product(name: "ReactNativeHeaders", package: "ReactNative"),
106
+ .product(name: "ReactNativeDependenciesHeaders", package: "ReactNative"),
107
+ // RNWgpuViewSpec, generated by the app's codegen run.
108
+ .product(name: "ReactAppHeaders", package: "React-GeneratedCode"),
109
+ "WebGPUDawn",
110
+ ],
111
+ // apple/ and cpp/ have no common ancestor below the package root, and
112
+ // .headerSearchPath cannot escape the target path.
173
113
  path: ".",
114
+ // Explicit, so SwiftPM never walks node_modules, lib, android, or the
115
+ // headers-only cpp/webgpu and cpp/dawn trees.
174
116
  sources: ["apple", "cpp/jsi", "cpp/rnwgpu"],
117
+ // SwiftPM requires a public headers directory inside the target and
118
+ // defaults to "include", which this package does not have.
175
119
  publicHeadersPath: "apple",
176
- cSettings: internalHeaderDirs.map { CSetting.headerSearchPath($0) }
177
- + follyDefines.map { CSetting.define($0.name, to: $0.value) }
178
- + [.unsafeFlags(cFamilyIncludeFlags)],
179
- cxxSettings: internalHeaderDirs.map { CXXSetting.headerSearchPath($0) }
180
- + follyDefines.map { CXXSetting.define($0.name, to: $0.value) }
181
- + [.unsafeFlags(cFamilyIncludeFlags)],
120
+ cxxSettings: internalHeaderDirs.map { CXXSetting.headerSearchPath($0) } + [
121
+ // CocoaPods forces both project-wide; the SwiftPM path defines neither.
122
+ // React Native's headers gate on them, and the prebuilt React.framework
123
+ // was built with both, so the module must see the same declarations.
124
+ .define("RCT_NEW_ARCH_ENABLED", to: "1"),
125
+ .define("RCT_REMOVE_LEGACY_ARCH", to: "1"),
126
+
127
+ // React's prebuilt C++ ABI is NDEBUG-gated (DebugStringConvertible's
128
+ // vtable and ShadowNode layout diverge). Omitting either breaks the
129
+ // Release link.
130
+ .define("DEBUG", .when(configuration: .debug)),
131
+ .define("NDEBUG", .when(configuration: .release)),
132
+ ],
182
133
  linkerSettings: [
134
+ // VideoPlayer uses AVFoundation / CoreMedia, shared-texture surfaces
135
+ // use CoreVideo (CVPixelBuffer), and ImageIO provides CGImageSource,
136
+ // the image decoder behind createImageBitmap.
183
137
  .linkedFramework("AVFoundation"),
184
138
  .linkedFramework("CoreMedia"),
185
139
  .linkedFramework("CoreVideo"),
@@ -187,5 +141,6 @@ let package = Package(
187
141
  ]
188
142
  ),
189
143
  ],
144
+ // React Native's headers need C++20.
190
145
  cxxLanguageStandard: .cxx20
191
146
  )
@@ -6,6 +6,7 @@
6
6
  #include <jsi/jsi.h>
7
7
 
8
8
  #include <ReactCommon/CallInvokerHolder.h>
9
+ #include <android/hardware_buffer_jni.h>
9
10
  #include <android/native_window_jni.h>
10
11
  #include <webgpu/webgpu_cpp.h>
11
12
 
@@ -25,7 +26,8 @@ extern "C" JNIEXPORT void JNICALL Java_com_webgpu_WebGPUModule_initializeNative(
25
26
  auto jsCallInvoker{
26
27
  facebook::jni::alias_ref<facebook::react::CallInvokerHolder::javaobject>{
27
28
  reinterpret_cast<facebook::react::CallInvokerHolder::javaobject>(
28
- jsCallInvokerHolder)} -> cthis()->getCallInvoker()};
29
+ jsCallInvokerHolder)} -> cthis()
30
+ ->getCallInvoker()};
29
31
  auto platformContext =
30
32
  std::make_shared<rnwgpu::AndroidPlatformContext>(globalBlobModule);
31
33
  manager = std::make_shared<rnwgpu::RNWebGPUManager>(runtime, jsCallInvoker,
@@ -91,3 +93,142 @@ extern "C" JNIEXPORT void JNICALL Java_com_webgpu_WebGPUView_onViewDestroyed(
91
93
  }
92
94
  registry.removeSurfaceInfo(contextId);
93
95
  }
96
+
97
+ // --- WebGPUHardwareBufferView (AHB-pool presentation)
98
+ // ---------------------------------
99
+
100
+ namespace {
101
+
102
+ // Weak handle on a WebGPUHardwareBufferView used by the native fence waiter to
103
+ // wake the UI thread when a frame is ready. Holding it weakly means a view that
104
+ // RN has dropped is not kept alive by a SurfaceInfo that outlives it (the
105
+ // registry keeps the SurfaceInfo until onDropViewInstance).
106
+ struct HardwareBufferViewWaker {
107
+ jweak view = nullptr;
108
+ jmethodID onFrameReady = nullptr;
109
+
110
+ HardwareBufferViewWaker(JNIEnv *env, jobject thiz) {
111
+ view = env->NewWeakGlobalRef(thiz);
112
+ jclass cls = env->GetObjectClass(thiz);
113
+ onFrameReady = env->GetMethodID(cls, "onNativeFrameReady", "()V");
114
+ env->DeleteLocalRef(cls);
115
+ }
116
+
117
+ ~HardwareBufferViewWaker() {
118
+ // May run on the waiter or JS thread; ThreadScope attaches if needed.
119
+ facebook::jni::ThreadScope scope;
120
+ JNIEnv *env = facebook::jni::Environment::current();
121
+ if (env != nullptr && view != nullptr) {
122
+ env->DeleteWeakGlobalRef(view);
123
+ }
124
+ }
125
+
126
+ // Called on the waiter thread, outside the pool lock.
127
+ void operator()() const {
128
+ facebook::jni::ThreadScope scope;
129
+ JNIEnv *env = facebook::jni::Environment::current();
130
+ if (env == nullptr || onFrameReady == nullptr) {
131
+ return;
132
+ }
133
+ jobject local = env->NewLocalRef(view);
134
+ if (local == nullptr) {
135
+ return; // the view was garbage collected
136
+ }
137
+ env->CallVoidMethod(local, onFrameReady);
138
+ if (env->ExceptionCheck()) {
139
+ env->ExceptionClear();
140
+ }
141
+ env->DeleteLocalRef(local);
142
+ }
143
+ };
144
+
145
+ } // namespace
146
+
147
+ // Turn on pool mode for this context. dpW/dpH is the canvas-client (dp) size;
148
+ // the native pool buffers are sized from the canvas drawing buffer lazily.
149
+ extern "C" JNIEXPORT void JNICALL
150
+ Java_com_webgpu_WebGPUHardwareBufferView_nEnablePool(JNIEnv *env, jobject thiz,
151
+ jint contextId, jint dpW,
152
+ jint dpH) {
153
+ auto &registry = rnwgpu::SurfaceRegistry::getInstance();
154
+ auto info = registry.getSurfaceInfoOrCreate(
155
+ contextId, manager->_gpu, static_cast<int>(dpW), static_cast<int>(dpH));
156
+ auto waker = std::make_shared<HardwareBufferViewWaker>(env, thiz);
157
+ info->setPoolFrameReadyCallback([waker]() { (*waker)(); });
158
+ info->enablePool(static_cast<int>(dpW), static_cast<int>(dpH));
159
+ }
160
+
161
+ // Keep the canvas-client (dp) size in sync on resize.
162
+ extern "C" JNIEXPORT void JNICALL
163
+ Java_com_webgpu_WebGPUHardwareBufferView_nSetClientSize(JNIEnv *env,
164
+ jobject thiz,
165
+ jint contextId,
166
+ jint dpW, jint dpH) {
167
+ auto &registry = rnwgpu::SurfaceRegistry::getInstance();
168
+ auto info = registry.getSurfaceInfo(contextId);
169
+ if (info != nullptr) {
170
+ info->setPoolClientSize(static_cast<int>(dpW), static_cast<int>(dpH));
171
+ }
172
+ }
173
+
174
+ // The HardwareBuffer backing a (generation, slot), for the view to wrap in a
175
+ // Bitmap. Returns null for a retired generation.
176
+ extern "C" JNIEXPORT jobject JNICALL
177
+ Java_com_webgpu_WebGPUHardwareBufferView_nGetHardwareBuffer(
178
+ JNIEnv *env, jobject thiz, jint contextId, jint generation, jint slot) {
179
+ auto &registry = rnwgpu::SurfaceRegistry::getInstance();
180
+ auto info = registry.getSurfaceInfo(contextId);
181
+ if (info == nullptr) {
182
+ return nullptr;
183
+ }
184
+ void *ahb = info->poolBufferForDisplay(static_cast<uint32_t>(generation),
185
+ static_cast<int>(slot));
186
+ if (ahb == nullptr) {
187
+ return nullptr;
188
+ }
189
+ // toHardwareBuffer acquires its own ref for the returned jobject; the pool
190
+ // keeps the underlying buffer alive independently.
191
+ return AHardwareBuffer_toHardwareBuffer(env,
192
+ static_cast<AHardwareBuffer *>(ahb));
193
+ }
194
+
195
+ // Latest signaled frame ready to display, encoded (generation << 32 | slot), or
196
+ // -1 when nothing is new. Called on the UI thread after onNativeFrameReady.
197
+ extern "C" JNIEXPORT jlong JNICALL
198
+ Java_com_webgpu_WebGPUHardwareBufferView_nPollReady(JNIEnv *env, jobject thiz,
199
+ jint contextId) {
200
+ auto &registry = rnwgpu::SurfaceRegistry::getInstance();
201
+ auto info = registry.getSurfaceInfo(contextId);
202
+ if (info == nullptr) {
203
+ return -1;
204
+ }
205
+ return info->poolPollReady();
206
+ }
207
+
208
+ // The view is done displaying (and holding) a slot; return it to the pool.
209
+ extern "C" JNIEXPORT void JNICALL
210
+ Java_com_webgpu_WebGPUHardwareBufferView_nReleaseSlot(JNIEnv *env, jobject thiz,
211
+ jint contextId,
212
+ jint generation,
213
+ jint slot) {
214
+ auto &registry = rnwgpu::SurfaceRegistry::getInstance();
215
+ auto info = registry.getSurfaceInfo(contextId);
216
+ if (info != nullptr) {
217
+ info->poolReleaseSlot(static_cast<uint32_t>(generation),
218
+ static_cast<int>(slot));
219
+ }
220
+ }
221
+
222
+ // View detached / hidden: keep the canvas alive by falling back to an offscreen
223
+ // texture (mirrors switchToOffscreenSurface for the surface path).
224
+ extern "C" JNIEXPORT void JNICALL
225
+ Java_com_webgpu_WebGPUHardwareBufferView_nSwitchToOffscreen(JNIEnv *env,
226
+ jobject thiz,
227
+ jint contextId) {
228
+ auto &registry = rnwgpu::SurfaceRegistry::getInstance();
229
+ auto info = registry.getSurfaceInfo(contextId);
230
+ if (info != nullptr) {
231
+ info->setPoolFrameReadyCallback(nullptr);
232
+ info->switchToOffscreen();
233
+ }
234
+ }
@@ -22,4 +22,6 @@ public interface WebGPUAPI {
22
22
  );
23
23
 
24
24
  void surfaceOffscreen();
25
+
26
+ int getContextId();
25
27
  }
@@ -0,0 +1,278 @@
1
+ package com.webgpu;
2
+
3
+ import android.annotation.SuppressLint;
4
+ import android.content.Context;
5
+ import android.graphics.Bitmap;
6
+ import android.graphics.Canvas;
7
+ import android.graphics.Paint;
8
+ import android.graphics.Rect;
9
+ import android.hardware.HardwareBuffer;
10
+ import android.os.Build;
11
+ import android.os.Handler;
12
+ import android.os.Looper;
13
+ import android.view.View;
14
+
15
+ import androidx.annotation.Keep;
16
+ import androidx.annotation.NonNull;
17
+ import androidx.annotation.RequiresApi;
18
+
19
+ import java.util.ArrayDeque;
20
+ import java.util.HashMap;
21
+ import java.util.Iterator;
22
+ import java.util.Map;
23
+ import java.util.concurrent.atomic.AtomicBoolean;
24
+
25
+ /**
26
+ * A "normal RN view" backend for the WebGPU canvas.
27
+ *
28
+ * <p>WebGPU renders into a native pool of AHardwareBuffers (sized from the canvas drawing buffer,
29
+ * like the swapchain) that Dawn imports as SharedTextureMemory; see SurfaceRegistry.h. Each finished
30
+ * buffer is drawn inline here via {@link Bitmap#wrapHardwareBuffer} + {@link Canvas#drawBitmap}, so
31
+ * this is a plain {@link View}: parent transforms, clipping, alpha, z-order and animations all
32
+ * apply, with no GL interop and no extra copy.
33
+ *
34
+ * <p>This view is a pure consumer: it enables pool mode, and whenever the native fence waiter
35
+ * signals that a frame is ready it asks native for the latest ready (gen, slot), wraps that buffer
36
+ * in a cached Bitmap, and draws it scaled to the current bounds. Acquire is rigorous (native blocks
37
+ * on Dawn's render-complete fence before a frame is ready); release is heuristic via a held-ring of
38
+ * {@link #HELD_MAX} frames.
39
+ */
40
+ @RequiresApi(api = Build.VERSION_CODES.Q)
41
+ @SuppressLint("ViewConstructor")
42
+ public class WebGPUHardwareBufferView extends View {
43
+
44
+ private static final int HELD_MAX = 2; // displayed frames kept before release (release safety)
45
+
46
+ private final WebGPUAPI mApi;
47
+ private final Paint mPaint;
48
+ private final Rect mSrc = new Rect();
49
+ private final Rect mDst = new Rect();
50
+
51
+ // token (generation << 32 | slot) -> wrapped hardware Bitmap (one per slot per generation).
52
+ private final Map<Long, Bitmap> mBitmaps = new HashMap<>();
53
+ // Held-ring of displayed frame tokens (newest last). The newest is what onDraw shows.
54
+ private final ArrayDeque<Long> mHeld = new ArrayDeque<>();
55
+
56
+ // Native -> UI thread wake-up. The fence waiter calls onNativeFrameReady() off the UI thread;
57
+ // we coalesce those into at most one pending consume() on the main looper.
58
+ private final Handler mMainHandler = new Handler(Looper.getMainLooper());
59
+ private final AtomicBoolean mConsumePosted = new AtomicBoolean(false);
60
+ private final Runnable mConsume = this::consume;
61
+
62
+ private Bitmap mDisplayed;
63
+ private int mDisplayedW;
64
+ private int mDisplayedH;
65
+ private int mCurrentGen;
66
+
67
+ private boolean mAttached;
68
+ private boolean mEnabled;
69
+
70
+ public WebGPUHardwareBufferView(Context context, WebGPUAPI api) {
71
+ super(context);
72
+ mApi = api;
73
+ mPaint = new Paint(Paint.FILTER_BITMAP_FLAG);
74
+ setWillNotDraw(false);
75
+ }
76
+
77
+ private int contextId() {
78
+ return mApi.getContextId();
79
+ }
80
+
81
+ private static int genOf(long token) {
82
+ return (int) (token >>> 32);
83
+ }
84
+
85
+ private static int slotOf(long token) {
86
+ return (int) (token & 0xffffffffL);
87
+ }
88
+
89
+ // --- Sizing ----------------------------------------------------------------
90
+
91
+ private void enablePool(int w, int h) {
92
+ final float density = getResources().getDisplayMetrics().density;
93
+ nEnablePool(contextId(), Math.max(1, Math.round(w / density)),
94
+ Math.max(1, Math.round(h / density)));
95
+ mEnabled = true;
96
+ }
97
+
98
+ @Override
99
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
100
+ super.onSizeChanged(w, h, oldw, oldh);
101
+ if (w <= 0 || h <= 0) {
102
+ return;
103
+ }
104
+ if (!mEnabled) {
105
+ enablePool(w, h);
106
+ } else {
107
+ // Native reallocates the pool from the canvas drawing buffer; we only keep the dp
108
+ // canvas-client size in sync so JS computes the right canvas.width/height.
109
+ final float density = getResources().getDisplayMetrics().density;
110
+ nSetClientSize(contextId(), Math.max(1, Math.round(w / density)),
111
+ Math.max(1, Math.round(h / density)));
112
+ }
113
+ }
114
+
115
+ // --- Consume (UI thread, woken by the native fence waiter) -----------------
116
+
117
+ /** Called from the native fence-waiter thread (see cpp-adapter.cpp) when a frame is ready. */
118
+ @Keep
119
+ private void onNativeFrameReady() {
120
+ if (mConsumePosted.compareAndSet(false, true)) {
121
+ mMainHandler.post(mConsume);
122
+ }
123
+ }
124
+
125
+ private void consume() {
126
+ mConsumePosted.set(false);
127
+ if (!mAttached) {
128
+ return;
129
+ }
130
+ long token = nPollReady(contextId());
131
+ if (token >= 0) {
132
+ onFrameReady(token);
133
+ }
134
+ }
135
+
136
+ private void onFrameReady(long token) {
137
+ final int gen = genOf(token);
138
+ final int slot = slotOf(token);
139
+
140
+ if (gen > mCurrentGen) {
141
+ mCurrentGen = gen;
142
+ recycleStaleGenerations();
143
+ }
144
+
145
+ Bitmap bmp = mBitmaps.get(token);
146
+ if (bmp == null) {
147
+ HardwareBuffer hb = nGetHardwareBuffer(contextId(), gen, slot);
148
+ if (hb == null) {
149
+ return; // generation already retired; drop this stale frame
150
+ }
151
+ // wrapHardwareBuffer takes its own ref, so the wrapper can be closed immediately.
152
+ bmp = Bitmap.wrapHardwareBuffer(hb, null);
153
+ hb.close();
154
+ if (bmp == null) {
155
+ // Cannot display this frame; hand the slot straight back so the pool
156
+ // does not drain (native marked it Displayed when we polled it).
157
+ nReleaseSlot(contextId(), gen, slot);
158
+ return;
159
+ }
160
+ mBitmaps.put(token, bmp);
161
+ }
162
+
163
+ mDisplayed = bmp;
164
+ mDisplayedW = bmp.getWidth();
165
+ mDisplayedH = bmp.getHeight();
166
+
167
+ mHeld.addLast(token);
168
+ while (mHeld.size() > HELD_MAX) {
169
+ long old = mHeld.pollFirst();
170
+ if (genOf(old) == mCurrentGen) {
171
+ // Live generation reuses slots: hand it back once HWUI has had time to sample it.
172
+ nReleaseSlot(contextId(), mCurrentGen, slotOf(old));
173
+ } else {
174
+ // Old generation: its slot is never re-rendered, so just drop the Bitmap.
175
+ recycleToken(old);
176
+ }
177
+ }
178
+ invalidate();
179
+ }
180
+
181
+ private void recycleStaleGenerations() {
182
+ // Keep the current and previous generation (cross-fade); recycle older cached Bitmaps that are
183
+ // no longer held.
184
+ Iterator<Map.Entry<Long, Bitmap>> it = mBitmaps.entrySet().iterator();
185
+ while (it.hasNext()) {
186
+ Map.Entry<Long, Bitmap> e = it.next();
187
+ if (genOf(e.getKey()) < mCurrentGen - 1 && !mHeld.contains(e.getKey())) {
188
+ Bitmap b = e.getValue();
189
+ if (b != mDisplayed && !b.isRecycled()) {
190
+ b.recycle();
191
+ }
192
+ it.remove();
193
+ }
194
+ }
195
+ }
196
+
197
+ private void recycleToken(long token) {
198
+ Bitmap b = mBitmaps.remove(token);
199
+ if (b != null && b != mDisplayed && !b.isRecycled()) {
200
+ b.recycle();
201
+ }
202
+ }
203
+
204
+ @Override
205
+ protected void onDraw(Canvas canvas) {
206
+ super.onDraw(canvas);
207
+ Bitmap bmp = mDisplayed;
208
+ if (bmp == null || bmp.isRecycled()) {
209
+ return;
210
+ }
211
+ // Always scale the last good frame to the current bounds, so a buffer that lags the view size
212
+ // (mid-resize) is shown stretched rather than blank.
213
+ mSrc.set(0, 0, mDisplayedW, mDisplayedH);
214
+ mDst.set(0, 0, getWidth(), getHeight());
215
+ canvas.drawBitmap(bmp, mSrc, mDst, mPaint);
216
+ }
217
+
218
+ // --- Lifecycle -------------------------------------------------------------
219
+
220
+ @Override
221
+ protected void onAttachedToWindow() {
222
+ super.onAttachedToWindow();
223
+ mAttached = true;
224
+ if (!mEnabled && getWidth() > 0 && getHeight() > 0) {
225
+ enablePool(getWidth(), getHeight());
226
+ }
227
+ // Pick up anything that became ready while we were detached.
228
+ onNativeFrameReady();
229
+ }
230
+
231
+ @Override
232
+ protected void onDetachedFromWindow() {
233
+ super.onDetachedFromWindow();
234
+ mAttached = false;
235
+ mEnabled = false;
236
+ mMainHandler.removeCallbacks(mConsume);
237
+ mConsumePosted.set(false);
238
+
239
+ // Keep the canvas alive offscreen, then drop all GPU resources. The
240
+ // SurfaceInfo stays registered so a transient detach/re-attach keeps
241
+ // working; WebGPUViewManager.onDropViewInstance removes it when RN drops
242
+ // the view for good.
243
+ nSwitchToOffscreen(contextId());
244
+
245
+ mDisplayed = null;
246
+ mHeld.clear();
247
+ for (Bitmap b : mBitmaps.values()) {
248
+ if (!b.isRecycled()) {
249
+ b.recycle();
250
+ }
251
+ }
252
+ mBitmaps.clear();
253
+ mCurrentGen = 0;
254
+ }
255
+
256
+ @Override
257
+ protected void onVisibilityChanged(@NonNull View changedView, int visibility) {
258
+ super.onVisibilityChanged(changedView, visibility);
259
+ if (visibility == VISIBLE && mAttached && !mEnabled
260
+ && getWidth() > 0 && getHeight() > 0) {
261
+ enablePool(getWidth(), getHeight());
262
+ }
263
+ }
264
+
265
+ // --- Native (cpp-adapter.cpp) ----------------------------------------------
266
+
267
+ private native void nEnablePool(int contextId, int dpW, int dpH);
268
+
269
+ private native void nSetClientSize(int contextId, int dpW, int dpH);
270
+
271
+ private native long nPollReady(int contextId);
272
+
273
+ private native HardwareBuffer nGetHardwareBuffer(int contextId, int generation, int slot);
274
+
275
+ private native void nReleaseSlot(int contextId, int generation, int slot);
276
+
277
+ private native void nSwitchToOffscreen(int contextId);
278
+ }