2P by GN⁺ 19시간전 | ★ favorite | 댓글 1개
  • CAMLBOY는 OCaml로 개발되어 브라우저에서 동작하는 Game Boy 에뮬레이터
  • OCaml의 중상규모 프로젝트 개발 및 고급 기능 사용법을 실제로 익히기 위해 선택된 프로젝트임
  • 기본 구조, 추상화, GADT, 펑터, 런타임 모듈 교체 등 다양한 OCaml 언어 특성을 실용적으로 활용함
  • 브라우저에서 60FPS로 동작하며, 성능 개선 과정과 병목 분석, 최적화 경험을 공유함
  • OCaml 생태계, 테스트 자동화, 그리고 에뮬레이터 개발이 실무 능력 향상에 미치는 영향을 정리함

프로젝트 개요

  • 몇 달간 CAMLBOY 프로젝트를 진행하며, OCaml로 Game Boy 에뮬레이터를 제작함
  • 데모 페이지에서 실행 가능하며, 다양한 homebrew ROM을 포함함
  • 저장소는 GitHub에 공개됨

OCaml 학습 동기와 프로젝트 선정 배경

  • 새로운 언어 학습 시, 중/대규모 코드 작성 방법고급 기능의 실제 활용법에 한계를 느낌
  • 이러한 문제 해결을 위해 실질적인 프로젝트 경험의 필요성을 느껴 Game Boy 에뮬레이터 개발을 선택함
  • 이유
    • 사양이 명확해 구현 범위가 정해져 있음
    • 충분히 복잡하지만 수개월 내 완료 가능한 크기임
    • 개인적 동기가 큼

에뮬레이터 목표

  • 가독성 및 유지보수성을 중시한 코드 작성
  • js_of_ocamlJavaScript로 컴파일해 브라우저에서 실행
  • 모바일 브라우저에서도 플레이 가능한 FPS 달성
  • 다양한 컴파일러 백엔드 성능 벤치마크 구현

본문 목표 및 주요 내용

이 글의 목적은 OCaml로 Game Boy 에뮬레이터를 제작하는 여정 공유임
다루는 내용:

  • Game Boy 아키텍처 개요
  • 테스트 가능하고 재사용성 높은 코드 구조화 방법
  • functor, GADT, 일급 모듈 등 고급 OCaml 기능 실전 활용
  • 성능 병목 찾기, 최적화 및 개선 경험
  • OCaml 전반에 대한 생각

전체 구조와 주요 인터페이스

  • CPU, Timer, GPU 등 주요 하드웨어가 동기화된 클럭에 따라 동작
  • 버스는 주소에 따라 각 하드웨어 모듈에 데이터 접근/전달 기능 담당
  • 각 하드웨어 모듈은 Addressable_intf.S 인터페이스를 구현함
  • 버스 전체는 Word_addressable_intf.S 인터페이스를 따름

메인 루프 동작 방식

  • 하드웨어의 동기화를 위해 메인 루프에서 아래 순환 단계 실행
    1. CPU 명령 1개 실행 및 소비된 사이클수 기록
    2. 같은 사이클수만큼 Timer, GPU 진행
  • 이 방법으로 실제 하드웨어의 동기화 상태 모사
  • 구현 코드 예시와 함께 설명 제공

8비트, 16비트 데이터 읽기/쓰기 추상화

  • 다수 모듈이 8비트 데이터 입출력 인터페이스 (Addressable_intf.S) 구현
  • 16비트 읽기/쓰기 확장은 Word_addressable_intf.S를 통해 상속 및 추가 기능 확장
  • OCaml의 서명(signature) , 모듈 타입 포함(include) 방식으로 추상화 계층 구성

버스, 레지스터, CPU 구현

  • 버스: 각 하드웨어 모듈에 대한 주소 기반 라우팅 기능 담당, 메모리맵 기준 분기처리
  • 레지스터: 8비트, 16비트 레지스터 읽기/쓰기 인터페이스 제공
  • CPU: 초기에는 버스 의존성이 강해 테스트가 어려움
    • 펑터(functor) 적용으로 의존성 추상화 및 목(mock) 주입 가능
    • 이를 통해 단위 테스트 작성이 훨씬 쉬워짐

인스트럭션 세트 표현 (GADT 활용)

  • Game Boy는 8/16비트 명령 모두 존재, 인스트럭션 정의의 타입 안정성 필요
  • 단순 variant 방식은 복잡한 패턴 매칭 반환값 타입 충돌 문제 발생
  • GADT(Generalized Algebraic Data Type) 를 적용해 입력 및 출력 타입 모두 안전하게 매칭 가능
  • GADT 적용 시 각 인스트럭션의 인자 타입, 반환값 타입 모두 정확하게 타입 추론 가능
  • 복잡한 명령어 패턴 및 파라미터에 안전하게 대응

카트리지 및 런타임 모듈 선택

  • Game Boy 카트리지는 단순 ROM 외 추가 하드웨어(MBC, 타이머 등) 포함 가능
  • 각 타입별로 모듈 별도 구현 및 런타임에 맞는 모듈 선택 필요
  • 일급 모듈로 런타임 모듈 전환 및 확장성 실현

테스트와 탐색적 개발

  • test ROMppx_expect 활용
    • 기능별 테스트 ROM: 산술연산, MBC 지원 등 구체적 영역 검증
    • 실패시 화면 출력 등 명확한 진단 가능
  • 통합 테스트로 대규모 리팩토링, 새로운 기능 추가 시 신뢰도 확보
  • 탐색적 개발 방식 적용: 테스트 ROM으로 반복적으로 구현 및 검증

브라우저 UI 및 성능 최적화

  • js_of_ocaml쉽게 JS 빌드
  • Brr 라이브러리로 Javascript DOM API에 OCaml 방식으로 안전하게 접근
  • 초기 성능(20FPS)은 낮았으나, 크롬 프로파일러로 GPU, 타이머, Bigstringaf 등 병목 분석
  • 각 모듈별로 최적화 커밋 진행, JS 빌드에서 비효율적인 인라이닝 비활성화로 최종 60FPS(PC/모바일) 달성
  • 네이티브 빌드에서는 1000FPS까지 성능 발휘

벤치마크 및 하드웨어 비교

  • 헤드리스 벤치마크 모드 구현해 각 환경별 FPS 측정 가능

에뮬레이터 개발과 실무 능력

  • 경쟁프로그래밍과 비슷하게, 명확한 사양 해석 → 구현 → 검증 루프 반복
  • 사양 기반 개발/테스트 진행에 실질적 도움이 되는 경험

최신 OCaml 생태계 및 도구 발전

  • dune간편한 빌드 시스템 경험 제공
  • Merlin, OCamlformat 등으로 자동완성, 코드 네비게이션, 포매팅 용이
  • setup-ocaml로 Github Actions에도 손쉽게 적용 가능

함수형 언어에 대한 소고

  • 함수형 언어란 사이드 이펙트 최소화라는 설명에 의문을 가짐
  • 추상화하에 숨은 mutable 상태는 성능을 위해 적극적으로 사용
  • 필자는 정적 타입, 패턴매칭, 모듈 시스템, 타입 추론 등을 선호함

불편사항 및 추상화 의존 비용

  • 종속성 관리 표준화가 아직 복잡하고 설명 부족 (opam 등)
  • 모듈-펑터 구조로 추상화를 가미하면, 의존성 계층 전체 구조까지 수정 필요
  • OOP와 달리 추상화 도입 시 상위 의존 모듈 작성법까지 변경 필요

추천 학습 자료

결론

  • CAMLBOY 프로젝트를 통해 OCaml의 고급 기능과 테스트, 추상화, 브라우저 호환성 등을 실용적으로 경험
  • 생태계 발전과 현실적인 개발 경험에서 얻은 장점과 한계 명확히 인식
  • 에뮬레이터 개발이 중급 이상의 개발자 실력 향상에 실질적인 도움이 됨
Hacker News 의견
  • 에뮬레이터, 가상머신, 바이트코드 인터프리터 작성에 어떤 특정 프로그래밍 언어가 더 적합하다고 자신 있게 말할 사람 있는지 궁금함. 여기서 "더 나은" 기준은 성능이나 구현 오류 줄이기가 아니라, 직접 구현하고 탐구할 때 더 직관적이고, 뭔가를 더 배우고, 구현 경험 자체가 보람되고 재미있다는 측면임. 예를 들어 Erlang은 분산 시스템 영역에서 명확한 목표가 있고, 그 영역을 위한 도메인 지식과 언어 설계가 일치해서 써보면 분산 시스템과 Erlang 자체에 대해 깊이 있는 이해를 얻게 됨. 이런 식으로 "기계 동작을 코드로 표현하는 것"이 타겟인 언어가 있을지 궁금함

    • 시스템 프로그래밍 언어인 C, C++, Rust, Zig가 개인적으로 가장 "만족도 높은" 선택임을 강조하고 싶음. 이 언어들은 데이터 타입(예: uint8)이 메모리의 바이트와 곧바로 매핑되고, memcpy 같은 연산이 곧장 blit 작업과 같음. JavaScript 같은 언어에서 Number 타입을 비트 연산용 바이트로 바꿔쓰며 고생하는 일이 거의 없음. 자바스크립트로 에뮬레이터 만들다 보면 이런 문제를 바로 맞닥뜨림. 물론, 어떤 언어든 그래픽 표시와 충분한 메모리만 지원된다면 다 비슷하게 굴릴 수 있고, 결국 자신이 가장 편한 언어를 선택할 때 최고의 즐거움을 느낄 수 있음

    • Haskell은 DSL과 컴파일러에 필요한 데이터 변환에 뛰어난 성능을 보임. OCaml, Lisp, 패턴매칭과 ADT를 지원하는 현대적인 언어도 모두 적합함. Modern C++도 variant 타입 등으로 비슷한 걸 시도할 수 있지만, 깔끔하진 않음. 실제로 에뮬레이터에서 게임을 돌릴 생각이면 C나 C++이 표준 선택. Rust도 그럭저럭 가능할 거 같긴 한데, 저수준 메모리 조작은 잘 모르겠음

    • 에뮬레이터, 가상머신, 바이트코드 인터프리터를 만들기에 특별히 더 나은 언어는 없다는 입장임. 배열(임의 인덱스에 상수 시간 접근)과 비트 연산만 있으면 구현이 엄청 쉬움. JIT까지 고려하지 않는 수준에서는 함수형 언어도 배열, 비트 연산을 지원함

    • sml, 그 중에서도 MLTon 방언을 추천하고 싶음. OCaml이 좋은 거의 모든 이유를 공유하지만, 개인적으로는 ML-계열 언어 중에서 더 나은 완성형이라는 평가임. OCaml에서 그리운 건 applicative functor 정도인데, 이건 모듈 구조만 약간 다를 뿐 큰 차이 아님

    • 브라우저 안에서 재미와 실험 중심이라면 Elm도 좋은 옵션임. 비슷한 프로젝트로 elmboy 참고 추천

  • 이 글은 Ocaml뿐 아니라 Game Boy 에뮬레이터 구현 과정을 알차게 정리한 내용으로 정말 멋진 자료임. 필자에게 감사함 전함. 덧붙여, 브라우저 안에서 어셈블러 에디터와, 어셈블러/링커/로더까지 한데 합쳐진 SPA로 Gameboy 홈브류 개발 체험을 누구나 쉽게 할 수 있게 만든다면- 임베디드 개발 교육에 좋을 것 같다는 아이디어를 오래전부터 가지고 있었음

    • rgbds-live 프로젝트는 이 아이디어와 비슷하고, RGBDS가 내장됨. rgbds-live
  • 혹시 Game Boy 에뮬레이터에서 사운드 구현에 관한 튜토리얼을 찾는 경우가 있을지 궁금함. 대부분의 튜토리얼이 사운드를 설명하지 않고, 직접 구현해보려고 해도 자료만으로 이해와 구현이 어려웠음

    • 공식적인 튜토리얼은 아니지만, 내가 직접 구현한 방식을 요약한 2개 슬라이드 자료를 공유함: 슬라이드 자료 Game Boy 사운드는 채널이 4개 있고, 각 채널은 매 틱마다 0~15 사이 값 출력. 에뮬레이터는 이를 더해(산술 평균), 0~255 범위로 스케일링, 사운드 버퍼로 내보내야 함. 틱 레이트(4.19MHz)와 사운드 출력(22kHz 등)에 맞게, 약 190틱마다 한 값 출력. 채널별 특징은 이 자료에 잘 정리됨. 1번, 2번 채널은 사각파(0/15반복), 3번 채널은 임의 파형(메모리 읽기), 4번 채널은 노이즈, LSFR 기반. 예시 코드 SoundModeX.java 참조 추천

    • 이 자료도 꽤 괜찮음

    • 이 유튜브 영상도 참고할만함

  • 정말 멋진 글과 쿨한 프로젝트라는 인상

  • 데모가 너무 빠르게 동작한다는 점이 눈에 띔. Throttle 체크박스가 별 효과 없음. 오히려 체크 해제하면 더 느려지는 느낌. Throttle 켜면 240fps, 끄면 180fps임. Throttle 켤 때 1초가 실제 에뮬레이터에서는 약 4초로 느려짐. 아마 모니터 주사율이 240Hz인 점과 연관 있어 보임

    • 아마도 requestAnimationFrame()만 호출하고 deltaTime 계산이 누락된 듯함
  • 정말 아름다운 글이라고 생각함. 이런 자료 공유해줘서 고마움. Rust로 Game Boy 에뮬레이터를 직접 만들어보고 싶어졌고, 블로그 글이 큰 영감이 되었기에 북마크해둠

  • 정말 멋지게 functor랑 GADT 쓰는 예시임. CHIP 8이나 NES 에뮬레이터와 비교해보고 싶고, CAMLBOY를 ocaml-wasm으로 WASM에 포팅해보는 것도 흥미로울 것 같음

    • js_of_ocaml의 새 WASM 백엔드(wasm_of_ocaml)가 있어서, 이미 CAMLBOY를 WASM에서 돌릴 수 있을 것임