react-native-alpha-video-gpu 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +949 -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 +77 -4
- package/react-native/android/build.gradle +92 -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 +264 -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,2722 @@
|
|
|
1
|
+
#import "AlphaVideoGPU.h"
|
|
2
|
+
#import "AlphaVideoGpuSource.h"
|
|
3
|
+
#import <TargetConditionals.h>
|
|
4
|
+
#import <sys/mman.h>
|
|
5
|
+
#import <sys/stat.h>
|
|
6
|
+
#import <fcntl.h>
|
|
7
|
+
#import <unistd.h>
|
|
8
|
+
|
|
9
|
+
// Third-party sources compiled into this file, with their own warnings left alone
|
|
10
|
+
#pragma clang diagnostic push
|
|
11
|
+
#pragma clang diagnostic ignored "-Weverything"
|
|
12
|
+
|
|
13
|
+
#define ZSTD_STATIC_LINKING_ONLY
|
|
14
|
+
#include "zstd/zstd.h"
|
|
15
|
+
#include "zstd/zstd.c"
|
|
16
|
+
|
|
17
|
+
// Software ASTC decoder, for the simulator
|
|
18
|
+
#include "astc_decomp.cpp"
|
|
19
|
+
|
|
20
|
+
#pragma clang diagnostic pop
|
|
21
|
+
|
|
22
|
+
#include "zstd_seek.h"
|
|
23
|
+
|
|
24
|
+
// How long a display tick spends catching up to a seek; the rest goes on over the next ticks
|
|
25
|
+
static const CFTimeInterval kSeekSkipSliceSeconds = 0.008;
|
|
26
|
+
|
|
27
|
+
// GPU slots when streaming with the ram strategy, or an uncompressed file: frames are uploaded
|
|
28
|
+
// as they're shown, and the previous ones may still be drawing
|
|
29
|
+
static const NSInteger kRamRingSize = 4;
|
|
30
|
+
|
|
31
|
+
// The store's cap when maxStorageBytes isn't set
|
|
32
|
+
static const int64_t kAVGDefaultMaxStorageBytes = 512LL * 1024 * 1024;
|
|
33
|
+
// Bytes of a level read from a reader at a time
|
|
34
|
+
static const size_t kAVGReaderChunkBytes = 256 * 1024;
|
|
35
|
+
// Bytes read first from a reader for the header and metadata, which most files fit in
|
|
36
|
+
static const int64_t kAVGHeaderGuessBytes = 16 * 1024;
|
|
37
|
+
|
|
38
|
+
static_assert(AVGSourceReaderPending == AVG_ZSTD_READ_PENDING, "a pending read means the same to both");
|
|
39
|
+
|
|
40
|
+
#define AVG_VLOG(fmt, ...) do { if (self.verbose) NSLog(@"AlphaVideoGpuView: " fmt, ##__VA_ARGS__); } while (0)
|
|
41
|
+
|
|
42
|
+
static void AVGWarnWithoutAddOn(void)
|
|
43
|
+
{
|
|
44
|
+
static dispatch_once_t once;
|
|
45
|
+
dispatch_once(&once, ^{
|
|
46
|
+
NSLog(@"[AlphaVideoGPU] range-source isn't installed (the RangeSource SwiftPM product, the alpha-video-gpu pod's "
|
|
47
|
+
@"Source subspec, or for React Native npm i react-native-range-source): URLs are downloaded whole for each "
|
|
48
|
+
@"play, without range requests, storage, maxStorageBytes or cacheKey");
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
#pragma mark - Source resolution
|
|
53
|
+
|
|
54
|
+
static BOOL AVGIsRemote(NSString *uri) {
|
|
55
|
+
return [uri hasPrefix:@"https://"] || [uri hasPrefix:@"http://"];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The file an asset:// (in the app bundle), file:// or absolute-path source names. */
|
|
59
|
+
static NSString *AVGLocalPath(NSString *uri, NSString **error) {
|
|
60
|
+
NSString *path = nil;
|
|
61
|
+
if ([uri hasPrefix:@"asset://"]) {
|
|
62
|
+
NSString *relative = [uri substringFromIndex:@"asset://".length];
|
|
63
|
+
relative = [relative stringByRemovingPercentEncoding] ?: relative;
|
|
64
|
+
while ([relative hasPrefix:@"/"]) {
|
|
65
|
+
relative = [relative substringFromIndex:1];
|
|
66
|
+
}
|
|
67
|
+
if (relative.length > 0) {
|
|
68
|
+
path = [[NSBundle mainBundle].resourcePath stringByAppendingPathComponent:relative];
|
|
69
|
+
}
|
|
70
|
+
} else if ([uri hasPrefix:@"file://"]) {
|
|
71
|
+
NSURL *url = [NSURL URLWithString:uri];
|
|
72
|
+
path = url.isFileURL ? url.path : [uri substringFromIndex:@"file://".length];
|
|
73
|
+
} else if ([uri hasPrefix:@"/"]) {
|
|
74
|
+
path = uri;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!path) {
|
|
78
|
+
*error = [NSString stringWithFormat:@"Unsupported source '%@'. Use https://, file://, an absolute path or asset://", uri];
|
|
79
|
+
return nil;
|
|
80
|
+
}
|
|
81
|
+
if (![[NSFileManager defaultManager] fileExistsAtPath:path]) {
|
|
82
|
+
*error = [NSString stringWithFormat:@"File not found: %@", uri];
|
|
83
|
+
return nil;
|
|
84
|
+
}
|
|
85
|
+
return path;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** A level's bytes through a reader, for AVGZstdStream's read function. */
|
|
89
|
+
typedef struct {
|
|
90
|
+
void *reader; // the view's AVGSourceReader, which it holds while the stream reads
|
|
91
|
+
uint64_t levelOffset;
|
|
92
|
+
} AVGLevelInput;
|
|
93
|
+
|
|
94
|
+
static int64_t AVGReadLevel(void *context, uint64_t offset, uint8_t *buffer, size_t length) {
|
|
95
|
+
AVGLevelInput *input = (AVGLevelInput *)context;
|
|
96
|
+
return [(__bridge AVGSourceReader *)input->reader tryReadAt:(int64_t)(input->levelOffset + offset)
|
|
97
|
+
into:buffer
|
|
98
|
+
length:length];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
@interface AlphaVideoGpuView () <MTKViewDelegate>
|
|
102
|
+
|
|
103
|
+
@property (nonatomic, strong) id<MTLCommandQueue> commandQueue;
|
|
104
|
+
@property (nonatomic, strong) id<MTLBuffer> vertexBuffer;
|
|
105
|
+
|
|
106
|
+
@property (nonatomic, assign) NSInteger currentFrame;
|
|
107
|
+
@property (nonatomic, assign) NSInteger totalFrames;
|
|
108
|
+
@property (nonatomic, assign) CGFloat frameRate;
|
|
109
|
+
@property (nonatomic, assign) BOOL isPlaying;
|
|
110
|
+
@property (nonatomic, assign) CFTimeInterval frameInterval;
|
|
111
|
+
|
|
112
|
+
@property (nonatomic, assign) NSInteger frameWidth;
|
|
113
|
+
@property (nonatomic, assign) NSInteger frameHeight;
|
|
114
|
+
|
|
115
|
+
@property (nonatomic, strong) CADisplayLink *displayLink;
|
|
116
|
+
@property (nonatomic, assign) CFTimeInterval lastFrameTime;
|
|
117
|
+
|
|
118
|
+
@end
|
|
119
|
+
|
|
120
|
+
/** Frame rate from KTXanimData (per-frame duration / timescale) in a key/value data block, or 0. */
|
|
121
|
+
static double AVGAnimFrameRate(const uint8_t *kvd, NSUInteger length) {
|
|
122
|
+
NSUInteger pos = 0;
|
|
123
|
+
while (pos + 4 <= length) {
|
|
124
|
+
uint32_t entryLength = avg_read_u32le(kvd + pos);
|
|
125
|
+
NSUInteger entryStart = pos + 4;
|
|
126
|
+
if (entryLength == 0 || entryStart + entryLength > length) break;
|
|
127
|
+
NSUInteger keyEnd = entryStart;
|
|
128
|
+
while (keyEnd < entryStart + entryLength && kvd[keyEnd] != 0) keyEnd++;
|
|
129
|
+
static const char kAnimKey[] = "KTXanimData";
|
|
130
|
+
if (keyEnd - entryStart == sizeof(kAnimKey) - 1 && memcmp(kvd + entryStart, kAnimKey, keyEnd - entryStart) == 0 &&
|
|
131
|
+
entryStart + entryLength >= keyEnd + 1 + 12) {
|
|
132
|
+
uint32_t duration = avg_read_u32le(kvd + keyEnd + 1);
|
|
133
|
+
uint32_t timescale = avg_read_u32le(kvd + keyEnd + 5);
|
|
134
|
+
return duration > 0 && timescale > 0 ? (double)timescale / duration : 0;
|
|
135
|
+
}
|
|
136
|
+
pos = entryStart + entryLength + (4 - entryLength % 4) % 4;
|
|
137
|
+
}
|
|
138
|
+
return 0;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Frame size, frame count and vkFormat from the KTX2 header, and the frame rate from KTXanimData
|
|
143
|
+
* (30 without it). Both are standard KTX 2.0; texenc writes no other metadata.
|
|
144
|
+
*/
|
|
145
|
+
static NSDictionary *AVGKTX2Metadata(const uint8_t *header, const uint8_t *kvd, NSUInteger kvdLength) {
|
|
146
|
+
double frameRate = kvd ? AVGAnimFrameRate(kvd, kvdLength) : 0;
|
|
147
|
+
return @{
|
|
148
|
+
@"vkFormat": @(avg_read_u32le(header + 12)),
|
|
149
|
+
@"frameWidth": @(avg_read_u32le(header + 20)),
|
|
150
|
+
@"frameHeight": @(avg_read_u32le(header + 24)),
|
|
151
|
+
@"frameCount": @(avg_read_u32le(header + 32)),
|
|
152
|
+
@"frameRate": @(frameRate > 0 ? frameRate : 30.0),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** ASTC (VkFormat 157-184, either colour space) in an array texture: the only files that play. */
|
|
157
|
+
static BOOL AVGIsASTCArray(NSDictionary *metadata) {
|
|
158
|
+
uint32_t vkFormat = [metadata[@"vkFormat"] unsignedIntValue];
|
|
159
|
+
return vkFormat >= 157 && vkFormat <= 184 && [metadata[@"frameCount"] unsignedIntValue] >= 1;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
NSString *const AlphaVideoGpuErrorNoSourceModule = @"NO_SOURCE_MODULE";
|
|
163
|
+
|
|
164
|
+
@implementation AlphaVideoGpuView {
|
|
165
|
+
MTLPixelFormat _metalPixelFormat;
|
|
166
|
+
BOOL _useCompressedFormat;
|
|
167
|
+
id<MTLSamplerState> _samplerState;
|
|
168
|
+
NSString *_sourceUri;
|
|
169
|
+
// The local file the current source resolved to (a bundle file or a path)
|
|
170
|
+
NSString *_localPath;
|
|
171
|
+
// What a URL or data source is read through, while it plays
|
|
172
|
+
AVGSourceReader *_reader;
|
|
173
|
+
// The headers and data source the current source was opened with
|
|
174
|
+
NSDictionary<NSString *, NSString *> *_playingHeaders;
|
|
175
|
+
NSString *_playingDataSource;
|
|
176
|
+
// A whole file read from _reader, which _mmapData points into in place of a mapping
|
|
177
|
+
NSData *_heldFileData;
|
|
178
|
+
// The zstd stream's input from _reader, and the buffer it reads into
|
|
179
|
+
AVGLevelInput _levelInput;
|
|
180
|
+
NSMutableData *_streamChunk;
|
|
181
|
+
// Streaming stopped on a read or decompression error
|
|
182
|
+
BOOL _streamFailed;
|
|
183
|
+
// Bumped for every new source, so a slow download for an old one is dropped
|
|
184
|
+
NSUInteger _sourceGeneration;
|
|
185
|
+
// The cache key the loaded/loading source belongs to. Compared against self.cacheKey
|
|
186
|
+
// to tell a re-signed URL for the same object apart from a new asset.
|
|
187
|
+
NSString *_loadedCacheKey;
|
|
188
|
+
BOOL _isSourceLoaded;
|
|
189
|
+
BOOL _isSourceLoading;
|
|
190
|
+
BOOL _isDestroyed;
|
|
191
|
+
|
|
192
|
+
id<MTLTexture> _textureArray;
|
|
193
|
+
NSInteger _textureArrayLayerCount;
|
|
194
|
+
id<MTLRenderPipelineState> _arrayPipelineState;
|
|
195
|
+
id<MTLBuffer> _arrayUniformBuffer;
|
|
196
|
+
|
|
197
|
+
// Streaming ring buffer support (for efficient memory usage)
|
|
198
|
+
BOOL _streamingRingBufferMode;
|
|
199
|
+
NSInteger _streamingRingBufferSize;
|
|
200
|
+
NSMutableArray<NSNumber *> *_ringSlots; // Tracks which virtual frame is in each GPU slot
|
|
201
|
+
NSMutableDictionary<NSNumber *, NSData *> *_frameCache; // CPU frame cache
|
|
202
|
+
NSInteger _decoderPosition; // Current decoder frame position
|
|
203
|
+
NSInteger _decoderLoopIteration; // Which loop iteration the decoder is on
|
|
204
|
+
NSInteger _playbackLoopIteration; // Which loop iteration playback is on
|
|
205
|
+
BOOL _useVramStrategy; // True for VRAM-heavy mode
|
|
206
|
+
// Per pass, for tuning streamingPrefetchAhead: the fewest frames decoded after the shown one,
|
|
207
|
+
// and how often playback waited for the decoder
|
|
208
|
+
NSInteger _fewestAhead;
|
|
209
|
+
NSInteger _stalls;
|
|
210
|
+
BOOL _stalled;
|
|
211
|
+
|
|
212
|
+
// The shown frame changed (a load or a seek) and is drawn once decoded, playing or not
|
|
213
|
+
BOOL _frameOwed;
|
|
214
|
+
// A clip that doesn't loop has reached its last frame; playing again starts it over
|
|
215
|
+
BOOL _ended;
|
|
216
|
+
|
|
217
|
+
// Streaming decompression state
|
|
218
|
+
ZSTD_DStream *_dstream;
|
|
219
|
+
NSData *_compressedData;
|
|
220
|
+
// Reads _compressedData through _dstream
|
|
221
|
+
AVGZstdStream _zstdStream;
|
|
222
|
+
// Empty when the level is one zstd stream
|
|
223
|
+
AVGZstdSeekTable _seekTable;
|
|
224
|
+
// Decoded bytes still to drop before _decoderPosition: a seek catching up
|
|
225
|
+
uint64_t _pendingSkipBytes;
|
|
226
|
+
NSMutableData *_frameAccumulator;
|
|
227
|
+
uint32_t _streamBytesPerLayer;
|
|
228
|
+
uint64_t _streamLevel0Offset;
|
|
229
|
+
|
|
230
|
+
// Uncompressed streaming (mmap)
|
|
231
|
+
int _mmapFd;
|
|
232
|
+
void *_mmapData;
|
|
233
|
+
size_t _mmapSize;
|
|
234
|
+
BOOL _isUncompressedStreaming;
|
|
235
|
+
|
|
236
|
+
// Decoded RGBA cache for simulator ASTC decompression (background thread)
|
|
237
|
+
NSMutableDictionary<NSNumber *, NSData *> *_decodedRgbaCache;
|
|
238
|
+
NSInteger _uncompressedDecoderPosition;
|
|
239
|
+
dispatch_queue_t _astcDecodeQueue;
|
|
240
|
+
BOOL _uncompressedDecodeQueued; // main thread only
|
|
241
|
+
|
|
242
|
+
// ASTC block dimensions (derived from vkFormat, default 4x4)
|
|
243
|
+
uint32_t _astcBlockWidth;
|
|
244
|
+
uint32_t _astcBlockHeight;
|
|
245
|
+
|
|
246
|
+
// Staging buffers for async blit uploads (double-buffered)
|
|
247
|
+
id<MTLBuffer> _stagingBuffers[2];
|
|
248
|
+
NSInteger _currentStagingBuffer;
|
|
249
|
+
id<MTLCommandQueue> _blitCommandQueue;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
- (instancetype)initWithFrame:(CGRect)frame
|
|
253
|
+
{
|
|
254
|
+
AVG_VLOG(@"initWithFrame %@", NSStringFromCGRect(frame));
|
|
255
|
+
|
|
256
|
+
self = [super initWithFrame:frame];
|
|
257
|
+
if (self) {
|
|
258
|
+
self.device = MTLCreateSystemDefaultDevice();
|
|
259
|
+
AVG_VLOG(@"Metal device=%@", self.device.name);
|
|
260
|
+
self.commandQueue = [self.device newCommandQueue];
|
|
261
|
+
self.delegate = self;
|
|
262
|
+
|
|
263
|
+
// Configure MTKView to render on demand only (we control timing via CADisplayLink)
|
|
264
|
+
self.paused = YES;
|
|
265
|
+
self.enableSetNeedsDisplay = YES;
|
|
266
|
+
|
|
267
|
+
// CRITICAL: Set up transparency
|
|
268
|
+
self.opaque = NO;
|
|
269
|
+
self.backgroundColor = [UIColor clearColor];
|
|
270
|
+
self.layer.opaque = NO;
|
|
271
|
+
self.clearColor = MTLClearColorMake(0, 0, 0, 0);
|
|
272
|
+
|
|
273
|
+
// Initialize with default values
|
|
274
|
+
self.currentFrame = 0;
|
|
275
|
+
self.totalFrames = 0;
|
|
276
|
+
self.frameRate = 30.0;
|
|
277
|
+
self.isPlaying = NO;
|
|
278
|
+
self.loop = YES;
|
|
279
|
+
self.playbackSpeed = 1.0;
|
|
280
|
+
self.frameInterval = 1.0 / self.frameRate;
|
|
281
|
+
self.enableFrameEvents = NO; // Disabled by default for performance
|
|
282
|
+
_isSourceLoaded = NO;
|
|
283
|
+
_isSourceLoading = NO;
|
|
284
|
+
_isDestroyed = NO;
|
|
285
|
+
|
|
286
|
+
// Streaming configuration defaults (match Android)
|
|
287
|
+
self.streamingPrefetchAhead = 8; // Frames to decode ahead
|
|
288
|
+
self.memoryStrategy = @"vram"; // vram, ram
|
|
289
|
+
|
|
290
|
+
// Initialize streaming state
|
|
291
|
+
_streamingRingBufferMode = NO;
|
|
292
|
+
_streamingRingBufferSize = 0;
|
|
293
|
+
_ringSlots = nil;
|
|
294
|
+
_frameCache = nil;
|
|
295
|
+
_decoderPosition = 0;
|
|
296
|
+
_decoderLoopIteration = 0;
|
|
297
|
+
_playbackLoopIteration = 0;
|
|
298
|
+
_useVramStrategy = NO;
|
|
299
|
+
_dstream = NULL;
|
|
300
|
+
_compressedData = nil;
|
|
301
|
+
memset(&_zstdStream, 0, sizeof(_zstdStream));
|
|
302
|
+
memset(&_seekTable, 0, sizeof(_seekTable));
|
|
303
|
+
_pendingSkipBytes = 0;
|
|
304
|
+
_frameAccumulator = nil;
|
|
305
|
+
_streamBytesPerLayer = 0;
|
|
306
|
+
_streamLevel0Offset = 0;
|
|
307
|
+
_mmapFd = -1;
|
|
308
|
+
_mmapData = NULL;
|
|
309
|
+
_mmapSize = 0;
|
|
310
|
+
_isUncompressedStreaming = NO;
|
|
311
|
+
_decodedRgbaCache = nil;
|
|
312
|
+
_uncompressedDecoderPosition = 0;
|
|
313
|
+
_astcDecodeQueue = dispatch_queue_create("dev.mediacache.alphavideo.gpu.astcdecode", DISPATCH_QUEUE_SERIAL);
|
|
314
|
+
_astcBlockWidth = 4;
|
|
315
|
+
_astcBlockHeight = 4;
|
|
316
|
+
|
|
317
|
+
// Detect and cache optimal texture format
|
|
318
|
+
[self detectAndCacheTextureFormat];
|
|
319
|
+
|
|
320
|
+
// Default values
|
|
321
|
+
self.autoPlay = YES;
|
|
322
|
+
self.stream = YES;
|
|
323
|
+
self.storage = nil;
|
|
324
|
+
|
|
325
|
+
[self setupMetal];
|
|
326
|
+
[self setupVertexData];
|
|
327
|
+
}
|
|
328
|
+
return self;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
- (void)detectAndCacheTextureFormat
|
|
332
|
+
{
|
|
333
|
+
// Check if running on simulator
|
|
334
|
+
#if TARGET_OS_SIMULATOR
|
|
335
|
+
AVG_VLOG(@"Running on simulator - decoding ASTC to RGBA in software");
|
|
336
|
+
_metalPixelFormat = MTLPixelFormatRGBA8Unorm;
|
|
337
|
+
_useCompressedFormat = NO;
|
|
338
|
+
#else
|
|
339
|
+
// Check for ASTC support (all real iOS devices with Metal support ASTC)
|
|
340
|
+
if (@available(iOS 8.0, *)) {
|
|
341
|
+
// Check device capabilities - ASTC is supported on A8 and later
|
|
342
|
+
if ([self.device supportsFamily:MTLGPUFamilyApple2]) {
|
|
343
|
+
AVG_VLOG(@"The GPU decodes ASTC");
|
|
344
|
+
_metalPixelFormat = MTLPixelFormatASTC_4x4_LDR;
|
|
345
|
+
_useCompressedFormat = YES;
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Fallback to RGBA32 for older devices
|
|
351
|
+
AVG_VLOG(@"No GPU ASTC support - decoding ASTC to RGBA in software");
|
|
352
|
+
_metalPixelFormat = MTLPixelFormatRGBA8Unorm;
|
|
353
|
+
_useCompressedFormat = NO;
|
|
354
|
+
#endif
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Map a KTX2 vkFormat to the corresponding MTLPixelFormat and ASTC block dimensions.
|
|
359
|
+
* Supports all 14 ASTC block sizes (UNORM/SRGB pairs, VkFormat 157–184).
|
|
360
|
+
* Updates _metalPixelFormat, _astcBlockWidth, _astcBlockHeight.
|
|
361
|
+
* Returns YES if the vkFormat is a recognized ASTC format.
|
|
362
|
+
*/
|
|
363
|
+
- (BOOL)configureASTCFormatFromVkFormat:(uint32_t)vkFormat
|
|
364
|
+
{
|
|
365
|
+
// All ASTC VkFormats come in UNORM/SRGB pairs.
|
|
366
|
+
// Both map to the LDR MTLPixelFormat, as on Android and web: the encoder stores premultiplied
|
|
367
|
+
// sRGB-encoded bytes that must reach the drawable unchanged, and an _sRGB format would
|
|
368
|
+
// linearise them on sampling with nothing converting back.
|
|
369
|
+
struct ASTCFormatInfo {
|
|
370
|
+
uint32_t vkFormatUnorm;
|
|
371
|
+
uint32_t vkFormatSrgb;
|
|
372
|
+
MTLPixelFormat pixelFormat;
|
|
373
|
+
uint32_t blockWidth;
|
|
374
|
+
uint32_t blockHeight;
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
static const ASTCFormatInfo formats[] = {
|
|
378
|
+
{ 157, 158, MTLPixelFormatASTC_4x4_LDR, 4, 4 },
|
|
379
|
+
{ 159, 160, MTLPixelFormatASTC_5x4_LDR, 5, 4 },
|
|
380
|
+
{ 161, 162, MTLPixelFormatASTC_5x5_LDR, 5, 5 },
|
|
381
|
+
{ 163, 164, MTLPixelFormatASTC_6x5_LDR, 6, 5 },
|
|
382
|
+
{ 165, 166, MTLPixelFormatASTC_6x6_LDR, 6, 6 },
|
|
383
|
+
{ 167, 168, MTLPixelFormatASTC_8x5_LDR, 8, 5 },
|
|
384
|
+
{ 169, 170, MTLPixelFormatASTC_8x6_LDR, 8, 6 },
|
|
385
|
+
{ 171, 172, MTLPixelFormatASTC_8x8_LDR, 8, 8 },
|
|
386
|
+
{ 173, 174, MTLPixelFormatASTC_10x5_LDR, 10, 5 },
|
|
387
|
+
{ 175, 176, MTLPixelFormatASTC_10x6_LDR, 10, 6 },
|
|
388
|
+
{ 177, 178, MTLPixelFormatASTC_10x8_LDR, 10, 8 },
|
|
389
|
+
{ 179, 180, MTLPixelFormatASTC_10x10_LDR, 10, 10 },
|
|
390
|
+
{ 181, 182, MTLPixelFormatASTC_12x10_LDR, 12, 10 },
|
|
391
|
+
{ 183, 184, MTLPixelFormatASTC_12x12_LDR, 12, 12 },
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
for (const auto &fmt : formats) {
|
|
395
|
+
if (vkFormat == fmt.vkFormatUnorm || vkFormat == fmt.vkFormatSrgb) {
|
|
396
|
+
// Always set block dimensions (needed for software decoding on simulator)
|
|
397
|
+
_astcBlockWidth = fmt.blockWidth;
|
|
398
|
+
_astcBlockHeight = fmt.blockHeight;
|
|
399
|
+
|
|
400
|
+
// Only update pixel format if using compressed format (real device)
|
|
401
|
+
// On simulator, _useCompressedFormat is NO and _metalPixelFormat is RGBA8Unorm
|
|
402
|
+
#if !TARGET_OS_SIMULATOR
|
|
403
|
+
_metalPixelFormat = fmt.pixelFormat;
|
|
404
|
+
#endif
|
|
405
|
+
AVG_VLOG(@"ASTC format: %ux%u (VkFormat=%u, MTLPixelFormat=%lu, useCompressed=%d)",
|
|
406
|
+
fmt.blockWidth, fmt.blockHeight, vkFormat, (unsigned long)_metalPixelFormat, _useCompressedFormat);
|
|
407
|
+
return YES;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
AVG_VLOG(@"Unknown ASTC VkFormat: %u, falling back to 4x4", vkFormat);
|
|
412
|
+
#if !TARGET_OS_SIMULATOR
|
|
413
|
+
_metalPixelFormat = MTLPixelFormatASTC_4x4_LDR;
|
|
414
|
+
#endif
|
|
415
|
+
_astcBlockWidth = 4;
|
|
416
|
+
_astcBlockHeight = 4;
|
|
417
|
+
return NO;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Calculate bytes per layer for the current ASTC block size.
|
|
422
|
+
* All ASTC blocks are 16 bytes (128 bits) regardless of block dimensions.
|
|
423
|
+
*/
|
|
424
|
+
- (uint32_t)astcBytesPerLayerForWidth:(uint32_t)width height:(uint32_t)height
|
|
425
|
+
{
|
|
426
|
+
uint32_t blocksX = (width + _astcBlockWidth - 1) / _astcBlockWidth;
|
|
427
|
+
uint32_t blocksY = (height + _astcBlockHeight - 1) / _astcBlockHeight;
|
|
428
|
+
return blocksX * blocksY * 16;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
- (void)setSource:(NSString *)source
|
|
432
|
+
{
|
|
433
|
+
AVG_VLOG(@"setSource %@", source);
|
|
434
|
+
NSString *uri = source.length > 0 ? source : nil;
|
|
435
|
+
|
|
436
|
+
if (!uri) {
|
|
437
|
+
_source = nil;
|
|
438
|
+
_sourceUri = nil;
|
|
439
|
+
_localPath = nil;
|
|
440
|
+
_sourceGeneration++;
|
|
441
|
+
[self cleanup];
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Check if source changed
|
|
446
|
+
BOOL sameReads = [self readsAsPlaying];
|
|
447
|
+
if ([_sourceUri isEqualToString:uri] && sameReads && (_isSourceLoaded || _isSourceLoading)) {
|
|
448
|
+
AVG_VLOG(@"Source unchanged and already loaded/loading, skipping");
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// A presigned URL is re-signed every time the app lists its media, so the URL changes
|
|
453
|
+
// while still addressing the same object. When the cache key matches what is already
|
|
454
|
+
// on screen this is a credential refresh, not a new asset, and reloading would
|
|
455
|
+
// interrupt a loop that is supposed to run until unmount.
|
|
456
|
+
if (self.cacheKey.length > 0 && [self.cacheKey isEqualToString:_loadedCacheKey] && sameReads
|
|
457
|
+
&& (_isSourceLoaded || _isSourceLoading)) {
|
|
458
|
+
AVG_VLOG(@"Same cacheKey (%@), ignoring URL change", self.cacheKey);
|
|
459
|
+
_source = [uri copy];
|
|
460
|
+
_sourceUri = [uri copy];
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Frees the previous source: its decoder, mapping or reader, and frames
|
|
465
|
+
[self cleanup];
|
|
466
|
+
_source = [uri copy];
|
|
467
|
+
_sourceUri = [uri copy];
|
|
468
|
+
_loadedCacheKey = [self.cacheKey copy];
|
|
469
|
+
_isSourceLoaded = NO;
|
|
470
|
+
_isSourceLoading = YES;
|
|
471
|
+
_isDestroyed = NO;
|
|
472
|
+
self.currentFrame = 0;
|
|
473
|
+
_ended = NO;
|
|
474
|
+
_frameOwed = NO;
|
|
475
|
+
|
|
476
|
+
_localPath = nil;
|
|
477
|
+
[self notifyStateChange:@"loading"];
|
|
478
|
+
[self resolveAndLoadSource];
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/** Whether the headers and data source set are the ones the current source was opened with. */
|
|
482
|
+
- (BOOL)readsAsPlaying
|
|
483
|
+
{
|
|
484
|
+
NSString *dataSource = self.dataSource.length > 0 ? self.dataSource : nil;
|
|
485
|
+
return [self.headers ?: @{} isEqualToDictionary:_playingHeaders ?: @{}]
|
|
486
|
+
&& (dataSource == _playingDataSource || [dataSource isEqualToString:_playingDataSource]);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
- (void)setStorage:(NSString *)storage
|
|
490
|
+
{
|
|
491
|
+
_storage = [@[ @"disk", @"memory", @"none" ] containsObject:storage] ? [storage copy] : @"auto";
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Opens _sourceUri off the main thread, then loads it: a URL or a data source through a reader,
|
|
496
|
+
* anything else as a local file. Starts on the next main-queue turn, so that properties set in
|
|
497
|
+
* the same turn as source (React Native's, in any order) apply to it.
|
|
498
|
+
*/
|
|
499
|
+
- (void)resolveAndLoadSource
|
|
500
|
+
{
|
|
501
|
+
NSUInteger generation = ++_sourceGeneration;
|
|
502
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
503
|
+
if (generation != self->_sourceGeneration) return;
|
|
504
|
+
if (self->_isDestroyed) {
|
|
505
|
+
self->_isSourceLoading = NO;
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
[self openSource:generation];
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
- (void)openSource:(NSUInteger)generation
|
|
513
|
+
{
|
|
514
|
+
NSString *uri = _sourceUri;
|
|
515
|
+
_loadedCacheKey = [self.cacheKey copy];
|
|
516
|
+
_playingHeaders = [self.headers copy] ?: @{};
|
|
517
|
+
_playingDataSource = self.dataSource.length > 0 ? [self.dataSource copy] : nil;
|
|
518
|
+
NSString *dataSourceName = _playingDataSource;
|
|
519
|
+
BOOL readsRanges = dataSourceName != nil || AVGIsRemote(uri);
|
|
520
|
+
if (readsRanges && !AVGRangeSourceAvailable()) {
|
|
521
|
+
if (dataSourceName) {
|
|
522
|
+
[self notifyError:[NSString stringWithFormat:@"The data source \"%@\" needs the range-source add-on (the "
|
|
523
|
+
@"RangeSource SwiftPM product, the alpha-video-gpu pod's Source "
|
|
524
|
+
@"subspec, or for React Native react-native-range-source), which "
|
|
525
|
+
@"the app doesn't include", dataSourceName]
|
|
526
|
+
code:AlphaVideoGpuErrorNoSourceModule];
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
AVGWarnWithoutAddOn();
|
|
530
|
+
}
|
|
531
|
+
NSDictionary<NSString *, NSString *> *headers = _playingHeaders;
|
|
532
|
+
// auto: the disk for URLs; a data source's caching is its own
|
|
533
|
+
NSString *storage = ![self.storage isEqualToString:@"auto"] ? self.storage : dataSourceName ? @"none" : @"disk";
|
|
534
|
+
NSString *key = self.cacheKey.length > 0 ? self.cacheKey : uri;
|
|
535
|
+
int64_t maxStorageBytes = self.maxStorageBytes > 0 ? self.maxStorageBytes : kAVGDefaultMaxStorageBytes;
|
|
536
|
+
AVG_VLOG(@"Resolving source %@", uri);
|
|
537
|
+
|
|
538
|
+
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
|
|
539
|
+
NSString *error = nil;
|
|
540
|
+
NSString *path = nil;
|
|
541
|
+
AVGSourceReader *reader = nil;
|
|
542
|
+
if (readsRanges) {
|
|
543
|
+
NSURL *url = [NSURL URLWithString:uri];
|
|
544
|
+
reader = url ? [AVGSourceReader openURL:url
|
|
545
|
+
dataSource:dataSourceName
|
|
546
|
+
headers:headers
|
|
547
|
+
storage:storage
|
|
548
|
+
key:key
|
|
549
|
+
maxStorageBytes:maxStorageBytes
|
|
550
|
+
error:&error]
|
|
551
|
+
: nil;
|
|
552
|
+
if (!reader) error = [NSString stringWithFormat:@"Loading %@ failed: %@", uri, error ?: @"not a URL"];
|
|
553
|
+
} else {
|
|
554
|
+
path = AVGLocalPath(uri, &error);
|
|
555
|
+
}
|
|
556
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
557
|
+
if (generation != self->_sourceGeneration) {
|
|
558
|
+
[reader close];
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
if (self->_isDestroyed) {
|
|
562
|
+
[reader close];
|
|
563
|
+
self->_isSourceLoading = NO;
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
if (reader) {
|
|
567
|
+
AVG_VLOG(@"Opened %@ (%lld bytes, storage %@)", uri, reader.size, storage);
|
|
568
|
+
self->_reader = reader;
|
|
569
|
+
[self loadKTX2FromReader:reader];
|
|
570
|
+
} else if (path) {
|
|
571
|
+
self->_localPath = path;
|
|
572
|
+
[self loadKTX2FromFile:path];
|
|
573
|
+
} else {
|
|
574
|
+
[self notifyError:error];
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
- (void)notifyStateChange:(NSString *)state
|
|
581
|
+
{
|
|
582
|
+
if (self.onStateChange) self.onStateChange(state);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
- (void)notifyLoad
|
|
586
|
+
{
|
|
587
|
+
// Every load path reports here, once it can draw; show the first frame even before playing
|
|
588
|
+
_frameOwed = YES;
|
|
589
|
+
[self updateDisplayLink];
|
|
590
|
+
if (self.onLoad) self.onLoad(self.frameWidth, self.frameHeight, self.totalFrames, self.frameRate);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
- (void)notifyFrameUpdate
|
|
594
|
+
{
|
|
595
|
+
if (self.onFrameUpdate) self.onFrameUpdate(self.currentFrame, self.totalFrames);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
- (void)notifyFinished
|
|
599
|
+
{
|
|
600
|
+
if (self.onFinished) self.onFinished();
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
- (void)notifyError:(NSString *)error
|
|
604
|
+
{
|
|
605
|
+
[self notifyError:error code:nil];
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
- (void)notifyError:(NSString *)error code:(nullable NSString *)code
|
|
609
|
+
{
|
|
610
|
+
_isSourceLoading = NO;
|
|
611
|
+
NSLog(@"AlphaVideoGpuView: Error: %@", error);
|
|
612
|
+
if (self.onError) self.onError(error ?: @"Unknown error", code);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
- (void)setupMetal
|
|
616
|
+
{
|
|
617
|
+
// Create vertex shader
|
|
618
|
+
NSString *vertexShaderSource = @"#include <metal_stdlib>\n"
|
|
619
|
+
"using namespace metal;\n"
|
|
620
|
+
"struct VertexOut {\n"
|
|
621
|
+
" float4 position [[position]];\n"
|
|
622
|
+
" float2 texCoord;\n"
|
|
623
|
+
"};\n"
|
|
624
|
+
"vertex VertexOut vertex_main(uint vertexID [[vertex_id]]) {\n"
|
|
625
|
+
" VertexOut out;\n"
|
|
626
|
+
" const float2 positions[6] = {\n"
|
|
627
|
+
" float2(-1.0, -1.0), float2(1.0, -1.0), float2(-1.0, 1.0),\n"
|
|
628
|
+
" float2(-1.0, 1.0), float2(1.0, -1.0), float2(1.0, 1.0)\n"
|
|
629
|
+
" };\n"
|
|
630
|
+
" const float2 texCoords[6] = {\n"
|
|
631
|
+
" float2(0.0, 1.0), float2(1.0, 1.0), float2(0.0, 0.0),\n"
|
|
632
|
+
" float2(0.0, 0.0), float2(1.0, 1.0), float2(1.0, 0.0)\n"
|
|
633
|
+
" };\n"
|
|
634
|
+
" out.position = float4(positions[vertexID], 0.0, 1.0);\n"
|
|
635
|
+
" out.texCoord = texCoords[vertexID];\n"
|
|
636
|
+
" return out;\n"
|
|
637
|
+
"}";
|
|
638
|
+
|
|
639
|
+
AVG_VLOG(@"Compiling Metal shaders...");
|
|
640
|
+
|
|
641
|
+
NSError *error = nil;
|
|
642
|
+
id<MTLLibrary> vertexLibrary = [self.device newLibraryWithSource:vertexShaderSource options:nil error:&error];
|
|
643
|
+
if (error) {
|
|
644
|
+
NSLog(@"AlphaVideoGpuView: Vertex shader compilation error: %@", error);
|
|
645
|
+
}
|
|
646
|
+
id<MTLFunction> vertexFunction = [vertexLibrary newFunctionWithName:@"vertex_main"];
|
|
647
|
+
if (!vertexFunction) {
|
|
648
|
+
NSLog(@"AlphaVideoGpuView: Failed to get vertex_main function!");
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// Each frame is a layer of a texture array
|
|
652
|
+
NSString *arrayFragmentShaderSource = @"#include <metal_stdlib>\n"
|
|
653
|
+
"using namespace metal;\n"
|
|
654
|
+
"struct VertexOut {\n"
|
|
655
|
+
" float4 position [[position]];\n"
|
|
656
|
+
" float2 texCoord;\n"
|
|
657
|
+
"};\n"
|
|
658
|
+
"struct ArrayUniforms {\n"
|
|
659
|
+
" float layer;\n"
|
|
660
|
+
"};\n"
|
|
661
|
+
"fragment float4 fragment_array(VertexOut in [[stage_in]],\n"
|
|
662
|
+
" texture2d_array<float> tex [[texture(0)]],\n"
|
|
663
|
+
" sampler s [[sampler(0)]],\n"
|
|
664
|
+
" constant ArrayUniforms &uniforms [[buffer(0)]]) {\n"
|
|
665
|
+
" return tex.sample(s, in.texCoord, uint(uniforms.layer));\n"
|
|
666
|
+
"}";
|
|
667
|
+
|
|
668
|
+
error = nil;
|
|
669
|
+
id<MTLLibrary> arrayFragmentLibrary = [self.device newLibraryWithSource:arrayFragmentShaderSource options:nil error:&error];
|
|
670
|
+
if (error) {
|
|
671
|
+
NSLog(@"AlphaVideoGpuView: Array fragment shader compilation error: %@", error);
|
|
672
|
+
}
|
|
673
|
+
id<MTLFunction> arrayFragmentFunction = [arrayFragmentLibrary newFunctionWithName:@"fragment_array"];
|
|
674
|
+
|
|
675
|
+
if (arrayFragmentFunction) {
|
|
676
|
+
MTLRenderPipelineDescriptor *arrayPipelineDescriptor = [[MTLRenderPipelineDescriptor alloc] init];
|
|
677
|
+
arrayPipelineDescriptor.vertexFunction = vertexFunction;
|
|
678
|
+
arrayPipelineDescriptor.fragmentFunction = arrayFragmentFunction;
|
|
679
|
+
arrayPipelineDescriptor.colorAttachments[0].pixelFormat = self.colorPixelFormat;
|
|
680
|
+
arrayPipelineDescriptor.colorAttachments[0].blendingEnabled = YES;
|
|
681
|
+
arrayPipelineDescriptor.colorAttachments[0].rgbBlendOperation = MTLBlendOperationAdd;
|
|
682
|
+
arrayPipelineDescriptor.colorAttachments[0].alphaBlendOperation = MTLBlendOperationAdd;
|
|
683
|
+
arrayPipelineDescriptor.colorAttachments[0].sourceRGBBlendFactor = MTLBlendFactorOne;
|
|
684
|
+
arrayPipelineDescriptor.colorAttachments[0].sourceAlphaBlendFactor = MTLBlendFactorOne;
|
|
685
|
+
arrayPipelineDescriptor.colorAttachments[0].destinationRGBBlendFactor = MTLBlendFactorOneMinusSourceAlpha;
|
|
686
|
+
arrayPipelineDescriptor.colorAttachments[0].destinationAlphaBlendFactor = MTLBlendFactorOneMinusSourceAlpha;
|
|
687
|
+
|
|
688
|
+
error = nil;
|
|
689
|
+
_arrayPipelineState = [self.device newRenderPipelineStateWithDescriptor:arrayPipelineDescriptor error:&error];
|
|
690
|
+
if (error) {
|
|
691
|
+
NSLog(@"AlphaVideoGpuView: Failed to create array pipeline state: %@", error);
|
|
692
|
+
} else {
|
|
693
|
+
AVG_VLOG(@"Array pipeline state created successfully");
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// Create uniform buffer for texture array (just layer index)
|
|
698
|
+
_arrayUniformBuffer = [self.device newBufferWithLength:16 options:MTLResourceStorageModeShared];
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
- (void)setupVertexData
|
|
702
|
+
{
|
|
703
|
+
// Create sampler state
|
|
704
|
+
MTLSamplerDescriptor *samplerDescriptor = [[MTLSamplerDescriptor alloc] init];
|
|
705
|
+
samplerDescriptor.minFilter = MTLSamplerMinMagFilterLinear;
|
|
706
|
+
samplerDescriptor.magFilter = MTLSamplerMinMagFilterLinear;
|
|
707
|
+
samplerDescriptor.sAddressMode = MTLSamplerAddressModeClampToEdge;
|
|
708
|
+
samplerDescriptor.tAddressMode = MTLSamplerAddressModeClampToEdge;
|
|
709
|
+
_samplerState = [self.device newSamplerStateWithDescriptor:samplerDescriptor];
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/** Reads the frame metadata from data holding the file through its key/value data; nil if it isn't KTX2. */
|
|
713
|
+
- (NSDictionary *)extractKTX2Metadata:(NSData *)ktx2Data
|
|
714
|
+
{
|
|
715
|
+
if (ktx2Data.length < 80) return nil;
|
|
716
|
+
const uint8_t *bytes = (const uint8_t *)ktx2Data.bytes;
|
|
717
|
+
if (bytes[0] != 0xAB || bytes[1] != 0x4B || bytes[2] != 0x54 || bytes[3] != 0x58) return nil;
|
|
718
|
+
|
|
719
|
+
uint32_t kvdOffset = avg_read_u32le(bytes + 56);
|
|
720
|
+
uint32_t kvdLength = avg_read_u32le(bytes + 60);
|
|
721
|
+
BOOL kvdInRange = kvdLength > 0 && (uint64_t)kvdOffset + kvdLength <= ktx2Data.length;
|
|
722
|
+
return AVGKTX2Metadata(bytes, kvdInRange ? bytes + kvdOffset : NULL, kvdInRange ? kvdLength : 0);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
- (void)loadKTX2FromFile:(NSString *)path
|
|
726
|
+
{
|
|
727
|
+
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
|
|
728
|
+
// With stream, an uncompressed texture array is mmap-streamed rather than loaded into memory
|
|
729
|
+
if ([self checkAndPlayUncompressedStreaming:path]) {
|
|
730
|
+
AVG_VLOG(@"Using uncompressed streaming mode");
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// Fall back to loading entire file
|
|
735
|
+
NSData *fileData = [NSData dataWithContentsOfFile:path options:NSDataReadingMappedIfSafe error:nil];
|
|
736
|
+
if (!fileData) {
|
|
737
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
738
|
+
[self notifyError:@"Failed to load file"];
|
|
739
|
+
});
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
NSDictionary *metadata = [self extractKTX2Metadata:fileData];
|
|
744
|
+
if (!metadata) {
|
|
745
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
746
|
+
[self notifyError:@"Not a KTX2 file"];
|
|
747
|
+
});
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// Continue on main thread with parsed metadata
|
|
752
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
753
|
+
[self applyMetadataAndLoadTextures:metadata sourcePath:path fileData:nil reader:nil];
|
|
754
|
+
});
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* Loads from a reader: the header and metadata first, then a streamed zstd level as it plays, or
|
|
760
|
+
* anything else read whole into memory.
|
|
761
|
+
*/
|
|
762
|
+
- (void)loadKTX2FromReader:(AVGSourceReader *)reader
|
|
763
|
+
{
|
|
764
|
+
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
|
|
765
|
+
NSString *error = nil;
|
|
766
|
+
NSData *fileData = [self readHeaderFrom:reader error:&error];
|
|
767
|
+
NSDictionary *metadata = fileData ? [self extractKTX2Metadata:fileData] : nil;
|
|
768
|
+
if (fileData && !metadata) {
|
|
769
|
+
error = @"Not a KTX2 file";
|
|
770
|
+
}
|
|
771
|
+
uint32_t supercompression = metadata ? avg_read_u32le((const uint8_t *)fileData.bytes + 44) : 0;
|
|
772
|
+
if (metadata && self.stream && supercompression == 0) {
|
|
773
|
+
fileData = [reader readAll:&error];
|
|
774
|
+
if (!fileData) metadata = nil;
|
|
775
|
+
if (metadata && [self checkAndPlayUncompressedStreamingFromData:fileData metadata:metadata]) return;
|
|
776
|
+
}
|
|
777
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
778
|
+
if (reader != self->_reader) return;
|
|
779
|
+
if (!metadata) {
|
|
780
|
+
[self notifyError:error];
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
[self applyMetadataAndLoadTextures:metadata sourcePath:nil fileData:fileData reader:reader];
|
|
784
|
+
});
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/** The header through the key/value data, where the metadata is. */
|
|
789
|
+
- (NSData *)readHeaderFrom:(AVGSourceReader *)reader error:(NSString **)error
|
|
790
|
+
{
|
|
791
|
+
int64_t size = reader.size;
|
|
792
|
+
if (size < 0) {
|
|
793
|
+
*error = @"The file's size isn't known (the server sent no Content-Length or Content-Range)";
|
|
794
|
+
return nil;
|
|
795
|
+
}
|
|
796
|
+
if (size < 104) {
|
|
797
|
+
*error = [NSString stringWithFormat:@"Not a KTX2 file (%lld bytes)", size];
|
|
798
|
+
return nil;
|
|
799
|
+
}
|
|
800
|
+
int64_t first = MIN(size, kAVGHeaderGuessBytes);
|
|
801
|
+
NSData *header = [reader readAt:0 length:first error:error];
|
|
802
|
+
if (!header) return nil;
|
|
803
|
+
const uint8_t *bytes = (const uint8_t *)header.bytes;
|
|
804
|
+
int64_t kvdEnd = (int64_t)avg_read_u32le(bytes + 56) + avg_read_u32le(bytes + 60);
|
|
805
|
+
if (kvdEnd > first && kvdEnd <= size) header = [reader readAt:0 length:kvdEnd error:error];
|
|
806
|
+
return header;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
/**
|
|
810
|
+
* The seek table from a level's last bytes, or an empty one when it has none. NO with error set
|
|
811
|
+
* when they can't be read.
|
|
812
|
+
*/
|
|
813
|
+
- (BOOL)readSeekTable:(AVGZstdSeekTable *)table
|
|
814
|
+
from:(AVGSourceReader *)reader
|
|
815
|
+
levelOffset:(uint64_t)levelOffset
|
|
816
|
+
levelLength:(uint64_t)levelLength
|
|
817
|
+
decompressedLength:(uint64_t)decompressedLength
|
|
818
|
+
error:(NSString **)error
|
|
819
|
+
{
|
|
820
|
+
memset(table, 0, sizeof(*table));
|
|
821
|
+
uint64_t guess = MIN(levelLength, (uint64_t)AVG_ZSTD_SEEK_TAIL_GUESS);
|
|
822
|
+
NSData *tail = [reader readAt:(int64_t)(levelOffset + levelLength - guess) length:(int64_t)guess error:error];
|
|
823
|
+
if (!tail) return NO;
|
|
824
|
+
uint64_t needed = avg_zstd_seek_table_tail_size((const uint8_t *)tail.bytes, tail.length);
|
|
825
|
+
if (needed > guess && needed <= levelLength) {
|
|
826
|
+
tail = [reader readAt:(int64_t)(levelOffset + levelLength - needed) length:(int64_t)needed error:error];
|
|
827
|
+
if (!tail) return NO;
|
|
828
|
+
}
|
|
829
|
+
avg_zstd_seek_table_parse_tail((const uint8_t *)tail.bytes, tail.length, levelLength, decompressedLength, table);
|
|
830
|
+
return YES;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
/** Ends streaming on a read or decompression error, with onError. */
|
|
834
|
+
- (void)failStream:(const char *)error
|
|
835
|
+
{
|
|
836
|
+
if (_streamFailed) return;
|
|
837
|
+
_streamFailed = YES;
|
|
838
|
+
NSString *why = @(error);
|
|
839
|
+
if (_reader && strcmp(error, "reading failed") == 0) why = _reader.lastError;
|
|
840
|
+
_frameOwed = NO;
|
|
841
|
+
[self pause];
|
|
842
|
+
[self updateDisplayLink];
|
|
843
|
+
[self notifyError:[NSString stringWithFormat:@"Reading %@ failed: %@", _sourceUri, why]];
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
#pragma mark - On-Demand Frame Decoding
|
|
847
|
+
|
|
848
|
+
- (void)decodeFramesAhead:(NSInteger)currentVirtualPos
|
|
849
|
+
{
|
|
850
|
+
if (!_dstream || (!_zstdStream.data && !_zstdStream.read) || !_streamingRingBufferMode || _streamFailed) {
|
|
851
|
+
static int skipCount = 0;
|
|
852
|
+
if (skipCount++ < 5) {
|
|
853
|
+
AVG_VLOG(@"decodeFramesAhead skipping - dstream=%p, ringMode=%d, failed=%d",
|
|
854
|
+
_dstream, _streamingRingBufferMode, _streamFailed);
|
|
855
|
+
}
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
// A seek still catching up: nothing past it can be decoded yet
|
|
860
|
+
if (![self skipPendingBytesUntil:CACurrentMediaTime() + kSeekSkipSliceSeconds]) return;
|
|
861
|
+
|
|
862
|
+
// Calculate how many frames to decode ahead
|
|
863
|
+
// Use relative distance to handle wraparound correctly (matching Android approach)
|
|
864
|
+
NSInteger vPosModulus = self.totalFrames * 2; // Bounded virtual position space
|
|
865
|
+
NSInteger currentDecodeVPos = (_decoderLoopIteration * self.totalFrames + _decoderPosition) % vPosModulus;
|
|
866
|
+
|
|
867
|
+
// Calculate frames ahead using relative distance
|
|
868
|
+
NSInteger framesAhead = (currentDecodeVPos - currentVirtualPos + vPosModulus) % vPosModulus;
|
|
869
|
+
// framesAhead counts the shown frame
|
|
870
|
+
NSInteger framesToDecode = self.streamingPrefetchAhead + 1 - framesAhead;
|
|
871
|
+
|
|
872
|
+
if (framesToDecode <= 0) {
|
|
873
|
+
static int skipLogCount = 0;
|
|
874
|
+
if (skipLogCount++ < 10 || skipLogCount % 60 == 0) {
|
|
875
|
+
AVG_VLOG(@"decodeFramesAhead skipping - vPos=%ld, decodeVPos=%ld, framesAhead=%ld (already ahead enough)",
|
|
876
|
+
(long)currentVirtualPos, (long)currentDecodeVPos, (long)framesAhead);
|
|
877
|
+
}
|
|
878
|
+
return; // Already ahead enough
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
static int decodeLogCount = 0;
|
|
882
|
+
if (decodeLogCount++ < 20 || framesToDecode > 0) {
|
|
883
|
+
static int logThrottle = 0;
|
|
884
|
+
if (logThrottle++ % 30 == 0 || framesToDecode > 0) {
|
|
885
|
+
AVG_VLOG(@"decodeFramesAhead vPos=%ld, decodeVPos=%ld, framesAhead=%ld, framesToDecode=%ld, decoderPos=%ld, decoderLoop=%ld",
|
|
886
|
+
(long)currentVirtualPos, (long)currentDecodeVPos, (long)framesAhead, (long)framesToDecode,
|
|
887
|
+
(long)_decoderPosition, (long)_decoderLoopIteration);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
while (framesToDecode > 0) {
|
|
892
|
+
// Check if we need to restart decompression for looping
|
|
893
|
+
if (_decoderPosition >= self.totalFrames) {
|
|
894
|
+
_decoderPosition = 0;
|
|
895
|
+
_decoderLoopIteration++;
|
|
896
|
+
|
|
897
|
+
avg_zstd_stream_restart(&_zstdStream, 0);
|
|
898
|
+
[_frameAccumulator setLength:0];
|
|
899
|
+
|
|
900
|
+
AVG_VLOG(@"Decoder wrap-around: loop %ld, decoding from the start", (long)_decoderLoopIteration);
|
|
901
|
+
|
|
902
|
+
// After wrap-around, recalculate how many frames we need to decode
|
|
903
|
+
// Use the same formula as at the start of the method
|
|
904
|
+
NSInteger newDecodeVPos = (_decoderLoopIteration * self.totalFrames + _decoderPosition) % vPosModulus;
|
|
905
|
+
NSInteger newFramesAhead = (newDecodeVPos - currentVirtualPos + vPosModulus) % vPosModulus;
|
|
906
|
+
framesToDecode = self.streamingPrefetchAhead + 1 - newFramesAhead;
|
|
907
|
+
if (framesToDecode <= 0) {
|
|
908
|
+
AVG_VLOG(@"After wrap-around: already ahead enough, stopping decode");
|
|
909
|
+
break;
|
|
910
|
+
}
|
|
911
|
+
AVG_VLOG(@"After wrap-around: newDecodeVPos=%ld, framesAhead=%ld, framesToDecode=%ld",
|
|
912
|
+
(long)newDecodeVPos, (long)newFramesAhead, (long)framesToDecode);
|
|
913
|
+
// Don't continue - fall through to decompress the header and first frames
|
|
914
|
+
// The continue was causing us to skip decompression after wrap-around
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
// Decompress the rest of the frame straight into the accumulator, across zstd frame ends
|
|
918
|
+
NSUInteger held = _frameAccumulator.length;
|
|
919
|
+
if (held < _streamBytesPerLayer) {
|
|
920
|
+
const char *error = NULL;
|
|
921
|
+
[_frameAccumulator setLength:_streamBytesPerLayer];
|
|
922
|
+
size_t got = avg_zstd_stream_read(&_zstdStream, (uint8_t *)_frameAccumulator.mutableBytes + held,
|
|
923
|
+
_streamBytesPerLayer - held, &error);
|
|
924
|
+
[_frameAccumulator setLength:held + got];
|
|
925
|
+
if (error) {
|
|
926
|
+
[self failStream:error];
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
// Short of a frame while input is on its way: the next display tick carries on
|
|
930
|
+
if (_frameAccumulator.length < _streamBytesPerLayer && !_zstdStream.inputPending) {
|
|
931
|
+
AVG_VLOG(@"ZSTD stream ended at decoderPos=%ld, accumLen=%lu",
|
|
932
|
+
(long)_decoderPosition, (unsigned long)_frameAccumulator.length);
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
// Process complete frame
|
|
937
|
+
if (_frameAccumulator.length >= _streamBytesPerLayer) {
|
|
938
|
+
NSInteger virtualPos = (_decoderLoopIteration * self.totalFrames + _decoderPosition) % vPosModulus;
|
|
939
|
+
NSInteger gpuSlot = _decoderPosition % _streamingRingBufferSize;
|
|
940
|
+
|
|
941
|
+
// Check if we can upload to this GPU slot
|
|
942
|
+
NSInteger slotCurrentVPos = [_ringSlots[gpuSlot] integerValue];
|
|
943
|
+
|
|
944
|
+
// Calculate if slot is "old" (behind playback) using relative distance
|
|
945
|
+
// A slot is old if it's behind the current playback position
|
|
946
|
+
// We also allow overwriting if the slot is from a different loop iteration
|
|
947
|
+
// (i.e., slot has vPos from loop 0 but we're decoding for loop 1)
|
|
948
|
+
NSInteger slotAge = (currentVirtualPos - slotCurrentVPos + vPosModulus) % vPosModulus;
|
|
949
|
+
BOOL slotIsOld = slotAge > 0 && slotAge < vPosModulus / 2;
|
|
950
|
+
|
|
951
|
+
// Also allow overwriting if the slot's frame is from a previous loop
|
|
952
|
+
// This handles the wrap-around case where decoder is ahead of playback
|
|
953
|
+
NSInteger slotActualFrame = slotCurrentVPos % self.totalFrames;
|
|
954
|
+
NSInteger decoderActualFrame = _decoderPosition;
|
|
955
|
+
BOOL slotFromPreviousLoop = (slotCurrentVPos != -1) &&
|
|
956
|
+
(slotActualFrame == decoderActualFrame) &&
|
|
957
|
+
(slotCurrentVPos != virtualPos);
|
|
958
|
+
|
|
959
|
+
BOOL canOverwrite = (slotCurrentVPos == -1) || (slotCurrentVPos == virtualPos) || slotIsOld || slotFromPreviousLoop;
|
|
960
|
+
|
|
961
|
+
// In RAM mode, ALWAYS cache the frame first (CPU cache is primary storage)
|
|
962
|
+
// In VRAM mode, only cache if we can't upload to GPU
|
|
963
|
+
if (!_useVramStrategy) {
|
|
964
|
+
// RAM mode: Always cache to CPU
|
|
965
|
+
NSData *frameData = [NSData dataWithBytes:_frameAccumulator.bytes length:_streamBytesPerLayer];
|
|
966
|
+
_frameCache[@(virtualPos)] = frameData;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
if (canOverwrite && slotCurrentVPos != virtualPos) {
|
|
970
|
+
// Upload frame to GPU
|
|
971
|
+
static int uploadLogCount = 0;
|
|
972
|
+
if (uploadLogCount++ < 30) {
|
|
973
|
+
AVG_VLOG(@"Uploading frame %ld to GPU slot %ld (vPos=%ld, loop=%ld)",
|
|
974
|
+
(long)_decoderPosition, (long)gpuSlot, (long)virtualPos, (long)_decoderLoopIteration);
|
|
975
|
+
}
|
|
976
|
+
[self uploadFrameToGPU:_frameAccumulator.bytes slot:gpuSlot];
|
|
977
|
+
_ringSlots[gpuSlot] = @(virtualPos);
|
|
978
|
+
} else if (!canOverwrite && _useVramStrategy) {
|
|
979
|
+
// VRAM mode: Can't upload, cache temporarily
|
|
980
|
+
static int blockLogCount = 0;
|
|
981
|
+
if (blockLogCount++ < 10) {
|
|
982
|
+
NSLog(@"AlphaVideoGpuView: Can't upload frame %ld to slot %ld - caching (slotVPos=%ld, virtualPos=%ld, slotAge=%ld)",
|
|
983
|
+
(long)_decoderPosition, (long)gpuSlot, (long)slotCurrentVPos, (long)virtualPos, (long)slotAge);
|
|
984
|
+
}
|
|
985
|
+
NSData *frameData = [NSData dataWithBytes:_frameAccumulator.bytes length:_streamBytesPerLayer];
|
|
986
|
+
_frameCache[@(virtualPos)] = frameData;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
_decoderPosition++;
|
|
990
|
+
framesToDecode--;
|
|
991
|
+
|
|
992
|
+
// Remove processed frame data (in-place shift, no allocation)
|
|
993
|
+
if (_frameAccumulator.length > _streamBytesPerLayer) {
|
|
994
|
+
[_frameAccumulator replaceBytesInRange:NSMakeRange(0, _streamBytesPerLayer) withBytes:NULL length:0];
|
|
995
|
+
} else {
|
|
996
|
+
[_frameAccumulator setLength:0];
|
|
997
|
+
}
|
|
998
|
+
} else {
|
|
999
|
+
break; // No more data available
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
/**
|
|
1005
|
+
* Decompresses and drops the bytes a seek still has to get past, until the deadline
|
|
1006
|
+
* (CACurrentMediaTime()); YES once none are left.
|
|
1007
|
+
*/
|
|
1008
|
+
- (BOOL)skipPendingBytesUntil:(CFTimeInterval)deadline
|
|
1009
|
+
{
|
|
1010
|
+
if (_pendingSkipBytes == 0) return YES;
|
|
1011
|
+
uint64_t held = MIN((uint64_t)_frameAccumulator.length, _pendingSkipBytes);
|
|
1012
|
+
[_frameAccumulator replaceBytesInRange:NSMakeRange(0, (NSUInteger)held) withBytes:NULL length:0];
|
|
1013
|
+
_pendingSkipBytes -= held;
|
|
1014
|
+
|
|
1015
|
+
NSMutableData *scratch = [NSMutableData dataWithLength:ZSTD_DStreamOutSize()];
|
|
1016
|
+
while (_pendingSkipBytes > 0) {
|
|
1017
|
+
const char *error = NULL;
|
|
1018
|
+
size_t want = (size_t)MIN((uint64_t)scratch.length, _pendingSkipBytes);
|
|
1019
|
+
size_t got = avg_zstd_stream_read(&_zstdStream, scratch.mutableBytes, want, &error);
|
|
1020
|
+
if (error) {
|
|
1021
|
+
[self failStream:error];
|
|
1022
|
+
return NO;
|
|
1023
|
+
}
|
|
1024
|
+
if (got == 0) {
|
|
1025
|
+
if (_zstdStream.inputPending) return NO;
|
|
1026
|
+
_pendingSkipBytes = 0; // past the end of the data
|
|
1027
|
+
break;
|
|
1028
|
+
}
|
|
1029
|
+
_pendingSkipBytes -= got;
|
|
1030
|
+
if (_pendingSkipBytes > 0 && CACurrentMediaTime() >= deadline) return NO;
|
|
1031
|
+
}
|
|
1032
|
+
return YES;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
/**
|
|
1036
|
+
* Points the streaming decoder at target, keeping what is decoded when target is among the
|
|
1037
|
+
* buffered frames (from the shown one up to the decoder).
|
|
1038
|
+
*/
|
|
1039
|
+
- (void)moveStreamTo:(NSInteger)target
|
|
1040
|
+
{
|
|
1041
|
+
if (_isUncompressedStreaming) {
|
|
1042
|
+
if (!_useCompressedFormat && _decodedRgbaCache) {
|
|
1043
|
+
// Decode blocks in flight read and advance the position, so reset it between them
|
|
1044
|
+
dispatch_sync(_astcDecodeQueue, ^{
|
|
1045
|
+
@synchronized(self->_decodedRgbaCache) {
|
|
1046
|
+
[self->_decodedRgbaCache removeAllObjects];
|
|
1047
|
+
}
|
|
1048
|
+
self->_uncompressedDecoderPosition = target;
|
|
1049
|
+
self->_decoderLoopIteration = 0;
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
1052
|
+
} else if (_dstream) {
|
|
1053
|
+
BOOL samePass = _decoderLoopIteration % 2 == _playbackLoopIteration % 2;
|
|
1054
|
+
if (samePass && target >= self.currentFrame && target < _decoderPosition) return;
|
|
1055
|
+
// Carry on decoding when that is no further from the target than the nearest seek point
|
|
1056
|
+
// (the level start, without a seek table). The skip itself happens over the next display
|
|
1057
|
+
// ticks, in decodeFramesAhead.
|
|
1058
|
+
uint64_t bytesPerLayer = _streamBytesPerLayer;
|
|
1059
|
+
uint64_t targetBytes = (uint64_t)target * bytesPerLayer;
|
|
1060
|
+
AVGZstdSeekPoint point = avg_zstd_seek_point_before(&_seekTable, targetBytes);
|
|
1061
|
+
uint64_t decoded = (uint64_t)_decoderPosition * bytesPerLayer + _frameAccumulator.length - _pendingSkipBytes;
|
|
1062
|
+
if (samePass && target >= _decoderPosition && avg_zstd_carry_on(decoded, point)) {
|
|
1063
|
+
_pendingSkipBytes += (uint64_t)(target - _decoderPosition) * bytesPerLayer;
|
|
1064
|
+
} else {
|
|
1065
|
+
avg_zstd_stream_restart(&_zstdStream, point.compressedOffset);
|
|
1066
|
+
[_frameAccumulator setLength:0];
|
|
1067
|
+
_pendingSkipBytes = targetBytes - point.decompressedOffset;
|
|
1068
|
+
}
|
|
1069
|
+
_decoderPosition = target;
|
|
1070
|
+
_decoderLoopIteration = 0;
|
|
1071
|
+
[_frameCache removeAllObjects];
|
|
1072
|
+
}
|
|
1073
|
+
_playbackLoopIteration = 0;
|
|
1074
|
+
for (NSUInteger i = 0; i < _ringSlots.count; i++) {
|
|
1075
|
+
_ringSlots[i] = @(-1);
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
/** Decodes towards the shown frame; YES once it can be drawn. */
|
|
1080
|
+
- (BOOL)prepareShownFrame
|
|
1081
|
+
{
|
|
1082
|
+
if (!_streamingRingBufferMode) return YES;
|
|
1083
|
+
NSInteger virtualPos = (_playbackLoopIteration * self.totalFrames + self.currentFrame) % (self.totalFrames * 2);
|
|
1084
|
+
NSInteger gpuSlot = self.currentFrame % _streamingRingBufferSize;
|
|
1085
|
+
if ([_ringSlots[gpuSlot] integerValue] == virtualPos) return YES;
|
|
1086
|
+
if (_isUncompressedStreaming) {
|
|
1087
|
+
// A device uploads straight from the mapping as it draws
|
|
1088
|
+
if (_useCompressedFormat || !_decodedRgbaCache) return YES;
|
|
1089
|
+
[self decodeUncompressedFramesAhead:virtualPos];
|
|
1090
|
+
@synchronized(_decodedRgbaCache) {
|
|
1091
|
+
return _decodedRgbaCache[@(virtualPos)] != nil;
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
[self decodeFramesAhead:virtualPos];
|
|
1095
|
+
return _frameCache[@(virtualPos)] != nil || [_ringSlots[gpuSlot] integerValue] == virtualPos;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
- (void)uploadFrameToGPU:(const void *)frameData slot:(NSInteger)slot
|
|
1099
|
+
{
|
|
1100
|
+
if (!_textureArray) return;
|
|
1101
|
+
|
|
1102
|
+
MTLRegion region = MTLRegionMake2D(0, 0, self.frameWidth, self.frameHeight);
|
|
1103
|
+
|
|
1104
|
+
if (_useCompressedFormat) {
|
|
1105
|
+
// Use async blit if staging buffers are available
|
|
1106
|
+
if (_stagingBuffers[0] && _blitCommandQueue) {
|
|
1107
|
+
// Copy to staging buffer
|
|
1108
|
+
id<MTLBuffer> staging = _stagingBuffers[_currentStagingBuffer];
|
|
1109
|
+
memcpy(staging.contents, frameData, _streamBytesPerLayer);
|
|
1110
|
+
|
|
1111
|
+
// Create blit command buffer
|
|
1112
|
+
id<MTLCommandBuffer> cmdBuf = [_blitCommandQueue commandBuffer];
|
|
1113
|
+
id<MTLBlitCommandEncoder> blit = [cmdBuf blitCommandEncoder];
|
|
1114
|
+
|
|
1115
|
+
// Async copy from buffer to texture
|
|
1116
|
+
[blit copyFromBuffer:staging
|
|
1117
|
+
sourceOffset:0
|
|
1118
|
+
sourceBytesPerRow:0
|
|
1119
|
+
sourceBytesPerImage:_streamBytesPerLayer
|
|
1120
|
+
sourceSize:MTLSizeMake(self.frameWidth, self.frameHeight, 1)
|
|
1121
|
+
toTexture:_textureArray
|
|
1122
|
+
destinationSlice:slot
|
|
1123
|
+
destinationLevel:0
|
|
1124
|
+
destinationOrigin:MTLOriginMake(0, 0, 0)];
|
|
1125
|
+
|
|
1126
|
+
[blit endEncoding];
|
|
1127
|
+
[cmdBuf commit]; // Async - returns immediately
|
|
1128
|
+
|
|
1129
|
+
// Swap staging buffer for next upload
|
|
1130
|
+
_currentStagingBuffer = 1 - _currentStagingBuffer;
|
|
1131
|
+
} else {
|
|
1132
|
+
// Fallback to synchronous replaceRegion
|
|
1133
|
+
[_textureArray replaceRegion:region
|
|
1134
|
+
mipmapLevel:0
|
|
1135
|
+
slice:slot
|
|
1136
|
+
withBytes:frameData
|
|
1137
|
+
bytesPerRow:0
|
|
1138
|
+
bytesPerImage:_streamBytesPerLayer];
|
|
1139
|
+
}
|
|
1140
|
+
} else {
|
|
1141
|
+
// Decode ASTC to RGBA for simulator
|
|
1142
|
+
uint32_t bw = _astcBlockWidth;
|
|
1143
|
+
uint32_t bh = _astcBlockHeight;
|
|
1144
|
+
uint32_t blocksX = ((uint32_t)self.frameWidth + bw - 1) / bw;
|
|
1145
|
+
uint32_t blocksY = ((uint32_t)self.frameHeight + bh - 1) / bh;
|
|
1146
|
+
std::vector<uint8_t> rgbaData(self.frameWidth * self.frameHeight * 4);
|
|
1147
|
+
|
|
1148
|
+
const uint8_t *astcData = (const uint8_t *)frameData;
|
|
1149
|
+
for (uint32_t by = 0; by < blocksY; by++) {
|
|
1150
|
+
for (uint32_t bx = 0; bx < blocksX; bx++) {
|
|
1151
|
+
const uint8_t *block = astcData + (by * blocksX + bx) * 16;
|
|
1152
|
+
uint8_t blockPixels[12 * 12 * 4]; // max ASTC block size
|
|
1153
|
+
basisu::astc::decompress(blockPixels, block, false, bw, bh);
|
|
1154
|
+
|
|
1155
|
+
for (uint32_t y = 0; y < bh && (by * bh + y) < self.frameHeight; y++) {
|
|
1156
|
+
for (uint32_t x = 0; x < bw && (bx * bw + x) < self.frameWidth; x++) {
|
|
1157
|
+
uint32_t dstX = bx * bw + x;
|
|
1158
|
+
uint32_t dstY = by * bh + y;
|
|
1159
|
+
uint8_t *dst = rgbaData.data() + (dstY * self.frameWidth + dstX) * 4;
|
|
1160
|
+
uint8_t *src = blockPixels + (y * bw + x) * 4;
|
|
1161
|
+
memcpy(dst, src, 4);
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
[_textureArray replaceRegion:region
|
|
1168
|
+
mipmapLevel:0
|
|
1169
|
+
slice:slot
|
|
1170
|
+
withBytes:rgbaData.data()
|
|
1171
|
+
bytesPerRow:self.frameWidth * 4
|
|
1172
|
+
bytesPerImage:self.frameWidth * self.frameHeight * 4];
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
- (void)ensureFrameInGPU:(NSInteger)virtualPos
|
|
1177
|
+
{
|
|
1178
|
+
if (!_streamingRingBufferMode) return;
|
|
1179
|
+
|
|
1180
|
+
NSInteger actualFrame = virtualPos % self.totalFrames;
|
|
1181
|
+
NSInteger gpuSlot = actualFrame % _streamingRingBufferSize;
|
|
1182
|
+
|
|
1183
|
+
// Check if already loaded
|
|
1184
|
+
NSInteger slotVPos = [_ringSlots[gpuSlot] integerValue];
|
|
1185
|
+
if (slotVPos == virtualPos) return;
|
|
1186
|
+
|
|
1187
|
+
// Check CPU cache
|
|
1188
|
+
NSData *frameData = _frameCache[@(virtualPos)];
|
|
1189
|
+
if (!frameData) {
|
|
1190
|
+
static int missCount = 0;
|
|
1191
|
+
if (missCount++ < 30) {
|
|
1192
|
+
AVG_VLOG(@"ensureFrameInGPU - cache miss for vPos=%ld, slot=%ld has vPos=%ld, cacheSize=%lu",
|
|
1193
|
+
(long)virtualPos, (long)gpuSlot, (long)slotVPos, (unsigned long)_frameCache.count);
|
|
1194
|
+
}
|
|
1195
|
+
return;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
// Upload to GPU
|
|
1199
|
+
static int ensureUploadCount = 0;
|
|
1200
|
+
if (ensureUploadCount++ < 30) {
|
|
1201
|
+
AVG_VLOG(@"ensureFrameInGPU - uploading vPos=%ld to slot=%ld from cache",
|
|
1202
|
+
(long)virtualPos, (long)gpuSlot);
|
|
1203
|
+
}
|
|
1204
|
+
[self uploadFrameToGPU:frameData.bytes slot:gpuSlot];
|
|
1205
|
+
_ringSlots[gpuSlot] = @(virtualPos);
|
|
1206
|
+
|
|
1207
|
+
// In VRAM mode, remove from CPU cache after upload
|
|
1208
|
+
if (_useVramStrategy) {
|
|
1209
|
+
[_frameCache removeObjectForKey:@(virtualPos)];
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
- (void)evictOldFrames:(NSInteger)currentVirtualPos
|
|
1214
|
+
{
|
|
1215
|
+
if (!_frameCache) return;
|
|
1216
|
+
|
|
1217
|
+
NSInteger vPosModulus = self.totalFrames * 2;
|
|
1218
|
+
NSMutableArray *toRemove = [NSMutableArray array];
|
|
1219
|
+
|
|
1220
|
+
if (_useVramStrategy) {
|
|
1221
|
+
// VRAM mode: Only evict frames that have been played
|
|
1222
|
+
for (NSNumber *virtualPos in _frameCache.allKeys) {
|
|
1223
|
+
NSInteger vPos = [virtualPos integerValue];
|
|
1224
|
+
NSInteger age = (currentVirtualPos - vPos + vPosModulus) % vPosModulus;
|
|
1225
|
+
// Frame is "old" if it's behind playback (age > 0) but not wrapped around (age < half modulus)
|
|
1226
|
+
if (age > 0 && age < vPosModulus / 2) {
|
|
1227
|
+
[toRemove addObject:virtualPos];
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
} else {
|
|
1231
|
+
// RAM mode: Use sliding window with relative distance
|
|
1232
|
+
// Keep the previous frame and the ones decoded ahead
|
|
1233
|
+
for (NSNumber *virtualPos in _frameCache.allKeys) {
|
|
1234
|
+
NSInteger vPos = [virtualPos integerValue];
|
|
1235
|
+
NSInteger distance = (vPos - currentVirtualPos + vPosModulus) % vPosModulus;
|
|
1236
|
+
// distance > vPosModulus - 2 means frame is 1 behind (wrapped)
|
|
1237
|
+
BOOL isBehind = distance > (NSInteger)(vPosModulus - 2);
|
|
1238
|
+
BOOL isAhead = distance <= self.streamingPrefetchAhead;
|
|
1239
|
+
if (!isBehind && !isAhead) {
|
|
1240
|
+
[toRemove addObject:virtualPos];
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
for (NSNumber *virtualPos in toRemove) {
|
|
1246
|
+
[_frameCache removeObjectForKey:virtualPos];
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
#pragma mark - Uncompressed Streaming (mmap)
|
|
1251
|
+
|
|
1252
|
+
// With the stream prop, plays an uncompressed KTX2 texture array by mmap streaming, reading only
|
|
1253
|
+
// the header and metadata so the file is never loaded into memory.
|
|
1254
|
+
- (BOOL)checkAndPlayUncompressedStreaming:(NSString *)filePath
|
|
1255
|
+
{
|
|
1256
|
+
if (!self.stream) return NO;
|
|
1257
|
+
AVG_VLOG(@"checkAndPlayUncompressedStreaming: %@", filePath);
|
|
1258
|
+
|
|
1259
|
+
// Open file
|
|
1260
|
+
int fd = open([filePath UTF8String], O_RDONLY);
|
|
1261
|
+
if (fd < 0) {
|
|
1262
|
+
NSLog(@"AlphaVideoGpuView: Failed to open file for mmap");
|
|
1263
|
+
return NO;
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
// Get file size
|
|
1267
|
+
struct stat st;
|
|
1268
|
+
if (fstat(fd, &st) < 0) {
|
|
1269
|
+
close(fd);
|
|
1270
|
+
return NO;
|
|
1271
|
+
}
|
|
1272
|
+
size_t fileSize = st.st_size;
|
|
1273
|
+
|
|
1274
|
+
// Read header (first 256 bytes)
|
|
1275
|
+
uint8_t headerBytes[256];
|
|
1276
|
+
if (read(fd, headerBytes, 256) < 100) {
|
|
1277
|
+
close(fd);
|
|
1278
|
+
return NO;
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
// Check KTX2 magic
|
|
1282
|
+
uint8_t ktx2Magic[] = {0xAB, 0x4B, 0x54, 0x58, 0x20, 0x32, 0x30, 0xBB, 0x0D, 0x0A, 0x1A, 0x0A};
|
|
1283
|
+
if (memcmp(headerBytes, ktx2Magic, 12) != 0) {
|
|
1284
|
+
close(fd);
|
|
1285
|
+
return NO;
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
// Check supercompressionScheme at offset 44
|
|
1289
|
+
uint32_t supercompression = *(uint32_t *)(headerBytes + 44);
|
|
1290
|
+
if (supercompression != 0) {
|
|
1291
|
+
close(fd);
|
|
1292
|
+
return NO; // Not uncompressed
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
// Read the key/value data for KTXanimData
|
|
1296
|
+
uint32_t kvdOffset = *(uint32_t *)(headerBytes + 56);
|
|
1297
|
+
uint32_t kvdLength = *(uint32_t *)(headerBytes + 60);
|
|
1298
|
+
NSMutableData *kvdBytes = [NSMutableData dataWithLength:MIN(kvdLength, 4096u)];
|
|
1299
|
+
if (kvdBytes.length > 0) {
|
|
1300
|
+
ssize_t kvdRead = pread(fd, kvdBytes.mutableBytes, kvdBytes.length, kvdOffset);
|
|
1301
|
+
kvdBytes.length = kvdRead > 0 ? (NSUInteger)kvdRead : 0;
|
|
1302
|
+
}
|
|
1303
|
+
NSDictionary *metadata = AVGKTX2Metadata(headerBytes, (const uint8_t *)kvdBytes.bytes, kvdBytes.length);
|
|
1304
|
+
// Only ASTC texture arrays stream
|
|
1305
|
+
if (!AVGIsASTCArray(metadata)) {
|
|
1306
|
+
close(fd);
|
|
1307
|
+
return NO;
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
AVG_VLOG(@"Detected uncompressed streaming KTX2 - using mmap");
|
|
1311
|
+
|
|
1312
|
+
// Memory map the file
|
|
1313
|
+
void *mmapData = mmap(NULL, fileSize, PROT_READ, MAP_PRIVATE, fd, 0);
|
|
1314
|
+
if (mmapData == MAP_FAILED) {
|
|
1315
|
+
NSLog(@"AlphaVideoGpuView: mmap failed");
|
|
1316
|
+
close(fd);
|
|
1317
|
+
return NO;
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
// Store mmap state
|
|
1321
|
+
_mmapFd = fd;
|
|
1322
|
+
_mmapData = mmapData;
|
|
1323
|
+
_mmapSize = fileSize;
|
|
1324
|
+
return [self startUncompressedStreamingWithMetadata:metadata];
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
/** checkAndPlayUncompressedStreaming: for a whole file read from a reader, held in memory. */
|
|
1328
|
+
- (BOOL)checkAndPlayUncompressedStreamingFromData:(NSData *)fileData metadata:(NSDictionary *)metadata
|
|
1329
|
+
{
|
|
1330
|
+
if (!self.stream || fileData.length < 104) return NO;
|
|
1331
|
+
if (avg_read_u32le((const uint8_t *)fileData.bytes + 44) != 0) return NO;
|
|
1332
|
+
if (!AVGIsASTCArray(metadata)) return NO;
|
|
1333
|
+
_heldFileData = fileData;
|
|
1334
|
+
_mmapData = (void *)fileData.bytes;
|
|
1335
|
+
_mmapSize = fileData.length;
|
|
1336
|
+
_mmapFd = -1;
|
|
1337
|
+
return [self startUncompressedStreamingWithMetadata:metadata];
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
/** Plays the uncompressed texture array at _mmapData: a mapping, or a file held in memory. */
|
|
1341
|
+
- (BOOL)startUncompressedStreamingWithMetadata:(NSDictionary *)metadata
|
|
1342
|
+
{
|
|
1343
|
+
_isUncompressedStreaming = YES;
|
|
1344
|
+
|
|
1345
|
+
// Parse KTX2 header from mmap
|
|
1346
|
+
const uint8_t *data = (const uint8_t *)_mmapData;
|
|
1347
|
+
uint32_t vkFormat = *(uint32_t *)(data + 12);
|
|
1348
|
+
uint32_t width = *(uint32_t *)(data + 20);
|
|
1349
|
+
uint32_t height = *(uint32_t *)(data + 24);
|
|
1350
|
+
uint32_t layerCount = *(uint32_t *)(data + 32);
|
|
1351
|
+
if (layerCount == 0) layerCount = 1;
|
|
1352
|
+
uint64_t level0Offset = *(uint64_t *)(data + 80);
|
|
1353
|
+
|
|
1354
|
+
// Configure ASTC format from vkFormat
|
|
1355
|
+
[self configureASTCFormatFromVkFormat:vkFormat];
|
|
1356
|
+
uint32_t bytesPerLayer = [self astcBytesPerLayerForWidth:width height:height];
|
|
1357
|
+
|
|
1358
|
+
AVG_VLOG(@"Uncompressed streaming: %ux%u, %u layers, %u bytes/layer (ASTC %ux%u), offset=%llu",
|
|
1359
|
+
width, height, layerCount, bytesPerLayer, _astcBlockWidth, _astcBlockHeight, level0Offset);
|
|
1360
|
+
|
|
1361
|
+
// Frames are read from the mapping as they're shown, nothing ahead (but on the simulator,
|
|
1362
|
+
// which decodes them to RGBA in memory), so a small ring does for either memory strategy
|
|
1363
|
+
BOOL useVramStrategy = ![self.memoryStrategy isEqualToString:@"ram"];
|
|
1364
|
+
NSInteger gpuCacheSize = MIN(kRamRingSize, (NSInteger)layerCount);
|
|
1365
|
+
|
|
1366
|
+
// Setup on main thread
|
|
1367
|
+
dispatch_sync(dispatch_get_main_queue(), ^{
|
|
1368
|
+
self.totalFrames = layerCount;
|
|
1369
|
+
self.frameWidth = width;
|
|
1370
|
+
self.frameHeight = height;
|
|
1371
|
+
self.frameRate = [metadata[@"frameRate"] doubleValue];
|
|
1372
|
+
self.frameInterval = 1.0 / self.frameRate;
|
|
1373
|
+
|
|
1374
|
+
self->_textureArrayLayerCount = layerCount;
|
|
1375
|
+
self->_streamingRingBufferMode = YES;
|
|
1376
|
+
self->_streamingRingBufferSize = gpuCacheSize;
|
|
1377
|
+
self->_useVramStrategy = useVramStrategy;
|
|
1378
|
+
self->_streamBytesPerLayer = bytesPerLayer;
|
|
1379
|
+
self->_streamLevel0Offset = level0Offset;
|
|
1380
|
+
|
|
1381
|
+
// Initialize ring buffer tracking
|
|
1382
|
+
self->_ringSlots = [NSMutableArray arrayWithCapacity:gpuCacheSize];
|
|
1383
|
+
for (NSInteger i = 0; i < gpuCacheSize; i++) {
|
|
1384
|
+
[self->_ringSlots addObject:@(-1)];
|
|
1385
|
+
}
|
|
1386
|
+
self->_playbackLoopIteration = 0;
|
|
1387
|
+
|
|
1388
|
+
// Initialize decoded RGBA cache for simulator ASTC decompression
|
|
1389
|
+
if (!self->_useCompressedFormat) {
|
|
1390
|
+
self->_decodedRgbaCache = [NSMutableDictionary dictionary];
|
|
1391
|
+
self->_uncompressedDecoderPosition = 0;
|
|
1392
|
+
self->_decoderLoopIteration = 0;
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
// Create texture array with ring buffer size
|
|
1396
|
+
MTLTextureDescriptor *descriptor = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:self->_metalPixelFormat
|
|
1397
|
+
width:width
|
|
1398
|
+
height:height
|
|
1399
|
+
mipmapped:NO];
|
|
1400
|
+
descriptor.textureType = MTLTextureType2DArray;
|
|
1401
|
+
descriptor.arrayLength = gpuCacheSize;
|
|
1402
|
+
descriptor.usage = MTLTextureUsageShaderRead;
|
|
1403
|
+
|
|
1404
|
+
self->_textureArray = [self.device newTextureWithDescriptor:descriptor];
|
|
1405
|
+
|
|
1406
|
+
if (!self->_textureArray) {
|
|
1407
|
+
[self notifyError:@"Failed to create texture array for uncompressed streaming"];
|
|
1408
|
+
return;
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
AVG_VLOG(@"Created uncompressed streaming texture array: %ux%u, %ld GPU slots",
|
|
1412
|
+
width, height, (long)gpuCacheSize);
|
|
1413
|
+
|
|
1414
|
+
self->_isSourceLoaded = YES;
|
|
1415
|
+
self->_isSourceLoading = NO;
|
|
1416
|
+
[self notifyLoad];
|
|
1417
|
+
[self notifyStateChange:@"ready"];
|
|
1418
|
+
|
|
1419
|
+
// Pre-decode initial frames on background thread to avoid freeze
|
|
1420
|
+
if (!self->_useCompressedFormat) {
|
|
1421
|
+
// Simulator: decode ASTC to RGBA on background, then upload to GPU
|
|
1422
|
+
dispatch_async(self->_astcDecodeQueue, ^{
|
|
1423
|
+
NSInteger framesToPreload = MIN(gpuCacheSize, (NSInteger)layerCount);
|
|
1424
|
+
AVG_VLOG(@"Pre-decoding %ld initial frames for uncompressed streaming (simulator)", (long)framesToPreload);
|
|
1425
|
+
|
|
1426
|
+
for (NSInteger i = 0; i < framesToPreload && self->_mmapData; i++) {
|
|
1427
|
+
// Decode ASTC to RGBA
|
|
1428
|
+
const uint8_t *data = (const uint8_t *)self->_mmapData;
|
|
1429
|
+
uint64_t frameOffset = self->_streamLevel0Offset + (uint64_t)i * self->_streamBytesPerLayer;
|
|
1430
|
+
const uint8_t *frameData = data + frameOffset;
|
|
1431
|
+
|
|
1432
|
+
uint32_t bw = self->_astcBlockWidth;
|
|
1433
|
+
uint32_t bh = self->_astcBlockHeight;
|
|
1434
|
+
uint32_t blocksX = (width + bw - 1) / bw;
|
|
1435
|
+
uint32_t blocksY = (height + bh - 1) / bh;
|
|
1436
|
+
NSMutableData *rgbaData = [NSMutableData dataWithLength:width * height * 4];
|
|
1437
|
+
uint8_t *rgba = (uint8_t *)rgbaData.mutableBytes;
|
|
1438
|
+
|
|
1439
|
+
for (uint32_t by = 0; by < blocksY; by++) {
|
|
1440
|
+
for (uint32_t bx = 0; bx < blocksX; bx++) {
|
|
1441
|
+
const uint8_t *block = frameData + (by * blocksX + bx) * 16;
|
|
1442
|
+
uint8_t blockPixels[12 * 12 * 4]; // max ASTC block size
|
|
1443
|
+
basisu::astc::decompress(blockPixels, block, false, bw, bh);
|
|
1444
|
+
|
|
1445
|
+
for (uint32_t y = 0; y < bh && (by * bh + y) < height; y++) {
|
|
1446
|
+
for (uint32_t x = 0; x < bw && (bx * bw + x) < width; x++) {
|
|
1447
|
+
uint32_t dstX = bx * bw + x;
|
|
1448
|
+
uint32_t dstY = by * bh + y;
|
|
1449
|
+
uint8_t *dst = rgba + (dstY * width + dstX) * 4;
|
|
1450
|
+
uint8_t *src = blockPixels + (y * bw + x) * 4;
|
|
1451
|
+
memcpy(dst, src, 4);
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
// Cache decoded frame
|
|
1458
|
+
@synchronized(self->_decodedRgbaCache) {
|
|
1459
|
+
self->_decodedRgbaCache[@(i)] = rgbaData;
|
|
1460
|
+
}
|
|
1461
|
+
self->_uncompressedDecoderPosition = i + 1;
|
|
1462
|
+
}
|
|
1463
|
+
if (self->_uncompressedDecoderPosition >= self.totalFrames) {
|
|
1464
|
+
self->_uncompressedDecoderPosition = 0;
|
|
1465
|
+
self->_decoderLoopIteration++;
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
// Upload to GPU on main thread (fast - just memcpy)
|
|
1469
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
1470
|
+
for (NSInteger i = 0; i < framesToPreload; i++) {
|
|
1471
|
+
NSInteger slot = i % gpuCacheSize;
|
|
1472
|
+
// A seek since the decode has emptied the cache
|
|
1473
|
+
if ([self uploadDecodedFrameToGPU:i slot:slot]) {
|
|
1474
|
+
self->_ringSlots[slot] = @(i);
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
AVG_VLOG(@"Pre-decode complete, starting playback");
|
|
1479
|
+
|
|
1480
|
+
if (self.autoPlay && !self.playbackPaused) {
|
|
1481
|
+
[self play];
|
|
1482
|
+
}
|
|
1483
|
+
});
|
|
1484
|
+
});
|
|
1485
|
+
} else {
|
|
1486
|
+
// Real device: direct upload (fast, no ASTC decode needed)
|
|
1487
|
+
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
|
|
1488
|
+
NSInteger framesToPreload = MIN(gpuCacheSize, (NSInteger)layerCount);
|
|
1489
|
+
AVG_VLOG(@"Pre-uploading %ld initial frames for uncompressed streaming (device)", (long)framesToPreload);
|
|
1490
|
+
|
|
1491
|
+
for (NSInteger i = 0; i < framesToPreload; i++) {
|
|
1492
|
+
NSInteger slot = i % gpuCacheSize;
|
|
1493
|
+
[self uploadUncompressedFrame:i toSlot:slot];
|
|
1494
|
+
self->_ringSlots[slot] = @(i);
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
AVG_VLOG(@"Pre-upload complete, starting playback");
|
|
1498
|
+
|
|
1499
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
1500
|
+
if (self.autoPlay && !self.playbackPaused) {
|
|
1501
|
+
[self play];
|
|
1502
|
+
}
|
|
1503
|
+
});
|
|
1504
|
+
});
|
|
1505
|
+
}
|
|
1506
|
+
});
|
|
1507
|
+
|
|
1508
|
+
return YES;
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
- (void)uploadUncompressedFrame:(NSInteger)frameIndex toSlot:(NSInteger)slot
|
|
1512
|
+
{
|
|
1513
|
+
if (!_mmapData || !_textureArray) return;
|
|
1514
|
+
|
|
1515
|
+
const uint8_t *data = (const uint8_t *)_mmapData;
|
|
1516
|
+
uint64_t frameOffset = _streamLevel0Offset + (uint64_t)frameIndex * _streamBytesPerLayer;
|
|
1517
|
+
const uint8_t *frameData = data + frameOffset;
|
|
1518
|
+
|
|
1519
|
+
MTLRegion region = MTLRegionMake2D(0, 0, self.frameWidth, self.frameHeight);
|
|
1520
|
+
|
|
1521
|
+
if (_useCompressedFormat) {
|
|
1522
|
+
[_textureArray replaceRegion:region
|
|
1523
|
+
mipmapLevel:0
|
|
1524
|
+
slice:slot
|
|
1525
|
+
withBytes:frameData
|
|
1526
|
+
bytesPerRow:0
|
|
1527
|
+
bytesPerImage:_streamBytesPerLayer];
|
|
1528
|
+
} else {
|
|
1529
|
+
// Decode ASTC to RGBA for simulator
|
|
1530
|
+
uint32_t bw = _astcBlockWidth;
|
|
1531
|
+
uint32_t bh = _astcBlockHeight;
|
|
1532
|
+
uint32_t blocksX = ((uint32_t)self.frameWidth + bw - 1) / bw;
|
|
1533
|
+
uint32_t blocksY = ((uint32_t)self.frameHeight + bh - 1) / bh;
|
|
1534
|
+
std::vector<uint8_t> rgbaData(self.frameWidth * self.frameHeight * 4);
|
|
1535
|
+
|
|
1536
|
+
for (uint32_t by = 0; by < blocksY; by++) {
|
|
1537
|
+
for (uint32_t bx = 0; bx < blocksX; bx++) {
|
|
1538
|
+
const uint8_t *block = frameData + (by * blocksX + bx) * 16;
|
|
1539
|
+
uint8_t blockPixels[12 * 12 * 4]; // max ASTC block size
|
|
1540
|
+
basisu::astc::decompress(blockPixels, block, false, bw, bh);
|
|
1541
|
+
|
|
1542
|
+
for (uint32_t y = 0; y < bh && (by * bh + y) < self.frameHeight; y++) {
|
|
1543
|
+
for (uint32_t x = 0; x < bw && (bx * bw + x) < self.frameWidth; x++) {
|
|
1544
|
+
uint32_t dstX = bx * bw + x;
|
|
1545
|
+
uint32_t dstY = by * bh + y;
|
|
1546
|
+
uint8_t *dst = rgbaData.data() + (dstY * self.frameWidth + dstX) * 4;
|
|
1547
|
+
uint8_t *src = blockPixels + (y * bw + x) * 4;
|
|
1548
|
+
memcpy(dst, src, 4);
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
[_textureArray replaceRegion:region
|
|
1555
|
+
mipmapLevel:0
|
|
1556
|
+
slice:slot
|
|
1557
|
+
withBytes:rgbaData.data()
|
|
1558
|
+
bytesPerRow:self.frameWidth * 4
|
|
1559
|
+
bytesPerImage:self.frameWidth * self.frameHeight * 4];
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
// Decode ASTC frames ahead on background thread for uncompressed streaming (simulator only)
|
|
1564
|
+
- (void)decodeUncompressedFramesAhead:(NSInteger)currentVirtualPos
|
|
1565
|
+
{
|
|
1566
|
+
if (!_mmapData || !_isUncompressedStreaming || _useCompressedFormat) return;
|
|
1567
|
+
if (_uncompressedDecodeQueued) return;
|
|
1568
|
+
_uncompressedDecodeQueued = YES;
|
|
1569
|
+
|
|
1570
|
+
// Only needed on simulator where we decode ASTC to RGBA
|
|
1571
|
+
NSInteger vPosModulus = self.totalFrames * 2;
|
|
1572
|
+
// framesAhead counts the shown frame
|
|
1573
|
+
NSInteger aheadLimit = self.streamingPrefetchAhead + 1;
|
|
1574
|
+
|
|
1575
|
+
// The position belongs to the decode queue, so the batch is sized there
|
|
1576
|
+
dispatch_async(_astcDecodeQueue, ^{
|
|
1577
|
+
NSInteger currentDecodeVPos = (self->_decoderLoopIteration * self.totalFrames + self->_uncompressedDecoderPosition) % vPosModulus;
|
|
1578
|
+
NSInteger framesAhead = (currentDecodeVPos - currentVirtualPos + vPosModulus) % vPosModulus;
|
|
1579
|
+
NSInteger framesToDecode = aheadLimit - framesAhead;
|
|
1580
|
+
for (NSInteger i = 0; i < framesToDecode && self->_mmapData; i++) {
|
|
1581
|
+
NSInteger frameIndex = self->_uncompressedDecoderPosition;
|
|
1582
|
+
NSInteger virtualPos = (self->_decoderLoopIteration * self.totalFrames + frameIndex) % vPosModulus;
|
|
1583
|
+
|
|
1584
|
+
// Skip if already decoded
|
|
1585
|
+
if (self->_decodedRgbaCache[@(virtualPos)]) {
|
|
1586
|
+
self->_uncompressedDecoderPosition++;
|
|
1587
|
+
if (self->_uncompressedDecoderPosition >= self.totalFrames) {
|
|
1588
|
+
self->_uncompressedDecoderPosition = 0;
|
|
1589
|
+
self->_decoderLoopIteration++;
|
|
1590
|
+
}
|
|
1591
|
+
continue;
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
// Decode ASTC to RGBA
|
|
1595
|
+
const uint8_t *data = (const uint8_t *)self->_mmapData;
|
|
1596
|
+
uint64_t frameOffset = self->_streamLevel0Offset + (uint64_t)frameIndex * self->_streamBytesPerLayer;
|
|
1597
|
+
const uint8_t *frameData = data + frameOffset;
|
|
1598
|
+
|
|
1599
|
+
uint32_t bw = self->_astcBlockWidth;
|
|
1600
|
+
uint32_t bh = self->_astcBlockHeight;
|
|
1601
|
+
uint32_t blocksX = ((uint32_t)self.frameWidth + bw - 1) / bw;
|
|
1602
|
+
uint32_t blocksY = ((uint32_t)self.frameHeight + bh - 1) / bh;
|
|
1603
|
+
NSMutableData *rgbaData = [NSMutableData dataWithLength:self.frameWidth * self.frameHeight * 4];
|
|
1604
|
+
uint8_t *rgba = (uint8_t *)rgbaData.mutableBytes;
|
|
1605
|
+
|
|
1606
|
+
for (uint32_t by = 0; by < blocksY; by++) {
|
|
1607
|
+
for (uint32_t bx = 0; bx < blocksX; bx++) {
|
|
1608
|
+
const uint8_t *block = frameData + (by * blocksX + bx) * 16;
|
|
1609
|
+
uint8_t blockPixels[12 * 12 * 4]; // max ASTC block size
|
|
1610
|
+
basisu::astc::decompress(blockPixels, block, false, bw, bh);
|
|
1611
|
+
|
|
1612
|
+
for (uint32_t y = 0; y < bh && (by * bh + y) < self.frameHeight; y++) {
|
|
1613
|
+
for (uint32_t x = 0; x < bw && (bx * bw + x) < self.frameWidth; x++) {
|
|
1614
|
+
uint32_t dstX = bx * bw + x;
|
|
1615
|
+
uint32_t dstY = by * bh + y;
|
|
1616
|
+
uint8_t *dst = rgba + (dstY * self.frameWidth + dstX) * 4;
|
|
1617
|
+
uint8_t *src = blockPixels + (y * bw + x) * 4;
|
|
1618
|
+
memcpy(dst, src, 4);
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
// Cache the decoded frame
|
|
1625
|
+
@synchronized(self->_decodedRgbaCache) {
|
|
1626
|
+
self->_decodedRgbaCache[@(virtualPos)] = rgbaData;
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
self->_uncompressedDecoderPosition++;
|
|
1630
|
+
if (self->_uncompressedDecoderPosition >= self.totalFrames) {
|
|
1631
|
+
self->_uncompressedDecoderPosition = 0;
|
|
1632
|
+
self->_decoderLoopIteration++;
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
1636
|
+
self->_uncompressedDecodeQueued = NO;
|
|
1637
|
+
});
|
|
1638
|
+
});
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
// Upload pre-decoded RGBA frame to GPU (fast, main thread safe); NO when it isn't decoded
|
|
1642
|
+
- (BOOL)uploadDecodedFrameToGPU:(NSInteger)virtualPos slot:(NSInteger)slot
|
|
1643
|
+
{
|
|
1644
|
+
NSData *rgbaData;
|
|
1645
|
+
@synchronized(_decodedRgbaCache) {
|
|
1646
|
+
rgbaData = _decodedRgbaCache[@(virtualPos)];
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
if (!rgbaData || !_textureArray) return NO;
|
|
1650
|
+
|
|
1651
|
+
MTLRegion region = MTLRegionMake2D(0, 0, self.frameWidth, self.frameHeight);
|
|
1652
|
+
[_textureArray replaceRegion:region
|
|
1653
|
+
mipmapLevel:0
|
|
1654
|
+
slice:slot
|
|
1655
|
+
withBytes:rgbaData.bytes
|
|
1656
|
+
bytesPerRow:self.frameWidth * 4
|
|
1657
|
+
bytesPerImage:self.frameWidth * self.frameHeight * 4];
|
|
1658
|
+
return YES;
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
// Evict old decoded frames from cache
|
|
1662
|
+
- (void)evictOldDecodedFrames:(NSInteger)currentVirtualPos
|
|
1663
|
+
{
|
|
1664
|
+
if (!_decodedRgbaCache) return;
|
|
1665
|
+
|
|
1666
|
+
NSInteger vPosModulus = self.totalFrames * 2;
|
|
1667
|
+
NSMutableArray *toRemove = [NSMutableArray array];
|
|
1668
|
+
|
|
1669
|
+
@synchronized(_decodedRgbaCache) {
|
|
1670
|
+
for (NSNumber *virtualPos in _decodedRgbaCache.allKeys) {
|
|
1671
|
+
NSInteger vPos = [virtualPos integerValue];
|
|
1672
|
+
NSInteger distance = (vPos - currentVirtualPos + vPosModulus) % vPosModulus;
|
|
1673
|
+
BOOL isBehind = distance > (NSInteger)(vPosModulus - 2);
|
|
1674
|
+
BOOL isAhead = distance <= self.streamingPrefetchAhead;
|
|
1675
|
+
if (!isBehind && !isAhead) {
|
|
1676
|
+
[toRemove addObject:virtualPos];
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
for (NSNumber *virtualPos in toRemove) {
|
|
1681
|
+
[_decodedRgbaCache removeObjectForKey:virtualPos];
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
/**
|
|
1687
|
+
* With a reader, fileData holds the file from its start, through at least the header; otherwise
|
|
1688
|
+
* it is nil and the file is at sourcePath.
|
|
1689
|
+
*/
|
|
1690
|
+
- (void)applyMetadataAndLoadTextures:(NSDictionary *)metadata
|
|
1691
|
+
sourcePath:(NSString *)sourcePath
|
|
1692
|
+
fileData:(NSData *)fileData
|
|
1693
|
+
reader:(AVGSourceReader *)reader
|
|
1694
|
+
{
|
|
1695
|
+
if (!AVGIsASTCArray(metadata)) {
|
|
1696
|
+
[self notifyError:[NSString stringWithFormat:@"Only ASTC texture arrays play (this file has vkFormat %@ and %@ layers); encode it with texenc",
|
|
1697
|
+
metadata[@"vkFormat"], metadata[@"frameCount"]]];
|
|
1698
|
+
return;
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
self.totalFrames = [metadata[@"frameCount"] integerValue];
|
|
1702
|
+
self.frameWidth = [metadata[@"frameWidth"] integerValue];
|
|
1703
|
+
self.frameHeight = [metadata[@"frameHeight"] integerValue];
|
|
1704
|
+
self.frameRate = [metadata[@"frameRate"] doubleValue];
|
|
1705
|
+
self.frameInterval = 1.0 / self.frameRate;
|
|
1706
|
+
_textureArrayLayerCount = self.totalFrames;
|
|
1707
|
+
|
|
1708
|
+
AVG_VLOG(@"Metadata: %ld frames, %ldx%ld, %.3f fps",
|
|
1709
|
+
(long)self.totalFrames, (long)self.frameWidth, (long)self.frameHeight, self.frameRate);
|
|
1710
|
+
|
|
1711
|
+
[self loadTextureArray:metadata sourcePath:sourcePath fileData:fileData reader:reader];
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
/**
|
|
1715
|
+
* Decode ASTC compressed data to RGBA using proper decoder.
|
|
1716
|
+
* Uses basisu::astc::decompress from astc_decomp.cpp
|
|
1717
|
+
* Supports all ASTC block sizes via _astcBlockWidth/_astcBlockHeight.
|
|
1718
|
+
*/
|
|
1719
|
+
- (NSData *)decodeASTCToRGBA:(NSData *)astcData width:(uint32_t)width height:(uint32_t)height
|
|
1720
|
+
{
|
|
1721
|
+
const uint8_t *blocks = (const uint8_t *)[astcData bytes];
|
|
1722
|
+
const uint32_t bw = _astcBlockWidth;
|
|
1723
|
+
const uint32_t bh = _astcBlockHeight;
|
|
1724
|
+
uint32_t blocksX = (width + bw - 1) / bw;
|
|
1725
|
+
uint32_t blocksY = (height + bh - 1) / bh;
|
|
1726
|
+
|
|
1727
|
+
NSMutableData *rgbaData = [NSMutableData dataWithLength:width * height * 4];
|
|
1728
|
+
uint8_t *rgba = (uint8_t *)[rgbaData mutableBytes];
|
|
1729
|
+
memset(rgba, 0, width * height * 4);
|
|
1730
|
+
|
|
1731
|
+
// Temporary buffer for one decoded block (max 12x12)
|
|
1732
|
+
uint8_t blockPixels[12 * 12 * 4];
|
|
1733
|
+
|
|
1734
|
+
for (uint32_t by = 0; by < blocksY; by++) {
|
|
1735
|
+
for (uint32_t bx = 0; bx < blocksX; bx++) {
|
|
1736
|
+
const uint8_t *block = blocks + (by * blocksX + bx) * 16;
|
|
1737
|
+
|
|
1738
|
+
// Use proper ASTC decoder (sRGB = false for linear decoding)
|
|
1739
|
+
bool success = basisu::astc::decompress(blockPixels, block, false, bw, bh);
|
|
1740
|
+
if (!success) {
|
|
1741
|
+
// Fill with magenta to indicate decode error
|
|
1742
|
+
uint32_t pixelsInBlock = bw * bh;
|
|
1743
|
+
for (uint32_t i = 0; i < pixelsInBlock; i++) {
|
|
1744
|
+
blockPixels[i * 4 + 0] = 255;
|
|
1745
|
+
blockPixels[i * 4 + 1] = 0;
|
|
1746
|
+
blockPixels[i * 4 + 2] = 255;
|
|
1747
|
+
blockPixels[i * 4 + 3] = 255;
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
// Copy block to output
|
|
1752
|
+
for (uint32_t y = 0; y < bh && (by * bh + y) < height; y++) {
|
|
1753
|
+
for (uint32_t x = 0; x < bw && (bx * bw + x) < width; x++) {
|
|
1754
|
+
uint32_t dstX = bx * bw + x;
|
|
1755
|
+
uint32_t dstY = by * bh + y;
|
|
1756
|
+
uint8_t *dst = rgba + (dstY * width + dstX) * 4;
|
|
1757
|
+
uint8_t *src = blockPixels + (y * bw + x) * 4;
|
|
1758
|
+
dst[0] = src[0];
|
|
1759
|
+
dst[1] = src[1];
|
|
1760
|
+
dst[2] = src[2];
|
|
1761
|
+
dst[3] = src[3];
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
AVG_VLOG(@"Decoded ASTC %ux%u (%u x %u blocks, block=%ux%u) to RGBA", width, height, blocksX, blocksY, bw, bh);
|
|
1768
|
+
return rgbaData;
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
#pragma mark - Texture Array Loading
|
|
1772
|
+
|
|
1773
|
+
/**
|
|
1774
|
+
* Load a KTX2 texture array (one frame per layer).
|
|
1775
|
+
* Uses Metal's MTLTextureType2DArray for efficient frame switching.
|
|
1776
|
+
*/
|
|
1777
|
+
- (void)loadTextureArray:(NSDictionary *)metadata
|
|
1778
|
+
sourcePath:(NSString *)filePath
|
|
1779
|
+
fileData:(NSData *)fileData
|
|
1780
|
+
reader:(AVGSourceReader *)reader
|
|
1781
|
+
{
|
|
1782
|
+
// Check for streaming mode from stream prop (not metadata)
|
|
1783
|
+
BOOL streamMode = self.stream;
|
|
1784
|
+
|
|
1785
|
+
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
|
|
1786
|
+
// Load the KTX2 file
|
|
1787
|
+
NSData *ktx2Data = fileData ?: [NSData dataWithContentsOfFile:filePath options:NSDataReadingMappedIfSafe error:nil];
|
|
1788
|
+
|
|
1789
|
+
if (!ktx2Data || ktx2Data.length < 104) {
|
|
1790
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
1791
|
+
[self notifyError:@"Failed to load texture array KTX2 file"];
|
|
1792
|
+
});
|
|
1793
|
+
return;
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
// Parse KTX2 header
|
|
1797
|
+
const uint8_t *bytes = (const uint8_t *)[ktx2Data bytes];
|
|
1798
|
+
uint32_t vkFormat = *(uint32_t *)(bytes + 12);
|
|
1799
|
+
uint32_t width = *(uint32_t *)(bytes + 20);
|
|
1800
|
+
uint32_t height = *(uint32_t *)(bytes + 24);
|
|
1801
|
+
uint32_t layerCount = *(uint32_t *)(bytes + 32);
|
|
1802
|
+
uint32_t supercompressionScheme = *(uint32_t *)(bytes + 44);
|
|
1803
|
+
|
|
1804
|
+
// Level 0 index (at byte 80)
|
|
1805
|
+
uint64_t level0Offset = *(uint64_t *)(bytes + 80);
|
|
1806
|
+
uint64_t level0Length = *(uint64_t *)(bytes + 88);
|
|
1807
|
+
uint64_t level0UncompressedLength = *(uint64_t *)(bytes + 96);
|
|
1808
|
+
|
|
1809
|
+
// Configure ASTC format from vkFormat
|
|
1810
|
+
dispatch_sync(dispatch_get_main_queue(), ^{
|
|
1811
|
+
[self configureASTCFormatFromVkFormat:vkFormat];
|
|
1812
|
+
});
|
|
1813
|
+
|
|
1814
|
+
AVG_VLOG(@"Texture array: %ux%u, %u layers, supercompression=%u, streamMode=%d, vkFormat=%u",
|
|
1815
|
+
width, height, layerCount, supercompressionScheme, streamMode, vkFormat);
|
|
1816
|
+
|
|
1817
|
+
// Check for internal zstd streaming mode
|
|
1818
|
+
if (streamMode && supercompressionScheme == 2 && reader) {
|
|
1819
|
+
AVG_VLOG(@"Using internal zstd streaming mode, from a reader");
|
|
1820
|
+
[self loadInternalZstdStreamingFromReader:reader
|
|
1821
|
+
width:width
|
|
1822
|
+
height:height
|
|
1823
|
+
layerCount:layerCount
|
|
1824
|
+
level0Offset:level0Offset
|
|
1825
|
+
level0Length:level0Length
|
|
1826
|
+
metadata:metadata];
|
|
1827
|
+
return;
|
|
1828
|
+
}
|
|
1829
|
+
if (streamMode && supercompressionScheme == 2 && filePath) {
|
|
1830
|
+
AVG_VLOG(@"Using internal zstd streaming mode");
|
|
1831
|
+
[self loadInternalZstdStreamingTextureArray:filePath
|
|
1832
|
+
width:width
|
|
1833
|
+
height:height
|
|
1834
|
+
layerCount:layerCount
|
|
1835
|
+
level0Offset:level0Offset
|
|
1836
|
+
level0Length:level0Length
|
|
1837
|
+
metadata:metadata];
|
|
1838
|
+
return;
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
// Not streamed: a reader's file is read whole, past the header
|
|
1842
|
+
if (reader && (int64_t)ktx2Data.length < reader.size) {
|
|
1843
|
+
NSString *error = nil;
|
|
1844
|
+
ktx2Data = [reader readAll:&error];
|
|
1845
|
+
if (!ktx2Data) {
|
|
1846
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
1847
|
+
if (reader == self->_reader) [self notifyError:error];
|
|
1848
|
+
});
|
|
1849
|
+
return;
|
|
1850
|
+
}
|
|
1851
|
+
bytes = (const uint8_t *)[ktx2Data bytes];
|
|
1852
|
+
}
|
|
1853
|
+
if (level0Offset + level0Length > ktx2Data.length) {
|
|
1854
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
1855
|
+
[self notifyError:@"The KTX2 file is shorter than its level index says"];
|
|
1856
|
+
});
|
|
1857
|
+
return;
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
// Extract ASTC data (non-streaming path)
|
|
1861
|
+
NSData *astcData = nil;
|
|
1862
|
+
|
|
1863
|
+
if (supercompressionScheme == 2) {
|
|
1864
|
+
// Zstd compressed - decompress using zstd (all at once)
|
|
1865
|
+
const void *compressedData = bytes + level0Offset;
|
|
1866
|
+
size_t compressedSize = (size_t)level0Length;
|
|
1867
|
+
size_t decompressedSize = (size_t)level0UncompressedLength;
|
|
1868
|
+
|
|
1869
|
+
NSMutableData *decompressedData = [NSMutableData dataWithLength:decompressedSize];
|
|
1870
|
+
size_t result = ZSTD_decompress([decompressedData mutableBytes], decompressedSize,
|
|
1871
|
+
compressedData, compressedSize);
|
|
1872
|
+
|
|
1873
|
+
if (ZSTD_isError(result)) {
|
|
1874
|
+
NSLog(@"AlphaVideoGpuView: Zstd decompression failed: %s", ZSTD_getErrorName(result));
|
|
1875
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
1876
|
+
[self notifyError:@"Failed to decompress texture array"];
|
|
1877
|
+
});
|
|
1878
|
+
return;
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
astcData = decompressedData;
|
|
1882
|
+
AVG_VLOG(@"Decompressed %llu -> %zu bytes", level0Length, result);
|
|
1883
|
+
} else {
|
|
1884
|
+
// Uncompressed
|
|
1885
|
+
astcData = [NSData dataWithBytes:bytes + level0Offset length:(NSUInteger)level0Length];
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
// Calculate bytes per layer using current ASTC block dimensions
|
|
1889
|
+
uint32_t blocksX = (width + self->_astcBlockWidth - 1) / self->_astcBlockWidth;
|
|
1890
|
+
uint32_t blocksY = (height + self->_astcBlockHeight - 1) / self->_astcBlockHeight;
|
|
1891
|
+
uint32_t bytesPerLayer = blocksX * blocksY * 16;
|
|
1892
|
+
|
|
1893
|
+
AVG_VLOG(@"ASTC data: %lu bytes, %u bytes/layer (ASTC %ux%u), %u layers",
|
|
1894
|
+
(unsigned long)astcData.length, bytesPerLayer, self->_astcBlockWidth, self->_astcBlockHeight, layerCount);
|
|
1895
|
+
|
|
1896
|
+
// Pre-decode ASTC to RGBA on background thread for simulator (avoid main thread freeze)
|
|
1897
|
+
NSMutableArray<NSData *> *decodedLayers = nil;
|
|
1898
|
+
if (!self->_useCompressedFormat) {
|
|
1899
|
+
AVG_VLOG(@"Pre-decoding %u ASTC layers to RGBA on background thread (simulator)", layerCount);
|
|
1900
|
+
decodedLayers = [NSMutableArray arrayWithCapacity:layerCount];
|
|
1901
|
+
const uint8_t *astcBytes = (const uint8_t *)[astcData bytes];
|
|
1902
|
+
|
|
1903
|
+
for (uint32_t layer = 0; layer < layerCount; layer++) {
|
|
1904
|
+
const uint8_t *layerData = astcBytes + (layer * bytesPerLayer);
|
|
1905
|
+
NSData *layerASTCData = [NSData dataWithBytesNoCopy:(void *)layerData length:bytesPerLayer freeWhenDone:NO];
|
|
1906
|
+
NSData *rgbaData = [self decodeASTCToRGBA:layerASTCData width:width height:height];
|
|
1907
|
+
if (rgbaData) {
|
|
1908
|
+
[decodedLayers addObject:rgbaData];
|
|
1909
|
+
} else {
|
|
1910
|
+
// Create empty placeholder to maintain layer indices
|
|
1911
|
+
[decodedLayers addObject:[NSData data]];
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
AVG_VLOG(@"Pre-decode complete, uploading to GPU");
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
// Create texture array and upload on main thread (fast - just memcpy)
|
|
1918
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
1919
|
+
if (self->_isDestroyed) return;
|
|
1920
|
+
|
|
1921
|
+
// Create texture array descriptor
|
|
1922
|
+
// For non-streaming bulk load, use Private storage for better GPU read performance
|
|
1923
|
+
// Create staging texture with Shared storage, then blit to Private final texture
|
|
1924
|
+
MTLTextureDescriptor *descriptor = [[MTLTextureDescriptor alloc] init];
|
|
1925
|
+
descriptor.textureType = MTLTextureType2DArray;
|
|
1926
|
+
descriptor.pixelFormat = self->_useCompressedFormat ? self->_metalPixelFormat : MTLPixelFormatRGBA8Unorm;
|
|
1927
|
+
descriptor.width = width;
|
|
1928
|
+
descriptor.height = height;
|
|
1929
|
+
descriptor.arrayLength = layerCount;
|
|
1930
|
+
descriptor.mipmapLevelCount = 1;
|
|
1931
|
+
descriptor.usage = MTLTextureUsageShaderRead;
|
|
1932
|
+
|
|
1933
|
+
// Use Private storage for final texture (GPU-optimized layout)
|
|
1934
|
+
// Only use Private storage for compressed format (real device) - simulator needs Shared for CPU writes
|
|
1935
|
+
BOOL usePrivateStorage = self->_useCompressedFormat;
|
|
1936
|
+
|
|
1937
|
+
id<MTLTexture> stagingTexture = nil;
|
|
1938
|
+
if (usePrivateStorage) {
|
|
1939
|
+
// Create staging texture with Shared storage for CPU writes
|
|
1940
|
+
descriptor.storageMode = MTLStorageModeShared;
|
|
1941
|
+
stagingTexture = [self.device newTextureWithDescriptor:descriptor];
|
|
1942
|
+
|
|
1943
|
+
// Create final texture with Private storage for GPU-optimized reads
|
|
1944
|
+
descriptor.storageMode = MTLStorageModePrivate;
|
|
1945
|
+
self->_textureArray = [self.device newTextureWithDescriptor:descriptor];
|
|
1946
|
+
} else {
|
|
1947
|
+
descriptor.storageMode = MTLStorageModeShared;
|
|
1948
|
+
self->_textureArray = [self.device newTextureWithDescriptor:descriptor];
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1951
|
+
if (!self->_textureArray || (usePrivateStorage && !stagingTexture)) {
|
|
1952
|
+
[self notifyError:@"Failed to create texture array"];
|
|
1953
|
+
return;
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
// Upload each layer to staging texture (or directly to final if not using Private)
|
|
1957
|
+
id<MTLTexture> uploadTarget = usePrivateStorage ? stagingTexture : self->_textureArray;
|
|
1958
|
+
const uint8_t *astcBytes = (const uint8_t *)[astcData bytes];
|
|
1959
|
+
|
|
1960
|
+
for (uint32_t layer = 0; layer < layerCount; layer++) {
|
|
1961
|
+
MTLRegion region = MTLRegionMake2D(0, 0, width, height);
|
|
1962
|
+
|
|
1963
|
+
if (self->_useCompressedFormat) {
|
|
1964
|
+
// Upload compressed ASTC directly (real device)
|
|
1965
|
+
// bytesPerRow must be 0 for compressed textures per Apple docs
|
|
1966
|
+
const uint8_t *layerData = astcBytes + (layer * bytesPerLayer);
|
|
1967
|
+
[uploadTarget replaceRegion:region
|
|
1968
|
+
mipmapLevel:0
|
|
1969
|
+
slice:layer
|
|
1970
|
+
withBytes:layerData
|
|
1971
|
+
bytesPerRow:0
|
|
1972
|
+
bytesPerImage:bytesPerLayer];
|
|
1973
|
+
} else {
|
|
1974
|
+
// Upload pre-decoded RGBA (simulator) - uses Shared storage, no staging needed
|
|
1975
|
+
if (layer < decodedLayers.count && decodedLayers[layer].length > 0) {
|
|
1976
|
+
[uploadTarget replaceRegion:region
|
|
1977
|
+
mipmapLevel:0
|
|
1978
|
+
slice:layer
|
|
1979
|
+
withBytes:[decodedLayers[layer] bytes]
|
|
1980
|
+
bytesPerRow:width * 4
|
|
1981
|
+
bytesPerImage:width * height * 4];
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1986
|
+
// Blit from staging to Private final texture if using Private storage
|
|
1987
|
+
if (usePrivateStorage && stagingTexture) {
|
|
1988
|
+
id<MTLCommandQueue> cmdQueue = self->_blitCommandQueue ?: [self.device newCommandQueue];
|
|
1989
|
+
id<MTLCommandBuffer> cmdBuf = [cmdQueue commandBuffer];
|
|
1990
|
+
id<MTLBlitCommandEncoder> blit = [cmdBuf blitCommandEncoder];
|
|
1991
|
+
|
|
1992
|
+
// Copy all layers from staging to final
|
|
1993
|
+
[blit copyFromTexture:stagingTexture toTexture:self->_textureArray];
|
|
1994
|
+
|
|
1995
|
+
[blit endEncoding];
|
|
1996
|
+
[cmdBuf commit];
|
|
1997
|
+
[cmdBuf waitUntilCompleted]; // Wait for blit to complete before releasing staging
|
|
1998
|
+
|
|
1999
|
+
// Staging texture will be released when it goes out of scope
|
|
2000
|
+
AVG_VLOG(@"Blitted staging texture to Private storage");
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
self->_textureArrayLayerCount = layerCount;
|
|
2004
|
+
self->_isSourceLoaded = YES;
|
|
2005
|
+
self->_isSourceLoading = NO;
|
|
2006
|
+
|
|
2007
|
+
AVG_VLOG(@"✓ Texture array loaded: %ux%u, %u layers (Private storage: %@)",
|
|
2008
|
+
width, height, layerCount, usePrivateStorage ? @"YES" : @"NO");
|
|
2009
|
+
|
|
2010
|
+
[self notifyLoad];
|
|
2011
|
+
[self notifyStateChange:@"ready"];
|
|
2012
|
+
|
|
2013
|
+
if (self.autoPlay && !self.playbackPaused) {
|
|
2014
|
+
[self play];
|
|
2015
|
+
}
|
|
2016
|
+
});
|
|
2017
|
+
});
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
#pragma mark - Internal Zstd Streaming
|
|
2021
|
+
|
|
2022
|
+
/**
|
|
2023
|
+
* Load a KTX2 texture array with internal zstd compression using streaming decompression.
|
|
2024
|
+
* The KTX2 header is uncompressed and only the level0 data is zstd compressed
|
|
2025
|
+
* (supercompressionScheme=2), so the layers are decompressed as they play.
|
|
2026
|
+
*/
|
|
2027
|
+
- (void)loadInternalZstdStreamingTextureArray:(NSString *)filePath
|
|
2028
|
+
width:(uint32_t)width
|
|
2029
|
+
height:(uint32_t)height
|
|
2030
|
+
layerCount:(uint32_t)layerCount
|
|
2031
|
+
level0Offset:(uint64_t)level0Offset
|
|
2032
|
+
level0Length:(uint64_t)level0Length
|
|
2033
|
+
metadata:(NSDictionary *)metadata
|
|
2034
|
+
{
|
|
2035
|
+
AVG_VLOG(@"Starting internal zstd streaming: %@", filePath);
|
|
2036
|
+
AVG_VLOG(@"Internal zstd: %ux%u, %u layers, offset=%llu, compressedSize=%llu",
|
|
2037
|
+
width, height, layerCount, level0Offset, level0Length);
|
|
2038
|
+
|
|
2039
|
+
// Open file and mmap only the compressed data portion
|
|
2040
|
+
int fd = open([filePath UTF8String], O_RDONLY);
|
|
2041
|
+
if (fd < 0) {
|
|
2042
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
2043
|
+
[self notifyError:@"Failed to open file for internal zstd streaming"];
|
|
2044
|
+
});
|
|
2045
|
+
return;
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
// Get file size for validation
|
|
2049
|
+
struct stat st;
|
|
2050
|
+
if (fstat(fd, &st) < 0) {
|
|
2051
|
+
close(fd);
|
|
2052
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
2053
|
+
[self notifyError:@"Failed to stat file"];
|
|
2054
|
+
});
|
|
2055
|
+
return;
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2058
|
+
// Mmap the compressed data portion (page-aligned)
|
|
2059
|
+
long pageSize = sysconf(_SC_PAGE_SIZE);
|
|
2060
|
+
off_t alignedOffset = (level0Offset / pageSize) * pageSize;
|
|
2061
|
+
size_t offsetAdjustment = level0Offset - alignedOffset;
|
|
2062
|
+
size_t mmapLength = level0Length + offsetAdjustment;
|
|
2063
|
+
|
|
2064
|
+
void *mmapData = mmap(NULL, mmapLength, PROT_READ, MAP_PRIVATE, fd, alignedOffset);
|
|
2065
|
+
if (mmapData == MAP_FAILED) {
|
|
2066
|
+
close(fd);
|
|
2067
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
2068
|
+
[self notifyError:@"Failed to mmap internal zstd data"];
|
|
2069
|
+
});
|
|
2070
|
+
return;
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
// Advise kernel for sequential access
|
|
2074
|
+
madvise(mmapData, mmapLength, MADV_SEQUENTIAL);
|
|
2075
|
+
|
|
2076
|
+
// Create ZSTD_DStream for streaming decompression
|
|
2077
|
+
ZSTD_DStream *dstream = ZSTD_createDStream();
|
|
2078
|
+
if (!dstream) {
|
|
2079
|
+
munmap(mmapData, mmapLength);
|
|
2080
|
+
close(fd);
|
|
2081
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
2082
|
+
[self notifyError:@"Failed to create ZSTD_DStream"];
|
|
2083
|
+
});
|
|
2084
|
+
return;
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
size_t initResult = ZSTD_initDStream(dstream);
|
|
2088
|
+
if (ZSTD_isError(initResult)) {
|
|
2089
|
+
ZSTD_freeDStream(dstream);
|
|
2090
|
+
munmap(mmapData, mmapLength);
|
|
2091
|
+
close(fd);
|
|
2092
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
2093
|
+
[self notifyError:@"Failed to init ZSTD_DStream"];
|
|
2094
|
+
});
|
|
2095
|
+
return;
|
|
2096
|
+
}
|
|
2097
|
+
|
|
2098
|
+
const uint8_t *compressedDataPtr = (const uint8_t *)mmapData + offsetAdjustment;
|
|
2099
|
+
uint32_t bytesPerLayer = [self astcBytesPerLayerForWidth:width height:height];
|
|
2100
|
+
[self startInternalZstdStreaming:dstream
|
|
2101
|
+
width:width
|
|
2102
|
+
height:height
|
|
2103
|
+
layerCount:layerCount
|
|
2104
|
+
metadata:metadata
|
|
2105
|
+
adopt:^BOOL {
|
|
2106
|
+
self->_compressedData = [NSData dataWithBytesNoCopy:(void *)compressedDataPtr
|
|
2107
|
+
length:level0Length
|
|
2108
|
+
freeWhenDone:NO];
|
|
2109
|
+
self->_zstdStream = (AVGZstdStream){dstream, compressedDataPtr, (size_t)level0Length, 0, false};
|
|
2110
|
+
avg_zstd_seek_table_free(&self->_seekTable);
|
|
2111
|
+
if (avg_zstd_seek_table_parse(compressedDataPtr, (size_t)level0Length,
|
|
2112
|
+
(uint64_t)bytesPerLayer * layerCount, &self->_seekTable)) {
|
|
2113
|
+
AVG_VLOG(@"Seek table: %zu seek points", self->_seekTable.count);
|
|
2114
|
+
} else {
|
|
2115
|
+
AVG_VLOG(@"No seek table: seeks decode from the start");
|
|
2116
|
+
}
|
|
2117
|
+
|
|
2118
|
+
// Store mmap info for cleanup and loop restart
|
|
2119
|
+
self->_mmapFd = fd;
|
|
2120
|
+
self->_mmapData = mmapData;
|
|
2121
|
+
self->_mmapSize = mmapLength;
|
|
2122
|
+
return YES;
|
|
2123
|
+
}];
|
|
2124
|
+
}
|
|
2125
|
+
|
|
2126
|
+
/** loadInternalZstdStreamingTextureArray: for a level read through a reader as it plays. */
|
|
2127
|
+
- (void)loadInternalZstdStreamingFromReader:(AVGSourceReader *)reader
|
|
2128
|
+
width:(uint32_t)width
|
|
2129
|
+
height:(uint32_t)height
|
|
2130
|
+
layerCount:(uint32_t)layerCount
|
|
2131
|
+
level0Offset:(uint64_t)level0Offset
|
|
2132
|
+
level0Length:(uint64_t)level0Length
|
|
2133
|
+
metadata:(NSDictionary *)metadata
|
|
2134
|
+
{
|
|
2135
|
+
uint32_t bytesPerLayer = [self astcBytesPerLayerForWidth:width height:height];
|
|
2136
|
+
NSString *error = nil;
|
|
2137
|
+
AVGZstdSeekTable table;
|
|
2138
|
+
if (![self readSeekTable:&table
|
|
2139
|
+
from:reader
|
|
2140
|
+
levelOffset:level0Offset
|
|
2141
|
+
levelLength:level0Length
|
|
2142
|
+
decompressedLength:(uint64_t)bytesPerLayer * layerCount
|
|
2143
|
+
error:&error]) {
|
|
2144
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
2145
|
+
if (reader == self->_reader) [self notifyError:error];
|
|
2146
|
+
});
|
|
2147
|
+
return;
|
|
2148
|
+
}
|
|
2149
|
+
AVG_VLOG(@"%@", table.count > 0 ? [NSString stringWithFormat:@"Seek table: %zu seek points", table.count]
|
|
2150
|
+
: @"No seek table: seeks decode from the start");
|
|
2151
|
+
|
|
2152
|
+
ZSTD_DStream *dstream = ZSTD_createDStream();
|
|
2153
|
+
if (!dstream || ZSTD_isError(ZSTD_initDStream(dstream))) {
|
|
2154
|
+
ZSTD_freeDStream(dstream);
|
|
2155
|
+
avg_zstd_seek_table_free(&table);
|
|
2156
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
2157
|
+
[self notifyError:@"Failed to create ZSTD_DStream"];
|
|
2158
|
+
});
|
|
2159
|
+
return;
|
|
2160
|
+
}
|
|
2161
|
+
NSMutableData *chunk = [NSMutableData dataWithLength:kAVGReaderChunkBytes];
|
|
2162
|
+
__block AVGZstdSeekTable seekTable = table;
|
|
2163
|
+
[self startInternalZstdStreaming:dstream
|
|
2164
|
+
width:width
|
|
2165
|
+
height:height
|
|
2166
|
+
layerCount:layerCount
|
|
2167
|
+
metadata:metadata
|
|
2168
|
+
adopt:^BOOL {
|
|
2169
|
+
if (reader != self->_reader) {
|
|
2170
|
+
ZSTD_freeDStream(dstream);
|
|
2171
|
+
avg_zstd_seek_table_free(&seekTable);
|
|
2172
|
+
return NO;
|
|
2173
|
+
}
|
|
2174
|
+
self->_streamChunk = chunk;
|
|
2175
|
+
self->_levelInput.reader = (__bridge void *)reader;
|
|
2176
|
+
self->_levelInput.levelOffset = level0Offset;
|
|
2177
|
+
AVGZstdStream stream;
|
|
2178
|
+
memset(&stream, 0, sizeof(stream));
|
|
2179
|
+
stream.dstream = dstream;
|
|
2180
|
+
stream.size = (size_t)level0Length;
|
|
2181
|
+
stream.read = AVGReadLevel;
|
|
2182
|
+
stream.readContext = &self->_levelInput;
|
|
2183
|
+
stream.chunk = (uint8_t *)chunk.mutableBytes;
|
|
2184
|
+
stream.chunkCapacity = chunk.length;
|
|
2185
|
+
self->_zstdStream = stream;
|
|
2186
|
+
avg_zstd_seek_table_free(&self->_seekTable);
|
|
2187
|
+
self->_seekTable = seekTable;
|
|
2188
|
+
return YES;
|
|
2189
|
+
}];
|
|
2190
|
+
}
|
|
2191
|
+
|
|
2192
|
+
/**
|
|
2193
|
+
* The setup both zstd streams share, on the main queue: adopt installs the stream's input
|
|
2194
|
+
* (NO to drop it, for a source no longer showing), then the ring and the first frames.
|
|
2195
|
+
*/
|
|
2196
|
+
- (void)startInternalZstdStreaming:(ZSTD_DStream *)dstream
|
|
2197
|
+
width:(uint32_t)width
|
|
2198
|
+
height:(uint32_t)height
|
|
2199
|
+
layerCount:(uint32_t)layerCount
|
|
2200
|
+
metadata:(NSDictionary *)metadata
|
|
2201
|
+
adopt:(BOOL (^)(void))adopt
|
|
2202
|
+
{
|
|
2203
|
+
// Calculate bytes per layer using current ASTC block dimensions
|
|
2204
|
+
uint32_t bytesPerLayer = [self astcBytesPerLayerForWidth:width height:height];
|
|
2205
|
+
|
|
2206
|
+
// vram decodes into the ring: the shown frame, the ones decoded after it, and the previous
|
|
2207
|
+
// frame, which the GPU may still be drawing. ram uploads each frame as it's shown.
|
|
2208
|
+
BOOL useVramStrategy = ![self.memoryStrategy isEqualToString:@"ram"];
|
|
2209
|
+
NSInteger gpuCacheSize = MIN(useVramStrategy ? self.streamingPrefetchAhead + 2 : kRamRingSize,
|
|
2210
|
+
(NSInteger)layerCount);
|
|
2211
|
+
|
|
2212
|
+
AVG_VLOG(@"Internal zstd streaming params: strategy=%@, gpuCache=%ld, bytesPerLayer=%u (ASTC %ux%u)",
|
|
2213
|
+
self.memoryStrategy, (long)gpuCacheSize, bytesPerLayer, _astcBlockWidth, _astcBlockHeight);
|
|
2214
|
+
|
|
2215
|
+
// Setup on main thread
|
|
2216
|
+
dispatch_sync(dispatch_get_main_queue(), ^{
|
|
2217
|
+
if (!adopt()) return;
|
|
2218
|
+
|
|
2219
|
+
// Apply metadata
|
|
2220
|
+
self.totalFrames = layerCount;
|
|
2221
|
+
self.frameWidth = width;
|
|
2222
|
+
self.frameHeight = height;
|
|
2223
|
+
self.frameRate = [metadata[@"frameRate"] doubleValue];
|
|
2224
|
+
self.frameInterval = 1.0 / self.frameRate;
|
|
2225
|
+
|
|
2226
|
+
self->_textureArrayLayerCount = layerCount;
|
|
2227
|
+
self->_streamingRingBufferMode = YES;
|
|
2228
|
+
self->_streamingRingBufferSize = gpuCacheSize;
|
|
2229
|
+
self->_useVramStrategy = useVramStrategy;
|
|
2230
|
+
|
|
2231
|
+
// Initialize ring buffer tracking
|
|
2232
|
+
self->_ringSlots = [NSMutableArray arrayWithCapacity:gpuCacheSize];
|
|
2233
|
+
for (NSInteger i = 0; i < gpuCacheSize; i++) {
|
|
2234
|
+
[self->_ringSlots addObject:@(-1)];
|
|
2235
|
+
}
|
|
2236
|
+
self->_frameCache = [NSMutableDictionary dictionary];
|
|
2237
|
+
self->_decoderPosition = 0;
|
|
2238
|
+
self->_decoderLoopIteration = 0;
|
|
2239
|
+
self->_playbackLoopIteration = 0;
|
|
2240
|
+
self->_fewestAhead = layerCount;
|
|
2241
|
+
self->_stalls = 0;
|
|
2242
|
+
|
|
2243
|
+
self->_dstream = dstream;
|
|
2244
|
+
self->_pendingSkipBytes = 0;
|
|
2245
|
+
self->_frameAccumulator = [NSMutableData data];
|
|
2246
|
+
self->_streamBytesPerLayer = bytesPerLayer;
|
|
2247
|
+
self->_streamLevel0Offset = 0; // the stream's offsets start at the level0 data
|
|
2248
|
+
|
|
2249
|
+
// Create texture array with ring buffer size
|
|
2250
|
+
MTLTextureDescriptor *descriptor = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:self->_metalPixelFormat
|
|
2251
|
+
width:width
|
|
2252
|
+
height:height
|
|
2253
|
+
mipmapped:NO];
|
|
2254
|
+
descriptor.textureType = MTLTextureType2DArray;
|
|
2255
|
+
descriptor.arrayLength = gpuCacheSize;
|
|
2256
|
+
descriptor.usage = MTLTextureUsageShaderRead;
|
|
2257
|
+
|
|
2258
|
+
self->_textureArray = [self.device newTextureWithDescriptor:descriptor];
|
|
2259
|
+
|
|
2260
|
+
if (!self->_textureArray) {
|
|
2261
|
+
[self notifyError:@"Failed to create texture array for internal zstd streaming"];
|
|
2262
|
+
return;
|
|
2263
|
+
}
|
|
2264
|
+
|
|
2265
|
+
AVG_VLOG(@"Created internal zstd streaming texture array: %ux%u, %ld GPU slots",
|
|
2266
|
+
width, height, (long)gpuCacheSize);
|
|
2267
|
+
|
|
2268
|
+
// Pre-decode initial frames
|
|
2269
|
+
[self decodeFramesAhead:0];
|
|
2270
|
+
|
|
2271
|
+
self->_isSourceLoaded = YES;
|
|
2272
|
+
self->_isSourceLoading = NO;
|
|
2273
|
+
[self notifyLoad];
|
|
2274
|
+
[self notifyStateChange:@"ready"];
|
|
2275
|
+
|
|
2276
|
+
if (self.autoPlay && !self.playbackPaused) {
|
|
2277
|
+
[self play];
|
|
2278
|
+
}
|
|
2279
|
+
});
|
|
2280
|
+
|
|
2281
|
+
AVG_VLOG(@"Internal zstd streaming setup complete");
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
- (void)play
|
|
2285
|
+
{
|
|
2286
|
+
AVG_VLOG(@"play called - isPlaying=%d, totalFrames=%ld", self.isPlaying, (long)self.totalFrames);
|
|
2287
|
+
|
|
2288
|
+
if (!self.isPlaying && self.totalFrames > 0) {
|
|
2289
|
+
AVG_VLOG(@"Starting playback...");
|
|
2290
|
+
if (_ended) {
|
|
2291
|
+
[self seekToFrame:0];
|
|
2292
|
+
}
|
|
2293
|
+
self.isPlaying = YES;
|
|
2294
|
+
self.lastFrameTime = CACurrentMediaTime();
|
|
2295
|
+
[self updateDisplayLink];
|
|
2296
|
+
|
|
2297
|
+
[self notifyStateChange:@"playing"];
|
|
2298
|
+
}
|
|
2299
|
+
}
|
|
2300
|
+
|
|
2301
|
+
- (void)pause
|
|
2302
|
+
{
|
|
2303
|
+
if (self.isPlaying) {
|
|
2304
|
+
self.isPlaying = NO;
|
|
2305
|
+
[self updateDisplayLink];
|
|
2306
|
+
[self notifyStateChange:@"paused"];
|
|
2307
|
+
}
|
|
2308
|
+
}
|
|
2309
|
+
|
|
2310
|
+
- (void)stop
|
|
2311
|
+
{
|
|
2312
|
+
[self pause];
|
|
2313
|
+
[self seekToFrame:0];
|
|
2314
|
+
[self notifyStateChange:@"stopped"];
|
|
2315
|
+
}
|
|
2316
|
+
|
|
2317
|
+
- (void)seekToFrame:(NSInteger)frame
|
|
2318
|
+
{
|
|
2319
|
+
if (!_isSourceLoaded || self.totalFrames <= 0) return;
|
|
2320
|
+
NSInteger target = MAX(0, MIN(frame, self.totalFrames - 1));
|
|
2321
|
+
_ended = NO;
|
|
2322
|
+
if (_streamingRingBufferMode) {
|
|
2323
|
+
[self moveStreamTo:target];
|
|
2324
|
+
}
|
|
2325
|
+
self.currentFrame = target;
|
|
2326
|
+
self.lastFrameTime = CACurrentMediaTime();
|
|
2327
|
+
_frameOwed = YES;
|
|
2328
|
+
[self drawOwedFrame];
|
|
2329
|
+
[self updateDisplayLink];
|
|
2330
|
+
[self notifyFrameUpdate];
|
|
2331
|
+
}
|
|
2332
|
+
|
|
2333
|
+
- (void)seekToTime:(NSTimeInterval)seconds
|
|
2334
|
+
{
|
|
2335
|
+
double frame = floor(MAX(seconds, 0) * self.frameRate + 1e-6);
|
|
2336
|
+
[self seekToFrame:(NSInteger)MIN(frame, (double)INT32_MAX)];
|
|
2337
|
+
}
|
|
2338
|
+
|
|
2339
|
+
/** Draws the frame a load or a seek owes, once it is decoded. */
|
|
2340
|
+
- (void)drawOwedFrame
|
|
2341
|
+
{
|
|
2342
|
+
if (!_frameOwed || ![self prepareShownFrame]) return;
|
|
2343
|
+
_frameOwed = NO;
|
|
2344
|
+
[self setNeedsDisplay];
|
|
2345
|
+
}
|
|
2346
|
+
|
|
2347
|
+
/** Runs the display link while playing, or until an owed frame is drawn. */
|
|
2348
|
+
- (void)updateDisplayLink
|
|
2349
|
+
{
|
|
2350
|
+
BOOL needed = self.isPlaying || _frameOwed;
|
|
2351
|
+
if (needed && !self.displayLink) {
|
|
2352
|
+
self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(updateFrame)];
|
|
2353
|
+
self.displayLink.preferredFramesPerSecond = (NSInteger)ceil(self.frameRate);
|
|
2354
|
+
[self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
|
|
2355
|
+
} else if (!needed && self.displayLink) {
|
|
2356
|
+
[self.displayLink invalidate];
|
|
2357
|
+
self.displayLink = nil;
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
|
|
2361
|
+
// Same range as Android and the web
|
|
2362
|
+
- (void)setStreamingPrefetchAhead:(NSInteger)frames
|
|
2363
|
+
{
|
|
2364
|
+
_streamingPrefetchAhead = MAX(1, MIN(100, frames));
|
|
2365
|
+
}
|
|
2366
|
+
|
|
2367
|
+
- (void)setPlaybackSpeed:(CGFloat)speed
|
|
2368
|
+
{
|
|
2369
|
+
_playbackSpeed = speed;
|
|
2370
|
+
_frameInterval = 1.0 / (_frameRate * speed);
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2373
|
+
- (void)setPlaybackPaused:(BOOL)newPaused
|
|
2374
|
+
{
|
|
2375
|
+
if (_playbackPaused != newPaused) {
|
|
2376
|
+
_playbackPaused = newPaused;
|
|
2377
|
+
AVG_VLOG(@"setPlaybackPaused: %d", newPaused);
|
|
2378
|
+
|
|
2379
|
+
if (newPaused) {
|
|
2380
|
+
// Stop playback
|
|
2381
|
+
[self pause];
|
|
2382
|
+
} else {
|
|
2383
|
+
// Resume playback if we were playing before and source is loaded
|
|
2384
|
+
if (_isSourceLoaded && self.autoPlay && !self.isPlaying) {
|
|
2385
|
+
[self play];
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2389
|
+
}
|
|
2390
|
+
|
|
2391
|
+
- (void)updateFrame
|
|
2392
|
+
{
|
|
2393
|
+
if (_frameOwed) {
|
|
2394
|
+
[self drawOwedFrame];
|
|
2395
|
+
if (!_frameOwed) self.lastFrameTime = CACurrentMediaTime();
|
|
2396
|
+
[self updateDisplayLink];
|
|
2397
|
+
return;
|
|
2398
|
+
}
|
|
2399
|
+
if (!self.isPlaying) {
|
|
2400
|
+
[self updateDisplayLink];
|
|
2401
|
+
return;
|
|
2402
|
+
}
|
|
2403
|
+
|
|
2404
|
+
CFTimeInterval currentTime = CACurrentMediaTime();
|
|
2405
|
+
|
|
2406
|
+
CFTimeInterval elapsed = currentTime - self.lastFrameTime;
|
|
2407
|
+
|
|
2408
|
+
if (elapsed >= self.frameInterval / self.playbackSpeed) {
|
|
2409
|
+
self.lastFrameTime = currentTime - fmod(elapsed, self.frameInterval / self.playbackSpeed);
|
|
2410
|
+
|
|
2411
|
+
// For streaming ring buffer mode, handle on-demand decoding/upload
|
|
2412
|
+
if (_streamingRingBufferMode) {
|
|
2413
|
+
NSInteger vPosModulus = self.totalFrames * 2;
|
|
2414
|
+
NSInteger virtualPos = (_playbackLoopIteration * self.totalFrames + self.currentFrame) % vPosModulus;
|
|
2415
|
+
NSInteger gpuSlot = self.currentFrame % _streamingRingBufferSize;
|
|
2416
|
+
|
|
2417
|
+
if (_isUncompressedStreaming) {
|
|
2418
|
+
// Uncompressed streaming
|
|
2419
|
+
if (!_useCompressedFormat && _decodedRgbaCache) {
|
|
2420
|
+
// Simulator: use background-decoded RGBA cache
|
|
2421
|
+
[self decodeUncompressedFramesAhead:virtualPos];
|
|
2422
|
+
[self evictOldDecodedFrames:virtualPos];
|
|
2423
|
+
|
|
2424
|
+
NSInteger slotVPos = [_ringSlots[gpuSlot] integerValue];
|
|
2425
|
+
if (slotVPos != virtualPos) {
|
|
2426
|
+
NSData *rgbaData;
|
|
2427
|
+
@synchronized(_decodedRgbaCache) {
|
|
2428
|
+
rgbaData = _decodedRgbaCache[@(virtualPos)];
|
|
2429
|
+
}
|
|
2430
|
+
if (rgbaData) {
|
|
2431
|
+
[self uploadDecodedFrameToGPU:virtualPos slot:gpuSlot];
|
|
2432
|
+
_ringSlots[gpuSlot] = @(virtualPos);
|
|
2433
|
+
} else {
|
|
2434
|
+
// Frame not decoded yet - skip this update
|
|
2435
|
+
return;
|
|
2436
|
+
}
|
|
2437
|
+
}
|
|
2438
|
+
} else {
|
|
2439
|
+
// Real device: upload directly from mmap (fast, native ASTC)
|
|
2440
|
+
NSInteger slotVPos = [_ringSlots[gpuSlot] integerValue];
|
|
2441
|
+
if (slotVPos != virtualPos) {
|
|
2442
|
+
[self uploadUncompressedFrame:self.currentFrame toSlot:gpuSlot];
|
|
2443
|
+
_ringSlots[gpuSlot] = @(virtualPos);
|
|
2444
|
+
}
|
|
2445
|
+
}
|
|
2446
|
+
} else {
|
|
2447
|
+
// Zstd streaming: decode frames ahead
|
|
2448
|
+
[self decodeFramesAhead:virtualPos];
|
|
2449
|
+
|
|
2450
|
+
// Evict old frames from CPU cache
|
|
2451
|
+
[self evictOldFrames:virtualPos];
|
|
2452
|
+
|
|
2453
|
+
// Ensure current frame is in GPU
|
|
2454
|
+
[self ensureFrameInGPU:virtualPos];
|
|
2455
|
+
|
|
2456
|
+
// Check if frame is ready
|
|
2457
|
+
NSInteger slotVPos = [_ringSlots[gpuSlot] integerValue];
|
|
2458
|
+
|
|
2459
|
+
if (slotVPos != virtualPos && !_frameCache[@(virtualPos)]) {
|
|
2460
|
+
// Frame not ready yet - skip this update
|
|
2461
|
+
if (!_stalled) {
|
|
2462
|
+
_stalled = YES;
|
|
2463
|
+
_stalls++;
|
|
2464
|
+
}
|
|
2465
|
+
static int notReadyCount = 0;
|
|
2466
|
+
if (notReadyCount++ < 20) {
|
|
2467
|
+
AVG_VLOG(@"Frame not ready - frame=%ld, vPos=%ld, slot=%ld has vPos=%ld, cacheSize=%lu, decoderPos=%ld",
|
|
2468
|
+
(long)self.currentFrame, (long)virtualPos, (long)gpuSlot, (long)slotVPos,
|
|
2469
|
+
(unsigned long)_frameCache.count, (long)_decoderPosition);
|
|
2470
|
+
}
|
|
2471
|
+
return;
|
|
2472
|
+
}
|
|
2473
|
+
_stalled = NO;
|
|
2474
|
+
NSInteger decodeVPos = (_decoderLoopIteration * self.totalFrames + _decoderPosition) % vPosModulus;
|
|
2475
|
+
_fewestAhead = MIN(_fewestAhead, (decodeVPos - virtualPos + vPosModulus) % vPosModulus - 1);
|
|
2476
|
+
}
|
|
2477
|
+
}
|
|
2478
|
+
|
|
2479
|
+
self.currentFrame++;
|
|
2480
|
+
|
|
2481
|
+
if (self.currentFrame >= self.totalFrames) {
|
|
2482
|
+
if (_streamingRingBufferMode && !_isUncompressedStreaming) {
|
|
2483
|
+
AVG_VLOG(@"Pass buffer: fewest frames ahead %ld of %ld, stalls %ld",
|
|
2484
|
+
(long)_fewestAhead, (long)self.streamingPrefetchAhead, (long)_stalls);
|
|
2485
|
+
_fewestAhead = self.totalFrames;
|
|
2486
|
+
_stalls = 0;
|
|
2487
|
+
}
|
|
2488
|
+
if (self.loop) {
|
|
2489
|
+
self.currentFrame = 0;
|
|
2490
|
+
if (_streamingRingBufferMode) {
|
|
2491
|
+
_playbackLoopIteration++;
|
|
2492
|
+
NSInteger nextVPos = (_playbackLoopIteration * self.totalFrames) % (self.totalFrames * 2);
|
|
2493
|
+
AVG_VLOG(@"Playback loop wrap-around: iteration %ld, nextVPos=%ld, decoderLoop=%ld, decoderPos=%ld, cacheSize=%lu",
|
|
2494
|
+
(long)_playbackLoopIteration, (long)nextVPos, (long)_decoderLoopIteration, (long)_decoderPosition, (unsigned long)_frameCache.count);
|
|
2495
|
+
// Log first few slot states
|
|
2496
|
+
for (int i = 0; self.verbose && i < MIN(4, _streamingRingBufferSize); i++) {
|
|
2497
|
+
AVG_VLOG(@" slot[%d] = vPos %ld", i, (long)[_ringSlots[i] integerValue]);
|
|
2498
|
+
}
|
|
2499
|
+
// Log if next frame is in cache
|
|
2500
|
+
AVG_VLOG(@" cache has nextVPos=%ld: %@", (long)nextVPos, _frameCache[@(nextVPos)] ? @"YES" : @"NO");
|
|
2501
|
+
|
|
2502
|
+
// CRITICAL: Immediately upload frame 0 of new loop to GPU before next render
|
|
2503
|
+
// This prevents showing stale frame from previous loop
|
|
2504
|
+
[self ensureFrameInGPU:nextVPos];
|
|
2505
|
+
}
|
|
2506
|
+
} else {
|
|
2507
|
+
// The last frame stays on screen
|
|
2508
|
+
self.currentFrame = self.totalFrames - 1;
|
|
2509
|
+
_ended = YES;
|
|
2510
|
+
self.isPlaying = NO;
|
|
2511
|
+
[self updateDisplayLink];
|
|
2512
|
+
[self notifyFinished];
|
|
2513
|
+
[self notifyStateChange:@"stopped"];
|
|
2514
|
+
return;
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2517
|
+
|
|
2518
|
+
if (self.enableFrameEvents) {
|
|
2519
|
+
[self notifyFrameUpdate];
|
|
2520
|
+
}
|
|
2521
|
+
[self setNeedsDisplay];
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2525
|
+
- (void)drawInMTKView:(MTKView *)view
|
|
2526
|
+
{
|
|
2527
|
+
static int drawCount = 0;
|
|
2528
|
+
|
|
2529
|
+
if (_textureArray && _arrayPipelineState) {
|
|
2530
|
+
if (drawCount < 5) {
|
|
2531
|
+
AVG_VLOG(@"drawInMTKView - rendering texture array frame %ld", (long)self.currentFrame);
|
|
2532
|
+
drawCount++;
|
|
2533
|
+
}
|
|
2534
|
+
|
|
2535
|
+
// Update layer uniform - use ring buffer slot for streaming mode
|
|
2536
|
+
float layer;
|
|
2537
|
+
if (_streamingRingBufferMode) {
|
|
2538
|
+
NSInteger gpuSlot = self.currentFrame % _streamingRingBufferSize;
|
|
2539
|
+
|
|
2540
|
+
// CRITICAL: Ensure the GPU slot has the correct frame before rendering
|
|
2541
|
+
// This handles the loop boundary case where the slot has stale data
|
|
2542
|
+
NSInteger vPosModulus = self.totalFrames * 2;
|
|
2543
|
+
NSInteger virtualPos = (_playbackLoopIteration * self.totalFrames + self.currentFrame) % vPosModulus;
|
|
2544
|
+
NSInteger slotVPos = [_ringSlots[gpuSlot] integerValue];
|
|
2545
|
+
|
|
2546
|
+
if (slotVPos != virtualPos) {
|
|
2547
|
+
// Slot has wrong frame - upload correct frame
|
|
2548
|
+
if (_isUncompressedStreaming) {
|
|
2549
|
+
if (!_useCompressedFormat && _decodedRgbaCache) {
|
|
2550
|
+
// Simulator: upload from decoded RGBA cache
|
|
2551
|
+
NSData *rgbaData;
|
|
2552
|
+
@synchronized(_decodedRgbaCache) {
|
|
2553
|
+
rgbaData = _decodedRgbaCache[@(virtualPos)];
|
|
2554
|
+
}
|
|
2555
|
+
if (rgbaData) {
|
|
2556
|
+
[self uploadDecodedFrameToGPU:virtualPos slot:gpuSlot];
|
|
2557
|
+
_ringSlots[gpuSlot] = @(virtualPos);
|
|
2558
|
+
}
|
|
2559
|
+
} else {
|
|
2560
|
+
// Real device: upload directly from mmap
|
|
2561
|
+
[self uploadUncompressedFrame:self.currentFrame toSlot:gpuSlot];
|
|
2562
|
+
_ringSlots[gpuSlot] = @(virtualPos);
|
|
2563
|
+
}
|
|
2564
|
+
} else {
|
|
2565
|
+
// Zstd: upload from cache
|
|
2566
|
+
NSData *frameData = _frameCache[@(virtualPos)];
|
|
2567
|
+
if (frameData) {
|
|
2568
|
+
[self uploadFrameToGPU:frameData.bytes slot:gpuSlot];
|
|
2569
|
+
_ringSlots[gpuSlot] = @(virtualPos);
|
|
2570
|
+
}
|
|
2571
|
+
}
|
|
2572
|
+
}
|
|
2573
|
+
|
|
2574
|
+
layer = (float)gpuSlot;
|
|
2575
|
+
} else {
|
|
2576
|
+
layer = (float)self.currentFrame;
|
|
2577
|
+
}
|
|
2578
|
+
memcpy([_arrayUniformBuffer contents], &layer, sizeof(float));
|
|
2579
|
+
|
|
2580
|
+
id<MTLCommandBuffer> commandBuffer = [self.commandQueue commandBuffer];
|
|
2581
|
+
MTLRenderPassDescriptor *renderPassDescriptor = view.currentRenderPassDescriptor;
|
|
2582
|
+
|
|
2583
|
+
if (renderPassDescriptor) {
|
|
2584
|
+
id<MTLRenderCommandEncoder> renderEncoder = [commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor];
|
|
2585
|
+
|
|
2586
|
+
[renderEncoder setRenderPipelineState:_arrayPipelineState];
|
|
2587
|
+
[renderEncoder setFragmentTexture:_textureArray atIndex:0];
|
|
2588
|
+
[renderEncoder setFragmentSamplerState:_samplerState atIndex:0];
|
|
2589
|
+
[renderEncoder setFragmentBuffer:_arrayUniformBuffer offset:0 atIndex:0];
|
|
2590
|
+
|
|
2591
|
+
[renderEncoder drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:6];
|
|
2592
|
+
[renderEncoder endEncoding];
|
|
2593
|
+
|
|
2594
|
+
[commandBuffer presentDrawable:view.currentDrawable];
|
|
2595
|
+
}
|
|
2596
|
+
|
|
2597
|
+
[commandBuffer commit];
|
|
2598
|
+
}
|
|
2599
|
+
}
|
|
2600
|
+
|
|
2601
|
+
- (void)mtkView:(MTKView *)view drawableSizeWillChange:(CGSize)size
|
|
2602
|
+
{
|
|
2603
|
+
// Handle view size changes if needed
|
|
2604
|
+
}
|
|
2605
|
+
|
|
2606
|
+
#pragma mark - Lifecycle
|
|
2607
|
+
|
|
2608
|
+
- (void)cleanup
|
|
2609
|
+
{
|
|
2610
|
+
AVG_VLOG(@"cleanup");
|
|
2611
|
+
_isDestroyed = YES;
|
|
2612
|
+
_isSourceLoaded = NO;
|
|
2613
|
+
_isSourceLoading = NO;
|
|
2614
|
+
_frameOwed = NO;
|
|
2615
|
+
_ended = NO;
|
|
2616
|
+
|
|
2617
|
+
// Stop playback
|
|
2618
|
+
self.isPlaying = NO;
|
|
2619
|
+
if (self.displayLink) {
|
|
2620
|
+
[self.displayLink invalidate];
|
|
2621
|
+
self.displayLink = nil;
|
|
2622
|
+
}
|
|
2623
|
+
|
|
2624
|
+
_textureArray = nil;
|
|
2625
|
+
|
|
2626
|
+
// Cleanup streaming state
|
|
2627
|
+
if (_dstream) {
|
|
2628
|
+
ZSTD_freeDStream(_dstream);
|
|
2629
|
+
_dstream = NULL;
|
|
2630
|
+
}
|
|
2631
|
+
_compressedData = nil;
|
|
2632
|
+
memset(&_zstdStream, 0, sizeof(_zstdStream));
|
|
2633
|
+
avg_zstd_seek_table_free(&_seekTable);
|
|
2634
|
+
_streamChunk = nil;
|
|
2635
|
+
memset(&_levelInput, 0, sizeof(_levelInput));
|
|
2636
|
+
_streamFailed = NO;
|
|
2637
|
+
// Reads still waiting on it, for a load in flight, fail; the load then drops them
|
|
2638
|
+
[_reader close];
|
|
2639
|
+
_reader = nil;
|
|
2640
|
+
_pendingSkipBytes = 0;
|
|
2641
|
+
_frameAccumulator = nil;
|
|
2642
|
+
_ringSlots = nil;
|
|
2643
|
+
_frameCache = nil;
|
|
2644
|
+
_streamingRingBufferMode = NO;
|
|
2645
|
+
|
|
2646
|
+
// Cleanup mmap. The simulator's ASTC decode blocks read the mapping on _astcDecodeQueue and
|
|
2647
|
+
// stop once _mmapData is NULL, so unmap after the block in flight, on the same serial queue.
|
|
2648
|
+
void *mmapData = _mmapData;
|
|
2649
|
+
size_t mmapSize = _mmapSize;
|
|
2650
|
+
int mmapFd = _mmapFd;
|
|
2651
|
+
_mmapData = NULL;
|
|
2652
|
+
_mmapFd = -1;
|
|
2653
|
+
_mmapSize = 0;
|
|
2654
|
+
NSData *heldFileData = _heldFileData;
|
|
2655
|
+
_heldFileData = nil;
|
|
2656
|
+
if (heldFileData) {
|
|
2657
|
+
// Not a mapping: released after the block in flight
|
|
2658
|
+
dispatch_async(_astcDecodeQueue, ^{
|
|
2659
|
+
(void)heldFileData;
|
|
2660
|
+
});
|
|
2661
|
+
} else if ((mmapData && mmapData != MAP_FAILED) || mmapFd >= 0) {
|
|
2662
|
+
dispatch_async(_astcDecodeQueue, ^{
|
|
2663
|
+
if (mmapData && mmapData != MAP_FAILED) munmap(mmapData, mmapSize);
|
|
2664
|
+
if (mmapFd >= 0) close(mmapFd);
|
|
2665
|
+
});
|
|
2666
|
+
}
|
|
2667
|
+
_isUncompressedStreaming = NO;
|
|
2668
|
+
|
|
2669
|
+
// Cleanup staging buffers for async blit
|
|
2670
|
+
_stagingBuffers[0] = nil;
|
|
2671
|
+
_stagingBuffers[1] = nil;
|
|
2672
|
+
_blitCommandQueue = nil;
|
|
2673
|
+
}
|
|
2674
|
+
|
|
2675
|
+
- (void)willMoveToSuperview:(UIView *)newSuperview
|
|
2676
|
+
{
|
|
2677
|
+
[super willMoveToSuperview:newSuperview];
|
|
2678
|
+
|
|
2679
|
+
if (newSuperview == nil) {
|
|
2680
|
+
// View is being removed - cleanup
|
|
2681
|
+
AVG_VLOG(@"willMoveToSuperview nil - cleaning up");
|
|
2682
|
+
[self cleanup];
|
|
2683
|
+
} else {
|
|
2684
|
+
// View is being added - restart if we have a source
|
|
2685
|
+
AVG_VLOG(@"willMoveToSuperview - view added");
|
|
2686
|
+
_isDestroyed = NO;
|
|
2687
|
+
|
|
2688
|
+
// If we have a source but it's not loaded or loading, reload it
|
|
2689
|
+
if (_source && !_isSourceLoaded && !_isSourceLoading) {
|
|
2690
|
+
AVG_VLOG(@"Restarting playback after view re-added");
|
|
2691
|
+
_isSourceLoading = YES;
|
|
2692
|
+
if (_localPath) {
|
|
2693
|
+
[self loadKTX2FromFile:_localPath];
|
|
2694
|
+
} else {
|
|
2695
|
+
[self resolveAndLoadSource];
|
|
2696
|
+
}
|
|
2697
|
+
}
|
|
2698
|
+
}
|
|
2699
|
+
}
|
|
2700
|
+
|
|
2701
|
+
- (void)didMoveToWindow
|
|
2702
|
+
{
|
|
2703
|
+
[super didMoveToWindow];
|
|
2704
|
+
|
|
2705
|
+
if (self.window == nil) {
|
|
2706
|
+
// View removed from window - pause but don't cleanup yet
|
|
2707
|
+
AVG_VLOG(@"didMoveToWindow nil - pausing");
|
|
2708
|
+
[self pause];
|
|
2709
|
+
} else if (_source && _isSourceLoaded && self.autoPlay && !self.playbackPaused && !self.isPlaying) {
|
|
2710
|
+
// View added to window and we have loaded source - resume
|
|
2711
|
+
AVG_VLOG(@"didMoveToWindow - resuming playback");
|
|
2712
|
+
[self play];
|
|
2713
|
+
}
|
|
2714
|
+
}
|
|
2715
|
+
|
|
2716
|
+
- (void)dealloc
|
|
2717
|
+
{
|
|
2718
|
+
AVG_VLOG(@"dealloc");
|
|
2719
|
+
[self cleanup];
|
|
2720
|
+
}
|
|
2721
|
+
|
|
2722
|
+
@end
|