TinyPiXOS 조사
조사 대상: TinyPiXOS/TinyPiXOS (master 브랜치, 2026-08 기준)
1. TinyPiXOS란
TinyPiXOS는 중국에서 개발된 오픈소스 경량 임베디드 데스크톱 OS다. 산업용 터치스크린, 의료기기 HMI 등을 타깃으로 하며, 가장 큰 특징은 X11/Wayland 그래픽 스택을 완전히 배제하고 순수 C/C++로 엔드투엔드 그래픽 스택을 재구축했다는 점이다. 구성은 다음과 같다.
- TpWM — 자체 개발 윈도우 매니저 (합성/입력 라우팅)
- TpGUI — 위젯 기반 GUI 프레임워크 (Qt와 유사한 API 설계:
TpWidget,TpPainter, signal-slot) - TpCore / TpUtils — 코어 및 유틸리티 라이브러리
이 중 TpGUI의 모든 픽셀은 ThorVG가 그린다. Cairo나 Skia 같은 별도 2D 라이브러리 없이, 도형·텍스트·이미지·이펙트·애니메이션 전부를 ThorVG 단일 엔진으로 처리하는, “ThorVG를 GUI 프레임워크의 유일한 래스터라이저로 채택한” 드문 사례다.
2. 빌드 통합 방식 — 서브모듈 + 정적 링크
TinyPiXOS는 ThorVG를 git 서브모듈로 가져와 정적 라이브러리로 빌드해 넣는다.
# .gitmodules
[submodule "3rdparty/thorvg"]
path = 3rdparty/thorvg
url = https://gitee.com/tinypixos/thorvg.git
deps.mk의 빌드 레시피를 보면 ThorVG의 어떤 기능을 켜서 쓰는지 정확히 알 수 있다.
# deps.mk
thorvg:
cd $(src_dir)/thorvg; \
meson setup builddir \
-Dbuildtype=release \
-Dloaders="all" -Dengines="sw,gl" \
-Dsavers="all" \
-Dexamples=false \
-Dlog=false \
--default-library=static \
...
cp $(prefix)/thorvg/include/* $(core_root)/src/include_p/TpGUI/thorVG/; \
cp $(prefix)/thorvg/lib/libthorvg.a $(core_root)/src/depend_lib/static/$(libDir)/;
주목할 점:
-Dloaders="all"— SVG, Lottie, PNG, JPG, WebP 로더를 전부 활성화. TpGUI의 이미지 시스템이 ThorVG 로더에 전적으로 의존하기 때문이다 (후술).-Dengines="sw,gl"— SW 래스터라이저를 기본으로 쓰되, GL 백엔드도 빌드해 둔다. 실제 코드에는USE_OPENGL매크로로tvg::GlCanvas전환 경로가 준비되어 있다 (아직 실험 단계, 주석으로 “暂时无用/당분간 미사용” 표기).- 정적 링크 — 빌드 산출물
libthorvg.a와 헤더(thorvg.h)를 트리 내부(src/include_p/TpGUI/thorVG/)로 복사해 TpGUI에 직접 임베드한다. 헤더가 저장소에 커밋되어 있어 TpGUI 소스 어디서든#include "thorVG/thorvg.h"로 접근한다.
포크 저장소(TinyPiXOS/thorvg)를 업스트림과 비교(gh api repos/thorvg/thorvg/compare/main...TinyPiXOS:thorvg:main)해 보면 ahead 0 / behind 392 — 즉 자체 패치 없이 업스트림 v1.0 개발 라인의 스냅샷을 미러링해 버전만 고정(pinning)해서 쓰고 있다. ThorVG의 공개 API만으로 OS급 GUI 스택을 구성할 수 있었다는 뜻이기도 하다.
3. 핵심 아키텍처 — 위젯 트리를 ThorVG Scene 트리로 1:1 미러링
TinyPiXOS 통합의 가장 흥미로운 설계는 리테인드 모드(retained-mode) Scene 그래프 활용이다. Qt의 QPainter가 매 프레임 즉시 모드(immediate-mode)로 픽셀을 찍는 것과 달리, TpGUI는 위젯 계층 구조 자체를 ThorVG의 tvg::Scene 트리로 그대로 미러링한다.
3.1 위젯마다 Scene 하나
모든 TpWidget은 생성 시 자신의 tvg::Scene을 소유한다.
// src/src/TpGUI/GUI/TpWidget.cpp
widgetData->tvgScene = tvg::Scene::gen();
widgetData->tvgScene->ref();
부모-자식 관계가 맺어지면 Scene도 부모 Scene에 push되어 트리를 형성한다.
// TpWidget.cpp — setParent 경로
lastParentWidgetData->tvgScene->remove(widgetData->tvgScene); // 이전 부모에서 분리
parentWidgetData->tvgScene->push(widgetData->tvgScene); // 새 부모에 연결
위젯의 이동/클리핑도 픽셀 연산이 아니라 Scene 변환으로 처리된다.
// src/include_p/TpGUI/GUI/TpWidget_p.h
widgetData->tvgScene->translate(x, y); // 위젯 이동 = Scene 변환
auto clipper = tvg::Shape::gen();
widgetData->tvgScene->clip(std::move(clipper)); // 위젯 경계 = Scene 클리핑
// 가시성 역시:
widgetData->tvgScene->visible(visible);
즉 위젯 트리 = Scene 트리이며, 레이아웃 변경·투명도·클리핑·표시 여부가 전부 ThorVG의 Scene 속성으로 위임된다.
3.2 화면(Screen)마다 SwCanvas 하나, 타깃은 윈도우 매니저의 공유 버퍼
최상위 TpScreen이 tvg::SwCanvas를 소유하고, 루트 Scene을 캔버스에 push한다.
// src/src/TpGUI/GUI/TpScreen.cpp — 생성자
uint32_t cores = std::thread::hardware_concurrency();
tvg::Initializer::init(cores / 2); // CPU 코어 절반을 ThorVG 렌더 스레드로
screenData->swCanvas = tvg::SwCanvas::gen(tvg::EngineOption::SmartRender);
screenData->swCanvas->push(screenData->tvgScene);
두 가지가 핵심이다.
Initializer::init(cores / 2)— ThorVG 내부 태스크 스케줄러에 CPU 코어의 절반을 할당. 임베디드 환경에서 UI 렌더링이 앱 로직과 코어를 나눠 쓰도록 한 균형점이다.EngineOption::SmartRender— ThorVG v1.0의 부분 렌더링(dirty region) 최적화를 활성화. 리테인드 Scene 구조와 결합하면, 프레임마다 변경된 Paint의 바운딩 영역만 다시 래스터라이즈된다. GPU 없는 산업용 보드에서 전체 화면 리드로우를 피하는 결정적 최적화다.
캔버스의 출력 대상은 TpWM 윈도우 매니저가 관리하는 공유 메모리 서피스다. 리사이즈 시점에 바인딩된다.
// TpScreen.cpp — onResizeEvent
screenData->swCanvas->target(
(uint32_t *)screenData->wmSurface->matrix(), // TpWM 공유 서피스의 픽셀 버퍼
surfaceWidth, surfaceWidth, surfaceHeight,
tvg::ColorSpace::ARGB8888);
ThorVG가 윈도우 매니저의 버퍼에 직접 그리므로, 중간 블리팅(blit) 단계나 별도 프레임버퍼 복사가 없다. “X11/Wayland 없이 초경량 스택”이라는 TinyPiXOS의 주장에서 ThorVG가 사실상 컴포지터 직전 단계까지 담당하는 셈이다.
3.3 프레임 파이프라인 — update()에서 픽셀까지
전체 흐름을 코드로 추적하면 다음과 같다.
위젯.update()
→ TpApp::postUpdateEvent() // 더티 사각형을 큐에 적재
→ TpApp::run() 이벤트 루프 (16ms 주기 ≈ 60fps)
→ DownUpdateCommand() // ① 더티 렉트 병합 ② 페인트 ③ 래스터라이즈
→ tinyPiX_wf_update() // TpWM에 갱신 영역 통지 → 합성
DownUpdateCommand(src/include_p/TpGUI/GUI/TpApp_p.h)가 프레임의 심장이다.
// ① 윈도우별로 더티 사각형 병합
hasRect.unions(taskRect);
// ② 위젯 트리 재귀 페인트 (Scene 재구성)
if (updateWidgetIter.first->visible())
drawWidget(paintInput, updateWidgetIter.first);
// ③ ThorVG 래스터라이즈 — 프레임당 단 한 번
tvg::SwCanvas *topCanvas = (tvg::SwCanvas *)topScreen->canvasPtr();
topCanvas->update();
topCanvas->draw();
topCanvas->sync();
// ④ 윈도우 매니저에 변경 영역만 통지
tinyPiX_wf_update(updateInfo.first, updateRect.x(), updateRect.y(), ...);
여러 위젯이 같은 프레임에 update()를 호출해도 사각형이 병합되고 draw()/sync()는 화면당 한 번만 실행된다. ThorVG의 비동기 렌더링 모델(draw()는 태스크 디스패치, sync()는 완료 대기)을 프레임 경계에 정확히 맞춰 쓰는 교과서적 사용법이다.
drawWidget의 재귀 구조도 눈여겨볼 만하다.
// TpObjectFunction.hpp
static void drawWidget(TpPaintEventInput &input, TpWidget *obj)
{
tvg::Scene *childScene = (tvg::Scene *)obj->scenePtr();
childPainter->setScene(topCanvas, childScene);
childScene->remove(); // 이전 프레임 Paint 전부 제거
parentWidgetData->tvgScene->push(widgetData->tvgScene); // z-order 재정렬
bool ret = obj->onPaintEvent(&event); // 위젯이 Painter로 Scene 재구성
childScene->opacity(255 * obj->windowOpacity()); // 위젯 투명도 → Scene 투명도
childScene->push(tvg::SceneEffect::ClearAll);
if (obj->enableGraphicsEffect()) // 블러 이펙트 → SceneEffect
childScene->push(tvg::SceneEffect::GaussianBlur,
blurEffectObj.blurRadius(), ...);
if (ret) childPaint(childSet, &event); // 자식 위젯 재귀
}
Scene::remove()로 이전 프레임의 Paint들을 비우고 위젯의onPaintEvent가 새로 채운다 — 위젯 단위로는 즉시 모드처럼 쓰되, 트리 전체는 리테인드로 유지하는 하이브리드다.- 위젯 블러 효과가
tvg::SceneEffect::GaussianBlur로 직결된다. TpGUI의TpGraphicsBlurEffectAPI는 ThorVG SceneEffect의 얇은 래퍼이며,TpPainter::paintTest에는 배경 Scene에 블러를 걸어 만드는 글래스모피즘(유리 패널) 실험 코드도 있다 — 임베디드 GUI에서 SceneEffect로 iOS풍 프로스티드 글래스를 구현하려는 시도다.
4. 아키텍처 다이어그램
┌─────────────────────────────────────────────────────────┐
│ App (TpGUI 사용 애플리케이션) │
│ │
│ TpWidget 트리 ═(1:1 미러)═▶ tvg::Scene 트리 │
│ ├─ TpButton ──drawRect──▶ tvg::Shape │
│ ├─ TpLabel ──drawText──▶ tvg::Text (TTF 직접로드) │
│ ├─ TpImage ──drawImage─▶ tvg::Picture (SVG/WebP) │
│ └─ TpLottieAnimation ──────▶ tvg::Animation │
│ │
│ TpApp::run() 16ms 루프 │
│ └─ DownUpdateCommand: 더티렉트 병합 → onPaintEvent 재귀 │
│ └─ SwCanvas(SmartRender)::update/draw/sync │
│ │ target(ARGB8888) │
├─────────────────────▼───────────────────────────────────┤
│ TpWM 공유 메모리 서피스 ◀─ ThorVG가 직접 래스터라이즈 │
│ └─ tinyPiX_wf_update(dirty rect) → 합성 │
├─────────────────────────────────────────────────────────┤
│ Linux (커스텀 커널) / 프레임버퍼 │
└─────────────────────────────────────────────────────────┘
5. 기술적 인사이트 정리
① ThorVG를 “벡터 뷰어”가 아니라 “GUI 렌더링 커널”로 쓴 사례.
대부분의 ThorVG 도입 사례(LVGL, dotLottie 등)는 기존 렌더러 옆에 Lottie/SVG 재생기를 붙이는 형태다. TinyPiXOS는 반대로 도형·텍스트·이미지·이펙트·애니메이션·윈도우 합성 직전 단계까지 전부 ThorVG 하나로 수렴시켰다. TpPainterData에 다른 래스터라이저가 아예 존재하지 않는다.
② 리테인드 Scene 그래프 + SmartRender의 시너지.
위젯 트리를 Scene 트리로 미러링했기 때문에, 위젯 하나가 바뀌면 해당 Scene만 재구성되고 EngineOption::SmartRender가 그 영역만 다시 그린다. 더티 렉트 병합(TpGUI 계층) + 부분 래스터라이즈(ThorVG 계층) + 부분 합성(TpWM 계층)의 3단 부분 갱신 파이프라인이 GPU 없는 보드에서 데스크톱급 UI를 성립시키는 핵심이다.
③ 업스트림 무수정 사용. 포크는 버전 고정용 미러일 뿐(ahead 0), 커스텀 패치가 없다. OS 렌더링 코어라는 극한 용례에서도 ThorVG 공개 API(Scene/Shape/Text/Picture/Animation/SceneEffect)만으로 충분했다는 것은 API 설계 완성도에 대한 실전 방증이다.
복습용 요약
용어 미니 사전
| 용어 | 뜻 |
|---|---|
| 더티 렉트 | 다시 그려야 하는 직사각형 영역. 화면 전체 대신 바뀐 부분만 갱신하기 위한 좌표 기록 |
| 리테인드 모드 | 엔진이 Scene 트리(무엇이 어디 있는지)를 계속 보유 → “뭐가 바뀌었는지” 알 수 있어 부분 갱신 가능. 즉시 모드는 draw 호출이 바로 픽셀을 찍고 상태를 안 남김 |
| SmartRender | ThorVG v1.0의 부분 렌더링 옵션 (EngineOption::SmartRender). 변경된 Paint의 바운딩 영역만 재래스터라이즈. TinyPiXOS는 이걸 켜서 쓰는 쪽 |
| 서피스 | 창 하나의 화면 내용을 담는 픽셀 버퍼 |
| 공유 메모리 서피스 | 앱과 WM은 별개 프로세스라 메모리가 격리됨 → 픽셀 전송(복사) 대신 같은 메모리를 함께 보게 만든 서피스. Wayland wl_shm과 같은 아이디어 |
draw() / sync() | 비동기 렌더링: draw = 일감 던지기, sync = 완료 대기. 프레임당 화면마다 한 번 |
댓글
Discussion 원문