今回は、メモリの一部をフレームバッファに設定し、その内容を指定されたビットマップ(のデバイスコンテキスト)に描画するビデオシステムを作ってみます。また、そのテストのために仮想CPUにも加算系コードとジャンプコードを追加しました。
今回のビデオシステムは、以下のような仕様です。
基本的には、メモリの一部に割り当てられたフレームバッファを指定されたデバイスコンテキストに描くだけなのですが、描画のタイミングをI/Oで指定します。つまり、メモリの特定アドレスをI/Oポートとして使用し、そこに1が書き込まれたら描画するわけです。フレームバッファは1ピクセル32ビットですから、描画時にはこれを32ビットDIBのピクセル列として扱えば良いでしょう。
では、具体的にビデオシステムを実装していきましょう。今回は、ビデオシステムをCVideoクラスで定義します。ただ、ビデオシステムは環境による差が大きいので、仮想CPU同様にインターフェースCVVIDEOを通して仮想マシンと接続することにしました。
class CVVIDEO { public: virtual I32 getIOSize() const=0; virtual bool setIO(UI32)=0; virtual bool setVRAM(UI32,I32)=0; virtual bool proc()=0; }; class CVideo { public: CVideo(CMemory *,HDC *); virtual I32 getIOSize() const; virtual bool setIO(UI32); virtual bool setVRAM(UI32,I32); virtual bool proc(); private: BITMAPINFO biInfo; HDC *lpHdc; I32 i32Width,i32Height,i32Bytes,i32Length,i32VRAMSize; UI32 u32IO,u32Pixels; CMemory *pmMemory; bool setSize(I32,I32); bool setBpp(I32); };
今回のビデオシステムに必要な情報は、メモリシステムへの参照、描画用のデバイスコンテキスト、そしてメモリシステム上のフレームバッファとI/Oのアドレスですね。これらの情報は、ビデオシステムクラスのコンストラクタとアドレス設定用関数で設定するようにしました。
CVideo::CVideo(CMemory *pmArg,HDC *lpArg) { pmMemory=pmArg; lpHdc=lpArg; // DIBヘッダ設定 biInfo.bmiHeader.biSize=sizeof(BITMAPINFOHEADER); biInfo.bmiHeader.biPlanes=1; biInfo.bmiHeader.biCompression=BI_RGB; setBpp(32); setSize(128,128); } I32 CVideo::getIOSize() const { return 1; } bool CVideo::setIO(UI32 u32Arg) { u32IO=u32Arg; return true; } bool CVideo::setVRAM(UI32 u32Arg,I32 i32Arg) { u32Pixels=u32Arg; i32VRAMSize=i32Arg; return true; }
また、以下のような色形式・描画サイズの設定関数も定義しておきます。
bool CVideo::setBpp(I32 i32Arg) { if (i32Arg!=32) return false; i32Bytes=4; biInfo.bmiHeader.biBitCount=i32Arg; return true; } bool CVideo::setSize(I32 i32Arg1,I32 i32Arg2) { if (i32Arg1 % 4!=0 || i32Arg2 % 4!=0) return false; i32Width=i32Arg1; i32Height=i32Arg2; i32Length=i32Width*i32Bytes; biInfo.bmiHeader.biWidth=i32Width; biInfo.bmiHeader.biHeight=-i32Height; return true; }
仮想マシンの方では、起動時にCVideoのインスタンスを生成し、アドレスを割り当てます。今回は、I/Oにアドレス128の1バイトを、フレームバッファに129からの65536バイトを割り当てるようにしました。lpArgはメインプログラムから渡された、ビデオシステムが描画すべきデバイスコンテキストのポインタです。
pmMemory=new CMemory(128+1+128*128*4); pcpCpu=new CCpu(pmMemory,0); pvVideo=new CVideo(pmMemory,(HDC *)lpArg); pvVideo->setIO(128); pvVideo->setVRAM(128+1,128*128*4);
仮想マシンは、1ステップ毎にビデオシステムの実行関数proc()を呼び出します。呼び出すタイミングは、仮想CPUの実行後にしましょう。
bool CVm::step() { try { pcpCpu->exec(); } catch (logic_error e) { MessageBox(NULL,e.what(),"エラー",MB_OK); return false; } pvVideo->proc(); return true; }
ビデオシステムのproc()では、I/Oに割り当てられた領域に1が書き込まれていたらフレームバッファを描画し、0に戻す処理を行います。つまり、直前に行われた仮想CPUの処理でI/Oに1が書き込まれると、ここでフレームバッファのピクセル列が描画されるわけです。
bool CVideo::proc() { if (pmMemory->read8(u32IO)==1) { StretchDIBits(*lpHdc,0,0,i32Width,i32Height, 0,0,i32Width,i32Height, pmMemory->getBufferAddress(u32Pixels),&biInfo, DIB_RGB_COLORS,SRCCOPY); pmMemory->write8(u32IO,0); } return true; }
以上のことから、プログラム中でビデオシステムを通して描画を行う流れは以下のようになります。
たとえば、128×128ピクセルの中央(64,64)に青(0x000000ff)を書き込んで表示する場合、(64,64)のアドレスは129+64*4+128*64*4=33153(0x00008181)なのでここに0x000000ffを書いてからI/Oのアドレス0x80に1を書き込みます。
ld32 r0,00008181 ld32 r1,00000080 ld32 (r0),000000ff ld8 (r1),01
次に、フレームバッファに連続的に値を書き込んでいくため、仮想CPUに加算系コードとジャンプコードを追加しておきましょう。今回追加するのは以下のコードです。
コード値 | 定数 | ニーモック | コードサイズ |
10 | ADDRR32 | add32 r1,r2 | 4 (0010r1r2) |
---|---|---|---|
11 | ADDRI8 | add8 r1,Imm8 | 4 (0011r1Imm8) |
12 | ADDRI32 | add32 r1,Imm32 | 8 (0012r100,Imm32) |
13 | ADDRM8 | add8 r1,(r2) | 4 (0013r1r2) |
14 | ADDRM16 | add16 r1,(r2) | 4 (0014r1r2) |
15 | ADDRM32 | add32 r1,(r2) | 4 (0015r1r2) |
16 | ADDMR32 | add8 (r1),r2 | 4 (0016r1r2) |
90 | JUMPI32 | jump Imm32 | 8 (00900000,Imm32) |
加算コードは、いずれも右側の値を左側に加算し結果を左側に代入する、というもので、基本的に代入系と同様メモリシステムのread/writeやレジスタ操作を中心とした処理になります。ただ、加算コードでは処理中にオーバーフロー(桁あふれ)が起こった場合はステータスレジスタのOFフラグ(SR_OF)をセット、起こらなければリセットしましょう。この判定のために、32ビットの値2つを足した結果が0xffffffffより大きくなる(オーバーフロー)か判定する関数u32OF()を以下のように定義しておきます。
bool CCpu::u32OF(UI32 u32Arg1,UI32 u32Arg2) const { UI32 u32Wrk=0xffffffff; u32Wrk-=u32Arg1; return u32Wrk<u32Arg2; }
加算処理では、この関数が返す結果をOFフラグを設定するsetSROF()に渡すようにしました。たとえば、この関数を使って「r1にr2で指定されたアドレスの内容を32ビット符号なし整数として加算する」ADDRM32は、以下のように処理します。
case ADDRM32: u32Wrk1=getRegister((u32Code & 0xff00) >> 8); u32Wrk2=pmMemory->read32(getRegister(u32Code & 0xff)); setSROF(u32OF(u32Wrk2,u32Wrk3)); setRegister((u32Code & 0xff00) >> 8,u32Wrk1+u32Wrk2); u32Addr=u32Arg+4; break;
ジャンプコードは単純にコードの後4バイトで指定された値をPCに設定するだけです。
case JUMPI32: u32Addr=pmMemory->read32(u32Arg+4); break;
メインプログラムでは、以下のようにしてビデオシステムが描画するビットマップを作成し、そのビットマップに描画するHDCを仮想マシンに渡しています。あとは仮想マシンが渡されたHDCをビデオシステムに通知することで、ビデオシステムがこのHDCを描画対象として認識するわけですね。
hBMP=CreateCompatibleBitmap(GetDC(hwnd),128,128); hdcScreen=CreateCompatibleDC(GetDC(hwnd)); SelectObject(hdcScreen,hBMP); pvmVm=new CVm((void *)&hdcScreen);
今回は、メモリの先頭部分に以下のようなループ内でアドレスを加算しながらフレームバッファ領域に水色(0x0000ffff)を書き込み、続いてI/Oに1を書き込むプログラムを入れておきました。
0000 ld32 r0,0x00000181 0008 ld8 r1,0x80 000c ld32 (r0),0x0000ffff 0014 add32 r0,0x00000200 001c ld8 (r1),0x01 0020 jump 0x0000000c
プログラムでは、まずr0にフレームバッファの(0,64)のアドレス(0x00000181)を、r1にビデオシステムのI/Oのアドレス(0x00000080)を入れます。そして、r0のアドレスに0000ffffを書き込んで水色の点を打ち、512を加えることでr0にフレームバッファ上の次の座標(一ライン下)のアドレスを入れます。これでフレームバッファに点が打たれたので、次にr1のアドレスに1を書き込んで結果を描画。最後に、点を打つコードにジャンプしてループ先頭に戻ります。
実行すると左下の方に黒い描画領域が表示されるので、進行ボタンでコードを実行し続け、ここに水色の点が打たれていく様子を確認してください。さらに、r0/r1やループするPCの値も確認してみましょう。