2013/01/23

DirectShow のループ

自前のウィンドウを持ってる場合は IMediaEventEx でメッセージを登録するのが楽そうだけど、ライブラリの中とかコンソールアプリケーションなんかではウィンドウメッセージを受け取れないので使えない。

そういうときは、別スレッドを立ち上げて IMediaEvent::WaitForCompletion で再生終了を検出するのがよさそうだった。

 

というメモ。

GPU!

リアルタイムに絵を大量にブレンドするのは、やっぱり CPU ではきつかったので GPU の力を借りることにした。

image

上の画像は NHK クリエイティブライブラリから借りてきた映像を 5 つ同時に表示してみてるところ。さらにテスト用 α チャンネル入りの画像をブレンドしてみてる。

  1. Haali Media Splitter → ffdshow Video Decoder → Video Mixing Renderer 9 のグラフを構築
  2. VMR9 にカスタムプレゼンタを設定。グラフを再生すると、IDirect3D9Surface の形でサーフェスを取得できるので、それを共有用に自前で作ったテクスチャにコピー
  3. ミキサーでそれぞれの共有用テクスチャを取得して、シェーダーで合成してプレーンに書き込む
  4. メインメモリ転送用のバッファに結果をコピー
  5. メインメモリに結果をコピー

という感じ。キャプチャから合成まで、GPU メモリ間の転送だけで済むので(たぶん)負荷は軽いはず。

CPU 使用率は 10% ほど。ドロップもなく軽快に動いてくれた。

 

C++ スキルが 0.2 くらい上がったかもしれない。

2013/01/06

ビデオミキサー

そういうわけで作ったらしい。

image

左下の緑のは、XAML のアニメーション再生のテスト。時刻はカスタムアセンブリの読み込みテスト。そして半透明の六角形はアルファブレンドのテスト。

60 FPS で動かしてみてるけど CPU 結構食ってる気がする。それでも 1 コア使い切ってないように見えるからまだ平気。

拡大縮小処理がまだ入っていないので、出力は 1280 x 720 固定。

とりあえず動くようにしただけなので、公開は未定。

アルファブレンドその2

修正版。

__declspec(align(16)) unsigned char alpha_mask[16] = 
{
    0x06, 0x80, 0x06, 0x80, 0x06, 0x80, 0x80, 0x80,
    0x0e, 0x80, 0x0e, 0x80, 0x0e, 0x80, 0x80, 0x80,
};
__declspec(align(16)) unsigned char swap_endian_bl[16] = 
{
    0x80, 0x00, 0x80, 0x02, 0x80, 0x04, 0x80, 0x06,
    0x80, 0x08, 0x80, 0x0a, 0x80, 0x0c, 0x80, 0x0e,
};
__declspec(align(16)) unsigned char swap_endian_lb[16] = 
{
    0x01, 0x80, 0x03, 0x80, 0x05, 0x80, 0x07, 0x80,
    0x09, 0x80, 0x0b, 0x80, 0x0d, 0x80, 0x0f, 0x80,
};
__declspec(align(16)) unsigned int andnot_mask[4] = 
{
    0xFF00FF00, 0xFF00FF00, 0xFF00FF00, 0xFF00FF00
};

//
// Alpha blending implement optimized with SSE4
//
void alpha_blend_sse4(void* background, void* foreground, int pixels)
{
    int count = pixels / 4;
    __asm 
    {
        mov ecx, count
        mov esi, dword ptr background
        mov edi, dword ptr foreground
        
        movdqa xmm6, dword ptr swap_endian_bl
        movdqa xmm7, dword ptr alpha_mask

L0:
        //-------------------------------------- Get B.rgb
        movdqa xmm0, [esi]    // B.argb
        movhlps xmm1, xmm0

        pmovzxbw xmm2, xmm0 // B.rgb
        pmovzxbw xmm3, xmm1 // B.rgb

        // using: xmm2, xmm3

        //-------------------------------------- Get F.rgb
        movdqa xmm4, [edi] // F.argb
        movhlps xmm5, xmm4

        // escape swap_endian_bl and alpha_mask to xmm0 and xmm1
        movdqa xmm0, xmm6
        movdqa xmm1, xmm7

        pmovzxbw xmm6, xmm4 // F.rgb
        pmovzxbw xmm7, xmm5 // F.rgb

        // using: xmm2, xmm3, xmm6, xmm7

        //-------------------------------------- Get F.a
        movdqa xmm4, xmm6
        movdqa xmm5, xmm7
        pshufb xmm4, xmm1 // F.a   xmm1 = alpha_mask
        pshufb xmm5, xmm1 // F.a
        
        // using: xmm2, xmm3, xmm4, xmm5, xmm6, xmm7

        //-------------------------------------- Flip endianness
        pshufb xmm2, xmm0 // xmm0 = swap_endian_bl
        pshufb xmm3, xmm0
        pshufb xmm4, xmm0
        pshufb xmm5, xmm0
        pshufb xmm6, xmm0
        pshufb xmm7, xmm0

        
        //-------------------------------------- Calc X = F.rgb * F.a
        pmulhuw xmm6, xmm4
        pmulhuw xmm7, xmm5

        //-------------------------------------- Calc Y = 1 - B.a
        pandn xmm4, dword ptr andnot_mask
        pandn xmm5, dword ptr andnot_mask
        
        //-------------------------------------- Calc Z = B.rgb * Y
        pmulhuw xmm4, xmm2
        pmulhuw xmm5, xmm3

        //-------------------------------------- Calc R = X + Z
        paddusw xmm6, xmm4
        paddusw xmm7, xmm5

        //-------------------------------------- Flip endianness
        movdqa xmm2, dword ptr swap_endian_lb
        pshufb xmm6, xmm2
        pshufb xmm7, xmm2

        //-------------------------------------- Pack values
        packuswb xmm6, xmm7

        //-------------------------------------- Write result
        movdqa [esi], xmm6
        
        //-------------------------------------- Restore escaped values
        movdqa xmm6, xmm0
        movdqa xmm7, xmm1
        
        //-------------------------------------- 
        add esi, 16
        add edi, 16
        sub ecx, 1
        jnz L0
    };
}

切り捨てが起こってしまう気がするけど、何枚も重ねないし問題ないぜ!

背景画像
base

前景画像
1

結果
out

コード使いたいという方は MIT ライセンスでどうぞ。

アルファブレンド

__declspec(align(16)) unsigned int erase_alpha[4] = 
{
    0xFF000000, 0xFF000000, 0xFF000000, 0xFF000000
};
__declspec(align(16)) unsigned char alpha_mask[16] = 
{
    0x06, 0x80, 0x06, 0x80, 0x06, 0x80, 0x06, 0x80,
    0x0e, 0x80, 0x0e, 0x80, 0x0e, 0x80, 0x0e, 0x80,
};
__declspec(align(16)) unsigned char swap_endian_bl[16] = 
{
    0x80, 0x00, 0x80, 0x02, 0x80, 0x04, 0x80, 0x06,
    0x80, 0x08, 0x80, 0x0a, 0x80, 0x0c, 0x80, 0x0e,
};
__declspec(align(16)) unsigned char swap_endian_lb[16] = 
{
    0x01, 0x80, 0x03, 0x80, 0x05, 0x80, 0x07, 0x80,
    0x09, 0x80, 0x0b, 0x80, 0x0d, 0x80, 0x0f, 0x80,
};

void alpha_blend_sse4(void* background, void* foreground, int count)
{
    __asm 
    {
        mov ecx, count
        mov esi, dword ptr background
        mov edi, dword ptr foreground
        
        movdqa xmm6, dword ptr swap_endian_bl
        movdqa xmm7, dword ptr alpha_mask

L0:
        //-------------------------------------- Get B.rgb
        movdqa xmm0, [esi]    // B.argb
        movhlps xmm1, xmm0

        pmovzxbw xmm2, xmm0 // B.rgb
        pmovzxbw xmm3, xmm1 // B.rgb

        // using: xmm2, xmm3

        //-------------------------------------- Get F.rgb
        movdqa xmm4, [edi] // F.argb
        movhlps xmm5, xmm4

        // escape swap_endian_bl and alpha_mask to xmm0 and xmm1
        movdqa xmm0, xmm6
        movdqa xmm1, xmm7

        pmovzxbw xmm6, xmm4 // F.rgb
        pmovzxbw xmm7, xmm5 // F.rgb

        // using: xmm2, xmm3, xmm6, xmm7

        //-------------------------------------- Get F.a
        movdqa xmm4, xmm6
        movdqa xmm5, xmm7
        pshufb xmm4, xmm1 // F.a   xmm1 = alpha_mask
        pshufb xmm5, xmm1 // F.a
        
        // using: xmm2, xmm3, xmm4, xmm5, xmm6, xmm7

        //-------------------------------------- Flip endianness
        pshufb xmm2, xmm0 // xmm0 = swap_endian_bl
        pshufb xmm3, xmm0
        pshufb xmm4, xmm0
        pshufb xmm5, xmm0
        pshufb xmm6, xmm0
        pshufb xmm7, xmm0

        
        //-------------------------------------- Calc F.rgb * F.a
        pmulhuw xmm6, xmm4
        pmulhuw xmm7, xmm5
        
        //-------------------------------------- Calc B.rgb * F.a
        pmulhuw xmm4, xmm2
        pmulhuw xmm5, xmm3

        //-------------------------------------- Calc (F.rgb * F.a) + B.rgb
        paddq xmm6, xmm2
        paddq xmm7, xmm3

        //-------------------------------------- Calc ((F.rgb * F.a) + B.rgb) - (B.rgb * F.a)
        psubq xmm4, xmm6
        psubq xmm5, xmm7

        //-------------------------------------- Flip endianness
        movdqa xmm2, dword ptr swap_endian_lb
        pshufb xmm4, xmm2
        pshufb xmm5, xmm2

        //-------------------------------------- Pack values
        packuswb xmm4, xmm5
        por xmm4, dword ptr erase_alpha

        //-------------------------------------- Write result
        movdqa [esi], xmm4

        
        //-------------------------------------- Restore escaped values
        movdqa xmm6, xmm0
        movdqa xmm7, xmm1
        
        //-------------------------------------- 
        add esi, 16
        add edi, 16
        sub ecx, 1
        jnz L0
    };
}

初のインラインアセンブラで頑張ったけどまだ色がおかしい……。

処理時間は Core i7 920 上で 1280 x 720 の画像を 2 枚重ねると 1.2 msec くらい。普通にやるより 5 倍近く速い。

AVX っていう命令系も使えればメモリへアクセスしてる部分が減りそうなんだけど、Sandy 以降の CPU じゃないと使えないらしい。残念。

2013/01/03

Producer-Consumer 問題

スレッドからスレッドへと、次々にデータを受け渡すときに必要になるのがこれ。

描画スレッドを別スレッドに移動させるときに必要だったのでござる。http://www.atmarkit.co.jp/fdotnet/mthread/mthread04/mthread04_02.html の例をジェネリクスにしただけでござる。

class BoundedBuffer<T> : IDisposable
    where T: IDisposable
{
    public int Max { get; private set; }
    public int Count
    {
        get
        {
            lock (lockObject)
            {
                return queue.Count;
            }
        }
    }

    private Queue<T> queue;
    private object lockObject = new object();


    public BoundedBuffer(int maxCount)
    {
        this.Max = maxCount;
        this.queue = new Queue<T>(Max);
    }

    public void Enqueue(T value)
    {
        Monitor.Enter(lockObject);
        try
        {
            while (queue.Count >= Max)
            {
                Monitor.Wait(lockObject);
            }
            queue.Enqueue(value);
            Monitor.PulseAll(lockObject);
        }
        catch
        {
        }
        finally
        {
            Monitor.Exit(lockObject);
        }
    }

    public T Dequeue()
    {
        Monitor.Enter(lockObject);
        T result = default(T);
        try
        {
            while (!queue.Any())
            {
                Monitor.Wait(lockObject);
            }
            result = queue.Dequeue();
            Monitor.PulseAll(lockObject);
        }
        catch (Exception e)
        {

        }
        finally
        {
            Monitor.Exit(lockObject);
        }

        return result;
    }

    public void Dispose()
    {
        lock (lockObject)
        {
            T value;
            while ((value = queue.Dequeue()) != null)
            {
                value.Dispose();
            }
        }
    }
}

そのものずばりの解答を見ているとはいえ、上手く動いてくれると気持ち良いでござる。

RenderTargetBitmap の罠

大晦日に作ったフィルタで複雑な XAML ファイルを読み込んでみたら、10 ~ 15 FPS しか出てなかった。調べてみたら RenderTargetBitmap の Render メソッドで 50 msec くらいかかってた。

よく考えてみれば RenderTargetBitmap はメインメモリ上にあるので、ソフトウェアで XAML が描画されてるということになる。ハードウェアで描画するためには、ビデオメモリ上のレンダーターゲットに書き込んでもらわねばならないからだ。

ハードウェア上のレンダーターゲットからメインメモリに画像データを転送する機能は WPF には実装されておらず、されたとしてもストールが発生して非常にコストのかかる処理になってしまうので非現実的っぽい。(非同期で転送すればいいのかもだけど。)

改善策としては、現在の WPF レンダラの実装だと1 コアしか描画に使ってないので、描画するパーツを分けてそれぞれ独立に描画させるとか。面倒なのでやらないけど。

そもそもがテロップ用に作ったものなので、そんなに複雑なものは表示させないという。