This page is a mirror of Tepples' nesdev forum mirror (URL TBD).
Last updated on Oct-18-2019 Download

Screen split and vertical scrolling in the bottom half

Screen split and vertical scrolling in the bottom half
by on (#78583)
I wonder, how it is possible to have vertical scrolling in bottom part of a split screen. It is said in the docs that writes in Y scroll register only applied in the beginning of a frame and can't be changed mid-frame, and emulators acts accordingly, so you can't just change Y scroll after split.

Together with PPU_ADDR registers it is easy to get split screen with vertical scroll in the upper part and static bottom part. It does not work vice versa because of the reason mentioned above, and PPU_ADDR only allow to set vertical position at a character row, not a pixel row.

However, there are examples that it is possible to have static upper part with vertical scrolling in the bottom part, like in Ninja Gaiden 3 (vertical part of first level). Is some trick was used there to achieve the result?

by on (#78585)
Yes there is a trick : The trick is to write the VRAM adress of the tile you want to "scroll" to to $2006. (This works for both horizontal and vertical scrolling by the way).
The con is that you have a granularly of a tile in both axis, so you have to compensate for that by using $2005 (horizontally) and by using timed code (vertically).

by on (#78586)
You can do it with the good old $2005/$2006 combo (example code). With this you can change the scroll to any location you want anywhere in the screen, just make sure that the final writes happen during HBlank for a clean split.

What Bregalad described is the solution that was used before the $2005/$2006 behavior exploited above was known, and it has some limitations.

by on (#78587)
What Bregalad said is actually a third of my first post in another words, except for 'timed code', which is new info, but not really explains anything.

Thanks for the example, I'll check it.

Edit: the example does not work for me. Maybe I'm doing something wrong or don't understand something. Which idea is behind this code, how and why it should work? I.e. how vertical pixel-perfect position achieved?

by on (#78593)
The "address" you write to $2006 isn't actually an address, but a series of counters that determine various things, such as the current tile, the current nametable, the current row of the current tile, etc.

When you use $2007, the various counters you set with $2006 are lined up to behave like an actual address, for accessing the PPU's address space.

When the PPU is rendering however, the various bits of the value you write to $2006 will take on a different meaning, such that $2006 no longer behaves like an address. Trust me, I was confused by this for a long time. :P

Here's what the 2006-2005-2005-2006 trick does:

Code:
2006/1 --vv NNVV
2005/2 VVVV Vvvv
2005/1 HHHH Hhhh
2006/2 VVVH HHHH

The first 2006 write can only set the fine vertical scrolling to 0-3, because the two most significant bits of the value you write are ignored (replaced by 0). The only thing you need to worry about is the NN bits (which select the nametable you want), because all of the other bits will be overwritten by the next write you'll make in the next step.

The "second" write to 2005 will set the fine vertical scroll value (v) correctly, overwriting whatever value you used in the first 2006 write. This write also sets the coarse vertical scrolling (V), again overwriting whatever you used for the two V bits in the previous step. Be aware, the three lower V bits will be overwritten by the last step, so what you set them to here doesn't matter.

The "first" write to 2005 will set the fine horizontal scroll value (h). The coarse horizontal scroll (H) will be completely overwritten by the next step, so don't worry about what value you use here.

The final write, the second write to 2006, will set the coarse horizontal scrolling (H), and will overwrite the lower 3 bits of the coarse vertical scrolling (V). After this write, all of the values you've written will take effect.

So, removing all of the extra bits that get overwritten, this is what you write to 2006/2005/2005/2006:
Code:
2006/1 ---- NN-- (nametable select)
2005/2 VV-- -vvv (upper two bits of coarse V scroll, all bits of fine V scroll)
2005/1 ---- -hhh (fine horizontal scrolling) (takes effect immediately)
2006/2 VVVH HHHH (lower three bits of coarse V scroll, all bits of coarse H scroll)


Correct me if I'm wrong, but only the last two writes need to be in h-blank. The first two writes won't have any effect on the screen.

by on (#78594)
Shiru wrote:
the example does not work for me. Maybe I'm doing something wrong or don't understand something.

It has worked fine for me and for tepples, maybe you are doing something wrong. Keep in mind that ScrollX and ScrollY are 16-bit variables, although only 9 bits are actually used. Also note that the low byte of the Y scroll should always be between 0 and 239, otherwise you'll be rendering attribute tables as if they were name tables.

Quote:
Which idea is behind this code, how and why it should work? I.e. how vertical pixel-perfect position achieved?

It's based on the information contained in the famous "The Skinny on NES Scrolling" document. The fact is that it's possible to change the scroll with 2 $2006 writes, but since this register was not meant for setting the scroll there's 1 bit (which is part of the Y scroll) that gets cleared when $2006 is written to. Loopy's document describes which bits get set and cleared when different PPU registers are written to, so through a combination of $2005 and $2006 writes it's possible to set all the scroll bits. This requires some bit shifting to make sure each bit is where it's supposed to be.

by on (#78609)
Drag wrote:
Code:
2006/1 ---- NN-- (nametable select)
2005/2 VV-- -vvv (upper two bits of coarse V scroll, all bits of fine V scroll)
2005/1 ---- -hhh (fine horizontal scrolling) (takes effect immediately)
2006/2 VVVH HHHH (lower three bits of coarse V scroll, all bits of coarse H scroll)


Thanks, this explained everything and I got it to work. Would be nice to have this in the wiki.

by on (#78610)
Well to each their own. I MUCH prefer the "adress" approach, it makes a lot more sense to me.

by on (#78620)
It just doesn't allow you to do what the OP needs. But yea, the address approach is much less brain bleeding.

And this info is already on the wiki: PPU scrolling

by on (#78628)
Bregalad wrote:
Well to each their own. I MUCH prefer the "adress" approach, it makes a lot more sense to me.

The problem is that you can't always do a clean split with it... Even if you use timed code to set the vertical scroll in one of 8 scanlines you'll get scrolling artifacts in the split area.

by on (#78642)
Quote:
The problem is that you can't always do a clean split with it... Even if you use timed code to set the vertical scroll in one of 8 scanlines you'll get scrolling artifacts in the split area.

I think you can always do a clean split with it. If there is glitches then it has something to do with the fine-tuned scanline timing where you do the writes, and not the write themselves.
I think only writes to $2005/1 and $2006/2 takes effect during the frame. That way, if you only write to $2006 you only have one write to fit in HBlank instead of two, which makes the thing easier (hmm I guess).

If it's the 8-pixel granularity you are talking about then this is not a problem. In many cases it won't be a problem at all because you just don't need a lower granuarly. For example in Ninja Gainden 3 vertical rooms, there is a black unused bar on the bottom of the status bar, and this is used to hide this.
Also, I think you can specify fine scroll values 0 to 3 by using bits 12 and 13 of the adress (it's a mystery why), so you get some kind of fine scrolling, only values 4 to 7 aren't availble.

Even if you HAD to acess values 4 to 7, I think you could get away with $2005, $2005, $2006, $2006 writes. The first two would work as usual (but the second $2005 write would get ignored), then the last two would just be there so that the second $2005 writes actually takes effect.
Of course the $2006 adress has to match with the $2005 scroll value to avoid glitches.

by on (#78645)
The first 2006 write will change the horizontal nametable for the next scanline, but other than that, only the last 2 writes have any visible effect.
So you can complete the last two writes very quickly by using different registers for the Store instruction.

You can do something like this:
Code:
lda value1 ;9 pixels time
sta $2006  ;12 pixels time
lda value2 ;9 pixels time
sta $2005 ;12 pixels time
lda value3 ;9 pixels time
ldx value4 ;9 pixels time
sta $2005 ;12 pixels time
stx $2006 ;12 pixels time

So the last two writes have no problem fitting inside hblank time.

If you use horizontal/single screen mirroring, changing the horizontal nametable also does nothing at all.

by on (#78650)
For other uses of the trick, such as scrolling every scanline of the display individually, it's helpful to have tables. These 2 have always worked for me, so far:
http://www.parodius.com/~memblers/nes/vram_hi.bin
http://www.parodius.com/~memblers/nes/vram_lo.bin
Those tables in ROM, of course you would want aligned to 256-byte boundaries so it doesn't cross pages when reading.

Feel free to use it, and this code if it helps any. The delay at the beginning would need to be rewritten to adapt it to another program. This is JSR'd to from NMI, and the code preceding it must be branch-less (or predictable). This is NTSC timed, loops once per scanline for 162 lines. "scroll_table" is a table in RAM that lists the vertical scroll position for every scanline. So if the table in RAM is just a backwards count for example, you would see an upside-down background.

Code:
scroll_timing_code:

                  ldx #3
:
                  ldy #$A3
:
                  dey
                  bne :-
                  dex
                  bne :--

                  nop
                  nop
                  nop

                  ldy #0
   scanline_loop:
                  lda scroll_table,y            ; 4    4
                  tax                           ; 2    6
                  lda vram_addr_hi,x            ; 4    10
                  sta $2006                     ; 4    14
                  stx $2005                     ; 4    18
                  lda #0                        ; 2    20
                  sta $2005                     ; 4    24
                  lda vram_addr_lo,x            ; 4    28
                  sta $2006                     ; 4    32
                                                ;
                  lda irrational_counter        ; 3    35
                  clc                           ; 2    37
                  adc #$55                      ; 2    39
                  sta irrational_counter        ; 3    42
                  bcc @nowhere                  ; 2/3  44.6
   @nowhere:                                    ;
                                                ;
                  ldx #11                       ; x*5 + 1
   :                                            ;
                  dex                           ;
                  bne :-                        ;

                  nop
                  nop
                  nop
                                                ;
                  iny                           ; 2    46.6
                  cpy #162                      ; 2    48.6
                  bne scanline_loop             ; 3    51.6
                                                ;
                                                ; 2    53.6
                                                ; 59
                  rts

by on (#78886)
I got a major problem with this trick on real HW: it just does not work properly. I haven't seen how it looks by myself, but basically it jitters for two tiles horizontally when scroll offset is greater than zero. Usual scroll works just fine (but no vertical scroll in this case, of course).

Maybe I did something wrong? It works in all the emulators I've tried:

Code:
lda <GAME_CAM_X+1
   and #1
   asl a
   asl a
   sta PPU_ADDR   ;---- NN--
   lda <GAME_CAM_Y
   sta PPU_SCROLL   ;VV-- -vvv
   lda <GAME_CAM_X
   and #7
   sta PPU_SCROLL   ;---- -hhh
   lda <GAME_CAM_X
   lsr a
   lsr a
   lsr a
   ora #$80
   sta PPU_ADDR   ;VVVH HHHH
   rts


GAME_CAM_X is a word 0..256 (not greater), GAME_CAM_Y is 0..7.

by on (#78894)
Shiru wrote:
I got a major problem with this trick on real HW

This has to be a problem with your implementation, because my ROM works perfectly fine in all of my consoles, and lots of other people have used this trick with success.

Quote:
Code:
lda <GAME_CAM_X+1
   and #1
   asl a
   asl a
   sta PPU_ADDR   ;---- NN--
   lda <GAME_CAM_Y
   sta PPU_SCROLL   ;VV-- -vvv
   lda <GAME_CAM_X
   and #7
   sta PPU_SCROLL   ;---- -hhh
   lda <GAME_CAM_X
   lsr a
   lsr a
   lsr a
   ora #$80
   sta PPU_ADDR   ;VVVH HHHH
   rts

Your second PPU_ADDR write looks pretty incomplete... Why are you writing %100HHHHH to it? And why is GAME_CAM_Y only 0..7? I really can't see what you're trying to accomplish here... with these limitations you obviously can't be aiming for "free" scrolling, so I don't really know what to say. I also don't see any reason for the "and #7"... Is there any reason for you to not write all 8 bits?

by on (#78897)
It is a free scrolling limited to 8 pixels vertically. ora #$80 to move start of scrolling area to certain row, because it has stat bar above. And #7 there is because in Drag's table other bits marked as unused, why would I write garbage there?

I'm glad that it works for other people on consoles etc, but it does not work for me on HW which I don't have, and it works in all the emulators, so I can't see the problem to fix it. So I need help.

tokumaru wrote:
just make sure that the final writes happen during HBlank for a clean split.

What exactly 'clean' means? What happens if these writes aren't in HBlank time, is scroll still works, but with some garbage on the split line, or it does not work properly?

by on (#78899)
Correct me if I'm wrong.
Quote:
What exactly 'clean' means? What happens if these writes aren't in HBlank time, is scroll still works, but with some garbage on the split line, or it does not work properly?

PPU(palletes for example) can be updated only while Vbalnk(NMI),When PPU is turned off and during Hblank(When no pixel is rendered).
Scrolling will work propely, but with garbage.
This topicis a example of wirtes to pallete in non Hblank time when Screen split is done.[/url]

by on (#78904)
Shiru wrote:
What exactly 'clean' means? What happens if these writes aren't in HBlank time, is scroll still works, but with some garbage on the split line, or it does not work properly?

Just garbage on the split line, with a possible jitter of 1 scanline. Could you share more of the code you're using? I want to help you get this right, but I really don't know enough. The limitations on the scroll values you mentioned before really threw me off.

What are you using for timing? Are you using timed code, IRQs, sprite 0 hit, what? You have to make sure that the split code runs at the same time every frame, so it's important that your timing method is flawless.

by on (#78905)
I solved the problem for this project by omiting vertical scroll because lack of time. However, I still would like to figure out what was wrong.

Vertical scroll was needed for screen shake effect, thus vertical range is very limited (it was actually just 0 and 1). Screen split done with sprite 0.

There is no additional related code, just this. It was replaced with this code (works on HW, no vertical scroll anymore):

Code:
   lda <GAME_CAM_X+1
   and #1
   ora <GAME_CHR_ANIM ;%100nn000
   sta PPU_CTRL
   lda <GAME_CAM_X
   sta PPU_SCROLL
   lda #0
   sta PPU_SCROLL


I'll publish full source later.