はじめに
Apple Silicon Mac で、自前ビルドの Wine + DXMT(D3D11→Metal)を使って Windows専用の Steam ゲームを動かそうとしたとき、最後まで残ったのが「ウィンドウは出るのに中身が真っ黒」という問題でした。
同じ症状は Steam クライアント本体や、CEF(Chromium Embedded Framework)を埋め込んだ多くのアプリでも起きます。この記事では原因と、macOS の IOSurface を使った解決の"考え方"を、コード全文ではなく「なぜそうなるか/どこを橋渡しすればいいか」に絞って解説します。
症状:ウィンドウは出る、中身だけ黒い
起動するとウィンドウ枠は生成される。でも中身が真っ黒のまま。クラッシュはしていない。ログにも致命的なエラーは出ない――この「動いてるのに映らない」が厄介でした。
根本原因:絵を描くプロセスと、窓を持つプロセスが別
- 最近の Chromium / CEF は「GPU合成を必ず別プロセスで動かす」設計
- GPUプロセスが描画し、browserプロセスがウィンドウを所有する
- 両者の間に「合成済みフレームを転送する経路(present path)」が必要
- ところが mainline Wine にはこの cross-process present 相当の機能がなく、D3D→Metal 変換層(DXMT)も「cross-process swapchain は未対応」とエラーを返す
- 結果、ウィンドウは生成されてもフレームが届かず黒のまま
-cef-disable-gpu が"逃げ"である理由
GPU合成を切ってソフト描画に落とせば一応は映ります。が、パフォーマンスが大きく落ちてゲーム用途では実用になりません。正面から「フレーム転送経路」を作るべきだと判断しました。
解法の考え方:IOSurface でプロセス間を橋渡しする
macOS にはプロセス間で GPU メモリを共有できる IOSurface があります。これで:
- GPUプロセス側:合成フレームを IOSurface 裏打ちの Metal テクスチャに blit
- プロセス間で「グローバルな IOSurfaceID」を受け渡す(IPC/ファイル経由)
- ウィンドウプロセス側:
IOSurfaceLookup(id)で surface を取得し、可視レイヤーの contents に貼る - consumer 側に約16ms(60fps相当)の poll タイマーを置き
setNeedsDisplay
効いた重要設定
-
IOSurfaceCreateの辞書にIOSurfaceIsGlobal = YESを必ず設定(別プロセスから Lookup するため。これが無いと取得できない) - producer 側はキーに
GetAncestor(hWnd, GA_ROOT)で取った root HWND を使う(子ウィンドウ単位だと取り違える)
透明の壁(ハマりどころ)
そのままだと不透明合成になり重なりがおかしくなる。DXMT・winemac 側の計3箇所で opaque フラグを非opaque化して解決しました。
どこまでが「珍しい」のか(正直に)
これは完成品ではなく既知の制限があります。また CrossOver は実際よく出来ていて、これは「商用ツールを買わずにどこまで行けるか」を詰めた記録です。
完全な再現手順・パッチ全文
この記事は概念に絞りました。実際のパッチ全文・自前 Wine のビルド手順・起動スクリプト・段階検証は Zenn にまとめています。
- 概念記事(無料): https://zenn.dev/niixolabs/articles/wine-cef-cross-process-present
- 顛末記(無料): https://zenn.dev/niixolabs/articles/m1-mac-steam-wine-story
- 完全な手順書(Zenn本・¥800、全7章中3章無料): https://zenn.dev/niixolabs/books/m1-mac-steam-wine-build