2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

個人開発のリアルタイムリモートデスクトップアプリの性能と詳細の話

2
Last updated at Posted at 2026-01-14

私が英語でまとめた記事の日本語翻訳です。前回紹介したリモートデスクトップアプリの性能と詳細が書かれています

元記事

前回記事

※絶賛、仕事探してます。ご一緒したい方は下記連絡先まで

要約(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 • を使用してください

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?