Game boy模拟器:集成

Posted 妇男主人

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Game boy模拟器:集成相关的知识,希望对你有一定的参考价值。

在第 4 部分中,详细探讨了 GameBoy 的图形子系统,并将仿真放在一起。没有一套GPU的寄存器映射在软件中处理,图形子系统不能被仿真器使用;一旦这些寄存器可用,仿真器基本上就可以进行基本使用了。

GPU寄存器

GameBoy 的图形单元在内存映射的 I/O 空间中有一系列映射到内存中的寄存器。为了获得具有背景图像的工作仿真,GPU 将需要以下寄存器(其他寄存器也可用于 GPU,将在本系列的后面部分进行探讨)。

基本 GPU 寄存器

地址寄存器状态
0xFF40LCD and GPU control读 / 写
0xFF42Scroll-Y读 / 写
0xFF43Scroll-X读 / 写
0xFF44Current scan line只读
0xFF47Background palette只写

背景调色板寄存器之前已经被探索过,它由四个 2 位调色板条目组成。滚动寄存器和扫描线计数器是全字节值;这留下了 LCD 控制寄存器,它由 8 个独立的标志组成,用于控制 GPU 的各个部分。

GPU控制寄存器

功能01
0Background: on/offOffOn
1Sprites: on/offOffOn
2Sprites: size (pixels)8x88x16
3Background: tile map#0#1
4Background: tile set#0#1
5Window: on/offOffOn
6Window: tile map#0#1
7Display: on/offOffOn

在上表中,GPU 的附加功能出现了:可以出现在背景上方的“窗口”层,以及可以在背景和窗口上移动的角色对象。这些附加功能将在需要时涵盖;同时,背景标志对于基本渲染功能最重要。特别是,在这里可以看到如何更改背景瓦片地图和瓦片集,只需翻转寄存器 0x​​FF40 中的位即可。

实现: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模拟器:集成的主要内容,如果未能解决你的问题,请参考以下文章

Game boy模拟器:运行内存

Game boy模拟器:图形

Game boy模拟器:内存池

Game boy模拟器:完整的 Z80 内核CPU源码

Game boy模拟器:CPU

Game boy模拟器:输入