Skip to content

SamplingWithComputeShader

HexagramNM edited this page Mar 1, 2025 · 3 revisions

コンピュートシェーダを用いて、共有テクスチャから映像サンプルを取得する

DirectShowの仮想カメラで映像フレームのピクセルデータを送る際、すでに共有テクスチャに映すべき画像ができているため、このテクスチャのピクセルデータを渡せば良いです。しかし、フォーマットに違いがあるため、以下のような変換処理が必要です。

  • ピクセルデータからアルファ成分を取り除く

    • 共有テクスチャ:BGRAの4バイト -> 映像サンプル:BGRの3バイト
  • 一番下にあるピクセルのデータがメモリ上で先頭に来るように並び変える

    • 共有テクスチャと映像サンプルで上下が逆

DirectShowでは直接DirectXのテクスチャを渡すことはできないため、一度CPU上にピクセルデータをコピーすることになります。コピーしてからCPU上で上記の変換処理を行うこともできますが、 DirectXのコンピュートシェーダを使うことで、CPUにピクセルデータをコピーする前にGPU上で変換処理を行うことができます。 GPU上で並列に処理され、効率よく変換処理を行うことができます。

このコンピュートシェーダではDirectXのデバイス、デバイスコンテキストを使用します。詳細はこちらをご覧ください。以降のコードでは、デバイスは_dxDevice、デバイスコンテキストは_dxDeviceContextの変数に入っているものとします。

コードの詳細(変数の型など)についてはNM_WCVCam_DS/NMVCamFilter.hNM_WCVCam_DS/NMVCamPin.cppをご覧ください。

コンピュートシェーダに必要なものの準備

コンピュートシェーダで必要なものをまとめると以下の通りになります。

また、出力先のGPU上のバッファはCPUからバッファ内のデータにアクセスできないため、別途CPUからアクセス可能なバッファを作成し、そこにデータをコピーする必要があります。

それぞれの関係を図示すると以下のような感じになります。(イメージで描いているので、少々厳密性に欠けるところはありますが)

ComputeShaderFlow.jpg

入力となる共有テクスチャ

NM_WindowCaptureで作成している、キャプチャしたウィンドウ画像を含むテクスチャを入力に使うため、その取得が必要です。DirectXのデバイスにあるOpenSharedResourceByNameを使用すれば、取得することができます。詳しくはこちらをご覧ください。

また、共有テクスチャは他プロセスからアクセスされうるため、映像サンプル取得時にあらかじめmutexを取得し、排他処理を行う必要もあります。 詳しくはこちらをご覧ください。

シェーダリソースビュー

入力となる共有テクスチャをコンピュートシェーダに紐づけるためのものです。

シェーダリソースビューの作成やデバイスコンテキストへの設定は、コンピュートシェーダを実行する度に行います。コンピュートシェーダ実行の度に、シェーダリソースビューをデバイスコンテキストに設定しないと、更新されたテクスチャが反映されず、映像が止まってしまいます。(シェーダリソースビューの作成はDirectXのデバイス設定時に一度行えば良さそうです。)

  • シェーダリソースビューの作成
CD3D11_SHADER_RESOURCE_VIEW_DESC shaderResourceViewDesc(D3D11_SRV_DIMENSION_TEXTURE2D, DXGI_FORMAT_B8G8R8A8_UNORM);

// _sharedCaptureWindowTexture: 共有テクスチャ
// _formatterSRV: シェーダリソースビュー
_dxDevice->CreateShaderResourceView(_sharedCaptureWindowTexture.get(),
    &shaderResourceViewDesc, _formatterSRV.put());
  • デバイスコンテキストへの設定
ID3D11ShaderResourceView* tempShaderResourceViewPtr[] = { _formatterSRV.get() };
_dxDeviceContext->CSSetShaderResources(0, 1, tempShaderResourceViewPtr);

コンピュートシェーダ

テクスチャを入力に、並列に計算処理を行うためのシェーダコードです。コンパイルは通常の頂点シェーダやピクセルシェーダと同じ要領で行います。コンパイルやデバイスコンテキストへの設定はDirectXのデバイスを設定する際に一度行う必要があります。

// hlslFormatterCode: コンピュートシェーダコードの文字列
size_t hlslSize = std::strlen(hlslFormatterCode);

std::string csThreadsStr = std::to_string(CS_THREADS_NUM);
std::string windowWidthStr = std::to_string(VCAM_VIDEO_WIDTH);
std::string windowHeightStr = std::to_string(VCAM_VIDEO_HEIGHT);
com_ptr<ID3DBlob> compiledCS;

// 以下のようにシェーダコード内のマクロを置換するよう設定することもできる。
D3D_SHADER_MACRO csMacro[] = {
    "CS_THREADS_NUM_IN_CS", csThreadsStr.c_str(),
    "VCAM_VIDEO_WIDTH_IN_CS", windowWidthStr.c_str(),
    "VCAM_VIDEO_HEIGHT_IN_CS", windowHeightStr.c_str(),
    NULL, NULL
};

// コンピュートシェーダのコンパイルや作成(頂点シェーダやピクセルシェーダと同じ)
D3DCompile(hlslFormatterCode, hlslSize, nullptr, csMacro, nullptr,
    "formatterMain", "cs_5_0", 0, 0, compiledCS.put(), nullptr);
_dxDevice->CreateComputeShader(compiledCS->GetBufferPointer(),
    compiledCS->GetBufferSize(), nullptr, _formatterCS.put());

// デバイスコンテキストへの設定
_dxDeviceContext->CSSetShader(_formatterCS.get(), 0, 0);

シェーダコードの文字列は、以下の記事を参考にinclude文を用いて直接埋め込んでいます。また、SampleFormatter.hlslはビルド時にコンパイルしないよう、Visual Studioでコードのプロパティを開き、「項目の種類」を「ビルドに含めない」に変更する必要があります。

C++ソース内にシェーダソースを文字列として埋め込む

#define HLSL_EXTERNAL_INCLUDE(...) #__VA_ARGS__

const char* hlslFormatterCode =
#include "SampleFormatter.hlsl"
;

出力先のGPU上のバッファ

コンピュートシェーダの結果を格納するGPU上のバッファです。特に以下のフラグに注意して設定する必要があります。

  • BindFlags: D3D11_BIND_UNORDERED_ACCESSに指定

    後述のUnordered Access Viewに紐づけ、コンピュートシェーダからアクセスできるようにするための設定

  • MiscFlags: D3D11_RESOURCE_MISC_BUFFER_ALLOW_RAW_VIEWSに指定

    コンピュートシェーダ上でバッファをRWByteAddressBufferでアクセスできるようにし、バイト単位でデータを書き込めるようにするための設定

GPU上のバッファはDirectXのデバイスを設定する際に一度作成しておく必要があります。

UINT bufferByteSize = VCAM_VIDEO_WIDTH * VCAM_VIDEO_HEIGHT * PIXEL_BYTE;

D3D11_BUFFER_DESC bufferDesc;
bufferDesc.ByteWidth = bufferByteSize;
bufferDesc.Usage = D3D11_USAGE_DEFAULT;
bufferDesc.BindFlags = D3D11_BIND_UNORDERED_ACCESS;
bufferDesc.CPUAccessFlags = 0;
bufferDesc.MiscFlags = D3D11_RESOURCE_MISC_BUFFER_ALLOW_RAW_VIEWS;

// _gpuFormatterBuffer: 出力先のGPU上のバッファ
_dxDevice->CreateBuffer(&bufferDesc, nullptr, _gpuFormatterBuffer.put());

Unordered Access View

GPU上のバッファはコンピュートシェーダで処理する際に、GPUの複数スレッドから読み書きが行われます。その際に競合なくGPU上のバッファにアクセスできるようにするためのものが、Unordered Access Viewです(ドキュメント)。ここでいうUnordered Accessというのは、複数スレッドから順序を問わずに読み書きのアクセスがされることを指すようです。

このUnordered Access Viewの作成とデバイスコンテキストへの設定はDirectXのデバイスを設定する際に一度行う必要があります。

// Unordered Access Viewの作成
D3D11_UNORDERED_ACCESS_VIEW_DESC uavDesc;
UINT bufferByteSize = VCAM_VIDEO_WIDTH * VCAM_VIDEO_HEIGHT * PIXEL_BYTE;
uavDesc.ViewDimension = D3D11_UAV_DIMENSION_BUFFER;
uavDesc.Format = DXGI_FORMAT_R32_TYPELESS;
uavDesc.Buffer.FirstElement = 0;
uavDesc.Buffer.NumElements = bufferByteSize / 4;
uavDesc.Buffer.Flags = D3D11_BUFFER_UAV_FLAG_RAW;
_dxDevice->CreateUnorderedAccessView(_gpuFormatterBuffer.get(), &uavDesc, _formatterUAV.put());

// Unordered Access Viewをデバイスコンテキストに設定
ID3D11UnorderedAccessView* uavs[] = { _formatterUAV.get() };
UINT initialCounts[] = { 0 };
_dxDeviceContext->CSSetUnorderedAccessViews(0, 1, uavs, initialCounts);

CPUからアクセス可能なバッファ

GPU上のバッファはCPUからアクセスできません。逆にCPUからアクセス可能なバッファをシェーダの入出力に設定することはできません。そのため、GPU上のバッファとCPUからアクセス可能なバッファを両方作っておき、コンピュートシェーダでの処理後に、GPU上のバッファにあるデータをCPUからアクセス可能なバッファにコピーする流れとなります。

CPUからアクセス可能なバッファの場合は、CPUAccessFlagsD3D11_CPU_ACCESS_READにしておく必要があります。CPUからアクセス可能なバッファの作成はDirectXのデバイスを設定する際に一度行う必要があります。

UINT bufferByteSize = VCAM_VIDEO_WIDTH * VCAM_VIDEO_HEIGHT * PIXEL_BYTE;

D3D11_BUFFER_DESC bufferDesc;
bufferDesc.ByteWidth = bufferByteSize;
bufferDesc.Usage = D3D11_USAGE_STAGING;
bufferDesc.BindFlags = 0;
bufferDesc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
bufferDesc.MiscFlags = 0;

// _cpuSampleBuffer: CPUからアクセス可能なバッファ
_dxDevice->CreateBuffer(&bufferDesc, nullptr, _cpuSampleBuffer.put());

コンピュートシェーダの実行

コンピュートシェーダはデバイスコンテキストのDispatchメソッドを呼び出すことで実行されます。詳細は後述しますが、コンピュートシェーダでは、処理を行うのに使用するスレッドの数をシェーダコード内で指定します。このDispatchメソッドの引数はシェーダコードで指定された複数スレッドのグループをいくつ実行するかを指定するものとなります。

Dispatch(gx, gy, gz)と指定した場合はスレッドグループをgx × gy × gzの数だけ実行することになります。(さらにそれぞれのスレッドグループはシェーダコードで指定された数だけのスレッドをもつことになります。)

_dxDeviceContext->Dispatch(VCAM_VIDEO_WIDTH / (CS_THREADS_NUM * 4), VCAM_VIDEO_HEIGHT / CS_THREADS_NUM, 1);

GPUからCPUへのメモリコピー

デバイスコンテキストのCopyResourceメソッドでバッファ間のデータコピーを行うことができます。

// _cpuSampleBuffer: CPUからアクセス可能なバッファ
// _gpuFormatterBuffer: GPU上のバッファ
_dxDeviceContext->CopyResource(_cpuSampleBuffer.get(), _gpuFormatterBuffer.get());

また、CPUからアクセス可能なバッファはIDXGISurfaceMapメソッドを用いることで、DXGI_MAPPED_RECT構造体から中身のデータにアクセスすることができるようになります。アクセスし終えたら、必ずIDXGISurfaceUnmapメソッドを呼び出してください。

com_ptr<IDXGISurface> dxgiSurface;
_cpuSampleBuffer->QueryInterface(IID_PPV_ARGS(dxgiSurface.put()));

DXGI_MAPPED_RECT mapFromCpuSampleBuffer;
dxgiSurface->Map(&mapFromCpuSampleBuffer, DXGI_MAP_READ);

// CPUからアクセス可能なバッファにあるピクセルデータを、DirectShowの仮想カメラに送るメモリにコピー
// sampleData: DirectShowの仮想カメラに送る映像の1フレーム分のピクセルデータ(LPByte型)
CopyMemory((PVOID)sampleData, (PVOID)mapFromCpuSampleBuffer.pBits, 
    VCAM_VIDEO_WIDTH * VCAM_VIDEO_HEIGHT * PIXEL_BYTE);

dxgiSurface->Unmap();

コンピュートシェーダコードの解説

処理について

以下のシェーダコードでテクスチャでのBGRAのフォーマットを、映像サンプルでのBGRのフォーマットに変換しています。offscreenTextureで入力として与えた共有テクスチャに、outputBufferで出力先であるGPU上のバッファにアクセスできます。

最後のindexの計算でy座標にあたる部分をdispatchThreadId.yではなく(VCAM_VIDEO_HEIGHT_IN_CS - dispatchThreadId.y - 1)とすることで、映像サンプルに合わせて上下が反転するようにしています。

HLSL_EXTERNAL_INCLUDE(

Texture2D<float4> offscreenTexture : register(t0);
RWByteAddressBuffer outputBuffer: register(u0);

[numthreads(CS_THREADS_NUM_IN_CS, CS_THREADS_NUM_IN_CS, 1)]
void formatterMain(uint3 dispatchThreadId: SV_DispatchThreadID)
{
    float4 pixel0 = offscreenTexture.Load(int3(4 * dispatchThreadId.x, dispatchThreadId.y, 0));
    float4 pixel1 = offscreenTexture.Load(int3(4 * dispatchThreadId.x + 1, dispatchThreadId.y, 0));
    float4 pixel2 = offscreenTexture.Load(int3(4 * dispatchThreadId.x + 2, dispatchThreadId.y, 0));
    float4 pixel3 = offscreenTexture.Load(int3(4 * dispatchThreadId.x + 3, dispatchThreadId.y, 0));
    
    uint3 bgr24_3;
    bgr24_3.x = (uint(pixel0.b * 255.0) & 0xFF) | ((uint(pixel0.g * 255.0) & 0xFF) << 8)
        | ((uint(pixel0.r * 255.0) & 0xFF) << 16) | ((uint(pixel1.b * 255.0) & 0xFF) << 24);
    bgr24_3.y = (uint(pixel1.g * 255.0) & 0xFF) | ((uint(pixel1.r * 255.0) & 0xFF) << 8)
        | ((uint(pixel2.b * 255.0) & 0xFF) << 16) | ((uint(pixel2.g * 255.0) & 0xFF) << 24);
    bgr24_3.z = (uint(pixel2.r * 255.0) & 0xFF) | ((uint(pixel3.b * 255.0) & 0xFF) << 8)
        | ((uint(pixel3.g * 255.0) & 0xFF) << 16) | ((uint(pixel3.r * 255.0) & 0xFF) << 24);
    
    uint index = ((VCAM_VIDEO_HEIGHT_IN_CS - dispatchThreadId.y - 1) * VCAM_VIDEO_WIDTH_IN_CS 
        + 4 * dispatchThreadId.x) * 3;
    outputBuffer.Store3(index, bgr24_3);
}

)

注意点として、RWByteAddressBufferがメモリアラインメントの影響で4バイト単位でのアクセスしかできないことが挙げられます。そのため、1つの処理で横4ピクセル分をまとめて処理しています。こうすることで、GPU上のバッファに4ピクセル×3バイト=計12バイトを1回の処理で書き込むようにしています。映像の縦と横のピクセル数は基本4の倍数であるので、余りを考える必要もありません。

dispatchThreadIdについて

コンピュートシェーダでは、[numthreads(tx, ty, tz)]で1つのスレッドグループあたりのスレッド数を指定します。この場合、tx × ty × tzの数だけスレッドグループ内にスレッドが作られます。ただ、下記のドキュメントにあるようにスレッドグループ内のスレッド数に上限があり、コンピュートシェーダのバージョンがcs_5_0の場合は1024個が上限です。

numthreads


dispatchThreadIdはそれぞれのスレッドのIDにあたるもので、x, y, zに対応した3つの整数値からなります。簡単にいうと、numthreadsの引数とデバイスコンテキストのDispatchの引数に応じて下記のような範囲でx, y, zの整数値を取り、全ての組み合わせに対して1回ずつコンピュートシェーダの処理が走ります。

[numthreads(tx, ty, tz)], Dispatch(gx, gy, gz)の場合 [numthreads(20, 20, 1)], Dispatch(24, 54, 1)の場合
x成分 0 ~ (tx × gx - 1) 0 ~ 479
y成分 0 ~ (ty × gy - 1) 0 ~ 1079
z成分 0 ~ (tz × gz - 1) 0 ~ 0

戻る

Clone this wiki locally