aboutsummaryrefslogtreecommitdiff
;;;
;; 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