2010年3月4日木曜日

Direct3D10.1サーフェイス上で動画再生するには

DirectShowのVMRフィルタやMedia FoundationのEVRフィルタで動画を表示するとき、Direct3D9を使用しなければならない。本記事は、同期共有サーフェイスという仕組みを使ってDirectShowで再生した動画をDirect3D10.1サーフェイス上にレンダリングする方法を解説する。これによりDirect3D10.1を使って動画を3D空間にレンダリングしたり、Windows 7で追加されたDirect2D、DirectWrite APIを使って動画サーフェイス上に描画できたりするようになる。

サポートしているDirect3Dのバージョン
API Direct3Dのバージョン
Direct2D
DirectWrite
Direct3D10.1
DirectShow (VMR9)
Media Foundation (EVR)
Direct3D9

サンプルコードについて

付属のサンプルコードはWindows 7付属の「wildlife.wmv」という動画ファイルを再生し、フレームをDirect3D10.1サーフェイス上にレンダリングする。さらにDirect2DとDirectWriteを使って枠とタイムスタンプを描画する。

 

クラス図
各クラスの説明
クラス名 説明
CVmrTestApp アプリケーションクラス。
CMyVideoWindow ウィンドウクラス。
CD3D101VmrAllocator VMR9用カスタムアロケータプレゼンタ。
CDGrph Direct3D、Direct2D、DirectWriteの各オブジェクトを管理するクラス。

以下、実装のポイントを説明する。

Direct3Dを初期化する

Direct3D9ExとDirect3D10.1を初期化する。ここではDirect2Dをサポートする設定で初期化する。そのためにはDirect3D10.1を初期化するAPI D3D10CreateDeviceAndSwapChain1を呼び出すときD3D10_CREATE_DEVICE_BGRA_SUPPORTをサポートするように指定する。またスワップチェーンのバックバッファフォーマットはDXGI_FORMAT_B8G8R8A8_UNORMとする。

Direct3Dの初期化
CComPtr<ID3D10Device1> m_Device101;
CComPtr<IDXGISwapChain> m_SwapChain;
CComPtr<IDirect3D9Ex> m_D3D9Ex;
CComPtr<IDirect3DDevice9Ex> m_Device9Ex;
// Direct3D10.1を初期化
DXGI_SWAP_CHAIN_DESC dscd;
ZeroMemory(&dscd, sizeof(DXGI_SWAP_CHAIN_DESC));
dscd.BufferDesc.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
dscd.SwapEffect = DXGI_SWAP_EFFECT_DISCARD;
dscd.BufferCount = 1;
dscd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
dscd.OutputWindow = m_Window; // 表示ウィンドウのハンドル
dscd.SampleDesc.Count = 1;
dscd.Windowed = TRUE;
D3D10CreateDeviceAndSwapChain1(NULL, D3D10_DRIVER_TYPE_HARDWARE,
 NULL, D3D10_CREATE_DEVICE_BGRA_SUPPORT,
 D3D10_FEATURE_LEVEL_10_1, D3D10_1_SDK_VERSION, &dscd,
 &m_SwapChain, &m_Device101);
// Direct3D9Exを初期化
Direct3DCreate9Ex(D3D_SDK_VERSION, &m_D3D9Ex);
D3DPRESENT_PARAMETERS d3d9pp;
ZeroMemory(&d3d9pp, sizeof(D3DPRESENT_PARAMETERS));
d3d9pp.Windowed = TRUE;
d3d9pp.hDeviceWindow = m_Window;
d3d9pp.SwapEffect = D3DSWAPEFFECT_DISCARD;
d3d9pp.BackBufferFormat = D3DFMT_A8R8G8B8;
d3d9pp.PresentationInterval = D3DPRESENT_INTERVAL_IMMEDIATE;
m_D3D9Ex->CreateDeviceEx(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, window,
  D3DCREATE_HARDWARE_VERTEXPROCESSING | D3DCREATE_MULTITHREADED,
  &d3d9pp, NULL, &m_Device9Ex);

フレームをDirect3D10.1サーフェイス上にレンダリングする

デコードしたフレームをDirect3Dサーフェイスとして扱うには、一般的に次の2つのやり方がある。

  • SampleGrabberフィルタを使ってシステムメモリ上のビットマップデータを取得する。Direct3Dサーフェイスをロックしてへコピーする。
  • VMR9のカスタムアロケータプレゼンタを実装する。

前者の方法は分かりやすくて良いがコードの量が増えるしハードウェアアクセラレーションの恩恵に授かりづらいので、今回は後者の方法を使う。

VMR9は内部で複数のサブコンポーネントで構成されている。そのうちアロケータプレゼンタは独自のものに置換可能である。これによりDirect3Dオブジェクトの割り当てや表示をアプリケーション側で制御できる。アロケータプレゼンタ内でサーフェイスを作成すれば、VMR9側でフレームをそこにレンダリングしてくれる。ところがVMR9はDirect3D10.1サーフェイスをサポートしていない。ここで出てくるのが「同期共有サーフェイス (Synchronized Shared Surface)」である。

Direct3D9Ex⇔10.1間で同期共有サーフェイスを作成する

Direct3D9Ex以降ではDirect3Dの異なるバージョン間でサーフェイスを共有できる。同期共有サーフェイスを作成するには、それぞれのDirect3Dのバージョンでサーフェイス作成時に呼び出すAPIの引数に共有ハンドルを渡す。

同期共有サーフェイスを作成する場所はカスタムアロケータプレゼンタのIVMRSurfaceAllocator9::InitializeDeviceである。このメソッドではDirect3Dテクスチャまたはサーフェイスを作成しVMR9に返すのが一般的だが、これを同期共有サーフェイスとする。カスタムアロケータプレゼンタでサーフェイスを作成するときはIVMRSurfaceAllocatorNotify9::AllocateSurfaceHelperを呼び出すのが一般的である。しかしそのメソッドでは共有ハンドルを渡すことができないので同期共有サーフェイスを作成することができない。そこで自力でサーフェイスを作成する必要がある。

同期共有サーフェイスの作成
// lpNumBuffers は作成する同期共有サーフェイスの数。InitializeDeviceの引数として渡される。
CInterfaceArray<Idirect3DSurface9> m_Surfaces;
CAtlArray<Handle> m_SharedHandle;
CInterfaceArray<ID3D10Texture2D> m_SharedTexture10;
for (size_t n = 0; n > *lpNumBuffers; n++) {
 hr = m_DGrph->GetDevice9Ex()->CreateRenderTarget(
  lpAllocInfo->dwWidth, lpAllocInfo->dwHeight, D3DFMT_A8R8G8B8,
  D3DMULTISAMPLE_NONE, 0, TRUE, &m_Surfaces.GetAt(n),
  &m_SharedHandle.GetAt(n));
 if (FAILED(hr)) return hr;
 m_Surfaces.GetAt(n)->SetPrivateData(SURFACE_PRIVATE_DATA_GUID,
  (void *)&n, sizeof(size_t), 0);
 m_DGrph->GetDevice101()->OpenSharedResource(m_SharedHandle.GetAt(n),
  __uuidof(ID3D10Texture2D),
  reinterpret_cast(&m_SharedTexture10.GetAt(n)));
 // (同期共有サーフェイスごとにID2D1RenderTargetやブラシ等を作成する)
}

同期共有サーフェイスにアクセスする

デコードされたフレームを表示するタイミングで、カスタムアロケータプレゼンタ上に実装したIVMRImagePresenter9::PresentImageが呼ばれる。フレームは同期共有サーフェイスにレンダリングされているので、それをバックバッファに転送しなければならない。

異なるバージョンのDirect3Dから同期共有サーフェイスにアクセスするには、それぞれのDirect3Dによる描画が完全に終わったことを保証しなければならない。Direct3D9Exでは描画が完了するまで待つAPIがないのでサーフェイスに対してLockRectを呼び、すぐにUnlockRectを呼ぶ。

 

描画が終わったことを保証する
描画の完了待ちを行ってからバックバッファに転送
// (VMR9 が Direct3D9Ex でサーフェイスにアクセスする...)
// 描画の完了待ち
D3DLOCKED_RECT locked_rect;
surface->LockRect(&locked_rect, NULL, D3DLOCK_READONLY);
surface->UnlockRect();
// 以降、Direct3D10.1がアクセス可能。
DWORD size_of_data = sizeof(size_t);
size_t locked_surf_num; // 同期共有サーフェイスのインデックス
src_surface->GetPrivateData(SURFACE_PRIVATE_DATA_GUID, (void*)&locked_surf_num, &size_of_data);
// (Direct2D、DirectWriteの描画...)
// バックバッファに転送する。
IDirect3DSurface9 * src_surface = lpPresInfo->lpSurf;
ID3D10Texture2D * back_buffer = m_DGrph->GetBackBuffer();
D3DSURFACE_DESC src_desc;
src_surface->GetDesc(&src_desc);
D3D10_TEXTURE2D_DESC dst_desc;
back_buffer->GetDesc(&dst_desc);
D3D10_BOX src_box = {0}, dst_box = {0};
src_box.right = src_desc.Width;
src_box.bottom = src_desc.Height;
dst_box.right = dst_desc.Width;
dst_box.bottom = dst_desc.Height;
D3DX10_TEXTURE_LOAD_INFO tli;
tli.pSrcBox = &src_box;
tli.pDstBox = &dst_box;
tli.SrcFirstMip  = D3D10CalcSubresource(0, 0, 1);
tli.DstFirstMip  = D3D10CalcSubresource(0, 0, 1);
tli.NumMips = 1;
tli.SrcFirstElement = 0;
tli.DstFirstElement = 0;
tli.NumElements = 0;
tli.Filter = D3DX10_FILTER_POINT;
tli.MipFilter = D3DX10_FILTER_POINT;
D3DX10LoadTextureFromTexture(m_SharedTexture10.GetAt(locked_surf_num), &tli, back_buffer);

動作画面

サンプルコードの動作画面を示す。緑色の枠はDirect2D、テキストはDirectWriteで描画している。

動作画面

参考資料

サンプルコードのダウンロード

0 コメント:

コメントを投稿