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