月別アーカイブ: 2013年10月

Mac OS X 10.9 Mavericks の OpenGL 4.1 対応と Windows 8.1 Intel HD 4000 Driver

MacOS X が 10.9 Mavericks から OpenGL 4.1 対応になりました。
特筆すべき点は Intel HD 4000 でも OpenGL 4.1 が使えることです。
Windows のドライバはまだ OpenGL 4.0 のままなので逆転が起こっています。

しかしながら Windows のドライバも、Windows 8.1 向けの
15.33.5.64.3316 (10.18.10.3316) では改善されていることがわかりました。

● Mac OS X 10.9

下記の通り 4.1 になっています。

// Mac OS X 10.9 Mavericks

GL_VERSION: 4.1 INTEL-8.18.26
GL_RENDERER: Intel HD Graphics 4000 OpenGL Engine
GL_VENDOR: Intel Inc.
GL_SHADING_LANGUAGE_VERSION: 4.10

実際に試してみましたが、NSOpenGLView の NSOpenGLPFAOpenGLProfile には
シンボルが追加されていませんでした。

以前の記事で書いたように NSOpenGLProfileVersion3_2Core のままで構わないようです。
OpenGL 4.1 の context が作られています。

ちなみに Direct3D11 対応の RADEON でも OpenGL 4.1 までです。

// Mac OS X 10.9 Mavericks

GL_VERSION: 4.1 ATI-1.14.21
GL_RENDERER: AMD Radeon HD 6750M OpenGL Engine
GL_VENDOR: ATI Technologies Inc.
GL_SHADING_LANGUAGE_VERSION: 4.10

Direct3D 10 世代の GPU では OpenGL 3.3 が使えるようになりました。

// Mac OS X 10.9 Mavericks

GL_VERSION: 3.3 NVIDIA-8.18.27 310.40.05f01
GL_RENDERER: NVIDIA GeForce 9400 OpenGL Engine
GL_VENDOR: NVIDIA Corporation
GL_SHADING_LANGUAGE_VERSION: 3.30

まとめると下記の通り。

                 D3D11世代GPU    D3D10世代GPU
---------------------------------------------
Mac OS X 10.8    OpenGL 3.2      OpenGL 3.2
Mac OS X 10.9    OpenGL 4.1      OpenGL 3.3

GPU 毎の詳細は下記のページに載せています。

Desktop GPU Extensions

● Windows 8.1 と Intel HD 4000

Intel HD 4000 の Windows ドライバも Windows 8.1 向けに
15.33.5.64.3316 (10.18.10.3316) がリリースされています。
直前に出た Windows 8.0 用 15.31.17.64.3257 (9.18.10.3257) と比べると
いろいろと機能拡張されていることがわかります。(詳細は下記リンクより)

Intel HD Graphics 4000 OpenGL 4.0 extensions

// Windows 8.0 (2013/09/11)

GL_VERSION: 4.0.0 - Build 9.18.10.3257
GL_RENDERER: Intel(R) HD Graphics 4000
GL_VENDOR: Intel
GL_SHADING_LANGUAGE_VERSION: 4.00 - Build 9.18.10.3257

// Windows 8.1 (2013/10/17)

GL_VERSION: 4.0.0 - Build 10.18.10.3316
GL_RENDERER: Intel(R) HD Graphics 4000
GL_VENDOR: Intel
GL_SHADING_LANGUAGE_VERSION: 4.00 - Build 10.18.10.3316

例えば Atomic Counter や BPTC (Direct3D の BC6H/BC7) texture など
4.2 以上の機能も徐々に取り入れられているようです。

特に GL_ARB_debug_output 対応は、開発者にとっては非常に大きな意味を持つはずです。
glDebugMessageCallbackARB() でメッセージが送られてくることを確認しました。
これで RADEON/GeForce/Intel HD すべての GPU で debug_output が
使えることになります。

もう一点、これまで 64bit bulid で glUseProgram() がクラッシュする
現象に悩まれており、HD 4000 では 64bit バイナリが使えませんでした。
Windows 8.1 x64 + 15.33.5.64.3316 (10.18.10.3316) の組み合わせでは
64bit バイナリがきちんと動作しています。

sRGB framebuffer の問題はまだ完全ではないようです。
Uniform mat3x4 の問題は未検証です。

関連エントリ
Mac OS X で OpenGL の描画 (Xcode5/retina)
OpenGL のエラー判定と OpenGL 4.3 Debug Output

OpenGL ES 2.0 で迷路シェーダー

昔 Direct3D 10 で作成した迷路シェーダーを Mobile GPU に移植してみました。
if 文の塊で非常に複雑な内容となっています。
そのため GPU によってはいくつか問題がありました。

迷路生成 (Nexus 10)
gpumaze 迷路生成

迷路探索 (Nexus 7)
gpudotmaze 迷路探索

このシェーダーでは各ピクセルが状態を持っており、自分の周囲のピクセルを
参照して次の状態を決定します。
例えば壁、床、成長点、移動判定中など、状態遷移の繰り返しで迷路の生成や
探索を行っています。
周囲のサンプリングは上下左右の 4点 + 自分自身を加えた 5回です。

プログラムでは毎フレーム迷路サイズの矩形を一回だけレンダリングしています。
例えば 512×512 なら、262144個のすべてのピクセルに対して上記の
サンプリングと遷移判定を行っていることになります。

そのため原理的にはテクスチャサイズ (迷路サイズ) だけで速度が決定し、
移動点や作業点が増えても (1つでも10万でも) 速度はほぼ変わらないことになります。
(2倍未満の若干の変化はあります。後述)

乱数は CPU で生成したものを毎フレーム GPU に渡しています。
バッファが小さいために固定パターンが目立ってしまうので、
これも GPU で生成できればもっと質の良い物が作れるかもしれません。

下記はさまざまな GPU で走らせた結果です。

              迷路生成 size                 迷路探索 size
              2048  1024   512   256   128  2048  1024   512   256   128
------------------------------------------------------------------------
1.Adreno 320  21.0  59.3  60.0  60.0  60.0  22.3  60.0  60.0  60.0  60.0
2.Mali-T604   11.1  41.3  60.0  60.0  60.0  19.0  58.5  60.0  60.0  60.0
3.Vivant GC4K  3.6  13.9  39.6  60.0  60.0   ---  13.4  41.3  60.0  60.0
4.Mali-400MP4  1.9   7.3  24.3  52.4  60.0   ---  13.8  38.1  60.0  60.0
5.Tegra 3      1.5   5.7  22.3  60.0  60.0   ---   6.0  20.3  43.8  60.0
6.PVR SGX540   0.5   5.2  21.0  60.0  60.0   ---   6.5  23.8  60.0  60.0
7.Adreno 220   ---   3.5  14.1  50.0  60.0   ---   ---  13.0  36.6  60.0※
8.Adreno 200   ---   ---   ---   ---  10.9   ---   ---   ---   ---   6.0※

・数値は FrameRate、値が大きい方が高速
・※ Adreno 220/200 は動作結果に問題あり

表示された fps 値を読み取っただけなので、あまり厳密な測定ではありません。
この中では Adreno 320 が圧倒的な速度となっています。

ほとんどの GPU では生成よりも探索の方が高速でした。
例外は Tegra3 で、探索の方が速度が落ちています。
Adreno 220/200 は一応速度を調べましたが正しく動いておりません。

● 条件分岐と条件付き代入

比較的すぐに動作する GPU と、対応のために修正が必要な GPU がありました。
一番問題だったのは Adreno 220 で、初期のコードではコンパイルが通るものの
Link Error が発生します。

// (A)
vec4 Head( vec4 color )
{
    if( isUp( color ) ){
       ~
       return  vec4( D_RESERVED, color.y, 0.0, 0.0 );
    }
    if( isDown( color ) ){
       ~
       return  vec4( D_RESERVED, color.y, 0.0, 0.0 );
    }
    if( isLeft( color ) ){
       ~
       return  vec4( D_RESERVED, color.y, 0.0, 0.0 );
    }
    if( isRight( color ) ){
       ~
       return  vec4( D_RESERVED, color.y, 0.0, 0.0 );
    }
    ~
    return  color;
}
// (B)
vec4 Head( vec4 color )
{
    if( isUp( color ) ){
       ~
       color= vec4( D_RESERVED, color.y, 0.0, 0.0 );
    }else if( isDown( color ) ){
       ~
       color= vec4( D_RESERVED, color.y, 0.0, 0.0 );
    }else if( isLeft( color ) ){
       ~
       color= vec4( D_RESERVED, color.y, 0.0, 0.0 );
    }else if( isRight( color ) ){
       ~
       color= vec4( D_RESERVED, color.y, 0.0, 0.0 );
    }else{
    	~
    }
    return  color;
}

いろいろ試した結果、(A) のように条件分岐していた命令を、
(B) のように条件代入に置き換えることでエラーを回避出来ました。
コンパイル自体は通っていたので、命令スロットやレジスタなど、何らかの
GPU リソースが Link 時にあふれていた可能性があります。

Adreno 220 でもシェーダーは動作するようになりましたが、
生成結果が意図したものになっておらずまだ問題があります。

             迷路生成                 迷路探索
------------------------------------------------------
Mali-400MP4  壁が切れることがある     正しく動作する
Adreno 220   成長が途中で止まる       ドットが動かない
Adreno 200   正しく動作するが重い     ドットが動かない

Mali-400MP4 は一見正常に見えるものの、壁が途切れている部分があります。
Adreno 200 は Adreno 220 よりまともですが処理能力が足りていません。

また (A) を (B) のように書き換えることで、
他の GPU でもパフォーマンスに影響が生じました。

(C)表              迷路生成
                   2048x2048    1024x1024    512x512   256x256
--------------------------------------------------------------
1.(A) Adreno 320   25.7~16.3   60.0~58.8   60.0         60.0 
1.(B) Adreno 320   18.6~17.7   60.0         60.0         60.0
2.(A) Mali-T604    11.9~10.3   42.8~39.9   60.0         60.0 
2.(B) Mali-T604    12.2~10.5   43.4~42.8   60.0         60.0
3.(A) Vivant GC4K   3.6~       13.9~8.8    46.8~32.5   60.0 
3.(B) Vivant GC4K   3.6~       14.6~9.3    47.6~31.2   60.0
4.(A) Mali-400MP4   1.9~        7.3~       24.6~24.0   52.4 
4.(B) Mali-400MP4   1.9~        7.4~7.3    23.8~23.5   54.0
5.(A) Tegra 3       1.5          5.7         22.3         60.0 
5.(B) Tegra 3       1.8          7.0         27.3         60.0     
6.(A) PVR SGX540    0.5          4.5~5.9    18.4~23.6   60.0
6.(B) PVR SGX540    0.5          4.5~6.0    17.8~23.8   60.0

・数値は FrameRate、値が大きい方が高速

特に Adreno 320 は、(B) に書き換えることで著しく速度が落ちました。
逆に Tegra 3 は (B) の方が高速になっています。
他の GPU でもここまで顕著ではありませんが影響が出ているようです。

予想よりも GLSL は、比較的書いたとおりにそのままコンパイルされて
いるのではないかと考えられます。
その様子は動的分岐の結果からもわかります。

●動的分岐

(C)表の fps 値で「25.7~16.3」と範囲が書かれているものがあります。
これは迷路生成初期が 25.7fps で、全部埋まる頃に 16.3fps まで
落ちていることを意味しています。

考えられる要因としてシェーダーの動的分岐があります。
迷路生成初期はほとんど全部床面の状態なので、レンダリング時は
同一ブロック(WARP)内のピクセルが同じ方向に分岐していると考えられます。
この場合他の分岐状態を実行する必要がありません。

終盤になって複雑になると、ブロック内の各ピクセルの状態がばらばらになるので
シェーダーは可能性のあるすべての分岐コードを実行していると考えられます。

Adreno 320 / Vivante / Mali-T604 などの OpenGL ES 3.0 世代の GPU は
fps 値が変化しているので、分岐の複雑さが速度に影響を与えていると考えられます。

特に Adreno 320 は変化が大きくなっています。
また (A) の分岐コードを (B) の条件代入に置き換えると、fps の変化が
大幅に少なくなることもわかります。
ここからも、記述通りに代入命令に置き換わっているのではないかと考えられます。

Adreno 320 の (B) のコードは最初は遅いものの、分岐判定のコストが
無いためか、複雑な終盤は落ち込みが少なく速度を維持しています。

Tegra 3 / Mali-400MP は速度が一定しておりほとんど変化がありませんでした。
おそらくすべての分岐を通過しているか、またはコンパイラが
分岐命令を置き換えているのではないかと思います。

Tegra 3 は (B) の方が速いので、命令置換ではなくそのまま分岐命令の分
コストが発生していると考えられます。

Adreno 220 はそもそも (B) でなければ動かないので変化が生じていません。
PowerVR SGX540 は理由はわかりませんが、条件が複雑になる終盤の方が
速度が上がっており特徴的な結果となっています。

●その他修正など

Tegra 3 は Fragment Shader で Uniform 配列に対する動的な index による
アクセスができません。
OpenGL ES 2.0 は Unified Shader が多いので忘れていましたが、
もともと Direct3D 9 ShaderModel 3.0 の仕様でも PixelShader では
index が使えませんでした。Tegra 3 の仕様で正解です。

PowerVR SGX540 では (B) の置き換えでシェーダーが動作しなくなる問題がありました。
lowp の誤差が原因で、PowerVR では mediump 以上を使用しています。

安定して動作したのは Adreno 320, Mali-T604, Vivante です。
いずれも OpenGL ES 3.0 対応 GPU です。

●アプリ

移植したシェーダーを Android の Live壁紙 にしてみました。

Google Play: GPU迷路 (作成のみ)
Google Play: GPUドット迷路 (作成+迷路探索)

上記の速度テストは、アプリのプレビュー(画面モード=ズーム)で測定しています。
GPU によっては負荷が高すぎるため 2048×2048 は除外しています。

●テスト機の詳細

   Device         OS   SoC                        GPU
-------------------------------------------------------------------
1. Nexus 7 2013   4.3  Snapdragon S4 Pro APQ8064  Adreno 320
2. Nexus 10       4.3  Exynos 5 Dual (5250)       Mali-T604
3. dtab 01        4.1  K3V2                       Immersion.16 (Vivante)
4. MOMO7 DE       4.1  RK3066                     Mali-400MP4
5. Nexus 7 2012   4.3  Tegra 3                    ULP GeForce(12)
6. Kindle Fire    4.2  OMAP4430                   PowerVR SGX540
7. EVO 3D ISW12HT 4.0  Snapdragon S3 MSM8660      Adreno 220
8. Desire X06HT   4.1  Snapdragon S1 QSD8250      Adreno 220

関連エントリ
GLSL について、互換性や問題点など
2007/09/19 Direct3D 10 ShaderModel4.0 迷路の自動探索Shader
2007/09/18 Direct3D ShaderModel4.0 Shaderで迷路作成

zip ファイルの展開 (zlib で zip にアクセスする (2) )

zip のアクセスはファイル単位なので比較的容易で、
圧縮解凍も zlib を使うことが出来ます。
apk や ipa, jar など様々なところで利用されています。

前回に続き、実際に zip ファイルを展開してみます。

zip ヘッダやファイルの情報はリトルエンディアンで格納されていますが、
アライメントが考慮されていないのでバイトアクセスが必要になります。

今後多用するのでメモリアクセス命令を作っておきます。

typedef unsigned short  UI16;
typedef unsigned int    UI32;

namespace le {
    inline UI32 Load2( const unsigned char* ptr )
    {
        return  (ptr[0]) | (ptr[1]<< 8);
    }
    inline UI32 Load4( const unsigned char* ptr )
    {
        return  (ptr[0]) | (ptr[1]<< 8) | (ptr[2]<<16) | (ptr[3]<<24);
    }
    inline void Store2( unsigned char* ptr, UI32 value )
    {
        ptr[0]= static_cast( value );
        ptr[1]= static_cast( value >> 8);
    }
    inline void Store4( unsigned char* ptr, UI32 value )
    {
        ptr[0]= static_cast( value );
        ptr[1]= static_cast( value >> 8 );
        ptr[2]= static_cast( value >>16 );
        ptr[3]= static_cast( value >>24 );
    }
}

これを用いて、unaligned access 用のデータタイプを定義します。

struct UI16LE {
    unsigned char   Memory[2];
    UI32 Get() const
    {
        return  le::Load2( Memory );
    }
    void Set( UI32 value )
    {
        le::Store2( Memory, value );
    }
};

struct UI32LE {
    unsigned char   Memory[4];
    UI32 Get() const
    {
        return  le::Load4( Memory );
    }
    void Set( UI32 value )
    {
        le::Store4( Memory, value );
    }
};

UI16LE, UI32LE はそれぞれ UI16 (unsigned short), UI32 (unsigned int) と
対になっており、2byte, 4byte の符号なし整数型です。
アライメントを考慮する必要がなく、かつプラットフォーム非依存で
LittleEndian で読み書きします。

これで zip のヘッダ構造を定義できるようになりました。
下記は読み出しに必要な最小限で、32bit ヘッダのみとなっています。

enum {
    ZIP_SIGNATURE_LOCAL32           = 0x04034b50,
    ZIP_SIGNATURE_CENTRALDIR32      = 0x02014b50,
    ZIP_SIGNATURE_CENTRALDIR32_EOR  = 0x06054b50,
};

struct T_ZIP_LOCAL32 {
    UI32LE  Signature;  // PK0304 = 0x04034b50
    UI16LE  Extract;
    UI16LE  BitFlag;
    UI16LE  Method;
    UI16LE  Time;
    UI16LE  Date;
    UI32LE  CRC32;
    UI32LE  CompressedSize;
    UI32LE  UncompressedSize;
    UI16LE  NameLength;
    UI16LE  ExtraLength;
};

struct T_ZIP_CENTRALDIR32 {
    UI32LE  Signature;  // PK0102 = 0x02014b50
    UI16LE  Version;
    UI16LE  Extract;
    UI16LE  BitFlag;
    UI16LE  Method;
    UI16LE  Time;
    UI16LE  Date;
    UI32LE  CRC32;
    UI32LE  CompressedSize;
    UI32LE  UncompressedSize;
    UI16LE  NameLength;
    UI16LE  ExtraLength;
    UI16LE  CommentLength;
    UI16LE  DiskNumberStart;
    UI16LE  InternalAttributes;
    UI32LE  ExternalAttributes;
    UI32LE  LocalHeaderOffset;
public:
    const char* GetNamePointer() const
    {
        return  reinterpret_cast( reinterpret_cast( this ) + sizeof(T_ZIP_CENTRALDIR32) );
    }
    const bool IsDirectory() const
    {
        unsigned int    name_length= NameLength.Get();
        return  name_length && GetNamePointer()[ name_length-1 ] == '/';
    }
    const T_ZIP_CENTRALDIR32* Next() const
    {
        return  reinterpret_cast( reinterpret_cast( this ) + sizeof(T_ZIP_CENTRALDIR32) + NameLength.Get() + ExtraLength.Get() + CommentLength.Get() );
    }
};

struct T_ZIP_CENTRALDIR32_EOR {
    UI32LE  Signature;          // PK0506 = 0x06054b50
    UI16LE  NumberOfThisDisk;
    UI16LE  StartDisk;
    UI16LE  TotalEntriesDisk;
    UI16LE  TotalEntries;
    UI32LE  CentralDirectorySize;
    UI32LE  CentralDirectoryOffset;
    UI16LE  CommentLength;
};

前回の手順通り、まずは EOR (End of Central Directory record) を検索します。

const int   EOR_LOCATOR_SIZE= 512;
unsigned char   Locator[EOR_LOCATOR_SIZE];

file.Seek( -EOR_LOCATOR_SIZE, SEEK_END );
file.Read( Locator, EOR_LOCATOR_SIZE );

const unsigned char*  ptr= Locator;
const unsigned char*  end_ptr= ptr +  EOR_LOCATOR_SIZE - sizeof(T_ZIP_CENTRALDIR32_EOR) + sizeof(UI32LE);

for(; ptr < end_ptr ; ptr++ ){
    if( *ptr == 'P' && le::Load4( ptr ) == ZIP_SIGNATURE_CENTRALDIR32_EOR ){
	// found
    }
}

↑上のコードはファイルの終端から 512byte を読み込んで、EOR の Signature
"PK0506" (0x06054b50) を検索しています。
T_ZIP_CENTRALDIR32_EOR の CommentLength の分だけ追加情報を挿入できるので、
必ずしもこの範囲に存在するとは限りません。
見つからない場合にさらに読み進める処理が必要かもしれません。

EOR がわかれば CentralDirectoryOffset, CentralDirectorySize を使って
Central Directory を読み込むことが出来ます。
ファイル名検索に何度も利用するので、最初にメモリに読み込んでおきます。
EOR 読み込みと合わせて class 化します。

class ZipDirectory {
    unsigned int    DirectoryEntries;
    Buffer          DirectoryImage;
public:
    const T_ZIP_CENTRALDIR32*   Begin() const
    {
        return  DirectoryImage.Address();
    }

    bool IsEnd( const T_ZIP_CENTRALDIR32* dir_ptr ) const
    {
        return  reinterpret_cast(dir_ptr) >= reinterpret_cast(DirectoryImage.Address()) + DirectoryImage.Size();
    }

    static const unsigned char* FindEOR( const unsigned char* ptr, size_t size )
    {
        const unsigned char*    end_ptr= ptr + size - sizeof(T_ZIP_CENTRALDIR32_EOR) + sizeof(UI32LE);
        for(; ptr < end_ptr ; ptr++ ){
            if( *ptr == 'P' && le::Load4( ptr ) == ZIP_SIGNATURE_CENTRALDIR32_EOR ){
                return  ptr;
            }
        }
        return  NULL;
    }

    bool Load( const char* zip_file_name )
    {
        File    file;
        if( !file.Open( zip_file_name ) ){
            return  false;
        }
        const int   EOR_LOCATOR_SIZE= 512;
        unsigned char   Locator[EOR_LOCATOR_SIZE];
        file.Seek( -EOR_LOCATOR_SIZE, SEEK_END );
        size_t  read_size= file.Read( Locator, EOR_LOCATOR_SIZE );

        const unsigned char*    ptr= FindEOR( Locator, read_size );
        if( !ptr ){
            file.Close();
            return  false;
        }
        const T_ZIP_CENTRALDIR32_EOR*   eor= reinterpret_cast( ptr );
        unsigned int    dir_size= eor->CentralDirectorySize.Get();
        DirectoryEntries= eor->TotalEntries.Get();
        DirectoryImage.Alloc( dir_size );

        file.Seek( eor->CentralDirectoryOffset.Get(), SEEK_SET );
        file.Read( DirectoryImage.Address(), dir_size );
        file.Close();
        return  true;
    }
};

これで zip ファイル内のファイル情報にアクセスできるようになりました。
下記は ZipDirectory を使ってファイル名一覧を取り出すサンプルです。
T_ZIP_CENTRALDIR32 内のファイル名が 0 終端になっていない点に注意。

void file_lsit( const char* zip_file_name )
{
    ZipDirectory    directory;
    directory.Load( zip_file_name );

    const int   NAME_BUFFER_SIZE= 512;
    char    name_buffer[ NAME_BUFFER_SIZE ];

    const T_ZIP_CENTRALDIR32*   dir_ptr= directory.Begin();
    for(; !directory.IsEnd( dir_ptr ) ; dir_ptr= dir_ptr->Next() ){

        unsigned int    name_length= dir_ptr->NameLength.Get();
        assert( name_length < NAME_BUFFER_SIZE );

        // ファイル名の取り出し
        memcpy( name_buffer, dir_ptr->GetNamePointer(), name_length );
        name_buffer[name_length]= '\0';
        printf( "%8d %8d %s\n", uncompressed_size, compressed_size, name_buffer );
    }
}

ZipDirectory の中に出てくる Buffer は汎用的なメモリ確保を行っています。

class Buffer {
    void*   Memory;
    size_t  MemorySize;
    Buffer( const Buffer& ){}
    Buffer& operator=( const Buffer& src ){ return *this; }
public:
    Buffer() : Memory( NULL ), MemorySize( 0 ){}
    ~Buffer()
    {
        Release();
    }
    void Release()
    {
        if( Memory ){
            free( Memory );
            Memory= NULL;
        }
    }
    void Alloc( size_t size )
    {
        Release();
        Memory= malloc( size );
        MemorySize= size;
    }
    void Shrink( size_t size )
    {
        assert( size <= MemorySize );
        MemorySize= size;
    }
    size_t Size() const
    {
        return  MemorySize;
    }
    unsigned long SizeLong() const
    {
        return  static_cast( MemorySize );
    }
    template
    T* Address() const
    {
        return  reinterpret_cast( Memory );
    }
};

同様に File 型も必要に応じて作ります。
例えば stdio なら下記の通り。

class File {
    FILE*   Fp;
public:
    File() : Fp( NULL ){}
    ~File()
    {
        Close();
    }
    bool Open( const char* file_name )
    {
#if _WINDOWS
        fopen_s( &Fp, file_name, "rb" );
#else
        Fp= fopen( file_name, "rb" );
#endif
        return  Fp != NULL;
    }
    bool Create( const char* file_name )
    {
#if _WINDOWS
        fopen_s( &Fp, file_name, "wb" );
#else
        Fp= fopen( file_name, "wb" );
#endif
        return  Fp != NULL;
    }
    void Close()
    {
        if( Fp ){
            fclose( Fp );
            Fp= NULL;
        }
    }
    size_t Read( void* buffer, size_t size )
    {
        return  fread( buffer, 1, size, Fp );
    }
    size_t Write( void* buffer, size_t size )
    {
        return  fwrite( buffer, 1, size, Fp );
    }
    void Seek( long long offset, int origin )
    {
#if _WINDOWS
        _fseeki64( Fp, offset, origin );
#else
        fseek( Fp, offset, origin );
#endif
    }
};

ファイル情報がとれたので、あとはファイルの内容を読み出すだけです。
下記 UnzipFile() は、zip 内のファイルを解凍することが出来ます。

bool UnzipFile( const T_ZIP_CENTRALDIR32* entry, const char* zip_file_name, const char* extract_file_name )
{
    File    zip_file;
    if( !zip_file.Open( zip_file_name ) ){
        return  false;
    }

    // Local Header の読み込み
    T_ZIP_LOCAL32   LocalHeader;
    zip_file.Seek( entry->LocalHeaderOffset.Get(), SEEK_SET );
    zip_file.Read( &LocalHeader, sizeof(T_ZIP_LOCAL32) );
    assert( LocalHeader.Signature.Get() == ZIP_SIGNATURE_LOCAL32 );

    // Offset の算出
    unsigned int    local_offset= sizeof(T_ZIP_LOCAL32) + LocalHeader.NameLength.Get() + LocalHeader.ExtraLength.Get();
    unsigned int    uncompressed_size= entry->UncompressedSize.Get();
    unsigned int    compressed_size= entry->CompressedSize.Get();

    // データ本体の読み込み
    Buffer  src_buffer;
    src_buffer.Alloc( compressed_size );

    zip_file.Seek( entry->LocalHeaderOffset.Get() + local_offset, SEEK_SET );
    zip_file.Read( src_buffer.Address(), compressed_size );

    Buffer  dest_buffer;
    dest_buffer.Alloc( uncompressed_size );

    switch( entry->Method.Get() ){
    case 0: // 非圧縮
        assert( uncompressed_size == compressed_size );
        memcpy( dest_buffer.Address(), src_buffer.Address(), uncompressed_size );
        break;
    case 8: // 圧縮されている場合
        if( !zlib_uncompress_raw( dest_buffer, src_buffer ) ){
            return  false;
        }
        break;
    default:
        assert( 0 );
        break;
    }

    // CRC の確認
    if( crc32( 0, dest_buffer.Address(), uncompressed_size ) != entry->CRC32.Get() ){
        return  false;
    }

    // 書き込み
    File    file;
    if( !file.Create( extract_file_name ) ){
        return  false;
    }

    file.Write( dest_buffer.Address(), dest_buffer.Size() );
    file.Close();

    return  true;
}

UnzipFile() では、Central Directory (T_ZIP_CENTRALDIR32) の情報を元に
まず Local Header (T_ZIP_LOCAL32) を読み込んでいます。
T_ZIP_LOCAL32 の NameLength と ExtraLength から実際のデータ位置が求まるので、
あらためてデータ本体を読み込みます。

データが圧縮されている場合は、前回解説したとおり zlib の raw format と
みなして展開しています。

size_t zlib_uncompress_raw( Buffer& dest, const Buffer& source )
{
    z_stream    stream;
    memset( &stream, 0, sizeof(z_stream) );
    stream.next_in= source.Address();
    stream.avail_in= source.SizeLong();
    stream.next_out= dest.Address();
    stream.avail_out= dest.SizeLong();

    // raw format の指定
    if( inflateInit2( &stream, -MAX_WBITS ) != Z_OK ){
        return  0;
    }

    int err= inflate( &stream, Z_FINISH );
    if( err != Z_STREAM_END && err != Z_OK ){
        return  0;
    }
    err= inflateEnd( &stream );
    assert( dest.Size() == stream.total_out );
    return  stream.total_out;
}

ここまでのコードで zip 内ファイルを取り出すことが可能です。

ただし展開パスにディレクトリが含まれている場合は書き込みに失敗するので、
もう少しだけ手を加えてみます。
下記の MakePath() は階層に対応した mkdir() です。(終端に '/' が必要)

static bool MakeDirectory( const char* path )
{
#if _WINDOWS
    return  _mkdir( path ) == 0;
#else
    return  mkdir( path, 0755 ) == 0;
#endif
}

static void MakePath( const char* extract_file_name )
{
    size_t  length= strlen( extract_file_name );
    Buffer  path_buffer;
    path_buffer.Alloc( length + 1 );
    char*       str= path_buffer.Address();
    const char* ptr= extract_file_name;
    for(; *ptr ;){
        if( *ptr == '/' ){
            *str= '\0';
            MakeDirectory( path_buffer.Address() );
        }
        *str++= *ptr++;
    }
}

同じパスを何度も mkdir するのは非効率なので、前回と同じパスは省きます。
でもあまり効果が無いかもしれません。

struct PathCache {
    Buffer  PrevPath;

    bool IsNewPath( const char* path )
    {
        const char* last_path= NULL;
        for( const char* ptr= path ; *ptr ; ptr++ ){
            if( *ptr == '/' ){
                last_path= ptr + 1;
            }
        }
        if( last_path ){
            size_t  size= last_path - path;
            if( PrevPath.Size() == size + 1 ){
                if( memcmp( PrevPath.Address(), path, size ) == 0 ){
                    return  false;
                }
            }
            PrevPath.Alloc( size + 1 );
            memcpy( PrevPath.Address(), path, size );
            PrevPath.Address()[size]= '\0';
            return  true;
        }
        return  false;
    }
    const char* GetPath() const
    {
        return  PrevPath.Address();
    }
};

zip の展開ができるようになりました。
下記 Unzip() は階層付きで zip に含まれる全ファイルを展開することが出来ます。

void Unzip( const char* zip_file_name )
{
    ZipDirectory    directory;
    directory.Load( zip_file_name );

    PathCache   path_cache;

    const T_ZIP_CENTRALDIR32*   dir_ptr= directory.Begin();
    for(; !directory.IsEnd( dir_ptr ) ; dir_ptr= dir_ptr->Next() ){
        assert( dir_ptr->Signature.Get() == ZIP_SIGNATURE_CENTRALDIR32 );

        unsigned int    name_length= dir_ptr->NameLength.Get();
        const int   NAME_BUFFER_SIZE= 512;
        char    name_buffer[ NAME_BUFFER_SIZE ];
        assert( name_length < NAME_BUFFER_SIZE );
        memcpy( name_buffer, dir_ptr->GetNamePointer(), name_length );
        name_buffer[name_length]= '\0';

        if( !dir_ptr->IsDirectory() ){
            if( path_cache.IsNewPath( name_buffer ) ){
                MakePath( path_cache.GetPath() );
            }
            UnzipFile( dir_ptr, zip_file_name, name_buffer );
        }
    }
}
int main( int argc, char** argv )
{
    for(; --argc ; Unzip( *++argv ) );
    return  0;
}

最終的なコードはこちら。Windows と Linux で確認しています。

zip2.cpp

関連エントリ
データ圧縮 zlib と gzip と zip (zlib で zip にアクセスする)

データ圧縮 zlib と gzip と zip (zlib で zip にアクセスする)

Windows を除いてほとんどのプラットフォームには zlib が入っており、
データの圧縮に利用することが出来ます。

zlib

メモリ上のイメージを圧縮するだけなら下記のとおり。

// [1]
// 圧縮データが入るバッファの確保
uLong  buffer_size= compressBound( src_size );
Bytef* buffer= memory_alloc( buffer_size );

// 圧縮
compress( buffer, &buffer_size, src_memory, src_size );

// 圧縮後のサイズ
compressed_size= buffer_size;

圧縮率を指定するなら compress() の代わりに compress2() を使います。
展開も同様。

// [2]
uLong buffer_size= uncompressed_size;
uncompress( buffer, &buffer_size, src_memory, src_size );

解凍時は展開後の元のサイズ (src_size) が必要になるので、
圧縮時に保存しておく必要があります。

●ヘッダと動作モード

圧縮されたデータには、先頭にフォーマットを表す最小限のヘッダ、
最後にチェックサム Adler-32 が付加されます。
このヘッダ形式は zlib 独自のもので、gzip や zip 等とは互換性がありません。
チェックサムも一般的な CRC とは異なっています。

ただし圧縮アルゴリズム自体は広く使われている Deflate で、
ヘッダ&フッタが違うだけでデータ本体は gzip/zip などと同じです。
そのため zlib には、gzip のヘッダを用いて読み書きを行うモードがあります。

[1]/[2] の compress()/uncompress() 相当を低レベルな API で書きなおしたのが
下記の例です。

// [3] Compress (zlib)
size_t Compress_zlib( void* dest_memory, size_t dest_size, const void* src_memory, size_t src_size )
{
   z_stream  stream;
   memset( &stream, 0, sizeof(z_stream) );
   stream.next_in= (Bytef*)src_memory;
   stream.avail_in= src_size;
   stream.next_out= (Bytef*)dest_memory;
   stream.avail_out= dest_size;

   deflateInit2( &stream, COMPRESS_LEVEL, Z_DEFLATED, MAX_WBITS, 5, 8, Z_DEFAULT_STRATEGY );
   int result= deflate( &stream, Z_FINISH )
   if( result != ZSTREAM_END && result != Z_OK ){
      // error
      return  0;
   }
   deflateEnd( &stream );

   return  stream.total_out;  // compressed_size
}
// [4] Uncompress (zlib)
size_t Uncompress_zlib( void* dest_memory, size_t dest_size, const void* src_memory, size_t src_size )
{
   z_stream  stream;
   memset( &stream, 0, sizeof(z_stream) );
   stream.next_in= (Bytef*)src_memory;
   stream.avail_in= src_size;
   stream.next_out= (Bytef*)dest_memory;
   stream.avail_out= dest_size;

   inflateInit2( &stream, MAX_WBITS );
   int result= inflate( &stream, Z_FINISH )
   if( result != ZSTREAM_END && result != Z_OK ){
      // error
      return  0;
   }
   inflateEnd( &stream );

   assert( dest_size == stream.total_out );

   return  stream.total_out;  // uncompressed_size
}

deflateInit2() / inflateInit2() の引数のうち、上の [3]/[4] で
MAX_WBITS が入っている所が windowBits の指定場所になります。
windowBits には下記の特殊な用法があります。

(1) windowBits       =   8~15    zlib format
(2) windowBits + 16  =  24~31    gzip format
(3) windowBits + 32  =  40~47    zlib/gzip format (inflate のみ)
(4) -windowBits      =  -8~-15   raw format

(2) のようにパラメータに 16 を加えると gzip フォーマットになります。
チェックサムも CRC-32 です。
例えば上の例 [3]/[4] だと MAX_WBITS の代わりに (MAX_WBITS+16) を与える
ことになります。

(3) inflateInit2() の場合 windowBits に 32 を加えると zlib/gzip の
自動判別ができるようになります。

(4) のように負数を入れると余計なデータが出力されません。
ヘッダも付かずにチェックサム計算も省かれます。
zip のように互換性のないファイル構造でも、データ部分の圧縮と解凍に
zlib を利用出来るようになります。( Method 8 )

まとめると下記の通り。zlib には 3種類の動作モードがあります。

               windowBits      header       checksum
-----------------------------------------------------
zlib format    MAX_WBITS       zlib 形式    Adler-32
gzip format   (MAX_WBITS|16)   gzip 形式    CRC-32
raw format    -MAX_WBITS       無し         無し

● Windows

zlib は Windows でも利用できますが、64bit (x64) では少々問題があります。

zlib のサイズ値に uLong (unsigned long) 型が用いられているので、
同じ 64bit OS でも Windows とそれ以外で型のサイズが変わっています。
Windows は LLP64 なので 32bit、Linux x64, OSX, IOS arm64 は LP64 なので
64bit になります。

例えば上の [3]/[4] のコードは size_t を使用しています。
size_t は x64 で 64bit なので、Windows x64 のみ zlib API に渡すときに
32bit への変換が必要になってしまいます。
互換性の意味でも long はできるだけ避けたいのですが、
Windows x64 では諦めてキャストを使っています。

● zip の構造とデータアクセス

zip はデータ圧縮だけでなくアーカイバの機能も含まれています。
tar + gzip の組み合わせはアーカイブした後に一度に圧縮しますが、
zip は個々のファイルを個別に圧縮しています。

また圧縮はファイルの中身だけで、ヘッダや含まれるファイル情報
(Local Header, Central Directory など) は含まれません。
そのため小さいファイルが大量に含まれるケースでは zip の効率は落ちます。

反面、ファイル単位でランダムアクセスが可能で、必要なファイルだけを
取り出すことができます。
アクセスするためには下記の手順が必要になります。

(1) EOR を検索して、Central Directory の位置を特定する
(2) Central Directory を読み込んで必要なファイルを検索する
(3) (2) からファイルの Local Header の位置が求まるので読み込む
(4) Local Header からデータの位置を求めて特定する。

End of Central Directory record (EOR) の位置は不定なので、
ファイルの終端から逆方向に読み込んで SIGNATURE の検索が必要。
EOR が特定できれば Central Directory の位置とサイズがわかるので
まとめて読み込むことが出来ます。

Central Directory 内の個々の情報は不定長なので、一度先頭から
読み進める必要があります。
Local Header の位置、ファイル名、圧縮前後のファイルサイズ、CRC、
圧縮メソッドなどはここに含まれます。
ディレクトリも個別エントリとして含まれており、サイズは 0 で
ファイル名の最後に ‘/’ が付きます。

Local Header はファイル本体の前に挿入されています。
こちらも長さ不定で、読み込んでみるまでデータの位置が厳密に求まりません。
適当なサイズを指定してとりあえず読み込みます。

Local Header からデータの位置が特定できるので、
実際のデータ部分を zip ファイルからロードできます。
Method が 8 なら圧縮されているため zlib の raw format として解凍できます。
解凍後に zlib の crc32() 等を使って CRC チェックを行います。

デフォルトで用いられている圧縮 Method は 0 と 8 です。
Method 8 が Deflate で Method 0 は非圧縮となっています。

下記の資料を参考にしました。

APPNOTE.TXT – .ZIP File Format Specification

簡単にデータ位置を特定できない点は少々やっかいですが、
Central Directory は先キャッシュ出来ますし、Local header も
データと同時に多めにサイズを指定してまとめて読み込んでしまうことも出来ます。
解凍も zlib が使えますし、広く使われているだけあって zip の
データアクセスは比較的容易にできます。

iPhone 5s A7 arm64 専用命令の速度 (2) (ARMv8 AArch64)

ARMv8 の AES 命令を使ってみたので下記 CPU ベンチを更新しました。

CPU ベンチ

以下抜粋

CPU            arch           GHz  time      MB/sec   1GHzあたり
---------------------------------------------------------------
Apple A7 CPU   ARMv8 + AES    1.3  0.13s   837.54MB/s  644.26MB
Apple A7 CPU   ARMv8 (arm64)  1.3  1.04s   104.27MB/s   80.21MB
Apple A7 CPU   ARMv7          1.3  1.16s    93.04MB/s   71.57MB
Cortex-A15     ARMv7          1.7  1.49s    72.61MB/s   42.71MB
A6 swift       ARMv7          1.3  1.87s    57.96MB/s   44.58MB
Krait          ARMv7          1.5  2.28s    47.64MB/s   31.82MB
A5 Cortex-A9   ARMv7          0.8  5.78s    18.76MB/s   23.44MB

Core i7 3930K  x64 + AES-NI   3.2  0.05s  2299.54MB/s  718.61MB
Core i7 3930K  x86 + AES-NI   3.2  0.06s  1682.35MB/s  525.74MB
Core i7 2600S  x64 + AES-NI   2.8  0.05s  2113.03MB/s  754.66MB
Core i7 2600S  x86 + AES-NI   2.8  0.06s  1683.66MB/s  601.31MB
Core i7 620M   x64 + AES-NI   2.7  0.08s  1410.61MB/s  528.32MB
Core i7 620M   x86 + AES-NI   2.7  0.10s  1064.06MB/s  398.53MB
Core i7 3930K  x64            3.2  0.48s   228.05MB/s   71.26MB
Core i7 3930K  x86            3.2  0.50s   216.50MB/s   67.66MB
Core2 duo x64  x64            2.4  0.75s   143.56MB/s   59.81MB
Core2 duo x86  x86            2.4  0.85s   127.99MB/s   53.33MB
Atom N270      x86            1.6  4.21s    25.74MB/s   16.09MB

 ・「MB/sec」が大きいほうが高速
 ・「1GHzあたり」は同一 CPU クロックでの比較

A7 ARMv8 + AES を使うことで、命令未使用時のおよそ 8倍、
ARMv7 (32bit) 時の 9倍高速に実行できています。

ただし AES-NI 共にコンパイラの intrinsic (builtin) を並べてるだけなので、
実際にはもっと最適化できる可能性があります。

Intel の AES-NI は非常にシンプルで、1 round 毎に 1命令、
最終 round のみ別命令が割り当てられていました。

ARMv8 の場合は AddRoundKey, SubBytes, ShiftRows と
MixColumns が分かれています。
並べる命令数は増えますが、この組み合わせで最終 round も表現できるので
専用命令が要りません。
また AES-NI とは xor(eor) の位置がちょうど 1つずれる形になります。

関連エントリ
iPhone 5s A7 CPU の速度比較 arm64 (ARMv8 AArch64)
iPhone 5s A7 CPU の浮動小数点演算速度 (2) (arm64/AArch64/64bit)
Nexus 10 CPU Cortex-A15 の速度
Nexus 10 CPU Cortex-A15 の浮動小数点演算速度
iPad 4/iPad mini A6X/A5 の CPU/GPU 速度
iPhone 5 / A6 の CPU 速度 その 3
benchmark 関連