1
2/*
3 * Copyright 2012 Google Inc.
4 *
5 * Use of this source code is governed by a BSD-style license that can be
6 * found in the LICENSE file.
7 */
8
9#include "SkRadialGradient.h"
10#include "SkRadialGradient_Table.h"
11#include "SkNx.h"
12
13#define kSQRT_TABLE_BITS    11
14#define kSQRT_TABLE_SIZE    (1 << kSQRT_TABLE_BITS)
15
16SK_COMPILE_ASSERT(sizeof(gSqrt8Table) == kSQRT_TABLE_SIZE, SqrtTableSizesMatch);
17
18#if 0
19
20#include <stdio.h>
21
22void SkRadialGradient_BuildTable() {
23    // build it 0..127 x 0..127, so we use 2^15 - 1 in the numerator for our "fixed" table
24
25    FILE* file = ::fopen("SkRadialGradient_Table.h", "w");
26    SkASSERT(file);
27    ::fprintf(file, "static const uint8_t gSqrt8Table[] = {\n");
28
29    for (int i = 0; i < kSQRT_TABLE_SIZE; i++) {
30        if ((i & 15) == 0) {
31            ::fprintf(file, "\t");
32        }
33
34        uint8_t value = SkToU8(SkFixedSqrt(i * SK_Fixed1 / kSQRT_TABLE_SIZE) >> 8);
35
36        ::fprintf(file, "0x%02X", value);
37        if (i < kSQRT_TABLE_SIZE-1) {
38            ::fprintf(file, ", ");
39        }
40        if ((i & 15) == 15) {
41            ::fprintf(file, "\n");
42        }
43    }
44    ::fprintf(file, "};\n");
45    ::fclose(file);
46}
47
48#endif
49
50namespace {
51
52// GCC doesn't like using static functions as template arguments.  So force these to be non-static.
53inline SkFixed mirror_tileproc_nonstatic(SkFixed x) {
54    return mirror_tileproc(x);
55}
56
57inline SkFixed repeat_tileproc_nonstatic(SkFixed x) {
58    return repeat_tileproc(x);
59}
60
61SkMatrix rad_to_unit_matrix(const SkPoint& center, SkScalar radius) {
62    SkScalar    inv = SkScalarInvert(radius);
63
64    SkMatrix matrix;
65    matrix.setTranslate(-center.fX, -center.fY);
66    matrix.postScale(inv, inv);
67    return matrix;
68}
69
70typedef void (* RadialShade16Proc)(SkScalar sfx, SkScalar sdx,
71        SkScalar sfy, SkScalar sdy,
72        uint16_t* dstC, const uint16_t* cache,
73        int toggle, int count);
74
75void shadeSpan16_radial_clamp(SkScalar sfx, SkScalar sdx,
76        SkScalar sfy, SkScalar sdy,
77        uint16_t* SK_RESTRICT dstC, const uint16_t* SK_RESTRICT cache,
78        int toggle, int count) {
79    const uint8_t* SK_RESTRICT sqrt_table = gSqrt8Table;
80
81    /* knock these down so we can pin against +- 0x7FFF, which is an
82       immediate load, rather than 0xFFFF which is slower. This is a
83       compromise, since it reduces our precision, but that appears
84       to be visually OK. If we decide this is OK for all of our cases,
85       we could (it seems) put this scale-down into fDstToIndex,
86       to avoid having to do these extra shifts each time.
87    */
88    SkFixed fx = SkScalarToFixed(sfx) >> 1;
89    SkFixed dx = SkScalarToFixed(sdx) >> 1;
90    SkFixed fy = SkScalarToFixed(sfy) >> 1;
91    SkFixed dy = SkScalarToFixed(sdy) >> 1;
92    // might perform this check for the other modes,
93    // but the win will be a smaller % of the total
94    if (dy == 0) {
95        fy = SkPin32(fy, -0xFFFF >> 1, 0xFFFF >> 1);
96        fy *= fy;
97        do {
98            unsigned xx = SkPin32(fx, -0xFFFF >> 1, 0xFFFF >> 1);
99            unsigned fi = (xx * xx + fy) >> (14 + 16 - kSQRT_TABLE_BITS);
100            fi = SkFastMin32(fi, 0xFFFF >> (16 - kSQRT_TABLE_BITS));
101            fx += dx;
102            *dstC++ = cache[toggle +
103                            (sqrt_table[fi] >> SkGradientShaderBase::kSqrt16Shift)];
104            toggle = next_dither_toggle16(toggle);
105        } while (--count != 0);
106    } else {
107        do {
108            unsigned xx = SkPin32(fx, -0xFFFF >> 1, 0xFFFF >> 1);
109            unsigned fi = SkPin32(fy, -0xFFFF >> 1, 0xFFFF >> 1);
110            fi = (xx * xx + fi * fi) >> (14 + 16 - kSQRT_TABLE_BITS);
111            fi = SkFastMin32(fi, 0xFFFF >> (16 - kSQRT_TABLE_BITS));
112            fx += dx;
113            fy += dy;
114            *dstC++ = cache[toggle +
115                            (sqrt_table[fi] >> SkGradientShaderBase::kSqrt16Shift)];
116            toggle = next_dither_toggle16(toggle);
117        } while (--count != 0);
118    }
119}
120
121template <SkFixed (*TileProc)(SkFixed)>
122void shadeSpan16_radial(SkScalar fx, SkScalar dx, SkScalar fy, SkScalar dy,
123                        uint16_t* SK_RESTRICT dstC, const uint16_t* SK_RESTRICT cache,
124                        int toggle, int count) {
125    do {
126        const SkFixed dist = SkFloatToFixed(sk_float_sqrt(fx*fx + fy*fy));
127        const unsigned fi = TileProc(dist);
128        SkASSERT(fi <= 0xFFFF);
129        *dstC++ = cache[toggle + (fi >> SkGradientShaderBase::kCache16Shift)];
130        toggle = next_dither_toggle16(toggle);
131        fx += dx;
132        fy += dy;
133    } while (--count != 0);
134}
135
136void shadeSpan16_radial_mirror(SkScalar fx, SkScalar dx, SkScalar fy, SkScalar dy,
137                               uint16_t* SK_RESTRICT dstC, const uint16_t* SK_RESTRICT cache,
138                               int toggle, int count) {
139    shadeSpan16_radial<mirror_tileproc_nonstatic>(fx, dx, fy, dy, dstC, cache, toggle, count);
140}
141
142void shadeSpan16_radial_repeat(SkScalar fx, SkScalar dx, SkScalar fy, SkScalar dy,
143                               uint16_t* SK_RESTRICT dstC, const uint16_t* SK_RESTRICT cache,
144                               int toggle, int count) {
145    shadeSpan16_radial<repeat_tileproc_nonstatic>(fx, dx, fy, dy, dstC, cache, toggle, count);
146}
147
148}  // namespace
149
150/////////////////////////////////////////////////////////////////////
151
152SkRadialGradient::SkRadialGradient(const SkPoint& center, SkScalar radius, const Descriptor& desc)
153    : SkGradientShaderBase(desc, rad_to_unit_matrix(center, radius))
154    , fCenter(center)
155    , fRadius(radius) {
156}
157
158size_t SkRadialGradient::contextSize() const {
159    return sizeof(RadialGradientContext);
160}
161
162SkShader::Context* SkRadialGradient::onCreateContext(const ContextRec& rec, void* storage) const {
163    return SkNEW_PLACEMENT_ARGS(storage, RadialGradientContext, (*this, rec));
164}
165
166SkRadialGradient::RadialGradientContext::RadialGradientContext(
167        const SkRadialGradient& shader, const ContextRec& rec)
168    : INHERITED(shader, rec) {}
169
170void SkRadialGradient::RadialGradientContext::shadeSpan16(int x, int y, uint16_t* dstCParam,
171                                                          int count) {
172    SkASSERT(count > 0);
173
174    const SkRadialGradient& radialGradient = static_cast<const SkRadialGradient&>(fShader);
175
176    uint16_t* SK_RESTRICT dstC = dstCParam;
177
178    SkPoint             srcPt;
179    SkMatrix::MapXYProc dstProc = fDstToIndexProc;
180    TileProc            proc = radialGradient.fTileProc;
181    const uint16_t* SK_RESTRICT cache = fCache->getCache16();
182    int                 toggle = init_dither_toggle16(x, y);
183
184    if (fDstToIndexClass != kPerspective_MatrixClass) {
185        dstProc(fDstToIndex, SkIntToScalar(x) + SK_ScalarHalf,
186                             SkIntToScalar(y) + SK_ScalarHalf, &srcPt);
187
188        SkScalar sdx = fDstToIndex.getScaleX();
189        SkScalar sdy = fDstToIndex.getSkewY();
190
191        if (fDstToIndexClass == kFixedStepInX_MatrixClass) {
192            SkFixed storage[2];
193            (void)fDstToIndex.fixedStepInX(SkIntToScalar(y),
194                                           &storage[0], &storage[1]);
195            sdx = SkFixedToScalar(storage[0]);
196            sdy = SkFixedToScalar(storage[1]);
197        } else {
198            SkASSERT(fDstToIndexClass == kLinear_MatrixClass);
199        }
200
201        RadialShade16Proc shadeProc = shadeSpan16_radial_repeat;
202        if (SkShader::kClamp_TileMode == radialGradient.fTileMode) {
203            shadeProc = shadeSpan16_radial_clamp;
204        } else if (SkShader::kMirror_TileMode == radialGradient.fTileMode) {
205            shadeProc = shadeSpan16_radial_mirror;
206        } else {
207            SkASSERT(SkShader::kRepeat_TileMode == radialGradient.fTileMode);
208        }
209        (*shadeProc)(srcPt.fX, sdx, srcPt.fY, sdy, dstC,
210                     cache, toggle, count);
211    } else {    // perspective case
212        SkScalar dstX = SkIntToScalar(x);
213        SkScalar dstY = SkIntToScalar(y);
214        do {
215            dstProc(fDstToIndex, dstX, dstY, &srcPt);
216            unsigned fi = proc(SkScalarToFixed(srcPt.length()));
217            SkASSERT(fi <= 0xFFFF);
218
219            int index = fi >> (16 - kCache16Bits);
220            *dstC++ = cache[toggle + index];
221            toggle = next_dither_toggle16(toggle);
222
223            dstX += SK_Scalar1;
224        } while (--count != 0);
225    }
226}
227
228SkShader::BitmapType SkRadialGradient::asABitmap(SkBitmap* bitmap,
229    SkMatrix* matrix, SkShader::TileMode* xy) const {
230    if (bitmap) {
231        this->getGradientTableBitmap(bitmap);
232    }
233    if (matrix) {
234        matrix->setScale(SkIntToScalar(kCache32Count),
235                         SkIntToScalar(kCache32Count));
236        matrix->preConcat(fPtsToUnit);
237    }
238    if (xy) {
239        xy[0] = fTileMode;
240        xy[1] = kClamp_TileMode;
241    }
242    return kRadial_BitmapType;
243}
244
245SkShader::GradientType SkRadialGradient::asAGradient(GradientInfo* info) const {
246    if (info) {
247        commonAsAGradient(info);
248        info->fPoint[0] = fCenter;
249        info->fRadius[0] = fRadius;
250    }
251    return kRadial_GradientType;
252}
253
254SkFlattenable* SkRadialGradient::CreateProc(SkReadBuffer& buffer) {
255    DescriptorScope desc;
256    if (!desc.unflatten(buffer)) {
257        return NULL;
258    }
259    const SkPoint center = buffer.readPoint();
260    const SkScalar radius = buffer.readScalar();
261    return SkGradientShader::CreateRadial(center, radius, desc.fColors, desc.fPos, desc.fCount,
262                                          desc.fTileMode, desc.fGradFlags, desc.fLocalMatrix);
263}
264
265void SkRadialGradient::flatten(SkWriteBuffer& buffer) const {
266    this->INHERITED::flatten(buffer);
267    buffer.writePoint(fCenter);
268    buffer.writeScalar(fRadius);
269}
270
271namespace {
272
273inline bool radial_completely_pinned(SkScalar fx, SkScalar dx, SkScalar fy, SkScalar dy) {
274    // fast, overly-conservative test: checks unit square instead of unit circle
275    bool xClamped = (fx >= 1 && dx >= 0) || (fx <= -1 && dx <= 0);
276    bool yClamped = (fy >= 1 && dy >= 0) || (fy <= -1 && dy <= 0);
277    return xClamped || yClamped;
278}
279
280typedef void (* RadialShadeProc)(SkScalar sfx, SkScalar sdx,
281        SkScalar sfy, SkScalar sdy,
282        SkPMColor* dstC, const SkPMColor* cache,
283        int count, int toggle);
284
285static inline Sk4f fast_sqrt(const Sk4f& R) {
286    // R * R.rsqrt0() is much faster, but it's non-monotonic, which isn't so pretty for gradients.
287    return R * R.rsqrt1();
288}
289
290static inline Sk4f sum_squares(const Sk4f& a, const Sk4f& b) {
291    return a * a + b * b;
292}
293
294void shadeSpan_radial_clamp2(SkScalar sfx, SkScalar sdx, SkScalar sfy, SkScalar sdy,
295                             SkPMColor* SK_RESTRICT dstC, const SkPMColor* SK_RESTRICT cache,
296                             int count, int toggle) {
297    if (radial_completely_pinned(sfx, sdx, sfy, sdy)) {
298        unsigned fi = SkGradientShaderBase::kCache32Count - 1;
299        sk_memset32_dither(dstC,
300                           cache[toggle + fi],
301                           cache[next_dither_toggle(toggle) + fi],
302                           count);
303    } else {
304        const Sk4f max(255);
305        const float scale = 255;
306        sfx *= scale;
307        sfy *= scale;
308        sdx *= scale;
309        sdy *= scale;
310        const Sk4f fx4(sfx, sfx + sdx, sfx + 2*sdx, sfx + 3*sdx);
311        const Sk4f fy4(sfy, sfy + sdy, sfy + 2*sdy, sfy + 3*sdy);
312        const Sk4f dx4(sdx * 4);
313        const Sk4f dy4(sdy * 4);
314
315        Sk4f tmpxy = fx4 * dx4 + fy4 * dy4;
316        Sk4f tmpdxdy = sum_squares(dx4, dy4);
317        Sk4f R = sum_squares(fx4, fy4);
318        Sk4f dR = tmpxy + tmpxy + tmpdxdy;
319        const Sk4f ddR = tmpdxdy + tmpdxdy;
320
321        for (int i = 0; i < (count >> 2); ++i) {
322            Sk4f dist = Sk4f::Min(fast_sqrt(R), max);
323            R += dR;
324            dR += ddR;
325
326            int fi[4];
327            dist.castTrunc().store(fi);
328
329            for (int i = 0; i < 4; i++) {
330                *dstC++ = cache[toggle + fi[i]];
331                toggle = next_dither_toggle(toggle);
332            }
333        }
334        count &= 3;
335        if (count) {
336            Sk4f dist = Sk4f::Min(fast_sqrt(R), max);
337
338            int fi[4];
339            dist.castTrunc().store(fi);
340            for (int i = 0; i < count; i++) {
341                *dstC++ = cache[toggle + fi[i]];
342                toggle = next_dither_toggle(toggle);
343            }
344        }
345    }
346}
347
348// Unrolling this loop doesn't seem to help (when float); we're stalling to
349// get the results of the sqrt (?), and don't have enough extra registers to
350// have many in flight.
351template <SkFixed (*TileProc)(SkFixed)>
352void shadeSpan_radial(SkScalar fx, SkScalar dx, SkScalar fy, SkScalar dy,
353                      SkPMColor* SK_RESTRICT dstC, const SkPMColor* SK_RESTRICT cache,
354                      int count, int toggle) {
355    do {
356        const SkFixed dist = SkFloatToFixed(sk_float_sqrt(fx*fx + fy*fy));
357        const unsigned fi = TileProc(dist);
358        SkASSERT(fi <= 0xFFFF);
359        *dstC++ = cache[toggle + (fi >> SkGradientShaderBase::kCache32Shift)];
360        toggle = next_dither_toggle(toggle);
361        fx += dx;
362        fy += dy;
363    } while (--count != 0);
364}
365
366void shadeSpan_radial_mirror(SkScalar fx, SkScalar dx, SkScalar fy, SkScalar dy,
367                             SkPMColor* SK_RESTRICT dstC, const SkPMColor* SK_RESTRICT cache,
368                             int count, int toggle) {
369    shadeSpan_radial<mirror_tileproc_nonstatic>(fx, dx, fy, dy, dstC, cache, count, toggle);
370}
371
372void shadeSpan_radial_repeat(SkScalar fx, SkScalar dx, SkScalar fy, SkScalar dy,
373                             SkPMColor* SK_RESTRICT dstC, const SkPMColor* SK_RESTRICT cache,
374                             int count, int toggle) {
375    shadeSpan_radial<repeat_tileproc_nonstatic>(fx, dx, fy, dy, dstC, cache, count, toggle);
376}
377
378}  // namespace
379
380void SkRadialGradient::RadialGradientContext::shadeSpan(int x, int y,
381                                                        SkPMColor* SK_RESTRICT dstC, int count) {
382    SkASSERT(count > 0);
383
384    const SkRadialGradient& radialGradient = static_cast<const SkRadialGradient&>(fShader);
385
386    SkPoint             srcPt;
387    SkMatrix::MapXYProc dstProc = fDstToIndexProc;
388    TileProc            proc = radialGradient.fTileProc;
389    const SkPMColor* SK_RESTRICT cache = fCache->getCache32();
390    int toggle = init_dither_toggle(x, y);
391
392    if (fDstToIndexClass != kPerspective_MatrixClass) {
393        dstProc(fDstToIndex, SkIntToScalar(x) + SK_ScalarHalf,
394                             SkIntToScalar(y) + SK_ScalarHalf, &srcPt);
395        SkScalar sdx = fDstToIndex.getScaleX();
396        SkScalar sdy = fDstToIndex.getSkewY();
397
398        if (fDstToIndexClass == kFixedStepInX_MatrixClass) {
399            SkFixed storage[2];
400            (void)fDstToIndex.fixedStepInX(SkIntToScalar(y),
401                                           &storage[0], &storage[1]);
402            sdx = SkFixedToScalar(storage[0]);
403            sdy = SkFixedToScalar(storage[1]);
404        } else {
405            SkASSERT(fDstToIndexClass == kLinear_MatrixClass);
406        }
407
408        RadialShadeProc shadeProc = shadeSpan_radial_repeat;
409        if (SkShader::kClamp_TileMode == radialGradient.fTileMode) {
410            shadeProc = shadeSpan_radial_clamp2;
411        } else if (SkShader::kMirror_TileMode == radialGradient.fTileMode) {
412            shadeProc = shadeSpan_radial_mirror;
413        } else {
414            SkASSERT(SkShader::kRepeat_TileMode == radialGradient.fTileMode);
415        }
416        (*shadeProc)(srcPt.fX, sdx, srcPt.fY, sdy, dstC, cache, count, toggle);
417    } else {    // perspective case
418        SkScalar dstX = SkIntToScalar(x);
419        SkScalar dstY = SkIntToScalar(y);
420        do {
421            dstProc(fDstToIndex, dstX, dstY, &srcPt);
422            unsigned fi = proc(SkScalarToFixed(srcPt.length()));
423            SkASSERT(fi <= 0xFFFF);
424            *dstC++ = cache[fi >> SkGradientShaderBase::kCache32Shift];
425            dstX += SK_Scalar1;
426        } while (--count != 0);
427    }
428}
429
430/////////////////////////////////////////////////////////////////////
431
432#if SK_SUPPORT_GPU
433
434#include "SkGr.h"
435#include "gl/builders/GrGLProgramBuilder.h"
436
437class GrGLRadialGradient : public GrGLGradientEffect {
438public:
439
440    GrGLRadialGradient(const GrProcessor&) {}
441    virtual ~GrGLRadialGradient() { }
442
443    virtual void emitCode(GrGLFPBuilder*,
444                          const GrFragmentProcessor&,
445                          const char* outputColor,
446                          const char* inputColor,
447                          const TransformedCoordsArray&,
448                          const TextureSamplerArray&) override;
449
450    static void GenKey(const GrProcessor& processor, const GrGLSLCaps&, GrProcessorKeyBuilder* b) {
451        b->add32(GenBaseGradientKey(processor));
452    }
453
454private:
455
456    typedef GrGLGradientEffect INHERITED;
457
458};
459
460/////////////////////////////////////////////////////////////////////
461
462class GrRadialGradient : public GrGradientEffect {
463public:
464    static GrFragmentProcessor* Create(GrContext* ctx,
465                                       const SkRadialGradient& shader,
466                                       const SkMatrix& matrix,
467                                       SkShader::TileMode tm) {
468        return SkNEW_ARGS(GrRadialGradient, (ctx, shader, matrix, tm));
469    }
470
471    virtual ~GrRadialGradient() { }
472
473    const char* name() const override { return "Radial Gradient"; }
474
475    virtual void getGLProcessorKey(const GrGLSLCaps& caps,
476                                   GrProcessorKeyBuilder* b) const override {
477        GrGLRadialGradient::GenKey(*this, caps, b);
478    }
479
480    GrGLFragmentProcessor* createGLInstance() const override {
481        return SkNEW_ARGS(GrGLRadialGradient, (*this));
482    }
483
484private:
485    GrRadialGradient(GrContext* ctx,
486                     const SkRadialGradient& shader,
487                     const SkMatrix& matrix,
488                     SkShader::TileMode tm)
489        : INHERITED(ctx, shader, matrix, tm) {
490        this->initClassID<GrRadialGradient>();
491    }
492
493    GR_DECLARE_FRAGMENT_PROCESSOR_TEST;
494
495    typedef GrGradientEffect INHERITED;
496};
497
498/////////////////////////////////////////////////////////////////////
499
500GR_DEFINE_FRAGMENT_PROCESSOR_TEST(GrRadialGradient);
501
502GrFragmentProcessor* GrRadialGradient::TestCreate(SkRandom* random,
503                                                  GrContext* context,
504                                                  const GrDrawTargetCaps&,
505                                                  GrTexture**) {
506    SkPoint center = {random->nextUScalar1(), random->nextUScalar1()};
507    SkScalar radius = random->nextUScalar1();
508
509    SkColor colors[kMaxRandomGradientColors];
510    SkScalar stopsArray[kMaxRandomGradientColors];
511    SkScalar* stops = stopsArray;
512    SkShader::TileMode tm;
513    int colorCount = RandomGradientParams(random, colors, &stops, &tm);
514    SkAutoTUnref<SkShader> shader(SkGradientShader::CreateRadial(center, radius,
515                                                                 colors, stops, colorCount,
516                                                                 tm));
517    SkPaint paint;
518    GrColor paintColor;
519    GrFragmentProcessor* fp;
520    SkAssertResult(shader->asFragmentProcessor(context, paint,
521                                               GrTest::TestMatrix(random), NULL,
522                                               &paintColor, &fp));
523    return fp;
524}
525
526/////////////////////////////////////////////////////////////////////
527
528void GrGLRadialGradient::emitCode(GrGLFPBuilder* builder,
529                                  const GrFragmentProcessor& fp,
530                                  const char* outputColor,
531                                  const char* inputColor,
532                                  const TransformedCoordsArray& coords,
533                                  const TextureSamplerArray& samplers) {
534    const GrRadialGradient& ge = fp.cast<GrRadialGradient>();
535    this->emitUniforms(builder, ge);
536    SkString t("length(");
537    t.append(builder->getFragmentShaderBuilder()->ensureFSCoords2D(coords, 0));
538    t.append(")");
539    this->emitColor(builder, ge, t.c_str(), outputColor, inputColor, samplers);
540}
541
542/////////////////////////////////////////////////////////////////////
543
544bool SkRadialGradient::asFragmentProcessor(GrContext* context, const SkPaint& paint,
545                                           const SkMatrix& viewM,
546                                           const SkMatrix* localMatrix, GrColor* paintColor,
547                                           GrFragmentProcessor** fp) const {
548    SkASSERT(context);
549
550    SkMatrix matrix;
551    if (!this->getLocalMatrix().invert(&matrix)) {
552        return false;
553    }
554    if (localMatrix) {
555        SkMatrix inv;
556        if (!localMatrix->invert(&inv)) {
557            return false;
558        }
559        matrix.postConcat(inv);
560    }
561    matrix.postConcat(fPtsToUnit);
562
563    *paintColor = SkColor2GrColorJustAlpha(paint.getColor());
564    *fp = GrRadialGradient::Create(context, *this, matrix, fTileMode);
565
566    return true;
567}
568
569#else
570
571bool SkRadialGradient::asFragmentProcessor(GrContext*, const SkPaint&, const SkMatrix&,
572                                           const SkMatrix*, GrColor*,
573                                           GrFragmentProcessor**) const {
574    SkDEBUGFAIL("Should not call in GPU-less build");
575    return false;
576}
577
578#endif
579
580#ifndef SK_IGNORE_TO_STRING
581void SkRadialGradient::toString(SkString* str) const {
582    str->append("SkRadialGradient: (");
583
584    str->append("center: (");
585    str->appendScalar(fCenter.fX);
586    str->append(", ");
587    str->appendScalar(fCenter.fY);
588    str->append(") radius: ");
589    str->appendScalar(fRadius);
590    str->append(" ");
591
592    this->INHERITED::toString(str);
593
594    str->append(")");
595}
596#endif
597