ComputeShader

はじめに

エンジニアの小林です。以前、Forward+レンダリングなどで、Compute Shaderを使い、
ライトカリングなど処理しましたが、今回、ある作業で、Compute Shaderを使用した時のお話になります。

ある作業

ゲーム開発が進行してきますと、高解像度のキャプチャ画像がほしい、ですとか、
ゲーム画面をそのまま動画にしたいので、フレーム毎にFullHD解像度でキャプチャしてほしい、といった案件があったりします。
そんなに、複雑なことでもないですし、まあ、時間かからず完了できるでしょうと、実装を開始しました。

まずは実装

担当したプロジェクトのゲーム画面のフレームバッファは、R11G11B10といったフォーマットです。そして、出力するファイルフォーマットは、RGB8のbitmapになります。
テクスチャフォーマットが違いますし、また、GPU上で扱われるテクスチャはリニアに並んでいません。なので、CPUからアクセス可能なバッファへコピーします(このコピー時にピクセルがリニアにアクセスできる状態になります)

この状態から、1Pixel毎に変換をかけて、別バッファに格納するというシンプルな処理をします。そして、ファイルに保存し、毎フレーム繰り返せば、完了です。

が、実行してみると、おっそろしく遅く、1フレーム毎にキャプチャするのに、3, 4秒待たされるような状態です。
尺が、1分、2分もあるようなデモを60FPSでキャプチャしたら…、と考えると、とても、「これでキャプチャをお願いします。」とは言えないものでした。
1920 x 1080 ですから、200万回ものループ処理をしていることになります。コードを組みつつ、薄々想像していましたが、それを超える遅さでした…。

改善

CPUでメインスレッドのみで変換していたので、それは確かに重いですが、ワーカースレッド等を使わずに、Compute Shaderで、RGB8フォーマットにして出力してもいいのでは、と思い、実装してみました。

ComputeShaderは、テクスチャもフェッチできるので、読み取ったtexelをリニア空間から、sRGBに変換、念のため、0.0 から 1.0 でクランプし、RGB8に変換、別バッファに格納する、といたって、シンプルなコードです。
これをゲームの描画コマンド実行が完了した後に、上記のCompute Shaderを実行させ、出来たメモリバッファをファイルに保存するようにしました。
すると、劇的な速度改善が出来ました。体感できるほど、速くなりました。ただ、CPUで処理していた時は、シングルスレッドで処理していたので、フェアではないですが…。
(今となっては、FragmentShaderで、RGB8のレンダーターゲットに変更して、フレームバッファをテクスチャにして描画しても良かったかも、、とは思います)

Windowsで再現

以前、作成したFoward+レンダリングのデモに疑似環境として、実装してみました。

ComputeShader Code

uniform sampler2D s_colorSampler;

layout(std430, binding=0) buffer OutBuffer
{
    uint8_t b_buffer[];
};

layout( local_size_x = 32, local_size_y = 32, local_size_z = 1 ) in;


// メイン
void main()
{
	ivec2 texSize = textureSize(s_colorSampler, 0);
	if (gl_GlobalInvocationID.x >= texSize.x || gl_GlobalInvocationID.y >= texSize.y)
		return;

	// テクスチャフェッチ
	vec4 color = texelFetch(s_colorSampler, ivec2(gl_GlobalInvocationID.xy), 0);

	// クランプ
	color.rgb = clamp(color.rgb, 0.0, 1.0);
	// 0.0 - 1.0 を 0 - 255へ
	color.rgb *= vec3(255.0);

	// 格納先を求める
	uint offset = gl_GlobalInvocationID.y * (gl_WorkGroupSize.x * gl_NumWorkGroups.x) + 
					(gl_WorkGroupID.x * gl_WorkGroupSize.x) + gl_LocalInvocationID.x;
	offset *= 3;

	b_buffer[offset + 0] = uint8_t(color.b);
	b_buffer[offset + 1] = uint8_t(color.g);
	b_buffer[offset + 2] = uint8_t(color.r);

}

Cpu側ComputeShader Dispatch

	glUseProgram( compShaderId );

	unsigned int	samplerLoc = toRgb8Shader_->GetUniformLocation("s_colorSampler");

	glActiveTexture(GL_TEXTURE0);
	glBindTexture(GL_TEXTURE_2D, colorBuf_);
	glUniform1i(samplerLoc, 0);

	glBindBuffer(GL_SHADER_STORAGE_BUFFER, mOutBufferId);
	glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, mOutBufferId);

	glDispatchCompute(WIDTH/32, std::ceil(HEIGHT/32), 1);

	void* outBuffer = glMapBuffer(GL_SHADER_STORAGE_BUFFER, GL_READ_ONLY);
	memcpy(dst, outBuffer, 3 * WIDTH * HEIGHT);
	glUnmapBuffer(GL_SHADER_STORAGE_BUFFER);

ComputeShaderでの変換は、Cpuの負荷には計測されませんし、Dispathまでの処理が、非常に速く、効果が期待できました。

計測結果は以下の通りです。

CPUで変換 GPU(ComputeShader)で変換
8、9ms 38、39ms

ComputeShaderでの変換の方が、かなり遅い結果に…。
OpenGLで組んでますが、ComputeShaderで変換し出力したShaderStorageBufferを読み取るため、glMapBuffer()を使用したら、これがかなり遅く(30数msかかる…)、望んだ結果になりませんでした。
glReadPixels()は、ここまで遅くならないので、Fragment Shaderで、変換した方が結果、速くなるかもしれません。

さいごに

以上となります。
疑似環境での結果が、何とも締まらない結果になってしまいました…。
ちょっとしたことでも、ComputeShaderを使用して、改善した事例をお話しました。
最後まで読んで頂いた方、ありがとうございました。