forked from expo/expo
/
EXGLContext.m
296 lines (239 loc) · 10.1 KB
/
EXGLContext.m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
// Copyright 2016-present 650 Industries. All rights reserved.
#import <EXGL/EXGLContext.h>
#import <EXGL/EXGLObjectManager.h>
#import <UMCore/UMUtilities.h>
#import <UMCore/UMUIManager.h>
#import <UMCore/UMJavaScriptContextProvider.h>
#import <UMFileSystemInterface/UMFileSystemInterface.h>
#include <OpenGLES/ES3/gl.h>
#include <OpenGLES/ES3/glext.h>
#define BLOCK_SAFE_RUN(block, ...) block ? block(__VA_ARGS__) : nil
@interface EXGLContext ()
@property (nonatomic, strong) dispatch_queue_t glQueue;
@property (nonatomic, weak) UMModuleRegistry *moduleRegistry;
@property (nonatomic, weak) EXGLObjectManager *objectManager;
@end
@implementation EXGLContext
- (instancetype)initWithDelegate:(id<EXGLContextDelegate>)delegate andModuleRegistry:(nonnull UMModuleRegistry *)moduleRegistry
{
if (self = [super init]) {
self.delegate = delegate;
_moduleRegistry = moduleRegistry;
_objectManager = (EXGLObjectManager *)[_moduleRegistry getExportedModuleOfClass:[EXGLObjectManager class]];
_glQueue = dispatch_queue_create("host.exp.gl", DISPATCH_QUEUE_SERIAL);
_eaglCtx = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3] ?: [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
}
return self;
}
- (BOOL)isInitialized
{
return _contextId != 0;
}
- (EAGLContext *)createSharedEAGLContext
{
return [[EAGLContext alloc] initWithAPI:[_eaglCtx API] sharegroup:[_eaglCtx sharegroup]];
}
- (void)runInEAGLContext:(EAGLContext*)context callback:(void(^)(void))callback
{
[EAGLContext setCurrentContext:context];
callback();
glFlush();
[EAGLContext setCurrentContext:nil];
}
- (void)runAsync:(void(^)(void))callback
{
if (_glQueue) {
dispatch_async(_glQueue, ^{
[self runInEAGLContext:self->_eaglCtx callback:callback];
});
}
}
- (void)initialize:(void(^)(BOOL))callback
{
id<UMUIManager> uiManager = [_moduleRegistry getModuleImplementingProtocol:@protocol(UMUIManager)];
id<UMJavaScriptContextProvider> jsContextProvider = [_moduleRegistry getModuleImplementingProtocol:@protocol(UMJavaScriptContextProvider)];
JSGlobalContextRef jsContextRef = [jsContextProvider javaScriptContextRef];
if (jsContextRef) {
__weak __typeof__(self) weakSelf = self;
__weak __typeof__(uiManager) weakUIManager = uiManager;
[uiManager dispatchOnClientThread:^{
EXGLContext *self = weakSelf;
id<UMUIManager> uiManager = weakUIManager;
if (!self || !uiManager) {
BLOCK_SAFE_RUN(callback, NO);
return;
}
self->_contextId = UEXGLContextCreate(jsContextRef);
[self->_objectManager saveContext:self];
UEXGLContextSetFlushMethodObjc(self->_contextId, ^{
[self flush];
});
if ([self.delegate respondsToSelector:@selector(glContextInitialized:)]) {
[self.delegate glContextInitialized:self];
}
BLOCK_SAFE_RUN(callback, YES);
}];
} else {
BLOCK_SAFE_RUN(callback, NO);
UMLogWarn(@"EXGL: Can only run on JavaScriptCore! Do you have 'Remote Debugging' enabled in your app's Developer Menu (https://facebook.github.io/react-native/docs/debugging.html)? EXGL is not supported while using Remote Debugging, you will need to disable it to use EXGL.");
}
}
- (void)flush
{
[self runAsync:^{
UEXGLContextFlush(self->_contextId);
if ([self.delegate respondsToSelector:@selector(glContextFlushed:)]) {
[self.delegate glContextFlushed:self];
}
}];
}
- (void)destroy
{
[self runAsync:^{
if ([self.delegate respondsToSelector:@selector(glContextWillDestroy:)]) {
[self.delegate glContextWillDestroy:self];
}
// Flush all the stuff
UEXGLContextFlush(self->_contextId);
// Destroy JS binding
UEXGLContextDestroy(self->_contextId);
// Remove from dictionary of contexts
[self->_objectManager deleteContextWithId:@(self->_contextId)];
}];
}
# pragma mark - snapshots
// Saves the contents of the framebuffer to a file.
// Possible options:
// - `flip`: if true, the image will be flipped vertically.
// - `framebuffer`: WebGLFramebuffer that we will be reading from. If not specified, the default framebuffer for this context will be used.
// - `rect`: { x, y, width, height } object used to crop the snapshot.
// - `format`: "jpeg" or "png" - specifies what type of compression and file extension should be used.
// - `compress`: A value in 0 - 1 range specyfing compression level. JPEG format only.
- (void)takeSnapshotWithOptions:(nonnull NSDictionary *)options
resolve:(UMPromiseResolveBlock)resolve
reject:(UMPromiseRejectBlock)reject
{
[self flush];
[self runAsync:^{
NSDictionary *rect = options[@"rect"] ?: [self currentViewport];
BOOL flip = options[@"flip"] != nil && [options[@"flip"] boolValue];
NSString *format = options[@"format"];
int x = [rect[@"x"] intValue];
int y = [rect[@"y"] intValue];
int width = [rect[@"width"] intValue];
int height = [rect[@"height"] intValue];
// Save surrounding framebuffer
GLint prevFramebuffer;
glGetIntegerv(GL_FRAMEBUFFER_BINDING, &prevFramebuffer);
// Set source framebuffer that we take snapshot from
GLint sourceFramebuffer = 0;
if (options[@"framebuffer"] && options[@"framebuffer"][@"id"]) {
int exglFramebufferId = [options[@"framebuffer"][@"id"] intValue];
sourceFramebuffer = UEXGLContextGetObject(self.contextId, exglFramebufferId);
} else {
// headless context doesn't have default framebuffer, so we use the current one
sourceFramebuffer = [self defaultFramebuffer] || prevFramebuffer;
}
if (sourceFramebuffer == 0) {
reject(
@"E_GL_NO_FRAMEBUFFER",
nil,
UMErrorWithMessage(@"No framebuffer bound. Create and bind one to take a snapshot from it.")
);
return;
}
if (width <= 0 || height <= 0) {
reject(
@"E_GL_INVALID_VIEWPORT",
nil,
UMErrorWithMessage(@"Rect's width and height must be greater than 0. If you didn't set `rect` option, check if the viewport is set correctly.")
);
return;
}
// Bind source framebuffer
glBindFramebuffer(GL_FRAMEBUFFER, sourceFramebuffer);
// Allocate pixel buffer and read pixels
NSInteger dataLength = width * height * 4;
GLubyte *buffer = (GLubyte *) malloc(dataLength * sizeof(GLubyte));
glReadBuffer(GL_COLOR_ATTACHMENT0);
glReadPixels(x, y, width, height, GL_RGBA, GL_UNSIGNED_BYTE, buffer);
// Create CGImage
CGDataProviderRef providerRef = CGDataProviderCreateWithData(NULL, buffer, dataLength, NULL);
CGColorSpaceRef colorspaceRef = CGColorSpaceCreateDeviceRGB();
CGImageRef imageRef = CGImageCreate(width, height, 8, 32, width * 4, colorspaceRef, kCGBitmapByteOrder32Big | kCGImageAlphaPremultipliedLast,
providerRef, NULL, true, kCGRenderingIntentDefault);
// Begin image context
CGFloat scale = [UMUtilities screenScale];
NSInteger widthInPoints = width / scale;
NSInteger heightInPoints = height / scale;
UIGraphicsBeginImageContextWithOptions(CGSizeMake(widthInPoints, heightInPoints), NO, scale);
// Flip and draw image to CGImage
CGContextRef cgContext = UIGraphicsGetCurrentContext();
if (flip) {
CGAffineTransform flipVertical = CGAffineTransformMake(1, 0, 0, -1, 0, heightInPoints);
CGContextConcatCTM(cgContext, flipVertical);
}
CGContextDrawImage(cgContext, CGRectMake(0.0, 0.0, widthInPoints, heightInPoints), imageRef);
// Retrieve the UIImage from the current context
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
// Cleanup
free(buffer);
CFRelease(providerRef);
CFRelease(colorspaceRef);
CGImageRelease(imageRef);
// Write image to file
NSData *imageData;
NSString *extension;
if ([format isEqualToString:@"webp"]) {
UMLogWarn(@"iOS doesn't support 'webp' representation, so 'takeSnapshot' won't work with that format. The image is going to be exported as 'png', but consider using a different code for iOS. Check this docs to learn how to do platform specific code (https://reactnative.dev/docs/platform-specific-code)");
imageData = UIImagePNGRepresentation(image);
extension = @".png";
}
else if ([format isEqualToString:@"png"]) {
imageData = UIImagePNGRepresentation(image);
extension = @".png";
} else {
float compress = 1.0;
if (options[@"compress"] != nil) {
compress = [(NSString *)options[@"compress"] floatValue];
}
imageData = UIImageJPEGRepresentation(image, compress);
extension = @".jpeg";
}
NSString *filePath = [self generateSnapshotPathWithExtension:extension];
[imageData writeToFile:filePath atomically:YES];
// Restore surrounding framebuffer
glBindFramebuffer(GL_FRAMEBUFFER, prevFramebuffer);
// Return result object which imitates Expo.Asset so it can be used again to fill the texture
NSMutableDictionary *result = [[NSMutableDictionary alloc] init];
NSString *fileUrl = [[NSURL fileURLWithPath:filePath] absoluteString];
result[@"uri"] = fileUrl;
result[@"localUri"] = fileUrl;
result[@"width"] = @(width);
result[@"height"] = @(height);
resolve(result);
}];
}
- (NSDictionary *)currentViewport
{
GLint viewport[4];
glGetIntegerv(GL_VIEWPORT, viewport);
return @{ @"x": @(viewport[0]), @"y": @(viewport[1]), @"width": @(viewport[2]), @"height": @(viewport[3]) };
}
- (GLint)defaultFramebuffer
{
if ([self.delegate respondsToSelector:@selector(glContextGetDefaultFramebuffer)]) {
return [self.delegate glContextGetDefaultFramebuffer];
}
return 0;
}
- (NSString *)generateSnapshotPathWithExtension:(NSString *)extension
{
id<UMFileSystemInterface> fileSystem = [_moduleRegistry getModuleImplementingProtocol:@protocol(UMFileSystemInterface)];
NSString *directory = [fileSystem.cachesDirectory stringByAppendingPathComponent:@"GLView"];
NSString *fileName = [[[NSUUID UUID] UUIDString] stringByAppendingString:extension];
[fileSystem ensureDirExistsWithPath:directory];
return [directory stringByAppendingPathComponent:fileName];
}
@end