LoginSignup
6
3

More than 1 year has passed since last update.

zig で OpenGL、そして wasm

Last updated at Posted at 2022-09-10

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

  • 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
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

を移植。

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 で関数をロードする必要がある。

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();
    }
}
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 管理のみ。

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 は見えない。

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 の関数をリンクするようにできる。

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

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

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

js のリンク

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

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

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

c のリンク

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

gl のラッパーを作成

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 版が動くのを確認する。

その4 zig の wasm build

build.zig を使う方法

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

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

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

build.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 シンボルの明示が必要

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

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>
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 動かしてみる

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 です。

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 に渡してやります。

// 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, グローバル変数が全部入っている。

build.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 関数からもらう。

// グローバル変数に固定長のバッファを確保する手抜き例
// malloc のような関数を公開する手もある
var buffer: [1024]u8 = undefined;
export fn ENGINE_getGlobalInput() callconv(.C) *u8 {
    return &buffer[0];
}
const dstPtr = instance.exports.getGlobalAddress();
const memory = instance.exports.memory.buffer;
// memory[dstPtr] が zig の buffer[0] に相当する。あとは適当に内容を更新する

その他

zig の logger 出力を browser の console に接続する

早めにやっておくと print debug が捗る。

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 のソースにステップインしてステップ実行できた。

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

終わり

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

6
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
3