SVX日記
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>
<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を重ねて完成だ。結果はこんな感じ。