OpenGLにコンパイルしてもらう時点でシェーダにはある程度最適化が行われるようですが、 モバイルデバイスではあまりコストの掛かる最適化は行われないだろうという事で、 オフラインでGLSLの最適化をしてくれるフレームワークとしてglsl-optimizerがあります。 サポートされているGLSLバージョンはES2.0とES3.0もカバーしていますが、未対応の拡張もあります。
元々はUnityが機械生成した冗長なGLSLを最適化するために作られたようですが、 一応手書きのシェーダの最適化に使うこともできます。 ただし、組み込みを前提としているようでコマンドラインからキックするバイナリは提供されていません。 とりあえずここでは、staticライブラリをビルドしてスタンドアローンで実行するまでのコードを紹介します。
glsl-optimizerをgit cloneして、
projects/xcode5/glsl_optimizer_lib.xcodeproj
をXcodeで開き、そのままビルドしてC++ライブラリのlibglsl_optimizer.a
を生成します。
glsl_optimizer.h
とlibglsl_optimizer.a
をコピーしてきて、適当にmain関数を書きます。
#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
#include <getopt.h>
#include <sys/stat.h>
#include "glsl_optimizer.h"
int main (int argc, char **argv)
{
glslopt_shader_type shader_type = kGlslOptShaderFragment;
int ch;
while ((ch = getopt( argc, argv, "vf")) != -1 ){
switch (ch) {
case 'v':
shader_type = kGlslOptShaderVertex;
break;
case 'f':
shader_type = kGlslOptShaderFragment;
break;
}
}
argc -= optind;
argv += optind;
if (argc != 1) exit(1);
struct stat st;
stat(*argv, &st);
size_t fsize = st.st_size;
if (fsize == 0) exit(1);
FILE *fp = fopen(*argv, "r");
if (!fp) exit(1);
char *shader_source = (char *)malloc(fsize+1);
if (!shader_source) exit(1);
shader_source[fsize] = '\0';
if (fread(shader_source, fsize, 1, fp) != 1) exit(1);
glslopt_ctx *ctx = glslopt_initialize(kGlslTargetOpenGLES20);
glslopt_shader *shader = glslopt_optimize(ctx, shader_type, shader_source, 0);
free(shader_source);
int exit_code = 1;
if (glslopt_get_status(shader)) {
const char *newSource = glslopt_get_output(shader);
fprintf(stdout, "%s", newSource);
exit_code = 0;
} else {
const char *errorLog = glslopt_get_log(shader);
fprintf(stderr, "%s", errorLog);
exit_code = 1;
}
glslopt_shader_delete(shader);
glslopt_cleanup(ctx);
exit(exit_code);
}
Makefile。
TARGET=glsl_optimizer
CC=g++
CFLAGS= -O2 -Wall
SRCS=$(wildcard *.cpp)
OBJS=$(SRCS:%.cpp=%.o)
STATICLIBS=libglsl_optimizer.a
HEADERS=$(wildcard *.h)
LDFLAGS=
all: $(TARGET)
$(TARGET): $(OBJS)
$(CC) $(CFLAGS) -o $@ $^ $(STATICLIBS) $(LDFLAGS)
%o: %c $(HEADERS)
$(CC) $(CFLAGS) -c $<
clean:
$(RM) $(OBJS) $(TARGET)
このサンプルの場合は、シェーダ種別のフラグとファイルパスを与えて成功すればstdoutに変換結果を吐きます。
- 頂点シェーダ
$ ./glsl_optimizer -v /pass/to/shader/vs.glsl
- フラグメントシェーダ
$ ./glsl_optimizer -f /pass/to/shader/fs.glsl
スタンドアローンにしておけば、XcodeのCustom Buid Ruleでシェーダをバイナリに突っ込む際に変換するパスを挟む事も可能です。
今回のサンプルの場合はバイナリを/usr/local/bin
にインストールしたとして次のようなスクリプトになります。
if [[ "$INPUT_FILE_NAME" == *_vs.glsl ]]; then
glsl_optimizer_opt="-v"
elif [[ "$INPUT_FILE_NAME" == *_fs.glsl ]]; then
glsl_optimizer_opt="-f"
fi
cd "$INPUT_FILE_DIR"
/usr/local/bin/glsl_optimizer ${glsl_optimizer_opt} "$INPUT_FILE_NAME" > "$DERIVED_SOURCES_DIR/$INPUT_FILE_BASE.glsl" || exit 1
cd "$DERIVED_SOURCES_DIR"
/usr/bin/xxd -i "$INPUT_FILE_BASE.glsl" | sed s/}\;/,0x00}\;/ > "$INPUT_FILE_BASE.glsl.c"
手書きのシェーダの場合は大体頭打ちなのであまりパフォーマンス向上を実感出来ないかもしれませんが、 非効率な書き方をしている箇所をフィードバックしてもらう用途にも使えますね。
Metalの時代ではオフラインコンパイラが付くので、こうした最適化は過去の遺物となるだろう。