cocos2d::CCGLProgramがアレなワケ

 前回のオマケ的な何か。

 前回記事の最後で、私は次のように書きました:

ともあれ、上記の結果から、原則としてfor版を使いつつ、for版が使えない時にだけ非for版を使ってやればよさそうだとわかります。
実はこの部分にもこっそりと妙な罠が潜んでいるのですが、これについてはまた別の記事にて。

 というわけでその「別の記事」が今回です。

cocos2d::CCGLProgramのおかしな仕様

 まず、次のコードを、適当なところに書いてみます。さて、どのようになるでしょうか?
 コードは Cocos2d-x 2系ですが、3系でも同様の事が起こるので試してみて下さい。

CCGLProgram *prog = new CCGLProgram();
bool result = prog->initWithVertexShaderByteArray("a invalid vertex shader.", "a invalid fragment shader.");
CCLog(result ? "success" : "failure");
CC_SAFE_DELETE(prog);

 この時、"a invalid vertex(fragment) shader."というのは明らかに GLSL としては不正な文字列ですから、initWithVertexShaderByteArray()は失敗するでしょう。常人であれば、CCGLProgram自身は未使用のまま削除されるので描画には全く影響を及ぼさず、幾つかのコンパイルエラーメッセージと "failure" が出力されるだけ、と考えるのが普通だろうと思います。

 が……何と、実際は 違います
 かといってもちろん "success" が出力されるわけでもなく、あろう事かプログラムが 強制終了 という憂き目に遭うでしょう。

一体どういうこと?

 この、にわかには信じ難い仕様の原因を、試しに Cocos2d-x のソースコードを読んで探ってみます。

bool CCGLProgram::compileShader(GLuint * shader, GLenum type, const GLchar* source)
{
    GLint status;
    
    //// 中略 ////

    glGetShaderiv(*shader, GL_COMPILE_STATUS, &status);

    if (! status)
    {
        //// 中略 ////

#if (CC_TARGET_PLATFORM == CC_PLATFORM_WINRT)
        return false;
#else
        abort();
#endif
    }
    return (status == GL_TRUE);
}

 ……おわかりでしょうか? この燦然と輝くabort();の文字。
 ! statusという条件は GLSL のコンパイルに失敗した時のものですから、どうやら Cocos2d-x は GLSL のコンパイルに失敗すると問答無用でプログラムを異常終了させてくれるようです。

 普通に考えてありえない。一体、何のために引数がbool型になっているのやら(どうやらsource == nullptrの時にだけfalseみたいですよ)……というわけで皆さんは、Cocos2d-x を導入したら必ず、このabort()は削りましょう。

ライブラリ側を弄りたくないよって人は

 自分だけで開発しているならともかく、複数人で開発している場合、Cocos2d-x のライブラリ側はなるべく弄りたくないでしょう。
 そんな時は、CCGLProgram::compileShader()を参考に、コンパイル可能かどうか調べる関数を作ってやりましょう。

 やる事は簡単です。ほとんどCCGLProgram::compileShader()をコピってくるだけ。

/**
 * GLSLソースコードがコンパイル可能かどうか確認する
 *
 * @param GLenum  type   シェーダのタイプ(GL_VERTEX_SHADER, GL_FRAGMENT_SHADER, ...)
 * @param GLchar* source GLSLソースコード
 */
bool isCompilable(GLenum type, const GLchar *source)
{
  const GLchar *sources[] = {
#if (CC_TARGET_PLATFORM != CC_PLATFORM_WIN32 && CC_TARGET_PLATFORM != CC_PLATFORM_LINUX && CC_TARGET_PLATFORM != CC_PLATFORM_MAC)
    (type == GL_VERTEX_SHADER ? "precision highp float;\n" : "precision mediump float;\n"),
#endif
    "uniform mat4 CC_PMatrix;\n"
    "uniform mat4 CC_MVMatrix;\n"
    "uniform mat4 CC_MVPMatrix;\n"
    "uniform vec4 CC_Time;\n"
    "uniform vec4 CC_SinTime;\n"
    "uniform vec4 CC_CosTime;\n"
    "uniform vec4 CC_Random01;\n"
    "//CC INCLUDES END\n\n",
    source,
  } ;

  GLuint shader = glCreateShader(type);
  glShaderSource(shader, sizeof(sources)/sizeof(*sources), sources, nullptr);
  glCompileShader(shader);
  GLint status ;
  glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
  bool result = (status != GL_FALSE) ;
  glDeleteShader(shader) ;

  return result ;
}

 CCGLProgram::initWithVertexShaderByteArray()にシェーダのソースを渡す前に、一旦この関数で有効性を確認する事にします。もしくは、CCGLProgramを継承したクラスを作って、initWithVertexShaderByteArray()の前に必ず有効性チェックをかける事にしても構いません(ただし、CCGLProgram::initWithVertexShaderByteArray()は non-virtual なので、そこだけ注意)。
 こうしておけば、デバイスごとに最適なシェーダプログラムを走らせる事が容易になるでしょう。