216 lines
8.5 KiB
C++
216 lines
8.5 KiB
C++
#include "Rendering/AdjustmentSurface.h"
|
|
#include "AddressSpaceLimit.h"
|
|
#include "Rendering/LayerRenderer.h"
|
|
#include "RenderFixtures.h"
|
|
#include <QPixmap>
|
|
#include <QtTest>
|
|
|
|
class AdjustmentSurfaceTests : public QObject {
|
|
Q_OBJECT
|
|
private slots:
|
|
void init();
|
|
void theBodyDrawsInThePaintersOwnCoordinates();
|
|
void onlyWhatThePainterShowsIsAllocated();
|
|
void handBlendedModesWorkOnAnyDevice();
|
|
void pastItsPixelBudgetNothingIsDrawn();
|
|
void theSurfaceStartsClear();
|
|
void theSurfaceComesBackUnderThePaintersOwnState();
|
|
void aSurfaceThatCannotBeAllocatedIsLoggedAndNothingIsDrawn();
|
|
};
|
|
|
|
void AdjustmentSurfaceTests::init()
|
|
{
|
|
QTest::failOnWarning(QRegularExpression("saved states|Unbalanced save/restore"));
|
|
}
|
|
|
|
void AdjustmentSurfaceTests::theBodyDrawsInThePaintersOwnCoordinates()
|
|
{
|
|
const QImage image = noise(20, 12, 1, 200);
|
|
const LayerTransform transform{.origin = {3, 2}, .size = {20, 12}, .rotation = 20, .sampling = LayerSampling::smooth};
|
|
const auto scene = [&](bool throughSurface) {
|
|
QImage surface = noise(64, 48, 2);
|
|
QPainter painter(&surface);
|
|
painter.translate(5.5, 4);
|
|
painter.scale(1.5, 1.5);
|
|
const auto body = [&](QPainter &target) { LayerRenderer::draw(image, transform, transform.center(), target, {.opacity = 0.8}); };
|
|
if (throughSurface)
|
|
AdjustmentSurface::draw(painter, body);
|
|
else
|
|
body(painter);
|
|
painter.end();
|
|
return surface;
|
|
};
|
|
QCOMPARE(largestAnywhere(scene(false), scene(true)), 0);
|
|
QVERIFY(scene(true) != noise(64, 48, 2));
|
|
}
|
|
|
|
void AdjustmentSurfaceTests::onlyWhatThePainterShowsIsAllocated()
|
|
{
|
|
QImage surface = BrushRaster::context(64, 48, false);
|
|
QPainter painter(&surface);
|
|
painter.scale(2, 2);
|
|
painter.setClipRect(QRectF(4, 6, 10, 5));
|
|
QSize allocated;
|
|
QTransform placement;
|
|
AdjustmentSurface::draw(painter, [&](QPainter &target) {
|
|
allocated = QSize(target.device()->width(), target.device()->height());
|
|
placement = target.deviceTransform();
|
|
target.fillRect(QRectF(0, 0, 32, 24), QColor(0, 0, 255));
|
|
});
|
|
painter.end();
|
|
QCOMPARE(allocated, QSize(20, 10));
|
|
QCOMPARE(placement, QTransform::fromScale(2, 2) * QTransform::fromTranslate(-8, -12));
|
|
QCOMPARE(surface.pixel(8, 12), qRgba(0, 0, 255, 255));
|
|
QCOMPARE(surface.pixel(27, 21), qRgba(0, 0, 255, 255));
|
|
QCOMPARE(surface.pixel(7, 12), qRgba(0, 0, 0, 0));
|
|
QCOMPARE(surface.pixel(28, 21), qRgba(0, 0, 0, 0));
|
|
QCOMPARE(surface.pixel(8, 22), qRgba(0, 0, 0, 0));
|
|
|
|
// A turned painter's box passes the device; the surface stays.
|
|
QImage turned = BrushRaster::context(40, 30, false);
|
|
QPainter rotated(&turned);
|
|
rotated.translate(20, 15);
|
|
rotated.rotate(30);
|
|
AdjustmentSurface::draw(rotated, [&](QPainter &target) { allocated = QSize(target.device()->width(), target.device()->height()); });
|
|
QCOMPARE(allocated, QSize(40, 30));
|
|
}
|
|
|
|
void AdjustmentSurfaceTests::handBlendedModesWorkOnAnyDevice()
|
|
{
|
|
const LayerTransform transform = placedAt({0, 0}, {8, 8});
|
|
const auto body = [&](QPainter &target) {
|
|
LayerRenderer::draw(solid(8, 8, qRgba(200, 40, 40, 255)), transform, transform.center(), target);
|
|
LayerRenderer::draw(solid(8, 8, qRgba(20, 20, 220, 255)), transform, transform.center(), target, {.blendMode = LayerBlendMode::luminosity});
|
|
};
|
|
QImage expected = BrushRaster::context(8, 8, false);
|
|
{
|
|
QPainter painter(&expected);
|
|
body(painter);
|
|
}
|
|
// No pixmap reads back; a surface supplies the backdrop.
|
|
QPixmap pixmap(8, 8);
|
|
pixmap.fill(Qt::transparent);
|
|
{
|
|
QPainter painter(&pixmap);
|
|
QVERIFY_EXCEPTION_THROWN(body(painter), std::logic_error);
|
|
}
|
|
pixmap.fill(Qt::transparent);
|
|
{
|
|
QPainter painter(&pixmap);
|
|
AdjustmentSurface::draw(painter, body);
|
|
}
|
|
QCOMPARE(pixmap.toImage().convertToFormat(QImage::Format_RGBA8888_Premultiplied), expected);
|
|
QVERIFY(expected.pixel(4, 4) != qRgba(200, 40, 40, 255));
|
|
}
|
|
|
|
void AdjustmentSurfaceTests::pastItsPixelBudgetNothingIsDrawn()
|
|
{
|
|
QImage surface = BrushRaster::context(8, 8, false);
|
|
QPainter painter(&surface);
|
|
// 64 device pixels; past that 16 units; past that, none.
|
|
painter.scale(2, 2);
|
|
bool called = false;
|
|
QTest::ignoreMessage(QtWarningMsg, "an adjustment surface passes its pixel budget: QSize(8, 8)");
|
|
AdjustmentSurface::draw(painter, [&](QPainter &) { called = true; }, 0, 15);
|
|
QVERIFY(!called);
|
|
QSize allocated;
|
|
AdjustmentSurface::draw(painter, [&](QPainter &target) {
|
|
allocated = QSize(target.device()->width(), target.device()->height());
|
|
target.fillRect(QRectF(0, 0, 2, 4), Qt::blue);
|
|
target.fillRect(QRectF(2, 0, 2, 4), Qt::green);
|
|
}, 0, 63);
|
|
QCOMPARE(allocated, QSize(4, 4));
|
|
// A device pixel at a quarter: one pixel, not none.
|
|
painter.save();
|
|
painter.scale(2, 2);
|
|
painter.setClipRect(QRectF(0, 0, 0.25, 1));
|
|
allocated = QSize();
|
|
AdjustmentSurface::draw(painter, [&](QPainter &target) { allocated = QSize(target.device()->width(), target.device()->height()); }, 0, 3);
|
|
painter.restore();
|
|
QCOMPARE(allocated, QSize(1, 1));
|
|
// Enlarged smoothly: the seam between blue and green blends.
|
|
QCOMPARE(surface.pixel(0, 0), qRgba(0, 0, 255, 255));
|
|
QCOMPARE(surface.pixel(7, 0), qRgba(0, 255, 0, 255));
|
|
QVERIFY(qBlue(surface.pixel(4, 0)) > 0 && qBlue(surface.pixel(4, 0)) < 255);
|
|
AdjustmentSurface::draw(painter, [&](QPainter &target) {
|
|
called = true;
|
|
target.fillRect(QRectF(0, 0, 4, 4), Qt::red);
|
|
}, 64);
|
|
painter.end();
|
|
QVERIFY(called);
|
|
QCOMPARE(surface.pixel(7, 7), qRgba(255, 0, 0, 255));
|
|
}
|
|
|
|
void AdjustmentSurfaceTests::theSurfaceStartsClear()
|
|
{
|
|
const QImage background = noise(64, 48, 3);
|
|
for (int round = 0; round < 8; ++round) {
|
|
// The next surface may get freed memory full of ones.
|
|
for (int dirty = 0; dirty < 4; ++dirty) {
|
|
QImage scratch(64, 48, QImage::Format_RGBA8888_Premultiplied);
|
|
scratch.fill(0xffffffff);
|
|
}
|
|
QImage surface = background;
|
|
QPainter painter(&surface);
|
|
AdjustmentSurface::draw(painter, [](QPainter &target) { target.fillRect(QRectF(0, 0, 8, 8), Qt::red); });
|
|
painter.end();
|
|
QCOMPARE(surface.pixel(4, 4), qRgba(255, 0, 0, 255));
|
|
QCOMPARE(surface.copy(8, 8, 56, 40), background.copy(8, 8, 56, 40));
|
|
}
|
|
}
|
|
|
|
void AdjustmentSurfaceTests::theSurfaceComesBackUnderThePaintersOwnState()
|
|
{
|
|
const auto body = [](QPainter &target) { target.fillRect(QRectF(0, 0, 8, 6), QColor(128, 128, 128)); };
|
|
// Opacity.
|
|
QImage faded = solid(8, 6, qRgba(200, 100, 50, 255));
|
|
{
|
|
QPainter painter(&faded);
|
|
painter.setOpacity(0);
|
|
AdjustmentSurface::draw(painter, body);
|
|
QCOMPARE(painter.opacity(), 0.0);
|
|
}
|
|
QCOMPARE(faded, solid(8, 6, qRgba(200, 100, 50, 255)));
|
|
// Composition mode.
|
|
QImage multiplied = solid(8, 6, qRgba(200, 100, 50, 255));
|
|
{
|
|
QPainter painter(&multiplied);
|
|
painter.setCompositionMode(QPainter::CompositionMode_Multiply);
|
|
AdjustmentSurface::draw(painter, body);
|
|
QCOMPARE(painter.compositionMode(), QPainter::CompositionMode_Multiply);
|
|
}
|
|
QCOMPARE(multiplied.pixel(7, 5), qRgba(100, 50, 25, 255));
|
|
// A clip of two rectangles; the painter's transform stays.
|
|
QImage clipped = solid(8, 6, qRgba(200, 100, 50, 255));
|
|
{
|
|
QPainter painter(&clipped);
|
|
painter.translate(1, 1);
|
|
painter.setClipRegion(QRegion(0, 0, 2, 2) + QRegion(4, 2, 2, 2));
|
|
AdjustmentSurface::draw(painter, body);
|
|
QCOMPARE(painter.worldTransform(), QTransform::fromTranslate(1, 1));
|
|
}
|
|
QCOMPARE(clipped.pixel(1, 1), qRgba(128, 128, 128, 255));
|
|
QCOMPARE(clipped.pixel(5, 3), qRgba(128, 128, 128, 255));
|
|
QCOMPARE(clipped.pixel(4, 2), qRgba(200, 100, 50, 255));
|
|
QCOMPARE(clipped.pixel(0, 0), qRgba(200, 100, 50, 255));
|
|
}
|
|
|
|
void AdjustmentSurfaceTests::aSurfaceThatCannotBeAllocatedIsLoggedAndNothingIsDrawn()
|
|
{
|
|
// The surface would need 256 MB.
|
|
QImage device(8192, 8192, QImage::Format_Alpha8);
|
|
device.fill(0);
|
|
QPainter painter(&device);
|
|
bool called = false;
|
|
std::optional<AddressSpaceLimit> limit(std::in_place, 96 * 1024 * 1024);
|
|
QTest::ignoreMessage(QtWarningMsg, "an adjustment surface could not be allocated: QSize(8192, 8192)");
|
|
AdjustmentSurface::draw(painter, [&](QPainter &) { called = true; });
|
|
limit.reset();
|
|
painter.end();
|
|
QVERIFY(!called);
|
|
QCOMPARE(int(device.constScanLine(100)[100]), 0);
|
|
}
|
|
|
|
QTEST_MAIN(AdjustmentSurfaceTests)
|
|
#include "AdjustmentSurfaceTests.moc"
|