Open6

WebGL-Native: Node.jsから呼び出す準備

okuokuokuoku

prev: https://zenn.dev/okuoku/scraps/9e2926081051f7
next: https://zenn.dev/okuoku/scraps/4529390fd6ff82

作戦

前回まででWebGLの各メソッドをC言語APIに開くことができたので、これを ffi-napi とか fastcall のようなモジュールを利用してNode.jsから呼ぶのが次の目標となる。

https://www.npmjs.com/package/ffi-napi

https://www.npmjs.com/package/fastcall

デプロイの容易性を考えるとN-APIを使用したモジュールの方が好ましいので今回は ffi-napi を使うことになりそう。自動的にC側のオブジェクトを解放する際に必要になる弱参照は別モジュールの weak-napi で提供されている。

https://www.npmjs.com/package/weak-napi

というわけで、Node.js側のライブラリは揃ってそうなので、それ用のDLLを作るのが次の目標となる。

やること

  1. 絵出し 。そもそもOpenGL ES2のコードで絵が出てこないとどうしようもない。
  2. コンテキスト作成APIの実装 。DOMを持たないので、 createElement に相当するAPIは新規設計してやる必要がある。
  3. DLL化 。Shared libraryにして絵出しアプリと同等の描画をDLL呼び出しで得る。
okuokuokuoku

とりあえず静的リンクで絵出し

APIトレースの都合で、PowerVRのOpenGLエミュレータを使った。システムのOpenGL ES2(IntelのオンボードGPUが提供するES2コンテキスト)ではEGLが使われないので、OpenGL ES用のツールではAPIトレースできない。 ... そしてIntelのツールはWindows版ではOpenGLをサポートしてない。Android版は有るのに。。

        /* Draw something */
        cwgl_viewport(NULL, 0, 0, w, h);
        cwgl_clearColor(NULL, col, col, col, 1.0f);
        cwgl_clear(NULL, 0x4000 /* COLOR BUFFER BIT */);

こんな感じのコードで描画ができた。実際には、これらの cwgl_ APIをJavaScript上のWebGLにwrapし、Node.jsから呼べるようにするのが最終目標となる。

APIトレースとかはarticleに分けた。

https://zenn.dev/okuoku/articles/dccb1d0587ba57

CMakeビルドへの統合

今回もビルドにはCMakeを使っている。

SDL2

今回はプラットフォーム抽象化レイヤとして一旦SDL2を使うことにした。使いなれてるし、後で置き換えるのも簡単なので。

SDL2はVulkan SDK付属のものを使用している。インストーラは環境変数 VK_SDK_PATH を設定してくれるのでパスの選択にはそれが使える。(この変数は後方互換用なので新しいほうに置き換える ★)

    file(TO_CMAKE_PATH $ENV{VK_SDK_PATH} vksdk)
    if(EXISTS ${vksdk})
        set(sdl2_inc ${vksdk}/Third-Party/Include/SDL2)
        set(sdl2_lib ${vksdk}/Third-Party/Bin)
        set(sdl2_dll ${vksdk}/Third-Party/Bin/SDL2.dll)
    endif()

GLES2エミュレータ

ちょっとダサいがパス決めうちにした。PowerVRのサイトからPVRVFrameとPVRCarbonをダウンロードしてインストールしておく必要がある。(レジストリとか見るのが正攻法だと思うがどうせ一時的だし面倒なのでやらない)

    file(TO_CMAKE_PATH "C:/Imagination Technologies/PowerVR_Graphics/PowerVR_Tools" pvrroot)
    if(EXISTS ${pvrroot})
        # PVRVFrame 
        set(emu_lib "${pvrroot}/PVRVFrame/Library/Windows_x86_64")
        # PVRCarbon
        set(es2cap_lib "${pvrroot}/PVRCarbon/Recorder/GLES/Windows_x86_64")
    endif()

DLLのコピー

これらのDLLはPATHに入っていないので、POST_BUILD イベントで適当にDLLをコピーしてやる。

    add_custom_command(TARGET testapp
        POST_BUILD
        # Deploy SDL2.dll, GLES2
        COMMAND ${CMAKE_COMMAND} -E copy 
        ${sdl2_dll} 
        ${emu_lib}/libEGL.dll
        ${emu_lib}/libGLESv2.dll
        ${CMAKE_CURRENT_BINARY_DIR}
        )

Windows上のSDL2でOpenGL ES2エミュレータを使う

SDL2のデフォルトでは、ESコンテキストを要求してもデスクトップOpenGLのOpenGL ES2コンテキストを使用してしまうため、Hint SDL_HINT_OPENGL_ES_DRIVERを設定してやる必要がある。

    SDL_SetHint(SDL_HINT_OPENGL_ES_DRIVER, "1");
okuokuokuoku

RenderDocでAPIトレースする

... RENDERDOC_HOOK_EGL=0 すれば普通にOpenGLレベルのトレースができた。。(SDLがEGLをwrapしてしまうため、素のキャプチャはうまくいかない)

じゃぁキャプチャの統合は要らないかな。。

okuokuokuoku

DLL化

https://github.com/okuoku/cwgl-proto/commit/29fd753d89bf9cf060d96c18aa8a70f433263935

Win32のDLL(いわゆるshared library)としてC関数を公開するには、DLLのビルド時に __declspec(dllexport) を付けてビルドし、DLLを使うアプリのビルド時は __declspec(dllimport) を付けてビルドする必要がある。

このために、全てのC関数は最初から CWGL_API マクロを先頭に付けて宣言してあった。

DLLを呼び出すコードはSDLやOpenGLの関数は一切呼ぶ必要がなくなる。

    for(;;){
        cwgl_ctx_frame_begin(ctx);

        float step = frame % 256;
        float col = 1.0f * step / 256.0f;

        /* Draw something */
        cwgl_viewport(ctx, 0, 0, w, h);
        cwgl_clearColor(ctx, col, col, col, 1.0f);
        cwgl_clear(ctx, 0x4000 /* COLOR BUFFER BIT */);

        cwgl_ctx_frame_end(ctx);
        frame ++;
    }
okuokuokuoku

Node.jsへの移植

↑ の C コードをNode.jsに移植するとこのようになる:

for(;;){
    let step = frame % 256;
    let col = 1.0 * step / 256.0;

    CWGL.cwgl_ctx_frame_begin(ctx);

    CWGL.cwgl_viewport(ctx, 0, 0, w, h);
    CWGL.cwgl_clearColor(ctx, col, col, col, 1.0);
    CWGL.cwgl_clear(ctx, 0x4000 /* COLOR BUFFER BIT */);
    
    CWGL.cwgl_ctx_frame_end(ctx);
    frame++;
}

C言語へのブリッジ CWGL は、npmのライブラリ ffi-napi の手続きを使って生成できる。

// Types
const cwglCtx = REF.refType(REF.types.void);

const libdef = {
    cwgl_init: [ "int", []],
    cwgl_ctx_frame_begin: ["void", [cwglCtx]],
    cwgl_ctx_frame_end: ["void", [cwglCtx]],
    cwgl_ctx_create: [ cwglCtx, ["int","int","int","int"]],
    cwgl_viewport: [ "void" , [cwglCtx, "int","int","int","int"]],
    cwgl_clearColor: ["void", [cwglCtx, "float","float","float","float"]],
    cwgl_clear: ["void", [cwglCtx, "int"]]
};

const CWGL = FFI.Library("../out/build/x64-Debug/cwgl.dll", libdef);

... 今 cwgl_ APIは165個あるが、それら全部について宣言を用意する必要がある。。