aboutsummaryrefslogtreecommitdiff
path: root/scroll
diff options
context:
space:
mode:
Diffstat (limited to 'scroll')
-rw-r--r--scroll/README.md91
-rw-r--r--scroll/include/background.s16
-rw-r--r--scroll/mmc3.s382
-rw-r--r--scroll/roulette.s8
-rw-r--r--scroll/sprite0.s8
-rw-r--r--scroll/toggle4.s1
6 files changed, 459 insertions, 47 deletions
diff --git a/scroll/README.md b/scroll/README.md
index 6005965..e179d62 100644
--- a/scroll/README.md
+++ b/scroll/README.md
@@ -1,7 +1,7 @@
## A primer to scrolling
-Scrolling is a big topic and it's something that evolved with the NES hardware.
-These set of examples try to cover it as much as possible while being
+Scrolling is a big topic and it's something that evolved with the NES/Famicom
+hardware. These set of examples try to cover it as much as possible while being
approachable. But before diving into some more realistic examples, let's first
try to understand the concept of scrolling in NES/Famicom programming.
@@ -17,9 +17,10 @@ mirroring scenario, and we are modifying the [PPU scroll
register](https://www.nesdev.org/wiki/PPU_registers#PPUSCROLL) to move between
one or the other. Another important note, easily missed when programming
scrolling on the NES/Famicom for the first time, is that whenever the PPU scroll
-"wraps around", you should also update the base nametable address from the [PPU
-control register](https://www.nesdev.org/wiki/PPU_registers#PPUCTRL). That
-happens in two cases:
+"wraps around" between two different nametables, you should also update the base
+nametable address from the [PPU control
+register](https://www.nesdev.org/wiki/PPU_registers#PPUCTRL). That happens in
+two cases:
1. If you are scrolling right and PPU scroll turns into `$00`, then it means
that there's nothing else to show from the origin nametable, and that `$00`
@@ -33,10 +34,10 @@ perspective of a programmer interfacing with the PPU), it really makes sense.
All in all, the scroll register is relative to whatever base nametable is set on
the control register.
-This looks rather simplistic but some games used this technique. For example, in
-Dropzone it was used to perform some effects on the title screen. Hence,
-performing a simple scroll between two nametables is not just for learning
-purposes, it was also used in real life games.
+Overall, this example looks rather simplistic but some games used this
+technique. For example, in Dropzone it was used to perform some effects on the
+title screen. Hence, performing a simple scroll between two nametables is not
+just for learning purposes, it was also used in real life games.
## Scrolling multiple screens to the right
@@ -54,9 +55,10 @@ This is all accomplished by dropping the notion of tiles and speaking in
up to 16x16 pixel blocks. These blocks are the ones being continuously loaded
when the player moves, and they are the ones being considered for collision
checks. This is all better explained and with all the gory details inside of the
-[./include](./include) directory, which is somewhat of a library for the rest of
-the scrolling examples. The concepts at display here are more complex than they
-look, so take your time.
+[./include](./include) directory, which is somewhat of a library/engine for the
+rest of the scrolling examples. The concepts at display here are more complex
+than they look, so take your time reading through the code on
+[./include](./include).
Also note that different games had different ways on how to handle metatiles, so
don't go out from these examples thinking "oh, so this is how *all* games mapped
@@ -79,10 +81,11 @@ In games like Super Marios Bros. or Punch-out, this was achieved thanks to the
"sprite 0 hit" detection, which was a special feature from the PPU in which it
would flip a bit on the [PPU status
register](https://www.nesdev.org/wiki/PPU_registers#PPUSTATUS) whenever a
-background element was found to collide with the first sprite in OAM. That being
-said, both the sprite and the background element need to be opaque (that is, not
-using the first color from the palette), and there shouldn't be in a special
-scenario like the PPU being disabled or the sprite being on a hidden margin.
+background element was found to collide with the first sprite in
+[OAM](https://www.nesdev.org/wiki/PPU_OAM). That being said, both the sprite and
+the background element need to be opaque (that is, not using the first color
+from the palette), and there shouldn't be in a special scenario like the PPU
+being disabled or the sprite being on a hidden margin.
Because all of this, both Super Mario Bros. and Punch-out (and many other
games), place the first sprite inside of a background element being displayed
@@ -99,28 +102,48 @@ gives us this result:
## Bringing the status bar down below
-TBD: see also explanation below
+That being said, the sprite 0 hit detection technique is quite hacky, but more
+than that it is a waste of CPU resources: the CPU is spending a lot of time just
+waiting for this hit to happen instead of preparing for the next frame. That is,
+the CPU is wasting a lot of time constantly polling for some scanline.
+
+Fortunately, some later mapper chips (see examples on [basics/](../basics/) to
+understand what these are) like the [MMC3](https://www.nesdev.org/wiki/MMC3)
+provided methods for setting up an IRQ for a needed scanline. That is, the CPU,
+instead of constantly polling to detect whenever a given scanline was hit, could
+just tell the PPU "hey, just tell me whenever you reach this given scanline";
+and the PPU would send an IRQ on that condition. This way, the CPU could devote
+its resources to compute the next frame, and then, halfway computing the next
+frame, it would stop this task to fulfill an IRQ sent by the PPU, resuming
+shortly after. This is a much better usage of CPU time, and it could be done
+multiple times per frame. Thus, it is a technique that allowed for intricate
+effects, as it is shown by parallax effects in Ninja Gaiden II, or roulette-like
+minigames as in Super Mario Bros. 3.
+
+In [mmc3.s](./mmc3.s) we go for the most basic usage of this technique: let the
+scroll go on as usual, and then on a given scanline we will reset the scroll
+back to 0. This will allow us to show a "status bar", which is basically the
+same "This is a message" thing from the `sprite0.s` example. This looks
+something like this:
+
+<div align="center">
+ <img src="../docs/mmc3.gif" alt="mmc3.gif" />
+</div>
## Scrolling in different ways in the same frame
-Some chips like the MMC3 give programmers a lot of flexibility when it comes to
-mid-frame customization. That is, chips like the MMC3 give an interface in which
-programmers can ask the chip to submit an IRQ on a given exact scanline. One
-main usage of this was to allow a top section of the screen to scroll, while
-leaving a small section at the bottom not to scroll. This way, games were no
-longer required to have a status bar at the top and they could have it at the
-bottom. But these chips allow for a lot of flexibility, so programmers can get
-playful with it. One simple example is the roulette mini-game from Super Mario
-Bros. 3. In here the game asks for two scanline IRQs and then the scroll
-direction is changed on each given IRQ. This way, the background is split in
-three sections that move in different directions/speed. Something similar (but
-more simple) has been reproduced in [roulette.s](./roulette.s), giving the
-following result:
+As explained above, chips like the MMC3 give programmers a lot of flexibility
+when it comes to mid-frame customization. That is, chips like the MMC3 give an
+interface in which programmers can ask the chip to submit an IRQ on a given
+exact scanline, multiple times per frame. The main usage for this technique is
+the one explored above in `mmc3.s`, but these chips allow for a lot of
+flexibility, so programmers can get playful with it. One simple example is the
+roulette mini-game from Super Mario Bros. 3. In here the game asks for two
+scanline IRQs and then the scroll direction is changed on each given IRQ. This
+way, the background is split in three sections that move in different
+directions/speed. Something similar (but more simple) has been reproduced in
+[roulette.s](./roulette.s), giving the following result:
<div align="center">
<img src="../docs/roulette.gif" alt="roulette.gif" />
</div>
-
-## Expanding to have multiple scrolling directions
-
-TBD: toggle4.s
diff --git a/scroll/include/background.s b/scroll/include/background.s
index 69725fd..e637380 100644
--- a/scroll/include/background.s
+++ b/scroll/include/background.s
@@ -36,6 +36,16 @@
BACKGROUND_ROW_OFFSET = 0
.endif
+ ;; The maximum row coordinate this engine is allowed to go. That is, until
+ ;; which row each column is supposed to be rendered. By default it's $0F,
+ ;; but it could be set to something else to allow a status element down the
+ ;; screen.
+ ;;
+ ;; NOTE: configurable.
+ .ifndef BACKGROUND_ROW_MAX
+ BACKGROUND_ROW_MAX = $0F
+ .endif
+
;; Current column (or the next to be loaded by functions like
;; `load_column`).
zp_cur_column = $75
@@ -251,7 +261,7 @@
;; We are actually at the end of the screen definition. Then the offset
;; is simply the current row until the end. If that turns out to be
;; zero, then we are done.
- lda #$0F
+ lda #BACKGROUND_ROW_MAX
sec
sbc zp_cur_row
beq @done
@@ -293,7 +303,7 @@
;; We are done pushing default metatiles. Check if we are done with the
;; column entirely.
lda zp_cur_row
- cmp #$0F
+ cmp #BACKGROUND_ROW_MAX
beq @done
;; Push the metatile pointed by the screen pointer.
@@ -498,7 +508,7 @@
@to_the_end:
;; We aren't: go until the last row.
- lda #$0F
+ lda #BACKGROUND_ROW_MAX
@compute:
;; Subtract the current row with the one we are trying to reach.
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
diff --git a/scroll/roulette.s b/scroll/roulette.s
index dae89de..b0c2a61 100644
--- a/scroll/roulette.s
+++ b/scroll/roulette.s
@@ -17,7 +17,7 @@
;; mini-game. That being said, usually games used this capability to handle
;; scroll on the top part of the screen, and then resetting the scroll on the
;; lower part, so they could show a status section (again, as Super Mario Bros.
-;; 3 does inside of a level).
+;; 3 does inside of a level, and in mmc3.s here).
;; Include helpful definitions.
.include "../shared/mmc3.s"
@@ -32,9 +32,9 @@
.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
+ .byte $10
+ .byte $10
+ .byte $42, $08
.res 8, 0
.segment "VECTORS"
diff --git a/scroll/sprite0.s b/scroll/sprite0.s
index a1944e6..7675a47 100644
--- a/scroll/sprite0.s
+++ b/scroll/sprite0.s
@@ -1,8 +1,8 @@
;;;
-;; Show like `level.s` but at the very top 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 through sprite 0 collision detection, which is a
+;; The same as in `level.s` but at the very top 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 through sprite 0 collision detection, which is a
;; technique is quite often for early games on the NES/Famicom library (e.g.
;; Super Mario Bros.).
;;
diff --git a/scroll/toggle4.s b/scroll/toggle4.s
deleted file mode 100644
index 2be19f5..0000000
--- a/scroll/toggle4.s
+++ /dev/null
@@ -1 +0,0 @@
-;; TODO: like toggle.s but you can choose between the "four" nametables.