WPFの画面にOpenGLの描画結果を出したい、という要件でまず名前が挙がるのが D3DImage です。ところがサンプルを写しても真っ黒のまま、あるいはリモートデスクトップ経由で何も映らない、という詰まり方をします。原因のほとんどは実装の細部ではありません。D3DImageが受け取れるサーフェスの型がただ1種類しかない、という前提を踏み外していることにあります。この記事では、その制約を起点に、OpenGLの描画をD3DImageへ渡すために何が必須になるのかを一次情報から順に確定させます。
まとめ
D3DImageに渡せるのは IDirect3DSurface9、つまりDirect3D 9のサーフェスだけです。D3DResourceType 列挙体のメンバーは IDirect3DSurface9 = 0 の1つしかなく、OpenGLのテクスチャもDirect3D 11のテクスチャも直接は渡せません。
そこで間にDirect3D 9のサーフェスを置き、OpenGL側から WGL_NV_DX_interop でそのサーフェスを共有します。このとき現在のWindows(WDDM環境)では、共有するDirect3D 9デバイスが Direct3D9Ex(IDirect3DDevice9Ex)でなければなりません。素の IDirect3DDevice9 は拡張の仕様上そもそも登録できず、加えてWPF側もハードウェア更新の条件として9Exを要求します。動くかどうかの問題と速いかどうかの問題が、どちらも同じ答えに収束する。ここに選択の余地はありません。
自力で組む理由がなければ、OpenTK.GLWpfControl 4.3.6を入れるのが最短経路です。このコントロールの実装は、まさに上記の9Exデバイス生成とNV_DX_interop登録、そして SetBackBuffer の呼び出しをそのまま行っています。以降で、その中で何が起きているのかと、DirectX 11を描画元にしたい場合の現実的な選択肢を見ていきます。
D3DImageが受け取れるサーフェスの型
D3DResourceTypeが持つ唯一の値
Microsoft Learnの D3DResourceType 列挙体のリファレンスに載っているメンバーは、次の1行だけです。
| 名前 | 値 | 説明 |
|---|---|---|
| IDirect3DSurface9 | 0 | IDirect3DSurface9 を指定 |
D3DImageとこの列挙体は .NET Framework 3.5 SP1(2008年8月)で追加されたAPIで、以降、現行のWindowsデスクトップ11.0まで内容が変わっていません。WPFが公開した相互運用の窓口は、2008年に用意されたDirect3D 9世代のまま増えていない、ということです。つまり「D3DImageがDirectX 11に対応する日」を待つ設計判断は成立しません。DirectX 11以降を描画元にしたい場合も、最終的にDirect3D 9のサーフェスへ落とす経路が要ります。
Lock・SetBackBuffer・AddDirtyRect・Unlockの更新手順
D3DImageはバックバッファとフロントバッファの2枚を管理します。バックバッファが開発者側のDirect3Dサーフェスで、Unlock を呼んだ時点でフロントバッファへ転送され、画面に出る仕組みです。XAML側では System.Windows.Interop 名前空間が既定のXAML名前空間に含まれないため、明示的にマッピングします。XAMLの名前空間宣言の書き方に不安がある場合は先に押さえておくと迷いません。
<Window x:Class="D3DHost.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:i="clr-namespace:System.Windows.Interop;assembly=PresentationCore">
<Grid>
<Image>
<Image.Source>
<i:D3DImage x:Name="d3dimg" />
</Image.Source>
</Image>
</Grid>
</Window>
コードビハインド側の更新は、ロックしてからサーフェスを割り当て、描画し、変更矩形を申告して解放する、という固定の順序です。第3引数の true はソフトウェア描画へのフォールバックを有効にする指定で、これを省くとリモートデスクトップ環境で何も表示されません(理由は後述します)。
using System;
using System.Windows; // Int32Rect
using System.Windows.Interop; // D3DImage, D3DResourceType
using System.Runtime.InteropServices; // DllImport, Marshal
// ネイティブ側が保持するIDirect3DSurface9を受け取る(戻り値はHRESULT)
[DllImport("D3DCode.dll")]
static extern int GetBackBufferNoRef(out IntPtr pSurface);
IntPtr pSurface = IntPtr.Zero;
Marshal.ThrowExceptionForHR(GetBackBufferNoRef(out pSurface));
d3dimg.Lock();
d3dimg.SetBackBuffer(D3DResourceType.IDirect3DSurface9, pSurface, true);
// ここでネイティブ側のDirect3D描画を実行する
d3dimg.AddDirtyRect(new Int32Rect(0, 0, d3dimg.PixelWidth, d3dimg.PixelHeight));
d3dimg.Unlock();
同じ IntPtr で SetBackBuffer を毎フレーム呼び直しても構いません。Microsoftのコード例にも「同じ IntPtr での再呼び出しは no-op で、性能上の代償はない」とコメントが添えられています。むしろ注意すべきは AddDirtyRect のほうです。これを呼び忘れると、描画が走っていても画面が更新されません。真っ黒のまま動かないときの典型的な原因がここです。
OpenGLの描画結果をD3DImageへ渡す経路
WGL_NV_DX_interopが共有できる資源と関数
OpenGLとDirectXの橋渡しを担うのが WGL_NV_DX_interop 拡張です。仕様書の Status 行は「Complete. Shipping with NVIDIA release 265 drivers, November 2010」で、名前にNVを冠していますが、GLWpfControlはIntel・AMD・NVIDIAで動作するとしています。この拡張はDirectXの頂点バッファをOpenGLのバッファオブジェクトとして、DirectXのサーフェスをOpenGLのテクスチャまたはレンダーバッファとして直接参照させます。定義される関数は8本です。
wglDXOpenDeviceNV/wglDXCloseDeviceNV— DirectXデバイスを相互運用に登録・解除wglDXRegisterObjectNV/wglDXUnregisterObjectNV— 個別の資源をGL側の名前へ結び付けるwglDXLockObjectsNV/wglDXUnlockObjectsNV— GLが触る区間の排他制御wglDXObjectAccessNV— アクセス種別の変更wglDXSetResourceShareHandleNV— 共有ハンドルの関連付け
登録時に指定するアクセス種別は WGL_ACCESS_READ_ONLY_NV(0x0000)、WGL_ACCESS_READ_WRITE_NV(0x0001)、WGL_ACCESS_WRITE_DISCARD_NV(0x0002)の3つです。最後のひとつは既存内容の破棄を許す指定ですが、後述するGLWpfControlの実装は AccessReadWrite で登録しており、D3DImage経由の描画で書き込み専用に落とす最適化は行っていません。
WDDM環境でDirect3D9Exが必須になる条件
ここが最大の落とし穴です。仕様書の wgl.devicetypes 表は、対応デバイスと制約を次のように定めています(下2行は後述するinterop2が追加した分です)。
| デバイス型 | 制約 |
|---|---|
| IDirect3DDevice9 | WDDM不可/MULTITHREADED必須 |
| IDirect3DDevice9Ex | MULTITHREADED必須 |
| ID3D10Device | WDDMのみ/マルチスレッド必須 |
| ID3D11Device | WDDMのみ/マルチスレッド必須 |
仕様書は本文でも「WDDM環境でDirectX 9の資源を共有したい場合、その資源を所有するDirect3Dデバイスは Direct3D9Ex デバイスであることが必須」と述べています。WDDM(Windowsのディスプレイドライバーモデル)はWindows Vista以降のすべてのWindowsで使われるため、これは事実上「今のWindowsでは常に9Exが要る」という意味になります。素の Direct3DCreate9 で作ったデバイスのサーフェスをGLへ登録しようとして失敗するなら、それはコードの誤りではなく仕様どおりの拒否です。
WPF側からも同じ結論が出ます。Microsoftの相互運用パフォーマンス指針は、WDDM環境では IDirect3DDevice9Ex 上にサーフェスを作れとし、それ以外の設定では「サーフェスがソフトウェア経由でコピーされ、性能が著しく低下する」と明記しています。なお、D3DImage単体であれば素の IDirect3DDevice9 でも表示自体はできます。9Exが必須になるのは、OpenGLとサーフェスを共有する経路と、ハードウェア転送を維持したい場合の2条件です。
GLWpfControlでの実装手順
OpenTK.GLWpfControlは、この一連の処理を実装済みのWPFコントロールです。NuGetの最新は4.3.6(2026年2月28日公開)で、パッケージ説明のとおりOpenTK 4.9.4以降を前提とします。バージョン3.0.0以降はNV_DX_interopによる相互運用に切り替わっており、後述するエアスペースの制約を受けません。Visual Studio 2026でもVS2022でも、必要な作業はNuGetから OpenTK.GLWpfControl を追加することだけです。D3DImage自体はPresentationCore.dllに含まれるため、WPFプロジェクトを作った時点で追加インストールなしに使えます。
XAMLに名前空間を足してコントロールを置きます。
<Window x:Class="GLHost.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:glWpfControl="clr-namespace:OpenTK.Wpf;assembly=GLWpfControl">
<Grid>
<glWpfControl:GLWpfControl
x:Name="OpenTkControl"
Render="OpenTkControl_OnRender" />
</Grid>
</Window>
コードビハインドでは InitializeComponent の後に Start を呼び、描画は Render ハンドラーに書きます。設定を省いて Start() と呼ぶこともできます。その場合はXAMLの Settings プロパティが使われ、そこも書いていなければ MajorVersion = 3、MinorVersion = 3、Samples = 0 になります。使いたいGLバージョンがあるなら明示してください。
using System; // TimeSpan
using OpenTK.Wpf;
using OpenTK.Graphics.OpenGL4;
using OpenTK.Mathematics;
// MainWindow クラスの内部(クラス定義は省略)
public MainWindow() {
InitializeComponent();
var settings = new GLWpfControlSettings {
MajorVersion = 4,
MinorVersion = 6,
Samples = 4, // MSAAのサンプル数。範囲は0〜16で、0と1はMSAA無し
RenderContinuously = false // 既定はtrue。falseならInvalidateVisual時だけ再描画
};
OpenTkControl.Start(settings);
}
private void OpenTkControl_OnRender(TimeSpan delta) {
GL.ClearColor(Color4.Blue);
GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
}
RenderContinuously を false にする指定は、CADのビューアのように操作したときだけ描き直せば足りる画面で効きます。既定の true のままだと静止していても描画イベントが回り続け、ノートPCではバッテリーを削ります。Samples を2以上にしたときは、GL側のマルチサンプルFBOで解決してからDirect3Dのサーフェスへ転送する実装になる点も押さえておくと、負荷の増え方を見積もれます。
このコントロールの内部を読むと、前節の必須条件がそのまま実装されていることを確認できます。DXGLContext.cs は Direct3DCreate9Ex でコンテキストを作り、CreateDeviceEx に HardwareVertexProcessing・Multithreaded・PureDevice・FpuPreserve を渡してデバイスを生成。GLWpfControlRenderer.cs は Wgl.DXRegisterObjectNV で描画先を登録し、毎フレーム Lock → SetBackBuffer(D3DResourceType.IDirect3DSurface9, ハンドル, true) → AddDirtyRect → Unlock を呼びます。第3引数が true で固定されている点は、後の章の挙動に効いてきます。
DirectX 11を描画元にする場合の要件
interop2が対応する資源型
WGL_NV_DX_interop2 は、元の拡張をDirectX 10・10.1・11の資源へ広げたものです。Status は「Complete. Shipping with NVIDIA release 275 drivers, June 2011」、Revision 2 の最終更新は2011年10月4日で、新しい関数もトークンも追加されません。wglDXRegisterObjectNV に渡せる資源型として ID3D10Texture1D/ID3D11Texture1D、ID3D10Texture2D/ID3D11Texture2D、ID3D10Texture3D/ID3D11Texture3D、バッファ資源として ID3D10Buffer/ID3D11Buffer が加わり、wglDXOpenDeviceNV に渡せるデバイス型として ID3D10Device・ID3D11Device が加わる、という拡張のしかたです。DirectX 10・11の資源については wglDXSetResourceShareHandleNV の呼び出しが不要になる、という緩和も入っています。
ただし、これはあくまでOpenGLとDirectXの間の話にすぎません。前述のとおりWPFへ出す口はD3DImageのままで、Direct3D 9のサーフェスへ落とす段は残ります。なおinterop2の仕様書にDirectX 12の記載はありません。DirectX 12を描画元にする場合の定番は、D3D11On12CreateDevice(d3d11on12.h)でDirect3D 12デバイスの上にDirect3D 11デバイスを作り、ID3D11On12Device::CreateWrappedResource でDirect3D 12の資源をDirect3D 11の資源として包んでから、共有経由でDirect3D 9Exのサーフェスへ渡す経路です。
D3D11Imageパッケージの公開状況
そのコピー段を肩代わりする目的で公開されたのが、Microsoftの Microsoft.Wpf.Interop.DirectX-x64(D3D11Image)です。NuGetの説明文は「DirectX 11 または DirectX 10 をサポートするD3DImageの一種」で、要件だけを見れば理想的に映ります。パッケージIDにアーキテクチャの接尾辞が付いている点に注意してください。接尾辞なしのIDはNuGetに存在しません。しかし採用を検討するなら、まず公開状況を確認すべきです。
| 項目 | 実測値 |
|---|---|
| 最新バージョン | 0.9.0-beta-22856 |
| NuGet公開日 | 2015-11-04 |
| 正式版 | なし(x86・x64で別パッケージ) |
| リポジトリ | microsoft/WPFDXInterop |
| GitHubリリース | 0件 |
| 既定ブランチ最終コミット | 2022-10-07 |
2015年11月のベータから、正式版が出ないまま10年以上が経過しています。新規案件でこのパッケージに依存するのは避けるべきです。DirectX 11の描画をWPFへ出す必要が本当にあるなら、共有テクスチャを自前でDirect3D 9Exのサーフェスへ解決する実装を持つか、次章の判断に従ってホスティング方式そのものを見直すほうが、長期の保守コストは低く収まります。
HwndHostとD3DImageの使い分け基準
もう一つの選択肢が HwndHost による子HWNDの埋め込みです。描画性能だけを見ればD3DImageより有利になり得ますが、エアスペースという構造上の制約が付いてきます。Microsoftの説明では、トップレベルウィンドウ内の各ピクセルはちょうど1つのHWNDに属し、そのピクセルの上に描画するものはすべて同じ描画技術に属していなければなりません。
結果として、WPFの要素をHwndHostの領域の上に重ねて描くことはできません。半透明の要素をまたがらせる構成にいたっては、そのピクセルをDirectXとWPFが共同で所有する必要が生じるため「構築できない」と明言されています。ドロップダウン、ツールチップ、モーダルのオーバーレイ。いずれも3Dビューの上に出るのが自然なUIで、それが出せないという制約は、実装が進んでから判明すると設計のやり直しになります。
判断基準ははっきりしています。3Dビューの上にWPFのUIを重ねる予定が少しでもあるなら、D3DImage(またはそれを内部で使うGLWpfControl)を選んでください。3Dビューが矩形領域を占有し、その上に何も重ねず、フレームレートが最優先という条件が確定しているときだけHwndHostが有利です。迷う段階なら前者を選ぶべきで、理由は移行コストの非対称性にあります。後からHwndHostへ移すのは容易ですが、HwndHostで組んだUIをD3DImageへ移す作業は、重ね合わせを前提に画面設計をやり直すことと同義です。
ハードウェア更新を維持するデバイス設定
更新がハードウェアになる条件
D3DImageはフロントバッファへの転送が入るぶん、純粋なDirect3Dアプリケーションほどは速くなりません。Microsoftの指針も「D3DImageは控えめに使う」ことを勧めており、インスタンス数が増えるほどコマンドバッファのフラッシュが増えて性能が落ちます。具体的な上限数は書かれていませんが、この構造からすると画面あたり1つに抑える設計が無難でしょう。
そのうえで、転送がハードウェアで行われるかソフトウェアコピーに落ちるかは設定で決まります。Microsoftが公開している更新性能の一覧では、WDDM環境かつ9Exデバイスであれば D3DFMT_X8R8G8B8 と D3DFMT_A8R8G8B8 のどちらでも、またサーフェスがロック可能かどうかにかかわらず、すべてハードウェア更新になります。ピクセル形式選びで悩む必要はありません。分岐点は9Exかどうかだけです。ビデオカード側には D3DDEVCAPS2_CAN_STRETCHRECT_FROM_TEXTURES と D3DCAPS2_CANSHARERESOURCE の対応が要ります。
デバイス生成時に指定する2つのフラグ
D3DCREATE_MULTITHREADED は外せません。指定すると性能は下がりますが、WPFの描画システムが別スレッドからこのデバイスのメソッドを呼ぶためです。あわせて、2つのスレッドが同時にデバイスへ触れないよう、ロックプロトコルを正しく守る必要があります。
D3DCREATE_FPU_PRESERVE は、描画をWPFのマネージスレッドで行う場合に強く推奨されます。これを外すとDirect3Dの描画がWPFの倍精度演算の精度を落とし、レイアウトに描画不正を持ち込みます。GLWpfControlが Multithreaded と FpuPreserve を両方指定しているのは、この2点への対応です。
複数モニター構成では、バックバッファを作ったアダプターとフロントバッファを表示するアダプターが食い違うと、アダプター間のコピーが発生して高くつきます。WDDM+9Exで、同一ビデオカード内の別アダプターであれば代償はありません。
フロントバッファ喪失とソフトウェア描画への対処
enableSoftwareFallbackで変わる復帰処理
フロントバッファは、画面ロック、フルスクリーン排他のDirect3Dアプリケーションの起動、ユーザー切り替えといったシステム側の動作で利用できなくなります。このとき IsFrontBufferAvailableChanged イベントが発生。以降の挙動は SetBackBuffer のどのオーバーロードを使ったかで変わります。
enableSoftwareFallback を指定しない、または false にした場合、描画システムはバックバッファへの参照を手放し、何も表示されなくなります。復帰時にはイベントで通知が来るので、SetBackBuffer を呼び直して描画を再開してください。true にした場合は参照が保持されるため、復帰時の呼び直しは不要です。デバイス自体が失われたときだけ、backBuffer に null を渡して解放し、有効なサーフェスで再設定します。
リモートデスクトップで表示が消える条件
見落とされやすい挙動がもう1点あります。WPFがソフトウェア描画になる状況、たとえばリモートデスクトップ接続経由では、D3DImageはDirect3Dコンテンツを表示しません。ただし SetBackBuffer(D3DResourceType, IntPtr, Boolean) に true を指定していれば表示されます。
業務システムをリモートデスクトップで使う運用があるなら、3引数のオーバーロードを true で呼ぶことが実質的な必須要件です。「開発機では出るのに、お客様環境のリモートデスクトップだけ真っ黒」という報告を受けたら、まずここを疑ってください。
経路別の保守状況と2026年時点の採用判断
技術的に可能かどうかと、これから10年保守できるかどうかは別の問いです。WPFでのグラフィックス相互運用は選択肢が複数あるように見えて、実際に更新が続いているものは限られます。判断材料として、各経路の最終更新を並べます。
| 経路 | 最新の安定版 | その公開日 |
|---|---|---|
| OpenTK.GLWpfControl | 4.3.6 | 2026-02-28 |
| OpenTK | 4.9.4 | 2025-03-17(5.0系はプレビュー継続中) |
| Microsoft.Wpf.Interop.DirectX-x64 | 安定版なし | 0.9.0-beta-22856 が2015-11-04 |
| WGL_NV_DX_interop2 仕様 | Revision 2 | 2011-10-04 |
OpenGLを描画元にするなら、答えは1つです。GLWpfControlだけが2026年に入っても更新が続いており、必須条件の実装も一次仕様と一致しています。自前でNV_DX_interopを叩く価値があるのは、既存のネイティブ描画エンジンを抱えていて外部依存を増やせない場合に限られます。逆に、DirectX 12の描画エンジンをWPFへ埋め込む要件では、上表の停滞とinterop2の対象外という条件が重なるため割に合いません。描画部分を別ウィンドウに分離するか、UI基盤ごと見直すほうが早く終わります。
これから新規にWindowsのデスクトップアプリを起こす場合は、そもそもWPFで3Dビューを抱える構成が妥当かを先に問うべきです。WinUI 3とWPFの違いと弱点を踏まえ、既存資産の量と要員のスキルで判断してください。既存のWPF資産に3Dビューを足すという条件であれば、D3DImage経由が最も摩擦の少ない選択です。WPFが2026年時点でも現役といえる根拠は、まさにこうした相互運用の口が維持され続けている点にあります。
よくある質問
D3DImageにDirectX 11のテクスチャをそのまま渡せますか
渡せません。D3DResourceType のメンバーは IDirect3DSurface9 の1つだけで、これは.NET Framework 3.5 SP1で追加されて以降、現行まで変わっていません。DirectX 11を描画元にする場合は、どこかでDirect3D 9Exのサーフェスへコピーする段が必要です。
OpenGLの描画をWPFに出す最短の方法は何ですか
NuGetでOpenTK.GLWpfControl 4.3.6を導入し、XAMLに GLWpfControl を置いて Start を呼ぶ方法です。9Exデバイスの生成とNV_DX_interopの登録は実装済みで、OpenTK 4.9.4以降が前提になります。
Visual StudioでD3DImageを使うのに追加のインストールは必要ですか
D3DImage自体はPresentationCore.dllに含まれるため、WPFのプロジェクトを作れば追加インストールなしで使えます。追加が要るのは描画側で、OpenGLならOpenTKとGLWpfControlのパッケージです。
リモートデスクトップ経由だけ何も表示されないのはなぜですか
WPFがソフトウェア描画になる環境では、D3DImageはDirect3Dコンテンツを表示しない仕様だからです。SetBackBuffer(D3DResourceType, IntPtr, Boolean) の第3引数に true を指定すると表示されます。
HwndHostからD3DImageへ移すと描画は速くなりますか
速くはなりません。D3DImageはフロントバッファへの転送が入るため、素の埋め込みより不利です。移す理由は速度ではなく、3Dビューの上にWPFのUIを重ねられるようになることにあります。