Skip to content

Update DisplayList rendering unittests for new SufaceProvider API and to be able to test Impeller#185820

Merged
auto-submit[bot] merged 10 commits into
flutter:masterfrom
flar:update-dl-rendering-unittests
May 6, 2026
Merged

Update DisplayList rendering unittests for new SufaceProvider API and to be able to test Impeller#185820
auto-submit[bot] merged 10 commits into
flutter:masterfrom
flar:update-dl-rendering-unittests

Conversation

@flar

@flar flar commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

#185270 created a refactored SurfaceProvider API that enables better control over which backend tests and benchmarks can use. While the primitive rendering benchmarks were updated for the new code, the rendering validation tests (display_list_rendertests) were still using a hybrid of the old API and some back doors to test on various platforms. This PR updates the rendering tests to use the new API, fills out its implementation on all of the supported platforms and makes the rendering validation tests more appropriate to run on all platforms.

It also removes a few outdated tests that we no longer care about, such as:

  • How do the bounds of a DisplayList compare to the bounds of an equivalent SkPicture?
  • Does the Skia backend draw the same thing if we execute commands directly on its SkCanvas compared to recording those same commands in SkPicture or DisplayList?
  • Do the SkPicture and DisplayList contain approximately the same number of operations?

We still test the following:

  • Does the rendering operation produce actual output for every combination of attributes (no remaining cases where the rendering back end simply punts on an operation)
  • Does the rendering draw outside of the bounds declared by the DisplayList?
  • When we draw something with and without an attribute or context mutation (clip, transform, or save layer) does it actually produce the same or different pixels compared to our expectation?
  • If a combination of rendering operation and attributes recorded in a DisplayList claims that it supports "group opacity" (i.e. the ability to apply any opacity from outside the DisplayList to each rendering operation so that the caller does not have to wrap the display list in a SaveLayer to achieve the group opacity) then does it do that correctly when we render the constructed DisplayList with a requested group opacity?

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 documentation (doc comments with ///).
  • 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.

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. 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.

@flutter-dashboard flutter-dashboard Bot added the CICD Run CI/CD label Apr 30, 2026
@github-actions github-actions Bot added engine flutter/engine related. See also e: labels. e: impeller Impeller rendering backend issues and features requests labels Apr 30, 2026
@github-actions github-actions Bot removed the CICD Run CI/CD label Apr 30, 2026
@flar flar added the CICD Run CI/CD label Apr 30, 2026
@github-actions github-actions Bot removed the CICD Run CI/CD label Apr 30, 2026
@flar flar added the CICD Run CI/CD label Apr 30, 2026
@flar flar marked this pull request as ready for review May 1, 2026 02:41

@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 refactors the display list testing framework to unify snapshotting capabilities across Skia and Impeller backends. Key changes include adding SnapshotToPixelData and SnapshotToImage to the DlSurfaceInstance interface, implementing backend-specific pixel data wrappers, and updating PlaygroundSwitches handling. Review feedback identifies an off-by-one error in Impeller bounds checking, a significant memory over-allocation in Skia pixel data, and potential null pointer dereferences. It also highlights an inconsistency where the Impeller implementation returns a live texture instead of a static snapshot, which could lead to flaky tests.

Comment thread engine/src/flutter/display_list/testing/skia/dl_test_surface_instance_skia.cc Outdated
@github-actions github-actions Bot removed the CICD Run CI/CD label May 1, 2026
@flar flar added the CICD Run CI/CD label May 1, 2026
@flar flar requested review from b-luk, gaaclarke and walley892 May 1, 2026 23:41

@gaaclarke gaaclarke left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Mostly looks good. I'm confused what this change is doing, can you update the description please? The description talks about what is being removed clearly.

Please make smaller PRs.

playground_ = MakePlayground(impeller::PlaygroundBackend::kMetalSDF);
impeller::PlaygroundSwitches switches;
switches.enable_wide_gamut = true;
switches.flags.use_sdfs = true;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is redundant information, can't we just infer it from the PlaygroundBackend?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The use_sdfs is set when you ask for MetalSDF, but it isn't really mentioned by the method which takes a set of switches as an argument so the chain of responsibility for how the switches is set up is not really explicit.

if (pixels_.get()) {
pixmap_.reset(info, pixels_.get(), info.minRowBytes());
if (raster_image->readPixels(pixmap_, 0, 0)) {
is_valid_ = true;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I would like it if we could leave the "IsValid" pattern behind and use absl::StatusOr with factories instead. Nothing good can come from constructing invalid objects.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This would be really difficult to untangle. The main issue is that SkPixmap is built around raw pointers and so we need to manage their data storage specially. I could try to work around that and do something with raw pointers, but the Skia versions of these backends has a limited future. Some of the tests could be lifted into the calling method, but in the end, we're going to need an IsValid to know if the pixel readback into the private Pixmap field succeeded.

Also, IsValid is private API. This class is private to this file and the method is not exposed through any public API. It is only for the calling internal method to know if the Skia APIs succeeded in the constructor. I agree that public non-IsValid objects are bad, but these aren't public (unless they are valid).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Yea, trying to fix it everywhere is out of scope of this PR. But since we are editing this class we can do the right thing here by introducing the factory method and always returning true for IsValid().

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'm not sure I was clear. There is no "everywhere" here, IsValid doesn't escape this file and is only used in one place. The work that can stump validity has to be done in a constructor, not a factory. So, even if I hide this behind a factory, the constructor needs to have to have a way to return a failure mode. And if it does so, does it matter if that validity is fielded by a factory method in this class or another method in this same file?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I might be able to get around this by constructing a Pixmap temporarily for the readback. It looks like it's just an "annotated pointer". I'd still need a good way to transfer the allocated memory from the factory to the constructor...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Actually, switching this was quite simple.

SkPixmap is just a holder of related data. It doesn't "own" any of it, including not owning the pixel pointer. So, it can be declared, used for read back, and then just dropped on the floor. The only ownership issue is the pixel pointer which is a unique_ptr that can just be transferred into the constructor.

size_t width() const override { return pixmap_.info().width(); }
size_t height() const override { return pixmap_.info().height(); }

virtual bool write(const std::string& path) const override {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

no action required: I'd like it if we used absl::Status here too.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Future work if you file an issue. These are just test utilities, not used in production code.

Comment thread engine/src/flutter/display_list/testing/skia/dl_test_surface_instance_skia.h Outdated
Comment thread engine/src/flutter/display_list/testing/dl_rendering_unittests.cc Outdated
Comment on lines +909 to +911
static std::string FailureImageDirectory;
static bool SaveFailureImages;
static std::vector<std::string> FailureImageFilenames;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

These don't match naming rules for variables.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The rules are somewhat ambiguous because they define the naming convention for static constant members and define exceptions to that rule. These are likely better served by making the fields private and providing accessor methods for the functionality.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The fields are now private.

Comment thread engine/src/flutter/display_list/testing/dl_rendering_unittests.cc Outdated
Comment thread engine/src/flutter/display_list/testing/dl_rendering_unittests.cc Outdated

std::optional<DlSurfaceProvider::BackendType> DlSurfaceProvider::NameToBackend(
const std::string& name) {
#define PROCESS_BACKEND(name, BACKEND) \

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Does this need to be a macro? Please add a prefix to avoid colliding macros if you keep it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Kept the macro, renamed it.


/// Read back the current contents of the surface and return it as a
/// DlPixelData structure.
virtual std::shared_ptr<DlPixelData> SnapshotToPixelData() const = 0;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Shouldn't this be returning a unique_ptr?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Why? We don't necessarily need uniqueness and you can't share a unique pointer. shared_ptr seems like the best option here so that we don't limit the use of them by the caller.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It's the opposite of that. By returning shared_ptr you are forcing users to have the semantics for multiple owners. unique_ptr's can be implicitly cast to shared_ptr if someone wants a shared_ptr.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

TIL...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This means I can cache the Providers in the test harness to avoid repeated instantiations. I'm not sure if that will make any performance improvement, but it couldn't hurt. Minimally it saves us from having to allocate the primary surface over and over for those tests that iterate over the providers in an inner loop. Future work...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

One down-side to returning a unique_ptr when the caller wants a shared_ptr is that the object is no longer co-allocated with its control block. make_shared does one allocation for both the control block and the object.

@github-actions github-actions Bot removed the CICD Run CI/CD label May 5, 2026
@flar flar added the CICD Run CI/CD label May 5, 2026
@flar flar requested a review from gaaclarke May 5, 2026 10:59

@gaaclarke gaaclarke left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks, changes are looking good. I just have 2 outstanding requests:

  1. Mitigate the IsValid design for the newly added class
  2. return unique_ptr instead of shared_ptr from the factory function

Comment thread engine/src/flutter/display_list/testing/dl_test_surface_provider.cc Outdated
if (pixels_.get()) {
pixmap_.reset(info, pixels_.get(), info.minRowBytes());
if (raster_image->readPixels(pixmap_, 0, 0)) {
is_valid_ = true;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Yea, trying to fix it everywhere is out of scope of this PR. But since we are editing this class we can do the right thing here by introducing the factory method and always returning true for IsValid().

@github-actions github-actions Bot removed the CICD Run CI/CD label May 5, 2026
@flar flar added the CICD Run CI/CD label May 5, 2026
@flar flar requested a review from gaaclarke May 5, 2026 23:58

@gaaclarke gaaclarke left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

thanks jim, lookin good

@flar flar added the autosubmit Merge PR when tree becomes green via auto submit App label May 6, 2026
@auto-submit auto-submit Bot added this pull request to the merge queue May 6, 2026
Merged via the queue into flutter:master with commit 240c85c May 6, 2026
201 of 202 checks passed
@flutter-dashboard flutter-dashboard Bot removed the autosubmit Merge PR when tree becomes green via auto submit App label May 6, 2026
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.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants