Improve window decorations in OpenGL scene

When creating the texture containing the window decorations, the drawing
code now directly handles the rotation for the left and right
decoration, instead of rotating the image after it has been drawn.

The padding, to prevent texture bleeding, is now a fixed value instead
of being scaled. With this change, there are no longer visual artifacts
for window decorations with rounded corners, when the scaling value is
fractional.
This commit is contained in:
Julius Zint 2021-12-16 17:36:47 +01:00 committed by Méven Car
parent e655dc7b42
commit 3b4d558371
4 changed files with 143 additions and 144 deletions

View file

@ -14,6 +14,8 @@
#include "scene.h"
#include "utils.h"
#include <cmath>
#include <KDecoration2/Decoration>
#include <KDecoration2/DecoratedClient>
@ -190,78 +192,75 @@ DecorationRenderer *DecorationItem::renderer() const
return m_renderer.data();
}
WindowQuad buildQuad(const QRect &partRect, const QPoint &textureOffset,
const qreal devicePixelRatio, bool rotated)
{
const QRect &r = partRect;
const int p = DecorationRenderer::TexturePad;
const int x0 = r.x();
const int y0 = r.y();
const int x1 = r.x() + r.width();
const int y1 = r.y() + r.height();
int u0 = textureOffset.x() + p;
int v0 = textureOffset.y() + p;
int u1 = textureOffset.x() + p + (r.width() * devicePixelRatio);
int v1 = textureOffset.y() + p + (r.height() * devicePixelRatio);
if (rotated) {
u0 = textureOffset.x() + p;
v0 = textureOffset.y() + p + (r.width() * devicePixelRatio);
u1 = textureOffset.x() + p + (r.height() * devicePixelRatio);
v1 = textureOffset.y() + p;
}
WindowQuad quad;
quad[0] = WindowVertex(x0, y0, u0, v0); // Top-left
quad[1] = WindowVertex(x1, y0, u1, v0); // Top-right
quad[2] = WindowVertex(x1, y1, u1, v1); // Bottom-right
quad[3] = WindowVertex(x0, y1, u0, v1); // Bottom-left
return quad;
}
WindowQuadList DecorationItem::buildQuads() const
{
if (m_window->frameMargins().isNull()) {
return WindowQuadList();
}
QRect rects[4];
QRect left, top, right, bottom;
const qreal devicePixelRatio = m_renderer->devicePixelRatio();
const int texturePad = DecorationRenderer::TexturePad;
if (const AbstractClient *client = qobject_cast<const AbstractClient *>(m_window)) {
client->layoutDecorationRects(rects[0], rects[1], rects[2], rects[3]);
client->layoutDecorationRects(left, top, right, bottom);
} else if (const Deleted *deleted = qobject_cast<const Deleted *>(m_window)) {
deleted->layoutDecorationRects(rects[0], rects[1], rects[2], rects[3]);
deleted->layoutDecorationRects(left, top, right, bottom);
}
const qreal textureScale = m_renderer->devicePixelRatio();
const int padding = 1;
const int topHeight = std::ceil(top.height() * devicePixelRatio);
const int bottomHeight = std::ceil(bottom.height() * devicePixelRatio);
const int leftWidth = std::ceil(left.width() * devicePixelRatio);
const QPoint topSpritePosition(padding, padding);
const QPoint bottomSpritePosition(padding, topSpritePosition.y() + rects[1].height() + 2 * padding);
const QPoint leftSpritePosition(bottomSpritePosition.y() + rects[3].height() + 2 * padding, padding);
const QPoint rightSpritePosition(leftSpritePosition.x() + rects[0].width() + 2 * padding, padding);
const QPoint offsets[4] = {
QPoint(-rects[0].x(), -rects[0].y()) + leftSpritePosition,
QPoint(-rects[1].x(), -rects[1].y()) + topSpritePosition,
QPoint(-rects[2].x(), -rects[2].y()) + rightSpritePosition,
QPoint(-rects[3].x(), -rects[3].y()) + bottomSpritePosition,
};
const Qt::Orientation orientations[4] = {
Qt::Vertical, // Left
Qt::Horizontal, // Top
Qt::Vertical, // Right
Qt::Horizontal, // Bottom
};
const QPoint topPosition(0, 0);
const QPoint bottomPosition(0, topPosition.y() + topHeight + (2 * texturePad));
const QPoint leftPosition(0, bottomPosition.y() + bottomHeight + (2 * texturePad));
const QPoint rightPosition(0, leftPosition.y() + leftWidth + (2 * texturePad));
WindowQuadList list;
list.reserve(4);
for (int i = 0; i < 4; ++i) {
const QRect &r = rects[i];
if (!r.isValid()) {
continue;
}
const int x0 = r.x();
const int y0 = r.y();
const int x1 = r.x() + r.width();
const int y1 = r.y() + r.height();
const int u0 = (x0 + offsets[i].x()) * textureScale;
const int v0 = (y0 + offsets[i].y()) * textureScale;
const int u1 = (x1 + offsets[i].x()) * textureScale;
const int v1 = (y1 + offsets[i].y()) * textureScale;
WindowQuad quad;
if (orientations[i] == Qt::Vertical) {
quad[0] = WindowVertex(x0, y0, v0, u0); // Top-left
quad[1] = WindowVertex(x1, y0, v0, u1); // Top-right
quad[2] = WindowVertex(x1, y1, v1, u1); // Bottom-right
quad[3] = WindowVertex(x0, y1, v1, u0); // Bottom-left
} else {
quad[0] = WindowVertex(x0, y0, u0, v0); // Top-left
quad[1] = WindowVertex(x1, y0, u1, v0); // Top-right
quad[2] = WindowVertex(x1, y1, u1, v1); // Bottom-right
quad[3] = WindowVertex(x0, y1, u0, v1); // Bottom-left
}
list.append(quad);
if (left.isValid()) {
list.append(buildQuad(left, leftPosition, devicePixelRatio, true));
}
if (top.isValid()) {
list.append(buildQuad(top, topPosition, devicePixelRatio, false));
}
if (right.isValid()) {
list.append(buildQuad(right, rightPosition, devicePixelRatio, true));
}
if (bottom.isValid()) {
list.append(buildQuad(bottom, bottomPosition, devicePixelRatio, false));
}
return list;
}

View file

@ -41,6 +41,9 @@ public:
qreal devicePixelRatio() const;
void setDevicePixelRatio(qreal dpr);
// Reserve some space for padding. We pad decoration parts to avoid texture bleeding.
static const int TexturePad = 1;
Q_SIGNALS:
void damaged(const QRegion &region);

View file

@ -1530,31 +1530,6 @@ SceneOpenGLDecorationRenderer::~SceneOpenGLDecorationRenderer()
}
}
// Rotates the given source rect 90° counter-clockwise,
// and flips it vertically
static QImage rotate(const QImage &srcImage, const QRect &srcRect)
{
auto dpr = srcImage.devicePixelRatio();
QImage image(srcRect.height() * dpr, srcRect.width() * dpr, srcImage.format());
image.setDevicePixelRatio(dpr);
const QPoint srcPoint(srcRect.x() * dpr, srcRect.y() * dpr);
const uint32_t *src = reinterpret_cast<const uint32_t *>(srcImage.bits());
uint32_t *dst = reinterpret_cast<uint32_t *>(image.bits());
for (int x = 0; x < image.width(); x++) {
const uint32_t *s = src + (srcPoint.y() + x) * srcImage.width() + srcPoint.x();
uint32_t *d = dst + x;
for (int y = 0; y < image.height(); y++) {
*d = s[y];
d += image.width();
}
}
return image;
}
static void clamp_row(int left, int width, int right, const uint32_t *src, uint32_t *dest)
{
std::fill_n(dest, left, *src);
@ -1620,71 +1595,94 @@ void SceneOpenGLDecorationRenderer::render(const QRegion &region)
QRect left, top, right, bottom;
client()->client()->layoutDecorationRects(left, top, right, bottom);
// We pad each part in the decoration atlas in order to avoid texture bleeding.
const int padding = 1;
const int topHeight = std::ceil(top.height() * devicePixelRatio());
const int bottomHeight = std::ceil(bottom.height() * devicePixelRatio());
const int leftWidth = std::ceil(left.width() * devicePixelRatio());
auto renderPart = [=](const QRect &geo, const QRect &partRect, const QPoint &position, bool rotated = false) {
if (!geo.isValid()) {
return;
}
const QPoint topPosition(0, 0);
const QPoint bottomPosition(0, topPosition.y() + topHeight + (2 * TexturePad));
const QPoint leftPosition(0, bottomPosition.y() + bottomHeight + (2 * TexturePad));
const QPoint rightPosition(0, leftPosition.y() + leftWidth + (2 * TexturePad));
QRect rect = geo;
const QRect dirtyRect = region.boundingRect();
// We allow partial decoration updates and it might just so happen that the dirty region
// is completely contained inside the decoration part, i.e. the dirty region doesn't touch
// any of the decoration's edges. In that case, we should **not** pad the dirty region.
if (rect.left() == partRect.left()) {
rect.setLeft(rect.left() - padding);
}
if (rect.top() == partRect.top()) {
rect.setTop(rect.top() - padding);
}
if (rect.right() == partRect.right()) {
rect.setRight(rect.right() + padding);
}
if (rect.bottom() == partRect.bottom()) {
rect.setBottom(rect.bottom() + padding);
}
renderPart(top.intersected(dirtyRect), top, topPosition);
renderPart(bottom.intersected(dirtyRect), bottom, bottomPosition);
renderPart(left.intersected(dirtyRect), left, leftPosition, true);
renderPart(right.intersected(dirtyRect), right, rightPosition, true);
}
QRect viewport = geo.translated(-rect.x(), -rect.y());
void SceneOpenGLDecorationRenderer::renderPart(const QRect &rect, const QRect &partRect,
const QPoint &textureOffset, bool rotated)
{
if (!rect.isValid()) {
return;
}
// We allow partial decoration updates and it might just so happen that the
// dirty region is completely contained inside the decoration part, i.e.
// the dirty region doesn't touch any of the decoration's edges. In that
// case, we should **not** pad the dirty region.
const QMargins padding = texturePadForPart(rect, partRect);
int verticalPadding = padding.top() + padding.bottom();
int horizontalPadding = padding.left() + padding.right();
QImage image(rect.size() * devicePixelRatio(), QImage::Format_ARGB32_Premultiplied);
image.setDevicePixelRatio(devicePixelRatio());
image.fill(Qt::transparent);
QSize imageSize = rect.size() * devicePixelRatio();
if (rotated) {
imageSize = QSize(imageSize.height(), imageSize.width());
}
QSize paddedImageSize = imageSize;
paddedImageSize.rheight() += verticalPadding;
paddedImageSize.rwidth() += horizontalPadding;
QImage image(paddedImageSize, QImage::Format_ARGB32_Premultiplied);
image.setDevicePixelRatio(devicePixelRatio());
image.fill(Qt::transparent);
QPainter painter(&image);
painter.setRenderHint(QPainter::Antialiasing);
painter.setViewport(QRect(viewport.topLeft(), viewport.size() * devicePixelRatio()));
painter.setWindow(QRect(geo.topLeft(), geo.size() * qPainterEffectiveDevicePixelRatio(&painter)));
painter.setClipRect(geo);
renderToPainter(&painter, geo);
painter.end();
QRect padClip = QRect(padding.left(), padding.top(), imageSize.width(), imageSize.height());
QPainter painter(&image);
const qreal inverseScale = 1.0 / devicePixelRatio();
painter.scale(inverseScale, inverseScale);
painter.setRenderHint(QPainter::Antialiasing);
painter.setClipRect(padClip);
painter.translate(padding.left(), padding.top());
if (rotated) {
painter.translate(0, imageSize.height());
painter.rotate(-90);
}
painter.scale(devicePixelRatio(), devicePixelRatio());
painter.translate(-rect.topLeft());
renderToPainter(&painter, rect);
painter.end();
const QRect viewportScaled(viewport.topLeft() * devicePixelRatio(), viewport.size() * devicePixelRatio());
const bool isIntegerScaling = qFuzzyCompare(devicePixelRatio(), std::ceil(devicePixelRatio()));
clamp(image, isIntegerScaling ? viewportScaled : viewportScaled.marginsRemoved({1, 1, 1, 1}));
// fill padding pixels by copying from the neighbour row
clamp(image, padClip);
if (rotated) {
// TODO: get this done directly when rendering to the image
image = rotate(image, QRect(QPoint(), rect.size()));
viewport = QRect(viewport.y(), viewport.x(), viewport.height(), viewport.width());
}
QPoint dirtyOffset = (rect.topLeft() - partRect.topLeft()) * devicePixelRatio();
if (padding.top() == 0) {
dirtyOffset.ry() += TexturePad;
}
if (padding.left() == 0) {
dirtyOffset.rx() += TexturePad;
}
m_texture->update(image, textureOffset + dirtyOffset);
}
const QPoint dirtyOffset = geo.topLeft() - partRect.topLeft();
m_texture->update(image, (position + dirtyOffset - viewport.topLeft()) * image.devicePixelRatio());
};
const QRect geometry = region.boundingRect();
const QPoint topPosition(padding, padding);
const QPoint bottomPosition(padding, topPosition.y() + top.height() + 2 * padding);
const QPoint leftPosition(padding, bottomPosition.y() + bottom.height() + 2 * padding);
const QPoint rightPosition(padding, leftPosition.y() + left.width() + 2 * padding);
renderPart(left.intersected(geometry), left, leftPosition, true);
renderPart(top.intersected(geometry), top, topPosition);
renderPart(right.intersected(geometry), right, rightPosition, true);
renderPart(bottom.intersected(geometry), bottom, bottomPosition);
const QMargins SceneOpenGLDecorationRenderer::texturePadForPart(
const QRect &rect, const QRect &partRect)
{
QMargins result = QMargins(0, 0, 0, 0);
if (rect.top() == partRect.top()) {
result.setTop(TexturePad);
}
if (rect.bottom() == partRect.bottom()) {
result.setBottom(TexturePad);
}
if (rect.left() == partRect.left()) {
result.setLeft(TexturePad);
}
if (rect.right() == partRect.right()) {
result.setRight(TexturePad);
}
return result;
}
static int align(int value, int align)
@ -1702,15 +1700,12 @@ void SceneOpenGLDecorationRenderer::resizeTexture()
qMax(left.height(), right.height()));
size.rheight() = top.height() + bottom.height() +
left.width() + right.width();
size *= devicePixelRatio();
// Reserve some space for padding. We pad decoration parts to avoid texture bleeding.
const int padding = 1;
size.rwidth() += 2 * padding;
size.rheight() += 4 * 2 * padding;
size.rheight() += 4 * (2 * TexturePad);
size.rwidth() += 2 * TexturePad;
size.rwidth() = align(size.width(), 128);
size *= devicePixelRatio();
if (m_texture && m_texture->size() == size)
return;

View file

@ -215,6 +215,8 @@ public:
}
private:
void renderPart(const QRect &rect, const QRect &partRect, const QPoint &textureOffset, bool rotated = false);
static const QMargins texturePadForPart(const QRect &rect, const QRect &partRect);
void resizeTexture();
QScopedPointer<GLTexture> m_texture;
};