Game boy模拟器:集成
Posted 妇男主人
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Game boy模拟器:集成相关的知识,希望对你有一定的参考价值。
在第 4 部分中,详细探讨了 GameBoy 的图形子系统,并将仿真放在一起。没有一套GPU的寄存器映射在软件中处理,图形子系统不能被仿真器使用;一旦这些寄存器可用,仿真器基本上就可以进行基本使用了。
GPU寄存器
GameBoy 的图形单元在内存映射的 I/O 空间中有一系列映射到内存中的寄存器。为了获得具有背景图像的工作仿真,GPU 将需要以下寄存器(其他寄存器也可用于 GPU,将在本系列的后面部分进行探讨)。
基本 GPU 寄存器
地址 | 寄存器 | 状态 |
---|---|---|
0xFF40 | LCD and GPU control | 读 / 写 |
0xFF42 | Scroll-Y | 读 / 写 |
0xFF43 | Scroll-X | 读 / 写 |
0xFF44 | Current scan line | 只读 |
0xFF47 | Background palette | 只写 |
背景调色板寄存器之前已经被探索过,它由四个 2 位调色板条目组成。滚动寄存器和扫描线计数器是全字节值;这留下了 LCD 控制寄存器,它由 8 个独立的标志组成,用于控制 GPU 的各个部分。
GPU控制寄存器
位 | 功能 | 0 | 1 |
---|---|---|---|
0 | Background: on/off | Off | On |
1 | Sprites: on/off | Off | On |
2 | Sprites: size (pixels) | 8x8 | 8x16 |
3 | Background: tile map | #0 | #1 |
4 | Background: tile set | #0 | #1 |
5 | Window: on/off | Off | On |
6 | Window: tile map | #0 | #1 |
7 | Display: on/off | Off | On |
在上表中,GPU 的附加功能出现了:可以出现在背景上方的“窗口”层,以及可以在背景和窗口上移动的角色对象。这些附加功能将在需要时涵盖;同时,背景标志对于基本渲染功能最重要。特别是,在这里可以看到如何更改背景瓦片地图和瓦片集,只需翻转寄存器 0xFF40
中的位即可。
实现:GPU 寄存器
借助概念性的 GPU 寄存器布局,只需将这些地址的处理程序添加到 MMU 即可实现仿真。这可以通过将 GPU 更新硬编码到 MMU 中来完成,或者定义一系列寄存器,其中将从 MMU 调用 GPU,以便从那里完成更专业的处理。 出于模块化的考虑,这里采用了后一种方法。
零页 I/O:GPU
位于 MMU.js 中
rb: function(addr)
{
switch(addr & 0xF000)
{
...
case 0xF000:
switch(addr & 0x0F00)
{
...
// Zero-page
case 0xF00:
if(addr >= 0xFF80)
{
return MMU._zram[addr & 0x7F];
}
else
{
// I/O control handling
switch(addr & 0x00F0)
{
// GPU (64 registers)
case 0x40: case 0x50: case 0x60: case 0x70:
return GPU.rb(addr);
}
return 0;
}
}
}
},
wb: function(addr, val)
{
switch(addr & 0xF000)
{
...
case 0xF000:
switch(addr & 0x0F00)
{
...
// Zero-page
case 0xF00:
if(addr >= 0xFF80)
{
MMU._zram[addr & 0x7F] = val;
}
else
{
// I/O
switch(addr & 0x00F0)
{
// GPU
case 0x40: case 0x50: case 0x60: case 0x70:
GPU.wb(addr, val);
break;
}
}
break;
}
break;
}
}
寄存器处理
位于 GPU.js 中
rb: function(addr)
{
switch(addr)
{
// LCD Control
case 0xFF40:
return (GPU._switchbg ? 0x01 : 0x00) |
(GPU._bgmap ? 0x08 : 0x00) |
(GPU._bgtile ? 0x10 : 0x00) |
(GPU._switchlcd ? 0x80 : 0x00);
// Scroll Y
case 0xFF42:
return GPU._scy;
// Scroll X
case 0xFF43:
return GPU._scx;
// Current scanline
case 0xFF44:
return GPU._line;
}
},
wb: function(addr, val)
{
switch(addr)
{
// LCD Control
case 0xFF40:
GPU._switchbg = (val & 0x01) ? 1 : 0;
GPU._bgmap = (val & 0x08) ? 1 : 0;
GPU._bgtile = (val & 0x10) ? 1 : 0;
GPU._switchlcd = (val & 0x80) ? 1 : 0;
break;
// Scroll Y
case 0xFF42:
GPU._scy = val;
break;
// Scroll X
case 0xFF43:
GPU._scx = val;
break;
// Background palette
case 0xFF47:
for(var i = 0; i < 4; i++)
{
switch((val >> (i * 2)) & 3)
{
case 0: GPU._pal[i] = [255,255,255,255]; break;
case 1: GPU._pal[i] = [192,192,192,255]; break;
case 2: GPU._pal[i] = [ 96, 96, 96,255]; break;
case 3: GPU._pal[i] = [ 0, 0, 0,255]; break;
}
}
break;
}
}
运行中的每帧
目前,模拟器 CPU 的调度循环永远运行,没有暂停。模拟器最基本的界面允许重置或暂停模拟;为了实现这一点,必须使用已知的时间量作为仿真器接口的基本单位。可以使用三种可能的时间单位:
- 指令:提供在每条 CPU 指令后暂停的机会。这会导致大量开销,因为 CPU 执行的每一步都必须调用调度函数;在 4.19MHz 频率下,必须采取许多步骤才能产生可观的数量。
- 行扫描:在 GPU 渲染每一行后暂停。这会产生较少的开销,但调度程序仍然必须每秒调用数千次;此外,在画布显示与当前扫描线不对应的状态下,可以暂停仿真。
- 帧:允许在整个帧被模拟、渲染并推送到画布后停止模拟。这提供了计时精度和最佳速度的最佳折衷,同时确保模拟画布与 GPU 状态一致。
由于每一帧由144条扫描线和10行垂直空白组成,每条扫描线运行需要456个时钟周期,因此一帧的长度为70224个时钟。与在仿真开始时初始化每个子系统的仿真器级重置功能结合使用,仿真器本身可以运行,并提供基本接口。
index.html:模拟器界面
<canvas id="screen" width="160" height="144"></canvas>
<a id="reset">Reset</a> | <a id="run">Run</a>
jsGB.js:重置和调度
jsGB = {
reset: function()
{
GPU.reset();
MMU.reset();
Z80.reset();
MMU.load('test.gb');
},
frame: function()
{
var fclk = Z80._clock.t + 70224;
do
{
Z80._map[MMU.rb(Z80._r.pc++)]();
Z80._r.pc &= 65535;
Z80._clock.m += Z80._r.m;
Z80._clock.t += Z80._r.t;
GPU.step();
} while(Z80._clock.t < fclk);
},
_interval: null,
run: function()
{
if(!jsGB._interval)
{
jsGB._interval = setTimeout(jsGB.frame, 1);
document.getElementById('run').innerHTML = 'Pause';
}
else
{
clearInterval(jsGB._interval);
jsGB._interval = null;
document.getElementById('run').innerHTML = 'Run';
}
}
};
window.onload = function()
{
document.getElementById('reset').onclick = jsGB.reset;
document.getElementById('run').onclick = jsGB.run;
jsGB.reset();
};
测试
将这些代码组合在一起的结果:模拟器能够加载和运行基于图形的演示。在这种情况下,正在加载的测试 ROM 是由Doug Lanford编写的滚动测试:当按下方向键盘按钮之一时,显示的背景将滚动。在这种特殊情况下,在未模拟键盘的情况下,会显示静态背景。
在下一部分中,这块拼图将就位:一个键盘模拟,它可以为模拟程序提供适当的输入。
以上是关于Game boy模拟器:集成的主要内容,如果未能解决你的问题,请参考以下文章