renderer: revise the Bounding Box Behavior

- modify the concept of AABB to apply only to transformed shapes.
- transform points before computing the bounding box min/max
  to obtain a more compact shape region.
- trimmming memory by removing the cached matrix, about 36kb
  of memory has been reduced per paint instance.
This commit is contained in:
Hermet Park 2025-03-19 00:33:23 +09:00 committed by Hermet Park
parent a7efd0f199
commit 333e65ef7a
18 changed files with 113 additions and 105 deletions

View file

@ -439,8 +439,6 @@ public:
* @retval Result::InvalidArguments @p pt4 is @c nullptr.
* @retval Result::InsufficientCondition If it failed to compute the bounding box (mostly due to invalid path information).
*
* @note The paint must be pushed into a canvas and updated before calling this function.
*
* @see Paint::bounds(float* x, float* y, folat* w, float* h)
* @see Canvas::update()
*
@ -449,9 +447,9 @@ public:
Result bounds(Point* pt4) const noexcept;
/**
* @brief Retrieves the axis-aligned bounding box (AABB) of the paint object in local space.
* @brief Retrieves the axis-aligned bounding box (AABB) of the paint object in canvas space.
*
* This function returns the bounding box of the paint object relative to its local coordinate system, without applying any transformations.
* This function returns the bounding box of the paint, as an axis-aligned bounding box (AABB) after transformations are applied.
*
* @param[out] x The x-coordinate of the upper-left corner of the bounding box.
* @param[out] y The y-coordinate of the upper-left corner of the bounding box.
@ -460,8 +458,6 @@ public:
*
* @retval Result::InsufficientCondition If it failed to compute the bounding box (mostly due to invalid path information).
*
* @note The bounding box is calculated in the object's local space, meaning transformations such as scaling, rotation, or translation are not applied.
*
* @see Paint::bounds(Point* pt4)
* @see Canvas::update()
*/

View file

@ -912,9 +912,9 @@ TVG_API Tvg_Paint* tvg_paint_duplicate(Tvg_Paint* paint);
/**
* @brief Retrieves the axis-aligned bounding box (AABB) of the paint object in local space.
* @brief Retrieves the axis-aligned bounding box (AABB) of the paint object in canvas space.
*
* This function returns the bounding box of the paint object relative to its local coordinate system, without applying any transformations.
* This function returns the bounding box of the paint, as an axis-aligned bounding box (AABB) after transformations are applied.
*
* @param[in] paint The Tvg_Paint object of which to get the bounds.
* @param[out] x The x-coordinate of the upper-left corner of the bounding box.
@ -926,8 +926,6 @@ TVG_API Tvg_Paint* tvg_paint_duplicate(Tvg_Paint* paint);
* @retval TVG_RESULT_INVALID_ARGUMENT An invalid @p paint.
* @retval TVG_RESULT_INSUFFICIENT_CONDITION If it failed to compute the bounding box (mostly due to invalid path information).
*
* @note The bounding box is calculated in the object's local space, meaning transformations such as scaling, rotation, or translation are not applied.
*
* @see tvg_paint_get_obb()
* @see tvg_canvas_update_paint()
*/
@ -946,8 +944,6 @@ TVG_API Tvg_Result tvg_paint_get_aabb(const Tvg_Paint* paint, float* x, float* y
* @retval TVG_RESULT_INVALID_ARGUMENT @p paint or @p pt4 is invalid.
* @retval TVG_RESULT_INSUFFICIENT_CONDITION If it failed to compute the bounding box (mostly due to invalid path information).
*
* @note The paint must be pushed into a canvas and updated before calling this function.
*
* @see tvg_paint_get_aabb()
* @see tvg_canvas_update_paint()
*

View file

@ -115,6 +115,12 @@ static inline void identity(Matrix* m)
}
static inline constexpr const Matrix identity()
{
return {1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f};
}
static inline void scale(Matrix* m, const Point& p)
{
m->e11 *= p.x;
@ -178,6 +184,20 @@ Point normal(const Point& p1, const Point& p2);
void normalize(Point& pt);
static inline constexpr const Point operator*=(Point& pt, const Matrix* m)
{
if (m) pt *= *m;
return pt;
}
static inline Point operator*(const Point& pt, const Matrix* m)
{
if (!m) return pt;
return pt * *m;
}
static inline Point min(const Point& lhs, const Point& rhs)
{
return {std::min(lhs.x, rhs.x), std::min(lhs.y, rhs.y)};

View file

@ -553,8 +553,7 @@ void LottieBuilder::updateStar(LottiePolyStar* star, float frameNo, Matrix* tran
hasRoundness = true;
}
Point in = {x, y};
if (transform) in *= *transform;
auto in = Point{x, y} * transform;
shape->moveTo(in.x, in.y);
for (size_t i = 0; i < numPoints; i++) {
@ -595,18 +594,12 @@ void LottieBuilder::updateStar(LottiePolyStar* star, float frameNo, Matrix* tran
cp2x *= partialPointAmount;
cp2y *= partialPointAmount;
}
Point in2 = {previousX - cp1x, previousY - cp1y};
Point in3 = {x + cp2x, y + cp2y};
Point in4 = {x, y};
if (transform) {
in2 *= *transform;
in3 *= *transform;
in4 *= *transform;
}
auto in2 = Point{previousX - cp1x, previousY - cp1y} * transform;
auto in3 = Point{x + cp2x, y + cp2y} * transform;
auto in4 = Point{x, y} * transform;
shape->cubicTo(in2.x, in2.y, in3.x, in3.y, in4.x, in4.y);
} else {
Point in = {x, y};
if (transform) in *= *transform;
auto in = Point{x, y} * transform;
shape->lineTo(in.x, in.y);
}
angle += dTheta * direction;
@ -651,8 +644,7 @@ void LottieBuilder::updatePolygon(LottieGroup* parent, LottiePolyStar* star, flo
}
}
Point in = {x, y};
if (transform) in *= *transform;
auto in = Point{x, y} * transform;
shape->moveTo(in.x, in.y);
for (size_t i = 0; i < ptsCnt; i++) {
@ -674,14 +666,9 @@ void LottieBuilder::updatePolygon(LottieGroup* parent, LottiePolyStar* star, flo
auto cp2x = radius * outerRoundness * POLYGON_MAGIC_NUMBER * cp2Dx;
auto cp2y = radius * outerRoundness * POLYGON_MAGIC_NUMBER * cp2Dy;
Point in2 = {previousX - cp1x, previousY - cp1y};
Point in3 = {x + cp2x, y + cp2y};
Point in4 = {x, y};
if (transform) {
in2 *= *transform;
in3 *= *transform;
in4 *= *transform;
}
auto in2 = Point{previousX - cp1x, previousY - cp1y} * transform;
auto in3 = Point{x + cp2x, y + cp2y} * transform;
auto in4 = Point{x, y} * transform;
shape->cubicTo(in2.x, in2.y, in3.x, in3.y, in4.x, in4.y);
} else {
Point in = {x, y};

View file

@ -2996,7 +2996,7 @@ static void _inheritGradient(SvgLoaderData* loader, SvgStyleGradient* to, SvgSty
if (!to->transform && from->transform) {
to->transform = tvg::malloc<Matrix*>(sizeof(Matrix));
if (to->transform) memcpy(to->transform, from->transform, sizeof(Matrix));
if (to->transform) *to->transform = *from->transform;
}
if (to->type == SvgGradientType::Linear) {
@ -3061,7 +3061,7 @@ static SvgStyleGradient* _cloneGradient(SvgStyleGradient* from)
if (from->transform) {
grad->transform = tvg::calloc<Matrix*>(1, sizeof(Matrix));
if (grad->transform) memcpy(grad->transform, from->transform, sizeof(Matrix));
if (grad->transform) *grad->transform = *from->transform;
}
if (grad->type == SvgGradientType::Linear) {

View file

@ -50,7 +50,7 @@ static inline bool _isGroupType(SvgNodeType type)
static Box _boundingBox(Paint* shape)
{
float x, y, w, h;
PAINT(shape)->bounds(&x, &y, &w, &h, false);
PAINT(shape)->bounds(&x, &y, &w, &h, nullptr, false);
return {x, y, w, h};
}
@ -219,7 +219,7 @@ static bool _appendClipChild(SvgLoaderData& loaderData, SvgNode* node, Shape* sh
if (node->type == SvgNodeType::Use) {
if (node->child.count != 1) return false;
auto child = *(node->child.data);
Matrix finalTransform = {1, 0, 0, 0, 1, 0, 0, 0, 1};
auto finalTransform = tvg::identity();
if (node->transform) finalTransform = *node->transform;
if (node->node.use.x != 0.0f || node->node.use.y != 0.0f) {
finalTransform *= {1, 0, node->node.use.x, 0, 1, node->node.use.y, 0, 0, 1};
@ -234,7 +234,7 @@ static bool _appendClipChild(SvgLoaderData& loaderData, SvgNode* node, Shape* sh
static Matrix _compositionTransform(Paint* paint, const SvgNode* node, const SvgNode* compNode, SvgNodeType type)
{
Matrix m = {1, 0, 0, 0, 1, 0, 0, 0, 1};
auto m = tvg::identity();
//The initial mask transformation ignored according to the SVG standard.
if (node->transform && type != SvgNodeType::Mask) {
m = *node->transform;
@ -244,7 +244,7 @@ static Matrix _compositionTransform(Paint* paint, const SvgNode* node, const Svg
}
if (!compNode->node.clip.userSpace) {
float x, y, w, h;
PAINT(paint)->bounds(&x, &y, &w, &h, false);
PAINT(paint)->bounds(&x, &y, &w, &h, nullptr, false);
m *= {w, 0, x, 0, h, y, 0, 0, 1};
}
return m;
@ -685,7 +685,7 @@ static Paint* _imageBuildHelper(SvgLoaderData& loaderData, SvgNode* node, const
auto sy = node->node.image.h / h;
m = {sx, 0, node->node.image.x, 0, sy, node->node.image.y, 0, 0, 1};
} else {
m = {1, 0, 0, 0, 1, 0, 0, 0, 1};
m = tvg::identity();
}
if (node->transform) m = *node->transform * m;
picture->transform(m);
@ -773,7 +773,7 @@ static Scene* _useBuildHelper(SvgLoaderData& loaderData, const SvgNode* node, co
auto scene = _sceneBuildHelper(loaderData, node, vBox, svgPath, false, depth + 1);
// mUseTransform = mUseTransform * mTranslate
Matrix mUseTransform = {1, 0, 0, 0, 1, 0, 0, 0, 1};
auto mUseTransform = tvg::identity();
if (node->transform) mUseTransform = *node->transform;
if (node->node.use.x != 0.0f || node->node.use.y != 0.0f) {
Matrix mTranslate = {1, 0, node->node.use.x, 0, 1, node->node.use.y, 0, 0, 1};
@ -789,7 +789,7 @@ static Scene* _useBuildHelper(SvgLoaderData& loaderData, const SvgNode* node, co
auto vw = (symbol.hasViewBox ? symbol.vw : width);
auto vh = (symbol.hasViewBox ? symbol.vh : height);
Matrix mViewBox = {1, 0, 0, 0, 1, 0, 0, 0, 1};
auto mViewBox = tvg::identity();
if ((!tvg::equal(width, vw) || !tvg::equal(height, vh)) && vw > 0 && vh > 0) {
Box box = {symbol.vx, symbol.vy, vw, vh};
mViewBox = _calculateAspectRatioMatrix(symbol.align, symbol.meetOrSlice, width, height, box);
@ -864,7 +864,7 @@ static Paint* _textBuildHelper(SvgLoaderData& loaderData, const SvgNode* node, c
Matrix textTransform;
if (node->transform) textTransform = *node->transform;
else textTransform = {1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f};
else textTransform = tvg::identity();
translateR(&textTransform, {node->node.text.x, node->node.text.y - textNode->fontSize});
text->transform(textTransform);

View file

@ -630,7 +630,7 @@ bool SwRenderer::endComposite(RenderCompositor* cmp)
//Default is alpha blending
if (p->method == MaskMethod::None) {
Matrix m = {1, 0, 0, 0, 1, 0, 0, 0, 1};
auto m = tvg::identity();
return rasterImage(surface, &p->image, m, p->bbox, p->opacity);
}

View file

@ -71,7 +71,7 @@ struct Canvas::Impl
auto flag = RenderUpdateFlag::None;
if (status == Status::Damaged || force) flag = RenderUpdateFlag::All;
auto m = Matrix{1, 0, 0, 0, 1, 0, 0, 0, 1};
auto m = tvg::identity();
if (!renderer->preUpdate()) return Result::InsufficientCondition;

View file

@ -24,6 +24,7 @@
#define _TVG_FILL_H_
#include "tvgCommon.h"
#include "tvgMath.h"
#define LINEAR(A) PIMPL(A, LinearGradient)
#define RADIAL(A) PIMPL(A, RadialGradient)
@ -31,7 +32,7 @@
struct Fill::Impl
{
ColorStop* colorStops = nullptr;
Matrix transform = {1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f};
Matrix transform = tvg::identity();
uint16_t cnt = 0;
FillSpread spread = FillSpread::Pad;

View file

@ -267,8 +267,7 @@ RenderData Paint::Impl::update(RenderMethod* renderer, const Matrix& pm, Array<R
RenderData rd = nullptr;
tr.cm = pm * tr.m;
PAINT_METHOD(rd, update(renderer, tr.cm, clips, opacity, newFlag, clipper));
PAINT_METHOD(rd, update(renderer, pm * tr.m, clips, opacity, newFlag, clipper));
/* 4. Composition Post Processing */
if (compFastTrack == Result::Success) renderer->viewport(viewport);
@ -278,10 +277,10 @@ RenderData Paint::Impl::update(RenderMethod* renderer, const Matrix& pm, Array<R
}
bool Paint::Impl::bounds(float* x, float* y, float* w, float* h, bool stroking)
Result Paint::Impl::bounds(float* x, float* y, float* w, float* h, Matrix* pm, bool stroking)
{
Point pts[4];
if (!bounds(pts, false, stroking, false)) return false;
if (bounds(pts, pm, false, stroking) != Result::Success) return Result::InsufficientCondition;
Point min = {FLT_MAX, FLT_MAX};
Point max = {-FLT_MAX, -FLT_MAX};
@ -297,23 +296,17 @@ bool Paint::Impl::bounds(float* x, float* y, float* w, float* h, bool stroking)
if (y) *y = min.y;
if (w) *w = max.x - min.x;
if (h) *h = max.y - min.y;
return true;
return Result::Success;
}
bool Paint::Impl::bounds(Point* pt4, bool transformed, bool stroking, bool origin)
Result Paint::Impl::bounds(Point* pt4, Matrix* pm, bool obb, bool stroking)
{
bool ret;
PAINT_METHOD(ret, bounds(pt4, stroking));
if (!ret || !transformed) return ret;
const auto& m = this->transform(origin);
pt4[0] *= m;
pt4[1] *= m;
pt4[2] *= m;
pt4[3] *= m;
auto m = this->transform();
if (pm) m = *pm * m;
Result ret;
PAINT_METHOD(ret, bounds(pt4, m, obb, stroking));
return ret;
}
@ -369,16 +362,16 @@ Matrix& Paint::transform() noexcept
Result Paint::bounds(float* x, float* y, float* w, float* h) const noexcept
{
if (pImpl->bounds(x, y, w, h, true)) return Result::Success;
return Result::InsufficientCondition;
auto pm = pImpl->ptransform();
return pImpl->bounds(x, y, w, h, &pm, true);
}
Result Paint::bounds(Point* pt4) const noexcept
{
if (!pt4) return Result::InvalidArguments;
if (pImpl->bounds(pt4, true, true, true)) return Result::Success;
return Result::InsufficientCondition;
auto pm = pImpl->ptransform();
return pImpl->bounds(pt4, &pm, true, true);
}

View file

@ -59,7 +59,6 @@ namespace tvg
struct {
Matrix m; //input matrix
Matrix cm; //multipled parents matrix
float degree; //rotation degree
float scale; //scale factor
bool overriding; //user transform?
@ -144,14 +143,24 @@ namespace tvg
return true;
}
Matrix& transform(bool origin = false)
Matrix& transform()
{
//update transform
if (renderFlag & RenderUpdateFlag::Transform) tr.update();
if (origin) return tr.cm;
return tr.m;
}
Matrix ptransform()
{
auto p = this;
auto tm = identity();
while (p->parent) {
p = PAINT(p->parent);
tm = p->transform() * tm;
}
return tm;
}
Result clip(Paint* clp)
{
if (PAINT(clp)->parent) return Result::InsufficientCondition;
@ -263,8 +272,8 @@ namespace tvg
RenderRegion bounds(RenderMethod* renderer) const;
Iterator* iterator();
bool bounds(float* x, float* y, float* w, float* h, bool stroking);
bool bounds(Point* pt4, bool transformed, bool stroking, bool origin = false);
Result bounds(float* x, float* y, float* w, float* h, Matrix* pm, bool stroking);
Result bounds(Point* pt4, Matrix* pm, bool obb, bool stroking);
RenderData update(RenderMethod* renderer, const Matrix& pm, Array<RenderData>& clips, uint8_t opacity, RenderUpdateFlag pFlag, bool clipper = false);
bool render(RenderMethod* renderer);
Paint* duplicate(Paint* ret = nullptr);

View file

@ -114,13 +114,13 @@ struct Picture::Impl : Paint::Impl
return Result::Success;
}
bool bounds(Point* pt4, bool stroking)
Result bounds(Point* pt4, Matrix& m, TVG_UNUSED bool obb, TVG_UNUSED bool stroking)
{
pt4[0] = {0.0f, 0.0f};
pt4[1] = {w, 0.0f};
pt4[2] = {w, h};
pt4[3] = {0.0f, h};
return true;
pt4[0] = Point{0.0f, 0.0f} * m;
pt4[1] = Point{w, 0.0f} * m;
pt4[2] = Point{w, h} * m;
pt4[3] = Point{0.0f, h} * m;
return Result::Success;
}
Result load(const char* filename)

View file

@ -44,7 +44,7 @@ uint32_t RenderMethod::unref()
/* RenderPath Class Implementation */
/************************************************************************/
bool RenderPath::bounds(float* x, float* y, float* w, float* h)
bool RenderPath::bounds(Matrix* m, float* x, float* y, float* w, float* h)
{
//unexpected
if (cmds.empty() || cmds.first() == PathCommand::CubicTo) return false;
@ -55,11 +55,11 @@ bool RenderPath::bounds(float* x, float* y, float* w, float* h)
auto pt = pts.begin();
auto cmd = cmds.begin();
auto assign = [&](Point* pt, Point& min, Point& max) -> void {
if (pt->x < min.x) min.x = pt->x;
if (pt->y < min.y) min.y = pt->y;
if (pt->x > max.x) max.x = pt->x;
if (pt->y > max.y) max.y = pt->y;
auto assign = [&](const Point& pt, Point& min, Point& max) -> void {
if (pt.x < min.x) min.x = pt.x;
if (pt.y < min.y) min.y = pt.y;
if (pt.x > max.x) max.x = pt.x;
if (pt.y > max.y) max.y = pt.y;
};
while (cmd < cmds.end()) {
@ -69,19 +69,19 @@ bool RenderPath::bounds(float* x, float* y, float* w, float* h)
if (cmd + 1 < cmds.end()) {
auto next = *(cmd + 1);
if (next == PathCommand::LineTo || next == PathCommand::CubicTo) {
assign(pt, min, max);
assign(*pt * m, min, max);
}
}
++pt;
break;
}
case PathCommand::LineTo: {
assign(pt, min, max);
assign(*pt * m, min, max);
++pt;
break;
}
case PathCommand::CubicTo: {
Bezier bz = {pt[-1], pt[0], pt[1], pt[2]};
Bezier bz = {pt[-1] * m, pt[0] * m, pt[1] * m, pt[2] * m};
bz.bounds(min, max);
pt += 3;
break;

View file

@ -104,7 +104,7 @@ struct RenderPath
cmds.clear();
}
bool bounds(float* x, float* y, float* w, float* h);
bool bounds(Matrix* m, float* x, float* y, float* w, float* h);
};
struct RenderTrimPath

View file

@ -194,16 +194,16 @@ struct Scene::Impl : Paint::Impl
return ret;
}
bool bounds(Point* pt4, bool stroking)
Result bounds(Point* pt4, Matrix& m, TVG_UNUSED bool obb, bool stroking)
{
if (paints.empty()) return false;
if (paints.empty()) return Result::InsufficientCondition;
Point min = {FLT_MAX, FLT_MAX};
Point max = {-FLT_MAX, -FLT_MAX};
for (auto paint : paints) {
Point tmp[4];
if (!PAINT(paint)->bounds(tmp, true, stroking)) continue;
if (PAINT(paint)->bounds(tmp, nullptr, false, stroking) != Result::Success) continue;
//Merge regions
for (int i = 0; i < 4; ++i) {
if (tmp[i].x < min.x) min.x = tmp[i].x;
@ -212,12 +212,12 @@ struct Scene::Impl : Paint::Impl
if (tmp[i].y > max.y) max.y = tmp[i].y;
}
}
pt4[0] = min;
pt4[1] = {max.x, min.y};
pt4[2] = max;
pt4[3] = {min.x, max.y};
pt4[0] = min * m;
pt4[1] = Point{max.x, min.y} * m;
pt4[2] = max * m;
pt4[3] = Point{min.x, max.y} * m;
return true;
return Result::Success;
}
Paint* duplicate(Paint* ret)

View file

@ -116,10 +116,10 @@ struct Shape::Impl : Paint::Impl
return renderer->region(rd);
}
bool bounds(Point* pt4, bool stroking)
Result bounds(Point* pt4, Matrix& m, bool obb, bool stroking)
{
float x, y, w, h;
if (!rs.path.bounds(&x, &y, &w, &h)) return false;
if (!rs.path.bounds(obb ? nullptr : &m, &x, &y, &w, &h)) return Result::InsufficientCondition;
//Stroke feathering
if (stroking && rs.stroke) {
@ -134,7 +134,14 @@ struct Shape::Impl : Paint::Impl
pt4[2] = {x + w, y + h};
pt4[3] = {x, y + h};
return true;
if (obb) {
pt4[0] *= m;
pt4[1] *= m;
pt4[2] *= m;
pt4[3] *= m;
}
return Result::Success;
}
void reserveCmd(uint32_t cmdCnt)

View file

@ -135,11 +135,10 @@ struct Text::Impl : Paint::Impl
return PAINT(shape)->update(renderer, transform, clips, opacity, pFlag, false);
}
bool bounds(Point* pt4, TVG_UNUSED bool stroking)
Result bounds(Point* pt4, Matrix& m, bool obb, TVG_UNUSED bool stroking)
{
if (!load()) return false;
PAINT(shape)->bounds(pt4, true, true, false);
return true;
if (!load()) return Result::InsufficientCondition;
return PAINT(shape)->bounds(pt4, &m, obb, true);
}
Paint* duplicate(Paint* ret)

View file

@ -133,8 +133,8 @@ TEST_CASE("Bounding Box", "[tvgPaint]")
REQUIRE(shape->appendRect(0.0f, 10.0f, 20.0f, 100.0f, 50.0f, 50.0f) == Result::Success);
REQUIRE(shape->translate(100.0f, 111.0f) == Result::Success);
REQUIRE(shape->bounds(&x, &y, &w, &h) == Result::Success);
REQUIRE(x == 0.0f);
REQUIRE(y == 10.0f);
REQUIRE(x == 100.0f);
REQUIRE(y == 121.0f);
REQUIRE(w == 20.0f);
REQUIRE(h == 100.0f);