UnityのNativeArrayを使ってASTCをロードする

# まえおき
ASTC対応GPUが増えてきた関係でモバイルでは最早必須レベルとなったASTCフォーマット。
またNativeArrayもUnity2018以降から様々な低レベルAPIが追加され、独自処理にも活用しやすくなりました。
今回はAB化せずNativeArray経由でASTCフォーマットをロードする方法を紹介したいと思います。

# NativeArrayを使ってASTCをロードするメリット
- UI等で使う1枚絵といったケースでわざわざAB化しなくても良い（ビルド時間削減
- ABにしない関係でASTCエンコードにUnityが不要になる（パイプラインに組み込みやすい
- png/jpgならAB化せずとも`Texture2D.LoadImage`でロードできるが、メモリをマネージド領域にすべて展開する必要があり、メモリ負荷が高い
- png/jpgのデコードはCPU側になるためCPU負荷も高い

#前準備
## 下調べ
今回はABではないためUnityのASTCエンコードには頼らず、自前でASTCエンコードを行います。
幸いにもARMから公式のエンコーダが公開されてますので、今回はそれを使います。

今回使うASTCエンコーダー
https://github.com/ARM-software/astc-encoder

`./astcenc -c sample.png sample.astc 6x6 -medium`

こんな感じでお手軽にエンコードできる。Unityも不要なのでciツールとしても導入しやすいと思います。

またエンコードしたデータの展開先はNativeArrayですが、GPUにアップロードするには`Texture2D.LoadRawTextureData`を使います。
ただこの`Texture2D.LoadRawTextureData`は`Texture2D.LoadImage`と違い明示的にフォーマットと画像のサイズを指定する必要があります。
ただ、ここで画像メタ情報を別途提供するのはナンセンスなのでASTCフォーマットに含まれるメタ情報からASTCのブロックサイズと画像サイズを取得します。

## ASTCのフォーマットを読む

調べたところASTCは以下のようなフォーマットになっているようです。

|格納バイト|格納情報|
|---|---|
|0~3 byte|マジックナンバー `0x5CA1AB13`
|4 byte|xのブロックサイズ。(6x6)の場合は`0x6`
|5 byte|yのブロックサイズ。(6x6)の場合は`0x6`
|6 byte|zのブロックサイズ。2D画像では常にここは`0x1`
|7~9 byte|画像のxサイズ。(1920x1080)なら`0x000780`
|10~12 byte|画像のyサイズ。(1920x1080)なら`0x000438`
|13~15 byte|画像のzサイズ。2D画像なら常にここは`0x000001`

なので1920x1080画像のブロックサイズ6x6でASTCにエンコードするとヘッダは以下のようになります
（バイトオーダーはリトルエンディアン
`13ABA15C 06060180 07003804 00010000`
zに関する情報はちょっとわからなかったですが、基本的に1になるようなのであまり気にしなくて良いようです。

このメタ情報を使って`Texture2D.LoadRawTextureData`でロードします

# 実装

```c#

using System;
using System.IO;
using System.Linq;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using Unity.IO.LowLevel.Unsafe;
using UnityEngine;
using UnityEngine.UI;

public unsafe class AsyncLoader : MonoBehaviour
{
    const int AstcBlockInfoSize = 3;
    const int AstcFullHeaderSize = 16;
    static readonly byte[] AstcHeaderMagicNumber = { 0x5C, 0xA1, 0xAB, 0x13 };

    NativeArray<ReadCommand> commands;
    ReadHandle readHandle;
    Action callback;

    void Start()
    {
        // astcファイルを生成した際のコマンド
        // ./astcenc -c sample.png sample.astc 6x6 -medium
        var filePath = Path.Combine(Application.dataPath, "StreamingAssets/Sign_Okay.astc");
        var fileSize = new FileInfo(filePath).Length;
        commands = new NativeArray<ReadCommand>(1, Allocator.Persistent)
        {
            [0] = new ReadCommand
            {
                Offset = 0,
                Size = fileSize,
                Buffer = (byte*)UnsafeUtility.Malloc(fileSize, UnsafeUtility.AlignOf<byte>(), Allocator.Persistent),
            },
        };
        readHandle = AsyncReadManager.Read(filePath, (ReadCommand*)commands.GetUnsafePtr(), 1);
        callback = () =>
        {
            var buffer = (byte*)commands[0].Buffer;
            if (!IsValid(buffer))
            {
                Dispose();
                throw new NotSupportedException("ASTCファイル内のヘッダが正しくありません");
            }

            var (width, height) = GetTexSize(buffer);
            buffer += AstcFullHeaderSize;
            var textureDataSize = fileSize - AstcFullHeaderSize;
            var tex = new Texture2D(width, height, TextureFormat.ASTC_6x6, false);
            tex.LoadRawTextureData((IntPtr)buffer, (int)textureDataSize);
            tex.Apply();

            GetComponent<RawImage>().texture = tex;
            Dispose();
        };
    }

    void Update()
    {
        if (readHandle.IsValid() && readHandle.Status == ReadStatus.Complete)
        {
            callback?.Invoke();
            callback = null;
        }
    }

    void OnDestroy()
    {
        Dispose();
    }

    static bool IsValid(byte* buffer)
    {
        var isAstc = true;
        var magicNumber =  BitConverter.IsLittleEndian ? AstcHeaderMagicNumber.Reverse() : AstcHeaderMagicNumber;
        foreach (var b in magicNumber)
        {
            isAstc &= *buffer == b;
            if (!isAstc)
            {
                return false;
            }

            buffer++;
        }
        return true;
    }

    static (int Width, int Height) GetTexSize(byte* buffer)
    {
        buffer += AstcHeaderMagicNumber.Length + AstcBlockInfoSize;
        if (BitConverter.IsLittleEndian)
        {
            var width = *buffer++ | *buffer++ << 8 | *buffer++ << 16;
            var height = *buffer++ | *buffer++ << 8 | *buffer << 16;
            return (width, height);
        }
        else
        {
            var width = *buffer++ << 16 | *buffer++ << 8 | *buffer++;
            var height = *buffer++ << 16 | *buffer++ << 8 | *buffer;
            return (width, height);
        }
    }

    void Dispose()
    {
        readHandle.Dispose();
        UnsafeUtility.Free(commands[0].Buffer, Allocator.Persistent);
        commands.Dispose();
    }
}
```

今回は手抜き工事で`ASTC_6x6`フォーマットでハードコーディングしていますが
もちろんメタ情報を使ってブロックサイズを取得することで動的にフォーマットを指定することも可能です。

## 結果

<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30346/0fba3527-295b-a804-2a45-db280013898c.png">

## 実装ポイント
実装ポイントとしては2点です

### NativeArrayの解放を必ず忘れない
これはNativrArrayを使う実装すべてにいえることですが、必ず解放を忘れないようにしましょう。
解放を忘れるとリークしてクラッシュします。

### `LoadRawTextureData`ではASTCヘッダ部のメタ情報は不要
ここはハマったポイントなのですが`LoadRawTextureData`を使う場合ASTCのヘッダ情報は不要ですのでヘッダの16バイト分を除いて`LoadRawTextureData`に渡すようにしましょう。
ヘッダを除かずに渡した場合はヘッダ情報もボディと解釈され、その部分にノイズが表示されてしまいます。

<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30346/e752fc4b-4eef-8181-6839-ff004c207d79.png">

## 注意事項
PCではASTCに対応してないGPUが多いです。
そのためUnityでもStandalone環境下ではASTCはCPUデコードとなっていますので、PC向けでは使うメリットがないこととパフォーマンス計測時にはご注意ください。

# 最後に
個人的には依存関係がないかつ2Dテクスチャについてはこの方法を使うのは効果的なのでオススメです。
対応端末がASTC対応端末に限定されてしまうのがデメリットですが、かなり古い端末をサポートしない限りはASTCに対応しているので、新規プロジェクトで導入する分にはまず問題にならないかと思います。

またNativeArrayがこういった低レベルAPIを提供してくれるのは嬉しい限りですね。
DOTSも近いうちにアニメーション対応がなされ、いよいよ現場での利用が現実的になってきそうなのでメモリレイアウトとかちゃんと考えていきたい！

# 参考
https://community.arm.com/developer/tools-software/graphics/f/discussions/2252/astc-evaluation-codec