blob: d75d2cc105a90bd0331d4b51111103a8be261ee5 [file] [log] [blame]
/*
* Copyright 2018 The Android Open Source Project
*
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#include "src/core/SkGlyphRunPainter.h"
#if SK_SUPPORT_GPU
#include "src/gpu/ganesh/GrColorInfo.h"
#include "src/gpu/ganesh/GrDirectContextPriv.h"
#include "src/gpu/ganesh/text/GrSDFTControl.h"
#include "src/gpu/ganesh/text/GrTextBlobRedrawCoordinator.h"
#include "src/gpu/ganesh/v1/SurfaceDrawContext_v1.h"
#endif // SK_SUPPORT_GPU
#include "include/core/SkBitmap.h"
#include "include/core/SkColorFilter.h"
#include "include/core/SkColorSpace.h"
#include "include/core/SkMaskFilter.h"
#include "include/core/SkPathEffect.h"
#include "include/private/SkTDArray.h"
#include "src/core/SkDevice.h"
#include "src/core/SkDistanceFieldGen.h"
#include "src/core/SkDraw.h"
#include "src/core/SkEnumerate.h"
#include "src/core/SkFontPriv.h"
#include "src/core/SkGlyphBuffer.h"
#include "src/core/SkRasterClip.h"
#include "src/core/SkScalerCache.h"
#include "src/core/SkStrikeCache.h"
#include "src/core/SkStrikeForGPU.h"
#include "src/core/SkStrikeSpec.h"
#include "src/core/SkTraceEvent.h"
#include <cinttypes>
#include <climits>
namespace {
// TODO: unify with code in GrSDFTControl.cpp
SkScalerContextFlags compute_scaler_context_flags(const SkColorSpace* cs) {
// If we're doing linear blending, then we can disable the gamma hacks.
// Otherwise, leave them on. In either case, we still want the contrast boost:
// TODO: Can we be even smarter about mask gamma based on the dest transfer function?
if (cs && cs->gammaIsLinear()) {
return SkScalerContextFlags::kBoostContrast;
} else {
return SkScalerContextFlags::kFakeGammaAndBoostContrast;
}
}
} // namespace
// -- SkGlyphRunListPainterCPU ---------------------------------------------------------------------
SkGlyphRunListPainterCPU::SkGlyphRunListPainterCPU(const SkSurfaceProps& props,
SkColorType colorType,
SkColorSpace* cs)
: fDeviceProps{props}
, fBitmapFallbackProps{SkSurfaceProps{props.flags(), kUnknown_SkPixelGeometry}}
, fColorType{colorType}
, fScalerContextFlags{compute_scaler_context_flags(cs)} {}
void SkGlyphRunListPainterCPU::drawForBitmapDevice(
SkCanvas* canvas, const BitmapDevicePainter* bitmapDevice,
const SkGlyphRunList& glyphRunList, const SkPaint& paint, const SkMatrix& drawMatrix) {
auto bufferScope = SkSubRunBuffers::EnsureBuffers(glyphRunList);
auto [accepted, rejected] = bufferScope.buffers();
// The bitmap blitters can only draw lcd text to a N32 bitmap in srcOver. Otherwise,
// convert the lcd text into A8 text. The props communicates this to the scaler.
auto& props = (kN32_SkColorType == fColorType && paint.isSrcOver())
? fDeviceProps
: fBitmapFallbackProps;
SkPoint drawOrigin = glyphRunList.origin();
SkMatrix positionMatrix{drawMatrix};
positionMatrix.preTranslate(drawOrigin.x(), drawOrigin.y());
for (auto& glyphRun : glyphRunList) {
const SkFont& runFont = glyphRun.font();
rejected->setSource(glyphRun.source());
if (SkStrikeSpec::ShouldDrawAsPath(paint, runFont, positionMatrix)) {
auto [strikeSpec, strikeToSourceScale] =
SkStrikeSpec::MakePath(runFont, paint, props, fScalerContextFlags);
auto strike = strikeSpec.findOrCreateStrike();
accepted->startSource(rejected->source());
strike->prepareForPathDrawing(accepted, rejected);
rejected->flipRejectsToSource();
// The paint we draw paths with must have the same anti-aliasing state as the runFont
// allowing the paths to have the same edging as the glyph masks.
SkPaint pathPaint = paint;
pathPaint.setAntiAlias(runFont.hasSomeAntiAliasing());
const bool stroking = pathPaint.getStyle() != SkPaint::kFill_Style;
const bool hairline = pathPaint.getStrokeWidth() == 0;
const bool needsExactCTM = pathPaint.getShader() ||
pathPaint.getPathEffect() ||
pathPaint.getMaskFilter() ||
(stroking && !hairline);
if (!needsExactCTM) {
for (auto [variant, pos] : accepted->accepted()) {
const SkPath* path = variant.glyph()->path();
SkMatrix m;
SkPoint translate = drawOrigin + pos;
m.setScaleTranslate(strikeToSourceScale, strikeToSourceScale,
translate.x(), translate.y());
SkAutoCanvasRestore acr(canvas, true);
canvas->concat(m);
canvas->drawPath(*path, pathPaint);
}
} else {
for (auto [variant, pos] : accepted->accepted()) {
const SkPath* path = variant.glyph()->path();
SkMatrix m;
SkPoint translate = drawOrigin + pos;
m.setScaleTranslate(strikeToSourceScale, strikeToSourceScale,
translate.x(), translate.y());
SkPath deviceOutline;
path->transform(m, &deviceOutline);
deviceOutline.setIsVolatile(true);
canvas->drawPath(deviceOutline, pathPaint);
}
}
if (!rejected->source().empty()) {
accepted->startSource(rejected->source());
strike->prepareForDrawableDrawing(accepted, rejected);
rejected->flipRejectsToSource();
for (auto [variant, pos] : accepted->accepted()) {
SkDrawable* drawable = variant.glyph()->drawable();
SkMatrix m;
SkPoint translate = drawOrigin + pos;
m.setScaleTranslate(strikeToSourceScale, strikeToSourceScale,
translate.x(), translate.y());
SkAutoCanvasRestore acr(canvas, false);
SkRect drawableBounds = drawable->getBounds();
m.mapRect(&drawableBounds);
canvas->saveLayer(&drawableBounds, &paint);
drawable->draw(canvas, &m);
}
}
}
if (!rejected->source().empty() && !positionMatrix.hasPerspective()) {
SkStrikeSpec strikeSpec = SkStrikeSpec::MakeMask(
runFont, paint, props, fScalerContextFlags, positionMatrix);
auto strike = strikeSpec.findOrCreateStrike();
accepted->startDevicePositioning(
rejected->source(), positionMatrix, strike->roundingSpec());
strike->prepareForDrawingMasksCPU(accepted);
rejected->flipRejectsToSource();
bitmapDevice->paintMasks(accepted, paint);
}
if (!rejected->source().empty()) {
std::vector<SkPoint> sourcePositions;
// Create a strike is source space to calculate scale information.
SkStrikeSpec scaleStrikeSpec = SkStrikeSpec::MakeMask(
runFont, paint, props, fScalerContextFlags, SkMatrix::I());
SkBulkGlyphMetrics metrics{scaleStrikeSpec};
auto glyphIDs = rejected->source().get<0>();
auto positions = rejected->source().get<1>();
SkSpan<const SkGlyph*> glyphs = metrics.glyphs(glyphIDs);
SkScalar maxScale = SK_ScalarMin;
// Calculate the scale that makes the longest edge 1:1 with its side in the cache.
for (auto [glyph, pos] : SkMakeZip(glyphs, positions)) {
SkPoint corners[4];
SkPoint srcPos = pos + drawOrigin;
// Store off the positions in device space to position the glyphs during drawing.
sourcePositions.push_back(srcPos);
SkRect rect = glyph->rect();
rect.makeOffset(srcPos);
positionMatrix.mapRectToQuad(corners, rect);
// left top -> right top
SkScalar scale = (corners[1] - corners[0]).length() / rect.width();
maxScale = std::max(maxScale, scale);
// right top -> right bottom
scale = (corners[2] - corners[1]).length() / rect.height();
maxScale = std::max(maxScale, scale);
// right bottom -> left bottom
scale = (corners[3] - corners[2]).length() / rect.width();
maxScale = std::max(maxScale, scale);
// left bottom -> left top
scale = (corners[0] - corners[3]).length() / rect.height();
maxScale = std::max(maxScale, scale);
}
if (maxScale * runFont.getSize() > 256) {
maxScale = 256.0f / runFont.getSize();
}
SkMatrix cacheScale = SkMatrix::Scale(maxScale, maxScale);
SkStrikeSpec strikeSpec = SkStrikeSpec::MakeMask(
runFont, paint, props, fScalerContextFlags, cacheScale);
auto strike = strikeSpec.findOrCreateStrike();
// Figure out all the positions and packed glyphIDs based on the device matrix.
accepted->startDevicePositioning(
rejected->source(), positionMatrix, strike->roundingSpec());
strike->prepareForDrawingMasksCPU(accepted);
auto variants = accepted->accepted().get<0>();
for (auto [variant, srcPos] : SkMakeZip(variants, sourcePositions)) {
const SkGlyph* glyph = variant.glyph();
SkMask mask = glyph->mask();
// TODO: is this needed will A8 and BW just work?
if (mask.fFormat != SkMask::kARGB32_Format) {
continue;
}
SkBitmap bm;
bm.installPixels(SkImageInfo::MakeN32Premul(mask.fBounds.size()),
mask.fImage,
mask.fRowBytes);
// Since the glyph in the cache is scaled by maxScale, its top left vector is too
// long. Reduce it to find proper positions on the device.
SkPoint realPos =
srcPos + SkPoint::Make(mask.fBounds.left(), mask.fBounds.top())*(1.0f/maxScale);
// Calculate the preConcat matrix for drawBitmap to get the rectangle from the
// glyph cache (which is multiplied by maxScale) to land in the right place.
SkMatrix translate = SkMatrix::Translate(realPos);
translate.preScale(1.0f/maxScale, 1.0f/maxScale);
// Draw the bitmap using the rect from the scaled cache, and not the source
// rectangle for the glyph.
bitmapDevice->drawBitmap(
bm, translate, nullptr, SkSamplingOptions{SkFilterMode::kLinear},
paint);
}
rejected->flipRejectsToSource();
}
// TODO: have the mask stage above reject the glyphs that are too big, and handle the
// rejects in a more sophisticated stage.
}
}
// -- SkGlyphRunListPainter ------------------------------------------------------------------------
#if SK_SUPPORT_GPU
SkGlyphRunListPainter::SkGlyphRunListPainter(const SkSurfaceProps& props,
SkScalerContextFlags flags,
SkStrikeForGPUCacheInterface* strikeCache)
: fDeviceProps{props}
, fScalerContextFlags{flags}
, fStrikeCache{strikeCache} {}
SkGlyphRunListPainter::SkGlyphRunListPainter(const SkSurfaceProps& props,
const SkColorSpace* colorSpace,
SkStrikeForGPUCacheInterface* strikeCache)
: SkGlyphRunListPainter{props, compute_scaler_context_flags(colorSpace), strikeCache} {}
SkGlyphRunListPainter::SkGlyphRunListPainter(const skgpu::v1::SurfaceDrawContext& sdc)
: SkGlyphRunListPainter{sdc.surfaceProps(),
compute_scaler_context_flags(sdc.colorInfo().colorSpace()),
SkStrikeCache::GlobalStrikeCache()} {}
// Use the following in your args.gn to dump telemetry for diagnosing chrome Renderer/GPU
// differences.
// extra_cflags = ["-D", "SK_TRACE_GLYPH_RUN_PROCESS"]
namespace {
#if defined(SK_TRACE_GLYPH_RUN_PROCESS)
static const constexpr bool kTrace = true;
#else
static const constexpr bool kTrace = false;
#endif
}
void SkGlyphRunListPainter::categorizeGlyphRunList(SkGlyphRunPainterInterface* process,
const SkGlyphRunList& glyphRunList,
const SkMatrix& positionMatrix,
const SkPaint& runPaint,
const GrSDFTControl& control,
const char* tag) {
[[maybe_unused]] SkString msg;
if constexpr (kTrace) {
const uint64_t uniqueID = glyphRunList.uniqueID();
msg.appendf("\nStart glyph run processing");
if (tag != nullptr) {
msg.appendf(" for %s ", tag);
if (uniqueID != SK_InvalidUniqueID) {
msg.appendf(" uniqueID: %" PRIu64, uniqueID);
}
}
msg.appendf("\n matrix\n");
msg.appendf(" %7.3g %7.3g %7.3g\n %7.3g %7.3g %7.3g\n",
positionMatrix[0], positionMatrix[1], positionMatrix[2],
positionMatrix[3], positionMatrix[4], positionMatrix[5]);
}
auto bufferScope = SkSubRunBuffers::EnsureBuffers(glyphRunList);
auto [accepted, rejected] = bufferScope.buffers();
for (auto& glyphRun : glyphRunList) {
rejected->setSource(glyphRun.source());
const SkFont& runFont = glyphRun.font();
// Only consider using direct or SDFT drawing if not drawing hairlines and not perspective.
if ((runPaint.getStyle() != SkPaint::kStroke_Style || runPaint.getStrokeWidth() != 0)
&& !positionMatrix.hasPerspective()) {
SkScalar approximateDeviceTextSize =
SkFontPriv::ApproximateTransformedTextSize(runFont, positionMatrix);
if (control.isSDFT(approximateDeviceTextSize, runPaint)) {
// Process SDFT - This should be the .009% case.
const auto& [strikeSpec, strikeToSourceScale, matrixRange] =
SkStrikeSpec::MakeSDFT(
runFont, runPaint, fDeviceProps, positionMatrix, control);
if constexpr(kTrace) {
msg.appendf(" SDFT case:\n%s", strikeSpec.dump().c_str());
}
if (!SkScalarNearlyZero(strikeToSourceScale)) {
SkScopedStrikeForGPU strike = strikeSpec.findOrCreateScopedStrike(fStrikeCache);
accepted->startSource(rejected->source());
if constexpr (kTrace) {
msg.appendf(" glyphs:(x,y):\n %s\n", accepted->dumpInput().c_str());
}
strike->prepareForSDFTDrawing(accepted, rejected);
rejected->flipRejectsToSource();
if (process && !accepted->empty()) {
// processSourceSDFT must be called even if there are no glyphs to make sure
// runs are set correctly.
process->processSourceSDFT(accepted->accepted(),
strike->getUnderlyingStrike(),
strikeToSourceScale,
runFont,
matrixRange);
}
}
}
if (!rejected->source().empty()) {
// Process masks including ARGB - this should be the 99.99% case.
// This will handle medium size emoji that are sharing the run with SDFT drawn text.
// If things are too big they will be passed along to the drawing of last resort
// below.
SkStrikeSpec strikeSpec = SkStrikeSpec::MakeMask(
runFont, runPaint, fDeviceProps, fScalerContextFlags, positionMatrix);
if constexpr (kTrace) {
msg.appendf(" Mask case:\n%s", strikeSpec.dump().c_str());
}
SkScopedStrikeForGPU strike = strikeSpec.findOrCreateScopedStrike(fStrikeCache);
accepted->startDevicePositioning(
rejected->source(), positionMatrix, strike->roundingSpec());
if constexpr (kTrace) {
msg.appendf(" glyphs:(x,y):\n %s\n", accepted->dumpInput().c_str());
}
strike->prepareForMaskDrawing(accepted, rejected);
rejected->flipRejectsToSource();
if (process && !accepted->empty()) {
// processDeviceMasks must be called even if there are no glyphs to make sure
// runs are set correctly.
process->processDeviceMasks(
accepted->accepted(), strike->getUnderlyingStrike());
}
}
}
// Glyphs are generated in different scales relative to the source space. Masks are drawn
// in device space, and SDFT and Paths are draw in a fixed constant space. The
// maxDimensionInSourceSpace is used to calculate the factor from strike space to source
// space.
SkScalar maxDimensionInSourceSpace = 0.0;
if (!rejected->source().empty()) {
// Drawable case - handle big things with that have a drawable.
auto [strikeSpec, strikeToSourceScale] =
SkStrikeSpec::MakePath(runFont, runPaint, fDeviceProps, fScalerContextFlags);
if constexpr (kTrace) {
msg.appendf(" Drawable case:\n%s", strikeSpec.dump().c_str());
}
if (!SkScalarNearlyZero(strikeToSourceScale)) {
SkScopedStrikeForGPU strike = strikeSpec.findOrCreateScopedStrike(fStrikeCache);
accepted->startSource(rejected->source());
if constexpr (kTrace) {
msg.appendf(" glyphs:(x,y):\n %s\n", accepted->dumpInput().c_str());
}
strike->prepareForDrawableDrawing(accepted, rejected);
rejected->flipRejectsToSource();
auto [minHint, maxHint] = rejected->maxDimensionHint();
maxDimensionInSourceSpace = SkScalarCeilToScalar(maxHint * strikeToSourceScale);
if (process && !accepted->empty()) {
// processSourceDrawables must be called even if there are no glyphs to make
// sure runs are set correctly.
process->processSourceDrawables(accepted->accepted(),
strike->getUnderlyingStrike(),
strikeSpec.descriptor(),
strikeToSourceScale);
}
}
}
if (!rejected->source().empty()) {
// Path case - handle big things without color and that have a path.
auto [strikeSpec, strikeToSourceScale] =
SkStrikeSpec::MakePath(runFont, runPaint, fDeviceProps, fScalerContextFlags);
#if defined(SK_TRACE_GLYPH_RUN_PROCESS)
msg.appendf(" Path case:\n%s", strikeSpec.dump().c_str());
#endif
if (!SkScalarNearlyZero(strikeToSourceScale)) {
SkScopedStrikeForGPU strike = strikeSpec.findOrCreateScopedStrike(fStrikeCache);
accepted->startSource(rejected->source());
if constexpr (kTrace) {
msg.appendf(" glyphs:(x,y):\n %s\n", accepted->dumpInput().c_str());
}
strike->prepareForPathDrawing(accepted, rejected);
rejected->flipRejectsToSource();
auto [minHint, maxHint] = rejected->maxDimensionHint();
maxDimensionInSourceSpace = SkScalarCeilToScalar(maxHint * strikeToSourceScale);
if (process && !accepted->empty()) {
// processSourcePaths must be called even if there are no glyphs to make sure
// runs are set correctly.
process->processSourcePaths(accepted->accepted(),
runFont,
strikeSpec.descriptor(),
strikeToSourceScale);
}
}
}
if (!rejected->source().empty() && maxDimensionInSourceSpace != 0) {
// Draw of last resort. Scale the bitmap to the screen.
auto [strikeSpec, strikeToSourceScale] = SkStrikeSpec::MakeSourceFallback(
runFont, runPaint, fDeviceProps,
fScalerContextFlags, maxDimensionInSourceSpace);
if constexpr (kTrace) {
msg.appendf("Transformed case:\n%s", strikeSpec.dump().c_str());
}
if (!SkScalarNearlyZero(strikeToSourceScale)) {
SkScopedStrikeForGPU strike = strikeSpec.findOrCreateScopedStrike(fStrikeCache);
accepted->startSource(rejected->source());
if constexpr (kTrace) {
msg.appendf("glyphs:(x,y):\n %s\n", accepted->dumpInput().c_str());
}
strike->prepareForMaskDrawing(accepted, rejected);
rejected->flipRejectsToSource();
SkASSERT(rejected->source().empty());
if (process && !accepted->empty()) {
process->processSourceMasks(
accepted->accepted(), strike->getUnderlyingStrike(), strikeToSourceScale);
}
}
}
}
if constexpr (kTrace) {
msg.appendf("End glyph run processing");
if (tag != nullptr) {
msg.appendf(" for %s ", tag);
}
SkDebugf("%s\n", msg.c_str());
}
}
#endif // SK_SUPPORT_GPU
auto SkSubRunBuffers::EnsureBuffers(const SkGlyphRunList& glyphRunList) -> ScopedBuffers {
size_t size = 0;
for (const SkGlyphRun& run : glyphRunList) {
size = std::max(run.runSize(), size);
}
return ScopedBuffers(glyphRunList.buffers(), size);
}
SkSubRunBuffers::ScopedBuffers::ScopedBuffers(SkSubRunBuffers* buffers, size_t size)
: fBuffers{buffers} {
fBuffers->fAccepted.ensureSize(size);
}
SkSubRunBuffers::ScopedBuffers::~ScopedBuffers() {
fBuffers->fAccepted.reset();
fBuffers->fRejected.reset();
}
SkVector SkGlyphPositionRoundingSpec::HalfAxisSampleFreq(
bool isSubpixel, SkAxisAlignment axisAlignment) {
if (!isSubpixel) {
return {SK_ScalarHalf, SK_ScalarHalf};
} else {
switch (axisAlignment) {
case SkAxisAlignment::kX:
return {SkPackedGlyphID::kSubpixelRound, SK_ScalarHalf};
case SkAxisAlignment::kY:
return {SK_ScalarHalf, SkPackedGlyphID::kSubpixelRound};
case SkAxisAlignment::kNone:
return {SkPackedGlyphID::kSubpixelRound, SkPackedGlyphID::kSubpixelRound};
}
}
// Some compilers need this.
return {0, 0};
}
SkIPoint SkGlyphPositionRoundingSpec::IgnorePositionMask(
bool isSubpixel, SkAxisAlignment axisAlignment) {
return SkIPoint::Make((!isSubpixel || axisAlignment == SkAxisAlignment::kY) ? 0 : ~0,
(!isSubpixel || axisAlignment == SkAxisAlignment::kX) ? 0 : ~0);
}
SkIPoint SkGlyphPositionRoundingSpec::IgnorePositionFieldMask(bool isSubpixel,
SkAxisAlignment axisAlignment) {
SkIPoint ignoreMask = IgnorePositionMask(isSubpixel, axisAlignment);
SkIPoint answer{ignoreMask.x() & SkPackedGlyphID::kXYFieldMask.x(),
ignoreMask.y() & SkPackedGlyphID::kXYFieldMask.y()};
return answer;
}
SkGlyphPositionRoundingSpec::SkGlyphPositionRoundingSpec(
bool isSubpixel,SkAxisAlignment axisAlignment)
: halfAxisSampleFreq{HalfAxisSampleFreq(isSubpixel, axisAlignment)}
, ignorePositionMask{IgnorePositionMask(isSubpixel, axisAlignment)}
, ignorePositionFieldMask {IgnorePositionFieldMask(isSubpixel, axisAlignment)}{ }