
Playdate라는 게임기 알고 계신가요? 손바닥만 한 노란색 휴대용 게임기인데, 옆에 크랭크(돌리는 손잡이)가 달려 있어요. 맥 앱으로 유명한 Panic이 만들었어요. 화면은 400×240 해상도의 1비트 흑백 디스플레이예요. 안에는 ARM Cortex-M7 기반 마이크로컨트롤러가 들어 있고요. 요즘 스마트폰에 비하면 거의 전자계산기 수준이거든요. 그런데 이 기기로 놀랄 만큼 부드럽고 화려한 게임이 계속 나와요. 비결은 개발자들이 C로 짜내는 '지저분하지만 효과는 확실한' 최적화예요.
Playdate 개발자 포럼에 올라온 이 글이 바로 그런 이야기예요. C로 Playdate 게임을 만들 때 쓰는 실전 최적화 비법을 다루거든요. 교과서에선 잘 권하지 않지만 실제로 프레임을 살려주는 기법들이에요. 여기서는 이런 상황에서 자주 쓰이는 대표 기법들을 소개하고, 왜 효과가 있는지도 같이 풀어볼게요.
왜 Playdate에선 최적화가 생존 문제일까
Playdate SDK는 Lua와 C를 둘 다 지원해요. Lua는 배우기 쉽고 금방 만들어볼 수 있어서 좋아요. 그런데 오브젝트 수백 개가 날아다니거나 픽셀 단위 이펙트를 그리기 시작하면 금방 한계에 부딪혀요. 그래서 무거운 부분은 C로 짜는 게 보통이에요.
문제는 C로 짜도 여유가 별로 없다는 거예요. 클럭이 168MHz 안팎이니까 30fps를 목표로 하면 한 프레임에 쓸 수 있는 CPU 사이클은 대략 560만 개예요. 많아 보이죠? 그런데 화면 전체가 96,000픽셀이라 픽셀마다 계산을 하면 픽셀당 60사이클도 안 남아요. PC에선 신경도 안 쓰던 작은 낭비가 여기선 전부 프레임 드랍으로 돌아오는 거죠.
대표적인 '지저분한' 트릭들
1. double은 쓰지 마세요. Playdate 칩에는 하드웨어 부동소수점 장치(FPU)가 있지만 단정밀도(float) 연산에 맞춰져 있어요. 함정은 C에서 1.5 같은 숫자 리터럴이 기본적으로 double이라는 거예요. x * 1.5라고 쓰는 순간 x가 double로 바뀌고 훨씬 느린 연산이 돌아가요. 그래서 1.5f처럼 f를 붙이고 sin 대신 sinf, sqrt 대신 sqrtf를 쓰는 습관이 기본이에요. 컴파일러 옵션 -fsingle-precision-constant로 강제할 수도 있고요.
2. 고정소수점(fixed-point) 활용. 이게 뭐냐면, 소수를 정수로 흉내 내는 방법이에요. 16.16 형식이라면 32비트 정수의 위쪽 16비트를 정수부로, 아래쪽 16비트를 소수부로 써요. 그래서 1.5는 1.5×65536 = 98304라는 정수로 저장돼요. 덧셈은 그냥 정수 덧셈이고, 곱셈은 곱한 다음 16비트 시프트하면 끝이에요. 픽셀 좌표는 어차피 정수라서 float와 int를 오가는 변환 비용이 사라져요. 결과도 항상 똑같이 나와서 리플레이 기능을 만들 때 유리하고요.
3. 프레임버퍼에 직접 쓰기. SDK 그리기 함수는 편하지만 범용이라 오버헤드가 있어요. getFrame()으로 화면 버퍼 포인터를 받아서 비트를 직접 만지면 훨씬 빨라질 수 있어요. 1비트 화면이라 1바이트에 픽셀 8개가 들어가요. 한 줄은 32비트 정렬을 위해 52바이트(LCD_ROWSIZE)로 잡혀 있고, 끝의 2바이트는 무시돼요. 바이트 대신 32비트 단위로 쓰면 메모리에 한 번 쓸 때마다 픽셀 32개가 처리되죠.
uint32_t pattern = 0xAAAAAAAA; // 1010... 줄무늬
uint8_t* fb = pd->graphics->getFrame();
uint32_t row = (uint32_t)(fb + y * LCD_ROWSIZE);
for (int i = 0; i < 13; i++) row[i] = pattern;
pd->graphics->markUpdatedRows(y, y);
버퍼에 직접 썼다면 markUpdatedRows()로 바뀐 줄을 꼭 알려줘야 해요. 거꾸로 말하면, 바뀐 줄만 갱신되게 관리하는 것 자체가 큰 최적화예요.
4. 룩업 테이블과 데이터 배치. sin이나 cos 같은 계산은 미리 표로 만들어두고 꺼내 쓰는 게 고전 기법이에요. 메모리 몇 KB를 내주고 CPU 시간을 사는 거죠. 그리고 16MB 메인 메모리는 칩 바깥에 붙은 외부 RAM이라 칩 안쪽 SRAM보다 느려요. CPU 캐시는 KB 단위라 아주 작고요. 그러니 데이터를 여기저기 흩어놓지 말고, 순서대로 훑을 수 있게 배열에 모아두세요. 게임 루프 안에서는 메모리를 할당하지 말고 미리 풀(pool)을 잡아두는 게 좋아요.
5. 측정은 무조건 실기기에서. 시뮬레이터는 PC CPU로 돌아가서 병목이 완전히 다르게 보여요. resetElapsedTime()과 getElapsedTime()으로 구간마다 시간을 재고, 진짜 병목부터 잡아야 해요.
업계 맥락: 레트로의 지혜가 돌아왔다
이 트릭들 상당수는 게임보이와 슈퍼패미컴, 90년대 PC 데모씬 시절의 기법이에요. 흥미로운 건 이렇게 제약이 심한 환경에서 개발할 일이 다시 늘고 있다는 거예요. PICO-8 같은 판타지 콘솔도 있고, ESP32나 RP2040 위에서 돌아가는 게임과 UI도 있어요. 스마트워치와 IoT 기기도 같은 고민을 하고요. 그러니까 Playdate 전용 꼼수가 아니라 임베디드 C 전반에 통하는 지식이에요.
한국 개발자에게 주는 시사점
펌웨어나 임베디드 개발자라면 거의 그대로 가져다 쓸 수 있어요. Cortex-M 계열 칩은 국내 가전이나 IoT 제품에도 흔하니까요. 웹이나 앱 개발자는 당장 쓸 일이 없겠지만, 하드웨어가 실제로 어떻게 일하는지 감을 잡는 데 이만한 교재가 없어요. 불필요한 타입 변환을 줄이고 데이터를 캐시 친화적으로 배치하는 건 서버 튜닝에서도 똑같이 통하거든요. SDK는 무료고 시뮬레이터도 있어서 기기 없이도 시작해볼 수 있어요.
마무리
제약이 심한 하드웨어에서는 '예쁜 코드'보다 '하드웨어를 아는 코드'가 이겨요.
여러분이 써본 최적화 중에 가장 지저분했지만 효과가 좋았던 건 뭐였나요? 그런 트릭을 쓸 때 가독성과 성능 사이의 선은 어디에 그으시나요?
🔗 출처: Hacker News