KLabGames Tech Blog

KLabは、多くのスマートフォン向けゲームを開発・提供しています。 スマートフォン向けゲームの開発と運用は、Webソーシャルゲームと比べて格段に複雑で、またコンシューマゲーム開発とも異なったノウハウが求められる領域もあります。 このブログでは、KLabのゲーム開発・運用の中で培われた様々な技術や挑戦とそのノウハウについて、広く紹介していきます。

カテゴリ: その他

(本稿はKLab Advent Calendar 2016 の9日目の記事になります)

こんにちは, @mecha_g3 です.

ISUCON6 の優勝賞金で新型の MacBookPro を購入しました. キーボードの打ち心地にどうも慣れられなかったり, USB-C の周辺機器がたくさん増えたりしていますが, 概ね満足しています.

格ゲーの攻略記事のようなタイトルですが, この1ヶ月ほど CODE VS for STUDENT というプログラミングコンテストに注力していたので, 今日はそれについて書きたいと思います.

CODE VS とは

CODE VS とは, お題となるゲームの, 最強のAIを開発するプログラミングコンテストです.

CODE VS for STUDENT 公式サイト https://student.codevs.jp

主催はチームラボさんとキャリフルさんで, KLabはスポンサーをさせて頂いてます.

決勝イベントが明日 12/10(土) に行われます. 決勝イベントでは, 予選で勝ち上がった学生 16 名 (Normalコース, Hardコース各8名) が対戦します.

また, 僕は学生を除いたランキングで 1 位だったので, エキシビジョンマッチに招待していただきました. 優勝した学生のAIと, 僕のAIが対戦します.

生放送もされるようなので, 是非観てください!

この記事の後半は, コンテストに参加していない方は興味がない内容になっているかもしれないので, 最初に宣伝しておきます.

ゲームのルール

今回のゲームは, CODEVS 2.0 のリメイク版で, 対戦形式の落ちものパズルです.

ルールの詳細は以下の PDF に記されています. https://student.codevs.jp/assets/files/rule.pdf

ゲームイメージ

ゲームのリプレイ動画を撮影しました. 動画を見ると, ルールのイメージを掴みやすいです.

ブロック

1~9 の数字が書かれた 1x1 のブロックです.

パック

3x3 の領域にブロックが 3~4 個入っています. ゲーム開始時に全300ターン分のパックが与えられます.

フィールド

幅10, 高さ16 のフィールドで, ここにパックを投下していきます.

消滅と連鎖

フィールド上で 縦, 横, 斜めのブロックの数字の和が 10 になった所が消えます. 消えたブロックの上にブロックが積まれていた場合, 上のブロックが落下します. ブロックが消えた事により上のブロックが落下し, 落下したブロックが消える. という動作を繰り返し, 連鎖が発生します.

得点

連鎖を行うと、以下の得点計算式に従って得点を得ることができます.

score = ∑ floor(1.3 ^ i) * floor(Ei / 2)
i: 連鎖数
Ei: 消滅カウント(合計が 10 になったブロックの数)

連鎖数に応じて指数的にスコアが上昇するので, 連鎖数が一番大事です. 連鎖の最後 (連鎖尾) の消滅カウントでスコアが数倍変わってくるので, 消滅カウントも無視できない存在です.

お邪魔ブロック

得られた得点に応じて, 相手にお邪魔ストックが加算されます. お邪魔ストックがある場合, パックの空いている領域にお邪魔ブロック (消すことができないブロック) が挿入されます.

自分にお邪魔ストックがある場合に得点を得た場合, お邪魔ストックが消費 (相殺) されます.

勝敗判定

パックがフィールドの外に溢れたり, 思考の制限時間を過ぎると敗北になります. 思考時間は 1 手最大 20 秒, 1 ゲームの持ち時間は 180 秒です.

基本的な考察

AIのプログラムは, 標準入力でゲームの情報を取得し, 各ターンにおいてパックを何回転させてどこに投下するかを標準出力に出力します.

全ターンのパックは事前に分かるので, 落下と連鎖のシミュレーションを行い, 大連鎖を作ることを目標に探索するプログラムを作る事になります.

長いターン使って大連鎖を作ろうとしていると, 相手が先に連鎖を発火し, 邪魔ブロックが降って来て大連鎖を発火できずに負けてしまいます.

逆に, 短いターンで連鎖を作っていると低いスコアになってしまい, 相手に邪魔ブロックが大して降らないため, 相手は連鎖を伸ばすことができてしまいます.

そこそこ短いターンで出来るだけ大きな連鎖を作る探索が, このゲームで一番大事なポイントです.

ここからは, 僕がこの探索部分をどう改良していったかについて掘り下げていきます.

アプローチ

落とし方は 1ターンあたり 48 通りあるので, 4 ターン先(48^4 = 5308416) まで調べるのに数秒かかりそうです.

実際には有効な落とし方はもう少し少ないですが, 全探索だと 5, 6 ターンが限界だと思います.

相手のフィールドを埋め尽くす程の邪魔ブロックを降らせるためには, 16 ~ 20 連鎖程度必要で, そこから考えると 20 ターン程度必要です.

このような探索の問題では, 評価関数を作り, 各深さ毎に評価値の高い上位 K個 を残すことを繰り返すビームサーチという手法がよく使われます. (K をビーム幅と呼びます)

もちろん厳密な解は得られませんが, 適切な評価関数が設定できれば, 短い時間でそこそこ良い解を得ることができます.

僕は最初ビームサーチで実装していましたが, 今回の問題では時間制限がシビアだったので, 時間調整のしやすいビームサーチの亜種である chokudaiサーチを採用しました.

ベンチマーク

探索の改良を試したり, 評価関数を調整した結果, それが良くなったか悪くなったかを比較できるようにする必要があります.

今回僕は, 公式クライアントから 300 ゲームの入力ファイルを抽出し, 各入力で 20 ターン先まで探索し, できた連鎖のスコアの平均値を見て調整しました.

1 回 15 秒かけて探索するので 1 時間半かかってしまいますが, Google Compute Engine で 16 コアのマシンを借りて並列で走らせる事で 5 分程で計測することができるようになりました.

GCE 上に Jenkins をインストールしたプリエンプティブインスタンスを用意し, ベンチマークをかけたい時だけ立ち上げて実行, 終わったら個人的に使っている slack に結果を通知し, インスタンスを停止する. という運用で使っていました. 今回 CODE VS のために使った金額は 400 円程度でした.

slackへの通知イメージ

評価関数

「連鎖数が大きくなるようにしたい」というのはどういう評価関数にしたら良いのか悩みどころですが, 評価関数の中で実際に連鎖を起こしてみて何連鎖したのかを見るのが簡単です.

各列に対して 1-9 のブロックを落とすシミュレーションをしてみると, 何連鎖する盤面なのかを正確に見積もることができます.

ただ, 1 状態を評価するのに 90 回もシミュレーションをするのはとても無駄が多く遅いので, ある程度サボらないといけません.

僕の場合は, フィールドを広く使って, 左端から右端(あるいは右端から左端) に向かって連鎖を伝搬させていくような積み上げ方を見つけて欲しかったので, 発火点を左端か右端に固定しました.

最初の評価関数は

200 * 左端の列に 4 を落としたときに発生するスコア + 連鎖前のブロック数

でした. すごく雑ですが, わりと強くて最初の 1 週間程度 1 位を維持していました.

ここから, ベンチマークの結果を見ながら評価関数を調整していきました.

最終的には

ランダム(0.0 ~ 1.0)
+ 100000 * 連鎖数
+ 1000 * 連鎖前のブロック数
+ 10 * 連鎖尾の消滅カウント
- 100 * 連鎖あたりの平均消滅ブロック数
- 1 * 連鎖後に残っているブロック数

としました.

お邪魔ブロックが混入してきた場合は発火に使うブロックを 1-9 で試すようにしましたが, 他は同じです.

各パラメータの重みは比較の優先度を決めているだけです.

ランダム

連鎖数が増えるまでの盤面は優劣をうまく付けれないので, 同じスコアが並びやすいです.

同じスコアが並んだ場合, 特定の順序 (探索キューに入れた順など) で偏りが出てしまうので, これを分散させる目的で入れています.

連鎖数

発火目標ターンのパックをフィールドの端に落とした際に発生する連鎖数です.

この方法で評価した場合, 発火点は固定なので, 連鎖尾を伸ばしていくような探索になります.

スコアをそのまま評価関数にしてしまうと, 連鎖数を伸ばすよりも同時消し数を増やしてしまうほうが良いと評価してしまうことになるので, 最終的に連鎖が伸びにくくなってしまいます.

連鎖前のブロック数

フィールド中に存在する, お邪魔ブロックを除いた 1-9 のブロックの数です.

連鎖を積み上げていく途中で, 無駄に消してしまうとそれだけでもったいないので, ブロック数が多い方が良い状態としました.

連鎖尾の消滅カウント

連鎖尾も一応評価したいのですが, それよりも連鎖数を伸ばして欲しいので評価値としてはかなり優先度を低くしてあります. 気持ちです.

連鎖あたりの平均消滅ブロック数

連鎖で消えたブロック数 / (連鎖数+1) で計算しています.

限られた数のブロックで大きな連鎖を作って欲しいので, 同時消しが多いのは無駄です. 連鎖あたりの平均消滅ブロック数が少ないほうが良い状態としました.

連鎖後に残っているブロック数

8 や 9 といった大きい数字のブロックは 1 や 2 が来ないと消えません. 一方 1 や 2 は他の数字のブロックとも消えます. 連鎖が終わった後で 8 や 9 が残りやすく, これが実質的にお邪魔ブロックになって次回連鎖のスコアが下がってしまう現象が見られたのでこれを入れました.

高速化

速い方がたくさんの盤面を探索できるので, 制限時間内に良い連鎖を見つけやすいです.

テストの作成

シミュレータのテストがあると安心してリファクタリングや高速化に着手できます.

今回のルールでは, CODEVS のクライアントのリバースエンジニアリングは禁止されていませんでした.

クライアントの jar をライブラリとしてインポートした Java プロジェクトを作る一般的なテクを用いて, ログファイルから入出力を抽出するプログラムを作りました.

オンライン対戦のログファイルを大量に用意し, そこから入出力を作ってテストケースとしました.

評価用の落とし方の固定

ある程度探索が進むと, 発火に使うパックの落とし方はほぼ決まってしまうので, 評価関数の中で色々な落とし方を試して評価するのは無駄が多いです. なので, 4 連鎖以上が発生した場合は, 次から評価関数内での連鎖数の評価に使うパックを固定するようにしました.

精度は落ちますが, かなり無駄が省けるのでスコアは上がりました.

同一盤面の除去

高速化とは少し違う話ですが, 一般的に探索の途中で同じ盤面を 2 回探索しないようにするのは多くの場合必須な処理です. これを同一盤面の除去と呼んでいます.

今回はゲームの性質上, 完全におなじフィールドは現れにくいので効果は薄いですが, 探索中のフィールドをビジュアライズしてみると, 少なからず現れていたので除去を行いました.

フィールドのハッシュ値を計算し, 探索済のハッシュ値を保持しておきます. 同じハッシュ値のフィールドが現れた場合, 処理をスキップします.

探索のキューに入れるタイミングではなく, キューから取り出すタイミングで判定することでほとんどコストをかけずに除去することができます.

同一盤面を除去することで, 多少ですが無駄な探索を減らす事ができました.

落下処理の高速化

予選の対戦サーバは Xeon E5-2670 v2 だったのですが, 決勝参加者向けに提供された環境は Xeon E5-2666 v3 でした. (おそらく c4.large)

調べた所 v3 では Haswell 世代の命令が使えます.

Haswell から使える命令に PEXT (Parallel bits extract) という命令があります. これは値 val と ビットマスク mask を入力し, val のビット列の中から mask 指定したビットを抽出し, それを下位ビットに寄せたビット列を作る命令です.

さて, ブロックの数字は 0~11 (お邪魔ブロックは11で表される) なので 4bit あれば表現でき, フィールドは幅10 高さ16なので, 64bit 整数 10 個で表現することができます.

このようにフィールドを表現すると, PEXT を消去が行われた列に対して使うだけで落下処理が実装できます.

// erased_y_mask := 消えたブロックの位置が 1111 となるようなビットマスク
// m_[x] := 1列を表す uint64_t 型の変数
m_[x] = _pext_u64(m_[x], ~erased_y_mask);

落下処理のために生まれたかのような命令ですね.

消去処理の高速化

消去処理では, フィールド中にある隣接したブロックの数字の和が 10 になる場所を探さなければなりません.

和が 10 になるかどうかの計算は, しゃくとり法というテクニックを使うことで, 各列に対して 1 度走査するだけで行うことができます.

あるブロックが落下した結果, そのブロックに隣接したブロックの数字の和が 10 になることで消去が発生するので, 落下が発生していない領域は調べる必要がありません.

落下が発生した行, 列と, それに対応する斜めの行を記録しておき, 消去が発生しえない範囲に対しては消去判定を行わないようにしました.

もう少し具体的に説明すると, あるブロックが落下し (x, y) で止まったとしたとき, 以下のビットマスクをつくります.

x_mask |= 1 << x
y_mask |= 1 << y
z_mask |= 1 << (x + y)
w_mask |= 1 << (9 - x + y)

x_mask, y_mask はそれぞれ縦消し, 横消しが発生する可能性のある列を表します. z_mask, w_mask はそれぞれ左上から右下, 左下から右上に向かっての斜め消しが発生する可能性のある列を表します.

ビットが立っている縦, 横, 斜めだけを調べ, さらに横と斜めは x_mask によって調べる範囲を絞ることができます.

これらの評価関数の調整や高速化により, 20 ターン発火での平均得点は 400 点程度から 730 点程度まで上がりました.

また, 20 ターン発火では火力不足で負けてしまう事があったので, 最終的には 22 ターン発火にしました. 22 ターンでは平均 1070 点程になりました.

今夜勝ちたい実装

エキシビジョンマッチ向けに実装した勝ちたいだけのちょっとズルい実装です.

ルール的に禁止していないことを確認した上で実装しています.

相手の思考時間を使う

ゲーム開始直後の 1 ターン目で 22 ターン先まで探索して落とし方をキューに入れておき, その後は邪魔ブロックが降ってこない限り, キュー入れた落とし方を再生するような実装になっています.

相手が 1 ターンあたり 1 秒つかって探索してくるタイプの場合, 20 秒程 CPU が暇することになります.

なので, キューの内容を再生中も裏で探索を継続しておき, よりよい連鎖が見つかったら途中で乗り換えるという実装をしました.

相手に合わせて発火ターン数を調整する

十分にダメージが与えられる量のお邪魔ブロックを送ることができるなら, 先に発火したほうが勝ちやすいルールになっているように思います. なので, 対戦時のログをファイルに記録しておき, 以下のルールに従って初回攻撃のターンを調整する機能を実装しました.

前回の対戦で勝利した場合

  • 前回の初回攻撃のターンを使用する

前回の対戦で敗北した場合

  • 相手の方が先に初回攻撃を行っていたら, 相手の初回攻撃ターンでこちらも初回攻撃を行う
  • 自分の方が先に初回攻撃を行っていたら, 初回攻撃ターンを +1

ボツネタ

考えたけど上手く行かなかった事, 実装できなかったことなどです.

発火点を成長させる

連鎖を組んでいく際に, 発火点を成長させるのと, 連鎖尾を成長させるのでどちらが強いのでしょうか.

当たり前のことですが, 上から落下させたブロックは, 下から上に積み上がっていきます.

発火点を固定し, 連鎖尾を成長させる場合, 発火点は必然的にフィールドの下の方になってしまいます. フィールドの下の方でブロックを消すと, 上に乗っているたくさんのブロックが落下し, 多くのブロックがズレることになります. たくさんのブロックズレるということは, たくさんのブロックが消える可能性があるわけですが, 連鎖数を稼ぎたいので, 同時消しではなく 2, 3 個ずつ消えて欲しいです. 一度ズレたけど消えなかったブロックは, 今後同様のズレの状態になったとしても消えません.

この消えないズレの組み合わせが徐々に連鎖の成長を制限するため, 連鎖尾を成長させる方針はある程度で頭打ちになります.

発火点を成長させていくと, 発火点が上になるためこの制限を受けづらくなり, フィールド全体を使って連鎖を作ることができます.

ズレの理論に関しては, CODE VS 2.0 の colun さんの説明がわかりやすいです.

また, 連鎖尾の同時消しはスコアにそこそこ大きな影響を与えます. 発火点を伸ばす場合, 連鎖尾の同時消しを作った後に発火点を伸ばすことができるので, 高いスコアを取りやすいと考えられます.

つまり, 発火点を成長させる方針は, 以下のようなメリットがあります.

  • フィールド全体を使って大連鎖を作ることができる
  • 連鎖尾の同時消しを作ることができる

一方で, 連鎖尾を伸ばす連鎖は, 以下のようなメリットがあります.

  • 少ないブロックで柔軟な連鎖を組みやすい
  • 発火点を固定することで評価が高速にできる
  • 邪魔ブロックが降ってきても発火点さえ潰れなければ連鎖は残る

CODE VS 2.0 予選では発火点を成長させる連鎖が強かったのですが, 今回の CODEVS for STUDENT では, スコアに対して相手に送る邪魔ブロックの数が多く, 素早く連鎖を作ることが大事だったため, 連鎖尾を伸ばす方針にしました.

マルチスレッド化

CPU を使い切るために, 探索をマルチスレッドで動かす実装を行いました.

できるだけロック時間が減るように探索の仕方を工夫し, 手元の PC では 2 スレッドにした際に約 2 倍探索できるようになりました.

しかし, 決勝の環境はハイパースレッディングの 2CPU だったので, 1 スレッドと比べて 1.2 倍程度にしかなりませんでした.

プログラムが複雑になってしまったのと, 探索順序が実行するたびに微妙に変わってしまい, 他のチューニングがやりにくくなってしまったので破棄しました.

ベクトル演算による高速化

連鎖のシミュレーションのうち, 一番ボトルネックになるのは, 加算や減算をたくさんして部分和が 10 になるところを探す消去判定です.

加算や減算をたくさんするので, ベクトル演算を使いまとめて演算を行うことで高速化できるはずだと考えて, 3 日ほど AVX2 の関数の使い方を覚えベクトル演算版のシミュレータを書きました.

消去判定のしゃくとり法において, 10 を超えた部分に関して戻って減算をする部分でどうしても演算回数を減らすことができず, 影響範囲のあるところだけ調べるナイーブなしゃくとり法に比べ, 1/3 程度の速度しか出せませんでした.

もっとベクトル演算に慣れて, ベクトル演算しやすいアルゴリズムを思いついたら速くなるかもしれませんが, 今回はあきらめました.

評価関数の自動調整

各評価パラメータの重みが, 比較優先度を決めているだけになってしまいました.

ブロックの位置関係など, もう少しパラメータを増やし, 重みを機械学習的な手法での自動調整に挑戦してみたかったです.

僕が今夜勝ちたいだけの実装をしている間に, 決勝に出場する piroz さんはこの自動調整に挑戦していたらしく, とても尊敬しています.

結果的にうまく調整できなかったようですが, コンテストを通しての成長という観点で僕は完全に負けています.

pirozさんの記事

ソースコード

エキシビジョン提出時のソースコードを公開します.

後に気づいたバグがそのままだったり, 人に読ませるつもりのない思考ログやコミットログが残っていますが, コンテスト参加時のリポジトリをそのまま公開します.

https://bitbucket.org/mecha_g3/codevsfs/src

まとめ

明日は CODEVS for STUDENT の決勝イベントが行われます. エキシビジョンマッチで僕の今夜勝ちたいAIが優勝者と対戦します.

生放送是非観てください!

2017年は機械学習勉強するぞ!!!!


@mecha_g3

(本稿はKLab Advent Calendar 2016 の4日目の記事になります)

Travis CIはよく知られたCIサービスの一つです。読者の方々の中にも、個人的なプロジェクトのCIに利用している人は多いのではないでしょうか。一方で、設定ファイル .travis.yml 中に秘密情報を暗号化して記述できることはあまり知られていないかもしれません。

YAML中での暗号化のやり方はTravis CIのドキュメント「Encryption keys」にも書いてあるのですが、 travis encrypt コマンドによりAPIトークンなどの秘密情報を暗号化して .travis.yml 中に記述するような仕組みになっています。この情報はTravis CI側で復号されてCIプロセス中で利用することができます。

今回指摘する内容は、この暗号の強度が多くのプロジェクトにおいて不足しているのではないかという点です。というのも、2015年4月以前に作られたTravis CIプロジェクトではRSA 1024bit鍵による暗号化が利用されているのです。

本稿ではTravis CIの暗号化の仕組みを簡単に説明した上で、どういうときに危険性があるかの詳細と鍵ペア再生成の方法を紹介します。

travisコマンドの概要

まずは travis コマンドを用いた暗号化について簡単に紹介します。 travis コマンドというのは gem install travis でインストールされるRuby製のコマンドで、内部的にTravis CIのAPIを叩いて色々とよしなにやってくれる便利コマンドです。

そのサブコマンドの一つが travis encrypt で、これを使えば任意の文字列を暗号化して .travis.yml 中に記述することができます。たとえば、Travis CI連携しているGitHubプロジェクト直下のディレクトリで下記コマンドを実行してみましょう。

$ travis encrypt 'FOO=secret_information'
Please add the following to your .travis.yml file:

  secure: "BEC97APcjoBsKRRGS4DCcQoLCviHTzK88JxfEq0wDfJ4+kfuLktyXEbHbG6Ct9cP+KLnwxIDBamf0pgOS7iQGLLb5Irn00fn4JEBeHd6kyTXQbyuPSe/NffVceg5vq8RWPT8nlWzVHD3wtjJFWz/Ocm6q5RkqvOtLszwM1Nc0Ig="

上記コマンドで出力された行を .travis.yml 中に書けば FOO=secret_information と書いたのと同じ意味になります。この仕組みにより、APIトークンやメールアドレスなどをYAML中に記述して外部サービスとの連携に使うことができます。

travisコマンドによる暗号化の中身

さて、この暗号化はどのような仕組みなのでしょうか? travis コマンドの中身を見ると、その正体がRSAを利用した公開鍵暗号であるとわかります。具体的には、travis encrypt コマンドが公開鍵で暗号化を行い、暗号文を受け取ったTravis CIが秘密鍵で復号するような仕組みになっています。

この暗号化に用いられるRSA鍵ペアの生成はTravis CI内部で暗黙に行われており、秘密鍵はTravis CIだけが持っています。つまり、公開鍵で暗号化した文字列は暗号化した本人でさえ復号できず、Travis CIだけが復号できるというわけです。

言い換えると、この暗号化を信用するためのポイントは2点だと言えます。

  1. Travis CIが秘密鍵や復号済みデータを流出させるようなことが無い
  2. 公開鍵への攻撃により秘密鍵を求められるようなことが無い

これさえ守られていれば、暗号化された情報は安全だと言えるでしょう。逆に上記のうちどちらかの問題が発生するようだと、秘密情報を .travis.yml に平文で書いたのと同じということになりかねません。本当に大丈夫なのでしょうか?

実は2012年に少数のプロジェクトで1の問題が発生したことがあるようですが、今後は安全だと仮定するしかないでしょう。今回は2の問題について議論していきます。

公開鍵への攻撃の可能性

今回指摘したいのは、RSA公開鍵への攻撃の可能性についてです。

まずはTravis CIの公開鍵にアクセスしてみましょう。リポジトリに紐付く公開鍵は下記のようにAPI経由で取得できます(参照:「Travis CI - API Reference - Repository Keys」)。URL中の hnw/php-build というのがユーザー名/リポジトリ名になっています。

$ curl -s https://api.travis-ci.org/repos/hnw/php-build/key | jq -r '.key'
-----BEGIN RSA PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDOQb++oR7aBL6TfjSZbo/ssNrE
sV9FJmOn5TZktfAgLFv7T5c93Iot1k6ha7OO0FaZyf67bR+5Nou4Vd4SaiFpvb38
NMj4Pz9Smdwi3pWisqcgZaQOOpe9IB0nTAGhzZp8+2EPC1syRUi30FXOD03xnL0q
X8rhgIkuD6415tGP3QIDAQAB
-----END RSA PUBLIC KEY-----
$

ところで、この公開鍵は短そうに見えますよね。実はこれが1024bit鍵です。RSA 1024bit鍵の素因数分解は現代のスパコンでもかなり手強い計算ではありますが、コンピュータの性能向上により5年後なり10年後なりには現実的に攻撃可能になると考えられています。「暗号の2010年問題」の頃に2048bit以上の鍵への移行が叫ばれたことを考えても、1024bit鍵の寿命が近いのは間違いないと言えるでしょう。

ちなみに、鍵ペアが1024bitになっているのは一定以上古いプロジェクトだけです。最近のプロジェクトでは4096bit鍵が利用されていますので、今後プロジェクトを作る分にはこの問題はありません。正確な時期はわかりませんが、2015年4月頃に4096bit鍵に切り替わったように推測しています。

ご自分のプロジェクトの鍵ペアの鍵長を確認したい場合、上のようにAPIから公開鍵を確認すればわかります。公開鍵がASCIIで210文字くらいだったら1024bit鍵、730文字くらいだったら4096bit鍵ということになります。わざわざ鍵を確認しなくても、 travis encrypt の結果の長さも鍵長と同じサイズになるので、そこからも判断できます。

鍵の中身を正確に把握したい場合は openssl asn1parsedumpasn1 などを試してみてください。

鍵ペアを再生成する手順

もしかすると短い鍵ペアを使っているプロジェクトが見つかったかもしれませんね。もしそうだとしても、鍵ペアを再生成するAPIが提供されていますので安心してください。ちなみに、現時点ではAPIを直接叩く以外の方法は提供されていません。

鍵ペアの再生成APIを利用するにはAPIトークンが必要です。これは travis login コマンドでログインした後で travis token コマンドを実行することで取得できます。このAPIトークンを下記のように Authorization: ヘッダから送信すればAPIの認証が成功します。ただし、下記の hnw/php-timecop はユーザー名およびプロジェクト名です。

$ curl -d '' -H 'Authorization: token abcdefghijkl1234567890' -s https://api.travis-ci.org/repos/hnw/php-timecop/key

API呼び出しに成功すると新たな鍵ペアが生成されます。失敗した場合はおそらくトークンの指定が間違っています(ヘッダ中の token は消してはいけません)。元の鍵ペアが1024bitだった場合でも再生成すれば4096bit鍵になりますので、あと20年くらいは安心だといえそうです。

もちろん、鍵ペアを再生成した場合は改めて秘密情報の暗号化を行う必要があります。また、古い暗号文を遠い将来第三者が解読する可能性まで考えれば、暗号化対象のパスワード変更やAPIトークン再発行なども合わせて行うのが良いでしょう。

まとめ

  • Travis CIのtravis encryptによる暗号化にはRSAが使われている
  • そのRSA鍵の鍵長が1024bitのことがあるので要確認
  • 1024bitでは暗号強度の観点から中長期的に不安
  • 2015年4月頃までにTravis CI上に作られたプロジェクトが該当
  • API経由で鍵ペアの再生成ができる
  • 再生成すると4096bit鍵になる

本題とはズレますが、RSAを署名ではなく暗号化に使っている事例は比較的珍しい気がするので、その観点でも面白い話題といえるかもしれません。


@hnw

(本稿はKLab Advent Calendar 2016 の1日目の記事になります)

今年もアドベントカレンダーの季節がやって来ました。お祭り騒ぎと年末進行の勢いで、普段は書かないような変なネタも含めてKLabエンジニア陣が多彩なエントリーをお届けします。

最初は昨年に引き続きKLabアドベントカレンダーの旗振り役をさせていただいてますoho-sからです。 よろしくお願いします。

はじめに 「進歩した科学は魔法と見分けがつかない」

皆さんは不老不死に興味はありますか? 古今東西、人々は伝説に宗教にそして現実にそれを追い求めて来ました。 でも、そもそも「生きている」ってどういうことなのでしょう。 現代の生物学においても、その定義は非常に曖昧です。 特に急進的な主張としては、一個体の生物を構成する物理化学生物的な現象を全て完全に正確に模倣したら、それは生きているということではないか、というものです。 そして、その主張にのっとり、近年のSFで科学的に現実的? な方法としてよく見られるのが、脳のコンピュータへのアップロードです。

これは、人間の脳を構成するあらゆる情報をデータ化したうえで、ものすごい性能のコンピュータにシミュレーションさせることで、人間がコンピュータ上で生きていけるのではないか、という考え方です。 そうすれば、コンピュータ上のあらゆるデータと同じように、バックアップもとれるし、コピーもできるし、電源がある限り動き続けられるし、これ、不老不死じゃん! ということです。

もちろん、人間の神経細胞は140億個以上あるといわれ、それら同士の結合はさらに大変な数になります。当然現在の最高性能のコンピュータをもってしても、シミュレーションは不可能です。 さらに、神経細胞同士の接続の性質も完全にわかっているとはいいがたい状況です。

では、一方で、「その人」を維持するに足る正確さで各神経細胞や、その相互の接続の情報を取得することができるのでしょうか?

生物学の世界では、生物の全遺伝情報、いわゆるゲノムがデータ化できた現在、次のターゲットは、脳の神経細胞とその全結合情報のデータ化となっています。 電子顕微鏡やその他の観測手段を用いてデータ化していくのです。 そして、一個体の全神経細胞とその全接続の情報のことを「コネクトーム」と呼び、一部の生物のコネクトームはインターネットで公開さえされているのです。

つまり、自分自身のコネクトームを作成し、それをシミュレーションできたら不老不死になれそうです。 そう、SFの登場人物たちはきっとこうやって、いとも簡単に脳をコンピュータにアップロードし、コピーし転送しバックアップしアップグレードし、そして進化しているのでしょう。

しかし、ちょっと待ってください。エンジニアなら「それどんなミドルウェア使ってるの?」って気になりませんか? RDBMSのバックアップさえ一仕事なのに、一貫性や同一性やその他諸々は本当に大丈夫なのか。むしろどんなデータ構造なのか。 気になるところはいくらでも出て来ます。 そこで、GraphDBです。

コネクトームとGraphDB 「電気線虫はアンドロイドの夢を見るか?」

まずはコネクトームとはどのようなデータになるのかを具体的にお話ししましょう。 現在、公開されているコネクトームとしては、線虫( C. Elegans )のコネクトームがあります。 線虫は体長1mmほど、全細胞数およそ1000個、神経細胞は300個くらいです。 コネクトームの実態は、神経細胞をノード、それらの接続をエッジとしたグラフデータです。エッジの属性として、電気的・化学的結合の強さが格納されます。 つまり、このようなデータを永続化したり神経活動をシミュレーションしたりなどの様々な処理をするには、グラフデータを扱うのに適したデータ管理システム、そう、GraphDBがあればいいということになります。

GraphDBとは、その名の通りグラフデータ構造を格納して処理するためのDBMSです。 有名なものとしては、Neo4jなどがあります。 ノードとエッジからなるネットワークのようなデータ構造を、テーブルからなるデータ構造を管理するRDBMSのように、管理するものです。 RDBMSでグラフ構造を扱ったことのある人は、その煩雑さと低性能に悩まされたことがあるかもしれません。RDBMSでグラフ構造を扱うのは非常に難しいと言われています。GraphDBは最初からグラフデータ構造を扱うために設計されているために、はるかに速く安全にグラフ構造を管理できるのです。

GraphDBや、グラフデータを扱う仕組みは、例えばSNSのユーザー間の関係性であったり、検索対象ドキュメントの間の関係性であったりと、様々な対象が現実にあり、近年利用が進んでいる分野です。

ということで、本稿では、Go言語でグラフデータを格納し永続化できる簡単な仕組みを実際に作成し、そこに線虫のコネクトームを格納して、その線虫を不老不死にしてみましょう。

GraphDBを作る 「Connect! A to B」

まずはバックエンドのデータストアを決めます。もちろんここでグラフに向いた独自データストアを作り始めてもいいのですが、今回はLevelDBを使います。GoでLevelDBを使うに当たっては、公式ドキュメントと、こちらを参考にしました。 LevelDBはいわゆるKVSなので、グラフデータに適したキーを設計しなければいけません。今回は、以下のようなキーの構造にしました。

ノード

Name Size Description
NodePrefix=0x01 1byte プリフィックス
type 2byte タイプ
ID 4byte ID

エッジ

Name Size Description
EdgePrefix=0x02 1byte プリフィックス
FromID 4byte 始点ノードのID
Direction 1Byte エッジの方向
Type 2Byte タイプ
ToID 4byte 終点ノードのID

LevelDBはバイト列の前方一致による検索が可能なので、これで、

  • ノードの全検索
  • ノードのタイプによる検索
  • ノードのタイプとIDによる検索
  • エッジの全検索
  • エッジの始点IDでの検索
  • 始点IDと方向での検索
  • 始点IDと方向とタイプでの検索
  • 始点IDと方向とタイプと終点IDでの検索

ができます。 一方で、このキー構造だと、終点から始点への検索ができないので、エッジを格納するときは、始点から終点までの前向きエッジと終点から始点までの後ろ向きエッジを同時に格納することで解決しました。

こちらが、実際に作ってみたリポジトリです。

以下にノードとエッジのコードを提示します。

また、DBのOpenや検索等のためのコードが以下になります。

公開されている多くのGraphDBには、ロックやトランザクションや各種制約、クエリ言語やサーバーとして動かすための仕組みなどがありますが、今回はLevelDBにグラフ構造を永続化して、簡単なノードの検索とエッジの検索ができるようになったということで、ここまでにしておきます。

線虫のコネクトームを格納する 「発進」

線虫のコネクトームデータはこちらのものを利用しました。オリジナルのデータはdoi: 10.1126/science.1221762.を参照してください。dot形式のデータで取得してテキストエディタ等で置換とマクロでTSV形式の単純なデータに変換しました。

実際のデータはこのようなものです。

これを作成したGraphDBに読み込みます。

読み込みは以下のようなコードを書きました。

// InputFromTSV read two tsv files (node and edge) and insert node and edge into DB
func inputFromTSV(db *graphdb.GraphDB, nodefilename string, nodetype int16, edgefilename string, edgetype int16) {
    nodefile, err := os.Open(nodefilename)
    if err != nil {
        panic(err)
    }
    defer nodefile.Close()

    nodereader := csv.NewReader(nodefile)
    nodereader.Comma = '\t'
    for {
        record, err := nodereader.Read()
        if err == io.EOF {
            break
        } else if err != nil {
            panic(err)
        }

        node := &graphdb.Node{
            Nodetype: nodetype,
            ID:       no2ID(record[0]),
            Value:    []byte(record[1]),
        }
        db.AddNode(node)
    }

    // ==========================================================================

    edgefile, err := os.Open(edgefilename)
    if err != nil {
        panic(err)
    }
    defer edgefile.Close()

    edgereader := csv.NewReader(edgefile)
    edgereader.Comma = '\t'
    for {
        record, err := edgereader.Read()
        if err == io.EOF {
            break
        } else if err != nil {
            panic(err)
        }
        db.AddEdge(no2ID(record[0]), no2ID(record[1]), edgetype, []byte(record[2]+","+record[3]))
    }
}

func no2ID(no string) []byte {
    noint, _ := strconv.Atoi(no)
    id := make([]byte, 4)
    binary.LittleEndian.PutUint32(id, uint32(noint))
    return id
}

そして、まずは永続化した線虫のコネクトームをDOT形式で書き出し、データの確認をします。

func main() {
    db, _ := graphdb.Open("celegans.db")
    inputFromTSV(db, "c.elegans_neural.male_node.tsv", NEURON, "c.elegans_neural.male_edge.tsv", CONNECTION)

    db.PrintGraph2DOT()
}

以下が出力したdot形式ファイルをGraphVizで書き出したものです。

C. Elegans のコネクトーム

次に、ノードに接続するエッジを検索してみましょう。 コードはこんな感じになります。

func main() {
    db, _ := graphdb.Open("celegans.db")

    start := db.GetNode(NEURON, no2ID("0"))

    edges := db.GetNodesEdge(start)

    for _, edge := range edges {
        fmt.Println(graphdb.Byte2string(edge.To))
    }
}

さらに、こうすると、Hop数を変えてノードを羅列できます。

func main() {
    db, _ := graphdb.Open("celegans.db")

    start := db.GetNode(NEURON, no2ID("0"))
    fmt.Println(graphdb.Byte2string(start.ID))

    rec(db, start, 0, 4)
}

func rec(db *graphdb.GraphDB, node *graphdb.Node, curdepth int, maxdepth int) {
    curdepth++
    if curdepth == maxdepth {
        return
    }

    edges := db.GetNodesEdge(node)

    for _, edge := range edges {
        fmt.Println(strings.Repeat(" ", curdepth) + graphdb.Byte2string(edge.To))

        next := db.GetNode(NEURON, edge.To)

        rec(db, next, curdepth, maxdepth)
    }
}

いかがでしょう。これで僕のPC上で線虫が不老不死となりました。

・・・そんなわけない!

まとめ 「未来を予測する最善の方法」

今回はあくまでも実生物の神経ネットワークを使いやすい形で静的に永続化したにすぎず、生物のダイナミックな「生きている」という性質は、シミュレーションを動かしたりしなければ得られません。不老不死ははるか遠い。

今回、なぜこのような題材を選んだかということを最後に説明しないと意味不明になりそうなので、説明しておきます。

まず、GraphDBに興味があったこと、特に、今あるものを利用するのではなく、どう実現されているのかに興味があったので実際に作ってみようと考えました。 また既存のGraphDBはサーバーを立てて使うものが多いのですが、プログラム組み込みで使うものが欲しかったというのもあります。 と言っているところで、EliasDBというものを見つけました。 今回は、それRDBMSでもいいじゃんというレベルしか実装できませんでしたが、GraphDBはとても面白いので、今後も勉強していきたいと思います。

そして、GraphDBを使った題材を考えていて、コネクトームというものを知り、これだ!と思ったわけです。 コネクトームに関しては、ロボットに線虫のコネクトームを元にしたシミュレーションシステムを接続し、電子線虫サイボーグを作る研究など、とてつもなく未来な研究が実際に行われています。例えば、ここです。興味のある方は調べてみると面白いと思います。

それでは、引き続きこの後のKLabアドベントカレンダーをお楽しみください。

はじめに

オンラインゲームでは、金貨やコインといった ゲーム内で通貨のように利用可能なアイテム が必ずといって良いほど登場しますよね。
先日もこのTech Blogにて、ゲーム内で発行する仮想通貨のデータ分析業務についての記事が掲載されましたが、ゲーム内通貨の運用は各種法令等も絡むことからKLabでも特に注意して取り扱いを行っている業務の一つです。

そこで今日はゲーム内通貨の運用に関する業務やKLabとしての考え方を、会計上の観点からもう少し掘り下げてご紹介します。

法令におけるゲーム内の金貨等の位置づけ

オンラインゲーム内でアイテムの購入に利用できる金貨やコインといったトークン、上記ではゲーム内通貨と言いましたが、これは資金決済に関する法律(以下資金決済法、とします)で各種の規定がされています。そして上記のようなゲーム内の概念は、この法律の中では前払式支払手段という名前で定義付けがされています。

前払式支払手段の定義について条文にはちょっと難しく書いてあるのですが、できるだけウソにならない範囲で簡単に書くと以下のようになります。

  1. 発行の際にその金額・数量等が紙の証票(例としてチケットなど)またはデジタルデータとして記録されていること
  2. 1が対価と引き替えに利用者に対して発行されていること、つまり有償であること
  3. 1の内容と紐付く、IDや番号などが発行されていること
  4. 物品の購入やサービスを受ける際などに利用できるものであること

※ただし上記を満たす場合でも使用期限が6ヶ月以内のものは原則として対象外

上記はゲーム内の金貨やコインだけでなく、デパートなどで発行される商品券や交通機関などで使える回数券やプリペイドカード、英会話教室の前売りチケットなども上記の特徴を持つことが多く、つまりこれらは法律上同じ立て付けのものということになります。

ところで今年5月に改正資金決済法が成立し、bitcoinといった近年出てきた新しい決済手段に関する条文が追加されました。そこで このbitcoinのような新しい概念は、条文の中では 「仮想通貨」 という名称で定義づけされました。
「仮想通貨」というとどこかで聞いたような名前ですが、ここでいう仮想通貨とは前述の特定のサービスやゲーム内でのみ利用可能な前払式支払手段とは法律上別物でして、ここでは不特定の相手に対して利用可能なものを指します(正確にはそのほかにも仮想通貨を構成する要件があります)。従って法令改正後も前述の前払式支払手段に関する条文はほぼそのまま残っています。

なお冒頭でご紹介した記事のように、我々KLab内部ではゲーム内の金貨やコイン=前払式支払手段のことは 仮想通貨 と呼んでいるのですが、上記の法令改正の絡みもあり誤解を招くためこの記事の中では、法令に則り正確に 前払式支払手段 と呼ぶことにします。

オンラインゲームでの前払式支払手段の会計処理

当たり前の話ですが、会計において一番大事なことは売上を正確に勘定することです。
会計上、売上を計上するタイミングは物販業においては引渡基準と言って 原則的には役務提供(顧客に対してサービス提供や商品の引き渡しを行うこと)を完了した時点とすること になっています。
例えば自動車の販売を行った場合、売上を立てるのは契約日や入金日ではなく、この引渡基準に基づいて登録日あるいは実際に顧客に対して車の引き渡しを行った日とする運用にするのが一般的です。

この点でオンラインゲームにおける前払式支払手段は次のように若干特殊な性格を持ちます。
※ゲームにおける金貨やコインなどは無償でも配布することもありますが、ここでは有償で販売された物で無償配布のものは含まない(本来の意味での前払式支払手段)前提で説明します

オンラインゲームにおける前払式支払手段の流れ

上記で示すようにオンラインゲームにおける前払式支払手段とは、iOSのApp StoreやAndroidのGoogle Playストアのようなスマートフォンでの「決済プラットフォーム」によるアプリ内課金や、その他クレジットカード決済などで販売される「商品」です。しかし、顧客がその「商品」(例えばコインや金貨)を購入する動機は、これそのものにあるわけではなく、これと引き替えに何かアイテムを購入したい、とかサービスを受けたい、というさらに別の目的にあるわけです。
逆にコンテンツ提供者=”前払式支払手段の提供者”の視点からすると、前払式支払手段を販売しただけの段階では商品を引き渡す義務がまだ残っている状態だと考えることもできます。
つまり前払式支払手段は債権を表章した有価証券のようなものと考えると、前払式支払手段販売時点ではゲーム内における役務提供は完了していないため、この時点では売上計上は まだ できない、と考えるのがもっとも合理的なのです。最初にプリペイドカードや英会話前売りチケットのことを書きましたが、これと同じと考えればわかりやすいと思います。

これを会計的に整理して言うと、顧客が「決済プラットフォーム」で代金を支払った時点でまもなくコンテンツ提供者への入金は発生するもののこの時点では役務提供の終了(=売上発生)とは見做さず、 前受金処理(負債として計上すること)を行う 、ということになります。つまり顧客が実際に前払式支払手段を利用してアイテムを引き替えた時が前受金を取り崩すタイミング=すなわち売上として計上を行うタイミングになるわけです。

帳簿イメージ

この項の最初に書いた通り、会計において一番重要なことは売上を正確に計上することです。従って前払式支払手段を利用した業務においては、販売・発行時の情報と同じくらい、 消費した際の情報が大事 だということがおわかり頂けると思います。

前払式支払手段の残高管理と売上計上

多くのゲームにおいて前払式支払手段はまとめ売りの際に値段を下げることがあります。
例えば、金貨100枚だと120円(@1.2円)だが1,200枚まとめて買うと1,000円(@1.0円)になってお得!……といった販促方法は一般的にもよく見られるものですよね。

どの価格で購入した前払式支払手段であっても同様に利用できるという前提であれば、アイテム引き替えの実装だけを考えた場合、その前払式支払手段の総残高さえ分かればよいことになります。つまりそれぞれの単価を気にする必要は無いわけです。
一方で、上記で述べたように「売上を正確に計上する」という観点ではその実装だけでは問題が発生します。

  • それぞれ「単価いくらの前払式支払手段が消費されたか?」を記録しておかないと売上が正確に計上できない
  • 単一アプリにおける同一の前払式支払手段でも、Google PlayストアとKindleアプリのように購入元の「決済プラットフォーム」が混在することがある(売上は「決済プラットフォーム」毎にそれぞれ勘定するのが妥当であるため)

……ということがあるためです。そのため単に総残高を記録するだけでなく、前払式支払手段の種類別の 預入・払出のルールが厳密に運用 されていなければなりません。
このような要件は実は一般的な商品の在庫管理の考え方そのものでして、これに倣って前払式支払手段の残高を管理していくことになります。

在庫管理の基本的な方式は先入先出法、後入先出法、移動平均法といったものがありますが、「古いものから消費」というルールが顧客から見て最も自然でわかりやすいこと、預入・払出記録の1:1の突き合わせができるため詳細なトレース・分析が行いやすいなどの理由からKLabでは現状ほとんどのケースで先入先出法を採用しています。

さて、会計上一般的に在庫の管理を行う場合には 商品有高帳 という帳簿を作成します。前払式支払手段の内部管理においてもこれとほぼ同様のことを行うことになります。
例として、とあるゲームの金貨(=前払式支払手段)の預入・払出のサンプルを以下に示します。

金貨預入・払出サンプル

上記の例では、顧客であるユーザAは日を分けて金貨をそれぞれ100枚(単価 @1.2円)、1,200枚(単価 @1.0円)と購入し、その次の日にアイテムショップで500枚消費しています。ということで残高は (100+1,200)-500=800枚になる計算です。
肝心の内訳としては、先入先出で古いものから消費していくルールですから、単価1.2円の金貨全部と単価1.0円の金貨一部が取り崩され、後者が800枚だけ残ることになります。

このデータさえあれば売上を導出するのは簡単ですね。上記の「払出」に当たるログを全顧客分サマリーすれば良いわけです。
さらにここから前払式支払手段の残高も明らかなので、これをエビデンスとして資金決済法で定められた供託(後述します)を行うことができます。

その他資金決済法で定められた業務

これまで前払式支払手段の会計業務についてご説明してきましたが、KLabは資金決済法で定められる自家型の前払式支払手段発行者に該当しますので、上記の他にも法で定められた業務が存在します。
以下に簡単にご紹介します。

前払式支払手段の発行届出

資金決済法では基準日(毎年3月末・9月末)未使用残高が1,000万円を越える自家型発行者は、管轄の財務局長に前払式支払手段に関する情報(前払式支払手段の名称やその単価等)を書面で届出することが定められています。
KLabもこれに当たるため、関東財務局に前払式支払手段に関する届出を行っています。

発行保証金の供託

資金決済法では、保証金として発行済みの前払式支払手段の1/2以上の金銭を供託することが定められています。上述の発行届出と同様に、基準日残高でこれを計上します。
これは利用者保護のための運用で、万が一発行者が破綻してしまったような場合にはここから優先的に配当を行うことで、支払い済みの代金を丸損しないようにしているというわけです。

前払式支払手段の払戻し

銀行法や出資法という法律では、免許なしに事業として他人のお金を預かったり、その払戻しを行ったり、または送金をしたりすることを禁止しています(これらは社会的影響が大きな業務だからです)。
もし前払式支払手段がいつでも自由に払戻しができてしまうと、一旦それを購入後、好きなときに現金化ができるということになります。これは上述したお金の預入と実質同じことができることになってしまうため、発行済みの前払式支払手段を払戻しすることは資金決済法で禁止されているのです。この規定があるため、前払式支払手段の発行業者は滅多なことでは払戻しに応じることはありません。

一方で発行者が前払式支払手段を廃止した場合には、上記の例外として利用者に対して購入済みの前払式支払手段について払戻しの対応を行わなければならないことになっています。
利用者は発行者が設定した申し出期間(法令上最低60日間)内に申し出を行うことで払戻しが受けられることになります。
ちなみに、払戻しの期間を超えた場合は除斥(当該利用者を通常の払戻し手続きから除外して、相当する額の前払式支払手段残高を控除すること)することが可能になります。除斥された場合でも民法上の債権が消滅するわけではありませんが、この期間を過ぎてしまうとお金の回収が面倒になってしまいますので、未使用分の前払式支払手段があるサービスが終了してしまった場合は、事業者の告知を見たらぜひ早めに手続きをしてくださいね。

おわりに

アイテム課金型オンラインゲームというジャンルは、比較的新しい業態であるため日々利用者を保護するための新たな法令やガイドラインが検討されています。一方で、バックエンドの業務は通常の商品の在庫管理や売上管理等と考え方に大きな差はありません。
みなさんが前払式支払手段に関する業務を行うことがありましたら、この記事を何かの参考にしていただければ幸いです。


Shimanuki

Introduction - はじめに

R&D部に所属しつつ、社内のPlaygroundエンジンを開発しているピコアです。 今回のテーマは最新描画APIのVulkanです。 Vulkanはグラフィックス処理APIトレンドの最先端にあるもので、ゲーム開発者が従来よりもGPUのパワーをきめ細かく制御できます。 そのかわりに開発者がさまざまな責任を負うことにもなります。

Vulkanの使い方や細かなAPIの説明などはネット上のあちこちで見つけられるので、今回はあまり触れません。 この記事の目的は、ゲームエンジンの開発者が実際にVulkanを利用した感想、学習中に得られた経験・苦労、そしてVulkanの良いところについての知見の共有です。

History of 3D Graphic APIs - 描画APIの歴史

まず、グラフィックスAPIの歴史と変遷をおさらいします。 内容は設計思想の流れと時代に応じた変化を理解するための概要に留めます(例えば3DfxのGlideは無視する)。

OpenGL : ご先祖です。1992年1月に公開されました。 当時のGPUの設計は現在とかなり違っていて、決まった固定パイプラインで描画するAPIでした。 制限もありましたが、OpenGLの良いところはそれ以前の仕組みと違って、ハードウェアに依存しないAPIだったことです。 そして関数ポインターを利用して拡張機能を簡単に追加できる仕組みがあります。 そのおかげで、登場から25年近くが経った現在でもOpenGLにはまだ未来があります。 時代と共に技術の進歩へ追従してきた、現代のハードウェアでもまともに動くAPIです。

Windowsの世界ではDirectXが1995年にMicrosoft社から発表されました。
MicrosoftはDOSからWindowsへの移行にあたり、WinGやDirectDrawのAPIを公開したのちに、3D描画のAPIを公開しました。
DirectXの枠組みは音声、コントローラー入力など多くの仕様を含みますが、今回はDirect3Dの話です。 Microsoft社が設計したDirectXの大前提は、COMを使ったOOP(Object Oriented Programming)モデルです。
そして、ハードウェアの進化にあわせて、徐々に新しいバージョンのDirectXをリリースするという方針です。
OS上には以前のDirectXもインストールされており、古いアプリケーションがそのまま動作します。 APIを維持して前のバージョンとの互換性を保つという設計思想ではないことに注意が必要です。
DirectXは1995年~2003年の間に9個ものバージョンがリリースされました(DirectX 1.0 -> 9.0bまで全体的に24 Revisions)。
バージョンごとにAPI設計を大きく変更することで、OpenGLよりも理解しやすく時代にあわせて洗練されたAPI体系を保ってきました。 ドキュメントもOpenGLより詳細に書かれており、またWindows OSがゲーム環境として成長したことで、マーケットをリードするAPIとなりました。

Evolution of APIs over time

1. 固定パイプライン時代

パイプラインとは、頂点の入力からピクセルがバッファに描画されるまでの処理です。
登場当初のDirectXの機能はOpenGLと大差なく、ともに固定パイプラインを採用していました。

固定パイプライン時代は名前の通り、描画機能はすべてハードウェアに実装されたパスで計算され、それ以外の描画オペレーションは選択できませんでした。 このため、光の計算や素材の描画の仕方などはすべて固定されていました。

しかし、それでも進化は激しいものでした。 固定パイプラインといえども、DirectX 7まで変更がありました。そこから、たくさんの変更もありましたが、詳細はリンクを読んで下さい。

対するOpenGLについては、OpenGLのデータパスのイメージ図などを参照してください。

OpenGL対DirectXという枠組みでは政治的な戦いやマーケットでの力関係もあって、性能面やそのあとに出た新ハードウェア機能の対応面などOpenGLが遅れていた部分がありました。 しかし、OpenGLのメリットはWindows環境以外でも動作することであり、生き残る事は出来ました。

2. Shader誕生

DirectX 8.0からシェーダーが追加されました。 ポリゴンの頂点毎に自由な計算(プログラム可能)をおこない、さらに計算結果をピクセル毎に受け取って自由に処理(プログラム可能)できるようになりました。 これらをVertex / Pixel Shaderと呼びます。

OpenGLの世界でも、OpenGL 1.4からシェーダーをExtensionとしてサポートし、2.0から本体サポートに含まれるようになりました(2004年ごろ)。

3. パイプライン内にプログラム可能な部分を追加

その後に、Geometry ShaderやTessellation Shaderも追加されました。 一方DirectX 9まで存在した擬似的な固定パイプラインAPIがDirectX 10で削除されました(OpenGLES 1.x -> OpenGLES 2.xでも同じく、固定パイプラインの機能がAPIから消えた)。 つまりここでプログラマの学習ハードルが高くなりました。

これ以降、画面にピクセルを描画するためには自前でVertex ShaderとPixel Shaderを組むことになります。

4. ステートを先に持っていく、先に情報をフォーマットし、使いまわしの効率をあげる

さらにCPUの負担をおさえるために、ゲームプログラマ側では描画ステートの変更タイミングを変更する取り組みがおこなわれました。 従来は描画実行タイミングでおこなっていた処理を生成/設定タイミングへと移動し、実行時負荷を低減するというものです。

ここで「作成時」と「利用時」を分けて考えた場合、仮に「作成時コスト」が大きくなったとしてもそれはプログラム実行中に一度だけ必要なコストです。 描画に利用するデータを「作成時」に事前計算することで、何度も発生する「利用時コスト」を減らせるならば、積極的にコストを「作成時」へ押し付けて可能な限り「利用時コスト」を0に近づけるのが合理的です。

この考え方とトレンドはのちにVulkanまで続いていきます。

5. より自由度の高いGPGPU(General-purpose computing on GPU)のモデルも追加

そしてComputeモデル(OpenCLまたはNVIDIAのCUDAに近い)も描画APIに追加されました。 つまり描画専用ではなく、もっと自由な形でGPUに計算させる仕組みが追加されました。
これによって例えば物理計算、音声データ処理なども出来るようになりました。 描画の面でも、ピクセルに直接関係しない描画処理にもGPUを利用できます(例:グローバル・イルミネーションの計算)。

ハードウェア性能を限界まで引き出すという点については、歴史的に専用ゲーム機のほうが先行してきました。 PlayStation / Xbox世代のゲーム機においても、粒度のこまかなAPIが開発者へ公開され、性能向上に活用できました。 しかしPCの世界ではマシンごとにハードウェア構成が異なるため、粒度のこまかなAPIを提供することは難しいとされてきました。

6. OpenGLとDirectXに残された大きな弱点: マルチスレッド動作

さて、レンダリング機能が年々強化されていったOpenGLとDirectXですが、ともに処理モデル上の大きな弱点を持っていました。 複数のスレッドを利用して(マルチスレッドで)描画したいという開発者のニーズを満たすAPIの不在です。

まず、OpenGLにはマルチスレッドモデルがまったく存在しないに等しいです。 スレッドごとにContextを持つことはできますが、描画命令を実行できるOpenGLコンテキストはアプリケーション毎に1個しかありません。 マルチスレッドを活用しようとしても、2個目以降は非同期のテクスチャアップロードにしか使えないのです。 さらに、コンテキスト間でのテクスチャ共有には同期の面で注意を払う必要があります。

DirectXでは、DirectX 11からマルチスレッド描画がサポートされました。

DirectX 10までの世代とOpenGLをターゲットとするゲームエンジンのなかには、複数のCPUコアを有効利用するために自前の処理レイヤを構築しているものもありました。 具体的には、グラフィックスAPIを擬似的にマルチスレッド化するラッパーを作り、複数スレッドから描画コマンド列を作成する仕組みです。 記録した描画コマンド列はメインスレッドがまとめてGPUへ転送します。

この仕組みはゲームエンジン側のコードに新たなオーバーヘッド(データ構造をパースしてグラフィックAPI関数の呼び出しに必要な描画ステート、テクスチャ、モデル情報を検索するコスト)を生むため、非効率的と感じるかもしれません。 しかしこのコストを払ってもなお、自前でグラフィックAPI呼び出しに相当するコマンドキューを作成してデータ列を詰め込み、レンダリング用のスレッドがそれを取り出して利用するという仕組みには処理効率上のメリットがありました。

さて、先にDirectX 11でマルチスレッド描画がサポートされたと述べました。 このなかでモデルを変更してコマンド列を貯める仕組みが追加されましたが、貯めたバッファのコミットは依然としてメインスレッドの役目でした。 つまり、DirectX 11の方式はゲームエンジン開発者が自前でコマンド列を貯めて発行していた方式と基本的に変わりません。 唯一の違いはこれらの処理をDirectX自体がおこなうことです。 このため、コマンド列を貯めるのと並行してさまざまな準備処理を先行させられます。 たとえば、コマンド間の差分のみを含むセットへと変換したり、GPUが実行しやすい形に変換するなどの処理です。

7. グラフィックスAPIパラダイムの転換前夜

DirectX 11リリース後、2012年末ごろまでの業界状況は次のようなものでした。

  • ハードウェアの進化ペースは低下しつつも続いていた(例:Conservative Rasterization)。

  • DirectXやOpenGLはプログラマーの開発負担を低減するために、さまざまな処理を裏側でおこなっていた。 場合により、ドライバーがいきなり重い作業をおこなったりと、開発者を悩ませることもあった(この負荷を避けるために、DirectXの癖を把握して、わざとある無駄なオペーレションを実行してタイミングをずらすハックもあったと聞きます)。

この頃のMicrosoft社のスタンスは「DirectXは11で最後」というものでした。 もちろん最後ではないはずですが、しばらくの間はもう抜本的な変更を加えた新しいAPIは出さないという意味でした。

いっぽうで、ゲーム開発者は昔から「たとえ記述が面倒だとしても、できるだけ完全なコントロールが欲しい」と要求してきました。 DirectXやOpenGLの提供する水準ではまだまだ足りないと感じ、専用ゲーム機並の自由なアクセスをしたいというものです。 AAAゲームを出しているゲーム会社の人間は最先端の技術を理解し、できるだけ質の高い物を作ろうとするため、当然の事です。 彼らはMicrosoftのスタンスに対して、別なルートを探していました。 もちろんKhronos Groupの中でもこの議論がおそらくあったと思います(携帯端末でのゲーム市場が伸びる中で、バッテリー消費をおさえつつ高品質なグラフィック処理を実現したいという流れもありました)。

8. Mantleの登場と各社の追従、Vulkanへ

そして、Mantleが登場しました。 MantleとはAMD社(GPUメーカー), EA DICE(ゲーム会社)が組んで開発したAPIです。 Mantleの仕様は、他のGPUメーカーがMantle APIをサポートしたいならば、自由にやって良いという条件のもとで公開されました。 そしてEA DICEのFrostbite 3エンジンでの成果がかなり良く、業界に革命ともいえる大きなショックを与えました。

もちろん、いくら優れたAPIであっても標準仕様ではない独自APIが成功する例は少なく、MantleがメインストリームAPIになる可能性は低いものでした。 さらにMantleはPC向けの独自APIでモバイル環境を考慮していなかったため、たとえばRenderPassの概念が存在しませんでした。

しかし、まずは寝ていた巨人が起きました。 Microsoftが1年のずれでDirectX 12を発表したのです。

そして、Khronos GroupがMantleをベースにVulkanの仕様を仕上げました

さて、この間にApple社がMetalという描画APIを公開しました。 これはVulkanやDirectX 12に近い感じです。 細かくみていませんが、イメージ的にVulkanより扱いやすいと思います。 残念な事にAppleがVulkanをサポートする気配は今のところありません。 Metalの上で実行出来るVulkanのライブラリ(名前はMolten)が存在しますが、どこまで機能が動くのかはわかりません。

重要なのは、VulkanがMantleのコピーではなく、DirectX 12とも違う特徴を持つことです。 VulkanにはMantleにはなかった物も入っています。 VulkanとDirectX 12の概念はとても近いですが、Vulkanははじめからマルチプラットフォームを想定しています。 モバイルも、Windows OSもLinuxもサポートします。 PS4やXbox OneもAMDのチップを使っているので、Vulkan対応が可能だと考えられますが、政治的な意味でVulkan対応の可能性は低そうです。 しかし今後の新しい環境、ゲーム機などがサポートする可能性は十分あると考えられます。

つづいて、Vulkanが従来のAPIとどう違うかを見ていきましょう。

Vulkan addition in more detail - Vulkanが追加する機能と概念

Everything in advance / ほぼすべての状態、設定を先にオブジェクトへビルドする

  • 1/ 全体の描画パイプラインをイメージするオブジェクトを先に作成可能(Input Streamの設定・Shader・Blend・Depth Buffer・Stencil・View Portのdefault値の設定など)
  • 2/ パイプラインにつながるテクスチャーやテクスチャーサンプリング方法、Uniform値を指定
  • 3/ 描画コマンドバッファで描画命令を貯められる。これらは再利用も可能で、どのスレッドからでも作成可能。GPUに送る事も出来る(出来上がったオブジェクトはある状態を表現するため、Immutable型で変更出来ません)。

この3点によって、従来のOpenGLでは描画時にかかっていた処理コストをすべて作成時のコストに移動できます。 事前処理によって内部でGPUの一番わかりやすい形(GPUでの利用時にオーバーヘッドが最も少ない形)のデータに変換することも可能です。 さらに描画命令を貯める事で処理差分のみをGPUに対して発行する最適化も可能で、これも処理負担減に貢献します。

つまり、以前にあった描画時にコミットのオーバーヘッドやステートの変更によってドライバーがやらないといけない重い仕事が消えます。 データはすべて事前に計算されているため、最低限の差分適用で前の処理から次の処理へ切り替えることもでき、GPUにとってもやさしい設計です。

描画コマンドも再利用可能なので、さらにコストダウンにつながります。 一回だけの作成コストで複雑な命令を組み合わせ、その後はずっと描画にコミットするだけです。

ここで重要なのは、描画コマンドのバッファを再利用する際に、画面が全く同じ見た目になる必要はないということです。 例えばカメラの角度を変えたり、キャラクターの場所や動きを変える事は簡単に出来ます。 例えば10万個の草の描画命令を簡単に再利用することが可能です。 コマンドバッファが利用するバッファの中身を差し替えることで、同じコマンドバッファを利用して異なる物を描画する事も可能です。

Reusability in Vulkan

  • Render Passes 利用するバッファ、書き込み用・読み込み用の依存グラフを定義できます。 これによって画面出力までのバッファの流れが整理され、Vulkanドライバがハードの制御を最適化できます。

  • Free Memory Management / 自由なメモリ管理 DirectXやOpenGLでは、メモリのアロケーション時にHandleが戻ってきます(テクスチャー, VBOなど、とにかくオブジェクトポインターやIDが戻ってくる)。

Vulkan APIを利用するアプリケーションでも、Vulkanに対してメモリのアロケーションを要求します。 しかし、このときに対象メモリの用途がテクスチャーなのかバッファなのか、その他のユースケースを明示します。 要求したメモリはOpenGLやDirectXと同じく、Handleとして戻ってきます。

端末のプロフィール情報から、どれぐらいのメモリがあるのか、それらの種類は何か、なども知る事が出来ます。 この情報を利用して、メモリのアロケーション時にどの種類のメモリをどれだけ利用するか設定できます。 そして、割り当てられたメモリを利用する時に必ずすべてのAPIにHandleのオフセットを設定します。

これによって、自然な形ですべてのメモリブロックに対する自前のアロケーションの仕組みを作成できます。 メモリのポインタが直接見えないにも関わらず、Handle + Offsetのお陰でポインタを持っているのと同じ事が可能です。 これはすごく頭の良い仕組みだと思いました。 そして、対象がCPUから見えるメモリであれば、Handleからアドレスにマッピング出来ます(GPUへのメモリ転送によく利用される)。

  • Threading Modelの特徴と制約 基本的にスレッド毎にコマンドバッファで描画コマンドを貯めるのは変わりません。 かわるのはどのスレッドでもコミットが可能という点です。 ただし、同時に2つ以上のスレッドが同じQueueにアクセスしてはなりません。 同時アクセスを防ぐための制御はユーザの責任でおこないます。

すべてのデータの種類に関する、細かなルールは以下のページにあります。

https://www.khronos.org/registry/vulkan/specs/1.0/xhtml/vkspec.html#fundamentals-threadingbehavior

Vulkan Basic - Vulkanの基本

First Setup

初期設定

  • Enumerate Layers (=API Wrappers) and Extensions. ここでVulkan用にインストールされているレイヤー(APIのラッパーの理解で良い、目的によって、いろいろなレイヤーが存在する)を列挙します。 この段階で拡張関数の取得も可能です。

  • Create Instance of Vulkan. Vulkanのインスタンスを作成します。

  • List all physical GPU devices supporting Vulkan. 実際にコンピュター内にVulkan対応の物理デバイス(つまりグラフィックカード)のリストを取得します。
    • Can list / access all the features / properties / memory properties / extensions of device. デバイスごとにサポートされている機能、機能の制限、メモリの種類と制限、拡張の情報を取得できます。
    • Can list all the queues supported by the physical device. そしてデバイスが対応しているQueue一覧(Queueは命令の配列を実行する物)を取得できます。

  • Create a physical instance. 物理デバイスのプロファイルをVulkan APIでインスタンス化します。

  • Create a logical instance from physical device. そして、物理デバイスのインスタンスから、論理デバイスを作成します。

  • Get Queue and all ressources from logical device. 論理デバイスでリソース、Queueなどを取得します(作業はほぼすべて論理デバイスで行う)。

  • Job Done / 設定完了


Swap Chain Setup

Swap Chain / 描画用のスワップチェイン

  • Get Extensions for presentation. 表示用の拡張APIを取得します。

  • Create Surface Surfaceを作成します。

  • Get surface support and possible format. Surfaceが対応しているフォーマットや機能を確認します。

  • Get,select and set presentation mode. 表示の仕方(モデル)を取る、選択し、設定します。

  • Create Swap Chain スワップチェインを作成します。

  • Get images from swap chain. スワップチェイン内からのイメージ一覧を取得します。

    • Create command buffer for post/pre presentation for each images イメージ毎に描画前・後の表示用のCommand Bufferを作成します。
    • Related Image Memory Barrier メモリバリアを作成します。
    • Create View for each image. イメージ毎にViewを作成します。

  • Use 2 semaphores : 2つのセマフォを作成します。
    • One for presentation complete. 表示完了用です。
    • One for rendering complete. 描画完了用です。

そして、

  • Get format properties supported, create Depth/Stencil Buffer,Image and view. 対応できるフォーマットを取得し、Depth/Stencilバッファ、イメージ、ビューを作成します。
    • Allocate necessary memory and bind both. 必要なメモリを割り当ててこれらを紐付けます。

  • For each views of all images from the swap chain, create a framebuffer linking the depth buffer view and the image views.


Rendering / 描画

  • Pre Rendering / 描画前
    • Get Next Image ID. (=Current) / 次の描画バッファのIDを取得します。
    • Post Present Buffer / 描画後コマンドバッファ
      • Begin Command/コマンドの開始
      • Push Image Memory Barrier with Current Image / 現在のイメージオブジェクトにメモリバリアを設定
      • End Command / コマンドの完了
      • Send to Queue / Queueに転送
    • Wait for Queue to complete. (Queue idle wait) / Queueの完了待ち
  • Rendering itself / 描画本体
    • Submit current frame command queues [### エンジンの描画コマンド ###] with following condition / 現在のイメージオブジェクトに合っているコマンドバッファに[### エンジンの描画コマンド ###]を次の条件付きでコミット
      • Wait for "present complete semaphore" / 表示完了セマフォ待ちを指定
      • Set for "render complete semaphore" / 描画完了セマフォ待ちを指定
      • Send to Queue.
  • Post Rendering / 描画後
    • Pre Present Buffer
      • Begin Command / コマンドの開始
      • Push Image Memory Barrier with Current Image / 現在のイメージオブジェクトにメモリバリアを設定
      • End Command / コマンドの完了
      • Send to Queue / Queueに転送
    • Swap Chain Queue Present API with Current Image with following condition :
      • Wait "render complete semaphore" / 描画完了セマフォ待ち
    • Wait for Queue Idle. / Queueの完了待ち

[### エンジンの描画コマンド ###] は自分が描画したい物です。 もちろん、ここで挙げたのは単純なケースで、1つのターゲットバッファしか使わないパターンです。

What Vulkan is for - Vulkanは何のため

Not For :

  • 単純なテスト・単純な物で描画したい場合。基本的にVulkanは通常のアプリケーション本体に書く物ではありません。描画フレームワーク内から呼び出す物です。
  • CPUが描画以外の処理に忙しい場合。Vulkanの性質を活かすためには、描画用の前処理にCPU時間を割く必要があるためです。
  • GPUの細かな事(内部構造)を知らない方
  • OpenGL(ES) / DirectXの仕様を読んで悩んでいる方。
  • Single Thread描画。これはちょっと微妙な意見です。実際Single Threadの場合でもVulkanをうまく使うことで効果は出せます。しかし、性能を最も引き出せるのは複数のスレッドで描画した場合だと思います。

For :

  • CPUバウンド描画。今までOpenGLやDirectXを使って複数のコアで作業していても、描画による処理でCPUが足りなかったパターン。
  • たくさんのdraw callが必要なアプリケーション。
  • 安定した描画時間が必要な場合。ドライバの勝手な裏作業をなくすことで、描画にかかる時間を見積もりやすくなります。

つまり、描画APIの勉強以外、ほとんどOpenGL系でも十分です。 しかも、OpenGL AZDO Extensionを利用すれば、Vulkanとほぼ同じ事を楽にできます。

すなわち、Vulkanは仕事上で専門に描画APIを叩く方に向いている仕組みです。 個人的には、勉強・趣味とゲームエンジン/3D CAD類の開発者以外はあまり調べなくても困らないのではないかと思います。

What I found difficult - 難しいと思ったところ

今回の記事を書くにあたって、Vulkanのすべての機能を使ったわけではありません。 また仕様を細かいところまで完全にマスターして、理解したわけではありません。 あくまでも、使った分の個人的な感想です。

  • ユースケースごとの説明が足りない

このフラグの組み合わせで本当にいいのか、という確証を得られない場合がありました。 このため、あるフラグを設定しない場合はどうなるのかなどの疑問を持ちながら開発を進めました。 特にメモリのタイプ・転送について、またViewの切り替えについて、不明確な部分が多かったです。 例えばRender Targetのreadとwriteの状態を切り替える概念を理解しても、実装・設定に関して、不安なところがありました。

  • 同じくRender passについて、説明が物足りない

APIの説明はいいのですが、ユースケースやパターンに関する説明が不足しています。 APIの説明を読んでも、見えていないところもあります。

  • API仕様書ではなく、概念やユースケースを説明する本が存在しない

新しいAPIであるため一定は仕方ないことですが、これによってDirectXやOpenGLでの経験がないプログラマがVulkanを理解するのが難しくなっていると思います。 また、DirectXやOpenGLのAPIをたくさん叩いた経験があったとしても、内部で起きている事・GPUの設計を理解していなければ理解しにくい部分があります。 暗黙的なノウハウがかなり多いと感じています。

Hints - ヒント

Vulkanを利用した開発をおこなう際にヒントとなりそうなものをいくつか紹介します。

C++ラッパーはオーバーヘッドがなく、しょうもないミスを防ぎやすく、メンテナンスしやすいVulkanコードを組めます。 使わない理由はないと思います。

型安全なEnum指定によって定数指定ミスを防げますし、IDEで自動的に関数やEnumを補完できるため、開発がかなり楽になります。 今回は勉強のために、作業途中でC++版の自前ラッパーを作ったのですが、本記事の執筆時点でNVIDIAがC++ヘッダの自動生成ツールを提供しています。 Vulkanコードを真剣に組むなら、使うべきだと思います。

なお、NVIDIAのC++ヘッダ生成ツールはKhronos Groupからオフィシャルに認められて、2016年7月末からKhronos Groupによってメンテナンスされています

  • Debug Layerを使ったバリデーションを活用しましょう。

大変役に立ちます。 もちろん、もうちょっとわかりやすいエラーを出せばいいのにと感じることもあります。 それでも無言でcrashするより、大変素晴らしいです。 そして、エラーではないが推奨されない指定についてWarningを出してくれるケースもありました。 これによって、コピペによるフラグの設定ミスなどを修正出来ました。

Vulkan Validation Layersの使い方を説明したサンプルコードはこちらです。 http://gpuopen.com/using-the-vulkan-validation-layers/

In the case of Playground

当社で開発している2Dゲームエンジン"Playground"でVulkanを実験的にサポートするために行った対応を紹介します。 エンジン固有の事情も含まれるため、前提を補足しつつ説明します。

  • CPU Batching and dynamic buffer binding.

Playgroundではもともと描画用の頂点データを実行時にCPU側で計算しているため、GPU側のバッファを一切使っていません。 OpenGLのドライバーと同じ働きをし、命令コマンドを貯めながら、転送する頂点のデータも貯める構造です。 そして、描画命令を実行する前に先にメモリの転送を行います。

  • OpenGL like wrapper in engine (not modern stateless)

エンジンの実行時にOpenGLのステート切り替えをしていたため、エンジンのRefactoringはおこなわずに同じ動作を再現しました。 フラグとステートの差分を管理し、使うPipelineを切り替えるようにしました。 (Supported states : Viewport, scissor, depth read/write, depth function.)

  • Dynamic Shaders, target buffers : not supported.

エンジンではシェーダーやターゲットバッファの切り替えをサポートしていますが、今回は最小限の作業で可能な範囲の対応をおこなったため、シェーダーの切り替えやターゲットの切り替え部分は未実装です。

  • Texture Switch. テクスチャーの切り替え

Vulkanでは二つの方法が存在すると認識しているのですが、試したのは片方だけです。 OpenGLやDirectXのようなダイナミックな切り替えはVulkanの場合、PipelineにつなぐDescriptor Set Layoutをテクスチャー毎に作成(OpenGL系でいうと、Uniform値の塊を設定するオブジェクト)することで実現します。 もう一つの方法は、複数のDescriptor Set Layoutを作成せずに、すべてのテクスチャーを配列化して、indexでアクセスする方法です(試していないし、実装方法を調べていません)。

今回対応したものはこんな感じです。

Draw Command In Playground

Vulkan SDK, Sample Code of vulkan over the net :

とにかくVulkanを学んでみたいという方のために、Vulkan SDKとネット上に存在するVulkan関連のサンプルコードを追いかけて学習するためのおすすめフローを簡単に紹介します。

まずはKhronos Groupのホームページから始めましょう。 SDKへのリンク、仕様書、ドライバーなど、すべてそろっています。 https://www.khronos.org/vulkan/

個人的にNVIDIAのカードを持っているため、NVIDIAのドライバーをインストールし、LunarGのSDKもインストールしました。 LunarGのSDKをインストールすると、Debug/Validationレイヤーが追加されるので役に立つと思います。

今回の記事で扱ったのは感想や全体的な話がメインでした。 サンプルコードをもとにして具体的にVulkanを試したい場合は、次の順でみていくと良いでしょう。

Sascha Willemsのサンプルが一番機能が揃っていると同時にやる事が多く、フレームワーク化されています。 このため、生のAPIコールを連続的に綺麗に見ていきたい場合は、少々苦労するかもしれません(LunarGのAPI Dumpレイヤーを使ってよいかもしれませんが)。

将来は?

モダンなAPI(Vulkan, DirectX 12, Metal)はGPUの内部構造にフィットしやすい・計算モデルを近づけているため、今後のハードウェアの進化にあたって、APIも進化すると個人的に思います。 今までハードルだった古いAPIの体系が消えたことで、今後のAPI拡張はハードの性能を十分に引き出しながら進められると思います。 例えば、Raytracingの拡張、Conservative Rasterizationの拡張、などなど、すでに進化は始まっています。 例: http://gpuopen.com/gcn-shader-extensions-for-direct3d-and-vulkan/

Update :

Vulkanの調査とPlaygroundエンジンへの組み込み作業をおこなったのは2016年6月でした。 そしてこの記事を書いたのは7月でしたが、記事公開までの間の7月にSIGGRAPH 2016が開催されました。 このなかでVulkanに関する良い資料が公開されていたので紹介します。

https://www.khronos.org/assets/uploads/developers/library/2016-siggraph/3D-BOF-SIGGRAPH_Jul16.pdf

Conclusion :

Vulkanは低レベルAPIであると言われることが多いですが、個人的にはそうは思いません。 たしかに気をつけるべき事、やる事はかなり増えますが、今までの描画APIの流れで考えると自然なものが多いです。

つまり、Vulkanは完全に新しい概念というわけではなく、これまでの歴史の流れと追加されてきた概念の続きを担うAPIです。 具体的には、可能な限り事前処理できるものを増やす、データの再利用生を高める、描画時の負担を減らす、メモリアクセスをより柔軟にコントロールできるようにする、というトレンドに沿うものです。 これらを実現しやすくなるかわりに、GPUや描画パイプラインの細かいところまですべて理解する必要があります(覚える事は多いですが、昔からやっている描画エンジニアなら特に問題ありません)。

現時点のVulkanが抱える問題は、まだ丁寧なドキュメントがないことです。 ある事を実現したい時に複数の方法を考えつくことが多いのですが、何が「正しい」かという答えが出ない場合もあります(そもそも答えは存在しないかもしれません)。 つまり、ユースケースごとのわかりやすい例がありません。 おそらく何年間もゲーム機やPCでのグラフィック描画で苦労した経験者は悩む事が少ないでしょう。 私は描画APIをバリバリ触ってゲーム機向けの開発をおこなったことがないため、正当な評価をするための能力や経験が乏しい部分もありますが、より暗黙的なノウハウに依存しないマニュアル作りとさまざまな全体図/例の提示がほしいと感じました。

まずは、今後のGraham Sellers(AMD)のVulkan本に期待したいと思います。 https://www.amazon.com/Vulkan-Programming-Guide-Official-Learning/dp/0134464540

↑このページのトップヘ