blob: 721091b669b592f9a409782b3e9546ea65405075 [file] [log] [blame]
/****************************************************************************
**
** Copyright (C) 2016 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the QtQuick module of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 3 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL3 included in the
** packaging of this file. Please review the following information to
** ensure the GNU Lesser General Public License version 3 requirements
** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 2.0 or (at your option) the GNU General
** Public license version 3 or any later version approved by the KDE Free
** Qt Foundation. The licenses are as published by the Free Software
** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-2.0.html and
** https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/
#include "qquickshapenvprrenderer_p.h"
#include <QOpenGLExtraFunctions>
#include <QOpenGLFramebufferObject>
#include <QOpenGLShaderProgram>
#include <QOpenGLBuffer>
#include <qmath.h>
#include <private/qpainterpath_p.h>
#include <private/qquickpath_p_p.h>
QT_BEGIN_NAMESPACE
void QQuickShapeNvprRenderer::beginSync(int totalCount)
{
if (m_sp.count() != totalCount) {
m_sp.resize(totalCount);
m_accDirty |= DirtyList;
}
}
void QQuickShapeNvprRenderer::setPath(int index, const QQuickPath *path)
{
ShapePathGuiData &d(m_sp[index]);
convertPath(path, &d);
d.dirty |= DirtyPath;
m_accDirty |= DirtyPath;
}
void QQuickShapeNvprRenderer::setStrokeColor(int index, const QColor &color)
{
ShapePathGuiData &d(m_sp[index]);
d.strokeColor = color;
d.dirty |= DirtyStyle;
m_accDirty |= DirtyStyle;
}
void QQuickShapeNvprRenderer::setStrokeWidth(int index, qreal w)
{
ShapePathGuiData &d(m_sp[index]);
d.strokeWidth = w;
d.dirty |= DirtyStyle;
m_accDirty |= DirtyStyle;
}
void QQuickShapeNvprRenderer::setFillColor(int index, const QColor &color)
{
ShapePathGuiData &d(m_sp[index]);
d.fillColor = color;
d.dirty |= DirtyStyle;
m_accDirty |= DirtyStyle;
}
void QQuickShapeNvprRenderer::setFillRule(int index, QQuickShapePath::FillRule fillRule)
{
ShapePathGuiData &d(m_sp[index]);
d.fillRule = fillRule;
d.dirty |= DirtyFillRule;
m_accDirty |= DirtyFillRule;
}
void QQuickShapeNvprRenderer::setJoinStyle(int index, QQuickShapePath::JoinStyle joinStyle, int miterLimit)
{
ShapePathGuiData &d(m_sp[index]);
d.joinStyle = joinStyle;
d.miterLimit = miterLimit;
d.dirty |= DirtyStyle;
m_accDirty |= DirtyStyle;
}
void QQuickShapeNvprRenderer::setCapStyle(int index, QQuickShapePath::CapStyle capStyle)
{
ShapePathGuiData &d(m_sp[index]);
d.capStyle = capStyle;
d.dirty |= DirtyStyle;
m_accDirty |= DirtyStyle;
}
void QQuickShapeNvprRenderer::setStrokeStyle(int index, QQuickShapePath::StrokeStyle strokeStyle,
qreal dashOffset, const QVector<qreal> &dashPattern)
{
ShapePathGuiData &d(m_sp[index]);
d.dashActive = strokeStyle == QQuickShapePath::DashLine;
d.dashOffset = dashOffset;
d.dashPattern = dashPattern;
d.dirty |= DirtyDash;
m_accDirty |= DirtyDash;
}
void QQuickShapeNvprRenderer::setFillGradient(int index, QQuickShapeGradient *gradient)
{
ShapePathGuiData &d(m_sp[index]);
if (gradient) {
d.fillGradient.stops = gradient->gradientStops(); // sorted
d.fillGradient.spread = gradient->spread();
if (QQuickShapeLinearGradient *g = qobject_cast<QQuickShapeLinearGradient *>(gradient)) {
d.fillGradientActive = LinearGradient;
d.fillGradient.a = QPointF(g->x1(), g->y1());
d.fillGradient.b = QPointF(g->x2(), g->y2());
} else if (QQuickShapeRadialGradient *g = qobject_cast<QQuickShapeRadialGradient *>(gradient)) {
d.fillGradientActive = RadialGradient;
d.fillGradient.a = QPointF(g->centerX(), g->centerY());
d.fillGradient.b = QPointF(g->focalX(), g->focalY());
d.fillGradient.v0 = g->centerRadius();
d.fillGradient.v1 = g->focalRadius();
} else if (QQuickShapeConicalGradient *g = qobject_cast<QQuickShapeConicalGradient *>(gradient)) {
d.fillGradientActive = ConicalGradient;
d.fillGradient.a = QPointF(g->centerX(), g->centerY());
d.fillGradient.v0 = g->angle();
} else {
Q_UNREACHABLE();
}
} else {
d.fillGradientActive = NoGradient;
}
d.dirty |= DirtyFillGradient;
m_accDirty |= DirtyFillGradient;
}
void QQuickShapeNvprRenderer::endSync(bool)
{
}
void QQuickShapeNvprRenderer::setNode(QQuickShapeNvprRenderNode *node)
{
if (m_node != node) {
m_node = node;
m_accDirty |= DirtyList;
}
}
QDebug operator<<(QDebug debug, const QQuickShapeNvprRenderer::NvprPath &path)
{
QDebugStateSaver saver(debug);
debug.space().noquote();
if (!path.str.isEmpty()) {
debug << "Path with SVG string" << path.str;
return debug;
}
debug << "Path with" << path.cmd.count() << "commands";
int ci = 0;
for (GLubyte cmd : path.cmd) {
static struct { GLubyte cmd; const char *s; int coordCount; } nameTabs[] = {
{ GL_MOVE_TO_NV, "moveTo", 2 },
{ GL_LINE_TO_NV, "lineTo", 2 },
{ GL_QUADRATIC_CURVE_TO_NV, "quadTo", 4 },
{ GL_CUBIC_CURVE_TO_NV, "cubicTo", 6 },
{ GL_LARGE_CW_ARC_TO_NV, "arcTo-large-CW", 5 },
{ GL_LARGE_CCW_ARC_TO_NV, "arcTo-large-CCW", 5 },
{ GL_SMALL_CW_ARC_TO_NV, "arcTo-small-CW", 5 },
{ GL_SMALL_CCW_ARC_TO_NV, "arcTo-small-CCW", 5 },
{ GL_CLOSE_PATH_NV, "closePath", 0 } };
for (const auto &nameTab : nameTabs) {
if (nameTab.cmd == cmd) {
QByteArray cs;
for (int j = 0; j < nameTab.coordCount; ++j) {
cs.append(QByteArray::number(path.coord[ci++]));
cs.append(' ');
}
debug << "\n " << nameTab.s << " " << cs;
break;
}
}
}
return debug;
}
static inline void appendCoords(QVector<GLfloat> *v, QQuickCurve *c, QPointF *pos)
{
QPointF p(c->hasRelativeX() ? pos->x() + c->relativeX() : c->x(),
c->hasRelativeY() ? pos->y() + c->relativeY() : c->y());
v->append(p.x());
v->append(p.y());
*pos = p;
}
static inline void appendControlCoords(QVector<GLfloat> *v, QQuickPathQuad *c, const QPointF &pos)
{
QPointF p(c->hasRelativeControlX() ? pos.x() + c->relativeControlX() : c->controlX(),
c->hasRelativeControlY() ? pos.y() + c->relativeControlY() : c->controlY());
v->append(p.x());
v->append(p.y());
}
static inline void appendControl1Coords(QVector<GLfloat> *v, QQuickPathCubic *c, const QPointF &pos)
{
QPointF p(c->hasRelativeControl1X() ? pos.x() + c->relativeControl1X() : c->control1X(),
c->hasRelativeControl1Y() ? pos.y() + c->relativeControl1Y() : c->control1Y());
v->append(p.x());
v->append(p.y());
}
static inline void appendControl2Coords(QVector<GLfloat> *v, QQuickPathCubic *c, const QPointF &pos)
{
QPointF p(c->hasRelativeControl2X() ? pos.x() + c->relativeControl2X() : c->control2X(),
c->hasRelativeControl2Y() ? pos.y() + c->relativeControl2Y() : c->control2Y());
v->append(p.x());
v->append(p.y());
}
void QQuickShapeNvprRenderer::convertPath(const QQuickPath *path, ShapePathGuiData *d)
{
d->path = NvprPath();
if (!path)
return;
const QList<QQuickPathElement *> &pp(QQuickPathPrivate::get(path)->_pathElements);
if (pp.isEmpty())
return;
QPointF startPos(path->startX(), path->startY());
QPointF pos(startPos);
if (!qFuzzyIsNull(pos.x()) || !qFuzzyIsNull(pos.y())) {
d->path.cmd.append(GL_MOVE_TO_NV);
d->path.coord.append(pos.x());
d->path.coord.append(pos.y());
}
for (QQuickPathElement *e : pp) {
if (QQuickPathMove *o = qobject_cast<QQuickPathMove *>(e)) {
d->path.cmd.append(GL_MOVE_TO_NV);
appendCoords(&d->path.coord, o, &pos);
startPos = pos;
} else if (QQuickPathLine *o = qobject_cast<QQuickPathLine *>(e)) {
d->path.cmd.append(GL_LINE_TO_NV);
appendCoords(&d->path.coord, o, &pos);
} else if (QQuickPathQuad *o = qobject_cast<QQuickPathQuad *>(e)) {
d->path.cmd.append(GL_QUADRATIC_CURVE_TO_NV);
appendControlCoords(&d->path.coord, o, pos);
appendCoords(&d->path.coord, o, &pos);
} else if (QQuickPathCubic *o = qobject_cast<QQuickPathCubic *>(e)) {
d->path.cmd.append(GL_CUBIC_CURVE_TO_NV);
appendControl1Coords(&d->path.coord, o, pos);
appendControl2Coords(&d->path.coord, o, pos);
appendCoords(&d->path.coord, o, &pos);
} else if (QQuickPathArc *o = qobject_cast<QQuickPathArc *>(e)) {
const bool sweepFlag = o->direction() == QQuickPathArc::Clockwise; // maps to CCW, not a typo
GLenum cmd;
if (o->useLargeArc())
cmd = sweepFlag ? GL_LARGE_CCW_ARC_TO_NV : GL_LARGE_CW_ARC_TO_NV;
else
cmd = sweepFlag ? GL_SMALL_CCW_ARC_TO_NV : GL_SMALL_CW_ARC_TO_NV;
d->path.cmd.append(cmd);
d->path.coord.append(o->radiusX());
d->path.coord.append(o->radiusY());
d->path.coord.append(o->xAxisRotation());
appendCoords(&d->path.coord, o, &pos);
} else if (QQuickPathSvg *o = qobject_cast<QQuickPathSvg *>(e)) {
// PathSvg cannot be combined with other elements. But take at
// least startX and startY into account.
if (d->path.str.isEmpty())
d->path.str = QString(QStringLiteral("M %1 %2 ")).arg(pos.x()).arg(pos.y()).toUtf8();
d->path.str.append(o->path().toUtf8());
} else if (QQuickPathAngleArc *o = qobject_cast<QQuickPathAngleArc *>(e)) {
QRectF rect(o->centerX() - o->radiusX(), o->centerY() - o->radiusY(), o->radiusX() * 2, o->radiusY() * 2);
QPointF startPoint;
QPointF endPoint;
qt_find_ellipse_coords(rect, o->startAngle(), -o->sweepAngle(), &startPoint, &endPoint);
// get to our starting position
if (o->moveToStart())
d->path.cmd.append(GL_MOVE_TO_NV);
else
d->path.cmd.append(GL_LINE_TO_NV); // ### should we check if startPoint == pos?
d->path.coord.append(startPoint.x());
d->path.coord.append(startPoint.y());
const bool sweepFlag = o->sweepAngle() > 0; // maps to CCW, not a typo
d->path.cmd.append(qAbs(o->sweepAngle()) > 180.0
? (sweepFlag ? GL_LARGE_CCW_ARC_TO_NV : GL_LARGE_CW_ARC_TO_NV)
: (sweepFlag ? GL_SMALL_CCW_ARC_TO_NV : GL_SMALL_CW_ARC_TO_NV));
d->path.coord.append(o->radiusX());
d->path.coord.append(o->radiusY());
d->path.coord.append(0); // xAxisRotation
d->path.coord.append(endPoint.x());
d->path.coord.append(endPoint.y());
pos = endPoint;
} else {
qWarning() << "Shape/NVPR: unsupported Path element" << e;
}
}
// For compatibility with QTriangulatingStroker. SVG and others would not
// implicitly close the path when end_pos == start_pos (start_pos being the
// last moveTo pos); that would still need an explicit 'z' or similar. We
// don't have an explicit close command, so just fake a close when the
// positions match.
if (pos == startPos)
d->path.cmd.append(GL_CLOSE_PATH_NV);
}
static inline QVector4D qsg_premultiply(const QColor &c, float globalOpacity)
{
const float o = c.alphaF() * globalOpacity;
return QVector4D(c.redF() * o, c.greenF() * o, c.blueF() * o, o);
}
void QQuickShapeNvprRenderer::updateNode()
{
// Called on the render thread with gui blocked -> update the node with its
// own copy of all relevant data.
if (!m_accDirty)
return;
const int count = m_sp.count();
const bool listChanged = m_accDirty & DirtyList;
if (listChanged)
m_node->m_sp.resize(count);
for (int i = 0; i < count; ++i) {
ShapePathGuiData &src(m_sp[i]);
QQuickShapeNvprRenderNode::ShapePathRenderData &dst(m_node->m_sp[i]);
int dirty = src.dirty;
src.dirty = 0;
if (listChanged)
dirty |= DirtyPath | DirtyStyle | DirtyFillRule | DirtyDash | DirtyFillGradient;
// updateNode() can be called several times with different dirty
// states before render() gets invoked. So accumulate.
dst.dirty |= dirty;
if (dirty & DirtyPath)
dst.source = src.path;
if (dirty & DirtyStyle) {
dst.strokeWidth = src.strokeWidth;
dst.strokeColor = qsg_premultiply(src.strokeColor, 1.0f);
dst.fillColor = qsg_premultiply(src.fillColor, 1.0f);
switch (src.joinStyle) {
case QQuickShapePath::MiterJoin:
dst.joinStyle = GL_MITER_TRUNCATE_NV;
break;
case QQuickShapePath::BevelJoin:
dst.joinStyle = GL_BEVEL_NV;
break;
case QQuickShapePath::RoundJoin:
dst.joinStyle = GL_ROUND_NV;
break;
default:
Q_UNREACHABLE();
}
dst.miterLimit = src.miterLimit;
switch (src.capStyle) {
case QQuickShapePath::FlatCap:
dst.capStyle = GL_FLAT;
break;
case QQuickShapePath::SquareCap:
dst.capStyle = GL_SQUARE_NV;
break;
case QQuickShapePath::RoundCap:
dst.capStyle = GL_ROUND_NV;
break;
default:
Q_UNREACHABLE();
}
}
if (dirty & DirtyFillRule) {
switch (src.fillRule) {
case QQuickShapePath::OddEvenFill:
dst.fillRule = GL_INVERT;
break;
case QQuickShapePath::WindingFill:
dst.fillRule = GL_COUNT_UP_NV;
break;
default:
Q_UNREACHABLE();
}
}
if (dirty & DirtyDash) {
// Multiply by strokeWidth because the Shape API follows QPen
// meaning the input dash pattern and dash offset here are in width units.
dst.dashOffset = src.dashOffset * src.strokeWidth;
if (src.dashActive) {
if (src.dashPattern.isEmpty()) {
// default values for DashLine as defined in qpen.cpp
dst.dashPattern.resize(2);
dst.dashPattern[0] = 4 * src.strokeWidth; // dash
dst.dashPattern[1] = 2 * src.strokeWidth; // space
} else {
dst.dashPattern.resize(src.dashPattern.count());
for (int i = 0; i < src.dashPattern.count(); ++i)
dst.dashPattern[i] = GLfloat(src.dashPattern[i]) * src.strokeWidth;
// QPen expects a dash pattern of even length and so should we
if (src.dashPattern.count() % 2 != 0) {
qWarning("QQuickShapeNvprRenderNode: dash pattern not of even length");
dst.dashPattern << src.strokeWidth;
}
}
} else {
dst.dashPattern.clear();
}
}
if (dirty & DirtyFillGradient) {
dst.fillGradientActive = src.fillGradientActive;
if (src.fillGradientActive)
dst.fillGradient = src.fillGradient;
}
}
m_node->markDirty(QSGNode::DirtyMaterial);
m_accDirty = 0;
}
bool QQuickShapeNvprRenderNode::nvprInited = false;
QQuickNvprFunctions QQuickShapeNvprRenderNode::nvpr;
QQuickNvprMaterialManager QQuickShapeNvprRenderNode::mtlmgr;
QQuickShapeNvprRenderNode::~QQuickShapeNvprRenderNode()
{
releaseResources();
}
void QQuickShapeNvprRenderNode::releaseResources()
{
for (ShapePathRenderData &d : m_sp) {
if (d.path) {
nvpr.deletePaths(d.path, 1);
d.path = 0;
}
if (d.fallbackFbo) {
delete d.fallbackFbo;
d.fallbackFbo = nullptr;
}
}
m_fallbackBlitter.destroy();
}
void QQuickNvprMaterialManager::create(QQuickNvprFunctions *nvpr)
{
m_nvpr = nvpr;
}
void QQuickNvprMaterialManager::releaseResources()
{
QOpenGLExtraFunctions *f = QOpenGLContext::currentContext()->extraFunctions();
for (MaterialDesc &mtl : m_materials) {
if (mtl.ppl) {
f->glDeleteProgramPipelines(1, &mtl.ppl);
mtl = MaterialDesc();
}
}
}
QQuickNvprMaterialManager::MaterialDesc *QQuickNvprMaterialManager::activateMaterial(Material m)
{
QOpenGLExtraFunctions *f = QOpenGLContext::currentContext()->extraFunctions();
MaterialDesc &mtl(m_materials[m]);
if (!mtl.ppl) {
if (m == MatSolid) {
static const char *fragSrc =
"#version 310 es\n"
"precision highp float;\n"
"out vec4 fragColor;\n"
"uniform vec4 color;\n"
"uniform float opacity;\n"
"void main() {\n"
" fragColor = color * opacity;\n"
"}\n";
if (!m_nvpr->createFragmentOnlyPipeline(fragSrc, &mtl.ppl, &mtl.prg)) {
qWarning("NVPR: Failed to create shader pipeline for solid fill");
return nullptr;
}
Q_ASSERT(mtl.ppl && mtl.prg);
mtl.uniLoc[0] = f->glGetProgramResourceLocation(mtl.prg, GL_UNIFORM, "color");
Q_ASSERT(mtl.uniLoc[0] >= 0);
mtl.uniLoc[1] = f->glGetProgramResourceLocation(mtl.prg, GL_UNIFORM, "opacity");
Q_ASSERT(mtl.uniLoc[1] >= 0);
} else if (m == MatLinearGradient) {
static const char *fragSrc =
"#version 310 es\n"
"precision highp float;\n"
"layout(location = 0) in vec2 uv;"
"uniform float opacity;\n"
"uniform sampler2D gradTab;\n"
"uniform vec2 gradStart;\n"
"uniform vec2 gradEnd;\n"
"out vec4 fragColor;\n"
"void main() {\n"
" vec2 gradVec = gradEnd - gradStart;\n"
" float gradTabIndex = dot(gradVec, uv - gradStart) / (gradVec.x * gradVec.x + gradVec.y * gradVec.y);\n"
" fragColor = texture(gradTab, vec2(gradTabIndex, 0.5)) * opacity;\n"
"}\n";
if (!m_nvpr->createFragmentOnlyPipeline(fragSrc, &mtl.ppl, &mtl.prg)) {
qWarning("NVPR: Failed to create shader pipeline for linear gradient");
return nullptr;
}
Q_ASSERT(mtl.ppl && mtl.prg);
mtl.uniLoc[1] = f->glGetProgramResourceLocation(mtl.prg, GL_UNIFORM, "opacity");
Q_ASSERT(mtl.uniLoc[1] >= 0);
mtl.uniLoc[2] = f->glGetProgramResourceLocation(mtl.prg, GL_UNIFORM, "gradStart");
Q_ASSERT(mtl.uniLoc[2] >= 0);
mtl.uniLoc[3] = f->glGetProgramResourceLocation(mtl.prg, GL_UNIFORM, "gradEnd");
Q_ASSERT(mtl.uniLoc[3] >= 0);
} else if (m == MatRadialGradient) {
static const char *fragSrc =
"#version 310 es\n"
"precision highp float;\n"
"uniform sampler2D gradTab;\n"
"uniform float opacity;\n"
"uniform vec2 focalToCenter;\n"
"uniform float centerRadius;\n"
"uniform float focalRadius;\n"
"uniform vec2 translationPoint;\n"
"layout(location = 0) in vec2 uv;\n"
"out vec4 fragColor;\n"
"void main() {\n"
" vec2 coord = uv - translationPoint;\n"
" float rd = centerRadius - focalRadius;\n"
" float b = 2.0 * (rd * focalRadius + dot(coord, focalToCenter));\n"
" float fmp2_m_radius2 = -focalToCenter.x * focalToCenter.x - focalToCenter.y * focalToCenter.y + rd * rd;\n"
" float inverse_2_fmp2_m_radius2 = 1.0 / (2.0 * fmp2_m_radius2);\n"
" float det = b * b - 4.0 * fmp2_m_radius2 * ((focalRadius * focalRadius) - dot(coord, coord));\n"
" vec4 result = vec4(0.0);\n"
" if (det >= 0.0) {\n"
" float detSqrt = sqrt(det);\n"
" float w = max((-b - detSqrt) * inverse_2_fmp2_m_radius2, (-b + detSqrt) * inverse_2_fmp2_m_radius2);\n"
" if (focalRadius + w * (centerRadius - focalRadius) >= 0.0)\n"
" result = texture(gradTab, vec2(w, 0.5)) * opacity;\n"
" }\n"
" fragColor = result;\n"
"}\n";
if (!m_nvpr->createFragmentOnlyPipeline(fragSrc, &mtl.ppl, &mtl.prg)) {
qWarning("NVPR: Failed to create shader pipeline for radial gradient");
return nullptr;
}
Q_ASSERT(mtl.ppl && mtl.prg);
mtl.uniLoc[1] = f->glGetProgramResourceLocation(mtl.prg, GL_UNIFORM, "opacity");
Q_ASSERT(mtl.uniLoc[1] >= 0);
mtl.uniLoc[2] = f->glGetProgramResourceLocation(mtl.prg, GL_UNIFORM, "focalToCenter");
Q_ASSERT(mtl.uniLoc[2] >= 0);
mtl.uniLoc[3] = f->glGetProgramResourceLocation(mtl.prg, GL_UNIFORM, "centerRadius");
Q_ASSERT(mtl.uniLoc[3] >= 0);
mtl.uniLoc[4] = f->glGetProgramResourceLocation(mtl.prg, GL_UNIFORM, "focalRadius");
Q_ASSERT(mtl.uniLoc[4] >= 0);
mtl.uniLoc[5] = f->glGetProgramResourceLocation(mtl.prg, GL_UNIFORM, "translationPoint");
Q_ASSERT(mtl.uniLoc[5] >= 0);
} else if (m == MatConicalGradient) {
static const char *fragSrc =
"#version 310 es\n"
"precision highp float;\n"
"#define INVERSE_2PI 0.1591549430918953358\n"
"uniform sampler2D gradTab;\n"
"uniform float opacity;\n"
"uniform float angle;\n"
"uniform vec2 translationPoint;\n"
"layout(location = 0) in vec2 uv;\n"
"out vec4 fragColor;\n"
"void main() {\n"
" vec2 coord = uv - translationPoint;\n"
" float t;\n"
" if (abs(coord.y) == abs(coord.x))\n"
" t = (atan(-coord.y + 0.002, coord.x) + angle) * INVERSE_2PI;\n"
" else\n"
" t = (atan(-coord.y, coord.x) + angle) * INVERSE_2PI;\n"
" fragColor = texture(gradTab, vec2(t - floor(t), 0.5)) * opacity;\n"
"}\n";
if (!m_nvpr->createFragmentOnlyPipeline(fragSrc, &mtl.ppl, &mtl.prg)) {
qWarning("NVPR: Failed to create shader pipeline for conical gradient");
return nullptr;
}
Q_ASSERT(mtl.ppl && mtl.prg);
mtl.uniLoc[1] = f->glGetProgramResourceLocation(mtl.prg, GL_UNIFORM, "opacity");
Q_ASSERT(mtl.uniLoc[1] >= 0);
mtl.uniLoc[2] = f->glGetProgramResourceLocation(mtl.prg, GL_UNIFORM, "angle");
Q_ASSERT(mtl.uniLoc[2] >= 0);
mtl.uniLoc[3] = f->glGetProgramResourceLocation(mtl.prg, GL_UNIFORM, "translationPoint");
Q_ASSERT(mtl.uniLoc[3] >= 0);
} else {
Q_UNREACHABLE();
}
}
f->glBindProgramPipeline(mtl.ppl);
return &mtl;
}
void QQuickShapeNvprRenderNode::updatePath(ShapePathRenderData *d)
{
if (d->dirty & QQuickShapeNvprRenderer::DirtyPath) {
if (!d->path) {
d->path = nvpr.genPaths(1);
Q_ASSERT(d->path != 0);
}
if (d->source.str.isEmpty()) {
nvpr.pathCommands(d->path, d->source.cmd.count(), d->source.cmd.constData(),
d->source.coord.count(), GL_FLOAT, d->source.coord.constData());
} else {
nvpr.pathString(d->path, GL_PATH_FORMAT_SVG_NV, d->source.str.count(), d->source.str.constData());
}
}
if (d->dirty & QQuickShapeNvprRenderer::DirtyStyle) {
nvpr.pathParameterf(d->path, GL_PATH_STROKE_WIDTH_NV, d->strokeWidth);
nvpr.pathParameteri(d->path, GL_PATH_JOIN_STYLE_NV, d->joinStyle);
nvpr.pathParameteri(d->path, GL_PATH_MITER_LIMIT_NV, d->miterLimit);
nvpr.pathParameteri(d->path, GL_PATH_END_CAPS_NV, d->capStyle);
nvpr.pathParameteri(d->path, GL_PATH_DASH_CAPS_NV, d->capStyle);
}
if (d->dirty & QQuickShapeNvprRenderer::DirtyDash) {
nvpr.pathParameterf(d->path, GL_PATH_DASH_OFFSET_NV, d->dashOffset);
// count == 0 -> no dash
nvpr.pathDashArray(d->path, d->dashPattern.count(), d->dashPattern.constData());
}
if (d->dirty)
d->fallbackValid = false;
}
void QQuickShapeNvprRenderNode::renderStroke(ShapePathRenderData *d, int strokeStencilValue, int writeMask)
{
QQuickNvprMaterialManager::MaterialDesc *mtl = mtlmgr.activateMaterial(QQuickNvprMaterialManager::MatSolid);
f->glProgramUniform4f(mtl->prg, mtl->uniLoc[0],
d->strokeColor.x(), d->strokeColor.y(), d->strokeColor.z(), d->strokeColor.w());
f->glProgramUniform1f(mtl->prg, mtl->uniLoc[1], inheritedOpacity());
nvpr.stencilThenCoverStrokePath(d->path, strokeStencilValue, writeMask, GL_CONVEX_HULL_NV);
}
void QQuickShapeNvprRenderNode::renderFill(ShapePathRenderData *d)
{
QQuickNvprMaterialManager::MaterialDesc *mtl = nullptr;
if (d->fillGradientActive) {
QQuickShapeGradient::SpreadMode spread = d->fillGradient.spread;
if (d->fillGradientActive == QQuickAbstractPathRenderer::LinearGradient) {
mtl = mtlmgr.activateMaterial(QQuickNvprMaterialManager::MatLinearGradient);
// uv = vec2(coeff[0] * x + coeff[1] * y + coeff[2], coeff[3] * x + coeff[4] * y + coeff[5])
// where x and y are in path coordinate space, which is just what
// we need since the gradient's start and stop are in that space too.
GLfloat coeff[6] = { 1, 0, 0,
0, 1, 0 };
nvpr.programPathFragmentInputGen(mtl->prg, 0, GL_OBJECT_LINEAR_NV, 2, coeff);
f->glProgramUniform2f(mtl->prg, mtl->uniLoc[2], d->fillGradient.a.x(), d->fillGradient.a.y());
f->glProgramUniform2f(mtl->prg, mtl->uniLoc[3], d->fillGradient.b.x(), d->fillGradient.b.y());
} else if (d->fillGradientActive == QQuickAbstractPathRenderer::RadialGradient) {
mtl = mtlmgr.activateMaterial(QQuickNvprMaterialManager::MatRadialGradient);
// simply drive uv (location 0) with x and y, just like for the linear gradient
GLfloat coeff[6] = { 1, 0, 0,
0, 1, 0 };
nvpr.programPathFragmentInputGen(mtl->prg, 0, GL_OBJECT_LINEAR_NV, 2, coeff);
const QPointF centerPoint = d->fillGradient.a;
const QPointF focalPoint = d->fillGradient.b;
const QPointF focalToCenter = centerPoint - focalPoint;
const GLfloat centerRadius = d->fillGradient.v0;
const GLfloat focalRadius = d->fillGradient.v1;
f->glProgramUniform2f(mtl->prg, mtl->uniLoc[2], focalToCenter.x(), focalToCenter.y());
f->glProgramUniform1f(mtl->prg, mtl->uniLoc[3], centerRadius);
f->glProgramUniform1f(mtl->prg, mtl->uniLoc[4], focalRadius);
f->glProgramUniform2f(mtl->prg, mtl->uniLoc[5], focalPoint.x(), focalPoint.y());
} else if (d->fillGradientActive == QQuickAbstractPathRenderer::ConicalGradient) {
mtl = mtlmgr.activateMaterial(QQuickNvprMaterialManager::MatConicalGradient);
// same old
GLfloat coeff[6] = { 1, 0, 0,
0, 1, 0 };
nvpr.programPathFragmentInputGen(mtl->prg, 0, GL_OBJECT_LINEAR_NV, 2, coeff);
const QPointF centerPoint = d->fillGradient.a;
const GLfloat angle = -qDegreesToRadians(d->fillGradient.v0);
f->glProgramUniform1f(mtl->prg, mtl->uniLoc[2], angle);
f->glProgramUniform2f(mtl->prg, mtl->uniLoc[3], centerPoint.x(), centerPoint.y());
spread = QQuickShapeGradient::RepeatSpread;
} else {
Q_UNREACHABLE();
}
const QQuickShapeGradientCacheKey cacheKey(d->fillGradient.stops, spread);
QSGTexture *tx = QQuickShapeGradientOpenGLCache::currentCache()->get(cacheKey);
tx->bind();
} else {
mtl = mtlmgr.activateMaterial(QQuickNvprMaterialManager::MatSolid);
f->glProgramUniform4f(mtl->prg, mtl->uniLoc[0],
d->fillColor.x(), d->fillColor.y(), d->fillColor.z(), d->fillColor.w());
}
f->glProgramUniform1f(mtl->prg, mtl->uniLoc[1], inheritedOpacity());
const int writeMask = 0xFF;
nvpr.stencilThenCoverFillPath(d->path, d->fillRule, writeMask, GL_BOUNDING_BOX_NV);
}
void QQuickShapeNvprRenderNode::renderOffscreenFill(ShapePathRenderData *d)
{
if (d->fallbackValid && d->fallbackFbo)
return;
GLfloat bb[4];
nvpr.getPathParameterfv(d->path, GL_PATH_STROKE_BOUNDING_BOX_NV, bb);
QSize sz = QSizeF(bb[2] - bb[0] + 1, bb[3] - bb[1] + 1).toSize();
d->fallbackSize = QSize(qMax(32, sz.width()), qMax(32, sz.height()));
d->fallbackTopLeft = QPointF(bb[0], bb[1]);
if (d->fallbackFbo && d->fallbackFbo->size() != d->fallbackSize) {
delete d->fallbackFbo;
d->fallbackFbo = nullptr;
}
if (!d->fallbackFbo)
d->fallbackFbo = new QOpenGLFramebufferObject(d->fallbackSize, QOpenGLFramebufferObject::CombinedDepthStencil);
if (!d->fallbackFbo->bind())
return;
GLint prevViewport[4];
f->glGetIntegerv(GL_VIEWPORT, prevViewport);
f->glViewport(0, 0, d->fallbackSize.width(), d->fallbackSize.height());
f->glDisable(GL_DEPTH_TEST);
f->glClearColor(0, 0, 0, 0);
f->glClearStencil(0);
f->glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
f->glStencilFunc(GL_NOTEQUAL, 0, 0xFF);
f->glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
QMatrix4x4 mv;
mv.translate(-d->fallbackTopLeft.x(), -d->fallbackTopLeft.y());
nvpr.matrixLoadf(GL_PATH_MODELVIEW_NV, mv.constData());
QMatrix4x4 proj;
proj.ortho(0, d->fallbackSize.width(), d->fallbackSize.height(), 0, 1, -1);
nvpr.matrixLoadf(GL_PATH_PROJECTION_NV, proj.constData());
renderFill(d);
d->fallbackFbo->release();
f->glEnable(GL_DEPTH_TEST);
f->glViewport(prevViewport[0], prevViewport[1], prevViewport[2], prevViewport[3]);
d->fallbackValid = true;
}
void QQuickShapeNvprRenderNode::setupStencilForCover(bool stencilClip, int sv)
{
if (!stencilClip) {
// Assume stencil buffer is cleared to 0 for each frame.
// Within the frame dppass=GL_ZERO for glStencilOp ensures stencil is reset and so no need to clear.
f->glStencilFunc(GL_NOTEQUAL, 0, 0xFF);
f->glStencilOp(GL_KEEP, GL_KEEP, GL_ZERO);
} else {
f->glStencilFunc(GL_LESS, sv, 0xFF); // pass if (sv & 0xFF) < (stencil_value & 0xFF)
f->glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE); // dppass: replace with the original value (clip's stencil ref value)
}
}
void QQuickShapeNvprRenderNode::render(const RenderState *state)
{
f = QOpenGLContext::currentContext()->extraFunctions();
if (!nvprInited) {
if (!nvpr.create()) {
qWarning("NVPR init failed");
return;
}
mtlmgr.create(&nvpr);
nvprInited = true;
}
f->glUseProgram(0);
f->glStencilMask(~0);
f->glEnable(GL_STENCIL_TEST);
const bool stencilClip = state->stencilEnabled();
// when true, the stencil buffer already has a clip path with a ref value of sv
const int sv = state->stencilValue();
const bool hasScissor = state->scissorEnabled();
if (hasScissor) {
// scissor rect is already set, just enable scissoring
f->glEnable(GL_SCISSOR_TEST);
}
// Depth test against the opaque batches rendered before.
f->glEnable(GL_DEPTH_TEST);
f->glDepthFunc(GL_LESS);
nvpr.pathCoverDepthFunc(GL_LESS);
nvpr.pathStencilDepthOffset(-0.05f, -1);
bool reloadMatrices = true;
for (ShapePathRenderData &d : m_sp) {
updatePath(&d);
const bool hasFill = d.hasFill();
const bool hasStroke = d.hasStroke();
if (hasFill && stencilClip) {
// Fall back to a texture when complex clipping is in use and we have
// to fill. Reconciling glStencilFillPath's and the scenegraph's clip
// stencil semantics has not succeeded so far...
if (hasScissor)
f->glDisable(GL_SCISSOR_TEST);
renderOffscreenFill(&d);
reloadMatrices = true;
if (hasScissor)
f->glEnable(GL_SCISSOR_TEST);
}
if (reloadMatrices) {
reloadMatrices = false;
nvpr.matrixLoadf(GL_PATH_MODELVIEW_NV, matrix()->constData());
nvpr.matrixLoadf(GL_PATH_PROJECTION_NV, state->projectionMatrix()->constData());
}
// Fill!
if (hasFill) {
if (!stencilClip) {
setupStencilForCover(false, 0);
renderFill(&d);
} else {
if (!m_fallbackBlitter.isCreated())
m_fallbackBlitter.create();
f->glStencilFunc(GL_EQUAL, sv, 0xFF);
f->glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
QMatrix4x4 mv = *matrix();
mv.translate(d.fallbackTopLeft.x(), d.fallbackTopLeft.y());
m_fallbackBlitter.texturedQuad(d.fallbackFbo->texture(), d.fallbackFbo->size(),
*state->projectionMatrix(), mv,
inheritedOpacity());
}
}
// Stroke!
if (hasStroke) {
const int strokeStencilValue = 0x80;
const int writeMask = 0x80;
setupStencilForCover(stencilClip, sv);
if (stencilClip) {
// for the stencil step (eff. read mask == 0xFF & ~writeMask)
nvpr.pathStencilFunc(GL_EQUAL, sv, 0xFF);
// With stencilCLip == true the read mask for the stencil test before the stencil step is 0x7F.
// This assumes the clip stencil value is <= 127.
if (sv >= strokeStencilValue)
qWarning("Shape/NVPR: stencil clip ref value %d too large; expect rendering errors", sv);
}
renderStroke(&d, strokeStencilValue, writeMask);
}
if (stencilClip)
nvpr.pathStencilFunc(GL_ALWAYS, 0, ~0);
d.dirty = 0;
}
f->glBindProgramPipeline(0);
}
QSGRenderNode::StateFlags QQuickShapeNvprRenderNode::changedStates() const
{
return BlendState | StencilState | DepthState | ScissorState;
}
QSGRenderNode::RenderingFlags QQuickShapeNvprRenderNode::flags() const
{
return DepthAwareRendering; // avoid hitting the less optimal no-opaque-batch path in the renderer
}
bool QQuickShapeNvprRenderNode::isSupported()
{
static const bool nvprDisabled = qEnvironmentVariableIntValue("QT_NO_NVPR") != 0;
return !nvprDisabled && QQuickNvprFunctions::isSupported();
}
bool QQuickNvprBlitter::create()
{
if (isCreated())
destroy();
m_program = new QOpenGLShaderProgram;
if (QOpenGLContext::currentContext()->format().profile() == QSurfaceFormat::CoreProfile) {
m_program->addCacheableShaderFromSourceFile(QOpenGLShader::Vertex, QStringLiteral(":/qt-project.org/shapes/shaders/blit_core.vert"));
m_program->addCacheableShaderFromSourceFile(QOpenGLShader::Fragment, QStringLiteral(":/qt-project.org/shapes/shaders/blit_core.frag"));
} else {
m_program->addCacheableShaderFromSourceFile(QOpenGLShader::Vertex, QStringLiteral(":/qt-project.org/shapes/shaders/blit.vert"));
m_program->addCacheableShaderFromSourceFile(QOpenGLShader::Fragment, QStringLiteral(":/qt-project.org/shapes/shaders/blit.frag"));
}
m_program->bindAttributeLocation("qt_Vertex", 0);
m_program->bindAttributeLocation("qt_MultiTexCoord0", 1);
if (!m_program->link())
return false;
m_matrixLoc = m_program->uniformLocation("qt_Matrix");
m_opacityLoc = m_program->uniformLocation("qt_Opacity");
m_buffer = new QOpenGLBuffer;
if (!m_buffer->create())
return false;
m_buffer->bind();
m_buffer->allocate(4 * sizeof(GLfloat) * 6);
m_buffer->release();
return true;
}
void QQuickNvprBlitter::destroy()
{
if (m_program) {
delete m_program;
m_program = nullptr;
}
if (m_buffer) {
delete m_buffer;
m_buffer = nullptr;
}
}
void QQuickNvprBlitter::texturedQuad(GLuint textureId, const QSize &size,
const QMatrix4x4 &proj, const QMatrix4x4 &modelview,
float opacity)
{
QOpenGLExtraFunctions *f = QOpenGLContext::currentContext()->extraFunctions();
m_program->bind();
QMatrix4x4 m = proj * modelview;
m_program->setUniformValue(m_matrixLoc, m);
m_program->setUniformValue(m_opacityLoc, opacity);
m_buffer->bind();
if (size != m_prevSize) {
m_prevSize = size;
QPointF p0(size.width() - 1, size.height() - 1);
QPointF p1(0, 0);
QPointF p2(0, size.height() - 1);
QPointF p3(size.width() - 1, 0);
GLfloat vertices[6 * 4] = {
GLfloat(p0.x()), GLfloat(p0.y()), 1, 0,
GLfloat(p1.x()), GLfloat(p1.y()), 0, 1,
GLfloat(p2.x()), GLfloat(p2.y()), 0, 0,
GLfloat(p0.x()), GLfloat(p0.y()), 1, 0,
GLfloat(p3.x()), GLfloat(p3.y()), 1, 1,
GLfloat(p1.x()), GLfloat(p1.y()), 0, 1,
};
m_buffer->write(0, vertices, sizeof(vertices));
}
m_program->enableAttributeArray(0);
m_program->enableAttributeArray(1);
f->glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(GLfloat), nullptr);
f->glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(GLfloat), (const void *) (2 * sizeof(GLfloat)));
f->glBindTexture(GL_TEXTURE_2D, textureId);
f->glDrawArrays(GL_TRIANGLES, 0, 6);
f->glBindTexture(GL_TEXTURE_2D, 0);
m_buffer->release();
m_program->release();
}
QT_END_NAMESPACE