Saturday 23 July 2016

Part 19: Emulating the joystick

Foreword

In the previous blog we fixed the intro screen.

In this blog we will implement joystick emulation and see till which point the game will start up when we hit the fire button.

Implementing Joystick emulation

In the previous blog we covered the theory of a joystick interfaces to the C64.

We can therefore go straight ahead and start to implement joystick emulation to our emulator.


The joystick emulation in our emulator will also be interfacing with the keyboard, so we can utilise the keyboard class for joystick emulation.

So, which keys on the keyboard will we be using for the joystick? I was thinking to use the cursor keys on the numpad and Numpad zero for the fire button. To operate the joystick, Numlock should be off.

You may remember that the keyboard class maintains an array of which keys are hold down at any instance. We can also use this array to store joystick keys that are down. It is important though that the main keyboard emulation functionality should ignore these keys.

To ignore these keys we should add a default selector to the switch statement of the getScanCode method:

  function getScanCode(chr) {
    switch (chr) {
      case KEY_A:
        return 10;
      break;
      case KEY_B:
        return 28;
      break;
      case KEY_C:
        return 20;
      break;
      case KEY_D:
        return 18;

...
      case KEY_6:
        return 19;
      break;
      case KEY_7:
        return 24;
      break;
      case KEY_8:
        return 27;
      break;
      case KEY_9:
        return 32;
      break;
      default: return -1;

    }
    
  }


This default selector has the effect that for all joystick keys a -1 will be returned.

getColumnByte should then check for this -1 value and ignore if it is the case:

  this.getColumnByte = function(rowByte) {
    var rowArray = [0,0,0,0,0,0,0,0];
    rowByte = ~rowByte;
    rowByte = rowByte & 0xff;
    var i;
    for (i = 0; i < keyarray.length; i++) {
      var scanCode = getScanCode(keyarray[i]);
      var rowNum = (scanCode & 0x38) >> 3;
      if (scanCode != -1) {
        rowArray[rowNum] = rowArray[rowNum] | (1 << (scanCode & 7));
      }
    }

    ...

  }

Our joystick keys will now be ignored by our main keyboard emulation functionality.

We should now add a method that returns the joystick byte when CIA#1 register is read:

  this.getJoyStickByte = function () {
    var result = 0;
    for (i = 0; i < keyarray.length; i++) {
      var temp = keyarray[i];
      switch (temp) {
        //left
        case 37:
          result = result | 4;
        break;
        //right
        case 39:
          result = result | 8;
        break;
        //up
        case 38:
          result = result | 1;
        break;
        //down
        case 40:
          result = result | 2;
        break;
        //fire
        case 45:
          result = result | 16;
        break;
      }
    }
    return result;
  }


Our Memory class should invoke this method at the correct moment:

  function ciaRead(address) {
    if (address == 0xdc00) {
      return (~keyboardInstance.getJoyStickByte()) & 0xff;
    } else if (address == 0xdc01) {
      return keyboardInstance.getColumnByte(mainMem[0xdc00]);
    } else if (address == 0xdc04) {
      return mytimerA.getTimerLow();
    } else if (address == 0xdc05) {
      return mytimerA.getTimerHigh();
    } else if (address == 0xdc06) {
      return mytimerB.getTimerLow();
    } else if (address == 0xdc07) {
      return mytimerB.getTimerHigh();
    } else if (address == 0xdc0d) {
      return myinterruptController.getInterrupts();
    } else if (address == 0xdc0e) {
      return mytimerA.getControlRegister();
    } else if (address == 0xdc0f) {
      return mytimerB.getControlRegister();
    } else {
      return mainMem[address];
    }

  }

That was relatively painless!

Lets take our emulator for a test drive with these changes.

With the intro screen showing, lets hit the fire button, aka numpad 0. We can see that the game started, but we are presented again with the garbled screen that we briefly saw in the previous blog:


Some closer investigation of the VIC registers revealed that raster interrupts is enabled when this screen is shown.

We haven't enabled rasterline interrupts as yet within our emulator, but in the next section we will do that.

Implementing raster interrupts

Lets start coding the raster interrupts.

First, we implement the following methods:

...
  function rasterIntEnabled() {
    return (registers[0x1a] & 1) == 1;
  }

  function targetRasterReached() {
    var temp = registers[0x11] & 0x80;
    temp = temp << 1;
    temp = temp | (registers[0x12]);
    var tempCurrentLine = (cycleline == 312) ? 0 : cycleline;
    return (temp == tempCurrentLine);
  }
...

The first method determines if raster interrupts is enabled and the second method determines if the target raster line is reached yet. The target raster line is stored in register 12h and bit 7 of register 11h.

Something that might need some explanation is the check for 312 in the targetRasterReached method. Raster lines are within the range 0 to 311. At some point when this method gets called the raster line might be 312, e.g. roll over to 0 not yet applied. This line of code just caters for this condition.

Now, both these methods needs to be called in the processPixels method:

  this.processpixels = function() {
      ...
      if (cycleInLine > 63) {
        cycleInLine = 0;
        cycleline++;
        if (targetRasterReached() & rasterIntEnabled()) {
          registers[0x19] = registers[0x19] | 1 | 128;
        }
        updateCharPos();
      }
      ...
  }

So, if an interrupt occurred the applicable bit within register 19h is set.

With this code implemented, machine code polling the VIC interrupt register to see if a raster interrupt occurred would work correctly. However, at this point in time we would not be able to physically interrupt our emulated CPU with a raster interrupt.

We need to add some functionality to our CPU class to check during each call to step whether a raster interrupt occurred. This require us to also inject the Video class into our video class as a dependency. So lets add property and a setter for a video class into our cpu class:

function cpu(memory) {

...
 var myvideo;
...
  this.setVideo = function (video) {
    myvideo = video;
  }
...

}

We will let the index page handle the dependency injection for us:

      ...
      var mycpu = new cpu(mymem);
      myAlarmManager.setCpu(mycpu);
      myInterruptController.setCpu(mycpu);
      mycpu.setInterruptController(myInterruptController);
      var myvideo = new video(document.getElementById("screen"), mymem, mycpu);
      mymem.setVideo(myvideo);
      mycpu.setVideo(myvideo);
      ...

So far, so good. Next we need to add a method to out video class which will indicate for our Cpu whether an raster interrupt has occurred:

  this.vicIntOccured = function() {
    return registers[0x19] >= 128;
  }

Our Cpu needs to invoke this method:

function cpu(memory) {

...
  this.step = function () {
    if ((myInterruptController.getCpuInterruptOcurred() | myvideo.vicIntOccured()) & (interruptflag == 0)) {
        interruptOcurred = 0;
        Push(pc >> 8);
        Push(pc & 0xff);
        breakflag = 0;
        Push(getStatusFlagsAsByte());
        breakflag = 1;
        interruptflag = 1;
        tempVal = localMem.readMem(0xffff) * 256;
        tempVal = tempVal + localMem.readMem(0xfffe);
        pc = tempVal;
    }
...

}

We are almost done. We need to, however, give some attention attention to interrupt acknowledgement on the VIC side, very similar to what we have done in previous blogs with the CIA chip.

The interrupt acknowledge mechanism works a bit different in the VIC than in a CIA chip.

If you don't acknowledge interrupts on a CIA, your CPU will not receive new IRQ's.

The VIC works the the opposite. If you don't acknowledge an interrupt on the VIC, it will keep interrupting the CPU each time you clear the Interrupt disable flag.

Also, on the VIC, to acknowledge an intergroup you need  to write a 1 to the applicable bit in register 19.

With all this changes our writeReg method looks as follows:

  this.writeReg = function (number, value) {
    if (number == 0x19) {
      var temp = ~value & registers[number];
      temp = temp & 0xf;
      registers[number] = (temp > 0) ? (temp | 128) : 0;
    } else {
      registers[number] = value;
    }
  }

Before we take our emulator for a test drive there is just one more hack we should remove:

      function runBatch() {
        if (!running)
          return;
        //myvideo.updateCanvas();
        //var targetCycleCount =  mycpu.getCycleCount() + 20000;
        while (true) { 
          mycpu.step();
          myAlarmManager.processAlarms();
          //if (mycpu.getCycleCount() > 6000000)            
          //  mymem.setSimulateKeypress();
          //var blankingPeriodLow = targetCycleCount - 100;
          //if (myvideo.getCurrentLine() > 284) {
          //  mymem.writeMem(0xD012, 0);
          //} else  {
          //  mymem.writeMem(0xD012, 1);
          //}          if (mycpu.getPc() == breakpoint) {
            stopEm();
            return;
          }
          var framefinished = myvideo.processpixels();
          if (framefinished)            
            return;
        }
        //mycpu.setInterrupt();
      }

This hack stems from the early life of our emulator.

Lets us start our emulator again. This time, the screen looks better:


The majority of the screen is still garbled, but at least is the statusbar visible and the time updating every second.

Lets see what happens when we move around wit the the left/right arrows keys.

There is definitely some movement, and although we cannot see Dan Dare, we can see a battle with a Treen:


We will fix these remaining issues in the next blog or two.

In Summary

In this blog we implemented joystick emulation enabling us to start the game.

There is still a number of issues with rendering the graphic of the game.

We managed to fix the status bar and perfom some basic moves with the joystick.

In the next blog we investigate why the rest of the screen is garbled.

No comments:

Post a Comment