aboutsummaryrefslogtreecommitdiff
path: root/scroll
diff options
context:
space:
mode:
Diffstat (limited to 'scroll')
-rw-r--r--scroll/README.md66
-rw-r--r--scroll/include/all.s18
-rw-r--r--scroll/include/background.s498
-rw-r--r--scroll/include/buffer.s258
-rw-r--r--scroll/include/collision.s234
-rw-r--r--scroll/include/driver.s98
-rw-r--r--scroll/include/globals.s72
-rw-r--r--scroll/include/metatile.s209
-rw-r--r--scroll/include/oam.s2
-rw-r--r--scroll/include/palettes.s24
-rw-r--r--scroll/include/player.s303
-rw-r--r--scroll/include/ppu.s23
-rw-r--r--scroll/include/reset.s62
-rw-r--r--scroll/level.s144
-rw-r--r--scroll/roulette.s43
-rw-r--r--scroll/toggle.s1
-rw-r--r--scroll/toggle4.s1
17 files changed, 1925 insertions, 131 deletions
diff --git a/scroll/README.md b/scroll/README.md
index be33f87..25033a4 100644
--- a/scroll/README.md
+++ b/scroll/README.md
@@ -1,19 +1,59 @@
-## Scrolling
+## A primer to scrolling
Scrolling is a big topic and it's something that evolved with the NES hardware.
This set of examples try to cover it as much as possible while being
-approachable as single files.
+approachable. But before diving into some more realistic examples, let's first
+try to understand the concept of scrolling in NES/Famicom programming.
-First of all, you should take a look at `level.s`, which shows how games can
-scroll a level that spans multiple screens and continuously load/unload the
-next/previous sections of the level.
+The very basic concepts of scrolling can be seen in `toggle.s`, which gives you
+this as a result:
-The second example is `sprite0.s`, which covers the scrolling done by games such
-as Super Mario Bros. or Punch-out. That is, we use the "sprite 0 hit" detection
-to keep the top level part of the screen from moving (so to show relevant
-information), while allowing the rest of the screen to scroll as expected. In
-the end, it's the same example as `level.s` (same level to scroll), but the top
-part does not move and shows a "THIS DOES NOT MOVE" message.
+TBD
+
+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.
+
+## Scrolling multiple screens to the right
+
+With the basics covered, now let's see how a game can scroll past two screens
+worth of data. This is delivered on the `level.s` example, and pressing "Select"
+allows you to toggle between different "levels". This gives you the following
+results:
+
+![level.gif](../docs/level.gif)
+
+This is all accomplished by dropping the notion of tiles and speaking in
+"metatile" terms. That is, instead of dividing the screen in 8x8 pixels, we go
+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 and more complex than they
+look, so take your time. 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 things on screen!". This is just one way to do
+so, every game came with its own engine with its own quirks. Consider, for
+example, how Megaman games had "meta-metatiles" (a concept also used in modern
+games like [Micro Mages](https://youtu.be/ZWQ0591PAxM?si=kE69LfgpaW6t-Sr3)).
+
+Last but not least, bear in mind that this "engine" comes with some big
+limitations, like the inability to scroll to the left.
+
+## Detecting collision on sprite 0
+
+Another limitation from the `level.s` example is how *everything* scrolls. This
+would be a bummer for most games from the era since they would've wanted to
+reserve some space on screen to show a "status" bar: how many lifes you have,
+score, etc.
+
+In games like Super Marios Bros. or Punch-out, this was achieved thanks to the
+"sprite 0 hit" detection. TBD
+
+## Bringing the status bar down below
+
+TBD: see also explanation below
## Scrolling in different ways in the same frame
@@ -32,3 +72,7 @@ more simple) has been reproduced in [roulette.s](./roulette.s), giving the
following result:
![roulette.png](../docs/roulette.gif)
+
+## Expanding to have multiple scrolling directions
+
+TBD: toggle4.s
diff --git a/scroll/include/all.s b/scroll/include/all.s
new file mode 100644
index 0000000..e9f059d
--- /dev/null
+++ b/scroll/include/all.s
@@ -0,0 +1,18 @@
+;;; Instead of figuring out the includes for all the examples, simply include
+;;; this one and run with it.
+
+.include "apu.s"
+.include "oam.s"
+.include "ppu.s"
+.include "../../shared/asm.s"
+.include "../../shared/joypad.s"
+
+.include "reset.s"
+.include "palettes.s"
+.include "metatile.s"
+.include "globals.s"
+.include "collision.s"
+.include "buffer.s"
+.include "background.s"
+.include "player.s"
+.include "driver.s"
diff --git a/scroll/include/background.s b/scroll/include/background.s
new file mode 100644
index 0000000..f392792
--- /dev/null
+++ b/scroll/include/background.s
@@ -0,0 +1,498 @@
+;; Reset the background cycle so a new column can be loaded even if the cycle
+;; was stopped at the end of the loading of the previous screen.
+.macro RESET_BACKGROUND_CYCLE
+ lda #$FF
+ sta Background::zp_background_cycle
+.endmacro
+
+;; All functions and related variables for keeping up with the background
+;; elements. This is by far the most extensive and complex piece of code from
+;; the scrolling examples, and it's the core of the render engine for them.
+;;
+;; Documentation has been added for it as much as possible, so follow along to
+;; better grasp how to use and grok this piece of code.
+.scope Background
+ ;; Current value for the scroll on the X axis.
+ zp_scroll = $95
+
+ ;; Instead of attempting to load a column every time the player movement
+ ;; requires it, stall things a bit (as much as defined in
+ ;; `BACKGROUND_CYCLE_MAX`). This allows for loading columns at a slower
+ ;; pace. This cycle is diregarded if you set it to
+ ;; `DISABLED_BACGROUND_CYCLE`, which it just means something like "no more
+ ;; columns to load".
+ zp_background_cycle = $96
+
+ ;; See `zp_background_cycle`.
+ DISABLED_BACKGROUND_CYCLE = $FE
+ BACKGROUND_CYCLE_MAX = $02
+
+ ;; Current column (or the next to be loaded by functions like
+ ;; `load_column`).
+ zp_cur_column = $75
+
+ ;; Current row (or the next to be loaded by functions like `load_column`).
+ zp_cur_row = $76
+
+ .proc init
+ ;; Load the first level.
+ ldx #$00
+ JAL load_level_x
+ ;; NOTE: returned value ignored as the level *must* have loaded at this
+ ;; point.
+ .endproc
+
+ ;; Resets all the variables which touch in any way the loading of columns,
+ ;; levels, etc. This is already being called by `load_level_x`, so outside
+ ;; of this usage you shouldn't be calling it.
+ .proc reset
+ RESET_BACKGROUND_CYCLE
+
+ lda #$00
+ sta zp_scroll
+ sta Buffer::zp_vram_idx
+ sta zp_cur_row
+ sta zp_cur_column
+
+ rts
+ .endproc
+
+ ;; Load the level indexed by the `x` register. This function requires that
+ ;; the PPU is disabled. If this is not the case, then it sets PPU::zp_mask
+ ;; in a way so to disable the PPU on the next `nmi` call, and finally sets
+ ;; `a` to 0 (i.e. level could not be loaded). Otherwise you can expect the
+ ;; `a` register to contain 1 after calling this function.
+ ;;
+ ;; Moreover, the screen pointer is going to be left at the start of the
+ ;; second screen, so future `load_column` calls can rely on this pointer
+ ;; being set properly.
+ .proc load_level_x
+ ;; Is the PPU set to be disabled?
+ lda PPU::zp_mask
+ and #%00111100
+ beq @load_level
+
+ ;; The PPU is not disabled, set the mask so the PPU is disabled.
+ lda #$00
+ sta PPU::zp_mask
+
+ ;; Reset the nametable being used.
+ lda #%11111100
+ and PPU::zp_control
+ sta PPU::zp_control
+
+ ;; Enable the `ppu` flag so these changes are not ignored.
+ lda Globals::zp_flags
+ ora #%01000000
+ and #%11011111
+ sta Globals::zp_flags
+ lda #0
+ rts
+
+ @load_level:
+ ;; Start by resetting any variables that touch on the loading of columns
+ ;; in any way.
+ jsr Background::reset
+
+ ;; Reset all relevant flags.
+ lda #%11000011
+ and Globals::zp_flags
+ sta Globals::zp_flags
+
+ ;; Initialize the screen pointer by using the argument as passed through
+ ;; the `x` register.
+ lda levels_lo, x
+ sta Metatile::zp_screen_ptr
+ lda levels_hi, x
+ sta Metatile::zp_screen_ptr + 1
+
+ @loop:
+ jsr load_column
+ ;; NOTE: returned value ignored.
+
+ ;; Increase the column being used and loop if we are not at the end of
+ ;; the screen yet.
+ lda #0
+ sta zp_cur_row
+ inc zp_cur_column
+ lda zp_cur_column
+ cmp #$10
+ bne @loop
+
+ ;; Advance the screen pointer so it points to the second screen.
+ ADVANCE_SCREEN_PTR(1)
+
+ ;; Enable back the PPU on the next NMI call.
+ lda #%00011110
+ sta PPU::zp_mask
+
+ ;; And set the `ppu`, `level` and `nametable` flags.
+ lda #%01100100
+ ora Globals::zp_flags
+ sta Globals::zp_flags
+
+ ;; Reset things again just in case.
+ jsr Background::reset
+
+ lda #1
+ rts
+ .endproc
+
+ ;; Translates the given metatile coordinate byte as given on the `a`
+ ;; register and sets the first four argument variables from it. A "metatile
+ ;; coordinate byte" is the first byte of a metatile reference on the screen
+ ;; definition. Hence, the high nibble contains the Y metatile coordinate,
+ ;; and the low nibble contains the X metatile coordinate.
+ ;;
+ ;; After calling this function you will have the following:
+ ;;
+ ;; - zp_arg0: Y metatile coordinate (i.e. the high nibble as given on the
+ ;; `a` register shifted 4 times right).
+ ;; - zp_arg1: X metatile coordinate (i.e. the low nibble as given on the
+ ;; `a` register and the high nibble zero'ed out).
+ ;; - zp_arg2: low byte for the corresponding PPU base address.
+ ;; - zp_arg3: high byte for the corresponding PPU base address.
+ .proc translate_mt_coordinates_to_arguments
+ ;; Save the X metatile coordinate. This is also saved into a temporary
+ ;; variable so it can be messed up when figuring out the PPU address.
+ tay
+ and #%00001111
+ sta Globals::zp_arg1
+ sta Globals::zp_tmp0
+
+ ;; Save the Y metatile coordinate.
+ tya
+ and #%11110000
+ lsr
+ lsr
+ lsr
+ lsr
+ sta Globals::zp_arg0
+
+ ;; Now that the metatile coordinates have been isolated, let's go for
+ ;; the PPU address.
+
+ ;; The initial value for the high byte of the PPU address depends on the
+ ;; nametable being used. For this we can take advantage of the fact that
+ ;; this information is on bit 2 of the global flags. If we mask this
+ ;; out, and add to it the base value of `$20`, we get either `$20` or
+ ;; `$24`, which is the base high byte for either nametable.
+ lda #%00000100
+ and Globals::zp_flags
+ clc
+ adc #$20
+ sta Globals::zp_arg3
+
+ ;; Every 4 rows the high byte of the PPU address increases by one. This
+ ;; means that with the Y metatile position, divide it by 4 (shift right
+ ;; twice), and you can add that to the high byte directly.
+ lda Globals::zp_arg0
+ lsr
+ lsr
+ clc
+ adc Globals::zp_arg3
+ sta Globals::zp_arg3
+
+ ;; After the high byte has been sorted out, notice that for the low byte
+ ;; the base can only be the four values as defined in `ppu_offsets`,
+ ;; as they repeat every four times.
+ lda Globals::zp_arg0
+ and #%00000011
+ tax
+ lda ppu_offsets, x
+
+ ;; To that base we need to apply the X coordinate. Since each metatile
+ ;; takes two tiles on the PPU, we can simply take the value on the X and
+ ;; shift it left once. And finally add the base low byte from the row
+ ;; with this shifted X value.
+ asl Globals::zp_tmp0
+ clc
+ adc Globals::zp_tmp0
+ sta Globals::zp_arg2
+
+ rts
+ ppu_offsets:
+ .byte $00, $40, $80, $C0
+ .endproc
+
+ ;; Load the column as indexed by `Background::zp_cur_column` and
+ ;; `Background:zp_cur_row`. This means that this function can be called at
+ ;; any time, and so rendering columns in multiple iterations is indeed
+ ;; possible. This is important to keep a small VRAM buffer.
+ ;;
+ ;; Returns 1 if the column has been completely loaded, 0 if there are rows
+ ;; still to be loaded for the current column after calling this function.
+ ;;
+ ;; This function also sets the `ppu` and the `column` flags as expected in
+ ;; each scenario, but it will *not* call functions such as
+ ;; `prepare_next_column`, as that is to be done by the caller if necessary.
+ .proc load_column
+ @start:
+ ;; First, compute the offset of the current metatile definition. That
+ ;; is, what's left from the current row until the Y position of the next
+ ;; metatile to push. First things first, check whether we are at the end
+ ;; of the screen definition.
+ ldy #0
+ lda (Metatile::zp_screen_ptr), y
+ cmp #$FF
+ bne @fetch_offset
+
+ ;; 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
+ sec
+ sbc zp_cur_row
+ beq @done
+ sta Globals::zp_idx
+ jmp @default_loop
+
+ @fetch_offset:
+ ;; Otherwise, if we weren't at the end of the screen definition, fetch
+ ;; the offset now. Note that if the offset is zero, then we can simply
+ ;; go and push the metatile definition.
+ jsr find_offset
+ beq @push
+
+ ;; This is an inner loop, which pushes the default metatile
+ ;; `Globals::zp_idx` - 1 times.
+ sta Globals::zp_idx
+ @default_loop:
+ ;; Make up a fake metatile position at the currently evaluated pair of
+ ;; row/column. Afterwards just push it. Doing all this dance is
+ ;; certainly not too expensive, but at the same time it could be easily
+ ;; optimized. This is not done out of simplicity.
+ lda zp_cur_row
+ asl
+ asl
+ asl
+ asl
+ ora zp_cur_column
+ jsr translate_mt_coordinates_to_arguments
+ lda #$00
+ jsr Buffer::push_metatile
+ beq @end
+
+ ;; The metatile was successfully pushed, now increase the row and check
+ ;; if we have to push another default metatile.
+ inc zp_cur_row
+ dec Globals::zp_idx
+ bne @default_loop
+
+ ;; We are done pushing default metatiles. Check if we are done with the
+ ;; column entirely.
+ lda zp_cur_row
+ cmp #$0F
+ beq @done
+
+ ;; Push the metatile pointed by the screen pointer.
+ @push:
+ ;; Pick up the current metatile position byte and set the arguments as
+ ;; expected by `Buffer::push_metatile`,
+ ;; `Collision::set_background_collision` and related functions.
+ ldy #0
+ lda (Metatile::zp_screen_ptr), y
+ jsr translate_mt_coordinates_to_arguments
+
+ ;; And now call `push_metatile` with the proper metatile definition.
+ ldy #1
+ lda (Metatile::zp_screen_ptr), y
+ jsr Buffer::push_metatile
+ beq @end
+
+ ;; The two bytes for the metatile have been consumed, now advance the
+ ;; screen pointer two bytes to point to the next metatile for the
+ ;; screen.
+ ADVANCE_SCREEN_PTR(2)
+
+ ;; Increase the row being evaluated and go for another iteration if this
+ ;; wasn't the last row for the column.
+ inc zp_cur_row
+ lda zp_cur_row
+ cmp #$10
+ bne @start
+
+ @done:
+ ;; Set the `ppu` flag and unset the `column` one. That is, a column is
+ ;; now fully buffered and the PPU needs to update things.
+ lda Globals::zp_flags
+ ora #%01000000
+ and #%11101111
+ sta Globals::zp_flags
+
+ lda #1
+ rts
+ @end:
+ ;; Set the `ppu` and the `column` flags. Even if the column is not fully
+ ;; buffered yet, we can already consume the VRAM buffer and empty it.
+ ;; Setting the `column` flag will ask code elsewhere to beware of a
+ ;; pending column to be loaded and act on it (e.g. by calling
+ ;; `load_column` again to finish the task).
+ lda Globals::zp_flags
+ ora #%01010000
+ sta Globals::zp_flags
+
+ lda #0
+ rts
+ .endproc
+
+ ;; Load a new column if the engine detects that it was needed. That is, it
+ ;; will ask to load a column unless a column is already being rendered, or
+ ;; the background cycle has not been consumed yet (or disabled entirely).
+ .proc load_column_if_needed
+ ;; If the `column` or the `level` flags are set, then we can skip the
+ ;; loading altogether.
+ lda #%00011000
+ and Globals::zp_flags
+ bne @end
+
+ ;; Is the background cycle disabled? If so just go to the end.
+ lda Background::zp_background_cycle
+ cmp #DISABLED_BACKGROUND_CYCLE
+ beq @end
+
+ ;; Instead of loading a column every time it can be loaded, we add a bit
+ ;; of a hiccup (i.e. "background cycle") so an entire level is not
+ ;; loaded right away after the player moves one bit.
+ inc Background::zp_background_cycle
+ beq @do_load
+ lda Background::zp_background_cycle
+ cmp #BACKGROUND_CYCLE_MAX
+ bne @end
+ lda #$00
+ sta Background::zp_background_cycle
+
+ @do_load:
+ ;; Load the column and increase the current column if it was fully
+ ;; buffered. Otherwise it will be picked up by the `update` function.
+ jsr load_column
+ beq @end
+ JAL prepare_next_column
+
+ @end:
+ rts
+ .endproc
+
+ ;; Load the next background column. You want to call this function on the
+ ;; outside whenever a new column might be needed. This function will
+ ;; eventually call `load_column_if_needed`, and it will also handle
+ ;; overflows on the scroll values, changing the nametable being used, etc.
+ ;;
+ ;; Because of the above, this is the most suitable function to call whenever
+ ;; you want to load a new column from the outside code, even if you are
+ ;; unsure whether it's really needed.
+ .proc load_next_background
+ ;; Was there an overflow on the scroll's value?
+ lda Background::zp_scroll
+ bne @load_column_if_needed
+
+ ;; Reset the background cycle so the next screen is picked up.
+ RESET_BACKGROUND_CYCLE
+
+ ;; Flip the nametable being used so the scroll makes sense for the
+ ;; current context. After that, skip the loading of the column because
+ ;; otherwise it would start loading on the currently displayed nametable
+ ;; and that would make for weird glitches.
+ lda PPU::zp_control
+ eor #%00000001
+ sta PPU::zp_control
+ jmp @update_ppu_and_quit
+
+ @load_column_if_needed:
+ jsr Background::load_column_if_needed
+
+ @update_ppu_and_quit:
+ ;; Regardless of whether the loading of the column happened or not, the
+ ;; scroll register has to change. Hence, set the `ppu` flag so at least
+ ;; the PPU::SCROLL register is updated.
+ lda Globals::zp_flags
+ ora #%01000000
+ sta Globals::zp_flags
+
+ rts
+ .endproc
+
+ ;; Reset all the variables in preparation for the loading of a new column.
+ ;; This function assumes that a column has already been fully buffered; and
+ ;; it will also handle the case where it was the last column of the
+ ;; screen/level by poking the right flags.
+ .proc prepare_next_column
+ ;; The row index always has to be set to zero.
+ lda #0
+ sta zp_cur_row
+
+ ;; Is this the last column? If not, just quit.
+ lda zp_cur_column
+ cmp #$0F
+ bne @end
+
+ ;; Flip the nametable to be used.
+ lda Globals::zp_flags
+ eor #%00000100
+ sta Globals::zp_flags
+
+ ;; Mark the background cycle as over. This will make functions like
+ ;; `load_column_if_needed` to refrain from loading a new column
+ ;; altogether.
+ lda #DISABLED_BACKGROUND_CYCLE
+ sta zp_background_cycle
+
+ ;; Reset the column to use. Note that the `@end` section will increase
+ ;; the column to be used.
+ lda #$FF
+ sta zp_cur_column
+
+ ;; If we were at the last column, we were out of metatiles for the
+ ;; current screen. Hence, move to the next screen if possible.
+ ADVANCE_SCREEN_PTR(1)
+
+ ;; Do we have another "$FF" byte? If so then we are at the end of the
+ ;; level entirely.
+ ldy #0
+ lda (Metatile::zp_screen_ptr), y
+ cmp #$FF
+ bne @end
+
+ ;; Mark the end of the level.
+ lda #%00001000
+ ora Globals::zp_flags
+ sta Globals::zp_flags
+
+ @end:
+ ;; Increase the current column and quit.
+ inc zp_cur_column
+ rts
+ .endproc
+
+ ;;;
+ ;; Find the distance between the currently evaluated row and the one pointed
+ ;; by the given argument. The argument is a byte where the Y value is
+ ;; located on the high nibble, as expected from a metatile definition.
+ ;;
+ ;; NOTE: the argument and the returned value are on the `a` register.
+ .proc find_offset
+ ;; Are we at the right column?
+ sta Globals::zp_arg0
+ and #%00001111
+ cmp zp_cur_column
+ bne @to_the_end
+
+ ;; We are: go until the current Y index.
+ lda Globals::zp_arg0
+ lsr
+ lsr
+ lsr
+ lsr
+ jmp @compute
+
+ @to_the_end:
+ ;; We aren't: go until the last row.
+ lda #$0F
+
+ @compute:
+ ;; Subtract the current row with the one we are trying to reach.
+ sec
+ sbc zp_cur_row
+ rts
+ .endproc
+.endscope
diff --git a/scroll/include/buffer.s b/scroll/include/buffer.s
new file mode 100644
index 0000000..fd08c92
--- /dev/null
+++ b/scroll/include/buffer.s
@@ -0,0 +1,258 @@
+;;;
+;; VRAM buffer
+;;
+;; As a general rule, you do *not* modify the PPU memory outside of `nmi` code:
+;; this can incur into graphical glitches because of writing into PPU memory
+;; while the PPU is consuming it. Instead, you append all writes into a VRAM
+;; buffer so the changes are finally committed during VBlank.
+;;
+;; The format of the VRAM buffer being used here is quite straight-forward
+;; and needs three bytes per update: two bytes for the PPU address, and
+;; another for the data to be passed into the PPU. This means that for each
+;; metatile we need 3 bytes per tile update * 4 tiles per metatile = 12
+;; bytes per update on a metatile. This is important when considering the
+;; capacity for this buffer. One idea could be to allow a whole column of
+;; metatiles to fit this buffer, which would require 12 bytes per metatile *
+;; 15 rows = 180 bytes. This does not look like a lot at first, but bear in
+;; mind that flushing this buffer has to be done during VBlank, which has a
+;; very tight time frame. Testing this number I noticed that even with 160
+;; bytes for this buffer (the suggested size on the NesDev wiki:
+;; https://www.nesdev.org/wiki/Sample_RAM_map) the NMI code would be
+;; struggling to fit the time frame.
+;;
+;; Thus, by poking with different values, I ended up with 96 bytes for this
+;; buffer, which allows for updates on 8 metatiles (a little more than half
+;; a screen's column). This looks quite reasonable with all the tests I have
+;; done.
+;;
+;; As a final note, on a real game you'd want to be more "aggressive" than this,
+;; as the format on the VRAM buffer directly influences how fast the code during
+;; VBlank can go. Hence, this buffer might not be suitable for your case.
+.scope Buffer
+ ;; The index of the next element to be evaluated on the VRAM buffer. If this
+ ;; index reaches `VRAM_BUFFER_CAP` then we are sure that we are done
+ ;; flushing updates. It's up for the user to notice when the buffer is full
+ ;; and should stop sending in more updates: the user should stop things as
+ ;; they are and continue on the next frame by picking up this index again.
+ zp_vram_idx = $19
+
+ ;; The VRAM buffer lives at a somewhat dangerous RAM address: $0100. This
+ ;; can be dangerous because we set the stack to start at $01FF and grows in
+ ;; decreasing addresses. Considering that the buffer is capped at $60, it
+ ;; means that if there are no buffer overflows, the memory layout for the
+ ;; $01xx RAM space is:
+ ;;
+ ;; - $0100-$0159: VRAM buffer (96 bytes).
+ ;; - $0160-$01FF: Stack (160 bytes).
+ ;;
+ ;; Crossing fingers there are no leaks on the VRAM buffer, we have 160 bytes
+ ;; for the stack, which realistically should be more than enough for any
+ ;; game out there. That is, unless you are abusing the stack a la
+ ;; Battletoads.
+ m_vram_buffer = $0100
+
+ ;; As explained above, the maximum capacity for the VRAM buffer, which
+ ;; amounts to 96 bytes (allowing for updates on 8 metatiles during VBlank).
+ VRAM_BUFFER_CAP = $60
+
+ ;; A tile in the VRAM buffer spans 3 bytes: 2 for the address, 1 for the
+ ;; sprite identifier. Since a metatile is made up of 4 tiles, then a
+ ;; metatile "bucket" is 3 * 4 = 12 bytes long.
+ METATILE_SIZE = 3 * 4
+
+ ;; Push the given metatile into the graphics and the collision map. How the
+ ;; graphics are updated depends on whether the PPU is enabled or not. If
+ ;; it's enabled then all updates are pushed into the VRAM buffer (given that
+ ;; enough space is available on this buffer, see more on the return value).
+ ;; Otherwise it pushes data into the PPU directly so everything is good
+ ;; after enabling the PPU back. This function expects the following
+ ;; *parameters*:
+ ;;
+ ;; - `a` register: the metatile definition as described in `metatile.s`.
+ ;; More specifically, the second byte on a metatile reference on the
+ ;; screen. Hence, something like `$81` for metatile with ID = 1 and with
+ ;; collision.
+ ;; - `zp_arg0`: the Y metatile coordinate.
+ ;; - `zp_arg1`: the X metatile coordinate.
+ ;; - `zp_arg2`: low byte for the corresponding PPU base address.
+ ;; - `zp_arg3`: high byte for the corresponding PPU base address.
+ ;;
+ ;; For the memory arguments see:
+ ;; `Background::translate_mt_coordinates_to_arguments`, as calling this
+ ;; function will set these values properly.
+ ;;
+ ;; *Returns* 1 if the push was successful, 0 otherwise. A push can only
+ ;; *not* be successful if the VRAM buffer doesn't have enough space for the
+ ;; push.
+ ;;
+ ;; NOTE: the VRAM size check also happens even if the PPU is disabled. That
+ ;; is, whenever you disable the PPU, make sure to mark the VRAM buffer as
+ ;; free as well.
+ .proc push_metatile
+ ;; Do we actually have room for this metatile? If not then just return
+ ;; early.
+ ldx zp_vram_idx
+ cpx #(VRAM_BUFFER_CAP - METATILE_SIZE)
+ bne @has_enough_space
+ lda #0
+ rts
+
+ @has_enough_space:
+ ;; Set the proper collision for the given metatile definition, while
+ ;; also preserving the value from the `a` register.
+ pha
+ jsr Collision::set_background_collision
+ pla
+
+ ;; Set the `y` register to contain the index on the `metatiles` table
+ ;; from `metatile.s`. This index will then be incremented on each
+ ;; iteration to index each tile on a metatile.
+ A_TO_METATILE_INDEX
+ tay
+
+ ;; Is the PPU enabled?
+ lda #%00011000
+ and PPU::zp_mask
+ bne @buffered_push
+
+ ;; The PPU is disabled, we can push directly graphics into it.
+ ldx #$00
+ @direct_push_loop:
+ bit PPU::STATUS
+
+ ;; Write the high byte of the PPU address being used.
+ lda Globals::zp_arg3
+ sta PPU::ADDRESS
+
+ ;; The low byte for the PPU address is the base as given from the
+ ;; parameter plus an offset that we have pre-computed (nothing too
+ ;; misterious, just check what we need to add for each tile of the
+ ;; metatile).
+ lda ppu_offsets, x
+ clc
+ adc Globals::zp_arg2
+ sta PPU::ADDRESS
+
+ ;; Load the tile id for this part of the metatile and increment the `y`
+ ;; register in preparation for the next iteration.
+ lda (Metatile::zp_metatile_ptr), y
+ sta PPU::DATA
+ iny
+
+ inx
+ cpx #4
+ bne @direct_push_loop
+ jmp @end
+
+ @buffered_push:
+ ;; Set on `Globals::zp_tmp0` the index on the metatile table, and on
+ ;; `Globals::zp_tmp1` the loop index.
+ sty Globals::zp_tmp0
+ lda #$00
+ sta Globals::zp_tmp1
+ ldx zp_vram_idx
+
+ @buffered_push_loop:
+ ;; The first element being pushed is the high byte for the PPU address
+ ;; to be used, which can be pushed directly.
+ lda Globals::zp_arg3
+ sta m_vram_buffer, x
+ inx
+
+ ;; The low byte needs some addition as in `@direct_push_loop`, but
+ ;; taking into account that the index is handled in memory instead of
+ ;; registers.
+ ldy Globals::zp_tmp1
+ lda ppu_offsets, y
+ clc
+ adc Globals::zp_arg2
+ sta m_vram_buffer, x
+ inx
+
+ ;; Load on `y` the index on the metatile table and increase it for the
+ ;; next iteration.
+ ldy Globals::zp_tmp0
+ inc Globals::zp_tmp0
+
+ ;; And push the tile id for this part.
+ lda (Metatile::zp_metatile_ptr), y
+ sta m_vram_buffer, x
+ inx
+
+ ;; Should we continue?
+ inc Globals::zp_tmp1
+ lda Globals::zp_tmp1
+ cmp #4
+ bne @buffered_push_loop
+
+ ;; And save the current index for the VRAM buffer.
+ stx zp_vram_idx
+
+ @end:
+ ;; End of the push, set the proper return value.
+ lda #1
+ rts
+ ppu_offsets:
+ .byte $00, $01, $20, $21
+ .endproc
+
+ ;; Flush the current contents of the VRAM buffer into the PPU. This function
+ ;; will also set the "ppu" flag if data was actually pushed.
+ ;;
+ ;; NOTE (NMI): this function should *only* be called during VBlank.
+ .proc flush_vram_buffer
+ ldx #$FF
+
+ @loop:
+ inx
+ cpx zp_vram_idx
+ beq @after_loop
+
+ bit PPU::STATUS
+
+ lda m_vram_buffer, x
+ sta PPU::ADDRESS
+
+ inx
+ lda m_vram_buffer, x
+ sta PPU::ADDRESS
+
+ inx
+ lda m_vram_buffer, x
+ sta PPU::DATA
+
+ jmp @loop
+
+ @after_loop:
+ ;; If no loop cycle was performed then, in theory, there is no data to
+ ;; be buffered into PPU. If this was not the case, and things had to be
+ ;; updated anyways (e.g. the scroll moved regardless of this), then it's
+ ;; up for the caller to decide.
+ lda zp_vram_idx
+ beq @end
+
+ ;; Buffer a write into the PPU control register, so the scroll is also
+ ;; left at a known state after touching the PPU address register.
+ lda #%01000000
+ ora Globals::zp_flags
+ sta Globals::zp_flags
+
+ ;; If there were some bytes actually consumed by this loop, ensure that
+ ;; it's reset.
+ lda #0
+ sta zp_vram_idx
+
+ @end:
+ rts
+ .endproc
+.endscope
+
+;; Flush the VRAM buffer if there are pending updates.
+;;
+;; NOTE (NMI): this macro should *only* be used during VBlank.
+.macro FLUSH_PENDING_VRAM_BUFFER
+ lda Buffer::zp_vram_idx
+ beq :+
+ jsr Buffer::flush_vram_buffer
+:
+.endmacro
diff --git a/scroll/include/collision.s b/scroll/include/collision.s
new file mode 100644
index 0000000..37debc5
--- /dev/null
+++ b/scroll/include/collision.s
@@ -0,0 +1,234 @@
+;;;
+;; Collisions for the background are handled by building up an array starting at
+;; RAM address $0700. This array contains a list of values to be considered when
+;; checking up background collision, and it's expected to be built when loading
+;; a screen or parts of it (e.g. `load_column`).
+;;
+;; In summary, each row of metatiles is mapped by two bytes. There are 16 tiles
+;; in a row (see `metatile.s`), so each bit from these two bytes map whether
+;; that metatile has a collision block or not. More specifically, the first bit
+;; from the first byte maps the first metatile on that row, the second bit from
+;; the first byte the second metatile on that row, and so on. Considering that
+;; there are 15 rows of tiles * 2 bytes for each row, it means that we can get a
+;; whole screen mapped with 30 bytes. As explained on the `metatile.s` file and
+;; taking the example of Super Mario Bros., things could have been further
+;; compressed by disregarding some of the rows which are anyways unaccessible by
+;; the player (e.g. by the HUD), but I have tried to keep things generic.
+;;
+;; Last but not least, we reserve space for two screens: one per nametable.
+;; Thus, the memory usage looks like this:
+;;
+;; - $0700-$071D: collision map for the screen on nametable 0.
+;; - $071E-$073B: collision map for the screen on nametable 1.
+.scope Collision
+ ;; The high byte value for the collision map (which lives at $0700 in RAM).
+ COLLISION_MAP_HI = $07
+
+ ;; Starting index for the collision map of the second screen.
+ COLLISION_MAP_SECOND = $1E
+
+ ;; Shadow references to Player::zp_screen_{x,y} and Background::zp_scroll.
+ ;; Not awesome, but cc65 doesn't allow to re-open scopes or similar
+ ;; code-sharing scenarios.
+ zp_player_screen_x = $30
+ zp_player_screen_y = $31
+ zp_background_scroll = $95
+
+ ;; Set/unset the proper bit on the collision map for the given metatile
+ ;; position and collision bit.
+ ;;
+ ;; The `a` register has to contain the collision information as given on a
+ ;; metatile reference for a screen. Hence, something like `$81` informs that
+ ;; metatile with id 1 is a solid block, while `$01` would mean the same but
+ ;; unsetting collision for that block.
+ ;;
+ ;; In memory you have to pass `Globals::zp_arg0` and `Globals::zp_arg1`,
+ ;; which contain the Y and X metatile coordinates as passed around in
+ ;; `Buffer::push_metatile`. This function won't touch other parameters, so
+ ;; future code can rely on `Globals::zp_arg2` and the rest to be preserved
+ ;; (i.e. only `Globals::zp_arg0` and `Globals::zp_arg1` will be affected).
+ .proc set_background_collision
+ ;; Preserve the collision byte for later.
+ sta Globals::zp_tmp0
+
+ ;; Select the screen to be used and save it into the `y` register, as
+ ;; expected by the `get_background_collision_y` function.
+ lda Globals::zp_flags
+ and #%00000100
+ lsr
+ lsr
+ tay
+
+ ;; Get the bitmap address for the Y and X coordinates.
+ jsr get_background_collision_y
+ tax
+
+ ;; Should we set or unset the collision bit?
+ lda Globals::zp_tmp0
+ bmi @set_collision
+
+ ;; We have to unset it: flip the mask, unset the given bit and save the
+ ;; result.
+ txa
+ eor #$FF
+ and (Globals::zp_arg0), y
+ sta (Globals::zp_arg0), y
+ rts
+
+ @set_collision:
+ ;; We have to set it: apply the given mask and save the result.
+ txa
+ ora (Globals::zp_arg0), y
+ sta (Globals::zp_arg0), y
+ rts
+ .endproc
+
+ ;; Get the collision value for the Y and X positions of a metatile. These
+ ;; two coordinates are given in memory arguments in this exact order, while
+ ;; the `y` register selects which map to pick from (i.e. the screen).
+ ;;
+ ;; Returns two bytes in memory which correspond to the 16-bit pointer to the
+ ;; row section that contains this metatile (remember that a row of metatiles
+ ;; in screen is split in two). This pointer is complemented by the fact that
+ ;; the `y` register will be set to the proper value. Hence, after calling
+ ;; this function you can get the collision bitmap for the row section with:
+ ;;
+ ;; lda (Globals::zp_arg0), y
+ ;;
+ ;; The `a` register will contain the mask which pin points the exact
+ ;; location on the X coordinate. This is useful for collision detection.
+ ;; Hence, combining with the above you can do:
+ ;;
+ ;; jsr get_background_collision_y
+ ;; and (Globals::zp_arg0), y ; Use the mask on `a` against the bitmap.
+ ;; bne @collision ; Collision!
+ ;;
+ .proc get_background_collision_y
+ ;; Each row is made up of two bytes worth of bitmaps. Hence, the Y
+ ;; metatile position has to be multiplied by two to get to the proper
+ ;; row.
+ lda Globals::zp_arg0
+ asl
+
+ ;; Depending on the nametable being mapped, we have to pick one map or
+ ;; the other. Since they are contiguous, this is a matter of simply
+ ;; adding the base address for the second map if that's the case.
+ cpy #1
+ bne @store_base_pointer
+ clc
+ adc #COLLISION_MAP_SECOND
+ @store_base_pointer:
+ sta Globals::zp_arg0
+
+ ;; Now it's time to figure things out given the value for the X
+ ;; coordinate. First of all, set on the `y` register which bitmap to
+ ;; pick for the row of metatiles (remember that a row is split in two
+ ;; bitmaps). Moreover, update the coordinate if it's on the second
+ ;; bitmap.
+ lda Globals::zp_arg1
+ ldy #0
+ cmp #8
+ bcc @eval_x
+ sec
+ sbc #8
+ iny
+
+ ;; With that, now we have to shift as many times as the X coordinate to
+ ;; get the mask for the bitmap we are pointing at.
+ @eval_x:
+ tax
+ lda #1
+ cpx #0
+ beq @end
+ @loop:
+ asl
+ dex
+ bne @loop
+
+ @end:
+ ;; The only thing missing is to set the high byte of the end 16-bit
+ ;; pointer.
+ ldx #COLLISION_MAP_HI
+ stx Globals::zp_arg1
+
+ ;; NOTE: Returned value is left untouched into the `a` register,
+ ;; computed in the previous loop.
+
+ rts
+ .endproc
+
+ ;; Translates the player's screen coordinates into metatile ones.
+ ;;
+ ;; This function takes two memory arguments, which are offsets to the Y and
+ ;; X screen coordinates respectively. The screen coordinates are taken
+ ;; directly from `Player::zp_screen_{y,x}`, and it also handles
+ ;; `Background::zp_scroll`.
+ ;;
+ ;; In turn this function will store in `zp_arg0` the Y metatile coordinate,
+ ;; and in `zp_arg1` the X one. The `y` register will also point if we are in
+ ;; nametable 0 or 1.
+ .proc screen_to_mt_coordinates
+ ;; Add up the player's screen coordinates with the given Y offset.
+ ;; Transforming that result into a metatile index is basically a matter
+ ;; of shifting right 4 times, since metatiles are 16x16 pixels.
+ lda Globals::zp_arg0
+ clc
+ adc Collision::zp_player_screen_y
+ lsr
+ lsr
+ lsr
+ lsr
+ sta Globals::zp_arg0
+
+ ;; The `y` register is generally the nametable where the focus is on.
+ ;; This is handled on the PPU control register, and shadowed by a
+ ;; `PPU::zp_control` variable. This will have to be adjusted depending
+ ;; on the current scroll. See more below.
+ lda #%00000001
+ and PPU::zp_control
+ tay
+
+ ;; Before figuring out the X metatile coordinates, compute the metatile
+ ;; coordinates if we were only to consider the scroll. This gives us
+ ;; information on from which metatile column is the current nametable
+ ;; visible. It will be used to correct the `y` register whenever columns
+ ;; from the next nametable start appearing on the player's area.
+ lda Collision::zp_background_scroll
+ lsr
+ lsr
+ lsr
+ lsr
+ sta Globals::zp_tmp0
+
+ ;; The coordinates for the X axis is similar to the Y one but we also
+ ;; need to take into account scrolling. Note that overflow is permitted
+ ;; as this will be handled by the correction code below (i.e. if the X
+ ;; metatile is beyond what can be seen by the current nametable, focus
+ ;; on the next one).
+ lda Globals::zp_arg1
+ clc
+ adc Collision::zp_player_screen_x
+ clc
+ adc Collision::zp_background_scroll
+ lsr
+ lsr
+ lsr
+ lsr
+ sta Globals::zp_arg1
+
+ ;; Is the X metatile coordinate actually smaller than the scrolling
+ ;; view?
+ cmp Globals::zp_tmp0
+ bcs @end
+
+ ;; Yes! Then we are on the next nametable. Amend the value on the `y`
+ ;; register and make sure it's either 1 or 0.
+ iny
+ tya
+ and #%00000001
+ tay
+
+ @end:
+ rts
+ .endproc
+.endscope
diff --git a/scroll/include/driver.s b/scroll/include/driver.s
new file mode 100644
index 0000000..abb3f5e
--- /dev/null
+++ b/scroll/include/driver.s
@@ -0,0 +1,98 @@
+;; The runtime of the game.
+.scope Driver
+ ;; Index of the current level.
+ zp_level = $77
+
+ ;; Whenever "Select" is pressed, it's used to count how many cycles have to
+ ;; pass before allowing another level switch.
+ zp_counter = $97
+
+ ;; Value that will be set for the `zp_counter` whenever the "Select" button
+ ;; is pressed for the first time.
+ SELECT_COUNTER_VALUE = $20
+
+ ;; Initialize global variables and other variables which have an impact on
+ ;; the runtime of the game.
+ .proc init
+ lda #0
+ sta Globals::zp_arg0
+ sta Globals::zp_arg1
+ sta Globals::zp_arg2
+ sta Globals::zp_arg3
+ sta Globals::zp_arg4
+ sta Globals::zp_tmp0
+ sta Globals::zp_tmp1
+ sta Globals::zp_tmp2
+ sta Globals::zp_tmp3
+ sta Globals::zp_idx
+ sta Globals::zp_flags
+ sta zp_level
+ sta zp_counter
+
+ rts
+ .endproc
+
+ ;; Function to be called at each iteration of the game loop. It loads any
+ ;; pending background columns and checks for level selection.
+ .proc update
+ ;; Are we actually loading a new level?
+ lda #%00100000
+ and Globals::zp_flags
+ beq @new_level
+
+ ;; Is the user actually allowed to hit the 'Select' button?
+ lda zp_counter
+ beq @check_select
+ dec zp_counter
+ jmp @check_column
+
+ @check_select:
+ ;; If `select` is pressed, then go for a new level. Otherwise check
+ ;; whether there is a pending column to be loaded.
+ lda #Joypad::BUTTON_SELECT
+ and Joypad::m_buttons1
+ beq @check_column
+
+ ;; Set the counter.
+ lda #SELECT_COUNTER_VALUE
+ sta zp_counter
+
+ ;; 'Select' was pressed and we weren't loading another level. Hence,
+ ;; increase the level index and load it.
+ inc zp_level
+ @new_level:
+ ;; Check the level index.
+ ldx zp_level
+ cpx #$02
+ bne :+
+ ldx #$00
+ :
+ ;; And load the computed level.
+ stx zp_level
+ jsr Background::load_level_x
+ beq :+
+
+ ;; Level could be rendered, enable back the PPU.
+ lda PPU::zp_mask
+ sta PPU::MASK
+
+ ;; Set the player back to its position.
+ RESET_PLAYER_POSITION
+ :
+ rts
+
+ @check_column:
+ ;; Do nothing if the `column` flag is not set.
+ lda #%00010000
+ and Globals::zp_flags
+ beq @end
+
+ ;; Continue loading the pending column.
+ jsr Background::load_column
+ beq @end
+ JAL Background::prepare_next_column
+
+ @end:
+ rts
+ .endproc
+.endscope
diff --git a/scroll/include/globals.s b/scroll/include/globals.s
index d941736..87a93ea 100644
--- a/scroll/include/globals.s
+++ b/scroll/include/globals.s
@@ -1,43 +1,39 @@
-;; Resets the background buffer to a zero-sized array.
-.macro CLEAR_BACKGROUND_BUFFER
- lda #$FF
- sta Globals::m_background_buffer
-.endmacro
-
-;; TODO: macro PUSH_TO_BACKGROUND_BUFFER_X
-
-;; Global variables used throughout the scrolling examples.
+;; Global variables used throughout the code base.
.scope Globals
;;;
- ;; Temporary values.
-
- m_tmp_1 = $90
- m_tmp_2 = $91
- m_tmp_3 = $92
-
- ;; Current value for the scroll on the X axis.
- m_scroll = $93
-
- m_background_idx = $19
+ ;; Argument values as defined in https://github.com/mssola/style.nes. Note
+ ;; that these variables can also be used as temporary variables.
+ zp_arg0 = $90
+ zp_arg1 = $91
+ zp_arg2 = $92
+ zp_arg3 = $93
+ zp_arg4 = $94
- ;; TODO
- m_collisions = $20
-
- ;; TODO
- m_background_buffer = $40
-
- ;; Initialize global variables.
- .proc init
- lda #0
- sta m_tmp_1
- sta m_tmp_2
- sta m_tmp_3
- sta m_scroll
- sta m_background_idx
-
- CLEAR_BACKGROUND_BUFFER
- sta m_collisions
+ ;;;
+ ;; Random values that can be used inside of functions for temporary values
+ ;; so `zp_argX` variables are not overwritten as often.
+ zp_tmp0 = $9A
+ zp_tmp1 = $9B
+ zp_tmp2 = $9C
+ zp_tmp3 = $9D
- rts
- .endproc
+ ;;;
+ ;; Reserve a byte of memory for preserving indices on memory. This is needed
+ ;; whenever the `x` and `y` registers might not be reliable because of
+ ;; underlying `jsr` calls that might tamper with their values. Sometimes
+ ;; saving the value in memory is enough instead of playing with the stack.
+ zp_idx = $9E
+
+ ;; Flags that manage the state of the game.
+ ;;
+ ;; | Bit | Short name | Meaning when set |
+ ;; |-----+------------+-------------------------------------------------------------|
+ ;; | 7 | render | Game logic is over, block main code until NMI code is over. |
+ ;; | 6 | ppu | PPU registers (2000 and scroll) have to be updated. |
+ ;; | 5 | level | The current level is active. |
+ ;; | 4 | column | The current column is still being buffered into the PPU. |
+ ;; | 3 | end | We are at the end of the level |
+ ;; | 2 | nametable | Next nametable to be used |
+ ;; | 1-0 | - | Unused |
+ zp_flags = $20
.endscope
diff --git a/scroll/include/metatile.s b/scroll/include/metatile.s
new file mode 100644
index 0000000..01dd0e8
--- /dev/null
+++ b/scroll/include/metatile.s
@@ -0,0 +1,209 @@
+;;;
+;; This file contains definitions for metatiles and screens.
+;;
+;; *Metatiles* are a way to group multiple related background tiles into a
+;; single entity. For the programs using this file, a metatile is a square of 4
+;; tiles: that is, a block of 16x16 pixels. Screens are going to use metatiles
+;; as building blocks, never tiles in on themselves. This means that rendering
+;; and collision checking is done at the metatile level, never at the tile
+;; level. This simplifies memory consumption a lot (e.g. smaller collision maps,
+;; smaller screen definitions, etc.). As how things are defined here, a metatile
+;; is a list of four consecutive bytes, which each represent the index on the
+;; pattern table for the top-left, top-right, bottom-left and bottom-right tiles
+;; respectively.
+;;
+;; Because of the above, a *screen* is laid out as a grid of 16x15 metatiles.
+;; This means that we can set the metatile position with a single byte. In our
+;; case, the high nibble will represent the "y coordinate" and the low one the
+;; "x coordinate". For example, if we have a byte like so "$12", then it means
+;; that it's the metatile at "1" on the vertical axis of the metatile grid, and
+;; "2" on the horizontal one. In the rest of the code we would also say that $01
+;; are its Y "metatile coordinates" and $02 its X "metatile coordinates". We
+;; could have gone a step further and do like Super Mario Bros. which disregards
+;; some of the metatile rows because they are not entirely visible anyways on a
+;; CRT screen and because of the HUD on top. This amount of compression is not
+;; needed here. That being said, there is one important gotcha: tiles have to be
+;; sorted, both on the X and the Y axis. This makes the background loader
+;; snappier.
+;;
+;; Other than that, we use another byte to store properties for each metatile
+;; that we are placing. Since this is quite simple, we just reserve bit 7 to set
+;; whether the metatile is to be considered for collisions or not, and the rest
+;; encodes the metatile index. The metatile index simply identifies which of the
+;; metatiles in `metatiles` we are referencing. Thus, a value of "$81" means
+;; that we want the metatile with index 1 on `metatiles` and that it has to be
+;; considered for collision checking.
+;;
+;; All in all, a screen here is a bunch of pairs of bytes, each pair encoding a
+;; metatile for the screen. A screen definition stops whenever there is the byte
+;; $FF.
+;;
+;; A *level* consists of one or more screens, each of them enclosed by the
+;; termination byte $FF. A level is finished whenever we find the termination
+;; $FF after another $FF one. Levels are indexed by the `levels_lo` and
+;; `levels_hi` lists, which contain the low byte and the high byte respectively
+;; of the address for each level. Splitting a 16-bit pointer into two separate
+;; lists is quite convenient due to the architecture of the 6502 and indexing
+;; operations. The level list is also closed by a '$FF' byte, which would result
+;; in an impossible $FFFF pointer.
+;;
+;; One missing (and obvious) feature is allowing to also specify different
+;; values for the attribute table. But I thought that this would add more
+;; complexity to a code that was already too complex for the sake of giving away
+;; an example.
+
+;; Advance the screen pointer by the given `increment`.
+.macro ADVANCE_SCREEN_PTR increment
+ lda Metatile::zp_screen_ptr
+ clc
+ adc #increment
+ sta Metatile::zp_screen_ptr
+ lda #0
+ adc Metatile::zp_screen_ptr + 1
+ sta Metatile::zp_screen_ptr + 1
+.endmacro
+
+;; Transform the current value of the `a` register to a proper index for the
+;; `metatiles` table. Note that the current value on the `a` register is assumed
+;; to be a metatile definition (that is, the second byte of a metatile on a
+;; screen). The end result is also left on the `a` register.
+.macro A_TO_METATILE_INDEX
+ ;; Each metatile definition on `metatiles` is 4 bytes long. Hence, the index
+ ;; on that table is simply the given index multiplied by four. Or more
+ ;; simply, shifted left twice. Moreover, shifting left at least once removes
+ ;; the most significant bit, which was the collision bit that we needed to
+ ;; discard anyways. All in all, this operation for now is simply shifting
+ ;; the current value twice.
+ asl
+ asl
+.endmacro
+
+;; Holds variables which are useful to manipulate metatiles and how they are
+;; laid out on screens.
+.scope Metatile
+ ;; The "screen pointer". A 16-bit pointer which points to the current
+ ;; metatile definition for the current screen.
+ zp_screen_ptr = $70
+ zp_screen_ptr1 = $71
+
+ ;; The "metatile pointer". A 16-bit pointer which points to the base table
+ ;; of metatile definitions.
+ zp_metatile_ptr = $72
+ zp_metatile_ptr2 = $73
+
+ ;; Initialize the pointers to handle metatiles on the game.
+ .proc init
+ lda #<metatiles
+ sta zp_metatile_ptr
+ lda #>metatiles
+ sta zp_metatile_ptr + 1
+
+ ;; NOTE: the screen pointer is supposed to be initialized when loading a
+ ;; new level.
+
+ rts
+ .endproc
+.endscope
+
+;; List of metatiles.
+metatiles:
+ ;; Default metatile: transparent.
+ .byte $00, $00, $00, $00
+
+ ;; Super Mario Bros. block.
+ .byte $05, $07, $06, $08
+
+ ;; Super Mario Bros. ground.
+ .byte $02, $01, $03, $04
+
+;; Low bytes of the address for each level.
+levels_lo:
+ .byte <level1, <level2
+
+ ;; End of levels.
+ .byte $FF
+
+;; High bytes of the address for each level.
+levels_hi:
+ .byte >level1, >level2
+
+ ;; End of levels.
+ .byte $FF
+
+;;;
+;; List of levels.
+
+level1:
+ ;;;
+ ;; Screen 1.
+ .byte $10, $81
+ .byte $40, $81
+ .byte $50, $81
+ .byte $80, $81
+ .byte $A0, $81
+ .byte $94, $01
+ .byte $65, $81
+ .byte $75, $81
+ .byte $37, $81
+ .byte $38, $81
+ .byte $39, $81
+ .byte $3A, $81
+ .byte $3B, $81
+ .byte $3C, $81
+ .byte $4C, $81
+ .byte $3D, $81
+ .byte $4F, $81
+ .byte $FF
+
+ ;;;
+ ;; Screen 2.
+ .byte $22, $81
+ .byte $A4, $81
+ .byte $C6, $81
+ .byte $FF
+
+ ;;;
+ ;; Screen 3.
+ .byte $43, $81
+ .byte $24, $81
+ .byte $A6, $81
+ .byte $FF
+
+ ;;;
+ ;; Screen 4.
+ .byte $75, $81
+ .byte $37, $81
+ .byte $38, $81
+ .byte $39, $81
+ .byte $3A, $81
+ .byte $FF
+
+ ;;;
+ ;; End of level.
+ .byte $FF
+
+level2:
+ ;;;
+ ;; Screen 1.
+ .byte $14, $81
+ .byte $25, $81
+ .byte $36, $81
+ .byte $FF
+
+ ;;;
+ ;; Screen 2.
+ .byte $47, $81
+ .byte $58, $81
+ .byte $69, $81
+ .byte $FF
+
+ ;;;
+ ;; Screen 3.
+ .byte $7A, $81
+ .byte $8B, $81
+ .byte $9C, $81
+ .byte $FF
+
+ ;;;
+ ;; End of level.
+ .byte $FF
diff --git a/scroll/include/oam.s b/scroll/include/oam.s
index ecd1b7d..5680a93 100644
--- a/scroll/include/oam.s
+++ b/scroll/include/oam.s
@@ -1,5 +1,3 @@
-.segment "CODE"
-
.scope OAM
ADDR = $2003
DMA = $4014
diff --git a/scroll/include/palettes.s b/scroll/include/palettes.s
index 9403b7b..1b9f20c 100644
--- a/scroll/include/palettes.s
+++ b/scroll/include/palettes.s
@@ -3,8 +3,18 @@
DEFAULT_COLOR = $11
;; Copies all the palettes for our game into the proper PPU address.
+ ;;
+ ;; NOTE: as explained in `metatile.s`, the engine for the scrolling examples
+ ;; lack the ability to update the attributes for each tile. This is
+ ;; embarrasing, but adding support for it would make these examples more
+ ;; complex than they need to be. Hence we just reproduce the same palette
+ ;; all over and avoid glitches on real hardware or emulators with randomized
+ ;; memory.
.proc init
- PPU_ADDR $3F00
+ lda #$3F
+ sta PPU::ADDRESS
+ lda #$00
+ sta PPU::ADDRESS
ldx #0
@load_palettes_loop:
@@ -17,14 +27,14 @@
palettes:
;; Background
.byte DEFAULT_COLOR, $36, $17, $0F
- .byte DEFAULT_COLOR, $00, $00, $00
- .byte DEFAULT_COLOR, $00, $00, $00
- .byte DEFAULT_COLOR, $00, $00, $00
+ .byte DEFAULT_COLOR, $36, $17, $0F
+ .byte DEFAULT_COLOR, $36, $17, $0F
+ .byte DEFAULT_COLOR, $36, $17, $0F
;; Foreground
.byte DEFAULT_COLOR, $28, $0F, $30
- .byte DEFAULT_COLOR, $00, $00, $00
- .byte DEFAULT_COLOR, $00, $00, $00
- .byte DEFAULT_COLOR, $00, $00, $00
+ .byte DEFAULT_COLOR, $28, $0F, $30
+ .byte DEFAULT_COLOR, $28, $0F, $30
+ .byte DEFAULT_COLOR, $28, $0F, $30
.endproc
.endscope
diff --git a/scroll/include/player.s b/scroll/include/player.s
index 95d710d..239cec6 100644
--- a/scroll/include/player.s
+++ b/scroll/include/player.s
@@ -1,87 +1,322 @@
+;; Set the player to its initial coordinates.
+.macro RESET_PLAYER_POSITION
+ lda #40
+ sta Player::zp_screen_x
+ sta Player::zp_screen_y
+.endmacro
+
;; This is the player code that gets re-used on all scrolling examples. This
;; will show a meta-sprite made up of four sprites that make up diskun. You can
;; move this player with the arrows, and the movement will be pretty basic (1
-;; pixel each time). Furthermore, the sprite handles collision with background
-;; elements (TODO), which is shared across all the scrolling examples.
+;; pixel at a time). Furthermore, the sprite handles collision with background
+;; elements, which is shared across all the scrolling examples.
;;
-;; The sprite is stored in OAM memory $200-$20F, and the $30 and $31 memory
-;; addresses are used for keeping up with the screen coordinates.
+;; The sprite is stored in OAM memory $204-$213 (hence sprite0 is left out so
+;; other examples can use it), and the $30 and $31 memory addresses are used for
+;; keeping up with the screen coordinates.
.scope Player
- m_screen_x = $30
- m_screen_y = $31
+ ;; The minimum value that the player is allowed to have on the Y axis.
+ MIN_SCREEN_Y = 8
+
+ ;; The maximum value of X that is allowed before we start updating the
+ ;; scroll value.
+ MAX_SCREEN_X = 144
+
+ ;; The height for the player when it comes to detecting the edge of the
+ ;; screen or background collisions.
+ PLAYER_HEIGHT = 25
+
+ ;; The width for the player when it comes to detecting the edge of the
+ ;; screen or background collisions.
+ PLAYER_WIDTH = 16
+
+ ;; Indeces for the collisions table as defined on `check_bg_collision_x` so
+ ;; the setting of the `x` register is less magical.
+ TOP_COLLISION_INDEX = (0 << 2)
+ RIGHT_COLLISION_INDEX = (1 << 2)
+ DOWN_COLLISION_INDEX = (2 << 2)
+ LEFT_COLLISION_INDEX = (3 << 2)
+ ;; Coordinates on the X axis.
+ ;; NOTE: shadowed in collision.s. If you change it here, change it there as
+ ;; well.
+ zp_screen_x = $30
+
+ ;; Coordinates on the Y axis.
+ ;; NOTE: shadowed in collision.s. If you change it here, change it there as
+ ;; well.
+ zp_screen_y = $31
+
+ ;; Initialize the sprite for the player.
.proc init
- lda #40
- sta m_screen_x
- sta m_screen_y
+ RESET_PLAYER_POSITION
lda #$01
- sta $201
- lda #$02
sta $205
- lda #$11
+ lda #$02
sta $209
- lda #$12
+ lda #$11
sta $20D
+ lda #$12
+ sta $211
lda #0
- sta $202
sta $206
sta $20A
sta $20E
+ sta $212
rts
.endproc
+ ;; The update for the player is simply about checking for the d-pad. Note
+ ;; that if pressing right, then the scroll value might also move, which
+ ;; might mean to load the next background column until the end of the
+ ;; screen.
.proc update
- jsr update_coordinates
- jsr update_sprites
- rts
- .endproc
-
- .proc update_coordinates
+ ;; Is the player requesting to go up?
lda #Joypad::BUTTON_UP
and Joypad::m_buttons1
beq @check_down
- dec m_screen_y
+
+ ;; If we are already at the top disregard this button press and check
+ ;; for the left button.
+ lda #MIN_SCREEN_Y
+ cmp zp_screen_y
+ beq @check_left
+
+ ;; If, for whatever reason, we are below the minimum value, then reset
+ ;; the value to this minimum Y value and jump to check the left button.
+ bcc @update_y_up
+ sta zp_screen_y
+ jmp @check_left
+
+ @update_y_up:
+ ;; Try to go up and check for a collision with the background.
+ dec zp_screen_y
+ dec zp_screen_y
+ ldx #TOP_COLLISION_INDEX
+ jsr check_bg_collision_x
+ beq @check_left
+
+ ;; A collision was detected. Then get back to the old value of Y and
+ ;; move on.
+ inc zp_screen_y
+ inc zp_screen_y
jmp @check_left
+
@check_down:
+ ;; Is the player requesting to go down?
lda #Joypad::BUTTON_DOWN
and Joypad::m_buttons1
beq @check_left
- inc m_screen_y
+
+ ;; We have to move down unless we are already at the very bottom.
+ lda zp_screen_y
+ cmp #(240 - PLAYER_HEIGHT)
+ bcc @update_y_down
+ lda #(240 - PLAYER_HEIGHT)
+ sta zp_screen_y
+ jmp @check_left
+ @update_y_down:
+ ;; Try to go down as requested, but check for a collision with the
+ ;; background.
+ inc zp_screen_y
+ inc zp_screen_y
+ ldx #DOWN_COLLISION_INDEX
+ jsr check_bg_collision_x
+ beq @check_left
+
+ ;; There was a collision with the background, restore back the value on
+ ;; Y and move on.
+ dec zp_screen_y
+ dec zp_screen_y
+
@check_left:
+ ;; Is the player requesting to go left?
lda #Joypad::BUTTON_LEFT
and Joypad::m_buttons1
beq @check_right
- dec m_screen_x
+
+ ;; We have to move left unless we are already at the leftmost edge.
+ lda zp_screen_x
+ bne :+
+ rts
+ :
+ ;; We are not at the leftmost edge, try to go left while also checking
+ ;; for a background collision.
+ dec zp_screen_x
+ dec zp_screen_x
+ ldx #LEFT_COLLISION_INDEX
+ jsr check_bg_collision_x
+ bne :+
rts
+ :
+ ;; There was a collision, restore back the value on X and quit.
+ inc zp_screen_x
+ inc zp_screen_x
+ rts
+
@check_right:
+ ;; Last check! Is the player requesting to go right?
lda #Joypad::BUTTON_RIGHT
and Joypad::m_buttons1
+ bne @check_level_end
+ rts
+
+ @check_level_end:
+ ;; Are we at the last screen? If so consume the rest of the missing
+ ;; scroll. Whenever that is done (i.e. there are no more screens and the
+ ;; scroll sits at a zero value), then we can no longer load more columns
+ ;; or update the scroll: stick to updating the X position.
+ lda #%00001000
+ and Globals::zp_flags
+ beq @check_max_screen
+ lda Background::zp_scroll
+ beq @update_x
+
+ @check_max_screen:
+ ;; Is the X position already at the MAX_SCREEN_X? If so, then we don't
+ ;; move the character: we move the scroll instead.
+ lda zp_screen_x
+ cmp #Player::MAX_SCREEN_X
+ bcc @update_x
+
+ ;; Moving will then be a matter of increasing the scroll value.
+ inc Background::zp_scroll
+ inc Background::zp_scroll
+ ldx #RIGHT_COLLISION_INDEX
+ jsr check_bg_collision_x
+ bne @collision_on_scroll
+
+ ;; If there are no collisions in the scrolling scenario, then we might
+ ;; need to load the next background column in advance. Whether that is
+ ;; possible or something that we want to do is handled automatically by
+ ;; the `Background::load_next_background` function. Hence, call this
+ ;; function and quit.
+ JAL Background::load_next_background
+
+ @collision_on_scroll:
+ ;; Collision was given. Hence, restore the value for the scroll and
+ ;; quit.
+ dec Background::zp_scroll
+ dec Background::zp_scroll
+ rts
+
+ @update_x:
+ ;; Do not overwrap the screen.
+ lda zp_screen_x
+ cmp #(256 - PLAYER_WIDTH)
beq @end
- inc m_screen_x
- ;; TODO: move scroll
+
+ ;; We are not about to overwrap the screen, let's update the value on
+ ;; the X axis and check whether a collision would happen.
+ inc zp_screen_x
+ inc zp_screen_x
+ ldx #RIGHT_COLLISION_INDEX
+ jsr check_bg_collision_x
+ beq @end
+
+ ;; There was a collision, roll back the value for the X axis.
+ dec zp_screen_x
+ dec zp_screen_x
+
@end:
rts
.endproc
- .proc update_sprites
- lda m_screen_x
- sta $203
- sta $20B
- clc
- adc #8
+ ;; Check collision between the player and any background element. Note that
+ ;; collision is not tested on all edges for the player, but you have to
+ ;; provide a "collision index", which refers to which edges to test. See the
+ ;; "*_COLLISION_INDEX" constants above and the `edges` data included inside
+ ;; of this function. This collision index has to be provided on the `x`
+ ;; register.
+ ;;
+ ;; Returns a non-zero value on the `a` register if a collision was detected,
+ ;; zero otherwise.
+ .proc check_bg_collision_x
+ ;; We have to check for a collision on both the left and the right
+ ;; edges, which should follow the same code. Unrolling it is simply
+ ;; faster and more clear than setting up a loop.
+ .repeat 2, I
+ ;; Save the offset on the Y coordinate.
+ lda edges, x
+ sta Globals::zp_arg0
+
+ ;; Save the offset on the X coordinate.
+ inx
+ lda edges, x
+ sta Globals::zp_arg1
+
+ ;; Save the edge index in preparation for the following calls.
+ .if I = 0
+ inx
+ stx Globals::zp_idx
+ .endif
+
+ ;; Transform screen coordinates into metatile ones and fetch the
+ ;; collision bitmap for it with its mask.
+ jsr Collision::screen_to_mt_coordinates
+ jsr Collision::get_background_collision_y
+
+ ;; Restore back the edge index.
+ .if I = 0
+ ldx Globals::zp_idx
+ .endif
+
+ ;; Checking for a collision is as simple as performing an AND with the
+ ;; bitmap at the `a` register and the byte that can be addressed with
+ ;; the given pointer. If the result is non-zero, then we found a match.
+ and (Globals::zp_arg0), y
+ .if I = 0
+ ;; If I = 1 we are falling through as expected anyways, so this
+ ;; instruction is not needed.
+ bne @collision
+ .endif
+ .endrepeat
+
+ ;; The last instruction before ending up here will already have the `a`
+ ;; register lined up to the proper return value. If a collision was
+ ;; found (in either left/right case), `bne @collision` is good, and
+ ;; hence `a` has an expected non-zero value. The same is true when not
+ ;; in a collision.
+ @collision:
+ rts
+
+ edges:
+ ;; Up: top-left (y, x), top-right (y, x)
+ .byte $02, $02, $02, $0F
+
+ ;; Right: top-right (y, x), bottom-right (y, x)
+ .byte $02, $0F, $0F, $0F
+
+ ;; Down: bottom-left (y, x), bottom-right (y, x)
+ .byte $0F, $02, $0F, $0F
+
+ ;; Left: top-left (y, x), bottom-left (y, x)
+ .byte $02, $01, $0F, $01
+ .endproc
+
+ ;; Update the memory on the PPU with the current values for the screen
+ ;; coordinates of the player.
+ ;;
+ ;; NOTE: only call this function on NMI Code.
+ .proc update_sprite
+ lda zp_screen_x
sta $207
sta $20F
+ clc
+ adc #8
+ sta $20B
+ sta $213
- lda m_screen_y
- sta $200
+ lda zp_screen_y
sta $204
+ sta $208
clc
adc #8
- sta $208
sta $20C
+ sta $210
rts
.endproc
diff --git a/scroll/include/ppu.s b/scroll/include/ppu.s
index 23fd978..34410a0 100644
--- a/scroll/include/ppu.s
+++ b/scroll/include/ppu.s
@@ -1,5 +1,3 @@
-.segment "CODE"
-
.scope PPU
CONTROL = $2000
MASK = $2001
@@ -7,21 +5,10 @@
SCROLL = $2005
ADDRESS = $2006
DATA = $2007
-.endscope
-.macro PPU_ADDR address
- lda #.HIBYTE(address)
- sta PPU::ADDRESS
- lda #.LOBYTE(address)
- sta PPU::ADDRESS
-.endmacro
+ ;; Variable that shadows the value on PPU::CONTROL.
+ zp_control = $80
-;; WRITE_PPU_DATA is a macro that will write into PPU::ADDRESS the given address
-;; and into PPU::DATA the given byte value.
-.macro WRITE_PPU_DATA address, value
- bit PPU::STATUS
-
- PPU_ADDR address
- lda #value
- sta PPU::DATA
-.endmacro
+ ;; Variable that shadows the value on PPU::MASK.
+ zp_mask = $81
+.endscope
diff --git a/scroll/include/reset.s b/scroll/include/reset.s
new file mode 100644
index 0000000..2402795
--- /dev/null
+++ b/scroll/include/reset.s
@@ -0,0 +1,62 @@
+;; Check `basics/sprite.s` for a deeper look on the logic below.
+.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
+
+ lda #$00
+ sta OAM::ADDR
+ lda #$02
+ sta OAM::DMA
+
+@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
diff --git a/scroll/level.s b/scroll/level.s
index 9480a4a..3bca454 100644
--- a/scroll/level.s
+++ b/scroll/level.s
@@ -1 +1,143 @@
-;; TODO: Scroll everything and show how to load the level
+.segment "HEADER"
+ .byte 'N', 'E', 'S', $1A
+
+ .byte $02
+ .byte $01
+
+ .byte $01 ; Vertical mirroring (important since we are using horizontal scrolling!)
+ .byte $00
+
+.segment "VECTORS"
+ .addr nmi, reset, irq
+
+.segment "CODE"
+
+.include "include/all.s"
+
+.proc main
+ ;; The PPU mask is zero'ed out so no background nor sprites will be shown at
+ ;; first. This will be the task of the `load_level_x` function which is
+ ;; called during background initialization.
+ lda #$00
+ sta PPU::zp_mask
+ sta PPU::MASK
+
+ ;; Initialize engine.
+ jsr Palettes::init
+ jsr Metatile::init
+ jsr Driver::init
+ jsr Background::init
+ jsr Player::init
+
+ ;; Accept back interrupts.
+ cli
+
+ ;; 7: allow NMI; 5-4: background pattern table at $0000, sprites at $1000.
+ ;;
+ ;; NOTE: we store this same value both into the PPU register and into this
+ ;; special `zp_control` variable. This variable will shadow the contents for
+ ;; the PPU control register. Any operation that requires changing the value
+ ;; for this register, from now on, we will simply touch this variable
+ ;; instead. This is because from now on touching this register is only safe
+ ;; inside of Vblank (a.k.a. NMI code). Our `nmi` handler has special code to
+ ;; check whether there was an update on this register's value and update
+ ;; things accordingly.
+ lda #%10001000
+ sta PPU::zp_control
+ sta PPU::CONTROL
+
+@main_game_loop:
+ jsr joypad_read
+ jsr Player::update
+ jsr Driver::update
+
+ ;; Set the `render` flag, meaning that the code logic is over and we can
+ ;; safely go into `nmi` code.
+ lda #%10000000
+ ora Globals::zp_flags
+ sta Globals::zp_flags
+
+@wait_for_render:
+ bit Globals::zp_flags
+ bmi @wait_for_render
+
+ ;; Rendering is done, we can perform another iteration of the loop!
+ jmp @main_game_loop
+.endproc
+
+;; The basics are laid out in `basics/sprite.s`. Read the comments below for
+;; more info.
+.proc nmi
+ ;; Skip this entirely if code is not finished. This is a safeguard, but
+ ;; whenever it happens it means that we are stalling at least for a frame.
+ bit Globals::zp_flags
+ bpl @next
+
+ ;; Save registers
+ pha
+ txa
+ pha
+ tya
+ pha
+
+ ;; Update the sprite of the player. For this simple game, it just means to
+ ;; update its position.
+ jsr Player::update_sprite
+
+ ;; Render stuff.
+ OAM_WRITE_SPRITES
+
+ ;; Flush any pending background updates. This macro actually hides the most
+ ;; expensive part executed during VBlank. Check the document on `buffer.s`
+ ;; for more information.
+ FLUSH_PENDING_VRAM_BUFFER
+
+ ;; Should we update PPU registers? If not, then we can go down to the next
+ ;; section. Otherwise we need to update the PPU registers that have been
+ ;; buffered and set the scroll.
+ bit Globals::zp_flags
+ bvc @after_ppu
+
+ ;; Zero out the "ppu" flag.
+ lda #%10111111
+ and Globals::zp_flags
+ sta Globals::zp_flags
+
+ ;; Reset the PPU address latch.
+ bit PPU::STATUS
+
+ ;; Update the PPU control/mask registers with their buffered values.
+ lda PPU::zp_control
+ sta PPU::CONTROL
+ lda PPU::zp_mask
+ sta PPU::MASK
+
+ ;; Update scroll.
+ lda Background::zp_scroll
+ sta PPU::SCROLL
+ lda #$00
+ sta PPU::SCROLL
+
+@after_ppu:
+ ;; And unset the `render` flag so the `main` code is unblocked.
+ lda #%01111111
+ and Globals::zp_flags
+ sta Globals::zp_flags
+
+ ;; Restore registers.
+ pla
+ tay
+ pla
+ tax
+ pla
+@next:
+ rti
+.endproc
+
+;; Unused.
+.proc irq
+ rti
+.endproc
+
+.segment "CHARS"
+ .incbin "../assets/diskun.chr"
diff --git a/scroll/roulette.s b/scroll/roulette.s
index aca7d72..dae89de 100644
--- a/scroll/roulette.s
+++ b/scroll/roulette.s
@@ -24,10 +24,10 @@
;; Variables used on this example.
.scope Vars
- top_scroll = $00
- center_scroll = $01
- bottom_scroll = $02
- is_bottom = $04 ; 0 -> scroll center; 1 -> scroll bottom
+ zp_top_scroll = $00
+ zp_center_scroll = $01
+ zp_bottom_scroll = $02
+ zp_is_bottom = $04 ; 0 -> scroll center; 1 -> scroll bottom
.endscope
.segment "HEADER"
@@ -114,7 +114,7 @@
.segment "TAIL"
.include "../shared/ppu.s"
-reset:
+.proc reset
sei
cld
@@ -126,7 +126,7 @@ reset:
ldx #$40
stx $4017
- ldx #$ff
+ ldx #$FF
txs
inx
@@ -172,7 +172,7 @@ reset:
inx
bne @ram_reset_loop
- lda #$ef
+ lda #$EF
@sprite_reset_loop:
sta $200, x
inx
@@ -199,6 +199,7 @@ reset:
dex
bne @palettes_reset_loop
jmp main
+.endproc
;; The main function is used here only for further initialization purposes.
.proc main
@@ -369,7 +370,7 @@ reset:
;; has to do mainly two things:
;; 1. Set up a scanline IRQ so the scroll at the center/bottom is different.
;; 2. Set the scroll for the top region.
-nmi:
+.proc nmi
bit $20
bpl @next
@@ -406,10 +407,10 @@ nmi:
;; the top section. The scroll will only happen on the X axis and it's going
;; to be a bit fast.
bit $2002
- lda Vars::top_scroll
+ lda Vars::zp_top_scroll
clc
adc #2
- sta Vars::top_scroll
+ sta Vars::zp_top_scroll
sta $2005
lda #$00
sta $2005
@@ -427,6 +428,7 @@ nmi:
pla
@next:
rti
+.endproc
;;;
;; NOTE: handle a scanline IRQ.
@@ -435,7 +437,7 @@ nmi:
;; moment, but we have disabled the frame counter on our `reset` code, so on the
;; context of the MMC3 chip the only thing left are scanline IRQs, which we have
;; set up on `nmi` code.
-irq:
+.proc irq
;; Save current context.
pha
txa
@@ -448,30 +450,30 @@ irq:
stx MMC3::IRQ_DISABLE
;; What are we trying to scroll, exactly?
- lda Vars::is_bottom
+ lda Vars::zp_is_bottom
beq @scroll_right
;; We are scrolling the bottom section, which scrolls in the same direction
;; as the top one but a bit slower at that. Load the next scroll value on
- ;; the `a` register and `Vars::bottom_scroll`.
- lda Vars::bottom_scroll
+ ;; the `a` register and `Vars::zp_bottom_scroll`.
+ lda Vars::zp_bottom_scroll
clc
adc #1
- sta Vars::bottom_scroll
+ sta Vars::zp_bottom_scroll
ldy #0
- sty Vars::is_bottom
+ sty Vars::zp_is_bottom
jmp @do_scroll
@scroll_right:
;; We are scrolling the center, which works by going on the opposite
;; direction as the top and bottom sections. Load the next scroll value on
- ;; the `a` register and `Vars::center_scroll`.
- lda Vars::center_scroll
+ ;; the `a` register and `Vars::zp_center_scroll`.
+ lda Vars::zp_center_scroll
sec
adc #$FD
- sta Vars::center_scroll
+ sta Vars::zp_center_scroll
ldy #1
- sty Vars::is_bottom
+ sty Vars::zp_is_bottom
;; We are at the center, but there is still the bottom section to be
;; scrolled differently. Hence, set a new scanline IRQ 80 lines ahead of
@@ -497,6 +499,7 @@ irq:
pla
rti
+.endproc
;;; NOTE: pretty much the same as `fx/blink.s`.
diff --git a/scroll/toggle.s b/scroll/toggle.s
new file mode 100644
index 0000000..fe99cca
--- /dev/null
+++ b/scroll/toggle.s
@@ -0,0 +1 @@
+;; TODO: simply toggle between two namespaces with smooth scrolling.
diff --git a/scroll/toggle4.s b/scroll/toggle4.s
new file mode 100644
index 0000000..2be19f5
--- /dev/null
+++ b/scroll/toggle4.s
@@ -0,0 +1 @@
+;; TODO: like toggle.s but you can choose between the "four" nametables.