handle perspective glyph drawing correctly

For GPU:
Handling a blob that moved around in perspective reused cached
information incorrectly. Always recalculate the sub run
information when drawing perspective.

For Bitmap:
Make color emoji draw correctly.

Testing:
Add a GM that draws a single text blob in multiple different
perspective orientations.

Bug: skia:10473
Bug: skia:8914

Change-Id: I945e3326804ec47bf8cbca0e3cf4a17afc9ba5f0
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/400598
Reviewed-by: Ben Wagner <bungeman@google.com>
Commit-Queue: Herb Derby <herb@google.com>
This commit is contained in:
Herb Derby 2021-04-23 16:21:58 -04:00 committed by Skia Commit-Bot
parent e4b8c3aa73
commit 47c88cc9d6
7 changed files with 163 additions and 7 deletions

View File

@ -79,7 +79,6 @@ protected:
canvas->drawSimpleText(text, strlen(text), SkTextEncoding::kUTF8, 10, y, font, paint);
y += metrics.fDescent + metrics.fLeading;
}
}
private:
@ -138,7 +137,72 @@ protected:
y += metrics.fDescent + metrics.fLeading;
}
}
private:
using INHERITED = GM;
};
class ScaledEmojiPerspectiveGM : public GM {
public:
ScaledEmojiPerspectiveGM() {}
protected:
struct EmojiFont {
sk_sp<SkTypeface> fTypeface;
const char* fText;
} fEmojiFont;
void onOnceBeforeDraw() override {
fEmojiFont.fTypeface = ToolUtils::emoji_typeface();
fEmojiFont.fText = ToolUtils::emoji_sample_text();
}
SkString onShortName() override {
return SkString("scaledemojiperspective");
}
SkISize onISize() override { return SkISize::Make(1200, 1200); }
void onDraw(SkCanvas* canvas) override {
canvas->drawColor(SK_ColorGRAY);
SkMatrix taper;
taper.setPerspY(-0.0025f);
SkPaint paint;
SkFont font;
font.setTypeface(fEmojiFont.fTypeface);
font.setSize(40);
const char* text = "\xF0\x9F\x98\x80"
"\xE2\x99\xA2"; // 😀♢;
sk_sp<SkTextBlob> blob = make_hpos_test_blob_utf8(text, font);
// draw text at different point sizes
// Testing GPU bitmap path, SDF path with no scaling,
// SDF path with scaling, path rendering with scaling
SkFontMetrics metrics;
font.getMetrics(&metrics);
for (auto rotate : {0.0, 45.0, 90.0, 135.0, 180.0, 225.0, 270.0, 315.0}) {
canvas->save();
SkMatrix perspective;
perspective.postTranslate(-600, -600);
perspective.postConcat(taper);
perspective.postRotate(rotate);
perspective.postTranslate(600, 600);
canvas->concat(perspective);
SkScalar y = 670;
for (int i = 0; i < 5; i++) {
y += -metrics.fAscent;
// Draw with an origin.
canvas->drawTextBlob(blob, 565, y, paint);
y += metrics.fDescent + metrics.fLeading;
}
canvas->restore();
}
}
private:
@ -149,5 +213,5 @@ private:
DEF_GM(return new ScaledEmojiGM;)
DEF_GM(return new ScaledEmojiPosGM;)
DEF_GM(return new ScaledEmojiPerspectiveGM;)
} // namespace skiagm

View File

@ -59,7 +59,7 @@ public:
/* If dstOrNull is null, computes a dst by mapping the bitmap's bounds through the matrix. */
void drawBitmap(const SkBitmap&, const SkMatrix&, const SkRect* dstOrNull,
const SkSamplingOptions&, const SkPaint&) const;
const SkSamplingOptions&, const SkPaint&) const override;
void drawSprite(const SkBitmap&, int x, int y, const SkPaint&) const;
void drawGlyphRunList(const SkGlyphRunList& glyphRunList,
const SkPaint& paint,

View File

@ -118,7 +118,7 @@ void SkGlyphRunListPainter::drawForBitmapDevice(
bitmapDevice->paintPaths(
&fDrawable, strikeSpec.strikeToSourceRatio(), drawOrigin, pathPaint);
}
if (!fRejects.source().empty()) {
if (!fRejects.source().empty() && !deviceMatrix.hasPerspective()) {
SkStrikeSpec strikeSpec = SkStrikeSpec::MakeMask(
runFont, paint, props, fScalerContextFlags, deviceMatrix);
@ -127,8 +127,93 @@ void SkGlyphRunListPainter::drawForBitmapDevice(
fDrawable.startBitmapDevice(
fRejects.source(), drawOrigin, deviceMatrix, strike->roundingSpec());
strike->prepareForDrawingMasksCPU(&fDrawable);
fRejects.flipRejectsToSource();
bitmapDevice->paintMasks(&fDrawable, paint);
}
if (!fRejects.source().empty()) {
SkMatrix runMatrix = deviceMatrix;
runMatrix.preTranslate(drawOrigin.x(), drawOrigin.y());
std::vector<SkPoint> sourcePositions;
// Create a strike is source space to calculate scale information.
SkStrikeSpec scaleStrikeSpec = SkStrikeSpec::MakeMask(
runFont, paint, props, fScalerContextFlags, SkMatrix::I());
SkBulkGlyphMetrics metrics{scaleStrikeSpec};
auto glyphIDs = fRejects.source().get<0>();
auto positions = fRejects.source().get<1>();
SkSpan<const SkGlyph*> glyphs = metrics.glyphs(glyphIDs);
SkScalar maxScale = SK_ScalarMin;
// Calculate the scale that makes the longest edge 1:1 with its side in the cache.
for (auto [glyph, pos] : SkMakeZip(glyphs, positions)) {
SkPoint corners[4];
SkPoint srcPos = pos + drawOrigin;
// Store off the positions in device space to position the glyphs during drawing.
sourcePositions.push_back(srcPos);
SkRect rect = glyph->rect();
rect.makeOffset(srcPos);
runMatrix.mapRectToQuad(corners, rect);
// left top -> right top
SkScalar scale = (corners[1] - corners[0]).length() / rect.width();
maxScale = std::max(maxScale, scale);
// right top -> right bottom
scale = (corners[2] - corners[1]).length() / rect.height();
maxScale = std::max(maxScale, scale);
// right bottom -> left bottom
scale = (corners[3] - corners[2]).length() / rect.width();
maxScale = std::max(maxScale, scale);
// left bottom -> left top
scale = (corners[0] - corners[3]).length() / rect.height();
maxScale = std::max(maxScale, scale);
}
if (maxScale * runFont.getSize() > 256) {
maxScale = 256.0f / runFont.getSize();
}
SkMatrix cacheScale = SkMatrix::Scale(maxScale, maxScale);
SkStrikeSpec strikeSpec = SkStrikeSpec::MakeMask(
runFont, paint, props, fScalerContextFlags, cacheScale);
auto strike = strikeSpec.findOrCreateStrike();
// Figure out all the positions and packed glyphIDs based on the device matrix.
fDrawable.startBitmapDevice(
fRejects.source(), drawOrigin, deviceMatrix, strike->roundingSpec());
strike->prepareForDrawingMasksCPU(&fDrawable);
auto variants = fDrawable.drawable().get<0>();
for (auto [variant, srcPos] : SkMakeZip(variants, sourcePositions)) {
SkGlyph* glyph = variant.glyph();
SkMask mask = glyph->mask();
// TODO: is this needed will A8 and BW just work?
if (mask.fFormat != SkMask::kARGB32_Format) {
continue;
}
SkBitmap bm;
bm.installPixels(SkImageInfo::MakeN32Premul(mask.fBounds.size()),
mask.fImage,
mask.fRowBytes);
// Since the glyph in the cache is scaled by maxScale, its top left vector is too
// long. Reduce it to find proper positions on the device.
SkPoint realPos = srcPos
+ SkPoint::Make(mask.fBounds.left(), mask.fBounds.top()) * (1.0f/maxScale);
// Calculate the preConcat matrix for drawBitmap to get the rectangle from the
// glyph cache (which is multiplied by maxScale) to land in the right place.
SkMatrix translate = SkMatrix::Translate(realPos);
translate.preScale(1.0f/maxScale, 1.0f/maxScale);
// Draw the bitmap using the rect from the scaled cache, and not the source
// rectangle for the glyph.
bitmapDevice->drawBitmap(
bm, translate, nullptr, SkSamplingOptions{SkFilterMode::kLinear},
paint);
}
fRejects.flipRejectsToSource();
}
// TODO: have the mask stage above reject the glyphs that are too big, and handle the
// rejects in a more sophisticated stage.

View File

@ -74,6 +74,8 @@ public:
const SkPaint& paint) const = 0;
virtual void paintMasks(SkDrawableGlyphBuffer* drawables, const SkPaint& paint) const = 0;
virtual void drawBitmap(const SkBitmap&, const SkMatrix&, const SkRect* dstOrNull,
const SkSamplingOptions&, const SkPaint&) const = 0;
};
void drawForBitmapDevice(

View File

@ -61,6 +61,9 @@ public:
}
}
void drawBitmap(const SkBitmap&, const SkMatrix&, const SkRect* dstOrNull,
const SkSamplingOptions&, const SkPaint&) const override {}
void onDrawGlyphRunList(const SkGlyphRunList& glyphRunList, const SkPaint& paint) override {
SkASSERT(!glyphRunList.hasRSXForm());
fPainter.drawForBitmapDevice(glyphRunList, paint, fOverdrawCanvas->getTotalMatrix(), this);

View File

@ -395,7 +395,6 @@ void GrSurfaceDrawContext::drawGlyphRunListWithCache(const GrClip* clip,
if (blob == nullptr || !blob->canReuse(paint, drawMatrix)) {
if (blob != nullptr) {
SkASSERT(!drawMatrix.hasPerspective());
// We have to remake the blob because changes may invalidate our masks.
// TODO we could probably get away with reuse most of the time if the pointer is unique,
// but we'd have to clear the SubRun information

View File

@ -1528,8 +1528,11 @@ bool GrTextBlob::hasPerspective() const { return fInitialMatrix.hasPerspective()
bool GrTextBlob::canReuse(const SkPaint& paint, const SkMatrix& drawMatrix) const {
// A singular matrix will create a GrTextBlob with no SubRuns, but unknown glyphs can
// also cause empty runs. If there are no subRuns, then regenerate.
if ((fSubRunList.isEmpty() || fSomeGlyphsExcluded) && fInitialMatrix != drawMatrix) {
// also cause empty runs. If there are no subRuns or some glyphs were excluded or perspective,
// then regenerate when the matrices don't match.
if ((fSubRunList.isEmpty() || fSomeGlyphsExcluded || hasPerspective()) &&
fInitialMatrix != drawMatrix)
{
return false;
}