월간 보관물: 2013 9월

[WAYLAND] 2. 컴포지터(compositor)

이번에는 윈도우 매니저의 가장 기본적인 기능인 윈도우 생성과 화면 갱신에 대해 알아보도록 하겠다. 우리가 일반적으로 윈도우 환경에서 프로그램(웹브라우저, 게임 등)을 실행하면 창이 하나 뜨고, 그 안에 해당 프로그램이 보여주고자 하는 것들이 담겨 있다. 그리고 사용자는 마우스를 이용하여 해당 윈도우를 화면의 어디든 옮길 수 있고, 화면 전체로 키울 수도 있고, 안 보이게 할 수도 있다. 이러한 과정들이 실제로 어떻게 이루어지는지에 대해 WAYLAND 를 기반으로 간단히 알아보도록 하겠다.

먼저 위에서 설명한 과정을 컴포지터(COMPOSITOR)와 쉘(SHELL) 두 가지로 나누어서 다시 설명하도록 하겠다. 컴포지터는 화면 위에 겹쳐있는 모든 윈도우들을 하나의 스크린 위에 합쳐서 보여주는 역할을 한다. 예를 들어, 우리가 두 개의 웹브라우저를 겹치게 띄워놓고 하나의 웹브라우저를 마우스로 클릭하면, 선택한 웹브라우저가 앞으로 나와서 뒤에 있는 웹브라우저를 가리게 된다. 반대로 뒤에 있는 웹브라우저를 마우스로 클릭하게 하면, 뒤에 있던 웹브라우저가 앞으로 나와서 이전에 앞에 있던 웹브라우저를 가리게 된다. 이처럼 컴포지터는 모든 윈도우들의 우선순위를 이용하여 하나의 스크린 위에 차곡차곡 쌓은 것처럼 결과화면을 만들어주는 역할을 한다. 그리고 우리가 웹브라우저의 테두리를 마우스로 클릭해서 드래그하면 윈도우가 옮겨지고, 오른쪽 위에 있는 최대/최소 버튼을 누르게 되면 윈도우가 화면 전체를 덮거나 사라진다. 이러한 과정들은 바로 쉘이 처리해준다. 즉, 모든 윈도우의 화면 상에서의 위치와 크기를 결정하는 것은 쉘의 역할이고, 이러한 정보를 이용하여 하나의 화면으로 합쳐주는 것이 컴포지터의 역할이다. 오늘은 컴포지터에 대해서 좀 더 알아보도록 하겠다. (시작 메뉴나 가젯 등 기본적인 윈도우 환경을 꾸며주는 역할 또한 쉘이 한다고 보면 된다.)

윈도우 생성 과정

WAYLAND 는 컴포지터의 역할을 수행하기 위해 wl_compositor 인터페이스를 제공한다. 이 인터페이스에는 create_surface 리퀘스트가 정의되어 있는데, 이 리퀘스트가 바로 클라이언트(응용 프로그램)가 새로운 윈도우를 만들고 싶을때 사용하는 것이다. 즉, 우리가 일반적으로 윈도우라고 부르는게 WAYLAND 에서는 surface 라는 객체로 관리된다. 다음 코드는 클라이언트가 wl_compositor 인터페이스의 create_surface 리퀘스트를 요청하는 코드의 일부분이다.


...
window->surface = wl_compositor_create_surface(display->compositor);
...

위의 코드처럼 클라이언트가 wl_compositor_create_surface() 함수를 이용하여 새로운 surface 를 요청하면 서버(컴포지터)는 해당 요청을 처리한 후, wl_surface 인터페이스 객체를 넘겨준다. (이와 같이, 상위 wl_compositor 인터페이스 객체는 wl_registry 에서 생성하고, 하위 wl_surface 인터페이스 객체는 리퀘스트를 이용하여 생성하는 방식은 WAYLAND 에서 자주 사용되는 방식이다.) 이렇게 넘겨받은 wl_surface 인터페이스 객체는 클라이언트가 해당 surface 의 내용을 변경할 때 사용하는 인터페이스이다. 지금까지의 과정을 간단히 요약하면 다음과 같다.

  • 서버는 wl_compositor 인터페이스를 global 로 등록해둔다.
  • 클라이언트는 wl_compositor 인터페이스를 바인딩 한다.
  • 클라이언트는 wl_compositor_create_surface() 함수를 이용하여 새로운 surface 를 요청한다.
  • 서버는 새로운 surface 를 생성하고, wl_surface 인터페이스 객체를 넘겨준다.
  • 클라이언트는 wl_surface 인터페이스를 이용하여 해당 surface 의 내용을 변경한다.

윈도우 화면 갱신 과정

위에서 설명한 것처럼 클라이언트는 wl_surface 인터페이스를 이용하여 윈도우의 내용을 원하는대로 변경할 수 있다. 그렇다면 실제로 변경할 내용을 어떻게 서버에게 전달할까? 여기에는 크게 보면, 공유메모리 방식과 공유 EGL 텍스쳐 방식 두 가지가 있다. 오늘은 전반적인 과정에 대해 설명하는 자리이니 간단한 공유메모리 방식을 이용하여 설명하도록 하겠다.

static int create_shm_buffer(struct display *display, struct buffer *buffer, int width, int height, uint32_t format)
{
  struct wl_shm_pool *pool;
  int fd, size, stride;
  void *data;

  stride = width * 4;
  size = stride * height;

  fd = os_create_anonymous_file(size);
  if (fd < 0) {
    fprintf(stderr, "creating a buffer file for %d B failed: %m\n", size);
    return -1;
  }

  data = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);

  if (data == MAP_FAILED) {
    fprintf(stderr, "mmap failed: %m\n");
    close(fd);
    return -1;
  }

  pool = wl_shm_create_pool(display->shm, fd, size);

  buffer->buffer = wl_shm_pool_create_buffer(pool, 0, width, height, stride, format);

  wl_buffer_add_listener(buffer->buffer, &buffer_listener, buffer);
  wl_shm_pool_destroy(pool);
  close(fd);

  buffer->shm_data = data;

  return 0;
}

위의 코드는 클라이언트가 surface 의 내용을 변경하기 위해 공유메모리를 이용하여 wl_buffer 인터페이스 객체를 생성하는 부분이다. 일단 클라이언트는 필요한 크기의 임시파일을 생성하고(10번째 줄) mmap() 을 이용하여 메모리를 매핑한다(16번째 줄). 그리고 wl_shm 인터페이스를 이용하여 임시파일의 파일디스크립터와 크기를 서버에게 보낸다(24번째 줄). (클라이언트에서 생성한 파일디스크립터를 서로 다른 프로세스로 동작하는 서버에게 보내서 어떻게 사용하는지는 이 글의 마지막에 간단히 설명하도록 하겠다.) 그리고 서버는 wl_shm_pool 인터페이스 객체를 클라이언트에게 넘겨주고, 클라이언트는 wl_shm_pool 인터페이스를 이용하여 원하는 크기의 wl_buffer 인터페이스 객체를 생성한다(26번째 줄). 이 wl_buffer 인터페이스 객체를 이용하여 클라이언트가 임시파일로 생성해둔 공유메모리 영역을 서버와 공유하게 된다.

클라이언트는 해당 공유메모리 영역을 원하는 내용(이미지)으로 채운 후 다음과 같은 간단한 과정을 통하여 서버에게 surface 의 내용 변경을 요청한다.

wl_surface_attach(window->surface, buffer->buffer, 0, 0);
wl_surface_damage(window->surface, 20, 20, window->width - 40, window->height - 40);

if (callback)
  wl_callback_destroy(callback);

window->callback = wl_surface_frame(window->surface);
wl_callback_add_listener(window->callback, &frame_listener, window);
wl_surface_commit(window->surface);

위의 코드를 보면, wl_surface_attach() 는 surface 에 buffer 를 연결해주고, wl_surface_damage() 는 실제 변경이 발생한 영역을 알려주는 역할을 한다. 그리고 wl_surface_frame() 은 wl_callback 인터페이스 객체를 넘겨주는데, 이는 서버에서 해당 buffer 를 이용하여 surface 를 변경하고 나서 클라이언트에게 완료 이벤트를 넘겨주기 위해 사용하는 것이다. 마지막으로 wl_surface_commit() 을 요청하면 서버는 해당 요청을 큐에 추가하고 다음 컴포지터 갱신 때 해당하는 내용을 반영한다.

지금까지 WAYLAND 기반의 윈도우 매니저가 어떤 방식으로 윈도우를 생성하고, 화면을 갱신하는지를 간단히 설명하였다. 컴포지터의 자세한 내부 동작 과정은 기본 동작 과정들을 충분히 설명한 다음 진행할 계획이다.

마지막으로 위에서 설명하기로 했던 서로 다른 프로세스인 클라이언트와 서버가 어떻게 파일디스크립터를 공유할 수 있는지에 대해 간단히 설명하도록 하겠다. 이는 클라이언트와 서버가 기본적으로 유닉스 도메인 소켓을 이용한다는 전제하에 가능한 것이다. 유닉스 도메인 소켓은 SCM (Socket-level Control Message) 라는 기능을 제공한다. 이 기능을 이용하여 클라이언트가 파일디스크립터를 전송하면 커널은 내부적으로 파일디스크립터가 참조하는 파일(커널 내부적으로 관리하는 열린 파일 참조 객체)을 미리 보관해두고, 서버가 해당 메세지를 수신하면 보관해둔 파일을 서버 프로세스를 위한 새로운 파일디스크립터에 연결하여 서버에게 알려준다.

[RTRT] 멋진 실시간 렌더링 데모 공개!!!

드디어 이런 프로젝트가 나오기 시작하네요^^ 진짜 멋지네요~~~

많이 봐오던 OpenGL 이나 DirectX 기반의 3D 실시간 게임이랑은 느낌이 많이 다르죠? 마치 영화 속의 한 장면을 보는것같은…왜냐하면 영화 속 CG 를 만드는 기술과 유사한 기술(레이 트레이싱)을 사용하기 때문입니다. (NEMO-UX 의 기반이 되는 기술도 동일한 기술입니다.)

최근 그래픽 하드웨어와 소프트웨어의 엄청난 발전으로 이제 이런 그래픽이 실시간으로 가능하게 되었습니다. 언리얼 엔진이나 크라이시스 엔진에서는 하이브리드 방식으로 레스터라이징 기반에 부분적으로 레이 트레이싱 기술을 적용하기 시작했고, 어도브에서도 실시간 레이 트레이싱 기술을 적용하기 시작했습니다. 아직은 대규모의 게임을 완벽하게 실시간 레이 트레이싱으로 돌리기는 힘들지만, 상대적으로 훨씬 단순한 UX 부터 시작하여 널리 쓰이기 시작할 것 같습니다.

아직 해당 프로젝트에 대한 정보가 공개된건 거의 없지만, Arauna Real-time Ray Tracing 을 개발하던 곳에서 개발한 것 같습니다. 동영상만 봤을 때는 NVIDIA OptiX 기반의 progressive path tracing 방식을 사용한 것 같긴한데 확인해봐야겠네요. 아무튼 무척이나 기대되는 프로젝트입니다.