2016年4月10日日曜日

iOSとOSXでのvsyncの取り方の違い

私がiOSアプリを開発した時に最初に詰まったのが、vsync(垂直同期)の発生タイミングを検知する方法で、色々調べて(確かmameか何かのソースを読んで) CADisplayLink を使えば良いことが分かりました。

CADisplayLinkの使い方は大雑把に説明すると下記のような感じです。

MyView.h
@interface MyView : UIView
@property CADisplayLink* displayLink;
@end

MyView.m
- (id)initWithFrame:(CGRect)frame {
  if ((self = [super initWithFrame:frame])!=nil) {
    ...CALayerの初期化など...
    _displayLink=[CADisplayLink displayLinkWithTarget:self selector:@selector(setNeedsDisplay)];
    [_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
  }
  return self;
}

こういう風にすることで、vsyncの発生間隔の都度setNeedsDisplayが呼び出されて、60fpsでViewの描画がされるようになります。

そんなのNSTimerで良いのではないか?と思われるかもしれませんが、NSTimerで1/60秒間隔でsetNeedsDisplayをコールすると、若干画面時にブレ(ガクガク感)みたいなものが発生してしまうのでダメです。
何故、NSTimerではダメなのかというと、垂直同期の呼び出し間隔は常に一定ではなく、17ms, 17ms, 16ms 17ms, 17ms, 16ms... という風になっているので、常に一定間隔でコールしているとズレが生じてしまう為ではないかと思われます。

そして、OSX用のゲームを開発しようとした時、CADisplayLinkはUIじゃないからOSXでもそのまま使えるよね(※)などと考えていたのですが、甘かった。OSXの場合、CVDisplayLinkを使う必要があります。
iOSとOSXでは同じCocoaアプリでも作り方が微妙に違います。例えば、iOSのUIViewはOSXには無くて、代わりにNSViewがあったりとか。プレフィックス「UI」のクラスはiOS用で「NS」はOSX用という感じです。名前が違うので当然機能も微妙に違います。ですが、CADisplayLinkはプレフィクスがUIじゃなくてCA(CoreAnimation)だから、OSXでもそのまま使えるよねなどと考えていた時期が私にもありました。
CVDisplayLinkを用いた場合の実装は下記です。

MyView.h
@interface MyView : NSView
@property CVDisplayLinkRef displayLink;
@end

MyView.m
- (id)initWithFrame:(CGRect)frame
{
  if ((self = [super initWithFrame:frame]) != nil) {
    ...CALayerの初期化など...
    CVDisplayLinkCreateWithActiveCGDisplays(&_displayLink);
    CVDisplayLinkSetOutputCallback(_displayLink, MyDisplayLinkCallback, (__bridge void *)self);
    CVDisplayLinkStart(_displayLink);
  }
  return self;
}

static CVReturn MyDisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeStamp *now, const CVTimeStamp *outputTime, CVOptionFlags flagsIn, CVOptionFlags *flagsOut, void *context)
{
  [(__bridge VGSLayer *)context performSelectorOnMainThread:@selector(vsync) withObject:nil waitUntilDone:NO];
  return kCVReturnSuccess;
}

- (void)vsync
{
  [self.myLayer setNeedsDisplay];
}

若干気をつけないといけないのは、CVDisplayLinkSetOutputCallbackで指定できるコールバックの呼出し規約がC(Objective-cではない)ということですかね。(なので、若干面倒なブリッジが入っています)

あと、上記の例では省略していますが、何度も使い回されるタイプのViewの場合、確保したディスプレイリンクの停止(CVDisplayLinkStop)や解放(CVDisplayLinkRelease)が必要です。
詳細は下記を参照。
https://developer.apple.com/library/mac/documentation/QuartzCore/Reference/CVDisplayLinkRef/

0 件のコメント:

コメントを投稿

注: コメントを投稿できるのは、このブログのメンバーだけです。

合理的ではないものを作りたい

ここ最近、実機版の東方VGSの開発が忙しくて、東方VGSの曲追加が滞っています。 東方VGS(実機版)のデザインを作りながら検討中。基本レトロUIベースですがシークバーはモダンに倣おうかな…とか pic.twitter.com/YOYprlDsYD — SUZUKI PLAN (...