The reason I started this project is to learn more about C++, as we all know the best way of learning a programming language is to do projects, DO PROJECTS!!
I used Austin Morlan's website to learn how to build it, it's quite good (https://austinmorlan.com/posts/chip8_emulator/). I made some tweaks which I found to be better for me. I will not be posting the whole codebase here, it's too long. What I will be sharing are snippets of code, what I learned from it, and what I found amazing or funny (projects can have their own jokes).
What is an Emulator ?
An emulator is just hardware or software that lets the host system replicate conditions like the CPU, memory systems, clock cycles, etc., of the guest system whose functions/behaviour they want to simulate. It helps to bridge the architectural gap by making sure that each instruction code can be executed. In the case of Chip8, we have to simulate the hardware restrictions of the 1970s: a 64x32 screen, a 16-key keypad, timers, and a buzz sound.
If you google Chip8, you will see that it is not actually a real physical device. It is a virtual machine/interpreter where you can interpret games (that was the intended purpose), like Pong or Space Invaders. It was a virtual language created in 1977 AD for a computer called COSMAC VIP.
Building in C++
I wanted to get familiar with C++, that's why I am here. Building a Chip8 emulator in C++. Well, I learned you need headers, classes to define objects, the standard library, built-in objects like std::ifstream, std::streampos, and so on. I will explain some parts that left a mark in my memory.
Header Files
Well, before C++, I had only used a header file for an FPGA (Tang Nano 9K) project which I did. It made the LED blink in intervals. But now I understand more, such as how we create a blueprint of the class which we will be using to create objects in the future. Two modes:
Public:
The attributes and methods of the said class can be accessed by other functions or parts of the program that are not in the same class. It can be accessed from inside and outside the class. Mostly used for accessibility and ease of use.
Private:
The term is called Encapsulation, where the scope is just within the class. Nobody from outside the class can access and change data from the attributes and methods. Well, it is good if you don't want some other part of the program to change things by mistake.
Here, in my header, I put the important attributes like memory, program counter, etc., in private so that only the objects of the Chip8 class can access it.
class Chip8
{
public:
Chip8();
void LoadROM(char const* filename);
private:
uint8_t registers[16]{};
uint8_t memory[4096]{};
uint16_t index{};
uint_16_t pc{};
uint16_t stack[16]{};
uint8_t sp{};
uint8_t delayTimer{};
uint8_t keypad[16]{};
uint32_t video[64 * 32]{};
uint16_t opcode;
};
If you are wondering what is that block of code which is public? well it is a constructor. Lets talk about it.
Constructors in C++
1 Chip8::Chip8()
2 //vs
3 void Chip8::LoadROM()
4 //vs
5 std::ifstream()
Constructors never have return types like void or int; their names must match the class, like in the above example Chip8::Chip8. Here, line 1 represents a default constructor which has no values inside the parentheses. In line 3, void Chip8::LoadROM(), we just created a method/function/action for the Chip8 class to perform when invoked. Line 5 is something special: std is a built-in prefix that tells the compiler to look for functions, objects, or variables inside the built-in library of C++. It is like a built-in tool that C++ provides. Like ifstream here, which is an object from std that reads data from files on the computer instead of the console.
I also wanna talk about "::", the scope specifier (the beauty of C++ they say), which tells the compiler, in our example "std::ifstream", that ifstream belongs to the standard library. Everything is from this library; hence, no clashes in the future with names or data assignments.
Member Initializer List
Now look at this
Chip8::Chip8()
: randGen(std::chrono::system_clock::now().time_since_epoch().count())
{.....
}
Do you see "::" then ":"? What is the difference? If one is a scope specifier, the other is a Member Initializer List. It helps the constructor to build space and get that particular variable ready to work, without wasting time by creating it again inside the constructor during the initialization process for the object. We can add more to the initialization list by just separating them with comma. You can look at the code below:
Chip8::Chip8()
: randGen(std::chrono::system_clock::now().time_since_epoch().count()),
pc(0x200),
delayTimer(0)
{}
Infinite Wait
I also learned the way to wait, when u want a key value pressed, but is not pressed then we just loop by decreasing the PC by 2 (2 because each instruction is 16 byte for Chip8), which gives the illusion of holding still, but its just same instruction on loop. I dont know about you guys, but to me when I undrstood, that i think my mind just unlocked a different way of looking at what wait is? what pause it.
Reading Instructions
The code below is one of the instructions, which has its own function. There is alot to unpack.
1 void Chip8::OP_8xyE() //Set Vx = Vx SHL 1
2 {
3 uint8_t Vx = (opcode & 0x0F00u) >> 8u;
4
5 // Save MSB in VF
6 registers[0xF] = (registers[Vx] & 0x80u) >> 7u;
7
8 registers[Vx] <<= 1;
9 }
Here, I wrote a function of class Chip8 which returns nothing, hence void. The instruction here is 8xyE. I read that the naming convention for the Chip-8 opcode was restricted by memory space, hence you are going to find a few things out of order which will not make sense (Go take a look to see what I mean).
Let's look at line 3. Here, the uint8_t datatype means unsigned integer of 8 bits. What's the difference between this and a normal unsigned integer? A standard int is usually 4 bytes (32 bits), while this is strictly 1 byte (8 bits). And no matter which machine architecture you are using, it will always be exactly 8 bits.
(opcode & 0x0F00u) — is this line not something that you would expect to see on a hardcore programmer's screen in a movie or something? Here, a Bitwise AND is happening. In a sense, this operation will combine two streams of binary data and unify it by doing an AND switch. Like 101 AND 100 = 100. Just saying, if you study computers, you know the AND operation!
So, how does the AND logic help us here? Think of F as a mask. In hexadecimal, F is 1111 in binary (all ones), and 0 is 0000 (all zeros). When we AND the opcode (8xyE) with the mask (0x0F00), the zeros wipe out the 8, the y, and the E. Only the digit that is placed at the same position as the F survives. So, 0x8xyE AND 0x0F00 leaves us with exactly 0x0x00. The u at the end is just saying the 0x0F00 is an unsigned integer.
Next, >> is another bitwise operator which shifts the value 8 bits to the right. Because each hex character is 4 bits long, shifting right by 8 bits pushes the value over by exactly two hex spaces. It takes our 0x0x00 and slides it right to become 0x000x. We successfully extracted x!
Wait, let's clear something up that confused me at first. In the opcode 8xyE, you might be wondering: "I only have 16 registers (0 to F), so how can I reference 'x' which doesn't even exist?"
Here is the trick: 8xyE is just a template. The x and y are placeholders. In a real game, the emulator doesn't see 8xyE; it reads an actual hex number like 835E. The 8 and the E tell our emulator which operation to perform. The x becomes 3, and the y becomes 5. Since x is a single hex digit, it will always be a number from 0 to F (0 to 15 in decimal), which perfectly matches our 16 registers!
So, back in line 3: uint8_t Vx = (opcode & 0x0F00u) >> 8u;
This line doesn't give us the value of the register. It extracts that x (like the 3) to give us the box number (the index). It tells the program, "We need to look inside register 3." The actual value sitting inside that register was put there by some previous instruction earlier in the game.
Now, to line 6. The Chip-8 has 16 registers (0-F), and here we are assigning a value to register F (registers[0xF]). I did a bitwise AND to the register value Vx (which we got from line 3) with 0x80u and shifted it right by 7 (>> 7u). But why??
Let me explain. In line 8, we are going to shift the entire Vx register to the left by 1. But when you shift binary numbers left, the leftmost bit (the Most Significant Bit, or MSB) falls off the edge and gets lost. The Chip-8 instruction manual tells us we have to catch that falling bit and save it in the VF register (registers[0xF]) as a flag.
Here is the magic: 0x80 in binary is 1000 0000. When we use the AND operator (&) against our register, it acts like a mask that zeros out all the bits except for that very first one on the far left. So, we are left with either 1000 0000 (if the bit was a 1) or 0000 0000 (if the bit was a 0).
But we don't want to store 1000 0000; we just want a simple 1 or 0. That is where >> 7u comes in! Shifting it right by 7 spaces takes that bit from the far left and slides it all the way to the far right, turning 1000 0000 into 0000 0001. Boom, we just saved our MSB!
Finally, we get to line 8:
registers[Vx] <<= 1;
Now that we saved our falling MSB in the flag register (0xF), we can safely do what this instruction was actually designed to do. We go into registersVx, grab the data sitting inside, and use <<= 1 to shift all its bits to the left by 1. Fun fact: in binary, shifting left by 1 is the exact same thing as multiplying the number by 2!
register[Vx] << = 1
//same as
register[Vx] = register[Vx] * 2
Function Pointer Tables
I remember when I was a kid, I used to go to McDonald's with my dad and he would ask me, "Which number do you want?" I always said Number 3, as it was a McSpicy with fries and a drink. This reminds me of the exact same idea. Instead of saying "I want a McSpicy, fries, and a drink," you just say "3" and they know exactly what it is.
Similarly, here, a function pointer table is just an array of pointers that point to functions.
Conditional statements like switch and if are used to find which line of code to execute according to an event. But with an array of pointers, we just provide a number and point straight to a specific location. Done.
This helps us reduce the code size and gives us O(1) time complexity. Because we don't need to search or compare, the computer just takes in the index, multiplies it by the pointer size, and jumps straight to the exact memory address! (Such a smart idea)
Score Board (Keep Count)
void CHip8::OP_Fx33()
{
uint8_Vx = (opcode & 0x0F00u) >> 8u;
uint8_t value = registers[Vx];
//Ones
memory[index + 2] = value % 10;
value /= 10;
//Tens
memory[index +1] = value % 10;
value /= 10;
//hundreds
memory[index] = value % 10;
}
This instruction, Fx33, is used for something called Binary-Coded Decimal (BCD). Computers think in binary and hex, but what if a game like Pong needs to display a score of "156" on the screen to the human player? The game needs to draw the sprite for "1", then "5", then "6". It needs a way to break that single number apart into individual digits.
The code above does this using a super clever math trick with the Modulo (%) and Division (/) operators, working backwards from right to left!
Let's pretend our value is 156.
First, we do 156 % 10. Modulo gives us the remainder of division, which is exactly 6. We store that 6 in memory as our "ones" place. Next, we do value /= 10. Because this is integer math, it doesn't give us decimals; it just chops the 6 right off the end, leaving us with 15.
Then we just rinse and repeat!
15 % 10 leaves us with 5 (our tens place). Chop it off (15 / 10) and we are left with 1.
Finally, 1 % 10 leaves us with 1 (our hundreds place).
We just successfully peeled apart the number digit by digit and stored them in memory so the screen can draw them later.
Saving Game
The instruction Fx55 is used to save the game.
1 void Chip8::OP_Fx55()
2 {
3 uint8_t Vx = (opcode & 0x0F00u) >> 8u;
4
5 for (uint8_t i = 0; i <= Vx; ++i)
6 {
7 memory[index + i] = registers[i];
8 }
9 }
Here, we did the usual: a bitwise AND (&), followed by bit shifting 8 bits to the right (>>), and assigned that 1 byte of data to our 8-bit unsigned integer variable named Vx.
After that, we created a for loop where we initialized an unsigned integer uint8_t i = 0. I was wondering why it had to be uint8_t, but it makes sense because our registers use the same data type. We only need to count up to 15 anyway, so it's the most memory-efficient choice. We loop from 0 up to that specific "x" register index.
Also, a thing to note: ++i is better than i++. When we do ++i (pre-increment), we don't have to create a temporary clone of i. With i++ (post-increment), C++ creates a copy of the variable after the first loop, does the arithmetic to add 1, and returns it. But ++i just adds 1 directly while the condition itself is being evaluated. It is preemptive and slightly faster.
Then for line 7, where is this data actually going? The index variable here represents the CHIP-8's special I register (Index register). This register holds a memory address pointing to a specific location in the CHIP-8's main RAM. By doing memory[index + i], we make sure the memory steps forward one block at a time to handle each register's value.
At first, I thought this meant we were saving the whole state of the game—all the indexes, the program counter, everything. But no! This isn't a "Save Game" feature like on a PlayStation. The emulator isn't saving to your hard drive. Games used this instruction to temporarily back up a few variables from the registers into the working RAM. So, that we can do some other task and later the game uses a different instruction (Fx65) to load them back out.
What kind of magic is this ?
void Chip8::Table0()
{
((*this).*(table0([opcode & 0x000Fu]))();
}
When I first wrote this line, I looked at it and thought, what kind of magic is this? It looks like someone just smashed their keyboard. But if we break it down, it actually perfectly connects to our McDonald's Function Pointer Table from earlier!
In the CHIP-8, there are a couple of different instructions that start with a 0 (like 00E0 to clear the screen, and 00EE to return from a subroutine). Because they share the same starting number, our emulator needs a secondary menu (table0) to tell them apart. To do that, it needs to look at the very last digit.
First, we see our old friend the Bitwise AND: opcode & 0x000Fu. Because the F is at the very end of the mask this time, it wipes out the first three digits of the opcode and saves only the last one. If our opcode is 00EE, it leaves us with just the E.
That E (which is 14 in decimal) is our menu number. We pass it into table0[...] like an array index, and it instantly finds the correct function pointer for us
The Syntax, why is it like that? Cause life wants us to find beauty in the ugly.
((this).( ... ))();.
This is just standard (but ugly) C++ syntax for calling a function pointer that belongs to a class.
- this is a pointer pointing to our current CHIP-8 object.
- .* is an operator that tells C++ to bind the function we just found in the table to our specific CHIP-8 machine.
- The () at the very end is the trigger, it tells the program, "Okay, execute the function now!" So, in one single, crazy-looking line of code, we extract the menu number, look up the recipe, and cook the food. Magic!
Beauty of instructions? Or the curse of instructions numbering convention?
Ngl, when I first wrote this part, I was just shocked to see so much code and found it highly redundant.
typedef void (Chip8::*Chip8Func)();
Chip8Func table[0xF + 1];
Chip8Func table0[0xE + 1];
Chip8Func table8[0xE + 1];
Chip8Func tableE[0xE + 1];
Chip8Func tableF[0x65 + 1];
void Table0();
void Table8();
void TableE();
void TableF();
void OP_NULL();
Remember this from above? "I read that the naming convention for the Chip-8 opcode was restricted by memory space, hence you are going to find a few things out of order which will not make sense (Go take a look to see what I mean)." Now you will get why!
Why do we need so many tables? This is what I call the curse of the Chip-8 instruction numbering convention. The creators of Chip-8 back in 1977 didn't organize things perfectly. Some starting numbers have just one instruction. But numbers like 0, 8, E, and F have a whole bunch of different instructions packed inside them.
Let's look at a few cool C++ tricks happening here:
typedef: Remember that ugly function pointer syntax from the last section? Instead of writing void (Chip8::*PointerName)() over and over, typedef lets us create a custom nickname. We named it Chip8Func. Now, we can just use Chip8Func to create our arrays, which looks so much cleaner.
The + 1 in the Arrays: Why table[0xF + 1]? Arrays in C++ start counting at 0. Since 0xF is 15, an array of size 15 would only go up to index 14. By adding + 1 (making the size 16), we guarantee we have enough room to safely use F as an index. Look at tableF[0x65 + 1]. The F instructions go all the way up to Fx65 (like the memory save instruction we just looked at). So, we literally have to build an array with 102 empty slots just to reach index 0x65!
OP_NULL(): Because we just created an array with 102 slots for tableF, but only a handful of them are actual CHIP-8 instructions, what happens if the emulator accidentally picks an empty slot? It crashes. OP_NULL() is our safety net. We fill all the empty menu slots with OP_NULL, which basically tells the emulator, "Sorry, that item is not on the menu," and prevents the whole program from blowing up.
Access modifiers
While trying to compile the code, I got a lot of errors: syntax errors, non-existing arguments in the class blueprint, couldn't find the missing SDL2 library for displaying the sprites, and so on. But the main thing that haunted me was Access Modifiers.
I am being honest right now, I just removed my private: tag so everything is public-scoped in my header file.
Here is what went wrong. When main.cpp was trying to talk to my Chip-8 brain, it ran into this concept called Access Modifiers. In C++, anything listed under private: is strictly locked inside the class. That means the "outside world" (main.cpp) is completely forbidden from touching it.
The compiler was yelling at me that video, keypad, and Cycle() were "private within this context." Our main.cpp file needs to read the keypad, draw the video array to the screen, and run the CPU cycle, but it was locked out.
The Hypothetical Fix:
Open chip8.h. Find uint32_t video[64 * 32]{};, uint8_t keypad[16]{};, and void Cycle();(which were originally sitting under the private: label). Cut and paste them higher up in the file so they sit directly under the public: label. Boom. Connection fixed.
But I deleted my private: scope entirely. I am proud of my robust solution, haha!
Conclusion
Yes, I am making it formal by calling this title Conclusion. I did learn a good amount, and yes, I am a better C++ programmer than I was when I started it. Will I become an emulator-making machine? Nope, unless someone pays me a lot, or I wanna play some games that are specific to that architecture/machine. The main goal for me to start this journey of building Chip-8 was to learn through practice, I need to increase the volume of work.
This is not a tutorial, or a documentation, it is just a letter. One twinkling stardust, which I wanna collect enough of to paint my own starry night sky. Thank you for reading this. And as always, Amor Fati to you all.
[Tetris Gameplay on my Chip8 Emulator]

Yes, I am good at Tetris.
Top comments (1)
very clear and informative. keep it up! 👍🏽