【问题标题】:The right way to make a continuously redrawn Metal NSView制作连续重绘Metal NSView的正确方法
【发布时间】:2016-06-13 16:18:52
【问题描述】:

我正在学习 Metal 和 Cocoa,并尝试将样板应用程序作为未来实验的平台。作为该过程的一部分,我正在实现一个视图,该视图将以 60fps 重绘自身(或者更准确地说,它的 CAMetalLayer 的内容)。也出于教育目的,我避免使用MTKView(用于“学习可可部分”)。这是我如何解决问题的缩写代码 sn-p:

@implementation MyMetalView // which is a subclass of NSView

- (BOOL) isOpaque {
    return YES;
}

- (NSViewLayerContentsRedrawPolicy) layerContentsRedrawPolicy {
    return NSViewLayerContentsRedrawOnSetNeedsDisplay;
}

- (CALayer *) makeBackingLayer {
    // create CAMetalLayer with default device
}

- (BOOL) wantsLayer {
    return YES;
}

- (BOOL) wantsUpdateLayer {
    return YES;
}

- (void) displayLayer:(CALayer *)layer {
    id<MTLCommandBuffer> cmdBuffer = [_commandQueue commandBuffer];
    id<CAMetalDrawable> drawable = [((CAMetalLayer *) layer) nextDrawable];

    [cmdBuffer enqueue];
    [cmdBuffer presentDrawable:drawable];

    // rendering

    [cmdBuffer commit];
}

@end

int main() {
    // init app, window and MyMetalView instance

    // invocation will call [myMetalViewInstance setNeedsDisplay:YES]
    [NSTimer scheduledTimerWithTimeInterval:1./60. invocation:setNeedsDisplayInvokation repeats:YES];

    [NSApp run];
    return 0;
}

这是做我想做的事情的正确方法吗?还是我选择了一个长期且不推荐的方法?

【问题讨论】:

    标签: macos cocoa metal


    【解决方案1】:

    强烈推荐使用CVDisplayLink 而不是通用的NSTimer 来驱动需要匹配显示器刷新率的动画。

    您需要创建一个 ivar 或属性来保存 CVDisplayLinkRef

    CVDisplayLinkRef displayLink;
    

    然后,当您的视图进入屏幕并且您想要开始制作动画时,您将创建、配置和启动显示链接:

    CVDisplayLinkCreateWithActiveCGDisplays(&displayLink);
    CVDisplayLinkSetOutputCallback(displayLink, &MyDisplayLinkCallback, self);
    CVDisplayLinkStart(displayLink);
    

    显示链接回调应该是一个静态函数。它将在显示器的 v-blank 周期开始时调用(在没有物理 v-blank 的现代显示器上,这仍然以常规的 60Hz 节奏发生):

    static CVReturn MyDisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeStamp* now, const CVTimeStamp* outputTime, CVOptionFlags flagsIn, CVOptionFlags* flagsOut, void* displayLinkContext)
    {
    
        [(MyMetalView *)displayLinkContext setNeedsDisplay:YES];
        return kCVReturnSuccess;
    }
    

    当您的视图离开显示时,或者如果您想暂停,您可以释放显示链接并将其取消:

    CVDisplayLinkRelease(displayLink);
    

    【讨论】:

    • 我可以直接在MyDisplayLinkCallback中调用我的渲染代码吗?
    • 我个人会将我的绘图留在我的updateLayer 实现或updateLayer 调用的方法中。最好只使用回调函数作为反弹回视图实现的一种方式,而不是跨类实现和回调分解视图代码(超出必要的程度)。
    • 我的意思是从回调中调用视图的updateLayer,而不是setNeedsDisplay:YES。如果我理解正确,setNeedsDisplay:YES 只会在未来某个时间schedules 视图重绘,这会引入额外的延迟,不是吗?
    • 让窗口服务器处理这种事情通常是个好主意。与您不会直接调用drawRect: 一样,您也不应该直接调用updateLayer 以规避更新周期。如果您想说服自己(或证明我错了),只需在回调函数和updateLayer 中打印出当前时间即可。我怀疑您会发现延迟时间远少于一帧。
    【解决方案2】:

    以下@warrenm 解决方案添加 dispatch_sync 以刷新和其他次要:

    #import "imageDrawer.h"
    #import "image/ImageBuffer.h"
    #import "common.hpp"
    
    @implementation imageDrawer {
        CVDisplayLinkRef displayLink;
    }
    
    CVReturn MyDisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeStamp* now, const CVTimeStamp* outputTime, CVOptionFlags flagsIn, CVOptionFlags* flagsOut, void* displayLinkContext)
    {
        dispatch_sync(dispatch_get_main_queue(), ^{
            [(__bridge imageDrawer*)displayLinkContext setNeedsDisplay:YES];
        });
        return kCVReturnSuccess;
    }
    
    -(void)setContDisplay {
        CVDisplayLinkCreateWithActiveCGDisplays(&displayLink);
        CVDisplayLinkSetOutputCallback(displayLink, &MyDisplayLinkCallback, (__bridge void*)self);
        CVDisplayLinkStart(displayLink);
    }
    
    -(void)awakeFromNib {
    
        [self setContDisplay];
    }
    
    - (void)drawRect:(NSRect)rect {
        [super drawRect:rect];
    
        int w=rect.size.width, h=rect.size.height;
        // do the drawing...
    }
    
    @end
    

    【讨论】:

      猜你喜欢
      • 2017-09-02
      • 1970-01-01
      • 2013-01-12
      • 2011-03-05
      • 2016-02-20
      • 1970-01-01
      • 2013-06-18
      • 2011-04-26
      • 2020-02-20
      相关资源
      最近更新 更多