Fix P3-to-sRGB color conversion to operate in linear light#181720
Fix P3-to-sRGB color conversion to operate in linear light#181720gaaclarke merged 6 commits intoflutter:masterfrom
Conversation
There was a problem hiding this comment.
Code Review
This pull request correctly fixes a mathematical error in the P3-to-sRGB color conversion by ensuring the transformation operates in linear light, as it should. The changes replace the direct affine matrix multiplication on gamma-encoded values with a proper 3-step pipeline: gamma decoding, applying a 3x3 matrix in linear space, and then gamma encoding. This fix is consistently applied in both C++ (dl_color.cc) and Dart (painting.dart), and the reverse sRGB-to-P3 conversion is also corrected in the Dart implementation. The addition of new unit tests for both C++ and Dart is excellent, as they verify the new conversion logic, particularly for mid-range colors where the previous error was most significant. My review includes suggestions to replace magic numbers with named constants to improve code readability and maintainability.
|
Re-running flutter-dashboard / Mac_arm64 build_tests_4_4 since it looks like a flake Thanks for the PR! |
There was a problem hiding this comment.
This is awesome, thanks @westito. I double checked all the tests with Apple's color utility and they are good. Apple gives slightly different values but that might be captured in the epsilon of the assertions. The test coverage is great, no need to add tests.
In fact there is nothing else you need to do. I've made a few notes about minor cleanup and deduplication which I'll look into. I'll commit them here or implement in a followup.
| // gamma encode. | ||
| DlColor p3ToExtendedSrgb(const DlColor& color) { | ||
| // Linearize P3 values (P3 uses same transfer function as sRGB). | ||
| double r_lin = srgbEOTFExtended(static_cast<double>(color.getRedF())); |
There was a problem hiding this comment.
no action required: do we want to just keep everything in floats?
| double b_lin = srgbEOTFExtended(static_cast<double>(color.getBlueF())); | ||
|
|
||
| // Apply 3x3 P3-to-sRGB matrix in linear space. | ||
| double r_srgb_lin = kP3ToSrgbLinear[0] * r_lin + kP3ToSrgbLinear[1] * g_lin + |
There was a problem hiding this comment.
no action required: we have matrix multiplication available in our api
| matrix[11], // | ||
| color_space); | ||
|
|
||
| // sRGB standard constants for transfer functions. |
There was a problem hiding this comment.
no action required: comments should be in doxygen format
| // Red channel diff: 0.2569 — well above isClose tolerance of 1/256. | ||
| DlColor p3_teal(1.0, 30.0f / 255.0f, 202.0f / 255.0f, 211.0f / 255.0f, | ||
| DlColorSpace::kDisplayP3); | ||
| DlColor expected_teal(1.0, -0.3764f, 0.8065f, 0.8372f, |
There was a problem hiding this comment.
no action required: apple's color tool gives (-0.3778, 0.8084, 0.8358)
|
|
||
| // Converts Display P3 (gamma-encoded) to extended sRGB (gamma-encoded). | ||
| // Pipeline: EOTF(decode) -> 3x3 matrix -> OETF(encode). | ||
| class _P3ToSrgbTransform implements _ColorTransform { |
There was a problem hiding this comment.
no action required: We should call into c++ for this instead of implementing it twice (now that it is a bit more involved)
|
|
||
| class _MatrixColorTransform implements _ColorTransform { | ||
| const _MatrixColorTransform(this.values); | ||
| // sRGB standard constants for transfer functions. |
There was a problem hiding this comment.
no action required: let's find a way to share this with web.
gaaclarke
left a comment
There was a problem hiding this comment.
I looked briefly into deduplicating the logic and it is a bit involved. If dart:ui calls into c++ then the code can't be shared with web. There is also no existing good way to share logic between the web and standard dart:ui so I don't want to try to do it on top of this PR. Everything else is minor nits and LGTM. Thanks again.
|
Thanks! Deduplication may be possible: Color conversion could be done at Dart level when Color is constructed. The problem is, current constructor is constant, any change would cause API break. That's why I chose to implement it at the native level. |
jtmcdole
left a comment
There was a problem hiding this comment.
LGTW (looks good to wikipedia specification)
I found the issue for the slight difference: iOS engine uses |
When the feature was originally implemented we looked into this and there was a noticeable difference in performance from what I recall. The team was reticent to give up any performance for wide gamut colors. We can reevaluate that in the future as things change. There isn't a lot of bandwidth to allocate to that now, especially considering wide gamut for android would be a higher priority in my mind. |
flutter/flutter@1d9d6a9...9eafba4 2026-02-01 [email protected] Roll Skia from a8ceddc2bca0 to 4ee9eb659ae0 (1 revision) (flutter/flutter#181773) 2026-02-01 [email protected] Roll Skia from a5a535bc21a3 to a8ceddc2bca0 (1 revision) (flutter/flutter#181772) 2026-02-01 [email protected] Roll Dart SDK from cde322c518b9 to 84abc8d58ec8 (1 revision) (flutter/flutter#181767) 2026-02-01 [email protected] Roll Dart SDK from c72b3f527a07 to cde322c518b9 (1 revision) (flutter/flutter#181763) 2026-01-31 [email protected] Roll Dart SDK from 43de79f08887 to c72b3f527a07 (1 revision) (flutter/flutter#181762) 2026-01-31 [email protected] Roll Fuchsia Linux SDK from 27OsrsrKcrr2pOaqY... to nI52U4LJMrBv8G1M9... (flutter/flutter#181760) 2026-01-31 [email protected] Roll Dart SDK from a0aac59705e5 to 43de79f08887 (1 revision) (flutter/flutter#181758) 2026-01-31 [email protected] Roll Skia from b704afbcd8e7 to a5a535bc21a3 (1 revision) (flutter/flutter#181757) 2026-01-31 [email protected] Roll Dart SDK from d6f77f9098e1 to a0aac59705e5 (1 revision) (flutter/flutter#181756) 2026-01-31 [email protected] Roll Skia from 6f9511ce11ba to b704afbcd8e7 (4 revisions) (flutter/flutter#181753) 2026-01-31 [email protected] Roll Dart SDK from 0cf997832948 to d6f77f9098e1 (2 revisions) (flutter/flutter#181749) 2026-01-31 [email protected] Roll Skia from 33c00c0f3b8b to 6f9511ce11ba (1 revision) (flutter/flutter#181744) 2026-01-31 [email protected] Roll Dart SDK from be3ffc51d407 to 0cf997832948 (1 revision) (flutter/flutter#181740) 2026-01-30 [email protected] Roll Skia from b62d43b59dd6 to 33c00c0f3b8b (1 revision) (flutter/flutter#181735) 2026-01-30 [email protected] [Impeller] Fix type conversion warnings seen on Windows when Impeller GL API wrappers log arguments with function types (flutter/flutter#181734) 2026-01-30 [email protected] Update New Android API Docs (flutter/flutter#180604) 2026-01-30 [email protected] Roll Fuchsia Linux SDK from isy1ARvK-3bsvtfc-... to 27OsrsrKcrr2pOaqY... (flutter/flutter#181733) 2026-01-30 [email protected] Fix P3-to-sRGB color conversion to operate in linear light (flutter/flutter#181720) 2026-01-30 [email protected] chore: deflake Linux_mokey flutter_engine_group_performance (flutter/flutter#181624) 2026-01-30 [email protected] Roll Dart SDK from 2703fd9733ce to be3ffc51d407 (1 revision) (flutter/flutter#181729) 2026-01-30 [email protected] Ensure content-sizing resize is run on UI thread (flutter/flutter#181686) 2026-01-30 [email protected] [ Tool ] Cleanup `ResidentCompiler` initialization logic (flutter/flutter#181421) 2026-01-30 [email protected] [native assets] Split debug info into `.dsym` files (flutter/flutter#181533) 2026-01-30 [email protected] Roll Skia from 4745eb2fe837 to b62d43b59dd6 (1 revision) (flutter/flutter#181727) 2026-01-30 [email protected] add ccache support for custom toolchain (flutter/flutter#180737) 2026-01-30 [email protected] update GLFW to latest and use EGL context creation on linux (flutter/flutter#181259) 2026-01-30 [email protected] add stacktrace support when requested for host builds (flutter/flutter#181264) If this roll has caused a breakage, revert this CL and stop the roller 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
…81720) <!-- Thanks for filing a pull request! Reviewers are typically assigned within a week of filing a request. To learn more about code review, see our documentation on Tree Hygiene: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md --> ### Problem The P3-to-sRGB color conversion applies an affine matrix directly to gamma-encoded values in both the C++ engine (`dl_color.cc`) and Dart (`painting.dart`). This is mathematically incorrect — color space conversion matrices must operate in linear light. The result is P3 colors that appear nearly identical to sRGB instead of showing the full gamut difference. For example, P3 `#1ECAD3` (30/255, 202/255, 211/255) converts to: - **Old (wrong):** extendedSRGB (-0.12, 0.86, 0.87) - **New (correct):** extendedSRGB (-0.38, 0.81, 0.84) The red channel error of 0.26 is clearly visible — colors appear washed out instead of vivid. ### Fix Replace the single affine matrix multiply with the correct 3-step pipeline: 1. **Linearize** — decode gamma via sRGB EOTF 2. **Transform** — apply 3x3 P3-to-sRGB matrix in linear space 3. **Encode** — re-apply gamma via sRGB OETF Extended range (negative values from out-of-gamut colors) is handled by mirroring the transfer function. **C++ (`dl_color.cc`):** Replace affine matrix with `p3ToExtendedSrgb()` using `double` precision. **Dart (`painting.dart`):** Replace `_MatrixColorTransform` with `_P3ToSrgbTransform` and `_SrgbToP3Transform` classes. Also fixes the sRGB-to-P3 direction. ### Performance Negligible. The C++ conversion runs once per paint setup in `ReadColor`, not per frame or per pixel. The Dart conversion runs once per `Color.withValues()` call. ### Tests - **C++:** Added `ColorSpaceP3ToExtendedSRGBLinearLight` test in `dl_color_unittests.cc` - **Dart:** Added 3 tests in `colors_test.dart`: mid-range P3→extendedSRGB, P3 green→extendedSRGB, sRGB→P3 round-trip All new tests fail with the old code and pass with the fix. Existing tests continue to pass. ## Issue: flutter#181717 ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [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. - [ ] All existing and new tests are passing. **New tests pass, can't run golden tests!** --------- Co-authored-by: gaaclarke <[email protected]>
…81720) <!-- Thanks for filing a pull request! Reviewers are typically assigned within a week of filing a request. To learn more about code review, see our documentation on Tree Hygiene: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md --> ### Problem The P3-to-sRGB color conversion applies an affine matrix directly to gamma-encoded values in both the C++ engine (`dl_color.cc`) and Dart (`painting.dart`). This is mathematically incorrect — color space conversion matrices must operate in linear light. The result is P3 colors that appear nearly identical to sRGB instead of showing the full gamut difference. For example, P3 `#1ECAD3` (30/255, 202/255, 211/255) converts to: - **Old (wrong):** extendedSRGB (-0.12, 0.86, 0.87) - **New (correct):** extendedSRGB (-0.38, 0.81, 0.84) The red channel error of 0.26 is clearly visible — colors appear washed out instead of vivid. ### Fix Replace the single affine matrix multiply with the correct 3-step pipeline: 1. **Linearize** — decode gamma via sRGB EOTF 2. **Transform** — apply 3x3 P3-to-sRGB matrix in linear space 3. **Encode** — re-apply gamma via sRGB OETF Extended range (negative values from out-of-gamut colors) is handled by mirroring the transfer function. **C++ (`dl_color.cc`):** Replace affine matrix with `p3ToExtendedSrgb()` using `double` precision. **Dart (`painting.dart`):** Replace `_MatrixColorTransform` with `_P3ToSrgbTransform` and `_SrgbToP3Transform` classes. Also fixes the sRGB-to-P3 direction. ### Performance Negligible. The C++ conversion runs once per paint setup in `ReadColor`, not per frame or per pixel. The Dart conversion runs once per `Color.withValues()` call. ### Tests - **C++:** Added `ColorSpaceP3ToExtendedSRGBLinearLight` test in `dl_color_unittests.cc` - **Dart:** Added 3 tests in `colors_test.dart`: mid-range P3→extendedSRGB, P3 green→extendedSRGB, sRGB→P3 round-trip All new tests fail with the old code and pass with the fix. Existing tests continue to pass. ## Issue: flutter#181717 ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [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. - [ ] All existing and new tests are passing. **New tests pass, can't run golden tests!** --------- Co-authored-by: gaaclarke <[email protected]>
…81720) <!-- Thanks for filing a pull request! Reviewers are typically assigned within a week of filing a request. To learn more about code review, see our documentation on Tree Hygiene: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md --> ### Problem The P3-to-sRGB color conversion applies an affine matrix directly to gamma-encoded values in both the C++ engine (`dl_color.cc`) and Dart (`painting.dart`). This is mathematically incorrect — color space conversion matrices must operate in linear light. The result is P3 colors that appear nearly identical to sRGB instead of showing the full gamut difference. For example, P3 `#1ECAD3` (30/255, 202/255, 211/255) converts to: - **Old (wrong):** extendedSRGB (-0.12, 0.86, 0.87) - **New (correct):** extendedSRGB (-0.38, 0.81, 0.84) The red channel error of 0.26 is clearly visible — colors appear washed out instead of vivid. ### Fix Replace the single affine matrix multiply with the correct 3-step pipeline: 1. **Linearize** — decode gamma via sRGB EOTF 2. **Transform** — apply 3x3 P3-to-sRGB matrix in linear space 3. **Encode** — re-apply gamma via sRGB OETF Extended range (negative values from out-of-gamut colors) is handled by mirroring the transfer function. **C++ (`dl_color.cc`):** Replace affine matrix with `p3ToExtendedSrgb()` using `double` precision. **Dart (`painting.dart`):** Replace `_MatrixColorTransform` with `_P3ToSrgbTransform` and `_SrgbToP3Transform` classes. Also fixes the sRGB-to-P3 direction. ### Performance Negligible. The C++ conversion runs once per paint setup in `ReadColor`, not per frame or per pixel. The Dart conversion runs once per `Color.withValues()` call. ### Tests - **C++:** Added `ColorSpaceP3ToExtendedSRGBLinearLight` test in `dl_color_unittests.cc` - **Dart:** Added 3 tests in `colors_test.dart`: mid-range P3→extendedSRGB, P3 green→extendedSRGB, sRGB→P3 round-trip All new tests fail with the old code and pass with the fix. Existing tests continue to pass. ## Issue: flutter#181717 ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [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. - [ ] All existing and new tests are passing. **New tests pass, can't run golden tests!** --------- Co-authored-by: gaaclarke <[email protected]>
Problem
The P3-to-sRGB color conversion applies an affine matrix directly to gamma-encoded values in both the C++ engine (
dl_color.cc) and Dart (painting.dart). This is mathematically incorrect — color space conversion matrices must operate in linear light. The result is P3 colors that appear nearly identical to sRGB instead of showing the full gamut difference.For example, P3
#1ECAD3(30/255, 202/255, 211/255) converts to:The red channel error of 0.26 is clearly visible — colors appear washed out instead of vivid.
Fix
Replace the single affine matrix multiply with the correct 3-step pipeline:
Extended range (negative values from out-of-gamut colors) is handled by mirroring the transfer function.
C++ (
dl_color.cc): Replace affine matrix withp3ToExtendedSrgb()usingdoubleprecision.Dart (
painting.dart): Replace_MatrixColorTransformwith_P3ToSrgbTransformand_SrgbToP3Transformclasses. Also fixes the sRGB-to-P3 direction.Performance
Negligible. The C++ conversion runs once per paint setup in
ReadColor, not per frame or per pixel. The Dart conversion runs once perColor.withValues()call.Tests
ColorSpaceP3ToExtendedSRGBLinearLighttest indl_color_unittests.cccolors_test.dart: mid-range P3→extendedSRGB, P3 green→extendedSRGB, sRGB→P3 round-tripAll new tests fail with the old code and pass with the fix. Existing tests continue to pass.
Issue:
#181717
Pre-launch Checklist
///).