Ubuntu Weekly Recipe

第627回コンテナの中でもWindowsのゲームを

第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]⁠。もうひとつデバイスファイルのパーミッションをなんとかする必要があります。

コンテナの中と外で先ほど追加されたデバイスファイルのパーミッションを比較してみましょう。上記にコンテナの中のパーミッションが記載されているため、コンテナの外であるホスト上から見た場合の例を掲載しておきます。

$ 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クライアントを起動するために次のような作業を行います。

  1. steamクライアントが起動していなかったら起動する
  2. JoyStickデバイスが見えなかったら、接続もしくは再接続を促す
  3. JoyStickデバイスのパーミッションを適切に起動する
  4. コンテナの中の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さえ動けば「同じ環境」にできます。またシステム側のアップデートとは独立して環境を維持できます。ぜひ、いろんなツールをコンテナ化してみましょう。

おすすめ記事

記事・ニュース一覧