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