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.
- package/LICENSE +21 -0
- package/README.md +953 -2
- package/THIRD_PARTY_NOTICES.md +14 -0
- package/android/src/main/AndroidManifest.xml +9 -0
- package/android/src/main/cpp/CMakeLists.txt +41 -0
- package/android/src/main/cpp/alphavideogpu_jni.cpp +1168 -0
- package/android/src/main/cpp/astc_decomp.cpp +1550 -0
- package/android/src/main/cpp/astc_decomp.h +41 -0
- package/android/src/main/cpp/source_jni.cpp +242 -0
- package/android/src/main/cpp/source_jni.h +13 -0
- package/android/src/main/java/dev/mediacache/alphavideo/gpu/AlphaVideoGpuNative.java +43 -0
- package/android/src/main/java/dev/mediacache/alphavideo/gpu/AlphaVideoGpuReader.java +209 -0
- package/android/src/main/java/dev/mediacache/alphavideo/gpu/AlphaVideoGpuSource.java +328 -0
- package/android/src/main/java/dev/mediacache/alphavideo/gpu/AlphaVideoGpuView.java +2126 -0
- package/android/src/main/java/dev/mediacache/alphavideo/gpu/Fetcher.java +37 -0
- package/android/src/main/java/dev/mediacache/alphavideo/gpu/HttpFetcher.java +156 -0
- package/android/src/main/java/dev/mediacache/alphavideo/gpu/Ktx2Metadata.java +78 -0
- package/android/src/main/java/dev/mediacache/alphavideo/gpu/RangeSourceLink.java +54 -0
- package/android/src/main/java/dev/mediacache/alphavideo/gpu/ZstdSeekTable.java +120 -0
- package/android/src/rangesource/java/dev/mediacache/alphavideo/gpu/AppDataSource.java +50 -0
- package/ios/Sources/AlphaVideoGPU/AlphaVideoGpuSource.h +63 -0
- package/ios/Sources/AlphaVideoGPU/AlphaVideoGpuSource.mm +522 -0
- package/ios/Sources/AlphaVideoGPU/AlphaVideoGpuView.mm +2722 -0
- package/ios/Sources/AlphaVideoGPU/astc_decomp.cpp +1550 -0
- package/ios/Sources/AlphaVideoGPU/astc_decomp.h +41 -0
- package/ios/Sources/AlphaVideoGPU/include/AlphaVideoGPU.h +110 -0
- package/ios/Sources/AlphaVideoGPU/zstd_seek.h +250 -0
- package/package.json +78 -4
- package/react-native/android/build.gradle +95 -0
- package/react-native/android/src/main/AndroidManifest.xml +9 -0
- package/react-native/android/src/main/java/dev/mediacache/alphavideo/gpu/reactnative/AlphaVideoGpuPackage.java +26 -0
- package/react-native/android/src/main/java/dev/mediacache/alphavideo/gpu/reactnative/AlphaVideoGpuViewManager.java +187 -0
- package/react-native/android/src/main/java/dev/mediacache/alphavideo/gpu/reactnative/PlayerEventForwarder.java +107 -0
- package/react-native/android/src/newarch/java/dev/mediacache/alphavideo/gpu/reactnative/AlphaVideoGpuViewManagerSpec.java +23 -0
- package/react-native/android/src/norangesource/java/dev/mediacache/alphavideo/gpu/AppDataSource.java +15 -0
- package/react-native/android/src/oldarch/java/dev/mediacache/alphavideo/gpu/reactnative/AlphaVideoGpuViewManagerSpec.java +56 -0
- package/react-native/ios/RNAlphaVideoGpuViewComponentView.mm +176 -0
- package/react-native/ios/RNAlphaVideoGpuViewManager.mm +175 -0
- package/react-native/src/AlphaVideoGpuView.js +2 -0
- package/react-native/src/AlphaVideoGpuView.web.tsx +296 -0
- package/react-native/src/AlphaVideoGpuViewNativeComponent.ts +128 -0
- package/react-native/src/AlphaVideoGpuViewNativeComponent.web.ts +2 -0
- package/react-native/src/index.d.ts +95 -0
- package/react-native/src/index.js +162 -0
- package/react-native-alpha-video-gpu.podspec +37 -0
- package/react-native.config.js +12 -0
- package/third_party/licenses/Apache-2.0.txt +208 -0
- package/third_party/range-source/LICENSE +21 -0
- package/third_party/range-source/README.md +10 -0
- package/third_party/range-source/include/range_source.h +395 -0
- package/third_party/range-source/include/range_source_download.h +309 -0
- package/third_party/zstd/LICENSE +30 -0
- package/third_party/zstd/zstd.c +38717 -0
- 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"
|