+  RHDN Forum Archive
|-+  Romhacking
| |-+  ROM Hacking Discussion
| | |-+  writing text to screen using NES
Pages: [1]
Author Topic: writing text to screen using NES  (Read 2 times)
frantik
Guest
« on: January 22, 2008, 07:29:03 pm »

Ok so I'm sure this is pretty straight forward but I'm not sure exactly how to do it

I want to display a short string onto the screen in SMB instead of raising the star flag at the end of the level.  I've looked at code which writes text and it seems like it just writes the tile number to the VRAM buffer and it displays it.. but when I do this it causes half the screen to turn white


text:
   .db 0c 18 1e 1b 1c 0e  24  0c 15 0e 0a 1b ; "course clear"


   LDX #$0c
loop:
   LDA text,x
   STA $3001,x
   DEX
   BNE loop:


I looked at the original code more closely and it looks like I need to load the VRAM offset.. so will something like this work?  or am i still way off? 


   LDX $3000
   LDY #$00
loop:
   LDA text,y
   STA $3001,x
   INX
   INY
   CPY #$0c
   BNE loop:
   

ok well i tried the above code and it caused a different kind of error but im still obviously far from doing it right lol
« Last Edit: January 22, 2008, 07:54:17 pm by frantik »
Disch
Guest
« Reply #1 on: January 22, 2008, 09:19:14 pm »

$3000 and $3001 are not valid CPU addresses (they're mirrors of $2000 and $2001 respectively -- which are PPU regs which control various display properties).


The background is built from "nametables".  A nametable is nothing more than a big block of $400 bytes.  Each byte indicates which 8x8 pixel tile to draw on the screen.  SMB has nametables at ppu$2000 and ppu$2400  (note these are in PPU space -- not CPU space -- so you don't access them with 'STA $2000' -- see below).

You can see how nametables work in FCEUXD -- just open up the nametable viewer (change the "display on scanline" value to 33) and watch how it changes as the screen scrolls.  You can also hover over each tile in the nametable viewer to know which PPU address each tile sits at.

Drawing text simply amounts to drawing different tiles to the nametable.  Exactly where on the nametable depends on which part of the nametable is visible, and where on the screen you want to draw it.



Now note that nametables are in PPU space (aka "VRAM").  So you don't write to it directly.  Instead you have to use two PPU registers:

$2006 <--  sets the "PPU address".  You write to it twice, once for the high byte of the address, and again for the low byte
$2007 <--  PPU I/O port -- writes to this address go to the current address in PPU space


So if, say, you wanted to draw tile $16 (the letter 'M') to $21AE (around the middle of the upper-left nametable) you'd do the following:

Code:
LDA #$21
STA $2006  ; first write sets high byte... PPU address now $21xx
LDA #$AE
STA $2006  ; second write sets low byte... PPU address now $21AE

LDA #$16
STA $2007  ; $16 gets written to PPU space at whatever address the PPU address is set to.
    ;  Since the PPU addr is $21AE, it gets written to ppu$21AE (drawing a new tile to the nametable)

After the $2007 write, the PPU address is auto-incremented to $21AF (so you don't have to keep writing to $2006 between every tile).  Note that it might auto-increment to $21CE instead (+32 instead of +1) if set that way (but it probably won't).  To be sure you'll have to see what value was last written to $2000.  I don't feel like explaining $2000 in detail -- just get a copy of nestech.txt or something.


Also note... this is super important:

You cannot access $2007 during rendering.  Accessing $2006 is legal (games often do it to split the screen) -- but it's probably not a good idea for you here because you'd end up screwing the scroll up.  So this drawing should only be done

1)  During VBlank (shortly after an NMI fires)
or
2)  When the PPU is off (BG and sprite rendering are both disabled -- see register $2001)

You can specifically turn the PPU off and then are free to draw whatever... but that also means that the PPU won't draw tiles to the screen (you'll get a solid black or blue screen for maybe a frame -- or maybe only part of a frame depending on how long you have the PPU off).  So it's probably best to fit this small drawing routine in VBlank.
frantik
Guest
« Reply #2 on: January 22, 2008, 09:49:09 pm »

ok well I was using $3000 cause thats how the rest of the SMB code does it.

I was able to get the normal text display routine to display text such as the warp zone message or game over, etc.  Here is the trace for "game over".  I could just use the game over text since the game over message is never displayed in SMU but i still need to make it centered on the screen and it's offset by the scroll amount so I need to have a custom text display
Code:
  .db $22, $0b, $09, $10, $0a, $16, $0e, $24 ; "GAME OVER"
  .db $18, $1f, $0e, $1b
  .db $ff

Code:
$D365:A9 03     LDA #$03                   A:90 X:00 Y:08 P:NvUbdIzC ; set text message to 3
$D367:20 08 88  JSR $8808                  A:03 X:00 Y:08 P:nvUbdIzC  ; call text display routine


$8808:48        PHA                        A:03 X:00 Y:08 P:nvUbdIzC
$8809:0A        ASL                        A:03 X:00 Y:08 P:nvUbdIzC
$880A:A8        TAY                        A:06 X:00 Y:08 P:nvUbdIzc
$880B:C0 04     CPY #$04                   A:06 X:00 Y:06 P:nvUbdIzc
$880D:90 0C     BCC $881B                  A:06 X:00 Y:06 P:nvUbdIzC
$880F:C0 08     CPY #$08                   A:06 X:00 Y:06 P:nvUbdIzC
$8811:90 02     BCC $8815                  A:06 X:00 Y:06 P:NvUbdIzc
$8815:AD 7A 07  LDA $077A = #$00           A:06 X:00 Y:06 P:NvUbdIzc
$8818:D0 01     BNE $881B                  A:00 X:00 Y:06 P:nvUbdIZc
$881A:C8        INY                        A:00 X:00 Y:06 P:nvUbdIZc
$881B:BE FE 87  LDX $87FE,Y @ $8805 = #$61 A:00 X:00 Y:07 P:nvUbdIzc
$881E:A0 00     LDY #$00                   A:00 X:61 Y:07 P:nvUbdIzc

start of loop:

$8820:BD 52 87  LDA $8752,X @ $87B3 = #$22 A:00 X:61 Y:00 P:nvUbdIZc
$8823:C9 FF     CMP #$FF                   A:22 X:61 Y:00 P:nvUbdIzc
$8825:F0 07     BEQ $882E                  A:22 X:61 Y:00 P:nvUbdIzc
$8827:99 01 03  STA $0301,Y @ $0301 = #$00 A:22 X:61 Y:00 P:nvUbdIzc
$882A:E8        INX                        A:22 X:61 Y:00 P:nvUbdIzc
$882B:C8        INY                        A:22 X:62 Y:00 P:nvUbdIzc
$882C:D0 F2     BNE $8820                  A:22 X:62 Y:01 P:nvUbdIzc

$8820:BD 52 87  LDA $8752,X @ $87B4 = #$0B A:22 X:62 Y:01 P:nvUbdIzc
$8823:C9 FF     CMP #$FF                   A:0B X:62 Y:01 P:nvUbdIzc
$8825:F0 07     BEQ $882E                  A:0B X:62 Y:01 P:nvUbdIzc
$8827:99 01 03  STA $0301,Y @ $0302 = #$7A A:0B X:62 Y:01 P:nvUbdIzc
$882A:E8        INX                        A:0B X:62 Y:01 P:nvUbdIzc
$882B:C8        INY                        A:0B X:63 Y:01 P:nvUbdIzc
$882C:D0 F2     BNE $8820                  A:0B X:63 Y:02 P:nvUbdIzc

... loop removed ...

$8820:BD 52 87  LDA $8752,X @ $87BE = #$1B A:0E X:6C Y:0B P:nvUbdIzc
$8823:C9 FF     CMP #$FF                   A:1B X:6C Y:0B P:nvUbdIzc
$8825:F0 07     BEQ $882E                  A:1B X:6C Y:0B P:nvUbdIzc
$8827:99 01 03  STA $0301,Y @ $030C = #$01 A:1B X:6C Y:0B P:nvUbdIzc
$882A:E8        INX                        A:1B X:6C Y:0B P:nvUbdIzc
$882B:C8        INY                        A:1B X:6D Y:0B P:nvUbdIzc
$882C:D0 F2     BNE $8820                  A:1B X:6D Y:0C P:nvUbdIzc

$8820:BD 52 87  LDA $8752,X @ $87BF = #$FF A:1B X:6D Y:0C P:nvUbdIzc
$8823:C9 FF     CMP #$FF                   A:FF X:6D Y:0C P:NvUbdIzc
$8825:F0 07     BEQ $882E                  A:FF X:6D Y:0C P:nvUbdIZC
$882E:A9 00     LDA #$00                   A:FF X:6D Y:0C P:nvUbdIZC
$8830:99 01 03  STA $0301,Y @ $030D = #$05 A:00 X:6D Y:0C P:nvUbdIZC
$8833:68        PLA                        A:00 X:6D Y:0C P:nvUbdIZC
$8834:AA        TAX                        A:03 X:6D Y:0C P:nvUbdIzC
$8835:C9 04     CMP #$04                   A:03 X:03 Y:0C P:nvUbdIzC
$8837:B0 49     BCS $8882                  A:03 X:03 Y:0C P:NvUbdIzc
$8839:CA        DEX                        A:03 X:03 Y:0C P:NvUbdIzc
$883A:D0 23     BNE $885F                  A:03 X:02 Y:0C P:nvUbdIzc
$885F:AD 7A 07  LDA $077A = #$00           A:03 X:02 Y:0C P:nvUbdIzc
$8862:F0 1D     BEQ $8881                  A:00 X:02 Y:0C P:nvUbdIZc
$8881:60        RTS 

it seems like if you just write the correct values to $3001 it should work but i dunno why it doesnt when i make a custom routine...
« Last Edit: January 22, 2008, 09:55:09 pm by frantik »
Disch
Guest
« Reply #3 on: January 22, 2008, 10:02:53 pm »

Quote from: frantik on January 22, 2008, 09:49:09 pm
ok well I was using $3000 cause thats how the rest of the SMB code does it.

I highly doubt that.  As I said -- $3000 is a mirror of the register at $2000.  There's no way SMB is ever writing to it.

Upon looking at the code you pasted -- I don't see $3000 or $3001 anywhere in it.  I do see $0301 (which is RAM).

The game probably writes the string to RAM at $0301 first and then later copies that string to the nametables in NMI or something.
frantik
Guest
« Reply #4 on: January 22, 2008, 10:07:51 pm »

lol!!!! i can't believe i was thinking it was $3001 when it was $0301

ok well i got some text to display on the screen now duuuuuuur hopefully it shouldn't be too tough to make it display in the correct location regardless of the scroll offset


edit:

well it's not as easy as i thought.. for some reason the text won't display if it's on the left side of the level page change.  i would like to just write to memory directly but i dont think it's during vblank when it's happening

edit2:

well i tried writing directly to 2006/2007 and it caused the entire background to be messed up Sad  i think i may have to give up on this idea since i really have no idea what im doing.  plus for some reason on some levels the text doesnt display at all lol so i'm at a loss
« Last Edit: January 23, 2008, 03:21:34 am by frantik »
Disch
Guest
« Reply #5 on: January 23, 2008, 12:12:17 pm »

The screen messing up is a good indication that you're trying to draw during rendering.  You have to either be in VBlank or explicitly shut the PPU off (or both).

There are two ways you can go about this (well there are more, I'm sure, but these are two I can think of).

1)  Figure out how SMB updates the screen and simply add a new "job" to its list.  After quickly skimming the disassembly, I see there's a "UpdateScreen" routine that gets called every VBlank.  If you can figure out how to make that work to draw your text, that might be the way to go.  Though I didn't look at this routine in great detail.

2)  Make a custom routine and jump to it every VBlank (after an NMI).  An NMI fires right at the start of VBlank, so that means that when the NMI vector gets jumped to, that is when it is safe to do your drawing.  If you have a free byte of RAM you can use it as a flag to signal when you want to draw the text to the screen.

To signal the "stage clear" text to draw:
Code:
INC your_free_byte_of_RAM

Then in NMI, cut off the original game's NMI code and do this before it:
Code:
  LDA your_free_byte_of_RAM
  BEQ normal_NMI_routine   ; skip over your text drawing if it's not signaled to draw

  ; ... draw text here (with $2006/$2007 writes)

  DEC your_free_byte_of_RAM  ; clear the signal

normal_NMI_routine:
  ; ... game's normal NMI code here

As long as you keep the text drawing relatively quick -- this should work fine.
Nightcrawler
Guest
« Reply #6 on: January 23, 2008, 12:18:14 pm »

Thank goodness for the invention of buffering techniques and the back buffer, huh? Having to do everything on these old consoles within the confines of blanking periods is limiting and annoying. I've done a few hacks before where there wasn't enough vblank time after NMI  and other game code was finished to do the task I wanted to do in a frame. On those, I'd either have to modify the NMI routine, somehow improve my process to transfer less data and be quicker, or break it up across several frames.
frantik
Guest
« Reply #7 on: January 23, 2008, 01:19:25 pm »

Quote from: Disch on January 23, 2008, 12:12:17 pm
1)  Figure out how SMB updates the screen and simply add a new "job" to its list.  After quickly skimming the disassembly, I see there's a "UpdateScreen" routine that gets called every VBlank.  If you can figure out how to make that work to draw your text, that might be the way to go.  Though I didn't look at this routine in great detail..

I suspect writing to $0301 is how you access this routine.  I was thinking bout it and I bet the text is written to a specific nametable, but since the scrolling causes various nametables to be displayed, sometimes the text is no displayed.  this is already the case with warp zones and castles.. if your don't put the warp zone pipes/mushroom retainer in the right place the text doesn't display.

what this means is that I'd have to redesign the levels so the text will display which doesn't sound particularly appealing
Disch
Guest
« Reply #8 on: January 23, 2008, 01:37:37 pm »

Quote from: Nightcrawler on January 23, 2008, 12:18:14 pm
Thank goodness for the invention of buffering techniques and the back buffer, huh?

For reals.

Quote from: frantik on January 23, 2008, 01:19:25 pm
I suspect writing to $0301 is how you access this routine.

I suppose that is likely... but I'm not sure.  The routine seems to load the target address and other flags from a given pointer.  The thing that's weird about the NMI code is it seems to load that pointer from a fixed table (wtf?).  I can't really make sense of that, unless it's trying to draw the exact same thing every frame -- which makes no sense.

Maybe the fixed table has pointers to RAM, and that $0301 is one of them?  I'm sure you could figure it out if you examined the disassembly.


You could also draw the text with sprites.  Since there aren't many/any sprites on-screen after you beat a stage.  The big problem with that, though, is that the text graphics are on the right-hand pattern table, and sprites use the left-hand pattern table -- so you'd have to switch those (but do that after sprite-0 hit -- otherwise the game will likely deadlock).  Plus you can't have more than 8 sprites on a scanline, so you have to put "stage" and "clear" on seperate lines
frantik
Guest
« Reply #9 on: January 23, 2008, 03:55:58 pm »

eh, i think i'm going to give up on this idea.. it's more of an extra anyways that I was hoping to implement easily but like i said it will require the redesign of levels to do it the easy way.  I'd like to do it the 'hard way' but to be honest im not super good with all of the hardware aspects of the NES so a lot of what you've suggested im still not sure how to do lol (like switching the graphics tables or disabling the ppu, etc)
Pages: [1]  


Powered by SMF 1.1.4 | SMF © 2006-2007, Simple Machines LLC