diff options
author | joshualitt <joshualitt@chromium.org> | 2015-12-11 06:11:21 -0800 |
---|---|---|
committer | Commit bot <commit-bot@chromium.org> | 2015-12-11 06:11:21 -0800 |
commit | e804292e805917002cc3d7baa7f967fb20d2c7cb (patch) | |
tree | 5470bacbebdec462e03929dd07d1955249b31230 /src/gpu/text | |
parent | 296779832fd6175547d991ca67c735a824cadb66 (diff) |
Move all text stuff to its own folder
BUG=skia:
Review URL: https://codereview.chromium.org/1521453002
Diffstat (limited to 'src/gpu/text')
-rw-r--r-- | src/gpu/text/GrAtlasTextBlob.cpp | 500 | ||||
-rw-r--r-- | src/gpu/text/GrAtlasTextBlob.h | 388 | ||||
-rw-r--r-- | src/gpu/text/GrAtlasTextContext.cpp | 822 | ||||
-rw-r--r-- | src/gpu/text/GrAtlasTextContext.h | 131 | ||||
-rw-r--r-- | src/gpu/text/GrBatchFontCache.cpp | 236 | ||||
-rw-r--r-- | src/gpu/text/GrBatchFontCache.h | 228 | ||||
-rw-r--r-- | src/gpu/text/GrDistanceFieldAdjustTable.cpp | 93 | ||||
-rw-r--r-- | src/gpu/text/GrDistanceFieldAdjustTable.h | 31 | ||||
-rw-r--r-- | src/gpu/text/GrFontScaler.cpp | 207 | ||||
-rw-r--r-- | src/gpu/text/GrFontScaler.h | 69 | ||||
-rw-r--r-- | src/gpu/text/GrStencilAndCoverTextContext.cpp | 616 | ||||
-rw-r--r-- | src/gpu/text/GrStencilAndCoverTextContext.h | 145 | ||||
-rw-r--r-- | src/gpu/text/GrTextBlobCache.cpp | 58 | ||||
-rw-r--r-- | src/gpu/text/GrTextBlobCache.h | 158 | ||||
-rw-r--r-- | src/gpu/text/GrTextContext.cpp | 176 | ||||
-rw-r--r-- | src/gpu/text/GrTextContext.h | 76 | ||||
-rw-r--r-- | src/gpu/text/GrTextUtils.cpp | 200 | ||||
-rw-r--r-- | src/gpu/text/GrTextUtils.h | 70 |
18 files changed, 4204 insertions, 0 deletions
diff --git a/src/gpu/text/GrAtlasTextBlob.cpp b/src/gpu/text/GrAtlasTextBlob.cpp new file mode 100644 index 0000000000..dee6d133e3 --- /dev/null +++ b/src/gpu/text/GrAtlasTextBlob.cpp @@ -0,0 +1,500 @@ +/* + * Copyright 2015 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#include "GrAtlasTextBlob.h" + +#include "GrBlurUtils.h" +#include "GrContext.h" +#include "GrDrawContext.h" +#include "GrTextUtils.h" +#include "SkColorFilter.h" +#include "SkDrawFilter.h" +#include "SkTextBlobRunIterator.h" +#include "batches/GrAtlasTextBatch.h" + +void GrAtlasTextBlob::appendGlyph(int runIndex, + const SkRect& positions, + GrColor color, + GrBatchTextStrike* strike, + GrGlyph* glyph, + GrFontScaler* scaler, const SkGlyph& skGlyph, + SkScalar x, SkScalar y, SkScalar scale, bool applyVM) { + + // If the glyph is too large we fall back to paths + if (glyph->fTooLargeForAtlas) { + this->appendLargeGlyph(glyph, scaler, skGlyph, x, y, scale, applyVM); + return; + } + + Run& run = fRuns[runIndex]; + GrMaskFormat format = glyph->fMaskFormat; + + Run::SubRunInfo* subRun = &run.fSubRunInfo.back(); + if (run.fInitialized && subRun->maskFormat() != format) { + subRun = &run.push_back(); + subRun->setStrike(strike); + } else if (!run.fInitialized) { + subRun->setStrike(strike); + } + + run.fInitialized = true; + + size_t vertexStride = GetVertexStride(format); + + subRun->setMaskFormat(format); + + run.fVertexBounds.joinNonEmptyArg(positions); + subRun->setColor(color); + + intptr_t vertex = reinterpret_cast<intptr_t>(this->fVertices + subRun->vertexEndIndex()); + + if (kARGB_GrMaskFormat != glyph->fMaskFormat) { + // V0 + SkPoint* position = reinterpret_cast<SkPoint*>(vertex); + position->set(positions.fLeft, positions.fTop); + SkColor* colorPtr = reinterpret_cast<SkColor*>(vertex + sizeof(SkPoint)); + *colorPtr = color; + vertex += vertexStride; + + // V1 + position = reinterpret_cast<SkPoint*>(vertex); + position->set(positions.fLeft, positions.fBottom); + colorPtr = reinterpret_cast<SkColor*>(vertex + sizeof(SkPoint)); + *colorPtr = color; + vertex += vertexStride; + + // V2 + position = reinterpret_cast<SkPoint*>(vertex); + position->set(positions.fRight, positions.fBottom); + colorPtr = reinterpret_cast<SkColor*>(vertex + sizeof(SkPoint)); + *colorPtr = color; + vertex += vertexStride; + + // V3 + position = reinterpret_cast<SkPoint*>(vertex); + position->set(positions.fRight, positions.fTop); + colorPtr = reinterpret_cast<SkColor*>(vertex + sizeof(SkPoint)); + *colorPtr = color; + } else { + // V0 + SkPoint* position = reinterpret_cast<SkPoint*>(vertex); + position->set(positions.fLeft, positions.fTop); + vertex += vertexStride; + + // V1 + position = reinterpret_cast<SkPoint*>(vertex); + position->set(positions.fLeft, positions.fBottom); + vertex += vertexStride; + + // V2 + position = reinterpret_cast<SkPoint*>(vertex); + position->set(positions.fRight, positions.fBottom); + vertex += vertexStride; + + // V3 + position = reinterpret_cast<SkPoint*>(vertex); + position->set(positions.fRight, positions.fTop); + } + subRun->appendVertices(vertexStride); + fGlyphs[subRun->glyphEndIndex()] = glyph; + subRun->glyphAppended(); +} + +void GrAtlasTextBlob::appendLargeGlyph(GrGlyph* glyph, GrFontScaler* scaler, const SkGlyph& skGlyph, + SkScalar x, SkScalar y, SkScalar scale, bool applyVM) { + if (nullptr == glyph->fPath) { + const SkPath* glyphPath = scaler->getGlyphPath(skGlyph); + if (!glyphPath) { + return; + } + + glyph->fPath = new SkPath(*glyphPath); + } + fBigGlyphs.push_back(GrAtlasTextBlob::BigGlyph(*glyph->fPath, x, y, scale, applyVM)); +} + +bool GrAtlasTextBlob::mustRegenerate(SkScalar* outTransX, SkScalar* outTransY, + const SkPaint& paint, + GrColor color, const SkMaskFilter::BlurRec& blurRec, + const SkMatrix& viewMatrix, SkScalar x, SkScalar y) { + // If we have LCD text then our canonical color will be set to transparent, in this case we have + // to regenerate the blob on any color change + // We use the grPaint to get any color filter effects + if (fKey.fCanonicalColor == SK_ColorTRANSPARENT && + fPaintColor != color) { + return true; + } + + if (fViewMatrix.hasPerspective() != viewMatrix.hasPerspective()) { + return true; + } + + if (fViewMatrix.hasPerspective() && !fViewMatrix.cheapEqualTo(viewMatrix)) { + return true; + } + + // We only cache one masked version + if (fKey.fHasBlur && + (fBlurRec.fSigma != blurRec.fSigma || + fBlurRec.fStyle != blurRec.fStyle || + fBlurRec.fQuality != blurRec.fQuality)) { + return true; + } + + // Similarly, we only cache one version for each style + if (fKey.fStyle != SkPaint::kFill_Style && + (fStrokeInfo.fFrameWidth != paint.getStrokeWidth() || + fStrokeInfo.fMiterLimit != paint.getStrokeMiter() || + fStrokeInfo.fJoin != paint.getStrokeJoin())) { + return true; + } + + // Mixed blobs must be regenerated. We could probably figure out a way to do integer scrolls + // for mixed blobs if this becomes an issue. + if (this->hasBitmap() && this->hasDistanceField()) { + // Identical viewmatrices and we can reuse in all cases + if (fViewMatrix.cheapEqualTo(viewMatrix) && x == fX && y == fY) { + return false; + } + return true; + } + + if (this->hasBitmap()) { + if (fViewMatrix.getScaleX() != viewMatrix.getScaleX() || + fViewMatrix.getScaleY() != viewMatrix.getScaleY() || + fViewMatrix.getSkewX() != viewMatrix.getSkewX() || + fViewMatrix.getSkewY() != viewMatrix.getSkewY()) { + return true; + } + + // We can update the positions in the cachedtextblobs 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 - fX) + + viewMatrix.getSkewX() * (y - fY) - + fViewMatrix.getTranslateX(); + SkScalar transY = viewMatrix.getTranslateY() + + viewMatrix.getSkewY() * (x - fX) + + viewMatrix.getScaleY() * (y - fY) - + fViewMatrix.getTranslateY(); + if (!SkScalarIsInt(transX) || !SkScalarIsInt(transY) ) { + return true; + } + + (*outTransX) = transX; + (*outTransY) = transY; + } else if (this->hasDistanceField()) { + // A scale outside of [blob.fMaxMinScale, blob.fMinMaxScale] would result in a different + // distance field being generated, so we have to regenerate in those cases + SkScalar newMaxScale = viewMatrix.getMaxScale(); + SkScalar oldMaxScale = fViewMatrix.getMaxScale(); + SkScalar scaleAdjust = newMaxScale / oldMaxScale; + if (scaleAdjust < fMaxMinScale || scaleAdjust > fMinMaxScale) { + return true; + } + + (*outTransX) = x - fX; + (*outTransY) = y - fY; + } + + + // If we can reuse the blob, then make sure we update the blob's viewmatrix, and x/y + // offsets. Note, we offset the vertex bounds right before flushing + fViewMatrix = viewMatrix; + fX = x; + fY = y; + + // It is possible that a blob has neither distanceField nor bitmaptext. This is in the case + // when all of the runs inside the blob are drawn as paths. In this case, we always regenerate + // the blob anyways at flush time, so no need to regenerate explicitly + return false; +} + +GrDrawBatch* GrAtlasTextBlob::createBatch(const Run::SubRunInfo& info, + int glyphCount, int run, int subRun, + GrColor color, SkScalar transX, SkScalar transY, + const SkPaint& skPaint, const SkSurfaceProps& props, + const GrDistanceFieldAdjustTable* distanceAdjustTable, + GrBatchFontCache* cache) { + GrMaskFormat format = info.maskFormat(); + GrColor subRunColor; + if (kARGB_GrMaskFormat == format) { + uint8_t paintAlpha = skPaint.getAlpha(); + subRunColor = SkColorSetARGB(paintAlpha, paintAlpha, paintAlpha, paintAlpha); + } else { + subRunColor = color; + } + + GrAtlasTextBatch* batch; + if (info.drawAsDistanceFields()) { + SkColor filteredColor; + SkColorFilter* colorFilter = skPaint.getColorFilter(); + if (colorFilter) { + filteredColor = colorFilter->filterColor(skPaint.getColor()); + } else { + filteredColor = skPaint.getColor(); + } + bool useBGR = SkPixelGeometryIsBGR(props.pixelGeometry()); + batch = GrAtlasTextBatch::CreateDistanceField(glyphCount, cache, + distanceAdjustTable, filteredColor, + info.hasUseLCDText(), useBGR); + } else { + batch = GrAtlasTextBatch::CreateBitmap(format, glyphCount, cache); + } + GrAtlasTextBatch::Geometry& geometry = batch->geometry(); + geometry.fBlob = SkRef(this); + geometry.fRun = run; + geometry.fSubRun = subRun; + geometry.fColor = subRunColor; + geometry.fTransX = transX; + geometry.fTransY = transY; + batch->init(); + + return batch; +} + +inline +void GrAtlasTextBlob::flushRun(GrDrawContext* dc, GrPipelineBuilder* pipelineBuilder, + int run, GrColor color, + SkScalar transX, SkScalar transY, + const SkPaint& skPaint, const SkSurfaceProps& props, + const GrDistanceFieldAdjustTable* distanceAdjustTable, + GrBatchFontCache* cache) { + for (int subRun = 0; subRun < fRuns[run].fSubRunInfo.count(); subRun++) { + const Run::SubRunInfo& info = fRuns[run].fSubRunInfo[subRun]; + int glyphCount = info.glyphCount(); + if (0 == glyphCount) { + continue; + } + + SkAutoTUnref<GrDrawBatch> batch(this->createBatch(info, glyphCount, run, + subRun, color, transX, transY, + skPaint, props, + distanceAdjustTable, cache)); + dc->drawBatch(pipelineBuilder, batch); + } +} + +void GrAtlasTextBlob::flushBigGlyphs(GrContext* context, GrDrawContext* dc, + const GrClip& clip, const SkPaint& skPaint, + SkScalar transX, SkScalar transY, + const SkIRect& clipBounds) { + for (int i = 0; i < fBigGlyphs.count(); i++) { + GrAtlasTextBlob::BigGlyph& bigGlyph = fBigGlyphs[i]; + bigGlyph.fVx += transX; + bigGlyph.fVy += transY; + SkMatrix ctm; + ctm.setScale(bigGlyph.fScale, bigGlyph.fScale); + ctm.postTranslate(bigGlyph.fVx, bigGlyph.fVy); + if (bigGlyph.fApplyVM) { + ctm.postConcat(fViewMatrix); + } + + GrBlurUtils::drawPathWithMaskFilter(context, dc, clip, bigGlyph.fPath, + skPaint, ctm, nullptr, clipBounds, false); + } +} + +void GrAtlasTextBlob::flushRunAsPaths(GrContext* context, GrDrawContext* dc, + const SkSurfaceProps& props, + const SkTextBlobRunIterator& it, + const GrClip& clip, const SkPaint& skPaint, + SkDrawFilter* drawFilter, const SkMatrix& viewMatrix, + const SkIRect& clipBounds, SkScalar x, SkScalar y) { + SkPaint runPaint = skPaint; + + size_t textLen = it.glyphCount() * sizeof(uint16_t); + const SkPoint& offset = it.offset(); + + it.applyFontToPaint(&runPaint); + + if (drawFilter && !drawFilter->filter(&runPaint, SkDrawFilter::kText_Type)) { + return; + } + + runPaint.setFlags(GrTextContext::FilterTextFlags(props, runPaint)); + + switch (it.positioning()) { + case SkTextBlob::kDefault_Positioning: + GrTextUtils::DrawTextAsPath(context, dc, clip, runPaint, viewMatrix, + (const char *)it.glyphs(), + textLen, x + offset.x(), y + offset.y(), clipBounds); + break; + case SkTextBlob::kHorizontal_Positioning: + GrTextUtils::DrawPosTextAsPath(context, dc, props, clip, runPaint, viewMatrix, + (const char*)it.glyphs(), + textLen, it.pos(), 1, SkPoint::Make(x, y + offset.y()), + clipBounds); + break; + case SkTextBlob::kFull_Positioning: + GrTextUtils::DrawPosTextAsPath(context, dc, props, clip, runPaint, viewMatrix, + (const char*)it.glyphs(), + textLen, it.pos(), 2, SkPoint::Make(x, y), clipBounds); + break; + } +} + +void GrAtlasTextBlob::flushCached(GrContext* context, + GrDrawContext* dc, + const SkTextBlob* blob, + const SkSurfaceProps& props, + const GrDistanceFieldAdjustTable* distanceAdjustTable, + const SkPaint& skPaint, + const GrPaint& grPaint, + SkDrawFilter* drawFilter, + const GrClip& clip, + const SkMatrix& viewMatrix, + const SkIRect& clipBounds, + SkScalar x, SkScalar y, + SkScalar transX, SkScalar transY) { + // We loop through the runs of the blob, flushing each. If any run is too large, then we flush + // it as paths + GrPipelineBuilder pipelineBuilder(grPaint, dc->accessRenderTarget(), clip); + + GrColor color = grPaint.getColor(); + + SkTextBlobRunIterator it(blob); + for (int run = 0; !it.done(); it.next(), run++) { + if (fRuns[run].fDrawAsPaths) { + this->flushRunAsPaths(context, dc, props, it, clip, skPaint, + drawFilter, viewMatrix, clipBounds, x, y); + continue; + } + fRuns[run].fVertexBounds.offset(transX, transY); + this->flushRun(dc, &pipelineBuilder, run, color, + transX, transY, skPaint, props, + distanceAdjustTable, context->getBatchFontCache()); + } + + // Now flush big glyphs + this->flushBigGlyphs(context, dc, clip, skPaint, transX, transY, clipBounds); +} + +void GrAtlasTextBlob::flushThrowaway(GrContext* context, + GrDrawContext* dc, + const SkSurfaceProps& props, + const GrDistanceFieldAdjustTable* distanceAdjustTable, + const SkPaint& skPaint, + const GrPaint& grPaint, + const GrClip& clip, + const SkIRect& clipBounds) { + GrPipelineBuilder pipelineBuilder(grPaint, dc->accessRenderTarget(), clip); + + GrColor color = grPaint.getColor(); + for (int run = 0; run < fRunCount; run++) { + this->flushRun(dc, &pipelineBuilder, run, color, 0, 0, skPaint, props, + distanceAdjustTable, context->getBatchFontCache()); + } + + // Now flush big glyphs + this->flushBigGlyphs(context, dc, clip, skPaint, 0, 0, clipBounds); +} + + +// TODO get this code building again +#ifdef CACHE_SANITY_CHECK +void GrAtlasTextBlob::AssertEqual(const GrAtlasTextBlob& l, const GrAtlasTextBlob& r) { + SkASSERT(l.fSize == r.fSize); + SkASSERT(l.fPool == r.fPool); + + SkASSERT(l.fBlurRec.fSigma == r.fBlurRec.fSigma); + SkASSERT(l.fBlurRec.fStyle == r.fBlurRec.fStyle); + SkASSERT(l.fBlurRec.fQuality == r.fBlurRec.fQuality); + + SkASSERT(l.fStrokeInfo.fFrameWidth == r.fStrokeInfo.fFrameWidth); + SkASSERT(l.fStrokeInfo.fMiterLimit == r.fStrokeInfo.fMiterLimit); + SkASSERT(l.fStrokeInfo.fJoin == r.fStrokeInfo.fJoin); + + SkASSERT(l.fBigGlyphs.count() == r.fBigGlyphs.count()); + for (int i = 0; i < l.fBigGlyphs.count(); i++) { + const BigGlyph& lBigGlyph = l.fBigGlyphs[i]; + const BigGlyph& rBigGlyph = r.fBigGlyphs[i]; + + SkASSERT(lBigGlyph.fPath == rBigGlyph.fPath); + // We can't assert that these have the same translations + } + + SkASSERT(l.fKey == r.fKey); + SkASSERT(l.fViewMatrix.cheapEqualTo(r.fViewMatrix)); + SkASSERT(l.fPaintColor == r.fPaintColor); + SkASSERT(l.fMaxMinScale == r.fMaxMinScale); + SkASSERT(l.fMinMaxScale == r.fMinMaxScale); + SkASSERT(l.fTextType == r.fTextType); + + SkASSERT(l.fRunCount == r.fRunCount); + for (int i = 0; i < l.fRunCount; i++) { + const Run& lRun = l.fRuns[i]; + const Run& rRun = r.fRuns[i]; + + if (lRun.fStrike.get()) { + SkASSERT(rRun.fStrike.get()); + SkASSERT(GrBatchTextStrike::GetKey(*lRun.fStrike) == + GrBatchTextStrike::GetKey(*rRun.fStrike)); + + } else { + SkASSERT(!rRun.fStrike.get()); + } + + if (lRun.fTypeface.get()) { + SkASSERT(rRun.fTypeface.get()); + SkASSERT(SkTypeface::Equal(lRun.fTypeface, rRun.fTypeface)); + } else { + SkASSERT(!rRun.fTypeface.get()); + } + + // We offset bounds right before flush time so they will not be correct here + //SkASSERT(lRun.fVertexBounds == rRun.fVertexBounds); + + SkASSERT(lRun.fDescriptor.getDesc()); + SkASSERT(rRun.fDescriptor.getDesc()); + SkASSERT(lRun.fDescriptor.getDesc()->equals(*rRun.fDescriptor.getDesc())); + + if (lRun.fOverrideDescriptor.get()) { + SkASSERT(lRun.fOverrideDescriptor->getDesc()); + SkASSERT(rRun.fOverrideDescriptor.get() && rRun.fOverrideDescriptor->getDesc());; + SkASSERT(lRun.fOverrideDescriptor->getDesc()->equals( + *rRun.fOverrideDescriptor->getDesc())); + } else { + SkASSERT(!rRun.fOverrideDescriptor.get()); + } + + // color can be changed + //SkASSERT(lRun.fColor == rRun.fColor); + SkASSERT(lRun.fInitialized == rRun.fInitialized); + SkASSERT(lRun.fDrawAsPaths == rRun.fDrawAsPaths); + + SkASSERT(lRun.fSubRunInfo.count() == rRun.fSubRunInfo.count()); + for(int j = 0; j < lRun.fSubRunInfo.count(); j++) { + const Run::SubRunInfo& lSubRun = lRun.fSubRunInfo[j]; + const Run::SubRunInfo& rSubRun = rRun.fSubRunInfo[j]; + + SkASSERT(lSubRun.fVertexStartIndex == rSubRun.fVertexStartIndex); + SkASSERT(lSubRun.fVertexEndIndex == rSubRun.fVertexEndIndex); + SkASSERT(lSubRun.fGlyphStartIndex == rSubRun.fGlyphStartIndex); + SkASSERT(lSubRun.fGlyphEndIndex == rSubRun.fGlyphEndIndex); + SkASSERT(lSubRun.fTextRatio == rSubRun.fTextRatio); + SkASSERT(lSubRun.fMaskFormat == rSubRun.fMaskFormat); + SkASSERT(lSubRun.fDrawAsDistanceFields == rSubRun.fDrawAsDistanceFields); + SkASSERT(lSubRun.fUseLCDText == rSubRun.fUseLCDText); + + //We can't compare the bulk use tokens with this method + /* + SkASSERT(lSubRun.fBulkUseToken.fPlotsToUpdate.count() == + rSubRun.fBulkUseToken.fPlotsToUpdate.count()); + SkASSERT(lSubRun.fBulkUseToken.fPlotAlreadyUpdated == + rSubRun.fBulkUseToken.fPlotAlreadyUpdated); + for (int k = 0; k < lSubRun.fBulkUseToken.fPlotsToUpdate.count(); k++) { + SkASSERT(lSubRun.fBulkUseToken.fPlotsToUpdate[k] == + rSubRun.fBulkUseToken.fPlotsToUpdate[k]); + }*/ + } + } +} + +#endif diff --git a/src/gpu/text/GrAtlasTextBlob.h b/src/gpu/text/GrAtlasTextBlob.h new file mode 100644 index 0000000000..d99ae707e0 --- /dev/null +++ b/src/gpu/text/GrAtlasTextBlob.h @@ -0,0 +1,388 @@ +/* + * Copyright 2015 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef GrAtlasTextBlob_DEFINED +#define GrAtlasTextBlob_DEFINED + +#include "GrBatchAtlas.h" +#include "GrBatchFontCache.h" +#include "GrColor.h" +#include "GrMemoryPool.h" +#include "SkDescriptor.h" +#include "SkMaskFilter.h" +#include "SkSurfaceProps.h" +#include "SkTInternalLList.h" + +struct GrDistanceFieldAdjustTable; +class GrTextContext; +class SkDrawFilter; +class SkTextBlob; +class SkTextBlobRunIterator; + +// With this flag enabled, the GrAtlasTextContext will, as a sanity check, regenerate every blob +// that comes in to verify the integrity of its cache +//#define CACHE_SANITY_CHECK // VERY SLOW + +/* + * A GrAtlasTextBlob contains a fully processed SkTextBlob, suitable for nearly immediate drawing + * on the GPU. These are initially created with valid positions and colors, but invalid + * texture coordinates. The GrAtlasTextBlob itself has a few Blob-wide properties, and also + * consists of a number of runs. Runs inside a blob are flushed individually so they can be + * reordered. + * + * The only thing(aside from a memcopy) required to flush a GrAtlasTextBlob is to ensure that + * the GrAtlas will not evict anything the Blob needs. + * + * Note: This struct should really be named GrCachedAtasTextBlob, but that is too verbose. + * + * *WARNING* If you add new fields to this struct, then you may need to to update AssertEqual + */ +class GrAtlasTextBlob : public SkNVRefCnt<GrAtlasTextBlob> { +public: + SK_DECLARE_INTERNAL_LLIST_INTERFACE(GrAtlasTextBlob); + + /* + * Each Run inside of the blob can have its texture coordinates regenerated if required. + * To determine if regeneration is necessary, fAtlasGeneration is used. If there have been + * any evictions inside of the atlas, then we will simply regenerate Runs. We could track + * this at a more fine grained level, but its not clear if this is worth it, as evictions + * should be fairly rare. + * + * One additional point, each run can contain glyphs with any of the three mask formats. + * We call these SubRuns. Because a subrun must be a contiguous range, we have to create + * a new subrun each time the mask format changes in a run. In theory, a run can have as + * many SubRuns as it has glyphs, ie if a run alternates between color emoji and A8. In + * practice, the vast majority of runs have only a single subrun. + * + * Finally, for runs where the entire thing is too large for the GrAtlasTextContext to + * handle, we have a bit to mark the run as flusahable via rendering as paths. It is worth + * pointing. It would be a bit expensive to figure out ahead of time whether or not a run + * can flush in this manner, so we always allocate vertices for the run, regardless of + * whether or not it is too large. The benefit of this strategy is that we can always reuse + * a blob allocation regardless of viewmatrix changes. We could store positions for these + * glyphs. However, its not clear if this is a win because we'd still have to either go the + * glyph cache to get the path at flush time, or hold onto the path in the cache, which + * would greatly increase the memory of these cached items. + */ + struct Run { + Run() + : fInitialized(false) + , fDrawAsPaths(false) { + fVertexBounds.setLargestInverted(); + // To ensure we always have one subrun, we push back a fresh run here + fSubRunInfo.push_back(); + } + struct SubRunInfo { + SubRunInfo() + : fAtlasGeneration(GrBatchAtlas::kInvalidAtlasGeneration) + , fVertexStartIndex(0) + , fVertexEndIndex(0) + , fGlyphStartIndex(0) + , fGlyphEndIndex(0) + , fColor(GrColor_ILLEGAL) + , fMaskFormat(kA8_GrMaskFormat) + , fDrawAsDistanceFields(false) + , fUseLCDText(false) {} + SubRunInfo(const SubRunInfo& that) + : fBulkUseToken(that.fBulkUseToken) + , fStrike(SkSafeRef(that.fStrike.get())) + , fAtlasGeneration(that.fAtlasGeneration) + , fVertexStartIndex(that.fVertexStartIndex) + , fVertexEndIndex(that.fVertexEndIndex) + , fGlyphStartIndex(that.fGlyphStartIndex) + , fGlyphEndIndex(that.fGlyphEndIndex) + , fColor(that.fColor) + , fMaskFormat(that.fMaskFormat) + , fDrawAsDistanceFields(that.fDrawAsDistanceFields) + , fUseLCDText(that.fUseLCDText) { + } + + // TODO when this object is more internal, drop the privacy + void resetBulkUseToken() { fBulkUseToken.reset(); } + GrBatchAtlas::BulkUseTokenUpdater* bulkUseToken() { return &fBulkUseToken; } + void setStrike(GrBatchTextStrike* strike) { fStrike.reset(SkRef(strike)); } + GrBatchTextStrike* strike() const { return fStrike.get(); } + + void setAtlasGeneration(uint64_t atlasGeneration) { fAtlasGeneration = atlasGeneration;} + uint64_t atlasGeneration() const { return fAtlasGeneration; } + + size_t byteCount() const { return fVertexEndIndex - fVertexStartIndex; } + size_t vertexStartIndex() const { return fVertexStartIndex; } + size_t vertexEndIndex() const { return fVertexEndIndex; } + void appendVertices(size_t vertexStride) { + fVertexEndIndex += vertexStride * kVerticesPerGlyph; + } + + uint32_t glyphCount() const { return fGlyphEndIndex - fGlyphStartIndex; } + uint32_t glyphStartIndex() const { return fGlyphStartIndex; } + uint32_t glyphEndIndex() const { return fGlyphEndIndex; } + void glyphAppended() { fGlyphEndIndex++; } + void setColor(GrColor color) { fColor = color; } + GrColor color() const { return fColor; } + void setMaskFormat(GrMaskFormat format) { fMaskFormat = format; } + GrMaskFormat maskFormat() const { return fMaskFormat; } + + void setAsSuccessor(const SubRunInfo& prev) { + fGlyphStartIndex = prev.glyphEndIndex(); + fGlyphEndIndex = prev.glyphEndIndex(); + + fVertexStartIndex = prev.vertexEndIndex(); + fVertexEndIndex = prev.vertexEndIndex(); + } + + // df properties + void setUseLCDText(bool useLCDText) { fUseLCDText = useLCDText; } + bool hasUseLCDText() const { return fUseLCDText; } + void setDrawAsDistanceFields() { fDrawAsDistanceFields = true; } + bool drawAsDistanceFields() const { return fDrawAsDistanceFields; } + + private: + GrBatchAtlas::BulkUseTokenUpdater fBulkUseToken; + SkAutoTUnref<GrBatchTextStrike> fStrike; + uint64_t fAtlasGeneration; + size_t fVertexStartIndex; + size_t fVertexEndIndex; + uint32_t fGlyphStartIndex; + uint32_t fGlyphEndIndex; + GrColor fColor; + GrMaskFormat fMaskFormat; + bool fDrawAsDistanceFields; // df property + bool fUseLCDText; // df property + }; + + SubRunInfo& push_back() { + // Forward glyph / vertex information to seed the new sub run + SubRunInfo& newSubRun = fSubRunInfo.push_back(); + const SubRunInfo& prevSubRun = fSubRunInfo.fromBack(1); + + newSubRun.setAsSuccessor(prevSubRun); + return newSubRun; + } + static const int kMinSubRuns = 1; + SkAutoTUnref<SkTypeface> fTypeface; + SkRect fVertexBounds; + SkSTArray<kMinSubRuns, SubRunInfo> fSubRunInfo; + SkAutoDescriptor fDescriptor; + + // Distance field text cannot draw coloremoji, and so has to fall back. However, + // though the distance field text and the coloremoji may share the same run, they + // will have different descriptors. If fOverrideDescriptor is non-nullptr, then it + // will be used in place of the run's descriptor to regen texture coords + SkAutoTDelete<SkAutoDescriptor> fOverrideDescriptor; // df properties + bool fInitialized; + bool fDrawAsPaths; + }; + + struct BigGlyph { + BigGlyph(const SkPath& path, SkScalar vx, SkScalar vy, SkScalar scale, bool applyVM) + : fPath(path) + , fVx(vx) + , fVy(vy) + , fScale(scale) + , fApplyVM(applyVM) {} + SkPath fPath; + SkScalar fVx; + SkScalar fVy; + SkScalar fScale; + bool fApplyVM; + }; + + struct Key { + Key() { + sk_bzero(this, sizeof(Key)); + } + uint32_t fUniqueID; + // Color may affect the gamma of the mask we generate, but in a fairly limited way. + // Each color is assigned to on of a fixed number of buckets based on its + // luminance. For each luminance bucket there is a "canonical color" that + // represents the bucket. This functionality is currently only supported for A8 + SkColor fCanonicalColor; + SkPaint::Style fStyle; + SkPixelGeometry fPixelGeometry; + bool fHasBlur; + + bool operator==(const Key& other) const { + return 0 == memcmp(this, &other, sizeof(Key)); + } + }; + + struct StrokeInfo { + SkScalar fFrameWidth; + SkScalar fMiterLimit; + SkPaint::Join fJoin; + }; + + enum TextType { + kHasDistanceField_TextType = 0x1, + kHasBitmap_TextType = 0x2, + }; + + // all glyph / vertex offsets are into these pools. + unsigned char* fVertices; + GrGlyph** fGlyphs; + Run* fRuns; + GrMemoryPool* fPool; + SkMaskFilter::BlurRec fBlurRec; + StrokeInfo fStrokeInfo; + SkTArray<BigGlyph> fBigGlyphs; + Key fKey; + SkMatrix fViewMatrix; + GrColor fPaintColor; + SkScalar fX; + SkScalar fY; + + // We can reuse distance field text, but only if the new viewmatrix would not result in + // a mip change. Because there can be multiple runs in a blob, we track the overall + // maximum minimum scale, and minimum maximum scale, we can support before we need to regen + SkScalar fMaxMinScale; + SkScalar fMinMaxScale; + int fRunCount; + uint8_t fTextType; + + GrAtlasTextBlob() + : fMaxMinScale(-SK_ScalarMax) + , fMinMaxScale(SK_ScalarMax) + , fTextType(0) {} + + ~GrAtlasTextBlob() { + for (int i = 0; i < fRunCount; i++) { + fRuns[i].~Run(); + } + } + + static const Key& GetKey(const GrAtlasTextBlob& blob) { + return blob.fKey; + } + + static uint32_t Hash(const Key& key) { + return SkChecksum::Murmur3(&key, sizeof(Key)); + } + + void operator delete(void* p) { + GrAtlasTextBlob* blob = reinterpret_cast<GrAtlasTextBlob*>(p); + blob->fPool->release(p); + } + void* operator new(size_t) { + SkFAIL("All blobs are created by placement new."); + return sk_malloc_throw(0); + } + + void* operator new(size_t, void* p) { return p; } + void operator delete(void* target, void* placement) { + ::operator delete(target, placement); + } + + bool hasDistanceField() const { return SkToBool(fTextType & kHasDistanceField_TextType); } + bool hasBitmap() const { return SkToBool(fTextType & kHasBitmap_TextType); } + void setHasDistanceField() { fTextType |= kHasDistanceField_TextType; } + void setHasBitmap() { fTextType |= kHasBitmap_TextType; } + + void push_back_run(int currRun) { + SkASSERT(currRun < fRunCount); + if (currRun > 0) { + Run::SubRunInfo& newRun = fRuns[currRun].fSubRunInfo.back(); + Run::SubRunInfo& lastRun = fRuns[currRun - 1].fSubRunInfo.back(); + newRun.setAsSuccessor(lastRun); + } + } + + // Appends a glyph to the blob. If the glyph is too large, the glyph will be appended + // as a path. + void appendGlyph(int runIndex, + const SkRect& positions, + GrColor color, + GrBatchTextStrike* strike, + GrGlyph* glyph, + GrFontScaler* scaler, const SkGlyph& skGlyph, + SkScalar x, SkScalar y, SkScalar scale, bool applyVM); + + static size_t GetVertexStride(GrMaskFormat maskFormat) { + switch (maskFormat) { + case kA8_GrMaskFormat: + return kGrayTextVASize; + case kARGB_GrMaskFormat: + return kColorTextVASize; + default: + return kLCDTextVASize; + } + } + + bool mustRegenerate(SkScalar* outTransX, SkScalar* outTransY, const SkPaint& paint, + GrColor color, const SkMaskFilter::BlurRec& blurRec, + const SkMatrix& viewMatrix, SkScalar x, SkScalar y); + + // flush a GrAtlasTextBlob associated with a SkTextBlob + void flushCached(GrContext* context, + GrDrawContext* dc, + const SkTextBlob* blob, + const SkSurfaceProps& props, + const GrDistanceFieldAdjustTable* distanceAdjustTable, + const SkPaint& skPaint, + const GrPaint& grPaint, + SkDrawFilter* drawFilter, + const GrClip& clip, + const SkMatrix& viewMatrix, + const SkIRect& clipBounds, + SkScalar x, SkScalar y, + SkScalar transX, SkScalar transY); + + // flush a throwaway GrAtlasTextBlob *not* associated with an SkTextBlob + void flushThrowaway(GrContext* context, + GrDrawContext* dc, + const SkSurfaceProps& props, + const GrDistanceFieldAdjustTable* distanceAdjustTable, + const SkPaint& skPaint, + const GrPaint& grPaint, + const GrClip& clip, + const SkIRect& clipBounds); + + // position + local coord + static const size_t kColorTextVASize = sizeof(SkPoint) + sizeof(SkIPoint16); + static const size_t kGrayTextVASize = sizeof(SkPoint) + sizeof(GrColor) + sizeof(SkIPoint16); + static const size_t kLCDTextVASize = kGrayTextVASize; + static const int kVerticesPerGlyph = 4; + +#ifdef CACHE_SANITY_CHECK + static void AssertEqual(const GrAtlasTextBlob&, const GrAtlasTextBlob&); + size_t fSize; +#endif + + // We'd like to inline this and make it private, but there is some test code which calls it. + // TODO refactor this + GrDrawBatch* createBatch(const Run::SubRunInfo& info, + int glyphCount, int run, int subRun, + GrColor color, SkScalar transX, SkScalar transY, + const SkPaint& skPaint, const SkSurfaceProps& props, + const GrDistanceFieldAdjustTable* distanceAdjustTable, + GrBatchFontCache* cache); + +private: + void appendLargeGlyph(GrGlyph* glyph, GrFontScaler* scaler, const SkGlyph& skGlyph, + SkScalar x, SkScalar y, SkScalar scale, bool applyVM); + + inline void flushRun(GrDrawContext* dc, GrPipelineBuilder* pipelineBuilder, + int run, GrColor color, + SkScalar transX, SkScalar transY, + const SkPaint& skPaint, const SkSurfaceProps& props, + const GrDistanceFieldAdjustTable* distanceAdjustTable, + GrBatchFontCache* cache); + + void flushBigGlyphs(GrContext* context, GrDrawContext* dc, + const GrClip& clip, const SkPaint& skPaint, + SkScalar transX, SkScalar transY, + const SkIRect& clipBounds); + + void flushRunAsPaths(GrContext* context, + GrDrawContext* dc, + const SkSurfaceProps& props, + const SkTextBlobRunIterator& it, + const GrClip& clip, const SkPaint& skPaint, + SkDrawFilter* drawFilter, const SkMatrix& viewMatrix, + const SkIRect& clipBounds, SkScalar x, SkScalar y); +}; + +#endif diff --git a/src/gpu/text/GrAtlasTextContext.cpp b/src/gpu/text/GrAtlasTextContext.cpp new file mode 100644 index 0000000000..aca839cf2c --- /dev/null +++ b/src/gpu/text/GrAtlasTextContext.cpp @@ -0,0 +1,822 @@ +/* + * Copyright 2015 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ +#include "GrAtlasTextContext.h" + +#include "GrDrawContext.h" +#include "GrDrawTarget.h" +#include "GrFontScaler.h" +#include "GrStrokeInfo.h" +#include "GrTextBlobCache.h" +#include "GrTexturePriv.h" +#include "GrTextUtils.h" +#include "GrVertexBuffer.h" + +#include "SkAutoKern.h" +#include "SkColorPriv.h" +#include "SkColorFilter.h" +#include "SkDistanceFieldGen.h" +#include "SkDraw.h" +#include "SkDrawFilter.h" +#include "SkDrawProcs.h" +#include "SkFindAndPlaceGlyph.h" +#include "SkGlyphCache.h" +#include "SkGpuDevice.h" +#include "SkGrPriv.h" +#include "SkPath.h" +#include "SkRTConf.h" +#include "SkStrokeRec.h" +#include "SkTextBlob.h" +#include "SkTextMapStateProc.h" + +#include "batches/GrAtlasTextBatch.h" + +namespace { +static const int kMinDFFontSize = 18; +static const int kSmallDFFontSize = 32; +static const int kSmallDFFontLimit = 32; +static const int kMediumDFFontSize = 72; +static const int kMediumDFFontLimit = 72; +static const int kLargeDFFontSize = 162; +#ifdef SK_BUILD_FOR_ANDROID +static const int kLargeDFFontLimit = 384; +#else +static const int kLargeDFFontLimit = 2 * kLargeDFFontSize; +#endif +}; + +GrAtlasTextContext::GrAtlasTextContext(GrContext* context, const SkSurfaceProps& surfaceProps) + : INHERITED(context, surfaceProps) + , fDistanceAdjustTable(new GrDistanceFieldAdjustTable) { + // We overallocate vertices in our textblobs based on the assumption that A8 has the greatest + // vertexStride + static_assert(GrAtlasTextBlob::kGrayTextVASize >= GrAtlasTextBlob::kColorTextVASize && + GrAtlasTextBlob::kGrayTextVASize >= GrAtlasTextBlob::kLCDTextVASize, + "vertex_attribute_changed"); + fCurrStrike = nullptr; + fCache = context->getTextBlobCache(); +} + + +GrAtlasTextContext* GrAtlasTextContext::Create(GrContext* context, + const SkSurfaceProps& surfaceProps) { + return new GrAtlasTextContext(context, surfaceProps); +} + +bool GrAtlasTextContext::canDraw(const SkPaint& skPaint, const SkMatrix& viewMatrix) { + return this->canDrawAsDistanceFields(skPaint, viewMatrix) || + !SkDraw::ShouldDrawTextAsPaths(skPaint, viewMatrix); +} + +GrColor GrAtlasTextContext::ComputeCanonicalColor(const SkPaint& paint, bool lcd) { + GrColor canonicalColor = paint.computeLuminanceColor(); + if (lcd) { + // This is the correct computation, but there are tons of cases where LCD can be overridden. + // For now we just regenerate if any run in a textblob has LCD. + // TODO figure out where all of these overrides are and see if we can incorporate that logic + // at a higher level *OR* use sRGB + SkASSERT(false); + //canonicalColor = SkMaskGamma::CanonicalColor(canonicalColor); + } else { + // A8, though can have mixed BMP text but it shouldn't matter because BMP text won't have + // gamma corrected masks anyways, nor color + U8CPU lum = SkComputeLuminance(SkColorGetR(canonicalColor), + SkColorGetG(canonicalColor), + SkColorGetB(canonicalColor)); + // reduce to our finite number of bits + canonicalColor = SkMaskGamma::CanonicalColor(SkColorSetRGB(lum, lum, lum)); + } + return canonicalColor; +} + +// TODO if this function ever shows up in profiling, then we can compute this value when the +// textblob is being built and cache it. However, for the time being textblobs mostly only have 1 +// run so this is not a big deal to compute here. +bool GrAtlasTextContext::HasLCD(const SkTextBlob* blob) { + SkTextBlobRunIterator it(blob); + for (; !it.done(); it.next()) { + if (it.isLCD()) { + return true; + } + } + return false; +} + +inline SkGlyphCache* GrAtlasTextContext::setupCache(GrAtlasTextBlob::Run* run, + const SkPaint& skPaint, + const SkMatrix* viewMatrix, + bool noGamma) { + skPaint.getScalerContextDescriptor(&run->fDescriptor, fSurfaceProps, viewMatrix, noGamma); + run->fTypeface.reset(SkSafeRef(skPaint.getTypeface())); + return SkGlyphCache::DetachCache(run->fTypeface, run->fDescriptor.getDesc()); +} + +void GrAtlasTextContext::drawTextBlob(GrDrawContext* dc, + const GrClip& clip, const SkPaint& skPaint, + const SkMatrix& viewMatrix, const SkTextBlob* blob, + SkScalar x, SkScalar y, + SkDrawFilter* drawFilter, const SkIRect& clipBounds) { + // If we have been abandoned, then don't draw + if (fContext->abandoned()) { + return; + } + + SkAutoTUnref<GrAtlasTextBlob> cacheBlob; + SkMaskFilter::BlurRec blurRec; + GrAtlasTextBlob::Key key; + // It might be worth caching these things, but its not clear at this time + // TODO for animated mask filters, this will fill up our cache. We need a safeguard here + const SkMaskFilter* mf = skPaint.getMaskFilter(); + bool canCache = !(skPaint.getPathEffect() || + (mf && !mf->asABlur(&blurRec)) || + drawFilter); + + if (canCache) { + bool hasLCD = HasLCD(blob); + + // We canonicalize all non-lcd draws to use kUnknown_SkPixelGeometry + SkPixelGeometry pixelGeometry = hasLCD ? fSurfaceProps.pixelGeometry() : + kUnknown_SkPixelGeometry; + + // TODO we want to figure out a way to be able to use the canonical color on LCD text, + // see the note on ComputeCanonicalColor above. We pick a dummy value for LCD text to + // ensure we always match the same key + GrColor canonicalColor = hasLCD ? SK_ColorTRANSPARENT : + ComputeCanonicalColor(skPaint, hasLCD); + + key.fPixelGeometry = pixelGeometry; + key.fUniqueID = blob->uniqueID(); + key.fStyle = skPaint.getStyle(); + key.fHasBlur = SkToBool(mf); + key.fCanonicalColor = canonicalColor; + cacheBlob.reset(SkSafeRef(fCache->find(key))); + } + + SkScalar transX = 0.f; + SkScalar transY = 0.f; + + // Though for the time being runs in the textblob can override the paint, they only touch font + // info. + GrPaint grPaint; + if (!SkPaintToGrPaint(fContext, skPaint, viewMatrix, &grPaint)) { + return; + } + + if (cacheBlob) { + if (cacheBlob->mustRegenerate(&transX, &transY, skPaint, grPaint.getColor(), blurRec, + viewMatrix, x, y)) { + // We have to remake the blob because changes may invalidate our masks. + // TODO we could probably get away reuse most of the time if the pointer is unique, + // but we'd have to clear the subrun information + fCache->remove(cacheBlob); + cacheBlob.reset(SkRef(fCache->createCachedBlob(blob, key, blurRec, skPaint, + GrAtlasTextBlob::kGrayTextVASize))); + this->regenerateTextBlob(cacheBlob, skPaint, grPaint.getColor(), viewMatrix, + blob, x, y, drawFilter, clip); + } else { + fCache->makeMRU(cacheBlob); +#ifdef CACHE_SANITY_CHECK + { + int glyphCount = 0; + int runCount = 0; + GrTextBlobCache::BlobGlyphCount(&glyphCount, &runCount, blob); + SkAutoTUnref<GrAtlasTextBlob> sanityBlob(fCache->createBlob(glyphCount, runCount, + kGrayTextVASize)); + GrTextBlobCache::SetupCacheBlobKey(sanityBlob, key, blurRec, skPaint); + this->regenerateTextBlob(sanityBlob, skPaint, grPaint.getColor(), viewMatrix, + blob, x, y, drawFilter, clip); + GrAtlasTextBlob::AssertEqual(*sanityBlob, *cacheBlob); + } + +#endif + } + } else { + if (canCache) { + cacheBlob.reset(SkRef(fCache->createCachedBlob(blob, key, blurRec, skPaint, + GrAtlasTextBlob::kGrayTextVASize))); + } else { + cacheBlob.reset(fCache->createBlob(blob, GrAtlasTextBlob::kGrayTextVASize)); + } + this->regenerateTextBlob(cacheBlob, skPaint, grPaint.getColor(), viewMatrix, + blob, x, y, drawFilter, clip); + } + + cacheBlob->flushCached(fContext, dc, blob, fSurfaceProps, fDistanceAdjustTable, skPaint, + grPaint, drawFilter, clip, viewMatrix, clipBounds, x, y, transX, transY); +} + +inline bool GrAtlasTextContext::canDrawAsDistanceFields(const SkPaint& skPaint, + const SkMatrix& viewMatrix) { + // TODO: support perspective (need getMaxScale replacement) + if (viewMatrix.hasPerspective()) { + return false; + } + + SkScalar maxScale = viewMatrix.getMaxScale(); + SkScalar scaledTextSize = maxScale*skPaint.getTextSize(); + // Hinted text looks far better at small resolutions + // Scaling up beyond 2x yields undesireable artifacts + if (scaledTextSize < kMinDFFontSize || scaledTextSize > kLargeDFFontLimit) { + return false; + } + + bool useDFT = fSurfaceProps.isUseDeviceIndependentFonts(); +#if SK_FORCE_DISTANCE_FIELD_TEXT + useDFT = true; +#endif + + if (!useDFT && scaledTextSize < kLargeDFFontSize) { + return false; + } + + // rasterizers and mask filters modify alpha, which doesn't + // translate well to distance + if (skPaint.getRasterizer() || skPaint.getMaskFilter() || + !fContext->caps()->shaderCaps()->shaderDerivativeSupport()) { + return false; + } + + // TODO: add some stroking support + if (skPaint.getStyle() != SkPaint::kFill_Style) { + return false; + } + + return true; +} + +void GrAtlasTextContext::regenerateTextBlob(GrAtlasTextBlob* cacheBlob, + const SkPaint& skPaint, GrColor color, + const SkMatrix& viewMatrix, + const SkTextBlob* blob, SkScalar x, SkScalar y, + SkDrawFilter* drawFilter, + const GrClip& clip) { + // The color here is the GrPaint color, and it is used to determine whether we + // have to regenerate LCD text blobs. + // We use this color vs the SkPaint color because it has the colorfilter applied. + cacheBlob->fPaintColor = color; + cacheBlob->fViewMatrix = viewMatrix; + cacheBlob->fX = x; + cacheBlob->fY = y; + + // Regenerate textblob + SkPaint runPaint = skPaint; + SkTextBlobRunIterator it(blob); + for (int run = 0; !it.done(); it.next(), run++) { + int glyphCount = it.glyphCount(); + size_t textLen = glyphCount * sizeof(uint16_t); + const SkPoint& offset = it.offset(); + // applyFontToPaint() always overwrites the exact same attributes, + // so it is safe to not re-seed the paint for this reason. + it.applyFontToPaint(&runPaint); + + if (drawFilter && !drawFilter->filter(&runPaint, SkDrawFilter::kText_Type)) { + // A false return from filter() means we should abort the current draw. + runPaint = skPaint; + continue; + } + + runPaint.setFlags(FilterTextFlags(fSurfaceProps, runPaint)); + + cacheBlob->push_back_run(run); + + if (this->canDrawAsDistanceFields(runPaint, viewMatrix)) { + cacheBlob->setHasDistanceField(); + SkPaint dfPaint = runPaint; + SkScalar textRatio; + this->initDistanceFieldPaint(cacheBlob, &dfPaint, &textRatio, viewMatrix); + Run& runIdx = cacheBlob->fRuns[run]; + PerSubRunInfo& subRun = runIdx.fSubRunInfo.back(); + subRun.setUseLCDText(runPaint.isLCDRenderText()); + subRun.setDrawAsDistanceFields(); + + SkTDArray<char> fallbackTxt; + SkTDArray<SkScalar> fallbackPos; + SkPoint dfOffset; + int scalarsPerPosition = 2; + switch (it.positioning()) { + case SkTextBlob::kDefault_Positioning: { + this->internalDrawDFText(cacheBlob, run, dfPaint, color, viewMatrix, + (const char *)it.glyphs(), textLen, + x + offset.x(), y + offset.y(), textRatio, + &fallbackTxt, &fallbackPos, &dfOffset, runPaint); + break; + } + case SkTextBlob::kHorizontal_Positioning: { + scalarsPerPosition = 1; + dfOffset = SkPoint::Make(x, y + offset.y()); + this->internalDrawDFPosText(cacheBlob, run, dfPaint, color, viewMatrix, + (const char*)it.glyphs(), textLen, it.pos(), + scalarsPerPosition, dfOffset, textRatio, + &fallbackTxt, &fallbackPos); + break; + } + case SkTextBlob::kFull_Positioning: { + dfOffset = SkPoint::Make(x, y); + this->internalDrawDFPosText(cacheBlob, run, dfPaint, color, viewMatrix, + (const char*)it.glyphs(), textLen, it.pos(), + scalarsPerPosition, dfOffset, textRatio, + &fallbackTxt, &fallbackPos); + break; + } + } + if (fallbackTxt.count()) { + this->fallbackDrawPosText(cacheBlob, run, clip, color, runPaint, viewMatrix, + fallbackTxt, fallbackPos, scalarsPerPosition, dfOffset); + } + } else if (SkDraw::ShouldDrawTextAsPaths(runPaint, viewMatrix)) { + cacheBlob->fRuns[run].fDrawAsPaths = true; + } else { + cacheBlob->setHasBitmap(); + SkGlyphCache* cache = this->setupCache(&cacheBlob->fRuns[run], runPaint, &viewMatrix, + false); + switch (it.positioning()) { + case SkTextBlob::kDefault_Positioning: + GrTextUtils::DrawBmpText(cacheBlob, run, fContext->getBatchFontCache(), + cache, runPaint, color, viewMatrix, + (const char *)it.glyphs(), textLen, + x + offset.x(), y + offset.y()); + break; + case SkTextBlob::kHorizontal_Positioning: + GrTextUtils::DrawBmpPosText(cacheBlob, run, fContext->getBatchFontCache(), + cache, runPaint, color, viewMatrix, + (const char*)it.glyphs(), textLen, it.pos(), 1, + SkPoint::Make(x, y + offset.y())); + break; + case SkTextBlob::kFull_Positioning: + GrTextUtils::DrawBmpPosText(cacheBlob, run, fContext->getBatchFontCache(), + cache, runPaint, color, viewMatrix, + (const char*)it.glyphs(), textLen, it.pos(), 2, + SkPoint::Make(x, y)); + break; + } + SkGlyphCache::AttachCache(cache); + } + + if (drawFilter) { + // A draw filter may change the paint arbitrarily, so we must re-seed in this case. + runPaint = skPaint; + } + } +} + +inline void GrAtlasTextContext::initDistanceFieldPaint(GrAtlasTextBlob* blob, + SkPaint* skPaint, + SkScalar* textRatio, + const SkMatrix& viewMatrix) { + // getMaxScale doesn't support perspective, so neither do we at the moment + SkASSERT(!viewMatrix.hasPerspective()); + SkScalar maxScale = viewMatrix.getMaxScale(); + SkScalar textSize = skPaint->getTextSize(); + SkScalar scaledTextSize = textSize; + // if we have non-unity scale, we need to choose our base text size + // based on the SkPaint's text size multiplied by the max scale factor + // TODO: do we need to do this if we're scaling down (i.e. maxScale < 1)? + if (maxScale > 0 && !SkScalarNearlyEqual(maxScale, SK_Scalar1)) { + scaledTextSize *= maxScale; + } + + // We have three sizes of distance field text, and within each size 'bucket' there is a floor + // and ceiling. A scale outside of this range would require regenerating the distance fields + SkScalar dfMaskScaleFloor; + SkScalar dfMaskScaleCeil; + if (scaledTextSize <= kSmallDFFontLimit) { + dfMaskScaleFloor = kMinDFFontSize; + dfMaskScaleCeil = kSmallDFFontLimit; + *textRatio = textSize / kSmallDFFontSize; + skPaint->setTextSize(SkIntToScalar(kSmallDFFontSize)); + } else if (scaledTextSize <= kMediumDFFontLimit) { + dfMaskScaleFloor = kSmallDFFontLimit; + dfMaskScaleCeil = kMediumDFFontLimit; + *textRatio = textSize / kMediumDFFontSize; + skPaint->setTextSize(SkIntToScalar(kMediumDFFontSize)); + } else { + dfMaskScaleFloor = kMediumDFFontLimit; + dfMaskScaleCeil = kLargeDFFontLimit; + *textRatio = textSize / kLargeDFFontSize; + skPaint->setTextSize(SkIntToScalar(kLargeDFFontSize)); + } + + // Because there can be multiple runs in the blob, we want the overall maxMinScale, and + // minMaxScale to make regeneration decisions. Specifically, we want the maximum minimum scale + // we can tolerate before we'd drop to a lower mip size, and the minimum maximum scale we can + // tolerate before we'd have to move to a large mip size. When we actually test these values + // we look at the delta in scale between the new viewmatrix and the old viewmatrix, and test + // against these values to decide if we can reuse or not(ie, will a given scale change our mip + // level) + SkASSERT(dfMaskScaleFloor <= scaledTextSize && scaledTextSize <= dfMaskScaleCeil); + blob->fMaxMinScale = SkMaxScalar(dfMaskScaleFloor / scaledTextSize, blob->fMaxMinScale); + blob->fMinMaxScale = SkMinScalar(dfMaskScaleCeil / scaledTextSize, blob->fMinMaxScale); + + skPaint->setLCDRenderText(false); + skPaint->setAutohinted(false); + skPaint->setHinting(SkPaint::kNormal_Hinting); + skPaint->setSubpixelText(true); +} + +inline void GrAtlasTextContext::fallbackDrawPosText(GrAtlasTextBlob* blob, + int runIndex, + const GrClip& clip, + GrColor color, + const SkPaint& skPaint, + const SkMatrix& viewMatrix, + const SkTDArray<char>& fallbackTxt, + const SkTDArray<SkScalar>& fallbackPos, + int scalarsPerPosition, + const SkPoint& offset) { + SkASSERT(fallbackTxt.count()); + blob->setHasBitmap(); + Run& run = blob->fRuns[runIndex]; + // Push back a new subrun to fill and set the override descriptor + run.push_back(); + run.fOverrideDescriptor.reset(new SkAutoDescriptor); + skPaint.getScalerContextDescriptor(run.fOverrideDescriptor, + fSurfaceProps, &viewMatrix, false); + SkGlyphCache* cache = SkGlyphCache::DetachCache(run.fTypeface, + run.fOverrideDescriptor->getDesc()); + GrTextUtils::DrawBmpPosText(blob, runIndex, fContext->getBatchFontCache(), cache, skPaint, + color, viewMatrix, fallbackTxt.begin(), fallbackTxt.count(), + fallbackPos.begin(), scalarsPerPosition, offset); + SkGlyphCache::AttachCache(cache); +} + +inline GrAtlasTextBlob* +GrAtlasTextContext::setupDFBlob(int glyphCount, const SkPaint& origPaint, + const SkMatrix& viewMatrix, SkPaint* dfPaint, + SkScalar* textRatio) { + GrAtlasTextBlob* blob = fCache->createBlob(glyphCount, 1, GrAtlasTextBlob::kGrayTextVASize); + + *dfPaint = origPaint; + this->initDistanceFieldPaint(blob, dfPaint, textRatio, viewMatrix); + blob->fViewMatrix = viewMatrix; + Run& run = blob->fRuns[0]; + PerSubRunInfo& subRun = run.fSubRunInfo.back(); + subRun.setUseLCDText(origPaint.isLCDRenderText()); + subRun.setDrawAsDistanceFields(); + + return blob; +} + +inline GrAtlasTextBlob* +GrAtlasTextContext::createDrawTextBlob(const GrClip& clip, + const GrPaint& paint, const SkPaint& skPaint, + const SkMatrix& viewMatrix, + const char text[], size_t byteLength, + SkScalar x, SkScalar y, const SkIRect& regionClipBounds) { + int glyphCount = skPaint.countText(text, byteLength); + + GrAtlasTextBlob* blob; + if (this->canDrawAsDistanceFields(skPaint, viewMatrix)) { + SkPaint dfPaint; + SkScalar textRatio; + blob = this->setupDFBlob(glyphCount, skPaint, viewMatrix, &dfPaint, &textRatio); + + SkTDArray<char> fallbackTxt; + SkTDArray<SkScalar> fallbackPos; + SkPoint offset; + this->internalDrawDFText(blob, 0, dfPaint, paint.getColor(), viewMatrix, text, + byteLength, x, y, textRatio, &fallbackTxt, &fallbackPos, + &offset, skPaint); + if (fallbackTxt.count()) { + this->fallbackDrawPosText(blob, 0, clip, paint.getColor(), skPaint, viewMatrix, + fallbackTxt, fallbackPos, 2, offset); + } + } else { + blob = fCache->createBlob(glyphCount, 1, GrAtlasTextBlob::kGrayTextVASize); + blob->fViewMatrix = viewMatrix; + + SkGlyphCache* cache = this->setupCache(&blob->fRuns[0], skPaint, &viewMatrix, false); + GrTextUtils::DrawBmpText(blob, 0, fContext->getBatchFontCache(), cache, skPaint, + paint.getColor(), viewMatrix, text, byteLength, x, y); + SkGlyphCache::AttachCache(cache); + } + return blob; +} + +inline GrAtlasTextBlob* +GrAtlasTextContext::createDrawPosTextBlob(const GrClip& clip, + const GrPaint& paint, const SkPaint& skPaint, + const SkMatrix& viewMatrix, + const char text[], size_t byteLength, + const SkScalar pos[], int scalarsPerPosition, + const SkPoint& offset, const SkIRect& regionClipBounds) { + int glyphCount = skPaint.countText(text, byteLength); + + GrAtlasTextBlob* blob; + if (this->canDrawAsDistanceFields(skPaint, viewMatrix)) { + SkPaint dfPaint; + SkScalar textRatio; + blob = this->setupDFBlob(glyphCount, skPaint, viewMatrix, &dfPaint, &textRatio); + + SkTDArray<char> fallbackTxt; + SkTDArray<SkScalar> fallbackPos; + this->internalDrawDFPosText(blob, 0, dfPaint, paint.getColor(), viewMatrix, text, + byteLength, pos, scalarsPerPosition, offset, + textRatio, &fallbackTxt, &fallbackPos); + if (fallbackTxt.count()) { + this->fallbackDrawPosText(blob, 0, clip, paint.getColor(), skPaint, viewMatrix, + fallbackTxt, fallbackPos, scalarsPerPosition, offset); + } + } else { + blob = fCache->createBlob(glyphCount, 1, GrAtlasTextBlob::kGrayTextVASize); + blob->fViewMatrix = viewMatrix; + SkGlyphCache* cache = this->setupCache(&blob->fRuns[0], skPaint, &viewMatrix, false); + GrTextUtils::DrawBmpPosText(blob, 0, fContext->getBatchFontCache(), cache, skPaint, + paint.getColor(), viewMatrix, text, + byteLength, pos, scalarsPerPosition, offset); + SkGlyphCache::AttachCache(cache); + } + return blob; +} + +void GrAtlasTextContext::onDrawText(GrDrawContext* dc, + const GrClip& clip, + const GrPaint& paint, const SkPaint& skPaint, + const SkMatrix& viewMatrix, + const char text[], size_t byteLength, + SkScalar x, SkScalar y, const SkIRect& regionClipBounds) { + SkAutoTUnref<GrAtlasTextBlob> blob( + this->createDrawTextBlob(clip, paint, skPaint, viewMatrix, + text, byteLength, x, y, regionClipBounds)); + blob->flushThrowaway(fContext, dc, fSurfaceProps, fDistanceAdjustTable, skPaint, paint, + clip, regionClipBounds); +} + +void GrAtlasTextContext::onDrawPosText(GrDrawContext* dc, + const GrClip& clip, + const GrPaint& paint, const SkPaint& skPaint, + const SkMatrix& viewMatrix, + const char text[], size_t byteLength, + const SkScalar pos[], int scalarsPerPosition, + const SkPoint& offset, const SkIRect& regionClipBounds) { + SkAutoTUnref<GrAtlasTextBlob> blob( + this->createDrawPosTextBlob(clip, paint, skPaint, viewMatrix, + text, byteLength, + pos, scalarsPerPosition, + offset, regionClipBounds)); + + blob->flushThrowaway(fContext, dc, fSurfaceProps, fDistanceAdjustTable, skPaint, paint, clip, + regionClipBounds); +} + +void GrAtlasTextContext::internalDrawDFText(GrAtlasTextBlob* blob, int runIndex, + const SkPaint& skPaint, GrColor color, + const SkMatrix& viewMatrix, + const char text[], size_t byteLength, + SkScalar x, SkScalar y, + SkScalar textRatio, + SkTDArray<char>* fallbackTxt, + SkTDArray<SkScalar>* fallbackPos, + SkPoint* offset, + const SkPaint& origPaint) { + SkASSERT(byteLength == 0 || text != nullptr); + + // nothing to draw + if (text == nullptr || byteLength == 0) { + return; + } + + SkDrawCacheProc glyphCacheProc = origPaint.getDrawCacheProc(); + SkAutoDescriptor desc; + origPaint.getScalerContextDescriptor(&desc, fSurfaceProps, nullptr, true); + SkGlyphCache* origPaintCache = SkGlyphCache::DetachCache(origPaint.getTypeface(), + desc.getDesc()); + + SkTArray<SkScalar> positions; + + const char* textPtr = text; + SkFixed stopX = 0; + SkFixed stopY = 0; + SkFixed origin = 0; + switch (origPaint.getTextAlign()) { + case SkPaint::kRight_Align: origin = SK_Fixed1; break; + case SkPaint::kCenter_Align: origin = SK_FixedHalf; break; + case SkPaint::kLeft_Align: origin = 0; break; + } + + SkAutoKern autokern; + const char* stop = text + byteLength; + while (textPtr < stop) { + // don't need x, y here, since all subpixel variants will have the + // same advance + const SkGlyph& glyph = glyphCacheProc(origPaintCache, &textPtr, 0, 0); + + SkFixed width = glyph.fAdvanceX + autokern.adjust(glyph); + positions.push_back(SkFixedToScalar(stopX + SkFixedMul(origin, width))); + + SkFixed height = glyph.fAdvanceY; + positions.push_back(SkFixedToScalar(stopY + SkFixedMul(origin, height))); + + stopX += width; + stopY += height; + } + SkASSERT(textPtr == stop); + + SkGlyphCache::AttachCache(origPaintCache); + + // now adjust starting point depending on alignment + SkScalar alignX = SkFixedToScalar(stopX); + SkScalar alignY = SkFixedToScalar(stopY); + if (origPaint.getTextAlign() == SkPaint::kCenter_Align) { + alignX = SkScalarHalf(alignX); + alignY = SkScalarHalf(alignY); + } else if (origPaint.getTextAlign() == SkPaint::kLeft_Align) { + alignX = 0; + alignY = 0; + } + x -= alignX; + y -= alignY; + *offset = SkPoint::Make(x, y); + + this->internalDrawDFPosText(blob, runIndex, skPaint, color, viewMatrix, text, byteLength, + positions.begin(), 2, *offset, textRatio, fallbackTxt, + fallbackPos); +} + +void GrAtlasTextContext::internalDrawDFPosText(GrAtlasTextBlob* blob, int runIndex, + const SkPaint& skPaint, GrColor color, + const SkMatrix& viewMatrix, + const char text[], size_t byteLength, + const SkScalar pos[], int scalarsPerPosition, + const SkPoint& offset, + SkScalar textRatio, + SkTDArray<char>* fallbackTxt, + SkTDArray<SkScalar>* fallbackPos) { + + SkASSERT(byteLength == 0 || text != nullptr); + SkASSERT(1 == scalarsPerPosition || 2 == scalarsPerPosition); + + // nothing to draw + if (text == nullptr || byteLength == 0) { + return; + } + + fCurrStrike = nullptr; + + SkDrawCacheProc glyphCacheProc = skPaint.getDrawCacheProc(); + SkGlyphCache* cache = this->setupCache(&blob->fRuns[runIndex], skPaint, nullptr, true); + GrFontScaler* fontScaler = GetGrFontScaler(cache); + + const char* stop = text + byteLength; + + if (SkPaint::kLeft_Align == skPaint.getTextAlign()) { + while (text < stop) { + const char* lastText = text; + // the last 2 parameters are ignored + const SkGlyph& glyph = glyphCacheProc(cache, &text, 0, 0); + + if (glyph.fWidth) { + SkScalar x = offset.x() + pos[0]; + SkScalar y = offset.y() + (2 == scalarsPerPosition ? pos[1] : 0); + + if (!this->dfAppendGlyph(blob, + runIndex, + glyph, + x, y, color, fontScaler, + textRatio, viewMatrix)) { + // couldn't append, send to fallback + fallbackTxt->append(SkToInt(text-lastText), lastText); + *fallbackPos->append() = pos[0]; + if (2 == scalarsPerPosition) { + *fallbackPos->append() = pos[1]; + } + } + } + pos += scalarsPerPosition; + } + } else { + SkScalar alignMul = SkPaint::kCenter_Align == skPaint.getTextAlign() ? SK_ScalarHalf + : SK_Scalar1; + while (text < stop) { + const char* lastText = text; + // the last 2 parameters are ignored + const SkGlyph& glyph = glyphCacheProc(cache, &text, 0, 0); + + if (glyph.fWidth) { + SkScalar x = offset.x() + pos[0]; + SkScalar y = offset.y() + (2 == scalarsPerPosition ? pos[1] : 0); + + SkScalar advanceX = SkFixedToScalar(glyph.fAdvanceX) * alignMul * textRatio; + SkScalar advanceY = SkFixedToScalar(glyph.fAdvanceY) * alignMul * textRatio; + + if (!this->dfAppendGlyph(blob, + runIndex, + glyph, + x - advanceX, y - advanceY, color, + fontScaler, + textRatio, + viewMatrix)) { + // couldn't append, send to fallback + fallbackTxt->append(SkToInt(text-lastText), lastText); + *fallbackPos->append() = pos[0]; + if (2 == scalarsPerPosition) { + *fallbackPos->append() = pos[1]; + } + } + } + pos += scalarsPerPosition; + } + } + + SkGlyphCache::AttachCache(cache); +} + +bool GrAtlasTextContext::dfAppendGlyph(GrAtlasTextBlob* blob, int runIndex, + const SkGlyph& skGlyph, + SkScalar sx, SkScalar sy, GrColor color, + GrFontScaler* scaler, + SkScalar textRatio, const SkMatrix& viewMatrix) { + if (!fCurrStrike) { + fCurrStrike = fContext->getBatchFontCache()->getStrike(scaler); + } + + GrGlyph::PackedID id = GrGlyph::Pack(skGlyph.getGlyphID(), + skGlyph.getSubXFixed(), + skGlyph.getSubYFixed(), + GrGlyph::kDistance_MaskStyle); + GrGlyph* glyph = fCurrStrike->getGlyph(skGlyph, id, scaler); + if (!glyph) { + return true; + } + + // fallback to color glyph support + if (kA8_GrMaskFormat != glyph->fMaskFormat) { + return false; + } + + SkScalar dx = SkIntToScalar(glyph->fBounds.fLeft + SK_DistanceFieldInset); + SkScalar dy = SkIntToScalar(glyph->fBounds.fTop + SK_DistanceFieldInset); + SkScalar width = SkIntToScalar(glyph->fBounds.width() - 2 * SK_DistanceFieldInset); + SkScalar height = SkIntToScalar(glyph->fBounds.height() - 2 * SK_DistanceFieldInset); + + SkScalar scale = textRatio; + dx *= scale; + dy *= scale; + width *= scale; + height *= scale; + sx += dx; + sy += dy; + SkRect glyphRect = SkRect::MakeXYWH(sx, sy, width, height); + + blob->appendGlyph(runIndex, glyphRect, color, fCurrStrike, glyph, scaler, skGlyph, + sx - dx, sy - dy, scale, true); + return true; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// + +#ifdef GR_TEST_UTILS + +DRAW_BATCH_TEST_DEFINE(TextBlobBatch) { + static uint32_t gContextID = SK_InvalidGenID; + static GrAtlasTextContext* gTextContext = nullptr; + static SkSurfaceProps gSurfaceProps(SkSurfaceProps::kLegacyFontHost_InitType); + + if (context->uniqueID() != gContextID) { + gContextID = context->uniqueID(); + delete gTextContext; + + // We don't yet test the fall back to paths in the GrTextContext base class. This is mostly + // because we don't really want to have a gpu device here. + // We enable distance fields by twiddling a knob on the paint + gTextContext = GrAtlasTextContext::Create(context, gSurfaceProps); + } + + // Setup dummy SkPaint / GrPaint + GrColor color = GrRandomColor(random); + SkMatrix viewMatrix = GrTest::TestMatrixInvertible(random); + SkPaint skPaint; + skPaint.setColor(color); + skPaint.setLCDRenderText(random->nextBool()); + skPaint.setAntiAlias(skPaint.isLCDRenderText() ? true : random->nextBool()); + skPaint.setSubpixelText(random->nextBool()); + + GrPaint grPaint; + if (!SkPaintToGrPaint(context, skPaint, viewMatrix, &grPaint)) { + SkFAIL("couldn't convert paint\n"); + } + + const char* text = "The quick brown fox jumps over the lazy dog."; + int textLen = (int)strlen(text); + + // Setup clip + GrClip clip; + SkIRect noClip = SkIRect::MakeLargest(); + + // right now we don't handle textblobs, nor do we handle drawPosText. Since we only + // intend to test the batch with this unit test, that is okay. + SkAutoTUnref<GrAtlasTextBlob> blob( + gTextContext->createDrawTextBlob(clip, grPaint, skPaint, viewMatrix, text, + static_cast<size_t>(textLen), 0, 0, noClip)); + + SkScalar transX = static_cast<SkScalar>(random->nextU()); + SkScalar transY = static_cast<SkScalar>(random->nextU()); + const GrAtlasTextBlob::Run::SubRunInfo& info = blob->fRuns[0].fSubRunInfo[0]; + return blob->createBatch(info, textLen, 0, 0, color, transX, transY, skPaint, + gSurfaceProps, gTextContext->dfAdjustTable(), + context->getBatchFontCache()); +} + +#endif diff --git a/src/gpu/text/GrAtlasTextContext.h b/src/gpu/text/GrAtlasTextContext.h new file mode 100644 index 0000000000..47f05404de --- /dev/null +++ b/src/gpu/text/GrAtlasTextContext.h @@ -0,0 +1,131 @@ +/* + * Copyright 2015 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef GrAtlasTextContext_DEFINED +#define GrAtlasTextContext_DEFINED + +#include "GrTextContext.h" + +#include "GrAtlasTextBlob.h" +#include "GrDistanceFieldAdjustTable.h" +#include "GrGeometryProcessor.h" +#include "SkTextBlobRunIterator.h" + +#ifdef GR_TEST_UTILS +#include "GrBatchTest.h" +#endif + +class GrDrawBatch; +class GrDrawContext; +class GrDrawTarget; +class GrPipelineBuilder; +class GrTextBlobCache; +class SkGlyph; + +/* + * This class implements GrTextContext using standard bitmap fonts, and can also process textblobs. + */ +class GrAtlasTextContext : public GrTextContext { +public: + static GrAtlasTextContext* Create(GrContext*, const SkSurfaceProps&); + +private: + GrAtlasTextContext(GrContext*, const SkSurfaceProps&); + ~GrAtlasTextContext() override {} + + bool canDraw(const SkPaint&, const SkMatrix& viewMatrix) override; + + void onDrawText(GrDrawContext*, const GrClip&, const GrPaint&, const SkPaint&, + const SkMatrix& viewMatrix, const char text[], size_t byteLength, + SkScalar x, SkScalar y, const SkIRect& regionClipBounds) override; + void onDrawPosText(GrDrawContext*, const GrClip&, const GrPaint&, + const SkPaint&, const SkMatrix& viewMatrix, + const char text[], size_t byteLength, + const SkScalar pos[], int scalarsPerPosition, + const SkPoint& offset, const SkIRect& regionClipBounds) override; + void drawTextBlob(GrDrawContext*, const GrClip&, const SkPaint&, + const SkMatrix& viewMatrix, const SkTextBlob*, SkScalar x, SkScalar y, + SkDrawFilter*, const SkIRect& clipBounds) override; + + typedef GrAtlasTextBlob::Run Run; + typedef Run::SubRunInfo PerSubRunInfo; + + inline bool canDrawAsDistanceFields(const SkPaint&, const SkMatrix& viewMatrix); + GrAtlasTextBlob* setupDFBlob(int glyphCount, const SkPaint& origPaint, + const SkMatrix& viewMatrix, SkPaint* dfPaint, + SkScalar* textRatio); + void bmpAppendGlyph(GrAtlasTextBlob*, int runIndex, const SkGlyph&, int left, int top, + GrColor color, GrFontScaler*); + bool dfAppendGlyph(GrAtlasTextBlob*, int runIndex, const SkGlyph&, SkScalar sx, SkScalar sy, + GrColor color, GrFontScaler*, SkScalar textRatio, + const SkMatrix& viewMatrix); + + // A helper for drawing BitmapText in a run of distance fields + inline void fallbackDrawPosText(GrAtlasTextBlob*, int runIndex, + const GrClip&, GrColor color, + const SkPaint&, const SkMatrix& viewMatrix, + const SkTDArray<char>& fallbackTxt, + const SkTDArray<SkScalar>& fallbackPos, + int scalarsPerPosition, + const SkPoint& offset); + + void internalDrawDFText(GrAtlasTextBlob*, int runIndex, const SkPaint&, + GrColor color, const SkMatrix& viewMatrix, + const char text[], size_t byteLength, + SkScalar x, SkScalar y, + SkScalar textRatio, + SkTDArray<char>* fallbackTxt, + SkTDArray<SkScalar>* fallbackPos, + SkPoint* offset, const SkPaint& origPaint); + void internalDrawDFPosText(GrAtlasTextBlob*, int runIndex, const SkPaint&, + GrColor color, const SkMatrix& viewMatrix, + const char text[], size_t byteLength, + const SkScalar pos[], int scalarsPerPosition, + const SkPoint& offset, + SkScalar textRatio, + SkTDArray<char>* fallbackTxt, + SkTDArray<SkScalar>* fallbackPos); + + // sets up the descriptor on the blob and returns a detached cache. Client must attach + inline static GrColor ComputeCanonicalColor(const SkPaint&, bool lcd); + inline SkGlyphCache* setupCache(Run*, const SkPaint&, const SkMatrix* viewMatrix, bool noGamma); + void regenerateTextBlob(GrAtlasTextBlob* bmp, const SkPaint& skPaint, GrColor, + const SkMatrix& viewMatrix, + const SkTextBlob* blob, SkScalar x, SkScalar y, + SkDrawFilter* drawFilter, + const GrClip&); + inline static bool HasLCD(const SkTextBlob*); + inline void initDistanceFieldPaint(GrAtlasTextBlob*, SkPaint*, SkScalar* textRatio, + const SkMatrix&); + + // Test methods + // TODO this is really ugly. It'd be much nicer if positioning could be moved to batch + inline GrAtlasTextBlob* createDrawTextBlob(const GrClip&, const GrPaint&, + const SkPaint&, const SkMatrix& viewMatrix, + const char text[], size_t byteLength, + SkScalar x, SkScalar y, + const SkIRect& regionClipBounds); + inline GrAtlasTextBlob* createDrawPosTextBlob(const GrClip&, const GrPaint&, + const SkPaint&, const SkMatrix& viewMatrix, + const char text[], size_t byteLength, + const SkScalar pos[], int scalarsPerPosition, + const SkPoint& offset, + const SkIRect& regionClipBounds); + const GrDistanceFieldAdjustTable* dfAdjustTable() const { return fDistanceAdjustTable; } + + GrBatchTextStrike* fCurrStrike; + GrTextBlobCache* fCache; + SkAutoTUnref<const GrDistanceFieldAdjustTable> fDistanceAdjustTable; + +#ifdef GR_TEST_UTILS + DRAW_BATCH_TEST_FRIEND(TextBlobBatch); +#endif + + typedef GrTextContext INHERITED; +}; + +#endif diff --git a/src/gpu/text/GrBatchFontCache.cpp b/src/gpu/text/GrBatchFontCache.cpp new file mode 100644 index 0000000000..ad76e4d71f --- /dev/null +++ b/src/gpu/text/GrBatchFontCache.cpp @@ -0,0 +1,236 @@ +/* + * Copyright 2015 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#include "GrBatchFontCache.h" +#include "GrContext.h" +#include "GrGpu.h" +#include "GrRectanizer.h" +#include "GrResourceProvider.h" +#include "GrSurfacePriv.h" +#include "SkString.h" + +#include "SkDistanceFieldGen.h" + +/////////////////////////////////////////////////////////////////////////////// + +bool GrBatchFontCache::initAtlas(GrMaskFormat format) { + int index = MaskFormatToAtlasIndex(format); + if (!fAtlases[index]) { + GrPixelConfig config = MaskFormatToPixelConfig(format); + int width = fAtlasConfigs[index].fWidth; + int height = fAtlasConfigs[index].fHeight; + int numPlotsX = fAtlasConfigs[index].numPlotsX(); + int numPlotsY = fAtlasConfigs[index].numPlotsY(); + + fAtlases[index] = + fContext->resourceProvider()->createAtlas(config, width, height, + numPlotsX, numPlotsY, + &GrBatchFontCache::HandleEviction, + (void*)this); + if (!fAtlases[index]) { + return false; + } + } + return true; +} + +GrBatchFontCache::GrBatchFontCache(GrContext* context) + : fContext(context) + , fPreserveStrike(nullptr) { + for (int i = 0; i < kMaskFormatCount; ++i) { + fAtlases[i] = nullptr; + } + + // setup default atlas configs + fAtlasConfigs[kA8_GrMaskFormat].fWidth = 2048; + fAtlasConfigs[kA8_GrMaskFormat].fHeight = 2048; + fAtlasConfigs[kA8_GrMaskFormat].fPlotWidth = 512; + fAtlasConfigs[kA8_GrMaskFormat].fPlotHeight = 256; + + fAtlasConfigs[kA565_GrMaskFormat].fWidth = 1024; + fAtlasConfigs[kA565_GrMaskFormat].fHeight = 2048; + fAtlasConfigs[kA565_GrMaskFormat].fPlotWidth = 256; + fAtlasConfigs[kA565_GrMaskFormat].fPlotHeight = 256; + + fAtlasConfigs[kARGB_GrMaskFormat].fWidth = 1024; + fAtlasConfigs[kARGB_GrMaskFormat].fHeight = 2048; + fAtlasConfigs[kARGB_GrMaskFormat].fPlotWidth = 256; + fAtlasConfigs[kARGB_GrMaskFormat].fPlotHeight = 256; +} + +GrBatchFontCache::~GrBatchFontCache() { + SkTDynamicHash<GrBatchTextStrike, GrFontDescKey>::Iter iter(&fCache); + while (!iter.done()) { + (*iter).fIsAbandoned = true; + (*iter).unref(); + ++iter; + } + for (int i = 0; i < kMaskFormatCount; ++i) { + delete fAtlases[i]; + } +} + +void GrBatchFontCache::freeAll() { + SkTDynamicHash<GrBatchTextStrike, GrFontDescKey>::Iter iter(&fCache); + while (!iter.done()) { + (*iter).fIsAbandoned = true; + (*iter).unref(); + ++iter; + } + fCache.rewind(); + for (int i = 0; i < kMaskFormatCount; ++i) { + delete fAtlases[i]; + fAtlases[i] = nullptr; + } +} + +void GrBatchFontCache::HandleEviction(GrBatchAtlas::AtlasID id, void* ptr) { + GrBatchFontCache* fontCache = reinterpret_cast<GrBatchFontCache*>(ptr); + + SkTDynamicHash<GrBatchTextStrike, GrFontDescKey>::Iter iter(&fontCache->fCache); + for (; !iter.done(); ++iter) { + GrBatchTextStrike* strike = &*iter; + strike->removeID(id); + + // clear out any empty strikes. We will preserve the strike whose call to addToAtlas + // triggered the eviction + if (strike != fontCache->fPreserveStrike && 0 == strike->fAtlasedGlyphs) { + fontCache->fCache.remove(*(strike->fFontScalerKey)); + strike->fIsAbandoned = true; + strike->unref(); + } + } +} + +void GrBatchFontCache::dump() const { + static int gDumpCount = 0; + for (int i = 0; i < kMaskFormatCount; ++i) { + if (fAtlases[i]) { + GrTexture* texture = fAtlases[i]->getTexture(); + if (texture) { + SkString filename; +#ifdef SK_BUILD_FOR_ANDROID + filename.printf("/sdcard/fontcache_%d%d.png", gDumpCount, i); +#else + filename.printf("fontcache_%d%d.png", gDumpCount, i); +#endif + texture->surfacePriv().savePixels(filename.c_str()); + } + } + } + ++gDumpCount; +} + +void GrBatchFontCache::setAtlasSizes_ForTesting(const GrBatchAtlasConfig configs[3]) { + // delete any old atlases, this should be safe to do as long as we are not in the middle of a + // flush + for (int i = 0; i < kMaskFormatCount; i++) { + if (fAtlases[i]) { + delete fAtlases[i]; + fAtlases[i] = nullptr; + } + } + memcpy(fAtlasConfigs, configs, sizeof(fAtlasConfigs)); +} + +/////////////////////////////////////////////////////////////////////////////// + +/* + The text strike is specific to a given font/style/matrix setup, which is + represented by the GrHostFontScaler object we are given in getGlyph(). + + We map a 32bit glyphID to a GrGlyph record, which in turn points to a + atlas and a position within that texture. + */ + +GrBatchTextStrike::GrBatchTextStrike(GrBatchFontCache* cache, const GrFontDescKey* key) + : fFontScalerKey(SkRef(key)) + , fPool(9/*start allocations at 512 bytes*/) + , fAtlasedGlyphs(0) + , fIsAbandoned(false) { + + fBatchFontCache = cache; // no need to ref, it won't go away before we do +} + +GrBatchTextStrike::~GrBatchTextStrike() { + SkTDynamicHash<GrGlyph, GrGlyph::PackedID>::Iter iter(&fCache); + while (!iter.done()) { + (*iter).free(); + ++iter; + } +} + +GrGlyph* GrBatchTextStrike::generateGlyph(const SkGlyph& skGlyph, GrGlyph::PackedID packed, + GrFontScaler* scaler) { + SkIRect bounds; + if (GrGlyph::kDistance_MaskStyle == GrGlyph::UnpackMaskStyle(packed)) { + if (!scaler->getPackedGlyphDFBounds(skGlyph, &bounds)) { + return nullptr; + } + } else { + if (!scaler->getPackedGlyphBounds(skGlyph, &bounds)) { + return nullptr; + } + } + GrMaskFormat format = scaler->getPackedGlyphMaskFormat(skGlyph); + + GrGlyph* glyph = (GrGlyph*)fPool.alloc(sizeof(GrGlyph)); + glyph->init(packed, bounds, format); + fCache.add(glyph); + return glyph; +} + +void GrBatchTextStrike::removeID(GrBatchAtlas::AtlasID id) { + SkTDynamicHash<GrGlyph, GrGlyph::PackedID>::Iter iter(&fCache); + while (!iter.done()) { + if (id == (*iter).fID) { + (*iter).fID = GrBatchAtlas::kInvalidAtlasID; + fAtlasedGlyphs--; + SkASSERT(fAtlasedGlyphs >= 0); + } + ++iter; + } +} + +bool GrBatchTextStrike::addGlyphToAtlas(GrDrawBatch::Target* target, + GrGlyph* glyph, + GrFontScaler* scaler, + GrMaskFormat expectedMaskFormat) { + SkASSERT(glyph); + SkASSERT(scaler); + SkASSERT(fCache.find(glyph->fPackedID)); + + SkAutoUnref ar(SkSafeRef(scaler)); + + int bytesPerPixel = GrMaskFormatBytesPerPixel(expectedMaskFormat); + + size_t size = glyph->fBounds.area() * bytesPerPixel; + SkAutoSMalloc<1024> storage(size); + + const SkGlyph& skGlyph = scaler->grToSkGlyph(glyph->fPackedID); + if (GrGlyph::kDistance_MaskStyle == GrGlyph::UnpackMaskStyle(glyph->fPackedID)) { + if (!scaler->getPackedGlyphDFImage(skGlyph, glyph->width(), glyph->height(), + storage.get())) { + return false; + } + } else { + if (!scaler->getPackedGlyphImage(skGlyph, glyph->width(), glyph->height(), + glyph->width() * bytesPerPixel, expectedMaskFormat, + storage.get())) { + return false; + } + } + + bool success = fBatchFontCache->addToAtlas(this, &glyph->fID, target, expectedMaskFormat, + glyph->width(), glyph->height(), + storage.get(), &glyph->fAtlasLocation); + if (success) { + SkASSERT(GrBatchAtlas::kInvalidAtlasID != glyph->fID); + fAtlasedGlyphs++; + } + return success; +} diff --git a/src/gpu/text/GrBatchFontCache.h b/src/gpu/text/GrBatchFontCache.h new file mode 100644 index 0000000000..46ab1c8274 --- /dev/null +++ b/src/gpu/text/GrBatchFontCache.h @@ -0,0 +1,228 @@ +/* + * Copyright 2015 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef GrBatchFontCache_DEFINED +#define GrBatchFontCache_DEFINED + +#include "GrBatchAtlas.h" +#include "GrFontScaler.h" +#include "GrGlyph.h" +#include "SkGlyph.h" +#include "SkTDynamicHash.h" +#include "SkVarAlloc.h" + +class GrBatchFontCache; +class GrGpu; + +/** + * The GrBatchTextStrike manages a pool of CPU backing memory for GrGlyphs. This backing memory + * is indexed by a PackedID and GrFontScaler. The GrFontScaler is what actually creates the mask. + */ +class GrBatchTextStrike : public SkNVRefCnt<GrBatchTextStrike> { +public: + GrBatchTextStrike(GrBatchFontCache*, const GrFontDescKey* fontScalerKey); + ~GrBatchTextStrike(); + + const GrFontDescKey* getFontScalerKey() const { return fFontScalerKey; } + GrBatchFontCache* getBatchFontCache() const { return fBatchFontCache; } + + inline GrGlyph* getGlyph(const SkGlyph& skGlyph, GrGlyph::PackedID packed, + GrFontScaler* scaler) { + GrGlyph* glyph = fCache.find(packed); + if (nullptr == glyph) { + glyph = this->generateGlyph(skGlyph, packed, scaler); + } + return glyph; + } + + // This variant of the above function is called by TextBatch. At this point, it is possible + // that the maskformat of the glyph differs from what we expect. In these cases we will just + // draw a clear square. + // skbug:4143 crbug:510931 + inline GrGlyph* getGlyph(GrGlyph::PackedID packed, + GrMaskFormat expectedMaskFormat, + GrFontScaler* scaler) { + GrGlyph* glyph = fCache.find(packed); + if (nullptr == glyph) { + // We could return this to the caller, but in practice it adds code complexity for + // potentially little benefit(ie, if the glyph is not in our font cache, then its not + // in the atlas and we're going to be doing a texture upload anyways). + const SkGlyph& skGlyph = scaler->grToSkGlyph(packed); + glyph = this->generateGlyph(skGlyph, packed, scaler); + glyph->fMaskFormat = expectedMaskFormat; + } + return glyph; + } + + // returns true if glyph successfully added to texture atlas, false otherwise. If the glyph's + // mask format has changed, then addGlyphToAtlas will draw a clear box. This will almost never + // happen. + // TODO we can handle some of these cases if we really want to, but the long term solution is to + // get the actual glyph image itself when we get the glyph metrics. + bool addGlyphToAtlas(GrDrawBatch::Target*, GrGlyph*, GrFontScaler*, + GrMaskFormat expectedMaskFormat); + + // testing + int countGlyphs() const { return fCache.count(); } + + // remove any references to this plot + void removeID(GrBatchAtlas::AtlasID); + + // If a TextStrike is abandoned by the cache, then the caller must get a new strike + bool isAbandoned() const { return fIsAbandoned; } + + static const GrFontDescKey& GetKey(const GrBatchTextStrike& ts) { + return *(ts.fFontScalerKey); + } + static uint32_t Hash(const GrFontDescKey& key) { + return key.getHash(); + } + +private: + SkTDynamicHash<GrGlyph, GrGlyph::PackedID> fCache; + SkAutoTUnref<const GrFontDescKey> fFontScalerKey; + SkVarAlloc fPool; + + GrBatchFontCache* fBatchFontCache; + int fAtlasedGlyphs; + bool fIsAbandoned; + + GrGlyph* generateGlyph(const SkGlyph&, GrGlyph::PackedID, GrFontScaler*); + + friend class GrBatchFontCache; +}; + +/* + * GrBatchFontCache manages strikes which are indexed by a GrFontScaler. These strikes can then be + * used to individual Glyph Masks. The GrBatchFontCache also manages GrBatchAtlases, though this is + * more or less transparent to the client(aside from atlasGeneration, described below). + * Note - we used to initialize the backing atlas for the GrBatchFontCache at initialization time. + * However, this caused a regression, even when the GrBatchFontCache was unused. We now initialize + * the backing atlases lazily. Its not immediately clear why this improves the situation. + */ +class GrBatchFontCache { +public: + GrBatchFontCache(GrContext*); + ~GrBatchFontCache(); + // The user of the cache may hold a long-lived ref to the returned strike. However, actions by + // another client of the cache may cause the strike to be purged while it is still reffed. + // Therefore, the caller must check GrBatchTextStrike::isAbandoned() if there are other + // interactions with the cache since the strike was received. + inline GrBatchTextStrike* getStrike(GrFontScaler* scaler) { + GrBatchTextStrike* strike = fCache.find(*(scaler->getKey())); + if (nullptr == strike) { + strike = this->generateStrike(scaler); + } + return strike; + } + + void freeAll(); + + // if getTexture returns nullptr, the client must not try to use other functions on the + // GrBatchFontCache which use the atlas. This function *must* be called first, before other + // functions which use the atlas. + GrTexture* getTexture(GrMaskFormat format) { + if (this->initAtlas(format)) { + return this->getAtlas(format)->getTexture(); + } + return nullptr; + } + + bool hasGlyph(GrGlyph* glyph) { + SkASSERT(glyph); + return this->getAtlas(glyph->fMaskFormat)->hasID(glyph->fID); + } + + // To ensure the GrBatchAtlas does not evict the Glyph Mask from its texture backing store, + // the client must pass in the current batch token along with the GrGlyph. + // A BulkUseTokenUpdater is used to manage bulk last use token updating in the Atlas. + // For convenience, this function will also set the use token for the current glyph if required + // NOTE: the bulk uploader is only valid if the subrun has a valid atlasGeneration + void addGlyphToBulkAndSetUseToken(GrBatchAtlas::BulkUseTokenUpdater* updater, + GrGlyph* glyph, GrBatchToken token) { + SkASSERT(glyph); + updater->add(glyph->fID); + this->getAtlas(glyph->fMaskFormat)->setLastUseToken(glyph->fID, token); + } + + void setUseTokenBulk(const GrBatchAtlas::BulkUseTokenUpdater& updater, + GrBatchToken token, + GrMaskFormat format) { + this->getAtlas(format)->setLastUseTokenBulk(updater, token); + } + + // add to texture atlas that matches this format + bool addToAtlas(GrBatchTextStrike* strike, GrBatchAtlas::AtlasID* id, + GrDrawBatch::Target* target, + GrMaskFormat format, int width, int height, const void* image, + SkIPoint16* loc) { + fPreserveStrike = strike; + return this->getAtlas(format)->addToAtlas(id, target, width, height, image, loc); + } + + // Some clients may wish to verify the integrity of the texture backing store of the + // GrBatchAtlas. The atlasGeneration returned below is a monitonically increasing number which + // changes everytime something is removed from the texture backing store. + uint64_t atlasGeneration(GrMaskFormat format) const { + return this->getAtlas(format)->atlasGeneration(); + } + + /////////////////////////////////////////////////////////////////////////// + // Functions intended debug only + void dump() const; + + void setAtlasSizes_ForTesting(const GrBatchAtlasConfig configs[3]); + +private: + static GrPixelConfig MaskFormatToPixelConfig(GrMaskFormat format) { + static const GrPixelConfig kPixelConfigs[] = { + kAlpha_8_GrPixelConfig, + kRGB_565_GrPixelConfig, + kSkia8888_GrPixelConfig + }; + static_assert(SK_ARRAY_COUNT(kPixelConfigs) == kMaskFormatCount, "array_size_mismatch"); + + return kPixelConfigs[format]; + } + + // There is a 1:1 mapping between GrMaskFormats and atlas indices + static int MaskFormatToAtlasIndex(GrMaskFormat format) { + static const int sAtlasIndices[] = { + kA8_GrMaskFormat, + kA565_GrMaskFormat, + kARGB_GrMaskFormat, + }; + static_assert(SK_ARRAY_COUNT(sAtlasIndices) == kMaskFormatCount, "array_size_mismatch"); + + SkASSERT(sAtlasIndices[format] < kMaskFormatCount); + return sAtlasIndices[format]; + } + + bool initAtlas(GrMaskFormat); + + GrBatchTextStrike* generateStrike(GrFontScaler* scaler) { + GrBatchTextStrike* strike = new GrBatchTextStrike(this, scaler->getKey()); + fCache.add(strike); + return strike; + } + + GrBatchAtlas* getAtlas(GrMaskFormat format) const { + int atlasIndex = MaskFormatToAtlasIndex(format); + SkASSERT(fAtlases[atlasIndex]); + return fAtlases[atlasIndex]; + } + + static void HandleEviction(GrBatchAtlas::AtlasID, void*); + + GrContext* fContext; + SkTDynamicHash<GrBatchTextStrike, GrFontDescKey> fCache; + GrBatchAtlas* fAtlases[kMaskFormatCount]; + GrBatchTextStrike* fPreserveStrike; + GrBatchAtlasConfig fAtlasConfigs[kMaskFormatCount]; +}; + +#endif diff --git a/src/gpu/text/GrDistanceFieldAdjustTable.cpp b/src/gpu/text/GrDistanceFieldAdjustTable.cpp new file mode 100644 index 0000000000..1c5aeceb80 --- /dev/null +++ b/src/gpu/text/GrDistanceFieldAdjustTable.cpp @@ -0,0 +1,93 @@ +/* + * Copyright 2015 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#include "GrDistanceFieldAdjustTable.h" + +#include "SkScalerContext.h" + +SkDEBUGCODE(static const int kExpectedDistanceAdjustTableSize = 8;) + +void GrDistanceFieldAdjustTable::buildDistanceAdjustTable() { + // This is used for an approximation of the mask gamma hack, used by raster and bitmap + // text. The mask gamma hack is based off of guessing what the blend color is going to + // be, and adjusting the mask so that when run through the linear blend will + // produce the value closest to the desired result. However, in practice this means + // that the 'adjusted' mask is just increasing or decreasing the coverage of + // the mask depending on what it is thought it will blit against. For black (on + // assumed white) this means that coverages are decreased (on a curve). For white (on + // assumed black) this means that coverages are increased (on a a curve). At + // middle (perceptual) gray (which could be blit against anything) the coverages + // remain the same. + // + // The idea here is that instead of determining the initial (real) coverage and + // then adjusting that coverage, we determine an adjusted coverage directly by + // essentially manipulating the geometry (in this case, the distance to the glyph + // edge). So for black (on assumed white) this thins a bit; for white (on + // assumed black) this fake bolds the geometry a bit. + // + // The distance adjustment is calculated by determining the actual coverage value which + // when fed into in the mask gamma table gives us an 'adjusted coverage' value of 0.5. This + // actual coverage value (assuming it's between 0 and 1) corresponds to a distance from the + // actual edge. So by subtracting this distance adjustment and computing without the + // the coverage adjustment we should get 0.5 coverage at the same point. + // + // This has several implications: + // For non-gray lcd smoothed text, each subpixel essentially is using a + // slightly different geometry. + // + // For black (on assumed white) this may not cover some pixels which were + // previously covered; however those pixels would have been only slightly + // covered and that slight coverage would have been decreased anyway. Also, some pixels + // which were previously fully covered may no longer be fully covered. + // + // For white (on assumed black) this may cover some pixels which weren't + // previously covered at all. + + int width, height; + size_t size; + +#ifdef SK_GAMMA_CONTRAST + SkScalar contrast = SK_GAMMA_CONTRAST; +#else + SkScalar contrast = 0.5f; +#endif + SkScalar paintGamma = SK_GAMMA_EXPONENT; + SkScalar deviceGamma = SK_GAMMA_EXPONENT; + + size = SkScalerContext::GetGammaLUTSize(contrast, paintGamma, deviceGamma, + &width, &height); + + SkASSERT(kExpectedDistanceAdjustTableSize == height); + fTable = new SkScalar[height]; + + SkAutoTArray<uint8_t> data((int)size); + SkScalerContext::GetGammaLUTData(contrast, paintGamma, deviceGamma, data.get()); + + // find the inverse points where we cross 0.5 + // binsearch might be better, but we only need to do this once on creation + for (int row = 0; row < height; ++row) { + uint8_t* rowPtr = data.get() + row*width; + for (int col = 0; col < width - 1; ++col) { + if (rowPtr[col] <= 127 && rowPtr[col + 1] >= 128) { + // compute point where a mask value will give us a result of 0.5 + float interp = (127.5f - rowPtr[col]) / (rowPtr[col + 1] - rowPtr[col]); + float borderAlpha = (col + interp) / 255.f; + + // compute t value for that alpha + // this is an approximate inverse for smoothstep() + float t = borderAlpha*(borderAlpha*(4.0f*borderAlpha - 6.0f) + 5.0f) / 3.0f; + + // compute distance which gives us that t value + const float kDistanceFieldAAFactor = 0.65f; // should match SK_DistanceFieldAAFactor + float d = 2.0f*kDistanceFieldAAFactor*t - kDistanceFieldAAFactor; + + fTable[row] = d; + break; + } + } + } +} diff --git a/src/gpu/text/GrDistanceFieldAdjustTable.h b/src/gpu/text/GrDistanceFieldAdjustTable.h new file mode 100644 index 0000000000..f7d8bee089 --- /dev/null +++ b/src/gpu/text/GrDistanceFieldAdjustTable.h @@ -0,0 +1,31 @@ +/* + * Copyright 2015 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef GrDistanceFieldAdjustTable_DEFINED +#define GrDistanceFieldAdjustTable_DEFINED + +#include "SkRefCnt.h" +#include "SkScalar.h" + +// Distance field text needs this table to compute a value for use in the fragment shader. +// Because the GrAtlasTextContext can go out of scope before the final flush, this needs to be +// refcnted and malloced +struct GrDistanceFieldAdjustTable : public SkNVRefCnt<GrDistanceFieldAdjustTable> { + GrDistanceFieldAdjustTable() { this->buildDistanceAdjustTable(); } + ~GrDistanceFieldAdjustTable() { delete[] fTable; } + + const SkScalar& operator[] (int i) const { + return fTable[i]; + } + +private: + void buildDistanceAdjustTable(); + + SkScalar* fTable; +}; + +#endif diff --git a/src/gpu/text/GrFontScaler.cpp b/src/gpu/text/GrFontScaler.cpp new file mode 100644 index 0000000000..c8412027c0 --- /dev/null +++ b/src/gpu/text/GrFontScaler.cpp @@ -0,0 +1,207 @@ + +/* + * Copyright 2010 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#include "GrFontScaler.h" +#include "SkDescriptor.h" +#include "SkDistanceFieldGen.h" +#include "SkGlyphCache.h" + +/////////////////////////////////////////////////////////////////////////////// + +GrFontScaler::GrFontScaler(SkGlyphCache* strike) { + fStrike = strike; + fKey = nullptr; +} + +GrFontScaler::~GrFontScaler() { + SkSafeUnref(fKey); +} + +GrMaskFormat GrFontScaler::getMaskFormat() const { + SkMask::Format format = fStrike->getMaskFormat(); + switch (format) { + case SkMask::kBW_Format: + // fall through to kA8 -- we store BW glyphs in our 8-bit cache + case SkMask::kA8_Format: + return kA8_GrMaskFormat; + case SkMask::kLCD16_Format: + return kA565_GrMaskFormat; + case SkMask::kARGB32_Format: + return kARGB_GrMaskFormat; + default: + SkDEBUGFAIL("unsupported SkMask::Format"); + return kA8_GrMaskFormat; + } +} + +const GrFontDescKey* GrFontScaler::getKey() { + if (nullptr == fKey) { + fKey = new GrFontDescKey(fStrike->getDescriptor()); + } + return fKey; +} + +GrMaskFormat GrFontScaler::getPackedGlyphMaskFormat(const SkGlyph& glyph) const { + SkMask::Format format = static_cast<SkMask::Format>(glyph.fMaskFormat); + switch (format) { + case SkMask::kBW_Format: + // fall through to kA8 -- we store BW glyphs in our 8-bit cache + case SkMask::kA8_Format: + return kA8_GrMaskFormat; + case SkMask::kLCD16_Format: + return kA565_GrMaskFormat; + case SkMask::kARGB32_Format: + return kARGB_GrMaskFormat; + default: + SkDEBUGFAIL("unsupported SkMask::Format"); + return kA8_GrMaskFormat; + } +} + +bool GrFontScaler::getPackedGlyphBounds(const SkGlyph& glyph, SkIRect* bounds) { +#if 1 + // crbug:510931 + // Retrieving the image from the cache can actually change the mask format. + fStrike->findImage(glyph); +#endif + bounds->setXYWH(glyph.fLeft, glyph.fTop, glyph.fWidth, glyph.fHeight); + + return true; +} + +bool GrFontScaler::getPackedGlyphDFBounds(const SkGlyph& glyph, SkIRect* bounds) { +#if 1 + // crbug:510931 + // Retrieving the image from the cache can actually change the mask format. + fStrike->findImage(glyph); +#endif + bounds->setXYWH(glyph.fLeft, glyph.fTop, glyph.fWidth, glyph.fHeight); + bounds->outset(SK_DistanceFieldPad, SK_DistanceFieldPad); + + return true; +} + +namespace { +// expands each bit in a bitmask to 0 or ~0 of type INT_TYPE. Used to expand a BW glyph mask to +// A8, RGB565, or RGBA8888. +template <typename INT_TYPE> +void expand_bits(INT_TYPE* dst, + const uint8_t* src, + int width, + int height, + int dstRowBytes, + int srcRowBytes) { + for (int i = 0; i < height; ++i) { + int rowWritesLeft = width; + const uint8_t* s = src; + INT_TYPE* d = dst; + while (rowWritesLeft > 0) { + unsigned mask = *s++; + for (int i = 7; i >= 0 && rowWritesLeft; --i, --rowWritesLeft) { + *d++ = (mask & (1 << i)) ? (INT_TYPE)(~0UL) : 0; + } + } + dst = reinterpret_cast<INT_TYPE*>(reinterpret_cast<intptr_t>(dst) + dstRowBytes); + src += srcRowBytes; + } +} +} + +bool GrFontScaler::getPackedGlyphImage(const SkGlyph& glyph, int width, int height, int dstRB, + GrMaskFormat expectedMaskFormat, void* dst) { + SkASSERT(glyph.fWidth == width); + SkASSERT(glyph.fHeight == height); + const void* src = fStrike->findImage(glyph); + if (nullptr == src) { + return false; + } + + // crbug:510931 + // Retrieving the image from the cache can actually change the mask format. This case is very + // uncommon so for now we just draw a clear box for these glyphs. + if (getPackedGlyphMaskFormat(glyph) != expectedMaskFormat) { + const int bpp = GrMaskFormatBytesPerPixel(expectedMaskFormat); + for (int y = 0; y < height; y++) { + sk_bzero(dst, width * bpp); + dst = (char*)dst + dstRB; + } + return true; + } + + int srcRB = glyph.rowBytes(); + // The windows font host sometimes has BW glyphs in a non-BW strike. So it is important here to + // check the glyph's format, not the strike's format, and to be able to convert to any of the + // GrMaskFormats. + if (SkMask::kBW_Format == glyph.fMaskFormat) { + // expand bits to our mask type + const uint8_t* bits = reinterpret_cast<const uint8_t*>(src); + switch (expectedMaskFormat) { + case kA8_GrMaskFormat:{ + uint8_t* bytes = reinterpret_cast<uint8_t*>(dst); + expand_bits(bytes, bits, width, height, dstRB, srcRB); + break; + } + case kA565_GrMaskFormat: { + uint16_t* rgb565 = reinterpret_cast<uint16_t*>(dst); + expand_bits(rgb565, bits, width, height, dstRB, srcRB); + break; + } + default: + SkFAIL("Invalid GrMaskFormat"); + } + } else if (srcRB == dstRB) { + memcpy(dst, src, dstRB * height); + } else { + const int bbp = GrMaskFormatBytesPerPixel(expectedMaskFormat); + for (int y = 0; y < height; y++) { + memcpy(dst, src, width * bbp); + src = (const char*)src + srcRB; + dst = (char*)dst + dstRB; + } + } + return true; +} + +bool GrFontScaler::getPackedGlyphDFImage(const SkGlyph& glyph, int width, int height, void* dst) { + SkASSERT(glyph.fWidth + 2*SK_DistanceFieldPad == width); + SkASSERT(glyph.fHeight + 2*SK_DistanceFieldPad == height); + const void* image = fStrike->findImage(glyph); + if (nullptr == image) { + return false; + } + // now generate the distance field + SkASSERT(dst); + SkMask::Format maskFormat = static_cast<SkMask::Format>(glyph.fMaskFormat); + if (SkMask::kA8_Format == maskFormat) { + // make the distance field from the image + SkGenerateDistanceFieldFromA8Image((unsigned char*)dst, + (unsigned char*)image, + glyph.fWidth, glyph.fHeight, + glyph.rowBytes()); + } else if (SkMask::kBW_Format == maskFormat) { + // make the distance field from the image + SkGenerateDistanceFieldFromBWImage((unsigned char*)dst, + (unsigned char*)image, + glyph.fWidth, glyph.fHeight, + glyph.rowBytes()); + } else { + return false; + } + + return true; +} + +const SkPath* GrFontScaler::getGlyphPath(const SkGlyph& glyph) { + return fStrike->findPath(glyph); +} + +const SkGlyph& GrFontScaler::grToSkGlyph(GrGlyph::PackedID id) { + return fStrike->getGlyphIDMetrics(GrGlyph::UnpackID(id), + GrGlyph::UnpackFixedX(id), + GrGlyph::UnpackFixedY(id)); +} diff --git a/src/gpu/text/GrFontScaler.h b/src/gpu/text/GrFontScaler.h new file mode 100644 index 0000000000..e42c7a1da0 --- /dev/null +++ b/src/gpu/text/GrFontScaler.h @@ -0,0 +1,69 @@ +/* + * Copyright 2010 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef GrFontScaler_DEFINED +#define GrFontScaler_DEFINED + +#include "GrGlyph.h" +#include "GrTypes.h" + +#include "SkDescriptor.h" + +class SkGlyph; +class SkPath; + +/* + * Wrapper class to turn a font cache descriptor into a key + * for GrFontScaler-related lookups + */ +class GrFontDescKey : public SkRefCnt { +public: + explicit GrFontDescKey(const SkDescriptor& desc) : fDesc(desc), fHash(desc.getChecksum()) {} + + uint32_t getHash() const { return fHash; } + + bool operator==(const GrFontDescKey& rh) const { + return fHash == rh.fHash && fDesc.getDesc()->equals(*rh.fDesc.getDesc()); + } + +private: + SkAutoDescriptor fDesc; + uint32_t fHash; + + typedef SkRefCnt INHERITED; +}; + +/* + * This is Gr's interface to the host platform's font scaler. + * + * The client is responsible for instantiating this. The instance is created + * for a specific font+size+matrix. + */ +class GrFontScaler : public SkRefCnt { +public: + explicit GrFontScaler(SkGlyphCache* strike); + virtual ~GrFontScaler(); + + const GrFontDescKey* getKey(); + GrMaskFormat getMaskFormat() const; + GrMaskFormat getPackedGlyphMaskFormat(const SkGlyph&) const; + bool getPackedGlyphBounds(const SkGlyph&, SkIRect* bounds); + bool getPackedGlyphImage(const SkGlyph&, int width, int height, int rowBytes, + GrMaskFormat expectedMaskFormat, void* image); + bool getPackedGlyphDFBounds(const SkGlyph&, SkIRect* bounds); + bool getPackedGlyphDFImage(const SkGlyph&, int width, int height, void* image); + const SkPath* getGlyphPath(const SkGlyph&); + const SkGlyph& grToSkGlyph(GrGlyph::PackedID); + +private: + SkGlyphCache* fStrike; + GrFontDescKey* fKey; + + typedef SkRefCnt INHERITED; +}; + +#endif diff --git a/src/gpu/text/GrStencilAndCoverTextContext.cpp b/src/gpu/text/GrStencilAndCoverTextContext.cpp new file mode 100644 index 0000000000..d28f1a803a --- /dev/null +++ b/src/gpu/text/GrStencilAndCoverTextContext.cpp @@ -0,0 +1,616 @@ +/* + * Copyright 2014 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#include "GrStencilAndCoverTextContext.h" +#include "GrAtlasTextContext.h" +#include "GrDrawContext.h" +#include "GrDrawTarget.h" +#include "GrPath.h" +#include "GrPathRange.h" +#include "GrResourceProvider.h" +#include "SkAutoKern.h" +#include "SkDraw.h" +#include "SkDrawProcs.h" +#include "SkGlyphCache.h" +#include "SkGpuDevice.h" +#include "SkGrPriv.h" +#include "SkPath.h" +#include "SkTextBlobRunIterator.h" +#include "SkTextMapStateProc.h" +#include "SkTextFormatParams.h" + +#include "batches/GrDrawPathBatch.h" + +template<typename Key, typename Val> static void delete_hash_map_entry(const Key&, Val* val) { + SkASSERT(*val); + delete *val; +} + +template<typename T> static void delete_hash_table_entry(T* val) { + SkASSERT(*val); + delete *val; +} + +GrStencilAndCoverTextContext::GrStencilAndCoverTextContext(GrContext* context, + const SkSurfaceProps& surfaceProps) + : INHERITED(context, surfaceProps), + fCacheSize(0) { +} + +GrStencilAndCoverTextContext* +GrStencilAndCoverTextContext::Create(GrContext* context, const SkSurfaceProps& surfaceProps) { + GrStencilAndCoverTextContext* textContext = + new GrStencilAndCoverTextContext(context, surfaceProps); + textContext->fFallbackTextContext = GrAtlasTextContext::Create(context, surfaceProps); + + return textContext; +} + +GrStencilAndCoverTextContext::~GrStencilAndCoverTextContext() { + fBlobIdCache.foreach(delete_hash_map_entry<uint32_t, TextBlob*>); + fBlobKeyCache.foreach(delete_hash_table_entry<TextBlob*>); +} + +bool GrStencilAndCoverTextContext::internalCanDraw(const SkPaint& skPaint) { + if (skPaint.getRasterizer()) { + return false; + } + if (skPaint.getMaskFilter()) { + return false; + } + if (SkPathEffect* pe = skPaint.getPathEffect()) { + if (pe->asADash(nullptr) != SkPathEffect::kDash_DashType) { + return false; + } + } + // No hairlines. They would require new paths with customized strokes for every new draw matrix. + return SkPaint::kStroke_Style != skPaint.getStyle() || 0 != skPaint.getStrokeWidth(); +} + +void GrStencilAndCoverTextContext::onDrawText(GrDrawContext* dc, + const GrClip& clip, + const GrPaint& paint, + const SkPaint& skPaint, + const SkMatrix& viewMatrix, + const char text[], + size_t byteLength, + SkScalar x, SkScalar y, + const SkIRect& clipBounds) { + TextRun run(skPaint); + GrPipelineBuilder pipelineBuilder(paint, dc->accessRenderTarget(), clip); + run.setText(text, byteLength, x, y); + run.draw(fContext, dc, &pipelineBuilder, paint.getColor(), viewMatrix, 0, 0, clipBounds, + fFallbackTextContext, skPaint); +} + +void GrStencilAndCoverTextContext::onDrawPosText(GrDrawContext* dc, + const GrClip& clip, + const GrPaint& paint, + const SkPaint& skPaint, + const SkMatrix& viewMatrix, + const char text[], + size_t byteLength, + const SkScalar pos[], + int scalarsPerPosition, + const SkPoint& offset, + const SkIRect& clipBounds) { + TextRun run(skPaint); + GrPipelineBuilder pipelineBuilder(paint, dc->accessRenderTarget(), clip); + run.setPosText(text, byteLength, pos, scalarsPerPosition, offset); + run.draw(fContext, dc, &pipelineBuilder, paint.getColor(), viewMatrix, 0, 0, clipBounds, + fFallbackTextContext, skPaint); +} + +void GrStencilAndCoverTextContext::drawTextBlob(GrDrawContext* dc, + const GrClip& clip, const SkPaint& skPaint, + const SkMatrix& viewMatrix, + const SkTextBlob* skBlob, SkScalar x, SkScalar y, + SkDrawFilter* drawFilter, + const SkIRect& clipBounds) { + if (!this->internalCanDraw(skPaint)) { + fFallbackTextContext->drawTextBlob(dc, clip, skPaint, viewMatrix, skBlob, x, y, + drawFilter, clipBounds); + return; + } + + if (drawFilter || skPaint.getPathEffect()) { + // This draw can't be cached. + INHERITED::drawTextBlob(dc, clip, skPaint, viewMatrix, skBlob, x, y, drawFilter, + clipBounds); + return; + } + + if (fContext->abandoned()) { + return; + } + + GrPaint paint; + if (!SkPaintToGrPaint(fContext, skPaint, viewMatrix, &paint)) { + return; + } + + const TextBlob& blob = this->findOrCreateTextBlob(skBlob, skPaint); + GrPipelineBuilder pipelineBuilder(paint, dc->accessRenderTarget(), clip); + + TextBlob::Iter iter(blob); + for (TextRun* run = iter.get(); run; run = iter.next()) { + run->draw(fContext, dc, &pipelineBuilder, paint.getColor(), viewMatrix, x, y, clipBounds, + fFallbackTextContext, skPaint); + run->releaseGlyphCache(); + } +} + +const GrStencilAndCoverTextContext::TextBlob& +GrStencilAndCoverTextContext::findOrCreateTextBlob(const SkTextBlob* skBlob, + const SkPaint& skPaint) { + // The font-related parameters are baked into the text blob and will override this skPaint, so + // the only remaining properties that can affect a TextBlob are the ones related to stroke. + if (SkPaint::kFill_Style == skPaint.getStyle()) { // Fast path. + if (TextBlob** found = fBlobIdCache.find(skBlob->uniqueID())) { + fLRUList.remove(*found); + fLRUList.addToTail(*found); + return **found; + } + TextBlob* blob = new TextBlob(skBlob->uniqueID(), skBlob, skPaint); + this->purgeToFit(*blob); + fBlobIdCache.set(skBlob->uniqueID(), blob); + fLRUList.addToTail(blob); + fCacheSize += blob->cpuMemorySize(); + return *blob; + } else { + GrStrokeInfo stroke(skPaint); + SkSTArray<4, uint32_t, true> key; + key.reset(1 + stroke.computeUniqueKeyFragmentData32Cnt()); + key[0] = skBlob->uniqueID(); + stroke.asUniqueKeyFragment(&key[1]); + if (TextBlob** found = fBlobKeyCache.find(key)) { + fLRUList.remove(*found); + fLRUList.addToTail(*found); + return **found; + } + TextBlob* blob = new TextBlob(key, skBlob, skPaint); + this->purgeToFit(*blob); + fBlobKeyCache.set(blob); + fLRUList.addToTail(blob); + fCacheSize += blob->cpuMemorySize(); + return *blob; + } +} + +void GrStencilAndCoverTextContext::purgeToFit(const TextBlob& blob) { + static const size_t maxCacheSize = 4 * 1024 * 1024; // Allow up to 4 MB for caching text blobs. + + size_t maxSizeForNewBlob = maxCacheSize - blob.cpuMemorySize(); + while (fCacheSize && fCacheSize > maxSizeForNewBlob) { + TextBlob* lru = fLRUList.head(); + if (1 == lru->key().count()) { + // 1-length keys are unterstood to be the blob id. + fBlobIdCache.remove(lru->key()[0]); + } else { + fBlobKeyCache.remove(lru->key()); + } + fLRUList.remove(lru); + fCacheSize -= lru->cpuMemorySize(); + delete lru; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +void GrStencilAndCoverTextContext::TextBlob::init(const SkTextBlob* skBlob, + const SkPaint& skPaint) { + fCpuMemorySize = sizeof(TextBlob); + SkPaint runPaint(skPaint); + for (SkTextBlobRunIterator iter(skBlob); !iter.done(); iter.next()) { + iter.applyFontToPaint(&runPaint); // No need to re-seed the paint. + TextRun* run = this->addToTail(runPaint); + + const char* text = reinterpret_cast<const char*>(iter.glyphs()); + size_t byteLength = sizeof(uint16_t) * iter.glyphCount(); + const SkPoint& runOffset = iter.offset(); + + switch (iter.positioning()) { + case SkTextBlob::kDefault_Positioning: + run->setText(text, byteLength, runOffset.fX, runOffset.fY); + break; + case SkTextBlob::kHorizontal_Positioning: + run->setPosText(text, byteLength, iter.pos(), 1, SkPoint::Make(0, runOffset.fY)); + break; + case SkTextBlob::kFull_Positioning: + run->setPosText(text, byteLength, iter.pos(), 2, SkPoint::Make(0, 0)); + break; + } + + fCpuMemorySize += run->computeSizeInCache(); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +class GrStencilAndCoverTextContext::FallbackBlobBuilder { +public: + FallbackBlobBuilder() : fBuffIdx(0), fCount(0) {} + + bool isInitialized() const { return SkToBool(fBuilder); } + + void init(const SkPaint& font, SkScalar textRatio); + + void appendGlyph(uint16_t glyphId, const SkPoint& pos); + + const SkTextBlob* buildIfNeeded(int* count); + +private: + enum { kWriteBufferSize = 1024 }; + + void flush(); + + SkAutoTDelete<SkTextBlobBuilder> fBuilder; + SkPaint fFont; + int fBuffIdx; + int fCount; + uint16_t fGlyphIds[kWriteBufferSize]; + SkPoint fPositions[kWriteBufferSize]; +}; + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +GrStencilAndCoverTextContext::TextRun::TextRun(const SkPaint& fontAndStroke) + : fStroke(fontAndStroke), + fFont(fontAndStroke), + fTotalGlyphCount(0), + fFallbackGlyphCount(0), + fDetachedGlyphCache(nullptr), + fLastDrawnGlyphsID(SK_InvalidUniqueID) { + SkASSERT(!fStroke.isHairlineStyle()); // Hairlines are not supported. + + // Setting to "fill" ensures that no strokes get baked into font outlines. (We use the GPU path + // rendering API for stroking). + fFont.setStyle(SkPaint::kFill_Style); + + if (fFont.isFakeBoldText() && SkStrokeRec::kStroke_Style != fStroke.getStyle()) { + // Instead of letting fake bold get baked into the glyph outlines, do it with GPU stroke. + SkScalar fakeBoldScale = SkScalarInterpFunc(fFont.getTextSize(), + kStdFakeBoldInterpKeys, + kStdFakeBoldInterpValues, + kStdFakeBoldInterpLength); + SkScalar extra = SkScalarMul(fFont.getTextSize(), fakeBoldScale); + fStroke.setStrokeStyle(fStroke.needToApply() ? fStroke.getWidth() + extra : extra, + true /*strokeAndFill*/); + + fFont.setFakeBoldText(false); + } + + if (!fFont.getPathEffect() && !fStroke.isDashed()) { + // We can draw the glyphs from canonically sized paths. + fTextRatio = fFont.getTextSize() / SkPaint::kCanonicalTextSizeForPaths; + fTextInverseRatio = SkPaint::kCanonicalTextSizeForPaths / fFont.getTextSize(); + + // Compensate for the glyphs being scaled by fTextRatio. + if (!fStroke.isFillStyle()) { + fStroke.setStrokeStyle(fStroke.getWidth() / fTextRatio, + SkStrokeRec::kStrokeAndFill_Style == fStroke.getStyle()); + } + + fFont.setLinearText(true); + fFont.setLCDRenderText(false); + fFont.setAutohinted(false); + fFont.setHinting(SkPaint::kNo_Hinting); + fFont.setSubpixelText(true); + fFont.setTextSize(SkIntToScalar(SkPaint::kCanonicalTextSizeForPaths)); + + fUsingRawGlyphPaths = SK_Scalar1 == fFont.getTextScaleX() && + 0 == fFont.getTextSkewX() && + !fFont.isFakeBoldText() && + !fFont.isVerticalText(); + } else { + fTextRatio = fTextInverseRatio = 1.0f; + fUsingRawGlyphPaths = false; + } + + // Generate the key that will be used to cache the GPU glyph path objects. + if (fUsingRawGlyphPaths && fStroke.isFillStyle()) { + static const GrUniqueKey::Domain kRawFillPathGlyphDomain = GrUniqueKey::GenerateDomain(); + + const SkTypeface* typeface = fFont.getTypeface(); + GrUniqueKey::Builder builder(&fGlyphPathsKey, kRawFillPathGlyphDomain, 1); + reinterpret_cast<uint32_t&>(builder[0]) = typeface ? typeface->uniqueID() : 0; + } else { + static const GrUniqueKey::Domain kPathGlyphDomain = GrUniqueKey::GenerateDomain(); + + int strokeDataCount = fStroke.computeUniqueKeyFragmentData32Cnt(); + if (fUsingRawGlyphPaths) { + const SkTypeface* typeface = fFont.getTypeface(); + GrUniqueKey::Builder builder(&fGlyphPathsKey, kPathGlyphDomain, 2 + strokeDataCount); + reinterpret_cast<uint32_t&>(builder[0]) = typeface ? typeface->uniqueID() : 0; + reinterpret_cast<uint32_t&>(builder[1]) = strokeDataCount; + fStroke.asUniqueKeyFragment(&builder[2]); + } else { + SkGlyphCache* glyphCache = this->getGlyphCache(); + const SkTypeface* typeface = glyphCache->getScalerContext()->getTypeface(); + const SkDescriptor* desc = &glyphCache->getDescriptor(); + int descDataCount = (desc->getLength() + 3) / 4; + GrUniqueKey::Builder builder(&fGlyphPathsKey, kPathGlyphDomain, + 2 + strokeDataCount + descDataCount); + reinterpret_cast<uint32_t&>(builder[0]) = typeface ? typeface->uniqueID() : 0; + reinterpret_cast<uint32_t&>(builder[1]) = strokeDataCount | (descDataCount << 16); + fStroke.asUniqueKeyFragment(&builder[2]); + memcpy(&builder[2 + strokeDataCount], desc, desc->getLength()); + } + } +} + +GrStencilAndCoverTextContext::TextRun::~TextRun() { + this->releaseGlyphCache(); +} + +void GrStencilAndCoverTextContext::TextRun::setText(const char text[], size_t byteLength, + SkScalar x, SkScalar y) { + SkASSERT(byteLength == 0 || text != nullptr); + + SkGlyphCache* glyphCache = this->getGlyphCache(); + SkDrawCacheProc glyphCacheProc = fFont.getDrawCacheProc(); + + fTotalGlyphCount = fFont.countText(text, byteLength); + fInstanceData.reset(InstanceData::Alloc(GrPathRendering::kTranslate_PathTransformType, + fTotalGlyphCount)); + + const char* stop = text + byteLength; + + // Measure first if needed. + if (fFont.getTextAlign() != SkPaint::kLeft_Align) { + SkFixed stopX = 0; + SkFixed stopY = 0; + + const char* textPtr = text; + while (textPtr < stop) { + // We don't need x, y here, since all subpixel variants will have the + // same advance. + const SkGlyph& glyph = glyphCacheProc(glyphCache, &textPtr, 0, 0); + + stopX += glyph.fAdvanceX; + stopY += glyph.fAdvanceY; + } + SkASSERT(textPtr == stop); + + SkScalar alignX = SkFixedToScalar(stopX) * fTextRatio; + SkScalar alignY = SkFixedToScalar(stopY) * fTextRatio; + + if (fFont.getTextAlign() == SkPaint::kCenter_Align) { + alignX = SkScalarHalf(alignX); + alignY = SkScalarHalf(alignY); + } + + x -= alignX; + y -= alignY; + } + + SkAutoKern autokern; + + SkFixed fixedSizeRatio = SkScalarToFixed(fTextRatio); + + SkFixed fx = SkScalarToFixed(x); + SkFixed fy = SkScalarToFixed(y); + FallbackBlobBuilder fallback; + while (text < stop) { + const SkGlyph& glyph = glyphCacheProc(glyphCache, &text, 0, 0); + fx += SkFixedMul(autokern.adjust(glyph), fixedSizeRatio); + if (glyph.fWidth) { + this->appendGlyph(glyph, SkPoint::Make(SkFixedToScalar(fx), SkFixedToScalar(fy)), + &fallback); + } + + fx += SkFixedMul(glyph.fAdvanceX, fixedSizeRatio); + fy += SkFixedMul(glyph.fAdvanceY, fixedSizeRatio); + } + + fFallbackTextBlob.reset(fallback.buildIfNeeded(&fFallbackGlyphCount)); +} + +void GrStencilAndCoverTextContext::TextRun::setPosText(const char text[], size_t byteLength, + const SkScalar pos[], int scalarsPerPosition, + const SkPoint& offset) { + SkASSERT(byteLength == 0 || text != nullptr); + SkASSERT(1 == scalarsPerPosition || 2 == scalarsPerPosition); + + SkGlyphCache* glyphCache = this->getGlyphCache(); + SkDrawCacheProc glyphCacheProc = fFont.getDrawCacheProc(); + + fTotalGlyphCount = fFont.countText(text, byteLength); + fInstanceData.reset(InstanceData::Alloc(GrPathRendering::kTranslate_PathTransformType, + fTotalGlyphCount)); + + const char* stop = text + byteLength; + + SkTextMapStateProc tmsProc(SkMatrix::I(), offset, scalarsPerPosition); + SkTextAlignProc alignProc(fFont.getTextAlign()); + FallbackBlobBuilder fallback; + while (text < stop) { + const SkGlyph& glyph = glyphCacheProc(glyphCache, &text, 0, 0); + if (glyph.fWidth) { + SkPoint tmsLoc; + tmsProc(pos, &tmsLoc); + SkPoint loc; + alignProc(tmsLoc, glyph, &loc); + + this->appendGlyph(glyph, loc, &fallback); + } + pos += scalarsPerPosition; + } + + fFallbackTextBlob.reset(fallback.buildIfNeeded(&fFallbackGlyphCount)); +} + +GrPathRange* GrStencilAndCoverTextContext::TextRun::createGlyphs(GrContext* ctx) const { + GrPathRange* glyphs = static_cast<GrPathRange*>( + ctx->resourceProvider()->findAndRefResourceByUniqueKey(fGlyphPathsKey)); + if (nullptr == glyphs) { + if (fUsingRawGlyphPaths) { + glyphs = ctx->resourceProvider()->createGlyphs(fFont.getTypeface(), nullptr, fStroke); + } else { + SkGlyphCache* cache = this->getGlyphCache(); + glyphs = ctx->resourceProvider()->createGlyphs(cache->getScalerContext()->getTypeface(), + &cache->getDescriptor(), + fStroke); + } + ctx->resourceProvider()->assignUniqueKeyToResource(fGlyphPathsKey, glyphs); + } + return glyphs; +} + +inline void GrStencilAndCoverTextContext::TextRun::appendGlyph(const SkGlyph& glyph, + const SkPoint& pos, + FallbackBlobBuilder* fallback) { + // Stick the glyphs we can't draw into the fallback text blob. + if (SkMask::kARGB32_Format == glyph.fMaskFormat) { + if (!fallback->isInitialized()) { + fallback->init(fFont, fTextRatio); + } + fallback->appendGlyph(glyph.getGlyphID(), pos); + } else { + fInstanceData->append(glyph.getGlyphID(), fTextInverseRatio * pos.x(), + fTextInverseRatio * pos.y()); + } +} + +void GrStencilAndCoverTextContext::TextRun::draw(GrContext* ctx, + GrDrawContext* dc, + GrPipelineBuilder* pipelineBuilder, + GrColor color, + const SkMatrix& viewMatrix, + SkScalar x, SkScalar y, + const SkIRect& clipBounds, + GrTextContext* fallbackTextContext, + const SkPaint& originalSkPaint) const { + SkASSERT(fInstanceData); + SkASSERT(dc->accessRenderTarget()->isStencilBufferMultisampled() || !fFont.isAntiAlias()); + + if (fInstanceData->count()) { + pipelineBuilder->setState(GrPipelineBuilder::kHWAntialias_Flag, fFont.isAntiAlias()); + + GR_STATIC_CONST_SAME_STENCIL(kStencilPass, + kZero_StencilOp, + kKeep_StencilOp, + kNotEqual_StencilFunc, + 0xffff, + 0x0000, + 0xffff); + + *pipelineBuilder->stencil() = kStencilPass; + + SkAutoTUnref<GrPathRange> glyphs(this->createGlyphs(ctx)); + if (fLastDrawnGlyphsID != glyphs->getUniqueID()) { + // Either this is the first draw or the glyphs object was purged since last draw. + glyphs->loadPathsIfNeeded(fInstanceData->indices(), fInstanceData->count()); + fLastDrawnGlyphsID = glyphs->getUniqueID(); + } + + // Don't compute a bounding box. For dst copy texture, we'll opt instead for it to just copy + // the entire dst. Realistically this is a moot point, because any context that supports + // NV_path_rendering will also support NV_blend_equation_advanced. + // For clipping we'll just skip any optimizations based on the bounds. This does, however, + // hurt batching. + SkRect bounds = SkRect::MakeIWH(pipelineBuilder->getRenderTarget()->width(), + pipelineBuilder->getRenderTarget()->height()); + + SkAutoTUnref<GrDrawPathBatchBase> batch( + GrDrawPathRangeBatch::Create(viewMatrix, fTextRatio, fTextInverseRatio * x, + fTextInverseRatio * y, color, + GrPathRendering::kWinding_FillType, glyphs, fInstanceData, + bounds)); + + dc->drawPathBatch(*pipelineBuilder, batch); + } + + if (fFallbackTextBlob) { + SkPaint fallbackSkPaint(originalSkPaint); + fStroke.applyToPaint(&fallbackSkPaint); + if (!fStroke.isFillStyle()) { + fallbackSkPaint.setStrokeWidth(fStroke.getWidth() * fTextRatio); + } + + fallbackTextContext->drawTextBlob(dc, pipelineBuilder->clip(), fallbackSkPaint, viewMatrix, + fFallbackTextBlob, x, y, nullptr, clipBounds); + } +} + +SkGlyphCache* GrStencilAndCoverTextContext::TextRun::getGlyphCache() const { + if (!fDetachedGlyphCache) { + fDetachedGlyphCache = fFont.detachCache(nullptr, nullptr, true /*ignoreGamma*/); + } + return fDetachedGlyphCache; +} + + +void GrStencilAndCoverTextContext::TextRun::releaseGlyphCache() const { + if (fDetachedGlyphCache) { + SkGlyphCache::AttachCache(fDetachedGlyphCache); + fDetachedGlyphCache = nullptr; + } +} + +size_t GrStencilAndCoverTextContext::TextRun::computeSizeInCache() const { + size_t size = sizeof(TextRun) + fGlyphPathsKey.size(); + // The instance data always reserves enough space for every glyph. + size += (fTotalGlyphCount + fFallbackGlyphCount) * (sizeof(uint16_t) + 2 * sizeof(float)); + if (fInstanceData) { + size += sizeof(InstanceData); + } + if (fFallbackTextBlob) { + size += sizeof(SkTextBlob); + } + return size; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +void GrStencilAndCoverTextContext::FallbackBlobBuilder::init(const SkPaint& font, + SkScalar textRatio) { + SkASSERT(!this->isInitialized()); + fBuilder.reset(new SkTextBlobBuilder); + fFont = font; + fFont.setTextAlign(SkPaint::kLeft_Align); // The glyph positions will already account for align. + fFont.setTextEncoding(SkPaint::kGlyphID_TextEncoding); + // No need for subpixel positioning with bitmap glyphs. TODO: revisit if non-bitmap color glyphs + // show up and https://code.google.com/p/skia/issues/detail?id=4408 gets resolved. + fFont.setSubpixelText(false); + fFont.setTextSize(fFont.getTextSize() * textRatio); + fBuffIdx = 0; +} + +void GrStencilAndCoverTextContext::FallbackBlobBuilder::appendGlyph(uint16_t glyphId, + const SkPoint& pos) { + SkASSERT(this->isInitialized()); + if (fBuffIdx >= kWriteBufferSize) { + this->flush(); + } + fGlyphIds[fBuffIdx] = glyphId; + fPositions[fBuffIdx] = pos; + fBuffIdx++; + fCount++; +} + +void GrStencilAndCoverTextContext::FallbackBlobBuilder::flush() { + SkASSERT(this->isInitialized()); + SkASSERT(fBuffIdx <= kWriteBufferSize); + if (!fBuffIdx) { + return; + } + // This will automatically merge with previous runs since we use the same font. + const SkTextBlobBuilder::RunBuffer& buff = fBuilder->allocRunPos(fFont, fBuffIdx); + memcpy(buff.glyphs, fGlyphIds, fBuffIdx * sizeof(uint16_t)); + memcpy(buff.pos, fPositions[0].asScalars(), fBuffIdx * 2 * sizeof(SkScalar)); + fBuffIdx = 0; +} + +const SkTextBlob* GrStencilAndCoverTextContext::FallbackBlobBuilder::buildIfNeeded(int *count) { + *count = fCount; + if (fCount) { + this->flush(); + return fBuilder->build(); + } + return nullptr; +} diff --git a/src/gpu/text/GrStencilAndCoverTextContext.h b/src/gpu/text/GrStencilAndCoverTextContext.h new file mode 100644 index 0000000000..dab71e0578 --- /dev/null +++ b/src/gpu/text/GrStencilAndCoverTextContext.h @@ -0,0 +1,145 @@ +/* + * Copyright 2014 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef GrStencilAndCoverTextContext_DEFINED +#define GrStencilAndCoverTextContext_DEFINED + +#include "GrTextContext.h" +#include "GrDrawTarget.h" +#include "GrStrokeInfo.h" +#include "SkTHash.h" +#include "SkTInternalLList.h" +#include "SkTLList.h" +#include "batches/GrDrawPathBatch.h" + +class GrTextStrike; +class GrPath; +class SkSurfaceProps; + +/* + * This class implements text rendering using stencil and cover path rendering + * (by the means of GrDrawTarget::drawPath). + * This class exposes the functionality through GrTextContext interface. + */ +class GrStencilAndCoverTextContext : public GrTextContext { +public: + static GrStencilAndCoverTextContext* Create(GrContext*, const SkSurfaceProps&); + + virtual ~GrStencilAndCoverTextContext(); + +private: + GrStencilAndCoverTextContext(GrContext*, const SkSurfaceProps&); + + bool canDraw(const SkPaint& skPaint, const SkMatrix&) override { + return this->internalCanDraw(skPaint); + } + + bool internalCanDraw(const SkPaint&); + + void onDrawText(GrDrawContext*, const GrClip&, const GrPaint&, const SkPaint&, + const SkMatrix& viewMatrix, + const char text[], size_t byteLength, + SkScalar x, SkScalar y, const SkIRect& clipBounds) override; + void onDrawPosText(GrDrawContext*, + const GrClip&, const GrPaint&, const SkPaint&, + const SkMatrix& viewMatrix, + const char text[], size_t byteLength, + const SkScalar pos[], int scalarsPerPosition, + const SkPoint& offset, const SkIRect& clipBounds) override; + void drawTextBlob(GrDrawContext*, const GrClip&, const SkPaint&, + const SkMatrix& viewMatrix, const SkTextBlob*, SkScalar x, SkScalar y, + SkDrawFilter*, const SkIRect& clipBounds) override; + + class FallbackBlobBuilder; + + class TextRun { + public: + TextRun(const SkPaint& fontAndStroke); + ~TextRun(); + + void setText(const char text[], size_t byteLength, SkScalar x, SkScalar y); + + void setPosText(const char text[], size_t byteLength, const SkScalar pos[], + int scalarsPerPosition, const SkPoint& offset); + + void draw(GrContext*, GrDrawContext*, GrPipelineBuilder*, GrColor, const SkMatrix&, + SkScalar x, SkScalar y, const SkIRect& clipBounds, + GrTextContext* fallbackTextContext, const SkPaint& originalSkPaint) const; + + void releaseGlyphCache() const; + + size_t computeSizeInCache() const; + + private: + typedef GrDrawPathRangeBatch::InstanceData InstanceData; + + SkGlyphCache* getGlyphCache() const; + GrPathRange* createGlyphs(GrContext*) const; + void appendGlyph(const SkGlyph&, const SkPoint&, FallbackBlobBuilder*); + + GrStrokeInfo fStroke; + SkPaint fFont; + SkScalar fTextRatio; + float fTextInverseRatio; + bool fUsingRawGlyphPaths; + GrUniqueKey fGlyphPathsKey; + int fTotalGlyphCount; + SkAutoTUnref<InstanceData> fInstanceData; + int fFallbackGlyphCount; + SkAutoTUnref<const SkTextBlob> fFallbackTextBlob; + mutable SkGlyphCache* fDetachedGlyphCache; + mutable uint32_t fLastDrawnGlyphsID; + }; + + // Text blobs/caches. + + class TextBlob : public SkTLList<TextRun, 1> { + public: + typedef SkTArray<uint32_t, true> Key; + + static const Key& GetKey(const TextBlob* blob) { return blob->key(); } + + static uint32_t Hash(const Key& key) { + SkASSERT(key.count() > 1); // 1-length keys should be using the blob-id hash map. + return SkChecksum::Murmur3(key.begin(), sizeof(uint32_t) * key.count()); + } + + TextBlob(uint32_t blobId, const SkTextBlob* skBlob, const SkPaint& skPaint) + : fKey(&blobId, 1) { this->init(skBlob, skPaint); } + + TextBlob(const Key& key, const SkTextBlob* skBlob, const SkPaint& skPaint) + : fKey(key) { + // 1-length keys are unterstood to be the blob id and must use the other constructor. + SkASSERT(fKey.count() > 1); + this->init(skBlob, skPaint); + } + + const Key& key() const { return fKey; } + + size_t cpuMemorySize() const { return fCpuMemorySize; } + + private: + void init(const SkTextBlob*, const SkPaint&); + + const SkSTArray<1, uint32_t, true> fKey; + size_t fCpuMemorySize; + + SK_DECLARE_INTERNAL_LLIST_INTERFACE(TextBlob); + }; + + const TextBlob& findOrCreateTextBlob(const SkTextBlob*, const SkPaint&); + void purgeToFit(const TextBlob&); + + SkTHashMap<uint32_t, TextBlob*> fBlobIdCache; + SkTHashTable<TextBlob*, const TextBlob::Key&, TextBlob> fBlobKeyCache; + SkTInternalLList<TextBlob> fLRUList; + size_t fCacheSize; + + typedef GrTextContext INHERITED; +}; + +#endif diff --git a/src/gpu/text/GrTextBlobCache.cpp b/src/gpu/text/GrTextBlobCache.cpp new file mode 100644 index 0000000000..f11b7c60cb --- /dev/null +++ b/src/gpu/text/GrTextBlobCache.cpp @@ -0,0 +1,58 @@ +/* + * Copyright 2015 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#include "GrTextBlobCache.h" + +static const int kVerticesPerGlyph = 4; + +GrTextBlobCache::~GrTextBlobCache() { + this->freeAll(); +} + +GrAtlasTextBlob* GrTextBlobCache::createBlob(int glyphCount, int runCount, size_t maxVASize) { + // We allocate size for the GrAtlasTextBlob itself, plus size for the vertices array, + // and size for the glyphIds array. + size_t verticesCount = glyphCount * kVerticesPerGlyph * maxVASize; + size_t size = sizeof(GrAtlasTextBlob) + + verticesCount + + glyphCount * sizeof(GrGlyph**) + + sizeof(GrAtlasTextBlob::Run) * runCount; + + void* allocation = fPool.allocate(size); +#ifdef CACHE_SANITY_CHECK + sk_bzero(allocation, size); +#endif + + GrAtlasTextBlob* cacheBlob = new (allocation) GrAtlasTextBlob; +#ifdef CACHE_SANITY_CHECK + cacheBlob->fSize = size; +#endif + + // setup offsets for vertices / glyphs + cacheBlob->fVertices = sizeof(GrAtlasTextBlob) + reinterpret_cast<unsigned char*>(cacheBlob); + cacheBlob->fGlyphs = reinterpret_cast<GrGlyph**>(cacheBlob->fVertices + verticesCount); + cacheBlob->fRuns = reinterpret_cast<GrAtlasTextBlob::Run*>(cacheBlob->fGlyphs + glyphCount); + + // Initialize runs + for (int i = 0; i < runCount; i++) { + new (&cacheBlob->fRuns[i]) GrAtlasTextBlob::Run; + } + cacheBlob->fRunCount = runCount; + cacheBlob->fPool = &fPool; + return cacheBlob; +} + +void GrTextBlobCache::freeAll() { + SkTDynamicHash<GrAtlasTextBlob, GrAtlasTextBlob::Key>::Iter iter(&fCache); + while (!iter.done()) { + GrAtlasTextBlob* blob = &(*iter); + fBlobList.remove(blob); + blob->unref(); + ++iter; + } + fCache.rewind(); +} diff --git a/src/gpu/text/GrTextBlobCache.h b/src/gpu/text/GrTextBlobCache.h new file mode 100644 index 0000000000..8eee9d13db --- /dev/null +++ b/src/gpu/text/GrTextBlobCache.h @@ -0,0 +1,158 @@ +/* + * Copyright 2015 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef GrTextBlobCache_DEFINED +#define GrTextBlobCache_DEFINED + +#include "GrAtlasTextContext.h" +#include "SkTDynamicHash.h" +#include "SkTextBlobRunIterator.h" + +class GrTextBlobCache { +public: + /** + * The callback function used by the cache when it is still over budget after a purge. The + * passed in 'data' is the same 'data' handed to setOverbudgetCallback. + */ + typedef void (*PFOverBudgetCB)(void* data); + + GrTextBlobCache(PFOverBudgetCB cb, void* data) + : fPool(kPreAllocSize, kMinGrowthSize) + , fCallback(cb) + , fData(data) + , fBudget(kDefaultBudget) { + SkASSERT(cb && data); + } + ~GrTextBlobCache(); + + // creates an uncached blob + GrAtlasTextBlob* createBlob(int glyphCount, int runCount, size_t maxVASize); + GrAtlasTextBlob* createBlob(const SkTextBlob* blob, size_t maxVAStride) { + int glyphCount = 0; + int runCount = 0; + BlobGlyphCount(&glyphCount, &runCount, blob); + GrAtlasTextBlob* cacheBlob = this->createBlob(glyphCount, runCount, maxVAStride); + return cacheBlob; + } + + static void SetupCacheBlobKey(GrAtlasTextBlob* cacheBlob, + const GrAtlasTextBlob::Key& key, + const SkMaskFilter::BlurRec& blurRec, + const SkPaint& paint) { + cacheBlob->fKey = key; + if (key.fHasBlur) { + cacheBlob->fBlurRec = blurRec; + } + if (key.fStyle != SkPaint::kFill_Style) { + cacheBlob->fStrokeInfo.fFrameWidth = paint.getStrokeWidth(); + cacheBlob->fStrokeInfo.fMiterLimit = paint.getStrokeMiter(); + cacheBlob->fStrokeInfo.fJoin = paint.getStrokeJoin(); + } + } + + GrAtlasTextBlob* createCachedBlob(const SkTextBlob* blob, + const GrAtlasTextBlob::Key& key, + const SkMaskFilter::BlurRec& blurRec, + const SkPaint& paint, + size_t maxVAStride) { + int glyphCount = 0; + int runCount = 0; + BlobGlyphCount(&glyphCount, &runCount, blob); + GrAtlasTextBlob* cacheBlob = this->createBlob(glyphCount, runCount, maxVAStride); + SetupCacheBlobKey(cacheBlob, key, blurRec, paint); + this->add(cacheBlob); + return cacheBlob; + } + + GrAtlasTextBlob* find(const GrAtlasTextBlob::Key& key) { + return fCache.find(key); + } + + void remove(GrAtlasTextBlob* blob) { + fCache.remove(blob->fKey); + fBlobList.remove(blob); + blob->unref(); + } + + void add(GrAtlasTextBlob* blob) { + fCache.add(blob); + fBlobList.addToHead(blob); + + this->checkPurge(blob); + } + + void makeMRU(GrAtlasTextBlob* blob) { + if (fBlobList.head() == blob) { + return; + } + + fBlobList.remove(blob); + fBlobList.addToHead(blob); + } + + void freeAll(); + + // TODO move to SkTextBlob + static void BlobGlyphCount(int* glyphCount, int* runCount, const SkTextBlob* blob) { + SkTextBlobRunIterator itCounter(blob); + for (; !itCounter.done(); itCounter.next(), (*runCount)++) { + *glyphCount += itCounter.glyphCount(); + } + } + + void setBudget(size_t budget) { + fBudget = budget; + this->checkPurge(); + } + +private: + typedef SkTInternalLList<GrAtlasTextBlob> BitmapBlobList; + + void checkPurge(GrAtlasTextBlob* blob = nullptr) { + // If we are overbudget, then unref until we are below budget again + if (fPool.size() > fBudget) { + BitmapBlobList::Iter iter; + iter.init(fBlobList, BitmapBlobList::Iter::kTail_IterStart); + GrAtlasTextBlob* lruBlob = nullptr; + while (fPool.size() > fBudget && (lruBlob = iter.get()) && lruBlob != blob) { + fCache.remove(lruBlob->fKey); + + // Backup the iterator before removing and unrefing the blob + iter.prev(); + fBlobList.remove(lruBlob); + lruBlob->unref(); + } + + // If we break out of the loop with lruBlob == blob, then we haven't purged enough + // use the call back and try to free some more. If we are still overbudget after this, + // then this single textblob is over our budget + if (blob && lruBlob == blob) { + (*fCallback)(fData); + } + +#ifdef SPEW_BUDGET_MESSAGE + if (fPool.size() > fBudget) { + SkDebugf("Single textblob is larger than our whole budget"); + } +#endif + } + } + + // Budget was chosen to be ~4 megabytes. The min alloc and pre alloc sizes in the pool are + // based off of the largest cached textblob I have seen in the skps(a couple of kilobytes). + static const int kPreAllocSize = 1 << 17; + static const int kMinGrowthSize = 1 << 17; + static const int kDefaultBudget = 1 << 22; + BitmapBlobList fBlobList; + SkTDynamicHash<GrAtlasTextBlob, GrAtlasTextBlob::Key> fCache; + GrMemoryPool fPool; + PFOverBudgetCB fCallback; + void* fData; + size_t fBudget; +}; + +#endif diff --git a/src/gpu/text/GrTextContext.cpp b/src/gpu/text/GrTextContext.cpp new file mode 100644 index 0000000000..8ff2523a73 --- /dev/null +++ b/src/gpu/text/GrTextContext.cpp @@ -0,0 +1,176 @@ +/* + * Copyright 2010 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#include "GrTextContext.h" +#include "GrContext.h" +#include "GrFontScaler.h" +#include "GrTextUtils.h" + +#include "SkDrawFilter.h" +#include "SkGlyphCache.h" +#include "SkGrPriv.h" +#include "SkTextBlobRunIterator.h" + +GrTextContext::GrTextContext(GrContext* context, const SkSurfaceProps& surfaceProps) + : fFallbackTextContext(nullptr) + , fContext(context) + , fSurfaceProps(surfaceProps) { +} + +GrTextContext::~GrTextContext() { + delete fFallbackTextContext; +} + +void GrTextContext::drawText(GrDrawContext* dc, + const GrClip& clip, const GrPaint& paint, + const SkPaint& skPaint, const SkMatrix& viewMatrix, + const char text[], size_t byteLength, + SkScalar x, SkScalar y, const SkIRect& clipBounds) { + if (fContext->abandoned()) { + return; + } + + GrTextContext* textContext = this; + do { + if (textContext->canDraw(skPaint, viewMatrix)) { + textContext->onDrawText(dc, clip, paint, skPaint, viewMatrix, + text, byteLength, x, y, clipBounds); + return; + } + textContext = textContext->fFallbackTextContext; + } while (textContext); + + // fall back to drawing as a path + GrTextUtils::DrawTextAsPath(fContext, dc, clip, skPaint, viewMatrix, text, byteLength, x, y, + clipBounds); +} + +void GrTextContext::drawPosText(GrDrawContext* dc, + const GrClip& clip, const GrPaint& paint, + const SkPaint& skPaint, const SkMatrix& viewMatrix, + const char text[], size_t byteLength, + const SkScalar pos[], int scalarsPerPosition, + const SkPoint& offset, const SkIRect& clipBounds) { + if (fContext->abandoned()) { + return; + } + + GrTextContext* textContext = this; + do { + if (textContext->canDraw(skPaint, viewMatrix)) { + textContext->onDrawPosText(dc, clip, paint, skPaint, viewMatrix, + text, byteLength, pos, + scalarsPerPosition, offset, clipBounds); + return; + } + textContext = textContext->fFallbackTextContext; + } while (textContext); + + // fall back to drawing as a path + GrTextUtils::DrawPosTextAsPath(fContext, dc, fSurfaceProps, clip, skPaint, viewMatrix, text, + byteLength, pos, scalarsPerPosition, offset, clipBounds); +} + +bool GrTextContext::ShouldDisableLCD(const SkPaint& paint) { + if (!SkXfermode::AsMode(paint.getXfermode(), nullptr) || + paint.getMaskFilter() || + paint.getRasterizer() || + paint.getPathEffect() || + paint.isFakeBoldText() || + paint.getStyle() != SkPaint::kFill_Style) + { + return true; + } + return false; +} + +uint32_t GrTextContext::FilterTextFlags(const SkSurfaceProps& surfaceProps, const SkPaint& paint) { + uint32_t flags = paint.getFlags(); + + if (!paint.isLCDRenderText() || !paint.isAntiAlias()) { + return flags; + } + + if (kUnknown_SkPixelGeometry == surfaceProps.pixelGeometry() || ShouldDisableLCD(paint)) { + flags &= ~SkPaint::kLCDRenderText_Flag; + flags |= SkPaint::kGenA8FromLCD_Flag; + } + + return flags; +} + +void GrTextContext::drawTextBlob(GrDrawContext* dc, + const GrClip& clip, const SkPaint& skPaint, + const SkMatrix& viewMatrix, const SkTextBlob* blob, + SkScalar x, SkScalar y, + SkDrawFilter* drawFilter, const SkIRect& clipBounds) { + SkPaint runPaint = skPaint; + + SkTextBlobRunIterator it(blob); + for (;!it.done(); it.next()) { + size_t textLen = it.glyphCount() * sizeof(uint16_t); + const SkPoint& offset = it.offset(); + // applyFontToPaint() always overwrites the exact same attributes, + // so it is safe to not re-seed the paint for this reason. + it.applyFontToPaint(&runPaint); + + if (drawFilter && !drawFilter->filter(&runPaint, SkDrawFilter::kText_Type)) { + // A false return from filter() means we should abort the current draw. + runPaint = skPaint; + continue; + } + + runPaint.setFlags(FilterTextFlags(fSurfaceProps, runPaint)); + + GrPaint grPaint; + if (!SkPaintToGrPaint(fContext, runPaint, viewMatrix, &grPaint)) { + return; + } + + switch (it.positioning()) { + case SkTextBlob::kDefault_Positioning: + this->drawText(dc, clip, grPaint, runPaint, viewMatrix, (const char *)it.glyphs(), + textLen, x + offset.x(), y + offset.y(), clipBounds); + break; + case SkTextBlob::kHorizontal_Positioning: + this->drawPosText(dc, clip, grPaint, runPaint, viewMatrix, (const char*)it.glyphs(), + textLen, it.pos(), 1, SkPoint::Make(x, y + offset.y()), clipBounds); + break; + case SkTextBlob::kFull_Positioning: + this->drawPosText(dc, clip, grPaint, runPaint, viewMatrix, (const char*)it.glyphs(), + textLen, it.pos(), 2, SkPoint::Make(x, y), clipBounds); + break; + default: + SkFAIL("unhandled positioning mode"); + } + + if (drawFilter) { + // A draw filter may change the paint arbitrarily, so we must re-seed in this case. + runPaint = skPaint; + } + } +} + +static void GlyphCacheAuxProc(void* data) { + GrFontScaler* scaler = (GrFontScaler*)data; + SkSafeUnref(scaler); +} + +GrFontScaler* GrTextContext::GetGrFontScaler(SkGlyphCache* cache) { + void* auxData; + GrFontScaler* scaler = nullptr; + + if (cache->getAuxProcData(GlyphCacheAuxProc, &auxData)) { + scaler = (GrFontScaler*)auxData; + } + if (nullptr == scaler) { + scaler = new GrFontScaler(cache); + cache->setAuxProc(GlyphCacheAuxProc, scaler); + } + + return scaler; +} diff --git a/src/gpu/text/GrTextContext.h b/src/gpu/text/GrTextContext.h new file mode 100644 index 0000000000..206b34a403 --- /dev/null +++ b/src/gpu/text/GrTextContext.h @@ -0,0 +1,76 @@ +/* + * Copyright 2010 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef GrTextContext_DEFINED +#define GrTextContext_DEFINED + +#include "GrClip.h" +#include "GrGlyph.h" +#include "GrPaint.h" +#include "SkSurfaceProps.h" +#include "SkPostConfig.h" + +class GrClip; +class GrContext; +class GrDrawContext; +class GrFontScaler; +class SkDrawFilter; +class SkTextBlob; + +/* + * This class wraps the state for a single text render + */ +class GrTextContext { +public: + virtual ~GrTextContext(); + + void drawText(GrDrawContext* dc, + const GrClip&, const GrPaint&, const SkPaint&, + const SkMatrix& viewMatrix, const char text[], size_t byteLength, SkScalar x, + SkScalar y, const SkIRect& clipBounds); + void drawPosText(GrDrawContext* dc, + const GrClip&, const GrPaint&, const SkPaint&, + const SkMatrix& viewMatrix, + const char text[], size_t byteLength, + const SkScalar pos[], int scalarsPerPosition, + const SkPoint& offset, const SkIRect& clipBounds); + virtual void drawTextBlob(GrDrawContext* dc, const GrClip&, const SkPaint&, + const SkMatrix& viewMatrix, const SkTextBlob*, + SkScalar x, SkScalar y, + SkDrawFilter*, const SkIRect& clipBounds); + + static bool ShouldDisableLCD(const SkPaint& paint); + +protected: + GrTextContext* fFallbackTextContext; + GrContext* fContext; + SkSurfaceProps fSurfaceProps; + + GrTextContext(GrContext*, const SkSurfaceProps&); + + virtual bool canDraw(const SkPaint&, const SkMatrix& viewMatrix) = 0; + + virtual void onDrawText(GrDrawContext*, const GrClip&, + const GrPaint&, const SkPaint&, + const SkMatrix& viewMatrix, const char text[], size_t byteLength, + SkScalar x, SkScalar y, const SkIRect& clipBounds) = 0; + virtual void onDrawPosText(GrDrawContext*, const GrClip&, + const GrPaint&, const SkPaint&, + const SkMatrix& viewMatrix, + const char text[], size_t byteLength, + const SkScalar pos[], int scalarsPerPosition, + const SkPoint& offset, const SkIRect& clipBounds) = 0; + + static GrFontScaler* GetGrFontScaler(SkGlyphCache* cache); + static uint32_t FilterTextFlags(const SkSurfaceProps& surfaceProps, const SkPaint& paint); + + friend class GrAtlasTextBatch; + friend class GrAtlasTextBlob; // for FilterTextFlags + friend class GrTextUtils; // for some static functions +}; + +#endif diff --git a/src/gpu/text/GrTextUtils.cpp b/src/gpu/text/GrTextUtils.cpp new file mode 100644 index 0000000000..293ea4d891 --- /dev/null +++ b/src/gpu/text/GrTextUtils.cpp @@ -0,0 +1,200 @@ +/* + * Copyright 2015 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#include "GrTextUtils.h" + +#include "GrAtlasTextBlob.h" +#include "GrBatchFontCache.h" +#include "GrBlurUtils.h" +#include "GrContext.h" +#include "GrDrawContext.h" +#include "GrTextContext.h" +#include "SkDrawProcs.h" +#include "SkFindAndPlaceGlyph.h" +#include "SkGlyphCache.h" +#include "SkPaint.h" +#include "SkRect.h" +#include "SkTextMapStateProc.h" +#include "SkTextToPathIter.h" + +void GrTextUtils::DrawBmpText(GrAtlasTextBlob* blob, int runIndex, + GrBatchFontCache* fontCache, + SkGlyphCache* cache, const SkPaint& skPaint, + GrColor color, + const SkMatrix& viewMatrix, + const char text[], size_t byteLength, + SkScalar x, SkScalar y) { + SkASSERT(byteLength == 0 || text != nullptr); + + // nothing to draw + if (text == nullptr || byteLength == 0) { + return; + } + + GrBatchTextStrike* currStrike = nullptr; + + // Get GrFontScaler from cache + GrFontScaler* fontScaler = GrTextContext::GetGrFontScaler(cache); + + SkFindAndPlaceGlyph::ProcessText( + skPaint.getTextEncoding(), text, byteLength, + {x, y}, viewMatrix, skPaint.getTextAlign(), + cache, + [&](const SkGlyph& glyph, SkPoint position, SkPoint rounding) { + position += rounding; + BmpAppendGlyph( + blob, runIndex, fontCache, &currStrike, glyph, + SkScalarFloorToInt(position.fX), SkScalarFloorToInt(position.fY), + color, fontScaler); + } + ); +} + +void GrTextUtils::DrawBmpPosText(GrAtlasTextBlob* blob, int runIndex, + GrBatchFontCache* fontCache, + SkGlyphCache* cache, const SkPaint& skPaint, + GrColor color, + const SkMatrix& viewMatrix, + const char text[], size_t byteLength, + const SkScalar pos[], int scalarsPerPosition, + const SkPoint& offset) { + SkASSERT(byteLength == 0 || text != nullptr); + SkASSERT(1 == scalarsPerPosition || 2 == scalarsPerPosition); + + // nothing to draw + if (text == nullptr || byteLength == 0) { + return; + } + + GrBatchTextStrike* currStrike = nullptr; + + // Get GrFontScaler from cache + GrFontScaler* fontScaler = GrTextContext::GetGrFontScaler(cache); + + SkFindAndPlaceGlyph::ProcessPosText( + skPaint.getTextEncoding(), text, byteLength, + offset, viewMatrix, pos, scalarsPerPosition, + skPaint.getTextAlign(), cache, + [&](const SkGlyph& glyph, SkPoint position, SkPoint rounding) { + position += rounding; + BmpAppendGlyph( + blob, runIndex, fontCache, &currStrike, glyph, + SkScalarFloorToInt(position.fX), SkScalarFloorToInt(position.fY), + color, fontScaler); + } + ); +} + +void GrTextUtils::BmpAppendGlyph(GrAtlasTextBlob* blob, int runIndex, + GrBatchFontCache* fontCache, + GrBatchTextStrike** strike, const SkGlyph& skGlyph, + int vx, int vy, GrColor color, GrFontScaler* scaler) { + if (!*strike) { + *strike = fontCache->getStrike(scaler); + } + + GrGlyph::PackedID id = GrGlyph::Pack(skGlyph.getGlyphID(), + skGlyph.getSubXFixed(), + skGlyph.getSubYFixed(), + GrGlyph::kCoverage_MaskStyle); + GrGlyph* glyph = (*strike)->getGlyph(skGlyph, id, scaler); + if (!glyph) { + return; + } + + int x = vx + glyph->fBounds.fLeft; + int y = vy + glyph->fBounds.fTop; + + // keep them as ints until we've done the clip-test + int width = glyph->fBounds.width(); + int height = glyph->fBounds.height(); + + SkRect r; + r.fLeft = SkIntToScalar(x); + r.fTop = SkIntToScalar(y); + r.fRight = r.fLeft + SkIntToScalar(width); + r.fBottom = r.fTop + SkIntToScalar(height); + + blob->appendGlyph(runIndex, r, color, *strike, glyph, scaler, skGlyph, + SkIntToScalar(vx), SkIntToScalar(vy), 1.0f, false); +} + +void GrTextUtils::DrawTextAsPath(GrContext* context, GrDrawContext* dc, + const GrClip& clip, + const SkPaint& skPaint, const SkMatrix& viewMatrix, + const char text[], size_t byteLength, SkScalar x, SkScalar y, + const SkIRect& clipBounds) { + SkTextToPathIter iter(text, byteLength, skPaint, true); + + SkMatrix matrix; + matrix.setScale(iter.getPathScale(), iter.getPathScale()); + matrix.postTranslate(x, y); + + const SkPath* iterPath; + SkScalar xpos, prevXPos = 0; + + while (iter.next(&iterPath, &xpos)) { + matrix.postTranslate(xpos - prevXPos, 0); + if (iterPath) { + const SkPaint& pnt = iter.getPaint(); + GrBlurUtils::drawPathWithMaskFilter(context, dc, clip, *iterPath, + pnt, viewMatrix, &matrix, clipBounds, false); + } + prevXPos = xpos; + } +} + +void GrTextUtils::DrawPosTextAsPath(GrContext* context, + GrDrawContext* dc, + const SkSurfaceProps& props, + const GrClip& clip, + const SkPaint& origPaint, const SkMatrix& viewMatrix, + const char text[], size_t byteLength, + const SkScalar pos[], int scalarsPerPosition, + const SkPoint& offset, const SkIRect& clipBounds) { + // setup our std paint, in hopes of getting hits in the cache + SkPaint paint(origPaint); + SkScalar matrixScale = paint.setupForAsPaths(); + + SkMatrix matrix; + matrix.setScale(matrixScale, matrixScale); + + // Temporarily jam in kFill, so we only ever ask for the raw outline from the cache. + paint.setStyle(SkPaint::kFill_Style); + paint.setPathEffect(nullptr); + + SkDrawCacheProc glyphCacheProc = paint.getDrawCacheProc(); + SkAutoGlyphCache autoCache(paint, &props, nullptr); + SkGlyphCache* cache = autoCache.getCache(); + + const char* stop = text + byteLength; + SkTextAlignProc alignProc(paint.getTextAlign()); + SkTextMapStateProc tmsProc(SkMatrix::I(), offset, scalarsPerPosition); + + // Now restore the original settings, so we "draw" with whatever style/stroking. + paint.setStyle(origPaint.getStyle()); + paint.setPathEffect(origPaint.getPathEffect()); + + while (text < stop) { + const SkGlyph& glyph = glyphCacheProc(cache, &text, 0, 0); + if (glyph.fWidth) { + const SkPath* path = cache->findPath(glyph); + if (path) { + SkPoint tmsLoc; + tmsProc(pos, &tmsLoc); + SkPoint loc; + alignProc(tmsLoc, glyph, &loc); + + matrix[SkMatrix::kMTransX] = loc.fX; + matrix[SkMatrix::kMTransY] = loc.fY; + GrBlurUtils::drawPathWithMaskFilter(context, dc, clip, *path, paint, + viewMatrix, &matrix, clipBounds, false); + } + } + pos += scalarsPerPosition; + } +} diff --git a/src/gpu/text/GrTextUtils.h b/src/gpu/text/GrTextUtils.h new file mode 100644 index 0000000000..8f673afbed --- /dev/null +++ b/src/gpu/text/GrTextUtils.h @@ -0,0 +1,70 @@ +/* + * Copyright 2015 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef GrTextUtils_DEFINED +#define GrTextUtils_DEFINED + +#include "GrColor.h" +#include "SkScalar.h" + +class GrAtlasTextBlob; +class GrBatchFontCache; +class GrBatchTextStrike; +class GrClip; +class GrContext; +class GrDrawContext; +class GrFontScaler; +class SkGlyph; +class SkMatrix; +struct SkIRect; +class SkPaint; +struct SkPoint; +class SkGlyphCache; +class SkSurfaceProps; + +/* + * A class to house a bunch of common text utilities. This class should *ONLY* have static + * functions. It is not a namespace only because we wish to friend SkPaint + * + */ +class GrTextUtils { +public: + // Functions for appending BMP text to GrAtlasTextBlob + static void DrawBmpText(GrAtlasTextBlob*, int runIndex, + GrBatchFontCache*, SkGlyphCache*, const SkPaint&, + GrColor, const SkMatrix& viewMatrix, + const char text[], size_t byteLength, + SkScalar x, SkScalar y); + + static void DrawBmpPosText(GrAtlasTextBlob*, int runIndex, + GrBatchFontCache*, SkGlyphCache*, const SkPaint&, + GrColor, const SkMatrix& viewMatrix, + const char text[], size_t byteLength, + const SkScalar pos[], int scalarsPerPosition, + const SkPoint& offset); + + // Functions for drawing text as paths + static void DrawTextAsPath(GrContext*, GrDrawContext*, const GrClip& clip, + const SkPaint& origPaint, const SkMatrix& viewMatrix, + const char text[], size_t byteLength, SkScalar x, SkScalar y, + const SkIRect& clipBounds); + + static void DrawPosTextAsPath(GrContext* context, + GrDrawContext* dc, + const SkSurfaceProps& props, + const GrClip& clip, + const SkPaint& origPaint, const SkMatrix& viewMatrix, + const char text[], size_t byteLength, + const SkScalar pos[], int scalarsPerPosition, + const SkPoint& offset, const SkIRect& clipBounds); +private: + static void BmpAppendGlyph(GrAtlasTextBlob*, int runIndex, GrBatchFontCache*, + GrBatchTextStrike**, const SkGlyph&, int left, int top, + GrColor color, GrFontScaler*); +}; + +#endif |