Z80을 한계까지 밀어붙이기: ZX Spectrum에서 실시간 3D 렌더링
많은 개발자에게 ZX Spectrum은 1980년대의 향수를 불러일으키는 유물—Z80 CPU와 겨우 48K RAM을 가진 기계입니다. 그러나 수학과 저수준 엔지니어링의 교차점에 관심이 있는 사람들에게는 최적화를 위한 매력적인 놀이터로 남아 있습니다. 이렇게 제한된 하드웨어에서 실시간 3D 그래픽을 구현하는 도전은 단순히 향수의 문제가 아니라, 프로세서의 모든 사이클을 짜내는 마스터클래스입니다.
Thanassis (ttsiodras)의 최근 프로젝트에서 3D 포인트 렌더러가 ZX Spectrum 48K로 포팅되었습니다. 이 프로젝트는 고수준 C 코드에서 고도로 최적화된 Z80 어셈블리로의 여정을 보여주며, 개발자가 하드웨어 레지스터를 직접 제어할 때 나타나는 뚜렷한 성능 차이를 드러냅니다.
성능 차이: C vs. 어셈블리
Z80에서 3D 투영을 구현하는 것은 본질적으로 어렵습니다. 프로세서는 부동소수점 지원이 없고 레지스터가 제한적이기 때문입니다. 초기 구현은 z88dk 크로스 컴파일러를 사용해 C로 작성되었습니다. 기능은 했지만, C 버전은 플레이 가능한 프레임 레이트를 유지하는 데 어려움을 겪었으며, **6.2 프레임 per 초 (fps)**에 불과했습니다.
이를 개선하기 위해 저자는 손으로 작성한 Z80 어셈블리로 전환했습니다. 범용 C 컴파일러보다 Z80 레지스터를 더 효율적으로 활용하고, 비용이 많이 드는 나눗셈 연산을 역수 조회 테이블을 이용한 곱셈으로 대체함으로써 성능이 14.0 fps로 급상승했습니다.
더 높은 속도를 원하는 사람들을 위해 사전 계산 버전이 개발되었습니다. 목표 픽셀과 비디오 RAM 위치를 미리 계산함으로써 렌더러는 놀라운 40 fps에 도달했습니다. 이는 레트로 컴퓨팅에서 반복되는 주제를 강조합니다: 런타임 계산이 너무 비싸면 작업을 빌드 파이프라인으로 옮기세요.
제한된 하드웨어를 위한 수학적 최적화
3.5MHz 프로세서에서 3D 렌더링을 실현 가능하게 만들기 위해, 저자는 투영 방정식을 단순화하는 여러 영리한 수학적 지름길을 사용했습니다.
시점 궤도 돌리기
3D 모델을 회전시키는 대신(복잡한 행렬 곱셈이 필요함), 저자는 시점을 모델 주위로 궤도 돌리도록 로직을 변경했습니다. 이는 런타임 방정식을 가장 기본적인 형태로 단순화했습니다:
int wxnew = points[i][0] - mcos;
int x = 128 + ((points[i][1] + msin) / wxnew);
int y = 96 - (points[i][2] / wxnew);
이 접근법은 곱셈과 시프트를 없애고, 두 번의 나눗셈과 기본적인 덧셈/뺄셈만을 사용합니다.
빌드 파이프라인 및 사전 스케일링
Z80은 부동소수점을 처리할 수 없기 때문에, 모든 원본 데이터(원래는 Python에 있음)는 $S = 8960$의 계수로 사전 스케일링되어 정수로 변환됩니다. 빌드 파이프라인은 또한 축 교환을 수행합니다([X, Y, Z]를 [X, Z, Y]로 변경). 이를 통해 렌더러는 먼저 깊이와 화면 Y를 계산할 수 있으며, 점이 수직 범위를 벗어나면 화면 X 계산을 완전히 건너뛰어 소중한 CPU 사이클을 절약합니다.
역수 조회 테이블
나눗셈은 Z80에서 가장 비용이 많이 드는 연산 중 하나입니다. 내부 루프를 최적화하기 위해 저자는 "페이지 기반" 조회를 활용했습니다. 테이블 오프셋의 고바이트를 H 레지스터에, 인덱스를 L 레지스터에 로드하면 CPU는 (HL)에서 역수 값을 거의 즉시 읽어들일 수 있어, 나눗셈을 곱셈으로 전환합니다.
커뮤니티의 기술적 통찰
이 프로젝트는 레트로 프로그래밍 애호가들 사이에서 어셈블리와 현대 도구의 본질에 대한 논의를 촉발했습니다. 한 기여자 @flohofwoe는 80년대 개발의 인식된 "느림"이 반드시 어셈블리 언어 자체 때문이 아니라 현대 도구의 부재 때문이라고 지적했습니다. 현대 IDE, 매크로 어셈블러, 빠른 에뮬레이터 디버그 루프를 사용하면, 개발자가 수동 데이터 레이아웃과 서브루틴 호출을 주요 추상화로 편안히 다룰 수 있을 때 어셈블리 개발은 고수준 언어만큼 생산적일 수 있습니다.
@Dwedit가 공유한 또 다른 최적화 팁은 IX와 IY 레지스터를 드물게 사용하여 Z80을 Game Boy CPU처럼 다루는 것입니다. 이 레지스터들은 다른 레지스터 쌍보다 느리기 때문입니다. 대신 256바이트 정렬된 테이블을 사용하면 개발자는 레지스터 쌍의 저바이트를 인덱싱에, 고바이트를 테이블 베이스에 사용할 수 있어 메모리 접근을 더욱 효율화합니다.
결론
3D 포인트 렌더러를 ZX Spectrum에 포팅한 것은 하드웨어 제약이 종종 가장 창의적인 엔지니어링을 이끌어낸다는 점을 상기시켜 줍니다. 계산을 빌드 파이프라인으로 옮기고, 시점 궤도 돌리기를 통해 기하학을 단순화하며, 손으로 최적화한 어셈블리를 통해 C 컴파일러의 한계를 우회함으로써, 원래 설계되지 않은 하드웨어에서도 부드러운 3D 시각 효과를 구현할 수 있습니다.