Loading libs/renderengine/Android.bp +1 −0 Original line number Diff line number Diff line Loading @@ -107,6 +107,7 @@ filegroup { "skia/filters/GainmapFactory.cpp", "skia/filters/GaussianBlurFilter.cpp", "skia/filters/KawaseBlurDualFilter.cpp", "skia/filters/KawaseBlurDualFilterV2.cpp", "skia/filters/KawaseBlurFilter.cpp", "skia/filters/LutShader.cpp", "skia/filters/MouriMap.cpp", Loading libs/renderengine/skia/SkiaRenderEngine.cpp +6 −1 Original line number Diff line number Diff line Loading @@ -80,6 +80,7 @@ #include "filters/GainmapFactory.h" #include "filters/GaussianBlurFilter.h" #include "filters/KawaseBlurDualFilter.h" #include "filters/KawaseBlurDualFilterV2.h" #include "filters/KawaseBlurFilter.h" #include "filters/LutShader.h" #include "filters/MouriMap.h" Loading Loading @@ -303,7 +304,11 @@ SkiaRenderEngine::SkiaRenderEngine(Threaded threaded, PixelFormat pixelFormat, } case BlurAlgorithm::KAWASE_DUAL_FILTER: { ALOGD("Background Blurs Enabled (Kawase dual-filtering algorithm)"); if (FlagManager::getInstance().window_blur_kawase2_fix_aliasing()) { mBlurFilter = new KawaseBlurDualFilterV2(mRuntimeEffectManager); } else { mBlurFilter = new KawaseBlurDualFilter(mRuntimeEffectManager); } break; } default: { Loading libs/renderengine/skia/filters/KawaseBlurDualFilterV2.cpp 0 → 100644 +249 −0 Original line number Diff line number Diff line /* * Copyright 2025 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #define ATRACE_TAG ATRACE_TAG_GRAPHICS #include "KawaseBlurDualFilterV2.h" #include <SkAlphaType.h> #include <SkBlendMode.h> #include <SkCanvas.h> #include <SkData.h> #include <SkPaint.h> #include <SkRRect.h> #include <SkRuntimeEffect.h> #include <SkShader.h> #include <SkSize.h> #include <SkString.h> #include <SkSurface.h> #include <SkTileMode.h> #include <include/gpu/GpuTypes.h> #include <include/gpu/ganesh/SkSurfaceGanesh.h> #include <log/log.h> #include <utils/Trace.h> #include "RuntimeEffectManager.h" namespace android { namespace renderengine { namespace skia { KawaseBlurDualFilterV2::KawaseBlurDualFilterV2(RuntimeEffectManager& effectManager) : BlurFilter(effectManager) { // Samples each vertex of a diamond using a total of 4 samples. // This shader is used for the initial 4x down-sampling pass, with the sampling offset // handpicked to minimize aliasing. const SkString kQuarterResDownSampleBlurString(R"( uniform shader child; const float2 STEP_0 = float2( 0.25, 0.25); const float2 STEP_1 = float2( 0.25, -0.25); const float2 STEP_2 = float2(-0.25, -0.25); const float2 STEP_3 = float2(-0.25, 0.25); half4 main(float2 xy) { half3 c = child.eval(xy + STEP_0).rgb; c += child.eval(xy + STEP_1).rgb; c += child.eval(xy + STEP_2).rgb; c += child.eval(xy + STEP_3).rgb; return half4(c * 0.25, 1.0); } )"); // Samples each vertex of a diamond plus the center pixel, using a total of 5 samples. // This shader is used for the 2x down-sampling passes after the initial pass. const SkString kHalfResDownSampleBlurString(R"( uniform shader child; const float2 STEP_0 = float2( 0.5, 0.5); const float2 STEP_1 = float2( 0.5, -0.5); const float2 STEP_2 = float2(-0.5, -0.5); const float2 STEP_3 = float2(-0.5, 0.5); half4 main(float2 xy) { half3 c = child.eval(xy).rgb * 4.0; c += child.eval(xy + STEP_0).rgb; c += child.eval(xy + STEP_1).rgb; c += child.eval(xy + STEP_2).rgb; c += child.eval(xy + STEP_3).rgb; return half4(c * 0.125, 1.0); } )"); // A shader to sample each vertex of a unit regular heptagon, plus the original fragment // coordinate, using a total of 8 samples. const SkString kUpSampleBlurString(R"( uniform shader child; uniform float in_blurOffset; uniform float in_crossFade; uniform float in_weightedCrossFade; const float2 STEP_0 = float2( 1.0, 0.0); const float2 STEP_1 = float2( 0.623489802, 0.781831482); const float2 STEP_2 = float2(-0.222520934, 0.974927912); const float2 STEP_3 = float2(-0.900968868, 0.433883739); const float2 STEP_4 = float2( 0.900968868, -0.433883739); const float2 STEP_5 = float2(-0.222520934, -0.974927912); const float2 STEP_6 = float2(-0.623489802, -0.781831482); half4 main(float2 xy) { half3 c = child.eval(xy).rgb; c += child.eval(xy + STEP_0 * in_blurOffset).rgb; c += child.eval(xy + STEP_1 * in_blurOffset).rgb; c += child.eval(xy + STEP_2 * in_blurOffset).rgb; c += child.eval(xy + STEP_3 * in_blurOffset).rgb; c += child.eval(xy + STEP_4 * in_blurOffset).rgb; c += child.eval(xy + STEP_5 * in_blurOffset).rgb; c += child.eval(xy + STEP_6 * in_blurOffset).rgb; return half4(c * in_weightedCrossFade, in_crossFade); } )"); mQuarterResDownSampleBlurEffect = effectManager.createAndStoreRuntimeEffect( RuntimeEffectManager::KnownId::kKawaseBlurDualFilterV2_QuarterResDownSampleBlurEffect, "KawaseBlurDualFilterV2_QuarterResDownSampleBlurEffect", kQuarterResDownSampleBlurString); mHalfResDownSampleBlurEffect = effectManager.createAndStoreRuntimeEffect( RuntimeEffectManager::KnownId::kKawaseBlurDualFilterV2_HalfResDownSampleBlurEffect, "KawaseBlurDualFilterV2_HalfResDownSampleBlurEffect", kHalfResDownSampleBlurString); mUpSampleBlurEffect = effectManager .createAndStoreRuntimeEffect(RuntimeEffectManager::KnownId:: kKawaseBlurDualFilterV2_UpSampleBlurEffect, "KawaseBlurDualFilterV2_UpSampleBlurEffect", kUpSampleBlurString); } void KawaseBlurDualFilterV2::blurInto(const sk_sp<SkSurface>& drawSurface, const sk_sp<SkImage>& readImage, const float radius, const float alpha, const sk_sp<SkRuntimeEffect>& blurEffect) const { const float scale = static_cast<float>(drawSurface->width()) / readImage->width(); SkMatrix blurMatrix = SkMatrix::Scale(scale, scale); blurInto(drawSurface, readImage->makeShader(SkTileMode::kClamp, SkTileMode::kClamp, SkSamplingOptions(SkFilterMode::kLinear, SkMipmapMode::kNone), blurMatrix), radius, alpha, blurEffect); } void KawaseBlurDualFilterV2::blurInto(const sk_sp<SkSurface>& drawSurface, sk_sp<SkShader> input, const float radius, const float alpha, const sk_sp<SkRuntimeEffect>& blurEffect) const { SkPaint paint; if (blurEffect == mUpSampleBlurEffect) { if (radius == 0) { paint.setShader(std::move(input)); paint.setAlphaf(alpha); } else { SkRuntimeShaderBuilder blurBuilder(blurEffect); blurBuilder.child("child") = std::move(input); if (blurEffect == mUpSampleBlurEffect) { blurBuilder.uniform("in_crossFade") = alpha; blurBuilder.uniform("in_weightedCrossFade") = alpha * 0.125f; blurBuilder.uniform("in_blurOffset") = radius; } paint.setShader(blurBuilder.makeShader(nullptr)); } } else { SkRuntimeShaderBuilder blurBuilder(blurEffect); blurBuilder.child("child") = std::move(input); paint.setShader(blurBuilder.makeShader(nullptr)); } paint.setBlendMode(alpha == 1.0f ? SkBlendMode::kSrc : SkBlendMode::kSrcOver); drawSurface->getCanvas()->drawPaint(paint); } sk_sp<SkImage> KawaseBlurDualFilterV2::generate(SkiaGpuContext* context, const uint32_t blurRadius, const sk_sp<SkImage> input, const SkRect& blurRect) const { // Apply a conversion factor of (1 / sqrt(3)) to match Skia's built-in blur as used by // RenderEffect. See the comment in SkBlurMask.cpp for reasoning behind this. const float radius = blurRadius * 0.57735f; // Use a variable number of blur passes depending on the radius. The non-integer part of this // calculation is used to mix the final pass into the second-last with an alpha blend. constexpr int kMaxSurfaces = 4; const float filterDepth = std::min(kMaxSurfaces - 1.0f, radius * kInputScale / 2.5f); const int filterPasses = std::min(kMaxSurfaces - 1, static_cast<int>(ceil(filterDepth))); auto makeSurface = [&](float scale) -> sk_sp<SkSurface> { const auto newW = ceil(static_cast<float>(blurRect.width() / scale)); const auto newH = ceil(static_cast<float>(blurRect.height() / scale)); sk_sp<SkSurface> surface = context->createRenderTarget(input->imageInfo().makeWH(newW, newH)); LOG_ALWAYS_FATAL_IF(!surface, "%s: Failed to create surface for blurring!", __func__); return surface; }; // Render into surfaces downscaled by 1x, 2x, 4x and 8x from the initial downscale. sk_sp<SkSurface> surfaces[kMaxSurfaces] = {filterPasses >= 0 ? makeSurface(1 * kInverseInputScale) : nullptr, filterPasses >= 1 ? makeSurface(2 * kInverseInputScale) : nullptr, filterPasses >= 2 ? makeSurface(4 * kInverseInputScale) : nullptr, filterPasses >= 3 ? makeSurface(8 * kInverseInputScale) : nullptr}; // Kawase is an approximation of Gaussian, but behaves differently because it is made up of many // simpler blurs. A transformation is required to approximate the same effect as Gaussian. float sumSquaredR = 0; float sumSquaredStep = 0; for (int i = 0; i < filterPasses; i++) { const float alpha = std::min(1.0f, filterDepth - i); sumSquaredR += powf(powf(2.0f, i - 1) * alpha * M_SQRT2, 2.0f); sumSquaredStep += powf(powf(2.0f, i) * alpha, 2.0f); } // Solve for R = sqrt(sum(r_i^2)). float step = sqrt(max(0.0f, powf(radius * kInputScale, 2) - sumSquaredR) / (sumSquaredStep == 0 ? 1 : sumSquaredStep)); // Start by downscaling and doing the first blur pass { // For sampling Skia's API expects the inverse of what logically seems appropriate. In this // case one may expect Translate(blurRect.fLeft, blurRect.fTop) * Scale(kInverseInputScale) // but instead we must do the inverse. SkMatrix blurMatrix = SkMatrix::Translate(-blurRect.fLeft, -blurRect.fTop); blurMatrix.postScale(kInputScale, kInputScale); const auto sourceShader = input->makeShader(SkTileMode::kClamp, SkTileMode::kClamp, SkSamplingOptions(SkFilterMode::kLinear, SkMipmapMode::kNone), blurMatrix); blurInto(surfaces[0], std::move(sourceShader), 0, // unused blur radius. The blur effect hardcodes the radius. 1.0f, mQuarterResDownSampleBlurEffect); } // Next the remaining downscale blur passes. for (int i = 0; i < filterPasses; i++) { blurInto(surfaces[i + 1], surfaces[i]->makeTemporaryImage(), 0, // unused blur radius. The blur effect hardcodes the radius. 1.0f, mHalfResDownSampleBlurEffect); } // Finally blur+upscale back to our original size. for (int i = filterPasses - 1; i >= 0; i--) { int index = kMaxSurfaces * 2 - 2 - i; blurInto(surfaces[i], surfaces[i + 1]->makeTemporaryImage(), step, std::min(1.0f, filterDepth - i), mUpSampleBlurEffect); } return surfaces[0]->makeTemporaryImage(); } } // namespace skia } // namespace renderengine } // namespace android libs/renderengine/skia/filters/KawaseBlurDualFilterV2.h 0 → 100644 +61 −0 Original line number Diff line number Diff line /* * Copyright 2025 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include <SkCanvas.h> #include <SkImage.h> #include <SkRuntimeEffect.h> #include <SkSurface.h> #include "BlurFilter.h" #include "RuntimeEffectManager.h" namespace android { namespace renderengine { namespace skia { /** * This is an implementation of a Kawase blur with dual-filtering passes, as described in here: * https://community.arm.com/cfs-file/__key/communityserver-blogs-components-weblogfiles/00-00-00-20-66/siggraph2015_2D00_mmg_2D00_marius_2D00_slides.pdf * https://community.arm.com/cfs-file/__key/communityserver-blogs-components-weblogfiles/00-00-00-20-66/siggraph2015_2D00_mmg_2D00_marius_2D00_notes.pdf * It uses fixed blur radii in the downsampling passes to minimize aliasing, and dynamic radii * in the upsampling passes to produce sufficiently smooth blurs and support animation. */ class KawaseBlurDualFilterV2 : public BlurFilter { public: explicit KawaseBlurDualFilterV2(RuntimeEffectManager& effectManager); virtual ~KawaseBlurDualFilterV2() {} // Execute blur, saving it to a texture sk_sp<SkImage> generate(SkiaGpuContext* context, const uint32_t radius, const sk_sp<SkImage> blurInput, const SkRect& blurRect) const override; private: sk_sp<SkRuntimeEffect> mQuarterResDownSampleBlurEffect; sk_sp<SkRuntimeEffect> mHalfResDownSampleBlurEffect; sk_sp<SkRuntimeEffect> mUpSampleBlurEffect; void blurInto(const sk_sp<SkSurface>& drawSurface, const sk_sp<SkImage>& readImage, const float radius, const float alpha, const sk_sp<SkRuntimeEffect>&) const; void blurInto(const sk_sp<SkSurface>& drawSurface, const sk_sp<SkShader> input, const float radius, const float alpha, const sk_sp<SkRuntimeEffect>&) const; }; } // namespace skia } // namespace renderengine } // namespace android libs/renderengine/skia/filters/RuntimeEffectManager.h +3 −0 Original line number Diff line number Diff line Loading @@ -47,6 +47,9 @@ public: kGainmapEffect, kKawaseBlurDualFilter_LowSampleBlurEffect, kKawaseBlurDualFilter_HighSampleBlurEffect, kKawaseBlurDualFilterV2_QuarterResDownSampleBlurEffect, kKawaseBlurDualFilterV2_HalfResDownSampleBlurEffect, kKawaseBlurDualFilterV2_UpSampleBlurEffect, kKawaseBlurEffect, kLutEffect, kMouriMap_CrossTalkAndChunk16x16Effect, Loading Loading
libs/renderengine/Android.bp +1 −0 Original line number Diff line number Diff line Loading @@ -107,6 +107,7 @@ filegroup { "skia/filters/GainmapFactory.cpp", "skia/filters/GaussianBlurFilter.cpp", "skia/filters/KawaseBlurDualFilter.cpp", "skia/filters/KawaseBlurDualFilterV2.cpp", "skia/filters/KawaseBlurFilter.cpp", "skia/filters/LutShader.cpp", "skia/filters/MouriMap.cpp", Loading
libs/renderengine/skia/SkiaRenderEngine.cpp +6 −1 Original line number Diff line number Diff line Loading @@ -80,6 +80,7 @@ #include "filters/GainmapFactory.h" #include "filters/GaussianBlurFilter.h" #include "filters/KawaseBlurDualFilter.h" #include "filters/KawaseBlurDualFilterV2.h" #include "filters/KawaseBlurFilter.h" #include "filters/LutShader.h" #include "filters/MouriMap.h" Loading Loading @@ -303,7 +304,11 @@ SkiaRenderEngine::SkiaRenderEngine(Threaded threaded, PixelFormat pixelFormat, } case BlurAlgorithm::KAWASE_DUAL_FILTER: { ALOGD("Background Blurs Enabled (Kawase dual-filtering algorithm)"); if (FlagManager::getInstance().window_blur_kawase2_fix_aliasing()) { mBlurFilter = new KawaseBlurDualFilterV2(mRuntimeEffectManager); } else { mBlurFilter = new KawaseBlurDualFilter(mRuntimeEffectManager); } break; } default: { Loading
libs/renderengine/skia/filters/KawaseBlurDualFilterV2.cpp 0 → 100644 +249 −0 Original line number Diff line number Diff line /* * Copyright 2025 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #define ATRACE_TAG ATRACE_TAG_GRAPHICS #include "KawaseBlurDualFilterV2.h" #include <SkAlphaType.h> #include <SkBlendMode.h> #include <SkCanvas.h> #include <SkData.h> #include <SkPaint.h> #include <SkRRect.h> #include <SkRuntimeEffect.h> #include <SkShader.h> #include <SkSize.h> #include <SkString.h> #include <SkSurface.h> #include <SkTileMode.h> #include <include/gpu/GpuTypes.h> #include <include/gpu/ganesh/SkSurfaceGanesh.h> #include <log/log.h> #include <utils/Trace.h> #include "RuntimeEffectManager.h" namespace android { namespace renderengine { namespace skia { KawaseBlurDualFilterV2::KawaseBlurDualFilterV2(RuntimeEffectManager& effectManager) : BlurFilter(effectManager) { // Samples each vertex of a diamond using a total of 4 samples. // This shader is used for the initial 4x down-sampling pass, with the sampling offset // handpicked to minimize aliasing. const SkString kQuarterResDownSampleBlurString(R"( uniform shader child; const float2 STEP_0 = float2( 0.25, 0.25); const float2 STEP_1 = float2( 0.25, -0.25); const float2 STEP_2 = float2(-0.25, -0.25); const float2 STEP_3 = float2(-0.25, 0.25); half4 main(float2 xy) { half3 c = child.eval(xy + STEP_0).rgb; c += child.eval(xy + STEP_1).rgb; c += child.eval(xy + STEP_2).rgb; c += child.eval(xy + STEP_3).rgb; return half4(c * 0.25, 1.0); } )"); // Samples each vertex of a diamond plus the center pixel, using a total of 5 samples. // This shader is used for the 2x down-sampling passes after the initial pass. const SkString kHalfResDownSampleBlurString(R"( uniform shader child; const float2 STEP_0 = float2( 0.5, 0.5); const float2 STEP_1 = float2( 0.5, -0.5); const float2 STEP_2 = float2(-0.5, -0.5); const float2 STEP_3 = float2(-0.5, 0.5); half4 main(float2 xy) { half3 c = child.eval(xy).rgb * 4.0; c += child.eval(xy + STEP_0).rgb; c += child.eval(xy + STEP_1).rgb; c += child.eval(xy + STEP_2).rgb; c += child.eval(xy + STEP_3).rgb; return half4(c * 0.125, 1.0); } )"); // A shader to sample each vertex of a unit regular heptagon, plus the original fragment // coordinate, using a total of 8 samples. const SkString kUpSampleBlurString(R"( uniform shader child; uniform float in_blurOffset; uniform float in_crossFade; uniform float in_weightedCrossFade; const float2 STEP_0 = float2( 1.0, 0.0); const float2 STEP_1 = float2( 0.623489802, 0.781831482); const float2 STEP_2 = float2(-0.222520934, 0.974927912); const float2 STEP_3 = float2(-0.900968868, 0.433883739); const float2 STEP_4 = float2( 0.900968868, -0.433883739); const float2 STEP_5 = float2(-0.222520934, -0.974927912); const float2 STEP_6 = float2(-0.623489802, -0.781831482); half4 main(float2 xy) { half3 c = child.eval(xy).rgb; c += child.eval(xy + STEP_0 * in_blurOffset).rgb; c += child.eval(xy + STEP_1 * in_blurOffset).rgb; c += child.eval(xy + STEP_2 * in_blurOffset).rgb; c += child.eval(xy + STEP_3 * in_blurOffset).rgb; c += child.eval(xy + STEP_4 * in_blurOffset).rgb; c += child.eval(xy + STEP_5 * in_blurOffset).rgb; c += child.eval(xy + STEP_6 * in_blurOffset).rgb; return half4(c * in_weightedCrossFade, in_crossFade); } )"); mQuarterResDownSampleBlurEffect = effectManager.createAndStoreRuntimeEffect( RuntimeEffectManager::KnownId::kKawaseBlurDualFilterV2_QuarterResDownSampleBlurEffect, "KawaseBlurDualFilterV2_QuarterResDownSampleBlurEffect", kQuarterResDownSampleBlurString); mHalfResDownSampleBlurEffect = effectManager.createAndStoreRuntimeEffect( RuntimeEffectManager::KnownId::kKawaseBlurDualFilterV2_HalfResDownSampleBlurEffect, "KawaseBlurDualFilterV2_HalfResDownSampleBlurEffect", kHalfResDownSampleBlurString); mUpSampleBlurEffect = effectManager .createAndStoreRuntimeEffect(RuntimeEffectManager::KnownId:: kKawaseBlurDualFilterV2_UpSampleBlurEffect, "KawaseBlurDualFilterV2_UpSampleBlurEffect", kUpSampleBlurString); } void KawaseBlurDualFilterV2::blurInto(const sk_sp<SkSurface>& drawSurface, const sk_sp<SkImage>& readImage, const float radius, const float alpha, const sk_sp<SkRuntimeEffect>& blurEffect) const { const float scale = static_cast<float>(drawSurface->width()) / readImage->width(); SkMatrix blurMatrix = SkMatrix::Scale(scale, scale); blurInto(drawSurface, readImage->makeShader(SkTileMode::kClamp, SkTileMode::kClamp, SkSamplingOptions(SkFilterMode::kLinear, SkMipmapMode::kNone), blurMatrix), radius, alpha, blurEffect); } void KawaseBlurDualFilterV2::blurInto(const sk_sp<SkSurface>& drawSurface, sk_sp<SkShader> input, const float radius, const float alpha, const sk_sp<SkRuntimeEffect>& blurEffect) const { SkPaint paint; if (blurEffect == mUpSampleBlurEffect) { if (radius == 0) { paint.setShader(std::move(input)); paint.setAlphaf(alpha); } else { SkRuntimeShaderBuilder blurBuilder(blurEffect); blurBuilder.child("child") = std::move(input); if (blurEffect == mUpSampleBlurEffect) { blurBuilder.uniform("in_crossFade") = alpha; blurBuilder.uniform("in_weightedCrossFade") = alpha * 0.125f; blurBuilder.uniform("in_blurOffset") = radius; } paint.setShader(blurBuilder.makeShader(nullptr)); } } else { SkRuntimeShaderBuilder blurBuilder(blurEffect); blurBuilder.child("child") = std::move(input); paint.setShader(blurBuilder.makeShader(nullptr)); } paint.setBlendMode(alpha == 1.0f ? SkBlendMode::kSrc : SkBlendMode::kSrcOver); drawSurface->getCanvas()->drawPaint(paint); } sk_sp<SkImage> KawaseBlurDualFilterV2::generate(SkiaGpuContext* context, const uint32_t blurRadius, const sk_sp<SkImage> input, const SkRect& blurRect) const { // Apply a conversion factor of (1 / sqrt(3)) to match Skia's built-in blur as used by // RenderEffect. See the comment in SkBlurMask.cpp for reasoning behind this. const float radius = blurRadius * 0.57735f; // Use a variable number of blur passes depending on the radius. The non-integer part of this // calculation is used to mix the final pass into the second-last with an alpha blend. constexpr int kMaxSurfaces = 4; const float filterDepth = std::min(kMaxSurfaces - 1.0f, radius * kInputScale / 2.5f); const int filterPasses = std::min(kMaxSurfaces - 1, static_cast<int>(ceil(filterDepth))); auto makeSurface = [&](float scale) -> sk_sp<SkSurface> { const auto newW = ceil(static_cast<float>(blurRect.width() / scale)); const auto newH = ceil(static_cast<float>(blurRect.height() / scale)); sk_sp<SkSurface> surface = context->createRenderTarget(input->imageInfo().makeWH(newW, newH)); LOG_ALWAYS_FATAL_IF(!surface, "%s: Failed to create surface for blurring!", __func__); return surface; }; // Render into surfaces downscaled by 1x, 2x, 4x and 8x from the initial downscale. sk_sp<SkSurface> surfaces[kMaxSurfaces] = {filterPasses >= 0 ? makeSurface(1 * kInverseInputScale) : nullptr, filterPasses >= 1 ? makeSurface(2 * kInverseInputScale) : nullptr, filterPasses >= 2 ? makeSurface(4 * kInverseInputScale) : nullptr, filterPasses >= 3 ? makeSurface(8 * kInverseInputScale) : nullptr}; // Kawase is an approximation of Gaussian, but behaves differently because it is made up of many // simpler blurs. A transformation is required to approximate the same effect as Gaussian. float sumSquaredR = 0; float sumSquaredStep = 0; for (int i = 0; i < filterPasses; i++) { const float alpha = std::min(1.0f, filterDepth - i); sumSquaredR += powf(powf(2.0f, i - 1) * alpha * M_SQRT2, 2.0f); sumSquaredStep += powf(powf(2.0f, i) * alpha, 2.0f); } // Solve for R = sqrt(sum(r_i^2)). float step = sqrt(max(0.0f, powf(radius * kInputScale, 2) - sumSquaredR) / (sumSquaredStep == 0 ? 1 : sumSquaredStep)); // Start by downscaling and doing the first blur pass { // For sampling Skia's API expects the inverse of what logically seems appropriate. In this // case one may expect Translate(blurRect.fLeft, blurRect.fTop) * Scale(kInverseInputScale) // but instead we must do the inverse. SkMatrix blurMatrix = SkMatrix::Translate(-blurRect.fLeft, -blurRect.fTop); blurMatrix.postScale(kInputScale, kInputScale); const auto sourceShader = input->makeShader(SkTileMode::kClamp, SkTileMode::kClamp, SkSamplingOptions(SkFilterMode::kLinear, SkMipmapMode::kNone), blurMatrix); blurInto(surfaces[0], std::move(sourceShader), 0, // unused blur radius. The blur effect hardcodes the radius. 1.0f, mQuarterResDownSampleBlurEffect); } // Next the remaining downscale blur passes. for (int i = 0; i < filterPasses; i++) { blurInto(surfaces[i + 1], surfaces[i]->makeTemporaryImage(), 0, // unused blur radius. The blur effect hardcodes the radius. 1.0f, mHalfResDownSampleBlurEffect); } // Finally blur+upscale back to our original size. for (int i = filterPasses - 1; i >= 0; i--) { int index = kMaxSurfaces * 2 - 2 - i; blurInto(surfaces[i], surfaces[i + 1]->makeTemporaryImage(), step, std::min(1.0f, filterDepth - i), mUpSampleBlurEffect); } return surfaces[0]->makeTemporaryImage(); } } // namespace skia } // namespace renderengine } // namespace android
libs/renderengine/skia/filters/KawaseBlurDualFilterV2.h 0 → 100644 +61 −0 Original line number Diff line number Diff line /* * Copyright 2025 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include <SkCanvas.h> #include <SkImage.h> #include <SkRuntimeEffect.h> #include <SkSurface.h> #include "BlurFilter.h" #include "RuntimeEffectManager.h" namespace android { namespace renderengine { namespace skia { /** * This is an implementation of a Kawase blur with dual-filtering passes, as described in here: * https://community.arm.com/cfs-file/__key/communityserver-blogs-components-weblogfiles/00-00-00-20-66/siggraph2015_2D00_mmg_2D00_marius_2D00_slides.pdf * https://community.arm.com/cfs-file/__key/communityserver-blogs-components-weblogfiles/00-00-00-20-66/siggraph2015_2D00_mmg_2D00_marius_2D00_notes.pdf * It uses fixed blur radii in the downsampling passes to minimize aliasing, and dynamic radii * in the upsampling passes to produce sufficiently smooth blurs and support animation. */ class KawaseBlurDualFilterV2 : public BlurFilter { public: explicit KawaseBlurDualFilterV2(RuntimeEffectManager& effectManager); virtual ~KawaseBlurDualFilterV2() {} // Execute blur, saving it to a texture sk_sp<SkImage> generate(SkiaGpuContext* context, const uint32_t radius, const sk_sp<SkImage> blurInput, const SkRect& blurRect) const override; private: sk_sp<SkRuntimeEffect> mQuarterResDownSampleBlurEffect; sk_sp<SkRuntimeEffect> mHalfResDownSampleBlurEffect; sk_sp<SkRuntimeEffect> mUpSampleBlurEffect; void blurInto(const sk_sp<SkSurface>& drawSurface, const sk_sp<SkImage>& readImage, const float radius, const float alpha, const sk_sp<SkRuntimeEffect>&) const; void blurInto(const sk_sp<SkSurface>& drawSurface, const sk_sp<SkShader> input, const float radius, const float alpha, const sk_sp<SkRuntimeEffect>&) const; }; } // namespace skia } // namespace renderengine } // namespace android
libs/renderengine/skia/filters/RuntimeEffectManager.h +3 −0 Original line number Diff line number Diff line Loading @@ -47,6 +47,9 @@ public: kGainmapEffect, kKawaseBlurDualFilter_LowSampleBlurEffect, kKawaseBlurDualFilter_HighSampleBlurEffect, kKawaseBlurDualFilterV2_QuarterResDownSampleBlurEffect, kKawaseBlurDualFilterV2_HalfResDownSampleBlurEffect, kKawaseBlurDualFilterV2_UpSampleBlurEffect, kKawaseBlurEffect, kLutEffect, kMouriMap_CrossTalkAndChunk16x16Effect, Loading