Monorepo for Aesthetic.Computer
aesthetic.computer
1; Fullscreen Background Example for the Nintendo Game Boy
2; by Dave VanEe 2022
3; Tested with RGBDS 1.0.0
4; License: CC0 (https://creativecommons.org/publicdomain/zero/1.0/)
5
6include "hardware.inc" ; Include hardware definitions so we can use nice names for things
7
8
9SECTION "MemCopy Routine", ROM0
10; Since we're copying data few times, we'll define a reusable memory copy routine
11; Copy BC bytes of data from HL to DE
12MemCopy:
13 ld a, [hli] ; Load a byte from the address HL points to into the register A, increment HL
14 ld [de], a ; Load the byte in the A register to the address DE points to
15 inc de ; Increment the destination pointer in DE
16 dec bc ; Decrement the loop counter in BC
17 ld a, b ; Load the value in B into A
18 or c ; Logical OR the value in A (from B) with C
19 jr nz, MemCopy ; If B and C are both zero, OR B will be zero, otherwise keep looping
20 ret ; Return back to where the routine was called from
21
22
23; The VBlank vector is where execution is passed when the VBlank interrupt fires
24SECTION "VBlank Vector", ROM0[$40]
25; We only have 8 bytes here, so take care of what we can in 5 bytes and then jump to the rest of the handler
26VBlank:
27 push af ; Push AF to the stack
28 ld a, 1 ; Load a non-zero value into A
29 ldh [hVBlankDone], a; Set a flag indicating VBlank has fired, which will allow WaitVBlank to exit
30 jp VBlankHandler ; Jump to the rest of the handler
31
32
33; The STAT vector is where execution is passed when the STAT interrupt fires
34SECTION "STAT Vector", ROM0[$48]
35; We can fit the entire handler inside the 8 bytes we have, so there's no need to jump away here
36 push af ; Push AF to the stack
37 ld a, LCDC_ON | LCDC_BLOCK21 | LCDC_BG_ON | LCDC_OBJ_OFF | LCDC_WIN_OFF
38 ldh [rLCDC], a ; Set the background to use the tile starting at $8800 after the LYC interrupt line
39 pop af ; Pop AF off the stack
40 reti ; Return and enable interrupts (ret + ei)
41
42
43; The rest of the VBlank handler is contained in ROM0 to ensure it's always accessible without banking
44SECTION "VBlank Handler", ROM0
45VBlankHandler:
46 ld a, LCDC_ON | LCDC_BLOCK01 | LCDC_BG_ON | LCDC_OBJ_OFF | LCDC_WIN_OFF
47 ldh [rLCDC], a ; Set the background to use the tile starting at $8000 from the top of the screen
48 pop af ; Pop AF off the stack
49 reti ; Return and enable interrupts (ret + ei)
50
51
52; Define a section that starts at the point the bootrom execution ends
53SECTION "Start", ROM0[$0100]
54 jp EntryPoint ; Jump past the header space to our actual code
55
56 ds $150-@, 0 ; Allocate space for RGBFIX to insert our ROM header by allocating
57 ; the number of bytes from our current location (@) to the end of the
58 ; header ($150)
59
60EntryPoint:
61 di ; Disable interrupts during setup
62 ld sp, $e000 ; Set the stack pointer to the end of WRAM
63
64 ; Turn off the LCD when it's safe to do so (during VBlank)
65.waitVBlank
66 ldh a, [rLY] ; Read the LY register to check the current scanline
67 cp SCREEN_HEIGHT_PX ; Compare the current scanline to the first scanline of VBlank
68 jr c, .waitVBlank ; Loop as long as the carry flag is set
69 xor a ; Once we exit the loop we're safely in VBlank
70 ldh [rLCDC], a ; Disable the LCD (must be done during VBlank to protect the LCD)
71
72 ; Copy the first 240 tiles to VRAM starting at $8000
73 ld hl, TileData ; Load the source address of our tiles into HL
74 ld de, STARTOF(VRAM); Load the destination address in VRAM into DE
75 ld bc, $10*240 ; Load the number of bytes to copy into BC (16 bytes per tile)
76 call MemCopy ; Call our general-purpose memory copy routine
77
78 ; Copy the remaining 120 tiles to VRAM starting at $9000
79 ; Note: HL is already pointing at the start of the data we want to copy
80 ld de, STARTOF(VRAM)+$1000 ; Load the destination address in VRAM into DE
81 ld bc, $10*120 ; Load the number of bytes to copy into BC (16 bytes per tile)
82 call MemCopy ; Call our general-purpose memory copy routine
83
84 ; Create a tilemap which includes all 360 tiles, in order, starting at tile 0
85 ld hl, TILEMAP0 ; Point HL to the first byte of the tilemap ($9800)
86 ld de, TILEMAP_WIDTH - SCREEN_WIDTH ; Load the number of tiles to skip per row (32-20) into DE
87 xor a ; Store the tile ID we're writing in A, starting with zero
88 ld c, SCREEN_HEIGHT ; Load the total number of rows (18) into C
89.yLoop
90 ld b, SCREEN_WIDTH ; Load the number of tiles per row (20) into B
91.xLoop
92 ld [hli], a ; Load the current tile ID into HL and increment HL
93 inc a ; Increment the tile ID in A
94 dec b ; Decrement the X counter
95 jr nz, .xLoop ; Loop until we've written a full row
96 add hl, de ; Add the offset to advance to the next row in VRAM to HL
97 ld b, a ; Move the tile ID (A) to B temporarily to free A up
98 ld a, c ; Load the rows remaining into A
99 cp 7 ; Compare A to 7 to see if we only have 6 rows remaining
100 ; (we compare to 7 instead of 6 since we haven't decremented C yet)
101 jr nz, .dontReset ; If we aren't at the "6 rows remaining" point don't reset the tile ID
102 ld b, 0 ; Load zero into B as the starting tile ID for the bottom portion
103.dontReset
104 ld a, b ; Recover the tile ID from B (either the old one or the reset one) into A
105 dec c ; Decrement the row counter
106 jr nz, .yLoop ; Loop until we've written the top set of tile IDs
107
108 ; Setup palettes and scrolling
109 ld a, %11100100 ; Define a 4-shade palette from darkest (11) to lightest (00)
110 ldh [rBGP], a ; Set the background palette
111
112 ld a, 0 ; Load zero into the register A
113 ldh [rSCX], a ; Set the background scroll registers to show the top-left
114 ldh [rSCY], a ; corner of the background in the top-left corner of the screen
115
116 ldh [hVBlankDone], a; Initialize the hVBlankDone flag just to be safe
117
118 ; Setup the VBlank and STAT interrupts
119 ld a, STAT_LYC ; Load the flag to enable LYC STAT interrupts into A
120 ldh [rSTAT], a ; Load the prepared flag into rSTAT to enable the LY=LYC interrupt source
121 ld a, 96 ; Set which line to trigger the LY=LYC interrupt on by setting the rLYC register
122 ldh [rLYC], a ; ...
123 ld a, IE_VBLANK | IE_STAT ; Load the flag to enable the VBlank and STAT interrupts into A
124 ldh [rIE], a ; Load the prepared flag into the interrupt enable register
125 xor a ; Set A to zero
126 ldh [rIF], a ; Clear any lingering flags from the interrupt flag register to avoid false interrupts
127 ei ; enable interrupts!
128
129 ; Combine flag constants defined in hardware.inc into a single value with logical ORs and load it into A
130 ; Note that some of these constants (LCDC_BG_OFF, LCDC_OBJ8, LCDC_WIN_OFF) are zero, but are included for clarity
131 ld a, LCDC_ON | LCDC_BLOCK01 | LCDC_BG_ON | LCDC_OBJ_OFF | LCDC_WIN_OFF
132 ldh [rLCDC], a ; Enable and configure the LCD to show the background
133
134LoopForever:
135 call WaitVBlank ; Call our routine which halts until the hVBlankDone flag is set
136 jr LoopForever ; Loop forever
137
138SECTION "WaitVBlank", ROM0
139; Since we have multiple interrupts enabled, using a single HALT in our mainloop isn't enough to sync the
140; mainloop to the screen refresh. Since we only have two interrupts (LY=LYC and VBlank), we could HALT twice,
141; but that scales poorly. Instead, we zero a flag, and then halt in a loop until our VBlank handler sets
142; that flag, letting us know it's fired.
143WaitVBlank:
144 xor a ; Zero the hVBlankDone flag before starting our loop
145 ldh [hVBlankDone], a; ...
146.loop
147 halt ; Halt the CPU, waiting until an interrupt fires (it could be STAT or VBlank)
148 ldh a, [hVBlankDone]; Load the hVBlankDone flag value into A
149 or a ; This is a shortcut version of 'cp 0', to see if the flag has been set by our VBlank handler
150 jr z, .loop ; If the flag isn't set, halt again
151 ret ; Return back to where the routine was called from
152
153SECTION "VBlank Variables", HRAM
154; Reserve space in HRAM to track when the VBlank interrupt has fired
155hVBlankDone:
156 ds 1
157
158SECTION "Tile Data", ROMX
159TileData:
160 incbin "starry-night-tiles.2bpp" ; Include binary tile data inline using incbin