화면에 픽셀 그리기

픽셀 점찍기

UEFI 에서 픽셀을 찍으려면 부트서비스에서 GOP(Graphics Output Protocol)을 열어서 인터페이스 포인터를 가져오고 Mode 멤버를 통해 프레임버퍼 등의 정보를 얻어올 수 있다.

  • gop->Mode->FrameBufferBase: 한 화면의 각 픽셀을 가리키는 메모리 베이스 주소이다. 1px 당 3byte로 구성되어 “픽셀 수 * 픽셀당 바이트” 만큼의 메모리 공간을 점유하고 있고 메모리에 값을 쓰면 화면에 표시되는 구조이다.
  • gop->Mode->FrameBufferSize: 프레임버퍼가 몇바이트인지 알 수 있다. “총 픽셀 수 * 픽셀당 바이트”
  • gop->Mode->Info->PixelFormat: 정수 값으로 픽셀 포맷이 어떻게 생겼는지 알려준다. 이 내용으로 픽셀당 몇바이트인지 알 수 있다.
  • gop->Mode->Info->HorizontalResolution, VertcalResolution: 가로 세로 각 몇 픽셀인지 알 수 있다. (해상도)
Print(L"Resolution: %ux%u, Pixel Format: %s, %u pixels/line\n",
  gop->Mode->Info->HorizontalResolution,
  gop->Mode->Info->VerticalResolution,
  GetPixelFormatUnicode(gop->Mode->Info->PixelFormat),
  gop->Mode->Info->PixelsPerScanLine);
Print(L"Frame Buffer: 0x%0lx - 0x%0lx, Size: %lu bytes\n",
  gop->Mode->FrameBufferBase,
  gop->Mode->FrameBufferBase + gop->Mode->FrameBufferSize,
  gop->Mode->FrameBufferSize);  

dc06cb68-665e-41b7-86a9-a2ec4c48b379
dc06cb68-665e-41b7-86a9-a2ec4c48b379

UEFI 소스코드

GOP 가져오기

부트서비스에서 gop 프로토콜을 가져와서 두번째 인자인 gop에 인터페이스를 저장하는 함수이다.

이 값은 부팅과정(펌웨어)에서 GPU를 초기화하고 스캔해서 GOP 모드를 구성한 값이고, gop 프로토콜을 가져오면서 현재 초기화된 값을 가져오게 된다.

EFI_STATUS OpenGOP(EFI_HANDLE image_handle,
                   EFI_GRAPHICS_OUTPUT_PROTOCOL** gop) {
  UINTN num_gop_handles = 0;
  EFI_HANDLE* gop_handles = NULL;
  gBS->LocateHandleBuffer(
      ByProtocol,
      &gEfiGraphicsOutputProtocolGuid,
      NULL,
      &num_gop_handles,
      &gop_handles);

  gBS->OpenProtocol(
      gop_handles[0],
      &gEfiGraphicsOutputProtocolGuid,
      (VOID**)gop,
      image_handle,
      NULL,
      EFI_OPEN_PROTOCOL_BY_HANDLE_PROTOCOL);

  FreePool(gop_handles);

  return EFI_SUCCESS;
}

픽셀 포맷을 문자열로 표시

픽셀 포맷이 enum 값이기 때문에 문자열로 변환해주는 유틸 함수이다.

  • PixelRedGreenBlueReserved8BitPerColor : 4byte 형식으로 (R8, G8, B8, Reserved8) 이렇게 구성되어 있으며, Reserved는 예약 공간으로 표준 GOP에서는 사용되지 않는데, GOP 구현에서는 Alpha 로 구현해서 사용하기도 한다.
    • GOP 드라이버가 GPU의 픽셀포맷을 지정하고 프레임버퍼를 복사해주면 GPU가 화면에 출력하는 방식이다.
  • PixelBlueGreenRedReserved8BitPerColor : (B8, G8, R8, Reserved8) 형식이다.
const CHAR16* GetPixelFormatUnicode(EFI_GRAPHICS_PIXEL_FORMAT fmt) {
  switch (fmt) {
    case PixelRedGreenBlueReserved8BitPerColor:
      return L"PixelRedGreenBlueReserved8BitPerColor";   // 4byte
    case PixelBlueGreenRedReserved8BitPerColor:
      return L"PixelBlueGreenRedReserved8BitPerColor";
    case PixelBitMask: 
      return L"PixelBitMask";
    case PixelBltOnly:
      return L"PixelBltOnly";
    default:
      return L"InvalidPixelFormat";
  }
}

프레임버퍼에 값 쓰기

프레임 버퍼의 모든 오프셋에 255(0xff) 값을 쓰는데, 그럼 픽셀의 색을 지정하는 모든 바이트가 0xff가 되고, 모든 픽셀에 흰색이 그려진다.

  EFI_GRAPHICS_OUTPUT_PROTOCOL* gop;
  OpenGOP(image_handle, &gop);

  UINT8* frame_buffer = (UINT8*)gop->Mode->FrameBufferBase;
  for (UINTN i = 0; i < gop->Mode->FrameBufferSize; ++i) {
    frame_buffer[i] = 255;
  }

커널에서 픽셀 점찍기

부트로더에서 얻은 프레임버퍼를 커널로 넘기고, 커널에서 프레임버퍼에 값을 쓰면 된다.

typedef void EntryPointType(UINT64, UINT64);
EntryPointType* entry_point = (EntryPointType*)entry_addr;
entry_point(gop->Mode->FrameBufferBase, gop->Mode->FrameBufferSize);

커널에서는 그냥 인자로 받은 프레임버퍼를 접근해서 사용하는건 동일하다.

#include <cstdint>

extern "C" void KernelMain(uint64_t frame_buffer_base,
                           uint64_t frame_buffer_size) {
  uint8_t* frame_buffer = reinterpret_cast<uint8_t*>(frame_buffer_base);
  for (uint64_t i = 0; i < frame_buffer_size; ++i) {
    frame_buffer[i] = i % 256;
  }
  while (1) __asm__("hlt");
}

PixelWriter 클래스 구현

점을 좀 더 쉽게 찍기 위해 PixelWriter 클래스를 구현할 것이다. 어렵진 않다.

1. UEFI 에서 GOP 정보 세팅

frame_buffer_config.hpp

UEFI에서 제공하는 픽셀 데이터 형식은 4가지의 픽셀 데이터 형식이 있지만 렌더링의 간소화를 위해 픽셀당 4byte 크기의 두가지 타입만 사용한다.

#pragma once
#include <stdint.h>

// 사용할 두가지 픽셀 타입
enum PixelFormat {
  kPixelRGBResv8BitPerColor,
  kPixelBGRResv8BitPerColor,
};

// 픽셀 찍을때 사용할 정보들만 저장
struct FrameBufferConfig {
  uint8_t* frame_buffer;
  uint32_t pixels_per_scan_line;
  uint32_t horizontal_resolution;
  uint32_t vertical_resolution;
  enum PixelFormat pixel_format;
};

UefiMain

// 이전에 OpenGOP에서 얻어온 값들을 구조체에 저장해서 전달해준다. 
  struct FrameBufferConfig config = {
    (UINT8*)gop->Mode->FrameBufferBase,
    gop->Mode->Info->PixelsPerScanLine,
    gop->Mode->Info->HorizontalResolution,
    gop->Mode->Info->VerticalResolution,
    0
  };

  switch (gop->Mode->Info->PixelFormat) {
    case PixelRedGreenBlueReserved8BitPerColor:
      config.pixel_format = kPixelRGBResv8BitPerColor;
      break;
    case PixelBlueGreenRedReserved8BitPerColor:
      config.pixel_format = kPixelBGRResv8BitPerColor;
      break;
    default:
      _IfErrorHalt(L"Unimplemented pixel format", gop->Mode->Info->PixelFormat);
  }
// ... 커널에 gop config 전달
  entry_point(&config);

2. kernel 에서 클래스에 세팅하고 픽셀찍기

클래스 정의

struct PixelColor {
  uint8_t r, g, b;
};

// 추상 클래스 생성
class PixelWriter {
public:
// 멤버 초기화리스트. 복사해서 멤버에 저장
  PixelWriter(const FrameBufferConfig& config) : config_{config} {
  }
// 디폴트 소멸자를 사용하겠다는 의미 
  virtual ~PixelWriter() = default;
  virtual void Write(int x, int y, const PixelColor& c) = 0;

protected:
// 픽셀 좌표를 받아서 프레임버퍼상 위치를 계산해주는 함수.
// 픽셀당 4byte짜리 모드이기 때문에 계산한 위치에 4를 곱한다.
// 메모리상에선 2차원 배열이 1차원 배열처럼 연속적으로 할당되어 있다. 
// 바이트 크기가 다른 모드면 당연히 맞춰서 수정해야되지만, 지원하는 방식 모두 4byte이다.
  uint8_t* PixelAt(int x, int y) {
    return config_.frame_buffer + 4 * (config_.pixels_per_scan_line * y + x);
  }

private:
  const FrameBufferConfig& config_;
};

// 상속받아서 RGBReserve 모드에 맞게 출력하는 코드
// 모드마다 PixelWriter를 상속받아 구현해주면 된다.
class RGBResv8BitPerColorPixelWriter : public PixelWriter {
public:
// c++11 부터 도입된 생성자 상속 문법. 부모클래스의 생성자 형태 그대로 사용하겠다는 의미.
  using PixelWriter::PixelWriter;

  virtual void Write(int x, int y, const PixelColor& c) override {
    auto p = PixelAt(x, y);
    p[0] = c.r;
    p[1] = c.g;
    p[2] = c.b;
    p[3] = 0xFF;   // 어차피 사용되지는 않지만, 명시적으로 초기화 해주는것이 좋다.
  }
};

new / delete

new는 할당과 초기화를 담당하는데, c++에서는 이미 할당한 공간에서 초기화만 진행할수도 있다. 이걸 placement new라고 부른다.

동적할당은 커널에서 해주는 기능이지만, 현재는 힙에 할당해줄 수 있는 기능이 없어서 전역 메모리공간에서 필요한 크기만큼 할당해두고 그 메모리 공간을 사용해서 placement new 로 클래스 초기화만 한다.

void* operator new(size_t size, void* buf) {
  return buf;
}

void operator delete(void* obj) noexcept {
}

// PixelWriter의 멤버로 config_의 레퍼런스(포인터) + vtable 의 크기인 16byte 가 된다.
char pixel_writer_buf[sizeof(RGBResv8BitPerColorPixelWriter)];
PixelWriter* pixel_writer;

extern "C" void KernelMain(const FrameBufferConfig& frame_buffer_config) {
// ...
// placement new 문법. 
// 이미 전역메모리에 할당한 pixel_writer_buf 를 사용해서 할당과정은 생략
// RGBResv8BitPerColorPixelWriter 타입에 인자로 전달받은 frame_buffer_config 를 사용하여 초기화
// 초기화된 전역메모리 공간을 pixel_writer 에 포인터로 저장.
  pixel_writer = new(pixel_writer_buf) RGBResv8BitPerColorPixelWriter{frame_buffer_config};
// ...
}

KernelMain

extern "C" void KernelMain(const FrameBufferConfig& frame_buffer_config) {
// 전달받은 gop 설정에 맞춰서 픽셀모드 선택
  switch (frame_buffer_config.pixel_format) {
    case kPixelRGBResv8BitPerColor:
// placement new
      pixel_writer = new(pixel_writer_buf) RGBResv8BitPerColorPixelWriter{frame_buffer_config};
      break;
    case kPixelBGRResv8BitPerColor:
      pixel_writer = new(pixel_writer_buf) BGRResv8BitPerColorPixelWriter{frame_buffer_config};
      break;
  }

// 전체 픽셀 돌면서 상자는 초록색 {0, 255, 0}, 배경은 흰색 {255, 255, 255} 으로 그린다.
  for (int x = 0; x < frame_buffer_config.horizontal_resolution; ++x) {
    for (int y = 0; y < frame_buffer_config.vertical_resolution; ++y) {
      if ((x >= 0 && x < 200) && (y >= 0 && y < 100))
        pixel_writer->Write(x, y, {0, 255, 0});
      else
        pixel_writer->Write(x, y, {255, 255, 255});
    }
  }

  while (1) __asm__("hlt");
}
ESC
Type to search...