Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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-$02FFRAM - 일반적으로 Shadow OAM for Sprite (256 bytes)OAM에 넣기전 cpu에 있는 256 bytes
$2000-$2007PPU (V-RAM) 레지스터 (8)
$2008-$3FFFPPU - Mirror($2000-$2007) x 1024
$4014OAM DMA앞선 cpu 256bytes를 ppu OAM에 복사

PPU 레지스터

  • https://www.nesdev.org/wiki/PPU_registers#PPUADDR
from CPUPPU (V-RAM) 레지스터
$2000PPUCTRLPPU 제어 설정 (네임테이블 선택, NMI 활성화 등)
$2001PPUMASK화면 표시 설정 (배경/스프라이트 렌더링 온/오프)
$2002PPUSTATUSPPU 상태 플래그 (VBlank 시작 여부 등)
$2003OAMADDROAM 주소 지정
$2004OAMDATAOAM 데이터 읽기/쓰기
$2005PPUSCROLL카메라 스크롤 위치 제어 (X, Y 좌표 축 입력)
$2006PPUADDRVRAM 주소 설정 (2번에 걸쳐 16비트 주소 작성)
$2007PPUDATAVRAM 데이터 읽기/쓰기

Loopy 레지스터

  • https://www.nesdev.org/wiki/PPU_scrolling#PPU_internal_registers
Loopy 레지스터
vCurrent VRAM Address현재 PPU가 배경을 그리기 위해 참조하고 있는 실제 VRAM 주소 (가장 중요)
tTemporary VRAM AddressCPU가 주소나 스크롤을 설정할 때 임시로 저장해두는 공간
xFine X Scroll픽셀 단위의 미세한 가로 스크롤 값 (3비트)
wWrite Toggle$2005, $2006 레지스터는 8비트인데 주소는 16비트(실제론 15비트)라, 첫 번째 쓴 값인지 두 번째 쓴 값인지 구별하는 스위치 (0 또는 1)

PPU 주소

PPU addr
$0000-$0FFFPattern Table 0
$1000-$1FFFPattern Table 1
$2000-$23BFName Table 0 - 배치정보 960byte
$23C0-$23FFAttribute Table 0 - 64byte
$2400-$27BFNametable 1
$27C0-$27FFAttribute Table 1
$2800-$2BBFNametable 2
$2BF0-$2BFFAttribute Table 2
$2C00-$2FBFNametable 3
$2FC0-$2FFFAttribute Table 3
$3000-$3EFFName/Attribute Table Mirror - 0/1/2/3
$3F00-$3F0FPalette - Background
$3F10-$3F1FPalette - Sprite
$3F1F-$3FFFPalette - Mirror x 6
Palette - $3F00-$3F1F 32바이트배경 16 / 스프라이트 16
$3F00Palette(Background) - sub 0Universal background color (backdrop)
$3F01-$3F03Palette(Background) - sub 03 colors
$3F04Palette(Background) - sub 1$3F00 - Mirror
$3F05-$3F07Palette(Background) - sub 13 colors
$3F08Palette(Background) - sub 23F00 - Mirror
$3F09-$3F0BPalette(Background) - sub 23 colors
$3F0CPalette(Background) - sub 3$3F00 - Mirror
$3F0D-$3F0FPalette(Background) - sub 33 colors
$3F10Palette(Sprite) - sub 0$3F00 - Mirror
$3F11-$3F13Palette(Sprite) - sub 03 colors
$3F14Palette(Sprite) - sub 1$3F00 - Mirror
$3F15-$3F17Palette(Sprite) - sub 13 colors
$3F18Palette(Sprite) - sub 2$3F00 - Mirror
$3F19-$3F1BPalette(Sprite) - sub 23 colors
$3F1CPalette(Sprite) - sub 3$3F00 - Mirror
$3F1D-$3F1FPalette(Sprite) - sub 33 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를 증가시켜 다음 주소를 가리키도록함