react-native-skia-webgpu-video 0.1.0-beta.3 → 0.1.0-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -32
- package/android/BUILD_VALIDATION.md +1 -1
- package/android/CMakeLists.txt +3 -2
- package/android/cpp/ExportHintSession.cpp +114 -0
- package/android/cpp/ExportHintSession.h +35 -0
- package/android/cpp/VideoEncoderHostObject.cpp +76 -101
- package/android/cpp/VideoEncoderHostObject.h +14 -9
- package/android/cpp/VideoFrame.cpp +3 -5
- package/android/cpp/cpp-adapter.cpp +7 -4
- package/android/src/main/java/com/azzapp/rnskv/GLFrameExtractor.java +3 -9
- package/android/src/main/java/com/azzapp/rnskv/VideoCompositionDecoder.java +8 -11
- package/android/src/main/java/com/azzapp/rnskv/VideoEncoder.java +68 -28
- package/docs/FRAME_PIXEL_FORMAT.md +1 -1
- package/docs/MIGRATION.md +1 -1
- package/docs/UPSTREAM_NOTES.md +15 -2
- package/lib/module/canvas.js +3 -5
- package/lib/module/canvas.js.map +1 -1
- package/lib/module/exportFrames.js +6 -8
- package/lib/module/exportFrames.js.map +1 -1
- package/lib/module/exportTarget.js +183 -40
- package/lib/module/exportTarget.js.map +1 -1
- package/lib/module/exportVideoComposition.js +53 -12
- package/lib/module/exportVideoComposition.js.map +1 -1
- package/lib/module/frameInterop.js +9 -12
- package/lib/module/frameInterop.js.map +1 -1
- package/lib/module/gpu.js +0 -2
- package/lib/module/gpu.js.map +1 -1
- package/lib/module/gpuDevice.js +12 -0
- package/lib/module/gpuDevice.js.map +1 -1
- package/lib/module/previewLifecycle.js +0 -1
- package/lib/module/previewLifecycle.js.map +1 -1
- package/lib/module/yuvBlit.js +28 -21
- package/lib/module/yuvBlit.js.map +1 -1
- package/lib/typescript/src/canvas.d.ts.map +1 -1
- package/lib/typescript/src/exportFrames.d.ts +3 -2
- package/lib/typescript/src/exportFrames.d.ts.map +1 -1
- package/lib/typescript/src/exportTarget.d.ts +53 -14
- package/lib/typescript/src/exportTarget.d.ts.map +1 -1
- package/lib/typescript/src/exportVideoComposition.d.ts +2 -1
- package/lib/typescript/src/exportVideoComposition.d.ts.map +1 -1
- package/lib/typescript/src/frameInterop.d.ts.map +1 -1
- package/lib/typescript/src/gpu.d.ts.map +1 -1
- package/lib/typescript/src/gpuDevice.d.ts +5 -1
- package/lib/typescript/src/gpuDevice.d.ts.map +1 -1
- package/lib/typescript/src/previewLifecycle.d.ts.map +1 -1
- package/lib/typescript/src/types.d.ts +10 -5
- package/lib/typescript/src/types.d.ts.map +1 -1
- package/lib/typescript/src/yuvBlit.d.ts +16 -14
- package/lib/typescript/src/yuvBlit.d.ts.map +1 -1
- package/package.json +6 -6
- package/patches/react-native-skia+3.7.4.patch +146 -0
- package/scripts/skia-canvas-patch.json +22 -258
- package/src/canvas.ts +3 -7
- package/src/exportFrames.ts +8 -9
- package/src/exportTarget.ts +217 -56
- package/src/exportVideoComposition.ts +87 -26
- package/src/frameInterop.ts +9 -11
- package/src/gpu.ts +0 -2
- package/src/gpuDevice.ts +26 -1
- package/src/previewLifecycle.ts +0 -1
- package/src/types.ts +10 -5
- package/src/yuvBlit.ts +43 -28
- package/android/cpp/EncoderTargets.cpp +0 -226
- package/android/cpp/EncoderTargets.h +0 -70
- package/patches/react-native-skia+3.7.1.patch +0 -502
package/src/gpuDevice.ts
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
importDevice,
|
|
9
9
|
installWebGPU,
|
|
10
10
|
} from "react-native-webgpu";
|
|
11
|
-
import type { NativeVideoFrame } from "react-native-webgpu";
|
|
11
|
+
import type { NativeVideoFrame, RNCanvasContext } from "react-native-webgpu";
|
|
12
12
|
import RNSkiaVideoModule from "./RNSkiaVideoModule";
|
|
13
13
|
|
|
14
14
|
// Evaluate on the RN runtime. NativeObject serializers carry these objects to
|
|
@@ -61,11 +61,36 @@ export const createNativeVideoGpuFrame = (
|
|
|
61
61
|
return nativeWebGPU.createVideoFrameFromNativeBuffer(pointer);
|
|
62
62
|
};
|
|
63
63
|
|
|
64
|
+
/** Android: a canvas context presenting to a native window that is not a
|
|
65
|
+
* view, such as an encoder's input Surface. */
|
|
66
|
+
export const createNativeWindowGpuContext = (
|
|
67
|
+
window: bigint,
|
|
68
|
+
width: number,
|
|
69
|
+
height: number,
|
|
70
|
+
): RNCanvasContext => {
|
|
71
|
+
"worklet";
|
|
72
|
+
if (
|
|
73
|
+
typeof nativeWebGPU?.MakeWebGPUCanvasContextFromNativeWindow !== "function"
|
|
74
|
+
)
|
|
75
|
+
throw new Error(
|
|
76
|
+
"React Native WebGPU cannot present to a native window; 0.19.0 or later is required",
|
|
77
|
+
);
|
|
78
|
+
return nativeWebGPU.MakeWebGPUCanvasContextFromNativeWindow(
|
|
79
|
+
window,
|
|
80
|
+
width,
|
|
81
|
+
height,
|
|
82
|
+
);
|
|
83
|
+
};
|
|
84
|
+
|
|
64
85
|
export const nativeVideoTextureUsage =
|
|
65
86
|
GPUTextureUsage.RENDER_ATTACHMENT |
|
|
66
87
|
GPUTextureUsage.TEXTURE_BINDING |
|
|
67
88
|
GPUTextureUsage.COPY_SRC;
|
|
68
89
|
|
|
90
|
+
// An encoder's input window only receives copies of the frames Skia drew.
|
|
91
|
+
export const encoderWindowTextureUsage =
|
|
92
|
+
GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_DST;
|
|
93
|
+
|
|
69
94
|
// CPU RGBA uploads additionally need COPY_DST. Keep the native-buffer blit's
|
|
70
95
|
// descriptor unchanged: that path renders into its destination attachment.
|
|
71
96
|
export const rgbaVideoTextureUsage =
|
package/src/previewLifecycle.ts
CHANGED
package/src/types.ts
CHANGED
|
@@ -421,14 +421,19 @@ export type VideoEncoder = {
|
|
|
421
421
|
*/
|
|
422
422
|
encodeFrame(texture: unknown, time: number): void;
|
|
423
423
|
/**
|
|
424
|
-
*
|
|
425
|
-
*
|
|
426
|
-
* frame (on Android with the sync-fds ending its GPU access, in `fences`),
|
|
427
|
-
* or release it. One at a time.
|
|
424
|
+
* iOS: lends the next pool buffer (a CVPixelBuffer) to draw into on the
|
|
425
|
+
* GPU. Pass it back as a native-buffer frame, or release it. One at a time.
|
|
428
426
|
*/
|
|
429
427
|
acquireFrameBuffer?(): bigint;
|
|
430
|
-
/**
|
|
428
|
+
/** iOS: returns a lent buffer that will not be encoded. */
|
|
431
429
|
releaseFrameBuffer?(): void;
|
|
430
|
+
/**
|
|
431
|
+
* Android: the codec's input Surface, as an ANativeWindow pointer for a
|
|
432
|
+
* WebGPU canvas context to present to. Each frame is then encoded as
|
|
433
|
+
* `{ kind: "window", present }`: the encoder sets the frame's time on the
|
|
434
|
+
* window and calls `present`, which presents the context.
|
|
435
|
+
*/
|
|
436
|
+
acquireInputWindow?(): bigint;
|
|
432
437
|
/*
|
|
433
438
|
* Finish writing the video to the output file.
|
|
434
439
|
*/
|
package/src/yuvBlit.ts
CHANGED
|
@@ -68,40 +68,52 @@ struct VsOut { @builtin(position) position: vec4f, @location(0) uv: vec2f };
|
|
|
68
68
|
// primaries to sRGB, and every export brightens the midtones again.
|
|
69
69
|
const sourceColorSpace = { primaries: "srgb", transfer: "srgb" } as const;
|
|
70
70
|
|
|
71
|
-
/**
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
export const
|
|
71
|
+
/** A decoder buffer's crop rectangle once the buffer is turned clockwise for
|
|
72
|
+
* display: the blit samples that upright region, and draws it at the size of
|
|
73
|
+
* its target, which a capped or explicit frame size makes smaller. Throws
|
|
74
|
+
* unless the rectangle lies in the buffer. */
|
|
75
|
+
export const yuvFrameRegion = (
|
|
76
76
|
region: { x: number; y: number; width: number; height: number },
|
|
77
77
|
rotation: number,
|
|
78
78
|
buffer: { width: number; height: number },
|
|
79
|
-
|
|
80
|
-
): { x: number; y: number } => {
|
|
79
|
+
): { x: number; y: number; width: number; height: number } => {
|
|
81
80
|
"worklet";
|
|
82
81
|
const turned = rotation === 90 || rotation === 270;
|
|
83
82
|
if (
|
|
84
83
|
(rotation !== 0 && !turned && rotation !== 180) ||
|
|
85
84
|
region.x < 0 ||
|
|
86
85
|
region.y < 0 ||
|
|
86
|
+
region.width <= 0 ||
|
|
87
|
+
region.height <= 0 ||
|
|
87
88
|
region.x + region.width > buffer.width ||
|
|
88
|
-
region.y + region.height > buffer.height
|
|
89
|
-
(turned ? region.height : region.width) !== frame.width ||
|
|
90
|
-
(turned ? region.width : region.height) !== frame.height
|
|
89
|
+
region.y + region.height > buffer.height
|
|
91
90
|
) {
|
|
92
91
|
throw new Error("Native video buffer region or YUV layout mismatch");
|
|
93
92
|
}
|
|
94
|
-
const
|
|
93
|
+
const width = turned ? region.height : region.width;
|
|
94
|
+
const height = turned ? region.width : region.height;
|
|
95
95
|
if (rotation === 90)
|
|
96
|
-
return {
|
|
96
|
+
return {
|
|
97
|
+
x: buffer.height - region.y - region.height,
|
|
98
|
+
y: region.x,
|
|
99
|
+
width,
|
|
100
|
+
height,
|
|
101
|
+
};
|
|
97
102
|
if (rotation === 180)
|
|
98
103
|
return {
|
|
99
|
-
x: width - region.x - region.width,
|
|
100
|
-
y: height - region.y - region.height,
|
|
104
|
+
x: buffer.width - region.x - region.width,
|
|
105
|
+
y: buffer.height - region.y - region.height,
|
|
106
|
+
width,
|
|
107
|
+
height,
|
|
101
108
|
};
|
|
102
109
|
if (rotation === 270)
|
|
103
|
-
return {
|
|
104
|
-
|
|
110
|
+
return {
|
|
111
|
+
x: region.y,
|
|
112
|
+
y: buffer.width - region.x - region.width,
|
|
113
|
+
width,
|
|
114
|
+
height,
|
|
115
|
+
};
|
|
116
|
+
return { x: region.x, y: region.y, width, height };
|
|
105
117
|
};
|
|
106
118
|
|
|
107
119
|
const getYuvBlit = (device: GPUDevice): YuvBlit => {
|
|
@@ -251,10 +263,11 @@ const importYuvBuffer = (
|
|
|
251
263
|
};
|
|
252
264
|
|
|
253
265
|
/**
|
|
254
|
-
* Draws the upright `
|
|
255
|
-
*
|
|
256
|
-
*
|
|
257
|
-
* frame's lease or `source`, as
|
|
266
|
+
* Draws the upright `region` of a decoder buffer (see yuvFrameRegion) into
|
|
267
|
+
* the whole of `target`, converting YUV to RGB and scaling it to the target's
|
|
268
|
+
* size; the buffer is imported on its first frame. Submits the draw: the
|
|
269
|
+
* caller waits for it before releasing the frame's lease or `source`, as
|
|
270
|
+
* after copyExternalImageToTexture.
|
|
258
271
|
*/
|
|
259
272
|
export const blitYuvFrame = (
|
|
260
273
|
device: GPUDevice,
|
|
@@ -262,9 +275,7 @@ export const blitYuvFrame = (
|
|
|
262
275
|
producer: number,
|
|
263
276
|
source: NativeVideoFrame,
|
|
264
277
|
rotation: number,
|
|
265
|
-
|
|
266
|
-
width: number,
|
|
267
|
-
height: number,
|
|
278
|
+
region: { x: number; y: number; width: number; height: number },
|
|
268
279
|
target: GPUTexture,
|
|
269
280
|
): void => {
|
|
270
281
|
"worklet";
|
|
@@ -297,10 +308,10 @@ export const blitYuvFrame = (
|
|
|
297
308
|
const frameWidth = turned ? source.height : source.width;
|
|
298
309
|
const frameHeight = turned ? source.width : source.height;
|
|
299
310
|
const mapping = [
|
|
300
|
-
width / frameWidth,
|
|
301
|
-
height / frameHeight,
|
|
302
|
-
|
|
303
|
-
|
|
311
|
+
region.width / frameWidth,
|
|
312
|
+
region.height / frameHeight,
|
|
313
|
+
region.x / frameWidth,
|
|
314
|
+
region.y / frameHeight,
|
|
304
315
|
];
|
|
305
316
|
if (mapping.some((value, i) => value !== entry.mapping[i])) {
|
|
306
317
|
device.queue.writeBuffer(entry.uniforms, 0, new Float32Array(mapping));
|
|
@@ -355,7 +366,11 @@ export const blitNv12Frame = (
|
|
|
355
366
|
size: 16,
|
|
356
367
|
usage: videoUniformBufferUsage,
|
|
357
368
|
});
|
|
358
|
-
device.queue.writeBuffer(
|
|
369
|
+
device.queue.writeBuffer(
|
|
370
|
+
blit.wholeFrame,
|
|
371
|
+
0,
|
|
372
|
+
new Float32Array([1, 1, 0, 0]),
|
|
373
|
+
);
|
|
359
374
|
}
|
|
360
375
|
const external = device.importExternalTexture({
|
|
361
376
|
label: "video decoder buffer",
|
|
@@ -1,226 +0,0 @@
|
|
|
1
|
-
#include "EncoderTargets.h"
|
|
2
|
-
#include "RNSVMemoryBudget.h"
|
|
3
|
-
#include <GLES2/gl2ext.h>
|
|
4
|
-
#include <cerrno>
|
|
5
|
-
#include <poll.h>
|
|
6
|
-
#include <stdexcept>
|
|
7
|
-
#include <string>
|
|
8
|
-
#include <unistd.h>
|
|
9
|
-
|
|
10
|
-
namespace RNSkiaVideo {
|
|
11
|
-
namespace {
|
|
12
|
-
constexpr EGLTimeKHR kWaitNanoseconds = 5'000'000'000ULL;
|
|
13
|
-
constexpr int kFenceTimeoutMs = 5000;
|
|
14
|
-
|
|
15
|
-
struct EglFunctions {
|
|
16
|
-
PFNEGLGETNATIVECLIENTBUFFERANDROIDPROC clientBuffer;
|
|
17
|
-
PFNEGLCREATEIMAGEKHRPROC createImage;
|
|
18
|
-
PFNEGLDESTROYIMAGEKHRPROC destroyImage;
|
|
19
|
-
PFNGLEGLIMAGETARGETTEXTURE2DOESPROC imageTarget;
|
|
20
|
-
PFNEGLCREATESYNCKHRPROC createSync;
|
|
21
|
-
PFNEGLCLIENTWAITSYNCKHRPROC clientWaitSync;
|
|
22
|
-
PFNEGLDESTROYSYNCKHRPROC destroySync;
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
const EglFunctions& egl() {
|
|
26
|
-
static const EglFunctions functions = [] {
|
|
27
|
-
EglFunctions f{
|
|
28
|
-
reinterpret_cast<PFNEGLGETNATIVECLIENTBUFFERANDROIDPROC>(
|
|
29
|
-
eglGetProcAddress("eglGetNativeClientBufferANDROID")),
|
|
30
|
-
reinterpret_cast<PFNEGLCREATEIMAGEKHRPROC>(eglGetProcAddress("eglCreateImageKHR")),
|
|
31
|
-
reinterpret_cast<PFNEGLDESTROYIMAGEKHRPROC>(eglGetProcAddress("eglDestroyImageKHR")),
|
|
32
|
-
reinterpret_cast<PFNGLEGLIMAGETARGETTEXTURE2DOESPROC>(
|
|
33
|
-
eglGetProcAddress("glEGLImageTargetTexture2DOES")),
|
|
34
|
-
reinterpret_cast<PFNEGLCREATESYNCKHRPROC>(eglGetProcAddress("eglCreateSyncKHR")),
|
|
35
|
-
reinterpret_cast<PFNEGLCLIENTWAITSYNCKHRPROC>(eglGetProcAddress("eglClientWaitSyncKHR")),
|
|
36
|
-
reinterpret_cast<PFNEGLDESTROYSYNCKHRPROC>(eglGetProcAddress("eglDestroySyncKHR")),
|
|
37
|
-
};
|
|
38
|
-
if (!f.clientBuffer || !f.createImage || !f.destroyImage || !f.imageTarget ||
|
|
39
|
-
!f.createSync || !f.clientWaitSync || !f.destroySync)
|
|
40
|
-
throw std::runtime_error("EGL hardware buffer or fence entry points are unavailable");
|
|
41
|
-
return f;
|
|
42
|
-
}();
|
|
43
|
-
return functions;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/** Waits for a sync-fd to signal, then closes it. */
|
|
47
|
-
void waitFence(int& fd) {
|
|
48
|
-
if (fd < 0) return; // Already signaled.
|
|
49
|
-
pollfd entry{fd, POLLIN, 0};
|
|
50
|
-
int result;
|
|
51
|
-
do {
|
|
52
|
-
result = poll(&entry, 1, kFenceTimeoutMs);
|
|
53
|
-
} while (result < 0 && errno == EINTR);
|
|
54
|
-
const int error = errno;
|
|
55
|
-
close(fd);
|
|
56
|
-
fd = -1;
|
|
57
|
-
if (result == 0) throw std::runtime_error("GPU export frame did not complete within 5 seconds");
|
|
58
|
-
if (result < 0) throw std::runtime_error("Cannot wait on GPU export fence: errno " + std::to_string(error));
|
|
59
|
-
if (entry.revents & (POLLERR | POLLNVAL))
|
|
60
|
-
throw std::runtime_error("Invalid GPU export fence");
|
|
61
|
-
}
|
|
62
|
-
} // namespace
|
|
63
|
-
|
|
64
|
-
EncoderTargets::EncoderTargets(int width, int height) : width(width), height(height) {
|
|
65
|
-
if (width <= 0 || height <= 0 || width > 16384 || height > 16384)
|
|
66
|
-
throw std::invalid_argument("Invalid encoder buffer dimensions");
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
EncoderTargets::~EncoderTargets() {
|
|
70
|
-
// clear() could not run with the encoder context current: its textures went
|
|
71
|
-
// with that context. Images, syncs and buffers need no current context.
|
|
72
|
-
for (auto& slot : slots) {
|
|
73
|
-
if (slot.reads != EGL_NO_SYNC_KHR) egl().destroySync(slot.display, slot.reads);
|
|
74
|
-
if (slot.image != EGL_NO_IMAGE_KHR) egl().destroyImage(slot.display, slot.image);
|
|
75
|
-
if (slot.buffer) AHardwareBuffer_release(slot.buffer);
|
|
76
|
-
if (slot.reservation) MemoryBudget::instance().release(slot.reservation);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
bool EncoderTargets::waitReads(Slot& slot, EGLTimeKHR timeout) {
|
|
81
|
-
if (slot.reads == EGL_NO_SYNC_KHR) return true;
|
|
82
|
-
const auto status = egl().clientWaitSync(slot.display, slot.reads, 0, timeout);
|
|
83
|
-
if (status == EGL_TIMEOUT_EXPIRED_KHR) return false;
|
|
84
|
-
if (status != EGL_CONDITION_SATISFIED_KHR)
|
|
85
|
-
throw std::runtime_error("Cannot wait on encoder buffer reads: EGL error " + std::to_string(eglGetError()));
|
|
86
|
-
egl().destroySync(slot.display, slot.reads);
|
|
87
|
-
slot.reads = EGL_NO_SYNC_KHR;
|
|
88
|
-
return true;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
void EncoderTargets::allocate(Slot& slot) {
|
|
92
|
-
slot.reservation = MemoryBudget::instance().reserve(
|
|
93
|
-
static_cast<uint64_t>(width) * height * 4, "android encoder ahardwarebuffer");
|
|
94
|
-
AHardwareBuffer_Desc descriptor{};
|
|
95
|
-
descriptor.width = width;
|
|
96
|
-
descriptor.height = height;
|
|
97
|
-
descriptor.layers = 1;
|
|
98
|
-
descriptor.format = AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM;
|
|
99
|
-
descriptor.usage = AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE | AHARDWAREBUFFER_USAGE_GPU_COLOR_OUTPUT;
|
|
100
|
-
const int result = AHardwareBuffer_allocate(&descriptor, &slot.buffer);
|
|
101
|
-
if (result != 0 || !slot.buffer)
|
|
102
|
-
throw std::runtime_error("Encoder AHardwareBuffer allocation failed: " + std::to_string(result));
|
|
103
|
-
AHardwareBuffer_Desc actual{};
|
|
104
|
-
AHardwareBuffer_describe(slot.buffer, &actual);
|
|
105
|
-
if (actual.width != descriptor.width || actual.height != descriptor.height ||
|
|
106
|
-
actual.format != descriptor.format || actual.stride < descriptor.width ||
|
|
107
|
-
(actual.usage & descriptor.usage) != descriptor.usage)
|
|
108
|
-
throw std::runtime_error("Invalid encoder AHardwareBuffer layout");
|
|
109
|
-
MemoryBudget::instance().resize(slot.reservation, static_cast<uint64_t>(actual.stride) * height * 4,
|
|
110
|
-
"android encoder ahardwarebuffer padded stride");
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
void EncoderTargets::bindTexture(Slot& slot) {
|
|
114
|
-
if (slot.texture) return;
|
|
115
|
-
const auto display = eglGetCurrentDisplay();
|
|
116
|
-
if (display == EGL_NO_DISPLAY || eglGetCurrentContext() == EGL_NO_CONTEXT)
|
|
117
|
-
throw std::runtime_error("Encoder buffer import needs the encoder EGL context");
|
|
118
|
-
const EGLint attributes[] = {EGL_IMAGE_PRESERVED_KHR, EGL_TRUE, EGL_NONE};
|
|
119
|
-
slot.display = display;
|
|
120
|
-
slot.image = egl().createImage(display, EGL_NO_CONTEXT, EGL_NATIVE_BUFFER_ANDROID,
|
|
121
|
-
egl().clientBuffer(slot.buffer), attributes);
|
|
122
|
-
if (slot.image == EGL_NO_IMAGE_KHR)
|
|
123
|
-
throw std::runtime_error("Cannot import encoder AHardwareBuffer: EGL error " + std::to_string(eglGetError()));
|
|
124
|
-
GLint previous = 0;
|
|
125
|
-
glGetIntegerv(GL_TEXTURE_BINDING_2D, &previous);
|
|
126
|
-
glGenTextures(1, &slot.texture);
|
|
127
|
-
glBindTexture(GL_TEXTURE_2D, slot.texture);
|
|
128
|
-
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
|
129
|
-
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
|
130
|
-
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
|
131
|
-
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
|
132
|
-
egl().imageTarget(GL_TEXTURE_2D, slot.image);
|
|
133
|
-
glBindTexture(GL_TEXTURE_2D, previous);
|
|
134
|
-
const auto error = glGetError();
|
|
135
|
-
if (error != GL_NO_ERROR)
|
|
136
|
-
throw std::runtime_error("Encoder AHardwareBuffer texture binding failed: GL error " + std::to_string(error));
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
AHardwareBuffer* EncoderTargets::lend() {
|
|
140
|
-
if (lent >= 0 || encoding >= 0)
|
|
141
|
-
throw std::runtime_error("Previous encoder frame buffer was neither encoded nor released");
|
|
142
|
-
// A buffer whose last frame the encoder already sampled.
|
|
143
|
-
for (size_t i = 0; i < slots.size(); ++i) {
|
|
144
|
-
if (waitReads(slots[i], 0)) {
|
|
145
|
-
lent = static_cast<int>(i);
|
|
146
|
-
return slots[i].buffer;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
if (slots.size() < kCapacity) {
|
|
150
|
-
slots.emplace_back();
|
|
151
|
-
try {
|
|
152
|
-
allocate(slots.back());
|
|
153
|
-
} catch (...) {
|
|
154
|
-
auto& slot = slots.back();
|
|
155
|
-
if (slot.buffer) AHardwareBuffer_release(slot.buffer);
|
|
156
|
-
if (slot.reservation) MemoryBudget::instance().release(slot.reservation);
|
|
157
|
-
slots.pop_back();
|
|
158
|
-
throw;
|
|
159
|
-
}
|
|
160
|
-
lent = static_cast<int>(slots.size() - 1);
|
|
161
|
-
return slots.back().buffer;
|
|
162
|
-
}
|
|
163
|
-
for (size_t i = 0; i < slots.size(); ++i) {
|
|
164
|
-
if (waitReads(slots[i], kWaitNanoseconds)) {
|
|
165
|
-
lent = static_cast<int>(i);
|
|
166
|
-
return slots[i].buffer;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
throw std::runtime_error("Encoder buffers stayed in use for 5 seconds");
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
void EncoderTargets::giveBack() { lent = -1; }
|
|
173
|
-
|
|
174
|
-
bool EncoderTargets::isLent(const AHardwareBuffer* buffer) const {
|
|
175
|
-
return lent >= 0 && buffer && slots[lent].buffer == buffer;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
GLuint EncoderTargets::beginEncode(const AHardwareBuffer* buffer, std::vector<int>& fences) {
|
|
179
|
-
if (!isLent(buffer)) throw std::runtime_error("Encoder frame buffer is not the one lent");
|
|
180
|
-
// The renderer's writes, and its release of the buffer, end before GL reads.
|
|
181
|
-
for (auto& fence : fences) waitFence(fence);
|
|
182
|
-
auto& slot = slots[lent];
|
|
183
|
-
bindTexture(slot);
|
|
184
|
-
encoding = lent;
|
|
185
|
-
lent = -1;
|
|
186
|
-
return slot.texture;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
void EncoderTargets::finishEncode() {
|
|
190
|
-
if (encoding < 0) return;
|
|
191
|
-
auto& slot = slots[encoding];
|
|
192
|
-
encoding = -1;
|
|
193
|
-
const auto display = eglGetCurrentDisplay();
|
|
194
|
-
slot.reads = display == EGL_NO_DISPLAY ? EGL_NO_SYNC_KHR
|
|
195
|
-
: egl().createSync(display, EGL_SYNC_FENCE_KHR, nullptr);
|
|
196
|
-
if (slot.reads == EGL_NO_SYNC_KHR) {
|
|
197
|
-
// No fence to poll later: complete the reads now.
|
|
198
|
-
glFinish();
|
|
199
|
-
return;
|
|
200
|
-
}
|
|
201
|
-
slot.display = display;
|
|
202
|
-
glFlush();
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
void EncoderTargets::clear() {
|
|
206
|
-
for (auto& slot : slots) {
|
|
207
|
-
if (!waitReads(slot, kWaitNanoseconds)) glFinish();
|
|
208
|
-
if (slot.reads != EGL_NO_SYNC_KHR) {
|
|
209
|
-
egl().destroySync(slot.display, slot.reads);
|
|
210
|
-
slot.reads = EGL_NO_SYNC_KHR;
|
|
211
|
-
}
|
|
212
|
-
if (slot.texture) glDeleteTextures(1, &slot.texture);
|
|
213
|
-
slot.texture = 0;
|
|
214
|
-
if (slot.image != EGL_NO_IMAGE_KHR) egl().destroyImage(slot.display, slot.image);
|
|
215
|
-
slot.image = EGL_NO_IMAGE_KHR;
|
|
216
|
-
if (slot.buffer) AHardwareBuffer_release(slot.buffer);
|
|
217
|
-
slot.buffer = nullptr;
|
|
218
|
-
if (slot.reservation) MemoryBudget::instance().release(slot.reservation);
|
|
219
|
-
slot.reservation = 0;
|
|
220
|
-
}
|
|
221
|
-
slots.clear();
|
|
222
|
-
lent = -1;
|
|
223
|
-
encoding = -1;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
} // namespace RNSkiaVideo
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
#pragma once
|
|
2
|
-
|
|
3
|
-
#include <EGL/egl.h>
|
|
4
|
-
#include <EGL/eglext.h>
|
|
5
|
-
#include <GLES2/gl2.h>
|
|
6
|
-
#include <android/hardware_buffer.h>
|
|
7
|
-
#include <array>
|
|
8
|
-
#include <cstdint>
|
|
9
|
-
#include <vector>
|
|
10
|
-
|
|
11
|
-
namespace RNSkiaVideo {
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* RGBA AHardwareBuffers lent to JS for the GPU export: Skia renders into one
|
|
15
|
-
* through WebGPU, then the encoder's EGL context samples it into the codec's
|
|
16
|
-
* input Surface. No CPU readback or upload. Bounded, one lent at a time.
|
|
17
|
-
*
|
|
18
|
-
* Methods that touch GL objects require the encoder's EGL context current.
|
|
19
|
-
* A buffer is lent again only once the GL reads of its previous frame are
|
|
20
|
-
* complete; the renderer's own completion is the sync-fd it hands back.
|
|
21
|
-
*/
|
|
22
|
-
class EncoderTargets {
|
|
23
|
-
public:
|
|
24
|
-
static constexpr size_t kCapacity = 3;
|
|
25
|
-
|
|
26
|
-
EncoderTargets(int width, int height);
|
|
27
|
-
~EncoderTargets();
|
|
28
|
-
EncoderTargets(const EncoderTargets&) = delete;
|
|
29
|
-
EncoderTargets& operator=(const EncoderTargets&) = delete;
|
|
30
|
-
|
|
31
|
-
/** A buffer without pending GL reads; allocates up to kCapacity. */
|
|
32
|
-
AHardwareBuffer* lend();
|
|
33
|
-
/** Returns the lent buffer unencoded. Its renderer must have finished. */
|
|
34
|
-
void giveBack();
|
|
35
|
-
bool isLent(const AHardwareBuffer* buffer) const;
|
|
36
|
-
/**
|
|
37
|
-
* Waits on the renderer's sync-fds (taking ownership of each, -1 meaning
|
|
38
|
-
* already signaled) and returns the lent buffer's texture, bound to its
|
|
39
|
-
* EGLImage in the current context. The buffer stays reserved until
|
|
40
|
-
* finishEncode().
|
|
41
|
-
*/
|
|
42
|
-
GLuint beginEncode(const AHardwareBuffer* buffer, std::vector<int>& fences);
|
|
43
|
-
/** After the draw sampling the texture, with the encoder context current. */
|
|
44
|
-
void finishEncode();
|
|
45
|
-
/** Encoder context current: waits for pending reads and frees everything. */
|
|
46
|
-
void clear();
|
|
47
|
-
bool empty() const { return slots.empty(); }
|
|
48
|
-
|
|
49
|
-
private:
|
|
50
|
-
struct Slot {
|
|
51
|
-
AHardwareBuffer* buffer = nullptr;
|
|
52
|
-
uint64_t reservation = 0;
|
|
53
|
-
EGLDisplay display = EGL_NO_DISPLAY;
|
|
54
|
-
EGLImageKHR image = EGL_NO_IMAGE_KHR;
|
|
55
|
-
GLuint texture = 0;
|
|
56
|
-
EGLSyncKHR reads = EGL_NO_SYNC_KHR;
|
|
57
|
-
};
|
|
58
|
-
int width;
|
|
59
|
-
int height;
|
|
60
|
-
std::vector<Slot> slots;
|
|
61
|
-
// The lent slot, then the slot being encoded; -1 when none.
|
|
62
|
-
int lent = -1;
|
|
63
|
-
int encoding = -1;
|
|
64
|
-
|
|
65
|
-
bool waitReads(Slot& slot, EGLTimeKHR timeout);
|
|
66
|
-
void allocate(Slot& slot);
|
|
67
|
-
void bindTexture(Slot& slot);
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
} // namespace RNSkiaVideo
|