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