はじめに
これまでの記事では、WPFのD3DImageを利用してMedia FoundationでUSBカメラから取得した映像をGPUで表示する方法を紹介してきました。
D3DImageはWPFでDirect3Dの描画結果を表示するための便利な仕組みですが、毎フレームAddDirtyRect()による更新通知が必要になるなど、少し癖のある実装になります。
一方、WinUI 3ではSwapChainPanelが用意されており、DirectXとの親和性が高く、よりシンプルな構成でGPU描画を実現できます。
今回は、前回作成したWPF版をWinUI 3へ移植する際に変更した点を中心に紹介します。
WPFからWinUI 3へ移植する際の変更点
今回の移植では、画像処理やMedia Foundation、VideoProcessorによるGPU変換処理はそのまま利用しています。
変更したのは、画面へ表示する部分だけです。
| WPF | WinUI 3 |
|---|---|
| D3DImage | SwapChainPanel |
| AddDirtyRect()で画面更新 | Present()で画面更新 |
| SurfaceをUIへ渡す | SwapChainをUIへ設定 |
表示方法が変わるだけなので、画像処理やMedia Foundation、VideoProcessorなどのNative側の処理はほぼそのまま流用できます。一方で、WPF版ではC++/CLIを介してNativeコードを呼び出していましたが、WinUI 3ではこの構成が扱いにくいため、Native側は通常のDLLとして作成し、C#からP/Invokeで呼び出す構成へ変更しています。
WinUI 3側の変更
SwapChainPanelを配置する
WPFではImageコントロールへD3DImageを設定していましたが、WinUI 3ではSwapChainPanelを配置します。
<Grid Background="Black" >
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" Orientation="Horizontal" Height="48" Background="LightGray" RequestedTheme="Light">
<ComboBox x:Name="CameraListBox" Width="300" Margin="5" IsEnabled="{x:Bind ViewModel.IsCameraStartEnabled, Mode=OneWay}" ItemsSource="{x:Bind ViewModel.CameraDevices}" SelectedItem="{x:Bind ViewModel.CameraDevice, Mode=OneWay}" SelectionChanged="CameraListBox_SelectionChanged" IsEditable="False" />
<ComboBox x:Name="FormatListBox" Width="400" Margin="5" IsEnabled="{x:Bind ViewModel.IsCameraStartEnabled, Mode=OneWay}" ItemsSource="{x:Bind ViewModel.CameraFormats}" SelectedItem="{x:Bind ViewModel.CameraFormat, Mode=OneWay}" IsEditable="False" />
<Button Content="Start Camera" Width="160" Margin="5" IsEnabled="{x:Bind ViewModel.IsCameraStartEnabled, Mode=OneWay}" Click="StartCamera_Click"/>
<Button Content="Stop Camera" Width="160" Margin="5" IsEnabled="{x:Bind ViewModel.IsCameraStopEnabled, Mode=OneWay}" Click="StopCamera_Click"/>
</StackPanel>
<SwapChainPanel x:Name="CameraSwapChainPanel" Grid.Row="1" />
</Grid>
これだけでGPU描画先となるコントロールを用意できます。
なお、WPFではDockPanelを利用してレイアウトを構成していましたが、WinUI 3にはDockPanelがありません。そのため今回はGridとRowDefinitionを利用し、DockPanelと同じようなレイアウトになるよう構成しています。
SwapChainPanelをNativeへ渡す
WinUI 3では、SwapChainPanelそのものをNativeコードから直接扱うことはできません。
そのため、C#側ではIUnknownを取得し、Native側へ渡します。
/// <summary>
/// キャプチャ開始ボタンの処理
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void StartCamera_Click(object sender, RoutedEventArgs e)
{
if (ViewModel.IsCameraCapturing)
{
return;
}
IntPtr swapChainPtr = MarshalInspectable<object>.FromManaged(CameraSwapChainPanel);
if (!cameraController.Start(swapChainPtr, FormatListBox.SelectedIndex))
{
return;
}
ViewModel.IsCameraCapturing = true;
}
Native側では、このIUnknownからISwapChainPanelNativeを取得し、生成したSwapChainを関連付けます。
これにより、以降の描画はすべてNative側で行えるようになります。
AddDirtyRect()のような更新処理は不要
WPFでは毎フレーム
Lock()
SetBackBuffer()
AddDirtyRect()
Unlock()
を実行する必要がありました。
しかし、WinUI 3ではこのような更新通知は不要です。
描画が終わったらNative側でPresent()を呼び出すだけで画面が更新されます。
そのため、C#側には描画更新処理を書く必要がありません。
ウィンドウサイズ変更を通知する
WPFのImageコントロールでは、Stretch="Uniform"などを指定することで、コントロールのサイズに合わせて描画内容が自動的に拡大・縮小されます。
一方、SwapChainPanelでは、このような自動リサイズは行われません。SwapChainは作成時の描画サイズのまま表示されるため、ウィンドウサイズが変更された場合は、Native側で描画サイズを更新する必要があります。
そのため、SwapChainPanelのサイズ変更イベントで、新しい描画サイズをNative側へ通知しています。
/// <summary>
/// SwapChainPanelのサイズが変更されたことをカメラコントローラーに通知する
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void CameraSwapChainPanel_SizeChanged(object sender, SizeChangedEventArgs e)
{
cameraController.Resize((int)e.NewSize.Width, (int)e.NewSize.Height);
}
Native側では、受け取ったサイズをもとにResizeBuffers()を実行し、SwapChainの描画サイズを更新しています。
Native側の変更
SwapChainPanelへSwapChainを設定する
WPFではSurfaceをUIへ渡していました。
WinUI 3では考え方が逆になります。
まず、UIから受け取ったIUnknownを
IUnknown
↓
ISwapChainPanelNative
へ変換します。
その後、Native側で生成したSwapChainを関連付けます。
// ISwapChainPanelNativeを取り出す
CComPtr<ISwapChainPanelNative> panelNative;
swap_chain_ptr->QueryInterface(IID_PPV_ARGS(&panelNative));
// SwapChain を SwapChainPanel に関連付ける。
panelNative->SetSwapChain(m_swap_chain);
これだけで、SwapChainへの描画内容が画面へ表示されます。
Surfaceを管理する必要がなくなるため、描画処理は非常にシンプルになります。
補足
ISwapChainPanelNativeを利用するためのヘッダー設定など、環境構築でいくつかハマりどころがあります。本記事では本題から外れるため割愛しましたが、困った点があればコメント欄でお気軽に質問してください。
描画後はPresentするだけ
映像の描画が終わったら
// バックバッファを黒でクリア
float clearColor[] = { 0.0f, 0.0f, 0.0f, 1.0f };
m_d3d11_context->ClearRenderTargetView(m_d3d_render_target_view, clearColor);
// Video Processor でスケーリングして描画する。
D3D11_VIDEO_PROCESSOR_STREAM stream = {};
stream.Enable = TRUE;
stream.pInputSurface = m_d3d_input_view;
m_d3d_video_context->VideoProcessorBlt(
m_d3d_video_processor,
m_d3d_output_view,
0,
1,
&stream
);
// SwapChainへ描画結果を反映する。
m_swap_chain->Present(1, 0);
を呼び出すだけです。
WPFで必要だった
- Lock()
- Unlock()
- AddDirtyRect()
などの処理は不要になり、描画更新はすべてSwapChainが管理してくれます。
ですが、ここで一つポイントがあります。
最初にバックバッファを黒で塗りつぶしていることです。
今回は、Native側で描画サイズを制御しているため、映像の縦横比(アスペクト比)を維持したまま描画しています。その結果、描画されない領域が発生しますが、あらかじめ黒でクリアしていることで、その部分が黒帯(レターボックス/ピラーボックス)として表示されます。
映像を画面いっぱいに引き伸ばすこともできますが、本サンプルでは映像本来のアスペクト比を維持することを優先しています。
まとめ
WinUI 3では、表示先をSwapChainPanelへ変更するだけで、画像処理の流れ自体はほとんど変更する必要がありません。
一方で、D3DImage特有の更新処理が不要になるため、プログラム全体はWPF版よりもシンプルになります。
特に、毎フレームC#側へ制御を戻してAddDirtyRect()を呼び出す必要がなくなり、描画処理をNative側だけで完結できるのは大きなメリットです。
また、CPU使用率を比較したところ、私の環境では次のような結果になりました。
| 描画方式 | CPU使用率(参考値) |
|---|---|
| WPF(D3DImage + CPUで色変換) | 約1.5% |
| WPF(D3DImage + VideoProcessor) | 約0.8% |
| WinUI 3(SwapChainPanel) | 約0.3% |
※測定環境やGPUによって結果は異なります。
もちろん、CPU使用率の差はアプリケーション全体の構成や測定環境にも左右されますが、SwapChainPanelでは描画更新をNative側だけで完結できるため、WPF版よりオーバーヘッドが少なくなったことが、この結果につながっていると考えています。
今回、WPF版をWinUI 3へ移植してみましたが、GPU描画という観点では非常に満足できる結果になりました。
SwapChainPanelはD3DImageよりもシンプルに扱うことができ、描画更新もPresent()だけで済むため、実装は分かりやすく、CPU使用率も改善しました。
一方で、開発環境については、まだ改善してほしい点もあります。
Visual Studio 2026になっても、WPFで利用できていたようなXAMLデザイナーは用意されておらず、レイアウトを確認しながら開発することができません。XAMLを編集して実行し、動作を確認するという作業が中心になります。
また、WinUI 3ではWPFで利用できたコントロールや機能が一部なくなっており、DockPanelのように代替手段を探しながら移植しなければならない場面もありました。ひとつひとつは大きな問題ではありませんが、既存のWPFアプリケーションを移植する場合は、このような細かな違いへの対応に意外と時間を取られます。
それでも、GPUとの親和性や将来性を考えると、新規にWindowsアプリケーションを開発するのであれば、WinUI 3は十分魅力のある選択肢だと感じました。
サンプルソース
今回紹介したサンプルは、GitHubで公開しています。
記事では、SwapChainPanelへの描画方法を中心に解説しましたが、実際のサンプルには、SwapChainPanelとの関連付けや、描画領域のリサイズ処理、アスペクト比を維持した表示、P/InvokeによるNativeライブラリとの連携なども含まれています。
実際に動かしながら読んでいただくと、記事だけでは伝えきれなかった実装の流れも分かりやすいと思います。