화면에 픽셀 그리기

화면에 픽셀 그리기

2025년 3월 13일

픽셀 점찍기 #

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

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

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


UEFI 소스코드 #

GOP 가져오기 #

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

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

 1EFI_STATUS OpenGOP(EFI_HANDLE image_handle,
 2                   EFI_GRAPHICS_OUTPUT_PROTOCOL** gop) {
 3  UINTN num_gop_handles = 0;
 4  EFI_HANDLE* gop_handles = NULL;
 5  gBS->LocateHandleBuffer(
 6      ByProtocol,
 7      &gEfiGraphicsOutputProtocolGuid,
 8      NULL,
 9      &num_gop_handles,
10      &gop_handles);
11
12  gBS->OpenProtocol(
13      gop_handles[0],
14      &gEfiGraphicsOutputProtocolGuid,
15      (VOID**)gop,
16      image_handle,
17      NULL,
18      EFI_OPEN_PROTOCOL_BY_HANDLE_PROTOCOL);
19
20  FreePool(gop_handles);
21
22  return EFI_SUCCESS;
23}

픽셀 포맷을 문자열로 표시 #

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

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

프레임버퍼에 값 쓰기 #

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

1  EFI_GRAPHICS_OUTPUT_PROTOCOL* gop;
2  OpenGOP(image_handle, &gop);
3
4  UINT8* frame_buffer = (UINT8*)gop->Mode->FrameBufferBase;
5  for (UINTN i = 0; i < gop->Mode->FrameBufferSize; ++i) {
6    frame_buffer[i] = 255;
7  }

커널에서 픽셀 점찍기 #

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

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

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

 1#include <cstdint>
 2
 3extern "C" void KernelMain(uint64_t frame_buffer_base,
 4                           uint64_t frame_buffer_size) {
 5  uint8_t* frame_buffer = reinterpret_cast<uint8_t*>(frame_buffer_base);
 6  for (uint64_t i = 0; i < frame_buffer_size; ++i) {
 7    frame_buffer[i] = i % 256;
 8  }
 9  while (1) __asm__("hlt");
10}

PixelWriter 클래스 구현 #

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

1. UEFI 에서 GOP 정보 세팅 #

frame_buffer_config.hpp #

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

 1#pragma once
 2#include <stdint.h>
 3
 4// 사용할 두가지 픽셀 타입
 5enum PixelFormat {
 6  kPixelRGBResv8BitPerColor,
 7  kPixelBGRResv8BitPerColor,
 8};
 9
10// 픽셀 찍을때 사용할 정보들만 저장
11struct FrameBufferConfig {
12  uint8_t* frame_buffer;
13  uint32_t pixels_per_scan_line;
14  uint32_t horizontal_resolution;
15  uint32_t vertical_resolution;
16  enum PixelFormat pixel_format;
17};

UefiMain #

 1// 이전에 OpenGOP에서 얻어온 값들을 구조체에 저장해서 전달해준다. 
 2  struct FrameBufferConfig config = {
 3    (UINT8*)gop->Mode->FrameBufferBase,
 4    gop->Mode->Info->PixelsPerScanLine,
 5    gop->Mode->Info->HorizontalResolution,
 6    gop->Mode->Info->VerticalResolution,
 7    0
 8  };
 9
10  switch (gop->Mode->Info->PixelFormat) {
11    case PixelRedGreenBlueReserved8BitPerColor:
12      config.pixel_format = kPixelRGBResv8BitPerColor;
13      break;
14    case PixelBlueGreenRedReserved8BitPerColor:
15      config.pixel_format = kPixelBGRResv8BitPerColor;
16      break;
17    default:
18      _IfErrorHalt(L"Unimplemented pixel format", gop->Mode->Info->PixelFormat);
19  }
20// ... 커널에 gop config 전달
21  entry_point(&config);

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

클래스 정의 #

 1struct PixelColor {
 2  uint8_t r, g, b;
 3};
 4
 5// 추상 클래스 생성
 6class PixelWriter {
 7public:
 8// 멤버 초기화리스트. 복사해서 멤버에 저장
 9  PixelWriter(const FrameBufferConfig& config) : config_{config} {
10  }
11// 디폴트 소멸자를 사용하겠다는 의미 
12  virtual ~PixelWriter() = default;
13  virtual void Write(int x, int y, const PixelColor& c) = 0;
14
15protected:
16// 픽셀 좌표를 받아서 프레임버퍼상 위치를 계산해주는 함수.
17// 픽셀당 4byte짜리 모드이기 때문에 계산한 위치에 4를 곱한다.
18// 메모리상에선 2차원 배열이 1차원 배열처럼 연속적으로 할당되어 있다. 
19// 바이트 크기가 다른 모드면 당연히 맞춰서 수정해야되지만, 지원하는 방식 모두 4byte이다.
20  uint8_t* PixelAt(int x, int y) {
21    return config_.frame_buffer + 4 * (config_.pixels_per_scan_line * y + x);
22  }
23
24private:
25  const FrameBufferConfig& config_;
26};
27
28// 상속받아서 RGBReserve 모드에 맞게 출력하는 코드
29// 모드마다 PixelWriter를 상속받아 구현해주면 된다.
30class RGBResv8BitPerColorPixelWriter : public PixelWriter {
31public:
32// c++11 부터 도입된 생성자 상속 문법. 부모클래스의 생성자 형태 그대로 사용하겠다는 의미.
33  using PixelWriter::PixelWriter;
34
35  virtual void Write(int x, int y, const PixelColor& c) override {
36    auto p = PixelAt(x, y);
37    p[0] = c.r;
38    p[1] = c.g;
39    p[2] = c.b;
40    p[3] = 0xFF;   // 어차피 사용되지는 않지만, 명시적으로 초기화 해주는것이 좋다.
41  }
42};

new / delete #

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

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

 1void* operator new(size_t size, void* buf) {
 2  return buf;
 3}
 4
 5void operator delete(void* obj) noexcept {
 6}
 7
 8// PixelWriter의 멤버로 config_의 레퍼런스(포인터) + vtable 의 크기인 16byte 가 된다.
 9char pixel_writer_buf[sizeof(RGBResv8BitPerColorPixelWriter)];
10PixelWriter* pixel_writer;
11
12extern "C" void KernelMain(const FrameBufferConfig& frame_buffer_config) {
13// ...
14// placement new 문법. 
15// 이미 전역메모리에 할당한 pixel_writer_buf 를 사용해서 할당과정은 생략
16// RGBResv8BitPerColorPixelWriter 타입에 인자로 전달받은 frame_buffer_config 를 사용하여 초기화
17// 초기화된 전역메모리 공간을 pixel_writer 에 포인터로 저장.
18  pixel_writer = new(pixel_writer_buf) RGBResv8BitPerColorPixelWriter{frame_buffer_config};
19// ...
20}

KernelMain #

 1extern "C" void KernelMain(const FrameBufferConfig& frame_buffer_config) {
 2// 전달받은 gop 설정에 맞춰서 픽셀모드 선택
 3  switch (frame_buffer_config.pixel_format) {
 4    case kPixelRGBResv8BitPerColor:
 5// placement new
 6      pixel_writer = new(pixel_writer_buf) RGBResv8BitPerColorPixelWriter{frame_buffer_config};
 7      break;
 8    case kPixelBGRResv8BitPerColor:
 9      pixel_writer = new(pixel_writer_buf) BGRResv8BitPerColorPixelWriter{frame_buffer_config};
10      break;
11  }
12
13// 전체 픽셀 돌면서 상자는 초록색 {0, 255, 0}, 배경은 흰색 {255, 255, 255} 으로 그린다.
14  for (int x = 0; x < frame_buffer_config.horizontal_resolution; ++x) {
15    for (int y = 0; y < frame_buffer_config.vertical_resolution; ++y) {
16      if ((x >= 0 && x < 200) && (y >= 0 && y < 100))
17        pixel_writer->Write(x, y, {0, 255, 0});
18      else
19        pixel_writer->Write(x, y, {255, 255, 255});
20    }
21  }
22
23  while (1) __asm__("hlt");
24}
comments powered by Disqus