2 个版本

0.1.1 2022 年 9 月 28 日
0.1.0 2022 年 8 月 14 日

#126仿真器

MPL-2.0 许可证

3MB
5K SLoC

Rust 编写并转换为 WebAssembly 的 Gameboy 仿真器

免责声明 1.0:本项目不支持或推广任何形式的盗版活动。构建或安装仿真器的行为并不违法。像过去几十年中创建的许多其他仿真器一样,此 项目仅作为一个案例研究

WebAssembly 快速 5 秒演示

Quick demo

免责声明 1.2:需要在 ubuntu-20.04macOS-最新版windows-latest 上通过测试。本项目使用 rustc 1.62.1 (e092d0b6b 2022-07-16) 创建。

免责声明 1.3:仿真器仍在开发中,您可以通过 测试待办事项 跟踪其开发。

tl;dr 1.0:跳转到 使用示例

●  报告

WebGL 与 WebAssembly 使用 Rust 的桌面 GUI
Demo Web with Assembly Demo Desktop GUI
Firefox 102.0.1 (64 位) macOS 11.6.2,16GB,四核 Intel Core i7,1.2 GHz
makeweb makedesktop

●  内部结构

GameBoy Internals GameBoy Internals

●  仿真器和一些 Game Boy 历史...

仿真器通常允许宿主系统运行为访客系统设计的软件或使用外围设备。仿真指的是电子设备中计算机程序模仿(或模仿)另一程序或设备的能力。在 Game Boy 的情况下,大部分工作涉及处理围绕(Z80 CPU 的)8 位总线。Game Boy CPU 是 Intel 8080 和 Zilog Z80 的混合体。在计算机中,仿真器是使一个计算机系统(称为宿主)表现得像另一个计算机系统(称为访客)的硬件或软件。

关于 Z80...

Zilog Z80

Z80是Zilog公司推出的第一款产品,一款8位微处理器。Z80由Federico Faggin于1974年底构思,并于1975年初由他和他的11名员工开始开发。第一台工作样机于1976年3月交付,并于1976年7月正式上市。凭借Z80的收益,公司在接下来的两年内建立了自己的芯片工厂,员工人数增长超过一千人。

这意味着需要操作大量的单个字节,尤其是在导航通过巨大的ROM和RAM存储库时。Game Boy具有相当简单的架构——获取按钮输入需要读取特定的内存地址,将像素写入屏幕涉及到将字节推送到VRAM的特定位置。

CPU          - 8-bit (Similar to the Z80 processor)
Clock Speed  - 4.194304MHz (4.295454MHz for SGB, max. 8.4MHz for CGB)
Work RAM     - 8K Byte (32K Byte for CGB)
Video RAM    - 8K Byte (16K Byte for CGB)
Screen Size  - 2.6"
Resolution   - 160x144 (20x18 tiles)
Max sprites  - Max 40 per screen, 10 per line
Sprite sizes - 8x8 or 8x16
Palettes     - 1x4 BG, 2x3 OBJ (for CGB: 8x4 BG, 8x3 OBJ)
Colors       - 4 grayshades (32768 colors for CGB)
Horiz Sync   - 9198 KHz (9420 KHz for SGB)
Vert Sync    - 59.73 Hz (61.17 Hz for SGB)
Sound        - 4 channels with stereo sound
Power        - DC6V 0.7W (DC3V 0.7W for GB Pocket, DC3V 0.6W for CGB)

Game Boy有四个操作按钮,分别标记为“A”、“B”、“SELECT”和“START”,以及一个方向垫(d-pad)。设备右侧有一个音量控制旋钮,左侧有一个类似的旋钮来调整对比度。在Game Boy的顶部,有一个滑动开关和Game Boy卡带插槽。开关包括一个物理锁定功能,防止用户在设备开启时插入或拔出卡带。任天堂建议用户将卡带留在插槽中,以防止灰尘和污垢进入系统。

pub enum Button {
    A,
    B,
    Left,
    Right,
    Up,
    Down,
    Start,
    Select,
}

Game Boy包含可选的输入或输出连接器。在系统左侧有一个外部3.5 mm × 1.35 mm直流电源插座,允许用户使用外部充电电池组或AC适配器(需单独购买)代替四节AA电池。Game Boy需要至少150 mA的6 V直流电。设备底部有一个3.5 mm立体声耳机插孔,允许用户使用附带耳机或外部扬声器收听音频。

设备右侧提供一个端口,允许用户通过连接线连接到另一个Game Boy系统,前提是两个用户都在玩支持相互连接的游戏(通常是同一款游戏的副本,尽管例如宝可梦游戏可以在不同世代之间连接)。该端口还可以用于连接Game Boy打印机。连接线最初是为玩家玩两人对战游戏(如俄罗斯方块)而设计的。然而,游戏开发者Satoshi Tajiri后来将连接线技术用作流行宝可梦视频游戏系列的通信和网络方式。

总之,关于Game Boy的历史就聊到这里。

Z80被设计成与已经存在的Intel 8080二进制兼容。这意味着8080中发现的指令集也被Z80实现(本质上,8080可以看作是Z80的一个子集)。Game Boy的定制混合芯片官方名称为Sharp LR35902

●  LR35902 ~ 高级架构

CPU (src/cpu)

中央处理单元(CPU),也称为中央处理器、主处理器或简称为处理器,是执行计算机程序中指令的电子电路。CPU执行由程序中的指令指定的基本算术、逻辑、控制和输入/输出(I/O)操作。这与外部组件(如主存储器和I/O电路)以及图形处理单元(GPU)等专用处理器形成对比。

Game Boy CPU由8个不同的“寄存器”组成。寄存器负责在CPU执行各种指令时保存可以操作的小数据片段。Game Boy的CPU是一款8位CPU,这意味着它的每个寄存器可以存储8位(1字节)的数据。CPU有8个不同的寄存器,分别标记为abcdefhl

每个寄存器都是1字节,因此每个寄存器可以存储从0到255的值。一个u8可以完成这个任务,因为它可以存储介于0(2^8 - 1)之间的值。

$$ u8PossibleValues=(2^8) $$

assert_eq!(u8::MIN, 0);
assert_eq!(u8::MAX, 255);

以下是一个示例

pub struct Registers {
    pub a: u8,
    pub b: u8,
    pub c: u8,
    pub d: u8,
    pub e: u8,
    pub h: u8,
    pub l: u8
}

如果指令的第一个字节有256个可能的值,那么基本表中有256个可能的指令。这个表在Gameboy Z80操作码映射中详细说明:[链接](http://imrannazar.com/Gameboy-Z80-Opcode-Map)。

内存(src/mmu

MMU map

  • 来自[链接](http://imrannazar.com/GameBoy-Emulation-in-JavaScript:-Memory)的图像

Game Boy有一个16位地址总线,提供可寻址范围65535字节(或64KB)。Game Boy的所有硬件组件,包括RAM、ROM、视频RAM和I/O端口,都如图所示进行内存映射。

Memory map
  • 来自[链接](http://marc.rawer.de/Gameboy/Docs/GBProject.pdf)的图像
64KB
内存映射
  • 0x0000 - 0x00FF:引导ROM
  • 0x0000 - 0x3FFF:游戏ROM Bank 0
  • 0x4000 - 0x7FFF:游戏ROM Bank N
  • 0x8000 - 0x97FF:图块RAM
  • 0x9800 - 0x9FFF:背景地图
  • 0xA000 - 0xBFFF:卡带RAM
  • 0xC000 - 0xDFFF:工作RAM
  • 0xE000 - 0xFDFF:回声RAM
  • 0xFE00 - 0xFE9F:OAM(对象属性内存)
  • 0xFEA0 - 0xFEFF:未使用
  • 0xFF00 - 0xFF7F:I/O寄存器
  • 0xFF80 - 0xFFFE:高RAM区域
  • 0xFFFF:中断启用寄存器

更多信息:[链接](https://rylev.github.io/DMG-01/public/book/memory_map.html)

您可以使用类似以下内容设置初始内存结构

fn power_on(&mut self) {
    self.write_byte(0xFF05, 0);
    self.write_byte(0xFF06, 0);
    self.write_byte(0xFF07, 0);
    self.write_byte(0xFF10, 0x80);
    self.write_byte(0xFF11, 0xBF);
    self.write_byte(0xFF12, 0xF3);
    self.write_byte(0xFF14, 0xBF);
    self.write_byte(0xFF16, 0x3F);
    self.write_byte(0xFF16, 0x3F);
    self.write_byte(0xFF17, 0);
    self.write_byte(0xFF19, 0xBF);
    self.write_byte(0xFF1A, 0x7F);
    self.write_byte(0xFF1B, 0xFF);
    self.write_byte(0xFF1C, 0x9F);
    self.write_byte(0xFF1E, 0xFF);
    self.write_byte(0xFF20, 0xFF);
    self.write_byte(0xFF21, 0);
    self.write_byte(0xFF22, 0);
    self.write_byte(0xFF23, 0xBF);
    self.write_byte(0xFF24, 0x77);
    self.write_byte(0xFF25, 0xF3);
    self.write_byte(0xFF26, 0xF1);
    self.write_byte(0xFF40, 0x91);
    self.write_byte(0xFF42, 0);
    self.write_byte(0xFF43, 0);
    self.write_byte(0xFF45, 0);
    self.write_byte(0xFF47, 0xFC);
    self.write_byte(0xFF48, 0xFF);
    self.write_byte(0xFF49, 0xFF);
    self.write_byte(0xFF4A, 0);
    self.write_byte(0xFF4B, 0);
}

GPU(src/gpu

原版Game Boy的屏幕分辨率为160×144像素

pub const WIDTH: usize = 160;
pub const HEIGHT: usize = 144;

链接:[GitHub](https://github.com/raphamorim/LR35902/blob/main/src/gb.rs#L3-L4)

[...] TODO: 在此处添加更多关于GPU的信息。

●  使用示例

Web ~ JavaScript/WASM使用

  1. 使用Yarn或NPM安装lr35902
npm install lr35902
# yarn install lr35902
  1. 导入渲染函数
import { render } from 'lr35902';
  1. 配置它
const romFile = document.querySelector('#rom-file');
romFile.addEventListener('change', function readFile() {
  loadRom(this.files[0]);
});

async function loadRom(file) {
  const { size, name } = file;
  const arrayBuffer = await file.arrayBuffer();
  const u8View = new Uint8Array(arrayBuffer);
  render(rom);
}

桌面 ~ Rust使用

tl;dr: 你可以在示例文件夹中查看桌面示例(/examples/desktop

  1. lr35902作为依赖项添加,并确保在desktop上启用功能
[dependencies]
lr35902 = { version = "0.1.0", features = ["desktop"] }
  1. 导入gameboyrenderer::render
use lr35902::gameboy::{Gameboy, RenderMode::Desktop};
  1. 配置它

默认缩放是1(160x144)。

fn main() {
    let mut gb = Gameboy::new();
    match gb.load_rom("./sample-rom.gb") {
        Ok(..) => {
            gb.render(Desktop);
        }
        Err(err) => {
            println!("{:?}", err);
        }
    }
}

●  TODO

  • 桌面
    • 创建桌面窗口
  • 浏览器
    • 使用端口3000创建服务器
  • 键盘处理程序
    • Web
    • 桌面
  • 经典Gameboy指令(.gb
  • 在浏览器中读取ROM并发送到WASM(Gameboy::load_with_u8_vec
  • 扩展屏幕模块以使用WASM在Web上渲染
    • 使用WebGl渲染
  • 支持.cgb ROM(Game Boy Color)
  • Gameboy::setScale(u8)用于桌面
  • 音频
    • 桌面
    • Web
  • 经典Gameboy测试(/tests
  • 支持.sgb ROM(Super Game Boy)

●  测试

这些测试基于Blargg的Gameboy硬件测试ROM。

仓库链接:https://github.com/retrio/gb-test-roms.git

测试(最初在Openemu中运行) LR35902状态
CPU instructions test 失败
Bits Unused test 失败
Halt Bug test 失败
Bits bank1 test 失败
GGB Sound test 失败

●  开发日志

本节仅供个人使用。

日记条目 屏幕截图 备注
周六,2022年8月6日 Diary Aug 6 CPU正在运行,但尚未通过GPU或使用内存(MMU)功能连接。
周六,2022年8月10日 Diary Aug 10 CPU已连接到GPU和MMU,但使用错误的数据在屏幕上绘图。有关寄存器执行顺序的一些问题
周日,2022年8月14日 Diary Aug 14 覆盖的操作:0x00, 0xC3, 0x31, 0x3E, 0xE0, 0x97, 0xEA, 0x01, 0xCD, 0x21, 0x16, 0x1E, 0xF0, 0xE6, 0x20, 0xA, 0x22, 0x03, 21, 194, 29, 201, 197, 0x47, 14, 0x09, 193, 68, 77, 213, 25, 209, 0x06, 0x05, 35, 251, 243, 245, 61, 229, 42, 254, 40, 225, 250, 60, 241, 217, 47, 203, 176, 160, 120, 126, 202.

0xCD 操作:0x37, 0x77, 0x7F, 0x6F, 0x67, 0x47, 0x4F

●  资源 & 参考

依赖项

约7-11MB
约208K SLoC