コード」カテゴリーアーカイブ

WindowsのDPIスケーリング処理について一考察

WindowModePatchで実装するのに大分困っていたDPIスケーリング処理(DPIAwareness)についてメモ書き代わりに処理について書いておきます。ちなみに、英語の読み間違いを防ぐためにいっておくと、日本語的には「DPIによるスケーリング(解像度変更)処理」というべきなのでしょうが、英語では「DPIAwareness」となっており、Awarenessの意味は「気がつく」「知る」なので、直訳すると「DPIについてプログラム側が気がついているかどうかを処理する」という意味になってしまいます。これが私が処理について誤解した原因なのですが…。

 

DPIスケーリングを指定する3つのモード

はい、まずは私が翻訳をミスったポイントです。簡単に調べてみるとほかの日本語サイトではちょっと解説してあったのでなんともいえないのですが。基本的にはプログラム側が指定できるDPIスケーリングのモードは3つあります。それが

  1. DPI処理について関知しない(Unaware)
  2. DPI処理をアプリケーション初期化時にのみ行い、後はシステムに任せる(System aware)
  3. DPI処理を完全にアプリケーション側で行う。途中で(ウィンドウの位置移動によって表示を行うモニタが変更されるといった原因で)変更される場合にはメッセージを送るので、そのメッセージを確認してアプリケーション側が処理を行う(Per Monitor DPI aware)

となります。ちょっと注意してほしいのが1.のUnawareパターンですが、これはつまり「アプリケーション側は何もやらなくてもシステム側(すなわちOS側)が勝手にやるので気にしなくてもいいよ」という意味です。プログラム側からこれを設定すると表示上は勝手にスケーリングしてしまうのでWindowModePatchでは非常に都合が悪いモードになっています。通常のアプリケーションでは面倒であれば1.のパターンを使うのもありだと思います。まあ、今時のゲームであれば解像度調整機能は必須なので1.のUnawareを使うことはまずないと思います。

 

DPIスケーリング処理は通常はマニフェスト側で設定する

Microsoft側としてはこちらを想定しているようです。というのも、この後愚痴のように出てきますがアプリケーションが起動してからだとスケーリングモードの変更がプロセス単位でできないことが多いのです。(これについては推察になりますが、おそらくWindowModePatchと同じように座標を設定/取得する関数の処理をスケーリングに合わせて変更する、というものが割り込むことが「想定されるのか否か」によって初期化の方法が変わるのために難しいのではないか?というものです。)まあ、通常のアプリケーションならマニフェスト指定が簡単でしょうね。

やり方としては

  1. VisualStudioなどのビルドツールにおいてマニフェストの設定で「DPIの認識」を設定する
  2. マニフェスト内のwindowSettingsセクション内にdpiAware値およびdpiAwareness値を設定する

というものです。前者の方が勝手に作ってくれる分簡単ですね。なお、マニフェストにDPIに関する設定がないときはシステム側が勝手にスケーリングをするDPIUnaware状態となりますので注意してください。

 

アプリケーション側からの設定はOSごとに方法が異なる

まず紹介されている方法として自身のプロセス全体にDPIに関する設定をかける方法。MSDNでは

  • SetProcessDPIAware関数(Vista~Win8.0)
  • SetProcessDPIAwareness関数(Win8.1~Win10 1607以前)
  • SetProcessDpiAwarenessContext関数(Win10 1607以降)

によりできる、と書いてあるので、該当するフラグをつかって設定すれば良さそうなのですが、私がデバッグした限りにおいてこれらがうまく設定できた試しがない、というのが悲しいところでした。書いてある注意点としては「ウィンドウが作成される前にこの関数を使って設定するように」と書いてあるはずなのですが、やってみてもすべて「Access Denied」(すでにDPIのモードが設定されているため設定不可能)ではねられて終わりでした。なぜなんだよ~と思いながら書いています。

というわけで仕方がないので…。Win8.1以前では対応策がこれ以上ないようですが、Win10以降であればスレッド単位、もしくはウィンドウ単位でDPIAwarenessが設定できるのでそちらを使います。スレッド単位の場合はCreateWindowEx関数などウィンドウ作成系の関数の前に呼び出せばモード変更が可能なようです。使う関数は

  • GetThreadDpiAwarenessContext関数
  • SetThreadDpiAwarenessContext関数

になります。一応Get側で取得して予定しているモード値と異なっているときSet側で設定する、という動きになります。Win10以外のOSにも対応する場合はuser32.dllにインポートされているのでたいていの場合GetModuleHandle関数からGetProcAddress関数で取得することになります。

面倒なのはこれらで使うDPI_AWARENESS_CONTEXTという値です。

というのは、WindowsSDKのバージョンの関係で自分で定義する場合が多いこととこれら2つの関数が返すDPI_AWARENESS_CONTEXTの値はMSDNで定義されている各値と同じものを返さない場合がある、ということです。前者についてはDPI_AWARENESS_CONTEXTは列挙子ではなくハンドル扱い、と書かれていますのでUINT_PTR型などとしないとGetProcAddress関数で処理を持ってきたときに見事な失敗となりますし、後者はマニフェストや(この後紹介する)実行ファイルのの互換性設定でDPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2相当の値を設定してあるからといってGetThreadDpiAwarenessContext関数はMSDNでDPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2として定義されている値である(-4)ではなく別の値を返すパターンがあるということです。これが判定にはやっかいすぎる現象だと思います。設定する場合の値はMSDNの値が正しいようですが…。

なお、WindowModePatchでは前者はちゃんとハンドルとして定義する構文を借りて値を再定義していますし、後者についてはGetAwarenessFromDpiAwarenessContext関数を一度通してDPI_AWARENESS列挙子にすることでモードを判定するようにしています。この辺も実装に手間取った理由だったりします。

 

あとは必要に応じてDPI変更時の処理を組み込むだけ

DPIを変更する必要がある場合はWM_DPICHANGEDメッセージがウィンドウプロシージャに飛んできますのでこれを見てウィンドウの大きさや文字の大きさなどを制御します。このあたりは今回の考察範囲からは外れますので詳しく書きません。

 

使用者側から見るのであれば実行ファイルのプロパティから設定可能

Win10系であればこれが可能です。Win8.1系でもできるかな?やり方としては対象アプリケーションの実行ファイルのプロパティを開き「互換性」タブから「高DPI設定の変更」を選択して誰がスケーリングをするのか設定が可能です。ちなみにこれを使うと単にウィンドウモード時の解像度が低いだけなら無理矢理解像度を引き上げることが可能だったりします。メニューバーまで拡大されてしまうのはご愛敬といったところですか。WindowModePatchのような自由度はないですが、OSの機能ですのでうまく使えるのであれば損はないかな~と思ったりもしています。

 

ゲームの場合は直接解像度を見るので気にしない方がよい機能かな

と思います。といっても、名前の入力などでダイアログを出す必要がある場合は対応する必要もあるかもしれません、とだけ付け加えておきましょう。

開発環境をVisualStudio2017へ移行してみた

最新のVS2019ではないのが悲しいですが…。MovieLayerPlayerやらWindowModePatchを再度構築できる環境を整えるためにVisualStudio2017でビルドができるようにいろいろと設定やらファイルの文字コードやらを変更していってみるといろいろと大変なことがわかってしまって「ぎゃ~~~」と言いながら作業していました…。

 

(VS2010から見たら)だいぶ変わってしまったVS2017でした

結局ビルド環境を整えなおすのに丸2日かかってしまいました。しかもとりあえずビルドに成功したのはMovieLayerPlayerの方でWindowModePatchはまだテストしていない状態です。まあ、こちらを先にやったのはほかのライブラリのビルドなどを考えた結果なのですが…。

 

ディレクトリの設定はVS2010と変わらず

この辺はこちらの記事を参考にできます。自分で書いて自分で忘れているのが悲しいですね…。特にDirectXや自分で作ったライブラリ・インクルードファイルを参照する場合はこの設定がないとちょっと厳しいので気を付けましょう。DirectXのSDKはDirectX9系だと良くてもJune2010なのでインストールしたくないため仮想マシンで一度展開して必要なものだけコピーしていますので、そのためにもディレクトリの設定は設定ファイルからやらないとまずい、ということもあったりします。

 

ソースコードの文字コード設定には気を付けて

今回文字コードをShiftJIS(CodePage932)からUNICODE系へと変更しています。面倒なのが、

  • ソースコード(cpp,hなど)はBOM付きUTF-8へ
  • リソースファイル(rc)はUTF-16へ

にしないとまずいということ。ソースコードにBOMを付け忘れると勝手にShiftJISと認識されて大量のエラーが出てきますので気を付けて。

またリソースファイル(rc)はVS2017の移行+文字コード変更いろいろな変更があり特に、

  • インクルードしているヘッダをafxres.hからwinres.hに変更すること
  • 文字コード設定(#pragma codepage)を削除して文字コードをUTF-16と認識させる

という作業が必要になります。なぜかrcファイルではBOM付きUTF-8は認められない様子なので気を付けてください。

 

ライブラリを構築するときの依存性設定がさらに厄介に

VS2010では依存性を持たせるだけでは自動的にリンクがされず、[共通プロパティ]=>[Frameworkと参照]で依存するプロジェクトを参照として追加する必要がある、とは書いたのですが、これがVS2017になると、ソリューションエクスプローラから対象のプロジェクトを開くとソースファイルやヘッダファイルがあるところに「参照」となぜかこちらに参照設定が来ていますのでこれで設定しましょう。こちらに参照設定をすることでライブラリがリンクされるようになります。

そしてどうも設定ミスなのかバグなのかわからない現象にはまってしまったのが、「依存しているライブラリをリンクしてくっつける場合、依存先(つまり子)のライブラリの構築時にプリコンパイル済みヘッダが設定されているとそのライブラリを使うプログラムを構築するときにLNK2019エラーが発生してリンクに失敗する」という現象でした。つまい、複数のライブラリをまとめて一つのライブラリとして扱う場合、親以外のライブラリにプリコンパイル済みヘッダを設定しているとプログラムを作るときに失敗する、という現象になります。何か設定で回避できるのかどうかよくわかりませんが、今のところ単純な回避策は「プリコンパイルを使用しないように設定してビルドする」以外思いつかないのが現状だったり。解決法を知っている人は教えてください…。

 

bind2ndやらmem_funやらが非推奨に

なっています。正しく言うならbind2ndやmem_funがfunctionalに移ったため、そちらをインクルードしないとコンパイルに失敗する、というものですが、どうせこの機会なのでbind2ndはbindとplaceholdersの組み合わせに、mem_funはmem_fnを使ったパターンに切り替えてしまいました。bind1stやbind2ndとかの限定版よりbindの方が使いやすいのでそちらに切り替えてしまいましょう。

 

WindowsXP以前に必要としていたルーチンはもう必要ないよね…?

ということで、いくつか片づけました。特にGetVersionExを使っていた部分はDeprecatedとなっていますのでVersionHelperのIsWindows系関数でバージョンを判定するように変更するとか、cpuidに関するルーチンを再構築したりとか、x86系のコンパイルでSSE2はもう当たり前だと思うので一部のプロジェクトで設定するとかしています。これで早くなるかどうかはよくわかりませんが…。

 

いくつか最新機能も取り入れてみた

私の場合はWindowModePatchなどの関係もあり、解像度の設定で解像度を文字列で表す、というのをやっているので4Kが3840×2160になる、とかWindows10に関するバージョン検知なども少し書き直しています。次回のWindowModePatchのアップデートはまずはVisualStudio2017でのコンパイル版にすることと、解像度を増やすことと、あとはスケーリング処理に関する部分を入れてみることでしょうか。といってもどれだけ時間が取れるのかは未定ですが。

 

しばらくWindowModePatchの機能向上に取り組む予定

この手の情報は基本的にTwitterに書くのですが、今回はblog側にも書いておこうかと思います。

 

やはりDirectDraw周りのエミュレーションがあまい

特にDirectDraw+Direct3Dの組み合わせになるとかなり微妙なようです。DirectDrawをそのまま使うタイプの解像度処理だと色深度によって問題が出ることもあるらしく・・・というところで、Direct3D9を使ったエミュレーションだとその辺が完全に実装できていないのでエラーを返される場合も多々あるようです。

 

ということでその辺の処理を追加実装する予定に

一応どう実装するかは決めているので後はコツコツとエミュレーションを書いていくだけです。例えばDirect3DをDirect3D9を使ってエミュレーションするクラスの出だしは

 

class CDirect3DOnDirect3D9 : public CUnknownBase, public CDirectDraw7OnDirect3D9Holder, public IDirect3D, public IDirect3D2, public IDirect3D3{

 

となります。なんじゃ、こりゃ?といいたくなるプログラムコードです。しかし本当にこういうコードを内部的に書いています。特にひどいのがIDirect3DとIDirect3D2とIDirect3D3を同時に実装するというところ。もう少し各クラスに互換性があれば上位だけを実装して強制型キャストですむのですが、この3つにはその手の関係が微妙にないのでこんな実装をするしかないようです。ちなみにIDirect3D7だとIDirect3D3以前とはまた違った実装をしているのでこれは単独で組んでいます。おそらく残りの実装も面倒なのでしょうね。まあ、暇な時間を見つけて作っていきたいと思います。

SwiftでC言語互換のポインタ操作が面倒

というかそんなことをしなければ良いのかも知れませんが画素などを直接操作する場合には必要になってしまうこともないわけではないのでいろいろとやってみました。ちなみに、普通であればそんな操作をする場合であればC言語やObjective-Cにコードを持って行ってそこで処理した結果をSwiftに拾い上げた方が良いと思います。まあ、それは置いておいて、どんなコートが互換コードとなるのか見てみるとその性質が見えてくるのではないでしょうか。

 

SwiftでC言語互換のポインタを扱う

まずはこれから。この辺はまだSwiftの言語仕様などを解説している本やページを見れば書いてあると思いますがちょっとだけ解説。関数ポインタやクラスのポインタでは扱いが微妙に異なりますが通常こんなものが必要なのは生のデータを扱う時だと思いますのでそれを念頭に。

まずポインタの種類から。Swiftではポインタを扱うことができませんから何らかの方法でポインタを保持する必要があります。そのためにとある型が用意されています。それがUnsafePointer<T>、UnsafeMutablePointer<T>、AutoreleasingUnsafeMutablePointer<T>の三つになります。それぞれ、以下のような役割となります。

C言語/Objective-Cの型 Swiftの型
const T * UnsafePointer<T>
T * UnsafeMutablePointer<T>
T ** AutoreleasingUnsafeMutablePointer<T>

Tの部分は使用する型に置き換えて下さい。ちなみに、C言語/Objective-Cではvoidと書くvoid型ですが、SwiftではVoidとVが大文字となり、NULL(nil)ポインタはSwiftではすべてnilで扱われます。また、クラスオブジェクトを扱わないのであれば上の2つだけでデータ操作は何とかなると思います。AutoreleaseingUnsafeMutablePointerはどちらかというとObjective-Cのオブジェクトを保持しておくためにあるのでC言語レベルの処理では使わないはずです。

 

ポインタのキャスト

ポインタなのでキャストできないとどうしようもありません。が、Swift上では独立した型なのでそのままコピーは一切できません。C言語と同じようにキャスト構文を書く必要があります。たとえば、C言語上で

unsigned char *buf;
int *array;
・・・
array = (int *)buf

と書きたい場面があるとします。このとき、型キャストを書くと

var buf: UnsafeMutablePointer<UInt8>
var array: UnsafeMutablePointer<Int32>
・・・
array = UnsafeMutablePointer<Int32>(buf)

となります。このあたりはまだ理解しやすいのではないかと思います。微妙に見えるのは型キャストのたびに変換する型のオブジェクトを擬似的に作成するところでしょうか。ちなみに、unsigned charは8bitの符号無し整数と考えてUInt8に、intは32bit符号あり整数と考えてInt32と型を限定してつけているのにも注意です。変数のサイズも考えて書かないと互換とは言い切れなくなるので。

 

UnsafePointerやUnsafeMutablePointerのポインタを参照する

ポインタを参照する時の手順です。単純にポインタとしてアクセスする場合もありますし、ポインタを配列のように扱ってインデックスをつけて処理する場合もあります。この場合はどうするのかというと・・・。いろいろとパターンがあるのでいくつかのパターンで見ていきます。基本的にはUnsafePointer<T>やUnsafeMutablePointer<T>が持つプロパティであるmemoryを通します。が、このmemoryの使い勝手が微妙に面倒。特に構造体で各メンバ単体での取得ができなかったり、インデックス参照ができなかったり、と。

で、例ですが、一応構造体の例もあるので構造体を一つ宣言してそれについてのアクセスも見ていきます。意味が無い処理も多少ありますがサンプルと言うことで。

typedef struct clr32{ unsigned char b,g,r,a; } CLR32;

const CLR32 *p1; const int *p2; CLR32 *p3; int *p4; unsigned char gray;
・・・
// 以下p1,p2,p3,p4には有効なアドレスが入っているものとする(メモリ領域は十分にあるものとする)
// 読み込み+計算処理
gray = (unsinged char)((int)p1->r * 306 + (int)p1->g * 601 + (int)p1->b * 116) >> 10);

// 書き込み処理
p3->b = p3->g = p3->r = gray; p3->a = p1->a; *p4 = *p2;

// 読み込み+書き込み(インデックス、ポインタ演算)
p3[1] = *(p1 + 1); p4[1] = *(p2 + 1);

という処理があったとします。なんかグレースケール変換の処理が混じっていますがまあ気にせず。これをSwiftで再現すると・・・

internal struct CLR32{
    var b: UInt8 = 0
    var g: UInt8 = 0
    var r: UInt8 = 0
    var a: UInt8 = 0
}
var p1: UnsafePointer<CLR32>
var p2: UnsafePointer<Int32>
var p3: UnsafeMutablePointer<CLR32>
var p4: UnsafeMutablePointer<Int32>

・・・
// 以下p1,p2,p3,p4には有効なアドレスが入っているものとする(メモリ領域は十分にあるものとする)

// 読み込み+計算処理
let p1mem = p1.memory
let gray = UInt8((Int(p1mem.r) * 306 + Int(p1mem.g) * 601 + Int(p1mem.b) * 116) >> 10)
let clr = CLR32(b: gray, g: gray, r: gray, a: p1mem.a)

// 書き込み処理
p3.memory = clr
p4.memory = p2.memory

// 読み込み+書き込み(インデックス、ポインタ演算)
// 書き方はポインタ演算の書き方のみ可能
p3.advancedBy(1).memory = p1.advancedBy(1).memory
p4.advancedBy(1).memory = p2.advancedBy(1).memory

どうですか。面倒になったでしょう。memoryを通して扱うのですが、特に構造体などで注意ですが構造体のプロパティ(C言語などではメンバ)を通すと書き込み特性が失われてしまうので読み込む場合はそのまま参照してもどうと言うことはないのですが、構造体のデータを書き込むときは書き込む前にデータを集めて一気に書き込まないとうまくいきません。また、インデックス付きの場合はadvancedByでインデックス移動したポインタオブジェクトを新たに作成してその場所のmemoryを参照する、というやり方が必要になります。

 

UnsafePointerやUnsafeMutablePointerのポインタ位置を動かす

C言語などではインクリメントなどで次の要素を簡単に指せるのですが、Swift上で「再現する」となると意外と面倒です。一つにはインデックスを別変数にして参照する、という方法があり、これは上に紹介したやり方が使えますね。では本当に再現するとどんなコードになるのか、というと。まずC言語上で

int *p;

//ポインタのインクリメント
p++;

//ポインタを二つ動かす
p += 2

となるコードですが、(ここまでの説明を見た人なら何となく分かると思いますが)これをSwiftで書くと

var p: UnsafeMutablePointer<Int32>

//ポインタのインクリメント
p = p.advancedBy(1)

//ポインタを二つ動かす
p = p.advancedBy(2)

とadvancedByを用いた形式に早変わりです。移動量はオブジェクトの分だけ動くのでsizeof等の考え方は必要ないのがありがたいですか。関数の戻り値を代入しているように見えますが、一枚後ろではオブジェクトを生成してそれを保持するようにするコードなのでポインタを動かすたびにオブジェクトを生成しているように見えて嫌な感じもする人もいるかも。

 

UnsafeMutablePointerでのメモリの確保および解放

もちろん「C言語互換のポインタ」と言っているだけあってメモリの確保および解放は普通にできます。UnsafePointerはconstポインタなのでメモリの確保や解放とは無縁でしょうから無視して。C言語でメモリを確保する場合はだいたい

int *array;

//メモリの確保
array = (int *)calloc(100,sizeof(int)); //もしくはmalloc(100 * sizeof(int))
・・・
//メモリの解放
free(array);

と書きますが、これをSwiftに持って行くと

var array: UnsafeMutablePointer<Int32>

//メモリの確保
array = UnsafeMutablePointer<Int32>.alloc(100)
・・・
//メモリの解放
array.dealloc(100);

となります。メモリの解放時に解放するデータサイズを指定する必要があるのでメモリを確保した本体は同じポインタを保持した方がわかりやすいと思います。

で、私がとあることを忘れてテスト段階でアプリを落としまくった要注意点が一つ。

UnsafeMutablePointerはC言語のポインタをSwiftで扱うためのものなので(UnsafeMutablePointer自身はARCの管理下にあるが)UnsafeMutablePointer内部のポインタにARCなんて便利な構造はなく、メモリ管理はプログラマが行わなければならない。つまり確保したメモリはUnsafeMutablePointerがオブジェクトとして解放されるタイミングになってもdeallocは呼ばれないので自分で呼び出さないとメモリリークを起こすことに注意しよう

はじめはUnsafeMutablePointerのスコープが切れた段階で自動解放されるものだと思っていたので対応するコードを書かなかったのですが・・・。書かないとだめですので解放のポイントを間違えないようにしましょう。

 

無理矢理書かない方がいいのかも

と思わないでもない今日この頃。無理に書くくらいならC言語で書いたコードを連結した方がはるかにわかりやすいですね。メモリの確保と解放だけをSwift上で行うことになるはずなのでその場合はメモリの管理を忘れずに。

なお、本来ならSwiftのような言語では変数宣言は実際にその変数を使うまで遅らせて宣言と同時に初期化するのが正しい作法です。が、今回はC言語との対比を見るためにそのような書き方をせずに書いていますのであしからず。

CPUIDの処理もいろいろと変わったのね

まあ、知ってはいましたが。

VisualStudioのバージョンを2010に変更したことでCPUIDを参照するための組み込み命令も拡張されましたのでそれの対応処理をいろいろと書いていました。そのための資料も一新したのでちょっと書いてみたいと思います。

 

まずは基礎。CPUIDとは

で、何も知らない人のためにCPUIDの説明から。CPUIDとはx86系のCPUが持つ命令の一つで、CPUをある状態にして実行することで対象のCPUが持つ様々な情報を得ることができる命令です。特に昔(10年ほど前)だとADVシステムを作成するときにCPUの拡張命令であるMMXやSSE2を持っているかどうかという情報がゲームの実行速度(特に描画関係)に大きな影響を与えるために拡張命令が使用できるかどうかを調べて命令を切り替える、という作業が必要であったり、サポートのために実行しているPCの実行環境を出すために必要な命令として使用していたものです。

x86と書いているのでIntelのCPUでもAMDのCPUでも実装していますし、x86-64環境でも動作する命令です。もちろんこれはCPU上で直接実行する命令ですので組み込み命令を記述できるプログラミング言語でしか直接の実行はできません。CPUIDの情報を受け取るような別の処理を持っているプログラム言語もあるかもしれませんがそれはまあそれということで。

今日ではほとんど話題にもならなくなってしまった命令でもあります。そこまで拡張命令にこだわるとすればCPUを使った科学技術計算や動画のエンコードなどCPUの処理を限界まで引き出すときに対象の命令が有効かどうかを調べたりCPU-Zなどで自分のCPUを調べるときにCPU-Z上で実行される、という形でしか見ることは無いと思います。

ちなみにこの命令は実行しているCPUがIntel製かAMD製か、などを直接判別するために使われる命令でもあります。そのため、CPUIDの結果はCPUの種類ごとにかなり異なったものとなる場合分けがとんでもなく必要な命令だったりします。CPUID命令実行時にどのような処理結果となるかを直接知りたい人は各社が公開している資料を見てみるといいと思います。Intelの場合は「Intel 64 and IA-32 Architectures Software Developer’s Manual 2A」に、AMDの場合は「AMD64 Architecture Programmer’s Manual Volume 3 General-Purpose and System Instructions」に記述があります。

 

命令のサポートフラグがいろんな場所に・・・

本当にいろんなところにフラグがあるんです・・・。CPUが実行可能な命令を取得するためにCPUIDを実行するわけですが、この実行手順が異様なほど面倒。今だとAVX2が実行可能であるかを調べる手順として考えられますが、どんな感じか見てみましょう。ここ数年で発売されているx86命令をサポートするCPUでは実装がほぼ当たり前となっているようなx86-64の動作フラグなどもこんな方法でチェックされます。なお、この手順では「CPUID命令は実行可能である」という条件を入れてあります。昔はこの条件がないことがありテストコードも書いていたものですが、今更そんなものもいらないでしょう。というわけで以下が手順。基本的にCPUIDで入出力するのは32bit値なのでそれに従います。

なお、もしインラインアセンブラなどで命令を記述する場合、使うレジスタはeax,ebx,ecx,edxの4つです。VisualStudioの組み込み命令の場合、第一引数に32bit変数x4の配列(命令が終了したとき、配列にはそれぞれeax,ebx,ecx,edxの値が返る)、第二引数にeaxに与える命令の初期値、__cpuidexの場合は第三引数にecxに与える命令の初期値、となります。

  1. eax=0としてcpuidを実行。eaxに基本情報として取得できるcpuidの最大値、ebx,edx,ecxの順番にCPUの制作元文字列(ASCII)が返る。
  2. 基本情報に1以上が含まれない場合はこれで終了。
  3. eax=0x80000000としてcpuidを実行。eaxに拡張情報として取得できるcpuidの最大値が返る。なお、eaxに(符号なしとして)0x80000000を超える値が返らないときは拡張情報は無し。
  4. eax=1としてcpuidを実行。eaxにCPUのバージョン情報、edxに拡張命令フラグその1、ecxに拡張命令フラグその4が返る。
  5. 拡張情報がない場合はこれで終了
  6. eax=0x80000001としてcpuidを実行。edxに拡張命令フラグその2、ecxに拡張命令フラグその3が返る
  7. eax=0x80000002~0x80000004まではCPUの名前情報が返る。
  8. 基本情報に7以上が含まれない場合はこれで終了。
  9. eax=7、ecx=0としてcpuidを実行。eaxにeax=7としたときにecxに与えることができる最大値、ebxに拡張命令フラグその5が返る。

とまあ、こんな感じになります。ちょっとおもしろいのが拡張命令フラグの順番ですよね。なぜこうなってしまったか?はフラグが何を指しているかを見ればわかります。

フラグ種類 拡張命令フラグとしてよく確認される機能
 拡張命令フラグその1  TSC(TimeStampCounter),MMX,SSE,SSE2,SMT(HyperThreading)
 拡張命令フラグその2   3DNow!,3DNow! Extensions, AMD64(IntelのCPUではIntel64)
 拡張命令フラグその3  SSE4A,FMA4
 拡張命令フラグその4  SSE3,SSSE3,SSE4.1,SSE4.2,AESNI,AVX
 拡張命令フラグその5  AVX2

見てわかるとおり、その2とその3はAMDが先行して取り入れた機能に関するフラグになっています。そのため、Intel側の資料を見るとフラグの状態が微妙になってしまっています。このあたりはCPUの開発競争によるものなのでこういうこともある、というわけですね。フラグの順序をこのようにしたわけもこれです。

ちなみに、今後増えるとしたら拡張命令フラグその5と同じような取得の方法の場所に増えていくのだと思われます。そのためにわざわざ枝番なんてものを使って取得するようになっているのですからね。

 

ちなみにコードにすると

こんな感じになります。

#include <intrin.h>

#define CPUCMDSET_TSC		0x00000001
#define CPUCMDSET_MMX		0x00000002
#define CPUCMDSET_SSE		0x00000004
#define CPUCMDSET_SSE2		0x00000008
#define CPUCMDSET_3DNOW		0x00000010
#define CPUCMDSET_3DNOWEXT	0x00000020
#define CPUCMDSET_SMT		0x00000040
#define CPUCMDSET_64BIT		0x00000080
#define CPUCMDSET_SSE3		0x00000100
#define CPUCMDSET_SSSE3		0x00000200
#define CPUCMDSET_SSE4A		0x00001000
#define CPUCMDSET_FMA4		0x00002000
#define CPUCMDSET_SSE41		0x00004000
#define CPUCMDSET_SSE42		0x00008000
#define CPUCMDSET_AESNI		0x00010000
#define CPUCMDSET_AVX		0x00040000
#define CPUCMDSET_AVX2		0x00080000

unsigned int cpuidcheck(char vendor[16],char name[64])
{
	int cpuinfo[4]; int *ptr; unsigned int flag; int basicmax,extendmax,structuredmax; int i;

	//初期化
	flag = 0x00000000; basicmax = extendmax = structuredmax = 0;

	//基本命令領域長およびCPUVendorの取得
	__cpuid(cpuinfo,0x00000000);
	basicmax = cpuinfo[0]; ptr = (int *)(&vendor[0]);
	*ptr++ = cpuinfo[1]; *ptr++ = cpuinfo[3]; *ptr++ = cpuinfo[2]; *ptr = 0;
	
	//拡張情報領域長の取得
	__cpuid(cpuinfo,0x80000000);
	extendmax = cpuinfo[0];

	//拡張命令フラグその1
	__cpuid(cpuinfo,0x00000001);
	if(cpuinfo[3] & (1 << 4)) flag |= CPUCMDSET_TSC;
	if(cpuinfo[3] & (1 << 23)) flag |= CPUCMDSET_MMX;
	if(cpuinfo[3] & (1 << 25)) flag |= CPUCMDSET_SSE;
	if(cpuinfo[3] & (1 << 26)) flag |= CPUCMDSET_SSE2;
	if(cpuinfo[3] & (1 << 28)) flag |= CPUCMDSET_SMT;

	//拡張命令フラグその4
	if(cpuinfo[2] & (1 << 0)) flag |= CPUCMDSET_SSE3;
	if(cpuinfo[2] & (1 << 9)) flag |= CPUCMDSET_SSSE3;
	if(cpuinfo[2] & (1 << 19)) flag |= CPUCMDSET_SSE41;
	if(cpuinfo[2] & (1 << 20)) flag |= CPUCMDSET_SSE42;
	if(cpuinfo[2] & (1 << 25)) flag |= CPUCMDSET_AESNI;
	if(cpuinfo[2] & (1 << 28)) flag |= CPUCMDSET_AVX;

	//拡張領域がない場合は以降を行わない
	if((unsigned int)extendmax < (unsigned int)0x800000000) return flag;

	//拡張命令フラグその2
	__cpuid(cpuinfo,0x80000001);
	if(cpuinfo[3] & (1 << 31)) flag |= CPUCMDSET_3DNOW;
	if(cpuinfo[3] & (1 << 30)) flag |= CPUCMDSET_3DNOWEXT;
	if(cpuinfo[3] & (1 << 29)) flag |= CPUCMDSET_64BIT;

	//拡張命令フラグその3
	if(cpuinfo[2] & (1 << 6)) flag |= CPUCMDSET_SSE4A;
	if(cpuinfo[2] & (1 << 16)) flag |= CPUCMDSET_FMA4;
	
	//CPUNameの取得
	ptr = (int *)(&name[0]);
	__cpuid(cpuinfo,0x80000002); for(i = 0;i < 4;i++){ *ptr++ = cpuinfo[i]; }
	__cpuid(cpuinfo,0x80000003); for(i = 0;i < 4;i++){ *ptr++ = cpuinfo[i]; }
	__cpuid(cpuinfo,0x80000004); for(i = 0;i < 4;i++){ *ptr++ = cpuinfo[i]; }

	//基本領域がない場合は以降を行わない
	if(basicmax < 7) return flag;

	//拡張命令フラグその5
	__cpuidex(cpuinfo,7,0);
	structuredmax = cpuinfo[0];
	if(cpuinfo[1] & (1 << 5)) flag |= CPUCMDSET_AVX2;

	return flag;
}

最後の__cpuidexの組み込み命令がVS2005にはなかったので実装できなかったのですが、今ならばできるわけですね。

 

でも今だとCPUIDを使ってそこまで確認することの意味って薄いよね

組み替えを行っていて思ったことだったりします。一応組んではいますが、ADVゲームレベルでこれだけ調べてもあまり意味はありませんし、SSE2が使えれば十分、という速度まで処理を引き上げないとどうかと思いますし。