WPFアプリのリモートデスクトップ対応[グラフィックス関係編]


WPFで開発したアプリがリモートデスクトップで正しく動作しなかったことってありませんか。
「リモートデスクトップなんで、ごめんなさいねー」でサポートしないことがあったりなかったり。
一昔前の私はあったり。

しかしながらコロナ禍の今、そうもいっていられなくなりました。
そこで、出会ったリモートデスクトップ問題とその解決についてまとめます。

※今回はグラフィックス関係についてのみ扱います。

検証環境の用意

リモートデスクトップ接続中ではハードウェア描画に制限がかかります。
ローカル開発環境にハードウェア描画に制限をかけ検証環境をデバッグのしやすい環境を手元に作ります。
グラフィックスの問題の追及はこれで大方OKでしょう。

以下のドキュメントを参考にレジストリキー HKEY_CURRENT_USER\SOFTWARE\Microsoft\Avalon.Graphics\DisableHWAcceleration に値を設定します。
https://docs.microsoft.com/dotnet/framework/wpf/graphics-multimedia/graphics-rendering-registry-settings
この環境下であらかた確認対処します。

あらかた確認対処後、実際にリモートデスクトップ接続を作り試します。
パソコンを複数台用意する必要がありますが、そうもいかない方はスマホアプリの利用を検討してください。

(症状1)D3DImage が描画されない

原因はドキュメント書いてありました。
リモートデスクトップ接続中やソフトウェア描画中は表示なしとあります。
https://docs.microsoft.com/archive/blogs/wpf3d/d3dimage-and-software-rendering

対処法は オーバーライドが2つある SetBuckBuffer メソッドの内enableSoftwareFallback が指定できる方を使い、ソフトウェア描画時であれば true、ハードウェア描画時であれば false を指定します。

このソフトウェア描画時かどうか情報は毎度判断するのは高くつくので、然るべきタイミングで作ってキャッシュするのがよいでしょう。
以下に実装の断片を用意しました。

private static bool MakeIsSoftwareRenderingMode()
{
    // Rendering tier
    var renderingTier = RenderCapability.Tier >> 16;
    if (renderingTier == 0)
        return true;

    // Remote desktop
    if (GetSystemMetrics(SM_REMOTESESSION) != 0)
        return true;

    // DisableHWAcceleration
    try
    {
        var subKey = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Avalon.Graphics");
        if (subKey != null)
        {
            var d = (int) subKey.GetValue("DisableHWAcceleration");
            if (d != 0)
                return true;
        }
    }
    catch
    {
        // ignored
    }

    return false;
}

private const int SM_REMOTESESSION = 0x1000;

[DllImport("user32")]
private static extern int GetSystemMetrics(int index);

// --------------------------------------------------------
// 初期化時
SystemEvents.SessionSwitch += SystemEventsOnSessionSwitch;
isSoftwareRenderingMode = MakeIsSoftwareRenderingMode():

// 後処理時
SystemEvents.SessionSwitch -= SystemEventsOnSessionSwitch

private void SystemEventsOnSessionSwitch(object sender, SessionSwitchEventArgs e)
{
    // セッションのロックが解除されたとき、リモート接続されたとき
    if (e.Reason == SessionSwitchReason.SessionUnlock)
        isSoftwareRenderingMode = MakeIsSoftwareRenderingMode():
}

bool isSoftwareRenderingMode;

(症状2)ShaderEffect が動作しない

原因はドキュメント書いてありました。
ShaderEffect

PS 2.0 シェーダーは、ソフトウェアでレンダリングするときに実行されます。
ただし、PS 3.0 がシステムのハードウェアでサポートされている場合でも、
ソフトウェアのレンダリング中に PS 3.0 シェーダーは実行されません。

とあります。
私の場合の原因はPS3.0の利用していたから、対処法はPS2.0で再実装しました。そもそもPS2.0では命令数が足りなくてPS3.0にしたのですが、これを何とかなる範囲で何とかしました。

一般的には PS2.0内で収まるのであれば、PS2.0でシェーダーをコンパイルしなおします。