啟動NES模擬器,打開我們經典的超級馬裡奧1。
選擇工具->查看器->圖形查看器。會出現如下的一個窗口。
在該窗口上單擊,畫面還會改變。
這些畫面有什麼意義,VirtiaNES模擬器是如何顯示出這些畫面的?
以上幾個問題就是這篇博文的主題了。
響應函數
菜單選項 “圖形查看器” 的響應函數是:
WNDCMD CMainFrame::OnViewCommand( WNDCMDPARAM )
所在文件 Source Files\MainFrame.cpp
VS系列IDE的查找功能應該算是比較強大的,函數具體在哪一行我就不講了。以下是該響應函數的代碼:
WNDCMD CMainFrame::OnViewCommand( WNDCMDPARAM ) { if( !Emu.IsRunning() || !Nes ) return; switch( uID ) { case ID_VIEW_PATTERN: if( !m_PatternView.m_hWnd ) { m_PatternView.Create( HWND_DESKTOP ); } ::SetWindowPos( m_PatternView.m_hWnd, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE|SWP_NOSIZE ); break; case ID_VIEW_NAMETABLE: if( !m_NameTableView.m_hWnd ) { m_NameTableView.Create( HWND_DESKTOP ); } ::SetWindowPos( m_NameTableView.m_hWnd, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE|SWP_NOSIZE ); break; case ID_VIEW_PALETTE: if( !m_PaletteView.m_hWnd ) { m_PaletteView.Create( HWND_DESKTOP ); } ::SetWindowPos( m_PaletteView.m_hWnd, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE|SWP_NOSIZE ); break; case ID_VIEW_MEMORY: if( !m_MemoryView.m_hWnd ) { m_MemoryView.Create( HWND_DESKTOP ); } ::SetWindowPos( m_MemoryView.m_hWnd, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE|SWP_NOSIZE ); break; default: break; } }
第1行,函數的參數WNDCMDPARAM是個宏定義,其實是HWND hWnd, UINT uID
hWnd是父窗口也就是主界面的句柄。uID是菜單項的ID。
13-18,其余行的代碼暫且不管。由於我們選擇的是圖像查看器,所以就來到了這個分支。如果是第一 次執行這段代碼,m_PatternView中的窗口是還未創建的。
m_PatternView中的窗口創建成功之後,將窗口顯示出來,這個函數的任務就結束了。
CPatternView
所在文件 Source Files/PatternView.cpp Header Files/PatternView.h
上節中的 m_PatternView 就是類CPatternView的一個對象。
在CPatternView中,Create函數創建窗口,初始化數據。由於創建了窗口,所以響應函數OnCreate被觸發。OnCreate裡會開啟一個定時器,進而又觸發響應函數OnTimer。
好了,OnTimer才是這個類的重點。OnTimer會不斷讀取最新數據,並顯示在窗口中。
位圖信息頭的初始化如下(Create函數中)
m_BitmapHdr.bih.biSize = sizeof(BITMAPINFOHEADER); m_BitmapHdr.bih.biWidth = 128; m_BitmapHdr.bih.biHeight = -256; m_BitmapHdr.bih.biPlanes = 1; m_BitmapHdr.bih.biBitCount = 8; m_BitmapHdr.bih.biCompression = BI_RGB; m_BitmapHdr.bih.biClrUsed = 16;
從中可以看到顯示在圖形查看器中的位圖 寬128個像素,高256個像素,用8位表示一種顏色也就是256色圖),實際使用了16種顏色也就是調色板有16種顏色)。
再繼續之前,先插入些和NES有關的小知識。NES文件中有兩個調色盤鏡像什麼的暫時不考慮),分別是背景調色盤和精靈調色盤。兩個調色盤各占用16字節的大小,每個字節是一個索引,代表了256色中的一色。
好了繼續。Create函數快結束的地方,有一句代碼。
DirectDraw.GetPaletteData( m_Palette );
m_Palette是一個字節數組, 大小為256 * 4個字節,正好能表示256種顏色。再加上這個變量的命名,我猜測這句代碼的功能是將被索引的256個顏色保存在m_Palette中。
差不多該研究一下OnTimer函數了。函數代碼一起貼出來比較亂,所以分開來貼)
LPBYTE pPAL = (m_SelectPal<4)?&BGPAL[m_SelectPal*4]:&SPPAL[(m_SelectPal&3)*4]; m_BitmapHdr.rgb[0] = m_Palette[pPAL[0]]; m_BitmapHdr.rgb[1] = m_Palette[pPAL[1]]; m_BitmapHdr.rgb[2] = m_Palette[pPAL[2]]; m_BitmapHdr.rgb[3] = m_Palette[pPAL[3]];
m_SelectPal是一個0-7的整數,鼠標左鍵點擊後會加1。這也就是為什麼點擊圖形查看器,上面的畫面會改變。
1-2行代碼的作用就是依次從BGPAL背景調色板)或SPPAL精靈調色板)中取出4個顏色索引值。
3-6行代碼可以看出圖形查看器的每一幅畫面其實只有4種顏色。
m_lpPattern是CPatternView的成員變量。保存的是待顯示位圖的像素數據。以下代碼是m_lpPattern的賦值:
for( INT i = 0; i < 8; i++ ) { if( m_lpBank[i] != PPU_MEM_BANK[i] || PPU_MEM_TYPE[i] == BANKTYPE_CRAM ) { m_lpBank[i] = PPU_MEM_BANK[i]; LPBYTE lpPtn = PPU_MEM_BANK[i]; for( INT p = 0; p < 64; p++ ) { LPBYTE lpScn = &m_lpPattern[i*32*128+(p&15)*8+(p/16)*8*128]; for( INT y = 0; y < 8; y++ ) { BYTE chr_l = lpPtn[y]; BYTE chr_h = lpPtn[y+8]; lpScn[0] = ((chr_h>>6)&2)|((chr_l>>7)&1); lpScn[4] = ((chr_h>>2)&2)|((chr_l>>3)&1); lpScn[1] = ((chr_h>>5)&2)|((chr_l>>6)&1); lpScn[5] = ((chr_h>>1)&2)|((chr_l>>2)&1); lpScn[2] = ((chr_h>>4)&2)|((chr_l>>5)&1); lpScn[6] = ((chr_h>>0)&2)|((chr_l>>1)&1); lpScn[3] = ((chr_h>>3)&2)|((chr_l>>4)&1); lpScn[7] = ((chr_h<<1)&2)|((chr_l>>0)&1); // Next line lpScn+=128; } // Next pattern lpPtn+=16; } } }
PPU_MEM_BANK是一個長度為12的指針數組。前8個指針指向圖案表,後4個指向命名表或屬性表。每個指針指向的空間大小為1K。
想深入了解圖案表、命名表或和屬性表的話,可以下載下面這個文檔,看圖形處理器一章。
http://down.51cto.com/data/951473
下面先講講我對於圖案表、命名表和屬性表的理解。
NES的游戲畫面其實是由32*30個Tile組成的,每個Tile有8*8個像素。NES的畫面只有16種顏色,因此只需要4位就可以表示一個像素。
那麼每個Tile中8*8的像素是如何求得的呢?
命名表中保存了Tile的編號,Tile儲存在圖案表裡。一張圖案表裡有256個Tile,因此尋址一個Tile要一個字節。一張命名表的大小可以算出來了,1字節*32*30=960字節。
圖案表中真正保存的是Tile中的像素的低2位。上面還提到1個圖案表保存了256個Tile,1個Tile有8*8個像素。那麼一個圖案表的大小是2位*256*8*8=32768位=4096字節=4kb
圖案表儲存一個Tile用16個字節,其中1、9字節用來求Tile的第1行像素,2、10字節用來求第二行像素,依次類推。具體計算方式比如,第1行像素的第一個,高1位是第9字節的最高位,低1位是第1字節的最高位。也就是說後八個字節共64位是64個像素的高位,前八個字節共64位是64個像素的低位。假設第1個字節是01010011,第9個字節是10101111,那麼第一行像素就是2 1 2 1 2 2 3 3。
屬性表比較小。1024字節的空間,命名表占用了960字節,剩下64字節就留給屬性表了,屬性表和命名表是配對出現的。屬性表的一個字節保存的是4*4個Tile高2位的像素。計算一下大小32*32/4/4=64字節。
不得不說,這看似別扭的處理方式,在當時那種硬件資源缺乏的年代,還是挺不錯的。
好了,繼續回到我們的代碼。
這是OnTimer函數最重要的一部分代碼為了方便查看,再貼一次)。
for( INT i = 0; i < 8; i++ ) { if( m_lpBank[i] != PPU_MEM_BANK[i] || PPU_MEM_TYPE[i] == BANKTYPE_CRAM ) { m_lpBank[i] = PPU_MEM_BANK[i]; LPBYTE lpPtn = PPU_MEM_BANK[i]; for( INT p = 0; p < 64; p++ ) { LPBYTE lpScn = &m_lpPattern[i*32*128+(p&15)*8+(p/16)*8*128]; for( INT y = 0; y < 8; y++ ) { BYTE chr_l = lpPtn[y]; BYTE chr_h = lpPtn[y+8]; lpScn[0] = ((chr_h>>6)&2)|((chr_l>>7)&1); lpScn[4] = ((chr_h>>2)&2)|((chr_l>>3)&1); lpScn[1] = ((chr_h>>5)&2)|((chr_l>>6)&1); lpScn[5] = ((chr_h>>1)&2)|((chr_l>>2)&1); lpScn[2] = ((chr_h>>4)&2)|((chr_l>>5)&1); lpScn[6] = ((chr_h>>0)&2)|((chr_l>>1)&1); lpScn[3] = ((chr_h>>3)&2)|((chr_l>>4)&1); lpScn[7] = ((chr_h<<1)&2)|((chr_l>>0)&1); // Next line lpScn+=128; } // Next pattern lpPtn+=16; } } }
有以上基礎,看這段代碼就簡單了。
代碼裡有3個for循環。每個for循環的循環次數我看了很久才明白。第1行的8是這麼出來的,在NES中有2個圖案表,第1個是背景圖案表,第2個是精靈圖案表。代碼的作者還進一步把一個圖案表分成了4份,為什麼這樣,我也不清楚,至少在我看的資料裡還沒有說把1個圖案表分成4份的,總之4*2,8就出來了。
第2,3行的判斷是什麼意思,我還不明白。在我調試程序的時候,這個判斷基本都是為true。
第5行在讀取圖案表的1/4部分
第6行的循環此處64比較明確,1個圖案表有256個tile,四分之一的圖案表就是64個tile。可以知道這個循環是在畫tile。可以試著把64改成1,重新編譯程序,運行,就可以看到1個tile是什麼樣的了。
第9行的循環次數是8,1次循環是在畫1行像素。有了上文對圖案表的講解,具體1個像素是怎麼求出來的應該已經可以理解了。
有一點我還想說一下,代碼的原作者對12-19行的代碼語句的排序實在是干擾了我很久,我一直以為這樣的排序是有什麼具體含義的,要是寫成下面這樣的話,規律就很明顯了。
lpScn[0] = ((chr_h>>6)&2)|((chr_l>>7)&1); lpScn[1] = ((chr_h>>5)&2)|((chr_l>>6)&1); lpScn[2] = ((chr_h>>4)&2)|((chr_l>>5)&1); lpScn[3] = ((chr_h>>3)&2)|((chr_l>>4)&1); lpScn[4] = ((chr_h>>2)&2)|((chr_l>>3)&1); lpScn[5] = ((chr_h>>1)&2)|((chr_l>>2)&1); lpScn[6] = ((chr_h>>0)&2)|((chr_l>>1)&1); lpScn[7] = ((chr_h<<1)&2)|((chr_l>>0)&1);
第21行的意思再解釋一下。tile是正方形的,畫完一行8個像素之後,畫下一行前要先給lpScn加上圖像的寬度128。
第24行比較簡單,圖案表的1個tile已經讀取完了,tile的大小是16字節,為了讀取下一個tile,就得給圖案表指針lpPtn加上16。
好了圖像查看器模塊的代碼差不多就說到這了,下一節講解卷軸查看器的代碼。卷軸就是背景,算是趁熱打鐵吧,學習了圖案表、命名表、屬性表,看看背景是如何根據這三張表得到的。
本文出自 “三人乘虎” 博客,請務必保留此出處http://darhx.blog.51cto.com/7920146/1300053