ADBでAndroid機の解像度を変える方法と、その用途

こんにちは。技術部平山です。

今日は、

adb shell wm size 360x640

で、usbでつながったAndroid機の解像度を360x640にできる、というだけの話題です。

この記事の残りは、「なんでそんなことがうれしいか」という話であり、 情報自体は「ADB 解像度」で検索して出てくるサイトの方が有益かと思います。

動機

最近、カジュアルゲームで遊ぶことが結構あるのですが、 カジュアルと言いつつも、要求する性能はかなり高いケースが多いのです。

私の京セラS2は、2017年製でそう古くもなく、OSもAndroid9なのですが、 GPU性能はなかなかに絶望的です。Unityの標準シェーダで全画面描画(1280x720)されると、 確定で30fpsを下回ります。 しかしながらカジュアルゲームはフレームレートが低いと遊びにくいものが多く、 普通にやったのではまるで遊べないのです。

しかし、ここで解像度を下げられれば、事態は改善するはずです。 その方法は、調べたらあっさり出てきました。上のコマンドです。 モノによっては、解像度を縦横半分にすると、理論値通りフレームレートが4倍になったりもします。

さて、カジュアルゲームの話は私の個人的な事情ですが、もちろんのことこれは開発にも使えるのです。

重い時にまず調べるべきは「塗り」

ゲームのフレームレートが出ない、となった時に、真っ先に調べるべきは、 「それが塗り(=フラグメントシェーダ処理)によるのか、そうでないのか」 です。 プロファイリングなんて後でいいから、とにかく「塗り」です(異論ありそうですけど)。何故か。

CPUの性能差なんかよりも、GPUの性能差の方がずっと大きいからです。

以前ベンチマークの記事で数字を示した通り、 安物と高級機の CPUの性能差はたかだか5倍程度ですが、GPUでは数十倍にもなります。 5万円の機械が3万円の機械の5倍速い、10万円の機械は5万円の機械の5倍速い、 みたいなことが現実に起こっており、それでもう25倍の性能差、ということになるわけです。

プロファイリングして重い所を見つけて直しても、倍速にまでなることは稀ですし、 GPUが遅い機械ではCPUをいくらいじっても改善しません。 どちらかと言えば、CPU側のプロファイリングは、「このフレームだけガツンと重い」 という「負荷スパイク」を見つけることが主目的になると考えています。 フレームレートを上げたければ、断然GPUが優先です。

塗りかどうか調べる簡単な方法

さて、では「処理落ちが塗りのせいかどうか」を知るにはどうすればいいでしょうか?

解像度を下げましょう。 それでフレームレートが上がったら、塗りのせいだとわかります。 adbを使って解像度を下げれば、これを数十秒で試せます。最高の手段です。

Screen.SetResolution() を仕込んでビルドを作る、となれば10分では無理ですし、 いろいろな解像度を試すとなればデバグ機能を仕込んだりビルドし直したりせねばなりません。

エディタ上で解像度を変えるのはゲームビューの設定を変えるだけなので楽ですが、 エディタはPC上で動いており、スマホとは性能が異なります。

ADBなら実機ですぐに見られるのです。

しかも、自分で開発してないアプリであっても試すことができます。 普通にインストールしたアプリを、いくつかの解像度で遊んでみればいいのです。 フレームレートの表示がなくても、フレームレートが60なのか30なのか20なのかは、だいたいわかります。 いくつかのゲームで試してみましたが、多くのゲームは解像度を下げることでかなりフレームレートが上がりました。 つまり、それらのゲームはGPU性能が足りなくてフレームレートが落ちていたわけです。 フラグメントシェーダの処理を軽くできればフレームレートが上がるはずだ、 ということが、コードを見るまでもなくわかります。

塗りであるとわかった場合

さて、このテストで、「塗りのせいでフレームレートが落ちていた」とわかった場合、 どうすればいいでしょうか。

一つは「製品の解像度を落としてしまう」ことです。 以前FixedDPI設定の記事 に書いたように、解像度に制限をかけてしまうのは乱暴ですが低コストな手段です。 もっと真面目にやるなら、Screen.SetResolution()を使って、 お客さんが設定できるようにするのが良いでしょう。

もちろん「真面目に最適化する」という道もあります。 シェーダを軽くし、重ね描きを削減し、場合によっては機械の性能を見て機能のOn/Offを実装する、 という地味で高コストな道です。 この場合は、解像度を変えてみることで「何倍速くしたらいいのか」の目安もすぐに得られます。 解像度をどこまで落とした所で目標FPSに達するか、を見ればいいのです。 1280x720で足りず、960x540で60fpsに達したのであれば、だいたい1.8倍くらい 高速化すれば元の解像度のままでも60fps出せる、ということが想像できます。 その時点で「あ、そりゃ無理だ」となったら、解像度を落とすなり、 フレームレートの目標を落とすなりすることになります。

また、「いくら解像度を下げても目標に達しない」場合は、 塗り以外に問題があることがわかります。例えばDrawCallが多すぎてCPUで引っかかっていれば、 いくら解像度を下げても、一定以上には速くなりません。 その場合も、「今のCPU負荷なら、解像度はこれ以上下げても無駄」 「今のCPU負荷なら、x倍以上ピクセル処理を高速化しても無駄」 というラインがわかり、それはそれで有益です。

塗りでなくなった場合の指針

ついでなので、「問題が塗りじゃなかった」という場合に疑うべき所をリストにしてみます。

  • GPU
    • 頂点処理。頂点数が多すぎていないか?
  • CPU
    • DrawCallやらSetPassやら
    • スクリプト
    • 物理

頂点をムチャクチャ出している覚えがなければ、ほぼCPUでしょう。 その時はプロファイラの出番です。 すぐに直せる、あからさまなミスが見つかることを祈るのみです。

関連コマンドまとめ

他の記事を見ればわかることですが、便宜のために関連コマンドをまとめておきます。

adb shell wm size

デフォルトのサイズと、今の設定を表示します。

adb shell wm size 360x640

解像度を設定します。ちなみに360x640は私のお気に入りで、絵が視認できる範囲で 一番低い解像度かな、という印象です。twitterやslackもおおよそ使えます。

adb shell wm density

デフォルトと現在の画素密度を表示します。例えば私の機械だと、デフォルトが320です。 解像度を360x640にした時には、これを次のコマンドで160に落とすと、 ホーム画面のレイアウトが元と同一になります。

adb shell wm density 160

画素密度を設定します。解像度を落としたらこれも変えておかないと、 ホーム画面等でレイアウトが変わってしまいます。

終わりに

私のスマホは元の解像度が720x1280なのですが、もう1週間以上360x640にしたままにして使っています。 そうしないとゲームがまともに動かないのです。 大手が作る豪華なゲームよりもカジュアルゲームの方が圧倒的に辛いのが面白い所ですね。 slackもtwitterも、汚ないですが慣れました。 ゲームの絵も汚ないのですが、フレームレートが低いよりはマシです。

さて、おそらく次に機械を買い換えると、解像度は1920x1080を超えてくるはずです。 最近は細長いのが流行ってますから、2160x1080とかになるかもしれません。 現在の1280x720に比して、ピクセル数が2倍以上になるわけですが、 機械の性能は2倍どころか1.5倍になるかどうかすら怪しいのが現状です。 よって、おそらくは機械を買い換えたことでフレームレートが落ちることになります。

ゲーム開発者の皆様には 世の中には最新なのにADBで解像度を下げないとゲームできない機械が普通に売られているんだよ ということを知っていただけると幸いです。 OSのバージョンやメモリの量と性能はほとんど関係ない、 ということも広く知られると良いなあと思います。 Android9でメモリ3GB積んでいても、GPUが貧弱なマシンはいくらでもあり、 そのくせ解像度だけは高いのです。 そういった事情については、トライエースさんのサイトにある、 2019年のCEDECでの講演(PPTXファイル直リンク) が参考になるかと思います(slideshareに置いていただけたらうれしいな!)。

ところで、「おまえ、なんでそんな安い機械にこだわってんの?」 と思われる方もいらっしゃるのではないでしょうか。

高い機械を買いたくない、というような話ではありません。 高級機を買ったら最適化に興味を失うと100%断言できる からです。そして、周囲の大半がiPhoneを使っていて、 日常的に低性能Androidを使っている人がほとんどいないからです。

もしチーム全員が高級機だったら、絶対感覚が狂います。 頭ではわかってても、感覚としては低性能機のことがわかりません。 実感としてストレスを感じていないと、 「これは直さないとダメだよ!遊べないよ!」という切実さは出てこないのです。

確かに、もうちょっと上の性能のものを使ってもいい気はします。 このレベルの機械を使っている人はたぶんゲームをしないでしょう。 なにしろADBを使わないと遊べないのですから。

しかし逆に言えば、このレベルでも快適に動くようにできれば、 もっと多くのお客さんに遊んでもらえるかもしれない、ということでもありますし、 それはそれほど難しいことでもないと思うのです。 とりわけカジュアルゲームにおいては。

ちなみに、京セラS2は実に頑丈で、風呂に沈んでもなんともなく、熱も出ず、 私は非常に気にいっております。 考えてみたら、PHSの時代からずっと京セラ使ってますね、私。

UnloadUnusedAssets()って必要なの?

こんにちは。技術部平山です。

今回はUnityのResources.UnloadUnusedAssets()について書きます。

結論は 「呼ばないといけないっぽいので呼ぼう。頻度はいい感じで」 という何ら面白味のないものですので、そこに至る経緯に興味がある方のみお読みください。

動機

最近、非常な高頻度でUnloadUnusedAssets()を呼んでいるコードに出会いました。 「操作によって画面に何か出た時には大抵呼んでいる」というくらいの高頻度です。

「いや、こんなに呼ばなくてもいいんじゃないの?」と思ったわけですが、 考えてみれば、「そもそも呼ばないといけないものなのかどうか」 を知りません。いろいろな人がいろいろなことを言っていますが、 中の人による公式な発言ではなく、 調べた上で書いている記事であっても、所詮は「特定のバージョンの話」です。

とはいえ、こんな基本的な所は納得していないと怖くて仕方がありませんので、 私も「特定のバージョンで調べてみた」をやることにしました。

この記事では2018.4.11を用いています。 また、エディタとmacOSのStandaloneビルドのみの調査となっています。

呼ばなかったらどうなるのか?

実験は簡単です。

デカいテクスチャをロードしてUI.RawImageに貼って表示することを繰り返します。

public class Main : MonoBehaviour
{
    [SerializeField] RawImage rawImage;
    int index;
    int count = 100;
    UnityWebRequest req;
    string message;

    void Update()
    {
        if (req == null)
        {
            var appDir = System.IO.Path.GetFullPath(".");
            var url = string.Format("file://{0}/TestData/{1}.png", appDir, index.ToString());
            Debug.Log(url);
            message = url;
            req = UnityWebRequestTexture.GetTexture(url);
            req.SendWebRequest();
            index = (index + 1) % count;
        }
        else if (req.isDone)
        {
            rawImage.texture = DownloadHandlerTexture.GetContent(req);
            req.Dispose(); // これ忘れちゃダメよ!!(実は最初忘れてた)
            req = null;
        }
    }
}

プロジェクトフォルダにTestDataというフォルダを作り、そこに0.pngから99.pngまで 4096x4096の巨大pngファイルを配置しておきます。 あとはこれを実行です。UnityWebRequestでPNGをロードしてRawImageに貼ることを繰り返します。 新しいテクスチャをRawImageに差した所で古いテクスチャへの参照はなくなるので、 Unityが自動解放してくれるなら使用メモリ量は一定以上増えないはずです。

この、テストプログラムはプロジェクトごとGithubに置いてあります。 テスト用のpngはメニューから生成できます(アップロードすると大きいので)。

エディタでは

f:id:hirasho0:20191120105931p:plain f:id:hirasho0:20191120105921p:plain

Unityのプロファイラと、macOS標準のアクティビティモニターで見たのが上の画像です。 プロファイラではテクスチャのメモリ量が増え続けており、Unityの使用メモリ量が5GBを超えていますね。

さらにアクティビティモニターでも見てみますが、やはりデカいです。12GBを超えています。 プロファイラの画面写真よりも少し時間が経った頃のものなので、Unityのプロファイラよりも大きな値になっていますが、 「止まる気配がない」ということでは同じです。

実際、放っておくと、

f:id:hirasho0:20191120105924p:plain

OSがメモリ不足に陥って警告を出します。110GB以上使ってますね。こりゃダメです。

なお、この状態でUnloadUnusedMemory()を呼ぶと、メモリが一気に解放されて、 1GBくらいまで落ちます。少なくともエディタではこの関数を手動で呼ばないとダメだ、 ということがわかりました。

PCビルドでは

ではPCビルドで試してみましょう。エディタでは解放を指示しても解放されない、 といった話は調べると結構出てきます。ビルドでは自動解放される、 ということであれば、エディタでだけ呼べばいい、ということにもなるでしょう。

f:id:hirasho0:20191120105928p:plain

変わりませんね。どんどん増え続けています。プロファイラで見ても、アクティビティモニターで見ても同じです。 そして、UnlodaUnusedAssets()を手動で呼べば、一気に解放されます。

つまり、ビルドでも呼ばないとダメということがわかりました。

結論は?

呼ばないとダメだってことです。

もしかしたら他のバージョン、他の機械向けのビルドでは 自動解放されるかもしれませんが、保険を考えるなら呼ぶべきでしょう。

なお、Unityが内部で自動的に呼ぶこともある、という記述 も見つかりましたが、公式のマニュアルに書かれているわけでもなく、 しかも昔のバージョンの話ですから、今もそうかはわかりません。

確かに、「AdditiveでなくSceneをロードした時は中で呼ばれる」が本当であれば、 シーンロード後に自力で呼ぶことは無駄であり、 お客さんに余計なスパイクを見せることになります。 省いて良いなら省きたいところです。 しかし公式情報と言うには弱いですし、 弊社の製品の場合は基本Additive でシーンをロードして、 いらなくなったものを手動でUnloadSceneAsync()しているケースが多く、 やはり手動での呼び出しは必要と考える方が無難かと思います。

考えてみてください。 「呼ばなくていいのに呼んだ時」に起こることはせいぜいスパイクですが、 「呼ばないとけないのに呼ばなかった時」に起こることはクラッシュです。 重みが違いすぎます。

じゃあどれくらい呼ぶの?

となると実際に問題になるのは「いつ呼ぶの?どれくらい呼ぶの?」ですよね。

呼ばずにいる時間が長くなるほど、アプリが使うメモリ量は増えることになります。 しかし、大して解放できない時に呼んでも、無駄なスパイクを発生させるだけです。

製品の性質や作り方によるとは思うのですが、平山の個人的な考えとしては、

  • 客が操作できる時間、画面で何かがアニメーションしている時間には呼びたくない。
  • 一回画面が暗転する、NowLoading表示が出る、といった場所では念の為呼んでおく。

というくらいな気がします。 今のUnityは昔と違って非同期処理なので、「呼んだら即スパイク決定」というわけでもなく、 実際どれくらい処理時間がかかるかはわかりません。 メモリ内にあるアセットの数や、機械の性能、スレッドの状況にも影響されるでしょう。 しかし、できれば無駄なことはしたくありません。

例えばステージクリア型のアクションゲームであれば、ステージとステージの間には呼んだ方が良さそうです。 また、カードゲームで「デッキ編集画面」「カード購入画面」みたいなものが 分かれていて、それぞれがそれなりなアセット量があってロード時間が0.2秒以上あるようであれば、 そのタイミングで呼んでおいてもいい気がします。

ただ、もし「エンドレスなゲームで何十分も操作可能な時間が続き切れ目がない」 となったらどうしましょうかね。 その場合は、何らかの「必ずある程度の時間で訪れる何かのイベント」 に付随してUnloadUnusedAssets()しておかないとマズいことになるでしょう。 あるいは、「ゲームが始まったらInstantiateもロードもしない」 という設計にしてしまえば、呼ぶ必要がなくなります。 それも一つの選択肢でしょう。 私は「ある程度は開始時生成、足りなくなった分だけ動的生成」の方が好きですけどね。

おまけ: エディタ拡張とUnloadUnusedAsset()

このUnloadUnusedAssets()、エディタでアセットを編集している最中に呼ぶと、編集中のアセットがいきなりnullになることがありました。

例えば、AssetDatabase.LoadAssetAtPath() でマテリアルをロードして、値を書き換えてから、AssetDatabase.SaveAssets() でセーブ、という処理をすることがあるかと思いますが(最近遭遇したのではNGUIのアトラス生成でした)、 この途中でUnloadUnusedAssets()の呼び出しが紛れ込むと、 いきなりアセット(ここではマテリアル)がnullに化けることになります。

マニュアルにあるように、EditorUtility.SetDirty を呼んでいれば問題なく、実際呼んでいるのですが、呼ぶタイミングが遅かったのです。 値を書き換えてから呼ぶのが自然に思えますが、書き換える前にnullにされてしまえば、 書き換える時に死ぬことになります。 SetDirtyされていない場合、どのGameObjectからも参照されておらず、static変数からも辿れないアセットは、 「使われていない」とみなされて消されるのです。

「いや、エディタ拡張の処理してる時にUnlaodUnusedAssetsなんて呼ばないでしょ」 と思われるかもしれませんが、罠があります。 AssetDatabase.Refresh() が中でこれを呼んでいるのです。いかにもエディタ拡張から呼びそうですよね。

結局、EditorUtility.SetDirty をロード直後に呼ぶようにした上で、良く見たらRefreshが不要だったので呼ばなくしました。 SaveAssets()やRefresh()は0.5秒以上かかることもある重い処理でして、できれば避けたいのです。

なお、この件は5.6.5でのお話なので、今はまた挙動が違うかもしれません。