https://skia-review.googlesource.com/c/skia/+/241038
From 2ada5eca6454cdeae8d7dbb05a12844e9268bf86 Mon Sep 17 00:00:00 2001
From: Herb Derby <herb@google.com>
Date: Thu, 12 Sep 2019 10:58:26 -0400
Subject: [PATCH] A fix and unit test for jagged characters
Test 1 - half pixel rendering
Adjust the positions of the glyph quads so they never land on a 1/2
pixel position. Having a 1/2 pixel position causes some drivers to
point sample the glyphs poorly because the round differently for
different pixels.
Test 2 - smooth scrolling
Move a textblob very slowly in y to make sure in matches a freshly
constructed textblob.
Fix:
Regen the GrTextBlob if the translation is not by whole pixels.
Bug:
https://buganizer.corp.google.com/issues/139472690
Change-Id: I161bae18693c93c5492c798b288939d0f7e33730
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/241038
Commit-Queue: Ben Wagner <bungeman@google.com>
Reviewed-by: Jim Van Verth <jvanverth@google.com>
Reviewed-by: Ben Wagner <bungeman@google.com>
---
diff --git a/src/gpu/text/GrTextBlob.cpp b/src/gpu/text/GrTextBlob.cpp
index 0ab8f6d..0c52feb 100644
--- a/src/gpu/text/GrTextBlob.cpp
+++ b/src/gpu/text/GrTextBlob.cpp
@@ -127,24 +127,25 @@
return true;
}
- // If the text blob only has full pixel glyphs, then fractional part of the position does
- // not affect the SkGlyphs used.
- if (anyRunHasSubpixelPosition) {
- // We can update the positions in the text blob without regenerating the whole
- // blob, but only for integer translations.
- // This cool bit of math will determine the necessary translation to apply to the
- // already generated vertex coordinates to move them to the correct position.
- SkScalar transX = viewMatrix.getTranslateX() +
- viewMatrix.getScaleX() * (x - fInitialX) +
- viewMatrix.getSkewX() * (y - fInitialY) -
- fInitialViewMatrix.getTranslateX();
- SkScalar transY = viewMatrix.getTranslateY() +
- viewMatrix.getSkewY() * (x - fInitialX) +
- viewMatrix.getScaleY() * (y - fInitialY) -
- fInitialViewMatrix.getTranslateY();
- if (!SkScalarIsInt(transX) || !SkScalarIsInt(transY)) {
- return true;
- }
+ // TODO(herb): this is not needed for full pixel glyph choice, but is needed to adjust
+ // the quads properly. Devise a system that regenerates the quads from original data
+ // using the transform to allow this to be used in general.
+
+ // We can update the positions in the text blob without regenerating the whole
+ // blob, but only for integer translations.
+ // This cool bit of math will determine the necessary translation to apply to the
+ // already generated vertex coordinates to move them to the correct position.
+ // Figure out the translation in view space given a translation in source space.
+ SkScalar transX = viewMatrix.getTranslateX() +
+ viewMatrix.getScaleX() * (x - fInitialX) +
+ viewMatrix.getSkewX() * (y - fInitialY) -
+ fInitialViewMatrix.getTranslateX();
+ SkScalar transY = viewMatrix.getTranslateY() +
+ viewMatrix.getSkewY() * (x - fInitialX) +
+ viewMatrix.getScaleY() * (y - fInitialY) -
+ fInitialViewMatrix.getTranslateY();
+ if (!SkScalarIsInt(transX) || !SkScalarIsInt(transY)) {
+ return true;
}
} else if (this->hasDistanceField()) {
// A scale outside of [blob.fMaxMinScale, blob.fMinMaxScale] would result in a different
diff --git a/tests/TextBlobCacheTest.cpp b/tests/TextBlobCacheTest.cpp
index 5523a9d..6cae4c2 100644
--- a/tests/TextBlobCacheTest.cpp
+++ b/tests/TextBlobCacheTest.cpp
@@ -7,6 +7,8 @@
#include "sk_tool_utils.h"
+#include <string>
+
#include "SkCanvas.h"
#include "SkFontMgr.h"
#include "SkGlyphRun.h"
@@ -170,3 +172,155 @@
DEF_GPUTEST_FOR_NULLGL_CONTEXT(TextBlobStressAbnormal, reporter, ctxInfo) {
text_blob_cache_inner(reporter, ctxInfo.grContext(), 256, 256, 10, false, true);
}
+
+static const int kScreenDim = 160;
+
+static SkBitmap draw_blob(SkTextBlob* blob, SkSurface* surface, SkPoint offset) {
+
+ SkPaint paint;
+
+ SkCanvas* canvas = surface->getCanvas();
+ canvas->save();
+ canvas->drawColor(SK_ColorWHITE, SkBlendMode::kSrc);
+ canvas->translate(offset.fX, offset.fY);
+ canvas->drawTextBlob(blob, 0, 0, paint);
+ SkBitmap bitmap;
+ bitmap.allocN32Pixels(kScreenDim, kScreenDim);
+ surface->readPixels(bitmap, 0, 0);
+ canvas->restore();
+ return bitmap;
+}
+
+static bool compare_bitmaps(const SkBitmap& expected, const SkBitmap& actual) {
+ SkASSERT(expected.width() == actual.width());
+ SkASSERT(expected.height() == actual.height());
+ for (int i = 0; i < expected.width(); ++i) {
+ for (int j = 0; j < expected.height(); ++j) {
+ SkColor expectedColor = expected.getColor(i, j);
+ SkColor actualColor = actual.getColor(i, j);
+ if (expectedColor != actualColor) {
+ return false;
+ }
+ }
+ }
+ return true;
+}
+
+static sk_sp<SkTextBlob> make_blob() {
+ auto tf = SkTypeface::MakeFromName("Roboto2-Regular", SkFontStyle());
+ SkFont font;
+ font.setTypeface(tf);
+ font.setSubpixel(false);
+ font.setEdging(SkFont::Edging::kAlias);
+ font.setSize(24);
+
+ static char text[] = "HekpqB";
+ static const int maxGlyphLen = sizeof(text) * 4;
+ SkGlyphID glyphs[maxGlyphLen];
+ int glyphCount =
+ tf->charsToGlyphs(text, SkTypeface::Encoding::kUTF8_Encoding, glyphs, sizeof(text)-1);
+
+ SkTextBlobBuilder builder;
+ const auto& runBuffer = builder.allocRun(font, glyphCount, 0, 0);
+ for (int i = 0; i < glyphCount; i++) {
+ runBuffer.glyphs[i] = glyphs[i];
+ }
+ return builder.make();
+}
+
+static const bool kDumpPngs = false;
+// dump pngs needs a "good" and a "bad" directory to put the results in. This allows the use of the
+// skdiff tool to visualize the differences.
+
+void write_png(const std::string& filename, const SkBitmap& bitmap) {
+ auto data = SkEncodeBitmap(bitmap, SkEncodedImageFormat::kPNG, 0);
+ SkFILEWStream w{filename.c_str()};
+ w.write(data->data(), data->size());
+ w.fsync();
+}
+
+DEF_GPUTEST_FOR_RENDERING_CONTEXTS(TextBlobJaggedGlyph, reporter, ctxInfo) {
+ auto grContext = ctxInfo.grContext();
+ const SkImageInfo info =
+ SkImageInfo::Make(kScreenDim, kScreenDim, kN32_SkColorType, kPremul_SkAlphaType);
+ auto surface = SkSurface::MakeRenderTarget(grContext, SkBudgeted::kNo, info);
+
+ auto blob = make_blob();
+
+ for (int y = 40; y < kScreenDim - 40; y++) {
+ SkBitmap base = draw_blob(blob.get(), surface.get(), {40, y + 0.0f});
+ SkBitmap half = draw_blob(blob.get(), surface.get(), {40, y + 0.5f});
+ SkBitmap unit = draw_blob(blob.get(), surface.get(), {40, y + 1.0f});
+ bool isOk = compare_bitmaps(base, half) || compare_bitmaps(unit, half);
+ REPORTER_ASSERT(reporter, isOk);
+ if (!isOk) {
+ if (kDumpPngs) {
+ {
+ std::string filename = "bad/half-y" + std::to_string(y) + ".png";
+ write_png(filename, half);
+ }
+ {
+ std::string filename = "good/half-y" + std::to_string(y) + ".png";
+ write_png(filename, base);
+ }
+ }
+ break;
+ }
+ }
+
+ // This is not as useful as Y because advances are not integer, which causes different letter
+ // spacing.
+#if 0
+ blob = make_blob();
+ for (int x = 40; x < kScreenDim - 40; x++) {
+ SkBitmap base = draw_blob(blob.get(), surface.get(), {x + 0.0f, 40});
+ SkBitmap half = draw_blob(blob.get(), surface.get(), {x + 0.5f, 40});
+ SkBitmap unit = draw_blob(blob.get(), surface.get(), {x + 1.0f, 40});
+ bool isOk = compare_bitmaps(base, half) || compare_bitmaps(unit, half);
+ REPORTER_ASSERT(reporter, isOk);
+ if (!isOk) {
+ if (kDumpPngs) {
+ {
+ std::string filename = "bad/half-x" + std::to_string(x) + ".png";
+ write_png(filename, half);
+ }
+ {
+ std::string filename = "good/half-x" + std::to_string(x) + ".png";
+ write_png(filename, base);
+ }
+ }
+ break;
+ }
+ }
+#endif
+}
+
+DEF_GPUTEST_FOR_RENDERING_CONTEXTS(TextBlobSmoothScroll, reporter, ctxInfo) {
+ auto grContext = ctxInfo.grContext();
+ const SkImageInfo info =
+ SkImageInfo::Make(kScreenDim, kScreenDim, kN32_SkColorType, kPremul_SkAlphaType);
+ auto surface = SkSurface::MakeRenderTarget(grContext, SkBudgeted::kNo, info);
+
+ auto movingBlob = make_blob();
+
+ for (SkScalar y = 40; y < 50; y += 1.0/8.0) {
+ auto expectedBlob = make_blob();
+ auto expectedBitMap = draw_blob(expectedBlob.get(), surface.get(), {40, y});
+ auto movingBitmap = draw_blob(movingBlob.get(), surface.get(), {40, y});
+ bool isOk = compare_bitmaps(expectedBitMap, movingBitmap);
+ REPORTER_ASSERT(reporter, isOk);
+ if (!isOk) {
+ if (kDumpPngs) {
+ {
+ std::string filename = "bad/scroll-y" + std::to_string(y) + ".png";
+ write_png(filename, movingBitmap);
+ }
+ {
+ std::string filename = "good/scroll-y" + std::to_string(y) + ".png";
+ write_png(filename, expectedBitMap);
+ }
+ }
+ break;
+ }
+ }
+}