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

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

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

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

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

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


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

* [Remote Desktop 8](https://play.google.com/store/apps/details?id=com.microsoft.rdc.android) (Android)
* [Microsoft リモート デスクトップ](https://apps.apple.com/jp/app/id714464092) (iOS)





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

対処法は **オーバーライドが２つある [SetBuckBuffer](https://docs.microsoft.com/ja-jp/dotnet/api/system.windows.interop.d3dimage.setbackbuffer?view=netcore-3.1#System_Windows_Interop_D3DImage_SetBackBuffer_System_Windows_Interop_D3DResourceType_System_IntPtr_System_Boolean_) メソッドの内```enableSoftwareFallback``` が指定できる方を使い、ソフトウェア描画時であれば ```true```、ハードウェア描画時であれば ```false``` を指定します。**

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

```c#
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;
```


# （症状２）ShaderEffect が動作しない

原因はドキュメント書いてありました。
[ShaderEffect](https://docs.microsoft.com/ja-jp/dotnet/api/system.windows.media.effects.shadereffect?view=netcore-3.1)

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

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

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