SVX日記
2023-12-01(Fri) WebAssemblyのstackで開発がstuck
「WebAssemblyのひとつだけの使い道」ということで、ボチボチと開発を進めていたのだが、どうにも不可解な動きがあって、理解するまでにだいぶかかってしまった。
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
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
^^^^
push val
if (result i32)
i32.push 10
else
i32.push 20
end
call log
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)
^^
i32.push 100
push val
if (result i32)
i32.push 10
i32.add
else
i32.push 20
i32.add
end
call log
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
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
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,%
ちなみに、今回のwasmモジュールは最大512x512のパターンまで回せるのだが、WebAssemblyを使わないバージョンだと、負荷率は2500%を越え、フレームレートは2前後と紙芝居以下のレベルになってしまう一方で、WebAssemblyバージョンなら、負荷率は概ね20%以下で、まだまだ鼻歌交じりとなる。128x128と512x512では面積比が16倍なので、150%が2400%になることは理屈に合う。WebAssemblyなら512x512でも20%前後ということは、軽く100倍を越える速度が得られているということになる。
そりゃ、直接にニーモニックで書いて、しかも割とカリカリに最適化してあるんだから、そうこなくっちゃってとこだ。100倍というのは苦労に見合う結果に思える。先には数倍という話も聞いたが、あれはRustなどからコンパイルした場合の話なのだろうな。ちなみに「割とカリカリに最適化してある」ものの、今回はベクトル演算命令は使っていない。それを使えばさらに速度は上がるはずだ。まぁ、上がっても20%以下だろうが。
2023-12-31(Sun) Gmailの「簡易HTML形式」を使い続けたい
さらに、何であっても年単位で継続しなければそれなりの成果は得られない、ということにも改めて気付かされた。自分は歳を取って継続することができるようになり、十分に有意義な時間を過ごせていると思える。引き続き変化する可能性も排除せずに、さらに成長していきたい。
今年を振り返ると、2回のロングなドライブが貴重な体験だったかな。そして、WebAssemblyを学び習作を完成させたことも達成感があった。
特に東北ドライブの後は何だか気分が晴れない期間が続いたこともあり、そのまま無気力な状態が続いたらどうしようかと不安だった。が、課題が見つかったら自然に調子が出てきて、気づけばWebAssemblyだ。自ら学ぶ力が衰えていないことを確認できた気がして嬉しかったな。
で、ここんとこ、このブログ含め自分用のサービスのホストを移行しているのだが、新発見をしつつもなかなかに調子よく進められている。で、表題のGmailだ。なんでも「簡易HTML形式」が終了してしまうらしい。特にセンタクリックでパパパッと複数のメールを連続して開けなくなってしまったのがうっとおしい。
こうなったら再び自作のメーラ「Maverick」を引っ張り出してくるほかあるまい。最近は使っていなかったので、コンテナ関係ののお色直しは必要なものの、OAuth2.0認証に対応させておいたおかげで、数時間で構築完了。
「たぶん使わないんだけど」なんて書いたが、そんなことはないんだよね。あらゆる「取り組んだこと」は、大抵は後で役立つものなのだ。