티스토리에 글을 쓰다가 문득 위에 "동영상" 이라는 버튼이 보였습니다. 이게 뭘까? 한참을 고민하다가 설마하는 생각에 제가 가지고 있는 동영상을 올려보니 블로그에 표시되는 것이 아니겠습니까? @0@ 이글루스에서는 상상도 할 수 없던 것이 티스토리로 옮기니까 가능해 지는군요.
생각난 김에 예전에 만들어 뒀던 닌텐도 DS( NDS )용 작은 커널(Kernel) 시연 동영상을 올립니다. ^^ 커널은 OS(Operating System)의 핵심적인 부분으로 주변기기 제어에 필요한 잡다한 코드들을 제외한 핵심이라고 보시면 되는데요, 어차피 닌텐도 DS( NDS )는 그렇게 주변기기가 많지 않고 마이크 말고는 다 지원하고 있으니 작은 OS라고 해도 괜찮을 것 같습니다.
아래에서 시연 동영상을 보실 수 있습니다. 시작 화면이 시커멓게 나와서 조금 안타깝군요. ㅡ_ㅜ... 운영체제답게 멀티 태스킹을 지원하고 사운드 및 키패드를 이용해서 게임(??)을 Play 할 수 있습니다. 정겨운 버블버블 음악이 게임 내내 흘러나오고 @가 $를 다 먹으면 게임이 끝납니다. ^^;;;
NDS의 홈브루 게임 중에서도 Defence Tower 류의 게임을 몇개 구할 수 있는데, 게임을 하다 흥미가 생겨서 하나 만들어 보기로 했다.
1.지도(Map) 디자인
NDS의 화면은 256 * 192 Pixel의 듀얼로 구성되어있다. 위쪽 화면은 상태를 표시하는 화면으로 사용하고, 아래쪽 화면은 게임 진행 및 타워 설치 등등으로 사용하도록 하자. 그럼 이제 화면을 적절히 나누어서 최소 타일로 설정해야 하는데, 하나의 타일 크기가 적의 크기가 되도록 하자.
일단 최소 단위 블럭은 12 pixel * 12 pixel로 정했고 이것을 NDS의 한 화면으로 나누면 대충 아래와 같은 21 개 * 16 개의 Block이 나온다. 아래는 최외각을 블럭으로 쌓고 그 외의 공간을 중립 공간으로 설정한 맵의 구성이다.
<Map Design>
이제 최소 블럭 하나씩을 할당해서 적(Enemy)와 타워(Tower)를 할당한 후, 각 타워의 공격 범위(Sight)를 표시하면 아래와 같다.
<적과 타워 설치>
위의 그림은 공격범위가 3인 타워와 공격범위가 2인 타워를 나타낸다. 각 타워는 해당 범위내에 있는 적을 검색해서 적절한 타이밍에 총을 발사하면 될 것이다.
2.전체 디자인
게임의 전체적인 구성은 아래와 같이 되어있다.
<클래스 다이어그램>
게임의 주체는 Tower, Bullet, Enemy의 3가지로 구성되어있다.
Tower : 타워. 유저가 설치하여 적(Enemy)를 막도록 하는 구조물
Bullet : 타워가 적을 향해 발사하는 총알
Enemy : 유저를 괴롭히는 적
매 Tick 마다 각각의 주체는 적을 검색하고 총알을 쏘고 타워를 피하는 등등의 각자 맡은 일을 한다. 전략의 경우는 여러가지가 있을 수 있기 때문에 Strategy 패턴을 이용하여 쉽게 교체가 가능하도록 했다(전략부분에 대해서는 아래의 알고리즘 항목을 참고하도록 하자).
MapUtil 클래스를 통해서 Map에 접근함으로써 여기저기 중복될 수 있는 맵 관련 함수를 한 곳에 모았다. 게임의 전체적인 진행은 GameMain 클래스가 맡고 있으며 외부에서 GameMain 클래스를 통해서 게임을 진행하고 현재 진행 상태를 판단하게 된다.
3.알고리즘(Algorithm)
3.1 타워 알고리즘(Tower Algorithm)
타워의 알고리즘은 아주 간단하다. 타워의 발사 속도 및 범위를 이용해서 주변의 적을 검색하고 주변에 적이 있을 경우 총을 발사하면 된다. 발사 한 후에는 일정시간 이상 대기한 후 다시 발사하면 된다.
타워의 종류 또한 여러가지 있을 수 있으나, 일단 지금은 단발식 타워만 고려하고 업그레이드가 가능하도록 구현하였다.
3.2 적 알고리즘(Enemy Algorithm)
적의 알고리즘은 약간 까다롭다. 적의 레벨에 따라서 여러 알고리즘이 존재할 수 있는데, 아래와 같은 요건을 고려하였다.
무조건 시작점에서 출구점 방향(왼쪽에서 오른쪽)으로 이동해야 한다.
벽 및 타워는 통과할 수 없다.
적은 타워의 영향권을 가끔 벗어날 수 있다.
저 레벨의 적은 타워의 발사 범위 영향을 거의 고려하지 않는다.
고 레벨의 적은 타워의 발사 범위 영향을 많이 고려한다.
글로 쓸려니 약간 추상적인데, 코드로 보면 간단하다. 테스트 방법 및 코드는 아래를 참고하자.
3.3 적 알고리즘 테스트
MFC를 이용해서 간단히 알고리즘을 테스트하는 프로그램을 제작했다. 직접 눈으로 보면서 프로그래밍하면 더 나을 것 같아서 테스트 프로그램을 만들었다.
<알고리즘 테스트 화면>
각 ASCII 값이 나타내는 의미는 아래와 같다.
"1" : 타워. 실제 NDS로 포팅된 후에는 "T"로 바뀜
"X" : 적
"@" : 벽
붉은색 점 : 타워에서 발사된 총알
프로그램의 흰 바탕 위에 클릭을 하면 적절히 타워("1")이 설치되고 시작을 누르면 시뮬레이션이 시작된다. 더블 버퍼링을 하지 않았기 때문에 눈이 아프다는 큰 문제가... ㅜ_ㅜ
테스트 프로그램은 알고리즘을 테스트하기 위한 프로그램이고, 추후 알고리즘을 추가하고 싶을 때 먼저 테스트 한 후 NDS로 포팅하면 된다. 테스트 프로그램은 아래 첨부 파일에 올려 놓았다.
3.4 알고리즘 구성 방법
적(Enemy)는 다양한 알고리즘으로 구성되어야 하므로 Strategy 패턴을 이용하여 구성하였다. 아래는 전체 클래스 다이어그램에서 전략 알고리즘 부분을 표시한 부분이다.
<클래스다이어그램-Strategy 패턴>
현재 3가지 타입의 전략이 있는데, 목표를 향해 직선으로 이동하고 중간에 장애물이 있을 때, 아래 위로 살짝 피하는 MoveStraightStrategy를 필두로 하여, 그것을 상속받아서 조금 수정한 AvoidTowerStrategy, 그리고 멍청하게 행동하는 FoolishStrategy가 있다.
전략은 적이 생성될 때 랜덤하게 선택된다.
3.5 알고리즘 추가 및 테스트
알고리즘은 아래의 CStrategy를 상속받아서 DeterminDirection() 함수를 구현해 주기만 하면 끝난다.
virtual int DetermineDirection( int iX, int iY, int iLastDirection ) = 0;
};
리턴값으로 DIRECTION_UP 과 같은 방향 매크로를 넘겨주면 그 방향으로 이동한다. iLastDirection에 이전에 움직였던 방향값이 넘어옴으로 이를 잘 활용하면 여러가지 패턴을 만들 수 있다. 실제 구현 예제는 위를 상속받은 구체 Strategy 클래스를 참고하도록 하자.
3.NDS 포팅
3.1 NDS 개발 환경 및 포팅
NDS에 윈도우 라이브러리가 구현되어있으므로(물론 내가 만들었다. 자세한 내용은 02 NDS 윈도우 시스템(Windows System)을 참고하자) 위에서 설명한 알고리즘 테스트 프로그램의 소스를 크게 수정없이 사용할 수 있다. 차이라면 개인적인 취향 때문에 함수 몇개가 덜 구현되거나 스타일이 조금 다르다는 정도...?
아무것도 없는 허허벌판(??)에서 윈도우 라이브러리를 사용하기가 쉽지 않은데, 그래서 템플릿으로 사용할 수 있는 프로젝트를 올려놓았다. 26 윈도우 라이브러리(Window Library) 사용을 위한 프로젝트(Project) 만들기에서 프로젝트 파일을 찾을 수 있다. libfat도 같이 사용하도록 되어있으니 필요없는 사람은 makefile을 수정해서 빼도록하자(libfat를 빼니 홈브루 크기가 120K 정도 줄어든것 같다. @0@ 이럴수가!!! ).
포팅에 대한 내용은 크게 다루지 않을 것이며 궁금한 사람은 Diff 프로그램으로 위의 테스트 프로그램과 NDS로 포팅된 소스의 내용을 비교해 보면 알 수 있을 것이다. 메인 소스는 거의 바뀌지 않았음을 알 수 있다. 이 얼마나 행복한 일인가... ㅜ_ㅜ
3.2 실행 화면 및 게임 방법
게임 방법은 아주 간단하다. 타워를 클릭하면 설치하는 데, 일단 설치된 타워에 인접해서 설치는 불가능하다는 조건만 명심하면 된다(이 조건으로 인해 게임 난이도가 살짝 올라갔다. ㅜ_ㅜ). 즉 대충 놓으면 결과적으로 화면에 놓을 수 있는 타워의 개수도 적어지고 게임이 일찍 끝날 확률이 높다.
Select 버튼 : 게임을 처음부터 다시 시작
Start 버튼 : 펌웨어 화면으로 이동
빈곳에 터치 스크린 클릭 : Money가 5 이상 있을 때 타워 설치. 단 인접한 곳에 타워가 없어야 함
타워에 터치스크린 클릭 : Money가 20이상 있을 때 타워 업그레이드. 타워는 3단까지 업그레이드 가능
<게임을 실행한 화면>
4.마치면서...
디펜스 타워에 빠져서 개발하게 된 홈브루 치고는 좀 허술한데... 실제로 플레이 해보니 나름 할만해서 만족하고 있다. 추후 업그레이드는 별로 고려하지 않고 있지만, 적의 알고리즘이나 타워 설치 부분은 아쉬운 부분이 많아서 업그레이드 할지도...
매번 프로그램을 작성하면서 느끼는 것이지만 정말 UI에 소질이 없는 것 같다. 프로그램 작성하는 시간보다 그림 그리는데 시간이 더 많이드니... ㅡ_ㅡa... 이거 원 산출이 안맞아서... ㅜ_ㅜ..
NDS 사용자라면 문쉘(MoonShell)을 모르는 사람이 없을 것이다. 스킨 기능과 동영상 재생기능, MP3 Play기능 및 파일 보기 기능 등등~ 상당히 많은 기능들을 포함하고 있는 무시무시한 쉘이다. 시간 날때마다 문쉘의 소스를 뒤져보고 있지만, 어떻게 이렇게 복잡한 소스를 관리하는지 의문이 들 정도로 소스가 빼곡히 들어 차있다.
KKAMAGUI NDS Shell은 문쉘의 기능에서 필요한 기능만 추려내고 내가 필요한 기능을 넣어서 사용하기위한 간단한 NDS용 쉘(Shell) 프로그램이다. 문쉘이 비하면 아직 기능이 많이 부족하지만, 시간 날때마다 짬짬히 기능을 추가하고 있으니 언젠가는 문쉘처럼 될 수 있을 것이라 생각한다. ^^;;;
그럼 이제부터 KKAMAGUI NDS Shell에 대해서 알아보자.
1.프로그램 기능
오늘(2007/11/01 05:38:07)까지 구현된 기능은 아래와 같다.
듀얼 스크린 기능 : R 키를 통해 상하 스크린을 플립할 수 있는 기능. A키를 이용하여 Program Manager를 이동시켜 두 화면 모두 프로그램을 실행할 수 있음
파일 브라우징 기능 및 텍스트 뷰어 기능: File & Text Viewer를 이용하여 파일 리스트를 표시하고 L 키와 파일 리스트의 항목을 클릭함으로써 파일을 볼 수 있는 기능
한글 출력 기능 : 윈도우 타이틀 및 텍스트 뷰어에 한글을 출력하는 기능. 유니코드는 아직 미지원
KKAMAGUI NDS Shell은 FAT Library, Window Library, Reset Library를 사용한다. 각각의 library에 대한 내용은 아래를 참고하여 설치하면 된다.
FAT Library : libfat는 devkitPro를 사용하면 기본으로 깔려있기 때문에 굳이 손댈 필요는 없음. 다만 링크 후에 실행하기위해서는DLDI 패치를 수행해야 함. 새로 libfat를 컴파일해서 DLDI를 하는 불편함을 줄이고 싶은 사람은 01 libfat 업그레이드 문서를 참고
위의 라이브러리중 하나라도 빠지면 컴파일 또는 링크가 되지 않으므로 빠짐없이 설치하도록 하자.
3.소스 구성
KKAMAGUI NDS Shell의 대부분의 소스는 윈도우 라이브러리(Window Library)의 기본 윈도우와 리스트 윈도우, 스킨 윈도우를 상속받아서 커스터마이징(Customizing)하는 형태로 만들어졌다. 거의 윈도우 라이브러리를 테스트하기위한 프로젝트라고 해도 과언이 아닌데, 소스를 전부 설명하기는 어려우므로 첨부에 있는 소스 파일을 참고하도록 하자.
윈도우 라이브러리 사용에 대한 자세한 내용은 추후에 별도의 문서로 올릴 예정이니, 구조만 참고하면 될 듯하다. ^^;;;
4.사용방법 및 실행화면
버튼별 역할은 아래와 같다.
R 버튼:
스크린 전환 기능. 상하 LCD를 바꿔서 모든 스크린을 터치로 제어 가능하게 만듬
L 버튼:
File & Text Viewer에서 파일 or 디렉토리 선택 모드로 전환
선택 모드 사용 시 리스트의 항목 중에 터치 스크린으로 클릭된 부분이 붉은 색으로 변하고 터치를 때면 해당 항목의 파일이 Text Viewer로 표시되거나 해당 디렉토리로 이동
화면 터치:
윈도우 타이틀을 터치하는 경우 윈도우 이동
닫기 버튼인 경우 윈도우 닫기. 리스트 윈도우의 경우 스크롤 또는 항목 선택
Text Viewer의 경우 스크롤
A 버튼:
Program Manager 표시
화면 전환이 일어나서 Program Manager를 표시해야 하거나 Program Manager를 닫은 경우 다시 화면 가운데 표시
아래는 실행화면이다.
<실행화면>
5.마치면서...
몇가지 기능밖에 없는 허접한 쉘(Shell)이지만 NDS에서 쉘을 만들려고 고민하는 사람들에게는 좋은 예제라 생각한다. 앞으로 MP3 Play 정도는 추가할 예정인데, 어떻게 될지 모르겠다. ^^;;;;
어느 방법을 사용하던지 중요한 점은 NDS에서 편하게 불러쓰고 화면에 뿌릴 수 있어야 한다는 점이다. 그래서 KNG(KKAMAGUI NDS Graphic) 포맷을 개발하게 되었고, RGB555 포맷으로 그대로 변환하여 저장하게 하여 별도의 처리없이 화면에 표시가능하도록 구성했다.
이제 KNG에 대해서 하나하나 알아보자.
1.KNG 구성
KNG는 RGB555 포맷을 기본으로 사용하도록 만들어졌고 크게 헤더 부분과 데이터 부분, 두부분으로 구성되어있다. 헤더를 굳이 만들 필요가 있는가 하는 사람도 있겠지만, PSP와 같은 기기로 확장을 생각하고 있기 때문에 나름 범용으로 쓰기위해 헤더와 데이터로 구분했다.
아래는 헤더의 구성이다.
// 매크로 정의 #define VERSION_NDS_0_0 0x00
#pragma pack( push, 1 )
// 헤더 구조체
typedef struct kkamaguiNdsGraphicStruct
{ char vcSignature[ 3 ]; // KNG 설정 BYTE bVersion; // 이미지 포맷 버전, 0x00 설정 int iWidth; // 이미지 넓이 int iHeight; // 이미지 높이 } KNG, * PKNG;
#pragma pack( pop )
각 부분의 역할은 아래와 같다.
vcSignature : 파일 포맷의 유효성을 검증하기위한 방편
bVersion : 다양한 데이터 양식을 지원하기위해 넣은 버전 번호. 매크로에 정의되어있듯 NDS RGB555 포맷은 0x00으로 정의되어있음
iWidth : 이미지의 가로 넓이
iHeight : 이미지의 세로 넓이
데이터는 헤더의 바로 뒤에 연달아서 나오게 되어있으므로 헤더를 뛰어넘고 데이터를 접근하면 Raw Data에 바로 접근할 수 있다. 데이터 양을 줄이기 위해서는 Run-Length Encoding과 같은 방법을 사용해서 처리하면 사이즈를 줄일 수 있지만, 현재 버전에서는 굳이 넣지 않았다.
NDS Firmware 문서를 뒤져보면 Firmware에서 Run-Length Decoding과 Huffman Decoding 루틴을 제공하는 것을 알 수 있는데, 사이즈가 부담스러우면 두가지 방법을 사용하는 것도 괜찮은듯 하다.
2.KNG 파일 포맷으로 변환
KNG로 변환하는 것은 아주 간단하다. 헤더를 생성한 다음, RGB 포맷을 RGB555 포맷으로 수정하여 저장하면 끝이다. 첨부에 포함된 변환 프로그램 소스를 보면 쉽게 알 수 있는데, 핵심 부분은 CMyImage 클래스 이다. FreeImage 라이브러리를 사용해서 이미지를 읽고 헤더를 생성한후 RGB555 포맷으로 저장한다. 학교 수업때 사용한 프로젝트를 약간 변경한 소스여서 지저분한데, 마음에 안들면 새로 만들도록 하자.
아래는 CMyImage 클래스의 간단한 사용법이다.
CMyImage clImage;
... 생략 ...
clImage.LoadImage( "a.jpg" );
clImage.SaveImageWithConvert();
clImage.UnloadImage();
위를 실행하고 나면 a.jpg.kng 라는 파일이 a.jpg 파일이 있는 같은 폴더에 생성된다.
변환하는 과정을 좀더 상세히 살펴보자. 어떻게 변경하는 것일까? 아래는 실제 변환하는 소스이다.
/**
이미지를 변환과 함께 저장한다.
*/
BOOL CMyImage::SaveImageWithConvert( void )
{
CSize clSize;
int i;
int j;
RGBQUAD stRgb;
char vcNewFileName[ 256 ];
KNG stHeader;
int iFd;
WORD wValue;
WORD wR;
WORD wG;
WORD wB;
// 파일 이름을 설정한다.
vcNewFileName[ 0 ] = '\0';
_snprintf( vcNewFileName, sizeof( vcNewFileName ), "%s.kng", m_vcFileName );
pwDstAddr이 RGB555로 설정된 비디오 메모리의 주소이고, pwSrcAddr이 KNG 파일 내의 Raw Data의 주소이다. 간단히 메모리간의 복사로 해결할 수 있다.
4.변환 툴 사용법
이미지 변환툴은 jpg, png, bmp와 같은 그래픽 파일 포맷을 지원한다. 단 사이즈 조작 기능은 없으니 사이즈를 조작하려면 그림판이나 포토샵을 이용해서 이미 조작해야 할 것이다. 원활한 사용을 위해서는 이미지 크기가 256 X 192 이하로 하는게 좋다. 이 이상된다면 출력 루틴을 수정하고 그래픽 모드 또한 수정해야 할 것이므로, 정신 건강에 매우 해롭다. ㅡ_ㅡa...
아래는 변환툴을 실행한 화면이다.
<실행화면>
메뉴의 파일->이미지 파일 선택을 클릭하여 변환할 파일을 선택하거나, 툴바의 열기 버튼을 클릭하여(위의 붉은색) 변환할 파일을 선택하면 이미지 변환이 수행되고 아래와 같은 화면이 표시되면 정상적으로 변환 된 것이다.
<이미지 변환 성공>
해당 이미지가 있는 폴더에 보면 .kng 확장자가 붙은 파일이 추가로 생성되었음을 알 수 있다. 이 파일을 롬파일에 추가하거나 libfat를 이용해서 불러 사용하면 된다. 롬파일에 이미지 또는 파일을 추가하는 방법은 참고. 롬 파일에 데이터(사운드, 이미지 등등) 파일 포함 방법 문서를 참고하도록 하자.
5.마치면서...
NDS용 파일 포맷을 생성하고 변환하는 방법에 대해서 간단히 알아보았다. 이것으로 홈브루를 더욱 예쁘게 만들 수 있게 되었다. 더욱 홈브루 개발에 정진하자. @0@)/~!!!
resetlib의 main.c 파일과 main.cpp 파일에 포함된 함수(SoftReset)에 대한 링크 문제로 main.c를 main.cpp로 수정함
resetlib.h 파일에 extern "C"를 추가하여 링크 문제 해결
0.시작하면서...
소프트웨어 리셋 라이브러리의 모체는 23 Soft Reset 분석 에 나와있는 문쉘(moonshell)의 Reset.mse 플러그인 프로그램이다. 문쉘 플러그인으로 동작하게 만든 것을 소스를 변경하여 libfat만 있으면 동작 가능하도록 수정했다.
모든 소스에 대한 권리는 문쉘의 권리를 따르며 이 소스를 사용하거나 임의 수정하여 얻는 불이익에 대해서는 책임지지 않는다. ^^;;;
뭐 원래 오픈 소스이기 때문에 큰일이야 나겠냐 만은... ㅡ_ㅡa... 그래도 혹시나 모르니까 미리 알려둔다.
1.주된 변경 사항
1.1 GBA FS -> libfat
앞서 말했듯이 모체는 문쉘의 소스이다. 문쉘 소스는 과거의 FAT 라이브러리인 GBA FS를 사용하기 때문에 일반적으로 사용하는 libfat와는 맞지 않다. 그렇다고 리셋 기능 때문에 GBA FS를 그대로 사용하기에는 쓸모없는 코드가 너무 많이 들어가므로 이 부분을 libfat를 사용하도록 수정했다. 이 부분에 대한 자세한 내용은 23 Soft Reset 분석 문서를 참고하면 자세하게 알 수 있다.
1.2 IPCEX -> IPC
소프트웨어적인 리셋을 위해서는 ARM9과 ARM7 모두 리셋해야 하는데, ARM7의 리셋 신호를 위해 문쉘은 IPCEX라는 별도의 구조체를 사용하고 있다. 이것을 수정하여 libnds의 기본 정보는 IPC 구조체를 그대로 사용하도록 하고, mail 관련 정보를 사용하도록 했다. 자세한 내용은 아래의 사용 예제를 보면 쉽게 알 수 있다.
2.라이브러리 설치
2.1 소스 및 라이브러리 다운로드
resetlib를 사용하기위해서는 헤더파일과 라이브러리 파일만 libnds 폴더에 복사해 주면 된다. 아래 첨부에 있는 resetlib.zip을 다운받아서 압축을 해제하면 아래와 같이 소스파일과 헤더파일 그리고 라이브러리 파일이 생긴다.
<헤더 및 라이브러리>
2.2 라이브러리 파일 복사
lib 아래에 있는 라이브러리 파일을 devkitPro가 설치된 폴더에 있는 libnds\lib 폴더 아래로 복사한다.
<라이브러리 파일 복사>
2.3 헤더 파일 복사
resetlib.h 파일을 devkitPro가 설치된 폴더에 있는 libnds\include 폴더 아래로 복사한다.
<헤더 파일 복사>
이로써 라이브러리 설치가 완료되었다. 정말 간단하지 않은가? @0@)/~!!!
3.사용 예제
resetlib를 사용하기 위해서는 makefile을 수정해서 resetlib를 추가해주는 과정과 ARM9과 ARM7 소스에서 각각 Sync를 맞춰서 SoftReset() 함수를 호출해 주는 과정이 필요하다. 즉 ARM9만 사용하게 되어있는 프로젝트는 소프트웨어 리셋을 사용할 수 없다는 말이다.
ARM9 프로젝트를 ARM7과 ARM9을 사용하는 프로젝트로 변경하는 것은 그렇게 어려운 일이 아니니 devkitPro 예제에 templete 폴더에 있는 combined 템플릿을 이용하여 수정하도록 하자.
이제 makefile 수정방법부터 하나하나 알아보자.
3.1 makefile 수정
resetlib는 ARM9용과 ARM7 용이 따로 제작되어있다. 따라서 각각에 맞게 수정해 줘야 한다.
3.1.1 ARM9 수정
LIBS 부분을 아래와 같이 수정하여 libfat와 libreset9을 사용하도록 한다.
#---------------------------------------------------------------------------------
# any extra libraries we wish to link with the project
#--------------------------------------------------------------------------------- LIBS := -lfat -lnds9 -lreset9
3.1.2 ARM7 수정
LIBS 부분을 아래와 같이 수정하여 libfat와 libreset7을 사용하도록 한다.
LIBS := -lnds7 -lreset7
3.2 ARM9 및 ARM7 코드
3.2.1 ARM9 코드
간단히 헤더를 추가하고 libfat의 fatInitDefault() 함수를 호출해서 초기화를 수행한 다음 SoftReset() 함수를 호출해 주면 된다. 아주 간단하다.
// 추가해야 하는 부분
#include <fat.h>
#include <resetlib.h>
//---------------------------------------------------------------------------------
int main(void) {
//---------------------------------------------------------------------------------
REG_IME=0;
NDS를 사용하는 사람이라면 문쉘에 대해서 모르는 사람이 없을 것이다. MP3 Player는 물론 Text Viewer, Binary Viewer, 동영상 재생까지 모든 기능을 갖추고 있는 만능 쉘이다. 뭔지 모르는 사람들은... 끄응... 구글링이라도 한번... ㅡ_ㅡa...
나도 간단한 쉘을 만들고 있기에, MP3 Player에 과감히 도전해 보았다.
1.MP3 Decoder Library 선택
문쉘이 쓰고 있는 MP3 Decoder는 libmad로써 나름 괜찮은 성능을 자랑한다. 하지만 단점이라면 큰 크기랄까... ㅡ_ㅡ;;; 실제 libmad를 다운받아서 ARM7에 올렸는데, Huffman 테이블의 크기도 크고 코드 량이 많아서 NDS의 ARM7쪽 코드 및 데이터 영역을 넘어서 버렸다. 컴파일 다 하고 링크할 때 오류가 나서 빌드가 안되는 말도안되는 상황이... ㅡ_ㅡ;;;;
문쉘은 필요없는 부분을 추려내서 사이즈를 줄인 것 같은데, 자세하게 분석해 보지 않아서 정확하게는 모르겠다.
마구 추려내기 꺼림직하여 좀더 작은 크기의 library를 찾아보니 Helix library가 있었다. https://helixcommunity.org/ 에서 다운로드 받을 수 있는데, 절차가 아주 까다롭고 불편했다. 뭐 여러가지 검색을 하다보니 겨우 소스를 구할 수 있었는데, helix.zip 를 클릭하면 된다(첨부에도 넣어놨다).
Helix Community에서도 볼 수 있지만 장점이라면 fixed point에 최적화 되었고 작은 크기와 적은 CPU 사용량이랄까... 진짜 그런지는 모르겠지만 일단 크기가 작다니 믿고 쓰기로 했다.
2.Helix Decoder 컴파일
일단 소스를 ARM9 이나 ARM7에 부은 다음 makefile을 적절히 수정해야 한다. 일단 Helix 소스가 MyLibrary 아래에 있다고 가정하고 makefile을 수정하는 예이다.
ARCH := -mthumb-interwork <== 반드시 -mthumb 을 제거해야 한다. -mthumb를 제거하지 않으면 library 빌드 시에 ARM 모드 명령을 처리할 수 없어서 에러가 발생한다.
CFLAGS := -g -Wall -O1 -DARM \ <== 다양한 플랫폼을 제공하기 때문에 arm 용이라는 매크로를 정의해 준다. -march=armv5te -mtune=arm946e-s -fomit-frame-pointer\
-ffast-math \
$(ARCH)
... 생략 ...
위에서 보듯 -mthumb를 제거해야 한다. mthumb는 thumb 모드로 컴파일하라는 옵션인데, 이것을 지우지 않아서 빌드가 안되 한참 고생했다. 위의 예제는 ARM9 에서 빌드하여 사용한다고 가정했다.
//iMyPrintf("\x1b[20;0HTouch x = %04X, %04X\n", touchXY.x, touchXY.px);
//iMyPrintf("Touch y = %04X, %04X\n", touchXY.y, touchXY.py);
}
return 0;
}
4.실제 적용
Sound 출력 부분은 ARM7 만이 접근할 수 있다. 따라서 ARM9으로 디코딩한 후에 ARM7쪽에 동기를 맞추어 버퍼를 넘겨주던지, 아니면 ARM9에서 파일을 읽어서 ARM7에 넘겨주고 ARM7이 디코딩하고 결과를 출력하던지... 2가지 방법이 있다.
ARM7에서 파일을 읽고 디코딩하고 출력하는 방법은 안될까? 안타깝지만 테스트 결과 libfat가 ARM7에서 동작하지 않았다. 고로 우리의 선택은 두가지 중에 하나를 해야 하는데, 현재(2007/10/13 21:24:43) 테스트 프로그램은 ARM9에서 읽어서 약 10초간 디코딩한 후에 디코딩 버퍼를 ARM7에 넘겨주고 ARM7은 마치 자기가 디코딩해서 출력하는 양 버퍼를 잘라서 Timer에 맞추어 더블 버퍼링 비슷(?)하게 동작한다.
즉 목표는 ARM9에서는 파일만 읽어서 데이터를 ARM7에 넘겨주고 ARM7이 디코딩 + 출력까지 다하는 것이다.
ARM9 이던 ARM7 이던 소리를 제대로 출력하기 위해서는 MP3 디코더에 의해서 디코딩된 프레임이 나왔을때 이를 적절한 타이밍에 잘 출력해 줘야 한다. 즉 타이밍이라는 문제에 봉착하는 것이다. 실제 Helix로 MP3 파일을 디코딩하면 하나의 프레임당 2304 개의 sample이 나온다. 스테레오라고 가정할 때 1152개의 L/R Sample이 나오는데, 44100Hz로 Sampling된 경우 겨우 몇 ms 정도 출력하는 양이다.
우리가 제대로 된 소리를 들을려면 이 프레임을 계속 디코딩하여 sample을 얻고 sample을 정확한 시간에 출력해 주는 것이 관건인데, 이 문제로 거의 3일을 고민했다. 수많은 테스트를 거쳐서(삽질도 포함해서... ㅡ_ㅜ... 사운드 출력함수를 잘못쓰다니... 젠장... ㅜ_ㅜ...) 겨우 제대로된 소리를 출력할 수 있었다.
중요한 테크닉은 타이머 및 인터럽트를 사용해서 sample이 Play 완료 되는 시점을 정확하게 구하고 이 시점에서 다음 버퍼를 다른 채널로 Play 시키는 것이다. 같은 채널로 Play 시키면 소리가 정지됬다가 나오므로 약간 잡음이 들린다.
이 타이밍 문제에 대한 자세한 내용은 아래의 6.테스트 및 진행과정 을 참고하도록 하자.
첨부에 포함된 테스트 프로그램은 Root Directory에 있는 a.mp3 파일을 읽어서 10초 분량을 디코딩한 뒤에 ARM7에 넘겨서 Play한 예제이다. 아직 갈길이 멀지만 기념으로 올린다.
libnds를 이용해서 프로그램을 작성하는 예제는 충분히 많이 있다. 하지만 이것만 가지고는 무엇인가 부족하다. NDS에서 멀티 태스킹을 지원할 수 는 없을까? NDS는 타이머를 4개나 가지고 있기 때문에 시분할 멀티 태스킹을 하는데 전혀 문제가 없다.
커널이 되기위해서는 빠질 수 없는 기능이 멀티 태스킹이고 하니, NDS에 태스크 스위칭(Task Swithcing) 기능을 넣어보도록 하자. 컨텍스트 저장에 사용되는 곳은 OS 프레임워크와 동일하게 각 태스크 스택의 가장 아래부분(0 쪽에 가까운 부분)에 저장되고 아래의 순서로 총 16개의 값이 저장된다.
위의 그림은 아래에서 설명할 소스코드를 이해하는데 도움이 되므로 꼭 봐두도록 하자
1.System/User 모드에서 태스크 스위칭(Task Switching)
유저모드에서 간단히 태스크 스위칭 하는 방법은 스택에다가 레지스터를 전부 넣고 스택을 스위칭 한 뒤에 전부 빼면된다. 저장하는 방식은 스택의 Bottom에서 레지스터의 갯수만큼을 저장하고 다시 복원하면 간단히 해결할 수 있다.
태스크 스위치를 수행하는 함수
SwitchTask: // 0x0000 -> 0xFFFF로 올라가니까 넣고 증가시켜야 한다.
STMIA R0, { R0-R14 }
LDMIA R1, { R0-R13, pc}
2.타이머 인터럽트(Timer Interrupt)를 통한 태스크 스위칭(Task Swithcing)
2.1 인터럽트 핸들러(Interrupt Handler) 코드와 스택(Stack)
타이머 인터럽트를 통해 태스트스위칭을 할때 User/System 모드와 다른 점은 모드가 User/System 모드에서 IRQ 모드로 바뀐다는 것이다. 프로세서의 모드에 대한 내용은 참고. ARM Processor Overview의 내용을 참고하도록 하고 실질적인 문제에 대해서 알아보자.
가장 큰 문제는 레지스터가 뱅크되어 IRQ 모드의 R13(sp)와 R14(lr), 그리고 SPSR을 가진다는 것이다. 따라서 정상적인 태스트 스위칭을 위해서는 현재 태스크를 저장할 때 User/System 모드의 레지스터 및 SPSR을 저장해야 하며 복원할 태스크 또한 유저모드의 R13과 R14를 잘 복원해주고 유저모드의 CPSR에SPSR에 넣어준 다음 리턴해야 한다.
문제는 이것만이 아니다. 더 큰 문제는 NDS BIOS 라이브러리에 있는 인터럽트 처리 루틴을 통과한 후에 내가 제어를 이어받는 다는 것인데, NDS BIOS에서 인터럽트 선행 처리를 위해 무슨 일을 하는지 모르면 컨텍스트(Context)를 정확하게 저장할 수 없다. 상당히 충격적인 내용인데... BIOS와 동작 호환을 위해서는 이 부분에 대한 처리를 해줘야.. ㅜ_ㅜ...
일단 NDS의 인터럽트 디스패처(Interrupt Dispatcher) 소스는 \devkitPro\libnds\source\source\common 폴더의interruptDispatcher.s 파일에서 찾을 수 있고 아래와 같다.
코드의 중요부분은 IRQ Table에서 해당 처리 루틴을 찾아서 IRQ 모드에서 System 모드로 변경한 다음 함수를 호출하는 부분이다. 즉 타이머 인터럽트가 발생하면 내가 만든 타미어 핸들러를 호출하고 이 타이머 핸들러에서 리턴을 하면 정상적인 루트를 통해 다시 복구하도록 되어있다.
그렇다면 IntrMain 이라는 코드는 NDS의 BIOS가 불러준다는 이야긴데... 어떻게 찾아서 저 함수를 불러주는 것일까?
답은 아래는 \devkitPro\libnds\source\source\common 폴더에 있는 Interrupts.c 소스에서 찾을 수 있다. irqInit() 함수에서 위의 IntrMain 함수를 BIOS에서 호출해 줄 핸들러로 등록한다.
........ 생략 ........
//---------------------------------------------------------------------------------
void irqInit() {
//---------------------------------------------------------------------------------
int i;
// Set all interrupts to dummy functions.
for(i = 0; i < MAX_INTERRUPTS; i ++)
{
irqTable[i].handler = irqDummy;
irqTable[i].mask = 0;
}
**IRQ_HANDLER = IntrMain;**
REG_IE = 0; // disable all interrupts
REG_IF = IRQ_ALL; // clear all pending interrupts
REG_IME = 1; // enable global interrupt
}
........ 생략 ........
여기서 잠깐.... 뭔가 이상한 점이 느껴지지 않는가? 보통 인터럽트가 발생하면 컨텍스트(Context)를 다 저장해 주고 인터럽트 처리 함수를 호출하여 인터럽트에 대한 처리를 한다음 다시 컨텍스트를 복구하는 절차를 거친다. 그런데 위의 InterruptDispatcher.s 소스에서는 그런 루틴이 보이지 않는다.
ARM 같은 경우라면 ldmfd sp!, {r0-r13} 등과 같은 코드가 있어야 할터인데... 이로보아 IntrMain 코드는 NDS의 BIOS에 의해 불려지는 코드임을 추측할 수 있다.
The user ARM9 interrupt vector is thus at DTCM+0x3FFC
In both cases, the BIOS flag word used inswi0x4 and 0x5 is 4 bytes before the interrupt vector.
위에서 보면 알 수 있듯이 R0, R1, R2, R3, R12, lr을 저장하고 핸들러를 부른다음 IntrMain(붉은 색으로 표시된 부분)을 부르는 것을 알 수 있다. 그 뒤 다시 스택에서 레지스터를 다 복원한 다음 리턴한다. 모드 전환과 같은 코드가 없는 걸 봐서 libnds의 interruptDispatcher.s 파일에 있는 IntrMain 함수를 호출할때는 IRQ 스택을 사용하는 상태이다.
System 모드의 스택은 태스크마다 분리되어있기 때문에 저장해도 별 문제가 없지만 IRQ 레벨의 스택은 공통적으로 사용하기 때문에 만약 타이머 IRQ에서 다시 다른 IRQ가 발생하여 IRQ 스택에 데이터를 PUSH나 POP하면 문제가 발생한다.
일단 위험에 대해서는 위에서 많이 설명했으니 Interrupt가 발생하여 BIOS -> libnds의 IntrMain -> MyTimerHandler 순서로 호출되었을 때 System모드의 스택과 IRQ 모드의 스택 내용을 한번 보자.
<사용자 인터럽트 처리 함수까지 왔을 때의 스택의 모습>
위에서 IRQ 스택과 System 스택 2개로 나누어 진 것은 libnds의 Handler함수(IntrMain)에 의해서 System 모드로 전환되기 때문이다.
사용자 핸들러 함수(My Handler)가 불린 시점은 이미 System 모드로 전환된 상태이며 스택에 System 모드의 LR(R14) 레지스터의 값이 스택에 저장되어있다. 태스크 스위칭을 이 상황에서 수행하려면 저장하는 컨택스트는 IRQ 스택에서 SPSR과 R0~R3, R12, IRQ_LR을 빼서 저장해야 하고 System 스택에서는 SYSEM_LR을 빼서 저장해야 한다.
2.2 ARM 모드와 THUMB 모드
그런데 이것이 끝이 아니었다. 근 3일 정도를 스택을 계속 보면서 고민했는데, 그냥 리턴을 해서는 정상적으로 동작하지 않았다.
뭐가 문제일까? 그렇게 3일을 고민한 끝에 ARM 모드와 THUMB 모드의 전환... 이라는 내용이 머리를 스쳤다. 컴파일 옵션을 보면 우리가 C로 만든 태스크는 컴파일 옵션에 의해 THUMB 모드로 동작하게 되어있다. 그리고 ARM 모드로 컴파일된 코드를 호출할 수 있도록 Interwork 라는 옵션도 같이 들어있다. 이 말은 ARM 모드와 THUMB 모드를 넘나들며 함수를 호출할 때 중간에 Proxy 함수를 이용해서 호출해서 자동으로 처리해 준다는 이야긴데.... 일반적인 함수 호출에는 큰 문제가 없는 것 같았다. 하지만... 컨텍스트 스위칭의 경우에는 다르다. 레지스터 하나라도 복구가 잘못되면 그냥 크래쉬.. ㅜ_ㅜ...
주의할 점을 하나하나 집어보도록 하자.
2.2.1 CPSR의 설정
C 코드로 태스크 함수를 작성할 경우는 THUMB 모드 코드가 나오므로 태스크를 실행할때 당연히 CPU가 THUMB 모드로 설정되어야 한다. 따라서 복원할 CPSR의 상태를 THUMB Bit를 1을 키고 SYSTEM 모드(0x1F)로 설정해야 한다. 그런데 만약 어셈블리어 코드로 태스크 함수를 작성하는 경우는 어셈블리어 코드의 생성 옵션(.ARM or .THUMB) 같은 옵션에 맞게 설정해 주어야 한다.
2.2.2 ARM 모드 코드와 THUMB 모드 코드
C 코드(THUMB 코드)에서 어셈블리어 코드(ARM 코드)를 호출하면 어떻게 될까? 컴파일러가 자동적으로 Proxy 함수를 생성하여 C 함수 코드(THUMB 코드) -> THUMB/ARM 변경 코드 -> 어셈블리어 코드(ARM 코드)의 순서로 호출되게 된다. 여기서 잠깐 생각해 볼 것이 Proxy 함수를 거치면 상태 값이 살짝 바뀐다는 것이다. 여기에 대한 자세한 내용은 참고. THUMB 코드와 ARM 코드 및 상호 호출(Interwork) 부분을 참고 하도록 하자. 따라서 이 부분에 대한 처리도 해줘야 하므로 상당히 복잡해 진다.
어셈블리어 코드는 .ARM 모드에서 작성되어 모든 레지스터를 다 저장하고 복구하도록 되어있다. 하지만 타이머 인터럽트에 의해 불리어지는 C 함수는 THUMB 모드로 컴파일 되었고, 이 C 함수를 불러주는 인터럽트 함수는 ARM 모드에서 컴파일 된 핸들러 루틴이다.
이쯤되면 골치가 지끈 아파지고 쉽지 않을꺼라는 생각이 들텐데... 이 문제 때문에 3일을 고생했다. 결국은 요령으로 해결하긴 했지만... ARM 모드와 THUMB 모드를 번갈아가며 사용할 때 태스크 스위칭이 이렇게 힘든 줄 몰랐다.
2.2.3 태스크 스위칭(Task Switching) 코드
한참을 고생한 뒤에 나온 소스가 아래의 태스크 스위칭 코드이다.
@---------------------------------------------------------------------------------
.section ".text"
.global SwitchTask
.global SwitchTask2
.global isrTimerInAsm
.extern isrTimerInC
.global g_dwCurTask
.global g_dwNextTask
@---------------------------------------------------------------------------------
.align 4
.arm
@ Timer Interrupt에서 바로 호출해 주는 함수
isrTimerInAsm:
PUSH { LR }
BL isrTimerInC
LDR R0, g_dwCurTask
LDR R1, g_dwNextTask
CMP R1, #0
BEQ TIMEREND
ADD R2, SP, #0x04
bl SwitchTask2
TIMEREND:
POP { LR }
BX LR
/*
Switch Task
SYSTEM_R0-R14(LR), IRQ_R14, SPSR 순서로 저장한다.
IME가 불가된 상태에서 호출하면 안된다.
이 함수가 끝나면 가능상태로 바뀌기 때문이다.
*/
SwitchTask2:
// 인터럽트 불가 설정
MOV R3, #0x4000000
STR R3, [ R3, #0x208 ]
MOV SP, R2
POP { LR }
// System 모드 레지스터를 저장한다.
// R4-R14 저장
ADD R3, R0, #16
STMIA R3, { R4-R14 }^
MRS R3, CPSR
BIC R3, R3, #0xDF
ORR R3, R3, #0x92 // ISR 모드로 변경
MSR CPSR, R3
LDMFD SP!, { R5-R6, R7 }
//STR R6, [ R7, #0x208 ]
// SPSR 저장
//MSR SPSR, R0
STR R5, [ R0, #64 ]
// R0-R3, R12, IRQ_LR 저장
LDMFD SP!, { R5-R8, R12, LR }
STMIA R0, { R5-R8 }
STR R12, [ R0, #48 ]
SUB LR, LR, #0x04
STR LR, [ R0, #60 ]
// 여기까지 오면 저장 끝...
// 여기서 부터는 복원 시작..
// SPSR 복원
LDR R5, [ R1, #64 ]
MSR SPSR, R5
LDR LR, [ R1, #60 ]
// 인터럽트 가능 설정
MOV R3, #0x4000001
STR R3, [ R3, #0x207 ]
LDMIA R1, { R0-R14 }^
//SUBS PC, LR, #0x00
MOVS PC, LR
/*
Task Switch용 데이터를 저장하는 공간
*/
g_dwCurTask: nop
nop
g_dwNextTask:
nop
nop
간단한 아이디어는 아래와 같다.
libnds에 의해 호출되는 타이머 인터럽트 핸들러는 ARM 코드로 작성한다.
스케줄링을 통해 스위칭할 태스크의 변수를 설정하고 기타 처리를하는 함수는 C로 작성한다.(THUMB 코드가 된다.)
스케줄링 함수에서는 전역 변수 g_dwCurTask, g_dwNextTask 변수에 태스크 스위칭할 태스크 구조체를 넣어서 리턴한다.
타이머 핸들러 함수(ARM 코드)에서에 전역 변수 g_dwCurTask, g_dwNextTask를 이용하여** 태스크 스위칭 함수(ARM 코드)**를 호출한다.
태스크 스위칭 함수로 들어오면 스택 구조는 위 그림에서 보는 것과 같은 형태로 되어있으므로 IRQ 모드 및 SYSTEM 모드를 오가면서 태스크를 저장하고 복원한다.
THUMB 모드에서 이 함수를 호출 할 경우에는 Proxy 함수(THUMB에서 ARM 모드로 변경하는 함수)를 통해 호출되므로 모드가 변경되어 호출된다. 따라서 이 THUMB 모드 함수에서 태스크 스위칭 함수(ARM 코드)를 부르게 되면 모드 전환같은 곤란한 문제가 생기므로 약간 곤란하다. 이를 피하기 위해 내가 만든 타이머 인터럽트 핸들러 함수(ARM 코드)에서 태스크 스위칭 함수를 호출하여준다.
만약 스케줄러가 NULL 값을 전역 변수 g_dwNextTask로 설정하면 태스크 스위칭을 호출하지 않고 정상적인 루트로 리턴을 하여 돌아간다. THUMB 모드에서 ARM 모드로 이동했을때 Proxy를 사용한다는 것은 알고 있었지만... 그게 이렇게 큰 문제를 일으킬줄은 몰랐다. 단순한 함수의 호출같은 문제는 크게 관계 없지만... 컨텍스트 스위칭의 경우는 세세한 부분까지 신경을 써줘야 하는 부분인데... 이렇게 되니 완전 눈물이 나는... ㅜ_ㅜ
단순히 타이머를 20ms 마다 튀도록 설정해 놓고 isrTimerInC 함수에서 스케줄러 함수를 부르는 것을 볼 수 있다. 스케줄러 함수는 g_dwCurTask 및 g_dwNextTask 에 스위칭 할 태스크들을 설정해서 리턴한다. isrTimerInC 함수가 리턴되고 나면 위에서 설명했던 isrTimerInAsm 함수에서 실제로 스위칭하는 함수를 부르게 된다.
3.2 Snake 게임
원래 Snake 게임은 꼬리가 길어지고 점점 사과가 늘어나고 그래야 하지만.... 내가 만든 Snake 게임은... 꼬리도 안 길어지고 사과만 먹으면 된다. 사과(녹색 $ 표시)를 다 먹으면 먹은 사과의 개수와 Play하는데 걸린 시간이 표시된다. 사과를 다 먹기 전에 벽(하얀색 #)에 부딪히게되면 게임이 종료되므로 주의해야 한다. 게임이 종료되면 A 버튼을 누르면 다시 게임을 제개할 수 있다.
제일 주의할 점은.... 중독성이 있으니 자제를 해야한다는 것이다. 시간 갱신에 힘쓰다보면 큰일이 생기니 적당히 하도록 하자. @0@)/~~
3.3 기타 테스크
다른 테스크들은 매우 간단한 일만 하는 테스크이므로 굳이 설명을 하지 않겠다. 소스를 보면 쉽게 이해가 될터이니...(하는 일도 별로 없고.. ㅡ_ㅡ;;;)
4.실행화면
<NDS 커널의 실행화면>
NDS 커널을 실행하면 위와 같은 화면이 나온다. 총 5개의 태스크가 돌아가는 화면이며 각 태스크는 아래와 같은 위치에 있다.
위의 붉은 색 사각형 하나하나가 다 개별적인 태스크로 동작하며 시분할 스케줄링 기법을 이용하여 동시(??)에 동작한다. 특히 Task3번의 경우는 2초마다 사운드를 출력하도록 되어있기 때문에 주기적으로 울리는 소리를 들을 수 있다.
처음 NDS 홈브루를 개발하면서 libfat의 능력 및 libnds의 테스트 용으로 간단한 메모장 프로그램인 KKAMAGUI Notepad를 개발하였다(자세한 내용은 00 KKAMAGUI Notepad 참조). 테스트 결과 libfat가 약간 불안함을 발견하였고, 터치스크린의 경우 튀는 문제가 발생하였다.
하지만 분석 결과 튀는 순간이 처음 터치스크린을 찍는 부분과 터치스크린에서 펜을 때어낼 때임을 발견하였고 이를 버퍼와 범위 체크를 통해 어느정도 완화시켜서 그렇게 큰 문제는 되지 않았다.
그후 5개월이 지난 지금... NDS에 대한 분석이 진행되면서 ARM7 코드도 같이 삽입하고 테스트 할 수 있게 되어 터치스크린 문제를 해결하고 기존에 부족했던 기능을 추가하여 새롭게 업그레이드를 했다. 그것이 바로 Advanced KKAMAGUI Notepad이다.
1.터치스크린(Touch Screen) 튐 현상 해결
터치스크린의 튐 현상은 libnds의 고질적인 문제였다. 여러 홈브루를 테스트해봐도 터치가 튀는 현상을 발견할 수 있다. 이 부분을 해결하려면 ARM7 코드를 손을 대어야 했는데, 결국 해냈다( 장하다 KKAMAGUI @0@)/~~).
기존의 KAMAGUI Notepad는 NDS가 접혔을 때... 즉 폴딩(Folding) 되었을 때 LCD를 끄는 처리만 되어있었다. 다른 게임들을 보면 알겠지만 NDS의 LED가 깜빡이면서 대기중인 것을 표시한다. Advanced KKAMAGUI Notepad 또한 그 기능을 추가하여 폴딩 되었을때 LED를 깜빡이도록 했다.
2007/10/18 21:12:24 : MovableWnd가 없어지고 Move 기능이 별도의 클래스로 이동함. 타이틀바를 클릭했을 때, 윈도우 이동을 지원하고 싶으면 CMovable 클래스를 사용하면 됨
0.시작하면서...
내가 만든 NDS 윈도우 시스템(Window System)에 대한 문서이다. NDS 윈도우 시스템은 최대한 윈도우의 MFC 구조와 비슷하게 하여 윈도우 프로그래밍에 익숙한 사람이라면 누구나 쉽게 접근할 수 있도록 하는 것이 목적이다.
현재 Base Window와 DC 정도만 구현되어있으며, 이것을 활용하여 만들어진 홈브류는 00 KKAMAGUI NOTEPAD 가 있다.
현재 전체적인 윈도우의 구조는 아래와 같이 구성되어있다.
<전체 클래스 다이어그램>
주의할 점은 아래와 같다.
최대한 비슷하게 만들려는 의도지만, 여건상 메시지 처리 부분과 함수 호출의 순서에 약간 차이가 있을 수 있다. 따라서 윈도우 프로그래밍에 대한 지식으로 접근하면 제대로 동작하지 않을 수 있다.
자동으로 처리되는 것은 거의 없다. 하다못해 ShowWindow() 함수를 이용해서 윈도우를 숨겼을 때, 단순히 윈도우를 그려주지만 않을 뿐 나머지 처리는 알아서 다 해야한다. 즉 원하는 게 있으면 전부 손으로 작성해야 한다.
참고할 점은 아래와 같다.
라이브러리는 RGB555 포맷을 사용하는 Frame Buffer 모드를 기본으로 사용한다고 가정하고 제작되었다.
만약 다른 모드를 사용한다면 2D Raster 함수 부분을 적절하게 수정해야 할 것이다.
아래는 Frame Buffer 모드로 설정하는 예제이다.
/**
Main LCD 및 SUB LCD를 모두 16bit 256 * 196 로 설정한다.
Frame Buffer와 같이 쓸 수 있도록 수정한다.
*/
void InitVideoMode()
{
// 16bit Color Mode 5
videoSetMode( MODE_5_2D | DISPLAY_BG3_ACTIVE );
videoSetModeSub( MODE_5_2D | DISPLAY_BG3_ACTIVE );
// Video Memory를 설정한다. MAIN 같은 경우는 2개의 VRAM이 맵핑되어있으므로
// 더블 버퍼의 사용도 가능하다.
vramSetMainBanks( VRAM_A_MAIN_BG_0x06000000, VRAM_B_MAIN_BG_0x06020000,
VRAM_C_SUB_BG, VRAM_D_LCD);
// Background에 대한 설정을 한다. BG_BMP_BASE를 조절하면 스크롤 및
// 더블 버퍼를 구현할 수 있다.
BG3_CR = BG_BMP16_256x256 | BG_BMP_BASE( 0 ); //| BG_PRIORITY( 3 );
// scale을 1, rotation을 0으로 설정하여 frame buffer와 같게 만듬
BG3_XDX = 1 << 8;
BG3_XDY = 0;
BG3_YDX = 0;
BG3_YDY = 1 << 8;
// Translation(Reference Point X/Y-Coordinate)을 0으로 설정
// 표시하는 위치를 옮기고 싶으면 조절
BG3_CX = 0;
BG3_CY = 0;
// x축 및 y축으로 100 pixel 이동
//BG3_CX = 100 << 8;
//BG3_CY = 100 << 8;
윈도우 라이브러리는 클립핑이 지원되는 라인 그리기 함수 및 사각형 그리기 함수를 지원한다.
라인을 그리는 함수는 Bresenham 알고리즘을 사용하고 클립핑 알고리즘으로는 cohen-sutherland 알고리즘을 이용했다. 사각형 타입은 Fill Rect와 Rect 두가지 타입이 있는데, Rect는 라인을 그리는 함수를 이용하여 구현되었다.
2D Raster 함수들은 클립핑 영역을 받도록 되어있는데, 일단 기본 설정은 스크린에 그리는 것으로 고정되어있다. 따라서 Y 좌표의 계산은 Y * SCREEN_WIDTH로 설정되어있으며 추후 스크린이 아닌 메모리 영역에 그릴 경우 이 부분을 수정할 필요가 있다.
2D Raster 함수들은 아래와 같이 구성되어있고 CDC 클래스에서 사용된다.
void DrawLine( RECT stClippingRect, void* pvAddr, int iX1, int iY1, int iX2,
int iY2, unsigned short usColor );
void DrawBox( RECT stClippingRect, void* pvAddr, int iX1, int iY1, int iX2,
int iY2, unsigned short usColor, bool bFill );
void DrawPixel( RECT stClippingRect, void* pvAddr, int iX, int iY,
unsigned short usColor );
2.한글/영문 출력
한글/영문 출력 부분은 자작한 폰트 생성 툴을 이용해서 비트마스크 폰트를 생성한 후 이를 출력하는 방법으로 구현하였다. 물론 클립핑에 대한 처리도 되어있다.
현재 폰트는 12x12 크기의 한글 폰트와 6x12 크기의 영문 폰트로 구성되어있다. Font 출력에 대한 함수들은 아래와 같이 구성되어있고 CDC 클래스에서 사용된다.
// 한글/영어 다 찍을 수 있다.
void PrintChar( RECT stClippingRect, void* pvBaseAddr, int iX, int iY,
unsigned short usForeColor, unsigned short usBackColor, char cChar );
void PrintString( RECT stClippingRect, void* pvBaseAddr, int iX, int iY,
unsigned short usForeColor, unsigned short usBackColor, char* pcString );
Z-Order는 간단하게 구현되어있다. 윈도우 메니져가 Z-Order의 첫번째 윈도우만 관리하고 나머지는 윈도우 자신이 다음에 위치하는 윈도우의 포인터를 가지는 방식으로 관리한다. 이렇게 함으로써 윈도우 메니져의 자료구조를 단순하게 하고 윈도우 관리에 필요한 추가적인 자료구조를 없앴다.
Z-Order의 구성은 윈도우를 화면에 그리는 순서와도 관계가 있는데 Z-Order의 Bottom에서부터 Z-Order의 Top 순으로 OnPaint() 함수를 호출해서 그린다.
아래는 Z-Order의 순서와 실제 화면과의 관계를 나타낸 그림이다.
<Z-Order & Screen>
4.윈도우 그리기 및 그리는 최소 영역 설정
윈도우즈나 X-Window 같은 윈도우 시스템의 경우, 무식하게 화면 전체를 계속해서 갱신하지 않는다. 또한 윈도우 하나의 내용이 갱신되었다고 해서 윈도우 전체를 다시 그리는 일도 하지 않는다. 변화되면 해당 부분만 그려서 그리는 영역을 최소화하고, 이를 효율적으로 관리하여 효율을 최대한 높이기 위해 노력한다(화면을 그리는데 너무 많은 processing power를 사용한다면 일은 언제하는가? ㅡ_ㅡ;;;)
처음에는 세세한 부분까지 변화된 영역을 검출하여 처리하려 했다가 알고리즘이 너무 복잡해지는 바람에 생략했었다. 뭐 사실 대충만든 프로토타입이 잘 작동해준 원인도 있지만... 윈도우 라이브러리를 계속 개발하다보니 속도가 점점 떨어지는 것이 눈에 보여서 어쩔 수 없이 넣게 되었다.
윈도우 라이브러리에서 효율을 높이기위해 사용하는 방식은 3가지다.
더블 버퍼링 : 메인 메모리를 할당하여 윈도우를 그리고 완전히 그려진 뒤에 비디오 메모리로 고속 전송한다. 고속 전송 함수는 간단히 어셈으로 짰다. ㅡ_ㅡ;;; 속도가 의심스러운 사람도 있을텐데, memcpy 함수보다 2배는 빠르니 믿어도 된다.(프로파일링해서 나온 결과다. 프로파일러에 대해서는 22 타이머(Timer)를 이용한 프로파일러(Profiler) 만들기 를 참고하자.)
일괄 그리기 : 윈도우가 변화되었다고 해서 버퍼에 직접 그리지 않는다. 윈도우를 다시 그려야 한다는 플래그와 영역만을 윈도우 매니저에게 알리고 윈도우 매니져는 메시지 처리가 끝난 뒤 일괄적으로 윈도우를 다시 그린다.
그리기 영역 설정 : 조그만 윈도우가 변했다고 전체를 다시 그리는건 문제가 있다. 따라서 그려야 할 영역이 있을 때 이 영역들을 단순한 사각형 더하기로 계산하여 해당 영역에 있는 윈도우만 다시 그리도록 한다. 간단하지만 의외로 효율이 괜찮다.
이제 하나하나에 대해서 자세히 알아보자.
4.1 더블 버퍼링(Double Buffering)
게임을 개발한 사람 or 윈도우 그림판 같은 프로그램을 개발해본 사람이라면 더블 버퍼링을 모르는 사람이 없을 것이다. 다행이도 NDS에는 비디오 메모리가 많기 때문에 비디오 메모리를 사용한 더블 버퍼링을 사용할 수 있지만, 조사해본 결과 한가지가 부족했다. 2개의 화면이므로 최소 4개의 메모리가 필요한데, 실제로 그림을 표시하기위해 사용 가능한 메모리는 3개 였다. ㅡ_ㅜ...
물론 화면 하나는 비디오 메모리를 사용해서 비디오 스위칭을 이용한 더블 버퍼링을 사용하고, 나머지 하나는 메모리 전송을 이용한 더블버퍼링을 사용하면 되지만 윈도우 라이브러리가 복잡해지는 문제가 있어서 둘다 메인 메모리를 이용한 더블 버퍼링을 하기로 결정했다.
참고. NDS 속도에 대한 몇가지 테스트 문서를 참고하면 NDS에서 memcpy, for를 사용한 메모리 전송에 대한 결과를 알 수 있다. 실제 memcpy를 사용하면 7 ~ 8 ms정도가 걸리는데 자작한 고속 전송 함수를 사용하면 4 ms정도에 출력할 수 있다.
4.2 메시지 처리(Message Processing) 및 일괄 그리기(Batch Drawing)
일괄로 그리기란 윈도우가 변경되는 시점에 그래픽 버퍼에 그리는 것이 아니라 변경된 윈도우에 상태를 저장한 뒤에 나중에 일괄적으로 모든 윈도우를 Z-Order의 역순으로 그려나가는 것을 말한다. 이렇게 한 이유는 NDS의 빈약한(??) 프로세싱 능력과 작은 메모리 때문에 윈도우들을 개별 태스크 or 개별 메시지 큐로 구분하지 못했기 때문이다.
그렇다면 어떻게 멀티 윈도우를 구현한 것일까? 약간의 꼼수를 이용했다. 아래를 보자.
4.2.1 메시지 처리(Message Processing)
방법은 간단하다. 원래 윈도우 시스템이 개별 윈도우의 메시지 큐에 메시지를 던져줘서 처리하던 방식이라면 내가 만든 윈도우 시스템은 메시지를 Z-Order 순서로 전달하는 방식이라 할 수 있다. 다시 말하면 하나의 메시지를 전체 윈도우가 돌아가면서 사용한다는 말인데, 별도의 처리가 없으면 메시지가 전체에게 전달되므로 동작에 문제가 생긴다.
따라서 이를 규제하는 방식이 윈도우 영역 및 메시지 종류, 그리고 윈도우 핸들을 이용해서 비교하여 자신의 핸들 or 자신의 윈도우 영역에서 발생한 것이면 메시지의 전달을 더이상 하지 않고 삭제함으로써 무한 메시지 전달을 막았다.
아래는 이것을 그림으로 나타낸 것이다.
<메시지 처리>
좌측 상단의 4번 윈도우 자리에 클릭이 일어났을 때 해당 메시지가 어떻게 전달되고 어디서 처리가 완료되는가 하는 그림이다. 메시지는 Z-Order 순서로 전달되다가 4번 윈도우의 처리 루틴에서 자신의 영역에서 발생한 메시지 임을 깨닿고 메시지 처리를 완료한 후 Background 윈도우로 전달하지 않았다.
위와 같은 알고리즘을 사용하기위한 전제조건은 무엇일까?
한가지는 어플리케이션 프로그래머가 OnMessage() 함수에서 적당히 잘 처리한 다음, 메시지를 다른 윈도우에 전달해줘야 한다는 점이다. 위와 같은 상황에서 1번 윈도우에서 메시지를 더이상 뒤로 전달하지 않는다면 다른 윈도우는 메시지를 받지 못하기 때문에 굶주림 상태에 빠진다. 하지만 너무 잘 전달해서 자신의 메시지를 뒤로 전달하면 동작에 문제가 발생한다. 신중한 처리가 필요하다.
다른 한가지는 윈도우는 메시지 처리 루틴에서 너무 시간을 끌면 뒤에 윈도우에게 메시지가 전달되는 시간이 더 걸리기 때문에 시간에 민감한 프로그램을 작성할 때 주의해야 한다는 점이다. 특히 윈도우를 다시 그리거나 하는 작업은 시간이 많이 걸리기 때문에 OnPaint() 같은 함수를 직접 호출해서 버퍼에 다시 그리는 일은 하지 말고RedrawWindow() 함수를 사용해서 윈도우 매니져가 일괄로 화면을 그리도록 해야 한다.
4.2.2 일괄 그리기(Batch Drawing)
NDS의 빈약한 처리 능력 때문에 일괄 그리기를 선택했다고 앞서 이야기 했다. 일괄 그리기를 하면 좋은 점이 뭘까?
첫째는 윈도우가 겹겹이 쌓여있을 때 끼어있는 윈도우의 "일부"를 그리는 문제를 간단히 처리할 수 있다. 위의 <메시지 처리>그림에서 2번 윈도우가 갱신되었다면 실제로 그려야하는 영역은 2번 윈도우의 영역에서 1번 윈도우의 영역을 뺀 부분이다. 이 시점에서 버퍼를 바로 갱신하려면 2번 윈도우를 그리고 1번 윈도우를 그 위에 다시 그리면 된다. 이러한 단순한 경우라면 괜찮지만 만약 윈도우가 겹겹이 쌓여있고 다들 일부분이 보이는 상태인데, 그 일부분만 보이는 화면이 계속 갱신되는 상황이라면??? 윈도우 위치에 따른 영역 계산 + 일부분 Draw 등등의 작업을 수행해야 한다. 알고리즘이 점점 복잡해지고 계산량도 점점 늘어난다. 실제로 이러한 방식으로 이전 KKAMAGUI OS를 개발했었는데, 계산량이 많아지다보니 오히려 성능이 그리 좋지 않았다. 그래서 고민한 끝에 일괄 그리기를 이용하여 처리했다.
둘째는 윈도우 메시지를 전달하는 시간을 줄일 수 있다. 현재의 윈도우 라이브러리는 윈도우가 다른 윈도우에게 메시지를 전달하는 구조로 되어있기 때문에, 시간이 많이 걸리는 작업을 메시지 처리 루틴에서 하게되면 치명적이다. 윈도우를 다시 그리는 역할을 윈도우 매니져로 넘김으로써 메시지를 좀 더 빠르게 전달할 수 있고, 윈도우를 계속해서 다시 그리는 일을 하지 않음으로써 응답속도의 향상을 노릴 수 있다.
4.3 그리는 영역 설정
위에서 일괄 그리기의 장점을 이야기 했다. 그렇다고 무턱대고 화면 전체를 다시 그려서 전송하면 효율이 좋을까? 전체 영역 중에 작은 윈도우 일부만 변했는데, 윈도우 전체를 다시 그리는 것은 비효율적이다. 따라서 그리는 영역의 범위를 정하여 해당 범위에 들어있는 윈도우만 다시 그린 후, 그리는 영역이 현재 윈도우 내부에 포함된다면 더이상 그리지 않도록 한다. 이렇게 하면 조금이나마 효율을 높일 수 있다.
그리는 영역의 설정은 간단한 사각형 더하기로 해결했는데, 알고리즘은 아주 간단하다. 아래의 그림과 같이 각 영역의 Min/Max값을 얻어서 해당 영역 안을 그리도록 했다.
<그리는 영역 설정>
위의 그림과 같이 1번 윈도우와 2번 윈도우가 변했다면 두 윈도우의 범위를 Min/Max한 붉은색 사각형이 다시 그려질 영역이 된다. 따라서 이 영역으로 Z-Order를 따라가면서 이 영역을 포함한 윈도우를 찾게되고 해당 윈도우를 그리는 것이다. 위에서 보면 1번 윈도우 및 2번 윈도우 그리고 3번 윈도우가 위 영역에 포함되므로 그려야 된다. 하지만 3번 윈도우 영역이 그리는 영역을 완전히 포함하므로 Background 윈도우는 다시 그릴 필요가 없으므로 3번 윈도우까지만 그린다.
위와 같이 간단한 알고리즘을 이용해서 다시 그릴 영역을 어느정도 줄일 수 있다. 효율은... 그저 그렇다.. ㅡ_ㅡ;;; 하지만 안하는 것 보다는 좋다.
5.윈도우 관련 클래스 구현
5.1 CWnd 구현
CWnd 클래스는 아래와 같이 정의되어있다.
// 기본 윈도우 클래스
class CWnd
{
protected:
RECT m_stWindowRect;
bool m_bShow;
WORD* m_wDisplayAddr;
char m_vcTitle[ 33 ];
// 윈도우에 그리기위해 넣음
CWindowDC m_clWindowDC;
// ZOrder를 위해 넣음
CWnd* m_pclZOrderNext;
CWnd* m_pclParent;
//--------------------------------------------------------------------------
// 아래는 Window Manager 관련 또는 기타 함수들
// 쓰는 쪽에서는 아래의 함수를 건드릴 필요 없음
//--------------------------------------------------------------------------
void AddWindowToManager( CWnd* pclWnd );
void DeleteWindowFromManager( CWnd* pclWnd );
RECT CalcXButtonPosizion( void );
CWindowManager* GetWindowManagerPtr( void );
//--------------------------------------------------------------------------
// 아래는 오버로딩할 수 있는 함수 목록
// 아래의 함수만 상속받은 클래스에서 필요하면 재정의하자.
//-------------------------------------------------------------------------- virtual void OnEraseBkgnd( CDC* pclDC );
virtual bool OnMessage( PMESSAGE pstMessage );
public:
CWnd();
virtual ~CWnd();
//--------------------------------------------------------------------------
// 아래는 윈도우 메니져에서 사용할 용도로 설정된 함수들
// 상속을 받아서 사용하는 경우라면 쓸 필요 없다.
//--------------------------------------------------------------------------
void RedrawZOrderBehindWindow( RECT stRect );
void SetZOrderNext( CWnd* pclWindow );
CWnd* GetZOrderNext( void );
//--------------------------------------------------------------------------
// 아래는 윈도우 사용에 관련된 함수들
// 상속을 받아서 사용하는 경우라면 쓸 필요 없다.
//-------------------------------------------------------------------------- void Create( void* pvAddr, RECT stRect, char* pcName, bool bShow,
CWnd* pclParent );
void SetDisplayAddr( void* pvAddr );
void* GetDisplayAddr( void );
bool MoveWindow( int iX1, int iY1, int iX2, int iY2 );
bool MoveWindow( RECT stRect );
void ShowWindow( bool bShow );
void RedrawWindow( void );
void SetWindowText( char* pcTitle );
char* GetWindowText( void );
RECT GetWindowRect( void );
RECT GetClientRect( void );
void DestroyWindow( void );
bool IsInWindowRect( int iX, int iY );
bool IsInWindowRect( RECT stRect );
bool IsInXButtonRect( int iX, int iY );
bool IsInTitleBarRect( int iX, int iY );
bool IsXButtonDownMessage( PMESSAGE pstMessage );
//--------------------------------------------------------------------------
// 아래는 오버로딩할 수 있는 함수 목록
// 아래의 함수만 상속받은 클래스에서 필요하면 재정의하자.
//-------------------------------------------------------------------------- virtual void OnPaint( void );
virtual void OnDestroy( void );
virtual void DrawXButton( CDC* pclDC );
virtual void DrawTitleBar( CDC* pclDC );};
최대한 MFC 클래스와 비슷하게 구현하려 노력했다. 낯익은 함수들이 반가울 것이다. CWnd 클래스는 모든 윈도우의 기본이 되는 윈도우로써 윈도우 기본 프레임을 그려주는 역할과 윈도우 메니져의 일부로써 자료구조 역할을 수행한다. 하지만 사실 거의 Draw 쪽 함수만 처리가 되어있을 뿐, 메시지 처리라든지 윈도우 이동에 관한 부분은 거의 가지고 있지 않다.
정말 "기본" 적인 윈도우 구성에 대한 정보만 가지고 있다.
5.2 CSkinWnd 구현
CSkinWnd는 그래픽 파일을 넣어주면 해당 그래픽 파일을 BitBlt 해서 윈도우에 스킨을 씌우는 프로그램이다. 스킨을 씌우는 방법은 향후 Library Tutorial을 다루면서 설명할 것이다. 그래픽 포맷인 KNG(KKAMAGUI NDS Graphic)에 대해서는 추후에 다룰 예정이다. 뭐 사실 RGB555 포맷으로 바꿔주는 것 밖에는 없다.
class CSkinWnd : public CWnd
{
private:
BYTE* m_pbKNGBuffer;
윈도우 프로그래밍할 때 다이얼로그에 스킨을 씌워본 사람이라면 낯이 익은 코드일 것이다. 그래픽 포맷과 이미지 크기의 제한만 제외하면 거의 윈도우 프로그램하는 것과 같다.
5.3 CListWnd 구현
CListWnd는 윈도우의 List Box와 비슷한 기능을 하는 클래스다. 각 라인에 항목을 표시하고 선택된 항목을 표시해주는 기능을 한다. 부가적으로 Call Back을 지원하여 그려질 라인별로 그리는 방식에 대한 처리나 선택되었을 때 처리에 대한 구현도 표함하고 있다. CListWnd 자체는 Text String 기반으로 동작하지만, 구조체와 같은 정보를 삽입하고 Call Back만 잘 처리해 준다면 기타 여러가지 자료 구조도 표시할 수 있다. 이 부분에 대해서는 홈브루 중에 File Explorer를 참고하면 된다.
class CListWnd : public CWnd
{
protected:
CList m_clContentList;
int m_iTouchX;
int m_iTouchY;
int m_iLastX;
int m_iLastY;
int m_iViewItemNumber;
bool m_bSelectMode;
bool m_bLButtonDown;
private:
bool ProcessScrollMode( PMESSAGE pstMessage );
bool ProcessSelectMode( PMESSAGE pstMessage );
bool ProcessTitleBarMessage( int iX, int iY );
//--------------------------------------------------------------------------
// 리스트에 Item을 다른 방식으로 표시하고 싶으면 아래의 함수를 오버로딩해서
// 원하는 대로 그리면 된다.
// 한 라인 라인을 그릴때 호출되는 Call back 함수
//-------------------------------------------------------------------------- virtual void RenderContent( RECT stDrawRect, CDC* pclDC, void* pvContent,
int iItemIndex );
// 리스트의 Item이 선택되었을 때 호출되는 함수
virtual void OnSelectCursorDown( void );
virtual void OnSelectCursorUp( void );};
5.4 CChildWnd 구현
CChildWnd는 CWnd와 같으나 다른 점은 Window Manager에 등록이 되지 않기 때문에, 메시지에 대한 처리는 부모 윈도우가 Child Window에 넣어줘야 한다는 점이다. 이렇게 하면 편리한 점은 자식 윈도우를 부모 윈도우에 다 맡김으로써 Window Manager에 로드를 줄일 수 있고 Z-Order의 처리가 아주 간단하기 때문이다.
// Child Wnd는 윈도우 매니저에 속하지 않는다는 점 말고는 CWnd와 동일하다.
class CChildWnd : public CWnd
{
private:
BYTE* m_pbKNGBuffer;
CDC는 윈도우의 대표적인 그래픽 관련 클래스이다. CPaintDC와 CWindowDC도 구현되어있으며, 차의점은 CPaintDC 같은 경우 Title Bar의 높이를 자동으로 빼서 계산해 준다는 점이다.
// 윈도우 관련 DC를 처리하는 Base Class
// 하는일 없음
class CDC
{
protected:
CWnd* m_pclWindow;
CBitmap* m_pclBitmap;
int m_iLastX;
int m_iLastY;
int m_iTitleHeight;
protected:
void DrawRect( PRECT pstRect, WORD wColor, bool bFill );
WORD* GetBufferAddr();
RECT GetBufferRect();
bool MoveTo( int iX, int iY );
bool LineTo( int iX, int iY, WORD wColor );
void FillRect( PRECT pstRect, WORD wColor );
void FillRect( int iX1, int iY1, int iX2, int iY2, WORD wColor );
void FrameRect( PRECT pstRect, WORD wColor );
void FrameRect( int iX1, int iY1, int iX2, int iY2, WORD wColor );
void TextOut( int iX, int iY, char* pcString, WORD wForeColor,
WORD wBackColor );
void BitBlt( int iDstX, int iDstY, int iWidth, int iHeight,
CDC* pclDC, int iSrcX, int iSrcY, int iMode );
void SetPixel( PPOINT pstPoint, WORD wColor );};
// Paint DC
class CPaintDC : public CDC
{
public:
CPaintDC( CWnd* pclWindow );
~CPaintDC();
};
// Window DC
class CWindowDC : public CDC
{
public:
CWindowDC();
CWindowDC( CWnd* pclWindow );
~CWindowDC();
};
5.6 CBitmap 구현
CBitmap은 간단히 Bitmap 파일을 관리하는 클래스로 단순한 버퍼 역할을 한다. 지금은 KNG 파일 포맷을 사용하도록 되어있는데 아래에 KNG 구조체의 정보를 앞에 포함하고 내부 데이터는 RGB555 포맷을 사용하는 아주 간단한 구조의 파일이다.
#define KNG_SIGNATURE "KNG"
// KKAMAGUI NDS GRAPHIC 파일의 헤더 구조체
typedef struct kkamaguiNdsGraphicStruct
{
char vcSignature[ 3 ]; // KNG 설정
BYTE bVersion; // 이미지 포맷 버전, 0x00 설정
int iWidth; // 이미지 넓이
int iHeight; // 이미지 높이
} KNG, * PKNG;
// Bitmap 관련 클래스
class CBitmap
{
private:
WORD* m_pwBitmap;
SIZE m_stSize;
public:
CBitmap();
~CBitmap();
WORD* GetBufferAddr();
RECT GetBitmapRect();
bool LoadKNG( BYTE* pbKNG );};
5.7 비디오 메모리 고속 전송 구현
ARM 레지스터를 최대한 사용해서 전송하는 방식을 사용한다. 나름 빠르다.
// Frame Buffer 통짜를 빠르게 LCD로 복사해 주는 함수
// 256 * 192 * 2 = 98304Byte
// r12 => Loop Count r2~r11 => 40Byte
// 98304Byte = 40 * 2457(Loop Count) + 24(Remain)
FastFrameBufferDump:
push { r4 - r12 }
// 2457 값을 바로 넣을 수 없어서 쉬프트 연산으로 넣는다.
mov r4, #153
mov r5, #9
add r12, r5, r4, LSL #4
/**
두 속성을 교환
*/
void SwapCodes( BYTE* bC1, BYTE* bC2 )
{
BYTE bTemp;
bTemp = *bC1;
*bC1 = *bC2;
*bC2 = bTemp;
}
/**
Line을 클립핑한다.
*/
void ClipLines( RECT stClippingRect, void* pvAddr, POINT stP1, POINT stP2,
WORD wColor )
{
BYTE bCode1, bCode2;
int iDone = FALSE, iDraw = FALSE;
float m;
if( ACCEPT( bCode1, bCode2 ) )
{
//두점 다 안에 있으면 끝
iDone = TRUE;
iDraw = TRUE;
}
else
{
if( REJECT( bCode1, bCode2 ) )
{
//두점다 화면 밖이면 끝
iDone = TRUE;
}
else
{
// 만약 점 1이 안에 있으면 점 2를 사용한다.
if( INSIDE( bCode1 ) )
{
//p1이 중앙에 있으면 p2와 교환
SwapPoints( &stP1, &stP2 );
SwapCodes( &bCode1, &bCode2 );
}
//=============================================================================
//
// Bresenham 알고리즘을 이용한 클립핑
//
//=============================================================================
/**
라인을 그린다.
*/void BresenhamLine( void* pvAddr, int iX1, int iY1, int iX2, int iY2,
unsigned short usColor )
{
int dy = iY2 - iY1;
int dx = iX2 - iX1;
int stepx, stepy;
unsigned short* pusStartAddr = ( unsigned short* ) pvAddr;
/**
Box를 그린다.
*/
void DrawBox( RECT stClippingRect, void* pvAddr, int iX1, int iY1, int iX2,
int iY2, unsigned short usColor, bool bFill )
{
int j;
int iTemp;
int iCount;
WORD* pwAddr = ( WORD* ) pvAddr;
WORD* pwTemp;
// 속을 체우게 되어있으면 영역을 체운다.
if( bFill == true )
{
// 영역 클립핑을 한다.
MinMaxClipping( stClippingRect, &iX1, &iY1 );
MinMaxClipping( stClippingRect, &iX2, &iY2 );
iCount = iX2 - iX1 + 1;
iTemp = iY1 * SCREEN_WIDTH + iX1;