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/buffer.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/buffer.s')
| -rw-r--r-- | scroll/include/buffer.s | 258 |
1 files changed, 258 insertions, 0 deletions
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 |
