react-native-skia-webgpu-video 0.0.0-stage → 0.1.0-beta.1

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.
Files changed (237) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +303 -2
  3. package/android/BUILD_VALIDATION.md +62 -0
  4. package/android/CMakeLists.txt +99 -0
  5. package/android/build-validation.json +262 -0
  6. package/android/build.gradle +185 -0
  7. package/android/cpp/EncoderTargets.cpp +226 -0
  8. package/android/cpp/EncoderTargets.h +70 -0
  9. package/android/cpp/JNIHelpers.cpp +15 -0
  10. package/android/cpp/JNIHelpers.h +20 -0
  11. package/android/cpp/NativeEventDispatcher.cpp +59 -0
  12. package/android/cpp/NativeEventDispatcher.h +31 -0
  13. package/android/cpp/NativeHardwareBuffer.cpp +397 -0
  14. package/android/cpp/NativeHardwareBuffer.h +34 -0
  15. package/android/cpp/NativeRgbaBuffer.cpp +45 -0
  16. package/android/cpp/NativeRgbaBuffer.h +9 -0
  17. package/android/cpp/ReturnedVideoFrames.h +37 -0
  18. package/android/cpp/RgbaFrameLease.h +53 -0
  19. package/android/cpp/VideoCapabilities.cpp +87 -0
  20. package/android/cpp/VideoCapabilities.h +44 -0
  21. package/android/cpp/VideoComposition.cpp +170 -0
  22. package/android/cpp/VideoComposition.h +36 -0
  23. package/android/cpp/VideoCompositionFramesExtractor.cpp +82 -0
  24. package/android/cpp/VideoCompositionFramesExtractor.h +48 -0
  25. package/android/cpp/VideoCompositionFramesExtractorHostObject.cpp +230 -0
  26. package/android/cpp/VideoCompositionFramesExtractorHostObject.h +42 -0
  27. package/android/cpp/VideoCompositionFramesExtractorSyncHostObject.cpp +133 -0
  28. package/android/cpp/VideoCompositionFramesExtractorSyncHostObject.h +46 -0
  29. package/android/cpp/VideoEncoderHostObject.cpp +318 -0
  30. package/android/cpp/VideoEncoderHostObject.h +76 -0
  31. package/android/cpp/VideoFrame.cpp +293 -0
  32. package/android/cpp/VideoFrame.h +27 -0
  33. package/android/cpp/VideoPlayer.cpp +96 -0
  34. package/android/cpp/VideoPlayer.h +52 -0
  35. package/android/cpp/VideoPlayerHostObject.cpp +225 -0
  36. package/android/cpp/VideoPlayerHostObject.h +43 -0
  37. package/android/cpp/cpp-adapter.cpp +316 -0
  38. package/android/gradle.properties +5 -0
  39. package/android/instrumentation-inventory.json +178 -0
  40. package/android/proguard-rules.pro +4 -0
  41. package/android/src/main/AndroidManifest.xml +2 -0
  42. package/android/src/main/java/com/azzapp/rnskv/AudioCompositionExporter.java +537 -0
  43. package/android/src/main/java/com/azzapp/rnskv/AudioCompositionPlayer.java +321 -0
  44. package/android/src/main/java/com/azzapp/rnskv/BufferLifetime.java +55 -0
  45. package/android/src/main/java/com/azzapp/rnskv/DecodeRequests.java +100 -0
  46. package/android/src/main/java/com/azzapp/rnskv/DecoderWindow.java +55 -0
  47. package/android/src/main/java/com/azzapp/rnskv/DisplayVsyncPeriod.java +72 -0
  48. package/android/src/main/java/com/azzapp/rnskv/EGLResourcesHolder.java +132 -0
  49. package/android/src/main/java/com/azzapp/rnskv/EGLUtils.java +225 -0
  50. package/android/src/main/java/com/azzapp/rnskv/FrameSize.java +50 -0
  51. package/android/src/main/java/com/azzapp/rnskv/GLFrameExtractor.java +510 -0
  52. package/android/src/main/java/com/azzapp/rnskv/JNIHelpers.java +12 -0
  53. package/android/src/main/java/com/azzapp/rnskv/NativeEventDispatcher.java +25 -0
  54. package/android/src/main/java/com/azzapp/rnskv/NativeHardwareBuffer.java +62 -0
  55. package/android/src/main/java/com/azzapp/rnskv/NativeRgbaBuffer.java +43 -0
  56. package/android/src/main/java/com/azzapp/rnskv/ReactNativeSkiaVideoModule.java +45 -0
  57. package/android/src/main/java/com/azzapp/rnskv/ReactNativeSkiaVideoPackage.java +43 -0
  58. package/android/src/main/java/com/azzapp/rnskv/ReactNativeSkiaVideoSpec.java +9 -0
  59. package/android/src/main/java/com/azzapp/rnskv/RgbaBufferAllocator.java +114 -0
  60. package/android/src/main/java/com/azzapp/rnskv/RgbaLayout.java +33 -0
  61. package/android/src/main/java/com/azzapp/rnskv/SurfaceOutputPolicy.java +13 -0
  62. package/android/src/main/java/com/azzapp/rnskv/TextureRenderer.java +150 -0
  63. package/android/src/main/java/com/azzapp/rnskv/TimeHelpers.java +11 -0
  64. package/android/src/main/java/com/azzapp/rnskv/VideoCapabilities.java +354 -0
  65. package/android/src/main/java/com/azzapp/rnskv/VideoComposition.java +147 -0
  66. package/android/src/main/java/com/azzapp/rnskv/VideoCompositionDecoder.java +779 -0
  67. package/android/src/main/java/com/azzapp/rnskv/VideoCompositionFramesExtractor.java +463 -0
  68. package/android/src/main/java/com/azzapp/rnskv/VideoCompositionFramesExtractorSync.java +241 -0
  69. package/android/src/main/java/com/azzapp/rnskv/VideoCompositionItemDecoder.java +492 -0
  70. package/android/src/main/java/com/azzapp/rnskv/VideoEncoder.java +639 -0
  71. package/android/src/main/java/com/azzapp/rnskv/VideoFrame.java +74 -0
  72. package/android/src/main/java/com/azzapp/rnskv/VideoPlayer.java +361 -0
  73. package/cpp/RNSVCheckedSizes.h +54 -0
  74. package/cpp/RNSVEventEmitter.cpp +199 -0
  75. package/cpp/RNSVEventEmitter.h +31 -0
  76. package/cpp/RNSVHostObject.cpp +79 -0
  77. package/cpp/RNSVHostObject.h +67 -0
  78. package/cpp/RNSVMemoryBudget.cpp +68 -0
  79. package/cpp/RNSVMemoryBudget.h +100 -0
  80. package/cpp/RNSVRuntimeLifecycleMonitor.cpp +88 -0
  81. package/cpp/RNSVRuntimeLifecycleMonitor.h +33 -0
  82. package/docs/ANDROID_DEVICE_306.md +95 -0
  83. package/docs/ANDROID_EXPORT_CACHE_306.md +48 -0
  84. package/docs/ANDROID_MEMORY_RETENTION_306.md +67 -0
  85. package/docs/BENCHMARKS.md +368 -0
  86. package/docs/IOS_DEVICE_TESTS.md +115 -0
  87. package/docs/IOS_SIMULATOR_TESTS.md +74 -0
  88. package/docs/MIGRATION.md +82 -0
  89. package/docs/NATIVE_MEMORY_INVESTIGATION_306.md +88 -0
  90. package/docs/PERFORMANCE_AB_IPHONE_303.md +196 -0
  91. package/docs/PERFORMANCE_AB_SIMULATOR.md +135 -0
  92. package/docs/PERFORMANCE_AB_SOAK_SIMULATOR.md +84 -0
  93. package/docs/PERFORMANCE_IPHONE_306_OPTIMIZATION.md +76 -0
  94. package/docs/PERFORMANCE_SIMULATOR.md +89 -0
  95. package/docs/PERFORMANCE_SIMULATOR_INITIAL.md +96 -0
  96. package/docs/PERFORMANCE_SOAK_IPHONE_303.md +41 -0
  97. package/docs/UPSTREAM_NOTES.md +188 -0
  98. package/docs/assets/banner.svg +26 -0
  99. package/examples/webgpu-overlay.ts +147 -0
  100. package/ios/AVAssetTrackUtils.h +24 -0
  101. package/ios/AudioCompositionUtils.h +27 -0
  102. package/ios/AudioCompositionUtils.mm +113 -0
  103. package/ios/DecoderWindow.h +52 -0
  104. package/ios/NativeResourceStats.h +32 -0
  105. package/ios/RNSVColorSpace.h +50 -0
  106. package/ios/RNSVJSIUtils.h +46 -0
  107. package/ios/RNSVScopedPixelBuffer.h +17 -0
  108. package/ios/RNSVVideoPlayer.h +46 -0
  109. package/ios/RNSVVideoPlayer.mm +410 -0
  110. package/ios/ReactNativeSkiaVideo.h +8 -0
  111. package/ios/ReactNativeSkiaVideo.mm +310 -0
  112. package/ios/SeekPolicy.h +27 -0
  113. package/ios/VideoComposition.h +148 -0
  114. package/ios/VideoCompositionFramesExtractorHostObject.h +105 -0
  115. package/ios/VideoCompositionFramesExtractorHostObject.mm +686 -0
  116. package/ios/VideoCompositionFramesExtractorSyncHostObject.h +37 -0
  117. package/ios/VideoCompositionFramesExtractorSyncHostObject.mm +168 -0
  118. package/ios/VideoCompositionItemDecoder.h +60 -0
  119. package/ios/VideoCompositionItemDecoder.mm +480 -0
  120. package/ios/VideoEncoderHostObject.h +83 -0
  121. package/ios/VideoEncoderHostObject.mm +706 -0
  122. package/ios/VideoFrame.h +66 -0
  123. package/ios/VideoFrame.mm +108 -0
  124. package/ios/VideoPlayerHostObject.h +52 -0
  125. package/ios/VideoPlayerHostObject.mm +332 -0
  126. package/lib/module/NativeReactNativeSkiaVideo.js +5 -0
  127. package/lib/module/NativeReactNativeSkiaVideo.js.map +1 -0
  128. package/lib/module/RNSkiaVideoModule.js +20 -0
  129. package/lib/module/RNSkiaVideoModule.js.map +1 -0
  130. package/lib/module/canvas.js +18 -0
  131. package/lib/module/canvas.js.map +1 -0
  132. package/lib/module/exportCache.js +22 -0
  133. package/lib/module/exportCache.js.map +1 -0
  134. package/lib/module/exportFrames.js +338 -0
  135. package/lib/module/exportFrames.js.map +1 -0
  136. package/lib/module/exportQueue.js +52 -0
  137. package/lib/module/exportQueue.js.map +1 -0
  138. package/lib/module/exportTarget.js +137 -0
  139. package/lib/module/exportTarget.js.map +1 -0
  140. package/lib/module/exportVideoComposition.js +492 -0
  141. package/lib/module/exportVideoComposition.js.map +1 -0
  142. package/lib/module/frameInterop.js +532 -0
  143. package/lib/module/frameInterop.js.map +1 -0
  144. package/lib/module/gpu.js +222 -0
  145. package/lib/module/gpu.js.map +1 -0
  146. package/lib/module/gpuDevice.js +54 -0
  147. package/lib/module/gpuDevice.js.map +1 -0
  148. package/lib/module/index.js +36 -0
  149. package/lib/module/index.js.map +1 -0
  150. package/lib/module/memory.js +38 -0
  151. package/lib/module/memory.js.map +1 -0
  152. package/lib/module/package.json +1 -0
  153. package/lib/module/previewLifecycle.js +52 -0
  154. package/lib/module/previewLifecycle.js.map +1 -0
  155. package/lib/module/runtimeYield.js +10 -0
  156. package/lib/module/runtimeYield.js.map +1 -0
  157. package/lib/module/types.js +4 -0
  158. package/lib/module/types.js.map +1 -0
  159. package/lib/module/utils/useEventListener.js +18 -0
  160. package/lib/module/utils/useEventListener.js.map +1 -0
  161. package/lib/module/videoCompositionPlayer.js +368 -0
  162. package/lib/module/videoCompositionPlayer.js.map +1 -0
  163. package/lib/module/videoFrameImage.js +185 -0
  164. package/lib/module/videoFrameImage.js.map +1 -0
  165. package/lib/module/videoPlayer.js +172 -0
  166. package/lib/module/videoPlayer.js.map +1 -0
  167. package/lib/module/yuvBlit.js +299 -0
  168. package/lib/module/yuvBlit.js.map +1 -0
  169. package/lib/typescript/package.json +1 -0
  170. package/lib/typescript/src/NativeReactNativeSkiaVideo.d.ts +7 -0
  171. package/lib/typescript/src/NativeReactNativeSkiaVideo.d.ts.map +1 -0
  172. package/lib/typescript/src/RNSkiaVideoModule.d.ts +4 -0
  173. package/lib/typescript/src/RNSkiaVideoModule.d.ts.map +1 -0
  174. package/lib/typescript/src/canvas.d.ts +4 -0
  175. package/lib/typescript/src/canvas.d.ts.map +1 -0
  176. package/lib/typescript/src/exportCache.d.ts +6 -0
  177. package/lib/typescript/src/exportCache.d.ts.map +1 -0
  178. package/lib/typescript/src/exportFrames.d.ts +57 -0
  179. package/lib/typescript/src/exportFrames.d.ts.map +1 -0
  180. package/lib/typescript/src/exportQueue.d.ts +5 -0
  181. package/lib/typescript/src/exportQueue.d.ts.map +1 -0
  182. package/lib/typescript/src/exportTarget.d.ts +28 -0
  183. package/lib/typescript/src/exportTarget.d.ts.map +1 -0
  184. package/lib/typescript/src/exportVideoComposition.d.ts +68 -0
  185. package/lib/typescript/src/exportVideoComposition.d.ts.map +1 -0
  186. package/lib/typescript/src/frameInterop.d.ts +17 -0
  187. package/lib/typescript/src/frameInterop.d.ts.map +1 -0
  188. package/lib/typescript/src/gpu.d.ts +39 -0
  189. package/lib/typescript/src/gpu.d.ts.map +1 -0
  190. package/lib/typescript/src/gpuDevice.d.ts +11 -0
  191. package/lib/typescript/src/gpuDevice.d.ts.map +1 -0
  192. package/lib/typescript/src/index.d.ts +21 -0
  193. package/lib/typescript/src/index.d.ts.map +1 -0
  194. package/lib/typescript/src/memory.d.ts +25 -0
  195. package/lib/typescript/src/memory.d.ts.map +1 -0
  196. package/lib/typescript/src/previewLifecycle.d.ts +22 -0
  197. package/lib/typescript/src/previewLifecycle.d.ts.map +1 -0
  198. package/lib/typescript/src/runtimeYield.d.ts +4 -0
  199. package/lib/typescript/src/runtimeYield.d.ts.map +1 -0
  200. package/lib/typescript/src/types.d.ts +631 -0
  201. package/lib/typescript/src/types.d.ts.map +1 -0
  202. package/lib/typescript/src/utils/useEventListener.d.ts +5 -0
  203. package/lib/typescript/src/utils/useEventListener.d.ts.map +1 -0
  204. package/lib/typescript/src/videoCompositionPlayer.d.ts +105 -0
  205. package/lib/typescript/src/videoCompositionPlayer.d.ts.map +1 -0
  206. package/lib/typescript/src/videoFrameImage.d.ts +60 -0
  207. package/lib/typescript/src/videoFrameImage.d.ts.map +1 -0
  208. package/lib/typescript/src/videoPlayer.d.ts +99 -0
  209. package/lib/typescript/src/videoPlayer.d.ts.map +1 -0
  210. package/lib/typescript/src/yuvBlit.d.ts +50 -0
  211. package/lib/typescript/src/yuvBlit.d.ts.map +1 -0
  212. package/package.json +155 -4
  213. package/patches/react-native-skia+3.7.0.patch +502 -0
  214. package/react-native-skia-webgpu-video.podspec +35 -0
  215. package/scripts/apply-skia-canvas-patch.mjs +48 -0
  216. package/scripts/skia-canvas-patch.json +291 -0
  217. package/src/NativeReactNativeSkiaVideo.ts +8 -0
  218. package/src/RNSkiaVideoModule.ts +27 -0
  219. package/src/canvas.ts +22 -0
  220. package/src/exportCache.ts +27 -0
  221. package/src/exportFrames.ts +421 -0
  222. package/src/exportQueue.ts +65 -0
  223. package/src/exportTarget.ts +179 -0
  224. package/src/exportVideoComposition.ts +718 -0
  225. package/src/frameInterop.ts +684 -0
  226. package/src/gpu.ts +290 -0
  227. package/src/gpuDevice.ts +80 -0
  228. package/src/index.tsx +45 -0
  229. package/src/memory.ts +48 -0
  230. package/src/previewLifecycle.ts +60 -0
  231. package/src/runtimeYield.ts +6 -0
  232. package/src/types.ts +661 -0
  233. package/src/utils/useEventListener.ts +22 -0
  234. package/src/videoCompositionPlayer.ts +568 -0
  235. package/src/videoFrameImage.ts +205 -0
  236. package/src/videoPlayer.ts +318 -0
  237. package/src/yuvBlit.ts +331 -0
@@ -0,0 +1,79 @@
1
+ #include "RNSVHostObject.h"
2
+
3
+ #include <utility>
4
+
5
+ namespace RNSkiaVideo {
6
+
7
+ RNSVHostObject::~RNSVHostObject() {
8
+ std::lock_guard<std::mutex> lock(cacheMutex);
9
+ for (auto& entry : caches) {
10
+ RuntimeLifecycleMonitor::removeListener(*entry.first, this);
11
+ }
12
+ caches.clear();
13
+ }
14
+
15
+ void RNSVHostObject::onRuntimeDestroyed(jsi::Runtime* runtime) {
16
+ // The runtime is going away: release its values now, while it is still
17
+ // able to invalidate them.
18
+ std::lock_guard<std::mutex> lock(cacheMutex);
19
+ caches.erase(runtime);
20
+ }
21
+
22
+ RNSVHostObject::RuntimeCache& RNSVHostObject::cacheFor(jsi::Runtime& runtime) {
23
+ auto entry = caches.find(&runtime);
24
+ if (entry == caches.end()) {
25
+ RuntimeLifecycleMonitor::addListener(runtime, shared_from_this());
26
+ entry = caches.emplace(&runtime, RuntimeCache{}).first;
27
+ }
28
+ return entry->second;
29
+ }
30
+
31
+ jsi::Value RNSVHostObject::getFunction(jsi::Runtime& runtime,
32
+ const std::string& name,
33
+ unsigned int paramCount,
34
+ jsi::HostFunctionType&& function) {
35
+ std::lock_guard<std::mutex> lock(cacheMutex);
36
+ auto& cache = cacheFor(runtime);
37
+ auto entry = cache.values.find(name);
38
+ if (entry == cache.values.end()) {
39
+ std::weak_ptr<RNSVHostObject> owner = shared_from_this();
40
+ auto jsFunction = jsi::Function::createFromHostFunction(
41
+ runtime, jsi::PropNameID::forAscii(runtime, name), paramCount,
42
+ [owner, function = std::move(function)](
43
+ jsi::Runtime& rt, const jsi::Value& self,
44
+ const jsi::Value* arguments, size_t count) {
45
+ // The function may have been extracted from its HostObject. Keep
46
+ // that owner alive through the call without creating a cache cycle.
47
+ auto retained = owner.lock();
48
+ if (!retained) throw jsi::JSError(rt, "Video resource is no longer alive");
49
+ return function(rt, self, arguments, count);
50
+ });
51
+ entry = cache.values.emplace(name, jsi::Value(runtime, jsFunction)).first;
52
+ }
53
+ return jsi::Value(runtime, entry->second);
54
+ }
55
+
56
+ jsi::Value RNSVHostObject::getVersionedObject(
57
+ jsi::Runtime& runtime, const std::string& key, double version,
58
+ const std::function<void(jsi::Object&)>& fill) {
59
+ std::lock_guard<std::mutex> lock(cacheMutex);
60
+ auto& cache = cacheFor(runtime);
61
+ auto entry = cache.values.find(key);
62
+ auto versionEntry = cache.versions.find(key);
63
+ if (entry == cache.values.end() || versionEntry == cache.versions.end() ||
64
+ versionEntry->second != version) {
65
+ // A fresh object for each version. Filled in place, the previous one kept
66
+ // the properties the new content no longer has: the frame of an item
67
+ // whose decoder had closed stayed in the frames object, with a texture
68
+ // deleted since, and drawing it after a seek back into the item threw
69
+ // from the frame callback (Android) or showed a stale picture (iOS).
70
+ jsi::Object object(runtime);
71
+ fill(object);
72
+ entry = cache.values.insert_or_assign(key, jsi::Value(runtime, object))
73
+ .first;
74
+ cache.versions[key] = version;
75
+ }
76
+ return jsi::Value(runtime, entry->second);
77
+ }
78
+
79
+ } // namespace RNSkiaVideo
@@ -0,0 +1,67 @@
1
+ #pragma once
2
+
3
+ #include <functional>
4
+ #include <jsi/jsi.h>
5
+ #include <mutex>
6
+ #include <string>
7
+ #include <unordered_map>
8
+
9
+ #include "RNSVRuntimeLifecycleMonitor.h"
10
+
11
+ namespace RNSkiaVideo {
12
+ using namespace facebook;
13
+
14
+ /**
15
+ * jsi::HostObject that memoizes, per runtime, the JS objects it hands out.
16
+ *
17
+ * `jsi::Function::createFromHostFunction` allocates a JS function object and
18
+ * a native closure on every call. The frame loops of this library read
19
+ * `decodeNextFrame`, `decodeCompositionFrames`, `encodeFrame`… on the UI or
20
+ * export runtime at every vsync, so creating those functions on each property
21
+ * access adds a couple of garbage collected objects per frame for nothing.
22
+ * Subclasses create each function once per runtime through `getFunction`.
23
+ *
24
+ * Cached values are dropped when their runtime is torn down (dev reload), so
25
+ * no jsi::Value ever outlives its runtime.
26
+ */
27
+ class RNSVHostObject : public jsi::HostObject, public RuntimeLifecycleListener,
28
+ public std::enable_shared_from_this<RNSVHostObject> {
29
+ public:
30
+ ~RNSVHostObject() override;
31
+ void onRuntimeDestroyed(jsi::Runtime* runtime) override;
32
+
33
+ protected:
34
+ /**
35
+ * Returns the host function `name` of this object for `runtime`, creating
36
+ * it with `function` the first time only.
37
+ */
38
+ jsi::Value getFunction(jsi::Runtime& runtime, const std::string& name,
39
+ unsigned int paramCount,
40
+ jsi::HostFunctionType&& function);
41
+
42
+ /**
43
+ * Returns the object cached under `key` for `runtime`. A new object is made
44
+ * and filled through `fill` only when `version` differs from the version of
45
+ * the cached one, so it holds exactly what `fill` sets. Callers keep a
46
+ * monotonic version of the content they expose, so an unchanged content
47
+ * costs no allocation.
48
+ */
49
+ jsi::Value
50
+ getVersionedObject(jsi::Runtime& runtime, const std::string& key,
51
+ double version,
52
+ const std::function<void(jsi::Object&)>& fill);
53
+
54
+ private:
55
+ struct RuntimeCache {
56
+ std::unordered_map<std::string, jsi::Value> values;
57
+ std::unordered_map<std::string, double> versions;
58
+ };
59
+ std::mutex cacheMutex;
60
+ std::unordered_map<jsi::Runtime*, RuntimeCache> caches;
61
+
62
+ // Must be called with cacheMutex held; registers the runtime listener on
63
+ // the first use of a runtime.
64
+ RuntimeCache& cacheFor(jsi::Runtime& runtime);
65
+ };
66
+
67
+ } // namespace RNSkiaVideo
@@ -0,0 +1,68 @@
1
+ #include "RNSVMemoryBudget.h"
2
+ #include "RNSVRuntimeLifecycleMonitor.h"
3
+ #include <cmath>
4
+ #include <jsi/jsi.h>
5
+
6
+ namespace RNSkiaVideo {
7
+ namespace {
8
+ class BudgetRuntimeListener final : public RuntimeLifecycleListener {
9
+ public:
10
+ void onRuntimeDestroyed(facebook::jsi::Runtime* runtime) override {
11
+ MemoryBudget::instance().releaseOwner(runtime);
12
+ }
13
+ };
14
+ const auto runtimeListener = std::make_shared<BudgetRuntimeListener>();
15
+ uint64_t positiveInteger(facebook::jsi::Runtime& rt,
16
+ const facebook::jsi::Value& value) {
17
+ if (!value.isNumber()) throw facebook::jsi::JSError(rt, "Expected an integer");
18
+ const auto number = value.asNumber();
19
+ if (!std::isfinite(number) || number < 1 ||
20
+ number > 9007199254740991.0 || std::floor(number) != number) {
21
+ throw facebook::jsi::JSError(rt, "Expected a positive safe integer");
22
+ }
23
+ return static_cast<uint64_t>(number);
24
+ }
25
+ }
26
+
27
+ void MemoryBudget::install(facebook::jsi::Runtime& runtime,
28
+ facebook::jsi::Object& module) {
29
+ namespace jsi = facebook::jsi;
30
+ RuntimeLifecycleMonitor::addListener(runtime, runtimeListener);
31
+ module.setProperty(runtime, "frameProtocolVersion", 1);
32
+ module.setProperty(runtime, "reserveMemory", jsi::Function::createFromHostFunction(
33
+ runtime, jsi::PropNameID::forAscii(runtime, "reserveMemory"), 2,
34
+ [](jsi::Runtime& rt, const jsi::Value&, const jsi::Value* args, size_t count) {
35
+ if (count < 1) throw jsi::JSError(rt, "Missing memory size");
36
+ RuntimeLifecycleMonitor::addListener(rt, runtimeListener);
37
+ const auto bytes = positiveInteger(rt, args[0]);
38
+ const auto label = count > 1 && args[1].isString()
39
+ ? args[1].asString(rt).utf8(rt) : "video resource";
40
+ return jsi::Value(static_cast<double>(instance().reserve(bytes, label, &rt)));
41
+ }));
42
+ module.setProperty(runtime, "releaseMemory", jsi::Function::createFromHostFunction(
43
+ runtime, jsi::PropNameID::forAscii(runtime, "releaseMemory"), 1,
44
+ [](jsi::Runtime& rt, const jsi::Value&, const jsi::Value* args, size_t count) {
45
+ if (count < 1) throw jsi::JSError(rt, "Missing reservation");
46
+ instance().release(positiveInteger(rt, args[0]));
47
+ return jsi::Value::undefined();
48
+ }));
49
+ module.setProperty(runtime, "configureMemoryBudget", jsi::Function::createFromHostFunction(
50
+ runtime, jsi::PropNameID::forAscii(runtime, "configureMemoryBudget"), 1,
51
+ [](jsi::Runtime& rt, const jsi::Value&, const jsi::Value* args, size_t count) {
52
+ if (count < 1) throw jsi::JSError(rt, "Missing memory limit");
53
+ instance().configure(positiveInteger(rt, args[0]));
54
+ return jsi::Value::undefined();
55
+ }));
56
+ module.setProperty(runtime, "getMemoryBudgetStats", jsi::Function::createFromHostFunction(
57
+ runtime, jsi::PropNameID::forAscii(runtime, "getMemoryBudgetStats"), 0,
58
+ [](jsi::Runtime& rt, const jsi::Value&, const jsi::Value*, size_t) {
59
+ const auto stats = instance().snapshot();
60
+ jsi::Object result(rt);
61
+ result.setProperty(rt, "currentBytes", static_cast<double>(stats.currentBytes));
62
+ result.setProperty(rt, "peakBytes", static_cast<double>(stats.peakBytes));
63
+ result.setProperty(rt, "allocations", static_cast<double>(stats.allocations));
64
+ result.setProperty(rt, "maxBytes", static_cast<double>(stats.maxBytes));
65
+ return jsi::Value(std::move(result));
66
+ }));
67
+ }
68
+ } // namespace RNSkiaVideo
@@ -0,0 +1,100 @@
1
+ #pragma once
2
+
3
+ #include <cstdint>
4
+ #include <limits>
5
+ #include <mutex>
6
+ #include <stdexcept>
7
+ #include <string>
8
+ #include <unordered_map>
9
+
10
+ namespace facebook::jsi { class Runtime; class Object; }
11
+
12
+ namespace RNSkiaVideo {
13
+
14
+ // Shared by all worklet runtimes. This accounts owned resources, not the
15
+ // opaque caches allocated internally by the graphics driver or media codec.
16
+ class MemoryBudget {
17
+ public:
18
+ struct Snapshot {
19
+ uint64_t currentBytes, peakBytes, allocations, maxBytes;
20
+ };
21
+ static MemoryBudget& instance() {
22
+ static MemoryBudget budget;
23
+ return budget;
24
+ }
25
+ uint64_t reserve(uint64_t bytes, const std::string& label,
26
+ const void* owner = nullptr) {
27
+ std::lock_guard<std::mutex> lock(mutex);
28
+ if (bytes == 0 || bytes > maxBytes || currentBytes > maxBytes - bytes) {
29
+ throw std::runtime_error("Video memory budget exceeded: " + label);
30
+ }
31
+ if (nextToken >= 9007199254740991ULL) {
32
+ throw std::runtime_error("Video memory reservation identifiers exhausted");
33
+ }
34
+ const auto token = nextToken++;
35
+ reservations.emplace(token, Reservation{bytes, owner});
36
+ currentBytes += bytes;
37
+ if (currentBytes > peakBytes) peakBytes = currentBytes;
38
+ return token;
39
+ }
40
+ void release(uint64_t token) {
41
+ std::lock_guard<std::mutex> lock(mutex);
42
+ const auto found = reservations.find(token);
43
+ if (found == reservations.end()) return;
44
+ currentBytes -= found->second.bytes;
45
+ reservations.erase(found);
46
+ }
47
+ void resize(uint64_t token, uint64_t bytes, const std::string& label) {
48
+ std::lock_guard<std::mutex> lock(mutex);
49
+ auto found = reservations.find(token);
50
+ if (found == reservations.end()) throw std::runtime_error("Unknown video memory reservation");
51
+ auto remaining = currentBytes - found->second.bytes;
52
+ if (bytes == 0 || bytes > maxBytes || remaining > maxBytes - bytes)
53
+ throw std::runtime_error("Video memory budget exceeded: " + label);
54
+ found->second.bytes = bytes;
55
+ currentBytes = remaining + bytes;
56
+ if (currentBytes > peakBytes) peakBytes = currentBytes;
57
+ }
58
+ void releaseOwner(const void* owner) {
59
+ std::lock_guard<std::mutex> lock(mutex);
60
+ for (auto it = reservations.begin(); it != reservations.end();) {
61
+ if (it->second.owner == owner) {
62
+ currentBytes -= it->second.bytes;
63
+ it = reservations.erase(it);
64
+ } else ++it;
65
+ }
66
+ }
67
+ void configure(uint64_t bytes) {
68
+ std::lock_guard<std::mutex> lock(mutex);
69
+ if (bytes == 0 || bytes < currentBytes) {
70
+ throw std::runtime_error("Video memory limit is below current reservations");
71
+ }
72
+ maxBytes = bytes;
73
+ }
74
+ Snapshot snapshot() {
75
+ std::lock_guard<std::mutex> lock(mutex);
76
+ return {currentBytes, peakBytes, reservations.size(), maxBytes};
77
+ }
78
+ static void install(facebook::jsi::Runtime&, facebook::jsi::Object&);
79
+
80
+ private:
81
+ struct Reservation { uint64_t bytes; const void* owner; };
82
+ std::mutex mutex;
83
+ std::unordered_map<uint64_t, Reservation> reservations;
84
+ uint64_t nextToken = 1;
85
+ uint64_t currentBytes = 0, peakBytes = 0;
86
+ uint64_t maxBytes = 256ULL * 1024 * 1024;
87
+ };
88
+
89
+ // Use for C++ owned buffers. Reservations are returned even on exceptions.
90
+ class MemoryReservation {
91
+ public:
92
+ MemoryReservation(uint64_t bytes, const std::string& label)
93
+ : token(MemoryBudget::instance().reserve(bytes, label)) {}
94
+ ~MemoryReservation() { MemoryBudget::instance().release(token); }
95
+ MemoryReservation(const MemoryReservation&) = delete;
96
+ MemoryReservation& operator=(const MemoryReservation&) = delete;
97
+ private:
98
+ uint64_t token;
99
+ };
100
+ } // namespace RNSkiaVideo
@@ -0,0 +1,88 @@
1
+ #include "RNSVRuntimeLifecycleMonitor.h"
2
+
3
+ #include <algorithm>
4
+ #include <memory>
5
+ #include <mutex>
6
+ #include <unordered_map>
7
+ #include <vector>
8
+ #include <utility>
9
+
10
+ namespace RNSkiaVideo {
11
+
12
+ struct ListenerRegistration {
13
+ RuntimeLifecycleListener* identity;
14
+ std::weak_ptr<RuntimeLifecycleListener> listener;
15
+ };
16
+ static std::unordered_map<jsi::Runtime*, std::vector<ListenerRegistration>> listeners;
17
+ static std::mutex listenersMutex;
18
+
19
+ struct RuntimeLifecycleMonitorObject : public jsi::HostObject {
20
+ jsi::Runtime* runtime;
21
+ explicit RuntimeLifecycleMonitorObject(jsi::Runtime* runtime)
22
+ : runtime(runtime) {}
23
+ ~RuntimeLifecycleMonitorObject() override {
24
+ std::vector<ListenerRegistration> registrations;
25
+ {
26
+ std::lock_guard<std::mutex> lock(listenersMutex);
27
+ auto entry = listeners.find(runtime);
28
+ if (entry != listeners.end()) {
29
+ registrations = std::move(entry->second);
30
+ listeners.erase(entry);
31
+ }
32
+ }
33
+ // A listener's final reference can unregister it. Release the registry
34
+ // mutex before locking or destroying those references.
35
+ for (const auto& registration : registrations) {
36
+ if (auto listener = registration.listener.lock()) {
37
+ listener->onRuntimeDestroyed(runtime);
38
+ }
39
+ }
40
+ }
41
+ };
42
+
43
+ void RuntimeLifecycleMonitor::addListener(jsi::Runtime& runtime,
44
+ const std::shared_ptr<RuntimeLifecycleListener>& listener) {
45
+ bool installMonitor = false;
46
+ {
47
+ std::lock_guard<std::mutex> lock(listenersMutex);
48
+ auto entry = listeners.find(&runtime);
49
+ if (entry == listeners.end()) {
50
+ listeners.emplace(&runtime, std::vector<ListenerRegistration>{
51
+ {listener.get(), listener}});
52
+ installMonitor = true;
53
+ } else {
54
+ const auto identity = listener.get();
55
+ auto& entries = entry->second;
56
+ entries.erase(std::remove_if(entries.begin(), entries.end(),
57
+ [](const auto& item) {
58
+ return item.listener.expired();
59
+ }), entries.end());
60
+ bool registered = false;
61
+ for (const auto& item : entries) registered |= item.identity == identity;
62
+ if (!registered) entries.push_back({identity, listener});
63
+ }
64
+ }
65
+ if (installMonitor) {
66
+ // The global object owns the monitor: when the runtime is torn down the
67
+ // monitor is destroyed with it and notifies the listeners.
68
+ runtime.global().setProperty(
69
+ runtime, "__rnskv_rt_lifecycle_monitor",
70
+ jsi::Object::createFromHostObject(
71
+ runtime, std::make_shared<RuntimeLifecycleMonitorObject>(&runtime)));
72
+ }
73
+ }
74
+
75
+ void RuntimeLifecycleMonitor::removeListener(
76
+ jsi::Runtime& runtime, RuntimeLifecycleListener* listener) {
77
+ std::lock_guard<std::mutex> lock(listenersMutex);
78
+ auto entry = listeners.find(&runtime);
79
+ if (entry != listeners.end()) {
80
+ auto& entries = entry->second;
81
+ entries.erase(std::remove_if(entries.begin(), entries.end(),
82
+ [listener](const auto& item) {
83
+ return item.identity == listener;
84
+ }), entries.end());
85
+ }
86
+ }
87
+
88
+ } // namespace RNSkiaVideo
@@ -0,0 +1,33 @@
1
+ #pragma once
2
+
3
+ #include <jsi/jsi.h>
4
+ #include <memory>
5
+
6
+ namespace RNSkiaVideo {
7
+
8
+ namespace jsi = facebook::jsi;
9
+
10
+ /**
11
+ * Gets notified right before a jsi::Runtime is torn down. Anything that keeps
12
+ * jsi values of that runtime alive (see RNSVHostObject) has to drop them at
13
+ * that point: a jsi::Value destroyed after its runtime is undefined behaviour.
14
+ */
15
+ struct RuntimeLifecycleListener {
16
+ virtual ~RuntimeLifecycleListener() {}
17
+ virtual void onRuntimeDestroyed(jsi::Runtime* runtime) = 0;
18
+ };
19
+
20
+ /**
21
+ * Runtime destruction is observed through a host object installed on the
22
+ * runtime's global object: its destructor runs while the runtime is being
23
+ * torn down. Listeners must be added from the runtime's own thread; they can
24
+ * be removed from any thread.
25
+ */
26
+ struct RuntimeLifecycleMonitor {
27
+ static void addListener(jsi::Runtime& runtime,
28
+ const std::shared_ptr<RuntimeLifecycleListener>& listener);
29
+ static void removeListener(jsi::Runtime& runtime,
30
+ RuntimeLifecycleListener* listener);
31
+ };
32
+
33
+ } // namespace RNSkiaVideo
@@ -0,0 +1,95 @@
1
+ # Android physical-device diagnostics — Skia 3.0.6
2
+
3
+ Latest follow-up (2026-10-08): the export-worklet fix passed two comparison workloads and four consecutive full montages on Pixel 8a. Two-montage idle PSS fell from 737.83 to 577.20 MiB; the fourth extended-run settled PSS decreased from 603.57 to 593.80 MiB. See [current memory results](NATIVE_MEMORY_INVESTIGATION_306.md). The frozen transport campaigns below retain their original binaries and results.
4
+
5
+ The earlier AHardwareBuffer Release transport checkpoint passed all 24 focused physical-device attempts on a Google Pixel 8a (Android 17, API 37): three repetitions of eight playback/export scenarios, including H264/HEVC full 4K in copy and direct modes. Tracked owned bytes and resource reservations returned to zero after each strict two-second cleanup. The earlier CPU failures are preserved below. This qualifies these focused operations, not the full instrumentation suite or permanent leak absence.
6
+
7
+ [Machine-readable evidence](../benchmark/android-device-306-verification.json) preserves per-attempt status, raw-file hashes, outstanding reservations and binary/source fingerprints. Raw reports stay local and ignored because they include device identifiers. This is a candidate-only diagnostic campaign, without a valid old-library A/B comparison or a cross-platform performance comparison.
8
+
9
+ ## Current AHardwareBuffer checkpoint
10
+
11
+ Release APK SHA-256 `59afb795cfc9fe86d5bbbbaaa487cf0129130dd6538d0f5faf5d6bc11a6edbfa`; source snapshot `35c1f59a69c1a2d6cb04170fed083e3c6f81c51cd94efc35a490181cdd06ee43` over 123 preserved source files. The binary uses the same RN/Skia/WebGPU versions, fixtures, seed, original resolutions, 512 MiB quota, 720 × 720 physical-pixel preview and 640 × 360 exports as the diagnostic workload. The app was restarted for every attempt, producing 24 distinct processes. All 986 native samples reported nominal thermal state, normal power mode and active foreground lifecycle; zero samples were dropped. Before the additional repetitions, the phone was AC powered, at 100% battery and 31.1 °C.
12
+
13
+ | Case | Strict passes | Median peak RSS (MiB) | Median peak PSS (MiB) |
14
+ | --- | ---: | ---: | ---: |
15
+ | H264 1080p copy | 3/3 | 337.676 | 308.423 |
16
+ | H264 4K copy | 3/3 | 408.977 | 550.610 |
17
+ | HEVC 4K copy | 3/3 | 408.812 | 541.787 |
18
+ | H264 1080p direct | 3/3 | 337.691 | 307.981 |
19
+ | H264 4K direct | 3/3 | 409.098 | 553.601 |
20
+ | HEVC 4K direct | 3/3 | 408.762 | 511.776 |
21
+ | H264 export copy | 3/3 | 309.879 | 226.266 |
22
+ | H264 export direct | 3/3 | 309.641 | 226.619 |
23
+
24
+ RSS/PSS peaks are sampled independently. Owned bytes and reservations were zero in every final settled sample; decoder-count and in-flight-frame collectors remain unavailable. Settled physical memory, per-attempt peaks and raw hashes remain in the machine-readable report. Returning owned reservations to zero does not establish zero driver caches or the absence of leaks during longer use.
25
+
26
+ The 4K callback cadences were 45.442/s for H264 copy, 42.795/s for H264 direct, 44.578/s for HEVC copy and 43.352/s for HEVC direct. The 1080p values were 57.707/s and 58.088/s. All callback-duration p95 upper histogram bounds were ≤ 0.25 ms. These are CPU callback diagnostics, excluding preload/import and GPU completion; they are not decoded or presented FPS. Frame availability was present at every draw; decoder drops remain unknown.
27
+
28
+ All six exports produced 60 decoded frames at 640 × 360, H264, 30 fps, with exact monotonic presentation timestamps from zero through 59/30 seconds and progress 60/60. Median full pipeline time (`encodeMs`) was 2354.867 ms in copy mode and 2227.471 ms in direct mode; stat/probe time was 386.236/370.346 ms, and pipeline plus probe/removal (`exportMs`) was 2819.146/2767.245 ms. Generic export pixel/audio validators remain unavailable.
29
+
30
+ A separate functional CPU/AHB oracle passed 10/10 cases across padded dimensions, portrait, 90° rotation, H264 full 4K and HEVC full 4K, each in copy/direct mode. Two decoded timestamps per case compared 66,560 sampled RGBA channels with maximum error zero. The first raw frame remained readable and importable after later decode and producer teardown. Six targeted native AHB ownership, finish/abort, quota and read-validation tests passed with zero skips in 0.168 s; 43 JVM tests also passed. The oracle uses readback and is excluded from performance measurements. It validates sampled SDR patches, not whole-image fidelity, HDR or audio.
31
+
32
+ Decode renders the codec texture into immutable AHardwareBuffer storage, completes the producer GL fence synchronously, and imports it through the shared WebGPU device before an owned GPU snapshot. Normal AHB rendering performs no RGBA CPU readback or full-frame CPU copy. Export continues through CPU RGBA upload. End-to-end zero-copy, complete GPU memory, a 256 MiB quota and long-duration stability have not been qualified. The one-attempt CPU scoped-copy reference uses a different binary and is insufficient for a controlled A/B improvement claim.
33
+
34
+ ## Current AHardwareBuffer lifecycle follow-up
35
+
36
+ Four additional existing catalog cases each passed once on the same frozen Release APK, including their strict cleanup criterion: `paused-clock-and-resume`, `closed-item-leaves-frame-map`, `held-image-survives-seek`, and `4k-x8-lazy-direct`. Their 1267 native samples were all nominal/normal/active with zero samples dropped. The paused clock moved by zero during the 250 ms pause and advanced by 0.425587 s after resume; frame-map pruning removed clips 0/1 and retained only clip 2.
37
+
38
+ The case named `held-image-survives-seek` performed a paused seek and 2.5 s wait followed by five seeks. It does not retain and compare an independent SkImage, and its immutability pixel validator remains unavailable. Its pass establishes operation completion and cleanup, while the separate CPU/AHB oracle above validates sampled pixels on a retained raw frame after producer teardown. Paused seeks use clock/visible-item-ID checks, with native presentation-timestamp identity unavailable.
39
+
40
+ The lazy/direct montage used eight sequential full-resolution H264/HEVC 4K clips and completed play, 10 seeks, 81 scrub requests, loop, export, 25 churn cycles, a callback diagnostic and 40 remount cycles in 144.242 s. Its export contained 360 decoded 1080 × 1080 H264 frames at 30 fps over 12 s, with exact monotonic PTS from zero through 359/30 s and progress 360/360. Generic montage pixel/audio validators were not supplied.
41
+
42
+ This montage had a substantially higher physical memory cost than the simple playback cases: peak RSS **800.250 MiB**, peak PSS **1037.908 MiB** and peak tracked owned bytes **257.080 MiB**. Final settled RSS remained **785.816 MiB**, although tracked owned bytes and reservations returned to zero. The physical retention is not attributed to a particular driver/cache/VM component. One completed set of 25 churn/40 remount cycles establishes neither a memory plateau nor endurance or leak absence. The 256 MiB quota and eager/concurrent pressure remain unqualified.
43
+
44
+ Five existing JNI CPU-fallback ownership/refusal tests passed again on the current instrumentation artifact with zero skips in 0.896 s, separately from the six AHB tests. The benchmark app was force-stopped after testing to release KEEP_SCREEN_ON; no system settings were changed, and no owned test runner or sampler remains active.
45
+
46
+ The subsequent [Android cache-retention investigation](ANDROID_MEMORY_RETENTION_306.md) separates process memory categories and measures Graphite caches with a diagnostic build. It identifies reclaimable cache retention and a separate upstream reference-count defect. These experiments do not change the production qualification above or establish that the remaining process memory is leak-free.
47
+
48
+ The [export-cache follow-up](ANDROID_EXPORT_CACHE_306.md) records the later production changes and their own binary identity, pixel checks and repeated montage results. Keep those measurements separate from this original AHB checkpoint.
49
+
50
+ ## Historical CPU diagnostic checkpoint
51
+
52
+ The prior CPU implementation completed all six 1080p and six export operations, but its twelve full-resolution 4K attempts failed controlled ART allocation; every attempt failed strict cleanup. Neither quota nor cleanup thresholds were relaxed.
53
+
54
+ ## Historical CPU workload and identity
55
+
56
+ - React Native 0.86.2, Skia 3.0.6, WebGPU 0.12.1; arm64-v8a Release on a physical Pixel 8a.
57
+ - Candidate APK SHA-256 `352f728d99ab6b69e75b732580c3cab12c8905bebe31f400f6afc8f399e7e08e`; bundle `fa577288aa58d4beecb8aa71ce8b51f14ceb1eee84f17942043461ea786d5c22`; source snapshot `82ef4f57aca124e1725649f314cdf7ee425442f261894ce54e89587c78cb9b97` (source bytes correspond to commit `2ab84c8`).
58
+ - Catalog `2026-10-07.1`, identical seed, eight cases with three independent cold processes each. Each raw one-case execution has repetition zero; the external campaign records repetitions zero through two separately.
59
+ - Preview: 720 × 720 physical pixels, four seconds, original uncapped 1080p or 4K H264/HEVC inputs, no seeks. Export: two seconds, 60 frames, 640 × 360 H264 at 30 fps and a requested 2 Mbit/s.
60
+ - Library memory budget: 512 MiB. All 911 native samples reported nominal thermal state, normal power mode and active foreground lifecycle. No collection samples were dropped. The phone was AC powered; battery temperature before the campaign was 37.7 °C.
61
+
62
+ Both requested copy and direct modes use the historical CPU RGBA readback/upload fallback. The label “direct” does not establish a zero-copy implementation. The Graphite import uses a reusable WebGPU texture and an owned GPU snapshot rather than creating a new cached raster upload for each frame.
63
+
64
+ ## Recorded results
65
+
66
+ Peaks below are medians over three attempts. RSS and PSS are sampled independently, so their peak values must not be divided or interpreted as simultaneous memory categories.
67
+
68
+ | Case | Completed operations | Strict passes | Peak RSS (MiB) | Peak PSS (MiB) |
69
+ | --- | ---: | ---: | ---: | ---: |
70
+ | H264 1080p copy | 3/3 | 0/3 | 612.336 | 563.594 |
71
+ | H264 4K copy | 0/3 | 0/3 | 720.227 | 754.432 |
72
+ | HEVC 4K copy | 0/3 | 0/3 | 724.922 | 780.497 |
73
+ | H264 1080p direct | 3/3 | 0/3 | 607.219 | 567.692 |
74
+ | H264 4K direct | 0/3 | 0/3 | 744.273 | 768.242 |
75
+ | HEVC 4K direct | 0/3 | 0/3 | 744.051 | 754.546 |
76
+ | H264 export copy | 3/3 | 0/3 | 365.293 | 280.858 |
77
+ | H264 export direct | 3/3 | 0/3 | 363.887 | 279.066 |
78
+
79
+ Completed 1080p operations recorded median callback cadences of 56.885/s (copy) and 55.820/s (direct), with the callback-duration p95 in the ≤ 0.25 ms histogram bucket. These are callback diagnostics, not decoded-video FPS, presented FPS or GPU duration. Callback duration excludes frame preload/upload. Zero missing-frame availability does not establish zero dropped decoder frames; decoder drops and complete GPU timing remain unavailable.
80
+
81
+ All six exports produced 60 decoded frames at 640 × 360, H264, 30 fps, with exact monotonic presentation timestamps from zero through 59/30 seconds. Generic pixel and audio validation remain unavailable. Median full export pipeline time (`encodeMs`) was 2168.225 ms (copy) and 2100.074 ms (direct); the separate stat/probe time was 360.318/352.350 ms. Pipeline plus probe and output removal (`exportMs`) was 2612.350/2547.482 ms. `elapsedMs` additionally includes pre-export settling and is not an export-duration metric.
82
+
83
+ Outstanding native backing reservations remain visible in every settled record. The two-second cleanup failure alone cannot distinguish deferred Java/Hermes collection from a permanent leak. Decoder-count and in-flight-frame collectors are unavailable and are not reported as zero. Android native-heap PSS is not allocator heap size; complete process GPU allocation and iOS-style physical footprint are unavailable.
84
+
85
+ ## Bounded allocation retry follow-up
86
+
87
+ A separate APK (`a883499979179fa6c17ae4887a6eafd4d411043ac40a4a5a75f689f2e39d8924`) retries only the backing allocation once after draining already queued dead phantom references, with the same owner and reservation. It never forces GC, recycles live aliases, increases the quota or reduces the decode resolution.
88
+
89
+ A single full-resolution H264 4K attempt still failed with a controlled allocation error after two attempts and failed strict cleanup. No repeated performance campaign was launched for that unsuccessful recovery. The corresponding 37 JVM tests and five physical JNI ownership/refusal tests passed. These unit results do not qualify 4K playback or whole-application cleanup.
90
+
91
+ The subsequent scoped-copy APK (`789735e095937cc168865986a353613d29f065feeff40bca8676ab0895b2ef0a`, source snapshot `0a842f1f6f02ca4bb6f274050adcb15897b2cbc78aa76770b1aa94171ccfca3f`) completed one attempt for each of the same eight cases without OOM, including all four full-resolution 4K copy/direct cases and both exports. All eight still failed strict two-second cleanup. Five JNI ownership tests passed again (0.572 s). This establishes completion of these bounded operations; it is not a complete memory qualification or a repeated performance comparison. The extra CPU copy and per-attempt physical memory remain visible in the machine-readable follow-up. The later AHardwareBuffer result above is a separate binary and campaign; these historical CPU failures remain unchanged.
92
+
93
+ ## Native instrumentation scope
94
+
95
+ The current inventory contains the original 40 named cases / 86 parameterized executions plus ownership additions, for 52 named cases / 102 planned executions. The full current suite has not passed. Six AHB ownership tests passed on the current AHB checkpoint. The five CPU ownership/refusal tests passed on the earlier scoped-copy checkpoint. Before the ART guard change, 15 focused decode/export tests passed; a separate earlier full-suite attempt completed two passes, failed one lazy-4K cadence assertion and crashed during one eager eight-4K allocation, leaving 90 executions unrun. The record preserves these different source checkpoints.
@@ -0,0 +1,48 @@
1
+ # Export cache and provider ownership — Skia 3.0.6
2
+
3
+ Historical cache/provider-fix checkpoint (2026-10-07). The additional native/VM growth described below was subsequently investigated and substantially reduced by the [export-worklet fix](NATIVE_MEMORY_INVESTIGATION_306.md), measured on both physical platforms on October 8. This report preserves the earlier measurements; its iPhone validation and unresolved-growth statements describe that earlier checkpoint.
4
+
5
+ The library now adopts the initial reference of Skia's `ImageProvider` and releases unused resources from its dedicated export recorder when an export session closes. Public playback/export signatures are unchanged. Reapply the guarded Skia patch and rebuild the native application.
6
+
7
+ ## Scope of the change
8
+
9
+ `ImageProvider::Make()` now uses `sk_make_sp<ImageProvider>()`. The original `sk_ref_sp(new ImageProvider)` increments the newly allocated object's initial reference and leaves it orphaned when its smart pointers close. The installed-factory ownership test uses the real Skia reference counter and verifies that an alias remains valid until its own final release.
10
+
11
+ Export cleanup first drains drawing and any frame processor, closes decoder/encoder/image/canvas/surface consumers, then invokes the private Skia recorder cleanup on the dedicated export runtime. The native method checks recording insertion and synchronous submission, drops the temporary recording and frees unused resources from that recorder only. Reservation release and successful promise settlement follow. Failure retains the existing quarantine and blocks reuse of the runtime. An old native binary is rejected before export allocations.
12
+
13
+ This policy does not trim the preview/UI recorder or the shared context, change their budgets, destroy the shared GPU device, force GC, or downscale video. It is not the broad experimental purge from the [earlier investigation](ANDROID_MEMORY_RETENTION_306.md). Preview, 3D and ML consumers can continue to retain their own resources; this is not a whole-process memory limit. Queue completion still uses the shared GPU queue, so this is not a promise of zero impact on concurrent GPU latency.
14
+
15
+ ## Physical Android results
16
+
17
+ The [machine-readable evidence](../benchmark/android-memory-optimization-306.json) records binary/source fingerprints, raw-report hashes, sampled conditions, memory categories, export outputs, pixel comparisons and remount checkpoints. Reports are sanitized; raw device identifiers stay local.
18
+
19
+ The production candidate APK is `fd4f6f277207d809fe40203439fcfa62b34e1bfcaffe42ad51adaf48db9f6254`. All workloads use the physical Pixel 8a, Android 17, arm64 Release, Skia 3.0.6 / WebGPU 0.12.1. The montage uses the same eight full-resolution sequential H264/HEVC 4K clips, 512 MiB owned quota, 720-square preview, 360-frame 1080-square export, 25 churn cycles and 40 remount cycles per case.
20
+
21
+ One cold-process montage, total PSS after 60 seconds at rest:
22
+
23
+ | Build | Total PSS (MiB) | GL mtrack (MiB) |
24
+ | --- | ---: | ---: |
25
+ | Original production AHB checkpoint | 702.78 | 397.46 |
26
+ | Provider reference correction only | 692.97 | 397.40 |
27
+ | Provider correction + export recorder cleanup | 584.62 | 289.32 |
28
+
29
+ The combined candidate is 118.16 MiB below the original and 108.35 MiB below the provider-only run. These are observed differences from one run per build, not a statistical savings guarantee. The reference correction alone did not materially reduce the GL mtrack category. The process remains much larger than the roughly 164 MiB idle menu, and tracked owned resources returning to zero does not explain all retained memory.
30
+
31
+ Within the final 40-remount operation, `/proc/self/status VmRSS` increased by approximately 13.19 MiB in the baseline run, 12.87 MiB with the provider correction alone and 13.27 MiB with both corrections. This specific growth is not resolved by the changes. These RSS measurements must not be mixed with `dumpsys TOTAL RSS` or interpreted as exact live allocation counts.
32
+
33
+ Two successive montages were then run in one cold process for each binary, with the same protocol: 50 churn cycles, 80 remount cycles and two 360-frame exports per process. Both cases passed in both binaries. After the second montage and 60 seconds at rest, total PSS was **861.14 MiB for the original versus 737.83 MiB for the candidate**, an observed reduction of **123.31 MiB (14.32%)**.
34
+
35
+ The candidate nevertheless grows substantially across the two montages. Native-heap PSS and the Unknown category finish at approximately 142/137 MiB in both binaries, versus approximately 92/74 MiB after a single montage. The change reduces retained graphics memory; it does not resolve that additional native/VM growth. These categories include the benchmark process and its instrumentation, and do not yet attribute outstanding allocations to the library, framework, driver or collector. Two montages do not establish a plateau.
36
+
37
+ All seven montage cases represented in the evidence (including the earlier single baseline) passed, with nominal/normal/active conditions in every native sample, no dropped collection samples and zero owned bytes/resources in every settled sample. All seven exports decoded to 360 frames with exact monotonic PTS at 30 fps. The candidate's three montage cases and its pixel oracle passed. The corrected APK was restored on the Pixel after comparison, and the test application was stopped to release KEEP_SCREEN_ON.
38
+
39
+ The final candidate also passed the CPU/AHB pixel oracle: ten cases covering padded dimensions, portrait, 90-degree rotation, H264/HEVC full 4K and copy/direct modes. The 66,560 compared RGBA channels had maximum error zero, including the retained raw frame after producer teardown. This separate functional readback is excluded from memory/performance measurements. It checks sampled SDR regions, not whole-image fidelity, HDR or audio.
40
+
41
+ ## Validation and limits
42
+
43
+ - 195 JavaScript tests pass, including cleanup ordering, refusal to purge after a failed surface drain, missing-native-patch preflight and quarantine after a failed cache drain.
44
+ - Five guarded-installer tests pass, including upgrade from the earlier canvas-only patch, repeat application and conflict refusal before mutation.
45
+ - Portable native policy/ownership tests pass with AddressSanitizer and UndefinedBehaviorSanitizer, including the installed provider factory.
46
+ - Android arm64 Release compilation, TypeScript, package build and strict package validation pass. The patched `JsiSkApi`/surface headers compile against the iOS simulator SDK; the new behavior has not been measured on a physical iPhone.
47
+
48
+ The result establishes a narrower production memory improvement and correct completion of these bounded operations. It does not establish a memory plateau, leak absence, full native-suite coverage, a 256 MiB quota, or faster presented video FPS. Remaining growth needs allocation-stack attribution separating recorder caches, Dawn/driver allocations, native heap and VM backing stores. Preserve the same workload and collectors for that investigation; do not mask growth by increasing quotas or forcing collection.