요약
바뀐 부분만 큰 것부터 다시 그리는 방식으로 1981년 IBM PC에서 그래픽 모드 영상을 재생했습니다.
데모 제작자 트릭스터(짐 레너드)가 2014년에 쓴 기술 회고 1편입니다. 그는 2007년 강연에서 CGA 그래픽 모드로는 영상을 재생할 수 없다고 했고, 이 글에서 그 말을 스스로 뒤집은 과정을 설명합니다.
왜 중요한가
- 매 프레임 화면 전체를 바꿔야 한다는 전제를 버리자 불가능하다던 계산이 달라졌습니다.
- 한계를 고정된 바이트 수가 아니라 프레임당 쓸 수 있는 시간으로 다시 정의했습니다.
- 8088이 실시간으로 내릴 수 없는 판단을 모두 인코딩 단계로 옮긴 점이 핵심입니다.
핵심 내용
- 24fps로 16KB 화면을 통째로 바꾸려면 초당 384KB가 필요해 CGA 쓰기 속도와 디스크 속도를 모두 넘습니다.
- 바뀐 연속 바이트 묶음(델타)을 길이순으로 정렬해 큰 것부터 쓰고, 예산이 모자라면 작은 변화만 버립니다.
- 같은 값이 이어지는 묶음은 REP STOSB(메모리를 한 값으로 빠르게 채우는 명령)로 채우고 값끼리 모아 처리합니다.
- 컴포지트 출력에서 제대로 보이는 160×200 16색 모드와 640×200 2색 모드를 골랐습니다.
- 픽셀 하나의 변화가 화면 전체로 번지는 오차 확산 대신 규칙적인 패턴 디더링을 썼습니다.
HN 반응
- 당시 기계를 써 본 이용자들은 dir 결과가 한 글자씩 찍히던 기억을 떠올리며 감탄했고, ISA 버스 대역폭에 대한 보충 설명이 이어졌습니다.
- 한 이용자는 2편에서 델타 합치기를 NP-난해(효율적 풀이가 알려지지 않은 문제)라고 한 것은 틀렸고 동적 계획법으로 풀린다고 지적했습니다.
이 기계들을 직접 써 본 사람으로서, 정말 믿기지 않는 일입니다.
dir을 치면 글자가 화면에 나타나는 모습이 말 그대로 눈에 보였던 게 기억나요. 영상? 색깔? 농담이시죠?DOS와 BIOS는 텍스트 출력이 유난히 느렸습니다. 기본적으로 글자 하나가 DOS를 거쳐 BIOS까지 갔다가 되돌아오는 방식이었습니다.
텍스트 모드 세그먼트(제 기억이 맞다면 0xb800)에 직접 접근하면 훨씬 빨랐습니다. 글꼴은 매 주사선마다 하드웨어가 알아서 그려 주니, 수천 바이트 남짓한 비디오 메모리를 초당 수십만 바이트 속도로 바꾸기만 하면 됐습니다.
원래의 IBM PC 5150에서 ISA 버스는 이론상 초당 9.5메가바이트까지 낼 수 있었을 텐데, 프로세서가 그 근처에도 못 갔을 거라고 생각합니다. 그래도 초당 1메가바이트는 넘겼다고 해도 놀랍지 않습니다. 그러면 4000바이트짜리 80×25 화면 하나를 4밀리초에 다시 그릴 수 있다는 뜻입니다. 다만 호환 기종의 CGA는 그렇게 하면 화면에 눈(snow)이 내렸기 때문에, 갱신을 HBI와 VBI 구간으로 제한해야 했고 그만큼 갱신 속도가 눈에 띄게 느려졌습니다. IBM의 실제 하드웨어는 듀얼 포트 VRAM을 써서 이 문제를 피했습니다.
8비트 ISA 버스의 이론상 최대 속도는 1.19MB/s였습니다. 버스 구조상 전송 한 번에 항상 4사이클이 걸렸고(4사이클 상태 기계를 가진 8288이 이를 강제했습니다. 제 기억으론 주소에 한 사이클, 데이터에 한 사이클, 나머지 두 사이클은 대기 상태였습니다), 전송 한 번에 8비트씩, 4.77MHz로 동작했습니다.
실제로 쓸 수 있는 대역폭은 언제나 훨씬 낮아서, 버스를 어떻게 쓰느냐에 따라 80kB/s에서 380kB/s를 조금 넘는 수준이었습니다. CPU가 같은 버스에서 명령어를 가져와야 했기 때문입니다. 그래도 버스는 대체로 늘 1.19MB/s로 돌고 있었다고 할 수 있습니다. 기본 장치(ROM, RAM, 흔한 단순 ISA 카드)가 모두 기본 4사이클 전송에 항상 응답할 수 있었으니까요. 다만 대부분의 사이클이 실제 데이터가 아니라 명령어를 가져오는 데 쓰였을 뿐입니다.
이후 기종들은 버스 폭을 넓히고 클럭을 올려 버스를 빠르게 만들었습니다. AT에서는 필수 대기 상태 하나를 없애 이론상 5.55MB/s가 되었고, 빠른 호환 기종에서는 마지막 대기 상태까지 없애 이론상 8.33MB/s가 되었습니다.
감사합니다. 메인보드의 RAM 칩이 실제 ISA 버스에 물리적으로 연결돼 있었다는 건 미처 몰랐습니다! 이쪽으로는 저보다 훨씬 많이 아시는 것 같네요.
흥미롭네요, 감사합니다! 궁금한 게 있는데, LODSW/STOSW/MOVSW에 REP와 REPcc 접두사를 붙이면 첫 번째 반복 이후로는 명령어를 해독할 필요가 없으니 전송률을 더 높일 수 있지 않았을까요?
IBM의 BIOS는 자사 CGA 하드웨어가 싱글 포트 RAM이라 눈(snow)이 생겼기 때문에, 스크롤하는 동안 화면을 꺼 버렸습니다. 나중에 나온 호환 기종이 대체로 더 나았지, 그 반대가 아니었습니다.
제가 틀렸을 수도 있습니다. 알려 주셔서 감사합니다. 참고로 제가 쓰던, 눈이 내리던 CGA는 호환 기종이었습니다.
저도요. 그러다 XT에서 AT(8086에서 286)로 바꿨더니 디스크도 빨라져서, 어쩔 수 없이 /p 매개변수를 익혀야 했죠 :)
이런 글이 올라오는 건 언제 봐도 멋집니다. 영상을 x86 기계어 명령으로 "인코딩"한다는 발상이 정말 마음에 듭니다!
사소한 것까지 따지는 제 안의 너드가 참지 못하고 짚자면, 2부 글에서 델타를 합치는 문제가 NP-hard라고 한 주장은 사실이 아닙니다. 사실 이 문제는 DP로 O(n^3) 시간에 최적으로 풀 수 있습니다. g(i, j)를 입력 델타 i..j를 하나의(즉, 합쳐진) 출력 델타로 인코딩하는 데 필요한 최소 바이트 수라고 합시다. g(i, j)를 알면 g(i, j+1)을 계산하는 데는 상수 시간이 들므로, 이 함수의 모든 값을 총 O(n^2) 시간에 계산할 수 있습니다. 이제 f(i, j)를 입력 델타 i..j를 (합쳤든 아니든) 어떤 출력 델타 열로든 인코딩하는 데 필요한 최소 바이트 수라고 합시다. 그런 열은 반드시 (0개일 수도 있는) 몇 개의 최적으로 인코딩된 출력 델타 뒤에, 입력 델타 j에서 끝나는 최적으로 인코딩된 출력 델타 하나가 오는 꼴입니다. 즉 어떤 k에 대해 f(i, j)는 f(i, k) + g(k+1, j)와 같아야 하고, 시도해 볼 k는 많아야 n개이므로 f()의 각 값을 O(n)에 계산할 수 있습니다. 계산할 값이 O(n^2)개이니 전체로는 O(n^3) 시간입니다. 그러면 f(1, n)이 입력 델타 전체를 인코딩하는 데 필요한 최소 바이트 수이고, DP 행렬을 표준적인 방식으로 역추적하면 실제 출력 델타 열을 복원할 수 있습니다.
정말 믿기지 않는 대목은 하드웨어의 제약과 소프트웨어를 견주어 볼 때입니다.
([2014])
더 문명화된 시절이었죠
말 그대로요.
순수한 비전(祕傳)의 마법이라 할 만합니다. 저자에게 경의를 표합니다.