あらすじ
この記事は、OpenSiv3DのLinux版の実装をWeb版に移植する際、変更が必要となった項目と、それをOpenSiv3D Web版ではどのように対応したかをまとめた記事です。
- SIMD命令を使うコードを、SIMD命令が使えない環境でも動作するようにする
- 使用するグラフィックAPIをOpenGL 4.xからOpenGL ES3.0に切り替える
- マルチスレッド(並列)なコードを、setTimeoutを使って擬似マルチスレッド(並行)なコードにする
OpenSiv3D v0.4.3ではWeb版は非公式プロジェクトとして開発継続中、OpenSiv3D v0.6では実験的機能として公式プロジェクトに組み込まれる予定です。
この記事の立ち位置は、次に紹介するOpenSiv3DのWebブラウザ版を実装しようとする試みをまとめた記事の延長線上にあります。
SIMDEの導入
WebAssemblyの仕様において、WebAssemblyでも固定長ビット(128bit)のSIMD命令を使えるようにするという提案がなされています1。しかし、2020年12月現在、WebAssembly SIMDに対応しているブラウザはChromeのみという状況2であり、WebAssembly SIMDを使うには早すぎる状況と考えられます。一方、WebAssembly SIMDの対応状況が進み、近い将来にWebAssembly SIMDを使っても問題ない状況が訪れるとも考えられます。
この2点から、次の2項目が必須条件となりました。
- WebAssembly SIMDを使わない実装のため、SIMD命令の代替実装を備えていること
- WebAssembly SIMDを使った実装に切り替えやすいこと
OpenSiv3D Web版では、この2項目を満たすライブラリとして、https://github.com/simd-everywhere/simde を採用しました。
OpenGL ES3.0への切り替え
WebGL2はOpenGL ES3.0をベースに作成されており、emscriptenのJavaScirptライブラリを使用して、OpenGL ES3.0のほとんどのの機能をそのまま使用することができます。しかし、OpenGL 4.xとOpenGL ES3.0の機能の差異はかなり大きく、実装の変更を余儀なくされた部分も数多くありました。
次の機能はOpenGL 4.xでもOpenGL ES3.0でもほとんど差異なく扱えました。
- シェーダ定数 (Uniform Buffer Object)
- 頂点バッファの取り回し (Vertex Array Object)
一方、次の機能(関数)はOpenGL ES3.0には存在しません。
- Program Pipeline
- glPolygonMode
- glDrawElementsBaseVertex
Program Pipelineの代替実装
OpenSiv3DのWebブラウザ版をEmscriptenで作ろうとした話 にもある通り、glCreateShaderProgramのリファレンスページ には、glCreateShaderProgram
の代替実装が紹介されています。
glCreateShaderProgramの代替実装
const GLuint shader = glCreateShader(type);
if (shader) {
glShaderSource(shader, count, strings, NULL);
glCompileShader(shader);
const GLuint program = glCreateProgram();
if (program) {
GLint compiled = GL_FALSE;
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
glProgramParameteri(program, GL_PROGRAM_SEPARABLE, GL_TRUE);
if (compiled) {
glAttachShader(program, shader);
glLinkProgram(program);
glDetachShader(program, shader);
}
/* append-shader-info-log-to-program-info-log */
}
glDeleteShader(shader);
return program;
} else {
return 0;
}
しかしながら、WebGL2ではglCreateShaderProgram
の実装どころか、glProgramParameteri
の実装自体が存在しません3。すなわち、頂点シェーダとフラグメントシェーダを別々のシェーダプログラムにリンクしてからアタッチするという方法自体がとれません。
そのため、描画に使用しようとしている頂点シェーダとフラグメントシェーダのハンドルを都度保存し、はじめて描画が必要となったときに、ペアとなっている頂点シェーダとフラグメントシェーダでシェーダプログラムのリンクを行い、そのシェーダプログラムをキャッシュするように実装を変更しました。
Program Pipelineの代替実装
class ShaderPipeline
{
public:
enum class ProgramState
{
Created,
Cached,
Invalid
};
private:
HashTable<Vec2, GLuint> m_programs;
GLuint m_currentProgram = 0;
GLuint m_currentVS = 0;
GLuint m_currentPS = 0;
GLuint linkShaders()
{
if (m_currentVS == 0 || m_currentPS == 0)
{
return 0;
}
GLuint program = ::glCreateProgram();
::glAttachShader(program, m_currentVS);
::glAttachShader(program, m_currentPS);
::glLinkProgram(program);
::glDetachShader(program, m_currentVS);
::glDetachShader(program, m_currentPS);
return program;
}
ProgramState setCurrentProgram()
{
Vec2 shaderSet { m_currentVS, m_currentPS };
if (m_programs.contains(shaderSet))
{
m_currentProgram = m_programs[shaderSet];
return ProgramState::Cached;
}
else
{
m_currentProgram = linkShaders();
if (m_currentProgram != 0)
{
m_programs.emplace(shaderSet, m_currentProgram);
return ProgramState::Created;
}
else
{
return ProgramState::Invalid;
}
}
}
public:
~ShaderPipeline()
{
for (auto [shaderSet, program] : m_programs)
{
::glDeleteProgram(program);
}
}
void setVS(GLuint vsProgramHandle)
{
m_currentVS = vsProgramHandle;
}
void setPS(GLuint psProgramHandle)
{
m_currentPS = psProgramHandle;
}
ProgramState use()
{
auto programState = setCurrentProgram();
::glUseProgram(m_currentProgram);
return programState;
}
};
glDrawElementsBaseVertexの代替実装
glDrawElementsBaseVertex
自体はOpenGL ES3.2でも利用可能です4。しかし、WebGL2はOpenGL ES3.0互換、残念ながらWebGL2にはglDrawElementsBaseVertex
は実装されていません。
とはいっても、glDrawElementsBaseVertex
とglDrawElements
の違いは、頂点インデックスの取り扱い方を指定できるかどうかの違いしかありません。OpenSiv3D Web版では、適宜頂点バッファへの書き込み位置をリセットすることで対応しました。
並列なコードの並行化
マルチスレッド自体はJavaScriptのWorkerが存在するので、マルチスレッドへの対応はできなくはありません。しかし、2018年からSpectre対応のために各ブラウザでSharedArrayBufferが無効化され5、低コストでスレッド間のメモリ共有をすることができない状況が続いています。
一方、JavaScriptではゲームループを単純なwhileとSleepの無限ループで実装することはできず、setTimeoutまたはrequestAnimationFrameを代わりに使うことなどして、適宜ブラウザに処理を戻す必要があります6。
OpenSiv3D Web版ではこのメインループの実装を前提にすることに加えて、ワーカースレッドでもこの無限ループ中のSleepの代わりにsetTimeoutを使うようにしました。ちょうどOSがシングルコアのCPUでもタスクスケジューリングを行なってマルチプロセスを実現するように、JavaScriptでもsetTimeoutを使った自主的なタスクスケジューリングを行なって、擬似的なマルチスレッドという並列(にみせかけた並行)処理をさせてます。
スレッドの代替実装 (Audio実装部分)
class SimpleVoice_AL
{
private:
// ~~ 中略 ~~
bool onUpdate()
{
if (!m_source)
{
return true;
}
if (m_isActive)
{
feed();
ALint sampleOffset = 0;
::alGetSourcei(m_source, AL_SAMPLE_OFFSET, &sampleOffset);
}
ALint currentState = 0;
::alGetSourcei(m_source, AL_SOURCE_STATE, ¤tState);
if (!m_isEnd && m_isActive && currentState == AL_STOPPED)
{
onStreamEnd();
}
if (m_abort)
{
return false;
}
return true;
}
static void onUpdateHelper(void *userData) {
if (static_cast<SimpleVoice_AL*>(userData)->onUpdate()) {
::emscripten_set_timeout(&SimpleVoice_AL::onUpdateHelper, 10.0, userData);
}
}
あとがき
SIMD命令の対応とデスクトップOpenGLからOpenGLESへの切り替えは、デスクトップアプリをWebGLに移植するときに限らず、デスクトップアプリをAndroid, iOS(OpenGLESは非推奨になってしまいましたが...)に移植するときにも有用ではないかと考えています。
この記事が、OpenSiv3D Android版やiOS版の開発が進むきっかけになればという他力本願で本稿を締めさせていただきます。最後まで読んでくださり、ありがとうございました。
-
WebAssembly/simd, "WebAssembly 128-bit packed SIMD Extension", https://github.com/WebAssembly/simd/blob/master/proposals/simd/SIMD.md, 2020/12/ 7 閲覧 ↩
-
Chrome Support Status, "Feature: WebAssembly SIMD", https://www.chromestatus.com/feature/6533147810332672, 2020/12/ 7 閲覧 ↩
-
Khronos Group, "WebGL 2.0 Specification", https://www.khronos.org/registry/webgl/specs/latest/2.0/#5.5, 2020/12/ 7 閲覧 ↩
-
Khronos Group, "glDrawElementsBaseVertex", https://www.khronos.org/registry/OpenGL-Refpages/es3/html/glDrawElementsBaseVertex.xhtml, 2020/12/ 7 閲覧 ↩
-
MDN Web Docs, "SharedArrayBuffer", https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer, 2020/12/ 7 閲覧 ↩
-
RYOSKATE, "OpenSiv3DのWebブラウザ版をEmscriptenで作ろうとした話", https://qiita.com/RYOSKATE/items/8644e711bf331d978551#メインループ, 2020/12/ 7 閲覧 ↩