float->UNORM変換丸め確認

ということで、先日*1予告していたROPのfloat->UNORM変換が単純な「切り捨て」ではなく、「近い数値へ丸め」となっているかどうかの検証をやってみた。

検証プログラムの概要

こんな感じのプログラムで検証をしたいと思う。

  1. ComputeShaderで3DテクスチャにRGB->HSV->RGB変換した値を書き込む。RGBそれぞれ0〜255の3次元とする
  2. テクスチャをSTAGINGでCPU_ACCESS_READなテクスチャにロードしてくる
  3. テクスチャをMap()してひたすら期待値と比較する

ComputeShaderの実行され方(想像)

ドキュメントが見当たらないのでゴトウさんの記事*2を読んでの想像だけど、きっとこんな感じで実行されると思う。

  • ID3D11DeviceContext::Dispatch(x, y, z)はx*y*z個のスレッドグループを作る
  • 各スレッドグループはSIMD Engine 20個に順次振り分けられる
  • スレッドグループごとにnumthreadsで指定した数のスレッドが実行される
  • SIMD Engine内で16個のプロセッサが動くので16スレッドずつ実行される
  • 命令は4cycleずつ発射されるので16x4スレッドは同じ命令を実行する
  • 4cycleかけて64スレッドずつ実行される

ComputeShaderソース

シェーダはこんな感じ。(コメントを追記して再掲)

#include "hsv.hlsl"     // 先日のhlslコードそのまま(今回は省略)

#define     NUM_N           256     // 1次元分の個数

// スレッド数。Radeonは16x4が1セット(Wavefront?)なので64の倍数になるようにする
#define     NUM_THREAD_X    16
#ifdef THREAD_COVERED_ALL           // 256個全部を1スレッドグループで処理する場合
    #define NUM_THREAD_Y    (NUM_N / (NUM_THREAD_X * NUM_THREAD_Z))
#else                               // おとなしく64個ずつ1スレッドグループで処理する場合
    #define NUM_THREAD_Y     4
#endif
#define     NUM_THREAD_Z     1

RWTexture3D<float4> g_result : register( u0 );  // 出力先テクスチャ(UAV0でバインド)

[numthreads(NUM_THREAD_X, NUM_THREAD_Y, NUM_THREAD_Z)]  // 1個のスレッドグループに割り当てるスレッドの数を指定
void CS(
    uint3 Gid : SV_GroupID,     // スレッドグループの通番
    uint GI : SV_GroupIndex     // スレッドの通番
)
{
    // 自分の番号をRGB値にする
#ifdef GI_IS_B
    uint3       rgb = uint3(Gid.x, Gid.y, Gid.z*NUM_THREAD_X*NUM_THREAD_Y*NUM_THREAD_Z+GI);
#else
    uint3       rgb = uint3(Gid.x*NUM_THREAD_X*NUM_THREAD_Y*NUM_THREAD_Z+GI, Gid.y, Gid.z);
#endif
    const float div = 1.0f / 255.0f;    // 0〜255を0.0〜1.0に変換するための係数

    g_result[rgb] = float4(hsv2rgb(rgb2hsv(div * rgb)), 1.0f);  // RGB->HSV->再RGBしてテクスチャへwrite
}

THREAD_COVERED_ALLとGI_IS_Bのdefineは性能評価用の処理分けなので次回以降で説明予定。
シェーダは(x, y, z)それぞれ256個に分割して合計16M個のスレッドを動かしている。numthreadsで指定したスレッド数NUM_THREAD_X×NUM_THREAD_Y×NUM_THREAD_Z個のスレッドは1つのスレッドグループとして上述した通り1つのSIMD Engineで実行される(タブンネー)。
シェーダコードとしてはグループID、スレッドIDから上記の(x, y, z)256個のIDをそのままRGB値として、HSV-RGB変換後の値をテクスチャ座標(x, y, z)にRGB値を指定してテクスチャに書き込んでいる。
つまり、変換誤差が無ければ(x, y, z)座標値がそのままテクスチャのRGB値と一致するようにしてある。

比較ルーチン

ソース内コメントの通りなので説明は省略。
HLSL側でRGBをそのまま出力先座標(x, y, z)としているので読み取り側もあわせる。テクスチャはR8G8B8A8_UNORMにしているので、先日*3記事にした通りCPUからはCOLORREFでアクセスする。
CpuAccessResourceはすでに先日*4記事にしているのでそちらを参照。

bool cmpRgb(int r, int g, int b, COLORREF color)
{
    return (r == GetRValue(color)) && (g == GetGValue(color)) && (b == GetBValue(color));
}
void printRgb(int r, int g, int b, COLORREF color)
{
    // 元のRGB値と差分を表示する
    std::cout << str(boost::format("(%d,%d,%d) -> (%d,%d,%d)") % r % g % b % (GetRValue(color) - r) % (GetGValue(color) - g) % (GetBValue(color) - b)) << std::endl;
}
void reportDifferenceRgb(ID3D11DeviceContext *context, ID3D11Texture3D *texture)
{
    // この後自動でMap()
    const CpuAccessResource<COLORREF> &readable = CpuAccessResource<COLORREF>::getReadableTexture(context, texture);

    for(int r=0;r<256;r++){
        for(int g=0;g<256;g++){
            for(int b=0;b<256;b++){
                COLORREF        quad = *readable.get3d(r, g, b);    // 値をもらってきて(rgbがそのままxyz座標)
                if(!cmpRgb(r, g, b, quad)){     // 比較して異なっていたら
                    printRgb(r, g, b, quad);    // 表示する
                }
            }
        }
    }
    // 自動でUnmap()
}

初期化

前回までに解説したルーチンを呼んでいるだけなので詳細は省略。
エラー処理も長いだけで本質的じゃないので掲載省略。

boost::intrusive_ptr<ID3D11Device>                  g_device;
boost::intrusive_ptr<ID3D11DeviceContext>           g_context;
boost::intrusive_ptr<ID3D11ComputeShader>           g_cs;
boost::intrusive_ptr<ID3D11Texture3D>               g_rgb_gpu;
boost::intrusive_ptr<ID3D11UnorderedAccessView>     g_uav;
boost::intrusive_ptr<ID3D11Texture3D>               g_rgb_cpu;
//-----------------------------------------------------------------------------
void initDevice(void)
{
    ID3D11Device            *dev;
    ID3D11DeviceContext     *con;

    // この関数は以前CopyResource()でPowerMediaDockの速度を見た時のD3D11CreateDevice()を関数化しただけ
    createDeviceOnly(NULL, &dev, &con);     // NULLはIDXGIAdapter *

    g_device = comPtr(dev);
    g_context = comPtr(con);
}
void initTexture(void)
{
    Resolution  res(256, 256, 256);     // RGBそれぞれ0〜255の256個

    // USAGE_DEFAULTを指定するラッパなので詳細は省略
    g_rgb_gpu = comPtr(createGpuTexture3d(g_device.get(), res, DXGI_FORMAT_R8G8B8A8_UNORM, D3D11_BIND_UNORDERED_ACCESS));
    g_uav = comPtr(createUav3d(g_device.get(), g_rgb_gpu.get(), DXGI_FORMAT_R8G8B8A8_UNORM));
    // STAGINGでCPU_ACCESS_READ
    g_rgb_cpu = comPtr(createCpuTexture3d(g_device.get(), res, DXGI_FORMAT_R8G8B8A8_UNORM));
}
void setupShader(const std::string &filename, const D3D10_SHADER_MACRO *defines)
{
    boost::intrusive_ptr<ID3DBlob>      blob;

    blob = comPtr(loadShaderBlob(filename, defines, "CS", "cs_5_0"));
    g_cs = comPtr(createComputeShader(g_device.get(), blob.get()));
}
void setupAllBShader(void)
{
    const D3D10_SHADER_MACRO        defines[] = {
        {"GI_IS_B", "1"},               // B側をスレッドに割り当てる
        {"THREAD_COVERED_ALL", "1"},    // スレッド側でBをすべてカバーする
        {NULL, NULL}
    };

    setupShader("all_b.blob", defines);
}
void setupApartBShader(void)
{
    const D3D10_SHADER_MACRO        defines[] = {
        {"GI_IS_B", "1"},               // B側をスレッドに割り当てる
        {NULL, NULL}
    };

    setupShader("apart_b.blob", defines);
}
void setupAllRShader(void)
{
    const D3D10_SHADER_MACRO        defines[] = {
        {"THREAD_COVERED_ALL", "1"},    // スレッド側でRをすべてカバーする
        {NULL, NULL}
    };

    setupShader("all_r.blob", defines);
}
void setupApartRShader(void)
{
    const D3D10_SHADER_MACRO        defines[] = {
        {NULL, NULL}
    };

    setupShader("apart_r.blob", defines);
}
void setupCompute(void)
{
    ID3D11UnorderedAccessView   *v[] = {g_uav.get()};

    g_context->CSSetShader(g_cs.get(), NULL, 0);
    g_context->CSSetUnorderedAccessViews(0, countof(v), v, 0);
}
void kickRGB(int r, int g, int b)
{
    g_context->Dispatch(r, g, b);
}
void kickAllB(void)
{
    kickRGB(256, 256, 1);
}
void kickApartB(void)
{
    // apart側はスレッドに64個、スレッドグループ側に4グループ担当させる
    kickRGB(256, 256, 256/(16*4));
}
void kickAllR(void)
{
    kickRGB(1, 256, 256);
}
void kickApartR(void)
{
    kickRGB(256/(16*4), 256, 256);
}
void readbackResult(void)
{
    g_context->CopyResource(g_rgb_cpu.get(), g_rgb_gpu.get());
}
void reportResult(void)
{
    reportDifferenceRgb(g_context.get(), g_rgb_cpu.get());
}

制御関連

性能評価用のループがある以外は特筆すべき内容なし。
計測処理が入っていて見づらいけど、

  1. シェーダを実行する
  2. テクスチャを読み戻す
  3. 期待値と照合する

を実施しているだけ

int         NUM_CYCLE       = 10;       // 実行繰り返し数(性能評価用途)
bool        IS_B            = false;    // GI_IS_Bをdefineする場合はtrue(性能評価用途)
//-----------------------------------------------------------------------------
void setCycle(const std::string &arg)
{
    if(arg == ""){
        return;
    }

    NUM_CYCLE = static_cast<int>(std::strtol(arg.c_str(), NULL, 0));
}
void setIsB(const std::string &arg)
{
    IS_B = arg == "b";
}
void setupEnvironment(void)
{
    initDevice();
    initTexture();
}
void measureAny(bool is_all)
{
    Utility::StopWatch      part_watch;
    Utility::StopWatch      total_watch;

    void (*setup_shader_table[2][2])(void) = {{setupApartRShader, setupAllRShader},{setupApartBShader, setupAllBShader}};
    void (*kick_table        [2][2])(void) = {{kickApartR,        kickAllR},       {kickApartB,        kickAllB}       };

    const std::string kind = str(boost::format("%s %s") % ((is_all)? "All" : "Apart") % ((IS_B)? "B": "R"));


    // シェーダを設定
    setup_shader_table[IS_B][is_all]();
    setupCompute();

    std::cout << str(boost::format("%s start.") % kind) << std::endl;
    total_watch.start();
    part_watch.start();

    // 実行ループ
    for(int i=0;i<NUM_CYCLE;i++){
        kick_table[IS_B][is_all]();
    }

    part_watch.stop();
    std::cout << str(boost::format("%s %d cycle done. %d.%09dsec.") % kind % NUM_CYCLE % part_watch.getSec() % part_watch.getNsec()) << std::endl;
    part_watch.start();

    // 結果読み取り、照合
    readbackResult();
    reportResult();

    part_watch.stop();
    total_watch.stop();
    std::cout << str(boost::format("%s verify done. %d.%09dsec.") % kind % part_watch.getSec() % part_watch.getNsec()) << std::endl;

    std::cout << str(boost::format("%s total %d.%09dsec.") % kind % total_watch.getSec() % total_watch.getNsec()) << std::endl;
}
void measureAll(void)
{
    measureAny(true);
}
void measureApart(void)
{
    measureAny(false);
}

main関数

int main(int argc, char *argv[])
{
    if(argc > 1){
        setCycle(argv[1]);
    }
    if(argc > 2){
        setIsB(argv[2]);
    }

    try{
        setupEnvironment();
        measureAll();
        measureApart();
    }catch(const std::exception &e){
        std::cerr << e.what() << std::endl;
    }

    return 0;
}

結果

実際に動かしてみると差分は表示されなかった。
先日のCPU側での検証でfloatによる数値計算上の誤差が確認されていたわけだけど、今回の結果からROPがfloat->UNORM変換する際に「近い数値へ丸め」を実施してくれているってことになる。
誤差は+方向と−方向の両方があったので、仮にfloatからUNORMへ変換する際に単純に255倍して少数部を切り捨てていたら−方向の誤差がある個所で差分が出てしまう。−方向の誤差があっても四捨五入的な丸めを実施していれば、例えば8.999的な数値が9に繰り上がるので問題は無い。このあたりはCPU側での検証でわざと少数切り捨てを実施して確認済み。(記事としては掲載してないけど)

繰り返しになるが、reportDifferenceRgb()関数の説明箇所に書いた通り、(そして以前記事に書いた通り)R8G8B8A8_UNORMのテクスチャはCOLORREFとしてアクセスすると良い感じであることが確認できた。同じくシェーダでfloat4の(x, y, z, w)がそのまま(R, G, B, A)として出力されていることが確認できた。(仮に違っていたら差分として表示されるため)

ところで

DirectXヘルプの「Direct3D 10」−「Programming Guide」−「Resources」−「Data Conversion Rules
」ページの「Integer Conversion」表の「Source Data Type」が「FLOAT」、「Destination Data Type」が「UNORM」の欄に

  • c = c + 0.5f.
  • The decimal fraction is dropped, and the remaining floating point (integral) value is converted directly to an integer.

がROPでの変換ルールなのかな。
まんま四捨五入って気がするね。

え?
ヘルプに書いてあるならわざわざプログラムで確認しなくても良いじゃん。だって?

そうだね…(´・ω・`)

記事にするためにいろいろ裏を取っていたら発見したんだ…
とほほ。

次回予告

で、せっかくComputeShaderのコードを書いたので性能評価のコードもくっつけてみた。ってことで次回以降でいろいろ見ていきたいなぁと思う。
ちなみに「GI_IS_B」側は死ぬほど重い。というか、むしろループ数を300回にしたら

ディスプレイドライバが応答を停止しましたが、正常に回復しました。

になって死亡扱いされちゃう…orz

このあたり(重い理由)も考察してみようと思う。
例によって迷宮入りしそうな気もするケドネー。

*1:d:id:maminus:20130223

*2:具体的にどこを見たとかじゃないけどあえてあげるならこのあたり?

*3:d:id:maminus:20130221

*4:d:id:maminus:20130224