aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMiquel Sabaté Solà <mikisabate@gmail.com>2025-01-23 16:14:12 +0100
committerMiquel Sabaté Solà <mikisabate@gmail.com>2025-02-04 22:00:07 +0100
commitfdf44365e56c3741f47cbd421a57f0d074a5492c (patch)
tree92b55944dd84171f6c42857402b1f90f835abfa2
parent6c207c192b7ce1a292249afd7b0a3499be70d03c (diff)
downloadcode.nes-fdf44365e56c3741f47cbd421a57f0d074a5492c.tar.gz
code.nes-fdf44365e56c3741f47cbd421a57f0d074a5492c.zip
fx: Provide an example of PPU bank switching
Signed-off-by: Miquel Sabaté Solà <mikisabate@gmail.com>
-rw-r--r--Makefile9
-rw-r--r--README.md3
-rw-r--r--assets/diskun0.chrbin0 -> 2048 bytes
-rw-r--r--assets/diskun1.chrbin0 -> 2048 bytes
-rw-r--r--config/mmc3.cfg93
-rw-r--r--fx/README.md14
-rw-r--r--fx/blink.s395
-rw-r--r--shared/mmc3.s23
8 files changed, 531 insertions, 6 deletions
diff --git a/Makefile b/Makefile
index 7b89700..461c0b5 100644
--- a/Makefile
+++ b/Makefile
@@ -18,14 +18,14 @@ clean:
@rm -rf out
@find . -type f -name "*.o" -delete
@find . -type f -name "*.nes" -delete
- @mkdir -p out/basics out/scroll out/space
+ @mkdir -p out/basics out/scroll out/space out/fx
.PHONY: deps
deps:
@which $(CC65) >/dev/null 2>/dev/null || (echo "ERROR: $(CC65) not found." && false)
.PHONY: build
-build: basics space scroll
+build: basics space scroll fx
.PHONY: basics
basics:
@@ -56,3 +56,8 @@ space:
scroll:
$(E) " CC scroll"
$(Q) $(CC65) $(CCOPTS) scroll/level.s -C config/nrom.cfg -o out/scroll/level.nes
+
+.PHONY: fx
+fx:
+ $(E) " CC fx/blink"
+ $(Q) $(CC65) $(CCOPTS) fx/blink.s -C config/mmc3.cfg -o out/fx/blink.nes
diff --git a/README.md b/README.md
index 476e48f..729a8cb 100644
--- a/README.md
+++ b/README.md
@@ -37,7 +37,8 @@ The examples are distributed like this:
but we make it move and perform an action like shooting bullets.
- `scroll`: different scrolling tactics. Read the
[scroll/README.md](./scroll/README.md) file for more info.
-- `fx`: miscellanous effects that can be achieved with this humble machine.
+- `fx`: simple graphical effects that can be pulled off. Read the
+ [fx/README.md](./fx/README.md) file for more info.
## Other projects
diff --git a/assets/diskun0.chr b/assets/diskun0.chr
new file mode 100644
index 0000000..f009361
--- /dev/null
+++ b/assets/diskun0.chr
Binary files differ
diff --git a/assets/diskun1.chr b/assets/diskun1.chr
new file mode 100644
index 0000000..10da5ed
--- /dev/null
+++ b/assets/diskun1.chr
Binary files differ
diff --git a/config/mmc3.cfg b/config/mmc3.cfg
new file mode 100644
index 0000000..9dbfa05
--- /dev/null
+++ b/config/mmc3.cfg
@@ -0,0 +1,93 @@
+MEMORY {
+ # iNES header
+ HEADER: file = %O, start = $0000, size = $0010, fill = yes;
+
+ # First 15 switchable banks at $8000.
+ PRG0_00: start = $8000, size = $2000, file = %O, fill = yes, fillval = $00;
+ PRG0_01: start = $8000, size = $2000, file = %O, fill = yes, fillval = $01;
+ PRG0_02: start = $8000, size = $2000, file = %O, fill = yes, fillval = $02;
+ PRG0_03: start = $8000, size = $2000, file = %O, fill = yes, fillval = $03;
+ PRG0_04: start = $8000, size = $2000, file = %O, fill = yes, fillval = $04;
+ PRG0_05: start = $8000, size = $2000, file = %O, fill = yes, fillval = $05;
+ PRG0_06: start = $8000, size = $2000, file = %O, fill = yes, fillval = $06;
+ PRG0_07: start = $8000, size = $2000, file = %O, fill = yes, fillval = $07;
+ PRG0_08: start = $8000, size = $2000, file = %O, fill = yes, fillval = $08;
+ PRG0_09: start = $8000, size = $2000, file = %O, fill = yes, fillval = $09;
+ PRG0_0A: start = $8000, size = $2000, file = %O, fill = yes, fillval = $0A;
+ PRG0_0B: start = $8000, size = $2000, file = %O, fill = yes, fillval = $0B;
+ PRG0_0C: start = $8000, size = $2000, file = %O, fill = yes, fillval = $0C;
+ PRG0_0D: start = $8000, size = $2000, file = %O, fill = yes, fillval = $0D;
+ PRG0_0E: start = $8000, size = $2000, file = %O, fill = yes, fillval = $0E;
+
+ # Last 15 switchable banks at $A000.
+ PRG1_00: start = $A000, size = $2000, file = %O, fill = yes, fillval = $10;
+ PRG1_01: start = $A000, size = $2000, file = %O, fill = yes, fillval = $11;
+ PRG1_02: start = $A000, size = $2000, file = %O, fill = yes, fillval = $12;
+ PRG1_03: start = $A000, size = $2000, file = %O, fill = yes, fillval = $13;
+ PRG1_04: start = $A000, size = $2000, file = %O, fill = yes, fillval = $14;
+ PRG1_05: start = $A000, size = $2000, file = %O, fill = yes, fillval = $15;
+ PRG1_06: start = $A000, size = $2000, file = %O, fill = yes, fillval = $16;
+ PRG1_07: start = $A000, size = $2000, file = %O, fill = yes, fillval = $17;
+ PRG1_08: start = $A000, size = $2000, file = %O, fill = yes, fillval = $18;
+ PRG1_09: start = $A000, size = $2000, file = %O, fill = yes, fillval = $19;
+ PRG1_0A: start = $A000, size = $2000, file = %O, fill = yes, fillval = $1A;
+ PRG1_0B: start = $A000, size = $2000, file = %O, fill = yes, fillval = $1B;
+ PRG1_0C: start = $A000, size = $2000, file = %O, fill = yes, fillval = $1C;
+ PRG1_0D: start = $A000, size = $2000, file = %O, fill = yes, fillval = $1D;
+ PRG1_0E: start = $A000, size = $2000, file = %O, fill = yes, fillval = $1E;
+
+ # Last two fixed banks + vectors.
+ FIXED: start = $C000, size = $2000, fill = yes, fillval = $FA, define = yes;
+ TAIL: start = $E000, size = $1FFA, fill = yes, fillval = $FB, define = yes;
+ ROMV: start = $FFFA, size = $0006, fill = yes, fillval = $FC, define = yes;
+
+ # CHR ROM. Groupped all together as we don't need as much flexibility
+ # really, but `incbin` statements will suffice.
+ CHARS: start = $0000, size = $20000, file = %O, fill = no;
+}
+
+SEGMENTS {
+ HEADER: load = HEADER, type = ro;
+
+ # 15 first switchable banks.
+ PRG0_00: load = PRG0_00, type = ro;
+ PRG0_01: load = PRG0_01, type = ro;
+ PRG0_02: load = PRG0_02, type = ro;
+ PRG0_03: load = PRG0_03, type = ro;
+ PRG0_04: load = PRG0_04, type = ro;
+ PRG0_05: load = PRG0_05, type = ro;
+ PRG0_06: load = PRG0_06, type = ro;
+ PRG0_07: load = PRG0_07, type = ro;
+ PRG0_08: load = PRG0_08, type = ro;
+ PRG0_09: load = PRG0_09, type = ro;
+ PRG0_0A: load = PRG0_0A, type = ro;
+ PRG0_0B: load = PRG0_0B, type = ro;
+ PRG0_0C: load = PRG0_0C, type = ro;
+ PRG0_0D: load = PRG0_0D, type = ro;
+ PRG0_0E: load = PRG0_0E, type = ro;
+
+ # 15 last switchable banks.
+ PRG1_00: load = PRG1_00, type = ro;
+ PRG1_01: load = PRG1_01, type = ro;
+ PRG1_02: load = PRG1_02, type = ro;
+ PRG1_03: load = PRG1_03, type = ro;
+ PRG1_04: load = PRG1_04, type = ro;
+ PRG1_05: load = PRG1_05, type = ro;
+ PRG1_06: load = PRG1_06, type = ro;
+ PRG1_07: load = PRG1_07, type = ro;
+ PRG1_08: load = PRG1_08, type = ro;
+ PRG1_09: load = PRG1_09, type = ro;
+ PRG1_0A: load = PRG1_0A, type = ro;
+ PRG1_0B: load = PRG1_0B, type = ro;
+ PRG1_0C: load = PRG1_0C, type = ro;
+ PRG1_0D: load = PRG1_0D, type = ro;
+ PRG1_0E: load = PRG1_0E, type = ro;
+
+ # Last two fixed banks + vectors.
+ FIXED: load = FIXED, type = ro;
+ TAIL: load = TAIL, type = ro;
+ VECTORS: load = ROMV, type = ro;
+
+ # CHR-ROM is groupped all together.
+ CHARS: load = CHARS, type = ro;
+}
diff --git a/fx/README.md b/fx/README.md
index 746e922..e7194dc 100644
--- a/fx/README.md
+++ b/fx/README.md
@@ -1,4 +1,12 @@
-TBD
+## Effects by bank switching on CHR-ROM
-- Animating through CHR bank switching (a la Megaman 5 with MMC3)
-- Shrinking a sprite rendered through CHR-RAM
+Some games like Megaman 5/6 performed some subtle background effects by
+performing bank switching on the PPU thanks to the capabilities of the MMC3.
+That is, the trick is to devote at least two similar 1KB chunks where one
+contains a subtly changed version of the other. This way, you can simply perform
+a periodic bank switch and the PPU will render subtly different things every
+time, without the CPU having to dedicate any resources on changing any values on
+the data.
+
+A very simple example is provided in [blink.s](./blink.s) where a character
+blinks periodically.
diff --git a/fx/blink.s b/fx/blink.s
new file mode 100644
index 0000000..c4d141d
--- /dev/null
+++ b/fx/blink.s
@@ -0,0 +1,395 @@
+;;;
+;; 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`, which also perform bank switching albeit with simpler
+;; hardware.
+;;
+;; This examples basically makes use of the bank switching capabilities of the
+;; MMC3 chip, so the handling of IRQs is left to other examples.
+;;
+;; 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
+ counter = $00
+ 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"
+
+;;; NOTE: the main bulk of this example. Comments only for the parts which are
+;;; specific to this example.
+.segment "TAIL"
+
+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. 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.
+ ;; 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.
+
+@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
+
+;;; NOTE: mainly as usual except that a bit of game loop has been added to
+;;; handle the blinking state.
+.proc main
+ jsr Diskun::init_palettes
+ jsr init_sprites
+
+ cli
+ lda #%10010000
+ sta $2000
+ lda #%00011110
+ sta $2001
+
+@main_game_loop:
+ jsr joypad_read
+ 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::counter
+ cmp #$20
+ bne @main_game_loop
+
+ ;; Reset the counter
+ lda #0
+ sta Vars::counter
+
+ ;; The whole trick is done on R0. So select it.
+ lda #0
+ sta MMC3::BANK_SELECT
+
+ ;; The value for the register is either 0 or 2 depending on its last value.
+ lda Vars::last_bank
+ beq :+
+ lda #0
+ beq @set
+:
+ lda #2
+@set:
+ ;; Save which is the bank being used both internally and onto the MMC3 chip.
+ sta Vars::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.
+nmi:
+ bit $20
+ bpl @next
+
+ pha
+ txa
+ pha
+ tya
+ pha
+
+ ;; Increase the counter for the blinking.
+ inc Vars::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
+
+;;; NOTE: IRQ is disabled when setting up the MMC3 chip for this example.
+irq:
+ rti
+
+;;; 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 realy only used, and
+;;; the rest are left with a default value ($00).
+
+.segment "CHARS"
+;; First 8KB (note that diskun0.chr and diskun1.chr are both 2KB long)
+.incbin "../assets/diskun0.chr" ; First half of the first pattern table has the default diskun character.
+.incbin "../assets/diskun1.chr" ; Second half of the first pattern table simply has the blinking version.
+.res $1000, $00 ; Second pattern table is left with the default value.
+
+;; 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
diff --git a/shared/mmc3.s b/shared/mmc3.s
new file mode 100644
index 0000000..b3f484a
--- /dev/null
+++ b/shared/mmc3.s
@@ -0,0 +1,23 @@
+;;; Useful macros and constants for development on the MMC3 chip.
+
+.scope MMC3
+ BANK_SELECT = $8000
+ BANK_DATA = $8001
+ MIRRORING = $A000
+ RAM_PROTECT = $A001
+ IRQ_LATCH = $C000
+ IRQ_RELOAD = $C001
+ IRQ_DISABLE = $E000
+ IRQ_ENABLE = $E001
+.endscope
+
+.macro BANK_REGISTER_SET REGISTER_ID, REGISTER_VALUE
+ .if REGISTER_ID < 0 || REGISTER_ID > 7
+ .error "bad value for REGISTER_ID when bank switching"
+ .endif
+
+ lda #REGISTER_ID
+ sta MMC3::BANK_SELECT
+ lda #REGISTER_VALUE
+ sta MMC3::BANK_DATA
+.endmacro