WebGL1のAPIレビュー(1周目)
![okuoku](https://res.cloudinary.com/zenn/image/fetch/s--xsxQRr40--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/1e7fbed78b.jpeg)
prev: NONE
next: https://zenn.dev/okuoku/scraps/a8ffaa73862409
WebGL1のCバインディングを作りたいのでAPIレビューをする
EmscriptenはOpenGL ES2のエミュレーション層を提供するが、別に無くても良い気がするので。
1周目
目的: WebGL1のWebIDLをC APIに変換する変換ルールの構築
WASM MVPで動作しないといけないのでanyrefのような拡張は前提にできない。なのでWebGLのAPIにreference countingやJavaScript文字列配列 → C文字列 のようなAPI変換が必要になる。
また、EmscriptenはOpenGL ES2 → WebGL1の方向の変換を行っているが、今回やりたいのは逆方向の WebGL1 → OpenGL ES2にあたるためその点も注意する必要がある。
forkする調査項目
- JavaScript配列や文字列の一般的なC binding。QuickJSとかの組込みAPIをレビューすれば良いかな。
- リファレンスカウントでの Ref / Release APIの呼び方。例えばCairoだと Ref / Destroy https://www.cairographics.org/manual/cairo-cairo-t.html#cairo-reference だがAppleは Ref / Release だった気がする。 → 意外と直接的にリファレンスを扱えるJSエンジンが少い のでちょっと考える必要があるな。。でも明示的にscopeオブジェクトを作らせるのは明かにやりすぎだし。。 https://zenn.dev/okuoku/scraps/bda203e29f2d2a
-
<canvas>
や他のImageSourceとの結合の方法。一旦動作確認に十分な奴だけを用意するのが良さそう。- メモリ領域の bulk in / bulk out 。
![okuoku](https://res.cloudinary.com/zenn/image/fetch/s--xsxQRr40--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/1e7fbed78b.jpeg)
ES2の仕様書順に並び替え
ほぼ1年前じゃねぇか... enable
/ disable
が何故か規格書に見当らないんだよな。。リファレンスには当然存在するし、ES2以外の規格書にもある。
![okuoku](https://res.cloudinary.com/zenn/image/fetch/s--xsxQRr40--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/1e7fbed78b.jpeg)
メソッド以外
もう300行を切った。基本的にWebIDL上の interface
類は、C API上では ..._ref
..._release
のAPIでリファレンスカウントを手動で行うオブジェクトになる。
EmscriptenにはWebIDL binderが存在するが、これはC++的なクラスになってしまうので、C APIを目指す立場からはちょっと都合が悪い。
![okuoku](https://res.cloudinary.com/zenn/image/fetch/s--xsxQRr40--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/1e7fbed78b.jpeg)
(空行を整理)
先にやれよ
![okuoku](https://res.cloudinary.com/zenn/image/fetch/s--xsxQRr40--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/1e7fbed78b.jpeg)
あと 114 行...
TrivialなAPI
数値を入力し void
か数値を返すAPIはTrivialなAPIということで単純にC APIに変換できる。たとえば、
boolean isContextLost();
は、
int cwgl_isContextLost(cwgl_ctx_t* ctx); // → 0 for false, 1 for true
のようになる。
配列を受けとるAPIは、
void uniform4i(WebGLUniformLocation? location, GLint x, GLint y, GLint z, GLint w);
void uniform4iv(WebGLUniformLocation? location, Int32Array v);
void uniform4iv(WebGLUniformLocation? location, sequence<long> v);
↓
void cwgl_uniform4i(cwgl_ctx_t* ctx, cwgl_UniformLocation_t* location, int x, int y, int z, int w);
のように、最大に分解されたバリアントのみを残す。 本来のGLES2にはglUniform4iv
といった配列を渡すAPIがあるが 、ポインタを受け渡すAPIはバリデーションコストが高いのでなるべく避ける。
例外は行列を渡すAPIで、こればっかりは避けようがないのでポインタ渡しとなる。
void uniformMatrix4fv(WebGLUniformLocation? location, GLboolean transpose, sequence<GLfloat> value);
↓
void cwgl_uniformMatrix4fv(cwgl_ctx_t* ctx, cwgl_UniformLocation_t* location, int count, int transpose, float* value);
WebGLUniformLocation → cwgl_UniformLocation_t
簡単のため、オブジェクト WebGLUniformLocation
を先に処理してしまっている。この値は 本来のWebGLではオブジェクトだがGLES2ではただのint
であり 、ちょっと悩みどころとなっている。
JavaScript的なオブジェクトは C API ではポインタで表現されるべきだが、GLES2をwrapするときは当然余計なオーバヘッドになってしまう。
今回は一旦愚直にオブジェクトで表現することにした ■ 。
![okuoku](https://res.cloudinary.com/zenn/image/fetch/s--xsxQRr40--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/1e7fbed78b.jpeg)
is〜 API
// 6.1.4 Texture Queries
GLboolean isTexture(WebGLTexture? texture);
// 6.1.6 Buffer Object Queries
GLboolean isBuffer(WebGLBuffer? buffer);
// 6.1.7 Framebuffer Object and Renderbuffer Queries
GLboolean isFramebuffer(WebGLFramebuffer? framebuffer);
GLboolean isRenderbuffer(WebGLRenderbuffer? renderbuffer);
// 6.1.8 Shader and Program Queries
GLboolean isShader(WebGLShader? shader);
GLboolean isProgram(WebGLProgram? program);
is〜 APIは比較的trivialで、全て単純に変換できる。
int cwgl_isTexture(cwgl_ctx_t* ctx, cwgl_WebGLTexture_t* texture);
あまり直感的でないポイントとして、 isTexture
は createTexture
したものについて常に true
を返却するわけではなく、bindまでは false
を返す。
let tex = gl.createTexture();
let is_texture = gl.isTexture(tex); // → false
オブジェクトの寿命はもうちょっと複雑なトピックなので createProgram
の方で後述。
![okuoku](https://res.cloudinary.com/zenn/image/fetch/s--xsxQRr40--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/1e7fbed78b.jpeg)
シェーダー関連API
ライフサイクル
// 2.10.1 Loading and Creating Shader Source
// missing: ReleaseShaderCompiler
WebGLShader? createShader(GLenum type);
void compileShader(WebGLShader? shader);
void deleteShader(WebGLShader? shader);
// 2.10.3 Program Objects
WebGLProgram? createProgram();
void attachShader(WebGLProgram? program, WebGLShader? shader);
void detachShader(WebGLProgram? program, WebGLShader? shader);
void linkProgram(WebGLProgram? program);
void useProgram(WebGLProgram? program);
void deleteProgram(WebGLProgram? program);
WebGLでは事前コンパイルしたシェーダは使えないため、シェーダコンパイラはWebGLの利用期間全域で利用可能ということになっている。というわけで ReleaseShaderCompiler
に相当するオペレーションは無い。
元々がリファレンスカウント等の無いOpenGLから派生したAPIであるため、全ての create
には対応する delete
が存在する。ここで delete
するのはあくまでハンドルであるため、使用中(シェーダーの場合 attach
された後)は解放されず、後続のオペレーションもエラーにならない。ただし 新規の attach
は失敗する。
このライフサイクルを許容するためには deleteの他にreleaseも用意する必要がある 。つまり、
cwgl_Shader_t* cwgl_createShader(cwgl_ctx_t* ctx, int type);
void cwgl_deleteShader(cwgl_ctx_t* ctx, cwgl_Shader_t* shader);
void cwgl_Shader_release(cwgl_ctx_t* ctx, cwgl_Shader_t* shader);
ポイントは、 deleteShader
後でも cwgl_shader_t*
は有効なままで、他のAPIに渡すのが合法であるという点で、一度 createShader
で確保したオブジェクトは release
で解放しなければならない。
その他のWebGLでオブジェクトについても、オブジェクトを返却するAPIは対応する release
操作が必要となる。逆に、Cバインディングではリファレンスカウントを明示的に上げる acquire
操作は提供しない ■ 。
パラメタ
// 2.10.4 Shader Variables
WebGLActiveInfo? getActiveAttrib(WebGLProgram? program, GLuint index);
GLint getAttribLocation(WebGLProgram? program, DOMString name);
void bindAttribLocation(WebGLProgram? program, GLuint index, DOMString name);
WebGLUniformLocation? getUniformLocation(WebGLProgram? program, DOMString name);
WebGLActiveInfo? getActiveUniform(WebGLProgram? program, GLuint index);
GLのシェーダー処理系にはリフレクション機構が備え付けとなっており、パラメタはソースコード上の名前で指定できる。このため、APIは文字列を取ることになる。
API的には単純なC文字列引数で良い。つまり、
cwgl_UniformLocation_t* cwgl_getUniformLocation(cwgl_ctx_t* ctx, cwgl_Program_t* program, const char* name);
void cwgl_UniformLocation_release(cwgl_ctx_t* ctx, cwgl_UniformLocation_t*);
ソースコード
// 2.10.1 Loading and Creating Shader Source
void shaderSource(WebGLShader? shader, DOMString source);
// 2.10.5 Shader Execution
void validateProgram(WebGLProgram? program);
ソースコードはパラメタの一種と見做せる。
クエリ
// 6.1.8 Shader and Program Queries
sequence<WebGLShader>? getAttachedShaders(WebGLProgram? program);
WebGLShaderPrecisionFormat? getShaderPrecisionFormat(GLenum shadertype, GLenum precisiontype);
GLsizeiptr getVertexAttribOffset(GLuint index, GLenum pname); // GetVertexAttribPointer
配列のやりとりはなかなか難しい。。WebGLの仕様において、配列はuniformな配列のみを使用する(= 配列の各要素が全て同じ型である)。また、 取得操作に副作用がない 。このため、配列はバラしてオブジェクトを1つ1つ受けとった体で運用する ■ 。
副作用がないため、クライアント側に配列用のストレージを確保させ、不足していたらやりなおさせることができる。
cwgl_query_result_t cwgl_getAttachedShaders(cwgl_ctx_t* ctx, cwgl_Program_t* program, cwgl_Shader_t** out_shaders, unsigned int buflen, unsigned int* out_reslen);
ゼロ長の結果は一応合法であるため、それと失敗を区別するためにAPI自体も結果を返却する。領域不足などの失敗も発生する可能性がある。
![okuoku](https://res.cloudinary.com/zenn/image/fetch/s--xsxQRr40--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/1e7fbed78b.jpeg)
テクスチャ / バッファのライフサイクルおよび設定
// 3.7.13 Texture Objects
void bindTexture(GLenum target, WebGLTexture? texture);
void deleteTexture(WebGLTexture? texture);
WebGLTexture? createTexture(); // GenTextures
// 4.4.1 Binding and Managing Framebuffer Objects
void bindFramebuffer(GLenum target, WebGLFramebuffer? framebuffer);
void deleteFramebuffer(WebGLFramebuffer? framebuffer);
WebGLFramebuffer? createFramebuffer(); // genFramebuffers
// 4.4.3 Renderbuffer Objects
void bindRenderbuffer(GLenum target, WebGLRenderbuffer? renderbuffer);
void deleteRenderbuffer(WebGLRenderbuffer? renderbuffer);
WebGLRenderbuffer? createRenderbuffer(); // genRenderBuffers
void framebufferRenderbuffer(GLenum target, GLenum attachment, GLenum renderbuffertarget, WebGLRenderbuffer? renderbuffer);
void framebufferTexture2D(GLenum target, GLenum attachment, GLenum textarget, WebGLTexture? texture, GLint level);
シェーダと一緒。
![okuoku](https://res.cloudinary.com/zenn/image/fetch/s--xsxQRr40--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/1e7fbed78b.jpeg)
VRAM操作
// 2.9 Buffer Objects
void bindBuffer(GLenum target, WebGLBuffer? buffer);
void deleteBuffer(WebGLBuffer? buffer);
WebGLBuffer? createBuffer(); // genBuffers
void bufferData(GLenum target, GLsizeiptr size, GLenum usage);
void bufferData(GLenum target, BufferDataSource? data, GLenum usage);
void bufferSubData(GLenum target, GLintptr offset, BufferDataSource? data);
// 3.7.1 Texture Image Specification
void texImage2D(GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height, GLint border, GLenum format, GLenum type, ArrayBufferView? pixels);
void texImage2D(GLenum target, GLint level, GLenum internalformat, GLenum format, GLenum type, TexImageSource? source); // May throw DOMException
// 3.7.2 Alternate Texture Image Specification Commands
void copyTexImage2D(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width, GLsizei height, GLint border);
void texSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLenum type, ArrayBufferView? pixels);
void texSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLenum format, GLenum type, TexImageSource? source); // May throw DOMException
void copyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width, GLsizei height);
// 3.7.3 Compressed Texture Images
void compressedTexImage2D(GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height, GLint border, ArrayBufferView data);
void compressedTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, ArrayBufferView data);
// 4.3.1 Reading Pixels
void readPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, ArrayBufferView? pixels);
問題児。
TexImageSource の省略
テクスチャをVRAMにアップロードする texImage2D
が2つあることから判るように、これらはJavaScriptバイト配列(ArrayBufferView
)と、実際のDOM画像(TexImageSource
)の2種類のバリアントが存在する。ArrayBufferViewの方は明かにCPU側のRAMに載っているが、TexImageSourceの方は実際にDOM経由で画面に描画される関係上GPU側のRAMに載っている可能性があり、効率的に処理できる可能性があるため。
ただ、Emscriptenでビルドされたコード、要するにUnity WebGLの動作には不要なので一旦TexImageSourceのバリアントは省略する ■ 。
ArrayBufferView 引数
ArrayBufferViewの引数は、 const void*
(読み取り方向の場合) または void*
(書き込み方向の場合) および上限を示す size_t
に置き換えられる。
ただし、読み取り方向にせよ書き込み方向にせよ、指定されたArrayBufferViewのサイズが実際に必要なバッファサイズとは限らないため、配慮が必要かもしれない。
本来のOpenGLにはバッファ長の指定が存在しないが、WebGLの場合はバッファ長が不足した場合にはエラーを返却する必要があるため、指定が必要になる。
void cwgl_texImage2D(cwgl_ctx_t* ctx, int target, int level, int internalformat, int width, int height, int border, int format, int type, const void* pixels, size_t pixelslen);
void cwgl_readPixels(cwgl_ctx_t* ctx, int x, int y, int width, int height, int format, int type, void* pixels, size_t pixelslen);
WebGLではピクセルフォーマットとArrayのフォーマットは一致を強制しているため、各要素のアラインメントは問題にならない。
PixelStorei の追加機能
WebGLは通常のOpenGLに比べて追加機能を提供している。(Y flipおよびpremultiplied alpha、DOM画像の色空間変換)
![okuoku](https://res.cloudinary.com/zenn/image/fetch/s--xsxQRr40--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/1e7fbed78b.jpeg)
get〜 API (any返し)
// 6.1.1 Simple Queries
any getParameter(GLenum pname); // GetBooleanv / GetIntegerv / GetFloatv
// 6.1.3 Enumerated Queries
any getTexParameter(GLenum target, GLenum pname);
any getBufferParameter(GLenum target, GLenum pname);
any getFramebufferAttachmentParameter(GLenum target, GLenum attachment, GLenum pname);
any getRenderbufferParameter(GLenum target, GLenum pname);
// 6.1.8 Shader and Program Queries
any getShaderParameter(WebGLShader? shader, GLenum pname); // GetShaderiv
any getProgramParameter(WebGLProgram? program, GLenum pname); // GetProgramiv
any getVertexAttrib(GLuint index, GLenum pname);
any getUniform(WebGLProgram? program, WebGLUniformLocation? location);
JavaScriptのAPIらしく、OpenGLでは型付きだったリクエストは、WebGLでは型無しに改められている。
基本的にはユーザは事前にどの型のオブジェクトが結果として得られるか判っているため、型ごとに分解してしまえば良い。つまり、
cwgl_query_result_t cwgl_getParameter_int(cwgl_ctx_t* ctx, int pname, int* out); // Bool and Int
cwgl_query_result_t cwgl_getParameter_float(cwgl_ctx_t* ctx, int pname, float* out);
getUniform
は例外的に引数から返却値の型を決定できないが、そもそも通常のシチュエーションではユーザがセットした値を再度GLコンテキストに尋きにくるのは非効率なのでユーザにtry & errorさせてしまって構わない。
![okuoku](https://res.cloudinary.com/zenn/image/fetch/s--xsxQRr40--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/1e7fbed78b.jpeg)
get〜 API (文字列返し)
// 6.1.8 Shader and Program Queries
DOMString? getProgramInfoLog(WebGLProgram? program);
DOMString? getShaderInfoLog(WebGLShader? shader);
DOMString? getShaderSource(WebGLShader? shader);
JavaScriptの文字列はimmutableであるため、一度作成された文字列の内容や長さが変化することはない。というわけで、ユーザにバッファを用意させ、そこに受けとる方式とする。
cwgl_string_t* cwgl_getProgramInfoLog(cwgl_ctx_t* ctx, cwgl_Program_t* program);
size_t cwgl_string_len(cwgl_ctx_t* ctx, cwgl_string_t* str);
int cwgl_string_read(cwgl_ctx_t* ctx, cwgl_string_t* str, char* buf, size_t buflen);
void cwgl_string_release(cwgl_ctx_t* ctx, cwgl_string_t* str);
get〜 API (文字配列返し)
// Platform
sequence<DOMString>? getSupportedExtensions();
getAttachedShaders
と同様に処理する。
cwgl_query_result_t cwgl_getSupportedExtensions(cwgl_ctx_t* ctx, cwgl_string_t** str, size_t buflen, size_t* reslen);