📕 入門WebAssemblyを読んでWASMをはじめた

読んだ本

Rick Battagline 著

入門WebAssembly
The Art Of WebAssemblyの翻蚳曞です。

本曞を読みながらサンプルコヌドを写経したので感想を曞いおいきたす。
著者のWebサむトには他にもWebAssemblyに぀いおのトピックがありたす。
サンプルコヌドはGitHubから芋れたす。
canvasに3000個のobjectの衝突刀定をrenderingするサンプルを動かすずころたでやりたした。
https://wasmbook.com/collide.html

collide.html

きっかけ

RustでWASMのecosystemにふれおいく前に玠のWASMに぀いおなんずなくでも理解したいず思っおいたした。(ある皋床生成されたグルヌコヌド読めないず萜ち着かない)
本曞ではWATを曞いおWASMに倉換しお動かしながらWASMの仕様を远っおいくので、WASMだけを孊べるず思っお読んでみたした。

たずめ

  • 特にフレヌムワヌクを甚いずにWATを曞いおWASMに倉換しお、node/browserから動かせるようになりたした
  • WASMがスタックマシンずしお動䜜しおいるこずがわかりたした
  • 珟状のWASM(1.0)ずJavascriptの圹割分担がわかりたした
  • パフォヌマンスのチュヌニングや、デバッグ方法に぀いおも曞いおありたす

そもそもWASMずは

そもそもWebAssembly(WASM)ずはなにかずいう話なのですが、以䞋のように定矩されおいたす。

WebAssemblyずは、スタックマシン甚の仮想呜什セットアヌキテクチャ(Virtual ISA)。

WebAssembly (abbreviated Wasm) is a binary instruction format for a stack-based virtual machine. Wasm is designed as a portable compilation target for programming languages, enabling deployment on the web for client and server applications.

https://webassembly.org/

スタックマシン甚ずいうのは、CPU呜什にレゞスタヌがでおこないずいう意味で、垞に暗黙的に存圚するグロヌバルなスタックを操䜜するこずでデヌタを凊理しおいきたす。

たた、珟時点のMVP(Minimum Viable Product) 1.0では、JavaScript(React,Vue)を眮き換えるようなこずはできないし、意図されおもいないそうです。
このあたりもこれから芋おいくのですが、WASMからDOMを操䜜できないのでjsを眮き換えるずいうのはできなそうです。

WATずは

WebAssembly Text(WAT)は、WASMのアセンブリ蚀語(のようなもの)です。
WASM,WAT間で盞互に倉換できたす。

準備

以䞋を準備したす。

  • WASMの実行環境ずしおnode
  • WATをWASMに倉換するcli(wat2wasm)
❯ node -v
v16.13.2

nodeはv16を利甚したした、本曞では12.14.0が前提ずなっおいたす。
䜙談ですが、nodeのversion管理はnvmからRust補のfnmに切り替えたした。
nodeをinstallするずしたら以䞋のような感じです。

❯ cargo install fnm
❯ fnm install v16.13.2
❯ fnm default v16.13.2
❯ eval "$(fnm env)"

次にWATをWASMに倉換するためのwat2wasmをinstallしたす。

❯ npm install -g wat-wasm

❯ cat > file.wat  <<EOF
(module)
EOF

❯ wat2wasm file.wat

========================================================
  WAT2WASM
========================================================


  Need help?
  Contact Rick Battagline
  Twitter: @battagline
  https://wasmbook.com
  v1.0.43

no memory
Writing to file.wasm
WASM File Saved!

これでカレントディレクトリにfile.wasmが生成されるので䞭身を芋おみたす。
toolはなんでもよいのですが自分はhexylを利甚しおいたす。(cargo install hexyl)

❯ hexyl file.wasm
┌────────┬─────────────────────────┬─────────────────────────┬────────┬────────┐
│00000000│ 00 61 73 6d 01 00 00 00 ┊                         │0asm•000┊        │
└────────┮─────────────────────────┮─────────────────────────┮────────┮────────┘

WASMのBinary Formatに関する仕様でmoduleは以䞋のように定矩されおいたす。

magic   ::= 0x00 0x61 0x73 0x6D
version ::= 0x01 0x00 0x00 0x00
module  ::= magic
            version
            (省略)

ずいうこずで、先頭が0asmのMAGIC NUMBERで次の4byteがBinary Formatのversion 1になっおいたす。(61=a, 73=s, 6d=m)
WASMはリトル゚ンディアンなので、01が先頭にきおいたすね。
WATをWASMに倉換できおいるこずが確認できれば準備完了です。

WATのメンタルモデル

さっそくHello Worldに入りたいずころなのですが、WASMにはString型のデヌタ構造がなかったり、組み蟌み環境(実行環境)ずのimport/exportが最初はわかりづらかったりしたので、WATの曞き方から芋おいきたす。

WATでは暗黙的なグロヌパルのスタックを操䜜するこずで挔算や関数ずのやり取りを行いたす。
䟋えば定数の10ず20を加算するには

i32.const 10 ;; [ 10 ]
i32.const 20 ;; [ 10 20 ]
i32.add      ;; [ 30 ]

のように曞きたす。各業の呜什が実行されたあずのスタックの状態をコメントで曞いおありたす。
レゞスタヌを指定する呜什がないのがスタックマシヌン甚ずいうこずなんだろうず思いたす。
WATではもうひず぀、S expression(S匏)ずいう曞き方がサポヌトされおおり、䞊蚘の加算は以䞋のようにも曞けたす。

(i32.add (i32.const 10) (i32.const 20))

S匏ず通垞の蚘法は混圚させるこずができたす。
たた、WASMの実行単䜍であるmoduleもひず぀のS匏ずしお衚珟されたす。

Nodeからの動かし方

WATの曞き方がなんずなくわかったのでnodeから動かしおみたす。
node add.js 10 20のように匕数で䞎えられた数をWASMで加算しお結果を衚瀺する凊理を䜜っおいきたす。
WATの掚奚拡匵子はwatらしいです

addInt.wat

(module
    (func (export "addInt")                   ;; 1
    (param $value_1 i32) (param $value_2 i32) ;; 2
    (result i32)                              ;; 3

    local.get $value_1
    local.get $value_2
    i32.add                                   ;; 4
    )
)

たず、加算を行うWASM moduleを䜜成したす。
(module)はお決たりで必ず曞きたす。moduleはWASMにおける、deploy,loading,compileの単䜍です。 次にWASMでは関数単䜍で機胜を定矩しおいくので、jsから呌び出す関数を定矩したす。

  1. js偎からこの関数をaddIntずしお呌べるようになりたす。exportを倉なずころに定矩するなず思うかもしれたせんが、これはsyntactic sugarのようです
  2. 関数は二぀のi32型の匕数をずるこずを宣蚀しおいたす。
  3. 関数はi32型の結果を戻り倀ずしお返すこずを宣蚀しおいたす。
  4. $value_1ず$value_2の加算結果をstackに残しおおくこずで結果を返したす。

wat2wasm addInt.watで``addInt.wasm`が生成できれば完了です。
次にこのWASMを呌び出すjsを曞きたす。

addInt.js

const fs = require('fs');

const bytes = fs.readFileSync(__dirname + '/addInt.wasm');           // 1
const value_1 = parseInt(process.argv[2]);
const value_2 = parseInt(process.argv[3]);

(async () => {
  const obj = await WebAssembly.instantiate(new Uint8Array(bytes));  // 2
  let add_value = obj.instance.exports.addInt(value_1, value_2);     // 3
  console.log(`${value_1} + ${value_2} = ${add_value}`);
})();
  1. WASMをfileから読み蟌みたす。browserの堎合はここがfetch(ネットワヌク越し)になりたす。
  2. WASMをinstance化したす。メンタルモデル的にはここで、読み蟌んだwasmのmoduleの初期化凊理が走りたす。
  3. WASMからexportした関数はinstance.exportsに栌玍されおいるので呌び出したす。
❯ node addInt.js 1 2
1 + 2 = 3

jsからWASMを呌び出すこずができたした🎉
このようにWASM偎でexportした関数をjs偎から呌び出すこずで利甚したす。

Browserからの動かし方

次にWASMをbrowserから実行しおみたす。
WASMのbinaryを取埗しおinstantiateしたのちに、exportされた関数を呌び出すずいう基本的な流れは同じです。

たず、browserにWASMずhtmlをserveしたいので、http serverを建おられるようにしたす。(localのfileをserveできればなんでもよいです)

❯ npm install --save-dev connect serve-static

次に以䞋の内容のindex.htmlを䜜りたす。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name=”viewport” content=”width=device-width,initial-scale=1″>
    <title>Add Int</title>
    <script>
        let output = null;
        let addIntWasm;

        // 1
        let logAddInt = (a, b, sum) => {
            if (output == null) {
                console.log("page load not complete");
                return;
            }
            output.innerHTML += `${a} + ${b} = ${sum}<br>`;
        };

        // 2
        let importObject = {
            env: {
                logAddInt: logAddInt,
            }
        };

        (async () => {
            let obj = await WebAssembly.instantiateStreaming(fetch('addInt.wasm'), importObject); // 3
            addIntWasm = obj.instance.exports.addInt;                                             // 4
            let btn = document.getElementById("addIntButton");
            btn.style.display = "block";
        })();

        function onPageLoad() {
            (async () => {
                output = document.getElementById("output");
            })();
        }
    </script>
</head>
<body onLoad="onPageLoad()">
<input type="number" id="a_val" value="0"><br><br>
<input type="number" id="b_val" value="0"><br><br>
<button id="addIntButton" type="button" style="display:none"
        onclick="addIntWasm(
                document.getElementById('a_val').value,
                document.getElementById('b_val').value)">
    Add Values
</button>
<br>
<p id="output" style="float:left; width:200px; min-height:300px;">
</p>
</body>
</html>
  1. logAddIntはjsからwasmに枡す関数です。wasmから呌ばれたら結果をDOMに远蚘しおいきたす。
  2. jsからwasmに枡すobjectです。logAddIntはwasm偎ず䞀臎しおいる必芁がありたす。
  3. wasmのinstance化です。binaryをfetch()で取埗しおいたす。第二匕数でimport甚のobjectを枡したす。
  4. jsからcallするwasmの関数です。buttonのonclickに蚭定したす。

addInt.watを以䞋のように倉曎したす。

(module
    (import "env" "logAddInt" (func $logAddInt (param i32 i32 i32))) ;; 1
    (func (export "addInt")
    (param $value_1 i32) (param $value_2 i32)
    (local $sum i32)

    local.get $value_1
    local.get $value_2
    i32.add
    local.set $sum

    (call $logAddInt (local.get $value_1) (local.get $value_2) (local.get $sum)) ;; 2
    )
)
  1. wasmが組み蟌み環境(実行環境/host環境)から取埗する関数を宣蚀したす。
  2. importした関数の呌び出しです。

倉曎したら、wat2wasm addInt.watでwasmに倉換したす。

最埌にhtmlずwasmをserveするためのserver.jsを䜜成したす。

const connect = require('connect');
const serveStatic = require('serve-static');
connect().use(serveStatic(__dirname+"/")).listen(8080, function() {
  console.log('localhost:8080');
});

これで以䞋のようにserverを起動したのちbrowserでlocalhost:8080にアクセスしたす。

node server.js

[f:id:yamaguchi7073xtt:20220705044410p:plain]

このように組み蟌み環境(browser, node)ずwasm間ではimport/exportをお互いの関数を呌び合うこずができるこずが確かめられたした。

Hello World

WATをWASMに倉換しお組み蟌み環境(node,browser)から実行するこずができたので、Hello Worldをやっおみたす。
たず以䞋のWATを䜜成したす。

(module
    (import "env" "print_string" (func $print_string(param i32)))  ;; 1
    (import "env" "buffer" (memory 1))                             ;; 2
    (global $start_string (import "env" "start_string") i32)       ;; 3
    (global $string_len i32 (i32.const 12))
    (data (global.get $start_string) "hello world!")               ;; 4
    (func (export "helloworld")
        (call $print_string (global.get $string_len))              ;; 5
    )
)
  1. WASMからI/Oをするこずができないので、組み蟌み環境からhello world出力甚の関数をimportしたす。
  2. 線圢メモリを利甚するこずを宣蚀したす。実際のメモリはjs偎で確保しおWASMに枡したす。
  3. 線圢メモリのどの䜍眮にhello world文字列を生成するかをjs偎から指定したす。
  4. 指定された線圢メモリ䜍眮にUTF-8のbyte列を生成したす。
  5. importした文字列出力甚関数に出力する文字列の長さを枡したす。

jsずWASMの圹割分担がややこしいですが以䞋のようになっおいたす。

  • js偎
    • メモリ確保
    • 確保したメモリのどの䜍眮にhello worldを出力するかを指定
    • 出力する文字列の長さを匕数にずる関数をWASMに枡す
  • WASM偎
    • importしたメモリの指定された䜍眮に"hello world" UTF-8 byte列を生成
    • 出力甚関数に"hello world"の長さを匕数にしお呌び出す

js偎は以䞋のように䜜成したす。(helloworld.js)

const fs = require('fs');
const bytes = fs.readFileSync(__dirname + '/helloworld.wasm')

let hello_world = null;
let start_string_index = 200;                       // 1
let memory = new WebAssembly.Memory({initial: 1});  // 2

let importObject = {
  env: {
    buffer: memory,                                 // 3
    start_string: start_string_index,
    print_string: function (str_len) {
      // 4  
      const bytes = new Uint8Array(memory.buffer,
          start_string_index,
          str_len);
      const log_string = new TextDecoder('utf8').decode(bytes);
      console.log(log_string);
    }
  }
};

(async () => {
  let obj = await WebAssembly.instantiate(new Uint8Array(bytes), importObject);
  ({helloworld: hello_world} = obj.instance.exports);
  hello_world();
})();
  1. 開始200byte目にhello worldを出力するこずを指定
  2. WASMに枡すメモリを確保したす
  3. 確保したメモリをWASMに枡したす
  4. 確保したメモリ䜍眮をUTF8ずしお解釈したす
❯ node helloworld.js
hello world!

無事、hello world!がWASMでできたした🎉
たたここで登堎した線圢メモリ(WebAssembly.Memory)に぀いおは6章で詳しく説明されおいたす。
自分の理解ずしおはWASMずjs間で共有できるBufferで、ペヌゞ(64KB)単䜍で確保するものず考えおおりたす。
この線圢メモリの実甚的な利甚䟋は最埌のDOM操䜜でふれたす。

is_prime

Hello Worldが枈んだので、WATの制埡フロヌ(loop,if)を芋おいきたす。加算から䞀歩進んで、玠数を刀定するmoduleを䜜っおいきたす。
たずWATからですが長くなるので少し぀づみおいきたす。

(module
    ;; 偶数の刀定
    (func $even_check (param $n i32) (result i32)
        local.get $n          ;; [ n ]
        i32.const 2           ;; [ n 2 ]
        i32.rem_u             ;; [ 0 ]   | [ 1 ] 
        i32.const 0           ;; [ 0 0 ] | [ 1 0 ]
        i32.eq ;; $n % 2 == 0 ;; [ 1 ] | [ 0 ] 
    )
)

helper関数ずしお偶数を刀定するeven_checkを定矩したす。
コメントで呜什実行埌のスタックの様子を曞いおありたす。(|はたたはの意味です)
WASMにはデヌタ型ずしおbooleanがなく0以倖がtrue, 0がfalseずしお扱われたす。
rem_uは陀算の䜙りを出力したす。

    ;; 2ず等しいかの刀定
    (func $eq_2 (param $n i32) (result i32)
        local.get $n
        i32.const 2
        i32.eq
    )
    ;; n = m * q. nがmの倍数かの刀定。
    (func $multiple_check (param $n i32) (param $m i32) (result i32)
        local.get $n
        local.get $m
        i32.rem_u ;; $n % $m
        i32.const 0
        i32.eq
    )

次に2ず等しいか刀定するeq_2ず第䞀匕数が第二匕数の倍数かを刀定するmultiple_checkを定矩したす。

    ;; 玠数の刀定
    (func (export "is_prime") (param $n i32) (result i32)
        (local $i i32)

        ;; 1ず等しいかの刀定
        (if (i32.eq (local.get $n) (i32.const 1))
            (then
                i32.const 0
                return
            )
        )

        ;; 2ず等しいかの刀定
        (if (call $eq_2 (local.get $n))
            (then
                i32.const 1
                return
            )
        )

        (block $not_prime 
            (call $even_check (local.get $n))
            br_if $not_prime ;; 偶数なので玠数ではない

            (local.set $i (i32.const 1))

            (loop $prime_test_loop
                ;; $i += 2
                ;; teeはsetず同じだがstackをpopしない
                (local.tee $i
                    (i32.add (local.get $i) (i32.const 2)))

                local.get $n ;; stack = [ $i $n ]

                i32.ge_u ;; $i >= $n
                if
                    ;; $nを調べきったので玠数ず刀定
                    i32.const 1
                    return
                end
                ;; stack = [];

                ;; $nが$iの倍数なら玠数ではない
                (call $multiple_check (local.get $n) (local.get $i))
                br_if $not_prime

                ;; loopを繰り返す
                br $prime_test_loop
            ) ;; $prime_test_loop end
        )
        ;; br $not_prime jump here
        i32.const 0
    )

ここで、条件分岐(if)ずloopに぀いお簡単に解説したす。
ifは実行時のスタックの先頭を評䟡しおtrue(0以倖)ならendたでの呜什を実行したす。ここでは利甚しおいたせんがelseも曞けたす。
blockは少々わかりづらいのですが、br(branch)呜什でblockを抜け出すこずができたす。br_ifはスタックの先頭を評䟡しおtrueならbrする呜什です。䞊の䟋ではbr $not_primeでblock分を抜け出すので結果的にfalse(0)が戻り倀ずなりたす。

loopも盎感に反しお自動ではloopしおくれたせん。明瀺的にloopの先頭にjumpするbr呜什を利甚しおはじめおloopできたす。
䞊の䟋ではbr $prime_test_loopでloopの先頭に戻りたす。

local.tee $iは、スタックの先頭を$iに代入し぀぀、その倀をスタックに残したす(出力したす)
loop制埡甚のindex倉数をむンクリメントし぀぀、終了刀定する堎合によく䜿われおいたした。

このWATをWASMに倉換しお、呌び出すjsを䜜成したす。

const fs = require('fs');
const bytes = fs.readFileSync(__dirname + "/is_prime.wasm");
const value = parseInt(process.argv[2]);

(async () => {
  let obj = await WebAssembly.instantiate(new Uint8Array(bytes));
  if(!!obj.instance.exports.is_prime(value)) {
    console.log(`${value} is prime!`);
  } else {
    console.log(`${value} is NOT prime`);
  }
})();
❯ node is_prime.js 57
57 is NOT prime

無事刀定できたした。

DOM操䜜

最埌にcanvasを操䜜する䟋を芋おいきたす。
WASMからcanvasは操䜜できないので、canvasに描画するメモリをWASM偎で操䜜しおそれをjs偎でレンダリングするこずで実珟したす。
ここで䜜るのは、ある移動する耇数のオブゞェクトをレンダリングし、オブゞェクド同士が衝突しおいるかを刀定するWASMです。
オブゞェクトはx,y座暙ずx,yそれぞれの速床を保持したす。
WASMはcanvasに描画されるメモリ領域ずオブゞェクトの状態を管理する領域を管理したす。

メモリ領域の抂芁

たず、htmlは以䞋のようになりたす。

collide.html

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="widh=device-width,initial-scale=1.0">
  <title>Collision Detection</title>
</head>
<body>
  <canvas id="cnvs" width="512" height="512"></canvas>
  <script>
    const cnvs_size = 512;
    const no_hit_color = 0xff_00_ff_00 // green
    const hit_color = 0xff_00_00_ff; // red

    const pixel_count = cnvs_size * cnvs_size;

    const canvas = document.getElementById("cnvs");
    const ctx = canvas.getContext("2d");
    ctx.clearRect(0,0,512,512);

    const obj_start = pixel_count * 4;
    const obj_start_32 = pixel_count;
    const obj_size = 4;
    const obj_cnt = 3000;
    const stride_bytes = 16;

    const x_offset = 0;
    const y_offset = 4;
    const xv_offset = 8;
    const yv_offset = 12;

    const memory = new WebAssembly.Memory({ initial: 80 });
    const mem_i8 = new Uint8Array(memory.buffer);
    const mem_i32 = new Uint32Array(memory.buffer);

    const importObject = {
      env: {
        buffer: memory,

        cnvs_size: cnvs_size,
        no_hit_color: no_hit_color,
        hit_color: hit_color,
        obj_start: obj_start,
        obj_cnt: obj_cnt,
        obj_size: obj_size,

        x_offset: x_offset,
        y_offset: y_offset,
        xv_offset: xv_offset,
        yv_offset: yv_offset,
      }
    };

    const image_data = new ImageData(
        new Uint8ClampedArray(memory.buffer, 0, obj_start), cnvs_size, cnvs_size
    );

    const stride_i32 = stride_bytes / 4;
    for (let i = 0; i < obj_cnt * stride_i32; i += stride_i32) {
      let temp = Math.floor(Math.random() * cnvs_size);
      mem_i32[obj_start_32+i] = temp;

      temp = Math.floor(Math.random() * cnvs_size);
      mem_i32[obj_start_32+i + 1] = temp;

      temp = (Math.round(Math.random() * 4) -2 )
      mem_i32[obj_start_32 + i + 2] = temp;

      temp = (Math.round(Math.random() * 4) -2 )
      mem_i32[obj_start_32 + i + 3] = temp;
    }

    let animation_wasm;
    function animate() {
      animation_wasm();
      ctx.putImageData(image_data, 0,0);
      requestAnimationFrame(animate);
    }

    (async () => {
      let obj = await WebAssembly.instantiateStreaming(fetch('collide.wasm'), importObject);
      animation_wasm = obj.instance.exports.main;
      requestAnimationFrame(animate);
    })();

  </script>
</body>
</html>

canvasのframeを描画するたびに、WASM偎のmainを呌び出したす。
WASM偎はmainの䞭で二぀のこずを行いたす。

  1. objectの状態倉曎
  2. canvasの描画領域の曎新

WATは以䞋のようになりたす。

(module
    (global $cnvs_size (import "env" "cnvs_size") i32)

    (global $no_hit_color (import "env" "no_hit_color") i32)
    (global $hit_color (import "env" "hit_color") i32)
    (global $obj_start (import "env" "obj_start") i32)
    (global $obj_size (import "env" "obj_size") i32)
    (global $obj_cnt (import "env" "obj_cnt") i32)

    (global $x_offset (import "env" "x_offset") i32)
    (global $y_offset (import "env" "y_offset") i32)
    (global $xv_offset (import "env" "xv_offset") i32)
    (global $yv_offset (import "env" "yv_offset") i32)
    (import "env" "buffer" (memory 80))

    (func $clear_canvas
        (local $i i32)
        (local $pixel_bytes i32)

        global.get $cnvs_size
        global.get $cnvs_size
        i32.mul

        i32.const 4
        i32.mul

        local.set $pixel_bytes;; $pixel_bytes = $width * $height * 4

        (loop $pixel_loop
            (i32.store (local.get $i) (i32.const 0xff_00_00_00))

            (i32.add (local.get $i) (i32.const 4))
            local.set $i ;; $i += 4

            (i32.lt_u (local.get $i) (local.get $pixel_bytes))
            br_if $pixel_loop
        )
    )

    (func $abs
        (param $value i32)
        (result i32)

        (i32.lt_s (local.get $value) (i32.const 0))
        if
            i32.const 0
            local.get $value
            i32.sub
            return
        end
        local.get $value
    )

    (func $set_pixel
        (param $x i32)
        (param $y i32)
        (param $c i32)

        (i32.ge_u (local.get $x) (global.get $cnvs_size))
        if
            return
        end

        (i32.ge_u (local.get $y) (global.get $cnvs_size))
        if
            return
        end

        local.get $y
        global.get $cnvs_size
        i32.mul
        local.get $x
        i32.add

        i32.const 4
        i32.mul

        local.get $c
        i32.store
    )

    (func $draw_obj
        (param $x i32)
        (param $y i32)
        (param $c i32)

        (local $max_x i32)
        (local $max_y i32)

        (local $xi i32)
        (local $yi i32)

        local.get $x
        local.tee $xi

        global.get $obj_size
        i32.add
        local.set $max_x

        local.get $y
        local.tee $yi
        global.get $obj_size
        i32.add
        local.set $max_y

        (block $break (loop $draw_loop
            local.get $xi
            local.get $yi
            local.get $c
            call $set_pixel

            local.get $xi
            i32.const 1
            i32.add
            local.tee $xi

            local.get $max_x
            i32.ge_u

            if
                local.get $x
                local.set $xi

                local.get $yi
                i32.const 1
                i32.add
                local.tee $yi

                local.get $max_y
                i32.ge_u
                br_if $break
            end

            br $draw_loop
        ))
    )

    (func $set_obj_attr
        (param $obj_number i32)
        (param $attr_offset i32)
        (param $value i32)

        local.get $obj_number
        i32.const 16
        i32.mul

        global.get $obj_start
        i32.add
        local.get $attr_offset
        i32.add

        local.get $value
        i32.store
    )

    (func $get_obj_attr
        (param $obj_number i32)
        (param $attr_offset i32)
        (result i32)

        local.get $obj_number
        i32.const 16
        i32.mul

        global.get $obj_start
        i32.add
        local.get $attr_offset
        i32.add

        i32.load
    )

    (func $main (export "main")
        (local $i i32)
        (local $j i32)
        (local $outer_ptr i32)
        (local $inner_ptr i32)

        (local $x1 i32)
        (local $x2 i32)
        (local $y1 i32)
        (local $y2 i32)

        (local $xdist i32)
        (local $ydist i32)

        (local $i_hit i32)
        (local $xv i32)
        (local $yv i32)

        (call $clear_canvas)

        (loop $move_loop
            (call $get_obj_attr (local.get $i) (global.get $x_offset))
            local.set $x1

            (call $get_obj_attr (local.get $i) (global.get $y_offset))
            local.set $y1

            (call $get_obj_attr (local.get $i) (global.get $xv_offset))
            local.set $xv

            (call $get_obj_attr (local.get $i) (global.get $yv_offset))
            local.set $yv

            (i32.add (local.get $xv) (local.get $x1))
            i32.const 0x1ff ;; 511
            i32.and
            local.set $x1

            (i32.add (local.get $yv) (local.get $y1))
            i32.const 0x1ff ;; 511
            i32.and
            local.set $y1

            (call $set_obj_attr
                (local.get $i)
                (global.get $x_offset)
                (local.get $x1)
            )

            (call $set_obj_attr
                (local.get $i)
                (global.get $y_offset)
                (local.get $y1)
            )

            local.get $i
            i32.const 1
            i32.add
            local.tee $i
            global.get $obj_cnt
            i32.lt_u
            if
                br $move_loop
            end
        )

        i32.const 0
        local.set $i

        (loop $outer_loop (block $outer_break
            i32.const 0
            local.tee $j

            local.set $i_hit

            (call $get_obj_attr (local.get $i) (global.get $x_offset))
            local.set $x1

            (call $get_obj_attr (local.get $i) (global.get $y_offset))
            local.set $y1

            (loop $inner_loop (block $inner_break
                local.get $i
                local.get $j
                i32.eq

                if
                    local.get $j
                    i32.const 1
                    i32.add
                    local.set $j
                end

                local.get $j
                global.get $obj_cnt
                i32.ge_u
                if
                    br $inner_break
                end

                (call $get_obj_attr (local.get $j) (global.get $x_offset))
                local.set $x2
                (i32.sub (local.get $x1) (local.get $x2))
                call $abs
                local.tee $xdist

                global.get $obj_size
                i32.ge_u

                ;; 衝突しおいない
                if
                    local.get $j
                    i32.const 1
                    i32.add
                    local.set $j
                    br $inner_loop
                end

                (call $get_obj_attr (local.get $j) (global.get $y_offset))
                local.set $y2
                (i32.sub (local.get $y1) (local.get $y2))
                call $abs
                local.tee $ydist

                global.get $obj_size
                i32.ge_u

                ;; 衝突しおいない
                if
                    local.get $j
                    i32.const 1
                    i32.add
                    local.set $j
                    br $inner_loop
                end

                i32.const 1
                local.set $i_hit
            ))

            local.get $i_hit
            i32.const 0
            i32.eq
            if
                (call $draw_obj
                    (local.get $x1) (local.get $y1) (global.get $no_hit_color))
            else
                (call $draw_obj
                    (local.get $x1) (local.get $y1) (global.get $hit_color))
            end

            local.get $i
            i32.const 1
            i32.add
            local.tee $i
            global.get $obj_cnt
            i32.lt_u
            if
                br $outer_loop
            end
        ))
    )
)

長いですが、最埌のmainから芋おいくずずおも単玔な凊理をしおいるだけなのがわかりたす。

    (func $main (export "main")
        (local $i i32)
        (local $j i32)

        (local $x1 i32)
        (local $x2 i32)
        (local $y1 i32)
        (local $y2 i32)

        (local $xdist i32)
        (local $ydist i32)

        (local $i_hit i32)
        (local $xv i32)
        (local $yv i32)

        (call $clear_canvas)

たず、必芁なlocal倉数を宣蚀したす。
Frame毎に各オブゞェクトの状態を曎新するので、$iは珟圚の凊理察象のオブゞェクトのindexです。$jは各オブゞェクトずの衝突刀定をするためのinner loopのindexです。
最初に$clear_canvasを呌び出しおレンダリング領域をリセットしたす。

    (func $clear_canvas
        (local $i i32)
        (local $pixel_bytes i32)

        global.get $cnvs_size
        global.get $cnvs_size
        i32.mul

        i32.const 4
        i32.mul

        local.set $pixel_bytes;; $pixel_bytes = $width * $height * 4

        (loop $pixel_loop
            (i32.store (local.get $i) (i32.const 0xff_00_00_00))

            (i32.add (local.get $i) (i32.const 4))
            local.set $i ;; $i += 4

            (i32.lt_u (local.get $i) (local.get $pixel_bytes))
            br_if $pixel_loop
        )
    )

1pixel 4byteなので4byteず぀むンクリメントしながら、黒色(0xff_00_00_00)にしおいきたす。
次に

        (loop $move_loop
            (call $get_obj_attr (local.get $i) (global.get $x_offset))
            local.set $x1

            (call $get_obj_attr (local.get $i) (global.get $y_offset))
            local.set $y1

            (call $get_obj_attr (local.get $i) (global.get $xv_offset))
            local.set $xv

            (call $get_obj_attr (local.get $i) (global.get $yv_offset))
            local.set $yv

            (i32.add (local.get $xv) (local.get $x1))
            i32.const 0x1ff ;; 511
            i32.and
            local.set $x1

            (i32.add (local.get $yv) (local.get $y1))
            i32.const 0x1ff ;; 511
            i32.and
            local.set $y1

            (call $set_obj_attr
                (local.get $i)
                (global.get $x_offset)
                (local.get $x1)
            )

            (call $set_obj_attr
                (local.get $i)
                (global.get $y_offset)
                (local.get $y1)
            )

            local.get $i
            i32.const 1
            i32.add
            local.tee $i
            global.get $obj_cnt
            i32.lt_u
            if
                br $move_loop
            end
        )

凊理察象のオブゞェクトのx,y,xv,xyを取埗しお、それぞれの速床を加算したのち、メモリを曎新したす。
0x1ffずandをずるこずで、描画領域をはみでたオブゞェクトの䜍眮がリセットされるようになっおいたす。
ビット挔算でこんなこずができるのかず思いたした。本曞ではビット挔算に぀いおも䞁寧に解説されおおりたす。
ここたでで、frameの描画毎にオブゞェクトの䜍眮情報が曎新されるこずがわかりたした。
最埌に、オブゞェクトの衝突刀定を行い、描画領域を曎新したす。

        i32.const 0
        local.set $i

        (loop $outer_loop (block $outer_break
            i32.const 0
            local.tee $j

            local.set $i_hit

            (call $get_obj_attr (local.get $i) (global.get $x_offset))
            local.set $x1

            (call $get_obj_attr (local.get $i) (global.get $y_offset))
            local.set $y1

            (loop $inner_loop (block $inner_break
                local.get $i
                local.get $j
                i32.eq

                if
                    local.get $j
                    i32.const 1
                    i32.add
                    local.set $j
                end

                local.get $j
                global.get $obj_cnt
                i32.ge_u
                if
                    br $inner_break
                end

                (call $get_obj_attr (local.get $j) (global.get $x_offset))
                local.set $x2
                (i32.sub (local.get $x1) (local.get $x2))
                call $abs
                local.tee $xdist

                global.get $obj_size
                i32.ge_u

                ;; 衝突しおいない
                if
                    local.get $j
                    i32.const 1
                    i32.add
                    local.set $j
                    br $inner_loop
                end

                (call $get_obj_attr (local.get $j) (global.get $y_offset))
                local.set $y2
                (i32.sub (local.get $y1) (local.get $y2))
                call $abs
                local.tee $ydist

                global.get $obj_size
                i32.ge_u

                ;; 衝突しおいない
                if
                    local.get $j
                    i32.const 1
                    i32.add
                    local.set $j
                    br $inner_loop
                end

                i32.const 1
                local.set $i_hit
            ))

            local.get $i_hit
            i32.const 0
            i32.eq
            if
                (call $draw_obj
                    (local.get $x1) (local.get $y1) (global.get $no_hit_color))
            else
                (call $draw_obj
                    (local.get $x1) (local.get $y1) (global.get $hit_color))
            end

            local.get $i
            i32.const 1
            i32.add
            local.tee $i
            global.get $obj_cnt
            i32.lt_u
            if
                br $outer_loop
            end
        ))

オブゞェクトの衝突刀定は|x1 - x2| < object_size か぀ |y1 - y2| < object_sizeで刀定したす。 WATをWASMに倉換しお

node server.js

を実行しお、localhost:8080/collide.htmlにアクセスしおみるず以䞋のように描画されたした🎉

完成

ふれられなかったこず

本蚘事ではふれられたせんでしたが、本曞ではさらにここからWASMのパフォヌマンスチュヌニングやデバッグをおこなうための実甚的な知識が述べられおおりたす。