GLCanvas.m 16.5 KB
Newer Older
1 2 3 4

#import "RCTBridge.h"
#import "RCTUtils.h"
#import "RCTConvert.h"
5
#import "RCTEventDispatcher.h"
6
#import "RCTLog.h"
Dima's avatar
Dima committed
7
#import "RCTProfile.h"
8
#import "RNGLContext.h"
9 10 11 12 13
#import "GLCanvas.h"
#import "GLShader.h"
#import "GLTexture.h"
#import "GLImage.h"
#import "GLRenderData.h"
14
#import "UIView+React.h"
15

Gaëtan Renaudeau's avatar
Gaëtan Renaudeau committed
16 17 18 19 20 21 22 23 24 25 26 27 28
NSString* srcResource (id res)
{
  NSString *src;
  if ([res isKindOfClass:[NSString class]]) {
    src = [RCTConvert NSString:res];
  } else {
    BOOL isStatic = [RCTConvert BOOL:res[@"isStatic"]];
    src = [RCTConvert NSString:res[@"path"]];
    if (!src || isStatic) src = [RCTConvert NSString:res[@"uri"]];
  }
  return src;
}

29 30 31 32 33 34 35 36 37 38
NSArray* diff (NSArray* a, NSArray* b) {
  NSMutableArray *arr = [[NSMutableArray alloc] init];
  for (NSString* k in a) {
    if (![b containsObject:k]) {
      [arr addObject:k];
    }
  }
  return arr;
}

39 40 41 42
// For reference, see implementation of gl-shader's GLCanvas

@implementation GLCanvas
{
43
  RCTBridge *_bridge;
44

45
  GLRenderData *_renderData;
46

47
  NSArray *_contentData;
48
  NSArray *_contentTextures;
49
  NSDictionary *_images; // This caches the currently used images (imageSrc -> GLReactImage)
50

51
  BOOL _opaque; // opaque prop (if false, the GLCanvas will become transparent)
52

53
  BOOL _deferredRendering; // This flag indicates a render has been deferred to the next frame (when using contents)
54

55
  GLint defaultFBO;
56

57
  NSMutableArray *_preloaded;
58 59
  BOOL _dirtyOnLoad;
  BOOL _neverRendered;
60

61
  NSTimer *animationTimer;
62

63
  BOOL _needSync;
64 65 66
  
  NSMutableArray *_captureConfigs;
  BOOL _captureScheduled;
67 68 69 70 71 72 73
}

- (instancetype)initWithBridge:(RCTBridge *)bridge
{
  if ((self = [super init])) {
    _bridge = bridge;
    _images = @{};
74
    _preloaded = [[NSMutableArray alloc] init];
75 76
    _captureConfigs = [[NSMutableArray alloc] init];
    _captureScheduled = false;
77 78
    _dirtyOnLoad = true;
    _neverRendered = true;
79
    self.context = [bridge.rnglContext getContext];
80 81 82 83 84 85
  }
  return self;
}

RCT_NOT_IMPLEMENTED(-init)

86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101
- (void)dealloc
{
  _bridge = nil;
  _images = nil;
  _preloaded = nil;
  _captureConfigs = nil;
  _contentData = nil;
  _contentTextures = nil;
  _data = nil;
  _renderData = nil;
  if (animationTimer) {
    [animationTimer invalidate];
    animationTimer = nil;
  }
}

Gaëtan Renaudeau's avatar
Gaëtan Renaudeau committed
102 103
//// Props Setters

104
- (void) requestCaptureFrame: (CaptureConfig *)config
105 106
{
  [self setNeedsDisplay];
107 108 109 110 111 112
  for (CaptureConfig *existing in _captureConfigs) {
    if ([existing isEqualToCaptureConfig:config]) {
      return;
    }
  }
  [_captureConfigs addObject:config];
113 114
}

115 116 117
-(void)setImagesToPreload:(NSArray *)imagesToPreload
{
  _imagesToPreload = imagesToPreload;
118
  [self requestSyncData];
119
}
120 121 122 123 124 125 126

- (void)setOpaque:(BOOL)opaque
{
  _opaque = opaque;
  [self setNeedsDisplay];
}

127 128
- (void)setRenderId:(NSNumber *)renderId
{
Gaëtan Renaudeau's avatar
Gaëtan Renaudeau committed
129
  if ([_nbContentTextures intValue] > 0) {
130 131 132 133
    [self setNeedsDisplay];
  }
}

134 135
- (void)setAutoRedraw:(BOOL)autoRedraw
{
136
  _autoRedraw = autoRedraw;
Gaëtan Renaudeau's avatar
Gaëtan Renaudeau committed
137 138 139 140 141 142
  [self performSelectorOnMainThread:@selector(syncAutoRedraw) withObject:nil waitUntilDone:false];
}

- (void)syncAutoRedraw
{
  if (_autoRedraw) {
143
    if (!animationTimer)
144
      animationTimer =
145 146
      [NSTimer scheduledTimerWithTimeInterval:1.0/60.0
                                       target:self
147
                                     selector:@selector(autoRedrawUpdate)
148 149 150 151 152 153 154 155 156 157
                                     userInfo:nil
                                      repeats:YES];
  }
  else {
    if (animationTimer) {
      [animationTimer invalidate];
    }
  }
}

158
- (void)setPointerEvents:(RCTPointerEvents)pointerEvents
159
{
160 161 162 163
  self.userInteractionEnabled = (pointerEvents != RCTPointerEventsNone);
  if (pointerEvents == RCTPointerEventsBoxNone) {
    self.accessibilityViewIsModal = NO;
  }
164 165
}

166 167 168 169 170 171
- (void)setPixelRatio:(NSNumber *)pixelRatio
{
  self.contentScaleFactor = [pixelRatio floatValue];
  [self setNeedsDisplay];
}

172 173 174
- (void)setData:(GLData *)data
{
  _data = data;
175
  _renderData = nil;
176 177 178
  [self requestSyncData];
}

Gaëtan Renaudeau's avatar
Gaëtan Renaudeau committed
179 180 181 182 183 184 185
- (void)setNbContentTextures:(NSNumber *)nbContentTextures
{
  _nbContentTextures = nbContentTextures;
}

//// Sync methods (called from props setters)

186 187
- (void)requestSyncData
{
188 189
  _needSync = true;
  [self setNeedsDisplay];
190 191
}

192
- (bool)syncData:(NSError **)error
193 194
{
  @autoreleasepool {
195

196 197
    NSDictionary *prevImages = _images;
    NSMutableDictionary *images = [[NSMutableDictionary alloc] init];
198

199 200 201
    GLRenderData * (^traverseTree) (GLData *data);
    __block __weak GLRenderData * (^weak_traverseTree)(GLData *data);
    weak_traverseTree = traverseTree = ^GLRenderData *(GLData *data) {
202 203
      NSNumber *width = data.width;
      NSNumber *height = data.height;
204
      NSNumber *pixelRatio = data.pixelRatio;
205
      int fboId = [data.fboId intValue];
206

207 208
      NSMutableArray *contextChildren = [[NSMutableArray alloc] init];
      for (GLData *child in data.contextChildren) {
209 210 211
        GLRenderData *node = weak_traverseTree(child);
        if (node == nil) return nil;
        [contextChildren addObject:node];
212
      }
213

214 215
      NSMutableArray *children = [[NSMutableArray alloc] init];
      for (GLData *child in data.children) {
216 217 218
        GLRenderData *node = weak_traverseTree(child);
        if (node == nil) return nil;
        [children addObject:node];
219
      }
220

221
      GLShader *shader = [_bridge.rnglContext getShader:data.shader];
222
      if (shader == nil) return nil;
223
      if (![shader ensureCompiles:error]) return nil;
224

225 226 227 228 229 230 231
      NSDictionary *uniformTypes = [shader uniformTypes];
      NSMutableDictionary *uniforms = [[NSMutableDictionary alloc] init];
      NSMutableDictionary *textures = [[NSMutableDictionary alloc] init];
      int units = 0;
      for (NSString *uniformName in data.uniforms) {
        id value = [data.uniforms objectForKey:uniformName];
        GLenum type = [uniformTypes[uniformName] intValue];
232 233


234
        if (type == GL_SAMPLER_2D || type == GL_SAMPLER_CUBE) {
235
          uniforms[uniformName] = [NSNumber numberWithInt:units++];
236
          if ([value isEqual:[NSNull null]]) {
237
            GLTexture *emptyTexture = [[GLTexture alloc] init];
238
            [emptyTexture setPixels:nil];
239
            textures[uniformName] = emptyTexture;
240
          }
241 242 243 244 245 246 247 248
          else {
            NSString *type = [RCTConvert NSString:value[@"type"]];
            if ([type isEqualToString:@"content"]) {
              int id = [[RCTConvert NSNumber:value[@"id"]] intValue];
              if (id >= [_contentTextures count]) {
                [self resizeUniformContentTextures:id+1];
              }
              textures[uniformName] = _contentTextures[id];
249
            }
250 251
            else if ([type isEqualToString:@"fbo"]) {
              NSNumber *id = [RCTConvert NSNumber:value[@"id"]];
252
              GLFBO *fbo = [_bridge.rnglContext getFBO:id];
253 254 255 256 257 258 259
              textures[uniformName] = fbo.color[0];
            }
            else if ([type isEqualToString:@"uri"]) {
              NSString *src = srcResource(value);
              if (!src) {
                RCTLogError(@"texture uniform '%@': Invalid uri format '%@'", uniformName, value);
              }
260

261 262 263 264 265 266 267
              GLImage *image = images[src];
              if (image == nil) {
                image = prevImages[src];
                if (image != nil)
                  images[src] = image;
              }
              if (image == nil) {
Gaëtan Renaudeau's avatar
Gaëtan Renaudeau committed
268
                __weak GLCanvas *weakSelf = self;
269
                image = [[GLImage alloc] initWithBridge:_bridge withOnLoad:^{
Gaëtan Renaudeau's avatar
Gaëtan Renaudeau committed
270
                  if (weakSelf) [weakSelf onImageLoad:src];
271 272
                }];
                image.src = src;
273
                images[src] = image;
274 275
              }
              textures[uniformName] = [image getTexture];
276
            }
277 278
            else {
              RCTLogError(@"texture uniform '%@': Unexpected type '%@'", uniformName, type);
279 280 281 282 283 284 285
            }
          }
        }
        else {
          uniforms[uniformName] = value;
        }
      }
286

287 288 289 290 291
      int maxTextureUnits;
      glGetIntegerv(GL_MAX_TEXTURE_IMAGE_UNITS, &maxTextureUnits);
      if (units > maxTextureUnits) {
        RCTLogError(@"Maximum number of texture reach. got %i >= max %i", units, maxTextureUnits);
      }
292

293 294 295 296 297
      for (NSString *uniformName in shader.uniformTypes) {
        if (uniforms[uniformName] == nil) {
          RCTLogError(@"All defined uniforms must be provided. Missing '%@'", uniformName);
        }
      }
298

299 300 301 302
      return [[GLRenderData alloc]
              initWithShader:shader
              withUniforms:uniforms
              withTextures:textures
303 304
              withWidth:(int)([width floatValue] * [pixelRatio floatValue])
              withHeight:(int)([height floatValue] * [pixelRatio floatValue])
305 306
              withFboId:fboId
              withContextChildren:contextChildren
307
              withChildren:children];
308
    };
309

310 311 312 313
    GLRenderData *res = traverseTree(_data);
    if (res != nil) {
      _renderData = traverseTree(_data);
      _images = images;
314 315 316
      for (NSString *src in diff([prevImages allKeys], [images allKeys])) {
        [_preloaded removeObject:src];
      }
317
      return true;
318
    }
Gaëtan Renaudeau's avatar
Gaëtan Renaudeau committed
319
    else {
320
      return false;
321
    }
322 323 324
  }
}

325
- (void)syncContentData
326
{
327
  RCT_PROFILE_BEGIN_EVENT(0, @"GLCanvas syncContentData", nil);
328 329 330 331 332
  NSMutableArray *contentData = [[NSMutableArray alloc] init];
  int nb = [_nbContentTextures intValue];
  for (int i = 0; i < nb; i++) {
    UIView *view = self.superview.subviews[i]; // We take siblings by index (closely related to the JS code)
    GLImageData *imgData = nil;
333
    if (view) {
334 335 336
      UIView *v = [view.subviews count] == 1 ?
      view.subviews[0] :
      view;
337
      imgData = [GLImageData genPixelsWithView:v withPixelRatio:self.contentScaleFactor];
338
    } else {
339
      imgData = nil;
340
    }
Gaëtan Renaudeau's avatar
Gaëtan Renaudeau committed
341
    if (imgData) contentData[i] = imgData;
342 343
  }
  _contentData = contentData;
344
  _deferredRendering = false;
345 346
  [self setNeedsDisplay];
  RCT_PROFILE_END_EVENT(0, @"gl", nil);
347 348 349 350 351 352 353 354
}


- (void)syncContentTextures
{
  unsigned long max = MIN([_contentData count], [_contentTextures count]);
  for (int i=0; i<max; i++) {
    [_contentTextures[i] setPixels:_contentData[i]];
355 356 357
  }
}

358 359 360 361 362 363 364 365 366 367
- (BOOL)haveRemainingToPreload
{
  for (id res in _imagesToPreload) {
    if (![_preloaded containsObject:srcResource(res)]) {
      return true;
    }
  }
  return false;
}

Gaëtan Renaudeau's avatar
Gaëtan Renaudeau committed
368 369 370

//// Draw

371
- (void) autoRedrawUpdate
372
{
373 374 375 376 377
  if ([self haveRemainingToPreload]) {
    return;
  }
  if ([_nbContentTextures intValue] > 0) {
    [self syncContentData];
378
  }
379 380 381
  [self setNeedsDisplay];
}

382 383
- (void)drawRect:(CGRect)rect
{
384
  self.layer.opaque = _opaque;
385

Gaëtan Renaudeau's avatar
Gaëtan Renaudeau committed
386 387
  if (_neverRendered) {
    _neverRendered = false;
388 389
    glClearColor(0.0, 0.0, 0.0, 0.0);
    glClear(GL_COLOR_BUFFER_BIT);
Gaëtan Renaudeau's avatar
Gaëtan Renaudeau committed
390
  }
391

392
  if (_needSync) {
393
    NSError *error;
394 395 396 397 398
    BOOL syncSuccessful = [self syncData:&error];
    BOOL errorCanBeRecovered = error==nil || (error.code != GLLinkingFailure && error.code != GLCompileFailure);
    if (!syncSuccessful && errorCanBeRecovered) {
      // something failed but is recoverable, retry in one tick
      [self performSelectorOnMainThread:@selector(setNeedsDisplay) withObject:nil waitUntilDone:NO];
399 400 401 402
    }
    else {
      _needSync = false;
    }
403
  }
404

405
  if ([self haveRemainingToPreload]) {
406 407
    return;
  }
408

409
  bool willRender = !_deferredRendering;
410

411
  if ([_nbContentTextures intValue] > 0 && !_autoRedraw) {
Gaëtan Renaudeau's avatar
Gaëtan Renaudeau committed
412
    _deferredRendering = true;
413
    [self performSelectorOnMainThread:@selector(syncContentData) withObject:nil waitUntilDone:NO];
414
  }
415

416
  if (willRender) {
417
    [self render];
418 419
    if (!_captureScheduled && [_captureConfigs count] > 0) {
      _captureScheduled = true;
Gaëtan Renaudeau's avatar
Gaëtan Renaudeau committed
420
      [self performSelectorOnMainThread:@selector(capture) withObject:nil waitUntilDone:NO];
421
    }
422 423 424
  }
}

425
-(void) capture
Gaëtan Renaudeau's avatar
Gaëtan Renaudeau committed
426
{
427 428 429
  _captureScheduled = false;
  if (!self.onGLCaptureFrame) return;
  
Gaëtan Renaudeau's avatar
Gaëtan Renaudeau committed
430
  UIImage *frameImage = [self snapshot];
431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474
  
  for (CaptureConfig *config in _captureConfigs) {
    id result;
    id error;
    
    BOOL isPng = [config.type isEqualToString:@"png"];
    BOOL isJpeg = !isPng && ([config.type isEqualToString:@"jpeg"] || [config.type isEqualToString:@"jpg"]);
    
    BOOL isBase64 = [config.format isEqualToString:@"base64"];
    BOOL isFile = !isBase64 && [config.format isEqualToString:@"file"];
    
    NSData *frameData =
    isPng ? UIImagePNGRepresentation(frameImage) :
    isJpeg ? UIImageJPEGRepresentation(frameImage, [config.quality floatValue]) :
    nil;
    
    if (!frameData) {
      error = [NSString stringWithFormat:@"Unsupported capture type '%@'", config.type];
    }
    else if (isBase64) {
      NSString *base64 = [frameData base64EncodedStringWithOptions: NSDataBase64Encoding64CharacterLineLength];
      result = [NSString stringWithFormat:@"data:image/%@;base64,%@", config.type, base64];
    }
    else if (isFile) {
      NSError *e;
      if (![frameData writeToFile:config.filePath options:0 error:&e]) {
        error = [NSString stringWithFormat:@"Could not write file: %@", e.localizedDescription];
      }
      else {
        result = [NSString stringWithFormat:@"file://%@", config.filePath];
      }
    }
    else {
      error = [NSString stringWithFormat:@"Unsupported capture format '%@'", config.format];
    }
    
    NSMutableDictionary *response = [[NSMutableDictionary alloc] init];
    response[@"config"] = [config dictionary];
    if (error) response[@"error"] = error;
    if (result) response[@"result"] = result;
    self.onGLCaptureFrame(response);
  }
  
  _captureConfigs = [[NSMutableArray alloc] init];
Gaëtan Renaudeau's avatar
Gaëtan Renaudeau committed
475 476
}

477
- (void)render
478
{
479 480
  GLRenderData *rd = _renderData;
  if (!rd) return;
Gaëtan Renaudeau's avatar
Gaëtan Renaudeau committed
481
  RCT_PROFILE_BEGIN_EVENT(0, @"GLCanvas render", nil);
482

483
  @autoreleasepool {
484

485 486 487
    void (^recDraw) (GLRenderData *renderData);
    __block __weak void (^weak_recDraw) (GLRenderData *renderData);
    weak_recDraw = recDraw = ^void(GLRenderData *renderData) {
488 489
      int w = renderData.width;
      int h = renderData.height;
490

491 492
      for (GLRenderData *child in renderData.contextChildren)
        weak_recDraw(child);
493

494
      for (GLRenderData *child in renderData.children)
495
        weak_recDraw(child);
496

497
      if (renderData.fboId == -1) {
498 499 500 501
        glBindFramebuffer(GL_FRAMEBUFFER, defaultFBO);
        glViewport(0, 0, w, h);
      }
      else {
502
        GLFBO *fbo = [_bridge.rnglContext getFBO:[NSNumber numberWithInt:renderData.fboId]];
503 504 505
        [fbo setShapeWithWidth:w withHeight:h];
        [fbo bind];
      }
506

507
      [renderData.shader bind];
508

509 510 511 512 513
      for (NSString *uniformName in renderData.textures) {
        GLTexture *texture = renderData.textures[uniformName];
        int unit = [((NSNumber *)renderData.uniforms[uniformName]) intValue];
        [texture bind:unit];
      }
514

515 516 517
      for (NSString *uniformName in renderData.uniforms) {
        [renderData.shader setUniform:uniformName withValue:renderData.uniforms[uniformName]];
      }
518

519
      glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
520 521
      glClearColor(0.0, 0.0, 0.0, 0.0);
      glClear(GL_COLOR_BUFFER_BIT);
522 523
      glDrawArrays(GL_TRIANGLES, 0, 6);
    };
524

525
    // DRAWING THE SCENE
526

527
    [self syncContentTextures];
528

529
    glGetIntegerv(GL_FRAMEBUFFER_BINDING, &defaultFBO);
530
    glEnable(GL_BLEND);
531
    recDraw(rd);
532
    glDisable(GL_BLEND);
533
    glBindFramebuffer(GL_FRAMEBUFFER, defaultFBO);
534
    glBindBuffer(GL_ARRAY_BUFFER, 0);
535

536 537 538 539
    if (_dirtyOnLoad && ![self haveRemainingToPreload]) {
      _dirtyOnLoad = false;
      [self dispatchOnLoad];
    }
540
  }
541

Gaëtan Renaudeau's avatar
Gaëtan Renaudeau committed
542
  RCT_PROFILE_END_EVENT(0, @"gl", nil);
543 544
}

Gaëtan Renaudeau's avatar
Gaëtan Renaudeau committed
545 546 547 548
//// utility methods

- (void)onImageLoad:(NSString *)loaded
{
549 550 551 552 553 554 555
  [_preloaded addObject:loaded];
  int count = [self countPreloaded];
  int total = (int) [_imagesToPreload count];
  double progress = ((double) count) / ((double) total);
  [self dispatchOnProgress:progress withLoaded:count withTotal:total];
  _dirtyOnLoad = true;
  [self requestSyncData];
Gaëtan Renaudeau's avatar
Gaëtan Renaudeau committed
556 557 558 559 560 561 562 563 564 565 566
}

- (int)countPreloaded
{
  int nb = 0;
  for (id toload in _imagesToPreload) {
    if ([_preloaded containsObject:srcResource(toload)])
      nb++;
  }
  return nb;
}
567

Gaëtan Renaudeau's avatar
Gaëtan Renaudeau committed
568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585
- (void)resizeUniformContentTextures:(int)n
{
  int length = (int) [_contentTextures count];
  if (length == n) return;
  if (n < length) {
    _contentTextures = [_contentTextures subarrayWithRange:NSMakeRange(0, n)];
  }
  else {
    NSMutableArray *contentTextures = [[NSMutableArray alloc] initWithArray:_contentTextures];
    for (int i = (int) [_contentTextures count]; i < n; i++) {
      [contentTextures addObject:[[GLTexture alloc] init]];
    }
    _contentTextures = contentTextures;
  }
}

- (void)dispatchOnLoad
{
586
  if (self.onGLLoad) self.onGLLoad(@{});
Gaëtan Renaudeau's avatar
Gaëtan Renaudeau committed
587 588 589 590
}

- (void)dispatchOnProgress: (double)progress withLoaded:(int)loaded withTotal:(int)total
{
591
  if (self.onGLProgress) self.onGLProgress(
Gaëtan Renaudeau's avatar
Gaëtan Renaudeau committed
592 593 594 595 596
                                           @{
                                             @"progress": @(RCTZeroIfNaN(progress)),
                                             @"loaded": @(RCTZeroIfNaN(loaded)),
                                             @"total": @(RCTZeroIfNaN(total))
                                             });
597 598
}

Gaëtan Renaudeau's avatar
Gaëtan Renaudeau committed
599
@end