VirtuaNES 是經典的 NES (FC) 模擬器,其源碼展示了 PPU (Picture Processing Unit) 圖形處理的核心邏輯。本文深度解析「圖形查看器」功能,揭示 NES 圖案表、調色板、命名表如何組合生成遊戲畫面。
// MainFrame.cpp
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;
// ... 其他查看器
}
}
SetTimer(1, 50, NULL) 每 50ms 刷新
// Create() 中
m_BitmapHdr.bih.biSize = sizeof(BITMAPINFOHEADER);
m_BitmapHdr.bih.biWidth = 128; // 圖像寬度
m_BitmapHdr.bih.biHeight = -256; // 負值表示自上而下 DIB
m_BitmapHdr.bih.biPlanes = 1;
m_BitmapHdr.bih.biBitCount = 8; // 8位色,256色模式
m_BitmapHdr.bih.biCompression = BI_RGB;
m_BitmapHdr.bih.biClrUsed = 16; // 實際使用 16 色調色板
| 表類型 | 大小 | 內容 | 作用 |
|---|---|---|---|
| 圖案表 | 4KB (兩個各 4KB) | 256 個 8×8 圖塊 | 角色/背景圖形像素數據(每像素 2 位) |
| 命名表 | 960 字節 | 32×30 圖塊索引 | 背景佈局,每字節指向一個圖塊 |
| 屬性表 | 64 字節 | 每 4×4 圖塊共享調色板 | 為圖塊提供高 2 位色彩數據 |
// m_SelectPal: 0-7,點擊窗口切換
LPBYTE pPAL = (m_SelectPal < 4) ?
&BGPAL[m_SelectPal * 4] : &SPPAL[(m_SelectPal & 3) * 4];
// 將 4 個調色板索引映射到系統 256 色
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]];
每幅圖案表圖像僅使用 4 種顏色(2 位像素 + 2 位屬性)。
// m_lpPattern: 目標像素緩衝區 (128×256)
// PPU_MEM_BANK[0..7]: 8 個 1KB 圖案表銀行指標
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];
// 每銀行 64 個圖塊 (256/4)
for (INT p = 0; p < 64; p++) {
// 計算目標圖塊在 128×256 緩衝區中的位置
LPBYTE lpScn = &m_lpPattern[i*32*128 + (p&15)*8 + (p/16)*8*128];
// 每圖塊 8 行
for (INT y = 0; y < 8; y++) {
BYTE chr_l = lpPtn[y]; // 低位平面
BYTE chr_h = lpPtn[y + 8]; // 高位平面
// 位運算解碼:每字節 8 像素,高低位平面組合成 2 位色值
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);
lpScn += 128; // 下一行 (圖像寬度 128)
}
lpPtn += 16; // 下一圖塊 (16 字節 = 8低位 + 8高位)
}
}
}
圖案表存儲格式 (16 字節/圖塊): 字節 0-7 : 低位平面 (bit 7-0 → 像素 0-7 低位) 字節 8-15 : 高位平面 (bit 7-0 → 像素 0-7 高位) 像素色值 = (高位 << 1) | 低位 → 0,1,2,3 對應調色板 4 色 解碼優化:原代碼打亂 0-7 順序,實際邏輯為按位順序提取 正確順序應為: lpScn[0] = 高bit7<<1 | 低bit7 lpScn[1] = 高bit6<<1 | 低bit6 ... lpScn[7] = 高bit0<<1 | 低bit0
| 變量 | 類型 | 含義 |
|---|---|---|
| PPU_MEM_BANK[12] | LPBYTE* | 前 8 個指向圖案表,後 4 個指向命名/屬性表 |
| PPU_MEM_TYPE[12] | int | 銀行類型標識 (BANKTYPE_CRAM 等) |
| m_lpBank[8] | LPBYTE | 快取上次圖案表銀行指標,檢測變化 |
| m_lpPattern | LPBYTE | 128×256 像素緩衝區,每字節一像素 (0-3) |
| m_Palette[1024] | BYTE | 256 色 × 4 字節 (RGBA) 系統調色板 |
通過分析 VirtuaNES 圖形查看器,可見 NES PPU 圖形系統的精妙設計:在極其有限的硬體資源下(2KB VRAM + 256 色調色板),通過圖案表、命名表、屬性表、調色板四級間接寻址,實現了豐富的圖形顯示能力。這種「間接寻址 + 位平面分離」的思想,對理解現代 GPU 纹理採樣、瓦片渲染同樣具有啟發意義。