| // Copyright 2013 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "impeller/entity/contents/filters/gaussian_blur_filter_contents.h" |
| |
| #include <cmath> |
| |
| #include "flutter/fml/make_copyable.h" |
| #include "impeller/entity/contents/clip_contents.h" |
| #include "impeller/entity/contents/color_source_contents.h" |
| #include "impeller/entity/contents/content_context.h" |
| #include "impeller/entity/texture_fill.frag.h" |
| #include "impeller/entity/texture_fill.vert.h" |
| #include "impeller/renderer/command.h" |
| #include "impeller/renderer/render_pass.h" |
| #include "impeller/renderer/texture_mipmap.h" |
| #include "impeller/renderer/vertex_buffer_builder.h" |
| |
| namespace impeller { |
| |
| using GaussianBlurVertexShader = KernelPipeline::VertexShader; |
| using GaussianBlurFragmentShader = KernelPipeline::FragmentShader; |
| |
| const int32_t GaussianBlurFilterContents::kBlurFilterRequiredMipCount = 4; |
| |
| namespace { |
| |
| // 48 comes from kernel.glsl. |
| const int32_t kMaxKernelSize = 48; |
| |
| SamplerDescriptor MakeSamplerDescriptor(MinMagFilter filter, |
| SamplerAddressMode address_mode) { |
| SamplerDescriptor sampler_desc; |
| sampler_desc.min_filter = filter; |
| sampler_desc.mag_filter = filter; |
| sampler_desc.width_address_mode = address_mode; |
| sampler_desc.height_address_mode = address_mode; |
| return sampler_desc; |
| } |
| |
| template <typename T> |
| void BindVertices(RenderPass& pass, |
| HostBuffer& host_buffer, |
| std::initializer_list<typename T::PerVertexData>&& vertices) { |
| VertexBufferBuilder<typename T::PerVertexData> vtx_builder; |
| vtx_builder.AddVertices(vertices); |
| pass.SetVertexBuffer(vtx_builder.CreateVertexBuffer(host_buffer)); |
| } |
| |
| void SetTileMode(SamplerDescriptor* descriptor, |
| const ContentContext& renderer, |
| Entity::TileMode tile_mode) { |
| switch (tile_mode) { |
| case Entity::TileMode::kDecal: |
| if (renderer.GetDeviceCapabilities().SupportsDecalSamplerAddressMode()) { |
| descriptor->width_address_mode = SamplerAddressMode::kDecal; |
| descriptor->height_address_mode = SamplerAddressMode::kDecal; |
| } |
| break; |
| case Entity::TileMode::kClamp: |
| descriptor->width_address_mode = SamplerAddressMode::kClampToEdge; |
| descriptor->height_address_mode = SamplerAddressMode::kClampToEdge; |
| break; |
| case Entity::TileMode::kMirror: |
| descriptor->width_address_mode = SamplerAddressMode::kMirror; |
| descriptor->height_address_mode = SamplerAddressMode::kMirror; |
| break; |
| case Entity::TileMode::kRepeat: |
| descriptor->width_address_mode = SamplerAddressMode::kRepeat; |
| descriptor->height_address_mode = SamplerAddressMode::kRepeat; |
| break; |
| } |
| } |
| |
| /// Makes a subpass that will render the scaled down input and add the |
| /// transparent gutter required for the blur halo. |
| fml::StatusOr<RenderTarget> MakeDownsampleSubpass( |
| const ContentContext& renderer, |
| const std::shared_ptr<CommandBuffer>& command_buffer, |
| std::shared_ptr<Texture> input_texture, |
| const SamplerDescriptor& sampler_descriptor, |
| const Quad& uvs, |
| const ISize& subpass_size, |
| Entity::TileMode tile_mode) { |
| ContentContext::SubpassCallback subpass_callback = |
| [&](const ContentContext& renderer, RenderPass& pass) { |
| HostBuffer& host_buffer = renderer.GetTransientsBuffer(); |
| |
| pass.SetCommandLabel("Gaussian blur downsample"); |
| auto pipeline_options = OptionsFromPass(pass); |
| pipeline_options.primitive_type = PrimitiveType::kTriangleStrip; |
| pass.SetPipeline(renderer.GetTexturePipeline(pipeline_options)); |
| |
| TextureFillVertexShader::FrameInfo frame_info; |
| frame_info.mvp = Matrix::MakeOrthographic(ISize(1, 1)); |
| frame_info.texture_sampler_y_coord_scale = 1.0; |
| frame_info.alpha = 1.0; |
| |
| BindVertices<TextureFillVertexShader>(pass, host_buffer, |
| { |
| {Point(0, 0), uvs[0]}, |
| {Point(1, 0), uvs[1]}, |
| {Point(0, 1), uvs[2]}, |
| {Point(1, 1), uvs[3]}, |
| }); |
| |
| SamplerDescriptor linear_sampler_descriptor = sampler_descriptor; |
| SetTileMode(&linear_sampler_descriptor, renderer, tile_mode); |
| linear_sampler_descriptor.mag_filter = MinMagFilter::kLinear; |
| linear_sampler_descriptor.min_filter = MinMagFilter::kLinear; |
| TextureFillVertexShader::BindFrameInfo( |
| pass, host_buffer.EmplaceUniform(frame_info)); |
| TextureFillFragmentShader::BindTextureSampler( |
| pass, input_texture, |
| renderer.GetContext()->GetSamplerLibrary()->GetSampler( |
| linear_sampler_descriptor)); |
| |
| return pass.Draw().ok(); |
| }; |
| fml::StatusOr<RenderTarget> render_target = renderer.MakeSubpass( |
| "Gaussian Blur Filter", subpass_size, command_buffer, subpass_callback); |
| return render_target; |
| } |
| |
| fml::StatusOr<RenderTarget> MakeBlurSubpass( |
| const ContentContext& renderer, |
| const std::shared_ptr<CommandBuffer>& command_buffer, |
| const RenderTarget& input_pass, |
| const SamplerDescriptor& sampler_descriptor, |
| Entity::TileMode tile_mode, |
| const BlurParameters& blur_info, |
| std::optional<RenderTarget> destination_target, |
| const Quad& blur_uvs) { |
| if (blur_info.blur_sigma < kEhCloseEnough) { |
| return input_pass; |
| } |
| |
| std::shared_ptr<Texture> input_texture = input_pass.GetRenderTargetTexture(); |
| |
| // TODO(gaaclarke): This blurs the whole image, but because we know the clip |
| // region we could focus on just blurring that. |
| ISize subpass_size = input_texture->GetSize(); |
| ContentContext::SubpassCallback subpass_callback = |
| [&](const ContentContext& renderer, RenderPass& pass) { |
| GaussianBlurVertexShader::FrameInfo frame_info{ |
| .mvp = Matrix::MakeOrthographic(ISize(1, 1)), |
| .texture_sampler_y_coord_scale = 1.0}; |
| |
| HostBuffer& host_buffer = renderer.GetTransientsBuffer(); |
| |
| ContentContextOptions options = OptionsFromPass(pass); |
| options.primitive_type = PrimitiveType::kTriangleStrip; |
| |
| if (tile_mode == Entity::TileMode::kDecal && |
| !renderer.GetDeviceCapabilities() |
| .SupportsDecalSamplerAddressMode()) { |
| pass.SetPipeline(renderer.GetKernelDecalPipeline(options)); |
| } else { |
| pass.SetPipeline(renderer.GetKernelPipeline(options)); |
| } |
| |
| BindVertices<GaussianBlurVertexShader>(pass, host_buffer, |
| { |
| {blur_uvs[0], blur_uvs[0]}, |
| {blur_uvs[1], blur_uvs[1]}, |
| {blur_uvs[2], blur_uvs[2]}, |
| {blur_uvs[3], blur_uvs[3]}, |
| }); |
| |
| SamplerDescriptor linear_sampler_descriptor = sampler_descriptor; |
| linear_sampler_descriptor.mag_filter = MinMagFilter::kLinear; |
| linear_sampler_descriptor.min_filter = MinMagFilter::kLinear; |
| GaussianBlurFragmentShader::BindTextureSampler( |
| pass, input_texture, |
| renderer.GetContext()->GetSamplerLibrary()->GetSampler( |
| linear_sampler_descriptor)); |
| GaussianBlurVertexShader::BindFrameInfo( |
| pass, host_buffer.EmplaceUniform(frame_info)); |
| KernelPipeline::FragmentShader::KernelSamples kernel_samples = |
| LerpHackKernelSamples(GenerateBlurInfo(blur_info)); |
| FML_CHECK(kernel_samples.sample_count < kMaxKernelSize); |
| GaussianBlurFragmentShader::BindKernelSamples( |
| pass, host_buffer.EmplaceUniform(kernel_samples)); |
| return pass.Draw().ok(); |
| }; |
| if (destination_target.has_value()) { |
| return renderer.MakeSubpass("Gaussian Blur Filter", |
| destination_target.value(), command_buffer, |
| subpass_callback); |
| } else { |
| return renderer.MakeSubpass("Gaussian Blur Filter", subpass_size, |
| command_buffer, subpass_callback); |
| } |
| } |
| |
| /// Returns `rect` relative to `reference`, where Rect::MakeXYWH(0,0,1,1) will |
| /// be returned when `rect` == `reference`. |
| Rect MakeReferenceUVs(const Rect& reference, const Rect& rect) { |
| Rect result = Rect::MakeOriginSize(rect.GetOrigin() - reference.GetOrigin(), |
| rect.GetSize()); |
| return result.Scale(1.0f / Vector2(reference.GetSize())); |
| } |
| |
| int ScaleBlurRadius(Scalar radius, Scalar scalar) { |
| return static_cast<int>(std::round(radius * scalar)); |
| } |
| |
| Entity ApplyClippedBlurStyle(Entity::ClipOperation clip_operation, |
| const Entity& entity, |
| const std::shared_ptr<FilterInput>& input, |
| const Snapshot& input_snapshot, |
| Entity blur_entity, |
| const std::shared_ptr<Geometry>& geometry) { |
| auto clip_contents = std::make_shared<ClipContents>(); |
| clip_contents->SetClipOperation(clip_operation); |
| clip_contents->SetGeometry(geometry); |
| Entity clipper; |
| clipper.SetContents(clip_contents); |
| auto restore = std::make_unique<ClipRestoreContents>(); |
| Matrix entity_transform = entity.GetTransform(); |
| Matrix blur_transform = blur_entity.GetTransform(); |
| auto renderer = fml::MakeCopyable( |
| [blur_entity = blur_entity.Clone(), clipper = std::move(clipper), |
| restore = std::move(restore), entity_transform, |
| blur_transform](const ContentContext& renderer, const Entity& entity, |
| RenderPass& pass) mutable { |
| bool result = true; |
| clipper.SetClipDepth(entity.GetClipDepth()); |
| clipper.SetTransform(entity.GetTransform() * entity_transform); |
| result = clipper.Render(renderer, pass) && result; |
| blur_entity.SetClipDepth(entity.GetClipDepth()); |
| blur_entity.SetTransform(entity.GetTransform() * blur_transform); |
| result = blur_entity.Render(renderer, pass) && result; |
| return result; |
| }); |
| auto coverage = |
| fml::MakeCopyable([blur_entity = std::move(blur_entity), |
| blur_transform](const Entity& entity) mutable { |
| blur_entity.SetTransform(entity.GetTransform() * blur_transform); |
| return blur_entity.GetCoverage(); |
| }); |
| Entity result; |
| result.SetContents(Contents::MakeAnonymous(renderer, coverage)); |
| return result; |
| } |
| |
| Entity ApplyBlurStyle(FilterContents::BlurStyle blur_style, |
| const Entity& entity, |
| const std::shared_ptr<FilterInput>& input, |
| const Snapshot& input_snapshot, |
| Entity blur_entity, |
| const std::shared_ptr<Geometry>& geometry) { |
| switch (blur_style) { |
| case FilterContents::BlurStyle::kNormal: |
| return blur_entity; |
| case FilterContents::BlurStyle::kInner: |
| return ApplyClippedBlurStyle(Entity::ClipOperation::kIntersect, entity, |
| input, input_snapshot, |
| std::move(blur_entity), geometry); |
| break; |
| case FilterContents::BlurStyle::kOuter: |
| return ApplyClippedBlurStyle(Entity::ClipOperation::kDifference, entity, |
| input, input_snapshot, |
| std::move(blur_entity), geometry); |
| case FilterContents::BlurStyle::kSolid: { |
| Entity snapshot_entity = |
| Entity::FromSnapshot(input_snapshot, entity.GetBlendMode()); |
| Entity result; |
| Matrix blurred_transform = blur_entity.GetTransform(); |
| Matrix snapshot_transform = snapshot_entity.GetTransform(); |
| result.SetContents(Contents::MakeAnonymous( |
| fml::MakeCopyable([blur_entity = blur_entity.Clone(), |
| blurred_transform, snapshot_transform, |
| snapshot_entity = std::move(snapshot_entity)]( |
| const ContentContext& renderer, |
| const Entity& entity, |
| RenderPass& pass) mutable { |
| bool result = true; |
| blur_entity.SetClipDepth(entity.GetClipDepth()); |
| blur_entity.SetTransform(entity.GetTransform() * blurred_transform); |
| result = result && blur_entity.Render(renderer, pass); |
| snapshot_entity.SetTransform(entity.GetTransform() * |
| snapshot_transform); |
| snapshot_entity.SetClipDepth(entity.GetClipDepth()); |
| result = result && snapshot_entity.Render(renderer, pass); |
| return result; |
| }), |
| fml::MakeCopyable([blur_entity = blur_entity.Clone(), |
| blurred_transform](const Entity& entity) mutable { |
| blur_entity.SetTransform(entity.GetTransform() * blurred_transform); |
| return blur_entity.GetCoverage(); |
| }))); |
| return result; |
| } |
| } |
| } |
| } // namespace |
| |
| std::string_view GaussianBlurFilterContents::kNoMipsError = |
| "Applying gaussian blur without mipmap."; |
| |
| GaussianBlurFilterContents::GaussianBlurFilterContents( |
| Scalar sigma_x, |
| Scalar sigma_y, |
| Entity::TileMode tile_mode, |
| BlurStyle mask_blur_style, |
| const std::shared_ptr<Geometry>& mask_geometry) |
| : sigma_x_(sigma_x), |
| sigma_y_(sigma_y), |
| tile_mode_(tile_mode), |
| mask_blur_style_(mask_blur_style), |
| mask_geometry_(mask_geometry) { |
| // This is supposed to be enforced at a higher level. |
| FML_DCHECK(mask_blur_style == BlurStyle::kNormal || mask_geometry); |
| } |
| |
| // This value was extracted from Skia, see: |
| // * https://github.com/google/skia/blob/d29cc3fe182f6e8a8539004a6a4ee8251677a6fd/src/gpu/ganesh/GrBlurUtils.cpp#L2561-L2576 |
| // * https://github.com/google/skia/blob/d29cc3fe182f6e8a8539004a6a4ee8251677a6fd/src/gpu/BlurUtils.h#L57 |
| Scalar GaussianBlurFilterContents::CalculateScale(Scalar sigma) { |
| if (sigma <= 4) { |
| return 1.0; |
| } |
| Scalar raw_result = 4.0 / sigma; |
| // Round to the nearest 1/(2^n) to get the best quality down scaling. |
| Scalar exponent = round(log2f(raw_result)); |
| // Don't scale down below 1/16th to preserve signal. |
| exponent = std::max(-4.0f, exponent); |
| Scalar rounded = powf(2.0f, exponent); |
| Scalar result = rounded; |
| // Extend the range of the 1/8th downsample based on the effective kernel size |
| // for the blur. |
| if (rounded < 0.125f) { |
| Scalar rounded_plus = powf(2.0f, exponent + 1); |
| Scalar blur_radius = CalculateBlurRadius(sigma); |
| int kernel_size_plus = (ScaleBlurRadius(blur_radius, rounded_plus) * 2) + 1; |
| // This constant was picked by looking at the results to make sure no |
| // shimmering was introduced at the highest sigma values that downscale to |
| // 1/16th. |
| static constexpr int32_t kEighthDownsampleKernalWidthMax = 41; |
| result = kernel_size_plus <= kEighthDownsampleKernalWidthMax ? rounded_plus |
| : rounded; |
| } |
| return result; |
| }; |
| |
| std::optional<Rect> GaussianBlurFilterContents::GetFilterSourceCoverage( |
| const Matrix& effect_transform, |
| const Rect& output_limit) const { |
| Vector2 scaled_sigma = {ScaleSigma(sigma_x_), ScaleSigma(sigma_y_)}; |
| Vector2 blur_radius = {CalculateBlurRadius(scaled_sigma.x), |
| CalculateBlurRadius(scaled_sigma.y)}; |
| Vector3 blur_radii = |
| effect_transform.Basis() * Vector3{blur_radius.x, blur_radius.y, 0.0}; |
| return output_limit.Expand(Point(blur_radii.x, blur_radii.y)); |
| } |
| |
| std::optional<Rect> GaussianBlurFilterContents::GetFilterCoverage( |
| const FilterInput::Vector& inputs, |
| const Entity& entity, |
| const Matrix& effect_transform) const { |
| if (inputs.empty()) { |
| return {}; |
| } |
| |
| std::optional<Rect> input_coverage = inputs[0]->GetCoverage(entity); |
| if (!input_coverage.has_value()) { |
| return {}; |
| } |
| |
| Vector2 scaled_sigma = (effect_transform.Basis() * |
| Vector2(ScaleSigma(sigma_x_), ScaleSigma(sigma_y_))) |
| .Abs(); |
| Vector2 blur_radius = Vector2(CalculateBlurRadius(scaled_sigma.x), |
| CalculateBlurRadius(scaled_sigma.y)); |
| Vector2 padding(ceil(blur_radius.x), ceil(blur_radius.y)); |
| Vector2 local_padding = (entity.GetTransform().Basis() * padding).Abs(); |
| return input_coverage.value().Expand(Point(local_padding.x, local_padding.y)); |
| } |
| |
| std::optional<Entity> GaussianBlurFilterContents::RenderFilter( |
| const FilterInput::Vector& inputs, |
| const ContentContext& renderer, |
| const Entity& entity, |
| const Matrix& effect_transform, |
| const Rect& coverage, |
| const std::optional<Rect>& coverage_hint) const { |
| if (inputs.empty()) { |
| return std::nullopt; |
| } |
| |
| Vector2 scaled_sigma = (effect_transform.Basis() * |
| Vector2(ScaleSigma(sigma_x_), ScaleSigma(sigma_y_))) |
| .Abs(); |
| Vector2 blur_radius = Vector2(CalculateBlurRadius(scaled_sigma.x), |
| CalculateBlurRadius(scaled_sigma.y)); |
| Vector2 padding(ceil(blur_radius.x), ceil(blur_radius.y)); |
| Vector2 local_padding = (entity.GetTransform().Basis() * padding).Abs(); |
| |
| // Apply as much of the desired padding as possible from the source. This may |
| // be ignored so must be accounted for in the downsample pass by adding a |
| // transparent gutter. |
| std::optional<Rect> expanded_coverage_hint; |
| if (coverage_hint.has_value()) { |
| expanded_coverage_hint = coverage_hint->Expand(local_padding); |
| } |
| |
| int32_t mip_count = kBlurFilterRequiredMipCount; |
| if (renderer.GetContext()->GetBackendType() == |
| Context::BackendType::kOpenGLES) { |
| // TODO(https://github.com/flutter/flutter/issues/141732): Implement mip map |
| // generation on opengles. |
| mip_count = 1; |
| } |
| |
| std::optional<Snapshot> input_snapshot = |
| inputs[0]->GetSnapshot("GaussianBlur", renderer, entity, |
| /*coverage_limit=*/expanded_coverage_hint, |
| /*mip_count=*/mip_count); |
| if (!input_snapshot.has_value()) { |
| return std::nullopt; |
| } |
| |
| if (scaled_sigma.x < kEhCloseEnough && scaled_sigma.y < kEhCloseEnough) { |
| return Entity::FromSnapshot(input_snapshot.value(), |
| entity.GetBlendMode()); // No blur to render. |
| } |
| |
| // In order to avoid shimmering in downsampling step, we should have mips. |
| if (input_snapshot->texture->GetMipCount() <= 1) { |
| FML_DLOG(ERROR) << kNoMipsError; |
| } |
| FML_DCHECK(!input_snapshot->texture->NeedsMipmapGeneration()); |
| |
| Scalar desired_scalar = |
| std::min(CalculateScale(scaled_sigma.x), CalculateScale(scaled_sigma.y)); |
| // TODO(jonahwilliams): If desired_scalar is 1.0 and we fully acquired the |
| // gutter from the expanded_coverage_hint, we can skip the downsample pass. |
| // pass. |
| Vector2 downsample_scalar(desired_scalar, desired_scalar); |
| Rect source_rect = Rect::MakeSize(input_snapshot->texture->GetSize()); |
| Rect source_rect_padded = source_rect.Expand(padding); |
| Matrix padding_snapshot_adjustment = Matrix::MakeTranslation(-padding); |
| // TODO(gaaclarke): The padding could be removed if we know it's not needed or |
| // resized to account for the expanded_clip_coverage. There doesn't appear |
| // to be the math to make those calculations though. The following |
| // optimization works, but causes a shimmer as a result of |
| // https://github.com/flutter/flutter/issues/140193 so it isn't applied. |
| // |
| // !input_snapshot->GetCoverage()->Expand(-local_padding) |
| // .Contains(coverage_hint.value())) |
| Vector2 downsampled_size = source_rect_padded.GetSize() * downsample_scalar; |
| ISize subpass_size = |
| ISize(round(downsampled_size.x), round(downsampled_size.y)); |
| Vector2 effective_scalar = |
| Vector2(subpass_size) / source_rect_padded.GetSize(); |
| |
| Quad uvs = CalculateUVs(inputs[0], entity, source_rect_padded, |
| input_snapshot->texture->GetSize()); |
| |
| std::shared_ptr<CommandBuffer> command_buffer = |
| renderer.GetContext()->CreateCommandBuffer(); |
| if (!command_buffer) { |
| return std::nullopt; |
| } |
| |
| fml::StatusOr<RenderTarget> pass1_out = MakeDownsampleSubpass( |
| renderer, command_buffer, input_snapshot->texture, |
| input_snapshot->sampler_descriptor, uvs, subpass_size, tile_mode_); |
| |
| if (!pass1_out.ok()) { |
| return std::nullopt; |
| } |
| |
| Vector2 pass1_pixel_size = |
| 1.0 / Vector2(pass1_out.value().GetRenderTargetTexture()->GetSize()); |
| |
| std::optional<Rect> input_snapshot_coverage = input_snapshot->GetCoverage(); |
| Quad blur_uvs = {Point(0, 0), Point(1, 0), Point(0, 1), Point(1, 1)}; |
| if (expanded_coverage_hint.has_value() && |
| input_snapshot_coverage.has_value() && |
| // TODO(https://github.com/flutter/flutter/issues/140890): Remove this |
| // condition. There is some flaw in coverage stopping us from using this |
| // today. I attempted to use source coordinates to calculate the uvs, |
| // but that didn't work either. |
| input_snapshot.has_value() && |
| input_snapshot.value().transform.IsTranslationScaleOnly()) { |
| // Only process the uvs where the blur is happening, not the whole texture. |
| std::optional<Rect> uvs = MakeReferenceUVs(input_snapshot_coverage.value(), |
| expanded_coverage_hint.value()) |
| .Intersection(Rect::MakeSize(Size(1, 1))); |
| FML_DCHECK(uvs.has_value()); |
| if (uvs.has_value()) { |
| blur_uvs[0] = uvs->GetLeftTop(); |
| blur_uvs[1] = uvs->GetRightTop(); |
| blur_uvs[2] = uvs->GetLeftBottom(); |
| blur_uvs[3] = uvs->GetRightBottom(); |
| } |
| } |
| |
| fml::StatusOr<RenderTarget> pass2_out = MakeBlurSubpass( |
| renderer, command_buffer, /*input_pass=*/pass1_out.value(), |
| input_snapshot->sampler_descriptor, tile_mode_, |
| BlurParameters{ |
| .blur_uv_offset = Point(0.0, pass1_pixel_size.y), |
| .blur_sigma = scaled_sigma.y * effective_scalar.y, |
| .blur_radius = ScaleBlurRadius(blur_radius.y, effective_scalar.y), |
| .step_size = 1, |
| }, |
| /*destination_target=*/std::nullopt, blur_uvs); |
| |
| if (!pass2_out.ok()) { |
| return std::nullopt; |
| } |
| |
| // Only ping pong if the first pass actually created a render target. |
| auto pass3_destination = pass2_out.value().GetRenderTargetTexture() != |
| pass1_out.value().GetRenderTargetTexture() |
| ? std::optional<RenderTarget>(pass1_out.value()) |
| : std::optional<RenderTarget>(std::nullopt); |
| |
| fml::StatusOr<RenderTarget> pass3_out = MakeBlurSubpass( |
| renderer, command_buffer, /*input_pass=*/pass2_out.value(), |
| input_snapshot->sampler_descriptor, tile_mode_, |
| BlurParameters{ |
| .blur_uv_offset = Point(pass1_pixel_size.x, 0.0), |
| .blur_sigma = scaled_sigma.x * effective_scalar.x, |
| .blur_radius = ScaleBlurRadius(blur_radius.x, effective_scalar.x), |
| .step_size = 1, |
| }, |
| pass3_destination, blur_uvs); |
| |
| if (!pass3_out.ok()) { |
| return std::nullopt; |
| } |
| |
| if (!renderer.GetContext() |
| ->GetCommandQueue() |
| ->Submit(/*buffers=*/{command_buffer}) |
| .ok()) { |
| return std::nullopt; |
| } |
| |
| // The ping-pong approach requires that each render pass output has the same |
| // size. |
| FML_DCHECK((pass1_out.value().GetRenderTargetSize() == |
| pass2_out.value().GetRenderTargetSize()) && |
| (pass2_out.value().GetRenderTargetSize() == |
| pass3_out.value().GetRenderTargetSize())); |
| |
| SamplerDescriptor sampler_desc = MakeSamplerDescriptor( |
| MinMagFilter::kLinear, SamplerAddressMode::kClampToEdge); |
| |
| Entity blur_output_entity = Entity::FromSnapshot( |
| Snapshot{.texture = pass3_out.value().GetRenderTargetTexture(), |
| .transform = input_snapshot->transform * |
| padding_snapshot_adjustment * |
| Matrix::MakeScale(1 / effective_scalar), |
| .sampler_descriptor = sampler_desc, |
| .opacity = input_snapshot->opacity}, |
| entity.GetBlendMode()); |
| |
| return ApplyBlurStyle(mask_blur_style_, entity, inputs[0], |
| input_snapshot.value(), std::move(blur_output_entity), |
| mask_geometry_); |
| } |
| |
| Scalar GaussianBlurFilterContents::CalculateBlurRadius(Scalar sigma) { |
| return static_cast<Radius>(Sigma(sigma)).radius; |
| } |
| |
| Quad GaussianBlurFilterContents::CalculateUVs( |
| const std::shared_ptr<FilterInput>& filter_input, |
| const Entity& entity, |
| const Rect& source_rect, |
| const ISize& texture_size) { |
| Matrix input_transform = filter_input->GetLocalTransform(entity); |
| Quad coverage_quad = source_rect.GetTransformedPoints(input_transform); |
| |
| Matrix uv_transform = Matrix::MakeScale( |
| {1.0f / texture_size.width, 1.0f / texture_size.height, 1.0f}); |
| return uv_transform.Transform(coverage_quad); |
| } |
| |
| // This function was calculated by observing Skia's behavior. Its blur at 500 |
| // seemed to be 0.15. Since we clamp at 500 I solved the quadratic equation |
| // that puts the minima there and a f(0)=1. |
| Scalar GaussianBlurFilterContents::ScaleSigma(Scalar sigma) { |
| // Limit the kernel size to 1000x1000 pixels, like Skia does. |
| Scalar clamped = std::min(sigma, 500.0f); |
| constexpr Scalar a = 3.4e-06; |
| constexpr Scalar b = -3.4e-3; |
| constexpr Scalar c = 1.f; |
| Scalar scalar = c + b * clamped + a * clamped * clamped; |
| return clamped * scalar; |
| } |
| |
| KernelPipeline::FragmentShader::KernelSamples GenerateBlurInfo( |
| BlurParameters parameters) { |
| KernelPipeline::FragmentShader::KernelSamples result; |
| result.sample_count = |
| ((2 * parameters.blur_radius) / parameters.step_size) + 1; |
| |
| // Chop off the last samples if the radius >= 3 where they account for < 1.56% |
| // of the result. |
| int x_offset = 0; |
| if (parameters.blur_radius >= 3) { |
| result.sample_count -= 2; |
| x_offset = 1; |
| } |
| |
| Scalar tally = 0.0f; |
| for (int i = 0; i < result.sample_count; ++i) { |
| int x = x_offset + (i * parameters.step_size) - parameters.blur_radius; |
| result.samples[i] = KernelPipeline::FragmentShader::KernelSample{ |
| .uv_offset = parameters.blur_uv_offset * x, |
| .coefficient = expf(-0.5f * (x * x) / |
| (parameters.blur_sigma * parameters.blur_sigma)) / |
| (sqrtf(2.0f * M_PI) * parameters.blur_sigma), |
| }; |
| tally += result.samples[i].coefficient; |
| } |
| |
| // Make sure everything adds up to 1. |
| for (auto& sample : result.samples) { |
| sample.coefficient /= tally; |
| } |
| |
| return result; |
| } |
| |
| // This works by shrinking the kernel size by 2 and relying on lerp to read |
| // between the samples. |
| KernelPipeline::FragmentShader::KernelSamples LerpHackKernelSamples( |
| KernelPipeline::FragmentShader::KernelSamples parameters) { |
| KernelPipeline::FragmentShader::KernelSamples result; |
| result.sample_count = ((parameters.sample_count - 1) / 2) + 1; |
| int32_t middle = result.sample_count / 2; |
| int32_t j = 0; |
| for (int i = 0; i < result.sample_count; i++) { |
| if (i == middle) { |
| result.samples[i] = parameters.samples[j++]; |
| } else { |
| KernelPipeline::FragmentShader::KernelSample left = parameters.samples[j]; |
| KernelPipeline::FragmentShader::KernelSample right = |
| parameters.samples[j + 1]; |
| result.samples[i] = KernelPipeline::FragmentShader::KernelSample{ |
| .uv_offset = (left.uv_offset * left.coefficient + |
| right.uv_offset * right.coefficient) / |
| (left.coefficient + right.coefficient), |
| .coefficient = left.coefficient + right.coefficient, |
| }; |
| j += 2; |
| } |
| } |
| |
| return result; |
| } |
| |
| } // namespace impeller |