use SkIPoint for positions in GrTextBlob

Instead of using SkPoint to store position information,
we can constrain the position information to be integers.
This will lead to two future improvements.
  * Shrink position information to SkIPoint16
  * Use integer arithmetic instead of floating point
    in vertex calculation.

 I'm interested in feedback on the comment which describes
 the math behind this technique.

Change-Id: I8441bcbcad99d07e6d6d5e1788d1a47d87f22923
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/306948
Commit-Queue: Herb Derby <herb@google.com>
Reviewed-by: Ben Wagner <bungeman@google.com>
Reviewed-by: Robert Phillips <robertphillips@google.com>
This commit is contained in:
Herb Derby 2020-07-30 16:57:22 -04:00 committed by Skia Commit-Bot
parent 86c272aa65
commit 1fbb331bbb
6 changed files with 155 additions and 63 deletions

View File

@ -68,41 +68,38 @@ void SkDrawableGlyphBuffer::startBitmapDevice(
SkDEBUGCODE(fPhase = kInput);
}
void SkDrawableGlyphBuffer::startGPUDevice(
SkPoint SkDrawableGlyphBuffer::startGPUDevice(
const SkZip<const SkGlyphID, const SkPoint>& source,
SkPoint origin, const SkMatrix& viewMatrix,
const SkGlyphPositionRoundingSpec& roundingSpec) {
fInputSize = source.size();
fDrawableSize = 0;
// Map the positions including subpixel position.
auto positions = source.get<1>();
SkMatrix matrix = viewMatrix;
matrix.preTranslate(origin.x(), origin.y());
// Q = [M][T](0,0).
SkPoint Q = matrix.mapXY(0, 0);
SkMatrix device = viewMatrix;
SkPoint halfSampleFreq = roundingSpec.halfAxisSampleFreq;
matrix.postTranslate(halfSampleFreq.x(), halfSampleFreq.y());
matrix.mapPoints(fPositions, positions.data(), positions.size());
device.postTranslate(halfSampleFreq.x(), halfSampleFreq.y());
device.preTranslate(origin.x(), origin.y());
// Mask for controlling axis alignment.
SkIPoint mask = roundingSpec.ignorePositionFieldMask;
auto positions = source.get<1>();
device.mapPoints(fPositions, positions.data(), positions.size());
// Convert glyph ids and positions to packed glyph ids.
SkZip<const SkGlyphID, const SkPoint> withMappedPos =
SkMakeZip(source.get<0>(), fPositions.get());
SkGlyphVariant* packedIDCursor = fMultiBuffer;
for (auto [glyphID, pos] : withMappedPos) {
*packedIDCursor++ = SkPackedGlyphID{glyphID, pos, mask};
}
auto floor = [](SkPoint pt) -> SkPoint {
return {SkScalarFloorToScalar(pt.x()), SkScalarFloorToScalar(pt.y())};
};
for (SkPoint& pos : SkSpan<SkPoint>(fPositions, source.size())) {
SkPoint P = SkPoint::Make(SkScalarFloorToScalar(pos.x()), SkScalarFloorToScalar(pos.y()));
pos = P - Q;
// q = [Q](0,0,1) = [R][V][O](0,0,1).
SkPoint q = device.mapXY(0, 0);
SkPoint qFloor = floor(q);
for (auto [packedGlyphID, glyphID, pos]
: SkMakeZip(fMultiBuffer.get(), source.get<0>(), fPositions.get())) {
packedGlyphID = SkPackedGlyphID{glyphID, pos, roundingSpec.ignorePositionFieldMask};
pos = floor(pos - qFloor);
}
SkDEBUGCODE(fPhase = kInput);
// Return the residual = Floor(q) - q + (rx,ry,0).
return qFloor - q + roundingSpec.halfAxisSampleFreq;
}

View File

@ -155,22 +155,96 @@ public:
// Load the buffer with SkPackedGlyphIDs, calculating positions so they can be constant.
//
// A final device position is computed in the following manner:
// [x,y] = Floor[M][T][x',y']^t
// M is complicated but includes the rounding offsets for subpixel positioning.
// T is the translation matrix derived from the text blob origin.
// The final position is {Floor(x), Floor(y)}. If we want to move this position around in
// device space given a start origin T in source space and a end position T' in source space
// and new device matrix M', we need to calculate a suitable device space translation V. We
// know that V must be integer.
// V = [M'][T'](0,0)^t - [M][T](0,0)^t.
// V = Q' - Q
// So all the positions Ps are translated by V to translate from T to T' in source space. We can
// generate Ps such that we just need to add any Q' to the constant Ps to get a final positions.
// So, a single point P = {Floor(x)-Q_x, Floor(y)-Q_y}; this does not have to be integer.
// This allows positioning to be P + Q', which given ideal numbers would be an integer. Since
// the addition is done with floating point, it must be rounded.
void startGPUDevice(
// We are looking for constant values for the x,y positions for all the glyphs that are not
// dependant on the device origin mapping Q such that we can just add a new value to translate
// all the glyph positions to a new device origin mapping Q'. We want (cx,cy,0) + [Q'](0,0,1)
// draw the blob with device origin Q'. Ultimately we show there is an integer solution for
// the glyph positions where (ix,iy,0) + ([Q'](0,0,1) + (sx,sy,0)) both parts of the top
// level + are integers, and preserve all the flooring properties.
//
// Given (px,py) the glyph origin in source space. The glyph origin in device space (x,y) is:
// (x,y,1) = Floor([R][V][O](px,py,1))
// where:
// * R - is the rounding matrix given as translate(sampling_freq_x/2, sampling_freq_y/2).
// * V - is the mapping from source space to device space.
// * O - is the blob origin given, as translate(origin.x(), origin.y()).
// * (px,py,1) - is the vector of the glyph origin in source space. There is a position for
// each glyph.
//
// It is given that if there is a change in position from V to V', and O to O' that the upper
// 2x2 of V and V' are the same.
//
// The three matrices R,V, and O constitute the device mapping [Q] = [R][V][O], and the
// device origin is given by q = [Q](0,0,1). Thus,
// (x,y,1) = Floor([Q](0,0,1) + [V](px,py,0)) = Floor(q + [V](px,py,0))
// Note: [V](px,py,0) is the vector transformed without the translation portion of V. That
// translation of V is accounted for in q.
//
// If we want to translate the blob from the device mapping Q to the device mapping
// [Q'] = [R'][V'][O], we can use the following translation. Restate as q' - q.
// (x',y',1) = Floor(q + [V](px,py,0) + q' - q).
//
// We are given that q' - q is an integer translation. We can move the integer translation out
// from the Floor expression as:
// (x',y',1) = Floor(q + [V](px,py,0)) + q' - q (1)
//
// We can now see that (cx,cy,0) is constructed by dropping q' from above.
// (cx,cy,0) = Floor(q + [V](px,py,0)) - q
//
// Notice that cx and cy are not guaranteed to be integers because q is not
// constrained to be integer; only q' - q is constrained to be an integer.
//
// Let Floor(q) be the integer portion the vector elements and {q} be the fractional portion
// which is calculated as q - Floor(q). This vector has a zero in the third place due to the
// subtraction.
// Rewriting (1) with this substitution of Floor(q) + {q} for q.
// (x',y',1) = Floor(q + [V](px,py,0)) + q' - q
// becomes,
// (x',y',1) = Floor(Floor(q) + {q} + [V](px,py,0)) + q' - (q + {q})
// simplifying by moving Floor(q) out of the Floor() because it is integer,
// (x',y',1) = Floor({q} + [V](px,py,0)) + q' + Floor(q) - Floor(q) - {q}
// removing terms that result in zero gives,
// (x',y',1) = Floor({q} + [V](px,py,0)) + q' - {q}
// Notice that q' - {q} and Floor({q} + [V](px,py,0)) are integer.
// Let,
// (ix,iy,0) = Floor({q} + [V](px,py,0)),
// (sx,sy,0) = -{q}.
// I call the (sx,sy,0) value the residual.
// Thus,
// (x',y',1) = (ix,iy,0) + (q' + (sx,sy,0)). (2)
//
// As a matter of practicality, we have the following already calculated for sub-pixel
// positioning, and use it to calculate (ix,iy,0):
// (fx,fy,1) = [R][V][O](px,py,1)
// = [Q](0,0,1) + [V](px,py,0)
// = q + [V](px,py,0)
// = Floor(q) + {q} + [V](px,py,0)
// So,
// (ix,iy,0) = Floor((fx,fy,1) - Floor(q)).
//
// When calculating [Q'] = [R][V'][O'] we don't have the values for [R]. Notice that [R] is a
// post translation to [V'][O']. This means that the values of R are added directly to the
// translation values of [V'][O']. So, if [V'][O'](0,0,1) results in the vector (tx,ty,1)
// then [R](tx,ty,0) = (tx + rx, ty + ry, 0). So, in practice we don't have the full [Q'] what
// is available is [Q''] = [V'][O']. We can add the rounding terms to the residual
// to account for not having [R]. Substituting -{q} for (sx,sy,0) in (2), gives:
// (x',y',1) = (ix,iy,0) + (q' - {q}).
// = (ix,iy,0) + ([Q'](0,0,1) - {q})
// = (ix,iy,0) + ([R][V'][O'](0,0,1) - {q})
// = (ix,iy,0) + ((rx,ry,0) + [V'][O'](0,0,1) - {q})
// = (ix,iy,0) + ([V'][O'](0,0,1) + (rx,ry,0) - {q}.
// So we redefine the residual to include the needed rounding terms.
// (sx',sy',0) = (rx,ry,0) - (q - Floor(q))
// = (rx,ry,0) + Floor(q) - q.
//
// Putting it all together:
// Q'' = [V'][O'](0,0,1)
// q'' = Q''(0, 0, 1)
// (x',y',1) = (ix,iy,0) + (q'' + (sx',sy',0)).
// Returns the residual -- (sx',sy',0).
SkPoint startGPUDevice(
const SkZip<const SkGlyphID, const SkPoint>& source,
SkPoint origin, const SkMatrix& viewMatrix,
const SkGlyphPositionRoundingSpec& roundingSpec);

View File

@ -190,14 +190,15 @@ void SkGlyphRunListPainter::processGlyphRunList(const SkGlyphRunList& glyphRunLi
SkScopedStrikeForGPU strike = strikeSpec.findOrCreateScopedStrike(fStrikeCache);
fDrawable.startGPUDevice(fRejects.source(), origin, drawMatrix, strike->roundingSpec());
SkPoint residual = fDrawable.startGPUDevice(
fRejects.source(), origin, drawMatrix, strike->roundingSpec());
strike->prepareForMaskDrawing(&fDrawable, &fRejects);
fRejects.flipRejectsToSource();
if (process && !fDrawable.drawableIsEmpty()) {
// processDeviceMasks must be called even if there are no glyphs to make sure runs
// are set correctly.
process->processDeviceMasks(fDrawable.drawable(), strikeSpec);
process->processDeviceMasks(fDrawable.drawable(), strikeSpec, residual);
}
}

View File

@ -133,7 +133,8 @@ public:
virtual ~SkGlyphRunPainterInterface() = default;
virtual void processDeviceMasks(const SkZip<SkGlyphVariant, SkPoint>& drawables,
const SkStrikeSpec& strikeSpec) = 0;
const SkStrikeSpec& strikeSpec,
SkPoint residual) = 0;
virtual void processSourceMasks(const SkZip<SkGlyphVariant, SkPoint>& drawables,
const SkStrikeSpec& strikeSpec) = 0;

View File

@ -226,11 +226,13 @@ static GrAtlasTextOp::MaskType op_mask_type(GrMaskFormat grMaskFormat) {
// -- GrDirectMaskSubRun ---------------------------------------------------------------------------
GrDirectMaskSubRun::GrDirectMaskSubRun(GrMaskFormat format,
SkPoint residual,
GrTextBlob* blob,
const SkRect& bounds,
SkSpan<const VertexData> vertexData,
GrGlyphVector glyphs)
: fMaskFormat{format}
, fResidual{residual}
, fBlob{blob}
, fVertexBounds{bounds}
, fVertexData{vertexData}
@ -239,10 +241,12 @@ GrDirectMaskSubRun::GrDirectMaskSubRun(GrMaskFormat format,
GrSubRun* GrDirectMaskSubRun::Make(const SkZip<SkGlyphVariant, SkPoint>& drawables,
const SkStrikeSpec& strikeSpec,
GrMaskFormat format,
SkPoint residual,
GrTextBlob* blob,
SkArenaAlloc* alloc) {
size_t vertexCount = drawables.size();
SkRect bounds = SkRectPriv::MakeLargestInverted();
auto initializer = [&](size_t i) {
auto [variant, pos] = drawables[i];
SkGlyph* skGlyph = variant;
@ -254,14 +258,14 @@ GrSubRun* GrDirectMaskSubRun::Make(const SkZip<SkGlyphVariant, SkPoint>& drawabl
rb = SkPoint::Make(r, b) + pos;
bounds.joinPossiblyEmptyRect(SkRect::MakeLTRB(lt.x(), lt.y(), rb.x(), rb.y()));
return lt;
return VertexData{SkScalarRoundToInt(lt.x()), SkScalarRoundToInt(lt.y())};
};
SkSpan<const VertexData> vertexData{
alloc->makeInitializedArray<VertexData>(vertexCount, initializer), vertexCount};
GrDirectMaskSubRun* subRun = alloc->make<GrDirectMaskSubRun>(
format, blob, bounds, vertexData,
format, residual, blob, bounds, vertexData,
GrGlyphVector::Make(strikeSpec, drawables.get<0>(), alloc));
return subRun;
@ -386,15 +390,15 @@ void GrDirectMaskSubRun::fillVertexData(void* vertexDst, int offset, int count,
auto direct2D = [&](auto dst, SkIRect* clip) {
// Rectangles in device space
SkPoint originInDeviceSpace = matrix.mapXY(0, 0);
SkPoint originInDeviceSpace = matrix.mapXY(0, 0) + fResidual;
SkIPoint originInDeviceSpaceI = {SkScalarRoundToInt(originInDeviceSpace.x()),
SkScalarRoundToInt(originInDeviceSpace.y())};
for (auto[quad, glyph, leftTop] : vertices(dst)) {
GrIRect16 rect = glyph->fAtlasLocator.rect();
int16_t w = rect.width(),
h = rect.height();
auto[l, t] = leftTop + originInDeviceSpaceI;
auto[al, at, ar, ab] = glyph->fAtlasLocator.getUVs();
auto[fl, ft] = leftTop + originInDeviceSpace;
int l = SkScalarRoundToInt(fl),
t = SkScalarRoundToInt(ft);
if (clip == nullptr) {
auto[dl, dt, dr, db] = SkRect::MakeLTRB(l, t, l + w, t + h);
quad[0] = {{dl, dt}, color, {al, at}}; // L,T
@ -411,16 +415,14 @@ void GrDirectMaskSubRun::fillVertexData(void* vertexDst, int offset, int count,
int tD = clipped.top() - devIRect.top();
int rD = clipped.right() - devIRect.right();
int bD = clipped.bottom() - devIRect.bottom();
int indexLT, indexRB;
std::tie(dl, dt, dr, db) = ltbr(clipped);
std::tie(tl, tt, indexLT) =
GrDrawOpAtlas::UnpackIndexFromTexCoords(al, at);
std::tie(tr, tb, indexRB) =
GrDrawOpAtlas::UnpackIndexFromTexCoords(ar, ab);
int index = glyph->fAtlasLocator.pageIndex();
std::tie(tl, tt) =
GrDrawOpAtlas::PackIndexInTexCoords(tl + lD, tt + tD, indexLT);
GrDrawOpAtlas::PackIndexInTexCoords(
rect.fLeft + lD, rect.fTop + tD, index);
std::tie(tr, tb) =
GrDrawOpAtlas::PackIndexInTexCoords(tr + rD, tb + bD, indexRB);
GrDrawOpAtlas::PackIndexInTexCoords(
rect.fRight + rD, rect.fBottom + bD, index);
} else {
// TODO: omit generating any vertex data for fully clipped glyphs ?
std::tie(dl, dt, dr, db) = std::make_tuple(0, 0, 0, 0);
@ -491,6 +493,7 @@ GrTransformedMaskSubRun::GrTransformedMaskSubRun(GrMaskFormat format,
GrSubRun* GrTransformedMaskSubRun::Make(const SkZip<SkGlyphVariant, SkPoint>& drawables,
const SkStrikeSpec& strikeSpec,
GrMaskFormat format,
SkPoint residual,
GrTextBlob* blob,
SkArenaAlloc* alloc) {
size_t vertexCount = drawables.size();
@ -1059,7 +1062,8 @@ template<typename AddSingleMaskFormat>
void GrTextBlob::addMultiMaskFormat(
AddSingleMaskFormat addSingle,
const SkZip<SkGlyphVariant, SkPoint>& drawables,
const SkStrikeSpec& strikeSpec) {
const SkStrikeSpec& strikeSpec,
SkPoint residual) {
this->setHasBitmap();
if (drawables.empty()) { return; }
@ -1072,14 +1076,19 @@ void GrTextBlob::addMultiMaskFormat(
GrMaskFormat nextFormat = GrGlyph::FormatFromSkGlyph(glyph->maskFormat());
if (format != nextFormat) {
auto sameFormat = drawables.subspan(startIndex, i - startIndex);
GrSubRun* subRun = addSingle(sameFormat, strikeSpec, format, this, &fAlloc);
GrSubRun* subRun = addSingle(sameFormat, strikeSpec, format, residual, this, &fAlloc);
this->insertSubRun(subRun);
format = nextFormat;
startIndex = i;
}
}
auto sameFormat = drawables.last(drawables.size() - startIndex);
GrSubRun* subRun = addSingle(sameFormat, strikeSpec, format, this, &fAlloc);
GrSubRun* subRun = addSingle(sameFormat,
strikeSpec,
format,
residual,
this,
&fAlloc);
this->insertSubRun(subRun);
}
@ -1098,9 +1107,10 @@ void GrTextBlob::insertSubRun(GrSubRun* subRun) {
}
void GrTextBlob::processDeviceMasks(const SkZip<SkGlyphVariant, SkPoint>& drawables,
const SkStrikeSpec& strikeSpec) {
const SkStrikeSpec& strikeSpec,
SkPoint residual) {
this->addMultiMaskFormat(GrDirectMaskSubRun::Make, drawables, strikeSpec);
this->addMultiMaskFormat(GrDirectMaskSubRun::Make, drawables, strikeSpec, residual);
}
void GrTextBlob::processSourcePaths(const SkZip<SkGlyphVariant, SkPoint>& drawables,
@ -1127,5 +1137,7 @@ void GrTextBlob::processSourceSDFT(const SkZip<SkGlyphVariant, SkPoint>& drawabl
void GrTextBlob::processSourceMasks(const SkZip<SkGlyphVariant, SkPoint>& drawables,
const SkStrikeSpec& strikeSpec) {
this->addMultiMaskFormat(GrTransformedMaskSubRun::Make, drawables, strikeSpec);
// In this case the residual is {0, 0} because it is not used to calculate the positions of
// transformed mask. Any value would do.
this->addMultiMaskFormat(GrTransformedMaskSubRun::Make, drawables, strikeSpec, {0, 0});
}

View File

@ -113,7 +113,8 @@ public:
void addMultiMaskFormat(
AddSingleMaskFormat addSingle,
const SkZip<SkGlyphVariant, SkPoint>& drawables,
const SkStrikeSpec& strikeSpec);
const SkStrikeSpec& strikeSpec,
SkPoint residual);
const SkTInternalLList<GrSubRun>& subRunList() const { return fSubRunList; }
@ -138,7 +139,8 @@ private:
// Methods to satisfy SkGlyphRunPainterInterface
void processDeviceMasks(const SkZip<SkGlyphVariant, SkPoint>& drawables,
const SkStrikeSpec& strikeSpec) override;
const SkStrikeSpec& strikeSpec,
SkPoint residual) override;
void processSourcePaths(const SkZip<SkGlyphVariant, SkPoint>& drawables,
const SkFont& runFont,
const SkStrikeSpec& strikeSpec) override;
@ -314,9 +316,10 @@ private:
// -- GrDirectMaskSubRun ---------------------------------------------------------------------------
class GrDirectMaskSubRun final : public GrAtlasSubRun {
public:
using VertexData = SkPoint; // The left top corner of the glyph bounding box.
using VertexData = SkIPoint;
GrDirectMaskSubRun(GrMaskFormat format,
SkPoint residual,
GrTextBlob* blob,
const SkRect& bounds,
SkSpan<const VertexData> vertexData,
@ -325,6 +328,7 @@ public:
static GrSubRun* Make(const SkZip<SkGlyphVariant, SkPoint>& drawables,
const SkStrikeSpec& strikeSpec,
GrMaskFormat format,
SkPoint residual,
GrTextBlob* blob,
SkArenaAlloc* alloc);
@ -350,10 +354,12 @@ public:
const SkMatrix& drawMatrix, SkPoint drawOrigin,
SkIRect clip) const override;
private:
// The rectangle that surrounds all the glyph bounding boxes in device space.
SkRect deviceRect(const SkMatrix& drawMatrix, SkPoint drawOrigin) const;
const GrMaskFormat fMaskFormat;
const SkPoint fResidual;
GrTextBlob* const fBlob;
// The vertex bounds in device space. The bounds are the joined rectangles of all the glyphs.
const SkRect fVertexBounds;
@ -384,6 +390,7 @@ public:
static GrSubRun* Make(const SkZip<SkGlyphVariant, SkPoint>& drawables,
const SkStrikeSpec& strikeSpec,
GrMaskFormat format,
SkPoint residual,
GrTextBlob* blob,
SkArenaAlloc* alloc);