From 87b21d8f73edfbc66ab55db5738ac29579892154 Mon Sep 17 00:00:00 2001 From: Jesse Luehrs Date: Sun, 19 Oct 2014 19:53:54 -0400 Subject: part 4 --- media/sprites.chr | Bin 0 -> 8192 bytes media/sprites.s | 197 ++++++++++++++++++++++++++++++++++++ posts/writing-an-nes-game-part-4.md | 186 ++++++++++++++++++++++++++++++++++ 3 files changed, 383 insertions(+) create mode 100644 media/sprites.chr create mode 100644 media/sprites.s create mode 100644 posts/writing-an-nes-game-part-4.md diff --git a/media/sprites.chr b/media/sprites.chr new file mode 100644 index 0000000..3713c9c Binary files /dev/null and b/media/sprites.chr differ diff --git a/media/sprites.s b/media/sprites.s new file mode 100644 index 0000000..5f0b3d9 --- /dev/null +++ b/media/sprites.s @@ -0,0 +1,197 @@ +.ROMBANKMAP +BANKSTOTAL 2 +BANKSIZE $4000 +BANKS 1 +BANKSIZE $2000 +BANKS 1 +.ENDRO + +.MEMORYMAP +DEFAULTSLOT 0 +SLOTSIZE $4000 +SLOT 0 $C000 +SLOTSIZE $2000 +SLOT 1 $0000 +.ENDME + +.ENUM $00 +sleeping DB +.ENDE + +; just use the actual locations in our copy of SPR-RAM rather than zero-page +; addresses, to avoid having to do multiple copies +.define sprite_x $0203 +.define sprite_y $0200 + + .bank 0 slot 0 + .org $0000 +RESET: + SEI + CLD + LDX #$FF + TXS + INX + STX $2000.w + STX $2001.w + STX $4010.w + LDX #$40 + STX $4017.w + +vblankwait1: + BIT $2002 + BPL vblankwait1 + +clrmem: + LDA #$00 + STA $0000, x + STA $0100, x + STA $0300, x + STA $0400, x + STA $0500, x + STA $0600, x + STA $0700, x + LDA #$FE + STA $0200, x + INX + BNE clrmem + + ; start with the sprite near the middle of the screen + LDA #$80 + STA sprite_x + STA sprite_y + +vblankwait2: + BIT $2002 + BPL vblankwait2 + + ; PPU is initialized here, so we can start writing data into it. this is safe + ; because we have not yet enabled drawing, and so we don't have to restrain + ; ourselves to vblank. + + ; first, we load the palettes into $3F00 and $3F10 +load_palettes: + LDA $2002 ; read here so that the next byte written to $2006 is the high + LDA #$3F ; byte of the address + STA $2006 ; write the high byte of the base address + LDA #$00 + STA $2006 ; write the low byte of the base address + LDX #$00 +load_palettes_loop: + LDA palette.w, x + STA $2007 + INX + CPX #$20 ; 16 byte background palette plus 16 byte sprite palette + BNE load_palettes_loop + + ; then we draw the background (doing that here because it won't be changing) + LDA #$20 + STA $2006 ; high byte of the starting address + LDA #$00 + STA $2006 ; low byte of the starting address + LDA #$01 ; pattern index 1 is our background tile + LDX #$04 ; this loop will load $2000-$23FF, which includes the + LDY #$00 ; attribute table range, but we can just adjust the +load_background_loop: ; palette to take that into account + STA $2007 + INY + BNE load_background_loop + DEX + BNE load_background_loop + + ; then we set the unchanging parts of our sprite (the pattern index and the + ; attributes) + LDA #$00 + STA $0201 ; pattern index 0 is our sprite + STA $0202 ; don't need any attributes + + ; enable the PPU + LDA #%10000000 + STA $2000 + LDA #%00011000 + STA $2001 + +loop: + INC sleeping +wait_for_vblank_end: + LDA sleeping + BNE wait_for_vblank_end + + LDA #$01 + STA $4016 + LDA #$00 + STA $4016 + + ; we don't care about a, b, select, start + LDA $4016 + LDA $4016 + LDA $4016 + LDA $4016 + +up: + LDA $4016 + AND #%00000001 + BEQ down + LDX sprite_y.w + DEX + STX sprite_y.w +down: + LDA $4016 + AND #%00000001 + BEQ left + LDX sprite_y.w + INX + STX sprite_y.w +left: + LDA $4016 + AND #%00000001 + BEQ right + LDX sprite_x.w + DEX + STX sprite_x.w +right: + LDA $4016 + AND #%00000001 + BEQ loop_end + LDX sprite_x.w + INX + STX sprite_x.w + +loop_end: + JMP loop + +NMI: + PHA + TXA + PHA + TYA + PHA + + ; now the only thing we need to do here is issue a DMA call to transfer our + ; sprite data into SPR-RAM + LDA #$00 + STA $2003 ; reset the SPR-RAM write offset + LDA #$02 + STA $4014 ; start the DMA transfer from $0200 + + LDA #$00 + STA sleeping + PLA + TAY + PLA + TAX + PLA + + RTI + +palette: + .db $0F,$30,$0F,$30,$0F,$30,$0F,$30,$0F,$30,$0F,$30,$0F,$30,$0F,$30 + .db $0F,$30,$0F,$30,$0F,$30,$0F,$30,$0F,$30,$0F,$30,$0F,$30,$0F,$30 + + .orga $FFFA + .dw NMI + .dw RESET + .dw 0 + + .bank 1 slot 1 + .org $0000 + .incbin "sprites.chr" diff --git a/posts/writing-an-nes-game-part-4.md b/posts/writing-an-nes-game-part-4.md new file mode 100644 index 0000000..d9a7c5c --- /dev/null +++ b/posts/writing-an-nes-game-part-4.md @@ -0,0 +1,186 @@ +--- +title: writing an nes game, part 4 +date: "2014-10-17 03:30" +tags: [hacker school, nes] +--- + +# Writing an NES game - graphics + +Now that we can handle input, it's time to learn how to draw things. All +drawing on the NES is handled by the PPU. The PPU does sprite-based drawing, +meaning that all drawing is two dimensional, and happens in 8x8 blocks. Colors +are determined by a 16-color palette, of which a given sprite can only use +four colors (transparency being one of the colors). There is a background +layer, containing 32x30 tiles, and a set of up to 64 sprites, which can each +be either in front of or behind the background. + +The PPU has its own memory space and mostly just runs on its own, handling the +redrawing automatically every frame. In order to tell it what to do, we have +to load various bits of data into its memory. As mentioned in an earlier post, +the PPU is only capable of doing one thing at a time, so this drawing must +happen at the beginning of the NMI interrupt, when we are guaranteed to be in +VBlank. + +There are four steps involved in drawing a sprite. First, we need the sprite +itself. The pixel data for sprites is stored in the *pattern table*, and is +typically (although not always) stored in CHR-ROM. It contains 16 bytes per +sprite, where each pixel contains two bits of data, so each sprite can contain +at most four colors (including transparent). Since CHR-ROM banks are 8KB +each, this provides enough room for two sets of 256 tiles - typically one set +is used for the background and the other set is used for foreground sprites, +although this is not required. The patterns are laid out in memory as two +chunks of 8 bytes each, where the first 8 bytes correspond to the high bit for +the pixel, and the second 8 bytes correspond to the low bit for the pixel +(each byte representing a row of pixels in the sprite). To help with +generating this sprite data, I have written a script called +[pnm2chr](https://metacpan.org/pod/distribution/Games-NES-SpriteMaker/bin/pnm2chr), +which can convert images in the .PBM/.PGM/.PPM formats (which most image +editors can produce) into CHR-ROM data, so that you can edit sprites in an +image editor instead of a hex editor. + +Once we have the pattern data, we then need the *palette table*, to determine +the actual colors that will be displayed for a given sprite. The way that +colors are determined is actually quite convoluted, going through several +layers of indirection. As a basis, the NES is capable of producing 52 distinct +colors (actually 51, since one of the colors ($0D) is outside of the NTSC +[gamut](https://en.wikipedia.org/wiki/Gamut), and can damage older TVs if +used). From those 52 colors, only 16 can be used at a time (although the 16 +colors can be distinct for the background layer and the sprite layer), and +this set of 16 colors is known as the palette. + +To determine which palette color to use for each pixel in a given background +tile, the two bits from the pattern table are combined with two additional +bits of data from the *attribute table* to create a four bit number (the +pattern bits being the low bits and the attribute bits being the high bits). +The attribute table is a 64-byte chunk of memory which stores two bits for +each 16x16 pixel area of the screen (so each sprite shares the same two-bit +palette with three other sprites in a 2x2 block). The attribute data itself is +packed into bytes such that each byte corresponds to a 4x4 block of sprites. +This is all pretty confusing, so here is a diagram (from the [NES Technical +Documentation](http://emu-docs.org/NES/nestech.txt)) which will hopefully make +things a bit clearer: + + +------------+------------+ + | Square 0 | Square 1 | #0-F represents an 8x8 tile + | #0 #1 | #4 #5 | + | #2 #3 | #6 #7 | Square [x] represents four (4) 8x8 tiles + +------------+------------+ (i.e. a 16x16 pixel grid) + | Square 2 | Square 3 | + | #8 #9 | #C #D | + | #A #B | #E #F | + +------------+------------+ + + Attribute Byte + (Square #) + ---------------- + 33221100 + ||||||+--- Upper two (2) colour bits for Square 0 (Tiles #0,1,2,3) + ||||+----- Upper two (2) colour bits for Square 1 (Tiles #4,5,6,7) + ||+------- Upper two (2) colour bits for Square 2 (Tiles #8,9,A,B) + +--------- Upper two (2) colour bits for Square 3 (Tiles #C,D,E,F) + +For sprites, the upper two bits of the palette index is specified when +requesting the sprite to be drawn. + +The data about which background tile to draw is then stored in the *name +table*. This is a sequence of bytes which correspond to offsets into the +pattern table. For instance, to draw the first pattern in the pattern table, +you would write a $00 to the corresponding location in the name table. The +name table data is the combined with the appropriate attribute table data to +get a palette index, which is then looked up in the palette table to determine +the actual colors to use when drawing the tile. + +The data about which sprites to draw is stored in an entirely separate area of +memory (not part of any address space at all), called the SPR-RAM (sprite +RAM). It is 256 bytes long, and holds four bytes for each of the 64 sprites +that the NES is capable of drawing at any given time. The first byte holds the +vertical offset for the sprite (where the top left of the screen is (0, 0)), +the second byte holds the index into the pattern table for the sprite to draw, +the third byte holds various attributes about the sprite, and the fourth byte +holds the horizontal offset for the sprite. The sprite attributes contain +these bits of data: + +* The low two bits (bits 0 and 1) contain the high bits of the palette index, + as described above. +* Bit 5 is set if the sprite should be drawn behind the background. +* Bit 6 is set if the sprite should be flipped horizontally. +* Bit 7 is set if the sprite should be flipped vertically. + +If you don't need all 64 sprites, you should just move the horizontal and +vertical coordinates such that the sprite is offscreen ($FE or so). + +Now that we have seen all of the different pieces of the PPU memory, here is +how it is all laid out in memory: + + $0000: Pattern Table 0 (typically in CHR-ROM) + $1000: Pattern Table 1 (typically in CHR-ROM) + $2000: Name Table 0 + $23C0: Attribute Table 0 + $2400: Name Table 1 + $27C0: Attribute Table 1 + $2800: (used for mirroring, which I won't discuss here) + $2BC0: (used for mirroring, which I won't discuss here) + $2C00: (used for mirroring, which I won't discuss here) + $2FC0: (used for mirroring, which I won't discuss here) + $3000: (used for mirroring, which I won't discuss here) + $3F00: Palette Table 0 (used for the background) + $3F10: Palette Table 1 (used for sprites) + $3F20: (used for mirroring, which I won't discuss here) + $4000: (used for mirroring, which I won't discuss here) + +So, to draw a background sprite at the top left of the screen, you would write +the sprite index to VRAM address $2000 (assuming default settings). + +The final piece of information necessary to be able to use the PPU is how to +transfer data from main memory into VRAM. This is done via certain +memory-mapped IO addresses. + +First, to copy data into SPR-RAM, you should write all of the sprite data to a +single page (a page is a 256-byte chunk of data whose addresses all start with +the same byte) in RAM, and then write the page number into address $4014 (the +$02 page ($0200-$02FF) is typically used for this purpose). The address to +start writing from can be set by writing a byte to address $2003, and so you +typically want to write $00 into $2003 before starting a full page transfer +with $4014. If you want to write to only certain parts of SPR-RAM, you can do +this via address $2004 - set the base address to write to via $2003 as above, +and then write a sequence of bytes to $2004 to store them into SPR-RAM. + +To copy data into the main VRAM address space, you use the addresses $2006 and +$2007 in the same way that $2003 and $2004 were used for SPR-RAM, except that +you need to write two bytes into $2006 before you start writing to $2007, +since the address space is larger. Since the order of the bytes matters here, +you can read from $2002 to ensure that the next byte written to $2006 will be +the high byte of the address. Note that $2006 is also used for scrolling +(which is based on the last address written to), and so you generally want to +write $2000 back into $2006 at the end of drawing. + +Finally, you need to initialize the PPU in a few ways in order to allow +drawing, which is done via $2000 and $2001. These addresses hold quite a few +different configuration bits, but the most important ones are: + +* Bit 7 of $2000 should be set to enable NMI interrupts (we did this last + time). +* Bit 4 of $2000 should be set to use pattern table 1 instead of 0 for + the background. +* Bit 3 of $2000 should be set to use pattern table 1 instead of 0 for + sprites. +* Bit 0 of $2000 should be set to use name table 1 instead of 0 (this is + actually more complicated due to mirroring, but we won't get into that). +* Bit 4 of $2001 should be set to enable the sprite layer. +* Bit 3 of $2001 should be set to enable the background layer. + +The default pattern and name tables will be fine for now, and so +initialization should set $2000 to $%10000000 and $2001 to $%00011000. + +[Here]({{urls.media}}/sprites.s) is a sample program which draws a background +and a sprite, and allows you to move the sprite around the background with the +controller D-pad. It will require a CHR-ROM data file with actual patterns in +it, so you can download that from [here]({{urls.media}}/sprites.chr) + +Further reading: +* [NES ASM Tutorial](http://nixw0rm.altervista.org/files/nesasm.pdf) +* [NES technical documentation](http://emu-docs.org/NES/nestech.txt) +* [NES ROM Quickstart](http://sadistech.com/nesromtool/romdoc.html) +* [NES 101](http://hackipedia.org/Platform/Nintendo/NES/tutorial%2c%20NES%20programming%20101/NES101.html) +* [CHR ROM vs. CHR RAM](http://wiki.nesdev.com/w/index.php/CHR_ROM_vs._CHR_RAM) +* [CHR data layout for The Legend of Zelda](http://www.computerarcheology.com/wiki/wiki/NES/Zelda) (Note that unlike what is described above, Zelda stores its pattern data in RAM rather than ROM.) -- cgit v1.2.3-54-g00ecf