Skip to content

[Impeller] Sample the render target rect in offscreen advanced blends - #192988

Merged
auto-submit[bot] merged 11 commits into
flutter:masterfrom
tguerin:impeller_blend_pixel_align
Sep 30, 2026
Merged

auto-submit[bot] merged 11 commits into
flutter:masterfrom
tguerin:impeller_blend_pixel_align

Conversation

@tguerin

@tguerin tguerin commented Sep 18, 2026 •

Copy link
Copy Markdown
Contributor

Problem

In the offscreen advanced blend path, the subpass render target is allocated with an integral size — ISize(subpass_coverage.GetSize()), a truncating conversion — and the quad spans that render target. But the texture coordinates are derived from the full, fractional coverage. The quad therefore displays a slightly larger region than it covers, scaling the contents by trunc(w)/w.

That factor changes whenever the coverage crosses a pixel boundary, so animated content is resampled differently every frame, which reads as shaking and crawling. The coverage does not need to be meaningfully fractional: in the per-draw path the blend runs over the whole pass (FlipBackdrop), and floating-point residue in the transform chain is enough — instrumenting master while running the Rive artboard below, the extent truncated on 29% of blend subpasses and the origin was fractional on 34%.

Only configurations without framebuffer fetch take this path — the iOS simulator, PowerVR, and Adreno on GLES. Devices with framebuffer fetch blend in-pass via ApplyFramebufferBlend, which is exact.

Fix

Derive the texture coordinates from the rect the render target actually represents, so the quad and the sampled region agree exactly. The render target size, the composite origin and the composited rect are all unchanged from master; PipelineBlend draws each input at its own size and transform and never had this mismatch, so it is not touched.

Verification

Measured on an iPhone 17 Pro simulator against a Rive artboard exercising Multiply, Screen, Overlay, Soft Light and Color Dodge, loaded with Factory.flutter so drawing goes through Flutter's canvas and into Impeller's blend paths. The landmark is the left edge of static artboard content that never moves in the animation, so every pixel of movement is the bug. 40 consecutive frames:

landmark master this PR
row 1 — position range 0.237 px 0.000 px
row 3 — position range 0.655 px 0.000 px
row 3 — frames moving >0.25 px 10 / 39 0 / 39
lib/main.dart
import 'package:flutter/material.dart';
import 'package:rive/rive.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await RiveNative.init();
  runApp(const MaterialApp(home: Scaffold(body: BlendModeTest())));
}

class BlendModeTest extends StatefulWidget {
  const BlendModeTest({super.key});

  @override
  State<BlendModeTest> createState() => _BlendModeTestState();
}

class _BlendModeTestState extends State<BlendModeTest> {
  File? _file;
  RiveWidgetController? _controller;

  @override
  void initState() {
    super.initState();
    _load();
  }

  Future<void> _load() async {
    // Factory.flutter routes drawing through Flutter's Canvas, so the artboard
    // goes through Impeller's blend paths. Factory.rive would use Rive's own
    // renderer and would not exercise this code at all.
    final file = await File.asset(
      'assets/blend_mode_test.riv',
      riveFactory: Factory.flutter,
    );
    if (file == null) {
      return;
    }
    setState(() {
      _file = file;
      _controller = RiveWidgetController(file);
    });
  }

  @override
  void dispose() {
    _controller?.dispose();
    _file?.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final controller = _controller;
    if (controller == null) {
      return const Center(child: CircularProgressIndicator());
    }
    return Container(
      color: const Color(0xFF9E9E9E),
      child: RiveWidget(controller: controller, fit: Fit.contain),
    );
  }
}
before after
Simulator.Screen.Recording.-.iPhone.17.Pro.-.2026-09-18.at.14.39.40.mov
Simulator.Screen.Recording.-.iPhone.17.Pro.-.2026-09-18.at.14.38.35.mov

(The recordings were made with an earlier revision of this change; the measured result for the final revision is identical.)

Golden change

This changes exactly one engine golden (of 466 Metal goldens compared locally): ColorFilterAdvancedBlendNoFbFetch. It is a correction, not a regression. That test stacks thirty full-frame blends, so the per-frame resampling error accumulates into a visible smear of every cell edge. Cell 2's image begins at x=400 (400 px pitch, 320 px images); row y=220:

x → 396 398 400 402 404 406
same test through framebuffer fetch (exact) black black (230,128,0) (230,128,0) (230,128,0) (230,128,0)
current golden (255,116,0) (255,137,0) (229,128,0) (229,128,0) (221,125,5) (199,121,20)
this PR black black (229,128,0) (229,128,0) (229,128,0) (229,128,0)

The current golden bleeds orange four pixels into the empty gap before the cell and blurs the edge over ~7 px; this PR matches the exact in-pass rendering to within 1/255. The golden will need triage.

Tests

DlGoldenTest.StackedOffscreenAdvancedBlendsDoNotResample renders a hard backdrop edge and blends over it thirty times with kLighten on a context without framebuffer fetch, then requires the result to match a single blend. Lighten is idempotent, so the only thing that can differ is resampling error accumulated across the stacked subpasses. A 0.4x canvas scale makes the full-pass coverage fractional the same way the golden does. Fails on master (RMSE 5.75), passes with this change (RMSE 0).

impeller_unittests *Blend*/*Filter*: 791 passing, 0 failing. The ColorFilterAdvancedBlendNoFbFetch golden exercises the same path with thirty stacked blends and visibly captures the defect.

Fixes #192980

Pre-launch Checklist

  • I read the Contributor Guide and followed the process outlined there for submitting PRs.
  • I read the AI contribution guidelines and understand my responsibilities, or I am not using AI tools.
  • I read the Tree Hygiene wiki page, which explains my responsibilities.
  • I read and followed the Flutter Style Guide, including [Features we expect every widget to implement].
  • I signed the CLA.
  • I listed at least one issue that this PR fixes in the description above.
  • I updated/added relevant in-code documentation (doc comments with ///).
  • If this PR introduces a new feature or capability, I created and linked a website documentation issue or PR in [flutter/website] (or verified none is needed).
  • I added new tests to check the change I am making, or this PR is test-exempt.
  • I followed the breaking change policy and added Data Driven Fixes where supported.
  • All existing and new tests are passing.

@github-actions github-actions Bot added engine flutter/engine related. See also e: labels. e: impeller Impeller rendering backend issues and features requests labels Sep 18, 2026
@google-cla

google-cla Bot commented Sep 18, 2026

Copy link
Copy Markdown

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

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

Copy link
Copy Markdown
Contributor

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 updates the blend filter contents in Impeller to snap coverage to the pixel grid using Rect::RoundOut in both AdvancedBlend and PipelineBlend paths, ensuring that texture coordinates, subpass extent, and composite translation align. It also adds unit tests to verify pixel alignment. Feedback is provided to restore the original capabilities at the end of the AdvancedBlendSubpassIsPixelAligned test to prevent global state pollution and potential test flakiness.

Comment thread engine/src/flutter/impeller/entity/entity_unittests.cc Outdated
Comment thread engine/src/flutter/impeller/entity/entity_unittests.cc Outdated
Advanced and pipeline blends that take the offscreen path derive three
things from the blend coverage, and they do not agree:

  * the texture coordinates, from the raw float coverage;
  * the subpass render target extent, from `ISize(coverage.GetSize())`,
    whose converting constructor truncates rather than rounds;
  * the composite translation, from the fractional coverage origin, drawn
    with the default nearest sampler.

An integer quad therefore displays a float sized region, scaling the
contents by trunc(w)/w. The factor changes whenever the coverage crosses
a pixel boundary, so animated content is resampled differently every
frame.

Snap the coverage to the device pixel grid once with `Rect::RoundOut` and
derive all three from that single integral rect. `Canvas::SaveLayer`
already does this for save layer subpasses, for the same nearest sampling
reason; the blend filter path never got the same treatment.

Only configurations without framebuffer fetch take this path: the iOS
simulator, PowerVR, and Adreno on GLES. Devices with framebuffer fetch
blend in pass via `ApplyFramebufferBlend`, which is exact.

Adds a regression test per offscreen path, asserting the composited
entity lands on the pixel grid when the input coverage is fractional.
Both fail without this change.

Fixes flutter#192980
@tguerin
tguerin force-pushed the impeller_blend_pixel_align branch from b3ec9da to df2b025 Compare September 22, 2026 20:02
@flutter-dashboard flutter-dashboard Bot removed the CICD Run CI/CD label Sep 22, 2026
@b-luk b-luk added the CICD Run CI/CD label Sep 22, 2026
@b-luk
b-luk requested a review from flar September 22, 2026 21:33
@flar

flar commented Sep 22, 2026

Copy link
Copy Markdown
Contributor

There are golden failures that look like a clipping issue.

Replace the RoundOut approach with one that leaves the subpass geometry
untouched: the render target size, composite origin and composited rect
are as on master. Only the texture coordinates change, derived from the
rect the render target actually represents so that the quad and the
sampled region agree exactly.

Rounding the subpass out changed the composited rect, and the earlier
unit tests asserted that alignment; both are dropped. The
ColorFilterAdvancedBlendNoFbFetch golden, which stacks thirty of these
blends and visibly captures the resampling error, is the regression
coverage.
@flutter-dashboard flutter-dashboard Bot removed the CICD Run CI/CD label Sep 23, 2026
@tguerin

tguerin commented Sep 23, 2026

Copy link
Copy Markdown
Contributor Author

You were right that it changes that golden — but digging in, it's a correction rather than a regression. That test stacks 30 full-frame blends, so the resampling error the PR fixes accumulates into a smear: the current golden bleeds orange ~4px into the empty gap before each cell. Rendering the same test through framebuffer fetch puts the edge exactly where this PR does.

I've also reworked the fix to be much smaller — it just makes the texture coords match the render target rect, with no change to the subpass geometry. The PR description has the pixel numbers. The golden will need triage.

@flutter-dashboard

Copy link
Copy Markdown

It looks like this pull request may not have tests. Please make sure to add tests or get an explicit test exemption before merging.

If you are not sure if you need tests, consider this rule of thumb: the purpose of a test is to make sure someone doesn't accidentally revert the fix. Ask yourself, is there anything in your PR that you feel it is important we not accidentally revert back to how it was before your fix?

Reviewers: Read the Tree Hygiene page and make sure this patch meets those guidelines before LGTMing. If you believe this PR qualifies for a test exemption, contact "@test-exemption-reviewer" in the #hackers channel in Discord (don't just cc them here, they won't see it!). The test exemption team is a small volunteer group, so all reviewers should feel empowered to ask for tests, without delegating that responsibility entirely to the test exemption group.

DlGoldenTest.StackedOffscreenAdvancedBlendsDoNotResample renders a hard
backdrop edge, then blends over it thirty times with kLighten on a mocked
context without framebuffer fetch, and requires the result to match a
single blend. Lighten is idempotent, so the only thing that can differ is
resampling error accumulated across the stacked subpasses.

A 0.4x scale on the canvas makes the full-pass coverage fractional the
same way the ColorFilterAdvancedBlendNoFbFetch golden does, so the
subpass render target is truncated. Without the fix the edge drifts and
the RMSE is about 5.75; with it the two images are identical.
@tguerin tguerin changed the title [Impeller] Pixel align blend subpass textures [Impeller] Sample the render target rect in offscreen advanced blends Sep 23, 2026
@b-luk b-luk added the CICD Run CI/CD label Sep 23, 2026
@flutter-dashboard

Copy link
Copy Markdown

CI had a failure that stopped further tests from running. We need to investigate to determine the root cause.

SHA at time of execution: ec76295.

Possible causes:

  • Configuration Changes: The .ci.yaml file might have been modified between the creation of this pull request and the start of this test run. This can lead to ci yaml validation errors.
  • Infrastructure Issues: Problems with the CI environment itself (e.g., quota) could have caused the failure.

A blank commit, or merging to head, will be required to resume running CI for this PR.

Error Details:

FormatException: ERROR: Mac_arm64_ios basic_material_app_ios__compile is a new builder added. it needs to be marked bringup: true
If ci.yaml wasn't changed, try `git fetch upstream && git merge upstream/master`



Stack trace:

#0      CiYaml._validate (package:cocoon_service/src/model/ci_yaml/ci_yaml.dart:395:7)
#1      new CiYaml (package:cocoon_service/src/model/ci_yaml/ci_yaml.dart:128:7)
#2      new CiYamlSet (package:cocoon_service/src/model/ci_yaml/ci_yaml.dart:48:23)
#3      CiYamlFetcher._getCiYaml (package:cocoon_service/src/service/scheduler/ci_yaml_fetcher.dart:124:12)
<asynchronous suspension>
#4      Scheduler.getPresubmitTargets (package:cocoon_service/src/service/scheduler.dart:1064:20)
<asynchronous suspension>
#5      Scheduler._getTestsForStage (package:cocoon_service/src/service/scheduler.dart:1367:14)
<asynchronous suspension>
#6      Scheduler._runCiTestingStage (package:cocoon_service/src/service/scheduler.dart:1408:30)
<asynchronous suspension>
#7      Scheduler.proceedToCiTestingStage (package:cocoon_service/src/service/scheduler.dart:1517:7)
<asynchronous suspension>
#8      Scheduler._closeSuccessfulEngineBuildStage (package:cocoon_service/src/service/scheduler.dart:1312:5)
<asynchronous suspension>
#9      Scheduler.processCheckRunCompleted (package:cocoon_service/src/service/scheduler.dart:1253:11)
<asynchronous suspension>
#10     PresubmitSubscription._processBuild (package:cocoon_service/src/request_handlers/presubmit_subscription.dart:228:7)
<asynchronous suspension>
#11     PresubmitSubscription.post (package:cocoon_service/src/request_handlers/presubmit_subscription.dart:119:5)
<asynchronous suspension>
#12     RequestHandler.service (package:cocoon_service/src/request_handling/request_handler.dart:42:20)
<asynchronous suspension>
#13     SubscriptionHandler.service (package:cocoon_service/src/request_handling/subscription_handler.dart:134:5)
<asynchronous suspension>
#14     createServer.<anonymous closure> (package:cocoon_service/server.dart:462:7)
<asynchronous suspension>
#15     main.<anonymous closure>.<anonymous closure> (file:///app/app_dart/bin/gae_server.dart:192:9)
<asynchronous suspension>

@flutter-dashboard flutter-dashboard Bot removed the CICD Run CI/CD label Sep 23, 2026
@b-luk

b-luk commented Sep 23, 2026

Copy link
Copy Markdown
Contributor

Merging main to resolve .ci.yaml issue

@b-luk b-luk added the CICD Run CI/CD label Sep 23, 2026
@flutter-dashboard

Copy link
Copy Markdown

Golden file changes have been found for this pull request. Click here to view and triage (e.g. because this is an intentional change).

If you are still iterating on this change and are not ready to resolve the images on the Flutter Gold dashboard, consider marking this PR as a draft pull request above. You will still be able to view image results on the dashboard, commenting will be silenced, and the check will not try to resolve itself until marked ready for review.

For more guidance, visit Writing a golden file test for package:flutter.

Reviewers: Read the Tree Hygiene page and make sure this patch meets those guidelines before LGTMing.

Changes reported for pull request #192988 at sha 671565f

@flutter-dashboard flutter-dashboard Bot added the will affect goldens Changes to golden files label Sep 23, 2026
Comment thread engine/src/flutter/impeller/entity/contents/filters/blend_filter_contents.cc Outdated
@tguerin
tguerin requested a review from flar September 30, 2026 14:47
@b-luk b-luk added the CICD Run CI/CD label Sep 30, 2026

@b-luk b-luk left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This looks good to me. Just one suggested nit

Comment thread engine/src/flutter/impeller/entity/contents/filters/blend_filter_contents.cc Outdated
render_target_coverage shares its origin with subpass_coverage, so the
translation between them is always zero.
@flutter-dashboard flutter-dashboard Bot removed the CICD Run CI/CD label Sep 30, 2026
@tguerin
tguerin requested a review from b-luk September 30, 2026 18:55
@flar flar added the CICD Run CI/CD label Sep 30, 2026
@flar

flar commented Sep 30, 2026

Copy link
Copy Markdown
Contributor

/gemini review

@flar flar left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM

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

Copy link
Copy Markdown
Contributor

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 updates the advanced blend filter in Impeller to snap the subpass render target to an integral size when framebuffer fetch is unavailable, preventing scaling drift and resampling issues on fractional boundaries. It also adds corresponding golden and unit tests. Feedback on the changes identifies a critical issue where the vertex buffer generation still uses coverage.GetSize() instead of render_target_size after the removal of the translation matrix from frame_info.mvp, which can lead to rendering distortion when a coverage hint is present.

@flutter-dashboard

Copy link
Copy Markdown

Golden file changes are available for triage from new commit, Click here to view.

For more guidance, visit Writing a golden file test for package:flutter.

Reviewers: Read the Tree Hygiene page and make sure this patch meets those guidelines before LGTMing.

Changes reported for pull request #192988 at sha ea33ce1

@b-luk b-luk added the autosubmit Merge PR when tree becomes green via auto submit App label Sep 30, 2026
@b-luk

b-luk commented Sep 30, 2026

Copy link
Copy Markdown
Contributor

I approved the updated goldens

@tguerin

tguerin commented Sep 30, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @flar and @b-luk for your patience and for helping me land this PR, really appreciate it!

@auto-submit
auto-submit Bot added this pull request to the merge queue Sep 30, 2026
Merged via the queue into flutter:master with commit 63768f5 Sep 30, 2026
30 checks passed
@flutter-dashboard flutter-dashboard Bot removed the autosubmit Merge PR when tree becomes green via auto submit App label Sep 30, 2026
auto-submit Bot pushed a commit to flutter/packages that referenced this pull request Oct 1, 2026
…13104)

Manual roll requested by [email protected]

flutter/flutter@d649d2b...e89fd0a

2026-10-01 [email protected] Manual roll ICU from 6ebb40c59477 to 5aa526207171 (2 revisions) (flutter/flutter#193661)
2026-10-01 [email protected] fix(web): hash source maps before rewriting sourceMappingURL with --web-content-hash (flutter/flutter#193614)
2026-10-01 [email protected] [flutter_tools] Bootstrap DeviceManager in ToolDependencies (flutter/flutter#193524)
2026-10-01 [email protected] [wasm] Set flutter_runtime_mode in tools/gn to_gn_wasm_args (flutter/flutter#193407)
2026-10-01 [email protected] Remove remaining `download_jdk` references (flutter/flutter#193562)
2026-10-01 [email protected] Synchronize dart2wasm feature probes in roll script and flutter_bootstrap.js (flutter/flutter#193271)
2026-10-01 [email protected] [web] Unskip web tests that now pass in gestures, services, and widgets (flutter/flutter#193512)
2026-10-01 [email protected] Roll Fuchsia Test Scripts from whZAV1mfT-Rdc0rQ8... to fCNknap4ZI0AMcvIf... (flutter/flutter#193640)
2026-10-01 [email protected] Roll Skia from 0575a37d0619 to f2d68e0b8863 (2 revisions) (flutter/flutter#193621)
2026-10-01 [email protected] perf(widgets): use const <Never>{} in InheritedModelElement and avoid redundant casts in RenderObjectElement.update (flutter/flutter#192913)
2026-10-01 [email protected] [web] Unskip and strengthen missing glyph tofu test on Wimp (flutter/flutter#193426)
2026-10-01 [email protected] [flutter_tools] Fix `flutter drive -d chrome` hanging on web (flutter/flutter#193459)
2026-10-01 [email protected] [web] Fix hang when Chromium cannot bind remote debugging port (flutter/flutter#193461)
2026-10-01 [email protected] [flutter_tools] Bundle data assets from hooks in flutter test (flutter/flutter#193435)
2026-10-01 [email protected] Roll Packages from 0ba9a82 to d5ec6db (11 revisions) (flutter/flutter#193641)
2026-10-01 [email protected] Revert: [flutter_tools] Fix DTD "Stream already subscribed" error and wait for analysis over DTD in widget preview (flutter/flutter#193637)
2026-10-01 [email protected] Roll Skia from 6182a3cf8f98 to 0575a37d0619 (5 revisions) (flutter/flutter#193613)
2026-10-01 [email protected] [flutter_tools] Fix DTD "Stream already subscribed" error and wait for analysis over DTD in widget preview (flutter/flutter#192930)
2026-09-30 [email protected] [Impeller] Sample the render target rect in offscreen advanced blends (flutter/flutter#192988)
2026-09-30 [email protected] Roll Skia from 01524014dff5 to 6182a3cf8f98 (11 revisions) (flutter/flutter#193594)
2026-09-30 [email protected] [web] Use Xvfb -displayfd and clean up Xvfb after Firefox test suites (flutter/flutter#193510)
2026-09-30 [email protected] Fixes bug where sibling node fail to update size (flutter/flutter#191220)
2026-09-30 [email protected] Roll Skia from c7b323126bf0 to 01524014dff5 (3 revisions) (flutter/flutter#193577)
2026-09-30 [email protected] Remove some Undefined Behavior from run_customer_testing_tests.dart. (flutter/flutter#193582)
2026-09-30 [email protected] Fix pointer id/coordinate mismatch in Android platform view multi-touch (flutter/flutter#191631)
2026-09-30 [email protected] Roll Packages from 3c6ce59 to 0ba9a82 (11 revisions) (flutter/flutter#193578)
2026-09-30 [email protected] Fix unresolved doc comment references in dev/ tooling (flutter/flutter#193501)

If this roll has caused a breakage, revert this CL and set the roller
to dry run mode using the controls here:
https://autoroll.skia.org/r/flutter-packages
Please CC [email protected],[email protected] on the revert to ensure that a human
is aware of the problem.

To file a bug in Packages: https://github.com/flutter/flutter/issues/new/choose

To report a problem with the AutoRoller itself, please file a bug:
https://issues.skia.org/issues/new?component=1389291&template=1850622

Documentation for the AutoRoller is here:
https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CICD Run CI/CD e: impeller Impeller rendering backend issues and features requests engine flutter/engine related. See also e: labels. will affect goldens Changes to golden files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Impeller] Advanced blend modes wobble: the blend subpass texture is truncated to an integer size and composited back at a fractional origin

4 participants