めざせ60FPS!
Foooはありがたいことに、とても快適に動作するという評価をいただいている。
今回はその快適さがどのように実現されているかについて紹介しようと思う。
高い要求と過酷な環境
Foooに最初に要求された動作水準は800×600サイズのフルカラー。
それに対し当時の標準的なPCのスペックは Celeron 800MHz 程度だった。
コンピュータが日々進歩を続けているとは言え、その環境は
800×600サイズのフルカラー表示をソフトウェア処理で行うにはまだまだ過酷だった。
どのくらい過酷かというと、800×600サイズの画像を1枚表示させるだけで30FPSなくらい。
言い換えると
画面に背景を表示するだけで30FPS
になってしまうのだ。
他に画像を表示すればあっという間に
20FPS…10FPS…5FPS…
ちなみに、FPSとは1秒の間に画面を再描画する回数(Frame Per Secnod)のことである。
モニタの標準的なFPSは60FPSであるが、
ゲームを快適にプレイできるようにするためには最低でも平均30FPS程度は出したいところだ。
それが、背景を表示するだけで30FPS…
もっと低スペックの環境も当然ありえるが400MHzだと15FPS、166MHzだと5FPS…
5FPSともなると、もはやまともにプレイできるような状態ではない。
400MHZでも最低30FPS、166MHzでも最低15FPSは出るようにしたい。
そのためには800×600で60FPS出るくらいじゃないとお話にならない。
無理じゃね!?
描画処理の最適化
泣き言を言っても始まらない。
FPSが出ないのは、一にも二にも描画に非常に時間がかかっているからである。
そこで最初に行った対策は
とにかく描画を速くする
ことである。
次のような最適化を施した。
・ケースバイケースごとに細かく処理分けして少しでも無駄な処理を省く
・コンパイラに頼らずアセンブラで最適化
・MMXを使う
・SSEを使う
・パイプラインストールを減らす
・メモリアライメントを意識する
・キャッシュヒット率を考える
・キャッシュにプリフェッチする
そんなこんなで~描画処理を1.5倍程度高速化することができた!
16msかかっていた800×600の画像の描画処理が11msになった。
──────しかしFPSにすると、30FPSだったのが40FPSになったくらい。
60FPSにはまるで届いていない。
無効領域処理
描画処理そのものの高速化はもう限界。
そこでもっと別の角度から高速化を行えないかと考えた。
それは…
無駄な描画をしない
ことである。
無駄な描画を省く最適化は一般的にカリング(Culling)と呼ばれる。
Cullingとは「間引き」という意味だ。
Windowsのウィンドウの描画においても、このカリング処理が行われている。
画面の再描画を行うには、画面を背景から何から全て描き直してしまうのが最もシンプルな方法である。
しかしそこには非常に多くの無駄が存在する。
まず画面が何も変化してないのに画面を書き直すのはまったくの無意味だ。
そのような画面の再描画は根本的に省くことができる。
さらに画面の中で前回描画した時から何も変化していない部分を再描画するのも大きな無駄だ。
何も変化していないのだから再描画する必要はない。
要するに
変化がない部分は再描画する必要がない
のである。
実際の最適化の様子を見てみよう。
赤い箱枠で示されているのが、変化があり再描画が行われている領域である。
箱枠がない部分は再描画の必要のない領域だ。
再描画領域が非常に小さく限定されていることがわかるだろう。
このような再描画の必要のある領域を『無効領域』(Invalid Rect)と呼ぶ。
Update Rect(更新領域), Dirty Rect(汚れた領域)などと呼ばれることもある。
この最適化の効果は絶大だった。
事実、この最適化によって”画面に動きがない部分では”あっさり60FPSを達成できた!
しかしこの最適化を活かすには、演出においてこの最適化を意識することが大切だ。
画面全体を動かすような演出をできるだけ避けるといった工夫である。
無効領域の分割管理
無効領域は効率のために矩形で管理するが、
無効領域は画面の中で飛び飛びに複数発生する可能性がある。
そのような無効領域を矩形で管理しようとすると無駄が発生してしまう。
次の例を見て欲しい。
雪の粒が画面を埋め尽くしているので、無効領域はほぼ画面サイズと等しくなっている。
しかしこの無効領域には明らかに大きな無駄がある。
再描画の必要のない部分まで無効領域に含めてしまっている。
このような無駄が発生するのは、無効領域を単一の矩形を管理しているせいだ。
この問題を解決するには
無効領域を複数の矩形で管理すればいい
実際の例を見て見よう。
無効領域が非常に細かく管理され、無駄が省かれている様子がわかるだろう。
けれども無効領域をあまりに細かく管理しすぎると
無効領域の管理の処理負荷が大きくなってしまうため加減が肝心だ。
この最適化により、画面の各所で複数のものが動作する場合でも
非常に高いFPSを出すことができるようになった。
遮蔽される面の除去
描画において大きな無駄がもう1つある。
見えない部分は描画する必要はない
のである。
複数の画像が重なっていて、後ろの画像が手前の画像によって覆い隠されている場合、
後ろの画像はどうせ隠れてしまうので描画する必要がない。
言われて見れば、至極当たり前の最適化である。
この最適化は3D方面ではオクルージョンカリング(Occlusion Culling)と呼ばれる。
Occlusionとは「遮蔽」という意味だ。
実際の動作の様子を見てみよう。
青い箱枠が遮蔽面、緑の箱枠が遮蔽しない面である。
この箱枠の形に描画処理が行われる。
遮蔽によって、背後の画像の領域が分割されている様子がわかるだろう。
この最適化の効果は思いのほか大きい。
実際の演出では画面の切り替えにフェードイン、フェードアウトが多様されるが
フェード幕で隠れた部品を、幕の背後に残したまま演出を続けるようなことが多々ある。
そのような場合にこの最適化がすごく活きる。
幕の背後の描画を自動的に行わないようになるからだ。
逆にこの最適化がないと、非常に大きな無駄が発生してしまう。
そして、ソレになかなか気がつかない。
Foooの演出において、真四角のコマが多用されているのもこの最適化が関連している。
コマを真四角にすることで、背景を遮蔽しやすくしているのだ。
そのようにすると、コマがたとえ画面に何十枚もある場合でも
背景を1枚描画するのとさほど変わらないコストで画面を描画できるのである。
遮蔽される無効領域の除去
次の最適化は併せ技だ。
見えない部分が変化しても再描画する必要はない
遮蔽されている部分で何かが動いても、見た目に変化は起きないので再描画する必要がない。
要するに「無効領域に対して遮蔽を行う」ということである。
これも実際の動作を見るのがわかりやすいだろう。
まとめ
全ての最適化を適用したもの。
このような最適化と、その最適化を意識した演出によって
Foooはソフトウェア処理でありながら高速な(高速に見える)動作を実現しているのである。
以下おまけ。