화면 레이어

화면 레이어

2025년 4월 20일

레이어와 윈도우 #

현재 커널에서는 호출한 함수에 따라 화면에서 해당되는 픽셀만 필요한 시점에 단 한번 그리게 된다.

마우스는 움직일때마다 업데이트 되지만, 배경은 그렇지 않기 때문에 마우스가 지나간 위치가 지워지게 되는데, 이걸 방지하기 위해 레이어를 두고 Z 인덱스 순서대로 레이어를 두고 화면을 다시 그려서 해결하려 한다.

9900fa96-d040-4f63-a516-e25c4a0d38a4

여기에서는 레이어 내부의 사각형 크기의 렌더링 영역을 윈도우라고 부를 것이기 때문에 Cursor 그림을 담고있는 윈도우도 있을 것이다.


윈도우 #

윈도우가 실제 렌더링할 그림에 대한 픽셀 정보(위치, 색 등)라고 보면 된다.
이 윈도우의 DrawTo 함수를 통해 데이터(픽셀정보)를 특정 PixelWriter의 프레임 버퍼에 쓰게 된다.

SetTransparentColor(PixelColor{1,1,1}); 로 투과색을 정할 수 있고, std::nullopt를 전달하면 제거할수도 있다.

 1class Window {
 2public:
 3  // Window와 관련있는 PixelWriter 클래스
 4  class WindowWriter : public PixelWriter {
 5  public:
 6    WindowWriter(Window& window) : window_{window} {}
 7    virtual void Write(int x, int y, const PixelColor& c) override {
 8      window_.At(x, y) = c;
 9    }
10    virtual int Width() const override { return window_.Width(); }
11    virtual int Height() const override { return window_.Height(); }
12
13  private:
14    Window& window_;
15  };
16
17  Window(int width, int height);
18  ~Window() = default;
19  Window(const Window& rhs) = delete;
20  Window& operator=(const Window& rhs) = delete;
21
22  // 특정 writer의 position 위치에 현재 윈도우를 그린다.
23  void DrawTo(PixelWriter& writer, Vector2D<int> position);
24  // 투과색을 지정한다. 
25  void SetTransparentColor(std::optional<PixelColor> c);
26  // 윈도우와 연결된 WinowWriter 리턴
27  WindowWriter* Writer();
28
29  // 지정한 위치의 픽셀 컬러를 반환
30  PixelColor& At(int x, int y);
31  const PixelColor& At(int x, int y) const;
32
33  // 윈도우(렌더링) 영역의 가로세로 픽셀길이
34  int Width() const;
35  int Height() const;
36
37private:
38  int width_, height_;
39  // 윈도우의 픽셀 데이터. 2차원 벡터
40  std::vector<std::vector<PixelColor>> data_{};
41  // 윈도우마다 writer가 하나씩 연결됨.
42  // 해당 writer를 이용해서 다른 그림도 이 윈도우에 그리게 할 수 있다.
43  WindowWriter writer_{*this};
44  std::optional<PixelColor> transparent_color_{std::nullopt};
45};
46
47Window::Window(int width, int height) : width_{width}, height_{height} {
48  data_.resize(height);
49  for (int y = 0; y < height; ++y) {
50    data_[y].resize(width);
51  }
52}
53
54// 여기에서의 Vector2D는 2개의 int 값을 저장할 수 있는 자료구조이다. 
55void Window::DrawTo(PixelWriter& writer, Vector2D<int> position) {
56  if (!transparent_color_) {
57    for (int y = 0; y < Height(); ++y) {
58      for (int x = 0; x < Width(); ++x) {
59        writer.Write(position.x + x, position.y + y, At(x, y));
60      }
61    }
62    return;
63  }
64  // transparent color
65}

레이어 #

레이어는 하나의 윈도우를 가지며, 윈도우는 데이터만을 담는 클래스였다면, 레이어는 윈도우의 위치 정보를 담는 클래스이다.

레이어는 id를 레이어 매니저로부터 할당받아 레이어 스택의 순서가 관리된다.

 1class Layer {
 2public:
 3  Layer(unsigned int id = 0);
 4  unsigned int ID() const;
 5
 6  Layer& SetWindow(const std::shared_ptr<Window>& window);
 7  std::shared_ptr<Window> GetWindow() const;
 8
 9  // 위치를 Layer에서 관리하기 때문에 pos를 변경할 수 있어야한다.
10  Layer& Move(Vector2D<int> pos);
11  Layer& MovRelative(Vector2D<int> pos_diff);
12
13  // 연결된 window의 DrawTo를 호출해준다. 전달되는 writer 기준으로 상대위치에 출력한다.
14  void DrawTo(PixelWriter& writer) const;
15
16private:
17  unsigned int id_;
18  // 절대좌표
19  Vector2D<int> pos_;
20  std::shared_ptr<Window> window_
21};

사실 layer에서는 window의 위치 정보를 담기 때문에 window에서 id와 pos를 관리해도 큰 문제는 없을거라 생각된다.


레이어 매니저 #

매니저를 통해 생성한 여러 레이어를 한꺼번에 관리하며 z index 순서대로 한꺼번에 출력하는 매니저 클래스이다.

 1class LayerManager {
 2public:
 3  void SetWriter(PixelWriter* writer);
 4  // 새로운 레이어를 생성하며 아이디를 부여해준다. 
 5  // NewLayer().SetWindow(...) 처럼 사용하기 위해 생성한 Layer& 를 리턴해준다.
 6  Layer& NewLayer();
 7
 8  // 관리하고 있는 전체 레이어를 출력 
 9  void Draw() const;
10  void Move(unsigned int id, Vector2D<int> new_position);
11  void MoveRelative(unsigned int id, Vector2D<int> pos_diff);
12
13  // 레이어의 z-index를 지정. 음수값이 들어오면 숨김처리.
14  void UpDown(unsigned int id, int new_height);
15  // 레이어를 layer_stack에서 제거해서 출력되지 않도록 숨긴다.
16  void Hide(unsigned int id);
17
18private:
19  // 레이어들을 출력할 writer
20  PixelWriter* writer_{nullptr};
21  // 생성한 레이어 ID를 관리하는 벡터
22  std::vector<std::unique_ptr<Layer>> layers_{};
23  // 레이어를 순서에 맞게 정렬하는 벡터 (출력할 레이어만 저장된다)
24  std::vector<Layer*> layer_stack_{};
25  unsigned int latest_id_{0};
26
27  Layer* FindLayer(unsigned int id);
28};
29// 글로벌로 사용하는 레이어매니저
30extern LayerManager* layer_manager;

함수의 구현부이다. Draw 함수를 보면 for문으로 하나씩 꺼내오는데, iterator 기준으로 맨 앞 (벡터의 0번인덱스) 부터 DrawTo를 호출해주기 때문에 결국 높은 인덱스에 있는 레이어가 늦게 그려져 맨 위에 그려지게된다.

 1void LayerManager::Draw() const {
 2  for (auto layer : layer_stack_) {
 3    layer->DrawTo(*writer_);
 4  }
 5}
 6
 7void LayerManager::UpDown(unsigned int id, int new_height) {
 8  // 음수라면 숨기기
 9  if (new_height < 0) {
10    Hide(id);
11    return;
12  }
13  // insert는 인덱스 it를 받기 때문에 layer_stack_ 의 맨 뒤에 추가한다.
14  if (new_height > layer_stack_.size()) {
15    new_height = layer_stack_.size();
16  }
17  // 레이어를 찾아서 레이어 스택에서 기존 위치, 새 위치 iterator 를 찾아둔다.
18  auto layer = FindLayer(id);
19  auto old_pos = std::find(layer_stack_.begin(), layer_stack_.end(), layer);
20  auto new_pos = layer_stack_.begin() + new_height;
21
22  // 기존 스택에 없던 레이어라면 그냥 새 위치에 추가. 
23  // 중간에 껴도 어차피 기존데이터는 한칸씩 밀린다. 
24  if (old_pos == layer_stack_.end()) {
25    layer_stack_.insert(new_pos, layer);
26    return;
27  }
28
29  // 이미 스택에 있었다면 기존 레이어를 제거하기 때문에 새로운 인덱스 공간을 만들지 않아도 된다. 
30  if (new_pos == layer_stack_.end()) {
31    --new_pos;
32  }
33  // 기존 위치에서 제거되니까 그 뒤에있던 애들은 zindex가 한칸씩 땡겨진다.
34  layer_stack_.erase(old_pos);
35  // 새로운 위치에 저장한다
36  layer_stack.insert(new_pos, layer);
37}
38
39void LayerManager::Hide(unsigned int id) {
40  auto layer = FindLayer(id);
41  auto pos = std::find(layer_stack_.begin(), layer_stack_.end(), layer);
42  if (pos != layer_stack_.end()) {
43    // 그냥 레이어 스택에서 지우면 Draw에서 빠져서 안보이고 size가 줄어든다.
44    layer_stack_.erase(pos);
45  }
46}
47
48Layer* LayerManager::FindLayer(unsigned int id) {
49  // 람다 정의. 찾는 id가 맞다면 리턴
50  auto pred = [id](const std::unique_ptr<Layer>& elem) {
51    return elem->ID() == id;
52  };
53  auto it = std::find_if(layers_.begin(), layers_.end(), pred);
54  if (it == layers_.end()) {
55    return nullptr;
56  }
57  // 원래 find_if는 ptr<Layer> 타입이라 get으로 Layer* 를 꺼내온다. 
58  return it->get(); 
59}
60
61// 전역 레이어매니저
62LayerManager* layer_manager;

배경과 콘솔과 마우스에 적용 #

pixel_writer가 전체 화면에 그리는 용도의 writer이다.

bgwindow와 mouse_window를 생성하고 콘솔은 bgwindow의 writer를 같이 사용하도록 하며 기본 세팅을 완료한다.
각 window마다 layer를 생성하고 zindex를 세팅한다.

마우스가 이동될 때마다 옵저버에서 마우스 이동 후 layer_manager->Draw() 를 호출하여 모든 레이어를 다시 그린다.

 1void MouseObserver(int8_t displacement_x, int8_t displacement_y) {
 2  // 마우스의 interrupt가 발생할 때마다 이동시키고 전체 레이어를 다시 그림
 3  layer_manager->MoveRelative(mouse_layer_id, {displacement_x, displacement_y});
 4  layer_manager->Draw();
 5}
 6
 7extern "C" void KernelMainNewStack(const FrameBufferConfig& frame_buffer_config_ref,
 8                           const MemoryMap& memory_map_ref) {
 9  const int kFrameWidth = frame_buffer_config.horizontal_resolution;
10  const int kFrameHeight = frame_buffer_config.vertical_resolution;
11
12  auto bgwindow = std::make_shared<Window>(kFrameWidth, kFrameHeight);
13  auto bgwriter = bgwindow->Writer();
14
15  // bgwriter에 Desktop을 한번 그림
16  DrawDesktop(*bgwriter);
17  console->SetWriter(bgwriter);
18
19  auto mouse_window = std::make_shared<Window>(kMouseCursorWidth, kMouseCursorHeight);
20  mouse_window->SetTransparentColor(kMouseTransparentColor);
21  // mousewriter에 마우스 한번 그림
22  DrawMouseCursor(mouse_window->Writer(), {0, 0});
23
24  layer_manager = new LayerManager;
25  layer_manager->SetWriter(pixel_writer);
26
27  auto bglayer_id = layer_manager->NewLayer()
28    .SetWindow(bgwindow)
29    .Move({0, 0})
30    .ID();
31
32  mouse_layer_id = layer_manager->NewLayer()
33    .SetWindow(mouse_window)
34    .Move({200, 200})
35    .ID();
36
37  // 레이어 zindex 위치 조정
38  layer_manager->UpDown(bglayer_id, 0);
39  layer_manager->UpDown(mouse_layer_id, 1);
40  layer_manager->Draw();
41}

콘솔과 백그라운드는 bgwriter에 그리고, 마우스는 mouse의 writer에 그린 상태에서 전역 프레임 writer인 pixel_writer에 bgwriter, mousewriter 를 매니저에서 계속해서 덧그리는 형태이다.

그래서 배경이나 마우스 자체는 한번만 그려도 되고, 각 윈도우 writer를 pixel_writer에 그리는것이다.

콘솔은 업데이트를 해도 bgwriter에 하게된다.

bfb8de61-da42-4a07-9299-9919ca19320f


최적화 #

마우스를 움직일때마다 MouseObserver 가 호출되고, layer_manager의 Draw가 호출되며 전체 레이어를 다시 그리게된다.
마우스는 아주 작은데 전체 배경까지 다시 그려야한다는게 비합리적이다.

layer_manager->Draw()
layer_stack_[i]->DrawTo(*writer)
window_->DrawTo(writer, pos_)
writer.Write(xpos, ypos, Window.At(x,y)) * window_size


Shadow Buffer 사용 #

window에는 그림 크기에 맞는 2차원 배열 안에서 픽셀의 색을 관리하며 Window의 At으로 가져와 writer 타입에 맞게 frame_buffer 에 색을 채워넣고 있다.

계속해서 정적인 픽셀을 그리게 되니 직접적으로 화면을 출력하는 버퍼인 frame_buffer 에 memcpy로 복사하면 될 것 같은데 몇가지 문제가 있다.

  • GOP는 UEFI에서 초기화된 포맷 gpt->Mode->Info->PixelFormat 에 맞춰 gop->Mode->frame_buffer 의 데이터를 픽셀로 해석하는데, Window에 저장된 벡터는 색의 정보가 저장된 것이고 gop의 포맷은 모른다.
  • 포맷에 따라 픽셀당 바이트 크기가 달라지기 때문에 memcpy를 하기 위해서는 동적 배열로 처리해야한다.
  • Window.DrawTo는 세팅된 transparent_color 를 참고해서 투과하는 색 위치는 그리지 않도록 구현되어 있는데, 투과색을 그대로 복사할 수는 없다.

FrameBuffer 클래스 #

FrameBufferConfig에 Raw PixelData가 저장된 2차원 배열 버퍼를 동적으로 만들고 복사할 수 있도록 구현한다. (이미 버퍼가 있는 경우엔 기존 config의 버퍼 사용)

 1class FrameBuffer {
 2public:
 3  Error Initialize(const FrameBufferConfig& config);
 4  Error Copy(Vector2D<int> pos, const FrameBuffer& src);
 5
 6  FrameBufferWriter& Writer() { return *writer_; }
 7
 8private:
 9  // config_.frame_buffer가 이 FrameBuffer의 실제 데이터 버퍼이다.
10  // Copy 함수에서는 전달받은 src를 이 데이터 버퍼에 복사한다.
11  FrameBufferConfig config_{};
12  // 현재 프레임 버퍼의 Raw 픽셀 데이터 배열 (이미 config_에 buffer가 있다면 사용하지 않음)
13  std::vector<uint8_t> buffer_{};
14  std::unique_ptr<FrameBufferWriter> writer_{};
15};
16
17// 몇 바이트가 한 픽셀을 이루고 있는지 알기 위해 구현.
18// 포맷마다 고정된 로우데이터 크기를 반환한다.   
19int BitsPerPixel(PixelFromat format) {
20  switch (format) {
21    case kPixelRGBResv8BitPerColor: return 32;
22    case kPixelBGRResv8BitPerColor: return 32;
23  }
24  return -1;
25}

FrameBuffer::Initialize #

 1Error FrameBuffer::Initialize(const FrameBufferConfig& config) {
 2  config_ = config;
 3
 4  // 지원하지 않는 픽셀 포맷인 경우
 5  const auto bits_per_pixel = BitsPerPixel(config_.pixel_format);
 6  if (bits_per_pixel <= 0) {
 7    return MAKE_ERROR(Error::kUnknownPixelFormat);
 8  }
 9
10  if (config_.frame_buffer) {
11    // 버퍼가 이미 있는경우엔 그냥 사이즈 0으로
12    buffer_.resize(0);
13  } else {
14    // 버퍼가 없으면 FrameBufferConfig에 맞게 전체 Raw 픽셀 데이터를 담을 수 있게 할당
15    buffer_.resize(
16      ((bits_per_pixel + 7) / 8)   // 1bit여도 1byte가 필요해서 올림처리
17      * config_.horizontal_resolution * config_.vertical_resolution);
18    // config_ 에도 할당한 버퍼의 주소 세팅
19    config_.frame_buffer = buffer_.data();
20    config_.pixels_per_scan_line = config_.horizontal_resolution;
21  }
22
23  // RGBResv8BitPerColorPixelWriter 타입의 unique_ptr 생성 후 writer_에 저장.
24  // FrameBuffer와 관련된 writer가 저장되기 때문에 이 FrameBuffer에서 소유, 관리 해도된다. 
25  switch (config_pixel_format) {
26  case kPixelRGBResv8BitPerColor:
27    writer_ = std::make_unique<RGBResv8BitPerColorPixelWriter>(config_);
28    break;
29  case kPixelBGRResv8BitPerColor:
30    writer_ = std::make_unique<BGRResv8BitPerColorPixelWriter>(config_);
31    break;
32  default:
33    return MAKE_ERROR(Error::kUnknownPixelFormat);
34  }
35
36  return MAKE_ERROR(Error::kSuccess);
37}

FrameBuffer::Copy #

프레임버퍼 src 전체를 dst의 pos 위치에서 라인단위로 복사하는 함수인데, 복사 범위는 사각형이다.

버퍼에서 복사할 데이터가 x 좌표와 width 에 따라 연속적이지 않을 수 있기 때문에 필요한 부분만 복사하려면 연속적이지 않게 되는 것이다.

915ab4a7-c728-4361-9679-007459253f55

복사할땐 src의 x,y 좌표, dst의 x,y 좌표, 복사할 길이를 잘 계산해서 복사해야한다.

 1Error FrameBuffer::Copy(Vector2D<int> pos, const FrameBuffer& src) {
 2  // 복사 src, dst 의 픽셀 포맷이 다르거나 지원하지 않는 포맷이라면 에러
 3  if (config_.pixel_format != src.config_.pixel_format) {
 4    return MAKE_ERROR(Error::kUnknownPixelFormat);
 5  }
 6  const auto bits_per_pixel = BitsPerPixel(config_.pixel_format);
 7  if (bits_per_pixel <= 0) {
 8    return MAKE_ERROR(Error::kUnknownPixelFormat);
 9  }
10
11  // src, dst의 가로 세로 길이 uint32_t 로 추론된다.
12  // 아래에서 x,y 와 계산한 결과가 음수가 나올 수 있기 때문에 int로 받는게 좋다.
13  const auto dst_w = config_.horizontal_resolution;
14  const auto dst_h = config_.vertical_resolution;
15  const auto src_w = src.config_.horizontal_resolution;
16  const auto src_h = src.config_.vertical_resolution;
17
18  // src, dst의 복사 시작 위치
19  const int dst_x = std::max(pos.x, 0);
20  const int dst_y = std::max(pos.y, 0);
21  const int src_x = std::max(-pos.x, 0);
22  const int src_y = std::max(-pos.y, 0);
23
24  const int bytes_per_pixel = (bits_per_pixel + 7) / 8;
25
26  // w가 x보다 작을 수 있기 때문에 계산 결과가 음수가 나올 수 있어서 int로 캐스팅한다.
27  // src의 위치가 src_2 위치일땐 dst로 계산하고 나머진 src로만 계산하면 된다.  
28  const int bytes_per_copy_line =
29      bytes_per_pixel * std::min(static_cast<int>(src_w - src_x), static_cast<int>(dst_w - dst_x));
30  const int copy_line = std::min(static_cast<int>(src_h - src_y), static_cast<int>(dst_h - dst_y));
31  if (bytes_per_copy_line <= 0 || copy_line <= 0) {
32    // 겹치는 그리기 영역이 없어서 그리지 않고 리턴
33    return MAKE_ERROR(Error::kSuccess);
34  }
35
36  uint8_t* dst_buf = config_.frame_buffer
37      + bytes_per_pixel * (config_.pixels_per_scan_line * dst_y + dst_x);
38  const uint8_t* src_buf = src.config_.frame_buffer
39      + bytes_per_pixel * (src.config_.pixels_per_scan_line * src_y + src_x);
40
41  for (int dy = 0; dy < copy_line; ++dy) {
42    memcpy(dst_buf, src_buf, bytes_per_copy_line);
43    // 라인당 바이트 수를 계속 더하면 x좌표는 고정되고 y좌표만 증가한다.
44    dst_buf += bytes_per_pixel * config_.pixels_per_scan_line;
45    src_buf += bytes_per_pixel * src.config_.pixels_per_scan_line;
46  }
47
48  return MAKE_ERROR(Error::kSuccess);
49}

Window와 Layer에 적용 #

 1extern "C" void KernelMainNewStack(const FrameBufferConfig& frame_buffer_config_ref, ... ) {
 2  FrameBuffer screen;
 3  if (auto err = screen.Initialize(frame_buffer_config)) {
 4    Log(kError, "failed to initialize frame buffer: %s at %s:%d\n",
 5      err.Name(), err.File(), err.Line());
 6  }
 7
 8  layer_manager = new LayerManager;
 9  layer_manager->SetWriter(&screen);
10  // ... 
11  layer_manager->Draw();
12}

layer_manager가 가지고있는 Writer(screen)가 실제 gop의 frame_buffer 주소와 연결되어 있다.

 1void LayerManager::Draw() const {
 2  for (auto layer : layer_stack_) {
 3    layer->DrawTo(*screen_);
 4  }
 5}
 6
 7// 각각의 레이어가 pos_ 에 그림을 그린다. 
 8void Layer::DrawTo(FrameBuffer& screen) const {
 9  if (window_) {
10    window_->DrawTo(screen, pos_);
11  }
12}
13
14void Window::DrawTo(FrameBuffer& dst, Vector2D<int> position) {
15  // transparent_color_ 가 세팅되지 않은 경우에는 복사해서 쓴다
16  if (!transparent_color_) {
17    dst.Copy(position, shadow_buffer_);
18    return;
19  }
20
21  // transparent_color_ 가 세팅되면 특정 부분에만 픽셀을 찍는다.
22  const auto tc = transparent_color_.value();
23  auto& writer = dst.Writer();
24  for (int y = 0; y < Height(); ++y) {
25    for (int x = 0; x < Width(); ++x) {
26      const auto c = At(Vector2D<int>{x, y});
27      if (c != tc) {
28        writer.Write(position + Vector2D<int>{x, y}, c);
29      }
30    }
31  }
32  return;
33}
34
35virtual void WindowWriter::Write(Vector2D<int> pos, const PixelColor& c) override {
36  window_.Write(pos, c);
37}
38
39void Window::Write(Vector2D<int> pos, PixelColor c) {
40  data_[pos.y][pos.x] = c;
41  shadow_buffer_.Writer().Write(pos, c);
42}
43
44void RGBResv8BitPerColorPixelWriter::Write(Vector2D<int> pos, const PixelColor& c) {
45  auto p = PixelAt(pos);
46  p[0] = c.r;
47  p[1] = c.g;
48  p[2] = c.b;
49}
  1. DrawDesktop(*pixel_writer) : 힙 관리 이전 layer_manager 없을 때 콘솔을 사용하기 위해 pixel_writer(gop의 프레임버퍼)에 배경을 먼저 그림
    • DrawDesktop → DrawRectangle → PixelWriter::Write → gop 프레임버퍼에 직접 배경을 그림
  2. console.SetWriter(pixel_writer) : printk를 사용하기 위해 콘솔에 pixel_writer 지정
    • SetWriter → printk, Log … → Console::PutString → PixelWriter::Write → gop 프레임버퍼에 직접 아스키 문자 그림
  3. DrawDesktop(*bgwriter) : layer_manager 를 사용할 배경 window에 미리 한번 Window::Write 를 호출해서 window에 그린다.
    • DrawDesktop → Window::Write → bgwindow에 데스크톱을 그려둠 (쉐도우 버퍼 + 픽셀버퍼)
  4. console->SetWriter(bgwriter) : 이 이후부터는 콘솔도 layer_manager의 배경 window로 같이 관리
    • SetWriter → printk, Log … → Console::PutString → PixelWriter::Write → bgwindow에 아스키 문자를 그림 → LayerManager::Draw → 마우스가 가만히 있을때도 bgwindow 에 출력한 문자열이 반영되도록 함
  5. DrawMouseCursor(*mousewriter) : layer_manager 를 사용할 마우스 window에 Window::Write를 호출해서 미리 그려둔다.
    • DrawDesktop → Window::Write → mousewindow에 커서를 그려둠 (쉐도우 버퍼 + 픽셀버퍼)
  6. layer_manager->Draw() : 레이어로 만들어둔 window 들을 전부 screen(gop 프레임버퍼) 에 순서에 맞게 그린다.
    • MouseObserver → LayerManager::Draw → Layer::DrawTo → Window::DrawTo → (transparent 없는경우) FrameBuffer::Copy → 쉐도우 버퍼에서 gop 프레임버퍼로 memcpy
    • ouseObserver → LayerManager::Draw → Layer::DrawTo → Window::DrawTo → (transparent 있는경우) WindowWriter::Write → Window::Write → PixelWriter::Write → 유효한 픽셀들만 gop 프레임버퍼에 직접 쓰기

콘솔 스크롤 최적화 #

9f7522ce-76be-4624-8454-38302d62a526

콘솔에서 각 라인을 출력할 때마다 시간 측정을 해보면, 스크롤이 발생할 때 (당연하지만) 전체 문자열을 PixelWriter를 이용해 배경색으로 다 덮고 다시 모든 글자를 그려야 돼서 속도가 아주 많이 느려진다

 1void Console::Newline() {
 2  cursor_column_ = 0;
 3  if (cursor_row_ < kRows - 1) {
 4    ++cursor_row_;
 5  } else {
 6  // 이 else 부분이 스크롤 발생 시 실행되는데, 
 7  // 전체를 배경색으로 지우고, 콘솔버퍼에 저장된 글자를 하나씩 다시 쓰고있다. 
 8    for (int y = 0; y < 16 * kRows; ++y) {
 9      for (int x = 0; x < 8 * kColumns; ++x) {
10        writer_->Write(Vector2D<int>{x, y}, bg_color_);
11      }
12    }
13    for (int row = 0; row < kRows - 1; ++row) {
14      memcpy(buffer_[row], buffer_[row + 1], kColumns + 1);
15      WriteString(*writer_, Vector2D<int>{0, 16 * row}, buffer_[row], fg_color_);
16    }
17    memset(buffer_[kRows - 1], 0, kColumns + 1);
18  }
19}

이것도 복사하는 형식으로 변경하면 빨라질 수 있다. 맨 위 한줄의 높이만큼 콘솔 버퍼를 복사하고 맨 아랫줄만 배경색으로 덮으면 된다.

 1// 쉐도우 버퍼의 Move 를 호출 
 2void Window::Move(Vector2D<int> dst_pos, const Rectangle<int>& src) {
 3  shadow_buffer_.Move(dst_pos, src);
 4}
 5
 6// 현재 버퍼 기준으로 Rectangle 영역을 dst_pos 위치로 이동시킨다. 
 7void FrameBuffer::Move(Vector2D<int> dst_pos, const Rectangle<int>& src) {
 8  const auto bytes_per_pixel = BytesPerPixel(config_.pixel_format);
 9  const auto bytes_per_scan_line = BytesPerScanLine(config_);
10
11  if (dst_pos.y < src.pos.y) {
12  // 위로 올리는 경우
13  // 같은 config_ 에 있는 버퍼 안에서 src -> dst 로 이동 (쉐도우 버퍼 복사 방식)
14    uint8_t* dst_buf = FrameAddrAt(dst_pos, config_);
15    const uint8_t* src_buf = FrameAddrAt(src.pos, config_);
16    for (int y = 0; y < src.size.y; ++y) {
17      memcpy(dst_buf, src_buf, bytes_per_pixel * src.size.x);
18      dst_buf += bytes_per_scan_line;
19      src_buf += bytes_per_scan_line;
20    }
21  } else {
22  // 아래로 내리는 경우
23    uint8_t* dst_buf = FrameAddrAt(dst_pos + Vector2D<int>{0, src.size.y - 1}, config_);
24    const uint8_t* src_buf = FrameAddrAt(src.pos + Vector2D<int>{0, src.size.y - 1}, config_);
25    for (int y = 0; y < src.size.y; ++y) {
26      memcpy(dst_buf, src_buf, bytes_per_pixel * src.size.x);
27      dst_buf -= bytes_per_scan_line;
28      src_buf -= bytes_per_scan_line;
29    }
30  }
31}

변경된 Console을 확인해보면 내부에서 관리하는 window 의 쉐도우 버퍼를 이용해서 복사 방식으로 Move 사용한다.

 1void Console::Newline() {
 2  cursor_column_ = 0;
 3  if (cursor_row_ < kRows - 1) {
 4    ++cursor_row_;
 5    return;
 6  }
 7
 8  if (window_) {
 9    // 옮길 범위 지정. 라인당 16픽셀이라 y 16 부터 시작 Console 크기만큼
10    Rectangle<int> move_src{{0, 16}, {8 * kColumns, 16 * (kRows - 1)}};
11    // 해당 범위의 사각형을 0, 0 으로 옮김.
12    // Console에서도 따로 그림을 그릴 수 있는 도화지가 필요하기 때문에 window를 추가한다.
13    // Move가 가능한 상황에서는 기존 문자열을 재출력할 필요가 없어서 buffer_ 를 관리하지 않는다.
14    window_->Move({0, 0}, move_src);
15    FillRectangle(*writer_, {0, 16 * (kRows - 1)}, {8 * kColumns, 16}, bg_color_);
16  } else {
17    // Console에 세팅된 window가 없다면 그냥 기존 방식 그대로 전부 지우고 다시그림
18    FillRectangle(*writer_, {0, 0}, {8 * kColumns, 16 * kRows}, bg_color_);
19    for (int row = 0; row < kRows - 1; ++row) {
20      memcpy(buffer_[row], buffer_[row + 1], kColumns + 1);
21      WriteString(*writer_, Vector2D<int>{0, 16 * row}, buffer_[row], fg_color_);
22    }
23    memset(buffer_[kRows - 1], 0, kColumns + 1);
24  }
25}

대략적으로 속도는 532000 정도에서 25000으로 줄어들었다.

6c9901ef-7c26-4843-b808-ada216215ced


레이어 렌더링 최적화 #

레이어 매니저의 모든 레이어를 다시그릴 필요 없이 변경된 레이어가 영향을 주는 만큼만 다시 그리면 된다.

 1// 관리하고 있는 전체 레이어 중 특정 영역만 다시 그리는 함수
 2void LayerManager::Draw(const Rectangle<int>& area) const {
 3  for (auto layer : layer_stack_) {
 4    layer->DrawTo(*screen_, area);
 5  }
 6}
 7
 8// 특정 레이어만 다시 그리는 함수.
 9// 해당 레이어의 사이즈 만큼 zbuffer상 위에 있는 모든 레이어가 다시 그려져야 한다. 
10// 변경된 레이어는 다시 그려졌을테니 덮고있는 모든 레이어를 그려야한다.
11// 그 변경된 레이어가 다시 그려진 영역만 다시 그리면된다. 
12void LayerManager::Draw(unsigned int id) const {
13  bool draw = false;
14  Rectangle<int> window_area;
15  for (auto layer : layer_stack_) {
16    if (layer->ID() == id) {
17      window_area.size = layer->GetWindow()->Size();
18      window_area.pos = layer->GetPosition();
19      draw = true;
20    }
21    // 스택 상 대상 레이어를 덮는 모든 레이어가 다시그려져야 한다.
22    // window_area : 변경된 레이어가 영향을 주는 영역 
23    if (draw) {
24      layer->DrawTo(*screen_, window_area);
25    }
26  }
27}

Window는 크기만 가지고있고, Layer가 window의 절대좌표(사실 dst기준이긴함)를 가지고 있는 클래스이다.
Window::DrawTo는 dst의 어떤 좌표(pos_)에 현재 window의 프레임 버퍼를 복사하는 함수이다.

이 구조에서 겹치는 영역만 렌더링하기 위해 area가 추가된다.
위의 LayerManager::Draw 함수를 보면 알 수 있듯 다시 그리는 Layer의 window 사이즈에 맞게 나머지 레이어들을 재 렌더링 하기 위해 인자로 전달한다.

그리고 현재 윈도우 영역에서 이벤트를 발생시킨 레이어에게 영향을 받은 영역만 dst.Copy로 전달한다.

 1void Layer::DrawTo(FrameBuffer& screen, const Rectangle<int>& area) const {
 2  if (window_) {
 3    window_->DrawTo(screen, pos_, area);
 4  }
 5}
 6
 7// dst: 레이어 매니저가 관리하는 프레임 버퍼 (렌더링 결과물 위치)
 8// pos: 현재 윈도우의 위치
 9// area: 프레임 버퍼 기준으로 영향을 받은 영역
10void Window::DrawTo(FrameBuffer& dst, Vector2D<int> pos, const Rectangle<int>& area) {
11  if (!transparent_color_) {
12    // 현재 윈도우의 절대주소, 크기
13    Rectangle<int> window_area{pos, Size()};
14    // 현재 윈도우에서 영향을 받은 영역 절대주소
15    Rectangle<int> intersection = area & window_area;
16    // arg[0]: 프레임버퍼 상 현재 윈도우에서 다시 그릴 영역
17    // arg[1]: 현재 윈도우의 프레임 버퍼 
18    // arg[2]: 현재 윈도우에서 영향을 받은 영역 (윈도우 기준 주소)
19    dst.Copy(intersection.pos, shadow_buffer_, {intersection.pos - pos, intersection.size});
20    return;
21  }
22  // ...
23}

Copy 함수는 config_(dst)에 src.config_ 의 버퍼를 영역 계산 후 메모리 카피하는 함수이다.

실 사용에서 dst는 layer_manager가 관리하는 프레임버퍼인 screen 이 된다.

 1// dst_pos: screen 프레임버퍼 상 현재 윈도우에서 다시 그릴 영역
 2// src: 현재 윈도우의 프레임 버퍼
 3// src_area: 현재 윈도우에서 영향을 받은 영역 (윈도우 기준 주소)
 4Error FrameBuffer::Copy(Vector2D<int> dst_pos, const FrameBuffer& src, const Rectangle<int>& src_area) {
 5  if (config_.pixel_format != src.config_.pixel_format) {
 6    return MAKE_ERROR(Error::kUnknownPixelFormat);
 7  }
 8
 9  const auto bytes_per_pixel = BytesPerPixel(config_.pixel_format);
10  if (bytes_per_pixel <= 0) {
11    return MAKE_ERROR(Error::kUnknownPixelFormat);
12  }
13  // 다시 렌더링할 영역 절대주소
14  const Rectangle<int> src_area_shifted{dst_pos, src_area.size};
15  // 현재 윈도우(src) 영역 주소
16  const Rectangle<int> src_outline{dst_pos - src_area.pos, FrameBufferSize(src.config_)};
17  // screen(dst)의 영역 주소
18  const Rectangle<int> dst_outline{{0, 0}, FrameBufferSize(config_)};
19  // 세가지 전부 겹치는 영역인 부분
20  const auto copy_area = dst_outline & src_outline & src_area_shifted;
21  // 잘린만큼 src 위치 이동
22  const auto src_start_pos  = src_area.pos + (copy_area.pos - dst_pos);
23
24  // 복사할 위치의 (screen)버퍼주소. 복사할 타겟을 어디에 붙여넣을건지?
25  uint8_t* dst_buf = FrameAddrAt(copy_area.pos, config_);
26  // 복사대상의 (window)버퍼주소. 그림을 어디서부터 복사할건지?
27  const uint8_t* src_buf = FrameAddrAt(src_start_pos, src.config_);
28
29  for (int y = 0; y < copy_area.size.y; ++y) {
30    memcpy(dst_buf, src_buf, bytes_per_pixel * copy_area.size.x);
31    dst_buf += BytesPerScanLine(config_);
32    src_buf += BytesPerScanLine(src.config_);
33  }
34
35  return MAKE_ERROR(Error::kSuccess);
36}

주의할 점 #

src_area 는 src의 렌더링 범위이기 때문에 src_start_pos = src_area.pos; 로 지정할 수 있다고 생각될 수 있지만, src가 0, 0 밖으로 벗어나 있더라도 클리핑을 하지 않은 순수한 영역을 표시하기 때문에 문제가 발생한다.

fa29f025-90b6-4107-935e-2846faf6d2a0

pos가 음수라면 사실 잘린 부분을 제외하고 시작해야 하지만, 0, 0 부터 복사해서 이상한 그림을 그리게 된다.

151576af-a6fd-4f90-84d4-decf0005bc19

x 좌표를 나가면 예상한대로 src의 0,0 부터 복사되지만 y 좌표를 나가면 까맣게 변한다. 이 버그는 FrameAddrAt 함수에서 uint32_t 인 pixels_per_scan_line와 int 형 음수인 pos.y 를 곱하면서 둘다 unsigned로 캐스팅된 후 곱해져 언더플로우가 발생하는 문제이다.

이것도 클리핑 문제를 해결하면서 src_start_pos 를 무조건 양수가 되게 만들면 해결된다.


백버퍼 #

아무리 최적화를 하더라도 마우스 커서나 윈도우가 깜빡이는건 피할수가 없다.

이유는 현재 LayerManager가 관리하는 screen 이라는 프레임버퍼에 필요한 메모리 영역만 실시간 업데이트를 하는 방식인데, 이녀석은 UEFI에서 초기화된 gop 버퍼로 값을 쓰는순간 화면에 반영되어 버린다.

아무리 속도가 빠르다 하더라도 콘솔 → 윈도우 → 마우스 순서로 렌더링되는 과정에서 실시간으로 한줄씩 memcpy 되기 때문에 어느 순간에는 윈도우나 마우스가 완성되지 않은 순간이 있는 것이다.

이를 방지하기 위해 백버퍼라는 프레임을 둬서 미리 프레임을 그려놓고 screen_에 한번에 반영하는 식으로 동작시켜야 한다.

 1class LayerManager {
 2  // ...
 3private:
 4  FrameBuffer* screen_{nullptr};
 5  mutable FrameBuffer back_buffer_{};
 6};
 7
 8void LayerManager::SetWriter(FrameBuffer* screen) {
 9  screen_ = screen;
10
11  // 기존 screen(gop)과 동일한 설정값의 다른 버퍼를 생성 
12  FrameBufferConfig back_config = screen->Config();
13  back_config.frame_buffer = nullptr;
14  // frame_buffer가 nullptr 일때 초기화하면 새로운 버퍼가 만들어진다.
15  back_buffer_.Initialize(back_config);
16}
17
18void LayerManager::Draw(unsigned int id) const {
19  bool draw = false;
20  Rectangle<int> window_area;
21  for (auto layer : layer_stack_) {
22    if (layer->ID() == id) {
23      window_area.size = layer->GetWindow()->Size();
24      window_area.pos = layer->GetPosition();
25      draw = true;
26    }
27    if (draw) {
28      // 백버퍼에 모든 레이어의 window 들을 그려놓고 
29      layer->DrawTo(back_buffer_, window_area);
30    }
31  }
32  // 변경된 크기만큼만 screen_에 복사한다.
33  screen_->Copy(window_area.pos, back_buffer_, window_area);
34}
comments powered by Disqus