화면에 픽셀 그리기
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);
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}