2026年6月5日金曜日

ゆるやかに学ぶC言語を読んで

Siv3Dを開発されている鈴木先生が書かれた「ゆるやかに学ぶC言語」をご恵贈いただきました。

 

未だにK&Rが教科書で使われているようなところもあると聞きますが,こちらの書籍はC23という最新の規格を元に解説されています.

一通り読ませていただき,これは初めてのプログラミング言語としてC言語を選んだりする場合には是非選んでいただきたい教科書だな,と感じました.また,教える側にも参考となる内容になっていると思います.


まず,ところどころに新しく出てくる単語には読み方が書いています.例えば,#include <stdio.h>はハッシュ・インクルード・エスティーディーアイオー・ドットエイチ,というように書いています.stdio.hはstudioと間違えやすいのですが,読み方が分かれば間違いも減るのではないでしょうか.

また,多くの学生に教えてきたことでの気付きなのか,つまずくポイントへの注意が随所にちりばめられています.人間,理解してしまうと理解できなかった頃のことを忘れてしまったりするので,こういうつまずきポイントは,教える側にも参考になる情報です.

この本の良いところの一つとして,このように多くのコードに実行結果が添えられていることがあります.

何か間違っていれば,すぐに気付くことができます.

他にも,単なる言語の入門書にとどまらないポイントとして,プログラムを書く上での考え方にも触れているところがとても良いと思います.
例えば,早期returnを使うことで,それ以降は条件が正しい前提で素直に書けるようになる,といったことが書いていたりします.

最後の方では,初心者から中級者へ,一歩進むためのポイントも書かれています.
画像処理の部分は,頑張れば今流行りのレイトレーシングにつなげることも出来るので,非常に面白い題材だと思います.

学校の授業でC言語が分からないという人は,是非こちらを読んでみてください.


最後に,鈴木先生のC++書籍が出るのも楽しみにしています.

2023年3月31日金曜日

証明作法を読んで

今月は共立出版から出版された証明作法を読みました.

著者曰く,証明問題を計算や式変形でなんとかしようとする理由を考えていたところ,そもそも証明の書き方を学んでいないのではないか,というところからこの本を書くに至ったそうです.

また,著者はプログラミング作法の訳者が指導教員だったらしく,書名もそこから取ったそうで,プログラマにとってもちょっと興味が惹かれる本です.
証明をプログラムのようにインデントを使って書いているのも特徴です.

証明というのは,ルールに従って仮定から証明したい命題に辿り着くまでの構造を作る,という点では確かにプログラムに似ていて,色々な例も書いてあるので,ある程度論理学などに触れたことがあれば読みやすい内容だと思います.

ただ,非常に抽象的です.証明の構成方法の解説のほとんどがφやχなど記号だけを使って説明しているので,抽象的に考えることに慣れていない人には難しいかもしれません.

最近Udemyで命題論理に関する講義を受けてみたのですが,そちらは逆に具体的な例を大量に使っていました.それはそれで長すぎる気もしたので,個人的には抽象的な方が好きです.

この本を横に置きながら,他の数学書の証明問題を読んでみるのも良いかもしれないですね.

2023年2月15日水曜日

プログラマーのためのCPU入門を読んで

 プログラマーのためのCPU入門を読みました.

この書籍は,ゲームの土台となるシステムを作っているプログラマーには是非とも読んでもらいたい内容となっています.書いてある内容は10年以上ゲーム業界にいると聞いたことがあったりする内容が多いです.つまり,この本を読むことで何年もかけて学んだような知識を得ることが出来るわけです.

昨今は,ゲーム業界でも専門化が進み,新卒で入社してくる社員には物理学だったり数学だったりの専門家ではあるけれど,情報の教育は受けていない,というような人もいたりします.そういった方たちに高速に動くコードを書いてもらうための助けにもなることでしょう.


第1章では,CPUが命令を順次処理していくことに触れられています.この辺は基本的な知識として是非知っておいてもらいたいレベルですね.


そして,第2章ではパイプラインやスーパースカラなどによって並列実行をすることで高速に実行できるようにすることを解説しています.まずここが分かっていないと,例えばif文は遅いといった話をしても通じないことでしょう.

実際,最近パイプラインという言葉さえも通じないことがあったのでこの書籍を紹介しました.


第3章はデータ依存関係で,例えばある計算の結果を次の計算で使う,というような依存関係がある場合にパイプラインが上手く詰まらず効率が下がるということが解説されています.そのため,実行順序を変更したりして解消することを解説しています.

また,学生時代だと計算量などの話をする場合にはどの処理も一律で扱っていることもあったでしょうが,処理によっては必要な時間が違う,といった話も出ています.これをレイテンシといって,計測するためのサンプルも用意されています.


第4章では,if文のような分岐命令について触れています.分岐は遅い,という話を聞いたことがあるかもしれませんが,これもパイプラインに関係していて,この章を読めば何故遅いのかが理解できることでしょう.


第5章では,キャッシュについて触れています.正直なところ,キャッシュまで考慮して高速化するようなことは,よっぽど中心部分を実装しているような人でないと必要が無いかもしれません.昨今のようにゲームエンジンを利用する場合であればなおさらです.でも,プロファイラなどを見る場合にキャッシュミスが多いというのはどういう意味なのか,といったことを理解するためにも,こういった知識は必要でしょう.

レンダリングプログラマはキャッシュがどうのとしょっちゅう言っている気がします.


第6章では,仮想記憶,特にアドレス変換周りについて触れています.内容としては若干キャッシュの話と近い部分もありつつ,そもそも仮想記憶が当たり前すぎて仮想記憶とは何か分かってない,という方も増えてきているので読んで損はないでしょう.


第7章では,I/O (input / output)について触れます.例えばマウスとかキーボードとかですね.ゲームだとコントローラなんかも含まれます.I/Oは遅いというのはよく聞くんですが,省電力とかコストの関係で意図的に遅くしている,という話は面白かったです.

昔のゲーム専用機のコントローラは今より速かったらしく,ゲームを移植したら反応が遅いってなって,原因を調べると,そもそもコントローラが遅くなっていた,なんてこともありました.


第8章は,システムコールなどOSの機能に関係する部分です.この辺まで考慮するのは本当に根本の部分を担当する人たちだと思いますが,例えばファイルオープンは結構時間がかかるので,複数のデータをまとめてファイルオープンを減らすとか,システムコールが必要な同期の仕組みを減らして速度を上げるといったことは実際に行われているので,何故こんなことをしているのだろう,と思ったときのヒントになることでしょう.


第9章はマルチプロセッサについてです.ここまでくると,ソフトウェアのそもそもの設計なんかが複数のプロセッサを生かせるようになっていないと難しい,というような話になってきます.ゲームなんかは,元々複数の要素を並列で動かして高速化するといったことをしてきているので,この辺は既にフレームワークとして設計に組み込まれているかもしれません.


第10章は,キャッシュコヒーレンス制御です.これは,複数のプロセッサが別々に持っているキャッシュの内容が一致しているようにする,という話です.確実に一致する状態を保とうとすると処理速度が落ちます.実際,その設定をしていたせいで遊べたものではない状態になったことがあります.そう,設定を一か所変えるだけで劇的に改善されたんです.

かといって,場合によってはプロセッサによっては違う値を取ってくるというのは,それはそれでよく分からない問題を引き起こしたりします.

実際にそういったことが起きないように対処するのはシステムの根本を作ってくれている人たちが対応してくれると思いますが,そういったことが起こるってことくらいは知っていて損は無いと思います.


第11章は,メモリオーダーとかメモリ順序付けと言われるものを解説しています.これはこれでやっかいな話で,コード上は順番に書いているのに,実際には違う順序で実行されたりアクセスされた結果,条件が成立したのに成立していないとか謎な状態になったりします.今時のゲームは並列処理が基本でこういった問題もちょくちょく発生しているので,是非とも押さえておいた方が良いでしょう.


第12章は,アトミックとか不可分操作と言われるものを解説しています.CASとかロックフリーとか聞いたことがあるかもしれません.例えば,キューを並列でアクセスすると簡単にクラッシュします.そういったことを防ぐためにも必要な知識なので,是非押さえておきたいところです.


第13章はまとめとして高速なソフトウェアを書く際に注目する点を解説しています.そのあとには,様々な参考文献もまとまっています.


多くの章には簡単な実験で速度の低下や効率化が確認できるコードが付いています.ただし,Linuxを前提としているので,そういった環境を用意できない方はご注意ください.


最後まで書いて,新人への課題図書にしても良いんじゃないかなと思いました.

2023年1月4日水曜日

継続的デリバリーのソフトウェア工学の感想

新年あけましておめでとうございます.
今年の抱負として,月に一冊は技術書の感想を書いてみようかな,と思い,長いこと更新していなかったブログを更新することにしました.

今回読んだのはこちら.
継続的デリバリーのソフトウェア工学(日経BOOKPLUS)

継続的デリバリーの,となっていますが,読んだ限りでは元のタイトルのModern Software Engineeringを実現する方法として継続的デリバリーも推している,という感じなのかな,という気がします.

序文では,この書籍は次のようなものだと述べています.

ソフトウェア工学に工学を取り戻す試み

つまり,現在のソフトウェア工学は工学になっていない,ということです.正確には,皆が誤解しているというところなのだと思います.実際,この書籍で紹介される多くの概念は過去に提唱されてきたものがほとんどです.

そのための流れとして,まず科学と工学の違い,そしてソフトウェア工学とはどうあるべきなのかが定義されます.そして,工学であるために必要となる学習と複雑さの管理 について説明し,最後にそれらのためのツールというかアイデアを紹介しています.

この書籍が良いところは,既に様々な書籍で紹介されているアイデアを整理し,それらがどう関係しあって良い結果をもたらすのか,ということが具体例も交えつつ解説されている点です.

例えば,テスト駆動開発(Test Driven Development, TDD)を用いると,疎結合な設計になりやすいであるとか,関心事の分離により変更が容易になるとかです.

TDDや継続的インテグレーション(Continuous Integration, CI),継続的デリバリー(Coninuous Delivery, CD)などは,良い設計をする方向への圧力となるのだと.

また,プログラマはどうしても個々人のレベルアップによる開発を夢見がちですが,それでは大規模化が困難であることにも触れています.ある程度の規模のソフトウェア開発に関わっている人ほど,その点については何となくでも気付いていると思います.

TDDとなると,どうしてもテストを書くのが面倒だと言う人は多いのではないでしょうか? しかし,この書籍を読めば,何故TDDが必要なのか,単純に単体テストではダメなのか,という質問をされた場合に返答できるようになると思います.

雑に要約すると,TDDでのテストは仕様を記述しているのであって,バグを検出することが第一の目的ではない,ということです.

他にも色々とありますが,この書籍ではTDDやCI,CD,他にはマイクロサービスやDevOpsなどによってもたらされる利点が語られていますが,そのものについては既知のものとして詳しく触れてはいません.

1つか2つソフトウェアの開発経験を積んだ後,まずはこの書籍を読み,知らない概念についてはそれぞれの参考文献を読む,という読み方をするのが良いと思います.

2020年7月20日月曜日

DirectX12の魔導書を読んで

DirectX12の魔導書は,翔泳社より出版されている,国内唯一のDirectX12に関する書籍です.なお,同人であればすらりんラボさんやノースブレインさんが出している書籍があります.

書評を書くにあたって,軽く自分の経歴を書いておくと,とあるゲームでリードプログラマを務めたり,ゲームエンジンに携わったりしています.

誤解しないで欲しいので最初に言っておきますが,私はこの本は良い本だと思っています.

誰のための書籍か

ゲームエンジンで独自のシェーダも扱える昨今,ゲームを作ったり表現の研究をしたりしたいのであれば,DirectX12を自分で扱う必要はありません.

ゲームエンジンを作る側になるにしても,描画周りの専門家になりたいというのでなければ,DirectX12を知る必要はありません.

描画のみを扱っているので,ゲームを作れるようになるかというと,なりません.ゲームを作れるようになるには,他にも乗り越える壁があります.

だったら読まなくても良いや,と思った人には向かない書籍です.

ぶっちゃけてしまうと,本書は多くの人には不要な書籍です.しかし,知識というのは,いつどこでどう役立つか分からないものです.自分が直接扱うわけではなくとも,描画周りの専門家と話をするときに役立つことだってあるでしょう.

また,手を動かす前提の書籍なので,キャラクタアニメーションだったり,ポストプロセスなどが,理論では良く分からないから,実際に実装して理解を深めたい,という方にはオススメです.

著者の川野先生は専門学校の先生で,学生が少しでも分かりやすいように,と色々と工夫されているようで,本書もなるべく理解しやすいように,ちょっとずつコードを追加していける形でサンプルが作られています.誰かに描画周りについて説明する場合の参考にもなるでしょう.

まとめると,すぐに役立つ知識が欲しい人には向きません.将来を見据え,何でも学ぶ姿勢の人にこそ合う本です.

この本の利点

最大の利点は,やはりサンプルです.理解を深めるために良い例題やサンプルを用意できるか,というのが専門書の重要なポイントだと思います.

同じ内容を扱った色々な専門書があるのも,説明の仕方や,例題を変えることで,少しでも理解が深まる人が増えれば,と考えてのことだと思います.

本書のサンプルは,徐々に機能を足していき,ある程度足し終えれば,ちょっと書き換えるだけで,次の例を実装できるようになっており,手を動かしていると楽しくなってきます.

キャラクタが表示できるとちょっと楽しくなり,それを増やせるとさらに楽しくなり,さらにアニメーションが付くと,さらに楽しくなります.

人によっては,ポストエフェクトなどの方が楽しいかもしれません.

あまりコードを分けすぎず,コードを把握するのも多少は楽な作りになっているので,細かく分かれすぎていて,クラスの関係が良く分からない,といったこともありません.

また,ImGUIやEffekseerといったミドルウェアとの連携についての情報はあまり無いので,それだけでもお金を出す価値はある気がします.

この本の欠点

利点を述べた以上,欠点も述べておくべきでしょう.

あくまでも公式ドキュメントなどを元に著者が調査した結果であるため,一部の用語が間違っていたり,勘違いを含んでいたりします.

また,論文などを参照しているわけでもないので,理論面で正確ではない部分や,著者も良く分かっていないと書いてある場合もあります.

ただ,それらは本書のターゲットがそういった厳密さを求めている人というわけではないので,気になるのであれば自分で調べれば良いでしょう.

また,コードの書き方についても若干癖があるので,そのあたりは,本書を読めば,GitHubのあちこちにあるDirectX12のサンプルコードを読み解く力が多少はついていることでしょうから,これが正解と思うのではなく,他のコードも色々と読むのが良いと思います.

手を動かす場合の注意点

まず,この書籍を読み進めながら手を動かすには,入門書レベルのC++は理解していて,外部ライブラリを利用する方法も知っているか,調べて解決できて,エラーが出た場合もある程度は自力で調べたりデバッガを利用して解決できる能力が必要です.

Visual Studio 2017 15.5以降,標準に準拠する,というオプションがデフォルトでオンになっており,サンプルコードの一部がビルドエラーになります.

自力でエラーを修正するか,プロジェクトのプロパティから,C/C++を選び,言語から準拠モードをいいえに切り替えましょう.

書籍の説明はところどころに抜けがあったりして,以前に設定したパラメータを切り替えておかないと上手く動かない部分があったりします.WinMergeなどのテキスト比較ツールを利用して,サンプルコードの前の章のコードと比較して差分を確認するなどしながら進めましょう.

また,9章ではリファクタリングと言いつつ,大幅な書き換えもあるので,その辺で一度復習しておくのも手です.なお,その関係で9章と8章の差分を取るのは困難というか不可能に近いです.

正誤表やIssue,Twitterなどを活用しよう

文章の間違いを見つけた場合,翔泳社の書籍紹介ページから正誤表に飛ぶことが出来るので,正誤表を確認してみましょう.載っていなければ,気軽に間違いを送ってみましょう.本当に間違いであれば,ちゃんと正誤表に載せてもらえます.

また,DirectX12のようにハードウェアに近い処理を扱う場合,利用しているハードウェアやドライバの影響で動かないことなどもあるでしょう.
そういうときは,サンプルコードがGitHubに上がっているので,気軽にIssueとして投げてみましょう.
著者の川野先生は忙しくて対応できないようですが,それを見た他の人が何かしらアクションしてくれるかもしれません.

また,Twitterなどで#DirectX12の魔導書 のタグを付けてつぶやくなどしていると,バグなどがあった場合に,著者であったり経験者の方がサポートしてくれたりします.

最後に

他のレビューでもあるように,この書籍には誤字脱字や説明の間違いなどが多々ありますが,世の中の専門書というものを見てみると,当然のように正誤表があり,色々と間違っていたり,版を重ねた際に誤植するなどもあるので,そのあたりは気にしない方が良いでしょう.
その点だけを以て,この書籍が良くないと断じているのであれば,世の中の多くの専門書が良くないことになります.

そもそも,最近はUnreal Engine 4やUnityなど,ゲームエンジンに関する書籍は多く出ているものの,こういった根元の部分を扱う書籍は長らく出ていませんでした.

勉強したくても,古い情報を元にWeb上で色々と調べて,断片的な情報をつなぎ合わせて,情報を更新していくしかありませんでした.

ちょうどそういった,根元の部分を扱うような書籍が出ると良いな,という話をしていた頃にこの書籍が出版され,情報や前例が少ない中で出版までこぎ着けてくださった川野先生や翔泳社の方には感謝しかありません.

この後に続く,XAudio2などのサウンドを扱った書籍や,DirectInputやXInputなど入力周りを扱った書籍など,自分で根元から作ってみたい人向けの書籍が増えることを期待します.

2020年6月25日木曜日

STLのtype_traitsを実装する その2

STLのtype_traitsを実装する その1の続き.

conjunctionを上手く実装できないか試行錯誤していて,たぶんこれでOKというのが出来たので書いておきます.

conditionalおよびconditional_tの実装

conditionalは,型のif文だと思えば良いでしょう.boolと型Tと型Fを渡すと,boolがtrueのときはtypeとしてTを,falseのときはtypeとしてFが手に入るわけです.

template <bool, class T, class F>
struct conditional
{
    using type = F;
};

template <class T, class F>
struct conditional<true, T, F>
{
    using type = T;
};

template <bool B, class T, class F>
using conditional_t = typename conditional<B, T, F>;

boolの値がtrueの場合を特殊化していますが,デフォルトはtypeがTになって,falseの場合はFになる,と逆にしても問題はありません.

conjunctionおよびconjunction_vの実装


conjunctionは,is_XXX系の型が特定の性質を持っているかを判定するtype traitsに対する論理積になります.通常の論理積と同じく,短絡評価が可能です.

つまり,is_integral_v<float> && is_floating_point_v<float>と書くと,最初のfloatが整数かどうかの判定でfalseになるので,全体としてfalseが決定するので,is_floating_point_v<float>は参照しない,というようなものです.

ただし,ここで問題なのが,&&でtype traitsの結果の論理積を取ってしまうと,参照はしないけれど,テンプレートの実体化は行われている,ということです.

例えば,has_valueというtype traitsがあって,与えられた型TがT::valueというような名前を持つかチェックする場合,has_value<int>が実体化されるとコンパイルエラーになるわけです.しかし,conjunctionを使うと,そういった問題は発生しません.

conjunction<is_floating_point<int>, int>;

最初の時点でfalseになるので,それ以降はテンプレートの実体化が行われず,2つ目のintはint::valueをチェックする,という処理が行われないわけです.

あとは,少しややこしいのが,親となるクラスを決定する方法です.

  • conjunction<>と実引数が0の場合,true_typeを継承.
  • conjunction<T0, T1, ..., Tn>の場合,Ti::valueがfalseなら,Tiを継承
  • conjunction<T0, T1, ..., Tn>の場合,全てのTi::valueがtrueなら,Tn(つまり最後の型)を継承

となるわけです.

というわけで,おそらく次のようになります.


// 実引数が0の場合は,ここで終わる
template <class... B>
struct conjunction : true_type {};

// 実引数が1の場合は,ここで終わる
template <class H>
struct conjunction<H> : conditional_t<static_cast<bool>(H::value), H, H>
{};

// それ以外
template <class H, class... T>
struct conjunction<H, T...> : conditional_t<static_cast<bool>(H::value), conjunction<T...>, H>
{};

template <class... B>
inline constexpr bool conjunction_v = conjunction<B...>::value;

最初の定義は,実引数が0の場合に適用されます.1つの場合,2つ目の定義で::valueを持っているかチェックした上でその型を利用します.
trueでもfalseでも,最後の1つならその型を使うことに変わりはないので,これで問題ないはずです.

3つ目の定義は,2つ以上ある場合で,trueなら残りをチェックし,falseならその型を継承するようにしています.

名前空間myに定義していたとして,次のようなテストをしています.

my::conjunction<my::is_integral<int>> c0;
my::is_integral<int> * pc0 = &c0;

my::conjunction<is_integral<int>, my::is_floating_point<int>> c1;
my::is_floating_point<int> * pc1 = &c1;

my::conjunction<is_integral<int>, my::is_floating_point<int>, int> c2;
my::is_floating_point<int> * pc2 = &c2;

// my::conjunction_v<int>; // コンパイルエラー(int::valueができないため)

static_assert(my::conjunction_v<>);
static_assert(my::conjunction_v<std::is_integral<int>>);
static_assert(my::conjunction_v<std::is_integral<int>, std::is_floating_point<float>>);
static_assert(!my::conjunction_v<std::is_integral<int>, std::is_floating_point<int>>);
// 短絡評価されるので,intを指定していても問題無し
static_assert(!my::conjunction_v<std::is_integral<float>, int>);

clangの実装でもMSVCの実装でも,condtionalを使っていなかったので,もしかしたら何か抜けがあるのかもしれませんが,今のところ上手く動作していそうです.

こういうパターンが網羅できないよ,というのがあれば指摘いただけると助かります.

2020年6月23日火曜日

STLのtype_traitsを実装する その1

C++の標準ライブラリにはtype_traitsという,型に対して様々な判定をしたり,変換をしたりするテンプレートが用意されています.こういうテンプレートをtype traitsと言います.

これを独自に実装するというのは車輪の再発明になりますが,例えばテンプレートで配列の場合のみ特殊化したい場合,どう書けば良いのか,などテンプレートの書き方について色々と勉強になります.

なお,ここではC++17以降を想定しています.

integral_constantの実装

まず,integral_constantを実装します.これは,type_traitsにある色々な判定系のテンプレートの土台になっています.また,boolの場合に限定したbool_constantエイリアステンプレートや,その値がtrueの場合のtrue_type,false_typeは,型が特定の条件を満たすか判定する際の土台として使います.

template <class T, T v>
struct integral_constant {
    static constexpr T value = v;
    using value_type = T;
    using type = integral_constant<T, v>
    constexpr operator value_type() const noexcept { return value; }
    constexpr value_type operator()() const noexcept { return value; }
};
template <bool B>
using bool_constant = integral_constant<bool, B>;
using true_type = bool_constant<true>;
using false_type = bool_constant<false>;

is_sameおよびis_same_vの実装


次に,2つのテンプレートパラメータの型が同じ場合にtrue,違う場合にfalseとなるvalueを持ったis_sameという型を実装します.また,通常はis_same<X, Y>::valueと書いて値を読み取るところを,C++14以降では,直接読み取れるようにした
is_same_vも追加されているので,そちらも実装します.

template <class T, class U>
struct is_same : false_type {};

template <class T>
struct is_same : true_type {};

template <class T, class U>
inline constexpr bool is_same_v = is_same<T, >::value;

このように,通常はfalse_typeを継承していて,型が同じ場合を特殊化してtrue_typeを継承するようにすることで,valueの値は型が同じときだけtrueになります.

これを実装しておくと,次に実装する,型を変形するtype traitsのテストがしやすくなります.is_sameも,次のようにstatic_assertを利用してテストしておきましょう.

static_assert(is_same_v<int, int>);
static_assert(!is_same_v<int, const int>);

本当は,もっと色々なパターンをテストしておいた方が良いでしょうが,その辺は実装する際に色々考えてみると良いでしょう.

remove/add系の実装


次に,型を変形するtype traitsを実装します.何故かというと,その型が何なのかを判定するis_XXX系のtype_traitsではconstなどは無視するケースも多々あり,そこでこれから実装するremove_constなどを利用するからです.

これらの型を変形するtype traitsには,typeという名前で変形後の型にアクセスできます.

まずは,constを取り除くremove_constを実装しましょう.また,typeに直接アクセスできるremove_const_tも実装します.

template <class T>
struct remove_const
{
    using type = T;
};

template <class T>
struct remove_const
{
    using type = T;
};

template <class T>
using remove_const_t = typename remove_const<T>::type;

エイリアステンプレートでtypeと取り出す際には,typenameが必要です.

逆に,constを付けるadd_constの実装はシンプルです.

template <class T>
struct add_const
{
    using type = const T;
};

template <class T>
using add_const_t = typename add_const<T>::type;

constで無い型にはconstが付き,既にconstな型なら追加のconstは無視されるんですね.

では,テストを書いておきましょう.

static_assert(is_same_v<remove_const<int>, int>);
static_assert(is_same_v<remove_const<const int>, int>);
static_assert(is_same_v<remove_const<const int *>, const int *>);
static_assert(is_same_v<remove_const<const int * const>, const int *>);

本当は,もう少し色々書くべきですが,省略します.ここで注目すべきは,ポインタの場合,constはポインタに対して付いているものが取り除かれる,ということです.
const int *がint *になると思った方もいるかもしれませんが,そうはなりません.

こんな感じで,独自のtype_traitsを実装していきます.

remove_constとadd_constのconstをvolatileに置き換えるだけで,remove_volatileとadd_volatileが作れます.

この2つを組み合わせれば,remove_cvやadd_cvが作れます.

その2は……気が向いたら書きます.

2020年5月20日水曜日

C++ Templates - The Complete Guide, 2nd 読書メモ 2章 クラステンプレート

C++ Templates - The Complete Guide, 2ndの2章「クラステンプレート」の読書メモ

スタックの実装を例にクラステンプレートについて解説している.


クラステンプレートの宣言


関数テンプレートと同様に,templateの後にテンプレート仮引数を並べ,クラス宣言を書く.

template <typename T>
class Stack {
  ...
};

クラステンプレート内では,Tを型として利用できる.

コピーコンストラクタなどを書く場合,次の2通りの書き方があるが,敢えて書くと意味があるように思わせるので,前者の方が良い.

Stack(const Stack &);
Stack(const Stack<T> &);

メンバ関数の実装


クラステンプレートのメンバ関数を実装する方法として,クラスの外側で定義する方法とクラス内で定義する方法が紹介されている.

ついでに,例外安全についても触れていて,Exceptional C++が紹介されている.

クラステンプレートの利用


基本的には,クラステンプレートを利用する場合は型を明示する必要があるが,C++17では型を省略する方法が追加され,後ほど解説する,とある.

また,呼び出されたテンプレート(メンバ)関数のみがインスタンス化される,とある.

friend


フレンド関数を定義するには,クラス内で宣言してしまう方法と,後から別途宣言する方法が2つ紹介されている.

template <typename U>
friend std::ostream & operator<<(std::ostream &, const Stack<U> &); // 別途非メンバ関数として定義する

template <typename T>
class Stack; // 前方宣言
template <typename T>
std::ostream & operator<<(std::ostream &, const Stack<T> &);

template <typename T>
class Stack {
    friend std::ostream & operator<< <T>(std::ostream &, const Stack<T> &);
};

テンプレートの特殊化


template <>として,クラス名の後に具体的な型を指定して定義することで,特定の型の場合について専用の実装を用意することができる.

例としては,std::vector<bool>など.

例えば,typename Tに対して,T *,とポインタ型にするような部分的な特殊化も可能.

デフォルト実引数


関数テンプレートと同様に,テンプレート仮引数にデフォルト実引数を指定することが可能.

タイプエイリアス


typedefのように型の別名を定義する方法として,usingによるエイリアス宣言が紹介されている.

typedefよりも = でつなぐ分,読みやすい,とも.

また,エイリアスはテンプレートにもできる.

template <typename T>
using DequeStack = Stack<T, std::deque<T>>;

C++14以降では,標準ライブラリでは 型とレイトである型から他の型を取り出す場合,今まではA::typeとしていたのを,A_tというようなエイリアスが用意された.

クラステンプレート実引数推論


C++17から,初期化時の実引数からクラステンプレートの型を推論できるようになった.使い方次第では便利なのだろうけれど,使いこなせない気がする.

文字列を利用して推論する場合の注意点についても触れている.コンストラクタが参照を受け取るようになっていると,文字列から推論しようとするとconst char [x]のような型になってしまうが,値渡しにしていると,const char *になる.

推論ガイド(deduction guide)についてもサラッと触れている.

テンプレート化された集成体


ユーザー定義または継承したコンストラクタを持たず,privateやprotectedな非staticメンバを持たず,仮想関数,仮想またはprivateまたはprotectedなベースクラスを持たないクラスまたは構造体をテンプレートにすることもできる.

2020年5月7日木曜日

C++ Templates - The Complete Guide, 2nd 読書メモ 1章 関数テンプレート

C++ Templates - The Complete Guide, 2ndの1章「関数テンプレート」の読書メモ

関数テンプレートとは

int max(int, int);
double max(double, double);
string max(string, string);

このように,型が違うだけで同じような関数をまとめて,次のように型名をtypenameで仮引数として定義できる.

template <typename T>
T max(T, T);

family of functions,という表現がポイントなのかな,と.

この関数テンプレートは,次のように呼び出せる.

max(1, 3);
max(1.0, 3.0);

このとき,具体的にTにintやdoulbeがあてはめられて関数が生成される.具体的な関数が生成されることをインスタンス化(instantiation)と呼び,生成された関数をテンプレートのインスタンス(instance)と呼ぶ.

Two-Phase Translation

テンプレートは,定義をしたタイミングと,具体的な型が割り当てられたタイミングの2つのタイミングで解釈が行われる.例えば,上記のmaxであれば大小比較が必要なので,大小比較が定義されていない型が渡されると,インスタンス化のタイミングでエラーとなる.

一方で,テンプレートの定義時点で定義されていない型名が関数テンプレート内で利用されている,といった文法上のエラーなどがある場合に,仕様上はエラー判定が行われるのだけれど,Microsoft Visual C++はその辺が緩い.Clangだとちゃんとエラーになる.その辺の挙動を切り替える方法については,次のリンク先にまとめてある.


型推論時の型変換

関数テンプレートは,呼び出し時に渡された実引数から型を自動的に決定する.これを型推論(Type Deduction)と言う.

この型推論が行われる際には,型変換が制限される.例えば,doubleを受け取る関数にintの値を渡しても,普段であれば暗黙の型変換が行われるが,関数テンプレートの場合,エラーになる.
max(1, 3.0); // コンパイルエラー

これを回避するには,明示的にキャストをするか,関数テンプレートの型を直接していする.

max(1, 3.0); // T=doubleとして判定される.

ちなみに,必要性が分からないのだけれど,デフォルト実引数を指定していたとしても,デフォルト実引数の型をもとに型推論は行われない,とのこと.

複数のテンプレート仮引数


標準ライブラリのmaxはそうなっていないけれど,説明のためにmaxのテンプレート仮引数を複数にして,複数の型を受け取れるようにしよう,という説明が進む.

こうすると,今度は返り値の型はどちらにそろえるんだ,といった問題が発生する.

そこで,返り値の型を推論する方法が示される.(書籍よりコードを抜粋)

template <typename T1, typename T2>
auto max(T1 a, T2 b) -> std::decay_t<decltype(true ? a : b)>
{
    return b < a ? a : b;
}

返り値の型を指定するところにautoを書いて,後ろにアロー(->)を書いて,decltypeで式の結果の型を求める.結果が参照などでは困る場合があるので,std::decay_tを利用して返り値の型に適切な型に変換する.

次のように,std::common_type_tを利用する方法も示されている.

template <typename T1, typename T2>
std::common_type_t max(T1 a, T2 b)
{
    return b < a ? a : b;
}

あくまでも説明のために出しているだけで,実際にこれをやると,intとdouble,doubleとint,のように型がひっくり返っているだけでも別の関数が生成されてしまい,プログラムサイズが無駄に増えるので望ましくないと思われる.

ちなみに,a > bとしないのは,C++の仕様を見るに,基本的には < を基準に比較演算が考えられているからだと思われる.

デフォルトテンプレート実引数


通常の関数と同様に,テンプレート仮引数にもデフォルト実引数を指定することができる.ただし,関数テンプレートにデフォルトテンプレート実引数を指定できるようになったのは,C++11以降なので注意が……今時必要ない気がするけど,未だにC++11さえも使えない環境もあるかもしれない.そうだとすると相当ツライ……

関数テンプレートのオーバーロード


通常の関数と同様に,関数テンプレートもオーバーロードできる.これ,意外と驚かれる.通常の関数もオーバーロードに含めた場合,どちらが優先されるかなどについては,13章で詳しく扱うようだ.第一版では9章だったので,4章ほど追加されているのかな.

その他


この単純なmax関数だけでも,色々考えることがあって,値渡しと参照渡し,どちらが良いのか,inlineにはできないのか,constexprは,など色々考えることがあるよ,と.

2020年5月6日水曜日

MSVCとClangにおけるテンプレートの名前の解釈の切り替え

Clangのドキュメントによると,Microsoft Visual C++では,テンプレートがインスタンス化されるまでインラインメソッドの本体の解析が行われないが,clangは行う.これだと,Windows系のヘッダファイルの解析時に問題になるので,-fdelayed-template-parsing,というオプションを指定することで,VC++と同じ挙動に出来る.

つまり,次のようなコードのコンパイルが通るようになってしまう.

template  void f(Type t) {
    Typo t; // 敢えてタイプミス
}

int main()
{
    // fのインスタンス化はしていない
}


一方で,VC++をClangと同じようにする方法がある.プロジェクト設定のC/C++の言語から,準拠モードを「はい(/permissive-)」にすれば良い.

詳細についてはC++ Team Blogで紹介されている.

MS Docsによれば,Visual Studio 2017 15.5以降ではデフォルトでこの設定が「はい」になるとのこと.逆に言えば,それより前から継続して使っているプロジェクトには設定されていないということなので,何かのきっかけで新しいプロジェクトを追加した際などには,この設定の差が問題になるかもしれない.

2020年3月17日火曜日

Android/iOS向けのC/C++ライブラリをビルドするときにCMakeで引っ掛かった話

iOS/Androidの両方でOSSのライブラリを使いたい.そして,CMakeLists.txtはなるべく共通にしたい,と思って弄っていてハマったことのメモ.

プラットフォームの区別でハマる


プラットフォームでフォルダを区別するために,フォルダ名を設定する処理を次のように書いてました.

if(APPLE)
  set(PLATFORM_NAME "iOS")
elseif(ANDROID)
  set(PLATFORM_NAME "Android")
endif()

project(library)

でも,上手くいかないので何故かと思いきや,projectの後に書かないといけなかったんですね.

project(library)

if(APPLE)
  set(PLATFORM_NAME "iOS")
elseif(ANDROID)
  set(PLATFORM_NAME "Android")
endif()

iOSのビルド設定でハマる


iOS向けのビルドをするために,CMAKE_OSX系の変数をsetする場合,projectより先に書かないといけない.ドキュメントにはちゃんと書いていました.適当に拾ってきたものを利用してちゃんと調べないとダメですね.

iOS向けのCheckIncludeFile


使う場合,CMAKE_TRY_COMPILE_TARGET_TYPEをSTATIC_LIBRARYにしておかないとダメなようです.

オブジェクトライブラリでハマる


ドキュメントでも名指しされていますが,Xcodeはオブジェクトライブラリだけを利用して,そこからstaticライブラリとsharedライブラリを作る,ということができません.

iOS/Androidの両方で使える記述にしようと思うと,ソースコードは変数にまとめるしかなさそうです.ちなみに,自分の場合はstaticライブラリだけで良かったので,add_libraryに直接書きました.

インストールでハマる


特定の名前で特定のフォルダに出力されるようにしようと思ったんです.で,installコマンドで良いか,と思ったんですが,Xcodeで上手くいかない.おそらく使い方の問題だとは思うのですが,色々と面倒になったところで,楽な方法を見つけました.

set_target_properties(
  library
  PROPERTIES
  ARCHIVE_OUTPUT_DIRECTORY_DEBUG lib/${PLATFORM_NAME}/Debug
  ARCHIVE_OUTPUT_DIRECTORY_RELEASE lib/${PLATFORM_NAME}/Release
  ARCHIVE_OUTPUT_NAME_DEBUG library_debug
  ARCHIVE_OUTPUT_NAME_RELEASE library_release
)

出力ディレクトリ(ARCHIVE_OUTPUT_DIRECTORY)と出力名(ARCHIVE_OUTPUT_NAME)をプロパティとして指定できるので,最初から出力先を指定しておけば良かったのです.

ビルドでハマる


Androidの場合,Ninjaを利用してビルドします.Ninjaはシングルコンフィギュレーション,つまりビルド設定を生成する際にDebugやReleaseを指定しておく必要があります.そのため,ビルドの際にはコンフィギュレーションを指定する必要はありません.

次のコマンドは,buildフォルダにビルド設定を生成した場合です.

cmake -G Ninja その他オプション -DCMAKE_BUILD_TYPE=Debug -B build
cmake --build build

一方,Xcodeはマルチコンフィギュレーション,つまり実際のビルド時にコンフィギュレーションを指定するタイプです.そのため,ビルドの手順がAndroidと若干異なります.

cmake -G Xcode その他のオプション -B build
cmake --build build --config Debug

2020年3月14日土曜日

コマンドラインでAndroid NDK向けにライブラリをビルドする

Android NDK向けのライブラリのビルド手順を探すと,基本的にはAndroid Studioでプロジェクトを作って,ビルドする,という情報が多い.

しかし!

Windowsのコマンドラインだけでビルドしたいのである.

というわけで,Android Stuidoがやってくれている作業を何とか再現してみる.

今回ターゲットにしたのは,libogg.Xiph.Org Foundationによって公開されているパテントフリーのマルチメディアコンテナフォーマットを扱うライブラリらしい.ぶっちゃけ,自分が使っているわけではないのでよく知らない.

取り合えず,このライブラリはCMake対応済みなので,最初のとっかかりとして楽だった.

まず,Javaの実行環境を用意する.OpneJDKをダウンロードしておいて,そのbinを含むフォルダへのパスをJAVA_HOMEとして設定しておく.

次に,Android SDK(Android Studioのダウンロードページの下の方にCommand line tools onlyとしてダウンロードリンクがある)をダウンロードし,展開すると,toolsフォルダがあるので,そのtoolsフォルダへのパスをANDROID_SDK_ROOTとする.

ANDROID_SDK_ROOT/binへのパスを環境変数PATHに足しておく.

sdkmanagerが実行できるようになるので,次のように必要なものをインストールする.

> sdkmanager "ndk;21.0.6113669"

すると,ANDROID_SDK_ROOT/ndk/21.0.6113669というフォルダが出来ているので,このフォルダをANDROID_NDK_HOMEとする.

次に,CMakeをインストールする.公式にAndroidに対応したのが3.7以降なので,3.7以降をインストールしておく.できれば最新版が望ましい.インストールの際は,全体で利用できるようにシステム環境変数のPATHにパスを登録しておくと,そのあとのあちこちで問題にならないかもしれない.

また,Ninja(https://ninja-build.org/)をダウンロードしておいて,適当にPATHに登録しておく.

次に,liboggのソースコードをダウンロードし,展開しておく.このフォルダへのパスをOGG_ROOTとする.

さて,ビルド用のフォルダはOGG_ROOTの外に作った方が良い.CMakeが生成する様々なファイルとliboggのファイルの区別がつきやすいからだ.

build/libogg-androidとでもしておこう.このフォルダに移動したら,ようやくCMakeを実行する.

> CMake -G Ninja -DCMAKE_BUILD_TYPE=Debug -DCMAKE_MAKE_PROGRAM=ninja -DCMAKE_TOOLCHAIN_FILE=ANDROID_NDK_HOME/build/cmake/android.toolchain.cmake -DANDROID_PLATFORM=21 -DANDROID_ABI=arm64-v8a -DANDROID_LD=lld OGG_ROOT
> ninja

これで,libogg.aが出来ているので,あとはコレをAndroid NDKを利用しているアプリのプロジェクトに組み込めば良い.

-DANDROID云々の部分については,https://developer.android.com/ndk/guides/cmake を参照する.

LDをlldにしているのは好みの問題なのでご自由に.

最適化されたバージョンが欲しければ,-DCMAKE_BUILD_TYPE=Releaseとすれば良い.

2020年2月19日水曜日

ゲームプログラマのお仕事

ゲーム専用機やモバイル向けのゲームプログラマとして10年ほどやってきて,ゲームプログラマの仕事ってこういうのだったなぁ,という記録です.


  1. ゲームを作る
    当たり前の話ですが,ゲームプログラマの仕事の1つはゲームを作ることです.
    ただ,ゲームを作る,と言っても最近は色々な知識が必要なため,専門家に分かれていたりします.

    例えば,プレイヤーを作る人,ノンプレイヤーを作る人,UIの挙動を実装する人,カメラの動きを制御する人,AIを実装する人などなどです.

    プレイヤーやノンプレイヤーを作る人の場合,3Dモデルの読み込みや表示の制御,モーションの制御,モーションに合わせたサウンドの制御,移動による衝突の制御,など色々な知識が必要になったりもします.

    小規模なチームの場合,一人で色々なことをしなければならない場合もあります.

    最近は,ゲームエンジンが細かい部分はサポートしてくれますが,それでも,その機能はどういった機能で,どういったことが出来るのか,どう設定すれば良いのか,といった知識は必要なので,日々の学習は必須です.
  2. 仕様を決める
    仕様を決めるのは,プロデューサーだったり,ディレクターだったり,ゲームデザイナーだったり,プランナーだったり,そういった人たちの仕事だと思うかもしれませんが,最終的な仕様はソースコードに実装されたものなので,仕様を決めるのもプログラマの仕事になります.

    もちろん好き勝手に,こっちの方が面白そうだから,と変えたりするわけではありません.

    他の職種の人は,言った仕様がどれくらい実装が大変なのか,正確には分かりません.また,実現したいことと仕様が合致しているとも限りません.よくよく聞いてみると,別の方法で実装した方が楽だし,実現したいことも出来る,という場合もよくあります.

    プログラマからすると簡単な実装に思えることが,他の職種からは大変そうなことに思えるので,頼むのは申し訳ない,なんて思っている場合もあるのです.その逆もあります.

    ですので,お互いに足りていない情報を確認し合い,少なくともプレイ感覚を確認できる形まで実装するのに必要な仕様を固めるのもプログラマの仕事なのです.
  3. PCトラブルシューティング
    これは,ある程度規模の大きな会社だと,別途ヘルプデスクなどあるかもしれませんが,ちょっとしたPCのトラブル,例えば電源が付かないとか,そういった簡単なトラブルの解決も求められたりします.

    電池切れとか,電圧や電流が足りない,といった物理的な問題の場合もあるので,この辺は,ソフトウェアの問題ばかり調べていてドツボにはまることもあるかもしれません.
  4. ワークフローの構築
    どういうデータを作って,どういうフォーマットで,どこに提出するのか,というワークフローを構築するのもプログラマの仕事です.

    これは仕様を決めるのと似ていて,データを提出するアーティストやプランナーが作業しやすく,かつ,プログラマとしてもなるべく扱いやすいデータをどうするか決める必要があります.

    また,ミスしやすいデータがあるのであれば,手動でチェックする機能を用意したり,バージョン管理に載せる前に自動でチェックするための仕組みを用意するのもプログラマの仕事です.
  5. CI
    昨今流行りのCI環境の構築もプログラマの仕事です.なので,サーバーやファイヤーウォールなどに関する知識が必要になったりします.

    もちろん,大規模なプロジェクトや会社になれば専門家もいるでしょうが,小さい規模だと自分でやる必要があったりします.

    普段はデバッグ機能ありの状態でゲームを作っていて,いざ本番向けの状態のゲームをビルドしようとしたら,コンパイルエラーだらけ,という事態を避けるためにも,自動ビルドによるチェックなどは重要です.
  6. ライセンスチェック
    色々なミドルウェアを使ったり,組み込みたいという話になった場合の,最初の段階でのライセンスチェックもプログラマの仕事です.もちろん,最終的には法律の専門家にお任せするのが良いのですが,原則利用してはいけないライセンスのミドルウェアなどもあります.そういったものが要望に上がった場合,その時点で弾くのはプログラマの役目です.

    また,たまに拾ってきたソースコードを組み込んでしまうようなプログラマもいます.そういった人がいないか気にするためにも,多少はソフトウェアライセンスやオープンソースに関する知識も必要になります.
  7. エンジンの選定,改造,実装
    昨今,多くの企業がゲームエンジンを利用しているので,どのゲームエンジンを利用するのか,選定する作業が必要になります.エンジンによって得意なこと,不得意なことがあるので,ある程度色々なエンジンに関する知識が求められます.

    また,エンジンによっては改造可能な部分があったりするので,必要に応じて改造することも求められます.

    企業によっては,自社エンジンを持っている場合があり,そういった企業にいる場合は,エンジンの開発側に回ることもあります.
  8. 技術的なアドバイス
    例えば,モバイルであれば,今度出る端末はどんな特徴があって,作っているゲームのチェックのために購入する必要があるのか無いのか,といった相談をされることがあります.

    ゲーム専用機であれば,最近はマルチプラットフォームだったりするので,こういう仕様ってどのハードでも実現できるのか,といった相談を受けることがあります.
  9. スケジュールを立てる
    実装する内容が決まったら,どれくらいの期間で出来そうか,というスケジュールを立てる必要があります.これは,プログラマの経験や知識によって大きく変わるので,実際に担当者に予想を出してもらうのが重要です.
  10. スケジュールを調整する
    これは,全てのゲームプログラマがやるわけではないですが,他の職種の作業スケジュールに合わせて,実装する内容をいつまでに実装するのか,出来なさそうであれば,他の職種の作業タイミングをズラしてもらうのか,担当者を変えるのか,といったスケジュール調整も必要です.

    リードプログラマや,各専門分野のリードプログラマになると,こういったことも求められます.
  11. チーム内のコミュニケーションツールの整備
    最近は,Microsoft TeamsやSlackなどコラボレーションツールが充実してきていますが,利用できない場合には,自分たちでSlackクローンを用意したり,その他のコミュニケーションツールを導入する必要があったりします.
  12. ドキュメントの整備
    ゲームプログラマの仕事は,ゲームを作るだけではありません.開発環境を構築するためのドキュメント,ワークフローに関するドキュメント,セーブや課金に関する資料整備,開発中の便利機能やQAを楽にするための機能に関するドキュメントなど,思っている以上に文章を書くことがあります.

    このドキュメントを読むのはプログラマだけでなく,他の職種向けだったりする場合もあるので,これくらいは分かるだろう,という前提が間違っていることも多々あります.

    例えば,コマンドラインを実行してください,と書いても,プログラマ以外はそもそもコマンドラインって何なのか,ということだってあり得ます.

    プログラマ以外にも通じる文章の書き方を学ぶ必要があるわけです.
  13. バグの原因調査
    最終的に,プログラマ以外の作業が原因と判明することも多々ありますが,だいたいの原因不明のバグの調査は,まずプログラマがやることになります.

    例えば,キャラクターがおかしな動きをする,という話になったとき,そもそもモーションのデータがおかしいのか,モーションの指定がおかしいのか,はたまた特定の条件の場合のみモーションの計算がおかしいのか,と色々と原因が考えられます.

    データがおかしい,ということになった場合でも,じゃあどう直すのが良いのか,ということが分からない場合もあります.

    そのためにも,ゲームプログラマは,データの直し方,もある程度知っている必要があるのです.MayaやBlenderといったDCCツールの使い方は分からなくても,この関節の回転のデータがおかしいので,そのデータを直してみてください,というくらいなら出来るはずです.
  14. ツールの開発
    開発中に問題のあるデータが見つかった場合,それを一括で修正するツールを実装する場合があります.

    CIのために,自動でデータをチェックするツールを実装する場合もあります.

    他にも,開発中にはいろいろな理由で色々なツールを作ることがあります.

    プログラマしか使わないツールであれば,コマンドラインで作ってしまうのも手ですが,他の職種も扱うツールの場合,GUIがあった方が良いこともあります.その場合,さっとC#などでツールを作れる知識も必要になります.

    他にも,例えばddsの画像をpngに変換する必要がある,というようなデータフォーマットの変換ツールを作ることもあります.この辺はややこしいので、既存のツールを内包する形にすることが多いと思いますが,その場合,どういった既存のツールがあって,利用できそうか,という知識が必要になります.

    Excelなどを改造してツールにすることもあるので,VBAなんかの知識もあると役立ったりします.
  15. 流行りのゲームやアニメに関する情報源
    ふわっと,何かこういうイメージのゲームってあるか,という質問だったり,こういうアニメの表現って出来るか,であったり,何か他のものを参照するための情報を求められることもあります.

    この辺は,ゲームプログラマに限らないのですが,実装が絡んできそうな内容の場合,やはりプログラマに質問が来たりするので,実際に詳しく知っているわけではなくても良いのですが,特徴的なゲームだったり,アニメだったりは名前くらいは押さえておいたほうが良かったりします.
  16. 処理時間の改善
    FPS(1秒当たりの画面の更新回数)が低いと,カクカクとした動きに見えたり,反応が悪いように感じたりするので,時間がかかっている処理を改善するのもプログラマの仕事です.

    処理時間を短くするには,原因を調査するためのプロファイラの使い方の知識であったり,原因が分かった際の対処方法に関する知識が必要だったりします.

    例えば,特定のAPIの呼び出しがブロックしていて時間がかかっている場合,非同期に呼び出せるAPIが無いか,呼び出せるなら,その部分を非同期にできないか,といったことを考えたりします.

    他にも,そもそもアルゴリズムが悪い場合もあるので,ある程度,その分野の基本的なアルゴリズムは知っておいた方が良いでしょう.例えば,単純な幅優先探索に置き換えるだけで劇的な改善をしたこともあります.

    また,処理時間を短くするのは,何もゲームの中だけの話ではありません.ツールの起動にやたらと時間がかかったり,キャラクターの設定に関するデータの出力にやたらと時間がかかったり,といった場合を改善するのもプログラマの仕事です.

    それまで1分ほどかかっていた作業が数秒程度になった,なんてこともあります.
思い付く範囲でダラダラと書いてみたところ,結構な量になってしまった.

書いてみて,結構幅広い知識を求められるよなぁ,と.

もちろん,最初から全部知っていろというわけではないのですが,スペシャリストになるのが難しそうであれば,色々と知っていた方が選択肢は多くなるのかなぁ,という気がします.

2020年2月3日月曜日

Substance Designer入門 Lesson3-01のパラメータ

Substance Designer入門

この本を読んでいて,そのままだとサンプルと同じ感じにする方法が分からなかったので,サンプルを見ながら,この数値にすればOK,というのを抜き出してみました.

Lesson 3-01

葉っぱの筋を作る,に出てくるTransformation 2D

Specific ParametersのTransform Matrixで,Edit Matrixを選び,X1を1.6153,Y1を0,X2を0,Y2を0.7413に設定する.
Offsetは図の通りに.

Levelsは,Specific Parametersの一番右のアイコンを押して,数値を指定できる状態にして,Level Out Lowを1に(下側の黒に相当).Level Out Highを0.6717903に(下側の白に相当).

草を並べる,のところからいくつか数値が図と違うようです.Tile SamplerのX Amountは19です.

Sizeも,最終的にはXが3.85で,Yが0.8になっています.

また,ColorのColor Randomに0.3が入っています.

Maskに使うShapeは,Scaleが0.86,SizeのXが0.76,Yが1になっています.

Gradient Mapは同じようにするのが大変です.


  • Position 0
    • R: 150,G: 190,B: 105,A: 255
  • Position 0.041
    • R: 22,G: 28,B: 15,A: 255
  • Position 0.204
    • R:19,G: 27,B: 3,A: 255
  • Position 0.317
    • R: 26,G: 40,B: 13,A: 255
  • Position 0.470
    • R: 76,G: 90,B: 33,A: 255
  • Position 0.641
    • R: 148,G 165,B: 92,A: 255
  • Position 0.684
    • R: 139,G: 153,B: 90,A: 255
  • Position 0.737
    • R: 159,G: 173,B: 107,A: 255
  • Position 0.782
    • R: 152,G:165,B:109,A: 255
  • Position 0.822
    • R: 217,G: 232,B: 160,A: 255
  • Position 0.865
    • R: 162,G: 173,B: 114,A: 255
  • Position 0.943
    • R: 162,G: 173,B: 114,A: 255
草を切り抜く,の部分のLevelsはLevel in Mid(上の灰色)を0.05353075に設定します.

プログラマのサガなのか,サンプルとの差があると何が原因なのか,ついつい調べてしまいますが,Substance Designerはちゃんと数値にアクセスする手段も用意されているようです.

Substance Designer入門にはちゃんとサンプルデータが付いてきていて,そちらはコメント付きで分かりやすくなっているので,プログラマの場合,そのサンプルを見て,デフォルトから変更されている部分を抽出すれば,サンプルと同じものが出来て,安心できると思います.

2020年1月30日木曜日

CMakeのプロジェクトにGoogle Testを組み込む方法の解読

C++で単体テストをする場合の候補として,Google Testがあります.

公式ドキュメントには,CMakeを利用している既存のプロジェクトにGoogle Testを組み込む方法が書いてあるのですが,何をしているのかパッと分からなかったので,じっくり読んでみました.

まず,次のような構造を想定します.

  • CMakeLists.txt
    • srcとtestをadd_subdirectoryで追加
  • src (プロジェクトのソースコード)
  • test
    • GoogleTest.CMakeLists.txt.in
    • CMakeLists.txt
  • build (ビルド用フォルダ,バージョン管理外)
GoogleTest.CMakeLists.txt.inの内容は,ドキュメントにあるCMakeLists.txt.inと同じです.

testフォルダのCMakeLists.txtの1行目,configure_fileが解釈されると,次のような状態になります.

  • build
    • googletest-download
      • CMakeLists.txt
そして,このgoogletest-downloadの中のCMakeLists.txtは,configure_file実行時のCMAKE_CURRENT_BINARY_DIRで解釈されるので,次のような内容になります.

ExternalProject_Add(googletest
    ... 省略 ...
    SOURCE_DIR "buildへの絶対パス/googletest-src"
    BINARY_DIR "buildへの絶対パス/googletest-build"
)

次に,execute_processにより,ルートのCMakeLists.txtに対してビルド設定を生成する際に,googletest-download内のCMakeLists.txtが実行され,フォルダが次のような構造になります.

  • build
    • googletest-download
    • googletest-src
    • googletest-build
これで,googletestを組み込む準備ができたので,そのあとで単体テスト用のターゲットをadd_executableして,gtest_mainとリンクし,add_testでテストに登録しているわけです.

取り合えず,これでプラットフォーム気にせずにGoogle Testを導入しつつ,コードが書けそう.

2019年12月4日水曜日

Developer PowerShell for VS 2019をx64に切り替える方法

Visual Studio 2019から,Developer PowerShell for VS 2019,というものが付いてきます.

これで,PowerShellからcl.exeを使うためにゴニョゴニョしなくて済む,と思ったら,コンパイラが32bit環境になっていました.

次のコマンドで確認したときに,パスがHostx86のx86になっていました.

(gcm cl).Source

どうしても64bit環境が使いたいのですが,コマンドプロンプトについては,x86,x86からx64のクロスコンパイル,x64,x64からx86のクロスコンパイル,としっかり組み合わせが用意されているのに,PowerShellについては用意されていません.

そこで,Developer PowerShell for VS 2019のリンクのプロパティを見て,実際に何をやっているのか確認してみました.

%SystemRoot%\system32\WindowsPowerShell\v1.0\powershell.exe -noe -c "&{Import-Module """C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\Common7\Tools\Microsoft.VisualStudio.DevShell.dll""";Enter-VsDevShell インスタンスID}"

インスタンスIDの部分は,たぶん環境によって変わります.

どうやら,Enter-VsDevShellというものを呼び出しているようです.取り合えずヘルプを見てみましょう.

Get-Help -Detailed Enter-VsDevShell

非常に簡単なことしか書いていませんが,-DevCmdArgumentsというものがあることに気付きます.これに -help を渡してみます.

Enter-VsDevShell インスタンスID -DevCmdArguments -help

すると,どうやらvsdevcmd.batを裏で呼んでいるようです.これについても情報が少ないのですが,あれこれ調べた結果,-arch=x64を指定してやれば良さそうです.

Developer PowerShell for VS 2019のリンクのプロパティからリンク先を書き換えてみましょう.
%SystemRoot%\system32\WindowsPowerShell\v1.0\powershell.exe -noe -c "&{Import-Module """C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\Common7\Tools\Microsoft.VisualStudio.DevShell.dll""";Enter-VsDevShell インスタンスID -DevCmdArguments -arch=x64}"

書き換えたら,そのリンクからPowerShellを起動します.そして,もう一度次のコマンドを実行します.

(gcm cl).Source

無事,Hostx64のx64が選択されています.

やってやりました!

via GIFMAGAZINE



ちなみにインスタンスIDは,書いてあるものを使えば良いのですが,間違えて消してしまったりした場合,vswhereというものを使えば見れるようです.

注意点として,どうもリンク先の長さに上限があるらしく,書き換え方次第では入りきらなくなるようです.

2019年12月3日火曜日

CMakeで生成したVisual C++のプロジェクトで例外を無効にする方法

CMake 3.15.5でVisual Studio 2019向けに生成したプロジェクトで,次のようなCMakeのコマンドを実行して確認する.

message("${CMAKE_CXX_FLAGS}")

すると,/EHscが入っていることが分かる.これを次のように削除する.

string(REPLACE " /EHsc" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}")

すると,プロパティページの「C/C++」の「コード生成」の「C++の例外を有効にする」が空欄になる.合わせて,「_HAS_EXCEPTIONS=0」をtarget_compile_definitionsで定義してやれば,例外を無効にできる.

他に何か良い方法があれば教えて欲しい.

コマンドラインからVisual Studioの仕組みを学ぶ2

前回はこちら
コマンドラインからVisual Studioの仕組みを学ぶ

マクロによるDebug版とRelease版の切り替え


さて,次のように_DEBUGの有無でデバッグ時とそれ以外で処理を変える,といったコードを書いたことはあるでしょうか?

#include <iostream>
using namespace std;

int main()
{
#if defined(_DEBUG)
    cout << "Debug" << endl;
#else
    cout << "Release" << endl;
#endif
}

この例だとあまり意味はないですが,例えば開発中のみデバッグ機能を有効にしたり,特別にログを出力するようにするなど,こういった書き方をすることは良くあります.

ところで,この _DEBUG というマクロはどこで定義されているのでしょうか? また,定義されていたとすると,デバッグ機能を無効にしたい場合は,わざわざ消さないといけないのでしょうか?

そんな面倒なことはしません.コマンドラインのオプションでマクロを定義することができます.これにより,コンパイル時にデバッグ有り版とデバッグ無し版を切り替えることができます.

マクロの定義には "/D マクロ" という書き方をします.

次のコマンドでは,_DEBUGが定義された状態でコンパイルします.
cl /EHsc /D _DEBUG main.cpp

一方,次のコマンドでは,_DEBUGが定義されていない状態でコンパイルします.
cl /EHsc main.cpp

ソースコードを読んでいて,どこにも定義されていないマクロに遭遇したことがあるかもしれません.そういったマクロは,このようにコンパイル時に定義されているのです.

Visual Studioのプロパティとしては,C/C++のプリプロセッサのプリプロセッサの定義になります.このプロパティは ; 区切りでマクロが定義されていて,コンパイル時にはそれを分解して,それぞれを /D と一緒にコンパイラに渡しているわけです.

プロパティの左上にある構成をDebugやReleaseで切り替えると,このプリプロセッサの定義,が変わることが分かります.

Visual Studioでは,DebugやReleaseといった目的やx64などのプラットフォームの組み合わせ毎に,こういった定義を設定することができて,その設定に合わせてコンパイラにマクロを渡してくれているわけです.

clangの場合,-D でマクロが定義できます.似ていますね.そして,clangを使っているXcodeでも,同じようにマクロを定義する項目があります.

まとめ

/D マクロ,でコンパイル時にマクロを定義した状態でコンパイルすることができます.
DebugやReleaseといった構成やx64などのプラットフォームを切り替えると,プロパティのプリプロセッサの定義が切り替わり,/D の形にしてコンパイラに渡してくれます.

おまけ

Windows.hを開いてみると,色々とコードを変更するためのマクロが書いています.特に,NOMINMAXは,次のようなコードがちゃんとコンパイルできるようにするために必要です.

#include <iostream>
#include <cmath>

int main()
{
 std::cout << std::min(1, 2) << endl;
}

どうしてNOMINMAXが必要なのか,実際に確かめてみると理解が深まるかもしれません.

ついでに,_USE_MATH_DEFINESについても調べておくと,役立つときがあるかもしれません.

2019年12月2日月曜日

コマンドラインからVisual Studioの仕組みを学ぶ



Visual Studioそのものについて解説した本や,Visual C++について解説した本はいくつかあります.ただ,Visual Studioの場合,C#前提だったり,Visual C++についての本でもC++のコードを書くことが前提だったりして,自分でライブラリを作ったり,他のライブラリを利用したりする場合の方法についてはあまり詳しくなかったりします.

このツイートの後に,コマンドラインを覚えれば,色々なIDEの設定などが分かるのでは,という話になったものの,そういった解説って無いよね,という話になったので,ちょっと書いてみようかと.

前提として,Visual Studio 2019でC++の開発環境が入っている前提とします.
また,cdやmkdirなど基本的なコマンドラインの機能についてはこの解説には含めないので,必要に応じて調べてみてください.

コマンドラインを使えるようにする

スタートメニューからVisual Studio 2019のフォルダを探し,Developer Command Prompt for VS 2019かDeveloper PowerShell for VS 2019を起動します.

起動したら,次のコマンドを実行してみましょう.

cl /?

この,cl というのがC++のコンパイラで,/?を渡すことで指定できるオプションの一覧を表示したわけです.

Visual C++の設定の多くは,このclに渡すオプションを生成するために使われます.

そして,ここが肝心なポイントなのですが,色々なC++コンパイラでコマンドラインから指定できるオプションには,共通の機能を提供するものが多々あるのです.

つまり,コマンドラインのオプションが分かっていれば,このオプションを設定するIDEの設定項目はどれか,という探し方ができるので,普段Visual Studioを使っていて,突然Xcodeを使うことになったとしても,何となく設定する方法が分かってしまうのです.

コンパイルしてみる

よくあるHello, Worldをコンパイルしてみましょう.

// main.cpp
#include <iostream>
using namespace std;

int main()
{
    cout << "Hello, World!" << endl;
}
cl main.cpp

コンパイルすると,何やら警告が出ます.着目するポイントは,warning(警告)という単語の直後のC4530という警告の番号です.これをGoogleなどで検索すると,Microsoftのドキュメントが表示されます.

他にもエラーや警告が出た場合,必ずその直後にこういったエラーの種類を示す番号が出ているので,Microsoftのドキュメントで検索してみましょう.エラーや警告の理由が分かれば,修正もしやすくなります.

詳細については省くとして,取り合えず/EHscを指定しろ,ということなので,次のように指定してみましょう.

cl /EHsc main.cpp

これで,問題なくコンパイルできました.同じフォルダにmain.exeとmain.objというファイルができているはずです.

さて,このobjってなんでしょう? これは,C++のソースコードをコンパイルした結果です.これに,標準ライブラリなどの機能をリンクさせて,最終的に実行可能ファイル(exe)になるのです.

簡単なソースコードの場合,コンパイルも一瞬で終わるので気になりませんが,大規模になってくるとコンパイル時間は数十分,数時間,場合によっては一晩かかったりするかもしれません.

できれば,変更した部分だけコンパイルするようにしたいところですよね.IDEは,objとcppを比較して,更新されている場合だけコンパイルする,といったことをすることで,コンパイル時間を短くする手助けをしてくれています.

単純に比較しているわけではなく,ヘッダファイルを書き換えた場合,そのヘッダファイルをインクルードしているcppも変更の対象にしてくれたり,と色々と賢くやってくれています.

ちなみに,この/EHscというのは,プロジェクトのプロパティのC/C++のコード生成のC++の例外を有効にする,で設定できる値です.

インクルードパスを指定する


さて,続いてインクルードパスについて解説します.まずは,次のような構造で必要なファイルを用意してください.

main.cppがあって,subフォルダがあって,その中にheader.hとsub.cppがある状態です.

|- main.cpp
|- sub
|   |- header.h
|   |- sub.cpp

それぞれのファイルの中身は次のようになります.

// sub/header.h
#ifndef HEADER_H_INCLUDED
#define HEADER_H_INCLUDED

int f();

#endif // HEADER_H_INCLUDED

// sub/sub.cpp
#include "header.h"

int f()
{
    return -1;
}

// main.cpp
#include <iostream>
#include "sub/header.h"

using namespace std;

int main()
{
    cout << f() << endl;
}

これをコンパイルしてみましょう.

cl /EHsc main.cpp sub/sub.cpp

いくつか注目するポイントがあります.

1つ目は,#includeの後の書き方に2種類あることです.一般的に,<と>で囲まれている場合,コンパイラが定義したパスからの相対パスでインクルードするファイルを探し,"で囲まれている場合,コンパイルしているファイルのある場所からの相対パスでインクルードするファイルを探した後に,コンパイラが定義したパスからの相対パスでインクルードするファイルを探します.

2つ目は,sub/header.hという書き方です.1つ目の説明でも書いたように,インクルードするファイルの指定は相対パスになるので,フォルダを指定して,そのフォルダ以下にあるファイルを指定することも可能なのです.

さて,先ほどからコンパイラが定義した場所から,と言っていますが,それを追加する方法があります.まずは,main.cppを次のように書き換えてみましょう.

// main.cpp
#include <iostream>
#include 
using namespace std; int main() { cout << f() << endl; }

次のように,/I subを追加すると,コンパイルできます.

cl /EHsc /I sub main.cpp sub/sub.cpp

/I の後にパスを書くことで,コンパイラがインクルードするファイルを探すパスのリストに追加することができます.

一般的には,他のライブラリを利用する場合,そのライブラリのヘッダファイルなどは一か所にまとまっていて,そのルートフォルダへのパスを/Iで指定することで利用できるようにします.

これは,プロジェクトのプロパティでは,C/C++の全般の追加のインクルードディレクトリに当たります.この項目に ; 区切りで設定したパスは,区切りで分けられ,それぞれ/I に指定されます.

同様の設定は,Xcodeにも存在しますし,Xcodeが利用しているclangというコンパイラにも同様の設定があります.clangの場合 -I で指定します.そう,似ているんですよ.

まとめ

コマンドラインのコンパイラへの理解を深めることで,IDEの設定項目についての理解を深め,プロジェクトの環境を整えることができるようになります.

まだまだ説明したいことはありますが,長くなるのでまた分けて書こうと思います.

考えている話題は,こんな感じです.

  • 警告レベルの変更
  • 警告やエラーの抑制
  • 警告をエラーに
  • 最適化レベルの変更
  • リンク関係の設定
  • ソースコードからコンパイラに指示を出す方法
  • ビルドイベント
  • C++言語仕様の選択

2019年8月2日金曜日

XAudio2SamplesのDirectX::LoadWAVAudioFromFileExを読み解く

入門XAudio2を読んでいて,45ページ目から48行目でサラッと流されているWAVファイルの読み込み処理,DirectX::LoadWAVAudioFromFileExについて気になったので読み解いてみようかと.

Qiitaに書くのは何か違うかな,と思ったのでこっちで.

RIFFについては知っている前提で.

RIFF(Resource Interchange File Format)についてはこちらを参照(Microsoft Docs)

DirectX::LoadWAVAudioFromFileEx関数で読み取った結果は,次のようなDirectX::WAVData構造体に保存されます.

namespace DirectX
{
    struct WAVData
    {
        const WAVEFORMATEX* wfx;
        const uint8_t* startAudio;
        uint32_t audioBytes;
        uint32_t loopStart;
        uint32_t loopLength;
        const uint32_t* seek;
        uint32_t seekCount;
    };
}

で,そのDirectX::LoadWAVAudioFromFileEx関数の宣言は次のような感じです.

namespace DirectX
{
    HRESULT LoadWAVAudioFromFileEx(
        _In_z_ const wchar_t* szFileName,
        _Inout std::unique_ptr& wavData,
        _Out_ WavData& result
    );
}

この,_In_z_とか_Inout_は,ソースコード注釈言語(Source-code Annotation Language, SAL)です.詳しくは,Microsoft Docsを検索してください.簡単に言うと,Visual Studioがコードの使い方に問題がないかチェックするための付加情報です.

szFileNameはファイルパス,wavDataにはファイルのデータ全てが格納されて,WAVData::startAudioはその中の音データの開始位置を指します.

さて,中を見ていきましょう.最初の数行は,ファイルパスへのポインタがNULLだったらエラー,出力先を0初期化,というだけなので飛ばします.



DWORD bytesRead = 0;
HRESULT hr = LoadAudioFromFile(szFileName, wavData, &bytesRead);

早速新しい関数が出てきました.


static HRESULT LoadAudioFromFile(
    _In_z_ const wchar_t* szFileName,
    _Inout_ std::unique_ptr& wavData,
    _Out_ DWORD* bytesRead
);

単純に,szFileNameのパスにあるファイルを開いて,wavDataにファイルの内容を書き込み,bytesReadに読み取ったバイト数を書き込む,というだけの関数です.

もう少し詳しく見てみると,Windows 8以降の場合は,CreateFile2を使ってファイルを開き,それより前の場合はCreateFileWを使ってファイルを開くようになっています.ちょっと調べてみたんですが,敢えて使い分ける理由がサッとは見つかりませんでした.

そして,Vista以降ならGetFileInformationByHandleExを使い,それより前の場合はGetFileSizeExを使ってファイルサイズを取得しています.これも,わざわざ使い分ける理由がいまいち不明です.GetFileInformationByHandleExの場合,エラー処理が入っているので,何らかの理由でファイルサイズがちゃんと取得できないケースがあるのかもしれません.

そして,このファイルサイズが32bitで表現できる範囲を超える場合や,WAVファイルとして最低限必要なRIFFチャンクと"fmt "チャンクのサイズ以下の場合,エラーとしています.

ここまで来ると,あとはwavDataに必要なサイズ分のメモリを割り当て,ファイルから読み取るだけです.この関数内で勝手にメモリを割り当てるからunique_ptrにしているんでしょうね.

さて,DirectX::LoadWAVAudioFromFileExに戻ります.


bool dpds, seek;
hr = WaveFindFormatAndData(
    wavData.get(),
    bytesRead,
    &result.wfx,
    &result.startAudio,
    &result.audioBytes,
    dpds,
    seek
);

またまた新しい関数です.


static HRESULT WaveFindFormatAndData(
    _In_reads_bytes_(wavDataSize) const uint8_t* wavData,
    _In_ size_t wavDataSize,
    _Outptr_ const WAVEFORMATEX** pwfx,
    _Outptr_ const uint8_t** pdata,
    _Out_ uint32_t* dataSize,
    _Out_ bool& dpds,
    _Out_ bool& seek
);


  • wavData
    • WAVファイルのデータを渡す
  • wavDataSize
    • wavDataのサイズを渡す
  • pwfx
    • wavData中のfmtチャンクから,WAVEFORMATEXの位置へのポインタを返す
  • pdata
    • wavData中のdataチャンクから,音データの開始位置へのポインタを返す
  • dataSize
    • pdataのサイズを返す
  • dpds
    • dpdsチャンクの有無を返す.dpdsチャンクはxWMAフォーマットの場合に存在する.
  • seek
    • Xbox OneでサポートされているXMA2フォーマットの場合にtrueになる,とコメントを読み解くと書いてある.

さて,この中で最初に呼ばれているFindChunkという関数は,次のような関数です.


static const RIFFChunk* FindChunk(
    _In_reads_bytes_(sizeBytes) const uint8_t* data,
    _In_ size_t sizeBytes,
    _In_ uint32_t tag
)

dataの先頭がRIFFのヘッダ(4バイトのfourccと4バイトのチャンクサイズ)と仮定して,sizeBytes先までにtagと一致するfourccを持つチャンクを探してくれるだけの関数です.

この関数を使って,まずはRIFFチャンクを探します.このRIFFチャンクがなかったり,RIFFチャンクのRIFFヘッダの直後のfourccがWAVEまたはXWMAで無い場合は正しいファイルでない,とエラーを返しています.

RIFFチャンクからRIFFヘッダとfourcc分のバイト数を進めた位置からサブチャンクが始まります.次に,このサブチャンクからfmtチャンクを探します.fmtチャンクには,少なくともWAVEFORMAT構造体のデータが含まれています.

このWAVEFORMAT構造体にはwFormatTagという音データの形式が格納されていて,PCM(pulse code modulation)形式や,ADPCMフォーマットである,といったことを確認し,その上でfmtチャンクのサイズに問題がないか,dpdsやseekは設定できるか,といったことをチェックしています.

fmtチャンクが終わったら,チャンクの終端に進み,dataチャンクを探し,サイズが渡されたデータを超えていないかチェックをしたら,dataチャンクのデータ部分の先頭をpdataに設定して,dataチャンクのサイズをdataSizeに設定して,この関数は終了です.

DirectX::LoadWAVAudioFromFileExに戻ると,次はWaveFindLoopInfo関数です.



static HRESULT WaveFindLoopInfo(
    _In_reads_bytes_(wavDataSize) const uint8_t* wavData,
    _In_ size_t wavDataSize,
    _Out_ uint32_t* pLoopStart,
    _Out_ uint32_t* pLoopLength
);

wavData,wavDataSizeはWaveFindFormatAndDataと同じなのでスキップします.この関数は,WAVファイルからループの開始サンプル数とループの長さのサンプル数を,それぞれpLoopStartとpLoopLengthが指す先に格納してくれます.

この中では,RIFFチャンクがWAVEかどうか確認しています.xWMAはループ情報を含まないようです.そして,DLSチャンク(fourccはwsmp)またはSampleチャンク(fourccはsmpl)からループ情報を取り出しています.

DLSは,Downloadable Soundの略っぽいですが,該当するデータが手元に無いので詳細は不明です.

smplチャンクは,コード中ではMIDIチャンクと書いているので,MIDIで作成した音しかループ情報を含んでいないのでしょうか?

このループ情報の取り出しは,場合によってはスキップしても良いかもしれません.

DirectX::LoadWAVAudioFromFileExに戻ると,dpdsやseekがtrueかどうかで次のような関数を呼び出して,必要なデータを取り出しています.


static HRESULT WaveFindTable(
    _In_reads_bytes_(wavDataSize) const uint8_t* wavData,
    _In_ size_t wavDataSize,
    _In_ uint32_t tag,
    _Outptr_result_maybenull_ const uint32_t** pData,
    _Out_ uint32_t* dataCount
);


  • wavData,wavDataSize
    • おなじみWAVファイルの中身とサイズ
  • tag
    • RIFFチャンクのサブチャンクから抜き出すチャンクのfourcc
    • dpdsの場合,dpdsチャンク,seekの場合seekチャンクを指定
  • pData
    • tagで指定したチャンクのデータ部分へのポインタを返す
    • 4バイトのデータの配列になっている
  • dataCount
    • tagで指定したチャンク内にある4バイトのデータの数
これでようやく終わりです.

この関数は,ファイルデータをすべてメモリに載せた上での処理になっています.しかし実際には,効果音などの短い音であればメモリに載せられても,BGMなどのようにある程度のサイズがあるデータは,必要な部分を徐々に読み取って再生するのが一般的だと思うので,そういった場合は,FindChunkのように都度必要なチャンクを探して回るのではなく,チャンクを発見する度に必要な情報を埋めていって,といった形にする必要があるかもしれません.