Archives

February 2011 の記事

BC6H/BC7/BPTC は GPU が対応しているテクスチャ圧縮の進化系です。
DXT よりも柔軟な bit 配置が可能で圧縮画質が向上しています。(前回)
その block 構造を調べてみました。


参考にした資料は下記の 2つです。

ARB_texture_compression_bptc
BC6HBC7EncoderDecoder11 Sample

基本的な考え方は S3TC(DXT) と同じです。
S3TC の場合 4x4 block 単位で基準となる 2つのカラー値を保持します。
この 2色を補間し 3 または 4 階調のカラーをパレットを生成します。
4x4 block の各 pixel は 2bit の index を持っており、生成されたパレットから
1色を選択します。

BPTC/BC6H/BC7 では格納されているカラーが endpoint、補間に用いる 2色のペアが
subset と呼ばれています。さらにさまざまな工夫が施されています。


●領域分割

4x4 block を複数の領域に分割して、領域ごとに補間カラー (subset) を持つことが
できます。分割数は BC6H で 2領域まで、BC7 では 3領域まで可能です。

例えば BC7 で 3領域存在している場合、それぞれが 2つのカラー (endpoint)
持っているため独立した RGB 値を 6色分保持していることになります。


・領域0 = color0 color1 の補間 から 1色選択
・領域1 = color2 color3 の補間 から 1色選択
・領域2 = color4 color5 の補間 から 1色選択

分割方法は自由に選べるわけではなく、プリセットされた分割パターンから選択
することになります。


●P-bit

BC7 では格納されているカラー値の精度を稼ぐために RGB で共有された追加ビットを
加えることが出来ます。

例えば RGB それぞれ 5bit で格納されている場合、下記のように最下位に P-bit
が追加されて 6bit になります。

     H----L
  R= RRRRRP
  G= GGGGGP
  B= BBBBBP

(この例の場合 SHARP X68000 の 16bit mode VRAM 構造 555I と同じです。)


●分割パターン

4x4 block を複数の領域 (partition) に分割して、それぞれ異なるカラー値
(endpoint) を参照することが出来ます。
BC7 の場合 2分割で 64通り、3分割で 64通りの 128種類存在します。

・BC7
 分割なし
 2分割、64通りから選択
 3分割、16通りから選択
 3分割、64通りから選択

・BC6H
 分割なし
 2分割、32通りから選択


●インデックス長

インデックス長は 2bit, 3bit, 4bit の 3通りあり、それぞれカラー補間によって
パレットが 4階調、8階調、16階調に展開されます。
インデックスサイズはモードによって異なります。

圧縮のために、一部のピクセルでは全階調から選択できないことがあります。
例えば BC7 mode 0 は 3bit index なので 8階調ありますが、先頭 pixel の
インデックスは 2bit しかなく前半の 4階調しか選択できません。


●独立インデックス

BC7 の Alpha 付きモードでは DXT3/DXT5 のように 16個の index を 2セット
保持することが可能です。共有するモードもあります。

・カラーとアルファのインデックスを共有
  それぞれ 16個の index があり独立している。合計 32個

・カラーとアルファのインデックスを共有
  16個のインデックスのみ


●独立インデックスの入れ替え

BC7 の Alpha 独立インデックスでは RGB が共有されます。
rotation モード指定により、特別扱いするチャンネル (component) を
Alpha 以外にも設定できます。

例えば BC7 mode 4 で rotation = 1 の場合 BGA が共有され、R のみ独立した
専用の index 参照が可能となります。


●BC7 のモード

選択可能な分割パターン、格納するカラー(endpoint) の bit サイズ等、画像に
合わせていくつかの組み合わせから選択することが出来ます。

BC7 では下記の 8モードが定義されています。4x4 block 単位で任意のモードを
選択することが可能です。

BC7 mode bit
mode0 =        1
mode1 =       10
mode2 =      100
mode3 =     1000
mode4 =    10000
mode5 =   100000
mode6 =  1000000
mode7 = 10000000

モード符号化の構造的上、モード数を拡張することが可能ですがデータの格納
効率は落ちます。


●BC7 の各モードの詳細

BC7 はモードごとに異なる複雑な bit 配置を取りますが、データは順番に詰めて
格納されるため BC6H のように飛び飛びの bit を拾い集める必要がありません。

BC7 mode 0
   3分割 16通り
      endpoint:  RGB 444 x6色 (subset x3) + P-bit
      index:     3bit (8段階)

    1bit    mode 指定
    4bit    partition 指定、16種類
    24bit   R 4bit x 6
    24bit   G 4bit x 6
    24bit   B 4bit x 6
    6bit    P-bit 1bit x 6
    45bit   index (3bit x13 + 2bit x3)


BC7 mode1
   2分割 64通り
      endpoint:  RGB 666 x4色 (subset x2) + P-bit (shared)
      index:     3bit (8段階)

    2bit    mode 指定
    6bit    partition 指定、64種類
    24bit   R 6bit x 4
    24bit   G 6bit x 4
    24bit   B 6bit x 4
    1bit    shared P-bit
    47bit   index (3bit x14 + 2bit x1)


BC7 mode2
   3分割 64通り
      endpoint:  RGB 555 x6色 (subset x3)
      index:     2bit (4段階)

    3bit    mode 指定
    6bit    partition 指定、64種類
    30bit   R 5bit x 6
    30bit   G 5bit x 6
    30bit   B 5bit x 6
    29bit   index (2bit x13 + 1bit x3)


BC7 mode3
   2分割 64通り
      endpoint:  RGB 777 x4色 (subset x2) + P-bit
      index:     2bit (4段階)

    4bit    mode 指定
    6bit    partition 指定、64種類
    28bit   R 7bit x 4
    28bit   G 7bit x 4
    28bit   B 7bit x 4
    4bit    P-bit 1bit x 4
    30bit   index (2bit x14 + 1bit x2)


BC7 mode4
   分割なし
      endpoint:  RGBA 5556 x2色 (subset x1)
      index:     2bit (4段階), 3bit (8階調)

    5bit    mode 指定
    2bit    rotation 指定 (A→RGB 入れ替え)
    1bit    Index 選択 (color 4段 + alpha 8段 or color8段 + alpha 4段)
    10bit   R 5bit x 2
    10bit   G 5bit x 2
    10bit   B 5bit x 2
    12bit   A 6bit x 2
    31bit   index (2bit x15 + 1bit x1)
    47bit   index (3bit x14 + 2bit x1)


BC7 mode5
   分割なし
      endpoint:  RGBA 7778 x2色 (subset x1)
      index:     2bit (4段階) x2

    6bit    mode 指定
    2bit    rotation 指定 (A→RGB 入れ替え)
    14bit   R 7bit x 2
    14bit   G 7bit x 2
    14bit   B 7bit x 2
    16bit   A 8bit x 2
    31bit   index (2bit x15 + 1bit x1)
    31bit   index (2bit x15 + 1bit x1)


BC7 mode6
   分割なし
      endpoint:  RGBA 7777 x2色 (subset x1) + P-bit
      index:     4bit (16段階)

    7bit    mode 指定
    14bit   R 7bit x 2
    14bit   G 7bit x 2
    14bit   B 7bit x 2
    14bit   A 7bit x 2
    2bit    P-bit 1bit x 2
    63bit   index (4bit x15 + 3bit x1)


BC7 mode7
   2分割 64通り
      endpoint:  RGBA 5555 x4色 (subset x2) + P-bit
      index:     2bit (4段階)

    8bit    mode 指定
    20bit   R 5bit x 4
    20bit   G 5bit x 4
    20bit   B 5bit x 4
    20bit   A 5bit x 4
    4bit    P-bit 1bit x 4
    30bit   index (2bit x14 + 1bit x2)

続きます。


関連エントリ
OpenGL の圧縮テクスチャ (3) BPTC, BC6H/BC7
OpenGL の圧縮テクスチャ (2) 法線圧縮
Android OpenGL ES 2.0 の圧縮テクスチャ


DXT1/DXT3/DXT5/ETC1 等の圧縮テクスチャは、カラー値 RGB をピクセル当たり
4bit (4bpp) に変換しています。
DXT3/5 など Alpha 付きの場合は 8bpp ですがカラー成分は 4bpp のままです。
Alpha チャンネル単独で 4bpp 分追加しています。

この垣根を無くして 8bpp 分をまるごとカラー情報に割り当てられれば、
より高品位な画像圧縮ができると考えられます。

単純に DXT1 相当と考えても、ちょうどテクスチャ 2枚分の情報を格納することが
できるわけです。4x4 エリアを 2分割してそれぞれに DXT1 相当の画素を入れたり、
保持しているベースカラーの精度を上げる事も考えられます。

Direct3D 11 で追加されたテクスチャフォーマット BC6H/BC7 はこのように
8bpp (4x4 block 128bit) を自由にリフォーマットして多くの情報を詰め込みます。

調べてみると、単に情報を増やすだけでなく画像に合わせてさまざまなモードを
選択できる自由度の高さが強みのようです。
データの格納方法もさまざまで、ベース値の精度も複数選択できます。
やはり 4x4 block は分割可能のようです。ただし ETC1 のような 4x2 ではなく
プリセットされた 32/64 通りから選べます。
その分ビット単位で情報の配置が変化するため非常に複雑な構造になっています。

・BC6H HDR 対応。16bit float に拡張
・BC7 SDR で解像度を優先

OpenGL にも BPTC と呼ばれる BC6H/BC7 相当の圧縮フォーマットが追加されています。
RADEON HD 5850 / GeForce GTX 460 で対応していることが確認できました。

DXGI(DirectX)                OpenGL
-----------------------------------------------------------------------
DXGI_FORMAT_BC6H_UF16        GL_COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT_ARB
DXGI_FORMAT_BC6H_SF16        GL_COMPRESSED_RGB_BPTC_SIGNED_FLOAT_ARB
DXGI_FORMAT_BC7_UNORM        GL_COMPRESSED_RGBA_BPTC_UNORM_ARB
DXGI_FORMAT_BC7_UNORM_SRGB   GL_COMPRESSED_SRGB_ALPHA_BPTC_UNORM_ARB

実際に Direct3D 11 SDK のコンバータ texconvex で変換した bc6h_uf16/sf16/bc7
画像を OpenGL 4.1 で描画できています。
S3TC(BC1/2/3) や 3DC(BC4/5) と同じようにバイナリで互換性が保たれているようです。

OpenGL や OpenGL ES 2.0 向け GPU で使える圧縮テクスチャの種類と対応をこちらにまとめました。

OpenGL/OpenGL ES 2.0 テクスチャフォーマットまとめ


関連エントリ
OpenGL の圧縮テクスチャ (2) 法線圧縮
Android OpenGL ES 2.0 の圧縮テクスチャ


ノーマルマップの圧縮フォーマットは 3DC が有名です。
Tangent Space の法線マップの場合 z の符号が不要なので (x,y,z) のうち
x,y のみ保存しておけば z= sqrt( 1- (x*x + y*y) ) で値が求まります。

上記の通りシェーダー側に多少追加コードが必要となるものの、RGB8 24bpp
ではなく LA8/RG8 など 16bpp の画像に保存することができます。

さらに DXT5 (8bpp) の color, alpha を独立した 2チャンネルとみなして
法線圧縮する手法 (DXT5n) があります。その応用で DXT5 の alpha 圧縮の構造を
用いて独立 2チャンネルを格納するのが 3DC です。
同じように 1 チャンネルの 4bpp フォーマットもあります。

Block Compression (Direct3D 10)

最近 OpenGL の GPU で見かける latc, rgtc も同様の構造を持った 1,2 チャンネル
圧縮フォーマットのようです。
3DC が Direct3D 10 以降 DXGI で BC4, BC5 と呼ばれているのと同様に、
OpenGL で定義された呼び方だと思われます。

ATI            DX9      DX10/DXGI   OpenGL ES    OpenGL
------------------------------------------------------------
3DC_X  4bpp    ATI1     BC4         LATC1        LATC1/RGTC1
3DC_XY 8bpp    ATI2     BC5         LATC2        LATC1/RGTC2


DX10/DXGI             FourCC  OpenGL
-----------------------------------------------------------------------------
DXGI_FORMAT_BC4_UNORM "BC4U"  GL_COMPRESSED_RED_RGTC1_EXT              0x8dbb
DXGI_FORMAT_BC4_SNORM "BC4S"  GL_COMPRESSED_SIGNED_RED_RGTC1_EXT       0x8dbc
DXGI_FORMAT_BC5_UNORM "ATI2"  GL_COMPRESSED_RED_GREEN_RGTC2_EXT        0x8dbd
DXGI_FORMAT_BC5_SNORM "BC5S"  GL_COMPRESSED_SIGNED_RED_GREEN_RGTC2_EXT 0x8dbe

LATC と RGTC の違いはチャンネルへの展開方法だけです。
RGTC は 3DC や BC5 と互換性があります。
その代わり LATC は DXT5n とシェーダーコードを共有できます。

vec4  tex_normal= texture2D( NormalMap, otexcoord );

// RGTC unsigned, 3DC_XY, RG8
tex_normal.xy= tex_normal.xy * 2.0 - 1.0;
tex_normal.z= sqrt( 1.0- dot( tex_normal.xy, tex_normal.xy ) );

// LATC unsigned, DXT5n, LA8
tex_normal.xy= tex_normal.yw * 2.0 - 1.0;
tex_normal.z= sqrt( 1.0- dot( tex_normal.xy, tex_normal.xy ) );

RADEON (GL4.1) の場合いくつか注意点があります。

glGetIntegerv( GL_COMPRESSED_TEXTURE_FORMATS, lists )
が列挙を返してきません。代わりに Extension String の
glGetStringi( GL_EXTENSIONS, i ) で判定する必要があります。
また GL_ATI_texture_compression_3dc があっても OpenGL ES 2.0 と違い
rgtc の方を使います。
つまり GL_3DC_XY ではなく GL_COMPRESSED_RED_GREEN_RGTC2_EXT 。

// GL_EXT_texture_compression_latc
GL_COMPRESSED_LUMINANCE_LATC1_EXT              0x8C70
GL_COMPRESSED_SIGNED_LUMINANCE_LATC1_EXT       0x8C71
GL_COMPRESSED_LUMINANCE_ALPHA_LATC2_EXT        0x8C72
GL_COMPRESSED_SIGNED_LUMINANCE_ALPHA_LATC2_EXT 0x8C73

// GL_EXT_texture_compression_rgtc
GL_COMPRESSED_RED_RGTC1_EXT                    0x8DBB
GL_COMPRESSED_SIGNED_RED_RGTC1_EXT             0x8DBC
GL_COMPRESSED_RED_GREEN_RGTC2_EXT              0x8DBD
GL_COMPRESSED_SIGNED_RED_GREEN_RGTC2_EXT       0x8DBE

OpenGL ES 2.0 GPU の場合 Adreno (ATI系) が 3DC、Tegra2 が LATC に対応
しています。
利用可能なカラー圧縮、法線圧縮フォーマットをまとめると下記の通りです。

          COLOR  COLOR-EA  COLOR-IA  NORMAL   NORMAL-S  1ch
--------------------------------------------------------------
PowerVR   PVRTC  --        PVRTC-A   --       --        --
Adreno    ATC    ATC-A     ATC-I     3DC_XY   --        3DC_X
Tegra     DXT1   DXT3      DXT5      LATC2    LATC2-S   LATC1
ZMS       DXT1   DXT3      DXT5      (DXT5n)  --        --
Mali      ETC1   --        --        --       --        --

対応していない GPU は非圧縮 LA8 で代用しなければなりません。
圧縮に対応していても RG(3DC) と LA でシェーダーを完全に共有するのは
難しそうです。

関連エントリ
Android OpenGL ES 2.0 の圧縮テクスチャ


OpenGL ES 2.0 では使用可能な圧縮テクスチャ形式が GPU 毎に異なっています。
カラー向けのフォーマットは現在大きく分けて下記の 4 通りあります。

(1) PVRTC 系
(2) ATITC 系
(3) S3TC(DXT) 系
(4) ETC 系

iOS
  PowerVR SGX           PVRTC

Android
  Qualcomm Adreno       ETC1, ATITC, 3DC, PALETTE
  ATI Imageon           ETC1, ATITC, 3DC, PALETTE
  Imagination PowerVR   ETC1, PVRTC
  NVIDIA Tegra2         ETC1, S3TC(DXT), LATC
  ZiiLabs ZMS-08 HD     ETC1, S3TC(DXT), PALETTE


●(1) PVRTC 系

PowerVR SGX 系各種。
iOS 系の全機種で使われているため対応ハードはかなり多いはずです。
Android の場合は Galaxy S/Tab の Samsung S5PC110 等。
フォーマットは下記の 4通り。

PVRTC  RGB   4bpp
PVRTC  RGBA  4bpp
PVRTC  RGB   2bpp
PVRTC  RGBA  2bpp

PVRTC は 2bpp/4bpp 選択可能で alpha チャンネルを含めることができるため
同一解像度なら他のフォーマットよりも少ない容量に変換されます。

反面、正方形しばりがあります。元画像が正方形でない場合は引き伸ばすことに
なるため逆に容量が大きくなるケースがあります。
2倍までのスケーリングなら 2bpp に落とすことでほぼ同等の画質と容量を維持
することができると思われます。


・元画像が 256x256 なら 4bpp に変換
・元画像が 128x256 なら 256x256 に拡大+ 2bpp に変換

変換ツールは開発者サイト PowerVR Insder にあります。

Imagination PowerVR Insider

テクスチャは専用の pvr 形式で保存しますが、pvr 形式自体は ETC,DXT 等
さまざまなフォーマットを格納可能です。


●(2) ATITC 系

ATI の Imageon 及び、Qualcomm の Snapdragon 等に搭載されている Adreno
シリーズで対応しています。
Android に限定すると対応端末はかなり多いと思われます。

             FourCC  bpp
ATITC  RGB   'ATC '  4bpp
ATITC  RGBA  'ATCA'  8bpp Explicit Alpha
ATITC  RGBA  'ATCI'  8bpp Interpolated Alpha

4bpp 時に 1bit alpha を含められない以外は圧縮率も機能共にほぼ
DXT1/DXT3/DXT5 と同等になっているようです。画質に関しては未調査。

圧縮ツールは AMD(ATI) 製のものがあります。
このツールは ATC や ETC を DDS 形式で保存します。

AMD The Compressonator

Adreno と改名した後 Qualcomm も開発者向けページを用意しています。

Adreno Graphics

ATITC 系 GPU は法線などカラー以外に応用可能な 3DC (BC4/5) も対応している
のが特徴です。


●(3) S3TC/DXT 系

NVIDIA Tegra2, 3DLabs ZMS-08 HD, PC 向け GPU 各種。
DirectX の標準フォーマットであり、OpenGL でも多くの PC 向け GPU が
対応しています。
とにかく対応しているツール数が多く、変換や扱いで困ることがありません。

             FourCC  bpp   dx10
S3TC   RGB   'DXT1'  4bpp  BC1
S3TC   RGBA  'DXT1'  4bpp  BC1 (1bit alpha)
S3TC   RGBA  'DXT3'  8bpp  BC2
S3TC   RGBA  'DXT5'  8bpp  BC3

DXT1 は 1bit の alpha 値を含めることが出来ますが、OpenGL の場合この
両者を区別しています。厳密には 4x4 pixel の block 単位で切り替えられる
ため DirectX では区別がありません。
DXT1/DXT3/DXT5 は Direct3D 10 以降 BC1/BC2/BC3 と呼ばれます。


●(4) ETC 系

Android の場合すべての GPU で利用できます。
ARM Mali など ETC のみ対応している GPU もあります。

iOS では PowerVR 一つなので問題ありませんが、Android では GPU 毎に
互換性のない様々な圧縮テクスチャ形式を持っています。
Android で唯一の共通フォーマットとして利用出来るのが ETC1 圧縮です。
Froyo 2.2 以降 ETC1 専用の API も用意されています。
iOS では使えませんが Android 上では PowerVR も ETC1 が使えます。

             FourCC  bpp
ETC    RGB   'ETC '  4bpp

ETC1 の圧縮は比較的多くのツールで対応しています。
例えば ATITC で紹介した AMD The Compressonator も ETC 変換可能で DDS として
保存できます。
ただし ETC 圧縮時に画質(変換速度)を指定できず、プレビューで極端に劣化した
画像が表示されることがあります。PVR 用の PVRTexTool ツールで読み込ませると
正しく表示されるため、The Compressonator だと ETC 画像の読み込み部に
何らかの問題がありそうです。

Ericsson 本家及び Mali のツールがあります。

Ericsson Texture Compression
Mali Developer Center


●ファイルフォーマット

     PVRTC ATITC S3TC  ETC  拡張
----------------------------------
DDS   --    ◯    ◎    ◯  FourCC
PVR   ◎    --    ◯    ◯
KTX   ◯    ◯    ◯    ◯  GL_*
PKM   --    --    --    ◎


●DDS

DirectX 標準フォーマットです。mipmap, cubemap, volume, array など各種形式に
対応可能で、古くから使われているためツール類が揃っています。
特殊なフォーマットは FourCC で識別しているので、4文字のコードが決まれば
新しい圧縮形式でも対応できます。

DDS Texture format memo


●PVR

PVRTC だけでなく DXT/ETC などさまざまな画像フォーマットに対応しているようです。
ATITC のファイルタイプは定義されていないので格納できません。
比較的ツールも揃っているようです。


●KTX

OpenGL / OpenGL ES 向けに新たに作られたフォーマットのようです。
PVRTexTool が対応しています。
GL のシンボル値をそのまま格納するので GL API と相性が良いのが特徴。
OpenGL で数値が定義されていれば、新しい圧縮形式でもそのまま保存する
ことが出来ます。

Khronos KTX File Format Specification

画像格納時の優先順が DDS と逆で、3d/cube/array を持ったデータが mipmap 個
並ぶ順番になっています。
ツールでは画像の上下など GL の定義に厳密に変換される可能性があるようです。
メタデータ KTXorientation を調べることで uv の向きがわかります。

ヘッダが Bigendian/Littleendian 両対応しているなど、ターゲットハードに
最適化した実行時向けのフォーマットと思われます。


●PKM

ETC1 (PACKMAN) のための専用フォーマットのようです。
Android の ETC1 用 API が対応しています。


●Android とテクスチャ

効率を考えると GPU Native なフォーマットの方が有利ですが管理が大変です。
それでも速度最優先なら全部持つしかなさそうです。

1. alpha 不要なら ETC1
2. alpha が必要で速度優先なら PVRTC,ATITC,S3TC,非圧縮 全部



Android OS 2.3、API Level 9 から NativeActivity が利用出来るようになります。
画面の初期化やイベントの取得も Java コードを通らずに C/C++ だけで
記述することが可能です。

とは言え実際にアプリケーションを記述する手段はこれまでとほとんど変わりません。
jni フォルダに C/C++ コードを置いて ndk-build を使います。
生成されるバイナリも従来同様 Dynamic Link Library (dll) ~.so です。

・今までの NDK (jni)
  Java 上で dll をロードし、interface class を経由して呼び出し

・NativeActivity
  システムが直接 dll をロードし dll 内のエントリポイントを直接呼び出す


●エントリポイントの指定

最初に呼ばれる関数名を AndroidManifest.xml に記述します。

・Java コードを含まないことを宣言
  <application> のアトリビュートに android:hasCode="false"

・dll 名の指定
  <activity> 宣言内に <meta-data> で指定する。
  例えば libappmain.so の場合

<meta-data android:name="android.app.lib_name" android:value="appmain" />

・エントリポイント名の指定
  例えば関数 Main_onCreate() から開始する場合

<meta-data android:name="android.app.func_name" android:value="Main_onCreate" />

これで Activity の起動時に libappmain.so 内の Main_onCreate() が
直接呼び出されるようになります。


●イベント

Java と全く同様に NativeActivity も onStart, onResume, onPause, onStop,
onDestroy, onRestart 等のイベントが呼ばれます。
あらかじめイベントごとに Callback 関数を登録しておく必要があります。

onCreate のみ特別で、Callback ではなく Activity 起動時のエントリ関数として
AndroidManifest.xml に記述します。
AndroidManifest.xml に func_name の記述がない場合は関数名
ANativeActivity_onCreate() とみなします。
この関数の実体はアプリケーション側で記述しておく必要があります。

(1) onCreate で AndroidManifest.xml に記述した関数が呼ばれる
(2) onCreate 内部で他のイベントの Callback 関数を登録する
(3) 各イベントで Callback 関数が呼ばれる


●android_native_app_glue

ndk の sample/native-activity や NativeActivity の Reference に
掲載されてるコードは android_native_app_glue を使っています。
これはサンプルに含まれる Utility Library です。
内部では下記の動作を行っています。

1. 各イベントごとの Callback 関数を登録する
2. NativeActivity 用のスレッドを生成する
3. イベントを格納するキューを作る
4. ユーザー定義関数 android_main() をスレッドから呼び出す

つまり、これにより android_main をイベント待ちのループとして記述できる
ようになります。Win32 API とかでよくあるスタイルです。


●static 領域の初期化

NativeActivity でも従来の jni 経由の NDK (dll) 呼び出しと全く同じです。
Activity が onDestroy で破棄されても dll 自体はしばらくメモリ上に
常駐する可能性があります。
この場合再び onCreate しても static 変数の値が初期化されずに以前の値を
残しています。

onCreate 時に自分で初期化するか前回試したような dll の分離が必要となります。


参考ページ
NativeActivity

関連エントリ
Android NDK の初期化と dll の分離
Android アプリケーションとプロセス


NDK では C/C++ のコードをそのまま使うことが出来ます。
Windows 上で作っていた lib やアプリケーションを移植し Android 上で
走らせています。
実際に動作するのですが少々問題があって、正しく起動する場合もあれば
アイコンをタッチした瞬間に落ちる場合があります。

Activity が終了しても Process イメージは残り、再び onCreate した時に
常駐している Native コードが再利用される可能性があるからです。
bss/data セグメントが初期化されていないことが原因でしょう。
static 変数は初期値に戻らず global オブジェクトのコンストラクタも走りません。

NVIDIA の資料でアプリ本体を別の dll に分離する手法が紹介されていたので
試してみました。

GameSauce 2010: Fast and Pretty: Making Responsive, Quality 3D Content on Android

Java Application からロードされるダミー (Thunk) dll を作り、その中で
自分でアプリ本体の dll を load/unload します。
確実に Unload できる点がポイントです。

NDK で複数の Dynamic Link Library (~.so) を build するには下記のようにします。

# Andriod.mak

LOCAL_PATH:= $(call my-dir)

#--------------------------------------
include $(CLEAR_VARS)

LOCAL_SRC_FILES := android_main.cpp ~
LOCAL_MODULE    := libappmain
LOCAL_CFLAGS    :=
LOCAL_C_INCLUDES:=
LOCAL_LDLIBS    := -llog -lGLESv2

include $(BUILD_SHARED_LIBRARY)
#--------------------------------------

#--------------------------------------
include $(CLEAR_VARS)

LOCAL_SRC_FILES := jniproxy.cpp
LOCAL_MODULE    := libjniproxy
LOCAL_CFLAGS    :=
LOCAL_LDLIBS    := -llog

include $(BUILD_SHARED_LIBRARY)
#--------------------------------------

ndk-build で 2つの dll ファイル libappmain.so, libjniproxy.so が作られます。
include $(CLEAR_VARS) は LOCAL_~ の変数を再定義可能にします。

Java 側でロードする dll は libjniproxy.so だけです。

// appmain.java

package jp.flatlib.appmain;
public class appmain {
     static {
         System.loadLibrary( "jniproxy" );
     }
     public static native void init();
     public static native void quit();
     public static native int  render();
     ~
}

jniproxy.cpp は libappmain.so のロード管理や呼び出しを行います。

// jniproxy.cpp

#include    <jni.h>
#include    <android/log.h>
#include    <dlfcn.h>

template<typename T>
void auto_load( T& a, void* ptr )
{
    a= reinterpret_cast<T>( ptr );
}

#define get_proc_0( image, func_name )  auto_load( proc_##func_name,  dlsym( image, #func_name ) )
#define get_proc( func_name )   get_proc_0( LoadApplication, func_name )

//-----------------------------------------------------------------------------
static void JNICALL (*proc_app_init)( JNIEnv* env, jobject obj );
static void JNICALL (*proc_app_quit)( JNIEnv* env, jobject obj );
static jint JNICALL (*proc_app_render)( JNIEnv* env, jobject obj );
~
//-----------------------------------------------------------------------------

static void*   LoadApplication= NULL;

static void InitializeApplication()
{
    // lib 読み込み
    LoadApplication= dlopen( "/data/data/jp.flatlib.appmain/lib/libappmain.so", RTLD_LAZY );
    if( !LoadApplication ){
    	~ error
    }

    // API 取り出し
    get_proc( app_init );
    get_proc( app_quit );
    get_proc( app_render );
    ~
}


static void FinalizeApplication()
{
    if( LoadApplication ){
        dlclose( LoadApplication );
        LoadApplication= NULL;
    }
}


extern "C" {
//-----------------------------------------------------------------------------

JNIEXPORT void JNICALL Java_jp_flatlib_appmain_appmain_init( JNIEnv* env, jobject obj )
{
    if( !LoadApplication ){
        InitializeApplication();
        proc_app_init( env, obj );
    }
}

JNIEXPORT void JNICALL Java_jp_flatlib_appmain_appmain_quit( JNIEnv* env, jobject obj )
{
    if( LoadApplication ){
        proc_app_quit( env, obj );
        FinalizeApplication();
    }
}

JNIEXPORT jint JNICALL Java_jp_flatlib_appmain_appmain_render( JNIEnv* env, jobject obj )
{
    return  LoadApplication ? proc_app_render( env, obj ) : 0;
}

~
//-----------------------------------------------------------------------------
};

元の android_main.cpp では jni 名を app_~ に変更しておきます。

// android_main.cpp
~
extern "C" {

JNIEXPORT void JNICALL app_init( JNIEnv* env, jobject obj )
{
}

JNIEXPORT void JNICALL app_quit( JNIEnv* env, jobject obj )
{
}

JNIEXPORT jint JNICALL app_render( JNIEnv* env, jobject obj )
{
}

~
};

アプリケーション側で dll の読み込みと終了タイミングを指定します。

スレッドの違いに注意が必要です。
例えば GL API は異なるスレッドから呼び出すことが出来ませんが、
GLSurfaceView.Renderer は別スレッドで動作しています。
Activity から呼び出す場合は GLSurfaceView.queueEvent() を使います。

mView.queueEvent( new Runnable(){ public void run(){ appmain.quit(); }  });

現在 Activity の onPause() と GLSurfaceView.Renderer の
onSurfaceChanged() を使っています。

dll の分離はうまくいっており、比較的簡単な追加コードだけで確実に初期化が
行われるようになりました。libjniproxy.so は 4KB 程度。
安定して動いています。

その代わり onPause()/onResume() で unload/load となるので、
端末を Sleep させたり別のアプリに切り替えただけでアプリケーションの
ndk 部は再起動とほぼ同様の状態となります。


関連エントリ
Android アプリケーションとプロセス


Android のアプリケーションは明確な終了をあまり意識させない作りになっています。
それでも使っているうちにだんだん Android の挙動が分かってきます。

・Home ボタンでホームメニューに戻っただけでは終了しない
・Back ボタンで戻る場合はアプリが終了していることが多い

よってアプリケーションの切り替え方法は 2種類あります。

(1) 現在の状態を保ったままバックグラウンドに切り替える方法
(2) 現在の状態を捨てて前の画面(アプリ)に戻る方法

さらにあまり意識していないだけでもう1つあることがわかりました。

(3) 現在の状態を保ったまま別のアプリを呼び出す。

例えばアプリ内から URL をタッチしてブラウザが呼ばれた場合。
また Home ボタン長押しでタスクマネージャーを呼び出し、そこからアプリを
呼び出した場合も同様です。
どちらも Back ボタンを押すと前のアプリの画面に戻ることが出来ます。

(2) はスタックから pop する動作で、ブラウザに例えると [←] ボタン。
(3) はスタックに push する動作で、ブラウザだとページ内のリンクのクリック。
(1) はスタックを切り替えます。ブラウザの新しい Tab を開くようなものです。

厳密にはアプリケーションは Activity という単位に分かれており、
実行したり外部から呼び出されるのはこの Activity です。

アプリケーションがどこかのサイトを示すなら Activity はページ相当でしょうか。
Android は複数のサイト(アプリ)間にまたがって各ページにリンクを貼ることが
可能で、その履歴が保存されます。

この Activity の呼び出しヒストリが保存されている一連のスタックを "Task" と
呼んでいます。やはりブラウザの Tab のように、操作によっては複数の履歴
スタック ("Task") が作られます。

これらのアプリが動作している VM は Linux の Process の上で動いています。


Android はマルチタスクの OS なので複数のアプリケーションが起動します。
動作中のアプリがバックグラウンドに残ってシステムが重くならないよう、
普段から意識して Back ボタンを多用していました。
ブラウザのように明確な Tab 切り替えができて、かつ Tab 単位で Close
ボタンがあればもっと分かりやすくなっていたのではないかと思います。


●複数の終了・中断状態

画面から見えなくなったりバックグラウンドに回るとアプリケーションは停止し
待機状態になります。Activity には下記のイベントが発生します。

・onPause
・onStop
・onDestory

Home ボタン等でバックグラウンドに切り替わった場合は下記の通り。

・onPause → onStop

アプリが切り替わっても部分的に画面が見ている場合は onPause だけです。
Back ボタンで前のアプリに戻る場合はさらに onDestory も呼ばれて終了します。

・onPause → onStop → onDestory

最初ここでわからなかったのが Linux process との関係です。
onDestory で Activity のインスタンスが削除されても Linux から見える
Process は残っています。

RAM 容量に十分な空きがある場合、Process 空間にロードされたプログラム
コードをすぐ削除せずに、キャッシュとして再利用することが目的のようです。

この状態ではメモリは占有していますが停止しているため CPU は消費しません。
メモリが足りなくなると即座に回収されます。

リソース\状態         (A)onPause/onStop   (B)onDestory   (C)未実行
-------------------------------------------------------------------
Activity インスタンス  存在                無し           無し
メモリ空間             専有                専有           無し
CPU リソース           アプリ依存で消費    無し           無し

(B) は Activity が終了しても Process が残っている状態を意味しています。
メモリが足りなくなった場合システムは (B)→(A) の順で回収します。

裏でアプリが動いて重くなるのは (A) 状態のまま多数存在していることが原因と
考えられます。ただしその挙動やリソース消費は厳密にはアプリ依存です。
(B) は CPU を消費しないキャッシュなので、一見 RAM を消費し Process が
残っているように見えますが気にする必要はなさそうです。

アプリを作る上で注意しなければならない点は下記の通り。

・onPause や onStop 状態からも強制終了させられる可能性がある
・onDestory でもプロセス空間は残っていることがある

特に NDK を使っていると 2つ目の点で困ったことが起こります。
続く


参考ページ
Application Fundamentals