Skip to content

macOS에서 Xbox 360 무선 컨트롤러 되살리기 — DriverKit 드라이버 개발기

오래된 서랍 속 Xbox 360 무선 컨트롤러와 클론 USB 리시버를, 최신 macOS(Tahoe)에서 게임 패드로 되살린 과정입니다. 커널 확장(kext)이 사라진 시대에 DriverKit로 밑바닥부터 만들면서 겪은 삽질과, 막힐 때마다 Codex를 붙여 뚫은 이야기를 정리했습니다.


TL;DR

  • 목표: 클론 Xbox 360 무선 리시버(USB 045E:02A9)를 시스템 전역 게임패드로 사용
  • 스택: DriverKit(.dext) — USBDriverKit로 리시버 제어 + 가상 HID 게임패드 게시
  • 가장 큰 벽: Apple GameController.framework가 우리 패드의 입력을 안 받음
  • 결정적 해결: macOS 내부의 게임패드 매핑 설정 DB를 리버스 → 등록된 실제 컨트롤러로 “위장”
  • 결과: SDL/에뮬레이터 + GameController 기반 상용 게임까지 정상 동작

1. 왜, 그리고 무엇을 만들었나

macOS는 보안 강화로 커널 확장(kext)을 사실상 폐기하고 DriverKit(유저스페이스 드라이버, .dext)로 옮겨갔습니다. 과거의 명작 360Controller kext는 최신 macOS에서 동작하지 않습니다. 그래서 목표를 이렇게 잡았습니다.

“리시버에서 입력을 읽어, macOS가 표준 게임 패드로 인식하는 가상 HID 장치를 게시하는 DriverKit 드라이버를 만든다.”

구성은 두 조각입니다.

  • 컨테이너 앱(Swift): 드라이버 설치·활성화 요청, 연결 로그 표시
  • dext(C++): USB 리시버 제어 + 가상 HID 게임패드
placeholder-architecture

2. 난관 ① USB 매칭 — 인터페이스가 안 보인다

클론 리시버는 꽂아도 데이터 인터페이스(FF/5D/81)를 미리 노출하지 않습니다. 그래서 매칭을 2단계로 설계했습니다.

  1. 장치 레벨 매칭(IOUSBHostDevice, idVendor/idProduct) → SetConfiguration()로 인터페이스를 깨움
  2. 깨어난 인터페이스 레벨(FF/5D/81) 재매칭 → 인터럽트 IN/OUT 파이프 탐색 후 읽기 시작

프로토콜은 Linux 커널 xpad.c를 참고했습니다. 연결 패킷(08 …), 입력 리포트(00 01 .. F0 뒤에 버튼/스틱/트리거), LED 링 명령 정도가 핵심입니다.

입력 패킷 예: 00 01 00 F0 00 13 [buttons1][buttons2][LT][RT][LX LX][LY LY][RX RX][RY RY]

3. 난관 ② “Exec format error” — 서명·엔타이틀먼트 지옥

dext를 활성화하니 로드가 실패하며 Exec format error(POSIX 8) 만 떨어졌습니다. 메시지가 불친절해 원인을 좁히기 어려웠습니다.

  • 처음엔 링커의 chained fixups 문제로 의심(→ 헛다리)
  • 진짜 원인은 kernel/amfid 로그의 Unsatisfied Entitlements: com.apple.developer.driverkit.transport.usb 엔타이틀먼트 값이 프로비저닝 프로파일의 와일드카드와 불일치
# 함정: 쉘 별칭 때문에 그냥 `log`는 빈 결과. 전체 경로로 봐야 한다.
/usr/bin/log show --last 5m --predicate 'eventMessage CONTAINS "Entitlements"'

해결:

  • 엔타이틀먼트의 transport.usb를 프로파일과 동일하게 와일드카드로 맞춤
  • 개발 서명 dext는 SIP 비활성화 + 시스템 확장 개발자 모드가 필요
    • 복구 모드(1TR) → csrutil disable → 재부팅 → systemextensionsctl developer on
placeholder-entitlements-log

💡 교훈: DriverKit 로드 실패는 대부분 “코드”가 아니라 서명·엔타이틀먼트·프로파일 3자 불일치입니다. 그리고 로그는 꼭 /usr/bin/log 전체 경로로 보세요.


4. 난관 ③ 가상 HID 장치 게시

USB는 읽히는데, 정작 가상 HID 게임패드를 만드는 IOService::Create가 계속 실패했습니다(BadArgument, Unsupported). 여러 IOClass 조합을 헤매다, Karabiner의 가상 HID 구현 패턴에서 답을 찾았습니다.

  • Info.plist에 중첩 dict(Xbox360HIDDeviceProperties)를 두고
  • IOClass = AppleUserHIDDevice, IOUserClass = 우리 커스텀 클래스
  • 드라이버에서 그 dict 이름으로 Create()

이 조합으로 마침내 HID 게임패드가 시스템에 등장했습니다.


5. 난관 ④ 입력이 “안 흐른다” — 문제를 반으로 가르기

패드는 떴고 macOS도 GCExtendedGamepad 4개로 인식하는데, 버튼을 눌러도 게임 컨트롤러 입력이 0건이었습니다. 원인 후보가 너무 많았습니다(파싱? 전달? 매핑?).

그래서 계층을 분리해 진단했습니다. IOHIDManager로 raw HID를 직접 모니터링한 것이죠.

값변경 usagePage=0x09 usage=0x01 value=1   # A 버튼
값변경 usagePage=0x01 usage=0x30 value=-6588 # 왼쪽 스틱 X
… (총 1134건)

raw HID는 완벽했습니다. 즉 USB→파싱→HID 전달까지는 정상이고, 문제는 오직 Apple GameController.framework의 매핑 계층 하나로 좁혀졌습니다. (참고로 이 raw HID만으로도 SDL 기반 게임·에뮬레이터 상당수는 이미 동작합니다.)

placeholder-hid-vs-gc

6. 돌파구 — macOS GameController 매핑을 리버스하다

여기서부터가 하이라이트입니다. 왜 인식은 되는데 입력이 안 올까?

  • VID를 Xbox(0x045E) 로 하면 → macOS의 전용 Xbox 플러그인(/System/Library/HIDPlugins/ServicePlugins/XboxOneHIDServicePlugin)이 가로채 GIP 포맷을 기대 → 우리 커스텀 리포트와 불일치 → 입력 0
  • 제네릭 VID로 하면 → 매칭되는 설정이 없어 → 아예 인식 안 됨

핵심은 macOS가 게임패드를 디스크립터로 범용 파싱하지 않는다는 점이었습니다. 대신 MobileAsset 설정 DB를 씁니다.

/System/Library/AssetsV2/…_GameController_DB1/…/GameControllers-Custom.bundle
  Info.plist  →  Devices[]: { IOPropertyMatch:{VendorID,ProductID,VersionNumber} → Personality }
  Personalities/
<Vendor>/<Model>.plist
      button.a → "UsageType == 1 AND UsageTypeIndex == 0"   # 버튼: 선언 순서 인덱스
      thumbstick.left.x → "UsageType == 2 AND UsageTypeIndex == 0"  # 축: 선언 순서

즉 macOS는 (VID, PID, Version) 으로 등록된 컨트롤러를 찾고, 그 personality가 정의한 “타입별 선언 순서” 로 HID 엘리먼트를 매핑합니다(SDL 시맨틱).

해결책: 우리 가상 패드를 DB에 등록된 실제 컨트롤러로 위장합니다.

  • VID/PID/Version = PowerA MOGA XP7-X+ (0x20D6:0x8909, ver 0x0100)
  • HID 디스크립터의 버튼·축·hat 선언 순서를 그 personality 인덱스에 정확히 맞춤

빌드해서 다시 눌러보니 —

[1] A=1 B=0 LX=+0.25 LY=-0.63 LT=0.11   … 입력 1916건

GameController.framework까지 완전 연동 성공. 🎉

placeholder-gc-success

💡 교훈: “인식은 되는데 입력이 없다”는 macOS 게임패드 문제의 90%는 VID/PID가 기대하는 리포트 포맷 불일치입니다. Xbox VID는 전용 플러그인으로 빨려가니, 오히려 일반 서드파티 컨트롤러로 위장하는 편이 깔끔합니다.


7. 실전 — 상용 게임 적용과 마무리 디테일

실제 게임(GameController.framework 사용)에 붙였더니 작동하지 않았습니다. 검증기에선 됐는데 게임에선 왜?

  • 원인: 리시버의 4개 슬롯이 각각 패드를 게시 → 가상 패드가 4개. 게임은 보통 첫 번째 패드(빈 슬롯) 를 잡아 입력이 없음
  • 수정: 연결된 슬롯만 HID 패드를 게시(연결 시 생성, 해제 시 제거) → 컨트롤러 1대 = 패드 1개

이후 남은 자잘한 디테일도 정리했습니다.

  • 왼쪽 스틱 상하 반전: Xbox 하드웨어는 “위 = 양수”, macOS 매핑은 HID 관례(아래 = 양수) → 스틱 Y 부호 반전
  • LED 깜박임: 연결 직후엔 컨트롤러가 LED 명령을 놓침 → 입력이 흐르기 시작할 때 1번 LED 명령 재전송으로 고정
placeholder-ingame

8. Codex는 어떻게 썼나

막히는 구간마다 Codex(rescue 서브에이전트) 를 붙였습니다. 특히 효과적이었던 지점:

  • 깊은 근본 원인 조사: “Exec format error”의 방향 탐색, macOS 내부 플러그인/설정 바이너리 분석 위임
  • 병렬 리서치: 내가 통합·테스트를 준비하는 동안 Codex가 자료를 파고들게 함

다만 배운 점도 있습니다.

  • Codex의 첫 진단(chained fixups)은 틀렸고, 진짜 원인은 로그에 있었습니다 → AI 진단도 로그로 검증해야 합니다
  • 하드웨어 의존(실기기 버튼 입력) 검증은 위임이 안 되므로 결정적 확인은 직접 수행
  • 한 번에 한 걸음씩 도는 구간에선, 핵심 분석은 내가 직접 파는 게 빨랐습니다

정리하면, Codex는 넓게 탐색·조사하는 데 강하고, 좁혀서 검증·판정하는 건 사람이 잡아야 균형이 맞았습니다.

placeholder-codex

9. 마치며 — 얻은 것들

  • 오래된 Xbox 360 컨트롤러가 최신 macOS에서 상용 게임 패드로 부활
  • DriverKit 서명/엔타이틀먼트의 실전 함정 지도를 얻음
  • 무엇보다 macOS GameController가 게임패드를 매핑하는 실제 메커니즘(설정 DB + VID/PID + 엘리먼트 선언순서)을 손에 넣음

가장 중요한 교훈은 하나였습니다.

“문제를 계층으로 가르면, 불가능해 보이던 버그가 한 줄짜리 원인으로 좁혀진다.” raw HID와 GameController를 분리한 순간, 3주짜리 미스터리가 “리포트 포맷 불일치” 한 줄로 정리됐습니다.


본 글의 코드/드라이버는 개인 학습·연구 목적입니다. Xbox, PowerA 등은 각 사의 상표이며, VID/PID 위장은 macOS 매핑 연동을 위한 기술적 방편입니다.