2021年8月1日日曜日

Unreal Eengin 4 でFASTBuildを利用する

はじめに

UE4 FASTBuildでC++やシェーダーを分散ビルドするによると, 4.26ではうまく動かないらしく, 実際に設定してみても動いていないように見えます.

4.26でFASTBuildの環境を作る

4.27からFASTBuildのバイナリをコピーしてくると動作します. 依存バイナリのダウンロード部分だけを追いきれなかったため, Setup.batで全てダウンロードしています.

4.27のFASTBuildバイナリを用意する

$ mkdir Unreal4.27 & Unreal4.27
$ git clone -b 4.27 https://github.com/EpicGames/UnrealEngine.git
$ Setup.bat

4.27から4.26へバイナリをコピーする

4.27から4.26へ次のディレクトリをコピーする. Engine\Extras\ThirdPartyNotUE\FASTBuild

共有ディレクトリを用意する

全てのホストとワーカから見える共有ディレクトリを用意します.

ビルド設定を用意する

ビルド設定にFASTBuildの設定を追加します.

ビルド設定を使う理由

UnrealBuildToolはFASTBuildが有効な場合, FBuildWorkerをGUIなしで起動しています. 実行済みのFBuildWorkerがある場合は, FBuildWorker.copyを作成して起動しているように見えます.

つまり, ホストのFASTBuildの設定を変更したいためにFBuildWorkerを自力で起動しても反映されません.

また, チームで設定を共有するためでもあります.

次の内容のBuildConfiguration.xml

<?xml version="1.0" encoding="utf-8" ?>
<Configuration xmlns="https://www.unrealengine.com/BuildConfiguration">
    <BuildConfiguration>
        <bAllowFASTBuild>true</bAllowFASTBuild>
    </BuildConfiguration>
    <FASTBuild>
        <FBuildBrokeragePath></FBuildBrokeragePath>
    </FASTBuild>
</Configuration>

次のいずれかに置きます.

  • Engine/Saved/UnrealBuildTool/BuildConfiguration.xml
  • User Folder/AppData/Roaming/Unreal Engine/UnrealBuildTool/BuildConfiguration.xml
  • My Documents/Unreal Engine/UnrealBuildTool/BuildConfiguration.xml

FBuildBrokeragePathは共有ディレクトリを指定します. その他の設定は, 公式 Build Configurationを参照してください.

ワーカでFBuildWorkerを起動する

FBuildWorkerを起動しておくだけです. ホストのFBuildWorkerUnrealBuildToolが起動しています.

プラットフォームごとのFASTBuild

現状でプラットフォームごとにFASTBuild利用の可否や設定を切り替える方法を見つけていません. BuildConfigurationのXMLスキーマでも, FASTBuildの設定はプラットフォームの子になっていません.

FASTBuildを正しく動作させるためにはコンパイラを統一することが重要で, コンシューマ機など固有のコンパイラを使うプラットフォーム向けビルドでFASTBuildを使う場合, もうひと手間必要です (WIP).

監視ツールについて

  • 次の2つに共通する事項
    • モニタリングに関しては, FASTBuildのログを監視して表示しています
  • FASTBuild-Dashboard
    • スタンドアロン型
    • 自身のFBuildWorkerを起動する
      • FBuildWorkerのプロセスが重複した場合の動作は追っていません
  • FASTBuildMonitor
    • Visual Studioの拡張, マーケットプレイスに上げていない
    • ログを監視してモニタリングするだけなので, FBuildWorkerの設定項目はない
    • Visual Studio 2019に対応していなく, プルリクエストも放置されている
    • FASTBuildMonitorをベースに自分でビルドする

シェーダコンパイル

シェーダのビルドは分散されていないように見えます.

(TRY) 4.27で次のファイルに変更があるため, 上書きして試す.

Engine\Source\Programs\UnrealBuildTool\Executors\Experimental\FASTBuild.cs

まとめ

シェーダコンパイルができない, プラットフォームごとに設定変更できない, と現状実用的と言い難いと思います.

2021年7月30日金曜日

Unreal Engine 4 でPakをLZ4圧縮

はじめに

LZ4に正式対応し, Pak File Compression Format(s)で指定できるように思えますが, そんなことはないです.

原因

FCompression::IsFormatValidが原因と思われます. コメントからすると, 不具合のように見えます.

    bool FCompression::IsFormatValid(FName FormatName)
    {
        // build in formats are always valid
        if (FormatName == NAME_Zlib || FormatName == NAME_Gzip)
        {
            return true;
        }

        // otherwise, if we can get the format class, we are good!
        return GetCompressionFormat(FormatName, false) != nullptr;
    }

圧縮形式

zlibのチェックサム計算も軽くはないことに注意です.

  • zlib
    • 圧縮率と速度のバランスが良いが, 今では旧い, zstandardを代わりに使うべき
    • checksumは必ず計算する
  • ZStandard
    • 圧縮率と速度のバランスが良い
    • checksumはオプション
  • LZ4
    • 圧縮・伸張速度に重点を置いたもの
      • LZSSだけなので上記2つより圧縮率は低い
    • 圧縮に時間をかけても, 伸張速度はほとんど変わらない
    • checksumはオプション
  • Lizard
    • LZ5を名乗る, LZ4を改良したと主張している

PakのLZ4圧縮

FCompressionを書き換えることは影響範囲を考えてやりたくないです. エンジンのプラグインを書きます. UnrealPakは独立したプログラムのため, 単純なエディタ拡張ではUnrealPakから見えません.

LZ4CompressionFormat.cpp
#include "LZ4Compression.h"
#include "CoreMinimal.h"
#include "Misc/Compression.h"
#include "Misc/ICompressionFormat.h"
#define LOCTEXT_NAMESPACE "FLZ4CompressionModule"

namespace
{
DEFINE_LOG_CATEGORY_STATIC(LogLZ4CompressionFormat, Log, All);
struct LZ4CompressionFormat : public ICompressionFormat
{
    FName GetCompressionFormatName() override;
    bool Compress(void* CompressedBuffer, int32& CompressedSize, const void* UncompressedBuffer, int32 UncompressedSize, int32 CompressionData) override;
    bool Uncompress(void* UncompressedBuffer, int32& UncompressedSize, const void* CompressedBuffer, int32 CompressedSize, int32 CompressionData) override;
    int32 GetCompressedBufferSize(int32 UncompressedSize, int32 CompressionData) override;
    uint32 GetVersion() override;
    FString GetDDCKeySuffix() override;
};

FName LZ4CompressionFormat::GetCompressionFormatName()
{
    return "lz4";
}

bool LZ4CompressionFormat::Compress(void* CompressedBuffer, int32& CompressedSize, const void* UncompressedBuffer, int32 UncompressedSize, int32 CompressionData)
{
    UE_LOG(LogLZ4CompressionFormat, Warning, TEXT("Compress"));
    if(!FCompression::CompressMemory(NAME_LZ4, CompressedBuffer, CompressedSize, UncompressedBuffer, UncompressedSize, COMPRESS_NoFlags, CompressionData)) {
        UE_LOG(LogLZ4CompressionFormat, Warning, TEXT("Faile to compress"));
        return false;
    }
    return true;
}

bool LZ4CompressionFormat::Uncompress(void* UncompressedBuffer, int32& UncompressedSize, const void* CompressedBuffer, int32 CompressedSize, int32 CompressionData)
{
    UE_LOG(LogLZ4CompressionFormat, Warning, TEXT("Uncompress"));
    if(!FCompression::UncompressMemory(NAME_LZ4, UncompressedBuffer, UncompressedSize, CompressedBuffer, CompressedSize, COMPRESS_NoFlags, CompressionData)) {
        UE_LOG(LogLZ4CompressionFormat, Warning, TEXT("Faile to uncompress"));
        return false;
    }
    return true;
}

int32 LZ4CompressionFormat::GetCompressedBufferSize(int32 UncompressedSize, int32 CompressionData)
{
    return FCompression::CompressMemoryBound(NAME_LZ4, UncompressedSize, COMPRESS_NoFlags, CompressionData);
}

uint32 LZ4CompressionFormat::GetVersion()
{
    return FCompression::GetCompressorVersion(NAME_LZ4);
}

FString LZ4CompressionFormat::GetDDCKeySuffix()
{
    static const FString suffix = "2AEE7CBB0BD24E71B0D516ECE2AB68C1";
    return suffix;
}
} // namespace

FLZ4CompressionModule::FLZ4CompressionModule()
    : compressionFormat_(nullptr)
{
}

FLZ4CompressionModule::~FLZ4CompressionModule()
{
    delete compressionFormat_;
}

void FLZ4CompressionModule::StartupModule()
{
    compressionFormat_ = new LZ4CompressionFormat();
    IModularFeatures::Get().RegisterModularFeature(COMPRESSION_FORMAT_FEATURE_NAME, compressionFormat_);
}

void FLZ4CompressionModule::ShutdownModule()
{
    IModularFeatures::Get().UnregisterModularFeature(COMPRESSION_FORMAT_FEATURE_NAME, compressionFormat_);
    delete compressionFormat_;
    compressionFormat_ = nullptr;
}

#undef LOCTEXT_NAMESPACE

IMPLEMENT_MODULE(FLZ4CompressionModule, LZ4Compression)

まとめ

今回は, Pak File Compression Format(s)lz4を書けばLZ4圧縮になるはずです. これを応用すれば好きなフォーマットにできるはずです.

2021年7月20日火曜日

Unreal Engine 4 Editorの耳障りな音を変更する

変更

次のSEを置き換えればよい.
Engine/Content/EditorSounds/Notifications

この調子だとほとんどのエディタリソースを置き換える必要がありそうです.

2021年7月16日金曜日

WSL上のConcourse CIで, Unreal Engine 4のビルドがしたい

はじめに

CI/CD環境の構築が属人化するなら, もう私が扱いやすい環境があればいいのではないか?, ということで脱Jenkinsを行い, ConcourseでどうしてもUE4のビルドを自動化したいです. WSLやWSL上のDockerの構築については省略します.

Jenkins

Jenkinsではだめなのかというと, 扱い辛いからです. よく属人化すると言われますが, その理由を説明した資料を私は知りません. 私のまわりでは次の理由だと思います.

  • 不安定
    • 全く設定を変更していないつもりでもビルドが壊れる
  • 設定が難しい
    • パイプライン追加以前では, VCSで管理したり, 再利用しようとするとシェルスクリプトやバッチファイルにするしかなかった. 少し高級なcronでしかなかった
      • シェルスクリプトやバッチファイルに詳しい人に依存する
    • パイプラインは, GroovyのDSLでわかりにくい
      • 他に役に立たないDSLを勉強したくない, ドキュメントもわかり辛い
      • DSLなのでJenkins上でしか実行できない. パイプラインを書いてコミットしてJenkins上で実行して失敗の繰り返し, 苦痛でしかない
  • CI/CDの重要性に興味・理解ある人しか, 修正・改善をしない
    • どんなツールを使ってもこのために属人化する

WSL

WSLである理由は環境移行が楽かもしれないということです. 今日の企業ではレンタルPCで社員の環境を作ることが多く, ビルドマシンも同じくレンタルPCが多いです. それほど頻繁ではないですが, レンタル期間終了とともに環境移行を強いられます. 以前にWindows上のVM環境にビルド環境を構築して移行を楽にしようと頑張りましたが失敗しました. 原因はファイルシステムのアクセスです. VMからはともかく, CI/CDツール越しではうまくいきませんでした. WSLならもう少し楽になるのでは?

UE4のビルド

Windows向けビルドしか試していませんが, RunUAT.batが呼び出せれば大体解決しそうです.

Concourse CI インストール

鍵生成

まず, コンコースのサーバ・クライアント・ユーティリティを兼ねたバイナリを取得します. このバイナリさえあればDockerは必要ないのですが, DBの構築も面倒なので, このバイナリは鍵生成だけに使います.

$curl -OL https://github.com/concourse/concourse/releases/download/vX.X.X/concou$rse-X.X.X-linux-amd64.tgz
$tar zxvf concourse-X.X.X-linux-amd64.tgz

バイナリを取得できたら鍵を生成します. ポイントはワーカの公開鍵を別名でコピーしている部分です.

$mkdir -p keys/web keys/worker
$./concourse/bin/concourse generate-key -t ssh -f ./tsa_host_key
$./concourse/bin/concourse generate-key -t ssh -f ./worker_key

$cp ./worker_key.pub ./keys/web/authorized_worker_keys

$mv ./tsa_host_key ./keys/web/
$mv ./worker_key ./keys/worker/
$mv ./tsa_host_key.pub ./keys/worker/
$mv ./worker_key.pub ./keys/web/ 

Docker-compose

docker-compose.ymlを作成します.

docker-compose.yml
version: '3'

services:
  concourse-db:
    image: postgres
    container_name: concourse-db
    environment:
      POSTGRES_DB: concourse
      POSTGRES_PASSWORD: concourse_pass
      POSTGRES_USER: concourse_user
      PGDATA: /var/lib/postgresql/data/pgdata
    volumes:
      - ./db/postgres:/var/lib/postgresql/data
      - ./db/logs:/var/log

  concourse:
    image: concourse/concourse
    container_name: concourse
    restart: unless-stopped
    command: web
    privileged: true
    depends_on: [concourse-db]
    ports: ["8000:8080", "2222:2222"]
    volumes: ["./keys/web:/concourse-keys"]
    environment:
      CONCOURSE_POSTGRES_HOST: concourse-db
      CONCOURSE_POSTGRES_USER: concourse_user
      CONCOURSE_POSTGRES_PASSWORD: concourse_pass
      CONCOURSE_POSTGRES_DATABASE: concourse
      CONCOURSE_EXTERNAL_URL: http://localhost:8000
      CONCOURSE_ADD_LOCAL_USER: admin:admin
      CONCOURSE_MAIN_TEAM_LOCAL_USER: admin
      CONCOURSE_WORKER_BAGGAGECLAIM_DRIVER: overlay
      
  concourse-worker:
    image: concourse/concourse
    privileged: true
    links: [concourse]
    depends_on: [concourse]
    command: worker
    volumes: ["./keys/worker:/concourse-keys"]
    environment:
      CONCOURSE_TSA_HOST: concourse:2222

Windowsのワーカ

これが重要です. WSL上のシェルからなんとかできるかと試行錯誤しましたが, 妥協してWindows上でワーカを動かす方が楽でした. Windows用のconcourseバイナリを取得して起動するだけです.

concourse worker --work-dir ./work --tsa-host localhost:2222 --tsa-public-key ./keys/worker/tsa_host_key.pub --tsa-worker-private-key ./keys/worker/worker_key

プロジェクト管理

targetはサーバに対応します. 1サーバで複数プロジェクトを管理するなら, チームを使用します.

fly -t server_name set-team --team-name project_name --local-user user_name

ログインは, -nでチーム名を指定します.

fly -t server_name login -n project_name -u user_name -p user_pass

ビルド

最も単純なビルド設定は次のようになります. 結局, バッチファイルに行きついていますが, バッチファイルの引数である程度柔軟にできると思います.

jobs:
  - name: build_quickstart
    plan:
      - task: build
        config:
          platform: windows
          run:
            path: cmd.exe
            args: ["/c", "chcp 65001 & cd /d ((PROJECT_ROOT)) & call build.bat"]

パラメータ((PROJECT_ROOT))は別のファイルenvironment.ymlで定義しています.

PROJECT_ROOT: "X:/Path to Unreal Project"

パイプライン設定時に-lオプションでパラメータ設定を指定します.

$ fly -t main sp -p build -c build.yml -l environment.yml

build.batも一応書いておきます, environment.batはRunUAT.batのパスを設定しているだけです.

call environment.bat
%UNREAL_RUNUAT% BuildCookRun -project=%~dp0/project.uproject -build -cook -stage -allmap -pak -nop4 -partialgc -platform=Win64 -clientconfig=Development

DebianでDockerサービスが起動に失敗する

DockerがiptablesでNATの設定をしようとするが, Debianは別のソフトウェアでNATを設定しているので失敗する, でいいのでしょうか. 

Docker forum 

sudo update-alternatives --set iptables /usr/sbin/iptables-legacy
sudo update-alternatives --set ip6tables /usr/sbin/ip6tables-legacy

2021年6月17日木曜日

Pimplはアンチパターンだと思います

はじめに

数年前は使うこともあったのですが, 現代にはふさわしくないと思い使わなくなりました.

利点と欠点, それについての考え

利点

  • includeを減らして, コンパイル時間の短縮
    • 2021年では, 計算機, コンパイラ, そしてリンカの性能が向上しました. これは使う理由にはなりません. 聞こえていますか?20年前から来たあなた
      • 時間は金で買うべきです, ビルドマシンぐらい金をかけましょう
    • 今のプロジェクトのビルドが遅い理由は, 次です. ビルドが遅すぎて, ビルドは一日一回, agileではなく, sluggishです, stupidプロジェクトでもいいです
      • Pimplを数多く使っているのに, includeが多い. コピペのせいもあるのですが
        • リファクタリングをしないので, コピペが修正されることは2度とない
      • inlineが多い
        • inlineを理解していない
        • コンパイラオプションで, できるだけinline展開するようにしている
        • inline展開で処理が速くなるとは限らない, これは今も昔も
      • コンパイラオプションで最大限の最適化を設定している
        • O2より上は速くなるかは環境依存, 遅くなることはよくある
          • 使用者が少ないので, 不具合も多くなる
        • コンパイラを理解していない
  • 実装の隠蔽
    • 例えば, JavaやC#で困ったことはありますか?
      • 自分の意に沿わないからといって, ヘッダを書き換えるようなエンジニア?をチームに置いておきますか? そちらの方が問題だと思います
    • インターフェイスを使って, FILEのようなことをすれば完全に隠蔽できるので, 使う理由にはなりません

欠点

  • メモリコストと実行速度に悪影響がある
    • 処理落ちがどうのなど, 言っていることとやっていることが異なる
  • 冗長
    • 処理を転送するだけの記述がコード量を増やす
    • コードを追うことに2倍のコストがかかる
    • たまにインターフェイスのメソッド名と, 実体のメソッド名が異なる
      • ガンジーでも助走付けて
  • ヘビークラスを作りやすい
    • これは当社比なのですが, 10年以上のエンジニア経験では, Pimplイディオムはくそでかクラスになりやすい傾向にあります
      • コンパイル時間の短縮という錦の旗, メモリコストと実行速度を考えると, Pimplイディオムがコンポジションに向いていないことが原因と思っています
    • コンポジションやアグリゲーションで, ソフトウェアを建築する, という考え方を持っている人には受け入れがたいです

まとめ

他人に強制するつもりはありませんが, 意味のないイディオムだと思います. 

2021年6月15日火曜日

BC5 圧縮テクスチャ

はじめに

プログラムから, テクスチャ圧縮がしたいです. BC1, BC3, BC6h BC7についてはispc texture compressionを使おうと思っていましたが, BC5がないのです.

https://software.intel.com/content/www/us/en/develop/articles/fast-ispc-texture-compressor-update.html

ガイドに仕様がちゃんと書いてあります. 簡単そうなので作ってしまいます.

https://docs.microsoft.com/en-gb/windows/win32/direct3d10/d3d10-graphics-programming-guide-resources-block-compression#bc5

ファイルフォーマット

4x4のブロックごとに圧縮するブロック圧縮です. BC4はRだけの1チャネル, BC5はRGの2チャネルを扱います. 4x4x2 = 32 bytesを次の tablex2 = 16 bytesに圧縮します. BC5_UNORMとBC5_SNORMの2バリエーションです. 2つはシェーダでサンプルした場合, [0 1]の浮動小数点数になるか, [-1 1]の浮動小数点になるかが違います.

テーブルは, 4x4の最小値と最大値が先頭に2バイト, 次に3bits x 16のテーブルへのインデックスが並びます.

/*
MSB                   LSB
| red_0               | 1 byte minimum color
| red_1               | 1 byte maximum color
| red_h | red_g | red_f | red_e | red_d | red_c | red_b | red_a | 3 bits x 8
| red_p | red_o | red_n | red_m | red_l | red_k | red_j | red_i | 3 bits x 8
*/

テーブルは明示的に持たず最大値, 最小値から計算します. UNORMとSNORMで1箇所異なりますが, 圧縮するだけならばどちらでも同じなので統一します.

static constexpr u32 Shift = 20;
void createTable(s32 colors[8], s32 block[16])
{
    colors[0] = block[0];
    colors[1] = block[0];
    for(u32 i = 1; i < 16; ++i) {
        colors[0] = minimum(colors[0], block[i]);
        colors[1] = maximum(colors[1], block[i]);
    }
    colors[0] <<= Shift;
    colors[1] <<= Shift;

    if(colors[1] < colors[0]) {
        // 6 interpolated color values
        colors[2] = (6 * colors[0] + 1 * colors[1]) / 7; // bit code 010
        colors[3] = (5 * colors[0] + 2 * colors[1]) / 7; // bit code 011
        colors[4] = (4 * colors[0] + 3 * colors[1]) / 7; // bit code 100
        colors[5] = (3 * colors[0] + 4 * colors[1]) / 7; // bit code 101
        colors[6] = (2 * colors[0] + 5 * colors[1]) / 7; // bit code 110
        colors[7] = (1 * colors[0] + 6 * colors[1]) / 7; // bit code 111
    } else {
        // 4 interpolated color values
        colors[2] = (4 * colors[0] + 1 * colors[1]) / 5; // bit code 010
        colors[3] = (3 * colors[0] + 2 * colors[1]) / 5; // bit code 011
        colors[4] = (2 * colors[0] + 3 * colors[1]) / 5; // bit code 100
        colors[5] = (1 * colors[0] + 4 * colors[1]) / 5; // bit code 101
        colors[6] = 0;                                   // bit code 110
        colors[7] = 255<<Shift;                          // bit code 111
    }
}

ピクセル値に最も近いテーブルインデックスを検索して, 符号化します.

u64 findNearest(s32 colors[8], s32 x)
{
    u32 index = 0;
    s32 mind = absolute(colors[0] - x);
    for(u32 i = 1; i < 8; ++i) {
        s32 d = absolute(colors[i] - x);
        if(d < mind) {
            mind = d;
            index = i;
        }
    }
    return index;
}

まとめ

GPUOpen Compressonatorと結果が異なるため, もっといい方法がありそうです.