黒サムネイルと二次症状を分ける
CMVS(ビジュアルノベル用ゲームエンジン)で発生した直接の症状は、黒いセーブサムネイルだった。ほかの4症状は、黒を回避するために追加した補償処理から発生していた。
保存直後は一瞬正しく見える
開き直すとサムネが壊れる
メニューバー帯が混入する
新規セーブが他スロットを汚染
上書きしてもサムネが更新されない
検証した仮説
各仮説を、実ファイル、実行ファイル、実機ログの順に照合した。
JPEG マーカー説棄却
.dat内のFF D8 FFをJPEGマーカーとみなしたが、デコードすると圧縮データ中の一致だった。
GDI BitBlt キャプチャ説棄却
window DCへのBitBltを疑ったが、cmvs64.exe内のBitBltはクリップボードコピー用の1箇所だけだった。画面取得にGDIを使う経路は確認できなかった。
GetRenderTargetData → LockRect 説半分正解
経路自体は存在したが、クリップボード/スクリーンショット用だった。セーブサムネイルはGetRenderTargetDataを通らない別の取得関数を使っていた。
192×108 per-slot 注入撤退(正しい判断)
スロット描画順を数えて192×108のプレビューへ注入したが、描画順が変動して別スロットへ入ったため撤回した。encode入力を192×108サーフェスとみなした前提も一致しなかった。
lockable RT shadow + last-good フィル場所は正解、意味論が誤り
黒画面は回避できたが、「黒なら明るいフレームを返す」という輝度と鮮度の判定だった。そのため凍結、複製、誤った上書きが発生し、Windows上の取得動作を再現できなかった。
Swift .dat 後追いパッチ退役
保存後に最新mtimeの.datをランチャー側で書き換える方式は、対象ファイルを誤判定し、CMVSのコンパニオンファイルまで変更した。根本経路の修正後は退役させた。
Present 後のバックバッファ破棄確定
CMVSはセーブ/ロード画面を開くと、GetBackBuffer + LockRect(READONLY)で直前のフレームを読む。Wine/MetalではPresent後の内容が失われ、独立したGPU bltとCPU mapの両方が黒だった。
0x810定数を使う取得関数の静的解析、観測専用buildの実機ログ、Windows実機比較の3条件が一致した。
3つの実行条件でフレーム経路を比較する
Renderから.dat保存までの5段階で、Present後のフレームがどこに残るかを比較する。
調査手順
実ファイル、実行ファイル、実機ログ、Windows実機の順に、取得経路と期待値を確定した。
セーブ47件を全数デコードする
CSV2/LZSSデコーダで全サムネイルを可視化した。黒い画像が永続化されていないことと、正しい入力ならCMVSの圧縮・保存処理が成立することを確認した。
実行ファイルから取得経路を特定する
PE importとD3D9 vtable callを調べ、LockRectのflag 0x810を使う取得関数を3箇所に限定した。セーブ用経路がGetBackBuffer + LockRectであることを特定した。
補償処理を止めた観測buildで測る
補償処理を停止し、生成、転送、lockを同じ時系列で記録した。READONLY lock時にCPU mapと独立GPU bltを測り、両方が黒であることを確認した。
Windows 実機でリファレンスを確定
Windows実機でもメニューバー帯とオーバーレイを含む画像が保存された。追加加工せず、Windowsと同じ取得範囲を合格条件にした。
Windowsの取得動作を再現する
Presentごとにバックバッファを保持し、CMVSのREADONLY lockへ同じ範囲を供給した。CMVS専用gateの内側で動作し、輝度や鮮度による置換判定は使わない。
観測結果
観測 A ── 修正前のreadback
[f 831] GetBackBuffer sc=0 idx=0 -> 7AA360
[f 831] LockRect surf=7AA360 1280x720
flags=0x810 acc=0xd bind=0x20
[f 831] GPU_PROBE blt_avg=0 ← GPU側でも黒
[f 831] LOCK_MAP map_avg=0 ← CPU mapも黒
GetDC / GetRTData / StretchRect : 0 回
ロック対象はバックバッファ。内容は CPU に届かないのではなく、Present 時点で GPU からも消えていた。
観測 B ── cmvs64.exeの取得経路
0x14001c59a call [rax+0x90] ; GetBackBuffer
0x14001c617 call [rax+0x68] ; LockRect(0x810)
; GetRTData を経由しない第3変種
0x14009095b call BitBlt ; exe 唯一のBitBlt
; → SetClipboardData(クリップボード)
mov r9d, 0x810 は exe 全体で3箇所のみ。すべてフレーム取得関数クラスタ内。GDI 画面キャプチャは存在しない。
観測 C ── last-presented供給後
[f 503] BB_SERVE_LAST_PRESENTED 1280x720 [f 503] LOCK_MAP map_avg=134 ← 明るい save032.dat cs1=61703 native LZSS 喫茶店の場面 save033.dat cs1=66556 native LZSS 森の場面 save999.dat(コンパニオン)無傷
2スロットと上書きで別のシーンを保存し、CMVSのnative LZSS形式とコンパニオンファイルの無変更を確認した。
再利用できる知見
資料より実体を優先する
記録上の完了状態を、実ファイル、実行ファイル、実際にロードされるDLLと照合する。配備物はhashとprovenanceで固定する。
補償処理と根本経路を分ける
5症状のうち4つは回避処理から発生していた。観測時は補償を止め、原因経路を確認してから不要なpatchを撤回する。
輝度判定ではなく取得動作を再現する
「黒なら別画像を返す」のではなく、Present後の直前フレームをREADONLY lockへ供給する。入力の意味に沿うと、凍結や誤上書きを避けられる。
結果が分かれる計測を先に置く
CPU mapと独立GPU bltを同じ時点で測ることで、「転送だけの問題」と「GPU側で既に消失」を区別できた。
リファレンス環境で期待値を決める
Windows実機でも帯やオーバーレイが保存された。見た目を独自に整えるのではなく、リファレンスと同じ取得範囲を合格条件にする。
現在の状態と制約
- shared d3d9は、CMVS用のsnapshot writer(画像を書き出す経路)とread/inject経路を含む、採用済みの組み合わせを使用する。
- CMVSのgame profileだけが
MELAMMU_CMVS_THUMBSを有効にする。ほかのengineではdefault-off。 - この組み合わせは2026-07-14に、CMVS回帰からの復旧を実機確認済み。
- Wine側writerを除去してScreenCaptureKitへ一本化する案は、CMVSが黒へ回帰したため撤回済み。
- 本稿のlast-presented経路を、DXVK、別engine、Melammu Galleryの手動スクリーンショットへ一般化しない。
- 実際に配備するd3d9.dllは、runtime manifestで選択された採用済みの組み合わせを正とする。