Krita Source Code Documentation
Loading...
Searching...
No Matches
kis_gap_map.cpp
Go to the documentation of this file.
1/*
2 * SPDX-FileCopyrightText: 2024 Maciej Jesionowski <yavnrh@gmail.com>
3 * SPDX-FileCopyrightText: 2018 by the MyPaint Development Team.
4 *
5 * SPDX-License-Identifier: GPL-2.0-or-later
6 */
7
8/*
9 * Attribution and license notice.
10 *
11 * The gap distance calculation method is adapted from MyPaint source code,
12 * originally implemented by Jesper Lloyd with the following copyright:
13 *
14 * Copyright (C) 2018 by the MyPaint Development Team.
15 *
16 * This program is free software; you can redistribute it and/or modify
17 * it under the terms of the GNU General Public License as published by
18 * the Free Software Foundation; either version 2 of the License, or
19 * (at your option) any later version.
20 */
21
22#include "kis_gap_map.h"
23#include <qglobal.h>
24#include <QtMath>
25#include <QMutex>
26#include <QMutexLocker>
27#include <KoColor.h>
29
30#if KIS_GAP_MAP_MEASURE_ELAPSED_TIME
31#include <QElapsedTimer>
32#endif
33
34namespace
35{
36
37// The following four transformation functions are used to parametrize the distance search algorithm.
38// All used together help cover the four octants (a 1/8th circular sector) of a half-circle covering a part of the map.
39
40ALWAYS_INLINE QPoint TransformNone(int x, int y, int xOffset, int yOffset)
41{
42 return {x + xOffset, y + yOffset};
43}
44
45ALWAYS_INLINE QPoint TransformRotateClockwiseMirrorHorizontally(int x, int y, int xOffset, int yOffset)
46{
47 return {x - yOffset, y - xOffset};
48}
49
50ALWAYS_INLINE QPoint TransformRotateClockwise(int x, int y, int xOffset, int yOffset)
51{
52 return {x - yOffset, y + xOffset};
53}
54
55ALWAYS_INLINE QPoint TransformMirrorHorizontally(int x, int y, int xOffset, int yOffset)
56{
57 return {x + xOffset, y - yOffset};
58}
59
60} // anonymous namespace
61
62template<bool BoundsCheck>
63bool KisGapMap::isOpaque(int x, int y)
64{
65#if KIS_GAP_MAP_DEBUG_LOGGING_AND_ASSERTS
66 const TileFlags flags = *tileFlagsPtr(x / TileSize, y / TileSize);
68 qDebug() << "ERROR: opacity at (" << x << "," << y << ") not loaded";
69 return false;
70 }
71#endif
72 if (BoundsCheck) {
73 if ((x >= 0) && (x < m_size.width()) && (y >= 0) && (y < m_size.height())) {
74 return dataPtr(x, y)->opacity == MIN_SELECTED;
75 } else {
76 return false;
77 }
78 } else {
79 return dataPtr(x, y)->opacity == MIN_SELECTED;
80 }
81}
82
83template<bool BoundsCheck>
84bool KisGapMap::isOpaque(const QPoint& p)
85{
86 return isOpaque<BoundsCheck>(p.x(), p.y());
87}
88
90 const QRect& mapBounds,
91 const FillOpacityFunc& fillOpacityFunc)
92 : m_gapSize(gapSize)
93 , m_size(mapBounds.size())
94 , m_numTiles(qCeil(static_cast<float>(m_size.width()) / TileSize),
95 qCeil(static_cast<float>(m_size.height()) / TileSize))
96 , m_fillOpacityFunc(fillOpacityFunc)
97 , m_deviceSp(new KisPaintDevice(KoColorSpaceRegistry::instance()->rgb8()))
98 , m_accessor(std::make_unique<KisTileOptimizedAccessor>(m_deviceSp))
99{
100 // Ensure the scanline fill uses the same coordinates.
101 KIS_ASSERT((mapBounds.x() == 0) && (mapBounds.y() == 0) &&
102 "Gap closing fill assumes x and y start at coordinate (0, 0)");
103
104 Data defaultPixel {};
105 defaultPixel.distance = DISTANCE_INFINITE;
106 defaultPixel.opacity = MAX_SELECTED; // here: max = transparent
107
108 KoColor color(reinterpret_cast<quint8*>(&defaultPixel), KoColorSpaceRegistry::instance()->rgb8());
110 m_deviceSp->fill(mapBounds, color);
111}
112
113void KisGapMap::loadOpacityTiles(const QRect& tileRect)
114{
115#if KIS_GAP_MAP_MEASURE_ELAPSED_TIME
116 QElapsedTimer timer;
117 timer.start();
118#endif
119
120 for (int ty = tileRect.top(); ty <= tileRect.bottom(); ++ty) {
121 for (int tx = tileRect.left(); tx <= tileRect.right(); ++tx) {
122 TileFlags* const pFlags = tileFlagsPtr(tx, ty);
123 if ((*pFlags & TILE_OPACITY_LOADED) == 0) {
124 // Resize and clamp to image bounds.
125 QRect rect(tx * TileSize, ty * TileSize, TileSize, TileSize);
126 rect.setRight(qMin(rect.right(), m_size.width() - 1));
127 rect.setBottom(qMin(rect.bottom(), m_size.height() - 1));
128
129#if KIS_GAP_MAP_DEBUG_LOGGING_AND_ASSERTS
130 qDebug() << "loadOpacityTiles()" << rect;
131#endif
132 // It's not too elegant to pass the device, but this performs the best for now.
133 const bool hasOpaquePixels = m_fillOpacityFunc(m_deviceSp.data(), rect);
134
135 // This tile is now loaded.
136 *pFlags |= TILE_OPACITY_LOADED | (hasOpaquePixels ? TILE_HAS_OPAQUE_PIXELS : 0);
137 }
138 }
139 }
140
141#if KIS_GAP_MAP_MEASURE_ELAPSED_TIME
142 m_opacityElapsedNanos += timer.nsecsElapsed();
143#endif
144}
145
147void KisGapMap::distanceSearchRowInnerLoop(bool boundsCheck, int y, int x1, int x2)
148{
149 if (boundsCheck) {
150 for (int x = x1; x <= x2; ++x) {
151 if (isOpaque<true>(x, y)) {
152 gapDistanceSearch<true>(x, y, TransformNone);
153 gapDistanceSearch<true>(x, y, TransformRotateClockwiseMirrorHorizontally);
154 gapDistanceSearch<true>(x, y, TransformRotateClockwise);
155 gapDistanceSearch<true>(x, y, TransformMirrorHorizontally);
156 }
157 }
158 } else {
159 for (int x = x1; x <= x2; ++x) {
160 if (isOpaque<false>(x, y)) {
161 gapDistanceSearch<false>(x, y, TransformNone);
162 gapDistanceSearch<false>(x, y, TransformRotateClockwiseMirrorHorizontally);
163 gapDistanceSearch<false>(x, y, TransformRotateClockwise);
164 gapDistanceSearch<false>(x, y, TransformMirrorHorizontally);
165 }
166 }
167 }
168}
169
180void KisGapMap::loadDistanceTile(const QPoint& tile, const QRect& nearbyTilesRect, int guardBand)
181{
182#if KIS_GAP_MAP_MEASURE_ELAPSED_TIME
183 QElapsedTimer timer;
184 timer.start();
185#endif
186
187 TileFlags* const pFlags = tileFlagsPtr(tile.x(), tile.y());
188
189 // This tile is now considered loaded.
190 *pFlags |= TILE_DISTANCE_LOADED;
191
192 // Optimization: If a tile is completely transparent (TILE_HAS_OPAQUE_PIXELS == 0), then
193 // we can skip the distance calculation for it. Unfortunately, with the guard bands we need
194 // to check the flags of the neighboring tiles as well.
195
196 const bool tileOpaque = (*pFlags & TILE_HAS_OPAQUE_PIXELS) != 0;
197 const bool tileOpaqueLeft = (nearbyTilesRect.left() == tile.x()) ? false : (*tileFlagsPtr(tile.x() - 1, tile.y()) & TILE_HAS_OPAQUE_PIXELS) != 0;
198 const bool tileOpaqueTopLeft = (nearbyTilesRect.left() == tile.x()) || (nearbyTilesRect.top() == tile.y()) ? false : (*tileFlagsPtr(tile.x() - 1, tile.y() - 1) & TILE_HAS_OPAQUE_PIXELS) != 0;
199 const bool tileOpaqueBottomLeft = (nearbyTilesRect.left() == tile.x()) || (nearbyTilesRect.bottom() == tile.y()) ? false : (*tileFlagsPtr(tile.x() - 1, tile.y() + 1) & TILE_HAS_OPAQUE_PIXELS) != 0;
200 const bool tileOpaqueTop = (nearbyTilesRect.top() == tile.y()) ? false : (*tileFlagsPtr(tile.x(), tile.y() - 1) & TILE_HAS_OPAQUE_PIXELS) != 0;
201 const bool tileOpaqueBottom = (nearbyTilesRect.bottom() == tile.y()) ? false : (*tileFlagsPtr(tile.x(), tile.y() + 1) & TILE_HAS_OPAQUE_PIXELS) != 0;
202
203 if (! (tileOpaqueTopLeft || tileOpaqueTop || tileOpaqueLeft || tileOpaque || tileOpaqueBottomLeft || tileOpaqueBottom)) {
204 // This tile as well as its surroundings are transparent.
205 // We can simply exit without explicitly initializing the tile. The paint device's default pixel is DISTANCE_INFINITE.
206
207#if KIS_GAP_MAP_MEASURE_ELAPSED_TIME
208 m_distanceElapsedNanos += timer.nsecsElapsed();
209#endif
210 return;
211 }
212
213 // The area of the image covered only by this tile.
214 QRect rect(tile.x() * TileSize, tile.y() * TileSize, TileSize, TileSize);
215 rect.setRight(qMin(rect.right(), m_size.width() - 1));
216 rect.setBottom(qMin(rect.bottom(), m_size.height() - 1));
217
218 // Compromise: At the tile size 64 px and the gap size 32 px, the guard band must be
219 // 31 px at most, because opacity is sampled in gap size + 1 radius, which could be two tiles
220 // away and that tile might not have been loaded yet. To avoid loading one row of that tile,
221 // we can clamp the guard band to 31. The error introduced by it should not be noticeable.
222
223 const int guardBandVertical = qMin(guardBand, 31);
224 const int y1 = tileOpaqueTopLeft || tileOpaqueTop ? qMax(0, rect.top() - guardBandVertical) : rect.top();
225 const int y2 = tileOpaqueBottomLeft || tileOpaqueBottom ? qMin(rect.bottom() + guardBandVertical, m_size.height() - 1) : rect.bottom();
226 const int x1Top = tileOpaqueTopLeft ? qMax(0, rect.left() - guardBand) : rect.left();
227 const int x1Middle = tileOpaqueLeft ? qMax(0, rect.left() - guardBand) : rect.left();
228 const int x1Bottom = tileOpaqueBottomLeft ? qMax(0, rect.left() - guardBand) : rect.left();
229 const int x2Top = tileOpaqueTop ? rect.right() : rect.left() - 1;
230 const int x2Middle = tileOpaque ? rect.right() : rect.left() - 1;
231 const int x2Bottom = tileOpaqueBottom ? rect.right() : rect.left() - 1;
232
233 // Apply conservative bounds checking. +1 for opacity.
234 const bool boundsCheck =
235 (rect.right() + (m_gapSize + 1) >= m_size.width()) || // no risk of accessing x<0
236 (y1 - (m_gapSize + 1) < 0) || (y2 + (m_gapSize + 1) >= m_size.height());
237
238 m_tilePosition = rect.topLeft();
239 m_tileDataPtr = reinterpret_cast<Data*>(m_accessor->tileRawData(tile.x(), tile.y()));
240
241 // Process the tile and its neighborhood in three passes:
242 // Top (the top guard bands)
243 for (int y = y1; y <= rect.top() - 1; ++y) {
244 distanceSearchRowInnerLoop(boundsCheck, y, x1Top, x2Top);
245 }
246 // Middle (the left guard band and the tile)
247 for (int y = rect.top(); y <= rect.bottom(); ++y) {
248 distanceSearchRowInnerLoop(boundsCheck, y, x1Middle, x2Middle);
249 }
250 // Bottom (the bottom guard bands)
251 for (int y = rect.bottom() + 1; y <= y2; ++y) {
252 distanceSearchRowInnerLoop(boundsCheck, y, x1Bottom, x2Bottom);
253 }
254
255#if KIS_GAP_MAP_MEASURE_ELAPSED_TIME
256 m_distanceElapsedNanos += timer.nsecsElapsed();
257#endif
258}
259
280template<bool BoundsCheck, typename CoordinateTransform>
281void KisGapMap::gapDistanceSearch(int x, int y, CoordinateTransform op)
282{
283 if (isOpaque<BoundsCheck>(op(x, y, 0, -1)) ||
284 isOpaque<BoundsCheck>(op(x, y, 1, -1))) {
285 return;
286 }
287
288 for (int yoffs = 2; yoffs < m_gapSize + 2; ++yoffs) {
289 const int yDistanceSq = (yoffs - 1) * (yoffs - 1);
290
291 for (int xoffs = 0; xoffs <= yoffs; ++xoffs) {
292 const int offsetDistance = yDistanceSq + xoffs * xoffs;
293
294 if (offsetDistance >= 1 + m_gapSize * m_gapSize) {
295 break;
296 }
297
298 if (isOpaque<BoundsCheck>(op(x, y, xoffs, -yoffs))) {
299 const float dx = static_cast<float>(xoffs) / (yoffs - 1);
300 float tx = 0;
301 int cx = 0;
302
303 for (int cy = 1; cy < yoffs; ++cy) {
304 updateDistance(op(x, y, cx, -cy), offsetDistance);
305
306 tx += dx;
307 if (static_cast<int>(tx) > cx) {
308 cx++;
309 updateDistance(op(x, y, cx, -cy), offsetDistance);
310 }
311
312 updateDistance(op(x, y, cx + 1, -cy), offsetDistance);
313 }
314 }
315 }
316 }
317}
318
319void KisGapMap::updateDistance(const QPoint& globalPosition, quint16 newDistance)
320{
321 const QPoint p = globalPosition - m_tilePosition;
322
323 if ((p.x() < 0) || (p.x() >= TileSize) || (p.y() < 0) || (p.y() >= TileSize)) {
324 return;
325 }
326
327 Data* ptr = m_tileDataPtr + p.x() + TileSize * p.y();
328 if (ptr->distance > newDistance) {
329 ptr->distance = newDistance;
330 }
331}
332
334quint16 KisGapMap::lazyDistance(int x, int y)
335{
336#if KIS_GAP_MAP_DEBUG_LOGGING_AND_ASSERTS
337 qDebug() << "lazyDistance() at (" << x << "," << y << ")";
338#endif
339
340 const int tx = x / TileSize;
341 const int ty = y / TileSize;
342
343 // Clamped tile neighborhood.
344 const QPoint topLeft(qMax(0, tx - 1),
345 qMax(0, ty - 1));
346 const QPoint bottomRight(qMin(tx + 1, m_numTiles.width() - 1),
347 qMin(ty + 1, m_numTiles.height() - 1));
348 const QRect nearbyTiles = QRect(topLeft, bottomRight);
349
350 // For opacity data, we always load all the adjacent tiles (up to 9 tiles in total).
351 loadOpacityTiles(nearbyTiles);
352
353 // For distance data, we always load a single tile.
354 loadDistanceTile(QPoint(tx, ty), nearbyTiles, m_gapSize);
355
356 // The data is now ready to be returned.
357 return dataPtr(x, y)->distance;
358}
const Params2D p
#define ALWAYS_INLINE
PythonPluginManager * instance
const QSize m_numTiles
Map size in tiles.
@ TILE_OPACITY_LOADED
Opacity data is available.
@ TILE_HAS_OPAQUE_PIXELS
Some pixels of the loaded tile are opaque.
@ TILE_DISTANCE_LOADED
Distance data is available.
quint8 TileFlags
const FillOpacityFunc m_fillOpacityFunc
A callback to get the opacity data from the fill class.
std::unique_ptr< KisTileOptimizedAccessor > m_accessor
An accessor for the paint device.
KisGapMap(int gapSize, const QRect &mapBounds, const FillOpacityFunc &fillOpacityFunc)
void updateDistance(const QPoint &globalPosition, quint16 newDistance)
const int m_gapSize
Gap size in pixels for this map.
ALWAYS_INLINE bool isOpaque(int x, int y)
void distanceSearchRowInnerLoop(bool boundsCheck, int y, int x1, int x2)
const QSize m_size
Size in pixels of the opacity/gap map.
QPoint m_tilePosition
The position of the currently computed tile compared to the whole region.
ALWAYS_INLINE Data * dataPtr(int x, int y)
static constexpr quint16 DISTANCE_INFINITE
Definition kis_gap_map.h:84
static constexpr int TileSize
ALWAYS_INLINE TileFlags * tileFlagsPtr(int tileX, int tileY)
quint16 lazyDistance(int x, int y)
KisPaintDeviceSP m_deviceSp
A 32-bit per pixel paint device that holds the distance and other data.
void loadOpacityTiles(const QRect &tileRect)
void gapDistanceSearch(int x, int y, CoordinateTransform op)
Data * m_tileDataPtr
The pointer to the currently computed tile data.
void loadDistanceTile(const QPoint &tile, const QRect &nearbyTilesRect, int guardBand)
void setDefaultPixel(const KoColor &defPixel)
void fill(const QRect &rc, const KoColor &color)
#define KIS_SAFE_ASSERT_RECOVER(cond)
Definition kis_assert.h:126
#define KIS_ASSERT(cond)
Definition kis_assert.h:33
const quint8 MAX_SELECTED
Definition kis_global.h:32
const quint8 MIN_SELECTED
Definition kis_global.h:33
static KoColorSpaceRegistry * instance()