CopyTextureForBrowser: Support display p3 to Srgb color space conversion

This CL add color space conversion bases for CopyTextureForBrowser.
Theoretically, it could support any color space conversion. But
test cases only cover (Srgb, DisplayP3) to (Srgb).
It could be expanded to more color spaces conversions.

Bug: dawn:1140
Change-Id: I332e6d1f7cf2424fd5f5af83c71fa45c98d2d8ac
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/70780
Reviewed-by: Corentin Wallez <cwallez@chromium.org>
Reviewed-by: Kai Ninomiya <kainino@chromium.org>
Commit-Queue: Shaobo Yan <shaobo.yan@intel.com>
diff --git a/dawn.json b/dawn.json
index 8310ea2..1ef3fc1 100644
--- a/dawn.json
+++ b/dawn.json
@@ -857,20 +857,40 @@
     },
     "alpha op": {
         "category": "enum",
-        "tags": ["dawn"],
+        "tags": ["deprecated"],
         "values": [
             {"value": 0, "name": "dont change"},
             {"value": 1, "name": "premultiply"},
             {"value": 2, "name": "unpremultiply"}
         ]
     },
+    "alpha mode": {
+        "category": "enum",
+        "tags": ["dawn"],
+        "values": [
+            {"value": 0, "name": "premultiplied"},
+            {"value": 1, "name": "unpremultiplied"}
+        ]
+    },
     "copy texture for browser options": {
         "category": "structure",
         "extensible": "in",
         "tags": ["dawn"],
+        "_TODO": "support number as length input",
         "members": [
-            {"name": "flipY", "type": "bool", "default": "false"},
-            {"name": "alphaOp", "type": "alpha op", "default": "dont change"}
+            {"name": "flip y", "type": "bool", "default": "false"},
+            {"name": "alpha op", "type": "alpha op", "default": "dont change", "tags": ["deprecated"]},
+            {"name": "needs color space conversion", "type": "bool", "default": "false"},
+            {"name": "src alpha mode", "type": "alpha mode", "default": "unpremultiplied"},
+            {"name": "transfer function parameters count", "type": "uint32_t", "default": "0"},
+            {"name": "src transfer function parameters", "type": "float", "annotation": "const*",
+                     "length": "transfer function parameters count", "default": "nullptr"},
+            {"name": "conversion matrix elements count", "type": "uint32_t", "default": "0"},
+            {"name": "conversion matrix", "type": "float", "annotation": "const*",
+                     "length": "conversion matrix elements count", "default": "nullptr"},
+            {"name": "dst transfer function parameters", "type": "float", "annotation": "const*",
+                     "length": "transfer function parameters count", "default": "nullptr"},
+            {"name": "dst alpha mode", "type": "alpha mode", "default": "unpremultiplied"}
         ]
     },
     "create compute pipeline async callback": {
diff --git a/src/dawn_native/CopyTextureForBrowserHelper.cpp b/src/dawn_native/CopyTextureForBrowserHelper.cpp
index bc93cb1..baf5ed2 100644
--- a/src/dawn_native/CopyTextureForBrowserHelper.cpp
+++ b/src/dawn_native/CopyTextureForBrowserHelper.cpp
@@ -14,6 +14,7 @@
 
 #include "dawn_native/CopyTextureForBrowserHelper.h"
 
+#include "common/Log.h"
 #include "dawn_native/BindGroup.h"
 #include "dawn_native/BindGroupLayout.h"
 #include "dawn_native/Buffer.h"
@@ -36,10 +37,25 @@
     namespace {
 
         static const char sCopyTextureForBrowserShader[] = R"(
-            [[block]] struct Uniforms {
-                u_scale: vec2<f32>;
-                u_offset: vec2<f32>;
-                u_alphaOp: u32;
+            struct GammaTransferParams {
+                G: f32;
+                A: f32;
+                B: f32;
+                C: f32;
+                D: f32;
+                E: f32;
+                F: f32;
+                padding: u32;
+            };
+
+            [[block]] struct Uniforms {                        // offset   align   size
+                scale: vec2<f32>;                              // 0        8       8
+                offset: vec2<f32>;                             // 8        8       8
+                steps_mask: u32;                               // 16       4       4  
+                // implicit padding;                           // 20               12
+                conversion_matrix: mat3x3<f32>;                // 32       16      48
+                gamma_decoding_params: GammaTransferParams;    // 80       4       32
+                gamma_encoding_params: GammaTransferParams;    // 112      4       32
             };
 
             [[binding(0), group(0)]] var<uniform> uniforms : Uniforms;
@@ -49,7 +65,26 @@
                 [[builtin(position)]] position : vec4<f32>;
             };
 
-            [[stage(vertex)]] fn vs_main(
+            // Chromium uses unified equation to construct gamma decoding function
+            // and gamma encoding function.
+            // The logic is:
+            //  if x < D
+            //      linear = C * x + F
+            //  nonlinear = pow(A * x + B, G) + E
+            // (https://source.chromium.org/chromium/chromium/src/+/main:ui/gfx/color_transform.cc;l=541)
+            // Expand the equation with sign() to make it handle all gamma conversions.
+            fn gamma_conversion(v: f32, params: GammaTransferParams) -> f32 {
+                // Linear part: C * x + F
+                if (abs(v) < params.D) {
+                    return sign(v) * (params.C * abs(v) + params.F);
+                }
+
+                // Gamma part: pow(A * x + B, G) + E
+                return sign(v) * (pow(params.A * abs(v) + params.B, params.G) + params.E);
+            }
+
+            [[stage(vertex)]]
+            fn vs_main(
                 [[builtin(vertex_index)]] VertexIndex : u32
             ) -> VertexOutputs {
                 var texcoord = array<vec2<f32>, 3>(
@@ -62,7 +97,7 @@
 
                 // Y component of scale is calculated by the copySizeHeight / textureHeight. Only
                 // flipY case can get negative number.
-                var flipY = uniforms.u_scale.y < 0.0;
+                var flipY = uniforms.scale.y < 0.0;
 
                 // Texture coordinate takes top-left as origin point. We need to map the
                 // texture to triangle carefully.
@@ -70,14 +105,14 @@
                     // We need to get the mirror positions(mirrored based on y = 0.5) on flip cases.
                     // Adopt transform to src texture and then mapping it to triangle coord which
                     // do a +1 shift on Y dimension will help us got that mirror position perfectly.
-                    output.texcoords = (texcoord[VertexIndex] * uniforms.u_scale + uniforms.u_offset) *
+                    output.texcoords = (texcoord[VertexIndex] * uniforms.scale + uniforms.offset) *
                         vec2<f32>(1.0, -1.0) + vec2<f32>(0.0, 1.0);
                 } else {
                     // For the normal case, we need to get the exact position.
                     // So mapping texture to triangle firstly then adopt the transform.
                     output.texcoords = (texcoord[VertexIndex] *
                         vec2<f32>(1.0, -1.0) + vec2<f32>(0.0, 1.0)) *
-                        uniforms.u_scale + uniforms.u_offset;
+                        uniforms.scale + uniforms.offset;
                 }
 
                 return output;
@@ -86,7 +121,8 @@
             [[binding(1), group(0)]] var mySampler: sampler;
             [[binding(2), group(0)]] var myTexture: texture_2d<f32>;
 
-            [[stage(fragment)]] fn fs_main(
+            [[stage(fragment)]]
+            fn fs_main(
                 [[location(0)]] texcoord : vec2<f32>
             ) -> [[location(0)]] vec4<f32> {
                 // Clamp the texcoord and discard the out-of-bound pixels.
@@ -98,45 +134,81 @@
 
                 // Swizzling of texture formats when sampling / rendering is handled by the
                 // hardware so we don't need special logic in this shader. This is covered by tests.
-                var srcColor = textureSample(myTexture, mySampler, texcoord);
+                var color = textureSample(myTexture, mySampler, texcoord);
 
-                // Handle alpha. Alpha here helps on the source content and dst content have
-                // different alpha config. There are three possible ops: DontChange, Premultiply
-                // and Unpremultiply.
-                // TODO(crbug.com/1217153): if wgsl support `constexpr` and allow it
-                // to be case selector, Replace 0u/1u/2u with a constexpr variable with
-                // meaningful name.
-                switch(uniforms.u_alphaOp) {
-                    case 0u: { // AlphaOp: DontChange
-                        break;
-                    }
-                    case 1u: { // AlphaOp: Premultiply
-                        srcColor = vec4<f32>(srcColor.rgb * srcColor.a, srcColor.a);
-                        break;
-                    }
-                    case 2u: { // AlphaOp: Unpremultiply
-                        if (srcColor.a != 0.0) {
-                            srcColor = vec4<f32>(srcColor.rgb / srcColor.a, srcColor.a);
-                        }
-                        break;
-                    }
-                    default: {
-                        break;
+                let kUnpremultiplyStep = 0x01u;
+                let kDecodeToLinearStep = 0x02u;
+                let kConvertToDstGamutStep = 0x04u;
+                let kEncodeToGammaStep = 0x08u;
+                let kPremultiplyStep = 0x10u;
+
+                // Unpremultiply step. Appling color space conversion op on premultiplied source texture
+                // also needs to unpremultiply first.
+                if (bool(uniforms.steps_mask & kUnpremultiplyStep)) {
+                    if (color.a != 0.0) {
+                        color = vec4<f32>(color.rgb / color.a, color.a);
                     }
                 }
 
-                return srcColor;
+                // Linearize the source color using the source color space’s
+                // transfer function if it is non-linear.
+                if (bool(uniforms.steps_mask & kDecodeToLinearStep)) {
+                    color = vec4<f32>(gamma_conversion(color.r, uniforms.gamma_decoding_params),
+                                      gamma_conversion(color.g, uniforms.gamma_decoding_params),
+                                      gamma_conversion(color.b, uniforms.gamma_decoding_params),
+                                      color.a);
+                }
+
+                // Convert unpremultiplied, linear source colors to the destination gamut by
+                // multiplying by a 3x3 matrix. Calculate transformFromXYZD50 * transformToXYZD50
+                // in CPU side and upload the final result in uniforms.
+                if (bool(uniforms.steps_mask & kConvertToDstGamutStep)) {
+                    color = vec4<f32>(uniforms.conversion_matrix * color.rgb, color.a);
+                }
+
+                // Encode that color using the inverse of the destination color
+                // space’s transfer function if it is non-linear.
+                if (bool(uniforms.steps_mask & kEncodeToGammaStep)) {
+                    color = vec4<f32>(gamma_conversion(color.r, uniforms.gamma_encoding_params),
+                                      gamma_conversion(color.g, uniforms.gamma_encoding_params),
+                                      gamma_conversion(color.b, uniforms.gamma_encoding_params),
+                                      color.a);
+                }
+
+                // Premultiply step.
+                if (bool(uniforms.steps_mask & kPremultiplyStep)) {
+                    color = vec4<f32>(color.rgb * color.a, color.a);
+                }
+
+                return color;
             }
         )";
 
+        // Follow the same order of skcms_TransferFunction
+        // https://source.chromium.org/chromium/chromium/src/+/main:third_party/skia/include/third_party/skcms/skcms.h;l=46;
+        struct GammaTransferParams {
+            float G = 0.0;
+            float A = 0.0;
+            float B = 0.0;
+            float C = 0.0;
+            float D = 0.0;
+            float E = 0.0;
+            float F = 0.0;
+            uint32_t padding = 0;
+        };
+
         struct Uniform {
             float scaleX;
             float scaleY;
             float offsetX;
             float offsetY;
-            wgpu::AlphaOp alphaOp;
+            uint32_t stepsMask = 0;
+            const std::array<uint32_t, 3> padding = {};  // 12 bytes padding
+            std::array<float, 12> conversionMatrix = {};
+            GammaTransferParams gammaDecodingParams = {};
+            GammaTransferParams gammaEncodingParams = {};
         };
-        static_assert(sizeof(Uniform) == 20, "");
+        static_assert(sizeof(Uniform) == 144, "");
 
         // TODO(crbug.com/dawn/856): Expand copyTextureForBrowser to support any
         // non-depth, non-stencil, non-compressed texture format pair copy. Now this API
@@ -232,7 +304,6 @@
 
             return GetCachedPipeline(store, dstFormat);
         }
-
     }  // anonymous namespace
 
     MaybeError ValidateCopyTextureForBrowser(DeviceBase* device,
@@ -276,7 +347,27 @@
                                                      destination->texture->GetFormat().format));
 
         DAWN_INVALID_IF(options->nextInChain != nullptr, "nextInChain must be nullptr");
+
+        // TODO(crbug.com/dawn/1140): Remove alphaOp and wgpu::AlphaState::DontChange.
         DAWN_TRY(ValidateAlphaOp(options->alphaOp));
+        DAWN_TRY(ValidateAlphaMode(options->srcAlphaMode));
+        DAWN_TRY(ValidateAlphaMode(options->dstAlphaMode));
+
+        if (options->needsColorSpaceConversion) {
+            DAWN_INVALID_IF(options->transferFunctionParametersCount != 7u,
+                            "Invalid transfer"
+                            " function parameter count (%u).",
+                            options->transferFunctionParametersCount);
+            DAWN_INVALID_IF(options->conversionMatrixElementsCount != 9u,
+                            "Invalid conversion matrix elements count (%u).",
+                            options->conversionMatrixElementsCount);
+            DAWN_INVALID_IF(options->srcTransferFunctionParameters == nullptr,
+                            "srcTransferFunctionParameters is nullptr when doing color conversion");
+            DAWN_INVALID_IF(options->conversionMatrix == nullptr,
+                            "conversionMatrix is nullptr when doing color conversion");
+            DAWN_INVALID_IF(options->dstTransferFunctionParameters == nullptr,
+                            "dstTransferFunctionParameters is nullptr when doing color conversion");
+        }
 
         return {};
     }
@@ -309,8 +400,7 @@
             copySize->width / static_cast<float>(srcTextureSize.width),
             copySize->height / static_cast<float>(srcTextureSize.height),  // scale
             source->origin.x / static_cast<float>(srcTextureSize.width),
-            source->origin.y / static_cast<float>(srcTextureSize.height),  // offset
-            wgpu::AlphaOp::DontChange  // alphaOp default value: DontChange
+            source->origin.y / static_cast<float>(srcTextureSize.height)  // offset
         };
 
         // Handle flipY. FlipY here means we flip the source texture firstly and then
@@ -321,8 +411,89 @@
             uniformData.offsetY += copySize->height / static_cast<float>(srcTextureSize.height);
         }
 
-        // Set alpha op.
-        uniformData.alphaOp = options->alphaOp;
+        uint32_t stepsMask = 0u;
+
+        // Steps to do color space conversion
+        // From https://skia.org/docs/user/color/
+        // - unpremultiply if the source color is premultiplied; Alpha is not involved in color
+        // management, and we need to divide it out if it’s multiplied in.
+        // - linearize the source color using the source color space’s transfer function
+        // - convert those unpremultiplied, linear source colors to XYZ D50 gamut by multiplying by
+        // a 3x3 matrix.
+        // - convert those XYZ D50 colors to the destination gamut by multiplying by a 3x3 matrix.
+        // - encode that color using the inverse of the destination color space’s transfer function.
+        // - premultiply by alpha if the destination is premultiplied.
+        // The reason to choose XYZ D50 as intermediate color space:
+        // From http://www.brucelindbloom.com/index.html?WorkingSpaceInfo.html
+        // "Since the Lab TIFF specification, the ICC profile specification and
+        // Adobe Photoshop all use a D50"
+        constexpr uint32_t kUnpremultiplyStep = 0x01;
+        constexpr uint32_t kDecodeToLinearStep = 0x02;
+        constexpr uint32_t kConvertToDstGamutStep = 0x04;
+        constexpr uint32_t kEncodeToGammaStep = 0x08;
+        constexpr uint32_t kPremultiplyStep = 0x10;
+
+        if (options->srcAlphaMode == wgpu::AlphaMode::Premultiplied) {
+            if (options->needsColorSpaceConversion ||
+                options->srcAlphaMode != options->dstAlphaMode) {
+                stepsMask |= kUnpremultiplyStep;
+            }
+        }
+
+        if (options->needsColorSpaceConversion) {
+            stepsMask |= kDecodeToLinearStep;
+            const float* decodingParams = options->srcTransferFunctionParameters;
+
+            uniformData.gammaDecodingParams = {
+                decodingParams[0], decodingParams[1], decodingParams[2], decodingParams[3],
+                decodingParams[4], decodingParams[5], decodingParams[6]};
+
+            stepsMask |= kConvertToDstGamutStep;
+            const float* matrix = options->conversionMatrix;
+            uniformData.conversionMatrix = {{
+                matrix[0],
+                matrix[1],
+                matrix[2],
+                0.0,
+                matrix[3],
+                matrix[4],
+                matrix[5],
+                0.0,
+                matrix[6],
+                matrix[7],
+                matrix[8],
+                0.0,
+            }};
+
+            stepsMask |= kEncodeToGammaStep;
+            const float* encodingParams = options->dstTransferFunctionParameters;
+
+            uniformData.gammaEncodingParams = {
+                encodingParams[0], encodingParams[1], encodingParams[2], encodingParams[3],
+                encodingParams[4], encodingParams[5], encodingParams[6]};
+        }
+
+        if (options->dstAlphaMode == wgpu::AlphaMode::Premultiplied) {
+            if (options->needsColorSpaceConversion ||
+                options->srcAlphaMode != options->dstAlphaMode) {
+                stepsMask |= kPremultiplyStep;
+            }
+        }
+
+        if (options->alphaOp != wgpu::AlphaOp::DontChange) {
+            dawn::WarningLog() << "CopyTextureForBrowserOption.alphaOp has been deprecated.";
+        }
+
+        // TODO(crbugs.com/dawn/1140): AlphaOp will be deprecated
+        if (options->alphaOp == wgpu::AlphaOp::Premultiply) {
+            stepsMask |= kPremultiplyStep;
+        }
+
+        if (options->alphaOp == wgpu::AlphaOp::Unpremultiply) {
+            stepsMask |= kUnpremultiplyStep;
+        }
+
+        uniformData.stepsMask = stepsMask;
 
         Ref<BufferBase> uniformBuffer;
         DAWN_TRY_ASSIGN(
diff --git a/src/tests/DawnTest.h b/src/tests/DawnTest.h
index 5e37c91..c38d6ee 100644
--- a/src/tests/DawnTest.h
+++ b/src/tests/DawnTest.h
@@ -25,6 +25,7 @@
 #include "tests/ParamGenerator.h"
 #include "tests/ToggleParser.h"
 #include "utils/ScopedAutoreleasePool.h"
+#include "utils/TextureUtils.h"
 
 #include <dawn_platform/DawnPlatform.h>
 #include <gmock/gmock.h>
@@ -95,6 +96,9 @@
 
 #define EXPECT_TEXTURE_EQ(...) AddTextureExpectation(__FILE__, __LINE__, __VA_ARGS__)
 
+#define EXPECT_TEXTURE_FLOAT16_EQ(...) \
+    AddTextureExpectation<float, uint16_t>(__FILE__, __LINE__, __VA_ARGS__)
+
 #define ASSERT_DEVICE_ERROR_MSG(statement, matcher)             \
     StartExpectDeviceError(matcher);                            \
     statement;                                                  \
@@ -360,6 +364,30 @@
                                               const wgpu::Texture& texture,
                                               wgpu::Origin3D origin,
                                               wgpu::Extent3D extent,
+                                              wgpu::TextureFormat format,
+                                              T tolerance = 0,
+                                              uint32_t level = 0,
+                                              wgpu::TextureAspect aspect = wgpu::TextureAspect::All,
+                                              uint32_t bytesPerRow = 0) {
+        uint32_t texelBlockSize = utils::GetTexelBlockSizeInBytes(format);
+        uint32_t texelComponentCount = utils::GetWGSLRenderableColorTextureComponentCount(format);
+
+        return AddTextureExpectationImpl(
+            file, line,
+            new detail::ExpectEq<T, U>(
+                expectedData,
+                texelComponentCount * extent.width * extent.height * extent.depthOrArrayLayers,
+                tolerance),
+            texture, origin, extent, level, aspect, texelBlockSize, bytesPerRow);
+    }
+
+    template <typename T, typename U = T>
+    std::ostringstream& AddTextureExpectation(const char* file,
+                                              int line,
+                                              const T* expectedData,
+                                              const wgpu::Texture& texture,
+                                              wgpu::Origin3D origin,
+                                              wgpu::Extent3D extent,
                                               uint32_t level = 0,
                                               wgpu::TextureAspect aspect = wgpu::TextureAspect::All,
                                               uint32_t bytesPerRow = 0) {
diff --git a/src/tests/end2end/CopyTextureForBrowserTests.cpp b/src/tests/end2end/CopyTextureForBrowserTests.cpp
index 7ee7ae8..829a23b 100644
--- a/src/tests/end2end/CopyTextureForBrowserTests.cpp
+++ b/src/tests/end2end/CopyTextureForBrowserTests.cpp
@@ -28,6 +28,11 @@
     static constexpr uint64_t kDefaultTextureWidth = 10;
     static constexpr uint64_t kDefaultTextureHeight = 1;
 
+    enum class ColorSpace : uint32_t {
+        SRGB = 0x00,
+        DisplayP3 = 0x01,
+    };
+
     using Alpha = wgpu::AlphaOp;
     DAWN_TEST_PARAM_STRUCT(AlphaTestParams, Alpha);
 
@@ -37,6 +42,10 @@
     using DstOrigin = wgpu::Origin3D;
     using CopySize = wgpu::Extent3D;
     using FlipY = bool;
+    using SrcColorSpace = ColorSpace;
+    using DstColorSpace = ColorSpace;
+    using SrcAlphaMode = wgpu::AlphaMode;
+    using DstAlphaMode = wgpu::AlphaMode;
 
     std::ostream& operator<<(std::ostream& o, wgpu::Origin3D origin) {
         o << origin.x << ", " << origin.y << ", " << origin.z;
@@ -48,9 +57,98 @@
         return o;
     }
 
+    std::ostream& operator<<(std::ostream& o, ColorSpace space) {
+        o << static_cast<uint32_t>(space);
+        return o;
+    }
+
     DAWN_TEST_PARAM_STRUCT(FormatTestParams, SrcFormat, DstFormat);
     DAWN_TEST_PARAM_STRUCT(SubRectTestParams, SrcOrigin, DstOrigin, CopySize, FlipY);
+    DAWN_TEST_PARAM_STRUCT(ColorSpaceTestParams,
+                           DstFormat,
+                           SrcColorSpace,
+                           DstColorSpace,
+                           SrcAlphaMode,
+                           DstAlphaMode);
 
+    // Color Space table
+    struct ColorSpaceInfo {
+        ColorSpace index;
+        std::array<float, 9> toXYZD50;    // 3x3 row major transform matrix
+        std::array<float, 9> fromXYZD50;  // inverse transform matrix of toXYZD50, precomputed
+        std::array<float, 7> gammaDecodingParams;  // Follow { A, B, G, E, epsilon, C, F } order
+        std::array<float, 7> gammaEncodingParams;  // inverse op of decoding, precomputed
+        bool isNonLinear;
+        bool isExtended;  // For extended color space.
+    };
+    static constexpr size_t kSupportedColorSpaceCount = 2;
+    static constexpr std::array<ColorSpaceInfo, kSupportedColorSpaceCount> ColorSpaceTable = {{
+        // sRGB,
+        // Got primary attributes from https://drafts.csswg.org/css-color/#predefined-sRGB
+        // Use matrices from
+        // http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html#WSMatrices
+        // Get gamma-linear conversion params from https://en.wikipedia.org/wiki/SRGB with some
+        // mathematics.
+        {
+            //
+            ColorSpace::SRGB,
+            {{
+                //
+                0.4360747, 0.3850649, 0.1430804,  //
+                0.2225045, 0.7168786, 0.0606169,  //
+                0.0139322, 0.0971045, 0.7141733   //
+            }},
+
+            {{
+                //
+                3.1338561, -1.6168667, -0.4906146,  //
+                -0.9787684, 1.9161415, 0.0334540,   //
+                0.0719453, -0.2289914, 1.4052427    //
+            }},
+
+            // {G, A, B, C, D, E, F, }
+            {{2.4, 1.0 / 1.055, 0.055 / 1.055, 1.0 / 12.92, 4.045e-02, 0.0, 0.0}},
+
+            {{1.0 / 2.4, 1.13711 /*pow(1.055, 2.4)*/, 0.0, 12.92f, 3.1308e-03, -0.055, 0.0}},
+
+            true,
+            true  //
+        },
+
+        // Display P3, got primary attributes from
+        // https://www.w3.org/TR/css-color-4/#valdef-color-display-p3
+        // Use equations found in
+        // http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html,
+        // Use Bradford method to do D65 to D50 transform.
+        // Get matrices with help of http://www.russellcottrell.com/photo/matrixCalculator.htm
+        // Gamma-linear conversion params is the same as Srgb.
+        {
+            //
+            ColorSpace::DisplayP3,
+            {{
+                //
+                0.5151114, 0.2919612, 0.1571274,  //
+                0.2411865, 0.6922440, 0.0665695,  //
+                -0.0010491, 0.0418832, 0.7842659  //
+            }},
+
+            {{
+                //
+                2.4039872, -0.9898498, -0.3976181,  //
+                -0.8422138, 1.7988188, 0.0160511,   //
+                0.0481937, -0.0973889, 1.2736887    //
+            }},
+
+            // {G, A, B, C, D, E, F, }
+            {{2.4, 1.0 / 1.055, 0.055 / 1.055, 1.0 / 12.92, 4.045e-02, 0.0, 0.0}},
+
+            {{1.0 / 2.4, 1.13711 /*pow(1.055, 2.4)*/, 0.0, 12.92f, 3.1308e-03, -0.055, 0.0}},
+
+            true,
+            false  //
+        }
+        //
+    }};
 }  // anonymous namespace
 
 template <typename Parent>
@@ -578,21 +676,323 @@
     }
 };
 
+class CopyTextureForBrowser_ColorSpace
+    : public CopyTextureForBrowserTests<DawnTestWithParams<ColorSpaceTestParams>> {
+  protected:
+    const ColorSpaceInfo& GetColorSpaceInfo(ColorSpace colorSpace) {
+        uint32_t index = static_cast<uint32_t>(colorSpace);
+        ASSERT(index < ColorSpaceTable.size());
+        ASSERT(ColorSpaceTable[index].index == colorSpace);
+        return ColorSpaceTable[index];
+    }
+
+    std::array<float, 9> GetConversionMatrix(ColorSpace src, ColorSpace dst) {
+        const ColorSpaceInfo& srcColorSpace = GetColorSpaceInfo(src);
+        const ColorSpaceInfo& dstColorSpace = GetColorSpaceInfo(dst);
+
+        const std::array<float, 9> toXYZD50 = srcColorSpace.toXYZD50;
+        const std::array<float, 9> fromXYZD50 = dstColorSpace.fromXYZD50;
+
+        // Fuse the transform matrix. The color space transformation equation is:
+        // Pixels = fromXYZD50 * toXYZD50 * Pixels.
+        // Calculate fromXYZD50 * toXYZD50 to simplify
+        // Add a padding in each row for Mat3x3 in wgsl uniform(mat3x3, Align(16), Size(48)).
+        std::array<float, 9> fuseMatrix = {};
+
+        // Mat3x3 * Mat3x3
+        for (uint32_t row = 0; row < 3; ++row) {
+            for (uint32_t col = 0; col < 3; ++col) {
+                // Transpose the matrix from row major to column major for wgsl.
+                fuseMatrix[col * 3 + row] = fromXYZD50[row * 3 + 0] * toXYZD50[col] +
+                                            fromXYZD50[row * 3 + 1] * toXYZD50[3 + col] +
+                                            fromXYZD50[row * 3 + 2] * toXYZD50[3 * 2 + col];
+            }
+        }
+
+        return fuseMatrix;
+    }
+
+    // TODO(crbug.com/dawn/1140): Generate source data automatically.
+    std::vector<RGBA8> GetSourceData(wgpu::AlphaMode srcTextureAlphaMode) {
+        if (srcTextureAlphaMode == wgpu::AlphaMode::Premultiplied) {
+            return std::vector<RGBA8>{
+                RGBA8(0, 102, 102, 102),  // a = 0.4
+                RGBA8(102, 0, 0, 102),    // a = 0.4
+                RGBA8(153, 0, 0, 153),    // a = 0.6
+                RGBA8(255, 0, 0, 255),    // a = 1.0
+
+                RGBA8(153, 0, 153, 153),  // a = 0.6
+                RGBA8(0, 102, 0, 102),    // a = 0.4
+                RGBA8(0, 153, 0, 153),    // a = 0.6
+                RGBA8(0, 255, 0, 255),    // a = 1.0
+
+                RGBA8(255, 255, 0, 255),  // a = 1.0
+                RGBA8(0, 0, 102, 102),    // a = 0.4
+                RGBA8(0, 0, 153, 153),    // a = 0.6
+                RGBA8(0, 0, 255, 255),    // a = 1.0
+            };
+        }
+
+        return std::vector<RGBA8>{
+            // Take RGBA8Unorm as example:
+            // R channel has different values
+            RGBA8(0, 255, 255, 255),  // r = 0.0
+            RGBA8(102, 0, 0, 255),    // r = 0.4
+            RGBA8(153, 0, 0, 255),    // r = 0.6
+            RGBA8(255, 0, 0, 255),    // r = 1.0
+
+            // G channel has different values
+            RGBA8(255, 0, 255, 255),  // g = 0.0
+            RGBA8(0, 102, 0, 255),    // g = 0.4
+            RGBA8(0, 153, 0, 255),    // g = 0.6
+            RGBA8(0, 255, 0, 255),    // g = 1.0
+
+            // B channel has different values
+            RGBA8(255, 255, 0, 255),  // b = 0.0
+            RGBA8(0, 0, 102, 255),    // b = 0.4
+            RGBA8(0, 0, 153, 255),    // b = 0.6
+            RGBA8(0, 0, 255, 255),    // b = 1.0
+        };
+    }
+
+    // TODO(crbug.com/dawn/1140): Current expected values are from ColorSync utils
+    // tool on Mac. Should implement CPU or compute shader algorithm to do color
+    // conversion and use the result as expected data.
+    std::vector<float> GetExpectedData(ColorSpace srcColorSpace,
+                                       ColorSpace dstColorSpace,
+                                       wgpu::AlphaMode srcTextureAlphaMode,
+                                       wgpu::AlphaMode dstTextureAlphaMode) {
+        if (srcTextureAlphaMode == wgpu::AlphaMode::Premultiplied) {
+            return GetExpectedDataForPremultipliedSource(srcColorSpace, dstColorSpace,
+                                                         dstTextureAlphaMode);
+        }
+
+        return GetExpectedDataForSeperateSource(srcColorSpace, dstColorSpace);
+    }
+
+    std::vector<float> GeneratePremultipliedResult(std::vector<float> result) {
+        // Four channels per pixel
+        for (uint32_t i = 0; i < result.size(); i += 4) {
+            result[i] *= result[i + 3];
+            result[i + 1] *= result[i + 3];
+            result[i + 2] *= result[i + 3];
+        }
+
+        return result;
+    }
+
+    std::vector<float> GetExpectedDataForPremultipliedSource(ColorSpace srcColorSpace,
+                                                             ColorSpace dstColorSpace,
+                                                             wgpu::AlphaMode dstTextureAlphaMode) {
+        if (srcColorSpace == dstColorSpace) {
+            std::vector<float> expected = {
+                0.0, 1.0, 1.0, 0.4,  //
+                1.0, 0.0, 0.0, 0.4,  //
+                1.0, 0.0, 0.0, 0.6,  //
+                1.0, 0.0, 0.0, 1.0,  //
+
+                1.0, 0.0, 1.0, 0.6,  //
+                0.0, 1.0, 0.0, 0.4,  //
+                0.0, 1.0, 0.0, 0.6,  //
+                0.0, 1.0, 0.0, 1.0,  //
+
+                1.0, 1.0, 0.0, 1.0,  //
+                0.0, 0.0, 1.0, 0.4,  //
+                0.0, 0.0, 1.0, 0.6,  //
+                0.0, 0.0, 1.0, 1.0,  //
+            };
+
+            return dstTextureAlphaMode == wgpu::AlphaMode::Premultiplied
+                       ? GeneratePremultipliedResult(expected)
+                       : expected;
+        }
+
+        switch (srcColorSpace) {
+            case ColorSpace::DisplayP3: {
+                switch (dstColorSpace) {
+                    case ColorSpace::SRGB: {
+                        std::vector<float> expected = {
+                            -0.5118, 1.0183,  1.0085,  0.4,  //
+                            1.093,   -0.2267, -0.1501, 0.4,  //
+                            1.093,   -0.2267, -0.1501, 0.6,  //
+                            1.093,   -0.2267, -0.1501, 1.0,  //
+
+                            1.093,   -0.2266, 1.0337,  0.6,  //
+                            -0.5118, 1.0183,  -0.3107, 0.4,  //
+                            -0.5118, 1.0183,  -0.3107, 0.6,  //
+                            -0.5118, 1.0183,  -0.3107, 1.0,  //
+
+                            0.9999,  1.0001,  -0.3462, 1.0,  //
+                            0.0002,  0.0004,  1.0419,  0.4,  //
+                            0.0002,  0.0004,  1.0419,  0.6,  //
+                            0.0002,  0.0004,  1.0419,  1.0,  //
+                        };
+
+                        return dstTextureAlphaMode == wgpu::AlphaMode::Premultiplied
+                                   ? GeneratePremultipliedResult(expected)
+                                   : expected;
+                    }
+                    default:
+                        UNREACHABLE();
+                }
+            }
+            default:
+                break;
+        }
+        UNREACHABLE();
+    }
+
+    std::vector<float> GetExpectedDataForSeperateSource(ColorSpace srcColorSpace,
+                                                        ColorSpace dstColorSpace) {
+        if (srcColorSpace == dstColorSpace) {
+            return std::vector<float>{
+                0.0, 1.0, 1.0, 1.0,  //
+                0.4, 0.0, 0.0, 1.0,  //
+                0.6, 0.0, 0.0, 1.0,  //
+                1.0, 0.0, 0.0, 1.0,  //
+
+                1.0, 0.0, 1.0, 1.0,  //
+                0.0, 0.4, 0.0, 1.0,  //
+                0.0, 0.6, 0.0, 1.0,  //
+                0.0, 1.0, 0.0, 1.0,  //
+
+                1.0, 1.0, 0.0, 1.0,  //
+                0.0, 0.0, 0.4, 1.0,  //
+                0.0, 0.0, 0.6, 1.0,  //
+                0.0, 0.0, 1.0, 1.0,  //
+            };
+        }
+
+        switch (srcColorSpace) {
+            case ColorSpace::DisplayP3: {
+                switch (dstColorSpace) {
+                    case ColorSpace::SRGB: {
+                        return std::vector<float>{
+                            -0.5118, 1.0183,  1.0085,  1.0,  //
+                            0.4401,  -0.0665, -0.0337, 1.0,  //
+                            0.6578,  -0.1199, -0.0723, 1.0,  //
+                            1.093,   -0.2267, -0.1501, 1.0,  //
+
+                            1.093,   -0.2266, 1.0337,  1.0,  //
+                            -0.1894, 0.4079,  -0.1027, 1.0,  //
+                            -0.2969, 0.6114,  -0.1720, 1.0,  //
+                            -0.5118, 1.0183,  -0.3107, 1.0,  //
+
+                            0.9999,  1.0001,  -0.3462, 1.0,  //
+                            0.0000,  0.0001,  0.4181,  1.0,  //
+                            0.0001,  0.0001,  0.6260,  1.0,  //
+                            0.0002,  0.0004,  1.0419,  1.0,  //
+                        };
+                    }
+                    default:
+                        UNREACHABLE();
+                }
+            }
+            default:
+                break;
+        }
+        UNREACHABLE();
+    }
+
+    void DoColorSpaceConversionTest() {
+        constexpr uint32_t kWidth = 12;
+        constexpr uint32_t kHeight = 1;
+
+        TextureSpec srcTextureSpec;
+        srcTextureSpec.textureSize = {kWidth, kHeight};
+
+        TextureSpec dstTextureSpec;
+        dstTextureSpec.textureSize = {kWidth, kHeight};
+        dstTextureSpec.format = GetParam().mDstFormat;
+
+        ColorSpace srcColorSpace = GetParam().mSrcColorSpace;
+        ColorSpace dstColorSpace = GetParam().mDstColorSpace;
+
+        ColorSpaceInfo srcColorSpaceInfo = GetColorSpaceInfo(srcColorSpace);
+        ColorSpaceInfo dstColorSpaceInfo = GetColorSpaceInfo(dstColorSpace);
+
+        std::array<float, 9> matrix = GetConversionMatrix(srcColorSpace, dstColorSpace);
+
+        wgpu::CopyTextureForBrowserOptions options = {};
+        options.needsColorSpaceConversion = srcColorSpace != dstColorSpace;
+        options.srcAlphaMode = GetParam().mSrcAlphaMode;
+        options.transferFunctionParametersCount = 7;
+        options.srcTransferFunctionParameters = srcColorSpaceInfo.gammaDecodingParams.data();
+        options.conversionMatrixElementsCount = 9;
+        options.conversionMatrix = matrix.data();
+        options.dstTransferFunctionParameters = dstColorSpaceInfo.gammaEncodingParams.data();
+        options.dstAlphaMode = GetParam().mDstAlphaMode;
+
+        std::vector<RGBA8> sourceTextureData = GetSourceData(options.srcAlphaMode);
+        const wgpu::Extent3D& copySize = {kWidth, kHeight};
+
+        const utils::TextureDataCopyLayout srcCopyLayout =
+            utils::GetTextureDataCopyLayoutForTextureAtLevel(
+                kTextureFormat,
+                {srcTextureSpec.textureSize.width, srcTextureSpec.textureSize.height},
+                srcTextureSpec.level);
+
+        wgpu::TextureUsage srcUsage = wgpu::TextureUsage::CopySrc | wgpu::TextureUsage::CopyDst |
+                                      wgpu::TextureUsage::TextureBinding;
+        wgpu::Texture srcTexture = this->CreateAndInitTexture(
+            srcTextureSpec, srcUsage, srcCopyLayout, sourceTextureData.data(),
+            sourceTextureData.size() * sizeof(RGBA8));
+
+        // Create dst texture.
+        wgpu::Texture dstTexture = this->CreateTexture(
+            dstTextureSpec, wgpu::TextureUsage::CopyDst | wgpu::TextureUsage::TextureBinding |
+                                wgpu::TextureUsage::RenderAttachment | wgpu::TextureUsage::CopySrc);
+
+        // Perform the texture to texture copy
+        this->RunCopyExternalImageToTexture(srcTextureSpec, srcTexture, dstTextureSpec, dstTexture,
+                                            copySize, options);
+
+        std::vector<float> expectedData = GetExpectedData(
+            srcColorSpace, dstColorSpace, options.srcAlphaMode, options.dstAlphaMode);
+
+        // The value provided by Apple's ColorSync Utility.
+        float tolerance = 0.001;
+        if (dstTextureSpec.format == wgpu::TextureFormat::RGBA16Float) {
+            EXPECT_TEXTURE_FLOAT16_EQ(expectedData.data(), dstTexture, {0, 0}, {kWidth, kHeight},
+                                      dstTextureSpec.format, tolerance);
+        } else {
+            EXPECT_TEXTURE_EQ(expectedData.data(), dstTexture, {0, 0}, {kWidth, kHeight},
+                              dstTextureSpec.format, tolerance);
+        }
+    }
+};
+
 // Verify CopyTextureForBrowserTests works with internal pipeline.
 // The case do copy without any transform.
 TEST_P(CopyTextureForBrowser_Basic, PassthroughCopy) {
+    // TODO(crbug.com/dawn/1232): Program link error on OpenGLES backend
+    DAWN_SUPPRESS_TEST_IF(IsOpenGLES());
+    DAWN_SUPPRESS_TEST_IF(IsOpenGL() && IsLinux());
+
     DoBasicCopyTest({10, 1});
 }
 
 TEST_P(CopyTextureForBrowser_Basic, VerifyCopyOnXDirection) {
+    // TODO(crbug.com/dawn/1232): Program link error on OpenGLES backend
+    DAWN_SUPPRESS_TEST_IF(IsOpenGLES());
+    DAWN_SUPPRESS_TEST_IF(IsOpenGL() && IsLinux());
+
     DoBasicCopyTest({1000, 1});
 }
 
 TEST_P(CopyTextureForBrowser_Basic, VerifyCopyOnYDirection) {
+    // TODO(crbug.com/dawn/1232): Program link error on OpenGLES backend
+    DAWN_SUPPRESS_TEST_IF(IsOpenGLES());
+    DAWN_SUPPRESS_TEST_IF(IsOpenGL() && IsLinux());
+
     DoBasicCopyTest({1, 1000});
 }
 
 TEST_P(CopyTextureForBrowser_Basic, VerifyCopyFromLargeTexture) {
+    // TODO(crbug.com/dawn/1232): Program link error on OpenGLES backend
+    DAWN_SUPPRESS_TEST_IF(IsOpenGLES());
+    DAWN_SUPPRESS_TEST_IF(IsOpenGL() && IsLinux());
+
     // TODO(crbug.com/dawn/1070): Flaky VK_DEVICE_LOST
     DAWN_SUPPRESS_TEST_IF(IsWindows() && IsVulkan() && IsIntel());
 
@@ -600,6 +1000,10 @@
 }
 
 TEST_P(CopyTextureForBrowser_Basic, VerifyFlipY) {
+    // TODO(crbug.com/dawn/1232): Program link error on OpenGLES backend
+    DAWN_SUPPRESS_TEST_IF(IsOpenGLES());
+    DAWN_SUPPRESS_TEST_IF(IsOpenGL() && IsLinux());
+
     wgpu::CopyTextureForBrowserOptions options = {};
     options.flipY = true;
 
@@ -607,6 +1011,10 @@
 }
 
 TEST_P(CopyTextureForBrowser_Basic, VerifyFlipYInSlimTexture) {
+    // TODO(crbug.com/dawn/1232): Program link error on OpenGLES backend
+    DAWN_SUPPRESS_TEST_IF(IsOpenGLES());
+    DAWN_SUPPRESS_TEST_IF(IsOpenGL() && IsLinux());
+
     wgpu::CopyTextureForBrowserOptions options = {};
     options.flipY = true;
 
@@ -626,6 +1034,7 @@
     // Skip OpenGLES backend because it fails on using RGBA8Unorm as
     // source texture format.
     DAWN_SUPPRESS_TEST_IF(IsOpenGLES());
+    DAWN_SUPPRESS_TEST_IF(IsOpenGL() && IsLinux());
 
     // Skip OpenGL backend on linux because it fails on using *-srgb format as
     // dst texture format
@@ -651,6 +1060,10 @@
 // green texture originally. After the subrect copy, affected part
 // in dst texture should be red and other part should remain green.
 TEST_P(CopyTextureForBrowser_SubRects, CopySubRect) {
+    // TODO(crbug.com/dawn/1232): Program link error on OpenGLES backend
+    DAWN_SUPPRESS_TEST_IF(IsOpenGLES());
+    DAWN_SUPPRESS_TEST_IF(IsOpenGL() && IsLinux());
+
     // Tests skip due to crbug.com/dawn/592.
     DAWN_SUPPRESS_TEST_IF(IsD3D12() && IsBackendValidationEnabled());
 
@@ -673,7 +1086,9 @@
 TEST_P(CopyTextureForBrowser_AlphaOps, alphaOp) {
     // Skip OpenGLES backend because it fails on using RGBA8Unorm as
     // source texture format.
+    // TODO(crbug.com/dawn/1232): Program link error on OpenGLES backend
     DAWN_SUPPRESS_TEST_IF(IsOpenGLES());
+    DAWN_SUPPRESS_TEST_IF(IsOpenGL() && IsLinux());
 
     // Tests skip due to crbug.com/dawn/1104.
     DAWN_SUPPRESS_TEST_IF(IsWARP());
@@ -686,3 +1101,27 @@
     {D3D12Backend(), MetalBackend(), OpenGLBackend(), OpenGLESBackend(), VulkanBackend()},
     std::vector<wgpu::AlphaOp>({wgpu::AlphaOp::DontChange, wgpu::AlphaOp::Premultiply,
                                 wgpu::AlphaOp::Unpremultiply}));
+
+// Verify |CopyTextureForBrowser| doing color space conversion.
+TEST_P(CopyTextureForBrowser_ColorSpace, colorSpaceConversion) {
+    // TODO(crbug.com/dawn/1232): Program link error on OpenGLES backend
+    DAWN_SUPPRESS_TEST_IF(IsOpenGLES());
+    DAWN_SUPPRESS_TEST_IF(IsOpenGL() && IsLinux());
+
+    // Tests skip due to crbug.com/dawn/1104.
+    DAWN_SUPPRESS_TEST_IF(IsWARP());
+
+    DoColorSpaceConversionTest();
+}
+
+DAWN_INSTANTIATE_TEST_P(CopyTextureForBrowser_ColorSpace,
+                        {D3D12Backend(), MetalBackend(), OpenGLBackend(), OpenGLESBackend(),
+                         VulkanBackend()},
+                        std::vector<wgpu::TextureFormat>({wgpu::TextureFormat::RGBA16Float,
+                                                          wgpu::TextureFormat::RGBA32Float}),
+                        std::vector<ColorSpace>({ColorSpace::SRGB, ColorSpace::DisplayP3}),
+                        std::vector<ColorSpace>({ColorSpace::SRGB}),
+                        std::vector<wgpu::AlphaMode>({wgpu::AlphaMode::Premultiplied,
+                                                      wgpu::AlphaMode::Unpremultiplied}),
+                        std::vector<wgpu::AlphaMode>({wgpu::AlphaMode::Premultiplied,
+                                                      wgpu::AlphaMode::Unpremultiplied}));
diff --git a/src/tests/unittests/validation/CopyTextureForBrowserTests.cpp b/src/tests/unittests/validation/CopyTextureForBrowserTests.cpp
index a242625..395d19c 100644
--- a/src/tests/unittests/validation/CopyTextureForBrowserTests.cpp
+++ b/src/tests/unittests/validation/CopyTextureForBrowserTests.cpp
@@ -50,12 +50,12 @@
                                    uint32_t dstLevel,
                                    wgpu::Origin3D dstOrigin,
                                    wgpu::Extent3D extent3D,
-                                   wgpu::TextureAspect aspect = wgpu::TextureAspect::All) {
+                                   wgpu::TextureAspect aspect = wgpu::TextureAspect::All,
+                                   wgpu::CopyTextureForBrowserOptions options = {}) {
         wgpu::ImageCopyTexture srcImageCopyTexture =
             utils::CreateImageCopyTexture(srcTexture, srcLevel, srcOrigin, aspect);
         wgpu::ImageCopyTexture dstImageCopyTexture =
             utils::CreateImageCopyTexture(dstTexture, dstLevel, dstOrigin, aspect);
-        wgpu::CopyTextureForBrowserOptions options = {};
 
         if (expectation == utils::Expectation::Success) {
             device.GetQueue().CopyTextureForBrowser(&srcImageCopyTexture, &dstImageCopyTexture,
@@ -254,3 +254,127 @@
     TestCopyTextureForBrowser(utils::Expectation::Failure, sourceMultiSampled4x, 0, {0, 0, 0},
                               destinationMultiSampled1x, 0, {0, 0, 0}, {0, 0, 1});
 }
+
+// Test color space conversion related attributes in CopyTextureForBrowserOptions.
+TEST_F(CopyTextureForBrowserTest, ColorSpaceConversion_ColorSpace) {
+    wgpu::Texture source =
+        Create2DTexture(16, 16, 5, 4, wgpu::TextureFormat::RGBA8Unorm,
+                        wgpu::TextureUsage::CopySrc | wgpu::TextureUsage::TextureBinding);
+    wgpu::Texture destination =
+        Create2DTexture(16, 16, 5, 4, wgpu::TextureFormat::RGBA8Unorm,
+                        wgpu::TextureUsage::CopyDst | wgpu::TextureUsage::RenderAttachment);
+
+    wgpu::CopyTextureForBrowserOptions options = {};
+    std::array<float, 7> srcTransferFunctionParameters = {};
+    std::array<float, 7> dstTransferFunctionParameters = {};
+    std::array<float, 9> conversionMatrix = {};
+    options.needsColorSpaceConversion = true;
+    options.srcTransferFunctionParameters = srcTransferFunctionParameters.data();
+    options.dstTransferFunctionParameters = dstTransferFunctionParameters.data();
+    options.conversionMatrix = conversionMatrix.data();
+    options.conversionMatrixElementsCount = 9;
+    options.transferFunctionParametersCount = 7;
+
+    // Valid cases
+    {
+        wgpu::CopyTextureForBrowserOptions validOptions = options;
+        TestCopyTextureForBrowser(utils::Expectation::Success, source, 0, {0, 0, 0}, destination, 0,
+                                  {0, 0, 0}, {4, 4, 1}, wgpu::TextureAspect::All, validOptions);
+
+        // if no color space conversion, no need to validate related attributes
+        wgpu::CopyTextureForBrowserOptions noColorSpaceConversion = options;
+        noColorSpaceConversion.needsColorSpaceConversion = false;
+        TestCopyTextureForBrowser(utils::Expectation::Success, source, 0, {0, 0, 0}, destination, 0,
+                                  {0, 0, 0}, {4, 4, 1}, wgpu::TextureAspect::All,
+                                  noColorSpaceConversion);
+    }
+
+    // Invalid cases: wrong transferFunctionParametersCount
+    {
+        // wrong: transferFunctionParametersCount must be 7
+        wgpu::CopyTextureForBrowserOptions invalidOptions = options;
+        invalidOptions.transferFunctionParametersCount = 6;
+        TestCopyTextureForBrowser(utils::Expectation::Failure, source, 0, {0, 0, 0}, destination, 0,
+                                  {0, 0, 0}, {4, 4, 1}, wgpu::TextureAspect::All, invalidOptions);
+    }
+
+    // Invalid cases: wrong conversionMatrixElementsCount
+    {
+        // wrong: conversionMatrixElementsCount
+        wgpu::CopyTextureForBrowserOptions invalidOptions = options;
+        invalidOptions.transferFunctionParametersCount = 10;
+        TestCopyTextureForBrowser(utils::Expectation::Failure, source, 0, {0, 0, 0}, destination, 0,
+                                  {0, 0, 0}, {4, 4, 1}, wgpu::TextureAspect::All, invalidOptions);
+    }
+
+    // Invalid cases: srcTransferFunctionParameters, dstTransferFunctionParameters or
+    // conversionMatrix is nullptr
+    {
+        wgpu::CopyTextureForBrowserOptions invalidOptions = options;
+        if (UsesWire()) {
+            invalidOptions.transferFunctionParametersCount = 0;
+        }
+        invalidOptions.srcTransferFunctionParameters = nullptr;
+        TestCopyTextureForBrowser(utils::Expectation::Failure, source, 0, {0, 0, 0}, destination, 0,
+                                  {0, 0, 0}, {4, 4, 1}, wgpu::TextureAspect::All, invalidOptions);
+    }
+
+    {
+        wgpu::CopyTextureForBrowserOptions invalidOptions = options;
+        if (UsesWire()) {
+            invalidOptions.transferFunctionParametersCount = 0;
+        }
+        invalidOptions.dstTransferFunctionParameters = nullptr;
+        TestCopyTextureForBrowser(utils::Expectation::Failure, source, 0, {0, 0, 0}, destination, 0,
+                                  {0, 0, 0}, {4, 4, 1}, wgpu::TextureAspect::All, invalidOptions);
+    }
+
+    {
+        wgpu::CopyTextureForBrowserOptions invalidOptions = options;
+        if (UsesWire()) {
+            invalidOptions.conversionMatrixElementsCount = 0;
+        }
+        invalidOptions.conversionMatrix = nullptr;
+        TestCopyTextureForBrowser(utils::Expectation::Failure, source, 0, {0, 0, 0}, destination, 0,
+                                  {0, 0, 0}, {4, 4, 1}, wgpu::TextureAspect::All, invalidOptions);
+    }
+}
+
+// Test option.srcAlphaMode/dstAlphaMode
+TEST_F(CopyTextureForBrowserTest, ColorSpaceConversion_TextureAlphaState) {
+    wgpu::Texture source =
+        Create2DTexture(16, 16, 5, 4, wgpu::TextureFormat::RGBA8Unorm,
+                        wgpu::TextureUsage::CopySrc | wgpu::TextureUsage::TextureBinding);
+    wgpu::Texture destination =
+        Create2DTexture(16, 16, 5, 4, wgpu::TextureFormat::RGBA8Unorm,
+                        wgpu::TextureUsage::CopyDst | wgpu::TextureUsage::RenderAttachment);
+
+    wgpu::CopyTextureForBrowserOptions options = {};
+
+    // Valid src texture alpha state and valid dst texture alpha state
+    {
+        options.srcAlphaMode = wgpu::AlphaMode::Premultiplied;
+        options.dstAlphaMode = wgpu::AlphaMode::Premultiplied;
+
+        TestCopyTextureForBrowser(utils::Expectation::Success, source, 0, {0, 0, 0}, destination, 0,
+                                  {0, 0, 0}, {4, 4, 1}, wgpu::TextureAspect::All, options);
+
+        options.srcAlphaMode = wgpu::AlphaMode::Premultiplied;
+        options.dstAlphaMode = wgpu::AlphaMode::Unpremultiplied;
+
+        TestCopyTextureForBrowser(utils::Expectation::Success, source, 0, {0, 0, 0}, destination, 0,
+                                  {0, 0, 0}, {4, 4, 1}, wgpu::TextureAspect::All, options);
+
+        options.srcAlphaMode = wgpu::AlphaMode::Unpremultiplied;
+        options.dstAlphaMode = wgpu::AlphaMode::Premultiplied;
+
+        TestCopyTextureForBrowser(utils::Expectation::Success, source, 0, {0, 0, 0}, destination, 0,
+                                  {0, 0, 0}, {4, 4, 1}, wgpu::TextureAspect::All, options);
+
+        options.srcAlphaMode = wgpu::AlphaMode::Unpremultiplied;
+        options.dstAlphaMode = wgpu::AlphaMode::Unpremultiplied;
+
+        TestCopyTextureForBrowser(utils::Expectation::Success, source, 0, {0, 0, 0}, destination, 0,
+                                  {0, 0, 0}, {4, 4, 1}, wgpu::TextureAspect::All, options);
+    }
+}