深夜開発

短期記憶しか保持できない私の為の外部記憶装置

RenderDocを使ったUnrealEngineプロジェクトのGPUデバッグ

RenderDocのインストール

  1. RenderDocインストーラ公式からダウンロード
  2. ダウンロードしたインストーラを実行
    • ※UnrealEngineを終了してからインストールを行う事
  3. セットアップウィザードが起動するのでNextをクリック
  4. I accept the terms in the License AgreementをチェックしてNextをクリック
  5. 個別設定をしないのならばTypicalまたはCompleteを選択後、Nextをクリック
  6. インストール設定に問題が無ければInstallをクリック
  7. インストールが完了したらFinishをクリック

UnrealEditor上からRenderDocを使う

  1. プラグイン設定を開く
  2. RenderDocプラグインを有効化
    1. RenderDocで検索
    2. プラグインにチェック
    3. 今すぐ再起動でエディタを再起動する
      RenderDocプラグインを有効化
  3. RenderDocプラグインの初期設定を行う
    1. プロジェクト設定を開く
      • メニュー『編集』→『プロジェクト設定』
      • ツールバー『設定』→『プロジェクト設定』
    2. 『プラグイン』→『RenderDoc』→『詳細設定』を開く
    3. スタートアップ時に自動アタッチにチェック
      • UnrealEditorの起動引数に-AttachRenderDocを追加することでも有効化可能
    4. RenderDoc実行ファイルパスにRenderDocをインストールしたパスを設定
      • RenderDocインストール時、特に変更していなければ C:\Program Files\RenderDoc となっているはず
    5. エディタを再起動する
      RenderDocの詳細設定
  4. 有効化されたRenderDocを使ってシーンキャプチャを行う
    • 右上のRenderDocアイコンをクリックしてキャプチャ
    • コンソールコマンドrenderdoc.CaptureFrameを実行してキャプチャ
      キャプチャフレーム
  5. キャプチャに成功すれば自動的にRenderDocが起動する

パッケージ上からRenderDocを使う

  1. RenderDocを起動し、Launch Applicationタブを開く
    LaunchApplicationタブを開く
  2. Executable Pathにプロファイルを行うアプリケーションの実行ファイルを指定し、Launchをクリックして起動
    • 作業ディレクトリを実行ファイルと別の場所を指定する場合のみ、Working Directoryを設定
    • 必要があれば起動引数をCommand-line Argumentsへ指定
      • 複数のRHIを含んだパッケージ作成していれば、起動引数から使用するRHIを指定可能
        • DirectX11: -d3d11
        • DirectX12: -d3d12
        • Vulkan: -vulkan
    • 通常UnrealEngineで作成したパッケージの場合、『Capture Option』→『Capture Child Processes』にチェックを入れる
      起動する
  3. 起動すると親プロセスタブが開くので、『Child Process』→『[ProjectName].exe [PID ???]』をダブルクリックして子プロセスのタブ開く
    子プロセスを開く
  4. 任意のタイミングで『Tools』→『Capture Frame(s) Immediately』をクリックするか、ゲーム画面上からF12を押すと、Captures collectedにキャプチャした内容が列挙される。
  5. 『Captures collected』に列挙されたキャプチャをダブルクリックで開くことで、詳細を確認することが出来る

RenderDoc以外のGPUプロファイラ

RenderDocは大抵のGPUデバッグできる反面、プロファイル用途として使用するには取得できる内部カウンタが不正確などといった欠点がある。 正確な内部カウンタを取得したい場合には、プロファイルを行いたい環境に合ったプロファイラを使ってください

  1. NVIDIA製品専用
  2. AMD製品専用
  3. Intel製品専用
  4. Windows専用
  5. Qualcomm Snapdragon(Adreno)専用
  6. Arm Mali専用

Win32 threadを使ったスレッド生成

Win32API

Microsoft Windows上で用いられるAPIで、スレッドに限らずあらゆる機能を提供する。 通常スレッドを生成するにはCreateThread関数を使用するが、諸事情で同じ機能を提供する_beginThread関数、AfxBeginThread関数なども存在する

Sample code

#include <stdio.h>
#include <windows.h>

#define STACK_SIZE (512 * 1024)

struct thread_args
{
    int r;
    int a;
    int b;
    int c;
}

// 生成したスレッドで実行する関数
UINT thread_entry(void *args) {
    auto targs = reinterpret_cast<thread_args*>(args);
    targs->r = targs->a * targs->b + targs->c;

    return 0;
}

int main()
{
    thread_arg args;
    args.r = 0;
    args.a = 10;
    args.b = 20;
    args.c = 30;

    UINT thread_id = 0;
    auto thread_handle = reinterpret_cast<HANDLE>(_beginthreadex(
        nullptr,                               // セキュリティ属性
        static_cast<unsigned int>(STACK_SIZE), // スタックサイズ
        thread_entry,                          // スレッドの実行を開始する関数
        &args,                                 // スレッド関数に渡される引数
        CREATE_SUSPENDED,                      // 初期化フラグ。『CREATE_SUSPENDED』を指定すると一時停止状態で生成される
        &thread_id                             // スレッド識別子
    ));

    // スレッド優先度を設定
    // 『THREAD_PRIORITY_ABOVE_NORMAL』は通常より1ポイント上の優先順位
    SetThreadPriority(thread_handle, THREAD_PRIORITY_ABOVE_NORMAL);

    // スレッドの実行を開始する
    ResumeThread(thread_handle);

    // スレッドの終了を待つ
    WaitForSingleObject(thread_handle, INFINITE);

    // スレッドハンドルを閉じる
    CloseHandle(thread_handle);

    // 結果を出力
    printf("value = %d\n", args.r);

    return 0;
}

出力

value = 230

std::threadを使ったスレッド生成

std::thread

C++11より導入された、新しい実行スレッドを生成、待機、その他の操作を行う仕組みを提供するテンプレートクラス。 std::threadのコンストラクタに与えられた関数オブジェクトと、このコンストラクタを呼び出したスレッドとの間で並行に処理が実行される。

コンストラクタの定義は、

template <class F, class ...Args>
explicit thread(F&& f, Args&&... args);

このような可変引数の形になっていて、ラムダ式と共に用いられる場合が多い。

機能は最小限のものしか提供されない為、実装環境に依存したスレッドハンドルを取得するメソッドが提供されており、

native_handle_type native_handle();

このハンドルを使う事で高度なスレッド操作を行うことが出来る。 通常このハンドルは、

  • Unix系環境(libstdc++、libc++)では『pthread_t』
  • MS Windows環境(Visual C++)では『HANDLE』

を表している。 各実装環境でのスレッド生成は以下を参照

kamai-tech-lab.hatenablog.com kamai-tech-lab.hatenablog.com

Sample code

#include <stdio.h>
#include <thread>

int main()
{
    int value = 0;

    // スレッドを作成、発行
    // ここで生成したスレッドの返り値は無視されるため、結果を受け取る変数の参照も渡しておく
    auto mad = std::thread([](int &r, int a, int b, int c)
    {
        r = a * b + c;
    }, std::ref(value), 10, 20, 30);


#ifdef WIN32
    // スレッド優先度を設定
    // 『THREAD_PRIORITY_BELOW_NORMAL』は通常より1ポイント下の優先順位
    SetThreadPriority(mad.native_handle(), THREAD_PRIORITY_BELOW_NORMAL);
#elif defined(__linux__)
    sched_param param;
    param.sched_priority = 10;

    // スケジューリングポリシー、優先度を設定
    pthread_setschedparam(mad.native_handle(), SCHED_RR, &param);
#endif

    // スレッドが完了するまで待つ
    mad.join();

    // 結果を出力
    printf("value = %d\n", value);
    
    return 0;
}
出力
value = 230

pthreadを使ったスレッド生成

pthread

一般的にPOSIXスレッドの標準実装ライブラリの事を指し、スレッドの生成や基本的な操作を行うことが出来る。 POSIXをサポートしている多くの環境(主にUnix系)で利用できる反面、標準では基本的な機能のみが提供されるため、環境に依存した高度な処理を行う場合には拡張命令を使用する必要がある。

Sample code

#include <stdio.h>
#include <pthread.h>

#define STACK_SIZE (512 * 1024)
#define SCHED_POLICY SCHED_OTHER
#define SCHED_PRIORITY 10

#define CLAMP(x, min_value, max_value) (x < min_value ? min_value : (max_value < x ? max_value : x))

struct thread_arg
{
    int r;
    int a;
    int b;
    int c;
};

// 生成したスレッドで実行する関数
void* thread_entry(void *args)
{
    auto targs = reinterpret_cast<thread_arg*>(args);
    targs->r = targs->a * targs->b + targs->c;

    return nullptr;
}

int main()
{
    pthread_attr_t attr;
    pthread_attr_init(&attr);

    // 生成するスレッドのスタックサイズを設定
    if (pthread_attr_setstacksize(&attr, STACK_SIZE) != 0) {
        pthread_attr_destroy(&attr);
        return 1;
    }

    // 呼び出しスレッドのスケジューリング属性を継承しないよう設定
    if (pthread_attr_setinheritsched(&attr, PTHREAD_EXPLICIT_SCHED) != 0) {
        pthread_attr_destroy(&attr);
        return 2;
    }

    // スケジューリングポリシーを設定
    if (pthread_attr_setschedpolicy(&attr, SCHED_POLICY) != 0) {
        pthread_attr_destroy(&attr);
        return 3;
    }

    sched_param param;

    const int min_priority = sched_get_priority_min(SCHED_POLICY);
    const int max_priority = sched_get_priority_max(SCHED_POLICY);
    param.sched_priority = CLAMP(SCHED_PRIORITY, min_priority, max_priority);

    // スケジューリング優先度を設定
    if (pthread_attr_setschedparam(&attr, &param) != 0) {
        pthread_attr_destroy(&attr);
        return 4;
    }
    
    thread_arg args;
    args.r = 0;
    args.a = 10;
    args.b = 20;
    args.c = 30;

    // スレッドのハンドル
    pthread_t thread;

    // スレッドを生成、発行
    if (pthread_create(&thread, &attr, thread_entry, &args) != 0) {
        pthread_attr_destroy(&attr);
        return 5;
    }

    // スレッド属性を破棄
    pthread_attr_destroy(&attr);

    // スレッドの終了を待つ
    // スレッドの内部メモリを解放する為、pthread_join または pthread_detach を必ず呼ばなければならない
    pthread_join(thread, nullptr);

    // 結果を出力
    printf("value = %d\n", args.r);

    return 0;
}
出力
value = 230