Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
- [Getting started](part2/getting-started.md)
- [Objects](part2/objects.md)
- [Functions](part2/functions.md)
- [VBlank interrupts](part2/vblank-interrupts.md)
- [Input](part2/input.md)
- [Collision](part2/collision.md)
- [Bricks](part2/bricks.md)
Expand Down
2 changes: 1 addition & 1 deletion src/part2/functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,4 @@ The registers serve as parameters to the function, so we'll leave them as-is.

</td></tr></tbody></table></div>

In the next chapter, we'll write another function, this time to read player input.
In the next chapter, we'll clean up the frame loop using the VBlank interrupt.
61 changes: 61 additions & 0 deletions src/part2/vblank-interrupts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# VBlank interrupts

So far, we have waited for VBlank by repeatedly reading `rLY` until the PPU reaches line 144.
That works, but it keeps the CPU busy doing nothing useful.
The Game Boy can tell the CPU when VBlank begins instead, using the VBlank interrupt.

An interrupt is a request from the hardware to pause the code currently running, jump to a fixed address, run a small handler, then return to the paused code.
Each interrupt has an address called its *vector*.
The VBlank interrupt vector is `$0040`, and `hardware.inc` gives that address the name `INT_HANDLER_VBLANK`.

Let's add a handler for it above the `"Header"` section:

```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/vblank-interrupts/main.asm:vblank-interrupt}}
{{#include ../../unbricked/vblank-interrupts/main.asm:vblank-interrupt}}
```

The handler does two small jobs.
It marks that VBlank happened, and it increments our frame counter.

Notice the `push af` and `pop af`.
An interrupt can happen between any two instructions in our main code, so the handler must not leave CPU registers changed unexpectedly.
Here we only use `a` and the flags, so saving `af` is enough.
If a handler used more registers, it would need to save those too.
Finally, `reti` returns from the interrupt handler and allows interrupts again.

Next, add one byte next to `wFrameCounter`:

```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/vblank-interrupts/main.asm:variables}}
{{#include ../../unbricked/vblank-interrupts/main.asm:variables}}
```

The CPU will not jump to our handler until we enable interrupts.
After initializing our variables, clear any pending interrupt requests, enable the VBlank interrupt in `rIE`, then enable interrupt handling with `ei`.

```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/vblank-interrupts/main.asm:enable-vblank-interrupt}}
{{#include ../../unbricked/vblank-interrupts/main.asm:enable-vblank-interrupt}}
```

Now we can replace the `rLY` wait loop with a function:

```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/vblank-interrupts/main.asm:wait-for-vblank}}
{{#include ../../unbricked/vblank-interrupts/main.asm:wait-for-vblank}}
```

`halt` stops the CPU until an interrupt occurs.
This lets the CPU sleep instead of burning cycles in a loop.
The `nop` after `halt` is a harmless instruction to resume on, which is a common convention around `halt`.

The function clears `wVBlankDone`, sleeps, and then checks whether the VBlank handler set the byte back to 1.
This matters more once a program has more than one interrupt enabled: if some other interrupt wakes the CPU first, the function just waits again.

Finally, clean up the main loop:

```rgbasm,linenos,start={{#line_no_of "" ../../unbricked/vblank-interrupts/main.asm:main}}
{{#include ../../unbricked/vblank-interrupts/main.asm:main}}
```

The frame counter is now updated by the interrupt handler, so the main loop no longer has to increment it manually.
This also makes the frame boundary explicit: each pass through the game logic starts after `WaitForVBlank` returns.

Up next, we will use that frame loop to read input once per frame.
43 changes: 35 additions & 8 deletions unbricked/audio/main.asm
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,20 @@ DEF BRICK_RIGHT EQU $06
DEF BLANK_TILE EQU $08
; ANCHOR_END: constants

SECTION "VBlank Interrupt", ROM0[INT_HANDLER_VBLANK]
VBlankInterrupt:
push af

ld a, 1
ld [wVBlankDone], a

ld a, [wFrameCounter]
inc a
ld [wFrameCounter], a

pop af
reti

SECTION "Header", ROM0[$100]

jp EntryPoint
Expand Down Expand Up @@ -90,17 +104,18 @@ ClearOam:
ld a, %11100100
ld [rOBP0], a

ld a, 0
xor a, a
ld [wFrameCounter], a
ld [wVBlankDone], a

; Enable the VBlank interrupt
ldh [rIF], a
ld a, IE_VBLANK
ldh [rIE], a
ei

Main:
ld a, [rLY]
cp 144
jp nc, Main
WaitVBlank2:
ld a, [rLY]
cp 144
jp c, WaitVBlank2
call WaitForVBlank

; Add the ball's momentum to its position in OAM.
ld a, [wBallMomentumX]
Expand Down Expand Up @@ -240,6 +255,17 @@ Right:
ld [STARTOF(OAM) + 1], a
jp Main

WaitForVBlank:
xor a, a
ld [wVBlankDone], a
.wait
halt
nop
ld a, [wVBlankDone]
and a, a
jp z, .wait
ret

; Convert a pixel position to a tilemap address
; hl = $9800 + X + Y * 32
; @param b: X
Expand Down Expand Up @@ -644,6 +670,7 @@ BallEnd:

SECTION "Counter", WRAM0
wFrameCounter: db
wVBlankDone: db

SECTION "Input Variables", WRAM0
wCurKeys: db
Expand Down
43 changes: 35 additions & 8 deletions unbricked/bcd/main.asm
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,20 @@ DEF SCORE_TENS EQU $9870
DEF SCORE_ONES EQU $9871
; ANCHOR_END: score-tile-location

SECTION "VBlank Interrupt", ROM0[INT_HANDLER_VBLANK]
VBlankInterrupt:
push af

ld a, 1
ld [wVBlankDone], a

ld a, [wFrameCounter]
inc a
ld [wFrameCounter], a

pop af
reti

SECTION "Header", ROM0[$100]

jp EntryPoint
Expand Down Expand Up @@ -95,21 +109,22 @@ ClearOam:
ld [rOBP0], a
; ANCHOR: init-variables
; Initialize global variables
ld a, 0
xor a, a
ld [wFrameCounter], a
ld [wVBlankDone], a
ld [wCurKeys], a
ld [wNewKeys], a
ld [wScore], a
; ANCHOR_END: init-variables

; Enable the VBlank interrupt
ldh [rIF], a
ld a, IE_VBLANK
ldh [rIE], a
ei

Main:
ld a, [rLY]
cp 144
jp nc, Main
WaitVBlank2:
ld a, [rLY]
cp 144
jp c, WaitVBlank2
call WaitForVBlank

; Add the ball's momentum to its position in OAM.
ld a, [wBallMomentumX]
Expand Down Expand Up @@ -242,6 +257,17 @@ Right:
ld [STARTOF(OAM) + 1], a
jp Main

WaitForVBlank:
xor a, a
ld [wVBlankDone], a
.wait
halt
nop
ld a, [wVBlankDone]
and a, a
jp z, .wait
ret

; Convert a pixel position to a tilemap address
; hl = $9800 + X + Y * 32
; @param b: X
Expand Down Expand Up @@ -741,6 +767,7 @@ BallEnd:

SECTION "Counter", WRAM0
wFrameCounter: db
wVBlankDone: db

SECTION "Input Variables", WRAM0
wCurKeys: db
Expand Down
43 changes: 35 additions & 8 deletions unbricked/bricks/main.asm
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,20 @@ DEF BRICK_RIGHT EQU $06
DEF BLANK_TILE EQU $08
; ANCHOR_END: constants

SECTION "VBlank Interrupt", ROM0[INT_HANDLER_VBLANK]
VBlankInterrupt:
push af

ld a, 1
ld [wVBlankDone], a

ld a, [wFrameCounter]
inc a
ld [wFrameCounter], a

pop af
reti

SECTION "Header", ROM0[$100]

jp EntryPoint
Expand Down Expand Up @@ -91,19 +105,20 @@ ClearOam:
ld [rOBP0], a

; Initialize global variables
ld a, 0
xor a, a
ld [wFrameCounter], a
ld [wVBlankDone], a
ld [wCurKeys], a
ld [wNewKeys], a

; Enable the VBlank interrupt
ldh [rIF], a
ld a, IE_VBLANK
ldh [rIE], a
ei

Main:
ld a, [rLY]
cp 144
jp nc, Main
WaitVBlank2:
ld a, [rLY]
cp 144
jp c, WaitVBlank2
call WaitForVBlank

; Add the ball's momentum to its position in OAM.
ld a, [wBallMomentumX]
Expand Down Expand Up @@ -238,6 +253,17 @@ Right:
ld [STARTOF(OAM) + 1], a
jp Main

WaitForVBlank:
xor a, a
ld [wVBlankDone], a
.wait
halt
nop
ld a, [wVBlankDone]
and a, a
jp z, .wait
ret

; Convert a pixel position to a tilemap address
; hl = $9800 + X + Y * 32
; @param b: X
Expand Down Expand Up @@ -614,6 +640,7 @@ BallEnd:

SECTION "Counter", WRAM0
wFrameCounter: db
wVBlankDone: db

SECTION "Input Variables", WRAM0
wCurKeys: db
Expand Down
43 changes: 35 additions & 8 deletions unbricked/collision/main.asm
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
INCLUDE "hardware.inc"

SECTION "VBlank Interrupt", ROM0[INT_HANDLER_VBLANK]
VBlankInterrupt:
push af

ld a, 1
ld [wVBlankDone], a

ld a, [wFrameCounter]
inc a
ld [wFrameCounter], a

pop af
reti

SECTION "Header", ROM0[$100]

jp EntryPoint
Expand Down Expand Up @@ -91,20 +105,21 @@ ClearOam:
ld [rOBP0], a

; Initialize global variables
ld a, 0
xor a, a
ld [wFrameCounter], a
ld [wVBlankDone], a
ld [wCurKeys], a
ld [wNewKeys], a

; Enable the VBlank interrupt
ldh [rIF], a
ld a, IE_VBLANK
ldh [rIE], a
ei

; ANCHOR: momentum
Main:
ld a, [rLY]
cp 144
jp nc, Main
WaitVBlank2:
ld a, [rLY]
cp 144
jp c, WaitVBlank2
call WaitForVBlank

; Add the ball's momentum to its position in OAM.
ld a, [wBallMomentumX]
Expand Down Expand Up @@ -240,6 +255,17 @@ Right:
ld [STARTOF(OAM) + 1], a
jp Main

WaitForVBlank:
xor a, a
ld [wVBlankDone], a
.wait
halt
nop
ld a, [wVBlankDone]
and a, a
jp z, .wait
ret

; ANCHOR: get-tile
; Convert a pixel position to a tilemap address
; hl = $9800 + X + Y * 32
Expand Down Expand Up @@ -604,6 +630,7 @@ BallEnd:
; ANCHOR: ram
SECTION "Counter", WRAM0
wFrameCounter: db
wVBlankDone: db

SECTION "Input Variables", WRAM0
wCurKeys: db
Expand Down
Loading