Today you write the main loop that ties every subsystem together, stepping the CPU, PPU, and timer in lockstep off the same shared cycle count for one complete frame. This is the last piece - run it against a real ROM and it produces an actual Game Boy screen.
Build the main loop that steps the CPU, PPU, and timer together for one full frame.
This is the lesson everything runs together. The main loop is the whole machine in miniature: service any pending interrupt, step the CPU one instruction, then advance the PPU and timer by the exact cycle count that instruction took, and repeat until a full frame’s worth of cycles (70,224) has elapsed. Every subsystem you built is driven off that one shared clock, which is why cycle accuracy mattered all along.
Point it at a real ROM, run one frame, and write the framebuffer to a PGM file - and there is a Game Boy screen, rendered by an emulator you built from a single 8-bit register up through opcodes, the stack, interrupts, and pixels. From here the road is open: sound, save RAM, more mapper types, cycle-exact PPU timing to pass the hardware test ROMs. But the machine lives. You built it.
func (gb *GameBoy) RunFrame() [144][160]uint8 {for cycles := 0; cycles < 70224; { // 154 lines * 456 cyclesgb.cpu.serviceInterrupts()n := gb.cpu.Step() // one instructiongb.ppu.Step(n) // advance displaygb.timer.Step(n) // advance timerscycles += n}return gb.ppu.RenderFrame()}// To prove a VBlank fired exactly once, add a small counter to the CPU and// bump it inside serviceInterrupts (lesson 40) whenever it dispatches VBlank;// assert it is 1 after a RunFrame.
The emulator main loop - driving every subsystem off the CPU cycle count.