たくさんの自由帳

Google Pixel 11 だけ OpenGL ES を使う自作アプリが起動しないので直した

投稿日 : | 0 日前

文字数(だいたい) : 2309

どうもこんばんわ。
下取りに出してPixel 11 Pro Foldにしました。下取りに出してもなお高い金を払う必要があるので、別に見送ってもよかったかもしれません。

使ってみた感じの感想ですが・・・

電波は確実によくなっている。カメラの隣のLEDじゃなくて、もうとにかく電波がよくなってます。
てかPixel 6以降が明らかに悪すぎたんですけどね多分、、

折りたたまない方の画面は確かにデカくなってます。
あとはなんか軽くなった気もします。まあこれでも物理的に重いけど、、

折りたたみの方はRAMは一世代前と同じく16GB搭載してますが、心なしか利用量が減っている気がします。
使ったばっかで常駐するプロセスがそもそも少ないとかもあるかも。

気になる点は・・・!

懐中電灯はなんかちょっとだけ暗い気がします。
カラーLEDが自撮りカメラの部分に搭載されたわけですが(決めたの人からの着信時に光らせたりできる)、この代償なのか懐中電灯が暗くなった気がします。なんかもっと明るかった気がします。気のせいな気もしなくはない。

あとGPUの性能は上がってないと思います。ゲームしないんで別に、、、

本題

Pixel 11だとOpenGL ESを使った自作アプリが起動しない!

java.lang.RuntimeException: glDrawArrays: glError 1282

原因

#version 300 es
#extension GL_EXT_YUV_target : require
precision mediump float;

in vec2 vTextureCoord;
uniform sampler2D sCanvasTexture;
uniform sampler2D sFboTexture;
uniform __samplerExternal2DY2YEXT sSurfaceTexture;

// 何を描画するか
// 1 SurfaceTexture(カメラや動画のデコード映像)
// 2 Bitmap(テキストや画像を描画した Canvas)
// 3 FBO
uniform int iDrawMode;

// SurfaceTexture 時のみ。クロマキー
uniform float chromakeyThreshold; // クロマキーのしきい値。0 でクロマキー無効
uniform vec4 chromakeyColor; // クロマキーにする色

// 出力色
out vec4 FragColor;

// https://github.com/android/camera-samples/blob/a07d5f1667b1c022dac2538d1f553df20016d89c/Camera2Video/app/src/main/java/com/example/android/camera2/video/HardwarePipeline.kt#L107
vec3 yuvToRgb(vec3 yuv) {
  const vec3 yuvOffset = vec3(0.0625, 0.5, 0.5);
  const mat3 yuvToRgbColorTransform = mat3(
    1.1689f, 1.1689f, 1.1689f,
    0.0000f, -0.1881f, 2.1502f,
    1.6853f, -0.6530f, 0.0000f
  );
  return clamp(yuvToRgbColorTransform * (yuv - yuvOffset), 0.0, 1.0);
}

void main() {   
  vec4 outColor = vec4(0.0, 0.0, 0.0, 1.0);

  if (iDrawMode == 1) {
    outColor.rgb = yuvToRgb(texture(sSurfaceTexture, vTextureCoord).rgb);
    
    // クロマキーで透過判定になったら discard
    if (chromakeyThreshold != .0 && length(outColor.rgb - chromakeyColor.rgb) < chromakeyThreshold) {
        discard;
    }
  } else if (iDrawMode == 2) {
    // テクスチャ座標なので Y を反転
    outColor = texture(sCanvasTexture, vec2(vTextureCoord.x, 1.0 - vTextureCoord.y));
  } else if (iDrawMode == 3) {
    outColor = texture(sFboTexture, vTextureCoord);
  }

  FragColor = outColor;
}

いろいろいじって分かった。
わたしのアプリは10 ビット HDR 映像を扱うのでuniform sampler2D cameraTextureではなく、uniform __samplerExternal2DY2YEXT cameraTextureを使っています。

で、__samplerExternal2DY2YEXTをuniform 変数で宣言している一方、テクスチャは転送せずにglDrawArrays()すると失敗するらしいです。
条件分岐でテクスチャを分岐していて、まあ条件分岐的にテクスチャは使われないからこのglDrawArrays()では転送しなくてもいいか。って感じで書いてたのですが、これがダメだった模様。
まあ私のフラグメントシェーダーの書き方が変だったのが原因だったわけですね。

ただ、sampler2DとsamplerExternalOESの場合は転送しなくても動いてたはずで、なんかよく分かりません、、

後述しますがPixel 11からOpenGL ESドライバーがANGLEになったせいなのかな~

修正した

そもそも一つのフラグメントシェーダーの中で無理に条件分岐を作る必要はないです。
代わりに描画したいテクスチャごと(動画の映像、Canvas、フレームバッファーオブジェクト)に分けてフラグメントシェーダーを用意する。

これから描画したいテクスチャに合わせてフラグメントシェーダー(正確にはプログラム)を切り替えるようにすることで動くように直しました。

まあ大体こんな感じのコードを書いています。

internal class OpenGlProgram(
    private val vertexShaderCode: String,
    private val fragmentShaderCode: String
) {

    fun prepare() { 
        // ここでプログラムを作って、シェーダーのコンパイルをする...
    }

    fun use() {
        // ここでプログラムを使う、glUseProgram() とか呼ぶ...
    }

    fun setUniform() {
        // uniform 変数を設定する...
    }
}

これを、以下のフラグメントシェーダーの分だけ作って

        /** バーテックスシェーダー。vTextureCoord でフラグメントシェーダーへテクスチャ座標を渡します */
        private const val VERTEX_SHADER = """#version 300 es
in vec4 aPosition;
in vec4 aTextureCoord;

uniform mat4 uMVPMatrix;
uniform mat4 uSTMatrix;

out vec2 vTextureCoord;

void main() {
  gl_Position = uMVPMatrix * aPosition;
  vTextureCoord = (uSTMatrix * aTextureCoord).xy;
}
"""

        /**
         * 10Bit HDR 動画のフレームを描画するときに使うフラグメントシェーダー
         *
         * CameraX いわく
         * HDR 動画の場合は GL_EXT_YUV_target を使うべきらしい。
         * SDR のときの samplerExternalOES でも動くには動くらしいが、YUV の方が良いらしい
         * https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:camera/camera-core/src/main/java/androidx/camera/core/processing/util/GLUtils.java;l=92
         */
        private val FRAGMENT_SHADER_10BIT_HDR_YUV = """#version 300 es
#extension GL_EXT_YUV_target : require
precision mediump float;

in vec2 vTextureCoord;
uniform __samplerExternal2DY2YEXT sSurfaceTexture;

uniform float chromakeyThreshold; // クロマキーのしきい値。0 でクロマキー無効
uniform vec4 chromakeyColor; // クロマキーにする色

out vec4 FragColor; // 出力色

// https://github.com/android/camera-samples/blob/a07d5f1667b1c022dac2538d1f553df20016d89c/Camera2Video/app/src/main/java/com/example/android/camera2/video/HardwarePipeline.kt#L107
vec3 yuvToRgb(vec3 yuv) {
  const vec3 yuvOffset = vec3(0.0625, 0.5, 0.5);
  const mat3 yuvToRgbColorTransform = mat3(
    1.1689f, 1.1689f, 1.1689f,
    0.0000f, -0.1881f, 2.1502f,
    1.6853f, -0.6530f, 0.0000f
  );
  return clamp(yuvToRgbColorTransform * (yuv - yuvOffset), 0.0, 1.0);
}

void main() {
    vec3 yuv = texture(sSurfaceTexture, vTextureCoord).xyz;
    vec3 rgb = yuvToRgb(yuv);
    
    // クロマキーで透過判定になったら discard
    if (chromakeyThreshold != .0 && length(rgb.rgb - chromakeyColor.rgb) < chromakeyThreshold) {
        discard;
    }
    
    FragColor = vec4(rgb, 1.0);
}
""".trimIndent()

        /** SDR 動画の時に使うフラグメントシェーダー */
        private val FRAGMENT_SHADER_SDR = """#version 300 es
#extension GL_OES_EGL_image_external_essl3 : require
precision mediump float;

in vec2 vTextureCoord;
uniform samplerExternalOES sSurfaceTexture;

uniform float chromakeyThreshold; // クロマキーのしきい値。0 でクロマキー無効
uniform vec4 chromakeyColor; // クロマキーにする色

out vec4 FragColor; // 出力色

void main() {
    vec4 color = texture(sSurfaceTexture, vTextureCoord);
    
    // クロマキーで透過判定になったら discard
    if (chromakeyThreshold != .0 && length(color.rgb - chromakeyColor.rgb) < chromakeyThreshold) {
        discard;
    }
    
    FragColor = color;
}
""".trimIndent()

        /** Bitmap に書いた Canvas を描画するときに使うフラグメントシェーダー */
        private val FRAGMENT_SHADER_CANVAS_BITMAP = """#version 300 es
precision mediump float;

in vec2 vTextureCoord;
uniform sampler2D sCanvasTexture;

out vec4 FragColor; // 出力色

void main() {
    // テクスチャ座標なので Y を反転
    FragColor = texture(sCanvasTexture, vec2(vTextureCoord.x, 1.0 - vTextureCoord.y));
}
""".trimIndent()

        /** FBO に描画された内容を描画するときに使うフラグメントシェーダー */
        private val FRAGMENT_SHADER_FBO = """#version 300 es
precision mediump float;
in vec2 vTextureCoord;
uniform sampler2D sFboTexture;

out vec4 FragColor; // 出力色

void main() {
    FragColor = texture(sFboTexture, vTextureCoord);
}
""".trimIndent()

使うときはこんな感じ

class AkariGraphicsTextureRenderer internal constructor(
    private val width: Int,
    private val height: Int,
    private val isEnableTenBitHdr: Boolean
) {
    // OpenGL ES のプログラムを描画別で作った
    // 一つのフラグメントシェーダーを使い描画内容を分岐する方法を使っていたが、一部の GPU(Pixel 11 ANGLE)では __samplerExternal2DY2YEXT を使う使わない関係なくシェーダーで定義した以上渡さないとエラーになってしまった
    // glDrawArrays: glError 1282
    // Canvas の内容を描画するときは __samplerExternal2DY2YEXT は渡されない状態なので動かなかった。
    // ので、それぞれでフラグメントシェーダーを分けることにした
    private var hdrGlProgram: OpenGlProgram? = null
    private var sdrGlProgram: OpenGlProgram? = null
    private var canvasGlProgram: OpenGlProgram? = null
    private var fboGlProgram: OpenGlProgram? = null

    // canvas で書いて OpenGLES に転写する
    suspend fun drawCanvas(draw: suspend Canvas.() -> Unit) {
        canvas.drawColor(0, PorterDuff.Mode.CLEAR)
        draw(canvas)

        canvasGlProgram?.use()

        GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, canvasBitmap, 0)
        checkGlError("GLUtils.texImage2D")

        // ...
    }
}

ことの始まり

Google Pixel 11シリーズは、OpenGL ESの命令をVulkanに翻訳して動かすANGLEドライバーを使っているみたいです。GPUの会社が作ったOpenGL ESのドライバーはもう無い感じ。
いつかのGoogle I/Oで発表してた気がするのでついにか~みたいな。

なので、GL コンテキストがあるスレッド(OpenGL ESが呼び出せるスレッド、makeCurrent()したスレッド)の中でこんな感じの処理を書くと、、

val vendor = GLES20.glGetString(GLES20.GL_VENDOR)
val renderer = GLES20.glGetString(GLES20.GL_RENDERER)
val version = GLES20.glGetString(GLES20.GL_VERSION)
println("OpenGL Info: vendor=$vendor, renderer=$renderer, version=$version")

こんな感じにANGLEだよ~みたいな出力になる。おお~

OpenGL Info: vendor=Google Inc. (Imagination Technologies), renderer=ANGLE (Imagination Technologies, Vulkan 1.4.317 (PowerVR C-Series CXTP-48-1536 MC1 (0x70061042)), PowerVR C-Series Vulkan Driver-1.662.3024), version=OpenGL ES 3.2 (ANGLE 2.1 git hash: 8d8796935e4faeaf19e67ac0a5949e71a5252e7f)

ちなみにPixel 10シリーズだとこうなってたはず?

vendor=Imagination Technologies / render=PowerVR D-Series DXT-48-1536 / version=OpenGL ES 3.2 build 26.1@6967606

で、これのせいなのか知らんけど自作アプリが起動しない。

java.lang.RuntimeException: glDrawArrays: glError 1282
	at io.github.takusan23.akaricore.graphics.AkariGraphicsTextureRenderer.checkGlError(AkariGraphicsTextureRenderer.kt:438)
	at io.github.takusan23.akaricore.graphics.AkariGraphicsTextureRenderer.drawCanvas(AkariGraphicsTextureRenderer.kt:107)
	at io.github.takusan23.akaridroid.canvasrender.VideoTrackRenderer.drawItemRendererToAkariGraphicsProcessor(VideoTrackRenderer.kt:427)
	at io.github.takusan23.akaridroid.canvasrender.VideoTrackRenderer.access$drawItemRendererToAkariGraphicsProcessor(VideoTrackRenderer.kt:58)
	at io.github.takusan23.akaridroid.canvasrender.VideoTrackRenderer$draw$2.invokeSuspend(VideoTrackRenderer.kt:206)
	at io.github.takusan23.akaridroid.canvasrender.VideoTrackRenderer$draw$2.invoke(VideoTrackRenderer.kt:8)
	at io.github.takusan23.akaridroid.canvasrender.VideoTrackRenderer$draw$2.invoke(VideoTrackRenderer.kt:4)
	at io.github.takusan23.akaricore.graphics.AkariGraphicsProcessor$drawOneshot$2.invokeSuspend(AkariGraphicsProcessor.kt:105)
	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:34)
	at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:100)
	at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:520)
	at java.util.concurrent.FutureTask.run(FutureTask.java:328)
	at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:323)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1100)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:1572)

いろいろ見た感じ__samplerExternal2DY2YEXTのせいっぽかったわけですが、ほかのサンプルコード(cameraXのHDR 動画の処理)とかを見る感じ__samplerExternal2DY2YEXTを使っているのに動いてるんですよね。
つまり私がなんかやらかしてる。

で色々見た結果、ANGLEドライバーだと?、__samplerExternal2DY2YEXTは使う使わないによらず、フラグメントシェーダーで宣言した以上一度はテクスチャを転送しないと、このようなエラーが出てしまうみたい。
まあ普通宣言したなら転送しろよという話ではある、、

おわりに

数年前くらいのGoogle I/OでOpenGL ESドライバーがANGLEになるのは数年先とか言って、今がこの数年先か~とか思ってたんですが、
それよりも前からANGLEをGLESドライバーとして使ってたスマホがあった模様。Galaxy SシリーズのExynos搭載版らしいです。(しかし日本版はSnapdragonなのでセーフ!)

おわりです。