Open11

WebGL1のAPIレビュー(1周目)

okuokuokuoku

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 。
okuokuokuoku

メソッド以外

https://gist.github.com/okuoku/fe88a3c390bab4a0678283ecd8deee7e/ab1bebce19b7e9fedb85042199f7cd8aadc19e13

もう300行を切った。基本的にWebIDL上の interface 類は、C API上では ..._ref ..._release のAPIでリファレンスカウントを手動で行うオブジェクトになる。

EmscriptenにはWebIDL binderが存在するが、これはC++的なクラスになってしまうので、C APIを目指す立場からはちょっと都合が悪い。

https://emscripten.org/docs/porting/connecting_cpp_and_javascript/WebIDL-Binder.html#webidl-binder

okuokuokuoku

あと 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するときは当然余計なオーバヘッドになってしまう。

今回は一旦愚直にオブジェクトで表現することにした ■ 。

okuokuokuoku

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);

あまり直感的でないポイントとして、 isTexturecreateTexture したものについて常に true を返却するわけではなく、bindまでは false を返す。

let tex = gl.createTexture();
let is_texture = gl.isTexture(tex); // → false

オブジェクトの寿命はもうちょっと複雑なトピックなので createProgram の方で後述。

okuokuokuoku

シェーダー関連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自体も結果を返却する。領域不足などの失敗も発生する可能性がある。

okuokuokuoku

テクスチャ / バッファのライフサイクルおよび設定

// 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);

シェーダと一緒。

okuokuokuoku

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画像の色空間変換)

okuokuokuoku

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させてしまって構わない。

okuokuokuoku

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);