aboutsummaryrefslogtreecommitdiff
path: root/scroll
diff options
context:
space:
mode:
Diffstat (limited to 'scroll')
-rw-r--r--scroll/README.md23
-rw-r--r--scroll/toggle.s273
2 files changed, 293 insertions, 3 deletions
diff --git a/scroll/README.md b/scroll/README.md
index e29186a..45c96e8 100644
--- a/scroll/README.md
+++ b/scroll/README.md
@@ -8,11 +8,30 @@ try to understand the concept of scrolling in NES/Famicom programming.
The very basic concepts of scrolling can be seen in [toggle.s](./toggle.s),
which gives you this as a result:
-TBD
+<div align="center">
+ <img src="../docs/toggle.gif" alt="toggle.gif" />
+</div>
That is, we only have filled the two nametable available, and we are modifying
the [PPU scroll register](https://www.nesdev.org/wiki/PPU_registers#PPUSCROLL)
-to move between one or the other.
+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 situations:
+
+1. If you are scrolling right and PPU scroll turns into a `$00` value, then it
+ means that there's nothing else to show from the origin nametable, and that
+ `$00` on the scroll means it's `$00` in respect to a new nametable.
+2. If you are scrolling left and the PPU scroll turns into `$FF` (i.e. the first
+ step on scrolling left), then the base nametable address has to be updated
+ because it's `$FF` from the point of view of the nametable from the left.
+
+It's easy to miss these points, but from a PPU perspective (and hence from the
+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,
diff --git a/scroll/toggle.s b/scroll/toggle.s
index fe99cca..b9214b4 100644
--- a/scroll/toggle.s
+++ b/scroll/toggle.s
@@ -1 +1,272 @@
-;; TODO: simply toggle between two namespaces with smooth scrolling.
+;;;
+;; Toggle between two nametables with smooth scrolling. You can press right/left
+;; to move on that direction, or press Select as a toggle.
+;;
+;; The main take away is that the scroll is the last thing to be updated on
+;; `nmi` code, and that the nametable on the PPU control register should be
+;; update if the camera changed focus. In other words, if we gradually move into
+;; the next nametable and the PPU scroll register wraps around; then the $00 on
+;; that register doesn't mean to go over the same screen, but that it's $00 on
+;; the other nametable. This is something that can be easily missed when
+;; programming scrolling on the NES/Famicom for the first time.
+;;
+;; Besides initialization and setup functions like `setup_screens`, the most
+;; relevant code is in `update` and `nmi`. Read the comments there carefully.
+
+.segment "HEADER"
+ .byte 'N', 'E', 'S', $1A
+ .byte $02, $01
+ .byte $01, $00
+
+.segment "VECTORS"
+ .addr nmi, reset, irq
+
+.segment "CHARS"
+.incbin "../assets/diskun.chr"
+
+.segment "CODE"
+
+.include "../shared/diskun.s"
+.include "./include/apu.s"
+.include "./include/ppu.s"
+.include "./include/oam.s"
+.include "./include/globals.s"
+.include "../shared/ppu.s"
+.include "../shared/clear.s"
+
+;; Variables used on this example.
+.scope Vars
+ ;; Whether we are scrolling or not.
+ zp_scrolling = $A0
+
+ ;; Right = 1; Left = 0.
+ zp_direction = $A1
+
+ ;; Scroll value on the X axis.
+ zp_scroll = $A2
+.endscope
+
+;; Setup both nametables to contain the background indications that we want.
+;; It's not much, just a square with a number to identify each screen.
+.proc setup_screens
+ CLEAR_SCREENS $20, $24
+
+ WRITE_PPU_DATA $21AE, $31
+ WRITE_PPU_DATA $21AF, $31
+ WRITE_PPU_DATA $21CE, $31
+ WRITE_PPU_DATA $21CF, $31
+
+ WRITE_PPU_DATA $25AE, $32
+ WRITE_PPU_DATA $25AF, $32
+ WRITE_PPU_DATA $25CE, $32
+ WRITE_PPU_DATA $25CF, $32
+
+ rts
+.endproc
+
+.proc update
+ ;; Are we already scrolling? If so just skip this.
+ lda Vars::zp_scrolling
+ bne @end
+
+ ;; Read the joypad.
+ jsr joypad_read
+
+ ;; Is the player pressing left? If so then the direction is to the left.
+ lda #Joypad::BUTTON_LEFT
+ and Joypad::m_buttons1
+ beq @check_right
+ inc Vars::zp_scrolling
+ lda #0
+ sta Vars::zp_direction
+ rts
+
+@check_right:
+ ;; Is the player pressing right? If so then the direction is to the right.
+ lda #Joypad::BUTTON_RIGHT
+ and Joypad::m_buttons1
+ beq @check_select
+ inc Vars::zp_scrolling
+ lda #1
+ sta Vars::zp_direction
+ rts
+
+@check_select:
+ ;; Is the player pressing Select? Then the direction depends on the current
+ ;; nametable.
+ lda #Joypad::BUTTON_SELECT
+ and Joypad::m_buttons1
+ beq @end
+ inc Vars::zp_scrolling
+ ldx #0
+ lda PPU::zp_control
+ and #%00000001
+ bne @left
+ inx
+@left:
+ stx Vars::zp_direction
+
+@end:
+ rts
+.endproc
+
+.proc main
+ lda #0
+ sta Globals::zp_flags
+ sta Vars::zp_scrolling
+ sta Vars::zp_direction
+ sta Vars::zp_scroll
+
+ jsr setup_screens
+ jsr Diskun::init_palettes
+
+ cli
+ lda #%10001000
+ sta PPU::zp_control
+ sta PPU::CONTROL
+ lda #%00011110
+ sta PPU::MASK
+
+@main_game_loop:
+ jsr 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
+
+.proc nmi
+ bit Globals::zp_flags
+ bpl @next
+
+ pha
+ txa
+ pha
+ tya
+ pha
+
+ ;; Are we scrolling at all? If not, then just go to `@set_scroll` which will
+ ;; simply set the scroll to 0 again.
+ lda Vars::zp_scrolling
+ beq @set_scroll
+
+ ;; We are scrolling... on which direction?
+ lda Vars::zp_direction
+ beq @left
+
+ ;; If we are scrolling right, flip the nametable if we already reached zero.
+ ;; Otherwise we can set the new scroll value.
+ inc Vars::zp_scroll
+ beq @flip_nametable
+ jmp @set_scroll
+
+@left:
+ ;; If we are scrolling left, and it's actually the first step, change the
+ ;; nametable already. Otherwise set the new scroll value.
+ dec Vars::zp_scroll
+ lda #$FF
+ cmp Vars::zp_scroll
+ bne @set_scroll
+
+@flip_nametable:
+ ;; Reset the PPU address latch.
+ bit PPU::STATUS
+
+ ;; Flip the current nametable.
+ lda PPU::zp_control
+ eor #%00000001
+ sta PPU::zp_control
+ sta PPU::CONTROL
+
+@set_scroll:
+ ;; Set whatever was computed as the new scroll. Note that `zp_scrolling` can
+ ;; be set at the same value, as we only care whenever it reaches 0, when it
+ ;; should stop scrolling.
+ lda Vars::zp_scroll
+ sta Vars::zp_scrolling
+ sta PPU::SCROLL
+ lda #$00
+ sta PPU::SCROLL
+
+ ;; And the rest as usual.
+
+ lda #%01111111
+ and Globals::zp_flags
+ sta Globals::zp_flags
+
+ pla
+ tay
+ pla
+ tax
+ pla
+@next:
+ rti
+.endproc
+
+.proc reset
+ sei
+ cld
+
+ ldx #$40
+ stx APU::FRAME_COUNTER
+
+ ldx #$FF
+ txs
+
+ inx
+ stx PPU::CONTROL
+ stx PPU::MASK
+ stx APU::DMC
+
+@vblankwait1:
+ bit PPU::STATUS
+ 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
+
+ OAM_WRITE_SPRITES
+
+@vblankwait2:
+ bit PPU::STATUS
+ bpl @vblankwait2
+
+ lda #$3F
+ sta PPU::ADDRESS
+ lda #$00
+ sta PPU::ADDRESS
+
+ lda #$0F
+ ldx #$20
+@palettes_reset_loop:
+ sta PPU::DATA
+ dex
+ bne @palettes_reset_loop
+
+ jmp main
+.endproc
+
+.proc irq
+ rti
+.endproc