Thursday, 15 December 2016

Part 16: Implementing the other Graphic Modes

Foreword

In the previous post we have jacked up the video rendering of our emulator a quite a bit. Here is a summarised list of the rendering enhancements:

  • Adding color and surrounding border
  • Implemented scanline rendering

With above mentioned enhancements we could manage to simulate the flashing borders with stripes when we loading the Game Dan Dare from the tape image.

There was, however, still a couple of things that didn't render correctly while the game was loading:

  • No splash screen
  • Intro screen looked a bit garbled
In this post we will be trying to solve above mentioned issues. This journey will involve implementing more of the graphics modes of the VIC-II. We will also need to properly implement the memory model of the VIC-II within our emulator.

To aid us in testing, it will also make sense to implement joystick emulation to our emulator. Without joystick emulation we will not be able to get past the intro screen to evaluate some of the other graphic modes.


Joystick Emulation

Let us start with implementing joystick emulation.

The basic idea is to draw the joystick on the screen of the Android Device and then check when user have selected a joystick direction or pressed the fire button.

The on screen joystick will look as follows:


In effect one control for selecting direction and another one for fire.

You will notice that for the directional control that I have catered for eight directions instead of four. This just makes handling on the touch screen easier when you need to go in a diagonal direction while simultaneously need to press the firebutton.

Our directional control can be described as a track divided into sectors. These sectors are spaced from each other by a couple of degrees.

So, how do we determine which direction is pressed down? To answer this question we need view the directional control as a circle and ask: Where are we on the circle?

An Android touch screen provides the current position of the user's finger on the screen as an (x,y) coordinate. First of all, we need to transform this coordinate to a coordinate relative to the centre of the directional control circle.

From our school days, such a transformed coordinate can also be thought of as a rectangular coordinate.

In order for us to determine which direction was selected we need to convert this rectangular coordinate to polar form, that is getting an angle in degrees and radius. We can then determine which direction was selected with the following table:


  • 112.5 Degrees - 67.5 Degrees = North
  • 67.5 Degrees - 22.5 Degrees = North East
  • 22.5 Degrees - (-22.5 Degrees) = East
  • (-22.5 Degrees) - (-67.5 Degrees) = South East
  • (-67.5 Degrees) - (-112.5) = South
  • (-112.5 Degrees) - (-157 Degrees) = South West
  • (-157 Degrees) - (-202.50 Degrees) = West
  • (-202.5 Degrees) - (-247.5 Degrees) = North West
The negative angles may look a bit weird. Just remember that we start at North and moving clockwise. Once going passed 0 Degrees, we will use encounter Negative angles, rather than counter from 360 Degrees.

All this logic will happen in the JoystickView class. To keep focused I will not discuss the technical details of this class. Maybe just remember that this class outputs the selected angle as an integer between 0 and 7. 0 is north and works in a clockwise direction until with number 7 you are at North West, as followed with the above mentioned table. 

Let us do some coding. Within memory.c add the following methods:

void Java_com_johan_emulator_engine_Emu6502_setFireButton(JNIEnv* pEnv, jobject pObj, jint fireButtonStatus) {
  if (fireButtonStatus)
    joystickStatus = joystickStatus | 16;
  else
    joystickStatus = joystickStatus & 0xef;
}

void Java_com_johan_emulator_engine_Emu6502_setJoystickDirectionButton(JNIEnv* pEnv, jobject pObj, jint fireButtonStatus) {
  //start at north, then goes clockwise till again at north
  joystickStatus = joystickStatus & 0xf8;
  switch (fireButtonStatus) {
    case 0: //North
      joystickStatus = joystickStatus | JOYSTICK_UP;
    break;

    case 1: //North East
      joystickStatus = joystickStatus | JOYSTICK_UP | JOYSTICK_RIGHT;
    break;

    case 2: //East
      joystickStatus = joystickStatus | JOYSTICK_RIGHT;
    break;

    case 3: //South East
      joystickStatus = joystickStatus | JOYSTICK_DOWN | JOYSTICK_RIGHT;
    break;

    case 4: //South
      joystickStatus = joystickStatus | JOYSTICK_DOWN;
    break;

    case 5: //South West
      joystickStatus = joystickStatus | JOYSTICK_DOWN | JOYSTICK_LEFT;
    break;

    case 6: //West
      joystickStatus = joystickStatus | JOYSTICK_LEFT;
    break;

    case 7: //North West
      joystickStatus = joystickStatus | JOYSTICK_LEFT | JOYSTICK_UP;
    break;

    default:
    break;

  }
}


When the user press or release either the fire button or one the directional buttons, one of these methods will be invoked.

The joystickStatus has the same format as the joystick byte at memory location DC00. Finally, we need to make the following change to cia_read:

jchar cia1_read(int address) {
  jchar result = 0;
  switch (address) {
    case 0xdc00:
      result =  ~joystickStatus & 0xff ;
    break;

    case 0xdc01:
      result = getKeyPortByte(mainMem[0xdc00]);
    break;
...
  }
...
} 

The VIC II Memory Model

Up to now we have hardcoded some assumptions into our emulator regarding VIC II memory access. The first assumption is character memory start at memory location 1024 and other assumption is that the character bitmap images is always retrieved from character ROM.

These assumptions will be sufficient for us to continue, so we will need to properly implement the VIC memory module.

We will use the following snippet of code to calculate the correct Video memory and character memory:

  int memPointer = memory_unclaimed_io_read(0xd018);
  int videoMemoryBase = memPointer & 0xf0;
  videoMemoryBase = videoMemoryBase << 6;
  int charROMBase = memPointer & 0xe;
  charROMBase = charROMBase << 10;

This snippet of code, however, will yield only 14-bit wide addresses. The other 2 bits is provided by location DD00 in the IO bank. The following methods within memory.c make use of location DD00:

void memory_read_batch(int *batch, int address, int count) {
  address = ((~IOUnclaimed[0xd00] & 3) << 14) | address;
  int i;
  for (i = 0; i < count; i++) {
    if ((address >= 0x1000 && address < 0x2000) || (address >= 0x9000 && address < 0xa000))
      batch[i] = charRom[address & 0xfff];
    else
      batch[i] = mainMem[address + i];
  }
}

jchar memory_read_vic_model(int address) {
  address = ((~IOUnclaimed[0xd00] & 3) << 14) | address;
  if ((address >= 0x1000 && address < 0x2000) || (address >= 0x9000 && address < 0xa000))
    return charRom[address & 0xfff];
  else
    return mainMem[address];

}


It should be noted that in the VIC-II memory model, the memory ranges 1000-2000 and 9000-A000 will always point to a location in Character ROM.

Implementing the Other Graphic Modes

Currently our processLine method within video.c only works with one graphics mode. To cater for the other modes, we need to add a case statement as follows:

static inline void processLine() {
  if (line_count > 299)
    return;

  updatelineCharPos();
  fillColor(24, memory_unclaimed_io_read(0xd020) & 0xf);
  int screenEnabled = (memory_unclaimed_io_read(0xd011) & 0x10) ? 1 : 0;
  if (screenLineRegion && screenEnabled) {
    jchar bitmapMode = (memory_unclaimed_io_read(0xd011) & 0x20) ? 1 : 0;
    jchar multiColorMode = (memory_unclaimed_io_read(0xd016) & 0x10) ? 1 : 0;
    jchar screenMode = (bitmapMode << 1) | (multiColorMode);
    switch (screenMode) {
      case 0: //Normal texmode
        drawScreenLineNormalText();
      break;

      case 1: //Multi color text mode
        drawScreenLineMultiColorText();
      break;

      case 2: //Standard bitmap mode
      break;

      case 3: //Multicolor bitmap
        drawScreenLineMultiColorBitmap();
      break;
    }

  } else {
    fillColor(320, memory_unclaimed_io_read(0xd020) & 0xf);
  }
  fillColor(24, memory_unclaimed_io_read(0xd020) & 0xf);
}

We form a 2-bit number, screenMode, from the bitmapMode bit and multicolorMode bit from locations D011 and D016 respectively. It is this 2-bit number that we use for the switch selector.

As you can see, we have implemented all the graphics modes except standard bitmap mode. This is because we don't need it yet for the game we are emulating.

Let us now look at the implementation of these graphic modes.

Standard text mode we have already covered in the previous post, so I will not be covering it here.

Let us look at the implementation of multi color text mode:

static inline void drawScreenLineMultiColorText() {
  int i;
  int batchCharMem[40];
  int batchColorMem[40];
  int color_tablet[4];
  int memPointer = memory_unclaimed_io_read(0xd018);
  int videoMemoryBase = memPointer & 0xf0;
  videoMemoryBase = videoMemoryBase << 6;
  int charROMBase = memPointer & 0xe;
  charROMBase = charROMBase << 10;

  int backgroundColor = memory_unclaimed_io_read(0xd021) & 0xf;
  memory_read_batch(batchCharMem, videoMemoryBase + posInCharMem, 40);
  memory_read_batch_io_unclaimed(batchColorMem, 0xd800 + posInCharMem, 40);
  for (i = 0; i < 40; i++) {
    jchar charcode = batchCharMem[i];//memory_read(1024 + i + posInCharMem);
    int bitmapDataRow = memory_read_vic_model(((charcode << 3) | (line_in_visible & 7)) + charROMBase);
    int j;
    int foregroundColor = batchColorMem[i] & 0xf;//memory_read(0xd800 + i + posInCharMem) & 0xf;
    if (foregroundColor & 8) {
      foregroundColor = foregroundColor & 7;
      color_tablet[0] = backgroundColor;
      color_tablet[1] = memory_unclaimed_io_read(0xd022) & 0xf;
      color_tablet[2] = memory_unclaimed_io_read(0xd023) & 0xf;
      color_tablet[3] = foregroundColor;
          for (j = 0; j < 4; j++) {
            int pixelSet = bitmapDataRow & 0xc0;
            pixelSet = pixelSet >> 6;

            g_buffer[posInBuffer] = colors_RGB_565[color_tablet[pixelSet]];
            posInBuffer++;

            g_buffer[posInBuffer] = colors_RGB_565[color_tablet[pixelSet]];
            posInBuffer++;

            bitmapDataRow = bitmapDataRow << 2;
          }

    } else {
      for (j = 0; j < 8; j++) {
        foregroundColor = foregroundColor & 7;
        int pixelSet = bitmapDataRow & 0x80;
        if (pixelSet) {
          g_buffer[posInBuffer] = colors_RGB_565[foregroundColor];
        } else {
          g_buffer[posInBuffer] = colors_RGB_565[backgroundColor];
        }
        posInBuffer++;
        bitmapDataRow = bitmapDataRow << 1;
      }
    }
  }
}

For each character to display we build up a four color tablet mapping to each of the four combinations of a pixel pair. For multi color text Mode, the color entries are defined as follows:


  • Entry 0: Color stored in D021
  • Entry 1: Color stored in D022
  • Entry 2: Color stored in D023
  • Entry 3: Foreground color, that is color stored in color RAM for this character

Multi color text mode has an interesting caveat. If bit 3 of the color stored in character RAM for the applicable character is set to zero, then that character will be displayed in standard bitmap mode. We test for this scenario via the outer if-statement.

Finally, let us look at the implementation of multi color bitmap mode:

static inline void drawScreenLineMultiColorBitmap() {
  int i;
  int batchCharMem[40];
  int batchColorMem[40];
  int memPointer = memory_unclaimed_io_read(0xd018);
  int videoMemoryBase = memPointer & 0xf0;
  videoMemoryBase = videoMemoryBase << 6;
  int charROMBase = memPointer & 0xe;
  charROMBase = charROMBase << 10;
  int color_tablet[4];
  color_tablet[0] = memory_unclaimed_io_read(0xd021) & 0xf;
  //int backgroundColor = memory_unclaimed_io_read(0xd021) & 0xf;
  memory_read_batch(batchCharMem, videoMemoryBase + posInCharMem, 40);
  memory_read_batch_io_unclaimed(batchColorMem, 0xd800 + posInCharMem, 40);
  for (i = 0; i < 40; i++) {
    jchar charcode = batchCharMem[i];//memory_read(1024 + i + posInCharMem);
    color_tablet[1] = charcode >> 4;
    color_tablet[2] = charcode & 0xf;
    color_tablet[3] = batchColorMem[i] & 0xf;
    int bitmapDataRow = memory_read_vic_model((((posInCharMem + i) << 3) | (line_in_visible & 7)) + charROMBase);
    int j;
    //int foregroundColor = batchColorMem[i] & 0xf;//memory_read(0xd800 + i + posInCharMem) & 0xf;

    for (j = 0; j < 4; j++) {
      int pixelSet = bitmapDataRow & 0xc0;
      pixelSet = pixelSet >> 6;

      g_buffer[posInBuffer] = colors_RGB_565[color_tablet[pixelSet]];
      posInBuffer++;

      g_buffer[posInBuffer] = colors_RGB_565[color_tablet[pixelSet]];
      posInBuffer++;

      bitmapDataRow = bitmapDataRow << 2;
    }
  }
}

Very similar to text mode multi color, but with subtle differences.

First, compare the assignment of the variable bitmapDataRow between the two modes. Multi color text mode uses character code whereas multi color bitmap mode uses the position in character memory as index.

The definition of the 4 color tablet is also different:


  • Entry 0: Background color
  • Entry 1: Upper 4 bits of character code in screen memory
  • Entry 2: Lower 4 bits of character code in screen memory
  • Entry 3: Color code in Color RAM for character.

Implementing Raster Interrupts

In order for the game Dan Dare to function properly within our emulator, we need to implement raster interrupts.

The basics of implementing raster interrupts within our emulator boils down to raising a flag when we hit a particular line number. So, we will start to implement this basic functionality within video.c:

...
jchar vic_interrupt = 0;
...
int raster_int_enabled() {
  return (memory_unclaimed_io_read(0xd01a) & 1) ? 1 : 0;
}


void video_line_expired(struct timer_struct *tdev) {
  tdev->remainingCycles = 63;
  processLine();
  line_count++;
  jchar RST_0_7 = memory_unclaimed_io_read(0xd012);
  jchar RST_8 = (memory_unclaimed_io_read(0xd011) & 0x80) << 1;
  int targetRasterLine = RST_8 | RST_0_7;
  if (line_count > 310) {
    line_count = 0;
    frameFinished = 1;
    posInBuffer = 0;
  }

    if ((targetRasterLine == line_count) && raster_int_enabled())
      vic_interrupt = vic_interrupt | 1 | 128;

}


We get the target raster line looking at location D012 and bit 7 of location D012. When an interrupt is triggered we set both bit 7 and bit 0 of vic_interrupt.

The next step, is to actually trigger an interrupt on our CPU with a raster interrupt. For this we first need to create a helper method within video.c:

int vic_raster_int_occured() {
  return (vic_interrupt > 128) ? 1 : 0;
}

We next need to invoke this method within cpu.c:

...
  void process_interrupts() {
    if (interruptFlag == 1)
      return;
    if ((trigger_irq() == 0) && (vic_raster_int_occured() == 0))
      return;
    pushWord(pc);
    breakFlag = 0;
    Push(getStatusFlagsAsByte());
    breakFlag = 1;
    interruptFlag = 1;
    int tempVal = memory_read(0xffff) * 256;
    tempVal = tempVal + memory_read(0xfffe);
    pc = tempVal;

  }
...

Next up, we need to interface memory.c with the raster interrupt functionality. For this, we need to add the following helper methods within video.c:

int read_vic_int_reg () {
  return vic_interrupt;
}

void write_vic_int_reg(jchar value) {
  value = ~value & 0x7f;
  vic_interrupt = vic_interrupt & value;
  if (vic_interrupt > 0)
    vic_interrupt = vic_interrupt | 128;
}

We have one method for reading the interrupt status register and another one for writing to it.

Writing to this register is provided for to clear/acknowledge interrupts.

Admitted, the way the VIC-II has implemented acknowledging interrupts is quite strange. In a CIA chip, for example, an interrupt is acknowledge simply by reading the Interrupt status register. In the VIC-II chip, however, you need to write a one to the interrupt bit in order to clear it.

I have implemented this interrupt acknowledgement mechanism with write_vic_int_reg. Basically I invert the value provided and then use the result to mask off the applicable bits.

Finally, we need to wire the above mentioned methods within memory.c:

...
jchar memory_read(int address) {
  if ((address >=0xa000) && (address < 0xc000) && basicROMEnabled())
    return basicROM[address & 0x1fff];
  else if ((address >=0xe000) && (address < 0x10000) && kernalROMEnabled())
    return kernalROM[address & 0x1fff];
  else if (address == 1)
    return read_port_1();
  else if ((address >=0xd000) && (address < 0xe000) && IOEnabled()) {
    if ((address >=0xdc00) && (address < 0xdc10))
      return cia1_read(address);
    else if (address == 0xd011) {
      int tempValue = IOUnclaimed[address & 0xfff] & 0x7f;
      tempValue = tempValue | ((line_count & 0x100) >> 1);
      return tempValue;
    }
    else if (address == 0xd012)
      return line_count & 0xff;
    else if (address == 0xd019)
      return read_vic_int_reg();
    else
      return IOUnclaimed[address & 0xfff];
  }
  else
    return mainMem[address];
}
...
void memory_write(int address, jchar value) {
  //if (((address >= 0xa000) && (address < 0xc000)) |
  //     ((address >= 0xe000) && (address < 0x10000)))
  //  return;

  if (address == 1)
    write_port_1(value);
  else if ((address >=0xd000) && (address < 0xe000) && IOEnabled()) {
    if((address >=0xdc00) & (address < 0xdc10))
      cia1_write(address, value);
    else if (address == 0xd019)
      write_vic_int_reg(value);
    else
      IOUnclaimed[address & 0xfff] = value;
  }
  else
    mainMem[address] = value;
}
...

A Test Run

With everything implemented, I have taken a couple of screenshots:





As you can, our game characters are still invisible. That is because they are sprites, which is functionality which we haven't implemented yet.

In Summary

In this post we have implemented the remaining Graphics modes required in order to render the game Dan Dare.

We have also implemented raster interrupt emulation and the VIC memory model.

In the next post we will be implementing sprite emulation.

Till next time!


No comments:

Post a Comment