新年早々、WindowsとLinuxのメモリ割り当て戦略の基本的な違いをすっかり忘れていて、ひどい目に合いました。
症状としてはSteamでゲームをしてると頻繁にゲームが落ちたり、ブラウザがクラッシュします。
疑った順に、
仮想メモリの枯渇でした。Windowsは仮想メモリを物理メモリ+ページングファイルの合計量までしか割り当てません。私の環境は物理メモリ16GB+ページングファイル1GBに切り詰めていたため、仮想メモリは17GBまでしか確保できません。
ゲーム+ブラウザを起動すると仮想メモリの消費量が17GBを超えるときがあります。仮想メモリの割り当て量が上限ギリギリに達して、ゲームもしくはブラウザの運が悪い方が、仮想メモリを要求すると、割り当てに失敗します。
するとNULLポインタが返り、NULLポインタにアクセスしてゲームorブラウザがクラッシュしてしまうようです。誰一人として、仮想メモリの割り当て失敗を想定せんの?誰か1人くらいVirtualAlloc() が失敗したって教えてくれても良いのに……。
ページングファイルを適当に増やせば(とりあえず16GBくらいにした)安定しました。
気づいたきっかけはゲーム(Cities: Skylines)のクラッシュダンプです。
エラーログを見るとpaging fileの空きが1MBしかありません。Windowsではこれは仮想メモリの空きを表すそうです。これを知らなかったがために、全然関係ないドライバとか熱暴走を疑い、遠回りしてしまいました。
タスクマネージャーで「コミット済み」の値をチェックすると、仮想メモリの使用量がわかります。これがゲーム+ブラウザで17GBを超えていました。
ダメ押しで、下記のようなVirtualAlloc() APIを呼んで仮想アドレスを大量にガメる(物理メモリはほぼ消費しない)プログラムを書いて、わざと仮想メモリだけを枯渇させました。
#include <cstdio>
#include <cstdlib>
#include <windows.h>
#define CNT 16
int main()
{
const size_t s = 1024 * 1024 * 1024;
char buf[1024], *pb;
void *p[CNT];
for (int c = 0; c < CNT; c++) {
p[c] = VirtualAlloc(NULL, s, MEM_COMMIT, PAGE_READWRITE);
FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM, NULL, GetLastError(), 0, buf, sizeof(buf) - 1, NULL);
printf("%s\n", buf);
pb = (char*)p[c];
for (size_t i = 0; i < s / 8192; i++)
pb[i] = (char)i;
}
for (int c = 0; c < CNT; c++)
VirtualFree(p[c], s, MEM_DECOMMIT);
return 0;
}
この状態でゲームを動かすと容易に同じクラッシュが起こせます。というわけで仮想メモリの枯渇で確定と判断しました。
WindowsとLinuxの仮想メモリ割り当て戦略は全く違うのに、同じノリでWindowsのページングファイルを削ってしまったことですね……。一応、違いは知っていたんですが、行動に活かせず思い切りハマりました。
Windowsは仮想メモリ割当てが保守的です。仮想メモリの割り当て上限=物理メモリ+ページファイルの合計となります。
Linuxは仮想メモリの割り当て上限>物理メモリ+スワップファイルの合計となります(over commitment)。
WindowsとLinuxのメモリ割り当て戦略は、利点と欠点が逆になるだけで、どっちもどっちです。
今回の教訓をおさらいすると、Windowsを使っているのに、Linuxと同じノリでページファイルを1GBとか小さいサイズに削ると、速攻で仮想メモリが枯渇してひどい目に合うんでやめようね!ってことです。
目次: Windows
Windowsで「コミット済み」(=仮想メモリの合計)の値を求める方法がさっぱりわかりません。タスクマネージャーの「パフォーマンス」タブには下記のように値が表示されています。
しかしタスクマネージャーの「詳細」タブに表示される、各プロセスのコミットサイズ(=仮想メモリサイズのことらしい)を足しても全く足りません。どういうこと??
コミットサイズが全然信用できない例を挙げれば、AMDのRadeonドライバ関連でAMDRSServ.exeというプロセスがいます。このプロセスをタスクマネージャーで見ると「5MBしか使ってないよ」とおっしゃっています。
ところがプロセスを強制終了させると、突然700MBほど(6.2GB → 5.5GB)仮想メモリが解放されます。700MBも使っているプロセスはありませんでしたが、700MBどこから来た?意味不明ですね。
たぶんカーネル側というかドライバ内で仮想メモリをでかく取ると「コミット済み」と「各プロセスのコミットサイズの合計」の乖離が激しくなるんじゃないか?と予想していますが、調べ方がわかりません。
Windowsを使っていて仮想メモリが枯渇するような事態に陥り、調べる必要が出てきたとしても、タスクマネージャーの表示してる「コミットサイズ」は全然信用できないってことです。ひどい作りだなあ、もう。
目次: Zephyr
開発用のマシンではDebian Testingを使っているのですが、久しぶりにdist-upgradeしたところPython 3.8が消えてしまいました。Python 3.9に移行したみたいです。
アップデート時は「そうなんだ、3.9になったんだな。」くらいの認識でスルーしまいたが、Zephyrを使おうとしたら異変に気づきました。なんとZephyr SDKのGDBが動きません。どうしてこうなった。
$ riscv64-zephyr-elf-gdb riscv64-zephyr-elf-gdb: error while loading shared libraries: libpython3.8.so.1.0: cannot open shared object file: No such file or directory
Debianは元々Zephyr SDKのサポート範囲に入っていない(Ubuntuのみ)ですし、Debian Testingなんてサポートされるはずがないので、自力で解決する必要があります。
Zephyr SDKのビルド手順は簡単ですが、Debian Testingだとうまくいきません。
$ git clone https://github.com/zephyrproject-rtos/sdk-ng $ cd sdk-ng $ ./go.sh riscv64 ./go.sh: 行17: python: コマンドが見つかりません
Pythonが見つからず怒られます。Debian Testingは /usr/bin/pythonがなくなったため、go.shのpythonをpython3に書き換えてあげると動きます。他にもPython 3.8を想定している箇所があるので、Python 3.9に直します。
diff --git a/configs/riscv64.config b/configs/riscv64.config
index 295f2c0..a9fc301 100644
--- a/configs/riscv64.config
+++ b/configs/riscv64.config
@@ -46,5 +46,5 @@ CT_CC_LANG_CXX=y
CT_CC_GCC_LIBSTDCXX_NANO=y
CT_DEBUG_GDB=y
CT_GDB_V_9_2=y
-CT_GDB_CROSS_PYTHON_BINARY="python3.8"
+CT_GDB_CROSS_PYTHON_BINARY="python3.9"
CT_GDB_CROSS_BUILD_NO_PYTHON=y
diff --git a/go.sh b/go.sh
index e5442fa..7a45fd8 100755
--- a/go.sh
+++ b/go.sh
@@ -14,7 +14,7 @@ fi
COMMIT="d7da3a9c7f0f3a90bb4c71b91aea6cbc2471a541"
GITDIR=${PWD}
-JOBS=$(python -c 'import multiprocessing as mp; print(mp.cpu_count())')
+JOBS=$(python3 -c 'import multiprocessing as mp; print(mp.cpu_count())')
unameOut="$(uname -s)"
unameMachine="$(uname -m)"
SDKはbuild/output以下に生成されます。RISC-V 64であればbuild/output/riscv64-zephyr-elfです。生成されたバイナリが動くか確かめましょう。
$ cd build/output/riscv64-zephyr-elf/bin $ ./riscv64-zephyr-elf-gdb Segmentation fault
SEGVで死にました。うーん、だめそうですね……。次回以降、直せないかトライします。
目次: RISC-V
たまにRISC-V向けのQEMUの動きを見たいときがあって、デバッグビルドをするのですが、やり方を忘れがちなのでメモしておきます。
$ mkdir build $ cd build $ ../configure --target-list=riscv32-softmmu,riscv32-linux-user,riscv64-softmmu,riscv64-linux-user \ --disable-docs --enable-debug ... qemu 5.2.50 Install prefix: /usr/local BIOS directory: share/qemu firmware path: /usr/local/share/qemu-firmware binary directory: bin library directory: lib module directory: lib/qemu libexec directory: libexec include directory: include config directory: /usr/local/etc local state directory: /usr/local/var Manual directory: share/man Doc directory: /usr/local/share/doc ... thread sanitizer: NO rng-none: NO Linux keyring: YES FUSE exports: NO FUSE lseek: NO Subprojects libvhost-user: YES Found ninja-1.10.1 at /usr/bin/ninja $ ninja
ビルドが成功するとbuildディレクトリ以下にqemu-system-riscv32やqemu-system-riscv64が生成されているはずです。
目次: Zephyr
GDBとPython 3.9の組み合わせは、SEGVでクラッシュしました。調べてみると FedoraのBugzilla にドンピシャの情報が載っていました。Python 3.9とGDBの組み合わせが動かなかったこと、Red Hatの人がパッチを作ってくれて、5/28に修正されたこと、などが書いてあります。
Zephyr SDKのGDBは9.2(2020年5月23日リリース)で、上記のPython 3.9で動かすための修正は入っていません。残念。選択肢としては下記の2つが考えられます。
GDBのバージョンを変えると新たな厄災を招く恐れがあるため、今回は保守的に9.2 + パッチで行こうと思います。
Zephyr SDKは内部でCrosstool-NGというツールチェーンのビルドツールを利用しています。Zephyr SDKに突撃する前にCrosstool-NGの仕組みをおさらいし、9.2 + パッチでビルドする方法を試します。
Zephyr SDKのconfigs/ ディレクトリの下を見ると *.configファイルがたくさんあります。実はこれらはCrosstool-NGのコンフィグファイルです。このファイルをCrosstool-NGにコピーするとツールチェーンが作成できます。わかりやすいですね。
$ cd sdk-ng $ ls configs/ arc.config nios2.config xtensa_intel_byt_adsp.config arm.config riscv64.config xtensa_intel_s1000.config arm64.config sparc.config xtensa_nxp_imx8m_adsp.config i586.config x86_64-zephyr-elf.config xtensa_nxp_imx_adsp.config iamcu.config xtensa_intel_apl_adsp.config xtensa_sample_controller.config mips.config xtensa_intel_bdw_adsp.config
Crosstool-NGはツールチェーンの各モジュールにパッチを当てられます。例えばGDB 9.2ならばpackages/gdb/9.2/ の下にパッチファイルを置くと、若い番号から順番にパッチ適用してくれます。
$ ls packages/gdb/9.2/ 0000-musl_fix.patch 0004-allow-android.patch 0001-uclibc-no-gettimeofday-clobber.patch chksum 0002-xtensa-make-sure-ar_base-is-initialized.patch version.desc 0003-WIP-end-of-prologue-detection-hack.patch
このディレクトリに0005-xxxx.patchのような名前のパッチを追加すると、0004-allow-android.patchのあとにパッチを当ててくれます。便利ですね。パッチ当て処理の実装を見ましょう。
# crosstool-ng/scripts/functions
CT_DoExtractPatch()
{
...
CT_Pushd "${src_dir}/${basename}"
for d in "${patch_dirs[@]}"; do
CT_DoLog DEBUG "Looking for patches in '${d}'..."
if [ -n "${d}" -a -d "${d}" ]; then
for p in "${d}"/*.patch; do #★パッチファイルを列挙、パッチ当てる
if [ -f "${p}" ]; then
CT_DoExecLog ALL ${patch} --no-backup-if-mismatch -g0 -F1 -p1 -f -i "${p}"
fi
done
fi
done
...
ビルド後にできるログbuild.logをLooking for patchesで検索していくと、パッチを当てている箇所が確認できます。
... [DEBUG] Looking for patches in 'crosstool-ng/packages/gdb/9.2'... [DEBUG] ==> Executing: '/usr/bin/patch' '--no-backup-if-mismatch' '-g0' '-F1' '-p1' '-f' '-i' 'crosstool-ng/packages/gdb/9.2/0000-musl_fix.patch' [ALL ] patching file gdb/linux-nat.c [ALL ] patching file gdb/stopcode.h [DEBUG] ==> Return status 0 [DEBUG] ==> Executing: '/usr/bin/patch' '--no-backup-if-mismatch' '-g0' '-F1' '-p1' '-f' '-i' 'crosstool-ng/packages/gdb/9.2/0001-uclibc-no-gettimeofday-clobber.patch' [ALL ] patching file gnulib/configure [ALL ] patching file gnulib/import/m4/gettimeofday.m4 [DEBUG] ==> Return status 0 ...
Crosstool-NGがパッチを当てる順はシェルのファイル列挙順のため、ファイル名は必ずしも数字で始める必要はないです。しかし、前例に習ったほうが良いでしょう。パッチを0005-fix-python3.9.patchという名前で追加します。
$ git clone https://github.com/crosstool-ng/crosstool-ng $ cd crosstool-ng $ cat > packages/gdb/9.2/0005-fix-python3.9.patch (パッチをコピペする) $ ./bootstrap $ ./configure --enable-local $ make $ cp ../sdk-ng/configs/riscv64.config ./.config $ ./ct-ng menuconfig $ ./ct-ng build
ビルド後にできるログbuild.logを確認して、パッチが当たっていることを確かめます。
... [DEBUG] Looking for patches in 'crosstool-ng/packages/gdb/9.2'... [DEBUG] ==> Executing: '/usr/bin/patch' '--no-backup-if-mismatch' '-g0' '-F1' '-p1' '-f' '-i' 'crosstool-ng/packages/gdb/9.2/0000-musl_fix.patch' [ALL ] patching file gdb/linux-nat.c [ALL ] patching file gdb/stopcode.h [DEBUG] ==> Return status 0 [DEBUG] ==> Executing: '/usr/bin/patch' '--no-backup-if-mismatch' '-g0' '-F1' '-p1' '-f' '-i' 'crosstool-ng/packages/gdb/9.2/0001-uclibc-no-gettimeofday-clobber.patch' [ALL ] patching file gnulib/configure [ALL ] patching file gnulib/import/m4/gettimeofday.m4 [DEBUG] ==> Return status 0 ... [DEBUG] ==> Executing: '/usr/bin/patch' '--no-backup-if-mismatch' '-g0' '-F1' '-p1' '-f' '-i' 'crosstool-ng/packages/gdb/9.2/0005-python3.9.patch' [ALL ] patching file gdb/python/python.c [ALL ] Hunk #1 succeeded at 234 (offset -4 lines). [ALL ] Hunk #2 succeeded at 271 (offset -4 lines). [ALL ] Hunk #3 succeeded at 952 (offset -19 lines). [ALL ] Hunk #4 succeeded at 1552 (offset -68 lines). [ALL ] Hunk #5 succeeded at 1720 (offset -70 lines). [ALL ] patch unexpectedly ends in middle of line [DEBUG] ==> Return status 0
GDBの開発メールアーカイブから適当にコピペしてパッチを作ったので、Hunkがずれてるよって怒られましたが、パッチ当ては成功しています。あまり気にしなくても良いでしょう。ビルド後は動作確認しましょう。
$ cd ~/x-tools/riscv64-zephyr-elf/bin $ ./riscv64-zephyr-elf-gdb --version GNU gdb (crosstool-NG 1.24.0.254_fcf3233) 9.2 Copyright (C) 2020 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law.
動きました。良かった良かった。
目次: Zephyr
前回はツールチェーンビルドの仕組みを確認するためCrosstool-NGに立ち返り、正常動作するバイナリが作成できましたが、手順が多くて面倒でした。実はZephyr SDKだけでパッチを当てる仕組みがあります。
Crosstool-NGのパッチ当ては前回紹介したpackagesの下にあるパッチを使うのが基本ですが、他の場所(ローカルパッチディレクトリ)も追加できます。CT_LOCAL_PATCH_DIRというコンフィグに設定されます。
$ ./ct-ng menuconfig Paths and misc options ---> () Local patch directory
Crosstool-NGのパッチ当て処理を見るとパッチを当てる順番を選択できるようになっています。
# crosstool-ng/scripts/functions
CT_DoExtractPatch()
{
...
CT_DoLog EXTRA "Patching ${basename}"
CT_DoExecLog ALL touch "${src_dir}/.${basename}.patching"
bundled_patch_dir="${CT_LIB_DIR}/packages/${pkg_dir}"
bundled_common_patch_dir="${CT_LIB_DIR}/packages/${pkg_name}"
local_patch_dir="${CT_LOCAL_PATCH_DIR}/${pkg_dir}" #★★ローカルパッチディレクトリ
#★ディレクトリ名を列挙
#★patch_orderは "bundled, local" になっていた、CT_PATCH_ORDERで決まるようだ
case "${patch_order}" in
bundled) patch_dirs=("${bundled_patch_dir}" "${bundled_common_patch_dir}");;
local) patch_dirs=("${local_patch_dir}");;
bundled,local) patch_dirs=("${bundled_patch_dir}" "${bundled_common_patch_dir}" "${local_patch_dir}");;
local,bundled) patch_dirs=("${local_patch_dir}" "${bundled_patch_dir}" "${bundled_common_patch_dir}");;
none) patch_dirs=;;
esac
CT_Pushd "${src_dir}/${basename}"
for d in "${patch_dirs[@]}"; do #★ディレクトリを順に辿る
CT_DoLog DEBUG "Looking for patches in '${d}'..."
if [ -n "${d}" -a -d "${d}" ]; then
for p in "${d}"/*.patch; do
if [ -f "${p}" ]; then
CT_DoExecLog ALL ${patch} --no-backup-if-mismatch -g0 -F1 -p1 -f -i "${p}"
fi
done
fi
done
...
今回のケースではpatch_orderは "bundled, local" になっていたので、packages -> ローカルパッチの順にパッチを当てるようです。patch_orderの決め方は深追いしていませんが、CT_PATCH_ORDERで決まるようです。
Zephyr SDKの設定ファイルを見るとローカルパッチディレクトリは下記のようになっています。
# configs/riscv64.config ... CT_LOCAL_PATCH_DIR="${CT_TOP_DIR}/../../patches"
CT_TOP_DIRとは何でしょう?ビルドログを追うとCT_TOP_DIR=/.../sdk-ng/build/build_riscv64でした。すなわちsdk-ng/patchesがローカルパッチディレクトリです。ディレクトリには既に6つほどのパッチが置かれています。
$ ls patches/gdb/9.2 0001-gdb-remote-make-tid-pid-type-long-in-write_ptid.patch 0002-gdb-Add-fixes-for-stdint-and-remote-debug-on-xtensa.patch 0003-gdb-xtensa-don-t-error-out-when-registers-cannot-be-.patch 0004-gdb-xtensa-use-remote-target-register-numer.patch 0005-gdb-arch-to-tell-whether-it-supports-g-packets.patch 0006-gdb-xtensa-support-disabling-use-of-g-packet.patch
ここにPython 3.9で落ちる問題を修正するパッチを追加します。
$ cd sdk-ng $ cat > patches/gdb/9.2/0007-gdb-fix-python3.9.patch (パッチをコピペする) $ ./go.sh riscv64
ビルドログはbuild/build_riscv64/build.logに作られます。
# Crosstool-NGの持っているパッチを当てているログ [DEBUG] Looking for patches in 'sdk-ng/share/crosstool-ng/packages/gdb/9.2'... [DEBUG] ==> Executing: '/usr/bin/patch' '--no-backup-if-mismatch' '-g0' '-F1' '-p1' '-f' '-i' 'sdk-ng/share/crosstool-ng/packages/gdb/9.2/0000-musl_fix.patch' [ALL ] patching file gdb/linux-nat.c [ALL ] patching file gdb/stopcode.h [DEBUG] ==> Return status 0 ... # Zephyr SDKのローカルパッチを当てているログ [DEBUG] Looking for patches in 'sdk-ng/share/crosstool-ng/packages/gdb'... [DEBUG] Looking for patches in 'sdk-ng/build/build_riscv64/../../patches/gdb/9.2'... [DEBUG] ==> Executing: '/usr/bin/patch' '--no-backup-if-mismatch' '-g0' '-F1' '-p1' '-f' '-i' 'sdk-ng/build/build_riscv64/../../patches/gdb/9.2/0001-gdb-remote-make-tid-pid-type-long-in-write_ptid.patch' [ALL ] patching file gdb/remote.c [DEBUG] ==> Return status 0 ... # 追加したパッチを当てているログ [DEBUG] ==> Executing: '/usr/bin/patch' '--no-backup-if-mismatch' '-g0' '-F1' '-p1' '-f' '-i' 'sdk-ng/build/build_riscv64/../../patches/gdb/9.2/0007-gdb-fix-python3.9.patch' [ALL ] patching file gdb/python/python.c [ALL ] Hunk #1 succeeded at 234 (offset -4 lines). [ALL ] Hunk #2 succeeded at 271 (offset -4 lines). [ALL ] Hunk #3 succeeded at 952 (offset -19 lines). [ALL ] Hunk #4 succeeded at 1552 (offset -68 lines). [ALL ] Hunk #5 succeeded at 1720 (offset -70 lines). [ALL ] patch unexpectedly ends in middle of line [DEBUG] ==> Return status 0
ちなみにZephyr SDKのビルド後、GDBのソースコードはsdk-ng/build/build_riscv64/.build/riscv64-zephyr-elf/src/gdbに展開されます。パッチが正常に当たったか確認するなら、このソースコードを見れば確実でしょう。
SDKはbuild/output以下に生成されます。RISC-V 64であればbuild/output/riscv64-zephyr-elfです。生成されたバイナリが動くか確かめましょう。
$ cd build/output/riscv64-zephyr-elf/bin $ ./riscv64-zephyr-elf-gdb --version GNU gdb (crosstool-NG 1.24.0.212_d7da3a9) 9.2 Copyright (C) 2020 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law.
動きました、良かった良かった。build/output下に生成されたriscv64-zephyr-elfディレクトリをZephyr SDKのインストールディレクトリ下にある同名のディレクトリと差し替えれば、GDBが動くはずです。
年末(2020年12月20日の日記参照)に、1月はSUPER VALUE 21 Jより安いチケットがVALUE 3で出ると予想しましたが、外れましたね。1/30の羽田→札幌便を見ると、最安はVALUE 3 Jで年末からほとんど変わっていません。
いやまあ、SUPER VALUE 21とVALUE 3の値段がほとんど変わらない時点で、かなり異常事態なんですけど……。
なんと全25便中、半分近い11便が欠航しています。飛ばす回数を自体を減らし、客単価の維持とおそらく各便ごとの黒字を確保しているのでしょう。単発で黒字が出たとしても、確実に収益にはダメージがありますよね。安いイメージが付いてしまうより良いのかなあ?
燃料費などは減便で節約できても、飛行機はリースなので飛んでも飛ばなくてもお金が掛かります。飛ばせるならいくらでも飛ばしたいでしょう。
早いところCOVID-19には収束してもらって、気軽に旅行や帰省できる日が来てほしいですね。ANAは今かなり苦しいでしょうけど、日本の翼を担う会社として何とか踏みとどまってほしいところ……。
久しぶりにPlayStation Vitaを起動したところ、ネットワークに繋げる系アプリがほぼ全滅していました。
プラットフォーム依存のゲーム機は、本体が元気でもサードパーティの撤退やサービス終了に伴って、勝手にポンコツになってしまい悲しいです。結構高かったのに……。
ソニー製
サードパーティ製
PlayStation Storeを見ると「アプリケーション」はたった14個、サードパーティ製のアプリは1つ(ROBOTICS; NOTES ELITE AR)だけです。PS Vitaは既に9年経過(2011年12月発売)しており、ぶっちゃけソニーすらもVitaを見放している節があり、もう完全にオワコンです。
それなりに大きなプラットフォームの終焉を間近で見たのは貴重な体験、とはいえ、買った人は何も嬉しくないよね……。
メモ: 技術系?の話はFacebookから転記しておくことにした。多少修正。
Zoomって、バージョンアップのお知らせを全くしてこないので、起動時に勝手にアップデートされていると思っていたんですが、全くそんなことはなかったですね。ずっと古いバージョンの5.3.1(2020/09/28リリース)のまま使っていました。
手動でアップデートしたところ、無事に最新版の5.4.9になりました。4ヶ月で大分数字が変わりましたね。
比較的新しいアプリの割にアップデートは保守的ですね?不思議な設計だな……??
Facebookのコメントで「ミーティングの後」にバージョンアップのお知らせが出ることを教えてもらいました。
ミーティング前にバージョンアップすると遅刻する可能性大なので、ミーティング終了後に表示するのは良いアイデアですね。でも残念ながら、見たことないんだよな……。うちのZoomは何か変なんだろうか??
メモ: 技術系?の話はFacebookから転記しておくことにした。加筆。