制作不断重绘的Metal NSView的正确方法
The right way to make a continuously redrawn Metal NSView
我正在学习 Metal 和 Cocoa 并尝试制作样板应用程序作为未来实验的平台。作为该过程的一部分,我正在实现一个将以 60fps 重绘自身(或者更准确地说,其 CAMetalLayer
的内容)的视图。同样出于教育目的,我避免使用 MTKView
(对于 "learning Cocoa part")。这是我如何解决问题的简短代码片段:
@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;
}
这是做我想做的事情的正确方法吗?还是我选择了一个很长但不推荐的方法?
强烈建议使用 CVDisplayLink
而不是通用的 NSTimer
来驱动需要匹配显示器刷新率的动画。
您需要创建一个 ivar 或 属性 来保存 CVDisplayLinkRef
:
CVDisplayLinkRef displayLink;
然后,当您的视图进入屏幕并且您想要开始动画时,您将创建、配置并启动显示 link:
CVDisplayLinkCreateWithActiveCGDisplays(&displayLink);
CVDisplayLinkSetOutputCallback(displayLink, &MyDisplayLinkCallback, self);
CVDisplayLinkStart(displayLink);
显示 link 回调应该是一个静态函数。它将在显示器的 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;
}
当您的视图离开显示时,或者如果您想暂停,您可以释放显示 link 并将其清除:
CVDisplayLinkRelease(displayLink);
按照@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
我正在学习 Metal 和 Cocoa 并尝试制作样板应用程序作为未来实验的平台。作为该过程的一部分,我正在实现一个将以 60fps 重绘自身(或者更准确地说,其 CAMetalLayer
的内容)的视图。同样出于教育目的,我避免使用 MTKView
(对于 "learning Cocoa part")。这是我如何解决问题的简短代码片段:
@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;
}
这是做我想做的事情的正确方法吗?还是我选择了一个很长但不推荐的方法?
强烈建议使用 CVDisplayLink
而不是通用的 NSTimer
来驱动需要匹配显示器刷新率的动画。
您需要创建一个 ivar 或 属性 来保存 CVDisplayLinkRef
:
CVDisplayLinkRef displayLink;
然后,当您的视图进入屏幕并且您想要开始动画时,您将创建、配置并启动显示 link:
CVDisplayLinkCreateWithActiveCGDisplays(&displayLink);
CVDisplayLinkSetOutputCallback(displayLink, &MyDisplayLinkCallback, self);
CVDisplayLinkStart(displayLink);
显示 link 回调应该是一个静态函数。它将在显示器的 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;
}
当您的视图离开显示时,或者如果您想暂停,您可以释放显示 link 并将其清除:
CVDisplayLinkRelease(displayLink);
按照@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