aboutsummaryrefslogtreecommitdiff
path: root/space
diff options
context:
space:
mode:
authorMiquel Sabaté Solà <msabate@suse.com>2023-10-24 17:40:19 +0200
committerMiquel Sabaté Solà <mikisabate@gmail.com>2025-02-04 20:58:36 +0100
commit149020464517ff3d021eef5ccd50d3939ad463e6 (patch)
tree056ec1919fe0c0d515e7a0a414916307b768ef84 /space
parentbe2414f5b11b182998fca7e87712ff0502c8ee93 (diff)
downloadcode.nes-149020464517ff3d021eef5ccd50d3939ad463e6.tar.gz
code.nes-149020464517ff3d021eef5ccd50d3939ad463e6.zip
Lay out a proper structure for the project
Let's divide things by topic instead of a broad "examples" directory. This will allow for simple files to co-exist together with full-blown projects. Signed-off-by: Miquel Sabaté Solà <msabate@suse.com>
Diffstat (limited to 'space')
-rw-r--r--space/Makefile15
-rw-r--r--space/assets/background.nambin0 -> 1024 bytes
-rw-r--r--space/assets/space.chrbin0 -> 8192 bytes
-rw-r--r--space/include/apu.s6
-rw-r--r--space/include/joypad.s63
-rw-r--r--space/include/oam.s13
-rw-r--r--space/include/ppu.s27
-rw-r--r--space/src/space.s147
-rw-r--r--space/src/states/game.s21
-rw-r--r--space/src/states/player.s356
-rw-r--r--space/src/vectors/irq.s6
-rw-r--r--space/src/vectors/nmi.s14
-rw-r--r--space/src/vectors/reset.s76
13 files changed, 744 insertions, 0 deletions
diff --git a/space/Makefile b/space/Makefile
new file mode 100644
index 0000000..e38607c
--- /dev/null
+++ b/space/Makefile
@@ -0,0 +1,15 @@
+CC65 ?= cl65
+CCOPTS ?= --verbose --target nes
+
+.PHONY: all
+all: clean build
+
+.PHONY: clean
+clean:
+ @rm -f space.nes
+
+.PHONY: build
+build: space.nes
+
+%.nes: src/%.s
+ $(CC65) $(CCOPTS) $< -o $@
diff --git a/space/assets/background.nam b/space/assets/background.nam
new file mode 100644
index 0000000..ea228ff
--- /dev/null
+++ b/space/assets/background.nam
Binary files differ
diff --git a/space/assets/space.chr b/space/assets/space.chr
new file mode 100644
index 0000000..ee1f47a
--- /dev/null
+++ b/space/assets/space.chr
Binary files differ
diff --git a/space/include/apu.s b/space/include/apu.s
new file mode 100644
index 0000000..32ccff2
--- /dev/null
+++ b/space/include/apu.s
@@ -0,0 +1,6 @@
+.segment "CODE"
+
+.scope APU
+ DMC = $4010
+ FRAME_COUNTER = $4017
+.endscope
diff --git a/space/include/joypad.s b/space/include/joypad.s
new file mode 100644
index 0000000..8711c28
--- /dev/null
+++ b/space/include/joypad.s
@@ -0,0 +1,63 @@
+.segment "CODE"
+
+;;;
+;; Joypad controller code. The following memory addresses are reserved: $21-$23.
+;;
+;; Memory address $21 is used for internal purposes, whereas $22 and $23 contain
+;; the bitmask of the buttons that are pressed from each controller.
+;;;
+
+.scope Joypad
+ ;; Button masks.
+ BUTTON_A = 1 << 7
+ BUTTON_B = 1 << 6
+ BUTTON_SELECT = 1 << 5
+ BUTTON_START = 1 << 4
+ BUTTON_UP = 1 << 3
+ BUTTON_DOWN = 1 << 2
+ BUTTON_LEFT = 1 << 1
+ BUTTON_RIGHT = 1 << 0
+
+ ;; Port addresses for controllers.
+ JOYPAD1 = $4016
+ JOYPAD2 = $4017
+
+ ;; We keep all the information from controller from mainly two variables:
+ ;; m_buttons1 and m_buttons2; containing respectively the buttons pressed
+ ;; for this frame for both controllers. The m_inv_buttons ($21) is an
+ ;; internal variable and should not be used for anything outside of this
+ ;; usage.
+ m_inv_buttons = $21
+ m_buttons1 = $22
+ m_buttons2 = $23
+
+ ;; READ_CONTROLLER reads the input from the controller mapped into the given
+ ;; port, and saves the state into the given `buttons` address.
+ ;; Implementation taken from NESHacker's example of smb3-like movement.
+ .macro READ_CONTROLLER port, buttons
+ lda m_inv_buttons
+ tay
+ lda #1
+ sta port
+ sta m_inv_buttons
+ lsr
+ sta port
+ :
+ lda port
+ lsr
+ rol m_inv_buttons
+ bcc :-
+ tya
+ eor m_inv_buttons
+ and m_inv_buttons
+ sta buttons
+ .endmacro
+
+ ;; read sets the values for m_buttons1 and m_buttons2 as read from both
+ ;; controllers.
+ .proc read
+ READ_CONTROLLER JOYPAD1, m_buttons1
+ READ_CONTROLLER JOYPAD2, m_buttons2
+ rts
+ .endproc
+.endscope
diff --git a/space/include/oam.s b/space/include/oam.s
new file mode 100644
index 0000000..ecd1b7d
--- /dev/null
+++ b/space/include/oam.s
@@ -0,0 +1,13 @@
+.segment "CODE"
+
+.scope OAM
+ ADDR = $2003
+ DMA = $4014
+.endscope
+
+.macro OAM_WRITE_SPRITES
+ lda #$00
+ sta OAM::ADDR
+ lda #$02
+ sta OAM::DMA
+.endmacro
diff --git a/space/include/ppu.s b/space/include/ppu.s
new file mode 100644
index 0000000..23fd978
--- /dev/null
+++ b/space/include/ppu.s
@@ -0,0 +1,27 @@
+.segment "CODE"
+
+.scope PPU
+ CONTROL = $2000
+ MASK = $2001
+ STATUS = $2002
+ SCROLL = $2005
+ ADDRESS = $2006
+ DATA = $2007
+.endscope
+
+.macro PPU_ADDR address
+ lda #.HIBYTE(address)
+ sta PPU::ADDRESS
+ lda #.LOBYTE(address)
+ sta PPU::ADDRESS
+.endmacro
+
+;; WRITE_PPU_DATA is a macro that will write into PPU::ADDRESS the given address
+;; and into PPU::DATA the given byte value.
+.macro WRITE_PPU_DATA address, value
+ bit PPU::STATUS
+
+ PPU_ADDR address
+ lda #value
+ sta PPU::DATA
+.endmacro
diff --git a/space/src/space.s b/space/src/space.s
new file mode 100644
index 0000000..eec893c
--- /dev/null
+++ b/space/src/space.s
@@ -0,0 +1,147 @@
+;;;
+;;; TODO: reduce the scope of this to just:
+;;; - Movement through subpixels.
+;;; - Shooting (no collision or anything)
+;;; -> link to jetpac.nes for more stuff
+;;;
+;; This is similar to the sprite.s example, but it expands on it greatly into a
+;; full game by:
+;; - Having a moving background.
+;; - The ship can be moved:
+;; - The movement is done through subpixels for a smoother experience.
+;; - The ship's sprites are updated accordingly: resting, acceleration, full
+;; speed.
+;; - Random asteroids will appear from time to time and they can collide with
+;; the ship:
+;; - A collision decreases the live status from the ship (cracks will appear
+;; to the sprite).
+;; - When the live status reaches 0 -> game over.
+;; - The ship can shoot and destroy asteroids.
+;; - There is a score.
+;;;
+
+.segment "HEADER"
+ .byte 'N', 'E', 'S', $1A
+
+ ;; 2x PRG-ROM; 1x CHR-ROM
+ .byte $02
+ .byte $01
+
+ ;; Horizontal mirroring, no special mapper.
+ .byte $00
+ .byte $00
+
+.segment "VECTORS"
+ .addr nmi, reset, irq
+
+.segment "CHARS"
+ .incbin "../assets/space.chr"
+
+.segment "STARTUP"
+
+.segment "CODE"
+
+.include "../include/apu.s"
+.include "../include/oam.s"
+.include "../include/ppu.s"
+.include "../include/joypad.s"
+
+.include "states/game.s"
+.include "states/player.s"
+.include "vectors/reset.s"
+.include "vectors/nmi.s"
+.include "vectors/irq.s"
+
+;;;
+;; This is our main subroutine, the reset procedure will call at the very end of
+;; initializing the hardware.
+;;;
+.proc main
+ ;; Before starting the game loop proper we initialize all our assets: load
+ ;; the palettes, nametables and sprites for this game.
+ jsr init_palettes
+ jsr init_nametable
+ jsr Player::init
+
+ ;; Reset scroll.
+ bit PPU::STATUS
+ lda #$00
+ sta PPU::SCROLL
+ sta PPU::SCROLL
+
+ cli
+
+ ;; 7: allow NMI; 5: sprite size is 8x16; 4: background pattern table starts
+ ;; at $1000.
+ lda #%10110000
+ sta PPU::CONTROL
+
+ ;; 4: show sprites; 3: show background; 2: show sprites in leftmost pixels
+ ;; on the screen; 1: same as 2 but for background.
+ lda #%00011110
+ sta PPU::MASK
+
+@main_game_loop:
+ jsr Joypad::read
+ jsr Player::Movement::update
+ jsr Player::Sprite::update
+
+ ;; This is a hand-shake between the code on `main` and the code on the
+ ;; `nmi`. See Game::flags for more.
+ SET_RENDER_FLAG
+@wait_for_render:
+ bit Game::flags
+ bmi @wait_for_render
+
+ ;; Rendering is done, we can perform another iteration of the loop!
+ jmp @main_game_loop
+.endproc
+
+;; init_palettes copies all the palettes for our game into the proper PPU
+;; address.
+.proc init_palettes
+ PPU_ADDR $3F00
+
+ ldx #0
+@load_palettes_loop:
+ lda palettes, x
+ sta PPU::DATA
+ inx
+ cpx #$20
+ bne @load_palettes_loop
+ rts
+palettes:
+ ;; Background
+ .byte $0F, $12, $22, $32
+ .byte $0F, $00, $28, $30
+ .byte $0F, $28, $16, $2D
+ .byte $0F, $28, $16, $2D
+
+ ;; Foreground
+ .byte $0F, $00, $05, $30
+ .byte $0F, $00, $00, $00
+ .byte $0F, $00, $00, $00
+ .byte $0F, $00, $00, $00
+.endproc
+
+;; init_nametable loads the relevant data to the nametable that is then going to
+;; be used in order to build up the background.
+.proc init_nametable
+ bit PPU::STATUS
+
+ ;; Big stars.
+ WRITE_PPU_DATA $20C8, $02
+ WRITE_PPU_DATA $227A, $02
+
+ ;; Small stars.
+ WRITE_PPU_DATA $20B9, $04
+ WRITE_PPU_DATA $21CE, $04
+ WRITE_PPU_DATA $21BA, $04
+ WRITE_PPU_DATA $22B8, $04
+ WRITE_PPU_DATA $22E7, $04
+
+ ;; Select palette 1 for one of the small stars, giving it a red-ish look.
+ WRITE_PPU_DATA $23CE, %00000001
+
+ rts
+.endproc
diff --git a/space/src/states/game.s b/space/src/states/game.s
new file mode 100644
index 0000000..ca8a49b
--- /dev/null
+++ b/space/src/states/game.s
@@ -0,0 +1,21 @@
+.scope Game
+ ;; Contains relevant flags for the execution of the game:
+ ;; - 7: set to 1 whenever the game logic is over and we can start
+ ;; rendering; set to 0 when rendering is done.
+ ;; - 6-0: unused.
+ flags = $20
+.endscope
+
+;; SET_RENDER_FLAG sets the render bit on Game::flags to 1.
+.macro SET_RENDER_FLAG
+ lda #%10000000
+ ora Game::flags
+ sta Game::flags
+.endmacro
+
+;; UNSET_RENDER_FLAG sets the render bit on Game::flags to 0.
+.macro UNSET_RENDER_FLAG
+ lda #%01111111
+ and Game::flags
+ sta Game::flags
+.endmacro
diff --git a/space/src/states/player.s b/space/src/states/player.s
new file mode 100644
index 0000000..a43f61e
--- /dev/null
+++ b/space/src/states/player.s
@@ -0,0 +1,356 @@
+;;;
+;; Player state: movement, animation, etc. The following memory addresses are
+;; reserved for the player:
+;; -> $30-$3F: internal data.
+;; -> $0200-$0207: OAM data.
+;;;
+.scope Player
+ ;; TODO: change names of pos_x and signed_x et al
+ m_pos_x = $30
+ m_pos_y = $31
+ m_velocity_x = $32
+ m_velocity_y = $33
+ m_target_velocity_x = $34
+ m_target_velocity_y = $35
+ m_signed_x = $36 ; NOTE !
+ m_signed_y = $38 ; NOTE !
+
+ ;; Initializes the player by initializing its internal data and loading some
+ ;; values of the sprite itself.
+ .proc init
+ ;; Initialize position + subpixel.
+ lda #$B0
+ sta m_signed_y
+ lda #$00
+ sta m_signed_y + 1
+ lda #$7A
+ sta m_signed_x
+ lda #$F0
+ sta m_signed_x + 1
+
+ ;; Initialize velocity.
+ lda #0
+ sta m_velocity_x
+ sta m_velocity_y
+ sta m_target_velocity_x
+ sta m_target_velocity_y
+
+ ;; The player itself is built with two identical sprites placed side by
+ ;; side, where the second one is flipped horizontally. Thus, the player
+ ;; takes up the first two slots on OAM data ($0200-$0207). Here we only
+ ;; need to select the sprite and the attributes, since the position will
+ ;; be updated on each game loop. Hence, here we select the sprite
+ ;; located at #0 on the pattern table, and then we set for the second
+ ;; one the horizontal flip bit for the attributes.
+ lda #0
+ sta $0201 ; First sprite select.
+ sta $0205 ; Second sprite select.
+ lda #%00000000
+ sta $0202 ; First sprite attributes.
+ lda #%01000000
+ sta $0206 ; Second sprite attributes.
+
+ rts
+ .endproc
+
+ ;; Contains all the subroutines that have to deal with computing the
+ ;; movement of the sprite depending on the previous state, the buttons being
+ ;; pressed, the current position, etc.
+ .scope Movement
+ .proc update
+ jsr set_target_velocity
+ jsr accelerate
+ jsr apply_velocity
+ jsr position_to_coordinates
+ rts
+ .endproc
+
+ ;; Set the target velocity for the X and Y axis given the current button
+ ;; presses.
+ .proc set_target_velocity
+ ;; The target velocity depends on whether B was pressed or not.
+ ;; Depending on that we will set the x index to point to one element
+ ;; of the velocity tables below or to another.
+ ldx #0
+ lda #Joypad::BUTTON_B
+ and Joypad::m_buttons1
+ beq @target_check_right
+ inx
+ @target_check_right:
+ ;; The algorithm from here on is pretty straight-forward. Check the
+ ;; right button. If it was not pressed jump to the left check. If it
+ ;; was pressed load the target velocity on the x-axis from the given
+ ;; table and jump into the arrow-up check.
+ lda #Joypad::BUTTON_RIGHT
+ and Joypad::m_buttons1
+ beq @target_check_left
+ lda positive_velocity, x
+ sta m_target_velocity_x
+ jmp @target_check_up
+
+ @target_check_left:
+ ;; Similar to before: if it was not pressed, then set the target
+ ;; velocity to 0, otherwise set the proper value and jump to the up
+ ;; check.
+ lda #Joypad::BUTTON_LEFT
+ and Joypad::m_buttons1
+ beq @target_no_x
+ lda negative_velocity, x
+ sta m_target_velocity_x
+ jmp @target_check_up
+
+ @target_no_x:
+ ;; None of the buttons on the X-axis were pressed. Set the target
+ ;; velocity to 0.
+ lda #0
+ sta m_target_velocity_x
+ ;; NOTE: walkthrough
+
+ @target_check_up:
+ ;; Same as before but we return early if it was pressed, otherwise
+ ;; we go into the arrow-down check.
+ lda #Joypad::BUTTON_UP
+ and Joypad::m_buttons1
+ beq @target_check_down
+ lda negative_velocity, x
+ sta m_target_velocity_y
+ rts
+
+ @target_check_down:
+ ;; If down was not pressed, go to the "no_y" case, otherwise return
+ ;; early after setting the proper Y target velocity.
+ lda #Joypad::BUTTON_DOWN
+ and Joypad::m_buttons1
+ beq @target_no_y
+ lda positive_velocity, x
+ sta m_target_velocity_y
+ rts
+
+ @target_no_y:
+ ;; None of the buttons on the Y-axis were pressed. Set the target
+ ;; velocity to 0.
+ lda #0
+ sta m_target_velocity_y
+ rts
+
+ ;; TODO
+ positive_velocity:
+ ;; $18: 0001 | 1000
+ ;; $28: 0010 | 1000
+ .byte $18, $28
+ negative_velocity:
+ ;; $E8: 1110 | 1000
+ ;; $D8: 1101 | 1000
+ .byte $E8, $D8
+ .endproc
+
+ ;; TODO: give it a closer look
+ .proc accelerate
+ lda m_velocity_x
+ sec
+ sbc m_target_velocity_x
+ bne @accelerate_x_check_greater
+ jmp @accelerate_y
+ @accelerate_x_check_greater:
+ bmi @accelerate_x_check_lesser
+ dec m_velocity_x
+ jmp @accelerate_y
+ @accelerate_x_check_lesser:
+ inc m_velocity_x
+
+ @accelerate_y:
+ lda m_velocity_y
+ sec
+ sbc m_target_velocity_y
+ bne @accelerate_y_check_greater
+ rts
+ @accelerate_y_check_greater:
+ bmi @accelerate_y_check_lesser
+ dec m_velocity_y
+ rts
+ @accelerate_y_check_lesser:
+ inc m_velocity_y
+ rts
+ .endproc
+
+ ;; TODO: give it a closer look
+ .proc apply_velocity
+ lda m_velocity_x
+ bmi @apply_negative_velocity_x
+
+ clc
+ adc m_signed_x
+ sta m_signed_x
+ lda #0 ;NOTE: adding possible carry!
+ adc m_signed_x + 1
+ sta m_signed_x + 1
+ jmp @apply_velocity_y
+
+ @apply_negative_velocity_x:
+ lda #0
+ sec
+ sbc m_velocity_x
+ sta $00
+ lda m_signed_x
+ sec
+ sbc $00
+ sta m_signed_x
+ lda m_signed_x + 1
+ sbc #0
+ sta m_signed_x + 1
+ ;; NOTE: walkthrough
+
+ @apply_velocity_y:
+ lda m_velocity_y
+ bmi @apply_negative_velocity_y
+
+ clc
+ adc m_signed_y
+ sta m_signed_y
+ lda #0
+ adc m_signed_y + 1
+ sta m_signed_y + 1
+ rts
+
+ @apply_negative_velocity_y:
+ lda #0
+ sec
+ sbc m_velocity_y
+ sta $00
+ lda m_signed_y
+ sec
+ sbc $00
+ sta m_signed_y
+ lda m_signed_y + 1
+ sbc #0
+ sta m_signed_y + 1
+ rts
+ .endproc
+
+ .proc position_to_coordinates
+ jsr position_to_coordinates_x
+ jsr position_to_coordinates_y
+
+ rts
+ .endproc
+
+ .proc position_to_coordinates_x
+ ;; Convert the fixed point position coordinate into screen coordinates
+ lda m_signed_x
+ sta $00
+ lda m_signed_x + 1
+ sta $01
+ lsr $01
+ ror $00
+ lsr $01
+ ror $00
+ lsr $01
+ ror $00
+ lsr $01
+ ror $00
+ ; Assume that everything is fine and save the sprite position
+ lda $00
+ sta m_pos_x
+
+ lda m_velocity_x
+ bmi @position_from_negative_velocity
+
+ lda $01
+ bne @bound_upper_x
+ lda $00
+ cmp #239
+ bcs @bound_upper_x
+ rts
+ @bound_upper_x:
+ lda #$EF
+ sta m_pos_x
+ lda #$0E
+ sta m_signed_x + 1
+ lda #$F0
+ sta m_signed_x
+ lda #0
+ sta m_velocity_x
+ rts
+ @position_from_negative_velocity:
+ lda m_signed_x + 1
+ bmi @bound_lower_x
+ rts
+ @bound_lower_x:
+ lda #0
+ sta m_signed_x
+ sta m_signed_x + 1
+ sta m_pos_x
+ sta m_velocity_x
+ rts
+ .endproc
+
+ .proc position_to_coordinates_y
+ ;; Convert the fixed point position coordinate into screen coordinates
+ lda m_signed_y
+ sta $00
+ lda m_signed_y + 1
+ sta $01
+ lsr $01
+ ror $00
+ lsr $01
+ ror $00
+ lsr $01
+ ror $00
+ lsr $01
+ ror $00
+ ; Assume that everything is fine and save the sprite position
+ lda $00
+ sta m_pos_y
+
+ lda m_velocity_y
+ bmi @position_from_negative_velocity_y
+
+ lda $01
+ bne @bound_upper_y
+ lda $00
+ cmp #239
+ bcs @bound_upper_y
+ rts
+ @bound_upper_y:
+ lda #$EF
+ sta m_pos_y
+ lda #$0E
+ sta m_signed_y + 1
+ lda #$F0
+ sta m_signed_y
+ lda #0
+ sta m_velocity_y
+ rts
+ @position_from_negative_velocity_y:
+ lda m_signed_y + 1
+ bmi @bound_lower_y
+ rts
+ @bound_lower_y:
+ lda #0
+ sta m_signed_y
+ sta m_signed_y + 1
+ sta m_pos_y
+ sta m_velocity_y
+ rts
+ .endproc
+ .endscope
+
+ ;; Functions related to the rendering and manipulation of the sprite itself.
+ .scope Sprite
+ ;; Update the sprite on OAM memory according to what we have in the
+ ;; internal data stored in $30-$3F.
+ .proc update
+ lda m_pos_y
+ sta $200
+ sta $204
+
+ lda m_pos_x
+ sta $203
+ clc
+ adc #8
+ sta $207
+
+ rts
+ .endproc
+ .endscope
+.endscope
diff --git a/space/src/vectors/irq.s b/space/src/vectors/irq.s
new file mode 100644
index 0000000..2be3a94
--- /dev/null
+++ b/space/src/vectors/irq.s
@@ -0,0 +1,6 @@
+.segment "CODE"
+
+;; Interrupt Requests handler.
+irq:
+ ;; Nothing to do for us here :)
+ rti
diff --git a/space/src/vectors/nmi.s b/space/src/vectors/nmi.s
new file mode 100644
index 0000000..4bc3dde
--- /dev/null
+++ b/space/src/vectors/nmi.s
@@ -0,0 +1,14 @@
+;; Non-Maskable Interrupts handler.
+nmi:
+ ;; As mentioned on the `main` subroutine, rendering will be skipped until
+ ;; the proper flag is set.
+ bit $20
+ bpl @next
+
+ ;; We can start rendering stuff.
+ OAM_WRITE_SPRITES
+
+ ;; And unset the render flag so the `main` code is unblocked.
+ UNSET_RENDER_FLAG
+@next:
+ rti
diff --git a/space/src/vectors/reset.s b/space/src/vectors/reset.s
new file mode 100644
index 0000000..afc9fa5
--- /dev/null
+++ b/space/src/vectors/reset.s
@@ -0,0 +1,76 @@
+.segment "CODE"
+
+reset:
+ ;; Ignore IRQs and disable decimal mode.
+ sei
+ cld
+
+ ;; Disable APU frame IRQ.
+ ldx #$40
+ stx APU::FRAME_COUNTER
+
+ ;; Set up the stack register with the proper value.
+ ldx #$ff
+ txs
+
+ ;; And now disable, in this order, NMI, rendering and DMC IRQs. Note that
+ ;; `x` was set to $ff, so increasing it by one results in a zero, which is
+ ;; the value then stored in the aforementioned memory locations.
+ inx
+ stx PPU::CONTROL
+ stx PPU::MASK
+ stx APU::DMC
+
+ ;;;
+ ;; NOTE: If you are using a mapper which needs some special configuration,
+ ;; now it would be a good time set it up. I am not using a special mapper,
+ ;; so there's nothing from me to do here.
+ ;;;
+
+ ;; First of the two VBLANK waits.
+@vblankwait1:
+ bit PPU::STATUS
+ bpl @vblankwait1
+
+ ;; Clear RAM memory. Notice that we are not clearing $200-$2ff, this is done
+ ;; later.
+ ldx #0
+ lda #0
+@ram_reset_loop:
+ sta $000, x
+ sta $100, x
+ sta $300, x
+ sta $400, x
+ sta $500, x
+ sta $600, x
+ sta $700, x
+ inx
+ bne @ram_reset_loop
+
+ ;; Reset sprites by moving them off-screen.
+ lda #$ef
+@sprite_reset_loop:
+ sta $200, x
+ inx
+ bne @sprite_reset_loop
+
+ ;; Write "sprites" into OAM.
+ OAM_WRITE_SPRITES
+
+ ;; Last VBLANK wait.
+@vblankwait2:
+ bit PPU::STATUS
+ bpl @vblankwait2
+
+ ;; Reset palettes.
+ PPU_ADDR $3F00
+
+ lda #$0F
+ ldx #$20
+@palettes_reset_loop:
+ sta PPU::DATA
+ dex
+ bne @palettes_reset_loop
+
+ ;; Jump into the main subroutine.
+ jmp main