libdrm의 modetest 분석하기
Buildroot로 x86 리눅스 QEMU를 에뮬레이터로 빌드하였고, 여기서 drm이 어떻게 동작하는지 분석하고 있다.
libdrm은 drm을 라이브러리로 만든 것인데, user space의 앱이 drm과 통신하려면 ioctls을 사용해야하기 때문에, libdrm을 만들었다.
modetest는 libdrm의 테스트로, X11나 Wayland 없이 직접 libdrm API를 사용하여 화면을 출력하는 테스트이다. ping 처럼 drm/kms를 포팅했을 때 잘 동작하는지 확인하는 것이라고 해야할까??
1. modetest의 역할
modetest는 DRM/KMS를 시험하는 테스트 툴이다.
주요 용도:
- connector, encoder, crtc, framebuffer, plane 정보를 dump
- mode set 수행
- atomic/legacy 경로를 시험
- 테스트 패턴(SMPTE, gradient 등) 출력
2. modetest로 화면 출력
QEMU 쉘에서 아래와 같이 인자와 함께 modetest를 호출하면 테스트 패턴이 화면에 출력된다.
$ modetest -M virtio_gpu -s 38@37:1024x768 -a -P 33@37:1024x768@XR24
테스트 패턴은 기본 값이 설정되어있는데, 아래처럼 -F로 다른 패턴을 설정하면 프레임 버퍼에 다른 패턴의 이미지가 렌더링된다.
modetest -M virtio_gpu -s 38@37:1024x768 -F tiles,plain
가능한 패턴 이름
smptetilesplaingradientnoisenoise-colorblack-white
패턴을 코드에서 바꾸기
modetest.c에서 직접 바꿀 수도 있다.
static enum util_fill_pattern primary_fill = UTIL_PATTERN_GRADIENT;
static enum util_fill_pattern secondary_fill = UTIL_PATTERN_PLAIN;
하지만 보통은 코드보다 -F 옵션을 쓰는 게 낫다.
패턴 생성 함수
패턴 생성은 여기서 실제 픽셀 데이터가 채워진다.
tests/util/pattern.c
중요 함수:
util_fill_pattern(...)fill_smpte(...)fill_smpte_yuv_planar(...)fill_smpte_rgb24(...)fill_smpte_rgb32(...)fill_smpte_rgb16(...)
SMPTE 컬러바의 색상 정의
pattern.c 안에는 SMPTE에 쓰이는 색 테이블이 있다.
- 상단 바
- 중간 바
- 하단 테스트 패턴
이런 식으로 구성되어 있으며, RGB/YUV 포맷에 맞춰 각 함수가 픽셀을 채운다.
modetest의 resource 구조
res에는 다음이 들어간다.
crtcsencodersconnectorsfbsplanes
각 구조체는 대략 이런 의미다.
crtc: 화면을 어떤 타이밍으로 표시할지 담당encoder: connector와 crtc를 연결하는 중간 계층connector: 물리/논리 출력단fb: 실제 frame bufferplane: 화면에 그릴 레이어
modetest에서 main 흐름 따라가기
modetest에서 아래 명령을 입력할 때 main에서 어떻게 동작하는지 따라가 보았다.
$ modetest -M virtio_gpu -s 38@37:1024x768 -a -P 33@37:1024x768@XR24
output/build/libdrm-2.4.131/tests/modetest/modetest.c
int main(int argc, char **argv)
{
// 인자 받는 코드
while ((c = getopt(argc, argv, optstr)) != -1) {
args++;
...
}
if (use_atomic) {
// 리눅스 커널(DRM 드라이버)에게 아토믹 전용 하드웨어 정보들을 숨기지 말고 전부 다 보여줘라고 권한을 요청하고 활성화
// Client Capability
// Legacy device/program에 아토믹 정보 노출 시, 오동작 가능성
ret = drmSetClientCap(dev.fd, DRM_CLIENT_CAP_ATOMIC, 1);
drmSetClientCap(dev.fd, DRM_CLIENT_CAP_WRITEBACK_CONNECTORS, 1);
if (ret) {
fprintf(stderr, "no atomic modesetting support: %s\n", strerror(errno));
drmClose(dev.fd);
return -1;
}
}
if (dev.use_atomic) {
// Atomic은 req에 적용할 것을 먼저 추가하고 마지막에 commit하는 구조
dev.req = drmModeAtomicAlloc(); // 아토믹 요청을 담을 메모리 구조체 할당
}
for (i = 0; i < prop_count; ++i) {
// Atomic인 경우 drmModeAtomicAddProperty로 req에 prop 추가
set_property(&dev, &prop_args[i]);
}
if (dev.use_atomic) {
if (set_preferred || (count && plane_count)) {
uint64_t cap = 0;
// 드라이버에게 DUMB_BUFFER 기능 지원 물어봄:
// 화면에 그림을 그리려면 비디오 메모리를 할당받아야 하는데, GPU 가속이 없는 단순 2D 메모리 버퍼 할당 API가
// 바로 DUMB_BUFFER. 이게 없으면 테스트 패턴(컬러 바)을 메모리에 올릴 방법이 없으므로 에러를 내고 종료
ret = drmGetCap(dev.fd, DRM_CAP_DUMB_BUFFER, &cap);
if (set_preferred || count) {
// req에 해상도, CRTC, Connector 설정 채우기
count = set_mode(&dev, &pipe_args, count);
}
if (dump_path) {
if (!pipe_has_writeback_connector(&dev, pipe_args, count)) {
fprintf(stderr, "No writeback connector found, can not dump.\n");
return 1;
}
writeback_config(&dev, pipe_args, count);
}
if (plane_count) {
// req에 Plane(레이어) 및 FB 설정 채우기
atomic_set_planes(&dev, plane_args, plane_count, false);
}
// 첫 번째 아토믹 커밋
// - DRM_MODE_ATOMIC_ALLOW_MODESET: 아토믹 모드에서는 완전히 새로운 해상도를 세팅하거나 모니터를 켜는 무거운
// 작업(Modeset)을 할 때 이 플래그를 명시해야만 커널이 허락해 줍니다.
// - 함수가 성공하면 모니터/QEMU 창에 우리가 원하는 화면(테스트 패턴)이 출력됩니다.
ret = drmModeAtomicCommit(dev.fd, dev.req, DRM_MODE_ATOMIC_ALLOW_MODESET, NULL);
if (ret) {
fprintf(stderr, "Atomic Commit failed [1]\n");
return 1;
}
/*
* Since only writeback connectors have an output fb, this should only be
* called for writeback.
*/
if (dump_path) {
// 라이트백(Writeback) 덤프 기능:
// 화면에 출력되는 픽셀 데이터를 실제 모니터로만 쏘는 게 아니라, 가상 커넥터를 통해 메모리로 다시
// 되돌려 받아서(Writeback) 파일(dump_path)로 저장(스크린샷 스냅샷)하는 동작입니다.
// 펜스(Fence)를 이용해 하드웨어가 픽셀을 다 채울 때까지 기다렸다가(poll) 파일로 덤프합니다.
ret = poll_writeback_fence(dev.writeback_fence_fd, 1000);
if (ret)
fprintf(stderr, "Poll for writeback error: %d. Skipping Dump.\n",
ret);
dump_output_fb(&dev, pipe_args, dump_path, count);
}
if (test_vsync)
atomic_test_page_flip(&dev, pipe_args, plane_args, plane_count);
if (drop_master) {
// 내가 디스플레이 제어권(Master)을 쥐고 있으면 다른 프로그램(X11 등)이 화면을 못 쓰니까
// 잠시 권한을 놔줍니다.
drmDropMaster(dev.fd);
}
// 사용자가 엔터(Enter) 키를 누르기 전까지 프로그램의 실행을 여기서 멈춰 둡니다.
getchar();
// req를 free하고 다시 alloc (새로운 req 할당)
drmModeAtomicFree(dev.req);
dev.req = drmModeAtomicAlloc();
/* XXX: properly teardown the preferred mode/plane state */
if (plane_count) {
// 플레인에 묶인 FB 해제 요청
atomic_clear_planes(&dev, plane_args, plane_count);
}
if (count) {
// CRTC, Connector 꺼달라고 요청
atomic_clear_mode(&dev, pipe_args, count);
}
}
// 두 번째 아토믹 커밋 (화면 끄기 및 메모리 해제)
ret = drmModeAtomicCommit(dev.fd, dev.req, DRM_MODE_ATOMIC_ALLOW_MODESET, NULL);
if (ret)
fprintf(stderr, "Atomic Commit failed\n");
if (count && plane_count) {
// 할당했던 Dumb 비디오 메모리 자체를 완전히 파괴(Free)
atomic_clear_FB(&dev, plane_args, plane_count);
}
// req를 free
drmModeAtomicFree(dev.req);