← 블로그 목록

[Study Note] Example 예제가 실제로 화면에 그려지기까지의 코드 흐름 추적하기. - 2

@jeongsunyong
  • #Study
목차

어제는 example의 StudyShape::content()가 호출되는 지점까지 확인했다.

bool content(tvg::Canvas* canvas, uint32_t w, uint32_t h) override
{
    auto shape = tvg::Shape::gen();
    shape->appendRect(100, 100, 300, 200);
    shape->fill(255, 0, 0);
    canvas->add(shape);

    return true;
}

이 네 개의 Public API가 ThorVG 내부에 어떤 상태를 만드는지 추적.

image

1. 네 개의 Public API

화면에 벡터 도형 하나를 출력하려면 필요한 최소의 정보들은 다음과 같다.

렌더링 입력 질문StudyShape의 선택Public API
어떤 그래픽 객체인가?ShapeShape::gen()
어떤 기하를 갖는가?300×200 사각형appendRect()
어떤 외형을 갖는가?빨간색 단색 Fillfill()
어느 Scene에서 그릴 것인가?Canvas의 root SceneCanvas::add()

이를 한 줄로 정리하면

그래픽 객체 생성 → Geometry 정의 → Appearance 정의 → 렌더링 Scene에 등록

이 단계에서는 Renderer가 나중에 처리할 backend 독립적인 장면 데이터를 구성한다. 직접 픽셀을 기록하지 않음 Shape의 geometry와 appearance를 구성하고, 변경 flag를 누적한 뒤 Canvas의 root Scene에 ownership과 함께 등록하는 과정이다.

image

2. Shape::gen()

Public API의 반환 타입은 Shape*이고, 실제 구현에서는 ShapeImpl을 생성한다.

Shape* Shape::gen() noexcept
{
    return new ShapeImpl;
}

tvgShape.cpp — Shape::gen()

ShapeImplShape를 상속하고, 내부에 Paint::ImplRenderShape를 가진다.

struct ShapeImpl : Shape
{
    Paint::Impl impl;
    RenderShape rs;
    uint8_t opacity;
};

tvgShape.h — ShapeImpl

Paint::Impl

Paint::Impl은 Shape뿐 아니라 다른 Paint 계열에도 필요한 공통 관리 상태를 가진다.

  • parent : 어느 Scene에 소속됐는가
  • renderer : 어느 Renderer와 연결됐는가
  • rd : backend가 준비한 RenderData
  • transform : 이동·회전·크기 변환
  • opacity : 투명도
  • renderFlag : 어떤 속성이 변경됐는가
  • refCnt : 객체 수명과 ownership

tvgPaint.h — Paint::Impl

RenderShape

RenderShape는 Shape 고유의 렌더링 입력을 가진다.

struct RenderShape
{
    RenderPath path;
    Fill* fill = nullptr;
    RenderColor color{};
    RenderStroke* stroke = nullptr;
    FillRule rule = FillRule::NonZero;
};

tvgRender.h — RenderShape

중간 정리하자면, ShapeImplPaintRenderShape를 가진다. 그래서 Paint와 RenderShape은 그래서 무엇인지? 상태 관리와 렌더링 입력이라는게 무엇인가? →

Paint::Impl
: 이 객체를 장면에서 어떻게 관리할 것인가?

RenderShape
: 이 도형이 어떤 모양과 스타일을 갖는가?

라고 할 수 있다.

표로 정리해보면

구분Paint::ImplRenderShape
핵심 질문이 객체를 어떻게 관리하는가?이 도형은 어떻게 생겼는가?
적용 범위모든 Paint 계열Shape
Scene 관계parent, ownership없음
객체 수명refCnt직접 관리하지 않음
변경 추적renderFlag실제 path/color 상태
기하 정보없음RenderPath
색상/선공통 opacity/blendfill, stroke, fill rule

결국 ShapeImpl은 Scene에 소속되는 그래픽 객체Renderer가 그릴 벡터 도형 데이터를 결합한 실제 Shape 객체다.

Public API의 관점에서는 Shape에는 내부 필드가 노출되지 않고, client는 Shape 인터페이스만 사용하면 된다. ThorVG 내부에서 ShapeImplRenderShape를 이용해 실제 상태를 관리.


3. appendRect() - 사각형도 내부에서는 Path다

image

Shape::appendRect()ShapeImpl::addRect()에 위임.

Result Shape::appendRect(float x, float y, float w, float h,
                         float rx, float ry, bool cw) noexcept
{
    to<ShapeImpl>(this)->addRect(x, y, w, h, rx, ry, cw);
    return Result::Success;
}

tvgShape.cpp — Shape::appendRect()

ShapeImpl::addRect()RenderShape::path에 사각형을 추가하고 Path update flag를 표시한다.

void addRect(float x, float y, float w, float h,
             float rx, float ry, bool cw)
{
    rs.path.addRect(x, y, w, h, rx, ry, cw);
    impl.mark(RenderUpdateFlag::Path);
}

tvgShape.h — ShapeImpl::addRect()

example (빨간 사각형) 에서는 radius를 전달하지 않았으므로 sharp rectangle 분기로 들어간다.

shape->appendRect(100, 100, 300, 200);

이 사각형은 내부에서 command 5개와 point 4개로 표현된다.

MoveTo → (400, 100)
LineTo → (400, 300)
LineTo → (100, 300)
LineTo → (100, 100)
Close

실제 구현은 다음과 같다.

cmds[0] = PathCommand::MoveTo;
cmds[1] = cmds[2] = cmds[3] = PathCommand::LineTo;
cmds[4] = PathCommand::Close;

pts[0] = {x + w, y};
pts[1] = {x + w, y + h};
pts[2] = {x, y + h};
pts[3] = {x, y};

tvgRender.cpp — RenderPath::addRect()

호출 전후의 상태는 다음과 같다.

rs.path.cmds.count: 0 → 5
rs.path.pts.count:  0 → 4
renderFlag:         Path 추가

여기서 한가지, Public API에는 appendRect()라는 함수가 있지만, 내부 입력 모델에서는 사각형도 결국 Path command와 point로 표현된다. 이후 Software Renderer가 단순 사각형을 별도의 fast-track으로 처리할 수 있지만, 이는 Renderer 단계의 최적화라고 한다.

입력 모델
사각형 → RenderPath

Renderer 최적화
조건이 맞으면 일반 RLE 대신 빠른 rect raster 경로

4. fill() — 단색 색상 저장.

Shape::fill(r, g, b, a)ShapeImpl에 위임한다.

Result Shape::fill(uint8_t r, uint8_t g,
                   uint8_t b, uint8_t a) noexcept
{
    to<ShapeImpl>(this)->fill(r, g, b, a);
    return Result::Success;
}

tvgShape.cpp — Shape::fill()

내부 구현에서는 기존 Gradient가 있다면 제거하고 단색 RGBA를 RenderShape::color에 저장한다.

void fill(uint8_t r, uint8_t g, uint8_t b, uint8_t a)
{
    if (rs.fill) {
        delete(rs.fill);
        rs.fill = nullptr;
        impl.mark(RenderUpdateFlag::Gradient);
    }

    if (r == rs.color.r && g == rs.color.g &&
        b == rs.color.b && a == rs.color.a) return;

    rs.color = {r, g, b, a};
    impl.mark(RenderUpdateFlag::Color);
}

tvgShape.h — ShapeImpl::fill()

StudyShape의 상태 변화는 다음과 같다.

호출 전 color: {0, 0, 0, 0}
호출 후 color: {255, 0, 0, 255}
rs.fill:       nullptr
renderFlag:    Color 추가

단색과 Gradient는 다음처럼 구분된다. (rs.fill 이 nullptr이냐 아니냐)

단색 Fill
→ rs.fill == nullptr
→ rs.color에 RGBA 저장

Gradient Fill
→ rs.fill이 LinearGradient 또는 RadialGradient를 가리킴

참고로, 현재는 단색 칠을 했기 때문에 Fill을 확인했지만 실제로 Fill은 Shape의 여러 표현 속성 중 하나일 뿐이다. Shape는 geometry만 가지는 것이 아니라 다음과 같은 여러 표현 정보를 함께 가진다는 점 참고.

Shape
ㄴ Path Geometry
ㄴ Solid / Gradient Fill
ㄴ Fill Rule
ㄴ Stroke
   ㄴ Width
   ㄴ Cap
   ㄴ Join
   ㄴ Miter Limit
   ...

5. RenderUpdateFlag — 무엇이 바뀌었는지 기록하기

appendRect()fill()은 데이터를 바꾸는 것과 함께 update flag를 표시한다.

enum RenderUpdateFlag : uint16_t {
    None = 0,
    Path = 1,
    Color = 2,
    Gradient = 4,
    Stroke = 8,
    Transform = 16,
    // ...
};

tvgRender.h — RenderUpdateFlag

Paint::Impl::mark()는 bit OR로 변경 사항을 누적한다.

void mark(RenderUpdateFlag flag)
{
    renderFlag |= flag;
}

tvgPaint.h — Paint::Impl::mark()

appendRect() 호출 후 Paint::Impl::renderFlag에는 Path가 표시된다. 이어서 fill()을 호출하면 Color flag가 OR 연산으로 누적되므로, Canvas에 추가하기 전 상태는 Path | Color가 된다.

이후 Canvas::add() 과정에서 Shape가 root Scene 좌표계로 편입되면서 Transform flag도 추가된다. 따라서 content()가 끝날 때의 최종 변경 상태는 Path | Color | Transform이다.

appendRect() → Path
fill()       → Color

API를 호출할 때마다 즉시 그림을 다시 만드는 것이 아니라, 어떤 속성이 변경됐는지 기록해 두고 이후 update 단계에서 필요한 부분을 준비하는 것.


6. Canvas::add() — Shape는 어느 Scene에 들어갈까

: 위 Fill까지 수행했을 때, Fill까지 완료된 Shape는 아직 어디에도 소속되지 않은 독립 객체이다. 어떻게 생겼는가는 정의됐지만 어느 화면에 그릴 것인가는 결정되지 않은 상태이다.

ThorVG가 어떤 Paint를 렌더링해야 하는지 알기 위해서는 해당 Paint가 Canvas가 관리하는 장면에 포함돼야 한다. 이 역할을 하는 API가 Canvas::add().

Canvas는 내부에 root Scene을 하나 가진다.

struct Canvas::Impl
{
    Scene* scene;
    RenderMethod* renderer;
    RenderRegion vport;
    Array<RenderData> clips;
    Status status;
};

tvgCanvas.h — Canvas::Impl

Canvas::Impl 생성 시 Scene::gen()으로 root Scene을 생성한다.

Impl() : scene(Scene::gen())
{
    scene->ref();
}

독립된 Shape는 Canvas의 root Scene에 등록 → 이후 update/draw 대상이 된다.

따라서 Canvas는 개념적으로 다음 구조를 가진다.

Canvas
ㄴ Canvas::Impl
   ㄴ Renderer
   ㄴ  Status
   ㄴ  Root Scene
      ㄴ  Paint List

add()의 실제 위임 경로

Canvas::add(shape)
→ Canvas::Impl::add(shape)
→ root Scene::add(shape)
→ SceneImpl::insert(shape)

Canvas::Impl::add()은 먼저 Paint가 다른 Renderer에 연결돼 있는지와 현재 Canvas가 Drawing 중인지 검사한다.

Result add(Paint* target, Paint* at)
{
    if (PAINT(target)->renderer &&
        PAINT(target)->renderer != renderer) {
        return Result::InsufficientCondition;
    }

    if (status == Status::Drawing) {
        return Result::InsufficientCondition;
    }

    status = Status::Painting;
    return scene->add(target, at);
}

tvgCanvas.h — Canvas::Impl::add()

그다음 SceneImpl::insert()가 Shape를 실제 Paint 목록에 넣는다.

if (timpl->parent) {
    return Result::InsufficientCondition;
}

target->ref();
timpl->mark(RenderUpdateFlag::Transform);
paints.push_back(target);
timpl->parent = this;

tvgScene.h — SceneImpl::insert()

StudyShape가 추가되면 상태가 다음처럼 바뀐다.

Canvas status:          Damaged → Painting
root Scene Paint count: 0 → 1
Shape parent:           nullptr → root Scene
Shape refCnt:           0 → 1
Shape renderFlag:       Path | Color → Path | Color | Transform
Shape renderer:         아직 nullptr

Renderer가 아직 nullptr인 이유는 add()가 Scene 등록까지만 담당하기 때문이다. Shape와 실제 SwRenderer의 연결 및 backend 전용 RenderData 준비는 이후 update 단계에서 일어난다.

7. API 호출이 끝난 시점의 전체 상태

StudyShape::content()가 반환되기 직전 상태를 하나로 합치면 다음과 같다.

image

8. Next

내부 구현을 따라가 보니 Example에서 빨간 사각형을 그리고자하는 API들은 바로 픽셀을 그리는 함수가 아니라, backend 독립적인 Shape 데이터를 구성하고 이를 Canvas의 렌더링 Scene에 등록하는 과정이었다. 다음 렌더링 루프를 위한 데이터를 단계적으로 구성하는 과정이라고 볼수도 있을 것 같다.

Public API
→ ShapeImpl 생성
→ RenderPath에 Geometry 저장
→ RenderShape에 Appearance 저장
→ Update flag 기록
→ Canvas root Scene에 ownership과 함께 등록

이 지점까지는 backend 독립적인 상태 구성이다. 아직 Software Rasterizer는 등장하지 않는다.

다음 글에서는 Window::ready()가 호출하는 Canvas::draw() 내부를 분석해 볼 것이다.

댓글

Discussion 원문