Today you make the interrupt gate actually open - when an interrupt is both enabled and pending, the CPU disables further interrupts, pushes the current PC, and jumps to that interrupt's fixed vector, just like a CALL. This is the mechanism that lets a game react to events like VBlank without polling for them.
Dispatch a pending, enabled interrupt by pushing PC and jumping to its fixed vector.
Now the gate actually opens. Between instructions, the CPU checks for a pending
and enabled interrupt - IE & IF with the corresponding bit set - and if IME
is on, it services the highest-priority one. Servicing looks exactly like a
CALL: disable further interrupts (IME off), acknowledge by clearing that
IF bit, push the current PC, and jump to the interrupt’s fixed vector.
VBlank’s vector is 0x0040.
The handler ends with RETI - a RET that also re-enables IME - popping the
saved PC so the interrupted code resumes as if nothing happened. This is the
mechanism that lets a game react to the screen finishing a frame or a button
being pressed without polling. With it working, the CPU can finally respond to
the hardware you are about to build.
func (c *CPU) serviceInterrupts() {pending := c.mem.Read(0xFFFF) & c.mem.Read(0xFF0F)if c.IME && pending&0x01 != 0 { // VBlankc.IME = falsec.clearIF(0) // acknowledgec.push16(c.PC)c.PC = 0x0040}}
Interrupt dispatch - the fixed vectors 0x40, 0x48, 0x50, 0x58, 0x60, and RETI.