이번에는 TPL에서 제공하는 옵션에 대해서 알아보겠습니다. 병렬 작업을 위해서는 먼저 병렬로 수행할 수 있게끔 작업을 분할하는 일이 먼저 수행되어야 합니다. 예를 들어서 10000개의 데이터를 4개의 스레드가 나눠서 처리해야 하고, 각 10000개의 작업이 독립적이라면, 이걸 공평하게 4등분 할 것인지, 혹은 다른 방법을 사용할 것인지등을 결정하는 것 말이죠.


- 두 가지 선택.

TPL에서 작업 분할을 할때 사용하는 방법으로 구간 분할과 로드 밸런싱이 있습니다. 구간 분할을 루프의 크기를 스레드의 개수만큼 공평하게 나눠서 처리하는 것을 말합니다. 단순하죠.  :) 그리고 로드밸런싱은
처리할 구간을 미리 정하지 않고, 처리해야 할 작업에 따라서 각 스레드가 처리할 구간의 크기나 스레드의 개수가 유동적으로 변하게 됩니다. 이때 스레드하나가 이 작업을 담당하게 됩니다. 이 스레드는 작업 상황을 지켜보면서 그때 그때 스레드를 생성해서 작업을 맡기등의 역할을 맡게 되는 거죠. 그래서 작업을 끝낸 스레드가 다음에 처리할 구간을 할당받기 전까지 기다리는 시간이 필요하고, 이런 동기화에 따른 부하가 발생하게 되는 거죠. 나눠주는 구간의 크기가 작을 수록 스레드는 자주 구간을 할당받으려고 기다려야 하고 성능은 더 저하되는 거죠. :)

Parallel.For의 기본 동작은 로드 밸런싱을 사용하도록 되어 있습니다. 그래서 위에서 언급한 동기화 이슈가 자주 발생하는 경우 싱글스레드로 실행되는 for루프에 비해서 그다지 성능의 우위를 보이지 못하게 됩니다. 예제로 확인해볼까욤? ㅋㅋ

using System;

using System.Linq;

using System.Threading.Tasks;

using System.Collections.Concurrent;

using System.Collections.Generic;

 

namespace Exam20

{

    class Program

    {

        static void RangeParallelForEach(int[] nums)

        {

            List<int> threadIds = new List<int>();

            var part = Partitioner.Create(0, nums.Length);

 

            Parallel.ForEach(part, (num, state) =>

            {

                if (!threadIds.Contains(Task.CurrentId.Value))

                {

                    threadIds.Add(Task.CurrentId.Value);

                }

 

                for (int i = num.Item1; i < num.Item2; i++)

                {

                    nums[i] = nums[i] * nums[i];

                }

            });

 

            Console.WriteLine("Thread ID list of RangeForEach");

            foreach (var id in threadIds)

            {

                Console.WriteLine("{0}", id.ToString());

            }

        }

 

        static void ParallelFor(int[] nums)

        {

            List<int> threadIds = new List<int>();

 

            Parallel.For(0, nums.Length, (i) =>

            {

                if (!threadIds.Contains(Task.CurrentId.Value))

                {

                    threadIds.Add(Task.CurrentId.Value);

                }

 

                nums[i] = nums[i] * nums[i];

            });

 

            Console.WriteLine("Thread ID list of ParallelFor");

            foreach (var id in threadIds)

            {

                Console.WriteLine("{0}", id.ToString());

            }

        }

 

        static void Main(string[] args)

        {

            int max = 50000000;

            int[] nums1 = Enumerable.Range(1, max).ToArray<int>();

            int[] nums2 = Enumerable.Range(1, max).ToArray<int>();

 

            ParallelFor(nums1);

            RangeParallelForEach(nums2);

        }

    }

}

위 코드를 보면 로드밸런싱을 사용하는 Parallel.For와 Partitioner.Create 메서드를 이용해서 구간분할을 사용하도록 설정한 Parallel.ForEach를 사용하는 코드를 포함하고 있습니다. 이렇게 수행하면서 각 방법 별로 몇 개의 스레드가 생성되는지 확인한 결과를 볼까요.

Thread ID list of ParallelFor
1
2
3
4
5
6
7
8
9
10
--------------중략--------------
91
92
93
94
95
96
97
98
99
100
101
Thread ID list of RangeForEach
102
103
104
105
106
107
108
Press any key to continue . . .

구간 분할을 사용하는 경우(RangeForEach)는 고작 7개의 스레드가 생성되어서 작업을 처리한 것에 비해서, 로드밸런싱을 사용한 경우는 무려 101개의 스레드가 생성된 것을 볼 수 있습니다. 스레드가 많이 교체되면서 생기는 비효율성은 굳이 말을 안해도 되겠죵. 그렇다면 다음과 같은 코드로 실제로 어떤 속도의 차이가 발생하는지 확인을 해보시져 :)

using System;

using System.Linq;

using System.Threading.Tasks;

using System.Collections.Concurrent;

 

namespace Exam21

{

    class Program

    {

        static void NormalFor(int[] nums)

        {

            for (int i = 0; i < nums.Length; i++)

            {

                nums[i] = nums[i] * nums[i];

            }

        }

 

        static void ParallelFor(int[] nums)

        {

            Parallel.For(0, nums.Length, (i) =>

            {

                nums[i] = nums[i] * nums[i];

            });

        }

 

        static void RangeParallelForEach(int[] nums)

        {

            var part = Partitioner.Create(0, nums.Length);

 

            Parallel.ForEach(part, (num, state) =>

            {

                for (int i = num.Item1; i < num.Item2; i++)

                {

                    nums[i] = nums[i] * nums[i];

                }

            });

        }

 

        static void Main(string[] args)

        {

            int max = 50000000;

            int[] nums1 = Enumerable.Range(1, 10).ToArray<int>();

            int[] nums2 = Enumerable.Range(1, 10).ToArray<int>();

            int[] nums3 = Enumerable.Range(1, 10).ToArray<int>();

 

            //JIT컴파일을 위해.

            NormalFor(nums1);

            ParallelFor(nums2);

            RangeParallelForEach(nums3);

 

            nums1 = Enumerable.Range(1, max).ToArray<int>();

            nums2 = Enumerable.Range(1, max).ToArray<int>();

            nums3 = Enumerable.Range(1, max).ToArray<int>();

 

            DateTime normalForStart = DateTime.Now;

            NormalFor(nums1);

            DateTime normalForEnd = DateTime.Now;

            TimeSpan normalForResult = normalForEnd - normalForStart;

 

            DateTime parallelForStart = DateTime.Now;

            ParallelFor(nums2);

            DateTime parallelForEnd = DateTime.Now;

            TimeSpan parallelForResult = parallelForEnd - parallelForStart;

 

            DateTime rangeForStart = DateTime.Now;

            RangeParallelForEach(nums3);

            DateTime rangeForEnd = DateTime.Now;

            TimeSpan rangeForResult = rangeForEnd - rangeForStart;

 

            Console.WriteLine("Single-threaded for : {0}", normalForResult);

            Console.WriteLine("Load-balancing Parallel.For : {0}", parallelForResult);

            Console.WriteLine("Range-partition Parallel.ForEach : {0}", rangeForResult);

        }

    }

}

좀 어글리한 코드이긴 하지만, 싱글 스레드를 사용한 for루프와 로드 밸런싱을 사용하는 Parallel.For, 그리고 구간 분할을 사용하도록 한 Parallel.ForEach의 성능을 각각 비교한 코드입니다. 그럼 결과를 확인해보죠!

Single-threaded for : 00:00:00.3000763
Load-balancing Parallel.For : 00:00:00.3430858
Range-partition Parallel.ForEach : 00:00:00.2130545
Press any key to continue . . .

조금은 의외의 결과가 나왔습니다. 오히려 병렬로 작업을 처리한 Parallel.For가 순차적으로 실행되는 for루프에 비해서 낮은 성능을 낸 것이죠. 왜 그럴까욤? 첫 번째 원인은 너무 빈번하게 스레드 간의 작업 전환을 하느라 오버헤드가 컸던 점을 들 수 있을 것 같구요. 그리고 다음은, 병렬 작업에 사용되는 델리게이트의 내용이 너무 짧아서 입니다. 위 코드를 보면 델리게이트의 내용이 간단한 연산 한 줄로 이루어진 것을 볼 수 있는데요. 위 처럼 호출 수가 작은 경우는 큰 문제가 되지 않지만, 위의 코드 처럼 많은 횟수를 호출하는 경우 델리게이트 호출은 부담이 됩니다. 델리게이트의 내용을 처리하는 것 보다 델리게이트를 호출하는 시간이 더 많이 걸리게 되면서 일반 for루프보다도 못한 성능을 보여주는 거죠. 그래서 Partitioner.Create메서드를 이용해서 큰 작업 덩어리로 잘라줘서 이 오버헤드를 줄일 수 있습니다. 그 결과 위와 같은 시간이 나오게 된거죠.

어떻나요? 단순히 for루프를 Parallel.For로 교체하는 것만 가지고는 제대로된 성능을 기대하기 힘듭니다. 혹시, "어? 뭐야 당신 이전 포스트에서는 단순히 Parallel.For를 쓰기만 해도 성능이 좋아진다며?" 하는 생각이 드셨다면... 당신은 저의 팬...???!?!? ㅋㅋㅋㅋ 아마 기억하시는 분은 없겠죠. 암튼, 주의하고 볼일 입니다. 오늘은 여기까징~! :)
저작자 표시 비영리 변경 금지
신고
크리에이티브 커먼즈 라이선스
Creative Commons License
지난 포스팅에서 Break와 Stop메서드의 차이에 대해서 시덥잖은 퀴즈를 남겼었는데욤, 뭐 별로 궁금한 분들이 없거나 너무 쉬워서 코에서 바람 좀 내뿜은 분들이 많으리라 생각합니다. 저는 이게 뭔 차이인지 잠깐 헤맸습니다만... 허허허 :) 그럼 차이점에 대해서 잠깐 이야기를 해볼까욤.


- Parallel.For와 for의 브레끼는 닮았더라.

Parallel.For의 Break메서드는 for의 break와 좀 닮았습니다. for루프에서 break를 만나게 되면 그 즉시 for루프를 중단하게 됩니다. 그리고 break가 걸리기 전 까지의 인덱스에 대해서는 루프가 실행되었음을 보장할 수 있습니다. for루프는 처음부터 순차적으로 실행되기 때문이죠. 그런데 Parallel.For에서는 루프문의 길이에 대해서 각각의 스레드가 조금씩 나눠서 담당하게 됩니다. 예를 들면 1-10000까지의 구간이라면, 스레드 하나가 1-5000까지, 다른 스레드가 5001-10000까지 뭐 이런식으로 말이죠. 이런 상황에서 5100번째 인덱스에서 Break가 호출된다면 어떻게 될까요? 기존의 for루프를 사용하던 관점에서 보자면 당연히 5109번째 인덱스까지는 다 처리되었을 거라고 예상하게 됩니다. 이런 개발자의 기대를 저버리지 않기 위해서 Break메서드를 호출하게 되면 Break가 호출된 인덱스보다 작은 인데스에 대해서 남아있는 작업은 모두 실행하도록 합니다. 예제를 볼까요?

using System;

using System.Threading.Tasks;

 

namespace ConsoleApplication1

{

    class Program

    {

        static void Main(string[] args)

        {

            Parallel.For(0, 20, (i, loopState) =>

            {

                if (i == 10)

                {

                    loopState.Break();

                    Console.WriteLine("Break at {0}", i.ToString());

                }

                else

                {

                    Console.WriteLine(i.ToString());

                }

            });

        }

    }

}


아주 간단하게 0부터 19까지 Parallel.For루프를 돌면서 i가 10일때 Break를 호출해서 루프를 중단하도록 하는 코드입니다. 실행할 때마다 결과가 다르게 나오는데요, 그 중에 가장 설명하기 좋은 결과를 고르면!

0

1

2

3

4

6

5

Break at 10

7

8

9

Press any key to continue . . .


대략 이렇습니다. 스레드가 구간을 나눠서 루프를 실행하는데, 먼저 실행한 스레드가 0-6까지를 처리하고 다른 스레드가 10을 처리하는 순간 Break를 만나게 되는 거죠. 하지만 여기서 중단 되지 않고, 다른 스레드가 7, 8, 9를 처리하도록 해줍니다. 그래서 10에서 Break가 호출되었더라도 바로 중단하지 않고 10보다 작은 인덱스가 모두 처리되도록 해주는 것이죠.


- 장비를 정지합니다... 어 안되잖아?

Break를 즉시 중단하는 개념으로 생각하다 보면 조금은 이상할 수 있습니다만, 사실 기존의 for루프와 유사한 동작을 제공한다는 개념에서는 바람직한 동작이 아닌가 싶습니다. 그런데, 데이터에서 뭔가를 검색하는 경우는 어떨까요? 4명이 근무하는 사무실을 생각해봅시다. 뭔가 중요한 서류를 찾아야 해서 사장 이하 4명이 구역을 나눠서 사무실을 뒤지기 시작합니다. 한 명은 책상을, 한 명은 캐비넷을, 또 한 명은 창고를, 또 한 명은 잉여짓을. 이렇게 4명이 찾다가 누군가 한 명이 그 서류를 발견했습니다!!! 그러면 그 사람은 '심봤다 어허으어릉러허엏엉 나 심봤쪄영 뿌우 'ㅅ'~'하고 외치겠죠? 그러면 그 사람들은 그 찾는 작업을 멈추게 됩니다. 그게 정상이겠죠. '그럴리가 없엉 어흐어흐어흐러라어헝'하면서 나머지를 계속 찾는 사람은 '안돼에~'. 허허허허 :)

이런 경우에는 Parallel.For를 즉시 중시해야 합니다. 그럴때! 바로 Stop을 호출하게 됩니다.

using System;

using System.Threading.Tasks;

 

namespace ConsoleApplication1

{

    class Program

    {

        static void Main(string[] args)

        {

            Parallel.For(0, 20, (i, loopState) =>

            {

                if (i == 10)

                {

                    loopState.Stop();

                    Console.WriteLine("Stop at {0}", i.ToString());

                }

                else

                {

                    Console.WriteLine(i.ToString());

                }

            });

        }

    }

}

Break대신에 Stop을 호출한 코드죠. 결과능!

0

5

Stop at 10

Press any key to continue . . .


짤 없습니다. 그냥 중단해버리는 거죠. 자 지난 포스팅에서 왜 Stop을 호출한 게 미세하게 빨랐는지 아시겠나요? 소수는 1과 자신 외에 하나라도 나눌 수 있는 숫자가 존재하면 안되는 거죠. 그래서 for루프에서 숫자를 바꿔가면서 나눠보다가 하나라도 나눠지는 게 발견되면 그 즉시 중단하는 게 맞는 거죠. Break는 발견되더라도, 그 보다 작은 인덱스에 대해서는 모두 수행하게 되기 때문에 Stop에 비해서 아주 미세하게 느린 결과가 나오게 되는 거죵. 허허허허허 :) 참 별 내용 없네요 ㅠㅠ...

그럼 오늘은 여기까징~!
저작자 표시 비영리 변경 금지
신고
크리에이티브 커먼즈 라이선스
Creative Commons License

김명신님께서 또 아주 좋은 지적을 해주셨습니다~~ List<T>도 ConcurrentBag<T>이든 하나의 컬렉션을 이용하는 건 바람직하지 않은 것 같다는 의견이셨는데요, 동감합니다 :) 그래서 제 나름대로 해결책을 강구해봤는데욤. 스레드 로컬 변수를 이용해서 스레드 별로 작업을 시키고, 나중에 하나의 리스트로 모으는 것입니다. 코드를 볼까욤~?

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
 
namespace ConsoleApplication1
{
    class Program
    {
        static readonly object _sync = new object();
 
        static IEnumerable<long> GetPrimeNumber(long num)
        {
            List<long> primeList = new List<long>();
 
            Parallel.For<List<long>>(0, num + 1, 
                () => new List<long>(), //스레드 로컬 변수 초기화
                (i, outerLoopState, subList) =>
            {
                bool isPrime = true;
                Parallel.For(2, i, (j, loopState) =>
                {
                    if (i % j == 0)
                    {
                        isPrime = false;
                        loopState.Break();
                    }
                });
 
                if (isPrime)
                {
                    subList.Add(i); //스레드 로컬 리스트에 추가
                }
 
                return subList;
            },
            (subList) => //각 스레드 종료 후에 취합
            {
                lock (_sync)
                {
                    primeList.AddRange(subList);
                }
            });
 
            return primeList;
        }
 
        static void Main(string[] args)
        {
            Stopwatch sw = new Stopwatch();
            sw.Start();
            IEnumerable<long> primeList = GetPrimeNumber(99999);
            sw.Stop();
 
            Console.WriteLine("Elapsed : {0}, Found prime counts : {1}",
                sw.Elapsed.ToString(),
                primeList.Count());
            //뭔가 한다.
        }
    }
}

위 코드를 보면, Parallel.For 루프에서 스레드 로컬 변수를 사용하도록 밑줄 친 부분들이 추가된 것을 볼 수 있습니다. 즉 이 Parallel.For는 List<long>타입의 스레드 지역 변수를 사용하며, 그 지역변수를 어떻게 초기화 할 것인지를 명시하고, 각 스레드가 종료할 때 지역 변수를 리턴하도록 한 것이죠. 그리고 Parallel.For의 마지막 매개변수로, 각 스레드가 종료할 때 어떤 동작을 취할 것인가를 명시하는데, 스레드가 끝날 때 리턴한 지역 변수가 매개변수로 들어오게 됩니다. 이렇게 하면 동기화를 좀 더 줄일 수 있는 것이죠. 그래서, 이왕 코드를 고친 김에 시간을 재봤습니다.

   ConcurrentBag<T>사용(초)  스레드 지역 변수 사용(초)
 GetPrimeNumber(99999)  2.54  2.52 
 GetPrimeNumber(199999)  9.32  9.25
 GetPrimeNumber(299999)  20.03  19.96

미세하게 스레드 지역 변수를 사용한 쪽이 빠르긴 한데요, 위 코드의 경우 하나의 리스트로 값을 합치는 데 시간이 좀 소요되어서 큰 속도 향상이 없는 것으로 보입니다. 동기화해서 수행해야 하는 작업이 간단할 수록 속도차이는 벌어질 것으로 보이는데요, 가능하면 각자 작업 후에 하나로 합치는 쪽이 더 효율적일 것 같네요.

부족한 실력이다 보니 이 이상 뭐가 떠오르지 않네요 -_-;; 더 나은 방법이 있으면 알려주세용~ :)

저작자 표시 비영리 변경 금지
신고
크리에이티브 커먼즈 라이선스
Creative Commons License

자~ 그럼 이제 부터는 다시 병렬 프로그래밍으로 돌아가서 조금 이야기를 해보도록 할 까욤~? 오래전에 써놓은 자료를 보다보니..... 제가 쓴 내용이지만 무슨 내용인지 이해가 안되는 글이 많아서 다시 정리를 하느라 조금 애를 먹고 있습니다. 하하하하 :) 뭐 제 수준이 딱 거기 까지니까 말이죵 호호호호.


- 병렬 작업을 중단할 때능?

기존의 for루프를 중단할 때는 그냥 간단하게 break하나 추가해넣으면 되었습니다. for루프는 정해진 순서에 따라서 순차적으로 실행되기 때문에 특정 조건에서 break를 만나서 루프가 중단 된다고 하더라도 항상 취소되기 이전까지의 내용은 모두 실행이 되었음을 보장할 수 있었죠. 즉,

for (int i = 0; i < 100; i++)

{

    if (i == 50)

    {

        break;

    }

}


위와 같은 코드가 있다고 할 때, i가 50이되어서 for루프가 중단된다고 할 때, i가 0-49일때는 이미 다 진행된 상태라는 것이죠. 그런데 이런 루프를 빠르게 처리하기 위해서 여러 스레드를 동시에 사용하는 Parallel.For를 사용했다고 한다면... 각 스레드가 나눠서 진행중인 작업을 어떻게 중단 시켜야 할까요?


- 브레이크를 걸자.

일단은 기존의 for루프와 가장 유사한 형태를 보이는 것 부터 살펴봅시당. 일단 비교를 위해서 다음과 같은 예제를 먼저 살펴보겠습니다.

using System;

using System.Collections.Concurrent;

using System.Collections.Generic;

using System.Diagnostics;

using System.Linq;

using System.Text;

using System.Threading.Tasks;

 

namespace ConsoleApplication1

{

    class Program

    {

        static IEnumerable<long> GetPrimeNumber(long num)

        {

            List<long> primeList = new List<long>();

 

            for (long i = 0; i <= num; i++)

            {

                bool isPrime = true;

                for (long j = 2; j < i; j++)

                {

                    if (i % j == 0)

                    {

                        isPrime = false;

                        break;

                    }

                }

                if (isPrime)

                {

                    primeList.Add(i);

                }

            }

 

            return primeList;

        }

       

        static void Main(string[] args)

        {

            Stopwatch sw = new Stopwatch();

            sw.Start();

            IEnumerable<long> primeList = GetPrimeNumber(99999);

            sw.Stop();

 

            Console.WriteLine("Elapsed : {0}, Found prime counts : {1}",

                sw.Elapsed.ToString(),

                primeList.Count());

            //뭔가 한다.

        }

    }

}


아주 간단한 코드인데요, 1부터 사용자가 입력한 숫자 중에서 소수를 구해서 리스트에 추가하는 코드입니다. 그리고 각 수가 소수인지 검사하는 부분에서 1이 아닌 수로 나눠지는 케이스가 발견 되면 그 즉시 break를 이용해서 중단을 시키고 있죠. 위 코드를 실행하면 CPU점유율은 아래와 같이 단일 스레드를 이용하는 것을 볼 수 있습니다.


그리고 위 코드를 3번 실행해서 얻은 평균 실행 시간은 6.28초 입니다. 그렇다면 위 코드의 Parallel.For버전을 살펴 볼까욤?

using System;

using System.Collections.Concurrent;

using System.Collections.Generic;

using System.Diagnostics;

using System.Linq;

using System.Text;

using System.Threading.Tasks;

 

namespace ConsoleApplication1

{

    class Program

    {

        static IEnumerable<long> GetPrimeNumber(long num)

        {

            List<long> primeList = new List<long>();

 

            Parallel.For(0, num + 1, (i) =>

            {

                bool isPrime = true;

                Parallel.For(2, i, (j, loopState) =>

                {

                    if (i % j == 0)

                    {

                        isPrime = false;

                        loopState.Break();

                    }

                });

 

                if (isPrime)

                {

                    primeList.Add(i);

                }

            });

 

            return primeList;

        }

       

        static void Main(string[] args)

        {

            Stopwatch sw = new Stopwatch();

            sw.Start();

            IEnumerable<long> primeList = GetPrimeNumber(99999);

            sw.Stop();

 

            Console.WriteLine("Elapsed : {0}, Found prime counts : {1}",

                sw.Elapsed.ToString(),

                primeList.Count());

            //뭔가 한다.

        }

    }

}


for를 Parallel.For로 바꿨고, break역시 Parallel.For의 인자로 넘어오는 loopState변수에 대해서 Break메서드를 호출하는 것으로 변경되었습니다. 이 코드를 실행해보면, 아래와 같이 CPU의 4개의 코어가 모두 동작하는 것을 볼 수 있습니다.




그리고 역시 위 코드를 3번 실행해서 얻은 평균 실행 시간은 5.32 초입니다. 전에 비해서 약 15%의 성능향상이 있었습니다. 물론 소수를 구하는 알고리즘 자체를 더 개선할 수도 있지만, 단순히 싱글 스레드를 이용한 코드와 작업을 병렬화 시켜서 멀티 코어를 이용한 코드간의 차이가 이정도라면 의미가 있지 않을까요? 그리고 좀더 CPU를 많이 사용하는 코드일 수록 그 차이는 더 벌어지겠죠 :)


- 그런데 사실은...

위 코드에서 몇자를 바꾸고 나면, 코드의 평균 실행 시간이 5.29초로 조금 더 떨어지게 됩니다. 그 코드를 올려드릴 테니 어디를 수정한 건지 한번 찾아보시죵 :)

using System;

using System.Collections.Concurrent;

using System.Collections.Generic;

using System.Diagnostics;

using System.Linq;

using System.Text;

using System.Threading.Tasks;

 

namespace ConsoleApplication1

{

    class Program

    {

        static IEnumerable<long> GetPrimeNumber(long num)

        {

            List<long> primeList = new List<long>();

 

            Parallel.For(0, num + 1, (i) =>

            {

                bool isPrime = true;

                Parallel.For(2, i, (j, loopState) =>

                {

                    if (i % j == 0)

                    {

                        isPrime = false;

                        loopState.Stop();

                    }

                });

 

                if (isPrime)

                {

                    primeList.Add(i);

                }

            });

 

            return primeList;

        }

        

        static void Main(string[] args)

        {

            Stopwatch sw = new Stopwatch();

            sw.Start();

            IEnumerable<long> primeList = GetPrimeNumber(99999);

            sw.Stop();

 

            Console.WriteLine("Elapsed : {0}, Found prime counts : {1}",

                sw.Elapsed.ToString(),

                primeList.Count());

            //뭔가 한다.

        }

    }

}


어딘지 찾으셨나요? 바로 loopState변수에 대해서 Break를 호출하던 것을 Stop으로 바꿔준 것입니다. 그래서 조금 더 효율적인 코드가 되는 것이죠. 왜 그럴까요? 그거에 대해서는!!!! 다음 포스팅에서 ㅋㅋㅋㅋㅋ


- 참고자료
http://stackoverflow.com/questions/1510124/program-to-find-prime-numbers

저작자 표시 비영리 변경 금지
신고
크리에이티브 커먼즈 라이선스
Creative Commons License

- 뭐 좀 더 편한 방법이?

지난 포스팅에서는 Monitor를 좀 더 간편하게 사용할 수 있게 해주는 lock구문에 대해서 살펴봤었습니다. 그런데... 더 편한 방법이 있다는 군요. 한 줄이면 된다고 합니다. 긴 말 필요없이 예제를 보죠!

using System;

using System.Threading.Tasks;

using System.Threading;

 

namespace Exam25

{

    class Program

    {

        static readonly int count = 10000000;

        static int sum = 0;

 

        static void IncreaseByOne()

        {

            for (int i = 0; i < count; i++)

            {

                Interlocked.Increment(ref sum);

            }

        }

 

        static void Main(string[] args)

        {

            Task task = Task.Factory.StartNew(IncreaseByOne);

 

            for (int i = 0; i < count; i++)

            {

                Interlocked.Decrement(ref sum);

            }

 

            task.Wait();

            Console.WriteLine("Result = {0}", sum);

        }

    }

}

위 예제는 정확하게 lock을 사용한 예제와 동일한 결과를 냅니다. Interlocked라는 클래스에 정의된 메서드를 통해서 1씩 증가시키고 감소를 시킨거죠. 즉, lock사용해서 명시적으로 락을 획득하는 대신에 스레드 안전한 방식으로 값을 증가시키고 감소시키도록 하는 것입니다. 그런데, 여기까지만 보면 약간의 의문이 생깁니다. 생각보다 더 간단해 보이지가 않는 다는 말이죠. 허허허허 :)

그런데 한 가지 장점이 더 있습니다. 바로 수행 속도 인데요. 잠금을 획득하고 해제하는 게 시간이 많이 걸리는 작업이다 보니, lock을 사용하는 것보다 Interlocked를 사용하는 게 조금 더 빠릅니다. 그럼 얼마나 더 빠르다는 건지 간단한 코드를 통해서 직접 확인을 해볼까욤?

using System;
using System.Threading.Tasks;
using System.Threading;

namespace Exam26
{
    class Program
    {
        readonly static object sync = new object();
        static readonly int count = 10000000;
        static int sum = 0;       

        static void SpeedTest()
        {
            DateTime lockStart = DateTime.Now;
            for (int i = 0; i < count; i++)
            {
                lock (sync)
                {
                    sum++;
                }
            }
            DateTime lockEnd = DateTime.Now;
            TimeSpan lockResult = lockEnd - lockStart;

            sum = 0;

            DateTime InterlockedStart = DateTime.Now;

            for (int i = 0; i < count; i++)
            {
                Interlocked.Increment(ref sum);
            }
            DateTime InterlockedEnd = DateTime.Now;
            TimeSpan InterlockedResult = InterlockedEnd - InterlockedStart;

            Console.WriteLine("lock Result : {0}", lockResult);
            Console.WriteLine("Interlocked Result : {0}", InterlockedResult);
        }

        static void Main(string[] args)
        {
            //JIT 컴파일 수행
            SpeedTest();

            SpeedTest();
        }
    }
}


수행된 결과를 볼까요?

lock Result : 00:00:00.3270844
Interlocked Result : 00:00:00.1820249
lock Result : 00:00:00.3090990
Interlocked Result : 00:00:00.1820285
Press any key to continue . . .

두 번째 실행된 결과를 보면 Interlocked를 사용한 쪽이 70%정도 더 빠른 속도를 보이는 것을 할 수 있습니다. 위 코드 같이 단순한 연산이 많이 수행되는 경우에는 Interlocked를 사용하는 쪽이 조금 더 유리할 수 있겠죠. 물론 요즘 세상에 별로 신경쓸만한 차이는 아니라고 생각이 듭니다. 하하하하 그냥 이런 것도 있다는 거죠 :)

그럼, 오늘도 이만 하구요~ 다음에 또 뵙죵 하하하 :)

저작자 표시 비영리 변경 금지
신고
크리에이티브 커먼즈 라이선스
Creative Commons License
- 인류 발전의 원동력.

귀.찮.다. 이 세자로 이루어진 표현이 인류의 발전을 이끌어왔다면 확대해석일까욤. 정말 죠낸죠낸쵸오낸 귀찮다 보니 사람들은 어떻게 하면 반복적인 작업을 줄일 수 있을까 하는 문제를 생각하게 됩니다. 그래서 더 나은 방법을 고안해 내고, 더 빠르고 더 간편한 시스템, 그리고 나아가서 자동화를 생각하게 됩니다. 그러다 보니, 수작업으로 이루어진 프로세스는 실수의 여지가 많은 반면에 자동화로 이루어진 프로세스에서는 그 여지가 굉장히 줄어들게 되는 부수적인 효과를 얻게 되는 것이죠.

물론 귀찮음으로 인해서 인류는 많은 퇴보도 했을 겁니다. 복잡하고 비효율적인 것도 몸에 익으면 편하게 느껴지게 됩니다. 그리고 더 깔끔한 방법으로 바꾸려 하면 머리속에서는 엄청난 저항이 일어나게 되죠. 이미 한번 구성되어서 굳어진 뇌의 회로를 다시 구성해야 한다는 그...... 귀찮음. 허허허허허허


- 락에 자동화를 도입!

지난 포스트에서 Monitor를 이용해서 락을 거는 방법을 알아봤습니다. 하지만 어떤가요? 매번 try-finally블록을 사용해서 락을 걸고 해야 한다면? 그리고 Enter는 있는데 Exit를 깜빡하게 빼먹었다면? 어머나!!!!

그래서 C#에서는 귀찮음을 싫어하는 인류의 특성을 감안해서 간편한 Syntactic Sugar를 제공합니다. lock이라는 구문이 바로 그것입니다. 하하하하 :) 그럼 간단한 예를 보죠.

using System;

using System.Threading.Tasks;

 

namespace Exam24

{

    class Program

    {

        readonly static object sync = new object();

        static readonly int count = 10000000;

        static int sum = 0;

 

        static void IncreaseByOne()

        {

            for (int i = 0; i < count; i++)

            {

                lock (sync)

                {

                    sum += 1;

                }

            }

        }

 

        static void Main(string[] args)

        {

            Task task = Task.Factory.StartNew(IncreaseByOne);

 

            for (int i = 0; i < count; i++)

            {

                lock (sync)

                {

                    sum -= 1;

                }

            }

 

            task.Wait();

            Console.WriteLine("Result = {0}", sum);

        }

    }

}

어떤가요? Monitor를 사용해서 해주던 번거로운 작업이 lock이라는 구문으로 임계영역을 감싸주는 것으로 간단하게 해결 되었습니다. 자, 그럼 lock이라는 편리한 구문 뒤에 실제로 생성되는 코드를 한번 확인해 볼까욤?

private static void Main(string[] args)

{

    Task task = Task.Factory.StartNew(new Action(Program.IncreaseByOne));

    for (int i = 0; i < count; i++)

    {

        object CS$2$0000;

        bool <>s__LockTaken1 = false;

        try

        {

            Monitor.Enter(CS$2$0000 = sync, ref <>s__LockTaken1);

            sum--;

        }

        finally

        {

            if (<>s__LockTaken1)

            {

                Monitor.Exit(CS$2$0000);

            }

        }

    }

    task.Wait();

    Console.WriteLine("Result = {0}", sum);

}

 

private static void IncreaseByOne()

{

    for (int i = 0; i < count; i++)

    {

        object CS$2$0000;

        bool <>s__LockTaken0 = false;

        try

        {

            Monitor.Enter(CS$2$0000 = sync, ref <>s__LockTaken0);

            sum++;

        }

        finally

        {

            if (<>s__LockTaken0)

            {

                Monitor.Exit(CS$2$0000);

            }

        }

    }

}

위에서 볼 수 있듯이 생성되는 코드는 Moniter를 이용한 코드와 정확하게 같은 코드를 사용하고 있습니다. 대신에 훨씬 간단하죠? 물론 lock구문을 통해서 락을 걸어줄 객체는 Monitor를 사용할 때와 동일하게 readonly static이어야 합니다.
 
락을 거는 대상이 되는 잠금 객체에 대해서 조금 덧붙이자면, 락을 걸어줄 객체는 값 형(Value 타입)이 아닌 참조 형(Reference 타입) 객체여야 합니다. 왜냐면, 값 형의 경우는 다른 변수에 값을 대입할 때 값을 복사해서 넘겨 주기 때문에 매번 메모리에 동일한 값이 새로 생겨나게 됩니다. 이렇게 되면 Enter메서드에서 받는 잠금 객체와 Exit메서드에서 받는 잠금 객체가 서로 다른 객체가 되므로 SynchronizationLockException이 발생하게 됩니다. 반면에 참조 형 객체의 경우는 동일한 객체에 대한 참조를 복사해서 넘겨주므로 항상 동일한 객체에 대해서 Enter와 Exit를 호출하게 되겠죠.

그럼~ 다음에 또 뵙죵~ :)
저작자 표시 비영리 변경 금지
신고
크리에이티브 커먼즈 라이선스
Creative Commons License

우와~!!!! 증말 오랜만입니다. 이게 얼마만이죠? 하하하하하하하하-_-

기다리는 분이 아무도 없다는 건 알지만, 저 자신을 위해서 그냥 한번 호들갑을 떨어봤습니다. 허허허허 원래 제가 쓰는 포스팅이 다 그렇죠. 병렬 프로그래밍에 대해서 정리해놓은 자료가 조금 있었는데요, 매우 늦긴 했지만 그래도 일단 공부하면서 정리해놓은 자료를 그냥 썩히기는 아까워서 조금씩 옮겨 적어볼까 합니다. 그래서 다시 시작하는 거지요! 호호호호 :)

언제나 그렇듯이 제 포스팅은 공부하면서 정리하는 겁니다. 다른 분들이 적으시는 것 처럼 내공이 깊지도 않고, 그다지 실용적이지도 않지요. 의견이 있으시면 거침없이! 하지만 예의는 쪼금 지켜 주시면서! 가르침을 더 해주시면 읽으시는 분들에게 더 도움이 될 겁니다. 짧은 인생 Love & Peace! 그럼 또 초큼씩 풀어 볼까욤? ㅋㅋㅋ


- 화장실이 한 개!!!

4인 가족 기준으로 화장실이 한 개라고 생각을 하면..... 어머나!!! 끔찍해!!! 출근 시간이 되면 화장실에 응가를 때리러 러시아워가 벌어집니다. 먼저 들어간 사람은 느긋하게 다이빙하는 응가에 예술 점수를 매기면서 시간을 보낼 수 있지만, 출근 시간과 이제 그만 포기하자고 하는 장과 협상을 벌이면서 조마조마해야 하죠. 화장실을 원하는 사람은 4명인데, 화장실이 하나이다 보니, 먼저 들어간 사람이 화장실을 선점하고 문을 잠가서 락을 걸어버리면, 화장실을 원하는 다른 사람들은 그 락이 해제 될 때까지 인고의 시간을 보내야 하는 거죠. 어머나 슬픈이야기...ㅠㅠ

그러면 락을 걸지 못하게 하면 효과적인 해결책이 될까요? 일보고 있는데 갑자기 문을 열고 들어와서는 '난 얼굴 씻고 이빨만 닦으련다 변기를 쓰는 것도 아니니 너랑 내가 이 화장실을 평화롭게 공유하면서 각박한 출근 시간에 조그마한 훈훈함이라도 나눠가지지 않겠느냐' 하면 뭐 괘안을 수도 있겠습니다만.... 조금만 더 생각을 해보면 말이죠. 응가라는 건 굉장한 집중이 요구됩니다. 그리고 아무도 보는 사람이 없다는 안도감 속에서야 차분히 진행될 수 있는 일종의 비밀 의식 같은 거죠. 화장실을 공유하는 훈훈함 속에서 응가를 제대로 할 수 는 없습니다. 그리고 세면을 하는 사람 역시 은은하게 풍겨나오는 향기에 정신을 빼앗기다 보면 어서빨리 탈출하고 싶은 욕구에 사로잡히게 되어 세면을 제대로 할 수 없게 됩니다.

즉, 락을 거는 게 가장 효율적이라는 거죠. 여담이지만 그래서 저는 공중화장실에서는 응가를 안하는 편입니다. 정신 사나워서 집중을 할 수 없거든요. :)


- 그래서 필요한 동기화

그래서 프로그래밍을 하면서도 동기화에 신경을 써줘야 합니다. 컴퓨터의 자원을 한정적이고 그걸 호시탐탐 선점하려는 스레드의 욕구를 잘 제어해줘야 하기 때문이죠. 그럼 동기화 없이 스레드의 탐욕에 맡긴 결과가 어떻게 되나 한번 볼까요. 후후후후

using System;

using System.Threading.Tasks;

 

namespace Exam22

{

    class Program

    {

        static readonly int count = 10000000;

        static int sum = 0;

 

        static void IncreaseByOne()

        {

            for (int i = 0; i < count; i++)

            {

                sum += 1;

            }

        }

 

        static void Main(string[] args)

        {

            Task task = Task.Factory.StartNew(IncreaseByOne);

 

            for (int i = 0; i < count; i++)

            {

                sum -= 1;

            }

 

            task.Wait();

            Console.WriteLine("Result = {0}", sum);

        }

    }

}

두 개의 스레드를 이용해서, 주 스레드에서는 sum의 값을 1씩 감소 시키고, 추가 스레드에서는 증가 시키도록 해놓은 평범한 예제입니다. 하지만, 서로 뺏고 뺏기는 리얼 야생 탐욕이 살아 숨쉬는 정글의 법칙이 녹아들어 있습니다. 결과를 보시죠.

Result = 17104

계속하려면 아무 키나 누르십시오...

분명히 같은 횟수로 1씩 증가시키고 감소를 시켰는데 이런 결과가 나오다니!!! 매번 실행 할때 마다 다른 값이 나오긴 하지만, 이 결과는 분명 이상합니다. 오래전 포스팅이지만 원자성에 대해서 잠깐 이야기를 한적이 있었는데요, 특정 작업이 실행 될 때 그 작업 외에 다른 작업이 끼어들어서는 안된다는 원칙을 이야기 하는 것입니다. 그렇다면! 위에서 사용중인 +=, -= 연산자는 분명히 원자성을 위반하고 있는 것으로 보입니다.

단순하게 'a += 1' 을 가지고 생각해볼까욤. 이 연산은 몇 가지 단계 나누어 집니다. 우선 a의 값을 가져오고, 가져온 값을 1 증가 시키고, 증가된 값을 다시 a에 대입하는 단계로 나누어 지는 거죠. 그런데 a값을 가져온 직후에 다른 스레드가 a의 값을 변화시킨다면 어떻게 될까요? 이런 거 말로 설명하면 설명하는 사람도 읽는 사람도 불통에 빠지게 됩니다. 현재 sum이라는 변수의 값이 5라고 가정하고 간단하게 표를 통해 설명 해보죠 ㅋ

 증가 스레드(sum += 1 수행) 감소 스레드(sum -= 1 수행) 
   5를 읽어옴
 5를 읽어옴  
 1증가시킴  
 6을 저장  
  1감소시킴 
  4를 저장 

위와 같은 순서로 수행이 된다면?

분명히 1증가와 1감소가 수행되었지만, 결과는 1감소만 수행된 셈이죠. 이런식으로 서로 방해와 방해를 하다보면 이상한 결과가 나오게 되는 겁니다. 그래서 화장실에 락을 걸어서 순서를 정해서 사용하도록 하듯이, 순서를 정해줘야 하는 겁니다. 하하하하 :)

- 모니터로 락을 걸자! 락을 걸자!

자 그럼 아주 간단하게, 모니터로 락을 걸어 볼까요? 준비물은 여러분이 지금 보고 있는 모니터를 사용하면 됩니....는 훼이크고 Monitor라는 클래스를 이용해서 락을 걸어보겠습니다. Enter와 Exit라는 메서드를 이용해서 락을 걸고 빠져나가도록 통제를 하게 되는 데요. 참고로 이렇게 둘 이상의 스레드가 동시에 접근하면 안되기 때문에 락을 걸어야 하는 부분을 임계여역(Critical Section)이라고 합니다. 허허허허. 그럼 Monitor를 사용한 예제를 한번 보시죰.

using System;

using System.Threading.Tasks;

using System.Threading;

 

namespace Exam23

{

    class Program

    {

        readonly static object sync = new object();

        static readonly int count = 10000000;

        static int sum = 0;

 

        static void IncreaseByOne()

        {

            for (int i = 0; i < count; i++)

            {

                bool locked = false;

                Monitor.Enter(sync, ref locked);

                try

                {

                    sum += 1;

                }

                finally

                {

                    if (locked)

                    {

                        Monitor.Exit(sync);

                    }

                }

            }

        }

 

        static void Main(string[] args)

        {

            Task task = Task.Factory.StartNew(IncreaseByOne);

 

            for (int i = 0; i < count; i++)

            {

                bool locked = false;

                Monitor.Enter(sync, ref locked);

                try

                {

                    sum -= 1;

                }

                finally

                {

                    if (locked)

                    {

                        Monitor.Exit(sync);

                    }

                }

            }

 

            task.Wait();

            Console.WriteLine("Result = {0}", sum);

        }

    }

}

자, 코드를 보면 락이 걸려있는지를 판별하기 위해서 readonly static으로 sync라는 변수를 선언하고, 그 변수에 대해서 락을 걸로 해제하는 것으로 특정영역에 대한 락을 걸도록 했습니다. 그리고 Monitor를 통해서 들어가고 나오는 순서를 정해주고 있죠. 만약에 주 스레드에서 sync에 대해서 락을 획득하고 sum을 감소시키는 중이라면, 다른 스레드는 sync에 대한 락이 해제 될 때까지 임계영역에 들어가지 못하고 대기하게 됩니다. 그렇다면? 각자가 원하는 작업을 하는 동안 다른 스레드가 방해를 하지 못하겠죵?

참고로 락을 걸고 해제하는 대상이 되는 sync라는 객체를 보면, readonly이므로 객체의 값을 변경시킬 수 없는 immutable한 객체이고, static이므로 유일하게 존재하게 됩니다. 이렇게 복사본이 없고, 값을 변경시킬 수 없는 객체에 대해서 락을 거는 것이 안전한 방법이지요. 위 코드를 실행하면 다음과 같은 결과가 나오게 됩니다.

Result = 0

계속하려면 아무 키나 누르십시오...

이거시 바로 우리가 기대했던 결과지요. 하하하하하하하하. 아... 좀 불편한 자세로 포스팅을 하고 있으려니 허리가..... 일단 오늘은 여기까지 하겠슴니다 하하하하 :)

저작자 표시 비영리 변경 금지
신고
크리에이티브 커먼즈 라이선스
Creative Commons License

[미리보는 C++ AMP-3] array와 array_view

DirectX 11 2011.12.21 08:00 Posted by 조진현

들어가기 앞서 지금까지 AMP가 GPU를 활용하는 프로그래밍 기법이라고,
제가 지속적으로 언급해 왔었습니다.
사실 이 말은 적절하지 않는 표현이였습니다.

얼마 전까지만 해도, 개발자에게 주어지는 프로세싱 유닛은 CPU와 GPU 뿐이였습니다.
CPU는 개발자의 활용 영역에 있었지만, GPU는 제한적으로 사용할 수 있었습니다.
왜냐하면 GPU를 사용하기 위해서는 DirectX API 사용이 필수였기 때문입니다.
그 DirectX 의 영역을 일반적인 개발자 영역으로 확장하는 것이 C++ AMP 입니다.
그런데 최근에 CPU와 GPU를 통합한 APU 라는 것이 등장했습니다.
앞으로 또 다른 프로세싱 유닛이 등장할지도 모르는 일입니다.
그래서 이런 프로세싱 유닛들을 통합한 용어가 필요하게 되었고,
C++ AMP에서는 이를 accelerator 라고 합니다.
즉, CPU와 GPU 그리고 APU 가 이 accelerator 에 속한다고 할 수 있습니다.
accelerator 는 C++ AMP 코드가 실행될 수 있는 이런 타겟을 표현합니다.
그래서 C++ AMP는 이 accelerator를 활용하는 프로그래밍 기법이라고
해석하는 것이
더 적절한 표현입니다.
앞으로 이 accelerator 라는 표현을 많이 사용할 것이니 확실히 알아두시기 바랍니다.


앞서 간단하게 작성했던 샘플을 다시 한번 보겠습니다.
 

void AddArrays(int n, int * pA, int * pB, int * pC)

{

    array_view<int,1> a(n, pA);

    array_view<int,1> b(n, pB);

    array_view<int,1> sum(n, pC);

 

    parallel_for_each(

        sum.grid,

        [=](index<1> i) restrict(direct3d)

        {

            sum[i] = a[i] + b[i];

        }

     );

}



array_view 라는 것이 먼저 눈에 보입니다.
C++ AMP 에서는 대규모 메모리를 의미하는 클래스로
array 와 array_view 라는 것이 있습니다.
기본적으로 이 두 클래스의 목적은
accelerator 상으로 데이터를 옮기기 위함 입니다.


array 의 경우에는 실제 데이터 배열입니다.
STL 의 컨테이너와 유사합니다.
반면 array_view 는 데이터 배열의 일종의 래퍼( wrapper ) 입니다.
그래서 array_view 는 STL의 이터레이터( iterator ) 와 유사한 동작을 합니다.
array_view는 한 번에 여러 데이터의 동시에 접근할 수 있으며,
랜덤 액세스( random-access ) 가 가능합니다.

array 에 의해서 정의되는 배열 데이터는 accelerator 상에 메모리를 가지게 됩니다.
이것은 개발자가 직접 정의해서 할당할 수도 있고,
런타임( runtime ) 에 의해서 자동적으로 생성될 수도 있습니다.
그렇기 때문에 실제 데이터가 생성되어질 때 깊은 복사( deep-copy )를 하게 됩니다.
우리가 일반적으로 오브젝트를 메모리에 생성했을 때와 같다고 생각하시면 됩니다.
array 는 다음과 같이 사용할 수 있습니다.( 샘플은 msdn 에서 가져왔습니다 )

vector<int> data(5);
for (int count = 0; count < 5; count++)
{
    data[count] = count;
}

array<int, 1> a(5, data);

parallel_for_each(
    a.grid,
    [=, &a](index<1> idx) restrict(direct3d)
    {
        a[idx] = a[idx] * 10;
    }
);

data = a;
for (int i = 0; i < 5; i++)
{
    cout << data[i] << "\n";
}



반면에 array_view는 이름에서 유추할 수 있듯이,
실제 데이터들은 다른 accelerator 상에 있고,
이를 연산을 위해서 복사를 하는 개념
입니다.

즉, 커널 함수가 실행될 때, 데이터가 복사됩니다.
( 커널 함수는 AMP 내의 람다 함수 부분을 의미합니다. )

이 array_view 개념은 DirectX11 에서 보셨던 분들은 쉽게 이해할 수 있는 개념입니다.
바로 ComputeShader 를 위해서 데이터들을 연결하는 바로 그 개념이기 때문입니다.
아래의 그림은 ComputeShader 의 동작 방식을 보여주는데,
SRV( shader resource view )와 UAV( unordered access view ) 라는 것이
결국 view 의 역할을 하는 것입니다.




DirectX11 과 연계해서 생각한다면,
array 라는 메모리 배열도 결국 텍스쳐 메모리라는 것을
눈치챌 수 있을 것입니다.
DirectX10 부터 텍스쳐 인터페이스는 꼭 이미지 데이터를 의미하지 않습니다.
대용량의 메모리 블럭의 의미에 더 가깝다는 것을 알아두시기 바랍니다.
텍스쳐의 개념을 사용하기 때문에 동시에 여러 데이터에 접근이 가능하고,
랜덤 액세스도 가능한 것입니다.^^

신고
크리에이티브 커먼즈 라이선스
Creative Commons License

앞서 작성했던 예제 샘플들은 사실 완전한 상태의 코드가 아닙니다. ( 죄송...^^ )
바로 이 디바이스-로스트와 관련한 상황이 있어야, 안정성을 향상시킬 수 있습니다.

디바이스-로스트의 처리는 다음과 같은 절차를 진행해야 합니다.

1. 디바이스-로스트가 발생함을 체크
2. 디바이스 의존적 리소스를 모두 제거
3. 디바이스 의존적 리소스를 재할당


이 세가지를 처리하는 것이 디바이스-로스트 상황에 대처하는 것입니다.
Direct2D 의 리소스와 관련한 내용은
http://vsts2010.net/593 글에서 제가 언급했었습니다.^^

그러면 하나씩 살펴보겠습니다.
우리가 진행했던 일반적인 렌더링 작업은 아래와 같습니다.


여기서 우리의 첫번째 단계를 처리합니다.
바로 hr = ::g_ipRT->EndDraw(); 부분입니다.
EndDraw()는 렌더링 작업의 결과를 리턴합니다.
리턴 값이 D2DERR_RECREATE_TARGET 이면, 바로 디바이스-로스트 상황입니다.
이름에서 유추할 수 있듯이 "에러가 났으니, 다시 생성하라" 입니다.

이번에 제가 사용할 샘플은 바로 이전 시간에 했던 알파이미지를 렌더링하는 샘플입니다.
이 샘플에서 디바이스 의존적인 리소스는 렌더 타겟과 비트맵입니다.
이 샘플에서 디바이스-로스트 상황이 발생한다면,
렌더타겟과 비트맵 리소스는 메모리에서 제거했다가 다시 생성해 주어야 합니다.

그래서 이번 샘플에서는 이들 리소스를 한번에 생성/삭제 하는 함수를 만들었습니다.


그리고 또 하나 고려해야 하는 부분이 있습니다.
바로 윈도우 사이즈의 변경입니다.
이 경우는 디바이스-로스트가 발생하지는 않습니다만,
순간적으로 화면이 깜빡이는 현상을 보이게 됩니다.
이 곳에도 역시 적절한(?) 처리를 해야 합니다.


렌더타겟의 리사이즈 작업이 실패하면,
역시 디바이스 의존적 리소스들을 모두 제거해 버립니다.

이제 WM_PAINT 이벤트를 위와 관련된 작업들과 연계해서 수정해야 합니다.


렌더타겟이 없는 경우는 디바이스-로스트 상황이거나 초기화 상태로 인식하고,
관련된 리소스를 생성합니다.
그리고 렌더타겟의 CheckWindowState()를 통해서 해당 윈도우가 가려져 있는지를 체크하고,
가려져 있지 않다면 렌더링 작업을 수행합니다.

렌더링 작업의 마지막에는 디바이시 로스트 상황을 체크해서
디바이스 의존적 리소스를 제거하고 있습니다. ( 앞서 언급했었죠..^^ )

이제 샘플이 약간은 안정성이 향상되었습니다.^^
이상으로 디바이스-로스트와 관련한 작업을 마치겠습니다.^^


신고
크리에이티브 커먼즈 라이선스
Creative Commons License

GPU를 활용하는 일은 모든 개발자에게 열려있는 길이여야 합니다.
하지만 DirectX를 직접적으로 활용해야만 하는
MS의 GPGPU 플랫폼인 DirectCompute는 그렇지가 않습니다.

그래픽카드라는게 원래 특수한 목적성을 가지고 등장한 장치이기 때문에,
이를 활용하는 사람들 또한 특정 영역에 국한되어 있는게 현실입니다.
'이제부터 GPGPU 를 적극 활용합시다!' 라고 생각을 하더라도, 
실제로 그것을 활용하기 위한 진입 장벽은 굉장히 높을 수 밖에 없습니다.

그러면 어떻게 해야만 이 장벽을 조금이라도 낮출 수 있을까요?
엔비디아의 CUDA 를 보면, 힌트가 있습니다.
하지만 몰라도 상관없습니다.^^
C++ 파일 내에서 컴파일러에 의해서 자동적으로 처리가 될 수 있으면 가장 좋지 않을까요?
순수 C++ 의 기능만 사용해서 컴파일러가 자동적으로 처리해 준다면,
개발자는 DirectX와 ComputeShader 에서 해방될 수 있을 것입니다.
그것이 바로 C++ AMP 가 등장하는 배경
입니다.
C++ AMP는 다음 버전의 VisualStudio 에 탑재 되어져서 등장할 예정이라고 합니다.


어떤 함수가 아래와 같이 있습니다.
void Func( ... )
{
    코드
}

위의 함수는 결국 컴파일러에 의해서 CPU 와 관련한 명령어를 생성하게 됩니다.
이를 AMP 적으로 확장하면 정확히 아래와 같이 구성됩니다.
void Func( ... ) restrict( cpu )
{
   코드
}

restrict 이라는 키워드를 함수에 적용함으로써 간단히 이를 구현합니다.
눈치가 좀 빠르신 분들이라면
'저 cpu를 gpu 로만 변경하면, gpu 로 컴파일 되어지는 것인가?' 라고 생각이 드실 겁니다.
네. 맞습니다.
그것이 바로 C++ AMP 가 DirectCompute 를 구현하는 방법입니다.
정확히는 아래와 같습니다.
void Func( ... ) restrict( direct3d )
{
   코드
}
'direct3d' 가 바로 'gpu' 를 의미합니다.
현재 이 옵션용 예약어는 확정적인 것은 아닙니다.
'direct3d' 가 확정될 수도 있고, 그렇지 않을 수도 있습니다.
아직 C++ AMP가 출시되지 않아서 유동적인 부분이 있습니다.
그 점 주의해서 읽어주시기 바랍니다.^^

다음 버전의 Visual C++ 부터는 
함수마다 저렇게 restrict 한정자에 컴파일 옵션을 지정해주어야 합니다.

물론 지정을 하지 않았을 때는, 디폴트로 restrict( cpu ) 로 자동 처리할 것입니다.

그러면 한 함수 내에서 CPU와 GPU를 활용해야 하는 경우는 어떻게 해야할까요?
void Func( ... ) restrict( direct3d, cpu )
{
   GPU를 사용하는 코드
   CPU를 사용하는 코드
}

위와 같이 혼합해서 사용하는 것도 가능합니다.
또한 오버로드와 관련한 이슈도 문제 없이 처리될 것입니다.
void Func( ... );
void Func( ... ) restrict( direct3d );

간단히 위와 같이 restrict 만으로 GPU를 사용하는 것이 완전히 된다면 얼마나 좋겠습니까만,
restrict( direct3d ) 로 정의되어지는 함수들은 그에 상응하는 규칙으로 코딩 작업을
해야만 합니다.
이것이 사실 그렇게 쉬운 개념만으로 이해할 수 있는 것은 아닙니다.
하지만 DirectCompute를 직접 제어하는 것보다는 쉽습니다.

다음 시간부터 C++ AMP 로 프로그래밍 하는 개념에 대해서 살펴보겠습니다.^^
신고
크리에이티브 커먼즈 라이선스
Creative Commons License

[KGC 2011] 발표 자료

DirectX 11 2011.11.14 08:00 Posted by 조진현


안녕하세요~ 조진현입니다.
얼마 전 대구에서 KGC 2011 행사가 있었습니다.
저는 그 곳에서 DirectX11 과 관련한 발표를 진행하고 왔습니다.
그래서 발표 슬라이드를 공개해 드립니다.

특히나 이번 발표 때, C++ AMP 에 대한 언급이 있었습니다.
AMP는 아래 링크에 흥배님이 자세히 설명해 주셨습니다.
http://vsts2010.net/591

빠른 시간 내에 발표 때 언급했던 C++ AMP 와 관련한 내용을
팀 블로그에 게재하도록 하겠습니다..^^

[조진현] [Kgc2011]direct x11 이야기
View more presentations from 진현 조.
신고
크리에이티브 커먼즈 라이선스
Creative Commons License

디바이스 로스트( Device Lost ) 라는 용어에 대해서 친숙한 분도 계실 것이고,
그렇지 않은 분도 계실 것입니다.
아마 Direct3D 를 다루어 보신 분들은 이 용어에 무척이나 친숙할 것입니다.

디바이스 로스트라는 것은 특정 상황을 얘기하는 것입니다.
이 상황은 지금까지 확보하고 있던 메모리 같은 리소스들이 모두 사라져서,
아무 작업도 할 수 없는 상황을 얘기합니다.
즉, 시스템 리소스가 모두 무효화 되어버린 상황이라 할 수 있습니다.

GDI 를 사용해서 렌더링 한다면, 갑자기 HDC 가 비활성화 되어버린 것이라 생각할 수 있습니다.
게임 개발에서 주로 사용하는 Direct3D를 사용하는 경우에는 Alt + Tab 문제라던지,
실시간으로 화면 사이즈를 조절하게 되었을 때 주로 나타납니다.
Direct3D로 개발하는 경우에는 예전부터 이 문제에 친숙했기 때문에,
이 상황에 대해서 리소스들을 복구하는 코드를 개발자들이 직접 작성해 주었습니다.

Direct2D도 DirectX의 하나이기 때문에 바로 이 디바이스 로스트 상황이 발생을 합니다.
그래서 개발자들이 이 상황에 대해서 직접 적절한 처리를 해주어야 합니다.

그런데 중요한 것은 현재의 디바이스 로스트 상황이 예전과는 다르다는 것입니다.
이 차이를 설명하려면 디스플레이 드라이버 모델까지 언급을 해야 합니다.

Windows XP 시대와 현재의 Windows 7 시대는 디스플레이 드라이버 모델이 다릅니다.
XP 시대는 XPDM 이라는 드라이버 모델을 사용하고 있으며,
윈도우 7 시대에는 WDDM( Windows Display Driver Model ) 라는 모델을 사용하고 있습니다.
( 모니터에 그리기까지의 과정이 다르다고 생각하시면 좋을 것 같습니다.^^ )
이 모델의 차이가 디바이스 로스트에서도 차이를 만들어 냅니다.

XPDM 은 XP때까지 Windows OS가 발전시켜온 드라이버 모델입니다.
즉, 옛 것을 꾸준히 업데이트 한 결과라 할 수 있습니다.
아무래도 너무 오래되다보니 복잡하고 난해한 구조가 되었고,
그 복잡성 때문에 버그들이 드라이버 내부에 있었다고 합니다.

또한 하드웨어의 발전에도 빠르게 대응하는 것에 굉장한 한계가 있었다고 합니다.

그래서 Windows Vista 때 이 문제를 극복하기 위해서 도입된
새로운 디스플레이 드라이버 모델이 바로 WDDM 입니다.
WDDM 은 기존의 복잡성을 최대한 버려서 심플한 구성을 가지도록 설계되었습니다.
기존의 XPDM 이 커널모드에서 대부분의 처리를 수행했던 것을
커널모드와 사용자 모드로 분리를 시켜서 이 디바이스 로스트에 대한 상황에 대한
대응을 용이하게 했습니다.

XPDM의 경우에는 커널에서 문제가 발생하면,
이 때문에 시스템 전체를 다시 시작해야 하는 불상사(?)가 꽤 있었습니다.
MS 통계에 따르면 우리가 흔히 보던 블루스크린 상황의 20% 정도가
바로 이 디스플레이 드라이버가 원인이였다고 합니다.
그렇기 때문에 간략화 된 커널 모드의 드라이버를 제공하고,
계산이 복잡한 대부분의 기능은 사용자 모드 드라이버로 이동시킴으로써
문제를 해당 애플리케이션 하나로 국한 시킬 수 있습니다.
이는 블루스크린 상황을 감소시킬 수 있는 하나의 방법이기도 합니다.

XP OS의 디스플레이 드라이버 모델 교체가 어렵기 때문에
WDDM 은 Vista 이상의 OS에서만 지원합니다.
결국 이는 최신의 DirectX 들이 XP OS 에서 정상적으로 작동하지 못하는
가장 근본적인 이유입니다.

WDDM과 XPDM 의 차이는 바로 GPU 활용에 있습니다.
XPDM 의 경우에는 GPU 관련 처리를 할 수 없었습니다.
왜냐하면 옛것을 꾸준히 계승시켜 발전시킨 모델이였기 때문에,
GPU 처리와 같은 큰 패러다임의 전환은 이 모델 자체를 변경하는 일이기 때문입니다.
현재 Vista 이후의 Windows OS에서는 대부분의 그래픽 작업과 윈도우 관리에
바로 이 GPU가 활용되고 있습니다.

XP 시대에서 DirectX를 사용하는 것은 GPU를 독점적으로 사용하는 작업이였습니다.
만약에 GPU 작업을 실행하는 중에 다른 애플리케이션에서 GPU를 사용하려 한다면,
GPU의 제어 권한을 빼앗기게 됩니다.
XP 시대에서는 GPU를 여러 애플리케이션에서 동시에 공유할 수가 없었습니다.

하지만 현 세대의 Windows OS에서는 GPU 메모리 관리자가 비디오 메모리를 관리하고 있으며,
GPU 스케줄러에 의해서 스케줄을 조정합니다.
우리가 '빠르다' 라고 하는 작업의 뒤에 바로 이런 작업들이 이루어지고 있습니다.

이러한 GPU 활용의 적극적인 도입은 결국 퀄리티의 향상까지 연결됩니다.
XP 시대의 XPDM 의 경우에는 GDI로 렌더링 작업을 할 때,
화면에 직접 그리는 개념으로 작업을 했습니다.
그렇기 때문에 윈도우를 이동하거나 사이즈를 조절하게 되면,
화면이 깜빡이는 것을 확인 할 수 있었습니다.
( 티어링과 같은 현상도 확인할 수 있었습니다.^^ )

또한 이 때 대량의 WM_PAINT 메시지가 발생해서 시스템에 상당한 부하를 주기도 했었습니다.
이는 XPDM 의 경우 모니터 화면에 직접 렌더링 작업을 했었기 때문입니다.

비디오 메모리가 풍족해진 현 세대에서는 오프-스크린 버퍼를 두어서,
화면이 아닌, 다른 버퍼에 렌더링 작업을 수행합니다.
( 흔히들 얘기하시는 더블-버퍼링 기법입니다.^^ )

이러한 것을 담당하는 것이 DWM( Desktop Window Manager ) 입니다.
이는 OS 에서 자동적으로 실행하기 때문에 쉽게 확인할 수 있습니다.


DWM 은 간단히 말해서 DirectX 애플리케이션입니다.
DWM 은 비디오 메모리를 만들어서 각 애플리케이션 화면을 모아서
우리에게 화면을 보여주는 역할을 합니다.
그렇기 때문에 DWM 은 오프-스크린 버퍼를 관리하는 기능을 가지고 있습니다.

WDDM 에 대해서 논할 내용도 상당히 많이 있습니다.
Vista 시절에는 WDDM 1.0 이였고,
Windows 7 에서는 WDDM 1.1 이 사용되고 있습니다.
역시 버전이 업데이트 되면서, 더 좋아진 부분이 있습니다.
1.1 에 관한 개선 사항은 아래의 링크로 대신합니다.^^
http://jacking.tistory.com/442


GPU를 활용한 많은 기능들이 현세대의 Windows OS에 기본적으로 탑재가 되어 있습니다.
그렇기 때문에 디바이스-로스트 같은 GPU 관련한 예외 상황들에 대한 처리를
개발자들이 해주어야 합니다.
XPDM의 경우와 개념도 다르고, 발생되는 상황도 다릅니다.
Direct2D의 경우 디바이스-로스트란,

그래픽카드가 일정시간 동안 응답을 하지 않을 때를 의미합니다
.
여기서 일정 시간이란 기본적으로 2초로 설정되어 있다고 합니다.
Direct2D 에서 디바이스-로스트는 잘 발생되지는 않습니다.^^

이와 관련한 실제 처리는 다음 시간에 계속하겠습니다.^^

신고
크리에이티브 커먼즈 라이선스
Creative Commons License


< 변환의 중심 점은 어디인가? >

Direct2D의 변환과 관련된 API는 모두 변환을 수행할 중심점을 함수 인자로 요구를 합니다.
그렇다면, 이 중심점은 스크린 기준에서 정의되어지는 것일까요?

다음과 같은 다람쥐 그림이 있다고 가정해 보겠습니다.


이 그림은 ( 100, 100 ) 의 크기로 그려지기를 원한다고 가정해 보겠습니다.

 


그리고 우리의 모니터에 ( 300, 200 ) 위치에 그려지기 원하도록 설정하겠습니다.
그러면, 우리에게 변환을 위한 중점을 설정하기 위한 기준 좌표는 어떻게 설정되어야 할까요?
그림의 좌측 상단을 변환의 중점으로 원한다면 ( 300, 200 )으로 설정하면 될까요?
그림의 좌측 상단이 변환의 중점이 된다는 말의 의미를 잘 되새겨 보시기 바랍니다.

그림의 중심을 기준으로 중점을 설정해 두는 것이 훨씬 쉬운 개념으로 변환할 수 있습니다.
즉, 위의 그림에서 변환의 중심이 되는 좌측 상단은 좌표는 ( 0, 0 ) 이 되는 것입니다.
만약, 우측 하단이 변환이 중심이 되길 원한다면 ( 100, 100 )을 설정하면 됩니다.

앞서 살펴 보았던 샘플에는 이 의미를 그냥 넘겼지만
이번에 제공되는 샘플에서는 이를 고려해서 모두 작성했으니,
유심히 살펴보시기 바랍니다.^^

우리는 이 개념을 다음과 같이 지금까지 사용하고 있었습니다.


D2D1_RECT_F 정의의 변수가 보이시나요?
바로 이 dxArea 를 기준으로 변환이 되는 중점을 설정하는 것이 이해하기가 편리합니다.^^

이번 샘플은 지난 번 샘플의 연장에 있습니다.
지난 번 샘플에서 이번 내용과 관련된 부분을 추가 했습니다.

 




< Scale( 확대/축소 ) >

확대/축소 작업은 1.0을 기준으로 이루어집니다.
1.0 보다 작으면 축소가, 1.0보다 크다면 확대가 이루어 집니다.
이 작업은 D2D1::Matrix3x2F::Scale() API를 통해서 이루어 지는데,
역시나 변환의 중점을 요구합니다.^^



만약 좌측 상단을 기준으로 확대를 하면 다음과 같은 개념입니다.






< Skew( 찌그러뜨리기 ) >

마지막 변환은 찌그러뜨리기 작업입니다.
이는 Matrix3x2F::Skew() API를 통해서 설정할 수 있습니다.
함수 인자로 받는 것은 X축과 Y축의 찌그러질 각도와 역시나 기준이 되는 중점입니다.



역시나 변환이 되는 중점에 따라 결과가 변합니다.
아래의 그림은 30도씩 찌끄러뜨린 결과입니다.
첫번째는 X축만 적용한 것이고, 두번째는 Y축을,
세번째는 X와 Y축 모두를 30도씩 찌그러뜨린 결과입니다.



부족하지만, 제가 작성한 샘플을 첨부해 드립니다.^^


 

신고
크리에이티브 커먼즈 라이선스
Creative Commons License


안녕하세요..^^

잠잠하던 DirectX 게시판에 이번 달에 작은 소식이 몇 가지 발표되었습니다.
먼저 Direct3D 11.1 이 릴리즈가 되었습니다.
아래 링크를 보시면 특징들을 확인하실 수 있습니다.
큰 기능의 추가는 다행이(?) 없으니 안심하시기 바랍니다.^^

http://msdn.microsoft.com/en-us/library/hh404562%28v=VS.85%29.aspx


다른 소식은 Visual Studio 11 소식입니다.
이번 Visual Studio 11 에서는 게임 개발 관련한 편의를 위해서
몇 가지 비쥬얼 적인 에디팅을 지원하는 듯 합니다.

아래는 관련 링크입니다.
http://blogs.msdn.com/b/jasonz/archive/2011/09/14/announcing-visual-studio-11-developer-preview.aspx


특히나 이번에 주목한 부분은 쉐이더 에디터가 Visual Studio 11로 통합되어진 것입니다.
( 설마 나중에 빠지는 것은 아니겠지요? ^^ )
앞으로 더 정보를 지켜봐야 하겠지만, 상당히 기대되는 부분인 것은 분명한 것 같습니다.^^





신고
크리에이티브 커먼즈 라이선스
Creative Commons License

 

  

이번 시간에는 직접 이미지를 화면에 표현하는 방법에 대해서 언급합니다.

Win32 API를 이용할 때, 우리는 '비트맵(Bitmap)' 이라는

그래픽 데이터 포맷을 읽어서 화면에 그려주었습니다.

사람마다 차이는 있겠지만, 일반적으로 다음과 같은 순서를 따라서 구성했을 것입니다.

 

  1. 비트맵을 읽기 위해서 파일을 오픈한다.

  2. 파일에서 헤더를 읽어 들인다.


  3. 비트맵 헤더의 정보를 통해서 관련 메모리를 생성하고,

    파일에서 색상 데이터에 대한 정보를 읽는다.


  4. DIBSection 을 생성하고, 실제 데이터를 읽는다.
  5. 그리고 마무리 한다.

 

이 순서는 수 많은 방법 중에 하나일 뿐이지만,

기본적으로 파일을 열어서 헤더를 먼저 읽고, 관련 메모리를 생성하고,

이후에 실제 데이터를 채우게 되는 순서는 공통된 작업입니다.

Direct2D 에서도 이와 같이 작업을 해도 되지만,

이미 편의를 위해 만들어진 라이브러리를 사용해서 조금 더 확장성 있는 작업을 할 필요가 있습니다.

지금부터는 WIC를 이용한 간단한 이미지 뷰어 작업을 해보겠습니다.

  

WIC( Windows Imaging Component )

 

DirectX 가 윈도우 운영체제 전반으로 광범위하게 활용되면서,

이들과 관련한 내용들을 분리할 필요가 있었습니다.

과거까지는 DirectX 는 게임 개발자들의 전유물에 가까웠기 때문에,

다른 개발자들도 손쉬운 개념으로 접근할 수 있는 그런 분류가 필요했습니다.


결과적으로 아래와 같이 분류가 되었습니다.  

 

WIC는 모든 이미지를 쉽게 처리할 수 있도록 만들어낸 COM 기반의 프레임워크입니다.

그림에서 보듯이 WIC도 하나의 큰 영역으로서 자리 잡고 있습니다.

( 참고로 DXVA는 영상 처리를 위한 프레임워크입니다. )

 

WIC를 이용한 이미지 처리는 앞서 GDI 기반에서 작성했던 것과는 완전히 다릅니다.

WIC는 PNG, JPG, GIF 등과 같은 거의 모든 주요한 이미지 형식을 포함하고,

기본 코덱들을 지원하고 있습니다.

 

말이 참 어렵죠?

쉽게 말해서, Direct2D 기반에서 이미지 처리를 하려면 WIC를 사용하면 쉽게 할 수 있다는 것입니다.

우리는 이것을 사용하는 순서와 방법에 대해서 배우기 위해서,

윈도우 화면에 이미지를 그려주는 간단한 애플리케이션을 만들어 볼 것입니다.

 

 

기본 WIC 프로그래밍

 

애플리케이션 마법사로 새로운 프로젝트를 만들고, stdax.h 에 다음을 추가를 합니다.

 

'WindowsCodecs.lib'와 'wincodec.h' 가 바로 WIC를 사용하기 위해 추가시킨 것입니다.

눈치 빠른 분들이라면, 이름에서 약간 앞으로의 작업 방향을 예측할 수 있을 것입니다.

 

이번 프로젝트에서 사용할 전역 변수들은 아래와 같습니다.

 

 

익숙한 개념이 눈에 보이지 않으십니까?

바로 IWICImagingFactory 입니다. 네 그렇습니다~

WIC 도 바로 팩토리 형태로 생성이 됩니다.

 

추가된 변수들은 아래와 같은 절차에 의해 값이 채워집니다.

즉, 아래는 WIC의 처리 과정입니다.

 

  1. WIC 팩토리를 만든다.

  2. 파일 경로를 기반으로 해서 디코더를 만든다.

  3. 디코딩된 프레임을 가져온다.

  4. 변환기에 넣어서 Direct2D 형식으로 변환한다.

  5. Direct2D 비트맵을 생성하고, 이를 렌더링한다.

 

이제 위의 절차를 실제로 어떻게 처리하는지를 차근차근 살펴보겠습니다.

 

가장 먼저하는 초기화 작업입니다.

앞서 언급했듯이, WIC 도 팩토리 개념으로 생성됩니다.

COM 기반이기 때문에 API 인자들이 굉장히 어려워 보일 수도 있지만, 관심을 둘 부분은 아닙니다.

위와 같은 방법으로만 하면, WIC가 생성 되어진다는 개념으로만 인식하고 다음 단계로 넘어갑니다.

 

 

 

다음 단계는 디코더를 만들고, 이를 기반으로 해서 Direct2D 형식으로 데이터를 변환하는 것입니다.

이를 위해서 가장 먼저 해야 하는 일은

이미지를 읽어들이기 위한 디코더( Decoder )를 만드는 일입니다.


갑자기 등장한 생소한 용어에 조금 혼란스러울 것 같습니다.

우리가 사용하는 모든 멀티미디어 파일( 이미지, 영상, 사운드 등 )들은

굉장히 어려운 방법으로 압축이 되어있습니다.


이들에 대한 원리나 형식을 이해하는 것도 중요한 일일 수도 있지만,

이는 간단하게 본 페이지에서 설명할 수 있는 내용이 아닙니다.

물론 저도 이와 관련한 전문가는 더더욱 아닙니다.

우리는 단지 API만으로 이들에 대한 고민을 해결할 수 있습니다.

바로 그것이 WIC의 존재 이유 중 하나 일 것입니다.^^

 

즉, 우리는 이미 만들어진 API를 이용해서 손쉽게 이미지 파일을 읽어올 수 있습니다.

그런 역할을 하는 것이 바로 디코더입니다.

아래는 디코더를 가지고 실제 작업을 하는 부분입니다.

 

 

디코더는 WIC 팩토리 멤버함수로써 생성이 되어집니다.

우리가 사용했던 이 API의 원형은 다음과 같습니다.

<코드>

HRESULT CreateDecoderFromFilename(

[in] LPCWSTR wzFilename,

[in] const GUID *pguidVendor,

[in] DWORD dwDesiredAccess,

[in] WICDecodeOptions metadataOptions,

[out, retval] IWICBitmapDecoder **ppIDecoder

);

</코드>

 

이 API는 주어진 이미지 파일을 기반으로 해서 디코더를 생성해 줍니다.

첫 번째 인자로 파일명이 들어갑니다.

이 파일을 기반으로 해서 적합한 디코더를 생성해 주게 되는 것입니다.

예를 들어, PNG 파일이면 PNG에 대한 디코더가 필요하다고 인식하고,

그에 맞는 디코더를 자동적으로 생성해 주는 것입니다.

 

두 번째 인자는 선호하는 디코더 벤더(vendor)의 GUID를 입력해야 하는데, 지금은 NULL을 사용합니다.

 

세 번째 인자로는 디코더에 대한 접근 방법을 명시합니다.

읽기(read), 쓰기(write), 혹은 둘 다 가능한지를 넣어주면,

가장 최적화된 방법과 메모리 위치를 가지는 디코드를 생성해 줍니다.

위의 예제에서는 읽기용으로만 디코더를 만들었습니다.

 

네 번째 인자는 디코더의 캐시 관련 옵션입니다.

우리가 인자로 넘긴 WICDecodeMetadataCacheOnDemand는

필요한 이미지 정보만 캐시 하도록 옵션을 준 것입니다.

다음 번에 언급할 지도 모르지만, 하나의 이미지 파일에는 여러 이미지들을 포함하고 있을 수 있습니다.

예를 들면 GIF 애니메이션 이미지 같은 것들이다.

이런 경우에 유용하게 캐시하려면, 다른 옵션을 주어야 할 것입니다.

 

마지막 인자는 생성된 디코더를 저장할 디코더의 포인터입니다.

여기까지 작업하면, 우린 이제 파일을 읽은 디코더를 소유하게 되는 것입니다.

뭔가 절차 상으로 굉장히 복잡한 것처럼 느껴지죠?

 

 

다음으로 할 작업은 프레임(frame) 작업입니다.

프레임이라는 것은 실제 픽셀 데이터를 가지고 있는 비트맵입니다.

앞서 잠깐 언급했듯이, 하나의 이미지 파일은 여러 장의 이미지가 존재할 수 있습니다.

그런 경우를 대비해서 체크를 해야겠지만,

우린 여기서 단 하나의 프레임만이 존재한다고 가정할 것입니다.

디코더의 멤버 함수인 GetFrame()를 통해서 우린 가장 첫 번째 프레임을 얻을 수 있습니다.

이 프레임을 얻는다는 것은 우리가 화면에 표현할 수 있는 이미지를 얻었다는 것입니다.

 

이제 우리는 디코더를 통해서 이미지를 Direct2D에서 표현할 수 있도록

적절하게 변환을 해주어야
합니다.

CreateFormatConverter() API는 이를 위해서 컨버터를 만들어줍니다.

그리고 이 컨버터를 우리가 원하는 형태로 초기화를 시켜 줍니다.

컨버터의 멤버함수 Initialize() 는 이미지를 컨버팅 하면서

픽셀 정보를 보정해 줄 수 있는 많은 옵션을 가지고 있습니다.

이들 옵션에 대한 세부 설정을 하지 않았습니다.

그래서 위에 인자들 형태로 주면, 별다른 이미지의 수정 없이 32비트 포맷으로 남게 됩니다.

 

이제 마지막으로 실제 렌더링 가능한 형태의 메모리를 생성해야 합니다.

렌더타겟의 멤버함수인 CreateBitmapFromWicBitmap() API를 통해서 이 작업을 하게 됩니다.

여기까지 하면, 이미지를 렌더링 하기 위한 준비작업이 모두 끝난 것입니다.

 

저는 여기에 모든 옵션들을 나열하지 않습니다.

( 기본 목적인 이미지를 띄우는데 충실하고자 합니다.^^ )

 

 

생소한 API들이 눈에 많이 띄지만, 이들은 일련의 절차에 지나지 않습니다.

중요한 개념은 이미지를 읽어 들일 디코더를 만들고,

이 이미지 데이터를 Direct2D가 표현할 수 있는 픽셀 데이터로 변환하는 것입니다.

그리고 이 데이터를 렌더타겟에서 표현할 수 있는 비트맵으로 만들어서

렌더링 가능한 상태로 만듭니다.

위의 코드는 바로 이 개념들을 표현하고 있는 것입니다.

 

그러면 실제 WM_PAINT 메시지를 통해서 이들이 어떻게 화면에 그려야 하는지 살펴보겠습니다.

 

WM_PAINT 메시지에서는 렌더타겟이 존재하지 않는 경우, 렌더타겟을 생성합니다.

렌더 타겟이 존재한다면, 비트맵을 그리고 있습니다.

렌더타겟의 렌더링 작업도 BeginDraw() / EndDraw() 의 매커니즘 내부에서

특정 상태를 기반으로 작업을 수행하게 됩니다.

우리는 Clear() 라는 API를 통해서 렌더타겟의 메모리를 흰색으로 채우고 있습니다.

그리고 현재 우리가 이미지를 (0,0) 위치에 (300,300) 크기로 렌더링 합니다.

 

마법의 함수 DrawBitmap()

 

앞선 작업을 통해서 우린 Direct2D를 이용해서 이미지를 화면에 그릴 수 있었습니다.

만약 우리가 읽어 들인 이미지의 일부분만을 화면에 그리고 싶다면 어떻게 해야 할까요?

혹은 흐릿한 효과를 주고 싶다면 어떻게 해야 할까요?

굉장히 어려운 일들 같지만, 이들 기능은 DrawBitmap() 에 모두 옵션 인자로서 존재하고 있습니다.

( 무척 고마운 일이지요..^^ )

그렇기 때문에, 우리는 이 함수를 잘 사용할 수 있어야 합니다.

API의 원형은 다음과 같습니다.

 

<코드>

virtual void DrawBitmap(

[in] ID2D1Bitmap *bitmap,

[in, optional] const D2D1_RECT_F *destinationRectangle = NULL,

         FLOAT opacity = 1.0f,

D2D1_BITMAP_INTERPOLATION_MODE interpolationMode =

D2D1_BITMAP_INTERPOLATION_MODE_LINEAR

,

[in, optional] const D2D1_RECT_F *sourceRectangle = NULL

) = 0;

</코드>

 

첫 번째 인자는 우리가 렌더링 작업을 수행할 이미지입니다.

 

두 번째 인자부터는 옵션적으로 설정할 수 있다.

두 번째 인자는 렌더링 작업을 수행할 화면의 영역을 설정합니다.

NULL 로 설정한다면, 렌더타겟의 원점에 그리게 됩니다.

만약 이미지 크기보다 크게 설정된다면, 자동적으로 이미지를 확대해서 보여주게 됩니다.

 

세 번째 인자는 투명도를 설정합니다.

범위는 0.0~1.0 사이의 값으로 0.0은 투명한 상태를 나타내고 1.0은 불투명한 상태를 나타냅니다.

 

네 번째 인자는 우리가 렌더링하는 이미지가 회전을 하거나 크기가 조정되었을 때,

어떻게 부드럽게 보일 것인가에 대한 옵션을 설정하는 부분입니다.

즉, 보간( interpolation ) 옵션입니다.

 

마지막 인자는 원본 이미지에서 일정 영역을 보여주고 싶을 때 영역을 입력하는 옵션입니다.

이 때 단위는 해당 이미지 파일의 사이즈를 기준으로 영역을 설정해 주면 됩니다.

 

그러면, 간단하게 실제로 이미지의 일부 영역을 약간 투명하게 보여지는 것을 프로그램으로 구현해자면,

앞서 작성했던, 이미지 뷰어의 기능에서 DrawBitmap()만 변경해주면 됩니다.

 

<코드>

HRESULT hr = E_FAIL;

::g_ipRT->BeginDraw();

::g_ipRT->SetTransform( ::D2D1::Matrix3x2F::Identity() );

::g_ipRT->Clear( ::D2D1::ColorF( ::D2D1::ColorF::White ) );

                            

if( ::g_ipD2DBitmap != nullptr )

{

    ::D2D1_RECT_F dxArea = ::D2D1::RectF( 0.0f, 0.0f, 500.0f, 500.0f );

    ::D2D1_RECT_F dxSrc = D2D1::RectF( 0.0f, 0.0f, 250.0f, 250.0f );

    ::g_ipRT->DrawBitmap( ::g_ipD2DBitmap, dxArea, 0.3f,

D2D1_BITMAP_INTERPOLATION_MODE_LINEAR, &dxSrc );

                

}

hr = ::g_ipRT->EndDraw();                

</코드>

 

우리는 간단하게 DrawBitmap() 의 인자들만 변경해주는 것만으로 이미지의 일부 영역만을 보여주고,

투명도를 조절할 수 있음을 확인해 보았습니다.

각각의 값을 변경시키면서, 여러가지 아이디어를 구상해 보기 바랍니다. ^^


아래 소스코드를 첨부합니다..


신고
크리에이티브 커먼즈 라이선스
Creative Commons License

- Previously on TFS Thriller Ssoul.

쏘울의 컬렉션을 연결하는 데 성공한 보람과 세식. 덤앤 더머, 반짝반짝 모래알 같은 그들도 뭉치니 미션을 해결할만한 지혜는 나오는 듯 하다. 초보자를 고려한듯 친절하게 아주 천천히 조금씩 교모해지는 쏘울의 미션. 그들은 집에 갈 수 있을까?


- 신의 한 수.

자 이제 쏘울이 또 뭐라고 하는지 들어볼까?”

왠지 집에 가라고 할 거 같아서 두근두근 거리는데?”

퍽이나

fu**?”

? mot*** fu**?”

(저자 주 : 발음을 잘 해보시면, 이해 가능한 하급 말장난 패턴입니다. 방송심의위원회의 규정을 준수하기 때문에 별표처리한 것 양해바랍니다.)

폭풍 같은 개드립이 휩쓸고 지나간 자리는 적막만이 감돌았다.

우리 쏘울 이야기나 들어볼까.”

그러지

2번 동영상 파일을 재생했다. 이제 조금씩 익숙해지는 쏘울의 오토튠 목소리.

그래그래그래. 여기까지 잘 온 거 같군. 자네들도 바보는 아닌 거 같애. 자 그럼 이번에는 재밌는 퍼즐을 하나 풀어볼까? 나름 쏘우 패러디인데 퍼즐 하나 없으면 섭하겠지? 자 지금부터 퍼즐을 설명할 테니 잘 들으라고. 과제물 폴더의 Lab2폴더를 열어봐. 그러면 List라는 텍스트파일이 있어 그걸 열어봐.”


어때? 익숙한 C#코드가 보이지? 자 이제, 비주얼 스튜디오로 가서 아까 다운로드한 프로젝트를 열어봐.”


"
그래 내 애정이 담긴 환영 메시지가 보이겠지. 감동의 눈물은 잠시 접어두라고. 자 그럼 저 환영 메시지를 지우고, 아까 내가 준비한 텍스트파일 안의 내용을 거기다 붙여."


좋아 다 되었겠지. 이제 퍼즐을 설명해주지. 두 목록을 하나의 목록으로 합쳐야 하네. 아이디 목록의 첫 번째 항목과 이름 목록의 첫 번째 항목, 그리고 다음은 두 번째 항목끼리 그렇게 말이지. 그리고 합친 목록의 항목을 한 줄에 하나씩 출력하는 거야. 각 항목의 속성들은 쉼표로 구분해서 말이지. , 해서는 안 되는 게 몇 가지 있어. 작성한 코드를 절대로 지워서는 안되네. 한 번의 체크인과 두 번의 변경 내용 취소로 코드 작성을 완료해야 하네. , 백 스페이스 키나 delete키를 눌러서는 안 된다는 거지. 그리고 배열의 인덱스를 사용해도 안 돼. 그리고 마지막으로 Main메서드 안의 코드는 10줄 이내여야 하네. 이 제약사항만 지키면 되는 거지. 그럼 행운을 빌어.”

난생 처음 받아보는 황당한 요구사항에 둘은 잠시 생각에 잠겨 허우적 대고 있었다. 그리고 세식이 먼저 말문을 열었다.

그러니까 집에 안 보내 준다는 거네?”

너 예전에 천국이 너네 집이라고 했었지? 지금 당장 입주할래?”

왜 그러냐능!! 가정이 있는 몸 이해 못하냐능!!”

그래~ 빨리 집에 가야 되니까 이거부터 해결하자고~”

그래! 집에 가야지! 합시다!”

잠깐 생각하는 시간을 갖고, 이번에도 세식이 먼저 입을 열었다.

그럼 일단 지금 상태로 체크인 해두자. 코드 지우지 말라고 했으니까 초기 상태는 저장해놔야 다시 시작을 하지.”

, 좋은 생각이군.”


오케이 체크인은 됐고. 이제 둘을 합쳐야 되는데.”


무의식 중에 for문을 작성하던 보람은 소스라치게 놀랐다.

“꺄악!!! 어머나!!!!!!!!!”

야 인덱스 쓰지 말라 그랬자나~ 왜 그러셩~”

일단 다시 원상 복구 시키자고…”


오케이 다시 돌아왔다.”

이제 변경 취소 한번 남았군.”

두 목록을 합쳐야 하는데 인덱스를 사용하면 안된다라... 그럼 LINQ를 사용하라는 말인 거 같은데.”

“LINQ는 니가 더 잘알잖아. 뭐 좋은 거 없어?”

이게 두 목록을 하나로그리고 각 목록의 항목을 하나로 합치면서뭔가 기억날 듯 말 듯 하네.”

봐바. 여기 각 목록이 나란히 일렬로 있다고 했을 때, 순차적으로 각 목록의 항목이 하나씩 만나서 새로운 항목을 만든다는 거잖아? 이런 거 처리하는 거 없어?”

오호설명을 들으니 Zip이 생각나는데?”

그렇게 코드를 조심조심 심사숙고하며 작성해 나가기 시작했다.

우선 결과로 나오는 타입이 기존에 존재하지 않는 타입이니까 var타입으로 받고, 아이디 목록을 기준으로 이름 항목을 가지고 하나씩 붙이게…”


항목을 합치는 람다식이 첫 번째 목록의 항목과 두 번째 목록의 항목을 받아서 TResult라는 타입의 새 항목을 만드는 거니까…”


이렇게 작성하고, foreach로 출력하면 되겠군.”


~ 작성 끝? 확실한 겨?”

아마그렇지 않을까?”

떨리는 손으로 ctrl+F5를 누르자.


오케이. 떴다!”

…..”

근데 있잖아이거 샘플 데이터를 유심히 보니, 쏘울의 취향을 알 것도 같은데…?”

뭔데?”

일단, 콩과 2는 홍진호를 가리키는 거고, 한승연은 스타 팬들의 1대 아이콘, 아이유는 2대 아이콘 이었단 말이지. , 쏘울은 스타에 매우 열광하는 사람이 아닐까 하는 거고…”

원빈이랑 김두현은?”

글세 원빈은 잘 모르겠고김두현은 수원 블루윙즈 선수이고, 등번호가 4번이었단 말이지. 그러니까 수원 서포터가 아닐까 하는 생각이 드는 군.”

그래서 누군지 알겠엉?”

전혀.”

왜 아는 거 처럼 말해?”

어쩌라고.”

쏘울의 미션을 또 하나 해결한 보람과 세식. 과연 집에는 언제쯤

저작자 표시 비영리 변경 금지
신고
크리에이티브 커먼즈 라이선스
Creative Commons License
- Previously on TFS Thriller Ssoul.

덤 앤 더머 같이 심각한 상황에서도 농담따먹기 하면 클릭클릭으로 난관을 해쳐가던 보람과 세식. 그들 앞에 쏘울의 두번째 미션이 떨어졌다. 미리 만들어서 분리해놓은 팀 프로젝트 컬렉션이 있으니 그걸 연결하고, 그 안에 저장된 소스를 다운로드 하라는 것.


- 쏘울 컬렉션.


근데 팀 프로젝트 컬렉션이 뭐냐?”

글세 컬렉션이니까, 뭐 이렇게 모아놓은 걸 컬렉션이라고 하잖아? 그러니까 팀 프로젝트를 모아 놓은 걸 말하는 거 아닐까?”

오오미 세식이가 이렇게 똑똑할 줄이야.”

여러분 이게 바로 박세식입니다.”

적절한 패러디로군.”

내가 좀 적절하지. 야 그건 이제 됐고, 빨리 컬렉션 파일이나 찾아보자. 나 빨리 집에 가야 돼.”

보람은 세식의 귀소본능을 뒤로하고 C드라이브의 과제물과 Lab1폴더를 열었다.


"음
. 일단 데이터베이스 파일이네. 일단은 SQL서버에 연결해봐야 겠다."

여기 친절하게 매니지먼트 스튜디오 설치 파일도 같이 넣어놨고만. 웃어야 할지 모르겠다.”

보람과 세식은 우선 매니지먼트 스튜디오를 설치하고, Tfs_ssoul 파일을 데이터베이스에 연결하기 시작했다.


오케이 연결은 끝났고 이제 TFS에서 연결해야 되는데, 어떻게 해야 되는 걸까

여기 TFS 관리 콘솔 인가 하는 거 있네, 관리 작업이니까 여기서 하는 거 아닐까?”


오오미 좋은 생각이군. , 여기 팀 프로젝트 컬렉션 항목이 있네.”

여기 보니까 컬렉션 연결이 있는데? 이거 아닌가?”

눌러 보지 뭐


오 데이터베이스 자동으로 찾아주는데? 역시 로컬에 같이 깔아놓으면 편하구만


준비 검사 통과 좋고

오케이, 연결 완료. 안에 팀 프로젝트가 뭐가 있나 볼까.”


"이름 짓는 거 참 특이하고만
. ssoulTeam이라…"

일단 이거 비주얼 스튜디오에서 여는 게 급선무로군.”

“아! 나 예전에 누가 이거 쓰는 거 봤는데 팀 탐색기 안에서 연결하고 하는 거 같더라.”


"오호 좋은 정보 감사
. 팀 탐색기에.. 팀 프로젝트에 연결이라는 버튼이 있군."


오케이 이렇게 팀 프로젝트 컬렉션 선택하고 연결


와 연결 됐다. 이제 소스 코드만 다운 받으면 되지?”

. 소스 코드는 아마도 소스 제어쪽에서 가능할 거 같은데더블 클릭하면 열리겠지?”


오 열렸다.”

로컬 경로라는 부분이 눈에 확! 들어오는 데.”

나도 확! 들어온다. 눌러봐


오케이 맞는 거 같네. 로컬 경로 어디에 다운하라고는 안 했으니까 그냥 C ssoulTeam 폴더로.”

좋아 좋아. 끝이다.”

. 고생했네 친구.”

뭐 고생은 같이 했지.”

그럼 우리 이제 집에 갈 수 있는 거야?”

그러니까 그건 쏘울 마음이라고.”

과연 두 친구는 이 방을 나가서 집으로 갈 수 있을까.(BGM - 인생 극장 엔딩)

저작자 표시 비영리 변경 금지
신고
크리에이티브 커먼즈 라이선스
Creative Commons License

이번 시간에는 지난 시간들까지 언급한 내용을 기반으로 해서,
간단한 테셀레이션 작업을 구현해 보려 합니다.

당연한 얘기이겠지만,
하드웨어 기반의 테셀레이션은 하드웨어의 지원이 없으면 매우 느립니다.
즉 DirectX11 이상을 지원하는 그래픽 카드가 아니면,
효과를 눈으로 확인하는 것조차 무척 고통스럽습니다.

그래서 이번 시간에 만들 테셀레이션은 간단히 삼각형 하나를 이용합니다.
우리는 이 삼각형 하나를 가지고 테셀레이션 작업을 수행할 것이며,
DirectX11 을 지원하지 않는 그래픽카드라면
강제적으로 REF 모드로 테셀레이션 작업을 수행하도록 합니다.

먼저 결과 샘플을 보면 아래와 같습니다.



이제 우리가 만들려는 그림이 그려졌으니, 직접 코딩 작업을 시작하겠습니다.
이 글에서는 DirectX11 의 기본 셋팅과 관련한 사항은 생략합니다..^^
자세한 API 적인 설명은 생략을 하니 DirectX 2010 6월 버전의 SDK 의 튜토리얼을 참고하시거나,
'알코코더의 DirectX11' 을 참고하시기 바랍니다.^^

우리가 이번 샘플에서 사용할 버텍스 데이터의 형식은 위치 정보만 있으면 됩니다.
이번 샘플에서는 최대한 간단하게 작성하는 것을 목적으로 했기 때문에,
많은 정보를 필요로 하지는 않습니다..^^
그래서 아래와 같이 간단한 버텍스 형식을 정의했습니다..^^



생소한 데이터 타입이 보입니다. 바로 XMFLOAT3 입니다.
DirectX11 부터는 D3DX 계열의 수학 데이터 타입들은 더 이상 업데이트 되지 않습니다.
지금부터는 XNA Math 라는 수학 라이브러리를 사용합니다.
그렇다고 더 이상 D3DX 계열의 수학 데이터 타입들을 사용할 수 없는 것은 아니니, 안심하시기 바랍니다.
이들에 대해서는 향후 언급할 기회가 있으니,
지금은 D3DX 계열의 수학 클래스 대신에 XNA Math 라는
새로운 수학 클래스를 사용한다는 정도로만 인식하고 넘어가겠습니다.^^


아래는 우리가 애플리케이션 전역으로 사용할 변수들의 선언입니다.



그 동안의 DirectX11을 언급하면서 꾸준히 언급되던 내용이기에 자세한 설명은 생략하겠습니다.

특이할 만한 것이라면, 래스터라이져 스테이트 오브젝트를 2개 만드는 것입니다.
이는 우리의 샘플이 솔리드( Solid ) 한 렌더링과 와이어프레임( Wire-Frame ) 기반의 렌더링으로
전환이 가능하기 때문입니다.

다음은 상수버퍼( ConstantBuffer ) 에 관한 전역 선언들 입니다.



우리는 월드 좌표계의 정점을 버퍼에 입력할 것입니다.
그래서 View-Projection 행렬만 변환을 위해서 필요합니다.
그리고 얼마나 테셀레이션 작업을 세밀하게 할지를 결정하는 상수를 하나 추가합니다.



쉐이더를 컴파일 해주는 보조 함수를 다음과 같이 하나 만듭니다.


이제 본격적으로 시작을 합니다.
InitD3D() 에 각종 초기화 작업을 수행합니다.
앞서 잠깐 언급드렸듯이,
DirectX11을 지원하는 하드웨어가 아니면, 강제로 REF 모드로 동작하도록 합니다.
또한 이 함수에서는 각 쉐이더 스테이지에 대응되는 HLSL 코드를 컴파일 해줍니다.
그리고 이들에 대한 각 오브젝트를 만듭니다.
초기화 작업은 주로 반복적인 작업이 많기 때문에, 설명은 생략합니다.

InitD3D() 에 버텍스버퍼의 데이터를 설정해 줘야 합니다.
이번 샘플에서는 월드 좌표로 정의된 삼각형을 사용할 것입니다.
또한 카메라 공간에 대한 설정도 같이 해 줍니다.
이들에 대한 코드는 아래와 같습니다.


이 정도로 초기화와 관련된 작업을 마무리 합니다.
이제는 프레임 관련한 처리를 작성합니다.( Render() )

이 Render() 부분에서는 상수버퍼에 설정할 데이터들을 다음과 같이 업데이트 합니다.

 


우리는 와이어프레임 모드와 솔리드 모드의 렌더링 방식 둘 다를 표현할 것이기에,
이들에 대한 설정도 아래와 같이 고려해 주어야 합니다.



그리고 마지막으로 입력되는 버텍스 형식을 알려주고 버텍스 버퍼를 연결한 후에,
그리기 작업을 수행합니다.^^



이제 키보드 이벤트에 따라 약간의 변화를 주는 작업을 합니다.
현재는 'w' 키로 렌더링 모드를 Wire 와 Solid 간의 토글이 되도록 설정합니다.
그리고 위/아래 방향키로 테셀레이션의 분할 정도를 증감합니다.

이번 작업은 여기까지 입니다.
지금까지 DX11을 살펴보면서, 언급된 내용들이 대부분이라 전체적으로 설명드리지는 않습니다.
( HLSL 코드도 최대한 간결하게 작성했습니다..^^ )
샘플을 같이 첨부드리니, 직접 작성하시면서 익혀보시기 바랍니다.^^

신고
크리에이티브 커먼즈 라이선스
Creative Commons License

 

첫 번째 Direct2D 프로그래밍~ 
 

지난 시간을 통해서 Direct2D의 필요성에 대해서, 제가 열심히(?) 언급해 드렸습니다.^^

이번 시간에는 Direct2D 프로그래밍의 세계에 대해서 들어가기 전에,

간단하게 프로그램을 작성해 볼 것입니다.

부끄럽지만, 저는 박식한 이론 내용 없이도 많은 프로그래밍 작업을 했었습니다.^^

그 만큼, 직접 프로그램을 작성하면 쉽게 이해할 수 있는 부분이 많이 있을 것입니다.

기본적으로 Direct2D는 2차원 그래픽을 만들기 위한 API입니다.

기존의 GDI를 이용한 프로그램의 일부분을 Direct2D로 대체를 하는 것만으로도 성능을 향상시킬 수 있습니다.

 

기본적으로 Direct2D로 작업하는 순서는 다음과 같습니다.

  1. Direct2D 팩토리를 생성한다.
  2. 팩토리에서 렌더타겟을 생성한다.
  3. 렌더타겟에서 리소스들을 생성한다.
  4. 생성 되어진 리소스들을 이용해서 그리기 작업을 수행한다.

 

Direct2D의 모든 작업은 위의 순서를 따릅니다.

이제 이들 순서를 어떻게 API로 표현하는지 살펴보겠습니다.

 

화면 작업을 위해 준비하기

 

첫 번째로 작성해볼 프로그램은 특정 색상으로 화면을 채우는 작업을 하는 것입니다.

먼저 마법사로 프로젝트를 생성하고, "stdafx.h" 헤더파일에 Direct2D와 관련된 선언을 추가시켜주기 바랍니다.

위의 내용은 Direct2D와 관련된 라이브러리와 헤더파일을 선언해 준 것입니다.

 

그리고 작업을 수행할 .cpp 파일에 전역 변수를 두 개 선언합니다.

 

Direct2D 프로그래밍을 위해서 가장 먼저 해야 하는 일은 ID2D1Factory 를 생성하는 일입니다.

 

D2D1CreateFactory()의 첫 번째 인자는 멀티 스레드 지원 여부를 설정합니다.

이번 내용에서는 싱글 스레드만을 사용합니다.( 멀티스레드 어려워요~~)

두 번째 인자는 팩토리가 생성되어서 결과를 반환 받을 수 있는 팩토리 포인터를 넘겨줍니다.

이것이 성공하면, Direct2D 와 관련된 작업을 할 수 있게 됩니다.( 참~~ 쉽죠잉!! )

 

우리가 만들려고 하는 프로그램이 화면에 어떤 내용을 그리는 것입니다.

화면에 무엇인가를 그린다는 개념은 하드웨어 입장에서 봤을 때는 메모리에 값을 쓰는 것입니다.

여러분들이 보고 있는 모니터 화면은 거대한 메모리에 색상 값이 기록되어 있는 것입니다.

 

이번에 할 일은 바로 이 메모리 영역을 생성하는 일입니다.

Direct2D의 가장 큰 장점이 바로 이 메모리 영역에 값을 기록하는 작업( 이하 렌더링 )이

GDI를 이용하는 것보다 훨씬 빠르다는 것입니다.

왜냐하면, 바로 이 메모리 영역이 그래픽 카드에 있기 때문입니다.

 

그리기 명령을 수행할 메모리 영역을 생성하기 위해서 다음과 같이 코딩을 합니다.

바로 이 메모리 영역을 렌더타겟( RenderTarget ) 이라 합니다.

 

 

CreateHwndRenderTarget() 의 첫번째 인자는 화면에 대한 정보를 설정합니다.

픽셀 포맷이나 DPI 등의 많은 플래그와 옵션이 있지만, 현재는 디폴트 정보로 넘겨주었습니다.

두 번째 인자는 하드웨어 가속을 받는 렌더링에 대한 옵션을 설정합니다.

간단하게 크기 정보만 넘겨주는 것으로 마무리했습니다.

마지막으로는 이 API 호출이 성공했을 때 리턴되어지는 렌더타겟의 포인터를 저장할 변수를 넣어주었습니다.

이렇게 함으로써 간단하게 우리는 하드웨어 가속을 받을 수 있는 렌더타겟을 생성할 수 있습니다.

 

옵션이나 인자에 대한 설명을 충분히 드리면 좋겠지만, 너무 많습니다.^^

중요한 인자나 옵션에 대해서만 설명 드리는 점 양해 부탁 드립니다.

 

이제 우리는 렌더타겟을 가지고 있으니 이 메모리 영역에 값을 쓰면, 모니터로 결과를 확인할 수 있습니다.

윈도우가 화면에 그리기 위해서 발생하는 메시지가 WM_PAINT 입니다.

저는 이 메시지를 처리해서, 원하는 색상으로 렌더타겟의 색상을 채울 것입니다.

다음과 같이 코딩을 합니다.

 

이번 코드를 실행시키면, 파란색으로 칠해진 윈도우 프로그램을 만나게 될 것입니다.

더 정확하게 얘기하면, 메모리 영역( 렌더타겟 )이 파란색 색상데이터로 채워진 것입니다.

샘플을 올려둡니다.( SimpleDraw.zip )

도움이 되셨으면 좋겠습니다.^^

 


GDI vs Direct2D 비교해 보기.  

제가 아무리 Direct2D가 GDI보다 좋다고 혼자 말하는 것 보다, 여러분들이 직접 결과를 확인하는 것이 좋습니다.

그래서 이번에는 GDI와 Direct2D를 이용해서 타원을 렌더링 할 것입니다.

그리고 결과로 나오는 것을 보고, 여러분들이 직접 확인해 보기 바랍니다.

 

GDI 로 작업하기.

 

GDI를 이용하는 것은 전통적인 윈도우 프로그래밍에서 사용되던 방식입니다.

주변에서 이와 관련한 많은 내용들을 접할 수 있어서, 내용에 대한 자세한 설명은 생략하겠습니다.

Windows 운영체제에서 모든 드로잉(Drawing) 작업은

디바이스 컨텍스트 오브젝트( device-context object )를 통해서 실행이 됩니다.

이를 줄여서 'DC' 라고 줄여서 얘기합니다.( 다 아시죠? ^^ )

DC란, 윈도우즈 운영체제에서 컴퓨터 모니터나 프린터에 그리기 명령을 수행하기 위한

여러 속성 정보들을 구조화한 데이터입니다.

우리는 Windows API를 이용해서 DC를 이용한 그리기 작업을 수행할 수 있습니다.

DC를 이용하면, 우리는 어떠한 하드웨어 장치와는 관련이 없이 공통된 형식으로 화면에 그릴 수 있습니다.

이것이 가능한 이유는 DC는 CPU가 그리기 작업을 처리해 주기 때문입니다.

 

<코드>

HDC hDC;

HBRUSH hBrush,

hOldBrush;

 

if (!(hDC = GetDC (hwnd)))

return;

 

hBrush = CreateSolidBrush (RGB(0, 255, 255));

hOldBrush = SelectObject (hDC, hBrush);

 

Rectangle (hDC, 0, 0, 100, 200);

SelectObject (hDC, hOldBrush);

DeleteObject (hBrush);

ReleaseDC (hwnd, hDC);

</코드>

 

위의 코드는 간단히 DC를 이용해서 사각형을 그리는 코드입니다.

위에서 보는 것과 같이,

GetDC() 라는 API 를 통해서 현재 윈도우 애플리케이션에 대한 DC를 얻어서 작업을 수행합니다.

DC와 관련된 코드를 살펴보면, SelectXXX() 형식을 자주 볼 수 있습니다.

이는 DC가 일종의 상태 정보를 유지하고 있기 때문입니다.

예를 들면, 빨간 펜과 파란 펜으로 두 개 동시에 작업을 할 수는 없습니다.

하나의 펜으로 먼저 작업을 한 후에, 작업한 펜을 제거하고 다음 펜을 선택한 후에 작업을 해야 한다는 얘기입니다.

이점을 잘 고려해서 작업을 해야 하는 것이죠.

 

이것은 DC를 이용하는 하나의 방법일 뿐입니다.

만약 WM_PAINT 메시지 내부에서 처리하고자 한다면, 아래와 같은 구조를 취할 수 있습니다.

<코드>

PAINTSTRUCT ps;

 

case WM_PAINT :

hdc = BeginPaint(hWnd, &ps);

{

DoSomething()

}

EndPaint(hWnd, &ps);

break;

</코드>

 

PAINTSTRUCT 는 내부에 DC 관련 멤버 변수를 가지고 있습니다.

BeginPaint()를 통해서 DC 정보가 채워지게 되는 것입니다.

이러한 DC를 활용하는 것이 GDI 를 이용하는 것의 핵심입니다.

이와 관련된 내용을 더 언급하고 싶지만, 이 정도에서 정리하겠습니다.

이미 많은 분들이 저 보다는 훨씬 많은 지식을 가지고 있을 것이며,

관련 자료들도 이미 훌륭히 찾아 볼 수 있기 때문입니다.^^

 

우리가 지금 하려는 것은 GDI와 Direct2D의 비교입니다.

프로젝트를 만들고, 시스템을 셋팅하시기 바랍니다.( Direct2D 오브젝트도 생성해 주어야 합니다. )

WM_PAINT 메시지에 이 두 가지 방법을 사용해서 렌더링 하는 함수를 호출했습니다.

( 제가 정의한 함수들입니다. )

 

 

먼저, Direct2D를 이용해서 타원을 렌더링 하는 코드를 살펴보겠습니다.

 

이 방법은 뒤에도 설명을 하겠지만, Direct2D에서 DC를 활용하는 렌더링 방법입니다.

몇몇 생소한 개념들이 보이지만, 이들에 대해서는 차후에 설명을 할 것입니다.

 

GDI를 활용해서 타원을 렌더링 하는 코드는 다음과 같습니다.

자, 이제 이 둘의 결과는 예제 코드를 실행시키면, 다음과 같이 확인할 수 있습니다.

 

 

 

이는 타원의 일부를 캡쳐한 화면입니다.

좌측은 Direct2D의 결과이고, 우측은 GDI를 이용한 결과입니다.

겉으로 보기에는 차이가 없어 보이지만,

자세히 보면 우측의 경우는 울퉁불퉁한 계단 현상이 심한 것을 알 수 있습니다.

( 잘 보이지 않는다면, 샘플을 실행시켜보시기 바랍니다.^^ )

분명히 같은 기능을 하는 두 함수이지만, 결과에서는 이렇게 차이가 납니다.

샘플 파일( BasicRender.zip )을 같이 첨부했으니, 도움이 되셨으면 좋겠습니다.^^


신고
크리에이티브 커먼즈 라이선스
Creative Commons License

[StartD2D-2] 왜 GPU 인가?

DirectX 11 2011.03.25 08:00 Posted by 조진현

 

우리가 예전에 생각했던 PC는 어떤 모습 이였을까요?

앞서 언급했듯이, 아주 오래 전의 PC들은 하나의 CPU를 통해서 연산을 수행하고

결과를 저장하는 구조를 가지고 있었습니다.

또한 오늘 날의 그래픽 처리를 위한 GPU라는 개념도 초창기에는 상상하기 힘든 개념 이였습니다.

하지만, 오늘 날의 PC는 CPU는 여러 개이며, GPU의 성능 또한 아주 막강합니다.

거기다 멀티 GPU인 상황이기도 합니다.

이런 변화들은 Windows 운영체제 차원에서 많은 변화를 요구하게 되었습니다.

사실 현재의 개발 환경은 굉장히 과도기적인 상태라고 할 수 있습니다.

이제 막 이러한 패러다임의 변화들에 대해서 많은 소프트웨어적인 기술들이 공개되고,

개발자들의 선택을 기다리고 있는 상황입니다.

( 대표적으로는, TBB나 CUDA 같은 기술들이 있을 것입니다.^^ )

 

XP 시대까지는 많은 부분들이 전통적인 아키텍쳐 구조들을 기반으로 해서 구현되었고,

꾸준히 개선되어 왔습니다.

즉, XP 시대까지는 싱글코어 기반으로 대부분의 아키텍쳐들이 설계되었습니다.

그래서 Windows XP가 안정적이고 훌륭한 운영체제로 평가 받는 것입니다.

 

하지만, Windows 7 운영체제를 시작으로 앞으로는 많은 수의 CPU를 활용한 구조와

GPU를 활용하는 구조로 변경되고 있으며, 빠르게 XP세대를 대체해 나갈 것입니다.

( 정확하게는 Windows Vista 운영체제부터 시작되었습니다.^^ )

 

앞서 DirectX의 탄생의 과정에 대해서 짧게 살펴보았습니다.

DirectX의 가장 큰 장점은 그래픽 하드웨어의 지원을 받아서

빠른 성능으로 고품질의 결과를 처리할 수 있다는 것이다.

마이크로소프트는 DirectX를 이용해서 고속으로 드라이버에 접근할 수 있는 구조를 만들었습니다.

이를 HAL 이라고 합니다.

 

결론을 얘기하자면,

DirectX 이용한 렌더링( rendering ) 작업이 GDI를 이용한 작업보다 훨씬 더 빠르고 뛰어납니다.

품질은 비교해 보면, 아래와 같습니다.

 

 

현 세대의 PC들은 막강한 성능의 GPU를 탑재하고 있으며,

이들은 대부분 게임과 같은 멀티미디어 관련 애플리케이션을 실행하지 않는 이상은

거의 사용되지 못하고 있었습니다.

그래서 Windows 7 운영체제는 이를 활용하기 위해서 화면에 그리는 작업 패러다임을

완전히 변경해 버렸습니다.( 물론 비스타도 포함됩니다.^^ )

아래의 그림이 이제는 윈도우즈 운영체제 환경의 기본 추상화 계층입니다.

 

 

 

위의 그림에서 보이는 것처럼, 이제 화면에 무엇인가를 그리는 모든 작업은

DirectX를 이용해서 수행하게 되었습니다.( DXGI가 바로 DirectX 입니다. )

이 말은 즉, 기본적으로 GPU를 활용한 한다는 의미입니다.

그렇다고 현재 GDI 가 당장 사라져 버린 것은 아닙니다.

아직까지는 호환성 유지를 위해서 상당기간 공존할 것입니다.

하지만 DirectX 를 활용하는 이 방법은 빠르게 GDI를 대체해 나갈 것이다.

 

 

CPU는 범용 목적으로 설계되었기 때문에,

렌더링 목적으로 설계된 GPU 보다는 렌더링에 대한 작업만큼은 느릴 수 밖에 없습니다.

왜냐하면 GPU는 복잡하고, 많은 수치 연산에 특화된 구조이기 때문입니다.

 

이 DirectX를 강력함을 사용하기 때문에 XP 세대의 운영체제보다 Windows 7이 좋습니다.

( 왠지 홍보하는 것처럼 들리겠지만, 부인할 수 없는 사실이랍니다. ^^ )

 

앞으로 윈도우 프로그래밍에서도 이 DirectX를 이용하는 것이 보편화 될 것입니다.

이제 윈도우 프로그래밍 세계도 큰 변화가 예고되고 있습니다.

 

왜 GPU인가?

 

오늘 날, 프로세싱 유닛의 관점에서 컴퓨터를 바라보면 아래와 같습니다.

 

 

위의 그림에서 CPU는 4개입니다. 이 말은 연산 처리가 가능한 유닛이 4개라는 얘기입니다.

오른쪽 그림은 그래픽 카드를 표현한 것인데,

그래픽 카드는 SIMD 형태로 데이터를 병렬적으로 처리할 수 있는 유닛이 매우 많이 존재합니다.

직관적으로 평가해도 좌측의 CPU 4개 보다는 훨씬 많아 보입니다.

컴퓨터에 CPU 3GHz 가 4개 구동되고 있다면, 초당 연산을 하는 횟수가 48~96GFlops 라고 합니다. ( GFlops 는 109 Flops입니다. )

반면 1GHz GPU 1개가 처리할 수 있는 연산 횟수는 1TeraFlops 입니다. ( TeraFlops는 1012 Flops 입니다. )
GPU는 실수(float) 처리와 병렬처리에 이미 최적화 된 유닛이기 때문에 이것이 가능합니다.

반면 CPU는 범용 목적으로 설계된 유닛입니다.

그래서, If 문과 같은 조건 분기 명령어들은 GPU보다 CPU가 훨씬 빠르게 처리할 수 있습니다.

지금까지는 GPU는 그래픽 처리만을 위해서 존재했었습니다.

특히나 게임과 같은 대용량의 그래픽 처리를 필요로 하는 경우에는
이들의 역할이 절대적 이였습니다.

하지만, 그 이외의 경우는 사용되고 있었을까요?

대답은 '아니다' 입니다.

게임과 같은 경우 DirectX를 통해서 이들을 활용할 수 있었지만,

일반 애플리케이션의 경우에는 이 GPU를 활용할 방법이 없었습니다.

즉, 일반 애플리케이션에서 GPU는 거의 아무 일도 하지 않고 방치되어 있는 것입니다.

CPU의 일을 GPU에게 분담해서 CPU의 부담을 줄이고,

GPU의 활용 능력을 극대화 하면 자연스럽게 최적화가 가능합니다.

그래서 GPU를 활용하는 것이 현재 Windows 7 운영체제에서는

하나의 중요한 이슈로서 자리 잡고 있습니다.

신고
크리에이티브 커먼즈 라이선스
Creative Commons License


 

티스토리 툴바