PSGPの無効化

先日,環境を変えてリプレイデータを再生すると同じ結果が再現されないという問題の対応コードを書きました.原因はD3DXの算術演算関数が返す値が,x87 FPUを利用した場合とSSEや3DNow!を利用した場合で誤差のオーダーで異なっていたことでした.この小さな差違のために同じコントローラ入力を行ってもゲーム進行が微妙に異なってしまい,リプレイの正常な再生ができなくなっていたようです.本来ならば行列計算などの算術演算コードを全て自前のルーチンに差し替えて解決すべき問題ですが,今回は手っ取り早い解決法としてProcessor Specific Graphics Pipeline (PSGP)を無効化させることにしました.PSGPが無効化されることで算術演算は常にx87 FPUで実行されるようになります.
DirectXのProcessor Specific Graphics Pipeline (PSGP)を無効にするには,以下のレジストリキーにDWORD型で1を設定します.

HKEY_LOCAL_MACHINE\software\Microsoft\Direct3D\DisablePSGP

しかしリプレイを再生させるためだけにシステム全体の設定を変更したくはありません.そこで静的リンクしたD3DXのルーチンがこのレジストリキーを実行時に読み取ると仮定して,RegQueryValueExのフックで対応できないか試してみました.以前紹介した(id:NyaRuRu:20040614#p3)APIHijackを使ってみます.

#include "stdafx.h"
#include "apihijack.h"

typedef LONG (WINAPI * RegQueryValueExEntry)(HKEY, LPCTSTR, LPDWORD, LPDWORD, LPBYTE, LPDWORD );
LONG WINAPI RegQueryValueExHook(HKEY, LPCTSTR, LPDWORD, LPDWORD, LPBYTE, LPDWORD );

SDLLHook APIHookTable = 
{
    "Advapi32.DLL",
    false, NULL,
    {
        { "RegQueryValueExA", RegQueryValueExHook },
        { NULL, NULL }
    }
};

enum APIHookTableOrder
{
    FuncRegQueryValueEx = 0,
};

LONG __stdcall RegQueryValueExHook(
  HKEY hKey,            // キーのハンドル
  LPCTSTR lpValueName,  // レジストリエントリ名
  LPDWORD lpReserved,   // 予約済み
  LPDWORD lpType,       // データ型が格納されるバッファ
  LPBYTE lpData,        // データが格納されるバッファ
  LPDWORD lpcbData      // データバッファのサイズ
)
{
    static RegQueryValueExEntry RegQueryValueExOriginal =
        (RegQueryValueExEntry) APIHookTable.Functions[FuncRegQueryValueEx].OrigFn;
    
    // DisablePSGP および DisableD3DXPSGP のみ決めうちで乗っ取り
    // 本来は hKey が HKEY_LOCAL_MACHINE\Software\Microsoft\Direct3D を指していることを確認すべきである
    if( strcmpi(lpValueName, "DisablePSGP") == 0 || strcmpi(lpValueName, "DisableD3DXPSGP") == 0 )
    {
        if( lpcbData != NULL && *lpcbData == 4 )
        {
            if( lpData != NULL )
            {
                *reinterpret_cast<DWORD*>(lpData) = 1;
            }
            if( lpType != NULL )
            {
                *lpType = REG_DWORD;
            }
            return ERROR_SUCCESS;
        }
    }

    // 通常はオリジナル版の RegQueryValueEx に処理を委譲する
    return RegQueryValueExOriginal( hKey, lpValueName, lpReserved, lpType, lpData, lpcbData );
}

int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE, LPSTR, int )
{
    HookAPICalls( &APIHookTable );

    ......
}

このコードを追加した場合,DirectXデバッグメッセージから確かにPSGPが無効化されていることが分かりました.というわけでこの方法に決定です*1.通常の商用アプリケーションにはこういうコードは組み込みたくないですけどね……

*1:ただし実際のAPIフックには『.NET&Windowsプログラマのためのデバッグテクニック徹底解説(asin:4891003529)』に付属していたHookImportedFunctionsByNames関数を組み込んでいます.