Chapter 1: A Computer in 4,096 Bytes¶
Chapter goal
Clean up the repository, add a chip8 build target, and write a program that
prints the complete state of the machine you are about to emulate.
You will not execute a single instruction today. You will build the thing that instructions will eventually change.
| Files | CMakeLists.txt, src/main.cpp |
| New opcodes | none — the machine only describes itself |
| Time | 30–45 minutes |
1. A program that pretends¶
An emulator is a program that implements another computer's contract.
That contract is everything a program is allowed to observe: the instructions,
the registers, the memory, the display, the keys, and the rate at which time
passes. If your program implements that contract faithfully enough, software
written for the original machine cannot tell the difference. It does not need
to know that there is no 1977 hardware underneath — only C++ and a while loop.
The machine being imitated is the guest. The machine doing the imitating is the host — in our case, your Mac. Almost every design decision in this course comes back to keeping those two words straight.
CHIP-8 has an unusually good origin story for this. In 1977, Joseph Weisbecker wrote it as an interpreter for the RCA COSMAC VIP, a small desk-sized computer built around an RCA 1802 processor. The VIP had a hexadecimal keypad, a monochrome display, and — depending on how much memory you had bought — a couple of kilobytes of RAM. CHIP-8 gave that machine a friendlier instruction set: sixteen 8-bit registers, a 4 KB address space, thirty-five instructions, sprites, a keypad, and two timers.
Because CHIP-8 was interpreted, the same game bytes could run on any machine that implemented the interpreter: the VIP, the DREAM 6800, later the HP 48 calculators, and today an emulator on your laptop. The "hardware" a CHIP-8 program targets is not the 1802. It is the interpreter's machine.
That is the machine we are going to build.
So we are not emulating a 1977 computer?
Not the whole one. We are emulating the virtual machine that CHIP-8 programs
were written for. This is why the instruction 0NNN, which called a routine
on the real 1802, will be a no-op in our emulator: no CHIP-8 game depends on
it, and implementing an 1802 as well would double the project for no gain.
CHIP-8 is a famously good first emulator because the machine is small enough to hold in your head. But small is not the same as trivial. Everything that makes emulation hard is present: a fetch–decode–execute loop, flag bits, timing, sprite collision, input, and a pile of historical disagreements about what the "correct" behavior is. You will meet all of it. You will just meet it one page at a time.
2. The data sheet¶
Before writing code, let us write down the machine. This is the table you will come back to for the next nineteen chapters.
| Part | Size | Job |
|---|---|---|
| Memory | 4,096 bytes | Holds the font, the program, and the program's variables |
| Registers | 16 × 8-bit, V0–VF |
The working set; VF doubles as the flag register |
| Index register | I, 16-bit |
The address used by memory and sprite instructions |
| Program counter | PC, 16-bit |
Where the next instruction lives |
| Stack | 16 × 16-bit | Return addresses for subroutine calls |
| Delay timer | 8-bit | Counts down at 60 Hz; programs read it to pace themselves |
| Sound timer | 8-bit | Counts down at 60 Hz; non-zero means "beep" |
| Display | 64 × 32 pixels | One bit per pixel, XOR-drawn |
| Keypad | 16 keys | The hexadecimal keypad, 0–F |
Two of those deserve a second look.
The address space is 4,096 bytes, and that is not an accident. Every
instruction that mentions an address has room for twelve bits — 0x000 to
0xFFF. Twelve bits is 4,096 values. The machine's memory is exactly as large
as its instructions can point at.
The timers count at 60 Hz. Not at the speed of the CPU. That distinction will matter enormously in Chapter 8: a game can run its logic thousands of times a second while its delay timer ticks sixty times a second, and it expects both.
The memory map¶
Memory is not one undifferentiated pool. The layout is part of the contract:
| Range | Contents |
|---|---|
0x000–0x04F |
Free in our model (on the VIP, the interpreter's 1802 code began here) |
0x050–0x09F |
The font: sixteen 4×5 digit sprites, five bytes each |
0x0A0–0x1FF |
Reserved |
0x200–0xFFF |
Program ROM and the program's work RAM — 3,584 bytes |
Programs start at 0x200 because the interpreter occupied the first 512 bytes
of the address space. Every CHIP-8 program in existence assumes this.
The font sits at 0x050 for a related reason: it lived inside that reserved
region, and the instruction FX29 ("point I at the sprite for digit X")
computes its address from there. The font is not special hardware. It is eighty
bytes of ordinary memory that the interpreter happened to place at a fixed
address, and programs hard-code that address.
A little hex, once
Addresses are written in hexadecimal because the machine is built from
powers of two. 0x200 is 512 in decimal; 0xFFF is 4,095. One hex digit is
four bits — a nibble — so 0xNNN is three nibbles, twelve bits, twelve
address lines. When you see 0x200 in a trace, read it as "the 513th byte",
which is where the first program byte lives.
Everything else on the data sheet — the display, the keypad, the timers — is interface, not memory. It is reached through instructions, not through loads and stores. That is a simplification compared with machines like the Game Boy, where hardware registers live in the address space. Enjoy it while it lasts.
3. The whole machine is one struct and one function¶
Here is the entire architecture of an interpreter, in one picture:
keys ──┐
▼
┌─────────────┐ step() ┌─────────────┐
│ Machine │ ───────────► │ Machine │
│ state │ │ state │
└─────────────┘ └─────────────┘
│
├──► display (pixels to show)
└──► timers (counters to decrement at 60 Hz)
The state is a value. step() reads the instruction at PC, changes the
state, and returns. An emulator is a loop around step().
Everything else is scheduling: how many times to call step() per frame, when
to decrement the timers, when to draw, when to read the keyboard. That
scheduling is the host's job, and we will keep it out of Machine from the
first line.
Today you write the left box: the state. In Chapter 4 you write step().
This order is deliberate. A machine that cannot run still has a shape, and having the shape written down — with named fields and the right widths — makes the instruction work concrete. It also gives us something to test against: we can print the state, compare it with the data sheet, and only then start changing it.
4. Clearing the workbench¶
The repository came with a placeholder target and a misspelled directory. Fix both before adding anything of your own.
Three notes on those commands:
inlcude(with the letters swapped) is empty and nothing includes it. The realinclude/directory will hold your headers from Chapter 3 onward. Git does not track empty directories, soinclude/will stay invisible to it for now; that is fine.build/is generated. It currently contains a cache from thedummytarget, and stale CMake caches are a rich source of confusing errors. Deleting it costs nothing and removes a whole category of "but I changed the file" bugs.dummy.cppis a two-line program that printslol. It has served its purpose.
Your tree should now look like this:
chip8-emulator/
├── CMakeLists.txt
├── include/ (empty for now)
├── references/
├── src/
│ └── main.cpp (you are about to create this)
├── tutorial/
└── zensical.toml
5. The build file¶
Open CMakeLists.txt and replace its contents. The first few blocks are
probably already there; read them anyway, because each one is doing a job you
will need to understand later.
cmake_minimum_required(VERSION 3.28)
# Force Homebrew LLVM before project() so it takes effect.
if(APPLE)
if(EXISTS "/opt/homebrew/opt/llvm/bin/clang++")
set(CMAKE_CXX_COMPILER "/opt/homebrew/opt/llvm/bin/clang++")
elseif(EXISTS "/usr/local/opt/llvm/bin/clang++")
set(CMAKE_CXX_COMPILER "/usr/local/opt/llvm/bin/clang++")
endif()
endif()
project(chip8-emulator LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
# Treat warnings as design feedback: fix them, do not silence them.
add_compile_options(
-Wall
-Wextra
-Wpedantic
-Wconversion
-Wsign-conversion
)
add_executable(chip8 src/main.cpp)
Why each piece is there:
cmake_minimum_required records the oldest CMake the project supports. It
is a promise to your future self, not a request for features.
The compiler block runs before project(). CMake chooses the compiler the
first time project() is called, and after that the choice is cached. Setting
CMAKE_CXX_COMPILER earlier is the supported way to say "use Homebrew's
clang++, not the one Apple ships". Apple's compiler is excellent, but its
C++23 library support lags; std::print, which we use in every chapter, is a
C++23 feature.
CMAKE_CXX_EXTENSIONS OFF means standard C++, not GNU extensions. If a
feature is a compiler extension, we want to know now rather than discover it
when the code moves to another compiler.
CMAKE_EXPORT_COMPILE_COMMANDS writes build/compile_commands.json. Your
editor and clang-tidy read it to know exactly how each file is compiled. It
costs nothing and makes tooling accurate.
The warning list is the project's spine. -Wall -Wextra -Wpedantic catch
ordinary mistakes. -Wconversion and -Wsign-conversion are stricter: they
complain when a value might change width or signedness silently. Emulator code
is full of uint8_t becoming uint16_t and back, so these two will nag you
constantly — and every nag is a place where a mask or a cast should make your
intent visible. Do not disable them. Learn to satisfy them.
add_executable(chip8 src/main.cpp) is the whole target. One executable
named chip8, built from one source file. Chapter 3 adds headers; Chapter 9
adds SDL; Chapter 14 adds a test target. For now, one file is enough.
What happened to include_directories?
The old file had a project-wide include path. It is gone until there is a
header to include. When headers arrive in Chapter 3, the include path comes
back as target_include_directories, which applies to the target that needs
it instead of every target in the project. That is the modern CMake habit,
and it is worth adopting from the start.
6. Writing the state¶
Create src/main.cpp. We will build it in four pieces.
6.1 Includes, names, and constants¶
// CHIP-8 from Scratch - Chapter 1: the machine's state, on paper.
#include <array>
#include <cstddef>
#include <cstdint>
#include <print>
#include <type_traits>
namespace chip8 {
using Byte = std::uint8_t;
using Word = std::uint16_t;
constexpr std::size_t kMemorySize = 4096;
constexpr std::size_t kRegisterCount = 16;
constexpr std::size_t kStackDepth = 16;
constexpr std::size_t kDisplayWidth = 64;
constexpr std::size_t kDisplayHeight = 32;
constexpr std::size_t kKeyCount = 16;
constexpr Word kProgramStart = 0x200;
<array> gives us std::array, a fixed-size array that lives inside its
containing object. <cstddef> gives std::size_t, the type of a size or an
index. <cstdint> gives the exact-width integers: std::uint8_t is guaranteed
to be 8 bits, unlike unsigned char, whose width is merely at least 8.
<print> is C++23's replacement for most uses of std::cout.
<type_traits> supplies std::is_trivially_copyable_v, which we use in a
moment.
The two aliases are not decoration. Emulator code is read far more often than
it is written, and Byte and Word say what the value means:
- A
Byteis the unit of memory and of a register. - A
Wordis a 16-bit value: a program counter, theIregister, a fetched instruction, or a stack entry.
std::uint16_t would work too, but the data sheet talks about bytes and words,
and now so does the code.
The constexpr constants are computed at compile time and cannot change at
runtime. They replace magic numbers — 64 appears nowhere in the program
except here. std::size_t is the right type for a count; Word is the right
type for kProgramStart, because it is an address that will be stored in a
16-bit register.
Everything lives in namespace chip8. A namespace keeps the machine's names
from colliding with the standard library's and with whatever else the host
program grows later.
6.2 The machine¶
This is the chapter's heart:
// Every fact the emulated machine can know. No pointers, no SDL, no clock:
// just the state a CHIP-8 program is allowed to observe.
struct Machine {
std::array<Byte, kMemorySize> memory{};
std::array<Byte, kRegisterCount> v{};
Word i = 0;
Word pc = kProgramStart;
std::array<Word, kStackDepth> stack{};
std::size_t sp = 0;
Byte delay_timer = 0;
Byte sound_timer = 0;
std::array<Byte, kDisplayWidth * kDisplayHeight> display{};
std::array<Byte, kKeyCount> keys{};
std::uint64_t cycles = 0;
};
// The machine is plain data: no pointers, no hidden ownership. That is what
// will let us copy it for save states and trace it for tests.
static_assert(std::is_trivially_copyable_v<Machine>);
Read it against the data sheet. Memory: 4,096 bytes. Registers: sixteen bytes. Index and program counter: sixteen bits each. Stack: sixteen words, with a separate stack pointer. Two timers. A 64×32 display. Sixteen keys. Nothing more, nothing less.
Some details worth pausing on:
- The
{}after each array means "zero-initialize". A freshly constructedMachinehas empty memory, zeroed registers, and a blank screen. A real VIP started with uninitialized memory, which is historically accurate and pedagogically awful; we choose a clean slate and note the difference. vis the register file,V0throughVF. The name matches the data sheet, and short names keep the instruction handlers readable.v[0xA]isVA.iandpcare 16 bits even though base CHIP-8 only uses 12. The extra bits cost nothing, are already the right type for XO-CHIP in Chapter 16, and prevent an entire class of wraparound bug.- The display is flat, not two-dimensional. Pixel
(x, y)isdisplay[y * kDisplayWidth + x]. Astd::array<std::array<Byte, 64>, 32>would read more naturally, but a flat array is easier to copy, easier to hand to a texture, and easy enough to index with a helper function later. - Keys are bytes, not
bools.std::array<bool, 16>is a real array ofbool(unlikestd::vector<bool>), but bytes keep the whole machine uniformly printable and copyable, and a key is naturally a 0/1 state. cyclesis ours, not the hardware's. The original machine had no such counter; it is bookkeeping for tracing, testing, and the debugger. It is in the state because it must be saved and restored with everything else.
The static_assert is a promise with a compiler check attached. Being
trivially copyable means a copy of a Machine is a byte-for-byte duplicate,
with no pointers or ownership to duplicate. That single property will let us
snapshot the machine for save states (Chapter 18), compare states in tests, and
pass it around without surprises. The moment someone adds a std::string or an
SDL pointer to this struct, the program stops compiling — which is exactly when
you want to find out.
Why not std::vector for memory?
Because the size is fixed by the hardware. std::array<Byte, 4096> says
"exactly 4,096 bytes, inside the object, no allocation". A std::vector
would say "some number of bytes, somewhere on the heap" and would make the
machine's size a runtime property. When a size is part of the machine's
definition, put it in the type.
6.3 Printing the state¶
A machine that cannot be observed cannot be debugged. Two small functions give us a view:
void print_machine(const Machine& machine) {
std::println("CHIP-8 from Scratch - the machine we are about to build");
std::println();
std::println(" {:<16} {} bytes", "memory", machine.memory.size());
std::println(" {:<16} {} x 8-bit", "registers", machine.v.size());
std::println(" {:<16} 16-bit 0x{:04X}", "index (I)",
machine.i);
std::println(" {:<16} 16-bit 0x{:04X}", "program counter",
machine.pc);
std::println(" {:<16} 16 x 16-bit depth {}", "stack", machine.sp);
std::println(" {:<16} 8-bit {}", "delay timer",
machine.delay_timer);
std::println(" {:<16} 8-bit {}", "sound timer",
machine.sound_timer);
std::println(" {:<16} {} x {} pixels monochrome", "display",
kDisplayWidth, kDisplayHeight);
std::println(" {:<16} {} keys", "keypad", machine.keys.size());
std::println(" {:<16} {}", "cycles", machine.cycles);
std::println();
std::print(" {:<16}", "V0-VF");
for (const Byte value : machine.v) {
std::print(" {:02X}", value);
}
std::println();
}
std::println prints its arguments and a newline; std::print prints without
one. Both use the same format language as std::format.
The format specifiers are doing visible work:
| Specifier | Meaning |
|---|---|
{} |
Print with the default format |
{:<16} |
Left-align in a field 16 characters wide |
{:02X} |
Uppercase hex, at least 2 digits, zero-padded |
{:04X} |
Uppercase hex, at least 4 digits, zero-padded |
The {:<16} on every line is what aligns the two columns. Strings are formatted
as strings; Byte values as integers; .size() returns std::size_t, which
std::print handles without a cast.
The last loop walks the register file without an index. That matters: it means we cannot accidentally use the wrong index, and it works the same if the register count ever changes.
The parameter is const Machine&: a reference, so the 4 KB machine is not
copied just to be printed, and const, so the printer cannot change it.
void print_memory_map() {
std::println();
std::println("Memory map");
std::println(" 0x000-0x04F free in our model");
std::println(" 0x050-0x09F font sprites: 16 digits, 5 bytes each");
std::println(" 0x0A0-0x1FF reserved");
std::println(" 0x200-0xFFF program ROM and work RAM (3584 bytes)");
}
This one is deliberately simple. It is documentation that runs.
6.4 Entry point¶
int main() {
const chip8::Machine machine{};
chip8::print_machine(machine);
chip8::print_memory_map();
return 0;
}
const chip8::Machine machine{}; value-initializes the machine. The {} is
not optional style: it is what asks for zero-initialized arrays. Machine
machine; would leave the arrays with indeterminate values (the scalar members
have default initializers, the arrays' {} saves them, but the habit matters
more than this one case). Write {} for values you intend to have a known
state.
const says this main only reads the machine. Later, main will own a
Machine and hand it to a step() function that changes it; for now, nothing
changes.
Finally, return 0; is the portable way to report success, and main is the
only place in this project that will ever call std::exit or return an error
code. Library code will report failures as values, not by terminating the
process. You will see that in Chapter 2, when a ROM might be missing.
7. The complete file¶
Here is everything in one place. Compare it with yours; the order of the
#include lines and the exact spelling of the constants are where typos hide.
cmake_minimum_required(VERSION 3.28)
# Force Homebrew LLVM before project() so it takes effect.
if(APPLE)
if(EXISTS "/opt/homebrew/opt/llvm/bin/clang++")
set(CMAKE_CXX_COMPILER "/opt/homebrew/opt/llvm/bin/clang++")
elseif(EXISTS "/usr/local/opt/llvm/bin/clang++")
set(CMAKE_CXX_COMPILER "/usr/local/opt/llvm/bin/clang++")
endif()
endif()
project(chip8-emulator LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
# Treat warnings as design feedback: fix them, do not silence them.
add_compile_options(
-Wall
-Wextra
-Wpedantic
-Wconversion
-Wsign-conversion
)
add_executable(chip8 src/main.cpp)
// CHIP-8 from Scratch - Chapter 1: the machine's state, on paper.
#include <array>
#include <cstddef>
#include <cstdint>
#include <print>
#include <type_traits>
namespace chip8 {
using Byte = std::uint8_t;
using Word = std::uint16_t;
constexpr std::size_t kMemorySize = 4096;
constexpr std::size_t kRegisterCount = 16;
constexpr std::size_t kStackDepth = 16;
constexpr std::size_t kDisplayWidth = 64;
constexpr std::size_t kDisplayHeight = 32;
constexpr std::size_t kKeyCount = 16;
constexpr Word kProgramStart = 0x200;
// Every fact the emulated machine can know. No pointers, no SDL, no clock:
// just the state a CHIP-8 program is allowed to observe.
struct Machine {
std::array<Byte, kMemorySize> memory{};
std::array<Byte, kRegisterCount> v{};
Word i = 0;
Word pc = kProgramStart;
std::array<Word, kStackDepth> stack{};
std::size_t sp = 0;
Byte delay_timer = 0;
Byte sound_timer = 0;
std::array<Byte, kDisplayWidth * kDisplayHeight> display{};
std::array<Byte, kKeyCount> keys{};
std::uint64_t cycles = 0;
};
// The machine is plain data: no pointers, no hidden ownership. That is what
// will let us copy it for save states and trace it for tests.
static_assert(std::is_trivially_copyable_v<Machine>);
void print_machine(const Machine& machine) {
std::println("CHIP-8 from Scratch - the machine we are about to build");
std::println();
std::println(" {:<16} {} bytes", "memory", machine.memory.size());
std::println(" {:<16} {} x 8-bit", "registers", machine.v.size());
std::println(" {:<16} 16-bit 0x{:04X}", "index (I)",
machine.i);
std::println(" {:<16} 16-bit 0x{:04X}", "program counter",
machine.pc);
std::println(" {:<16} 16 x 16-bit depth {}", "stack", machine.sp);
std::println(" {:<16} 8-bit {}", "delay timer",
machine.delay_timer);
std::println(" {:<16} 8-bit {}", "sound timer",
machine.sound_timer);
std::println(" {:<16} {} x {} pixels monochrome", "display",
kDisplayWidth, kDisplayHeight);
std::println(" {:<16} {} keys", "keypad", machine.keys.size());
std::println(" {:<16} {}", "cycles", machine.cycles);
std::println();
std::print(" {:<16}", "V0-VF");
for (const Byte value : machine.v) {
std::print(" {:02X}", value);
}
std::println();
}
void print_memory_map() {
std::println();
std::println("Memory map");
std::println(" 0x000-0x04F free in our model");
std::println(" 0x050-0x09F font sprites: 16 digits, 5 bytes each");
std::println(" 0x0A0-0x1FF reserved");
std::println(" 0x200-0xFFF program ROM and work RAM (3584 bytes)");
}
} // namespace chip8
int main() {
const chip8::Machine machine{};
chip8::print_machine(machine);
chip8::print_memory_map();
return 0;
}
8. Build and run¶
From the repository root:
You should see:
CHIP-8 from Scratch - the machine we are about to build
memory 4096 bytes
registers 16 x 8-bit
index (I) 16-bit 0x0000
program counter 16-bit 0x0200
stack 16 x 16-bit depth 0
delay timer 8-bit 0
sound timer 8-bit 0
display 64 x 32 pixels monochrome
keypad 16 keys
cycles 0
V0-VF 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
Memory map
0x000-0x04F free in our model
0x050-0x09F font sprites: 16 digits, 5 bytes each
0x0A0-0x1FF reserved
0x200-0xFFF program ROM and work RAM (3584 bytes)
That is the machine: empty, patiently waiting at 0x200 for a program that does
not exist yet.
If Ninja is not installed
Drop -G Ninja and CMake will use Makefiles instead. Everything else is
identical. Ninja is faster and has quieter output, which is why the
tutorial uses it.
9. Checkpoint¶
Before moving on, confirm all of these:
-
cmake --build buildfinishes with no warnings and no errors. -
./build/chip8exits with status 0 (echo $?prints 0). - The output contains
program counter 16-bit 0x0200. - The output contains all four memory-map ranges.
-
dummy.cppis gone andinlcude/no longer exists. -
git statusshowsCMakeLists.txtmodified,dummy.cppdeleted, andsrc/main.cppuntracked — and nothing else surprising.
10. When it does not work¶
| Symptom | Likely cause | Check |
|---|---|---|
'print' file not found or no member named 'print' in namespace 'std' |
CMake used Apple clang instead of Homebrew LLVM | grep CMAKE_CXX_COMPILER build/CMakeCache.txt; it should mention /opt/homebrew/opt/llvm/. Delete build/ and reconfigure. |
Cannot find source file: src/main.cpp |
The file is in the wrong directory or has a different name | ls src/; the path in add_executable is relative to the repository root |
CMake still mentions dummy |
Stale build directory | rm -rf build and configure again |
CMake Error: generator Ninja does not match the generator used previously |
build/ was configured with Makefiles |
Delete build/ |
| A warning about an unused constant | Typo in a name, or a member not used yet | Read the line number; -Werror is not on for your local build, so the program may still run |
Output shows 0x0000 for the program counter |
pc was not initialized from kProgramStart |
Compare with the complete file |
11. Exercises¶
1. Why 0x200? In one or two sentences, explain to someone who has never
seen CHIP-8 why programs do not start at address zero. Then explain why the font
is at 0x050 rather than, say, 0x800.
2. A register listing. Change print_machine so that each register is
printed on its own line:
Hint: a for loop with an index is the straightforward way:
for (std::size_t index = 0; index < machine.v.size(); ++index). Use
std::format or a table of names ("0123456789ABCDEF") if you want the digit
after the V. Notice that this is the first place where you index an array with
a variable — a habit we will need constantly.
3. How big is the machine? Add a line that prints sizeof(chip8::Machine).
Then print the size of the memory array and the display array separately. Are
the numbers what you expect? (The machine is about 6 KB, not 4 KB: the display,
stack, registers, and bookkeeping are real space too.) Finally, explain in one
sentence why a state that is small, plain, and copyable will matter when we add
a rewind debugger in Chapter 18.
12. Recap¶
- An emulator implements another machine's contract; the emulated machine is the guest, your Mac is the host.
- CHIP-8 is a virtual machine from 1977, originally interpreted on the COSMAC VIP. We emulate the virtual machine, not the 1802.
- The machine's state fits in one struct: 4 KB of memory, sixteen 8-bit registers, a 16-bit index and program counter, a sixteen-deep stack, two timers, a 64×32 display, and sixteen keys.
- Programs start at
0x200; the font lives at0x050; both are fixed by the interpreter's own history. std::array+constexprsizes make the machine's shape a compile-time fact.-Wconversionand-Wsign-conversionare on, and they are on purpose.static_assert(std::is_trivially_copyable_v<Machine>)is a design promise the compiler enforces: the machine is plain data.
13. What is next¶
The machine is empty. In Chapter 2 you will teach it to read: a real ROM, loaded into those 4,096 bytes, and a hex dump that lets you see the program you are about to run.
Further reading, for the history in this chapter: the CHIP-8 extensions reference and Tobias V. Langhoff's guide.