Search code examples
iosswiftmetal

Metal only draws to a portion of the view and inquiry about layers


I am following the setup for a basic metal tutorial and am trying to render a blank screen of uniform color.

For some reason I am getting this issue where Metal only draws to a part of the screen.

I set up a single view application and attached the MainStoryboard's view to a class I am creating. I set its background color to green.

Here is the class

class MBEMetalView: UIView {

    var device:MTLDevice?
    var MTLLayer:CAMetalLayer?

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)

        device = MTLCreateSystemDefaultDevice()

        //The book said to cast the layer to a CAMetalLayer 
        //but that causes a crash in the redraw method because
        //failed assertion `No rendertargets set in RenderPassDescriptor.'
        //So we do this.. which might be wasteful?
        MTLLayer = CAMetalLayer()
        MTLLayer?.device = device
        MTLLayer?.pixelFormat = MTLPixelFormat.bgra8Unorm
        MTLLayer?.framebufferOnly = true
        MTLLayer?.frame = frame
        layer.addSublayer(MTLLayer!)
    }

    override func didMoveToWindow() {
        redraw()
    }

    func redraw() {
        var drawable = MTLLayer?.nextDrawable()
        var texture = drawable?.texture

        var passDescriptor = MTLRenderPassDescriptor()
        passDescriptor.colorAttachments[0].texture = texture
        passDescriptor.colorAttachments[0].loadAction = MTLLoadAction.clear
        passDescriptor.colorAttachments[0].storeAction = MTLStoreAction.store
        passDescriptor.colorAttachments[0].clearColor = MTLClearColorMake(0, 1, 0, 1)


        var commandQueue = device?.makeCommandQueue()
        var commandBuffer = commandQueue?.makeCommandBuffer()
        var commandEncoder = commandBuffer?.makeRenderCommandEncoder(descriptor: passDescriptor)
        commandEncoder?.endEncoding()

        if let drawOn = drawable {
            commandBuffer?.present(drawOn)
            commandBuffer?.commit()
        }
    }

}

The class should have rendered the entire screen green however this is not the case it only fills up a portion as shown by this picture from an iPhone 6s plus (Red is the background of the view not what Metal is drawing). enter image description here

This result brings to mind a few questions:

  1. Why is Metal only rendering to a portion of the screen
  2. The tutorial I am following is doing it in Objective-C they were able to cast the layer of the inherited UIView as a CAMetal layer rather than creating a new layer (this allowed them to remove the .addSublayer line). However when I try this I get a crash that is shown in the comments? Why is this?
  3. How will the frame passed in be effected by devices with a safe area like an iPhone X? Will I have to something extra to draw outside of the safe area? Where can I get the dimensions of the safe area from my view's frame programmatically (incase new devices are released)

Solution

  • You set the CAMetalLayer's frame in init(coder) and never change it. But in init(coder) the final frame size is not known yet (it's just using whatever size you set in Interface Builder at this point but not the device size yet). In didMoveToWindow(), also set the frame of the Metal layer to its final size. (Or perhaps in layoutSubviews().)