这里写自定义目录标题

  • 模拟屏幕
  • 光栅图形
  • 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 相关的时序,必须存在时序更新函数,该函数在每条指令执行后都会被调用。

Z80.js:调度器

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 的背景图形系统背后的概念,并将代码放入渲染函数中以模拟它们。