土屋つかさの技術ブログは今か無しか

土屋つかさが主にプログラミングについて語るブログです。

#unity UIから「Save As...」ボタンを外したらUnityエディタがクラッシュするようになった話(TextMeshPro Deep Dive #4)

 年の瀬です。皆様いかがお過ごしでしょうか。2024年も大変お世話になりました。来年もよろしくお願いします。

 土屋はこの年末の忙しい最中、「Unityエディタ拡張の自作ツールで「Save As...」ボタンを削除したら特定タイミングでUnityエディタがクラッシュするようになる」という、中々にレアな(Unity内部の)バグを引きまして、原因特定に丸一週間を要しました。

 今回はその原因特定にいたるまでの記録です。状況がレア過ぎて応用が利く機会は恐らく無いですが、読み物としてお楽しみ頂ければと思います。

対応するUnityバージョン

  • Unity 6(6000.0.25f1)
  • Unity UI 2.0.0

何をしていたか:TMPro.FontAssetCreatorWindow.csをカスタマイズ実装していた。

 現在、土屋は、UnityでTextMeshPro(以下「TMP」)を経由せずに文字列を画面に描画できる、オリジナルのテキスト描画フレームワークを開発しています*1。この作業の過程で、TMPが提供している「Font Asset Creator」というツールを、自身のフレームワークに移植していました。

(これがFont Asset Creator。ウィンドウのサイズによってテクスチャプレビューの表値位置が下部/右部で切り替わります)

 Font Asset CreatorはUnityエディタ上で動作するツールで、TrueTypeフォントを読み込んでTMP_FontAssetアセットファイル(以下フォントアセット)を出力する機能を持ちます。

 フォントアセットには、フォントの各文字が描画された巨大なテクスチャ(「テクスチャアトラス」と言います)と、各文字のパラメータが格納されています。ゲーム実行時には、文字毎のパラメータを元にテクスチャアトラスから必要な領域を切り出して、画面に文字を描画する訳です。

 制作中のフレームワークではフォントアセットも独自フォーマットを使う予定で、このツールの実装を流用することにしたのでした。

「Font Asset Creator」ツールの構造

 「Font Asset Creator」ツール(以下「元ツール」)は、Unityのエディタ拡張の仕組みを使っていて、実装はTMPro.FontAssetCreatorWindow.csに記述されているFontAssetCreatorWindowクラスになります。

 このクラスでは、内部でTextCore.LowLevel.FontEngineと言うstaticクラスを呼んでいます。FontEngineはTrueTypeフォントファイルから情報を抽出したり、文字をテクスチャに描画する処理を担当しているネイティブコードです。つまり、このFontEngineが、元ツールの処理の中核を担っているわけです。

ツールの改造を開始

 Unity上でオリジナルのフォントアセット生成ツールを実装するには、FontEngineを制御する必要があります。しかし、FontEngineクラスはたいしてドキュメントが用意されておらず、またネイティブコードなので実装がブラックボックスです。

 そこで、元ツールを解析して、FontEngineを呼ぶ処理を流用する事にしました。元ツールは非常に機能が多く、またコードがメンテ性に難があったので、コード全体をリファクタリングしながら、土屋が必要な機能以外はゴリゴリ削っていく事にしました。

起きた事象:特定タイミングでUnityエディタがクラッシュするようになった。

 リファクタリングは順調に進んでいたのですが、ツールをテストしていると、不定期にUnityエディタがクラッシュする事に気づきました。

(↑この2週間ひたすら遭遇したポップアップ)

 クラッシュの発生条件は当初確定しておらず「ツールを起動して10秒以内に処理を走らせるとクラッシュする事が多い」くらいの、ぼんやりとした物でした。しかし元のツールでは発生しない不具合なので、当然土屋がリファクタリングした過程のどこかで、なにかしらの問題が起きた事になります。

 様々な仮説を立ててバグの原因特定を進めたものの、どうにも分かりません。最後はどうしようもなくなり、「今あるコードを少しずつ元のコードの状態に戻していって、クラッシュが発生しなくなるタイミングを見つける」という、恐ろしく手間のかかる作業を行う事になりました。この「コード巻き戻し戦略」によって、ようやく原因特定に至ったのでした。

 原因は大きく3つあり、そのいずれも「ある特定の処理を行うと、FontEngine内でメモリリークが発生する」という物でした。このメモリリークが遠因となって、最終的にクラッシュに至っていたのです。以下、3つの原因を見て行きます。

※注意※

 FontEngine自体は解析出来ていないので、以下はすべて土屋の推測による物です。予めご了承ください。

原因1:「Save As...」ボタンを外すとクラッシュする

 コードを少しずつロールバックさせた結果、「「Save As...」というボタンをカットするとクラッシュする」事がわかりました。

 ……なにそれ!?

 しかも、検証を続けた結果、名前に「.」が含まれるボタンが存在すればクラッシュしない事も分かりました。より不思議な状況ですが、なんとなくこういう事かなという推測が思いつきました。

 以下完全に想像なのですが、恐らくFontEngine内にデバッグ用のコードが残っていて、その中で「Save as...」ボタンがUI上に存在する事を前提に、ボタン名に含まれる「.」で検索して参照を取得しているのだと思われます。しかしボタンは削除されているのでnull参照となり、これがメモリリークを誘発するのでしょう。

 「Save As...」ボタンは土屋のツールには必要ないボタンだったため、改造時の最初期に外していました。まさかそんなところに原因があると思わず、コードのロールバックでも対応を後回しにしていたため、結果的に原因特定が遅くなったのでした。とほほ……。

 UIのボタンのいずれかに「.」が含まれていれば問題無いようなので、残っている「Save」ボタンの末尾にドットを付与して「Save.」ボタンにしました。嘘みたいな話ですが、これでクラッシュは収まったのでした。

原因2:不用意にDebug.Log()を実行するとクラッシュする

 Debug.Log()は、Unityにおけるprintf()デバッグ用メソッドです。簡単にコンソールに内部状態を出力できるので便利ですが、今回はこれも問題になりました。

 ツールの実行中、突然画面上のフォントが壊れる現象が起きました。ツール上の出力だけで無く、コンソールの文字も壊れてしまいます。再起動するしか復旧手段がなく、放置しているとUnityエディタがクラッシュしてしまいます。

(フォントが壊れている例。Unityエディタがこんな状態になるの、中々レアじゃないかと思います。)

 こちらも想像ですが、恐らく、FontEngineの実行中(FontEngineの処理は時間がかかるので、別スレッドで実行します)、FontEngineがUnityの文字描画処理を掴んでいて、その時にDebug.Log()、つまりコンソールへの文字描画処理を試みると、FontEngineと競合が起きて、処理が壊れるのだと思われます。

 元ツールでもDebug.Log()を使っている箇所があるので、この問題を回避する方法もあるのでしょうが、その為には元ツールと同じ処理フローにする必要があり、今回は「Debug.Log()は使わない」という事に決める事で回避しました(回避なのかそれ?)。

原因3:FaceInfoを取得するタイミングによって壊れる。

 FontEngine経由でTrueTypeフォントから取得したい情報は、大きく分けて「フォント全体の情報(FaceInfo)」「各文字の情報(Glyph)」「各文字のテクスチャ(フォントアトラス)」の3種類です。

 このうち、FaceInfoの情報については必要になるたびにFontEngineから取得していたのですが、どうも確実に取得出来るのは1回目のみで、2回目以降の取得が保証されないようで(!)、値がリセットされたりされなかったりして、これもクラッシュの原因になりました*2

 FaceInfoの取得を1回に絞り、その値を使いまわす事で解決しましたが、これも最初何が起きているのか分からず混乱しました。

おわりに

 とまあ、こんな風に複数のバグ条件が混在していたため、原因の特定に時間がかかったのでした。特に、原因を特定しようとコードにDebug.Log()を埋め込むと、それのせいでクラッシュが起きてしまうのが厳しかった……。

 なによりコードをロールバックさせている時に、「Save As...」ボタンを復活させた途端にクラッシュが止まった時は、本当に何が起きているのか分からず、呆然としてしまいまいた。長くコードを書いているとこういう状況にも遭遇するのだなあと感慨に耽ってみたり。

 これを書いている今は、ようやく改造ツールが安定して動くようになってまして、安心して年を越せそうです。みなさま、どうぞ良いお年をお過ごしくださいませ。

(おまけで開発中のツールのUIはこちら。元ツールの不要な機能を削っています。最下部の「Save.」ボタンの「.」に、土屋の一週間分の思いが込められています)

補足

余談ですが、FontEngineクラスはTMP(正確にはUnity.UIアセンブリ)内から呼び出す前提で実装されていて、必須メソッドの幾つかが別アセンブリからアクセスできません。この問題はリフレクションを使って回避出来ます。詳しくは以前の記事をどうぞ。

https://someiyoshino.info/entry/2024/12/12/224502

TextMeshPro Deep Dive 連載

#unity 深掘りTextMeshPro:SDFフォントデータによる文字描画実装(第1回:SDFの仕組み) - 土屋つかさの技術ブログは今か無しか

#unity SDFフォントデータによる文字描画実装(TextMeshPro Deep Dive #2) - 土屋つかさの技術ブログは今か無しか

#unity TextMeshPro内の低レイヤ処理クラスFontEngineの隠しメソッドにリフレクションでアクセスする(TextMeshPro Deep Dive #3) - 土屋つかさの技術ブログは今か無しか

#unity UIから「Save As...」ボタンを外したらUnityエディタがクラッシュするようになった話(TextMeshPro Deep Dive #4) - 土屋つかさの技術ブログは今か無しか

宣伝「Unityシェーダープログラミングの教科書シリーズ」

 土屋は同人誌「Unityシェーダープログラミングの教科書」シリーズを書いています。現在以下の5冊が出ています。

 これらの同人誌は、現在BOOTHの下記ストアにてPDF版を有料頒布しています。

s-games.booth.pm

 Unityのシェーダープログラミング関連について、世界で最も詳細な本だと自負しています。リンク先にサンプルページがあるので、是非御覧になってください。

*1:なぜそんな事をしているかはいずれ別記事で書きます。

*2:ただし、これについては深く調査していないので、コーディングミスの可能性もある