第B9回 Packed Byte Increment(究極はあまりにも遠すぎる^^) 00/06/28

最近、毎週土曜に、さ〜さんが遊びに来てくれます。ここから自転車で来れる距離なのです。ちょうどテレホタイムに来られたときは、さ〜さんと、他の人のホームページ見ながら、ああでも無いとか、こ〜でもないとか言ってます(笑)

先週の土曜日の話。第B3回で紹介した、SYNさんのホームページ(我々にとって、一種のカリスマやね^^)

http://www1.odn.ne.jp/synsyr/

の飽和加算/飽和減算のところに名前が掲載されたことに気を良くした我々は、飽和加算(さ〜さん言わく、ほんわかさんらしい^^)はいいけど、飽和減算はまだなんとかなるやろ?とその場で式をいじくり回していたのだが、根性なしなやねうらおは、30秒ほど考えて、もうええわ(笑)と投げ出した。さ〜さんは、その後も考えていてどうやら飽和加算との対称性から新たな方法を打ち出したようだ。

http://www1.webnik.ne.jp/%7Esanami/codemaster/index.html#CODE21

の第21回を参考のこと。やねうらおも、ちらっと頭のなかをよぎった方法だが、演算回数が同じだから…とか思っていたら、レジスタ効率がわずかに良いようだ。結論だけ書くと、

SubBlend : // ( dst = dst-srcの場合 )
  c = ((( ~dst & src )<<1) + ((~dst ^ src) & 0xfefefe)) & 0x1010100;
   mask = ((c >> 8) + 0x7f7f7f) ^ 0x7f7f7f;

  dst = (dst - src + c) & ~mask;

の最後を

  dst =  (dst | mask) - (src | mask);

とやることだった。

そのあと、さ〜さんと盛り上がったのが、擬似的なpacked byte increment/decrement。SYNさんのホームページから引用すると、

DWORD P_INC_US_B(DWORD x)
{
    DWORD dwNum = ((~(x & ((x & 0x7f7f7f7f) + 0x01010101))) & 0x80808080) >> 7;
    return x + dwNum;
}

DWORD P_DEC_US_B(DWORD x)
{
    DWORD dwNum = ((x | ((x | 0x80808080) - 0x01010101)) & 0x80808080) >> 7;
    return x - dwNum;
}

である。内容は、飽和するBYTEは0になるようにしたもの(dwNum)を求めて、それを足しているだけ。アルゴリズム的には、これが一番わかりやすいし、数字を変えることで、いろいろ応用も出来そうである。ただ、これが最速か?と議論していたとき、午後のこ〜だ〜の作者

http://homepage1.nifty.com/herumi/

のアイデアは、(packed BYTEではなく5ビット×3+1= 16ビットで、最上位ビットが余っているときのincrementの議論ですが)

31か否かを判定するにはやはり1を足して繰り上がりを見るのがスマートです.問題は繰り上がると隣のbitと干渉してしまう点にあります.それを防ぐために A=0b111101111011110 でマスクすることを考えます.偶数の時は1を足しても繰り上がりませんから, 1を足すのではなく x & B (B=0b000010000100001) を足せばよさそうです.ただ x & A + x & B = x ですから, 結局(x + (x & B))>>5 で繰り上がりを判定できます.x + x & 1は常に偶数になることに注意してください.全部まとめて書けば

  x += B - (( (x + (x & B)) >> 5 ) & B);

だった。

しかし、このコードと前者とどちらが速いのかは、微妙なところです。(演算回数・レジスタ効率の両面から評価しないといけないので、どちらがどうかは実際にプログラムしてみないと見えてこない部分でもある) それはともかく、この式は最後の部分でビット反転を行なっていますが、

   x += (( (x + (x & B)) >> 5 ) & B) ^ B;

としたほうがレジスタ効率はあがりそうです。いや、& Bがあるなら、事前にnotしてもOkですか?

   x += (~ ((x + (x & B)) >> 5) ) & B;

まあ、そうしてみると、SYNさんのより速いような気がします。(最上位ビットを潰すので、善し悪しを単純比較は出来ませんが、いま考えている32bppのDIBは最上位BYTEは未使用なので構わない) そんなわけで、やねうらおの結論。

   x += (~ ((x + (x & 0x010101)) >> 8) ) & 0x010101;

ついでに、>>8の部分、アセンブラならばキャリーを絡めたローテイト(rcr)にすれば3バイトではなく、4バイト同時にincrement出来ますね。

ちゅーことで、これで、1ポイントゲット〜!やねうらおは、さ〜さんと並びました。(何や?いつからポイント制になったんや?しかも、何を誰と競い合ってるって言うんや?(笑))

これの応用でPacked Byte Decrementも出来るかと思ったんですが、x + (x & B)が応用利かないんで、3分ほど考えて出来たコードは、これ

   x -= ((((x | 0x1010100) - 0x020201)>>8) | x) & 0x010101;

でした。0x020201ってところが、何と言うか、やねマジックですが^^ ここは、別に0x020202でも構いません。しかし、SYNさんのコードより改善されず...。よって、これは没。2匹目のドジョウは居なかったようだ(笑)


第BA回 3Dゲームについての考察(たまには無責任発言^^) 00/07/04

まず、第B8回のCreateDIBSectionの説明にやや誤りがあったので訂正します。('00/08/05 補足記事、第BC回に有り)

ひとつは、64Kでアラインされているのは、メモリマップトファイルの関係であって、1×1のDIBに実際にメモリを64K消費するのではないだろう?ということです。(中野@S-STYLEさんから指摘あり) 確かにその通りで、最初の一回だけメモリ64K+BITMAP構造体のためのメモリ約4Kを消費するだけで、以降はBITMAP構造体の分しか消費しません。

よって、メモリの消費量の上では問題ないように思えます。(64Kのアドレス空間を使われてしまうというのを許せるのかというのは別の問題としてありそうですが) しかし、Windows95/98系はリソースエリアは2Mという制限があるので、小さなビットマップを作りつづけると、先にその制限に引っ掛かるのではないかとのことです。(同じく中野@S-STYLEさんより) 確かに600回ほどCreateDIBSectionしたところでCreateDIBSection出来なくなりました。なるほど欠陥OSです^^(4K弱×600=2M?) 中野さん言わく、NT系ではこの制限はないので、NT系で開発していると、よくこの制限でハマルそうです。

もう一つ。

http://www.microsoft.com/JAPAN/support/kb/articles/J028/9/84.htm

にある通り、ユーザー定義のビットマスクは作成できるのではないかということです。(村瀬OSM修さんより指摘あり)

24bppではCreateDIBSectionは成功しないのと、Windows95ではユーザー定義のビットマスクは不可だったと思うので、この場合DirectDrawSurfaceのとりうるすべてのピクセルフォーマットとコンパチなDIBを作成するというのは不可能なようです。ただ、Win32のBitBltは、転送前に2つのDCがパレット的にコンパチであるか(適合パレット)、そしてピクセルフォーマット的にコンパチであるか(これは適合ピクセルフォーマットとでも呼ぶのだろうか?)をチェックするようで、その条件を満たせば単純なメモリ転送で済むので高速転送が期待できます。よって、16bppのときに、DirectDrawSurfaceをLockしてピクセルフォーマットを取得し、RGB555か565かを判別して(注:Win32のAPIではそれを判別する手段が無い)、それに合ったDIBを作るというのは転送の高速化テクとして有効かも知れません。

しかしやねー、Windows95なんてベンダーたるMircosoftがサポート打ち切っているようなOSの面倒までこっちで見ないといけないんでしょうか?セガがドリキャス出したいまになってサターンのゲーム開発しているようなもので、こういうのは時代錯誤ではないんでしょうか?(笑) とは言っても、ユーザーが多いんだから仕方ないという意味もありますが。


そういや、コンピュータのハードウェアは進歩したと言われていますが、3Dをやるにはまだ遅すぎます。えっ?3Dでゲームが動いてるじゃない?と言われれば、それはそうなんですが、いまの3Dのゲームは事前にほとんどの計算を終わらせてあります。よくて、関節の接点だけ動かせる程度です。たとえば服を風になびかせようと思えば、通例、服の格子点をとって、その格子点間に弾性バネを入れて物理シミュレーションを行ないますが、現在のプロセッサでは、こんなことをやっていたのでは到底、実時間内に計算が終わらないのです。たとえば女の子を歩かせて、その着ている服をこういうシミュレーションを行なって表示しようと思うと、PentiumIIIの1GHzでも、まったく間に合わないのです。かつて大学時代にOpenGLでその手のシミュレーションを実際にやっていたやねうらおが言うんだから間違いありません^^

もちろん、格子点の数を減らせばある程度、動かなくはないですが、あまり減らすと弾性バネなので振動し、発散しますし、満足のいく画像品質は得られません。また、リアルタイムに服を計算して動かす価値はあまりないでしょう…事前に計算してパターンで持っておくほうが、遥かに計算コストが低いからです。X−BOXがいかに秒間3億ポリゴン表示できようと、プロセッサのスピードがPentiumIIIの500MHz程度ならば、当然このような計算を実時間に行なわせることは不可能なのです。

そう考えると、いまの3Dのゲームは、視点の変えられる紙芝居か、ムービーに過ぎません。3Dのハードを搭載していれば、3Dでポリゴンを出して動かせるのは当たり前ですし、プログラミング技術的には何も凄くはありません。加色合成や環境マッピングは見た目は派手ですが、ハードの力で出来るものにいまさら驚くこともないです。

はっきり言いましょう。もう、正20面体の障害物を交わしながら突き進むプラズマライン(こんなゲーム誰も知らんか…)の時代ではないんです。ハードでやってんだからポリゴンは動いて当たり前。もちろんデータ作成には、それなりに苦労が伴いますが、それはプログラミング技術とは別の部分です。ゲームのフレームを支えるプログラミング技術は、10年も20年も前のものです。そこからほとんど前進していない。アイデアという面では、むしろ後退すらしています。

某専門学校の3D科(そこの卒業生と話したことがあるのですが)では、2年でプログラミングの基礎から制作まで行ないます。専門学校だから、高卒生が対象なわけで、行列の回転すら出来るのか怪しい連中を相手に、ベクトルの足し算から教えるのです。2年で何が出来るというのでしょうか?しかし、それでも卒業生は、(一応は)業界でどうにかこうにかやっています。彼らはスキャンコンバージョンも知らないし、グローシェーディングのルーチンを自前で書くことも出来ない、3Dのライブラリを呼び出して辛うじてポリゴンを描画できるだけ(というか、それが出来るのも卒業生の一握り)ですが、その程度の薄っぺらい知識でも、どうにか食っていける。技術レベルは、理工系の大学卒業した少し頭のいい人間を連れてきて3ヶ月も教えれば達成する程度のものです。

そういう意味では、ゲーム業界、特にプログラマって、これでいいんかな〜とか思います。そんなだから、本来は知的な生産活動をしているはずのプログラマが肉体労働と呼ばれるまでに“成り下がる”んではないかとか^^ いや、肉体労働してんのは、1ヶ月でアクションゲーム作らなきゃならない、うちのような会社だけかなぁ…(笑)


第BB回 波紋を呼んだ第B1回(オレ、OOPわかっとらんのか?^^) 00/08/04

私は連日連夜訪れるもんで、近くのコンビニのおばちゃんとフレンドリーになりました。昼間だと「こんにちは〜」とか声をかけてきてくれます。朝方だと「おっは〜」とか言ってくれます。しかし、それがまた、真昼間とか真夜中とかに行くもんで、どうも私はフリーターと思われているらしいのです。まあ、それは無理もありません。郷に行っては、郷に従えなので、おばちゃんの前ではフリーターの振りをしています(笑) 昨日は、レジで、私のこと、まじまじと見て、「生活大変そうやね、これつけといてあげるわ」と言って、消費期限切れかけのお弁当をくれました。きっと、忙しくて散髪に行けずにいる私の髪の毛がボサボサなのを見て、「この子は散髪するお金もないんだわ」と思われたに違いありません。なんというありがた迷惑な話なんでしょうか(笑) いや、気持ちはありがたいんだけどね、すっごく。

ところで、そんなこんなで、今日もそのコンビニに行ったところまたもやそのおばちゃんに出くわしてしまいました。とりあえず、会釈だけしたとき、「兄ちゃん、ここで働くか?」って言われました。おいおい。アンタの3倍は稼いどるわ!とも言えないシャイなやねうらおは、「あのー、働きたいのはやまやまなんですけど、ボク、生活とか不規則なもんで」と言って、あたりさわりなく断ろうと思ったのが運の尽き。そのまま30分ぐらい勧誘されました^^ 「それにボク、受験生なんですよ。東大の大学院の言語学科目指して頑張ってるです。雨も降ってきたの『も』は、雨以外の何を指すのかとか、人称は何故3つかだとか、日本語における動詞のテンスとアスペクトとか、そういう勉強をしています」とか適当なこと言って、その場からそそくさと離れて、パンだけ買って帰ろうと思ったとき本棚にあったヒカルの碁8巻と、最終兵器彼女の2巻が目にとまったのでした。ヒカルの碁はあとで立ち読みしたらいいけど、最終兵器彼女は買って帰ろう。そう思って、パンと最終兵器彼女を手にとってレジに直行。またもや、おばちゃんにまじまじと見つめられる。最終兵器彼女の彼女の文字を指しながら「兄ちゃん、彼女おらへんの」 おばちゃん、何ゆうかな(笑) そんなんどうでもええやろ?^^ 俺には、3ヶ月も早く誕生日プレゼントをくれる女がいるんじゃい!とも言えないし(誰や9ヶ月遅れのプレゼントやろ?と言ってるのは..)、とりあえず、貧乏苦学浪人生の振りに徹することに決めたやねうらおは、「ええ、いないんですよー」と調子を合わす。そうすると、おばちゃんからは信じられない言葉が。「それなら、アタシが彼女になったげよか」え゛ー。なんですか。そんなのアリですか。顔面蒼白になりました(笑) 「いやあ、ボクはいま、受験生なので..」と、当り障りなくごめんなさいしたところ、しばらく間を置いて、「そうか、それなら仕方ないか..これ、おまけしといてあげるね」と、今日は消費期限切れかけの弁当を2つも入れてくれたのでした。うー。もう、許してくれい(笑) 俺が何したっちゅーんじゃい。俺が悪かったんかい。などと自問自答していると「消費期限切れかけだから、早くたべてね。ねっ?」 などと言ってくるではないですか。早くたべてね..あたしも消費期限切れかけだから早くたべてね..と聞こえなくもないその台詞。あまりのご好意に、あんたは、とっくの昔に、消費期限切れとるやろー!とも言えない、やねうらおは、あ痛ててて〜と思いながらそのコンビニを後にしたのでした。ごめんな、おばちゃん。もうここには戻ってこないと思う(苦笑) というか、このおばちゃん、悪い人でないだけにこういうことされるとホント、辛いんだよな〜。


さて、第B1回の最後に書いてある内容は、波乱が波紋を呼んだようで、さまざまな人から質問を受けたました。某所で真剣に議論してくださった方もおられます。私の日記にそこまでしていただいたのではあまりにも申し訳ないので、少し補足させていただきたいと思います。

まず、問題の部分を再掲します。

もう一つの問題というのは、アプリケーションクラスにつき一つ用意しなければならないものを記述するためのC++の構文が存在しないことです。あるクラスXYZのstaticなメンバにしてしまうと、それは他のスレッドからも参照できてしまいます。アプリケーションクラスにつき一つにしたければ、アプリケーションクラスのメンバとしてXYZを持たせるしか無いのですが、そうすると、クラスXYZは、モジュール性(modularity)を損ねてしまいます。

私は、この部分に関してかなり独りよがりな表現をしていることをお詫びしなくてはなりません。たとえば、ここで言う、モジュール性とは何を意味するのか?そして、アプリケーションクラスとは具体的には何を指すのか?また、なぜそのようなことをしないといけないのか?など、不明な点が多すぎます。おまけにこれを読み解く上で重要な部分を誤記していることもお詫びしなくてはなりません。アプリケーションクラスにつき1つではなく、アプリケーションクラスのインスタンス1つにつき1つです。(弁解するわけではないですが、一応、このコーナーは日記なので、自分の思考過程を簡単に書きとどめたいというのが第一目的としてあります^^ 用語の定義が曖昧なのは、ある程度大目に見ていただければ幸いです)

まず、「モジュール性」という言葉のニュアンスについて考えてみましょう。質問をしていただいた方は、それぞれオブジェクト指向に関しては、相当、造詣が深い方ばかりだと思います。だからこそ、この部分にハテナマークが出たのだと思います。

RAD系のツールを使ったことのある方ならばご存知だと思いますが、それぞれの部品としての完成度が非常に高いのです。C++のクラスもまた、ひとつの部品とみなせますが、私としては、RAD系ツールほど完成された部品とは到底、考えられません。たとえば、第B7回の委譲最適化にしてもそうですが、OOPに詳しい人ならば、どうして同じインターフェースを持っているクラスをまた作らなくてはならないのだろう?だとか、どうして委譲する必要があるのだろう?とか、さまざまな疑問があがるはずです。(それは私の状況説明が欠けているのが原因なのですが) OOPの世界で考えられるモジュールとは、普通、機能レベルでの完結を意味するでしょう。だからこそ、C++の何が不足なんだい?と。なぜ第B7回のようなことをする必要があるんだ?と。

しかし、言うまでもなくプログラムは、部品だけでは、完成しません。それらをジョイントする部分が必要です。この部分は必要になるごとに書かなければなりません。この部分を、誰がいつ書くのかという点において、私はかねてから非常に疑問があるのです。一つのプロダクトを完成させようと思うと、このジョイントする部分も当然必要になるわけですが、この部分を毎回毎回、そのアプリケーションに合わせて書いているような気がしてならないのです。従来のOOPパラダイムでは、この部分の再生産性が、非常に低いと思うのです。

たとえば、class Xの現存するインスタンスすべてへのチェインを得るにはどうすれば良いのでしょうか?

一つの案は、これのコントロールクラスをXの“外側”に書くことです。

class XControl {
public:
 X* CreateX() { X*x = new X; m_chain.insert(x); return x; }
 void DeleteX() { m_chain.erase(x); delete x; }
 set<X*> m_chain;
};

あとは、必要に応じて、m_chainを辿れば良いでしょう。通例、推奨されるC++のプログラミングというのは、そういうスタイルだと思います。Xが部品だとしたら、XControlはそのジョイントというわけです。まあ、現実的にC++でプログラミングを行なっている場合、それが現実的な解決策だということはわかっているつもりです。それでも、あえて言わせていただいきたいと思うわけですよ、それで本当にいいのか?と。

class Xは、頻繁にチェインを辿る必要があるとします。たとえば、XはDirectDrawSurfaceのクラスで、ディスプレイモードの変更等に応答して、すべてのSurfaceがLOSTするので、Restoreするためには全インスタンスを知る必要があるとします。そのときに、こんな風にXControlを通して、チェインを管理していたのでは、「ディスプレイモードの変更等に応答する」部分(メッセージ処理するのだから、通例フレームワーク)が、事前にXControlのありか(インスタンスの所在)を知らなくてはなりません。もちろん、XControlのインスタンスが1つだとも保証されていません。要するに、こういうライブラリ設計をしてしまうと、すぐに破綻するのです。

もちろん手段はいろいろありますが、これをスマートに解決する方法はC++にはありません。(とか言ったら、また波紋を呼ぶのだろうか^^) また、これを破綻とは感じないベテランプログラマも多数おられるのも、わかります。しかし、何と言われようと、私はC++のクラスがプリミティブな意味での部品レベルの完結しかしていないがために、それらをアセンブリする余計な手間が発生してきているように思えるのです。ひとりの人が設計したライブラリを使うユーザーが、そのライブラリの部品同士を結合するためのジョイント部分を書かなければならないような気がするのです。そして、この部分は、私に言わせれば、まったくの無駄な部分なのです。(私が考える超強力なテンプレート機能と、いくつかの機能拡張を行なったC++のsupersetでは、少なくともジョイント部分の半分ぐらいは軽減されます…される気がします…されるといいな〜とか思います←なんだか弱気やね^^)

XControlについて、もう一つの実装案を考えてみましょう。Xのコンストラクタで、Xのstaticなメンバ変数

static set<X*> m_chain;

にinsert(this)して、Xのデストラクタでerase(this)すればどうでしょうか?要するに、XControlをXの内側に“閉包”してしまうのです。この実装案は、ある程度、うまくいきます。「ディスプレイモードの変更等に応答する」部分からは、このm_chainを辿ればいいわけですから。ところが、このstaticなメンバ変数というのが曲者で、これを書いてしまった時点で、アプリケーションに唯一しか存在できません。これではまずい状況も多々あるのです。それが、前述のようにマルチスレッドでプログラミングしていて、アプリケーションクラスが、「2つのスレッド(メッセージポンプ+ワーカー)+自分の所有する窓を1つ」持つような場合、アプリケーションに唯一ではなく、アプリケーションクラスのインスタンスに一つにつき、そのチェインが1つ欲しいのです。

これをどこに書くかということです。そりゃ、アプリケーションクラスのメンバとしてm_chainを持たせればOKなのは言うまでもありません。しかしそうするとclassXの設計者がアプリケーションクラスについて知っていなければならなくなります。これが凄く理不尽に感じるのです。だから普通は、さきほどのようにXControlというクラスを作って、こいつのメンバをアプリケーションクラスのメンバとして用意するような実装にするでしょう。そうすると、「ディスプレイモードの変更等に応答する」部分に、ユーザーが手を加えなければならないというわけです。

これで本当にモジュールと言えるのか?プログラミング用語として従来使われている意味ではなく、ひとつのユニットとして見たときの完成度と独立性すなわち相互依存性の少なさ、部品としての使いやすさ、機能レベルの完結性等々を考慮したときに、モジュラリティがあると言えるのかと。

これでもやはり私が、何のことを言っているかわからない人にはわからないでしょう。とりあえず理念は置いといて、実装技術で言えば、ownerポインタが欲しいのです。また、インスタンスの生成/消滅に関して、もっとインテリジェントなシステムが必要なのです。それは、必ずしもスマートポインタ的なメモリ管理のことを意味しません。私のプログラミング理念を実現しようとすれば、少なくともインスタンスは、自分が誰によって作られたのかを知る手段が必要になります。OOPのパラダイムからは外れるかも知れませんが、これは、ある程度納得していただけると思います。さらに、自分がどこで生成されたのかを知る手段も必要になります。んっ?どこで?と思われる方が大半でしょう。そうです。生成されたリージョンです。んっ?リージョン? まあ、詳しいことを、ここに書くこともないでしょう。いまの仕事がひと段落したら、オブジェクト指向スクリプトを実装するので、それをもって回答としたいと思います。(しかし、年内にあと3本ぐらい開発を控えてるんだけどなぁ..大変だなぁ。またデマゴンになってしまうんかなぁ(笑)>私)


第BC回 DIBSectionはいい奴か?(いまさら何を言うかな>俺) 00/08/05

このスーパープログラマーへの道ですが、回数は16進数表記です。だもんで「第57回」を「第五十七回」と読むのは誤りです。かと言って、「第ゴー・ナナ回」と読めばいいかというと、それだと何進数表記かわからないので、きっちり16進の桁数名を補足して、「第五七回」と読むと、どうでしょうか。もちろん、「第345回」ならば「第三二百五十六五回」。「第3456回」ならば、「第三四千九拾六二百五十六六回」って、これでは、何のことかわからんわい!>俺


あのあと、いろいろ考えて、DIB(CreateDIBSectionで作ったものに限る)は、DCも取得できるのでリソースの枯渇にさえ気をつければ案外便利ではないかという結論に至りました。

スピードも、DirectDrawSurfaceからDCを取得して描画するのであれば、MMXできちきちに書いたコード(32bpp->32RGB,32BGR,24RGB,24BGR,16bppRGB555,16bppRGB565,8bpp)には劣りますが、それなりに速いのです。素のPentiumで書いたコードよりは、よっぽど高速です。きっとハードウェア側で何とかしてくれるのでしょう。(というか、Windowsが出てから発売されたビデオカードは、それくらいハードの設計者も考慮しているに違いない)

ということで、なかなか便利なDIBSectionを積極的に使うことにしました。しかしGDI系の描画でよく使うCOLORREFは、下位からR,G,Bである。それに対して、一般的なビデオカードでは、これとは逆順であることのほうが多いのです。DirectDrawSurfaceもCOLORREFと逆順です。CreateDIBSectionで作るとどうなるかというと、ピクセルフォーマットを指定せずに32bppのDIBを作れば、後者になります。な、なんや?それやったらCOLORREFなんて、やめちまえとか思うけど、いまさら言っても後の祭りなのでしょう。

CreateDIBSectionを使うために、MSDNのオンラインヘルプでBITMAPINFOを引くと

16 ビットマップは最大 216 色です。

とか書いてある。2^16を日本語に翻訳するときに誤植しているのだが、2^16と216では全然話が違うではないか!正直、一瞬、何のことかと思ったですよ。しかもCreateDIBSectionとか、一番有用な関数は日本語化されてません。Windows95のときから備わっている関数の翻訳をするのに、何年待たす気なんでしょうか。ひょっとして、そういうところで技術の差別化を図っているんかな〜とか思わずにはいられないです。

まあ、DIBは、特に問題はないのですが、DirectDrawSurfaceは曲者であることに最近気づきました。たとえば32bppモードのとき、最上位バイトは何が入るのでしょうか?これは、α値が入っていることもあるし、ゴミが入っていることもあります。通常のBlt転送によっては、この部分に対しては関与しない。つまり、3Dのゲームで遊んでゴミが入ったまま、ということも当然ありうるのです。しかもGDIでのピクセル操作::SetPixel(GetDC(),x,y);というようにしたときも、この最上位バイトには関与しません。ところが、DirectDrawSurfaceをLockすると話は変わるのです。そのままDWORDでゴミまで取得できてしまうのです。ビデオメモリに対してアクセスするのは非常に遅いので、それを我慢しながらぐりぐりと画像をいじっていたのですが、獲得したDWORDのデータに対して、抜き色ありの転送を実現するためには、このゴミが載ってくることを考慮して、カラーキーと比較するために、PixelFormatからRGBMaskを割り出し、そいつでマスクしたのちに比較しなければならないのです。これには、流石に頭に来た。

あと、場所を指定してそのカラーキーを獲得するサンプルがDirectXのSDKにあるのをその昔、見た覚えがあるのですが、そう考えるとあれは間違っているのです。つまり、試しにSetPixelで(0,0)に点を打ち、サーフェースをLockしてその値を取得し、nBppでマスクするようなコードでは、いかんのです。言うまでもなく、PixelFormatからRGBMaskを割り出して、そいつとマスクしなければならないのです。最近のビデオカードだと、特にこの最上位バイトにはゴミが入っている可能性が高いので、要注意。もー困るなぁ(笑)

というか、先日アップした雑誌用の体験版、εLOGINの編集から抜き色のバグが指摘されて、原因調べてたら、そういうことだった。これって、俺が悪いんか?...まあ、悪いんだろうな...。まあ、ビデオカードのアクセラレーションを下げればうまく動くようなのだが…(アクセラレーションを下げると、ソフトウェアでバッファを持つので、そのときは最上位バイトにゴミは載ってこない) 何にしてもDirectDrawSurfaceは、矩形転送ぐらいしかアドバンテージが無いくせに、何かと手間が掛かる奴である。矩形転送しかない2Dのゲームって、PCエンジンのゲームじゃないんだから(笑)


第BD回 Alt+Tabを禁止せよ(..してええんか?) 00/08/08

この連載で、記念HITでCGもらえるって羨ましい〜とか書いた甲斐あって、鈴希 零さんから残暑見舞いをいただきました↓
氏はホームページではPentium用の飽和加算ルーチンを説明されています。なかなかのパワープログラマですね。

さて。フルスクリーン動作していて、Alt+Tabでウィンドゥモードに切り替えられて、そこからアプリに復帰した場合、その復帰コードを書くのは容易ではない。たとえば、前回の画像に対してエフェクトを掛けるようなトランジションを行なっている場合は特にそうである。

Alt+Tabからの復帰は、普通、WM_ACTIVATEAPPに対して復帰させるのだが、マルチスレッドにして、ワーカースレッドがDirectDrawオブジェクトを構築し、フルスクリーンモードにしていると、このイベントはWindowsNT系では来ないのである。おまけにCreateWindowしたスレッドと違うスレッドがSetCooprativeLevelするあたりで、どうも、バグるようだ(2日ぐらいかかって調べたが詳しい原因はよくわからない。標準コントロールでもこれに似た制限に出くわしたことがあるので、たぶん、シングルスレッド時代からの遺産だろう。どうもロギングしてみたところ、WM_MOVEとWM_SIZEが来るあたりで、別スレッドでDirectDrawオブジェクトでBlt等を行なうと、内部の何かを破壊してしまって、それ以降メッセージが正常に来ないようだ。ということは、DirectDrawは、スレッドセーフならぬ、プロセスセーフなんですか?(笑)) シングルスレッドに書き直したら、なんなく動いた。俺の3日間を返せ〜って感じ。

とりあえず、Alt+Tabを禁止する方法は、ここにある。

http://www.microsoft.com/JAPAN/support/kb/articles/J048/2/77.htm

ただ、Alt+Tabの禁止は、ユーザーアンフレンドリーと言わなければならないだろう。某大手流通の社長が、これを禁止しているソフトに対してたいへんお怒りだった。だからして、某大手流通から商用ソフトを出そうと思うとAlt+Tabは有効でなければならないという裏事情もあるのだ^^

余談ではあるが、Windowsのスレッディングはかなり特殊である。ハードウェアマウスカーソルを消す、::ShowCursorしようと思っても、::ShowCursorは、ウィンドゥに対して行なうくせにHWNDを引数として取らない。どうやっているかというと、どうも、内部的には自分のProcessIDから、それに属するHWNDを検索しているようだ。すなわち、::CreateWindowしたスレッドが::ShowCursorしないと意味がないのである。

ついでに言えば、二重起動を防止するために、Mutexを使って、もし既存であることがわかったら、既に起動しているほうのウィンドゥをアクティブにして終了したいとする。::FindWindowでウィンドゥキャプションで探すのは、簡単だが、キャプションが変わる場合は使えない。具体的な手順としては、::EnumWindowsで全てのウィンドゥを列挙して、callback関数のなかで、::GetWindowLong( hWnd, GWL_HINSTANCE );して、HWNDを取得して、::GetModuleFileName( hInst, szFileName, _MAX_PATH );して自分のモジュール名と比較すれば良いように思える。しかし、これでは全然ダメなのである。

なぜなら、HINSTANCEは、uniqueな(一意に定まる)ハンドルではないのである。実際に調べてみればわかると思うが、ほとんどのアプリのHINSTANCEは、0x4000000であって、自分のプロセス空間のなかで生活している。つまり、uniqueなハンドルではないわけで、そんなものを引数にとって、::GetModuleFileNameでは何が得られるというのだろう?そう。自分のプロセス空間におけるhInstanceの意味しかない。つまりは0x4000000を指定したところで自分のモジュールのファイル名しか得られない。なんたるダサい設計だろうか。

よって、::GetWindowLongでHINSTANCEを取得する方法では手詰まりで、HWNDからウィンドゥクラス名を取得して、それが自分が::RegisterClassしたものと同じかどうかを比較するしか無い。しかし、そうなると別フォルダで動いていても同じ実行ファイルだと、ウィンドゥクラス名は同じなので、そちらがアクティブになる。もちろん、これでいい場合もあるが、ygs2kのように、別のフォルダで動いているものは同じアプリケーションとは限らない場合は、大いに困る。ついでに設計思想からすれば、ウィンドゥクラス名はライブラリのうち、WindowClassが担うべきものであって、それを2重起動防止するためのクラスが知らなくてはならないのはいくぶん理不尽という話もある。(まあこれは、許せる範囲だが)

実は、::GetWindowModuleFileNameという隠しAPIがある。確かwinable.hで定義されていたと思うが、こいつを使えばHWNDからモジュール名が取得できる……と思わせきや、この関数も嘘を返すようだ。もう知らない(笑) 助けてくれ〜って感じ(笑) しかし、 なんですか?ひょっとして、このヘッダー、winableではなくて、W_INABLE(不能)の意味ですか?(笑)

ついでに言えば、ShowCursorは、結局ワーカースレッドからウィンドゥメッセージを送って(::SendMessage)、メッセージポンプ側で処理してもらうようにしたのだが、メッセージポンプ側が、WM_CLOSEするから、ワーカースレッドの終了を待機していると、ワーカースレッドからマウス描画のためにShowCursorする必要が生じて、::SendMessageを呼び出すものだから、::SendMessageでフリーズする。なぜなら、::SendMessageは内部的に、CriticalSectionのようなもので排他処理を行なっているから、WM_CLOSEから抜けないと、::SendMessageは処理できないのである。ところが、WM_CLOSEのなかでワーカースレッドがシグナル状態になる(=終了)を待っているので、そのままフリーズするという憂き目に逢着する。

WM_CLOSEのなかで、ワーカースレッドがシグナル状態になるのを待たなければいいと言う話もあるが、突然::DestroyWindowされて、不正なhWndになったりするとそこでワーカースレッドは落ちるのである。窓が消滅するまでにワーカースレッドに終了を通知し、ワーカースレッドがシグナル状態になるのを待機する必要がある。このへん、手抜きして、メッセージポンプ側ではワーカースレッドに停止信号を送って、一定時間待ってそれでダメならば強制終了、という処理にする手もなくは無いが、危険極まりないのでちょっとやりたくない。

ということで、安易にメッセージポンプ+ワーカースレッドで行こうとするやねうらおのマルチスレッディングな設計は破綻したのであった。Windowsの腐ったAPI仕様からすれば、本来は、ワーカースレッドの要求に応じて、コールバックで処理してくれるような設計にしなくてはならないのだろう。あと、リソースごとに対するCriticalSectionは、かなりやっかい。しかも、それでマルチスレッドの恩恵にあやかれるかというと、そうでもないような気がする。描画するのにトリプルバッファリングして、メッセージポンプ側にも描画の手伝いをさせたりするのならともかく、本来、メッセージポンプで処理されるのは、それほどCPU負荷の高くない処理なのだから...。


第BE回 低脳MMXプログラミング(の日々) 00/08/11

ところで、以前にも書いたとおり、やねうらおは、いまだに携帯を持ちません。だからこそ、勝手なことを言うんですが、4和音の着メロなんていけてないと思うんです。あれは、すぐにPCMになります。数年もすれば、ヴァーチャルアイドルに音声合成で好きなメッセージをしゃべらせることが出来るようになります。あと、i−modeもじきにJavaが動くようになるだろうし、Palm系のOSが走る携帯電話が出ます。あと、すぐに世界各国の辞書や事典がオンラインで照会できるようになるでしょうし、ストリーミング再生技術によって携帯で、テレビだって見れるようになります。あと主婦とかが携帯持つと、折込広告も電子配信になります。折込広告が配信されるかわりに基本料金がタダの携帯も発売されます。あと恋人サーチとかも出来るようになります。自分のプロフィールを登録しておけば、恋人募集中の人から条件に合った人を見つけてくれるのです。GPSシステムを搭載して道案内してくれる機種も出ます。あとGPS+恋人探しでは、恋人登録をしている者同士が近づくと、音が鳴って知らせてくれるのです。それがまた大ブレイクします。(すべて何の根拠もありません。単なる無責任発言です^^)

そういや、ちまたで発売が注目されているkeyのAIRですが、

http://key.visualarts.gr.jp/airq&a.htm

Windows2000ではCDDA のフェード機能がはたらきませんって言うのは、何なんでしょうか?確かにNT系では、直接CDのボリュームコントロールを行なうのは不可能だったような記憶はありますが、どうせ普通、CDのラインはサウンドカードに接続されているのですから、オーディオコントロールのボリュームミキシングで解決すればいいような気もします。yaneSDK2ndのCAudioMixerがそれです。

それはともかく、第B3回で紹介した50%のブレンド転送ですが、

(a+b)/2 = (a & b) + (((a ^ b) >> 1) & 0x7f7f7f7f 7f7f7f7f)

これ、MMXだと、最適化する余地はあるんでしょうか?シフトにpacked byteの右シフトを使うと最後の&が不要になるぞ!またもややねうらお1ポイントゲット〜(誰とポイント競っとんのや)とか思ったのですが、なんと、packed byteで右シフトする命令は無いのです。(packed wordとpacked dwordのものは存在する) おいおい〜。何て直交性の無い命令セットなんや..

そんな話をさ〜さんと話していると、packusdwが無いという話になりました。確かにそうだ。サチュレートするようなpackより、むしろサチュレートしないpackのほうが必要な機会は多いです。どうして、この命令付けとかへんかな〜。おまけに、某MMX解説本は、この存在しない命令(packusdw)をpack命令のサンプルとして挙げている。まさか、著者も無いとは思わなかったのだろうな〜。サチュレートしながらのpackが出来るならば、サチュレート無しのpackぐらい出来るだろう..と。誰だってそう思うだろう。でもそんな命令は無いのだ。このMMX本の著者にも同情する余地があります。

ところで、L1キャッシュのこと考えてたんです。L1キャッシュからパージ(追い出)されると、データのアクセスは著しく遅くなります。L1にあるうちは、1clockノンウェイトでアクセスできるのに、そうでなければ、その何十倍も遅いことさえありえます。つまり、L1に入るかどうかがとても重要なファクターなのです。この、L1キャッシュ、仮に8kあるとします。これは、プロセッサ1個につき、8kです。ということは、2つのアプリケーションを同時に走らせて、このL1キャッシュを奪い合ったなら、両方のアプリはどちらも、L1の恩恵にあやかれないというケースもありうるわけです。つまり、アプリケーションを2つ走らせると10倍遅くなる(?)Windowsの特性は、案外、そういったプロセッサレベルにまで立ち帰れば説明出来るのかも知れません。ちゅーか、プロセスディスクリプタを用意して、各プロセスごとにL1キャッシュ自体を切り替えするわけにはいかない(いかなかった)のでしょうか…。(単なる無責任発言です^^)


第BF回 毎日が月曜日(そろそろやめよう、この仕事..) 00/08/19

最近、やねうらおは働きすぎである。あの仕事とか、この仕事とか引き受けなきゃよかったとか、いろいろ思わなくもないが、いろいろ付き合いもあるのでそうも言ってられない。

とりあえず、知人に頼まれて、コミケ用のCGビュアーを作ることになった。たいしたものではないので、yaneSDK2ndをもってすれば6時間もあれば完成するのだが、先日ホームページにアップしたCDIB32のサンプルがWin95では動かないことが発覚した。何が悪いのか調べていたのだが、わかったことは、第BD回でちらっと出てきた、winable.hをincludeしてあると、それだけで落ちる(「システムに装着されているデバイスは動作していません」というエラーが出る/Win95にはGetWindowModuleFileNameが存在しないのが原因。しかし、そもそもその関数は呼び出してもいないのに、落ちるのである^^)ことがわかった。というか、それを突き止めるのに2時間ぐらいかかった。こんな怪しげなヘッダー、もう使わない(笑)

そんなわけで、yaneSDK2ndにあるCDIB32のサンプルは、こっそりコンパイルして差し替えておくことにした。(ここに書いたら、全然こっそりじゃないという話はある^^)

お約束ではあるが、ついでに指定したCGを壁紙化する関数を作らなくてはならない。Dr.GUIの連載見て、
::SystemParametersInfo(SPI_SETDESKWALLPAPER,0,lpfile,SPIF_UPDATEINIFILE);
と書いたのだが、これではIEのほうの壁紙は変更できない。

しばらく考えたが、どうしていいのかさっぱりわからないので、Webを検索してまわったがまともに解説しているサイトは見当たらない。それとも、この

HKEY_CURRENT_USER/Software/Microsoft/Internet Explorer/General/Wallpaper/

レジストリを直接いじるしか無いのだろうか...

こういうことを考えていると息が詰まって仕方がない。たまには息抜きにソースで顔文字を浮かび上がらせて遊ぼう。たとえば、無限ループならば、

for( ; ; ) { // ←永久に泣いてる

だとか、10進数で数字を表示するルーチンで、10で割りながら1の位を出力して行くルーチンならば、

for(;c;){ // ←タコです^^
  mes.Out(c % 10);
  c /= 10;
}

だとか。関数fの呼び出し。変数はOxOならば、

f(OxO); // ←ウサギ

だとか...あまり書くと馬鹿だと思われるから、今日は、これくらいにしておこう(笑)


第C0回 困ったときのデバッグ手法(知ってるようで知らない..?) 00/08/21

いきなりだが、VC++を使っているとしよう。Debugビルドでうまく動いていたのに、いざ製品版にしようと思ってReleaseビルドにするとメモリエラーで落ちることがある。非常に困ったちゃんである。

こういうことが無いように、assertマクロ(yaneSDK2ndではWARNINGマクロ)を至るところに入れて、NULLチェックや、範囲外チェックを日ごろから行なっておかなければならないのだが、毎日が月曜日だもんでそうも言ってられない。

一番確実な手法は、どこまで実行されているのか記録を取ることである。yaneSDK2ndでは次のようなマクロを用意している。

// トレース用のマクロ:ファイル名とラインナンバーをError.logに出力する
#define TRACE Err.Out("%s(%d)",__FILE__,__LINE__);

現在のファイル名と実行行を出力するマクロである。至るところに、このTRACEを仕掛けてどこまでが実行されているかを調べるのである。俗に言うロギング(logging)である。

そうは言うものの、そういう悠長なことも言ってられない。もっとスマートにデバッグできないのか?せめて、どの関数でメモリエラーが起きているのか調べられないのか?

方法はある。一つは、プロジェクト→設定→リンク→デバッグで、Mapファイルを生成することである。これで、どの関数がどのアドレスに位置しているのかがわかる。(普通のプログラムならば、このアドレスに0x400000を加算したのが実効アドレスだと考えて良い) これでエラーアドレスから関数を特定出来る。

もう一つの方法は、同じくプロジェクト→設定→リンク→デバッグで、デバッグ情報を出力することである。こうしておけば、実行中にメモリエラーが起こったならば、即デバッグに入ることが出来る。

しかし、この二つの方法では、メモリエラーが遅れて発生するような場合、あるいはメモリエラーが発生しない場合などは役に立たないかも知れない。そこで、もっと直感的なデバッグ手法を考えてみよう。

まず、根本に立ち戻って、DebugビルドとReleaseビルドの最大の違いは何か?

やねうらおも、こんなところにおいそれと書けるほど良くは知らないのだが(笑)、とりあえず初期化されていない変数の値が違う。Debugビルドであれば、0xcdcdcdcdで初期化されているものの、Releaseでは不定である。0のときもあれば、デタラメな数字であることもある。

そのため、Releaseビルドにして、不安定な動作をしているならば、それは変数の初期化忘れが第一候補として考えられる。クラスのコンストラクタですべてのメンバ変数の初期化は正しく行なわれているかぐらいはチェックしたほうが良いだろう。

二つ目の違いとして、実経験に基づくものだが、データ列のデリミタとしてNULLを使っているような場合、デバッグビルドの場合たいていデリミタのNULLを忘れていても、どこかにあるNULLで停止することが多い。ところが、Releaseビルドだと、行くところまで行きついてしまって、メモリエラーで落ちることが多い。

話はそれるが、それと言うのも、C++ではデータ列で、最後はNULLというのが保証されているような柔軟性のあるstruct定義

例)
 STEXT_DATA gText {
   "test.txt",
   "test2.txt",
   NULL, // ←最後がNULLを保証したい。
 }
;

が不可能というのもこのようなバグを誘発する原因として挙げられるかも知れない。(というか、それをサポートしている言語を私は知らないが) 型の正当性チェックとともに、structの正当性チェックという考え方も今後、流行るかも知れない。(全然、流行らないかも知れない^^)

三つ目は、メモリのガーベジ(?)による違いである。deleteしたオブジェクトに対してアクセスを行なった場合、もちろん動作は保証されないわけだが、DebugとReleaseで挙動が異なる(メモリエラーの発生するタイミングが異なる)ことがある。詳しくはよくわからない^^

しかし、最近、まともなOOP言語を見ていると、C++のように他のオブジェクトを指すポインタをオブジェクト消失後も持ち続けることが出来るというのは、いいのか〜とか思わないでもない。これでは、よほど気をつけて管理しないと、バグの原因となる。スマートポインタがコストがかかると言うならば、たとえばオブジェクト消失時に、何らかの方法でそのオブジェクトを指すポインタはすべてNULLにして、かつNULLポインタによる関数呼び出しは行なわないようにしてあればいいような気もする。そもそも、クラスのインスタンスのメンバ関数呼び出しは、オブジェクトに対してメッセージを送ることであって、オブジェクトが存在しない(受け手が存在しない)ならば、メッセージは当然届かなくて良いと思うのだが...。

('00/08/21 追加記事)

第BD回で紹介した、鈴希 零さんから、こんなメールが来てました。

VC++ では Debug バージョンの場合、delete すると
new する前の状態(0xcd とか 0xfd とかが埋まってる状態)に戻すようです。
そうすることによってメモリリークを検出できるようにしてます。

当然 Release ではそんな余計なコトしないから、
delete したあとも、上書きされてなければアクセスしてもヘーキなんです。

ってことだと思います。多分。
ホントに new する前の状態に戻すのかどうかは知りません。
雰囲気だけ汲み取ってやって下さい。

で、結局困ったときのデバッグ手法は何だったんですか?

にゃるほど〜。Debugビルドだとnewとかdeleteってやったら遅いですよね。きっと、そういったメモリリークとか、もろもろチェックしてるんでしょうね。

で、「結局困ったときのデバッグ手法」は、これだけやってダメなら、もう天に祈るしかないとか..(笑)

('00/08/22 追加記事)

吉田さんからは、こんなメールが来てました。

 その2「Map ファイルからエラーが発生したソース行を特定できる」
 
 #以下は、仕事で他の開発メンバーに送ったメールですが... (^_^;
 
 MSJの No.66 [BugSlayer]の話題に、クラッシュアドレスから、問題が発生したソースの行を取得する方法が載ってます。
 詳しくは、その部分を読んでもらえばいいですが、とりあえず、アプリケーションのビルド時にしておかなくてはならないことだけを書きます。
  1.プロジェクトの設定で、Link プロパティでMAP ファイルを作成する のオプション
 を付ける。
 2.オプションのエディットボックスに以下のパラメータを追加。
   /MAPINFO:EXPORTS /MAPINFO:LINES
 3.リリースしたバイナリの MAP ファイルをきちんと保管しておく。
 またそのアプリケーションが 自作の DLL を使用する場合は、DLL の基底アドレスを変更しておくこと。この方法の詳細は MSJ No.55 [BugSlayer] に書いてあります。
 
 手元に MSJ がないと不十分な情報なんですが、まあ、こういうこともできるというだけでも。

どうしようもないときはMapファイル出して、あとは逆アセしてました^^>私
かなりマヌケなことしてたんでしょうか(笑)>私

それから、沙夢さんからは、こんな情報をいただきました。

はじめまして、沙夢(シャム)と申します。

いつもやねさんの「Road To Super-Programmer」を楽しく読ませて頂いて
おります。年齢も(多分)近いこともあって、『こんなんわかるんか!?』とか
書いてあるネタで爆笑していたりします。


さて、第CO回での VC++ のデバッグの話なのですが、デバッグビルド時の
メモリ確保1回で、次のようになります・・・多分(^^;)。

new時:
+----(28バイト)----+--(4バイト)--+--(確保バイト数)--+--(4バイト)--+
| デバッグ管理領域 | FD FD FD FD | CD CD .... CD CD | FD FD FD FD |
+------------------+-------------+------------------+-------------+
                           ↑このアドレスが返される

delete時(解放済みのブロックを使わない場合):

+----(28バイト)----+--(4バイト)--+--(確保バイト数)--+--(4バイト)--+
| デバッグ管理領域 | FD FD FD FD | DD DD .... DD DD | FD FD FD FD |
+------------------+-------------+------------------+-------------+
                           ↑ 0xDD で埋める

#確かデフォルトは「解放済みブロックを使わない」です。


ということで、確保したバイト数を超えたかどうかのチェックに前後の "FD"
を、解放されたかどうかをチェックに "DD" を使っているみたいです。

また、deleteしたオブジェクトに対してのアクセスでリリースとデバッグで
挙動が異なるのは、解放したメモリを新たに使う(リリース)か
使わない(デバッグ)か、っていうのがあると思います。


Java だと確保したら自分では解放できない(newはあるけどdeleteがない)
ので、メモリ管理をシビアに考えなくて良いなぁ、とか最近思いました(笑)。

以上、参考になれば幸いです(^^)。
それでは。

にゃるほど、そうだったのか〜。勉強になるなぁ...。ありがとね>沙夢さん


戻る