Screen split and vertical scrolling in the bottom half

Discuss technical or other issues relating to programming the Nintendo Entertainment System, Famicom, or compatible systems. See the NESdev wiki for more information.

Moderator: Moderators

Shiru
Posts: 1161
Joined: Sat Jan 23, 2010 11:41 pm

Screen split and vertical scrolling in the bottom half

Post by Shiru »

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?
User avatar
Bregalad
Posts: 8029
Joined: Fri Nov 12, 2004 2:49 pm
Location: Divonne-les-bains, France

Post by Bregalad »

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).
Useless, lumbering half-wits don't scare us.
User avatar
tokumaru
Posts: 12385
Joined: Sat Feb 12, 2005 9:43 pm
Location: Rio de Janeiro - Brazil

Post by tokumaru »

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.
Shiru
Posts: 1161
Joined: Sat Jan 23, 2010 11:41 pm

Post by Shiru »

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?
Drag
Posts: 1559
Joined: Mon Sep 27, 2004 2:57 pm
Contact:

Post by Drag »

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: Select all

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: Select all

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.
Last edited by Drag on Mon May 23, 2011 4:57 pm, edited 1 time in total.
User avatar
tokumaru
Posts: 12385
Joined: Sat Feb 12, 2005 9:43 pm
Location: Rio de Janeiro - Brazil

Post by tokumaru »

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.
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.
Shiru
Posts: 1161
Joined: Sat Jan 23, 2010 11:41 pm

Post by Shiru »

Drag wrote:

Code: Select all

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.
User avatar
Bregalad
Posts: 8029
Joined: Fri Nov 12, 2004 2:49 pm
Location: Divonne-les-bains, France

Post by Bregalad »

Well to each their own. I MUCH prefer the "adress" approach, it makes a lot more sense to me.
Useless, lumbering half-wits don't scare us.
User avatar
qbradq
Posts: 972
Joined: Wed Oct 15, 2008 11:50 am

Post by qbradq »

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
User avatar
tokumaru
Posts: 12385
Joined: Sat Feb 12, 2005 9:43 pm
Location: Rio de Janeiro - Brazil

Post by tokumaru »

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.
User avatar
Bregalad
Posts: 8029
Joined: Fri Nov 12, 2004 2:49 pm
Location: Divonne-les-bains, France

Post by Bregalad »

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.
Useless, lumbering half-wits don't scare us.
User avatar
Dwedit
Posts: 4833
Joined: Fri Nov 19, 2004 7:35 pm
Contact:

Post by Dwedit »

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: Select all

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.
Here come the fortune cookies! Here come the fortune cookies! They're wearing paper hats!
User avatar
Memblers
Site Admin
Posts: 3995
Joined: Mon Sep 20, 2004 6:04 am
Location: Indianapolis
Contact:

Post by Memblers »

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: Select all

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
Shiru
Posts: 1161
Joined: Sat Jan 23, 2010 11:41 pm

Post by Shiru »

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: Select all

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.
User avatar
tokumaru
Posts: 12385
Joined: Sat Feb 12, 2005 9:43 pm
Location: Rio de Janeiro - Brazil

Post by tokumaru »

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.

Code: Select all

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?
Post Reply