Skip to content

Instantly share code, notes, and snippets.

@daltonclaybrook
Last active February 6, 2021 23:28
Show Gist options
  • Save daltonclaybrook/6218ec624ce0d8c524fda7fe11139f47 to your computer and use it in GitHub Desktop.
Save daltonclaybrook/6218ec624ce0d8c524fda7fe11139f47 to your computer and use it in GitHub Desktop.
include "hardware.inc" ; https://github.com/gbdev/hardware.inc
; Constants
LCD_ON_BIT EQU 7
LCD_BG_TILE_DATA_BIT EQU 4
RUBY_TOP_VRAM EQU _VRAM
; Define a 2-byte RGB color
;
; /1 = R, /2 = G, /3 = B
; Each color component is 5 bits (0-31)
; `RGB 31, 31, 31` == white
RGB: MACRO
dw (\3 << 10 | \2 << 5 | \1)
ENDM
section "VBlank Interrupt", rom0[$40]
reti
section "STAT Interrupt", rom0[$48]
jp CheckHBlank
section "Header", rom0[$100]
; Execution always begins at $100, but the header starts at $104
; so we need to jump quickly to another location.
EntryPoint::
di
jp Start
; The cartridge header is located here, which contains things like
; the game title, Nintendo logo, and GBC support flag.
; This instruction fills this space with zeros, and the `rgbfix`
; program will overwrite with the correct values later.
ds $150 - $104
section "Main", rom0[$150]
Start::
; At this moment, the `a` register contains a special value if we're running on a CGB, so
; we preserve this state by pushing it onto the stack
ld b, a
push bc ; preserve b which contains CGB state
call DisableLCD
pop bc ; reload bc
call LoadPalette
call CopyRubyTopIntoVRAM
call CopyRubyBottomIntoVRAM
call CopyTopTilesToBGMap
call CopyBottomTilesToBGMap
call EnableSTATInterrupt
call EnableLCD
.gameLoop
halt
jr .gameLoop
DisableLCD::
ld b, SCRN_Y ; Game Boy screen is 144 pixels tall. Any value over this and we're in V-Blank period.
.loop
ld a, [rLY] ; read y coordinate register
cp b
jr c, .loop ; if no carry occurs, we're in the vblank period
ld a, [rLCDC]
res LCD_ON_BIT, a ; turn off LCD
ld [rLCDC], a
ret
EnableLCD::
ld a, [rLCDC]
set LCD_ON_BIT, a ; turn on LCD
ld [rLCDC], a
ret
EnableSTATInterrupt::
ld a, %00001000 ; H-blank interrupt on stat register
ld [rSTAT], a
ld a, %00000011 ; LCD STAT & VBlank interrupts
ld [rIE], a ; enable interrupts
reti ; return and enable interrupts
LoadPalette::
ld a, $11 ; a value of $11 indicates CGB
cp b ; if a == b, this is a CGB
jp z, LoadCGBPalette
jp LoadDMGPalette
LoadCGBPalette::
ld a, %10000000 ; 7th bit is set to tell palette register to auto-increment.
ld [rBCPS], a
ld de, rBCPD
ld hl, Palette
ld c, 8 ; 8 bytes per palette, 2 bytes per color
.loop
ld a, [hli]
ld [de], a ; we're writing to this same register over and over. Under the hood, the palette index is being auto-incremented
dec c
jr nz, .loop
ret
LoadDMGPalette::
; ld a, %00011011 ; black, dark gray, light gray, white
ld a, %11100100 ; white, light gray, dark gray, black
ld [rBGP], a
ret
CopyRubyTopIntoVRAM::
xor a
ld hl, RUBY_TOP_VRAM
ld de, RubyTop
ld bc, RubyTopEnd - RubyTop ; count down
call StartVRAMCopy
ret
; The tile data at $8800-$97FF is accessed differently than the one at $8000-$8FFF
; BG map tile index 0 refers to the tile at $9000. Indexes over 127 wrap around to
; tiles $8800 and above. This function follows these same rules by copying the first
; tile into $9000 and eventually wrapping around.
CopyRubyBottomIntoVRAM::
xor a ; VRAM bank 0
ld hl, $9000 ; the first destination for ruby tiles
ld de, RubyBottom
ld bc, $800 ; the counter to use in the first copy. This is 1/3rd of tile RAM.
call StartVRAMCopy
xor a ; VRAM bank 0
ld hl, $8c00 ; the second destination
ld de, RubyBottom + $800 ; start where we left off
ld bc, RubyBottomEnd - (RubyBottom + $800) ; the count of remaining tiles
call StartVRAMCopy
ret
; @param a - VRAM bank index
; @param de - pointer to start of bytes
; @param bc - count of bytes
; @param hl - Address of first VRAM location
StartVRAMCopy::
ld [rVBK], a ; set the bank number to register a
.loop
ld a, [de]
ld [hli], a
inc de
dec bc
ld a, b
or c ; `dec bc` above does not set flags like `dec b` does (bummer). We have to do this dance to see if bc is zero.
jr nz, .loop
ret
; Populates the top half of the BG map with tiles. This procedure is simpler than the
; bottom half because of the way the tile data is stored in VRAM.
CopyTopTilesToBGMap::
xor a ; first tile and bank number
ld [rVBK], a ; set the bank number to register a
ld hl, _SCRN0 ; start drawing to top half of screen
ld d, SCRN_Y / 2 ; row countdown to stop drawing. we're drawing half the screen.
.startNextRow
ld c, 20 ; 20 tiles per row
.startNextTile
ld [hli], a
inc a ; next tile index
dec c ; decrememnt row counter
jr nz, .startNextTile
dec d
jr z, .finish ; if row countdown is zero, we are finished drawing the screen
push de
ld de, 12 ; add 12 to hl on each loop to wrap to next line
add hl, de ; wrap map to next line
pop de
jr .startNextRow
.finish
ret
; Populates the bottom half of the BG map with tiles. This procedure is more complicated than
; the top half because the pointer to tile data must skip over 64 tiles that are part of the
; top image
CopyBottomTilesToBGMap::
ld hl, _SCRN0 + $120 ; bottom half of the screen
ld b, 128 ; 128 tiles in the bottom 3rd of VRAM
ld d, SCRN_Y / 2 ; row countdown to stop drawing. we're drawing half the screen.
xor a ; start with first tile
.startNextRow
ld c, 20 ; 20 tiles per row
.startNextTile
ld [hli], a
inc a ; next tile index
dec b ; decrement tile section counter
jr nz, .wrapRowCheck
add a, 64 ; skip over 64 tiles that are part of the top image
.wrapRowCheck
dec c ; decrememnt row counter
jr nz, .startNextTile
dec d
jr z, .finish ; if row countdown is zero, we are finished drawing the screen
push de
ld de, 12 ; add 12 to hl on each loop to wrap to next line
add hl, de ; wrap map to next line
pop de
jr .startNextRow
.finish
ret
CheckHBlank::
push af
push bc
push de
push hl
ld a, [rSTAT]
and %11 ; mask off mode bits and sets zero flag if result is zero
jr nz, .finish ; mode is not zero, finish up
ld a, [rLY]
cp 143 ; 143 is last line before vblank. Switch to top tiles.
jr nz, .checkMidScreen
ld a, [rLCDC]
set LCD_BG_TILE_DATA_BIT, a ; set BG tile data select bit for tile range $8000-$8fff
ld [rLCDC], a
jr .finish
.checkMidScreen
; if next line is 72 (half of screen height), switch to upper tile set
cp 71 ; 71 is last line of top section. Switch to bottom tiles.
jr nz, .finish
ld a, [rLCDC]
res LCD_BG_TILE_DATA_BIT, a ; unset BG tile data select bit for tile range $8800-$97ff
ld [rLCDC], a
.finish
pop hl
pop de
pop bc
pop af
reti
section "Data", rom0
RubyTop::
incbin "ruby-top.2bpp"
RubyTopEnd::
RubyBottom::
incbin "ruby-bottom.2bpp"
RubyBottomEnd::
Palette::
RGB 29, 29, 25
RGB 27, 19, 23
RGB 1, 19, 21
RGB 17, 0, 10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment