第626回の「UbuntuでもSteamのWindowsゲームを! 」では、SteamならUbuntu上でもWindowsゲームをプレイできる可能性が高いことを示しました。今回はSteamそのものをLXDコンテナの中に閉じ込めて実行してみましょう。
ホストをできるだけキレイに保つために
Steamのインストーラーはソースが公開されているものの、ゲーム自体はもちろんのこと、Steamクライアントやランタイムの一部はプロプライエタリなソフトウェアです。このためホスト上で実行することに抵抗があるかもしれません。また、Steam側の制約でホスト上に32bitライブラリが必要です。よってSteamそのものをコンテナに閉じ込められるとホストをクリーンに保てます[1] 。ひとつの方法は非公式のFlatpak版パッケージ を使うことです。本記事ではLXDコンテナの中で公式のSteamクライアントを、GPUアクセラレーションが動く形で実行する方法を紹介しましょう。
これまでにも第416回 や第433回 、第589回 などで、GUIアプリケーションをLXDコンテナの中で動かす方法を紹介してきました。今回も基本的な作業は同じです。ただし今回は、LXD 4.0までに実装された諸々の機能を活用し、GPUやサウンドデバイス、ゲームコントローラーをコンテナの中から使えるようにします。
あらかじめ第521回の「入門システムコンテナマネージャーLXD 3.0 」などを参考に、最新のLXDをインストールしておいてください。記事は3.0の頃の話ですが、4.0でもインストール部分に大きな違いはありません。一点注意すべきは、LXDのパッケージ形式はsnap版に統合されている点です。Ubuntu 20.04 LTSでもDebianパッケージ版のlxdパッケージは残っていますが、snap版への移行用のダミーパッケージとなっています。今後はsnap版を使うようにしましょう。
steamコンテナの準備
では早速steamコンテナを作ってみましょう。今回は手作業で随時設定していますが、パッケージのインストールや日本語環境の設定についてはプロファイルやcloud-initで自動化してしまうという手もあります。
コンテナの作成:
$ lxc launch ubuntu:20.04 steam
$ lxc exec steam -- sh -c \
"apt update && apt full-upgrade -y && apt autoremove -y"
UID/GIDの設定:
$ lxc config set steam raw.idmap 'both 1000 1000'
パッケージのインストール:
$ lxc exec steam -- dpkg --add-architecture i386
$ lxc exec steam -- sh -c 'apt update && apt install -y \
x11-apps mesa-utils libgl1-mesa-glx:i386 \
libcanberra-gtk-module:i386 pulseaudio dbus-x11 \
language-pack-ja fonts-noto-cjk-extra \
fonts-noto-color-emoji'
日本語環境の設定:
$ lxc exec steam -- update-locale LANG=ja_JP.UTF-8
$ lxc exec steam -- timedatectl set-timezone Asia/Tokyo
今回は「Ubuntu 20.04 LTS」ベースのコンテナにしています。「 UID/GIDの設定」でUID/GID 1000番のユーザー・グループがホスト上とコンテナ上で同じになるように変更しています。これはコンテナの中からホスト上のXサーバーやPulseAudioのunixドメインソケットにアクセスできるようにするためです。「 raw.idmap
」については第479回の「LXDコンテナとホストの間でファイルを共有する方法 」を参照してください[2] 。
Steamクライアントをインストールするために、i386アーキテクチャーを有効化した上で、コンテナの中にグラフィックとサウンド関連のパッケージもインストールしています。ただしUbuntuのLXDコンテナなら最初からi386が有効化されているはずではあります。また日本語フォントもコンテナの中に存在しないと、日本語化したときに正しくUIを表示できません。
次にコンテナの中からホストのサウンドサーバーにアクセスできるようにしておきましょう。単にUnixドメインソケットをそのままコンテナの中に見せているだけです。
$ lxc exec steam -- sed -i "s/; enable-shm = yes/enable-shm = no/g" /etc/pulse/client.conf
$ lxc exec steam -- sh -c "echo export PULSE_SERVER=unix:/tmp/.pulse-native | tee --append /home/ubuntu/.profile"
$ lxc config device add steam pa disk source=/run/user/1000/pulse/native path=/tmp/.pulse-native
さらにコンテナからホストのXサーバーとGPUデバイスにアクセスできるようにします。
$ lxc exec steam -- usermod -aG video ubuntu
$ lxc config set steam environment.DISPLAY :0
$ lxc config device add steam xorg disk \
source=/tmp/.X11-unix/X0 path=/tmp/.X11-unix/X0
$ lxc config device add steam mygpu gpu \
gid=`getent group video | cut -d: -f3`
第532回の「LXDのコンテナからGPUを利用する 」でも説明しているように、コンテナの中のデバイスのグループがrootになってしまうため、videoになるよう調整しています。さらに一行目でUbuntuコンテナの初期アカウント(ubuntu)なら最初からvideoグループに所属しているはずではあります。なお「/tmp/.X11-unix/X0
」はグラフィカルログインして初めて作られます。ログイン画面が表示された状態でこのコンテナを起動するとうまく動きませんので注意してください。
同様の理由でコンテナの自動起動を停止しておいたほうが無難でしょう。後ほどSteamを起動するタイミングでコンテナも起動するスクリプトを作ります。
$ lxc config set steam boot.autostart false
steamのインストール
ようやくsteamをインストールできます。今回もSteam公式のパッケージをダウンロード&インストールします。
$ lxc exec steam -- \
wget https://steamcdn-a.akamaihd.net/client/installer/steam.deb
$ lxc exec steam -- apt install -y ./steam.deb
これまでの設定を反映するために、一度コンテナを再起動しておきましょう。
$ lxc restart steam
まずは動作確認のために端末からSteamを起動してみます。
$ lxc exec steam -- sudo --user=ubuntu --login steam
ubuntuアカウントでログインして、steam
コマンドを実行しているだけです。うまくいけばディスプレイにSteamクライアントが表示されるのではないでしょうか。もし表示されない場合は、コンテナの中に「/tmp/.X11-unix/X0
」や「/dev/dri/
」などが存在するか、そのパーミッションは正しいかを確認してください。
Steamクライアント起動後の初回設定やProtonの有効化については、前回の第626回 を参照してください。LXD上で動いているかどうかに関係なく同じ手順となります。
画面右上の最大化ボタンから「Big Picture Mode」に移行すると、Steamのロゴの表示と共に音が鳴るなずです。そこでサウンドが動いていることを確認すると良いでしょう。
ちなみに画面右上の閉じるボタンを押すと、Steamはバックグラウンドに移行します。LXD上で動かす場合はフロントに戻す術がないので注意しましょう。本当にSteamを終了したい場合は、画面左上の「Steam」メニューから「終了」を選んでください。ちなみに後述するスクリプトを使うと、LXDコンテナの中でsteamコマンドを実行するため、バックグラウンドに移行したクライアントを復帰できます。
ゲームコントローラー(JoyStick)デバイスの対応
ここまでの時点でキーボードによるゲームのプレイは可能になっています。ただしアクション系のゲームを楽しもうと思うと、どうしてもコントローラーのほうが良い場合が多々あります。LXDコンテナのSteamからホストに繋いだゲームコントローラー(JoyStick)デバイスが見えるようにしましょう。今回はPlayStation 4のDUALSHOCK 4コントローラーを使いますが、他のコントローラーでもおおよそ事情は同じはずです。
Linuxで一般的に使えるゲームコントローラーはUSBで接続する有線タイプと、Bluetoothで接続する無線タイプの2種類に大別されます。このうちBluetoothについては、LXDコンテナの中から見えるようにするのはいろいろ大変なようです 。無線のコントローラーは魅力的ではあるものの、今回は有線接続のみを考えましょう。
第475回の「廉価なFPGA開発ボード『Zybo』をUbuntuからプログラムする 」でも紹介しているように、LXDはベンダーIDとプロダクトIDを指定してusbタイプのデバイスをコンテナの中から見えるようになる仕組みが存在します。今回もそれを使いたいところですが、実はJoyStickデバイスについてはusbタイプのデバイスとして見せるだけでは足りません 。
一般的にJoyStickデバイスが接続されると「/dev/input/js0
」のようなデバイスファイルが作成され、それを読み書きすることでJoyStick API経由でのコントローラーを操作できます。JoyStick APIはカーネルがデータの抽象化を肩代わりしてくれるため、アプリケーション側はどんなJoyStickデバイスが繋がっていても気にせずに使えるというメリットがあります。
しかしながらアプリケーション側で、コントローラーの種別ごとに設定を変更したり、キャリブレーションを行いたい場合、JoyStick APIでは力不足です。最近のUSB/BluetoothのHID(Human Interface Device)に対応したデバイスに対しては「/dev/hidraw0
」なデバイスファイルも作成されます。よってより柔軟に操作したいケースでは、そのデバイスファイル経由でHIDRAW API経由でのコントローラーの操作を行います。
Steamの場合は、「 /dev/input/
」以下のファイルをチェックして、コントローラー系のデバイスがいたら、最終的に「/dev/hidraw0
」からデバイス情報を取得し、操作するという仕組みになっているようです。つまり、このふたつのファイルがいずれもLXDコンテナの中から見える必要があります。
これらのデバイスファイルはいずれもudev経由で作成します。しかしながら、第475回のように単にusbタイプのデバイスをLXDコンテナに渡しただけではこれらのデバイスは作られません。usbタイプのデバイスを渡す方式だと、JoyStickデバイスとして認識しするために必要なueventが、コンテナ内部のudevに渡されないからです。このためusbタイプではなく、LXD 3.20で追加されたunix-hotplugタイプのデバイスとして渡す必要があります。
まずはPS4コントローラーを接続した状態で、ホストからベンダーIDとプロダクトIDを確認しましょう。
$ lsusb
Bus 002 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub
Bus 001 Device 002: ID 8087:0a2b Intel Corp.
Bus 001 Device 003: ID 054c:05c4 Sony Corp. DualShock 4 [CUH-ZCT1x]
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
Bus 004 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub
Bus 003 Device 002: ID 046d:c52b Logitech, Inc. Unifying Receiver
Bus 003 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
上記だと「054c:05c4
」が「ベンダーID:プロダクトID
」になります。ちなみにPS4コントローラーはモデルによっていくつか異なるプロダクトIDが存在するようです。コンテナにこのデバイスを追加しましょう。
$ lxc config device add steam sony \
unix-hotplug vendorid=054c productid=05c4
この状態で一度、PS4コントローラーのUSBコネクターを挿抜してみましょう。次のように適切なデバイスファイルがコンテナの中からも見えるはずです。
$ lxc exec steam -- ls -l /dev/input/
total 0
crw-rw---- 1 root root 13, 84 Jul 4 01:40 event20
crw-rw---- 1 root root 13, 85 Jul 4 01:40 event21
crw-rw---- 1 root root 13, 86 Jul 4 01:40 event22
crw-rw-r-- 1 root root 13, 0 Jul 4 01:40 js0
crw-rw---- 1 root root 13, 33 Jul 4 01:40 mouse1
$ lxc exec steam -- ls -l /dev/hidraw*
ls: cannot access '/dev/hidraw0': No such file or directory
ls: cannot access '/dev/hidraw1': No such file or directory
crw------- 1 root root 239, 2 Jul 4 01:40 /dev/hidraw2
ちなみにPS4コントローラーはタッチパッド部分がマウスとして認識されるため、JoyStickとマウスの両方の仮想的なデバイスファイル(とその実名となる「eventXX」なファイル)が作られます。
また、unix-hotplugデバイスとして登録した結果、「 /dev/hidraw2
」が見えています。「 /dev/hidraw0
」と「/dev/hidraw1
」はコンテナの中からは見えないので上記のようなエラーになっています。これがusbデバイスとして登録した場合、「 /dev/input/js0
」や「/dev/hidraw2
」はコンテナの中から見えません。
さて、これでSteamからコントローラーが使えるようになったと思ったあなたは甘いです[3] 。もうひとつデバイスファイルのパーミッションをなんとかする必要があります。
[3] 「なぜLXDのコンテナの中からだとSteamがコントローラーを認識しないのか」を把握するまでに、まる一日近くかかったのでなんかテンションがおかしいです。結局、Steamをstrace経由で起動して、どのデバイスにどうやってアクセスしようとしているのか眺めることで、ようやく今回の手順に到達しました。誰かほめて。こういうときソースコードがあれば、もう少し簡単に原因が判明しただろうに。
コンテナの中と外で先ほど追加されたデバイスファイルのパーミッションを比較してみましょう。上記にコンテナの中のパーミッションが記載されているため、コンテナの外であるホスト上から見た場合の例を掲載しておきます。
$ ls -l /dev/input/js0
crw-rw-r--+ 1 root input 13, 0 7月 4 01:40 /dev/input/js0
$ ls -l /dev/hidraw*
crw------- 1 root root 239, 0 7月 4 00:40 /dev/hidraw0
crw------- 1 root root 239, 1 7月 4 00:40 /dev/hidraw1
crw------- 1 root root 239, 2 7月 4 01:40 /dev/hidraw2
すぐにわかるのはホスト上だと「/dev/input/js0
」はinputグループに所有権があるようですね。あとよくわからない「+
」マークがついています。実はこの「+
」がポイントです。これは「ACL (Access Control Lists) 」によるパーミッションの設定が行われていることを意味します。
具体的な設定内容はgetfacl
コマンドで確認できます。
$ getfacl -p /dev/input/js0
# file: /dev/input/js0
# owner: root
# group: input
user::rw-
user:shibata:rw-
group::rw-
mask::rw-
other::r--
つまり通常のパーミッションとは別にユーザー「shibata」の読み書き権限も付与されているということです。
JoyStickデバイスはudevルールによりこのACLが設定されるような仕組みになっています。udevルールファイルにおける「TAG+="uaccess"
」がそれで、このタグが付けられたデバイスファイルは、「 ユーザーがログインした際にそのユーザーの読み書きパーミッションがACLで設定される」ようになります[4] 。このため、inputグループに所属してなくても、ログインさえできればそのファイルを読み書きできます。
しかしながらLXDのインスタンスの中ではこのACLの設定が行われません[5] 。よってコンテナの中では「+
」マークがついておらず、ログインしたユーザーであってもinputグループに所属しない限りは「/dev/input/js0
」の読み書きができません。ただし通常のパーミッションの設定により、誰でも読み込みだけはできるようになっています。Steamに関して言えば、「 /dev/input/js0
」は読み込みだけできれば良いようです。
さらにSteamは「/dev/hidraw*
」の読み書きも行います。しかしながらこれはホストでもrootでしかアクセスできないようです。実はSteamをインストールした際にコントローラー向けのudevルールがインストールされる のですが、この中で「/dev/hidraw*
」に対しても「TAG+="uaccess"
」が付くようになっているのです。つまりSteamクライアントをインストールした環境であれば、ログインしたユーザーで「/dev/hidraw*
」の読み書きができるようになっていたというわけです。
さて、LXDコンテナの中でこれらに関してどう対応しましょうか。「 正しいやり方」とは言い難いのですが、一番手っ取り早いのが次の方法です。
Steamクライアントを実行するユーザーはinputグループに所属する
/dev/hidraw2
のグループオーナーをinputにしておき、グループが読み書きできるようにしておく
後者については、ホスト側で「/dev/hidraw*
」に対しても「TAG+="uaccess"
」が付くようにudevルールを追加しておくという手もあります。
「/dev/input/js0
」や「/dev/hidraw2
」の数字の部分は、接続順や接続されたデバイスの数によって変わりえます。このあたりの調整は後回しにすることにして、まずは手動で上記の設定を行ってうまくいくか試してみましょう。
$ lxc exec steam -- usermod -aG input ubuntu
$ lxc exec steam -- chmod g+rw /dev/hidraw2
$ lxc exec steam -- chgrp input /dev/hidraw2
一度、Steamクライアントを再起動してください。その状態で「設定」から「コントローラ」の「一般のコントローラ設定」を選択します。うまく設定できていれば、次の図のように「検出されたコントローラ」に接続しているコントローラーデバイスが表示されるはずです。
図1 接続したコントローラーが見えていたら成功
起動スクリプトとデスクトップファイルの作成
さてここまで話をもとに、起動スクリプトを作成しましょう。つまりデスクトップ環境にログインしたあと、Steamクライアントを起動するために次のような作業を行います。
steamクライアントが起動していなかったら起動する
JoyStickデバイスが見えなかったら、接続もしくは再接続を促す
JoyStickデバイスのパーミッションを適切に起動する
コンテナの中のsteamクライアントを起動する
次のコマンドでホスト上に起動スクリプトを作成します。
$ cat <<'EOF' > steam-launcher
#!/bin/sh
CONTAINER="$1"
TITLE="Steam Launcher"
if [ -z "${CONTAINER}" ]; then
notify-send "${TITLE}" "Please specify container name"
exit 1
fi
# check and start container
status=$(lxc list --format csv -c s "^${CONTAINER}$")
if [ "$status" != "RUNNING" ]; then
lxc start "${CONTAINER}"
lxc exec "${CONTAINER}" -- cloud-init status --wait
status=$(lxc list --format csv -c s "^${CONTAINER}$")
if [ "$status" != "RUNNING" ]; then
notify-send "${TITLE}" "Failed to start \"${CONTAINER}\" container"
exit 1
fi
fi
JOYSTICK=""
for d in $(lxc exec "${CONTAINER}" -- sh -c 'ls /dev/input/js*' 2>/dev/null) ; do
if lxc exec "${CONTAINER}" -- test -c "$d" ; then
DEVPATH=$(lxc exec "${CONTAINER}" -- udevadm info -q path "$d" | sed 's,input/.*$,,')
for h in $(lxc exec "${CONTAINER}" -- sh -c 'ls /dev/hidraw*' 2>/dev/null) ; do
if [ "$(lxc exec "${CONTAINER}" -- udevadm info -q path "$h" | sed 's,hidraw/.*$,,')" = "${DEVPATH}" ]; then
lxc exec "${CONTAINER}" -- chmod g+rw "$h"
lxc exec "${CONTAINER}" -- chgrp input "$h"
JOYSTICK="$h"
fi
done
fi
done
if [ -z "${JOYSTICK}" ]; then
notify-send "${TITLE}" "Not found JoyStick device. Please re-plug JoyStick."
exit 1
fi
lxc exec "${CONTAINER}" -- sudo --user=ubuntu --login steam
EOF
GitHub Gistにも同じ内容のデータをアップロードしてあります ので、スクリプトを直接ダウンロードしたい場合はそちらを利用してください。
本スクリプトは「スクリプト コンテナ名」のように指定して使います。
うまく動かなかったときは、ユーザーに通知するよう「notify-send
」コマンドを使っています。今回はデスクトップ環境で、デスクトップファイルから呼び出すことを想定しているため、標準エラー出力ではなく、あくまでデスクトップ上に通知を行っています。
コンテナの状態は「lxc list --format csv -c s コンテナ名
」で確認できます。「 --format csv
」にしておくとそのあとのスクリプトによる解釈が楽になります。より機械的な操作をするならjsonやyamlを指定するという手もあります。「 --columns s
(もしくは-c s
) 」で表示するフィールドを制限できます。詳しいことは「lxc help list
」を実行してください。
コンテナが起動していない場合は、起動するようにしています。起動完了は「cloud-init status --wait
」で待っています。これはcloud-initをインストールしているインスタンスなら、おおよそ有効です。
コンテナが起動したあとは、JoyStickデバイスがコンテナの中から見えるかどうかを確認し、それに紐付いたHIDRAWデバイスのパーミッションを変更しています。JoyStickデバイスがなければエラー終了します。
うまく動くようなら、パスが取っている場所にコピーしておきましょう。
$ sudo cp steam-launcher /usr/local/bin/
実際に使ってみるとわかるのですが、今回の方法だとコントローラーのホットプラグに対応できません。具体的にはゲーム中にコントローラーのUSBケーブルが抜けてしまうと、Steamクライアントを再起動しないとコントローラーによる操作ができなくなってしまいます。これは不便なので、本質的にはスクリプトではなくコンテナ内のudevルールで対応すべきでしょう。
デスクトップファイルの作成
次にこのスクリプトを実行するデスクトップファイルを作成します。デスクトップファイルを作成しておくと、Dashなどから検索できて便利です。
まずはデスクトップファイルに表示するアイコンを、コンテナ内にあるSteamのディレクトリからホストにコピーしておきましょう。
$ lxc file pull \
steam/home/ubuntu/.local/share/Steam/tenfoot/resource/images/steam_home.png \
~/.local/share/icons/
次にデスクトップファイルを作成します。
$ cat <<'EOF' > steam.desktop
[Desktop Entry]
Name=Steam on LXD
Comment=Play games on Steam
Exec=/usr/local/bin/steam-launcher steam
Icon=/home/shibata/.local/share/icons/steam_home.png
Terminal=false
Type=Application
Categories=Game;
[Desktop Action stop-container]
Nmae=Stop Steam container
Exec=lxc stop steam
内容はそこまで難しいものではないはずです。ちなみにUbuntu 20.04 LTSからはGameMode にも対応するようになりました。これはゲームプレイ時には十分な性能を発揮させるために、特定のプロセスに対する省電力機能などを無効化する仕組みです。steam-launcher
の前にgamemoderun
を追加すればゲームモードが有効化されるのですが、ゲームの実体がコンテナの中にあるために、単純にはゲームモードにならないようです。
stop-containerはSteamコンテナを停止するためのアクションです。このようなアクションはデスクトップアイコンを右クリックすることで選択できます。たとえば一旦ログアウトしたあと、つまり既存のX Window Systemを終了したあとに再度ログインしてSteamコンテナを使いたい場合、unixドメインソケットを再接続するために一度コンテナを停止する必要があります。steam-launcher
の中でSteamクライアント終了後は常にSteamコンテナを終了しても実現できるのですが、Steamクライアントを終了するたびにコンテナを再起動するのも効率が悪いので、必要なときだけ手作業でコンテナを停止するような形にしました。ちなみにシステム起動時の自動起動は無効化しているため、単にシステムを終了する際はわざわざコンテナを停止する必要はありません。
最後にデスクトップファイルを適切な場所にインストールしておきます。
$ desktop-file-install --dir ~/.local/share/applications/ steam.desktop
Super+Aでアクティビティの検索画面を開き「steam」を入力すると先程登録したアイコンが表示されます。右クリックして「お気に入りに追加」しておくと、画面左のドックに登録され、いつでも簡単にコンテナ内部のSteamを起動できるようになります。
図2 ホスト上の検索画面からもSteamを検索できるようになった
図3 「 お気に入りに追加」すればドックから直接起動できるようになる
あとは普通のSteamと同じように動くはずです。今回紹介した手順はSteamに関係なく、その他のGUIアプリケーションでも有効な手段です。一度コンテナ化してしまえば、他のシステム・バージョンでもLXDさえ動けば「同じ環境」にできます。またシステム側のアップデートとは独立して環境を維持できます。ぜひ、いろんなツールをコンテナ化してみましょう。