Page 1 of 1

Urgh.. Nametables..

Posted: Sat Jan 15, 2011 1:21 am
by Rednecker20
Well, I'm really really frustrated right now because this one silly thing isn't working. I think I've lost focus on the 'order of operations' so to speak, of the NES program flow. Simply put, right now I'm trying to display a text. "Press A" and when a is pressed I want that to change to "LED ON". I've heard that you can't write nametables outside of a blank. Well, if that is so, then how do games that show RAM variables such as score, etc. do it? I'd really like someone to tell me how that works, and an example if possible. I'd really like to turn this into saying "Press A" at the top and then a text at the middle telling weather or not if a flag is set.

Posted: Sat Jan 15, 2011 4:00 am
by koitsu
Games do it by writing to the nametable area of PPU memory during VBlank. :-) Remember, VBlank happens 60 times a second (or 50 for PAL systems).

You *can* write to NTs during VBlank, but it's tricky in the sense that adjusting some of the $2005/2006 registers causes the PPU to do things that you might not expect it to. Since you're starting out, stick to writing to the PPU during VBlank and you should be fine.

Posted: Sat Jan 15, 2011 6:22 am
by MetalSlime
One common solution is to create something called a drawing buffer. Outside of NMI you run your game logic and if you need to make any screen updates, you add them to your drawing buffer. Then when NMI comes around, signaling vblank, you unload the contents of your drawing buffer to the PPU.

The drawing buffer itself is just a block of RAM somewhere. The "screen updates" you add to the drawing buffer are strings that tell you:

1) how many bytes to copy
2) where to copy them (target PPU address)
3) which bytes to copy

For example, here's what a typical drawing buffer string might look like:

06 21 20 05 04 21 2F 18 0A

The first byte, 06, is "how many bytes to copy"
The next two bytes, 21 and 20, are the target PPU address ($2120)
The next six bytes are the "bytes to copy"

So, for your example, outside of NMI you'd read your controller input, and then you'd check it for an A press. If A is pressed, you add a string to your drawing buffer. Like above, this string will consist of:

1) how many bytes to copy (06 in this case "LED ON" is 6 characters)
2) target PPU address ($2108 in your code)
3) which bytes to copy ($15,$0E,$0D,$2C,$18,$17)

Then in your NMI routine you will have a section that will read the contents of your drawing buffer and translate it into PPU writes.

For a more detailed explanation of this kind of technique, check out Disch's NMI document.

Posted: Sat Jan 15, 2011 11:29 am
by tokumaru
Your program looks OK so far, except that you have 2 identical routines in there, for printing the different texts. To avoid that kind of redundancy, you want to use the indirect indexed addressing for reading data. Your "print text" routine would look something like this:

Code: Select all

PrintText:

	;multiply the string index by 2
	asl

	;copy the address of the string to RAM
	tay
	lda StringPointers+0, y
	sta StringPointer+0
	lda StringPointers+1, y
	sta StringPointer+1

	;reset PPU latch
	lda $2002

	;set the target address
	lda #$21
	sta $2006
	lda #$08
	sta $2006

	;prepare to read the first character
	ldy #$00

TextLoop:

	;load the character
	lda (TextPointer), y

	;skip to the end if this is the end of the string
	beq TextEnd

	;print the character
	sta $2007

	;move on to the next character
	iny
	bne TextLoop

TextEnd:

	;return
	rts
The main difference from your current routine is that it doesn't use a hardcoded address to read the string, it reads the address from a table of string pointers. Since each address is a word (2 bytes), you can have up to 128 of them in this table, which you define like this:

Code: Select all

Text0:
	.db "This is the first text", $00
Text1:
	.db "This is the second text", $00
Text2:
	.db "This is the third text", $00
Text3:
	.db "This is the fourth text", $00

TextPointers:
	.dw Text0
	.dw Text1
	.dw Text2
	.dw Text3
I chose to mark the end of each string with a 0 rather than specify their length, because it's easier that way. See how it works? Now you can print up to 128 strings without having to make a new routine for each one. All you have to do is put the index of the string you want to show (0 to 127) in the accumulator and call "PrintText". If you wanted, you could also make the target address dynamic, so that you could print text to different parts of the screen (you could for example use the X and Y registers to specify a coordinate on the screen where to put the text), instead of using a hardcoded location.

Anyway, this was just a friendly tip, and doesn't really have anything to do with what you actually asked! :lol: So back to your problem: if you want to change a message depending on a certain condition, which means that you have to monitor these condition every frame and request that a message be displayed in case they change.

From looking at your code, I see that you have an empty main loop and all the program logic runs in the NMI handler. This is fine, as long as you perform all the PPU updates first (because those MUST be performed during VBlank) and then run the program logic. So, your program logic will detect the need for a new message and will only request that it be written, it will not write it at that time (this is the drawing buffer concept MetalSlime talked about). Then, in the part of your NMI handler that deals with updating the PPU (the part that must run during VBlank), you'll check for any update requests and perform them as necessary. This will guarantee that all name table updates happen during Vblank.

EDIT: Maybe I should give you a more complete answer.

The simplest solution in your case would be to call the "PrintText" routine during Vblank when necessary. When your program detects that the state of the A button changed, it would set some variables (in your case, a single variable will do the trick) requesting that a new string be written. Something like this:

Code: Select all

	;request that the second string be printed
	lda #$01
	sta StringToPrint
Remember that we can have up to 128 strings with this solution? Because of that, only numbers from 0 to 127 are valid string indexes, anything beyond that is invalid. The cool thing about that is that the numbers from 128 to 255 have the highest bit set, which means that the CPU can detect them as negative numbers. This is perfect, because you can use that to indicate whether there is a string to print or not. So, at the start of your NMI routine you'd have:

Code: Select all

NMI:

	;load the index of the string to print
	lda StringToPrint

	;skip printing if the index is invalid
	bmi SkipPrinting

	;print the string
	jsr PrintText

	;prevent the string from being printed again
	lda #$ff
	sta StringToPrint

SkipPrinting:

Posted: Sat Jan 15, 2011 3:49 pm
by koitsu
I wanted to expand on something tokumaru touched on:
tokumaru wrote:... From looking at your code, I see that you have an empty main loop and all the program logic runs in the NMI handler. This is fine, as long as you perform all the PPU updates first (because those MUST be performed during VBlank) and then run the program logic. ...
It's important to understand that although this is completely fine to do (especially when starting out), VBlank itself lasts approximately ~2250 CPU cycles (that nesdev Wiki link tokumaru provided mentions this). Be sure to take loops and other things that happen within your code (within NMI) into consideration when developing something this way.

Basically the idea is: if there's "stuff" you can safely do outside of VBlank, do it outside of VBlank.

Posted: Sat Jan 15, 2011 4:12 pm
by tokumaru
koitsu is right, you should keep in mind that VBlank time is relatively short, and shouldn't be wasted with things that could be done at other times. Don't ever do things like move game characters around (some newbie tutorials do this, so watch out) or check for collisions during VBlank, because that would be a waste of precious time.

If you decide to continue with your current program structure (i.e. everything inside the NMI), always make sure to put PPU updates first, game logic later. The solution I recommended to you (calling the PrintText routine) is fine in this case, because 1: the strings aren't very long; 2: there is not much overhead to this routine (the time it takes to set up pointers and such isn't so significant); and 3: you are not doing any other updates besides the sprite DMA, so there's still plenty of free VBlank time.

However, if you needed to update much more of the name/attribute tables you'd have to go for a more drastic solution, that would involve setting up all the pointers and necessary variables beforehand, so that when VBlank starts you don't have to waste time with preparations and can start copying data right away.

Posted: Sat Jan 15, 2011 4:51 pm
by Rednecker20
Alright, thank you guys so much. The drawing buffer routine is great and worked wonders. I have one more trivial question, though. After the buffer is written to it draws perfectly, but once it's written to again, if the 2nd transfer was shorter than the first, remainders of the first will still be there even after clearing the entire buffer. I'm guessing I'm missing some update or something? I had an idea, just writing the whole (256?) tiles with a blank tile. However, this seems a bit inefficient to me.

Posted: Sat Jan 15, 2011 7:38 pm
by tokumaru
You could either pad your strings with extra spaces to make sure they overwrite the previous one, or clear the whole line before writing new strings. 256 bytes is too much to write during VBlank, there's no time for that.

Posted: Sat Jan 15, 2011 8:41 pm
by tepples
If you do one transfer, write a shorter string to the buffer, change the length byte, and do another transfer, then the garbage after the shorter string just won't get copied.

Posted: Sat Jan 15, 2011 8:48 pm
by tokumaru
I thought he was talking about garbage left in the name table, not the buffer. If you are going to replace a string with a smaller one, it would be a good idea to erase the whole line first.

Posted: Sun Jan 16, 2011 6:24 am
by MetalSlime
Yes, I'd pad the shorter message with spaces so that they overwrite the previous message completely.