D3D Shader/OpenGL」カテゴリーアーカイブ

Android OpenGL ES 3.0 対応 GPU を判定する方法

Android 4.3 から OpenGL ES 3.0 が使えるようになりました。
対応 GPU も順調に増えていますが、Android 4.3/4.4 が動く端末自体が
まだあまり多くありません。
GPU によっては OpenGL ES 3.0 の動作で問題が生じるものもあり、
互換性の意味でも OpenGL ES 2.0/3.0 両対応が望ましいでしょう。

iOS の方は比較的簡単で、今のところ 64bit 対応 (ARMv8 arm64) なら
OpenGL ES 3.0 にも対応しています。

Android 端末が OpenGL ES 3.0 に対応してるかどうか調べる方法は
ドキュメントに記載されています。

OpenGL ES : Checking OpenGL ES Version

ただしこの方法は以前も書いたように必ずしもうまく行きません。
1. 及びサンプルコードは OpenGL ES 3.0 の Context を作成し、
エラーかどうかで判断しています。
実際にいくつかの端末を調べてみると、GPU ドライバは未対応でも
エラーを返さない場合がほとんどです。

version != 2.0 で判定している GPU は OpenGL ES 1.1 の context を返しますし、
version < 2.0 で判定している GPU は OpenGL ES 2.0 の context になります。 2. の方法はうまく行きますが、一旦ダミーの Context を作成するため複雑です。
またこの判定方法は Android 限定です。

以下その具体的な方法

package jp.flatlib.gw;

import  android.opengl.GLES20;
import  android.os.Build;
import  android.graphics.SurfaceTexture;
import  javax.microedition.khronos.egl.EGL10;
import  javax.microedition.khronos.egl.EGLConfig;
import  javax.microedition.khronos.egl.EGLContext;
import  javax.microedition.khronos.egl.EGLDisplay;
import  javax.microedition.khronos.egl.EGLSurface;
import  android.util.Log;

public class GPUType {

    public static int       VersionCode= 0; // 200 or 300
    public static String    VersionString;
    public static String    RendererString;
    private static int      AdrenoID= 0;

    public static boolean   isGLES3()
    {
        return  VersionCode == 300;
    }

    public static boolean   isAdreno300s()
    {
        return  AdrenoID >= 300 && AdrenoID < 400;
    }

    private static int decodeAdrenoID( String gpu_name )
    {
        if( gpu_name.equals( "Adreno" ) || gpu_name.indexOf( "AMD Z430" ) >= 0 ){
            return  200;
        }else if( gpu_name.indexOf( "Adreno" ) >= 0 ){
            return  getAdrenoNumber( gpu_name );
        }
        return  0;
    }

    public static void  update()
    {

        EGL10   egl= (EGL10)EGLContext.getEGL();
        int[]   iparam= new int[8];

        if( Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB ){
            VersionCode= 200;
            return;
        }

        // Initialize
        EGLDisplay  display= egl.eglGetDisplay( EGL10.EGL_DEFAULT_DISPLAY );
        egl.eglInitialize( display, iparam );

        // Config
        int[]   config_attr= { EGL10.EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, EGL10.EGL_NONE };
        final int   CONFIG_SIZE= 1;
        EGLConfig[] config_array= new EGLConfig[CONFIG_SIZE];
        egl.eglChooseConfig( display, config_attr, config_array, CONFIG_SIZE, iparam );
        EGLConfig   config= config_array[0];

        // Surface
        SurfaceTexture  surfaceTexture= new SurfaceTexture( 0 );    // API Level 11
        EGLSurface  surface= egl.eglCreateWindowSurface( display, config, surfaceTexture, null );

        // Context
        int[] context_attr= { EGL_CONTEXT_CLIENT_VERSION, 2, EGL10.EGL_NONE };
        EGLContext  context= egl.eglCreateContext( display, config, EGL10.EGL_NO_CONTEXT, context_attr );

        // Make Current
        egl.eglMakeCurrent( display, surface, surface, context );

        // Query
        VersionString=  GLES20.glGetString( GLES20.GL_VERSION );
        RendererString= GLES20.glGetString( GLES20.GL_RENDERER );
        VersionCode= getGLVersionString( VersionString );
        AdrenoID= decodeAdrenoID( RendererString );


        // Special
        if( isAdreno300s() ){
            if( Build.VERSION.SDK_INT <= Build.VERSION_CODES.JELLY_BEAN_MR2 ){ // .. Android 4.3
                VersionCode= 200;   // GLES 2.0
            }
        }

        // Release
        egl.eglMakeCurrent( display, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_CONTEXT );
        egl.eglDestroyContext( display, context );
        egl.eglDestroySurface( display, surface );

    }

    private static int getGLVersionString( String version_string )
    {
        int length= version_string.length();
        int ci= 0;
        for(; ci < length ; ci++ ){
            char    ch= version_string.charAt( ci );
            if( ch >= '0' && ch <= '9' ){
                break;
            }
        }
        int version= 0;
        int num_shift= 100;
        for(; num_shift > 0 && ci < length ;){
            char    ch= version_string.charAt( ci++ );
            if( ch >= '0' && ch <= '9' ){
                if( num_shift == 100 ){
                    if( version_string.charAt( ci ) == '.' ){
                        ci++;
                    }
                }
                version+= (ch - '0') * num_shift;
                num_shift/= 10;
            }else{
                break;
            }
        }
        return  version;
    }


    private static int getAdrenoNumber( String text )
    {
        int length= text.length();
        int num= 0;
        boolean is_num= false;
        for( int i= 0 ; i< length ; i++ ){
            char    ch= text.charAt( i );
            if( is_num ){
                if( ch >= '0' && ch <= '9' ){
                    num*= 10;
                    num+= ch - '0';
                }else{
                    break;
                }
            }else{
                if( ch >= '0' && ch <= '9' ){
                    is_num= true;
                    num= ch - '0';
                }
            }
        }
        return  num;
    }
}

GPUType.update() でデータを集めて isGLES3() で結果がわかります。

Android 3.0 API Level 11 以降でないと動きませんが、
Android 4.2 (API Level 17) 以前は OpenGL ES 2.0 と決め打ちできるため
特に問題ないはずです。

同じ理由で、EGL10 の代わりに EGL14 を使うこともできます。
EGL14 は API Level 17 以降の API です。

Android 4.3 + Adreno 320 の OpenGL ES 3.0 には問題があるので、
この組み合わせでは OpenGL ES 2.0 を返すようにしています。

関連エントリ
Android 4.4 Adreno 320 の OpenGL ES 3.0 と Version 判定
iPhone 5s の Apple A7 GPU
Adreno 320 の OpenGL ES 3.0 と Uniform Block
Nexus 7 (2013) の Adreno 320 と OpenGL ES 3.0 (Android 4.3)

Android OpenGL ES と V-Sync (eglSwapInterval)

Android の描画も V-Sync に依存しており、一部デバイスを除いておよそ
60fps が上限となっています。
Android 4.1 Jelly Bean (API Level 17) 以降は SwapInterval で
0 を設定することができます。

desktop 版 OpenGL 同様、Interval=0 に設定すると非同期になります。
実際の画面に反映されているかどうかを考えなければ、
GPU の描画ループも 60fps を超えることが可能です。

// Java
import  android.opengl.EGL14;
import  android.opengl.EGLDisplay;

~

EGLDisplay  display14= EGL14.eglGetDisplay( EGL14.EGL_DEFAULT_DISPLAY );
EGL14.eglSwapInterval( display14, 0 );

EGLDisplay, EGLContext, EGLConfig, EGLSurface は android.opengl と
javax.microedition.khronos.egl の両方に存在します。
EGL14 を用いる場合は android.opengl の方を使用。

// NDK NativeActivity
eglSwapInterval( display, 0 );
// NDK + GLSurfaceView
EGLDisplay  display= eglGetDisplay( EGL_DEFAULT_DISPLAY );
eglSwapInterval( display, 0 );

ベンチマークで便利。

関連エントリ
iOS CADisplayLink とフレームレート
OpenGL の同期と描画速度の測定

Android 4.4 Adreno 320 の OpenGL ES 3.0 と Version 判定

Android 4.3 で OpenGL ES 3.0 が導入されましたが、Adreno 320 では
一部動作に問題がありました。
具体的には Uniform Block を使用すると glLinkProgram でエラーが発生します。
Android 4.4 ではこの問題が直っており、正しく動作することが確認できました。
GPU のドライバが更新されたためだと考えられます。

以前の症状について詳しくは こちら

# Nexus 7 (2013) Android 4.3
GL_VERSION: OpenGL ES 3.0 V@14.0
GL_RENDERER: Adreno (TM) 320
GL_VENDOR: Qualcomm
GL_SHADING_LANGUAGE_VERSION: OpenGL ES GLSL ES 3.00
# Nexus 7 (2013) Android 4.4
GL_VERSION: OpenGL ES 3.0 V@53.0 AU@  (CL@3776187)
GL_RENDERER: Adreno (TM) 320
GL_VENDOR: Qualcomm
GL_SHADING_LANGUAGE_VERSION: OpenGL ES GLSL ES 3.00

他にも glIntergerv() (Query) の値など修正が入っているようです。

CPU/GPU OpenGL ES Extension (Mobile GPU)

OpenGL ES 3.0 が動くデバイスもかなり増えてきました。
Android 4.4 なら Nexus 7 (2013) が使えるので、動作確認は比較的容易に
なったのではないかと思います。

ただ実際のアプリケーションでは、確実に OS が更新されるとわかっている
Nexus 以外で利用するのは正直厳しいと言えます。

他にも OpenGL ES 3.0 が使えるかどうかの判定がうまくいかないことがあります。

OpenGL ES : Checking OpenGL ES Version

GPU によっては 3.0 を渡して Context を作ってもエラーにならず、
OpenGL ES 1.1 の Context を返すものがあります。
(おそらくドライバが 2.0 かそれ以外で判別している)

2番目の Version 番号を使う方法は Android ではうまくいきます。
Nexus 10, Nexus 7 (2013) ともに 2.0 context を作っても GL_VERSION は
3.0 を返すためです。
ただし iOS や PC では作成した context と同じバージョン文字列を返すので
少々不自然な気もします。

関連エントリ
iPhone 5s の Apple A7 GPU
Adreno 320 の OpenGL ES 3.0 と Uniform Block
Nexus 7 (2013) の Adreno 320 と OpenGL ES 3.0 (Android 4.3)

Android NDK r9b と ARMv7A の hard-float 補足

要するに新しい Android NDK r9b に入れ替えると
実行が速くなってプログラムサイズも小さくなるということです。
Android.mk に下記の行を追加してコンパイルするだけ。

ifeq ($(TARGET_ARCH_ABI),armeabi-v7a)
LOCAL_CFLAGS += -mhard-float
LOCAL_LDFLAGS += -Wl,--no-warn-mismatch
endif

より詳しくはこちら

関連エントリ
Android NDK r9b と ARMv7A の hard-float

Android NDK r9b と ARMv7A の hard-float

Android 4.4 (KitKat) とともに NDK r9b がリリースされています。
RenderScript 対応などいくつか新しい機能がありますが、
その中に ARMv7A の hard-float 対応が含まれています。
ちょうど NDK を使った関数電卓アプリを作っているところだったので試してみました。

Android NDK

ちょっと電卓

Android/iOS など ARM デバイスではこれまで float ABI として softfp が用いられていました。
関数呼び出しなどの引数は、浮動小数点数であっても必ず整数レジスタ r を経由しており、
FPU の有無にかかわらず共通化出来るようになっています。

その代わり VFP が搭載されているデバイスでの実行効率とコード効率がわずかに犠牲になっています。
これまでの iOS/Android スマートフォンで VFP が搭載されていないのは
MSM7225 など一部の ARM11 (ARMv6, Android では ARMv5TE) に限られていました。
ARMv7A では存在しておらず softfp を用いる必要はありませんでした。

Android NDK r9b で hard-float に対応したので VFP/NEON レジスタを直接用いた
関数呼び出しが可能となっています。

●コンパイル方法

NDK で hard-float を指定する手順は下記の通り。
Android.mk に追加します。

LOCAL_CFLAGS += -mhard-float
LOCAL_LDFLAGS += -Wl,--no-warn-mismatch

ただし対応しているコンパイラは gcc のみで clang ではまだ使用できません。
このオプションを指定できるのは ARMv7A ( TARGET_ARCH_ABI = armeabi-v7a ) の場合だけです。

●外部ライブラリの宣言

システムや外部ライブラリは softfp でコンパイルされているので、
コンパイラに ABI の違いを正しく認識させる必要があります。
NDK r9b 付属のヘッダでは、各関数に下記の宣言が追加されています。
これは softfp 関数の呼び出しであることを意味しています。

__attribute__((pcs("aapcs")))

例えば NDK 付属の math.h ヘッダを見ると下記のように宣言されています。

// math.h より抜粋
double	acos(double) __NDK_FPABI_MATH__;
double	asin(double) __NDK_FPABI_MATH__;

__NDK_FPABI_MATH__ や __NDK_FPABI__ は sys/cdefs.h で定義されています。

// sys/cdefs.h より一部抜粋
#define __NDK_FPABI__ __attribute__((pcs("aapcs")))
#define __NDK_FPABI_MATH__ __NDK_FPABI__

●コンパイル結果と libm の hard_float

実際に hard-float でコンパイルした結果は下記の通り。
ローカル関数呼び出しは直接 d0 レジスタが用いられていることがわかります。

// hard-float
// t_value f_bittof( t_value val )

   vcvt.u32.f64 s0, d0
   vcvt.f64.f32 d0, s0
   bx           lr

↓これまでの softfp でコンパイルすると下記の通り。
64bit 倍精度浮動小数点数は r0/r1 と 2つの 32bit 整数レジスタを使って
受け渡しが行われています。

// softfp
// t_value f_bittof( t_value val )

    vmov            d6, r0, r1
    vcvt.u32.f64    s15, d6
    vcvt.f64.f32    d6, s15
    vmov            r0, r1, d6
    bx              lr

hard-float でコンパイルした場合も、外部のライブラリ呼び出しでは
下記のように r0/r1 レジスタへのへのコピーが発生します。

// hard-float (-lm)

    vmov    r0, r1, d0
    bl      0 
    vmov    d0, r0, r1

ただし libm は hard-float でコンパイルした static ライブラリが付属しているので、
直接 VFP/NEON レジスタによる呼び出しも出来ます。

LOCAL_CFLAGS += -mhard-float -D_NDK_MATH_NO_SOFTFP=1
LOCAL_LDFLAGS += -Wl,--no-warn-mismatch -lm_hard

-lm (libm) の代わりに -lm_hard (libm_hard) を指定します。
この場合ヘッダの attribute 宣言を外す必要があるので -D_NDK_MATH_NO_SOFTFP=1 も
必要です。
↓ libm 呼び出しでもレジスタ転送が無くなりました。

// hard-float (-lm_hard)

    b       0 

アプリケーションコードは小さくなりますが libm_hard は static なので
プログラムコード全体は増えます。

● libm 以外のライブラリ呼び出し

浮動小数点数を用いるライブラリは libm 以外にもあります。
例えば OpenGL ES 2.0 関数にも attribute 宣言が追加されています。

// GLES2/gl2.h より
GL_APICALL void         GL_APIENTRY glClearColor (GLclampf red, GLclampf green, GLclampf blue, GLclampf alpha);

// GLES2/gl2platform.h
#define GL_APICALL  KHRONOS_APICALL

// KHR/khrplatform.h より抜粋
#   define KHRONOS_APICALL __attribute__((visibility("default"))) __NDK_FPABI__

実際に glClearColor() を呼び出してみると、hard-float でも r0-r3 へのコピーが
行われていることがわかります。

// hard-float (debug build)

    vstr    s0, [fp, #-8]
    vstr    s1, [fp, #-12]
    vstr    s2, [fp, #-16]
    vstr    s3, [fp, #-20]	; 0xffffffec
    ldr     r0, [fp, #-8]
    ldr     r1, [fp, #-12]
    ldr     r2, [fp, #-16]
    ldr     r3, [fp, #-20]
    bl      0 

libm 以外では特に hard-float 版が用意されているわけではないので
softfp 同様の転送が行われます。

実際のアプリケーションでどの程度の差が出るかわかりませんが、
NDK でさらに最適化出来る余地ができました。
もっとも、iOS の方はすでに ARMv8 に移行しつつあります。
Android も 64bit 化が行われればこのような違いを意識する必要は無くなるでしょう。

関連エントリ
Nexus 7 の Ubuntu で ARM の abi softfp と hard-float を比べる