diff options
| author | Miquel Sabaté Solà <mikisabate@gmail.com> | 2025-03-10 22:55:41 +0100 |
|---|---|---|
| committer | Miquel Sabaté Solà <mikisabate@gmail.com> | 2025-03-10 23:22:22 +0100 |
| commit | ba0cf9e5293f258c58ced1fcc47f558e7419dae4 (patch) | |
| tree | b620422506e5b8a951f7e52756c116bfb1498ea3 /scroll/include/background.s | |
| parent | 1a74e6a1856673072a61abd0861759901bc2d939 (diff) | |
| download | code.nes-ba0cf9e5293f258c58ced1fcc47f558e7419dae4.tar.gz code.nes-ba0cf9e5293f258c58ced1fcc47f558e7419dae4.zip | |
Provide an example on multiscreen scrolling
Signed-off-by: Miquel Sabaté Solà <mikisabate@gmail.com>
Diffstat (limited to 'scroll/include/background.s')
| -rw-r--r-- | scroll/include/background.s | 498 |
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 |
