🛠️

ResoniteのヘッドレスサーバをARM64で動かしてみた

2024/12/24に公開

前置き

この記事は Resonite Advent Calendar 2024 24日目の記事です

自己紹介

ResoniteのMod作ったりしている一般エンジニアミーシェです。
NeosVR時代からいくつかMod作ってます。比較的多く使っていただいているおすすめMods:

  • BetterInventoryBrowser - インベントリにWinのexplorerのようにフォルダのピン止め機能や最近使ったフォルダの表示機能等を追加して少し使いやすくします
  • NeosBetterIMESupport - デスクトップモードで日本語入力する際にIME確定前の文字が見えないのを解決します
  • ResoniteScreenshotExtensions
    - スクリーンショットの保存周りを少し拡張して便利にします。VRCのように年月ごとにフォルダを分けたり、圧縮ありのwebpで保存できるようにしたり、ファイルにメタデータ情報を埋め込んだりできます

想定読者

  • Resoniteのヘッドレスを運用している/運用したい人
    • 特にarm64 CPUでの運用に興味がある人
  • .NET(C#), Dockerを使ったことがある

TL;DR

  • 今現在(2024/12)、arm64ネイティブでヘッドレスを動かすには少し工夫が必要だが、一回動く環境さえ作れれば継続運用の不安はそこまでない
  • 問題となるのは以下2点
    • SteamCMDがarm64に対応していない -> DepotDownloader(3rd partyのダウンローダ)を使用 or qemu/CI等x64環境でSteamCMDを使ってダウンロードしたものをDocker imageに焼き付けて使う
    • ネイティブライブラリへの依存 -> OSのパッケージマネージャから入手した各CPUアーキ向けprebuiltのものに差し替えてしまうので意外と何とかなる
  • .NET8/9化したのでエントリポイントを自作のものに差し替えたカスタムヘッドレス作りやすくなったぜ!

モチベーション

まず、なぜ変なことをしてまでARM64(AArch64) CPU上の環境で動かしたいのかを書いておきます。
自宅鯖でやるならRaspberryPiで動かしたい!とかなんでしょうが、私の場合はOracle CloudのVM無料枠の中でランニングコスト0円で性能の良いヘッドレスを建てたいからです。

少し前にマイクラサーバをこの無料枠で動かす記事がいくつか流行ってた気がします
例: https://qiita.com/sjchorcl/items/20f6741cc2090a1824c2

Oracle CloudのAlways Free(永久無料枠)の定義から関係部分を引用したのが下記です。

1VMまたは最大4VMとして使用できるArmベースのAmpere A1コアと24GBのメモリを1か月あたり3,000 OCPU時間と18,000GB時間で使用可能
アウトバウンド・データ転送: 1か月あたり10TB
https://www.oracle.com/jp/cloud/free/

VM1つなら、4コア,24GBメモリ,ネット帯域4Gbps(ベストエフォート)のVMを24時間動かしてもずっと無料です!しかも東京/大阪リージョンが使えます!
これはResoniteが公式でarm64に対応していないことを除けば、ヘッドレスを建てるのにとても良い環境だと思います。

筆者はNeosVR時代からこのVMでヘッドレスを運用しているので2年近く使ってる気がしますが、今のところ問題なく運用できているので、自分で問題解決をできる方にはおすすめです。

成果物

https://github.com/hantabaru1014/baru-reso-headless-container
近頃筆者が作成・使用しているカスタムヘッドレスです。以下特徴があります。

  • x64/arm64向けのDockerイメージをGithub Actionsでアプデのタイミングで自動ビルドしてprivate設定のghcr.io経由で使用
  • 独自のエントリポイント
    • 公式のCUI consoleではなく、gRPCをI/Fにして好き勝手に機能を拡張している

https://github.com/hantabaru1014/resonite-headless-arm64
この記事向けに上記のカスタムヘッドレスからarm64対応部分だけを抽出したリポジトリです。もし以降の内容に興味がある方がいれば参考になれば幸いです。

SteamCMDがarm64に対応していない

arm64でResonite Headlessを動かそうとした際の第一の障壁が、入手元であるSteamのLinux向けクライアントであるSteamCMDがarm64に対応していないことです。
NeosVRの時はアプデが停止した後だったのでscp等でx64なマシンからファイル転送していましたが、Resoniteになってアプデが来るようになったのでそうもいかなくなりました。
私的におすすめな方法は以下2つです。

DepotDownloader

https://github.com/SteamRE/DepotDownloader
非公式のダウンローダです。Pure C#製なおかげでarm64にも対応しています。
ファイルの入手からすべてarm64環境で完結させたいならこれが良いです。ただし、非公式クライアントであるため、Steamの仕様変更に振り回されて不安定になったり動かなくなったりします。最近もbetaブランチのダウンロードができなくなって困りました。

Github Actionで使用する際はこちらを参考にどうぞ: build-and-push-image.yml#L26
-filelist 引数に正規表現でダウンロードするファイルを指定できるので、Headlessフォルダ内だけ高速にダウンロードすることができます。

SteamCMDをx64で動かす

上で書いた通り、最近DepotDownloaderが不安定になってしまったため、現時点で筆者が採用している方法です。
arm64で動かすという記事なのに何を言っているんだという感じかもしれないですが、筆者の場合はGithubActionsでヘッドレスのコンテナイメージをビルドしており、そのrunnerはx64なのでCI上でダウンロードする場合には普通にSteamCMDのイメージを使用してダウンロードすればいいということに最近気づきました。手元の開発環境もVRをするために持っているx64なwin11のWSLなので筆者の場合はこれで十分でした。
arm64でやりたい場合もQEMUでSteamCMDのイメージを実行したほうが安定した方法な気がしています。

Github Actionで使用する際はこちらを参考にどうぞ: build-and-push-image.yml#L48
+app_update コマンドにbetaブランチを指定するところがハマりポイントです。ダブルクォートの位置に注意してください。

ネイティブライブラリへの依存をどうにかする

本題です。Resonite(FrooxEngine)の大部分はPure C#なので.NETそのものが対応しているCPU archであれば動くのですが、画像・音声・モデルデータといったデータの処理に使用しているライブラリがネイティブライブラリに依存しているのでそこをどうにかしないと起動に失敗します。
Headlessフォルダに入っているファイルの中で.soの拡張子のファイルがResoniteのヘッドレスが依存しているネイティブライブラリのすべてです。

$ ls -a | grep so$
brolib_x64.so
discord_game_sdk.so
libFreeImage.so
libassimp.so
libcrnlib.so
libfreetype6.so
libmsdfgen.so
libopus.so
libpdfium_x64.so
libsteam_api.so

これらの多くはオープンソースのライブラリなので、arm64向けのビルド済みバイナリを入手して来るか、ソースコードから自分でビルドして差し替えればOKです。
これらのネイティブライブラリのソースリポジトリですが、実はYDMS(Resoniteの開発元)のgithub organizationにフォークがあり、Github Actionでビルドワークフローが組まれていたりします。そこで、海外の有志がarm64向けのビルドワークフローを追加するPRを出していたりします()。いつかはResonite公式で積極的なサポートはしないけどもarm64向けビルドも配るよ的な感じになってくれればいいですね。

さて、方針は前述しましたが、実は上で調べたネイティブライブラリすべてを用意する必要はありません。ヘッドレスが動くのに必要のない or optionalのライブラリがあります。

  • optional (用意できるのであればあったほうがよさそう)
    • brolib_x64.so
      • データの圧縮に使用しています。これがないorうまく読み込めなくてもログにエラーが出るだけで問題なく動きはします。ただ、圧縮はできたほうがよいでしょう。
    • libpdfium_x64.so
      • PDFをゲーム内に取り込むのに使っているライブラリです。ヘッドレスには要らなそうです。無い場合、ホストしたセッション内に誰かがPDFを取り込むとログにエラーが出ますがセッションがクラッシュしたり、参加者がPDFを見れない等のことは今のところ全く起きていないです。
  • 必要ない(なんなら邪魔)
    • discord_game_sdk.so
      • Discord integration(Resoniteをプレイ中とか出るやつ)のためのライブラリです。ヘッドレスには必要ないですし、あっても何も機能しません。
    • libsteam_api.so
      • Steamと連携するライブラリです。ヘッドレスには必要ないですし、そもそもSteamはarm64で動かないのでビルド通しても意味ないです。

この他のライブラリも一般的なヘッドレスクライアントの動作としては無くても動きそうな感も実はあるのですが、上記のもの以外はOSのパッケージマネージャから容易に入手できる&ファイルを入れ替えるだけで動くのでわざわざ実験する必要もないでしょくらいに考えています。

NeosVRの頃はソースコードからarm64向けに自分でビルドしたバイナリに差し替えるやり方をしていたのですが、最近はDockerに乗せたのでイメージのクロスビルドのやりやすさからOSのパッケージマネージャから入手して差し替える方法を取っています。

Dockerfile

RUN apt-get update && apt-get install -y --no-install-recommends libassimp5 libfreeimage3 libfreetype6 libopus0 libbrotli1 zlib1g && rm -rf /var/lib/apt/lists/*

差し替えを行うスクリプト

#!/bin/bash

if [ "$(arch)" == "aarch64" ]; then
  cd /lib/aarch64-linux-gnu
else
  cd /lib/x86_64-linux-gnu
fi

cp libassimp.so.5 /app/libassimp.so
cp libfreeimage.so.3 /app/libFreeImage.so
cp libfreetype.so.6 /app/libfreetype6.so
cp libopus.so.0 /app/libopus.so
cp libbrotlicommon.so.1 libbrotlidec.so.1 libbrotlienc.so.1 /app/

さあ、これで動く!と思いきや、今の.NET9の環境ではDiscordとSteamのdllライブラリが障壁になります...
Monoで動いていたころは discord_game_sdk.solibsteam_api.so が無ければ読み込みエラーになるだけだったのですが、Monoに比べて.NET8/9はdllのビルドターゲットをちゃんと確認するらしく、Steamworks.NET.dllDiscordGameSDKWrapper.dll自体はネイティブライブラリで無いにも関わらず、x64向けのdllとしてビルドされていることで読み込もうとして段階で.NETがエラーで落ちます。
前述した通り、SteamとDiscordとの連携はヘッドレスでは必要ないのでFrooxEngine.dllを少し改造して依存を剝がしてあげる方針を取りました。筆者はModderなので、当初はModを作ることで達成しようとしましたが、依存自体を無かったことにするのは非常に骨が折れた上に、出来上がってからResoniteModLoaderが内部で使用しているMonoModがarm64での実行ができないことを知りましたorz (issue, 現在はWIPでマージされていないですが動くブランチを海外のResonite Modderさんが作られているので動くかも)
そこで、FrooxEngine.dllをModによるランタイムパッチではなく、ファイルを書き換えてしまう静的パッチしてしまうことにしました。

成果物です。 FrooxEngineの出来がいい(*)のでライブラリをusingしているクラスを消すだけで最初から無かったことにできます。素晴らしいですね!

public bool Patch(AssemblyDefinition assembly)
{
    var types = assembly.MainModule.Types;
    var removeTypes = types.Where(t =>
    {
        switch (t.Name)
        {
            case "SteamConnector":
            case "SteamVoiceAudioInputDriver":
            case "SteamListener":
            case "SteamConnection":
            case "SteamNetworkManager":
            case "DiscordConnector":
                return true;
        }
        return false;
    }).ToArray();
    foreach (var t in removeTypes)
    {
        types.Remove(t);
        Console.WriteLine($"Remove Type : {t.FullName}");
    }
    return true;
}

ここまでやったら無事arm64でヘッドレスが問題なく動きます!
興味がある方はこれまで説明した内容を実装したリポジトリを用意しましたので、参考にどうぞ!

https://github.com/hantabaru1014/resonite-headless-arm64

カスタムヘッドレスのすすめ

蛇足編です。ヘッドレスが.NET8/9で動くようになったおかげでパフォーマンスが向上したのは皆さんご存じのうれしい効果でしたが、ヘッドレス改造をしたい人から見ると追加でうれしいことがありました。
より最近の.NETライブラリが使えるようになった & ASP.NETと共存させやすくなったのです!

なので、筆者は上述したSteamとDiscordの依存を剥がす改造ついでに、エントリポイントをASP.NETにしてしまってI/Fを使いづらいCUI consoleからgRPCにしてFrooxEngineへの改造をせずにヘッドレスのできることを好き勝手に拡張しています。

↓ 好き勝手に拡張しているカスタムヘッドレス
https://github.com/hantabaru1014/baru-reso-headless-container

筆者は日本語が通じる中ではResoniteのModding/ヘッドレス改造周りに詳しい気がするので、興味があれば是非話しかけてください!
それでは良きResoniteライフを~!

Discussion