aboutsummaryrefslogtreecommitdiff
path: root/scroll/include/background.s
diff options
context:
space:
mode:
Diffstat (limited to 'scroll/include/background.s')
-rw-r--r--scroll/include/background.s498
1 files changed, 498 insertions, 0 deletions
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