;;;
;; Make a character blink by performing bank switching on the PPU.
;;
;; This is done by using the MMC3 chip. Read more about this mapper here:
;; https://www.nesdev.org/wiki/MMC3. The MMC3 is a pretty advanced chip, so
;; first go over the `basics/` directory for a better understanding on easier
;; topics. Most importantly, take a look at examples like `basics/persist.s` or
;; `basics/unrom.s`, which also perform bank switching albeit with simpler
;; hardware.
;;
;; This example basically makes use of the bank switching capabilities of the
;; MMC3 chip, so the handling of IRQs is left to other examples (e.g. see
;; `scroll/roulette.s`).
;;
;; On the context of the MMC3 chip, the CPU ROM space is divided into 4 regions
;; of 8KB each. Two of these regions are swappable, the rest are fixed. This is
;; configured when performing bank switching itself. The MMC3 chip has 8
;; registers which hold which bank to go for any given memory address. Because
;; there's only two swappable banks on the CPU memory space, only two registers
;; are given for the CPU: R6 and R7. Bank switching will be a matter of setting
;; which memory bank corresponds to these two registers. The CHR ROM is similar
;; but in there the space is divided into 6 regions (hence 6 registers): 2 of
;; 2KB and 4 of 1KB. Which regions are 2KB and which 1KB is also configured when
;; doing the bank switching itself. Again, for more in-depth views on all of
;; this, check out the NesDev wiki. This example tries to be detailed whenever
;; bank switching happens.
;;
;; With that being said, some games like Megaman 5/6 performed some background
;; effects by simply performing bank switching on the CHR ROM space (e.g.
;; animating leaves from palm trees). This can easily be achieved by simply
;; performing a bank switch on the PPU space periodically with two similar
;; segments. On this example the character blinks because we periodically switch
;; the bank on the first half of the first pattern table between
;; shared/diskun{0,1}.chr. The only difference between these two files are the
;; top sprites for the character. Notice that the code doesn't have to manually
;; specify which sprite ID to look on each iteration: it's the same ID all the
;; time, it's just that the data underneath each ID subtly changes.
;; Include helpful definitions.
.include "../shared/mmc3.s"
;; Variables used on this example.
.scope Vars
zp_counter = $00
zp_last_bank = $01
.endscope
.segment "HEADER"
.byte 'N', 'E', 'S', $1A
.byte $10 ; 16 * 16 PRG-ROM (256KB)
.byte $10 ; 16 * 8 CHR-ROM (128KB)
.byte $42, $08 ; Mapper 4, battery present, iNES 2.0 header
.res 8, 0
.segment "VECTORS"
.addr nmi, reset, irq
;;; NOTE: lots of banks, all of them empty since we don't need them :)
.segment "PRG0_00"
.byte $FF
.segment "PRG0_01"
.byte $FF
.segment "PRG0_02"
.byte $FF
.segment "PRG0_03"
.byte $FF
.segment "PRG0_04"
.byte $FF
.segment "PRG0_05"
.byte $FF
.segment "PRG0_06"
.byte $FF
.segment "PRG0_07"
.byte $FF
.segment "PRG0_08"
.byte $FF
.segment "PRG0_09"
.byte $FF
.segment "PRG0_0A"
.byte $FF
.segment "PRG0_0B"
.byte $FF
.segment "PRG0_0C"
.byte $FF
.segment "PRG0_0D"
.byte $FF
.segment "PRG0_0E"
.byte $FF
.segment "PRG1_00"
.byte $FF
.segment "PRG1_01"
.byte $FF
.segment "PRG1_02"
.byte $FF
.segment "PRG1_03"
.byte $FF
.segment "PRG1_04"
.byte $FF
.segment "PRG1_05"
.byte $FF
.segment "PRG1_06"
.byte $FF
.segment "PRG1_07"
.byte $FF
.segment "PRG1_08"
.byte $FF
.segment "PRG1_09"
.byte $FF
.segment "PRG1_0A"
.byte $FF
.segment "PRG1_0B"
.byte $FF
.segment "PRG1_0C"
.byte $FF
.segment "PRG1_0D"
.byte $FF
.segment "PRG1_0E"
.byte $FF
;;; NOTE: the first fixed PRG bank will simply contain utilities for moving the
;;; player around.
.segment "FIXED"
.include "../shared/diskun.s"
.include "../shared/clear.s"
;;; NOTE: the main bulk of this example. Comments only for the parts which are
;;; specific to this example.
.segment "TAIL"
.proc reset
sei
cld
ldx #$40
stx $4017
ldx #$FF
txs
inx
stx $2000
stx $2001
stx $4010
;;;
;; NOTE: Setup MMC3
;; Just like we saw with the MMC1 chip on `basics/persist.s`, mirroring is
;; configurable (i.e. not soldered in hardware as with the regular NROM).
;; There's a register you can set for this, tied to even addresses between
;; $A000-$BFFE.
;;
;; Set mirroring to vertical (0).
lda #$00
sta MMC3::MIRRORING
;; Interrupts are a whole topic of their own on the MMC3 chip. Here we just
;; disable them and leave it for other examples (e.g. see
;; `scroll/roulette.s`). Again, this is tied to a register mapped to a
;; region in memory (even addresses on $E000-$FFFE in this case).
sta MMC3::IRQ_DISABLE
;; PRG RAM can be protected from writes, but this is a feature that many
;; emulators choose to discard because it might conflict with the MMC6 chip
;; support. Long story short, here we just enable PRG RAM and leave it at
;; that.
lda #$80
sta MMC3::RAM_PROTECT
;; PRG banking setup. PRG-ROM is divided into 4 banks of 8KB each. The last
;; two banks are set to be fixed (otherwise the values below should have set
;; bit 7 each to make other regions fixed and others swappable, see the
;; documentation on NesDev). The first two banks are controlled by registers
;; 6 and 7. In this case, we will simply set the first two banks for these
;; two regions.
BANK_REGISTER_SET 6, 0
BANK_REGISTER_SET 7, 1
;; CHR banking setup. The PPU is divided into 8 banks 1KB each. Hence, we
;; have to fill up this 8KB of the usual CHR-ROM space through banking. The
;; first two registers controlling bank switching on the PPU, R0 and R1,
;; actually hold control over 2KB. Hence, the first two registers actually
;; span 4 banks (that is, the first pattern table on the PPU). We have to
;; account for that when writing into the first two registers (and that's
;; why they are set to 0 and 2 respectively). Other than that, R2-R5 hold
;; which bank goes into the other remaining regions which are 1KB each
;; (hence, the second pattern table is controlled via R2-R5). Because of
;; this, we can simply assign one 1KB bank to each register.
;;
;; To sum things up, on this setup the first pattern table is equally
;; divided by 2 and it's controlled by R0 and R1 respectively. The second
;; pattern table is equally divided by 4 and it's controlled by R2-R5.
BANK_REGISTER_SET 0, 0
BANK_REGISTER_SET 1, 2
BANK_REGISTER_SET 2, 4
BANK_REGISTER_SET 3, 5
BANK_REGISTER_SET 4, 6
BANK_REGISTER_SET 5, 7
;; NOTE: and from here on initialization proceeds as usual.
bit $2002
@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
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
.endproc
;;; NOTE: mainly as usual except that a bit of game loop has been added to
;;; handle the blinking state.
.proc main
;; The code will iterate between banks 4 and 6 on the pattern table, as
;; that's where sprites are located (check the CHARS segment for more info).
lda #4
sta Vars::zp_last_bank
CLEAR_SCREEN
jsr Diskun::init_palettes
jsr init_sprites
cli
;; NOTE: This looks like other examples, but here having background tiles on
;; the first pattern table and sprite tiles on the second pattern table is
;; not a matter of personal taste, but there are technical reasons for it.
;; If you check how IRQs work on the MMC3 chip:
;; https://www.nesdev.org/wiki/MMC3#IRQ_Specifics; you will find that on 8x8
;; tile mode having this arrangement actually spares us from a hardware bug.
;; Long story short, placing background tiles first and sprite tiles second
;; make scanline IRQs reliable. Hence, at least on all MMC3 examples, this
;; will be guaranteed.
lda #%10001000
sta $2000
lda #%00011110
sta $2001
@main_game_loop:
READ_JOYPAD1
jsr Diskun::update
lda #%10000000
ora $20
sta $20
@wait_for_render:
bit $20
bmi @wait_for_render
;; NOTE: let there be a game logic :D
;; Is the counter already at the limit? If not just restart the game loop.
lda Vars::zp_counter
cmp #$20
bne @main_game_loop
;; Reset the counter
lda #0
sta Vars::zp_counter
;; The whole trick is done on R2, which points to $1000, where the sprite
;; tiles begin. Hence, select it.
lda #2
sta MMC3::BANK_SELECT
;; The value for the register is either 4 or 6 depending on its last value.
lda Vars::zp_last_bank
cmp #4
beq :+
lda #4
jmp @set
:
lda #6
@set:
;; Save which is the bank being used both internally and onto the MMC3 chip.
sta Vars::zp_last_bank
sta MMC3::BANK_DATA
jmp @main_game_loop
.endproc
;; NOTE: from here on nothing remarkable in comparison to other examples.
.proc init_sprites
NUM_SPRITES = 4
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
.endproc
;;; NOTE: nothing to highlight here other than the counter is increased on each
;;; NMI.
.proc nmi
bit $20
bpl @next
pha
txa
pha
tya
pha
;; Increase the counter for the blinking.
inc Vars::zp_counter
jsr Diskun::nmi_update
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
.endproc
;;; NOTE: IRQ is disabled when setting up the MMC3 chip for this example.
.proc irq
rti
.endproc
;;; NOTE: The header for this game advertises 128KB for CHR-ROM. This is wildly
;;; too much for this example, but it's a reasonable size for an MMC3 game.
;;; Considering only one pair of pattern tables are available at any given
;;; moment (8KB), this means that we need space for 128 / 8 = 16 pairs of
;;; pattern tables here. For this example the first 8KB are really only used,
;;; and the rest are left with a default value ($00).
.segment "CHARS"
;; As explained when initializing the PPUCTRL register ($2000), it's actually
;; important to place background tiles first and sprite tiles second on the
;; MMC3. This is guaranteed here by setting the first pattern table as empty (we
;; have no background on this example, really). The second pattern table is then
;; filled with `diskun0.chr` and `diskun1.chr`, which are 2KB each. Hence, the
;; 4th CHR bank contains the regular character, and the 6th CHR bank contains
;; the blinking version. You can see these values used when initializing
;; `Vars::zp_last_bank`, or when performing bank switching.
.res $1000, $00
.incbin "../assets/diskun0.chr" ; First half of the second pattern table has the default diskun character.
.incbin "../assets/diskun1.chr" ; Second half of the second pattern table simply has the blinking version.
;; The 15 other 8KB portions are left empty.
.res $2000, $00
.res $2000, $00
.res $2000, $00
.res $2000, $00
.res $2000, $00
.res $2000, $00
.res $2000, $00
.res $2000, $00
.res $2000, $00
.res $2000, $00
.res $2000, $00
.res $2000, $00
.res $2000, $00
.res $2000, $00
.res $2000, $00