Coding into the Void

Coding into the Void

A blog that I’ll probably forget about after making thirty-one posts.

Yet Another Chip-8 Emulator: XO-Chip

XO-Chip is an extension of CHIP-8 written by John Earnest,1 who also made Octo, the foremost CHIP-8 (and Super-Chip and XO-CHIP) program writing tool. You might notice that this one is published considerably later than the CHIP-8 and Super-Chip posts.

That’s primarily because the audio and pitch instructions were giving me enough trouble that I ended up working on the display context (an arcade machine) to delay finishing the (mostly working) emulator.2

The XO-Chip specification is very detailed, and quite helpful, but, I assume partly because not many people have implemented an XO-Chip emulator, ambiguous in some critical cases.

Additionally, the way the Octo emulator is written mashes up CHIP-8, Super-Chip, and XO-Chip, so occasionally it’s difficult to tell which part goes to which. This meant that in some cases I needed to do a good deal of code archeology to figure out what the correct behavior was.

Yes, XO-Chip is also a superset of Super-Chip. It’s somewhat vague on that front, but there are some lines later in that document that give the game away, and, really, the presence of a scroll up command but not any of the others should make it obvious.

XO-Chip (version 1.13) adds 7 new instructions, modifies the behavior of 6 Super-Chip instructions, and modifies the behavior of 8 CHIP-8 instructions. This information is all in there, although often it’s written in Octo language terms instead of instruction terms.

Similar to how I did the Super-Chip instruction set, my RunOpcode method is a waterfall. It tries XO-Chip, then Super-Chip, then CHIP-8, stopping if one successfully handles the instruction. This allows me to have XO-Chip modifications of CHIP-8 instructions without putting XO-Chip concepts into the CHIP-8 code.

Less efficient and there’s some repetition, but I like the cleanliness it brings.

Since I’ve been attempting to flesh out the missing information with the previous two posts, I’ll try to make the changes as explicit as possible here.

Notably, XO-Chip changes the instructions from being fixed length to variable length, which in and of itself requires changes to several CHIP-8 behaviors.

Instruction Additions & Changes

XO-Chip

Save Vx–Vy 5xy2

This instruction saves the contents of Vx through Vy to memory, starting at i. The instruction doesn’t change i, unlike Fx55.4 If Vx is higher than Vy, it will write to memory in reverse order.

Load Vx–Vy 5xy3

This instruction loads the contents of Vx through Vy from memory, starting at i. The instruction doesn’t change i, unlike Fx65. If Vx is higher than Vy, it will write to memory in reverse order.

Load Long F000 nnnn

Yep, the first instruction that’s not two bytes. This bad boy is single-handedly responsible for making us change six CHIP-8 instructions.

While it causes problems for other instructions, it’s easy to handle by itself. It behaves almost identically to 1nnn, but you just read the next two bytes in memory to set i to.

Plane n Fn01

Sets the plane value to n. The documentation on this is quite good, but just note that it applies to all graphical operations: scroll, draw, and clear. It’s responsible for the remaining two CHIP-8 instruction changes and four of the six Super-Chip changes.

Also, the Octo emulator doesn’t handle it like this, but I pack all the drawing planes into the existing graphics buffer. It’s a byte in size and previously was holding only a bit of data. Even if this gets extended to 4 individual planes (the most this instruction could handle), it’ll have more than enough space to hold it.

Audio F002

This one seems to be as simple as writing the bytes to the PCM buffer. Unfortunately, that seems complicated to do in Unity, and I couldn’t figure out how to do that. For that reason,5 I’m surrendering on this opcode. If I ever figure it out, I’ll write the post.

Pitch Fx3A

Because I didn’t do the above instruction, there’s not much point in doing this one either. They both are currently unimplemented.

Scroll up n 00Dn

Scrolls up the screen by n pixels. Like the Super-Chip versions, pixels replacing the bottom pixels are empty. Note that this instruction takes into account the drawing plane, so one layer can be scrolled without touching the other.

Super-Chip

The XO-Chip specification lists Fx75 and Fx85 as new commands, but they’re just modifications of the existing Super-Chip ones. That was one of the details that confused me as to whether it was a superset of Super-Chip at first.

Scroll down n 00Cn

Same behavior, but modified to only affect the selected drawing planes.

Scroll right 4 pixels 00FB

Same behavior, but modified to only affect the selected drawing planes.

Scroll left 4 pixels 00FC

Same behavior, but modified to only affect the selected drawing planes.

Draws a 16x16 sprite Dxy0

Same behavior, but modified to only affect the selected drawing planes. This makes the collision logic a little more complicated if you pack the layers into one byte like I did, but it’s not too bad.

Save Cross-Program Registers Fx75

Saves to cross-program registers.6 The number of these registers has been increased from 8 to 16, which means you don’t need to guard against out of bound references.

Load Cross-Program Registers Fx85

Load from cross-program registers. The number of these registers has been increased from 8 to 16, which means you don’t need to guard against out of bound references.

CHIP-8

Clear Screen 00E0

Only the current planes are cleared.

Skip if Vx == kk 3xkk

If the next opcode is F000, increment the program counter by 4 instead of 2.

Skip if Vx != kk 4xkk

If the next opcode is F000, increment the program counter by 4 instead of 2.

Skip if Vx == Vy 5xy0

If the next opcode is F000, increment the program counter by 4 instead of 2.

Skip if Vx != Vy 9xy0

If the next opcode is F000, increment the program counter by 4 instead of 2.

Draws from Memory Dxyn

Same behavior, but modified to only affect the selected drawing planes. This makes the collision logic a little more complicated if you pack the layers into one byte like I did, but it’s not too bad.

Skip if input[Vx] Ex9E

If the next opcode is F000, increment the program counter by 4 instead of 2.

Skip if !input[Vx] ExA1

If the next opcode is F000, increment the program counter by 4 instead of 2.

Memory Changes

  • Memory increased from 0x1000 to 0x10000. This one’s not in the spec, but is in the emulator behind the enableXO flag.7
  • Cross-program registers increases to 16.
  • 16 bytes for audio memory.
  • One byte for plane.
  • One byte for pitch.

Pitfalls

Not Writing Tests

I was running into an arcane issue with ROMs not being rendered correctly. The program I was using to test, skyward, would only render the first line over and over before running into unrecognized instructions. I wrote tests and found the following bugs:

  • CHIP-8: Input key 0xF (V) would always return 0.8
  • SuperChip: Loading/saving to persistent registers would always do one less than intended.
  • XO-Chip: Loading/saving using 5xy2 and 5xy3 would always do one less than intended.
  • XO-Chip: Forgot to replace y– in a for loop with y++, causing it to attempt to read outside of memory.9
  • XO-Chip: My plane-aware set pixel method wasn’t actually setting the pixel properly.10 (This is the issue that broke skyward)
  • XO-Chip: Dxy0 was using FlipPixel() instead of FlipPixelXO().
  • XO-Chip: When drawing multiple planes, all would access the same texture.11

Something to note is that 3/7 bugs that I found were off by one errors. Guess it’s true what they say about it being one of the most common bugs.

Plane Management

When writing to multiple planes at once, higher planes read from a memory space after the earlier planes. It’s tempting to just loop across the planes and base the offset of the plane, but this is incorrect. You need to keep track of how many planes you’ve already written, not the current plane you’re writing.

Conclusion

This is another one that lacks a solid test ROM, which isn’t surprising. The greater complexity of the average game also means that it’s more complicated to track down the bugs that appear. I’m glad I went down this rabbit hole, but it’s making me even more glad I didn’t try to make a GameBoy emulator. I’m a bit disappointed I didn’t manage to implement all the XO-Chip instructions, but that’s life.

Thanks to everyone who created, documented, and programmed in these languages. I had a great time doing this. Hopefully I’ll find the time to join an Octojam so I can experience CHIP-8 and its expansions from the other side.


  1. It’s also an extension of Super-Chip, but I’ll get to that. ↩︎

  2. The program T8NKS also kindly surfaced multiple issues with my CHIP-8 input code, like two different instances of the emulator not waiting for a new frame when the “wait for input” instruction is run, and it checking for a key press instead of a key release, as mentioned here. I figured this out by slowly whittling down T8NKS into a test rom that reproduced the behavior. ↩︎

  3. Version 1.1 added the pitch instruction, and extended the persistent registers to 16. ↩︎

  4. Same as Fx55 in quirks mode. I won’t be mentioning them, as XO-Chip doesn’t introduce any quirks. ↩︎

  5. Also because it’s currently May 3rd and I last touched this code back on February 4th, and, if my past behavior is any indication, I likely won’t come back this year. ↩︎

  6. Referred to as flag registers by the docs. ↩︎

  7. The enableXO flag also starts the graphics size at 128x64, but doesn’t set the hires flag, so I’m not sure what the purpose is, since it makes it behave as if it’s set to 64x32 anyway. ↩︎

  8. No one cares about V, I guess. None of the programs I tested with used it. It just so happens that the last input is the bottom right key. ↩︎

  9. Amazingly, none of the programs I tested with used scroll up, as it would have been immediately obvious how it failed. Maybe Erik had the right idea when he didn’t add the command. ↩︎

  10. It was using some complex bitwise logic to set the graphics value to… itself. I’m actually not sure why any graphics were drawing. ↩︎

  11. I was using n to determine the sprite width, but the command is Dxy0, so n is always 0. ↩︎