2013/06/27

MacBook Pro 17inch Early 2011をSSD(6Gbps)に換装

■ 事の背景というかあらすじ
日頃から気分転換して作業したいなーとか
手軽なマシン無いかなーとか
MackBook Air(以下MBA)持ってないなーとか
ほしーなーとか、
よこせよーくれくれーおらーとか思ってたのですが、

最近発売されたMBAがあんまり魅力的だったので、
テヘペロッて感じで購入しました。

最新のMBAのなにが良いと言えば、
・CPUがHaswellで1日作業に耐えうるバッテリー(12時間)
・IEEE 802.11acに対応で、環境さえあればWi-Fi接続が1Gbps超える
・USB3.0がデフォ。Thunderboltにも対応
・SSDの接続規格がSATA6Gbpsを更に超えるPCIe
という凄まじきスペックで、
持ち運べるノートが欲しいなら買うしか無いレベルだったというわけですが、
その中でもSSDすごいなーっていう。
いや、Haswellも相当すごいんですが。


初めてSSD搭載機を所持したわけですが、やっぱりほんと早いんですね。
スリープから画面が立ち上がるまでに1秒くらい。
で、すごいなーすごいなーと言って触ってる横には、
メインマシンであるMacBook Pro(以下MPB)が(´・ω・`)ショボーンとしていたわけです。

さてそのMBP、
環境としてどう使っているかといえば、
・Xcodeでプログラミング&シミュレータでhogehoge
・偶にCubaseで音楽制作
・Parallels(CPU2コア、ドライブ80GB、メモリ3GB割り当て)
・同Windows 8 ProでPhotoshop CS4 Extended
・同Windows 8 ProでVisual Studio 2010 Express
・同Windows 8 Proで偶にゲーム

流石にこれだけやるとメモリ8GBでは使いきってしまって、
もう無理と思って16GBにしましたが、下手をするとギリギリまで行きます。
(普段は空き4GB〜8GB程度)
Parallelsで3GB以上メモリを食べちゃうので、まあそんなもんです。

で、購入して3ヶ月くらいでMBPのメモリを変えたのは効果大で、
結構サクサクになったんですね。
まあただ、やっぱりParallelsとかの仮想環境は、
まあ・・・いいかな位にしか動かなかったんですね。

今回のMBAのサクサクな動きをみて、
ああ、HDDのMBPもう無理だわってなって、精神的限界を迎えたので、
おもむろにSSD化だよね!ってなりました。



■ 作業対象について
タイトルにもありますが、以下MBPのスペック。
プロセッサ  2.2 GHz Intel Core i7
メモリ  16 GB 1333 MHz DDR3
ソフトウェア OS X 10.8.4(12E55)
ストレージ HDD 750GB

で、このHDDをCSSD-S6T256NHG5QというSSDに換装しました。
CFDの東芝製チップ採用のS6TNHG5Qシリーズってヤツです。
理論値として、
・Seq-Read 530MB/s
・Seq-Write 490MB/s
というハイスペック。ホントかいなー。
メーカー製品ページにもWinの定番CrystalDiskMarkによるベンチマーク結果ではほぼ公称スペックの結果が出てます。
6Gbpsでの接続というところがミソですね。

で、このSSD、購入時点でネット上に成功例が無かったため、
面白そうなのでコレに決めた次第です。
多分、秋葉の有名なMac店のサイトの購入ページが要因にもなってると思います。
あそこには同時期のMBPの内、17インチモデルだけは動作保証外とあって、
私も最初はためらいました。

まあでも、冒険でしょっ!( ゜∀゜)o彡



■ 作業内容
換装の方法は正直今さら書いても仕方ないですね。
沢山の先人という名の人柱ブログが存在していますから、
検索すれば直ぐに出てきます。

とりあえず、今回参考にさせて頂いたのはこちらの記事
ありがたやー(´・ω・`)

とりえずやることは、HDDに構築したシステムをSSDへ完全に移行すること。
大雑把な項目としては、
・SSDに換装する(上記リンク参照)
・Command + R押しでMacを立ち上げ、リカバリモードに入る
・タイムマシンかディスクユーティリティでSDDを初期化後データ移行

なお、リカバリモードは、ネットに繋がっていれば、
勝手にOSイメージをダウンロードしてきてくれますから、
先にSSDにOS Xをインストールとかはしなくて良かったです。
Macすごい! 感動しました。



■ 作業で困ったことや注意点
・【注意点1】トルクスドライバと2.5インチ外付けHDDケースを事前に用意!
 先人達のありがたい記事を流し読みするという愚行の結果、
 MBPの裏蓋を開けてから3時間ほど名古屋の大須商店街をさまよった末に、
 ドスパラ2Fと同ビル3Fにてトルクスドライバを含む、
 精密ドライバセットの入手に成功しました。(買ったのはは2Fで見つけたヤツ)

 前日に久しぶりの運動をして筋肉痛と疲労がガッチンコしてました。
 トルクスドライバの旅に出たときにはアレな魚の眼でした。
 必要サイズは各ブログで言われている通り、T6サイズです。

 取り出したシステムディスクを外付けHDDにするためには無論ケースが必要です。
 これも事前に購入します。安いところではUSB3.0対応で1000円もしないです。
 こっちはアメ横のツクモで780円のを買いました。
 いまはMBAにUSB3.0接続して快適に使用してます。

 参考:荒ぶりながら買ったドライバセットはこんなヤツ↓
 

【注意点2】データ移行の際のタイムマシンとディスクユーティリティは取り扱い注意!
 HDDからSSDへのデータ移行には、2つの方法があります。
 一つはTime Machine(タイムマシン)をつかう方法、
 もう一つはディスクユーティリティの復元機能を使う方法です。

 タイムマシンデータを使った移行についての注意点。
 タイムマシンは、基本的に初回バックアップのみフルで行ってくれます
 データ移行をする際は、フルバックアップのデータのみが対象です
 よって、そのデータがないとタイムマシンは使えません。
 残念なことに、私の場合はフルバックアップしていたにも関わらず、
 そのデータを認識してくれませんでした。
 もしかしたら、USB2.0接続で外付けHDD化していたので、
 認識するのに時間がかかりまくっていたのかもしれませんが、
 耐え切れなかったので、ぶった斬・・・諦めました。

 ディスクユーティリティを使ってSSDへシステムを復元する方法の注意点。
 取り外したHDDのOS X(システム)のパーティションのサイズは、
 復元先のストレージの容量以下でなければ復元出来ません。
 復元前にシステムディスクのパーティションサイズを変更する必要があります
 (要数時間)

 私の場合は、システムディスクを外付HDDとしてから、
 その上でパーティションサイズを変更しました。
 システム入りのパーティションの情報に少々不備があったらしく、
 パーティションサイズの変更を拒否されたため、
 試行錯誤していて、結局そうなりました。

 また、Mac起動時に外付けにしたシステム入りのHDDを接続していると、
 どうも、パーティションに関わる操作を拒否られました。
 外付けHDDの接続は、リカバリモードのメニュー画面が出てからで良いです。

 これらが正しく行われていれば、復元機能を使い、全く同じ環境をSSDに移行できます。
 移行アシスタントよりも精度良いです。(これも要数時間)
 ただし、一部の認証付きのソフトウェアは再認証が必要な場合があります。



■ 結果報告
非常に快適です。
MBPがMBPの威厳を取り戻して、いまは(`・ω・´)シャキーンとしてます。

動作保証とかは自己責任の世界なので元から気にしてませんが、
やってよかった感が凄いです。

Xcodeの立ち上がりは20秒近くだったのが、3〜5秒で立ち上がります。
Parallels上のWindowsのレジューム時間も、今まで20〜30秒だったのが、5〜8秒。
Photoshop CS4 Extendedも3秒以内にで立ち上がります。
とにかくParallelsの動作が圧倒的に高速になりました。


ベンチ結果は噂ほど大したことは無かったですが、これは環境によると思います。
ほぼ公称スペックで報告を上げている人も、某価格サイトにはいるので、
一概にどうとは言えませんが、とりあえず結果を貼っておきます。

測定はXbench Version 1.3にて行いました。

MacBook Pro 17-inch Early 2011 SSD換装後ベンチマーク結果
MacBook Pro 17-inch Early 2011 SSD換装後のベンチマーク結果



公称スペックは流石に出なかったものの、シーケンシャルReadが異常に高速。

このMBPとはまだ長い付き合いになりそうです。
そんでもって、たまには気分転換にMBA持ち出して、
カフェプログラミングを満喫したいところですな。

2012/11/26

コマンドプロンプトアプリケーションにおける終了時処理について

コマンドプロンプトアプリケーションにおける終了方法は、主に2つあります。

1つは、アプリケーションがプロセスを終え、自ら終了する場合。
もう一つは、ユーザが窓を閉じたり強制終了した場合です。

前者は普通、しっかり後処理されるプログラムであれば、正常に終了します。
問題なのは後者です。

プレイヤーが窓を閉じたり強制終了するというのは、割り込み処理であり、
本筋のプログラム実行中に、強制的に、
突然発生するものであるという事を念頭に置かなければなりません。


具体的な話として、
最近流行り(?)のオートセーブ機能付きのゲームを実現したいとします。
ここではゲーム終了時にゲームデータを保存します。

適当ですがこんな感じで書きました。(雰囲気です)
=============================================
int main(int argc, char* argv[])
{
  int ret = 0;

  // 初期化処理
  if(!GameInit()) {
    /* 何らかのエラー処理 */
    ret = -1;
  } else {
    // 実行処理(ゲームループ)
    if(!GameRunLoop()) {
      /* 実行時エラー */
      ret = -2;
    } else { 
      // データ保存処理
      GameSave();
    }
    // 終了時処理
    GameEnd();
  }

  return ret;
}
=============================================
ゲームループを抜けると、
実行中に異常検知さえなければ必ずデータ保存処理が実行されるため、
終了時にはオートセーブされるといった具合の作りです。
このソースだけ見ても特に大きな問題はないと思います。

問題は、ゲームループ中に窓を閉じられた場合です。
というよりこの作りからして、
初期化処理、保存処理、終了時処理は一瞬で終わるとした場合、
プレイヤーが窓を閉じるタイミングは、ほぼ100%ゲームループ中です。
そこで割り込みが発生するとどうなるでしょうか?

結論から言って上記の場合、以下のような悲惨な流れになります。
1.GameInit <= 実施
2.GameRunLoop <= 実施中に強制停止
3.GameSave <= 実施されず(頑張ったゲームデータが死亡)
4.GameEnd <= 実施されず(解放漏れ。最悪何かのハンドルを掴んだまま)

勿論意図するところとしては、1〜4本来全てを実施させたいのですが、
コンソールアプリケーションではこういう事が普通に起こります。

基本的にGUIでも同じことは言えますが、
GUIの場合、そもそもIDEを使用していることがほぼ当たり前で、
そういったIDEが終了処理を記述すべき場所を最初から提供していたりして、
注意不足でない限り、間違いは起こらないようになっています。

CUIはそういったものは用意されておらず、
つまり、最初から自分で意図して処理せねばなりません。


それほど詳細に調べ上げてた訳ではありませんが、
問題の内容をもう少し掘り下げてみます。

コマンドプロンプト(cmd.exe)に限った話をしますと、
まず、「閉じるボタン」ですが、
これは全てのスレッドを強制的に停止する悪魔のようなボタンです。
このボタンが押されると、割り込み要求を掛け、
その時点でプログラムはメインスレッドを停止しています。
割り込みスレッドをにて何らかの処理を挟み込まない限りなにも処理できません。

じゃあその割り込みスレッドに終了処理を挟み込めばいいと思うのが普通ですが、
しかし、簡単にはいきません。
それは、もうひとつの終了方法があるためです。

そのもうひとつの終了方法が「強制終了」です。
Ctrl + Alt + Delでタスクマネージャを起動してから良くやるアレですね。
強制終了を行うと、実は閉じるボタンとは違う動きをします。
そこが大きな問題になります。

強制終了ではメインスレッドに対して、終了要求をかけているだけです。
そこから一定の時間が経ってプログラムが自主的に終了しない場合、
ユーザに終了確認を取ります。ダイアログのアレです。
そこでユーザーが許可して、初めて全スレッドを停止します。

強制終了は閉じるボタンとは違い
タスクマネージャから強制終了をかけた瞬間に割り込み要求が発生します。
しかも、メインスレッドは割り込まれているだけで、停止はしていません。
そこが閉じるボタンとは全く違っていて、
つまり、割り込みスレッドが発生した後、
メインスレッドに割り込んだ位置へとプログラムカウンタを復帰し、
出来る限り処理を続行しようと努めます

その後問題なくプログラムが動き、ユーザーが強制終了を承認しない限り、
閉じるボタンのように本当に途中で強制終了することはありません。


正しい終了時処理前提として、システムの割り込みスレッドを捕捉し、
そこへユーザー処理を挟みこむ必要があるというのは確かですが、
以上のような「閉じるボタン」と「強制終了」の違いを理解していなければなりません


前置きが長くなりましたが、
この割り込みスレッドの中でユーザ定義の処理を挟み込むには、
以下のようにハンドラ関数をWindowsに事前通知しておく必要があります。
=============================================
#include <windows.h>

// 割り込みによるゲーム終了要求フラグ
static bool g_InterruptEndReq = false; 

int main(int argc, char* argv[])
{
  int ret = 0;

  // ハンドラ関数を登録
  SetConsoleCtrlHandler(HandlerRoutine, TRUE);

  // 初期化処理
  if(!GameInit()) {
    /* 何らかのエラー処理 */
    ret = -1;
  } else {
    // 実行処理(ゲームループ)
    // (内部ループ条件でg_InterruptEndReq == false判定)
    if(!GameRunLoop()) {
      /* 実行時エラー */
      ret = -2;
    } else { 
      // データ保存処理
      GameSave();
    }
    // 終了時処理
    GameEnd();
  }

  return ret;
}

// ハンドラ関数
BOOL WINAPI HandlerRoutine(DWORD dwCtrlType)
{
  BOOL ret = FASLE;

  // ウィンドウの終了イベント検出時
  // ※「閉じるボタン」でも、「強制終了」でも同じフラグ
  if(dwCtrlType == CTRL_CLOSE_EVENT) {
    // 割り込みによるゲーム終了フラグを立てる
    g_InterruptEndReq = true;
    ret = TRUE; // ハンドラ関数をの連続呼び出しを終了する
  }
  return ret;
}
=============================================
あくまで雰囲気ソースですが、
このようにすると、Windowsはコマンドプロンプトを閉じようとした場合、
ハンドラ関数へ特定のイベント処理をコールバックします。
そこで終了要求のフラグを設定し、
ゲームを終了しなければならないことを、ゲームループに通知します。

察しのよい方はお気づきかもしれませんが、
先ほどまでの説明の通り行くと、
このプログラムでは「閉じるボタン」を押された際には終了処理が発生しません。
そのままプログラムは落ちていしまいます。
なぜなら「閉じるボタン」からのはハンドラ関数終了時には、
メインスレッドが死んでおり、戻る場所がないからです。

しかし、こうしなければならない理由があります。

「強制終了」の場合、ハンドラ関数が呼び出された時点では、
メインスレッドの終了が確定していないため、
ハンドラ関数終了後、割り込みが発生したプログラムカウンタ位置に戻ります

つまりどういう事かというと、
ハンドラ関数で直接終了処理を行ってしまった場合
その後のメインスレッドの処理では当然復帰した位置から処理を継続しようとするため
終了処理にて破壊されたアドレスやら変数やらを使用することになってしまうのです。

ですから、コマンドプロンプトのゲームに関して言うなら、
ハンドラ関数で投げて良いのは終了要求のみだということになります。


また、上記ソースのコメントにもあるように、
ハンドラ関数では「閉じるボタン」か「強制終了」かを区別せず、
両方ともウィンドウを閉じるというイベントとして通知されてきます
そのために、ウィンドウの終了方法についての判定は出来ません。

ですので、ハンドラ関数で終了処理をしてしまうと、
「閉じるボタン」は良いかもしれませんが、
「強制終了」時は、ほぼ確実にメモリアクセス違反を起こします。


では「閉じるボタン」の処置はどうするか?

たどり着いた答えとしては、
閉じるボタンを無効にする意外に安全な道は無いということです。

あくまでここでつくろうとしているのはゲームですから、
幸い、ゲーム内メニューから終了することや、
ESCキーで終了するような動作はおおよそ一般的に考えて良いと思います。


そういうわけで、
=============================================
// 閉じるボタンを無効化
HMENU hmenu = GetSystemMenu(hConsole, FALSE);
RemoveMenu(hmenu, SC_CLOSE, MF_BYCOMMAND);
=============================================
として、メニューから閉じるボタンを破棄します。


(2016/01/02 コメントを頂き訂正)
なお、コマンドプロンプトウィンドウのハンドル(上記hConsole)を得る為
直接的なAPIは存在しませんので、以下のように取得します。
以下のAPIを呼び出します。
=============================================
HWND APIENTRY GetConsoleWindow(VOID)
=============================================


古い方法では、以下のような関数を定義・利用することで取得可能です。
(MS公式の資料に基づく取得方法なので、
 恐らくは上記のAPIも同様の動きかと思います。)
=============================================
HWND GetConsoleWindowHandle()
{
  HWND hwnd;
  char newTitle[1024]; // ハンドル取得用ウィンドウタイトル
  char oldTitle[1024]; // オリジナルウィンドウタイトル

  // 現在のウィンドウタイトルを取得
  GetConsoleTitle(oldTitle, sizeof oldTitle);

  // ユニークなタイトルに変更し、ウィンドウハンドルを特定
  sprintf(newTitle, "%d/%d\0", GetTickCount(), GetCurrentProcessId());
  SetConsoleTitle(newTitle);
  Sleep(40); // 誤作動防止に必要
  hwnd = FindWindow(NULL, newTitle);

  // タイトルを戻す
  SetConsoleTitle(oldTitle);

  return hwnd;
}
=============================================

これをアプリケーションの初期化時に組み込むことで、
閉じるボタンは無効となります。
つまりはハンドラ関数は強制終了時にのみ対応すれば良く、
閉じるボタンとの動作の違いに悩まされることは無くなります。


長くなりましたが、以上の仕組みによって、
コマンドプロンプトのゲームの終了時処理を※正しく動作させることが出来ます。


(※ただし、FPSを採用していない(キー入力待ちが発生する)ゲームでは、
 キー入力待ちが終わらない限り、この仕組みでは、
 必ずしもゲームを即時、正常終了させることは出来ません。
 これは当然のことで、キー入力待ちの処理を標準関数やAPI側で処理すると、
 自分では終了タイミングを制御できないからです。
 そこで、コンソールウィンドウをアクティブにして、
 そこへ無理矢理SendInputなどをしてみましたが動作しませんでした。
 これについては暇があればもう少し調査したいですが、
 FPS制御を導入すれば、必然的にハンドラ関数で出した終了要求が
 ゲームループで検知されて、終了処理が走るため、
 簡単に解決してしまう話なので特に対策不要と考えています。)

2012/11/25

コンソール(コマンドプロンプト)でダブルバッファリング


ゲームにおける画面更新で使われるテクニックとして、
ダブルバッファリングは余りにも有名ですが、
それはそもそも窓があって、画像があって、その上で語られることだったりします。

ところで、
コンソールゲームというのは、実験や勉強に向いています。
GUIであるがゆえに起こる、デザインやらの細かな(ゲームプログラミング以外の)話を余り考える必要はなく、
本来したいことに集中できるからで、特にGUIを必要としないアルゴリズムの検討、検証などに便利です。


ただ、少し突っ込んでリアルタイムなゲームをつくろうとか、
そういう事をやろうとすると、最初に必ずげんなりする現象に見舞われます。

つまり、「画面がチラつく」と。

画面を更新する際、本当に簡単な実装をしようとすると、
Windowsでは、system("cls")として画面をクリアしてから、
ゲームオブジェクトなんかをprintfとかcoutとかすることになります。

しかし通常、それらは1つのスクリーンバッファ上で行われるため、
一瞬画面が消えて再描画がされてしまい、
リアルタイムにしようものなら、チラつきが我慢出来ないほどになったりします。


そんなこんなで今回、
そういった不満を解決するべく、
コマンドプロンプトでダブルバッファリングをしようというお話です。

まず前提として、
ここで紹介するのはあくまでもWindowsのコマンドプロンプトの話で、
加えてWin32APIを使用します。そのためWindowsのみの話になるので悪しからず。


さて、
コマンドプロンプトは一つのプロセスに対し、
スクリーンバッファを複数もつことが出来ます。
そしてそれら複数のバッファを、自由なタイミングで切り替えて表示することが出来ます。

具体的に、
「スクリーンバッファを複数もつ」というのは、
============================================================
// スクリーンバッファを作成
HANDLE hSrceen = CreateConsoleScreenBuffer(
    GENERIC_READ | GENERIC_WRITE, 0, NULL, CONSOLE_TEXTMODE_BUFFER, NULL
);
if(hSrceen == INVALID_HANDLE_VALUE) {
 /* エラー処理 */
}
============================================================
などとして必要な分だけHANDLEを確保することで実現されます。


そうして必要な数のスクリーンバッファを新たに作成し、
============================================================
// 対象のスクリーンバッファをアクティブ化
SetConsoleActiveScreenBuffer(hSrceen);
============================================================
として、指定のスクリーンバッファ(描画したいものを反映したバッファ)を
実際に表示するのスクリーンバッファとして指定します。


その選択されたスクリーンバッファに対して、
============================================================
const char* str = "hogehoge";
WORD cell;

// スクリーンバッファを指定して文字を書き込む
WriteConsole(hSrceen, str, strlen(str), &cell, NULL);
============================================================
として、指定のスクリーンバッファに文字(ゲームオブジェクト)を書き込みます


CreateConsoleScreenBufferで作成したスクリーンバッファは、
プログラム終了時などに後処理として、
============================================================
// スクリーンバッファを解放
CloseHandle(hSrceen);
hSrceen = NULL;
============================================================
などととしておきます。


以上に上げたAPIの他にもまだ、ゲームを作る上で必要なAPIがあります。
例えばタイトルを設定したり、
カーソルの表示を切り替えたり、
フォントの色を変えたり などなど・・・

そういった事に関しては、
MicrosoftのオンラインMSDNのコンソールAPIページが参考になります。
ここを参考にすれば大抵はなんとかなるかと思います。


ここで注意事項ですが、
Win32 APIであるCreateConsoleScreenBuffer()は、
作成されるスクリーンバッファのサイズが既にあるウィンドウの複製サイズとなっており、
コマンドプロンプトのウィンドウの大きさとイコールのサイズです。

一方、
============================================================
// 標準のスクリーンバッファを取得
HANDLE hConsoleOut = GetStdHandle(STD_OUTPUT_HANDLE);
if(hConsoleOut == INVALID_HANDLE_VALUE) {
 /* エラー処理 */
}
============================================================
として取得できるスクリーンバッファは、
初期状態では縦のバッファサイズがウィンドウサイズを越えているため、
スクロールバーが付いています。

よって、GetStdHandle()で取得した標準スクリーンバッファと、
CreateConsoleScreenBuffer()で作成したもう1つのスクリーンバッファで、
そのままダブルバッファリングをしようとすると、
例えばFPS制御しているプログラムでは、
スクロールバーが付いている画面と、付いていない画面が交互に表示されてしまい、
目も当てられない事になります。

因みに、GetStdHandle()でハンドルを取得した場合も、
後処理などでCloseHandle()を呼び出して解放する必要があります。


先程のバッファサイズの違いの問題を回避するには、
GetStdHandle()して取得した標準スクリーンバッファのサイズ、
及びをウィンドウサイズを
============================================================
COORD coord = { MAX_WIDTH, MAX_HEIGHT };
SMALL_RECT sr = { 0, 0, (MAX_WIDTH-1), (MAX_HEIGHT-1) };

// バッファサイズ変更
SetConsoleScreenBufferSize(hConsoleOut, coord);
// ウィンドウサイズ変更
SetConsoleWindowInfo(hConsoleOut, TRUE, &sr); 
============================================================
などとするか、
そもそも、GetStdHandle()して取得した標準スクリーンバッファは使用しないことです。


上記のサイズ問題については良いとして、
もうひとつの問題があります。

標準のスクリーンバッファをいじるということは、
後々復帰処理の為に起動直後のバッファの内容を保持する必要が有るという問題です。

その問題から、私は標準のスクリーンバッファを使用しないことを推奨します。

そもそも、コマンドラインから「C:¥>game.exe」などとされた場合、
ゲーム中に標準のスクリーンバッファを使用していると、その内容を上書きしてしまいます。
標準のスクリーンバッファをクリアされるようなことは、
ユーザ兼プレイヤー側としては意図しない現象なのです。

何らかの作業中に、ちょっと寄り道してゲームを起動したら、ゲーム起動前に作業していた内容が全て消えていた、なんて事になるのですから。
そんな事はプレイヤーは想定していないというか、させること事自体おかしいのです。

ですから繰り返しますが、
標準のスクリーンバッファの内容を最初に保持するか、
または標準のスクリーンバッファには手をつけない作り方をしなければなりません。


さて、
ここまでの流れをまとめて、もう少し補足したプログラムとして、
初期に公開していた「カードチェイン」にダブルバッファリング対応したものをソース付きで公開します。
バージョン0.8.0から描画周りのベース処理がかなり改善されています。
ただし、ゲームの内容クラスであるCardChain.cppには一切手を付けていません。
(FPS対応はする必要がないゲームですので、していません。)

また、ダブルバッファリング(描画周り)以外の部分でも、特記すべき事項が2点あり、
それも既に適用していますが、そこはまた別の機会に書きたいと思います。

実行ファイルとソースのセットで、
ソースはVC++ 2010 Expressのプロジェクト付きです。

ゲームルールや操作方法、動作環、ライセンス、免責事項などについては、
前バージョンのカードチェインの説明と同様なので、
そちらをご参照ください。


カードチェイン ver. 0.9.0

▽ Download ▽
https://docs.google.com/open?id=0Bz1zlwZ1RkLWSFdZU3VaVEFiYWc

一括ダウンロード方法が不明な場合は、こちらを御覧ください。



以下、駄文です
〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜
実はダブルバッファリングをせずとも、「チラつきのない描画」はほぼ可能です。
ようは、「一度すべて消すからチラつく」ということです。
必要な部分を毎回上書きするように書き換えるか、
(※)ゲーム画面全体分の描画すべき情報をあらかじめ集約し、
それをもって一度だけのループで完成された画面の描画を行えば、
そもそもダブルバッファリングは不要です。
(※例えば、シングルバッファだと、
 フィールドマップを描画したのち、その上にキャラクターを描画しようしても、
 printfを同じ場所(セル)に2回適用した時点で時間差によるチラツキが発生します。
 その為、一度メモリ上にバッファを持って、その上でマップとキャラクターを統合すれば、
 そのバッファの内容を一度で描画する事ができます。
 応用して、常にゲーム画面全ての内容をそのメモリ上に構築するようにすれば、
 画面全体を1度のの描画で瞬時に書き換えられるため、チラつきはほぼ起こりません。)

が、それというのはそもそも、
コンソールゲームに完全に依存してしまう作りであり、
しかも、(※)そのゲームとは本来関係ない内容を、ゲーム側のソースに埋め込むことになります。
よって、移植性の乏しい物が出来上がってしまうのです。
また、一望しただけではよくわからないものになっている可能性もあります。
(※ただし、前述した描画用のメモリ管理を別機能として提供すればそのような問題は起こりませんが、
 実際にやろうとすると、カラフルなコンソールゲームである場合に不都合が生まれます。
 メモリ統合の際に文字毎の背景色を記憶するわけですが、
 それはマルチバイトのプロジェクトはともかとして、
 Unicodeプロジェクトを作成する場合、UTF-16』で全角半角の判定をし、
 その文字が1セル分なのか、2セル分なのかを判定し、背景色を適用しなければなりません。
 UTF-16では、S-JISやEUC-JPなどと違って、単純に半角と全角を判定できるものではありません。
 使用される文字が限定すればそういう事も出来ますが、
 どちらにしても、そういった制御を行うことで、パフォーマンスが落ちることは避けられませんし、
 非効率であることに変わりはないのです。)
そういうわけで、自由にバッファの内容を上書きできるダブルバッファリングは、
コンソールアプリケーションであっても有効なのです。


2012/10/27

ビットの位置(≒2の冪指数)を静的算出するtemplateメタプログラム

ビットフラグを使用していると、
たまに「そのフラグが立てているビットの位置」を知りたいというときがあります。


結論から言って、
以下のテンプレートクラス(構造体ですが、、)を書きました。
==================================
typedef unsigned int U32;

// ビット位置の静的算出
// 値Nについて、最大のビット位置を求める
//(例:N:127→7, N:128→8
template<U32 N> struct BitPos
{
 enum {
  pos = BitPos<N/2>::pos + 1,
 };
};
template<> struct BitPos<1> { enum { pos = 0 }; };  // 終了条件1
template<> struct BitPos<0> { enum { pos = -1 }; }; // 終了条件2
==================================


これはテンプレートメタプログラミングと呼ばれる、
C++のテクニックとしては古いものです。

このテクニックを利用すると、入力値に対しコンパイル時に値が算出されるため、
実行時の演算コストがなくなります。
コンパイル時に時間的なコストがあるといえばありますが。

動作的原理については、Wikipediaに詳しく書かれています。

このテンプレートは定数posの値を、
コンパイル時の再帰呼び出しによって決定します。

たとえば以下のように使用します。
==================================
static const U32 BIT_FLG = 0x04000000; // 例えばこんな定数がある

std::cout << BitPos<BIT_FLG>::pos << std::endl;


// 結果は26となる
// 0x04000000 => 0000 0100 0000 0000 0000 0000 0000 0000

==================================
 
大まかには以上です。 

※ 終了条件2について、-1としていますが、
この辺りは人によって意見が分かれるかもしれません。
個人的には、ビットフラグに0というのはフラグとして成り立っていないと考え、
設定値として無効な値であるから-1というエラー値を設定しています。
これが正しいというには難しいため、好みにより-1でも0でも良いと思います。


〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜
以下は説明と見せかけて駄文です。
興味のある方だけお読みください。


例えば、
==================================
typedef unsigned short U16;
static const U16 BIT_FLG_A = 0x1000;
static const U16 BIT_FLG_B = 0x2000;
static const U16 BIT_FLG_C = 0x4000;
static const U16 BIT_FLG_D = 0x8000;
==================================

という定義があったとして、
ある変数について、このビットフラグを扱うとき、
【0x0000】または【0x0001】という結果を、
演算で求めたい場合があるとします。

そのとき場合のコードはこうなると思います。
==================================
// 入力値 U16 hoge = 0x2018 から0x2000を得て、
// 0x0000 or 0x0001を得る

U16 ret = 0x0000;
ret = (hoge & BIT_FLG_B) >> 13;  // ここにリテラルが発生する
==================================

この例題だと正直、素直にif文でもよいですが、
ここではそういう話はしません。

コメントに記したように、
0x0001を得るために13bit右シフトを意味するリテラルを書くわけですが、
この13というリテラルは気持ち悪いと感じる事もあるでしょう。
だからこの13に対して BIT_FLG_Bの対となる定数をまた用意したりして、
==================================
// BIT_FLG_Bでマスクした結果から 
// 0x0000 or 0x0001を得るための定数値
static const U16 FLG_B_SHIFT_NUM = 13;


// 入力値 U16 hoge = 0x2018
U16 ret = 0x0000;
ret = (hoge & BIT_FLG_B) >> FLG_B_SHIFT_NUM;  // 定数に変更
==================================

とかやってはみるものの、
非常に無駄な作業をしている気がしてならなくなってくるわけです。

もう面倒だから13でいいやとか、投げやりにもなりたくなります。

更に、データフォーマットが変わってしまって、
型がU16からU8(unsigned charとします)に変えなければならない時、

この【FLG_B_SHIFT】の型、定数値も変えてやらなければならないという事態になります。
==================================
static const U8 BIT_FLG_A = 0x10;
static const U8 BIT_FLG_B = 0x20;
static const U8 BIT_FLG_C = 0x40;
static const U8 BIT_FLG_D = 0x80;

// BIT_FLG_Bでマスクした結果から 
// 0x00 or 0x01を得るための定数値
static const U8 FLG_B_SHIFT_NUM = 5;

// 入力値 U8 hoge = 0x24
U8 ret = 0x00;
ret = (hoge & BIT_FLG_B) >> FLG_B_SHIFT_NUM;
==================================
↑こんなふうに。



ではもっと、効率的で意味を可視化できる方法がないでしょうか?


ここで、0x20と5との関係性を考えたとき、
2^5 = 32 (=0x20)であるということがあります。
これは逆に32を2で5回割れば1となるというとです。

と言うことで、
冒頭のtemplateでは、1 < Nの関係が成り立つ間、
N÷2を再帰的に繰り返せばいいじゃないという発想のもと、ビット位置を算出しています。

前述のソースに適用すると、

==================================
static const U8 BIT_FLG_A = 0x10;
static const U8 BIT_FLG_B = 0x20;
static const U8 BIT_FLG_C = 0x40;
static const U8 BIT_FLG_D = 0x80;

// 入力値 U8 hoge = 0x24
U8 ret = 0x00;
ret = (hoge & BIT_FLG_B) >> BitPos<BIT_FLG_B>::pos;
==================================
となり、BIT_FLG_xがどんな値であるかを意識する必要がなくなります。

ただし、一つ難点があるといえば、
BitPos<xxx>::posとかいってますが、一体どこのposなのか判らないということです。

例えば0x24(0010 0100)という2つのフラグがある場合、
これに対してはどこのbit位置なのか?


そこで、冒頭のテンプレートを
以下のように変更することで、より意味を明確にしました。

==================================
// ビット位置の静的算出
// 値Nについて、最大のビット位置と最小のビット位置を求める
//(例:124→most:7, least:2、 128→most:8, least:8
template<U32 N> struct BitPos
{
 enum {
  most = BitPos<N/2>::most + 1,    // 最大位置
  least = ((N & 1) == 0) ? BitPos<N / 2>::least + 1 : 0 // 最小位置
 };
};
template<> struct BitPos<1> { enum { most = 0, least = 0 }; };
template<> struct BitPos<0> { enum { most = -1, least = -1 }; };
==================================
leastはNのLSBが0である間だけbit位置をカウントします。
LSBが1になれば、つまり最初の最小のビット位置を発見したということなので、
再帰呼び出しを終了します。
また、そもそも最初の入力値Nが0というのはビットフラグとして成り立っていないため、
強制的にカウントを-1として終了します。

N=1のときは、LSBが立っていることが明らかであるため、
結果を0ビット目とします。


以上、
長々とお付き合い頂き、ありがとうございました。

ダウンロードリンクの修正について

コマンドプロンプトゲーム「CardChain」のダウンロード先について、
 ダウンロードに承認が必要な状態になっていたようなので、
 承認が不要であるよう修正しました。

ダウンロード手順は難しくはないですが、
少し迷うこともあるかと思うので記載しておきます。


 ダウンロードリンクをクリックすると、以下のようにファイル一覧が表示されます

上部メニューから、「ファイル」を選択します。

ファイルから「ダウンロード」を選択します。

以上です。
今後のダウンロード先も、同じようにしたいと思います。

また、リンク切れなどの問題や、
やこうした方がもっと良いというご意見があればお知らせください。

2012/10/13

ビット逆転アルゴリズムのテンプレート化と最適化について

ビット逆転のアルゴリズムとして、効率的なコードとして、
以下のようなものがよく見られるようです。


=================================
unsigned int v;
v = ((v & 0xaaaaaaaa) >> 1) | ((v & 0x55555555) << 1);
v = ((v & 0xcccccccc) >> 2) | ((v & 0x33333333) << 2);
v = ((v & 0xf0f0f0f0) >> 4) | ((v & 0x0f0f0f0f) << 4);
v = ((v & 0xff00ff00) >> 8) | ((v & 0x00ff00ff) << 8);
v = (v >> 16) | (v << 16);
=================================

1bit単位、2bit単位、4bit単位、8bit単位で交換し、最後に16bit単位で交換した結果、ビットの並びが上下逆となります。
簡単に8bitでトレースしてみると、、

=================================
v == 01111001
  ↓  左側 ((01111001 & 10101010) >> 1)  => 00010100
  ↓  右側 ((01111001 & 01010101) << 1)  => 10100010
  ↓  00010100 | 10100010
v == 10110110
  ↓  左側 ((10110110 & 11001100) >> 2)  => 00100001
  ↓  右側 ((10110110 & 00110011) << 2)  => 11001000
  ↓  00100001 | 11001000
v == 11101001
  ↓  左側 (11101001 >> 4)  => 00001110
  ↓  右側 (11101001 << 4)  => 10010000
  ↓  00001110 | 10010000
v == 10011110
=================================

という感じです。
書きだしてみると少し長くなりましたが、
頭で考えたほうがかえって分かりやすいアルゴリズムだと思います。
上記のアルゴリズムを見つけたときは感心しました。

因みに最初に自作したのは以下のようなものでした。

=================================
// 自作初期型
T val = 0x89ABCDEF;

const T size = sizeof(T) * 8 - 1;
T tmp = 0;
for(T i = 0; i <= size; i++) {
 tmp |= (val & (1 << size)) >> (size - i);
 val <<= 1;
}
return tmp;
=================================


常にvalのMSBを取り出し、
tmpのLSBからMSBへ順番に型のサイズ分だけ繰り返して設定する事で逆転させています。
sizeofによって可変サイズに対応できるため、template可能です。
この場合のsizeofは、通常コンパイル時には最適化によって定数へと置き換えられ、
例えばunsigned short(以下 U16)の時、実質的なコードは、

=================================
// コンパイラ最適化後イメージ
U16 val = 0xABCD;
// const T size = sizeof(T) * 8 - 1; <=削除される
U16 tmp = 0;
for(U16 i = 0; i <= 15; i++) {
 tmp |= (val & 0x8000) >> (15 - i);
 val <<= 1;
}
return tmp;
=================================

と、言った感じになります。


しかしながら、
汎用的ではあるものの、冒頭のアルゴリズムを目にして、
うがががが(;´Д`)。。
となったため、
なんとか冒頭のアルゴリズムをtemplateに適用したいと考えました。

・・・
で、出来たのは以下のコード。

=================================
template<typename t> static T reverseBits(T v) {
 assert(sizeof T <= 4);
 switch(sizeof T) {
  case 4: v = (v >> 16) | (v << 16);
  case 2: v = ((v & 0xff00ff00) >> 8) | ((v & 0x00ff00ff) << 8);
  case 1: v = ((v & 0xf0f0f0f0) >> 4) | ((v & 0x0f0f0f0f) << 4);
   v = ((v & 0xcccccccc) >> 2) | ((v & 0x33333333) << 2);
   v = ((v & 0xaaaaaaaa) >> 1) | ((v & 0x55555555) << 1);
 }
 return v;
}
=================================


switch文が入りました。

冒頭のアルゴリズムの特性として、
各行の交換式は干渉していないということが挙げられます。
なので、順番がどうであれ、最後には交換が出来るはずです。

ということなので、

交換順を逆に定義した上で、
switch文を全て通り抜けるようにしてやることで、必要な分だけ演算を実施させます。


が、このswitch文は入っているように見えるだけで、実際には入らないです。

これは関数テンプレートであり、渡される型はコンパイル時には決まっており、
また、switchの対象が型サイズであるために、これもコンパイル時に解決され、
つまり最初からジャンプ先が決定しているのでswitchする必要がなく、
コンパイラの最適化により、型サイズによって直接的に必要な交換式のみを実行するコードが生成されます。

また、別の問題として考えられるのは、
上記の交換式はすべて32bitのリテラルが組み込まれていることです。
『v = ((v & 0xaaaaaaaa) >> 1) | ((v & 0x55555555) << 1);』
これはキャストが発生するのでは?と懸念が出なくもないですが、
これもよほど頭の悪いコンパイラでない限り、outする型に合わせたコードを生成します。
それに、心配であれば以下のようにしてしまえば確実です。
『v = ((v & static_cast<T>(0xaaaaaaaa)) >> 1) | ((v & static_cast<T>(0x55555555)) << 1);』
これも結局コンパイル時に型が決定するので正しいリテラルになりますね。

というわけで、
仮に各サイズでこの関数テンプレートが実体化された時の最適化後のコードのイメージは、以下のようになります。

=================================
【v == 32bit】
U32 reverseBits(U32 v) {
 v = (v >> 16) | (v << 16);
 v = ((v & 0xff00ff00) >> 8) | ((v & 0x00ff00ff) << 8);
 v = ((v & 0xf0f0f0f0) >> 4) | ((v & 0x0f0f0f0f) << 4);
 v = ((v & 0xcccccccc) >> 2) | ((v & 0x33333333) << 2);
 v = ((v & 0xaaaaaaaa) >> 1) | ((v & 0x55555555) << 1);
 return v;
}
【v == 16bit】
U16 reverseBits(U16 v) {
 v = ((v & 0xff00) >> 8) | ((v & 0x00ff) << 8);
 v = ((v & 0xf0f0) >> 4) | ((v & 0x0f0f) << 4);
 v = ((v & 0xcccc) >> 2) | ((v & 0x3333) << 2);
 v = ((v & 0xaaaa) >> 1) | ((v & 0x5555) << 1);
 return v;
}
【v == 8bit】
U8 reverseBits(U8 v) {
 v = ((v & 0xf0) >> 4) | ((v & 0x0f) << 4);
 v = ((v & 0xcc) >> 2) | ((v & 0x33) << 2);
 v = ((v & 0xaa) >> 1) | ((v & 0x55) << 1);
 return v;
}
=================================


2012/10/13 細かな加筆修正(内容は変更なし)

2012/10/06

コマンドプロンプトゲーム - Card Chain

暇を持て余しているときにコマンドプロンプトでゲーム作りました。
 (一応完結しているが、完全な実装ではないのでver.0.8.0)

実行ファイルとソースのセットです。

なお、ソースはVC++ 2010 Expressのプロジェクト付きです。


■ カードチェイン
▽ Download ▽
https://docs.google.com/file/d/0Bz1zlwZ1RkLWR3FvdVROZFZoaXc/edit

■ 作成環境
 ・Mac(Parallels上にインストールしたWindows XP)
 ・VC++ 2010 Express
 ・Win32 API

■ 動作確認環境
 ・Windows XP、Windows 7
 ・Mac(Parallels上にインストールしたWindows XP, Windows 8)

■ 説明
  1.概要
  ・1人用のカードゲーム
  ・「WhatIf?」だとか、後継の「COOL 104」とルールはほぼ同じ
  ・同じ数字か同じ柄のカードを、出来るだけ多く選択します。
  ・ただし、役によるボーナスなどはありません
  ・52枚の全てのカードを出しきればクリア
  ・10枚以上続けることができて、初めて得点

 2.操作方法
  ・ESCキーでゲームを終了します
  ・左右キーでカードを選びます
  ・ ENTER、Z、スペースのいずれかでカードを決定します

■ ライセンス
 「source」フォルダ内の「lib」フォルダ内のファイルを除き、
 その他の全てのソースファイルは、改変・再配布自由です。

 ただし、 このプログラムは乱数生成にメルセンヌ・ツイスタを使用しています。
 ソースファイルに含まれる「lib」フォルダに含まれているものがそれです。
 この部分に関して、ライセンスは以下のページをご参照ください。
 Mersenne Twister
 
 このゲームではメルセンヌ・ツイスタの関数宣言・定義の一部を改変しています。
 ご使用をお考えの場合は、上記のページにてオリジナルを入手しご使用下さい。

■ 免責
 作者は、このプログラムを使用して起きた不都合・不利益について、
 一切の責任を負いません。ご容赦ください。

■ 連絡事項
 面白そうなご意見や、ダメ出しや、質問等などあれば、
 下記コメント欄やメールなど、状況に応じた方法でご連絡ください。

■ ソースに関して ひとこと
 ・new とかdeleteとか一切不使用なのはそういう作りです。念のため。