这里写自定义目录标题
- 模拟屏幕
- 光栅图形
- GPU 帧时序
在本系列的前几部分中,已经布置了 GameBoy 模拟器的结构,并达到了可以加载游戏 ROM 的程度,并由模拟的 CPU 逐步完成。通过将仿真处理器连接到内存映射结构,现在可以将外围设备连接到系统。GameBoy 和任何游戏控制台使用的主要外围设备之一是图形处理器 (GPU):它是控制台的主要输出方法,处理器的大部分工作都在为 GPU 生成图形。
模拟屏幕
任天堂对 GameBoy 的内部名称是“点阵游戏”;它的显示器是尺寸为 160x144 的像素 LCD。如果将 LCD 中的每个像素都当作 HTML5 中的一个像素,则可以直接映射到宽 160 和高 144 的画布。 为了直接寻址 LCD 中的每个像素,可以操纵画布的内容作为“帧缓冲区”:包含整个画布的单个内存块,作为一系列 4 字节 RGBA 值。
index.html
<html>
<head>
<script type="text/javascript" src="js/fileread.js"></script>
<script type="text/javascript" src="js/log.js"></script>
<script type="text/javascript" src="js/gpu.js"></script>
<script type="text/javascript" src="js/mmu.js"></script>
<script type="text/javascript" src="js/key.js"></script>
<script type="text/javascript" src="js/timer.js"></script>
<script type="text/javascript" src="js/z80.js"></script>
<script type="text/javascript" src="js/tabs.js"></script>
<script type="text/javascript" src="js/xhr.js"></script>
<style type="text/css">
* { margin:0; padding: 0; }
body { padding: 5px; background-color: black; color: white; font-family:Arial, Helvetica, sans-serif; font-size:0.82em; }
#out { height:144px; width:160px; border: 1px solid white; float:left; margin:0 5px 0 0; }
#msg { margin: 5px; font-family: sans-serif; font-size: 0.82em; }
div.tab { height:124px; width:320px; border:1px solid white; margin:20px 5px 5px 165px; overflow:auto; }
ul.ops { float:left; list-style:none inside; }
ul.ops li { cursor:pointer; }
table#reg { font-size:11px; font-family:Lucida Console, Bitstream Vera Sans Mono, monospace; line-height:1em; }
table#reg td.regname { text-align:right; padding-left:1em; }
ul.tablist { list-style: none inside; position:relative; bottom:-1px; }
ul.tablist li { display: block; float: left; background: #444; border-top: 1px solid #444; border-bottom: 1px solid white; padding: 3px 0.5em; margin-right: 2px; cursor: pointer; font-size:9px; }
ul.tablist li.tab_hi { border-left: 1px solid white; border-right: 1px solid white; border-top: 1px solid white; border-bottom: 1px solid black; background: black; }
p#op_load { margin-left: 165px; }
input { background:black; color:white; border:1px solid white; width:5em; }
input#file { width:10em; }
div#tilectrl { float:left; margin:1em;}
div#tilepixels { width:96px; height:96px; border:1px solid white; float:left; margin:1em; }
div#tilepixels div { width:12px; height:12px; float:left; }
div.subcanv { width:160px; float:left; }
p.fps { float:right; text-align:right; }
</style>
</head>
<body>
<div id="out">
<canvas id="screen" width="160" height="144">
</canvas>
</div>
<ul class="tablist" id="tablist_debug">
<li rel="tab_message">Messages</li>
<li rel="tab_registers">Debugger</li>
<li rel="tab_tile">Tile View</li>
</ul>
<div class="tab" id="tab_message"><div id="msg"></div></div>
<div class="tab" id="tab_registers">
<table id="reg">
<tr>
<td class="regname">A</td><td class="reg" rel="a"></td>
<td class="regname">LCDC</td><td class="io" rel="40"></td>
<td class="regname">JOYP</td><td class="io" rel="00"></td>
</tr>
<tr>
<td class="regname">B</td><td class="reg" rel="b"></td>
<td class="regname">STAT</td><td class="io" rel="41"></td>
</tr>
<tr>
<td class="regname">C</td><td class="reg" rel="c"></td>
<td class="regname">SCY</td><td class="io" rel="42"></td>
<td class="regname">DIV</td><td class="io" rel="04"></td>
</tr>
<tr>
<td class="regname">D</td><td class=reg" rel="d"></td>
<td class="regname">SCX</td><td class="io" rel="43"></td>
<td class="regname">TIMA</td><td class="io" rel="05"></td>
</tr>
<tr>
<td class="regname">E</td><td class="reg" rel="e"></td>
<td class="regname">LY</td><td class="io" rel="44"></td>
<td class="regname">TMA</td><td class="io" rel="06"></td>
</tr>
<tr>
<td class="regname">HL</td><td class="reg" rel="hl"></td>
<td class="regname">LYC</td><td class="io" rel="45"></td>
<td class="regname">TCA</td><td class="io" rel="07"></td>
</tr>
<tr>
<td class="regname">PC</td><td class="reg" rel="pc"></td>
<td class="regname">IE</td><td class="io" rel="ff"></td>
</tr>
<tr>
<td class="regname">SP</td><td class="reg" rel="sp"></td>
<td class="regname">IF</td><td class="io" rel="0f"></td>
</tr>
<tr>
<td class="regname">F</td><td class="reg" rel="f"></td>
</tr>
</table>
</div>
<div class="tab" id="tab_tile">
<div id="tilectrl">
<input type="text" id="tilenum" value="0"><br>
<ul class="ops">
<li id="tileprev">Prev</li>
<li id="tilenext">Next</li>
</ul>
</div>
<div id="tilepixels"></div>
</div>
<div class="subcanv">
<ul class="ops">
<li id="op_reset">Reset</li>
<li id="op_run">Run</li>
<li id="op_step">Step</li>
</ul>
<p class="fps"><span id="fps">0</span> fps</p>
</div>
<p id="op_load">
Load <input type="text" id="file" value="tests/ttt.gb">
Break at <input type="text" id="breakpoint">
</p>
<script type="text/javascript">
jsGB = {
run_interval: 0,
trace: '',
frame: function() {
var fclock = Z80._clock.m+17556;
var brk = document.getElementById('breakpoint').value;
var t0 = new Date();
do {
if(Z80._halt) Z80._r.m=1;
else
{
// Z80._r.r = (Z80._r.r+1) & 127;
Z80._map[MMU.rb(Z80._r.pc++)]();
Z80._r.pc &= 65535;
}
if(Z80._r.ime && MMU._ie && MMU._if)
{
Z80._halt=0; Z80._r.ime=0;
var ifired = MMU._ie & MMU._if;
if(ifired&1) { MMU._if &= 0xFE; Z80._ops.RST40(); }
else if(ifired&2) { MMU._if &= 0xFD; Z80._ops.RST48(); }
else if(ifired&4) { MMU._if &= 0xFB; Z80._ops.RST50(); }
else if(ifired&8) { MMU._if &= 0xF7; Z80._ops.RST58(); }
else if(ifired&16) { MMU._if &= 0xEF; Z80._ops.RST60(); }
else { Z80._r.ime=1; }
}
//jsGB.dbgtrace();
Z80._clock.m += Z80._r.m;
GPU.checkline();
TIMER.inc();
if((brk && parseInt(brk,16)==Z80._r.pc) || Z80._stop)
{
jsGB.pause();
break;
}
} while(Z80._clock.m < fclock);
var t1 = new Date();
document.getElementById('fps').innerHTML=Math.round(10000/(t1-t0))/10;
},
reset: function() {
LOG.reset(); GPU.reset(); MMU.reset(); Z80.reset(); KEY.reset(); TIMER.reset();
Z80._r.pc=0x100;MMU._inbios=0;Z80._r.sp=0xFFFE;Z80._r.hl=0x014D;Z80._r.c=0x13;Z80._r.e=0xD8;Z80._r.a=1;
MMU.load(document.getElementById('file').value);
document.getElementById('op_reset').onclick=jsGB.reset;
document.getElementById('op_run').onclick=jsGB.run;
document.getElementById('op_run').innerHTML='Run';
document.getElementById('op_step').onclick=jsGB.step;
document.getElementById('tilepixels').innerHTML='';
var tp = document.createElement('div');
var x;
for(var i=0; i<64; i++)
{
document.getElementById('tilepixels').appendChild(tp);
tp = tp.cloneNode(false);
}
document.getElementById('tilenum').onupdate=jsGB.dbgtile();
document.getElementById('tileprev').onclick=function(){
var t=parseInt(document.getElementById('tilenum').value); t--; if(t<0) t=383;
document.getElementById('tilenum').value=t.toString();
jsGB.dbgtile();
};
document.getElementById('tilenext').onclick=function(){
var t=parseInt(document.getElementById('tilenum').value); t++; if(t>383) t=0;
document.getElementById('tilenum').value=t.toString();
jsGB.dbgtile();
};
jsGB.dbgupdate();
jsGB.dbgtile();
jsGB.trace = '';
tabMagic.init();
jsGB.pause();
LOG.out('MAIN', 'Reset.');
},
run: function() {
Z80._stop = 0;
jsGB.run_interval = setInterval(jsGB.frame,1);
document.getElementById('op_run').innerHTML = 'Pause';
document.getElementById('op_run').onclick = jsGB.pause;
},
pause: function() {
clearInterval(jsGB.run_interval);
Z80._stop = 1;
jsGB.dbgupdate();
document.getElementById('op_run').innerHTML = 'Run';
document.getElementById('op_run').onclick = jsGB.run;
//XHR.connect('/log.php', {trace:jsGB.trace}, {success:function(x){}});
},
dbgupdate: function() {
var t = document.getElementById('reg').getElementsByTagName('td');
var x,j,k;
for(var i=0; i<t.length; i++)
{
if(t[i].className=='reg')
{
switch(t[i].getAttribute('rel'))
{
case 'a': case 'b': case 'c': case 'd': case 'e':
eval('x=Z80._r.'+t[i].getAttribute('rel')+'.toString(16);if(x.length==1)x="0"+x;');
break;
case 'pc': case 'sp':
eval('x=Z80._r.'+t[i].getAttribute('rel')+'.toString(16);if(x.length<4){p="";for(j=4;j>x.length;j--)p+="0";x=p+x;}');
break;
case 'hl':
k = (Z80._r.h<<8)+Z80._r.l;
x = k.toString(16); if(x.length<4){p="";for(j=4;j>x.length;j--)p+="0";x=p+x;}
break;
case 'f':
x = (Z80._r.f>>4).toString(2);if(x.length<4){p="";for(j=4;j>x.length;j--)p+="0";x=p+x;}
break;
}
t[i].innerHTML = x;
}
else if(t[i].className=='io')
{
j = parseInt(t[i].getAttribute('rel'),16);
x = MMU.rb(0xFF00+j).toString(16);
if(typeof(x) != 'undefined')
{
if(x.length==1) x='0'+x;
t[i].innerHTML = x;
}
}
}
},
dbgtrace: function() {
var a = Z80._r.a.toString(16); if(a.length==1) a='0'+a;
var b = Z80._r.b.toString(16); if(b.length==1) b='0'+b;
var c = Z80._r.c.toString(16); if(c.length==1) c='0'+c;
var d = Z80._r.d.toString(16); if(d.length==1) d='0'+d;
var e = Z80._r.e.toString(16); if(e.length==1) e='0'+e;
var f = Z80._r.f.toString(16); if(f.length==1) f='0'+f;
var h = Z80._r.h.toString(16); if(h.length==1) h='0'+h;
var l = Z80._r.l.toString(16); if(l.length==1) l='0'+l;
var pc = Z80._r.pc.toString(16); if(pc.length<4) { p=''; for(i=4;i>pc.length;i--) p+='0'; pc=p+pc; }
var sp = Z80._r.sp.toString(16); if(sp.length<4) { p=''; for(i=4;i>sp.length;i--) p+='0'; sp=p+sp; }
jsGB.trace +=
("A"+a+"/B"+b+"/C"+c+"/D"+d+"/E"+e+"/F"+f+"/H"+h+"/L"+l+"/PC"+pc+"/SP"+sp+"\n");
},
dbgtile: function() {
var tn = parseInt(document.getElementById('tilenum').value);
var t = GPU._tilemap[tn];
var c = ['#ffffff','#c0c0c0','#606060','#000000'];
var d = document.getElementById('tilepixels').getElementsByTagName('div');
for(var y=0;y<8;y++)
for(var x=0;x<8;x++)
d[y*8+x].style.backgroundColor=c[t[y][x]];
},
step: function() {
if(Z80._r.ime && MMU._ie && MMU._if)
{
Z80._halt=0; Z80._r.ime=0;
if((MMU._ie&1) && (MMU._if&1))
{
MMU._if &= 0xFE; Z80._ops.RST40();
}
}
else
{
if(Z80._halt) { Z80._r.m=1; }
else
{
Z80._r.r = (Z80._r.r+1) & 127;
Z80._map[MMU.rb(Z80._r.pc++)]();
Z80._r.pc &= 65535;
}
}
Z80._clock.m += Z80._r.m; Z80._clock.t += (Z80._r.m*4);
GPU.checkline();
if(Z80._stop)
{
jsGB.pause();
}
jsGB.dbgupdate();
}
};
window.onload = jsGB.reset;
window.onkeydown = KEY.keydown;
window.onkeyup = KEY.keyup;
</script>
</body>
</html>
gpu.js
GPU = {
_vram: [],
_oam: [],
_reg: [],
_tilemap: [],
_objdata: [],
_objdatasorted: [],
_palette: {'bg':[], 'obj0':[], 'obj1':[]},
_scanrow: [],
_curline: 0,
_curscan: 0,
_linemode: 0,
_modeclocks: 0,
_yscrl: 0,
_xscrl: 0,
_raster: 0,
_ints: 0,
_lcdon: 0,
_bgon: 0,
_objon: 0,
_winon: 0,
_objsize: 0,
_bgtilebase: 0x0000,
_bgmapbase: 0x1800,
_wintilebase: 0x1800,
reset: function() {
for(var i=0; i<8192; i++) {
GPU._vram[i] = 0;
}
for(i=0; i<160; i++) {
GPU._oam[i] = 0;
}
for(i=0; i<4; i++) {
GPU._palette.bg[i] = 255;
GPU._palette.obj0[i] = 255;
GPU._palette.obj1[i] = 255;
}
for(i=0;i<512;i++)
{
GPU._tilemap[i] = [];
for(j=0;j<8;j++)
{
GPU._tilemap[i][j] = [];
for(k=0;k<8;k++)
{
GPU._tilemap[i][j][k] = 0;
}
}
}
LOG.out('GPU', 'Initialising screen.');
var c = document.getElementById('screen');
if(c && c.getContext)
{
GPU._canvas = c.getContext('2d');
if(!GPU._canvas)
{
throw new Error('GPU: Canvas context could not be created.');
}
else
{
if(GPU._canvas.createImageData)
GPU._scrn = GPU._canvas.createImageData(160,144);
else if(GPU._canvas.getImageData)
GPU._scrn = GPU._canvas.getImageData(0,0,160,144);
else
GPU._scrn = {'width':160, 'height':144, 'data':new Array(160*144*4)};
for(i=0; i<GPU._scrn.data.length; i++)
GPU._scrn.data[i]=255;
GPU._canvas.putImageData(GPU._scrn, 0,0);
}
}
GPU._curline=0;
GPU._curscan=0;
GPU._linemode=2;
GPU._modeclocks=0;
GPU._yscrl=0;
GPU._xscrl=0;
GPU._raster=0;
GPU._ints = 0;
GPU._lcdon = 0;
GPU._bgon = 0;
GPU._objon = 0;
GPU._winon = 0;
GPU._objsize = 0;
for(i=0; i<160; i++) GPU._scanrow[i] = 0;
for(i=0; i<40; i++)
{
GPU._objdata[i] = {'y':-16, 'x':-8, 'tile':0, 'palette':0, 'yflip':0, 'xflip':0, 'prio':0, 'num':i};
}
// Set to values expected by BIOS, to start
GPU._bgtilebase = 0x0000;
GPU._bgmapbase = 0x1800;
GPU._wintilebase = 0x1800;
LOG.out('GPU', 'Reset.');
},
checkline: function() {
GPU._modeclocks += Z80._r.m;
switch(GPU._linemode)
{
// In hblank
case 0:
if(GPU._modeclocks >= 51)
{
// End of hblank for last scanline; render screen
if(GPU._curline == 143)
{
GPU._linemode = 1;
GPU._canvas.putImageData(GPU._scrn, 0,0);
MMU._if |= 1;
}
else
{
GPU._linemode = 2;
}
GPU._curline++;
GPU._curscan += 640;
GPU._modeclocks=0;
}
break;
// In vblank
case 1:
if(GPU._modeclocks >= 114)
{
GPU._modeclocks = 0;
GPU._curline++;
if(GPU._curline > 153)
{
GPU._curline = 0;
GPU._curscan = 0;
GPU._linemode = 2;
}
}
break;
// In OAM-read mode
case 2:
if(GPU._modeclocks >= 20)
{
GPU._modeclocks = 0;
GPU._linemode = 3;
}
break;
// In VRAM-read mode
case 3:
// Render scanline at end of allotted time
if(GPU._modeclocks >= 43)
{
GPU._modeclocks = 0;
GPU._linemode = 0;
if(GPU._lcdon)
{
if(GPU._bgon)
{
var linebase = GPU._curscan;
var mapbase = GPU._bgmapbase + ((((GPU._curline+GPU._yscrl)&255)>>3)<<5);
var y = (GPU._curline+GPU._yscrl)&7;
var x = GPU._xscrl&7;
var t = (GPU._xscrl>>3)&31;
var pixel;
var w=160;
if(GPU._bgtilebase)
{
var tile = GPU._vram[mapbase+t];
if(tile<128) tile=256+tile;
var tilerow = GPU._tilemap[tile][y];
do
{
GPU._scanrow[160-x] = tilerow[x];
GPU._scrn.data[linebase+3] = GPU._palette.bg[tilerow[x]];
x++;
if(x==8) { t=(t+1)&31; x=0; tile=GPU._vram[mapbase+t]; if(tile<128) tile=256+tile; tilerow = GPU._tilemap[tile][y]; }
linebase+=4;
} while(--w);
}
else
{
var tilerow=GPU._tilemap[GPU._vram[mapbase+t]][y];
do
{
GPU._scanrow[160-x] = tilerow[x];
GPU._scrn.data[linebase+3] = GPU._palette.bg[tilerow[x]];
x++;
if(x==8) { t=(t+1)&31; x=0; tilerow=GPU._tilemap[GPU._vram[mapbase+t]][y]; }
linebase+=4;
} while(--w);
}
}
if(GPU._objon)
{
var cnt = 0;
if(GPU._objsize)
{
for(var i=0; i<40; i++)
{
}
}
else
{
var tilerow;
var obj;
var pal;
var pixel;
var x;
var linebase = GPU._curscan;
for(var i=0; i<40; i++)
{
obj = GPU._objdatasorted[i];
if(obj.y <= GPU._curline && (obj.y+8) > GPU._curline)
{
if(obj.yflip)
tilerow = GPU._tilemap[obj.tile][7-(GPU._curline-obj.y)];
else
tilerow = GPU._tilemap[obj.tile][GPU._curline-obj.y];
if(obj.palette) pal=GPU._palette.obj1;
else pal=GPU._palette.obj0;
linebase = (GPU._curline*160+obj.x)*4;
if(obj.xflip)
{
for(x=0; x<8; x++)
{
if(obj.x+x >=0 && obj.x+x < 160)
{
if(tilerow[7-x] && (obj.prio || !GPU._scanrow[x]))
{
GPU._scrn.data[linebase+3] = pal[tilerow[7-x]];
}
}
linebase+=4;
}
}
else
{
for(x=0; x<8; x++)
{
if(obj.x+x >=0 && obj.x+x < 160)
{
if(tilerow[x] && (obj.prio || !GPU._scanrow[x]))
{
GPU._scrn.data[linebase+3] = pal[tilerow[x]];
}
}
linebase+=4;
}
}
cnt++; if(cnt>10) break;
}
}
}
}
}
}
break;
}
},
updatetile: function(addr,val) {
var saddr = addr;
if(addr&1) { saddr--; addr--; }
var tile = (addr>>4)&511;
var y = (addr>>1)&7;
var sx;
for(var x=0;x<8;x++)
{
sx=1<<(7-x);
GPU._tilemap[tile][y][x] = ((GPU._vram[saddr]&sx)?1:0) | ((GPU._vram[saddr+1]&sx)?2:0);
}
},
updateoam: function(addr,val) {
addr-=0xFE00;
var obj=addr>>2;
if(obj<40)
{
switch(addr&3)
{
case 0: GPU._objdata[obj].y=val-16; break;
case 1: GPU._objdata[obj].x=val-8; break;
case 2:
if(GPU._objsize) GPU._objdata[obj].tile = (val&0xFE);
else GPU._objdata[obj].tile = val;
break;
case 3:
GPU._objdata[obj].palette = (val&0x10)?1:0;
GPU._objdata[obj].xflip = (val&0x20)?1:0;
GPU._objdata[obj].yflip = (val&0x40)?1:0;
GPU._objdata[obj].prio = (val&0x80)?1:0;
break;
}
}
GPU._objdatasorted = GPU._objdata;
GPU._objdatasorted.sort(function(a,b){
if(a.x>b.x) return -1;
if(a.num>b.num) return -1;
});
},
rb: function(addr) {
var gaddr = addr-0xFF40;
switch(gaddr)
{
case 0:
return (GPU._lcdon?0x80:0)|
((GPU._bgtilebase==0x0000)?0x10:0)|
((GPU._bgmapbase==0x1C00)?0x08:0)|
(GPU._objsize?0x04:0)|
(GPU._objon?0x02:0)|
(GPU._bgon?0x01:0);
case 1:
return (GPU._curline==GPU._raster?4:0)|GPU._linemode;
case 2:
return GPU._yscrl;
case 3:
return GPU._xscrl;
case 4:
return GPU._curline;
case 5:
return GPU._raster;
default:
return GPU._reg[gaddr];
}
},
wb: function(addr,val) {
var gaddr = addr-0xFF40;
GPU._reg[gaddr] = val;
switch(gaddr)
{
case 0:
GPU._lcdon = (val&0x80)?1:0;
GPU._bgtilebase = (val&0x10)?0x0000:0x0800;
GPU._bgmapbase = (val&0x08)?0x1C00:0x1800;
GPU._objsize = (val&0x04)?1:0;
GPU._objon = (val&0x02)?1:0;
GPU._bgon = (val&0x01)?1:0;
break;
case 2:
GPU._yscrl = val;
break;
case 3:
GPU._xscrl = val;
break;
case 5:
GPU._raster = val;
// OAM DMA
case 6:
var v;
for(var i=0; i<160; i++)
{
v = MMU.rb((val<<8)+i);
GPU._oam[i] = v;
GPU.updateoam(0xFE00+i, v);
}
break;
// BG palette mapping
case 7:
for(var i=0;i<4;i++)
{
switch((val>>(i*2))&3)
{
case 0: GPU._palette.bg[i] = 255; break;
case 1: GPU._palette.bg[i] = 192; break;
case 2: GPU._palette.bg[i] = 96; break;
case 3: GPU._palette.bg[i] = 0; break;
}
}
break;
// OBJ0 palette mapping
case 8:
for(var i=0;i<4;i++)
{
switch((val>>(i*2))&3)
{
case 0: GPU._palette.obj0[i] = 255; break;
case 1: GPU._palette.obj0[i] = 192; break;
case 2: GPU._palette.obj0[i] = 96; break;
case 3: GPU._palette.obj0[i] = 0; break;
}
}
break;
// OBJ1 palette mapping
case 9:
for(var i=0;i<4;i++)
{
switch((val>>(i*2))&3)
{
case 0: GPU._palette.obj1[i] = 255; break;
case 1: GPU._palette.obj1[i] = 192; break;
case 2: GPU._palette.obj1[i] = 96; break;
case 3: GPU._palette.obj1[i] = 0; break;
}
}
break;
}
}
};
一旦为屏幕数据分配了一块内存,就可以通过将 RGBA 分量写入块中该像素位置的四个值来设置单个像素的颜色;像素位置可由公式确定y * 160 + x。
光栅图形
在画布就位以接收 GameBoy 的图形输出后,下一步是模拟图形的制作。原来的GameBoy硬件在时序上模拟了阴极射线管(CRT):在CRT中,电子束逐行扫描屏幕(从左到右,从上到下),扫描结束后扫描过程返回到屏幕顶部。
可以看出,CRT 需要更多的时间来绘制扫描线,而不是简单地在有问题的像素上运行:需要一个“水平消隐”期,使光束从一行的末尾移动到下一行的开头. 类似地,每一帧的结束意味着一个“垂直消隐”期,而光束则返回到左上角。由于光束必须在垂直消隐中进一步移动,因此该时间段通常比水平消隐时间长得多。
同样,GameBoy 显示器呈现水平和垂直消隐期。此外,扫描线本身所花费的时间分为两部分:GPU 在访问视频内存和访问精灵属性内存之间切换,同时绘制扫描线。出于此仿真的目的,这两个部分是不同的,并且相互遵循。下表说明了 GPU 在每个周期内停留的时间,以运行在 4194304 Hz 的 CPU 的 T 时钟表示。
GPU 帧时序
时期 | GPU 模式编号 | 花费的时间(时钟) |
Scanline(访问OAM) | 2 | 80 |
扫描线(访问 VRAM) | 3 | 172 |
水平空白 | 0 | 204 |
一行(扫描和空白) | 456 | |
垂直空白 | 1 | 4560(10行) |
全帧(扫描和空白) | 70224 |
为了保持这些与仿真 CPU 相关的时序,必须存在时序更新函数,该函数在每条指令执行后都会被调用。
while (true) {
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();
}
GPU.js:时钟步长
_mode: 0,
_modeclock: 0,
_line: 0,
step: function()
{
GPU._modeclock += Z80._r.t;
switch(GPU._mode)
{
// OAM read mode, scanline active
case 2:
if(GPU._modeclock >= 80)
{
// Enter scanline mode 3
GPU._modeclock = 0;
GPU._mode = 3;
}
break;
// VRAM read mode, scanline active
// Treat end of mode 3 as end of scanline
case 3:
if(GPU._modeclock >= 172)
{
// Enter hblank
GPU._modeclock = 0;
GPU._mode = 0;
// Write a scanline to the framebuffer
GPU.renderscan();
}
break;
// Hblank
// After the last hblank, push the screen data to canvas
case 0:
if(GPU._modeclock >= 204)
{
GPU._modeclock = 0;
GPU._line++;
if(GPU._line == 143)
{
// Enter vblank
GPU._mode = 1;
GPU._canvas.putImageData(GPU._scrn, 0, 0);
}
else
{
GPU._mode = 2;
}
}
break;
// Vblank (10 lines)
case 1:
if(GPU._modeclock >= 456)
{
GPU._modeclock = 0;
GPU._line++;
if(GPU._line > 153)
{
// Restart scanning modes
GPU._mode = 2;
GPU._line = 0;
}
}
break;
}
}
在上面的代码中,GPU 的计时已经建立,但 GPU 的工作还没有到位:renderscan
是工作发生的地方。在本系列的下一部分中,将研究 GameBoy 的背景图形系统背后的概念,并将代码放入渲染函数中以模拟它们。