PPU (Picture Processing Unit)
이제 화면을 그려볼 차례
- vram(Video RAM, Console-Internal Ram, CIRAM)
- 해상도 256x240(RP2A03기준)
- V-Blank
렌더링은 어떻게?
- NES Graphics Explained
- NES Attribute Tables Explained
- https://nerdy-nights.nes.science/#main_tutorial-2
- https://nerdy-nights.nes.science/#main_tutorial-4
- everything2 - NES PPU
- https://nesmaker.nerdboard.nl/2024/10/14/nes-architecture-ppu-and-cpu-timing/
- https://blogs.oregonstate.edu/ericmorgan/2022/02/18/nes-emulation-ppu-quick-start/
- https://github.com/emu-russia/breaks/releases/tag/ppu-book-revB-english
- https://famicom.party/book/09-theppu/
- https://nesrocks.com/blog/nes-graphics/
- https://austinmorlan.com/posts/nes_rendering_overview/
일단 용어부터
- https://www.nesdev.org/wiki/PPU_pattern_tables
- https://www.nesdev.org/wiki/PPU_nametables
- https://www.nesdev.org/wiki/PPU_OAM
| 팔레트 | 사용가능한 64(0x3F)가지 색상 코드(Color Code)표(고유색은 56) |
| 서브 팔레트 | 타일마다 사용할 4가지 색상코드 (x0x1x2x3) |
| 배경 팔레트 | 4개의 서브 팔레트로 구성.(a0a1a2a3_b0b1b2b3_c0c1c2c3_d0d1d2d3) |
| vram $3F00-$3F0F 16byte 영역에 위치. | |
| 스프라이트 팔레트 | 4개의 서브 팔레트로 구성. |
| vram $3F10-$3F1F 16byte 영역에 위치. | |
| 패턴(Pattern) 테이블 | 서브 팔레트의 내부 인덱스(0,1,2,3) 모음. (16byte(타일) * 256개 = 4KB) |
| vram $0000-$0FFF 4KB 배경 / $1000-$1FFF 4KB 스프라이트 영역에 위치 | |
| 네임(Name) 테이블 | 패턴 테이블의 인덱스 모음 - 화면의 타일 배치 정보 (타일 32x30 = 960바이트) |
| 속성(Attribute) 테이블 | 몇번째 서브 팔레트 사용할지 고르는 인덱스(a,b,c,d). 배경 only |
| OAM(Object Attribute Memory) | vram과는 별도의 다른 메모리. 개당 4바이트, 최대 64개의 스프라이트 정보 (256바이트) 스프라이트 only |
배경은
화면:
256 * 240 = 61440 픽셀로 구성됨
32 * 30 = 960 타일로 구성됨 (256/8 * 240/8)
타일:
타일당: 8 * 8 = 64 픽셀
픽셀당 2비트로 서브 팔레트내의 몇번째 색상코드를 사용할지 저장
타일당: 64 * 2bit = 128bit = 16 byte (패턴테이블에 저장시)
패턴 테이블: 00 => (a,b,c,d)
vram $0000-$0FFF (4KB): 배경
4KB = 4096 byte = 16 byte * 256
네임 테이블:
vram $2000-$23BF (960byte): Name Table 0
1byte(8bit)로 표현할 수 있는 것은 2^8 = 256 개
타일 32 * 30 = 960개 => 패턴테이블 인덱스로 960바이트가 필요
메타(meta) 타일:
2 * 2 타일
속성 테이블: 00 => (0,1,2,3)
vram $23C0-$23FF (64byte): Attribute Table 0
화면에 있는 타일을 메타타일 단위로 나누면 (32/2) * (30/2) = 16 * 15 = 60
A | B
--+--
C | D
타일당 2비트씩 1byte: 0bDDCC_BBAA
배경 팔레트
vram $3F00-$3F0F (16byte) : a0a1a2a3_b0b1b2b3_c0c1c2c3_d0d1d2d3에서
- 픽셀(x, y)에서
- 타일 단위 (스크롤 고려) 좌표변환 네임테이블에서 타일번호 가져옴
- (메타타일 단위의)속성 테이블을 참조해서 (0,1,2,3)을 판별
- (타일 단위의)패턴 테이블을 참조해서 (a,b,c,d)를 판별
- 배경 팔레트 a0a1a2a3_b0b1b2b3_c0c1c2c3_d0d1d2d3에서 색상 코드를 가져와서
- 팔레트에서 색상코드에 맞는 색을 가져옴
- 뱅크 스위칭(Bank Switching)
- 화면 960타일인데 타일은 256종류라 표현하기 힘들게 된다면 게임 진행 중에 패턴 테이블의 내용 바꿔버림
스프라이트는
OAM (Object Attribute Memory): 00 => (0,1,2,3)
vram과는 별도의 다른 메모리.
개당 4바이트, 최대 64개의 스프라이트 정보
0| Y position
1| 76543210
||||||||
|||||||+- Bank ($0000 or $1000) of tiles
+++++++-- Tile number of top of sprite (0 to 254; bottom half gets the next tile)
2| 76543210
||||||||
||||||++- Palette (4 to 7) of sprite
|||+++--- Unimplemented (read 0)
||+------ Priority (0: in front of background; 1: behind background)
|+------- Flip sprite horizontally
+-------- Flip sprite vertically
3| X position
패턴 테이블: 00 => (a,b,c,d)
vram $1000-$1FFF (4KB): 배경
스프라이트 팔레트:
vram $3F10-$3F1F (16byte) : a0a1a2a3_b0b1b2b3_c0c1c2c3_d0d1d2d3에서
- 픽셀(x, y)에서
- OAM의 스프라이트 정보중 2번째 바이트의 팔레트 정보를 참조해서 (0,1,2,3)을 판별
- (타일 단위의)패턴 테이블을 참조해서 (a,b,c,d)를 판별
- 스프라이트 팔레트 a0a1a2a3_b0b1b2b3_c0c1c2c3_d0d1d2d3에서 색상 코드를 가져와서
- 팔레트에서 색상코드에 맞는 색을 가져옴
타일
그림 | 색상 인덱스(0 ~ 3)
. . . . . . * * | 00 00 00 00 00 00 01 01
. . . . . * * * | 00 00 00 00 00 01 01 01
. . . . * * * * | 00 00 00 00 01 01 01 01
. . . * * * * * | 00 00 00 01 01 01 01 01
. . * @ @ * * * | 00 00 01 11 11 01 01 01
. * * * # @ * * | 00 01 01 01 10 11 01 01
. * * * # @ @ @ | 00 01 01 01 10 11 11 11
* * * * # @ # * | 01 01 01 01 10 11 10 01
하드웨어 제약 때문에 hi비트랑 lo비트나눔
hi | lo
0 0 0 0 0 0 0 0 0x00 | 0 0 0 0 0 0 1 1 0x03
0 0 0 0 0 0 0 0 0x00 | 0 0 0 0 0 1 1 1 0x07
0 0 0 0 0 0 0 0 0x00 | 0 0 0 0 1 1 1 1 0x0F
0 0 0 0 0 0 0 0 0x00 | 0 0 0 1 1 1 1 1 0x1F
0 0 0 1 1 0 0 0 0x18 | 0 0 1 1 1 1 1 1 0x3F
0 0 0 0 1 1 0 0 0x0C | 0 1 1 1 0 1 1 1 0x77
0 0 0 0 1 1 1 1 0x0F | 0 1 1 1 0 1 1 1 0x77
0 0 0 0 1 1 1 0 0x0E | 1 1 1 1 0 1 0 1 0xF5
8 + 8 = 16 byte
저장시 lo8개 hi8개 순서로 Pattern Table에 저장
| $0000-$0FFF | Pattern Table 0 |
| $1000-$1FFF | Pattern Table 1 |
lo | hi
0x03 0x07 0x0F 0x1F 0x3F 0x77 0x77 0xF5 | 0x00 0x00 0x00 0x00 0x18 0x0C 0x0F 0x0E
스캔라인
262 scanline
3 | Vertical Sync |
+---------------------------------------+
14 | VBLANK |
+----------+-------------------+--------+
240 | border | VISIBLE_FRAME | border |
| HBLANK | <-- 256 pixel --> | HBLANK |
+----------+-------------------+--------+
5 | VBLANK |
<-- 341 ppu pixels -->
cpu 1 cycle당 ppu 3 step
ppu step중 nmi 트리거
256w x 240h
for 240
256 픽셀 렌더링
85 PPU
241
vBlank(vertical blanking)
Cpu.Nmi
cpu에서 ppu
| cpu addr | ||
|---|---|---|
| $0200-$02FF | RAM - 일반적으로 Shadow OAM for Sprite (256 bytes) | OAM에 넣기전 cpu에 있는 256 bytes |
| $2000-$2007 | PPU (V-RAM) 레지스터 (8) | |
| $2008-$3FFF | PPU - Mirror($2000-$2007) x 1024 | |
| $4014 | OAM DMA | 앞선 cpu 256bytes를 ppu OAM에 복사 |
PPU 레지스터
- https://www.nesdev.org/wiki/PPU_registers#PPUADDR
| from CPU | PPU (V-RAM) 레지스터 | |
|---|---|---|
| $2000 | PPUCTRL | PPU 제어 설정 (네임테이블 선택, NMI 활성화 등) |
| $2001 | PPUMASK | 화면 표시 설정 (배경/스프라이트 렌더링 온/오프) |
| $2002 | PPUSTATUS | PPU 상태 플래그 (VBlank 시작 여부 등) |
| $2003 | OAMADDR | OAM 주소 지정 |
| $2004 | OAMDATA | OAM 데이터 읽기/쓰기 |
| $2005 | PPUSCROLL | 카메라 스크롤 위치 제어 (X, Y 좌표 축 입력) |
| $2006 | PPUADDR | VRAM 주소 설정 (2번에 걸쳐 16비트 주소 작성) |
| $2007 | PPUDATA | VRAM 데이터 읽기/쓰기 |
Loopy 레지스터
- https://www.nesdev.org/wiki/PPU_scrolling#PPU_internal_registers
| Loopy 레지스터 | ||
|---|---|---|
| v | Current VRAM Address | 현재 PPU가 배경을 그리기 위해 참조하고 있는 실제 VRAM 주소 (가장 중요) |
| t | Temporary VRAM Address | CPU가 주소나 스크롤을 설정할 때 임시로 저장해두는 공간 |
| x | Fine X Scroll | 픽셀 단위의 미세한 가로 스크롤 값 (3비트) |
| w | Write Toggle | $2005, $2006 레지스터는 8비트인데 주소는 16비트(실제론 15비트)라, 첫 번째 쓴 값인지 두 번째 쓴 값인지 구별하는 스위치 (0 또는 1) |
PPU 주소
| PPU addr | |
|---|---|
| $0000-$0FFF | Pattern Table 0 |
| $1000-$1FFF | Pattern Table 1 |
| $2000-$23BF | Name Table 0 - 배치정보 960byte |
| $23C0-$23FF | Attribute Table 0 - 64byte |
| $2400-$27BF | Nametable 1 |
| $27C0-$27FF | Attribute Table 1 |
| $2800-$2BBF | Nametable 2 |
| $2BF0-$2BFF | Attribute Table 2 |
| $2C00-$2FBF | Nametable 3 |
| $2FC0-$2FFF | Attribute Table 3 |
| $3000-$3EFF | Name/Attribute Table Mirror - 0/1/2/3 |
| $3F00-$3F0F | Palette - Background |
| $3F10-$3F1F | Palette - Sprite |
| $3F1F-$3FFF | Palette - Mirror x 6 |
| Palette - $3F00-$3F1F 32바이트 | 배경 16 / 스프라이트 16 | |
|---|---|---|
| $3F00 | Palette(Background) - sub 0 | Universal background color (backdrop) |
| $3F01-$3F03 | Palette(Background) - sub 0 | 3 colors |
| $3F04 | Palette(Background) - sub 1 | $3F00 - Mirror |
| $3F05-$3F07 | Palette(Background) - sub 1 | 3 colors |
| $3F08 | Palette(Background) - sub 2 | 3F00 - Mirror |
| $3F09-$3F0B | Palette(Background) - sub 2 | 3 colors |
| $3F0C | Palette(Background) - sub 3 | $3F00 - Mirror |
| $3F0D-$3F0F | Palette(Background) - sub 3 | 3 colors |
| $3F10 | Palette(Sprite) - sub 0 | $3F00 - Mirror |
| $3F11-$3F13 | Palette(Sprite) - sub 0 | 3 colors |
| $3F14 | Palette(Sprite) - sub 1 | $3F00 - Mirror |
| $3F15-$3F17 | Palette(Sprite) - sub 1 | 3 colors |
| $3F18 | Palette(Sprite) - sub 2 | $3F00 - Mirror |
| $3F19-$3F1B | Palette(Sprite) - sub 2 | 3 colors |
| $3F1C | Palette(Sprite) - sub 3 | $3F00 - Mirror |
| $3F1D-$3F1F | Palette(Sprite) - sub 3 | 3 colors |
OAM DMA https://www.nesdev.org/wiki/DMA
미러링
비디오 램은 2KB임.
VRAM[0x0000-0x03FF] 1KB
VRAM[0x0400-0x07FF] 1KB
여기에 Nametable을 저장할꺼임. Nametable 0~3까지 하면 $2000-$2FFF, 즉 4k(0x0FFF)범위인데 메모리는 2KB(0x07FF)라 모자람.
Name Table
+-----+-----+
| NT0 | NT1 |
+-----+-----+
| NT2 | NT3 |
+-----+-----+
Actual VRAM
+-----+-----+
| A | B |
+-----+-----+
그래서 동일한 메모리를 가리키도록 미러링을 하는데 여기서 수평/수직으로 하는 방법이 있음.
Vertical Mirroring
- 가로 스크롤에 유리
- 세로로 한 화면 정도 이동하는 건 충분히 가능
+-----+-----+
| A | B |
+-----+-----+
| A | B |
+-----+-----+
NT0과 NT2가 동일한 메모리 A(1KB)참조
NT1과 NT3가 동일한 메모리 B(1KB)참조
Horizontal Mirroring
- 세로 스크롤에 유리
+-----+-----+
| A | A |
+-----+-----+
| B | B |
+-----+-----+
NT0과 NT1가 동일한 메모리 A(1KB)참조
NT2과 NT3가 동일한 메모리 B(1KB)참조
- Gauntlet - 2KB 카트리지에 VRAM을 박아놔서 미러링 안하고 가로+세로 스크롤이 가능.
- PPU의 2KB + 카트리지 2KB = 4KB(0x0FFF)로 A/B/C/D에 맞춤.
NMI (non-maskable interrupt)
- https://www.nesdev.org/wiki/NMI
팔레트(.pal) 파일
- https://www.nesdev.org/wiki/.pal
- pal 생성기
- https://bisqwit.iki.fi/utils/nespalette.php
- http://drag.wootest.net/misc/palgen.html
64 colors × 3 byte(8bpc sRGB) = 192 bytes
- cpu가 $2006(PPUADDR)를 사용하여 ppu한테 vram주소 지정
- 2바이트가 필요하지만, cpu가 1바이트씩 보내니 ppu내 loopy register T(Temporary VRAM Address)에 기록시작
- 2번째에 loopy register T(Temporary VRAM Address)가 차게되면 loopy register V(Current VRAM Address)에 복사
- cpu가 $2007(PPUDATA)로 데이터쓰면 ppu가 앞서 저장한 V에서 vram주소를 얻어 vram에 기록
- vram에 데이터를 기록 후 V를 증가시켜 다음 주소를 가리키도록함