[Impeller] Add filterQuality support to ImageFilter.shader (#188544)
Adds a `filterQuality` argument to `ImageFilter.shader` so the
implicitly bound image-filter input sampler can be configured instead of
always using nearest-neighbor sampling.
This threads the requested sampling quality through `dart:ui`,
DisplayList runtime-effect image filters, and Impeller’s runtime-effect
texture input setup. The default remains `FilterQuality.none` to
preserve existing behavior.
This also adds regression coverage with a fractional-texel shader to
verify that `BackdropFilter` + `ImageFilter.shader` honors
`FilterQuality`, plus equality coverage for shader image filters that
differ only by input sampling.
Fixes https://github.com/flutter/flutter/issues/188365
No changes were required in the `flutter/tests` repo.
## Pre-launch Checklist
- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [AI contribution guidelines] and understand my
responsibilities, or I am not using AI tools.
- [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].
If this change needs to override an active code freeze, provide a
comment explaining why. The code freeze workflow can be overridden by
code reviewers. See pinned issues for any active code freezes with
guidance.
**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
[AI contribution guidelines]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines
[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
---------
Co-authored-by: b-luk <97480502+b-luk@users.noreply.github.com>
diff --git a/engine/src/flutter/display_list/effects/dl_image_filter.cc b/engine/src/flutter/display_list/effects/dl_image_filter.cc
index 752baf6..1a66b76 100644
--- a/engine/src/flutter/display_list/effects/dl_image_filter.cc
+++ b/engine/src/flutter/display_list/effects/dl_image_filter.cc
@@ -34,9 +34,11 @@
std::shared_ptr<DlImageFilter> DlImageFilter::MakeRuntimeEffect(
sk_sp<DlRuntimeEffect> runtime_effect,
std::vector<std::shared_ptr<DlColorSource>> samplers,
- std::shared_ptr<std::vector<uint8_t>> uniform_data) {
+ std::shared_ptr<std::vector<uint8_t>> uniform_data,
+ DlImageSampling input_sampling) {
return DlRuntimeEffectImageFilter::Make(
- std::move(runtime_effect), std::move(samplers), std::move(uniform_data));
+ std::move(runtime_effect), std::move(samplers), std::move(uniform_data),
+ input_sampling);
}
std::shared_ptr<DlImageFilter> DlImageFilter::MakeColorFilter(
diff --git a/engine/src/flutter/display_list/effects/dl_image_filter.h b/engine/src/flutter/display_list/effects/dl_image_filter.h
index 911fe62..8104c54 100644
--- a/engine/src/flutter/display_list/effects/dl_image_filter.h
+++ b/engine/src/flutter/display_list/effects/dl_image_filter.h
@@ -67,7 +67,8 @@
static std::shared_ptr<DlImageFilter> MakeRuntimeEffect(
sk_sp<DlRuntimeEffect> runtime_effect,
std::vector<std::shared_ptr<DlColorSource>> samplers,
- std::shared_ptr<std::vector<uint8_t>> uniform_data);
+ std::shared_ptr<std::vector<uint8_t>> uniform_data,
+ DlImageSampling input_sampling = DlImageSampling::kNearestNeighbor);
static std::shared_ptr<DlImageFilter> MakeColorFilter(
const std::shared_ptr<const DlColorFilter>& filter);
diff --git a/engine/src/flutter/display_list/effects/dl_image_filter_unittests.cc b/engine/src/flutter/display_list/effects/dl_image_filter_unittests.cc
index aedbed3..de5c1da 100644
--- a/engine/src/flutter/display_list/effects/dl_image_filter_unittests.cc
+++ b/engine/src/flutter/display_list/effects/dl_image_filter_unittests.cc
@@ -907,6 +907,21 @@
EXPECT_NE(filter_a, filter_c);
}
+TEST(DisplayListImageFilter, RuntimeEffectEqualityWithInputSampling) {
+ DlRuntimeEffectImageFilter filter_a(nullptr, {nullptr},
+ std::make_shared<std::vector<uint8_t>>());
+ DlRuntimeEffectImageFilter filter_b(nullptr, {nullptr},
+ std::make_shared<std::vector<uint8_t>>());
+ DlRuntimeEffectImageFilter filter_c(nullptr, {nullptr},
+ std::make_shared<std::vector<uint8_t>>(),
+ DlImageSampling::kLinear);
+
+ EXPECT_EQ(filter_a.input_sampling(), DlImageSampling::kNearestNeighbor);
+ EXPECT_EQ(filter_c.input_sampling(), DlImageSampling::kLinear);
+ EXPECT_EQ(filter_a, filter_b);
+ EXPECT_NE(filter_a, filter_c);
+}
+
TEST(DisplayListImageFilter, RuntimeEffectEqualityWithSamplers) {
auto image_a =
DlColorSource::MakeImage(nullptr, DlTileMode::kClamp, DlTileMode::kDecal);
diff --git a/engine/src/flutter/display_list/effects/image_filters/dl_runtime_effect_image_filter.cc b/engine/src/flutter/display_list/effects/image_filters/dl_runtime_effect_image_filter.cc
index 9c6c285..d2c52f1 100644
--- a/engine/src/flutter/display_list/effects/image_filters/dl_runtime_effect_image_filter.cc
+++ b/engine/src/flutter/display_list/effects/image_filters/dl_runtime_effect_image_filter.cc
@@ -9,9 +9,11 @@
std::shared_ptr<DlImageFilter> DlRuntimeEffectImageFilter::Make(
sk_sp<DlRuntimeEffect> runtime_effect,
std::vector<std::shared_ptr<DlColorSource>> samplers,
- std::shared_ptr<std::vector<uint8_t>> uniform_data) {
+ std::shared_ptr<std::vector<uint8_t>> uniform_data,
+ DlImageSampling input_sampling) {
return std::make_shared<DlRuntimeEffectImageFilter>(
- std::move(runtime_effect), std::move(samplers), std::move(uniform_data));
+ std::move(runtime_effect), std::move(samplers), std::move(uniform_data),
+ input_sampling);
}
DlRect* DlRuntimeEffectImageFilter::map_local_bounds(
@@ -42,7 +44,8 @@
auto that = static_cast<const DlRuntimeEffectImageFilter*>(&other);
if (runtime_effect_ != that->runtime_effect_ ||
samplers_.size() != that->samplers().size() ||
- uniform_data_->size() != that->uniform_data()->size()) {
+ uniform_data_->size() != that->uniform_data()->size() ||
+ input_sampling_ != that->input_sampling()) {
return false;
}
for (auto i = 0u; i < samplers_.size(); i++) {
diff --git a/engine/src/flutter/display_list/effects/image_filters/dl_runtime_effect_image_filter.h b/engine/src/flutter/display_list/effects/image_filters/dl_runtime_effect_image_filter.h
index 391cfd9..ec72e58 100644
--- a/engine/src/flutter/display_list/effects/image_filters/dl_runtime_effect_image_filter.h
+++ b/engine/src/flutter/display_list/effects/image_filters/dl_runtime_effect_image_filter.h
@@ -17,20 +17,24 @@
explicit DlRuntimeEffectImageFilter(
sk_sp<DlRuntimeEffect> runtime_effect,
std::vector<std::shared_ptr<DlColorSource>> samplers,
- std::shared_ptr<std::vector<uint8_t>> uniform_data)
+ std::shared_ptr<std::vector<uint8_t>> uniform_data,
+ DlImageSampling input_sampling = DlImageSampling::kNearestNeighbor)
: runtime_effect_(std::move(runtime_effect)),
samplers_(std::move(samplers)),
- uniform_data_(std::move(uniform_data)) {}
+ uniform_data_(std::move(uniform_data)),
+ input_sampling_(input_sampling) {}
std::shared_ptr<DlImageFilter> shared() const override {
return std::make_shared<DlRuntimeEffectImageFilter>(
- this->runtime_effect_, this->samplers_, this->uniform_data_);
+ this->runtime_effect_, this->samplers_, this->uniform_data_,
+ this->input_sampling_);
}
static std::shared_ptr<DlImageFilter> Make(
sk_sp<DlRuntimeEffect> runtime_effect,
std::vector<std::shared_ptr<DlColorSource>> samplers,
- std::shared_ptr<std::vector<uint8_t>> uniform_data);
+ std::shared_ptr<std::vector<uint8_t>> uniform_data,
+ DlImageSampling input_sampling = DlImageSampling::kNearestNeighbor);
DlImageFilterType type() const override {
return DlImageFilterType::kRuntimeEffect;
@@ -66,6 +70,8 @@
return uniform_data_;
}
+ DlImageSampling input_sampling() const { return input_sampling_; }
+
protected:
bool equals_(const DlImageFilter& other) const override;
@@ -73,6 +79,7 @@
sk_sp<DlRuntimeEffect> runtime_effect_;
std::vector<std::shared_ptr<DlColorSource>> samplers_;
std::shared_ptr<std::vector<uint8_t>> uniform_data_;
+ DlImageSampling input_sampling_;
};
} // namespace flutter
diff --git a/engine/src/flutter/impeller/display_list/image_filter.cc b/engine/src/flutter/impeller/display_list/image_filter.cc
index e87964a..06981f4 100644
--- a/engine/src/flutter/impeller/display_list/image_filter.cc
+++ b/engine/src/flutter/impeller/display_list/image_filter.cc
@@ -117,13 +117,15 @@
runtime_filter->runtime_effect()->runtime_stage();
std::vector<RuntimeEffectContents::TextureInput> texture_inputs;
- size_t index = 0;
+ bool is_first = true;
for (const std::shared_ptr<flutter::DlColorSource>& sampler :
runtime_filter->samplers()) {
- if (index == 0 && sampler == nullptr) {
- // Insert placeholder for filter.
+ if (is_first) {
+ is_first = false;
+ // The first sampler is always the image filter input.
texture_inputs.push_back(
- {.sampler_descriptor = skia_conversions::ToSamplerDescriptor({}),
+ {.sampler_descriptor = skia_conversions::ToSamplerDescriptor(
+ runtime_filter->input_sampling()),
.texture = nullptr});
continue;
}
@@ -137,7 +139,6 @@
std::shared_ptr<impeller::Texture> texture =
image->image()->asImpellerImage()->GetCachedTexture(renderer);
FML_DCHECK(texture);
- index++;
texture_inputs.push_back({
.sampler_descriptor =
skia_conversions::ToSamplerDescriptor(image->sampling()),
diff --git a/engine/src/flutter/lib/ui/fixtures/shaders/general_shaders/BUILD.gn b/engine/src/flutter/lib/ui/fixtures/shaders/general_shaders/BUILD.gn
index 8b37a74..8b20c28 100644
--- a/engine/src/flutter/lib/ui/fixtures/shaders/general_shaders/BUILD.gn
+++ b/engine/src/flutter/lib/ui/fixtures/shaders/general_shaders/BUILD.gn
@@ -13,6 +13,7 @@
"circle_sdf.frag",
"double_sampler_swapped.frag",
"double_sampler.frag",
+ "filter_shader_fractional_texel.frag",
"filter_shader.frag",
"functions.frag",
"missing_size.frag",
diff --git a/engine/src/flutter/lib/ui/fixtures/shaders/general_shaders/filter_shader_fractional_texel.frag b/engine/src/flutter/lib/ui/fixtures/shaders/general_shaders/filter_shader_fractional_texel.frag
new file mode 100644
index 0000000..30a158d
--- /dev/null
+++ b/engine/src/flutter/lib/ui/fixtures/shaders/general_shaders/filter_shader_fractional_texel.frag
@@ -0,0 +1,15 @@
+// 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 <flutter/runtime_effect.glsl>
+
+uniform vec2 u_size;
+uniform sampler2D u_texture;
+
+out vec4 frag_color;
+
+void main() {
+ vec2 uv = (FlutterFragCoord().xy + vec2(0.25, 0.0)) / u_size;
+ frag_color = texture(u_texture, uv);
+}
diff --git a/engine/src/flutter/lib/ui/painting.dart b/engine/src/flutter/lib/ui/painting.dart
index 1268cba..e1cada2 100644
--- a/engine/src/flutter/lib/ui/painting.dart
+++ b/engine/src/flutter/lib/ui/painting.dart
@@ -4429,6 +4429,9 @@
/// also be at least one sampler2D uniform, the first of which will be set by
/// the engine to contain the filter input.
///
+ /// The optional [filterQuality] argument sets the quality level used to sample
+ /// the filter input. By default, it is set to [FilterQuality.none].
+ ///
/// When Impeller uses the OpenGL(ES) backend, the y-axis direction is
/// reversed. Custom fragment shaders must invert the y-axis on
/// GLES or they will render upside-down.
@@ -4458,7 +4461,10 @@
/// }
///
/// ```
- factory ImageFilter.shader(FragmentShader shader) {
+ factory ImageFilter.shader(
+ FragmentShader shader, {
+ FilterQuality filterQuality = FilterQuality.none,
+ }) {
if (!_impellerEnabled) {
throw UnsupportedError('ImageFilter.shader only supported with Impeller rendering engine.');
}
@@ -4477,7 +4483,7 @@
}
throw StateError(buffer.toString());
}
- return _FragmentShaderImageFilter(shader);
+ return _FragmentShaderImageFilter(shader, filterQuality);
}
/// Whether [ImageFilter.shader] is supported on the current backend.
@@ -4673,9 +4679,10 @@
}
class _FragmentShaderImageFilter implements ImageFilter {
- _FragmentShaderImageFilter(this.shader);
+ _FragmentShaderImageFilter(this.shader, this.filterQuality);
final FragmentShader shader;
+ final FilterQuality filterQuality;
late final _ImageFilter nativeFilter = _ImageFilter.shader(this);
@@ -4686,7 +4693,7 @@
String get debugShortDescription => 'shader';
@override
- String toString() => 'ImageFilter.shader(Shader#${shader.hashCode})';
+ String toString() => 'ImageFilter.shader(Shader#${shader.hashCode}, $filterQuality)';
@override
bool operator ==(Object other) {
@@ -4695,6 +4702,7 @@
}
return other is _FragmentShaderImageFilter &&
other.shader == shader &&
+ other.filterQuality == filterQuality &&
_equals(nativeFilter, other.nativeFilter);
}
@@ -4702,7 +4710,7 @@
external static bool _equals(_ImageFilter a, _ImageFilter b);
@override
- int get hashCode => shader.hashCode;
+ int get hashCode => Object.hash(shader, filterQuality);
}
/// An [ImageFilter] that is backed by a native DlImageFilter.
@@ -4770,7 +4778,7 @@
_ImageFilter.shader(_FragmentShaderImageFilter filter) : creator = filter {
_constructor();
- _initShader(filter.shader);
+ _initShader(filter.shader, filter.filterQuality.index);
}
@Native<Void Function(Handle)>(symbol: 'ImageFilter::Create')
@@ -4813,8 +4821,8 @@
)
external void _initComposed(_ImageFilter outerFilter, _ImageFilter innerFilter);
- @Native<Void Function(Pointer<Void>, Pointer<Void>)>(symbol: 'ImageFilter::initShader')
- external void _initShader(FragmentShader shader);
+ @Native<Void Function(Pointer<Void>, Pointer<Void>, Int32)>(symbol: 'ImageFilter::initShader')
+ external void _initShader(FragmentShader shader, int filterQuality);
/// The original Dart object that created the native wrapper, which retains
/// the values used for the filter.
diff --git a/engine/src/flutter/lib/ui/painting/fragment_program.cc b/engine/src/flutter/lib/ui/painting/fragment_program.cc
index 74f4a33..a9b8c45 100644
--- a/engine/src/flutter/lib/ui/painting/fragment_program.cc
+++ b/engine/src/flutter/lib/ui/painting/fragment_program.cc
@@ -271,9 +271,10 @@
std::shared_ptr<DlImageFilter> FragmentProgram::MakeDlImageFilter(
std::shared_ptr<std::vector<uint8_t>> float_uniforms,
- const std::vector<std::shared_ptr<DlColorSource>>& children) {
- return DlImageFilter::MakeRuntimeEffect(runtime_effect_, children,
- std::move(float_uniforms));
+ const std::vector<std::shared_ptr<DlColorSource>>& children,
+ DlImageSampling input_sampling) {
+ return DlImageFilter::MakeRuntimeEffect(
+ runtime_effect_, children, std::move(float_uniforms), input_sampling);
}
void FragmentProgram::Create(Dart_Handle wrapper) {
diff --git a/engine/src/flutter/lib/ui/painting/fragment_program.h b/engine/src/flutter/lib/ui/painting/fragment_program.h
index 65eb460..def8c47 100644
--- a/engine/src/flutter/lib/ui/painting/fragment_program.h
+++ b/engine/src/flutter/lib/ui/painting/fragment_program.h
@@ -41,7 +41,8 @@
std::shared_ptr<DlImageFilter> MakeDlImageFilter(
std::shared_ptr<std::vector<uint8_t>> float_uniforms,
- const std::vector<std::shared_ptr<DlColorSource>>& children);
+ const std::vector<std::shared_ptr<DlColorSource>>& children,
+ DlImageSampling input_sampling = DlImageSampling::kNearestNeighbor);
private:
FragmentProgram();
diff --git a/engine/src/flutter/lib/ui/painting/fragment_shader.cc b/engine/src/flutter/lib/ui/painting/fragment_shader.cc
index 7ce2f25..bf30884 100644
--- a/engine/src/flutter/lib/ui/painting/fragment_shader.cc
+++ b/engine/src/flutter/lib/ui/painting/fragment_shader.cc
@@ -94,7 +94,8 @@
uniform_floats[float_count_ + 2 * index + 1] = image->height();
}
-std::shared_ptr<DlImageFilter> ReusableFragmentShader::as_image_filter() const {
+std::shared_ptr<DlImageFilter> ReusableFragmentShader::as_image_filter(
+ DlImageSampling input_sampling) const {
FML_CHECK(program_);
// The lifetime of this object is longer than a frame, and the uniforms can be
@@ -104,7 +105,8 @@
uniform_data->resize(uniform_data_->size());
memcpy(uniform_data->data(), uniform_data_->bytes(), uniform_data->size());
- return program_->MakeDlImageFilter(std::move(uniform_data), samplers_);
+ return program_->MakeDlImageFilter(std::move(uniform_data), samplers_,
+ input_sampling);
}
std::shared_ptr<DlColorSource> ReusableFragmentShader::shader(
diff --git a/engine/src/flutter/lib/ui/painting/fragment_shader.h b/engine/src/flutter/lib/ui/painting/fragment_shader.h
index 2246dc5..60efb48 100644
--- a/engine/src/flutter/lib/ui/painting/fragment_shader.h
+++ b/engine/src/flutter/lib/ui/painting/fragment_shader.h
@@ -47,7 +47,8 @@
// |Shader|
std::shared_ptr<DlColorSource> shader(DlImageSampling) override;
- std::shared_ptr<DlImageFilter> as_image_filter() const;
+ std::shared_ptr<DlImageFilter> as_image_filter(
+ DlImageSampling input_sampling) const;
private:
ReusableFragmentShader(fml::RefPtr<FragmentProgram> program,
diff --git a/engine/src/flutter/lib/ui/painting/image_filter.cc b/engine/src/flutter/lib/ui/painting/image_filter.cc
index 41366bb..3fb5779 100644
--- a/engine/src/flutter/lib/ui/painting/image_filter.cc
+++ b/engine/src/flutter/lib/ui/painting/image_filter.cc
@@ -131,9 +131,10 @@
inner->filter(DlTileMode::kClamp));
}
-void ImageFilter::initShader(ReusableFragmentShader* shader) {
+void ImageFilter::initShader(ReusableFragmentShader* shader,
+ int filterQualityIndex) {
FML_DCHECK(shader);
- filter_ = shader->as_image_filter();
+ filter_ = shader->as_image_filter(SamplingFromIndex(filterQualityIndex));
}
bool ImageFilter::equals(Dart_Handle a_handle, Dart_Handle b_handle) {
diff --git a/engine/src/flutter/lib/ui/painting/image_filter.h b/engine/src/flutter/lib/ui/painting/image_filter.h
index 18a71c0..4744919 100644
--- a/engine/src/flutter/lib/ui/painting/image_filter.h
+++ b/engine/src/flutter/lib/ui/painting/image_filter.h
@@ -42,7 +42,7 @@
void initMatrix(const tonic::Float64List& matrix4, int filter_quality_index);
void initColorFilter(ColorFilter* colorFilter);
void initComposeFilter(ImageFilter* outer, ImageFilter* inner);
- void initShader(ReusableFragmentShader* shader);
+ void initShader(ReusableFragmentShader* shader, int filter_quality_index);
static bool equals(Dart_Handle a_handle, Dart_Handle b_handle);
const std::shared_ptr<DlImageFilter> filter(DlTileMode mode) const;
diff --git a/engine/src/flutter/lib/web_ui/lib/painting.dart b/engine/src/flutter/lib/web_ui/lib/painting.dart
index 73f133e..94ddea1 100644
--- a/engine/src/flutter/lib/web_ui/lib/painting.dart
+++ b/engine/src/flutter/lib/web_ui/lib/painting.dart
@@ -690,8 +690,12 @@
factory ImageFilter.compose({required ImageFilter outer, required ImageFilter inner}) =>
engine.renderer.composeImageFilters(outer: outer, inner: inner);
- // ignore: avoid_unused_constructor_parameters
- factory ImageFilter.shader(FragmentShader shader) {
+ factory ImageFilter.shader(
+ // ignore: avoid_unused_constructor_parameters
+ FragmentShader shader, {
+ // ignore: avoid_unused_constructor_parameters
+ FilterQuality filterQuality = FilterQuality.none,
+ }) {
throw UnsupportedError('ImageFilter.shader only supported with Impeller rendering engine.');
}
diff --git a/engine/src/flutter/testing/dart/fragment_shader_test.dart b/engine/src/flutter/testing/dart/fragment_shader_test.dart
index e130b65..79edd1b 100644
--- a/engine/src/flutter/testing/dart/fragment_shader_test.dart
+++ b/engine/src/flutter/testing/dart/fragment_shader_test.dart
@@ -1630,6 +1630,11 @@
expect(filter, filter_2);
expect(identical(filter, filter_2), false);
+ final filterLowQuality = ImageFilter.shader(shader, filterQuality: FilterQuality.low);
+ expect(filter, isNot(filterLowQuality));
+ expect(filterLowQuality, ImageFilter.shader(shader, filterQuality: FilterQuality.low));
+ expect(identical(filter, filterLowQuality), false);
+
shader.setFloat(0, 1);
final filter_3 = ImageFilter.shader(shader);
diff --git a/engine/src/flutter/testing/dart/painting_test.dart b/engine/src/flutter/testing/dart/painting_test.dart
index dc83c2b..d8a6b2b 100644
--- a/engine/src/flutter/testing/dart/painting_test.dart
+++ b/engine/src/flutter/testing/dart/painting_test.dart
@@ -9,6 +9,7 @@
import 'package:vector_math/vector_math_64.dart';
import 'goldens.dart';
+import 'impeller_enabled.dart';
typedef CanvasCallback = void Function(Canvas canvas);
@@ -94,16 +95,9 @@
test('BackdropFilter with multiple clips', () async {
// Regression test for https://github.com/flutter/flutter/issues/144211
- Picture makePicture(CanvasCallback callback) {
- final recorder = PictureRecorder();
- final canvas = Canvas(recorder);
- callback(canvas);
- return recorder.endRecording();
- }
-
final sceneBuilder = SceneBuilder();
- final Picture redClippedPicture = makePicture((Canvas canvas) {
+ final Picture redClippedPicture = _makePicture((Canvas canvas) {
canvas.drawPaint(Paint()..color = const Color(0xFFFFFFFF));
canvas.clipRect(const Rect.fromLTRB(10, 10, 200, 200));
canvas.clipRect(const Rect.fromLTRB(11, 10, 300, 200));
@@ -114,7 +108,7 @@
final matrix = Float64List(16);
sceneBuilder.pushBackdropFilter(ImageFilter.matrix(matrix));
- final Picture whitePicture = makePicture((Canvas canvas) {
+ final Picture whitePicture = _makePicture((Canvas canvas) {
canvas.drawPaint(Paint()..color = const Color(0xFFFFFFFF));
});
sceneBuilder.addPicture(Offset.zero, whitePicture);
@@ -135,59 +129,34 @@
redClippedPicture.dispose();
});
- Image backdropBlurWithTileMode(TileMode? tileMode) {
- Picture makePicture(CanvasCallback callback) {
- final recorder = PictureRecorder();
- final canvas = Canvas(recorder);
- callback(canvas);
- return recorder.endRecording();
+ test('BackdropFilter with ImageFilter.shader honors FilterQuality', () async {
+ // Regression test for https://github.com/flutter/flutter/issues/188365.
+ if (!impellerEnabled) {
+ print('Skipped for Skia.');
+ return;
}
- const double rectSize = 10;
- const count = 50;
- const double imgSize = rectSize * count;
+ // The helper draws a black/white striped backdrop and filters it with a
+ // shader that samples the backdrop at a fractional texel coordinate. Because
+ // the stripes are grayscale, checking a single color channel is enough to
+ // tell whether the sample came from a source texel or was interpolated.
+ final Image nearest = await _backdropShaderWithFilterQuality(FilterQuality.none);
+ final Image linear = await _backdropShaderWithFilterQuality(FilterQuality.low);
- final Picture blueGreenGridPicture = makePicture((Canvas canvas) {
- const white = Color(0xFFFFFFFF);
- const purple = Color(0xFFFF00FF);
- const blue = Color(0xFF0000FF);
- const green = Color(0xFF00FF00);
- const yellow = Color(0xFFFFFF00);
- const red = Color(0xFFFF0000);
- canvas.drawColor(white, BlendMode.src);
- for (var i = 0; i < count; i++) {
- for (var j = 0; j < count; j++) {
- final rectOdd = (i + j) & 1 == 0;
- final fg = (i < count / 2)
- ? ((j < count / 2) ? green : blue)
- : ((j < count / 2) ? yellow : red);
- canvas.drawRect(
- Rect.fromLTWH(i * rectSize, j * rectSize, rectSize, rectSize),
- Paint()..color = rectOdd ? fg : white,
- );
- }
- }
- canvas.drawRect(const Rect.fromLTWH(0, 0, imgSize, 1), Paint()..color = purple);
- canvas.drawRect(const Rect.fromLTWH(0, 0, 1, imgSize), Paint()..color = purple);
- canvas.drawRect(const Rect.fromLTWH(0, imgSize - 1, imgSize, 1), Paint()..color = purple);
- canvas.drawRect(const Rect.fromLTWH(imgSize - 1, 0, 1, imgSize), Paint()..color = purple);
- });
+ // Nearest-neighbor sampling should pick either black or white exactly.
+ // Linear sampling should blend the adjacent black and white stripes.
+ final int nearestSample = await _redAt(nearest, 0, 1);
+ final int linearSample = await _redAt(linear, 0, 1);
- final sceneBuilder = SceneBuilder();
- sceneBuilder.addPicture(Offset.zero, blueGreenGridPicture);
- sceneBuilder.pushBackdropFilter(ImageFilter.blur(sigmaX: 20, sigmaY: 20, tileMode: tileMode));
+ expect(nearestSample, anyOf(0, 255));
+ expect(linearSample, allOf(greaterThan(0), lessThan(255)));
- final Scene scene = sceneBuilder.build();
- final Image image = scene.toImageSync(imgSize.round(), imgSize.round());
-
- scene.dispose();
- blueGreenGridPicture.dispose();
-
- return image;
- }
+ nearest.dispose();
+ linear.dispose();
+ });
test('BackdropFilter with Blur honors TileMode.decal', () async {
- final Image image = backdropBlurWithTileMode(TileMode.decal);
+ final Image image = _backdropBlurWithTileMode(TileMode.decal);
final ImageComparer comparer = await ImageComparer.create();
await comparer.addGoldenImage(image, 'dart_ui_backdrop_filter_blur_decal_tile_mode.png');
@@ -196,7 +165,7 @@
});
test('BackdropFilter with Blur honors TileMode.clamp', () async {
- final Image image = backdropBlurWithTileMode(TileMode.clamp);
+ final Image image = _backdropBlurWithTileMode(TileMode.clamp);
final ImageComparer comparer = await ImageComparer.create();
await comparer.addGoldenImage(image, 'dart_ui_backdrop_filter_blur_clamp_tile_mode.png');
@@ -205,7 +174,7 @@
});
test('BackdropFilter with Blur honors TileMode.mirror', () async {
- final Image image = backdropBlurWithTileMode(TileMode.mirror);
+ final Image image = _backdropBlurWithTileMode(TileMode.mirror);
final ImageComparer comparer = await ImageComparer.create();
await comparer.addGoldenImage(image, 'dart_ui_backdrop_filter_blur_mirror_tile_mode.png');
@@ -214,7 +183,7 @@
});
test('BackdropFilter with Blur honors TileMode.repeated', () async {
- final Image image = backdropBlurWithTileMode(TileMode.repeated);
+ final Image image = _backdropBlurWithTileMode(TileMode.repeated);
final ImageComparer comparer = await ImageComparer.create();
await comparer.addGoldenImage(image, 'dart_ui_backdrop_filter_blur_repeated_tile_mode.png');
@@ -223,7 +192,7 @@
});
test('BackdropFilter with Blur default TileMode acts as TileMode.mirror', () async {
- final Image image = backdropBlurWithTileMode(null);
+ final Image image = _backdropBlurWithTileMode(null);
final ImageComparer comparer = await ImageComparer.create();
// It would be nice to compare the output here to the "mirror" golden
@@ -278,3 +247,102 @@
}
});
}
+
+Picture _makePicture(CanvasCallback callback) {
+ final recorder = PictureRecorder();
+ final canvas = Canvas(recorder);
+ callback(canvas);
+ return recorder.endRecording();
+}
+
+Image _backdropBlurWithTileMode(TileMode? tileMode) {
+ const double rectSize = 10;
+ const count = 50;
+ const double imgSize = rectSize * count;
+
+ final Picture blueGreenGridPicture = _makePicture((Canvas canvas) {
+ const white = Color(0xFFFFFFFF);
+ const purple = Color(0xFFFF00FF);
+ const blue = Color(0xFF0000FF);
+ const green = Color(0xFF00FF00);
+ const yellow = Color(0xFFFFFF00);
+ const red = Color(0xFFFF0000);
+ canvas.drawColor(white, BlendMode.src);
+ for (var i = 0; i < count; i++) {
+ for (var j = 0; j < count; j++) {
+ final rectOdd = (i + j) & 1 == 0;
+ final fg = (i < count / 2)
+ ? ((j < count / 2) ? green : blue)
+ : ((j < count / 2) ? yellow : red);
+ canvas.drawRect(
+ Rect.fromLTWH(i * rectSize, j * rectSize, rectSize, rectSize),
+ Paint()..color = rectOdd ? fg : white,
+ );
+ }
+ }
+ canvas.drawRect(const Rect.fromLTWH(0, 0, imgSize, 1), Paint()..color = purple);
+ canvas.drawRect(const Rect.fromLTWH(0, 0, 1, imgSize), Paint()..color = purple);
+ canvas.drawRect(const Rect.fromLTWH(0, imgSize - 1, imgSize, 1), Paint()..color = purple);
+ canvas.drawRect(const Rect.fromLTWH(imgSize - 1, 0, 1, imgSize), Paint()..color = purple);
+ });
+
+ final sceneBuilder = SceneBuilder();
+ sceneBuilder.addPicture(Offset.zero, blueGreenGridPicture);
+ sceneBuilder.pushBackdropFilter(ImageFilter.blur(sigmaX: 20, sigmaY: 20, tileMode: tileMode));
+
+ final Scene scene = sceneBuilder.build();
+ final Image image = scene.toImageSync(imgSize.round(), imgSize.round());
+
+ scene.dispose();
+ blueGreenGridPicture.dispose();
+
+ return image;
+}
+
+Future<Image> _backdropShaderWithFilterQuality(FilterQuality filterQuality) async {
+ const width = 16;
+ const height = 4;
+ const stripeWidth = 1.0;
+
+ final FragmentProgram program = await FragmentProgram.fromAsset(
+ 'filter_shader_fractional_texel.frag.iplr',
+ );
+ final FragmentShader shader = program.fragmentShader();
+
+ final Picture stripePicture = _makePicture((Canvas canvas) {
+ for (var x = 0; x < width; x++) {
+ canvas.drawRect(
+ Rect.fromLTWH(x * stripeWidth, 0, stripeWidth, height.toDouble()),
+ Paint()..color = x.isEven ? const Color(0xFF000000) : const Color(0xFFFFFFFF),
+ );
+ }
+ });
+
+ final Picture transparentPicture = _makePicture((Canvas canvas) {
+ canvas.drawRect(
+ Rect.fromLTWH(0, 0, width.toDouble(), height.toDouble()),
+ Paint()..color = const Color(0x00000000),
+ );
+ });
+
+ final sceneBuilder = SceneBuilder();
+ sceneBuilder.addPicture(Offset.zero, stripePicture);
+ sceneBuilder.pushBackdropFilter(ImageFilter.shader(shader, filterQuality: filterQuality));
+ sceneBuilder.addPicture(Offset.zero, transparentPicture);
+ sceneBuilder.pop();
+
+ final Scene scene = sceneBuilder.build();
+ final Image image = scene.toImageSync(width, height);
+
+ scene.dispose();
+ stripePicture.dispose();
+ transparentPicture.dispose();
+ shader.dispose();
+
+ return image;
+}
+
+Future<int> _redAt(Image image, int x, int y) async {
+ final ByteData data = (await image.toByteData())!;
+ return data.getUint8((y * image.width + x) * 4);
+}