私が英語でまとめた記事の日本語翻訳です。前回紹介したリモートデスクトップアプリの性能と詳細が書かれています
※絶賛、仕事探してます。ご一緒したい方は下記連絡先まで
要約(TL;DR)
結論:
有線LANで、サーバー/クライアントを別PCに置いた構成において、all-intra(毎フレームIDR)を無効化し、低遅延IP(B=0)を使用すると、エンドツーエンドで p50 53 ms、p95 71 ms、p99 93 ms を達成できました。
大半のフレームは 50–80 ms 帯に収まり、超過率も低いです(>80 ms 1.69%、>120 ms 0.67%)。まれに ~1 s のバースト(最大 ~1.08 s)が観測されましたが、体感では知覚できませんでした。
手段:
D3D11 Capture → CUDA Resize → NVENC → UDP/FEC → NVDEC → D3D12 Render
根拠:
クロック整合(clock-aligned)したフレーム単位CSVに基づき、パーセンタイル/超過率/1秒ワーストを算出
デモ:
なぜ重要か
-
直感的に「操作できている」感覚が保てます。p50 53 ms / p95 71 ms / p99 93 ms、かつ大半が 50–80 ms に収まるため、一般的なデスクトップ操作(タイピング、カーソル移動、ウィンドウドラッグ)がサブ100 ms の範囲に収まりやすく、有線LANでは即応性が維持されます。
-
ピークより「予測可能性」を重視できます。超過率は低く(>80 ms 1.69%、>120 ms 0.67%)、まれな ~1 s バーストも体感では知覚されませんでした。日常運用では、フローを壊すマイクロスタッターが減り、混在負荷下でも安定しやすくなります。
-
効くトレードオフです。all-intra(毎フレームIDR)を捨てて低遅延IP(B=0)+小さなVBVにすることで、エンコーダ/ネットワークのバースト性やキュー肥大を抑制できます。典型レイテンシや画質を大きく犠牲にせずに、テール(p95~p99)を締められます。
-
将来のネットワークに対する余裕ができます。LANで典型パスが 50–80 ms に安定していると、その後に Wi-Fi / WAN のジッタが乗っても予算が残り、操作感を自然に保ちやすくなります。VDI、リモート制作、軽量ゲーム操作の現実的なベースラインになります。
-
信頼できる証拠です。結果はクロック整合したフレーム単位CSV(WGC→RenderEnd)に基づき、パーセンタイル、超過率、最長連続超過、1秒ワーストケースを算出しています。平均値よりも知覚に近い指標で評価しています。
テスト環境
-
サーバー
OS: Windows 11 Pro 24H2
CPU: Ryzen 5 4500
GPU: NVIDIA RTX 4070 12GB (Driver 32.0.15.8129)
Memory: 32GB
NIC: Realtek RTL8125 2.5GbE Controller (Driver 1125.21.903.2024) -
クライアント
OS: Windows 11 Home 24H2
CPU: Intel Core i5-11400H
GPU: NVIDIA GTX 1650 4GB (Driver 32.015.8129)
Intel UHD Graphics (Driver 30.0.101.1340)
Memory: 16GB
NIC: Realtek PCIe GbE Family Controller (Driver 10.72.524.2024)
「E2Eレイテンシ」とは何か
定義(フレーム単位)
E2Eレイテンシは、サーバー側でキャプチャが完了してエンコード可能になった時点から、クライアント側でレンダリング完了直後にpresentされるまでを測定します。
E2E_i = client_present_ts[i] - (server_capture_ts[i] + clock_offset_ms_at_i)
-
server_capture_ts[i]: サーバーでキャプチャしたフレームがエンコード可能になった瞬間(例: WGC/D3D11でframe acquired後、フレームがreadyになった時点)。
-
client_present_ts[i]: クライアントでswap/presentが戻った直後の瞬間(レンダリング完了)。
含むもの/含まないもの
-
**含む:**capture → convert → encode → transport → decode → render → present。
-
**含まない:**入力デバイス遅延(キーボード/マウス)、パネルのスキャンアウト、表示の画素応答。
ホスト間のクロック整合(簡易NTP方式)
サーバーとクライアントは別PCで動作するため、クロックオフセットを推定してタイムスタンプを補正します。
Client sends ping at t0 (client clock)
Server receives at t1 (server clock), replies at t2 (server clock)
Client receives reply at t3 (client clock)
RTT = (t3 - t0) - (t2 - t1) offset (server→client) ≈ ((t1 - t0) + (t2 - t3)) / 2
-
これを周期的(例: 0.5–2 sごと)にサンプルし、RTT最小のサンプル群を保持しつつ、median/low-passで clock_offset_ms_at_i を推定します。
-
有線LANでは残差誤差は通常小さく(数ms程度)、本稿のスケールでは無視できるレベルです。
データの整備(Data hygiene)
-
ウォームアップフレーム(例: 最初のNフレーム)と、両方のタイムスタンプが揃っていないフレームを除外します。
-
可能な限り ≥20k フレームを確保し、テール統計を安定させます。
-
レポート項目: p50 / p95 / p99、超過率(例: >80 ms、>120 ms)、しきい値超過の最長連続、最悪の1秒ウィンドウ(件数と平均)。
この定義を採用する理由
ユーザーが体感するものに一致するからです。キャプチャされたデスクトップの変化が、実際にリモート画面に出るまでの時間を、ホスト間クロックを整合したうえで正確に測れます。
測定手法(Measurement Methodology)
ここではE2Eの再定義はしません(「E2Eの意味」を参照)。ログ済みのクロック補正済みフレーム単位E2Eレイテンシ(例: WGCからRenderEndまで: ms、frame_idで対応付け済み)に対して、素直な統計処理を行います。
-
前処理: ウォームアップフレーム、frame_idの欠損/不一致、負値や無効値を除外。
-
統計: p50 / p95 / p99 / p99.9、min / max / mean、超過率(例: >80 ms、>120 ms)、最長連続超過、最悪の1秒ウィンドウ(任意の1秒区間における件数、平均、最大)。
-
サンプリング: 1回の測定につき少なくとも ~20k フレーム。結果とともに正確なサンプル数も併記。
-
レポート: 指標の簡潔なテーブル(必要に応じて付録にヒストグラムや超過プロット)。
要するに、クロック整合済みのE2Eレイテンシログに対して記述統計量を計算しているだけです。
パイプライン構成(Pipeline Architecture)
概要
2つの動作モードは同じトランスポートとレンダーパスを共有しつつ、ワークロード特性に応じてキャプチャソースを切り替えます。
-
Normal mode: 生産性用途、マルチウィンドウ、DWM混在に最適化したデスクトップ/ウィンドウキャプチャ。
-
Game mode: フルスクリーン/flip-model/高リフレッシュのように、ゲーム側のswapchainペーシングが支配的になるケース向け。
-
Fallback path: CUDA interopが利用できない/不安定な環境向けのCUDAなし変換/エンコード経路。
Normal mode
Capture — D3D11 + Windows Graphics Capture (WGC)
-
WGCはファーストパーティで、コンポジタを意識した低オーバーヘッドなキャプチャAPIです。インジェクション/フック不要で分離性が高い点を評価しました。
-
DWM下でもフレーム供給が安定し、HiDPI/スケーリング/オクルージョンに強く、イベント駆動のframe poolが低遅延ループに適合します。
Convert — CUDA interop (~4 ms)
-
ゼロコピー interop: BGRAフレームをCUDAにマップし、単一のGPUパスでYUV 4:4:4へ変換します。
-
カーネルはメモリアクセスのcoalescingを意識してチューニングしており、典型的なRTXクラスGPUでは1080pフレームあたりエンドツーエンドで ~4 ms を実現します。
Encode — NVENC (HEVC 4:4:4, Low-Latency, B-frames 0)
-
入力: YUV 4:4:4。
-
設定: Intra QP 25、PQP 25、LowLatencyプロファイル、B-frames = 0。並べ替え遅延を避け、デコーダ出力レイテンシを決定的にします。
Game mode
Capture — D3D11 + Desktop Duplication (DXGI Output Duplication)
-
flip-model / exclusive-full-screen / VRR / 高Hzのpresentパターンで、スキャンアウトに同期したduplicationの方が予測可能性が高い場合に選択します。
-
dirty-rectsとリフレッシュ同期のカデンスにより、タイトルがGPUを強く駆動する状況や、オーバーレイ/コンポジタのヒューリスティクスが干渉する状況で安定性が上がります。
Convert — CUDA interop (~4 ms)
- Normal modeと同じCUDA経路: 共有リソースを用いてBGRA → YUV 4:4:4を ~4 ms で実行します。
Encode — NVENC (HEVC 4:4:4, Low-Latency, B-frames 0)
- 同じ低遅延設定を採用し、持続的なGPU負荷下でもエンコードジッタを抑えます。
フォールバック(CUDAなし)経路
CUDA interopが利用できない/不安定な場合に自動で使用します。
Normal mode(fallback)
-
Capture: D3D11 + WGC
-
Convert: ComputeShader経路 (~4 ms)、BGRA → B8G8R8A8(device-local)。
-
Encode: NVENCがB8G8R8A8を入力してHEVCを生成(Intra QP 25 / PQP 25 / LowLatency / B=0)。ベンダー側の変換はエンコード経路内で実行されます。
Game mode(fallback)
-
Capture: D3D11 + Desktop Duplication
-
Convert: ComputeShader (~4 ms)、BGRA → B8G8R8A8
-
Encode: NVENC → HEVC(Intra QP 25 / PQP 25 / LowLatency / B=0)
横断的制御(Cross-cutting controls)
Dynamic QP nudging
-
実行時ロジックで、キュー深度、超過率(>80/120 ms)、短期的なビットレート余裕に応じて、QPをベースライン(Intra/PQP 25)の周辺で小さく調整します。
-
目的: p50–p95帯を崩さずに、テールレイテンシを削ること。
Network — UDP + Adaptive FEC (Reed–Solomon)
-
トランスポートはUDPで、選択的再送は無効(遅延最優先)。
-
適応RSパリティで、直近のloss/RTT/reorder統計に基づき保護率を調整します。小さなジッタバッファで再生遅延を抑えつつ、負荷時は「最新フレーム優先(latest-frame wins)」を取ります。
クライアント側
Decode & Present
-
NVDECでHEVC 4:4:4をデコードし、interopでGPUメモリに展開します。
-
D3D12 Present(waitable swapchain)でデコード直後に合成/presentし、present直後のタイムスタンプをE2E算出に使用します。
タイミングと予算のメモ
-
両モードとも、captureからpresentまでGPU常駐・ゼロコピーの経路を狙います。
-
変換ステージはフレームあたり ~4 ms 近傍に固定し、エンコードはB=0でreorderキューを避け、VBV蓄積を最小化します。
-
高負荷時は、game modeはカデンスの予測可能性を優先し、normal modeはコンポジタ親和性とウィンドウ動作の健全性を優先します。
落とし穴と対処
CUDA × DirectX interop
症状
-
例が少なく一貫性がない。多くのスニペットは高負荷でクラッシュまたはスタールする。
-
ゼロコピーを安定させるまで数か月かかった。ストレス時に時々ティアリングや黒フレームが出る。
原因
-
GPU–GPU共有には厳密な所有権と同期が必要。フェンス/バリアのスコープが誤っていたり、D3D11/12のセマンティクスを混在させると破綻する。
-
隠れたCPUラウンドトリップ(staging copy、暗黙のMap/Unmap)がパスに紛れ込む。
対処
-
フレームはエンドツーエンドでGPU常駐にし、CPU readbackをしない。
-
各ホップ(WGC/DX → CUDA → NVENC)で明示的なフェンス/バリアを置き、デバッグレイヤを有効にして状態遷移を検証する。
-
interop経路(例: D3D11 <-> CUDA あるいは D3D12 <-> CUDA)を1つに統一し、全遷移を監査する。
NVENCの「罠」: graphics mode と CUDA mode
症状
-
D3D12 →(NVENCのためにD3D11が必要)→ フォーマット変換の体操になり、パイプラインが複雑化してレイテンシスパイクが出る。
-
コンシューマRTXではNV12のUAVが使えず、computeでNV12を書こうとして行き詰まる。
-
CUDAから単一のlinear NV12バッファを渡すと、NVENCが入力を拒否/破損させる。
原因
-
NVENCのD3Dモードが期待するリソース型/フラグが、compute側の設計と一致しない。
-
NV12のTyped UAVはコンシューマGPUで未対応。NV12への直接UAV書き込みは現実的ではない。
-
CUDAモードのNVENCは、アドホックな単一レイアウトではなく、planeごとのポインタと正しいpitchを期待する。
対処
-
NVENCをCUDA modeに切り替え、D3D12↔D3D11のインピーダンスを排除する。
-
CUDAでYとUV planeを別々に生成し、planeごとに正確なpitch/strideを設定してNVENCに渡す。
-
B-frames = 0、low-latencyプロファイル、小さなVBVを維持し、reorderキューとバッファ肥大を避ける。
フレームパイプラインの同期と安定性
症状
-
平均は低いのに、ときどきジッタやマイクロスタールが出る。負荷が変わるとバーストが起きる。
-
「だいたい滑らか」だが、まれな塊がp99を押し上げる。
原因
-
パイプライン(capture → convert → encode → send)の深さが過剰/不足で、キューが膨張(dilation)または枯渇(starvation)する。
-
ホットパスにブロッキング呼び出し(同期ログ、都度アロケーション、暗黙flush)が混入し、クリティカルセクションが広すぎる。
対処
-
Zero-CPU-wait設計: ブロッキング作業はフレームスレッドから外す。非同期ログ。プールアロケータ。
-
キュー深度はstarvationを避ける最小値に調整。圧力下では最古フレームを捨てる(「latest-wins」)。
-
ステージごとの enqueue/start/finish とキュー長を追跡し、最も遅いステージから調整する。
NV12経路後の文字可読性低下
症状
- NV12を実装すると、小さな文字やUIエッジが読みにくくなり、ぼやけ/ブロッキングが報告される。
原因
-
NV12は 4:2:0 のクロマサブサンプリングで、デスクトップはクロマに敏感(細い色エッジ、サブピクセルAA)。
-
クロマ損失とスケーリング/presentが合わさるとアーティファクトが増幅する。
対処
-
多少のレイテンシ/ビットレートコストを払ってでもYUV 4:4:4へ戻し、デスクトップ用途の可読性を優先する。
-
低遅延設定(HEVC 4:4:4、Intra QP 25 / PQP 25、B=0)を維持してテールレイテンシを制御する。
制約と今後
制約
本ビルドと評価は有線LANのみを対象としています。映像パイプラインはHEVCのみ(4:4:4)で、クライアント側はNVDEC/D3D12に依存するため、現状クライアントにはNVIDIA GPUが必要です。
今後
AV1対応を追加し、クライアントをIntel iGPU(Quick Sync)で動かすことを最初の非NVIDIAターゲットとして進めます。また、可能な限りリレーを迂回してE2Eレイテンシをさらに下げるために、P2Pトランスポートも実装します。
リンクと連絡先
コードとプロフィール
GitHub: https://github.com/YukiOhira0416
Components:
remote_server_capture — D3D11/WGC or Duplication → CUDA → NVENC
remote_client — NVDEC(HEVC 4:4:4) → D3D12 present, frame telemetry
remote_server_tasktray — daemon/control
連絡先
Email: xylish.hyper.cool [at] icloud [dot] com
件名: Hiring • Remote Desktop E2E • を使用してください