Tuesday, 24 May 2016

Part 10: Emulating the C64 Screen

Foreword

We ended off the previous section by confirming that booting the C64 system did indeed write the welcome message to screen memory.

This confirmation involved lots of manual checking: Working through a dump of screen memory and converting each hex number from screen code to ascii.

In this post we will relieve ourselves from this manual checking by implementing screen rendering. This screen rendering is going to be very basic: Text mode only, monochrome and a hardcoded screen address of 400h.

Some Feedback

From the time I wrote my last Blog I got two useful hints from Ed, one of the users at 6502.org.

The first thing Ed pointed out is that if you have Python installed on your pc, it is unnecessary to install a local webserver for development since Python comes bundled with one.

You invoke the bundled Python webserver from the commandline as follows: python -m SimpleHTTPServer

This will kick off a webserver listening at port 8000 and will serve pages from the current directory at which you invoked the command from.

Lastly Ed gave me some pointers to the use of GitHub pages. With this functionality Github will serve content from one of your GitHub repos as web pages. Quite nice if you want to give users the ability to give your emulator a quick spin without installing anything.

I actually gave GitHub pages a try myself. If you have a moment please visit http://ovalcode.github.io/

You will instantly have access to our emulator in a browser with changes covered in this blog and the next planned blog.

Hooking up the Character ROM

When implementing C64 text mode rendering, one sensible question might arise: Where do you get hold of a font closely resembling the C64 characters displayed to the screen? The answer is of course via the character ROM.

The character stores all the characters that the C64 can display as a set of images. Each image is an 8x8 pixel array where each pixel can be either set or transparent. Eight bytes would there be sufficient to store each character image.

All character images in the character rom is ordered by screen code. This means bytes 0 to 7 represent the image of screen code 0 (the @ sign), bytes 8 to 15 the image of screen code 1 (an A), bytes 16 to 23 the image of screen code 2 (a B), and so on.

Lets put this theory to the test by verifying the character image of screen code 1. First, let us us retrieve the bytes from the character rom by opening it in a hex editor:

From the hex dump lets take these values and cvert it to binary:

18 = 00011000
3C = 00111100
66 = 01100110
7E = 01111110
66 = 01100110
66 = 01100110
66 = 01100110
00 = 00000000

If you closely, you can see he ones form an A. Cool!

Next, lets write some code that will give our emulator access to the character ROM. We will task our Memory class with this duty. First lets create a arraybuffer for the contents of the character ROM:

  var mainMem = new Uint8Array(65536);
  var basicRom = new Uint8Array(8192);
  var kernalRom = new Uint8Array(8192);
  var charRom = new Uint8Array(4192);


Notice the character ROM is smaller than the other two ROMS. It is only 4KB.

Next, it is time to do another XMLHttpRequest sing and dance for the Character ROM:

//------------------------------------------------------------------------

var oReqChar = new XMLHttpRequest();
oReqChar.open("GET", "characters.bin", true);
oReqChar.responseType = "arraybuffer";

oReqChar.onload = function (oEvent) {
  var arrayBuffer = oReqChar.response; // Note: not oReq.responseText
  if (arrayBuffer) {
    charRom = new Uint8Array(arrayBuffer);
    downloadCompleted();
  }
};

oReqChar.send(null);


//------------------------------------------------------------------------


Don't forget to adjust the oustandingDownloads variable accordingly:

  var outstandingDownloads = 3;

We are almost done with the code changes to our Memory class. There is just one issue. Our Memory class in its current state has a flat memory model. So we can't really map it to a sensible area in Memory space for readMem to access.

To overcome this issue we will just, for now, implement a separate public method for accessing character ROM contents:

  this.readCharRom = function (address) {
    return charRom[address];
  }

We are done with changes to our memory class!

A Canvas to Write on

 OK, lets start to write code to emulate the C64 screen.

The first question would be how do you output the screen to a web page?

For this, HTML5 provide us with an element called the Canvas. Lets add a canvas element right away to our index.html page:

<body>
    <h1>6502 Emulator From Scratch</h1>
    <p>This is JavaScript Test</p>
<canvas id="screen" width="320" height="200">

</canvas><br/>
<textarea id="registers" name="reg" rows="1" cols="60"></textarea>
<br/>
<textarea id="memory" name="mem" rows="15" cols="60"></textarea>

This will add a canvas just below the titles. The canvas is 320 pixels wide and 200 pixels high. This is the same dimensions as the drawable area of the C64 screen.

 Now, how do you draw to this canvas. The w3schools.com website provide us with an example:

var c=document.getElementById("myCanvas");
var ctx=c.getContext("2d");
var imgData=ctx.createImageData(100,100);
for (var i=0;i<imgData.data.length;i+=4)
  {
  imgData.data[i+0]=255;
  imgData.data[i+1]=0;
  imgData.data[i+2]=0;
  imgData.data[i+3]=255;
  }
ctx.putImageData(imgData,10,10);

As you can see we need to jump through a couple of hoops to get to the raw pixel data: canvas->getConext()->createImageData().data. data is your actual array of pixels. Note that each pixel is represented by four elements in the following order:
  • Red
  • Green
  • Blue
  • Alpha
The Alpha value controls the amount of transparency of the canvas. Most of the time we will specify a value of 255, meaning the pixel is not transparent at all.

After you have manipulated the pixels remember to call putImage afterwards so the that the canvas is updated to the screen.

We now have enough to start coding. Our index.html file is getting more clunky by the day, so I will write this in a separate Javascript file called Video.js. First, we need to remember to add an include in index.html:

  <head>
    <title>6502 Emulator From Scratch</title>
    <script src="Memory.js"></script>
    <script src="Cpu.js"></script>
    <script src="Video.js"></script>
  </head>

Know we create a skeleton for Video.js:

function video(mycanvas, mem) {
  var localMem = mem;
  var ctx = mycanvas.getContext("2d");
  this.updateCanvas = function() {
    var imgData = ctx.createImageData(320, 200); }
}

updateCanvas is the method we will invoke to update the canvas.

Now the question is what do we put inside updateCanvas? We can start off with a loop iterating through all the characters of screen memory:

    var currentScreenPos;
    for (currentScreenPos = 0; currentScreenPos < 1000; currentScreenPos++) {
      var screenCode = localMem.readMem(1024 + currentScreenPos);
    }

For each screenCode we can get all its image bytes in charcater ROM:

    var currentScreenPos;
    for (currentScreenPos = 0; currentScreenPos < 1000; currentScreenPos++) {
      var screenCode = localMem.readMem(1024 + currentScreenPos);
      var currentRow;
      for (currentRow = 0; currentRow < 8; currentRow++) {
        var currentLine = localMem.readCharRom((screenCode << 3) + currentRow);
      } 
     }



Each row of image data we also need to loop through to get hold of the individual pixels:

      var currentRow, currentCol;
      for (currentRow = 0; currentRow < 8; currentRow++) {
        var currentLine = localMem.readCharRom((screenCode << 3) + currentRow);
        for (currentCol = 0; currentCol < 8; currentCol++) {
          var pixelSet = (currentLine & 0x80) == 0x80;          
          currentLine = currentLine <<1; 
        }      }

The pixelset variable will therefore hold information about the individual pixels. Note that the pixelset variable so it can either true (pixel is set) or false (pixel transparent).

Next, we should figure out where to write the pixel at hand on the canvas. We can write a small algorithm for this in pseudo code:

X-pixel Pos = (Number of characters to the left of screen) * 8 +currentCol
Y-pixel Pos = (Number of characters to the top of screen) * 8 + currentRow

One catch here is that we don't have a two dimension screen position (X and Y). We only have a linear address. We could easily determine the two dimensional position by dividing the linear address by 40. The integer part of the division result would then be your characters to the top and the remainder (e.g. modules) would be the number of characters to the left.

This would cause quiet a number of multiplications and divisions per second. You might potentially pay a performance penalty, so a better option would be to keep keep count of the two dimensional position in two separate variables:

    var currentScreenPos;
    var currentScreenX = 0;
    var currentScreenY = 0;    
    for (currentScreenPos = 0; currentScreenPos < 1000; currentScreenPos++) {
      var screenCode = localMem.readMem(1024 + currentScreenPos);
      if (currentScreenX == 40) {
        currentScreenX = 0;
        currentScreenY++;
      }     
    ...
      currentScreenX++;
    }




As you can see we have adjusted our main loop a bit. We introduced two new variables currentScreenX and currentScreenY. At the end of each loop iteration we increment currentScreenX. Remember our screen is 40 characters wide, so the range for currentScreenX is from 0 to 39. So once currentScreenX hits 40 its time to reset currentScreenX and increment currentScreenY by one. Lets implement this code:

      for (currentRow = 0; currentRow < 8; currentRow++) {
        var currentLine = localMem.readCharRom((screenCode << 3) + currentRow);
        for (currentCol = 0; currentCol < 8; currentCol++) {
          var pixelSet = (currentLine & 0x80) == 0x80;
          var pixelPosX = (currentScreenX << 3) + currentCol;
          var pixelPosY = (currentScreenY << 3) + currentRow;          
          currentLine = currentLine << 1;
        }
      }

We are almost finished coding. I am going to show you the remaining code and then I am going to explain it:

      for (currentRow = 0; currentRow < 8; currentRow++) {
        var currentLine = localMem.readCharRom((screenCode << 3) + currentRow);
        for (currentCol = 0; currentCol < 8; currentCol++) {
          var pixelSet = (currentLine & 0x80) == 0x80;
          var pixelPosX = (currentScreenX << 3) + currentCol;
          var pixelPosY = (currentScreenY << 3) + currentRow;    
          var posInBuffer = (pixelPosY * 320 + pixelPosX) << 2;
          if (pixelSet) {
            imgData.data[posInBuffer + 0] = 0;
            imgData.data[posInBuffer + 1] = 0;
            imgData.data[posInBuffer + 2] = 0;
            imgData.data[posInBuffer + 3] = 255;
          } else {
            imgData.data[posInBuffer + 0] = 255;
            imgData.data[posInBuffer + 1] = 255;
            imgData.data[posInBuffer + 2] = 255;
            imgData.data[posInBuffer + 3] = 255;

          }                
          currentLine = currentLine << 1;
        }
      }




First, notice the introduction of the variable posInBuffer. Since our data array is one dimensional, we need to convert our (X,Y) to a one dimensional address.

Also notice we are shifting the result left by two bits. This is equivalent to multiplying by 4(Remember each pixel consists of four elements).

Finally, we are writing a black pixel if pixel is set, otherwise a white one.

imgData now have prepared frame ready for display. In the final line of our updateCanvas method we should remember to call putImageData in order for the new frame to be displayed:

  ctx.putImageData(imgData,0,0);

What remains to be done is to create an instance of this video class in index.html:

      var mymem = new memory(postInit);
      var mycpu = new cpu(mymem);
      var myvideo = new video(document.getElementById("screen"), mymem);
      var mytimer;
      var running = false;
      var breakpoint = 0;


Slowing down to a real C64

In the previous section we wrote code for drawing one frame of our emulated screen.

Now the question, when do we invoke updateCavas? This question actually raises the the issue of syncronisation.

Up to this point in time I was a bit absent minded about making the emulator emulating the real speed of the C64. This is now high time do this.

First lets decide to emulate the PAL version of the C64. The PAL version renders 50 frames per second. So, we need to modify our runBatch method to accomodate this.

We will expand our runBatch command to not only render a batch of instructions, but also to render a frame. This means we should invoke our runBatch method 50 times a second. So out interval will need to change to a 1/50th of a second or 20 milliseconds:

      function startEm() {
        document.getElementById("btnStep").disabled = true;
        document.getElementById("btnRun").disabled = true;
        document.getElementById("btnStop").disabled = false;
        var myBreak = document.getElementById("breakpoint");
        breakpoint = parseInt(myBreak.value, 16);
        running = true;
        myTimer = setInterval(runBatch, 20);
      }

In order to syncronise our emulator to a real C64, we need to know how many instructions a real C64 would execute in 20 milliseconds. Well, actually instructions per second is a very crude term. A 6502 take anything from 2 to 7 clock cycles to execute.

A better question would be to ask: How many C64 clock cycles ticks in 20 milliseconds? That is easy to calculate. We know the CPU clock speed of the C64 is 1 MHz (For now I am going to stick with the theoretical number and not go into PAL/NTSC differences). So you just do 1000000/50. This amounts to 20000 clock cyces per 20 milliseconds.

Currently our CPU doesn't keep count of the number of clock cycles that passed, so we need to implement this first.

We start off with a private variable in our CPU class called cycleCount:

  var localMem = memory;
  var acc = 0;
  var x = 0;
  var y = 0;
  var pc = 0x400;
  var sp = 0xff;
  var zeroflag = 0;
  var negativeflag = 0;
  var carryflag =0;
  var overflowflag =0; 
  var decimalflag = 0;
  var interruptflag = 1;
  var breakflag = 1;
  var cycleCount = 0;


And of course, we need a getter, so that our runBatch method knows what is cooking:

    this.getCycleCount = function() {
      return cycleCount;
    }

We update the cycleCount variable within the step method of the Cpu class:

  this.step = function () {
    var opcode = localMem.readMem(pc);
    pc = pc + 1;
    var iLen = instructionLengths[opcode];
    var arg1 = 0;
    var arg2 = 0;
    var effectiveAdrress = 0;
    cycleCount = cycleCount + instructionCycles[opcode];
  ...
  }

We are now ready to modify the runBatch method. I am going to show the the whole method after the change and then discuss the changes:

      function runBatch() {
        if (!running)
          return;
        myvideo.updateCanvas();
        var targetCycleCount =  mycpu.getCycleCount() + 20000;
        while (mycpu.getCycleCount() < targetCycleCount) { 
          mycpu.step();
          var blankingPeriodLow = targetCycleCount - 100;
          if ((mycpu.getCycleCount() >= blankingPeriodLow) & (mycpu.getCycleCount() <= targetCycleCount)) {
            mymem.writeMem(0xD012, 0);
          } else  {
            mymem.writeMem(0xD012, 1);
          }
          if (mycpu.getPc() == breakpoint) {
            stopEm();
            return;
          }
        }
      }

Our for loop change to a while loop since it is now the responsibility of the CPU class to increment the cycleCount variable. The while loop only checks against a target clock cycle, which at the beginning of the loop was set to 20000 clock cycles in the future.

Ok, lets put everything to the test. On testing the speed was more or less as expected, and we see the familiar C64 welcome screen.


But, I don't see any flashing cursor!

I will leave this investigation on the missing cursor for the next Blog post. 

In Summary

In this blog we covered writing code to emulate a very basic C64 screen. It still doesn't show a flashing cursor, but we will cover this is the next blog.

Till next time!

No comments:

Post a Comment