前回の記事のようにglTexSubImage2Dでテクスチャに文字を書いてそのテクスチャを描画すれば、OpenGLで文字を書くことができます。しかし、文字列を描画する毎にglTexSubImage2Dを呼び出した場合、1フレームで描画する文字列の種類が増えてくるにつれて、実機では耐えられない重さになります。

実際、iPhoneシミュレータではフォント描画vsトライアングル描画の描画時間が0.1:0.9ぐらいなのですが、実機で動かすと0.5vs0.5と、フォント描画にもの凄く時間がかかることが分かります。これは、iPhoneシミュレータで使っているNVIDIAのアーキテクチャと、iPhoneで使っているPowerVRのアーキテクチャの違いに起因します。

NVIDIAのアーキテクチャ(というか普通のアーキテクチャ)だと、受け取った描画命令は順次実行され、Zバッファとの評価の結果、隠面になるものは、後の描画命令で上書きされます。つまり、裏に隠れる部分も描画しています。

しかし、PowerVRの場合は、独特のタイルベースのアーキテクチャとなっており、前もって描画命令をかなりの数キャッシュしておいて、ピクセルごとにどの描画命令が一番前面に来るかを計算し、実際に見えるピクセルだけを描画します。これにより、実際に見えるテクスチャだけを読みにいくため、モバイル端末等のバス幅の限られたデバイスでも高速に動作しているのです。

このように、PowerVRの特徴は、多くの描画命令をまとめて処理する構造にあります。しかし、glTexSubImage2Dが呼び出された場合、glTexSubImage2D以前に呼び出された描画命令で使われるテクスチャまで更新される可能性があるため、一度、glTexSubImage2D以前の描画命令を全て完了して、フレームバッファに絵を作る必要があります。

普通のアーキテクチャの場合は、描画命令は順次実行しているので、途中で描画をフラッシュしても大きな影響はないのですが、PowerVRの場合は違います。PowerVRは、もしも全命令をバッファリングすることができれば、1ピクセルに対して一回の描画だけで全ピクセルを完成させてしまえます。しかし、ここにglTexSubImage2Dが入るだけで、1ピクセルに対して二回の描画が必要になります。一気に二倍遅くなるわけです。

ということで、AppleのOpenGLES_ProgrammingGuideには下記の注釈があります。

---------------------------------------------
現在のところ、すべてのiPhoneハードウェアはタイルベースの遅延レンダラを使用 しています。このレンダラは、glTexSubImageとglCopyTexSubImageの呼び出しの際に特にコスト がかかります。詳細については、「タイルベースの遅延レンダリング(TBDR)」 (53 ページ)を参 照してください。

タイルベースの遅延レンダリング( TBDR)
PowerVR SGXはTBDR(Tile Based Deferred Rendering:タイルベースの遅延レンダリング)と呼ばれる 手法を用いています。レンダリングのためにOpenGL ESコマンドを送信すると、PowerVR SGXはある 程度の量のレンダリングコマンドが蓄積するまでレンダリングを保留し、蓄積されたコマンドを1 回のアクションで実行します。フレームバッファは複数のタイルに分割されており、シーンは1つ のタイルにつき1回描画されます。各タイルでは、タイル内で見えているコンテンツだけが描画さ れます。遅延レンダラの主な利点は、より効率的にメモリにアクセスできることです。レンダリン グをタイルに分割することによってGPUはフレームバッファからのピクセル値を効率的にキャッシュ できるため、デプステストやブレンドの効率が向上します。
遅延レンダリングのもう1つの利点は、GPUがフラグメントを処理する前に非表示のサーフェスを削 除できることです。表示されないピクセルは、テクスチャのサンプリングやフラグメント処理を実 行せずに破棄されます。したがって、GPUがシーンをレンダリングするために実行しなければなら ない計算量が大幅に削減されます。この機能の効果を最大限に高めるためには、シーンのできるだ け多くの部分を不透過なコンテンツで描画して、ブレンド、アルファテスト、およびGLSLシェーダ でのdiscard操作の使用を最小限に抑えるようにします。非表示のサーフェスの削除はハードウェア によって実行されるため、アプリケーションは全面から背面までのジオメトリを並べ替える必要が ありません。

遅延レンダラの下での操作の中には、従来のストリームレンダラの場合よりもコストがかかるもの もあります。先に説明したメモリ帯域幅と計算量の節約は、大きなシーンを処理する場合に最も効 果があります。小さいシーンのレンダリングを要求する(または、シーンのフラッシュを避けるた めにリソースを複製する)ようなOpenGL ESコマンドをハードウェアが受け取ると、レンダラの効 率は大幅に低下します。
たとえば、アプリケーションがglTexSubImageを呼び出してフレームの中央にあるテクスチャを更 新する場合、レンダラは更新後のテクスチャと以前のテクスチャの両方を同時に保持する必要があ るかもしれず、アプリケーション内のメモリ使用量が増加します。同様に、フレームバッファから ピクセルデータを読み込もうとすると、その前のコマンドがフレームバッファを変更する場合はそ れらが実行されている必要があります。
---------------------------------------------

ということで、文字を書くたびにglTexSubImageを呼び出していると、一気にFPSが低下します。メトセラの場合、20種類の文字列を同時に描画するあたりで、FPSが20から5程度に低下しました。

妥当な解決策は、glTexSubImageの呼び出し回数を減らすことです。具体的に、該当フレームで描画する文字列を全てリストアップしておき、フレームの最初で一つのテクスチャに描画してしまいます。同時に、各文字のテクスチャ上での位置を記憶しておきます。実際の文字列の描画では、テクスチャのuv座標で文字を選択します。これによって、glTexSubImageの呼び出し回数が、文字列の数から、一気に1回まで激減します。メトセラの例では、20から5に低下していたFPSが、15まで3倍程度改善しました。

課題は、512x512ピクセルの空間の有効利用ですが、とりあえずは文字列によってy座標を変えるだけで実装しました。この場合はフラグメンテーションとか考えなくていいのでかなりお手軽です。

int y=m_texture[m_pre_draw_select].height-size.height-m_pre_draw_offset;
UIColor *color=[UIColor colorWithRed:r/255.0f green:g/255.0f blue: b/255.0f alpha:1.0f];
[color set];
[text drawInRect:CGRectMake(0,y,sx,size.height) withFont:m_font lineBreakMode:UILineBreakModeWordWrap alignment:UITextAlignmentLeft];
m_pre_draw_offset+=size.height;
m_size_x[m_pre_draw_cnt]=size.width;
m_size_y[m_pre_draw_cnt]=size.height;
m_pre_draw_cnt++;

こんな感じで、drawInRectするY座標を変えながら描画していくだけです。将来的には、もっとかっこいいアルゴリズムで位置配置したいですね。

ということでPowerVRではglTexSubImageの呼び出しを最小限に抑えるのがオススメです。