1998년 닌텐도 64로 나온 레이싱 게임 F-Zero X를 휴대용 게임기인 닌텐도 3DS로 옮겨 입체 3D 화면으로 돌리고 싶다고 해 보자. 팬들이 옛 게임 파일을 분석해 C 언어 코드로 되살려 놓은 공개 소스는 이미 있다. 하지만 그 코드가 있다고 3DS에서 바로 켜지는 건 아니다. 3DS 그래픽 칩에 맞춰 화면 그리는 코드를 새로 짜야 하고 메모리와 소리 처리도 3DS에 맞게 고쳐야 한다. 그 위에서 1초에 60장(60fps)이 나오도록 속도까지 끌어올리려면 혼자 손으로 짜서 몇천 시간은 걸리는 일이다.
hypnotize.works에 올라온 기록은 이 일을 다른 방식으로 끝냈다. 사람 한 명이 방향만 잡아 주고 프로젝트 내내 켜 둔 지휘자 Claude 세션 하나가 할 일을 쪼개 하위 에이전트 147개에게 코딩을 시켰다. 시작하고 24일 만에 신형 New 3DS에서 F-Zero X가 입체 3D로 초당 59.6프레임에 돌아갔다. 이어진 43일 동안은 처음에 너무 느려서 포기했던 구형 Old 3DS까지 돌아가게 만들었다.
이 과정을 지휘한 사람은 처음부터 끝까지 코드를 한 줄도 읽지 않았다. 화면조차 볼 수 없는 AI 에이전트들을 데리고 어떻게 게임기 이식을 끝냈는지, 어디서 막혔고 돈은 얼마나 들었는지 따라가 본다.
원본 코드는 그대로 두고 세 가지 실행 파일을 만들다
완성된 v1.0.0은 코드 폴더 하나에서 실행 파일 세 종류를 만든다. 구형 Old 3DS용 기본 게임, 신형 New 3DS용 기본 게임, 그리고 코스와 레이싱 머신을 직접 만드는 64DD 확장팩을 합친 New 3DS용 버전이다. 그림이나 음악 같은 게임 데이터는 사용자가 자기 게임팩(ROM)에서 미리 뽑아 SD 카드에 넣어 두면 읽어 온다. 덕분에 배포 파일 안에는 저작권이 걸린 원본 데이터가 들어 있지 않다. 모든 게임 모드가 돌아가고 본체 옆 슬라이더를 밀면 화면의 입체 깊이도 조절된다.
코드는 세 층을 포개어 만들었다. 맨 아래에는 팬들이 복원한 F-Zero X 원본 코드(inspectredc/fzerox)가 있다. 그 위에는 닌텐도 64 게임을 다른 기기에서 돌아가게 통역해 주는 프로그램(libultraship)이 올라간다. 이 두 오픈소스는 원본 저장소를 직접 고치지 않고 그대로 둔 채 필요한 수정만 패치 파일 48개로 묶어 빌드할 때 덧씌웠다. 3DS용 개발 도구인 devkitARM에서 빌드가 되도록 기본 라이브러리 newlib를 13줄 고친 것이 외부 수정의 전부다.
맨 위층인 port/3ds 폴더에는 3DS 전용으로 새로 짠 코드가 모인다. 1차 개발이 끝났을 때 15,039줄 규모였다. 처음에는 닌텐도 64 게임이 내리는 그리기 명령 목록을 매 프레임 순서대로 통역해서 화면을 그렸다. 그러다 2차 개발에서는 통역 단계를 거치지 않고 자동차와 트랙의 위치 정보만 읽어 곧바로 3DS 그래픽 칩에 명령을 내리는 전용 렌더러를 새로 짰다. v1.0.0에서는 신형과 구형 모두 이 빠른 방식을 기본으로 쓴다.
에이전트는 3DS를 볼 수 없으니 사람이 SD 카드를 날랐다
작업은 프로젝트 내내 켜 둔 지휘자 Claude 세션 하나가 조율했다. 이 세션이 전체 기억을 유지하면서 작업 지시서를 쓰고 완성된 코드를 합치며 사람과 대화한다. 실제 코딩을 맡는 하위 에이전트들은 각자 분리된 작업 폴더(git 워크트리)와 전용 브랜치, 지시서 파일, 그리고 끝나기 전에 꼭 적어야 하는 진행 기록 파일을 하나씩 받는다. 하위 에이전트는 사람과 직접 대화하거나 중심 코드를 건드릴 수 없다. 지휘자 세션이 PC용 3DS 에뮬레이터로 먼저 돌려 보고 위험한 변경은 실제 게임기 확인까지 거쳐야 중심 코드에 합친다.
AI 에이전트는 실제 3DS 화면을 볼 수도 없고 버튼을 눌러 볼 수도 없다. 그래서 사람이 에이전트의 눈과 손이 되었다. 실행 파일을 SD 카드에 담아 3DS에 꽂고 직접 달린 뒤 기기에 남은 기록(로그) 파일을 다시 컴퓨터로 가져오는 식이다. 1차 개발 기간에 사람이 보낸 메시지 223개 가운데 “SD 카드 꽂았다”거나 “SD 카드 돌아왔다”는 말만 36번 나온다. 다른 메시지도 입체 3D 60fps에 도움이 되는 방법만 추리라거나 동시에 시킬 일이 더 있느냐고 묻는 등 방향과 판정에 집중됐다.
작업 규칙은 사고를 겪으며 하나씩 생겼다. 한 번은 오디오 에이전트가 문제를 고쳤다는데 그 코드가 엉뚱한 실행 파일에 들어간 줄 모르고 시간을 날렸다. 그 뒤로 모든 속도 개선 기능에는 당장 끌 수 있는 스위치를 달고 그 기능이 실제로 켜졌다는 증거를 로그에 한 줄로 꼭 남기게 했다. 사용량 한도나 서버 과부하로 에이전트가 중간에 꺼지는 일도 잦았다. 중간 저장(커밋) 없이 죽은 에이전트 때문에 하루를 날린 뒤로는 진행 파일부터 적고 일찍 커밋하라는 규칙을 세웠다. 여러 에이전트가 동시에 에뮬레이터를 켜다가 서로를 강제로 끄거나 SD 카드 파일을 덮어쓰던 사고는 번호표 구실을 하는 잠금 파일로 차례를 기다리게 해서 막았다.
2차 개발에서는 손품이 많이 줄었다. Wi-Fi로 3DS에 새 실행 파일을 바로 보내는 통로를 만들었고 새 화면 코드가 예전 화면과 픽셀 단위까지 똑같이 그려지는지 비교하는 장치도 붙였다. 여러 에이전트의 코드를 합친 뒤에는 코드가 섞이면서 생긴 버그만 찾아내는 검수 에이전트를 상시로 돌렸고 최종 반영은 끝까지 사람이 승인했다.
New 3DS에서 60fps까지 다섯 라운드
1차 개발은 2026년 8월 11일부터 9월 3일까지 24일이 걸렸고 주간 사용 한도를 다 써서 멈춘 날을 빼면 실제 작업일은 15일이다. 첫 나흘 동안 다섯 갈래로 기초 공사를 동시에 진행해 부팅 버그 6개를 잡고 PC 에뮬레이터에서 초당 12~20프레임으로 첫 레이스를 띄웠다. 8월 21일 실제 New 3DS에 처음 넣었을 때 48~50fps가 나왔고 9월 초에는 60fps를 채우려고 네 가지 속도 개선을 동시에 밀어붙였다.
| 라운드 | 중앙값 | 하위 10% | 60fps 도달 비율 |
|---|---|---|---|
| 캠페인 시작 | 51.3 | 39.2 | 9% |
| 반복 계산 재사용 (메모이제이션) | 57.2 | 46.6 | 29% |
| 화면 조각 그림 합치기 (HUD 아틀라스) · 오디오 정리 | 56.0 | 48.0 | 32% |
| 화면 그리는 작업을 두 번째 CPU 코어로 분리 | 57.6 | 44.6 | 14% |
| 다음 프레임 미리 준비 · 먼 물체 단순화 (자동 LOD) | 59.6 | 51.4 | 53% |
처음 속도를 올린 비결은 ‘계산 결과 다시 쓰기’와 ‘작은 그림 합치기’였다. 닌텐도 64의 그리기 명령을 3DS용으로 바꾸는 구간에서 매번 같은 계산을 반복하길래 한 번 바꾼 결과를 저장해 두고 다시 쓰게(메모이제이션) 하자 중앙값이 57.2fps로 뛰었다. 이어서 속도계나 순위 같은 화면 표시(HUD) 조각들을 큰 그림 한 장(아틀라스)에 모아 그래픽 칩이 그림을 갈아 끼우는 횟수를 줄였다.
네 번째 단계에서는 일손을 나누려고 화면 그리는 작업을 두 번째 CPU 코어로 따로 떼어 보냈다. 그런데 두 코어가 데이터를 주고받으며 기다리는 시간 때문에 60fps 도달 비율이 오히려 14%로 떨어졌다. 이 문제는 다음 프레임을 한 박자 미리 준비하는 방식과 멀리 있어 작게 보이는 자동차를 단순하게 그리는 기법(자동 LOD)을 함께 묶고 나서야 풀렸다. 9월 3일 실기에서 전체 Grand Prix 중앙값 59.6fps, 자동차 30대가 몰리는 구간의 하위 10%도 51.4fps가 나왔다. 이 수치는 당시 기록이며 v1.0.0에서 다시 재지는 않았다.
Old 3DS의 발목을 잡은 16KB 명령어 캐시
구형 Old 3DS는 CPU 속도가 268MHz로 느리고 메모리는 신형의 절반인 64MB뿐이라 첫 조사 때는 아예 포기했던 기종이다. 2차 개발에서 다시 도전하면서 먼저 메모리부터 줄였다. 에뮬레이션 메모리를 절반으로 깎고 배경 음악은 조금씩 읽어 재생하는 스트리밍으로 바꾸고 그래픽 텍스처 용량에도 상한선을 그었다. 그러자 켜자마자 꺼지던 구형 기기에서 9월 29일 마침내 게임이 켜져 약 10fps로 달렸다.
초당 60장을 그리려면 화면 한 장을 약 16.6밀리초(ms, 1천분의 1초) 안에 끝내야 하는데 첫 측정에서는 96ms나 걸렸다. 정작 그래픽 칩이 일한 시간은 3.6ms뿐이었다. 대신 게임 코드와 오디오 코드가 공용 데이터를 번갈아 쓰느라 서로 잠금(뮤텍스)이 풀리기를 기다리는 데 25.8ms를 쓰고 있었다. 잠그는 구간을 꼭 필요한 순간으로 좁히자 이 대기 시간은 0.2ms로 줄었다.
진짜 함정은 그다음이었다. PC 에뮬레이터에서 크게 빨라진 렌더러를 만들어 실제 Old 3DS에 넣었더니 프레임이 거의 움직이지 않았다. CPU 내부 측정 장치로 확인해 보니 명령어 한 줄을 실행하는 데 CPU 박자(클럭)가 평균 4.34번(CPI 4.34)이나 걸렸다. 원래는 한 줄에 한 박자 남짓이면 끝나야 정상인데 전체 시간의 43.6%를 계산이 아니라 다음에 실행할 코드 줄을 메모리에서 가져오느라 허비하고 있었다.
원인은 CPU 안의 작은 임시 작업대인 ‘명령어 캐시’ 크기에 있었다. CPU는 자주 실행하는 코드를 바로 옆 캐시에 올려 두고 빠르게 꺼내 읽는데 Old 3DS는 이 공간이 딱 16KB뿐이다. 매 프레임 반복해서 도는 코드가 16KB를 넘는 순간 캐시에 다 못 올라가서 매번 느린 메인 메모리까지 다녀와야 한다. 똑같은 반복문을 코드 크기만 바꿔 가며 기기에서 돌려 보니 16KB일 때는 명령어당 1.06클럭이던 것이 24KB에서 4.10클럭, 32KB에서 5.62클럭으로 네 배 넘게 느려졌다. PC 에뮬레이터는 이 16KB 한계를 흉내 내지 않으니 PC에서 얻은 이득이 실기로 이어지지 않았던 것이다.
처음에는 직전에 그린 화면을 각도만 살짝 틀어 다시 보여 주는 방식으로 초당 40장을 맞추려 했지만 하늘이 떨리고 화면 가장자리가 찢어졌다. 사용자는 눈속임 대신 진짜 60fps로 가자고 했다. 10월 1일과 2일에 새로 짠 전용 렌더러는 ‘16KB 캐시 안에 코드 넣기’에 맞춰 설계됐다. 큰 코드 하나를 돌리는 대신 계기판, 트랙, 머신, 하늘, 효과, 그림자 여섯 부분으로 일을 나눈 뒤 각 부분을 맡는 작은 반복문 하나가 모두 16KB 안에 들어가게 쪼갰다. 그러자 CPU가 화면 준비에 쓰는 시간이 21.1ms에서 9.1ms로 반 토막 났고 2D Grand Prix 중앙값 프레임 시간이 16.5ms 안으로 들어왔다.
입체 3D는 왼쪽 눈 화면을 그릴 때 그래픽 칩에 보낸 명령을 기록해 두었다가 오른쪽 눈 각도에 맞춰 재활용하는 방식으로 풀었다. 10월 8일 내부 개발 빌드로 달린 Grand Prix 레이스에서는 59~60fps가 나왔고 계산량이 많은 일부 코스는 안전장치가 걸려 30fps로 내려간다.
4,974달러와 사람 팀 35만 달러
1차 개발 동안 지휘 세션과 에이전트가 주고받은 토큰은 모두 31억 7천만 개다. 물량의 96%는 이미 읽은 대화 문맥을 싸게 다시 불러오는 캐시 읽기였고 요금에서 가장 큰 비중을 차지한 것은 모델이 새로 써낸 코드와 답변인 출력 토큰 1,320만 개였다. API 정가로 계산하면 4,974달러지만 사용자는 정액 구독제를 썼다. 해커뉴스 댓글에서 밝힌 실제 지출은 월 200달러 요금제 3개월분인 600달러였다.
가장 비싼 수업료는 8월 14일에 나갔다. 주간 한도에 걸려 작업이 멈춘 동안 대화가 900턴까지 길어진 에이전트 두 개가 꺼지지 않고 살아남았다. 이 둘이 큰 대화 기록을 수백 번 다시 읽으면서 각각 210달러와 209달러를 썼다. 맡은 지시서를 끝낸 에이전트는 바로 멈춰야 한다는 교훈을 여기서 얻었다.
같은 범위를 시니어 개발자 두 명과 파트타임 QA가 맡았다면 5~8개월 동안 25만~45만 달러가 들었을 것으로 추산된다. 중간값과 비교하면 약 70배 싸고 7~8배 빠르다. 물론 전문 팀이 붙었다면 남은 과제까지 닫고 더 다듬어진 결과를 냈을 것이다. 이 비용 차이가 보여 주는 진짜 변화는 단순히 돈을 아꼈다는 데 있지 않다. 될지 안 될지 모르는 아이디어 다섯 개를 동시에 시도해 보고 실기 숫자가 별로면 세 개쯤은 미련 없이 접을 수 있을 만큼 시도가 싸졌다는 데 있다.
코드를 한 줄도 읽지 않은 사람이 남긴 말
회고 사이트에서 사람이 직접 쓴 페이지는 하나뿐이고 나머지는 전부 작업을 수행한 에이전트가 썼다. 그 한 페이지에서 지휘자는 자기 역할을 기획자와 테스터 사이 어딘가로 설명한다. Claude가 전략을 제안하면 승인하거나 되묻고 막히면 조사를 시키고 에뮬레이터를 통과한 빌드를 3DS에 넣어 달려 본 뒤 로그를 가져다주는 일을 3주 동안 반복했다.
나는 코드를 한 줄도 읽지 않았다. 진짜 기술적 결정을 내린 적도 없다.
2022년 ChatGPT를 처음 쓴 날부터 개발자로서 깊은 두려움을 느꼈다는 고백도 있다. 직접 짰다면 몇천 시간이 들었을 텐데 삶의 시간은 유한하니 자기만의 오리지널 게임을 만드는 일과 저울질하면 손으로 할 이유가 없었다는 것이다. AI의 윤리는 흐릿하고 사회에 미칠 파장은 과소평가돼 있으며 이 프로젝트가 그 증거라는 말로 글은 끝난다.
보통 다중 에이전트 코딩은 자동 테스트가 갖춰진 웹 앱에서 시연된다. 반면 여기서는 에이전트가 볼 수 없는 옛 휴대용 게임기를 상대로 측정 주기가 SD 카드 왕복에 묶여 있었고 코드는 남의 저장소 두 곳에 패치를 얹은 구조였다. 그 조건에서 에이전트 147개가 길을 잃지 않게 붙잡아 준 것은 모델의 영리함이 아니었다. 당장 끌 수 있는 스위치와 한 줄짜리 증명 로그, 이른 커밋 같은 규칙, 그리고 실기 숫자를 들고 와서 예와 아니오를 말한 사람이었다.
출처: F-Zero X 3DS Port Case Study, hypnotize.works, 2026년 10월. Hacker News 토론