Protect GPU composited HDR images from the Windows brightness slider.

This adds code to the GPU composited image paths to treat decoded HDR
images as having an SDR white level equivalent to the display's during
upload. Currently all GPU compositing paths but RGB w/ in-process
rasterization work (due to https://crbug.com/1120719)

Bug: 1112437
Test: New unittests.
Change-Id: I66a7b54e542d845b35a27422076a373d985226a8
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2368286
Commit-Queue: Dale Curtis <dalecurtis@chromium.org>
Reviewed-by: ccameron <ccameron@chromium.org>
Reviewed-by: Khushal <khushalsagar@chromium.org>
Auto-Submit: Dale Curtis <dalecurtis@chromium.org>
Cr-Commit-Position: refs/heads/master@{#801578}
diff --git a/cc/paint/draw_image.cc b/cc/paint/draw_image.cc
index 8e920f6c..afa344d 100644
--- a/cc/paint/draw_image.cc
+++ b/cc/paint/draw_image.cc
@@ -4,6 +4,8 @@
 
 #include "cc/paint/draw_image.h"
 
+#include <utility>
+
 namespace cc {
 namespace {
 
@@ -41,19 +43,22 @@
                      SkFilterQuality filter_quality,
                      const SkMatrix& matrix,
                      base::Optional<size_t> frame_index,
-                     const base::Optional<gfx::ColorSpace>& color_space)
+                     const base::Optional<gfx::ColorSpace>& color_space,
+                     float sdr_white_level)
     : paint_image_(std::move(image)),
       src_rect_(src_rect),
       filter_quality_(filter_quality),
       frame_index_(frame_index),
-      target_color_space_(color_space) {
+      target_color_space_(color_space),
+      sdr_white_level_(sdr_white_level) {
   matrix_is_decomposable_ = ExtractScale(matrix, &scale_);
 }
 
 DrawImage::DrawImage(const DrawImage& other,
                      float scale_adjustment,
                      size_t frame_index,
-                     const gfx::ColorSpace& color_space)
+                     const gfx::ColorSpace& color_space,
+                     float sdr_white_level)
     : paint_image_(other.paint_image_),
       src_rect_(other.src_rect_),
       filter_quality_(other.filter_quality_),
@@ -61,7 +66,11 @@
                           other.scale_.height() * scale_adjustment)),
       matrix_is_decomposable_(other.matrix_is_decomposable_),
       frame_index_(frame_index),
-      target_color_space_(color_space) {}
+      target_color_space_(color_space),
+      sdr_white_level_(sdr_white_level) {
+  if (sdr_white_level_ == gfx::ColorSpace::kDefaultSDRWhiteLevel)
+    sdr_white_level_ = other.sdr_white_level_;
+}
 
 DrawImage::DrawImage(const DrawImage& other) = default;
 DrawImage::DrawImage(DrawImage&& other) = default;
@@ -74,7 +83,8 @@
   return paint_image_ == other.paint_image_ && src_rect_ == other.src_rect_ &&
          filter_quality_ == other.filter_quality_ && scale_ == other.scale_ &&
          matrix_is_decomposable_ == other.matrix_is_decomposable_ &&
-         target_color_space_ == other.target_color_space_;
+         target_color_space_ == other.target_color_space_ &&
+         sdr_white_level_ == other.sdr_white_level_;
 }
 
 }  // namespace cc
diff --git a/cc/paint/draw_image.h b/cc/paint/draw_image.h
index c1c6957b..984d85ad 100644
--- a/cc/paint/draw_image.h
+++ b/cc/paint/draw_image.h
@@ -31,13 +31,15 @@
             SkFilterQuality filter_quality,
             const SkMatrix& matrix,
             base::Optional<size_t> frame_index = base::nullopt,
-            const base::Optional<gfx::ColorSpace>& color_space = base::nullopt);
+            const base::Optional<gfx::ColorSpace>& color_space = base::nullopt,
+            float sdr_white_level = gfx::ColorSpace::kDefaultSDRWhiteLevel);
   // Constructs a DrawImage from |other| by adjusting its scale and setting a
   // new color_space.
   DrawImage(const DrawImage& other,
             float scale_adjustment,
             size_t frame_index,
-            const gfx::ColorSpace& color_space);
+            const gfx::ColorSpace& color_space,
+            float sdr_white_level = gfx::ColorSpace::kDefaultSDRWhiteLevel);
   DrawImage(const DrawImage& other);
   DrawImage(DrawImage&& other);
   ~DrawImage();
@@ -63,6 +65,7 @@
     DCHECK(frame_index_.has_value());
     return frame_index_.value();
   }
+  float sdr_white_level() const { return sdr_white_level_; }
 
  private:
   PaintImage paint_image_;
@@ -72,6 +75,12 @@
   bool matrix_is_decomposable_;
   base::Optional<size_t> frame_index_;
   base::Optional<gfx::ColorSpace> target_color_space_;
+
+  // The SDR white level in nits for the display. Only if |target_color_space_|
+  // is HDR will this have a value other than kDefaultSDRWhiteLevel. Used by the
+  // ImageDecodeCache to prevent HDR images from being affected by variable SDR
+  // white levels since rasterization is always treated as SDR at present.
+  float sdr_white_level_ = gfx::ColorSpace::kDefaultSDRWhiteLevel;
 };
 
 }  // namespace cc
diff --git a/cc/test/fake_tile_manager_client.cc b/cc/test/fake_tile_manager_client.cc
index 355736f..75147a59 100644
--- a/cc/test/fake_tile_manager_client.cc
+++ b/cc/test/fake_tile_manager_client.cc
@@ -28,6 +28,10 @@
   return color_space_;
 }
 
+float FakeTileManagerClient::GetSDRWhiteLevel() const {
+  return gfx::ColorSpace::kDefaultSDRWhiteLevel;
+}
+
 size_t FakeTileManagerClient::GetFrameIndexForImage(
     const PaintImage& paint_image,
     WhichTree tree) const {
diff --git a/cc/test/fake_tile_manager_client.h b/cc/test/fake_tile_manager_client.h
index 6fb3bd4c..c9c21b7 100644
--- a/cc/test/fake_tile_manager_client.h
+++ b/cc/test/fake_tile_manager_client.h
@@ -5,6 +5,7 @@
 #ifndef CC_TEST_FAKE_TILE_MANAGER_CLIENT_H_
 #define CC_TEST_FAKE_TILE_MANAGER_CLIENT_H_
 
+#include <memory>
 #include <vector>
 
 #include "cc/tiles/tile_manager.h"
@@ -29,6 +30,7 @@
   void SetIsLikelyToRequireADraw(bool is_likely_to_require_a_draw) override {}
   gfx::ColorSpace GetRasterColorSpace(
       gfx::ContentColorUsage content_color_usage) const override;
+  float GetSDRWhiteLevel() const override;
   void RequestImplSideInvalidationForCheckerImagedTiles() override {}
   size_t GetFrameIndexForImage(const PaintImage& paint_image,
                                WhichTree tree) const override;
diff --git a/cc/tiles/gpu_image_decode_cache.cc b/cc/tiles/gpu_image_decode_cache.cc
index a2875b8..1bb3886 100644
--- a/cc/tiles/gpu_image_decode_cache.cc
+++ b/cc/tiles/gpu_image_decode_cache.cc
@@ -2071,6 +2071,12 @@
     color_space = nullptr;
   }
 
+  // Will be nullptr for non-HDR images or when we're using the default level.
+  const bool needs_adjusted_color_space =
+      NeedsColorSpaceAdjustedForUpload(draw_image);
+  if (needs_adjusted_color_space)
+    decoded_target_colorspace = ColorSpaceForImageUpload(draw_image);
+
   if (image_data->mode == DecodedDataMode::kTransferCache) {
     DCHECK(use_transfer_cache_);
     if (image_data->decode.do_hardware_accelerated_decode()) {
@@ -2132,6 +2138,9 @@
       SkPixmap pixmap;
       if (!image_data->decode.image()->peekPixels(&pixmap))
         return;
+      if (needs_adjusted_color_space)
+        pixmap.setColorSpace(decoded_target_colorspace);
+
       ClientImageTransferCacheEntry image_entry(&pixmap, color_space.get(),
                                                 image_data->needs_mips);
       InsertTransferCacheEntry(image_entry, image_data);
@@ -2234,6 +2243,12 @@
     return;
   }
 
+  // TODO(crbug.com/1120719): The RGBX path is broken for HDR images.
+  if (needs_adjusted_color_space) {
+    uploaded_image =
+        uploaded_image->reinterpretColorSpace(decoded_target_colorspace);
+  }
+
   // RGBX decoding is below.
   // For kGpu, we upload and color convert (if necessary).
   if (image_data->mode == DecodedDataMode::kGpu) {
@@ -2846,6 +2861,21 @@
   return sk_ref_sp(image.paint_image().color_space());
 }
 
+bool GpuImageDecodeCache::NeedsColorSpaceAdjustedForUpload(
+    const DrawImage& image) const {
+  return image.sdr_white_level() != gfx::ColorSpace::kDefaultSDRWhiteLevel &&
+         image.paint_image().GetContentColorUsage() ==
+             gfx::ContentColorUsage::kHDR;
+}
+
+sk_sp<SkColorSpace> GpuImageDecodeCache::ColorSpaceForImageUpload(
+    const DrawImage& image) const {
+  DCHECK(NeedsColorSpaceAdjustedForUpload(image));
+  return gfx::ColorSpace(*image.paint_image().color_space())
+      .GetWithSDRWhiteLevel(image.sdr_white_level())
+      .ToSkColorSpace();
+}
+
 void GpuImageDecodeCache::CheckContextLockAcquiredIfNecessary() {
   if (!context_->GetLock())
     return;
@@ -2967,15 +2997,17 @@
                 draw_image.target_color_space().IsValid()
             ? draw_image.target_color_space().ToSkColorSpace()
             : nullptr;
-    sk_sp<SkColorSpace> decoded_color_space =
-        ColorSpaceForImageDecode(draw_image, image_data->mode);
+    sk_sp<SkColorSpace> upload_color_space =
+        NeedsColorSpaceAdjustedForUpload(draw_image)
+            ? ColorSpaceForImageUpload(draw_image)
+            : ColorSpaceForImageDecode(draw_image, image_data->mode);
     DCHECK(image_data->yuv_color_space.has_value());
     sk_sp<SkImage> yuv_image_with_mips_owned =
         CreateImageFromYUVATexturesInternal(
             image_y_with_mips_owned.get(), image_u_with_mips_owned.get(),
             image_v_with_mips_owned.get(), width, height,
             image_data->yuv_color_space.value(), color_space,
-            decoded_color_space);
+            upload_color_space);
     // In case of lost context
     if (!yuv_image_with_mips_owned) {
       DLOG(WARNING) << "TODO(crbug.com/740737): Context was lost. Early out.";
@@ -3008,6 +3040,15 @@
   // delete, delaying deletion.
   sk_sp<SkImage> previous_image = image_data->upload.image();
 
+#if DCHECK_IS_ON()
+  // For already uploaded images, the correct color space should already have
+  // been set during the upload process.
+  if (NeedsColorSpaceAdjustedForUpload(draw_image)) {
+    DCHECK(SkColorSpace::Equals(previous_image->colorSpace(),
+                                ColorSpaceForImageUpload(draw_image).get()));
+  }
+#endif
+
   // Generate a new image from the previous, adding mips.
   sk_sp<SkImage> image_with_mips = previous_image->makeTextureImage(
       context_->GrContext(), GrMipMapped::kYes);
diff --git a/cc/tiles/gpu_image_decode_cache.h b/cc/tiles/gpu_image_decode_cache.h
index bdf38e5..a65ba526 100644
--- a/cc/tiles/gpu_image_decode_cache.h
+++ b/cc/tiles/gpu_image_decode_cache.h
@@ -675,6 +675,11 @@
   sk_sp<SkColorSpace> ColorSpaceForImageDecode(const DrawImage& image,
                                                DecodedDataMode mode) const;
 
+  // HDR images need the SkColorSpace adjusted during upload to avoid white
+  // level issues on systems with variable SDR white levels (Windows).
+  bool NeedsColorSpaceAdjustedForUpload(const DrawImage& image) const;
+  sk_sp<SkColorSpace> ColorSpaceForImageUpload(const DrawImage& image) const;
+
   // Helper function to add a memory dump to |pmd| for a single texture
   // identified by |gl_id| with size |bytes| and |locked_size| equal to either
   // |bytes| or 0 depending on whether the texture is currently locked.
diff --git a/cc/tiles/gpu_image_decode_cache_unittest.cc b/cc/tiles/gpu_image_decode_cache_unittest.cc
index be5c137..0cf8325 100644
--- a/cc/tiles/gpu_image_decode_cache_unittest.cc
+++ b/cc/tiles/gpu_image_decode_cache_unittest.cc
@@ -468,7 +468,8 @@
       gfx::ColorSpace* color_space = nullptr,
       SkFilterQuality filter_quality = kMedium_SkFilterQuality,
       SkIRect* src_rect = nullptr,
-      size_t frame_index = PaintImage::kDefaultFrameIndex) {
+      size_t frame_index = PaintImage::kDefaultFrameIndex,
+      float sdr_white_level = gfx::ColorSpace::kDefaultSDRWhiteLevel) {
     SkIRect src_rectangle;
     gfx::ColorSpace cs;
     if (!src_rect) {
@@ -481,7 +482,7 @@
       color_space = &cs;
     }
     return DrawImage(paint_image, *src_rect, filter_quality, matrix,
-                     frame_index, *color_space);
+                     frame_index, *color_space, sdr_white_level);
   }
 
   GPUImageDecodeTestMockContextProvider* context_provider() {
@@ -573,7 +574,8 @@
       const DrawImage& draw_image,
       const base::Optional<uint32_t> transfer_cache_id,
       const SkISize plane_sizes[SkYUVASizeInfo::kMaxCount],
-      SkColorType expected_type = kGray_8_SkColorType) {
+      SkColorType expected_type = kGray_8_SkColorType,
+      const SkColorSpace* expected_cs = nullptr) {
     for (size_t i = 0; i < SkYUVASizeInfo::kMaxCount; ++i) {
       // TODO(crbug.com/910276): Skip alpha plane until supported in cache.
       if (i != SkYUVAIndex::kA_Index) {
@@ -591,6 +593,12 @@
         ASSERT_TRUE(uploaded_plane);
         EXPECT_EQ(plane_sizes[i], uploaded_plane->dimensions());
         EXPECT_EQ(expected_type, uploaded_plane->colorType());
+        if (expected_cs && use_transfer_cache_) {
+          EXPECT_TRUE(
+              SkColorSpace::Equals(expected_cs, uploaded_plane->colorSpace()));
+        } else if (expected_cs) {
+          // In-process raster sets the ColorSpace on the composite SkImage.
+        }
       }
     }
   }
@@ -1104,8 +1112,11 @@
                                     PaintImage::GetNextContentId())
                          .TakePaintImage();
 
+  constexpr float kCustomWhiteLevel = 200.f;
   DrawImage draw_image = CreateDrawImageInternal(
-      image, CreateMatrix(SkSize::Make(0.5f, 0.5f)), &color_space);
+      image, CreateMatrix(SkSize::Make(0.5f, 0.5f)), &color_space,
+      kMedium_SkFilterQuality, nullptr, PaintImage::kDefaultFrameIndex,
+      kCustomWhiteLevel);
   ImageDecodeCache::TaskResult result =
       cache->GetTaskForImageAndRef(draw_image, ImageDecodeCache::TracingInfo());
   EXPECT_EQ(draw_image.target_color_space(), color_space);
@@ -1127,6 +1138,11 @@
   EXPECT_TRUE(decoded_draw_image.is_budgeted());
   EXPECT_EQ(decoded_draw_image.image()->colorType(), kRGBA_F16_SkColorType);
 
+  auto cs = gfx::ColorSpace(*decoded_draw_image.image()->colorSpace());
+  float sdr_white_level;
+  ASSERT_TRUE(cs.GetPQSDRWhiteLevel(&sdr_white_level));
+  EXPECT_FLOAT_EQ(sdr_white_level, kCustomWhiteLevel);
+
   EXPECT_FALSE(cache->DiscardableIsLockedForTesting(draw_image));
 
   cache->DrawWithImageFinished(draw_image, decoded_draw_image);
@@ -2974,20 +2990,32 @@
   }
 
   auto decode_and_check_plane_sizes = [this](GpuImageDecodeCache* cache,
-                                             SkColorType yuv_color_type) {
+                                             SkColorType yuv_color_type,
+                                             gfx::ColorSpace target_cs) {
     SkFilterQuality filter_quality = kMedium_SkFilterQuality;
     SkSize requires_decode_at_original_scale = SkSize::Make(0.8f, 0.8f);
 
+    // When we're targeting HDR output, select a reasonable HDR color space for
+    // the decoded content.
+    gfx::ColorSpace decoded_cs;
+    if (target_cs.IsHDR())
+      decoded_cs = gfx::ColorSpace::CreateHDR10();
+
     // An unknown SkColorType means we expect fallback to RGB.
     PaintImage image =
         yuv_color_type == kUnknown_SkColorType
             ? CreatePaintImageForFallbackToRGB(GetNormalImageSize())
-            : CreatePaintImageInternal(GetNormalImageSize());
+            : CreatePaintImageInternal(GetNormalImageSize(),
+                                       decoded_cs.ToSkColorSpace());
 
-    DrawImage draw_image(image, SkIRect::MakeWH(image.width(), image.height()),
-                         filter_quality,
-                         CreateMatrix(requires_decode_at_original_scale),
-                         PaintImage::kDefaultFrameIndex, DefaultColorSpace());
+    float sdr_white_level = gfx::ColorSpace::kDefaultSDRWhiteLevel;
+    if (target_cs.IsHDR())
+      ASSERT_TRUE(target_cs.GetPQSDRWhiteLevel(&sdr_white_level));
+
+    DrawImage draw_image(
+        image, SkIRect::MakeWH(image.width(), image.height()), filter_quality,
+        CreateMatrix(requires_decode_at_original_scale),
+        PaintImage::kDefaultFrameIndex, target_cs, sdr_white_level);
     ImageDecodeCache::TaskResult result = cache->GetTaskForImageAndRef(
         draw_image, ImageDecodeCache::TracingInfo());
     EXPECT_TRUE(result.need_unref);
@@ -3019,8 +3047,22 @@
                                        true /* should_have_mips */);
       SkYUVASizeInfo yuv_size_info = GetYUVASizeInfo(
           GetNormalImageSize(), yuv_format_, yuv_bytes_per_pixel_);
+
+      // Decoded HDR images should have their SDR white level adjusted to match
+      // the display so we avoid scaling them by variable SDR brightness levels.
+      auto expected_cs = decoded_cs.IsHDR()
+                             ? decoded_cs.GetWithSDRWhiteLevel(sdr_white_level)
+                             : decoded_cs;
+
       VerifyUploadedPlaneSizes(cache, draw_image, transfer_cache_entry_id,
-                               yuv_size_info.fSizes, yuv_color_type);
+                               yuv_size_info.fSizes, yuv_color_type,
+                               expected_cs.ToSkColorSpace().get());
+
+      if (expected_cs.IsValid()) {
+        EXPECT_TRUE(
+            SkColorSpace::Equals(expected_cs.ToSkColorSpace().get(),
+                                 decoded_draw_image.image()->colorSpace()));
+      }
     } else {
       if (use_transfer_cache_) {
         EXPECT_FALSE(transfer_cache_helper_
@@ -3048,6 +3090,8 @@
     original_caps = context_provider_->ContextCapabilities();
   }
 
+  const auto hdr_cs = gfx::ColorSpace::CreateHDR10(/*sdr_white_level=*/200.0f);
+
   // Ensure that when R16 is supported, it's used and preferred over half-float.
   {
     auto r16_caps = original_caps;
@@ -3057,13 +3101,29 @@
     auto r16_cache = CreateCache();
 
     yuv_format_ = YUVSubsampling::k420;
-    decode_and_check_plane_sizes(r16_cache.get(), kA16_unorm_SkColorType);
+    decode_and_check_plane_sizes(r16_cache.get(), kA16_unorm_SkColorType,
+                                 DefaultColorSpace());
 
     yuv_format_ = YUVSubsampling::k422;
-    decode_and_check_plane_sizes(r16_cache.get(), kA16_unorm_SkColorType);
+    decode_and_check_plane_sizes(r16_cache.get(), kA16_unorm_SkColorType,
+                                 DefaultColorSpace());
 
     yuv_format_ = YUVSubsampling::k444;
-    decode_and_check_plane_sizes(r16_cache.get(), kA16_unorm_SkColorType);
+    decode_and_check_plane_sizes(r16_cache.get(), kA16_unorm_SkColorType,
+                                 DefaultColorSpace());
+
+    // Verify HDR decoding has white level adjustment.
+    yuv_format_ = YUVSubsampling::k420;
+    decode_and_check_plane_sizes(r16_cache.get(), kA16_unorm_SkColorType,
+                                 hdr_cs);
+
+    yuv_format_ = YUVSubsampling::k422;
+    decode_and_check_plane_sizes(r16_cache.get(), kA16_unorm_SkColorType,
+                                 hdr_cs);
+
+    yuv_format_ = YUVSubsampling::k444;
+    decode_and_check_plane_sizes(r16_cache.get(), kA16_unorm_SkColorType,
+                                 hdr_cs);
   }
 
   // Verify that half-float is used when R16 is not available.
@@ -3075,13 +3135,29 @@
     auto f16_cache = CreateCache();
 
     yuv_format_ = YUVSubsampling::k420;
-    decode_and_check_plane_sizes(f16_cache.get(), kA16_float_SkColorType);
+    decode_and_check_plane_sizes(f16_cache.get(), kA16_float_SkColorType,
+                                 DefaultColorSpace());
 
     yuv_format_ = YUVSubsampling::k422;
-    decode_and_check_plane_sizes(f16_cache.get(), kA16_float_SkColorType);
+    decode_and_check_plane_sizes(f16_cache.get(), kA16_float_SkColorType,
+                                 DefaultColorSpace());
 
     yuv_format_ = YUVSubsampling::k444;
-    decode_and_check_plane_sizes(f16_cache.get(), kA16_float_SkColorType);
+    decode_and_check_plane_sizes(f16_cache.get(), kA16_float_SkColorType,
+                                 DefaultColorSpace());
+
+    // Verify HDR decoding has white level adjustment.
+    yuv_format_ = YUVSubsampling::k420;
+    decode_and_check_plane_sizes(f16_cache.get(), kA16_float_SkColorType,
+                                 hdr_cs);
+
+    yuv_format_ = YUVSubsampling::k422;
+    decode_and_check_plane_sizes(f16_cache.get(), kA16_float_SkColorType,
+                                 hdr_cs);
+
+    yuv_format_ = YUVSubsampling::k444;
+    decode_and_check_plane_sizes(f16_cache.get(), kA16_float_SkColorType,
+                                 hdr_cs);
   }
 
   // Verify YUV16 is unsupported when neither R16 or half-float are available.
@@ -3093,13 +3169,16 @@
     auto no_yuv16_cache = CreateCache();
 
     yuv_format_ = YUVSubsampling::k420;
-    decode_and_check_plane_sizes(no_yuv16_cache.get(), kUnknown_SkColorType);
+    decode_and_check_plane_sizes(no_yuv16_cache.get(), kUnknown_SkColorType,
+                                 DefaultColorSpace());
 
     yuv_format_ = YUVSubsampling::k422;
-    decode_and_check_plane_sizes(no_yuv16_cache.get(), kUnknown_SkColorType);
+    decode_and_check_plane_sizes(no_yuv16_cache.get(), kUnknown_SkColorType,
+                                 DefaultColorSpace());
 
     yuv_format_ = YUVSubsampling::k444;
-    decode_and_check_plane_sizes(no_yuv16_cache.get(), kUnknown_SkColorType);
+    decode_and_check_plane_sizes(no_yuv16_cache.get(), kUnknown_SkColorType,
+                                 DefaultColorSpace());
   }
 }
 
diff --git a/cc/tiles/tile_manager.cc b/cc/tiles/tile_manager.cc
index 2d275cd..88dac92 100644
--- a/cc/tiles/tile_manager.cc
+++ b/cc/tiles/tile_manager.cc
@@ -747,6 +747,7 @@
         GetContentColorUsageForPrioritizedTile(prioritized_tile);
     const gfx::ColorSpace raster_color_space =
         client_->GetRasterColorSpace(content_color_usage);
+    const float sdr_white_level = client_->GetSDRWhiteLevel();
 
     // Tiles in the raster queue should either require raster or decode for
     // checker-images. If this tile does not need raster, process it only to
@@ -758,7 +759,7 @@
       DCHECK(prioritized_tile.should_decode_checkered_images_for_tile());
 
       AddCheckeredImagesToDecodeQueue(
-          prioritized_tile, raster_color_space,
+          prioritized_tile, raster_color_space, sdr_white_level,
           CheckerImageTracker::DecodeType::kRaster,
           &work_to_schedule.checker_image_decode_queue);
       continue;
@@ -819,7 +820,7 @@
       if (tile->raster_task_scheduled_with_checker_images() &&
           prioritized_tile.should_decode_checkered_images_for_tile()) {
         AddCheckeredImagesToDecodeQueue(
-            prioritized_tile, raster_color_space,
+            prioritized_tile, raster_color_space, sdr_white_level,
             CheckerImageTracker::DecodeType::kRaster,
             &work_to_schedule.checker_image_decode_queue);
       }
@@ -827,7 +828,7 @@
       // Creating the raster task here will acquire resources, but
       // this resource usage has already been accounted for above.
       auto raster_task = CreateRasterTask(prioritized_tile, raster_color_space,
-                                          &work_to_schedule);
+                                          sdr_white_level, &work_to_schedule);
       if (!raster_task) {
         continue;
       }
@@ -867,12 +868,13 @@
           GetContentColorUsageForPrioritizedTile(prioritized_tile);
       gfx::ColorSpace raster_color_space =
           client_->GetRasterColorSpace(content_color_usage);
+      const float sdr_white_level = client_->GetSDRWhiteLevel();
 
       Tile* tile = prioritized_tile.tile();
       if (tile->draw_info().is_checker_imaged() ||
           tile->raster_task_scheduled_with_checker_images()) {
         AddCheckeredImagesToDecodeQueue(
-            prioritized_tile, raster_color_space,
+            prioritized_tile, raster_color_space, sdr_white_level,
             CheckerImageTracker::DecodeType::kRaster,
             &work_to_schedule.checker_image_decode_queue);
       }
@@ -923,6 +925,7 @@
 void TileManager::PartitionImagesForCheckering(
     const PrioritizedTile& prioritized_tile,
     const gfx::ColorSpace& raster_color_space,
+    float sdr_white_level,
     std::vector<DrawImage>* sync_decoded_images,
     std::vector<PaintImage>* checkered_images,
     const gfx::Rect* invalidated_rect,
@@ -945,7 +948,7 @@
       (*image_to_frame_index)[image.stable_id()] = frame_index;
 
     DrawImage draw_image(*original_draw_image, tile->raster_transform().scale(),
-                         frame_index, raster_color_space);
+                         frame_index, raster_color_space, sdr_white_level);
     if (checker_image_tracker_.ShouldCheckerImage(draw_image, tree))
       checkered_images->push_back(draw_image.paint_image());
     else
@@ -956,6 +959,7 @@
 void TileManager::AddCheckeredImagesToDecodeQueue(
     const PrioritizedTile& prioritized_tile,
     const gfx::ColorSpace& raster_color_space,
+    float sdr_white_level,
     CheckerImageTracker::DecodeType decode_type,
     CheckerImageTracker::ImageDecodeQueue* image_decode_queue) {
   Tile* tile = prioritized_tile.tile();
@@ -963,12 +967,11 @@
   prioritized_tile.raster_source()->GetDiscardableImagesInRect(
       tile->enclosing_layer_rect(), &images_in_tile);
   WhichTree tree = tile->tiling()->tree();
-
   for (const auto* original_draw_image : images_in_tile) {
     size_t frame_index = client_->GetFrameIndexForImage(
         original_draw_image->paint_image(), tree);
     DrawImage draw_image(*original_draw_image, tile->raster_transform().scale(),
-                         frame_index, raster_color_space);
+                         frame_index, raster_color_space, sdr_white_level);
     if (checker_image_tracker_.ShouldCheckerImage(draw_image, tree)) {
       image_decode_queue->emplace_back(draw_image.paint_image(), decode_type);
     }
@@ -1062,12 +1065,13 @@
         GetContentColorUsageForPrioritizedTile(prioritized_tile);
     gfx::ColorSpace raster_color_space =
         client_->GetRasterColorSpace(content_color_usage);
+    float sdr_white_level = client_->GetSDRWhiteLevel();
 
     std::vector<DrawImage> sync_decoded_images;
     std::vector<PaintImage> checkered_images;
     PartitionImagesForCheckering(prioritized_tile, raster_color_space,
-                                 &sync_decoded_images, &checkered_images,
-                                 nullptr);
+                                 sdr_white_level, &sync_decoded_images,
+                                 &checkered_images, nullptr);
 
     // Add the sync decoded images to |new_locked_images| so they can be added
     // to the task graph.
@@ -1161,6 +1165,7 @@
 scoped_refptr<TileTask> TileManager::CreateRasterTask(
     const PrioritizedTile& prioritized_tile,
     const gfx::ColorSpace& raster_color_space,
+    float sdr_white_level,
     PrioritizedWorkToSchedule* work_to_schedule) {
   TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("cc.debug"),
                "TileManager::CreateRasterTask");
@@ -1222,8 +1227,9 @@
   base::flat_map<PaintImage::Id, size_t> image_id_to_current_frame_index;
   if (!skip_images) {
     PartitionImagesForCheckering(
-        prioritized_tile, raster_color_space, &sync_decoded_images,
-        &checkered_images, partial_tile_decode ? &invalidated_rect : nullptr,
+        prioritized_tile, raster_color_space, sdr_white_level,
+        &sync_decoded_images, &checkered_images,
+        partial_tile_decode ? &invalidated_rect : nullptr,
         &image_id_to_current_frame_index);
   }
 
diff --git a/cc/tiles/tile_manager.h b/cc/tiles/tile_manager.h
index 0e0b02f..c36a82a 100644
--- a/cc/tiles/tile_manager.h
+++ b/cc/tiles/tile_manager.h
@@ -85,6 +85,12 @@
   virtual gfx::ColorSpace GetRasterColorSpace(
       gfx::ContentColorUsage content_color_usage) const = 0;
 
+  // Return the SDR white level for rasterization. Some systems have variable
+  // white levels (e.g., Windows SDR brightness slider). This should return the
+  // level of the monitor on which the rasterized content will be displayed (and
+  // changing the SDR white level of the display will trigger a re-raster).
+  virtual float GetSDRWhiteLevel() const = 0;
+
   // Requests that a pending tree be scheduled to invalidate content on the
   // pending on active tree. This is currently used when tiles that are
   // rasterized with missing images need to be invalidated.
@@ -365,6 +371,7 @@
   scoped_refptr<TileTask> CreateRasterTask(
       const PrioritizedTile& prioritized_tile,
       const gfx::ColorSpace& raster_color_space,
+      float sdr_white_level,
       PrioritizedWorkToSchedule* work_to_schedule);
 
   std::unique_ptr<EvictionTilePriorityQueue>
@@ -398,6 +405,7 @@
   void PartitionImagesForCheckering(
       const PrioritizedTile& prioritized_tile,
       const gfx::ColorSpace& raster_color_space,
+      float sdr_white_level,
       std::vector<DrawImage>* sync_decoded_images,
       std::vector<PaintImage>* checkered_images,
       const gfx::Rect* invalidated_rect,
@@ -405,6 +413,7 @@
   void AddCheckeredImagesToDecodeQueue(
       const PrioritizedTile& prioritized_tile,
       const gfx::ColorSpace& raster_color_space,
+      float sdr_white_level,
       CheckerImageTracker::DecodeType decode_type,
       CheckerImageTracker::ImageDecodeQueue* image_decode_queue);
 
diff --git a/cc/tiles/tile_manager_unittest.cc b/cc/tiles/tile_manager_unittest.cc
index 626fa807..13703e7f 100644
--- a/cc/tiles/tile_manager_unittest.cc
+++ b/cc/tiles/tile_manager_unittest.cc
@@ -3452,9 +3452,13 @@
     constexpr gfx::Size kTileSize(500, 500);
     Region invalidation((gfx::Rect(kLayerBounds)));
     SetupPendingTree(raster_source, kTileSize, invalidation);
-    pending_layer()->layer_tree_impl()->SetDisplayColorSpaces(
-        gfx::DisplayColorSpaces(raster_cs));
 
+    constexpr float kCustomWhiteLevel = 200.f;
+    auto display_cs = gfx::DisplayColorSpaces(raster_cs);
+    if (raster_cs.IsHDR())
+      display_cs.SetSDRWhiteLevel(kCustomWhiteLevel);
+
+    pending_layer()->layer_tree_impl()->SetDisplayColorSpaces(display_cs);
     PictureLayerTilingSet* tiling_set =
         pending_layer()->picture_layer_tiling_set();
     PictureLayerTiling* pending_tiling = tiling_set->tiling_at(0);
@@ -3469,6 +3473,21 @@
     host_impl()->tile_manager()->PrepareTiles(host_impl()->global_tile_state());
     ASSERT_TRUE(host_impl()->tile_manager()->HasScheduledTileTasksForTesting());
 
+    auto pending_tiles = pending_tiling->AllTilesForTesting();
+    ASSERT_FALSE(pending_tiles.empty());
+
+    if (raster_cs.IsHDR()) {
+      // Only the last tile will have any pending tasks.
+      const auto& pending_tasks =
+          host_impl()->tile_manager()->decode_tasks_for_testing(
+              pending_tiles.back()->id());
+      EXPECT_FALSE(pending_tasks.empty());
+      for (const auto& draw_info : pending_tasks) {
+        EXPECT_EQ(draw_info.target_color_space(), raster_cs);
+        EXPECT_FLOAT_EQ(draw_info.sdr_white_level(), kCustomWhiteLevel);
+      }
+    }
+
     // Raster all tiles.
     static_cast<SynchronousTaskGraphRunner*>(task_graph_runner())
         ->RunUntilIdle();
diff --git a/cc/trees/layer_tree_host_impl.cc b/cc/trees/layer_tree_host_impl.cc
index c6ad4d4..16bb248 100644
--- a/cc/trees/layer_tree_host_impl.cc
+++ b/cc/trees/layer_tree_host_impl.cc
@@ -1779,11 +1779,14 @@
   }
 
   // The pending tree will has the most recently updated color space, so use it.
-  gfx::ColorSpace result;
+  gfx::DisplayColorSpaces display_cs;
   if (pending_tree_)
-    result = pending_tree_->display_color_spaces().GetScreenInfoColorSpace();
+    display_cs = pending_tree_->display_color_spaces();
   else if (active_tree_)
-    result = active_tree_->display_color_spaces().GetScreenInfoColorSpace();
+    display_cs = active_tree_->display_color_spaces();
+
+  auto result = display_cs.GetOutputColorSpace(gfx::ContentColorUsage::kHDR,
+                                               /*needs_alpha=*/false);
 
   // Always specify a color space if color correct rasterization is requested
   // (not specifying a color space indicates that no color conversion is
@@ -1799,6 +1802,20 @@
   return result;
 }
 
+float LayerTreeHostImpl::GetSDRWhiteLevel() const {
+  // If we are likely to software composite the resource, we use sRGB because
+  // software compositing is unable to perform color conversion.
+  if (!layer_tree_frame_sink_ || !layer_tree_frame_sink_->context_provider())
+    return gfx::ColorSpace::kDefaultSDRWhiteLevel;
+
+  // The pending tree will has the most recently updated color space, so use it.
+  if (pending_tree_)
+    return pending_tree_->display_color_spaces().GetSDRWhiteLevel();
+  if (active_tree_)
+    return active_tree_->display_color_spaces().GetSDRWhiteLevel();
+  return gfx::ColorSpace::kDefaultSDRWhiteLevel;
+}
+
 void LayerTreeHostImpl::RequestImplSideInvalidationForCheckerImagedTiles() {
   // When using impl-side invalidation for checker-imaging, a pending tree does
   // not need to be flushed as an independent update through the pipeline.
diff --git a/cc/trees/layer_tree_host_impl.h b/cc/trees/layer_tree_host_impl.h
index 6b17453..c2f8b67 100644
--- a/cc/trees/layer_tree_host_impl.h
+++ b/cc/trees/layer_tree_host_impl.h
@@ -535,6 +535,7 @@
   void SetIsLikelyToRequireADraw(bool is_likely_to_require_a_draw) override;
   gfx::ColorSpace GetRasterColorSpace(
       gfx::ContentColorUsage content_color_usage) const override;
+  float GetSDRWhiteLevel() const override;
   void RequestImplSideInvalidationForCheckerImagedTiles() override;
   size_t GetFrameIndexForImage(const PaintImage& paint_image,
                                WhichTree tree) const override;
diff --git a/cc/trees/layer_tree_host_impl_unittest.cc b/cc/trees/layer_tree_host_impl_unittest.cc
index 52904e6..745bd7a 100644
--- a/cc/trees/layer_tree_host_impl_unittest.cc
+++ b/cc/trees/layer_tree_host_impl_unittest.cc
@@ -15842,6 +15842,8 @@
   EXPECT_EQ(
       host_impl_->GetRasterColorSpace(gfx::ContentColorUsage::kWideColorGamut),
       gfx::ColorSpace::CreateDisplayP3D65());
+  EXPECT_EQ(gfx::ColorSpace::kDefaultSDRWhiteLevel,
+            host_impl_->GetSDRWhiteLevel());
 }
 
 TEST_P(ScrollUnifiedLayerTreeHostImplTest, RasterColorSpaceSoftware) {
@@ -15856,6 +15858,8 @@
   EXPECT_EQ(
       host_impl_->GetRasterColorSpace(gfx::ContentColorUsage::kWideColorGamut),
       gfx::ColorSpace::CreateSRGB());
+  EXPECT_EQ(gfx::ColorSpace::kDefaultSDRWhiteLevel,
+            host_impl_->GetSDRWhiteLevel());
 }
 
 TEST_P(ScrollUnifiedLayerTreeHostImplTest, RasterColorPrefersSRGB) {
@@ -15872,6 +15876,8 @@
   CreateHostImpl(settings, CreateLayerTreeFrameSink());
   host_impl_->active_tree()->SetDisplayColorSpaces(gfx::DisplayColorSpaces(p3));
   EXPECT_EQ(host_impl_->GetRasterColorSpace(gfx::ContentColorUsage::kSRGB), p3);
+  EXPECT_EQ(gfx::ColorSpace::kDefaultSDRWhiteLevel,
+            host_impl_->GetSDRWhiteLevel());
 }
 
 TEST_P(ScrollUnifiedLayerTreeHostImplTest, RasterColorSpaceHDR) {
@@ -15890,6 +15896,23 @@
       gfx::ColorSpace::CreateDisplayP3D65());
 
   EXPECT_EQ(host_impl_->GetRasterColorSpace(gfx::ContentColorUsage::kHDR), hdr);
+  EXPECT_EQ(gfx::ColorSpace::kDefaultSDRWhiteLevel,
+            host_impl_->GetSDRWhiteLevel());
+}
+
+TEST_P(ScrollUnifiedLayerTreeHostImplTest, SDRWhiteLevel) {
+  constexpr float kCustomWhiteLevel = 200.f;
+  auto hdr = gfx::ColorSpace::CreateHDR10();
+  auto display_cs = gfx::DisplayColorSpaces(hdr);
+  display_cs.SetSDRWhiteLevel(kCustomWhiteLevel);
+
+  LayerTreeSettings settings = DefaultSettings();
+  CreateHostImpl(settings, CreateLayerTreeFrameSink());
+  host_impl_->active_tree()->SetDisplayColorSpaces(display_cs);
+
+  // Non-HDR content should be rasterized in P3.
+  EXPECT_EQ(host_impl_->GetRasterColorSpace(gfx::ContentColorUsage::kHDR), hdr);
+  EXPECT_EQ(kCustomWhiteLevel, host_impl_->GetSDRWhiteLevel());
 }
 
 TEST_P(ScrollUnifiedLayerTreeHostImplTest, UpdatedTilingsForNonDrawingLayers) {
diff --git a/components/viz/service/display/gl_renderer_unittest.cc b/components/viz/service/display/gl_renderer_unittest.cc
index 3252da36..8ff3685 100644
--- a/components/viz/service/display/gl_renderer_unittest.cc
+++ b/components/viz/service/display/gl_renderer_unittest.cc
@@ -184,8 +184,7 @@
       const DirectRenderer::DrawingFrame& drawing_frame,
       bool validate_output_color_matrix) {
     renderer()->SetCurrentFrameForTesting(drawing_frame);
-    const size_t kNumSrcColorSpaces = 8;
-    gfx::ColorSpace src_color_spaces[kNumSrcColorSpaces] = {
+    const gfx::ColorSpace kSrcColorSpaces[] = {
         gfx::ColorSpace::CreateSRGB(),
         gfx::ColorSpace(gfx::ColorSpace::PrimaryID::ADOBE_RGB,
                         gfx::ColorSpace::TransferID::GAMMA28),
@@ -202,45 +201,43 @@
         // This won't be, because it has a set SDR white level.
         gfx::ColorSpace::CreateHDR10(123.0f),
     };
-    const size_t kNumDstColorSpaces = 4;
-    gfx::ColorSpace dst_color_spaces[kNumDstColorSpaces] = {
+    const gfx::ColorSpace kDstColorSpaces[] = {
         gfx::ColorSpace::CreateSRGB(),
         gfx::ColorSpace(gfx::ColorSpace::PrimaryID::ADOBE_RGB,
                         gfx::ColorSpace::TransferID::GAMMA18),
         gfx::ColorSpace::CreateExtendedSRGB(),
         gfx::ColorSpace::CreateSCRGBLinear(),
     };
-    for (size_t i = 0; i < kNumDstColorSpaces; ++i) {
-      for (size_t j = 0; j < kNumSrcColorSpaces; ++j) {
-        const auto& src_color_space = src_color_spaces[j];
-        const auto& dst_color_space = dst_color_spaces[i];
-
+    // Note: Use ASSERT_XXX() and not EXPECT_XXX() below since the size of the
+    // loop will lead to useless timeout failures on the bots otherwise.
+    for (const auto& src_color_space : kSrcColorSpaces) {
+      for (const auto& dst_color_space : kDstColorSpaces) {
         renderer()->SetUseProgram(program_key, src_color_space, dst_color_space,
                                   /*adjust_src_white_level=*/true);
-        EXPECT_TRUE(renderer()->current_program_->initialized());
+        ASSERT_TRUE(renderer()->current_program_->initialized());
 
         if (src_color_space != dst_color_space) {
           auto adjusted_color_space = src_color_space;
-          // Only in the iteration where we use an HDR color space without
-          // specifying an SDR white level should the white level be set by the
-          // renderer.
-          if (src_color_space == gfx::ColorSpace::CreateSCRGBLinear() ||
-              src_color_space == gfx::ColorSpace::CreateHDR10()) {
+          if (src_color_space.IsHDR()) {
             adjusted_color_space = src_color_space.GetWithSDRWhiteLevel(
                 drawing_frame.display_color_spaces.GetSDRWhiteLevel());
-            EXPECT_NE(adjusted_color_space, src_color_space);
           }
+          SCOPED_TRACE(
+              base::StringPrintf("adjusted_color_space=%s, dst_color_space=%s",
+                                 adjusted_color_space.ToString().c_str(),
+                                 dst_color_space.ToString().c_str()));
+
           auto color_transform = gfx::ColorTransform::NewColorTransform(
               adjusted_color_space, dst_color_space,
               gfx::ColorTransform::Intent::INTENT_PERCEPTUAL);
-          EXPECT_EQ(color_transform->GetShaderSource(),
+          ASSERT_EQ(color_transform->GetShaderSource(),
                     renderer()
                         ->current_program_->color_transform_for_testing()
                         ->GetShaderSource());
         }
 
         if (validate_output_color_matrix) {
-          EXPECT_NE(
+          ASSERT_NE(
               -1, renderer()->current_program_->output_color_matrix_location());
         }
       }
diff --git a/ui/gfx/color_space.cc b/ui/gfx/color_space.cc
index 768f84f..f6eb5f8 100644
--- a/ui/gfx/color_space.cc
+++ b/ui/gfx/color_space.cc
@@ -595,7 +595,7 @@
 
 ColorSpace ColorSpace::GetWithSDRWhiteLevel(float sdr_white_level) const {
   ColorSpace result = *this;
-  if (transfer_ == TransferID::SMPTEST2084 && transfer_params_[0] == 0.f) {
+  if (transfer_ == TransferID::SMPTEST2084) {
     result.transfer_params_[0] = sdr_white_level;
   } else if (transfer_ == TransferID::LINEAR_HDR) {
     result.transfer_ = TransferID::CUSTOM_HDR;
diff --git a/ui/gfx/color_space.h b/ui/gfx/color_space.h
index f541f701..08cf2ad 100644
--- a/ui/gfx/color_space.h
+++ b/ui/gfx/color_space.h
@@ -290,9 +290,9 @@
   // the caller but replacing the matrix and range with the given values.
   ColorSpace GetWithMatrixAndRange(MatrixID matrix, RangeID range) const;
 
-  // If this color space has a PQ or scRGB linear transfer function that did not
-  // specify an SDR white level, then return |this| with its SDR white level set
-  // to |sdr_white_level|. Otherwise return |this| unmodified.
+  // If this color space has a PQ or scRGB linear transfer function, then return
+  // |this| with its SDR white level set to |sdr_white_level|. Otherwise return
+  // |this| unmodified.
   ColorSpace GetWithSDRWhiteLevel(float sdr_white_level) const;
 
   // This will return nullptr for non-RGB spaces, spaces with non-FULL
diff --git a/ui/gfx/color_space_unittest.cc b/ui/gfx/color_space_unittest.cc
index 27700703..57a995c 100644
--- a/ui/gfx/color_space_unittest.cc
+++ b/ui/gfx/color_space_unittest.cc
@@ -329,6 +329,12 @@
   EXPECT_EQ(color_space.GetTransferID(), ColorSpace::TransferID::SMPTEST2084);
   EXPECT_TRUE(color_space.GetPQSDRWhiteLevel(&sdr_white_level));
   EXPECT_EQ(sdr_white_level, kCustomWhiteLevel);
+
+  constexpr float kCustomWhiteLevel2 = kCustomWhiteLevel * 2;
+  color_space = color_space.GetWithSDRWhiteLevel(kCustomWhiteLevel2);
+  EXPECT_EQ(color_space.GetTransferID(), ColorSpace::TransferID::SMPTEST2084);
+  EXPECT_TRUE(color_space.GetPQSDRWhiteLevel(&sdr_white_level));
+  EXPECT_EQ(sdr_white_level, kCustomWhiteLevel2);
 }
 
 TEST(ColorSpace, LinearHDRWhiteLevel) {