SVX日記

2004|04|05|06|07|08|09|10|11|12|
2005|01|02|03|04|05|06|07|08|09|10|11|12|
2006|01|02|03|04|05|06|07|08|09|10|11|12|
2007|01|02|03|04|05|06|07|08|09|10|11|12|
2008|01|02|03|04|05|06|07|08|09|10|11|12|
2009|01|02|03|04|05|06|07|08|09|10|11|12|
2010|01|02|03|04|05|06|07|08|09|10|11|12|
2011|01|02|03|04|05|06|07|08|09|10|11|12|
2012|01|02|03|04|05|06|07|08|09|10|11|12|
2013|01|02|03|04|05|06|07|08|09|10|11|12|
2014|01|02|03|04|05|06|07|08|09|10|11|12|
2015|01|02|03|04|05|06|07|08|09|10|11|12|
2016|01|02|03|04|05|06|07|08|09|10|11|12|
2017|01|02|03|04|05|06|07|08|09|10|11|12|
2018|01|02|03|04|05|06|07|08|09|10|11|12|
2019|01|02|03|04|05|06|07|08|09|10|11|12|
2020|01|02|03|04|05|06|07|08|09|10|11|12|
2021|01|02|03|04|05|06|07|08|09|10|11|12|
2022|01|02|03|04|05|06|07|08|09|10|11|12|
2023|01|02|03|04|05|06|07|08|09|10|11|12|
2024|01|02|03|04|

2023-10-12(Thu) WebAssembly $41

  偶然「RubyがWebAssemblyで動く」という記事を目にした。「WebAssembly」と聞くとブラウザ上で機械語が動くらしい印象を受けるが、具体的にはサッパリわからない。一般的なブラウザでRubyが動くなら実に喜ばしいことだが。しかし、Rubyを動かすためにはファイルシステムが……みたいな話になっている。よく理解できない。フムン。まずはRuby抜きでWebAssembly自体について学んでみたくなってきたな。

  そもそも、自分は8bitの頃にコンピュータを始めたクチで、その頃はBASICを理解したら、次は機械語というのが定番のルートだった。自分はSHARP党だったので、X1ではZ80、X68000ではMC68000、Oh!XキッカケでPICマイコンのアセンブラにも手を染めたことがあるが、そもそもWebAssemblyの「Assembly」はアセンブラの意味で正しいのだろうか?

  調べ始めると、どうもそれで正しいらしい。JavaScriptよりも高速な処理を目指したもので、そのバイナリは仮想マシンで処理されるものの、数倍の性能が得られるらしい。数倍……か。Z80の頃には、BASICと機械語では軽く数十倍以上の性能が得られたことに比べればずいぶん控えめな値ではあるが。

  しかし驚いたのは、あらゆるI/O機能が「ない」ことだ。つまり計算やメモリ操作しかできない。そんなもん何に使ったらいいのか。せっかく懐かしくアセンブラに手を染められるかと思ったのに、使い道のないものを学んでも仕方がない……ん? いや、あるッ! ひとつだけ思いついたぞ。うむ、じゃ、やってみるか。

  ということで、Web上でWebAssemblyの記事を探すと、CやRustから使う記事が多い。いや、そうじゃないんだ。せっかくなんだから、直接にニーモニック(mnemonic)を書きたいんだ。で、しつこく探すとそういう記事も多少は見つかる。

  なんにせよ、まずは動かしてみないことには面白くない。ちょっと調べると、WebAssemblyにおける「hello, world」は足し算をするものらしい。

;; https://developer.mozilla.org/ja/docs/WebAssembly/Understanding_the_text_format
(module
    (func (param $lhs i32) (param $rhs i32) (result i32)
        local.get   $lhs
        local.get   $rhs
        i32.add
    )
)

  そうそう。まずはニーモニックを知りたいんだよ。んが、WebAssemblyではニーモニックとは言わず、単にテキスト形式(text format)というらしい。拡張子は「wat」だ。

  watを書いたら、アセンブラでバイナリ形式に変換することになるが、Fedoraには関連ツールをまとめたパッケージが用意されている。「wabt」パッケージだ。

# dnf search webassem
wabt.x86_64 : The WebAssembly Binary Toolkit
# dnf install wabt

  いわゆるアセンブラに当たるのが「wat2wasm」だ。こうやってアセンブルする。

# wat2wasm add.wat

  バイナリ形式に変換後の拡張子は「wasm」だ。なるほど。バイナリに変換されている。

# ndump add.wasm
#Address    +0 +1 +2 +3 +4 +5 +6 +7  +8 +9 +A +B +C +D +E +F 'Character
#00000000 = 00 61 73 6D 01 00 00 00  01 07 01 60 02 7F 7F 01 '.asm....:...`....
#00000010 = 7F 03 02 01 00 0A 09 01  07 00 20 00 20 01 6A 0B '........:.. . .j.

  「wasm2wat」という逆アセンブラもある。

# wasm2wat add.wasm 
(module
  (type (;0;) (func (param i32 i32) (result i32)))
  (func (;0;) (type 0) (param i32 i32) (result i32)
    local.get 0
    local.get 1
    i32.add))

  なるほど。テキスト形式に戻っている。

  しかし、なにしろ動かしてみないことには面白くない。なんでも、wasmバイナリを動かすには、ブラウザでJavaScriptを実行し、そこから呼び出す形式になるらしい。まさに昔、BASICプログラムから機械語サブルーチンを呼び出す場合の「CALL」や「USR」と同じ形式ということだな。

  JavaScriptからWebAssemblyの関数を呼び出したい場合、WebAssembly側で関数を「export」する必要がある。WebAssembly観点での「輸出」だな。

;; https://developer.mozilla.org/ja/docs/WebAssembly/Understanding_the_text_format
(module
    (func (export "add") (param $lhs i32) (param $rhs i32) (result i32)
        local.get   $lhs
        local.get   $rhs
        i32.add
    )
)
<!-- https://developer.mozilla.org/ja/docs/WebAssembly/Understanding_the_text_format -->
<HTML>
    <HEAD>
        <TITLE>WebAssembly Add Test</TITLE>
    </HEAD>
    <BODY>
        <SCRIPT>
            WebAssembly.instantiateStreaming(fetch("add.wasm")).then(
                (obj) => {
                    console.log(obj.instance.exports.add(1, 2));
                }
            );
        </SCRIPT>
    </BODY>
</HTML>

  JavaScriptを含むhtmlと、機械語サブルーチンであるwasmは、/var/www/htmlの下に配置し、手元でApacheを上げ、ブラウザからhttp://localhost/add.htmlでアクセスする。何も画面に表示されなくても焦ってはいけない。「Chrome」なら右クリックからの「検証」「Console」し、ログに結果である「3」が……アレ? 出てないな。代わりにエラーが出ている。

Uncaught (in promise) TypeError: Failed to execute 'compile' on 'WebAssembly':
Incorrect response MIME type. Expected 'application/wasm'.

  ブラウザ依存かしら。「Firefox」で同じページを開き「要素を調査」「コンソール」で……アレ? こっちもか。「3」ではなくエラーが出ている。

TypeError: Response has unsupported MIME type

  よく読めばChromeの方が(ちょっと過剰にw)わかりやすい。Apacheがwasmを返す時の「MIME type」は「application/wasm」じゃないの? って言ってるワケだ。これは、Apache側に設定追加してやる必要がある。

/etc/httpd/conf/httpd.conf
<IfModule mime_module>
      :
    AddType application/wasm .wasm
      :
</IfModule>

  改めてブラウザからアクセスする。どちらのブラウザでも「3」が出力された。よっしゃよっしゃ。最初の任務は完了である。


2023-10-13(Fri) WebAssembly $42

  「一般的なブラウザでRubyが動くなら実に喜ばしい」などと思って始めたWebAssemblyだが、だんだんRubyはどうでもよくなってきた。Z80やX68000でやってたようなアセンブラ遊びをまたやれる。これだけCPUパフォーマンスが上がってきた今日この頃、ちゃんと価値を伴ったアセンブラ遊びが再びできるとは思ってもみなかった。調べるとWebAssemblyは、スタックマシンでレジスタの数に制限はないようだ。絶妙にネイティブアセンブラと違うところが面白いではないか。

  さて、WebAssemblyにおける「hello, world」に当たる足し算が終わったところで、次はJavaScriptとWebAssembly間で情報を共有できる「グローバル変数」をテストしてみることにする。

;; https://github.com/mdn/webassembly-examples/blob/main/js-api-examples/global.wat
(module
    (import "js" "global" (global $g (mut i32)))
 
    (func (export "getGlobal") (result i32)
        global.get  $g
    )
 
    (func (export "incGlobal")
        global.get  $g
        i32.const   1
        i32.add
        global.set  $g
    )
)
<!-- https://developer.mozilla.org/ja/docs/WebAssembly/JavaScript_interface/Global -->
<HTML>
    <HEAD>
        <TITLE>WebAssembly Global Test</TITLE>
    </HEAD>
    <BODY>
        <SCRIPT>
            const global = new WebAssembly.Global({ value: 'i32', mutable: true }, 8);
            console.log('a:', global.value);
 
            global.value = 16;
            console.log('b:', global.value);
 
            global.value += 8;
            console.log('c:', global.value);
 
            const importObjects = {
                js:         { global },
            };
            WebAssembly.instantiateStreaming(fetch('global.wasm'), importObjects).then(
                (obj) => {
                    let g = obj.instance.exports.getGlobal();
                    console.log('d:', g);
                    obj.instance.exports.incGlobal();
                    console.log('e:', global.value);
                },
            );
 
            console.log('f:', global.value);
        </SCRIPT>
    </BODY>
</HTML>
a: 8
b: 16
c: 24
f: 24
d: 24
e: 25

  WebAssembly.Globalでglobalというグローバル変数インスタンスを作り、JavaScript内で演算する様子をa, b, cで、WebAssembly内で演算する様子をd, e, fで確認する……と、意外な結果に。fの結果がdより前に出ていて、WebAssembly内の演算結果が反映されていない。

  どうも、参考にしたサンプルの「WebAssembly.instantiateStreaming」というwasmを読み込むメソッドに、並列実行を行う性質があることが原因のようだ。いやしかしこんな挙動は扱いにくいだけで、完全に余計なお世話なんだが……。

  だいぶアレコレと調べ上げウンウンと数多くの試行を繰り返した挙句、落ち着いたのが以下のJavaScriptコードだ。「Streaming」なのにワザワザ「await」するという冗談みたいなコードだが、これが一番シンプルで機械語サブルーチンを扱いやすい形にできる方法だった。

<!-- https://developer.mozilla.org/ja/docs/WebAssembly/JavaScript_interface/Global -->
<HTML>
    <HEAD>
        <TITLE>WebAssembly Global Test</TITLE>
    </HEAD>
    <BODY>
        <SCRIPT>
            async function main() {
 
                const global = new WebAssembly.Global({ value: 'i32', mutable: true }, 8);
                console.log('a:', global.value);
 
                global.value = 16;
                console.log('b:', global.value);
 
                global.value += 8;
                console.log('c:', global.value);
 
                const importObjects = {
                    js:         { global },
                };
                const obj = await WebAssembly.instantiateStreaming(fetch('global.wasm'), importObjects);
 
                let g = obj.instance.exports.getGlobal();
                console.log('d:', g);
                obj.instance.exports.incGlobal();
                console.log('e:', global.value);
 
                console.log('f:', global.value);
            }
            main();
        </SCRIPT>
    </BODY>
</HTML>
a: 8
b: 16
c: 24
d: 24
e: 25
f: 25

  結果も意図したとおりの内容になっている。よっしゃよっしゃ。任務完了。


2023-10-14(Sat) WebAssembly $43

  さて「グローバル変数」によりJavaScriptとWebAssembly間で情報を共有する方法をテストできたが、WebAssemblyは計算やメモリ操作しかできないので、どちらかというと大量のデータを処理する用途に向くわけだから、大きなメモリを共有してこそ真価が発揮されるわけだ。そこで次はJavaScriptとWebAssembly間でメモリ空間を共有できる「共有メモリ」をテストしてみることにする。

;; https://github.com/mdn/webassembly-examples/blob/main/js-api-examples/memory.wat
(module
    (memory $mem (import "js" "mem") 1)
    (func $log (import "console" "log") (param i32))
 
    (func (export "memsize")
        memory.size
        call        $log
    )
 
    (func (export "memStoreTest")
        i32.const   0       ;; adr
        i32.const   518     ;; val 0x_0000_0206
        i32.store
 
        i32.const   8       ;; adr
        i32.const   524     ;; val 0x_0000_020C
        i32.store
 
        i32.const   5       ;; adr
        i32.const   0x55
        i32.store8
 
        i32.const   6       ;; adr
        i32.const   0xAA
        i32.store8
 
        i32.const   0x0d    ;; adr
        i32.const   0x55AA
        i32.store16
 
        i32.const   -1
        call        $log
    )
 
    (func (export "memLoadTest") (param $adr i32)
        local.get   $adr
        i32.load
        call        $log
 
        local.get   $adr
        i32.load8_u
        call        $log
    )
)
<!-- https://developer.mozilla.org/ja/docs/WebAssembly/JavaScript_interface/Memory -->
<HTML>
    <HEAD>
        <TITLE>WebAssembly Memory Test</TITLE>
    </HEAD>
    <BODY>
        <SCRIPT>
            async function main() {
 
                const memory = new WebAssembly.Memory({ initial: 3, maximum: 8 });  // 1 PAGE = 64 KB
 
                const importObjects = {
                    js:         { mem: memory },
                    console:    { log: (arg) => console.log(arg) },
                };
                const obj = await WebAssembly.instantiateStreaming(fetch('memory.wasm'), importObjects);
                obj.instance.exports.memsize();
                memory.grow(2);                                 // +2 pages
                obj.instance.exports.memsize();
 
                const i8 = new Uint8Array(memory.buffer);
                const i32 = new Uint32Array(memory.buffer);
 
                function dump(i8) {                             // memory dump
                    const dump = [];
                    for(let p = 0; p < 16; p++) {
                        dump.push(i8[p].toString(16));
                    }
                    console.log('dump:', dump.join(' '));
                }
                dump(i8);
 
                for(let p = 0; p < 16; p++) {                   // 8bit store by JavaScript
                    i8[p] = 0x80 + (p << 1);
                }
                dump(i8);
 
                for(let p = 0; p < 16; p++) {                   // 32bit store by JavaScript
                    i32[p] = 0x80 + (p << 1);
                }
                dump(i8);                                       // little endian
 
                obj.instance.exports.memStoreTest();            // store by WebAssembly
                dump(i8);
 
                obj.instance.exports.memLoadTest(0);            // load by WebAssembly
                obj.instance.exports.memLoadTest(8);
            }
            main();
        </SCRIPT>
    </BODY>
</HTML>
3
5
dump: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
dump: 80 82 84 86 88 8a 8c 8e 90 92 94 96 98 9a 9c 9e
dump: 80 0 0 0 82 0 0 0 84 0 0 0 86 0 0 0
-1
dump: 6 2 0 0 82 55 aa 0 c 2 0 0 86 aa 55 0
518
6
524
12

  いろいろなことをやっているが、最初はJavaScript側でメモリを3ページ確保してWebAssembly側に渡し、それに2ページ追加してWebAssembly側に検知させている。

  その後、メモリの先頭16バイトをダンプ表示。JavaScript側では、Cの共用体のようにメモリをchar配列とint32配列とで共用する形にして、順に数字を格納していって、バイトオーダがリトルエンディアンであることを確認している。

  最後に、WebAssembly側で、int32での格納、charでの格納、int16での格納、デバッグログ出力(console.logの呼び出し、数字だけだけど)、int32での読出、charでの読出、その結果の出力、という感じ。

  このひと通りのテストを通じて、WebAssembly側で画像処理を行う目処が得られた気がする。よっしゃよっしゃ。任務完了。


2023-10-15(Sun) WebAssembly $00

  で、実践プログラミングの前の最後のテストとして、WebAssemblyの「制御構造」をテストしてみることにする。いわゆる、条件分岐(if)、繰り返し(loop)というヤツだ。

;; https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/Control_flow
(module
    (func $log (import "console" "log") (param i32))
 
    (func (export "ifTest") (param $val i32)
        local.get   $val
        if  (result i32)
            i32.const   456                     ;; not 0: True
        else
            i32.const   123                     ;; 0: False
        end
        call        $log
    )
 
    (func (export "loopTest") (param $times i32) (result i32)
        (local $i i32)
        (local $sum i32)
 
        i32.const   0                           ;; 初期値
        local.tee   $i
        local.set   $sum
        loop        $loop1
            local.get   $i                      ;; 処理1
            call        $log
 
            local.get   $sum                    ;; 処理2 $sum += $i
            local.get   $i
            i32.add
            local.set   $sum
 
            local.get   $i                      ;; $i += 1
            i32.const   1
            i32.add
            local.tee   $i
            local.get   $times
            i32.lt_s                            ;; $i < $times
            br_if       $loop1
        end
 
        local.get   $sum
    )
 
    (func (export "whileTest") (param $times i32)
        (local $i i32)
 
        local.get   $times                      ;; 初期値
        local.set   $i
        block       $loop1
            loop        $loop2
                local.get   $i                  ;; $i == 0
                i32.const   0
                i32.eq
                br_if       $loop1              ;; break
 
                local.get   $i                  ;; 処理1
                call $log
 
                local.get   $i                  ;; $i -= 1
                i32.const   1
                i32.sub
                local.set   $i
                br          $loop2              ;; continue
            end
        end
    )
)
<!-- https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/Control_flow -->
<HTML>
    <HEAD>
        <TITLE>WebAssembly Control Flow Test</TITLE>
    </HEAD>
    <BODY>
        <SCRIPT>
            async function main() {
                const importObjects = {
                    console:    { log: (arg) => console.log(arg) },
                };
                const obj = await WebAssembly.instantiateStreaming(fetch('controlflow.wasm'), importObjects);
 
                obj.instance.exports.ifTest(0);
                obj.instance.exports.ifTest(1);
                obj.instance.exports.ifTest(2);
 
                let sum = obj.instance.exports.loopTest(11);
                console.log('total:', sum);
 
                obj.instance.exports.whileTest(5);
            }
            main();
        </SCRIPT>
    </BODY>
</HTML>
123
456
456
0
1
2
3
4
5
6
7
8
9
10
total: 55
5
4
3
2
1

  まずは条件分岐(if)。WebAssemblyのifはスタックの先頭の値がゼロか非ゼロかで処理を分岐するものだ。JavaScriptから0を渡すとelseの123が返り、非ゼロを渡すと456が返っている。ちょっとクセがあるのは、アセンブラにはスタックのズレを検出する機能があり、if構造を抜けた際にスタックが高くなって(ズレて)いると、それが検出されてエラーになってしまうところだ。そういう場合「スタックに返り値を積んだんのですよ」と明示する必要がある。ifの後の「(result i32)」がそれだ。思い返せば、Z80の頃にプログラムを暴走させてしまう一番の原因はPUSHとPOPのズレだったよなぁ。

  次は繰り返し(loop)。カウンタは0から加算され、指定した回数だけ実行される。中身に処理がないのもアレなんでカウンタの累計を計算して返すようにしてみた。11回のループで0〜10が加算され結果の55が返っている。

  最後はwhileっぽい繰り返し。条件は最初に評価され、一度も実行されないことがありうるループ構造で、最も使用頻度が高いループ構造じゃないかな。WebAssemblyの場合、2重ループのようにしないと実現できないようだ。カウンタは指定した値から減算され、指定した回数だけ実行される。しかし、インデントをかますとアセンブラらしく見えないな。ないほうがいいのかな。

  というわけで、先月末くらいからダラダラと進めていたら半月もかかってしまったが、別に急ぐ必要もないのだし、やめてしまうより、やらないより、ダラダラと進めることには十分な価値があるのだ、ということにしておこう。さて、引き続き、最初に思いついた「WebAssemblyのひとつだけの使い道」のプログラミングに入るかな。イヒヒ。


2023-10-23(Mon) WebAssemblyでイメージを操作

  というわけで「WebAssemblyのひとつだけの使い道」のプログラミングを始めた。やりたいのは要するにイメージの操作だ。単純な計算を山ほどループで繰り返す。実にWebAssembly向きの処理である。

  いきなりだが、以下が機械語サブルーチン部分。イメージを表すRGBAの羅列を渡すと、GとBを抜いてくれるというもの。だいぶカリカリにチューン済み。結局、ループの最適解は減算&非ゼロ判定だな。それはそうと、見慣れないニーモニックが入っている。

(module
    (import "js" "mem" (memory $mem 1))
 
;;  filter(ソースの末尾 + 1 のアドレス)
    (func (export "filter") (param $src_adr i32)
        (local $dst_adr i32)
 
        push        src_adr                     ;; dst_adr = src_adr << 1
        i32.push    1
        i32.shl
        pop         dst_adr
 
loop1:  loop
 
        push        dst_adr                     ;; dst_adr -= 4
        i32.push    4
        i32.sub
        pop_push    dst_adr                     ;; [ dst_adr
 
        push        src_adr                     ;; src_adr -= 4
        i32.push    4
        i32.sub
        pop_push    src_adr                     ;; [ dst_adr src_adr
 
        i32.load                                ;; [ dst_adr color
        i32.push    0xFF0000FF                  ;; [ dst_adr color mask
        i32.and                                 ;; [ dst_adr color
        i32.store                               ;; [
 
        push        src_adr
        jp_nz       loop1                       ;; src_adr != 0 loop
 
        end
    )
)

  つうか、ちょっとは努力をしたつもりなのだが「i32.const」とか「local.set」とか「local.get」とか……ダメだわ。まったく頭に入ってこない。まったくスタックに出し入れしている感じが湧かない。結局、だいぶ前にZ80ライクニーモニックからPICニーモニックに変換するRubyスクリプトを書いた時と同じく、Z80ライクニーモニックからWebAssemblyのwat形式に変換するRubyスクリプトを書いてしまった。PIC用に作ったそれに比べれば、恐ろしく単純な変換しかしていないが、自分にはそれで十分にわかりやすく書ける。はて、自分は頭が固いのか柔らかいのか、どっちなのだろう……ゼッパチの魂百まで。怖い。

#!/usr/bin/env ruby
# coding: utf-8
 
wat80src = ARGV[0]
 
file_in = open(wat80src, 'r')
file_out = open(wat80src.gsub(/\.[^.]+$/, '') + '.wat', 'w', 0444)
 
file_in.each {|line|
    break if(line =~ /^__END__$/)
 
    unless(line =~ /^#/)
        line.chomp!
        if(line =~ /^(\w+):\s*(\w+)(\s*.*)/)                    # loop1:        loop
            line = "\t\t%s\t\t$%s%s" % [$2, $1, $3]
        end
        if(line =~ /^(\s+br\w*)\s+(\w+)(\s*.*)/)                #   br_if       loop1
            line = "%s\t\t$%s%s" % [$1, $2, $3]
        end
        if(line =~ /^(\s+)jp_nz\s+(\w+)(\s*.*)/)                #   jp_nz       loop1
            line = "%sbr_if\t\t$%s%s" % [$1, $2, $3]
        end
        if(line =~ /^(\s+\w+)\.push\s+([\d-]+)(\s*.*)/)         #   i32.push    10
            line = "%s.const\t\t%s%s" % [$1, $2, $3]
        end
        if(line =~ /^(\s+)pop\s+(\w+)(\s*.*)/)                  #   pop         i
            line = "%slocal.set\t\t$%s%s" % [$1, $2, $3]
        end
        if(line =~ /^(\s+)push\s+(\w+)(\s*.*)/)                 #   push        i
            line = "%slocal.get\t\t$%s%s" % [$1, $2, $3]
        end
        if(line =~ /^(\s+)pop_push\s+(\w+)(\s*.*)/)             #   pop_push    i
            line = "%slocal.tee\t\t$%s%s" % [$1, $2, $3]
        end
        if(line =~ /^(\s+)call\s+(\w+)(\s*.*)/)                 #   call        log
            line = "%scall\t\t$%s%s" % [$1, $2, $3]
        end
         line += $/
    end
 
    file_out.write line
}

  HTMLはこう。JavaScriptは外に出した。

<HTML>
    <HEAD>
        <TITLE>WebAssembly Graphics Test</TITLE>
    </HEAD>
    <BODY>
        <CANVAS id='canvas1' width='512' height='384'></CANVAS>
        <SCRIPT type='text/javascript' src='graphics.js'></SCRIPT>
    </BODY>
</HTML>

  で、例によって、JavaScriptはCoffeeScriptに書き直した。CoffeeScriptでasyncやawaitはどう書くのかと思ったら、awaitが含まれる関数は、自動的にasyncを付けてくれるらしい。CoffeeScript 1.xではダメで、2.0以上が必要なようだが。

'use strict'
 
# ソースイメージを読み込む
image_element = new Image
image_element.src = 'gra2.png'
 
# ソースイメージの読み込み完了を待つ
prep = ->
    if(!image_element.complete)
        setTimeout(prep, 100)
    else
        main()
 
main = ->
    screen_canvas_element = document.getElementById('canvas1')
    screen_context = screen_canvas_element.getContext('2d')
 
    # 枠描画、ソースイメージを描画
    screen_context.fillStyle = 'lightgray'
    screen_context.fillRect( 0,  0, image_element.width + 32, image_element.height + 32)
    screen_context.fillStyle = 'gray'
    screen_context.fillRect(16, 16, image_element.width, image_element.height)
    screen_context.drawImage(image_element, 16, 16)
    console.log('image_element:', image_element)
 
    # ソースイメージをデータ化
    work_canvas_element = document.createElement('canvas')
    work_canvas_element.width  = image_element.width
    work_canvas_element.height = image_element.height
    work_context = work_canvas_element.getContext('2d')
    work_context.drawImage(image_element, 0, 0)
#   source_image = work_context.getImageData(0, 0, image_element.width, image_element.height)
    source_bytes = work_context.getImageData(0, 0, image_element.width, image_element.height).data
    source_longs = new BigUint64Array(source_bytes.buffer, 0, source_bytes.length >> 3)     # コピーの高速化のために共用体化
#   console.log('source_image:', source_image)
    console.log('source_bytes:', source_bytes)
 
    # ワークメモリを確保
    work_memory = new WebAssembly.Memory({ initial: 1, maximum: 1 })    # 1 PAGE = 64 KB
    work_bytes = new Uint8ClampedArray(work_memory.buffer, 0, source_bytes.length)
    work_longs = new BigUint64Array(work_memory.buffer, 0, source_longs.length)             # コピーの高速化のために共用体化
 
    # ソースイメージデータをワークメモリにコピー
    for p in [0...source_longs.length]
        work_longs[p] = source_longs[p]
 
    # wasmをロード、メモリ操作(イメージデータの加工生成)を実行
    importObjects = {
        js:         { mem: work_memory },
        console:    { log: (arg) => console.log(arg) },
    }
    obj = await WebAssembly.instantiateStreaming(fetch('graphics.wasm'), importObjects)
    obj.instance.exports.filter(source_bytes.length)
 
    # 生成データをイメージ化
    filtered_bytes = new Uint8ClampedArray(work_memory.buffer, source_bytes.length, source_bytes.length)
    filtered_image = new ImageData(filtered_bytes, image_element.width, image_element.height)
    console.log('filtered_bytes:', filtered_bytes)
    console.log('filtered_image:', filtered_image)
 
    # スプライト(=キャンバス要素)を生成
    sprite_canvas_element = document.createElement('canvas')
    sprite_canvas_element.width  = image_element.width
    sprite_canvas_element.height = image_element.height
    sprite_context = sprite_canvas_element.getContext('2d')
    sprite_context.putImageData(filtered_image, 0, 0)
 
    # 枠描画、スプライトを描画
    screen_context.fillStyle = 'lightgray'
    screen_context.fillRect( 0, 64, image_element.width + 32, image_element.height + 32)
    screen_context.fillStyle = 'gray'
    screen_context.fillRect(16, 80, image_element.width, image_element.height)
    screen_context.drawImage(sprite_canvas_element, 16, 80)
    console.log('sprite_canvas_element:', sprite_canvas_element)
 
prep()

  結構、長い処理になってしまった。仕様上、何度も変換する必要があるのが面倒くさい。

  • 元となるpngを内部CANVASに描き、getImageDataでUint8ClampedArrayの形で取り出す。
  • それをWebAssembly.Memoryの領域にコピーする。
  • WebAssemblyの側でフィルタ処理を行う。
  • WebAssembly.Memoryの領域にUint8ClampedArrayの枠を被せて、ImageDataとして取り込む。
  • それを新たにスプライトとして扱うCANVASにputImageDataで描く、までが準備作業。

  最後に、表示されているCANVASに、drawImageでスプライトCANVASを重ねて完成だ。結果はこんな感じ。

  画像の説明

  というわけで「WebAssemblyのひとつだけの使い道」のプログラミングは成功。よっしゃよっしゃ。任務完了……ではない。実は今回のヤツも習作で、最終目標はもう一歩先にある。もうちっとだけ続くんじゃ。