Blender内部構造(2) メインループ

前回は、Blenderユーザから見てわかりやすいオペレータ実行時の処理を題材にしてBlenderの内部処理を説明しました。
今回はプログラムを書く人の視点を考え、プログラムの最初に実行されるmain関数の処理がどのソースコードに記載されているのか確認したあと、main関数の延長で実行されるアプリケーションのメインループで行われていることを見ていきたいと思います。

main関数

C言語の勉強を始めたばかりのときに、プログラムは main 関数から実行開始されることを最初に習うと思いますが、Blenderも同じように source/creator/creator.c にmain関数が宣言されています。

// source/creator/creator.c

// main関数
int main(int argc,
#ifdef WIN32
         const char **UNUSED(argv_c)
#else
         const char **argv
#endif
)
{
    // 各種初期化処理

    // メインループ
    WM_main(C);

    return 0;
}

Blenderのmain関数はさまざまな初期化を行ったあとに、WM_Main 関数を呼び出してアプリケーションのメインループに入ります。
本記事では、Blenderの初期化処理には触れずに、メインループの中身を見ていきたいと思います。

WM_Main関数

WM_Main 関数は、source/blender/windowmanager/intern/wm.c に宣言されています。

// source/blender/windowmanager/intern/wm.c

void WM_main(bContext *C)
{
  // Dependency Graphが評価される前にオペレータを実行しないようにする
  wm_event_do_refresh_wm_and_depsgraph(C);

  // メインループ
  while (1) {

    // GHOST経由でOS固有のイベントを取得し、各ウィンドウが持つイベントキューへ追加する
    wm_window_process_events(C);

    // 各ウィンドウが持つイベントキューのイベントそれぞれについて、
    // モーダルハンドラ、ウィンドウ/エリアリージョンのイベントハンドラを呼び出す
    wm_event_do_handlers(C);

    // ウィンドウマネージャに登録されたNotificationを処理する
    wm_event_do_notifiers(C);

    // 描画処理
    wm_draw_update(C);
  }
}

WM_Main 関数では、最初に wm_event_do_refresh_wm_and_depsgraph 関数を呼んだあと、while ループでアプリケーションのメインループに入ります。 メインループに入ったあとは、ユーザがBlenderを閉じない限りループ内の処理を繰り返します。

wm_event_do_refresh_wm_and_depsgraph関数

wm_event_do_refresh_wm_and_depsgraph 関数について理解するためには、Dependency Graphについて理解する必要があります。
Dependency Graph は、Blender 2.8より導入された、オブジェクト間の依存関係を表現したグラフです。
ある特定のオブジェクトを更新したときにすべてのオブジェクトを更新するかわりに、Dependency Graphを使って更新したオブジェクトに依存するオブジェクトのみを更新することで、処理量を少なくすることができます。

メインループの前に wm_event_do_refresh_wm_and_depsgraph 関数を呼び出すのは、このDependency Graphの評価が完了していない状態で、ユーザからのオペレータが実行できないようにするためです。
もし、Dependency Graphの評価が完了していない状態でオペレータが実行されてしまうと、正しい描画結果が得られなくなってしまいます。
Dependency Graphについては、wm_event_do_refresh_wm_and_depsgraph 関数の詳細とともにまた別の場所で詳細を見ていきたいと思います。

wm_window_process_events関数

メインループ内で最初に呼ばれる wm_window_process_events 関数は、GHOST経由でOSで発生したイベントを取得します。
GHOST とは、ウィンドウの生成やイベント処理など、Windows/Mac/LinuxなどのOSの違いを吸収するために作られたツールキットで、Generic Handy Operating System Toolkitの略です。
GHOSTが存在することで、BlenderはさまざまなOS上でアプリケーションを動作でき、かつOSに依存した実装をGHOST内に閉じ込めることができています。
GHOSTについては、別の機会に詳しく内部構造を解析した結果をまとめたいと思います。

さて、wm_window_process_events 関数の処理を見てみましょう。

// source/blender/windowmanager/intern/wm_window.c

void wm_window_process_events(const bContext *C)
{
  int hasevent;

  // メインスレッド以外のスレッドで本関数が呼ばれることは禁止
  BLI_assert(BLI_thread_is_main());

  // イベントを取得する(イベントが発生していたらTrueを返す)
  hasevent = GHOST_ProcessEvents(g_system, 0); /* 0 is no wait */

  if (hasevent) {
    // イベントを発火する
    GHOST_DispatchEvents(g_system);
  }
  // ウィンドウマネージャに登録されたタイマからのイベントを扱う。
  // イベントが発生していたら1を返す
  hasevent |= wm_window_timer(C);

  // 何もイベントが発生していないため、5msだけSleepする
  if (hasevent == 0) {
    PIL_sleep_ms(5);
  }
}

wm_window_process_events 関数は、メインスレッド以外から呼び出されることを禁止しています。
これは、GHOSTを使ったイベント制御をマルチスレッドで行う利点がないことと、グローバル変数を積極的に利用しているGHOSTの処理で、マルチスレッドに起因するバグを発生させることを防ぐためであると考えられます。

GHOST_ProcessEvents 関数は、OS上で発生したイベントを取得します。
OS上で発生したイベントは、Windows/Mac/Linuxなどそれぞれで異なる手続きでイベントを扱わなくてはならないのですが、このあたりの処理の違いはGHOSTが隠蔽してくれます。

GHOST_ProcessEvents 関数で取得したイベントは、GHOST_DispatchEvents 関数で処理されます。
GHOST_DispatchEvents 関数の役割は、OS上で発生したイベントをBlenderのウィンドウにOSに依存しない形で登録することです。

wm_window_timer 関数は、ウィンドウマネージャに登録されたタイマからタイマイベントが発生しているかどうかを確認します。
タイマイベントが発生している場合、ウィンドウに発生したタイマイベントを登録し、wm_window_timer 関数は 1 を返して終了します。

OS上のイベントが発生していなく、ウィンドウマネージャに登録されたタイマからもイベントが発生していない場合は、5msだけスリープします。
このように、イベントが発生していたらイベントの結果を即座に反映してインタラクティブ性を失わないようにするとともに、イベントが発生していない場合はCPUを休ませるような処理になっています。

wm_event_do_handlers関数

wm_window_process_events 関数によって各ウィンドウに登録されたイベントは、wm_event_do_handlers 関数によって処理されます。

// source/blender/windowmanager/intern/wm_event_system.c

void wm_event_do_handlers(bContext *C)
{

  for (win = wm->windows.first; win; win = win->next) {
    // ウィンドウに登録されたイベント1つを取得する
    while ((event = win->queue.first)) {
      // ウィンドウに登録されたすべてのモーダルハンドラを呼び出す
      action |= wm_handlers_do(C, event, &win->modalhandlers);

      if ((action & WM_HANDLER_BREAK) == 0) {
        ED_screen_areas_iter(win, screen, sa)
        {

          // Eventがエリア内で発生したものである場合
          if (wm_event_inside_rect(event, &sa->totrct)) {

            if ((action & WM_HANDLER_BREAK) == 0) {
              for (ar = sa->regionbase.first; ar; ar = ar->next) {
                // リージョン内でイベント発生した場合
                if (wm_event_inside_region(event, ar)) {

                  // リージョンに登録されたイベントハンドラを呼び出す
                  action |= wm_handlers_do(C, event, &ar->handlers);

                }
              }  // for (ar = sa->regionbase.first; ar; ar = ar->next)
            }

            if ((action & WM_HANDLER_BREAK) == 0) {
              // エリアに登録されたイベントハンドラを呼び出す
              action |= wm_handlers_do(C, event, &sa->handlers);
            }
          }

        }  // ED_screen_areas_iter(win, screen, sa)

        if ((action & WM_HANDLER_BREAK) == 0) {
          // ウィンドウに登録されたイベントハンドラを呼び出す
          action |= wm_handlers_do(C, event, &win->handlers);
        }
      }

      // イベントをキューから削除
      BLI_remlink(&win->queue, event);
      wm_event_free(event);

    }  // while ((event = win->queue.first))
  }  // for (win = wm->windows.first; win; win = win->next)

}  // void wm_event_do_handlers(bContext *C)

wm_event_do_handlers 関数は長い関数ですが、関数内で行っている処理を簡単にまとめると次のようになります。

  • wm_window_process_events 関数によってウィンドウに登録されたイベント1つ1つに対して、以下の順でハンドラを呼び出す
    1. ウィンドウに登録されたモーダルハンドラ
    2. リージョンに登録されたイベントハンドラ
    3. エリアに登録されたイベントハンドラ
    4. ウィンドウに登録されたイベントハンドラ
  • 呼び出したハンドラの戻り値が WM_HANDLER_BREAK である場合、以降のハンドラは一切呼び出されずに次のイベントの処理が始まる

なおここで示しているモーダルハンドラは、モーダルモード時に登録するハンドラを示し、WM_HANDLER_BREAK はモーダルハンドラが OPERATOR_RUNNING_MODAL を返したときに設定される値です。
モーダルモードに関しては、はじめてのBlenderアドオン開発 においてPython APIの観点からも説明していますので、こちらも参考にしてみてください。
この処理から、Python APIを使って登録したオペレータクラスのモーダルハンドラで {'RUNNING_MODAL'} を返すと、その後のイベントハンドラの処理が呼ばれなくなることがわかります。

wm_event_do_notifiers関数

wm_event_do_handlers 関数などによってイベントが処理されると、Blender内のデータが変化する場合があります。
Blender内のデータが変化したことで再描画などが必要だと判断されると、Notificationとしてウィンドウマネージャに登録されます。
wm_event_do_notifiers 関数は、ウィンドウマネージャに登録されたNotificationを処理するための関数です。

// source/blender/windowmanager/intern/wm_event_system.c

void wm_event_do_notifiers(bContext *C)
{
  // ウィンドウマネージャに登録されたNotificationを1つずつ処理する
  while ((note = BLI_pophead(&wm->queue))) {
    for (win = wm->windows.first; win; win = win->next) {
      if (...) {
         // 意味のないNotificationについては何もしない
      }
      else {
        // スクリーンのNotification処理
        ED_screen_do_listen(C, note);

        ED_screen_areas_iter(win, screen, sa)
        {
          // エリアのNotification処理
          ED_area_do_listen(win, sa, note, scene);

          for (ar = sa->regionbase.first; ar; ar = ar->next) {
            // リージョンのNotification処理
            ED_region_do_listen(win, sa, ar, note, scene);
          }  // for (ar = sa->regionbase.first; ar; ar = ar->next)

        }  // ED_screen_areas_iter(win, screen, sa)
      }
    }  // for (win = wm->windows.first; win; win = win->next)
  }  // while ((note = BLI_pophead(&wm->queue)))
}  // void wm_event_do_notifiers(bContext *C)

wm_event_do_notifiers 関数が行っていることは単純で、スクリーン、エリア、リージョンの順にNotificationを処理するだけです。
Notificationの処理では、wm_event_do_notifiers 関数の後に呼ばれる wm_draw_update 関数で再描画する対象を決めることが主になります。

wm_draw_update関数

メインループの最後に呼ばれる wm_draw_update 関数は、wm_event_do_notifiers 関数にて再描画が必要とされたスクリーン、エリア、リージョンを再描画します。

// source/blender/windowmanager/intern/wm_draw.c

void wm_draw_update(bContext *C)
{
  for (win = wm->windows.first; win; win = win->next) {
    // Blenderのアプリケーションウィンドウが最小化されていたら再描画しない
    if (state == GHOST_kWindowStateMinimized) {
      continue;
    }

    // 再描画不要なら、描画処理は呼ばない
    if (wm_draw_update_test_window(win)) {
      // 描画可能にするための諸設定
      wm_window_make_drawable(wm, win);
      ED_screen_ensure_updated(wm, win, screen);

      // 描画処理
      wm_draw_window(C, win);

      // 再描画フラグのクリアなど、描画後の後処理
      wm_draw_update_clear_window(win);

      // フロントバッファとバックバッファの切り替え
      wm_window_swap_buffers(win);
    }
  }  // for (win = wm->windows.first; win; win = win->next)
}  // void wm_draw_update(bContext *C)

ウィンドウマネージャで管理されるウィンドウ全てに対して、描画が必要か不要かを確認したうえで、描画が必要な場合は wm_draw_window 関数によって描画処理を行います。
Python APIの draw_handler_add 関数を使って、Pythonから登録した描画関数についても、wm_draw_window 関数で呼び出されて描画されます。

描画した後は、wm_draw_update_clear_window 関数を呼び出して再描画フラグのクリアなどの後処理を行ったあと、wm_window_swap_buffers 関数によりフロントバッファとバックバッファをスワップして終了です。

これでメインループの1ループ分の処理が完了したことになります。
まとめると、Blenderのメインループでは以下の処理を繰り返していると言えるでしょう。

  1. OSやウィンドウマネージャに登録されたイベントを取得し、各ウィンドウが持つイベントキューに追加する
  2. 各ウィンドウが持つイベントキューのイベントを処理する
  3. イベント処理によって再描画が必要となったスクリーン、エリア、リージョンに再描画フラグを立てる
  4. 再描画フラグが立っているスクリーン、エリア、リージョンを描画する

おわりに

Blenderの主要な処理であるメインループについて解析してみました。
メインループの処理を解析したことにより、Blenderの全体像が明らかになってきました。 この中で一番の発見であったのは、UIを持つアプリケーションにおいて頻繁に利用されるアーキテクチャ「MVCパターン」に従って、Blenderが実装されているということです。
アーキテクチャが理解できたことで、今後のソースコードの解析が捗りそうです。

Model Blender内部のデータ(オブジェクトやメッシュ、オペレータなど)
View wm_draw_window 関数
Controller wm_window_process_events 関数、wm_event_do_handlers 関数、wm_event_do_notifiers 関数

さて、第1回はBlednerユーザの入り口であるオペレータ、第2回はプログラムの入り口であるmain関数という流れで調べてきましたので、次回はBlenderのアドオンの入り口であるオペレータクラス(bpy.types.Operator から派生したクラス)の登録処理について解析したいと思います。