Date

Windows VistaやXPにはVideo Mixing Renderer 9(以下、VMR9)呼ばれるDirectShowフィルタが内蔵されている。このフィルタは、特定のウィンドウに画像を表示するフィルタだが、その動作をカスタマイズすることができるようになっている。

今回は自前で実装したリサンプリングアルゴリズムを適用してみる。VMR9 では動画を拡大、縮小表示するとき、一般に線形フィルタが適用されるが、それをカスタムシェーダを使ってLanczos Resamplingにどうにか置き換えてみたいと思う。さらに Windows Vista に実装されている Direct3D9Ex と組み合わせてみたい。

必要な環境

  1. Windows Vista / 7 (Direct3D9Ex を用いるので XP では動かない。)
  2. Visual Studio 2008
  3. Windows SDK for Windows Server 2008 and .NET Framework 3.5
  4. DirectX SDK (November 2008)
  5. Direct3D9Exおよびシェーダモデル3.0に対応したビデオカード

VMR9のカスタマイズの概要

VMR9は画像を表示するための各工程を担う複数のモジュールで構成されている。そのうち「アロケータプレゼンタ」と「イメージコンポジタ」と呼ばれるモジュールについては、自前の処理に置き換えることができる。アロケータプレゼンタは、Direct3DデバイスやDirect3Dサーフェイスの管理、また、実際に画面に表示する役割を担っている。イメージコンポジタはアロケータプレゼンタが確保したサーフェイスに対し、複数の入力画像をブレンドする。

今回は独自に実装したアロケータプレゼンタCScalerクラスを実装する。アロケータプレゼンタのクラスはIVMRSurfaceAllocator9IVMRImagePresenter9という2つのインターフェイスを継承しなければならない(図1)。なお、今回はCScalerクラスの実装にCComObjectRootExというATLのテンプレートクラスを取り入れている。ATLはアロケータプレゼンタの実装には必須ではないが、COMに関わるコードを簡潔に記述できるのでお勧めだ。サンプルプログラムではウィンドウ管理にもATLのCWindowImplテンプレートクラスを用いている。

フィルタグラフの構築

VMR9のインスタンスの生成と設定

VMR9を使うフィルタグラフを構築する。VMR9を使うにはCLSID_VideoMixingRenderer9というクラスIDでインスタンスを作成する。VMR9のインスタンスはインテリジェント接続で自動的に作成されないので、明示的に作成する必要がある。

1
2
CComPtr<IBaseFilter> m_Vmr;
m_Vmr.CoCreateInstance(CLSID_VideoMixingRenderer9);

VMR9のインスタンスを作成したら、他のフィルタと接続する前に以下の設定する。

  1. 入力ストリーム数を1に設定する。
  2. VMR9の操作モードをレンダリングレスモードにする。これによりVMRの既定のレンダリング処理が無効になる。
  3. VMR9とCScalerクラスを関連付ける。

1 と 2 は VMR9 から取得した IVMRFilterConfig9 インターフェイス、 3 は IVMRSurfaceAllocatorNotify9 インターフェイスで行う。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
CComPtr<IGraphBuilder> m_Graph;
CComPtr<IBaseFilter> m_Vmr; // VMR9
CComPtr<CScaler> m_VmrAlloc; // アロケータプレゼンタ
// VMR9のインスタンス作成
m_Graph.CoCreateInstance(CLSID_FilterGraph);
m_Vmr.CoCreateInstance(CLSID_VideoMixingRenderer9);
// 入力ストリームと操作モードの設定
CComQIPtr<IVMRFilterConfig9> vmr_config(m_Vmr);
vmr_config->SetNumberOfStreams(1);
vmr_config->SetRenderingMode(VMR9Mode_Renderless);
// VMR9とCScalerクラスを関連付ける
// hwndはレンダリング先のウィンドウハンドル
m_VmrAlloc.Attach(new CScaler(hwnd));
CComQIPtr<IVMRSurfaceAllocatorNotify9> vmr_notify(m_Vmr);
vmr_notify->AdviseSurfaceAllocator(0xACDCACDC, m_VmrAlloc);
m_VmrAlloc->AdviseNotify(vmr_notify);

VMR9と接続する

単に動画を再生するだけですので、まずVMR9のインスタンスを引数としてAddfilterを呼び、フィルタグラフへ追加する。それからRenderFileを呼び、残りのフィルタをフィルタグラフへ追加する。インテリジェント接続によってソースフィルタやデコーダフィルタが自動的に追加され、VMR9に向かって接続される。

1
2
3
4
m_Graph->AddFilter(m_Vmr, L"VMR9");
m_Graph->RenderFile(filename, NULL);
CComQIPtr<IMediaControl> mc(m_Graph);
mc->Run();

アロケータプレゼンタの実装

アロケータプレゼンタでは、次のような処理を実装する。

  1. Direct3D9ExとDirect3D9Exデバイスを初期化する。
  2. Direct3D9サーフェイスを確保する。
  3. 独自に使用するリソースを初期化する。
  4. レンダリング。
  5. デバイス消失時に復旧する。

Direct3D9ExとDirect3D9Exデバイスを初期化する

Direct3D9Exの初期化はCScalerのコンストラクタで行う。

1
2
3
4
5
HRESULT CScaler::FinalConstruct() {
    ATLTRACE("%s\n", __FUNCTION__);
    ATLENSURE(Direct3DCreate9Ex(D3D_SDK_VERSION, &m_D3D)==S_OK);
    return S_OK;
}

画像を表示するウィンドウを設定するSetWindowメソッドを定義する。VMR9とCScalerクラスを関連付けたら、表示するウィンドウのハンドルを指定してこのメソッドを呼ぶことにする。ここでDirect3D9Exデバイスの初期化を行う。

1
2
3
4
5
6
void CScaler::SetWindow(HWND hwnd) {
    ATLENSURE(IsWindow(hwnd)!=FALSE);
    m_Window=hwnd;
    ATLENSURE(m_D3D);
    ATLENSURE(CreateDevice()==S_OK);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
HRESULT CScaler::CreateDevice() {
    D3DPRESENT_PARAMETERS pp;
    CreatePresParam(pp);
    HRESULT hr;
    hr=m_D3D->CreateDeviceEx(D3DADAPTER_DEFAULT
        , D3DDEVTYPE_HAL, m_Window
        , D3DCREATE_SOFTWARE_VERTEXPROCESSING | D3DCREATE_MULTITHREADED
        , &pp, NULL, &m_D3DDev);
    hr=m_D3DDev->GetRenderTarget(0, &m_RenderTarget.p);
    return hr;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
//プレゼントパラメータの作成
HRESULT CScaler::CreatePresParam(D3DPRESENT_PARAMETERS &pp) {
    D3DDISPLAYMODE dm;
    ATLENSURE_RETURN_HR(m_D3D, E_UNEXPECTED);
    HRESULT hr=m_D3D->GetAdapterDisplayMode(D3DADAPTER_DEFAULT, &dm);
    if(FAILED(hr)) {
        return hr;
    }
    ZeroMemory(&pp, sizeof(D3DPRESENT_PARAMETERS));
    pp.Windowed=TRUE;
    pp.hDeviceWindow=m_Window;
    pp.SwapEffect=D3DSWAPEFFECT_COPY;
    pp.BackBufferFormat=dm.Format;
    return hr;
}

ここまではDirect3D9Exを初期化しただけで、まだVMR9と関連付けがされていない状態である。IDirect3DDevice9Exとそれに関連付けられているモニタのハンドルをVMR9に渡す必要がある。それにはVMR9から取得したIVMRSurfaceAllocatorNotify9::SetD3DDeviceIVMRSurfaceAllocator9::AdviseNotifyの中から呼び出す。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
STDMETHODIMP
CScaler::AdviseNotify(IVMRSurfaceAllocatorNotify9 *lpIVMRSurfAllocNotify) {
    CAutoLock my_lock(m_CritSec);
    ATLTRACE("%s\n", __FUNCTION__);
    ATLENSURE_RETURN_HR(m_D3D, E_UNEXPECTED);
    ATLENSURE_RETURN_HR(m_D3DDev, E_UNEXPECTED);
    ATLENSURE_RETURN_HR(lpIVMRSurfAllocNotify, E_POINTER);
    m_SurfAllocNotify=lpIVMRSurfAllocNotify;
    HMONITOR monitor=m_D3D->GetAdapterMonitor(D3DADAPTER_DEFAULT);
    return m_SurfAllocNotify->SetD3DDevice(m_D3DDev, monitor);
}

Direct3D9サーフェイスを確保する

デバイスの初期化まで終わったので、次はVMR9がアップストリームフィルタから入力する画像バッファとして使用するサーフェイスの確保をする。

Direct3D9サーフェイスや、その他のレンダリングに必要なリソースの確保はIVMRSurfaceAllocator9::InitializeDeviceで行う。

メソッドの定義は次のようになっている。

1
2
HRESULT STDMETHODCALLTYPE InitializeDevice(
DWORD_PTR dwUserID, VMR9AllocationInfo *lpAllocInfo, DWORD *lpNumBuffers);
dwUserID VMRを識別するID。(今回はVMRを1つしか使わないので無視。)
lpAllocInfo どのようなサーフェイスを確保するかを示す`VMR9AllocationInfo`構造体へのポインタ。
lpNumBuffers 確保するサーフェイスの数。

lpAllocInfodwWidthに入力画像の幅、dwHeightに高さが、それぞれ入っている。これらを元にサーフェイスを作成する。しかし、今回は次の2つの要件を満たす必要がある。

  1. カスタムシェーダを使ってLanzcos Resamplingを行うため、IDirect3DTexture9によるアクセスが可能である、つまりテクスチャとして扱えること。
  2. テクスチャのサイズは、幅、高さともに2の累乗であること。

アロケータプレゼンタ用のサーフェイスを作成するにはIDirect3D9Device::CreateTextureではなくIVMRSurfaceAllocatorNotify9::AllocateSurfaceHelperを使う。このときVMR9AllocationInfo::dwFlagsVMR9AllocFlag_TextureSurfaceフラグを追加すると、テクスチャとしてアクセスできる。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
STDMETHODIMP CScaler::InitializeDevice(DWORD_PTR dwUserID
    , VMR9AllocationInfo *lpAllocInfo, DWORD *lpNumBuffers)
{
    HRESULT hr=NOERROR;
    ATLENSURE_RETURN_HR(lpAllocInfo, E_POINTER);
    ATLENSURE_RETURN_HR(lpNumBuffers, E_POINTER);
    ATLENSURE_RETURN_HR(m_D3D, E_UNEXPECTED);
    ATLENSURE_RETURN_HR(m_D3DDev, E_UNEXPECTED);
    // 入力画像を包含する2の累乗サイズを求める
    DWORD texture_width=1, texture_height=1;
    while(texture_width <=lpAllocInfo->dwWidth)  texture_width*=2;
    while(texture_height<=lpAllocInfo->dwHeight) texture_height*=2;
    // TODO : 独自に使用するリソースを確保する処理を、ここに実装する
    // (Lanzcos Resampling で使用するリソースの確保。後述)
    // テクスチャを lpNumBuffers で指定された数だけ作成する
    lpAllocInfo->dwWidth =texture_width;
    lpAllocInfo->dwHeight=texture_height;
    lpAllocInfo->Format  =D3DFMT_X8R8G8B8;
    m_Surfaces.resize(*lpNumBuffers);
    lpAllocInfo->dwFlags|=VMR9AllocFlag_TextureSurface;
    hr=m_SurfAllocNotify->AllocateSurfaceHelper(lpAllocInfo
        , lpNumBuffers, &m_Surfaces[0]);
    return hr;
}

InitializeDeviceで確保したサーフェイスを返すIVMRSurfaceAllocator9::GetSurfaceを実装する。このメソッドで返すサーフェイスは、参照カウンタを増やす必要がある。VMR9がデコードされたフレームを描画する領域を提供するときに呼ばれる。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
STDMETHODIMP CScaler::GetSurface(DWORD_PTR dwUserID
    , DWORD SurfaceIndex, DWORD SurfaceFlags
    , IDirect3DSurface9 **lplpSurface)
{
    ATLENSURE_RETURN_HR(lplpSurface, E_POINTER);
    ATLENSURE_RETURN_HR(SurfaceIndex>=m_Surfaces.size(), E_INVALIDARG);
    CAutoLock my_lock(m_CritSec);
    *lplpSurface=m_Surfaces[SurfaceIndex];
    (*lplpSurface)->AddRef(); // 参照カウンタを増やす
    return S_OK;
}

Lanzcos Resampling で使用するリソースを初期化する

アロケータプレゼンタとしての必要最低限の初期化が終わったので、ここからはLanzcos Resamplingで使用するリソースを初期化していく。必要なリソースとしては次のものが挙げられる。

  1. 領域外参照対策テクスチャ
  2. 領域外参照対策テクスチャにレンダリングする頂点バッファ
  3. バックバッファにレンダリングするための頂点バッファ
  4. 頂点宣言
  5. Lanzcos Resamplingアルゴリズムを実装したエフェクト

このようなアロケータプレゼンタが独自に使用するリソースは、デバイス消失時に復旧する必要が無ければコンストラクタ、必要があるものはCScaler::InitializeDeviceで初期化すると良いだろう。(CScaler::InitializeDeviceの TODO コメントに書いた辺りに実装する。)

テクスチャ、頂点バッファ、頂点宣言、エフェクトの初期化については、一般的なDirect3Dアプリケーションと同じなので割愛する。画像を平面にレンダリングする場合に考慮すべき点について説明する。

領域外参照対策

Lanzcos Resamplingのようなアルゴリズムの場合、原画像の領域外を参照する。(今回はLanzcos2と呼ばれる方式を使うため横4縦4テクセルを参照する。)基準となる点が原画像の端に近い場合、領域外を参照するため正しい色を求めることができず、出力画像において不正な色になってしまう。

対策の方法はいくつかあるが、今回は「領域外参照対策テクスチャ」を別途用意する。このテクスチャには、原画像を左上に単純コピーし、余った領域には反転してコピーする。それから、領域外参照対策テクスチャをもとに、テクスチャサンプラーのアドレッシングモードをMirrorにしてバックバッファにレンダリングする。

たとえば原画像が640x480ピクセルの場合、1024x512ピクセルのテクスチャを用意する。左上に単純コピーしたもの、右上にX軸で反転、左下にY軸で反転、右下にX軸とY軸で反転した画像をレンダリングする。

テクセルとピクセルの対応

テクセルとピクセルを1対1でレンダリングする場合、0.5ピクセル左上にずらしてレンダリングするように頂点を作成する。なぜならDirect3Dのラスタライズルールでは座標(0,0)は左上隅にあるピクセルの「中央」を指しているからである。

0.5ピクセル左上にずらさずにレンダリングした場合、テクスチャが意図したとおりサンプリングされない。テクスチャサンプラーのフィルタがLinearの場合、周囲4点のピクセルをブレンドした結果が出力されるため、輪郭がぼやけてしまう。たとえフィルタがPointであっても意図しないテクセルがサンプリングされることがある。

なお、ラスタライズルールについてはMSDNのRasterization Rules (Direct3D 9)を参照すること。また、テクセルとピクセルの対応についてはDirectly Mapping Texels to Pixels (Direct3D 9)を参照すること。

リソースの解放

ここまで確保したリソースを解放する処理を実装する。CComPtrを使えば自動的にIUnknown::Releaseが呼ばれるので、このような実装は不要と考えてしまいそうだが、IVMRSurfaceAllocator9::TerminateDeviceが呼ばれたときとデバイス消失時は、手動で解放する必要がある。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
STDMETHODIMP CScaler::TerminateDevice(DWORD_PTR dwID) {
    ReleaseResources();
    return S_OK;
}
void CScaler::ReleaseResources() {
    CAutoLock my_lock(m_CritSec);
    for(size_t i=0;i<m_Surfaces.size();i++) {
        m_Surfaces[i].Release();
    }
    m_MirroredTexture.Release();
    m_VtxMirror.Release();
    m_VtxSrc.Release();
    m_VtxDecl.Release();
    m_Effect.Release();
}

レンダリング

レンダリングはIVMRImagePresenter9::PresentImageで行う。定義は次のようになっている。

1
HRESULT STDMETHODCALLTYPE PresentImage(DWORD_PTR dwUserID, VMR9PresentationInfo *lpPresInfo);
dwUserID VMRを識別するID。(今回はVMRを1つしか使わないので無視。)
lpPresInfo 入力画像の情報。

VMR9PresentationInfo::lpSurfにVMR9に入力された画像のサーフェイスへのポインタが入っている。これはInitializeDeviceで初期化したサーフェイスである。これをテクスチャとして、まず領域外参照対策テクスチャにレンダリングする。それから、Lanzcos Resamplingアルゴリズムを実装したカスタムシェーダを使ってバックバッファにレンダリングする。

レンダリングした後は IDirect3D9DeviceEx::PresentEx` でバックバッファをウィンドウに表示する。このとき、戻り値によっては追加の対応が必要である。

戻り値 対応内容
S_OKまたはS_PRESENT_OCCLUDED なにもしなくてよい。
S_PRESENT_MODE_CHANGED デバイスをリセットしなければならない。 `IDirect3DDevice9Ex::ResetEx`を呼び出す。
D3DERR_DEVICELOST リソースを全て解放し、再初期化する。
D3DERR_DEVICEHUNGまたD3DERR_DEVICEREMOVED エラー終了、または、他のデバイスを使って復帰する。

S_PRESENT_MODE_CHANGEDは解像度を変更したときに戻ってくる。解像度を変更するとDirect3D9ではデバイスが消失していたが、Direct3D9Exではデバイスは消失しない。(リセットは必要。)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
STDMETHODIMP CScaler::PresentImage(DWORD_PTR dwUserID
    , VMR9PresentationInfo *lpPresInfo)
{
    HRESULT hr;
    UINT num_of_passes;
    CAutoLock my_lock(m_CritSec);
    ATLTRACE("%s\n", __FUNCTION__);
    // VMR9PresentationInfo::lpSurf からIDirect3DTexture9を取り出す
    CComPtr<IDirect3DTexture9> src_texture;
    lpPresInfo->lpSurf->GetContainer(
        IID_IDirect3DTexture9, (void**)&src_texture);
    // 領域外参照対策テクスチャからIDirect3DSurface9を取り出す
    CComPtr<IDirect3DSurface9> mirrored_surface;
    m_MirroredTexture->GetSurfaceLevel(0, &mirrored_surface);
    m_D3DDev->BeginScene();
    m_D3DDev->SetVertexDeclaration(m_VtxDecl);
    // 領域外参照対策テクスチャにレンダリングする
    m_D3DDev->SetRenderTarget(0, mirrored_surface);
    m_D3DDev->Clear(0, NULL, D3DCLEAR_TARGET, 0, 1.0f, 0);
    m_Effect->SetTechnique("simple");
    m_Effect->SetTexture("__tex0", src_texture);
    m_D3DDev->SetStreamSource(0, m_VtxMirror, 0, sizeof(SCALER_VTX));
    m_Effect->Begin(&num_of_passes, 0);
    m_Effect->BeginPass(0);
    m_D3DDev->DrawPrimitive(D3DPT_TRIANGLESTRIP, 0, 4);
    m_D3DDev->DrawPrimitive(D3DPT_TRIANGLESTRIP, 6, 4);
    m_Effect->EndPass();
    m_Effect->End();
    // バックバッファにレンダリングする
    m_D3DDev->SetRenderTarget(0, m_RenderTarget);
    m_Effect->SetTechnique("lanzcos");
    m_Effect->SetTexture("__tex0", m_MirroredTexture);
    m_D3DDev->SetStreamSource(0, m_VtxSrc, 0, sizeof(SCALER_VTX));
    m_Effect->Begin(&num_of_passes, 0);
    m_Effect->BeginPass(0);
    m_D3DDev->DrawPrimitive(D3DPT_TRIANGLESTRIP, 0, 2);
    m_Effect->EndPass();
    m_Effect->End();
    m_D3DDev->EndScene();
    hr=m_D3DDev->PresentEx(NULL, NULL, NULL, NULL, NULL);
    if(hr==S_PRESENT_MODE_CHANGED) {
        // 解像度が変更されたときにS_PRESENT_MODE_CHANGEDとなる
        // ResetEx を呼び出してデバイスをリセットする必要がある
        D3DPRESENT_PARAMETERS pp;
        CreatePresParam(pp);
        m_D3DDev->ResetEx(&pp, NULL);
        return S_OK;
    }
    // hr==S_PRESENT_OCCLUDED 問題なし
    if(hr==D3DERR_DEVICELOST) {
        // エラーが発生したら全リソースを解放し、再度、初期化する
        ReleaseResources();
        m_RenderTarget.Release();
        m_D3DDev.Release();
        CreateDevice();
        HMONITOR monitor=
            m_D3D->GetAdapterMonitor(D3DADAPTER_DEFAULT);
        hr=m_SurfAllocNotify->ChangeD3DDevice(m_D3DDev, monitor);
    }
    if(hr==D3DERR_DEVICEHUNG || hr==D3DERR_DEVICEREMOVED) {
        // デバイスがハングまたは取り外された場合、エラーを返す
        return E_FAIL;
    }
    return hr;
}

カスタムシェーダの実装

Lanczos resamplingアルゴリズムをカスタムシェーダとして実装する。と、その前にLanczos resampling の式における α の値をいくつにするか決めておく。このαの値で参照するテクセル数が決まってくるが、今回はα=2とする。この場合、幅4高さ4の合計16テクセルを参照することになる。カスタムシェーダを使った16テクセル分のテクスチャサンプリングについては t-pot バイキュービック が詳しいので、そちらを参照のこと。

サンプリングの後は、各テクセルの重みを計算し、出力する色を決める。

まず、テクセルの左上端と、参照しようとしている点の距離を求める。ピクセルシェーダからfrac関数を使う。この関数は浮動小数の小数部だけを0以上1未満の値で返してくれる関数である。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
struct VSOUT_LANZCOS
{
    float4 Pos  : POSITION;
    float2 Tex0 : TEXCOORD0;
    float2 Tex1 : TEXCOORD1;
    // ... 中略
    float2 Tex7 : TEXCOORD7;
};
// 頂点シェーダからの入力
VSOUT_LANZCOS input;
// col00, col01, ... , col33 の各々に
// テクセルがサンプリングされているものとする。
// input.Tex5 が参照しているテクセルの左上端までの距離を計算する。
float2 uv_d=frac(input.Tex5*__tex0_size)-0.5f;

距離を求めたら、Lanczos resamplingの式に当てはめて、各テクセルの重みを計算する。このとき、計算の途中で除数が0になる場合があるので、max関数を使って0.000001未満にならないようにしている。

1
2
3
4
5
6
7
8
9
const float minpd=0.000001;
const float a=2.0;
const float pi=3.1415926;
float pd;
float4 wx, wy;
// 横方向(列)の重みを wx.x, wx.y, wx.z, wx.w にそれぞれ求める
pd=max(minpd, abs(pi*(uv_d.x+1)));
wx.x=a*sin(pd)*sin(pd/a)/(pd*pd);
// . . . 以下同様に、縦方向(行)の重みを wy.x, wy.y, wy.z, wy.w にそれぞれ求める(略)

サンプリングしたテクセルの色にそれぞれの重みを乗じた値を求める。例えばcol00に対してはwx.x*wy.xを乗じる。

最後に重みの合計で除算し、出力する色を求める。

動作確認

実際に動作を確認してみよう。ソースコードでPublicのサンプルビデオフォルダのButterfly.wmv再生するようにしてある。また、ウィンドウのサイズはCVmrTestApp::PreMessageLoopで直接指定しているので適宜変更すること。

https://github.com/mahorigahama/Vmr9_Test


Comments

comments powered by Disqus