StudyShape::content()에서 호출한 네 개의 Public API가 ThorVG 내부에 어떤 상태를 만드는지 확인했다.
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;
}
Shape::gen()
→ ShapeImpl 생성
→ appendRect()가 RenderShape.path 구성
→ fill()이 RenderShape.color 설정
→ Canvas::add()가 ShapeImpl을 root Scene에 등록
이 시점의 ShapeImpl에는 사각형의 Geometry와 Appearance를 나타내는 backend 독립적인 렌더링 입력 데이터와, 이를 Scene에서 관리하기 위한 상태가 구성되어 있다.
RenderShape에는 사각형 Path와 빨간색 Fill이 저장되어 있고, Paint::Impl에는 Path | Color | Transform 변경 flag가 누적되어 있다. But 아직 Shape의 renderer와 backend 전용 데이터인 rd는 nullptr이다.
오늘은 content()가 반환된 이후, 이 Shape가 Software Renderer를 거쳐 SDL Surface의 실제 픽셀이 되기까지 추적해 보았다.
1. 전체 Flow
Canvas가 Scene의 변경 사항을 확인하면 → Software Renderer가 Shape를 그릴 수 있는 형태로 준비. → Rasterizer가 target buffer에 픽셀을 기록한다. → example이 완성된 target을 Window에 표시
| 단계 | 역할 |
|---|---|
| Scene 구성 | Public API로 Shape 데이터를 만들고 Canvas에 등록 |
| Update / Prepare | 변경된 Shape를 선택된 backend가 처리할 데이터로 준비 |
| Render / Present | target에 픽셀을 기록하고 Window에 표시 |
여기서 어제 확인한 Paint와 RenderShape를 구성, canvas에 add하는것이 Scene구성 항목에 해당하는 부분이다. 오늘 범위는 SwShapeTask 생성 까지.
2. Canvas::draw()
StudyShape::content()가 반환되면 example 공통 실행부인 Window::ready()가 첫 렌더링을 시작한다.
content()와 draw()는 Canvas 객체를 통해 shape data 그릴 대상으로 연결한다고 보면 될것같다.
bool ready()
{
if (!canvas) return false;
if (!example->content(canvas, width, height)) return false;
// initiate the first rendering before window pop-up.
if (!verify(canvas->draw())) return false;
if (!verify(canvas->sync())) return false;
return true;
}
Canvas::draw()라는 이름만 보면 곧바로 target에 픽셀을 기록할 것 같지만, 내부에서는 먼저 Canvas의 상태를 확인한다.
Result draw(bool clear)
{
if (status == Status::Drawing) {
return Result::InsufficientCondition;
}
if (status == Status::Painting || status == Status::Damaged) {
update();
}
if (status != Status::Updating) {
return Result::InsufficientCondition;
}
if (clear && !renderer->clear()) return Result::InsufficientCondition;
if (!renderer->preRender()) return Result::InsufficientCondition;
if (!PAINT(scene)->render(renderer) || !renderer->postRender()) {
return Result::InsufficientCondition;
}
status = Status::Drawing;
return Result::Success;
}
tvgCanvas.h — Canvas::Impl::draw()
Canvas::add()가 호출되면서 Canvas 상태는 Painting이 된다. (상태를 바꾼다.)
StudyShape가 Canvas::update()를 명시적으로 호출하지 않아도 draw()가 먼저 update를 수행.
새로 추가되거나 변경된 Shape를 먼저 처리하는 것이라 보면 될 것 같다. 처리된 결과를 그림
Canvas::add(shape)
→ Canvas status = Painting
Canvas::draw()
→ Painting 상태 확인→ update() 암묵적 호출→ render() 수행
draw()는 내부적으로 Update, Render 두 작업을 묶는 상위 진입점이라고 볼 수 있을 것 같다.
update / prepare → backend가 그릴 수 있도록 데이터 준비
render → 준비된 데이터를 target에 기록
3. Canvas는 root Scene부터 순회
Canvas::Impl::update()는 identity matrix와 opacity 255를 시작값으로 root Scene의 update를 호출한다.
clips.clear();
auto m = tvg::identity();
PAINT(scene)->update(renderer, m, clips, 255, flag);
tvgCanvas.h — Canvas::Impl::update()
여기서 Canvas가 자신에게 추가된 Shape를 직접 하나씩 처리하는 것은 아니다. Canvas 내부의 root Scene에서 출발한다.
SceneImpl::update()는 자신이 가진 Paint 목록을 순회하며 각 Paint의 update를 호출한다.
for (auto paint : paints) {
PAINT(paint)->update(renderer, transform, clips, opacity, flag, false);
}
tvgScene.h — SceneImpl::update()
StudyShape의 root Scene에는 Paint가 하나만 등록되어 있는 상태이다. (빨간색 사각형 1개만 등록)
Canvas::Impl
ㄴ root SceneImpl
ㄴ paints[0] → ShapeImpl
그렇기 때문에 paints[0]인 빨간 사각형의 update가 호출된다.
Scene 안에 여러 Paint가 있거나 Scene이 중첩되더라도 같은 방식으로 Paint tree를 순회할 수 있다.
Canvas는 root Scene을 기준으로 렌더링할 Paint tree를 관리하고, 그 tree의 변경 사항을 현재 Canvas에 연결된 Renderer로 전달하는 실행 흐름도 관리한다. (Shape를 담는 컨테이너정도보다 더 큰 개념이다)
4. Paint::Impl::update() : 모든 Paint의 공통 처리
Scene에서 자식 Paint의 update를 호출하면 먼저 Paint::Impl::update()에 들어간다.
update에서는 Shape뿐 아니라 Scene, Picture, Text 등 Paint 계열에 공통으로 필요한 처리를 담당한다.
RenderData Paint::Impl::update(RenderMethod* renderer,
const Matrix& pm,
Array<RenderData>& clips,
uint8_t opacity,
RenderUpdateFlag flag,
bool clipper)
{
bool ret;
PAINT_METHOD(ret, skip((flag | renderFlag)));
if (ret) return rd;
if (this->renderer != renderer) {
renderer->ref();
this->renderer = renderer;
}
if (renderFlag & RenderUpdateFlag::Transform) {
tr.update();
}
opacity = MULTIPLY(opacity, this->opacity);
PAINT_METHOD(ret, update(renderer,
pm * tr.m,
clips,
opacity,
(flag | renderFlag),
clipper));
renderFlag = RenderUpdateFlag::None;
return rd;
}
tvgPaint.cpp — Paint::Impl::update()
전체 구현에는 masking, clipping, composition 처리도 포함되어 있지만, 현재 StudyShape에는 단순 사각형이므로 해당 처리들이 포함되어있지 않다. 핵심 흐름만 보면
변경 flag가 있는지 확인
→ Paint와 현재 Renderer 연결
→ transform과 opacity 계산
→ 구체 Paint 타입의 update() 호출
→ 처리한 renderFlag 초기화
저번 확인에서 ShapeImpl의 renderer는 아직 nullptr이었다.
Canvas::add() 직후
renderer → nullptr
rd → nullptr
이 값은 Paint::Impl::update()에서 처음으로 현재 Canvas의 Renderer를 가리키게 된다.
SW 모드로 실행했으므로 실제 Renderer 객체는 SwRenderer다.
Paint::Impl::renderer
nullptr → SwRenderer
그리고 Public API 호출로 누적한 Path | Color | Transform flag가 구체 Shape의 update에 전달된다. 처리가 끝나면 renderFlag는 None으로 초기화된다.
Public API를 호출할 때마다 즉시 전체 Shape를 다시 계산하는 것이 아니라, 무엇이 바뀌었는지를 flag로 모아 두었다가 update 단계에서 Renderer에 전달하는 구조.
5. ShapeImpl::update() — Software Renderer로 넘어가는 지점
impl.rd = renderer->prepare(
rs, impl.rd, transform, clips, opacity, flag, clipper
);
tvgShape.h — ShapeImpl::update()
여기서 rs는 RenderShape. 사각형 Path와 빨간색 Fill 정보가 들어 있다.
renderer의 타입은 공통 인터페이스인 RenderMethod*이지만, 현재 Canvas가 가진 실제 객체는 SwRenderer다. 따라서 renderer->prepare()를 호출하면 실제로 SwRenderer::prepare()가 실행된다.
ShapeImpl::update()
→ renderer->prepare(RenderShape, ...)
→ SwRenderer::prepare()
이 지점이 ThorVG의 공통 Shape 처리에서 Software Renderer 구현으로 넘어가는 경계다.
6. SwRenderer::prepare() — SwShapeTask 생성
SwRenderer::prepare()는 기존 backend 데이터가 있는지 확인한다.
auto task = static_cast<SwShapeTask*>(data);
if (task) {
task->done();
} else {
task = new SwShapeTask;
task->rshape = &rshape;
}
tvgSwRenderer.cpp — SwRenderer::prepare()
첫 draw에서는 Paint::Impl::rd가 nullptr이므로 새로운 SwShapeTask를 생성한다.
첫 update
impl.rd == nullptr
→ new SwShapeTask
→ task.rshape = &RenderShape
→ impl.rd = SwShapeTask
update가 끝난 뒤 ShapeImpl의 상태는 다음과 같이 연결된다.
RenderShape가 Software Renderer 전용 데이터로 바뀌는 것은 아니다. RenderShape는 그대로 유지되고, Software Renderer가 이를 참조하는 SwShapeTask를 만든다.
7. 정리
Canvas::draw()
→ root Scene의 Paint 순회
→ Paint 공통 상태 처리
→ ShapeImpl::update()
→ SwRenderer::prepare()
→ SwShapeTask 생성
요약하면, 위와 같이 content()에서 만든 Shape는 Canvas의 root Scene에 저장되고,
이후 draw()가 같은 Scene을 순회하며 변경된 Shape를 현재 Renderer에 전달한다.
SW 모드에서는 ShapeImpl::update()의 renderer->prepare() 호출을 통해 SwRenderer로 넘어가고,
RenderShape를 참조하는 SwShapeTask가 생성된다.
backend 독립적인 Shape 데이터
RenderShape
->
Software Renderer의 작업 데이터
SwShapeTask
이번 글에서는 Shape가 Software Renderer의 작업으로 준비되는 지점까지 확인했다.
다음 글에서는 SwShapeTask가 실제로 실행된 뒤, 사각형이 SDL Surface의 픽셀로 기록되고 Window에 표시되는 과정을 이어서 확인할 예정이다.
댓글
Discussion 원문