NES Emulator C++ 代码架构分析文档
1. 项目概述
本项目是一个基于 C++ 实现的 NES(Nintendo Entertainment System)模拟器,采用 SDL2 进行图形渲染和输入处理。项目采用模块化设计,模拟了 NES 主机的核心组件:CPU、PPU、卡带、手柄和总线系统。
技术栈:
- C++17
- SDL2(图形渲染和输入)
- CMake 构建系统
2. 目录结构
nes_emulator/
├── CMakeLists.txt # 构建配置
├── src/
│ ├── main.cpp # 主程序入口
│ ├── bus.cpp / bus.h # 地址总线
│ ├── cpu.cpp / cpu.h # CPU 模拟 (MOS 6502)
│ ├── cartridge.cpp / cartridge.h # 卡带/ROM 加载
│ ├── joypad.cpp / joypad.h # 手柄输入
│ ├── mem.h # 内存访问接口
│ ├── opcodes.cpp / opcodes.h # 操作码定义
│ ├── trace.cpp / trace.h # 调试跟踪
│ ├── ppu/
│ │ ├── ppu.cpp / ppu.h # 图像处理单元
│ │ └── registers/
│ │ ├── control.h / control.cpp # PPU 控制寄存器
│ │ ├── mask.h / mask.cpp # PPU 遮罩寄存器
│ │ ├── status.h / status.cpp # PPU 状态寄存器
│ │ ├── scroll.h / scroll.cpp # 滚动寄存器
│ │ └── addr.h / addr.cpp # 地址寄存器
│ └── render/
│ ├── render.cpp / render.h # 渲染模块
│ ├── frame.cpp / frame.h # 帧缓冲
│ └── palette.cpp / palette.h # 调色板
3. 模块详细分析
3.1 主程序入口 (main.cpp)
功能: 初始化 SDL2 窗口、加载 ROM、创建游戏循环、处理输入事件。
关键数据结构:
texture_data: 存储渲染纹理数据(512×480 像素,RGB 格式)key_map: SDL 键盘码到手柄按钮的映射表
关键函数:
render_tile_viewer(): 渲染 CHR ROM 瓦片查看器,显示 2 个 256 瓦片的存储体
游戏循环回调:
[&](NesPPU& ppu, Joypad& joypad) {
render::render(ppu, frame); // 渲染背景和精灵
render_tile_viewer(ppu, texture_data); // 渲染瓦片查看器
SDL_UpdateTexture(...); // 更新纹理
SDL_RenderPresent(renderer); // 显示帧
// 处理 SDL 事件(退出、按键)
}
3.2 总线系统 (bus.cpp / bus.h)
功能: 模拟 NES 地址总线,负责 CPU、RAM、PPU、卡带和手柄之间的数据路由。
关键成员:
| 成员 | 类型 | 说明 |
|——|——|——|
| cpu_vram_ | array<uint8_t, 2048> | 2KB CPU 内部 RAM |
| prg_rom_ | vector<uint8_t> | 程序 ROM |
| ppu_ | NesPPU | 图像处理单元实例 |
| joypad1_ | Joypad | 1 号手柄 |
| cycles_ | size_t | 累计周期数 |
地址映射:
| 地址范围 | 访问组件 |
|---|---|
| 0x0000-0x1FFF | CPU RAM(镜像重复) |
| 0x2000-0x3FFF | PPU 寄存器(镜像重复) |
| 0x4000-0x4015 | APU/音频寄存器 |
| 0x4016-0x4017 | 手柄寄存器 |
| 0x4014 | OAM DMA 传输 |
| 0x8000-0xFFFF | PRG ROM |
关键方法:
tick(cycles): 每 CPU 周期调用,同时推进 PPU(PPU 1 周期 = CPU 3 周期)poll_nmi_status(): 检测并清除 NMI 中断标志
3.3 CPU 模拟 (cpu.cpp / cpu.h)
功能: 模拟 MOS 6502 微处理器,执行 NES 游戏代码。
寄存器:
| 寄存器 | 位宽 | 说明 |
|——–|——|——|
| register_a | 8-bit | 累加器 |
| register_x | 8-bit | 索引寄存器 X |
| register_y | 8-bit | 索引寄存器 Y |
| status | 8-bit | 处理器状态标志 |
| program_counter | 16-bit | 程序计数器 |
| stack_pointer | 8-bit | 栈指针(初始 0xFD) |
状态标志位:
CARRY_FLAG_MASK = 0x01 // 进位标志
ZERO_FLAG_MASK = 0x02 // 零标志
INTERRUPT_DISABLE_MASK = 0x04 // 中断禁用
DECIMAL_MODE_MASK = 0x08 // BCD 模式
BREAK_COMMAND_MASK = 0x10 // 软件中断标志
OVERFLOW_FLAG_MASK = 0x40 // 溢出标志
NEGATIVE_FLAG_MASK = 0x80 // 负数标志
寻址模式(10 种):
Immediate: 立即寻址ZeroPage: 零页寻址ZeroPage_X/ZeroPage_Y: 零页索引寻址Absolute: 绝对寻址Absolute_X/Absolute_Y: 绝对索引寻址Indirect_X/Indirect_Y: 间接索引寻址NoneAddressing: 隐含寻址
指令执行流程:
1. 获取操作码 (opcode)
2. 解析操作数地址
3. 执行指令
4. 总线 tick(推进 PPU 周期)
5. 检查 NMI 中断
6. 调用回调函数
关键指令实现:
- 算术:
ADC(加法)、SBC(减法) - 逻辑:
AND、EOR、ORA - 移位:
ASL、LSR、ROL、ROR - 分支:
BCC、BCS、BEQ、BNE、BMI、BPL - 堆栈:
PHA、PLA、PHP、PLP
3.4 卡带模块 (cartridge.cpp / cartridge.h)
功能: 解析和加载 iNES 格式的 NES ROM 文件。
Rom 数据结构:
struct Rom {
vector<uint8_t> prg_rom; // 程序 ROM (16KB 页)
vector<uint8_t> chr_rom; // 字符 ROM (8KB 页,图形数据)
uint8_t mapper; // mapper 编号
Mirroring screen_mirroring; // 屏幕镜像模式
};
iNES 格式头部(16 字节): | 偏移 | 内容 | |——|——| | 0-3 | “NES” + 0x1A 标识 | | 4 | PRG ROM 大小(16KB 页数) | | 5 | CHR ROM 大小(8KB 页数) | | 6 | 标志位 0(mapper low、mirror、trainer) | | 7 | 标志位 1(mapper high、NES2.0) | | 8-15 | 保留 |
镜像模式:
Vertical: 垂直镜像(Nametable 0↔2, 1↔3)Horizontal: 水平镜像(Nametable 0↔1, 2↔3)FourScreen: 四屏模式
3.5 手柄输入 (joypad.cpp / joypad.h)
功能: 模拟标准 NES 手柄,支持 8 个按钮。
按钮定义:
enum class JoypadButton : uint8_t {
RIGHT = 0b10000000,
LEFT = 0b01000000,
DOWN = 0b00100000,
UP = 0b00010000,
START = 0b00001000,
SELECT = 0b00000100,
BUTTON_B = 0b00000010,
BUTTON_A = 0b00000001,
};
手柄状态机:
strobe_: 0 = 正常读取模式,1 = 固定读取 A 按钮button_index_: 当前读取的按钮索引(0-7)button_status_: 8 个按钮的按下状态位掩码
读操作行为:
- 当
strobe_ = 1时,始终返回 A 按钮状态 - 当
strobe_ = 0时,返回当前按钮状态,然后自动递增索引
3.6 PPU 模拟 (ppu/ppu.cpp / ppu.h)
功能: 模拟 NES 图像处理单元,负责渲染 256×240 像素画面。
核心数据结构:
class NesPPU {
vector<uint8_t> chr_rom; // 字符图形数据
Mirroring mirroring; // 镜像模式
ControlRegister ctrl; // PPUCTRL ($2000)
MaskRegister mask; // PPUMASK ($2001)
StatusRegister status; // PPUSTATUS ($2002)
ScrollRegister scroll; // PPUSCROLL ($2005)
AddrRegister addr; // PPUADDR ($2006)
array<uint8_t, 2048> vram; // 2KB 视频 RAM
array<uint8_t, 256> oam_data; // 精灵属性表
array<uint8_t, 32> palette_table; // 调色板数据
uint16_t scanline; // 当前扫描线 (0-261)
bool nmi_interrupt; // NMI 中断标志
};
扫描线时序:
- 每条扫描线 341 个 PPU 周期
- 扫描线 0-239: 可见画面
- 扫描线 241: 设置 VBLANK 标志,触发 NMI(如果启用)
- 扫描线 261: VBLANK 结束
OAM(精灵属性表)结构: 每个精灵 4 字节:
- Y 坐标
- 瓦片索引
- 属性(翻转、优先级、调色板)
- X 坐标
3.7 PPU 寄存器模块
3.7.1 控制寄存器 (control.h/cpp)
地址: $2000
位定义: | 位 | 名称 | 功能 | |—-|——|——| | 0-1 | NAMETABLE1/2 | 名称表地址选择 | | 2 | VRAM_ADD_INCREMENT | VRAM 地址增量(1 或 32) | | 3 | SPRITE_PATTERN_ADDR | 精灵图形地址(0x1000 或 0x0000) | | 4 | BACKROUND_PATTERN_ADDR | 背景图形地址 | | 5 | SPRITE_SIZE | 精灵大小(8×8 或 8×16) | | 6 | MASTER_SLAVE_SELECT | 主/从模式 | | 7 | GENERATE_NMI | VBLANK 时生成 NMI |
3.7.2 遮罩寄存器 (mask.h/cpp)
地址: $2001
位定义: | 位 | 名称 | 功能 | |—-|——|——| | 0 | GREYSCALE | 灰度模式 | | 1 | LEFTMOST_8PXL_BACKGROUND | 显示左侧 8 像素背景 | | 2 | LEFTMOST_8PXL_SPRITE | 显示左侧 8 像素精灵 | | 3 | SHOW_BACKGROUND | 显示背景 | | 4 | SHOW_SPRITES | 显示精灵 | | 5-7 | EMPHASISE_R/G/B | 颜色增强 |
3.7.3 状态寄存器 (status.h/cpp)
地址: $2002
位定义: | 位 | 名称 | 功能 | |—-|——|——| | 5 | SPRITE_OVERFLOW | 精灵溢出标志 | | 6 | SPRITE_ZERO_HIT | 精灵 0 碰撞标志 | | 7 | VBLANK_STARTED | VBLANK 开始标志 |
读取行为: 读取后自动清除 VBLANK 标志和地址锁存器。
3.7.4 滚动寄存器 (scroll.h/cpp)
功能: 双重写入寄存器,用于设置背景滚动偏移。
- 第一次写入 → scroll_x
- 第二次写入 → scroll_y
- latch 标志交替切换
3.7.5 地址寄存器 (addr.h/cpp)
功能: 双重写入 PPU 地址寄存器。
- 第一次写入 → 高字节
- 第二次写入 → 低字节
- 支持自动增量(1 或 32)
3.8 渲染模块 (render/)
3.8.1 帧缓冲 (frame.h/cpp)
功能: 管理 256×240 分辨率的帧缓冲。
数据结构:
class Frame {
vector<uint8_t> data_; // RGB 数据,256×240×3 字节
};
关键方法:
set_pixel(x, y, r, g, b): 设置像素(带边界检查)get_data(): 获取原始 RGB 数据
3.8.2 调色板 (palette.h/cpp)
功能: 定义 NES 的 64 色系统调色板。
static const array<array<uint8_t, 3>, 64> SYSTEM_PALETTE;
3.8.3 渲染器 (render.h/cpp)
命名空间: render
主要函数:
void render(const NesPPU& ppu, Frame& frame);
渲染流程:
- 获取当前名称表地址
- 根据镜像模式确定主/次名称表
- 渲染主名称表(应用滚动偏移)
- 如果有滚动溢出,渲染次名称表
- 渲染精灵(从 OAM 倒序读取,优先级低的先渲染)
瓦片渲染(8×8 像素):
每个瓦片使用 CHR ROM 中 16 字节:
- 低位平面 (bit 0): tile[y] (y=0..7)
- 高位平面 (bit 1): tile[y+8] (y=0..7)
像素值 = ((高位 & 1) << 1) | (低位 & 1)
精灵渲染特性:
- 支持水平/垂直翻转
- 透明像素(值=0)跳过
- 优先级处理(后渲染覆盖先渲染)
3.9 操作码定义 (opcodes.cpp / opcodes.h)
功能: 定义 6502 指令集的操作码、元数据(长度、周期数)和寻址模式。
OpCode 结构:
struct OpCode {
uint8_t code; // 操作码
const char* mnemonic; // 助记符
uint8_t len; // 指令长度(字节)
uint8_t cycles; // 执行周期数
AddressingMode mode; // 寻址模式
};
支持的操作码示例: | 操作码 | 指令 | 长度 | 周期 | |——–|——|——|——| | 0xA9 | LDA Immediate | 2 | 2 | | 0x6D | ADC Absolute | 3 | 4 | | 0x4C | JMP Absolute | 3 | 3 |
3.10 调试跟踪 (trace.cpp / trace.h)
功能: 生成 CPU 指令执行跟踪信息,用于调试。
输出格式:
PC OPCODE MNEMONIC OPERAND A:XX X:XX Y:XX P:XX SP:XX
4. 数据流与架构图
4.1 系统架构
┌─────────────────────────────────────────────────────────────────┐
│ main.cpp │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ SDL2 │ │ 游戏循环 │ │ 输入处理 │ │
│ │ 窗口/渲染 │ │ 回调 │ │ 事件 │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└────────────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Bus │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
│ │ CPU │ │ PPU │ │ 卡带 │ │ 手柄 │ │
│ │ (6502) │ │ 渲染 │ │ PRG ROM │ │ Joypad1 │ │
│ └────────────┘ └────────────┘ └────────────┘ └────────────┘ │
│ │ │ │ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
│ │ CPU RAM │ │ VRAM │ │ CHR ROM │ │
│ │ 2KB │ │ 2KB │ │ 图形 │ │
│ └────────────┘ └────────────┘ └────────────┘ │
└─────────────────────────────────────────────────────────────────┘
4.2 游戏循环数据流
┌─────────────────────────────────────────────────────────────────┐
│ 游戏循环 (每帧 ~60fps) │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ CPU::run_with_callback() │
│ ┌─────────────────────────────────────────────────────────────┐│
│ │ 1. 取指 (fetch opcode) ││
│ │ 2. 译码 (lookup opcode info) ││
│ │ 3. 执行 (execute instruction) ││
│ │ 4. 总线 tick ────────────────────────────────────────────► ││
│ │ │ ││
│ │ ▼ ││
│ │ Bus::tick(cycles) ││
│ │ │ ││
│ │ ▼ ││
│ │ PPU::tick(cpu_cycles * 3) ││
│ │ │ ││
│ │ 检测 VBLANK/NMI ─────────────────────────────► ││
│ │ │ ││
│ │ ▼ ││
│ │ 回调 (gameloop_callback) ││
│ │ │ ││
│ │ ▼ ││
│ │ render::render(ppu, frame) ──────► 渲染画面 ││
│ └─────────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────────┘
4.3 PPU 渲染流程
┌─────────────────────────────────────────────────────────────────┐
│ PPU 渲染周期 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 扫描线 0-239 (可见画面): │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ for each cycle (0-340): │ │
│ │ - 读取名称表/属性表 │ │
│ │ - 读取背景瓦片数据 │ │
│ │ - 读取精灵数据 │ │
│ │ - 混合输出像素 │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ 扫描线 241 (VBLANK): │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ - 设置 VBLANK 标志 │ │
│ │ - 如果 ctrl.generate_vblank_nmi() → 触发 NMI │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ 扫描线 261 (VBLANK 结束): │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ - 清除 VBLANK 标志 │ │
│ │ - 清除精灵碰撞标志 │ │
│ │ - 重置扫描线计数器 │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
4.4 内存地址映射
┌────────────────┬────────────────────────────────┬────────────────┐
│ 地址范围 │ 组件 │ 说明 │
├────────────────┼────────────────────────────────┼────────────────┤
│ 0x0000-0x1FFF │ CPU RAM (2KB, 镜像×4) │ 零页/栈/通用 │
├────────────────┼────────────────────────────────┼────────────────┤
│ 0x2000-0x3FFF │ PPU 寄存器 (8个, 镜像×4) │ 控制/状态/数据 │
│ ├────────────────────────────────┤ │
│ │ $2000 │ PPUCTRL │ 控制寄存器 │ │
│ │ $2001 │ PPUMASK │ 遮罩寄存器 │ │
│ │ $2002 │ PPUSTATUS │ 状态寄存器 │ │
│ │ $2003 │ OAMADDR │ OAM 地址 │ │
│ │ $2004 │ OAMDATA │ OAM 数据 │ │
│ │ $2005 │ PPUSCROLL │ 滚动寄存器 │ │
│ │ $2006 │ PPUADDR │ VRAM 地址 │ │
│ │ $2007 │ PPUDATA │ VRAM 数据 │ │
├────────────────┼────────────────────────────────┼────────────────┤
│ 0x4000-0x4013 │ APU 寄存器 │ 音频处理 │
│ 0x4014 │ OAM DMA │ 精灵数据传输 │
│ 0x4015 │ APU 状态 │ 音频状态 │
│ 0x4016-0x4017 │ 手柄 1/2 │ 输入读取 │
├────────────────┼────────────────────────────────┼────────────────┤
│ 0x8000-0xFFFF │ PRG ROM (最多 32KB) │ 程序代码 │
│ ├────────────────────────────────┤ │
│ │ $FFFA-$FFFB │ NMI 向量 │ │
│ │ $FFFC-$FFFD │ RESET 向量 │ │
│ │ $FFFE-$FFFF │ IRQ/BRK 向量 │ │
└────────────────┴────────────────────────────────┴────────────────┘
5. 关键算法说明
5.1 6502 ADC(加法)算法
void CPU::add_to_register_a(uint8_t data) {
uint16_t sum = register_a + data + ((status & CARRY_FLAG_MASK) ? 1 : 0);
// 进位检测
if (sum > 0xFF) {
set_carry_flag();
} else {
clear_carry_flag();
}
// 溢出检测 (补码溢出)
if ((data ^ result) & (result ^ register_a) & 0x80) {
status |= OVERFLOW_FLAG_MASK;
} else {
status &= ~OVERFLOW_FLAG_MASK;
}
set_register_a(result);
}
溢出判断逻辑: 当两个正数相加得到负数,或两个负数相加得到正数时发生溢出。
5.2 瓦片像素解码
// 从 CHR ROM 读取瓦片数据
const uint8_t* tile = &ppu.chr_rom[bank + tile_idx * 16];
for (int y = 0; y <= 7; y++) {
uint8_t upper = tile[y]; // 位平面 1
uint8_t lower = tile[y + 8]; // 位平面 0
for (int x = 7; x >= 0; x--) {
// 提取 2 位像素值 (0-3)
uint8_t value = ((lower & 1) << 1) | (upper & 1);
upper >>= 1;
lower >>= 1;
// value 决定调色板索引
// 0 = 透明,1/2/3 = 调色板颜色
}
}
5.3 名称表地址镜像
uint16_t NesPPU::mirror_vram_addr(uint16_t addr) const {
uint16_t mirrored = addr & 0b10111111111111; // 清除高位
uint16_t vram_index = mirrored - 0x2000; // 转 VRAM 索引
uint16_t name_table = vram_index / 0x400; // 名称表编号 (0-3)
switch (mirroring) {
case Mirroring::Vertical:
// 垂直镜像: 0↔2, 1↔3
if (name_table >= 2) vram_index -= 0x800;
break;
case Mirroring::Horizontal:
// 水平镜像: 0↔1, 2↔3
if (name_table == 2 || name_table == 1) vram_index -= 0x400;
if (name_table == 3) vram_index -= 0x800;
break;
// FourScreen 不做镜像
}
return vram_index;
}
5.4 分支指令偏移计算
void CPU::branch(bool condition) {
if (condition) {
int8_t offset = mem_read(program_counter); // 带符号偏移
uint16_t target = (program_counter + 1) + offset;
program_counter = target;
}
}
注意: 6502 分支指令的偏移是相对于下一条指令地址的有符号字节。
5.5 堆栈操作
void CPU::stack_push(uint8_t data) {
mem_write(STACK + stack_pointer, data);
stack_pointer = (stack_pointer - 1) & 0xFF; // 向下生长
}
uint8_t CPU::stack_pop() {
stack_pointer = (stack_pointer + 1) & 0xFF; // 向上收缩
return mem_read(STACK + stack_pointer);
}
void CPU::stack_push_u16(uint16_t data) {
stack_push((data >> 8) & 0xFF); // 高字节先入
stack_push(data & 0xFF); // 低字节后入
}
6502 堆栈特性: 堆栈固定在 0x0100-0x01FF,高地址向下生长。
6. 构建说明
依赖:
- CMake 3.10+
- SDL2 开发库
构建步骤:
cd nes_emulator
mkdir -p build && cd build
cmake ..
make
运行:
./nes_emulator <rom_file.nes>
键盘映射: | 键 | 手柄按钮 | |—-|———-| | 方向键 | 方向 | | Enter | Start | | Space | Select | | A | B 按钮 | | S | A 按钮 | | Esc | 退出 |
截图


参考
NES 模拟器
功能特性
- ✅ 完整的 NES 系统模拟
- MOS 6502 CPU (1.79 MHz NTSC)
- PPU (Picture Processing Unit) 图形处理器
- 2KB 内部 RAM
- Mapper 0 (NROM) 支持
- ✅ PPU 渲染
- 256×240 分辨率
- 背景层渲染(nametable + pattern table + attribute table)
- 精灵渲染(64 个精灵,8×8 像素)
- Sprite 0 碰撞检测
- VBLANK NMI 生成
- ✅ 输入控制
- NES 标准控制器支持
- 完整的键盘映射
- ✅ 显示
- SDL2 硬件加速渲染
- 60 FPS 同步
- 3× 缩放显示(768×720 窗口)
键盘控制
| 按键 | 功能 |
|---|---|
| 方向键 ↑↓←→ | 十字方向键 |
| Z 或 空格 | A 按钮(跳跃) |
| X 或 Ctrl | B 按钮(奔跑/射击) |
| Enter | Start(开始) |
| Tab | Select(选择) |
| ESC | 退出模拟器 |
快速开始
# 编译
mkdir build && cd build
cmake ..
make -j4
# 运行 Super Mario Bros.
./targets/nes/nes_emulator path/to/mario.nes
# 或使用快捷脚本
./run_mario.sh
支持的 ROM 格式
- iNES 1.0 格式 (.nes)
- Mapper 0 (NROM) - 16KB 或 32KB PRG-ROM
- 垂直/水平镜像模式
技术细节
PPU 实现
- 扫描线精确渲染(240 条可见扫描线)
- 正确的调色板格式(ABGR8888)
- Nametable 镜像支持
- 属性表颜色查找
控制器协议
- 地址 0x4016/0x4017
- Strobe 锁存机制
- 8 位移位寄存器读取
- 按键顺序:A, B, Select, Start, Up, Down, Left, Right
内存映射
0x0000-0x07FF: 内部 RAM (2KB,镜像到 0x1FFF)
0x2000-0x3FFF: PPU 寄存器 (8 字节,镜像)
0x4000-0x4017: APU 和 I/O 寄存器
0x4016-0x4017: 控制器端口
0x8000-0xFFFF: 卡带 ROM
已知限制
- 仅支持 Mapper 0 (NROM)
- 无音频 (APU) 支持
- 不支持保存状态
- 简化的滚动实现
测试游戏
经过测试的游戏:
- ✅ Super Mario Bros. - 完全可玩
- ✅ Donkey Kong - 显示正常
- ⚠️ 其他 Mapper 0 游戏 - 应该可以工作