@shopify/react-native-skia 2.4.20 → 2.5.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 (53) hide show
  1. package/android/CMakeLists.txt +12 -49
  2. package/android/build.gradle +37 -1
  3. package/apple/MetalContext.h +2 -3
  4. package/apple/SkiaCVPixelBufferUtils.h +11 -8
  5. package/apple/SkiaCVPixelBufferUtils.mm +229 -49
  6. package/apple/SkiaUIView.mm +4 -0
  7. package/cpp/api/JsiSkColor.h +53 -1
  8. package/cpp/api/JsiSkContourMeasureIter.h +1 -1
  9. package/cpp/api/JsiSkFont.h +3 -1
  10. package/cpp/api/JsiSkFontStyle.h +3 -1
  11. package/cpp/api/JsiSkHostObjects.h +13 -1
  12. package/cpp/api/JsiSkImage.h +3 -0
  13. package/cpp/api/JsiSkImageInfo.h +3 -1
  14. package/cpp/api/JsiSkMatrix.h +3 -1
  15. package/cpp/api/JsiSkPaint.h +3 -1
  16. package/cpp/api/JsiSkPoint.h +3 -1
  17. package/cpp/api/JsiSkRRect.h +3 -1
  18. package/cpp/api/JsiSkRSXform.h +3 -1
  19. package/cpp/api/JsiSkRect.h +3 -1
  20. package/cpp/api/JsiSkRuntimeShaderBuilder.h +1 -1
  21. package/cpp/api/JsiSkSVGFactory.h +1 -1
  22. package/cpp/jsi2/NativeObject.h +6 -1
  23. package/cpp/rnwgpu/ArrayBuffer.h +7 -2
  24. package/lib/commonjs/renderer/Canvas.d.ts +2 -0
  25. package/lib/commonjs/renderer/Canvas.js +0 -3
  26. package/lib/commonjs/renderer/Canvas.js.map +1 -1
  27. package/lib/commonjs/renderer/Offscreen.d.ts +3 -3
  28. package/lib/commonjs/renderer/Offscreen.js.map +1 -1
  29. package/lib/commonjs/skia/types/Image/Image.d.ts +2 -1
  30. package/lib/commonjs/skia/types/Image/Image.js.map +1 -1
  31. package/lib/commonjs/sksg/Container.native.js +3 -5
  32. package/lib/commonjs/sksg/Container.native.js.map +1 -1
  33. package/lib/module/renderer/Canvas.d.ts +2 -0
  34. package/lib/module/renderer/Canvas.js +0 -3
  35. package/lib/module/renderer/Canvas.js.map +1 -1
  36. package/lib/module/renderer/Offscreen.d.ts +3 -3
  37. package/lib/module/renderer/Offscreen.js.map +1 -1
  38. package/lib/module/skia/types/Image/Image.d.ts +2 -1
  39. package/lib/module/skia/types/Image/Image.js.map +1 -1
  40. package/lib/module/sksg/Container.native.js +3 -5
  41. package/lib/module/sksg/Container.native.js.map +1 -1
  42. package/lib/typescript/lib/commonjs/renderer/Offscreen.d.ts +2 -2
  43. package/lib/typescript/lib/module/renderer/Offscreen.d.ts +2 -2
  44. package/lib/typescript/src/renderer/Canvas.d.ts +2 -0
  45. package/lib/typescript/src/renderer/Offscreen.d.ts +3 -3
  46. package/lib/typescript/src/skia/types/Image/Image.d.ts +2 -1
  47. package/package.json +10 -30
  48. package/react-native-skia.podspec +110 -58
  49. package/src/renderer/Canvas.tsx +2 -3
  50. package/src/renderer/Offscreen.tsx +9 -3
  51. package/src/skia/types/Image/Image.ts +2 -1
  52. package/src/sksg/Container.native.ts +3 -4
  53. package/scripts/install-skia.mjs +0 -728
@@ -4,73 +4,36 @@ cmake_minimum_required(VERSION 3.4.1)
4
4
  set (CMAKE_VERBOSE_MAKEFILE ON)
5
5
  set (CMAKE_CXX_STANDARD 20)
6
6
 
7
- # Import prebuilt SKIA libraries path
8
- set (SKIA_LIBS_PATH "${CMAKE_CURRENT_SOURCE_DIR}/../libs/android/${ANDROID_ABI}")
7
+ # SKIA_LIBS_PATH is passed from Gradle (resolved via Node.js package resolution)
8
+ # Append the ABI to get the full path
9
+ set (SKIA_LIBS_PATH "${SKIA_LIBS_PATH}/${ANDROID_ABI}")
9
10
 
10
11
  # Check if Skia prebuilt binaries are installed
11
- # The postinstall script downloads these - if missing, the user needs to run it
12
12
  if(NOT EXISTS "${SKIA_LIBS_PATH}/libskia.a")
13
13
  message("")
14
14
  message("┌─────────────────────────────────────────────────────────────────────────────┐")
15
15
  message("│ │")
16
16
  message("│ ERROR: Skia prebuilt binaries not found! │")
17
17
  message("│ │")
18
- message("│ The postinstall script has not run. This is required to download the │")
19
- message("│ Skia binaries. Some package managers (pnpm, bun, yarn berry) require │")
20
- message("│ explicit trust for packages with postinstall scripts. │")
18
+ message("│ Could not find libskia.a at: ${SKIA_LIBS_PATH} │")
21
19
  message("│ │")
22
- message("│ To fix this: │")
23
- message("│ │")
24
- message("│ • npm/yarn classic: Run 'npm rebuild @shopify/react-native-skia' or │")
25
- message("│ reinstall the package │")
26
- message("│ │")
27
- message("│ • bun: Run 'bun add --trust @shopify/react-native-skia' │")
28
- message("│ │")
29
- message("│ • pnpm: Add to package.json: │")
30
- message("│ \"pnpm\": { \"onlyBuiltDependencies\": [\"@shopify/react-native-skia\"] }│")
31
- message("│ Then reinstall the package │")
20
+ message("│ Make sure react-native-skia-android is installed: │")
21
+ message("│ yarn add react-native-skia-android │")
32
22
  message("│ │")
33
23
  message("│ See: https://shopify.github.io/react-native-skia/docs/getting-started/installation │")
34
24
  message("│ │")
35
25
  message("└─────────────────────────────────────────────────────────────────────────────┘")
36
26
  message("")
37
- message(FATAL_ERROR "Skia prebuilt binaries not found at ${SKIA_LIBS_PATH}. Please run the postinstall script.")
27
+ message(FATAL_ERROR "Skia prebuilt binaries not found at ${SKIA_LIBS_PATH}")
38
28
  endif()
39
29
 
40
- # Import libskia first so we can check for symbols
30
+ # Import libskia
41
31
  add_library(skia STATIC IMPORTED)
42
32
  set_property(TARGET skia PROPERTY IMPORTED_LOCATION "${SKIA_LIBS_PATH}/libskia.a")
43
33
 
44
- # Check if Graphite is available
45
- # Detection method priority:
46
- # 1. SK_GRAPHITE environment variable (explicit override, fastest)
47
- # 2. Marker file in libs directory (set during Skia build)
48
- # 3. Fall back to nm symbol detection (slow on some CI systems)
49
- set(SK_GRAPHITE_AVAILABLE OFF)
50
-
51
- if(DEFINED ENV{SK_GRAPHITE})
52
- # Explicit override via environment variable
53
- if($ENV{SK_GRAPHITE})
54
- set(SK_GRAPHITE_AVAILABLE ON)
55
- message("-- SK_GRAPHITE detection: using environment variable (ON)")
56
- else()
57
- message("-- SK_GRAPHITE detection: using environment variable (OFF)")
58
- endif()
59
- elseif(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/../libs/android/graphite.enabled")
60
- # Marker file indicates Graphite-enabled build
61
- set(SK_GRAPHITE_AVAILABLE ON)
62
- message("-- SK_GRAPHITE detection: marker file found")
63
- else()
64
- message("-- SK_GRAPHITE detection: no marker file, assuming OFF")
65
- endif()
66
-
67
- if(SK_GRAPHITE_AVAILABLE)
68
- set(SK_GRAPHITE ON)
69
- message("-- SK_GRAPHITE: ON")
70
- else()
71
- set(SK_GRAPHITE OFF)
72
- message("-- SK_GRAPHITE: OFF")
73
- endif()
34
+ # SK_GRAPHITE is passed from Gradle
35
+ message("-- SKIA_LIBS_PATH: ${SKIA_LIBS_PATH}")
36
+ message("-- SK_GRAPHITE: ${SK_GRAPHITE}")
74
37
 
75
38
  string(APPEND CMAKE_CXX_FLAGS " -DSK_BUILD_FOR_ANDROID -DSK_DISABLE_LEGACY_SHAPER_FACTORY -DSK_IMAGE_READ_PIXELS_DISABLE_LEGACY_API -DFOLLY_NO_CONFIG=1 -DFOLLY_HAVE_CLOCK_GETTIME=1 -DFOLLY_HAVE_MEMRCHR=1 -DFOLLY_USE_LIBCPP=1 -DFOLLY_MOBILE=1 -DON_ANDROID -DONANDROID")
76
39
 
@@ -101,7 +64,7 @@ message("-- PREBUILT: " ${PREBUILT_DIR})
101
64
  message("-- BUILD : " ${build_DIR})
102
65
  message("-- LIBRN : " ${LIBRN_DIR})
103
66
 
104
- link_directories(../libs/android/${ANDROID_ABI}/)
67
+ link_directories(${SKIA_LIBS_PATH}/)
105
68
 
106
69
  if(SK_GRAPHITE)
107
70
  add_definitions(-DSK_GRAPHITE)
@@ -56,7 +56,41 @@ static def findNodeModules(baseDir) {
56
56
  throw new GradleException("React-Native-Skia: Failed to find node_modules/ path!")
57
57
  }
58
58
 
59
+ // Resolve npm package path using Node.js resolution (handles monorepos, pnpm, etc.)
60
+ def resolveSkiaPackage(packageName) {
61
+ def cmdResult = providers.exec {
62
+ commandLine "node", "-e", "console.log(require.resolve('${packageName}/package.json'))"
63
+ ignoreExitValue = true
64
+ }
65
+
66
+ if (cmdResult.result.get().exitValue == 0) {
67
+ def packageJsonPath = cmdResult.standardOutput.asText.get().trim()
68
+ return new File(packageJsonPath).parent
69
+ }
70
+
71
+ // Fallback: walk up directories looking for node_modules
72
+ def basePath = projectDir.toPath().normalize()
73
+ while (basePath) {
74
+ def candidate = Paths.get(basePath.toString(), "node_modules", packageName)
75
+ if (candidate.toFile().exists() && new File(candidate.toString(), "package.json").exists()) {
76
+ return candidate.toString()
77
+ }
78
+ basePath = basePath.getParent()
79
+ }
80
+
81
+ throw new GradleException("React-Native-Skia: Could not find ${packageName}. Make sure you have run 'yarn install' or 'npm install'.")
82
+ }
83
+
59
84
  def nodeModules = findNodeModules(projectDir)
85
+
86
+ // Resolve Skia Android package
87
+ def useGraphite = System.getenv("SK_GRAPHITE") == "1" || System.getenv("SK_GRAPHITE") == "true"
88
+ def skiaPackageName = useGraphite ? "react-native-skia-graphite-android" : "react-native-skia-android"
89
+ def skiaAndroidPackage = resolveSkiaPackage(skiaPackageName)
90
+ def skiaLibsPath = "${skiaAndroidPackage}/libs"
91
+
92
+ logger.warn("react-native-skia: SK_GRAPHITE: ${useGraphite}")
93
+ logger.warn("react-native-skia: Skia Android package: ${skiaAndroidPackage}")
60
94
  logger.warn("react-native-skia: node_modules/ found at: ${nodeModules}")
61
95
 
62
96
  def sourceBuild = false
@@ -158,6 +192,8 @@ android {
158
192
  "-DREACT_NATIVE_DIR=${defaultDir}",
159
193
  "-DNODE_MODULES_DIR=${nodeModules}",
160
194
  "-DPREBUILT_DIR=${prebuiltDir}",
195
+ "-DSKIA_LIBS_PATH=${skiaLibsPath}",
196
+ "-DSK_GRAPHITE=${useGraphite ? 'ON' : 'OFF'}",
161
197
  "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON"
162
198
 
163
199
  }
@@ -175,7 +211,7 @@ android {
175
211
 
176
212
  sourceSets.main {
177
213
  jniLibs {
178
- srcDirs += ['../libs/android']
214
+ srcDirs += [skiaLibsPath]
179
215
  }
180
216
  java {
181
217
  if (!isNewArchitectureEnabled()) {
@@ -65,7 +65,6 @@ public:
65
65
  }
66
66
 
67
67
  sk_sp<SkImage> MakeImageFromBuffer(void *buffer) {
68
-
69
68
  CVPixelBufferRef sampleBuffer = (CVPixelBufferRef)buffer;
70
69
  SkiaCVPixelBufferUtils::CVPixelBufferBaseFormat format =
71
70
  SkiaCVPixelBufferUtils::getCVPixelBufferBaseFormat(sampleBuffer);
@@ -73,12 +72,12 @@ public:
73
72
  case SkiaCVPixelBufferUtils::CVPixelBufferBaseFormat::rgb: {
74
73
  // CVPixelBuffer is in any RGB format, single-plane
75
74
  return SkiaCVPixelBufferUtils::RGB::makeSkImageFromCVPixelBuffer(
76
- _directContext.get(), sampleBuffer);
75
+ _device, _directContext.get(), sampleBuffer);
77
76
  }
78
77
  case SkiaCVPixelBufferUtils::CVPixelBufferBaseFormat::yuv: {
79
78
  // CVPixelBuffer is in any YUV format, multi-plane
80
79
  return SkiaCVPixelBufferUtils::YUV::makeSkImageFromCVPixelBuffer(
81
- _directContext.get(), sampleBuffer);
80
+ _device, _directContext.get(), sampleBuffer);
82
81
  }
83
82
  default:
84
83
  [[unlikely]] {
@@ -9,7 +9,7 @@
9
9
 
10
10
  #import <vector>
11
11
 
12
- #import <CoreMedia/CMSampleBuffer.h>
12
+ #import <CoreVideo/CVPixelBuffer.h>
13
13
  #import <CoreVideo/CVMetalTextureCache.h>
14
14
  #import <MetalKit/MetalKit.h>
15
15
 
@@ -84,7 +84,7 @@ public:
84
84
  CVPixelBuffer.
85
85
  */
86
86
  static sk_sp<SkImage>
87
- makeSkImageFromCVPixelBuffer(GrDirectContext *context,
87
+ makeSkImageFromCVPixelBuffer(id<MTLDevice> device, GrDirectContext *context,
88
88
  CVPixelBufferRef pixelBuffer);
89
89
 
90
90
  private:
@@ -98,21 +98,24 @@ public:
98
98
  CVPixelBuffer.
99
99
  */
100
100
  static sk_sp<SkImage>
101
- makeSkImageFromCVPixelBuffer(GrDirectContext *context,
101
+ makeSkImageFromCVPixelBuffer(id<MTLDevice> device, GrDirectContext *context,
102
102
  CVPixelBufferRef pixelBuffer);
103
103
 
104
104
  private:
105
105
  static SkYUVAInfo::PlaneConfig getPlaneConfig(OSType pixelFormat);
106
106
  static SkYUVAInfo::Subsampling getSubsampling(OSType pixelFormat);
107
- static SkYUVColorSpace getColorspace(OSType pixelFormat);
107
+ static SkYUVColorSpace getColorspace(CVPixelBufferRef pixelBuffer);
108
+ static bool isFullRangeYUVFormat(OSType pixelFormat);
109
+ static bool isTenBitYUVFormat(OSType pixelFormat);
110
+ static SkYUVColorSpace getSkYUVColorSpaceFromMatrix(CFStringRef matrix,
111
+ OSType pixelFormat);
108
112
  static SkYUVAInfo getYUVAInfoForCVPixelBuffer(CVPixelBufferRef pixelBuffer);
109
113
  };
110
114
 
111
115
  private:
112
- static CVMetalTextureCacheRef getTextureCache();
113
- static TextureHolder *
114
- getSkiaTextureForCVPixelBufferPlane(CVPixelBufferRef pixelBuffer,
115
- size_t planeIndex);
116
+ static CVMetalTextureCacheRef getTextureCache(id<MTLDevice> device);
117
+ static TextureHolder *getSkiaTextureForCVPixelBufferPlane(
118
+ id<MTLDevice> device, CVPixelBufferRef pixelBuffer, size_t planeIndex);
116
119
  static MTLPixelFormat
117
120
  getMTLPixelFormatForCVPixelBufferPlane(CVPixelBufferRef pixelBuffer,
118
121
  size_t planeIndex);
@@ -13,7 +13,7 @@
13
13
  #import "include/core/SkCanvas.h"
14
14
  #import "include/core/SkColorSpace.h"
15
15
 
16
- #import <CoreMedia/CMSampleBuffer.h>
16
+ #import <CoreVideo/CVImageBuffer.h>
17
17
  #import <CoreVideo/CVMetalTextureCache.h>
18
18
 
19
19
  #import <include/gpu/ganesh/GrBackendSurface.h>
@@ -28,6 +28,7 @@
28
28
  #pragma clang diagnostic pop
29
29
 
30
30
  #include <TargetConditionals.h>
31
+ #include <cmath>
31
32
  #if TARGET_RT_BIG_ENDIAN
32
33
  #define FourCC2Str(fourcc) \
33
34
  (const char[]) { \
@@ -90,6 +91,10 @@ SkiaCVPixelBufferUtils::getCVPixelBufferBaseFormat(
90
91
  case kCVPixelFormatType_420YpCbCr8PlanarFullRange:
91
92
  case kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange:
92
93
  case kCVPixelFormatType_420YpCbCr8BiPlanarFullRange:
94
+ case kCVPixelFormatType_422YpCbCr8BiPlanarVideoRange:
95
+ case kCVPixelFormatType_422YpCbCr8BiPlanarFullRange:
96
+ case kCVPixelFormatType_444YpCbCr8BiPlanarVideoRange:
97
+ case kCVPixelFormatType_444YpCbCr8BiPlanarFullRange:
93
98
  // 10-bit YUV formats
94
99
  case kCVPixelFormatType_420YpCbCr10BiPlanarVideoRange:
95
100
  case kCVPixelFormatType_420YpCbCr10BiPlanarFullRange:
@@ -134,13 +139,14 @@ SkColorType SkiaCVPixelBufferUtils::RGB::getCVPixelBufferColorType(
134
139
  }
135
140
 
136
141
  sk_sp<SkImage> SkiaCVPixelBufferUtils::RGB::makeSkImageFromCVPixelBuffer(
137
- GrDirectContext *context, CVPixelBufferRef pixelBuffer) {
142
+ id<MTLDevice> device, GrDirectContext *context,
143
+ CVPixelBufferRef pixelBuffer) {
138
144
  // 1. Get Skia color type for RGB buffer
139
145
  SkColorType colorType = getCVPixelBufferColorType(pixelBuffer);
140
146
 
141
147
  // 2. Get texture, RGB buffers only have one plane
142
- TextureHolder *texture =
143
- getSkiaTextureForCVPixelBufferPlane(pixelBuffer, /* planeIndex */ 0);
148
+ TextureHolder *texture = getSkiaTextureForCVPixelBufferPlane(
149
+ device, pixelBuffer, /* planeIndex */ 0);
144
150
 
145
151
  // 3. Convert to image with manual memory cleanup
146
152
  return SkImages::BorrowTextureFrom(
@@ -152,15 +158,22 @@ sk_sp<SkImage> SkiaCVPixelBufferUtils::RGB::makeSkImageFromCVPixelBuffer(
152
158
  // pragma MARK: YUV
153
159
 
154
160
  sk_sp<SkImage> SkiaCVPixelBufferUtils::YUV::makeSkImageFromCVPixelBuffer(
155
- GrDirectContext *context, CVPixelBufferRef pixelBuffer) {
161
+ id<MTLDevice> device, GrDirectContext *context,
162
+ CVPixelBufferRef pixelBuffer) {
156
163
  // 1. Get all planes (YUV, Y_UV, Y_U_V or Y_U_V_A)
157
- size_t planesCount = CVPixelBufferGetPlaneCount(pixelBuffer);
164
+ const size_t planesCount = CVPixelBufferGetPlaneCount(pixelBuffer);
165
+ if (planesCount > SkYUVAInfo::kMaxPlanes) [[unlikely]] {
166
+ throw std::runtime_error(
167
+ "CVPixelBuffer has " + std::to_string(planesCount) +
168
+ " textures, but the platform only supports a maximum of " +
169
+ std::to_string(SkYUVAInfo::kMaxPlanes) + " textures!");
170
+ }
158
171
  MultiTexturesHolder *texturesHolder = new MultiTexturesHolder();
159
- GrBackendTexture textures[planesCount];
172
+ GrBackendTexture textures[SkYUVAInfo::kMaxPlanes];
160
173
 
161
174
  for (size_t planeIndex = 0; planeIndex < planesCount; planeIndex++) {
162
175
  TextureHolder *textureHolder =
163
- getSkiaTextureForCVPixelBufferPlane(pixelBuffer, planeIndex);
176
+ getSkiaTextureForCVPixelBufferPlane(device, pixelBuffer, planeIndex);
164
177
  textures[planeIndex] = textureHolder->toGrBackendTexture();
165
178
  texturesHolder->addTexture(textureHolder);
166
179
  }
@@ -190,7 +203,7 @@ SkYUVAInfo SkiaCVPixelBufferUtils::YUV::getYUVAInfoForCVPixelBuffer(
190
203
  OSType format = CVPixelBufferGetPixelFormatType(pixelBuffer);
191
204
  SkYUVAInfo::PlaneConfig planeConfig = getPlaneConfig(format);
192
205
  SkYUVAInfo::Subsampling subsampling = getSubsampling(format);
193
- SkYUVColorSpace colorspace = getColorspace(format);
206
+ SkYUVColorSpace colorspace = getColorspace(pixelBuffer);
194
207
 
195
208
  return SkYUVAInfo(size, planeConfig, subsampling, colorspace);
196
209
  }
@@ -202,9 +215,13 @@ SkiaCVPixelBufferUtils::YUV::getPlaneConfig(OSType pixelFormat) {
202
215
  switch (pixelFormat) {
203
216
  case kCVPixelFormatType_420YpCbCr8Planar:
204
217
  case kCVPixelFormatType_420YpCbCr8PlanarFullRange:
205
- return SkYUVAInfo::PlaneConfig::kYUV;
218
+ return SkYUVAInfo::PlaneConfig::kY_U_V;
206
219
  case kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange:
207
220
  case kCVPixelFormatType_420YpCbCr8BiPlanarFullRange:
221
+ case kCVPixelFormatType_422YpCbCr8BiPlanarVideoRange:
222
+ case kCVPixelFormatType_422YpCbCr8BiPlanarFullRange:
223
+ case kCVPixelFormatType_444YpCbCr8BiPlanarVideoRange:
224
+ case kCVPixelFormatType_444YpCbCr8BiPlanarFullRange:
208
225
  case kCVPixelFormatType_420YpCbCr10BiPlanarVideoRange:
209
226
  case kCVPixelFormatType_420YpCbCr10BiPlanarFullRange:
210
227
  case kCVPixelFormatType_422YpCbCr10BiPlanarVideoRange:
@@ -233,9 +250,13 @@ SkiaCVPixelBufferUtils::YUV::getSubsampling(OSType pixelFormat) {
233
250
  case kCVPixelFormatType_420YpCbCr10BiPlanarVideoRange:
234
251
  case kCVPixelFormatType_420YpCbCr10BiPlanarFullRange:
235
252
  [[likely]] return SkYUVAInfo::Subsampling::k420;
253
+ case kCVPixelFormatType_422YpCbCr8BiPlanarVideoRange:
254
+ case kCVPixelFormatType_422YpCbCr8BiPlanarFullRange:
236
255
  case kCVPixelFormatType_422YpCbCr10BiPlanarVideoRange:
237
256
  case kCVPixelFormatType_422YpCbCr10BiPlanarFullRange:
238
257
  return SkYUVAInfo::Subsampling::k422;
258
+ case kCVPixelFormatType_444YpCbCr8BiPlanarVideoRange:
259
+ case kCVPixelFormatType_444YpCbCr8BiPlanarFullRange:
239
260
  case kCVPixelFormatType_444YpCbCr10BiPlanarVideoRange:
240
261
  case kCVPixelFormatType_444YpCbCr10BiPlanarFullRange:
241
262
  return SkYUVAInfo::Subsampling::k444;
@@ -249,46 +270,122 @@ SkiaCVPixelBufferUtils::YUV::getSubsampling(OSType pixelFormat) {
249
270
 
250
271
  // pragma MARK: YUV getColorspace()
251
272
 
252
- SkYUVColorSpace SkiaCVPixelBufferUtils::YUV::getColorspace(OSType pixelFormat) {
273
+ SkYUVColorSpace SkiaCVPixelBufferUtils::YUV::getColorspace(
274
+ CVPixelBufferRef pixelBuffer) {
275
+ const OSType pixelFormat = CVPixelBufferGetPixelFormatType(pixelBuffer);
276
+
277
+ CFTypeRef matrixAttachment =
278
+ CVBufferCopyAttachment(pixelBuffer, kCVImageBufferYCbCrMatrixKey, nullptr);
279
+ CFStringRef matrix = nullptr;
280
+ if (matrixAttachment != nullptr &&
281
+ CFGetTypeID(matrixAttachment) == CFStringGetTypeID()) {
282
+ matrix = reinterpret_cast<CFStringRef>(matrixAttachment);
283
+ }
284
+
285
+ SkYUVColorSpace colorspace = getSkYUVColorSpaceFromMatrix(matrix, pixelFormat);
286
+ if (matrixAttachment != nullptr) {
287
+ CFRelease(matrixAttachment);
288
+ }
289
+ return colorspace;
290
+ }
291
+
292
+ // pragma MARK: YUV helpers
293
+
294
+ bool SkiaCVPixelBufferUtils::YUV::isFullRangeYUVFormat(OSType pixelFormat) {
253
295
  switch (pixelFormat) {
254
- case kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange:
255
- [[likely]]
256
- // 8-bit limited
257
- return SkYUVColorSpace::kRec709_Limited_SkYUVColorSpace;
258
- case kCVPixelFormatType_420YpCbCr8Planar:
259
296
  case kCVPixelFormatType_420YpCbCr8PlanarFullRange:
260
297
  case kCVPixelFormatType_420YpCbCr8BiPlanarFullRange:
261
- [[likely]]
262
- // 8-bit full
263
- return SkYUVColorSpace::kRec709_Full_SkYUVColorSpace;
298
+ case kCVPixelFormatType_422YpCbCr8BiPlanarFullRange:
299
+ case kCVPixelFormatType_444YpCbCr8BiPlanarFullRange:
300
+ case kCVPixelFormatType_420YpCbCr10BiPlanarFullRange:
301
+ case kCVPixelFormatType_422YpCbCr10BiPlanarFullRange:
302
+ case kCVPixelFormatType_444YpCbCr10BiPlanarFullRange:
303
+ return true;
304
+ default:
305
+ return false;
306
+ }
307
+ }
308
+
309
+ bool SkiaCVPixelBufferUtils::YUV::isTenBitYUVFormat(OSType pixelFormat) {
310
+ switch (pixelFormat) {
264
311
  case kCVPixelFormatType_420YpCbCr10BiPlanarVideoRange:
265
- case kCVPixelFormatType_422YpCbCr10BiPlanarVideoRange:
266
- case kCVPixelFormatType_444YpCbCr10BiPlanarVideoRange:
267
- // 10-bit limited
268
- return SkYUVColorSpace::kBT2020_10bit_Limited_SkYUVColorSpace;
269
312
  case kCVPixelFormatType_420YpCbCr10BiPlanarFullRange:
313
+ case kCVPixelFormatType_422YpCbCr10BiPlanarVideoRange:
270
314
  case kCVPixelFormatType_422YpCbCr10BiPlanarFullRange:
315
+ case kCVPixelFormatType_444YpCbCr10BiPlanarVideoRange:
271
316
  case kCVPixelFormatType_444YpCbCr10BiPlanarFullRange:
272
- // 10-bit full
273
- return SkYUVColorSpace::kBT2020_10bit_Full_SkYUVColorSpace;
274
- // This can be extended with branches for specific YUV formats if Apple
275
- // uses new formats.
317
+ return true;
276
318
  default:
277
- [[unlikely]] throw std::runtime_error("Invalid pixel format! " +
278
- std::string(FourCC2Str(pixelFormat)));
319
+ return false;
320
+ }
321
+ }
322
+
323
+ SkYUVColorSpace SkiaCVPixelBufferUtils::YUV::getSkYUVColorSpaceFromMatrix(
324
+ CFStringRef matrix, OSType pixelFormat) {
325
+ const bool isFullRange = isFullRangeYUVFormat(pixelFormat);
326
+ const bool isTenBit = isTenBitYUVFormat(pixelFormat);
327
+
328
+ if (matrix != nullptr) {
329
+ if (CFEqual(matrix, kCVImageBufferYCbCrMatrix_ITU_R_2020)) {
330
+ if (isTenBit) {
331
+ return isFullRange ? kBT2020_10bit_Full_SkYUVColorSpace
332
+ : kBT2020_10bit_Limited_SkYUVColorSpace;
333
+ }
334
+ return isFullRange ? kBT2020_8bit_Full_SkYUVColorSpace
335
+ : kBT2020_8bit_Limited_SkYUVColorSpace;
336
+ }
337
+ if (CFEqual(matrix, kCVImageBufferYCbCrMatrix_ITU_R_601_4)) {
338
+ return isFullRange ? kJPEG_Full_SkYUVColorSpace
339
+ : kRec601_Limited_SkYUVColorSpace;
340
+ }
341
+ if (CFEqual(matrix, kCVImageBufferYCbCrMatrix_SMPTE_240M_1995)) {
342
+ return isFullRange ? kSMPTE240_Full_SkYUVColorSpace
343
+ : kSMPTE240_Limited_SkYUVColorSpace;
344
+ }
345
+ if (CFEqual(matrix, kCVImageBufferYCbCrMatrix_ITU_R_709_2)) {
346
+ return isFullRange ? kRec709_Full_SkYUVColorSpace
347
+ : kRec709_Limited_SkYUVColorSpace;
348
+ }
349
+ }
350
+
351
+ if (pixelFormat == kCVPixelFormatType_422YpCbCr8BiPlanarVideoRange ||
352
+ pixelFormat == kCVPixelFormatType_422YpCbCr8BiPlanarFullRange) {
353
+ return isFullRange ? kJPEG_Full_SkYUVColorSpace
354
+ : kRec601_Limited_SkYUVColorSpace;
355
+ }
356
+
357
+ // Fallback for buffers that don't provide matrix metadata.
358
+ if (isTenBit) {
359
+ return isFullRange ? kBT2020_10bit_Full_SkYUVColorSpace
360
+ : kBT2020_10bit_Limited_SkYUVColorSpace;
279
361
  }
362
+ return isFullRange ? kRec709_Full_SkYUVColorSpace
363
+ : kRec709_Limited_SkYUVColorSpace;
280
364
  }
281
365
 
282
366
  // pragma MARK: CVPixelBuffer -> Skia Texture
283
367
 
284
368
  TextureHolder *SkiaCVPixelBufferUtils::getSkiaTextureForCVPixelBufferPlane(
285
- CVPixelBufferRef pixelBuffer, size_t planeIndex) {
369
+ id<MTLDevice> device, CVPixelBufferRef pixelBuffer, size_t planeIndex) {
286
370
  // 1. Get cache
287
- CVMetalTextureCacheRef textureCache = getTextureCache();
371
+ CVMetalTextureCacheRef textureCache = getTextureCache(device);
288
372
 
289
- // 2. Get MetalTexture from CMSampleBuffer
290
- size_t width = CVPixelBufferGetWidthOfPlane(pixelBuffer, planeIndex);
291
- size_t height = CVPixelBufferGetHeightOfPlane(pixelBuffer, planeIndex);
373
+ // 2. Get MetalTexture from CVPixelBuffer
374
+ const size_t planesCount = CVPixelBufferGetPlaneCount(pixelBuffer);
375
+ if (planesCount == 0 && planeIndex != 0) [[unlikely]] {
376
+ throw std::runtime_error("Pixel buffer is not planar, but plane index " +
377
+ std::to_string(planeIndex) + " was requested.");
378
+ }
379
+ if (planesCount > 0 && planeIndex >= planesCount) [[unlikely]] {
380
+ throw std::runtime_error(
381
+ "Requested out-of-bounds plane index " + std::to_string(planeIndex) +
382
+ " for pixel buffer with " + std::to_string(planesCount) + " planes.");
383
+ }
384
+ size_t width = planesCount > 0 ? CVPixelBufferGetWidthOfPlane(pixelBuffer, planeIndex)
385
+ : CVPixelBufferGetWidth(pixelBuffer);
386
+ size_t height =
387
+ planesCount > 0 ? CVPixelBufferGetHeightOfPlane(pixelBuffer, planeIndex)
388
+ : CVPixelBufferGetHeight(pixelBuffer);
292
389
  MTLPixelFormat pixelFormat =
293
390
  getMTLPixelFormatForCVPixelBufferPlane(pixelBuffer, planeIndex);
294
391
 
@@ -299,7 +396,7 @@ TextureHolder *SkiaCVPixelBufferUtils::getSkiaTextureForCVPixelBufferPlane(
299
396
 
300
397
  if (result != kCVReturnSuccess) [[unlikely]] {
301
398
  throw std::runtime_error(
302
- "Failed to create Metal Texture from CMSampleBuffer! Result: " +
399
+ "Failed to create Metal Texture from CVPixelBuffer! Result: " +
303
400
  std::to_string(result));
304
401
  }
305
402
 
@@ -308,13 +405,13 @@ TextureHolder *SkiaCVPixelBufferUtils::getSkiaTextureForCVPixelBufferPlane(
308
405
 
309
406
  // pragma MARK: getTextureCache()
310
407
 
311
- CVMetalTextureCacheRef SkiaCVPixelBufferUtils::getTextureCache() {
408
+ CVMetalTextureCacheRef
409
+ SkiaCVPixelBufferUtils::getTextureCache(id<MTLDevice> device) {
312
410
  static CVMetalTextureCacheRef textureCache = nil;
313
411
  if (textureCache == nil) {
314
412
  // Create a new Texture Cache
315
- auto result = CVMetalTextureCacheCreate(kCFAllocatorDefault, nil,
316
- MTLCreateSystemDefaultDevice(), nil,
317
- &textureCache);
413
+ auto result = CVMetalTextureCacheCreate(kCFAllocatorDefault, nil, device,
414
+ nil, &textureCache);
318
415
  if (result != kCVReturnSuccess || textureCache == nil) {
319
416
  throw std::runtime_error("Failed to create Metal Texture Cache!");
320
417
  }
@@ -326,19 +423,102 @@ CVMetalTextureCacheRef SkiaCVPixelBufferUtils::getTextureCache() {
326
423
 
327
424
  MTLPixelFormat SkiaCVPixelBufferUtils::getMTLPixelFormatForCVPixelBufferPlane(
328
425
  CVPixelBufferRef pixelBuffer, size_t planeIndex) {
329
- size_t width = CVPixelBufferGetWidthOfPlane(pixelBuffer, planeIndex);
330
- size_t bytesPerRow =
331
- CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, planeIndex);
332
- double bytesPerPixel = round(static_cast<double>(bytesPerRow) / width);
426
+ const OSType format = CVPixelBufferGetPixelFormatType(pixelBuffer);
427
+ auto throwInvalidPlaneIndexForFormat = [&](size_t expectedPlanes)
428
+ -> MTLPixelFormat {
429
+ throw std::runtime_error(
430
+ "Invalid plane index " + std::to_string(planeIndex) +
431
+ " for pixel format " + std::string(FourCC2Str(format)) + " (expected 0.." +
432
+ std::to_string(expectedPlanes - 1) + ").");
433
+ };
434
+
435
+ switch (format) {
436
+ case kCVPixelFormatType_32BGRA:
437
+ // 1 plane, 8-bit interleaved BGRA.
438
+ if (planeIndex != 0) {
439
+ return throwInvalidPlaneIndexForFormat(1);
440
+ }
441
+ return MTLPixelFormatBGRA8Unorm;
442
+ case kCVPixelFormatType_32RGBA:
443
+ // 1 plane, 8-bit interleaved RGBA.
444
+ if (planeIndex != 0) {
445
+ return throwInvalidPlaneIndexForFormat(1);
446
+ }
447
+ return MTLPixelFormatRGBA8Unorm;
448
+ case kCVPixelFormatType_420YpCbCr8Planar:
449
+ case kCVPixelFormatType_420YpCbCr8PlanarFullRange:
450
+ // 3 planes, 8-bit 4:2:0 planar (Y, U, V), each plane is single channel.
451
+ switch (planeIndex) {
452
+ case 0:
453
+ case 1:
454
+ case 2:
455
+ return MTLPixelFormatR8Unorm;
456
+ default:
457
+ return throwInvalidPlaneIndexForFormat(3);
458
+ }
459
+ case kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange:
460
+ case kCVPixelFormatType_420YpCbCr8BiPlanarFullRange:
461
+ case kCVPixelFormatType_422YpCbCr8BiPlanarVideoRange:
462
+ case kCVPixelFormatType_422YpCbCr8BiPlanarFullRange:
463
+ case kCVPixelFormatType_444YpCbCr8BiPlanarVideoRange:
464
+ case kCVPixelFormatType_444YpCbCr8BiPlanarFullRange:
465
+ // 2 planes, 8-bit bi-planar (plane 0 = Y, plane 1 = interleaved CbCr).
466
+ switch (planeIndex) {
467
+ case 0:
468
+ return MTLPixelFormatR8Unorm;
469
+ case 1:
470
+ return MTLPixelFormatRG8Unorm;
471
+ default:
472
+ return throwInvalidPlaneIndexForFormat(2);
473
+ }
474
+ case kCVPixelFormatType_420YpCbCr10BiPlanarVideoRange:
475
+ case kCVPixelFormatType_420YpCbCr10BiPlanarFullRange:
476
+ case kCVPixelFormatType_422YpCbCr10BiPlanarVideoRange:
477
+ case kCVPixelFormatType_422YpCbCr10BiPlanarFullRange:
478
+ case kCVPixelFormatType_444YpCbCr10BiPlanarVideoRange:
479
+ case kCVPixelFormatType_444YpCbCr10BiPlanarFullRange:
480
+ // 2 planes, 10-bit bi-planar stored in 16-bit lanes (Y + interleaved CbCr).
481
+ switch (planeIndex) {
482
+ case 0:
483
+ return MTLPixelFormatR16Unorm;
484
+ case 1:
485
+ return MTLPixelFormatRG16Unorm;
486
+ default:
487
+ return throwInvalidPlaneIndexForFormat(2);
488
+ }
489
+ default:
490
+ break;
491
+ }
492
+
493
+ const size_t planesCount = CVPixelBufferGetPlaneCount(pixelBuffer);
494
+ const size_t width =
495
+ planesCount > 0 ? CVPixelBufferGetWidthOfPlane(pixelBuffer, planeIndex)
496
+ : CVPixelBufferGetWidth(pixelBuffer);
497
+ const size_t bytesPerRow =
498
+ planesCount > 0 ? CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, planeIndex)
499
+ : CVPixelBufferGetBytesPerRow(pixelBuffer);
500
+ if (width == 0) [[unlikely]] {
501
+ throw std::runtime_error("Invalid plane width for pixel format " +
502
+ std::string(FourCC2Str(format)) + "!");
503
+ }
504
+ if (bytesPerRow % width != 0) [[unlikely]] {
505
+ throw std::runtime_error(
506
+ "Invalid bytes per row! Bytes per row must be evenly divisible by width "
507
+ "for pixel format " +
508
+ std::string(FourCC2Str(format)) + "!");
509
+ }
510
+ const size_t bytesPerPixel = bytesPerRow / width;
333
511
  if (bytesPerPixel == 1) {
334
512
  return MTLPixelFormatR8Unorm;
335
- } else if (bytesPerPixel == 2) {
513
+ }
514
+ if (bytesPerPixel == 2) {
336
515
  return MTLPixelFormatRG8Unorm;
337
- } else if (bytesPerPixel == 4) {
516
+ }
517
+ if (bytesPerPixel == 4) {
338
518
  return MTLPixelFormatBGRA8Unorm;
339
- } else [[unlikely]] {
340
- throw std::runtime_error("Invalid bytes per row! Expected 1 (R), 2 (RG) or "
341
- "4 (RGBA), but received " +
342
- std::to_string(bytesPerPixel));
343
519
  }
520
+
521
+ [[unlikely]] throw std::runtime_error(
522
+ "Invalid bytes per row! Expected 1 (R), 2 (RG) or 4 (RGBA), but received " +
523
+ std::to_string(bytesPerPixel));
344
524
  }
@@ -1,4 +1,5 @@
1
1
  #import <React/RCTBridge.h>
2
+ #import <QuartzCore/CATransaction.h>
2
3
 
3
4
  #import "RNSkiaModule.h"
4
5
  #import "SkiaUIView.h"
@@ -140,7 +141,10 @@
140
141
  - (void)layoutSubviews {
141
142
  [super layoutSubviews];
142
143
  if (_impl != nullptr) {
144
+ [CATransaction begin];
145
+ [CATransaction setDisableActions:YES];
143
146
  _impl->setSize(self.bounds.size.width, self.bounds.size.height);
147
+ [CATransaction commit];
144
148
  }
145
149
  }
146
150