Let’s Have Fun With Interpreters and Bytecode VMs — Chapter 3

Portability.

Instead of compiling source code for distribution, we compile interpreters.

One interpreter compiled for specific hardware runs any source-code without the end user or programmer having to worry much at all.

Virtual MachineOur compiler engineers have thus far devised two ways of running the same program: one in which source code is compiled down into machine code for some specified hardware and another where some source is the machine code for some specified software.

The software in this sense is a computer in of itself, performing operations defined in the source by calling subroutines.

But as we’ve already experienced first hand, we can take this a step further.

Now that we have an interpreter that can run any high-level source on a target hardware, we can imagine an intermediate representation.

What if we were to devise something more compact when compared to the high-level source, but still more abstract than when compared to machine code.

We’ve now arrived now at Byte Code.

When an interpreter is consuming this intermediate representation, we typically refer to this as a Virtual Machine.

The virtual machine takes the best of both worlds: we approach similar levels of efficiency and speed that we get from machine code, with the portability we get from interpreted languages.

The Chip8So — with all of this in mind — what is it exactly that we are creating when we create a Chip8 emulator?In short, a Virtual Machine that interprets bytecode defined as the Chip8 Programming Language.

This means that there is no higher-level source code to compile down to bytecode, the bytecode itself is the abstraction.

Do note, however, that this is not to say there can be no higher-level source code.

Just that none is defined.

Our task is to write some software that takes the Chip8 bytecode and produces expected results — or a Chip8 bytecode interpreter.

But at the same time we’ll need to simulate the environment in which the Chip8 bytecode expects to be run, which is defined in the specifications.

This includes memory, registers, input, display and sound.

Let’s start out with a demo today:Controls:q – move leftw – firee – move rightesc – resetp – pause / resume program, show assembly infospace – (while paused) step through program.

MemoryThe Chip8 VM has commands 4,096 bytes of addressable memory.

Of which, the first 512 bytes are reserved for the interpreter.

The remaining 3,583 bytes is considered valid program space.

Additionally, the Chip8 has access to:A 16-bit Program CounterAn 8-bit Stack PointerA 32-byte stack ( sixteen 2-byte values)Sixteen 8-bit registersTwo 8-bit timers (sound, delay)A 16-bit index registerA 256-byte frame bufferIn order to implement a Chip8 VM to specification, we will need to account for all of these properties in a semi accurate way.

A note on accuracy: since we’re implementing this in Javascript, there’s a limit to how close we can get to this specification.

We can take advantage of typed arrays and create data structures to represent these ideas in spirit, but we are ultimately at the mercy of the Javascript VM.

For instance, Javascript is dynamically typed — the variables we use are not directly associated with the values they represent.

There’s also only one type of number: double-precision 64-bit binary.

We’re about as far away from the metal as we can get.

Overview of partsSo what do all of these things do anyway?We will be creating a very rudimentary implementation of the fetch-decode-execute cycle, of which the above parts are closely related.

The program counter contains the the address of the instruction that will be executed next.

The index register contains the address of some block of memory (for drawing sprites, for instance).

The stack is an array of sixteen 2-byte values and contains addresses for the interpreter to refer to.

This is used for subroutines.

The stack pointer determines the current level of the stack.

Registers 0 — E are general purpose 8-bit storage registers, used for storing values immediately available to the CPU.

Register F is special in that it is specifically used as a flag for some instructions, for determining borrows or collision for example.

The frame buffer directly translates to a 64×32 display where each bit represents 1 monochrome pixel.

�????????�Excitement IntensifiesIn addition to the above: there are two flags: draw and sound.

When set, the chip signals that it’s time to draw or make a sound.

With all of that info, we are READY to start our Chip8.

An imagining of the fetch-decode-execute cycleWhat about the opcodes?Remember before, when our second clever engineer said,“I can write a subroutine for each instruction and then call it whenever I encounter the corresponding code in the source.

”Well, this is our job.

For each opcode, we need to implement a method.

There are many ways to do this.

For example, because there’s so few opcodes for the Chip8, we could organize them under one giant switch statement.

But I would discourage you from doing so.

Instead, a cleaner technique involves (loosely) organizing the methods under their parent operations (math, display, flow etc.

).

We may then associate them through either an array, or (for maximum ease) an object.

The method names can either be a direct corollary to their opcode (mvi, draw etc…) or more descriptive (e.

g, jumpToSubroutine).

I use both depending on the operation, here’s a cohesive example:We do this for every single opcode.

The draw methodProbably the number one pain point for Chip8 implementations is the draw method so let’s actually dig into it.

DXYN — DRAW Register X, Register Y, N When the interpreter encounters the Draw subroutine, it will read the values stored in Register X and Register Y and draw a sprite to the screen at that location with N-height.

Recall that: The index register contains the address of some block of memory.

Index Register to Index Register + N defines the sprite we want to draw.

Each byte in memory in this range is a ROW, Each bit in every byte is a COLUMN.

That means all sprites are 8 columns wide, and can be at most 16 rows high.

Let’s take a look at our fontset:0xF0, 0x90, 0x90, 0x90, 0xF0, // 00×20, 0x60, 0x20, 0x20, 0x70, // 10xF0, 0x10, 0xF0, 0x80, 0xF0, // 20xF0, 0x10, 0xF0, 0x10, 0xF0, // 30×90, 0x90, 0xF0, 0x10, 0x10, // 40xF0, 0x80, 0xF0, 0x10, 0xF0, // 50xF0, 0x80, 0xF0, 0x90, 0xF0, // 60xF0, 0x10, 0x20, 0x40, 0x40, // 70xF0, 0x90, 0xF0, 0x90, 0xF0, // 80xF0, 0x90, 0xF0, 0x10, 0xF0, // 90xF0, 0x90, 0xF0, 0x90, 0x90, // A0xE0, 0x90, 0xE0, 0x90, 0xE0, // B0xF0, 0x80, 0x80, 0x80, 0xF0, // C0xE0, 0x90, 0x90, 0x90, 0xE0, // D0xF0, 0x80, 0xF0, 0x80, 0xF0, // E0xF0, 0x80, 0xF0, 0x80, 0x80 // FA font sprite is 5-bytes long, meaning 5 a character is rows high.

Fonts, like all sprites are 8 columns wide.

Let’s visualize a couple through their binary representation://"0"11110000 //0xF010010000 //0x90 10010000 //0x9010010000 //0x9011110000 //0xF0//"E"11110000 //0xF010000000 //0x8011110000 //0xF010000000 //0x8011110000 //0xF0Here’s two helpful equations for working with 1d / 2d arrays://x,y to indexi = x + width*y; //index to x, yx = i % width; y = floor(i / width);And here’s what an outline of the draw method might look like:Note the wrapping moduloFinally, and importantly: if a pixel is going from 1 to 0, set Register F to 1 otherwise set it to zero.

This is how we will perform collision detection.

Pro tip: take this slow, reference multiple sources (there’s plenty out there).

Write unit tests for each opcode method.

Though writing unit tests for each method might seem gruelling, it will force you to pay attention to the rules you’re implementing.

You will almost certainly not implement every opcode correctly on your first go.

When the inevitable bugs do arise, look at your unit tests and compare them to the specification.

With this knowledge, I send you on your way to write the Chip8 Emulator in full.

Wait… that’s it?!This is, essentially, all we need to know.

We’ve diagrammed and dug into the specifications, talked about the theory and the instruction set, talked through some example opcodes.

Now it’s just time to build it.

I’ve intentionally left some information out (such as cycle timing and peripheral specifications).

This is a learning process, the more you can do through discovery, the better you will learn for yourself.

I implore you to get through as much of it on your own as you can, but feel free to cross reference this repo here.

Up NextNow that we have a working disassembler and fully functioning emulator, the next logical step is be to craft an assembler.

We will begin by discussing the very basics of lexing and parsing, move on to assembling the disassembly we already have and discuss strategies to represent variables in a more rich form of disassembly.

At the end of next chapter, we’ll write our very own Chip8 Program.

.. More details

Leave a Reply