async0x4c

本文へスキップ
async0x4c
ツール一覧

PrismDesk

Android タブレットを Windows 11 のワイヤレスなサブモニタにする。HDR やペンの筆圧にも対応。

WindowsAndroidベータ版
PrismDesk — 手元のタブレットを、2 枚目のモニタに。Windows 11 の PC に、Android タブレットをワイヤレスでつなぐ。2944×1840 の動画で 5.0 Mbps、止まった画面で 8.9 kbps、同時接続 4 台 (実測)

PrismDesk は、Android タブレットを Windows PC のワイヤレスなサブモニタにするツール。製品としての紹介は公式サイトの prismdesk.app にまとめてある。製品の説明をしても面白味がないので、このページは作った側の視点で中で何をしていて、なぜそうしたのかを書く。

PrismDesk — Android タブレットを Windows のワイヤレスなサブモニタに手元の Android タブレットを、Windows 11 のワイヤレスなサブモニタに。設定作業なし、切れても自動で復帰、HDR とペン入力に対応。性能は実測値で公開しています。無料版あり・アカウント不要。PrismDesk

全体の流れ

ホストは C++ (Win32 / D3D11)、クライアントは Kotlin (Android) で書いている。1 フレームが端末に届くまでの経路は以下のようになる。

Windows ホスト → Android 端末txt
仮想ディスプレイ (IddCx ドライバ)
  → Windows.Graphics.Capture でキャプチャ (D3D11 テクスチャのまま)
  → GPU で色変換 (NV12 / P010)
  → NVENC か Media Foundation で H.264 / H.265
  → UDP で送信 (AES-256-GCM)            ───>  MediaCodec でデコード → SurfaceView
  ← SendInput / ペン入力として注入        <───  タッチ・ペン (TLS 1.3)
  ← 仮想カメラ "PrismDesk Camera"         <───  端末カメラ (H.264)

キャプチャからエンコードまで CPU を介さない。「毎フレームの経路に CPU↔GPU のコピーを増やさない」は不変条件として持っていて、GPU エンジンのカウンタとコードレビューの両方で確かめている。

本物のモニタを作る

画面の中に窓を 1 枚映すだけのアプリだと、ウィンドウをそこへ「置く」ことができない。PrismDesk は仮想ディスプレイドライバ (IddCx。上流は MttVDD で、そこからフォークした PrismDeskVdd)で Windows から見て解像度を持った増設モニタ を作り、その中身をキャプチャしている。

デバイスノードは SwDeviceCreate で作るが、このノードは 作ったプロセスが生きている間しか存在しない。なのでホストは起動のたびにノードを作り直していて、そこに管理者権限が要るので、ホストのアプリ自体が昇格を前提にしている。ドライバの導入・ファイアウォールの許可・ログオン時の自動起動は全部インストーラの仕事で、再起動したあとに利用者が触るものは何も無い。

止まった画面に 27 秒かかっていた

サブモニタは一日の大半、何も動いていない。そこで何も送らなければ帯域は限りなく 0 に近づく。実測で、静止画面は 8.9 kbps (0.24 fps) だった。

ところがこれには裏があって、静止画面から端末側のアプリを開き直すと、映像が戻るまで 27,427 ms かかっていた。エンコーダは「次に投入されたフレーム」に IDR を付ける。画面が止まっていると、その「次」がいつまでも来ない。以前は定期的に IDR を撒いていたので、偶然それが穴を埋めていた。

ここで定期 IDR を戻すと、アイドル帯域の不変条件 (10 kbps 以下) が壊れる。選んだ直し方は キーフレームを要求されてから 100 ms 新しいフレームが来なければ、直近のフレームを 1 枚だけ再投入する という手法。再配布は要求されたときだけなので、待機中の帯域は増えない。

直す前 直した後
アプリ再開 → 映像が戻るまで 27,427 ms 529〜1,260 ms (4 回)
要求 → IDR が出るまで — 23 ms
静止画面の帯域 8.9 kbps 1 kbps

HDR を最後まで HDR のまま運ぶ

HDR は「途中のどこかで SDR に落ちる」のが一番起きやすい壊れ方であり、落ちる場所はホスト側にもクライアント側にもあった。

ホスト側。 RGB→YUV の変換は GPU の VideoProcessor に任せているが、従来の D3D11_VIDEO_PROCESSOR_COLOR_SPACE には 伝達関数 (ガンマ) を指定する欄が無い。PQ も scRGB の線形も表せないので、DXGI_COLOR_SPACE_TYPE を取るVideoProcessorSetStreamColorSpace1 / VideoProcessorSetOutputColorSpace1 を使っている。

キャプチャした形式 入力の色空間 出力の色空間
scRGB FP16 RGB_FULL_G10_NONE_P709 YCBCR_STUDIO_G2084_LEFT_P2020
RGB10A2 (すでに PQ) RGB_FULL_G2084_NONE_P2020 YCBCR_STUDIO_G2084_LEFT_P2020
SDR BGRA8 RGB_FULL_G22_NONE_P709 YCBCR_STUDIO_G22_LEFT_P709

「HDR かどうか」を bool 1 つで持っていると、PQ と scRGB を区別できない (どちらも true になる)。キャプチャしたフレームには色空間そのものを持たせている。

クライアント側。 Window.setColorMode(COLOR_MODE_HDR) は Compose の UI 層にしか効かず、映像には効かない。映像のレイヤに要るのは SurfaceView.setDesiredHdrHeadroom() (API 35 以降) であることが分かった。効いているかどうかは目で見ず、統計の HDR/SDR 比で確かめる。Pixel 9 で 7.999703。1.0 のままなら、どこかで SDR に落ちている。

fps はうたわない

キャプチャは Windows が画面を合成したときにだけフレームを受け取る。なので出せるフレームレートは主画面のリフレッシュレートの約数になる可能性がある。75 Hz の主画面なら 75 / 37.5 / 25 で、60 はこの並びに無い。

「60 fps」と書くことはできるが、それは開発機のモニタの話で、使う人のモニタの話ではない。公式サイトでもフレームレートの数字は出していない。

暗号化: UDP に DTLS を使わない

経路 中身 保護
制御 (TCP) 能力交渉・セッション制御 TLS 1.3
入力 (TCP) タッチ・ペン・キーボード・マウス TLS 1.3
映像・音声・カメラ (UDP) 画面そのもの AES-256-GCM
探索 (UDP) ホスト名・ポート・証明書の指紋 平文

映像の経路は 落ちたら落ちたままでよい という前提で組んである。DTLS はデータグラムごとにレコード層と再送・順序制御を持ち込むので、この前提とぶつかる。要るのは秘匿と改竄検知だけなので、鍵は TLS の制御チャネルで配り、データグラムには AEAD を 1 枚だけ被せている (SRTP と同じ考え方)。タブレット側は PC の証明書の指紋をピン留めする。後から測り直した範囲では、暗号化による帯域の増加は 2% ほどだった。

数字は製品の外から測る

サブモニタのアプリは、デモでは良く見えても毎日使うと粗が出る。だから性能の数字は形容詞にせず、アプリ自身が報告する値を使わずに 測っている。NIC のバイト数、GPU エンジンの使用率、タブレット側は SurfaceFlinger のフレーム時刻を使っている。

項目 実測
2944×1840 で動画を映したときの帯域 5.0 Mbps (同じ試験・同じ映像で spacedesk は 24.7 Mbps)
Wi-Fi を 2・5・10 秒切って戻した 12/12 回、同じ仮想ディスプレイに自動で復帰
戻ってこない端末を片付けるまで 20.5 秒
同時接続 (キャプチャとエンコーダは共有) 4 台で確認
測定条件txt
ホスト    Core i7-11700 / RTX 3060 + UHD 750 / Windows 11 (build 26200) / 有線ギガビット
タブレット Lenovo Idea Tab Pro (Android 16) / 1840×2944 / Wi-Fi 6・RSSI −46 dBm / 同じ L2

実機での受け入れ確認も、人の手から外しつつある。開発機ではホストを UAC 無しで昇格起動し、同じ利用者に閉じた制御パイプから GUI と同じコマンドを流して、端末側は debug ビルドだけにある口を adb から叩く。画面を目で読まずに 1 本の確認を通せるようにした(この機能はインストーラには入れていない)。

prismdesk.app も自分で作っている

公式サイトの prismdesk.app も、製品と同じリポジトリから出している。フレームワークは使っておらず、markdown 以外に依存の無い Python のスクリプト 1 本で静的な HTML を組み、Cloudflare Pages に置いている。日本語と英語の両方がある。

作るときに決めたことがいくつかある。

  • 法務の文面はサイトに 1 文字も書かない。 利用規約やプライバシーポリシーは別のディレクトリにある文書を読んで HTML にするだけにして、情報源を 1 箇所にした
  • 法務の文書が、存在しないページへのリンクを含んでいたらビルドを止める。 法務文書はアプリにも同梱する配布物で、出荷した後は URL を直せないため
  • 送信先が埋まっていない登録フォームでは本番ビルドが通らない。 action のないフォームは、押した人の登録を黙って失う。気づく手段が無い種類の失敗なので、ビルドで止めている
  • Pages は git 連携ではなく直接アップロードで出している。 git 連携は Cloudflare にリポジトリ全体の読み取り権を渡す。製品のソースや署名の手順が入ったリポジトリを、サイトのためだけに渡す理由は無い

トップには、アプリの設定画面にある「選んだ設定で端末にどう映るか」のプレビューを、ブラウザで動くようにしたデモを置いた。向きや解像度を変えて、PC 側のウィンドウを端末側へドラッグできる。「まだできないこと」の節 も、購入の判断に要る情報としてあえてページの中ほどに置いている。

公開リポジトリ

製品のソースは非公開だが、ドキュメント・Issue・Discussions・リリースは GitHub で公開している。不具合の報告や要望はこちらで受け付ける。

GitHub - Asynchronous-0x4C/prismdesk-app: Documentation, issue tracker and releases for PrismDesk - a wireless second monitor for Windows, using an Android tablet.Documentation, issue tracker and releases for PrismDesk - a wireless second monitor for Windows, using an Android tablet. - Asynchronous-0x4C/prismdesk-appGitHub

無料版と Pro

無料版 Pro (US$22・買い切り)
同時に接続できる端末 1 台 最大 4 台
表示 SDR HDR
ペン・タッチ入力 ○ ○
音声 ○ ○
端末のカメラを Web カメラにする — ○

Pro は 1 つのキーで自分の Windows PC 3 台まで使える。サブスクリプションもアカウントも無い。アカウント・テレメトリ・自動の更新確認も無く、ネットワークの外へ出るのは「有効化」と「更新を確認」を押したときだけになっている。

動作環境

  • Windows 11 64 ビット (ビルド 22000 以降)、管理者権限。Windows 10 は試していないので対応とは書かない
  • H.264 のハードウェアエンコードができる GPU (NVIDIA NVENC、または Intel / AMD の Media Foundation エンコーダ)
  • Android 8.0 (API 26) 以降で、H.264 のハードウェアデコーダを積んだ端末
  • PC と端末が同じ LAN にあること (有線でも Wi-Fi でもよい)