メイン・ループ
このセクションでは、TouchGFXのグラフィック・エンジンの動作、特にメイン・ループの働きについてさらに説明します。 復習になりますが、グラフィック・エンジンの主なタスクは、アプリケーションのグラフィックス(UIモデル)をフレームバッファに描画することでした。 このプロセスが何度も繰り返され、画面上に新しいフレームが作成されます。
グラフィック・エンジンは、画面タッチやボタン押下などの外部イベントを収集します。 これらのイベントはフィルタ処理され、アプリケーションに送られます。 アプリケーションはこれらのイベントを使用して、UIモデルを更新します。 たとえば、 ユーザがスクリーンのボタン上をタッチしたときにボタンを押下状態に変更し、その後、ユーザがスクリーンをそれ以上タッチしなければ、ボタンを元の解放状態に戻すといった動作です。
最終的に、グラフィック・エンジンは更新されたモデルをフレームバッファに描画します。 このプロセスは永遠にループします。
フレームを描画した後、フレームバッファはディスプレイに転送され、そこでユーザはグラフィックスを目にすることができます。 ディスプレイ上の目障りなグリッチを防ぐために、ディスプレイへの転送はディスプレイと同期する必要があります。 一部のディスプレイでは、最小限の間隔で定期的に転送する必要があります。 その他のディスプレイでは、ディスプレイから信号が送信されたときに、転送することが必要になります。
グラフィック・エンジンは、ハードウェア抽象化レイヤからの"go"信号を待つことで、この同期化を実行します。 ハードウェア抽象化レイヤの詳細については、こちらを参照してください。
疑似コードでは、TouchGFXグラフィック・エンジン内部のメイン・ループは次のようになります。
while(true) {
collect(); // Collect events from outside
update(); // Update the application ui model
render(); // Render new updated graphics to the framebuffer
wait(); // Wait for 'go' from display
}
実際の実装ではコードがさらに深く関与しますが、上記の疑似コードはエンジンの主要部分の理解に役立ちます。
ここに示された4つのステージについて、以下に詳しく説明します。
Collect(収集)
このフェーズでは、グラフィック・エンジンは外部環境からイベントを収集します。 これらのイベントは、通常はタッチ・イベントとボタンです。
TouchGFXはイベントをサンプリングし、アプリケーションに伝えます。 元のタッチ・イベントは、以下に示すより具体的なタッチ・イベントに変換されます。
- クリック: ユーザがディスプレイを指で押すか、ディスプレイから指を離しました。
- ドラッグ: ユーザが(ディスプレイにタッチしたまま)ディスプレイ上で指を動かしました。
- ジェスチャ: ユーザが一方向に指をすばやく動かして離しました。 これはスワイプと呼ばれ、グラフィック・エンジンによって認識されます。
イベントは、現在アクティブなUI要素(ウィジェットなど)に送られます。
エンジンはティック・イベントも送ります。 このイベントは新しいフレーム(または時間のステップ)を表し、他に外部入力がない場合でも、常に送信されます。 このイベントは、アプリケーションによって、アニメーションやその他のタイムベースのアクション(一定時間経過した後に、一時停止画面への変更など)の駆動に使用されます。
Update(更新)
次に、グラフィック・エンジンがアプリケーションと連携して、収集したイベントを反映するようにUIを更新します。 グラフィック・エンジンは、現在アクティブなスクリーンを認知し、このオブジェクトにイベントを渡します。
基本原理として、エンジンがアプリケーション(UIモデルのスクリーンおよびウィジェット・オブジェクト)にイベントに関する情報を知らせます。 それに反応して、アプリケーションがディスプレイの特定部分の再描画を要求します。 アプリケーションは、イベントに反応して直接描画するのではなく、ウィジェットのプロパティを変更して再描画を要求します。
たとえば、Clickイベントが発生した場合、グラフィック・エンジンはスクリーン・オブジェクトのシーン・モデルを検索して、イベントを受け取るべきウィジェットを見つけます。 ImageやTextAreaなどの一部のウィジェットは、Clickイベントを受け取る必要がありません。 さらにこれらには空のイベント・ハンドラがあるので、何も起こりません。
Buttonなどの他のウィジェットは、Clickイベント(押下または解放)に反応します。 Buttonウィジェットは、押下されると別の画像を表示するように状態を変更し、タッチが解放されて元に戻ると、状態を元に戻します。
Buttonなどのウィジェットがその状態を変更した場合には、フレームバッファの再描画も必要になります。 ウィジェットは、イベントに反応して、このことをグラフィック・エンジンに伝える役割を担っています。 グラフィック・エンジンそのものが、収集したイベントに基づいてウィジェットを再描画するわけではありません。 ウィジェットは自身の内部状態(Buttonであれば、描画すべき画像)を追跡し、そのウィジェットがカバーするディスプレイ上の部分(長方形)の再描画を、グラフィック・エンジンに指示します。
アプリケーションそのものが、イベントに反応することもできます。 一般的に、次の2つの方法のいずれかが使用されます。
- TouchGFX Designerでウィジェットのインタラクションを設定: たとえば、Buttonが押されたときに別のウィジェットが表示されるように、インタラクションを設定できます。 このインタラクションは、Buttonが状態を変更し、Buttonそのものの再描画をグラフィック・エンジンから要求された後に実行されます。 インタラクションを使用して別の(非表示の)ウィジェットを表示する場合、アプリケーションもグラフィック・エンジンからの再描画を要求する必要があります。
- スクリーン上でのイベントへの反応: スクリーンそのものでイベントに反応することもできます。 イベント・ハンドラはScreenクラスの仮想関数です。 これらの関数は、アプリケーションのScreens内に再実装できます。 これは、ユーザがスクリーンにタッチしたときに、タッチがウィジェット上であれば場所に関係なく、常にアクションを実行するような場合に使用できます。
Screenクラスには以下のイベント・ハンドラがあります。 これらは、対応する外部イベントが収集されたときに、グラフィック・エンジンによって呼び出されます。
framework/include/touchgfx/Screen.hpp
virtual void handleClickEvent(const ClickEvent& evt);
virtual void handleDragEvent(const DragEvent& evt);
virtual void handleGestureEvent(const GestureEvent& evt);
virtual void handleTickEvent();
virtual void handleKeyEvent(uint8_t key);
これらのイベント・ハンドラには、任意のC++コードを挿入できます。 通常は、アプリケーションが一部のウィジェットの状態を更新するか、一部のアプリケーション固有の関数(ビジネス・ロジック)を呼び出します。
タイムベース更新
handleTickEventイベント・ハンドラは、すべてのフレームごとに呼び出されます。 これにより、アプリケーションはユーザ・インタフェースのタイムベース更新を実行できます。 例として、10秒後にウィジェットが消えていくようにすることが考えられます。 1秒に60フレームあるとすると、コードは以下のようになります。
void handleTickEvent() {
tickCounter += 1;
if (tickCounter == 600) {
myWidget.startFadeAnimation(0, 20); // Fade to 0 = invisible in 20 frames
}
}
グラフィック・エンジンは、Modelクラスでもイベント・ハンドラを呼び出します。 通常、このイベント・ハンドラは、メッセージ・キューの確認やGPIOのサンプリングといった繰り返しのアクションを実行するために使用されます。
void Model::tick() {
bool b = sampleGPIO_Input1(); // Sample polled IO
if (b) {
...
}
}
再描画の要求
上のButtonの例で説明したように、ウィジェットは状態を変更するときに再描画を要求する役割を担います。 この背後にあるメカニズムは無効化領域と呼ばれます。
Buttonが(たとえば解放から押下へ)状態を変更し、再描画が必要になった場合、Buttonウィジェットがカバーする領域は無効化領域になります。 グラフィック・エンジンは、フレームに対して要求されるこれらの無効化領域のリストを保持しています。 収集されたイベント(タッチ、ボタン、ティック)のすべてにおいて、1つ以上の無効化領域が生成される可能性があるので、フレームごとに多数の無効化領域が存在することになります。
Screenクラスのイベント・ハンドラも領域の再描画を要求できます。 ここでは、フレーム10のBoxウィジェット(box1)の色を変更し、Boxでinvalidateメソッドを呼び出すことで再描画を要求します。
void handleTickEvent() {
tickCounter += 1;
if (tickCounter == 10) {
box1.setColor(Color::getColorFrom24BitRGB(0xFF, 0x00, 0x00)); // Set color to red
box1.invalidate(); // Request redraw
}
}
この例では、グラフィック・エンジンがフレームごとに、handleTickEventハンドラを呼び出します。 10番目のフレームで、アプリケーション・コードが、box1によってカバーされる領域の再描画を要求します。 これに反応して、グラフィック・エンジンが、box1ウィジェットに保存されている色を使用して、フレームバッファ内でこの領域を再描画します。
下に示すユーザ・インタフェースでは、バックグラウンド画像上にButtonウィジェットとBoxウィジェットがあります。 ButtonがクリックされたときにBoxの色が変更されるように、Buttonにインタラクションを挿入した場合、ユーザがButtonをクリックすると2つの無効化領域(赤色で表示)が生まれます。
Boxの領域は無効化され、フレームバッファ内で新しい色が描画されます。 Buttonは自身も無効化し、再び解放状態の描画になります。
Render(描画)
前述したように、更新フェーズの結果として、再描画する領域(無効化領域)のリストが作成されます。 描画フェーズのタスクは、基本的にはこのリストの順番に沿って、これらの領域をカバーするウィジェットをフレームバッファに描画していくことです。
このフェーズはグラフィック・エンジンによって自動的に処理されます。 アプリケーションはシーン・モデル(UIのウィジェット)を定義済みで、一部の領域を無効化しています。 残りはエンジンが処理します。
グラフィック・エンジンは無効化領域を1つずつ処理します。 領域ごとに、エンジンはシーン・モデルをスキャンし、その領域によって(一部または全体が)カバーされるウィジェットのリストを収集します。
このウィジェットのリストが入手できたら、グラフィック・エンジンはウィジェット上で描画メソッドを呼び出します。 バックグラウンドのウィジェットから始まり、最前面のウィジェットが最後です。
ウィジェットの描画メソッドは、フレームバッファへの描画時に、そのウィジェットの状態(色など)を使用します。 ウィジェットの描画に必要な情報はすべて、更新フェーズの間にウィジェットに保存されている必要があります。 そうでない場合、描画フェーズでこの情報を使用できません。
Wait(待機)
TouchGFXグラフィック・エンジンは、次のフレームの更新と描画の前に、信号を待ちます。 フレームをできる限り速く連続で描画するのではなく、フレームとフレームの間で待機するのには2つの理由があります。
描画はディスプレイと同期します。 前述のとおり、一部のディスプレイではフレームバッファを繰り返し転送する必要があります。 この転送中に、勝手にフレームバッファに描画することはお勧めできません。 このためグラフィック・エンジンは、転送の開始後、描画開始まで短い時間待機します。 その他のディスプレイは、フレームバッファを転送する必要があるときには、マイクロコントローラに信号を送信します。 グラフィック・エンジンはこの信号を待ちます。
フレームは、固定レートで描画されます。 多くの場合、フレームが固定レートで描画される方がアプリケーションにとっては都合がよいです。一定時間続くアニメーションを簡単に作成できるからです。 たとえば、60 Hzのディスプレイがある場合、2秒のアニメーションを表示するには、120フレームで完了するようにプログラミングすることになります。
グラフィック・エンジンが待機している時間は、通常、アプリケーションの中で他の優先度の低いプロセスに使用されます。 このような場合、時間の無駄にはなりません。いずれにせよ、こうした優先度の低いプロセスはどこかの時点で実行する必要があるからです。
フレームバッファの処理
グラフィック・エンジンは、フレームバッファの更新前にディスプレイと同期するという、これまでの説明を思い出してください。 フレームバッファへの描画後、グラフィック・エンジンは、ディスプレイに更新後のフレームバッファが表示されていることを確認する必要もあります。
2つのフレームバッファ
最もシンプルなセットアップでは、2つのフレームバッファを使用できます。 グラフィック・エンジンは、2つのフレームバッファ間を切り替えます。 フレームを1つのフレームバッファに描画するときに、もう一方のフレームバッファからディスプレイに転送(および表示)されます。
この図では、パラレルRGBディスプレイが、LTDCコントローラに接続されているものとします。 つまり、フレームごとに、フレームバッファをディスプレイに転送する必要があります。 フレームバッファが2つあるので、グラフィック・エンジンは、1つのフレームバッファから転送中に、もう一方のフレームバッファに描画することができます。 このスキームはとても上手く機能するので、バッファを2つ持つことが可能であれば推奨されます。
上の図では、グラフィック・エンジンがフレームごとに描画を行うので、すべてのフレームで新しいフレームバッファの転送も行っています。
アプリケーションが何も更新を行わないフレームが存在することはよくあります。 何も描画されていないということです。 つまり、後続のフレームでは同じフレームバッファが再び転送されることになります。
アプリケーションはフレーム2では何も描画していないので、グラフィック・エンジンはフレーム3でフレームバッファ2を再転送します。
一般的なパラレルRGBディスプレイのリフレッシュ・レートはおよそ60 Hzです。 マイクロコントローラは、この更新頻度を維持する必要があります。 この更新頻度は、再び転送を開始する前に、新しいフレームを描画するための時間が16 msであることを意味します。 場合によっては、新しいフレームの描画時間が16 msを超えることもあります。 この場合、グラフィック・エンジンは単純に(前と)同じフレームをもう一度転送します。
フレーム1の描画時間は16 msを超えているので、前にフレームバッファ1に描画されたフレーム0が再転送されています。 フレームバッファ2の新しいフレームは、フレーム3で転送されます。 2つのフレームバッファが使用可能な場合、描画時間が非常に長くなる可能性があります。 新しいフレームの準備ができるまで、前のフレームが何度も再転送されます。
1つのフレームバッファ
一部のシステムでは、1つのフレームバッファ用のメモリしかないことがあります。 パラレルRGBディスプレイを使用する場合には、すべてのフレームをフレームバッファ1で転送せざるを得ません。
グラフィック・エンジンが強制的に描画するフレームバッファは、同時にディスプレイに転送もされているため、問題が発生しやすくなります。 注意して実行しなければ、前のフレームと新しいフレームが混在するフレームが、ディスプレイに表示される危険性が高くなります。
1つの解決策は、転送が完了するまで描画を保留し、転送が再開されるまでの時間枠でのみ描画することです。 転送時間がフレーム時間全体のかなりの部分を占めるので、これによって稼げる描画のための時間はわずかです。 もう1つの短所として、次の転送の開始時に描画が完了していない場合、やはり不完全なフレーム(ティアリング)が発生する可能性があります。
もう少し可能性の高い解決策は、転送済みのフレームバッファの量を追跡し、フレームバッファの当該部分への描画を制限することです。 転送が進むにつれ、描画アルゴリズムで使用可能なフレームバッファの量は増えていきます。
グラフィック・エンジンには、描画が正しく実行されていることをプログラマが確認するために役立つアルゴリズムが含まれています。
アプリケーションはフレームごとに、フレームバッファを更新し、描画します。
フレーム内で何も更新が行われていない場合、フレームバッファは変更なしで再転送されます。
描画時間が16 msを超える場合、再転送の開始時に描画が終了していません。
この状況では、グラフィック・エンジンは、転送されている部分が完全に描画済みであることを確認する必要があります。 そうでない場合、ディスプレイに未完成のフレームバッファが表示されます。
次のセクションでは、個々のウィジェットの描画時間について説明します。 プログラマが高パフォーマンスのアプリケーションを作成するために役立つ内容です。