react-native-skia-webgpu-video 0.1.0-beta.4 → 0.1.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 (46) hide show
  1. package/README.md +21 -23
  2. package/android/BUILD_VALIDATION.md +1 -1
  3. package/docs/FRAME_PIXEL_FORMAT.md +16 -1
  4. package/docs/UPSTREAM_NOTES.md +28 -1
  5. package/ios/RNSVColorSpace.h +8 -3
  6. package/lib/module/exportFrames.js +2 -2
  7. package/lib/module/exportFrames.js.map +1 -1
  8. package/lib/module/exportVideoComposition.js +25 -23
  9. package/lib/module/exportVideoComposition.js.map +1 -1
  10. package/lib/module/frameInterop.js +314 -70
  11. package/lib/module/frameInterop.js.map +1 -1
  12. package/lib/module/gpu.js +1 -1
  13. package/lib/module/gpu.js.map +1 -1
  14. package/lib/module/memory.js +7 -0
  15. package/lib/module/memory.js.map +1 -1
  16. package/lib/module/previewLifecycle.js +2 -0
  17. package/lib/module/previewLifecycle.js.map +1 -1
  18. package/lib/module/videoCompositionPlayer.js +65 -14
  19. package/lib/module/videoCompositionPlayer.js.map +1 -1
  20. package/lib/module/videoFrameImage.js +1 -1
  21. package/lib/module/yuvBlit.js +3 -3
  22. package/lib/typescript/src/exportVideoComposition.d.ts.map +1 -1
  23. package/lib/typescript/src/frameInterop.d.ts +16 -5
  24. package/lib/typescript/src/frameInterop.d.ts.map +1 -1
  25. package/lib/typescript/src/memory.d.ts +2 -0
  26. package/lib/typescript/src/memory.d.ts.map +1 -1
  27. package/lib/typescript/src/previewLifecycle.d.ts +2 -0
  28. package/lib/typescript/src/previewLifecycle.d.ts.map +1 -1
  29. package/lib/typescript/src/types.d.ts +8 -7
  30. package/lib/typescript/src/types.d.ts.map +1 -1
  31. package/lib/typescript/src/videoCompositionPlayer.d.ts +4 -5
  32. package/lib/typescript/src/videoCompositionPlayer.d.ts.map +1 -1
  33. package/lib/typescript/src/videoFrameImage.d.ts +1 -1
  34. package/package.json +6 -6
  35. package/patches/{react-native-skia+3.7.4.patch → react-native-skia+3.10.0.patch} +7 -3
  36. package/scripts/skia-canvas-patch.json +6 -6
  37. package/src/exportFrames.ts +2 -2
  38. package/src/exportVideoComposition.ts +43 -59
  39. package/src/frameInterop.ts +380 -85
  40. package/src/gpu.ts +1 -1
  41. package/src/memory.ts +6 -0
  42. package/src/previewLifecycle.ts +4 -0
  43. package/src/types.ts +8 -7
  44. package/src/videoCompositionPlayer.ts +76 -23
  45. package/src/videoFrameImage.ts +1 -1
  46. package/src/yuvBlit.ts +3 -3
@@ -1,6 +1,6 @@
1
1
  --- a/node_modules/react-native-skia/cpp/rnskia/RNDawnContext.h
2
2
  +++ b/node_modules/react-native-skia/cpp/rnskia/RNDawnContext.h
3
- @@ -481,6 +481,40 @@
3
+ @@ -492,6 +492,44 @@
4
4
  }
5
5
  current->context->submit(syncToCpu);
6
6
  return inserted;
@@ -17,6 +17,10 @@
17
17
  + throw std::runtime_error(
18
18
  + "Checked GPU submissions require a Graphite recorder");
19
19
  + }
20
+ + // As in submitRecording: the snap compiles the pipelines the recording
21
+ + // needs, inline, GPU work the pipeline warmup stays behind (see
22
+ + // RNSkGpuActivity), as is the run.
23
+ + RNSkGpuActivity::Scope activity;
20
24
  + auto recording = recorder->snap();
21
25
  + if (recording == nullptr) {
22
26
  + throw std::runtime_error("Cannot create checked Graphite recording");
@@ -118,7 +122,7 @@
118
122
  } // namespace RNSkia
119
123
  --- a/node_modules/react-native-skia/cpp/api/JsiSkApi.h
120
124
  +++ b/node_modules/react-native-skia/cpp/api/JsiSkApi.h
121
- @@ -159,6 +159,16 @@
125
+ @@ -167,6 +167,16 @@
122
126
  }
123
127
  JSI_HOST_FUNCTION(Color) {
124
128
  return JsiSkColor::createCtor()(runtime, thisValue, arguments, count);
@@ -135,7 +139,7 @@
135
139
  }
136
140
  JSI_HOST_FUNCTION(getNativeDevice) {
137
141
  // Raw WGPUDevice pointer of the Graphite device, as a BigInt. Consumed by
138
- @@ -264,6 +274,8 @@
142
+ @@ -267,6 +277,8 @@
139
143
  }
140
144
 
141
145
  static void definePrototype(jsi::Runtime &runtime, jsi::Object &prototype) {
@@ -1,13 +1,13 @@
1
1
  {
2
- "version": "3.7.4",
2
+ "version": "3.10.0",
3
3
  "files": {
4
4
  "cpp/rnskia/RNDawnContext.h": {
5
- "before": "7381d2c92b739deffa8429b72d66442b0a40655f3c66ce354bab81d2e058f874",
6
- "after": "1846064f3db7eaca296a2f9c3c5fd45904300437cd79d1eeab771f63cf644e34",
5
+ "before": "ed26c1390ac4912089b354651c970801d42db7318ed40c51c19357b2252c52d4",
6
+ "after": "bceaaf3cabcde96d4d3843210d74e2de23991631e85427ec34c4b481ff30e5a0",
7
7
  "edits": [
8
8
  {
9
9
  "before": " current->context->submit(syncToCpu);\n return inserted;\n }\n",
10
- "after": " current->context->submit(syncToCpu);\n return inserted;\n }\n\n // Checked only for surfaces that explicitly opt into this contract: like\n // submitRecording, but a recording that would not run throws instead of\n // being dropped. A valid empty Recording is accepted; Recorder::snap()\n // returns nullptr on failure.\n void submitRecordingChecked(\n skgpu::graphite::Recorder *recorder,\n skgpu::graphite::SyncToCpu syncToCpu = skgpu::graphite::SyncToCpu::kNo) {\n if (recorder == nullptr) {\n throw std::runtime_error(\n \"Checked GPU submissions require a Graphite recorder\");\n }\n auto recording = recorder->snap();\n if (recording == nullptr) {\n throw std::runtime_error(\"Cannot create checked Graphite recording\");\n }\n std::lock_guard<std::mutex> lock(_mutex);\n auto *current = _current.load(std::memory_order_acquire);\n if (generationOf(recorder) != current->id ||\n current->lost.load(std::memory_order_acquire)) {\n throw std::runtime_error(\n \"The GPU device was lost; a checked recording cannot run\");\n }\n skgpu::graphite::InsertRecordingInfo info;\n info.fRecording = recording.get();\n auto status = current->context->insertRecording(info);\n if (!static_cast<bool>(status)) {\n throw std::runtime_error(\"Graphite rejected a checked recording: \" +\n status.message());\n }\n if (!current->context->submit(syncToCpu)) {\n throw std::runtime_error(\"Graphite checked submission failed\");\n }\n }\n"
10
+ "after": " current->context->submit(syncToCpu);\n return inserted;\n }\n\n // Checked only for surfaces that explicitly opt into this contract: like\n // submitRecording, but a recording that would not run throws instead of\n // being dropped. A valid empty Recording is accepted; Recorder::snap()\n // returns nullptr on failure.\n void submitRecordingChecked(\n skgpu::graphite::Recorder *recorder,\n skgpu::graphite::SyncToCpu syncToCpu = skgpu::graphite::SyncToCpu::kNo) {\n if (recorder == nullptr) {\n throw std::runtime_error(\n \"Checked GPU submissions require a Graphite recorder\");\n }\n // As in submitRecording: the snap compiles the pipelines the recording\n // needs, inline, GPU work the pipeline warmup stays behind (see\n // RNSkGpuActivity), as is the run.\n RNSkGpuActivity::Scope activity;\n auto recording = recorder->snap();\n if (recording == nullptr) {\n throw std::runtime_error(\"Cannot create checked Graphite recording\");\n }\n std::lock_guard<std::mutex> lock(_mutex);\n auto *current = _current.load(std::memory_order_acquire);\n if (generationOf(recorder) != current->id ||\n current->lost.load(std::memory_order_acquire)) {\n throw std::runtime_error(\n \"The GPU device was lost; a checked recording cannot run\");\n }\n skgpu::graphite::InsertRecordingInfo info;\n info.fRecording = recording.get();\n auto status = current->context->insertRecording(info);\n if (!static_cast<bool>(status)) {\n throw std::runtime_error(\"Graphite rejected a checked recording: \" +\n status.message());\n }\n if (!current->context->submit(syncToCpu)) {\n throw std::runtime_error(\"Graphite checked submission failed\");\n }\n }\n"
11
11
  }
12
12
  ]
13
13
  },
@@ -38,8 +38,8 @@
38
38
  ]
39
39
  },
40
40
  "cpp/api/JsiSkApi.h": {
41
- "before": "72c8c6828c16ad9c764a749d5140f1a3536f83dd582dce65d6e7f42d88587a30",
42
- "after": "9920a47ed26d4713b1f515f2acb54663ca47f44426d3110aed47f0193925ca49",
41
+ "before": "07846e9776f57bd2638e2e3092931e5a1324d66710693a2f7e550f648fe13934",
42
+ "after": "5c0bf4231d658366c8a902221238c3a244b6444cf407468f80c1cd1718b2be16",
43
43
  "edits": [
44
44
  {
45
45
  "before": " JSI_HOST_FUNCTION(getNativeDevice) {\n",
@@ -223,7 +223,7 @@ const importYuvExportFrame = (
223
223
  region!,
224
224
  target.texture,
225
225
  );
226
- entry.image = Skia.Image.MakeImageFromGPUTexture(target.texture);
226
+ entry.image = Skia.Image.MakeFromGPUTexture(target.texture);
227
227
  if (!entry.image) throw new Error("Cannot wrap the decoded frame");
228
228
  return entry;
229
229
  } catch (error) {
@@ -290,7 +290,7 @@ export const importExportFrame = (
290
290
  entry.texture = entry.memory.createTexture();
291
291
  entry.memory.beginAccess(entry.texture, true);
292
292
  entry.accessing = true;
293
- entry.image = Skia.Image.MakeImageFromGPUTexture(entry.texture);
293
+ entry.image = Skia.Image.MakeFromGPUTexture(entry.texture);
294
294
  if (!entry.image) throw new Error("Cannot wrap the decoded frame");
295
295
  return entry;
296
296
  } catch (error) {
@@ -11,7 +11,7 @@ import {
11
11
  type WorkletRuntime,
12
12
  } from "react-native-worklets";
13
13
  import { AlphaType, BlendMode, ColorType, Skia } from "react-native-skia";
14
- import type { ImageInfo, SkCanvas, SkSurface } from "react-native-skia";
14
+ import type { SkCanvas, SkSurface } from "react-native-skia";
15
15
  import { Platform } from "react-native";
16
16
  import type {
17
17
  ExportOptions,
@@ -121,25 +121,11 @@ export type VideoExportArguments<T = undefined> = ExportOptions & {
121
121
  onProfile?: (profile: ExportProfile) => void;
122
122
  };
123
123
 
124
- // Native Skia 3.4.4 accepts destination/stride in readPixels; its
125
- // published Canvas type currently omits these arguments. Use only a private,
126
- // full-length, zero-offset Uint8Array (the native implementation ignores offsets).
127
- type ReadbackCanvas = {
128
- readPixels: (
129
- x: number,
130
- y: number,
131
- info: ImageInfo,
132
- destination: Uint8Array,
133
- bytesPerRow: number,
134
- ) => Uint8Array | Float32Array | null;
135
- };
136
-
137
124
  type ExportState = {
138
125
  encoder: VideoEncoder | null;
139
126
  extractor: VideoCompositionFramesExtractorSync | null;
140
127
  surface: SkSurface | null;
141
128
  canvas: SkCanvas | null;
142
- readback: Uint8Array | null;
143
129
  /** Draw into a lent encoder buffer instead of reading pixels back. */
144
130
  gpu: boolean;
145
131
  target: EncoderTarget | null;
@@ -401,7 +387,6 @@ const executeExport = <T>(args: VideoExportArguments<T>): Promise<void> =>
401
387
  }
402
388
  if (cleanup.safeToRelease) {
403
389
  state.frames = {};
404
- state.readback = null;
405
390
  delete storage.__rnskwgpuExport;
406
391
  } else {
407
392
  // One retained session at most; subsequent exports fail before
@@ -463,6 +448,10 @@ const executeExport = <T>(args: VideoExportArguments<T>): Promise<void> =>
463
448
  await finish(null, true);
464
449
  return false;
465
450
  }
451
+ // CPU readback path only: the frame's pixels, still on their way.
452
+ const readback: {
453
+ pixels: ReturnType<SkCanvas["readPixels"]> | null;
454
+ } = { pixels: null };
466
455
  const draw = () => {
467
456
  "worklet";
468
457
  if (state.gpu)
@@ -517,46 +506,14 @@ const executeExport = <T>(args: VideoExportArguments<T>): Promise<void> =>
517
506
  time,
518
507
  );
519
508
  } else {
520
- const readbackCanvas = canvas as unknown as ReadbackCanvas;
521
- const pixels = readbackCanvas.readPixels(
522
- 0,
523
- 0,
524
- {
525
- width: options.width,
526
- height: options.height,
527
- colorType: bgra ? ColorType.BGRA_8888 : ColorType.RGBA_8888,
528
- alphaType: AlphaType.Premul,
529
- },
530
- state.readback!,
531
- options.width * 4,
532
- );
533
- if (
534
- !(pixels instanceof Uint8Array) ||
535
- pixels.byteLength !== bytes ||
536
- pixels.byteOffset !== 0 ||
537
- pixels.buffer !== state.readback!.buffer
538
- ) {
539
- throw new Error("Video export pixel readback failed");
540
- }
541
- // Fallback: consumed synchronously by the bounded native pool.
542
- const storage =
543
- pixels.byteOffset === 0 &&
544
- pixels.buffer.byteLength === pixels.byteLength
545
- ? pixels.buffer
546
- : pixels.buffer.slice(
547
- pixels.byteOffset,
548
- pixels.byteOffset + pixels.byteLength,
549
- );
550
- state.encoder!.encodeFrame(
551
- {
552
- kind: bgra ? "bgra" : "rgba",
553
- data: storage,
554
- width: options.width,
555
- height: options.height,
556
- bytesPerRow: options.width * 4,
557
- },
558
- time,
559
- );
509
+ // Skia 3.9 reads back asynchronously, into a new array: the
510
+ // frame is encoded once it lands, after this scope.
511
+ readback.pixels = canvas.readPixels(0, 0, {
512
+ width: options.width,
513
+ height: options.height,
514
+ colorType: bgra ? ColorType.BGRA_8888 : ColorType.RGBA_8888,
515
+ alphaType: AlphaType.Premul,
516
+ });
560
517
  }
561
518
  lap("encodeMs");
562
519
  } catch (error) {
@@ -593,6 +550,35 @@ const executeExport = <T>(args: VideoExportArguments<T>): Promise<void> =>
593
550
  if (drawn.failed) throw drawn.failure;
594
551
  };
595
552
  runWithVideoAutoreleasePool(draw);
553
+ if (readback.pixels) {
554
+ const pixels = await readback.pixels;
555
+ lap("gpuMs");
556
+ // isView, not instanceof: true of a typed array of any realm.
557
+ if (!ArrayBuffer.isView(pixels) || pixels.byteLength !== bytes)
558
+ throw new Error("Video export pixel readback failed");
559
+ // Fallback: consumed synchronously by the bounded native pool.
560
+ const storage =
561
+ pixels.byteOffset === 0 &&
562
+ pixels.buffer.byteLength === pixels.byteLength
563
+ ? pixels.buffer
564
+ : pixels.buffer.slice(
565
+ pixels.byteOffset,
566
+ pixels.byteOffset + pixels.byteLength,
567
+ );
568
+ runWithVideoAutoreleasePool(() =>
569
+ state.encoder!.encodeFrame(
570
+ {
571
+ kind: bgra ? "bgra" : "rgba",
572
+ data: storage,
573
+ width: options.width,
574
+ height: options.height,
575
+ bytesPerRow: options.width * 4,
576
+ },
577
+ time,
578
+ ),
579
+ );
580
+ lap("encodeMs");
581
+ }
596
582
  state.index++;
597
583
  if (onProgress)
598
584
  scheduleOnRN(onProgress, {
@@ -660,7 +646,6 @@ const executeExport = <T>(args: VideoExportArguments<T>): Promise<void> =>
660
646
  extractor: null,
661
647
  surface: null,
662
648
  canvas: null,
663
- readback: null,
664
649
  gpu: false,
665
650
  target: null,
666
651
  window: null,
@@ -690,10 +675,9 @@ const executeExport = <T>(args: VideoExportArguments<T>): Promise<void> =>
690
675
  state.reservations.push(
691
676
  reserveVideoMemory(
692
677
  bytes * 2,
693
- "export reusable readback and transient raster",
678
+ "export readback and transient raster",
694
679
  ),
695
680
  );
696
- state.readback = new Uint8Array(bytes);
697
681
  state.hasRecorder = true;
698
682
  state.surface = Skia.Surface.MakeOffscreen(
699
683
  options.width,