/* * Copyright 2016 Google Inc. * * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ #include "Sk4fLinearGradient.h" #include "SkPaint.h" #include <cmath> #include <utility> namespace { template<ApplyPremul premul> void ramp(const Sk4f& c, const Sk4f& dc, SkPMColor dst[], int n, const Sk4f& bias0, const Sk4f& bias1) { SkASSERT(n > 0); const Sk4f dc2 = dc + dc, dc4 = dc2 + dc2; Sk4f c0 = c + DstTraits<premul>::pre_lerp_bias(bias0), c1 = c + dc + DstTraits<premul>::pre_lerp_bias(bias1), c2 = c0 + dc2, c3 = c1 + dc2; while (n >= 4) { DstTraits<premul>::store4x(c0, c1, c2, c3, dst, bias0, bias1); dst += 4; c0 = c0 + dc4; c1 = c1 + dc4; c2 = c2 + dc4; c3 = c3 + dc4; n -= 4; } if (n & 2) { DstTraits<premul>::store(c0, dst++, bias0); DstTraits<premul>::store(c1, dst++, bias1); c0 = c0 + dc2; } if (n & 1) { DstTraits<premul>::store(c0, dst, bias0); } } template<SkShader::TileMode> SkScalar pinFx(SkScalar); template<> SkScalar pinFx<SkShader::kClamp_TileMode>(SkScalar fx) { return fx; } template<> SkScalar pinFx<SkShader::kRepeat_TileMode>(SkScalar fx) { SkScalar f = SkScalarIsFinite(fx) ? SkScalarFraction(fx) : 0; if (f < 0) { f = SkTMin(f + 1, nextafterf(1, 0)); } SkASSERT(f >= 0); SkASSERT(f < 1.0f); return f; } template<> SkScalar pinFx<SkShader::kMirror_TileMode>(SkScalar fx) { SkScalar f = SkScalarIsFinite(fx) ? SkScalarMod(fx, 2.0f) : 0; if (f < 0) { f = SkTMin(f + 2, nextafterf(2, 0)); } SkASSERT(f >= 0); SkASSERT(f < 2.0f); return f; } // true when x is in [k1,k2], or [k2, k1] when the interval is reversed. // TODO(fmalita): hoist the reversed interval check out of this helper. bool in_range(SkScalar x, SkScalar k1, SkScalar k2) { SkASSERT(k1 != k2); return (k1 < k2) ? (x >= k1 && x <= k2) : (x >= k2 && x <= k1); } } // anonymous namespace SkLinearGradient:: LinearGradient4fContext::LinearGradient4fContext(const SkLinearGradient& shader, const ContextRec& rec) : INHERITED(shader, rec) { // Our fast path expects interval points to be monotonically increasing in x. const bool reverseIntervals = std::signbit(fDstToPos.getScaleX()); fIntervals.init(shader, rec.fDstColorSpace, shader.fTileMode, fColorsArePremul, rec.fPaint->getAlpha() * (1.0f / 255), reverseIntervals); SkASSERT(fIntervals->count() > 0); fCachedInterval = fIntervals->begin(); } const Sk4fGradientInterval* SkLinearGradient::LinearGradient4fContext::findInterval(SkScalar fx) const { SkASSERT(in_range(fx, fIntervals->front().fT0, fIntervals->back().fT1)); if (1) { // Linear search, using the last scanline interval as a starting point. SkASSERT(fCachedInterval >= fIntervals->begin()); SkASSERT(fCachedInterval < fIntervals->end()); const int search_dir = fDstToPos.getScaleX() >= 0 ? 1 : -1; while (!in_range(fx, fCachedInterval->fT0, fCachedInterval->fT1)) { fCachedInterval += search_dir; if (fCachedInterval >= fIntervals->end()) { fCachedInterval = fIntervals->begin(); } else if (fCachedInterval < fIntervals->begin()) { fCachedInterval = fIntervals->end() - 1; } } return fCachedInterval; } else { // Binary search. Seems less effective than linear + caching. const auto* i0 = fIntervals->begin(); const auto* i1 = fIntervals->end() - 1; while (i0 != i1) { SkASSERT(i0 < i1); SkASSERT(in_range(fx, i0->fT0, i1->fT1)); const auto* i = i0 + ((i1 - i0) >> 1); if (in_range(fx, i0->fT0, i->fT1)) { i1 = i; } else { SkASSERT(in_range(fx, i->fT1, i1->fT1)); i0 = i + 1; } } SkASSERT(in_range(fx, i0->fT0, i0->fT1)); return i0; } } void SkLinearGradient:: LinearGradient4fContext::shadeSpan(int x, int y, SkPMColor dst[], int count) { SkASSERT(count > 0); float bias0 = 0, bias1 = 0; if (fDither) { static constexpr float dither_cell[] = { -3/8.0f, 1/8.0f, 3/8.0f, -1/8.0f, }; const int rowIndex = (y & 1) << 1; bias0 = dither_cell[rowIndex + 0]; bias1 = dither_cell[rowIndex + 1]; if (x & 1) { using std::swap; swap(bias0, bias1); } } if (fColorsArePremul) { // In premul interpolation mode, components are pre-scaled by 255 and the store // op is truncating. We pre-bias here to achieve rounding. bias0 += 0.5f; bias1 += 0.5f; this->shadePremulSpan<ApplyPremul::False>(x, y, dst, count, bias0, bias1); } else { // In unpremul interpolation mode, Components are not pre-scaled. bias0 *= 1/255.0f; bias1 *= 1/255.0f; this->shadePremulSpan<ApplyPremul::True >(x, y, dst, count, bias0, bias1); } } template<ApplyPremul premul> void SkLinearGradient:: LinearGradient4fContext::shadePremulSpan(int x, int y, SkPMColor dst[], int count, float bias0, float bias1) const { const SkLinearGradient& shader = static_cast<const SkLinearGradient&>(fShader); switch (shader.fTileMode) { case kDecal_TileMode: SkASSERT(false); // decal only supported via stages // fall-through case kClamp_TileMode: this->shadeSpanInternal<premul, kClamp_TileMode >(x, y, dst, count, bias0, bias1); break; case kRepeat_TileMode: this->shadeSpanInternal<premul, kRepeat_TileMode>(x, y, dst, count, bias0, bias1); break; case kMirror_TileMode: this->shadeSpanInternal<premul, kMirror_TileMode>(x, y, dst, count, bias0, bias1); break; } } template<ApplyPremul premul, SkShader::TileMode tileMode> void SkLinearGradient:: LinearGradient4fContext::shadeSpanInternal(int x, int y, SkPMColor dst[], int count, float bias0, float bias1) const { SkPoint pt; fDstToPosProc(fDstToPos, x + SK_ScalarHalf, y + SK_ScalarHalf, &pt); const SkScalar fx = pinFx<tileMode>(pt.x()); const SkScalar dx = fDstToPos.getScaleX(); LinearIntervalProcessor<premul, tileMode> proc(fIntervals->begin(), fIntervals->end() - 1, this->findInterval(fx), fx, dx, SkScalarNearlyZero(dx * count)); Sk4f bias4f0(bias0), bias4f1(bias1); while (count > 0) { // What we really want here is SkTPin(advance, 1, count) // but that's a significant perf hit for >> stops; investigate. const int n = SkTMin(SkScalarTruncToInt(proc.currentAdvance() + 1), count); // The current interval advance can be +inf (e.g. when reaching // the clamp mode end intervals) - when that happens, we expect to // a) consume all remaining count in one swoop // b) return a zero color gradient SkASSERT(SkScalarIsFinite(proc.currentAdvance()) || (n == count && proc.currentRampIsZero())); if (proc.currentRampIsZero()) { DstTraits<premul>::store(proc.currentColor(), dst, n); } else { ramp<premul>(proc.currentColor(), proc.currentColorGrad(), dst, n, bias4f0, bias4f1); } proc.advance(SkIntToScalar(n)); count -= n; dst += n; if (n & 1) { using std::swap; swap(bias4f0, bias4f1); } } } template<ApplyPremul premul, SkShader::TileMode tileMode> class SkLinearGradient:: LinearGradient4fContext::LinearIntervalProcessor { public: LinearIntervalProcessor(const Sk4fGradientInterval* firstInterval, const Sk4fGradientInterval* lastInterval, const Sk4fGradientInterval* i, SkScalar fx, SkScalar dx, bool is_vertical) : fAdvX(is_vertical ? SK_ScalarInfinity : (i->fT1 - fx) / dx) , fFirstInterval(firstInterval) , fLastInterval(lastInterval) , fInterval(i) , fDx(dx) , fIsVertical(is_vertical) { SkASSERT(fAdvX >= 0); SkASSERT(firstInterval <= lastInterval); if (tileMode != kClamp_TileMode && !is_vertical) { const auto spanX = (lastInterval->fT1 - firstInterval->fT0) / dx; SkASSERT(spanX >= 0); // If we're in a repeating tile mode and the whole gradient is compressed into a // fraction of a pixel, we just use the average color in zero-ramp mode. // This also avoids cases where we make no progress due to interval advances being // close to zero. static constexpr SkScalar kMinSpanX = .25f; if (spanX < kMinSpanX) { this->init_average_props(); return; } } this->compute_interval_props(fx); } SkScalar currentAdvance() const { SkASSERT(fAdvX >= 0); SkASSERT(!std::isfinite(fAdvX) || fAdvX <= (fInterval->fT1 - fInterval->fT0) / fDx); return fAdvX; } bool currentRampIsZero() const { return fZeroRamp; } const Sk4f& currentColor() const { return fCc; } const Sk4f& currentColorGrad() const { return fDcDx; } void advance(SkScalar advX) { SkASSERT(advX > 0); SkASSERT(fAdvX >= 0); if (advX >= fAdvX) { advX = this->advance_interval(advX); } SkASSERT(advX < fAdvX); fCc = fCc + fDcDx * Sk4f(advX); fAdvX -= advX; } private: void compute_interval_props(SkScalar t) { SkASSERT(in_range(t, fInterval->fT0, fInterval->fT1)); const Sk4f dc = DstTraits<premul>::load(fInterval->fCg); fCc = DstTraits<premul>::load(fInterval->fCb) + dc * Sk4f(t); fDcDx = dc * fDx; fZeroRamp = fIsVertical || (dc == 0).allTrue(); } void init_average_props() { fAdvX = SK_ScalarInfinity; fZeroRamp = true; fDcDx = 0; fCc = Sk4f(0); // TODO: precompute the average at interval setup time? for (const auto* i = fFirstInterval; i <= fLastInterval; ++i) { // Each interval contributes its average color to the total/weighted average: // // C = (c0 + c1) / 2 = (Cb + Cg * t0 + Cb + Cg * t1) / 2 = Cb + Cg *(t0 + t1) / 2 // // Avg += C * (t1 - t0) // const auto c = DstTraits<premul>::load(i->fCb) + DstTraits<premul>::load(i->fCg) * (i->fT0 + i->fT1) * 0.5f; fCc = fCc + c * (i->fT1 - i->fT0); } } const Sk4fGradientInterval* next_interval(const Sk4fGradientInterval* i) const { SkASSERT(i >= fFirstInterval); SkASSERT(i <= fLastInterval); i++; if (tileMode == kClamp_TileMode) { SkASSERT(i <= fLastInterval); return i; } return (i <= fLastInterval) ? i : fFirstInterval; } SkScalar advance_interval(SkScalar advX) { SkASSERT(advX >= fAdvX); do { advX -= fAdvX; fInterval = this->next_interval(fInterval); fAdvX = (fInterval->fT1 - fInterval->fT0) / fDx; SkASSERT(fAdvX > 0); } while (advX >= fAdvX); compute_interval_props(fInterval->fT0); SkASSERT(advX >= 0); return advX; } // Current interval properties. Sk4f fDcDx; // dst color gradient (dc/dx) Sk4f fCc; // current color, interpolated in dst SkScalar fAdvX; // remaining interval advance in dst bool fZeroRamp; // current interval color grad is 0 const Sk4fGradientInterval* fFirstInterval; const Sk4fGradientInterval* fLastInterval; const Sk4fGradientInterval* fInterval; // current interval const SkScalar fDx; // 'dx' for consistency with other impls; actually dt/dx const bool fIsVertical; };