이것은 1999년 하이텔 게임 제작 동호회에 올렸던 강좌 4회분 입니다.

#####################################################

---------------------------------------------------------------------------
      델파이에서 MMX 코드를 (1/4)

   1. 델파이에서 MMX 코드를 사용하는 법
   2. MMX 체킹과 MMX 레지스터를 이용한 8 bytes 전송
   3. MMX 응용 (1) - 크로마키
   4. MMX 응용 (2) - 고속 행렬 연산
---------------------------------------------------------------------------

안녕하세요.. 안영기입니다.

다름 아니라 이번에는 델파이에서 MMX Technology를 이용하는 방법과 그 응용 프
로그램을 만드는 예제를 설명 드리고자 합니다.

MMX Technology 는 몇 년 전에 상당히 유행하면서, 컴퓨터가 뭔지도 모르는 사람
들조차도 '펜티엄 166'보단 '펜티엄 166 MMX'가 좋다는 것을 알 정도였지만,  정
작 프로그래머들에게는 MMX 라는 것이 또 다른 부담을 준 것이 사실이지요..
( 나중에 MMX와 펜티엄 프로가 합쳐져서 펜티엄 2 가 되는 결과를 나았습니다. )

MMX Technology 에 대해서 간단하게 설명하자면,  기존에 FPU 가 사용하던  스택
프레임을 절대 번지로 사용하여 64bit 레지스터인 mm0 부터 mm7까지의 8 개의 레
지스터로 확장을 하면서, MMX 파이프 라인으로 동시에 두 개 이상의 연산을 하도
록 만든 것입니다.

주로 사용되는 분야는 멀티미디어 부분인데, Intel 에서 소개하는 응용 예제에는
다음과 같은 것이 있습니다.

  1. 조건부 선택
     - 아래에 나오는 크로마키와 같은 기능이다. 단 크로마키와 같은 것은 그래
       픽에 국한된 것이지만 조건부 선택은 고속의 마스킹(masking)을 가능하게
       해준다.

  2. 크로마키 (Chroma Key)
     - 흔히 블루 스크린이나 그린 스크린으로 불리어지는 것이다.  영화를 찍을
       때 푸른색의 배경 뒤에 사람이 움직이는 것을 찍은 뒤  실제의 배경과 합
       성할 때,  실제 배경 위에 푸른색을 제외한 다른 색의 인물 스크린을  덧
       씌우는 방식으로 마치 그 사람이 실제 배경 안에서 행동하는 것처럼 보이
       는 기술이다.
       게임에서는 스프라이트(sprite; 요정)라고 불리는 기법이다.

  3. 고속 행렬 연산
     - MMX 명령어의 특징인 DSP 적인 요소를 극대화시킨 응용법이다. 행렬의 주
       된 연산은 곱한 뒤 더하는 연산이다.  MMX 는 이런 연산법을 지원하는 명
       령어가 있다.

  4. 24bit 컬러 제어
     - 위의 고속 행렬과 아래의 알파블렌딩을 결합한 것이라고 생각하면 된다.
       24bit 트루컬러에 대한 이미지 조합과 알파블렌딩이 가능하다.

  5. 알파블렌딩을 이용한 이미지 분해
     - 고속 행렬 연산을 그래픽 쪽으로 응용한 것이다.  알파 블렌딩은 주로 반
       투명과 오버랩으로 대표되는데, 이것 역시 각 RGB 별로 곱하고 더하는 연
       산이 주를 이루기 때문에 MMX 의 능력을 십분 발휘할 수 있는 요소다.

이렇듯 멀티미디어의 고속 출력에 주로 이용되며 MMX 가 탑재된 컴퓨터나 펜티엄
2 이상의 컴퓨터에서는 당연히 사용할 수 있는 기능들입니다.

그렇다면 이런 MMX 코드를 델파이에서 사용하려면 어떻게 될까요 ?  하지만 불행
하게도 델파이는 MMX 명령어 자체를 지원하지 않습니다.

그렇다면 우리의 사명은 무엇일까요 ? 네.. 그렇습니다.  델파이에서 MMX 코드를
사용할 수 있게 만드는 것입니다.

델파이가 Borland Pascal 6.0 이었을 때부터 지원하는 예약어가 있습니다.  바로
'asm' 과 'assembler' 입니다.

보통은 begin .. end 를 사용하지만 경우에 따라서는 asm ... end 를 사용 할 수
가 있습니다. 그리고,

procedure A;
begin
end;

대신에

procedure A; Assembler;
asm
end;

를 사용할 수가 있습니다. ( 델파이에서는 Assembler 예약어를 쓰지 않아도 되는
가 봅니다. )

또한,

procedure A;
begin
   .......
   asm
      .......
   end;
   .......
end;

처럼도 사용할 수가 있습니다.

begin ... end 와 asm ... end 의 차이점이라면,  다 아시겠지만 그 안에 파스칼
코드가 들어가는지 어셈블러 코드가 들어가는지의 차이입니다.

MMX 코드는  일단 모두 어셈블리어 명령입니다.  그러니 컴파일러 자체가 구현해
주는 코드 이외에 프로그래머가 직접 MMX 코드를 만들려면 어셈블리어를 사용 할
수 밖에는 없는 것입니다.  하지만 위에 말씀 드렸듯이 델파이는 MMX 어셈블리어
명령어를 지원하지 않습니다.  ( 델파이 5 는 지원하지 않지만 C++ 빌더 4 는 지
원합니다... )

예전에 Borland Pascal 이 32bit 레지스터와 386 이상의 명령어를 지원하지 않았
지만 그때도 훌륭하게 파워 유저들은 그 길을 찾아냈는데... 그것이 바로 기계어
코드를 바로 쓰는 것이었습니다.
( 32bit 레지스터는 항상 16bit 레지스터의 명령어 앞에 $66 이나 $67 이라는 접
  두어 바이트가 항상 붙었기 때문에  실제 코드의 명령어 앞에 일일이 접두어를
  붙였습니다. )

어셈블리어 명령어로 32bit 레지스터를 접근하기 위해서는  'mov' 라는 명령어를
사용했었습니다. 역시 아래와 같이 하면 델파이는 훌륭하게 컴파일 해 냅니다.

   asm
      mov   eax, [esi]
      mov   ecx, eax
   end;

아까도 말씀 드렸다시피 MMX 레지스터는 64bit 입니다. 그리고 64bit MMX 레지스
터에 대한 대입연산은 아래와 같은 어셈블리어로 가능합니다.

   asm
      movq  mm1, [esi]
      movq  mm2, mm1
   end;

하지만 불행하게도 델파이는 위와 같은 명령에는 에러를 냅니다. ( 그렇지만 C++
빌더4 에서는 제대로 컴파일이 됩니다. 같은 볼랜드사 컴파일러인데.... -_-; )
그렇다면 이 어셈블리어를 기계어로 바꾸면 어떨까요 ?

   asm
      db $0F,$6F,$0E    // movq  mm1, [esi]
      db $0F,$6F,$D1    // movq  mm2, mm1
   end;

실제  만들어지는 코드는 같지만  일단 컴파일러에서 에러를 만들지 않기 때문에
원하는 대로 결과를 내게 됩니다. 직접 기계어 바이트 코드로 쓴 것들을 풀이 하
자면....

      $0F,$6F,$0E
       |   |   +---- mm1, [esi]       
       |   +-------- movq
       +------------ MMX 코드라는 접두어

      $0F,$6F,$D1
       |   |   +---- mm2, mm1       
       |   +-------- movq
       +------------ MMX 코드라는 접두어

이런 식으로 풀어지기 때문에 약간의 수작업을 거치면 누구나 MMX 코드를 델파이
에 삽입할 수가 있습니다.
( 위의 기계어는 Intel 사이트에서 자료를 구할 수 있습니다. )

그런데... 제가 이러한 기계어 풀어쓰기 강좌를 하기도 전에 이런 수작업을 도와
주는 델파이 익스퍼트가 이미 나와 있었습니다. MMXasm 이라는 것으로.. DelphiX
를 만든 사람의 홈페이지에서 그것을 구할 수가 있습니다.

    http://www.ingjapan.ne.jp/hori/program.html

그래서 기계어 코드 풀어쓰기 보다는   MMX 코드를 직접 쓰는 쪽으로  중점을 둘
계획입니다. ( 일본인이 이런 걸 만드니 분한 느낌입니다만... )

(*

참고로 거기에는 정말 유용한 델파이 자료가 많습니다.

1999/10/24 DelphiX
           - Delphi 3, 4, 5用 DirectX コンポ-ネント
            ( 델파이 3,4,5 용 DirectX 콤포넌트 )
1999/09/09 DirectX 6 Header
           - Delphi 3, 4用 DirectX 6 ヘッダ-
            ( 델파이 3,4 용 DirectX 6 헤더 )
1997/11/11 WPColorDIalog
           - WordPad ライクなカラ-ダイアログ
            ( 워드패드 같은 컬러 다이얼로그 )
1998/05/07 MemCheck
           - メモリリ-ク回-チェッカ-
            ( 메모리 누수 체커 )
1999/08/29 MMXasm
           - Delphi 3, 4 で MMX 命令などを使えるようにするアドイン
            ( 델파이 3,4 에서 MMX 명령 등을 사용하게 해주는 애드인 )
1998/08/17 プロパティ電卓 ( 프로퍼티 계산기 )
           - オブジェクトインスペクタで表示される Integer 型のプロパティで
             式が使えるようにするプロパティエディタ
            ( 오브젝트 인스펙터에서 표시되는 integer 형의 프로퍼티로,
              식이 사용되도록 한 프로퍼티 에디터 )

*)

그리고 MMX 명령어 자체에 대한 것은 이미 하이텔에 강좌가 있으므로 ( 소프트웨
어 동호회 강좌란 < go soft 21 > ) 중복 강좌는 피하겠습니다.


---------------------------------------------------------------------------
                                                 copyright SMgal 1999/11/04

#####################################################


---------------------------------------------------------------------------
      델파이에서 MMX 코드를 (2/4)

   1. 델파이에서 MMX 코드를 사용하는 법
   2. MMX 체킹과 MMX 레지스터를 이용한 8 bytes 전송
   3. MMX 응용 (1) - 크로마키
   4. MMX 응용 (2) - 고속 행렬 연산
---------------------------------------------------------------------------

안녕하세요.. 안영기입니다.

이번 장에서는 MMX 의 기본 명령어 설명을 시작으로 MMX 체킹과 MMX 레지스터 사
용을 다루겠습니다.


MMX 명령어는 모두 57 개입니다. 하지만 확장된 것을 다 합쳐서 57개이며,  실제
로는 명령어 세트 개념이므로 아래의 명령어가 전부입니다.

1. 대수 연산

   1) 덧셈          - padd
   2) 뺄셈          - psub
   3) 곱셈          - pmul ( pmull / pmulh )
   4) 곱셈 + 덧셈   - pmadd
   5) 산술 쉬프트   - psra
   6) 비교          - pcmp??

2. 변환

   1) 포화 축소     - pack ( packss / packus )
   2) 확장          - punpck ( punpckl / punpckh )

3. 논리 연산

   1) AND           - pand
   2) AND NOT       - pandn
   3) OR            - por
   4) Exclusive OR  - pxor
   5) 왼쪽 쉬프트   - psll
   6) 오른쪽 쉬프트 - psrl

4. 데이터 이동

   1) 레지스터끼리  - mov ( movd / movq )
   2) 메모리로부터  - mov ( movd / movq )
   3) 메모리에      - mov ( movd / movq )

5. 기타

   1) EMMS          - emms

MMX 명령어는 나름대로 조어규칙이 있습니다.  위의 명령어들을 보시면 아시겠지
만 mov 와 emms 를 제외하고는 모두 p 로 시작합니다.

앞에 붙은 'p' 는 접두어로 'packed' 의 약자입니다. 그 다음에는 원래의 명령어
를 의미하는 단어가 오고 마지막에는 접미어가 붙는데 'us' ( 부호없는 포화모드
) 와 's' ( 부호 있는 포화모드 ) 가 붙습니다.  그리고 마지막에는 관련되는 데
이터형에 따라 'b', 'w', 'd', 'q' 가 붙습니다.

   < MMX 조어 규칙 예 >

    1. paddsb  - 포화모드의 부호있는 byte 단위의 덧셈

       p add s b
       |  |  | +------- byte 단위
       |  |  +--------- signed ( 부호있음 )
       |  +------------ 더하기 명령
       +--------------- packed

    2. movq    - 8 바이트 전송

       mov q
        |  +----------- 64 bit 단위
        +-------------- 전송명령

    3. pcmpeqw - word 단위로 같은지 비교

       p cmp eq w
       |  |  |  +------ word 단위
       |  |  +--------- 같을 조건 ( gt 가 붙으면 클 때가 된다. )
       |  +------------ 비교 명령
       +--------------- packed

이런 식으로 조합되며,  조합이 가능한가는 매뉴얼의 명령어 목록을 보시면 됩니
다. 그리고 명령어 자체에 대한 강좌는 '소프트웨어 동호회' ( go soft )의 강좌
를 참고 하시면 됩니다.

---------------------------------------------------------------------------

<< MMX 체크법 >>

일단 MMX 를 사용하기 위해서는 MMX 를 지원하는 CPU 가 있어야 합니다. 그 여부
를 알기 위해서 아래와 같은 방법으로 함수를 만들어 체크하시면 됩니다.

( 소스의 주석에 설명을 해 놓았으니 그걸 참고 하세요... )

| function  CheckMMX : Boolean;
| var
|    _EDX   : DWORD;
| begin
|    CheckMMX := FALSE;
|    try
|       asm
|          mov   eax, 1     //  eax에 1을 넣으면 CPUID 에서 '버전정보'를 돌
|                           //  려준다.
|          db $0F,$A2       /// CPUID
|                           //  CPU 의 정보를 알아오는 명령
|          mov   _EDX, edx  //  edx가 리턴값
|       end;
|    except
|       Exit;               //  프로세서가 CPUID 명령을 지원하지 않는다.
|                           //  즉 CPUID 명령어가 나오기 이전의 CPU 다.
|    end;
|                           //  MMX 를 지원하면 23번 비트가 set 된다
|    if (_EDX and $800000) > 0 then begin
|       try
|          asm
|             db $0F,$77    /// emms
|                           //  MMX 명령어를 하나 써 본다
|          end;
|          CheckMMX := TRUE;
|       except
|          Exit;            //  프로세서가 MMX 명령을 지원하지 않는다.
|       end;
|    end;
| end;

방식을 설명하면,

   1. 'CPUID' 명령을 사용해서 CPU 버전 정보를 알아 온다.
      만약 실패했으면 이 CPU 는 'CPUID' 명령을 지원하지 않는 이전 CPU 이다.

   2. 'CPUID' 명령 후에 edx 레지스터에 리턴 값이 돌아온다. 그중 23번 비트가
      세트되면 MMX 를 지원하는 CPU 이다.

   3. 실제로 MMX 명령인 'emms' 를 넣어 본다.  이 명령이 실패하면  그 CPU 는
      MMX 명령어를 지원하지않는 것이다.

< 참고 >

   1 장에서 말씀 드렸듯이 MMX 는 FPU 의 스택 프레임을 절대 번지로 사용한 레
   지스터를 사용한다고 했습니다.  즉  그것은 FPU 와 MMX 는 동시에 사용할 수
   없다는 것을 의미합니다.

   그래서 MMX 레지스터를 사용하고 나면 항상 FPU 스택 프레임을 비워주는 명령
   어를 줘야 하는데 그 명령어가 'emms' ( Empty MultiMedia State ) 입니다.
 ( 물론  FPU 의 실수 연산을 사용하는 곳이 있기 전에 사용하기만 하면 되는데,
   만약 FPU 를 이용한 실수연산을 하지않고 MMX 코드에 재진입한다면 사용할 필
   요가 없습니다. 'emms' 명령어의 클럭수가 많기 때문입니다. )

---------------------------------------------------------------------------

<< MMX 레지스터를 이용한 8 bytes 전송 >>

델파이에는 Move() 라는 명령어가 있는데  4 Bytes 고속 전송을 해주는 명령어로
상당한 퍼포먼스를 가지고 있습니다.  4 bytes 전송인 이유는 386 이상에서 지원
하는 32 비트 레지스터를 사용하기 때문입니다.
( movsd 명령어로 최대 32 bit 전송까지 가능. )

하지만 앞에도 말씀 드렸듯이 MMX 레지스터는 모두 64 bit 레지스터입니다. 한번
에 8 bytes 씩 데이터를 전송할 수 있다는 말인데..... 이번에는 MMX 레지스터를
이용해서 8 bytes 전송 함수를 만들어 보겠습니다.

( 역시 이번에도 소스의 주석에 설명을 해 놓았으니 그걸 참고하세요... )

| procedure MMXMove(const Sour; var Dest; Count : integer); Assembler
| asm
|
|       push   esi       //  esi를 스택에 넣는다
|       push   edi       //  edi를 스택에 넣는다
|
|       mov    esi, eax  //  Sour의 포인터
|       mov    edi, edx  //  Dest의 포인터
|                        //  Count는 처음부터 ecx에 들어가 있다.
|       shr    ecx, 3    //  Count div 8
|
| @@LOOP:                //  ALIGN 16 필요....
|       db $0F,$6F,$06   /// movq   mm0, [esi]
|                        //  esi가 가리키는 번지로부터 8바이트 꺼낸다
|       db $0F,$7F,$07   /// movq   [edi], mm0
|                        //  edi가 가리키는 번지에 8바이트 쓴다
|       add    esi, 8    //  esi 주소 8바이트 증가
|       add    edi, 8    //  edi 주소 8바이트 증가
|       loop   @@LOOP    //  ecx 만큼 반복
|
|       pop    edi       //  edi를 스택에서 꺼낸다
|       pop    esi       //  esi를 스택에서 꺼낸다
|
| end;

그저 평이한 전송 함수랑 같습니다. 단 Count는 8 의 배수가 되어야 제대로 동작
할 겁니다. 그 부분은 충분히 개량하실 수 있습니다.
( shr 에서 캐리가 발생할 때마다 movsb, movsw, movsd 를 하면 됩니다. )

그런데, 8 bytes 씩 전송하기 때문에 4 bytes 씩 전송하는 Move 명령어 보다 2배
빠를까요 ?  아쉽게도... 답은 '거의 비슷하다' 입니다.

80386 이상의 CPU 최적화가 항상 그렇듯 MMX 에서도 8 bytes 전송을 할 때 그 IP
( Instruction Pointer )가 메모리상의 절대번지가 8의 배수가 되는 곳에 위치해
야 하는데, 델파이의 인라인 어셈블러에서는 그 기능을 제대로 지원하지 않기 때
문입니다.

원래대로라면 'movq' 명령 앞에다가 16 바이트 단위로 주소를 정렬해주는 지시자
인 'ALIGN 16' 을 써줘야 합니다. ( C++ Builder는 되는데 왜 델파이는 지원하지
않을까요.. T_T; )

movq 명령에서 주의할 점이있는데, 아래와 같은 것들은 모두 에러를 냅니다.

   1. movq  mm0, eax

      - 이 경우에는 사이즈가 다르기 때문인데  MMX 레지스터를 제외하고는  64
        bit 레지스터가 없기 때문에 항상 'movq mm0, [esi]' 처럼 간접 주소 방
        식으로 사이즈를 맞춰줘야 합니다.

   2. movq  mm0, 1

      - 이 경우에는 상수 값을 바로 사용했는데.. 이것 역시 허용하지 않습니다.
        이럴 때는 아래처럼 해야 합니다.

        pxor  mm0, mm0
        mov   eax, 1
        movd  mm0, eax

그리고 역시 MMX 도 펜티엄의 최적화 규칙을 따르는데...

   movd  mm0, [esi]    // mm0 레지스터의 하위 4 바이트 쓰기
   movq  mm1, mm0      // mm0 레지스터 전체 읽기

처럼 레지스터의 일부분을 '쓰기' 한 후에 레지스터 전체를 '읽게' 되면 stall이
발생하여 제 속도를 내지 못합니다. 이럴 때는 이렇게 해주면 됩니다.

   pxor  mm0, mm0      // psubb  mm0, mm0 도 같은 효과
   movd  mm0, [esi]
   movq  mm1, mm0


---------------------------------------------------------------------------

<< MMX 레지스터를 사용한 팁들 >

1. mm0 레지스터를 0로 만들기

   pxor   mm0, mm0

2. mm0 의 모든 bit를 1 로 만들기

   pcmpeq mm0, mm0

3. mm0 의 8 개의 바이트를 1 로 초기화 하기

   pxor   mm0, mm0
   pcmpeq mm1, mm1
   psubb  mm0, mm1

4. mm0 의 각 word 단위로 보호있는 2^(n-1) 을 구하기

   pcmpeq mm0, mm0
   psrlw  mm0, (16 - n)

5. mm0 의 각 word 단위로 보호있는 -2^(n-1) 을 구하기

   pcmpeg mm0, mm0
   psrlw  mm0, n

6. mm0 의 절대 값을 mm1 에 넣기

   movq   mm1, mm0
   psraw  mm0, 15
   pxor   mm0, mm1
   psubs  mm1, mm0

7. 부호없는 값 mm0 과 mm1 의 절대 차를 구하기

   movq    mm2, mm0
   psubusb mm0, mm1
   psubusb mm1, mm2
   por     mm0, mm1

8. 부호있는 값 mm0 과 mm1 의 절대 차를 구하기

   movq    mm2, mm0
   pcmpgtw mm0, mm1
   movq    mm4, mm2
   pxor    mm2, mm1
   pand    mm2, mm0
   movq    mm3, mm2
   pxor    mm4, mm2
   pxor    mm1, mm3
   psubw   mm1, mm4

9. 복소수의 곱을 구하기
   ( mm0 : 하위 4 바이트중, 복소수의 실수부가 상위 2 바이트에,  허수부가 하
           위 2 바이트에 들어 있다.
     mm1 : 복소 계수가 2 바이트씩 [ Re -Im Im Re ] 형식으로 들어간다.     )

   punpckldq mm0, mm0  // 하위 4 바이트를 상위 4 바이트로 확장 복사한다.
   pmaddwd   mm0, mm1  // (m0.r*m1.r - m0.i*m1.i)(m0.r*m1.i - m0.i*m1.r)

   ( 결과는 mm0 에 상위 4 바이트가 실수부, 하위 4 바이트가 허수부로 된다. )

 

---------------------------------------------------------------------------
                                                 copyright SMgal 1999/11/04

#####################################################


---------------------------------------------------------------------------
      델파이에서 MMX 코드를 (3/4)

   1. 델파이에서 MMX 코드를 사용하는 법
   2. MMX 체킹과 MMX 레지스터를 이용한 8 bytes 전송
   3. MMX 응용 (1) - 크로마키
   4. MMX 응용 (2) - 고속 행렬 연산
---------------------------------------------------------------------------

안녕하세요.. 안영기입니다.

이번 장부터는 MMX 응용으로 들어가서  델파이에서 직접 구현을 해보는 차례입니
다. 이번에 하고자 하는 것은 크로마키인데, 1 장에서도 말씀 드렸듯이 영화에서
블루 스크린과 같은 개념이며 게임으로 적용되면 스프라이트가 된다고 했습니다.

예전에는 DX (Direct X) 상에서 스프라이트를 찍기 위안 알고리즘은 이렇습니다.

흔히 DX 256 컬러 모드에서 표면을 Lock하여 그곳에 스프라이트를 쓰고자 한다면
압축 스프라이트를 쓰던지 아니면 직접 1 byte 씩 투명색인지 아닌지를 체크해가
며 점 단위 연산을 하는 것이 보통이었습니다. ( 투명색이라는 것은 블루 스크린
일때는 푸른색 (0,0,255)입니다. )

예전 같으면 스프라이트를 찍기 위해 아래와 같이 투명색 판별을 했습니다.

      mov  ecx, Size.X

   LOOP_X:
      cmp  ptr byte [esi], TRANSPARENCY_COLOR
      je   SKIP
      stosb

   SKIP:
      inc  esi
      loop LOOP_X

이런 식이 되어서 점의 수만큼 'cmp'를 해야만 했었습니다.

하지만 지금 우리가 하고 있는 MMX 의 경우에는 투명색인지 판별조차 하지않고서
도 8 개의 스프라이트 픽셀을 동시에 전송하는 방법을 사용할 수가 있습니다.

일단 예전처럼 투명색인지 아닌지 판별하는 부분이 없어졌으므로  'je', 'jz' 라
는 명령어로 인한 분기의 부하를 줄이고  1 byte 씩 하던 전송을 한번에 8 bytes
씩 가능하게 해준다는 것에 그 의의를 둘 수가 있습니다.

풀소스에 들어가기 전에 핵심 부분만 추려보면 다음과 같습니다.

---------------------------------------------------------------------------

 >> 전제...

   esi     : 스프라이트 데이터
   edi     : Lock 된 Direct Draw Surface 의 미리 계산된 주소
   pMask   : char pMask[8] 로 정의되어 있으며 8개의 byte 가 모두 투명색
             Index 로 채워져 있다.

 >> 코드

   mov   ebx, pMask
   movq  mm4, [ebx]     // mm4 에 투명색 번호를 넣는다

   mov   ecx, Size.X    // Size.X 에는 X 스프라이트의 x 사이즈가 들어감
   shr   ecx, 3         // 동시에 8개의 점을 연산하므로

LOOP_X:
   movq  mm0, [edi]     // mm0 은 Destination
   movq  mm1, [esi]     // mm1 은 Source
   movq  mm2, mm1       // mm2 에 Source 데이터를 복사
   pcmpeqb mm2, mm4     // mm2 에 투명색에 따른 마스크를 생성
   movq  mm3, mm2       // mm3 에 마스크를 하나 더복사
   pandn mm2, mm1       // Source 스프라이트 부분만 복사
   pand  mm3, mm0       // Destination 의 갱신될 부분만 제거
   por   mm2, mm3       // Source 와 Destination 을 결합
   movq  [edi], mm2     // Destination 에 결과를 씀

   add   esi, 8         //  한번에 8 bytes 를 동시에 처리했으므로
   add   edi, 8

   loop  LOOP_X

 >> 코드분석

   가장 코어 부분만 보면 다음과 같습니다.

   - movq  mm0, [edi]

     mm0 레지스터에 edi 가 가리키는 데이터중 8 bytes 를 가져 옵니다.

     예>   mm0   =>   43 2D 1A E4 54 78 20 CA

   - movq  mm1, [esi]

     mm0 레지스터에 esi 가 가리키는 데이터중 8 bytes 를 가져 옵니다.

     예>   mm2   =>  10 2F F8 80 80 3D 80 CE

   - movq  mm2, mm1

     mm2 레지스터에 mm1 레지스터의 내용을 카피합니다.
     소스 데이터에서 투명색에 대한 정보를 얻기 위해서입니다.

   - pcmpeqb mm2, mm4

     위에 보시면 이미 mm4에는 1byte 짜리 마스크로 8bytes 가 채워져 있습니다.
     'pcmpeqb' 명령은 byte 단위로 8 개의 각 byte 가 같은지 같지 않은지를 판
     별합니다. 그래서 만약 값이 같다면 그 byte는 모두 1로 채우고 그렇지않다
     면 모두 0으로 채웁니다/

     예>     mm2   =>   10 2F F8 80 80 3D 80 CE
             mm4   =>   80 80 80 80 80 80 80 80   ( 투명색이 0x80 일때 )

      결과   mm2   =>   00 00 00 FF FF 00 FF 00

   - movq  mm3, mm2

     이렇게 만들어진 마스크를 mm3 에 하나 더 복사합니다.

   - pandn mm2, mm1       // Source 스프라이트 부분만 복사

     'pandn' 명령어는 mm2 를 반전한후 그 mm2 와 mm1 를 and 해서 mm2 로
     넣습니다.

     예>     mm2    =>   00 00 00 FF FF 00 FF 00
             mm2    =>   FF FF FF 00 00 FF 00 FF  ( 반전 )
             mm1    =>   10 2F F8 80 80 3D80 CE

      결과   mm2    =>   10 2F F8 00 00 3D 00 CE  ( mm2 & mm1 )

     결과에서 보시면 아시겠지만 원 스프라이트 데이터에서 투명색 부분만 '00'
     이 되었다는 것을 알 것입니다.

   - pand  mm3, mm0

     'pand' 명령어는 mm3 와 mm0 를 and 연산하여 mm3 에 넣습니다.

     예>     mm3    =>   00 00 00 FF FF 00 FF 00
             mm0    =>   43 2D 1A E4 54 78 20 CA

      결과   mm3    =>   00 00 00 E4 54 00 20 00

   - por   mm2, mm3

     'por' 명령어는 mm2 와 mm3 를 or 연산하여 mm2 에 넣습니다.

     예>     mm3    =>   00 00 00 E4 54 00 20 00
             mm2    =>   10 2F F8 00 00 3D 00 CE

      결과   mm2    =>   10 2F F8 E4 54 3D 20 CE

   - movq  [edi], mm2

     edi 가 가리키는 주소에 mm2 의 내용 8 bytes 를 쓴다.


   이렇게 해서 결국은 아래의 결과를 냅니다.

             [edi]   =>   43 2D 1A E4 54 78 20 CA ( 배경 )
             [esi]   =>   10 2F F8 80 80 3D 80 CE ( 스프라이트, 투명색 80 )

             [edi]   =>   10 2F F8 E4 54 3D 20 CE
                                   ~~ ~~    ~~    ( 투명색이었던 부분만
                                                    배경으로 치환         )

---------------------------------------------------------------------------

제 나름대로는 열심히 설명을 드린다고 하긴 했는데  제대로 이해가 되셨는지 모
르겠네요...

그리고 제가 8 bit 컬러 모드에 대해서만 설명했는데, 16bit 모드나 트루컬러 모
드에서도 똑같이 적용됩니다. 단 'pcmpeqb' 라는 명령어가 2bytes, 4bytes 씩 비
교하는 'pcmpeqw', 'pcmpeqd' 로 수정하시면 됩니다.

그리고 아래는 방금 해본 코드의 델파이 소스입니다.
( 모든 풀 소스와 실행 파일은 게임 제작 동호회 자료실에서 ( go gma 5 )
  =========================================================================
  5366 k2gma3   mmx_spr.zip   192K  156 SOUR  MMX 무분기 SPRITE 출력 소스&
  =========================================================================
  위의 자료를 받으시면 됩니다.

---------------------------------------------------------------------------
( 여기는 각각 서피스에서 직접 읽었지만  실제론 스프라이트가 유저 데이터형으
  로 들어가게 되겠죠... ( 이건 테스트라서 Direct Draw Surface 사용했음.. ))

procedure   SpriteCopy(DestX, DestY : integer;
                       SourX, SourY : integer;
                       Size         : TPoint;
                       Sour, Dest   : IDirectDrawSurface);
const
   TRANSPARENCY_VALUE  = 80; // 투명색이 80번 인덱스이다.
var
   SourDesc, DestDesc  : TDDSurfaceDesc;
   pSour, pDest, pMask : PByte;
   Transparency        : array[1..8] of byte;
begin

   FillChar(Transparency,8,TRANSPARENCY_VALUE);

   SourDesc.dwSize := SizeOf(TDDSurfaceDesc);
   with Basic do repeat
   until MakeItSo(Sour.Lock(PRect(nil)^,SourDesc,DDLOCK_SURFACEMEMORYPTR +
                                                 DDLOCK_WAIT,0));

   DestDesc.dwSize := SizeOf(TDDSurfaceDesc);
   with Basic do repeat
   until MakeItSo(Dest.Lock(PRect(nil)^,DestDesc,DDLOCK_SURFACEMEMORYPTR +
             DDLOCK_WAIT,0));

   pSour := PByte(DWORD(SourDesc.lpSurface)+SourY*SourDesc.lPitch+SourX);
   pDest := PByte(DWORD(DestDesc.lpSurface)+DestY*DestDesc.lPitch+DestX);
   pMask := Pointer(@Transparency);

   asm
         push  esi
         push  edi

         mov   esi, pMask
         db $0F,$6F,$26       /// movq  mm4, [esi]
                              //  mm4 에 투명색 번호를 넣는다
         mov   esi, pSour
         mov   edi, pDest

         mov   ecx, Size.Y

   @@LOOP_Y:

         push  ecx

         mov   ecx, Size.X
         shr   ecx, 3         // 동시에 8개의 점을 연산하므로


   @@LOOP_X:

         db $0F,$6F,$07       /// movq  mm0, [edi]
                              //  mm0 은 Destination
         db $0F,$6F,$0E       /// movq  mm1, [esi]
                              //  mm1 은 Source
         db $0F,$6F,$D1       /// movq  mm2, mm1
                              //  mm2 에 Source 데이터를 복사
         db $0F,$74,$D4       /// pcmpeqb mm2, mm4
                              //  mm2 에 투명색에 따른 마스크를 생성
         db $0F,$6F,$DA       /// movq  mm3, mm2
                              //  mm3 에 마스크를 하나 더 복사
         db $0F,$DF,$D1       /// pandn mm2, mm1
                              //  Source 스프라이트 부분만을 남김
         db $0F,$DB,$D8       /// pand  mm3, mm0
                              //  Destination 의 갱신될 부분만 제거
         db $0F,$EB,$D3       /// por   mm2, mm3
                              //  Source 와 Destination 을 결합
         db $0F,$7F,$17       /// movq  [edi], mm2
                              //  Destination 에 결과를 씀

         add   esi, 8
                              //  한번에 8 bytes 를 동시에 처리했으므로
         add   edi, 8

         loop  @@LOOP_X

         add   esi, SourDesc.lPitch
         sub   esi, Size.X
         add   edi, DestDesc.lPitch
         sub   edi, Size.X

         pop   ecx
         loop  @@LOOP_Y

         db $0F,$77              /// emms

         pop   edi
         pop   esi

   end;

   Sour.UnLock(SourDesc.lpSurface);
   Dest.UnLock(DestDesc.lpSurface);

end;

---------------------------------------------------------------------------
                                                 copyright SMgal 1999/11/04

#####################################################


---------------------------------------------------------------------------
      델파이에서 MMX 코드를 (4/4)

   1. 델파이에서 MMX 코드를 사용하는 법
   2. MMX 체킹과 MMX 레지스터를 이용한 8 bytes 전송
   3. MMX 응용 (1) - 크로마키
   4. MMX 응용 (2) - 고속 행렬 연산
---------------------------------------------------------------------------

안녕하세요.. 안영기입니다.

이번에는 MMX 의 또다른 응용인 3D 에 사용되는 고속 행렬 연산을 설명하고자 합
니다.

흔히 3D 게임을 만들 때 가장 기본이 되는 연산이며  확대, 축소, 이동, 각도 변
화 등등에 걸쳐 다용도로 쓰이는 부분입니다.

MMX가 탄생한 배경이 빠른 Multimedia 지원이니 만큼 이런 쪽에서도 지원이 되는
데요... 이에, 제가 소개 하고자 하는 응용이 바로 MMX 코드를 이용하여 그 행렬
계산을 빠르게 해보자는 의도입니다.

풀소스에 들어가기 전에 핵심 부분만 추려보면 다음과 같습니다.

-------------------------------------------------------------------------

 >> MMX 이론

구체적인 방식이라고 말한다면 MMX 명령어인 'pmaddwd'라는 명령어에 있습니다.
이 명령어는 여타의 DSP 칩에서와 같이 곱하고 동시에 더하는 연산을 합니다.
                                     ~~~~~~~~~~~~~~~~~~~~~~~~~
그리고 한번에 하나의 곱셈이 아닌  동시에 word 형 데이터 4개를 곱하고 더합니
다.

그렇다면 감이 오시겠죠 ?

3D 게임에서 사용하는 행렬은 4-3 를 주로 사용하고 4-3 끼리 곱하는 연산을 하
기 위해서 4 - 4 행렬로 확장을 해서 사용하게 됩니다.  물론 마지막 값은 항상
( 0, 0, 0, 1 )로 둡니다.

( 4-3 과 4-3 끼리는 절대 곱해질수 없죠. 고교 정석 수학 책을 참고 하시길..)

그리고 제가 이번에 실험하고자 하는 행렬식을 본다면...

  | x' |     |  a1  a2  a3  a0  | | x |
  | y' |  =  |  b1  b2  b3  b0  | | y |
  | z' |     |  c1  c2  c3  c0  | | z |
  | w' |     |  p1  p2  p3  p0  | | 1 |

이렇습니다.. 또한,

        x' = a1 * x +  a2 * y + a3 * z + a0

로 계산이 됩니다.

새로운 x 좌표인 x' 를 계산하기 위해서는 총 8개의 인자를 짝을 맞춰 곱하고 그
4개의 값을 더하는 형식입니다.

즉 이 방식이 바로 pmaddwd 명령어와 동일하죠 ?

그럼 pmaddwd 의 구체적인 동작을 얘기해 보겠습니다.

              1    2    3    4    5    6    7    8    <- 각 byte별

    mm0 =    (  w1  )  (  w2  )  (  w3  )  (  w4  )

    mm1 =    (  w1' )  (  w2' )  (  w3' )  (  w4' )

가 들어 있을 때 ...

    pmaddwd mm0, mm1

이란 명령을 실행하면..

              1    2    3    4    5    6    7    8    <- 각 byte별

    mm0 =    ( w1*w1' + w2*w2')  ( w3*w3' + w4*w4')

이렇게 word가 DWORD로 확장이 되면서 더해집니다.(mm0 레지스터는 8bytes짜리)

그리고 상위 4bytes 와 하위 4bytes 를 더하면 되는데..

    movq   mm1, mm0
    psrlq  mm1, 32
    paddd  mm0, mm1
    movd   eax, mm0

이렇게 하면 최종 결과인 w1*w1' + w2*w2' + w3*w3' + w4*w4' 가 eax에 들어가게
됩니다.

위에 있는 MMX 레지스터끼리의 대입이나 쉬프트는  모두 1 클럭만 소비하므로 빠
른 속도로 행렬 계산을 해 낼 수가 있는 것입니다.

---------------------------------------------------------------------------

 >> 구체적인 코드로 설명

위의 함수는 C = A * B 라는 행렬을 계산하기 위해  임의로 만들어진 예제식이며
( A = 4*4,  B = 4*1,  C = 4*1 )

   A : array[1..4,1..4] of word =
       ((1,2,3,4),(5,6,7,8),(9,10,11,12),(13,14,15,16));
   B : array[1..4] of word =
       (11,22,33,44);
   C : array[1..4] of word;  

   MMX_Matrix(A,B,C);

위와 같은 방식으로 사용하시면 C 에 결과가 돌아오는 방식입니다.

원래 우리들은 장황한 이론보다는 소스코드의 주석에 더 빨리 머리가 반응하므로
바로 소스에 주석을 다는 식으로 설명하겠습니다.

| procedure MMX_Matrix(var A, B, C); assembler;
| asm
|    mov     esi, eax         //  A 의 포인터 주소를 esi에
|
|    mov     ebx, edx         //  B 의 포인터 주소를 ebx에
|    db $0F,$6F,$13           /// movq    mm2, [ebx]
|                             //  mm2 에 B[]의 word형 인수 4개를 저장
|
|    mov     edi, ecx         //  C 의 포인터를 edi에
|
|    db $0F,$EF,$E4           /// pxor    mm4, mm4
|                             //  mm4 레지스터 초기화
|    mov     eax, 0000FFFFh
|    db $0F,$6E,$E0           /// movd    mm4, eax
|                             //  mm4 <- 0000 0000 0000 FFFF
|
|    // 1 행
|    db $0F,$6F,$06           /// movq    mm0, [esi]
|                             //  A[0]의 4개의 word형 인수를 저장
|    db $0F,$6F,$CA           /// movq    mm1, mm2
|                             //  mm1 을 B 의 값으로
|
|    db $0F,$F5,$C1           /// pmaddwd mm0, mm1
|                             //  문제의 연산, 설명은 위에서 했다..
|    db $0F,$6F,$C8           /// movq    mm1, mm0
|                             //  현재 곱하고 더한 결과가 4bytes씩 분리되어
|    db $0F,$73,$D1,$20       /// psrlq   mm1, 32
|                             //  있으므로 상위와 하위 4bytes 를 더해서
|    db $0F,$FE,$C1           /// paddd   mm0, mm1
|                             //  mm0 레지스터에 저장
|    db $0F,$DB,$C4           /// pand    mm0, mm4
|                             //  mm3 는 하위 2bytes 만 남게함
|    db $0F,$6F,$D8           /// movq    mm3, mm0
|                             //  mm3 의 C[0] 위치에 저장
|
|    // 2 행
|    db $0F,$6F,$46,$08       /// movq    mm0, [esi+8]
|                             //  A[2]의 4개의 word형 인수를 저장
|    db $0F,$6F,$CA           /// movq    mm1, mm2
|
|    db $0F,$F5,$C1           /// pmaddwd mm0, mm1
|    db $0F,$6F,$C8           /// movq    mm1, mm0
|    db $0F,$73,$D1,$20       /// psrlq   mm1, 32
|    db $0F,$FE,$C1           /// paddd   mm0, mm1
|    db $0F,$DB,$C4           /// pand    mm0, mm4
|    db $0F,$73,$F0,$10       /// psllq   mm0, 16
|    db $0F,$EB,$D8           /// por     mm3, mm0
|                             //  mm3 의 C[1] 위치에 저장
|
|    // 3 행
|    db $0F,$6F,$46,$10       /// movq    mm0, [esi+16]
|    db $0F,$6F,$CA           /// movq    mm1, mm2
|
|    db $0F,$F5,$C1           /// pmaddwd mm0, mm1
|    db $0F,$6F,$C8           /// movq    mm1, mm0
|    db $0F,$73,$D1,$20       /// psrlq   mm1, 32
|    db $0F,$FE,$C1           /// paddd   mm0, mm1
|    db $0F,$DB,$C4           /// pand    mm0, mm4
|    db $0F,$73,$F0,$20       /// psllq   mm0, 32
|    db $0F,$EB,$D8           /// por     mm3, mm0
|                             //  mm3 의 C[2] 위치에 저장
|
|    // 4 행
|    db $0F,$6F,$46,$18       /// movq    mm0, [esi+24]
|    db $0F,$6F,$CA           /// movq    mm1, mm2
|
|    db $0F,$F5,$C1           /// pmaddwd mm0, mm1
|    db $0F,$6F,$C8           /// movq    mm1, mm0
|    db $0F,$73,$D1,$20       /// psrlq   mm1, 32
|    db $0F,$FE,$C1           /// paddd   mm0, mm1
|    db $0F,$DB,$C4           /// pand    mm0, mm4
|    db $0F,$73,$F0,$30       /// psllq   mm0, 48
|    db $0F,$EB,$D8           /// por     mm3, mm0
|                             //  mm3 의 C[3] 위치에 저장
|
|    db $0F,$7F,$1F           /// movq    [edi], mm3
|                             //  mm3 의 내용을 C[] 에 복사
|
|    db $0F,$77               /// emms
|                             //  FPU 상태 비움
| end;

pmaddwd 명령의 용도만 알면 쉽게 이해가 될만한 내용이라  따로 설명은 하지 않
겠고 4개의 행을 각각 풀어쓴 것은 그나마 속도가 조금이나마 빨라지지 않겠나라
는 생각에 한 것입니다.

---------------------------------------------------------------------------

 >> 퍼포먼스

일단 가장 중요한 게 얼마나 빠르냐 ? 라는 것입니다.
빠르지 않다면 구태여 이런 것을 사용할 필요도 없고 사용해서도 안될 것입니다.

직접 델파이 코드로만 똑같은 기능을 하는 함수를 만들어 돌려 본 결과는 다음과
같습니다.

===========================================================================
일반 행렬 계산식으로 계산    :  1000000 번 호출하는데  평균 0.483 초
                                                       ~~~~~~~~~~~~~
MMX 행렬 계산식으로 계산     :  1000000 번 호출하는데  평균 0.222 초
                                                       ~~~~~~~~~~~~~
( 펜티엄 2 266MHz 에서 테스트.. )
===========================================================================

이렇게 약 2 배 이상의 성능 향상을 보이는 것으로 결과가 나왔습니다.

이제는 위와 같은 단순 행렬 계산식을  자신에게 맞는 3D 용 행렬 계산에 적용시
키는 일이 남았습니다.

그리고 트루컬러에서 MMX 알파블랜딩 방법도 위와 대동소이한 것이고 트루컬러의
RGB 값 분해도 거의 같은 맥락으로 보시면 됩니다.

---------------------------------------------------------------------------

<< 에필로그 >>

이 강좌는 예전에 게임 제작 동호회에서 했던 C++빌더용 강좌를 델파이로 컨버전
한 것입니다. MMX 에 대해서 더 자세하게 쓰고 싶지만 Intel 사이트에 있는 도큐
멘트가 워낙 잘되어 있어서 제가 설명해야 할 부분이 적어 진 것 같습니다.

그리고 이 강좌 이외에도 제가한 모든 강좌들과 그 소스들.. 그리고 제가 만들었
던 게임 소스들은 모두 제 홈페이지에 있습니다.

  < http://smgal.com/ >

그럼 마지막으로 한마디 하겠습니다.

             " A Mountain is a mountain, Water is water "


                                                 copyright SMgal 1999/11/04