Skip to content
This repository has been archived by the owner on Feb 25, 2025. It is now read-only.

Framework wide color linear gradients #54748

Merged

Conversation

gaaclarke
Copy link
Member

@gaaclarke gaaclarke commented Aug 23, 2024

This implements wide gamut color support for gradients in the engine.

issue: flutter/flutter#127855
integration test: flutter/flutter#153976

Pre-launch Checklist

  • I read the Contributor Guide and followed the process outlined there for submitting PRs.
  • I read the Tree Hygiene wiki page, which explains my responsibilities.
  • I read and followed the Flutter Style Guide and the C++, Objective-C, Java style guides.
  • I listed at least one issue that this PR fixes in the description above.
  • I added new tests to check the change I am making or feature I am adding, or the PR is test-exempt. See testing the engine for instructions on writing and running engine tests.
  • I updated/added relevant documentation (doc comments with ///).
  • I signed the CLA.
  • All existing and new tests are passing.

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

@github-actions github-actions bot added the platform-web Code specifically for the web engine label Aug 23, 2024
@gaaclarke gaaclarke force-pushed the framework-wide-color-linear-gradients branch from cad3ee7 to bfaefe5 Compare August 29, 2024 21:45
@gaaclarke gaaclarke force-pushed the framework-wide-color-linear-gradients branch from bfaefe5 to 5c0887f Compare September 3, 2024 16:48
@gaaclarke gaaclarke marked this pull request as ready for review September 3, 2024 19:58
@gaaclarke
Copy link
Member Author

@jonahwilliams @flar this PR relies on the integration test for verification. Let me know if there is a more direct way in the engine you'd like to see it tested. canvas_test.dart does execute this code.

for (int i = 0; i < colors.num_elements(); ++i) {
/// TODO(gaaclarke): Make this preserve wide gamut colors.
dl_colors.emplace_back(DlColor(colors[i]));
dl_colors.reserve(color_stops.num_elements());
Copy link
Member

Choose a reason for hiding this comment

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

Can't this go back to a cast now? We're copying the data into the display list again so we shouldn't need to allocate more storage.

Copy link
Member

Choose a reason for hiding this comment

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

Oh, we can't cast it anymore since the color space field.

In that case, can we update this API so we reserve and write the color bytes directly into the DL storage instead of allocating an intermediary?

Copy link
Member Author

Choose a reason for hiding this comment

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

In order to have a cast the memory layouts need to be identical. Here we have an array of floats, we need to insert the colorspace field into the memory layout if we wanted to use casts. The casts are super frail. I don't recommend it unless we get some data that supports it's worth the risk.

If we did find it was slowing us down, I would recommend doing the conversion on the stack when the number of colors is less than a certain size to catch most use cases before resorting to an unsafe cast.

Copy link
Member Author

Choose a reason for hiding this comment

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

Something like this:

if (num_colors <= 3) {
  std::array<DlColor, 3> dl_colors;
  //...
} else {
  std::vector<DlColor> dl_colors;
  dl_colors.reserve(num_colors);
  //...
}

I don't think it's worth the extra complexity, but I can do it if we want. I know we were bit in the past with too many heap allocations.

Copy link
Member

Choose a reason for hiding this comment

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

All this code is doing is passing the data into the DLBuilder so it can be copied into the display list. Can we copy into the final destination instead of allocating an intermediary?

Copy link
Contributor

Choose a reason for hiding this comment

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

I think I'm gradually absorbing the concern here. Talk of "DL" has me thinking of the display list object itself with recorded operations, but I think the concern here is that DlFooGradientColorSource embeds the data into a contiguous chunk and we are converting from one array to a temp vector of DlColor objects and then into the data co-located with the eventual DlColorSource object.

So, yes, it would be nice if we could copy directly from the constructor/factory argument into the pod allocation without first creating a vector of DlColor objects. I'd be happy to see that added as a future optimization and just stick with making some sort of list of DlColor objects for now to get this capability implemented in its simplest fashion.

We could also consider having the storage for these gradient objects be just an array of floats and a separate common CS. The gradient objects could still get you a single color in a getter by marrying the right 4 floats and the CS into a DlColor object, but there would also be accessors to the common CS value and the array of component float data which the backend might use more efficiently. It would also save on storage size.

Copy link
Contributor

Choose a reason for hiding this comment

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

Also, I will be coming through these classes to de-skia-fy them shortly, so I can implement such optimizations as I do that work. That work should all fall under "should have no impact at all on output" changes and so would hopefully pass up the chain without regressions.

Copy link
Member Author

Choose a reason for hiding this comment

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

I'd prefer we use a named factory so that it is clear what order the scalars are supposed to be in the array.

That's what I'm doing I'm refactoring DlColorSource to use the Builder pattern. That way we can pass in DlColor arrays or float arrays.

The logic that I'm asking you to add does not need an intergration test to cover it (in fact it may already be tested)

This PR is adding new functionality that is only tested in an integration test that has to live in another repo. This PR implements the feature while working within the bounds of the current API. I agree with your feedback and it will be addressed. That's why we went through the whole process of brainstorming a solution. However this PR does not add any new heap allocations, they were already there, removing them is going to be a bit of a refactor, one who would be served by having a test as a backstop to make sure we don't break the thing we are actually trying to implement.

We can withhold the approval of this PR until you see the refactor PR if you want. I don't understand why I'm not being trusted here.

Copy link
Member

Choose a reason for hiding this comment

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

Removing an unnecessary heap allocation does not require an integration test

Copy link
Member Author

Choose a reason for hiding this comment

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

Moving to the functions to DlScalar* is not going to work because there are 2 clients of DlLinearGradientColorSource::MakeLinear. In one case we have DlScalar*, in another we have DlColor*. Moving to DlScalar would require us to do heap allocations when converting in the other route. The other client is DlColorSource::shared(), I tried removing it and that runs deep coming from DlAttribute.

The only viable solution is to use the iterator pattern if we want to eliminate heap allocations in both cases. That would require moving more logic to templates or a dynamic dispatch, which would possibly eliminate any benefit of removing the tiny heap allocation.

I think the best bet is to eliminate the heap in most cases but have an overflow for gradients with large amounts of colors.

for (int i = 0; i < colors.num_elements(); ++i) {
/// TODO(gaaclarke): Make this preserve wide gamut colors.
dl_colors.emplace_back(DlColor(colors[i]));
dl_colors.reserve(num_colors);
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we have a helper method, or would that involve unnecessary copying? I thought the compiler optimized the case of returning a vector...??

Another way is to create the vector on the local stack and then hand it into the converter method by reference.

Copy link
Member Author

Choose a reason for hiding this comment

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

Can we have a helper method, or would that involve unnecessary copying? I thought the compiler optimized the case of returning a vector...??

Another way is to create the vector on the local stack and then hand it into the converter method by reference.

You can't have a vector on a stack. It will always allocate memory on the heap. The size is needed to be known at compilation time in order to put something on the stack (without trickery). I have a proposition in an above thread for using an std::array on the stack.

I think avoiding the copying will require DlColorSource some mechanism to read from the tonic collection directly.

Copy link
Contributor

Choose a reason for hiding this comment

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

The data has to be heap allocated because it is growable, that will happen whether a helper function is used or not. I'm referring to the object itself, but I suppose that the object isn't very big whether it is stack allocated or not. It's a vtable + data pointer + allocated size + used size.

Copy link
Member Author

Choose a reason for hiding this comment

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

Oh, I see, sorry I thought this was still related to the heap discussion. Yes we could have a helper function and yes C++ would perform the return value optimization.

@@ -4452,10 +4452,25 @@ enum TileMode {
decal,
}

Float32List _encodeWideColorList(List<Color> colors) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I remember talking about preserving the CS, but I'm guessing that gradients are going to want to normalize to a similar CS for lerping - and extendedSRGB is probably the best for fractional combinations?

Copy link
Member Author

Choose a reason for hiding this comment

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

We do preserve the colorspace from the users perspective. The conversion to extended srgb only happens when communicating the values to the engine.

@flar
Copy link
Contributor

flar commented Sep 3, 2024

@jonahwilliams @flar this PR relies on the integration test for verification. Let me know if there is a more direct way in the engine you'd like to see it tested. canvas_test.dart does execute this code.

Are there tests that test ui.Color(...) == DlColor(...)?

@gaaclarke
Copy link
Member Author

gaaclarke commented Sep 3, 2024

@jonahwilliams @flar this PR relies on the integration test for verification. Let me know if there is a more direct way in the engine you'd like to see it tested. canvas_test.dart does execute this code.

Are there tests that test ui.Color(...) == DlColor(...)?

The test Simple gradient, which is implicitly dithered in canvas_test.dart does check ui.Color versus a golden image.

edit: link -

test('Simple gradient, which is implicitly dithered', () async {

Copy link
Member

@jonahwilliams jonahwilliams left a comment

Choose a reason for hiding this comment

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

LGTM

@gaaclarke and I looked at this offline. I agree that the refactor can probably wait on this landing in the framework + other testing work. @flar would you be available to pair with @gaaclarke and come up with a way to remove the intermediate allocation?

@gaaclarke gaaclarke added the autosubmit Merge PR when tree becomes green via auto submit App label Sep 4, 2024
@auto-submit auto-submit bot merged commit 7418367 into flutter:main Sep 4, 2024
32 checks passed
engine-flutter-autoroll added a commit to engine-flutter-autoroll/flutter that referenced this pull request Sep 5, 2024
auto-submit bot pushed a commit to flutter/flutter that referenced this pull request Sep 5, 2024
…154653)

flutter/engine@4a01a3a...652a43b

2024-09-04 [email protected] Manual roll Dart SDK from 139867e57ba9 to 1a6246225b75 (18 revisions) (flutter/engine#54962)
2024-09-04 [email protected] Framework wide color linear gradients (flutter/engine#54748)
2024-09-04 [email protected] Add more `package:test` (removing usages of `package:litetest`) (flutter/engine#54882)

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

To file a bug in Flutter: 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
auto-submit bot pushed a commit to flutter/flutter that referenced this pull request Oct 25, 2024
issue: #127855
depends on flutter/engine#54748 being rolled into the framework
auto-submit bot added a commit to flutter/flutter that referenced this pull request Oct 25, 2024
Reverts: #153976
Initiated by: jonahwilliams
Reason for reverting: failing on postsubmit
Original PR Author: gaaclarke

Reviewed By: {jonahwilliams}

This change reverts the following previous change:
issue: #127855
depends on flutter/engine#54748 being rolled into the framework
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
autosubmit Merge PR when tree becomes green via auto submit App e: impeller platform-web Code specifically for the web engine
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants