зеркало из https://github.com/mozilla/moz-skia.git
Add special case circle blur for Ganesh
This makes the blurcircles bench go from ~33us to ~8us on Windows desktop. It will require layout test suppressions Review URL: https://codereview.chromium.org/1311583005
This commit is contained in:
Родитель
de5973b05b
Коммит
30c4cae7d3
|
@ -14,37 +14,43 @@
|
|||
|
||||
class BlurCirclesGM : public skiagm::GM {
|
||||
public:
|
||||
BlurCirclesGM()
|
||||
: fName("blurcircles") {
|
||||
}
|
||||
BlurCirclesGM() { }
|
||||
|
||||
protected:
|
||||
bool runAsBench() const override { return true; }
|
||||
|
||||
SkString onShortName() override {
|
||||
return fName;
|
||||
return SkString("blurcircles");
|
||||
}
|
||||
|
||||
SkISize onISize() override {
|
||||
return SkISize::Make(950, 950);
|
||||
}
|
||||
|
||||
void onOnceBeforeDraw() override {
|
||||
const float blurRadii[kNumBlurs] = { 1,5,10,20 };
|
||||
|
||||
for (int i = 0; i < kNumBlurs; ++i) {
|
||||
fBlurFilters[i].reset(SkBlurMaskFilter::Create(
|
||||
kNormal_SkBlurStyle,
|
||||
SkBlurMask::ConvertRadiusToSigma(SkIntToScalar(blurRadii[i])),
|
||||
SkBlurMaskFilter::kHighQuality_BlurFlag));
|
||||
}
|
||||
}
|
||||
|
||||
void onDraw(SkCanvas* canvas) override {
|
||||
canvas->scale(1.5f, 1.5f);
|
||||
canvas->translate(50,50);
|
||||
|
||||
const float blurRadii[] = { 1,5,10,20 };
|
||||
const int circleRadii[] = { 5,10,25,50 };
|
||||
for (size_t i = 0; i < SK_ARRAY_COUNT(blurRadii); ++i) {
|
||||
|
||||
for (size_t i = 0; i < kNumBlurs; ++i) {
|
||||
SkAutoCanvasRestore autoRestore(canvas, true);
|
||||
canvas->translate(0, SkIntToScalar(150*i));
|
||||
for (size_t j = 0; j < SK_ARRAY_COUNT(circleRadii); ++j) {
|
||||
SkMaskFilter* filter = SkBlurMaskFilter::Create(
|
||||
kNormal_SkBlurStyle,
|
||||
SkBlurMask::ConvertRadiusToSigma(SkIntToScalar(blurRadii[i])),
|
||||
SkBlurMaskFilter::kHighQuality_BlurFlag);
|
||||
SkPaint paint;
|
||||
paint.setColor(SK_ColorBLACK);
|
||||
paint.setMaskFilter(filter)->unref();
|
||||
paint.setMaskFilter(fBlurFilters[i]);
|
||||
|
||||
canvas->drawCircle(SkIntToScalar(50),SkIntToScalar(50),SkIntToScalar(circleRadii[j]),paint);
|
||||
canvas->translate(SkIntToScalar(150), 0);
|
||||
|
@ -52,7 +58,9 @@ protected:
|
|||
}
|
||||
}
|
||||
private:
|
||||
const SkString fName;
|
||||
static const int kNumBlurs = 4;
|
||||
|
||||
SkAutoTUnref<SkMaskFilter> fBlurFilters[kNumBlurs];
|
||||
|
||||
typedef skiagm::GM INHERITED;
|
||||
};
|
||||
|
|
|
@ -11,6 +11,9 @@
|
|||
#
|
||||
{
|
||||
'sources': [
|
||||
'<(skia_src_path)/effects/GrCircleBlurFragmentProcessor.cpp',
|
||||
'<(skia_src_path)/effects/GrCircleBlurFragmentProcessor.h',
|
||||
|
||||
'<(skia_src_path)/effects/Sk1DPathEffect.cpp',
|
||||
'<(skia_src_path)/effects/Sk2DPathEffect.cpp',
|
||||
'<(skia_src_path)/effects/SkAlphaThresholdFilter.cpp',
|
||||
|
|
|
@ -218,6 +218,16 @@ static inline int SkAlphaBlend255(S16CPU src, S16CPU dst, U8CPU alpha) {
|
|||
return dst + prod;
|
||||
}
|
||||
|
||||
static inline U8CPU SkUnitScalarClampToByte(SkScalar x) {
|
||||
if (x < 0) {
|
||||
return 0;
|
||||
}
|
||||
if (x >= SK_Scalar1) {
|
||||
return 255;
|
||||
}
|
||||
return SkScalarToFixed(x) >> 8;
|
||||
}
|
||||
|
||||
#define SK_R16_BITS 5
|
||||
#define SK_G16_BITS 6
|
||||
#define SK_B16_BITS 5
|
||||
|
|
|
@ -78,17 +78,31 @@ public:
|
|||
|
||||
/**
|
||||
* If asFragmentProcessor() fails the filter may be implemented on the GPU by a subclass
|
||||
* overriding filterMaskGPU (declared below). That code path requires constructing a src mask
|
||||
* as input. Since that is a potentially expensive operation, the subclass must also override
|
||||
* this function to indicate whether filterTextureMaskGPU would succeeed if the mask were to be
|
||||
* created.
|
||||
* overriding filterMaskGPU (declared below). That code path requires constructing a
|
||||
* src mask as input. Since that is a potentially expensive operation, the subclass must also
|
||||
* override this function to indicate whether filterTextureMaskGPU would succeeed if the mask
|
||||
* were to be created.
|
||||
*
|
||||
* 'maskRect' returns the device space portion of the mask that the filter needs. The mask
|
||||
* passed into 'filterMaskGPU' should have the same extent as 'maskRect' but be translated
|
||||
* to the upper-left corner of the mask (i.e., (maskRect.fLeft, maskRect.fTop) appears at
|
||||
* (0, 0) in the mask).
|
||||
* passed into 'filterMaskGPU' should have the same extent as 'maskRect' but be
|
||||
* translated to the upper-left corner of the mask (i.e., (maskRect.fLeft, maskRect.fTop)
|
||||
* appears at (0, 0) in the mask).
|
||||
*
|
||||
* Logically, how this works is:
|
||||
* canFilterMaskGPU is called
|
||||
* if (it returns true)
|
||||
* the returned mask rect is used for quick rejecting
|
||||
* either directFilterMaskGPU or directFilterRRectMaskGPU is then called
|
||||
* if (neither of them handle the blur)
|
||||
* the mask rect is used to generate the mask
|
||||
* filterMaskGPU is called to filter the mask
|
||||
*
|
||||
* TODO: this should work as:
|
||||
* if (canFilterMaskGPU(devShape, ...)) // rect, rrect, drrect, path
|
||||
* filterMaskGPU(devShape, ...)
|
||||
* this would hide the RRect special case and the mask generation
|
||||
*/
|
||||
virtual bool canFilterMaskGPU(const SkRect& devBounds,
|
||||
virtual bool canFilterMaskGPU(const SkRRect& devRRect,
|
||||
const SkIRect& clipBounds,
|
||||
const SkMatrix& ctm,
|
||||
SkRect* maskRect) const;
|
||||
|
|
|
@ -96,8 +96,13 @@ public:
|
|||
inline bool isRect() const { return kRect_Type == this->getType(); }
|
||||
inline bool isOval() const { return kOval_Type == this->getType(); }
|
||||
inline bool isSimple() const { return kSimple_Type == this->getType(); }
|
||||
// TODO: should isSimpleCircular & isCircle take a tolerance? This could help
|
||||
// instances where the mapping to device space is noisy.
|
||||
inline bool isSimpleCircular() const {
|
||||
return this->isSimple() && fRadii[0].fX == fRadii[0].fY;
|
||||
return this->isSimple() && SkScalarNearlyEqual(fRadii[0].fX, fRadii[0].fY);
|
||||
}
|
||||
inline bool isCircle() const {
|
||||
return this->isOval() && SkScalarNearlyEqual(fRadii[0].fX, fRadii[0].fY);
|
||||
}
|
||||
inline bool isNinePatch() const { return kNinePatch_Type == this->getType(); }
|
||||
inline bool isComplex() const { return kComplex_Type == this->getType(); }
|
||||
|
@ -140,6 +145,12 @@ public:
|
|||
return rr;
|
||||
}
|
||||
|
||||
static SkRRect MakeOval(const SkRect& oval) {
|
||||
SkRRect rr;
|
||||
rr.setOval(oval);
|
||||
return rr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set this RR to match the supplied oval. All x radii will equal half the
|
||||
* width and all y radii will equal half the height.
|
||||
|
|
|
@ -224,7 +224,7 @@ static inline bool SkScalarNearlyZero(SkScalar x,
|
|||
}
|
||||
|
||||
static inline bool SkScalarNearlyEqual(SkScalar x, SkScalar y,
|
||||
SkScalar tolerance = SK_ScalarNearlyZero) {
|
||||
SkScalar tolerance = SK_ScalarNearlyZero) {
|
||||
SkASSERT(tolerance >= 0);
|
||||
return SkScalarAbs(x-y) <= tolerance;
|
||||
}
|
||||
|
|
|
@ -70,21 +70,11 @@ void SkRGBToHSV(U8CPU r, U8CPU g, U8CPU b, SkScalar hsv[3]) {
|
|||
hsv[2] = v;
|
||||
}
|
||||
|
||||
static inline U8CPU UnitScalarToByte(SkScalar x) {
|
||||
if (x < 0) {
|
||||
return 0;
|
||||
}
|
||||
if (x >= SK_Scalar1) {
|
||||
return 255;
|
||||
}
|
||||
return SkScalarToFixed(x) >> 8;
|
||||
}
|
||||
|
||||
SkColor SkHSVToColor(U8CPU a, const SkScalar hsv[3]) {
|
||||
SkASSERT(hsv);
|
||||
|
||||
U8CPU s = UnitScalarToByte(hsv[1]);
|
||||
U8CPU v = UnitScalarToByte(hsv[2]);
|
||||
U8CPU s = SkUnitScalarClampToByte(hsv[1]);
|
||||
U8CPU v = SkUnitScalarClampToByte(hsv[2]);
|
||||
|
||||
if (0 == s) { // shade of gray
|
||||
return SkColorSetARGB(a, v, v, v);
|
||||
|
|
|
@ -309,7 +309,7 @@ bool SkMaskFilter::asFragmentProcessor(GrFragmentProcessor**, GrTexture*, const
|
|||
return false;
|
||||
}
|
||||
|
||||
bool SkMaskFilter::canFilterMaskGPU(const SkRect& devBounds,
|
||||
bool SkMaskFilter::canFilterMaskGPU(const SkRRect& devRRect,
|
||||
const SkIRect& clipBounds,
|
||||
const SkMatrix& ctm,
|
||||
SkRect* maskRect) const {
|
||||
|
|
|
@ -0,0 +1,259 @@
|
|||
|
||||
/*
|
||||
* 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 "GrCircleBlurFragmentProcessor.h"
|
||||
|
||||
#if SK_SUPPORT_GPU
|
||||
|
||||
#include "GrContext.h"
|
||||
#include "GrTextureProvider.h"
|
||||
|
||||
#include "gl/GrGLFragmentProcessor.h"
|
||||
#include "gl/builders/GrGLProgramBuilder.h"
|
||||
|
||||
class GrGLCircleBlurFragmentProcessor : public GrGLFragmentProcessor {
|
||||
public:
|
||||
GrGLCircleBlurFragmentProcessor(const GrProcessor&) {}
|
||||
void emitCode(EmitArgs&) override;
|
||||
|
||||
protected:
|
||||
void onSetData(const GrGLProgramDataManager&, const GrProcessor&) override;
|
||||
|
||||
private:
|
||||
GrGLProgramDataManager::UniformHandle fDataUniform;
|
||||
|
||||
typedef GrGLFragmentProcessor INHERITED;
|
||||
};
|
||||
|
||||
void GrGLCircleBlurFragmentProcessor::emitCode(EmitArgs& args) {
|
||||
|
||||
const char *dataName;
|
||||
|
||||
// The data is formatted as:
|
||||
// x,y - the center of the circle
|
||||
// z - the distance at which the intensity starts falling off (e.g., the start of the table)
|
||||
// w - the size of the profile texture
|
||||
fDataUniform = args.fBuilder->addUniform(GrGLProgramBuilder::kFragment_Visibility,
|
||||
kVec4f_GrSLType,
|
||||
kDefault_GrSLPrecision,
|
||||
"data",
|
||||
&dataName);
|
||||
|
||||
GrGLFragmentBuilder* fsBuilder = args.fBuilder->getFragmentShaderBuilder();
|
||||
const char *fragmentPos = fsBuilder->fragmentPosition();
|
||||
|
||||
if (args.fInputColor) {
|
||||
fsBuilder->codeAppendf("vec4 src=%s;", args.fInputColor);
|
||||
} else {
|
||||
fsBuilder->codeAppendf("vec4 src=vec4(1);");
|
||||
}
|
||||
|
||||
fsBuilder->codeAppendf("vec2 vec = %s.xy - %s.xy;", fragmentPos, dataName);
|
||||
fsBuilder->codeAppendf("float dist = (length(vec) - %s.z + 0.5) / %s.w;", dataName, dataName);
|
||||
|
||||
fsBuilder->codeAppendf("float intensity = ");
|
||||
fsBuilder->appendTextureLookup(args.fSamplers[0], "vec2(dist, 0.5)");
|
||||
fsBuilder->codeAppend(".a;");
|
||||
|
||||
fsBuilder->codeAppendf("%s = src * intensity;\n", args.fOutputColor );
|
||||
}
|
||||
|
||||
void GrGLCircleBlurFragmentProcessor::onSetData(const GrGLProgramDataManager& pdman,
|
||||
const GrProcessor& proc) {
|
||||
const GrCircleBlurFragmentProcessor& cbfp = proc.cast<GrCircleBlurFragmentProcessor>();
|
||||
const SkRect& circle = cbfp.circle();
|
||||
|
||||
// The data is formatted as:
|
||||
// x,y - the center of the circle
|
||||
// z - the distance at which the intensity starts falling off (e.g., the start of the table)
|
||||
// w - the size of the profile texture
|
||||
pdman.set4f(fDataUniform, circle.centerX(), circle.centerY(), cbfp.offset(),
|
||||
SkIntToScalar(cbfp.profileSize()));
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
GrCircleBlurFragmentProcessor::GrCircleBlurFragmentProcessor(const SkRect& circle,
|
||||
float sigma,
|
||||
float offset,
|
||||
GrTexture* blurProfile)
|
||||
: fCircle(circle)
|
||||
, fSigma(sigma)
|
||||
, fOffset(offset)
|
||||
, fBlurProfileAccess(blurProfile, GrTextureParams::kBilerp_FilterMode) {
|
||||
this->initClassID<GrCircleBlurFragmentProcessor>();
|
||||
this->addTextureAccess(&fBlurProfileAccess);
|
||||
this->setWillReadFragmentPosition();
|
||||
}
|
||||
|
||||
GrGLFragmentProcessor* GrCircleBlurFragmentProcessor::onCreateGLInstance() const {
|
||||
return new GrGLCircleBlurFragmentProcessor(*this);
|
||||
}
|
||||
|
||||
void GrCircleBlurFragmentProcessor::onGetGLProcessorKey(const GrGLSLCaps& caps,
|
||||
GrProcessorKeyBuilder* b) const {
|
||||
GrGLCircleBlurFragmentProcessor::GenKey(*this, caps, b);
|
||||
}
|
||||
|
||||
void GrCircleBlurFragmentProcessor::onComputeInvariantOutput(GrInvariantOutput* inout) const {
|
||||
inout->mulByUnknownSingleComponent();
|
||||
}
|
||||
|
||||
// Evaluate an AA circle function centered at the origin with 'radius' at (x,y)
|
||||
static inline float disk(float x, float y, float radius) {
|
||||
float distSq = x*x + y*y;
|
||||
if (distSq <= (radius-0.5f)*(radius-0.5f)) {
|
||||
return 1.0f;
|
||||
} else if (distSq >= (radius+0.5f)*(radius+0.5f)) {
|
||||
return 0.0f;
|
||||
} else {
|
||||
float ramp = radius + 0.5f - sqrt(distSq);
|
||||
SkASSERT(ramp >= 0.0f && ramp <= 1.0f);
|
||||
return ramp;
|
||||
}
|
||||
}
|
||||
|
||||
// Create the top half of an even-sized Gaussian kernel
|
||||
static void make_half_kernel(float* kernel, int kernelWH, float sigma) {
|
||||
SkASSERT(!(kernelWH & 1));
|
||||
|
||||
const float kernelOff = (kernelWH-1)/2.0f;
|
||||
|
||||
float b = 1.0f / (2.0f * sigma * sigma);
|
||||
// omit the scale term since we're just going to renormalize
|
||||
|
||||
float tot = 0.0f;
|
||||
for (int y = 0; y < kernelWH/2; ++y) {
|
||||
for (int x = 0; x < kernelWH/2; ++x) {
|
||||
// TODO: use a cheap approximation of the 2D Guassian?
|
||||
float x2 = (x-kernelOff) * (x-kernelOff);
|
||||
float y2 = (y-kernelOff) * (y-kernelOff);
|
||||
// The kernel is symmetric so only compute it once for both sides
|
||||
kernel[y*kernelWH+(kernelWH-x-1)] = kernel[y*kernelWH+x] = exp(-(x2 + y2) * b);
|
||||
tot += 2.0f * kernel[y*kernelWH+x];
|
||||
}
|
||||
}
|
||||
// Still normalize the half kernel to 1.0 (rather than 0.5) so we don't
|
||||
// have to scale by 2.0 after convolution.
|
||||
for (int y = 0; y < kernelWH/2; ++y) {
|
||||
for (int x = 0; x < kernelWH; ++x) {
|
||||
kernel[y*kernelWH+x] /= tot;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply the half-kernel at 't' away from the center of the circle
|
||||
static uint8_t eval_at(float t, float halfWidth, float* halfKernel, int kernelWH) {
|
||||
SkASSERT(!(kernelWH & 1));
|
||||
|
||||
const float kernelOff = (kernelWH-1)/2.0f;
|
||||
|
||||
float acc = 0;
|
||||
|
||||
for (int y = 0; y < kernelWH/2; ++y) {
|
||||
if (kernelOff-y > halfWidth+0.5f) {
|
||||
// All disk() samples in this row will be 0.0f
|
||||
continue;
|
||||
}
|
||||
|
||||
for (int x = 0; x < kernelWH; ++x) {
|
||||
float image = disk(t - kernelOff + x, -kernelOff + y, halfWidth);
|
||||
float kernel = halfKernel[y*kernelWH+x];
|
||||
acc += kernel * image;
|
||||
}
|
||||
}
|
||||
|
||||
return SkUnitScalarClampToByte(acc);
|
||||
}
|
||||
|
||||
static inline void compute_profile_offset_and_size(float halfWH, float sigma,
|
||||
float* offset, int* size) {
|
||||
|
||||
if (3*sigma <= halfWH) {
|
||||
// The circle is bigger than the Gaussian. In this case we know the interior of the
|
||||
// blurred circle is solid.
|
||||
*offset = halfWH - 3 * sigma; // This location maps to 0.5f in the weights texture.
|
||||
// It should always be 255.
|
||||
*size = SkScalarCeilToInt(6*sigma);
|
||||
} else {
|
||||
// The Gaussian is bigger than the circle.
|
||||
*offset = 0.0f;
|
||||
*size = SkScalarCeilToInt(halfWH + 3*sigma);
|
||||
}
|
||||
}
|
||||
|
||||
static uint8_t* create_profile(float halfWH, float sigma) {
|
||||
|
||||
int kernelWH = SkScalarCeilToInt(6.0f*sigma);
|
||||
kernelWH = (kernelWH + 1) & ~1; // make it the next even number up
|
||||
|
||||
SkAutoTArray<float> halfKernel(kernelWH*kernelWH/2);
|
||||
|
||||
make_half_kernel(halfKernel.get(), kernelWH, sigma);
|
||||
|
||||
float offset;
|
||||
int numSteps;
|
||||
|
||||
compute_profile_offset_and_size(halfWH, sigma, &offset, &numSteps);
|
||||
|
||||
uint8_t* weights = new uint8_t[numSteps];
|
||||
for (int i = 0; i < numSteps; ++i) {
|
||||
weights[i] = eval_at(offset+i, halfWH, halfKernel.get(), kernelWH);
|
||||
}
|
||||
|
||||
return weights;
|
||||
}
|
||||
|
||||
GrTexture* GrCircleBlurFragmentProcessor::CreateCircleBlurProfileTexture(
|
||||
GrTextureProvider* textureProvider,
|
||||
const SkRect& circle,
|
||||
float sigma,
|
||||
float* offset) {
|
||||
float halfWH = circle.width() / 2.0f;
|
||||
|
||||
int size;
|
||||
compute_profile_offset_and_size(halfWH, sigma, offset, &size);
|
||||
|
||||
GrSurfaceDesc texDesc;
|
||||
texDesc.fWidth = size;
|
||||
texDesc.fHeight = 1;
|
||||
texDesc.fConfig = kAlpha_8_GrPixelConfig;
|
||||
|
||||
static const GrUniqueKey::Domain kDomain = GrUniqueKey::GenerateDomain();
|
||||
GrUniqueKey key;
|
||||
GrUniqueKey::Builder builder(&key, kDomain, 2);
|
||||
// The profile curve varies with both the sigma of the Gaussian and the size of the
|
||||
// disk. Quantizing to 16.16 should be close enough though.
|
||||
builder[0] = SkScalarToFixed(sigma);
|
||||
builder[1] = SkScalarToFixed(halfWH);
|
||||
builder.finish();
|
||||
|
||||
GrTexture *blurProfile = textureProvider->findAndRefTextureByUniqueKey(key);
|
||||
|
||||
if (!blurProfile) {
|
||||
SkAutoTDeleteArray<uint8_t> profile(create_profile(halfWH, sigma));
|
||||
|
||||
blurProfile = textureProvider->createTexture(texDesc, true, profile.get(), 0);
|
||||
if (blurProfile) {
|
||||
textureProvider->assignUniqueKeyToTexture(key, blurProfile);
|
||||
}
|
||||
}
|
||||
|
||||
return blurProfile;
|
||||
}
|
||||
|
||||
GR_DEFINE_FRAGMENT_PROCESSOR_TEST(GrCircleBlurFragmentProcessor);
|
||||
|
||||
const GrFragmentProcessor* GrCircleBlurFragmentProcessor::TestCreate(GrProcessorTestData* d) {
|
||||
SkScalar wh = d->fRandom->nextRangeScalar(100.f, 1000.f);
|
||||
SkScalar sigma = d->fRandom->nextRangeF(1.f,10.f);
|
||||
SkRect circle = SkRect::MakeWH(wh, wh);
|
||||
return GrCircleBlurFragmentProcessor::Create(d->fContext->textureProvider(), circle, sigma);
|
||||
}
|
||||
|
||||
#endif
|
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
* 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 GrCircleBlurFragmentProcessor_DEFINED
|
||||
#define GrCircleBlurFragmentProcessor_DEFINED
|
||||
|
||||
#include "SkTypes.h"
|
||||
|
||||
#if SK_SUPPORT_GPU
|
||||
|
||||
#include "GrFragmentProcessor.h"
|
||||
#include "GrProcessorUnitTest.h"
|
||||
|
||||
class GrTextureProvider;
|
||||
|
||||
// This FP handles the special case of a blurred circle. It uses a 1D
|
||||
// profile that is just rotated about the origin of the circle.
|
||||
class GrCircleBlurFragmentProcessor : public GrFragmentProcessor {
|
||||
public:
|
||||
~GrCircleBlurFragmentProcessor() override {};
|
||||
|
||||
const char* name() const override { return "CircleBlur"; }
|
||||
|
||||
static const GrFragmentProcessor* Create(GrTextureProvider*textureProvider,
|
||||
const SkRect& circle, float sigma) {
|
||||
float offset;
|
||||
|
||||
SkAutoTUnref<GrTexture> blurProfile(CreateCircleBlurProfileTexture(textureProvider,
|
||||
circle,
|
||||
sigma,
|
||||
&offset));
|
||||
if (!blurProfile) {
|
||||
return nullptr;
|
||||
}
|
||||
return new GrCircleBlurFragmentProcessor(circle, sigma, offset, blurProfile);
|
||||
}
|
||||
|
||||
const SkRect& circle() const { return fCircle; }
|
||||
float sigma() const { return fSigma; }
|
||||
float offset() const { return fOffset; }
|
||||
int profileSize() const { return fBlurProfileAccess.getTexture()->width(); }
|
||||
|
||||
private:
|
||||
GrCircleBlurFragmentProcessor(const SkRect& circle, float sigma,
|
||||
float offset, GrTexture* blurProfile);
|
||||
|
||||
GrGLFragmentProcessor* onCreateGLInstance() const override;
|
||||
|
||||
void onGetGLProcessorKey(const GrGLSLCaps& caps, GrProcessorKeyBuilder* b) const override;
|
||||
|
||||
bool onIsEqual(const GrFragmentProcessor& other) const override {
|
||||
const GrCircleBlurFragmentProcessor& cbfp = other.cast<GrCircleBlurFragmentProcessor>();
|
||||
// fOffset is computed from the circle width and the sigma
|
||||
return this->circle().width() == cbfp.circle().width() && fSigma == cbfp.fSigma;
|
||||
}
|
||||
|
||||
void onComputeInvariantOutput(GrInvariantOutput* inout) const override;
|
||||
|
||||
static GrTexture* CreateCircleBlurProfileTexture(GrTextureProvider*,
|
||||
const SkRect& circle,
|
||||
float sigma, float* offset);
|
||||
|
||||
SkRect fCircle;
|
||||
float fSigma;
|
||||
float fOffset;
|
||||
GrTextureAccess fBlurProfileAccess;
|
||||
|
||||
GR_DECLARE_FRAGMENT_PROCESSOR_TEST;
|
||||
|
||||
typedef GrFragmentProcessor INHERITED;
|
||||
};
|
||||
|
||||
#endif
|
||||
#endif
|
|
@ -678,7 +678,7 @@ static float gaussianIntegral(float x) {
|
|||
memory returned in profile_out.
|
||||
*/
|
||||
|
||||
void SkBlurMask::ComputeBlurProfile(SkScalar sigma, uint8_t **profile_out) {
|
||||
uint8_t* SkBlurMask::ComputeBlurProfile(SkScalar sigma) {
|
||||
int size = SkScalarCeilToInt(6*sigma);
|
||||
|
||||
int center = size >> 1;
|
||||
|
@ -693,7 +693,7 @@ void SkBlurMask::ComputeBlurProfile(SkScalar sigma, uint8_t **profile_out) {
|
|||
profile[x] = 255 - (uint8_t) (255.f * gi);
|
||||
}
|
||||
|
||||
*profile_out = profile;
|
||||
return profile;
|
||||
}
|
||||
|
||||
// TODO MAYBE: Maintain a profile cache to avoid recomputing this for
|
||||
|
@ -769,10 +769,8 @@ bool SkBlurMask::BlurRect(SkScalar sigma, SkMask *dst,
|
|||
}
|
||||
return true;
|
||||
}
|
||||
uint8_t *profile = nullptr;
|
||||
|
||||
ComputeBlurProfile(sigma, &profile);
|
||||
SkAutoTDeleteArray<uint8_t> ada(profile);
|
||||
SkAutoTDeleteArray<uint8_t> profile(ComputeBlurProfile(sigma));
|
||||
|
||||
size_t dstSize = dst->computeImageSize();
|
||||
if (0 == dstSize) {
|
||||
|
@ -791,8 +789,8 @@ bool SkBlurMask::BlurRect(SkScalar sigma, SkMask *dst,
|
|||
SkAutoTMalloc<uint8_t> horizontalScanline(dstWidth);
|
||||
SkAutoTMalloc<uint8_t> verticalScanline(dstHeight);
|
||||
|
||||
ComputeBlurredScanline(horizontalScanline, profile, dstWidth, sigma);
|
||||
ComputeBlurredScanline(verticalScanline, profile, dstHeight, sigma);
|
||||
ComputeBlurredScanline(horizontalScanline, profile.get(), dstWidth, sigma);
|
||||
ComputeBlurredScanline(verticalScanline, profile.get(), dstHeight, sigma);
|
||||
|
||||
for (int y = 0 ; y < dstHeight ; ++y) {
|
||||
for (int x = 0 ; x < dstWidth ; x++) {
|
||||
|
|
|
@ -54,15 +54,14 @@ public:
|
|||
@param blurred_width The width of the final, blurred rectangle
|
||||
@param sharp_width The width of the original, unblurred rectangle.
|
||||
*/
|
||||
static uint8_t ProfileLookup(const uint8_t* profile, int loc, int blurred_width, int sharp_width);
|
||||
static uint8_t ProfileLookup(const uint8_t* profile, int loc, int blurredWidth, int sharpWidth);
|
||||
|
||||
/** Allocate memory for and populate the profile of a 1D blurred halfplane. The caller
|
||||
must free the memory. The amount of memory allocated will be exactly 6*sigma bytes.
|
||||
@param sigma The standard deviation of the gaussian blur kernel
|
||||
@param profile_out The location to store the allocated profile curve
|
||||
*/
|
||||
|
||||
static void ComputeBlurProfile(SkScalar sigma, uint8_t** profile_out);
|
||||
static uint8_t* ComputeBlurProfile(SkScalar sigma);
|
||||
|
||||
/** Compute an entire scanline of a blurred step function. This is a 1D helper that
|
||||
will produce both the horizontal and vertical profiles of the blurry rectangle.
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
#include "SkStrokeRec.h"
|
||||
|
||||
#if SK_SUPPORT_GPU
|
||||
#include "GrCircleBlurFragmentProcessor.h"
|
||||
#include "GrContext.h"
|
||||
#include "GrDrawContext.h"
|
||||
#include "GrTexture.h"
|
||||
|
@ -44,7 +45,7 @@ public:
|
|||
SkIPoint* margin) const override;
|
||||
|
||||
#if SK_SUPPORT_GPU
|
||||
bool canFilterMaskGPU(const SkRect& devBounds,
|
||||
bool canFilterMaskGPU(const SkRRect& devRRect,
|
||||
const SkIRect& clipBounds,
|
||||
const SkMatrix& ctm,
|
||||
SkRect* maskRect) const override;
|
||||
|
@ -166,27 +167,25 @@ bool SkBlurMaskFilterImpl::asABlur(BlurRec* rec) const {
|
|||
|
||||
bool SkBlurMaskFilterImpl::filterMask(SkMask* dst, const SkMask& src,
|
||||
const SkMatrix& matrix,
|
||||
SkIPoint* margin) const{
|
||||
SkIPoint* margin) const {
|
||||
SkScalar sigma = this->computeXformedSigma(matrix);
|
||||
return SkBlurMask::BoxBlur(dst, src, sigma, fBlurStyle, this->getQuality(), margin);
|
||||
}
|
||||
|
||||
bool SkBlurMaskFilterImpl::filterRectMask(SkMask* dst, const SkRect& r,
|
||||
const SkMatrix& matrix,
|
||||
SkIPoint* margin, SkMask::CreateMode createMode) const{
|
||||
SkIPoint* margin, SkMask::CreateMode createMode) const {
|
||||
SkScalar sigma = computeXformedSigma(matrix);
|
||||
|
||||
return SkBlurMask::BlurRect(sigma, dst, r, fBlurStyle,
|
||||
margin, createMode);
|
||||
return SkBlurMask::BlurRect(sigma, dst, r, fBlurStyle, margin, createMode);
|
||||
}
|
||||
|
||||
bool SkBlurMaskFilterImpl::filterRRectMask(SkMask* dst, const SkRRect& r,
|
||||
const SkMatrix& matrix,
|
||||
SkIPoint* margin, SkMask::CreateMode createMode) const{
|
||||
SkIPoint* margin, SkMask::CreateMode createMode) const {
|
||||
SkScalar sigma = computeXformedSigma(matrix);
|
||||
|
||||
return SkBlurMask::BlurRRect(sigma, dst, r, fBlurStyle,
|
||||
margin, createMode);
|
||||
return SkBlurMask::BlurRRect(sigma, dst, r, fBlurStyle, margin, createMode);
|
||||
}
|
||||
|
||||
#include "SkCanvas.h"
|
||||
|
@ -607,51 +606,42 @@ class GrGLRectBlurEffect;
|
|||
|
||||
class GrRectBlurEffect : public GrFragmentProcessor {
|
||||
public:
|
||||
virtual ~GrRectBlurEffect();
|
||||
~GrRectBlurEffect() override { }
|
||||
|
||||
const char* name() const override { return "RectBlur"; }
|
||||
|
||||
/**
|
||||
* Create a simple filter effect with custom bicubic coefficients.
|
||||
*/
|
||||
static GrFragmentProcessor* Create(GrTextureProvider *textureProvider, const SkRect& rect,
|
||||
float sigma) {
|
||||
GrTexture *blurProfileTexture = nullptr;
|
||||
static GrFragmentProcessor* Create(GrTextureProvider *textureProvider,
|
||||
const SkRect& rect, float sigma) {
|
||||
int doubleProfileSize = SkScalarCeilToInt(12*sigma);
|
||||
|
||||
if (doubleProfileSize >= rect.width() || doubleProfileSize >= rect.height()) {
|
||||
// if the blur sigma is too large so the gaussian overlaps the whole
|
||||
// rect in either direction, fall back to CPU path for now.
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool createdBlurProfileTexture = CreateBlurProfileTexture(
|
||||
textureProvider, sigma, &blurProfileTexture);
|
||||
SkAutoTUnref<GrTexture> hunref(blurProfileTexture);
|
||||
if (!createdBlurProfileTexture) {
|
||||
SkAutoTUnref<GrTexture> blurProfile(CreateBlurProfileTexture(textureProvider, sigma));
|
||||
if (!blurProfile) {
|
||||
return nullptr;
|
||||
}
|
||||
return new GrRectBlurEffect(rect, sigma, blurProfileTexture);
|
||||
return new GrRectBlurEffect(rect, sigma, blurProfile);
|
||||
}
|
||||
|
||||
const SkRect& getRect() const { return fRect; }
|
||||
float getSigma() const { return fSigma; }
|
||||
|
||||
private:
|
||||
GrRectBlurEffect(const SkRect& rect, float sigma, GrTexture *blurProfile);
|
||||
|
||||
GrGLFragmentProcessor* onCreateGLInstance() const override;
|
||||
|
||||
GrRectBlurEffect(const SkRect& rect, float sigma, GrTexture *blur_profile);
|
||||
|
||||
virtual void onGetGLProcessorKey(const GrGLSLCaps& caps,
|
||||
GrProcessorKeyBuilder* b) const override;
|
||||
void onGetGLProcessorKey(const GrGLSLCaps& caps, GrProcessorKeyBuilder* b) const override;
|
||||
|
||||
bool onIsEqual(const GrFragmentProcessor&) const override;
|
||||
|
||||
void onComputeInvariantOutput(GrInvariantOutput* inout) const override;
|
||||
|
||||
static bool CreateBlurProfileTexture(GrTextureProvider*, float sigma,
|
||||
GrTexture **blurProfileTexture);
|
||||
static GrTexture* CreateBlurProfileTexture(GrTextureProvider*, float sigma);
|
||||
|
||||
SkRect fRect;
|
||||
float fSigma;
|
||||
|
@ -665,7 +655,7 @@ private:
|
|||
class GrGLRectBlurEffect : public GrGLFragmentProcessor {
|
||||
public:
|
||||
GrGLRectBlurEffect(const GrProcessor&) {}
|
||||
virtual void emitCode(EmitArgs&) override;
|
||||
void emitCode(EmitArgs&) override;
|
||||
|
||||
protected:
|
||||
void onSetData(const GrGLProgramDataManager&, const GrProcessor&) override;
|
||||
|
@ -738,7 +728,7 @@ void GrGLRectBlurEffect::emitCode(EmitArgs& args) {
|
|||
}
|
||||
|
||||
void GrGLRectBlurEffect::onSetData(const GrGLProgramDataManager& pdman,
|
||||
const GrProcessor& proc) {
|
||||
const GrProcessor& proc) {
|
||||
const GrRectBlurEffect& rbe = proc.cast<GrRectBlurEffect>();
|
||||
SkRect rect = rbe.getRect();
|
||||
|
||||
|
@ -746,8 +736,8 @@ void GrGLRectBlurEffect::onSetData(const GrGLProgramDataManager& pdman,
|
|||
pdman.set1f(fProfileSizeUniform, SkScalarCeilToScalar(6*rbe.getSigma()));
|
||||
}
|
||||
|
||||
bool GrRectBlurEffect::CreateBlurProfileTexture(GrTextureProvider* textureProvider, float sigma,
|
||||
GrTexture **blurProfileTexture) {
|
||||
GrTexture* GrRectBlurEffect::CreateBlurProfileTexture(GrTextureProvider* textureProvider,
|
||||
float sigma) {
|
||||
GrSurfaceDesc texDesc;
|
||||
|
||||
unsigned int profileSize = SkScalarCeilToInt(6*sigma);
|
||||
|
@ -762,40 +752,29 @@ bool GrRectBlurEffect::CreateBlurProfileTexture(GrTextureProvider* textureProvid
|
|||
builder[0] = profileSize;
|
||||
builder.finish();
|
||||
|
||||
uint8_t *profile = nullptr;
|
||||
SkAutoTDeleteArray<uint8_t> ada(nullptr);
|
||||
GrTexture *blurProfile = textureProvider->findAndRefTextureByUniqueKey(key);
|
||||
|
||||
*blurProfileTexture = textureProvider->findAndRefTextureByUniqueKey(key);
|
||||
if (!blurProfile) {
|
||||
SkAutoTDeleteArray<uint8_t> profile(SkBlurMask::ComputeBlurProfile(sigma));
|
||||
|
||||
if (nullptr == *blurProfileTexture) {
|
||||
|
||||
SkBlurMask::ComputeBlurProfile(sigma, &profile);
|
||||
ada.reset(profile);
|
||||
|
||||
*blurProfileTexture = textureProvider->createTexture(texDesc, true, profile, 0);
|
||||
|
||||
if (nullptr == *blurProfileTexture) {
|
||||
return false;
|
||||
blurProfile = textureProvider->createTexture(texDesc, true, profile.get(), 0);
|
||||
if (blurProfile) {
|
||||
textureProvider->assignUniqueKeyToTexture(key, blurProfile);
|
||||
}
|
||||
textureProvider->assignUniqueKeyToTexture(key, *blurProfileTexture);
|
||||
}
|
||||
|
||||
return true;
|
||||
return blurProfile;
|
||||
}
|
||||
|
||||
GrRectBlurEffect::GrRectBlurEffect(const SkRect& rect, float sigma,
|
||||
GrTexture *blur_profile)
|
||||
: fRect(rect),
|
||||
fSigma(sigma),
|
||||
fBlurProfileAccess(blur_profile) {
|
||||
GrRectBlurEffect::GrRectBlurEffect(const SkRect& rect, float sigma, GrTexture *blurProfile)
|
||||
: fRect(rect)
|
||||
, fSigma(sigma)
|
||||
, fBlurProfileAccess(blurProfile) {
|
||||
this->initClassID<GrRectBlurEffect>();
|
||||
this->addTextureAccess(&fBlurProfileAccess);
|
||||
this->setWillReadFragmentPosition();
|
||||
}
|
||||
|
||||
GrRectBlurEffect::~GrRectBlurEffect() {
|
||||
}
|
||||
|
||||
void GrRectBlurEffect::onGetGLProcessorKey(const GrGLSLCaps& caps,
|
||||
GrProcessorKeyBuilder* b) const {
|
||||
GrGLRectBlurEffect::GenKey(*this, caps, b);
|
||||
|
@ -839,22 +818,31 @@ bool SkBlurMaskFilterImpl::directFilterMaskGPU(GrTextureProvider* texProvider,
|
|||
return false;
|
||||
}
|
||||
|
||||
SkRect rect;
|
||||
if (!path.isRect(&rect)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO: we could handle blurred stroked circles
|
||||
if (!strokeRec.isFillStyle()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
SkScalar xformedSigma = this->computeXformedSigma(viewMatrix);
|
||||
|
||||
int pad = SkScalarCeilToInt(6*xformedSigma)/2;
|
||||
rect.outset(SkIntToScalar(pad), SkIntToScalar(pad));
|
||||
SkAutoTUnref<const GrFragmentProcessor> fp;
|
||||
|
||||
SkRect rect;
|
||||
if (path.isRect(&rect)) {
|
||||
int pad = SkScalarCeilToInt(6*xformedSigma)/2;
|
||||
rect.outset(SkIntToScalar(pad), SkIntToScalar(pad));
|
||||
|
||||
fp.reset(GrRectBlurEffect::Create(texProvider, rect, xformedSigma));
|
||||
} else if (path.isOval(&rect) && SkScalarNearlyEqual(rect.width(), rect.height())) {
|
||||
fp.reset(GrCircleBlurFragmentProcessor::Create(texProvider, rect, xformedSigma));
|
||||
|
||||
// expand the rect for the coverage geometry
|
||||
int pad = SkScalarCeilToInt(6*xformedSigma)/2;
|
||||
rect.outset(SkIntToScalar(pad), SkIntToScalar(pad));
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
SkAutoTUnref<GrFragmentProcessor> fp(GrRectBlurEffect::Create(
|
||||
texProvider, rect, xformedSigma));
|
||||
if (!fp) {
|
||||
return false;
|
||||
}
|
||||
|
@ -870,10 +858,12 @@ bool SkBlurMaskFilterImpl::directFilterMaskGPU(GrTextureProvider* texProvider,
|
|||
return true;
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
class GrRRectBlurEffect : public GrFragmentProcessor {
|
||||
public:
|
||||
|
||||
static GrFragmentProcessor* Create(GrTextureProvider*, float sigma, const SkRRect&);
|
||||
static const GrFragmentProcessor* Create(GrTextureProvider*, float sigma, const SkRRect&);
|
||||
|
||||
virtual ~GrRRectBlurEffect() {};
|
||||
const char* name() const override { return "GrRRectBlur"; }
|
||||
|
@ -903,8 +893,12 @@ private:
|
|||
};
|
||||
|
||||
|
||||
GrFragmentProcessor* GrRRectBlurEffect::Create(GrTextureProvider* texProvider, float sigma,
|
||||
const SkRRect& rrect) {
|
||||
const GrFragmentProcessor* GrRRectBlurEffect::Create(GrTextureProvider* texProvider, float sigma,
|
||||
const SkRRect& rrect) {
|
||||
if (rrect.isCircle()) {
|
||||
return GrCircleBlurFragmentProcessor::Create(texProvider, rrect.rect(), sigma);
|
||||
}
|
||||
|
||||
if (!rrect.isSimpleCircular()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
@ -1129,8 +1123,8 @@ bool SkBlurMaskFilterImpl::directFilterRRectMaskGPU(GrTextureProvider* texProvid
|
|||
SkRect proxyRect = rrect.rect();
|
||||
proxyRect.outset(extra, extra);
|
||||
|
||||
SkAutoTUnref<GrFragmentProcessor> fp(GrRRectBlurEffect::Create(texProvider,
|
||||
xformedSigma, rrect));
|
||||
SkAutoTUnref<const GrFragmentProcessor> fp(GrRRectBlurEffect::Create(texProvider,
|
||||
xformedSigma, rrect));
|
||||
if (!fp) {
|
||||
return false;
|
||||
}
|
||||
|
@ -1146,7 +1140,7 @@ bool SkBlurMaskFilterImpl::directFilterRRectMaskGPU(GrTextureProvider* texProvid
|
|||
return true;
|
||||
}
|
||||
|
||||
bool SkBlurMaskFilterImpl::canFilterMaskGPU(const SkRect& srcBounds,
|
||||
bool SkBlurMaskFilterImpl::canFilterMaskGPU(const SkRRect& devRRect,
|
||||
const SkIRect& clipBounds,
|
||||
const SkMatrix& ctm,
|
||||
SkRect* maskRect) const {
|
||||
|
@ -1155,14 +1149,17 @@ bool SkBlurMaskFilterImpl::canFilterMaskGPU(const SkRect& srcBounds,
|
|||
return false;
|
||||
}
|
||||
|
||||
static const SkScalar kMIN_GPU_BLUR_SIZE = SkIntToScalar(64);
|
||||
static const SkScalar kMIN_GPU_BLUR_SIGMA = SkIntToScalar(32);
|
||||
// We always do circles on the GPU
|
||||
if (!devRRect.isCircle()) {
|
||||
static const SkScalar kMIN_GPU_BLUR_SIZE = SkIntToScalar(64);
|
||||
static const SkScalar kMIN_GPU_BLUR_SIGMA = SkIntToScalar(32);
|
||||
|
||||
if (srcBounds.width() <= kMIN_GPU_BLUR_SIZE &&
|
||||
srcBounds.height() <= kMIN_GPU_BLUR_SIZE &&
|
||||
xformedSigma <= kMIN_GPU_BLUR_SIGMA) {
|
||||
// We prefer to blur small rect with small radius via CPU.
|
||||
return false;
|
||||
if (devRRect.width() <= kMIN_GPU_BLUR_SIZE &&
|
||||
devRRect.height() <= kMIN_GPU_BLUR_SIZE &&
|
||||
xformedSigma <= kMIN_GPU_BLUR_SIGMA) {
|
||||
// We prefer to blur small rects with small radii on the CPU.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (nullptr == maskRect) {
|
||||
|
@ -1173,7 +1170,7 @@ bool SkBlurMaskFilterImpl::canFilterMaskGPU(const SkRect& srcBounds,
|
|||
float sigma3 = 3 * SkScalarToFloat(xformedSigma);
|
||||
|
||||
SkRect clipRect = SkRect::Make(clipBounds);
|
||||
SkRect srcRect(srcBounds);
|
||||
SkRect srcRect(devRRect.rect());
|
||||
|
||||
// Outset srcRect and clipRect by 3 * sigma, to compute affected blur area.
|
||||
srcRect.outset(sigma3, sigma3);
|
||||
|
|
|
@ -235,7 +235,7 @@ void GrBlurUtils::drawPathWithMaskFilter(GrContext* context,
|
|||
pathPtr->transform(viewMatrix, devPathPtr);
|
||||
|
||||
SkRect maskRect;
|
||||
if (paint.getMaskFilter()->canFilterMaskGPU(devPathPtr->getBounds(),
|
||||
if (paint.getMaskFilter()->canFilterMaskGPU(SkRRect::MakeRect(devPathPtr->getBounds()),
|
||||
clipBounds,
|
||||
viewMatrix,
|
||||
&maskRect)) {
|
||||
|
|
|
@ -50,7 +50,7 @@ GrProcessorTestFactory<GrGeometryProcessor>::GetFactories() {
|
|||
* we verify the count is as expected. If a new factory is added, then these numbers must be
|
||||
* manually adjusted.
|
||||
*/
|
||||
static const int kFPFactoryCount = 39;
|
||||
static const int kFPFactoryCount = 40;
|
||||
static const int kGPFactoryCount = 14;
|
||||
static const int kXPFactoryCount = 5;
|
||||
|
||||
|
|
|
@ -544,7 +544,7 @@ void SkGpuDevice::drawRRect(const SkDraw& draw, const SkRRect& rect,
|
|||
if (rect.transform(*draw.fMatrix, &devRRect)) {
|
||||
if (devRRect.allCornersCircular()) {
|
||||
SkRect maskRect;
|
||||
if (paint.getMaskFilter()->canFilterMaskGPU(devRRect.rect(),
|
||||
if (paint.getMaskFilter()->canFilterMaskGPU(devRRect,
|
||||
draw.fClip->getBounds(),
|
||||
*draw.fMatrix,
|
||||
&maskRect)) {
|
||||
|
@ -637,7 +637,9 @@ void SkGpuDevice::drawOval(const SkDraw& draw, const SkRect& oval,
|
|||
bool usePath = false;
|
||||
// some basic reasons we might need to call drawPath...
|
||||
if (paint.getMaskFilter()) {
|
||||
usePath = true;
|
||||
// The RRect path can handle special case blurring
|
||||
SkRRect rr = SkRRect::MakeOval(oval);
|
||||
return this->drawRRect(draw, rr, paint);
|
||||
} else {
|
||||
const SkPathEffect* pe = paint.getPathEffect();
|
||||
if (pe && !strokeInfo.isDashed()) {
|
||||
|
|
Загрузка…
Ссылка в новой задаче