2024年4月29日月曜日

Text Services Framework (TSF)のつづき

以前TSFについて実装した。
TSFにまつわる文字入力と、それを表示する仕組みの部分にいくつかのバグが見つかり、ここ何日かで直していた。


バグその1


開発当初から気づいてはいたんだけど、放置していたバグ。
入力文字をテクスチャに展開していき用意していた領域全部を使ってしまった場合、先頭に戻って1行分をクリアして再利用するようにしている。
このとき、表示するデータの中に1行目のテクスチャを利用する文字が含まれていた場合表示されなくなってしまう。

この不具合の修正は、次に消えるであろう数行に表示データが含まれている場合は、最新部分に移してしまうようにすることで対処してみた。

実際に実装はしてみたがそのチェック処理が若干重めになるのと、実際に不具合が起きた際に起きる表示が1フレームだけ表示されない(もしくは別の文字で表示される)だけなので、あえて元のままにした。


バグその2


漢字変換を通さない文字入力(半角アルファベット入力など)をした直後に、漢字変換をしようと、文字入力をすると入力できないというもの。
状況によって、変換候補だけが表示されたり、更に文字入力すると表示されるようになったり、ならなかったりという現象が起こった。

現象はMicrosoftIMEで起きて、Google日本語入力では起きない。
幸い現象は100%起こせて、いつでも検証出来たがなかなか原因がつかめなかった。

何回も試しているうちになんとなく原因はわかった。
通常はITextStoreACPの継承関数がコールバックとしてTextStore側から通知される形だけど、文字変換を通さない入力の場合、ロックを掛けて、カーソル位置や内部文字を変更してアンロックをして、TextStore側は変更内容を知らない状態。勝手にこっち側で内容を変更しただけなので、文字列が増えたことが伝わってない状態で文字列外のカーソル位置に入力しているように見えてるのではないかと推測した。
GetSelectionや、GetTextなどが呼ばれて内容が変わってるのはわかってるはずなんだけど、MicrosoftIMEはわからないらしい。

その線で調べてみた結果、ITextStoreACPSink::OnTextChangeを呼べばいいのではないかと言うことにたどり着いた。
試しに実装してみると正しく入力できるようになった。
他にもこちらだけで内容を変更している箇所がいくつかあったので通知を加え、ついでにカーソル位置が変わったらITextStoreACPSink::OnSelectionChangeも呼ぶようにしてみた。


バグその3


毎フレーム登録して描画するのは非効率なので、ラベルとしてID付きで登録できるようにして、更新がなければStructuredBufferの更新をしないようにした。また一部のラベルで更新があった場合でも他のラベルのバイナリを再作成不要なようにバイナリを保持するようにした。

試しに100個のラベルを登録して毎フレームの登録がなくなりCPU使用率が下がって、GPU使用率だけが増えることを確認しようとしたところプログラムが落ちてしまった。

このデータ管理に自作ライブラリのHashMapを利用していたが、今まで発覚しなかったバグを見つけた。
調べてみるとHashMapの拡張時にデータの移行がうまく行っていなかった。
キャパシティの75%以上になったタイミングでデータ拡張を行い今までのデータを新しく確保した領域に移す。
テンプレートに指定された型がstd::is_trivially_copyable_vがtrueなら要素をまとめてmemcpy、falseなら1要素毎にnewする仕組みに振り分けている。
moveで例外を飛ばさない(std::is_nothrow_move_constructible_v)という暗黙の条件もつけていた。この条件に合致しない型を指定した状態でmoveを行うと何もしないようにしていた。
今回指定した型がちょうどstd::is_nothrow_move_constructible_vでfalseを返しており、データ拡張時元データを引き継いでいない状態になっていた。

構造体にいくつかの型が含まれているが、自作ライブラリのすべてをstd::is_nothrow_move_constructible_vにしているつもりだったけど、そうでないものが含まれていた。
UTF-8用の文字列クラスがあり、バイナリクラス(vectorのunsigned char型)を継承して作っていた。バイナリクラスはstd::is_nothrow_move_constructible_vはtrueで問題ないが、これをテンプレートクラスで継承した型の場合、スーパクラス側の自身のmoveコンストラクタとmove operator=が表面に届いていないようで、別途定義する必要があった。

必要な定義を追加して、move時std::is_nothrow_move_constructible_vでない場合はassertを入れつつ、copyを呼び出すように修正。対応していない型のmoveに気づくようにするのと、最悪このままreleaseビルドした場合でも動くようにした。


バグその4


UTF-8型の文字列クラスにもう1つバグがあって、ICUの機能を組み込んで見た目の文字数を取得できるようにする際、BreakIteratorのキャッシュを用意していた。
一度書記素単位に分けた情報を作ったら、次回はそれを使い回す。
今自分で書いていてもすぐに起こるであろうバグそのままが起きていた。
登録されている文字の変化があった場合、キャッシュのクリアが必要だったがクリアしていなかったためバグその3を直したあとに文字化けするようになった。
内容が変化し得る箇所にクリア処理を追加した。


バグその5


「バグその3」を引き起こした機能の実装でバイナリをキャッシュに問題があった。
データの更新に合わせてキャッシュはクリアしている。
ただ「バグその1」で起こっていた、テクスチャが書き換わった際のフォローが抜けている。

テクスチャの書き換えが起こったら、すべてのキャッシュを削除するようにした。
本来は利用していたものだけキャッシュを削除すればスマートだが、厳密にチェックするとなると重くなる。「バグその1」で妥協したように処理済みデータに対してキャッシュを消してしまっても、表示されないのは1フレームのみなのでこちらも妥協することにした。

これも対応してみたはいいけど、テクスチャ書き換えの度に文字がフラッシュするので、キャッシュ消す必要もない。どうせフラッシュするならそのままにすることにより影響受けるとこだけに抑えられる。結局何もしないことにした。
「バグその1」はフラッシュしないところまで実装したけど、こっちの方は今のところフラッシュしない方法を思いついてない。