aboutsummaryrefslogtreecommitdiff
path: root/scroll/mmc3.s
diff options
context:
space:
mode:
authorMiquel Sabaté Solà <mikisabate@gmail.com>2025-03-12 19:42:43 +0100
committerMiquel Sabaté Solà <mikisabate@gmail.com>2025-03-12 19:42:43 +0100
commit49842414e2c570c511ef5c3c9e1860ee8d2fd504 (patch)
tree05424ae01847339432e8a5729469646be4c6887a /scroll/mmc3.s
parentc02659b85c1d623781c1baec48ea457df360a2cf (diff)
downloadcode.nes-49842414e2c570c511ef5c3c9e1860ee8d2fd504.tar.gz
code.nes-49842414e2c570c511ef5c3c9e1860ee8d2fd504.zip
scroll: Provide an example with the MMC3 chip
Signed-off-by: Miquel Sabaté Solà <mikisabate@gmail.com>
Diffstat (limited to 'scroll/mmc3.s')
-rw-r--r--scroll/mmc3.s382
1 files changed, 381 insertions, 1 deletions
diff --git a/scroll/mmc3.s b/scroll/mmc3.s
index 3f97a52..a498977 100644
--- a/scroll/mmc3.s
+++ b/scroll/mmc3.s
@@ -1 +1,381 @@
-;; TODO: Through MMC3
+;;;
+;; The same as in `level.s` but at the bottom of the screen we have a "This is a
+;; message" being shown. This message is part of the background but it does not
+;; scroll like the rest of the screen, but it stays at the same coordinates all
+;; the time. This is done via the MMC3 chip, and the same technique is further
+;; developed in `roulette.s`. In short, this mapper chip implements a bunch of
+;; features, and one of them is the ability to instruct the chip to send an IRQ
+;; on a given scanline. This is then done to mess with the scroll value and
+;; obtain different effects.
+;;
+;; This example shows the most basic usage of it, and it's what games like Super
+;; Mario Bros. 3 and Kirby's Adventure did: implement a status bar at the bottom
+;; of the screen in a way that is reliable and less CPU consuming than sprite 0
+;; hit detection as it's done in `sprite0.s`. That is, instead of wasting CPU
+;; cycles waiting for a sprite 0 hit, during VBlank we configure the chip to
+;; send us an IRQ for a given scanline. Once the PPU arrives at this scanline,
+;; then it sends us an IRQ in which we reset the scroll value. This scroll value
+;; will then be configured again during VBlank.
+;;
+;; The code is a mix between `level.s` and `fx/blink.s`, so consider these two
+;; examples as previous work and get to know them before jumping into this one.
+;; In here I have just written comments which are specific to this example.
+
+;; Just like `fx/blink.s`.
+.segment "HEADER"
+ .byte 'N', 'E', 'S', $1A
+ .byte $10, $10
+ .byte $42, $08
+ .res 8, 0
+
+.segment "VECTORS"
+ .addr nmi, reset, irq
+
+;; Just like `fx/blink.s`: empty on purpose as this is a simple example.
+
+.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
+.segment "FIXED"
+.byte $FF
+
+;; Everything happens on this segment. Check the `config/mmc3.cfg` for more
+;; information on where it is placed in the end.
+.segment "TAIL"
+
+;; Just like with `sprite0.s`, the engine in `include/` can be configured to a
+;; degree. In this case we instruct it to never go over the `$0D` row as this
+;; will be the one being used for showing the status bar.
+BACKGROUND_ROW_MAX = $0D
+.include "include/all.s"
+.include "../shared/mmc3.s"
+
+;; Clear out the a rows of tiles from the high byte for the PPU address as given
+;; in the `x` register, and the low byte as given on the `y` register.
+.proc clear_row_x_y
+ bit PPU::STATUS
+
+ stx PPU::ADDRESS
+ sty PPU::ADDRESS
+
+ lda #$00
+ ldx #$20
+@loop:
+ sta PPU::DATA
+ dex
+ bne @loop
+
+ rts
+.endproc
+
+;; Similar to `show_hud` in `sprite0.s`, we want to allocate this text in the
+;; background where the engine will not touch it.
+.proc show_status
+ ;; Clear out the space in which we want to allocate or status bar.
+ ldx #$23
+ ldy #$40
+ jsr clear_row_x_y
+ ldx #$23
+ ldy #$60
+ jsr clear_row_x_y
+ ldx #$23
+ ldy #$80
+ jsr clear_row_x_y
+ ldx #$27
+ ldy #$40
+ jsr clear_row_x_y
+ ldx #$27
+ ldy #$60
+ jsr clear_row_x_y
+ ldx #$27
+ ldy #$80
+ jsr clear_row_x_y
+
+ ;; Just like with `sprite0.s`, the message has to be repeated over the
+ ;; nametable on $2400 as the engine will flip the base nametable address
+ ;; whenever the scroll wraps around.
+
+ ;; This
+ WRITE_PPU_DATA $2368, $23
+ WRITE_PPU_DATA $2369, $17
+ WRITE_PPU_DATA $236A, $18
+ WRITE_PPU_DATA $236B, $22
+ WRITE_PPU_DATA $2768, $23
+ WRITE_PPU_DATA $2769, $17
+ WRITE_PPU_DATA $276A, $18
+ WRITE_PPU_DATA $276B, $22
+
+ ;; is
+ WRITE_PPU_DATA $236D, $18
+ WRITE_PPU_DATA $236E, $22
+ WRITE_PPU_DATA $276D, $18
+ WRITE_PPU_DATA $276E, $22
+
+ ;; a
+ WRITE_PPU_DATA $2370, $10
+ WRITE_PPU_DATA $2770, $10
+
+ ;; message
+ WRITE_PPU_DATA $2372, $1C
+ WRITE_PPU_DATA $2373, $14
+ WRITE_PPU_DATA $2374, $22
+ WRITE_PPU_DATA $2375, $22
+ WRITE_PPU_DATA $2376, $10
+ WRITE_PPU_DATA $2377, $16
+ WRITE_PPU_DATA $2378, $14
+ WRITE_PPU_DATA $2772, $1C
+ WRITE_PPU_DATA $2773, $14
+ WRITE_PPU_DATA $2774, $22
+ WRITE_PPU_DATA $2775, $22
+ WRITE_PPU_DATA $2776, $10
+ WRITE_PPU_DATA $2777, $16
+ WRITE_PPU_DATA $2778, $14
+
+ ;; NOTE: in stark contrast with `sprite0.s`, there's no need to waste a
+ ;; sprite for this purpose.
+
+ rts
+.endproc
+
+.proc main
+ ;; Setup the MMC3 chip. Note that this is better suited in the `reset`
+ ;; function, but the engine already provides one and I didn't want to start
+ ;; messing with `.ifdef` and the likes.
+ ;;
+ ;; NOTE: this is a copy-paste from `fx/blink.s`, so refer to that example on
+ ;; what any of the code below means.
+
+ lda #$00
+ sta MMC3::MIRRORING
+ sta MMC3::IRQ_DISABLE
+
+ lda #$80
+ sta MMC3::RAM_PROTECT
+
+ 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
+ BANK_REGISTER_SET 6, 0
+ BANK_REGISTER_SET 7, 1
+
+ ;; MMC3 configured, now go on as usual.
+
+ lda #$00
+ sta PPU::zp_mask
+ sta PPU::MASK
+
+ jsr Palettes::init
+ jsr Metatile::init
+ jsr Driver::init
+ jsr Background::init
+ jsr Player::init
+
+ ;; Show the status bar down below.
+ jsr show_status
+
+ cli
+
+ lda #%10001000
+ sta PPU::zp_control
+ sta PPU::CONTROL
+
+@main_game_loop:
+ READ_JOYPAD1
+ jsr Player::update
+ jsr Driver::update
+
+ lda #%10000000
+ ora Globals::zp_flags
+ sta Globals::zp_flags
+
+@wait_for_render:
+ bit Globals::zp_flags
+ bmi @wait_for_render
+
+ jmp @main_game_loop
+.endproc
+
+;; The basics are laid out in `basics/sprite.s`. Read the comments below for
+;; more info.
+.proc nmi
+ bit Globals::zp_flags
+ bpl @next
+
+ pha
+ txa
+ pha
+ tya
+ pha
+
+ ;; Just like in `fx/blink.s`, acknowledge any previous IRQ as a safety
+ ;; measure. But anyways set the next IRQ to happen on scanline 210.
+
+ ldx #$00
+ stx MMC3::IRQ_DISABLE
+
+ lda #210
+ sta MMC3::IRQ_LATCH
+ sta MMC3::IRQ_RELOAD
+ sta MMC3::IRQ_ENABLE
+
+ ;; From here on as in `level.s`.
+
+ jsr Player::update_sprite
+
+ OAM_WRITE_SPRITES
+
+ FLUSH_PENDING_VRAM_BUFFER
+
+ bit Globals::zp_flags
+ bvc @after_ppu
+
+ lda #%10111111
+ and Globals::zp_flags
+ sta Globals::zp_flags
+
+ bit PPU::STATUS
+
+ lda PPU::zp_control
+ sta PPU::CONTROL
+ lda PPU::zp_mask
+ sta PPU::MASK
+
+@after_ppu:
+ ;; NOTE: scroll is updated always. This is in contrast with `level.s`, but
+ ;; if we don't do that the scroll would be lost if the player stops moving
+ ;; the scroll position.
+ lda Background::zp_scroll
+ sta PPU::SCROLL
+ lda #$00
+ sta PPU::SCROLL
+
+ ;; And as usual again.
+
+ lda #%01111111
+ and Globals::zp_flags
+ sta Globals::zp_flags
+
+ pla
+ tay
+ pla
+ tax
+ pla
+@next:
+ rti
+.endproc
+
+;; This is a much simpler version of IRQ handling as you can see in examples
+;; such as `roulette.s`.
+.proc irq
+ ;; Save current context.
+ pha
+ txa
+ pha
+ tya
+ pha
+
+ ;; Disable IRQs and acknowledge the current one.
+ ldx #$00
+ stx MMC3::IRQ_DISABLE
+
+ ;; Reset the scroll so the status bar is kept in place. The scroll will be
+ ;; kept like this until VBlank happens, when the `nmi` function will set the
+ ;; scroll value to what's perceived by the player.
+ bit PPU::STATUS
+ lda #$00
+ sta $2005
+ sta $2005
+
+ ;; Restore previous context.
+ pla
+ tay
+ pla
+ tax
+ pla
+
+ rti
+.endproc
+
+.segment "CHARS"
+;; Similar to `fx/blink.s` but here we really needed something for the
+;; background :)
+.incbin "../assets/diskun-background.chr"
+.incbin "../assets/diskun0.chr"
+.incbin "../assets/diskun1.chr"
+
+;; 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