Artemis / iarsys保護 · 動画再生 · winegstreamer / EVR

デコード済みなのに動画が黒い

映像は作られていた。画面へ渡す説明情報が足りず、表示側に受け取ってもらえなかった。

問題映像データは作られていたが、画面へ渡す段階で39個の候補がすべて拒否された。
アプローチデータが届いた最も遠い地点から先をたどり、最初に拒否される受け渡しを計測した。
結果画面サイズと非圧縮性の情報を補うと受け渡しが成立し、実際の動画も最後まで再生できた。
この先で使う6語Webの処理パイプラインに置き換えて読めるよう、この事例での役割だけを説明する。
デコード
圧縮された動画データを、表示できる映像フレームへ戻す処理。今回はこの出力まで生成されていた。
Media Foundation
Windowsの動画再生基盤。入力、デコード、形式変換、表示といった複数の処理をつなぐ。
winegstreamer
Wineの動画APIをGStreamer上で動かすモジュール。この事例ではデコードと形式変換を担当する。
トポロジ
動画処理の各段階と接続順を表す構成。Webでいえば、複数のmiddlewareをつないだ処理経路に近い。
メディア型 / YUY2
処理間で渡す映像データの仕様書に当たる。YUY2は画素形式の名前で、寸法などの属性も必要になる。
EVR
デコード後の映像を受け取り、画面へ出すWindowsのレンダラ。今回はここで拒否されたが、原因は手前にあった。

デコード済み映像があるのに再生が始まらない

ゲームの末尾動画へ入ると、映像も音も出ないまま画面が黒くなった。見た目だけなら、動画を映像へ戻す処理が失敗したように見える。

しかし、動画が画面へ出るまでには複数の段階がある。黒画面だけを見ても、どこまで処理が進み、どこで止まったかは分からない。

まず、動画が画面へ届くまでを一枚で見る

今回は3段階目まで進んでいた。しかし、映像と一緒に渡す説明情報が足りず、4段階目の入口で拒否されていた。

対象はArtemisエンジンの64bitタイトルで、iarsys保護レイヤーを持つ。この事例では、表示側に届く直前の受け渡しを調査した。

調査の問い: 映像がすでに作られているなら、どの受け渡しで拒否され、どの説明情報が足りなかったのか。

映像データの存在から最初の仮説を棄却する

最初の仮説を設定だけで試さず、診断トレースから「処理が到達した最も遠い地点」を確認した。

FIRST HYPOTHESIS

音が無いのでデコードが失敗している

映像も音も出ない現象から、winegstreamerのデコード処理を最初の原因候補にした。

棄却
OBSERVATION

デコード後のバッファはすでに渡されていた

診断ログには、デコード後のデータをWine側へコピーする処理が4回残っていた。少なくとも、デコード後のデータを受け渡す段階までは到達している。

接続境界へ進む
判断: この観測だけでは停止地点を確定できない。ただし「無音だからデコード失敗」という仮説は棄却できる。次に、生成済みデータが下流へ接続される境界を調べる。
観測に使ったログ名と件数

wg_parser_stream_copy_bufferが4回記録されていた。この記録はデコード後のバッファが受け渡された証拠であり、正確な停止地点を単独で示すものではない。

最初に拒否された受け渡しを探す

通常のログには黒画面の理由が出ていなかった。そこで表示処理の終点ではなく、作られた映像を表示側が受け取る瞬間へ計測を入れた。

確認済みの地点から一段ずつ先へ進み、最初に失敗する受け渡しを探す。拒否された理由が分かれば、その情報を作った側へ遡れる。

計測すると、圧縮されていない映像が「圧縮済み」と誤判定され、39個の候補がすべて拒否されていた。これにより、調査対象を表示処理から「表示用の形式と説明情報を作った側」へ移せた。

受理判定で得た実測値
dwWidth 1280, dwHeight 720, is_compressed 1, Format YUY2 → 0xc00d36b4 ×39

YUY2は非圧縮の画素形式である。それにもかかわらず、WineのEVR mixerではis_compressed 1と判定されていた。

映像と一緒に渡す二つの情報が欠けていた

表示側へ渡すのは映像だけではない。「1280 × 720の大きさである」「圧縮されていない」といった、映像を正しく扱うための説明情報も必要になる。

今回は画素形式の名前だけがあり、画面サイズと非圧縮性を示す情報が欠けていた。拒否したのは表示側だが、直すべきなのは不完全な説明情報を作った変換側だった。

候補形式が表示側へ渡る実装上の経路

WineのEVR mixerは受け取れる入力型の一覧を返す処理に対応しておらず、GetInputAvailableTypeがE_NOTIMPLを返す。そのためトポロジ解決は、変換器が生成した候補型をそのままEVRへ提示する。

修正前

YUY2という形式名だけでは足りない

  • 画面サイズ 欠落
    MF_MT_FRAME_SIZEが無く、幅と高さを0として扱われる
  • 非圧縮であること 欠落
    MF_MT_ALL_SAMPLES_INDEPENDENTが無く、Wine実装では圧縮形式と判定される

候補型を拒否し、描画へ到達しない

修正後

変換器が型の成立条件を明示する

  • 画面サイズ 入力から引き継ぐ
    MF_MT_FRAME_SIZEに1280 × 720を保持する
  • 非圧縮であること 明示する
    MF_MT_ALL_SAMPLES_INDEPENDENTをTRUEにする

候補型を受理し、映像処理を生成できる

この時点: 黒画面の原因は二つの属性不足だと特定できた。ただし、まだ修正も実再生も確認していない。次に情報を作る側を直し、黒画面が消えるかを確かめる。
この原因を一般化しない理由

属性が無い場合の圧縮判定はMedia Foundation一般の定義ではなく、このWine実装の挙動である。実際の修正ではMF_MT_FIXED_SIZE_SAMPLESも併せて設定した。

情報を作る側を直し、実際の再生で確かめる

ここからが解決の段階である。変換側へ画面サイズと非圧縮性を補ったビルドに替え、黒画面だった末尾動画をもう一度再生した。

直したのは表示側の判定ではない。変換側が完全な説明情報を渡すようにし、画面サイズを入力から引き継ぎ、固定サイズかつ非圧縮の映像であることを明示した。その結果、39個の候補を拒否していた接続が成立し、ゲーム上でも末尾動画が映った。

変更した関数と検証構成

video_processor.cとcolor_convert.cの2関数を、兄弟関数と同じ属性設定へ揃えた。検証した構成には、トポロジ解決へ適応した11行の上流backportと、出力型へ属性を補う25行の変更が入っている。25行の有無だけを変えた比較試験は行っていないため、「属性追加だけで解決した」とは扱わない。

誤判定消失「圧縮済み」という内部判定が1から0へ変わった
接続成立型接続に関係する失敗を観測しなくなった
映像表示表示に必要な映像処理が生成され、黒画面だった動画が映った
完走確認末尾動画から後続シーンを経てタイトル画面へ戻った
ここで解決: 黒画面だった末尾動画が再生され、後続シーンからタイトル画面まで戻った。内部ログだけでなく、この一連の実動作まで通った時点を解決とした。

症状が現れた場所と原因を作った場所を分ける

今回の価値はEVR固有の属性名ではなく、理由の出ない拒否を、データが通過した地点と境界の判定から切り分けたことにある。

  1. 到達済みの地点を証拠で決める画面や音の有無だけで処理段階を推測せず、生成済みデータやログから確認する。
  2. 最初に拒否される境界を計測する終点の一般ログに理由が無ければ、入力を受理する判定へ観測点を置く。
  3. 拒否条件を作った生成側へ遡る受信側の制約を緩める前に、渡されたデータが契約を満たしているか確認する。
  4. 内部値と利用者が見る結果を両方確かめる判定値の反転に加え、実際の再生と後続処理まで受け入れ条件にする。
確認できていること

主対象と比較対象の実機再生

  • 検証構成は11行の上流backportと25行の属性追加を含む。
  • 黒画面を調べた主対象の末尾動画と、非回帰確認に使った同じエンジンの比較対象で再生を確認した。
この結果から断定しないこと

未分離の効果と適用範囲

  • 25行の属性追加だけによる効果は分離していない。
  • Artemis全般や他エンジンへは一般化しない。
  • 先行していたクラッシュ層はこの記事の対象外である。
  • 39件などの集計値は当時の実機トレースに基づき、生ログを再集計していない。