swiftmacoscore-animationmetalmtkview

Resizing MTKView scales old content before redraw


I'm using a MTKView to draw Metal content. It's configured as follows:

mtkView = MTKView(frame: self.view.frame, device: device)
mtkView.colorPixelFormat = .bgra8Unorm
mtkView.delegate = self
mtkView.sampleCount = 4
mtkView.isPaused = true
mtkView.enableSetNeedsDisplay = true

setFrameSize is overriden to trigger a redisplay.

Whenever the view resizes it scales its old content before it redraws everything. This gives a jittering feeling.

I tried setting the contentGravity property of the MTKView's layer to a non-resizing value, but that totally messes up the scale and position of the content. It seems MTKView doesn't want me to fiddle with that parameter.

How can I make sure that during a resize the content is always properly redrawn?


Solution

  • In my usage of Metal and MTKView, I tried various combinations of presentsWithTransaction and waitUntilScheduled without success. I still experienced occasional frames of stretched content in between frames of properly rendered content during live resize.

    Finally, I dropped MTKView altogether and made my own NSView subclass that uses CAMetalLayer and resize looks good now (without any use of presentsWithTransaction or waitUntilScheduled). One key bit is that I needed to set the layer's autoresizingMask to get the displayLayer method to be called every frame during window resize.

    Here's the header file:

    #import <Cocoa/Cocoa.h>
        
    @interface MyMTLView : NSView<CALayerDelegate>    
    @end
    

    Here's the implementation:

    #import <QuartzCore/CAMetalLayer.h>
    #import <Metal/Metal.h>
    
    @implementation MyMTLView
    
    - (id)initWithFrame:(NSRect)frame
    {
        if (!(self = [super initWithFrame:frame])) {
            return self;
        }
    
        // We want to be backed by a CAMetalLayer.
        self.wantsLayer = YES;
    
        // We want to redraw the layer during live window resize.
        self.layerContentsRedrawPolicy = NSViewLayerContentsRedrawDuringViewResize;
    
        // Not strictly necessary, but in case something goes wrong with live window
        // resize, this layer placement makes it more obvious what's going wrong.
        self.layerContentsPlacement = NSViewLayerContentsPlacementTopLeft;
    
        return self;
    }
    
    - (CALayer*)makeBackingLayer
    {
        CAMetalLayer* metalLayer = [CAMetalLayer layer];
        metalLayer.device = MTLCreateSystemDefaultDevice();
        metalLayer.delegate = self;
    
        // *Both* of these properties are crucial to getting displayLayer to be
        // called during live window resize.
        metalLayer.autoresizingMask = kCALayerHeightSizable | kCALayerWidthSizable;
        metalLayer.needsDisplayOnBoundsChange = YES;
    
        return metalLayer;
    }
    
    - (CAMetalLayer*)metalLayer
    {
        return (CAMetalLayer*)self.layer;
    }
    
    - (void)setFrameSize:(NSSize)newSize
    {
        [super setFrameSize:newSize];
    
        self.metalLayer.drawableSize = newSize;
    }
    
    - (void)displayLayer:(CALayer*)layer
    {
        // Do drawing with Metal.
    }
    
    @end
    

    For reference, I do all my Metal drawing in MTKView's drawRect method.