aboutsummaryrefslogtreecommitdiff
path: root/tests/src
diff options
context:
space:
mode:
authorMiquel Sabaté Solà <mikisabate@gmail.com>2025-01-10 12:49:15 +0100
committerMiquel Sabaté Solà <mikisabate@gmail.com>2025-01-10 12:49:15 +0100
commit7882e4474b77dfe4036f170cfce0e40b4f10ae93 (patch)
tree0e2dce224d853eaff638abef0781b7254557aec6 /tests/src
parent126e5eba61c3ca2b74fb9e7d52c91d90e8315a0c (diff)
downloadtools.nes-7882e4474b77dfe4036f170cfce0e40b4f10ae93.tar.gz
tools.nes-7882e4474b77dfe4036f170cfce0e40b4f10ae93.zip
tests: Bundle end-to-end tests into a tarball
This way I can simply compress all the examples to traverse with a single file, and thus no longer polluting this repository from code from other repositories. Signed-off-by: Miquel Sabaté Solà <mikisabate@gmail.com>
Diffstat (limited to 'tests/src')
-rw-r--r--tests/src/chr-ram.s279
-rw-r--r--tests/src/flicker/flicker.s280
-rw-r--r--tests/src/shared/diskun.s96
-rw-r--r--tests/src/shared/joypad.s86
-rw-r--r--tests/src/sprite.s618
5 files changed, 0 insertions, 1359 deletions
diff --git a/tests/src/chr-ram.s b/tests/src/chr-ram.s
deleted file mode 100644
index 401588d..0000000
--- a/tests/src/chr-ram.s
+++ /dev/null
@@ -1,279 +0,0 @@
-;;; From https://github.com/mssola/code.nes which will be open sourced soon
-;;; (pinky promess!).
-
-.segment "HEADER"
- .byte 'N', 'E', 'S', $1A
- .byte $08 ; 128KB of PRG-ROM (8 x 16KB)
- .byte $00 ; No CHR-ROM.
-
- .byte $20, $08 ; Mapper 2, horizontal mirroring, NES 2.0
-
- .byte $00
- .byte $00
- .byte $00
- .byte $07 ; 8192 (64 * 2^7) bytes CHR RAM, no battery
-
-.segment "VECTORS"
- .addr nmi, reset, irq
-
-.segment "BANK0"
-
-chr: .incbin "../assets/basic.chr"
-
-;;;
-;; We use the fixed bank for the core functionality.
-
-.segment "FIXED"
-
-;;;
-;; Bank switching. Not actually used here :D
-
-banktable:
- .byte $00, $01, $02, $03, $04, $05, $06
-
-m_current_bank = $00
-
-bankswitch:
- sty m_current_bank
-bankswitch_nosave:
- tya
- sta banktable, y
- rts
-
-;;;
-;; From here on the code is basically the same as `sprite.s`, but with a special
-;; twist that will be commented in.
-
-reset:
- sei
- cld
-
- ldx #$40
- stx $4017
-
- ldx #$ff
- txs
- inx
- stx $2000
- stx $2001
- stx $4010
-
-@vblankwait1:
- bit $2002
- bpl @vblankwait1
-
- 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
-
- lda #$ef
-@sprite_reset_loop:
- sta $200, x
- inx
- bne @sprite_reset_loop
-
- ;; NOTE: after sprites have been reset we can transfer data from PRG-ROM
- ;; into RAM so the rest of the code can assume that the data is there. This
- ;; is the main difference with the `sprite.s` example.
- jsr transfer_to_chr_ram
-
- lda #$00
- sta $2003
- lda #$02
- sta $4014
-
-@vblankwait2:
- bit $2002
- bpl @vblankwait2
-
- lda #$3F
- sta $2006
- lda #$00
- sta $2006
-
- lda #$0F
- ldx #$20
-@palettes_reset_loop:
- sta $2007
- dex
- bne @palettes_reset_loop
-
- jmp main
-
-;;;
-;; Transfer the CHR data from PRG-ROM into RAM.
-.proc transfer_to_chr_ram
- ;;;
- ;; The code is pretty much taken from the NESDev wiki (see:
- ;; https://www.nesdev.org/wiki/CHR_ROM_vs._CHR_RAM).
-
- ;; First we set a 16-bit pointer that points `chr`, which contains the
- ;; actual data. This pointer will be stored at $00-$01, which is probably
- ;; not ideal, but this is just an example.
- lda #<chr
- sta $00
- lda #>chr
- sta $01
-
- ;; Load the destination address into the PPU. We have to initialize both
- ;; pattern tables, one starting at $0000, and the other at $1000. Hence we
- ;; start by simply pointing at PPU address $0000, and the loop will simply
- ;; fill both tables from here.
- ldy #0
- sty $2006
- sty $2006
-
- ;; - x contains the number of 256-byte pages to copy.
- ;; - y will index within the page ($00-$FF).
- ldx #32
-loop:
- ;; First part of the loop: copy each byte of the current page.
- lda ($00), y
- sta $2007
- iny
- bne loop
-
- ;; Go to the next page and repeat the first part of the loop.
- inc $01
- dex
- bne loop
-
- rts
-.endproc
-
-;;;
-;; And from here on it's pretty much copied from `sprite.s`.
-
-.proc main
- jsr init_palettes
- jsr init_nametable
- jsr init_sprites
-
- cli
- lda #%10110000
- sta $2000
- lda #%00011110
- sta $2001
-
-@main_game_loop:
- lda #%10000000
- ora $20
- sta $20
-@wait_for_render:
- bit $20
- bmi @wait_for_render
-
- jmp @main_game_loop
-.endproc
-
-.proc init_palettes
- lda #$3F
- sta $2006
- lda #$00
- sta $2006
-
- ldx #0
-@load_palettes_loop:
- lda palettes, x
- sta $2007
- inx
- cpx #$20
- bne @load_palettes_loop
- rts
-palettes:
- .byte $0F, $12, $22, $32
- .byte $0F, $00, $28, $30
- .byte $0F, $28, $16, $2D
- .byte $0F, $28, $16, $2D
-
- .byte $0F, $00, $05, $30
- .byte $0F, $00, $00, $00
- .byte $0F, $00, $00, $00
- .byte $0F, $00, $00, $00
-.endproc
-
-.macro WRITE_PPU_DATA address, value
- bit $2002
- lda #.HIBYTE(address)
- sta $2006
- lda #.LOBYTE(address)
- sta $2006
- lda #value
- sta $2007
-.endmacro
-
-.proc init_nametable
- bit $2002
-
- WRITE_PPU_DATA $20C8, $02
- 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
- WRITE_PPU_DATA $227A, $02
-
- WRITE_PPU_DATA $23CE, %00000001
-
- rts
-.endproc
-
-.proc init_sprites
- NUM_SPRITES = 2
-
- ldx #$00
-@load_sprites_loop:
- lda initial_sprite_data, x
- sta $0200, x
- inx
- cpx #(4 * NUM_SPRITES)
- bne @load_sprites_loop
- rts
-initial_sprite_data:
- .byte $B0, $00, %00000000, $7A
- .byte $B0, $00, %01000000, $82
-.endproc
-
-nmi:
- bit $20
- bpl @next
-
- pha
- txa
- pha
- tya
- pha
-
- lda #$00
- sta $2003
- lda #$02
- sta $4014
-
- bit $2002
- lda #$00
- sta $2005
- sta $2005
-
- lda #%01111111
- and $20
- sta $20
-
- pla
- tay
- pla
- tax
- pla
-@next:
- rti
-
-irq:
- rti
diff --git a/tests/src/flicker/flicker.s b/tests/src/flicker/flicker.s
deleted file mode 100644
index 82dcfb7..0000000
--- a/tests/src/flicker/flicker.s
+++ /dev/null
@@ -1,280 +0,0 @@
-;;;
-;; This is a simple example of sprite cycling, so sprites don't disappear on a
-;; scanline overflow. This example is extremely simple, and on a real game you'd
-;; take into account lots of things that would make this process less tedious
-;; and less cycle consuming.
-;; Taken from https://github.com/mssola/code.nes.
-
-.segment "HEADER"
- .byte 'N', 'E', 'S', $1A
-
- .byte $02, $01
-
- .byte $00
- .byte $00
-
-.segment "VECTORS"
- .addr nmi, reset, irq
-
-.segment "CHARS"
-.incbin "../../assets/diskun.chr"
-
-.segment "CODE"
-
-.include "../shared/diskun.s"
-
-.proc main
- jsr Diskun::init_palettes
- jsr init_sprites
-
- cli
- lda #%10010000
- sta $2000 ; PPUCTRL
- lda #%00011110
- sta $2001 ; PPUMASK
-
-@main_game_loop:
- ;; NOTE: the logic is pretty simple: read the pad, move the player
- ;; accordingly, and apply the flickering effect.
- jsr joypad_read
- jsr Diskun::update
-
- ;; NOTE: comment this `jsr` out if you want to see what happens if no
- ;; flickering effect is applied (spoiler alert: the last character will
- ;; suddenly disappear :P).
- ;;
- ;; NOTE: this function is called mindlessly. In a real game you'd try to
- ;; detect scanline overflows, or you will try to be more careful with sprite
- ;; priorities and apply the flickering more carefully. Here it's applied for
- ;; (almost) every sprite on each frame and we roll with it.
- jsr apply_flicker
-
- lda #%10000000
- ora $20
- sta $20
-@wait_for_render:
- bit $20
- bmi @wait_for_render
-
- jmp @main_game_loop
-.endproc
-
-
-;;;
-;; NOTE: this is the actual meat of the example :D
-.proc apply_flicker
- ;; We will store a 16-bit pointer to the first sprite that can be flickered.
- ;; In this case, we will point to the first sprite from the first NPC. That
- ;; is, we suppose that, for whatever reason, we don't want to apply the
- ;; flickering effect for the player.
- lda #$10
- sta $40
- lda #$02
- sta $41
-
- ;;;
- ;; The algorithm is really straight-forward here: we will simply cycle the
- ;; sprites starting from the one being pointed at $40-$41 until the last
- ;; sprite. The main idea is that the OAM will show sprites on a scanline in
- ;; order. Hence, those sprites that are left beyond the eigth on a scanline
- ;; will suddenly disappear. Flickering is a technique by which sprites are
- ;; rotated from OAM priority, and hence they won't disappear always, but
- ;; just for a small amount of time: just enough to see them, creating a
- ;; flickering effect.
- ;;
- ;; For this, we will reserve four bytes (1 sprite) for auxiliary temporary
- ;; values ($42-$45) which will then be used in order to carry values along
- ;; the cycle.
-
- ;; We initialize the auxiliary bytes with the data from the last sprite to
- ;; be iterated since these are the values to be stored for the first sprite
- ;; now (that is, the last element is now the first one).
- lda $24C
- sta $42
- lda $24D
- sta $43
- lda $24E
- sta $44
- lda $24F
- sta $45
-
-@loop:
- ;; For each of the four bytes from a sprite, pick the value stored on its
- ;; auxiliary byte and save the old value into the auxiliary byte afterwards.
- ;; This way the current byte holds the value from the previous sprite, but
- ;; its value will be carry on into the next sprite.
- .repeat 4, I
- ldy #I
- lda $42, y
- pha
- lda ($40), y
- sta $42, y
- pla
- sta ($40), y
- .endrepeat
-
- ;; And move the sprite pointer 4 bytes (1 sprite). Note that we only need to
- ;; move the low byte since we know beforehand that everything will be inside
- ;; of the $2xx range.
- lda $40
- clc
- adc #4
- sta $40
-
- ;; We know that the last byte from the last sprite is held at $24F. Thus, if
- ;; the sprite pointer is already passed this point, we can break the loop.
- ;; Otherwise just carry on.
- cmp #$50
- bne @loop
-
- rts
-.endproc
-
-;;;
-;; NOTE: and from here on stuff that is not relevant for sprite flickering.
-
-nmi:
- bit $20
- bpl @next
-
- pha
- txa
- pha
- tya
- pha
-
- jsr Diskun::nmi_update
-
- lda #$00
- sta $2003 ; OAMADDR
- lda #$02
- sta $4014 ; OAMDMA
-
- bit $2002 ; PPUSTATUS
- lda #$00
- sta $2005 ; PPUSCROLL
- sta $2005 ; PPUSCROLL
-
- lda #%01111111
- and $20
- sta $20
-
- pla
- tay
- pla
- tax
- pla
-@next:
- rti
-
-reset:
- sei
- cld
-
- ldx #$40
- stx $4017 ; APU Frame Counter
-
- ldx #$ff
- txs
-
- inx
- stx $2000 ; PPUCTRL
- stx $2001 ; PPUMASK
- stx $4010 ; APU DMC
-
-@vblankwait1:
- bit $2002 ; PPUSTATUS
- bpl @vblankwait1
-
- 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 ; if x overflows back to #00, then we are done.
-
- lda #$ef
-@sprite_reset_loop:
- sta $200, x
- inx
- bne @sprite_reset_loop
-
- lda #$00
- sta $2003 ; OAMADDR
- lda #$02
- sta $4014 ; OAMDMA
-
-@vblankwait2:
- bit $2002 ; PPUSTATUS
- bpl @vblankwait2
-
- lda #$3F
- sta $2006 ; PPUADDR
- lda #$00
- sta $2006 ; PPUADDR
-
- lda #$0F
- ldx #$20
-@palettes_reset_loop:
- sta $2007 ; PPUDATA
- dex
- bne @palettes_reset_loop
-
- jmp main
-
-irq:
- rti
-
-.proc init_sprites
- NUM_SPRITES = 20
-
- lda #$40
- sta Diskun::m_screen_y
- lda #$46
- sta Diskun::m_screen_x
-
- ldx #$00
-@load_sprites_loop:
- lda initial_sprite_data, x
- sta $0200, x
- inx
- cpx #(4 * NUM_SPRITES)
- bne @load_sprites_loop
- rts
-initial_sprite_data:
- ;; $200-$20F
- .byte $40, $01, %00000000, $46
- .byte $40, $01, %01000000, $4E
- .byte $48, $11, %00000000, $46
- .byte $48, $11, %01000000, $4E
-
- ;; $210-$21F
- .byte $60, $01, %00000000, $68
- .byte $60, $01, %01000000, $70
- .byte $68, $11, %00000000, $68
- .byte $68, $11, %01000000, $70
-
- ;; $220-$22F
- .byte $60, $01, %00000000, $7A
- .byte $60, $01, %01000000, $82
- .byte $68, $11, %00000000, $7A
- .byte $68, $11, %01000000, $82
-
- ;; $230-$23F
- .byte $60, $01, %00000000, $8C
- .byte $60, $01, %01000000, $94
- .byte $68, $11, %00000000, $8C
- .byte $68, $11, %01000000, $94
-
- ;; $240-$24F
- .byte $60, $01, %00000000, $9E
- .byte $60, $01, %01000000, $A6
- .byte $68, $11, %00000000, $9E
- .byte $68, $11, %01000000, $A6
-.endproc
diff --git a/tests/src/shared/diskun.s b/tests/src/shared/diskun.s
deleted file mode 100644
index b644a83..0000000
--- a/tests/src/shared/diskun.s
+++ /dev/null
@@ -1,96 +0,0 @@
-;;; Simple sprite that can be moved with the dpad easily. Use it to have
-;;; something funny moving.
-;; Taken from https://github.com/mssola/code.nes.
-
-.include "joypad.s"
-
-.scope Diskun
- m_screen_x = $30
- m_screen_y = $31
-
- .proc init_palettes
- lda #$3F
- sta $2006 ; PPUADDR
- lda #$00
- sta $2006 ; PPUADDR
-
- ldx #0
- @load_palettes_loop:
- lda palettes, x
- sta $2007 ; PPUDATA
- inx
- cpx #$20
- bne @load_palettes_loop
- rts
- palettes:
- DEFAULT_COLOR = $11
-
- ;; Background
- .byte DEFAULT_COLOR, $36, $17, $0F
- .byte DEFAULT_COLOR, $00, $00, $00
- .byte DEFAULT_COLOR, $00, $00, $00
- .byte DEFAULT_COLOR, $00, $00, $00
-
- ;; Foreground
- .byte DEFAULT_COLOR, $28, $0F, $30
- .byte DEFAULT_COLOR, $00, $00, $00
- .byte DEFAULT_COLOR, $00, $00, $00
- .byte DEFAULT_COLOR, $00, $00, $00
-
- rts
- .endproc
-
- .proc update
- lda #Joypad::BUTTON_UP
- and Joypad::m_buttons1
- beq @check_down
-
- dec Diskun::m_screen_y
- dec Diskun::m_screen_y
- jmp @check_left
- @check_down:
- lda #Joypad::BUTTON_DOWN
- and Joypad::m_buttons1
- beq @check_left
-
- inc Diskun::m_screen_y
- inc Diskun::m_screen_y
- @check_left:
- lda #Joypad::BUTTON_LEFT
- and Joypad::m_buttons1
- beq @check_right
-
- dec Diskun::m_screen_x
- dec Diskun::m_screen_x
- @check_right:
- lda #Joypad::BUTTON_RIGHT
- and Joypad::m_buttons1
- beq @end
-
- inc Diskun::m_screen_x
- inc Diskun::m_screen_x
- @end:
- rts
- .endproc
-
- .proc nmi_update
- lda Diskun::m_screen_x
- sta $203
- sta $20B
- clc
- adc #8
- sta $207
- sta $20F
-
- lda Diskun::m_screen_y
- sta $200
- sta $204
- clc
- adc #8
- sta $208
- sta $20C
-
- rts
- .endproc
-.endscope
-
diff --git a/tests/src/shared/joypad.s b/tests/src/shared/joypad.s
deleted file mode 100644
index afd0a68..0000000
--- a/tests/src/shared/joypad.s
+++ /dev/null
@@ -1,86 +0,0 @@
-;; Taken from https://github.com/mssola/code.nes.
-
-.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
-
- ;; After running a `joypad_read_*` function these two variables will contain
- ;; the given result.
- m_buttons1 = $22
- m_buttons2 = $23
-.endscope
-
-;;;
-;; Read the first joypad. This method is fast but it might be vulnerable to the
-;; DPCM bug (see: https://www.nesdev.org/wiki/Controller_reading_code).
-joypad_unsafe_read:
- ldx #$00
- ;; NOTE: fallthrough
-
-;;;
-;; Read the joypad as indexed by the X register (0 for controller 1; 1 for
-;; controller 2). This method is fast but it might be vulnerable to the DPCM bug
-;; (see: https://www.nesdev.org/wiki/Controller_reading_code).
-joypad_unsafe_read_x:
- ;; Start the latch process.
- lda #$01
- sta Joypad::JOYPAD1
- sta Joypad::m_buttons1, x ; Bit as a guard for the loop below.
- lsr a
- sta Joypad::JOYPAD1
-
- ;; Now the joypad is ready to accept reads.
-@joypad_unsafe_read_x_loop:
- lda Joypad::JOYPAD1, x
- and #%00000011 ; Ignore bits other than controller.
- cmp #$01 ; Set carry if and only if nonzero.
- rol Joypad::m_buttons1, x ; Carry -> bit 0; bit 7 -> Carry
- bcc @joypad_unsafe_read_x_loop
- rts
-
-;;;
-;; Safely read the first controller via a re-read algorithm.
-joypad_read:
- ldx #$00
-
- ;; NOTE: uncomment these two lines to also read safely the second
- ;; controller.
- ;;
- ;; jsr joypad_read_x
- ;; inx
-
- ;; NOTE: fallthrough
-
-;;;
-;; Safely read via a re-read algorithm the joypad as indexed by the X register
-;; (0 for controller 1; 1 for controller 2).
-joypad_read_x:
- jsr joypad_unsafe_read_x
-
- ;; The main idea around a re-read algorithm is that you read the controller
- ;; "unsafely" once, then you do it again and compare both reads. If they
- ;; were the same then we are on the safe side. Otherwise we would need to
- ;; loop until we get two identical reads. This sounds bad but in practice
- ;; it's not so much (and hey, if it worked for Super Mario Bros. 3, it
- ;; should work for us too :P). Otherwise there is the algorithm via OAM DMA,
- ;; but it sure is tricky.
-@joypad_read_x_reread:
- lda Joypad::m_buttons1, x
- pha
- jsr joypad_unsafe_read_x
- pla
- cmp Joypad::m_buttons1, x
- bne @joypad_read_x_reread
-
- rts
diff --git a/tests/src/sprite.s b/tests/src/sprite.s
deleted file mode 100644
index d57426d..0000000
--- a/tests/src/sprite.s
+++ /dev/null
@@ -1,618 +0,0 @@
-;;; NOTE: taken from https://github.com/mssola/code.nes; which I will open
-;;; source sooner rather than later.
-;;;
-;; Show a sprite to the screen! This example also contains fully detailed
-;; explanations on each section on how an NES game is initialized and stored.
-
-;;;
-;; The iNES is the de facto standard for the distribution of NES binary programs
-;; and it's compatible with the format used by NES cartridges themselves (used,
-;; even, by the Wii Virtual Console). The layout is composed by segments in
-;; memory, which are defined with the `.segment` macro and known by the compiler
-;; through a linker configuration. You can provide a configuration of your own,
-;; but bear in mind that compilers like `cc65` (the one used here, which is the
-;; most common) already provide a default configuration for the linker that
-;; glues a set of pretty common defined named segments. You can read about this
-;; in `cfg/nes.cfg` from inside your cc65 installation. Otherwise, I have also
-;; written linker configuration files for other examples: take a look, for
-;; instance, at the `config/unrom.cfg` file, which is used by the
-;; `basics/unrom.s` program.
-;;;
-
-;;;
-;; The "HEADER" is the first segment of any iNES binary and it contains basic
-;; information about what this "cartridge" requires in order to work. Besides
-;; basic identification, it defines some relevant things like PRG and CHR sizes,
-;; plus mapping if desired. See https://www.nesdev.org/wiki/NES_2.0#Header for
-;; documentation on this, or also: https://www.nesdev.org/neshdr20.txt.
-;;;
-.segment "HEADER"
- ;; The first thing to do is to define the magic "NES\0" string identifier
- ;; ($1A is the ASCII that MS-DOS wanted as end of string). Some people write
- ;; it in hexadecimal through and through, but cc65 already compiles
- ;; characters into their proper hexadecimal values, so there's no need to be
- ;; extra cryptic here.
- .byte 'N', 'E', 'S', $1A
-
- ;; The next two bytes define the size of the PRG and CHR ROMs in this order.
- ;; Hence, the next two bytes define a 32KB (2 x 16KB) of PRG-ROM, and 8KB
- ;; (1 x 8KB) of CHR-ROM.
- .byte $02
- .byte $01
-
- ;; Next we have two bytes for selecting a mapper. This is a huge topic (see
- ;; the NesDev wiki for this), but it basically refers to the fact that some
- ;; cartridges had specific requirements on how to place their data, or the
- ;; amount of it they required, or how they expected their data to be
- ;; mirrored in the memory, etc. During the life-time of the NES, and as
- ;; developers pushed the boundaries of the NES hardware, more intricate
- ;; mappers were used on the hardware of cartridges themselves. Thus, in
- ;; these two bytes we are telling the emulator: "hey, act as if this was a
- ;; cartridge that used this kind of mapper".
- .byte $00 ; Horizontal mirroring (good for vertical scrollers)
- .byte $00 ; No mapper nor special-case flags.
-
- ;; The previous are the mandatory bytes in order to get a "cartridge" going.
- ;; After this there are some other bytes you can put into the header, like
- ;; specifying the region (NTSC vs PAL), but for now this falls out of my
- ;; radar :-)
-
-;;;
-;; This is the segment where we tell the processor where to find the code for
-;; three important topics: the Non-Maskable Interrupts handler, the Reset
-;; handler, and the IRQ handler. If you look at the configuration from `cc65`
-;; that was provided on your installation (or if you are pesky enough to create
-;; one yourself), you will notice that these vector addresses are placed at the
-;; very end of memory ($fffa-ffff). The NES (and emulators) will look at these
-;; three last positions in memory to know where to jump for each case.
-;;;
-.segment "VECTORS"
- .addr nmi
- .addr reset
- .addr irq
-
-.segment "CODE"
-
-;;;
-;; The Reset handler is executed when the whole thing starts (i.e. the user has
-;; pressed either the reset or the power on buttons on the NES). Thus, this
-;; piece of code is pretty standard and an implementation is even given in the
-;; NesDev wiki which is pretty much followed by everyone as I could see. The
-;; main idea is to leave the hardware in a known state and then jump into the
-;; main game subroutine.
-;;;
-reset:
- ;; We first instruct the NES to disable everything. That is, we don't want
- ;; any pesky interrupt to make us jump into the `nmi` section, for example,
- ;; before we have configured everything.
-
- ;; Ignore IRQs and disable decimal mode (the NES 6502 chip, for copyright
- ;; issues or legal reasons against MOS that I don't fully know nor care,
- ;; does not have decimal mode anyway, but it's considered good practice).
- sei
- cld
-
- ;; Disable APU frame IRQ. This is the first instance we see of Memory-Mapped
- ;; I/O. This is a core concept in NES programming and, to sum things up, the
- ;; memory range $2000-$6000 is reserved to I/O operations, and each address
- ;; is reserved to a specific hardware operation. This is because the NES CPU
- ;; doesn't directly control the PPU nor other chips. In this case, ranges
- ;; $4000-$4017 control the APU (Audio Processing Unit). More precisely, the
- ;; $4017 address controls what is called the "Frame counter" from the APU
- ;; (https://www.nesdev.org/wiki/APU#Frame_Counter_($4017)). Setting #$40 to
- ;; it disables it completely, so we are in a known state. If we were to use
- ;; sound, at the end of the reset code we should enable it back. We do *not*
- ;; do it here because we don't need it.
- ldx #$40
- stx $4017 ; APU Frame Counter
-
- ;; Set up the stack register with the proper value (the stack will grow in
- ;; decreasing order from $01FF -> $0100).
- 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.
- ;;
- ;; The one on $4010 refers again to the APU (as described before), and it
- ;; directly controls the DMC. Again, if you wanted sound, you should enable
- ;; this back after the whole reset block.
- ;;
- ;; On the other side, the low addresses of $2000 control the PPU. In
- ;; particular, we disable NMIs from the PPU by setting the PPUCTRL address
- ;; ($2000) to zero, and we do the same for the PPUMASK ($2001). Don't worry
- ;; about them just now, we will go deeper down below.
- inx
- stx $2000 ; PPUCTRL
- stx $2001 ; PPUMASK
- stx $4010 ; 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.
- ;;;
-
- ;; At this point, we have to wait for the PPU to stabilize. This is
- ;; typically done by checking a flag from the PPUSTATUS address ($2002) and
- ;; waiting until the proper value is set by the PPU. Since this wait can
- ;; take a while, programmers typically put other initialization code here,
- ;; like sprite resetting and such.
- ;;
- ;; The PPUSTATUS memory address contains general information on the status
- ;; of the PPU, and is read-only. Moreover, reading from the PPUSTATUS has a
- ;; convenient side-effect: it resets the "address latch" for PPUADDR. We
- ;; will see how we can take advantage of this when we use the PPU properly
- ;; down the road.
-
- ;; First of the two waits.
-@vblankwait1:
- bit $2002 ; PPUSTATUS
- bpl @vblankwait1
-
- ;; The PPU has at least started, now we have a bunch of cycles for it to
- ;; stabilize, which will be properly announced through the PPUSTATUS memory
- ;; address. Instead of just waiting for it again, we will take the chance to
- ;; initialize more stuff.
-
- ;; One typical thing to do is to leave the RAM in a known state. That is, we
- ;; will set to 0 addresses $0000 - $07FF. Apparently there are some people
- ;; who say that doing this is bad because it will hide programming mistakes
- ;; (e.g. bad initialization code). So, if you are one of these people, you
- ;; can safely remove this loop. Otherwise let's get the RAM clean. That
- ;; being said, notice that we are skipping $200-$2ff. This is no mistake as
- ;; you will see below.
- 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 ; if x overflows back to #00, then we are done.
-
- ;; And now another thing we can do is to reset the sprites. We will reserve
- ;; space for sprites in $0200-02ff (the range we did not initialize when
- ;; cleaning up RAM). With this in mind we will perform a DMA process right
- ;; after this block of code that will take this memory range and bulk it
- ;; into PPU OAM space.
- ;;
- ;; "Resetting sprites" is just a matter of giving them a value which will
- ;; not bother us in the future. One way to do this is to set each value to
- ;; $ef, which will give each "sprite" off-screen Y-coordinates. How any of
- ;; this is the case will be shown whenever we deal with loading proper
- ;; sprites below.
- lda #$ef
-@sprite_reset_loop:
- sta $200, x
- inx
- bne @sprite_reset_loop
-
- ;; And write these resetted "sprites" into the PPU. How does this work?
- ;; Well, address $2003 has the OAMADDR. That is, from which position the PPU
- ;; should start the DMA process. In our case we set it to the very
- ;; beginning. Then, if you write to the OAMDMA memory address ($4014), you
- ;; will instruct the PPU to start a DMA process starting at $1XX*N. XX is
- ;; the byte given at the OAMADDR, thus XX = #$00; and N = 2 (see `lda
- ;; #$02`). Therefore, we are telling the PPU to start the DMA process from
- ;; $200. The PPU will assume that the following 256 bytes of memory are the
- ;; ones to be copied, resulting in a DMA copy of $200-$2ff, right where we
- ;; stored the sprite data in advance.
- lda #$00
- sta $2003 ; OAMADDR
- lda #$02
- sta $4014 ; OAMDMA
-
- ;; As advertised, the other wait. Once we are out of this loop, we are 100%
- ;; guaranteed to have a properly working PPU which is ready to render stuff
- ;; into the screen.
-@vblankwait2:
- bit $2002 ; PPUSTATUS
- bpl @vblankwait2
-
- ;; And we also reset the palettes. How is this done? Well, the $2006 memory
- ;; address is the PPU address (PPUADDR). This address is given byte by byte,
- ;; the most significat byte first. Thus, the four lines below store into
- ;; $2006 the value $3F00. This is the first address where palettes are
- ;; stored. Palettes are the answer from old systems like the NES to: how can
- ;; you display this amount of colors on screen without taking too much
- ;; memory? The NES allows developers to store eight palettes (four
- ;; background, four foreground), and each palette group four colors. This
- ;; way, whenever we want to draw a sprite or a piece of background, we don't
- ;; specify which colors to pick for each pixel, but we rather apply a
- ;; palette to a sprite or background tile definition.
- lda #$3F
- sta $2006 ; PPUADDR
- lda #$00
- sta $2006 ; PPUADDR
-
- ;; After setting the address for the first palette, now we loop #$20 times:
- ;; 8 palettes * 4 bytes per palette (1 byte per color) = 32 (#$20
- ;; hexadecimal). At each iteration we will write into $2007 the value #$0F.
- ;; This memory address is the PPUDATA, and #$0F corresponds to a black
- ;; color. In each iteration, therefore, we are telling the PPU that we want
- ;; a black color for all the colors from all palettes. Moreover, whenever
- ;; you write into PPUDATA, the memory address for the next store is
- ;; incremented by one automatically. This is why we don't have to index the
- ;; write or anything like that, but we can simply write to the same memory
- ;; address 32 times and it will be performed onto 32 consecutive positions.
- lda #$0F
- ldx #$20
-@palettes_reset_loop:
- sta $2007 ; PPUDATA
- dex
- bne @palettes_reset_loop
-
- ;; At this point everything is clear and with a state we know, now we can
- ;; jump into our main subroutine and start loading sprites, palettes, etc.;
- ;; and start the game proper.
- jmp main
-
-;;;
-;; This is our main subroutine. At this point we can assume that the hardware
-;; has already been set to a proper and defined state. So now we can load all
-;; the data we need for our game and enter the main game loop.
-;;;
-.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 init_sprites
-
- ;; Remember when we disabled rendering and NMI on the reset code? Now it's
- ;; time to enable them back. Things to note:
- ;; - PPUCTRL: we enable three bits:
- ;; - 4: background pattern table starts at $1000. The CHR is divided by
- ;; two pattern tables, and each table contains 256 8x8 tiles. In my
- ;; case, my sprite data is located at the first pattern table (thus
- ;; $0000, and that's why the 3rd bit from PPUCTRL is set to zero),
- ;; and the background data is on the second pattern table (that
- ;; starting at $1000). In the end this all depends on how you want
- ;; to structure your CHR file.
- ;; - 5: sprite size is 8x16. Set this to 0 if your sprites in the
- ;; character file are 8x8.
- ;; - 7: allow a NMI at the start of the vertical blanking interval.
- ;; That is, whenever the screen has displayed one frame, send us
- ;; an interrupt so we can generate the next one (see the code on
- ;; `nmi`).
- ;; - PPUMASK: we enable 4 bits:
- ;; - 1: Show background in leftmost pixels on the screen.
- ;; - 2: Show sprites in leftmost pixels on the screen.
- ;; - 3: Show background.
- ;; - 4: Show sprites.
- ;;
- ;; As for showing on the leftmost pixels, this might not be a good idea if
- ;; the game is a scroller, since the game might flicker depending on how you
- ;; manage the camera. This is out of scope.
- cli
- lda #%10110000
- sta $2000 ; PPUCTRL
- lda #%00011110
- sta $2001 ; PPUMASK
-
-@main_game_loop:
- ;;;
- ;; NOTE: enter here your game loop logic.
- ;;;
-
- ;; This is a hand-shake between the code on `main` and the code on the
- ;; `nmi`. That is, whenever we are done with the game loop logic, we set
- ;; this flag to indicate that the rendering is doable. After this, the code
- ;; on `main` will be blocked until this flag is unset by the `nmi` code,
- ;; which will indicate that it can proceed with another iteration of the
- ;; game loop. On the contrary, the `nmi` code will be blocked until this
- ;; flag is set, at which point it will start rendering and unset the flag
- ;; whenever that is done.
- lda #%10000000
- ora $20
- sta $20
-@wait_for_render:
- bit $20
- 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
- ;; Remember these four instructions? That's what we also did when resetting
- ;; palettes on the reset code. That is, we are preparing the PPU to write
- ;; data starting from $3f00.
- lda #$3F
- sta $2006 ; PPUADDR
- lda #$00
- sta $2006 ; PPUADDR
-
- ;; We are going to copy the eight palettes stored in the `palettes` tag into
- ;; PPU. This is done by indexing with the `x` register, which is incremented
- ;; until it reaches #$20 (32 in hexadecimal), which corresponds to 8
- ;; palettes * 4 bytes each (just like we did in the reset code).
- ldx #0
-@load_palettes_loop:
- lda palettes, x
- sta $2007 ; PPUDATA
- inx
- cpx #$20
- bne @load_palettes_loop
- rts
-palettes:
- ;; Let there be palettes! This is the data for all the palettes we have for
- ;; our game. Each hexadecimal value is a byte that represents a color (you
- ;; can use YY-CHR to quickly take a look at which color represents each
- ;; hexadecimal value, or use any of the tables that can be found online, or
- ;; even fceux has this with its "Palette editor" under "Tools"). There is,
- ;; though, one big catch: the first color from all palettes *must* be the
- ;; same. To be more precise, it's not like the NES will explode if you pick
- ;; other colors, but the NES *will* assume that the first color from all
- ;; palettes is the one found in $3F00 (that is, the first color from the
- ;; first palette). In this case, $0F will be this "default color". Moreover,
- ;; for this simple game we are not using all palettes, and that's why for
- ;; some of them all four bytes are simply zero'ed.
-
- ;; 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
-
-;; WRITE_PPU_DATA is a macro that will write into PPUADDR the given address and
-;; into PPUDATA the given byte value.
-.macro WRITE_PPU_DATA address, value
- bit $2002 ; PPUSTATUS
- lda #.HIBYTE(address)
- sta $2006 ; PPUADDR
- lda #.LOBYTE(address)
- sta $2006 ; PPUADDR
- lda #value
- sta $2007 ; PPUDATA
-.endmacro
-
-;; init_nametable loads the relevant data to the nametable that is then going to
-;; be used in order to build up the background.
-;;
-;; NOTE: this function is called after the PPU has been initialized, but NMIs
-;; are still disabled. This is important because you **cannot** write into PPU
-;; data outside of VBlank space (during `nmi` code). If you do so it will result
-;; into rendering glitches. This is better explained at the `scroll` examples,
-;; where VRAM buffering techniques are applied and explained.
-.proc init_nametable
- ;; The general idea here is that the background defaults to the
- ;; "transparent" color, which is the first color on the palette. Then for
- ;; the background we only need to store into the PPU those elements which
- ;; are not the default thing. Hence, if we want to draw a couple of
- ;; background elements in our game in some positions, we have to instruct
- ;; the PPU where to put each item and where to find it on the pattern table.
- ;; After that, we can change/toggle the palette for a background tile if we
- ;; so want it, and we are done with it.
-
- ;; Anyways, before loading data into the PPU, we always have to check the
- ;; PPUSTATUS, that's why the first instruction of each block is a `bit
- ;; $2002`. This is a safe-guard so to reset the "address latch". That is, if
- ;; instead of writing two consecutive bytes we only wrote one by mistake (or
- ;; because we only wanted to write the high byte), it resets back its
- ;; internal index.
-
- bit $2002 ; PPUSTATUS
-
- ;; Load the first item for the background! Let's make sense of the values.
- ;;
- ;; We first need to set the address that the PPU will use (that is, which
- ;; couple of bytes we need to pass to PPUADDR). That is: which tile index is
- ;; to be used from the nametable (read more about PPU Nametables here:
- ;; https://www.nesdev.org/wiki/PPU_nametables). Think of this as the screen
- ;; being set as a grid of 8x8 pixels (tile), and each of these tiles is
- ;; indexed in PPU memory: 960 bytes of actual data, and 64 bytes for the
- ;; attribute table (see more on attribute tables below). You can draw this
- ;; by hand by using resources like Morhpcat's developer spreadsheet
- ;; (https://ko-fi.com/s/ad5d7601e0), based on the ones being used at
- ;; Nintendo. This is good but there are also handy tools that make things
- ;; easier for us. For example the NEXXT tool allows users to draw a full NES
- ;; screen and then it gives the offset address that you can use for each
- ;; drawn element. I did this and I got that the offset for the first element
- ;; was $0C8, which added to the base address of $2000 (start of the first
- ;; nametable), gave me the address $20C8. If you want to make more sense of
- ;; it by just reading the number, notice that each row has exactly 32 tiles.
- ;; Thus, you can divide 0x0C8 by 32 and you will get 6.25: that is, on the
- ;; vertical axis it's the row '6'. Then subtract 0xC8 by 192 (6 rows of 32
- ;; tiles) to get '8' as the value on the horizontal axis. As you can see,
- ;; doing things manually can be tedious, that's why people either use a dev
- ;; spreadsheet or a computer assisted tool.
- ;;
- ;; Anyways, if I want this background element to be rendered in the position
- ;; I envisioned on this tool, I need to write $20C8 into PPUADDR, which adds
- ;; for a total of two bytes to be loaded for operations that only support
- ;; one byte at a time. Hence, we have to load byte by byte (in little-endian
- ;; format) and store them. As for the PPUDATA address, I need to pass #$02
- ;; because that's the index inside of the CHR file of the star in the second
- ;; pattern table (check the PPUCTRL setting at the end of the `main`
- ;; function on why it's the second pattern table). You can check that by
- ;; opening the `.chr` file for this example on YY-CHR.
- lda #$20
- sta $2006 ; PPUADDR
- lda #$C8
- sta $2006 ; PPUADDR
- lda #$02
- sta $2007 ; PPUDATA
-
- ;; Now we have to do the same for the rest of the elements. I have gone
- ;; through the same process of fetching the offset from a canvas I drew on
- ;; NEXXT and here's the result. Instead of repeating te same code over and
- ;; over but with slightly different values, I am using this macro I created
- ;; to do the same.
- 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
- WRITE_PPU_DATA $227A, $02
-
- ;; Now everything we wanted to write for the background is there, but we
- ;; might want to toggle the colors for some of the elements. You can do this
- ;; with palettes, which for background tiles is done via attribute tables.
- ;; Each attribute table lives right after each nametable, in which you can
- ;; establish the palettes to be used for the tiles appearing on the
- ;; background screen as defined on that specific nametable. Information on
- ;; this is also given to us by tools like NEXXT. Shortly, for one of the
- ;; elements I want to change their palette I was told that the "attribute
- ;; offset" was $03CE. Again, added to the base address, it means that we
- ;; need to pass $23CE to the PPUADDR. The value for PPUDATA is a bit more
- ;; tricky: on the attribute table each 8x8 tile is broken down into 4
- ;; 2-sized squares. Each of these 4 squares of the tile has a palette
- ;; assigned to it ($00 by default). Thanks to this setup, a single byte can
- ;; encode four palettes by addressing each square: %44332211. The tool also
- ;; tells us in which of these smaller squares our element resides. With this
- ;; info, and since we have two bits available for each of these smaller
- ;; squares and, therefore, can encode up to 4 palettes, it means that we
- ;; just have to assign either on the range of 00-11 to the pair of bits
- ;; representing the smaller square that NEXXT is telling us that our
- ;; background element resides in. In this case it's the first square, so we
- ;; just need to assign $01 to the bits reserved to this square if I want to
- ;; change the palette to 1. This is what we pass as a value.
- WRITE_PPU_DATA $23CE, %00000001
-
- rts
-.endproc
-
-;; init_sprites loads all the sprites we want from our game.
-.proc init_sprites
- ;; This is tied to the number of sprites stored in the `initial_sprite_data`
- ;; section. Remember that this can be 64 *maximum*: 64 sprites * 4 bytes per
- ;; sprite = 256 bytes; which is the reserved space in memory we have for
- ;; sprites: $0200-$02ff.
- NUM_SPRITES = 2
-
- ;; The loading is quite straight-forward. We just store whatever is on
- ;; initial_sprite_data + x into $0200 + x, where x is our index that gets
- ;; increased on each iteration. We will stop the loop whenever x reaches
- ;; NUM_SPRITES * 4 (each sprite takes 4 bytes, more below).
- ldx #$00
-@load_sprites_loop:
- lda initial_sprite_data, x
- sta $0200, x
- inx
- cpx #(4 * NUM_SPRITES)
- bne @load_sprites_loop
- rts
-initial_sprite_data:
- ;; Each sprite consists of 4 bytes:
- ;; 1. The Y position.
- ;; 2. The sprite position on the pattern table in hexadecimal. Make sure to
- ;; get this position right by using YY-CHR (or whatever program you are
- ;; using to manipulate the CHR file). You can get this wrong also
- ;; depending if you are using 8x8 or 8x16 tiles mode (check the bit set
- ;; on PPUCTRL on the `main` code).
- ;; 3. The attributes for the sprite. The first two bits control which
- ;; palette is used to draw the sprite (in our case palette $00). Bits 2
- ;; to 4 are not used. Bit 5 sets the sprite behind the background if set
- ;; to 1; bit 6 flips the sprite horizontally; and bit 7 flips the sprite
- ;; vertically.
- ;; 4. The X position.
- ;;
- ;; In our case, notice that we are using the same sprite (located at $00),
- ;; but the second sprite is moved eight pixels right and flipped
- ;; horizontally. This is because the sprite located at $00 is just half of
- ;; the real "meta-sprite", and so we can build the entire "meta-sprite" by
- ;; simply flipping the same part horizontally. This is one of the many
- ;; tricks we can use to save space on CHR-ROM.
- .byte $B0, $00, %00000000, $7A
- .byte $B0, $00, %01000000, $82
-.endproc
-
-;;;
-;; Non-Maskable Interrupts handler. After being enabled by the initialization
-;; code, you can count on this code being called at the end of each PPU
-;; rendering frame during the Vertical Blanking Interval (VBLANK). This time
-;; span is pretty tight (around 2273 CPU cycles), so you better update
-;; everything to be rendered before that if you don't want graphical glitches.
-;;
-;; NOTE: NMI code can be more complex as we want to do more complex stuff, but
-;; we ought to keep things as simple and fast as possible. You can take a look
-;; at the examples from `scroll` for more complex NMI code that have to handle
-;; stuff like VRAM buffering or setting other PPU registers.
-;;;
-nmi:
- ;; As mentioned on the `main` subroutine, rendering will be skipped until
- ;; the proper flag is set.
- bit $20
- bpl @next
-
- ;; An NMI can happen at any time. Hopefully whenever that happens we are
- ;; already done with the main code, so replacing the current value of
- ;; registers isn't that big of a deal, but it's considered good practice to
- ;; not assume that (e.g. a particular frame being too laggy). Because of
- ;; this, we backup registers now and we restore them at the end.
- pha
- txa
- pha
- tya
- pha
-
- ;; We are instructed that we can start rendering stuff. Transfer the sprites
- ;; via OAM. This is the same we did when we resetted sprites in our `reset`
- ;; code.
- lda #$00
- sta $2003 ; OAMADDR
- lda #$02
- sta $4014 ; OAMDMA
-
- ;; Reset the scroll. This is needed because we have touched the PPUADDR in
- ;; multiple places. Touching the PPUADDR memory address will also toggle the
- ;; PPUSCROLL one because they share a register on hardware. Because of this,
- ;; we always need to reset the scroll back to the coordinates we want, and
- ;; we do it right here, when everything has already been sent and we are
- ;; done.
- bit $2002 ; PPUSTATUS
- lda #$00
- sta $2005 ; PPUSCROLL
- sta $2005 ; PPUSCROLL
-
- ;; And unset the render flag so the `main` code is unblocked.
- lda #%01111111
- and $20
- sta $20
-
- ;; Restore registers.
- pla
- tay
- pla
- tax
- pla
-@next:
- rti
-
-;;;
-;; Interrupt Requests handler. This is triggered by the NES' sound processor
-;; (APU) or by some specific cartridge hardware (e.g. something specific to a
-;; mapper). In our case we don't have to do anything here, so we just return
-;; from the interrupt.
-;;;
-irq:
- rti
-
-;;;
-;; Include into this all the data that needs to go into the CHR ROM. One typical
-;; implementation for this is by using the `.incbin` macro, which will blindly
-;; copy the bitmap that you have generated through a program such as YY-CHR into
-;; the CHR ROM. Note that not all games used the CHR-ROM for storing their
-;; assets and instead used the PRG-ROM for that as well (e.g. The Legend of
-;; Zelda). Take a look at the `basics/chr-ram.s` example for more. There are
-;; multiple technical reasons to do this, but this falls out of the scope of
-;; this example.
-;;;
-.segment "CHARS"
- .incbin "../assets/basic.chr"