🐳

Windows Dockerを駆使して静的リンクで仮想GPUとWHPXが使えるqemuをビルドするメモ

2024/12/29に公開

qemu 便利なんですが、諸般の事情(後述)で特殊な機能がいくつか必要で、自分が欲しい機能が全部揃ったものがその辺に落ちてなかったので自分でビルドすることにしました。

  • Windows版で、
  • 静的リンクで、
  • OpenGL(WebGL)のアクセラレートに対応していて、
  • Hyper-V(WSL2とかWindows Container)と一緒にうごくもの

まとめ

  1. WindowsネイティブのDockerでもビルド環境をコンテナ化でき、 -isolation process を使えば単なるchrootのような低オーバーヘッドで使えて実用的
  2. LinuxにKVMがあるように、WindowsにもWHPX(Windows Hypervisor Platform)がありqemuを高速化できる
  3. Windows版のqemuでもVirGLによる仮想GPUでOpenGLが使える

結果、WindowsでバッチリGPU仮想化が効くqemuが、静的リンクでビルドできました。

ただし、OpenGL ES 2.0レベルしか見えないのでコンピュートシェーダーが使えない。。

EDIT: デバッグしたらちゃんとES3.1になりました(後述)。

Dockerの"プロセス分離"コンテナとは?

環境を汚さずにソフトを使うために、 Windowsにも chroot(2) があったら良いのにな〜 というのはWindowsユーザーが誰しも思っていることだと思いますが、 Dockerの"プロセス分離"(process isolation)コンテナがほぼそれ です。

(ただし GUIアプリは使えません 。昔はリモートデスクトップを仕掛けたりできたと思うんですが、流石に便利すぎるためか塞がれてしまいました。)

Dockerをインストールして、以下のように docker run--isolation process を渡せばプロセス分離モードでコンテナを起動できます。

Dockerのインストール

おうちで使うだけなら、Docker Desktopを普通にインストールして、 Windowsコンテナに切り替え ることで使えるはずですが、手元ではDocker Desktopを使ってないので謎です。

Linuxコンテナのサポートが不要なら コマンドラインツールを直接インストール するのが簡単です。この方法ならDocker Desktopのライセンス要らないし。

※ Windowsのバージョンによって使えるコンテナイメージが異なります

# ホストが Windows 10 の人
docker run --rm -it --isolation process mcr.microsoft.com/windows/servercore:2004 cmd

# ホストが Windows 11 の人
docker run --rm -it --isolation process mcr.microsoft.com/windows/servercore:ltsc2022 cmd

Windows 10では公式でもプロセス分離モードを非サポートとするドキュメントがいくつかあり、真面目にサポートされていないようです。手元のWindows 10 22H2では servercore:2004 イメージでちゃんと動作しています。 ただ、そもそもこのイメージもサポートが切れています

Windows 11では 2022または2025のイメージが利用できます

プロセス分離でない通常のコンテナはHyper-Vコンテナとなり、(WSL2と同じように、)Hyper-V上で別のWindowsが動作します。Windows DockerはHyper-Vコンテナがデフォルトになっています。

メリット

Hyper-V上のWindowsコンテナに比べ、 高速で省リソースです 。ソフトウェアのビルドに使えるレベル。

また、Hyper-Vを無効化した環境でも使えることも一応メリットだと思います。

デメリット

セキュリティが無い 点には注意が必要です。ホストからはコンテナ内のプロセスが普通に観察されます(LinuxのDockerと同様)。

(↓ コンテナ内で実行しているpingがホスト上のprocess explorerで観察される)

また、カーネルバージョンが違うシステム間、つまりWindows 10とWindows 11でコンテナイメージを共有できません(Hyper-VコンテナならWindows11でWindows10用コンテナが動く)。まぁ、Windows 10はそろそろサポート切れるし大きな問題では無いですが。。

プロセス分離コンテナのアイソレーションのヤバさを象徴する現象としては、 Windows 10のコンテナが勝手にホストをスリープから復帰させる というものがあったりします。怪しいプログラムをサンドボックスするためにコンテナを使うのは止めましょう。

https://zenn.dev/okuoku/scraps/6e5bb77fae0461

あと、互換性が微妙にイマイチで、例えばCygwinはASLRを切らないと正常に動作しません https://cygwin.com/pipermail/cygwin/2022-December/252720.html

コンテナにMSYS2(gcc)をインストールする

今回はqemuをビルドしたいので、コンテナにgcc(のメジャーなWindows向けディストリビューションであるMSYS2)をインストールします。MSYS2の公式サイトから https://www.msys2.org/docs/ci/ 必要なパッケージを足して:

※ 冒頭にもコメントしてありますが、タグ qemubuild-windows-base として事前にベースイメージをタグしておく必要があります

Dockerfile
# Before building this image, tag Windows image based on your kernel
#   Win10: docker tag mcr.microsoft.com/windows/servercore:2004 qemubuild-windows-base
#   Win11: docker tag mcr.microsoft.com/windows/servercore:ltsc2022 qemubuild-windows-base
FROM qemubuild-windows-base

SHELL ["powershell", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue';"]

RUN [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; \
  Invoke-WebRequest -UseBasicParsing -uri "https://github.com/msys2/msys2-installer/releases/download/nightly-x86_64/msys2-base-x86_64-latest.sfx.exe" -OutFile msys2.exe; \
  .\msys2.exe -y -oC:\; \
  Remove-Item msys2.exe ; \
  function msys() { C:\msys64\usr\bin\bash.exe @('-lc') + @Args; } \
  msys ' '; \
  msys 'pacman --noconfirm -Syuu'; \
  msys 'pacman --noconfirm -Syuu'; \
  msys 'pacman --noconfirm -Scc'; \
  msys 'pacman --noconfirm -S git python ninja mingw-w64-ucrt-x86_64-toolchain base-devel python-setuptools mingw-w64-ucrt-x86_64-meson mingw-w64-ucrt-x86_64-ca-certificates mingw-w64-ucrt-x86_64-egl-headers mingw-w64-ucrt-x86_64-gles-headers mingw-w64-ucrt-x86_64-cmake mingw-w64-ucrt-x86_64-python-yaml' ; \
  msys 'pacman --noconfirm -Scc'; 

LABEL org.opencontainers.image.source=https://github.com/okuoku/qemubuild

今回はUCRT(Visual Studioのランタイムライブラリ)向けにビルドしたいので、 mingw-w64-ucrt- で始まるパッケージを選択します。

ビルドというか イメージの保存にはめっちゃ時間掛かります

ビルドは通常のビルド環境 git python ninja mingw-w64-ucrt-x86_64-toolchain の他に、 mingw-w64-ucrt-x86_64-ca-certificates (ビルド中にcurlで依存関係を拾ってくるプロジェクトが居るため) mingw-w64-ucrt-x86_64-python-yaml (libepoxyをGitリポジトリからビルドする際にpyyamlが必要なため) 等を追加してあります。

qemuと依存関係をビルドする

今回は依存関係を基本的に全部Gitのsubmoduleにしてまとめてチェックアウトすることに。ただしglibやqemuは追加の依存関係を ビルド中にチェックアウトやダウンロードする 仕組みになっているので、このGitリポジトリに入っていないソースコードも必要になってます。

https://github.com/okuoku/qemubuild

事前準備

  • 上記のリポジトリを git clone --recursive でcloneする。(必要に応じて git config --global core.longpaths true しておくのを忘れずに)
  • ベースになるWindowsイメージにタグ qemubuild-windows-base を打つ(ホストがWindows10だったら docker tag mcr.microsoft.com/windows/servercore:2004 qemubuild-windows-base)
  • Dockerイメージ qemubuild を用意する( 上記のDockerfile の通り)
  • Docker volume objs libs dist を用意する。中身は空で良い。 docker volume create objs など。

実際のビルド

上記の準備をやったあと、 cmake -P build.cmake でリポジトリ内のディレクトリ out にビルドしたqemuが出力されます。ホストにCMakeが入ってない場合は適当にインストールしてパスを通しておきます。

このリポジトリではビルド用にCMakeをシェルスクリプト的に使ってビルドしています。CMakeはどこにでもあるので。。

ビルド過程の解説

... ここで終わっちゃうと記事がめっちゃ短いので。。

Docker内のMSYSを使う

MSYSを正常に使うにはログインシェル(bash)を経由してコマンドを起動する必要があります。コマンドが長いので関数化することに。 リポジトリの中では、

    execute_process(COMMAND
        # --isolation process を指定してprocess分離コンテナを作成
        docker run --isolation process --rm
        # ボリュームの設定: libsは依存関係のインストール先、distは最終的なqemu
        "-vtmp:c:\\objs"
        "-vlibs:c:\\libs"
        "-vdist:c:\\dist"
        # e:/repos/qemubuild がソースコードのリポジトリ
        "-ve:/repos/qemubuild:c:\\srcs"
        "-ve:/repos/qemubuild/out:c:\\out"
        # qemubuild: 作成したMinGW入りコンテナのタグ
        qemubuild 
        # 実行するコマンドライン
        "c:\\msys64\\msys2_shell.cmd" -here -no-start -ucrt64 -defterm 
        -c "${script}"

※ このsnippet ではわかりやすさ重視でハードコードしていますが、さすがにどうかと思ったので リポジトリの方では自動検出にしました

キモは c:\msys64\msys2_shell.cmd -here -no-start -ucrt64 -defterm -c "シェルスクリプト" のように msys2_shell.cmd を使うことで、これ経由でコマンドを叩くことで MSYSターミナルと同じようにコマンドを起動 できます。

Mesonによる依存関係のビルド

qemuを始めとして、Gnome系、Freedesktop.org系のプロジェクトは一般にmeson https://mesonbuild.com/ をビルドシステムとして採用しています。ビルドの手順はどのプロジェクトでも、

  1. meson setup でプロジェクトファイルを生成する
  2. meson compile で実際のビルドを行う
  3. meson install でインストールする

で統一できるので、これもスクリプトにして使い廻すことにしました。 リポジトリの中では、

# setup: PKG_CONFIG_PATH 環境変数を設定して meson setup を呼ぶ
PKG_CONFIG_PATH=c:/libs/lib/pkgconfig /ucrt64/bin/meson setup
  # 共通オプション
  --prefix=c:/libs --buildtype=release -Ddefault_library=static
  # 追加オプション(必要なのはlibepoxyだけ)
  ${adddef}
  # ソースコードディレクトリ
  c:/srcs/deps/${projname}
  # ビルドディレクトリ -- このディレクトリにNinjaのプロジェクトが出力される
  c:/objs/${projname}

# compile: Ninjaを呼んでコンパイルする
/ucrt64/bin/meson compile -C c:/objs/${projname}

# install
/ucrt64/bin/meson install -C c:/objs/${projname}

どのプロジェクトでも共通のオプションとして:

  • --prefix : 非標準の場所にインストールする。ビルド後に消しやすくするため。
  • --buildtype : release (最適化有りでビルド)を指定する。
  • -Ddefault_library=static : 静的ライブラリだけビルドする

を指定しておきます。また、libepoxyは何故か Windows上では正常にビルドできなかった ので追加のオプションとして -Degl=yes を渡して います。

qemuは超クッソ激烈に多機能で、完全体のqemuをビルドしようとすると大量の依存ライブラリをビルドする必要があります。が、実は単に動かすだけであれば数点で済みます。

Mesonでビルドされるのは:

独自のビルドシステムを持っているのはSDL2(CMake)とqemu自身(configureスクリプト)です。

SDL2のビルド

デフォルトでは SDL2がOpenGLをロードしてしまい関数名が被って死ぬ ため、 SDL_OPENGL=OFF を渡してビルドする以外は普通のビルドです。

/ucrt64/bin/cmake -G Ninja
-DSDL_STATIC=ON -DSDL_SHARED=OFF -DSDL_OPENGL=OFF
-DCMAKE_BUILD_TYPE=RelWithDebInfo
-DCMAKE_INSTALL_PREFIX=c:/libs
-B c:/objs/SDL2 -S c:/srcs/deps/SDL2

qemuのビルド

これは めっちゃ試行錯誤しました 。。

PKG_CONFIG_PATH=c:/libs/lib/pkgconfig c:/srcs/qemu/configure
--enable-whpx --enable-system --enable-slirp --enable-vnc
--target-list=aarch64-softmmu,arm-softmmu,avr-softmmu,riscv32-softmmu,riscv64-softmmu,x86_64-softmmu
--prefix=c:/dist
--disable-gio --disable-curl --disable-zstd --disable-bzip2 --disable-curses
--static --extra-ldflags=-liconv --disable-werror --extra-cflags=-DLIBSLIRP_STATIC
  • --enable-whpx : Windows Hypervisor Platformを有効にする。これが無いとハイパーバイザを活用できないので通常のエミュレーターになってしまい遅い。WHPX版のqemuは最近のAndroid Emulatorの中身でもあるので、そこそこ安定していることが期待できる。ちなみにHyper-Vが無い環境向けに GoogleはKVMのWindows移植をメンテナンスしている 。昔はこの用途のためにIntelがHAXMを提供していたが止めてしまった。(更に昔はKQEMUというqemu専用のカーネルモジュールがあった)
  • --disable-gio --disable-curl --disable-zstd --disable-bzip2 --disable-curses : 使わない機能だが勝手に有効になってしまうもの。
  • --static 静的リンクする (今回の目的)
  • --extra-ldflags=-liconv glib2のバグ(多分)でiconvを明示的にリンクしなければならないのに pkgconfigの指定に入っていない問題 のワークアラウンド。動的リンクではDLLの依存関係で自然にiconvがリンクされるため問題にならない。
  • --disable-werror libslirpから関数がdeprecateされた問題 のワークアラウンド。
  • --extra-cflags=-DLIBSLIRP_STATIC 実はつい最近まで WindowsではDLL化が強制されていたが静的リンクがサポートされた ためそれを利用するためのdefine

ビルド結果の取り出し

build.cmake を実行するとDocker volumeの dist にビルド結果が格納されます。ホスト側に取り出すには、コンテナを起動してコピーします。

ビルドスクリプトでは 、単に cp しているだけです。

cp -rp c:/dist/* c:/out/

起動してみる

ビルドしたqemuを起動してみます。これにも事前準備が必要です。

まずデフォルト設定で起動するのを試す

ビルドされた qemu-system-x86_64.exe をダブルクリックすると、とりあえず何か出ます。何も出ない場合は、ビルドで何か間違ったことが起きたのだと思います。

Windows Hypervisor Platformの有効化

... これってデフォルトで入ってないんでしたっけ。。? Windows ハイパーバイザプラットフォームにチェックを入れます。 cf. Androidエミュレーターの説明

ちなみにHyper-Vとは別のチェックボックスです。

ANGLE

Windowsには標準でOpenGL ESが入っていないので、Google謹製のOpenGL ES実装であるANGLEを導入します。今回は https://github.com/mmozeiko/build-angle からダウンロードして、リリースに含まれるDLLをqemuと同じところにコピーしておきます。

HiDPIの無効化

4KモニタなどのWindows標準と異なるdpiのディスプレイを使用していると、上掲のスクリーンショットのように表示が微妙にぼやけてしまいます。これは、WindowsがアプリをHiDPI非対応と見做して勝手に拡大してしまうために起こります。

SDLにちゃんとしたHiDPIサポートが実装されたのはSDL3からなので、今回は手動でHiDPI対応アプリと詐称することにします。どうせGUI無いし。

qemuの実行ファイルを右クリック → プロパティ → 互換性 → 高DPI設定の変更 → 高DPIスケール設定の上書き → アプリケーション に設定します。

起動オプション

qemu-system-x86_64.exe \
# 適当にCPUモデルを設定する
-machine q35 -smp 6 -cpu qemu64-v1 \
# WHPXを使用する。最近のWindowsホストなら kernel-irqchip の指定は要らないはず
-accel whpx,kernel-irqchip=off \
# 日本語キーボードマッピング
-k ja \
# 細かいデバイス設定
-m 8192 -netdev user,id=net1,hostfwd=tcp:127.0.0.1:5522-:22 \
-device virtio-net-pci,netdev=net1 \
-drive file=e:/vm/alpine/alpine.qcow2,id=blk1,if=none \
-device virtio-blk-pci,drive=blk1 \
-usb -usbdevice tablet -audio driver=sdl,model=virtio \
# VirGLの設定
-device virtio-vga-gl -display sdl,gl=es

他のハイパーバイザは気を効かせて色々自動設定してくれますが、qemuの場合動く組合せを模索する必要があります。手元では、 -machine q35 -cpu qemu64-v1 -accel whpx,kernel-irqchip=off が最も安定して起動したのでこれを採用。

GPU仮想化が効いているか確認する

大抵のLinuxディストリビューションでは mesa-utils パッケージにある eglinfo でAPIの対応状況を確認できます。

eglinfo -p x11 でX11アプリケーションの状況、 eglinfo -p wayland でWaylandアプリケーションの状況が出力されます。

waylandの出力(抜粋)
Wayland platform:
EGL API version: 1.5
EGL vendor string: Mesa Project
EGL version string: 1.5
EGL client APIs: OpenGL OpenGL_ES
EGL driver name: virtio_gpu
  :
  :
OpenGL compatibility profile vendor: Mesa
OpenGL compatibility profile renderer: virgl (ANGLE (Intel, Intel(R) HD Graphics 530 (0x00001912) ...)
OpenGL compatibility profile version: 2.1 Mesa 24.3.2
  :
  :
OpenGL ES profile vendor: Mesa
OpenGL ES profile renderer: virgl (ANGLE (Intel, Intel(R) HD Graphics 530 (0x00001912) ...)
OpenGL ES profile version: OpenGL ES 2.0 Mesa 24.3.2
OpenGL ES profile shading language version: OpenGL ES GLSL ES 1.0.16

でも何故かOpenGL ES 2.0になっちゃうんですよね。。(解析中) 今回VirGLのバックエンドにはOpenGL ESを使っていますが、上記のようにゲストには通常のDesktop OpenGLも提供されます。

ちなみに有効にできていない場合は自動的に llvmpipe にフォールバックします。こちらはGPUを使わずCPUだけでレンダリングします。

諸般の事情(モチベーション)

いつもながら、なんでわざわざこんな事を...というのが当然の反応だと思います。

動的リンクは取り回しがわるい

そもそも qemu を使いたいだけならMSYSのパッケージにqemuがあります。

https://packages.msys2.org/base/mingw-w64-qemu

でもデバッグとかの都合でソースからビルドする必要があって、MSYS本来のパッケージと共存させることを考えると自前のビルドは静的リンクにした方が楽かなと。。

WSL2の仮想GPUは古いGPUでは使えない問題

WindowsによるGPU仮想化には比較的最近のGPUドライバが必要です。 Windows Containerの仮想GPUはWDDM 2.5WSL2のDirectX12パススルーはWDDM 2.9が必要 です。手元の環境は?

... WDDM2.1。 dxdiag 起動して確認したとき "マジで!?" って声が出たね。。

デスクトップ環境全体をRenderDocできる

qemuのVirGLを使うと、デスクトップ環境全体を一つのOpenGL ESアプリにできます。つまり、これを使ってデスクトップ環境全体をGPUトレースできることになります。

(WebGLのデモ https://webglsamples.org/field/field.html をFirefoxで表示したところをトレースしたところ: set RENDERDOC_HOOK_EGL=0 してDirectX側をトレースしているので上下反転している)

何故?WasmLinuxの目標の一つにWebブラウザやデスクトップ環境をWebブラウザ上で動作させるというのがあって、その前段階として qemuをWebGLに移植したい からです。そのためには、どんなGPU機能が使われているのかをGPUトレースを通じて調査する必要があります。

ちなみにqemuのWebAssembly移植は container2wasm の一環で既にあるようです(すごい) https://github.com/ktock/qemu-wasm 。WebGLのCバインディングは既に昔作ってあって、当時のUnity WebGLが動く程度の完成度があります:

https://zenn.dev/okuoku/articles/5a7a04e75234b3

そのWebGLバインディングをEGL + OpenGL ESにwrapするエミュレーターもWine上でDirectX8のゲームが動く程度には実装してあるので、

https://zenn.dev/link/comments/8fe75d0a45ffc9

あとは時間さえ掛ければできるはず。。いやその時間を捻出するのが一番難しいんですが。。

かんそう

いやまぁ PCをアップグレードしろ で終わる話を延々としてしまいましたが、もう開発の多くをmacやLinuxに移行してしまってあんまりWindows環境を整備してないんで。。時間もないし。。

Windows 10はもう古い

来年(2025年)の10月にはメインストリームサポートが終了します。 https://www.microsoft.com/ja-jp/atlife/article-windows10-portal-eos :

Windows 10 Home and Pro

   2015 年 7 月 29 日 サポート開始
   2025 年 10 月 14 日 サポート終了

DockerというかWindows Containerは真剣にサポートされた(?)のがWindows 11以降なので、こういう目的に使いたいならWindows 11を導入するべきでしょう。今回の事例で言うと、Windows 10で正常にプロセス分離コンテナが使えるWindowsのイメージは 2004 (2020年4月) のとっくにサポートが切れたバージョンです。

...まぁ手元ではWin11はテスト環境にしか入ってないですが。。Windows 10は最後の32bitカーネルがあるWindowsだから名残り惜しいんだよなぁ。。(タスクバーを縦に配置できるし)

ゲスト側がOpenGL ES 2.0になっちゃう問題

EDIT: 手元の環境では GL_MAX_VERTEX_UNIFORM_BLOCKS が規格下限の 12 しか無いのが原因でした。

https://zenn.dev/okuoku/scraps/6379ff959429e5

今のところGnome等のデスクトップ環境やWebブラウザ類はOpenGL ES 2.0でも問題なく動作しますが、 これだとWebGL2が使えない ので何とかしたいところ。。

昔のコメント( https://www.collabora.com/news-and-blog/blog/2019/08/28/virglrenderer-state-of-virtualized-virtual-worlds/#qcom1089 )によると拡張 GL_NV_conditional_render が必須らしいんですが、(GLESで)必要なシチュエーションが思いあたらない。。

今回の ANGLE + virglrenderer + Qemu の組合せは、 macやiOSで動作するUTMと同じ組合せ で、そちらではGLES 3.0が見えているのでANGLE上での不可能ではない。。はず。。ANGLEのバックエンドがOpenGL必須とかだったらダメですが。

Venus、gfxstream

今月リリースされたqemu 9.2ではVirGLによるOpenGLだけでなく venus によるVulkanのパススルーにも対応しています https://wiki.qemu.org/ChangeLog/9.2

virtio-gpu now support venus encapsulation for vulkan (need recent virglrenderer on host and mesa on guest)

なのでVulkanを使った方が良いとは思うんですが、Webに持っていくのがかなり無理ゲーな気がするんですよね。。VulkanとWebGPUの機能的なギャップが結構大きいので。。

VenusとVirGLの違いについてはChromeOSのblogがわかりやすいです:

https://chromeos.dev/en/posts/improving-vulkan-availability-with-venus

より"パススルー"に近いものとしてはgfxstream https://android.googlesource.com/platform/hardware/google/gfxstream/ があって、こちらがAndroidエミュレーターで使われています。(VenusやVirGLを使おうとしているissueも立ってますが... https://gitlab.freedesktop.org/virgl/virglrenderer/-/issues/563 )

qemuを手元でビルドする意義

先に挙げたWebAssembly版のqemu https://github.com/ktock/qemu-wasm 以外にも、UTMで使われている 高速インタプリタ版 のような面白いハックが割と試されているのが面白いのではないかと思います。自分も昔にCygwinに移植したりしました:

https://qiita.com/okuoku/items/7a3a4944745b0424d415

が、昔に比べるとqemuも複雑になってしまって割と準備が面倒だったりするので敬遠される方も多いんじゃないかなと。というわけで(比較的)簡単にできることを示すために記事にしてみました。

手元では、こういう風にしてビルドしたqemuをCI用のハイパーバイザとして使っています。最近は(Androidエミュレーターを動かす都合で) GitHub Actionsのworkerでもkvmが使える しビルド自動化との相性はHyper-VやVMware、VirtualBoxのようなサービスとして実装されたハイパーバイザよりも良いのではないかなと。

ただ、普通のデスクトップとして使うのは "閉じる" ボタンで確認なく閉じちゃうあたり微妙ですね(いっ敗)

Discussion