Sipeed Lichee Pi 4A 導入記

Sipeed Lichee Pi 4A を購入しました…! プレオーダーして発売を楽しみにしていたハイスペック RISC-V シングルボードコンピュータです。

https://sipeed.com/licheepi4a

Lichee Pi 4A
Powerful RISC-V SBC ever!

TH1520, 12nm, RISC-V 2.0G C910 x4
4 / 8 / 16 GB 64bit LPDDR4X-3733
TF Card, or 16 / 32 / 64 / 128 GB eMMC

今回購入したのは最初期バージョン(販売サイトではβの文字がついています)の 8GB memory + 8GB storage (日本円で 1.7万円ほど)のもとのなり、しばらくすると各バリエーションが増えていくと思います。(そういえばこちらのスペック表に eMMC 8GB 版の記載がされてないですね… Wiki によると16GB 版はなくて 8GB 版になったようです)

https://wiki.sipeed.com/hardware/en/lichee/th1520/lpi4a/1_intro.html

· eMMC: 可选 空贴、 8G、 32G、 128G

メインボード(?) と、LM4A と呼ばれる RISC-V SoC と eMMC・メモリーとが 260P SODIMM で接続されているのが特徴で、おそらく今後 LM4A の各バリエーションの単体売りや、LM4A を使った機器の販売が予定されているのだと思います。楽しみです…!


ドキュメント Wiki(英語翻訳中とのこと) :

https://wiki.sipeed.com/hardware/en/lichee/th1520/lpi4a/1_intro.html

LicheePi 4A 是基于 Lichee Module 4A 核心板的 高性能 RISC-V Linux 开发板,以 TH1520 为主控核心(4xC910@1.85G, RV64GCV,4TOPS@int8 NPU, 50GFLOP GPU),板载最大 16GB 64bit LPDDR4X,128GB eMMC,支持 HDMI+MIPI 双4K 显示输出,支持 4K 摄像头接入,双千兆网口(其中一个支持POE供电)和 4 个 USB3.0 接口,多种音频输入输出(由专用 C906 核心处理)。

Github リポジトリー(まだ 2023-05 時点では空です)

https://github.com/sipeed/LicheePi4A

LicheePi4A info&sdk

Linux カーネルと u-boot

https://github.com/revyos/thead-kernel

https://github.com/revyos/thead-u-boot

本記事(2023-05)は発売直後のものです。OS アップデート等で今後変わる可能性が高い情報ですが、動かしてみた部分をいくつか書いておきたいと思います。

  • HDMI とオーディオコネクタはしっかり奥まで差し込みましょう。特に HDMI は割と硬いので奥まで刺さないと写りません。
  • 空冷用のファンとサーマルグリースシートが付属します。プログラムのビルドなどで RISC-V 4コアを 1.85GHz で連続的に回し続けるとファン付きで 60度くらいまで上がりますので付けましょう。
  • 温度を観測していると SoC sensors が 40度以降でファンが回転始めるようです。部屋が寒いと OS 起動直後は動かない時がありますのでご安心を。そのうち回ります。
  • 電源は USB-C の 2A で SoC と HDMI、USB-SSD、Ethernet の動作は問題ありませんでした。Anker のスマホ充電用 USB Type C アダプタと(オーバースペックですが) 60W 通せる USB Type C to C ケーブルを使いました。
  • 12V DC Power は MISI LCDパネル(基板背面にコネクタあり)などを利用するときに使うとよいとのこと。

以下は OS に初期提供されている Debian イメージを使ったときの話題です。

執筆時点(2023-05) – LPi4A_20230511_gnome.zip

  • 消費電力にも関係しますがメインボード側(LM4A の外側)に付いている WiFi/BT モジュールは(技適の関係もありますので)disable に設定していて未検証です。
  • GPU については現在ドライバーがフルサポートではないようです。自分もいくつかの GPU アクセラレーションを使うソフトが真っ黒になるなどを確認しました。
  • HDMI からの音声出力は現在未サポートです。付属のラインアウトは I2S に接続されており、こちらから音が出ます。(パッチが入っているのを見ましたので、次のリリースで対応されそうです)

eMMC への OS イメージの焼き方

ドキュメント記載の通りですが、執筆時点(2023-05)で最新だった Debian – GNOME(LPi4A_20230511_gnome.zip)イメージを使っています。

ダウンロード:

https://wiki.sipeed.com/hardware/en/lichee/th1520/lpi4a/3_images.html

Mega 云盘:点我 (←ここにあります)

焼き方:

https://wiki.sipeed.com/hardware/en/lichee/th1520/lpi4a/4_burn_image.html

電源の USB-C の上にある BOOT ボタンを押しながら、USB-C を PC の USB-C に接続すれば T-HEAD USB download gadget として認識されるはずです。

自分は PC に Ubuntu/ThinkPad を使っているので dmesg lsusb は次のようになります。

$ sudo dmesg | tail -5
[ 2827.718195] usb 6-1: new high-speed USB device number 4 using xhci_hcd
[ 2827.882776] usb 6-1: New USB device found, idVendor=2345, idProduct=7654, bcdDevice= 2.23
[ 2827.882787] usb 6-1: New USB device strings: Mfr=1, Product=2, SerialNumber=0
[ 2827.882791] usb 6-1: Product: USB download gadget
[ 2827.882794] usb 6-1: Manufacturer: T-HEAD
$ lsusb | grep 2345
Bus 006 Device 004: ID 2345:7654 T-HEAD USB download gadget

Windows の方は先程のダウンロード先にドライバーと fastboot バイナリーがあります。

認識できたらアーカイブ内の burn_gnome_gpu.sh を実行すれば OK です。イメージ同梱の fastboot コマンドは Linux バイナリで、burn_gnome_gpu.sh もシェルスクリプトなので、Windows の方も WSL2 の Ubuntu でやったほうが簡単かもしれません。ドライバーも入れる必要がありません(認識のさせ方を記事の最後の「関連」に記載しておきます)

$ pwd
/home/hiromasa/devel/riscv/LPi4A/images/0511_gnome
$ ls -laF
合計 4259020
drwxrwxr-x 2 hiromasa hiromasa       4096  5月 12 12:04 ./
drwxrwxr-x 3 hiromasa hiromasa       4096  5月 17 16:05 ../
-rw-rw-r-- 1 hiromasa hiromasa   62914560  5月 12 11:35 boot-20230510-230240.ext4
-rwxrwxr-x 1 hiromasa hiromasa        368  5月 12 11:29 burn_gnome_gpu.sh*
-rwxrwxr-x 1 hiromasa hiromasa    2230360  5月  5 23:47 fastboot*
-rw-rw-r-- 1 hiromasa hiromasa       2022  5月 12 11:38 image_intro.txt
-rw-r--r-- 1 hiromasa hiromasa      68443  5月 12 11:36 light-lpi4a_1.85GHz.dtb
-rw-r--r-- 1 hiromasa hiromasa      68464  5月 12 11:36 light-lpi4a_2Ghz.dtb
-rw-rw-r-- 1 hiromasa hiromasa        259  5月 12 11:21 md5sum.txt
-rw-rw-r-- 1 hiromasa hiromasa 4294967296  5月 12 11:32 rootfs-20230511-183752-gnome.ext4
-rw-rw-r-- 1 hiromasa hiromasa     957728  5月 12 11:21 u-boot-with-spl-lpi4a-20230510.bin
$ cat burn_gnome_gpu.sh
#! /bin/sh
# Script to flash images via fastboot, edit image path first
#


sudo ./fastboot flash ram ./u-boot-with-spl-lpi4a-20230510.bin
sudo ./fastboot reboot
sleep 10
sudo ./fastboot flash uboot ./u-boot-with-spl-lpi4a-20230510.bin
sudo ./fastboot flash boot ./boot-20230510-230240.ext4
sudo ./fastboot flash root ./rootfs-20230511-183752-gnome.ext4

ちなみに、最初は慎重にやるために burn_gnome_gpu.sh を使わずに 1行づつ手で実行していたのですが flash uboot が waiting device になってしまいうまくいきませんでした。シェルスクリプトでやるとうまくいったので、reboot 後の sleep 10 後のタイミングが重要なのかもしれません。

$ ./fastboot --version
fastboot version 31.0.3-7562133
Installed as /home/hiromasa/devel/riscv/LPi4A/images/0511_gnome/fastboot
$ file fastboot
fastboot: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.24, not stripped
$ ./burn_gnome_gpu.sh
[sudo] hiromasa のパスワード:
Sending 'ram' (935 KB)                             OKAY [  0.248s]
Writing 'ram'                                      OKAY [  0.002s]
Finished. Total time: 0.262s
Rebooting                                          OKAY [  0.001s]
Finished. Total time: 0.202s
Sending 'uboot' (935 KB)                           OKAY [  0.057s]
Writing 'uboot'                                    OKAY [  0.030s]
Finished. Total time: 0.110s
Sending 'boot' (61440 KB)                          OKAY [  1.860s]
Writing 'boot'                                     OKAY [  1.439s]
Finished. Total time: 3.417s
Invalid sparse file format at header magic
Sending sparse 'root' 1/33 (113316 KB)             OKAY [  3.530s]
Writing 'root'                                     OKAY [  3.099s]
Sending sparse 'root' 2/33 (110992 KB)             OKAY [  3.236s]
Writing 'root'                                     OKAY [  2.869s]
...(snip)...
Sending sparse 'root' 32/33 (114684 KB)            OKAY [  3.522s]
Writing 'root'                                     OKAY [  2.620s]
Sending sparse 'root' 33/33 (9492 KB)              OKAY [  0.342s]
Writing 'root'                                     OKAY [  0.225s]
Finished. Total time: 213.113s
$ sudo dmesg | tail -20
[23502.628598] usb 6-1: new high-speed USB device number 4 using xhci_hcd
[23502.793534] usb 6-1: New USB device found, idVendor=2345, idProduct=7654, bcdDevice= 2.23
[23502.793545] usb 6-1: New USB device strings: Mfr=1, Product=2, SerialNumber=0
[23502.793549] usb 6-1: Product: USB download gadget
[23502.793552] usb 6-1: Manufacturer: T-HEAD
[23546.038600] usb 6-1: USB disconnect, device number 4
# ここで書き込み終了
[23547.584600] usb 6-1: new high-speed USB device number 5 using xhci_hcd
[23547.749402] usb 6-1: New USB device found, idVendor=1234, idProduct=8888, bcdDevice= 2.23
[23547.749415] usb 6-1: New USB device strings: Mfr=1, Product=2, SerialNumber=0
[23547.749419] usb 6-1: Product: USB download gadget
[23547.749422] usb 6-1: Manufacturer: U-Boot-THEAD
$ lsusb | grep 1234
Bus 006 Device 005: ID 1234:8888 Brain Actuated Technologies USB download gadget

起動

イメージが焼けたら HDMI をしっかり接続して USB-C 電源に接続すれば、ファンが始動し始め GUI が表示されると思います。ちなみにうちの 2560 x 1440 HDMI モニターだとログイン GDM から 2560 x 480 みたいな解像度で起動しました。w

$ cat /etc/debian_version
12.0
$ uname -a
Linux lpi4a 5.10.113-gbb4691fe5572 #1 SMP PREEMPT Wed May 10 14:16:50 UTC 2023 riscv64 GNU/Linux
$ arch
riscv64
$ cat /proc/cpuinfo | grep -e 'isa' -e 'freq'
isa             : rv64imafdcvsu
cpu-freq        : 1.848Ghz
isa             : rv64imafdcvsu
cpu-freq        : 1.848Ghz
isa             : rv64imafdcvsu
cpu-freq        : 1.848Ghz
isa             : rv64imafdcvsu
cpu-freq        : 1.848Ghz
$ lsmod
Module                  Size  Used by
pvrsrvkm             8585216  0
dwc3                 2584576  0
galcore              3170304  0
roles                  40960  1 dwc3
dwc3_thead             28672  0
$ sudo modinfo dwc3
filename:       /lib/modules/5.10.113-gbb4691fe5572/kernel/drivers/usb/dwc3/dwc3.ko
description:    DesignWare USB3 DRD Controller Driver
license:        GPL v2
author:         Felipe Balbi <balbi@ti.com>
alias:          platform:dwc3
alias:          of:N*T*Csynopsys,dwc3C*
alias:          of:N*T*Csynopsys,dwc3
alias:          of:N*T*Csnps,dwc3C*
alias:          of:N*T*Csnps,dwc3
depends:        roles
intree:         Y
name:           dwc3
vermagic:       5.10.113-gbb4691fe5572 SMP preempt mod_unload riscv
parm:           usb_mode:USB mode (int)
parm:           usb_speed:USB speed (int)

ログインユーザは debian と sipeed ユーザの 2つがデフォルトで用意されています。最初は sipeed/licheepi でログインすると良いでしょう。sudo することができます。

もし正しい解像度で Wayland セッションが起動しなかった場合は、GNOME 右上から Settings にたどり着いて Displays 項目から正しい解像度を選択してください。GDM ログイン画面の解像度については、本設定後、以下のコマンドで同じ解像度にできます。

sudo cp ~/.config/monitors.xml /var/lib/gdm3/.config/

eMMC の rootfs は 8GB 弱です。自分は開発ツールチェインを入れる関係で /mnt/ssd に ext4 フォーマットをした外付けの USB 3.1 SSD を接続してマウントしています。

開発専用にして fstab で /home ごとオートマウントしてもいいかもしれません。

$ sudo mkdir /mnt/ssd
$ sudo mount /dev/sda2 /mnt/ssd/
$ mount | grep ssd
/dev/sda2 on /mnt/ssd type ext4 (rw,relatime)
$ pwd
/home/sipeed
$ ls -laF | grep /mnt
lrwxrwxrwx  1 sipeed sipeed   28 May 18 15:51 .cargo -> /mnt/ssd/home/sipeed/.cargo//
lrwxrwxrwx  1 sipeed sipeed   29 May 18 16:10 .rustup -> /mnt/ssd/home/sipeed/.rustup//
lrwxrwxrwx  1 sipeed sipeed   28 May 18 15:51 .wasmer -> /mnt/ssd/home/sipeed/.wasmer/
lrwxrwxrwx  1 sipeed sipeed   30 May 18 15:51 .wasmtime -> /mnt/ssd/home/sipeed/.wasmtime/
lrwxrwxrwx  1 sipeed sipeed   27 May 18 15:51 devel -> /mnt/ssd/home/sipeed/devel//
lrwxrwxrwx  1 sipeed sipeed   25 May 19 03:02 vgm -> /mnt/ssd/home/sipeed/vgm//

gcc/clang や Rust 等を導入しています。また、リソース監視系のコマンドと openssh-server も入れました。

$ sudo apt install curl wget
$ sudo apt install build-essential cmake git
$ sudo apt install libssl-dev pkg-config
$ sudo apt install clang llvm-dev
$ sudo apt install lm-sensors nmon htop
$ sudo apt install ssh openssh-server # ssh リモート接続したい場合
$ sudo apt install ffmpeg

openssh-server で導入した SSH でのリモート接続はデフォルトでパスワード認証になっています。接続先は ip address コマンドで IP アドレスを取得すると良いでしょう。

また、執筆時点では DHCP では起動のたびにアドレスが変わってしまうようなので(MAC アドレスが変わってしまう)、GNOME のネットワーク設定から固定にしたほうがいいかもしれません。(既にパッチが入っているのを確認しましたので次のリリースで修正されるはずです)

Every boot, a random MAC address. #7

導入後のファイルシステム:

$ df -k | grep /
/dev/root        6923792  4687340   1893296  72% /
devtmpfs         3822156        0   3822156   0% /dev
tmpfs            3986160        0   3986160   0% /dev/shm
tmpfs            1594464     2272   1592192   1% /run
tmpfs               5120        8      5112   1% /run/lock
/dev/mmcblk0p2     52197    24842     23055  52% /boot
tmpfs             797232       28    797204   1% /run/user/1001
/dev/sda2      115012332 38564448  71671768  35% /mnt/ssd

なお、Rust toolchaine は Debian の rust-all ではなく rustup で導入しています。上記の通り Rust 系の導入場所はシンボリックリンクで USB-SSD 側に飛ばしていますので rootfs の容量には含まれていません。

$ # rustup
$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
$ # wasmer
$ curl https://get.wasmer.io -sSfL | sh
$ # wasmtime
curl https://wasmtime.dev/install.sh -sSf | bash
$ pwd
/home/sipeed/.cargo
$ cat config.toml
[env]
TMPDIR = { value = "/mnt/ssd/home/sipeed/tmp2", force = true }
$ # cargo install する場合は rootfs /tmp を使われないように --target-dir を指定
$ cargo install --target-dir /mnt/ssd/home/sipeed/tmp alacritty

以下、LicheePi 4A 上で Rust アプリケーションのビルドをしている様子。RISC-V 4コア全てが正しく使われ 100% となり、SoC の温度は 60度未満くらいに。ファンコントロールもうまく動作しています。

$ watch df -k
$ watch sensors
$ sudo cat /sys/devices/system/cpu/cpu*/cpufreq/cpuinfo_cur_freq

以下、観測できた CPU クロック周波数です。コメントは自分が書いたものです。

# /sys/devices/system/cpu/cpu*/cpufreq/cpuinfo_cur_freq の結果例
800000  # Silence
1500000 # Normal
1848000 # Peek
300000  # Sleep?

一日中 rustc などでビルドを回していたら、一度だけサーマルスロットリング機能が働いて速度が低下するのを確認しました。断続的に休んだり働いたりする動きとなります。

セルフホスト開発などでヘビーに使う場合は、ターミナルマルチプレクサなどで画面分割して、次のように CPU クロック等を watch して使うと良いでしょう。なお、Debian イメージは最大 1.85GHz で動作するように設定されています。(Wiki にありますが、個体によっては設定を変更して 2GHz でも動作するかもとのことです)

sudo watch -n1 cat /sys/devices/system/cpu/cpu*/cpufreq/cpuinfo_cur_fre

これらのリソース情報をターミナルマルチプレクサ Zellij を使い、リモート ssh 接続時に一気に表示するレイアウトファイル(.config/zellij/layouts/licheepi.kdl)をつくってみました。

後述していますが現在 LPi 上では Zellij が動作しないので、ssh 接続元ホスト側用です。Windows の方は WSL2/Ubuntu を使うと便利だと思います。

zellij -l licheepi

上記のコマンドひとつで以下のような画面となります。

Ubuntu/Alactirry での接続の様子(これでいつでも開発できる…!):

Windows Terminal/WSL2 Ubuntu での様子。

サイズやユーザ・パスワード、IP アドレス、ターミナルサイズに応じて適宜変更してください。

layout {
    tab name="LPi4A" focus=true {
        pane split_direction="vertical" {
            pane size="60%" close_on_exit=true {
                command "sshpass"
                args "-p" "licheepi" "ssh" "sipeed@172.16.15.201"
            }
            pane size="40%" {
                pane size="25%" close_on_exit=true {
                    command "sshpass"
                    args "-p" "licheepi" "ssh" "sipeed@172.16.15.201" "-t" "watch" "-n5" "df" "-k"
                }
                pane size="16%" close_on_exit=true {
                    command "sshpass"
                    args "-p" "licheepi" "ssh" "sipeed@172.16.15.201" "-t" "watch" "sudo" "cat" "/sys/devices/system/cpu/cpu*/cpufreq/cpuinfo_cur_freq"
                }
                pane size="16%" close_on_exit=true {
                    command "sshpass"
                    args "-p" "licheepi" "ssh" "sipeed@172.16.15.201" "-t" "watch" "sensors"
                }
                pane size="43%" close_on_exit=true {
                    command "sshpass"
                    args "-p" "licheepi" "ssh" "sipeed@172.16.15.201" "-t" "htop"
                }
            }
        }
    }
    tab name="Host"
    default_tab_template {
        pane size=1 borderless=true {
            plugin location="zellij:tab-bar"
        }
        children
        pane size=2 borderless=true {
            plugin location="zellij:status-bar"
        }
    }
}

ssh 接続にはパスワード認証を使っています。また LPi Debian ユーザには sudo に NOPASSWORD していますので、ご理解の上、必要ならアレンジしてください。

ホスト側 Linux

sudo apt install sshpass

LPi 側 Debian

echo "sipeed ALL=(ALL:ALL) NOPASSWD: ALL" | sudo tee /etc/sudoers.d/users

あとこれは趣味ですが、デフォルトエディタを vim にしています。

$ sudo update-alternatives --config editor
There are 3 choices for the alternative editor (providing /usr/bin/editor).

  Selection    Path                Priority   Status
------------------------------------------------------------
  0            /bin/nano            40        auto mode
  1            /bin/nano            40        manual mode
* 2            /usr/bin/vim.basic   30        manual mode
  3            /usr/bin/vim.tiny    15        manual mode

Press <enter> to keep the current choice[*], or type selection number:

Alacritty

試しに、GPU アクセラレーションを使う Rust 製のターミナルエミュレータである Alacritty のビルドを LPi 上でしてみました。

$ sudo apt install pkg-config libfontconfig-dev
$ time cargo install --target-dir /mnt/ssd/home/sipeed/tmp alacritty
...(snip)..
   Installed package `alacritty v0.12.0` (executable `alacritty`)
real    16m22.428s
user    60m41.044s
sys     1m26.451s

起動ですが(予想はしていましたが)恐らく GPU ドライバーが未完成のため真っ黒画面でした。コマンドを打つとカーソルは動いているのが見えるので動作はしていそうです。(スクリーンショット右下で CPU が MAX になっているのは偶然です)

Zellij

ターミナルマルチプレクサ・ワークスペースである Zellij のビルド。こちらも Rust 製。

$ time cargo install --target-dir /mnt/ssd/home/sipeed/tmp zellij
real    51m20.308s
user    127m50.878s
sys     2m56.126s
$ zellij
Error occurred in server:

  × Thread 'async-std/runtime' panicked.
  ├─▶ At /home/sipeed/.cargo/registry/src/github.com-1ecc6299db9ec823/wasmer-compiler-cranelift-2.3.0/src/config.rs:73:45
  ╰─▶ construct Cranelift ISA for triple: Unsupported

実はこれも予想通りの結果なのですが、Zellij がプラグインシステムのために内蔵する WebAssembly ランタイム(Wasmer 2.3)が riscv64gc-unknown-linux-gnu に対応していないため Unsuppoted でダウンしました。OK です。

上述の通り、本記事のスクリーンショットで使われている Zellij は、ssh 接続元のホスト側で動作しているものです。 LPi 4A 側でターミナルマルチプレクサを使いたい場合は tmux を使うと良いでしょう。

sudo apt install tmux

Wasmer

WebAssembly ランタイムである Wasmer(3.2 以降で RISC-V を initial support)を導入して、自分がつくっている VGM parser を動作させてみました。FM 音源ライブラリである ymfm を使って波形合成を行っています。

Wasmer の速度が思ったとおりにでなくてリアルタイム演算できていないのですが、メインボード側の I2S デバイスで発音しています。(詳しくは動画概要欄をご覧ください)

Rust LLVM ビルドしたプログラムの実行速度については現在いろいろ検証中です。どうも生成するバイナリーが機械の性能に対して想定より遅いような気がしていますが、はっきりとまだ特定できていません。

target=riscv64gc-unknown-linux-gnu だけでなく、LLVM に RISC-V C910 に合わせた optimize option を指定する必要があるかもしれません。

https://github.com/h1romas4/libymfm.wasm

This repository is an experimental WebAssembly build of the [ymfm](https://github.com/aaronsgiles/ymfm) Yamaha FM sound cores library.

自分がビルドをミスしており最適化オプションがうまくコンパイラに渡っていないだけでした…。修正後はシングルスレッド性能になりますが、非常にパワフルな速度で動作しています。

実行メモ:

$ # 54.vzg: Music Duration: 00:01:41.94
$ time wasmer run --cranelift ~/.local/share/libymfm/libymfm-cli.wasm --mapdir /:"$(pwd)" -- -r 44100 54.vgz > /dev/null

real    0m44.462s
user    0m42.783s
sys     0m1.856s
$ wasmer run --cranelift ~/.local/share/libymfm/libymfm-cli.wasm --mapdir /:"$(pwd)" \
-- -r 44100 54.vgz | ffplay -infbuf -nodisp -autoexit -f f32le -ar 44100 -ac 2 -i -

顛末をツイッターでいくつかつぶやいていますのでご参考程度で…

https://twitter.com/h1romas4/status/1660172653704146944

サウンド系メモ

Debian sid なので PipeWire と WirePlumber でオーディオデバイスが制御されています。Wayland セッション「未ログイン時」は auto_null というフォールバックデバイスになって I2S から発音しないようです。

LPi 関係ありませんが、、

pw-top

$ pw-top # Wayland session login 時
alsa_output.platform-lightsound_1.stereo-fallback
$ pw-top # Wayland session no login 時
auto_null
$ # wireplumber.service がこのあたりで動的に制御しているようだ
$ ls -alF /usr/share/wireplumber/scripts
$ ls -alF /usr/share/wireplumber/main.lua.d

ssh 経由で GUI を使わずに I2S を発音させたい場合は、(この方法が最良か分かりませんが)とりあえず、ユーザーを audio グループに入れておくと /dev/snd 配下の ALSA sound device が見えて認識され、Wayland セッションに入らなくても cli コマンドから PipeWire 経由で音声が再生できるようになります。

$ sudo usermod -aG audio sipeed
$ sudo reboot
$ sudo apt install alsa-utils
$ aplay -l
**** List of PLAYBACK Hardware Devices ****
card 0: LightSoundCard [Light-Sound-Card], device 0: light-i2s-dai-ES8156 HiFi es8156.5-0008-0 [light-i2s-dai-ES8156 HiFi es8156.5-0008-0]
  Subdevices: 1/1
  Subdevice #0: subdevice #0

ALSA 系メモ:

$ sudo dmesg | grep -A1 ALSA
[    5.765048] ALSA device list:
[    5.772741]   #0: Light-Sound-Card
$ cat /proc/devices | grep alsa
116 alsa
$ cat /proc/asound/devices
  0: [ 0]   : control
 16: [ 0- 0]: digital audio playback
 25: [ 0- 1]: digital audio capture
 33:        : timer
$ cat /proc/asound/cards
 0 [LightSoundCard ]: Light-Sound-Car - Light-Sound-Card
                      Light-Sound-Card
$ cat /proc/asound/pcm
00-00: light-i2s-dai-ES8156 HiFi es8156.5-0008-0 : light-i2s-dai-ES8156 HiFi es8156.5-0008-0 : playback 1
00-01: light-i2s-dai-ES7210 ADC 0 es7210.5-0040-1 : light-i2s-dai-ES7210 ADC 0 es7210.5-0040-1 : capture 1
$ ls -laF /dev/snd/
total 0
drwxr-xr-x   3 root root      140 Feb 28 11:15 ./
drwxr-xr-x  17 root root    14000 May 22 21:36 ../
drwxr-xr-x   2 root root       60 Feb 28 11:15 by-path/
crw-rw----+  1 root audio 116,  0 Feb 28 11:15 controlC0
crw-rw----+  1 root audio 116, 16 Feb 28 11:15 pcmC0D0p
crw-rw----+  1 root audio 116, 25 Feb 28 11:15 pcmC0D1c
crw-rw----+  1 root audio 116, 33 Feb 28 11:15 timer
$ # もし ALSA 直で音を鳴らす場合
$ aplay -D hw:0,0 test.wav

pipewire 経由で音がノイズだけになったり、サンプリングレートがおかしくなった時(何かのタイミングで発生):

$ systemctl --user restart pipewire.service pipewire-pulse.service

PipeWire 経由の cli からのボリューム制御はこちらの pw-volume が便利でした。Rust 製なのでビルドして使うと良いでしょう。(標準の wpctrl コマンドでも良いかもしれません)

https://github.com/smasher164/pw-volume

Basic interface to PipeWire volume controls

$ git colne https://github.com/smasher164/pw-volume.git
$ cd pw-volume
$ cargo build --release
$ target/release/pw-volume --help
pw-volume
Basic interface to PipeWire volume controls

USAGE:
    pw-volume <SUBCOMMAND>

OPTIONS:
    -h, --help    Prints help information

SUBCOMMANDS:
    change    adjusts volume by decimal percentage, e.g. '+1%', '-0.5%'
    mute      mutes audio [possible values: on, off, toggle]
    status    get volume and mute information
$ cp -p target/release/pw-volume ~/.local/bin/
$ pw-volume status
{"percentage":6, "tooltip":"6.3997%"}

なお、メインボード側に配置されているラインアウト I2S の音質ですが、オーディオブロックのハイパスフィルター値が高く設定されており低音が若干出にくくなっています。次の rev では修正されるとのこと。

MAME

Debian sid に MAME 0.251 が入っています。

$ sudo apt install mame

残念ながら SDL2 が opengl を見つけられないというメッセージで起動せず。

$ ./mame -window -video soft -window -videodriver wayland -audiodriver pipewire
OpenGL not supported on this driver: Couldn't find matching EGL config (call to eglChooseConfig failed, reporting an error of EGL_SUCCESS)
video_init: Initialization failed!

gles なら?vulkan なら?等

$ ./mame -window -video bgfx -bgfx_backend gles -window
../../../../../3rdparty/bgfx/src/glcontext_glx.cpp (198): BGFX FATAL 0x00000002: Failed to create GL 2.1 context.
Aborted
$ ./mame -window -video bgfx -bgfx_backend vulkan -window
../../../../../3rdparty/bgfx/src/glcontext_glx.cpp (198): BGFX FATAL 0x00000002: Failed to create GL 2.1 context.
Aborted
$ sudo apt install vulkan-tools
$ vulkaninfo
ERROR: [Loader Message] Code 0 : vkCreateInstance: Found no drivers!
Cannot create Vulkan instance.
This problem is often caused by a faulty installation of the Vulkan driver or attempting to use a GPU that does not support Vulkan.
ERROR at ./vulkaninfo/vulkaninfo.h:674:vkCreateInstance failed with ERROR_INCOMPATIBLE_DRIVER
$ sudo systemctl status pvrsrvkm.service
× pvrsrvkm.service - Imagination GPU BXM-4-64 driver init Service.
     Loaded: loaded (/etc/systemd/system/pvrsrvkm.service; enabled; preset: enabled)
     Active: failed (Result: exit-code) since Fri 2023-05-26 14:50:32 UTC; 1 day 23h ago
   Duration: 192ms
    Process: 385 ExecStart=sh usr/share/gpu/insmod.sh (code=exited, status=1/FAILURE)
   Main PID: 385 (code=exited, status=1/FAILURE)
        CPU: 171ms

May 26 14:50:32 lpi4a systemd[1]: Started pvrsrvkm.service - Imagination GPU BXM-4-64 driver init Service..
May 26 14:50:32 lpi4a sh[387]: insmod: ERROR: could not insert module /lib/modules/5.10.113-gbb4691fe5572/extra/pvrsrvkm.ko: File exists
May 26 14:50:32 lpi4a systemd[1]: pvrsrvkm.service: Main process exited, code=exited, status=1/FAILURE
May 26 14:50:32 lpi4a systemd[1]: pvrsrvkm.service: Failed with result 'exit-code'.
$ ls -alF /lib/firmware/rgx*
-rwxr-xr-x 1 debian debian 122880 May 26 14:14 /lib/firmware/rgx.fw.36.52.104.182*
-rwxr-xr-x 1 debian debian 306660 May 26 14:14 /lib/firmware/rgx.sh.36.52.104.182*
$ sudo modinfo pvrsrvkm
filename:       /lib/modules/5.10.113-gbb4691fe5572/extra/pvrsrvkm.ko
license:        Dual MIT/GPL
author:         Imagination Technologies Ltd. <gpl-support@imgtec.com>
alias:          platform:rgxthead
alias:          of:N*T*Cimg,gpuC*
alias:          of:N*T*Cimg,gpu
depends:
name:           pvrsrvkm
vermagic:       5.10.113-gbb4691fe5572 SMP preempt mod_unload riscv
parm:           gPVRDebugLevel:Sets the level of debug output (default 0x7) (uint)

自分がなにか勘違いしている可能性もありますが、いったん GPU ドライバーのアップデートを楽しみに待ちたいと思います。

なお MAME のビルドについてですが、Debian sid 版の 0.251 より先の執筆時最新の MAME 0.254 との間で、ビルドスクリプトから RISC-V へ渡るデフォルトコンパイルオプションが変わっていて、コンパイルとリンクがエラーで落ちてしまうようなので、最新 MAME ソースからビルドする場合は以下の patch を当ててみてください。

riscv64 build patch

diff --git a/scripts/genie.lua b/scripts/genie.lua
index 9a1f3140..310ec2f2 100644
--- a/scripts/genie.lua
+++ b/scripts/genie.lua
@@ -1134,6 +1134,10 @@ if (_OPTIONS["PLATFORM"]=="arm64") then
 end

 if (_OPTIONS["PLATFORM"]=="riscv64") then
+       buildoptions {
+               "-Wno-cast-align",
+               "-fpic",
+       }
        defines {
                "PTR64=1",
        }

LPi 4A GPU ドライバーがリリースされれば、MAME 0.253 から入った新 m68000 エミュコアを RISC-V で試すことができるはずです。

ちなみにコンパイル時間ですが、MAME のビルドをしている方だとおなじみだと思いますが make -j4 などで並行コンパイルすると、src/emu 配下の emumem* あたりでメモリーを 8GB 近くまでとってしまい I/O wait スローダウンしてしまうパターンがありますので、make -j3 くらいがいいかと思います。

$ sudo apt-get install libsdl2-dev libsdl2-ttf-dev libasound2-dev libxinerama-dev libxi-dev # qtbase5-dev qtbase5-dev-tools
$ git clone https://github.com/mamedev/mame.git
$ cd mame
$ # apply riscv64 build patch - scripts/genie.lua
$ time make -j3 SOURCES=src/mame/neogeo/neogeo.cpp USE_QTDEBUG=0

neogeo ドライバーだけ指定して 120分といったところでした。今回はベンチマーク的にセルフコンパイル(USB3 -SSD を使用 )していますが、さすがにこの規模になるとクロスコンパイル環境をつくったほうがいいかもしれません。

その他

Node.js は unofficial build で RISC-V 版があります。動くのかな?(未検証)

https://unofficial-builds.nodejs.org/download/release/v18.16.0/

node-v18.16.0-linux-riscv64.tar.gz

Chromium はメインラインに RISC-V ビルドがないようで対応版を以下で作業されているようです。まだ動作しないとドキュメントにはありました(未検証)

https://github.com/revyos/chromium-109.0.5414.119

Android が RISC-V 対応するとどこかで見たのでそのうちメインラインにも入るかもしれません。

ちなみにまだ詳しく見ていないですが、Firefox の WebAssembly モジュールは RISC-V では disable になっているような気がします(軽く動かしたら WebAssembly Object がないとエラーになりました)

おわりに

というわけで、非常に高性能な実機の RISC-V Linux を堪能できて楽しいボードだと感じています。各アプリケーションの RISC-V 対応も進化が見られてホットですね。

LM4A モジュールが単体発売されたら、自作でシンセサイザーになるボードをつくってみたいと夢見ていろいろ検証中です。モジュールを使ったプロダクトの発売もいろいろ予定されているようなので、そちらも楽しみにしています…!


関連

Zellij ターミナルワークスペースと WebAssembly プラグイン

自分は Linux をセットアップして最初に行う設定が tmux 関連だったりするのですが、新しい Windows 機の WSL2 にも秘伝の .tmux.conf を入れつつ、そうだ Zellij を試してみようと思い立ち、良い感じだったので紹介です。

Zellij

A terminal workspace with batteries included

Zellij (ぜりーじゅと読むそうです)は、いわゆるターミナルマルチプレクサーに種別されるソフトウェアで、セッション管理や、ターミナルエミュレータ上での画面分割やタブ制御を行うことができます。

Windows Terminal WSL2 上で動作している Zellij。

tmux よりも新しく作られていることもあり、デフォルトで自動起動スクリプトの出力やシステムクリップボード連携設定などが備わっており、また操作系も画面下部に表示することができるので非常に導入しやすいつくりとなっています。

導入

絶賛開発中 (記事執筆時点 0.36.0) の Rust でできたソフトウェアにて、導入は cargo install を使ってソースからビルドする方法が良いと思います。cargo なのでコマンド一発です。

Zellij User Guide – Installation

The easiest way to install Zellij is through a package for your OS.

If one is not available for your OS, you can download a prebuilt binary or even try Zellij without installing.

Otherwise, you can install it with Cargo.

(cargo を含む)Rust のツールチェインが未導入の場合は rustup で導入します。

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

導入後 PATH などが設定されますので、いったんシェルを落とし上げします。

cargo install で zellij を導入。

cargo install --locked zellij
$ ls -laF ~/.cargo/bin/ | grep zel
-rwxr-xr-x  1 hiromasa hiromasa 17380208  4月 20 17:31 zellij*

なお、cargo install で導入したバイナリーのアップデートは cargo-install-update を使うことで行うことができます。

# cargo-update の導入 (初回のみ)
cargo install cargo-install-update
# アップデート確認
cargo install-update -l
    Polling registry 'https://index.crates.io/'.....

Package         Installed  Latest   Needs update
espup           v0.2.9     v0.4.0   Yes
cargo-generate  v0.18.2    v0.18.2  No
cargo-update    v13.0.1    v13.0.1  No
powerline-rs    v0.2.0     v0.2.0   No
zellij          v0.36.0    v0.36.0  No
# Needs update が Yes なものをアップデート
cargo install-update -a

フォント

Zellij は標準で powerline フォントに対応しています。お使いのターミナルエミュレータのフォントに設定しましょう。自分は HackGen を使わせていただいています。(とてもきれいです…!)

https://github.com/yuru7/HackGen

白源 (はくげん/HackGen) は、プログラミング向け英文フォント Hack と、源ノ角ゴシックの派生フォント源柔ゴシックを合成したプログラミングフォントです。

設定フォントは「源ノ角ゴシックの派生フォント源柔ゴシックに、Powerline フォントを含む Nerd Fonts を合成した」 HackGen_NF_*.zipHackGenConsoleNF-Regular.ttfHackGen Console NF)を設定すると良いと思います。

なお、Windows Terminal だと Ricty 系の powerline フォントの罫線がずれるようです。

自動起動設定

zellij 起動確認後は、シェル設定ファイルに Zellij の自動起動設定を入れておくと便利です。

Zellij User Guide – Autostart on shell creation

Autostart a new zellij shell, if not already inside one. Shell dependent, fish:

自分は .bashrc ですので次のようになります。

echo 'eval "$(zellij setup --generate-auto-start bash)"' >> ~/.bashrc

ちなみに zellij setup --generate-auto-start bash の中身は次のようになっています。tmux だと自分で書かなければいけないので小粋な感じでぐー。

if [[ -z "$ZELLIJ" ]]; then
    if [[ "$ZELLIJ_AUTO_ATTACH" == "true" ]]; then
        zellij attach -c
    else
        zellij
    fi

    if [[ "$ZELLIJ_AUTO_EXIT" == "true" ]]; then
        exit
    fi
fi

というわけで、お好みで ZELLIJ_AUTO_ATTACH を設定すると、Windows Teminal などがダウンしたときにや誤ってタブを落とした時に自動復帰できて良いかと思います。

`.bashrc`

ZELLIJ_AUTO_ATTACH=true
eval "$(zellij setup --generate-auto-start bash)"

関連して Zellij の default key binding に Ctrl + s, s がバッファのサーチに割り当てられていて、自分はうっかりロックを忘れて tty に START/STOP を送ってしまうことがあったので、以下も一緒に入れて tty の制御コード受付を disable にしています。

if [[ -t 0 && $- = *i* ]]
then
    stty -ixon
fi

設定

Zellij はシングルバイナリーで動作するようになっていますが、設定ファイルを使うこともできます。デフォルト設定を最初に出力すると良いかと思います。

Zellij User Guide – Configuration

Zellij uses KDL as its configuration language.

mkdir ~/.config/zellij
# 設定ファイル
zellij setup --dump-config > ~/.config/zellij/config.kdl

自分のデフォルト設定から変えている定義は次の通りです。ほとんど変えてないですねw

// ペインのフレームをボーダレスに
pane_frames false
// 起動時の操作モードをインターフェース locked に
default_mode "locked"
// 配色テーマ
themes {
    tokyo-night-dark {
        fg 169 177 214
        bg 26 27 38
        black 56 62 90
        red 249 51 87
        green 158 206 106
        yellow 224 175 104
        blue 122 162 247
        magenta 187 154 247
        cyan 42 195 222
        white 192 202 245
        orange 255 158 100
    }
}
theme "tokyo-night-dark"

テーマは次のドキュメントからいただいてきたものです。

Zellij User Guide – Themes

Themes can be specified either in the configuration file under the themes section, or directly in a separate file.

レイアウト

Zellij の肝のひとつですが、レイアウトファイルを使うことによりペインに好きなパーツを配置することができます。

mkdir ~/.config/zellij/layouts
# デフォルト画面レイアウトファイル
zellij setup --dump-layout default > ~/.config/zellij/layouts/default.kdl

pain の一番目に後述する自分の WebAssembly プラグインを追加していますが、ファイルの内容は次のようになります。

layout {
    pane size=1 borderless=true {
        plugin location="file:/home/hiromasa/.config/zellij/plugins/zellij-datetime.wasm"
    }
    pane size=1 borderless=true {
        plugin location="zellij:tab-bar"
    }
    pane
    pane size=2 borderless=true {
        plugin location="zellij:status-bar"
    }
}

この定義が起動時の画面レイアウトと対応する形のつくりになっています。タブバーや下部のステータスバーがデフォルトプラグインとして実装されているのが分かります。

レイアウトファイルの記述方法は以下のドキュメントが参考になります。

Zellij User Guide – Layouts

Layouts are text files that define an arrangement of Zellij panes and tabs.

You can read more about creating a layout

また Zellij 起動時の引数でレイアウトファイルを指定できるため、作業によってワークスペースを切り替えるようなことも可能です。

シングルボードコンピュータへの ssh 接続を行い、各種リソースを表示するようなレイアウトを作成した例。(後述していますがこの例で使っている RISC-V Linux な SBC では直接 Zellij が動作しないため、ホスト側から ssh で接続しています。RPi 4A など Arm 機ではリモート側で Zellij を起動できるでしょう)

layout {
    tab name="LPi4A" focus=true {
        pane split_direction="vertical" {
            pane size="60%" close_on_exit=true {
                command "sshpass"
                args "-p" "licheepi" "ssh" "sipeed@172.16.15.201"
            }
            pane size="40%" {
                pane size="25%" close_on_exit=true {
                    command "sshpass"
                    args "-p" "licheepi" "ssh" "sipeed@172.16.15.201" "-t" "watch" "-n5" "df" "-k"
                }
                pane size="16%" close_on_exit=true {
                    command "sshpass"
                    args "-p" "licheepi" "ssh" "sipeed@172.16.15.201" "-t" "watch" "sudo" "cat" "/sys/devices/system/cpu/cpu*/cpufreq/cpuinfo_cur_freq"
                }
                pane size="16%" close_on_exit=true {
                    command "sshpass"
                    args "-p" "licheepi" "ssh" "sipeed@172.16.15.201" "-t" "watch" "sensors"
                }
                pane size="43%" close_on_exit=true {
                    command "sshpass"
                    args "-p" "licheepi" "ssh" "sipeed@172.16.15.201" "-t" "htop"
                }
            }
        }
    }
    tab name="Host"
    default_tab_template {
        pane size=1 borderless=true {
            plugin location="zellij:tab-bar"
        }
        children
        pane size=2 borderless=true {
            plugin location="zellij:status-bar"
        }
    }
}

Ubuntu/Alactirry 上の動作:

Windows Terminal/WSL2 Ubuntu で動作させている様子(WSL2 Ubuntu 上の zellij-server が接続を維持してくれるのでターミナルを落としてしまっても復帰できる例):

ペイン内の操作シンク

クラスタ構成の同一構成複数サーバとかで同じコマンドを入力したい時につかうあれも OK です。tab 操作から sync を選択することで、ペイン全てに同じ操作ができます。

WebAssembly プラグイン拡張

Zellij プラグインは WebAssembly/WASI で作成することができます。勉強がてら 日時 date-time を表示するプラグインを Rust でかいてみました。

https://github.com/h1romas4/zellij-datetime

This plugin adds a date and time pane to [Zellij](https://zellij.dev/), a terminal multiplexer.

amd64 Ubuntu/Alacritty で動作している様子。

なお、システムインターフェースは WASI API の範囲となります。とりあえずまだ TIMEZONE がとれない気がした(たぶん)ので +9 JST 固定でビルドしました。

WASI API 超えをしたい場合のアイディアメモ:(ホスト側で情報を取得するシェルスクリプトを動かして、結果をファイルかソケットでプロキシしていると思います)

https://github.com/yvt/zellij-cpulamp

Minimal CPU activity indicator plugin for Zellij

ファイルシステムへのセキュリティーポリシーなどがどうなっているかなど継続調査。ちなみに、WebAssembly ランタイムは Wasmer (執筆時点 2.3 系) が使われています。

Wasm を使っているため .wasm ファイルは amd64 や Arm といった CPU の違いを気にせず同一バイナリーが使えますので便利です。 .wasm を配置してレイアウトファイルで読み込ませれば動作を開始します。(Wasmer は 3.2 から RISC-V 対応なので残念ながら現時点では RISC-V 機では動作しません)

今回は Rust を使いましたが、言語を選ばずプラグインがかけるのも良いです。

ただ、まだ Rust と Zig 言語以外にはバインディングがなく、そのほかの言語では、引数をもらうのに WASI の stdin から JSON をパースする処理をかく必要がありそうです。Rust のバインディングも内部的にはこの構成になっています。この辺は、WebAssembly の Component Model (Interface Types) の仕様ができれば改善されることでしょう。


今回 Zellij のプラグインをかいていたら、昔よく VZ Editor のマクロをかいていたことを思い出しました。。Zellij 拡張も テトリスやスネークゲームなどもつくれそうな雰囲気です。

これだけ拡張性が高いと心強いですね。良ければお試しください。


関連

AssemblyScript で WebAssembly を知る

毎年恒例 ゆるWeb勉強会@札幌 Advent Calendar 2022 への投稿です…!

札幌で開催しているWeb系勉強会、「ゆるWeb勉強会@札幌」の2022年アドベントカレンダーです。

過去発表内容のまとめ、発表する機会の無かったネタ、勉強会に参加したいけどできなかったので、などなど、Webに関するキーワードを中心としつつ自由に書いていってください!

毎年 WebAssembly 関連を投稿させていただいておりますが、今年は WebAssembly が理解の手助けとなるような、入門記事を書いてみたいと思います。

AssemblyScript 言語

最初に WebAssembly のおおよそですが、 プログラムソースコードをビルドすることにより Wasm バイナリー(.wasm)を出力し、このファイルを WebAssembly ランタイムに読ませることでプログラムを動作させる技術です。

この記事で取り上げる AssemblyScript 言語は、TypeScript Like なソースコードを Wasm バイナリーにビルドできるコンパイラーです。npm で簡単にツールチェインが導入でき、また WebAssembly 専用の言語となっているため、端的に理解しやすく、入門にも最適だと感じます。

The AssemblyScript Book

AssemblyScript compiles a variant of TypeScript (opens new window) (a typed superset of JavaScript) to WebAssembly (opens new window) using Binaryen (opens new window), looking like:

WebAssembly ランタイムを持つ主要ユーザのひとつはウェブブラウザーで、モバイルを含む主要 4 ウェブブラウザーで .wasm を実行可能です。また、ウェブブラウザーから JavaScript ランタイムが node.js として取り出されたように、ウェブブラウザー外で .wasm を動かす動きも近年活発です。

なお、.wasm のビルドに対応しているプログラム言語は、大きく 2種類のパターンがあり、直接 .wasm にビルドするものと、言語のインタープリタ(ランタイム)を .wasm にして、ソースコード(もしくは中間コード)をそのまま与えて動作するものに分けられます。

前者の代表は、Rust、C/C++、TinyGo などなど、そして AssemblyScript もこの仲間です。

後者は、Python(Pyodide)、Ruby、.NET/C# などが該当します。つまり、これらのインタープリタやランタイムが C/C++ であるということに立脚すると、前者の方式で Wasm ビルドできればスクリプトファイルが実行できる、というイメージです。

というわけで、本記事では WebAssembly 自身の理解の手助けとなるよう、直接プログラムが .wasm となり、ランタイムとのインターフェースもシンプルで分かりやすい AssemblyScript を使って解説を進めていきます。

サンプルプログラム

AssemblyScript/WebAssembly を使って時計を描画するプログラムをつくってみました。

次のような要素が含まれています。

  • WebAssembly から直接画面描画をすることができないので、WebAssembly ランタイムから import した、指定した位置に「点」を描画する関数を呼び出し時計を描いている。
  • 上記のような特性を逆に利用すると、ランタイム環境に依存する処理が差し替えられるため、同一 WebAssembly プログラムを、ウェブブラウザーとマイコン(M5Stack) で動作させるマルチプラットフォームの例ともなっています。

ソースコード:

https://github.com/h1romas4/m5stack-core2-wasm3-as

M5Stack Core2 With Wasm3/AssemblyScript Demo

WebAssembly プログラムとランタイムのインターフェース

WebAssembly の上のプログラムはよく「計算しかできない」と言われます。これは Wasm がサンドボックスで動作し、その外側のリソースにアクセスできないためです。

外界との接続との唯一のインターフェースは、それぞれの関数の export/import と、メモリーのポインターです。

JavaScript からみた、.wasm は引数の型が不自由な関数が追加されているように見えます。

:ランタイムに関数を公開する AssemblyScript のソースコード:

– 引数の型が u32 という見慣れない型になっている。

export function clock(x: u32, y: u32, r: u32): void {
    analogClock = new AnalogClock(x, y, r);
}

export function tick(): void {
    if(analogClock != null) {
        analogClock.tick();
    }
}

:export function が入った AssemblyScript を .wasm にビルドしたアセンブリーが出力(抜粋):

– 引数が i32 という WebAssembly の型になっている。

  (export "clock" (func 16))
  (export "tick" (func 28))

  (func (;16;) (type 1) (param i32 i32 i32)
  (func (;28;) (type 2)

そしてこの関数は JavaScript から次のように呼び出すことができます。

.wasm をロードする JavaScript のソースコード:

instance.exports.wasm に定義された関数を取得できる。

async function loadWasm() {
    const response = await fetch(new URL('../dist/app.wasm', import.meta.url));
    const responseArrayBuffer = new Uint8Array(await response.arrayBuffer());
    const wasm_bytes = new Uint8Array(responseArrayBuffer).buffer;
    let module = await WebAssembly.compile(wasm_bytes);
    const instance = await WebAssembly.instantiate(module, {
        ...createImports()
    });
    wasmExports = instance.exports;
};

.wasm でエクスポートされた関数を JavaScript からコールするソースコード:

instance.exports にエクスポートされた関数の引数に number 型を渡している。

(async function() {
    await loadWasm();
    wasmExports.clock(160, 120, 120);
    setInterval(() => {
        wasmExports.tick();
        wasmExports.__collect() // clean up all garbage
    }, 500);
})();

ポイントは .wasm 関数の引数には i32, i64, f32, f64 といった WebAssembly で使える(数値)型しかとれないことです。 JavaScript のオブジェクトのようなものや、文字列、もちろん DOM などの WebIDL の型も(現在のところ)そのまま渡して処理はできません。

AssemblyScript inherits WebAssembly’s more specific integer, floating point and reference types:

逆もしかりで、.wasm 側から JavaScript の関数を呼び出す際もこれらの引数型しかとることができません。

:JavaScript の関数を import する AssemblyScript のコード:

export declare function draw_pixel(x: i32, y: i32, color: i32): void;

export declare function をビルドした .wasm のアセンブリー(抜粋):

  (type (;1;) (func (param i32 i32 i32)))
  (import "c3dev" "draw_pixel" (func (;0;) (type 1)))

:AssemblyScript に関数を公開する JavaScript のコード:

async function loadWasm() {
    // ..snip...
    const instance = await WebAssembly.instantiate(module, {
        ...createImports()
    });
    // ..snip...
};

function createImports() {
    let imports = [];
    // ..snip...
    imports['c3dev'] = {
        'draw_pixel': (x, y, color) => {
            canvasContext.fillStyle = convertRGB565toStyle(color);
            canvasContext.fillRect(x, y, 1, 1);
        },
    // ..snip...
}

.wasm 側で import した i32 という数値型 を持つ draw_pixel 関数を呼び出し、最終的に ウェブブラウザーの Canvas に指定座標にひとつ点を描いています。マイコン動作ではこの部分が LCD に点をひとつ描く C の関数が呼び出すようにしています。

(ちなみに、点が描ければ円も線も計算で描けるということで、サンプルプログラムの時計は AssemblyScript の演算処理により全て点だけで描画を行っています)

インターフェースをつくる

以上のように、WebAssembly でプログラムをかく場合は、WebAssembly ランタイムをホストしている環境との連携インターフェースの設計が肝で、特性を考慮して、どのような処理単位にすると効率的に処理できるのかを考えていきます。

また、今回の例では用いていませんが、メモリーのポインター値を共有することで、VRAM や音声波形など大きな演算結果(配列)をインターフェースすることもでき、Wasm 内の演算結果を外部に持ち出すことができます。

このようなホストへのインターフェースの標準化の実装のひとつとして WASI があり、システム時間や乱数シード、ファイルシステムやネットワークへのアクセスが規定されています。(ただしウェブブラウザーには WASI API は実装されていません)

WASI も WebAssembly 型の関数が沢山あるだけですので、覗いてみると理解しやすいかと思います。

また、Rust の wasm-pack(wasm-bindgen) や Emscripten(C/C++) など他の WebAssembly ツールチェインは、Wasm 型との引数合わせやラップ、JavaScript の呼び出しをソースコードジェネレートなどを、自動でやってくれるものとして捉えると考えやすいです。

WebAssembly のインターフェースを知ることで周辺技術が何をしているのかが見えてきそうだな、ということで本記事は書かれました。

処理を速くしたい部分や、JavaScript 外で便利なライブラリー、、圧縮展開、画像動画音声処理、言語解析器などなどを見つけたら、「WebAssembly(型) なインターフェース」を追加して呼び出す。まずは WebAssembly の実用的なユースケースのひとつと思いますので、ぜひお試しください。

関連

https://twitter.com/h1romas4/status/1610228824607985664
https://twitter.com/h1romas4/status/1612348626248044544