Skip to content

[CP-stable]Add buffer around rerasterized input to fragment shaders to maintain coordinate space when clipped#182512

Merged
auto-submit[bot] merged 1 commit intoflutter:flutter-3.41-candidate.0from
flutteractionsbot:cp-stable-5f3a29abacb5e97905c8ad1ec308e2c882dc808a
Feb 24, 2026
Merged

[CP-stable]Add buffer around rerasterized input to fragment shaders to maintain coordinate space when clipped#182512
auto-submit[bot] merged 1 commit intoflutter:flutter-3.41-candidate.0from
flutteractionsbot:cp-stable-5f3a29abacb5e97905c8ad1ec308e2c882dc808a

Conversation

@flutteractionsbot
Copy link

@flutteractionsbot flutteractionsbot commented Feb 17, 2026

This pull request is created by automatic cherry pick workflow
Please fill in the form below, and a flutter domain expert will evaluate this cherry pick request.

Issue Link:

What is the link to the issue this cherry-pick is addressing?

#181660

Impact Description:

Without this PR certain combinations of blur and custom fragment shaders don't render correctly. This is an impediment some researchers are doing for advanced fragment shaders like https://pub.dev/packages/liquid_glass_renderer

Changelog Description:

  • This PR makes sure all combinations of clip+blur+fragment shader behave in a consistent manner with respect to the coordinate system the fragment shader is calculated in.
  • Before this change adding a clip rect could change the coordinate space of fragment shaders
  • All impeller platforms are impacted.

flutter/181660 When using clip rects with ImageFilter.combine(fragment_shader, blur) on Impeller, the coordinate space of the fragment shader would fit the clipped region, not the region being clipped.

Workaround:

Is there a workaround for this issue?

No.

Risk:

What is the risk level of this cherry-pick?

The changed code happens inside a deep branch of logic that few code executes today.

  • Low
  • Medium
  • High

Test Coverage:

Are you confident that your fix is well-tested by automated tests?

  • Yes
  • No

Validation Steps:

What are the steps to validate that this fix works?

There is a reproduction in #181660

…coordinate space when clipped (flutter#181743)

fixes flutter#181660

This works by changing the rerasterization logic such that we will
rerasterize the output from the blur filter to an image that matches the
dimensions of the input. The undoes the optimization of the clip rect
but standardizes the input to the fragment shader so that it is behaving
as if the input was the same as a backdrop filter without the blur
before hand.

I think there may be opportunities in the future to find a way to make
this more efficient if we could find some way to rerasterize only to the
clipped region but tweak the coordinates in the fragment shader to make
it act like it has a whole image.

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

**Note**: The Flutter team is currently trialing the use of [Gemini Code
Assist for
GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code).
Comments from the `gemini-code-assist` bot should not be taken as
authoritative feedback from the Flutter team. If you find its comments
useful you can update your code accordingly, but if you are unsure or
disagree with the feedback, please feel free to wait for a Flutter team
member's review for guidance on which automated comments should be
addressed.

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
@flutteractionsbot flutteractionsbot added the cp: review Cherry-picks in the review queue label Feb 17, 2026
@flutteractionsbot
Copy link
Author

@gaaclarke please fill out the PR description above, afterwards the release team will review this request.

@flutter-dashboard
Copy link

This pull request was opened from and to a release candidate branch. This should only be done as part of the official Flutter release process. If you are attempting to make a regular contribution to the Flutter project, please close this PR and follow the instructions at Tree Hygiene for detailed instructions on contributing to Flutter.

Reviewers: Use caution before merging pull requests to release branches. Ensure the proper procedure has been followed.

@github-actions github-actions bot added engine flutter/engine related. See also e: labels. e: impeller Impeller rendering backend issues and features requests labels Feb 17, 2026
Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request addresses an issue with clipped runtime effects by adding a buffer around rerasterized inputs to fragment shaders. The core change is in RuntimeEffectFilterContents, where an AnonymousContents is used to adjust the coverage of the input snapshot, effectively re-padding it to maintain the correct coordinate space. The corresponding fragment shader runtime_stage_filter_circle.frag is updated to take the origin as a uniform instead of a hardcoded value. Several tests in aiks_dl_runtime_effect_unittests.cc are updated to use this new uniform, and a new interactive test is added to validate the behavior with clipped backdrop filters.

Comment on lines +331 to +337
struct FragUniforms {
Vector2 size;
Vector2 origin;
} frag_uniforms = {.size = Vector2(1, 1), .origin = Vector2(30.f, 30.f)};
auto uniform_data = std::make_shared<std::vector<uint8_t>>();
uniform_data->resize(sizeof(Vector2));
uniform_data->resize(sizeof(FragUniforms));
memcpy(uniform_data->data(), &frag_uniforms, sizeof(FragUniforms));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The FragUniforms struct and the logic to create uniform_data are duplicated in this test, ComposeBackdropRuntimeOuterBlurInnerSmallSigma, and ClippedComposeBackdropRuntimeOuterBlurInnerSmallSigma.

To improve maintainability and reduce code duplication, consider defining the FragUniforms struct once in a shared scope (like an anonymous namespace at the top of the file) and creating a helper function to generate the uniform_data.

For example:

namespace {
struct FragUniforms {
  Vector2 size;
  Vector2 origin;
};

std::shared_ptr<std::vector<uint8_t>> CreateUniformData(
    const FragUniforms& uniforms) {
  auto data = std::make_shared<std::vector<uint8_t>>(sizeof(FragUniforms));
  memcpy(data->data(), &uniforms, sizeof(FragUniforms));
  return data;
}
}  // namespace

// Then in your test:
auto uniform_data = CreateUniformData({.size = Vector2(1, 1), .origin = Vector2(30.f, 30.f)});

#include "impeller/entity/contents/anonymous_contents.h"
#include "impeller/entity/contents/runtime_effect_contents.h"
#include "impeller/entity/contents/texture_contents.h"
#include "impeller/geometry/point.h"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This file uses Vector2, which is defined in impeller/geometry/vector.h. While <impeller/geometry/point.h> might include it transitively, it's best practice to include the headers for all symbols you use directly. This aligns with the Google C++ Style Guide, which this repository follows.

Please add #include "impeller/geometry/vector.h" for clarity and to prevent potential build breaks if transitive includes change.

References
  1. The repository's style guide specifies adherence to the Google C++ Style Guide. The Google C++ Style Guide's 'Include What You Use' principle recommends directly including headers for all symbols used. (link)

Comment on lines +101 to +112
// The LT values come from the offset of the clip rect, that creates
// the clipping effect on the content that will be rendered from
// the fragment shader. The RB values define the region we'll be
// synthesizing and ultimately defines the width and the height of
// the rasterized image. The LT values can be thought of shifting
// the window that will be rasterized. Since we are shifting from
// the top-left corner, that is effectively pushing the the bottom
// right corner lower, outside of the rendering space. So, we can
// clamp those values to the coverage's RB values. This doesn't
// cause the fragment shader's rendering to deform because the
// magic width/height values sent to the fragment shader don't take
// the rasterized image's size into account.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This comment is helpful for understanding the 'why' behind this change, but this part is a bit confusing:

So, we can clamp those values to the coverage's RB values.

This suggests that clamping is being done, but the code below doesn't perform any clamping. If clamping is not necessary (as the rest of the comment seems to imply), it might be clearer to rephrase this to avoid confusion for future readers. For example, you could clarify why clamping is considered but ultimately not needed.

@gaaclarke gaaclarke marked this pull request as draft February 17, 2026 23:56
@gaaclarke gaaclarke marked this pull request as ready for review February 19, 2026 01:00
Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request addresses an issue where the coordinate space for fragment shaders could be altered when combined with clipping and blurring, leading to incorrect rendering. The fix involves re-rasterizing the input with an offset to preserve the coordinate space, and new tests are added to validate this behavior. My feedback focuses on improving code readability and maintainability by reducing duplication in the new tests and clarifying a complex comment in the implementation.

Comment on lines 335 to +337
auto uniform_data = std::make_shared<std::vector<uint8_t>>();
uniform_data->resize(sizeof(Vector2));
uniform_data->resize(sizeof(FragUniforms));
memcpy(uniform_data->data(), &frag_uniforms, sizeof(FragUniforms));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The FragUniforms struct and the logic to populate uniform_data are duplicated across several tests. To improve maintainability and adhere to the 'Optimize for readability' principle, consider defining the struct once and creating a helper function to generate the uniform data.

Additionally, the creation of uniform_data can be simplified to a single line, avoiding separate resize and memcpy calls.

Suggested change
auto uniform_data = std::make_shared<std::vector<uint8_t>>();
uniform_data->resize(sizeof(Vector2));
uniform_data->resize(sizeof(FragUniforms));
memcpy(uniform_data->data(), &frag_uniforms, sizeof(FragUniforms));
auto uniform_data = std::make_shared<std::vector<uint8_t>>(
reinterpret_cast<const uint8_t*>(&frag_uniforms),
reinterpret_cast<const uint8_t*>(&frag_uniforms) + sizeof(frag_uniforms));
References
  1. Code is read more often than it is written, so it should be optimized for readability. Reducing duplication and using more concise standard library features improves readability and maintainability. (link)

Comment on lines +90 to +112
// Use an AnonymousContents to restore the padding around the input that
// may have been cut out with a clip rect to maintain the correct
// coordinates for the fragment shader to perform.
auto anonymous_contents = AnonymousContents::Make(
[&texture_contents](const ContentContext& renderer,
const Entity& entity, RenderPass& pass) -> bool {
return texture_contents.Render(renderer, entity, pass);
},
[maybe_input_coverage,
entity_offset](const Entity& entity) -> std::optional<Rect> {
Rect coverage = maybe_input_coverage.value();
// The LT values come from the offset of the clip rect, that creates
// the clipping effect on the content that will be rendered from
// the fragment shader. The RB values define the region we'll be
// synthesizing and ultimately defines the width and the height of
// the rasterized image. The LT values can be thought of shifting
// the window that will be rasterized. Since we are shifting from
// the top-left corner, that is effectively pushing the the bottom
// right corner lower, outside of the rendering space. So, we can
// clamp those values to the coverage's RB values. This doesn't
// cause the fragment shader's rendering to deform because the
// magic width/height values sent to the fragment shader don't take
// the rasterized image's size into account.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The comments explaining this logic are a bit verbose, and the long explanation within the lambda can be confusing, especially the part about clamping which isn't reflected in the code. This goes against the 'Optimize for readability' and 'Documentation should be useful' guidelines.

To improve clarity, consider replacing these comments with a single, more concise explanation of the 'why' and 'how' before the AnonymousContents is created.

Suggested change
// Use an AnonymousContents to restore the padding around the input that
// may have been cut out with a clip rect to maintain the correct
// coordinates for the fragment shader to perform.
auto anonymous_contents = AnonymousContents::Make(
[&texture_contents](const ContentContext& renderer,
const Entity& entity, RenderPass& pass) -> bool {
return texture_contents.Render(renderer, entity, pass);
},
[maybe_input_coverage,
entity_offset](const Entity& entity) -> std::optional<Rect> {
Rect coverage = maybe_input_coverage.value();
// The LT values come from the offset of the clip rect, that creates
// the clipping effect on the content that will be rendered from
// the fragment shader. The RB values define the region we'll be
// synthesizing and ultimately defines the width and the height of
// the rasterized image. The LT values can be thought of shifting
// the window that will be rasterized. Since we are shifting from
// the top-left corner, that is effectively pushing the the bottom
// right corner lower, outside of the rendering space. So, we can
// clamp those values to the coverage's RB values. This doesn't
// cause the fragment shader's rendering to deform because the
// magic width/height values sent to the fragment shader don't take
// the rasterized image's size into account.
// When a runtime effect is applied after a clip, the input texture is
// clipped to the filter's coverage. This can alter the coordinate space
// that the fragment shader runs in, leading to incorrect rendering.
//
// To fix this, we re-rasterize the input into a new texture that is
// offset to match the clip's origin by wrapping it in an
// AnonymousContents with adjusted coverage. This preserves the global
// coordinate space for the fragment shader. The new coverage is defined
// by the clip offset (entity_offset) as the top-left, and the original
// input coverage's bottom-right.
auto anonymous_contents = AnonymousContents::Make(
[&texture_contents](const ContentContext& renderer,
const Entity& entity, RenderPass& pass) -> bool {
return texture_contents.Render(renderer, entity, pass);
},
[maybe_input_coverage,
entity_offset](const Entity& entity) -> std::optional<Rect> {
Rect coverage = maybe_input_coverage.value();
References
  1. Code is read more often than it is written. The documentation should be clear and concise to aid understanding. (link)
  2. Documentation should be useful and explain the why and the how of the code. Confusing or inaccurate comments detract from this. (link)

@reidbaker
Copy link
Contributor

@gaaclarke does the changelog entry flutter/181660 When using clip rects with ImageFilter.combine(fragment_shader, blur) on Impeller, the coordinate space of the fragment shader would fit the clipped region, not the region being clipped. apply to all platforms or a subset?

@flar can you give this a review and let me know why it is safe to land this without a test?

@gaaclarke
Copy link
Member

@gaaclarke does the changelog entry flutter/181660 When using clip rects with ImageFilter.combine(fragment_shader, blur) on Impeller, the coordinate space of the fragment shader would fit the clipped region, not the region being clipped. apply to all platforms or a subset?

It applies to all platforms using Impeller, yes.

@flar can you give this a review and let me know why it is safe to land this without a test?

@reidbaker , this cherry-pick includes a test.

@reidbaker
Copy link
Contributor

reidbaker commented Feb 23, 2026

@gaaclarke you are correct. I apologize. I scanned the folders for test and did not see the file with a test suffix.

@flar
Copy link
Contributor

flar commented Feb 23, 2026

@flar can you give this a review and let me know why it is safe to land this without a test?

@reidbaker , this cherry-pick includes a test.

And the unit test is something I believe I reviewed before so good on all (test-related) fronts.

@gaaclarke
Copy link
Member

@flar for process's sake I think @reidbaker is looking for someone other than the person the filed the cherry-pick to approve it. I know you reviewed it but you are verifying that what I said in the cherry-pick request is true and that there is no risk that would make it appropriate for main but not stable.

Copy link
Contributor

@flar flar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RSLGTM

@reidbaker reidbaker added the autosubmit Merge PR when tree becomes green via auto submit App label Feb 24, 2026
@auto-submit auto-submit bot merged commit 96900aa into flutter:flutter-3.41-candidate.0 Feb 24, 2026
162 checks passed
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Mar 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

autosubmit Merge PR when tree becomes green via auto submit App cp: review Cherry-picks in the review queue e: impeller Impeller rendering backend issues and features requests engine flutter/engine related. See also e: labels.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants