4k introの実装棚卸(1)exe編
4k intro作成したのですが、技術的な部分まとめないと次作るとき忘れると思ったので書いておきます。
(GPUSoundの記事書く途中でそういやこんなのあったな、と思い出しました)
デモはこんなの
a Stranger(Shadertoy移植版)
https://www.shadertoy.com/view/XtVfWy
ちょっとあとで4k intro版のソースコードも置いておきます。
## 4kbに収めるための技術
まず、下記URLに4k intro作成時に現行の環境で右往左往した部分について書いてます。
以下、別途実装方法や基本方針について書いていきます。
### 圧縮周り
Crinklerを利用。
http://crinkler.net/
Objファイルのリンク時にデータの加工を行うアプリケーション、のはず。
現状の環境で4kbの実行ファイル作るのだと必須なのかな・・・と。
(DOSアプリとかは別だよ!w)
これによって利用しないコードとかは削除されたりデータがまるまる圧縮されるみたい。
#### 一部ライブラリ関連で変な挙動が出るケースあり
最初サウンド周りをWASAPIで実装しようとしたんだけど、Crinklerとの処理の兼ね合いでリンクが失敗するケースがあった。
ちょっとどうしようもないので諦めたのであった。(ギリギリでGPUSoundを選んだのはそれで時間がなくなったため
同じようにライブラリ呼び出すとき等で謎のエラー(メモリ違反)でCrinklerが落っこちるケースがあるんだけど、エラーの原因を追っかける気力がなければ実装を諦めて別の方法にするのが早い(自分はこの考え)
### サイズを縮めるための記述
#### WinMain書かない
WinMain使わずにその前の準備関数「WinMainCRTStartup」を利用。
これにて準備を一部すっ飛ばしてる感じ。
#### デフォルトで標準ライブラリ使わない
一部メモリの確保は必要になるので、そのときはピンポイントで呼び出すなりWin32API側でGlobalAlloc使うなりして対応。
リソースは別にツール作って確認。
(ここらへんあとで設計書いとくのでそれ参照しながらだとどこでフックするかわかりやすいかも)
#### 変数を使い回す
OpenGLの初期化周りは使いまわしても問題ない(ただ処理順を管理しないとどっかでミスる)ので、結構使いまわします。
#### 変数にコンパクトに値を入れる
可読性の問題もありますが、複数の役割を目的として格納したりビット演算で特定領域は別のパラメータ、みたいなことしてる。
x86にコンパイルされるソースコードだと意味があるけど、GLSL上でこれを行うのは文字列扱いなので微妙なケースがある。
#### if分岐を使わない
処理自体の分岐はif文を利用しますが、閾値によって違う値を格納する場合、三項演算子を利用する。
これによって、圧縮後のバイト数が結構違うケースがある。
while・ifは結構大きくなるので可能な限り避ける。
#### inline関数を利用
無理やり展開する等の対応を行うケースがある。
ただ場合によりけりなので必要あれば削る・・・かも。
#### 値のキャストはできるだけ行わない
一部キャスト命令が標準ライブラリ準拠で行っているケースがあるみたいなので。
自分の場合こんな実装を加えてる。
(元はKiokuさんが公開してるソースコードで、更にその元はパッカーみたいですが)
“` C:キャストするコード.c
extern “C” {
int _fltused = 0x9875;
int __cdecl _ftol2_sse() {
int integral;
short oldfcw, newfcw;
__asm {
fstcw[oldfcw]
mov ax, [oldfcw]
or ax, 0c00h; chop
mov[newfcw], ax
fldcw[newfcw]
fistp dword ptr[integral]
fldcw[oldfcw]
}
return integral;
}
int __cdecl _ftol2() { return _ftol2_sse(); }// for VS.NET 2003
}
“`
今だったらもっと別のアプローチもあるかもしれない。(他の特殊な命令を使う等)
#### 数学関数周りはx87命令使う
今回のデモではそれほど使わない(GLSL上で使うことができる)んだけど、CPU側で音楽をレンダリングする際に使ったほうが記述を縮めることができる。
http://softwaretechnique.jp/OS_Development/Tips/IA32_X87_Instructions/X87_CONTROL.html
最近だと64klangとかがSSEとかをゴリゴリ使ってデータの転送を一度に多く行うみたいんことやってるみたいだし、まぁ特殊な命令のほうが記述が短くなるとそういう方向だわな、と。
#### アセンブリコードを吐く
今までアセンブリコードというものを見たことがなかったんだけど、圧縮の際どこが縮むんだろ?みたいなのを見る必要があったので、VisualStudio上で設定した。
C/C++→出力ファイル→アセンブリの出力
で吐き出せるので確認した。
必要があれば手動最適化してリンクだけを別途行う必要があるかもしれない。
#### #defineディレクティブがいつまで残る?
えっとこれリンク時に消えるのかな?と思ったけど一応。
アセンブリコード上だと#define での定義が大量に残って困った。
(シンセサイザーの譜面とかを定義したんだけどまるまる残ってたのです)
今回はシンセサイザーがGPU実装だったので定義を消したけど、#define周りでなんかありそう、くらいは考えといたほうが良いかも。
#### ucrt.libが必要なケース
メモリ確保-開放(malloc-free)を使うときucrt.libが必要なケースがあるので、必要があればリンクしとくこと。
特にVisualStudio2017。
必要あればプロジェクトに引っこ抜いてくる事も辞さない。
https://blogs.msdn.microsoft.com/jpvsblog/2016/08/04/ucrt_local_deployment/
前述の通りGlobalAllocでも問題ないケースがあるので、下手にライブラリを組み込まない分GlobalAllocがいいのかもしれない。
### 全体の設計
大まかに以下の順番で処理するように設計。
* 音楽の生成
* グラフィック周りのシェーダ定義
* 音楽再生
* ループ開始+キー入力+ループ命令
* グラフィックの描画
* ループ終わり
以下のライブラリを利用。
* OpenGL(opengl32.lib)・・・グラフィック描画・音楽生成
* メディア再生(winmm.lib)・・・WAV再生に利用
* CRT ライブラリ(ucrt.lib)・・・メモリ確保-開放まわり
他デフォルトで呼び出しているライブラリがあるけど省略。
必要なのはシェーダだけという超シンプルな実装になってる。
このため無駄な機能はつけない形で対応。
グラフィックと音声については別にシェーダ再生用ツールを作成し、それでイテレーションして作り込んでいくことにした。
ここらへんは次の記事で書く予定。(4kbに関する情報の時点で分量多すぎる)
### なんでこんなの書いてるの
本来ならマジックのタネあかしみたいで忌避するケースはあるかと思います。
ただ、結局のところ細かい知識の積み重ねでデモを作成してく必要があるので、ベースとなる知識をどっかに控えておいて、前提となる実装をベースにまた違った絵や音、びっくりする表現を実装するのが理想的なイテレーションなのかな、と考えます。
あと単純に4k intro作る人もっと増えてほしいし。
(今回実行ファイル形式のデモ作る人が比較的多かったみたいだしねー)
## 参考資料
Kiokuさんとこ
http://kioku.sys-k.net/tips/
あと見返したんだけどここらへんは参考になります。
https://qiita.com/k-takata/items/588f5c1ce754d54afd49
その他いっぱい。適時リンク張っていきます。