/* * Copyright 2023 Google LLC * * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ #include "src/gpu/BlurUtils.h" #include "include/core/SkBitmap.h" #include "include/core/SkColorPriv.h" #include "include/core/SkImageInfo.h" #include "include/core/SkRRect.h" #include "include/core/SkRect.h" #include "include/core/SkScalar.h" #include "include/core/SkSize.h" #include "include/private/base/SkAssert.h" #include "include/private/base/SkFloatingPoint.h" #include "include/private/base/SkMath.h" #include "include/private/base/SkPoint_impl.h" #include "include/private/base/SkTemplates.h" #include "include/private/base/SkTo.h" #include "src/base/SkMathPriv.h" #include #include #include #include #include #include namespace skgpu { /////////////////////////////////////////////////////////////////////////////// // Rect Blur /////////////////////////////////////////////////////////////////////////////// // TODO: it seems like there should be some synergy with SkBlurMask::ComputeBlurProfile SkBitmap CreateIntegralTable(int width) { SkBitmap table; if (width <= 0) { return table; } if (!table.tryAllocPixels(SkImageInfo::MakeA8(width, 1))) { return table; } *table.getAddr8(0, 0) = 255; const float invWidth = 1.f / width; for (int i = 1; i < width - 1; ++i) { float x = (i + 0.5f) * invWidth; x = (-6 * x + 3) * SK_ScalarRoot2Over2; float integral = 0.5f * (std::erf(x) + 1.f); *table.getAddr8(i, 0) = SkToU8(sk_float_round2int(255.f * integral)); } *table.getAddr8(width - 1, 0) = 0; table.setImmutable(); return table; } int ComputeIntegralTableWidth(float sixSigma) { // Check for NaN/infinity if (!SkIsFinite(sixSigma)) { return 0; } // Avoid overflow, covers both multiplying by 2 and finding next power of 2: // 2*((2^31-1)/4 + 1) = 2*(2^29-1) + 2 = 2^30 and SkNextPow2(2^30) = 2^30 if (sixSigma > SK_MaxS32 / 4 + 1) { return 0; } // The texture we're producing represents the integral of a normal distribution over a // six-sigma range centered at zero. We want enough resolution so that the linear // interpolation done in texture lookup doesn't introduce noticeable artifacts. We // conservatively choose to have 2 texels for each dst pixel. int minWidth = 2 * ((int)std::ceil(sixSigma)); // Bin by powers of 2 with a minimum so we get good profile reuse. return std::max(SkNextPow2(minWidth), 32); } /////////////////////////////////////////////////////////////////////////////// // Circle Blur /////////////////////////////////////////////////////////////////////////////// // Computes an unnormalized half kernel (right side). Returns the summation of all the half // kernel values. static float make_unnormalized_half_kernel(float* halfKernel, int halfKernelSize, float sigma) { const float invSigma = 1.0f / sigma; const float b = -0.5f * invSigma * invSigma; float tot = 0.0f; // Compute half kernel values at half pixel steps out from the center. float t = 0.5f; for (int i = 0; i < halfKernelSize; ++i) { float value = expf(t * t * b); tot += value; halfKernel[i] = value; t += 1.0f; } return tot; } // Create a Gaussian half-kernel (right side) and a summed area table given a sigma and number // of discrete steps. The half kernel is normalized to sum to 0.5. static void make_half_kernel_and_summed_table(float* halfKernel, float* summedHalfKernel, int halfKernelSize, float sigma) { // The half kernel should sum to 0.5 not 1.0. const float tot = 2.0f * make_unnormalized_half_kernel(halfKernel, halfKernelSize, sigma); float sum = 0.0f; for (int i = 0; i < halfKernelSize; ++i) { halfKernel[i] /= tot; sum += halfKernel[i]; summedHalfKernel[i] = sum; } } // Applies the 1D half kernel vertically at points along the x axis to a circle centered at the // origin with radius circleR. static void apply_kernel_in_y(float* results, int numSteps, float firstX, float circleR, int halfKernelSize, const float* summedHalfKernelTable) { float x = firstX; for (int i = 0; i < numSteps; ++i, x += 1.0f) { if (x < -circleR || x > circleR) { results[i] = 0; continue; } float y = sqrtf(circleR * circleR - x * x); // In the column at x we exit the circle at +y and -y // The summed table entry j is actually reflects an offset of j + 0.5. y -= 0.5f; int yInt = SkScalarFloorToInt(y); SkASSERT(yInt >= -1); if (y < 0) { results[i] = (y + 0.5f) * summedHalfKernelTable[0]; } else if (yInt >= halfKernelSize - 1) { results[i] = 0.5f; } else { float yFrac = y - yInt; results[i] = (1.0f - yFrac) * summedHalfKernelTable[yInt] + yFrac * summedHalfKernelTable[yInt + 1]; } } } // Apply a Gaussian at point (evalX, 0) to a circle centered at the origin with radius circleR. // This relies on having a half kernel computed for the Gaussian and a table of applications of // the half kernel in y to columns at (evalX - halfKernel, evalX - halfKernel + 1, ..., evalX + // halfKernel) passed in as yKernelEvaluations. static uint8_t eval_at(float evalX, float circleR, const float* halfKernel, int halfKernelSize, const float* yKernelEvaluations) { float acc = 0; float x = evalX - halfKernelSize; for (int i = 0; i < halfKernelSize; ++i, x += 1.0f) { if (x < -circleR || x > circleR) { continue; } float verticalEval = yKernelEvaluations[i]; acc += verticalEval * halfKernel[halfKernelSize - i - 1]; } for (int i = 0; i < halfKernelSize; ++i, x += 1.0f) { if (x < -circleR || x > circleR) { continue; } float verticalEval = yKernelEvaluations[i + halfKernelSize]; acc += verticalEval * halfKernel[i]; } // Since we applied a half kernel in y we multiply acc by 2 (the circle is symmetric about // the x axis). return SkUnitScalarClampToByte(2.0f * acc); } // This function creates a profile of a blurred circle. It does this by computing a kernel for // half the Gaussian and a matching summed area table. The summed area table is used to compute // an array of vertical applications of the half kernel to the circle along the x axis. The // table of y evaluations has 2 * k + n entries where k is the size of the half kernel and n is // the size of the profile being computed. Then for each of the n profile entries we walk out k // steps in each horizontal direction multiplying the corresponding y evaluation by the half // kernel entry and sum these values to compute the profile entry. SkBitmap CreateCircleProfile(float sigma, float radius, int profileWidth) { SkBitmap bitmap; if (!bitmap.tryAllocPixels(SkImageInfo::MakeA8(profileWidth, 1))) { return bitmap; } uint8_t* profile = bitmap.getAddr8(0, 0); const int numSteps = profileWidth; // The full kernel is 6 sigmas wide. int halfKernelSize = SkScalarCeilToInt(6.0f * sigma); // Round up to next multiple of 2 and then divide by 2. halfKernelSize = ((halfKernelSize + 1) & ~1) >> 1; // Number of x steps at which to apply kernel in y to cover all the profile samples in x. const int numYSteps = numSteps + 2 * halfKernelSize; skia_private::AutoTArray bulkAlloc(halfKernelSize + halfKernelSize + numYSteps); float* halfKernel = bulkAlloc.get(); float* summedKernel = bulkAlloc.get() + halfKernelSize; float* yEvals = bulkAlloc.get() + 2 * halfKernelSize; make_half_kernel_and_summed_table(halfKernel, summedKernel, halfKernelSize, sigma); float firstX = -halfKernelSize + 0.5f; apply_kernel_in_y(yEvals, numYSteps, firstX, radius, halfKernelSize, summedKernel); for (int i = 0; i < numSteps - 1; ++i) { float evalX = i + 0.5f; profile[i] = eval_at(evalX, radius, halfKernel, halfKernelSize, yEvals + i); } // Ensure the tail of the Gaussian goes to zero. profile[numSteps - 1] = 0; bitmap.setImmutable(); return bitmap; } SkBitmap CreateHalfPlaneProfile(int profileWidth) { SkASSERT(!(profileWidth & 0x1)); SkBitmap bitmap; if (!bitmap.tryAllocPixels(SkImageInfo::MakeA8(profileWidth, 1))) { return bitmap; } uint8_t* profile = bitmap.getAddr8(0, 0); // The full kernel is 6 sigmas wide. const float sigma = profileWidth / 6.0f; const int halfKernelSize = profileWidth / 2; skia_private::AutoTArray halfKernel(halfKernelSize); // The half kernel should sum to 0.5. const float tot = 2.0f * make_unnormalized_half_kernel(halfKernel.get(), halfKernelSize, sigma); float sum = 0.0f; // Populate the profile from the right edge to the middle. for (int i = 0; i < halfKernelSize; ++i) { halfKernel[halfKernelSize - i - 1] /= tot; sum += halfKernel[halfKernelSize - i - 1]; profile[profileWidth - i - 1] = SkUnitScalarClampToByte(sum); } // Populate the profile from the middle to the left edge (by flipping the half kernel and // continuing the summation). for (int i = 0; i < halfKernelSize; ++i) { sum += halfKernel[i]; profile[halfKernelSize - i - 1] = SkUnitScalarClampToByte(sum); } // Ensure the tail of the Gaussian goes to zero. profile[profileWidth - 1] = 0; bitmap.setImmutable(); return bitmap; } /////////////////////////////////////////////////////////////////////////////// // RRect Blur /////////////////////////////////////////////////////////////////////////////// // Evaluate the vertical blur at the specified 'y' value given the location of the top of the // rrect. static uint8_t eval_V(float top, int y, const uint8_t* integral, int integralSize, float sixSigma) { if (top < 0) { return 0; // an empty column } float fT = (top - y - 0.5f) * (integralSize / sixSigma); if (fT < 0) { return 255; } else if (fT >= integralSize - 1) { return 0; } int lower = (int)fT; float frac = fT - lower; SkASSERT(lower + 1 < integralSize); return integral[lower] * (1.0f - frac) + integral[lower + 1] * frac; } // Apply a gaussian 'kernel' horizontally at the specified 'x', 'y' location. static uint8_t eval_H(int x, int y, const std::vector& topVec, const float* kernel, int kernelSize, const uint8_t* integral, int integralSize, float sixSigma) { SkASSERT(0 <= x && x < (int)topVec.size()); SkASSERT(kernelSize % 2); float accum = 0.0f; int xSampleLoc = x - (kernelSize / 2); for (int i = 0; i < kernelSize; ++i, ++xSampleLoc) { if (xSampleLoc < 0 || xSampleLoc >= (int)topVec.size()) { continue; } accum += kernel[i] * eval_V(topVec[xSampleLoc], y, integral, integralSize, sixSigma); } return accum + 0.5f; } SkBitmap CreateRRectBlurMask(const SkRRect& rrectToDraw, const SkISize& dimensions, float sigma) { SkASSERT(!skgpu::BlurIsEffectivelyIdentity(sigma)); int radius = skgpu::BlurSigmaRadius(sigma); int kernelSize = skgpu::BlurKernelWidth(radius); SkASSERT(kernelSize % 2); SkASSERT(dimensions.width() % 2); SkASSERT(dimensions.height() % 2); SkVector radii = rrectToDraw.getSimpleRadii(); SkASSERT(SkScalarNearlyEqual(radii.fX, radii.fY)); const int halfWidthPlus1 = (dimensions.width() / 2) + 1; const int halfHeightPlus1 = (dimensions.height() / 2) + 1; std::unique_ptr kernel(new float[kernelSize]); skgpu::Compute1DBlurKernel(sigma, radius, SkSpan(kernel.get(), kernelSize)); const int tableWidth = ComputeIntegralTableWidth(6.0f * sigma); SkBitmap integral = CreateIntegralTable(tableWidth); if (integral.empty()) { return {}; } SkBitmap result; if (!result.tryAllocPixels(SkImageInfo::MakeA8(dimensions.width(), dimensions.height()))) { return {}; } std::vector topVec; topVec.reserve(dimensions.width()); for (int x = 0; x < dimensions.width(); ++x) { if (x < rrectToDraw.rect().fLeft || x > rrectToDraw.rect().fRight) { topVec.push_back(-1); } else { if (x + 0.5f < rrectToDraw.rect().fLeft + radii.fX) { // in the circular section float xDist = rrectToDraw.rect().fLeft + radii.fX - x - 0.5f; float h = sqrtf(radii.fX * radii.fX - xDist * xDist); SkASSERT(0 <= h && h < radii.fY); topVec.push_back(rrectToDraw.rect().fTop + radii.fX - h + 3 * sigma); } else { topVec.push_back(rrectToDraw.rect().fTop + 3 * sigma); } } } for (int y = 0; y < halfHeightPlus1; ++y) { uint8_t* scanline = result.getAddr8(0, y); for (int x = 0; x < halfWidthPlus1; ++x) { scanline[x] = eval_H(x, y, topVec, kernel.get(), kernelSize, integral.getAddr8(0, 0), integral.width(), 6.0f * sigma); scanline[dimensions.width() - x - 1] = scanline[x]; } memcpy(result.getAddr8(0, dimensions.height() - y - 1), scanline, result.rowBytes()); } result.setImmutable(); return result; } } // namespace skgpu