GPD WIN 4 を買わない理由をつぶした話
通勤中に電車の中でちょっとプログラムを試したりするためのUMPC をずっと求めていたのですが先日Amazon で値引きされていたのでGPD WIN 4 (16GB/1TB)を購入しました。
高額の買い物になるので、ネットでよく見かける 購入しない理由が金額なら買え、購入する理由が金額なら買うな の言葉に従い、値段以外の懸念事項について検討したのを書きとめます。
メモリ16GB/SSD 1TB は少々物足りない
- 現在家で使用しているPC と比較してみる
画面が 6 インチ
- Nintendo Switch よりやや小さい
- Switch で読みやすい文字の大きさを実際に測ってみると3 ミリ くらい
- 6 インチ 16 : 9 の縦は約 7.5 センチくらいなので最大25 行くらい
- 現状スマホでコードを読んでいるのに比べれば良い
GPD WIN Max 2 とどちらにするか
- 通勤中以外も使うなら画面の大きさとキーボードの2点でMax 2 のほうがよさそう
- 通勤中に使う場合はWIN 4 のほうが邪魔にならない
- 家で使う場合は画面、キーボードは外付けできる
まもなくより高スペックのGPD WIN 4 2023 が出る
- 性能が約25% 向上している
- 現バージョンでも今使用しているPC よりベンチマークが上、かつ現PC で困っていない
- クラウドファンディングならかなりお安く手に入る
- トラブル時の対応に不安があるので日本の代理店を通したほうが良い
- 入手まで最短3 か月くらいかかる
- 3 か月待てるほど自分に時間の余裕があるのか
結局悩んでる時間があるならさっさと購入してどんどん使ったほうがいいだろうというのが決め手となりました。タイムイズマネー。
購入後ほどなくGPD WIN Mini が発表されてこっちだったかも……とちょっと思いました。
Direct3D 12 でのシェーダーの動的リンク
解決したこと
Direct3D 12 のシェーダーで関連要素の組み合わせ(法線マップの有無、PBR パラメータの有無など) ごとのシェーダーを生成する方法としてコンパイル済みシェーダーをアプリケーション実行時にリンクする方法について調べました。
動機
いろいろな入力データに対応する場合にコンパイル済みシェーダーバイナリの数が爆発的に増えてしまう問題への対策として動的リンクが使えるのではないかと考えました。
たとえばピクセルシェーダーで法線を扱う場合、頂点ごとの法線から計算する方法と、法線マップから取得する方法があります。また物理ベースレンダリングのラフネスやメタルネス(メタリック) などのパラメータを、テクスチャがあればテクスチャから、なければ定数バッファから取得するといった場合分けも考えられます。
いろいろな入力データに対応したい場合、その方法もいくつかあります。
通常のプログラムであれば要素ごとにif 文などで分岐します。hlsl でも条件分岐は可能ですが通常は使用しません。シェーダープログラムでの条件分岐はパフォーマンスが悪く、また分岐を判定するための値を定数バッファなどで渡す手間もあります。
よく使用されているのは、要素ごとの入力をプリプロセッサの#if ~ #else ~ #endif 命令によって選択するなどして入力方法の組み合わせごとのシェーダーバイナリをコンパイルして生成する方法です。ドローコールの時に入力データに合わせたシェーダーを選択して実行するためシェーダープログラムでは条件分岐が不要となります。
この方法の問題として、シェーダーで扱う要素の組み合わせが指数オーダーで増えてしまいます。例えば分岐が10 ある場合の組み合わせは2 ^ 10 = 1024 通りです。シェーダープログラムを少し修正すると組み合わせ分のコンパイルを待たなければいけなかったり、多くのシェーダーバイナリファイルを管理しなければならなくなります。
動的リンクを導入することで、組み合わせる処理ごとに最低限のシェーダーバイナリファイルを用意し、実際に必要となった組み合わせのシェーダーバイナリだけを実行時に生成することでコンパイルの待ち時間とファイル数が抑えられると期待できます。
動的リンクについて
この記事では動的リンクという言葉を使用していますが、厳密な使い方ではありません。事前にシェーダーコードをコンパイルしておき、実行時にリンクして実行可能なシェーダーバイナリにすることを動的リンクと呼んでいます。
マイクロソフトのhlsl のドキュメントを見ると動的リンクを行う方法が二つ見つかります。一つはシェーダーリンク、もう一つはダイナミックリンクです。
シェーダーリンクはシェーダーをコンパイルして関数のライブラリを作成し、実行時に関数を組み合わせて実行可能なシェーダーとする方法です。
ダイナミックリンクはC++ のクラスの仮想関数のような仕組みで、呼び出される関数を動的に切り替える方法です。
それぞれの使用方法についてはリンク先のマイクロソフトのドキュメントを順番に読み進めていくのをおすすめし、ここでは細かい説明は省きます。
ダイナミックリンク
まず、ダイナミックリンクについて触れます。
// 法線を得る方法を提供するインターフェースを定義 interface iNormal { float3 GetNormal(float3 normal, float2 uv); }; // 頂点からの法線を選択するクラス class NormalFromVertex : iNormal { float3 GetNormal(float3 normal, float2 uv) { return normal; } }; // 法線マップからの法線を選択するクラス class NormalFromTexture : iNormal { float3 GetNormal(float3 normal, float2 uv) { return g_normalMap.Sample(g_sampler, uv).xyz; } } // インターフェースインスタンスを宣言 iNormal g_abstractNormal; // クラスインスタンスの宣言 cbuffer instances : register(b0) { NormalFromVertex g_normalFromVertex; NormalFromTexture g_normalFromTexture; }; // ピクセルシェーダー float4 PSMain(PS_INPUT input) : SV_TARGET { // コード省略 // g_abstractNormal にはg_normalFromVertex かg_normalFromTexture を // C++ コード側で割り当てる float3 normal = g_abstractNormal.GetNormal(input.normal, input.uv); // コード省略 }
ピクセルシェーダーのPSMain() のコードはほぼ省略していますが、機能を切り替えたい個所だけを差し替える形となっています。この仕組みはダイナミックリンクを使用しない通常の場合の延長線上にあり分かりやすいのではないかと思います。
しかしDirect3D 12 での使用方法で解決できない点がありました。
ダイナミックリンクではインターフェースインスタンスにクラスインスタンスを割り当てますが、そのためにID3D11ClassLinkage, ID3D11ClassInstance を使用します。C++ コード上で、まずID3D11ClassLinkage のインスタンスを作成しID3D11Device::CreatePixelShader() の引数として渡しておきます。それからID3D11ClassInstance インスタンスの配列を用意して実際に使用するクラスインスタンスを指定し、ID3D11DeviceContext::PSSetShader() にシェーダーとともに渡します(それぞれピクセルシェーダーの場合)。
Direct3D 12 ではパイプラインステートオブジェクトの作成時にコンパイル済みのシェーダーバイナリを渡す形で、ID3D11ClassLinkage, ID3D11ClassInstance をセットする方法が不明でした。
シェーダーリンク
前置きが長くなりましたが、今回採用したシェーダーリンクについて説明します。
と言っても、使用方法についてはシェーダーリンクの使用 のページに順を追って説明されており、そちらを読むだけで分かる人には分かるのではないかと思います。ここではそちらのページに明記されていない細かなポイントについて解説します。
シェーダーリンクの考え方
シェーダーリンクの中心となる仕組みとして、関数リンクグラフ(Function Linking Graph, FLG) があります。関数リンクグラフは、シェーダーライブラリに含まれる関数の引数と戻り値についてそれぞれどこから受け取ってどこに渡すのかを指定するものです。
個人的に関数リンクグラフの理解に時間がかかったので、それについて説明します。
動的リンクを用いて処理を切り替えるというのを、最初はC++ の仮想関数やダイナミックリンクのイメージでとらえていました。
// 法線の取得方法を切り替える例 floa4 PSMain(PS_INPUT input) : SV_TARGET { // コード省略 float3 normal = /* ここで呼び出す関数を差し替える */; // コード省略 }
一方で、関数リンクグラフで出来ることは関数の戻り値または入力ノードを、別の関数の引数もしくは出力ノードに渡すことだけです。PSMain 関数の中から法線を取得する関数にuv を渡したり戻り値を関数内の変数に代入する方法が不明でした。
関数リンクグラフを使用する場合には考え方を変える必要があり、ノードベースのシェーダーをイメージします。以下はライティング計算で使用する法線を頂点から取得する場合と法線マップから取得する場合の関数リンクグラフの図です。
頂点の法線を使用する場合
法線マップを使用する場合
図の青い囲みは入力ノード・出力ノードで、赤い囲みはシェーダーライブラリに含まれる関数を表しています。ライティング計算関数内で必要に応じて関数を呼び出して値を取得するのではなく、先に関数を呼び出して値を取得しておいてライティング計算関数の引数として渡す形です。
このように、シェーダーリンクでは動的に切り替えたい要素は関数の引数・戻り値として扱うような構成にします。
シェーダーライブラリのhlsl
シェーダーリンクで使用するhlsl で、通常とは違う点について説明します。
まず、シェーダーリンクで使用する関数にはexport キーワードをつけておきます。
export float3 GetNormalFromTexture(float2 uv) { return g_texture.Sample(g_sampler, uv).xyz; }
また、エントリーポイントとなる関数は必要ありません。関数リンクグラフを構築するときに入力シグネチャ、出力シグネチャを直接指定します。
シェーダーライブラリのコンパイル
hlsl をシェーダーライブラリとしてコンパイルする方法はシェーダーライブラリのパッケージ化 のページに記載があり、サンプルコードではD3DCompile の第7 引数・pTarget が"lib" + m_shaderModelSuffix
となっていて、m_shaderModelSuffix の内容が不明です。ライブラリとしてコンパイルする場合に指定するシェーダーモデルはlib_5_0, lib_6_0 のようになり、頂点シェーダーのvs_5_0, ピクセルシェーダーのps_5_0 などと同様の形式となります。
シェーダーリンクのC++ 側コード例
/** * @brief シェーダーライブラリからピクセルシェーダーをリンクして返す関数 * @param[in] module D3DLoadModule() でロードされたライブラリモジュール * @param[in] moduleInstance module->CreateInstance() で作成し、BindResource(), BindSampler(), BindConstantBuffer() でリソースを割り当てたライブラリのインスタンス * @param[in] useNormalMap true:法線無し、法線マップあり false:法線あり、法線マップ無し * @return リンクされたシェーダーバイナリ */ ID3DBlog * CreatePixelShader(ID3D11Module * module, ID3D11ModuleInstance * moduleInstance, bool useNormalMap) { // 関数リンクグラフを作成 ComPtr<ID3D11FunctionLinkingGraph> flg; D3DCreateFunctionLinkingGraph(0, &flg); // 入力ノードを作成 ComPtr<ID3D11LinkingNode> inputNode; // 法線マップを使用する場合は頂点に法線が含まれない想定 if (useNormalMap) { D3D11_PARAMETER_DESC paramDescs[2] { { "inPos", "SV_POSITION", D3D_SVT_FLOAT, D3D_SVC_VECTOR, 1, 4, D3D_INTERPOLATION_UNDEFINED, D3D_PF_IN, 0, 0, 0, 0 }, { "inUv", "TEXCOORD", D3D_SVT_FLOAT, D3D_SVC_VECTOR, 1, 2, D3D_INTERPOLATION_UNDEFINED, D3D_PF_IN, 0, 0, 0, 0 }, }; flg->SetInputSignature(paramDescs, 2, &inputNode); } else { D3D11_PARAMETER_DESC paramDescs[2] { { "inPos", "SV_POSITION", D3D_SVT_FLOAT, D3D_SVC_VECTOR, 1, 4, D3D_INTERPOLATION_UNDEFINED, D3D_PF_IN, 0, 0, 0, 0 }, { "inNormal", "NORMAL", D3D_SVT_FLOAT, D3D_SVC_VECTOR, 1, 3, D3D_INTERPOLATION_UNDEFINED, D3D_PF_IN, 0, 0, 0, 0 }, { "inUv", "TEXCOORD", D3D_SVT_FLOAT, D3D_SVC_VECTOR, 1, 2, D3D_INTERPOLATION_UNDEFINED, D3D_PF_IN, 0, 0, 0, 0 }, }; flg->SetInputSignature(paramDescs, 3, &inputNode); } // 法線マップから法線を取得する関数ノード ComPtr<ID3D11LinkingNode> normalFunc; if (useNormalMap) { // 法線マップがある場合のみ、関数のノードを作成 flg->CallFunction("", module, "GetNormalFromTexture", &normalFunc); // 入力ノードの[1] をGetNormalFromTexture() の引数に渡す flg->PassValue(inputNode.Get(), 1, normalFunc.Get(), 0); } // ライティング関数ノード ComPtr<ID3D11LinkingNode> lightingFunc; // ライティング関数のシグネチャは // float4 PSMain(float4 pos, float3 normal, float2 uv) : SV_TARGET flg->CallFunction("", module, "LightingMain", &lightingFunc); if (useNormalMap) { // 法線マップがある場合はPSMain() の引数normal に法線マップ関数の戻り値を渡す flg->PassValue(inputNode.Get(), 0, lightingFunc.Get(), 0); flg->PassValue(normalFunc.Get(), D3D_RETURN_PARAMETER_INDEX, lightingFunc.Get(), 1); flg->PassValue(inputNode.Get(), 1, lightingFunc.Get(), 2); } else { // 法線マップがない場合は入力ノードをそのままPSMain() の引数に渡す flg->PassValue(inputNode.Get(), 0, lightingFunc.Get(), 0); flg->PassValue(inputNode.Get(), 1, lightingFunc.Get(), 1); flg->PassValue(inputNode.Get(), 2, lightingFunc.Get(), 2); } // 出力ノードを作成 ComPtr<ID3D11LinkingNode> outputNode; D3D11_PARAMETER_DESC outParamDescs[1] { { "outTarget", "SV_TARGET", D3D_SVT_FLOAT, D3D_SVC_VECTOR, 1, 4, D3D_INTERPOLATION_UNDEFINED, D3D_PF_OUT, 0, 0, 0, 0 }, }; flg->SetOutputSignature(outParamDescs, 1, &outputNode); // ライティング関数の戻り値を出力ノードに渡す flg->PassValue(lightingFunc.Get(), D3D_RETURN_PARAMETER_INDEX, outputNode.Get(), 0); // シェーダーをリンク ComPtr<ID3D11Linker> linker; D3DCreateLinker(&linker); linker->UseLibrary(moduleInstance); ComPtr<ID3D11ModuleInstance> flgInstance; flg->CreateModuleInstance(&flgInstance, nullptr); ID3DBlob * shaderBlob; linker->Link(flgInstance.Get(), "PSMain", "ps_5_0", 0, &shaderBlob, nullptr); return shaderBlob; }
上記コードの戻り値となっているシェーダーバイナリ(ID3DBlob インスタンス) は、パイプラインステートオブジェクトを初期化するときの設定構造体D3D12_GRAPHICS_PIPELINE_STATE_DESC のPS に設定して使用します。
注意点として、ID3D11FunctionLinkingGraph のCallFunction() は、シェーダー内での実行順に呼び出す必要があります。実行順はある関数の戻り値を別の関数の引数として渡す場合に、戻り値を提供する側の関数が先になります。呼び出し順に問題がある場合PassValue() が失敗します。ID3D11FunctionLinkingGraph::ID3D11FunctionLinkingGraph() でエラーメッセージを取得すると、error X9010: ID3D11FunctionLinkingGraph::PassValueWithSwizzle: source node must preceed destination node in FLG
となっています。
複数の戻り値を扱いたい場合
関数から複数の要素を返したい場合にはreturn による戻り値ではなく、関数の引数にキーワードout をつけます。
void TransformPosition(in float4 inPos, out float4 outPos, out float4 outWorldPos) { outWorldPos = mul(inPos, g_modelMtx); outPos = mul(outWorldPos, g_viewProjMtx); }
関数リンクグラフのPassValue() では入力側ノードのパラメータ番号としてD3D_RETURN_PARAMETER_INDEX ではなく引数のインデックスを渡します。
flg->PassValue(posFunc.Get(), 1, outputNode.Get(), 0); flg->PassValue(posFunc.Get(), 2, outputNode.Get(), 1);
hlsl の関数の引数についてはこちらのページ で説明されています。
最後に
Direct3D 12 でのシェーダーリンクによるプログラム実行時のシェーダーの動的なリンクの説明は以上です。
いろいろとググったり試行錯誤した結果のプログラムは公式のシェーダーリンクの説明 を順番に読み進めた通りになったのでやっぱり公式ドキュメント大事だなというところですが、関数のノードを接続していく使い方は気付かないと詰まってしまうポイントではないかと思います。
同じところで詰まっている人の役に立てば幸いです。
CRC32 のテーブルを利用した最適化のメモ
解決したこと
CRC32 を計算するプログラムをテーブルを利用して最適化する場合の個人的な引っ掛かりポイントについて再確認しました。
- 元々の手順とテーブル利用の関連付け
- データテーブルの計算方法
環境
- Windows 10
- Visual Studio 2019
解決方法?
CRC32 そのものについての説明はこちらのスライド を参考にしました。
まずはスライドの説明に沿って実際にビット演算を手計算しました。
例えば入力データが文字'a' の場合
- 'a' = 0x61 = 01100001 =[ビット順序反転]=> 10000110
- 後ろに0 を32 bit 付加して1000011000000000000000000000000000000000
- 先頭32 bit を反転して0111100111111111111111111111111100000000
- 多項式は100000100110000010001110110110111
- シフトとxor の繰り返し
input :0111100111111111111111111111111100000000 polynomial: 100000100110000010001110110110111 xor result:0011100011001111101110001001001011000000 polynomial: 100000100110000010001110110110111 xor result:0001100001010111100110110010010000100000 polynomial: 100000100110000010001110110110111 xor result:0000100000011011100010101111111101010000 polynomial: 100000100110000010001110110110111 xor result:0000000000111101100000100001001011101000 入力データのMSB が32 bit より下位になったので終了しビットを反転 xor result:--------00111101100000100001001011101000 =>:--------11000010011111011110110100010111
- 結果のビット順序を反転した値の16 進数は0xe8b7be43
- python のcrc32 と同じ結果になることを確認する
ひっかかりポイント
CRC32 の説明などでしばしば「ビット順序の反転」が出てきますが、ここがよく理解できていなかったのがポイントでした。
C++ で任意の長さのデータを取り扱う場合は主に1 byte の基本型であるchar やuint8_t の配列として取り扱います。例えば入力データが"abcd" という長さ4 の文字列の場合、これをC++ 的に素直に表現すると
'a', 'b', 'c', 'd' = 0x61, 0x62, 0x63, 0x64 => 0110 0001 0110 0010 0110 0011 0110 0100
となります。これは、1 byte ごとに2 進数にして並べたものです。
ここで1 byte 中のビットの順序について考えてみます。'a' でいうと上位←0110 0001→下位
ですが、上位ビットと下位ビットはどちらが先なのか? というとリトルエンディアンでは下位ビットが先、ビッグエンディアンでは上位ビットが先、となります。これについては構造体のビットフィールドで確認することができます。
struct Data { union { uint8_t bit : 1; uint8_t value; }; }; Data d; std::memset(&d, 0, sizeof(Data)); d.bit = 1;
とした場合、d.value の値はリトルエンディアンだと0x01, ビッグエンディアンだと0x80 となります。
これを踏まえて改めて入力データ"abcd" をビットの入力順に2 進数表現する場合、リトルエンディアンであれば各1 byte については下位ビットから並べることになります。
'a', 'b', 'c', 'd' = 0x61, 0x62, 0x63, 0x64 => 1000 0110 0100 0110 1100 0110 0010 0110
C++ プログラム中で入力データのbyte 配列に手を加えなくても、modulo 2 の計算に対する入力としてビット列の先頭を最上位ビットとして扱うことで、ビット順序が反転したように見えます。実際にはプログラム・メモリ上の上位ビット方向とmodulo 2 計算上の上位ビット方向が逆であるということです。
プログラム実装時に使用する最上位ビットを省略した多項式は00000100110000010001110110110111 ですが、このときの上位ビット方向はmodulo 2 計算上のものであり、C++ プログラム上でのバイト列としての表現は0010 0000 1000 0011 1011 1000 1110 1101 => 0x20, 0x83, 0xb8, 0xed
となります。リトルエンディアンでの32 bit 値とする場合には先に来るバイトが下位になるので0xedb88320 です。
以上の、プログラム・メモリ上のビット順序とmodulo 2 計算時のビット順序の対応 について理解することで計算処理の各操作が何をしているのかもよくわかるようになりました。
データテーブルの計算方法
CRC32 計算の最適化用に作成するデータテーブルの内容は、8 bit のパターンごとに後続の32 bit に適用される多項式のxor を先に計算しておいたものです。modulo 2 で上位から下位に向けて1 のビットを見つけてxor を行う操作で、プログラム上では逆に下位ビットから処理することになります。
uint32_t table[256]; // x^32 を省略した多項式 const uint32_t polynomial = 0xedb88320; // テーブル各要素を計算 for (uint32_t i = 0; i < 256; ++i) { // xor 初期値 uint32_t xor = 0; // 入力データ uint32_t input = i; // 8 bit を処理 for (uint32_t j = 0; j < 8; ++j) { // ビットがたっていたら if (input & (1 << j)) { // 入力データにxor 適用 // ビットがたっているのは多項式のx^32 に対応する位置 // xor をとるのはその次からなのでi + 1 でシフトする input ^= (polynomial << (i + 1)); // 後続データに対してのxor // i = 0 の時には後続32 bit の先頭から見て // polynomial はmodulo 2 の計算上で7 bit 上位になる // プログラム上では下位に7 bit はみ出していることになる xor ^= (polynomial >> (7 - i)); } } // 結果をテーブルに収める table[i] = xor; }
その他
github にコードをコミットしました。
Visual C++ でのweak symbol
解決したこと
他のコンパイラでは __attribute__((weak))
などと指定するweak symbol をVisual C++ で使用する方法について調べました。
環境
- Windows 10
- Visual Studio 2019
解決方法
weak symbol はリンク時に他に同じ名前のシンボルが見つからなければリンクされるシンボルです。
通常はリンク時に同じ名前のシンボルが複数ある場合にはエラーとなります。
Visual C++ の場合、 /alternatename:a=b
というリンカオプションを使用して、a というシンボルの定義が見つからない場合には
b というシンボルをリンクするという方法で同様のことができます。
msvc waek symbol
で1年以内に更新された条件でググったら出てきた What does the /ALTERNATENAME linker switch do? を参考にしました。
サンプルコード
/* * C リンケージで宣言 */ /// 呼び出す関数名 extern "C" int func(); /// デフォルト実装 extern "C" int _default_func(); /* * リンカーへの指定 */ #if defined(_M_IX86) // x86 は_ が付加される #pragma comment(linker, "/alternatename:_func=__default_func") #elif defined(_M_IA64) || defined(_M_AMD64) // x64 は そのままの名前 #pragma comment(linker, "/alternatename:func=_default_func") #endif /* * C++ リンケージで宣言 */ /// 呼び出す関数名 extern int func2(); /// デフォルト実装 extern int _default_func2(); // Visual Studio 2019 では以下の指定でリンクできた // リンクエラーの時に出力されたエラーメッセージを参考に指定 #if defined(_M_IX86) #pragma comment(linker, "/alternatename:?func2@@YAHXZ=?_default_func2@@YAHXZ") #elif defined(_M_IA64) || defined(_M_AMD64) #pragma comment(linker, "/alternatename:?func2@@YAHXZ=?_default_func2@@YAHXZ") #endif // 1:デフォルト実装が呼び出される // 0:独自実装が呼び出される #define USE_DEFAULT 1 /// メイン関数 int main() { return func() + func2(); } #if !USE_DEFAULT /// C リンケージの独自実装 int func() { return 0; } /// C++ リンケージの独自実装 int func2() { return 0; } #endif /// C リンケージのデフォルト実装 int _default_func() { return 1; } /// C++ リンケージのデフォルト実装 int _default_func2() { return 2; }
/alternatename に指定するのはリンク時のシンボル名なのでソースコード中の関数名そのままではなく、 x86 のC リンケージの場合は名前の前に_ が付加され、C++ リンケージの場合は名前マングリングが適用されています。
その他
github にサンプルをコミットしました。
CMake でWindows SDK バージョンを指定する方法
解決したこと
CMake で生成したvcxproj のWindows SDK バージョンが10.0.18362.0 になっていたのを10.0.19041.0 になるようにしました。
環境
- Windows 10 (バージョン 1903, ビルド 18362.959)
- Visual Studio 2019
- CMake 3.18
解決方法
CMAKE_SYSTEM_VERSION のページに説明がありますが、新規のビルドツリーを作成するときに指定することができます。 out-of-source ビルド の場合にはコマンドプロンプトから
> mkdir build > cd build > cmake .. -G"Visual Studio 16 2019" -DCMAKE_SYSTEM_VERSION="10.0.19041.0"
と実行することで生成されるvcxproj のWindows SDK バージョンが指定の値になります。
-G"Visual Studio 16 2019" は生成するプロジェクトファイルの対象を指定しています。複数のバージョンのVisual Studio がインストールされている場合に指定します。
CMAKE_SYSTEM_VERSION の指定はコマンドライン上で行う必要があります。CMakeLists.txt でset で指定しても反映されません。
すでにcmake を実行してビルドツリーが作成済みの場合、一度build ディレクトリ内をすべて削除してから実行しなおす必要があります。
その他
インストールされているWindows SDK の中にホスト環境と一致するものがない場合、インストールされている中で一番新しいバージョンが選択されるようです。
参考リンク
C++ のダウンキャストについて考える
今どきのゲーム開発環境を実現しようとするとどうしてもC++ でダウンキャストが必要になってきます。
C++ でダウンキャストを行うにはdynamic_cast を使用するのが通常の手段ですがなかなか重い処理なのでゲームではあまり使いたくない選択肢です。
static_cast でもダウンキャストが可能です。例えばclass B がclass A を継承している場合A からB へのstatic_cast でダウンキャストが可能です。
class A {}; class B : public A {}; class C; B b; A * a = &b; B * pb = static_cast<B *>(a); // OK. 継承関係がある C * pc = static_cast<C *>(a); // NG. 継承関係がない
dynamic_cast とstatic_cast の違いは、dynamic_cast では実行時型情報を利用してダウンキャストの正当性をチェックするところです。
class A { public: virtual ~A() = default; } class B : public A{}; class C : public A{}; B b; A * a = &b; B * pb = dynamic_cast<B *>(a); // 正しくキャストされる C * pc = dynamic_cast<C *>(a); // nullptr を返す pb = static_cast<B *>(a); // 正しくキャストされる pc = static_cast<C *>(a); // 不正なポインタとなる
dynamic_cast ほどの汎用性は不要という条件であれば、独自に継承関係を解決してやればstatic_cast でそれなりに安全なキャストが可能です。
例えばstatic_cast ではvirtual 継承を使用している場合にはダウンキャストはできませんがそういう使い方はしない、など。
(virtual 継承でなくてもダイヤモンド継承してたらキャストはうまくいかないのでした。)
必要な機能は
- クラスに静的変数として型の識別子を持たせる
- インスタンスに型の識別子を持たせる
- 型の識別子を比較してキャストの正当性を判定する
といったところでしょうか。
これらを実装するときにC++ の機能だけだとわりと定型的な処理を各クラスごとに記述する場面にぶつかります(と予想します)。
こういうのを毎回手書きするのは面倒&エラーのもとになるのでこの辺りの解決策がポイントになりそうです。
解決策は考え中。