Bug 1910869 - Support preblending in DrawTargetWebgl's glyph caching. r=aosmond

Even though glyph masks are stored without any baked-in color, the blending weights
they represent are preblended so as to be gamma-aware on macOS and Windows. That means
the color requested for the glyph alters the mask itself. It is not sufficient to
store a single mask that can be used for any color, rather, there must be different
masks stored for each potential preblend ramp that is generated by Skia.

Skia uses a quantization scheme where only the high bits of the color are used to
generate the preblend ramp so as to minimize the number of tables generated, so we do
our best to mimic that scheme and avoid generating an excessive number of masks. This
allows reuse of masks for some subset of colors, albeit not every color.

However, coaxing Skia into generating preblended masks without baking in the final
rasterized color requires some subversive use of SkShader to alter the queried luminance
color without changing the actual color of an SkPaint. An alternative invasive approach
would be to modify Skia to support this via a flag in SkPaint directly, but that would
require patching Skia directly, which might be necessary in the future should SkShader
become deprecated.

Differential Revision: https://phabricator.services.mozilla.com/D219242
This commit is contained in:
Lee Salzman 2024-08-15 22:50:24 +00:00
parent ba4d3872bc
commit f3426b8589
5 changed files with 140 additions and 63 deletions

View File

@ -37,6 +37,10 @@
#include "gfxPlatform.h"
#ifdef XP_MACOSX
# include "mozilla/gfx/ScaledFontMac.h"
#endif
namespace mozilla::gfx {
BackingTexture::BackingTexture(const IntSize& aSize, SurfaceFormat aFormat,
@ -4178,6 +4182,43 @@ static bool CheckForColorGlyphs(const RefPtr<SourceSurface>& aSurface) {
return false;
}
// Quantize the preblend color used to key the cache, as only the high bits are
// used to determine the amount of preblending. This avoids excessive cache use.
// This roughly matches the quantization used in WebRender and Skia.
static DeviceColor QuantizePreblendColor(const DeviceColor& aColor,
bool aUseSubpixelAA) {
int32_t r = int32_t(aColor.r * 255.0f + 0.5f);
int32_t g = int32_t(aColor.r * 255.0f + 0.5f);
int32_t b = int32_t(aColor.r * 255.0f + 0.5f);
// Ensure that even if two values would normally quantize to the same bucket,
// that the reference value within the bucket still allows for accurate
// determination of whether light-on-dark or dark-on-light rasterization will
// be used (as on macOS).
bool lightOnDark = r >= 85 && g >= 85 && b >= 85 && r + g + b >= 2 * 255;
// Skia only uses the high 3 bits of each color component to cache preblend
// ramp tables.
constexpr int32_t lumBits = 3;
constexpr int32_t ceilMask = (1 << (8 - lumBits)) - 1;
constexpr int32_t floorMask = ((1 << lumBits) - 1) << (8 - lumBits);
if (!aUseSubpixelAA) {
// If not using subpixel AA, then quantize only the luminance, stored in the
// G channel.
g = (r * 54 + g * 183 + b * 19) >> 8;
g |= ceilMask;
// Still distinguish between light and dark in the key.
r = b = lightOnDark ? 255 : 0;
} else if (lightOnDark) {
r |= ceilMask;
g |= ceilMask;
b |= ceilMask;
} else {
r &= floorMask;
g &= floorMask;
b &= floorMask;
}
return DeviceColor{r / 255.0f, g / 255.0f, b / 255.0f, 1.0f};
}
// Draws glyphs to the WebGL target by trying to generate a cached texture for
// the text run that can be subsequently reused to quickly render the text run
// without using any software surfaces.
@ -4209,18 +4250,24 @@ bool SharedContextWebgl::DrawGlyphsAccel(ScaledFont* aFont,
DeviceColor color = aOptions.mCompositionOp == CompositionOp::OP_CLEAR
? DeviceColor(1, 1, 1, 1)
: static_cast<const ColorPattern&>(aPattern).mColor;
#ifdef XP_MACOSX
// On macOS, depending on whether the text is classified as light-on-dark or
// dark-on-light, we may end up with different amounts of dilation applied, so
// we can't use the same mask in the two circumstances, or the glyphs will be
// dilated incorrectly.
bool lightOnDark =
useBitmaps || (color.r >= 0.33f && color.g >= 0.33f && color.b >= 0.33f &&
color.r + color.g + color.b >= 2.0f);
#if defined(XP_MACOSX) || defined(XP_WIN)
// macOS and Windows use gamma-aware blending.
bool usePreblend = aUseSubpixelAA;
#else
// On other platforms, we assume no color-dependent dilation.
const bool lightOnDark = true;
// FreeType backends currently don't use any preblending.
bool usePreblend = false;
#endif
#ifdef XP_MACOSX
// If font smoothing is requested, even if there is no subpixel AA, gamma-
// aware blending might be used and differing amounts of dilation might be
// applied.
if (aFont->GetType() == FontType::MAC &&
static_cast<ScaledFontMac*>(aFont)->UseFontSmoothing()) {
usePreblend = true;
}
#endif
// If the font has bitmaps, use the color directly. Otherwise, the texture
// will hold a grayscale mask, so encode the key's subpixel and light-or-dark
// state in the color.
@ -4231,9 +4278,9 @@ bool SharedContextWebgl::DrawGlyphsAccel(ScaledFont* aFont,
HashNumber hash =
GlyphCacheEntry::HashGlyphs(aBuffer, quantizeTransform, quantizeScale);
DeviceColor colorOrMask =
useBitmaps
? color
: DeviceColor::Mask(aUseSubpixelAA ? 1 : 0, lightOnDark ? 1 : 0);
useBitmaps ? color
: (usePreblend ? QuantizePreblendColor(color, aUseSubpixelAA)
: DeviceColor::Mask(aUseSubpixelAA ? 1 : 0, 1));
IntRect clipRect(IntPoint(), mViewportSize);
RefPtr<GlyphCacheEntry> entry =
cache->FindEntry(aBuffer, colorOrMask, quantizeTransform, quantizeScale,
@ -4302,16 +4349,9 @@ bool SharedContextWebgl::DrawGlyphsAccel(ScaledFont* aFont,
// wasn't valid. Render the text run into a temporary target.
RefPtr<DrawTargetSkia> textDT = new DrawTargetSkia;
if (textDT->Init(intBounds.Size(),
lightOnDark && !useBitmaps && !aUseSubpixelAA
? SurfaceFormat::A8
: SurfaceFormat::B8G8R8A8)) {
if (!lightOnDark) {
// If rendering dark-on-light text, we need to clear the background to
// white while using an opaque alpha value to allow this.
textDT->FillRect(Rect(IntRect(IntPoint(), intBounds.Size())),
ColorPattern(DeviceColor(1, 1, 1, 1)),
DrawOptions(1.0f, CompositionOp::OP_OVER));
}
useBitmaps || usePreblend || aUseSubpixelAA
? SurfaceFormat::B8G8R8A8
: SurfaceFormat::A8)) {
textDT->SetTransform(currentTransform *
Matrix::Translation(-intBounds.TopLeft()));
textDT->SetPermitSubpixelAA(aUseSubpixelAA);
@ -4319,42 +4359,20 @@ bool SharedContextWebgl::DrawGlyphsAccel(ScaledFont* aFont,
aOptions.mAntialiasMode);
// If bitmaps might be used, then we have to supply the color, as color
// emoji may ignore it while grayscale bitmaps may use it, with no way to
// know ahead of time. Otherwise, assume the output will be a mask and
// just render it white to determine intensity. Depending on whether the
// text is light or dark, we render white or black text respectively.
ColorPattern colorPattern(
useBitmaps ? color : DeviceColor::Mask(lightOnDark ? 1 : 0, 1));
if (aStrokeOptions) {
textDT->StrokeGlyphs(aFont, aBuffer, colorPattern, *aStrokeOptions,
drawOptions);
// know ahead of time. If we are using preblending in some form, then the
// output also will depend on the supplied color. Otherwise, assume the
// output will be a mask and just render it white to determine intensity.
if (!useBitmaps && usePreblend) {
textDT->DrawGlyphMask(aFont, aBuffer, color, aStrokeOptions,
drawOptions);
} else {
textDT->FillGlyphs(aFont, aBuffer, colorPattern, drawOptions);
}
if (!lightOnDark) {
uint8_t* data = nullptr;
IntSize size;
int32_t stride = 0;
SurfaceFormat format = SurfaceFormat::UNKNOWN;
if (!textDT->LockBits(&data, &size, &stride, &format)) {
return false;
ColorPattern colorPattern(useBitmaps ? color : DeviceColor(1, 1, 1, 1));
if (aStrokeOptions) {
textDT->StrokeGlyphs(aFont, aBuffer, colorPattern, *aStrokeOptions,
drawOptions);
} else {
textDT->FillGlyphs(aFont, aBuffer, colorPattern, drawOptions);
}
uint8_t* row = data;
for (int y = 0; y < size.height; ++y) {
uint8_t* px = row;
for (int x = 0; x < size.width; ++x) {
// If rendering dark-on-light text, we need to invert the final mask
// so that it is in the expected white text on transparent black
// format. The alpha will be initialized to the largest of the
// values.
px[0] = 255 - px[0];
px[1] = 255 - px[1];
px[2] = 255 - px[2];
px[3] = std::max(px[0], std::max(px[1], px[2]));
px += 4;
}
row += stride;
}
textDT->ReleaseBits(data);
}
RefPtr<SourceSurface> textSurface = textDT->Snapshot();
if (textSurface) {

View File

@ -24,6 +24,10 @@
#include "skia/include/core/SkRegion.h"
#include "skia/include/effects/SkImageFilters.h"
#include "skia/include/private/base/SkMalloc.h"
#include "skia/src/core/SkEffectPriv.h"
#include "skia/src/core/SkRasterPipeline.h"
#include "skia/src/core/SkWriteBuffer.h"
#include "skia/src/shaders/SkEmptyShader.h"
#include "Blur.h"
#include "Logging.h"
#include "Tools.h"
@ -1252,7 +1256,8 @@ static bool CanDrawFont(ScaledFont* aFont) {
void DrawTargetSkia::DrawGlyphs(ScaledFont* aFont, const GlyphBuffer& aBuffer,
const Pattern& aPattern,
const StrokeOptions* aStrokeOptions,
const DrawOptions& aOptions) {
const DrawOptions& aOptions,
SkShader* aShader) {
if (!CanDrawFont(aFont)) {
return;
}
@ -1288,6 +1293,10 @@ void DrawTargetSkia::DrawGlyphs(ScaledFont* aFont, const GlyphBuffer& aBuffer,
skiaFont->SetupSkFontDrawOptions(font);
if (aShader) {
paint.mPaint.setShader(sk_ref_sp(aShader));
}
// Limit the amount of internal batch allocations Skia does.
const uint32_t kMaxGlyphBatchSize = 8192;
@ -1306,6 +1315,51 @@ void DrawTargetSkia::DrawGlyphs(ScaledFont* aFont, const GlyphBuffer& aBuffer,
}
}
// This shader overrides the luminance color used to generate the preblend
// tables for glyphs, without actually changing the rasterized color. This is
// necesary for subpixel AA blending which requires both the mask and color
// as separate inputs.
class GlyphMaskShader : public SkEmptyShader {
public:
explicit GlyphMaskShader(const DeviceColor& aColor)
: mColor({aColor.r, aColor.g, aColor.b, aColor.a}) {}
bool onAsLuminanceColor(SkColor4f* aLum) const override {
*aLum = mColor;
return true;
}
bool isOpaque() const override { return true; }
bool isConstant() const override { return true; }
void flatten(SkWriteBuffer& buffer) const override {
buffer.writeColor4f(mColor);
}
bool appendStages(const SkStageRec& rec,
const SkShaders::MatrixRec&) const override {
rec.fPipeline->appendConstantColor(rec.fAlloc,
SkColor4f{1, 1, 1, 1}.premul().vec());
return true;
}
private:
SkColor4f mColor;
};
void DrawTargetSkia::DrawGlyphMask(ScaledFont* aFont,
const GlyphBuffer& aBuffer,
const DeviceColor& aColor,
const StrokeOptions* aStrokeOptions,
const DrawOptions& aOptions) {
// Draw a mask using the GlyphMaskShader that can be used for subpixel AA
// but that uses the gamma preblend weighting of the given color, even though
// the mask itself does not use that color.
sk_sp<GlyphMaskShader> shader = sk_make_sp<GlyphMaskShader>(aColor);
DrawGlyphs(aFont, aBuffer, ColorPattern(DeviceColor(1, 1, 1, 1)),
aStrokeOptions, aOptions, shader.get());
}
Maybe<Rect> DrawTargetSkia::GetGlyphLocalBounds(
ScaledFont* aFont, const GlyphBuffer& aBuffer, const Pattern& aPattern,
const StrokeOptions* aStrokeOptions, const DrawOptions& aOptions) {

View File

@ -16,6 +16,7 @@
#endif
class SkCanvas;
class SkShader;
class SkSurface;
namespace mozilla {
@ -162,6 +163,11 @@ class DrawTargetSkia : public DrawTarget {
const StrokeOptions* aStrokeOptions,
const DrawOptions& aOptions);
void DrawGlyphMask(ScaledFont* aFont, const GlyphBuffer& aBuffer,
const DeviceColor& aColor,
const StrokeOptions* aStrokeOptions = nullptr,
const DrawOptions& aOptions = DrawOptions());
private:
friend class SourceSurfaceSkia;
@ -172,7 +178,8 @@ class DrawTargetSkia : public DrawTarget {
void DrawGlyphs(ScaledFont* aFont, const GlyphBuffer& aBuffer,
const Pattern& aPattern,
const StrokeOptions* aStrokeOptions = nullptr,
const DrawOptions& aOptions = DrawOptions());
const DrawOptions& aOptions = DrawOptions(),
SkShader* aShader = nullptr);
struct PushedLayer {
PushedLayer(bool aOldPermitSubpixelAA, SourceSurface* aMask)

View File

@ -62,6 +62,8 @@ class ScaledFontMac : public ScaledFontBase {
bool UseSubpixelPosition() const override { return true; }
bool UseFontSmoothing() const { return mUseFontSmoothing; }
cairo_font_face_t* CreateCairoFontFace(
cairo_font_options_t* aFontOptions) override;

View File

@ -5,10 +5,6 @@
if (os == "android") and swgl: [OK, TIMEOUT]
if os == "mac": ERROR
[Test that drawing serif produces the same result between canvas and OffscreenCanvas in a Worker]
expected:
if os == "mac": TIMEOUT
[Test that drawing fantasy produces the same result between canvas and OffscreenCanvas in a Worker]
expected:
if os == "mac": TIMEOUT