A trip through the Graphics Pipeline 2011 요약정리와 번역 중간 어디쯤(…) – Part 2

(이하는 A trip through the Graphics Pipeline 2011, part 2 를 번역한 내용임돠)

Part 2 – GPU 메모리 구조와 커맨드 프로세서

Not so fast

이 전 글에서 3D 렌더링 명령들이 GPU 에 도달하기까지 PC에서 거쳐가는 다양한 단계들에 대해 설명했슴다. 한 줄로 요약하자면 ‘님이 생각한거보다 더 복잡함’ 이져. 그리고는 커맨드 프로세서를 소개하고, 그것이 우리가 꼼꼼하게 준비한 커맨드 버퍼와 실질적으로 어떤 관련이 있는지 설명했구여. 음..어케 말해야되지.. 실은 거짓말을 했슴다. 실제로는 커맨드 프로세서를 이 글에서 처음 만나게 될 거예여. 이 커맨드 버퍼 어쩌구 하는 것은 모두 메모리를 통과한다는 사실을 명심하세여. 그 메모리는 PCI 익스프레스 버스를 통해 접근하는 시스템 메모리든, 로컬 비디오 메모리든 어쨌든 메모리임다. 그래픽스 파이프라인을 차례대로 살펴보기로 한 만큼, 커맨드 프로세서에 대해 알아보기 전에 이 메모리에 대해 먼저 잠깐 살펴보도록 하져.

메모리 서브시스템

GPU 는 범용의 CPU 나 다른 하드웨어에서 볼 수 있는 메모리 서브시스템과는 다른 형태의 메모리 서브시스템을 가지고 있슴다. 왜냐면 사용 패턴이 매우 다르기 때문이져. GPU의 메모리 서브시스템은 일반적인 머신에서 볼 수 있는 것과는 근본적으로 두 가지 면에서 차이가 남돠.

첫째로 GPU 의 메모리 서브시스템은 빠릅니다. 엄청 빠르져. Core i7 2600K 의 메모리 대역폭은 19GB/s 정도를 찍을 거예여. 날씨가 좋고 미풍이 불 때 다운힐을 한다면 말이져(…). 한편 GeForce GTX 480의 경우, 총 메모리 대역폭은 180GB/s 에 근접함다. 거의 한 자릿수가 차이나네여? 헐.

두번째는 GPU 메모리 서브시스템은 느립니다. 엄청 느려여. AnandTech에서 제공한 메모리 지연시간 자료에 클럭속도를 곱하면, 네할렘(1세대 Core i7) CPU에서 캐시 미스가 발생한 경우 약 140사이클 정도가 지연되는 걸 알 수 있슴다. 방금 언급한 GeForce GTX 480 의 경우 400에서 800사이클이 지연되져.(주 : 원문의 링크가 깨져서 같은 문서인 걸로 보이는 것을 찾아서 연결했는데, 원래 링크가 살아있을 때 본 적이 없어서 맞는지는 모르겠네여. 내용을 보니 맞는 거 같긴 한데…) 따라서 사이클 단위로 얘기한다면, GeForce GTX 480 은 Core i7 에 비해 4배가 조금 넘는 메모리 지연 시간을 가지고 있다..라고 할 수 있겠네여. 근데 방금 얘기한 Core i7 은 2.93GHz 로 동작하는 한편, GTX 480 의 셰이더 클럭은 1.4GHz 로 동작하니까.. 여기서 다시 2를 더 곱해줘야겠네여. 헐. 이거 또 한 자릿수 차이네여? 뭔가 희한한 일이 벌어지고 있군여. 이거 보아하니.. 뉴스에서 계속 떠들어대던 그 트레이드오프 라는 거 중에 하나구만!

넹 그렇슴다. GPU 는 대역폭을 엄청나게 증가시키는 대가로 엄청난 지연 시간을 지불한 거져(그리고 결과적으로 상당한 수준의 전력을 소비하게 되었지만, 이건 이 글의 범위를 벗어나니까). GPU 는 지연시간보다 처리량을 우선한다, 이게 일반적인 패턴이라는 것임다. 다시 말해 어떤 작업을 했는데 아직 결과가 안 나왔다면, 그걸 기다리고 있을 게 아니라 뭔가 다른 걸 한다! 라는 거져.

이게 바로 GPU 메모리에 대해 알아야 할 거의 대부분임다. 여기에 한 가지 더. DRAM에 대한 일반적인 내용인데, 이게 또 앞으로 계속 중요할거예여. DRAM 은 2D 격자 구조임다. 물리적으로든 논리적으로든 둘 다 그렇져. (가로) 행과 (세로) 열이 있어여. 이러한 행과 열이 교차하는 지점에는 트랜지스터와 커패시터가 있슴다. 여기서 이런 재료를 가지고 실제 메모리를 구성하는 방법이 궁금하다! 그러면 위키백과가 여러분의 친구가 되어줄거예여. 어쨌거나, 여기서 주목해야 할 부분은 DRAM 내부의 위치를 나타내는 주소는 행 주소와 열 주소로 나뉘어진다는 것이져. 그리고 DRAM 은 내부적으로 한 번에 읽기 쓰기를 할 때 항상 주어진 행의 모든 열에 접근을 한다는 것두여. 이것은 같은 분량의 접근이라 해도 DRAM 의 한 줄 내에서 다 이뤄질때가 여러 줄에 나뉘어 접근하는 경우보다 훨씬 싸게 먹힌다는 것을 의미하져. 지금은 이게 DRAM 에 관한 잡다한 정보들 중 하나인것 처럼 보이지만 앞으로 계속 더 중요해질검돠. 명심하셈. 시험에 꼭 나오는 거예여. 이러한 사실을 이 전 문단의 내용과 엮어 얘기해 보자면, 몇몇 바이트씩 메모리의 여기저기서 읽어대는것으로는 위에서 얘기한 메모리 대역폭 최고치에 도달할 수 없다 라는 것이져. 메모리 대역폭을 꽉 채우려면, 한 번 읽을 때 DRAM 한 줄을 완전히 채워서 읽어야 한다 이말임다.

PCIe 호스트 인터페이스

그래픽스 프로그래머 입장에서 하드웨어의 이 부분은 그닥 흥미롭지가 않슴다. 사실 GPU 하드웨어 구조도 마찬가지져. 하집만 중요한 건 얘가 너무 느려서 정체구간이 되기 때문에 언젠가 신경을 쓸 수 밖엔 없다는  사실이져. 따라서 님이 해야만 하는 건 이걸 잘 처리할 수 있는 능력자들을 구해서 이런 일이 일어나지 않게 하는거져(…). 그렇지 않다면 CPU 가 비디오 메모리나 여러 GPU 레지스터를 를 읽고 쓰는 일이라던가 GPU 가 메인 메모리(의 일부)를 읽고 쓰는 일이 이 PCIe 호스트 인터페이스를 통해서 일어나야 하는데, 이게 엄청 느려서 모두가 골치가 아플 거란 말이져. 얘가 느리기로는 메모리 지연보다도 더 심한데, 그게 신호가 칩에서 일단 나온 담에 슬롯으로 갔다가 메인보드 일부를 거쳐서 CPU어딘가에 도착하는데 한 1주일은 걸린단 말이져(아님 뭐 CPU/GPU 속도에 비해서 그만큼 느리게 느껴진다..라는 것이거나). 대역폭 자체는 그렇게 나쁘지 않슴다. 요새 GPU 대부분의 총 대역폭은 최대 8GB/s 로, 이는 CPU 총 대역폭의 절반에서 삼분의 일 정도로 쓸만한 수준임다.  그리고 옛날 표준이었던 AGP 가 CPU 에서 GPU 로의 채널은 고속인데 비해 반대 방향은 그렇지 못했던 것과 달리, PCIe 는 CPU와 GPU 간에 대칭형 연결 구조로 동작하져.

Some final memory bits and pieces

이제 3D 커맨드를 볼 수 있는 곳까지 정말 아주 가까이 왔슴다. 너무 가까이 와서 거의 맛을 볼 수 있을 정도져(주 : 이양반 드립 보소…). 하지만 여길 벗어나서 3D  커맨드로 가기위해서는 선택을 내려야 하는 것이 한 가지 더 있슴다. 바로 메모리에 대한 것인데, 메모리에는 (로컬) 비디오 메모리와 매핑된 시스템 메모리 두 종류가 있기 때문이져. 하나는 북쪽을 향해 하루 정도 가야 하고, 하나는 PCI Express 고속도로를 타고 1주일 정도 남쪽으로 가야 함돠(주 : 노스브리지와 사우스브리지 칩셋에 대한 비유인듯). 어느 쪽을 택해야할까여?

쉬운 해결책은 어디로 갈 지 알려주는 별도의 어드레스 라인을 추가해주는것임다. 간단하고 잘 동작하면서 많이 사용되었던 방법이져. 아니면 몇몇 콘솔에서처럼(하지만 PC는 해당사항이 없는) 단일 메모리 구조(Unified Memory Architecture) 로 가는 방법도 있슴다. 이 경우에는 ‘어느 쪽’으로 가느냐의 선택지가 없슴다. 그냥 ‘메모리’져. 뭔가 좀 더 근사한 걸 원한다면 MMU(메모리 관리 유닛, Memory Management Unit)을 추가하는 방법이 있슴다. 이렇게 하면 주소 공간을 완전히 가상화해서 빈번하게 접근이 일어나는 텍스처 부분들은 비디오 메모리(얘네는 빠르져)에 넣어두고, 다른 부분들은 시스템 메모리에, 그리고 나머지 대부분은 메모리에 아예 매핑하지도 않고 갑자기 짠 하고 나타나게 한다던가(주 : …) 아니면 좀 더 흔하게는 디스크에서 읽어들이는 마법을 부리게 할 수 있슴다. 근데 이 디스크에서 읽어들이는 게 50년 쯤 걸리는게 문제져. “메모리 접근 = 하루” 라는 비유를 기준으로 하면 HD(주 : 하드디스크인듯)에서 읽어들이는 건 정말로 이 정도 걸림돠. 제법 빠른 하드일 경우에여. 엿같져. 근데 옆길로 좀 샜네여.

그럼 MMU를 보도록 하져. 얘는 비디오메모리가 거의 다 썼을  때 실제로 데이터를 카피한다던가 하는 작업 없이도 비디오 메모리 주소 공간의 단편화를 제거할 수 있게 해 줌다. 짱이네염. 그리고 여러 프로세스가 하나의 GPU를 공유하는것을 훨씬 쉽게 만들어주기도 하구여. 이걸 하나 이용할 수 있긴 한데, 반드시 있어야한다고 규정되어있는지는 확실치가 않네여. 있으면 확실히 좋은 건 분명한데 말이져 (이 부분에 대해서 누구 좀 도와주실 분? 여기에 대해서 확실히 알게 되면 이 글을 업데이트할텐데, 지금은 솔직히 이걸 뒤져 볼 기분이 아니라서염(…)). 어쨌든, MMU/가상메모리는 캐시와 메모리 일관성consistency을 고려하지 않는 구조에서라고 해도 간단히 갖다 붙일 수 있을 만한 것이라고 할 수는 없슴다. 그런데 어떤 특정한 단계stage에 속한다고 하기도 어려운데, 어쨌든 어딘가에서 반드시 언급을 해야하긴 하기에 여기다 집어넣었슴다.

DMA엔진도 있슴다. 비싼 3D 하드웨어/셰이더 코어을 끌어들이지 않고 메모리를 이리저리 복사할 수 있져. 보통은 적어도 시스템 메모리와 비디오 메모리 간의 (양방향) 복사를 처리함다.  종종 비디오 메모리에서 비디오 메모리로 복사가 가능하기도 하져(VRAM 단편화 제거를 해야 할 때 유용하겠져). 보통 시스템 메모리에서 시스템 메모리로 복사는 처리할 수 없슴다. 메모리 복사 유닛이 아니라 GPU이기 때문이져. 시스템 메모리간 복사는 PCIe 버스를 두 번이나 통과할 필요가 없는 CPU에서 처리해야져.

업데이트 : 그림을 하나 그렸슴다. (그림을 집어넣기엔 레이아웃이 너무 좁아서 링크로 처리함다.) (…라고 저자가 썼으나 그냥 바로 그림 집어넣음ㅇㅇ 여긴 공간이 될거 같아서염 : 주)

이 그림에서 몇 가지 세부사항을 더 확인할 수 있슴다. GPU에는 여러 개의 메모리 컨트롤러가 달려 있고, 각각의 컨트롤러에는 여러 개의 메모리 뱅크를 제어하져. 그리고 이들의 앞에는 대역폭이 얼마나 필요하든 처리할 수 있을만큼 충분한 메모리 허브가 있슴다.

여기서 정리를 한 번 해 보져. CPU 단에서는 커맨드 버퍼가 준비되었슴다. 그리고 PCIe 호스트 인터페이스가 있는데, 이걸 통해서 CPU는 커맨드 버퍼에 대해 알려주고 그 주소를 레지스터에 쓸 수 있슴다. 그리고 그 주소로부터 데이터를 가져오는 로직이 있는데, 이는 다음과 같이 동작하져: 비디오 메모리에 커맨드 버퍼가 있어서 시스템 메모리로부터 (커맨드 버퍼 데이터가) PCIe 를 통해 전송되어야 할 경우, KMD가 DMA 전송을 준비해서 CPU든 GPU의 셰이더 코어든 어느 쪽도 계속해서 신경 써야 할 필요가 없게 해 줌다. 그러면 비디오 메모리에 메모리 서브시스템을 통해 접근할 수 있는 사본이 마련되져. 모든 경로가 파악되었으니, 바야흐로 몇몇 커맨드에 대해 알아볼 준비가 완료되었군여!

마침내 바로 그 커맨드 프로세서!

커맨드 프로세서에 대한 얘기는 요즈음 많이 볼 수 있는 다음 단어로 시작함다.

“버퍼링…”

앞서 언급한 바와 같이, 커맨드 프로세서까지의 메모리 경로 양쪽 모두 대역폭이 넓지만 동시에 지연시간 또한 큼다. 최근의 GPU 파이프라인에 있어서 이러한 문제를 우회하는 방법은 다수의 독립적 스레드를 돌리는 것이져. 하지만 이 경우에도, 커맨드 프로세서는 단 하나이고, 이 커맨드 프로세서는 커맨드 버퍼를 순서에 맞게 처리할 필요가 있슴다(왜냐면 이 커맨드 버퍼에는 상태 변경이라던가 올바른 순서대로 처리되어야 하는 렌더링 커맨드들이 들어 있기 때문이져). 그래서 차선택을 수행함다. 충분히 큰 버퍼를 마련해서 hiccup을 피할 수 있을만큼 많은 양의 커맨드들을 미리 가져다 두는 거져.

이 버퍼에서 커맨드들은 실제 커맨드 처리 프론트엔드로 보내어지는데, 이 프론트엔드는 기본적으로 어떻게 (하드웨어 특정적인 형태의) 명령들을 파싱해야하는지 아는 상태기계져. 몇몇 명령은 2D 렌더링과 관련이 있는데, 이는 별도의 2D 커맨드 프로세서가 없을 경우임다. 별도의 2D 커맨드 프로세서가 있으면 3D 프론트엔드는 이런 명령들을 볼 수 조차 없져. 어느 쪽이든 요즘 GPU들에는 여전히 2D 전용 하드웨어가 감춰져 있슴다. 다이 어딘가에 텍스트모드라던가 픽셀당 4비트 비트플레인 모드, 부드러운 스크롤 등을 처리하는 VGA 칩이 존재하는 것과 마찬가지로 말이져. 현미경 없이 다이에서 이런 것들을 찾아내는 데에 행운이 함께하기를. 어쨌든, 이러한 요소들이 존재하지만 앞으로 얘네들에 대해 언급하지는 않을거예여. 그리고 프리미티브들을 3D/셰이더 파이프로 넘겨주는 명령들이 있슴다. 유후~! 얘네는 다음 파트에서 다룰거예여. 3D/셰이더 파이프로 가는 또다른 명령들 중에는 여러 가지 이유로 인해(그리고 여러 파이프라인 설정하에서) 아무 것도 그리지 않는 명령도 있슴다. 얘네는 더 나중에 다룰게여.

그리고 상태를 변경하는 명령들이 있져. 프로그래머라면 얘네는 단순히 어떤 변수 값을 바꾸는 거라고 생각할 거예여. 그리고 기본적으로 그렇기도 하져. 하지만 GPU 는 대규모 병렬 컴퓨터massively parallel computer 이기에, 이러한 병렬 시스템에서 전역변수의 값을 그냥 바꾸고 아무 일 없기를 기대할 수는 없어여. 어떠한 강제된 불변식invariant에 의해 모든 것이 제대로 돌아갈 것이라는 보장이 있지 않는 한, 어딘가 버그가 있을 테고 언젠가는 그게 터질테니까여. 잘 알려진 여러 방법들이 존재하는데, 기본적으로 모든 칩들은 각각의 상태들에 대해 각기 다른 방법들을 사용함다.

  •  상태를 바꿀 때 마다, 그 상태값을 참조하는 모든 작업들은 완료되어야 함다(기본적으로 파이프라인의 일부를 비우는flush 것이져). 역사적으로 이 방법이 그래픽 칩들이 대부분의 상태 변경을 처리하는 방식임다 – 간단하고 비싸지 않져. 작업batch의 갯수가 적고, 삼각형 수도 얼마 안 되고 파이프라인도 짧다면 말이져. 그런데 맙소사, 작업과 삼각형 갯수가 늘어나고 파이프라인도 길어지면서, 이런 식의 접근에 드는 비용이 엄청 치고 올라가버렸네여. 물론 다음과 같은 경우에는 여전히 이런 방식이 사용되고 있슴다 : 자주 바뀌지 않는 상태거나(수십 번 정도의 부분적인 파이프라인 비우기flush 는 한 프레임 전체에 대해서는 그닥 큰 몫을 차지하지는 않져), 특정 방식으로의 구현 결과물이 매우 고비용이거나 구현하기 까다로운 상태일 경우져.
  • 하드웨어를 완전히 상태 없이stateless 구현할 수도 있슴다. 상태 변경 명령을 해당 변경을 처리해야 하는 단계까지 내려보낸 다음, 그 단계의 이후 단계로 보내는 모든 것에 현재 상태값을 덧붙여서 매 싸이클마다 보내면 되져. 상태값은 어디에도 저장되지 않되, 어디에나 함께 하게 되는것임다. 그렇게 하면 파이프라인상의 특정 스테이지에서 어떤 상태가 무슨 값인지 필요할때마다 읽어올 수 있게 되져. 왜냐면 그 상태값은 그 단계까지 넘겨져왔기 때문임다(그리고 그 상태값은 다시 그 다음 단계로 넘겨지게 되져). 만약 상태값을 저장하는 데 몇 비트만 필요하다면, 이 방법은 제법 저렴하고 실용적임다. 그렇지만 만약 그게 샘플 상태를 포함한 사용중인 텍스쳐 전체에 대한 상태값이라고 하면, 별로 효율적이지 못할거예여.
  • 상태값을 한 벌만 저장해두고 사용할 경우 렌더링 파이프라인의 어느 단계에서 상태값을 바꿀 때마다 파이프라인을 비우느라 내보내는 데이터 양이 엄청 많을 수 있슴다. 이럴 때엔 상태값 두 벌(혹은 네 벌)을 이용하게 하면 상태를 설정하는 앞단을 좀 더 앞서나가게 할 수 있져. 각 상태들에 대한 값을 두 벌 저장하기에 충분한 레지스터(슬롯)이 있다고 가정하고, 현재 처리중인 작업이 0번 슬롯을 참조하고 있다고 해 보져. 1번 슬롯의 상태값을 변경하는 것은 (0번 슬롯을 참조하며) 처리중인 그 작업을 멈추거나 영향을 주거나 하는 것 전혀 없이 처리가 가능함다. 이제 파이프라인을 따라 상태값 전체를 보낼 필요 없이, 명령당 0번과 1번 중 어느 슬롯인지를 선택하는데 필요한 1비트만 있으면 됨다. 물론, 슬롯 0번과 1번이 모두 사용중일 때 상태 변경 명령을 만나게 되면 대기해야 할 수 밖에 없겠져. 하지만 어쨌든 조금은 나아졌슴다. 이 기법은 슬롯이 두 개 보다 많을 경우에도 잘 동작함다.
  • 샘플러나 텍스처 Shader Resource View 상태의 경우, 한 번에 엄청나게 많은 수의 상태값을 설정할 수 있지만 실제로 그렇게 사용하는 경우는 잘 없슴다. 그래서 동시에 2벌의 상태값 세트를 운용하는 경우라고 해도, 이를 위해 2*128개의 텍스처 상태값을 저장하기 위한 공간을 예약해두지는 않져. 이 경우엔 일종의 ‘레지스터 이름 변경 기법’을 사용함다. 128개의 텍스처 디스크립터를 가지고 있는 풀pool을 이용하는 것이져. 만약 어떤 셰이더가 한번에 128개의 텍스처를 동시에 설정하려 한다면, 상태값 변경은 매우 오랜 시간이 걸리게 되겠져(주 : 텍스처 값 설정을 위해 결국 모든 물리적 텍스처 디스크립터들이 비워질때까지 이를 참조하는 이전 작업들이 완료되는 것을 기다려야 하기 때문). 그렇지만 보통은 한 번에 20개 미만의 텍스처를 사용할테고, 이 정도 갯수라면 이들을 여러 벌 운용할 만큼 충분한 공간이 되는 것임다.

위 항목들은 상태변경에 있어 모든 것을 포괄하는 것은 아님다. 하지만 요지는 애플리케이션단(혹은 이 문제에 있어서는 UMD나 KMD, 혹은 명령어 버퍼도)에 있어 변수의 값을 변경하는 것과 같이 간단해 보이는 것도, 처리를 하는 데 속도저하가 일어나지 않게 하기 위해서는 무시하지 못할 만큼 많은 분량의 하드웨어 지원이 필요할 수 있다는 것임다.

동기화

마침내 마지막 항목인 CPU/GPU 와 GPU/GPU 간의 동기화 처리 명령어군이네여.

일반적으로 이들은 모두 “이벤트 X가 발생하면 Y를 해라’의 형식임다. “Y를 해라”부분부터 먼저 다뤄볼게여. 여기엔 두 가지 적합한 선택지가 있슴다. 하나는 푸시 모델 통지push-model notification이예여. GPU가 CPU에게 무언가를 당장하라고 소리치는거져(“이봐 CPU! 지금 0번 디스플레이가 수직 귀선 기간vertical blanking interval에 들어가니까 티어링tearing 없이 버퍼를 바꾸려면 지금 해야할걸!”). 다른 하나는 풀 모델pull-model로, GPU가 뭔가 일어난 일들에 대해 기억하고 있고 CPU가 나중에 그걸 물어보는 것이져(“이봐 GPU, 가장 최근에 처리를 시작한 명령 버퍼 조각이 어느거지?” – “어디보자… id 303이네.”). 전자는 보통 인터럽트를 이용해 구현되고 자주 발생하지 않지만 우선순위가 높은 이벤트를 처리하는 쓰임다. 왜냐면 인터럽트가 꽤나 비싸거든여. 후자에 대해 알아야 하는 것은 CPU가 접근 가능한 GPU레지스터와, 어떤 이벤트가 발생했을 때 명령 버퍼로부터 이 레지스터들에 값을 쓰는 것이 어떻게 이뤄지는가 임다.

이러한 레지스터 16개가 있다고 가정하져. 레지스터 0번을 currentCommandBufferSeqId 라고 이름붙이져. GPU에 접수시키는 모든 커맨드 버퍼에 일련 번호를 붙이고(이 작업은 KMD에서 이뤄지져), 각 커맨드 버퍼의 처리를 시작할 때 “커맨드 버퍼 처리가 이 곳까지 도달했을 때, 레지스터 0에 값을 쓴다”는 동작을 추가함다. 그렇게 되면 오호라, 이제 GPU가 어느 커맨드 버퍼를 처리하는지 알 수 있게 되었네여! 그리고 커맨드 프로세서는 명령을 순서대로 엄격하게 처리하니까, 만약 id 303 번 커맨드 버퍼의 첫 번째 명령이 수행되었다면 id 302번을 포함해 그 앞의 커맨드 버퍼들은 모두 처리가 완료되었기에 KMD에 반환되어 할당을 해지하거나, 내용을 수정하거나, 싸구려 놀이공원으로 변경할 수 있다는 것이져.

이제 X가 무엇이 될 수 있는지 예를 들어보겠슴다. “여기까지 왔다면”이 그 중 하나가 될 수 있겠네여. 가장 간단하면서도 매우 유용한 예임다. 또다른 예는 “커맨드 버퍼 이 지점 이전에 오는 모든 셰이더들이 텍스처로부터 읽기를 완료했다면”이 될 수 있겠네여(이는 텍스처/렌더 타겟 메모리를 안전하게 반환할 수 있는 지점을 나타내져). “만약 활성화된 모든 렌더 타겟/UAV로의 렌더링이 완료되었다면” 도 있져(이는 해당 렌더타겟/UAV를 안전하게 텍스처로 이용할 수 있는 지점을 나타냄다). “만약 이 지점까지의 모든 작업이 완료되었다면” 이라던가, 그 외 여러가지가 있을 수 있슴다.

이러한 것들은 종종 담장fence으로 불리기도 하져. 상태 레지스터에 어떤 값들을 써넣을지 선택하는 데는 여러가지 방법이 있지만, 필자가 바람직하다고 보는 것은 순차적으로 증가하는 값을 사용하는 것임다(몇몇 비트는 다른 정보를 저장하는 데 쓸 수 있긴 하지만여). 넵, 지금 제가 어떤 합리적인 근거 없이 어떤 정보 한 조각을 덜렁 던진 것이긴 한데.. 왜 그랬는지 아마 님도 아실듯여. 여기에 대해선 나중에 별도의 포스팅으로 자세히 설명해볼까 함돠(이 시리즈는 아니지만여).

자 여기까지 절반은 왔슴다. 이제 GPU에서 CPU로 상태를 알려줘서 드라이버단에서 제대로 메모리 관리를 할 수 있게 되었네여(특히, 버텍스 버퍼나 명령 버퍼, 텍스처나 기타 리소스에 사용된 메모리를 언제 안전하게 반환할 수 있는지 알수 있게 되었져). 하지만 그게 전부가 아님돠. 퍼즐 한 조각이 빠져있져. 예를 들어, 만약 온전히 GPU단에서만 동기화를 해야 할 경우 어떻게 해야 할까여? 렌더 타겟의 예로 돌아가보져. 렌더타겟은 거기에 렌더링하는 작업이 끝나기 전에는 텍스처로 사용할 수 없슴다(그리고 몇 단계를 더 거쳐야 하는데, 이건 나중에 텍스처링 유닛을 다룰 때 상세히 설명할게여). 해법은 “대기”스타일의 명령어임다: “레지스터 M에 N이라는 값을 담길 때 까지 대기”같은 형태인거져. 값을 비교하는 연산은 “같거나” 가 되거나 “미만이거나”도 될 수 있슴다(이 경우 랩어라운드wraparound 도 고려하여 처리해야함을 유의하세여). 혹은 좀 더 복잡한 식이 될 수도 있는데, 간단히 설명하기 위해 “같거나”의 예를 든 것임다. 이 명령어가 있어 배치batch 를 접수시키기 전에 렌더타겟의 동기화가 가능해지져. 또한 GPU를 완전히 비우는flush 처리도 가능하게 해 줌다: “모든 대기중인 작업이 처리 완료되면 레지스터0번에 ++seqid 값을 기록” 이나 “레지스터0 의 값이 seqId 가 될 때 까지 대기” 같은 형태로 말이져. 좋아여 좋아. GPU/GPU 동기화가 문제가 해결되었네여. 그리고 좀 더 정교한 동기화 형태를 포함하는 Compute Shader 가 탑재된 DX11 이전까지, 이러한 방식은 GPU단 동기화를 처리하는 유일한 방법이었져. 일반적인 렌더링이라면 더 이상은 필요없져.

한편, 이러한 레지스터에 값을 쓰는 게 CPU단에서도 가능하게 되면, 이걸 식으로도 활용 가능함다. 특정 값이 될 때 까지 대기하는 명령을 포함하는 명령 버퍼의 일부를 접수시키고, GPU단이 아닌 CPU단에서 레지스터의 값을 변경하는 것이져. 이러한 기법은 D3D11 에서 지원되는, CPU단에서 락되어있는 버텍스/인덱스 버퍼(아마도 이는 다른 스레드에서 값을 쓰기 위해 락되어있겠져)를 참조하는 배치를 접수하는 식의 멀티스레드 렌더링을 구현하는 데 사용할 수 있슴다. 간단히 실제 렌더 콜 앞에 대기 명령을 채워넣고, 버텍스/인덱스 버퍼가 실제로 언락되었을 때 레지스터에 값을 쓰게 할 수도 있슴다. GPU가 해당 명령을 처리하기 전까지, 그 대기 명령은 단순한 no-op(주:아무 것도 하지 않는 명령)이져. 만약 그 명령을 처리하게 되면(주:그리고 그 때까지 버텍스/인덱스버퍼가 준비 완료되어 언락되지 않았다면), (커맨드 프로세서는) 시간을 보내며 데이터가 마련될 때까지 기다리게 되겠져. 근사하지 않나여? 사실 이러한 방식을 구현하는 건 CPU에서 쓰기 가능한 레지스터 없이도 구현 가능함다. “점프”명령을 통해 명령 버퍼를 접수시킨 후에도 수정 가능하다면 말이져. 자세한 사항은 관심있는 독자들을 위한 몫으로 남겨둘게여.

물론, 레지스터에 값을 쓰고 대기하는 모델이 아닌 다른 방식으로 구현도 가능함다. GPU/GPU 단 동기화를 처리하는데는 렌더타겟이 안전하게 사용할 수 있음을 보장하는 “렌더타겟 배리어rendertarget barrier” 명령과, “전부 비우기” 명령이 있으면 되져. 하지만 필자는 레지스터 형태의 모델을 더 선호함다. “사용중인 리소스의 CPU로의 역보고back-reporting” 와 “GPU단 내부에서의 동기화” 두 가지 몹을 잘 설계된 무기 하나로 일타쌍피할 수 있기 때문이져.

업데이트: 그림을 하나 그렸슴다. (주: 원 포스팅에서는 링크로 처리했지만 이번에도 그냥 그림을 바로 갖다박았슴다.)

좀 복잡해서 나중에 디테일을 좀 줄여 간단하게 만들긴 할텐데, 어쨌든 핵심은 다음과 같슴다: 커맨드 프로세서는 FIFO 가 선두에 있고, 그 담에 명령어 해석 로직이 있구여, 명령어 실행은 2D유닛, 3D 프론트엔드(일반적인 3D렌더링), 셰이더 유닛(컴퓨트 셰이더 처리)등과 같은 여러 블록이 서로 직접 통신하면서 처리됨다. 그리고 동기화/대기 명령을 처리하는 블럭이 있구여(얘네는 언급했던 외부 접근 가능한 레지스터를 가지고 있져), 명령 버퍼 점프/호출을 처리하는 유닛이 있슴다(얘가 FIFO로 가는 명령어를 가져올 주소를 변경하는 처리를 하져). 작업 처리를 요청받은 모든 유닛은 처리가 끝나면 완료 이벤트를 돌려줌다. 그러면 이를 통해 이를테면 어떤 텍스처가 더 이상 사용되지 않으면 거기에 할당된 메모리를 반환한다던가 하는 처리가 가능해지져.

닫는 글

다음으로는 실질적인 렌더링 작업을 처음으로 다루게 됨다. GPU를 다루는 시리즈 3번째 파트에서 마침내 실제 버텍스 데이터에 데이터에 대해 파보게 되는 것이져! (아직 삼각형을 그리는rasterize 건 아녜여. 요건 좀 더 있다가.)

사실 이미 이 단계에서 분기가 벌써 등장함다. 만약 컴퓨터 셰이더를 사용한다면, 다음 단계는… 컴퓨트 셰이더를 실행하는 것이져. 하지만 그러지 않을 거예여. 컴퓨트 셰이더는 좀 더 나중에 다루게 될 거예여. 일반적인 렌더링이 먼저져.

소소한 면책조항 하나: 다시 한 번 말하지만, 이 시리즈는 어떤 큰 그림을 그리면서, 필요한(혹은 재밌을만한) 부분을 상세히 들어다보는 형태로 진행됨다. 하지만 편의상(그리고 쉬운 이해를 위해) 생략된 부분이 많이 있다는 거, 하지만 정말 중요한 것은 빼먹지 않았다는 걸 믿어주십셔. 물론, 틀린 부분이 있을 수 있슴다. 만약 버그를 발견했다면 꼭 알려주세염!

그럼 다음 글에서 만나요~

A trip through the Graphics Pipeline 2011 요약정리와 번역 중간 어디쯤(…) – Part 1

(이하는 A trip through the Graphics Pipeline 2011, part 1 를 날림번역한 것임다.)


Part 1 – 소프트웨어 계층

 

응용 프로그램

니가 짠 코드ㅇㅇ. 니가 싼 버그(…)도 포함. (주:D3D나 OpenGL API 를 호출하는 코드가 포함된 프로그램을 얘기한다고 보면 될듯)

API 런타임

API 런타임은 다음과 같은 요청들을 니가 짠 코드로부터 받게 된다.

  • 리소스 생성
  • 스테이트 설정
  • 드로우콜 등

API 런타임은 넘겨받은 요청에 다음과 같은 처리를 한 뒤 작업들을 묶어서(batch) 유저 모드 드라이버(UMD)에게 넘겨준다. UMD 에 대한 설명은 바로 다음 섹션임.

  • 설정된 스테이트를 유지, 관리
  • 파라미터 유효성이나 그 외 에러나 일관성 검사 수행
  • 유저가 접근 가능한 리소스 관리
  • 셰이더 유효성 검사나 링키지(linkage)처리(D3D 는 이 단계에서 처리하지만, OpenGL 은 드라이버단에서 처리함)
  • 그 외 몇가지 작업들

유저 모드 그래픽 드라이버(user mode graphics driver, UMD)

CPU단의 마법들이 일어나는 곳. 니가 짠 코드가 호출한 API 에서 크래시가 발생할 때 운영체제가 “여기서 디졌어염 뿌우”하고 일러바치는 곳. 엔비디아는 nvd3dum.dll, AMD는 atiumd*.dll 뭐 그런 거.

유저모드라는 이름에서 알 수 있듯 응용 프로그램 코드와 같은 컨텍스트, 주소공간에서 돌아가고 권한 상승도 없다.

D3D가 호출하는 저수준 API(DDI)를 구현한 곳. DDI는 D3D API 와 유사한 형태인데, 메모리 관리와 같은 부분이 좀 더 명시적으로 처리되는 등의 차이가 있음.

(주 : 이 부분 부터는 순서도 좀 다르게 편집했고 의역도 엄청 들어감미다..)

셰이더 컴파일 과정의 일부가 처리되는 곳. 물론 셰이더는 HLSL컴파일러나 D3D API 런타임에서 처리되는 부분도 있기는 한데, 이러한 과정을 거친 것은 shader token stream 이며 UMD는 이를 받아 하드웨어와 밀접한 저수준의 컴파일단계를 처리한다.

좀 더 상세히 설명하자면, UMD 이전 단계에서의 셰이더 코드는 다음과 같은 검사를 거치게 된다.

  • 문법적으로 올바른지
  • D3D 제한사항들을 올바르게 지키고 있는지. 제한사항은 다음과 같다.
    • 올바른 타입을 사용하였는가
    • 텍스처/샘플 갯수 제한을 넘지 않았는가
    • 상수 버퍼 갯수 제한을 넘지 않았는가 등.

또한 여러 고수준 최적화가 적용된다. 이는 다음과 같다.

  • 다양한 루프 최적화
  • 사용하지 않는 코드 제거
  • constant propagation (주 : 어떤 수식이 컴파일타임에 상수로 계산될 수 있으면 그 식을 상수로 치환하는 것)
  • 분기 예측 등

UMD는 이러한 비싼 최적화들이 적용된 결과물을 넘겨받기 때문에 그만큼 부담을 덜게 된다.

UMD에서 수행되는 여러가지 저수준 최적화도 있는데, 이는 다음과 같다.

  • 레지스터 할당
  • loop unrolling (주 : 루프를 반복된 코드로 확장하는 것) 등

간단히 얘기하자면, 중간코드(Intermediate Representation, IR) 로 변환된 것을 뭔가 조금 더 컴파일해주는 정도라고 할 수 있는데, 이는 (저수준)컴파일러가 뭔가 좋은 결과를 내기 위해 엄청난 일을 할 필요가 없을만큼 D3D 바이트코드가 셰이더 하드웨어에 충분히 가깝기 때문이다. 하지만 그럼에도 불구하고 D3D가 알 필요도 없고 신경쓰지도 않는 하드웨어와 관련된 세부사항들이 있기에, 이 단계는 결코 사소한 단계라고 볼 수 없다. 이러한 세부사항들은 다음과 같다.

  • 하드웨어 자원 제한
  • 스케줄링 제약

그리고 유명한 게임들에 대해서는 엔비댜와 암드의 프로그래머들이 그 셰이더를 디벼보고 직접 새로 최적화해 작성한 셰이더가 대신 돌아가게 하는 경우가 있는데, 그러한 해당 게임의 검출과 셰이더 교체 또한 이 UMD 단에서 이뤄진다.

그리고 또한 몇몇 API state 들은 최종적으로 셰이더에 컴파일되어 적용된다. 예를 들어 드물게 사용되는 기능인 텍스처 경계 상태(texture border state)는 샘플러 코드에 구현되는 것이 아니라 셰이더에 추가 코드로서 에뮬레이션된다.(혹은 쌩까고 아예 동작하지 않도록 무시되기도 함). 이 말인 즉슨 한 셰이더가 대해 각각의 state 조합들이 적용된 여러 버전의 셰이더들이 존재할 수 있다는 얘기임.

덧붙여 말하자면 이게 바로 어떠한 셰이더나 리소스를 새로이 사용할 때에 지연이 발생하는 이유임ㅇㅇ. 왜냐면 수많은 생성/컴파일 작업이 드라이버에 의해 가능한한 늦춰지거든. 언제까지? 걔가 필요할 때 까지. 그래픽스 프로그래머들은 이 이야기를 다른 방향으로 접해서 알고 있는데, 그건 바로 뭔가가 그저 메모리만 공간만 확보받은 채로 있는 게 아니라 확실하게 생성되었음을 보장받으려면 그 뭔가를 사용하는 더미 드로우콜을 날려주는 것으로 “워밍업”을 하라는 것이다. 지저분하고 번거롭지만 이건 글쓴이가 1999년에 3D 하드웨어를 처음 시작했을 때 부터 주욱 그래왔으니 어쩌겠음? 그냥 익숙해지셈.

UMD는 또한 D3D9 “legacy” 셰이더들과 fixed pipeline 들도 처리한다. 물론 그들은 새로운 버전의 셰이더로 변환되어 사용된다. (그리고 그렇게 처리한지도 좀 되었음)

텍스처 생성 명령 처리 같은 메모리 관리도 UMD 에서 처리된다. 실질적으로는 KMD(kernel mode driver) 로 부터 얻은 커다란 메모리 영역을 다시 작은 영역으로 나눠 할당하는 것이다. 비디오 메모리의 어느 영역을 UMD가 볼 수 있는지, 그리고 시스템 메모리의 어느 영역에 GPU 가 접근할 수 있는지 매핑하는 것은 KMD의 권한이므로 UMD가 처리할 수 없다.

UMD는 텍스처의 swizzling 처리(주:원문 글쓴이의 다른 글로 링크함. shader 에서 쓰는 swizzling 과는 다른 개념. 텍스처 처리시 캐시 히트율을 높이기 위해 연속된 공간에 배치되는 픽셀의 순서를 바꾸어 저장하는 것을 말함..인데 언젠가는 원문도 각잡고 번역하든 요약정리를 하든 할 것임. 일단 그 전에는 원문 링크로…아. 이런 건 각주 시스템이 있으면 각주로 다는 게 좋을 거 같네ㅋ)를 하고 시스템 메모리와 (매핑된) 비디오 메모리간의 전송을 스케줄링하는 것과 같은 일을 처리하기도 한다. 물론 swizzling 의 경우 GPU 가 3D 파이프라인이 아닌 2D blitting 유닛을 통해 처리할 수도 있는데, 이렇게 할 수 없는 경우 UMD 에 의해서 CPU단에서 처리되는 것이지만. 가장 중요한 사실은 UMD 가 커맨드 버퍼(혹은 DMA 버퍼. 원문 글쓴이께서 커맨드버퍼와 DMA버퍼라는 단어를 섞어서 쓰시겠답니다ㅋ)의 내용을 작성한다는 것인데, 이 커맨드 버퍼는 KMD가 할당한 것이다. API런타임을 통해 요청한 상태 전환, 그리기 작업 등은 UMD에 의해 하드웨어가 알아먹는 커맨드로 변환된다. 텍스처나 셰이더를 비디오 메모리로 업로드하는 작업 등과 같은 많은 작업들이 이러한 변환에 의해 알아서 호출되게 된다.

UMD는 유저모드 코드이기에, 일반적으로 드라이버는 UMD로 최대한 많은 처리 요청을 밀어넣으려 할 것이다. UMD에 의해 처리되는 작업들은 비싼 커널모드 전환이 필요치 않고, 자유롭게 메모리를 할당할 수 있으며, 작업들을 멀티스레드로 돌릴 수 있는 등 여러 장점이 있는데, 이는 UMD가 보통의 DLL로 만들어져 있기 때문이다. (물론 애플리케이션에 의해 바로 로딩되는 것은 아니고 API에 의해 로딩되는 것이긴 하지만.) 드라이버 개발도중 만약 UMD가 크래시하면 이를 사용하는 앱 역시 크래시하지만 시스템은 크래시하지 않는다. 또한 시스템이 돌아가는 동안 간단히 교체할 수 있으며(왜냐면 그냥 DLL이니까!), 일반적인 디버거로 디버깅할 수 있는 등의 장점이 있다. 편리하고 효율적이져.

“유저 모드 드라이버” 가 아니라 “유저모드 드라이버들”

UMD는 이미 말했던바와 같이 단지 DLL이다. D3D의 가호를 받고 KMD에 곧바로 연결되어있기는 해도, 여전히 보통의 DLL이다. 그래서 호출한 프로세스와 같은 주소공간에서 돌아간다.

하지만 요즘의 OS들은 멀티태스킹이다. 그리고 사실 그런지 이미 좀 되었지.

계속해서 얘기하고 있는 이 “GPU”라는 물건은 공유 자원이다. 메인 디스플레이를 구동하는 GPU는 단 하나만 존재한다(SLI 나 크로스파이어 구성을 했더라도 말이지). (주 : 이러한 하나 뿐인 GPU를 멀티태스킹 OS에서 공유하여 쓸 수 있게 만드는 것은) 자동으로 되지 않는다. 옛날에는 3D를 사용하는 애플리케이션 하나가 활성화되면 다른 모든 애플리케이션들은 접근을 하지 못하게 하는 식으로 처리했다. 하지만 윈도우 시스템을 렌더링하는 데 GPU를 쓴다면 그렇게 할 수는 없다. 이것이 바로 GPU로의 접근을 중재하여 시분할로 처리해주는 같은 작업들을 행하는 구성요소가 필요한 이유이다.

스케줄러

스케줄러는 시스템 구성요소이다. 하지만 여기서 얘기하는 것은 CPU스케줄러나 IO스케줄러에 대해서가 아니라 그래픽스 스케줄러임을 명심하자. 스케줄러가 행하는 것은 니가 생각하는 바로 그것이다. 3D 파이프라인을 사용하려는 여러 앱들의 접근을 시분할로 중재해주는 것이져. 컨텍스트 스위칭이 발생하면, 적어도 GPU 의 상태 변환이 일어나고(이를 처리하기 위해 커맨드 버퍼에 추가 커맨드가 들어감), 비디오 메모리로 리소스가 올라가거나 내려오는 스와핑이 일어날 수도 있다. 그리고 당연히 어떤 한 시점에서 3D 파이프라인에 커맨드를 주는 것은 단 하나의 프로세스이다.

콘솔 프로그래머들은 PC 3D API 의 상당히 고수준이며 직접 제어할 수 없이 알아서 처리되는(hand-off) 특성에 의해 야기되는 성능 비용에 대해 종종 불만을 표시한다. 하지만 PC 의 3D API 나 드라이버가  해결해야 하는 문제들은 콘솔 게임에 비해 더 복잡하다. 예를 들자면 PC 에서는 모든 스테이트의 변화를 관리하여야 하는데, 이는 다른 앱이 언제 상태를 바꿔버릴지 알 수 없기 때문이다. 또한 PC 에서는 망가진 애플리케이션을 우회하고 이로 인해 발생하는 보이지 않는 성능 문제를 해결해야 한다. 이는 드라이버 개발자 자신을 포함해서 누구에게도 즐겁지 않은 상당히 성가신 관행이지만, 현실 세계에서는 비즈니스 관점이라는 게 있으니까여. 사람들은 잘 돌아가고 있는 것은 계속해서 (아주 부드럽게) 잘 돌아가기를 바라져 넵. 애플리케이션에다 대고 “그건 틀렸어 임마!”라고 외치고는 부루퉁한 표정으로 엄청 느린 길을 택해서 간다면 친구를 아무도 사귈 수 없을겁니다.

커널 모드 드라이버(kernel-mode drver, KMD)

KMD 는 하드웨어를 다루는 부분이다. UMD는 동시에 여러 인스턴스가 실행중일 수 있지만, KMD는 항상 오직 하나만 실행될 수 있다. 그리고 만약 그게 크래쉬된다면 잦되는거져ㅋ. 예전에는 블루스크린이었지만, 요즘 윈도는 크래쉬된 드라이버를 죽였다 다시 살리는 게 가능하다. 물론 이건 최소한 커널 메모리 손상이 아닌 단순한 크래시일때만 가능하다. 그렇지 않다면 끝장인거임ㅋ

커널 모드 드라이버는 모든 ‘단 하나 뿐인 것들’을 다룬다. 여러 앱이 GPU 메모리를 두고 다투지만, 실제 GPU 메모리는 하나밖에 없다. 누군가는 명령을 내리고 실제로 메모리를 할당하며 이를 매핑해야한다. 이와 유사하게 누군가는 시스템이 시작될 때 GPU를 초기화하거나, 디스플레이 모드를 설정하거나 디스플레이로부터 모드 정보를 얻어온다던가 하드웨어 마우스 커서를 관리하는 일, 하드웨어 타이머를 설정하여 GPU가 일정 시간 응답이 없으면 리셋을 하는 일, 인터럽트에 응답하는 일 등을 해야 한다. 이것이 바로 KMD가 하는 일이다.

또한 컨텐츠 보호 / DRM과 관련하여 비디오 플레이어와 GPU간에 보호/DRM 된 패스를 설정하여, 지저분한 유저 모드 코드들 통해 디코딩된 소중한 픽셀들이 드러나는 일이 없도록 해서, 이를 디스크에 덤프한다던가 하는 등의 끔찍한 일이 일어나지 않도록 해야 한다. KMD 는 이에 일부 관여한다.

가장 중요한 사실은 KMD가 관리하는 커맨드 버퍼가 하드웨어가 실제로 처리하는 바로 그 커맨드 버퍼라는 것이다. UMD 가 생성하는 커맨드 버퍼는 실제 커맨드 버퍼가 아니다. GPU 가 접근 가능한 랜덤한 메모리 조각에 불과하다. 실제로는 다음과 같은 일이 일어난다. UMD 가 커맨드 버퍼 작업을 마치면 이를 스케줄러에 접수시킨다. 스케줄러는 프로세스가 깨어날 때 까지 기다렸다가 UMD 커맨드버퍼를 KMD 커맨드버퍼에 넘겨준다. 그러면 KMD는 (UMD) 커맨드 버퍼를 호출하는 명령을 주 커맨드 버퍼에 써넣는데, 만약 GPU 가 메인 메모리를 읽을 수 없을 경우 (UMD) 커맨드 버퍼의 내용을 비디오 메모리로 먼저 DMA 한 다음에 이 작업을 수행한다. 메인 커맨드 버퍼는 보통 (아주 작은) 링 버퍼로, 거기에 실제 쓰여지는 것은 시스템/초기화 명령과 실제 3D 커맨드 버퍼를 호출하는 명령밖에 없다.

어쨌든 이 커맨드 버퍼는 메모리상에 존재하는 버퍼일 뿐이다. 그래픽 카드에게 이 버퍼의 위치는 보통 읽기 포인터와 쓰기 포인터를 통해 알려진다. 읽기 포인터는 주 커맨드 버퍼의 어디를 GPU 처리하고 있는지를 표시하며, 쓰기 포인터는 KMD 가 명령을 써 놓은 위치가 어디까지인지를(좀 더 정확히 얘기하자면 KMD 가 GPU 에게 어디까지 명령을 작성했는지 알려줬는지를) 나타낸다. 이들은 하드웨어 레지스터로, 메모리에 매핑되어 있으며 KMD 가 이를 주기적으로 업데이트한다(보통 KMD 가 새로운 작업 덩어리를 접수시킬 때 업데이트한다).

버스(bus)

KMD 가 이렇게 써 놨다고 해서 이게 바로 그래픽 카드로 가는 것은 아니다(그래픽 카드가 CPU 다이에 통합되어 있지 않은 다음에야). 먼저 버스를 통과해야 한다. 요즈음은 PCI 익스프레스이다. DMA 전송 등도 역시 버스를 거쳐야 한다. 이는 오래 걸리는 것은 아니지만, 어쨌든 이 여행의 또다른 단계이다. 그리고 마침내…

커맨드 프로세서command processor!

커맨드 프로세서는 GPU 의 프론트엔드이다. KMD 가 작성한 커맨드들을 실제로 읽어들이는 것이 이곳이다. 이미 글이 충분히 길어졌기에, 나머지는 다음 포스팅에서 계속.

OpenGL 의 몇가지 차이점들

OpenGL 은 이 글에서 설명한 내용과 매우 비슷하다. 하지만 API 와 UMD 가 딱 잘라 나뉘어지지 않는다. 그리고 D3D 와는 달리, (GLSL)셰이더 컴파일은 API가 아니라 드라이버단에서 전부 처리된다. 따라서 3D 하드웨어 제조사들마다 하나씩 GLSL 프론트엔드를 구현하게 되는데, 이는 기본적으로는 같은 스펙을 구현한 것이지만 각자의 버그나 특이성을 갖게 되는 부작용이 발생하게 된다. 노잼이져. 그리고 이는 드라이버가 셰이더를 만날 때 마다 최적화 – 그 ‘비싼’ 최적화들을 포함해서 – 를 처리해 한다는 것을 의미한다. D3D 의 바이트코드는 이런 문제에 대한 매우 깔끔한 해결책이다. 컴파일러가 하나밖에 없으니까. (따라서 제조사간에 미묘하게 달라서 호환되지 않는 방언들이 존재하는 문제도 발생하지 않게 된다.) 이 덕분에 너는 좀 더 비싼 데이터 흐름 분석을 할 수 있게 된다. <- 원문은 ‘and it allows for some costlier data-flow analysis than you would normally do’ 인데 costlier data-flow analysis 가 뭘 의미하는지 감이 안 잡히네? 뭔가 특정한 걸 의미하는 거 같은데… 걍 개별 드라이버에 의한 셰이더 버그나 특성 문제 해결하는 데 신경쓰지않고 나머지 작업에 집중할 수 있다 그런 뜻일라나?

생략되고 간소화된 부분들

이 글은 개요이기에 엄청나게 많은 미묘한 사항들을 적당히 얼버무리고 넘어갔다. 예를 들자면 스케줄러의 경우 딱 하나만 있는 것이 아니고 여러 가지 구현이 존재할 수 있다(드라이버가 이를 선택할 수 있음). CPU 와 GPU 간의 동기화에 대해서도 아직까지 전혀 설명하지 않았으며, 기타 등등 여러가지가 있다. 그리고 내가 무언가 중요한 것을 완전히 까먹고 있을 수도 있는데, 이러한 것은 고칠 수 있도록 알려 달라. 하지만 지금은 안녕. 담에보아~

A trip through the Graphics Pipeline 2011 요약정리 – Index

2013년 즈음이었는지 그보다 더 전이었는지 정확히 기억이 나지 않는데(원래 써놨는데 망할 텍큐 버그 때문에 한 문단을 날리면서…) 어쨌든 A trip through the Graphics Pipeline 2011 를 발견하고 읽으려고 시도했으나 몇 번 시도 끝에 엄청 더디게 진도는 나갔으나 곧 포기.. 또 읽다가 포기. 그리고 몇 년을 묵혀놨었는데…

마침 DirectX9 이후로 팽개쳐둔 그래픽스 API들에 대한 공부도 좀 해둬야겠단 생각이 든 차에 이 글이 다시 생각나서, 이번엔 좀 더 active 하게 읽기 위해 요약을 하면서 기록으로 남기고자 이 글을 시작.

…했으나 쓰다보니 요약이 아니라 전문번역에 가까워졌네-_-

어쨌거나 일단 이 글은 index 파트에 대한 요약임.

(이하는 A trip through the Graphics Pipeline 2011: Index 를 요약한 내용임다)

GPU에 실제로 구현된 D3D/OpenGL 그래픽스 파이프라인에 관한 글임.

GPU에 관해 넓은 범위에서의 개요를 다루거나 각 컴포넌트를 자세히 다루는 논문은 많지만 그 중간을 다루는 것이 없는 게 계속 신경쓰였다. (주:그래서 필자가 저 블로그 포스팅 시리즈들을 썼겠져.)

적어도 D3D9 이상, OpenGL 2.0 이상의 3D API 에 대한 지식을 가지고 있는 프로그래머를 대상으로 함.

초보용 아님.

레지스터라던가, FIFO 라던가, 캐시, 파이프라인이 뭐고 어떻게 동작하지와 같은 하드웨어에 대한 최소한의 지식도 필요함ㅇㅇ.

병렬 프로그래밍 메커니즘에 대한 최소한의 지식. 왜냐면 GPU 자체가 엄청 병렬 처리를 하는 컴퓨터니까.

(원래 여기에 한 문단이 더 있는데, 처음엔 요약으로 시작해서 빼버려도 될 거 같아서 생략했다가 Part 1을 진행하면서 번역 비슷하게 되어버려 좀 애매해졌는데.. 추후에 봐서 번역해 추가하던지 하겠슴ㅋ)

Part 2 – GPU memory architecture and the Command Processor.
Part 3 – 3D pipeline overview, vertex processing.
Part 4 – Texture samplers.
Part 5 – Primitive Assembly, Clip/Cull, Projection, and Viewport transform.
Part 6 – (Triangle) rasterization and setup.
Part 7 – Z/Stencil processing, 3 different ways.
Part 8 – Pixel processing – “fork phase”.
Part 9 – Pixel processing – “join phase”.
Part 10 – Geometry Shaders.
Part 11 – Stream-Out.
Part 12 – Tessellation.
Part 13 – Compute Shaders

Unity MatCap shader 분석(하려다 삽질한) 기록

작년 이맘때쯤엔 한창 신규 프로젝트의 세일즈 빌드 작업을 하고 있었다. 작업을 하다 보니 셰이더 코드를 좀 건드릴 일이 있었는데 그때 눈에 밟히는 부분이 있긴 했지만 딱히 파고 들 시간이 없어서 냅두다가 최근에서야 약간 여유가 생겨 다시 파보게 됨.

사실 딱히 복잡하다거나 뭔가 엄청난 신기술이거나 그런 거 아니고, 어찌보면 기본의 기본에 해당하는 부분인데 매번 ‘일단 돌아가니까 그걸로 ㅇㅋ’로 대충 뭉개고 넘어가던 부분인지라.. 이왕 삽질하면서 알게 된 거 정리라도 해 둬야 나중에 까먹고 또 삽질할때 시간이라도 좀 아끼지 하는 마음으로 써봄.

어쨌거나 문제의 발단이 된 코드는 이건데,

http://wiki.unity3d.com/index.php/MatCap

얼핏 봤을 땐 시선 방향을 범프를 적용해 어찌저찌 틀어서 이걸 이용해 matcap 텍스처의 픽셀을 가져다 쓰는..뭐 그런 코드인 거는 내가 잘 알겠다.

근데 구체적으로 뜯어보려니까 음? 어라라?? 스러운 부분이 한두군데가 아닌 것이었다. 정확히 얘기하자면 한두군데가 이상한 정도가 아니라 아니라 전체를 대충은 알아도 각 코드 한 줄 한 줄이 구체적으로 어떤 의미인지 정확히는 모르고 있었던 것이지라.

여튼 코드에서 뜯어봐야 할 핵심인 부분을 보자면,

vert 함수(버텍스 셰이더 코드)의

TANGENT_SPACE_ROTATION;
o.TtoV0 = mul(rotation, UNITY_MATRIX_IT_MV[0].xyz);
o.TtoV1 = mul(rotation, UNITY_MATRIX_IT_MV[1].xyz);

그리고 frag 함수(픽셀 셰이더 코드)의

vn.x = dot(i.TtoV0, normal);
vn.y = dot(i.TtoV1, normal);float4 matcapLookup = tex2D(_MatCap, vn*0.5 + 0.5);

각각 세 줄 씩.

일단 버텍스 셰이더 코드를 보자니.. 첨부터 못 보던 매크로 같은 물건이 보인다. TANGENT_SPACE_ROTATION

이건 뭔가 유니티 Shaderlab 에서 제공하는 변수 같은건가 하고 찾아보니까.. 딱히 매뉴얼에 정의가 된 건 안보임(내가 못찾았을수도),

그래서 혹시나 싶어 빌트인 셰이더 소스코드의 UnityCG.cginc 를 까보니 맞네… 거기 정의되어있는 매크로였음.

// Declares 3×3 matrix ‘rotation’, filled with tangent space basis
#define TANGENT_SPACE_ROTATION \
float3 binormal = cross( normalize(v.normal), normalize(v.tangent.xyz) ) * v.tangent.w; \
float3x3 rotation = float3x3( v.tangent.xyz, binormal, v.normal )

보니까 버텍스 셰이더 입력으로 노멀과 탄젠트가 들어오는데, 이걸로 바이노멀을 계산해서 탄젠트 스페이스상의 값으로 회전시켜주는(..것으로 보이는) rotation 이라는 변수명의 3×3 행렬을 만들어주는 매크로.

그리고 바로 다음줄과 그 다음줄에 등장하는 UNITY_MATRIX_IT_MV 라는 역시나 매크로스러워 보이는 이름의 무언가가 있는데.. 처음엔 이름으로 대충 때려잡기로 IT 는 inverse transform 일테고, MV 는 model 이랑 view 행렬을 얘기하는 거겠지..라고 생각.
근데 아니었음. 제대로 찾아보면 나오는데.. “Inverse transpose of model * view matrix” 임ㅋ. 그리고 매크로가 아니라 저거 자체가 변수이기도 하고.

어쨌든 여기까지 해서 모르던 변수나 매크로는 대충 확인이 되었으니.. 이걸 가지고 전체적인 흐름을 다시 한 번 보자면 버텍스 셰이더에서 탄젠트 스페이스로의 변환 행렬과 모델뷰 행렬을 곱해서 픽셀 셰이더로 넘기고, 이걸 픽셀 셰이더에서 노멀맵에서 언팩한 노멀 벡터랑 쿵짝해서 만든 실수값 두 개를 uv좌표로 써서 matcap 텍스처의 픽셀을 가져다 씀..인 것. 뭐 얼핏 봤을때의 그것이랑 같은 얘기. 근데 당장 제일 처음에 버텍스 셰이더에서 탄젠트 스페이스로 보내주는 벡터부터 뭐인지부터 모르겠다는거.

그래서 우선, 어떤 걸 모르고/헷갈리고 있었는지부터 적어보고자 함.

제일 처음이 바로 위에서 얘기한 탄젠트 스페이스로 보내지는..벡터가 뭔지 모르겠는데.. 이건 사실 코드상에서 뭘 곱해주는지가 명시적으로 드러나지 않는 상황. 그래서 나머지 코드들을 해석하고 나서 역으로 끼워맞춰야 하더라. 다 풀고 보니 결과적으로 이렇게 되긴 했는데, 모를 땐 처음부터 막혀서 막 헤매게 된 원인이기도 함.

그 다음이 TANGENT_SPACE_ROTATION 인데..
얘는 오브젝트의 로컬 스페이스에서 한 점의 탄젠트, 바이노멀, 노멀 벡터가 어느 방향인지를 나타내는 값을 가져다 이를 차례대로 행렬의 1, 2, 3행으로 쌓아서 행렬을 만든것.

근데 일단 이게 내가 생각한 것(탄젠트 스페이스로의 변환)이 맞는지 확인해보려고 하니 행렬과 벡터를 곱하는 순서가 어떻게 되는지부터 알아야겠단 생각이 들었음.

지금 회사에서 유니티 쓰기 전까지 일하면서는 주욱 D3D로만 작업을 하다보니 알고있고 익숙한 게 대부분 D3D기준인데, 예를 들자면 D3D 는 행렬에 벡터를 곱할 때 벡터가 먼저 오고 행렬이 뒤에 오는 형태라는 거. 이건 4×4 행렬로 트랜스폼을 한 방에 처리할 때 translation 에 해당하는 성분이 4행의 1, 2, 3열에 오게 된다.

근데 유니티의 셰이더코드는 Cg 기반이고, Cg는 예전에 얼핏 듣기로 행렬과 벡터의 곱셈이 행렬이 먼저 오고 벡터가 나중에 오는 형태라고 알고 있었는데.. 찾아보니 맞게 기억하고 있는거였음. 확인은 위키책의 해당 항목(이쪽이 좀 더 자세한데..위키책 사이트가 종종 터지는듯?) 이나 좀 덜 자세하지만 Cg 의 mul 함수 설명에서.

따라서 유니티 셰이더 코드의 4×4 행렬에서는 translation 에 해당하는 성분은 4열의 1, 2, 3행에 위치함. 사실 이 글에서 translation 위치 자체가 중요한 건 아닌데.. 내가 4×4 트랜스폼 행렬이 어느 방향을 향하는지 기억하는 기준이 translation 행 혹은 열의 위치인지라.

어쨌거나 다시 TANGENT_SPACE_ROTATION 로 돌아가서 보면, 매크로 코드에 의해 이 3×3 행렬의 1행은 탄젠트 벡터, 2행은 바이노멀 벡터, 3행이 노멀 벡터인데,

회전변환행렬을 구성할 때 회전된 결과인 세 축을 알면 그 축을 각각 회전행렬의 1, 2, 3행으로 쓸 수도 있다.. 정도의 아련한(…)기억이 있는지라, 일단 이게 맞는지 확인부터 해야겠다는 생각이 들었음. 근데 검색을 해도 찾기가 어려워서.. 그럼 이 행렬에다 x, y, z축의 방향벡터를 의미하는 (1, 0, 0), (0, 1, 0), (0, 0, 1) 을 곱해준 결과가 뭔지를 확인해보자로 넘어가게 됨.

즉, x축의 방향벡터인 (1, 0, 0)을 TANGENT_SPACE_ROTATION 의 결과물인 rotation 변수에 곱해주면 탄젠트 방향벡터가 나오겠지… 했는데 아니네? 탄젠트 벡터는 rotation 행렬의 1행인데 rotation 에 (1, 0, 0)을 곱해주면 1열 성분의 벡터가 나오자네. 즉 rotation 행렬을 transpose 한 것에 (1, 0, 0)을 곱해줘야 탄젠트벡터가 나오게 되니, 여기서 나는 rotation 변수를 계산할 때 단순히 1, 2, 3행에 탄젠트, 바이노멀, 노멀을 넣고 땡이 아니라 이걸 한 번 transpose 해 줘야 하는 코드에선 이걸 빼먹고 잘못 쓰고 있는 게 아닌가.. 라고 엉뚱한 생각을 했다-_-. 혹은 코드가 잘못되지 않았다면rotation 변수의 의미가 탄젠트 스페이스로의 회전이 아니라 탄젠트 스페이스에서 오브젝트의 로컬 스페이스로의 회전이 아닌가..라고 의심을 하게 됨(역시나 틀린 생각임). 이렇게 첫 부분도 그 다음도 계속 헷갈리고 있으니 나머지 부분도 엄청 삽질하게 될 것은 불을 보듯 뻔하고..

일단 그렇게 아리송다리송@_@한 상태에서.. 나머지 부분은 어케 문맥이랑 끼워맞춰보면 될라나 하는 심정으로 그 다음줄로 넘어가니…
어?
행렬이랑 행렬을 곱하는 게 나올 줄 알았는데 그게 아니라 행렬이랑 행렬의 1, 2 행 성분을 각각 곱해서 그걸 픽셀 셰이더로 넘겨주네?

이걸 보고서 노멀 언패킹때처럼 서로 직교하는 세 축의 경우 두 축만 값을 남기고 나머지 하나는 계산으로 만들어 쓰는건가? 아니면 행렬끼리 계산한 결과와 마지막에 곱해주는 벡터에 x, y 성분만 사용하는건가 하는 등의 생각이 들었는데.. 일단은 여기도 어떤 것인지 확실하게 확인하지 못한 상태로 또 넘어감(…제대로 하는 게 뭐냐ㅋ)

뭔지는 몰라도 어쨌든 행렬과 행렬을 곱하는거 같긴 한데.. 그렇다면 앞의 행렬(rotation 변수)과 뒤의 행렬(UNITY_MATRIX_IT_MV 변수) 의 을 곱해줘야 할텐데.. 왜 곱해주는 게 열이 아니라 행(UNITY_MATRIX_IT_MV[0] 과 UNITY_MATRIX_IT_MV[1])일까-_-. 여기서 혹시나, 정말 혹시나 행렬에 대해 [] 연산자를 쓸 경우 행이 아니라 열에 접근하는건가 하고 다시 아까 Cg 레퍼런스를 확인해봤는데 아님. [] 연산자로 접근했을 때 나오는 벡터는 행 하나가 맞음ㅋ.

그렇다면 저 식의 의미는 UNITY_MATRIX_IT_MV 가 아니라 UNITY_MATRIX_IT_MV 의 transpose 행렬을 곱해주는 것임. 그런데 여기서 UNITY_MATRIX_IT_MV를 직교행렬이라고 착각하고, 이것의 transpose 이니 역행렬이구나..라고 또 착각. 저 식이 UNITY_MATRIX_IT_MV 의 역행렬을 곱해주는 게 되려면 UNITY_MATRIX_IT_MV 가 직교행렬이어야 하는데, 정답편에서 얘기하겠지만 얘는 직교행렬이 아니기 때문에 역행렬이 아니라 그냥 transpose 인거. 왜 이런 식을 쓰는지 정확한 의미는 요 아래에서. 어쨌든 식에서 저 부분을 view space 서 model space 로 변환시켜주는 행렬, 다른 말로는 카메라 공간에서 모델의 로컬 공간으로 변환시켜주는 행렬이 된다고 생각했는데… 이 부분은 대략적으로는 맞긴 하지만 역시나 100% 정확한 표현은 아니고. 뭐 어쨌든 이런 혼돈의 카오스 상태에서 픽셀 셰이더 코드로 넘어갔음.

픽셀 셰이더 코드 조각에서는 첫 번째 줄과 두 번째 줄에서 내적을 해 주는 것을.. 버텍스 셰이더에서 픽셀 셰이더까지 이어지는 변환의 일부(마지막) 즉, 버텍스 셰이더에서 넘겨받은 행렬에 해당하는 것에 픽셀 셰이더가 노멀맵에서 뽑아낸 normal 벡터를 곱해서 변환을 하는 것으로 다시 한 번 거하게 착각을 해 버림. 근데 그거 아니구여… 어쨌거나 착각을 하고 있으니 결과가 어떤 의미인지도 감이 안 잡히고 어디서부터 손을 대야할지도 모르겠다 싶은 상황인거. 일단 이렇게 한 이틀 헤매다 3일째에 갑자기 지푸라기를 하나 붙잡게(…)되었다. 이제 정답편으로 넘어가보겠심다.

정답편

일단 첫빠따는 TANGENT_SPACE_ROTATION.
얘의 진정한 의미는 모델의 로컬 좌표상의 벡터값을 곱하면 탄젠트 스페이스상의 값이 튀어나오는 행렬, 즉, 모델 공간에서 탄젠트 공간으로 변환시켜주는 행렬임. 변수 이름과 설명에 rotation 이라고 되어 있어서 ‘회전을 시킨다’ 라고 생각을 하게 되어 내 경우엔 더 헷갈렸는데, 사실 A라는 좌표계(좌표축)상에서 어떤 방향을 나타내는 벡터가 있을 때 이걸 B라는 좌표축상에서는 어떤 값의 방향벡터로 나타내어지는지, 즉 사용하는 기준 좌표축이 바뀜에 따라 값이 변하는 것을 계산하기 위한 행렬임. 자세한 내용은 선형대수에서 기저변환(change of basis) 부분을 참조..해야 할 거임. 근데 지금 참고하는 책이랑 자료에서 학술적으루다가 정확하고 이쁘게 설명해 놓은 게 없음. 그래서 그냥 무식하게 확인한 방법을 설명해보겠심다.

우선 TANGENT_SPACE_ROTATION 의 결과로 나온 rotation 변수에 x축의 방향벡터 (1, 0, 0)을 곱해주는 상황을 다시 한 번 생각해보겠슴다. 그 곱해준 결과 벡터는… 음. 당장은 뭔지 모르겠네-_-? 탄젠트, 바이노멀, 노멀(TBN) 벡터의 각 x 성분으로 만든 벡터인데.. 이것만 봐선 뭔 의민지…

뭔가 알 수 있는게 나오려면 결과가 TBN 벡터 셋 중에 하나여야 하는데, 그러려면 rotation 이 아니라 rotation 의 transpose 에다가 (1, 0, 0) 을 곱해야겠져? 그래야 rotation 의 1행 성분인 탄젠트 벡터가 나오니께.

근데 이 탄젠트 벡터는 어느 공간상의 값이다? 넹. 모델 공간상의 값임다. 왜냐면 이걸 계산하는 데 쓰인 버텍스의 값이 모델 공간상의 값이니까여. 물론 이건 버텍스 셰이더에 넘어오기 이전에 이미 모델 데이터 레벨에서 정해지는 값이라.. 그러니까 애초에 모델링 툴에서 익스포트하거나 익스포트한 걸 엔진으로 임포트 할 때 정해지는(계산되는) 값이긴 한데.. 음.. 더 자세히 설명하면 여기서 너무 늘어질 수 있으니 일단 여기서는 그냥 모델공간상의 값입니다.. 정도로 넘어가는 걸로ㅋ

어쨌거나 x, y, z좌표축에 해당하는 (1, 0, 0), (0, 1, 0), (0, 0, 1)을 rotation 의 transpose 에다 각각 곱해주면 그 결과값은 모델공간에서의 TBN 벡터에 해당하는 방향값을 뱉아냄미당.

그런데 rotation 은 직교행렬(=행렬을 구성하는 서로 다른 행벡터끼리 내적하면 0. 왜냐면 탄젠트, 바이노멀, 노멀은 서로가 서로에게 수직인 벡터)이니까, rotation 의 transpose 는 rotation 의 역행렬이져.

바로 위 두 문장을 조합하면, rotation 의 역행렬은 좌표축 벡터를 모델공간상의 TBN(탄젠트, 바이노멀, 노멀) 벡터로 회전을 시켜주는 행렬이네여. 그럼 rotaion 은 뭐다? 모델공간상의 TBN 벡터를 좌표축 벡터가 되도록 회전시켜주겠져. 즉, 모델 공간에서 TBN벡터에 해당하는 벡터들을 rotation 행렬에 의해 변환시키면 결과값은 좌표축 벡터가 된다는 말임다. 탄젠트 바이노멀 노멀이 좌표축이 되는 공간이 뭐다? 뭐긴뭐야 탄젠트 공간이지.

즉, rotation 을 모델공간상의 값에다 곱해주면 결과값은 탄젠트 공간상의 값이 된다는 거. (워낙 말장난같긴 한데.. 좌표축 변환이라는 게 기준이 되는 축을 바꾸고 그 결과에 의해 값의 의미가 달라지는거라.. 아.. 나도 설명 좀 더 잘하고 싶다….)

한참을 장황하게 설명했는데, 어쨌든 결론은 TANGENT_SPACE_ROTATION 은 모델 공간의 값을 탄젠트 공간의 값으로 변화시켜주는 행렬을 계산해 이걸 rotation 이라는 이름의 변수에 넣어주는 매크로 라는 게 확실해졌슴다. 그리고 아직 끝나지 않았져. 결론은 결론인데 코드 첫 줄에 대한 결론이잖아. 나머지도 해석해야지ㅋ.

그 다음으로 UNITY_MATRIX_IT_MV 의 진정한 의미를 확인해보도록 하겠슴다.
유니티 문서에 따르면 UNITY_MATRIX_MV 는 Inverse transpose of model * view matrix 임다(아까도 한 얘기지만). 근데 말로 써놔서 좀 헷갈리긴 하는데, 저 말의 정확한 의미는 model * view matrix 를 inverse 한 것을 다시 transpose 한 거란 얘기져. transpose 한 것을 inverse 한 게 아님. (중요)

아우.. 뭘 inverse 랑 transpose 랑 구분을 하고 앉았냐. 둘이 같은거니 어느 게 먼저 와도 상관없이 그냥 상쇄되는거 아녀? 라고 생각하신다면.. 아녀. 그거 틀렸슴미다. model * view matrix 가 일단 직교행렬이 아닐 수 있으니까요. 언제 직교행렬이 아니냐? 며는.. 일단 translation 성분 값이 (0, 0, 0) 이 아닐 때도 그렇고, 스케일이 non-uniform 해도 그렇고.

근데 어차피 이 코드에선 4×4 행렬 중에 좌상단 3×3 값만 써서 노멀 회전시키는거니까, 그렇게되면 translation 성분은 사라지니 노멀이 좀 틀어져도 트랜스폼된 결과값만 노멀라이즈 해서 적당히 쓰면 상관없지 않음? 이라는 시도를 해 볼 수도 있겠지만.. 뭐 다른 수가 없다면 그렇게라도 할텐데 수학적으로 정확한 결과를 낼 수 있는 방법이 엄연히 있져. 바로 그게 inverse transpose of MV 라는 해괴한(…) 물건임. 구체적인 사항은 이 게시물에서 언급하는 이 게시물에서, 혹은 Eric Lengyel 저 3D 게임 프로그래밍&컴퓨터 그래픽을 위한 수학3.5장 법선벡터의 변환에서 확인 가능함다. (2판 한글판 기준으로 119쪽 부근). 그냥 간단히는 non-uniform scale 이 적용된 트랜스폼일때에도 노멀을 제대로 변환하는 용도로 사용..정도로만 정리해둡시다. 어쨌든 UNITY_MATRIX_IT_MV 에 의한 변환은 모델 공간의 노멀을 뷰 공간(카메라 공간)의 노멀로 바꾸는 용도임.

그럼 UNITY_MATRIX_IT_MV 의 1열과 2열이 아니라 1행과 2행을 가져다 rotation 에 곱하는 의미는 무엇인가… 우선 버텍스 셰이더 코드 두 번째 줄 우변의 코드를 식으로 옮겨 써 보겠슴다.

rotation x UNITY_MATRIX_IT_MV[0]

그리고 UNITY_MATRIX_IT_MV 를 네 문단 앞에서 얘기한대로  transpose(inverse(model x view)) 로 풀어서 치환해보져.

  rotation x UNITY_MATRIX_IT_MV[0]

= rotation x (transpose(inverse(model x view)))[0]

연산자를 보통은 이렇게 쓰지 않지만, 편의상 행렬의 열벡터를 가져오는 연산을 {} 이라 정의하면, 3×3행렬 M 에 대해서 다음과 같이 쓸 수 있겠져.

M[0] = (transpose(M)){0}

(transpose(M))[0] = M{0}

마지막 하늘색 박스에서 transpose(inverse(model x view)) 부분의 계산 결과를 하나의 행렬로 보고 바로 위 변환(두번째 줄)을 적용하면 다음과 같아집니다.

  rotation x (transpose(inverse(model x view)))[0]

= rotation x (inverse(model x view)){0}

그런데 행렬의 첫번째 열을 구하는 연산 M{0} 은 행렬 M 에 벡터 (1, 0, 0) 을 곱해주는 것과 같습니다. 따라서 다음과 같이 쓸 수 있게 되져.

  rotation x (inverse(model x view)){0}

= rotation x inverse(model x view)) x (1, 0, 0)

즉, 버텍스 셰이더 두 번째 줄 코드의 내용은 rotation x inverse(model x view) x (1, 0, 0) 이라는 식과 같다고 할 수 있겠네여. 이 식을 풀어 써 보면 벡터 (1, 0, 0) 를 model x view 행렬의 역행렬에 곱하고, 이걸 다시 rotation 에 곱해주는 식 되겠슴다.

model x view 행렬은 모델 로컬 공간에서 뷰(카메라) 공간으로 변환시켜주는 행렬인데.. 이것의 역행렬이니까 카메라 공간에서 모델 공간으로 변환시켜주는 행렬이져? 따라서 여기에 곱해주는 벡터 (1, 0, 0) 은 카메라 공간상의 벡터로 봐야 함다. 카메라 공간상에서 벡터 (1, 0, 0) 는? 화면상의 가로축이네여. 즉 식의 뒷부분은 화면상의 가로축을 모델 공간의 값으로 변환합니다.

rotation 은 저으기 위에서 얘기한대로 모델 공간에서 탄젠트 공간으로 변환시켜주는 행렬입니다. 바로 위에서 화면상의 가로축을 모델 공간상의 값으로 변환했는데, 여기에 rotation 을 곱해주면 다시 이 모델 공간상의 값이 탄젠트 공간의 값으로 바뀌는거죠. 결과적으로 이 식은 화면상의 가로축을 탄젠트 공간의 값으로 변환하게 됩니다.

같은 방식으로, 버텍스 셰이더의 세 번째 줄은 화면상의 세로축을 탄젠트 공간의 값으로 변환하게 되죠. 이렇게 구한 두 벡터는 버텍스 셰이더 출력값에 저장되어 픽셀 셰이더로 넘겨지게 됩니다.

픽셀 셰이더 코드의 해석은.. 일단 픽셀 셰이더가 넘겨받은 두 값 i.TtoV0 와 i.TtoV1 부터 봅시다. 얘네들은 바로 위 버텍스 셰이더에서 넘겨준 카메라공간의 가로와 세로축 방향을 나타내는 벡터입니다. 카메라 공간의 가로 세로 이러니 장황한데 좀 더 간단히 얘기하자면 화면의 가로와 세로 방향을 나타내는 벡터라는거져. 근데 이 값은 어느 공간상의 값이다? 넵. 탄젠트 공간.

이 탄젠트 공간상의 값과 내적을 해 주는 normal 벡터는 어디서 온 값이냐.. 이건 가져온 코드에서는 안 보이지만 노멀맵에서 언팩한 값이져(frag 함수 몸체 첫 줄의 UnpackNormal 호출 결과로 받는 값). 얘는 애초에 탄젠트 공간의 값임다. 왜 탄젠트 공간이냐면 탄젠트 공간이니까 탄젠트 공간이라 한 것인데 왜냐고 물으시면.. 이 아니라 이 부분에 대해서 풀어 쓰려니 분량이 너무 늘어날 거 같아 일단 UnpackNormal 의 결과값은 탄젠트 공간이다..라고만 해 두고 넘어가져. 탄젠트 공간상의 무슨 값? 노멀맵에서 가져온거니 노멀값이겠져? 정확히는 픽셀 셰이더에 의해서 그려지고 있는 해당 점의 노멀 방향값이져.

결국 이건 화면의 가로와 세로 방향 벡터랑 해당 점의 노멀 방향 벡터를 내적하는 게 되는데, 둘 다 탄젠트 공간의 값입니다. 따라서 애초에 착각했던것처럼 뭔가 변환은 아니게 되는거져. (굳이 결과에 끼워 맞추자면 텍스처의 uv 공간으로 변환하는거라고 할 수 있을지도 모르겠지만 이러면 더 헷갈리니까 이건 제쳐두고.)

두 벡터의 노멀을 어떤 의미로 봐야하는지를 알기 위해 두 벡터의 기하학적 정의를 한 번 살펴보도록 하져.

‘두 벡터의 내적’의 기하학적 정의는 두 벡터의 각각의 길이 값을 곱한 것에 두 벡터 사이각의 cosine 값을 곱한 값..입니다. 이 정의에 따라 픽셀 셰이더의 식을 해석해 보면, 버텍스 셰이더 코드의 첫째 줄은 화면의 가로방향 벡터와 노멀맵에서 가져온 노멀 벡터를 내적한 값을 계산하는 거져.

그런데 화면의 가로 방향 벡터는 식 사이에 숨어 있는 값 (1, 0, 0) 로 등장한 이후 픽셀셰이더로 넘어오기 전까지 계속해서 회전 변환만 적용된 상태입니다. 따라서 최초에 1이었던 길이는 픽셀 셰이더에서 내적 할 때 까지도 여전히 유지되고 있져. 방향 벡터는 보통 길이 1을 유지하도록 하는 경우가 많은데, 특히나 내적을 계산할 때 두 벡터 중 하나가 가 길이 1인 방향 벡터일 경우, 위에서 언급한 벡터 내적의 기하학적 정의에 대입해보면 나머지 한 벡터의 길이값에 두 벡터 사이각의 cosine 값을 곱한 것과 같은 값이 됩니다. 길이가 1인 벡터를 어떤 기준이 되는 벡터라고 보면, 이 기준 벡터와 다른 벡터를 내적한 값은 기준 벡터를 축으로 두었을 때 다른 벡터의 해당 축에 대한 좌표 성분값을 얻는 연산으로 볼 수 있는 것이져. 이런 관점에서 보면, 픽셀 셰이더의 첫째 줄과 둘째 줄 코드는 노멀 벡터를 화면상의 가로축과 세로축을 기준 축으로 하는 좌표계의 좌표값으로 변환하는 코드라 할 수 있슴다. 같은 말이긴 한데 좀 현실세계스러운 비유를 들어 설명하자면, 화면의 가로축과 세로축을 자(ruler)라고 생각하고, 노멀 벡터의 끝이 가르키는 점의 위치가 가로축과 세로축 방향으로 얼마나 되는지를 재어서 그 값을 vn.x 와 vn.y 에 저장하는 것이 픽셀셰이더 첫번째와 두번째 줄이 하는 일인 것이져.

그런데 노멀 벡터 역시 방향벡터로, 길이는 1을 유지하고 있습니다. 혹은 연산 도중 1이 아닌 값이 되게 되더라도 마지막이나 사용할 때에 1이 되도록 조정해주져. 방향 벡터이니 시작점을 원점에 두고, 길이가 1인 모든 벡터의 끝점에 점을 찍어보면… 넵. 아시다시피 이 점들의 집합은 원점을 중심으로 하는 반지름 1인 구면을 이루게 되져. 노멀 벡터들은 이 구면상의 값이라면 어떤 값이든 될 수 있는데, 픽셀 셰이더의 첫째 줄과 둘째 줄에 의한 연산을 하고 나온 결과값 (vn.x, vn.y)은 구면상의 각 점의 가로 세로 좌표값이니 이 값들을 2D 평면에 찍어보면 원점을 중심으로 반지름 1인 원을 가득 채우게 되져.

픽셀셰이더 두번째 줄 까지 계산한 결과를 좌표로 왜 2D평면상에 다시 점을 찍어보느냐는.. 이런 가정을 해 보져. 점을 찍을 평면에 그림이 그려져있다고 말이져. 그리고 해당 좌표에 점을 찍는 대신 그 점을 찍을 위치의 색깔을 가져다 다른 데 쓴다면? 넵. 그게 바로 픽셀셰이더 세번째 줄에서 tex2D 함수가 하는 일임돠.

그런데 tex2D 함수를 자세히 보면 파라미터로 넘겨주는 vx 를 바로 사용하지 않고 가공을 해 주고 있져? 이건 vx 의 x 와 y 값이 -1 에서 1 사이의 값이기 때문이져. 원점을 중심으로 반지름 1 짜리 원 내부의 점들이니까여. -1 에서 1 사이의 값에 0.5 를 곱해주면 -0.5 에서 0.5 사이의 값이 되고, 여기에 다시 0.5 를 더해주면 0 에서 1 사이의 값이 되져. 이렇게 하는 이유는 바로 텍스처 좌표가 0 에서 1 사이의 값을 가져야 하기 때문인 것이라는 거.

이렇게 구한 값을 픽셀셰이더의 출력값으로 사용하게 됩니다. 픽셀 셰이더라는 게 한 점을 그릴 때 그 점의 색깔을 무슨 값으로 하느냐를 계산하는 물건이져. 이 MatCap 셰이더의 경우는 해당 점의 노멀 방향벡터를 화면의 가로세로축이 만드는 평면상으로 옮겨서, 그 평면상에 붙어(…)있는 _MatCap 텍스처의 그 위치의 색깔로 화면에 그려주는 셰이더가 되는 겁니다. 그림을 그리면 좀 이해가 편할텐데 귀찮아서(…) 말로 설명을 하자면 노멀이 화면의 가로세로축과 완전히 수직한 경우, 즉 화면을 뚫고 나오거나 뚫고 들어가거나 하는 경우는 화면의 가로세로축에 의해 만들어지는 평면의 중점, 다시 말해 _MatCap 텍스처의 한 가운데 점의 색깔로 그려집니다. 그걸 기준으로 좌우로 기울어지든 상하로 기울어지든, 중점으로부터 좌/우측 혹은 아래/위쪽 위치에서 픽셀을 얻어 그 색깔로 그려지게 되는 것이져. 그리고 이쯤 되면 눈치 챌 수 있겠지만, _MatCap 텍스처에서 사용되지 않는 부분이 있는데 그건 바로 텍스처의 사각형 영역에서 이 영영을 꽉 채우는 원의 바깥부분, 즉 네 귀퉁이 인근 부분의 픽셀들이져. 얘네들을 가리키려면 해당 노멀의 길이가 1이 넘어야 하는데, 정상적인 노멀은 항상 길이가 1이므로 애는 절대 참조될 수 없기 때문에 영향을 미치지 않는 텍셀들이 되는거져.

어째 막판으로 갈수록 설명은 상세해지면서 내용은 더 날림이 된 거 같긴 한데, 일단 한 번 끊어야겠기에 여기까지 쓰고 일단락해야겠슴다. 나중에 좀 더 알아먹기 쉽게 고칠 수 있는 방법이 생각나면 그때 고치져 뭐. 끗.

집에서의 컴퓨팅 환경을 좌식에서 입식으로

페북에 썼던 글인데.. 찾아가기도 번거롭고 어쨌든 진행을 해야 할 거 같아서 진행상황 확인 및 기록용으로 블로그 포스팅으로 옮김.

일단 입식 환경에서 스탠딩데스크면의 높이, 그러니까 서 있는 바닥면부터 실제 키보드랑 모니터가 올라가는 테이블면까지의 높이차는 1100mm (110cm)

현재 사용하고 있는 좌탁의 높이가 395mm 이므로, 1100mm 에서 이걸 빼면 추가로 구입해야 하는 좌탁의 높이는 705mm 인 것이 된다.

좌탁 구매는 지금 회사에서 쓰고 있는 스탠딩데스크용도 좌탁 주문한 이곳에서. 원하는 높이를 지정해서 주문할 수 있음. 상판도 사무용가구급까지는 아니더라도(..가격 때문에 재질에 한계가 있으니) 사무실서 1년 써 봤는데 무난하게 만족스러웠음.

현재 쓰는 좌탁 가로세로 크기가 1500mm x 600mm 짜리인데, 좌탁 왼쪽편에 있는 저장용 본체…를 오른쪽으로 꺼내서 배치할까 함. 저장용이니까 이건 한 번 세팅해두면 하드디스크 끼고빼고 할 일은 거의 없고, 그나마 저장용으로 쓰는 2테라 하드 8개 중에 여섯개는 전면에서 착탈이 가능하도록 구성을 했지만.. 쓰다보니 결국 하드 뗏다 붙였다 할 일이 몇달에 한 번은 발생하더라고. 그래서 조금이라도 뚜껑 따기 쉽도록 오른쪽 공간으로 이동.

문제는 이렇게 해서 좌탁을 왼쪽으로 완전히 밀어 벽에 붙인 다음 위에다 높이 705mm 짜리 좌탁을 올리면.. 왼쪽 벽면의 베이스랑 기타 걸어둔거랑 충돌이 있다. 그래서 705mm 좌탁은 아래 깐 거랑 같은 가로 길이 1500mm 짜리는 안되겠고, 300mm 줄여서 1200mm x 600mm 짜리 상판으로 주문해야할듯.

이렇게 되면 705mm 짜리 상단 좌탁 아래쪽에 현재 쓰는 39인치 모니터가 들어가야 되는데… 아슬아슬하겠지만 일단 놓을수는 있을듯. 대신 지금 모니터 상단에 올려둔 센터스피커랑 키넥트 카메라는 벽쪽 선반에 올리던가 해야할듯. 그리고 이런 세팅이라 좌탁 주문은 5발다리가 아닌 4발다리로 해야겠는데.. 23인치 모니터 세 대 정도 올리는 정도는 버텨주겠지.

사실 스탠딩 환경 구성 진행이 갑작스럽게 본격화 된 건 중고 23인치 모니터를 오늘 싸게 구하면서 촉발된 감이 없지 않은데.. 뭐 어쨌든 모니터를 구해야겠다고 생각은 했고 21인치 네 개냐 40인치급 UHD 하나냐를 고민했었는데 지금 쓰는 39인치 크기를 생각하면 확실히 이건 데스크탑용으론 좀 불편한 감이 있어서 40인치 UHD는 드랍. 그리고 21인치 신품을 싼 걸로 맞추면 40인치급 UHD모니터 하나랑 비슷해지는데.. 굳이 그럴 필요가 있나 싶기도 하고 그렇게 하면 스탠드도 따로 맞추고 이래저래 귀찮음. 왜 4개였냐면.. 이건 기원이 좀 오래되었는데 작업할 땐 일반적인 멀티모니터로 쓰다가 Eyefinity 로 유사 UHD 환경을 세팅해 써볼까 하던 때가 있었던 적에 나온 생각이라.. 근데 이미 UHD 모니터가 일반화된 마당에 굳이 Eyefinity 같은 걸 쓰는 건 삽질이고…그리고 일해라AMD 드라이버 부서. 니네 Eyefinity 세팅 여전히 까다롭고 버그 많다. 여튼 그래서 쿼드러플모니터 세팅도 드랍.

유니티든 언리얼이든 요즘 엔진 에디터 돌릴 때 싱글모니터는 확실히 불편하고, 듀얼은 되야 쓸만하다 싶음. 트리플은 두 개에다 에디터 띄우고 나머지 하나는 도큐먼트나 웹페이지 띄워서 보려고. 그래서 트리플 모니터. 트리플만 되어도 별도의 브래킷 스탠드를 구해서 써야하지 않을까 하는 생각을 좀 해봤는데… 가격이 셉니다. UHD모니터 한 대 가격에서 조금 빠지는 정도. 그래서 드랍.

어차피 완전히 자유롭게 움직이며 쓸 건 아니고.. 옆으로 밀면서 서로 간격 조정 정도만 하면 되니까 알루미늄 프로파일 같은 걸로 슬라이딩 가능한 세팅으로 직접 구성해보면 어떨까 하는 생각도 들었음. 근데 이건 지금 구성하려는 좌탁 상판이 필요한 강도가 나올지, 그리고 내가 직접 설계조립해서 과연 실용성있는 결과물이 나올지도 의심스러운지라.. 걍 수동으로 밀어가면서 쓸라고. 아직 로또 안됐음.

현재 좌탁 위에 올라가있는 1) 39인치 모니터 2)23인치 구형 LCD모니터 3)24인치 UHD모니터 4)파일서버/이런저런스위치등등 중에서 아쉬운대로 데스크탑 멀티모니터로 쓰던 23인치 구형 LCD는 위에 올릴 좌탁 다리 공간 문제 등 때문에라도 빼버려야 할 거 같은데… 이게 컴포넌트랑 RGB 입력이 있어서 PS2 랑 구엑박을 물려놓는 용도로 쓴단 말이지. 이걸 책상서 빼버리는 건 거의 확정인데.. 이렇게 되면 PS2랑 구엑박도 같이 빠져버려야 되는 상황이라. 어디 PS2랑 구엑박 따로 놓을 공간이 있음 모르겠는데 지금은 자는 영역도 침범받을 정도로 공간이 부족하니-_- 얘네를 어떻게 할 지가 좀 고민이긴 함. 당장 켤 일은 별로 없는데 아주 가끔 땡길때 켜곤 해서…

스탠딩 환경 구성과는 별개이긴 하지만.. 데스크탑들 저장장치 정리 작업도 같은 시기에 진행해야하는지라 메인데탑용 SATA3 확장 카드, 5.25인치 베이에 장착하는 2.5인치 멀티랙도 주문해야함. 메인데탑 PCIE 슬롯 확인하고 내부 베이 및 SATA포트 사용 상황 확인해서 정리해야된다. 옛날엔 이런 거 신나서 했는데 이젠 귀찮다. 아아…

Ubuntu Software Updater 에서 gitlab 업데이트가 뭔가의 이유로 실패할때…

잘 돌아가고 있는 gitlab 을 뭐하러 업데이트하냐(…) 는 게으름으로 쓰고 있던 8.0.3 이 8.7.0 이 될 때 까지 업데이트를 안 하고 있었는데, 간만에 생각난 김에 업데이트나 하자 싶어 Software Update 를 켜 보니 이런저런 시스템 업데이트등등과 함께 gitlab 업데이트가 보이길래 일단 gitlab 을 제외한 나머지것들만 업데이트.

그리고 다 설치가 된 다음 gitlab 만 업데이트를 하는데, 아니나다를까 하다말고 중간에 뭔가 에러가 뜨면서 제대로 설치가 안된다. 정확히 어떤 메시지가 뜨는 지는 못 적어놨는데.. 대충 ‘뭔가의 이유로 실패했는데 뭔 이윤지는 안알랴줌’ 느낌의 에러 메시지.

근데 이게 몇 번을 설치해도 뭐 때문이다 보여지는 게 없고.. 로그가 어디 남나 하고 뒤져보니 처음 검색해서 나온 파일 위치에 파일은 있는데 사이즈가 0 바이트. 이 쯤 되니 더 찾기도 귀찮고 ‘어쩌라고? 운영체제부터 밀고 새로 설치하리?’ 싶은 생각이 들기 시작.

그러다가 시간이 늦어 일단 자고, 다음날인 오늘 다시 몇 번 다시 업데이트를 시도하다보니.. 업데이트 윈도 아래쪽에 detail 이라는 클릭 가능한 글씨가 보임. 눌러보니까 설치시 콘솔 출력을 주욱 보여주는데.. 뭐가 직직 찍히다가 희떡 닫히길래 이걸 또 두어번. 결국 화면캡쳐의 도움을 받아 /var/opt/gitlab/backups 디렉토리를 만들다 실패하는 상황이라는 걸 확인함.

단순한 윤모씨는 어..저 경로가 없는거니까 그냥 만들어주면 되지 않을까? 해서 짧은 유닉스 커맨드 지식으로 해당 위치에 backups 디렉토리를 만들었으나.. 여전히 비슷한 메시지가 나오는 상황. 이번엔 backups 아래에 db 라는 디렉토리가 없대네? 다시 만들어줌. 그리고 다시 업데이트.. 그랬더니 이번엔 디렉토리가 있어서 문제라고 나옴-_- 아 이건 아닌가부다.. 그러고선 검색하면 나오겠지 하고 찾아봄.

역시나 같은 건으로 질문한 게 있고, 그 질문에 ‘나도 그런데염’ 하고 질문했던 사람이 스스로 해결방법을 찾아 올려놓은 걸 발견했다.

https://gitlab.com/gitlab-org/omnibus-gitlab/issues/644

해답은 아까 내가 시도했던 거랑 비슷한데.. 다른 건 permission 이랑 소유권  설정을 해 줘야 하는거였음.

mkdir /var/opt/gitlab/backups
chown git /var/opt/gitlab/backups
chmod 700 /var/opt/gitlab/backups

누구한테? git 이라는 계정한테…

어쨌든 저렇게 하니까 잘 됨.

이렇게 굳이 글로 남긴 이유는.. 블로그가 방치 상태가 아니라는 의미로다가(…)

그리고 키핑용 글은 페북에다가 남겨놓을라니까 나중에 검색이 힘들어서, 귀찮아도 따로 글로 옮겨둬야겠단 생각이 들었고,

무엇보다 이걸 나름 키핑해야되는 내용이라고 생각한 건 몇 달이나 몇 년(…) 후에 이런 일을 또 겪으면 분명히 또 삽질을 하고 있을 거 같아서.

나는 리눅스를 높은 빈도로 다루는 환경에 있지도 않고, 예전에 빡시게 유닉스계열 운영체제를 써 본 적도 없다. 필요할때마다 잠깐 쓰고 내던져뒀다가 또 쓸 일 있으면 잠깐 만지고.. 근데 이 텀이 길어서 한 번 만진 후 다음 번에 만질 때 쯤이면 그 전에 하던 거 다 까먹은 상태거든. 그래서 부지런히 적어두기라도 해야 나중에 까먹은 거 다시 떠올리기라도 쉬울 거 같다는 생각이 들었다는 거.

(한 줄 요약 – 늙었음)

블로그 또(…) 재개

스팸봇들때문에 텍큐는 안되겠다 싶어서 내렸다가, 워드프레스로 옮겨가려고 보니 마땅찮은 점이 몇가지..해서 블로그 닫은채로 처박아둔게 한 반 년 된 거 같은데,

애플타르트타탕 레시피 계량 계산 다시 해 놓은거 꺼내 보려고

..만은 아니고 마침 텍큐 업데이트도 되고 있는 거 같고 해서 당분간 다시 써보자 싶기도 하고 레시피도 봐야겠고(…) 해서 다시 열었슴다.

근데 이거 뭐 인코딩 설정이나 이런저런 설정을 좀 만져줘야하는듯. 카테고리 클릭해서 들어가는거 안되고(이건 전엔 되었던거 같은데-_-), 검색은 예전부터 안됐는데 여전히 안되고.

게다가 예전에 쓰던 스킨이 뭐였는지 이름도 기억이 안난다.

..였다가 방금 어찌저찌 해서 스킨은 찾아서 적용.

아 오늘은 어제보다 더 늦게 자게 생겼네. 머리아파서 일찍 자려고 했더니만.. 이래갖고 내일 타르트 재료 사러 나갔다 올 수 있으려나.

오늘의 죽을 뻔 한 이야기

(이제는 연간도 아니고.. 격년간 업데이트 블로그가 되어버린 이 곳을 위해 잠시 묵념(…))

진공청소기가 얼마 전 부터 전원을 넣어도 켜졌다말았다 하길래, 전원스위치가 왠지 헐거운 느낌이 들어 접점부분이 눌리던가 해서 접촉불량 나는게 아닌가 하는 생각이 들었다. 접점부분을 지지하는 탄력이 있는 부분이 부러지던가 해서 제대로 접촉이 안 되는게 아닌가 하는거지. 실제로 옆으로 돌려서 들면 되는데 바로 세우면 안되고 하는 증상은 이러한 추측을 더욱 확고히 하게 하였으며, 기왕 이렇게 된 거 청기와로 돌진한다!..가 아니라 까서 확인을 해 보는 것이 엔지니어로서 훌륭한 자세가 아니겠는가 라고 스스로를 대견해 하며 분해하려고 보니,

본체 나사 다섯 개 중에 두 개의 머리가.. 십자도 일자도 아닌 – – 자 형(1자 홈 가운데 부분이 메꿔진 형태)인 것이라.

일자 드라이버 가운데를 갈아내어 써야 하나 어쩌나 고민만 하며 귀차니즘과 게으르니즘의 합공에 말려 몇 주(몇 달 인거 같기도 하다..) 미루다가, 마침 용산에 들른 김에 공구점으로 뛰어들어 “일자드라이버 비슷한데 가운데 홈 파진 드라이버 없나여?” / “ㅇㅇ 이거임 크기 고르셈.” / “오오!” 그래서 제일 큰 사이즈는 혹시나 너무 클까 싶어서 한 사이즈 아래 걸 사 갖고 왔는데.. 집에 와서 청소기 나사홈에 넣고 돌리자마자 뚝-_-하고 팁이 부러지더라. 이게 한 삼 주 전? 이야기.

그리고 그 이후로 다시 이런저런 사정(…)에 밀려 용산행을 미루다가.. 시간이 흘러흘러 계절이 바뀌며 날씨가 풀리고 꽃이 피고 나비가 날아다니는.. 건 내 알 바 아닌데 나방파리도 돌아다니기 시작하네?!?

이 집의 유일한..아니 싸니까 여러가지 단점이 있는데 그래도 이 보증금에 방 두개짜릴 어디가서 구하겠냐며..그런 관점에서는 장점이 여러 단점을 상쇄하고도 남지만, 그래도 용서할 수 없는 단점은 바로 겨울이 아닌 계절에는 나방파리가 시도 때도 없이 등장한다는 거.

나방파리를 사전에 박멸할 길을 몇 가지 시도해 보았으나 대부분 방법이 보이는 수를 상당히 줄일 수 있을 뿐 완전히 없앨 수는 없더라. 나는 돈이 없으니 현실과 빠른 타협을 진행하였고, 그 결과 보이는 나방파리를 보이는 족족 진공청소기로 빨아들여 잡는 방법을 선택하게 됨.

사정이 그러한데 겨울을 벗어나는 이 마당에 진공청소기가 이 모냥이니.. 귀차니즘은 용서해도 나방파리 돌아다니는 꼴은 용서할 수 없다며 갑자기 빡이 돌아 용산으로 튀어간다. ‘저녁시간이긴 하지만 어째저째 아직 문 열었을지도 몰라..’ 하지만 도착해보니 문 닫음. 그것도 저번에 그 드라이버 산 데만 닫고 옆에 다른 공구점 두어군데는 아직 안 닫았던데, 다른 곳에는 한 군데도 그 드라이버가 없더라. 울면서(그날 바람이 심해 맞바람 맞으며 걸으니까 눈물 좀 났음) 500m 정도를 걸어 언젠가 가 보려던 용문시장 동네 빵집엘 들러 빵을 사고, 오는 길에 을밀대에 들러 냉면으로 저녁을 해결. 이게 지난 주 얘기.

오늘에 이르러서야 드디어 용산의 그 공구점에서 가장 큰 사이즈의 – – 자 형 드라이버를 사서는(혹시 안 맞거나 지난 번 처럼 또 부러지면 일자드라이버 가운데 갈아내서 쓰려고 만능톱도 샀다), 집에 와 청소기를 분해해본다.

먼지보소.

분해하면서 구석구석에 보이는 먼지들 좀 떨어내 주고,

대망의 전원스위치 부분. 근데 접점부분이 바로 노출된 형태가 아니라 박스형 케이싱에 닫혀 있는 구조네.

하지만 다행스럽게도 납땜한걸 녹이지 않고 위쪽 케이싱 걸쇠만 잘 벗기면 열 수 있는지라 조심스레 열어보니, 이놈도 안쪽에도 먼지가 가득. ‘하하. 요놈의 먼지들 덕분에 접촉불량이 일어났나보군?’

먼지도 떨고, 접점 부품도 빼내서 빡빡 닦아주고(먼지랑 윤활제같은거 짬뽕인지 하여튼 시커먼게 막 닦여나옴. 이 때만 해도 이거다 싶었다), 혹시나 해서 접접부분들 접촉잘되게 밀어주는 스프링도 꺼내서 살짝 늘려서 탄성 좀 높여주고, 그러다가 그 쪼그만 스프링을 세 번인가 놓쳐서 찾는데 안 보여 쌍욕도 하고. 여튼 이래저래 열심히 떨어내고 닦고 한 다음 재조립. 전원을 넣어보니 잘 된다 싶..다가 다시 금방 같은 증상 발생. 이 산이 아닌가베?

아 뭐지.. 다시 분해해봐야하나.. 걍 싸구려 청소기 수명 다 된거라 치고 다이슨(…)청소기나 알아봐야되나.. 돈 없는데 추석상여금 나오면 그걸로나 어떻게 해 볼수 있을까 그 전엔 무리.. 등등등 오만 생각을 다 하다가 증상은 접촉불량인거 같은데 스위치가 아닌가보다! 좀 더 본격적으로 분해해보자! 는 생각에 이른다. 좀 전에는 완전히 다 열어본 건 아니고 전원스위치부분만 꺼내서 까 본 거였거든.

자 2차 분해 들어갑니다. 다시 전원 플러그 뽑고(이게 중요함.. 오늘 죽을 뻔한게 이거때문이거든) 나사 풀고 본체 커버도 완전히 들어내고, 모터뭉치부분도 들어내서 다시 분해하고, 모터부분도 분해..하다보니 아까는 못 열어본 부분 구석구석에 먼지가 천지삐까리네. 게다가 배기통로부분에 먼지필터같은 게 있는데, 여기에도 먼지가.. 그 뭐냐, 먼지가 너무 꽉차서 고밀도로 압축되어 마치 부직포같은 질감이 되어버린-_- 그런 부분이 등장. ‘얘가 바람 나가는 걸 막아서 접촉불량이 났나? 그럴 리가..’ 라면서도 어쨌든 연 김에 다 청소하자 싶어서 탈탈탈 털고 다 털고 완전히 털고, 전임 GK 못 털어서 아쉬웠던 부분을 여기서 다 해소하려는 듯 털어버린 다음, 다시 조립을 하는데..

하는데, 어라?

전원스위치쪽으로 이어진 선 중에 하나가 심하게 눌려 있는 걸 발견함. 잡았다 요놈! 네놈이 원흉이구나. 선이 심하게 눌려 끊어질락말락하면 충분히 접촉불량을 일으킬 수 있으니까 증상이랑 일치도 하고. 그래서 이땐 얜줄 알았어요.(아니었음)

어쨌거나 발견한 놈은 잡아야지. 처치에 들어간다. 인두랑 납을 꺼내고 작업용 접이상도 꺼내서 세팅하고 인두를 전원에 물린다. 전선 눌린 부분을 절단하고 피복을 양쪽 5mm정도 길이만큼 벗겨낸 다음 납땜 들어가기 전에 잊지 말고 수축튜브를 잘라 전선 한 쪽에 끼워 저으기 안쪽으로 밀어넣어둔다. 이제 본격 납질! 납을 조금 녹인 다음 이걸 전선 끝에 발라주고, 반대쪽 전선 끝에도 납을 발라주고. 작업은 혼자 하고 나는 손이 세 개가 아니니까 전선 한 쪽은 작업대 바닥에 테이프로 붙여 준 다음 나머지 전선 한 쪽과 인두를 각각의 손에 할당하고, 전선의 납이 묻은 부분끼리 맞댄 다음 인두로 마무리. 음 좋다. 납땜질은(전문가 수준엔 한참 못미치지만) 적어도 이제 써먹을만큼은 늘었구나 싶어 혼자서 잠시 만족 5초 정도. 한 쪽으로 밀어 둔 수축튜브를 납땜한 부분을 덮도록 옮긴 다음 라이터..를 켜니까 아놔 이놈 가스 다 나갔네. 어떻게 해야되나 고민하다가 얼마전에 찾은 케익 초 불 붙일때 쓰는 대형 성냥 모양 라이터를 구석에서 찾아 낸 걸 기억하고 갖고와보니.. 이놈도 가스가 다 됐는지 불이 비실비실..켜서 잠깐 지지고 꺼지면 다시 켜고 반복을 수십차례 해서 어찌어찌 수축튜브질도 마무리. 아 힘들었다! 하지만 재조립이 남았지..

어쨌거나 이번엔 성공적일거야! 를 되뇌며 조립완료! 전원인가! 위이잉 소리가 훨씬 커진 느낌(막힌 먼지 다 떨어냈으니까) 근데 또!!! 증상이 다시 발생!

다이슨을 한 5분 정도 그리워하는 시간을 다시 가진 다음,

불현듯 떠오르는 생각인 즉슨,

청소기를 들어올리다가 전원코드를 밟아먹거나 하면서 ‘아 이거 전원선 끊어지면 어떡하지?’ 라고 몇 번이나 걱정했었다..라는 거-_-

떨리는 마음으로 청소기를 다시 들고, 전원선이 본체랑 만나는 부분 근처를 붙잡은 다음 전원을 넣고, 전원선을 움직여보니 전원이 들어왔다가, 다시 전원선을 반대방향으로 움직이니 꺼졌다가, 또다시 아까 방향으으로 움지이니 켜졌다가 아 ㅅㅂ 유레카!!!

흥분된 마음으로 다시 세 번째 본체 분해를 시작합니다. 본체 열고 모터뭉치 덜어내고, 전원기판쪽을 만지작거리며 ‘이거 단선된 부분은 전원선이 본체로 들어오는 부분 근처인거 같은데, 어느부분을 얼만큼 잘라야 잘 잘랐다고 소문이날까’ 쯤을 생각하는 찰나,

찌리릿~

우허허허헉?!??! 인지 뭔지 하여튼 괴성을 내뱉으며 손에 들고 있던 걸 던져버리고 보니,

청소기가 순간적으로 돌아가다가 멈추고 있고,

전원플러그는 전원콘센트에 연결되어 있고

전원플러그는 전원콘센트에 연결되어 있고

전원플러그는 전원콘센트에 연결되어 있고

ㅇㅇ 맞음. 이번엔 정말 확실하다 싶어서 흥분한 나머지 분해하기 전에 전원플러그 뽑는 걸 깜빡했네요??

스스로에게 쌍욕을 하며 전원플러그부터 냅다 뽑고,

작업재개-_-

아까와 같은 요령으로 이번엔 전원선 두 가닥을 전부 자른 다음 납땜하고 수축튜브질. 납땜질은 반복학습주기에 딱 들어맞을 때에 다시 실습-_-에 들어가서 그런지 좀 전 보다 훨씬 이쁘게 완성됨. 여기서 다시 뿌듯뿌듯 해 하면서, 얼마전에 냉장고 도어 경첩부분 깨진거 케이블타이로 수리해낸걸 떠올리며 주말에 재활용센터 수리 알바나 뛸까 이런 뻘생각도 잠시 하고(…). 근데 수축튜브 지지려고 보니 라이터 가스는 다 떨어지고, 밖에 나가기는 귀찮고..아 어째야되나 하다가 결국 제과점 케익칼이랑 같이 처박아둔 생일초 불 붙이는 길쭉한 성냥(아까 얘기한 성냥형 라이터 말고 진짜 성냥)이 생각나서 찾아보니 있다. 이거 써야지. 잘 되네-_- 근데 두 번 하고싶지는 않다. 라이터 하나 사 두고.. 성냥형 라이터 저거는 가스 채워야 할텐데 가스 채우려면 어떻게 해야하지? 이런 생각을 하면서..

세 번째 조립에 들어감. 이쯤되니 조립도 무슨 게임 스피드런 뛰는 거 같더라-_- 금새 조립해낸 다음 전원 연결 먼지통부착 등등등 전원인가! 잘 된다! 요리조리 돌려봐도 꺼지지 않고 잘 된다 끼얏호!

.
.
.
참 기쁘기도 하겠다 드라이버며 사느라 들인 돈에 용산 몇 번 왔다갔다하느라 들어간 시간에 한 번 죽을뻔도 했는데.

근데 다 해놓고 뿌듯해 하는 걸 보면 이런 짓 하는 게 천성이다 싶기는 함ㅋ

애플 타르트 타탱(Apple Tart Tatin)

(지인간의 레시피 공유를 목적으로 작성 된 글이며, 저작권에 문제가 될 경우 저작권자께서 요청 주시면 삭제하도록 하겠습니다.)

파비앙 베르또 레시피. (월간 파티시에 2011년 11월호)

(먼저 잡지에 나온 원래 레시피를 그대로 옮기고.. 아래에 제가 만들 때 바꿔서 쓴 부분들을 따로 기재하겠음)

A 캐러멜라이즈드 애플

설탕 1000g
물 200g
버터 250g
사과 4개

1. 냄비에 설탕, 물을 넣고 골든 브라운 색이 날 때 까지 끓인다.
2. 1 에 버터를 넣고 섞은 다음 지름 17cm 의 (원형) 타르트 틀에 붓는다.
3. 껍질을 벗긴 사과를 4조각으로 자른 다음 2에 넣는다.
4. 130도 오븐에 넣고 1시간30분간 익힌다.

B 스위트 반죽

버터 425g
슈거파우더 235g
아몬드파우더 75g
박력분 615g
달걀 3개
소금 2g

1. 차가운 상태의 버터에 함께 체 친 슈거파우더, 아몬드파우더, 박력분을 넣고 섞는다.
2. 1 에 풀어놓은 달걀과 소금을 넣고 반죽한다.
3. 냉장고에서 2시간 동안 휴지한다.
4. 3 을 두께 0.2cm 로 밀어 편다.

마무리

생크림 적당량
식용 금박 적당량

1. 오븐에서 꺼낸 A(캐러멜라이즈드 애플)에 B(스위트 반죽)를 씌운다.
2. 170도 오븐에서 B(스위트 반죽)가 익을 때까지 굽는다.
3. 구워져 나온 2 를 뒤집은 다음 휘핑한 생크림과 식용 금박으로 장식한다.

그럼 이런 모냥새.

사용자 삽입 이미지

여기서부터는 지난번에 제가 만들 때 바꾼 부분들.

원래 레시피의 17cm 원형틀 대신 24cm x 9cm 의 사각틀로 변경.

캐러멜라이즈드 애플은 절반 분량(사과 2개분)으로 만들었는데, 두 번 실패하고 보니 캐러멜 양이 너무 많은 거 같아서 캐러멜은 반으로 줄인데서 다시 25% 더 줄였음. 그래도 좀 많다 싶던데, 이건 너무 줄이게 되면 캐러멜에 사과가 푹 잠기지 않아 제대로 익지가 않을 거 같아서 너무 줄일수는 없더라능. 이건 한번 더 만들어보고 다시 레시피 정리하려다가 일단 올려놓고 나중에 다시 만들게 되면 수정하든지 할게요.

캐러멜도 먼저 부은 다음 사과를 올리는 게 아니라 사과를 틀에 넣고 위에다 부어서 익혔구요.
 
사과를 4 조각으로 자르라는 부분도 16조각으로 변경(세로로 8쪽을 낸 다음 그걸 반으로 잘라 씀)

스위트 반죽은 원래 레시피 분량의 1/3로 반죽한걸 절반으로 나눠 쓰니까 사각틀에 딱 맞았어요. 애초에 1/6 로 반죽하지 않은 건 달걀 갯수 맞추느라. 근데 지금 생각해보니 반죽 두께가 0.2cm 보다는 확실히 더 두꺼웠던듯함.

제일 심각한 레시피 변경은 이 부분인데… 레시피대로 다 만들고 나서 꺼낸 다음 식혀서 뒤집으면… 캐러멜+사과즙이 줄줄 흘러내려요.ㅠㅠ 원래 분량대로 만들어야만 사과에 캐러멜이 이쁘게 코팅되는 모양으로 완성되는건지는 그대로 안 만들어봐서 모르겠는데, 여튼 그렇게 캐러멜을 다 흘려버리고 나면 모양새가 너무 안 좋거든요. 그래서 저는 뒤집을 때 망 아래다 그릇을 받쳐서 캐러멜을 받은 담에.. 이걸 절반정도 분량으로 졸여서(안 졸이고 그냥은 점도가 맞질 않음) 붓으로 위에다 칠해줬음. 귀찮긴 하지만 이래야 모양이 났어요.

그럼 여기서 수정한 레시피로 다시 한 번 정리.

A 캐러멜라이즈드 애플

설탕 375g
물 75g
버터 94g
사과 2개

1. 껍질을 벗긴 사과를 16조각으로 자른 다음 24 x 9 크기 사각 틀에 넣는다.
2. 냄비에 설탕, 물을 넣고 골든 브라운 색이 날 때 까지 끓인다.
3. 2 에 버터를 넣고 섞은 다음 1에 붓는다.
4. 130도 오븐에 넣고 1시간30분간 익힌다.

B 스위트 반죽

버터 142g
슈거파우더 77g
아몬드파우더 25g
박력분 205g
달걀 1개
소금 0.7g

1. 차가운 상태의 버터에 함께 체 친 슈가파우더, 아몬드파우더, 박력분을 넣고 섞는다.
2. 1 에 풀어놓은 달걀과 소금을 넣고 반죽한다.
3. 냉장고에서 2시간 동안 휴지한다.
4. 3의 절반을 0.2cm ..인지는 모르겠고;; 틀에 맞는 크기가 되게 밀어 편다.

마무리

1. 오븐에서 꺼낸 A(캐러멜라이즈드 애플)에 B(스위트 반죽)를 씌운다.
2. 170도 오븐에서 B(스위트 반죽)가 익을 때 까지 굽는다. (원본 레시피에는 시간이 안 나와 있는데, 제가 만들때는 한 15분 정도? 였던 듯.)
3. 구워져 나온 2를 잠시 식힌 다음, 망 아래 그릇을 받치고 뒤집어 흘러내리는 캐러멜사과즙을 받는다.
4. 3의 캐러멜 사과즙을 불에 올려 1/2 정도로 졸인 다음, 붓으로 사과 위에 발라 준다. (한 번은 귀찮아서 바르질 않고 그냥 들이부었는데, 그러면 설탕폭탄이 됩니다. 못먹어여;; )

끝!

음악과 함께하는 격동의 근현대(개인)사 rev.2

차암으로 오랜만의 블로그 포스팅.

이 글의 초판은 “음악과 함께하는 격동의 근현대사

새벽에 작업한답시고 컴터를 켜고 앉아있는데 일 진도는 안나가고 영 엉뚱한 짓만 하던 중에..
갑자기 업타운의 ‘다시 만나 줘’ 가 듣고싶어지더란 말씀.

들으면서 든 생각이.. ‘맨날 싸구려 헤드폰으로만 듣다가 (역시 싸구려지만)우퍼 딸린 스피커로 들으니 베이스 쿵쿵 울리는게 다르네. 좋구나~ 근데 왠지..왠지 대학 새내기시절 생각이 나네…’
..해서, 늘 하던것처럼 nowplaying 태그 트윗질을 할까 하다가.. 생각해보니 예전에 특정 시기와 특정 곡을 연관하여 글을 썼던 게 생각나서 보니..

다시 만나 줘..가 없네요?!

그렇잖아도 그 글 쓸때 좀 급히 쓰느라 뭔가 빠진 거 같지만 일단 쓰고보자..싶던 생각이 스쳤던 것도 같은 느낌이..

여튼, 서론이 (늘 그랬듯이) 매우 길었지만, 그때 쓴 글에다 슬쩍 내용추가만 할 까 하다가.. 이러저러한 이유로 새로 쓰는 게 나을 거 같아서..

(그리고 굳이 아직 블로그 죽인 거 아니라는 증빙도 할 겸…)

새로 글을 쓰겠음.

하지만 본 내용은 원래 글에서 거의 긁어다 붙인거라는 점은.. 그냥 넘어갑시다;;

어쨌거나 개인적으로 특정 시기(97년도 봄..이라던가 고3 가을..때라던가 등등등)를 생각하면 떠오르는 곡 혹은 앨범이 있고.. 반대로 특정 곡을 들으면 어떤 시기가 기억나는 경우도 있는데(오늘처럼),

그냥 그때그때 생각 날 때 마다 음악만 듣고 말 게 아니라 정리 해 두는 것도 재미있을 거 같아서.. 써 봅니다.

.시작.

1989년 국민학교5학년 겨울방학 – C.C.Catch ‘Backseat of your Cadillac’
– (89년 말인지 90년도 초였는지 모르겠다.. 5학년 겨울방학때였던 건 확실한데. 어쨌던 바로 그 89/90 Winter Season!ㅋ) 당시 Goldstar 휴대용카세트플레이어 번들 테잎에 수록되었던것으로 추정되는 곡. 5학년 겨울방학때 보이스카웃 머시기로 대구 팔공산 갔을때 귀에 내내 꽂고 있었던 터라..
회사 다니기 전까지 이 곡 제목이 도대체 뭔지 모르고 있었는데, 2000몇년도던가 나우누리(…)에서 돌아댕기던 ’80년대 유로댄스’라는 곡모음집에서 발견. 감동의 재회.

1990년 국민학교6학년 – 조정현 ‘그 아픔까지 사랑한거야’
– ….뭔가의 이유 때문에 단체로 벌 받으러 마루에 우루루 나가서 서 있는 동안, 친구 녀석이 이 노래를 부르 던 게 생각남. 근데 그때는 개사해 부르던거였어(…)

1992년 중2 여름 – 서태지형님의 1집
– 설명이 필요없심. 카세트 플레이어에 걸어놓고 테이프 처음부터 끝까지 ‘Yo! Taiji’ 부터 ‘Missing’ 까지 들으며 지냈…

1992년 중2 겨울방학 – 윤종신 2집 ‘Sorrow’
– 타이틀곡 ‘너의 결혼식’ 보다는 ‘이별연습’이 더 좋았다. 인순이 누님의 원곡이 있다는건 한참 뒤 노래방에서야 알게 되었(노래방 책은 곡제목정렬이지욤)지만..

1993년 중3 가을 – 015B 4집 ‘The Fourth Movement’
– 요 앨범도 통째로. 왠지 모르게 진주로 시험보러갔던때가 생각남.

1993년 중3 겨울방학 – Mr.2 ‘하얀겨울’
– 겨울방학이 되자마자 장만한 486SX 시스템에 딸려온 노래방 프로그램으로 무던히도 불러댔었지.

1994년 고1 봄 – Ace of Base 1집 ‘The Sign”
– 덥지도 춥지도 않은 날씨에 창문을 열어놓으면 기분좋게 살랑거리는 바람. 침대에 누워 맘에 드는 잡지를 읽으며 듣다가 잠들곤 하던 앨범. 요것도 통째로 사랑해주었다. 명반이지..

1994년 고1 가을 – Crash 1집 ‘Endless supply of Pain’
– 교실이데아의 ‘그 부분’을 부른 Crash의 보컬 안흥찬.
교실이데아를 통해 Crash 를 알게되고, Crash 가 부른 Smoke on the water 를 통해 Deep purple 을 알게 된.. 희한한 사슬의 연결고리.

1996년 고3 여름 – 서지원 ‘내 눈물 모아’ 삐삐밴드 2집 ‘불가능한 작전’
– 고3여름방학 보충수업 후 저녁까지의 자습시간… -ㅂ-

1996년 고3 겨울 – 노땐스 ‘골든 힛트 일집’
– 신해철,윤상 콤비의 프로젝트 앨범. 첫 앨범 제목이 ‘골든 힛트 일집’ 이다. 그리고 그 이후로 노땐스의 앨범이 나오지 않았지. 뭐, 프로젝트 그룹 같은 거(..같은 거 가 아니라 바로 그건가?)니까.

1997년 봄 – 업타운 ‘다시 만나 줘’
– 대학교 새내기때.. 미적분 시험 준비인지 과제인지 한답시고 두껍고 무거운 갈쿠리(Calculus) 책을 수원 학교에서 인천 친구 집까지 들고 가서 열심히(나름) 보면서 ‘뭔 소린지 하나도 머리에 안 들어와!!!’ 라고 방심하는 사이..에 머리속을 가득 채우고 있었던 바로 그 곡.

1997년 여름 – 자자 ‘버스 안에서’
– 이건 좀 이상한데, 이 곡이 포함된 앨범 발매는 97년이 아니라 96년 가을인데.. 이 곡 뜨는데 한참 걸린건가? 아님 내 기억이 뭔가 잘못된건가.. 여튼 이 곡과 연결된 내 기억은 분명 97년도 여름..여름방학때.

1998년 대학2학년 늦가을 – Chet Baker ‘As time goes by’
– 요새는 연락이 매우 뜸(..몇년에 한 번 정도…)해진 박정남양께 감사의 말을 전합니다.

1999년 대학3학년 봄 – S#arp ‘Lying’, 윤종신 7집 ‘후반(後半)’, 신해철의 ‘Monocrom’
– 대학 2학년 겨울방학때 홍렬이하고 같이 윤종신 7집앨범 콘서트에 갔었다. (지금 얘기지만.. 그때는 그렇게 낯설어보이던 인터컨티넨탈호텔 앞 길이, 10년 후엔 매일 다니는 출근길이 될 줄은..ㅋㅋㅋ) …앨범 수록곡은 한곡도 모른 상태로. 종신이형님이 빤히 보이는 앞쪽 자리였거든.(..두번째로 비싼 좌석이던가..홍렬이가 산 표라 잘 모르겠심) 그게 미안해서(?)였는지 3학년 봄에 시디까지 구입해서 열심히 들었는데..노래 좋더라. 하긴 종신이형님은 중학교시절부터 지금까지도 듣고 있으니.. 영향력을 미친 기간으로 보면 최장기로구나! 하여튼 이 앨범은 같은 시기 해철옹의 Monocrom 앨범과 함께 두 앨범과의 진하고 끈적한 삼각관계를 구성하던 한 축이었음.
S#arp ‘Lying’ 은 동대문으로 옷 사러 석호녀석과 갔던 때가 생각남.

1999년 대학3학년 가을 – Smile.dk ‘Butterfly’
– 말이 필요없는 DDR 의 명곡. 채보가 널널해 나같은 몸치도 퍼포 넣어가며 플레이 가능(…) 물론 그 시절 얘기임ㅋ

2000년 입사 첫 해 봄 – 플스2판 TTT BGM 中 샤오유 스테이지, 잭2 스테이지, 엔딩
– 첫월급…과 함께 발급된 내 생애 첫 신용카드로 긁어주신 일판 신품 플스2와 TTT(할부는 무려 9개월)
남코의 BGM 코드가 나와 잘 맞다는것을 확인.

2000년 입사 첫 해 여름 – 이박사 ‘Space Fantasy’
– 테크노뽕짝의 거장 닥터리. 그의 불후의 명곡. 당시 같이 일하던 명진이와 나는 이곡을 들으며 ‘세상에 이런 곡이’를 연발할 수 밖에 없었다.

2000년 입사 첫 해 가을 – Ridge Racer 5 BGM 中 ‘Euphoria’
– 그 당시 놀러나갈때 귀에 꽂고있었거든.

2001년 대만출장중 – S#arp 의 ‘For you’
– S#arp 의 원곡이 아니라, 당시 대만 사무실 칸막이 옆 노래연습하는곳에서 대만 가수(혹은 가수지망생..)이 번안해 연습하던 곡으로 처음 접함. 접한 정도가 아니라 거의 세뇌수준(2달 내내 들었심-ㅂ-)
그래서 그런지 원곡 들어보니 좋더만(…)

2001년 가을 부터 2006년 봄 까지는 Kyoto Jazz Massive 와 Jacques Loussier Trio 의 지배를 받았던 시기. 이 글의 관점에서는 일종의 암흑기라고 해야되나…
KJM 은 처음에 듣기 시작할때 물어보니(적어도 내 주위에선) 아는 사람도 없고 국내에선 음반 구하기도 어렵고.. 해서 어려운가부다..했는데, 나중에 알고보니 몇 차례 내한한 적도 있는 듯. 하지만 한 번도 가보진 못했다.
Jacques Loussier Trio 는 2005년 말에 큰맘먹고 공연 보러..갔는데 혼자 갔어!ㅋ 근데 영화나 공연은 정말 좋아하는 거라면(그리고 안타깝게도 취향이 일치하는 같이 갈만한 사람이 없다면) 혼자 가는 게 좋습니다. 이건 팩트예요 팩트.

2007년 (언제부턴지는 모르겠는데)~초여름 – Casiopea vs The Square Live
– 당시 휴대용 mp3p로 쓰던 PSP 로 줄창 들어댔음… 이거 라이브라 곡들 사이에 끊기지 않고 연결되는 게 많은데, 한 곡이 생각나서 듣기 시작하면 그 곡 이후로 몇곡을 연달아 들어야 하는 일이 종종 발생.

2007년 여름 – 배슬기 ‘말괄량이(feat. 카를로스 Of 업타운)’ (..이라고 벅스 플레이어에 제목이 적혀있음.. 한 자도 안 빼먹고 대소문자까지 그대로 옮김. 왠지 그래야 할 거 같아서…)
– 저 시기 즈음에 모 남성지의 ‘세련’을 주제로 곡을 추천하는 기사를 읽다 발견..한 곡.

그리고 이 시기 이후로는 다시 암흑기.. 인 것도 같고,
혹은 이 특정 시기와 연결되는 곡은 기억이 몇 년 숙성된 후에야 드러나는 거 같기도 하고..

언젠가 이 글의 세번째판을 쓰게 되는건.. 몇 년 후가 되려나…?

아.. 근데 다 적고 보니 음악과 시기는 있는데 근현대(개인)사는 어디간거냐ㅋ