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|

2023-12-01(Fri) WebAssemblyのstackで開発がstuck

 WebAssemblyのひとつだけの使い道」ということで、ボチボチと開発を進めていたのだが、どうにも不可解な動きがあって、理解するまでにだいぶかかってしまった。

  まずは、以下のサンプルコードだ。Wasmの関数に引数として0を渡すと20、非0を渡すと10がログに出力される。

main = ->
    importObjects = {
        console:    { log: (arg) => console.log(arg) },
    }
    obj = await WebAssembly.instantiateStreaming(fetch('test.wasm'), importObjects)
    console.log('call test(0)')
    obj.instance.exports.test(0)
    console.log('call test(1)')
    obj.instance.exports.test(1)
main()
(module
    (import "console" "log" (func $log (param i32)))
    (func (export "test") (param $val i32)
        push        val
        if
            i32.push    10
            call        log
        else
            i32.push    20
            call        log
        end
    )
)

  したがって、結果はこうなる。

call test(0)
20
call test(1)
10

  ところが、以下のようにlog出力をifブロックの外に出すとアセンブルエラーが起きてしまう。

        push        val
        if
            i32.push    10
        else
            i32.push    20
        end
        call        log
test.wat:10:4: error: type mismatch in if true branch, expected [] but got [i32]
            i32.const       10
            ^^^^^^^^^
test.wat:13:3: error: type mismatch in if false branch, expected [] but got [i32]
        end
        ^^^
test.wat:14:3: error: type mismatch in call, expected [i32] but got []
        call        $log
        ^^^^

  これは、以下のようにすると解決する。「ifブロックを出る時にi32を返しますよ」という宣言を加えるわけだ。

        push        val
        if  (result i32)
            i32.push    10
        else
            i32.push    20
        end
        call        log

  そんなら「ifブロックを出る時にi32を『ふたつ』返しますよ」という宣言をしたらどうなるかというと、アセンブルエラーが起きてしまう。

        push        val
        if  (result i32) (result i32)
            i32.push    10
            i32.push    10
        else
            i32.push    20
            i32.push    20
        end
        call        log
        call        log
test.wat:9:3: error: multiple if results not currently supported.
        if  (result i32) (result i32)
        ^^

  「not currently supported.」なので、将来的には可能になるのかもしれないが、現状では認められない、というように読める。

  じゃ、こういうコードはどうか?

        i32.push    100
        push        val
        if  (result i32)
            i32.push    10
            i32.add
        else
            i32.push    20
            i32.add
        end
        call        log

  あらかじめ、スタックに100を積んでおいて、引数として0を渡すと20、非0を渡すと10を加算した値をログに出力させたい……のだが、アセンブルエラーが起きてしまう。

test.wat:12:4: error: type mismatch in i32.add, expected [i32, i32] but got [i32]
            i32.add
            ^^^^^^^
test.wat:15:4: error: type mismatch in i32.add, expected [i32, i32] but got [i32]
            i32.add
            ^^^^^^^
test.wat:17:3: error: type mismatch in function, expected [] but got [i32]
        call        $log
        ^^^^

  こうなることがどうにも理解できなくて、長らくグダグダしていた。MDNのifの項を読んでも、特段なにも触れられていない。が、これは「ifは関数コール」のようなものだ、と理解するべきだという結論にたどり着いた。

  ifブロックに入ったら「スタックの内容は持ち込めない」し「スタックの内容は持ち出せない(ただし返値としてひとつだけは許容される)」ということで、これは関数コールの特性そのものである。これまでのアセンブラ知識が邪魔になって必要以上に理解するのに時間がかかってしまった。

  上記のプログラムは、中で加算せず、加算対象を返値として、外で加算する形とすれば意図した結果が得られる。

        i32.push    100
        push        val
        if  (result i32)
            i32.push    10
        else
            i32.push    20
        end
        i32.add
        call        log

  「スタックの内容は持ち込めない」のはblockも同じだ。上記を書き直すと以下のようになる。

        i32.push    100
blk1:   block   (result i32)
            i32.push    10
            push        val
            br_if       blk1
            drop
            i32.push    20
        end
        i32.add
        call        log

  以下は、引数が0だった場合に、100をログに出力させるプログラムだが、非0だった場合はスタックに100が残ることになる。が、それは許容されるらしい。関数コールと考えれば、それが捨てられるだろうことは、まぁ理解できなくもないのだが。

blk1:   block
            i32.push    100         ;; [ 100
            push        val         ;; [ 100 val
            br_if       blk1        ;; [ 100
            call        log         ;; [
        end
;;                                  ;; [ ???

  では、返値を設定したらどうなるか。様々に試したところ、以下のコードから挙動を掴むことができた。

        i32.push    99              ;; [ 99
blk1:   block   (result i32)
            i32.push    100         ;; [ 99 100
            i32.push    12          ;; [ 99 100 12
            i32.push    11          ;; [ 99 100 12 11
            i32.push    10          ;; [ 99 100 12 11 10
            push        val         ;; [ 99 100 12 11 10 val
            br_if       blk1        ;; [ 99 100 12 11 10
            call        log         ;; [ 99 100 12 11
            drop                    ;; [ 99 100 12
            drop                    ;; [ 99 100
        end
        call        log             ;; [ ???
        call        log             ;; [ ???

  結果はこうなる。

call test(0)
10
100
99
call test(1)
10
99

  引数が0だった場合、brをスルーし、中のlogで10を出力した後、11, 12をdropし、外のlogで100と99を出力して終了する。一方で、引数が非0だった場合、スタックが「99 100 12 11 10」の状態でbrでblockを抜けるが、その際、頭の10が返値となり、blockに入る時の状態である「99」の上に10を積んだ「99 10」という状態でblockを抜けるらしい。

  というわけで、挙動については掴むことができたが、むしろ面倒くさい仕様に思えてならない。さて、だいぶ寄り道してしまったが、本来の目的に戻ろう。


2023-11-10(Fri) CoffeeScript2にてMixinを試す

  ここのところ、ものスゴい学習欲と、それを越えるほどの課題が湧き上がってきて頭を休める間もない。WebAssemblyからSIMD命令(MMX/SSE/AVX)、浮動小数点演算、CPUキャッシュに飛び火し、なぜか線形代数の学び直しから、3DCG、AIに至るまで。

  こうなったら、ポリゴンのレンダラを経て、レイトレーシングのレンダラまで書いてみたくなってきてしまっているのだが、まずは当面の「WebAssemblyによるイメージ操作」を片付けなければならない。

  しかし最終的にアニメーションさせたいなら、以前に作ったシューティングゲームの既存のフレームを使ったほうがいい、ということになり、久々に引っ張り出してくると……動かねぇ。

  どこが引っかかっているのかと思ったらMixinだ。なぜか以前に書いたMixinの仕組みが動かない。まぁJavaScriptとしても、CoffeeScriptとしても、Mixinが正式な言語仕様として組み込まれているわけではないからな、と思いつつ、調べていくと意外と根が深い。

  そもそもJavaScriptにはクラスが存在しない。いや、最近まで存在しなかった。CoffeeScriptで書いていたので気づかなかったが、クラスが正式な言語仕様として組み込まれているわけではないところに、prototypeとかいう仕組みを通じてCoffeeScriptによりコネあげられていたのだ。ところが、最近になってJavaScriptにクラスの仕組みが組み込まれたので、CoffeeScript2からはコネあげをやめ、その仕組みをそのまま利用するようになった。

  それだけならよかったのだが、クラスでメソッドを定義するとprototypeに登録されないようなのだ。先のMixinの仕組みはprototypeを通じてコネあげられていたので、それができなくなってしまった、と。

// コネあげ版クラス(JavaScript)
const Animal = (function() {
    function Animal(name) {
        this.name = name;
    }
    Animal.prototype.walk = function() {
        console.log(this.name, 'walking.');
    };
    return Animal;
})();
// ネィティブクラス(JavaScript)
const Animal = class Animal {
    constructor(name) {
        this.name = name;
    }
    walk() {
        console.log(this.name, 'walking.');
    }
};
// どちらも同様に動くのだが
console.log(Animal.prototype);
const pochi = new Animal('Pochi');
pochi.walk();
// コネあげ版クラスだとprototypeにwalkがあるのに
$ node diff_func.js
Animal { walk: [Function] }
Pochi walking.
// ネィティブクラスだとprototypeがカラ
$ node diff_class.js
Animal {}
Pochi walking.

  じゃ、どーすればいいのかというと、こうすればいいらしい。

const Flying = (base) => class extends base {
    fly() {
        console.log(this.name, 'flying.');
    }
}
class Bird extends Flying(Animal) {};

  以前は「mixOf base, mixin」という以下のような書き方だったが「Flying(Animal)」って書き方も悪くはないなぁ。従来のmixOf関数の定義は必要なくなる。上記の定義自体がMixinを行う関数になっている。

class Bird extends mixOf Animal, Flying

  つうわけで、CoffeeScriptで書き直すとこうだ。

'use strict'
 
class Animal
    constructor: (name) ->
        @name = name
    walk: ->
        console.log(@name, 'walking.')
 
class Human extends Animal
    talk: ->
        console.log(@name, 'talking.')
 
Flying = (base) -> class extends base
    fly: ->
        console.log(@name, 'flying.')
 
Ejecting = (base) -> class extends base
    eject: ->
        console.log(@name, 'ejecting.')
 
class Bird extends Flying(Animal)
 
class Pilot extends Ejecting(Flying(Human))
 
pochi = new Animal('Pochi')
pochi.walk()
 
taro = new Human('Taro')
taro.walk()
taro.talk()
 
pichan = new Bird('Pi-chan')
pichan.walk()
pichan.fly()
 
tom = new Pilot('Tom')
tom.walk()
tom.talk()
tom.fly()
tom.eject()

  実行結果は以下。

$ node mixin.js
Pochi walking.
Taro walking.
Taro talking.
Pi-chan walking.
Pi-chan flying.
Tom walking.
Tom talking.
Tom flying.
Tom ejecting.

  完全に「ヤクの毛刈り」である。しかし、なんとか刈り終えた。先に進むとしよう。


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


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-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-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-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-09-11(Mon) クレギオン、ロケットガール、ダブルリーチ

  6月の中頃、ポイントが余っていることもあり、何か面白い小説でもないかと探していると目に止まったのが、野尻抱介氏の「クレギオン」シリーズ。軽い気持ちで電子書籍版を買ってみたところ、これが近年マレに見る面白さ。スペースオペラで、ダーティペアに近い雰囲気だが、それに比べハードSF成分が高いのに、展開はソフトという感じ。

  元は富士見ファンタジア文庫での出版だったが、今はハヤカワ文庫で再販されているという状況が驚くほど自然に思える内容だ。ライトノベルとハードSFの見事な融合。なんというかライトでハードってアルミノベル!?

  野尻氏の作品は、過去に「南極点のピアピア動画」「沈黙のフライバイ」「太陽の簒奪者」「ふわふわの泉」のいずれも読んでいて好印象だったが、それにも増して面白い。「クレギオン」は平成4年出版開始だが、まったく古さを感じさせない。

  1冊目の「ヴェイスの盲点」を読了したところで、躊躇なく2冊目の「フェイダーリンクの鯨」へ進む。が、ひとつ問題が。挿絵がまったくないので、いまひとつ宇宙空間の状況が入ってこない。主役メカであるアルフェッカ号の形もピンと来ていない。

  どうも、ハヤカワ文庫に移籍した時に挿絵が捨てられてしまったようだ。いや、それはあったほうがいいのだがな。まだ先に5冊もあるのに、このまま挿絵のない電子書籍版を買い進めるのは、特にこの作品においてはもったいなさすぎるように思える。

  しかし、一方で紙の本は食事中に読み進められないという問題がある。非常に不真面目な読書態度で申し訳ないのだが、最近の自分は「細切れの数分の時間を使って、数ページずつ読み進める」というスタイルなのだ。自分の場合、それでも十分に内容は頭に入るし、存分に楽しめるのだ。というわけで、それが解決になるのかはわからないものの、とりあえず「書見台」というものを買ってみることにした。

  画像の説明

  すると、これが想像以上に快適で、紙の本に限って読み進められないという問題が完全に解決されてしまった。未読の作品の都合もあるが、電子書籍を読む時間が寝る前だけになってしまったほどだ。

  で、3冊目の「アンクスの海賊」から紙の本で読み始めた……のだが、期待に反し、挿絵は人物画ばかりでSF的な情景を描いたものは皆無だった。ま、そうなるか。とはいえ、マージもメイも、オマケにロイドも、顔はあったほうがいい。結局、7冊目の「ベクフッドの虜」まで紙の本で調達してしまい、その勢いで、間違いなく「ロケットガール」シリーズも面白いハズだから読むべきと思い、これも紙の本を調達。当然、ハヤカワ版ではなく、富士見の再販版である。ついでに、さかのぼってクレギオンの1,2冊目も調達。

  画像の説明

  こんな気持ちになるのは珍しいが、7冊目の「ベクフッドの虜」はもったいなくて読まずに残し「ロケットガール」に進む。これまた、無闇に面白い。笹本祐一氏の「星のパイロット」シリーズと内容も面白さもだいぶ被るが、リアルな宇宙開発を舞台にしてるだけなのに、なんでこんなに面白いのか。

  そして、いま3冊目の「私と月につきあって」を読み終わったところである。それにしても、どの話にしても、もちっとエピローグを書いてもいいくらい、最終ページ直前までクライマックスである。ホント尻尾の先までアンコだなぁ。

  つうわけで、まちがいなく面白いが、読んでしまったら終わってしまうという、ダブルリーチ状態。どっちを先に読むべきか迷っているのであった。贅沢な悩みだなぁ。


2023-09-05(Tue) Rubyで端末画面に画像を表示する

  一応、CUIコンソール画面に画像を出力できたんだが、Maveに実装することを考えれば、縮小(ついでに拡大)機能と、右側のトリミング機能が必要だ。ついでに透過処理もできるとカッコいい。

  さて、透過処理はどうすれば……と考え込んだところで「▀」という「上半分塗りつぶし」文字と「▄」という「下半分塗りつぶし」文字を使い分ける必要があることに気づく。そういや、参考に読んだcatimgのコードで両方を使い分けていたのはそういうことだっとのか。

  しかし、どうも美しく実装できない。上から順に処理しつつ、上下2ドットを同時に処理する、というのが厄介。

  ちなみに、今回のケースもやや当てはまるのだが、こういう処理が必要な状況って時々ある。

# ./grouping.rb 
["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R"]
1: A,B,C,D,E
2: F,G,H,I,J
3: K,L,M,N,O
4: P,Q,R

  で、以下のようなコードを書いたりするのだが。

#!/usr/bin/env ruby
 
x = '@'; xs = []; 18.times {
    xs << x.succ!.dup
}
puts(xs.inspect)
 
n = 0; xp = []; xs.each {|x|
    xp << x
    if(xp.size == 5)
        puts('%d: %s' % [n += 1, xp.join(',')]) # same!
        xp.clear
    end
}
puts('%d: %s' % [n += 1, xp.join(',')])         # same!
 
__END__

  ここで「same!」とある行を2度書くのがすごくイヤなんだよな。こういうのに対応する構文を持つ言語ってあるのかしらん……とか考えつつ、その部分はさておき、他はどうにかこうにか納得できるコードが書けた。

#!/usr/bin/env ruby
 
require 'rmagick'
 
if(ARGV.size == 0)
    abort(<<USAGE)
Usage: rcatimg image_file [width] [trim]
USAGE
end
 
image = Magick::Image.read(ARGV[0])
w = image[0].columns
h = image[0].rows
 
tgt_w = `tput cols`.to_i
tgt_w > w and tgt_w = w
(it = ARGV[1]) and tgt_w = it.to_i
trm_x = false
(it = ARGV[2]) and trm_x = it.to_i
tgt_h = h * tgt_w / w
sstep = (h << 8) / tgt_h
 
# +---------+   tdms = [    [ 0b00, 'Ar;Ag;Ab', 'Wr;Wg;Wb' ],
# | A   C D |               [ 0b10,        nil, 'Xr;Xg;Xb' ],
# | W X   Z |               [ 0b01, 'Cr;Cg;Cb',        nil ],
# +---------+               [ 0b00, 'Dr;Dg;Db', 'Zr;Zg;Zb' ] ]
 
ess = [ "\e[38;2;%2$sm\e[48;2;%3$sm\u2580\e[m", # 0b00: f_col,  b_col,  [▀]
        "\e[38;2;%2$sm\u2580\e[m",              # 0b01: f_col,          [▀]
        "\e[38;2;%3$sm\u2584\e[m",              # 0b10:         f_col,  [▄]
        ' ' ]                                   # 0b11:                 [ ]
 
y8 = 0; tdms = []; tgt_h.times {
    x8 = 0; tgt_w.times {|cx|
        trm_x and (cx < trm_x or break)
        c = image[0].pixel_color(x8 >> 8, y8 >> 8)
        tdms[cx] ||= [0]
        tdms[cx][0] <<= 1
        if(c.opacity < 64)
            tdms[cx] << '%d;%d;%d' % [c.red >> 8, c.green >> 8, c.blue >> 8]
        else
            tdms[cx][0] += 1    # 透明
            tdms[cx] << nil
        end
        x8 += sstep
    }
    if(tdms[0].size == 3)
        tdms.each {|tdm|
            print(ess[tdm[0]] % tdm)
        }
        puts; tdms.clear
    end
    y8 += sstep
}
if(tdms.size != 0)
    tdms.each {|tdm|
        print(ess[(tdm[0] << 1) + 1] % tdm)
    }
    puts
end
 
if(ENV['INFO'])
    puts('#' * trm_x) if(trm_x)
    puts('#' * tgt_w)
    puts('[%d, %d] -> x%d/%d -> [%d, %d]' % [w, h, 256, sstep, tgt_w, tgt_h])
end
 
__END__

  画像の説明

  さて、次はMaveの方を見てみるかな。


2023-09-04(Mon) スイッチ、ングハブ、交換

  先月の23日、ネットにつながらなくなった。落雷でスイッチングハブが壊れたようだ。こんなことあるんだな。

  無線LANルータのオマケの4ポートをスイッチの代わりに、最低限のマシンだけ有線接続する応急対処の後、100Mで5ポートの手持ちのスイッチで増設、なんてことをやっているうちに、ポチっておいたハブが到着したので交換。NETGEARは最安ではないが、特段の何の問題も起こしてこなかった実績があるんだから、そりゃ続投だ。

  画像の説明

  思い返せば、自宅の新築に伴い24ポートを導入したが、ファンが異音を発するようになってきたので、ファンレスの16ポートに交換したところ、今回の破損だ。3年はちょっと短いがしゃーないな。