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