react-native-alpha-video-gpu 0.0.0-stage → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +953 -2
  3. package/THIRD_PARTY_NOTICES.md +14 -0
  4. package/android/src/main/AndroidManifest.xml +9 -0
  5. package/android/src/main/cpp/CMakeLists.txt +41 -0
  6. package/android/src/main/cpp/alphavideogpu_jni.cpp +1168 -0
  7. package/android/src/main/cpp/astc_decomp.cpp +1550 -0
  8. package/android/src/main/cpp/astc_decomp.h +41 -0
  9. package/android/src/main/cpp/source_jni.cpp +242 -0
  10. package/android/src/main/cpp/source_jni.h +13 -0
  11. package/android/src/main/java/dev/mediacache/alphavideo/gpu/AlphaVideoGpuNative.java +43 -0
  12. package/android/src/main/java/dev/mediacache/alphavideo/gpu/AlphaVideoGpuReader.java +209 -0
  13. package/android/src/main/java/dev/mediacache/alphavideo/gpu/AlphaVideoGpuSource.java +328 -0
  14. package/android/src/main/java/dev/mediacache/alphavideo/gpu/AlphaVideoGpuView.java +2126 -0
  15. package/android/src/main/java/dev/mediacache/alphavideo/gpu/Fetcher.java +37 -0
  16. package/android/src/main/java/dev/mediacache/alphavideo/gpu/HttpFetcher.java +156 -0
  17. package/android/src/main/java/dev/mediacache/alphavideo/gpu/Ktx2Metadata.java +78 -0
  18. package/android/src/main/java/dev/mediacache/alphavideo/gpu/RangeSourceLink.java +54 -0
  19. package/android/src/main/java/dev/mediacache/alphavideo/gpu/ZstdSeekTable.java +120 -0
  20. package/android/src/rangesource/java/dev/mediacache/alphavideo/gpu/AppDataSource.java +50 -0
  21. package/ios/Sources/AlphaVideoGPU/AlphaVideoGpuSource.h +63 -0
  22. package/ios/Sources/AlphaVideoGPU/AlphaVideoGpuSource.mm +522 -0
  23. package/ios/Sources/AlphaVideoGPU/AlphaVideoGpuView.mm +2722 -0
  24. package/ios/Sources/AlphaVideoGPU/astc_decomp.cpp +1550 -0
  25. package/ios/Sources/AlphaVideoGPU/astc_decomp.h +41 -0
  26. package/ios/Sources/AlphaVideoGPU/include/AlphaVideoGPU.h +110 -0
  27. package/ios/Sources/AlphaVideoGPU/zstd_seek.h +250 -0
  28. package/package.json +78 -4
  29. package/react-native/android/build.gradle +95 -0
  30. package/react-native/android/src/main/AndroidManifest.xml +9 -0
  31. package/react-native/android/src/main/java/dev/mediacache/alphavideo/gpu/reactnative/AlphaVideoGpuPackage.java +26 -0
  32. package/react-native/android/src/main/java/dev/mediacache/alphavideo/gpu/reactnative/AlphaVideoGpuViewManager.java +187 -0
  33. package/react-native/android/src/main/java/dev/mediacache/alphavideo/gpu/reactnative/PlayerEventForwarder.java +107 -0
  34. package/react-native/android/src/newarch/java/dev/mediacache/alphavideo/gpu/reactnative/AlphaVideoGpuViewManagerSpec.java +23 -0
  35. package/react-native/android/src/norangesource/java/dev/mediacache/alphavideo/gpu/AppDataSource.java +15 -0
  36. package/react-native/android/src/oldarch/java/dev/mediacache/alphavideo/gpu/reactnative/AlphaVideoGpuViewManagerSpec.java +56 -0
  37. package/react-native/ios/RNAlphaVideoGpuViewComponentView.mm +176 -0
  38. package/react-native/ios/RNAlphaVideoGpuViewManager.mm +175 -0
  39. package/react-native/src/AlphaVideoGpuView.js +2 -0
  40. package/react-native/src/AlphaVideoGpuView.web.tsx +296 -0
  41. package/react-native/src/AlphaVideoGpuViewNativeComponent.ts +128 -0
  42. package/react-native/src/AlphaVideoGpuViewNativeComponent.web.ts +2 -0
  43. package/react-native/src/index.d.ts +95 -0
  44. package/react-native/src/index.js +162 -0
  45. package/react-native-alpha-video-gpu.podspec +37 -0
  46. package/react-native.config.js +12 -0
  47. package/third_party/licenses/Apache-2.0.txt +208 -0
  48. package/third_party/range-source/LICENSE +21 -0
  49. package/third_party/range-source/README.md +10 -0
  50. package/third_party/range-source/include/range_source.h +395 -0
  51. package/third_party/range-source/include/range_source_download.h +309 -0
  52. package/third_party/zstd/LICENSE +30 -0
  53. package/third_party/zstd/zstd.c +38717 -0
  54. package/third_party/zstd/zstd.h +2450 -0
@@ -0,0 +1,1168 @@
1
+ #include <jni.h>
2
+ #include <android/log.h>
3
+ #include <android/asset_manager.h>
4
+ #include <android/asset_manager_jni.h>
5
+ #include <GLES3/gl3.h>
6
+ #include <GLES3/gl3ext.h>
7
+ #include <GLES2/gl2ext.h>
8
+ #include <EGL/egl.h>
9
+ #include <algorithm>
10
+ #include <memory>
11
+ #include <vector>
12
+ #include <string>
13
+ #include <cstring>
14
+ #include <map>
15
+ #include <mutex>
16
+ #include <atomic>
17
+
18
+ #define ZSTD_STATIC_LINKING_ONLY
19
+ #include "zstd/zstd.h"
20
+
21
+ #include "source_jni.h"
22
+
23
+ // Software ASTC decoder, for GPUs without ASTC (emulators)
24
+ #include "astc_decomp.h"
25
+
26
+ #define LOG_TAG "AlphaVideoGpu"
27
+ // Info traces are compiled in only when ALPHA_VIDEO_GPU_DEBUG_LOGS is defined.
28
+ #ifdef ALPHA_VIDEO_GPU_DEBUG_LOGS
29
+ #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
30
+ #else
31
+ #define LOGI(...) ((void)0)
32
+ #endif
33
+ #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
34
+ #define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__)
35
+
36
+ // GL_COMPRESSED_RGBA_ASTC constants for all 14 block sizes
37
+ #ifndef GL_COMPRESSED_RGBA_ASTC_4x4_KHR
38
+ #define GL_COMPRESSED_RGBA_ASTC_4x4_KHR 0x93B0
39
+ #endif
40
+ #ifndef GL_COMPRESSED_RGBA_ASTC_5x4_KHR
41
+ #define GL_COMPRESSED_RGBA_ASTC_5x4_KHR 0x93B1
42
+ #endif
43
+ #ifndef GL_COMPRESSED_RGBA_ASTC_5x5_KHR
44
+ #define GL_COMPRESSED_RGBA_ASTC_5x5_KHR 0x93B2
45
+ #endif
46
+ #ifndef GL_COMPRESSED_RGBA_ASTC_6x5_KHR
47
+ #define GL_COMPRESSED_RGBA_ASTC_6x5_KHR 0x93B3
48
+ #endif
49
+ #ifndef GL_COMPRESSED_RGBA_ASTC_6x6_KHR
50
+ #define GL_COMPRESSED_RGBA_ASTC_6x6_KHR 0x93B4
51
+ #endif
52
+ #ifndef GL_COMPRESSED_RGBA_ASTC_8x5_KHR
53
+ #define GL_COMPRESSED_RGBA_ASTC_8x5_KHR 0x93B5
54
+ #endif
55
+ #ifndef GL_COMPRESSED_RGBA_ASTC_8x6_KHR
56
+ #define GL_COMPRESSED_RGBA_ASTC_8x6_KHR 0x93B6
57
+ #endif
58
+ #ifndef GL_COMPRESSED_RGBA_ASTC_8x8_KHR
59
+ #define GL_COMPRESSED_RGBA_ASTC_8x8_KHR 0x93B7
60
+ #endif
61
+ #ifndef GL_COMPRESSED_RGBA_ASTC_10x5_KHR
62
+ #define GL_COMPRESSED_RGBA_ASTC_10x5_KHR 0x93B8
63
+ #endif
64
+ #ifndef GL_COMPRESSED_RGBA_ASTC_10x6_KHR
65
+ #define GL_COMPRESSED_RGBA_ASTC_10x6_KHR 0x93B9
66
+ #endif
67
+ #ifndef GL_COMPRESSED_RGBA_ASTC_10x8_KHR
68
+ #define GL_COMPRESSED_RGBA_ASTC_10x8_KHR 0x93BA
69
+ #endif
70
+ #ifndef GL_COMPRESSED_RGBA_ASTC_10x10_KHR
71
+ #define GL_COMPRESSED_RGBA_ASTC_10x10_KHR 0x93BB
72
+ #endif
73
+ #ifndef GL_COMPRESSED_RGBA_ASTC_12x10_KHR
74
+ #define GL_COMPRESSED_RGBA_ASTC_12x10_KHR 0x93BC
75
+ #endif
76
+ #ifndef GL_COMPRESSED_RGBA_ASTC_12x12_KHR
77
+ #define GL_COMPRESSED_RGBA_ASTC_12x12_KHR 0x93BD
78
+ #endif
79
+
80
+ // ASTC format info: maps VkFormat to GL format and block dimensions
81
+ struct ASTCFormatInfo {
82
+ uint32_t vk_format_unorm;
83
+ uint32_t vk_format_srgb;
84
+ GLenum gl_format;
85
+ int block_width;
86
+ int block_height;
87
+ };
88
+
89
+ static const ASTCFormatInfo g_astc_formats[] = {
90
+ { 157, 158, GL_COMPRESSED_RGBA_ASTC_4x4_KHR, 4, 4 },
91
+ { 159, 160, GL_COMPRESSED_RGBA_ASTC_5x4_KHR, 5, 4 },
92
+ { 161, 162, GL_COMPRESSED_RGBA_ASTC_5x5_KHR, 5, 5 },
93
+ { 163, 164, GL_COMPRESSED_RGBA_ASTC_6x5_KHR, 6, 5 },
94
+ { 165, 166, GL_COMPRESSED_RGBA_ASTC_6x6_KHR, 6, 6 },
95
+ { 167, 168, GL_COMPRESSED_RGBA_ASTC_8x5_KHR, 8, 5 },
96
+ { 169, 170, GL_COMPRESSED_RGBA_ASTC_8x6_KHR, 8, 6 },
97
+ { 171, 172, GL_COMPRESSED_RGBA_ASTC_8x8_KHR, 8, 8 },
98
+ { 173, 174, GL_COMPRESSED_RGBA_ASTC_10x5_KHR, 10, 5 },
99
+ { 175, 176, GL_COMPRESSED_RGBA_ASTC_10x6_KHR, 10, 6 },
100
+ { 177, 178, GL_COMPRESSED_RGBA_ASTC_10x8_KHR, 10, 8 },
101
+ { 179, 180, GL_COMPRESSED_RGBA_ASTC_10x10_KHR, 10, 10 },
102
+ { 181, 182, GL_COMPRESSED_RGBA_ASTC_12x10_KHR, 12, 10 },
103
+ { 183, 184, GL_COMPRESSED_RGBA_ASTC_12x12_KHR, 12, 12 },
104
+ };
105
+
106
+ // Look up ASTC format info from VkFormat. Returns nullptr if not found.
107
+ static const ASTCFormatInfo* get_astc_format_info(uint32_t vk_format) {
108
+ for (const auto& fmt : g_astc_formats) {
109
+ if (vk_format == fmt.vk_format_unorm || vk_format == fmt.vk_format_srgb) {
110
+ return &fmt;
111
+ }
112
+ }
113
+ return nullptr;
114
+ }
115
+
116
+ // Calculate bytes per layer for an ASTC texture (all blocks are 16 bytes)
117
+ static uint32_t astc_bytes_per_layer(int width, int height, int block_width, int block_height) {
118
+ int blocks_x = (width + block_width - 1) / block_width;
119
+ int blocks_y = (height + block_height - 1) / block_height;
120
+ return blocks_x * blocks_y * 16;
121
+ }
122
+
123
+ // PBO context for async texture uploads (per texture array)
124
+ struct PBOContext {
125
+ GLuint pbos[2]; // Double-buffered PBOs
126
+ GLsync fences[2]; // Fence sync objects to prevent overwriting in-flight PBOs
127
+ int current_pbo; // Index of PBO to use next (0 or 1)
128
+ GLsizei buffer_size; // Size of each PBO in bytes
129
+ bool initialized;
130
+
131
+ PBOContext() : current_pbo(0), buffer_size(0), initialized(false) {
132
+ pbos[0] = pbos[1] = 0;
133
+ fences[0] = fences[1] = nullptr;
134
+ }
135
+ };
136
+
137
+ // Each view has its own EGL context, and texture and buffer names are per context, so two views
138
+ // can hold the same texture ID. PBOs are keyed by the context that is current when they're made.
139
+ using PBOKey = std::pair<EGLContext, GLuint>;
140
+ static std::map<PBOKey, PBOContext> g_pbo_contexts;
141
+ static std::mutex g_pbo_mutex;
142
+
143
+ static PBOKey pboKey(GLuint texId) {
144
+ return {eglGetCurrentContext(), texId};
145
+ }
146
+
147
+ // Decode ASTC texture to RGBA using proper decoder (supports all block sizes)
148
+ static bool decode_astc_to_rgba(const uint8_t* astc_data, size_t astc_size,
149
+ int width, int height,
150
+ int block_width, int block_height,
151
+ std::vector<uint8_t>& rgba_output) {
152
+ int blocks_x = (width + block_width - 1) / block_width;
153
+ int blocks_y = (height + block_height - 1) / block_height;
154
+ size_t expected_size = blocks_x * blocks_y * 16; // 16 bytes per block
155
+
156
+ if (astc_size < expected_size) {
157
+ LOGE("ASTC data too small: %zu < %zu", astc_size, expected_size);
158
+ return false;
159
+ }
160
+
161
+ rgba_output.resize(width * height * 4);
162
+ memset(rgba_output.data(), 0, rgba_output.size());
163
+
164
+ // Temporary buffer for one decoded block (max 12x12)
165
+ uint8_t block_pixels[12 * 12 * 4];
166
+
167
+ for (int by = 0; by < blocks_y; by++) {
168
+ for (int bx = 0; bx < blocks_x; bx++) {
169
+ const uint8_t* block = astc_data + (by * blocks_x + bx) * 16;
170
+
171
+ // Use proper ASTC decoder (sRGB = false for linear decoding)
172
+ bool success = basisu::astc::decompress(block_pixels, block, false, block_width, block_height);
173
+ if (!success) {
174
+ LOGW("Failed to decode ASTC block at (%d, %d)", bx, by);
175
+ // Fill with magenta to indicate error
176
+ int pixels_in_block = block_width * block_height;
177
+ for (int i = 0; i < pixels_in_block; i++) {
178
+ block_pixels[i * 4 + 0] = 255;
179
+ block_pixels[i * 4 + 1] = 0;
180
+ block_pixels[i * 4 + 2] = 255;
181
+ block_pixels[i * 4 + 3] = 255;
182
+ }
183
+ }
184
+
185
+ // Copy block to output, handling edge blocks
186
+ for (int y = 0; y < block_height && (by * block_height + y) < height; y++) {
187
+ for (int x = 0; x < block_width && (bx * block_width + x) < width; x++) {
188
+ int dst_x = bx * block_width + x;
189
+ int dst_y = by * block_height + y;
190
+ uint8_t* dst = rgba_output.data() + (dst_y * width + dst_x) * 4;
191
+ uint8_t* src = block_pixels + (y * block_width + x) * 4;
192
+ dst[0] = src[0];
193
+ dst[1] = src[1];
194
+ dst[2] = src[2];
195
+ dst[3] = src[3];
196
+ }
197
+ }
198
+ }
199
+ }
200
+
201
+ LOGI("Decoded ASTC %dx%d (%d x %d blocks, block=%dx%d) to RGBA", width, height, blocks_x, blocks_y, block_width, block_height);
202
+ return true;
203
+ }
204
+
205
+ extern "C" {
206
+
207
+ JNIEXPORT void JNICALL
208
+ Java_dev_mediacache_alphavideo_gpu_AlphaVideoGpuNative_nativeDeleteTexture(JNIEnv *env, jclass clazz,
209
+ jint texture_id) {
210
+ GLuint id = (GLuint)texture_id;
211
+ glDeleteTextures(1, &id);
212
+ }
213
+
214
+ // Zstd decompression from one direct ByteBuffer (the file, mapped or read) into another
215
+ JNIEXPORT jboolean JNICALL
216
+ Java_dev_mediacache_alphavideo_gpu_AlphaVideoGpuNative_nativeDecompressZstdToBuffer(JNIEnv *env, jclass clazz,
217
+ jobject data, jlong offset,
218
+ jlong compressed_length,
219
+ jobject output_buffer,
220
+ jint uncompressed_length) {
221
+ const auto* input = static_cast<const uint8_t*>(env->GetDirectBufferAddress(data));
222
+ const jlong input_size = env->GetDirectBufferCapacity(data);
223
+ if (!input || offset < 0 || compressed_length < 0 || offset + compressed_length > input_size) {
224
+ LOGE("The Zstd input isn't a direct buffer holding the level");
225
+ return JNI_FALSE;
226
+ }
227
+
228
+ void* output_ptr = env->GetDirectBufferAddress(output_buffer);
229
+ if (!output_ptr) {
230
+ LOGE("Failed to get direct buffer address for Zstd output");
231
+ return JNI_FALSE;
232
+ }
233
+
234
+ size_t decompressed_size = ZSTD_decompress(
235
+ output_ptr, uncompressed_length,
236
+ input + offset, (size_t)compressed_length
237
+ );
238
+
239
+ if (ZSTD_isError(decompressed_size)) {
240
+ LOGE("Zstd decompression to buffer failed: %s", ZSTD_getErrorName(decompressed_size));
241
+ return JNI_FALSE;
242
+ }
243
+
244
+ if (decompressed_size != (size_t)uncompressed_length) {
245
+ LOGE("Zstd decompression size mismatch: expected %d, got %zu", uncompressed_length, decompressed_size);
246
+ return JNI_FALSE;
247
+ }
248
+
249
+ LOGI("Zstd decompressed %lld -> %zu bytes (zero-copy to direct buffer)", (long long)compressed_length, decompressed_size);
250
+
251
+ return JNI_TRUE;
252
+ }
253
+
254
+ /**
255
+ * Load a KTX2 texture array (one frame per layer).
256
+ * Uses OpenGL ES 3.0 GL_TEXTURE_2D_ARRAY for efficient frame switching.
257
+ * Returns [textureId, width, height, layerCount]
258
+ */
259
+ JNIEXPORT jintArray JNICALL
260
+ Java_dev_mediacache_alphavideo_gpu_AlphaVideoGpuNative_nativeLoadTextureArray(JNIEnv *env, jclass clazz,
261
+ jobject ktx2_data,
262
+ jboolean use_astc) {
263
+ const auto* bytes = static_cast<const uint8_t*>(env->GetDirectBufferAddress(ktx2_data));
264
+ const jlong data_length = env->GetDirectBufferCapacity(ktx2_data);
265
+
266
+ if (!bytes || data_length < 104) {
267
+ LOGE("Invalid KTX2 data for texture array");
268
+ return nullptr;
269
+ }
270
+
271
+ // Parse KTX2 header
272
+ uint32_t vkFormat = *(uint32_t*)(bytes + 12);
273
+ uint32_t width = *(uint32_t*)(bytes + 20);
274
+ uint32_t height = *(uint32_t*)(bytes + 24);
275
+ uint32_t layerCount = *(uint32_t*)(bytes + 32);
276
+ uint32_t supercompressionScheme = *(uint32_t*)(bytes + 44);
277
+
278
+ // Level 0 index (at byte 80)
279
+ uint64_t level0Offset = *(uint64_t*)(bytes + 80);
280
+ uint64_t level0Length = *(uint64_t*)(bytes + 88);
281
+ uint64_t level0UncompressedLength = *(uint64_t*)(bytes + 96);
282
+ if (level0Offset > (uint64_t)data_length || level0Length > (uint64_t)data_length - level0Offset) {
283
+ LOGE("Level 0 (%llu bytes at %llu) runs past the file's %lld bytes", (unsigned long long)level0Length,
284
+ (unsigned long long)level0Offset, (long long)data_length);
285
+ return nullptr;
286
+ }
287
+
288
+ // Look up ASTC format info from vkFormat
289
+ const ASTCFormatInfo* astcInfo = get_astc_format_info(vkFormat);
290
+ int bw = astcInfo ? astcInfo->block_width : 4;
291
+ int bh = astcInfo ? astcInfo->block_height : 4;
292
+ GLenum astcGlFormat = astcInfo ? astcInfo->gl_format : GL_COMPRESSED_RGBA_ASTC_4x4_KHR;
293
+
294
+ LOGI("Texture array: %ux%u, %u layers, supercompression=%u, vkFormat=%u (ASTC %dx%d)",
295
+ width, height, layerCount, supercompressionScheme, vkFormat, bw, bh);
296
+
297
+ // Extract ASTC data
298
+ std::vector<uint8_t> astcData;
299
+
300
+ if (supercompressionScheme == 2) {
301
+ // Zstd compressed - decompress
302
+ const void* compressedData = bytes + level0Offset;
303
+ size_t compressedSize = (size_t)level0Length;
304
+ size_t decompressedSize = (size_t)level0UncompressedLength;
305
+
306
+ astcData.resize(decompressedSize);
307
+ size_t result = ZSTD_decompress(astcData.data(), decompressedSize,
308
+ compressedData, compressedSize);
309
+
310
+ if (ZSTD_isError(result)) {
311
+ LOGE("Zstd decompression failed: %s", ZSTD_getErrorName(result));
312
+ return nullptr;
313
+ }
314
+
315
+ LOGI("Decompressed %llu -> %zu bytes", (unsigned long long)level0Length, result);
316
+ } else {
317
+ // Uncompressed
318
+ astcData.assign(bytes + level0Offset, bytes + level0Offset + level0Length);
319
+ }
320
+
321
+ // Calculate bytes per layer using dynamic block dimensions
322
+ uint32_t bytesPerLayer = astc_bytes_per_layer(width, height, bw, bh);
323
+
324
+ LOGI("ASTC data: %zu bytes, %u bytes/layer (block=%dx%d), %u layers",
325
+ astcData.size(), bytesPerLayer, bw, bh, layerCount);
326
+
327
+ // Clear any pending GL errors
328
+ while (glGetError() != GL_NO_ERROR) {}
329
+
330
+ // Create texture array
331
+ GLuint textureId;
332
+ glGenTextures(1, &textureId);
333
+ glBindTexture(GL_TEXTURE_2D_ARRAY, textureId);
334
+
335
+ GLenum bindError = glGetError();
336
+ if (bindError != GL_NO_ERROR) {
337
+ LOGE("glBindTexture GL_TEXTURE_2D_ARRAY error: 0x%x", bindError);
338
+ glDeleteTextures(1, &textureId);
339
+ return nullptr;
340
+ }
341
+
342
+ if (use_astc) {
343
+ // Upload compressed ASTC data directly using correct GL format
344
+ glCompressedTexImage3D(GL_TEXTURE_2D_ARRAY, 0, astcGlFormat,
345
+ width, height, layerCount, 0,
346
+ astcData.size(), astcData.data());
347
+ } else {
348
+ // Decode ASTC to RGBA for each layer and upload
349
+ glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_RGBA8,
350
+ width, height, layerCount, 0,
351
+ GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
352
+
353
+ for (uint32_t layer = 0; layer < layerCount; layer++) {
354
+ const uint8_t* layerData = astcData.data() + (layer * bytesPerLayer);
355
+ std::vector<uint8_t> rgbaData;
356
+
357
+ if (decode_astc_to_rgba(layerData, bytesPerLayer, width, height, bw, bh, rgbaData)) {
358
+ glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0,
359
+ 0, 0, layer,
360
+ width, height, 1,
361
+ GL_RGBA, GL_UNSIGNED_BYTE, rgbaData.data());
362
+ }
363
+ }
364
+ }
365
+
366
+ GLenum texError = glGetError();
367
+ if (texError != GL_NO_ERROR) {
368
+ LOGE("Texture array upload error: 0x%x", texError);
369
+ glDeleteTextures(1, &textureId);
370
+ return nullptr;
371
+ }
372
+
373
+ // Set texture parameters
374
+ glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
375
+ glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
376
+ glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
377
+ glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
378
+
379
+ LOGI("Created texture array: ID=%d (%ux%u, %u layers)", textureId, width, height, layerCount);
380
+
381
+ // Return [textureId, width, height, layerCount]
382
+ jintArray result = env->NewIntArray(4);
383
+ jint values[4] = { (jint)textureId, (jint)width, (jint)height, (jint)layerCount };
384
+ env->SetIntArrayRegion(result, 0, 4, values);
385
+
386
+ return result;
387
+ }
388
+
389
+ /**
390
+ * Streaming decompression support using ZSTD_DStream.
391
+ * Decompresses the level0 data of zstd-supercompressed KTX2 files as they play.
392
+ * Per-instance state management for multiple concurrent streams.
393
+ *
394
+ * Memory optimization: Uses memory-mapped files to avoid copying
395
+ * compressed data into Java heap and C++ heap.
396
+ */
397
+
398
+ #include <sys/mman.h>
399
+ #include <sys/stat.h>
400
+ #include <fcntl.h>
401
+ #include <unistd.h>
402
+
403
+ // Per-instance streaming decompression state
404
+ struct StreamingDecompressState {
405
+ ZSTD_DStream* dstream = nullptr;
406
+
407
+ // A local file: memory-mapped, zero copy
408
+ int fd = -1;
409
+ void* mmap_ptr = nullptr;
410
+ size_t mmap_size = 0;
411
+
412
+ // A URL or data source: read from a range reader into `chunk`, which holds the level's bytes
413
+ // [chunk_start, chunk_start + chunk_length)
414
+ std::shared_ptr<avg_rs::Reader> reader;
415
+ int64_t level_offset = 0;
416
+ std::vector<uint8_t> chunk;
417
+ size_t chunk_start = 0;
418
+ size_t chunk_length = 0;
419
+
420
+ // The mapping's bytes (null when reading from a reader), the level's size, and where the
421
+ // decoder is in it
422
+ const uint8_t* input_data = nullptr;
423
+ size_t input_size = 0;
424
+ size_t input_pos = 0;
425
+ // The last call filled the output mid-frame: zstd may hold more even with no input left
426
+ bool output_pending = false;
427
+ // Why the last call failed
428
+ std::string error;
429
+
430
+ ~StreamingDecompressState() {
431
+ if (dstream) {
432
+ ZSTD_freeDStream(dstream);
433
+ dstream = nullptr;
434
+ }
435
+ if (mmap_ptr && mmap_ptr != MAP_FAILED) {
436
+ munmap(mmap_ptr, mmap_size);
437
+ mmap_ptr = nullptr;
438
+ }
439
+ if (fd >= 0) {
440
+ close(fd);
441
+ fd = -1;
442
+ }
443
+ }
444
+ };
445
+
446
+ // Map of instance ID -> streaming state
447
+ static std::map<jlong, std::unique_ptr<StreamingDecompressState>> g_streaming_states;
448
+ static std::mutex g_streaming_mutex;
449
+ static std::atomic<jlong> g_next_stream_id{1};
450
+
451
+ /**
452
+ * Maps [offset, offset + length) of fd into the state's input. The descriptor is duplicated, so
453
+ * the caller may close its own copy. A negative length means "to the end of the file".
454
+ */
455
+ static bool mapFdRange(StreamingDecompressState* state, int fd, int64_t offset, int64_t length) {
456
+ state->fd = dup(fd);
457
+ if (state->fd < 0) {
458
+ LOGE("Failed to dup descriptor (errno=%d)", errno);
459
+ return false;
460
+ }
461
+
462
+ struct stat st;
463
+ if (fstat(state->fd, &st) < 0) {
464
+ LOGE("Failed to stat descriptor (errno=%d)", errno);
465
+ return false;
466
+ }
467
+ if (length < 0) length = st.st_size - offset;
468
+ if (offset < 0 || length <= 0 || offset + length > st.st_size) {
469
+ LOGE("Range [%lld, +%lld) is outside the file (%lld bytes)",
470
+ (long long)offset, (long long)length, (long long)st.st_size);
471
+ return false;
472
+ }
473
+
474
+ long page_size = sysconf(_SC_PAGE_SIZE);
475
+ off_t aligned_offset = (offset / page_size) * page_size;
476
+ size_t offset_adjustment = offset - aligned_offset;
477
+ size_t mmap_length = length + offset_adjustment;
478
+
479
+ void* ptr = mmap(nullptr, mmap_length, PROT_READ, MAP_PRIVATE, state->fd, aligned_offset);
480
+ if (ptr == MAP_FAILED) {
481
+ LOGE("Failed to mmap %zu bytes (errno=%d)", mmap_length, errno);
482
+ return false;
483
+ }
484
+ state->mmap_ptr = ptr;
485
+ state->mmap_size = mmap_length;
486
+ madvise(ptr, mmap_length, MADV_SEQUENTIAL);
487
+
488
+ state->input_data = static_cast<const uint8_t*>(ptr) + offset_adjustment;
489
+ state->input_size = length;
490
+ state->input_pos = 0;
491
+ return true;
492
+ }
493
+
494
+ /** Creates the zstd stream for a state whose input is set, and registers it. Returns 0 on error. */
495
+ static jlong registerStream(std::unique_ptr<StreamingDecompressState> state) {
496
+ state->dstream = ZSTD_createDStream();
497
+ if (!state->dstream) {
498
+ LOGE("Failed to create ZSTD_DStream");
499
+ return 0;
500
+ }
501
+ size_t initResult = ZSTD_initDStream(state->dstream);
502
+ if (ZSTD_isError(initResult)) {
503
+ LOGE("Failed to init ZSTD_DStream: %s", ZSTD_getErrorName(initResult));
504
+ return 0;
505
+ }
506
+
507
+ jlong streamId = g_next_stream_id.fetch_add(1);
508
+ std::lock_guard<std::mutex> lock(g_streaming_mutex);
509
+ g_streaming_states[streamId] = std::move(state);
510
+ return streamId;
511
+ }
512
+
513
+ // Bytes of the level read from a reader at a time
514
+ static constexpr size_t kReaderChunkBytes = 256 * 1024;
515
+
516
+ // Status values in the high 32 bits of nativeStreamingDecompressContinueToBuffer's result
517
+ static constexpr jlong kStreamMore = 0, kStreamDone = 1, kStreamPending = 2;
518
+
519
+ /**
520
+ * For a stream over a reader: makes `chunk` hold the bytes at input_pos, reading them if they're
521
+ * there. Returns kStreamMore when it does, kStreamPending while they're on their way, or -1 with
522
+ * `error` set if they can't be fetched.
523
+ */
524
+ static jlong fillChunk(StreamingDecompressState* state) {
525
+ if (state->input_pos >= state->chunk_start && state->input_pos < state->chunk_start + state->chunk_length) {
526
+ return kStreamMore;
527
+ }
528
+ size_t want = std::min(kReaderChunkBytes, state->input_size - state->input_pos);
529
+ int64_t n = state->reader->tryRead(state->level_offset + (int64_t)state->input_pos, state->chunk.data(), (int64_t)want);
530
+ if (n == avg_rs::Reader::kPending) return kStreamPending;
531
+ if (n <= 0) {
532
+ std::string why = state->reader->lastError();
533
+ state->error = n == 0 ? "the file ends inside the level" : (why.empty() ? "reading failed" : why);
534
+ return -1;
535
+ }
536
+ state->chunk_start = state->input_pos;
537
+ state->chunk_length = (size_t)n;
538
+ return kStreamMore;
539
+ }
540
+
541
+ /**
542
+ * Continue streaming decompression into a direct ByteBuffer (zero-copy).
543
+ * This avoids allocating new byte arrays on each call.
544
+ *
545
+ * @param stream_id The stream ID from init
546
+ * @param output_buffer A direct ByteBuffer to write decompressed data into
547
+ * @param max_output_bytes Maximum bytes to decompress
548
+ * @return -1 on an error (nativeStreamingError says why), else the status in the high 32 bits
549
+ * (0 = more to come, 1 = done, 2 = pending: a reader's bytes are on their way, call again
550
+ * later) and the bytes written in the low 32 bits
551
+ */
552
+ JNIEXPORT jlong JNICALL
553
+ Java_dev_mediacache_alphavideo_gpu_AlphaVideoGpuNative_nativeStreamingDecompressContinueToBuffer(JNIEnv *env, jclass clazz,
554
+ jlong stream_id,
555
+ jobject output_buffer,
556
+ jint max_output_bytes) {
557
+ StreamingDecompressState* state = nullptr;
558
+ {
559
+ std::lock_guard<std::mutex> lock(g_streaming_mutex);
560
+ auto it = g_streaming_states.find(stream_id);
561
+ if (it == g_streaming_states.end()) {
562
+ LOGE("Streaming decompression not found for stream %lld", (long long)stream_id);
563
+ return -1; // Error
564
+ }
565
+ state = it->second.get();
566
+ }
567
+
568
+ if (!state || !state->dstream) {
569
+ LOGE("Streaming decompression not initialized for stream %lld", (long long)stream_id);
570
+ return -1; // Error
571
+ }
572
+
573
+ // Check if we've consumed all input and zstd has nothing left to flush
574
+ if (state->input_pos >= state->input_size && !state->output_pending) {
575
+ return (kStreamDone << 32); // Done, 0 bytes written
576
+ }
577
+
578
+ // Get direct buffer address
579
+ void* buffer_ptr = env->GetDirectBufferAddress(output_buffer);
580
+ if (!buffer_ptr) {
581
+ LOGE("Failed to get direct buffer address");
582
+ return -1; // Error
583
+ }
584
+
585
+ // Setup input/output buffers: the mapping from the position on, or the chunk read from the
586
+ // reader. zstd may have output left to flush without new input.
587
+ ZSTD_inBuffer input = {nullptr, 0, 0};
588
+ size_t input_base = state->input_pos;
589
+ if (state->reader) {
590
+ if (state->input_pos < state->input_size) {
591
+ jlong filled = fillChunk(state);
592
+ if (filled == -1) {
593
+ LOGE("Streaming read failed for stream %lld: %s", (long long)stream_id, state->error.c_str());
594
+ return -1;
595
+ }
596
+ if (filled == kStreamPending && !state->output_pending) return (kStreamPending << 32);
597
+ if (filled == kStreamMore) {
598
+ input_base = state->chunk_start;
599
+ input = {state->chunk.data(), state->chunk_length, state->input_pos - state->chunk_start};
600
+ }
601
+ }
602
+ } else {
603
+ input = {state->input_data + state->input_pos, state->input_size - state->input_pos, 0};
604
+ }
605
+
606
+ ZSTD_outBuffer output = {
607
+ buffer_ptr,
608
+ (size_t)max_output_bytes,
609
+ 0
610
+ };
611
+
612
+ // Decompress
613
+ size_t result = ZSTD_decompressStream(state->dstream, &output, &input);
614
+
615
+ if (ZSTD_isError(result)) {
616
+ state->error = ZSTD_getErrorName(result);
617
+ LOGE("Streaming decompression error for stream %lld: %s", (long long)stream_id, ZSTD_getErrorName(result));
618
+ return -1; // Error
619
+ }
620
+
621
+ // Update input position
622
+ state->input_pos = input_base + input.pos;
623
+ // 0 means the frame is done and flushed; a level can hold several frames back to back
624
+ state->output_pending = output.pos == output.size && result != 0;
625
+
626
+ // Check if done
627
+ bool isDone = state->input_pos >= state->input_size && !state->output_pending;
628
+
629
+ // Return: high 32 bits = status (0=more, 1=done), low 32 bits = bytes written
630
+ jlong status = isDone ? 1 : 0;
631
+ return (status << 32) | (jlong)output.pos;
632
+ }
633
+
634
+ /**
635
+ * Starts the stream again at a zstd frame boundary of its input, such as a seek point from the
636
+ * level's seek table (0 is the level's start). The mapping is kept.
637
+ */
638
+ JNIEXPORT jboolean JNICALL
639
+ Java_dev_mediacache_alphavideo_gpu_AlphaVideoGpuNative_nativeStreamingRestartAt(JNIEnv *env, jclass clazz,
640
+ jlong stream_id,
641
+ jlong compressed_offset) {
642
+ std::lock_guard<std::mutex> lock(g_streaming_mutex);
643
+ auto it = g_streaming_states.find(stream_id);
644
+ if (it == g_streaming_states.end() || !it->second->dstream) return JNI_FALSE;
645
+ StreamingDecompressState* state = it->second.get();
646
+ if (compressed_offset < 0 || (size_t)compressed_offset > state->input_size) {
647
+ LOGE("Restart offset %lld is outside the stream (%zu bytes)", (long long)compressed_offset, state->input_size);
648
+ return JNI_FALSE;
649
+ }
650
+ size_t reset = ZSTD_DCtx_reset(state->dstream, ZSTD_reset_session_only);
651
+ if (ZSTD_isError(reset)) {
652
+ LOGE("Failed to reset ZSTD_DStream: %s", ZSTD_getErrorName(reset));
653
+ return JNI_FALSE;
654
+ }
655
+ state->input_pos = (size_t)compressed_offset;
656
+ state->output_pending = false;
657
+ return JNI_TRUE;
658
+ }
659
+
660
+ /**
661
+ * Clean up streaming decompression resources for a specific stream.
662
+ */
663
+ JNIEXPORT void JNICALL
664
+ Java_dev_mediacache_alphavideo_gpu_AlphaVideoGpuNative_nativeStreamingDecompressEnd(JNIEnv *env, jclass clazz,
665
+ jlong stream_id) {
666
+ std::lock_guard<std::mutex> lock(g_streaming_mutex);
667
+ auto it = g_streaming_states.find(stream_id);
668
+ if (it != g_streaming_states.end()) {
669
+ g_streaming_states.erase(it);
670
+ LOGI("Streaming decompression ended: stream %lld", (long long)stream_id);
671
+ }
672
+ }
673
+
674
+ /**
675
+ * Upload a single layer to an existing texture array.
676
+ * Used for streaming mode where layers are uploaded progressively.
677
+ */
678
+ JNIEXPORT jboolean JNICALL
679
+ Java_dev_mediacache_alphavideo_gpu_AlphaVideoGpuNative_nativeUploadTextureArrayLayer(JNIEnv *env, jclass clazz,
680
+ jint texture_id,
681
+ jint layer,
682
+ jint width, jint height,
683
+ jbyteArray astc_data,
684
+ jboolean use_astc,
685
+ jint gl_format,
686
+ jint block_w, jint block_h) {
687
+ int bw = (block_w > 0) ? block_w : 4;
688
+ int bh = (block_h > 0) ? block_h : 4;
689
+ GLenum astcGlFormat = (gl_format > 0) ? (GLenum)gl_format : GL_COMPRESSED_RGBA_ASTC_4x4_KHR;
690
+
691
+ jsize data_length = env->GetArrayLength(astc_data);
692
+ jbyte* data = env->GetByteArrayElements(astc_data, nullptr);
693
+ if (!data) {
694
+ LOGE("Failed to get layer data");
695
+ return JNI_FALSE;
696
+ }
697
+
698
+ glBindTexture(GL_TEXTURE_2D_ARRAY, (GLuint)texture_id);
699
+
700
+ GLenum bindError = glGetError();
701
+ if (bindError != GL_NO_ERROR) {
702
+ LOGE("glBindTexture error: 0x%x", bindError);
703
+ env->ReleaseByteArrayElements(astc_data, data, JNI_ABORT);
704
+ return JNI_FALSE;
705
+ }
706
+
707
+ if (use_astc) {
708
+ // Upload compressed ASTC data using correct GL format
709
+ glCompressedTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0,
710
+ 0, 0, layer,
711
+ width, height, 1,
712
+ astcGlFormat,
713
+ data_length, data);
714
+ } else {
715
+ // Decode ASTC to RGBA and upload
716
+ std::vector<uint8_t> rgbaData;
717
+ if (!decode_astc_to_rgba((const uint8_t*)data, data_length, width, height, bw, bh, rgbaData)) {
718
+ LOGE("Failed to decode ASTC for layer %d", layer);
719
+ env->ReleaseByteArrayElements(astc_data, data, JNI_ABORT);
720
+ return JNI_FALSE;
721
+ }
722
+
723
+ glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0,
724
+ 0, 0, layer,
725
+ width, height, 1,
726
+ GL_RGBA, GL_UNSIGNED_BYTE, rgbaData.data());
727
+ }
728
+
729
+ env->ReleaseByteArrayElements(astc_data, data, JNI_ABORT);
730
+
731
+ GLenum texError = glGetError();
732
+ if (texError != GL_NO_ERROR) {
733
+ LOGE("Texture layer upload error: 0x%x", texError);
734
+ return JNI_FALSE;
735
+ }
736
+
737
+ return JNI_TRUE;
738
+ }
739
+
740
+ /**
741
+ * Create an empty texture array for streaming mode.
742
+ * Returns texture ID, or 0 on error.
743
+ */
744
+ JNIEXPORT jint JNICALL
745
+ Java_dev_mediacache_alphavideo_gpu_AlphaVideoGpuNative_nativeCreateTextureArray(JNIEnv *env, jclass clazz,
746
+ jint width, jint height,
747
+ jint layer_count,
748
+ jboolean use_astc,
749
+ jint gl_format) {
750
+ GLenum astcGlFormat = (gl_format > 0) ? (GLenum)gl_format : GL_COMPRESSED_RGBA_ASTC_4x4_KHR;
751
+
752
+ // Clear any pending GL errors
753
+ while (glGetError() != GL_NO_ERROR) {}
754
+
755
+ GLuint textureId;
756
+ glGenTextures(1, &textureId);
757
+ glBindTexture(GL_TEXTURE_2D_ARRAY, textureId);
758
+
759
+ GLenum bindError = glGetError();
760
+ if (bindError != GL_NO_ERROR) {
761
+ LOGE("glBindTexture GL_TEXTURE_2D_ARRAY error: 0x%x", bindError);
762
+ glDeleteTextures(1, &textureId);
763
+ return 0;
764
+ }
765
+
766
+ if (use_astc) {
767
+ // Use glTexStorage3D for immutable storage with correct ASTC format
768
+ glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, astcGlFormat,
769
+ width, height, layer_count);
770
+ } else {
771
+ // Allocate RGBA texture storage
772
+ glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_RGBA8,
773
+ width, height, layer_count);
774
+ }
775
+
776
+ GLenum texError = glGetError();
777
+ if (texError != GL_NO_ERROR) {
778
+ LOGE("Texture array allocation error: 0x%x", texError);
779
+ glDeleteTextures(1, &textureId);
780
+ return 0;
781
+ }
782
+
783
+ // Set texture parameters
784
+ glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
785
+ glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
786
+ glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
787
+ glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
788
+
789
+ LOGI("Created empty texture array: ID=%d (%dx%d, %d layers, astc=%d)",
790
+ textureId, width, height, layer_count, use_astc);
791
+
792
+ return (jint)textureId;
793
+ }
794
+
795
+ /**
796
+ * Upload a single layer to an existing texture array from a direct ByteBuffer.
797
+ * Zero-copy version for VRAM streaming mode - avoids Java byte[] allocation.
798
+ */
799
+ JNIEXPORT jboolean JNICALL
800
+ Java_dev_mediacache_alphavideo_gpu_AlphaVideoGpuNative_nativeUploadTextureArrayLayerFromBuffer(JNIEnv *env, jclass clazz,
801
+ jint texture_id,
802
+ jint layer,
803
+ jint width, jint height,
804
+ jobject buffer,
805
+ jint data_length,
806
+ jboolean use_astc,
807
+ jint gl_format,
808
+ jint block_w, jint block_h) {
809
+ int bw = (block_w > 0) ? block_w : 4;
810
+ int bh = (block_h > 0) ? block_h : 4;
811
+ GLenum astcGlFormat = (gl_format > 0) ? (GLenum)gl_format : GL_COMPRESSED_RGBA_ASTC_4x4_KHR;
812
+
813
+ void* data = env->GetDirectBufferAddress(buffer);
814
+ if (!data) {
815
+ LOGE("Failed to get direct buffer address for layer upload");
816
+ return JNI_FALSE;
817
+ }
818
+
819
+ glBindTexture(GL_TEXTURE_2D_ARRAY, (GLuint)texture_id);
820
+
821
+ GLenum bindError = glGetError();
822
+ if (bindError != GL_NO_ERROR) {
823
+ LOGE("glBindTexture error: 0x%x", bindError);
824
+ return JNI_FALSE;
825
+ }
826
+
827
+ if (use_astc) {
828
+ // Upload compressed ASTC data directly from buffer
829
+ glCompressedTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0,
830
+ 0, 0, layer,
831
+ width, height, 1,
832
+ astcGlFormat,
833
+ data_length, data);
834
+ } else {
835
+ // Decode ASTC to RGBA and upload
836
+ std::vector<uint8_t> rgbaData;
837
+ if (!decode_astc_to_rgba((const uint8_t*)data, data_length, width, height, bw, bh, rgbaData)) {
838
+ LOGE("Failed to decode ASTC for layer %d", layer);
839
+ return JNI_FALSE;
840
+ }
841
+
842
+ glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0,
843
+ 0, 0, layer,
844
+ width, height, 1,
845
+ GL_RGBA, GL_UNSIGNED_BYTE, rgbaData.data());
846
+ }
847
+
848
+ GLenum texError = glGetError();
849
+ if (texError != GL_NO_ERROR) {
850
+ LOGE("Texture layer upload from buffer error: 0x%x", texError);
851
+ return JNI_FALSE;
852
+ }
853
+
854
+ return JNI_TRUE;
855
+ }
856
+
857
+ /**
858
+ * Upload a single layer to an existing texture array from a direct ByteBuffer with offset.
859
+ * Zero-copy version for non-stream RAM mode - reads from contiguous buffer at specified offset.
860
+ */
861
+ JNIEXPORT jboolean JNICALL
862
+ Java_dev_mediacache_alphavideo_gpu_AlphaVideoGpuNative_nativeUploadTextureArrayLayerFromBufferWithOffset(JNIEnv *env, jclass clazz,
863
+ jint texture_id,
864
+ jint layer,
865
+ jint width, jint height,
866
+ jobject buffer,
867
+ jint offset,
868
+ jint data_length,
869
+ jboolean use_astc,
870
+ jint gl_format,
871
+ jint block_w, jint block_h) {
872
+ int bw = (block_w > 0) ? block_w : 4;
873
+ int bh = (block_h > 0) ? block_h : 4;
874
+ GLenum astcGlFormat = (gl_format > 0) ? (GLenum)gl_format : GL_COMPRESSED_RGBA_ASTC_4x4_KHR;
875
+
876
+ void* buffer_base = env->GetDirectBufferAddress(buffer);
877
+ if (!buffer_base) {
878
+ LOGE("Failed to get direct buffer address for layer upload with offset");
879
+ return JNI_FALSE;
880
+ }
881
+
882
+ // Calculate data pointer with offset
883
+ const uint8_t* data = reinterpret_cast<const uint8_t*>(buffer_base) + offset;
884
+
885
+ glBindTexture(GL_TEXTURE_2D_ARRAY, (GLuint)texture_id);
886
+
887
+ GLenum bindError = glGetError();
888
+ if (bindError != GL_NO_ERROR) {
889
+ LOGE("glBindTexture error: 0x%x", bindError);
890
+ return JNI_FALSE;
891
+ }
892
+
893
+ if (use_astc) {
894
+ // Upload compressed ASTC data directly from buffer at offset
895
+ glCompressedTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0,
896
+ 0, 0, layer,
897
+ width, height, 1,
898
+ astcGlFormat,
899
+ data_length, data);
900
+ } else {
901
+ // Decode ASTC to RGBA and upload
902
+ std::vector<uint8_t> rgbaData;
903
+ if (!decode_astc_to_rgba(data, data_length, width, height, bw, bh, rgbaData)) {
904
+ LOGE("Failed to decode ASTC for layer %d (offset=%d)", layer, offset);
905
+ return JNI_FALSE;
906
+ }
907
+
908
+ glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0,
909
+ 0, 0, layer,
910
+ width, height, 1,
911
+ GL_RGBA, GL_UNSIGNED_BYTE, rgbaData.data());
912
+ }
913
+
914
+ GLenum texError = glGetError();
915
+ if (texError != GL_NO_ERROR) {
916
+ LOGE("Texture layer upload from buffer with offset error: 0x%x", texError);
917
+ return JNI_FALSE;
918
+ }
919
+
920
+ return JNI_TRUE;
921
+ }
922
+
923
+ /**
924
+ * Initialize PBOs for a texture array (call once per texture array).
925
+ * Creates double-buffered PBOs for async uploads.
926
+ */
927
+ JNIEXPORT jboolean JNICALL
928
+ Java_dev_mediacache_alphavideo_gpu_AlphaVideoGpuNative_nativeInitPBOs(JNIEnv *env, jclass clazz,
929
+ jint texture_id,
930
+ jint buffer_size) {
931
+ std::lock_guard<std::mutex> lock(g_pbo_mutex);
932
+
933
+ GLuint texId = (GLuint)texture_id;
934
+ PBOKey key = pboKey(texId);
935
+
936
+ // Check if already initialized
937
+ auto existing = g_pbo_contexts.find(key);
938
+ if (existing != g_pbo_contexts.end() && existing->second.initialized) {
939
+ LOGI("PBOs already initialized for texture %u", texId);
940
+ return JNI_TRUE;
941
+ }
942
+
943
+ PBOContext ctx;
944
+ ctx.buffer_size = buffer_size;
945
+
946
+ // Generate two PBOs for double-buffering
947
+ glGenBuffers(2, ctx.pbos);
948
+ GLenum err = glGetError();
949
+ if (err != GL_NO_ERROR) {
950
+ LOGE("glGenBuffers failed: 0x%x", err);
951
+ return JNI_FALSE;
952
+ }
953
+
954
+ // Allocate storage for both PBOs
955
+ for (int i = 0; i < 2; i++) {
956
+ glBindBuffer(GL_PIXEL_UNPACK_BUFFER, ctx.pbos[i]);
957
+ glBufferData(GL_PIXEL_UNPACK_BUFFER, buffer_size, nullptr, GL_STREAM_DRAW);
958
+ err = glGetError();
959
+ if (err != GL_NO_ERROR) {
960
+ LOGE("glBufferData for PBO %d failed: 0x%x", i, err);
961
+ glDeleteBuffers(2, ctx.pbos);
962
+ return JNI_FALSE;
963
+ }
964
+ ctx.fences[i] = nullptr;
965
+ }
966
+
967
+ glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
968
+
969
+ ctx.current_pbo = 0;
970
+ ctx.initialized = true;
971
+ g_pbo_contexts[key] = ctx;
972
+
973
+ LOGI("Initialized PBOs for texture %u: size=%d bytes each", texId, buffer_size);
974
+ return JNI_TRUE;
975
+ }
976
+
977
+ /**
978
+ * Cleanup PBOs for a texture array.
979
+ */
980
+ JNIEXPORT void JNICALL
981
+ Java_dev_mediacache_alphavideo_gpu_AlphaVideoGpuNative_nativeCleanupPBOs(JNIEnv *env, jclass clazz,
982
+ jint texture_id) {
983
+ std::lock_guard<std::mutex> lock(g_pbo_mutex);
984
+
985
+ GLuint texId = (GLuint)texture_id;
986
+ auto it = g_pbo_contexts.find(pboKey(texId));
987
+ if (it == g_pbo_contexts.end()) {
988
+ return;
989
+ }
990
+
991
+ PBOContext& ctx = it->second;
992
+
993
+ // Wait for any pending fences
994
+ for (int i = 0; i < 2; i++) {
995
+ if (ctx.fences[i]) {
996
+ glClientWaitSync(ctx.fences[i], GL_SYNC_FLUSH_COMMANDS_BIT, 1000000000); // 1 second timeout
997
+ glDeleteSync(ctx.fences[i]);
998
+ }
999
+ }
1000
+
1001
+ glDeleteBuffers(2, ctx.pbos);
1002
+ g_pbo_contexts.erase(it);
1003
+
1004
+ LOGI("Cleaned up PBOs for texture %u", texId);
1005
+ }
1006
+
1007
+ /**
1008
+ * Upload texture layer using PBO for async DMA.
1009
+ * The glCompressedTexSubImage3D call returns immediately while GPU DMAs in background.
1010
+ */
1011
+ JNIEXPORT jboolean JNICALL
1012
+ Java_dev_mediacache_alphavideo_gpu_AlphaVideoGpuNative_nativeUploadTextureArrayLayerPBO(JNIEnv *env, jclass clazz,
1013
+ jint texture_id,
1014
+ jint layer,
1015
+ jint width, jint height,
1016
+ jobject buffer,
1017
+ jint data_length,
1018
+ jboolean use_astc,
1019
+ jint gl_format,
1020
+ jint block_w, jint block_h) {
1021
+ // PBO upload only supports ASTC (no CPU decode path)
1022
+ if (!use_astc) {
1023
+ LOGW("PBO upload only supports ASTC, falling back to regular upload");
1024
+ return Java_dev_mediacache_alphavideo_gpu_AlphaVideoGpuNative_nativeUploadTextureArrayLayerFromBuffer(
1025
+ env, clazz, texture_id, layer, width, height, buffer, data_length, use_astc, gl_format, block_w, block_h);
1026
+ }
1027
+
1028
+ GLuint texId = (GLuint)texture_id;
1029
+ GLenum astcGlFormat = (gl_format > 0) ? (GLenum)gl_format : GL_COMPRESSED_RGBA_ASTC_4x4_KHR;
1030
+
1031
+ void* srcData = env->GetDirectBufferAddress(buffer);
1032
+ if (!srcData) {
1033
+ LOGE("Failed to get direct buffer address for PBO upload");
1034
+ return JNI_FALSE;
1035
+ }
1036
+
1037
+ PBOContext* ctx = nullptr;
1038
+ {
1039
+ std::lock_guard<std::mutex> lock(g_pbo_mutex);
1040
+ auto it = g_pbo_contexts.find(pboKey(texId));
1041
+ if (it == g_pbo_contexts.end() || !it->second.initialized) {
1042
+ LOGW("PBOs not initialized for texture %u, falling back to regular upload", texId);
1043
+ return Java_dev_mediacache_alphavideo_gpu_AlphaVideoGpuNative_nativeUploadTextureArrayLayerFromBuffer(
1044
+ env, clazz, texture_id, layer, width, height, buffer, data_length, use_astc, gl_format, block_w, block_h);
1045
+ }
1046
+ ctx = &it->second;
1047
+ }
1048
+
1049
+ if (data_length > ctx->buffer_size) {
1050
+ LOGE("Data length %d exceeds PBO size %d", data_length, ctx->buffer_size);
1051
+ return JNI_FALSE;
1052
+ }
1053
+
1054
+ int pboIndex = ctx->current_pbo;
1055
+ GLuint pbo = ctx->pbos[pboIndex];
1056
+
1057
+ // Wait for previous upload using this PBO to complete
1058
+ if (ctx->fences[pboIndex]) {
1059
+ GLenum waitResult = glClientWaitSync(ctx->fences[pboIndex], GL_SYNC_FLUSH_COMMANDS_BIT, 0);
1060
+ if (waitResult == GL_TIMEOUT_EXPIRED) {
1061
+ // PBO still in use, wait with timeout
1062
+ waitResult = glClientWaitSync(ctx->fences[pboIndex], GL_SYNC_FLUSH_COMMANDS_BIT, 10000000); // 10ms
1063
+ if (waitResult == GL_TIMEOUT_EXPIRED) {
1064
+ LOGW("PBO fence timeout, upload may stall");
1065
+ }
1066
+ }
1067
+ glDeleteSync(ctx->fences[pboIndex]);
1068
+ ctx->fences[pboIndex] = nullptr;
1069
+ }
1070
+
1071
+ // Bind PBO and map it
1072
+ glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pbo);
1073
+
1074
+ // Use glMapBufferRange for potentially better performance
1075
+ void* pboPtr = glMapBufferRange(GL_PIXEL_UNPACK_BUFFER, 0, data_length,
1076
+ GL_MAP_WRITE_BIT | GL_MAP_INVALIDATE_BUFFER_BIT);
1077
+ if (!pboPtr) {
1078
+ LOGE("glMapBufferRange failed");
1079
+ glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
1080
+ return JNI_FALSE;
1081
+ }
1082
+
1083
+ // Copy data into PBO
1084
+ memcpy(pboPtr, srcData, data_length);
1085
+
1086
+ glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER);
1087
+
1088
+ // Upload from PBO (async - returns immediately)
1089
+ glBindTexture(GL_TEXTURE_2D_ARRAY, texId);
1090
+ glCompressedTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0,
1091
+ 0, 0, layer,
1092
+ width, height, 1,
1093
+ astcGlFormat,
1094
+ data_length, (void*)0); // Offset 0 in bound PBO
1095
+
1096
+ // Create fence for this upload
1097
+ ctx->fences[pboIndex] = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
1098
+
1099
+ // Unbind PBO
1100
+ glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
1101
+
1102
+ // Swap to other PBO for next upload
1103
+ ctx->current_pbo = 1 - pboIndex;
1104
+
1105
+ GLenum err = glGetError();
1106
+ if (err != GL_NO_ERROR) {
1107
+ LOGE("PBO upload error: 0x%x", err);
1108
+ return JNI_FALSE;
1109
+ }
1110
+
1111
+ return JNI_TRUE;
1112
+ }
1113
+
1114
+ /**
1115
+ * Initialize streaming decompression for internal KTX2 zstd (supercompressionScheme=2).
1116
+ * Only the level0 data is compressed; the header is not.
1117
+ *
1118
+ * @param fd Descriptor of the file holding the KTX2 (it may start at an offset, e.g. an APK asset)
1119
+ * @param data_offset Offset of the compressed level0 data within the descriptor
1120
+ * @param compressed_size Size of compressed data
1121
+ * @return Stream ID (> 0), or 0 on error
1122
+ */
1123
+ JNIEXPORT jlong JNICALL
1124
+ Java_dev_mediacache_alphavideo_gpu_AlphaVideoGpuNative_nativeInternalZstdStreamingInit(JNIEnv *env, jclass clazz,
1125
+ jint fd,
1126
+ jlong data_offset,
1127
+ jlong compressed_size) {
1128
+ auto state = std::make_unique<StreamingDecompressState>();
1129
+ if (!mapFdRange(state.get(), fd, data_offset, compressed_size)) return 0;
1130
+ jlong streamId = registerStream(std::move(state));
1131
+ LOGI("Internal zstd streaming initialized: stream %lld, offset=%lld, size=%lld",
1132
+ (long long)streamId, (long long)data_offset, (long long)compressed_size);
1133
+ return streamId;
1134
+ }
1135
+
1136
+ /**
1137
+ * The same over a range reader (an AlphaVideoGpuReader reference, of which the stream keeps its
1138
+ * own): the level is read 256 KB at a time as it decodes, without waiting, so a call whose bytes
1139
+ * haven't arrived returns the pending status.
1140
+ *
1141
+ * @param level_offset Offset of the compressed level0 data in the file
1142
+ */
1143
+ JNIEXPORT jlong JNICALL
1144
+ Java_dev_mediacache_alphavideo_gpu_AlphaVideoGpuNative_nativeReaderZstdStreamingInit(JNIEnv *env, jclass clazz,
1145
+ jlong reader_reference,
1146
+ jlong level_offset,
1147
+ jlong compressed_size) {
1148
+ auto reader = readerFromReference(reader_reference);
1149
+ if (!reader || level_offset < 0 || compressed_size <= 0) return 0;
1150
+ auto state = std::make_unique<StreamingDecompressState>();
1151
+ state->reader = std::move(reader);
1152
+ state->level_offset = level_offset;
1153
+ state->input_size = (size_t)compressed_size;
1154
+ state->chunk.resize(kReaderChunkBytes);
1155
+ return registerStream(std::move(state));
1156
+ }
1157
+
1158
+ /** Why the stream's last call returned -1, or null. */
1159
+ JNIEXPORT jstring JNICALL
1160
+ Java_dev_mediacache_alphavideo_gpu_AlphaVideoGpuNative_nativeStreamingError(JNIEnv *env, jclass clazz,
1161
+ jlong stream_id) {
1162
+ std::lock_guard<std::mutex> lock(g_streaming_mutex);
1163
+ auto it = g_streaming_states.find(stream_id);
1164
+ if (it == g_streaming_states.end() || it->second->error.empty()) return nullptr;
1165
+ return env->NewStringUTF(it->second->error.c_str());
1166
+ }
1167
+
1168
+ } // extern "C"