MetalDeferredLighting/AAPLView.mm

/*
 Copyright (C) 2014 Apple Inc. All Rights Reserved.
 See LICENSE.txt for this sample’s licensing information
 
 Abstract:
 
  View for Deferred lighting Metal Sample Code. Manages  framebuffers and expects a delegate to repond to render commands to perform drawing. Can be configured with 4 color attachments, depth and stencil attachments.
  
 */
 
#import "AAPLView.h"
 
@implementation AAPLView
{
@private
    __weak CAMetalLayer *_metalLayer;
    
    BOOL _layerSizeDidUpdate;
    
    id <MTLTexture>  _depthTex;
    id <MTLTexture>  _stencilTex;
    id <MTLTexture>  _colorTextures[3]; // these are for textures 1-3 (as needed), texture 0 is owned by the drawable
}
 
@synthesize currentDrawable    = _currentDrawable;
@synthesize renderPassDescriptor = _renderPassDescriptor;
 
+ (Class)layerClass
{
    return [CAMetalLayer class];
}
 
- (void)initCommon
{
    self.opaque          = YES;
    self.backgroundColor = nil;
    
    // setting this to yes will allow Main thread to display framebuffer when
    // view:setNeedDisplay: is called by main thread
    _metalLayer = (CAMetalLayer *)self.layer;
    
    _device = MTLCreateSystemDefaultDevice();
    
    _metalLayer.device          = _device;
    _metalLayer.pixelFormat     = MTLPixelFormatBGRA8Unorm;
    
    [self updateDrawableSize];
}
 
- (void)didMoveToWindow
{
    self.contentScaleFactor = self.window.screen.nativeScale;
}
 
- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    
    if(self)
    {
        [self initCommon];
    }
    
    return self;
}
 
- (instancetype)initWithCoder:(NSCoder *)coder
{
    self = [super initWithCoder:coder];
    
    if(self)
    {
        [self initCommon];
    }
    return self;
}
 
- (void)releaseTextures
{
    _depthTex   = nil;
    _stencilTex = nil;
    
    for (int i = 0; i < 3; i++)
        _colorTextures[i] = nil;
}
 
 
- (MTLRenderPassColorAttachmentDescriptor *)colorAttachmentDescriptorWithTexture: (id <MTLTexture>) texture
                                                             clearColor: (MTLClearColor) clearColor
                                                             loadAction: (MTLLoadAction) loadAction
                                                            storeAction: (MTLStoreAction) storeAction
{
    MTLRenderPassColorAttachmentDescriptor *attachment = [MTLRenderPassColorAttachmentDescriptor new];
    attachment.texture = texture;
    attachment.loadAction = loadAction;
    attachment.storeAction = storeAction;
    attachment.clearColor = clearColor;
 
    return attachment;
}
 
- (void)setupRenderPassDescriptorForTexture:(id <MTLTexture>) texture
{
    // create renderpass descriptor lazily when we first need it
    if (_renderPassDescriptor == nil)
        _renderPassDescriptor = [MTLRenderPassDescriptor renderPassDescriptor];
    
    // set up the first color attachment at index 0.
    // This will be the attachment we present to the screen so set its store action to store.
    MTLRenderPassColorAttachmentDescriptor *colorAttachment0 = [self colorAttachmentDescriptorWithTexture:texture
                                                                                     clearColor:colorAttachmentClearValue[0]
                                                                                     loadAction:MTLLoadActionClear
                                                                                    storeAction:MTLStoreActionStore];
    [_renderPassDescriptor.colorAttachments setObject:colorAttachment0 atIndexedSubscript:0];
 
    // we only need to update the other attachments if something has changed (ie, rotation or layer changed size)
    BOOL doUpdate = (!_colorTextures[0]) || ( _colorTextures[0].width != texture.width  ) ||  (  _colorTextures[0].height != texture.height );
    if (doUpdate)
    {
        // color attachments 1..3 will not be presented and should be discarded so set their store action to dont care
        for (int i = 1; i <= 3; i++)
        if (colorAttachmentFormat[i] != MTLPixelFormatInvalid)
        {
            // color format 0 is only used by the drawable so we skip it here
            MTLTextureDescriptor* desc = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat: colorAttachmentFormat[i]
                                                                                            width: texture.width
                                                                                           height: texture.height
                                                                                        mipmapped: NO];
            _colorTextures[i-1] = [_device newTextureWithDescriptor: desc];
            
            MTLRenderPassColorAttachmentDescriptor *colorAttachment = [self colorAttachmentDescriptorWithTexture:_colorTextures[i-1]
                                                                                            clearColor:colorAttachmentClearValue[i]
                                                                                            loadAction:MTLLoadActionClear
                                                                                           storeAction:MTLStoreActionDontCare];
            [_renderPassDescriptor.colorAttachments setObject:colorAttachment atIndexedSubscript:i];
        }
    }
    
    if(depthPixelFormat != MTLPixelFormatInvalid)
    {
        BOOL doUpdate = ( _depthTex.width != texture.width  ) ||  ( _depthTex.height != texture.height );
        if(!_depthTex || doUpdate)
        {
            //  If we need a depth texture and don't have one, or if the depth texture we have is the wrong size
            //  Then allocate one of the proper size
            MTLTextureDescriptor* desc = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat: depthPixelFormat
                                                                                            width: texture.width
                                                                                           height: texture.height
                                                                                        mipmapped: NO];
            desc.textureType = MTLTextureType2D;
            _depthTex = [_device newTextureWithDescriptor: desc];
            
            MTLRenderPassDepthAttachmentDescriptor *depthAttachment = _renderPassDescriptor.depthAttachment;
            depthAttachment.texture = _depthTex;
            depthAttachment.loadAction = MTLLoadActionClear;
            depthAttachment.storeAction = MTLStoreActionDontCare;
            depthAttachment.clearDepth = 1.0;
        }
    }
    
    if(stencilPixelFormat != MTLPixelFormatInvalid)
    {
        BOOL doUpdate  =  ( _stencilTex.width != texture.width  ) || ( _stencilTex.height != texture.height );
        if(!_stencilTex || doUpdate)
        {
            //  If we need a stencil texture and don't have one, or if the depth texture we have is the wrong size
            //  Then allocate one of the proper size
            
            MTLTextureDescriptor* desc = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat: stencilPixelFormat
                                                                                            width: texture.width
                                                                                           height: texture.height
                                                                                        mipmapped: NO];
            
            desc.textureType =  MTLTextureType2D;
            _stencilTex = [_device newTextureWithDescriptor: desc];
            
            MTLRenderPassStencilAttachmentDescriptor* stencilAttachment = _renderPassDescriptor.stencilAttachment;
            stencilAttachment.texture = _stencilTex;
            stencilAttachment.loadAction = MTLLoadActionClear;
            stencilAttachment.storeAction = MTLStoreActionDontCare;
            stencilAttachment.clearStencil = 0;
        }
    }
}
 
- (MTLRenderPassDescriptor *)renderPassDescriptor
{
    id <CAMetalDrawable> drawable = self.currentDrawable;
    if(!drawable)
    {
        NSLog(@">> ERROR: Failed to get a drawable!");
        _renderPassDescriptor = nil;
    }
    else
    {
        [self setupRenderPassDescriptorForTexture: drawable.texture];
    }
    
    return _renderPassDescriptor;
}
 
- (id <CAMetalDrawable>)currentDrawable
{
    if (_currentDrawable == nil)
    {
        _currentDrawable = [_metalLayer nextDrawable];
        
        if(!_currentDrawable)
        {
            NSLog(@"CurrentDrawable is nil");
        }
    }
    
    return _currentDrawable;
}
 
- (void)updateDrawableSize
{
    CGSize drawableSize = self.bounds.size;
    drawableSize.width  *= self.contentScaleFactor;
    drawableSize.height *= self.contentScaleFactor;
    _metalLayer.drawableSize = drawableSize;
}
 
- (void)display
{
    // Create autorelease pool per frame to avoid possible deadlock situations
    // because there are 3 CAMetalDrawables sitting in an autorelease pool.
    @autoreleasepool
    {
        if(_layerSizeDidUpdate)
        {
            [self updateDrawableSize];
                        
            [_delegate reshape:self];
            
            _layerSizeDidUpdate = NO;
        }
        
        // draw
        [self.delegate render:self];
        
        _currentDrawable    = nil;
    }
}
 
- (void)setContentScaleFactor:(CGFloat)contentScaleFactor
{
    [super setContentScaleFactor:contentScaleFactor];
    
    _layerSizeDidUpdate = YES;
}
 
- (void)layoutSubviews
{
    [super layoutSubviews];
    
    _layerSizeDidUpdate = YES;
}
 
@end