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-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-12-06(Wed) WebAssemblyでトラディショナルな回転技術を再現

  というわけで、9月末頃に「やろう」と思い立ってから2ヶ月チョイもかかってしまった。まぁ、だらだらと取り組んでいたせいもあるが、初めての言語、かつ、相手が未知の仕様を含むアセンブラでは効率が上がらなかったのも無理はないかな。

  そう「WebAssemblyのひとつだけの使い道」とは、過去に実装に成功した「回転、拡大、縮小機能」をWebAssemblyを使って高速化することなのであった。WebAssemblyは計算やメモリ操作しかできず、複雑な関数計算も苦手なのだが、1980年台のトラディショナルな回転、拡大、縮小技術は、加算処理を山ほど行うだけなので、用途としてピッタリなのだ。

  というわけで、早速だが以下がその成果である。

  画像の説明

  wasmのコードは試行錯誤で書くには記述性が低すぎるし、可読性も低すぎる。ので、考え方の図と併せて、Rubyで作ったプロトタイプのコードも載せておく。処理はほぼ同じ。アセンブラ頭で書いたRubyコードw。これも相当に考えないと何をやっているのかわからんと思うが。

  画像の説明

rxy = vxys[v + 128]                             # サンプリングxy開始点計算用
hxy = vxys[v]                                   # 水平方向サンプリングxy間隔
vxy = vxys[v + 64]                              # 垂直方向サンプリングxy間隔
dest_adr = w * h                                # 描画先アドレス
 
dx = w >> 1; dy = h >> 1
_vx8 = dx * rxy[0] - dy * rxy[1] + (dx << 8)    # サンプリングxy開始点
_vy8 = dx * rxy[1] + dy * rxy[0] + (dy << 8)
 
begin
    x = w                                       # 水平方向カウンタを初期化
    _hx8 = (_vx8 += vxy[0])                     # サンプリング位置を垂直方向に進め、水平方向の初期位置としてセット
    _hy8 = (_vy8 += vxy[1])
    begin
        dest_adr -= 1
        _hx8 += hxy[0]; _hy8 += hxy[1]          # サンプリング位置を水平方向に進める
 
        (_hx8 < 0 or _hx8 >= (w << 8)) and next # サンプリング範囲外チェック
        (_hy8 < 0 or _hy8 >= (h << 8)) and next
 
        src_adr = (_hy8 >> 8) * w + (_hx8 >> 8) # サンプリングアドレス計算
        color = LegacyGraphics.point(src_adr & msk64, src_adr >> x64)
        win.pset(dest_adr & msk64, dest_adr >> x64, color)
    end until((x -= 1) == 0)
end until(dest_adr == 0)

  CPUが8bitの時代、大半はBASIC、処理速度が必要な部分のみ「CALL」や「USR」で機械語サブルーチンを呼び出す、というプログラミングスタイルがあったが、まさにそれと同じようなスタイルになっている。

  回転したパターンはキャッシュされるので、画面ではふたつのテトランを回しているものの、計算しているのはひとつ分だし、1回転した後はすべてキャッシュからの表示となる(呼び出し側からは透過的にそうなる)。今回のパターンは四隅までミッシリの128x128サイズなので端が欠けるが、透過処理もバッチリできているので、実用するなら256x256で回すことになるだろう(256x256なら181x181(256/√2)の大きさまで欠けずに回せることになる)。

  今回の表示は以前に作ったスケルティウスのエンジンを流用しているので、逆に言えば、既にシューティングゲームにそのまま利用できる形になっている。また別途、複数のコードをリンクする仕組みを自作しており、APIを揃えているので、リンクする物件を'rotnscl_wasm.bean'から'rotnscl.bean'に変更するだけで、WebAssemblyを使わないバージョンに戻すことができる。

  実は、JavaScriptのコンソールログを開くと、フレームレートや負荷率が出力されるようになっているのだが、WebAssemblyを使わないバージョンだと、負荷率が150%前後となり、60であるべきフレームレートが30前後に落ちてしまう。

now: ,1701848213042,frames: ,20, load: ,174,%
now: ,1701848215409,frames: ,28, load: ,180,%
now: ,1701848218383,frames: ,15, load: ,150,%
now: ,1701848220384,frames: ,32, load: ,144,%
now: ,1701848221614,frames: ,53, load: ,6,% ※キャッシュが完了
now: ,1701848222597,frames: ,62, load: ,0,%

  WebAssemblyのバージョンなら、負荷率は常に6%以下で、60のフレームレートは余裕で確保できる。

  ちなみに、今回のwasmモジュールは最大512x512のパターンまで回せるのだが、WebAssemblyを使わないバージョンだと、負荷率は2500%を越え、フレームレートは2前後と紙芝居以下のレベルになってしまう一方で、WebAssemblyバージョンなら、負荷率は概ね20%以下で、まだまだ鼻歌交じりとなる。128x128と512x512では面積比が16倍なので、150%が2400%になることは理屈に合う。WebAssemblyなら512x512でも20%前後ということは、軽く100倍を越える速度が得られているということになる。

  そりゃ、直接にニーモニックで書いて、しかも割とカリカリに最適化してあるんだから、そうこなくっちゃってとこだ。100倍というのは苦労に見合う結果に思える。先には数倍という話も聞いたが、あれはRustなどからコンパイルした場合の話なのだろうな。ちなみに「割とカリカリに最適化してある」ものの、今回はベクトル演算命令は使っていない。それを使えばさらに速度は上がるはずだ。まぁ、上がっても20%以下だろうが。

  というわけで、今回「回転」は実装したものの「拡大、縮小」は実装していない。WebAssemblyについてはすっかり習得できたので、やろうと思えばそう時間はかからないだろうが、一旦は目標達成としてまた別のことでも始めようかな。

  あー、スッキリした!


2023-12-31(Sun) Gmailの「簡易HTML形式」を使い続けたい

  年末だ。今年もだいぶ全力でいろいろな事に取り組んだ実感がある。が、歳を重ねるにつれ、できることは増えるものの、その速度は知れたものだ、ということもわかってきた気がする。

  さらに、何であっても年単位で継続しなければそれなりの成果は得られない、ということにも改めて気付かされた。自分は歳を取って継続することができるようになり、十分に有意義な時間を過ごせていると思える。引き続き変化する可能性も排除せずに、さらに成長していきたい。

  今年を振り返ると、2回のロングドライブが貴重な体験だったかな。そして、WebAssemblyを学び習作を完成させたことも達成感があった。

  特に東北ドライブの後は何だか気分が晴れない期間が続いたこともあり、そのまま無気力な状態が続いたらどうしようかと不安だった。が、課題が見つかったら自然に調子が出てきて、気づけばWebAssemblyだ。自ら学ぶ力が衰えていないことを確認できた気がして嬉しかったな。

  で、ここんとこ、このブログ含め自分用のサービスのホストを移行しているのだが、新発見をしつつもなかなかに調子よく進められている。で、表題のGmailだ。なんでも「簡易HTML形式」が終了してしまうらしい。特にセンタクリックでパパパッと複数のメールを連続して開けなくなってしまったのがうっとおしい。

  こうなったら再び自作のメーラ「Maverick」を引っ張り出してくるほかあるまい。最近は使っていなかったので、コンテナ関係ののお色直しは必要なものの、OAuth2.0認証に対応させておいたおかげで、数時間で構築完了。

  画像の説明

 たぶん使わないんだけど」なんて書いたが、そんなことはないんだよね。あらゆる「取り組んだこと」は、大抵は後で役立つものなのだ。

  来年の抱負はなし。なぜなら、既に「全力でいろいろな事に取り組めている」からだ。