zig で OpenGL、そして wasm

動くコード。
<https://github.com/ousttrue/zig-opengl-wasm>

- [github action で作った wasm](https://ousttrue.github.io/zig-opengl-wasm/)
- [microui を zig に移植して wasm化](https://ousttrue.github.io/microui/)

* zig で OpenGL する(Windows11 + GLFW `@cImport` の使い方, c のライブラリとのリンクの仕方)
* engine を dll に分離する(zig で dll を作る方法。`export`, `extern` )
* engine を wasm 化する( `target = wasm32-freestanding` )

という記事です。

* engine 部分を単一のコードで desktop と wasm 共用にできる！

# 更新

- 202304 `zig-0.11.0-dev.2336`
- `zig 0.10.0-dev.3952+9e070b653`

# 実行環境

- zig-0.11.0-dev.2336 に更新
- glfw を cmake で dynamic library 化するのをやめて、zig のプロジェクトに直接組み込むように構成を変更
- [desktop] Ubuntu22.04 動いた
- [desktop] Windows11 動いた
- [desktop] wsl 微妙に動かない。OpenGL が真っ黒
- [wasm]ubuntu 22.04 + firefox 動いた
- [wasm]Windows11 + chrome 動いた

# その1: GLFW window を glClear

<https://www.glfw.org/documentation.html>

を移植する。

## zig のプロジェクト作成
```
$ mkdir zig-opengl-wasm # project-root
$ cd zig-opengl-wasm
zig-opengl-wasm$ mkdir desktop
zig-opengl-wasm$ cd desktop
zig-opengl-wasm/desktop $ zig init-exe
```

## GLFW のソースを `build.zig` に取り込む

```
zig-opengl-wasm/desktop$ git submodule add https://github.com/glfw/glfw.git
```

```zig:build.zig
// exe.install の前に追加

// glfw
exe.linkLibC(); // System ライブラリを有効にする。WindowsKits などへの Include も有効になる
exe.addIncludePath("glfw/include"); // @cInclude がヘッダーを見つけられるように
exe.addCSourceFiles // glfw のソースとコンパイルフラグ
exe.linkSystemLibrary("OpenGL32"); // 必要なものを適当に
exe.linkSystemLibrary("Gdi32");
``

https://github.com/ousttrue/zig-opengl-wasm/blob/801422e310f1fd68b260ddb4ca3506f7ec00fb10/desktop/build.zig#L25-L50

## 関数を `@cImport` 経由で呼び出し

- exe.linkLibC()
- exe.addIncludePath("glfw/include");

でコンパイルできる。

- exe.linkLibC()
- exe.linkSystemLibrary("Gdi32");

でリンクできる。

```zig:main.zig
const std = @import("std");
const c = @cImport({
    @cInclude("GLFW/glfw3.h");
});

pub fn main() !void {
    std.debug.assert(c.glfwInit() != 0);
    defer c.glfwTerminate();
}
```

## vscode のデバッグ設定例

```.vscode/launch.json
{
    // Use IntelliSense to learn about possible attributes.
    // Hover to view descriptions of existing attributes.
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "name": "desktop",
            "type": "cppvsdbg",
            "request": "launch",
            "program": "${workspaceFolder}/zig-out/bin/desktop",
            "args": [],
            "stopAtEntry": false,
            "cwd": "${workspaceFolder}",
            "environment": [
                {
                    "name": "PATH",
                    "value": "${workspaceFolder}\\build\\src\\Debug;${env:PATH}" // dll にパスを通す
                }
            ],
            "console": "integratedTerminal"
        }
    ]
}
```

実行して 終了コードが `0` になれば成功。
dll に見つからなかったりすると `0` 以外が帰って異常終了になる。

## glClear まで

<https://www.glfw.org/documentation.html>

を移植。

```zig:main.zig
const std = @import("std");
const c = @cImport({
    @cInclude("GLFW/glfw3.h");
});

pub fn main() !void {
    // Initialize the library
    std.debug.assert(c.glfwInit() == 1);
    defer c.glfwTerminate();

    // Create a windowed mode window and its OpenGL context
    const window = c.glfwCreateWindow(640, 480, "Hello World", null, null);
    defer c.glfwDestroyWindow(window);

    // Make the window's context current
    c.glfwMakeContextCurrent(window);

    // Loop until the user closes the window
    while (c.glfwWindowShouldClose(window) == 0) {
        // Render here
        c.glClear(c.GL_COLOR_BUFFER_BIT);

        // Swap front and back buffers
        c.glfwSwapBuffers(window);

        // Poll for and process events
        c.glfwPollEvents();
    }
}
```

実行。黒い glfw Window が出れば成功。

# その2: glsl で triangle

<https://www.glfw.org/docs/latest/quick_guide.html#quick_example>

を移植する。
OpenGL4 の glsl を使うので、glad で関数をロードする必要がある。

```zig:main.zig
const std = @import("std");
const c = @cImport({
    @cInclude("glad/gl.h");
    @cDefine("GLFW_INCLUDE_NONE", &.{});
    @cInclude("GLFW/glfw3.h");
});

const Vertex = std.meta.Tuple(&.{ f32, f32, f32, f32, f32 });
const vertices = [3]Vertex{
    .{ -0.6, -0.4, 1.0, 0.0, 0.0 },
    .{ 0.6, -0.4, 0.0, 1.0, 0.0 },
    .{ 0.0, 0.6, 0.0, 0.0, 1.0 },
};

// 外部ファイルを読み込んで `[]const u8` 化する。テキストやバイナリなんでもOK
const vertex_shader_text: [*:0]const u8 = @embedFile("./shader.vs");
const fragment_shader_text: [*:0]const u8 = @embedFile("./shader.fs");

pub fn main() anyerror!void {

    // Initialize the library
    std.debug.assert(c.glfwInit() == 1);
    defer c.glfwTerminate();

    // Create a windowed mode window and its OpenGL context
    const window = c.glfwCreateWindow(640, 480, "Hello World", null, null);
    std.debug.assert(window != null);
    defer c.glfwDestroyWindow(window);

    // Make the window's context current
    c.glfwMakeContextCurrent(window);
    _ = c.gladLoadGL(c.glfwGetProcAddress); // glad による OpenGL 初期化
    c.glfwSwapInterval(1);

    var vertex_buffer: c.GLuint = undefined;
    c.glGenBuffers(1, &vertex_buffer);
    c.glBindBuffer(c.GL_ARRAY_BUFFER, vertex_buffer);
    c.glBufferData(c.GL_ARRAY_BUFFER, @sizeOf(@TypeOf(vertices)), &vertices, c.GL_STATIC_DRAW);

    const vertex_shader = c.glCreateShader(c.GL_VERTEX_SHADER);
    c.glShaderSource(vertex_shader, 1, &vertex_shader_text, null);
    c.glCompileShader(vertex_shader);

    const fragment_shader = c.glCreateShader(c.GL_FRAGMENT_SHADER);
    c.glShaderSource(fragment_shader, 1, &fragment_shader_text, null);
    c.glCompileShader(fragment_shader);

    const program = c.glCreateProgram();
    c.glAttachShader(program, vertex_shader);
    c.glAttachShader(program, fragment_shader);
    c.glLinkProgram(program);

    const mvp_location = c.glGetUniformLocation(program, "MVP");
    const vpos_location = c.glGetAttribLocation(program, "vPos");
    const vcol_location = c.glGetAttribLocation(program, "vCol");

    c.glEnableVertexAttribArray(@intCast(c_uint, vpos_location));
    c.glVertexAttribPointer(@intCast(c_uint, vpos_location), 2, c.GL_FLOAT, c.GL_FALSE, @sizeOf(Vertex), null);
    c.glEnableVertexAttribArray(@intCast(c_uint, vcol_location));
    c.glVertexAttribPointer(@intCast(c_uint, vcol_location), 3, c.GL_FLOAT, c.GL_FALSE, @sizeOf(Vertex), @intToPtr(*anyopaque, @sizeOf(f32) * 2));

    // Loop until the user closes the window
    while (c.glfwWindowShouldClose(window) == 0) {
        var width: c_int = undefined;
        var height: c_int = undefined;
        c.glfwGetFramebufferSize(window, &width, &height);
        // ratio = width / (float) height;

        // Render here
        c.glViewport(0, 0, width, height);
        c.glClear(c.GL_COLOR_BUFFER_BIT);

        var mvp = [_]f32{
            1, 0, 0, 0, //
            0, 1, 0, 0, //
            0, 0, 1, 0, //
            0, 0, 0, 1, //
        };

        c.glUseProgram(program);
        c.glUniformMatrix4fv(mvp_location, 1, c.GL_FALSE, &mvp);
        c.glDrawArrays(c.GL_TRIANGLES, 0, 3);

        // Swap front and back buffers
        c.glfwSwapBuffers(window);
        // Poll for and process events
        c.glfwPollEvents();
    }
}
```

```zig:build.zig
// exe.linkSystemLibrary("OpenGL32"); // glad 使うので不要
// glad
exe.addIncludePath("glfw/deps");
exe.addCSourceFile("glfw/deps/glad_gl.c", &.{});
```

実行。三角形が出れば成功。

# その3: wasm 化準備

以下のように Engine(OpenGL描画) 部分を `dll / wasm` なライブラリとする。

```
  +---------+
  |Engine   |同じ zig ソースから dll と wasm にビルドする
  +---------+
  ^         ^
  |dll      |wasm
+-------+ +-------+
|Desktop| |Browser|
|GLFW   | |WebGL  |
+-------+ +-------+
```

## zig で lib プロジェクトを作る

```
zig-opengl-wasm$ mkdir engin
zig-opengl-wasm$ cd engin
zig-opengl-wasm/desktop $ zig init-lib
```

dll には下記の2つの関数を実装する。

`extern fn ENGINE_init(p: *const anyopaque) callconv(.C) void;`
`extern fn ENGINE_render(width: c_int, height: c_int) callconv(.C) void;`

## GLFW側

OpenGL しない。Window 管理のみ。

```zig:desktop/src/main.zig
const std = @import("std");
const c = @cImport({
    @cDefine("GLFW_INCLUDE_NONE", &.{});
    @cInclude("GLFW/glfw3.h");
});

// engine の dll の関数宣言。body 無し
extern fn ENGINE_init(p: *const anyopaque) callconv(.C) void;
extern fn ENGINE_render(width: c_int, height: c_int) callconv(.C) void;

pub fn main() anyerror!void {

    // Initialize the library
    std.debug.assert(c.glfwInit() == 1);
    defer c.glfwTerminate();

    // Create a windowed mode window and its OpenGL context
    const window = c.glfwCreateWindow(640, 480, "Hello World", null, null);
    std.debug.assert(window != null);
    defer c.glfwDestroyWindow(window);

    // Make the window's context current
    c.glfwMakeContextCurrent(window);
    ENGINE_init(c.glfwGetProcAddress);
    c.glfwSwapInterval(1);

    // Loop until the user closes the window
    while (c.glfwWindowShouldClose(window) == 0) {
        // Poll for and process events
        c.glfwPollEvents();

        var width: c_int = undefined;
        var height: c_int = undefined;
        c.glfwGetFramebufferSize(window, &width, &height);
        // ratio = width / (float) height;

        ENGINE_render(width, height);

        // Swap front and back buffers
        c.glfwSwapBuffers(window);
    }
}
```

## dll の実装

OpenGL だけやる。GLFW は見えない。

```zig:engine/src/main.zig
const std = @import("std");
const c = @cImport({
    @cInclude("glad/gl.h");
});

const Vertex = std.meta.Tuple(&.{ f32, f32, f32, f32, f32 });
const vertices = [3]Vertex{
    .{ -0.6, -0.4, 1.0, 0.0, 0.0 },
    .{ 0.6, -0.4, 0.0, 1.0, 0.0 },
    .{ 0.0, 0.6, 0.0, 0.0, 1.0 },
};

const vertex_shader_text: [*:0]const u8 = @embedFile("./shader.vs");
const fragment_shader_text: [*:0]const u8 = @embedFile("./shader.fs");

var program: u32 = undefined;
var mvp_location: c_int = undefined;

export fn ENGINE_init(p: *anyopaque) callconv(.C) void {
    _ = c.gladLoadGL(@ptrCast(?*const fn([*c]const u8) callconv(.C) ?*const fn() callconv(.C) void, p));

    var vertex_buffer: c.GLuint = undefined;
    c.glGenBuffers(1, &vertex_buffer);
    c.glBindBuffer(c.GL_ARRAY_BUFFER, vertex_buffer);
    c.glBufferData(c.GL_ARRAY_BUFFER, @sizeOf(@TypeOf(vertices)), &vertices, c.GL_STATIC_DRAW);

    const vertex_shader = c.glCreateShader(c.GL_VERTEX_SHADER);
    c.glShaderSource(vertex_shader, 1, &vertex_shader_text, null);
    c.glCompileShader(vertex_shader);

    const fragment_shader = c.glCreateShader(c.GL_FRAGMENT_SHADER);
    c.glShaderSource(fragment_shader, 1, &fragment_shader_text, null);
    c.glCompileShader(fragment_shader);

    program = c.glCreateProgram();
    c.glAttachShader(program, vertex_shader);
    c.glAttachShader(program, fragment_shader);
    c.glLinkProgram(program);

    mvp_location = c.glGetUniformLocation(program, "MVP");
    const vpos_location = c.glGetAttribLocation(program, "vPos");
    const vcol_location = c.glGetAttribLocation(program, "vCol");

    c.glEnableVertexAttribArray(@intCast(c_uint, vpos_location));
    c.glVertexAttribPointer(@intCast(c_uint, vpos_location), 2, c.GL_FLOAT, c.GL_FALSE, @sizeOf(Vertex), null);
    c.glEnableVertexAttribArray(@intCast(c_uint, vcol_location));
    c.glVertexAttribPointer(@intCast(c_uint, vcol_location), 3, c.GL_FLOAT, c.GL_FALSE, @sizeOf(Vertex), @intToPtr(*anyopaque, @sizeOf(f32) * 2));
}

export fn ENGINE_render(width: c_int, height: c_int) callconv(.C) void {
    // Render here
    c.glViewport(0, 0, width, height);
    c.glClear(c.GL_COLOR_BUFFER_BIT);

    var mvp = [_]f32{
        1, 0, 0, 0, //
        0, 1, 0, 0, //
        0, 0, 1, 0, //
        0, 0, 0, 1, //
    };

    c.glUseProgram(program);
    c.glUniformMatrix4fv(mvp_location, 1, c.GL_FALSE, &mvp);
    c.glDrawArrays(c.GL_TRIANGLES, 0, 3);
}
```

## openg gl 関数を extern 関数化

wasm 化したときの WebGL 呼び出しと、native glfw の OpenGL4(glad) 呼び出し共通化するために、
OpenGL のラッパーを作る。
ラッパーはただの `extern 関数` で、wasm のときは javascript の関数をリンクし、native ビルドのときは c の関数をリンクするようにできる。

```zig
extern fn some(int a) callconv(.C) int;
```

:::note info
https://github.com/MasterQ32/zig-opengl

こういう感じで OpenGL-Registry からコード生成して glad も自前にするとよいかも
:::

### js のリンク

```js
fucntion some_js(a){ return a + 1; }

// 初期化の引数
const importObject = {
    env {
       some: some_js,
    }
};

// wasm の初期化時に extern 関数をリンクする
const instance = await WebAssembly.instantiate(compiled, importObject);
```

### c のリンク

```c
int some(int a){ return a+1; }
```

```zig:build.zig
    if (target.cpu_arch != std.Target.Cpu.Arch.wasm32) { // <- デスクトップのときだけ glad から供給する
        // glad
        lib.linkLibC();
        lib.addCSourceFile("src/glad_placeholders.c", &.{}); // OpenGL ラッパー関数の本体を記述。glad の関数に呼び替える
    }
```

### gl のラッパーを作成

```zig:build.zig
const std = @import("std");

pub fn build(b: *std.build.Builder) void {
    const target = b.standardTargetOptions(.{});

    // Standard release options allow the person running `zig build` to select
    // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall.
    const mode = b.standardReleaseOptions();

    const lib = b.addSharedLibrary("engine", "src/main.zig", .unversioned);
    lib.setTarget(target);
    lib.setBuildMode(mode);

    if (target.cpu_arch != std.Target.Cpu.Arch.wasm32) { // <- デスクトップのときだけ glad から供給する
        // glad
        lib.linkLibC();
        lib.addIncludePath("../desktop/glfw/deps");
        lib.addCSourceFile("../desktop/glfw/deps/glad_gl.c", &.{});
        lib.addCSourceFile("src/glad_placeholders.c", &.{}); // <- これ
    }

    lib.install();

    const main_tests = b.addTest("src/main.zig");
    main_tests.setBuildMode(mode);

    const test_step = b.step("test", "Run library tests");
    test_step.dependOn(&main_tests.step);
}
```

ビルドして dll 版が動くのを確認する。

# その４ zig の wasm build

## build.zig を使う方法

:::note info
よく紹介されている方法で以下のようなコマンドラインを使う方法がある。

`$ zig build-lib -target wasm32-wasi src/main.zig`

`zig build` じゃなくて `zig build-lib` (`zig build-exe`) を使っている。
これは zig ファイルを一個ずつコンパイルする方法で、
`build.zig` が使われません。
`@cImport` や自前定義の `Pkg` が絡むと長いコマンドラインが必要になります。
:::

`build.zig` でターゲットを指定する方法を説明する。

```zig:build.zig
    const target = b.standardTargetOptions(.{});
    lib.setTarget(target);
```

により `-Dtarget=` が有効になります。
ターゲットには `wasm32-freestanding` を選択する。
`wasm32-wasi` などもできるが、今回は Import 関数を少なくしたいので `wasm32-freestanding` を採用します。

```
engine$ zig build -Dtarget=wasm32-freestanding
```

ビルド => `zig-out/lib/engine.wasm`
ビルド自体はさくっとできる。

## export シンボルの明示が必要

:::note alert

breaking change!

https://github.com/ziglang/zig/issues/14139

wasm ビルドに以下の対応が必要

```build.zig
        lib.rdynamic = true;
        lib.export_symbol_names = &[_][]const u8{
            "ENGINE_init",
            "ENGINE_render",
            "ENGINE_getGlobalInput",
        };
```

:::


# その5 chrome で wasm を実行

参考

<https://github.com/fabioarnold/hello-webgl>

## index.html

```html:index.html
<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <link rel="stylesheet" href="index.css">
    <script type="module" src="index.js"></script>
</head>

<body>
    <canvas id="gl"></canvas>
</body>

</html>
```

```css:index.css
html,
body {
    margin: 0;
    padding: 0;
    width: 100%;
    height: 100%;
    overflow: hidden;
}

canvas#gl {
    margin: 0;
    padding: 0;
    width: 100%;
    height: 100%;
}
```

## index.js

zig のとこよりこっちのほうが量が多いかも。


とりあえず wasm 動かしてみる

```js:index.js
const importObject = {
    env: {

    },
};

// get
const response = await fetch('zig-out/lib/engine.wasm') // index.html からの相対パスで
// byte array
const buffer = await response.arrayBuffer();
// compile
const compiled = await WebAssembly.compile(buffer);

// compile の次で debugger で止める
```

### compiled.Exports

4つあります。
zig で export した公開関数が 3つ と memory です。

```zig
export fn ENGINE_init(p: *anyopaque) callconv(.C) void;
export fn ENGINE_render(width: c_int, height: c_int) callconv(.C) void;
export fn ENGINE_getGlobalInput() callconv(.C) *u8; // 追加で作った固定バッファ。jsからwasmに文字列をコピーする
```

> `callconv(.C)` は `wasm` には無関係。あってもなくても動きます。desktop の dll 向けで　`extern "C"` に相当。

### compiled.Imports

18あります。
zig で extern 宣言した OpenGL 関数郡です。

<https://github.com/ousttrue/zig-opengl-wasm/blob/master/engine/src/gl.zig#L78-L123>

これらの関数は, js の instanciate の 引数として wasm に渡してやります。

```js
// instanciate env に webgl などを埋め込む
const instance = await WebAssembly.instantiate(compiled, importObject);
```

まだ、作っていないので以下のエラーが出ます。

```
Uncaught LinkError: WebAssembly.instantiate(): Import #0 module="env" function="genBuffers" error: function import requires a callable
```

# その6 importObject.env に webgl 関数を供給する

<https://github.com/ousttrue/zig-opengl-wasm/blob/master/engine/index.js#L115>

細かいところはソースを見てもらうとして、 WebGL と OpenGL4 のギャップを埋める方法について

## canvas から gl context を作って、それの関数を呼び出すクロージャー
愚直に OpenGL4 仕様で実装する。

* 文字列の0terminate依存
* int, uint
* pointer返し、値返し
* (void*)型のoffset

あたりは変えたほうが、zig での扱いが楽かもしれぬ。

## object と id の変換
js側で array に蓄えて、index を id に変換する。
0 が無効値を表すものは、1 origin になるように `id=index+1` とするとよい。

# wasm のメモリ空間

wasm のメモリ空間は WebAssembly.instantiate 後に確定して `instance.exports.memory.buffer` になる。
この中に、wasm の stack や heap, グローバル変数が全部入っている。

:::note info

build.zig でスタックサイズを指定できる。
```zig
const lib = b.addSharedLibrary("zig_renderer", "src/main.zig", .unversioned);
if (target.cpu_arch == std.Target.Cpu.Arch.wasm32) {
    lib.stack_size = 6 * 1024 * 1024;
}
```
:::

wasm のメモリ空間は `Uint8Array` で、
ポインタはそれに対する index である。

## js から メモリを書き換える

wasm の外からは `instance.exports.memory.buffer` のどこに何があるか分からないので、
書き込み先のポインターを wasm の export 関数からもらう。

```zig
// グローバル変数に固定長のバッファを確保する手抜き例
// malloc のような関数を公開する手もある
var buffer: [1024]u8 = undefined;
export fn ENGINE_getGlobalInput() callconv(.C) *u8 {
    return &buffer[0];
}
```

```js
const dstPtr = instance.exports.getGlobalAddress();
const memory = instance.exports.memory.buffer;
// memory[dstPtr] が zig の buffer[0] に相当する。あとは適当に内容を更新する
```

# その他
## zig の logger 出力を browser の console に接続する
早めにやっておくと print debug が捗る。

```zig
pub extern fn console_logger(level: c_int, ptr: *const u8, size: c_int) void;

fn extern_write(level: c_int, m: []const u8) error{}!usize {
    if (m.len > 0) {
        console_logger(level, &m[0], @intCast(c_int, m.len));
    }
    return m.len;
}

pub fn log(
    comptime message_level: std.log.Level,
    comptime scope: @Type(.EnumLiteral),
    comptime format: []const u8,
    args: anytype,
) void {
    if (builtin.target.cpu.arch == .wasm32) { // <- wasm のときだけ extern 関数にリダイレクトする
        const level = switch (message_level) {
            .err => 0,
            .warn => 1,
            .info => 2,
            .debug => 3,
        };
        const w = std.io.Writer(c_int, error{}, extern_write){
            .context = level,
        };
        w.print(format, args) catch |err| {
            const err_name = @errorName(err);
            extern_write(0, err_name) catch unreachable;
        };
        _ = extern_write(level, "\n") catch unreachable;
    } else {
        std.log.defaultLog(message_level, scope, format, args);
    }
}
```

## chrome wasm デバッガー

zig のソースにステップインしてステップ実行できた。

- https://chrome.google.com/webstore/detail/cc%20%20-devtools-support-dwa/pdcpmagijalfljmkmjngeonclgbbannb
- https://developer.chrome.com/blog/wasm-debugging-2020/

### github action で wasm ビルドして github-pages で動かす

- https://github.com/ousttrue/zig-opengl-wasm/blob/master/.github/workflows/wasm.yml

# 終わり

Three-js とか Babylon.js のようなものを zig で作れるような気がする。