第48回 NULLとはなんや?(筒井康隆の同人誌か?<古い...) 99/6/1

NULLとはなんや?

NULLとはゼロのことである。C言語の定義ではなく、英語の辞書には、少なくとも「無効な」だとか零のことだと書いてある。

しかしC言語では、NULLの定義は「変数がオブジェクトを参照していないことを意味する特別な値」であって、0だとはどこにも書いていない。C言語のFAQを昔、ちらっと見たときに、確かVAX−11(なんやねんバッカスって?酒の神様か?)のCの実装系では、実際、0ではないみたいなことが書いてあった。

あんた、0でないってなー!VAX君!いますぐNULLは0にしなさい!!さもなくば、放課後生活指導室まで来なさい!!

そもそも、NULLは0と仮定して、NULLで埋めるのにZeroMemory使ったりするし、いちいちポインタが不正かどうかを

if (pObject == NULL) {

なんて書きたくねーんだよ〜!!だいたい、ガーベジコレクタのことなんて、てんで考えてないC言語を使っているときでさえ、オブジェクトを使い終わったら、

lpObject -> Release();
lpObject = NULL; // 礼儀:p

みたいにして、わざわざポインタにNULLをほうり込む律義なやねうらおをもってしても、

for(int i=0;i<256;i++) Objects[i]=NULL;

とは書かない。

ZeroMemory(&Objects,sizeof(Objects));

と書くよー。書かせてくれよー。ちゅーかなぁ、今日からNULLは0にしときなよ。そんなところで自由度持たせてもしかたないじゃん。いいかげん、実装系依存の定数なんてやめようよー。などと泣きごとを言いながらも、やっぱり教育的指導を受けそうなので少し反省して、今日のところはNULLは0でなくても許してやるぜ!とまたひと回り大人になったやねうらおであった。(なってへんて!>大人)


第49回 Windowsで、精度の良いタイマを得る方法について(elユーザー必見!?) 99/6/2

Windowsには使えそうなタイマ関数として、GetTickCountと、timeGetTimeがある。最初、日本語のオンラインヘルプで見ていて、気がつかなかったのであるが、英語のオンラインヘルプを見ると、前者はWin95/98では55msの分解能しか保証されていないことがわかる。(後者は1msまで保証されている)

そもそも、かたや1msの分解能のタイマを実装しておきながら、かたや55msの分解能しか保証しないタイマが存在するというのは、どう考えても違和感があるのだが、まー、ゲームでタイミング得るのにGetTickCountは使うなよーっちゅーことである。だいたいやねー、英文のオンラインマニュアルに書いて、日本語のオンラインマニュアルに書いてへんってどういうことや?日本人、パカあるから、細かいこと言ても無駄アルとか言って、ナメとんのちゃうやろなー。(そんな外人おらんて!)

ちなみに、ここで幾度となく紹介している有名なel(easy link library)であるが、GetTickCountを使用している。まあ、いかに有名なプログラマであったとしても、英文オンラインマニュアルを事細かに読んだりはしていないということである。などと黙っているのも失礼なので、ご報告差し上げた。作者Botchyさんから返答をいただいたので、それを紹介する。

∽―――――――――――――――――――――――――――――――――∽
  GetTickCount関数
∽―――――――――――――――――――――――――――――――――∽


 こんにちは、Botchyです。
timeGetTimeですかぁ、確かにヘルプ見れば1msって書いてありますね。それ
でGetTickCountですが、55msですか? そうなると18fps程度しか処理できな
いってことになりますよね。実際、100fpsでも300fpsでも処理してますので、
大丈夫かなと思うのですが……?

 試しに。
GetTickCountで77fpsを計測しましたが、フレーム更新時間は10〜20msで処理
してるので、問題なさそうです。それで、timeGetTimeでもやってみたのです
が、なるほど、安定しているわけですね。こちらは安定して12msという数値に
なりました。どうやら、関数を置き換えたほうが良さそうですね。

 なぜにGetTickCountを使うかというと。
答えは簡単、初期のWin32 APIヘルプには「1msで……」などと書いてなかっ
たからです(笑)。最新の……というか、ヘルプ形式が変わってからは、ちゃん
と書いてあるようですね(邪魔なので使ってないんです)。貴重な情報、あり
がとうございます。


 それでは。


∽―――――――――――――――――――――――――――――――――∽
  今でもBC++ 4.0のWin32 APIヘルプを使っている вотсну

  ∵ ιεττεγ ∵
  botchy@ha.bekkoame.ne.jp
  botchy@ma3.justnet.ne.jp

  ∵ ωογιδ ∵
  http://www.bekkoame.ne.jp/ha/botchy/index.html
  http://www3.justnet.ne.jp/~botchy/index.html
∽―――――――――――――――――――――――――――――――――∽

ちゅーことで、この文章を見る限り、elの次のバージョンからは、timeGetTimeで実装するように変わりそうである。elのソースでtimeGetTimeを見るたびに、やねうらおという奴が、その昔、地球上に存在していたということを思い出して欲しい。(なんや?お前、もう死ぬんか?>俺)


第4A回 アプリの2重起動の防止策(Mutexクラスについて) 99/6/3

『Windowsパワフルテクニック大全集』という本がインプレスから出ている。インプレスは、いつもやることがどこかマヌケなので学生時代から、ここの本だけはなるべく買わないように警戒していたのだが、タイトルにつられて知らぬ間に買ってしまっていたようである。

この本のなかにアプリの二重起動の防止法が書いてある。内容はFindWindowで、自分と同じタイトルを持つウィンドゥを探して、あればそのウィンドゥにShowWindowでそいつにフォーカスを移して終了というアルゴリズムである。はっきり言って、ド素人のプログラムである。

具体的に言えば、FindWindowしてから、ウィンドゥが登録され、表示されるまでにタイムラグがあるので、その間に別のタスクがFindWindowで同じタイトルのウィンドゥを探しても、そこでは引っかからず、二重起動されてしまうのである。そんな可能性は、100万分の1程度だとか言うかも知れないが、これが列車の制御であったらどうか?100万分の1の確率で衝突が起こるだけだよ、で済むのか?

本来的に、マルチスレッドのバグというのは、非常に見つけづらい。生じる可能性が低いし、再現するのが難しいからである。再現性がなくとも、バグはバグである。たとえ、1億回に一度しか起こり得ない現象だとしても、決して、そういうプログラムを書いてはいけない。

こういう場合は、Mutex objectを使えば良い。Mutexとは、MUTual EXclusion(相互排他)である。簡単に使い方をいうと、

hMutex = CreateMutex(NULL,true,"便所"); // おーい、誰か便所入ってるかー?
if (GetLastError()==ERROR_ALREADY_EXISTS) { // うお!!姉ちゃん先に入っとったんか!!

である。Win32のMutexで気をつけなければならないのは、作成するには、CreateMutexして、GetLastErrorでエラーが帰ってきていたら、既にMutex objectが存在する、エラーが帰ってきていなければ、作成は成功し、自分はそのMutex objectのオーナーになったのだということである。

なんかGetLastErrorで調べないといけないというのが、なんともダサイ設計だが、それさえ気づけば、簡単である。詳しくはWin32のマニュアルか、Advanced Windowsでも読めばよろしい。(あるいは読まなくても、もう使えるかも知れない) 二重起動防止にMutexを使うのは、常識である。

どうでもいいが、ゲームプログラマは、ちょっとマルチスレッドについてナメてる節がある。スレッド間はグローバル変数を通じてやりとりせよ、なんて平気で言う人がいる。はっきり言って、そういう人は、マルチスレッドの怖さがまるで理解できていない。こういうのは、システム設計とか、低レベル制御とかやってた人間のほうがまだマシである。

まあ、マルチスレッドの障害は、意外と深い問題なので、また機を改めて書くことにしたい。


第4B回 トリプルバッファリング(DirectDrawパワフルテクニック) 99/6/4

前回紹介した、悪著『Windowsパワフルテクニック大全集』の真似をして、パワフルテクニックとか書いてしまったが、トリプルバッファリングなんて、DirectXのオンラインマニュアルにだって書いてある。それを堂々とパワフルテクニックなどと書くあたり、やねうらおも、政治家へとまた一歩近づいたと言えよう。(うそ)

ともかく。マルチスレッドで、片方のスレッドはメッセージのポンプ、もう片方は、ゲームスレッドであるとする。メッセージスレッドは、ペイントの管理をしていて、描画タイミングが来れば自動的に画面の再描画を始める。再描画を始めるゆうてもやな、どこから転送すんねんゆうたら、フルスクリーンであれば、フリップの為に準備してるサーフェースだし、ウィンドゥモードなら、単なるバックサーフェースである。

ところが、ゲームスレッドは、そこをまだ描画中かも知れない。しかし、描画が完了したかどうかをスレッド間で通知し合うのは、そう簡単な話ではない。えー簡単だよー、とか言われるかも知れないが、簡単ではない!のである。(なんちゅー、実のない文章や...)

まー、今回は、その話ではないので、そのへんはサラっと流して、ゲームスレッドの描画完了をメッセージポンプスレッドが待ったり、あるいはその逆をしたりするのでは、とても効率が悪い。そこで、出てくるのがトリプルバッファリングである。これは、表示画面、裏画面と、さらにもう一枚、裏画面その2という3枚のサーフェースを用意することである。

メッセージポンプスレッドは、画面を更新するタイミングになれば、ゲームスレッドのことは無視して、裏画面を表示画面にblt(or flip)する。ゲームスレッドは、描画が終われば、裏画面その2と裏画面とを入れ替える(実際はポインタを入れ替えるだけ) こうすることによって、メッセージポンプスレッドはいつでも画面を表示できるという仕掛けである。そもそも、WM_PAINTメッセージがいつ飛んでくるかわからないので、このようにしてWM_PAINT要求に対していつでも応答できる準備をしておかなくてはならない。

というのは、理想論である。(どないやねん!)

実際のところ、トリプルバッファリングしようと思えば、画面のサーフェース以外に、2枚のサーフェース(ビデオメモリが望ましい)が必要であって、640×480×65536色ならば、少なくとも640*480*2bytes = 600KB必要ということである。実際には、サーフェース情報の保管のために、もう少し容量が必要なのである。こいつが3枚ということは、2MBをギリギリ超えてしまう(ことが多い)

ここ一年ほどでAGP対応のビデオカードが増えたし、メモリも飛躍的に増大したので、いまや32MBや64MBのビデオメモリも珍しくはないが、それであったとしても、1MBや2MBしかビデオメモリを載せていないユーザーもまだ数多くいるわけで、そういう意味では、トリプルバッファリングは、どうも現実的ではないような気もする。(もちろん、今後は常識になるだろうが) そんなわけで、トリプルバッファリングは採用しないほうが無難である。

でも、そうなるとマルチスレッドなので、スレッド間で描画タイミングを取り合う必要が生じてくる。こいつが実にうっとーしいのである。どれくらいうっとーしいかというと、平日に祝日があると、その週の土曜日は出勤日というぐらいうっとーしい!(しまった。それ、うちの会社のことやった〜)


第4C回 Mutexパワフルテクニック(もういいって!) 99/6/5

Mutexは、意外と遅い。意外と、ちゅーか、案の定ちゅーか。

開発に使っているNTマシンで、10万回Mutexをcreateしてreleaseしてみたところ、30秒(at Celeron366MHz)程度であった。つまり、秒間3000回程度しか実行できていない。

ということはPentium100MHzでは、秒間1000回ぐらいだろうか?仮に同期をとるため2つのスレッドがMutexObjectを秒間60回ずつ作成し合うとすれば、yaneMutexObjectの作成と解放は120回必要であって、マシンパワーの12%も消費することになる。こんなものは到底ゲームに使えやしねぇ。

グローバル変数で共有しろだとか言う人もいるけど、割り込みルーチンで、タイミングが超シビアなプログラムの開発をバブーとしかしゃべれなかったころからやってきたやねうらおにとって、そんなプログラムは恐くて書けんのだ。(何がバブーやねん。入浴剤か!)

スレッド間の同期がとれない以上、ゲームスレッドを作ってどうこうというのは、やっぱり却下である。そもそも、他のアプリに制御が移ったときに、ゲームスレッドを完全にSleepさせたいのだが、それをやらせるのがとても難しい。このアプリが現在アクティブであるかを示すグローバル変数bActiveを確保して、こいつを参照して、アクティブでなければ、メインスレッドが所有しているMutexオブジェクトをWaitForSingleObjectで待つことで待機するという手はなくもないが、なんだかとてもダサイし、レスポンスもいかにも悪そうである。そもそも、グローバル変数では、最悪、このプログラムは死ぬことになる。(ゲームスレッドがbActiveを参照するとfalseだったので、WaitForSingleObjectしようとしたが、そのタイミングで、メインスレッドがこのbActiveをtrueに書き換えたとしたら、ゲームスレッドは一生待ち惚けを食らわされることになる。これがマルチスレッドの怖さである!)

そんなわけで、ゲームスレッドは、突然やめることに決定。さようならyaneMutexクラス。さようなら、強制終了防止マクロ。いまさらフレームワークの大幅変更なんて、やってていいのか?そして、さようなら、説明のために大量に書いたサンプルプログラム。僕は、いつまでも君たちのことを忘れない。(ちゅーか、はよ忘れて、次のプログラム書かなヤバイねんて!)


第4D回 DirectDrawPaletteで半泣きになる(ええ加減、いてまうぞ!>DirectDrawPalette) 99/6/6

そもそも、Windowモードでパレットを使うと、paletteの調停がとってもややこしいのである。paletteなんてゆー共有のリソースをやね、ひとりで使おうっちゅーのがそもそもの間違いのような気もするのだが、こちらでパレットを変更すると他のアプリも変更しよる。俺にパレットを使わせろ!!と叫びたくなる。

まあ、パレット処理なんてWin32系のアプリの本(かonlineマニュアルでWM_PALETTECHANGEDとWM_QUERYNEWPALETTEあたりを調べれば良い)を見れば載っているだろうから、ここで詳しくは書かない。

えっ?DirectDrawPaletteでなく、パレットマネージャ通すの?

いやーばれたか(笑) まあ、パレットを実現するには、従来のようにパレットマネージャを経由してやるやりかたと、DirectDrawPaletteを使用するやりかたとあるわけである。当然後者のほうが、パレットマネージャを経由しない分だけ速い。

ところが、こいつが非常にうっとーしいのである。そもそも、Windowモードでのパレットというのは、気が狂うほどうっとおしい。elも、それに感づいたのか、256色は非サポートである(笑) そりゃ、フルカラーで、SurfaceをLockしてぐりぐりとピクセルデータをいじれば、半透明だろうがαブレンドだろうが、フェードイン、フェードアウト、ワイプイン、ワイプアウト、ディゾルブなどやりたい放題なのであるからして、256色なんてさっさと切り捨てるのが賢い選択であることは言うまでもない。

しかしやねー、いま作ってるプログラム(8月発売とか言ってるやつ)が、256色/65536色/フルカラーモードで、かつ、ウィンドゥモード/フルスクリーンモードで動かなきゃいけないんだよー。うお〜。そんなわけで、またもや地獄に落ちたやねうらおであった。めでたしめでたし。(何がめでたいねん!!)

ちゅーかなぁ、DirectDrawPalette使ってパレットをぐりぐりいじくってるサンプルが無いんやね。なんでないかっちゅーと、いろいろ罠があって、ちょっとやそっとじゃ書かれへんのよ(笑) みんなパレットマネージャ経由でリアライズしとるの。そっちのほうが文献多いし、いままでのノウハウが活かせるからやね。しかし、それでは進歩がなーい!!ので、ちょっとDirectDrawPaletteについて勉強しようではないか。

なんか、オンラインヘルプ見ると、2、3行書いたらええだけのように見える。具体的には、DirectDrawからCreatePalette呼び出して、そいつをプライマリサーフェースにSetPaletteでアタッチしとけばOKで途中で変更する必要があったら、パレットをSetEntries(0,0,256,pal)すれば、自動的に更新される...という風に読み取れる。あるいは、まったく違うパレットとすりかえるなら、別でCreatePaletteしといたパレットをSetPaletteすればOKという風に読み取れる。

ところが、これが甘いのだ。甘すぎるのだ。その程度の読みだと、このあと3度は地獄に落ちる。3度地獄に落ちたやねうらおが言うんだから、間違いなしである。「フルスクリーンでは、うまくいったけど、ウィンドゥモードだとうまくいかない」だとか「フルスクリーンモードでSetEntriesしているのにパレットが変わらない」だとか、「ウィンドゥモードだと、自分が希望しているようにビットマップが表示されない」だとか、その手の症状でお悩みの方は、数多くいるはずだ。そんな症状も、この記事を読めば効果てきめん!お代は、見てのお帰りだよ。なんや、お前、金とんのか!うそうそ。次回書くって。今日は、もう寝かせてよ〜。


第4E回 DirectDrawPaletteミラクルテクニック(DirectDrawPaletteにコテンパン) 99/6/7

単なるDirectDrawPaletteの正しい使い方を説明するだけでミラクルテクニックなどと申すとは、お主もワルじゃのぉ。ほっほっほ。

と、いきなり、わけわからん書き出しではあるが、このDirectDrawPaletteは見かけによらずかなりの強敵なのである。メーリングリストで誰かが尋ねているのを見たことがあるが、ほとんど誰も使ったことがなくって、オンラインマニュアル頼りに説明を延々と書いていた人がいたが(こういう親切な人を悪く言う気はないが)、それはちょっと甘いんでねーの、と言いたい。ちゅーか、ここでは、そういう人たちに、同じDirectX被害者の一人として救いの手を差しのべたいのである。

☆ 不思議現象プロファイルその1:フルスクリーンモードでSetEntriesしているのにパレットが変わらない

フルスクリーンは、Windowsのパレットマネージャの支配下から離れて、パレットは自由に制御できるはずやったんよ。だからして、ちょっとナメてかかっとったわけやね。それが、なんべんやってもパレット変わりよらへんの。ウィンドゥモードやったらいけて、フルスクリーンでなんであかんかなーと思ってね。

うーん。CreatePaletteでDDPCAPS_ALLOW256を指定せんとあかんと思ったひと、ハズレです。あのオプションは指定しなくとも256色すべてを変更することは出来るはずです。(保証はされとらん気もする)

まあね、単純なサンプルなら動いとったんよ。それが、自分のプログラムに組み込んで、アチョーとか、ホアターとか、ちょっとカラダを動かしてる間に動かんようになってしもたんよ。そうゆーことってようあるでしょ?(笑)

結局、このバグの原因を突き止めるのに3時間ぐらいかかったのだ。こんな不毛とも言える非生産的な時間を僕は知らない。この世界のどこかで、僕のように3時間かかってこの原因を突き止めようとしている人がいたら気の毒なので、ここに書いておきたい。

(原因)SetPaletteを複数回使ってはいけない

たったこれだけの事実に気づくのに3時間も要したとは!!なんと言う実りの少なさ!!ホンマ、なんでやねーんと言いたい。そもそも、SetPalette複数回使って同じパレットをサーフェースにアタッチさせたからって、なんであかんの?オンラインヘルプ見ると参照カウントが増えるだけってちゃんと書いてあるやん?えっ!ちょっと待て!!なんで増えるねん!何をどう増やす必要があるっちゅーねん!これが仕様なんか?どうしようもない仕様どうしよう(早口言葉)

(症状)SetPaletteを同じパレットに対して二回連続使うと、以降、そのパレットをSetEntriesで変更してもそれが画面に反映されなくなる。ただし、フルスクリーン固有のバグ。

一応、開発マシンとテストマシンの2台で試してみたけど、両方で同じ症状を確認したぞよ。もー、いい加減にして欲しいと言いたい。プログラム少年だったとか言うゲイツ君よ。あんた、ちょっとDirectDraw使ってゲーム作ってみなよ。死にたくなるから。絶対。それか、部下を全員抹殺したくなるかも。(本当)


第4F回 正確な60FPSの実現(ウエイトタイマ構成法) 99/6/8

どことなくマニアックになってきた、この連載であるが、今回は、正確な60FPS(秒間60フレーム)を実現するためのウエイトタイマ(時間待ち)ルーチンについて説明する。

よく、タイミングはDirectDrawでflipすれば速いマシンなら60FPSになるので、それで調整するという人もいるが、これは、

1.遅いマシンでは60FPSにならない(タイミングが必要ならtimeGetTimeで調整すべき。第49回参照)
2.Flipは、使わないほうが無難。(第44回参照)
3.画面のリフレッシュレートは60Hzとは限らない。

という3つの理由で間違っている。

あと、正確に60FPS刻むような待ち時間を計算できない人がよくいるが、以下のソースを見れば、なんとなくコツがつかめるはずである。コツとしては、

1.システムに負荷をかけないよう、余分な時間はSleepする。(Sleepの精度を考慮に入れて、直前まではSleepさせない)
2.今回の待ち時間は、1000/FPSとやってしまうと、端数分だけおかしくなる
3.かと言って、精度を出すためにdouble型で今回の待ち時間を計算するのは少し、無駄

の3ポイントぐらいであろう。速いマシンで正確に60FPSを刻まないのは、この待ち時間の消費のさせかたが間違っているからである。描画(blt)の直前にこのルーチンを呼べば、必ず安定して、指定のFPSになる。


void ys::ElapseTime(void){ // (C)yaneurao 1998-1999
// 厳粛かつ正確かつ効率良く時間待ちをする

if (iFPS == 0) return ; // Non-wait mode

static DWORD lastdraw = 0; // 前回の描画時刻
// (不運にも第一発目のtimeGetTime() == 0とかゆーこともあるが、それは構わない)

DWORD t = timeGetTime(); // 現在時刻

dwFPSWaitTT = (dwFPSWaitTT & 0xffff) + dwFPSWait; // 今回の待ち時間を計算

// dwFPSWaitは、待ち時間の小数以下を16ビットの精度で持っていると考えよ
// これにより、double型を持ち出す必要がなくなる。
// dwFPSWaitTT = 1000 * 0x10000 / FPS;である

DWORD dwWait = dwFPSWaitTT >> 16; // 結局のところ、今回は何ms待つねん?

// 1フレーム時間を経過しちょる。ただちに描画しなちゃい!
DWORD dwElp = (DWORD)(t - lastdraw); // 前回描画からいくら経過しとんねん?
if (dwElp>=dwWait) {
lastdraw = t;
return ;
}

// ほな、時間を潰すとすっか!

// まだ時間はたっぷりあるのか?
// 4ms以上消費する必要があるのならば、Sleepする
if (dwWait-dwElp >= 4) Sleep(dwWait-dwElp-3);
// いまdwWait>dwElpなのでdwWait-dwElp>=0と考えて良い

// 95/98/NTで測定したところSleep(1);で1ms単位でスリープするのは可能
// ただし、実装系依存の可能性もあるのでSleepの精度は3ms以内と仮定

while ((timeGetTime()-lastdraw)<dwWait) ;
// ループで時間を潰す(あまり好きじゃないけど)

// これで、時間つぶし完了!

lastdraw += dwWait; // ぴったりで描画が完了した仮定する。(端数を持ち込まないため)
}


しかし、きちっと、このようなFPS調整のための時間待ちルーチンを書いている人を見たことがない。

小数点以下をdoubleで表記するのではなく、256倍や65536倍して整数で代用するのは、常用テクなのだが、意外と知らない人が多い。もっとも、なんでもかでも1命令1クロックで実行してしまう最近のCPUではあまり意味のないことなのかも知れない。しかし、最近のプログラマは、lineやcircleをアセンブラで実装した経験が無いのか?cos,sinを65536倍した整数テーブルをいじくり回すプログラムを書かないのか?そんな人たちが3Dでぐりぐり動くゲームを作っているのか?!まったくもって驚きである。8月に恐怖の大王が来るというよりは、65536倍衝撃的なんだってば!!


戻る