본문 바로가기

Programming/Linux_Kernel

frame buffer 이야기(6) (7)

원문 :
http://kelp.or.kr/korweblog/stories.php?story=02/11/12/3234889
http://kelp.or.kr/korweblog/stories.php?story=02/11/23/8053717

글쓴이 : holelee (2002년 11월 12일 오후 06:27) 읽은수: 8,786 [ 임베디드강좌/이규명 인쇄용 페이지 ]
== 시작하기에 앞서
이 글에 있는 모든 내용은 글쓴이가 가지고 있는 ATI mach 64(2MB) 그래픽 카드가 달려있고 RedHat 8.0이 설치된 PC(또는 Permedia2(8MB) 그래픽 카드가 달려 있는 RedHat 7.3이 설치된 PC)에서 제대로 동작하지만 이 글에 있는 모든 내용이 정확하다고 말씀 드릴 수 없습니다. 이 글에 있는 내용을 따라 했을 때 혹 생길 지 모르는 모든 문제에 대해서 글쓴이는 책임을 지지 않습니다. 글의 내용 중에 잘못된 내용이 있거나 질문할 것이 있으면 위쪽의 “holelee”를 누르고 메일을 보내기 바랍니다.

== 시작
lseek/write를 이용하는 pixel 찍기는 우선 코딩상 귀찮습니다. lseek, write 시스템 호출이 에러를 리턴하는지 체크해야 하고 그에 따른 error 처리 루틴도 만들어야 하죠.(물론 mmap이 리턴한 address로 pixel을 찍거나 읽을 때도 잘못하면 에러가 발생할 수 있습니다. 그러나 에러가 Segmentation Fault나 Bus Error로 프로그램이 그냥 죽을 겁니다.) 그러나 그런 것만 가지고는 “다소 무식한(?)”이라는 표현이 조금 과한 표현입니다. 이제 왜 “다소 무식한(?)”지 알아보기 위해서 벤치마크를 해보도록 하겠습니다.

== 벤치마크 시스템
벤치마크를 담당할 시스템은 본인이 집에 가지고 있는 linux machine입니다. 컴퓨터 사양은 다음과 같습니다.
OS : RedHat 8.0(default로 깔리는 kernel)
CPU : PentiumPro 200Mhz
Graphic Card : ATI mach 64 CT 2MB
Frame buffer driver : atyfb

== 벤치마크 프로그램 : 다시 나온 random 네모 그리기
random 네모 그리기를 mmap을 이용하여 다시 작성했습니다. 이번에 작성된 프로그램을 fbrandrect2라고 부르고 전에 lseek/write를 이용하여 작성된 프로그램을 fbrandrect1이라고 부르겠습니다. 소스의 골격이나 구성은 fbrandrect1과 fbrandrect2 모두 동일합니다. 다만 무한 루프를 돌면 곤란하므로 두 프로그램 모두 그냥 1000개의 네모만 그리고 프로그램이 종료되도록 하였습니다. 그리고 rand 함수가 똑 같은 값을 리턴해야 두 프로그램의 동작이 똑같아 지므로 srand 함수를 이용하여 rand 함수의 seed도 주었습니다.
===============================
/*
* fbrandrect2.c : Frame buffer draw random rectangular example unsing mmap
*
* Copyright(C) 2002 holelee
*
*/

#include <stdio.h>
#include <stdlib.h> /* for exit */
#include <unistd.h> /* for open/close .. */
#include <fcntl.h> /* for O_RDONLY */
#include <sys/ioctl.h> /* for ioctl */
#include <sys/mman.h>
#include <linux/fb.h> /* for fb_var_screeninfo, FBIOGET_VSCREENINFO */

#define FBDEVFILE "/dev/fb"

int main()
{
int fbfd;
int ret;
struct fb_var_screeninfo fbvar;
unsigned short *pfbdata;
int count = 1000;

fbfd = open(FBDEVFILE, O_RDWR);
if(fbfd < 0)
{
perror("fbdev open");
exit(1);
}

ret = ioctl(fbfd, FBIOGET_VSCREENINFO, &fbvar);
if(ret < 0)
{
perror("fbdev ioctl");
exit(1);
}

if(fbvar.bits_per_pixel != 16)
{
fprintf(stderr, "bpp is not 16\n");
exit(1);
}

pfbdata = (unsigned short *)
mmap(0, fbvar.xres*fbvar.yres*16/8,
PROT_READ|PROT_WRITE, MAP_SHARED, fbfd, 0);

if((unsigned)pfbdata == (unsigned)-1)
{
perror("fbdev mmap");
exit(1);
}

srand(1); /* seed for rand */
while(0 < count--)
{
int xpos1, ypos1;
int xpos2, ypos2;
int offset;
int rpixel;
int t, tt;

/* random number between 0 and xres */
xpos1 = (int)((fbvar.xres*1.0*rand())/(RAND_MAX+1.0));
xpos2 = (int)((fbvar.xres*1.0*rand())/(RAND_MAX+1.0));

/* random number between 0 and yres */
ypos1 = (int)((fbvar.yres*1.0*rand())/(RAND_MAX+1.0));
ypos2 = (int)((fbvar.yres*1.0*rand())/(RAND_MAX+1.0));

if(xpos1 > xpos2)
{
t = xpos1;
xpos1 = xpos2;
xpos2 = t;
}

if(ypos1 > ypos2)
{
t = ypos1;
ypos1 = ypos2;
ypos2 = t;
}

/* random number between 0 and 65535(2^16-1) */
rpixel = (int)(65536.0*rand()/(RAND_MAX+1.0));

for(t = ypos1; t <= ypos2; t++)
{
offset = t*fbvar.xres;

for(tt = xpos1; tt <= xpos2; tt++)
*(pfbdata+offset+tt) = rpixel;;
}
}

munmap(pfbdata, fbvar.xres*fbvar.yres*16/8);
close(fbfd);
exit(0);
return 0;
}

===============================
소스는 굳이 설명을 하지 않겠습니다.

== 벤치 마크 결과
fbrandrect1, fbrandrect2 모두 gcc의 “–O2” 옵션을 주고 컴파일 하였고 time 명령어로 수행되는 시간을 측정해 보았습니다.

fbrandrect2
real 0m4.736s
user 0m4.730s
sys 0m0.006s
fbrandrect1
real 3m53.784s
user 0m34.213s
sys 3m19.572s

어떻습니까? 차이가 보입니까? fbrandrect2는 5초도 되지 않은 시간에 끝이 났고, fbrandrect1은 4분 가까이나 걸렸네요. 눈으로 네모가 그려지는 모습을 보더라도 확연히 성능 차이를 확인할 수 있습니다. 이제 왜 무식한 방법이라고 얘기했는지 이해할 수 있겠죠?

== fbrandrect1은 왜 성능이 떨어지는가?
Frame buffer 이야기와는 크게 상관은 없지만 fbrandrect1이 성능이 떨어지는 이유에 대해서 짤막하게 살펴 보겠습니다. 그 이유는 lseek/write 시스템 호출을 많이 했기 때문입니다. 시스템 호출(system call)은 software interrupt라는 instruction을 수행하게 되어 있습니다.(instruction 이름은 CPU architecture마다 다르게 부르고 instruction 자체도 CPU architecture마다 다르죠.) 그 instruction이 수행되면 CPU는 interrupt를 받은 경우와 동일하게 동작합니다. Interrupt를 받으면 우선 CPU안에 있는 레지스터를 지정된 위치에 저장하고 어떤 이유로 interrupt가 걸렸는지 살펴보고 그것에 따라서 kernel 내부에서 그 interrupt를 서비스하는 함수를 호출합니다. write 시스템 호출을 했다면 linux에서 서비스 함수는 sys_write일 겁니다. 이 sys_write는 file descriptor를 살펴서 그 파일이 frame buffer device인지 판단하고 다시 그 frame buffer driver의 write함수를 호출하게 됩니다. 그 함수의 수행이 끝나게 되면 schedule을 다시 해야 할 필요가 있는지 계산해보고 그렇지 않다면 다시 저장된 레지스터를 복원해 놓고 user application으로 리턴합니다. User application으로 돌아오게 되면 CPU의 cache가 비어있으므로 cache miss가 발생하여 DRAM으로부터 cache의 내용을 다시 채우게 됩니다. fbrandrect1이 한 pixel을 찍을 때마다 write 시스템 호출을 하므로 이 모든 일을 반복하게 됩니다. lseek 시스템 호출도 마찬가지죠. 따라서 성능은 엄청 떨어지죠.

== 마치며
다음 글의 내용은 아직 미정입니다. 다음 글을 적을 때까지는 시간이 좀 걸릴 것 같습니다.






== 시작하기에 앞서
이 글에 있는 모든 내용은 글쓴이가 가지고 있는 ATI mach 64(2MB) 그래픽 카드가 달려있고 RedHat 8.0이 설치된 PC(또는 Permedia2(8MB) 그래픽 카드가 달려 있는 RedHat 7.3이 설치된 PC)에서 제대로 동작하지만 이 글에 있는 모든 내용이 정확하다고 말씀 드릴 수 없습니다. 이 글에 있는 내용을 따라 했을 때 혹 생길 지 모르는 모든 문제에 대해서 글쓴이는 책임을 지지 않습니다. 글의 내용 중에 잘못된 내용이 있거나 질문할 것이 있으면 위쪽의 “holelee”를 누르고 메일을 보내기 바랍니다.


== 시작
pixel 찍기(쓰기)와 pixel 읽기를 알아보았으므로 사실 frame buffer에 대한 프로그래밍은 다 알아봤다고 볼 수 있습니다. 적어도 지금까지 테스트한 16bpp 환경에서는 말이죠. 그런데 random pixel 찍기나 random 네모 그리기는 그야말로 데모 수준이지 그렇게 쓸모가 있는 프로그램은 아니죠. 또한 원하는 색깔의 pixel이 제대로 나타나는지도 정확하게 알 수 없죠. 그래서 지금까지 알아본 내용을 바탕으로 작성된 bmp 파일 display 프로그램을 소개합니다. 사실 bmp 파일은 MS와 IBM이 만들었으므로 linux와 친하다고 볼 수는 없습니다.(상식 : GNU/linux와 가장 친하지 않은 그래픽 파일형식은 GIF죠.) 그래도 굳이 bmp 파일형식을 사용한 이유는 파일 구조와 압축 방식이 비교적 간단하고 쉽게 구할 수 있는 이미지 파일형식이기 때문입니다.


== fbbmp 프로그램

= 사용법
그냥 압축 풀고 make로 build하면 fbbmp라는 프로그램이 만들어 집니다. bmp 파일 이름을 인자로하여 수행하면 화면에 그 파일을 display합니다.(당연히 그 전에 frame buffer driver가 제대로 올라가 있어야 하고 16bpp로 셋팅되어 있어야 합니다.) 네 개의 bmp 파일이 포함되어 있으니 테스트하면 됩니다.

= 지원 환경
(1) 16bpp mmap이 가능한 frame buffer
=> mmap이 지원되지 않으면 lseek/write로 pixel을 찍도록 코딩을 할 수도 그런 경우가 많지도 않고 귀찮으므로 그냥 작성하였습니다.
(2) little endian machine
=> bmp 파일 형식이 little endian으로 되어 있는데 big endian machine까지 지원하도록 만들기 위해서는 byte를 swap해야 하는데 귀찮아서 그냥 두었습니다.

= 지원하는 bmp 파일 형식
BI_RLE4 압축을 한 4bpp 파일을 제외한 모든 bmp 파일을 지원합니다.(코딩하기 귀찮고 어짜피 테스트도 못하기 때문에 BI_RLE4 4bpp를 제외했습니다.)

= 테스트된 bmp 파일 형식
(1) 1bpp 파일 : kelp_logo_1bpp.bmp
(2) 4bpp(BI_RGB : 즉 압축되지 않은) 파일 : kelp_logo_4bpp.bmp
(3) 8bpp(BI_RGB : 즉 압축되지 않은) 파일 : tux_resize_8bpp.bmp
(4) 24bpp 파일 : lixgreen_24bpp.bmp
지원하는 모든 bmp 파일 형식에 대해서 테스트하고 싶었으나 bmp 파일을 구할 수 없어서 어쩔 수 없이 4가지 형식에 대해서만 테스트를 진행했습니다. 포함되어 있는 모든 bmp 파일은 인터넷을 돌아다니면서 몇 가지 골라온 것을 적당한 툴로 변환한 것입니다.
당연히 다른 형식에서 바르게 작동하는지 검사하지 못했으므로 버그가 있을 가능성이 큽니다. 혹 디스플레이가 되지 않는 bmp 파일이 있으면 메일을 보내 주길 바랍니다.

= 약간의 문제
급히 작성한 소스코드라서 error처리가 완전하지 않습니다. 따라서 손상된 bmp파일을 입력하면 어떻게 될지 모르겠습니다.


== 소스코드 설명
소스코드 설명은 없습니다. 소스코드를 설명하려면 bmp 파일 구조부터 설명해야 하는데 그것은 “frame buffer 이야기”라는 제목과 어울리지 않아서 그만 둡니다. bmp 파일 구조에 대해서는 reference에 나와있는 사이트에서 있는 관련 문서에 자세히 나와 있습니다. 관심있는 사람은 bmp 파일형식에 대해 알아본 후 소스코드를 직접 살펴보길 바랍니다.


== 중대한 에러 발견
모든 프로그래밍을 끝내고 Permedia2 frame buffer에 디스플레이를 해보고 잘된다고 생각했습니다. 집에 가서 글을 적으며 ATI mach64 frame buffer에서 테스트를 해보았더니 색깔이 깨지며 원하는 색상이 나오지 않는 문제가 발견되었습니다. 그래서 어떤 문제인지 알아보았더니 16bit pixel encoding 문제가 있었습니다.
“Frame buffer 이야기(3)”에서 16bpp의 pixel은 Red 5bit, Green 6bit, Blue 5bit이 MSB에서부터 순서대로 붙어 있다고 말했습니다. 이렇게 구성된 16bpp를 보통은 high color라고 부릅니다. 6만5천(정확하게는 2^16=65536) color를 표현할 수 있죠. 하지만 특별한 시스템에서는 16bpp에서의 pixel이 그렇게 표현되지 않고 다음과 같이 표현되는 경우도 있습니다.
============================================
| NotUsed(1) | Red(5) | Green(5) | Blue(5) |
============================================
즉 최상위비트(MSB)는 사용하지 않고 Red, Green, Blue가 각각 5bit씩 사용됩니다. 어찌보면 15bpp라고 할 수도 있겠지만 결국 1 pixel이 16bit로 표현되므로 16bpp입니다. 이런 경우 당연히 6만5천 color를 표현하지 못하고 3만2천(정확하게는 2^15=32768) color를 표현할 수 있습니다.
일반적인 PC 시스템에서는 보통 16bpp라고 하면 R(5), G(6), B(5)로 coding된 경우를 일컫는 것으로 알고 있어서 그렇게 계속 이야기를 진행하고 나중에 다른 bpp에 관한 이야기를 할 때 이 문제를 잠시만 언급하려고 했습니다. 그런데 ATI mach64 frame buffer driver(RedHat 8.0)상의 16bpp는 R(5),G(5),B(5)인 것으로 나타났습니다. 그래서 색상이 깨진 것이죠. 이 문제를 해결할 수 있는 방법을 언급하고 넘어가도록 하겠습니다.

== 어떻게 구분하는가?
문제점을 해결하기 위해서 가장 간단한 방법은 프로그램을 두 가지 버전으로 만들어보고 잘 되는 것을 사용하는 것이겠지만, 이런 식의 해결은 많은 사람들이 좋아하는 방법이 아니죠. 그럼 하나의 프로그램이 두 가지 pixel encoding 형식을 모두 지원하도록 만드는 방법이 좋은데 어떻게 frame buffer driver가 어떤 pixel encoding 형식을 사용하는 지를 알아낼 수 있는가가 핵심이 되겠습니다.
실제로 알아내는 방법은 아주 간단합니다. ioctl(FBIOGET_VSCREENINFO)를 통해 얻은 fb_var_screeninfo 구조체에 모든 내용이 들어 있습니다. 그 구조체 안에 있는 bits_per_pixel이라는 member는 두 가지 pixel encoding 방식 모두 16의 값을 가지므로 구분을 위해 사용할 수 없습니다. 구조체 member중에 struct fb_bitfield type의 red, green, blue, transp라고 하는 것들이 있는데 그것을 통해서 알 수 있습니다.(/usr/include/linux/fb.h나 직접 kernel 소스의 include/linux/fb.h를 살펴보길 바랍니다.) transp의 경우는 나중에 32bpp에 관한 설명을 할 때 설명하도록 하고 지금은 무시하기로 하겠습니다.(보통 16bpp상에는 투명도는 잘 사용되지 않으므로. 이 가정도 잘못될 수 있기는 하진만요.)
struct fb_bitfield는 다음과 같습니다.
struct fb_bitfield {
__u32 offset; /* beginning of bitfield */
__u32 length; /* length of bitfield */
__u32 msb_right; /* != 0 : Most significant bit is right*/
};
struct fb_bitfield red가 { 11, 5, 0 }의 값을 가진다면 이 의미는 pixel encoding에서 색상 Red가 11bit의 위치에서 시작하여 5 bit의 크기를 가지고 색상 Red를 나타내는 bit열의 MSB가 left(즉 전체 pixel encoding상 MSB쪽)임을 나타냅니다. msb_right는 정말 특수한 경우가 아니면 0이 되므로 무시하기로 합니다.
이제 R(5),G(6),B(5) 형식의 pixel encoding을 생각해 보면 fb_var_screeninfo내의 red, green, blue가 다음과 같은 값을 가지게 됩니다.
struct fb_bitfield red = { 11, 5, 0 };
struct fb_bitfield green = { 5, 6, 0 };
struct fb_bitfield blue = { 0, 5, 0 };
그리고 R(5),G(5),B(5) 형식의 pixel encoding의 경우는 다음과 같습니다.
struct fb_bitfield red = { 10, 5, 0 };
struct fb_bitfield green = { 5, 5, 0 };
struct fb_bitfield blue = { 0, 5, 0 };
이제 구분할 수 있겠죠.
사실 구분할 필요가 없습니다. Red, Blue, Green 각각의 색상이 연속적인(contiguous) bit열로 encoding되어 있는 모든 16bpp에 대해서 위의 세 개의 값을 통해서 나타낼 수 있습니다. 즉 16bpp라고 해서 pixel encoding이 꼭 R(5),G(6),B(5) 또는 R(5),G(5),B(5)의 경우가 아닐 수도 있고 그런 경우라고 해도 위의 세 개의 값을 이용하면 모두 encoding을 표현하는 것이 가능합니다. 즉 엽기적으로 R(14),G(1),B(1)의 pixel encoding도 표현할 수 있다는 말이죠.(물론 그야말로 엽기적인 경우일 뿐, 일반적인 상식으로는 별로 가능성을 고려할 필요가 없죠.)

== 수정 사항
fbpixel.c와 fbpixel2.c 에서 나온 makepixel이라는 함수를 바꾸어 16bpp의 경우 모두 지원할 수 있도록 만들어 보았습니다. 저번 makepixel 함수에 비해 길어졌지만 그냥 보기 쉽게 만들려고 한 것일 뿐, bit의 length와 offset을 고려한 것을 제외하면 더 복잡해 졌다고 볼 수는 없습니다.
===============================
typedef unsigned char ubyte;

static unsigned short makepixel(struct fb_var_screeninfo *pfbvar, ubyte r, ubyte g, ubyte b)
{
unsigned short rnew, gnew, bnew;

rnew = r >> (8-pfbvar->red.length);
gnew = g >> (8-pfbvar->green.length);
bnew = b >> (8-pfbvar->blue.length);

return (unsigned short) ((rnew << pfbvar->red.offset)
| (gnew << pfbvar->green.offset)
| (bnew << pfbvar->blue.offset));
}
===============================
이제 함수의 인자로 FBIOGET_VSCREENINFO ioctl을 통해 전달 받은 fb_var_screeninfo를 넘기고 있습니다. 그것의 red, green, blue의 값을 이용하여 pixel을 encoding하는 모습을 볼 수 있습니다. (fbbmp.c에서도 동일한 함수를 사용하였습니다.)
사실 좀더 정확하게 하려면 함수의 인자인 r, g, b가 unsigned char 형식이 아니라 unsigned short여야 가능한 모든 16bpp pixel encoding 형식을 지원할 수 있겠지만 일반적으로 16bpp에서 한 색상이 8bit이상은 되지 않을 것이라는 가정을 하고 그냥 사용합니다. 또한 8-length의 값이 음수가 될 수도 있으므로 그것을 고려해야 하겠지만 역시 같은 이유로 그냥 사용합니다.(shift 연산의 우측 operand가 음수일 때 C 언어의 behavior가 어떤지는 C 언어 reference 매뉴얼을 찾아봐야 하지만 귀찮으므로 넘어갑니다.)


== reference
(1) www.wotsit.com
=> 갖가지 파일 형식에 관한 문서들을 모아놓은 사이트로 bmp 파일 형식에 관한 문서도 다운로드 가능합니다.
(2) ezfb(www.akrobiz.com/ezfb/)
=> 소스코드를 다운로드하여 수행해 보길 바랍니다. 역시 bmp를 frame buffer에 display할 수 있는 프로그램뿐 아니라 frame buffer에서 pixel값을 읽어서 bmp로 저장하는 프로그램 등도 들어 있는 것으로 보입니다. 개인적으로 좋아하지 않는 코딩 스타일이라서 참고로 하지 않았습니다.


== 마치며
원래 이번 글은 소스코드만 올리고 그냥 짧게 넘어가려고 했는데 예기치 않은 문제가 발생하여 길어졌습니다. 물론 한 수 배운 셈이죠. 그리고 이번 글에서는 참 “귀찮은 것”이 많았는데 그냥 양해하길 바랍니다. 다음 글은 16bpp가 아닌 다른 bpp에서의 프로그래밍에 대해서 짤막하게 알아보도록 하겠습니다. 이제 “frame buffer 이야기”도 끝날 때가 머지 않았습니다.
 
예제코드 링크 : http://kelp.or.kr/korweblog/upload/29/20021123194053/bmp.tgz

'Programming > Linux_Kernel' 카테고리의 다른 글

SecureCRT 에서 menuconfig 보기  (0) 2009.06.08
frame buffer 이야기(8)(9)  (0) 2009.06.04
frame buffer 이야기(4) (5)  (0) 2009.06.04
frame buffer 이야기 (3)  (0) 2009.06.04
frame buffer 이야기 (2)  (0) 2009.06.04