1/*
2 * Copyright 2018 Google Inc.
3 *
4 * Use of this source code is governed by a BSD-style license that can be
5 * found in the LICENSE file.
6 */
7
8// Equivalent to SkTwoPointConicalGradient::Type
9enum class Type {
10    kRadial, kStrip, kFocal
11};
12
13layout(key) in Type type;
14layout(key) in bool isRadiusIncreasing;
15
16// Focal-specific optimizations
17layout(key) in bool isFocalOnCircle;
18layout(key) in bool isWellBehaved;
19layout(key) in bool isSwapped;
20layout(key) in bool isNativelyFocal;
21
22// focalParams is interpreted differently depending on if type is focal or degenerate when
23// degenerate, focalParams = (r0, r0^2), so strips will use .y and kRadial will use .x when focal,
24// focalParams = (1/r1, focalX = r0/(r0-r1)) The correct parameters are calculated once in Make for
25// each FP
26layout(tracked) in uniform half2 focalParams;
27
28half4 main(float2 p) {
29    float t = -1;
30    half v = 1; // validation flag, set to negative to discard fragment later
31
32    @switch (type) {
33        case Type::kStrip: {
34            half r0_2 = focalParams.y;
35            t = r0_2 - p.y * p.y;
36            if (t >= 0) {
37                t = p.x + sqrt(t);
38            } else {
39                v = -1;
40            }
41        }
42        break;
43        case Type::kRadial: {
44            half r0 = focalParams.x;
45            @if (isRadiusIncreasing) {
46                t = length(p) - r0;
47            } else {
48                t = -length(p) - r0;
49            }
50        }
51        break;
52        case Type::kFocal: {
53            half invR1 = focalParams.x;
54            half fx = focalParams.y;
55
56            float x_t = -1;
57            @if (isFocalOnCircle) {
58                x_t = dot(p, p) / p.x;
59            } else if (isWellBehaved) {
60                x_t = length(p) - p.x * invR1;
61            } else {
62                float temp = p.x * p.x - p.y * p.y;
63
64                // Only do sqrt if temp >= 0; this is significantly slower than checking temp >= 0
65                // in the if statement that checks r(t) >= 0. But GPU may break if we sqrt a
66                // negative float. (Although I havevn't observed that on any devices so far, and the
67                // old approach also does sqrt negative value without a check.) If the performance
68                // is really critical, maybe we should just compute the area where temp and x_t are
69                // always valid and drop all these ifs.
70                if (temp >= 0) {
71                    @if (isSwapped || !isRadiusIncreasing) {
72                        x_t = -sqrt(temp) - p.x * invR1;
73                    } else {
74                        x_t = sqrt(temp) - p.x * invR1;
75                    }
76                }
77            }
78
79            // The final calculation of t from x_t has lots of static optimizations but only do them
80            // when x_t is positive (which can be assumed true if isWellBehaved is true)
81            @if (!isWellBehaved) {
82                // This will still calculate t even though it will be ignored later in the pipeline
83                // to avoid a branch
84                if (x_t <= 0.0) {
85                    v = -1;
86                }
87            }
88            @if (isRadiusIncreasing) {
89                @if (isNativelyFocal) {
90                    t = x_t;
91                } else {
92                    t = x_t + fx;
93                }
94            } else {
95                @if (isNativelyFocal) {
96                    t = -x_t;
97                } else {
98                    t = -x_t + fx;
99                }
100            }
101
102            @if (isSwapped) {
103                t = 1 - t;
104            }
105        }
106        break;
107    }
108
109    return half4(half(t), v, 0, 0);
110}
111
112//////////////////////////////////////////////////////////////////////////////
113
114@header {
115    #include "src/gpu/effects/GrMatrixEffect.h"
116    #include "src/gpu/gradients/GrGradientShader.h"
117    #include "src/shaders/gradients/SkTwoPointConicalGradient.h"
118}
119
120// The 2 point conical gradient can reject a pixel so it does change opacity
121// even if the input was opaque, so disable that optimization
122@optimizationFlags {
123    kNone_OptimizationFlags
124}
125
126@make {
127    static std::unique_ptr<GrFragmentProcessor> Make(const SkTwoPointConicalGradient& gradient,
128                                                     const GrFPArgs& args);
129}
130
131@cppEnd {
132    // .fp files do not let you reference outside enum definitions, so we have to explicitly map
133    // between the two compatible enum defs
134    GrTwoPointConicalGradientLayout::Type convert_type(
135            SkTwoPointConicalGradient::Type type) {
136        switch(type) {
137            case SkTwoPointConicalGradient::Type::kRadial:
138                return GrTwoPointConicalGradientLayout::Type::kRadial;
139            case SkTwoPointConicalGradient::Type::kStrip:
140                return GrTwoPointConicalGradientLayout::Type::kStrip;
141            case SkTwoPointConicalGradient::Type::kFocal:
142                return GrTwoPointConicalGradientLayout::Type::kFocal;
143        }
144        SkDEBUGFAIL("Should not be reachable");
145        return GrTwoPointConicalGradientLayout::Type::kRadial;
146    }
147
148    std::unique_ptr<GrFragmentProcessor> GrTwoPointConicalGradientLayout::Make(
149            const SkTwoPointConicalGradient& grad, const GrFPArgs& args) {
150        GrTwoPointConicalGradientLayout::Type grType = convert_type(grad.getType());
151
152        // The focalData struct is only valid if isFocal is true
153        const SkTwoPointConicalGradient::FocalData& focalData = grad.getFocalData();
154        bool isFocal = grType == Type::kFocal;
155
156        // Calculate optimization switches from gradient specification
157        bool isFocalOnCircle = isFocal && focalData.isFocalOnCircle();
158        bool isWellBehaved = isFocal && focalData.isWellBehaved();
159        bool isSwapped = isFocal && focalData.isSwapped();
160        bool isNativelyFocal = isFocal && focalData.isNativelyFocal();
161
162        // Type-specific calculations: isRadiusIncreasing, focalParams, and the gradient matrix.
163        // However, all types start with the total inverse local matrix calculated from the shader
164        // and args
165        bool isRadiusIncreasing;
166        SkPoint focalParams; // really just a 2D tuple
167        SkMatrix matrix;
168
169        // Initialize the base matrix
170        if (!grad.totalLocalMatrix(args.fPreLocalMatrix)->invert(&matrix)) {
171            return nullptr;
172        }
173
174        if (isFocal) {
175            isRadiusIncreasing = (1 - focalData.fFocalX) > 0;
176
177            focalParams.set(1.0 / focalData.fR1, focalData.fFocalX);
178
179            matrix.postConcat(grad.getGradientMatrix());
180        } else if (grType == Type::kRadial) {
181            SkScalar dr = grad.getDiffRadius();
182            isRadiusIncreasing = dr >= 0;
183
184            SkScalar r0 = grad.getStartRadius() / dr;
185            focalParams.set(r0, r0 * r0);
186
187
188            // GPU radial matrix is different from the original matrix, since we map the diff radius
189            // to have |dr| = 1, so manually compute the final gradient matrix here.
190
191            // Map center to (0, 0)
192            matrix.postTranslate(-grad.getStartCenter().fX, -grad.getStartCenter().fY);
193
194            // scale |diffRadius| to 1
195            matrix.postScale(1 / dr, 1 / dr);
196        } else { // kStrip
197            isRadiusIncreasing = false; // kStrip doesn't use this flag
198
199            SkScalar r0 = grad.getStartRadius() / grad.getCenterX1();
200            focalParams.set(r0, r0 * r0);
201
202
203            matrix.postConcat(grad.getGradientMatrix());
204        }
205
206
207        return GrMatrixEffect::Make(
208                matrix, std::unique_ptr<GrFragmentProcessor>(new GrTwoPointConicalGradientLayout(
209                        grType, isRadiusIncreasing, isFocalOnCircle, isWellBehaved,
210                        isSwapped, isNativelyFocal, focalParams)));
211    }
212}
213
214//////////////////////////////////////////////////////////////////////////////
215
216@test(d) {
217    SkScalar scale = GrGradientShader::RandomParams::kGradientScale;
218    SkScalar offset = scale / 32.0f;
219
220    SkPoint center1, center2;
221    center1.fX = d->fRandom->nextRangeScalar(0.0f, scale);
222    center1.fY = d->fRandom->nextRangeScalar(0.0f, scale);
223    center2.fX = d->fRandom->nextRangeScalar(0.0f, scale);
224    center2.fY = d->fRandom->nextRangeScalar(0.0f, scale);
225    SkScalar radius1 = d->fRandom->nextRangeScalar(0.0f, scale);
226    SkScalar radius2 = d->fRandom->nextRangeScalar(0.0f, scale);
227
228    constexpr int   kTestTypeMask           = (1 << 2) - 1,
229                    kTestNativelyFocalBit   = (1 << 2),
230                    kTestFocalOnCircleBit   = (1 << 3),
231                    kTestSwappedBit         = (1 << 4);
232                    // We won't treat isWellDefined and isRadiusIncreasing specially because they
233                    // should have high probability to be turned on and off as we're getting random
234                    // radii and centers.
235
236    int mask = d->fRandom->nextU();
237    int type = mask & kTestTypeMask;
238    if (type == static_cast<int>(Type::kRadial)) {
239        center2 = center1;
240        // Make sure that the radii are different
241        if (SkScalarNearlyZero(radius1 - radius2)) {
242            radius2 += offset;
243        }
244    } else if (type == static_cast<int>(Type::kStrip)) {
245        radius1 = std::max(radius1, .1f); // Make sure that the radius is non-zero
246        radius2 = radius1;
247        // Make sure that the centers are different
248        if (SkScalarNearlyZero(SkPoint::Distance(center1, center2))) {
249            center2.fX += offset;
250        }
251    } else { // kFocal_Type
252        // Make sure that the centers are different
253        if (SkScalarNearlyZero(SkPoint::Distance(center1, center2))) {
254            center2.fX += offset;
255        }
256
257        if (kTestNativelyFocalBit & mask) {
258            radius1 = 0;
259        }
260        if (kTestFocalOnCircleBit & mask) {
261            radius2 = radius1 + SkPoint::Distance(center1, center2);
262        }
263        if (kTestSwappedBit & mask) {
264            std::swap(radius1, radius2);
265            radius2 = 0;
266        }
267
268        // Make sure that the radii are different
269        if (SkScalarNearlyZero(radius1 - radius2)) {
270            radius2 += offset;
271        }
272    }
273
274    if (SkScalarNearlyZero(radius1 - radius2) &&
275            SkScalarNearlyZero(SkPoint::Distance(center1, center2))) {
276        radius2 += offset; // make sure that we're not degenerated
277    }
278
279    GrGradientShader::RandomParams params(d->fRandom);
280    auto shader = params.fUseColors4f ?
281        SkGradientShader::MakeTwoPointConical(center1, radius1, center2, radius2,
282                                              params.fColors4f, params.fColorSpace, params.fStops,
283                                              params.fColorCount, params.fTileMode) :
284        SkGradientShader::MakeTwoPointConical(center1, radius1, center2, radius2,
285                                              params.fColors, params.fStops,
286                                              params.fColorCount, params.fTileMode);
287    GrTest::TestAsFPArgs asFPArgs(d);
288    std::unique_ptr<GrFragmentProcessor> fp = as_SB(shader)->asFragmentProcessor(asFPArgs.args());
289
290    SkASSERT_RELEASE(fp);
291    return fp;
292}
293