Diagonal scrolling help

This is an archive of a topic from NESdev BBS, taken in mid-October 2019 before a server upgrade.
View original topic
Diagonal scrolling help
by on (#237768)
I've been developing for SNES and I want to have levels that are two screens tall, except to save video RAM for tiles instead, I'm using 64x32 tilemaps, but I'm running into problems with some occasional wrong tiles on the top half of the level, corresponding to the right tiles for the bottom half in the same position. I think aside from that it works.

Image

Since this is my first time doing diagonal scrolling it's hard to tell if my implementation is wrong, or if the logic I'm trying to implement in the first place is wrong (so any implementation of that logic would be flawed) so I figured getting more eyes on my code would help.

I have a ColumnUpdateBuffer that gets DMA'd to ColumnUpdateAddress vertically, a full 32 tiles tall. I also have a RowUpdateBuffer that similarly gets DMA'd to RowUpdateAddress horizontally and it's always 64 tiles wide for simplicity, and I only write to the portion of it that will be visible. My level buffer is in LevelBuf and it is 256 wide * 32 tall * 2 bytes per metatile, arranged in a series of columns from left to right.

When the scrolling has changed: (simplified)

If the X scroll value passes a tile boundary:
  • If scrolling left, select a column 2 tiles left from the new scroll position
  • If scrolling right, select a column 34 tiles right from the new scroll position
  • (Selected column number will be called SelectedColumn)
  • Set ColumnUpdateAddress to tilemap base address + SelectedColumn AND 31, use second screen if needed
  • Prepare a pointer to start reading from level data (Level column is SelectedColumn / 2)
  • -----
  • Y = Y scroll position counted in metatiles
  • X = Y, restricted to fit ColumnUpdateBuffer
  • ColumnUpdateBuffer[X++] = Tile numbers[Level pointer[Y++]]
  • Repeat the previous step 15 more times, wrapping X around in the buffer.

If the Y scroll value passes a tile boundary:
  • If scrolling up, select a row 1 tiles up from the new scroll position
  • If scrolling down, select a row 29 tiles down from the new scroll position
  • (Selected row number will be called SelectedRow)
  • Set RowUpdateAddress to tilemap base address + SelectedRow * 32
  • Prepare a pointer to start reading from level data (Level column is X scroll position in metatiles, minus 1)
  • -----
  • Y = SelectedRow / 2
  • X = X scroll position in metatiles * 2, minus 1, constrained to the buffer size
  • RowUpdateBuffer[X++] = Tile numbers[Level pointer[Y]]
  • Move Level pointer ahead one column
  • Repeat previous two steps 20 times (probably more than needed. I was trying to make it slightly wider than the screen, hence starting a bit to the left of it)

Or probably a lot more clearly, the code itself:
scrolling.s
renderlevel.s
Re: Diagonal scrolling help
by on (#237769)
"Y = Y scroll position counted in metatiles"
This needs to be based on the previous frame's Y scroll position, in case you cross a tile boundary in both directions at once.
Re: Diagonal scrolling help
by on (#237770)
Yeah I thought about problems from both the row and column updating at once. Changing that doesn't seem to fix it, and it's hard to tell exactly what's failing. I probably need to start over on the scrolling code but I don't really have correct guidelines to work from.
Re: Diagonal scrolling help
by on (#237771)
Garbage tiles in diagonal scrolling often happen because of mismatched vertical and horizontal tile map updates in the same frame.

If you generate the new tile data for movement in one axis immediately after moving in that axis but before moving in the other axis, consider that that data will be off by one if the movent in the other axis also happens to trigger an update. If the updates are carried out in the same order they were prepared, things are probably gonna be fine, if not, you may end up with old data overwriting new data.

I don't know in which order you're doing everything, so what you need to do is simulate or debug the case when both axis need updating, and see if there are any wholes in your logic that could be causing old data to overwrite new data or leave certain spots with no updates at all.
Re: Diagonal scrolling help
by on (#237772)
I actually move both axes at once, and then after that, check for and handle any updating. You think it's better to move an axis, prepare an update, then move the other axis and prepare that update, then do the two DMAs in the same axis order used here?
Re: Diagonal scrolling help
by on (#237773)
Are you allowing 2 tiles on both sides so you can havr hdma effects?
Re: Diagonal scrolling help
by on (#237774)
psycopathicteen wrote:
Are you allowing 2 tiles on both sides so you can havr hdma effects?

I haven't thought that far ahead. I have 2 because my previous game extended far further than 2 due to handling scrolling updates in 32 pixel wide chunks, and I was just being a little safe even while shortening it.

Looks like simply making X scroll changes happen and be handled before Y scroll changes doesn't help. I think I might actually be messing up the part that fetches from the level (or sets up the pointer) since it's fetching from the bottom rather than just being some other kind of garbage. Something tricky here is I don't know if it's the row updater or the column updater that's flawed.
Re: Diagonal scrolling help
by on (#237777)
NovaSquirrel wrote:
You think it's better to move an axis, prepare an update, then move the other axis and prepare that update, then do the two DMAs in the same axis order used here?

I'm not sure, it's been a while since I coded an 8-way scrolling engine and I don't have any source code with me right now that I can check... but I distinctly remember having garbage tiles near the corners of the screen when scrolling diagonally and that having to do with the exact order in which horizontal and vertical updates were handled in the same frame.
Re: Diagonal scrolling help
by on (#237778)
I truly wish I could help here, but I'm having an extremely difficult time understanding any of what's being discussed. I clearly see "the problem" demonstrated in the screenshot, but I honestly do not understand how you'd end up with said problem unless your tilemap update data was incorrect based on several conditions (i.e. your code). Everything looks fine except for those few tiles; still screenshots don't help, sadly, but I'm not sure an animated GIF would either.

I would suggest stepping through your code in the bsnes-plus or Mesen-S debugger, and see what ends up in PPU RAM right after your DMA completes (since both emulators will let you examine system RAM in addition to PPU RAM). I assume you aren't running out of NMI time, either.

I've done diagonal scrolling before -- you just increment/decrement a 16-bit variable and write it to $210D-2114 as needed -- but this was simply panning backgrounds with a 32x32 layout (i.e. single-screen). I haven't done it with 64x32 or 32x64 or 64x64. But 64x32 may require you -- on vertical scrolls -- to update a row which shouldn't be visible (otherwise the player would see the update) -- most 64x32 games I see use a status bar of some kind to cover this, I believe. That said: if you have something that's two screens tall, why are you using 64x32 rather than 32x64?

As you've learned, the SNES is not like the NES at all -- there really isn't a "you need to do something with the A registers before the B registers" for screen panning. All of that NES-level nonsense is for the most part irrelevant.
Re: Diagonal scrolling help
by on (#237784)
Yeah I get the impression this is something I'll have to figure out for myself and that it's probably a problem with my implementation somewhere.

I used 64x32 instead of 32x64 to avoid needing to black out the leftmost 8 pixels, though if it's good enough for Kirby Super Star then perhaps it's good enough for me, at least to have something 100% working for now.

Longer term for actually fixing this I feel like it'd be a good idea to go and thoroughly test the row/column updating code and see if I can give it fixed values that result in a problem every frame, and then from there both know exactly what kinds of values are breaking and have it in a state where I can step in and debug. Or, if necessary, just try and rewrite all the scrolling code fresh, being careful and testing a lot along the way. It's not necessarily a lot of work wasted.
Re: Diagonal scrolling help
by on (#237787)
Maybe you're not updating the corner tiles.
Re: Diagonal scrolling help
by on (#237795)
psycopathicteen wrote:
Maybe you're not updating the corner tiles.

He did say that he's updating the whole 32 or 64 tiles, so I don't think he's missing any corners. It's true that sometimes people forget that they need to update areas wider/taller than the screen, because of the fine scroll.
Re: Diagonal scrolling help
by on (#237800)
And in the case of the SNES, there is no "fine scroll".
Re: Diagonal scrolling help
by on (#237801)
The screenshot looks as if only half of the 16bit map entry was written okay.
Ie. wrong tile number, but (seemingly) correct palette... or completely unchanged palette if it's same as old color?

For general testing: Maybe it helps if you remove the horizontal or vertical updating (to see which one is buggy).
And perhaps initially fill the whole screen area with known tiles/colors (to see what does or doesn't get updated later on).
Don't forget debuggers with vram viewers - that might also help so see what happens after each DMA.

koitsu wrote:
And in the case of the SNES, there is no "fine scroll".
I think "fine scroll" was referring to scroll amounts less than 8 pixels (the SNES does fortunately support that) (and it can result in needing 33 tiles per line, instead of 32 tiles in unscrolled pictures).
Re: Diagonal scrolling help
by on (#237807)
I looked through the source code and played with the demo and I think there is a missing corner tile. I think lowering the top loading seam would do the trick.
Re: Diagonal scrolling help
by on (#237810)
koitsu wrote:
And in the case of the SNES, there is no "fine scroll".

I used the term generically, basically referring to sub-tile scrolling. The internal implementation of that in the NES or SNES is irrelevant here.
Re: Diagonal scrolling help
by on (#237811)
psycopathicteen wrote:
I think lowering the top loading seam would do the trick.

That actually fixed it, thank you! I was seriously looking into trying to settle for a 32x64 tilemap and sacrificing one of the two windows on clipping the 4 pixels on the left and right like KSS (not the 8 left most like I thought) but I don't have to now.

That explains why it only occurred when scrolling upwards, why my column/row preparing code looked perfectly fine, why the order of the row and column didn't seem to matter, etc.

This is a big relief and now I can move on with the rest of the game. :p