[戻る]

ゲームの VSYNC 問題とその対策

VSYNC とは、ゲーム等のアプリケーションの動作速度を一定に保つための仕組みです。 目的と概念は単純ですが、それを取り巻く問題はとても複雑です。 ここでは、VSYNC の基礎的な事項と、 関連する問題を網羅的にまとめていきます。
注)2001/08/15 時点の記事をもとに加筆修正し 2020 年 8 月時点の状況を反映させた内容となっています。 VSYNC をめぐる状況は流動的です。現状と合致しない可能性があります。

用語と基礎知識

走査線(ラスター)
ブラウン管方式のモニタは、 ディスプレイ面の蛍光体に対して電子ビームを発射し、発光させることで画像を表示する仕組みになっています。 電子ビームが横 1 ラインずつ上から下まで走査することで、画面全体を発光させます。

電子ビームは、画面の左上端からスタートし右方向へ進みます。 右端に達すると左端に戻り、1 ライン下の走査線に移動します。 そして、右下端に到達すると画面の左上端に戻ります。

走査線の振る舞い
走査線の振る舞い。
矢印は電子ビームの軌跡を示す。

この電子ビームがなぞる横方向のラインを走査線またはラスターと呼びます。 昨今のモニタはブラウン管方式は主流ではなくなりましたが、 走査線の概念は仕様とし引き継がれ、現在も存在しています。

リフレッシュレート と フレームレート
リフレッシュレートは、モニタ側のスペックを示す数字です。 1 秒間に走査線が画面全体を何回走査するかを示します。 単位は Hz です。

一方、フレームレートは、モニタ側でなくゲーム側の数字で、 1 秒間にゲーム画面が何回更新されるかを示します。 秒間フレーム数を意味する frame per second の頭文字をとって、 fps と言う単位が使われます。

処理落ち
ゲームの処理負荷が増大すると、 1 フレームあたりの処理時間が長くなり、 フレームレートがリフレッシュレートを下回った状態になります。 これを処理落ちと言います。

HBLANK と VBLANK
走査線が画面右端から左端に戻る期間を HBLANK と言います。 H は水平を意味する horizontal の頭文字です。 日本語で表記すると「水平帰線期間」となります。 一方、走査線が画面下端から上端に戻る期間を VBLANK と言います。 V は垂直を意味する vertical の頭文字です。 日本語で表記すると「垂直帰線期間」となります。

帰線期間中はモニタへの画面出力が行われません。 この期間中であれば、 画面を書き換えても書き換え中の未完成の絵が表示される心配はありません。

HSYNC と VSYNC
HBLANK の開始を検出する同期処理を HSYNC と言います。 日本語で表記すると「水平同期」となります。 同様に、VBLANK の開始を検出する同期処理を VSYNC と言います。 日本語で表記すると「垂直同期」となります。

ゲームのフレームレートとモニタのリフレッシュレートがずれていると、 ゲーム画面の更新がガクガクと不安定になってしまいます。 通常ゲームソフトではこれを避けるため、 ゲームのフレームレートをモニタのリフレッシュレートに同期させて、 安定した画面表示を行います。 この時 VSYNC が利用されます。

フレームバッファの裏表とフリップ
ゲームのグラフィクスは、 フレームバッファと呼ばれる、ピクセル単位で画素情報を保持するバッファメモリ上に生成され、 そこからディスプレイに対して出力されます。 フレームバッファは、 現在画面に表示中の画像を保持する「表バッファ」と、 現在描画中の最新のフレームの画像を保持する「裏バッファ」に分けられます。 そして、裏バッファの絵が描き上がると、裏バッファと表バッファが交換されます。 これをフリップと言います。

フリップは、走査線が画面内を走っていない期間に行う必要があります。 HBLANK 期間中にフリップすることを HSYNC フリップ、 VBLANK 期間中にフリップすることを VSYNC フリップと言います。

ティアリング
HSYNC フリップでは、 画面上に走査線が走っている最中にフリップが行われることになります。 そして、 フリップした時点の走査線より上には前回フレームの描画結果、 走査線より下には最新フレームの描画結果が表示された状態になります。 つまり、特定の走査線の上下で異なる絵が表示されてしまうことになります。 これを、ティアリング(tearing 引き裂くの意味)と呼びます。


典型的なティアリング発生の様子。
Tear Point と書かれた位置で、絵がズレている。
(画像は wikipedia から抜粋 CC BY-SA 3.0

VSYNC フリップでは、走査線が画面内に存在しないタイミングでフリップするため、 ティアリングは発生しません。

スタッタリング
モニタのリフレッシュレートと、ゲームのフレームレートがズレていると、 たとえ VSYNC を取っていても、 同一のフレームが複数回連続表示されたり、 場合によってはフレームのスキップが発生し、 画面の更新がガクガクと不安定になります。

このような不安定な画面更新が起きることを、スタッタリング(stuttering)と呼びます。

ゲームでの VSYNC 実装の変遷

ゲームのフレームレートを一定に保ちたい場合、 単純に考えると、各フレームの CPU 処理の進行を VSYNC と同期させれば良いように思われます。 しかし実際には、そのような単純な実装は初期のころには存在しましたが、現在では使われなくなりました。

VSYNC 周りの実装は世代を追うごとに複雑化しています。 ここでは、VSYNC 手法の変遷を振り返ります。
1980 年代 : 常時 VSYNC
1980 年代のゲームの実装では、 CPU は次のフレームを処理開始する前に、 VSYNC 信号を監視して待つのが一般的でした。

ゲーム内の処理負荷が上昇して、 CPU の処理が VSYNC 信号発生に間に合わないと、信号取りこぼしが発生し、次の VSYNC 信号を待つことになります。 その結果、フレームレートは半分に落ち、スローモーションがかかったようになります。

初代アーケード版グラディウス(KONAMI 1985)
処理落ちが発生すると、60fps から 30fps まで一気に低下する。

1980 年代末 : VSYNC on/off 動的変更
1980 年代末ごろになると、 CPU 処理が重くなり VSYNC に間に合わない状況では VSYNC を取ることをあきらめるという実装が出てきました。 この方法では、従来のような「常時 VSYNC」実装とは異なり、 処理落ち状態になっても、一気にフレームレートが半分まで落ちずに済むというメリットがあります。 一方、処理落ち発生状況下では、スタッタリング(ゲームのフレームレートが激しく変動する現象)が発生し、 ゲームがプレイしづらく感じるというデメリットがあります。

CAPCOM では CP システムの時代にはすでにこのような実装が採用されていたとのことです。 CPU 処理が重くなり VSYNC に間に合わない状況を示す「負荷フラグ」というものがあり、 これが true になっているとき、CPU は VSYNC を無視した動作をしたとのことです。

Varth (CAPCOM 1992)
当時の認識では、「処理落ち発生=難易度が下がってラッキー」だった。
しかしこのゲームでは、処理落ちが発生すると VSYNC を取らない実装になっており、
スタッタリング発生により逆にゲームが難しくなっていた。

GPU 登場初期 : 処理落ち時はティアリング発生覚悟で HSYNC フリップ
ゲームのグラフィクス描画は、従来のスプライト方式から現在のような GPU 方式に置き換えられていきました。 GPU 世代以降では、 描画結果はフレームバッファ上に生成されます。

GPU 登場初期のフレームバッファは、 メモリ容量の制約から 表バッファ裏バッファそれぞれ 1 枚だけ用意することが普通でした。 これを「ダブルバッファリング」と呼びます。 ダブルバッファリングの場合、 1 フレーム分の処理が VSYNC までに間に合わなかった場合、 次の VSYNC まで待たさせることになるため、フレームレートが半分に落ちます。 これは以下のような理由からです。

  • 「表バッファ」はディスプレイに表示中なので、描き換えられない。
  • 「裏バッファ」には描画が終わった次のフレームの絵が置かれており、描き換えられない。
  • つまり表も裏も描き換えられない。VBLANK 期間が来るまで、次フレームの描画が開始できない。

これを避けるため、 VSYNC を取りこぼしたときは次の VSYNC を待たず、 ティアリング発生覚悟で直ちに HSYNC フリップするという手法が広く利用されました。

現行 GPU : トリプルバッファリングと、CPU の非同期化
VSYNC を待たず、次フレームの描画処理を開始できるようにするため、 現行のグラフィクス処理では、ダブルバッファリングに対してさらに「裏バッファ」を一つ追加し、 「裏バッファ」x2 と「表バッファ」のトリプルバッファリングが利用されます。

そして画面のフリップ処理は、CPU が直接行うのではなく、 「この描画が終わったら VSYNC を待って画面フリップする」という処理を CPU から GPU に対して予約し、 GPU にフリップさせる仕組みが利用されるようになりました。 これにより、CPU は VSYNC から解放され、VSYNC と非同期で動作可能になりました。

CPU が非同期化したことにより、 CPU は処理時間に余裕があるなら VSYNC を無視して先のフレームを先行処理することが可能になりました。 先行処理中は、先行フレーム数分の貯金があるような状態になり、 CPU 処理が重いフレームに出くわした時、 貯金を放出して処理落ちを回避することができます。 このように、 トリプルバッファリングは、 CPU の処理時間がフレーム間で大きく変動するような場合に、 見た目上のフレームレートを安定化させる緩衝効果もあります。

VSYNC をめぐる問題

ここでは、VSYNC をめぐる問題について過去の事例を振り返り、 かつてのゲームプログラムがこの問題に関してどのような苦労を抱えてきたのかを見ていきます。
PAL 方式と NTSC 方式とで周波数が異なる
PAL、NTSC は、アナログカラーテレビの送受信方式です。 PAL は欧州で使われる方式で、リフレッシュレートは 50Hz です。 NTSC は日本や北米で使われる方式で、リフレッシュレートは 60Hz です。

一般に、NTSC 環境向けに作られたゲームを PAL 環境にもっていくと、 ゲーム進行が 60Hz → 50Hz に低下するため、ゲームの難易度が低下します。

逆に、NTSC 環境で 20fps 動作(1 フレーム = 3 VSYNC)していたゲームでは、 PAL 環境では近いフレームレートとして 25fps(1 フレーム = 2 VSYNC)が利用された結果、 進行速度が上がってゲームの難易度が上昇する例もありました。 このような例として、Nintendo 64 版「ゼルダの伝説」が知られています。

このように、PAL 方式 NTSC 方式の違いが、ゲームの難易度に影響してしまうことがありました。

古い PC やアーケードゲーム機のリフレッシュレート ≠ 60Hz
厳密に NTSC などの規格に合致させる必要の無かった古い PC やゲーム基盤では、 リフレッシュレートは 60Hz ではなく、それよりも若干低い数字になっていることがあります。 例として、SHARP X68000 シリーズのモニタのリフレッシュレートは、56Hz 付近でした。 同時期のアーケードゲームの基盤にも、 56Hz 付近のフレームレートを持つものが多く存在します。

このような環境で作られたゲームを、現行の PC や家庭用ゲーム機に移植する場合、 60Hz のモニタ上でどう表示するのかという問題が生じます。 この問題の回避方法としては、以下のような選択肢があります。

  1. スタッタリング発生覚悟で VSYNC フリップ(一定間隔で同一フレームが 2 回表示される)
  2. ティアリング発生覚悟で HSYNC フリップ
  3. 隣接フレーム間でブレンド
  4. モニタのリフレッシュレートに合わせる

(1) の方法は、フレームレートがガクガクと不安定になってしまいます。 (2) の方法は、ティアリングによりゲーム画面が見づらくなります。 (3) の方法は、最新フレームの絵が低いブレンド率で表示される場合があり、 表示遅延に似た副作用が生じます。 (4) の方法は、ゲームの進行速度が変化してしまいます。 このように、どの方法を採用しても、問題を綺麗に解決することができません。

HSYNC フリップによるティアリングはスタッタリングを伴う
HSYNC フリップを利用する場合、 フリップによりティアリングが発生する走査線の位置は、 フリップ要求が出されるタイミングがフレーム毎に異なるため、激しく変動しがちです。

ティアリング発生位置が激しく変動する時、何が起きるのかを図で示します。

ティアリング発生の最悪ケース
ティアリング発生の最悪ケースその 1。
連続した 2 フレームの表示結果を示す。
図で示した範囲において、フレーム #n+1 の表示が完全にスキップされている。

ティアリング発生の最悪ケース
ティアリング発生の最悪ケースその 2。
図で示した範囲において、フレーム #n+3 が 2 回連続表示されている。

もし、最悪ケースその 1 とその 2 が交互に発生したらどうなるでしょうか? 画面の中央部分は 2 フレームに 1 回しか更新されない状態になります。 これは結果的に、スタッタリングと似たような現象となります。

このように、ティアリングによる視認性の悪化は、 一般に認識されている「画面のズレ」だけではなく、 ここで説明したようなフレームスキップや重複表示が原因でも起きます。 最悪の状況では、スタッタリングよりも酷い見た目となります。

トリプルバッファリングによる遅延の増大問題
現行世代のゲームグラフィクスでは、 トリプルバッファリングが採用され、 CPU が VSYNC から解放され非同期動作が可能になりました。 このことは、CPU の処理が表示に対して先行して進むことを可能にしましたが、 副作用として表示遅延が増大するという問題があります。 表示遅延は、ゲームの操作性を悪化させる 入力遅延問題 を引き起こす主な原因の一つになっています。

表示遅延の増大を避けるには、 GPU の描画に対して CPU が先行しすぎないように、適切な wait を入れる必要があります。 しかし、環境や API の世代によっては、これが不可能な場合があります。

Windows 環境では、Windows7 より導入された Desktop Window Manager(dwm)と呼ばれる仕組みが、 内部でトリプルバッファリング相当の動作をするようになりました。 Windows10 では、これを off にする手段も無効化されました。 この問題は、フルスクリーンモードや 従来の DirectX に存在した「排他モード」にしても回避することはできず、 いかなるゲームも遅延の増大から免れることはできないという事態が発生していました (後に DirectX12 の拡張で回避方法が提供された。後述する)。

Windows 環境の VSYNC/HSYNC フリップベストプラクティス

ここからは、Windows 上でのゲーム開発における VSYNC/HSYNC フリップのベストプラクティスについてまとめます。 VSYNC が取れるケースと取れないケースとで、対処方法が異なります。
VSYNC とリフレッシュレート指定はフルスクリーンモード時のみ可能
DirectX には VSYNC を取る API が提供されています。ただしこれはフルスクリーンモード時のみ有効です。 フルスクリーンモード時、モニタのリフレッシュレートが設定可能です。 ただしモニタがサポートしているリフレッシュレート以外に設定することはできません。

VSYNC フリップが利用できる場合
まず、フルスクリーン化とモニタのリフレッシュレート設定すべてが成功し、 VSYNC フリップが利用できるケースについて触れます。 このケースでは、もはや問題はなさそうに見えますが、 トリプルバッファリングによる遅延を解消する必要があります。
Independent Flip モードにする
まず、 画面フリップが最低遅延を意味する Independent Flip と呼ばれる状態になるように、条件を揃えていきます。 Independent Flip させるには、 スワップチェインの解像度をウィンドウ解像度に一致させる必要があるほか、 OS 側に解像度情報を伝える手順がセオリー通りになっている必要があります (手順を間違っていると、デバッグ実行時にコンソールに警告が出るのでその指示に従う)。 Independent Flip 状態になっているかどうかは、 PresentMon というツールを利用することで、アプリケーション実行時に確認可能です。

トリプルバッファリングの CPU 先行実行を抑制する
トリプルバッファリングは、 CPU に VSYNC と非同期の処理を可能にしたり、 ゲームのフレームレートを安定化させる効果がある一方で、 表示遅延を増大させる副作用があることについては、 先ほど述べたとおりです。 DirectX では、2018 年ごろになって、 ようやく DirectX12 の更新により表示遅延を回避する仕組みが導入されました。 (残念ならが、DirectX11 を利用している場合は、この恩恵を受けることができません。 DirectX11 の表示遅延軽減については、 GPU ベンダが独自に DirectX12 の低遅延化と同様の仕組みをドライバ越しで提供しているというカオスな状況が発生しています。)

CPU 先行実行を抑制するには、 GetFrameLatencyWaitableObject() という API を使い、 CPU の先行フレーム数を管理するセマフォを取得します。 このセマフォは、アプリケーションのメインループの進行に合わせて 適切にカウンタを更新(減少)してやる必要があります。

しかしそれでもまだ不十分です。 セオリー通りにこのセマフォを管理していると、1フレーム遅延になります。 DirectX12 で達成可能な最低遅延を満たすには、 GetFrameLatencyWaitableObject() で取得したセマフォを、 その場で一回 WaitForSingleObjectEx() などでカウントダウンしてやり、 カウンタ 0 の状態からスタートする必要があります。 このカウンタ 0 化は、セマフォの扱いを誤るとデッドロックの危険があるため、 Microsoft は積極的にサンプルコードなどでは提示していません。 しかし遅延を削減するには必須です。
VSYNC フリップが利用できず、HSYNC フリップを利用する場合
続いて、ウィンドウモード利用時など、 VSYNC フリップが利用できないケースについて触れます。 この場合、VSYNC の代わりに、タイマーを利用した HSYNC フリップを利用します。
マルチメディアタイマを利用する方法
Windows には、 マルチメディアタイマと呼ばれる高精度タイマが用意されています。 これは、timeGetTime() 関数で取得できます。

マルチメディアタイマを使用する場合、注意点があります。 マルチメディアタイマの分解能は、 timeBeginPeriod() 関数で指定した値になります。 よって、より高い精度を得たいならば、 事前に以下のように実行しておく必要があります。

	TIMECAPS tc;
	timeGetDevCaps(&tc, sizeof(TIMECAPS));

	/* マルチメディアタイマのサービス精度を最大に */
	timeBeginPeriod(tc.wPeriodMin);

timeGetTime() 関数の戻り値はミリ秒単位です。 従って、分解能は十分ではありません(参考:60fps は 16.666...ms です)。 マルチメディアタイマを利用したリフレッシュレートの近似は、 あまり良い方法とは言えません。

パフォーマンスカウンタを利用する方法
Windows には、 パフォーマンスカウンタと呼ばれる超高精度カウンタがあります。 こちらは 1ms 以下の精度が確保可能です。 ただし、パフォーマンスカウンタは、 厳密には全ての Windows 環境で利用できるわけではありません (一見利用可能に見えて、実際には常にタイマ値に 0 を返してきたり、様々です)。

	LARGE_INTEGER liPerfFreq;
	LARGE_INTEGER liPerfCount;
	memset(&liPerfFreq, 0, sizeof(liPerfFreq));
	memset(&liPerfCount, 0, sizeof(liPerfCount));

	double currentTime = 0.0;
	if (QueryPerformanceCounter(&liPerfCount) && QueryPerformanceFrequency(&liPerfFreq)) {
		double counter = (double)liPerfCount.QuadPart;
		double frequency = (double)liPerfFreq.QuadPart;
		currentTime	= counter * 1000000.0 / frequency;
	}


Adaptive HSYNC(HSYNC フリップの安定化)
HSYNC フリップが発生する走査線の位置がフレーム毎に変動すると、 走査線付近でひどいスタッタリングが発生し、 ゲーム画面が見づらくなる旨は先に述べた通りです。

この問題を改善するには、 HSYNC フリップ発生位置を安定化させる必要があります。 具体的には、 HSYNC フリップを行った時刻を上記のタイマー関数等で取得して保存しておき、 次回 HSYNC フリップするとき、 直前の HSYNC フリップから十分な時間(目安として、1 フレームの時間の 95% 程度)が経過するまで待ってから、 HSYNC フリップするようにします。

adaptive hsync
フレーム #n+1 の描画は短い時間で終了した。
#n+1 から #n+2 にフリップ可能だが、
今すぐフリップすると酷いスタッタリングを発生させる。

adaptive hsync
前回のフリップから十分な時間が経過するのを待って HSYNC フリップ。
ティアリングは発生するがフレームスキップされる範囲は最小化され、
ゲーム画面の表示が安定する。


2001/08/15 初出
2020/08/29 全面的リライト
文責: よっしん

[戻る]