@shopify/react-native-skia 2.13.0 → 2.14.0

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 (124) hide show
  1. package/android/CMakeLists.txt +2 -0
  2. package/android/cpp/jni/include/JniSkiaPictureView.h +16 -15
  3. package/android/cpp/rnskia-android/OpenGLContext.h +1 -1
  4. package/android/cpp/rnskia-android/gl/Surface.h +1 -1
  5. package/apple/RNSkApplePlatformContext.h +3 -4
  6. package/apple/RNSkiaModule.h +11 -3
  7. package/apple/RNSkiaModule.mm +46 -19
  8. package/apple/SkiaPictureView.mm +2 -2
  9. package/apple/SkiaUIView.h +0 -1
  10. package/apple/SkiaUIView.mm +1 -1
  11. package/apple/ViewScreenshotService.h +2 -2
  12. package/apple/ViewScreenshotService.mm +5 -4
  13. package/cpp/api/JsiSkImage.h +1 -1
  14. package/cpp/api/JsiSkImageFilter.h +3 -1
  15. package/cpp/api/JsiSkParagraph.h +2 -1
  16. package/cpp/api/JsiSkParagraphBuilder.h +1 -1
  17. package/cpp/api/JsiSkParagraphBuilderFactory.h +1 -1
  18. package/cpp/api/JsiSkPath.h +11 -435
  19. package/cpp/api/JsiSkPathBuilder.h +11 -4
  20. package/cpp/api/JsiSkPictureRecorder.h +2 -1
  21. package/cpp/api/JsiSkShader.h +3 -1
  22. package/cpp/api/JsiSkSkottie.h +29 -28
  23. package/cpp/api/JsiSkStrutStyle.h +1 -1
  24. package/cpp/api/JsiSkSurfaceFactory.h +41 -0
  25. package/cpp/api/recorder/Command.h +1 -1
  26. package/cpp/api/recorder/Convertor.h +153 -132
  27. package/cpp/api/recorder/DataTypes.h +12 -12
  28. package/cpp/api/recorder/DrawingCtx.h +1 -1
  29. package/cpp/api/recorder/ImageFit.h +8 -5
  30. package/cpp/api/recorder/JsiRecorder.h +14 -31
  31. package/cpp/api/recorder/Paint.h +3 -3
  32. package/cpp/api/recorder/RNRecorder.h +56 -0
  33. package/cpp/api/third_party/base64.cpp +1 -1
  34. package/cpp/jsi/JSICache.cpp +23 -0
  35. package/cpp/jsi/JSICache.h +28 -1
  36. package/cpp/jsi/NativeObject.h +36 -7
  37. package/cpp/jsi/ViewProperty.cpp +23 -0
  38. package/cpp/jsi/ViewProperty.h +31 -10
  39. package/cpp/rnskia/RNDawnContext.h +34 -1
  40. package/cpp/rnskia/RNSkJsiViewApi.h +69 -2
  41. package/cpp/rnskia/RNSkPictureView.cpp +58 -0
  42. package/cpp/rnskia/RNSkPictureView.h +110 -24
  43. package/lib/commonjs/Platform/IPlatform.js.map +1 -1
  44. package/lib/commonjs/animation/functions/interpolate.js.map +1 -1
  45. package/lib/commonjs/animation/functions/interpolatePaths.js +18 -0
  46. package/lib/commonjs/animation/functions/interpolatePaths.js.map +1 -1
  47. package/lib/commonjs/skia/core/Image.js.map +1 -1
  48. package/lib/commonjs/skia/types/NativeBuffer/NativeBufferFactory.js.map +1 -1
  49. package/lib/commonjs/skia/types/Path/Path.d.ts +2 -336
  50. package/lib/commonjs/skia/types/Path/Path.js.map +1 -1
  51. package/lib/commonjs/skia/types/Recorder.d.ts +10 -0
  52. package/lib/commonjs/skia/types/Recorder.js.map +1 -1
  53. package/lib/commonjs/skia/types/Shader/Shader.js.map +1 -1
  54. package/lib/commonjs/skia/types/Surface/SurfaceFactory.d.ts +16 -0
  55. package/lib/commonjs/skia/types/Surface/SurfaceFactory.js.map +1 -1
  56. package/lib/commonjs/skia/web/JsiSkPath.d.ts +4 -38
  57. package/lib/commonjs/skia/web/JsiSkPath.js +3 -275
  58. package/lib/commonjs/skia/web/JsiSkPath.js.map +1 -1
  59. package/lib/commonjs/skia/web/JsiSkSurfaceFactory.d.ts +2 -1
  60. package/lib/commonjs/skia/web/JsiSkSurfaceFactory.js +3 -0
  61. package/lib/commonjs/skia/web/JsiSkSurfaceFactory.js.map +1 -1
  62. package/lib/commonjs/sksg/Container.native.d.ts +15 -1
  63. package/lib/commonjs/sksg/Container.native.js +34 -33
  64. package/lib/commonjs/sksg/Container.native.js.map +1 -1
  65. package/lib/commonjs/specs/NativeSkiaModule.web.js +4 -0
  66. package/lib/commonjs/specs/NativeSkiaModule.web.js.map +1 -1
  67. package/lib/commonjs/views/types.d.ts +9 -0
  68. package/lib/commonjs/views/types.js.map +1 -1
  69. package/lib/module/Platform/IPlatform.js.map +1 -1
  70. package/lib/module/animation/functions/interpolate.js.map +1 -1
  71. package/lib/module/animation/functions/interpolatePaths.js +18 -0
  72. package/lib/module/animation/functions/interpolatePaths.js.map +1 -1
  73. package/lib/module/skia/core/Image.js.map +1 -1
  74. package/lib/module/skia/types/NativeBuffer/NativeBufferFactory.js.map +1 -1
  75. package/lib/module/skia/types/Path/Path.d.ts +2 -336
  76. package/lib/module/skia/types/Path/Path.js.map +1 -1
  77. package/lib/module/skia/types/Recorder.d.ts +10 -0
  78. package/lib/module/skia/types/Recorder.js.map +1 -1
  79. package/lib/module/skia/types/Shader/Shader.js.map +1 -1
  80. package/lib/module/skia/types/Surface/SurfaceFactory.d.ts +16 -0
  81. package/lib/module/skia/types/Surface/SurfaceFactory.js.map +1 -1
  82. package/lib/module/skia/web/JsiSkPath.d.ts +4 -38
  83. package/lib/module/skia/web/JsiSkPath.js +4 -276
  84. package/lib/module/skia/web/JsiSkPath.js.map +1 -1
  85. package/lib/module/skia/web/JsiSkSurfaceFactory.d.ts +2 -1
  86. package/lib/module/skia/web/JsiSkSurfaceFactory.js +4 -1
  87. package/lib/module/skia/web/JsiSkSurfaceFactory.js.map +1 -1
  88. package/lib/module/sksg/Container.native.d.ts +15 -1
  89. package/lib/module/sksg/Container.native.js +34 -33
  90. package/lib/module/sksg/Container.native.js.map +1 -1
  91. package/lib/module/specs/NativeSkiaModule.web.js +4 -0
  92. package/lib/module/specs/NativeSkiaModule.web.js.map +1 -1
  93. package/lib/module/views/types.d.ts +9 -0
  94. package/lib/module/views/types.js.map +1 -1
  95. package/lib/typescript/lib/commonjs/skia/web/JsiSkPath.d.ts +1 -35
  96. package/lib/typescript/lib/commonjs/skia/web/JsiSkSurfaceFactory.d.ts +1 -0
  97. package/lib/typescript/lib/commonjs/sksg/Container.native.d.ts +15 -1
  98. package/lib/typescript/lib/module/skia/web/JsiSkPath.d.ts +3 -37
  99. package/lib/typescript/lib/module/skia/web/JsiSkSurfaceFactory.d.ts +1 -0
  100. package/lib/typescript/lib/module/sksg/Container.native.d.ts +15 -1
  101. package/lib/typescript/src/skia/types/Path/Path.d.ts +2 -336
  102. package/lib/typescript/src/skia/types/Recorder.d.ts +10 -0
  103. package/lib/typescript/src/skia/types/Surface/SurfaceFactory.d.ts +16 -0
  104. package/lib/typescript/src/skia/web/JsiSkPath.d.ts +4 -38
  105. package/lib/typescript/src/skia/web/JsiSkSurfaceFactory.d.ts +2 -1
  106. package/lib/typescript/src/sksg/Container.native.d.ts +15 -1
  107. package/lib/typescript/src/views/types.d.ts +9 -0
  108. package/package.json +2 -2
  109. package/src/Platform/IPlatform.ts +1 -4
  110. package/src/animation/functions/interpolate.ts +1 -4
  111. package/src/animation/functions/interpolatePaths.ts +24 -0
  112. package/src/skia/core/Image.ts +1 -2
  113. package/src/skia/types/NativeBuffer/NativeBufferFactory.ts +2 -5
  114. package/src/skia/types/Path/Path.ts +2 -420
  115. package/src/skia/types/Recorder.ts +10 -0
  116. package/src/skia/types/Shader/Shader.ts +1 -5
  117. package/src/skia/types/Surface/SurfaceFactory.ts +17 -0
  118. package/src/skia/web/JsiSkPath.ts +4 -495
  119. package/src/skia/web/JsiSkSurfaceFactory.ts +6 -2
  120. package/src/sksg/Container.native.ts +34 -37
  121. package/src/specs/NativeSkiaModule.web.ts +4 -0
  122. package/src/views/types.ts +13 -0
  123. package/apple/SkiaManager.h +0 -23
  124. package/apple/SkiaManager.mm +0 -63
@@ -6,8 +6,8 @@ namespace RNSkia {
6
6
 
7
7
  using Uniforms = std::map<std::string, std::vector<float>>;
8
8
 
9
- std::vector<float> processArray(jsi::Runtime &runtime,
10
- const jsi::Array &array) {
9
+ inline std::vector<float> processArray(jsi::Runtime &runtime,
10
+ const jsi::Array &array) {
11
11
  std::vector<float> result;
12
12
  size_t length = array.length(runtime);
13
13
  result.reserve(length);
@@ -37,18 +37,18 @@ std::vector<float> processArray(jsi::Runtime &runtime,
37
37
  return result;
38
38
  }
39
39
 
40
- bool isJSPoint(jsi::Runtime &runtime, const jsi::Value &value) {
40
+ inline bool isJSPoint(jsi::Runtime &runtime, const jsi::Value &value) {
41
41
  return value.isObject() &&
42
42
  value.asObject(runtime).hasProperty(runtime, "x") &&
43
43
  value.asObject(runtime).hasProperty(runtime, "y");
44
44
  }
45
45
 
46
- bool isIndexable(jsi::Runtime &runtime, const jsi::Value &value) {
46
+ inline bool isIndexable(jsi::Runtime &runtime, const jsi::Value &value) {
47
47
  return value.isObject() && value.asObject(runtime).hasProperty(runtime, "0");
48
48
  }
49
49
 
50
- std::shared_ptr<SkRect> processRect(jsi::Runtime &runtime,
51
- const jsi::Value &value) {
50
+ inline std::shared_ptr<SkRect> processRect(jsi::Runtime &runtime,
51
+ const jsi::Value &value) {
52
52
  if (value.isObject()) {
53
53
  auto object = value.asObject(runtime);
54
54
  auto ptr = tryGetJsiObject<JsiSkRect>(runtime, object);
@@ -68,7 +68,7 @@ std::shared_ptr<SkRect> processRect(jsi::Runtime &runtime,
68
68
  return nullptr;
69
69
  }
70
70
 
71
- SkPoint processPoint(jsi::Runtime &runtime, const jsi::Value &value) {
71
+ inline SkPoint processPoint(jsi::Runtime &runtime, const jsi::Value &value) {
72
72
  if (value.isObject()) {
73
73
  auto object = value.asObject(runtime);
74
74
  if (object.hasProperty(runtime, "x") && object.hasProperty(runtime, "y")) {
@@ -78,11 +78,11 @@ SkPoint processPoint(jsi::Runtime &runtime, const jsi::Value &value) {
78
78
  }
79
79
  }
80
80
  throw std::runtime_error("Couldn't read point value");
81
- };
81
+ }
82
82
 
83
83
  // TODO: return the SkRRect directly
84
- std::shared_ptr<SkRRect> processRRect(jsi::Runtime &runtime,
85
- const jsi::Value &value) {
84
+ inline std::shared_ptr<SkRRect> processRRect(jsi::Runtime &runtime,
85
+ const jsi::Value &value) {
86
86
  if (value.isObject()) {
87
87
  auto object = value.asObject(runtime);
88
88
  auto ptr = tryGetJsiObject<JsiSkRRect>(runtime, object);
@@ -119,8 +119,8 @@ std::shared_ptr<SkRRect> processRRect(jsi::Runtime &runtime,
119
119
  }
120
120
 
121
121
  // Return SkPath instead of shared_ptr<SkPath>
122
- std::shared_ptr<SkPath> processPath(jsi::Runtime &runtime,
123
- const jsi::Value &value) {
122
+ inline std::shared_ptr<SkPath> processPath(jsi::Runtime &runtime,
123
+ const jsi::Value &value) {
124
124
  if (value.isString()) {
125
125
  auto pathString = value.getString(runtime).utf8(runtime);
126
126
  SkPath result;
@@ -52,7 +52,7 @@ struct Composers {
52
52
 
53
53
  class DrawingCtx {
54
54
  public:
55
- DrawingCtx(SkCanvas *canvas) : canvas(canvas) {
55
+ explicit DrawingCtx(SkCanvas *canvas) : canvas(canvas) {
56
56
  SkPaint paint;
57
57
  paint.setAntiAlias(true);
58
58
  paints.push_back(paint);
@@ -18,14 +18,14 @@ struct FitRects {
18
18
  SkRect dst;
19
19
  };
20
20
 
21
- SkRect inscribe(SkSize size, SkRect rect) {
21
+ inline SkRect inscribe(SkSize size, SkRect rect) {
22
22
  auto halfWidthDelta = (rect.width() - size.width()) / 2.0;
23
23
  auto halfHeightDelta = (rect.height() - size.height()) / 2.0;
24
24
  return SkRect::MakeXYWH(rect.x() + halfWidthDelta, rect.y() + halfHeightDelta,
25
25
  size.width(), size.height());
26
26
  }
27
27
 
28
- SkMatrix rect2rect(SkRect src, SkRect dst) {
28
+ inline SkMatrix rect2rect(SkRect src, SkRect dst) {
29
29
  auto sx = dst.width() / src.width();
30
30
  auto sy = dst.height() / src.height();
31
31
  auto tx = dst.x() - src.x() * sx;
@@ -36,9 +36,12 @@ SkMatrix rect2rect(SkRect src, SkRect dst) {
36
36
  return m3;
37
37
  }
38
38
 
39
- SkSize size(double width, double height) { return SkSize::Make(width, height); }
39
+ inline SkSize size(double width, double height) {
40
+ return SkSize::Make(width, height);
41
+ }
40
42
 
41
- FitSizes applyBoxFit(const std::string fit, SkSize input, SkSize output) {
43
+ inline FitSizes applyBoxFit(const std::string fit, SkSize input,
44
+ SkSize output) {
42
45
  SkSize src = size(0, 0);
43
46
  SkSize dst = size(0, 0);
44
47
 
@@ -96,7 +99,7 @@ FitSizes applyBoxFit(const std::string fit, SkSize input, SkSize output) {
96
99
  return {.src = src, .dst = dst};
97
100
  }
98
101
 
99
- FitRects fitRects(const std::string &fit, SkRect rect, SkRect rect2) {
102
+ inline FitRects fitRects(const std::string &fit, SkRect rect, SkRect rect2) {
100
103
  auto sizes = applyBoxFit(fit, size(rect.width(), rect.height()),
101
104
  size(rect2.width(), rect2.height()));
102
105
 
@@ -24,7 +24,7 @@ class JsiRecorder
24
24
  public:
25
25
  static constexpr const char *CLASS_NAME = "Recorder";
26
26
 
27
- JsiRecorder(std::shared_ptr<RNSkPlatformContext> context)
27
+ explicit JsiRecorder(std::shared_ptr<RNSkPlatformContext> context)
28
28
  : JsiSkWrappingSharedPtrNativeObject<JsiRecorder, Recorder>(
29
29
  std::move(context), std::make_shared<Recorder>()) {
30
30
  getObject()->_context = getContext();
@@ -70,18 +70,7 @@ public:
70
70
  throw jsi::JSError(runtime, "Invalid Picture object provided to play()");
71
71
  }
72
72
 
73
- // Create a new picture recorder to record into
74
- SkPictureRecorder pictureRecorder;
75
- SkISize size = SkISize::Make(2'000'000, 2'000'000);
76
- SkRect rect = SkRect::Make(size);
77
- auto canvas = pictureRecorder.beginRecording(rect, nullptr);
78
-
79
- // Play the recorded commands into the canvas
80
- DrawingCtx ctx(canvas);
81
- getObject()->play(&ctx);
82
-
83
- // Finish recording and get the new picture
84
- auto newPicture = pictureRecorder.finishRecordingAsPicture();
73
+ auto newPicture = getObject()->makePicture();
85
74
 
86
75
  // Update the existing JsiSkPicture object with the new SkPicture
87
76
  // This reuses the existing JavaScript object instead of creating a new one
@@ -95,24 +84,12 @@ public:
95
84
 
96
85
  JSI_HOST_FUNCTION(applyUpdates) {
97
86
  auto values = arguments[0].asObject(runtime).asArray(runtime);
98
- auto size = values.size(runtime);
99
- auto recorder = getObject();
100
- for (int i = 0; i < size; i++) {
101
- auto sharedValue = values.getValueAtIndex(runtime, i).asObject(runtime);
102
- auto name = "variable" + std::to_string(i);
103
- // Look up the conversion functions for this name
104
- auto it = recorder->variables.find(name);
105
- if (it != recorder->variables.end()) {
106
- // Execute each conversion function in the vector
107
- const auto &conversionFunctions = it->second;
108
- for (const auto &conversionFunc : conversionFunctions) {
109
- conversionFunc(runtime, sharedValue);
110
- }
111
- }
112
- }
87
+ getObject()->applyUpdates(runtime, values);
113
88
  return jsi::Value::undefined();
114
89
  }
115
90
 
91
+ JSI_HOST_FUNCTION(getId) { return jsi::Value(getObject()->id); }
92
+
116
93
  JSI_HOST_FUNCTION(saveGroup) {
117
94
  const jsi::Value *value = count > 0 ? &arguments[0] : nullptr;
118
95
  getObject()->saveGroup(runtime, value);
@@ -377,11 +354,17 @@ public:
377
354
  installHostMethod(runtime, prototype, "applyUpdates",
378
355
  &JsiRecorder::applyUpdates);
379
356
  installHostMethod(runtime, prototype, "reset", &JsiRecorder::reset);
357
+ installHostMethod(runtime, prototype, "getId", &JsiRecorder::getId);
380
358
  }
381
359
 
382
- // This has no basis in reality but since since these are private long-lived
383
- // objects, we think it is more than fine.
384
- size_t getMemoryPressure() override { return 5 * 1024 * 1024; }
360
+ // The recorder itself is a small command list. The resources it references
361
+ // (images, pictures, paths) report their own size through their wrappers
362
+ // and the pictures produced by play() report theirs. Do not put a made-up
363
+ // number here: a new recorder is created on every React commit, and the
364
+ // wrapper is disposed as soon as the view takes ownership of the recording
365
+ // (see NativeReanimatedContainer), so the charge would never drive a
366
+ // collection anyway.
367
+ size_t getMemoryPressure() override { return kMinMemoryPressure; }
385
368
 
386
369
  static const jsi::HostFunctionType
387
370
  createCtor(std::shared_ptr<RNSkPlatformContext> context) {
@@ -18,9 +18,9 @@ struct TransformProps {
18
18
  };
19
19
 
20
20
  // TODO: can we avoid a copy here?
21
- SkMatrix processTransform(std::optional<SkMatrix> &matrix,
22
- std::optional<SkM44> &transform,
23
- std::optional<SkPoint> &origin) {
21
+ inline SkMatrix processTransform(std::optional<SkMatrix> &matrix,
22
+ std::optional<SkM44> &transform,
23
+ std::optional<SkPoint> &origin) {
24
24
  SkMatrix m3;
25
25
  if (matrix.has_value()) {
26
26
  m3 = matrix.value();
@@ -1,14 +1,19 @@
1
1
  #pragma once
2
2
 
3
3
  #include <algorithm>
4
+ #include <atomic>
4
5
  #include <cmath>
6
+ #include <cstdint>
5
7
  #include <memory>
8
+ #include <mutex>
6
9
  #include <optional>
7
10
  #include <string>
8
11
  #include <vector>
9
12
 
10
13
  #include <jsi/jsi.h>
11
14
 
15
+ #include "include/core/SkPictureRecorder.h"
16
+
12
17
  #include "ColorFilters.h"
13
18
  #include "Command.h"
14
19
  #include "Convertor.h"
@@ -26,6 +31,11 @@ class Recorder {
26
31
  private:
27
32
  using CommandList = std::vector<std::unique_ptr<Command>>;
28
33
 
34
+ static double nextId() {
35
+ static std::atomic<uint64_t> counter{0};
36
+ return static_cast<double>(++counter);
37
+ }
38
+
29
39
  struct PendingGroup {
30
40
  GroupCommand *group;
31
41
  float zIndex;
@@ -35,6 +45,12 @@ private:
35
45
  CommandList commands;
36
46
  std::vector<CommandList *> commandStack;
37
47
 
48
+ // Once a view owns the recording, the Reanimated mapper writes the shared
49
+ // values into the commands (applyUpdates, UI thread) while the view replays
50
+ // them (play, main thread). Both hold this lock; nothing else does, so it
51
+ // never nests with the view's lock.
52
+ std::mutex _commandsMutex;
53
+
38
54
  CommandList &currentCommands() { return *commandStack.back(); }
39
55
 
40
56
  void pushCommand(std::unique_ptr<Command> command) {
@@ -94,6 +110,11 @@ public:
94
110
  std::shared_ptr<RNSkPlatformContext> _context;
95
111
  Variables variables;
96
112
 
113
+ // Unique per recording. The Reanimated mapper tags its updates with it so a
114
+ // mapper that outlives its recording (stopMapper() is asynchronous) cannot
115
+ // write into the recording that replaced it.
116
+ const double id = nextId();
117
+
97
118
  Recorder() { commandStack.push_back(&commands); }
98
119
  ~Recorder() {
99
120
  if (!_context || commands.empty()) {
@@ -382,10 +403,45 @@ public:
382
403
  }
383
404
 
384
405
  void play(DrawingCtx *ctx) {
406
+ std::lock_guard<std::mutex> lock(_commandsMutex);
385
407
  for (const auto &cmd : commands) {
386
408
  playCommand(ctx, cmd.get());
387
409
  }
388
410
  }
411
+
412
+ /**
413
+ * Plays the recording into a picture. The cull rect is deliberately huge:
414
+ * a recording is not tied to a view size.
415
+ */
416
+ sk_sp<SkPicture> makePicture() {
417
+ SkPictureRecorder pictureRecorder;
418
+ SkISize size = SkISize::Make(2'000'000, 2'000'000);
419
+ SkRect rect = SkRect::Make(size);
420
+ auto canvas = pictureRecorder.beginRecording(rect, nullptr);
421
+ DrawingCtx ctx(canvas);
422
+ play(&ctx);
423
+ return pictureRecorder.finishRecordingAsPicture();
424
+ }
425
+
426
+ /**
427
+ * Reads the current value of each shared value (in the order they were
428
+ * registered as variable0, variable1, ...) on the calling runtime and
429
+ * writes it into the recorded commands.
430
+ */
431
+ void applyUpdates(jsi::Runtime &runtime, const jsi::Array &values) {
432
+ std::lock_guard<std::mutex> lock(_commandsMutex);
433
+ auto size = values.size(runtime);
434
+ for (size_t i = 0; i < size; i++) {
435
+ auto sharedValue = values.getValueAtIndex(runtime, i).asObject(runtime);
436
+ auto name = "variable" + std::to_string(i);
437
+ auto it = variables.find(name);
438
+ if (it != variables.end()) {
439
+ for (const auto &conversionFunc : it->second) {
440
+ conversionFunc(runtime, sharedValue);
441
+ }
442
+ }
443
+ }
444
+ }
389
445
  };
390
446
 
391
447
  inline void Recorder::playCommand(DrawingCtx *ctx, Command *cmd) {
@@ -4,7 +4,7 @@
4
4
  * Use of this source code is governed by a BSD-style license that can be
5
5
  * found in the LICENSE file.
6
6
  */
7
- #include "base64.h"
7
+ #include "api/third_party/base64.h"
8
8
 
9
9
  #include <cstdint>
10
10
 
@@ -1,5 +1,6 @@
1
1
  #include "JSICache.h"
2
2
 
3
+ #include <algorithm>
3
4
  #include <iterator>
4
5
  #include <memory>
5
6
  #include <stdexcept>
@@ -96,4 +97,26 @@ jsi::Object &JSICache::setPrototype(PrototypeKey key, jsi::Object prototype) {
96
97
  return it->second;
97
98
  }
98
99
 
100
+ jsi::Value JSICache::lockWrapper(jsi::Runtime &runtime,
101
+ const void *nativeObject) {
102
+ auto it = _wrappers.find(nativeObject);
103
+ if (it == _wrappers.end()) {
104
+ return jsi::Value::undefined();
105
+ }
106
+ return it->second.lock(runtime);
107
+ }
108
+
109
+ void JSICache::setWrapper(jsi::Runtime &runtime, const void *nativeObject,
110
+ const jsi::Object &wrapper) {
111
+ _wrappers.insert_or_assign(nativeObject, jsi::WeakObject(runtime, wrapper));
112
+ if (_wrappers.size() < _pruneWrappersAt) {
113
+ return;
114
+ }
115
+ for (auto it = _wrappers.begin(); it != _wrappers.end();) {
116
+ it = it->second.lock(runtime).isUndefined() ? _wrappers.erase(it)
117
+ : std::next(it);
118
+ }
119
+ _pruneWrappersAt = std::max(kMinPruneWrappersAt, _wrappers.size() * 2);
120
+ }
121
+
99
122
  } // namespace RNJsi
@@ -13,7 +13,8 @@ namespace jsi = facebook::jsi;
13
13
 
14
14
  /**
15
15
  * Per-runtime store for JSI values that native code wants to keep across
16
- * calls (currently: the prototype object of every NativeObject class).
16
+ * calls: the prototype object of every NativeObject class, and the JS
17
+ * wrapper of every native object that has one on the runtime.
17
18
  *
18
19
  * Ownership model (borrowed from react-native-nitro-modules' JSICache):
19
20
  *
@@ -72,6 +73,26 @@ public:
72
73
  */
73
74
  jsi::Object &setPrototype(PrototypeKey key, jsi::Object prototype);
74
75
 
76
+ /**
77
+ * Returns the JS wrapper stored for `nativeObject` on this runtime, or
78
+ * undefined if none was stored or the wrapper has been collected.
79
+ *
80
+ * The key is the native object's address. A live wrapper keeps its native
81
+ * object alive (the object is the wrapper's NativeState), so as long as
82
+ * the entry locks it still belongs to that object. Once the wrapper has
83
+ * been collected the entry is stale and is skipped or replaced, so an
84
+ * address reused by a new object can never resolve to the old wrapper.
85
+ */
86
+ jsi::Value lockWrapper(jsi::Runtime &runtime, const void *nativeObject);
87
+
88
+ /**
89
+ * Stores `wrapper` as the JS wrapper of `nativeObject` on this runtime.
90
+ * Only a weak reference is kept: the wrapper stays collectable, and the
91
+ * native object is released when it goes away.
92
+ */
93
+ void setWrapper(jsi::Runtime &runtime, const void *nativeObject,
94
+ const jsi::Object &wrapper);
95
+
75
96
  JSICache() = default;
76
97
  ~JSICache() override = default;
77
98
  JSICache(const JSICache &) = delete;
@@ -81,6 +102,12 @@ private:
81
102
  static std::shared_ptr<JSICache> getOrCreateOnGlobal(jsi::Runtime &runtime);
82
103
 
83
104
  std::unordered_map<PrototypeKey, jsi::Object> _prototypes;
105
+ // Native object -> its wrapper on this runtime. Entries of collected
106
+ // wrappers are pruned in setWrapper once the map has doubled since the
107
+ // last pruning, so it stays proportional to the number of live wrappers.
108
+ std::unordered_map<const void *, jsi::WeakObject> _wrappers;
109
+ size_t _pruneWrappersAt = kMinPruneWrappersAt;
110
+ static constexpr size_t kMinPruneWrappersAt = 64;
84
111
 
85
112
  // Runtime -> cache. Weak on purpose: the runtime's global owns the cache.
86
113
  static std::mutex _registryMutex;
@@ -210,12 +210,12 @@ public:
210
210
  if (instance == nullptr) {
211
211
  throw jsi::JSError(rt, "Invalid boxed native object state");
212
212
  }
213
- // Unboxing creates a *view* of the object on the target runtime.
214
- // Keep the runtime the object was originally created on: async
215
- // native code (e.g. GPUDevice error/lost events) delivers into
216
- // the creation runtime, and rebinding it to a worklet runtime
217
- // would invoke main-runtime jsi::Functions on the wrong runtime
218
- // and thread.
213
+ // Unboxing hands back the wrapper cached for the target runtime,
214
+ // creating it on first use. Keep the runtime the object was
215
+ // originally created on: async native code (e.g. GPUDevice
216
+ // error/lost events) delivers into the creation runtime, and
217
+ // rebinding it to a worklet runtime would invoke main-runtime
218
+ // jsi::Functions on the wrong runtime and thread.
219
219
  auto *originalRuntime = instance->getCreationRuntime();
220
220
  auto value = Derived::create(rt, instance);
221
221
  if (originalRuntime != nullptr) {
@@ -262,10 +262,37 @@ public:
262
262
  }
263
263
 
264
264
  /**
265
- * Create a JS object with native state attached.
265
+ * Returns the JS wrapper of `instance` on `runtime`, creating it on first
266
+ * use.
267
+ *
268
+ * A native object has at most one live wrapper per runtime. The wrapper is
269
+ * cached (weakly, so it stays collectable) in the runtime's JSICache, and
270
+ * converting the same object again (returning it from another native
271
+ * call, unboxing it in a worklet on every frame, ...) hands back that
272
+ * wrapper instead of a new one. Each wrapper reports the native memory the
273
+ * object owns to the GC of its runtime through setExternalMemoryPressure,
274
+ * so the memory is charged exactly once per runtime that can reach the
275
+ * object, however often it is converted; charging every conversion used
276
+ * to multiply the amount by the number of captures in a worklet until
277
+ * Hermes hit its max heap size. The hint is refreshed on every round trip
278
+ * for objects whose size changes after creation (path and paragraph
279
+ * builders).
266
280
  */
267
281
  static jsi::Value create(jsi::Runtime &runtime,
268
282
  std::shared_ptr<Derived> instance) {
283
+ auto &cache = JSICache::get(runtime);
284
+ const void *key = instance.get();
285
+
286
+ auto existing = cache.lockWrapper(runtime, key);
287
+ if (existing.isObject()) {
288
+ auto pressure = instance->getMemoryPressure();
289
+ if (pressure > 0) {
290
+ existing.getObject(runtime).setExternalMemoryPressure(runtime,
291
+ pressure);
292
+ }
293
+ return existing;
294
+ }
295
+
269
296
  // Store creation runtime for logging etc.
270
297
  instance->setCreationRuntime(&runtime);
271
298
 
@@ -290,9 +317,11 @@ public:
290
317
  obj.setExternalMemoryPressure(runtime, pressure);
291
318
  }
292
319
 
320
+ cache.setWrapper(runtime, key, obj);
293
321
  return std::move(obj);
294
322
  }
295
323
 
324
+ public:
296
325
  /**
297
326
  * Get the native state from a JS value.
298
327
  * Throws if the value doesn't have the expected native state.
@@ -0,0 +1,23 @@
1
+ // The recorder headers depend on the include order established by
2
+ // JsiSkApi.h, so the probe lives here rather than in ViewProperty.h (which
3
+ // every platform view includes on its own).
4
+ #include "api/JsiSkApi.h"
5
+
6
+ #include "jsi/ViewProperty.h"
7
+
8
+ #include "api/JsiSkPicture.h"
9
+ #include "api/recorder/JsiRecorder.h"
10
+
11
+ namespace RNJsi {
12
+
13
+ ViewProperty::ViewProperty(jsi::Runtime &runtime, const jsi::Value &value) {
14
+ if (auto picture =
15
+ RNSkia::tryGetJsiObject<RNSkia::JsiSkPicture>(runtime, value)) {
16
+ _value = picture->getObject();
17
+ } else if (auto recorder =
18
+ RNSkia::tryGetJsiObject<RNSkia::JsiRecorder>(runtime, value)) {
19
+ _value = recorder->getObject();
20
+ }
21
+ }
22
+
23
+ } // namespace RNJsi
@@ -4,28 +4,49 @@
4
4
  #include <jsi/jsi.h>
5
5
  #include <memory>
6
6
  #include <string>
7
+ #include <utility>
7
8
  #include <variant>
8
9
 
9
- #include "api/JsiSkPicture.h"
10
+ #pragma clang diagnostic push
11
+ #pragma clang diagnostic ignored "-Wdocumentation"
12
+
13
+ #include "include/core/SkPicture.h"
14
+
15
+ #pragma clang diagnostic pop
16
+
17
+ namespace RNSkia {
18
+ class Recorder;
19
+ }
10
20
 
11
21
  namespace RNJsi {
12
22
  namespace jsi = facebook::jsi;
13
23
 
14
24
  class ViewProperty {
15
25
  public:
16
- ViewProperty(jsi::Runtime &runtime, const jsi::Value &value) {
17
- auto jsiPicture =
18
- RNSkia::tryGetJsiObject<RNSkia::JsiSkPicture>(runtime, value);
19
- if (jsiPicture) {
20
- _value = jsiPicture->getObject();
21
- }
22
- }
26
+ /**
27
+ * Probes the JS value for the native objects a view can display (a picture
28
+ * or a recorder); anything else yields an empty property. Defined in
29
+ * ViewProperty.cpp: the recorder headers depend on the include order
30
+ * established by JsiSkApi.h, which this header (included by every platform
31
+ * view) must not pull in.
32
+ */
33
+ ViewProperty(jsi::Runtime &runtime, const jsi::Value &value);
23
34
 
24
- bool isNull() { return std::holds_alternative<std::nullptr_t>(_value); }
35
+ bool isPicture() { return std::holds_alternative<sk_sp<SkPicture>>(_value); }
36
+
37
+ bool isRecorder() {
38
+ return std::holds_alternative<std::shared_ptr<RNSkia::Recorder>>(_value);
39
+ }
25
40
 
26
41
  sk_sp<SkPicture> getPicture() { return std::get<sk_sp<SkPicture>>(_value); }
27
42
 
43
+ std::shared_ptr<RNSkia::Recorder> getRecorder() {
44
+ return std::get<std::shared_ptr<RNSkia::Recorder>>(_value);
45
+ }
46
+
28
47
  private:
29
- std::variant<std::nullptr_t, sk_sp<SkPicture>> _value = nullptr;
48
+ std::variant<std::nullptr_t, sk_sp<SkPicture>,
49
+ std::shared_ptr<RNSkia::Recorder>>
50
+ _value = nullptr;
30
51
  };
31
52
  } // namespace RNJsi
@@ -112,7 +112,7 @@ public:
112
112
  int height = static_cast<int>(IOSurfaceGetHeight(ioSurface));
113
113
  #else
114
114
  wgpu::SharedTextureMemoryAHardwareBufferDescriptor platformDesc;
115
- auto ahb = (AHardwareBuffer *)buffer;
115
+ auto ahb = reinterpret_cast<AHardwareBuffer *>(buffer);
116
116
  platformDesc.handle = ahb;
117
117
  AHardwareBuffer_Desc adesc;
118
118
  AHardwareBuffer_describe(ahb, &adesc);
@@ -270,6 +270,39 @@ public:
270
270
  textureRef);
271
271
  }
272
272
 
273
+ // Create an SkSurface that draws straight into a WebGPU texture (zero-copy).
274
+ // The texture must have RenderAttachment usage; give it TextureBinding too
275
+ // to sample it from WebGPU (e.g. as a three.js texture) after each flush.
276
+ // The surface retains the texture for its lifetime.
277
+ sk_sp<SkSurface> MakeSurfaceFromTexture(wgpu::Texture texture) {
278
+ if (!texture) {
279
+ return nullptr;
280
+ }
281
+ if (!(texture.GetUsage() & wgpu::TextureUsage::RenderAttachment)) {
282
+ throw std::runtime_error(
283
+ "MakeSurfaceFromTexture: the texture needs RenderAttachment usage");
284
+ }
285
+
286
+ skgpu::graphite::BackendTexture backendTexture =
287
+ skgpu::graphite::BackendTextures::MakeDawn(texture.Get());
288
+
289
+ struct TextureRef {
290
+ wgpu::Texture texture;
291
+ };
292
+ auto textureRef = new TextureRef{texture};
293
+
294
+ // The color type is derived from the texture format.
295
+ return SkSurfaces::WrapBackendTexture(
296
+ getRecorder(), backendTexture,
297
+ nullptr, // colorspace
298
+ nullptr, // surfaceProps
299
+ [](void *context) {
300
+ auto ref = static_cast<TextureRef *>(context);
301
+ delete ref;
302
+ },
303
+ textureRef);
304
+ }
305
+
273
306
  // Create a WebGPU texture from an SkImage
274
307
  // Returns a texture with CopySrc and TextureBinding usage
275
308
  wgpu::Texture MakeTextureFromImage(sk_sp<SkImage> image) {