예제 코드: BG · Scene(A, B) · GaussianBlur → Fill
본문의 Pipeline 장면은 24×24 Canvas에 BG와 Scene(A, B)를 추가한 같은 입력에서 출발합니다. Scene은 자식 전체에 opacity 128을 적용하고, 두 Shape를 그린 결과에 GaussianBlur → Fill을 순서대로 실행합니다.
#include <array>
#include <memory>
#include <thorvg.h>
using namespace tvg;
Initializer::init(2);
{
constexpr uint32_t W = 24, H = 24;
std::array<uint32_t, W * H> pixels{};
auto canvas = std::unique_ptr<SwCanvas>(SwCanvas::gen());
canvas->target(pixels.data(), W, W, H, ColorSpace::ABGR8888);
auto bg = Shape::gen();
bg->appendRect(0, 0, W, H);
bg->fill(234, 241, 236);
auto a = Shape::gen();
a->moveTo(7, 5);
a->lineTo(3, 19);
a->lineTo(18, 19);
a->close();
a->fill(230, 97, 33);
auto b = Shape::gen();
b->appendCircle(16, 13, 5, 5);
b->fill(31, 102, 196);
auto scene = Scene::gen();
scene->add(a);
scene->add(b);
scene->opacity(128);
scene->add(SceneEffect::GaussianBlur, 1.4, 0, 0, 100);
scene->add(SceneEffect::Fill, 49, 150, 123, 255);
canvas->add(bg);
canvas->add(scene);
canvas->update();
canvas->draw(true);
canvas->sync();
}
Initializer::term();1. Pipeline

SceneImpl::update()는 자식 Paint의 Update를 호출한 뒤 renderer->prepare(effect, transform)으로 Effect 파라미터를 준비합니다. Shape의 Worker Task와 달리 Effect 준비는 호출 스레드에서 실행됩니다.
- GaussianBlur:
rd->kernel과 확장량rd->extends계산 - Fill:
valid = true설정 - Offscreen의 픽셀 처리는 이후 Draw에서 자식 Raster가 끝난 뒤 실행
코드 보기: 01. Effect Prepare — Scene Update · Blur/Fill 준비
호출 흐름
canvas->update()
-> SceneImpl::update()
-> 자식 Paint의 update()
-> SwRenderer::prepare(effect, transform)
-> effectGaussianBlurUpdate() / effectFillUpdate()자식 Paint의 준비 작업을 요청한 뒤, 호출 스레드에서 Effect 파라미터를 준비합니다. Blur의 rd는 kernel·확장량을 담는 데이터이며, Offscreen 픽셀 버퍼는 이후 Draw에서 확보합니다.
bool update(RenderMethod* renderer, const Matrix& transform, Array<RenderData>& clips, uint8_t opacity, RenderUpdateFlag flag, TVG_UNUSED bool clipper)
{
if (paints.empty()) return true;
if (needComposition(opacity)) {
/* Overriding opacity value. If this scene is half-translucent,
It must do intermediate composition with that opacity value. */
this->opacity = opacity;
opacity = 255;
}
//allow partial rendering?
auto recover = fixed ? renderer->partial(true) : false;
// 1. 자식 Update에서 Shape 등의 Task 준비를 요청합니다.
for (auto paint : paints) {
PAINT(paint)->update(renderer, transform, clips, opacity, flag, false);
}
//recover the condition
if (fixed) renderer->partial(recover);
// 2. 자식 Task의 완료를 기다리지 않고, 여기서 Effect 파라미터를 준비합니다.
if (effects) {
ARRAY_FOREACH(p, *effects) {
renderer->prepare(*p, transform);
}
}
//this viewport update is more performant than in bounds(). No idea.
vport = renderer->viewport();
if (fixed) {
auto pt = fsize * transform;
vport.intersect({{int32_t(round(transform.e13)), int32_t(round(transform.e23))}, {int32_t(round(pt.x)), int32_t(round(pt.y))}});
} else {
vdirty = true;
}
//bounds(renderer) here hinders parallelization
//TODO: we can bring the precise effects region here
if (fixed || effects) impl.damage(vport);
return true;
}void SwRenderer::prepare(RenderEffect* effect, const Matrix& transform)
{
// Effect 종류에 맞는 Update 함수를 호출합니다. 별도 SwTask를 요청하지 않습니다.
switch (effect->type) {
case SceneEffect::GaussianBlur: effectGaussianBlurUpdate(static_cast<RenderEffectGaussianBlur*>(effect), transform); break;
case SceneEffect::DropShadow: effectDropShadowUpdate(static_cast<RenderEffectDropShadow*>(effect), transform); break;
case SceneEffect::Fill: effectFillUpdate(static_cast<RenderEffectFill*>(effect)); break;
case SceneEffect::Tint: effectTintUpdate(static_cast<RenderEffectTint*>(effect)); break;
case SceneEffect::Tritone: effectTritoneUpdate(static_cast<RenderEffectTritone*>(effect)); break;
default: break;
}
}struct SwGaussianBlur
{
static constexpr int MAX_LEVEL = 3;
int level;
int kernel[MAX_LEVEL];
int extends;
};void effectGaussianBlurUpdate(RenderEffectGaussianBlur* params, const Matrix& transform)
{
if (!params->rd) params->rd = tvg::malloc<SwGaussianBlur>(sizeof(SwGaussianBlur));
auto rd = static_cast<SwGaussianBlur*>(params->rd);
//compute box kernel sizes
auto scaleSquared = transform.e11 * transform.e11 + transform.e12 * transform.e12;
rd->extends = _gaussianInit(rd, params->sigma * params->sigma * scaleSquared, params->quality);
params->valid = (rd->extends > 0);
}static int _gaussianInit(SwGaussianBlur* data, float sigma, int quality)
{
// 이 함수의 sigma 인자는 표준편차가 아닌 분산입니다.
const auto MAX_LEVEL = SwGaussianBlur::MAX_LEVEL;
if (tvg::zero(sigma)) return 0;
data->level = int(SwGaussianBlur::MAX_LEVEL * ((quality - 1) * 0.01f)) + 1;
//compute box kernel sizes
auto wl = (int) sqrt((12 * sigma / MAX_LEVEL) + 1);
if (wl % 2 == 0) --wl;
auto wu = wl + 2;
auto mi = (12 * sigma - MAX_LEVEL * wl * wl - 4 * MAX_LEVEL * wl - 3 * MAX_LEVEL) / (-4 * wl - 4);
auto m = int(mi + 0.5f);
auto extends = 0;
// 각 box filter의 반경을 저장하고, 반경의 합을 확장량으로 반환합니다.
for (int i = 0; i < data->level; i++) {
data->kernel[i] = ((i < m ? wl : wu) - 1) / 2;
extends += data->kernel[i];
}
return extends;
}void effectFillUpdate(RenderEffectFill* params)
{
// Fill 색상은 Scene::add()에서 저장되어 있습니다. 여기서는 valid만 설정합니다.
params->valid = true;
}코드 보기: 02. Effect 등록 · 순차 실행 — GaussianBlur → Fill
호출 흐름
scene->add(SceneEffect::GaussianBlur, ...)
-> Scene::add()
-> SceneImpl::add()
-> effects->push(blur)
scene->add(SceneEffect::Fill, ...)
-> Scene::add()
-> SceneImpl::add()
-> effects->push(fill)위 예제에서 Effect를 등록하는 부분입니다. effects 배열의 순서가 GaussianBlur → Fill이 됩니다.
// 1. Scene(A, B)에 Blur를 먼저 등록합니다.
scene->add(SceneEffect::GaussianBlur, 1.4, 0, 0, 100);
// 2. Blur 결과에 적용할 Fill을 다음에 등록합니다.
scene->add(SceneEffect::Fill, 49, 150, 123, 255);Result Scene::add(SceneEffect effect, ...) noexcept
{
va_list args;
va_start(args, effect);
auto ret = to<SceneImpl>(this)->add(effect, args);
va_end(args);
return ret;
}Result add(SceneEffect effect, va_list& args)
{
if (effect == SceneEffect::Clear) return resetEffects();
if (!this->effects) this->effects = new Array<RenderEffect*>;
RenderEffect* re = nullptr;
switch (effect) {
case SceneEffect::GaussianBlur: {
re = RenderEffectGaussianBlur::gen(args);
break;
}
case SceneEffect::DropShadow: {
re = RenderEffectDropShadow::gen(args);
break;
}
case SceneEffect::Fill: {
re = RenderEffectFill::gen(args);
break;
}
case SceneEffect::Tint: {
re = RenderEffectTint::gen(args);
break;
}
case SceneEffect::Tritone: {
re = RenderEffectTritone::gen(args);
break;
}
default: break;
}
if (!re) return Result::InvalidArguments;
// 호출된 순서대로 Effect를 배열 끝에 추가합니다.
this->effects->push(re);
return Result::Success;
}호출 흐름
canvas->draw()
-> SceneImpl::render()
-> target()
-> beginComposite()
-> 자식 A·B render()
-> SwRenderer::render(cmp, GaussianBlur, false)
-> SwRenderer::render(cmp, Fill, false)
-> endComposite()자식 Raster가 끝나면 유효한 Effect를 배열 순서대로 실행합니다. 이 예제는 Effect가 두 개이므로 direct = false이며, Fill이 Blur가 처리한 Offscreen을 이어받습니다.
bool render(RenderMethod* renderer, CompositionFlag flag)
{
if (paints.empty()) return true;
RenderCompositor* cmp = nullptr;
// its parent is already in composition mode, maybe parasitize its surface
auto incomposite = (uint8_t(CompositionFlag::PostProcessing) & uint8_t(flag)) && !effects;
auto ret = true;
renderer->blend(impl.blendMethod);
// 1. 자식을 그릴 Offscreen을 선택하고 합성 정보를 설정합니다.
if (!incomposite && impl.cmpFlag) {
cmp = renderer->target(bounds(), renderer->colorSpace(), impl.cmpFlag);
renderer->beginComposite(cmp, MaskMethod::None, opacity);
}
// 2. A와 B를 현재 Offscreen에 순서대로 그립니다.
for (auto paint : paints) {
ret &= paint->pImpl->render(renderer, impl.cmpFlag);
}
if (cmp) {
//Apply post effects if any.
if (effects) {
//Notify the possibility of the direct composition of the effect result to the origin surface.
auto direct = (effects->count == 1) & (impl.marked(CompositionFlag::PostProcessing));
// 3. 등록 순서대로 GaussianBlur → Fill을 실행합니다.
ARRAY_FOREACH(p, *effects) {
if ((*p)->valid) renderer->render(cmp, *p, direct);
}
}
// 4. Effect 처리가 끝나면 부모 Surface로 복귀해 합성합니다.
renderer->endComposite(cmp);
}
return ret;
}bool SwRenderer::render(RenderCompositor* cmp, const RenderEffect* effect, bool direct)
{
auto p = static_cast<SwCompositor*>(cmp);
if (p->image.channelSize != sizeof(uint32_t)) {
TVGERR("SW_ENGINE", "Not supported grayscale Gaussian Blur!");
return false;
}
//TODO: Support grayscale effects.
if (p->recoverSfc->channelSize != sizeof(uint32_t)) direct = false;
switch (effect->type) {
case SceneEffect::GaussianBlur: {
return effectGaussianBlur(p, request(surface->channelSize, true), static_cast<const RenderEffectGaussianBlur*>(effect));
}
case SceneEffect::DropShadow: {
auto cmp1 = request(surface->channelSize, true);
cmp1->compositor->valid = false; //prevent a conflict with cmp2 request.
auto cmp2 = request(surface->channelSize, true);
SwSurface* surfaces[] = {cmp1, cmp2};
auto ret = effectDropShadow(p, surfaces, static_cast<const RenderEffectDropShadow*>(effect), direct);
cmp1->compositor->valid = true;
return ret;
}
case SceneEffect::Fill: {
return effectFill(p, static_cast<const RenderEffectFill*>(effect), direct);
}
case SceneEffect::Tint: {
return effectTint(p, static_cast<const RenderEffectTint*>(effect), direct);
}
case SceneEffect::Tritone: {
return effectTritone(p, static_cast<const RenderEffectTritone*>(effect), direct);
}
default: return false;
}
}코드 보기: 03. Offscreen 버퍼 — request · target
호출 흐름
canvas->draw()
-> SceneImpl::render()
-> SwRenderer::target(region, colorSpace, flags)
-> SwRenderer::request(channelSize, square)
-> 캐시 재사용 또는 픽셀 버퍼 할당
-> rasterClear()
-> surface = Offscreenrequest()가 버퍼를 확보하고, target()이 부모 컨텍스트를 저장한 뒤 렌더 타겟을 전환합니다. 이 예제의 첫 Draw에서는 Offscreen 하나를 할당하며, Blur의 Scratch는 Effect 실행 중에 별도로 요청합니다.
SwSurface* SwRenderer::request(int channelSize, bool square)
{
SwSurface* cmp = nullptr;
uint32_t w, h;
// 1. 후처리 전치를 위해 양쪽 크기가 같은 버퍼를 요청할 수 있습니다.
if (square) {
//Same Dimensional Size is demanded for the Post Processing Fast Flipping
w = h = std::max(surface->w, surface->h);
} else {
w = surface->w;
h = surface->h;
}
//Use cached data
// 2. 재사용 가능하고 채널 수·크기가 일치하는 캐시를 찾습니다.
ARRAY_FOREACH(p, compositors) {
auto cur = *p;
if (cur->compositor->valid && cur->compositor->image.channelSize == channelSize) {
if (w == cur->w && h == cur->h) {
cmp = *p;
break;
}
}
}
//New Composition
if (!cmp) {
//Inherits attributes from main surface
cmp = new SwSurface(surface);
cmp->compositor = new SwCompositor;
// 3. 캐시가 없으면 여기서 실제 픽셀 저장 공간을 할당합니다.
cmp->compositor->image.data = tvg::malloc<pixel_t>(channelSize * w * h);
cmp->w = cmp->compositor->image.w = w;
cmp->h = cmp->compositor->image.h = h;
cmp->stride = cmp->compositor->image.stride = w;
cmp->compositor->image.direct = true;
cmp->compositor->valid = true;
cmp->channelSize = cmp->compositor->image.channelSize = channelSize;
compositors.push(cmp);
}
//Sync. This may have been modified by post-processing.
// 4. 후처리에서 저장 공간이 바뀔 수 있으므로 Surface의 포인터를 맞춥니다.
cmp->data = cmp->compositor->image.data;
return cmp;
}RenderCompositor* SwRenderer::target(const RenderRegion& region, ColorSpace cs, CompositionFlag flags)
{
auto bbox = RenderRegion::intersect(region, {{0, 0}, {int32_t(surface->w), int32_t(surface->h)}});
if (bbox.invalid()) return nullptr;
// 1. PostProcessing 플래그가 있으면 정사각형 버퍼를 요청합니다.
auto cmp = request(CHANNEL_SIZE(cs), (flags & CompositionFlag::PostProcessing));
// 2. endComposite()에서 복구할 부모 Surface와 compositor를 보관합니다.
cmp->compositor->recoverSfc = surface;
cmp->compositor->recoverCmp = surface->compositor;
// 선택한 버퍼를 사용 중으로 표시합니다.
cmp->compositor->valid = false;
cmp->compositor->bbox = bbox;
/* TODO: Currently, only blending might work.
Blending and composition must be handled together. */
// 3. 이번에 사용할 영역을 비웁니다.
rasterClear(cmp, bbox.x(), bbox.y(), bbox.w(), bbox.h());
//Switch render target
// 4. 이후 자식 Raster가 이 Offscreen에 기록하도록 전환합니다.
surface = cmp;
return cmp->compositor;
}코드 보기: 04. beginComposite — 합성 방식 · opacity
호출 흐름
canvas->draw()
-> SceneImpl::render()
-> SwRenderer::target()
-> SwRenderer::beginComposite(cmp, MaskMethod::None, 128)target()에서 선택한 Offscreen에 합성 방식과 그룹 opacity를 설정합니다. 이 예제는 MaskMethod::None이므로 렌더 타겟이 Offscreen으로 유지됩니다.
bool SwRenderer::beginComposite(RenderCompositor* cmp, MaskMethod method, uint8_t opacity)
{
if (!cmp) return false;
auto p = static_cast<SwCompositor*>(cmp);
// 1. 이 예제는 None과 그룹 opacity 128을 저장합니다.
p->method = method;
p->opacity = opacity;
//Current Context?
// 2. Mask가 지정된 경우에는 부모 Surface로 전환하고 mask compositor를 연결합니다.
if (p->method != MaskMethod::None) {
surface = p->recoverSfc;
surface->compositor = p;
}
return true;
}코드 보기: 05. endComposite — 부모 Surface 복구 · 최종 합성
호출 흐름
canvas->draw()
-> SceneImpl::render()
-> GaussianBlur → Fill 처리 완료
-> SwRenderer::endComposite(cmp)
-> 부모 Surface·compositor 복구
-> rasterDirectImage(surface, image, bbox, opacity)부모 Surface를 복구한 뒤, Offscreen 결과에 그룹 opacity를 반영하여 합성합니다. valid = true는 해당 버퍼를 이후 요청에서 재사용할 수 있음을 표시합니다.
bool SwRenderer::endComposite(RenderCompositor* cmp)
{
if (!cmp) return false;
auto p = static_cast<SwCompositor*>(cmp);
//Recover Context
// 1. 이 예제에서는 BG가 남아 있는 Canvas Surface로 복귀합니다.
surface = p->recoverSfc;
surface->compositor = p->recoverCmp;
//only invalid (currently used) surface can be composited
// 2. 단일 Effect의 direct 경로 등에서 이미 합성했다면 중복 처리를 생략합니다.
if (p->valid) return true;
// 버퍼를 재사용 가능한 상태로 돌립니다.
p->valid = true;
//Default is alpha blending
if (p->method == MaskMethod::None) {
// 3. GaussianBlur → Fill 결과를 opacity 128로 기존 Canvas 위에 합성합니다.
return rasterDirectImage(surface, p->image, p->bbox, p->opacity);
}
return true;
}2. Compositing Pipeline

auto scene = Scene::gen();
scene->add(a);
scene->add(b);
scene->opacity(128);
scene->add(SceneEffect::GaussianBlur, 1.4, 0, 0, 100);
scene->add(SceneEffect::Fill, 49, 150, 123, 255);
target()으로 Offscreen을 선택하고, beginComposite()에서 합성 방식과 그룹 opacity를 설정합니다. endComposite()는 Canvas를 복구한 뒤 rasterDirectImage()로 결과를 합성합니다.
Effect 체인은 앞선 Effect의 결과를 다음 Effect에 전달합니다. Blur가 처리한 Offscreen을 Fill이 그대로 이어받아 색을 바꾸며, 이 예제의 임시 버퍼는 Offscreen 1개와 Blur scratch 1개입니다.
Canvas의 BG는 Effect 처리 동안 유지됩니다. Effect 체인이 끝난 이미지 전체에 그룹 opacity를 적용하여, 기존 Canvas 위에 한 번 합성합니다.
2.1. beginComposite

렌더 타겟을 바꾸는 함수는 target()입니다. 기존 Canvas Surface를 recoverSfc에 보관하고, 요청한 offscreen의 처리 영역을 비운 뒤 surface = cmp로 전환합니다.
이어지는 beginComposite()는 합성 방식과 그룹 opacity를 설정합니다. 이 예제는 MaskMethod::None, Scene opacity 128이며, Canvas의 BG 픽셀은 유지됩니다.
- 처리 영역: 자식 bounds에 Effect 확장량을 더한 뒤 viewport와 교차
- 버퍼 크기: 후처리의 전치를 위해 현재 Surface의 긴 변을 기준으로 정사각형 확보
- 이 예제: offscreen 1개, Blur scratch는 아직 요청하지 않은 상태
- 재사용:
request()는 크기·채널이 맞는 사용 가능한 캐시를 우선 선택
코드 보기: target → beginComposite — Offscreen을 렌더 타겟으로 선택
호출 흐름
canvas->draw()
-> A/B Scene의 SceneImpl::render()
-> SwRenderer::target(region)
-> request()
-> rasterClear()
-> SwRenderer::beginComposite()target은 기존 Surface를 보관하고 offscreen으로 전환합니다. 이 예제의 beginComposite는 MaskMethod::None으로 합성 방식과 opacity를 설정합니다.
RenderCompositor* SwRenderer::target(const RenderRegion& region, ColorSpace cs, CompositionFlag flags)
{
auto bbox = RenderRegion::intersect(region, {{0, 0}, {int32_t(surface->w), int32_t(surface->h)}});
if (bbox.invalid()) return nullptr;
auto cmp = request(CHANNEL_SIZE(cs), (flags & CompositionFlag::PostProcessing));
cmp->compositor->recoverSfc = surface;
cmp->compositor->recoverCmp = surface->compositor;
cmp->compositor->valid = false;
cmp->compositor->bbox = bbox;
/* TODO: Currently, only blending might work.
Blending and composition must be handled together. */
rasterClear(cmp, bbox.x(), bbox.y(), bbox.w(), bbox.h());
//Switch render target
surface = cmp;
return cmp->compositor;
}bool SwRenderer::beginComposite(RenderCompositor* cmp, MaskMethod method, uint8_t opacity)
{
if (!cmp) return false;
auto p = static_cast<SwCompositor*>(cmp);
p->method = method;
p->opacity = opacity;
//Current Context?
if (p->method != MaskMethod::None) {
surface = p->recoverSfc;
surface->compositor = p;
}
return true;
}코드 보기: 임시 버퍼 — 캐시 재사용과 신규 할당
호출 흐름
canvas->draw()
-> SceneImpl::render()
-> SwRenderer::target(region)
-> SwRenderer::request()
-> 캐시 재사용 또는 새 버퍼 할당request는 크기·채널이 맞는 사용 가능한 버퍼를 우선 재사용합니다. endComposite의 valid=true는 이후 재사용할 수 있음을 표시합니다.
SwRenderer::request() — 캐시 검색과 할당
ARRAY_FOREACH(p, compositors) {
auto cur = *p;
if (cur->compositor->valid && cur->compositor->image.channelSize == channelSize) {
if (w == cur->w && h == cur->h) {
cmp = *p;
break;
}
}
}
//New Composition
if (!cmp) {
//Inherits attributes from main surface
cmp = new SwSurface(surface);
cmp->compositor = new SwCompositor;
cmp->compositor->image.data = tvg::malloc<pixel_t>(channelSize * w * h);
cmp->w = cmp->compositor->image.w = w;
cmp->h = cmp->compositor->image.h = h;
cmp->stride = cmp->compositor->image.stride = w;
cmp->compositor->image.direct = true;
cmp->compositor->valid = true;
cmp->channelSize = cmp->compositor->image.channelSize = channelSize;
compositors.push(cmp);
}2.2. renderShape

Scene은 자식 Paint를 등록 순서대로 렌더링하며, 각 renderShape()는 현재 surface에 픽셀을 기록합니다. 따라서 A와 B는 Canvas가 아닌, 앞 단계에서 선택한 offscreen에 합쳐집니다.
그림 왼쪽의 SwShapeTask · A/B는 Prepare에서 준비한 데이터를 보관하는 객체입니다. 가운데의 renderShape()가 같은 객체를 RenderData로 받아 done()으로 준비 완료를 기다립니다. 이 예제의 단색 Fill은 rasterShape() → _rasterRle()를 통해 현재 Offscreen에 기록되며, run()을 다시 호출하지 않습니다.
이 예제의 A·B Task opacity는 모두 255입니다. 그룹 opacity 128은 자식마다 적용하지 않고 마지막 합성에 사용하므로 겹치는 영역을 하나의 그룹으로 처리합니다.
- A: RLE 50개 → B: RLE 39개, 같은 픽셀 위치에는 나중에 그린 B의 결과 반영
- 필터 입력: path·gradient 객체가 아니라 합성된 premultiplied RGBA 이미지
- Canvas Surface: BG를 그린 상태로 유지
코드 보기: Prepare의 Task를 RenderData로 읽는 renderShape
Prepare — 준비 작업과 결과 저장소 생성
ShapeImpl::update()
-> SwRenderer::prepare(rs, impl.rd, ...)prepare()는 기존 SwShapeTask를 재사용하거나 생성하고, prepareCommon()이 반환한 같은 포인터를 RenderData로 돌려줍니다. ShapeImpl::update()는 이를 impl.rd에 보관합니다.
RenderData SwRenderer::prepare(const RenderShape& rshape, RenderData data, const Matrix& transform, const Array<RenderData>& clips, uint8_t opacity, RenderUpdateFlag flags, bool clipper)
{
auto task = static_cast<SwShapeTask*>(data);
if (task) task->done();
else {
task = new SwShapeTask;
task->rshape = &rshape;
}
task->clipper = clipper;
return prepareCommon(task, transform, clips, opacity, flags, (opacity == 0 && !clipper));
}갱신이 필요하면 prepareCommon()에서 TaskScheduler::request(task)로 준비 작업을 요청합니다. SwShapeTask::run()은 이 요청으로 실행됩니다. Worker가 있으면 비동기로 처리하고, 없으면 요청한 자리에서 실행합니다. (prepareCommon(), TaskSchedulerImpl::request())
Draw — 같은 RenderData를 읽어 현재 Surface에 기록
SceneImpl::render()
-> Paint::Impl::render() [자식 A 또는 B]
-> ShapeImpl::render()
-> SwRenderer::renderShape(impl.rd)data를 SwShapeTask*로 되돌린 뒤 done()으로 준비 완료를 확인합니다. 그다음 보관된 shape·rshape·opacity를 사용해 Fill·Stroke Raster 함수를 직접 호출합니다. 여기에는 TaskScheduler::request()나 run() 호출이 없습니다. 이 예제의 surface는 앞서 beginComposite()가 선택한 offscreen입니다.
bool SwRenderer::renderShape(RenderData data)
{
auto task = static_cast<SwShapeTask*>(data);
if (!task) return false;
task->done();
if (task->valid) {
auto fill = [](SwShapeTask* task, SwSurface* surface, const RenderRegion& bbox) {
if (auto fill = task->rshape->fill) {
rasterGradientShape(surface, &task->shape, bbox, fill, task->opacity);
} else {
RenderColor c;
task->rshape->fillColor(&c.r, &c.g, &c.b, &c.a);
c.a = MULTIPLY(task->opacity, c.a);
if (c.a > 0) rasterShape(surface, &task->shape, bbox, c);
}
};
auto stroke = [](SwShapeTask* task, SwSurface* surface, const RenderRegion& bbox) {
if (auto strokeFill = task->rshape->strokeFill()) {
rasterGradientStroke(surface, &task->shape, bbox, strokeFill, task->opacity);
} else {
RenderColor c;
if (task->rshape->strokeFill(&c.r, &c.g, &c.b, &c.a)) {
c.a = MULTIPLY(task->opacity, c.a);
if (c.a > 0) rasterStroke(surface, &task->shape, bbox, c);
}
}
};
//full scene or partial rendering
if (fulldraw || task->nodirty || task->pushed || dirtyRegion.deactivated()) {
if (task->rshape->strokeFirst()) {
stroke(task, surface, task->curBox);
fill(task, surface, task->shape.bbox);
} else {
fill(task, surface, task->shape.bbox);
stroke(task, surface, task->curBox);
}
} else if (task->curBox.valid()) {
for (int idx = 0; idx < RenderDirtyRegion::PARTITIONING; ++idx) {
if (!dirtyRegion.partition(idx).intersected(task->curBox)) continue;
ARRAY_FOREACH(p, dirtyRegion.get(idx)) {
if (task->curBox.max.x <= p->min.x) break; //dirtyRegion is sorted in x order
if (task->rshape->strokeFirst()) {
if (task->rshape->stroke && task->curBox.intersected(*p)) stroke(task, surface, RenderRegion::intersect(task->curBox, *p));
if (task->shape.bbox.intersected(*p)) fill(task, surface, RenderRegion::intersect(task->shape.bbox, *p));
} else {
if (task->shape.bbox.intersected(*p)) fill(task, surface, RenderRegion::intersect(task->shape.bbox, *p));
if (task->rshape->stroke && task->curBox.intersected(*p)) stroke(task, surface, RenderRegion::intersect(task->curBox, *p));
}
}
}
}
}
return task->complete();
}done() — 작업 실행이 아닌 완료 대기
SwRenderer::renderShape(impl.rd)
-> task->done()
-> Task::done()아래는 스레드 지원 빌드의 전체 함수입니다. 대기할 작업이 없으면 바로 반환하며, 준비가 진행 중이면 완료 신호를 기다립니다. 스레드 미지원 빌드에서는 done()이 빈 함수입니다.
void done()
{
if (!pending) return;
unique_lock<mutex> lock(mtx);
while (!ready) cv.wait(lock);
pending = false;
}renderShape() 끝의 task->complete()도 run()을 실행하지 않습니다. 현재 영역을 다음 프레임의 이전 영역으로 남기고 nodirty를 해제합니다.
Fill·Stroke의 Raster 경로 선택은 Draw/Raster에서 이어집니다. 이 절에서 중요한 차이는 같은 Raster가 Canvas 대신 현재 선택된 offscreen에 기록된다는 점입니다.
2.3. Effect chain

Effect 루프는 앞 Effect가 만든 이미지를 다음 Effect의 입력으로 사용합니다. 이 예제는 effects[0] = GaussianBlur, effects[1] = Fill이며, 두 Effect가 끝날 때까지 Canvas에는 기록하지 않습니다.
GaussianBlur만 scratch를 추가로 요청하고, Fill은 같은 offscreen을 제자리에서 수정합니다. 그림의 두 물리 버퍼는 Blur 동안 서로 역할을 바꾸지만, Shape를 그릴 렌더 타겟을 scratch로 전환하는 것은 아닙니다.
- Blur: 수평 3회 → 전치 → 수직 3회 → 역전치, 이 설정의 최종 결과는 원래 Offscreen에 저장
- Fill: Blur 결과의 alpha를 사용해 RGB 교체, 추가 버퍼 0개
- 임시 픽셀 버퍼: offscreen 1개 + scratch 1개, Canvas Surface는 별도
- 루프 종료 후에만
endComposite()호출
코드 보기: Effect chain — 등록 순서대로 실행
호출 흐름
canvas->draw()
-> A/B Scene의 SceneImpl::render()
-> 자식 Paint 렌더링
-> effects 순회
-> SwRenderer::render(cmp, effect, direct)
-> effect*()유효한 Effect를 등록 순서대로 실행합니다. 이 예제는 Effect가 두 개이므로 direct가 false입니다.
bool render(RenderMethod* renderer, CompositionFlag flag)
{
if (paints.empty()) return true;
RenderCompositor* cmp = nullptr;
// its parent is already in composition mode, maybe parasitize its surface
auto incomposite = (uint8_t(CompositionFlag::PostProcessing) & uint8_t(flag)) && !effects;
auto ret = true;
renderer->blend(impl.blendMethod);
if (!incomposite && impl.cmpFlag) {
cmp = renderer->target(bounds(), renderer->colorSpace(), impl.cmpFlag);
renderer->beginComposite(cmp, MaskMethod::None, opacity);
}
for (auto paint : paints) {
ret &= paint->pImpl->render(renderer, impl.cmpFlag);
}
if (cmp) {
//Apply post effects if any.
if (effects) {
// [핵심 1] 단일 Effect일 때 부모 Surface로 직접 합성할 가능성을 전달합니다.
// 이 예제는 GaussianBlur + Fill 두 개이므로 direct = false입니다.
auto direct = (effects->count == 1) & (impl.marked(CompositionFlag::PostProcessing));
// [핵심 2] 등록 순서대로 유효한 Effect를 실행합니다: GaussianBlur → Fill.
// 앞 Effect가 처리한 Offscreen을 다음 Effect가 이어받습니다.
ARRAY_FOREACH(p, *effects) {
if ((*p)->valid) renderer->render(cmp, *p, direct);
}
}
// Effect 체인이 끝난 뒤 부모 Surface로 복귀해 결과를 합성합니다.
renderer->endComposite(cmp);
}
return ret;
}단일 Effect의 direct 최적화
Effect가 하나인 경우에는 부모 Surface에 직접 합성할 가능성을 전달합니다. direct는 자식을 그릴 중간 입력 이미지가 없어지는 의미가 아닙니다.
- Fill·Tint·Tritone: 직접 합성 완료 후
cmp->valid = true, 후속 일반 합성 생략 - DropShadow: 그림자를 부모에 먼저 합성하고 본체를 이후 합성
- GaussianBlur: 중간 버퍼를 필터링한 뒤 일반 합성 사용
- 현재 예제: Effect가 두 개이므로
direct = false
호출 흐름 — Fill 하나만 등록한 경우
canvas->draw()
-> SceneImpl::render()
-> target() · beginComposite()
-> 자식 A·B를 Offscreen에 렌더링
-> direct = true
-> SwRenderer::render(cmp, effect, direct)
-> effectFill(): 부모 Surface에 직접 합성
-> cmp->valid = true
-> endComposite(): 부모 컨텍스트 복구 후 중복 합성 생략앞의 예제에서 Effect를 Fill 하나로 바꾸면 이 경로를 확인할 수 있습니다.
// 기존 GaussianBlur → Fill 체인을 지우고 Fill 하나만 등록합니다.
scene->add(SceneEffect::Clear);
scene->add(SceneEffect::Fill, 49, 150, 123, 255);
canvas->update();
canvas->draw(true);
canvas->sync();SceneImpl::render() — direct 조건
// [핵심 1] Effect가 하나이고 PostProcessing 플래그가 있을 때 direct 가능성을 전달합니다.
// 실제 직접 합성 여부는 아래 CPU Effect 구현에서 결정합니다.
auto direct = (effects->count == 1) & (impl.marked(CompositionFlag::PostProcessing));
ARRAY_FOREACH(p, *effects) {
if ((*p)->valid) renderer->render(cmp, *p, direct);
}SwRenderer::render(effect) — 전체 함수
bool SwRenderer::render(RenderCompositor* cmp, const RenderEffect* effect, bool direct)
{
auto p = static_cast<SwCompositor*>(cmp);
if (p->image.channelSize != sizeof(uint32_t)) {
TVGERR("SW_ENGINE", "Not supported grayscale Gaussian Blur!");
return false;
}
//TODO: Support grayscale effects.
// 부모 Surface가 32-bit 형식이 아니면 direct를 해제합니다.
if (p->recoverSfc->channelSize != sizeof(uint32_t)) direct = false;
switch (effect->type) {
case SceneEffect::GaussianBlur: {
// GaussianBlur에는 direct를 전달하지 않습니다. 필터링 후 endComposite()에서 합성합니다.
return effectGaussianBlur(p, request(surface->channelSize, true), static_cast<const RenderEffectGaussianBlur*>(effect));
}
case SceneEffect::DropShadow: {
auto cmp1 = request(surface->channelSize, true);
cmp1->compositor->valid = false; //prevent a conflict with cmp2 request.
auto cmp2 = request(surface->channelSize, true);
SwSurface* surfaces[] = {cmp1, cmp2};
auto ret = effectDropShadow(p, surfaces, static_cast<const RenderEffectDropShadow*>(effect), direct);
cmp1->compositor->valid = true;
return ret;
}
case SceneEffect::Fill: {
// Fill·Tint·Tritone은 direct를 받아 부모 Surface에 바로 합성할 수 있습니다.
return effectFill(p, static_cast<const RenderEffectFill*>(effect), direct);
}
case SceneEffect::Tint: {
return effectTint(p, static_cast<const RenderEffectTint*>(effect), direct);
}
case SceneEffect::Tritone: {
return effectTritone(p, static_cast<const RenderEffectTritone*>(effect), direct);
}
default: return false;
}
}Fill — 부모 Surface 기록과 후속 합성 생략
bool effectFill(SwCompositor* cmp, const RenderEffectFill* params, bool direct)
{
// direct에서는 최종 합성까지 수행하므로 그룹 opacity를 미리 반영합니다.
auto opacity = direct ? MULTIPLY(params->color[3], cmp->opacity) : params->color[3];
auto& bbox = cmp->bbox;
auto w = size_t(bbox.max.x - bbox.min.x);
auto h = size_t(bbox.max.y - bbox.min.y);
auto color = cmp->recoverSfc->join(params->color[0], params->color[1], params->color[2], 255);
TVGLOG("SW_ENGINE", "Fill region(%d, %d, %d, %d), param(%d %d %d %d)", bbox.min.x, bbox.min.y, bbox.max.x, bbox.max.y, params->color[0], params->color[1], params->color[2], params->color[3]);
if (direct) {
// [핵심 2] 입력은 Offscreen, 출력은 부모 Surface입니다.
auto dbuffer = cmp->recoverSfc->buf32 + (bbox.min.y * cmp->recoverSfc->stride + bbox.min.x);
auto sbuffer = cmp->image.buf32 + (bbox.min.y * cmp->image.stride + bbox.min.x);
for (size_t y = 0; y < h; ++y) {
auto dst = dbuffer;
auto src = sbuffer;
for (size_t x = 0; x < w; ++x, ++dst, ++src) {
auto a = MULTIPLY(opacity, A(*src));
auto tmp = ALPHA_BLEND(color, a);
// Fill 색상과 입력 alpha로 만든 결과를 부모의 기존 픽셀 위에 합성합니다.
*dst = tmp + ALPHA_BLEND(*dst, 255 - a);
}
dbuffer += cmp->recoverSfc->stride;
sbuffer += cmp->image.stride;
}
// [핵심 3] 합성을 마쳤으므로 endComposite()의 추가 픽셀 합성을 생략합니다.
cmp->valid = true; //no need the subsequent composition
} else {
// 일반 경로는 Offscreen만 수정합니다. 부모 합성은 endComposite()가 수행합니다.
auto dbuffer = cmp->image.buf32 + (bbox.min.y * cmp->image.stride + bbox.min.x);
for (size_t y = 0; y < h; ++y) {
auto dst = dbuffer;
for (size_t x = 0; x < w; ++x, ++dst) {
*dst = ALPHA_BLEND(color, MULTIPLY(opacity, A(*dst)));
}
dbuffer += cmp->image.stride;
}
}
return true;
}Tint와 Tritone도 direct 분기에서 recoverSfc에 결과를 합성한 뒤 cmp->valid = true를 설정합니다. (effectTint(), effectTritone())
DropShadow — 그림자를 먼저 합성하고 본체는 유지
DropShadow는 부모 Surface에 그림자를 먼저 합성한 뒤 원본 이미지를 cmp->image에 남깁니다. cmp->valid를 true로 바꾸지 않으므로 endComposite()에서 본체를 추가로 합성합니다.
bool effectDropShadow(SwCompositor* cmp, SwSurface* surface[2], const RenderEffectDropShadow* params, bool direct)
{
//FIXME: if the body is partially visible due to clipping, the shadow also becomes partially visible.
auto data = static_cast<SwDropShadow*>(params->rd);
auto& bbox = cmp->bbox;
auto w = (bbox.max.x - bbox.min.x);
auto h = (bbox.max.y - bbox.min.y);
//outside the screen
if (abs(data->offset.x) >= w || abs(data->offset.y) >= h) return true;
SwImage* buffer[] = {&surface[0]->compositor->image, &surface[1]->compositor->image};
auto color = cmp->recoverSfc->join(params->color[0], params->color[1], params->color[2], 255);
auto stride = cmp->image.stride;
auto front = cmp->image.buf32;
auto back = buffer[1]->buf32;
auto opacity = direct ? MULTIPLY(params->color[3], cmp->opacity) : params->color[3];
TVGLOG("SW_ENGINE", "DropShadow region(%d, %d, %d, %d) params(%f %f %f), level(%d)", bbox.min.x, bbox.min.y, bbox.max.x, bbox.max.y, params->angle, params->distance, params->sigma, data->level);
//no filter required
if (data->extends == 0) {
// Blur가 없는 경우에도 그림자만 부모 Surface에 직접 합성합니다.
if (direct) {
_dropShadowNoFilter(cmp->recoverSfc->buf32, cmp->image.buf32, cmp->recoverSfc->stride, cmp->image.stride, cmp->recoverSfc->w, cmp->recoverSfc->h, bbox, data->offset, color, opacity, direct);
} else {
_dropShadowNoFilter(buffer[1], &cmp->image, bbox, data->offset, color);
std::swap(cmp->image.buf32, buffer[1]->buf32);
}
return true;
}
//saving the original image in order to overlay it into the filtered image.
_dropShadowFilter(back, front, stride, w, h, bbox, data->kernel[0], color, false);
std::swap(front, buffer[0]->buf32);
std::swap(front, back);
//horizontal
for (int i = 1; i < data->level; ++i) {
_dropShadowFilter(back, front, stride, w, h, bbox, data->kernel[i], color, false);
std::swap(front, back);
}
//vertical
rasterXYFlip(front, back, stride, w, h, bbox, false);
std::swap(front, back);
for (int i = 0; i < data->level; ++i) {
_dropShadowFilter(back, front, stride, h, w, bbox, data->kernel[i], color, true);
std::swap(front, back);
}
rasterXYFlip(front, back, stride, h, w, bbox, true);
std::swap(cmp->image.buf32, back);
//draw to the main surface directly
// [핵심 4] 필터링한 그림자는 부모에 합성하고, cmp->image를 원본 본체로 복원합니다.
if (direct) {
_dropShadowShift(cmp->recoverSfc->buf32, cmp->image.buf32, cmp->recoverSfc->stride, cmp->image.stride, cmp->recoverSfc->w, cmp->recoverSfc->h, bbox, data->offset, opacity, direct);
// cmp->valid는 false로 유지하여 endComposite()가 본체를 합성하도록 합니다.
std::swap(cmp->image.buf32, buffer[0]->buf32);
return true;
}
//draw to the intermediate surface
rasterClear(surface[1], bbox.min.x, bbox.min.y, w, h);
_dropShadowShift(buffer[1]->buf32, cmp->image.buf32, buffer[1]->stride, cmp->image.stride, buffer[1]->w, buffer[1]->h, bbox, data->offset, opacity, direct);
std::swap(cmp->image.buf32, buffer[1]->buf32);
//compositing shadow and body
auto s = buffer[0]->buf32 + (bbox.min.y * buffer[0]->stride + bbox.min.x);
auto d = cmp->image.buf32 + (bbox.min.y * cmp->image.stride + bbox.min.x);
for (auto y = 0; y < h; ++y) {
rasterTranslucentPixel32(d, s, w, 255);
s += buffer[0]->stride;
d += cmp->image.stride;
}
return true;
}endComposite — 컨텍스트는 복구하고, 완료한 합성은 생략
SwRenderer::endComposite() — 전체 함수
bool SwRenderer::endComposite(RenderCompositor* cmp)
{
if (!cmp) return false;
auto p = static_cast<SwCompositor*>(cmp);
//Recover Context
// direct로 이미 합성했더라도 부모 Surface와 compositor는 먼저 복구합니다.
surface = p->recoverSfc;
surface->compositor = p->recoverCmp;
//only invalid (currently used) surface can be composited
// [핵심 5] Fill·Tint·Tritone이 직접 합성을 마쳤다면 여기서 종료합니다.
if (p->valid) return true;
p->valid = true;
//Default is alpha blending
if (p->method == MaskMethod::None) {
// GaussianBlur 결과, DropShadow 본체, 일반 Effect 체인은 여기서 합성합니다.
return rasterDirectImage(surface, p->image, p->bbox, p->opacity);
}
return true;
}2.4. endComposite

endComposite()는 렌더 타겟을 기존 Canvas Surface로 되돌린 뒤 결과를 합성합니다. Canvas를 새로 만들거나 배경을 다시 그리는 과정이 아니라, 보존된 BG 위에 Effect 결과를 기록하는 과정입니다.
이 예제는 사용 중인 compositor의 MaskMethod::None 경로입니다. 부모 컨텍스트를 복구한 뒤 p->valid = true로 재사용 가능 상태를 표시하고, rasterDirectImage()가 그룹 opacity를 반영해 기존 Canvas에 합성합니다. 이미 valid인 compositor는 컨텍스트 복구 후 합성 없이 반환합니다.
코드 보기: endComposite → rasterDirectImage — Canvas에 최종 합성
호출 흐름
canvas->draw()
-> A/B Scene의 SceneImpl::render()
-> effects 처리 완료
-> SwRenderer::endComposite()
-> rasterDirectImage()렌더 타겟을 Canvas Surface로 되돌린 뒤, offscreen의 결과에 그룹 opacity를 반영해 합성합니다.
bool SwRenderer::endComposite(RenderCompositor* cmp)
{
if (!cmp) return false;
auto p = static_cast<SwCompositor*>(cmp);
//Recover Context
surface = p->recoverSfc;
surface->compositor = p->recoverCmp;
//only invalid (currently used) surface can be composited
if (p->valid) return true;
p->valid = true;
//Default is alpha blending
if (p->method == MaskMethod::None) {
return rasterDirectImage(surface, p->image, p->bbox, p->opacity);
}
return true;
}2.5. postRender

postRender()는 Main Scene 렌더링이 돌아온 뒤 호출되며 Scene의 Effect 루프 밖에 있습니다. 이 장면은 Compositing이 끝난 위치만 연결합니다.
현재 예제의 ABGR8888은 premultiplied 바이트를 유지합니다. ABGR8888S·ARGB8888S에서는 unpremultiply 경로를 사용하지만, alpha가 255인 픽셀은 값이 바뀌지 않습니다.
draw()가 반환된 뒤의 task 정리와 Canvas 상태 복귀는 Sync/End에서 이어집니다.
NOTE
Premultiplied는 RGB에 alpha를 미리 곱해 저장하는 표현입니다. 내부 합성에서는 RGB를 정규화하는 나눗셈을 생략할 수 있어 계산이 간단해집니다. 보간·Blur처럼 이웃 픽셀의 채널값을 평균할 때도, alpha가 0인 픽셀의 숨은 RGB가 경계색에 섞이는 것을 피할 수 있습니다.
최종 출력은 호출자가 요청한 표현에 맞춥니다. ABGR8888S·ARGB8888S에서는 postRender()가 RGB를 복원하고, ABGR8888·ARGB8888에서는 Premultiplied를 유지합니다.
3. Effect Details
Effect의 공통 준비 경로는 Update/Prepare의 Appendix에서 다룹니다.
24 × 24 pixels · native CPU values · slowed for inspection.
3.1. GaussianBlur — Separable passes

API와 소스: GaussianBlur
// 시그마, 방향, 경계 처리, 품질
scene->add(SceneEffect::GaussianBlur, 1.4, 0, 0, 100);update()에서 필터 반경과 영역을 준비하고 draw()에서 필터링합니다. 이 예제는 sigma = 1.4, 양방향, 가장자리 제한, 품질 100으로 각 축에 반경 1인 필터를 세 번 적용합니다.
// effectGaussianBlurUpdate(): 커널 준비
auto scale = sqrt(transform.e11 * transform.e11 +
transform.e12 * transform.e12);
rd->extends = _gaussianInit(rd,
std::pow(params->sigma * scale, 2), params->quality);호출 흐름
canvas->draw()
-> SceneImpl::render()
-> SwRenderer::render(effect)
-> request(Scratch)
-> effectGaussianBlur()
-> _gaussianFilter()
-> rasterXYFlip()각 단계는 한 버퍼를 읽고 다른 버퍼에 기록합니다. 수직 필터링은 버퍼를 전치해 수평 접근 방식을 재사용합니다.
SwRenderer::render() — GaussianBlur 처리 분기
case SceneEffect::GaussianBlur: {
return effectGaussianBlur(p, request(surface->channelSize, true), static_cast<const RenderEffectGaussianBlur*>(effect));
}Scratch 버퍼 확보 — 캐시 재사용 또는 신규 할당
SceneImpl::render()
-> SwRenderer::render(cmp, effect, direct)
-> SwRenderer::request(surface->channelSize, true)위 GaussianBlur 분기의 request(..., true)가 Scratch로 쓸 SwSurface를 반환합니다. 매번 새로 할당하는 것은 아니며, 크기와 채널 크기가 맞는 사용 가능한 캐시를 먼저 찾고, 없을 때 tvg::malloc()으로 픽셀 버퍼를 만듭니다. square = true는 행·열 전치를 위해 현재 Surface의 긴 변을 기준으로 정사각형 버퍼를 확보한다는 뜻입니다.
SwSurface* SwRenderer::request(int channelSize, bool square)
{
SwSurface* cmp = nullptr;
uint32_t w, h;
if (square) {
// 후처리에서 빠르게 전치하려면 가로와 세로 크기가 같아야 합니다.
w = h = std::max(surface->w, surface->h);
} else {
w = surface->w;
h = surface->h;
}
// 캐시된 데이터 재사용
ARRAY_FOREACH(p, compositors) {
auto cur = *p;
if (cur->compositor->valid && cur->compositor->image.channelSize == channelSize) {
if (w == cur->w && h == cur->h) {
cmp = *p;
break;
}
}
}
// 새 합성 버퍼 생성
if (!cmp) {
// 메인 Surface의 속성 상속
cmp = new SwSurface(surface);
cmp->compositor = new SwCompositor;
cmp->compositor->image.data = tvg::malloc<pixel_t>(channelSize * w * h);
cmp->w = cmp->compositor->image.w = w;
cmp->h = cmp->compositor->image.h = h;
cmp->stride = cmp->compositor->image.stride = w;
cmp->compositor->image.direct = true;
cmp->compositor->valid = true;
cmp->channelSize = cmp->compositor->image.channelSize = channelSize;
compositors.push(cmp);
}
// 후처리에서 변경될 수 있는 버퍼 주소를 동기화합니다.
cmp->data = cmp->compositor->image.data;
return cmp;
}반환된 Surface는 아래 effectGaussianBlur()의 두 번째 인자입니다. cmp->image.buf32가 초기 front(Offscreen), surface->compositor->image.buf32가 초기 back(Scratch)이 됩니다. 이 요청은 Shape를 그릴 현재 타깃을 바꾸지 않으며, 필터는 두 버퍼를 읽기·쓰기 용도로 번갈아 사용합니다.
SwRenderer::render(cmp, effect, direct)
-> effectGaussianBlur(p, 반환된 Scratch Surface, params)bool effectGaussianBlur(SwCompositor* cmp, SwSurface* surface, const RenderEffectGaussianBlur* params)
{
auto& buffer = surface->compositor->image;
auto data = static_cast<SwGaussianBlur*>(params->rd);
auto& bbox = cmp->bbox;
auto w = (bbox.max.x - bbox.min.x);
auto h = (bbox.max.y - bbox.min.y);
auto stride = cmp->image.stride;
auto front = cmp->image.buf32;
auto back = buffer.buf32;
auto swapped = false;
TVGLOG("SW_ENGINE", "GaussianFilter region(%d, %d, %d, %d) params(%f %d %d), level(%d)", bbox.min.x, bbox.min.y, bbox.max.x, bbox.max.y, params->sigma, params->direction, params->border, data->level);
/* 가우시안 블러의 분리 가능성을 이용해 수평·수직 두 단계로
나누면 계산량을 줄일 수 있습니다. */
// 수평 방향
if (params->direction != 2) {
for (int i = 0; i < data->level; ++i) {
_gaussianFilter(reinterpret_cast<uint8_t*>(back), reinterpret_cast<uint8_t*>(front), stride, w, h, bbox, data->kernel[i], false);
std::swap(front, back);
swapped = !swapped;
}
}
// 수직 방향: x/y를 전치해 메모리 접근에 유리한 수평 방식으로 처리합니다.
if (params->direction != 1) {
rasterXYFlip(front, back, stride, w, h, bbox, false);
std::swap(front, back);
for (int i = 0; i < data->level; ++i) {
_gaussianFilter(reinterpret_cast<uint8_t*>(back), reinterpret_cast<uint8_t*>(front), stride, h, w, bbox, data->kernel[i], true);
std::swap(front, back);
swapped = !swapped;
}
rasterXYFlip(front, back, stride, h, w, bbox, true);
std::swap(front, back);
}
if (swapped) std::swap(cmp->image.buf8, buffer.buf8);
return true;
}_gaussianFilter() — 한 방향의 Box Filter 실행
SceneImpl::render()
-> SwRenderer::render(cmp, effect, direct)
-> effectGaussianBlur()
-> _gaussianFilter()src의 채널별 누적합을 이동시키며 2 × dimension + 1개 샘플의 평균을 dst에 기록합니다. 수직 방향도 전치한 버퍼에 같은 함수를 적용합니다. 아래 호출 경로는 기본 border = 0을 사용하며, 경계 밖 인덱스는 _gaussianRemap()에서 가장자리로 제한합니다.
_gaussianFilter() — 전체 함수, _gaussianRemap()
template<int border = 0>
static void _gaussianFilter(uint8_t* dst, uint8_t* src, int32_t stride, int32_t w, int32_t h, const RenderRegion& bbox, int32_t dimension, bool flipped)
{
if (flipped) {
src += (bbox.min.x * stride + bbox.min.y) << 2;
dst += (bbox.min.x * stride + bbox.min.y) << 2;
} else {
src += (bbox.min.y * stride + bbox.min.x) << 2;
dst += (bbox.min.y * stride + bbox.min.x) << 2;
}
auto iarr = 1.0f / (dimension + dimension + 1);
auto end = w - 1;
#pragma omp parallel for
for (int y = 0; y < h; ++y) {
auto p = y * stride;
auto i = p * 4; // 현재 인덱스
auto l = -(dimension + 1); // 왼쪽 인덱스
auto r = dimension; // 오른쪽 인덱스
int acc[4] = {0, 0, 0, 0}; // 이동 누적합
// 초기 누적합 계산
for (int x = l; x < r; ++x) {
auto id = (_gaussianRemap<border>(end, x) + p) * 4;
acc[0] += src[id++];
acc[1] += src[id++];
acc[2] += src[id++];
acc[3] += src[id];
}
// 필터링 실행
for (int x = 0; x < w; ++x, ++r, ++l) {
auto rid = (_gaussianRemap<border>(end, r) + p) * 4;
auto lid = (_gaussianRemap<border>(end, l) + p) * 4;
acc[0] += src[rid++] - src[lid++];
acc[1] += src[rid++] - src[lid++];
acc[2] += src[rid++] - src[lid++];
acc[3] += src[rid] - src[lid];
// 성능을 위해 반올림을 생략합니다. 원래 식은 acc[idx] * iarr + 0.5f입니다.
dst[i++] = static_cast<uint8_t>(acc[0] * iarr);
dst[i++] = static_cast<uint8_t>(acc[1] * iarr);
dst[i++] = static_cast<uint8_t>(acc[2] * iarr);
dst[i++] = static_cast<uint8_t>(acc[3] * iarr);
}
}
}3.2. Tint — Luma to color

API와 소스: Tint
scene->opacity(128);
scene->add(SceneEffect::GaussianBlur, 1.4, 0, 0, 100);
// 어두운 쪽 RGB, 밝은 쪽 RGB, 강도(%)
scene->add(SceneEffect::Tint, 31, 51, 97, 255, 199, 102, 75.0);이 Blur → Tint 예제는 direct = false 경로를 사용합니다. 강도 75%는 8비트 값 191이 되며, 픽셀 반복문에서 합성기 불투명도 128도 적용합니다.
호출 흐름
canvas->draw()
-> SceneImpl::render()
-> SwRenderer::render(effect)
-> effectTint(direct = false)
-> luma() / INTERPOLATE()
-> ALPHA_BLEND() 결과를 Offscreen에 기록// effectTint(): 직접 처리하지 않는 픽셀 반복문
for (size_t x = 0; x < w; ++x, ++dst) {
auto val = INTERPOLATE(white, black, luma((uint8_t*)dst));
if (params->intensity < 255) {
val = INTERPOLATE(val, *dst, params->intensity);
}
*dst = ALPHA_BLEND(val, MULTIPLY(opacity, A(*dst)));
}3.3. Tritone — Two branches, three colors

API와 소스: Tritone
scene->opacity(128);
scene->add(SceneEffect::GaussianBlur, 1.4, 0, 0, 100);
// 어두운 톤 RGB, 중간 톤 RGB, 밝은 톤 RGB, 원본 혼합 비율
scene->add(SceneEffect::Tritone,
28, 51, 97, 224, 99, 48, 255, 224, 143, 64);밝은 픽셀의 확대 예제는 불투명도 173, 혼합 값 111인 별도 입력으로, 메인 A·B trace를 바꾸지 않고 밝은 쪽 분기를 보여줍니다.
호출 흐름
canvas->draw()
-> SceneImpl::render()
-> SwRenderer::render(effect)
-> effectTritone(direct = false)
-> luma() / _trintone()
-> INTERPOLATE() / ALPHA_BLEND()
-> 같은 Offscreen 픽셀에 기록_trintone() · effectTritone(), SceneEffect API, 효과 처리 분기
// _trintone(): 휘도에 따른 분기
if (l < 128) {
auto a = std::min(l * 2, 255);
return ALPHA_BLEND(s, 255 - a) + ALPHA_BLEND(m, a);
} else {
auto a = 2 * std::max(0, l - 128);
return ALPHA_BLEND(m, 255 - a) + ALPHA_BLEND(h, a);
}
// effectTritone(): 직접 처리하지 않는 픽셀 반복문, blender != 0
for (size_t x = 0; x < w; ++x, ++dst) {
*dst = ALPHA_BLEND(
INTERPOLATE(*dst,
_trintone(shadow, midtone, highlight, luma((uint8_t*)dst)),
params->blender),
MULTIPLY(A(*dst), opacity));
}3.4. SceneEffect::Fill — Recolor through alpha

API와 소스: Fill
scene->add(SceneEffect::GaussianBlur, 1.4, 0, 0, 100);
// 채우기 RGB, 효과 불투명도
scene->add(SceneEffect::Fill, 49, 150, 123, 255);호출 흐름
canvas->draw()
-> SceneImpl::render()
-> SwRenderer::render(effect)
-> effectFill()
-> direct == false직접 처리하지 않는 경로에서는 효과의 불투명도를 사용하고, 그룹 불투명도는 이후 endComposite()에서 적용합니다.
auto opacity = direct ? MULTIPLY(params->color[3], cmp->opacity) : params->color[3];
auto& bbox = cmp->bbox;
auto w = size_t(bbox.max.x - bbox.min.x);
auto h = size_t(bbox.max.y - bbox.min.y);
auto color = cmp->recoverSfc->join(params->color[0], params->color[1], params->color[2], 255);
// ... 중간 코드 생략
auto dbuffer = cmp->image.buf32 + (bbox.min.y * cmp->image.stride + bbox.min.x);
for (size_t y = 0; y < h; ++y) {
auto dst = dbuffer;
for (size_t x = 0; x < w; ++x, ++dst) {
*dst = ALPHA_BLEND(color, MULTIPLY(opacity, A(*dst)));
}
dbuffer += cmp->image.stride;
}3.5. DropShadow — Alpha, offset, composite

API와 소스: DropShadow
scene->opacity(128);
// 그림자 RGB, 불투명도, 각도, 거리, 블러 시그마, 품질
scene->add(SceneEffect::DropShadow,
32, 40, 52, 180, 135.0, 4.0, 1.4, 100);이 단일 효과 예제는 direct = true 경로에서 임시 Surface 두 개로 원본을 보존하고 알파를 필터링한 뒤, 그림자를 먼저 그리고 endComposite()로 원본을 합성합니다.
// effectDropShadowUpdate(): 정수 좌표 이동량
auto radian = tvg::deg2rad(90.0f - params->angle)
- tvg::radian(transform);
rd->offset = {
(int32_t)((params->distance * scale) * cosf(radian)),
(int32_t)(-1.0f * (params->distance * scale) * sinf(radian))
};호출 흐름
canvas->draw()
-> SceneImpl::render()
-> SwRenderer::render(effect) / 임시 Surface 두 개 요청
-> effectDropShadow(direct = true)
-> _dropShadowFilter() / rasterXYFlip()
-> _dropShadowShift() / 원본 이미지 복원
-> endComposite()
-> rasterDirectImage(body)effectDropShadow(), 단일 효과의 직접 처리 분기
// 필터링과 역전치 후 직접 처리 경로
if (direct) {
_dropShadowShift(cmp->recoverSfc->buf32, cmp->image.buf32,
cmp->recoverSfc->stride, cmp->image.stride,
cmp->recoverSfc->w, cmp->recoverSfc->h,
bbox, data->offset, opacity, direct);
std::swap(cmp->image.buf32, buffer[0]->buf32);
return true;
}
// 이후 SceneImpl::render()에서 endComposite(cmp)를 호출합니다.3.5.1. 효과 적용 순서
각 순서를 새로 만든 Scene(A, B)에 적용합니다. 효과는 등록한 순서대로 실행됩니다.
DropShadow → Fill
scene->opacity(128);
scene->add(SceneEffect::DropShadow,
32, 40, 52, 180, 135.0, 4.0, 1.4, 100);
scene->add(SceneEffect::Fill, 49, 150, 123, 255);
Fill → DropShadow
scene->opacity(128);
scene->add(SceneEffect::Fill, 49, 150, 123, 255);
scene->add(SceneEffect::DropShadow,
32, 40, 52, 180, 135.0, 4.0, 1.4, 100);

Separate illustrative input; floating-point model.