1/*
2 * Copyright 2015 Google Inc.
3 *
4 * Use of this source code is governed by a BSD-style license that can be
5 * found in the LICENSE file.
6 */
7
8#include "tools/ToolUtils.h"
9
10#include <string>
11
12#include "include/core/SkBitmap.h"
13#include "include/core/SkCanvas.h"
14#include "include/core/SkFontMgr.h"
15#include "include/core/SkGraphics.h"
16#include "include/core/SkPaint.h"
17#include "include/core/SkPoint.h"
18#include "include/core/SkSurface.h"
19#include "include/core/SkTextBlob.h"
20#include "include/core/SkTypeface.h"
21#include "include/gpu/GrDirectContext.h"
22#include "src/core/SkGlyphRun.h"
23#include "src/gpu/GrDirectContextPriv.h"
24#include "tools/fonts/RandomScalerContext.h"
25
26#ifdef SK_BUILD_FOR_WIN
27    #include "include/ports/SkTypeface_win.h"
28#endif
29
30#include "tests/Test.h"
31
32#include "src/gpu/GrDirectContextPriv.h"
33#include "src/gpu/text/GrAtlasManager.h"
34#include "src/gpu/text/GrTextBlobCache.h"
35
36static void draw(SkCanvas* canvas, int redraw, const SkTArray<sk_sp<SkTextBlob>>& blobs) {
37    int yOffset = 0;
38    for (int r = 0; r < redraw; r++) {
39        for (int i = 0; i < blobs.count(); i++) {
40            const auto& blob = blobs[i];
41            const SkRect& bounds = blob->bounds();
42            yOffset += SkScalarCeilToInt(bounds.height());
43            SkPaint paint;
44            canvas->drawTextBlob(blob, 0, SkIntToScalar(yOffset), paint);
45        }
46    }
47}
48
49static const int kWidth = 1024;
50static const int kHeight = 768;
51
52static void setup_always_evict_atlas(GrDirectContext* dContext) {
53    dContext->priv().getAtlasManager()->setAtlasDimensionsToMinimum_ForTesting();
54}
55
56class GrTextBlobTestingPeer {
57public:
58    static void SetBudget(GrTextBlobCache* cache, size_t budget) {
59        SkAutoSpinlock lock{cache->fSpinLock};
60        cache->fSizeBudget = budget;
61        cache->internalCheckPurge();
62    }
63};
64
65// This test hammers the GPU textblobcache and font atlas
66static void text_blob_cache_inner(skiatest::Reporter* reporter, GrDirectContext* dContext,
67                                  int maxTotalText, int maxGlyphID, int maxFamilies, bool normal,
68                                  bool stressTest) {
69    // setup surface
70    uint32_t flags = 0;
71    SkSurfaceProps props(flags, kRGB_H_SkPixelGeometry);
72
73    // configure our context for maximum stressing of cache and atlas
74    if (stressTest) {
75        setup_always_evict_atlas(dContext);
76        GrTextBlobTestingPeer::SetBudget(dContext->priv().getTextBlobCache(), 0);
77    }
78
79    SkImageInfo info = SkImageInfo::Make(kWidth, kHeight, kRGBA_8888_SkColorType,
80                                         kPremul_SkAlphaType);
81    auto surface(SkSurface::MakeRenderTarget(dContext, SkBudgeted::kNo, info, 0, &props));
82    REPORTER_ASSERT(reporter, surface);
83    if (!surface) {
84        return;
85    }
86
87    SkCanvas* canvas = surface->getCanvas();
88
89    sk_sp<SkFontMgr> fm(SkFontMgr::RefDefault());
90
91    int count = std::min(fm->countFamilies(), maxFamilies);
92
93    // make a ton of text
94    SkAutoTArray<uint16_t> text(maxTotalText);
95    for (int i = 0; i < maxTotalText; i++) {
96        text[i] = i % maxGlyphID;
97    }
98
99    // generate textblobs
100    SkTArray<sk_sp<SkTextBlob>> blobs;
101    for (int i = 0; i < count; i++) {
102        SkFont font;
103        font.setSize(48); // draw big glyphs to really stress the atlas
104
105        SkString familyName;
106        fm->getFamilyName(i, &familyName);
107        sk_sp<SkFontStyleSet> set(fm->createStyleSet(i));
108        for (int j = 0; j < set->count(); ++j) {
109            SkFontStyle fs;
110            set->getStyle(j, &fs, nullptr);
111
112            // We use a typeface which randomy returns unexpected mask formats to fuzz
113            sk_sp<SkTypeface> orig(set->createTypeface(j));
114            if (normal) {
115                font.setTypeface(orig);
116            } else {
117                font.setTypeface(sk_make_sp<SkRandomTypeface>(orig, SkPaint(), true));
118            }
119
120            SkTextBlobBuilder builder;
121            for (int aa = 0; aa < 2; aa++) {
122                for (int subpixel = 0; subpixel < 2; subpixel++) {
123                    for (int lcd = 0; lcd < 2; lcd++) {
124                        font.setEdging(SkFont::Edging::kAlias);
125                        if (aa) {
126                            font.setEdging(SkFont::Edging::kAntiAlias);
127                            if (lcd) {
128                                font.setEdging(SkFont::Edging::kSubpixelAntiAlias);
129                            }
130                        }
131                        font.setSubpixel(SkToBool(subpixel));
132                        if (!SkToBool(lcd)) {
133                            font.setSize(160);
134                        }
135                        const SkTextBlobBuilder::RunBuffer& run = builder.allocRun(font,
136                                                                                   maxTotalText,
137                                                                                   0, 0,
138                                                                                   nullptr);
139                        memcpy(run.glyphs, text.get(), maxTotalText * sizeof(uint16_t));
140                    }
141                }
142            }
143            blobs.emplace_back(builder.make());
144        }
145    }
146
147    // create surface where LCD is impossible
148    info = SkImageInfo::Make(kWidth, kHeight, kRGBA_8888_SkColorType, kPremul_SkAlphaType);
149    SkSurfaceProps propsNoLCD(0, kUnknown_SkPixelGeometry);
150    auto surfaceNoLCD(canvas->makeSurface(info, &propsNoLCD));
151    REPORTER_ASSERT(reporter, surface);
152    if (!surface) {
153        return;
154    }
155
156    SkCanvas* canvasNoLCD = surfaceNoLCD->getCanvas();
157
158    // test redraw
159    draw(canvas, 2, blobs);
160    draw(canvasNoLCD, 2, blobs);
161
162    // test draw after free
163    dContext->freeGpuResources();
164    draw(canvas, 1, blobs);
165
166    dContext->freeGpuResources();
167    draw(canvasNoLCD, 1, blobs);
168
169    // test draw after abandon
170    dContext->abandonContext();
171    draw(canvas, 1, blobs);
172}
173
174#ifdef SK_ENABLE_SMALL_PAGE
175DEF_GPUTEST_FOR_MOCK_CONTEXT(TextBlobCache, reporter, ctxInfo) {
176    text_blob_cache_inner(reporter, ctxInfo.directContext(), 1024, 256, 30, true, true);
177}
178#else
179DEF_GPUTEST_FOR_MOCK_CONTEXT(TextBlobCache, reporter, ctxInfo) {
180    text_blob_cache_inner(reporter, ctxInfo.directContext(), 1024, 256, 30, true, false);
181}
182#endif
183
184DEF_GPUTEST_FOR_MOCK_CONTEXT(TextBlobStressCache, reporter, ctxInfo) {
185    text_blob_cache_inner(reporter, ctxInfo.directContext(), 256, 256, 10, true, true);
186}
187
188#ifdef SK_ENABLE_SMALL_PAGE
189DEF_GPUTEST_FOR_MOCK_CONTEXT(TextBlobAbnormal, reporter, ctxInfo) {
190    text_blob_cache_inner(reporter, ctxInfo.directContext(), 256, 256, 10, false, true);
191}
192#else
193DEF_GPUTEST_FOR_MOCK_CONTEXT(TextBlobAbnormal, reporter, ctxInfo) {
194    text_blob_cache_inner(reporter, ctxInfo.directContext(), 256, 256, 10, false, false);
195}
196#endif
197
198DEF_GPUTEST_FOR_MOCK_CONTEXT(TextBlobStressAbnormal, reporter, ctxInfo) {
199    text_blob_cache_inner(reporter, ctxInfo.directContext(), 256, 256, 10, false, true);
200}
201
202static const int kScreenDim = 160;
203
204static SkBitmap draw_blob(SkTextBlob* blob, SkSurface* surface, SkPoint offset) {
205
206    SkPaint paint;
207
208    SkCanvas* canvas = surface->getCanvas();
209    canvas->save();
210    canvas->drawColor(SK_ColorWHITE, SkBlendMode::kSrc);
211    canvas->translate(offset.fX, offset.fY);
212    canvas->drawTextBlob(blob, 0, 0, paint);
213    SkBitmap bitmap;
214    bitmap.allocN32Pixels(kScreenDim, kScreenDim);
215    surface->readPixels(bitmap, 0, 0);
216    canvas->restore();
217    return bitmap;
218}
219
220static bool compare_bitmaps(const SkBitmap& expected, const SkBitmap& actual) {
221    SkASSERT(expected.width() == actual.width());
222    SkASSERT(expected.height() == actual.height());
223    for (int i = 0; i < expected.width(); ++i) {
224        for (int j = 0; j < expected.height(); ++j) {
225            SkColor expectedColor = expected.getColor(i, j);
226            SkColor actualColor = actual.getColor(i, j);
227            if (expectedColor != actualColor) {
228                return false;
229            }
230        }
231    }
232    return true;
233}
234
235static sk_sp<SkTextBlob> make_blob() {
236    auto tf = SkTypeface::MakeFromName("Roboto2-Regular", SkFontStyle());
237    SkFont font;
238    font.setTypeface(tf);
239    font.setSubpixel(false);
240    font.setEdging(SkFont::Edging::kAlias);
241    font.setSize(24);
242
243    static char text[] = "HekpqB";
244    static const int maxGlyphLen = sizeof(text) * 4;
245    SkGlyphID glyphs[maxGlyphLen];
246    int glyphCount =
247            font.textToGlyphs(text, sizeof(text), SkTextEncoding::kUTF8, glyphs, maxGlyphLen);
248
249    SkTextBlobBuilder builder;
250    const auto& runBuffer = builder.allocRun(font, glyphCount, 0, 0);
251    for (int i = 0; i < glyphCount; i++) {
252        runBuffer.glyphs[i] = glyphs[i];
253    }
254    return builder.make();
255}
256
257// Turned off to pass on android and ios devices, which were running out of memory..
258#if 0
259static sk_sp<SkTextBlob> make_large_blob() {
260    auto tf = SkTypeface::MakeFromName("Roboto2-Regular", SkFontStyle());
261    SkFont font;
262    font.setTypeface(tf);
263    font.setSubpixel(false);
264    font.setEdging(SkFont::Edging::kAlias);
265    font.setSize(24);
266
267    const int mallocSize = 0x3c3c3bd; // x86 size
268    std::unique_ptr<char[]> text{new char[mallocSize + 1]};
269    if (text == nullptr) {
270        return nullptr;
271    }
272    for (int i = 0; i < mallocSize; i++) {
273        text[i] = 'x';
274    }
275    text[mallocSize] = 0;
276
277    static const int maxGlyphLen = mallocSize;
278    std::unique_ptr<SkGlyphID[]> glyphs{new SkGlyphID[maxGlyphLen]};
279    int glyphCount =
280            font.textToGlyphs(
281                    text.get(), mallocSize, SkTextEncoding::kUTF8, glyphs.get(), maxGlyphLen);
282    SkTextBlobBuilder builder;
283    const auto& runBuffer = builder.allocRun(font, glyphCount, 0, 0);
284    for (int i = 0; i < glyphCount; i++) {
285        runBuffer.glyphs[i] = glyphs[i];
286    }
287    return builder.make();
288}
289
290DEF_GPUTEST_FOR_RENDERING_CONTEXTS(TextBlobIntegerOverflowTest, reporter, ctxInfo) {
291    auto dContext = ctxInfo.directContext();
292    const SkImageInfo info =
293            SkImageInfo::Make(kScreenDim, kScreenDim, kN32_SkColorType, kPremul_SkAlphaType);
294    auto surface = SkSurface::MakeRenderTarget(dContext, SkBudgeted::kNo, info);
295
296    auto blob = make_large_blob();
297    int y = 40;
298    SkBitmap base = draw_blob(blob.get(), surface.get(), {40, y + 0.0f});
299}
300#endif
301
302static const bool kDumpPngs = true;
303// dump pngs needs a "good" and a "bad" directory to put the results in. This allows the use of the
304// skdiff tool to visualize the differences.
305
306void write_png(const std::string& filename, const SkBitmap& bitmap) {
307    auto data = SkEncodeBitmap(bitmap, SkEncodedImageFormat::kPNG, 0);
308    SkFILEWStream w{filename.c_str()};
309    w.write(data->data(), data->size());
310    w.fsync();
311}
312
313DEF_GPUTEST_FOR_RENDERING_CONTEXTS(TextBlobJaggedGlyph, reporter, ctxInfo) {
314    auto direct = ctxInfo.directContext();
315    const SkImageInfo info =
316            SkImageInfo::Make(kScreenDim, kScreenDim, kN32_SkColorType, kPremul_SkAlphaType);
317    auto surface = SkSurface::MakeRenderTarget(direct, SkBudgeted::kNo, info);
318
319    auto blob = make_blob();
320
321    for (int y = 40; y < kScreenDim - 40; y++) {
322        SkBitmap base = draw_blob(blob.get(), surface.get(), {40, y + 0.0f});
323        SkBitmap half = draw_blob(blob.get(), surface.get(), {40, y + 0.5f});
324        SkBitmap unit = draw_blob(blob.get(), surface.get(), {40, y + 1.0f});
325        bool isOk = compare_bitmaps(base, half) || compare_bitmaps(unit, half);
326        REPORTER_ASSERT(reporter, isOk);
327        if (!isOk) {
328            if (kDumpPngs) {
329                {
330                    std::string filename = "bad/half-y" + std::to_string(y) + ".png";
331                    write_png(filename, half);
332                }
333                {
334                    std::string filename = "good/half-y" + std::to_string(y) + ".png";
335                    write_png(filename, base);
336                }
337            }
338            break;
339        }
340    }
341
342    // Testing the x direction across all platforms does not workout, because letter spacing can
343    // change based on non-integer advance widths, but this has been useful for diagnosing problems.
344#if 0
345    blob = make_blob();
346    for (int x = 40; x < kScreenDim - 40; x++) {
347        SkBitmap base = draw_blob(blob.get(), surface.get(), {x + 0.0f, 40});
348        SkBitmap half = draw_blob(blob.get(), surface.get(), {x + 0.5f, 40});
349        SkBitmap unit = draw_blob(blob.get(), surface.get(), {x + 1.0f, 40});
350        bool isOk = compare_bitmaps(base, half) || compare_bitmaps(unit, half);
351        REPORTER_ASSERT(reporter, isOk);
352        if (!isOk) {
353            if (kDumpPngs) {
354                {
355                    std::string filename = "bad/half-x" + std::to_string(x) + ".png";
356                    write_png(filename, half);
357                }
358                {
359                    std::string filename = "good/half-x" + std::to_string(x) + ".png";
360                    write_png(filename, base);
361                }
362            }
363            break;
364        }
365    }
366#endif
367}
368
369DEF_GPUTEST_FOR_RENDERING_CONTEXTS(TextBlobSmoothScroll, reporter, ctxInfo) {
370    auto direct = ctxInfo.directContext();
371    const SkImageInfo info =
372            SkImageInfo::Make(kScreenDim, kScreenDim, kN32_SkColorType, kPremul_SkAlphaType);
373    auto surface = SkSurface::MakeRenderTarget(direct, SkBudgeted::kNo, info);
374
375    auto movingBlob = make_blob();
376
377    for (SkScalar y = 40; y < 50; y += 1.0/8.0) {
378        auto expectedBlob = make_blob();
379        auto expectedBitMap = draw_blob(expectedBlob.get(), surface.get(), {40, y});
380        auto movingBitmap = draw_blob(movingBlob.get(), surface.get(), {40, y});
381        bool isOk = compare_bitmaps(expectedBitMap, movingBitmap);
382        REPORTER_ASSERT(reporter, isOk);
383        if (!isOk) {
384            if (kDumpPngs) {
385                {
386                    std::string filename = "bad/scroll-y" + std::to_string(y) + ".png";
387                    write_png(filename, movingBitmap);
388                }
389                {
390                    std::string filename = "good/scroll-y" + std::to_string(y) + ".png";
391                    write_png(filename, expectedBitMap);
392                }
393            }
394            break;
395        }
396    }
397}
398