From 7882e4474b77dfe4036f170cfce0e40b4f10ae93 Mon Sep 17 00:00:00 2001 From: Miquel Sabaté Solà Date: Fri, 10 Jan 2025 12:49:15 +0100 Subject: tests: Bundle end-to-end tests into a tarball MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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à --- tests/src/chr-ram.s | 279 -------------------- tests/src/flicker/flicker.s | 280 -------------------- tests/src/shared/diskun.s | 96 ------- tests/src/shared/joypad.s | 86 ------ tests/src/sprite.s | 618 -------------------------------------------- 5 files changed, 1359 deletions(-) delete mode 100644 tests/src/chr-ram.s delete mode 100644 tests/src/flicker/flicker.s delete mode 100644 tests/src/shared/diskun.s delete mode 100644 tests/src/shared/joypad.s delete mode 100644 tests/src/sprite.s (limited to 'tests/src') 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 $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" -- cgit v1.2.3