Advertisements
Chip8 emulator running Space Invaders

Write a Chip8 retro gaming emulator in one day. Controller and Display.

Welcome to a new article on how to write your own Chip8 emulator. After introducing the Chip8 platform and loading the first ROM the time has come to do some serious stuff. I need my peripherals to work before playing some retro games. I am going to create the game controller for receiving key presses and the video RAM where sprites are drawn.

Chip8 emulator running Space Invaders
See the complete code on GitHub

The controller

The controller is the easiest part so I am going to start from there. The first thing to do is figure out what I need from it. The original controller is a small keyboard with 16 keys, this is the layout

  1 | 2 | 3 | C
  --+---+---+--
  4 | 5 | 6 | D
  --+---+---+--
  7 | 8 | 9 | E
  --+---+---+--
  A | 0 | B | F

Some emulators out there provide a way to map the keys in a way that is convenient on a modern PC keyboard. I decided not to do this by default. The game will often display instructions on which keys to use and I want to be aligned with the instructions. However the ability to remap the keys is still a nice feature to have, the original keys are all scattered around the modern keyboard and some games might be really hard to play. I want to create something that is flexible enough, basically I want to avoid hardcoding the keys into the code and instead provide some sort of customizable map.

I need a new class, the Controller. This has to provide at least an Array of 16 Booleans, one for each key. True in one location means that the user pressed the corresponding key.


val keys = Array.ofDim[Boolean](16)

Next thing I need is the key map, this will map a character to a location in the keys array. The idea is that when the user presses one key I will look into this table to figure out which location in the keys array should be set to true. This is the structure to change in order to remap the keys. By default however, I will keep the original structure


val keyMap = Map(
  '0' -> 0,
  '1' -> 1,
  '2' -> 2,
  '3' -> 3,
  '4' -> 4,
  '5' -> 5,
  '6' -> 6,
  '7' -> 7,
  '8' -> 8,
  '9' -> 9,
  'a' -> 10,
  'b' -> 11,
  'c' -> 12,
  'd' -> 13,
  'e' -> 14,
  'f' -> 15,
)

According to the Chip8 documentation there are three opcodes that deal with the controller. Two conditional opcodes and one wait. I should model my Controller class in such a way that will make it easy to implement the opcodes later on.

Ex9E - SKP Vx
Skip next instruction if key with the value of Vx is pressed.

ExA1 - SKNP Vx
Skip next instruction if key with the value of Vx is not pressed.

Fx0A - LD Vx, K
Wait for a key press, store the value of the key in Vx.

I need one method that checks the status of a specific key and returns true or false.


def isKeyPressed(key: Int): Boolean = {
  keys(key)
}

For the third opcode I am going to need two methods. The first method checks whether or not any key is pressed and another one that returns its value.


def isAnyKeyPressed() : Boolean = synchronized {
  keys.fold(false)(_ | _)
}
  
def getKeyPressed() : Int = {
  keys.indexWhere(_ == true)
}

I went a bit functional on these two.

The fold() method is a functional construct, it performs an operation between an accumulator value and every item in the keys array and then returns a value. In this case fold performs a logical or between an accumulator and every item in the list. The accumulator’s initial value is false. Every item in the list gets or’d with the result of the previous operation, or false for the first item. It’s just a very fancy way to check if any of the keys is pressed. Learn more about fold here.
The indexWhere() applies the lambda I provided (_ == true) to every item in the list and returns the first position where the lambda evaluates to true.

I have what I need to implement the Chip8 keypad opcodes later on. What I do not have is a way to interface with the actual keyboard. I want to use a Window for the Display, the window will receive keys pressed by the user and inform the Controller. For now all I have to do is add a method inside Controller that receives a key press/release. This method receives a character, remaps it to the correct key using the keyMap, and then sets the key to true or false.


def setKeyPressed(char: Char, pressed: Boolean): Unit = synchronized {
  try {
    keys(keyMap(char)) = pressed
  } catch {
    case _: NoSuchElementException => {}
  }
}

It is important that this method is thread safe, see the synchronized keyword. This is equivalent to Java’s synchronized. I have to be sure it is thread safe because the Display and the Cpu will both access the Controller from different threads. The Display will have it’s own thread to render the screen.

The video memory

Before creating a display I need a video memory. This is where the CPU will put the sprites. The idea is the following:

  1. The cpu fetches the opcode that draws a sprite.
  2. During execution of the opcode the cpu copies the sprite in video memory.
  3. The display uses a thread to refresh the screen, the thread reads from video memory 60 times per seconds and draws whatever it finds there onto the screen.

I need to update my Memory class to implement some sort of video RAM support. First thing to do is to define some constants like the size of the video ram and the size of sprites. The Chip8 screen is 64×32 pixels in size, and sprites are always 8 pixels wide.


object Memory {
  val RAM_SIZE = 4096
  val PROGRAM_START = 0x200.toShort
  val VIDEO_WIDTH = 64
  val VIDEO_HEIGHT = 32
  val SPRITE_WIDTH = 8
}

Now I need the video memory, the easiest way to implement it is an Array of Booleans. Each Boolean corresponds to one pixel on the screen. This is far from the optimal solution in terms of memory efficiency since I use a whole Boolean object for every pixel. But who cares? This approach will make it easier to read the video ram and draw the screen.


val video = Array.ofDim[Boolean](Memory.VIDEO_WIDTH * Memory.VIDEO_HEIGHT)

The next step is to create a proper interface that the Cpu will use to draw stuff. For this I’ll have another look at the Chip8 opcodes. There are only two opcodes that deal with the display.

00E0 - CLS
Clear the display.

Dxyn - DRW Vx, Vy, nibble
Display n-byte sprite starting at memory location I at (Vx, Vy), set VF = collision.

The first opcode clears the screen, so I need a method for that. It needs to be synchronized, any access to video ram has to be thread safe.


def clearVideo() : Unit = synchronized {
  for(i <- video.indices) {
    video(i) = false
  }
}

The second opcode draws a sprite on the screen, now this is interesting.

Drawing sprites

The method to draw a sprite receives the address in regular RAM where the sprite data is stored, the coordinates, and the height of the sprite in pixels. One important thing to remember is that the sprite has to wrap around the edge of the screen. This means that drawing a sprite at row 0 and column 31 will draw 1 pixel at the last column of the screen and 7 pixels starting at column 0, row 0. The same applies to rows. Another important thing to do here is check for collisions. The method will return true if a collision was detected while drawing the sprite.

The Chip8 “graphic pipeline” is as follows

  1. Figure out the coordinates of the pixel to be drawn.
  2. Retrieve the current value of the pixel at those coordinates.
  3. If the current value and the new value are both true, set the collision flag.
  4. Xor the current value of the pixel with the new value and update the pixel with the result.

This is the method I need to draw a sprite, notice the wrap around logic and the collision detection.


def showSprite(height:Int, fromAddress: Int, xCoord: Byte, yCoord: Byte) : Boolean = synchronized {
  var collisions = 0
  var sourceAddress = fromAddress

  for (y <- yCoord until yCoord + height) {
    val positiveY:Byte = y % Memory.VIDEO_HEIGHT

    for (x <- xCoord until xCoord + 8) { 
      val positiveX:Byte = x % Memory.VIDEO_WIDTH 
      val locationInVideoMemory:Short = (positiveY * Memory.VIDEO_WIDTH) + positiveX
      val currentRow = ramReadByte(sourceAddress)
      val currentPx = x - xCoord
      val mask:Byte = 0x80 >> currentPx
      val pixelShouldBeOn = (currentRow & mask) != 0

      val current = video(locationInVideoMemory)
      if (current && pixelShouldBeOn) collisions += 1
      video(locationInVideoMemory) ^= pixelShouldBeOn
    }

    sourceAddress += 1
  }

  collisions > 0
}

This method translates Bytes coming from RAM to Booleans for the video memory, a Byte value 10000000 means one row of pixels, the first one is on and all the others are off. A currentRow Byte coming from RAM is interpreted as a row of pixels with the first pixel in the row being the most significant bit. To translate this I use a mask, the initial value of the mask is 0x80, or 10000000 in binary. I perform and AND between the mask and the currentRow to figure out whether or not the first pixel has to be on or off. For the next pixels I keep shifting the mask one bit to the right and do the same, for the whole row of 8 pixels. After each row I increment the address in RAM in order to read the next byte. The process repeats for all the rows.

The display

I originally intended to show how the display draws graphics on the screen, however I decided to skip that part. This post is already quite large and after all the Display class is just a swing window that:

  • Uses a Thread to draw squares onto a JPanel, one square, one pixel.
  • Receives input events from the keyboard and informs the Controller about it using the methods defined above.

See the class on GitHub for all the details.
As I mentioned earlier all the rendering happens inside a dedicated thread. The thread starts as soon as the window opens up and terminates when the user clicks the x button on the window. The rendering thread keeps looping at a frequency of approximately 60 Hz and draws the entire video RAM onto the window.
With this approach I can decouple cpu execution and drawing so the time required to execute cpu instruction does not affect the refresh rate of 60 Hz of the screen.

We are getting there

All the basic blocks are ready. The last thing to do is to implement the Chip8 CPU and we will be able to see and play some games. Stay tuned, it’s coming!

Unique opportunity! Help a fellow grow his blog!

Hi there! If you’ve read this far maybe you think this was useful, or fun, or I don’t know what but for some reason You Got Here! Great! Please consider sharing this post with your network, I am trying to get The Code Butchery to grow so I can provide more content like this, will you help me in my journey? Thank you!

Series Navigation<< Write a Chip8 retro gaming emulator in one day. Loading the first ROM.Write a Chip8 retro gaming emulator in one day. The CPU. And it’s done. >>
Advertisements
Share this

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.