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