Repository navigation
[Impeller] Do not clamp the local coverage limit to the root render target - #195860
Conversation
|
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 both you and the PR author believe this PR qualifies for a test exemption, escalate to the tech lead for the changed area to review and apply the |
|
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. |
There was a problem hiding this comment.
Code Review
This pull request updates the Canvas::GetLocalCoverageLimit method to include an optional clamp_to_root parameter, allowing subpasses to bypass clamping to the root render target size. Corresponding updates were made to the method signature in the header file and at call sites in SaveLayer and DlDispatcherBase::drawDisplayList. A reviewer suggested that the clamping logic could be simplified further by removing the parameter entirely, as the intersection is a no-op on the root pass and potentially incorrect for subpasses.
| // For subpasses the root RT rect is irrelevant: the subpass texture can | ||
| // extend beyond the root RT (e.g. shifted by a translate, or expanded by | ||
| // an image filter). The pass_texture ∩ clip above is already the correct | ||
| // upper bound. On the root pass, pass_texture == root_RT, so this | ||
| // intersection is a no-op either way. | ||
| if (!clamp_to_root) { | ||
| return maybe_coverage_limit; | ||
| } | ||
| return maybe_coverage_limit->Intersection( | ||
| Rect::MakeSize(render_target_.GetRenderTargetSize())); |
There was a problem hiding this comment.
Since the intersection with the root render target size is a no-op on the root pass (where pass_texture == root_RT) and incorrect on subpasses (where the subpass texture can extend beyond the root RT), we can completely remove the clamping logic and the clamp_to_root parameter.\n\nThis simplifies the API and avoids having to pass /*clamp_to_root=*/false at all the call sites (e.g., in SaveLayer and DrawDisplayList).\n\nIf we remove the clamping entirely, GetLocalCoverageLimit will naturally return the correct bounded limit for both root passes and subpasses.
| // For subpasses the root RT rect is irrelevant: the subpass texture can | |
| // extend beyond the root RT (e.g. shifted by a translate, or expanded by | |
| // an image filter). The pass_texture ∩ clip above is already the correct | |
| // upper bound. On the root pass, pass_texture == root_RT, so this | |
| // intersection is a no-op either way. | |
| if (!clamp_to_root) { | |
| return maybe_coverage_limit; | |
| } | |
| return maybe_coverage_limit->Intersection( | |
| Rect::MakeSize(render_target_.GetRenderTargetSize())); | |
| return maybe_coverage_limit; |
References
- Suggest simplification and refactoring to enhance readability and maintainability, and write what you need and no more. (link)
There was a problem hiding this comment.
Thanks, but I would rather not drop the clamp entirely. In SaveLayer, the coverage limit is not only an upper bound: ComputeSaveLayerCoverage returns it directly as the subpass size when the output floods or the bounds are unbounded. Unclamping it there would grow offscreen textures, and it is not needed for this bug.
I have narrowed the change so only DrawDisplayList (culling) uses the unclamped value, and SaveLayer is unchanged. I also replaced the tests with one that fails without the fix.
There was a problem hiding this comment.
I think we do need to drop the clamp entirely.
Your fix covers the DrawDisplayList case, where DrawDisplayList is used inside a SaveLayer. It removes the clamp so when DrawDisplayList has contents drawn off screen, it doesn't get culled because the SaveLayer could have a filter that moves the off-screen contents on-screen.
The other case where GetLocalCoverageLimit is used is in SaveLayer to get the subpass size. Your fix leaves this unchanged. But I think this case has the same issue. If SaveLayer is called inside a parent SaveLayer, the child SaveLayer's off-screen contents could be moved on-screen by a filter on the parent SaveLayer. If the clamp is kept for this case, the child SaveLayer's off-screeen contents would get incorrectly culled and wouldn't get drawn when the parent SaveLayer's filter moves it on-screen.
This can be reproed with a modified version of your DrawDisplayListInTranslatedSaveLayerIsNotCulled test:
TEST_P(AiksTest, NestedSaveLayerInTranslatedSaveLayerIsNotCulled) {
DlPaint green;
green.setColor(DlColor::kGreen());
DisplayListBuilder builder;
builder.Scale(GetContentScale().x, GetContentScale().y);
DlPaint paint;
paint.setImageFilter(DlImageFilter::MakeMatrix(
DlMatrix::MakeTranslation({300, 0}), DlImageSampling::kLinear));
builder.SaveLayer(std::nullopt, &paint);
// Control: drawn directly into the parent layer.
builder.DrawCircle(DlPoint(-100, 100), 80, green);
DlPaint child_paint;
builder.SaveLayer(std::nullopt, &child_paint);
// Drawn inside the child SaveLayer; not visible when SaveLayer clamps.
builder.DrawCircle(DlPoint(-100, 300), 80, green);
builder.Restore();
builder.Restore();
ASSERT_TRUE(OpenPlaygroundHere(builder.Build()));
}As you mention, removing the clamping can result in growing offscreen textures. But this is correct and necessary. The offscreen texture must be sized to contain contents that ends up on-screen by the SaveLayer's translation.
There was a problem hiding this comment.
You are right, thanks. The child SaveLayer case reproduces with plain dart:ui on 3.38.3 (missing on Impeller, present on Skia); I added it to the issue. Done in 4244c8d: the clamp is removed entirely and NestedSaveLayerInTranslatedSaveLayerIsNotCulled is added.
|
I signed the CLA. |
…target DrawDisplayList culls a nested DisplayList against Canvas::GetLocalCoverageLimit(), which always intersects with the root render target. Inside a subpass this is too tight: content outside the root RT can still end up on-screen, for example when an ancestor SaveLayer has a matrix image filter that translates it. Such content is culled and never drawn. Add a clamp_to_root parameter (default true) and pass false only from DrawDisplayList. SaveLayer keeps the clamped value, because it can be returned directly as the subpass size and unclamping it would grow offscreen textures. On the root pass the two values are identical. Test: DrawDisplayListInTranslatedSaveLayerIsNotCulled. Fixes flutter#195859
344d142 to
379e320
Compare
| // For subpasses the root RT rect is irrelevant: the subpass texture can | ||
| // extend beyond the root RT (e.g. shifted by a translate, or expanded by | ||
| // an image filter). The pass_texture ∩ clip above is already the correct | ||
| // upper bound. On the root pass, pass_texture == root_RT, so this | ||
| // intersection is a no-op either way. | ||
| if (!clamp_to_root) { | ||
| return maybe_coverage_limit; | ||
| } | ||
| return maybe_coverage_limit->Intersection( | ||
| Rect::MakeSize(render_target_.GetRenderTargetSize())); |
There was a problem hiding this comment.
I think we do need to drop the clamp entirely.
Your fix covers the DrawDisplayList case, where DrawDisplayList is used inside a SaveLayer. It removes the clamp so when DrawDisplayList has contents drawn off screen, it doesn't get culled because the SaveLayer could have a filter that moves the off-screen contents on-screen.
The other case where GetLocalCoverageLimit is used is in SaveLayer to get the subpass size. Your fix leaves this unchanged. But I think this case has the same issue. If SaveLayer is called inside a parent SaveLayer, the child SaveLayer's off-screen contents could be moved on-screen by a filter on the parent SaveLayer. If the clamp is kept for this case, the child SaveLayer's off-screeen contents would get incorrectly culled and wouldn't get drawn when the parent SaveLayer's filter moves it on-screen.
This can be reproed with a modified version of your DrawDisplayListInTranslatedSaveLayerIsNotCulled test:
TEST_P(AiksTest, NestedSaveLayerInTranslatedSaveLayerIsNotCulled) {
DlPaint green;
green.setColor(DlColor::kGreen());
DisplayListBuilder builder;
builder.Scale(GetContentScale().x, GetContentScale().y);
DlPaint paint;
paint.setImageFilter(DlImageFilter::MakeMatrix(
DlMatrix::MakeTranslation({300, 0}), DlImageSampling::kLinear));
builder.SaveLayer(std::nullopt, &paint);
// Control: drawn directly into the parent layer.
builder.DrawCircle(DlPoint(-100, 100), 80, green);
DlPaint child_paint;
builder.SaveLayer(std::nullopt, &child_paint);
// Drawn inside the child SaveLayer; not visible when SaveLayer clamps.
builder.DrawCircle(DlPoint(-100, 300), 80, green);
builder.Restore();
builder.Restore();
ASSERT_TRUE(OpenPlaygroundHere(builder.Build()));
}As you mention, removing the clamping can result in growing offscreen textures. But this is correct and necessary. The offscreen texture must be sized to contain contents that ends up on-screen by the SaveLayer's translation.
Per review: SaveLayer has the same problem as DrawDisplayList. A child SaveLayer inside a parent SaveLayer whose image filter translates content on-screen was sized against the root RT, so its off-screen content was dropped. Remove the clamp_to_root parameter and the root RT intersection entirely; the pass texture and clip already bound the result, and on the root pass the pass texture is the root RT. Add NestedSaveLayerInTranslatedSaveLayerIsNotCulled (from review).
b-luk
left a comment
There was a problem hiding this comment.
Thanks for the changes, @caimengnan-ops.
Please also update the title and description, now that the changes are not limited to DrawDisplayList.
A tip regarding reviews: After making changes, if the PR is ready for review again you should click to re-request a review. That lets reviewers know the PR is ready to be looked at again.
| // Renders two green circles side by side. Both are drawn at x=-100 inside a | ||
| // SaveLayer whose image filter translates them +300 on-screen. The left one | ||
| // is drawn directly; the right one is drawn through DrawDisplayList (a nested |
There was a problem hiding this comment.
The description is incorrect. The circles are drawn with one on top and one below, not side by side. "Left one" and "right one" should be "top one" and "bottom one".
Same for the description of the nested save layer test below.
There was a problem hiding this comment.
Fixed in e508e3b, both test descriptions now say top/bottom.
| @@ -1823,8 +1823,12 @@ std::optional<Rect> Canvas::GetLocalCoverageLimit() const { | |||
| return std::nullopt; | |||
| } | |||
There was a problem hiding this comment.
This check is no longer needed. It was needed before, because we had to check that the optional had a value before making another Intersection call below. But that Intersection was removed. So we can just directly return maybe_coverage_limit (or return the result directly without a maybe_coverage_limit variable at all).
There was a problem hiding this comment.
Done in e508e3b. Rect::Intersection already returns nullopt when the rects do not overlap and a non-empty rect otherwise, so it now returns the intersection directly.
- GetLocalCoverageLimit: Rect::Intersection already returns nullopt when the rects do not overlap, and a non-empty result otherwise, so the has_value/IsEmpty check is no longer needed. Return it directly. - The test circles are stacked vertically; describe them as top/bottom instead of left/right.
|
Thanks for the review. All comments are addressed, and the title and description are updated. |
b-luk
left a comment
There was a problem hiding this comment.
Looking mostly good to me. I had a nit about the added comment. And you'll need to merge from head to pick up changes to pass the "ci.yaml validation" check.
| // Do not intersect with the root render target. Inside a subpass, content | ||
| // outside the root RT can still end up on-screen (e.g. an ancestor image | ||
| // filter translates it), so both culling and nested subpass sizing must be | ||
| // bounded by the current pass texture and clip only. On the root pass the | ||
| // pass texture is the root RT, so the result is unchanged there. |
There was a problem hiding this comment.
With the updated code, I think this comment is a bit confusing. I'd remove the second half of it, just leaving:
// Do not intersect with the root render target. Inside a subpass, content
// outside the root RT can still end up on-screen (e.g. an ancestor image
// filter translates it).
I think the rest just adds details that are confusing or not useful to someone trying to understand the code.
- "so both culling and nested subpass sizing must be bounded by the current pass texture and clip only." describes how the two current callers use the result. The function itself shouldn't need to know that, and the comment will go stale if callers are added or changed.
- "On the root pass the pass texture is the root RT, so the result is unchanged there" is saying that intersecting with the root render target would make no difference for the root pass case. But we're no longer doing this intersection in any case. So the comment only makes sense in the context of the PR and with knowledge of the old code. For someone just reading the new code, it's a confusing thing to note.
There was a problem hiding this comment.
Done in 24e2988, thanks. I also merged master in dc568a4.
|
Thanks! Addressed the comment and merged master. Could you re-add the CICD label so the checks can run again? |
|
@b-luk The workflows for the latest commit are waiting for approval ("action_required"), so the checks have not started. Could you approve them when you get a chance? Thanks! |
|
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 Reviewers: Read the Tree Hygiene page and make sure this patch meets those guidelines before LGTMing. |
|
The changed golden is correct behavior. The updated image shows a partially cropped white rectangle on the right side of the image. This white shape does not appear in the existing golden. This white shape is the "Debug" banner text (rendered with Ahem rectangles). It was incorrectly culled with the existing behavior. The new golden correctly draws it as intended. |
flutter/flutter@b98afb5...9c427b5 2026-11-12 [email protected] Roll Skia from d67d0f22e3ec to 9aa2513a4ac9 (2 revisions) (flutter/flutter#196429) 2026-11-12 [email protected] [flutter_tools] Move CpuArch to flutter_tools_core and define HostPlatform by osName and cpuArch (flutter/flutter#195841) 2026-11-12 [email protected] Roll Skia from 23e7e63789b8 to d67d0f22e3ec (2 revisions) (flutter/flutter#196424) 2026-11-12 [email protected] Uber compactify (flutter/flutter#196392) 2026-11-12 [email protected] Roll Skia from 3d7e335da165 to 23e7e63789b8 (12 revisions) (flutter/flutter#196422) 2026-11-12 [email protected] Roll Dart SDK from 13af329ea4f2 to e5add7a25b32 (1 revision) (flutter/flutter#196413) 2026-11-12 [email protected] Don't terminate the shared EGL display when an engine is disposed (flutter/flutter#194520) 2026-11-11 [email protected] Adds manifest flag to turn on SDFs for Android. (flutter/flutter#196056) 2026-11-11 [email protected] Remove bringup from Linux_pixel_7pro integration_ui_keyboard_inset_jump (flutter/flutter#196334) 2026-11-11 [email protected] Uses system default locale for text rendering when preferred locale isn't supported (flutter/flutter#195997) 2026-11-11 [email protected] [android]: prewarms shaders when the context is created (flutter/flutter#196069) 2026-11-11 [email protected] iOS: Recomposite platform views only if their parameters change (flutter/flutter#196287) 2026-11-11 [email protected] [flutter_tools] Forward encoding in ErrorHandlingFile.readAsStringSync (flutter/flutter#196136) 2026-11-11 [email protected] Roll Skia from b89c24bbab8c to 3d7e335da165 (8 revisions) (flutter/flutter#196390) 2026-11-11 [email protected] Add Flutter Gardener instructions on how to disable a test (flutter/flutter#196312) 2026-11-11 [email protected] [Impeller] Enable malioc analysis for framebuffer_blend shaders (flutter/flutter#196343) 2026-11-11 [email protected] [flutter_tools] Migrate general.shard Android, asset, and build tests off globals (flutter/flutter#196125) 2026-11-11 [email protected] Roll Dart SDK from 8f15c4ca2d41 to 13af329ea4f2 (1 revision) (flutter/flutter#196386) 2026-11-11 [email protected] [Impeller] Do not clamp the local coverage limit to the root render target (flutter/flutter#195860) 2026-11-11 [email protected] [Impeller] Use separate uniform buffers on VMware SVGA3D (flutter/flutter#196022) 2026-11-11 [email protected] [flutter_tools] Introduce AndroidSdkProvider for lazy native_assets SDK lookup (flutter/flutter#196122) 2026-11-11 [email protected] Moves linux to ThinLTO. (flutter/flutter#196314) 2026-11-11 [email protected] [flutter_tools] Migrate bin/fuchsia_tester.dart off globals and runInContext (flutter/flutter#196074) 2026-11-11 [email protected] Changes the tolerances for frame_rate_main_test.dart. (flutter/flutter#196304) 2026-11-11 [email protected] Roll Packages from 08faf64 to 653628e (16 revisions) (flutter/flutter#196381) 2026-11-11 [email protected] Allow SliverCrossAxisGroup children to lay out with zero cross-axis extent instead of asserting (flutter/flutter#191287) 2026-11-11 [email protected] Roll Skia from cc4c54f752ba to b89c24bbab8c (4 revisions) (flutter/flutter#196375) 2026-11-11 [email protected] [Linux] Fix layer offset when compositing with glBlitFramebuffer (flutter/flutter#196189) 2026-11-11 [email protected] [flutter_tools] Inject CrashReportSender into SwiftPM migration (flutter/flutter#196315) 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
Canvas::GetLocalCoverageLimit()intersected its result with the root render target. Inside a subpass this is too tight: content outside the root RT can still end up on-screen, for example when an ancestorSaveLayerhas a matrix image filter that translates it. Both callers were affected:DrawDisplayListuses the limit as its cull rect, so a nested DisplayList (e.g. adart:uiPicture) drawn off-screen inside such a layer was culled.SaveLayeruses the limit to size the subpass, so a childSaveLayerinside such a layer lost its off-screen content.This PR removes the root RT intersection. The limit is now bounded only by the current pass texture and the clip. On the root pass the pass texture is the root RT, so the result there is unchanged. Subpass textures in the affected cases can be larger, because they now have to hold the content that the ancestor filter moves on-screen.
Tests:
DrawDisplayListInTranslatedSaveLayerIsNotCulled: a circle drawn throughDrawDisplayListinside a translated SaveLayer.NestedSaveLayerInTranslatedSaveLayerIsNotCulled: a circle drawn inside a child SaveLayer inside a translated SaveLayer.Each test also draws a control circle directly into the parent layer. Before the fix only the control circle renders.
Both scenarios reproduce with plain
dart:uion Flutter 3.38.3 (flutter test --enable-impeller): the circle is missing on Impeller and present on Skia. See #195859.Fixes #195859