Linux上でWindows向けのデスクトップアプリを動かしていたら、「見えているのにクリックできない」 という症状にぶつかりました。 メニューは開く、保存もできる、タイムラインの描画も正常。でもキャンバスに置いた要素を クリックしても、うんともすんとも言わない。アプリの設定を疑って半日ほど溶かしたあと、 原因は互換レイヤーの描画APIが返す「境界矩形」でした。地味ですが再現性のある話なので、 切り分けの順番ごと残しておきます。
症状: 描画は正しい、当たり判定だけが死んでいる
再現手順はこうです。Windows向けの動画編集ソフトをWine上で起動し、プレビュー領域に 画像やテキストのアイテムを配置する。ここまでは何の問題もなく、アイテムは指定した位置に 正しいサイズで表示されます。ところがそのアイテムをマウスでクリックしても、選択状態にならず、選択枠も出ません。
この時点で分かっていたことを並べます。
- 描画は完全に正常。スクリーンショットを撮ると、アイテムは正しい位置・サイズで写る
- キーボードのタブ移動では選択できる。つまり要素はシーングラフ上に存在している
- 何もない余白をドラッグして範囲選択すると、なぜかアイテムが選べる
- 同じウィンドウ内のボタンやスライダーは普通に反応する
「キーボードでは選べる」が決定的でした。要素が存在しないのでも、描画が壊れているのでもない。マウス座標から要素を探す経路だけが壊れている。 そう絞れた瞬間に、疑う場所が一気に狭まりました。
切り分け: 疑う順番を固定する
こういう時、思いつきで設定画面をいじると時間が溶けるだけなので、疑う順番を決めて 上から潰していきます。今回は4段階でした。
1段目、マウス座標の変換ミス。Wineのウィンドウ内で座標がずれているなら、 クリック位置が常に少し外れているはずです。xdotoolで座標を数ピクセルずつ動かして 叩いてみましたが、反応は変わりませんでした。同じウィンドウのボタンは当たるので、 座標変換そのものは正常です。
2段目、描画パイプライン。スクリーンショットで確認した通り、 ピクセルは正しく出ています。
3段目、アプリ側の状態。設定ファイルやキャッシュの破損を疑って、 新しいプレフィックス(Wineの擬似Windows環境)を作り直し、初期設定のまま起動しました。 挙動は変わりません。
4段目、アプリに何を渡しているか。ここで初めて、 「アプリのコードは正しいのに、渡ってくる値が嘘」という可能性を正面から見ることになります。 そしてこれは、互換レイヤーで開発している人にとっては最頻出のパターンでもあります。
アプリは「見た目」ではなく「境界」を信じている
キャンバス系アプリの当たり判定は、たいていピクセルを読んでいるわけではありません。 描画した画像やコマンドリストに対して、「この描画はローカル座標で どこからどこまでの範囲を占めるか」をAPIに問い合わせ、その矩形から クリック判定用のクアッドを組み立てています。Draw系のAPIに 「ローカル境界を取得する」関数があり、アプリはその戻り値を信じているわけです。
で、その関数をWine側で叩いて戻り値を実測したところ、空の矩形が返っていました。 空の矩形から作ったクアッドは幅0・高さ0に縮退します。縮退したクアッドは どこをクリックしても内側になりません。だからクリックは常に外れる — 描画は 別経路なので無傷、という症状の説明がつきました。
腑に落ちないのは「なぜ空になるのか」です。答えは単純でした。 その描画の仕組みは命令を記録して後でまとめて再生するタイプのオブジェクトで、 記録の時点では「実際にどれくらいの範囲を塗るか」を一切計算していなかったのです。 境界を聞かれても、答える材料が手元にない。だから「空」を返す。 未実装のスタブと、正しく計算した結果の「空」は、外から見ると区別がつきません。
直し方: 記録するときに境界を累積する
方針は「記録しながら範囲を足していく」の一択です。再生時に計算しようとすると、 記録済みの命令を再走査する必要が出て、コストも複雑さも跳ね上がります。 記録側で累積しておけば、問い合わせは定数時間で済みます。
- 矩形を塗る命令: 矩形をそのまま累積範囲にunionする
- ビットマップ・画像を描く命令: 宛先矩形と、元画像のサイズから 実際に占める範囲を求めてunionする
- ジオメトリを塗る命令: ジオメトリの境界ボックスを使う
- 記録時にかかっている変換行列: これを掛け忘れると、拡大縮小や 回転をかけた要素だけ境界がずれる。地味ですが一番やらかしやすいところ
- 効果(エフェクト)が付いている場合: 効果自身はサイズを変えないので、 入力側のサイズを辿って解決する
修正後にもう一度実測すると、境界は(-640, -360) 〜 (640, 360)を返すようになり、 プレビュー上のアイテムをクリックすると選択枠が出るようになりました。 「クリックできない」という症状に対して、実際に変更したのは戻り値の中身だけです。描画コードは1行も触っていません。
もうひとつの罠: バージョン混在で起動即死
修正版をビルドして差し替えたら、今度はアプリが一瞬で落ちるようになりました。 ログにはX Error ... X_ConfigureWindow ... Value 0x0。 描画の修正とは関係のない、X11まわりのエラーです。
原因はビルド元のツリーが2つ混ざっていたことでした。Windows側のDLLは 多少バージョンがずれても案外動くので油断していたのですが、Linux側のドライバ (ウィンドウやX11を扱う部分)は本体とバージョンが揃っていないと、 ウィンドウ生成の段階で落ちます。Windows側は許容、Linux側は絶対に揃える — この非対称性を知らずに、片方だけ新しいツリーでビルドしたのが敗因でした。
対策は運用でカバーしました。ビルド元のツリーを1つに固定し、 「このディレクトリ以外からバイナリをコピーしない」と決める。それだけで、 原因不明の起動失敗が丸一日溶かす障害から、5分で気づける作業ミスに格下げされます。
この一件から持ち帰ったこと
一番大きいのは「描画は正しいのに操作だけ壊れている」なら、 描画APIの戻り値を疑うという引き出しが増えたことです。ピクセルを見ても 何も分からない領域なので、目視のデバッグが効きません。だからこそ、 疑わしい関数の戻り値を自分で叩いて数値を出すのが最短でした。
次に、「動かない」と「動かないように見える」を分けて考えること。 今回は要素は存在していて、見つけられていないだけでした。範囲選択では選べた、 キーボードでは選べた — この2つの事実が、症状を「存在しない」から 「探索経路が壊れている」へ読み替えさせてくれました。壊れ方の非対称性は、 原因の在処を教えてくれるサインです。
最後に、互換レイヤーの未実装は「空」として現れるという点。 例外を投げてくれればすぐ分かるのに、空の値を返して静かに動き続けるので、 アプリ側のバグに見えます。外部ライブラリの挙動が怪しいと感じたら、 疑う前に戻り値を実測する。泥臭いですが、これが一番速いです。