From 851b2189a8d31b9306f696c38988bbc554fa9e0c Mon Sep 17 00:00:00 2001 From: Volker Enderlein Date: Mon, 30 Sep 2019 13:48:44 +0200 Subject: Fix for bounding volume handling and calculation - Fixed Ritter algorithm implementation - Added notation of invalid bounding sphere (radius == -1.0) - Handle merging of invalid bounding sphere with valid ones - Added test cases and adjusted tests boundingsphere and proximityfilter - This is necessary to ensure the correct working for viewAll and viewEntity Task-number: QTBUG-78313 Change-Id: I1dc6d227cf9009f6fbd3230093c7a7a94fb05ae3 Reviewed-by: Paul Lemire --- src/render/frontend/sphere.cpp | 50 +++++- src/render/frontend/sphere_p.h | 13 +- src/render/jobs/calcboundingvolumejob.cpp | 78 +++++++--- .../render/boundingsphere/tst_boundingsphere.cpp | 172 ++++++++++++++++++--- .../proximityfiltering/tst_proximityfiltering.cpp | 38 +++++ 5 files changed, 299 insertions(+), 52 deletions(-) diff --git a/src/render/frontend/sphere.cpp b/src/render/frontend/sphere.cpp index 4909acaef..470dbfe59 100644 --- a/src/render/frontend/sphere.cpp +++ b/src/render/frontend/sphere.cpp @@ -44,6 +44,7 @@ #include #include +#include QT_BEGIN_NAMESPACE @@ -55,6 +56,9 @@ namespace { // returns true and intersection point q; false otherwise bool intersectRaySphere(const Qt3DRender::RayCasting::QRay3D &ray, const Qt3DRender::Render::Sphere &s, Vector3D *q = nullptr) { + if (s.isNull()) + return false; + const Vector3D p = ray.origin(); const Vector3D d = ray.direction(); const Vector3D m = p - s.center(); @@ -139,11 +143,31 @@ inline void sphereFromExtremePoints(Qt3DRender::Render::Sphere &s, const QVector inline void constructRitterSphere(Qt3DRender::Render::Sphere &s, const QVector &points) { - // Calculate the sphere encompassing two axially extreme points - sphereFromExtremePoints(s, points); - - // Now make sure the sphere bounds all points by growing if needed - s.expandToContain(points); + //def bounding_sphere(points): + // dist = lambda a,b: ((a[0] - b[0])**2 + (a[1] - b[1])**2 + (a[2] - b[2])**2)**0.5 + // x = points[0] + // y = max(points,key= lambda p: dist(p,x) ) + // z = max(points,key= lambda p: dist(p,y) ) + // bounding_sphere = (((y[0]+z[0])/2,(y[1]+z[1])/2,(y[2]+z[2])/2), dist(y,z)/2) + // + // exterior_points = [p for p in points if dist(p,bounding_sphere[0]) > bounding_sphere[1] ] + // while ( len(exterior_points) > 0 ): + // pt = exterior_points.pop() + // if (dist(pt, bounding_sphere[0]) > bounding_sphere[1]): + // bounding_sphere = (bounding_sphere[0],dist(pt,bounding_sphere[0])) + // + // return bounding_sphere + + const Vector3D x = points[0]; + const Vector3D y = *std::max_element(points.begin(), points.end(), [&x](const Vector3D& lhs, const Vector3D& rhs){ return (lhs - x).lengthSquared() < (rhs - x).lengthSquared(); }); + const Vector3D z = *std::max_element(points.begin(), points.end(), [&y](const Vector3D& lhs, const Vector3D& rhs){ return (lhs - y).lengthSquared() < (rhs - y).lengthSquared(); }); + + const Vector3D center = (y + z) * 0.5f; + const Vector3D maxDistPt = *std::max_element(points.begin(), points.end(), [¢er](const Vector3D& lhs, const Vector3D& rhs){ return (lhs - center).lengthSquared() < (rhs - center).lengthSquared(); }); + const float radius = (maxDistPt - center).length(); + + s.setCenter(center); + s.setRadius(radius); } } // anonymous namespace @@ -169,6 +193,12 @@ void Sphere::initializeFromPoints(const QVector &points) void Sphere::expandToContain(const Vector3D &p) { + if (isNull()) { + m_center = p; + m_radius = 0.0f; + return; + } + const Vector3D d = p - m_center; const float dist2 = d.lengthSquared(); @@ -184,6 +214,13 @@ void Sphere::expandToContain(const Vector3D &p) void Sphere::expandToContain(const Sphere &sphere) { + if (isNull()) { + *this = sphere; + return; + } else if (sphere.isNull()) { + return; + } + const Vector3D d(sphere.m_center - m_center); const float dist2 = d.lengthSquared(); @@ -206,6 +243,9 @@ void Sphere::expandToContain(const Sphere &sphere) Sphere Sphere::transformed(const Matrix4x4 &mat) const { + if (isNull()) + return *this; + // Transform extremities in x, y, and z directions to find extremities // of the resulting ellipsoid Vector3D x = mat.map(m_center + Vector3D(m_radius, 0.0f, 0.0f)); diff --git a/src/render/frontend/sphere_p.h b/src/render/frontend/sphere_p.h index 10cf92091..b7585f85a 100644 --- a/src/render/frontend/sphere_p.h +++ b/src/render/frontend/sphere_p.h @@ -69,7 +69,7 @@ class Q_3DRENDERSHARED_PRIVATE_EXPORT Sphere : public RayCasting::BoundingSphere public: inline Sphere(Qt3DCore::QNodeId i = Qt3DCore::QNodeId()) : m_center() - , m_radius(0.0f) + , m_radius(-1.0f) , m_id(i) {} @@ -82,7 +82,7 @@ public: void setCenter(const Vector3D &c); Vector3D center() const override; - inline bool isNull() { return m_center == Vector3D() && m_radius == 0.0f; } + bool isNull() const { return m_center == Vector3D() && m_radius == -1.0f; } void setRadius(float r); float radius() const override; @@ -131,7 +131,9 @@ inline Vector3D Sphere::center() const inline void Sphere::setRadius(float r) { - m_radius = r; + Q_ASSERT(r >= 0.0f); + if (r >= 0.0f) + m_radius = r; } inline float Sphere::radius() const @@ -142,11 +144,14 @@ inline float Sphere::radius() const inline void Sphere::clear() { m_center = Vector3D(); - m_radius = 0.0f; + m_radius = -1.0f; } inline bool intersects(const Sphere &a, const Sphere &b) { + if (a.isNull() || b.isNull()) + return false; + // Calculate squared distance between sphere centers const Vector3D d = a.center() - b.center(); const float distSq = Vector3D::dotProduct(d, d); diff --git a/src/render/jobs/calcboundingvolumejob.cpp b/src/render/jobs/calcboundingvolumejob.cpp index 113dc34ce..9af2f4f38 100644 --- a/src/render/jobs/calcboundingvolumejob.cpp +++ b/src/render/jobs/calcboundingvolumejob.cpp @@ -90,30 +90,42 @@ public: m_min = QVector3D(findExtremePoints.xMin, findExtremePoints.yMin, findExtremePoints.zMin); m_max = QVector3D(findExtremePoints.xMax, findExtremePoints.yMax, findExtremePoints.zMax); - // Calculate squared distance for the pairs of points - const float xDist2 = (findExtremePoints.xMaxPt - findExtremePoints.xMinPt).lengthSquared(); - const float yDist2 = (findExtremePoints.yMaxPt - findExtremePoints.yMinPt).lengthSquared(); - const float zDist2 = (findExtremePoints.zMaxPt - findExtremePoints.zMinPt).lengthSquared(); - - // Select most distant pair - Vector3D p = findExtremePoints.xMinPt; - Vector3D q = findExtremePoints.xMaxPt; - if (yDist2 > xDist2 && yDist2 > zDist2) { - p = findExtremePoints.yMinPt; - q = findExtremePoints.yMaxPt; + FindMaxDistantPoint maxDistantPointY(m_manager); + maxDistantPointY.setReferencePoint = true; + if (!maxDistantPointY.apply(positionAttribute, indexAttribute, drawVertexCount, + primitiveRestartEnabled, primitiveRestartIndex)) { + return false; } - if (zDist2 > xDist2 && zDist2 > yDist2) { - p = findExtremePoints.zMinPt; - q = findExtremePoints.zMaxPt; + if (maxDistantPointY.hasNoPoints) + return false; + + //const Vector3D x = maxDistantPointY.referencePt; + const Vector3D y = maxDistantPointY.maxDistPt; + + FindMaxDistantPoint maxDistantPointZ(m_manager); + maxDistantPointZ.setReferencePoint = false; + maxDistantPointZ.referencePt = y; + if (!maxDistantPointZ.apply(positionAttribute, indexAttribute, drawVertexCount, + primitiveRestartEnabled, primitiveRestartIndex)) { + return false; + } + const Vector3D z = maxDistantPointZ.maxDistPt; + + const Vector3D center = (y + z) * 0.5f; + + FindMaxDistantPoint maxDistantPointCenter(m_manager); + maxDistantPointCenter.setReferencePoint = false; + maxDistantPointCenter.referencePt = center; + if (!maxDistantPointCenter.apply(positionAttribute, indexAttribute, drawVertexCount, + primitiveRestartEnabled, primitiveRestartIndex)) { + return false; } - const Vector3D c = 0.5f * (p + q); - m_volume.setCenter(c); - m_volume.setRadius((q - c).length()); + const float radius = (center - maxDistantPointCenter.maxDistPt).length(); - ExpandSphere expandSphere(m_manager, m_volume); - if (!expandSphere.apply(positionAttribute, indexAttribute, drawVertexCount, - primitiveRestartEnabled, primitiveRestartIndex)) + m_volume = Qt3DRender::Render::Sphere(center, radius); + + if (m_volume.isNull()) return false; return true; @@ -172,18 +184,34 @@ private: } }; - class ExpandSphere : public Buffer3fVisitor + class FindMaxDistantPoint : public Buffer3fVisitor { public: - ExpandSphere(NodeManagers *manager, Sphere& volume) - : Buffer3fVisitor(manager), m_volume(volume) + FindMaxDistantPoint(NodeManagers *manager) + : Buffer3fVisitor(manager) { } - Sphere& m_volume; + float maxLengthSquared = 0.0f; + Vector3D maxDistPt; + Vector3D referencePt; + bool setReferencePoint = false; + bool hasNoPoints = true; + void visit(uint ndx, float x, float y, float z) override { Q_UNUSED(ndx); - m_volume.expandToContain(Vector3D(x, y, z)); + const Vector3D p = Vector3D(x, y, z); + + if (hasNoPoints && setReferencePoint) { + maxLengthSquared = 0.0f; + referencePt = p; + } + const float lengthSquared = (p - referencePt).lengthSquared(); + if ( lengthSquared >= maxLengthSquared ) { + maxDistPt = p; + maxLengthSquared = lengthSquared; + } + hasNoPoints = false; } }; }; diff --git a/tests/auto/render/boundingsphere/tst_boundingsphere.cpp b/tests/auto/render/boundingsphere/tst_boundingsphere.cpp index 40d992347..0379c883e 100644 --- a/tests/auto/render/boundingsphere/tst_boundingsphere.cpp +++ b/tests/auto/render/boundingsphere/tst_boundingsphere.cpp @@ -169,13 +169,150 @@ class tst_BoundingSphere : public Qt3DCore::QBackendNodeTester private: private Q_SLOTS: + void checkIsNull() { + auto defaultSphere = Qt3DRender::Render::Sphere(); + QVERIFY(defaultSphere.isNull()); + } + + void remainsNotNullAfterTransform() { + QMatrix4x4 mat; + mat.translate(-5,-5,-5); + auto mMat = Matrix4x4(mat); + auto pointSphere = Qt3DRender::Render::Sphere(Vector3D(5.f,5.f,5.f),0.f); + pointSphere.transform(mMat); + QVERIFY(!pointSphere.isNull()); + QVERIFY(pointSphere.center() == Vector3D(0.,0.,0)); + QVERIFY(pointSphere.radius() == 0.f); + } + + void remainsNullAfterTransform() { + QMatrix4x4 mat; + mat.translate(-5,-5,-5); + auto mMat = Matrix4x4(mat); + auto defaultSphere = Qt3DRender::Render::Sphere(); + defaultSphere.transform(mMat); + QVERIFY(defaultSphere.isNull()); + } + + void expandToContainSphere() { + auto firstValidSphere = Qt3DRender::Render::Sphere(Vector3D(-10.f,-10.f,-10.f),1.f); + auto secondValidSphere = Qt3DRender::Render::Sphere(Vector3D(10.f,10.f,10.f),1.f); + firstValidSphere.expandToContain(secondValidSphere); + QVERIFY(firstValidSphere.center()==Vector3D(0.f,0.f,0.f)); + float dist = static_cast((2 + sqrt(3.*(20)*(20)))/2.); + QVERIFY(qFuzzyCompare(firstValidSphere.radius(), dist)); + } + + void expandToContainSphereOneInvalid() { + auto firstValidSphere = Qt3DRender::Render::Sphere(Vector3D(-10.f,-10.f,-10.f),1.f); + auto defaultSphere = Qt3DRender::Render::Sphere(); + auto copiedSphere = firstValidSphere; + firstValidSphere.expandToContain(defaultSphere); + QVERIFY(firstValidSphere.center() == copiedSphere.center()); + QVERIFY(firstValidSphere.radius() == copiedSphere.radius()); + QVERIFY(!firstValidSphere.isNull()); + } + + void expandToContainOtherSphereInvalid() { + auto firstValidSphere = Qt3DRender::Render::Sphere(Vector3D(-10.f,-10.f,-10.f),1.f); + auto defaultSphere = Qt3DRender::Render::Sphere(); + defaultSphere.expandToContain(firstValidSphere); + QVERIFY(defaultSphere.center() == firstValidSphere.center()); + QVERIFY(defaultSphere.radius() == firstValidSphere.radius()); + QVERIFY(!defaultSphere.isNull()); + } + + void expandNullSphereWithNullSphere() { + auto defaultSphere = Qt3DRender::Render::Sphere(); + auto otherDefaultSphere = Qt3DRender::Render::Sphere(); + defaultSphere.expandToContain(otherDefaultSphere); + QVERIFY(defaultSphere.isNull()); + } + + void expandToContainPoint() { + auto firstValidSphere = Qt3DRender::Render::Sphere(Vector3D(-10.f,-10.f,-10.f),1.f); + firstValidSphere.expandToContain(Vector3D(0,0,0)); + QVERIFY(!firstValidSphere.isNull()); + float expectedRadius = static_cast((1 + qSqrt(3.*(10)*(10)))/2.); + QVERIFY(qFuzzyCompare(firstValidSphere.radius(), expectedRadius)); + } + + void nullSphereExpandToContainPoint() { + auto defaultSphere = Qt3DRender::Render::Sphere(); + defaultSphere.expandToContain(Vector3D(5,5,5)); + QVERIFY(!defaultSphere.isNull()); + QVERIFY(defaultSphere.center() == Vector3D(5,5,5)); + QVERIFY(qFuzzyIsNull(defaultSphere.radius())); + } + + void nullSphereExpandToOrigin() { + auto defaultSphere = Qt3DRender::Render::Sphere(); + defaultSphere.expandToContain(Vector3D(0,0,0)); + QVERIFY(!defaultSphere.isNull()); + QVERIFY(defaultSphere.center() == Vector3D(0,0,0)); + QVERIFY(qFuzzyIsNull(defaultSphere.radius())); + } + + void ritterSphereCubePoints() { + QVector cubePts={ + Vector3D(-0.5, -0.5, 0.5), + Vector3D( 0.5, -0.5, -0.5), + Vector3D(-0.5, 0.5, -0.5), + Vector3D( 0.5, 0.5, -0.5), + Vector3D(-0.5, -0.5, -0.5), + Vector3D( 0.5, -0.5, 0.5), + Vector3D(-0.5, 0.5, 0.5), + Vector3D( 0.5, 0.5, 0.5) + }; + auto ritterSphere=Qt3DRender::Render::Sphere::fromPoints(cubePts); + QVERIFY(!ritterSphere.isNull()); + QVERIFY(qFuzzyIsNull(ritterSphere.center().x())); + QVERIFY(qFuzzyIsNull(ritterSphere.center().y())); + QVERIFY(qFuzzyIsNull(ritterSphere.center().z())); + QVERIFY(qFuzzyCompare(ritterSphere.radius(), static_cast(qSqrt(3)/2))); + } + + void ritterSphereRandomPoints() { + QVector randomPts={ + Vector3D(-81, 55, 46), + Vector3D(-91, -73, -42), + Vector3D(-50, -76, -77), + Vector3D(-40, 63, 58), + Vector3D(-28, -2, -57), + Vector3D(84, 17, 33), + Vector3D(53, 11, -49), + Vector3D(-7, -24, -86), + Vector3D(-89, 6, 76), + Vector3D(46, -18, -27) + }; + + auto ritterSphere = Qt3DRender::Render::Sphere::fromPoints(randomPts); + QVERIFY(!ritterSphere.isNull()); + QVERIFY(qFuzzyCompare(ritterSphere.center().x(), 17.f)); + QVERIFY(qFuzzyCompare(ritterSphere.center().y(), -29.5f)); + QVERIFY(qFuzzyCompare(ritterSphere.center().z(), -22.0f)); + QVERIFY(qFuzzyCompare(ritterSphere.radius(), 148.66152831179963f)); + } + + void ritterSphereOnePoint() { + QVector singlePt={ + Vector3D(-0.5, -0.5, -0.5), + }; + auto ritterSphere = Qt3DRender::Render::Sphere::fromPoints(singlePt); + QVERIFY(!ritterSphere.isNull()); + QVERIFY(qFuzzyCompare(ritterSphere.center().x(), -0.5f)); + QVERIFY(qFuzzyCompare(ritterSphere.center().y(), -0.5f)); + QVERIFY(qFuzzyCompare(ritterSphere.center().z(), -0.5f)); + QVERIFY(qFuzzyIsNull(ritterSphere.radius())); + } + void checkExtraGeometries_data() { QTest::addColumn("qmlFile"); QTest::addColumn("sphereCenter"); QTest::addColumn("sphereRadius"); QTest::newRow("SphereMesh") << "qrc:/sphere.qml" << QVector3D(0.f, 0.f, 0.f) << 1.f; - QTest::newRow("CubeMesh") << "qrc:/cube.qml" << QVector3D(0.0928356f, -0.212021f, -0.0467958f) << 1.07583f; // weird! + QTest::newRow("CubeMesh") << "qrc:/cube.qml" << QVector3D(0.f, 0.f, 0.f) << static_cast(qSqrt(3.)/2.); // not weird at all } void checkExtraGeometries() @@ -200,9 +337,10 @@ private Q_SLOTS: const auto boundingSphere = test->sceneRoot()->worldBoundingVolumeWithChildren(); qDebug() << qmlFile << boundingSphere->radius() << boundingSphere->center(); QCOMPARE(boundingSphere->radius(), sphereRadius); - QVERIFY(qAbs(boundingSphere->center().x() - sphereCenter.x()) < 0.000001f); // qFuzzyCompare hates 0s - QVERIFY(qAbs(boundingSphere->center().y() - sphereCenter.y()) < 0.000001f); - QVERIFY(qAbs(boundingSphere->center().z() - sphereCenter.z()) < 0.000001f); + + QVERIFY(qFuzzyIsNull(boundingSphere->center().x() - sphereCenter.x())); + QVERIFY(qFuzzyIsNull(boundingSphere->center().y() - sphereCenter.y())); + QVERIFY(qFuzzyIsNull(boundingSphere->center().z() - sphereCenter.z())); } void checkCustomGeometry_data() @@ -212,10 +350,10 @@ private Q_SLOTS: QTest::addColumn("expectedCenter"); QTest::addColumn("expectedRadius"); QTest::addColumn("withPrimitiveRestart"); - QTest::newRow("all") << 0 << 0 << QVector3D(-0.488892f, 0.0192147f, -75.4804f) << 25.5442f << false; + QTest::newRow("all") << 0 << 0 << QVector3D(0.0f, 0.0f, -75.0f) << 25.03997f << false; QTest::newRow("first only") << 3 << 0 << QVector3D(0, 1, -100) << 1.0f << false; QTest::newRow("second only") << 3 << int(3 * sizeof(ushort)) << QVector3D(0, -1, -50) << 1.0f << false; - QTest::newRow("all with primitive restart") << 0 << 0 << QVector3D(-0.488892f, 0.0192147f, -75.4804f) << 25.5442f << true; + QTest::newRow("all with primitive restart") << 0 << 0 << QVector3D(0.0f, 0.0f, -75.0f) << 25.03997f << true; QTest::newRow("first only with primitive restart") << 4 << 0 << QVector3D(0, 1, -100) << 1.0f << true; QTest::newRow("second only with primitive restart") << 4 << int(3 * sizeof(ushort)) << QVector3D(0, -1, -50) << 1.0f << true; } @@ -341,18 +479,17 @@ private Q_SLOTS: float radius = entityBackend->localBoundingVolume()->radius(); qDebug() << radius << center; - // truncate and compare integers only - QVERIFY(int(radius) == int(expectedRadius)); - QVERIFY(int(center.x()) == int(expectedCenter.x())); - QVERIFY(int(center.y()) == int(expectedCenter.y())); - QVERIFY(int(center.z()) == int(expectedCenter.z())); + QCOMPARE(radius, expectedRadius); + QCOMPARE(center.x(), expectedCenter.x()); + QCOMPARE(center.y(), expectedCenter.y()); + QCOMPARE(center.z(), expectedCenter.z()); } void checkCustomPackedGeometry() { int drawVertexCount = 6; - QVector3D expectedCenter(-0.488892f, 0.0192147f, -75.4804f); - float expectedRadius = 25.5442f; + QVector3D expectedCenter(0.0f, 0.0f, -75.0f); + float expectedRadius = 25.03997f; // two triangles with different Z QByteArray vdata; @@ -432,11 +569,10 @@ private Q_SLOTS: float radius = entityBackend->localBoundingVolume()->radius(); qDebug() << radius << center; - // truncate and compare integers only - QVERIFY(int(radius) == int(expectedRadius)); - QVERIFY(int(center.x()) == int(expectedCenter.x())); - QVERIFY(int(center.y()) == int(expectedCenter.y())); - QVERIFY(int(center.z()) == int(expectedCenter.z())); + QCOMPARE(radius, expectedRadius); + QCOMPARE(center.x(), expectedCenter.x()); + QCOMPARE(center.y(), expectedCenter.y()); + QCOMPARE(center.z(), expectedCenter.z()); } }; diff --git a/tests/auto/render/proximityfiltering/tst_proximityfiltering.cpp b/tests/auto/render/proximityfiltering/tst_proximityfiltering.cpp index c8d862b2e..dcd39c785 100644 --- a/tests/auto/render/proximityfiltering/tst_proximityfiltering.cpp +++ b/tests/auto/render/proximityfiltering/tst_proximityfiltering.cpp @@ -29,6 +29,10 @@ #include #include #include +#include +#include +#include +#include #include #include #include @@ -43,6 +47,40 @@ namespace { Qt3DCore::QEntity *buildEntityAtDistance(float distance, Qt3DCore::QEntity *parent) { Qt3DCore::QEntity *entity = new Qt3DCore::QEntity(parent); + + // create geometry with a valid bounding volume - a single point is sufficient + auto geometry = new Qt3DRender::QGeometry; + auto vertexBuffer = new Qt3DRender::QBuffer(Qt3DRender::QBuffer::VertexBuffer, geometry); + + auto positionAttribute = new Qt3DRender::QAttribute; + positionAttribute->setName(Qt3DRender::QAttribute::defaultPositionAttributeName()); + positionAttribute->setAttributeType(Qt3DRender::QAttribute::VertexAttribute); + positionAttribute->setVertexBaseType(Qt3DRender::QAttribute::Float); + positionAttribute->setVertexSize(3); + positionAttribute->setByteStride(3 * sizeof(float)); + positionAttribute->setBuffer(vertexBuffer); + + QByteArray vertexBufferData; + vertexBufferData.resize(static_cast(3 * sizeof(float))); + + auto vertexArray = reinterpret_cast(vertexBufferData.data()); + + int i = 0; + vertexArray[i++] = 0.0f; + vertexArray[i++] = 0.0f; + vertexArray[i++] = 0.0f; + + vertexBuffer->setData(vertexBufferData); + positionAttribute->setCount(1); + + geometry->addAttribute(positionAttribute); + + auto geometryRenderer = new Qt3DRender::QGeometryRenderer; + geometryRenderer->setPrimitiveType(Qt3DRender::QGeometryRenderer::Points); + geometryRenderer->setGeometry(geometry); + + entity->addComponent(geometryRenderer); + Qt3DCore::QTransform *transform = new Qt3DCore::QTransform(parent); const QVector3D t = QVector3D(1.0f, 0.0f, 0.0f) * distance; -- cgit v1.2.3