XOR 기법을 이용한 스프라이트 이동과 백 버퍼 개선
이 글의 목적은 기존 ChibiAkumas의 튜토리얼을 참고하여 정상적인 NDS ROM 구조 + armips용 + 백 버퍼(Back Buffer) 개선을 구현하는 데 있다.
NDS VRAM 직접 출력 모드에서 스프라이트 이동을 구현하기에 앞서 디스플레이 모드에 대해 간단히 설명할 필요가 있다.
NDS 디스플레이 모드
NDS의 디스플레이 모드는 DISPCNT 레지스터(0x04000000)의 bit 16~17을 통해 설정할 수 있다.
| 값 | 바이너리 | 모드 | 설명 |
|---|---|---|---|
| 0 | 0b00 |
화면 꺼짐 | 디스플레이 비활성화 |
| 1 | 0b01 |
일반 2D 렌더링 | BG/OBJ/팔레트/타일 엔진 사용 |
| 2 | 0b10 |
VRAM 직접 출력 | 프레임 버퍼 방식 — (이번 글에서 사용) |
| 3 | 0b11 |
메인 메모리 직접 출력 | 메인 RAM에서 직접 화면 출력 |
이 중 Mode 2 (VRAM 직접 출력)는 타일 엔진, OAM, 팔레트 등을 일절 사용하지 않고, VRAM Bank A(0x06800000)의 256×192×16bpp 데이터를 LCD에 그대로 출력한다. 가장 원시적이고 직관적인 방식이므로 픽셀 단위의 제어 원리를 학습하는 데 적합하다.
정상적인 NDS ROM을 만들기 위한 조건
먼저 왜 헤더와 닌텐도 로고가 필요한지부터 생각해보자. NDS 펌웨어(BIOS)는 ROM을 부팅하기 전에 다음 세 가지를 엄격하게 검증한다.
- 헤더 오프셋 검증: 0x0B2 위치에 고정값
0x96이 존재하는지 - 닌텐도 로고 데이터 검증: 0x0C0~0x15B (156바이트) 데이터가 정확한지 (로고 CRC-16 값이
0xCF56이어야 함) - 헤더 체크섬 검증: 0x000~0x15D 영역의 CRC-16 값이 0x15E 위치의 값과 일치하는지
이 중 하나라도 틀리면 ROM이 부팅되지 않는다. 닌텐도 로고 데이터는 본래 상표권 보호 목적으로 포함된 것이며, 에뮬레이터에서도 이 검증을 수행하는 경우가 많기 때문이다. 따라서 빌드 후 체크섬을 자동으로 맞춰주는 아래와 같은 파이썬 패치 스크립트가 필요하다.
CRC 패치 스크립트 (Python)
import struct
def crc16(data):
crc = 0xFFFF
for byte in data:
crc ^= byte
for _ in range(8):
if crc & 1:
crc = (crc >> 1) ^ 0xA001
else:
crc >>= 1
return crc & 0xFFFF
with open("sprite_anim.nds", "r+b") as f:
header = bytearray(f.read(0x200))
logo_crc = crc16(header[0xC0:0x15C])
print(f"Logo CRC16: 0x{logo_crc:04X} (expected 0xCF56)")
header_crc = crc16(header[0x000:0x15E])
print(f"Header CRC16: 0x{header_crc:04X}")
struct.pack_into('<H', header, 0x15E, header_crc)
f.seek(0)
f.write(header)
print("Checksum patched!")
첫 번째 방식: XOR 기법으로 스프라이트 이동
가장 먼저 시도해 볼 수 있는 방법은 XOR 연산으로 화면(VRAM)에 직접 스프라이트를 그리고 지우는 기법이다. 화면 깜빡임은 발생하지만, 직접적인 연산을 통해 스프라이트를 제어할 수 있다.
A ⊕ B = C (그리기: 현재 화면 ⊕ 스프라이트 데이터)
C ⊕ B = A (지우기: 그려진 화면에 다시 스프라이트를 XOR하면 원래 화면으로 복원됨)
armips 구현 코드
.nds
.arm
.create "sprite_anim.nds", 0x02000000 - 0x8000
.org 0x02000000 - 0x8000
HeaderStart:
.ascii "LEARNASM.NET"
.ascii "0000"
.ascii "00"
.byte 0
.byte 0
.byte 0
.fill 7
.byte 0
.byte 0
.byte 0
.byte 4
.word Arm9_Start - HeaderStart
.word 0x02000000
.word 0x02000000
.word Arm9_End - Arm9_Start
.word Arm7_Start - HeaderStart
.word 0x03800000
.word 0x03800000
.word Arm7_End - Arm7_Start
.word 0
.word 0
.word 0
.word 0
.word 0
.word 0
.word 0
.word 0
.word 0x00586000
.word 0x001808F8
.word 0
.halfword 0
.halfword 0
.word 0
.word 0
.fill 8
.word 0
.word 0x4000
.fill 0x2A
.byte 0x96
.fill 0x0D
.byte 0x24,0xFF,0xAE,0x51,0x69,0x9A,0xA2,0x21,0x3D,0x84,0x82,0x0A,0x84,0xE4,0x09,0xAD
.byte 0x11,0x24,0x8B,0x98,0xC0,0x81,0x7F,0x21,0xA3,0x52,0xBE,0x19,0x93,0x09,0xCE,0x20
.byte 0x10,0x46,0x4A,0x4A,0xF8,0x27,0x31,0xEC,0x58,0xC7,0xE8,0x33,0x82,0xE3,0xCE,0xBF
.byte 0x85,0xF4,0xDF,0x94,0xCE,0x4B,0x09,0xC1,0x94,0x56,0x8A,0xC0,0x13,0x72,0xA7,0xFC
.byte 0x9F,0x84,0x4D,0x73,0xA3,0xCA,0x9A,0x61,0x58,0x97,0xA3,0x27,0xFC,0x03,0x98,0x76
.byte 0x23,0x1D,0xC7,0x61,0x03,0x04,0xAE,0x56,0xBF,0x38,0x84,0x00,0x40,0xA7,0x0E,0xFD
.byte 0xFF,0x52,0xFE,0x03,0x6F,0x95,0x30,0xF1,0x97,0xFB,0xC0,0x85,0x60,0xD6,0x80,0x25
.byte 0xA9,0x63,0xBE,0x03,0x01,0x4E,0x38,0xE2,0xF9,0xA2,0x34,0xFF,0xBB,0x3E,0x03,0x44
.byte 0x78,0x00,0x90,0xCB,0x88,0x11,0x3A,0x94,0x65,0xC0,0x7C,0x63,0x87,0xF0,0x3C,0xAF
.byte 0xD6,0x25,0xE4,0x8B,0x38,0x0A,0xAC,0x72,0x21,0xD4,0xF8,0x07
.halfword 0xCF56
.halfword 0x0000
.word 0
.word 0
.word 0
.word 0
.fill 0x90
.fill 0x7E00 - 4
Arm7_Start:
b Arm7_Start
Arm7_End:
.org 0x02000000
Arm9_Start:
ldr r0, =0x04000304
ldr r1, =0x820F
str r1, [r0]
ldr r0, =0x04000240
mov r1, 0x80
strb r1, [r0]
ldr r0, =0x04000000
ldr r1, =0x00020000
str r1, [r0]
mov r8, 10
mov r9, 10
bl ShowSprite
InfLoop:
ldr r3, =0x04000130
ldrh r0, [r3]
and r0, r0, 0b0000000011110000
cmp r0, 0b0000000011110000
beq InfLoop
bl ShowSprite
tst r0, 0b0000000001000000
bne JoyNotUp
cmp r9, 0
beq JoyNotUp
sub r9, r9, 1
JoyNotUp:
tst r0, 0b0000000010000000
bne JoyNotDown
cmp r9, 192-8
beq JoyNotDown
add r9, r9, 1
JoyNotDown:
tst r0, 0b0000000000100000
bne JoyNotLeft
cmp r8, 0
beq JoyNotLeft
sub r8, r8, 1
JoyNotLeft:
tst r0, 0b0000000000010000
bne JoyNotRight
cmp r8, 256-8
beq JoyNotRight
add r8, r8, 1
JoyNotRight:
bl ShowSprite
ldr r0, =0x10000
Delay:
subs r0, r0, 1
bne Delay
b InfLoop
ShowSprite:
ldr r10, =0x06800000
mov r1, 2
mul r2, r1, r8
add r10, r10, r2
mov r1, 256*2
mul r2, r1, r9
add r10, r10, r2
ldr r1, =SpriteTest
mov r6, 8
Sprite_NextLine:
mov r5, 8
STMFD sp!, {r10}
Sprite_NextByte:
ldrh r3, [r1], 2
ldrh r2, [r10]
eor r3, r3, r2
strh r3, [r10], 2
subs r5, r5, 1
bne Sprite_NextByte
LDMFD sp!, {r10}
add r10, r10, 256*2
subs r6, r6, 1
bne Sprite_NextLine
mov pc, lr
.pool
SpriteTest:
.halfword 0x8000, 0x8000, 0x83FF, 0x83FF, 0x83FF, 0x83FF, 0x8000, 0x8000
.halfword 0x8000, 0x83FF, 0x83FF, 0x83FF, 0x83FF, 0x83FF, 0x83FF, 0x8000
.halfword 0x83FF, 0x83FF, 0x801F, 0x83FF, 0x83FF, 0x801F, 0x83FF, 0x83FF
.halfword 0x83FF, 0x83FF, 0x83FF, 0x83FF, 0x83FF, 0x83FF, 0x83FF, 0x83FF
.halfword 0x83FF, 0x83FF, 0x83FF, 0x83FF, 0x83FF, 0x83FF, 0x83FF, 0x83FF
.halfword 0x83FF, 0x83FF, 0xFFE0, 0x83FF, 0x83FF, 0xFFE0, 0x83FF, 0x83FF
.halfword 0x8000, 0x83FF, 0x83FF, 0xFFE0, 0xFFE0, 0x83FF, 0x83FF, 0x8000
.halfword 0x8000, 0x8000, 0x83FF, 0x83FF, 0x83FF, 0x83FF, 0x8000, 0x8000
Arm9_End:
.close
XOR 방식의 핵심 문제는 화면에 출력 중인 VRAM을 직접 수정한다는 점이다. 이로 인해 다음과 같은 한계가 발생한다.
- 깜빡임(Flicker):
ShowSprite로 스프라이트를 지우고 새 위치에 다시 그리는 사이에 LCD가 화면을 갱신하면, 스프라이트가 사라진 프레임이 눈에 보인다. - VBlank 동기화 없음:
Delay루프로 타이밍을 잡고 있어서 LCD 갱신 주기와 무관하게 VRAM을 수정하게 된다. - 스프라이트 겹침 불가: 두 스프라이트가 겹치면 XOR 연산끼리 간섭하여 색이 깨질 우려가 있어 확장성에 좋지 않다.
- 배경 제한: 검정(
0x0000) 배경에서만 정상 동작한다.
개선된 방식: 백 버퍼 + VBlank + DMA 전송
이는 정석적인 개발 방식으로, 화면에 보이는 VRAM은 절대 직접 건드리지 않고 보이지 않는 백 버퍼(메인 RAM)에서 프레임을 완성한 뒤 VBlank 때 DMA로 한 번에 복사한다. 사용자 눈에는 항상 완성된 프레임만 보이므로 깜빡임이 사라지는 것을 확인할 수 있다.
정석적인 프레임 루프 구조
Active Display 기간 (라인 0~191)
→ LCD가 VRAM을 읽어 화면에 출력하는 중
→ 이 동안 CPU는 게임 로직 처리 및 백 버퍼에 렌더링 완료
VBlank 기간 (라인 192~262)
→ LCD가 화면을 갱신하지 않는 구간
→ 이미 완성된 백 버퍼를 DMA로 VRAM에 전송
armips 구현 코드
.nds
.arm
.create "sprite_anim.nds", 0x02000000 - 0x8000
.org 0x02000000 - 0x8000
HeaderStart:
.ascii "LEARNASM.NET"
.ascii "0000"
.ascii "00"
.byte 0
.byte 0
.byte 0
.fill 7
.byte 0
.byte 0
.byte 0
.byte 4
.word Arm9_Start - HeaderStart
.word 0x02000000
.word 0x02000000
.word Arm9_End - Arm9_Start
.word Arm7_Start - HeaderStart
.word 0x03800000
.word 0x03800000
.word Arm7_End - Arm7_Start
.word 0
.word 0
.word 0
.word 0
.word 0
.word 0
.word 0
.word 0
.word 0x00586000
.word 0x001808F8
.word 0
.halfword 0
.halfword 0
.word 0
.word 0
.fill 8
.word 0
.word 0x4000
.fill 0x2A
.byte 0x96
.fill 0x0D
.byte 0x24,0xFF,0xAE,0x51,0x69,0x9A,0xA2,0x21,0x3D,0x84,0x82,0x0A,0x84,0xE4,0x09,0xAD
.byte 0x11,0x24,0x8B,0x98,0xC0,0x81,0x7F,0x21,0xA3,0x52,0xBE,0x19,0x93,0x09,0xCE,0x20
.byte 0x10,0x46,0x4A,0x4A,0xF8,0x27,0x31,0xEC,0x58,0xC7,0xE8,0x33,0x82,0xE3,0xCE,0xBF
.byte 0x85,0xF4,0xDF,0x94,0xCE,0x4B,0x09,0xC1,0x94,0x56,0x8A,0xC0,0x13,0x72,0xA7,0xFC
.byte 0x9F,0x84,0x4D,0x73,0xA3,0xCA,0x9A,0x61,0x58,0x97,0xA3,0x27,0xFC,0x03,0x98,0x76
.byte 0x23,0x1D,0xC7,0x61,0x03,0x04,0xAE,0x56,0xBF,0x38,0x84,0x00,0x40,0xA7,0x0E,0xFD
.byte 0xFF,0x52,0xFE,0x03,0x6F,0x95,0x30,0xF1,0x97,0xFB,0xC0,0x85,0x60,0xD6,0x80,0x25
.byte 0xA9,0x63,0xBE,0x03,0x01,0x4E,0x38,0xE2,0xF9,0xA2,0x34,0xFF,0xBB,0x3E,0x03,0x44
.byte 0x78,0x00,0x90,0xCB,0x88,0x11,0x3A,0x94,0x65,0xC0,0x7C,0x63,0x87,0xF0,0x3C,0xAF
.byte 0xD6,0x25,0xE4,0x8B,0x38,0x0A,0xAC,0x72,0x21,0xD4,0xF8,0x07
.halfword 0xCF56
.halfword 0x0000
.word 0
.word 0
.word 0
.word 0
.fill 0x90
.fill 0x7E00 - 4
Arm7_Start:
b Arm7_Start
Arm7_End:
.org 0x02000000
Arm9_Start:
ldr sp, =0x02400000
ldr r0, =0x04000304
ldr r1, =0x820F
str r1, [r0]
ldr r0, =0x04000240
mov r1, #0x80
strb r1, [r0]
ldr r0, =0x04000000
ldr r1, =0x00020000
str r1, [r0]
mov r8, #10
mov r9, #10
bl ClearBackBuffer
FrameLoop:
ldr r3, =0x04000130
ldrh r0, [r3]
and r0, r0, #0xF0
tst r0, #0x40
bne JoyNotUp
cmp r9, #0
beq JoyNotUp
sub r9, r9, #1
JoyNotUp:
tst r0, #0x80
bne JoyNotDown
cmp r9, #(192-8)
beq JoyNotDown
add r9, r9, #1
JoyNotDown:
tst r0, #0x20
bne JoyNotLeft
cmp r8, #0
beq JoyNotLeft
sub r8, r8, #1
JoyNotLeft:
tst r0, #0x10
bne JoyNotRight
cmp r8, #(256-8)
beq JoyNotRight
add r8, r8, #1
JoyNotRight:
bl ClearBackBuffer
bl DrawSprite
bl WaitVBlank
bl DMATransfer
b FrameLoop
ClearBackBuffer:
stmfd sp!, {r0-r3, lr}
ldr r0, =BackBuffer
mov r1, #0
ldr r2, =24576
ClearLoop:
str r1, [r0], #4
subs r2, r2, #1
bne ClearLoop
ldmfd sp!, {r0-r3, pc}
DrawSprite:
stmfd sp!, {r0-r7, r10-r11, lr}
ldr r10, =BackBuffer
mov r1, #2
mul r2, r8, r1
add r10, r10, r2
ldr r1, =512
mul r2, r9, r1
add r10, r10, r2
ldr r11, =SpriteData
mov r6, #8
DrawSprite_Row:
mov r5, #8
stmfd sp!, {r10}
DrawSprite_Pixel:
ldrh r3, [r11], #2
tst r3, #0x8000
beq DrawSprite_Skip
strh r3, [r10]
DrawSprite_Skip:
add r10, r10, #2
subs r5, r5, #1
bne DrawSprite_Pixel
ldmfd sp!, {r10}
add r10, r10, #512
subs r6, r6, #1
bne DrawSprite_Row
ldmfd sp!, {r0-r7, r10-r11, pc}
WaitVBlank:
ldr r0, =0x04000006
WaitVBlank_NotYet:
ldrh r1, [r0]
cmp r1, #192
bne WaitVBlank_NotYet
mov pc, lr
DMATransfer:
stmfd sp!, {r0-r2, lr}
ldr r0, =0x040000D4
ldr r1, =BackBuffer
str r1, [r0]
ldr r1, =0x06800000
str r1, [r0, #4]
ldr r1, =0x84006000
str r1, [r0, #8]
ldmfd sp!, {r0-r2, pc}
.pool
SpriteData:
.halfword 0x0000, 0x0000, 0x83FF, 0x83FF, 0x83FF, 0x83FF, 0x0000, 0x0000
.halfword 0x0000, 0x83FF, 0x83FF, 0x83FF, 0x83FF, 0x83FF, 0x83FF, 0x0000
.halfword 0x83FF, 0x83FF, 0x801F, 0x83FF, 0x83FF, 0x801F, 0x83FF, 0x83FF
.halfword 0x83FF, 0x83FF, 0x83FF, 0x83FF, 0x83FF, 0x83FF, 0x83FF, 0x83FF
.halfword 0x83FF, 0x83FF, 0x83FF, 0x83FF, 0x83FF, 0x83FF, 0x83FF, 0x83FF
.halfword 0x83FF, 0x83FF, 0xFFE0, 0x83FF, 0x83FF, 0xFFE0, 0x83FF, 0x83FF
.halfword 0x0000, 0x83FF, 0x83FF, 0xFFE0, 0xFFE0, 0x83FF, 0x83FF, 0x0000
.halfword 0x0000, 0x0000, 0x83FF, 0x83FF, 0x83FF, 0x83FF, 0x0000, 0x0000
.align 4
BackBuffer:
.fill 256 * 192 * 2
Arm9_End:
.close
두 방식을 비교하여 요약하면 다음과 같다.
| 항목 | XOR 방식 | 백 버퍼 + DMA 방식 |
|---|---|---|
| 깜빡임 | 있음 | 없음 |
| VRAM 직접 수정 | 함 (프론트 버퍼) | 안 함 (백 버퍼에만 그림) |
| VBlank 동기화 | 없음 (Delay 루프) | 있음 (VCOUNT 대기) |
| VBlank에서 하는 일 | — | DMA 전송만 |
| 투명 처리 | 불가 | bit15로 투명/불투명 구분 |
| 스프라이트 겹침 | XOR 간섭으로 깨짐 | 정상 동작 |
| 배경 | 검정색만 가능 | 어떤 배경이든 가능 |
| 확장성 | 스프라이트가 커지면 한계 도달 | 렌더링은 Active 기간에 하므로 여유 있음 |
결과: 백 버퍼와 DMA를 적용한 결과, 화면 깜빡임 없이 부드럽게 이동하는 것을 확인할 수 있다.
![]()