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|05|06|07|08|09|10|11|

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