/*
 * Copyright 2026 Google LLC
 *
 * Use of this source code is governed by a BSD-style license that can be
 * found in the LICENSE file.
 */

#include "tests/Test.h"

#include "include/core/SkColorType.h"
#include "include/gpu/graphite/Context.h"
#include "include/gpu/graphite/TextureInfo.h"
#include "include/private/base/SkTArray.h"
#include "src/base/SkFloatBits.h"
#include "src/base/SkHalf.h"
#include "src/core/SkColorSpaceXformSteps.h"
#include "src/core/SkRasterPipeline.h"
#include "src/gpu/Swizzle.h"
#include "src/gpu/graphite/Caps.h"
#include "src/gpu/graphite/ContextPriv.h"
#include "src/gpu/graphite/TextureFormat.h"
#include "src/gpu/graphite/TextureFormatXferFn.h"
#include "src/gpu/graphite/TextureInfoPriv.h"
#include "tools/ToolUtils.h"

// Uncomment the SK_ABORT() to exit on first pixel mismatch to help debugging
#define STOP_ON_TRANSFER_FAILURE // SK_ABORT();

namespace skgpu::graphite {

// Types for defining color type and format behavior expectations
enum ChannelDataType {
    UNorm, Signed, Float, FNorm, sRGB, XR, Pad
};

struct Channel {
    char fName; // rgbayuv01 and G (for gray)
    int fBits;
    ChannelDataType fType;
};

struct ColorTypeExpectation {
    SkColorType fColorType;
    Swizzle fReadSwizzle;
    std::optional<Swizzle> fWriteSwizzle; // not set implies not renderable
};

struct FormatExpectation {
    TextureFormat fFormat;
    skia_private::TArray<Channel> fChannels;
    SkTextureCompressionType fCompressionType = SkTextureCompressionType::kNone;

    // Not set implies transfers are disabled; it is composed with a ColorTypeExpectation's
    // read or write swizzle to produce the expected readback/upload swizzle.
    std::optional<Swizzle> fXferSwizzle;
    // The first color type expectation is assumed to be the best fit.
    skia_private::TArray<ColorTypeExpectation> fCompatibleColorTypes;

    // All of the expectations for the fixed properties of a TextureFormat are derived from its
    // fChannels definition.
    bool isFloatingPoint() const { return this->hasType(Float) || this->hasType(FNorm); }
    bool hasDepth() const { return this->hasChannel('d'); }
    bool hasStencil() const { return this->hasChannel('s'); }
    bool hasDepthOrStencil() const { return this->hasDepth() || this->hasStencil(); }
    bool isMultiplanar() const {
        return this->hasChannel('y') && this->hasChannel('u') && this->hasChannel('v');
    }

    bool autoClamps() const {
        // Auto clamping is derived from the type of the channel with the most bits
        bool autoClamp = true;
        int maxBitSize = 0;
        for (const Channel& c : fChannels) {
            if (c.fBits > maxBitSize) {
                autoClamp = c.fType == UNorm || c.fType == sRGB;
                maxBitSize = c.fBits;
            }
        }
        return autoClamp;
    }

    int bytesPerBlock() const {
        if (fCompressionType != SkTextureCompressionType::kNone) {
            // At the moment, all supported compression types have the same bytes per block
            return 8;
        }

        int bitCount = 0;
        for (const Channel& c : fChannels) {
            bitCount += c.fBits;
        }
        return bitCount / 8;
    }

    uint32_t channelMask() const {
        uint32_t mask = 0;
        if (this->hasChannel('r') || this->hasChannel('y')) {
            mask |= kRed_SkColorChannelFlag;
        }
        if (this->hasChannel('g') || this->hasChannel('u')) {
            mask |= kGreen_SkColorChannelFlag;
        }
        if (this->hasChannel('b') || this->hasChannel('v')) {
            mask |= kBlue_SkColorChannelFlag;
        }
        if (this->hasChannel('a')) {
            mask |= kAlpha_SkColorChannelFlag;
        }
        // Other channels do not contribute to SkColorChannel mask
        return mask;
    }

private:
    bool hasChannel(char channel) const {
        for (const Channel& c : fChannels) {
            if (c.fName == channel) {
                return true;
            }
        }
        return false;
    }
    bool hasType(ChannelDataType type) const {
        for (const Channel& c : fChannels) {
            if (c.fType == type) {
                return true;
            }
        }
        return false;
    }
};

using PixelData = std::array<uint8_t, 16>; // The largest texel/pixel size is RGBA32F = 16 bytes

uint32_t channel_to_bits(const Channel& channel, float value) {
    switch (channel.fType) {
        case Pad:
            // Pad should only be used with 'x', which produces NaN in channel_to_float, so
            // replace `value` with the default 'x' bit pattern in gen_channel_values, and then
            // fall through to UNorm handling to adjust it to the right bit depth
            SkASSERT(channel.fName == 'x');
            value = 0b0101 / 15.f;
            [[fallthrough]];

        case sRGB:
            // sRGB data is stored in a non-linear gamma and automatically decodes to linear when
            // being sampled or rendered into. This means an SRGB_8888 image with a linear
            // SkColorSpace behaves like a regular 8888 image with an sRGB SkColorSpace. But
            // transfers between CPU and GPU assume the SkColorSpace is the same, so we need to
            // interpret the `v` as if it were an 8888 image with a linear SkColorSpace and map to
            // the sRGB encoding.
            if (channel.fType == sRGB) {
                value = skcms_TransferFunction_eval(skcms_sRGB_Inverse_TransferFunction(), value);
            }
            [[fallthrough]];

        case UNorm:
            return (uint32_t) std::round(value * ((1 << channel.fBits) - 1));

        case XR:
            // See SkRP_opts::store_1010102_xr
            SkASSERT(channel.fBits == 10);
            return std::min((uint32_t) std::round(value * 510 + 384), 1023u);

        case FNorm:
            // For simplicity fall through to Float, the Norm is just a hint about range
            [[fallthrough]];
        case Float:
            SkASSERT(channel.fBits == 16 || channel.fBits == 32);
            return channel.fBits == 16 ? (uint32_t) SkFloatToHalf(value) : SkFloat2Bits(value);

        case Signed:
            // This should only be used for 's' channels, which are already not reaching this code
            SK_ABORT("Should not be generating Signed values for pixel data");
    }
    SkUNREACHABLE;
}

float channel_to_float(const Channel& channel, uint32_t bits) {
    switch (channel.fType) {
        case sRGB: [[fallthrough]]; // first treat as unorm then apply gamma TF
        case UNorm: {
            float vf = bits * (1 / (float) ((1 << channel.fBits) - 1));
            if (channel.fType == sRGB) {
                vf = skcms_TransferFunction_eval(skcms_sRGB_TransferFunction(), vf);
            }
            return vf;
        }

        case XR:     return (bits - 384.f) * (1/510.f);

        case FNorm:  [[fallthrough]]; // Values are interpreted the same, values are in [0,1]
        case Float:  return channel.fBits == 16 ? SkHalfToFloat((SkHalf) bits) : SkBits2Float(bits);

        case Signed: [[fallthrough]];
        case Pad:    return SK_FloatNaN; // No floating point printing
    }
    SkUNREACHABLE;
}

char apply_swizzle_to_name(char dstName, const Swizzle& srcToDst) {
    switch (dstName) {
        case 'r': return srcToDst[0];
        case 'g': return srcToDst[1];
        case 'b': return srcToDst[2];
        case 'a': return srcToDst[3];
        default:  return dstName;
    }
}

// Generate a unique value for `channelName` by applying `srcToDst` to the source channels.
float gen_channel_value(char name,
                        const Swizzle& loadSrcSwizzle,
                        SkSpan<const Channel> srcChannels) {
    name = apply_swizzle_to_name(name, loadSrcSwizzle);

    // Try to look for an exact channel match, in which case we use the unique value corresponding
    // to that channel name.
    for (size_t c = 0; c < srcChannels.size(); ++c) {
        if (srcChannels[c].fName == name ||
            (srcChannels[c].fName == 'G' && (name == 'r' || name == 'g' || name == 'b'))) {
            uint32_t v;
            switch (srcChannels[c].fName) {
                case 'r': v = 0b0001; break;
                case 'g': v = 0b0010; break;
                case 'b': v = 0b0100; break;
                case 'a': v = 0b1010; break; // `A` must additionally fit into 2 bit channel formats
                case 'x': v = 0b0101; break;
                case '0': v = 0b0000; break;
                case '1': v = 0b1111; break;
                case 'G': v = 0b1100; break; // arbitrary starting Gray value
                default:
                    // NOTE: 'd', 's', 'y', 'u', 'v' are valid channel names, but the formats that
                    // have those should not be participating in the read/write transfer testing.
                    SK_ABORT("Bad source channel name for pixel data generation: %c", name);
            }
            // Route through the source channel's bit representation to account for rounding
            return channel_to_float(srcChannels[c], channel_to_bits(srcChannels[c], v / 15.f));
        }
    }

    // We didn't find an exact channel match, so we either use default values or apply gray handling
    switch (name) {
        // Trivial default values
        case 'x':                               return SK_FloatNaN;
        case 'r': case 'g': case 'b': case '0': return 0.f;
        case 'a': case '1':                     return 1.f;

        // Construct a gray value from r, g, and b values of the source
        case 'G': {
            float r = gen_channel_value('r', loadSrcSwizzle, srcChannels);
            float g = gen_channel_value('g', loadSrcSwizzle, srcChannels);
            float b = gen_channel_value('b', loadSrcSwizzle, srcChannels);

            // Since this is a linear sum, if we divided or multiplied by A after converting
            // through source's precision, we would arrive at the same value as if we applied
            // the premul/unpremul multiplication in gen_pixel_data applied to G
            return 0.2126f * r + 0.7142f * g + 0.0722f * b;
        }

        default:
            SK_ABORT("Bad dst channel name for pixel data generation: %c", name);
    }
}

// Src and Dst must be (uint32_t, PixelData) or (PixelData, uint32_t), and `dst` should be
// zero-initialized before calling. Returns the bitOffset for the next channel.
template <typename Src, typename Dst>
int copy_unaligned_bits(int numBits, int bitOffset, const Src& src, Dst& dst) {
    static_assert(std::is_same_v<Src, uint32_t> || std::is_same_v<Src, PixelData>);
    static_assert(std::is_same_v<Dst, uint32_t> || std::is_same_v<Dst, PixelData>);
    static_assert(!std::is_same_v<Src, Dst>);
    static constexpr bool kBytesToChannel = std::is_same_v<Src, PixelData>;

    int srcBitsLeft = numBits;
    while (srcBitsLeft > 0) {
        int arrayIndex = bitOffset / 8;
        int arrayBitsLeft = (arrayIndex + 1) * 8 - bitOffset;
        int copyBits = std::min(arrayBitsLeft, srcBitsLeft);

        int channelBitShift = numBits - srcBitsLeft;
        int arrayBitShift = 8 - arrayBitsLeft;

        if constexpr (kBytesToChannel) {
            dst |= ((src[arrayIndex] >> arrayBitShift) & ((1<<copyBits) - 1)) << channelBitShift;
        } else {
            dst[arrayIndex] |= ((src >> channelBitShift) & ((1<<copyBits) - 1)) << arrayBitShift;
        }
        srcBitsLeft -= copyBits;
        bitOffset += copyBits;
    }
    return bitOffset;
}

bool needs_premul(SkAlphaType srcAlphaType, SkAlphaType dstAlphaType) {
    return dstAlphaType == kPremul_SkAlphaType && (srcAlphaType == kUnpremul_SkAlphaType ||
                                                   srcAlphaType == kUnknown_SkAlphaType);
}
bool needs_unpremul(SkAlphaType srcAlphaType, SkAlphaType dstAlphaType) {
    return srcAlphaType == kPremul_SkAlphaType && (dstAlphaType == kUnpremul_SkAlphaType ||
                                                   dstAlphaType == kUnknown_SkAlphaType);
}

PixelData gen_pixel_data(SkSpan<const Channel> dstChannels,
                         Swizzle storeDstSwizzle,
                         SkAlphaType dstAlphaType,
                         SkSpan<const Channel> srcChannels,
                         Swizzle loadSrcSwizzle,
                         SkAlphaType srcAlphaType) {
    PixelData pixel{}; // zero-initialize all bytes
    int bitOffset = 0;
    for (size_t c = 0; c < dstChannels.size(); ++c) {
        char name = apply_swizzle_to_name(dstChannels[c].fName, storeDstSwizzle);
        float srcValue = gen_channel_value(name, loadSrcSwizzle, srcChannels);

        // Handle alpha type conversion
        if (name == 'a') {
            if (srcAlphaType == kOpaque_SkAlphaType) {
                srcValue = 1.f;
            }
        } else if (name == 'r' || name == 'g' || name == 'b' || name == 'G') {
            if (needs_premul(srcAlphaType, dstAlphaType)) {
                // Must multiply other channels by the alpha value
                const float srcAlpha = gen_channel_value('a', loadSrcSwizzle, srcChannels);
                srcValue *= srcAlpha;
            } else if (needs_unpremul(srcAlphaType, dstAlphaType)) {
                // Must divide the other channels by the alpha value
                const float srcAlpha = gen_channel_value('a', loadSrcSwizzle, srcChannels);
                srcValue *= 1.f / srcAlpha;
            }
            // else we're the same alpha type on both sides, or a mix of opaque and unknown, in
            // which case premul vs. unpremul is a no-op.
        }

        uint32_t channelValue = channel_to_bits(dstChannels[c], srcValue);
        bitOffset = copy_unaligned_bits(dstChannels[c].fBits, bitOffset, channelValue, pixel);
    }

    return pixel;
}

// No swizzling or data conversion variant for input data generation
PixelData gen_pixel_data(SkSpan<const Channel> channels, Swizzle readSwizzle, SkAlphaType alphaType) {
    return gen_pixel_data(channels, readSwizzle.invert(), alphaType,
                          channels, readSwizzle, alphaType);
}

const char* channel_type_name(ChannelDataType type) {
    switch (type) {
        case UNorm:  return "u";
        case Signed: return "s";
        case Float:  return "f";
        case FNorm:  return "fn";
        case sRGB:   return "srgb";
        case XR:     return "xr";
        case Pad:    return "_";
    }
    SkUNREACHABLE;
}

skia_private::TArray<SkString> print_channel_names(SkSpan<const Channel> channels) {
    skia_private::TArray<SkString> names;
    names.push_back(SkString()); // empty to align with channel values row label
    names.push_back(SkString("raw"));
    for (const Channel& c : channels) {
        const char* typeName = channel_type_name(c.fType);
        names.push_back(SkStringPrintf("%c(%s%d)", c.fName, typeName, c.fBits));
    }
    return names;
}

skia_private::TArray<SkString> print_channel_values(
        const char* prefix, SkSpan<const Channel> channels, const PixelData& pixel) {
    skia_private::TArray<SkString> values;

    values.push_back(SkString(prefix));

    values.push_back(SkString()); // Raw hex value
    for (int i = 0; i < 16; ++i) {
        values[1].appendf(" %0.2x", pixel[i]);
    }

    int bitOffset = 0;
    for (const Channel& c : channels) {
        uint32_t channelValue = 0;
        bitOffset = copy_unaligned_bits(c.fBits, bitOffset, pixel, channelValue);

        SkString binary;
        for (int i = 0; i < c.fBits; ++i) {
            binary.prependUnichar((channelValue >> i) & 1 ? '1' : '0');
        }
        float vf = channel_to_float(c, channelValue);

        SkString numeric;
        switch (c.fType) {
            case Pad:    numeric = "-"; break;
            case Signed: numeric = SkStringPrintf("%u", channelValue); break;

            case sRGB:   [[fallthrough]];
            case XR:
            case UNorm:  numeric = SkStringPrintf("0x%x / %.2f", channelValue, vf); break;

            case FNorm:  [[fallthrough]];
            case Float:  numeric = SkStringPrintf("%0.4f", vf); break;
        }
        values.push_back(SkStringPrintf("%s (%s)", binary.c_str(), numeric.c_str()));
    }

    return values;
}

void dump_string(const SkString& str, int length) {
    int strLen = SkTo<int>(str.size());
    SkASSERT(strLen <= length);
    SkDebugf("%*s", std::min(length, strLen), str.data());
    length -= strLen;
    while(length > 0) {
        SkDebugf(" ");
        length--;
    }
}

void dump_pixel_comparison(const SkString& inputName,
                           SkSpan<const Channel> inputChannels,
                           const PixelData& inputPixel,
                           const SkString& outputName,
                           SkSpan<const Channel> outputChannels,
                           const PixelData& expectedOutputPixel,
                           const PixelData& actualOutputPixel) {
    SkDebugf("Transfer from %s to %s:\n", inputName.c_str(), outputName.c_str());

    auto inputChannelNames = print_channel_names(inputChannels);
    auto inputChannelValues = print_channel_values("Input", inputChannels, inputPixel);

    auto outputChannelNames = print_channel_names(outputChannels);
    auto expectedChannelValues =
            print_channel_values("Expected", outputChannels, expectedOutputPixel);
    auto actualChannelValues =
            print_channel_values("Actual", outputChannels, actualOutputPixel);

    const int colCount = std::max(inputChannelNames.size(), outputChannelNames.size());
    auto rows = {inputChannelNames,
                 inputChannelValues,
                 outputChannelNames,
                 expectedChannelValues,
                 actualChannelValues};
    for (auto&& row : rows) {
        for (int c = 0; c < std::min(colCount, row.size()); ++c) {
            if (c == 0) {
                SkDebugf(" ");
            } else {
                SkDebugf(" | ");
            }
            int colWidth = 0;
            for (auto&& otherRow : rows) {
                if (c < otherRow.size()) {
                    colWidth = std::max(colWidth, SkTo<int>(otherRow[c].size()));
                }
            }
            dump_string(row[c], colWidth);
        }
        SkDebugf("\n");
    }
}

int channel_tolerance(SkSpan<const Channel> srcChannels, SkAlphaType srcAT,
                      SkSpan<const Channel> dstChannels, SkAlphaType dstAT) {
    bool srcHasSRGBOrGray = false;
    for (const Channel c : srcChannels) {
        srcHasSRGBOrGray |= c.fType == sRGB || c.fName == 'G';
    }
    bool dstHasSRGBOrGray = false;
    for (const Channel c : dstChannels) {
         dstHasSRGBOrGray |= c.fType == sRGB || c.fName == 'G';
    }
    // If the input or output data involves sRGB encoding/decoding or conversion to gray, or are
    // doing premul/unpremul math, we allow 1 bit of difference because of mismatches between how
    // SkRasterPipeline calculates channel values vs. the value generation in these tests.
    return srcHasSRGBOrGray ||
           dstHasSRGBOrGray ||
           needs_premul(srcAT, dstAT) ||
           needs_unpremul(srcAT, dstAT) ? 1 : 0;
}

bool compare_pixels(SkSpan<const Channel> channels,
                    const PixelData& expected,
                    const PixelData& actual,
                    int channelTolerance) {
    int bitOffset = 0;
    for (const Channel& c : channels) {
        uint32_t actualChannelBits = 0;
        uint32_t expectedChannelBits = 0;
        copy_unaligned_bits(c.fBits, bitOffset, expected, expectedChannelBits);
        copy_unaligned_bits(c.fBits, bitOffset, actual, actualChannelBits);
        bitOffset += c.fBits;

        int64_t channelDiff = static_cast<int64_t>(actualChannelBits) -
                              static_cast<int64_t>(expectedChannelBits);
        if (c.fType != Pad && std::abs(channelDiff) > channelTolerance) {
            return false;
        }
    }
    return true;
}

PixelData transfer_data(const TextureFormatXferFn& xferFn, const PixelData& inputData) {
    PixelData outputData{}; // zero-initialize for comparison stability on unwritten values.
    xferFn.run(1, 1, &inputData, sizeof(PixelData), &outputData, sizeof(PixelData));
    return outputData;
}

// Define the channel layout for every SkColorType for use in generating and validating the
// result of transferring data to or from a texture format.
[[maybe_unused]] static const struct ColorTypeChannels {
    SkColorType fColorType;
    Swizzle fEffectiveSwizzle; // Derivable from channel mask
    skia_private::TArray<Channel> fChannels;
} kColorTypeChannels[] {
    {kAlpha_8_SkColorType,            Swizzle("000a"), {{'a', 8, UNorm}}},
    // NOTE: 565 and 4444 are misnamed and are BGR and ABGR respectively.
    {kRGB_565_SkColorType,            Swizzle("rgb1"), {{'b', 5, UNorm},
                                                        {'g', 6, UNorm},
                                                        {'r', 5, UNorm}}},
    {kARGB_4444_SkColorType,          Swizzle("rgba"), {{'a', 4, UNorm},
                                                        {'b', 4, UNorm},
                                                        {'g', 4, UNorm},
                                                        {'r', 4, UNorm}}},
    {kRGBA_8888_SkColorType,          Swizzle("rgba"), {{'r', 8, UNorm},
                                                        {'g', 8, UNorm},
                                                        {'b', 8, UNorm},
                                                        {'a', 8, UNorm}}},
    {kRGB_888x_SkColorType,           Swizzle("rgb1"), {{'r', 8, UNorm},
                                                        {'g', 8, UNorm},
                                                        {'b', 8, UNorm},
                                                        {'x', 8, Pad}}},
    {kBGRA_8888_SkColorType,          Swizzle("rgba"), {{'b', 8, UNorm},
                                                        {'g', 8, UNorm},
                                                        {'r', 8, UNorm},
                                                        {'a', 8, UNorm}}},
    {kRGBA_1010102_SkColorType,       Swizzle("rgba"), {{'r', 10, UNorm},
                                                        {'g', 10, UNorm},
                                                        {'b', 10, UNorm},
                                                        {'a', 2, UNorm}}},
    {kBGRA_1010102_SkColorType,       Swizzle("rgba"), {{'b', 10, UNorm},
                                                        {'g', 10, UNorm},
                                                        {'r', 10, UNorm},
                                                        {'a', 2, UNorm}}},
    {kRGB_101010x_SkColorType,        Swizzle("rgb1"), {{'r', 10, UNorm},
                                                        {'g', 10, UNorm},
                                                        {'b', 10, UNorm},
                                                        {'x', 2, Pad}}},
    {kBGR_101010x_SkColorType,        Swizzle("rgb1"), {{'b', 10, UNorm},
                                                        {'g', 10, UNorm},
                                                        {'r', 10, UNorm},
                                                        {'x', 2, Pad}}},
    {kBGR_101010x_XR_SkColorType,     Swizzle("rgb1"), {{'b', 10, XR},
                                                        {'g', 10, XR},
                                                        {'r', 10, XR},
                                                        {'x', 2, Pad}}},
    {kBGRA_10101010_XR_SkColorType,   Swizzle("rgba"), {{'x', 6, Pad}, {'b', 10, XR},
                                                        {'x', 6, Pad}, {'g', 10, XR},
                                                        {'x', 6, Pad}, {'r', 10, XR},
                                                        {'x', 6, Pad}, {'a', 10, XR}}},
    {kRGBA_10x6_SkColorType,          Swizzle("rgba"), {{'x', 6, Pad}, {'r', 10, UNorm},
                                                        {'x', 6, Pad}, {'g', 10, UNorm},
                                                        {'x', 6, Pad}, {'b', 10, UNorm},
                                                        {'x', 6, Pad}, {'a', 10, UNorm}}},
    // NOTE: The swizzle is rrr1 since we store gray in the red channel of the texture, but we use
    // 'G' as the channel to force generating gray/luminance values in the tests instead of just 'r'
    {kGray_8_SkColorType,             Swizzle("rrr1"), {{'G', 8, UNorm}}},
    {kRGBA_F16Norm_SkColorType,       Swizzle("rgba"), {{'r', 16, FNorm},
                                                        {'g', 16, FNorm},
                                                        {'b', 16, FNorm},
                                                        {'a', 16, FNorm}}},
    {kRGBA_F16_SkColorType,           Swizzle("rgba"), {{'r', 16, Float},
                                                        {'g', 16, Float},
                                                        {'b', 16, Float},
                                                        {'a', 16, Float}}},
    {kRGB_F16F16F16x_SkColorType,     Swizzle("rgb1"), {{'r', 16, Float},
                                                        {'g', 16, Float},
                                                        {'b', 16, Float},
                                                        {'x', 16, Pad}}},
    {kRGBA_F32_SkColorType,           Swizzle("rgba"), {{'r', 32, Float},
                                                        {'g', 32, Float},
                                                        {'b', 32, Float},
                                                        {'a', 32, Float}}},
    {kR8G8_unorm_SkColorType,         Swizzle("rg01"), {{'r', 8, UNorm}, {'g', 8, UNorm}}},
    {kA16_float_SkColorType,          Swizzle("000a"), {{'a', 16, Float}}},
    {kR16_float_SkColorType,          Swizzle("r001"), {{'r', 16, Float}}},
    {kR16G16_float_SkColorType,       Swizzle("rg01"), {{'r', 16, Float}, {'g', 16, Float}}},
    {kA16_unorm_SkColorType,          Swizzle("000a"), {{'a', 16, UNorm}}},
    {kR16_unorm_SkColorType,          Swizzle("r001"), {{'r', 16, UNorm}}},
    {kR16G16_unorm_SkColorType,       Swizzle("rg01"), {{'r', 16, UNorm}, {'g', 16, UNorm}}},
    {kR16G16B16A16_unorm_SkColorType, Swizzle("rgba"), {{'r', 16, UNorm},
                                                        {'g', 16, UNorm},
                                                        {'b', 16, UNorm},
                                                        {'a', 16, UNorm}}},
    {kSRGBA_8888_SkColorType,         Swizzle("rgba"), {{'r', 8, sRGB},
                                                        {'g', 8, sRGB},
                                                        {'b', 8, sRGB},
                                                        {'a', 8, UNorm}}},
    {kR8_unorm_SkColorType,           Swizzle("r001"), {{'r', 8, UNorm}}},
};
// Must include one per SkColorType except for kUnknown
static_assert(std::size(kColorTypeChannels) == kSkColorTypeCnt - 1,
              "Missing channel definition for SkColorType");

// Full definition of compatibility and behavior between SkColorType and TextureFormat:
static const FormatExpectation kExpectations[] {
    {.fFormat=TextureFormat::kUnsupported,
     .fChannels={},
     .fXferSwizzle=std::nullopt,
     .fCompatibleColorTypes={}},

    {.fFormat=TextureFormat::kR8,
     .fChannels={{'r', 8, UNorm}},
     .fXferSwizzle=Swizzle("r001"),
     .fCompatibleColorTypes={{kR8_unorm_SkColorType, Swizzle::RGBA(), Swizzle::RGBA()},
                             {kAlpha_8_SkColorType,  Swizzle("000r"), Swizzle("a000")},
                             {kGray_8_SkColorType,   Swizzle("rrra"), std::nullopt}}},

    {.fFormat=TextureFormat::kR16,
     .fChannels={{'r', 16, UNorm}},
     .fXferSwizzle=Swizzle("r001"),
     .fCompatibleColorTypes={{kR16_unorm_SkColorType, Swizzle::RGBA(), Swizzle::RGBA()},
                             {kA16_unorm_SkColorType, Swizzle("000r"), Swizzle("a000")}}},

    {.fFormat=TextureFormat::kR16F,
     .fChannels={{'r', 16, Float}},
     .fXferSwizzle=Swizzle("r001"),
     .fCompatibleColorTypes={{kR16_float_SkColorType, Swizzle::RGBA(), Swizzle::RGBA()},
                             {kA16_float_SkColorType, Swizzle("000r"), Swizzle("a000")}}},

    {.fFormat=TextureFormat::kR32F,
     .fChannels={{'r', 32, Float}},
     .fXferSwizzle=std::nullopt,
     .fCompatibleColorTypes={{kR16_float_SkColorType, Swizzle::RGBA(), Swizzle::RGBA()}}},

    {.fFormat=TextureFormat::kA8,
     .fChannels={{'a', 8, UNorm}},
     .fXferSwizzle=Swizzle("000a"),
     .fCompatibleColorTypes={{kAlpha_8_SkColorType, Swizzle::RGBA(), Swizzle::RGBA()}}},

    {.fFormat=TextureFormat::kRG8,
     .fChannels={{'r', 8, UNorm}, {'g', 8, UNorm}},
     .fXferSwizzle=Swizzle("rg01"),
     .fCompatibleColorTypes={{kR8G8_unorm_SkColorType, Swizzle::RGBA(), Swizzle::RGBA()}}},

    {.fFormat=TextureFormat::kRG16,
     .fChannels={{'r', 16, UNorm}, {'g', 16, UNorm}},
     .fXferSwizzle=Swizzle("rg01"),
     .fCompatibleColorTypes={{kR16G16_unorm_SkColorType, Swizzle::RGBA(), Swizzle::RGBA()}}},

    {.fFormat=TextureFormat::kRG16F,
     .fChannels={{'r', 16, Float}, {'g', 16, Float}},
     .fXferSwizzle=Swizzle("rg01"),
     .fCompatibleColorTypes={{kR16G16_float_SkColorType, Swizzle::RGBA(), Swizzle::RGBA()}}},

    {.fFormat=TextureFormat::kRG32F,
     .fChannels={{'r', 32, Float}, {'g', 32, Float}},
     .fXferSwizzle=std::nullopt,
     .fCompatibleColorTypes={{kR16G16_float_SkColorType, Swizzle::RGBA(), Swizzle::RGBA()}}},

    {.fFormat=TextureFormat::kRGB8,
     .fChannels={{'r', 8, UNorm}, {'g', 8, UNorm}, {'b', 8, UNorm}},
     .fXferSwizzle=Swizzle("rgb1"),
     .fCompatibleColorTypes={{kRGB_888x_SkColorType, Swizzle::RGBA(), Swizzle::RGBA()}}},

    {.fFormat=TextureFormat::kBGR8,
     .fChannels={{'b', 8, UNorm}, {'g', 8, UNorm}, {'r', 8, UNorm}},
     .fXferSwizzle=Swizzle("rgb1"),
     .fCompatibleColorTypes={{kRGB_888x_SkColorType, Swizzle::RGBA(), Swizzle::RGBA()}}},

    {.fFormat=TextureFormat::kB5_G6_R5,
     .fChannels={{'b', 5, UNorm}, {'g', 6, UNorm}, {'r', 5, UNorm}},
     .fXferSwizzle=Swizzle("rgb1"),
     .fCompatibleColorTypes={{kRGB_565_SkColorType, Swizzle::RGBA(), Swizzle::RGBA()}}},

    {.fFormat=TextureFormat::kR5_G6_B5,
     .fChannels={{'r', 5, UNorm}, {'g', 6, UNorm}, {'b', 5, UNorm}},
     .fXferSwizzle=Swizzle("rgb1"),
     .fCompatibleColorTypes={{kRGB_565_SkColorType, Swizzle::RGBA(), Swizzle::RGBA()}}},

    {.fFormat=TextureFormat::kRGB16,
     .fChannels={{'r', 16, UNorm}, {'g', 16, UNorm}, {'b', 16, UNorm}},
     .fXferSwizzle=Swizzle("rgb1"),
     .fCompatibleColorTypes={{kR16G16B16A16_unorm_SkColorType, Swizzle::RGBA(), Swizzle::RGBA()}}},

    {.fFormat=TextureFormat::kRGB16F,
     .fChannels={{'r', 16, Float}, {'g', 16, Float}, {'b', 16, Float}},
     .fXferSwizzle=Swizzle("rgb1"),
     .fCompatibleColorTypes={{kRGB_F16F16F16x_SkColorType, Swizzle::RGBA(), Swizzle::RGBA()}}},

    {.fFormat=TextureFormat::kRGB32F,
     .fChannels={{'r', 32, Float}, {'g', 32, Float}, {'b', 32, Float}},
     .fXferSwizzle=Swizzle("rgb1"),
     .fCompatibleColorTypes={{kRGBA_F32_SkColorType, Swizzle::RGBA(), Swizzle::RGBA()}}},

    {.fFormat=TextureFormat::kRGB8_sRGB,
     .fChannels={{'r', 8, sRGB}, {'g', 8, sRGB}, {'b', 8, sRGB}},
     .fXferSwizzle=Swizzle("rgb1"),
     .fCompatibleColorTypes={{kSRGBA_8888_SkColorType, Swizzle::RGBA(), Swizzle::RGBA()}}},

    {.fFormat=TextureFormat::kBGR10_XR,
     .fChannels={{'b', 10, XR}, {'g', 10, XR}, {'r', 10, XR}, {'x', 2, Pad}},
     .fXferSwizzle=Swizzle("rgb1"),
     .fCompatibleColorTypes={{kBGR_101010x_XR_SkColorType, Swizzle::RGBA(), Swizzle::RGBA()}}},

    {.fFormat=TextureFormat::kRGBA8,
     .fChannels={{'r', 8, UNorm}, {'g', 8, UNorm}, {'b', 8, UNorm}, {'a', 8, UNorm}},
     .fXferSwizzle=Swizzle("rgba"),
     .fCompatibleColorTypes={{kRGBA_8888_SkColorType, Swizzle::RGBA(), Swizzle::RGBA()},
                             {kBGRA_8888_SkColorType, Swizzle::RGBA(), Swizzle::RGBA()},
                             {kRGB_888x_SkColorType,  Swizzle::RGB1(), std::nullopt}}},

    {.fFormat=TextureFormat::kRGBA16,
     .fChannels={{'r', 16, UNorm}, {'g', 16, UNorm}, {'b', 16, UNorm}, {'a', 16, UNorm}},
     .fXferSwizzle=Swizzle("rgba"),
     .fCompatibleColorTypes={{kR16G16B16A16_unorm_SkColorType, Swizzle::RGBA(), Swizzle::RGBA()}}},

    {.fFormat=TextureFormat::kRGBA16F,
     .fChannels={{'r', 16, Float}, {'g', 16, Float}, {'b', 16, Float}, {'a', 16, Float}},
     .fXferSwizzle=Swizzle("rgba"),
     .fCompatibleColorTypes={{kRGBA_F16_SkColorType,       Swizzle::RGBA(), Swizzle::RGBA()},
                             {kRGBA_F16Norm_SkColorType,   Swizzle::RGBA(), Swizzle::RGBA()},
                             {kRGB_F16F16F16x_SkColorType, Swizzle::RGB1(), std::nullopt}}},

    {.fFormat=TextureFormat::kRGBA32F,
     .fChannels={{'r', 32, Float}, {'g', 32, Float}, {'b', 32, Float}, {'a', 32, Float}},
     .fXferSwizzle=Swizzle("rgba"),
     .fCompatibleColorTypes={{kRGBA_F32_SkColorType, Swizzle::RGBA(), Swizzle::RGBA()}}},

    {.fFormat=TextureFormat::kRGB10_A2,
     .fChannels={{'r', 10, UNorm}, {'g', 10, UNorm}, {'b', 10, UNorm}, {'a', 2, UNorm}},
     .fXferSwizzle=Swizzle("rgba"),
     .fCompatibleColorTypes={{kRGBA_1010102_SkColorType, Swizzle::RGBA(), Swizzle::RGBA()},
                             {kRGB_101010x_SkColorType,  Swizzle::RGB1(), std::nullopt},
                             {kBGRA_1010102_SkColorType, Swizzle::RGBA(), Swizzle::RGBA()},
                             {kBGR_101010x_SkColorType,  Swizzle::RGB1(), std::nullopt}}},

    {.fFormat=TextureFormat::kRGBA10x6,
     .fChannels={{'x', 6, Pad}, {'r', 10, UNorm}, {'x', 6, Pad}, {'g', 10, UNorm},
                 {'x', 6, Pad}, {'b', 10, UNorm}, {'x', 6, Pad}, {'a', 10, UNorm}},
     .fXferSwizzle=Swizzle("rgba"),
     .fCompatibleColorTypes={{kRGBA_10x6_SkColorType, Swizzle::RGBA(), Swizzle::RGBA()}}},

    {.fFormat=TextureFormat::kRGBA8_sRGB,
     .fChannels={{'r', 8, sRGB}, {'g', 8, sRGB}, {'b', 8, sRGB}, {'a', 8, UNorm}},
     .fXferSwizzle=Swizzle("rgba"),
     .fCompatibleColorTypes={{kSRGBA_8888_SkColorType, Swizzle::RGBA(), Swizzle::RGBA()}}},

    {.fFormat=TextureFormat::kBGRA8,
     .fChannels={{'b', 8, UNorm}, {'g', 8, UNorm}, {'r', 8, UNorm}, {'a', 8, UNorm}},
     .fXferSwizzle=Swizzle("rgba"),
     .fCompatibleColorTypes={{kBGRA_8888_SkColorType, Swizzle::RGBA(), Swizzle::RGBA()},
                             {kRGBA_8888_SkColorType, Swizzle::RGBA(), Swizzle::RGBA()},
                             {kRGB_888x_SkColorType,  Swizzle::RGB1(), std::nullopt}}},

    {.fFormat=TextureFormat::kBGR10_A2,
     .fChannels={{'b', 10, UNorm}, {'g', 10, UNorm}, {'r', 10, UNorm}, {'a', 2, UNorm}},
     .fXferSwizzle=Swizzle("rgba"),
     .fCompatibleColorTypes={{kBGRA_1010102_SkColorType, Swizzle::RGBA(), Swizzle::RGBA()},
                             {kBGR_101010x_SkColorType,  Swizzle::RGB1(), std::nullopt},
                             {kRGBA_1010102_SkColorType, Swizzle::RGBA(), Swizzle::RGBA()},
                             {kRGB_101010x_SkColorType,  Swizzle::RGB1(), std::nullopt}}},

    {.fFormat=TextureFormat::kBGRA8_sRGB,
     .fChannels={{'b', 8, sRGB}, {'g', 8, sRGB}, {'r', 8, sRGB}, {'a', 8, UNorm}},
     .fXferSwizzle=Swizzle("rgba"),
     .fCompatibleColorTypes={{kSRGBA_8888_SkColorType, Swizzle::RGBA(), Swizzle::RGBA()}}},

    {.fFormat=TextureFormat::kABGR4,
     .fChannels={{'a', 4, UNorm}, {'b', 4, UNorm}, {'g', 4, UNorm}, {'r', 4, UNorm}},
     .fXferSwizzle=Swizzle("rgba"),
     .fCompatibleColorTypes={{kARGB_4444_SkColorType, Swizzle::RGBA(), Swizzle::RGBA()}}},

    {.fFormat=TextureFormat::kARGB4,
     // TODO(michaelludwig): kARGB_4444 color type is actually BGRA order. Historically, we
     // configured kARGB4 format to swizzle the channels on read and write in the shader so that the
     // CPU data could be uploaded directly. When we can perform a RB channel swap as part of
     // upload/readback, then this can change to RGBA swizzles.
     .fChannels={{'a', 4, UNorm}, {'r', 4, UNorm}, {'g', 4, UNorm}, {'b', 4, UNorm}},
     .fXferSwizzle=Swizzle("rgba"),
     .fCompatibleColorTypes={{kARGB_4444_SkColorType, Swizzle::BGRA(), Swizzle::BGRA()}}},

    {.fFormat=TextureFormat::kBGRA10x6_XR,
     .fChannels={{'x', 6, Pad}, {'b', 10, XR}, {'x', 6, Pad}, {'g', 10, XR},
                 {'x', 6, Pad}, {'r', 10, XR}, {'x', 6, Pad}, {'a', 10, XR}},
     .fXferSwizzle=Swizzle("rgba"),
     .fCompatibleColorTypes={{kBGRA_10101010_XR_SkColorType, Swizzle::RGBA(), Swizzle::RGBA()}}},

    // For compressed formats, the bytes per block represents actual compressed block size, not
    // just the size of a pixel.
    {.fFormat=TextureFormat::kRGB8_ETC2,
     .fChannels={{'r', 10, UNorm}, {'g', 8, UNorm}, {'b', 8, UNorm}},
     .fCompressionType=SkTextureCompressionType::kETC2_RGB8_UNORM,
     .fXferSwizzle=std::nullopt,
     .fCompatibleColorTypes={{kRGB_888x_SkColorType, Swizzle::RGBA(), std::nullopt}}},

    {.fFormat=TextureFormat::kRGB8_ETC2_sRGB,
     .fChannels={{'r', 10, sRGB}, {'g', 8, sRGB}, {'b', 8, sRGB}},
     .fCompressionType=SkTextureCompressionType::kETC2_RGB8_UNORM,
     .fXferSwizzle=std::nullopt,
     .fCompatibleColorTypes={{kSRGBA_8888_SkColorType, Swizzle::RGBA(), std::nullopt}}},

    {.fFormat=TextureFormat::kRGB8_BC1,
     .fChannels={{'r', 10, UNorm}, {'g', 8, UNorm}, {'b', 8, UNorm}},
     .fCompressionType=SkTextureCompressionType::kBC1_RGB8_UNORM,
     .fXferSwizzle=std::nullopt,
     .fCompatibleColorTypes={{kRGB_888x_SkColorType, Swizzle::RGBA(), std::nullopt}}},

    {.fFormat=TextureFormat::kRGBA8_BC1,
     .fChannels={{'r', 10, UNorm}, {'g', 8, UNorm}, {'b', 8, UNorm}, {'a', 8, UNorm}},
     .fCompressionType=SkTextureCompressionType::kBC1_RGBA8_UNORM,
     .fXferSwizzle=std::nullopt,
     .fCompatibleColorTypes={{kRGBA_8888_SkColorType, Swizzle::RGBA(), std::nullopt},
                             {kRGB_888x_SkColorType,  Swizzle::RGB1(), std::nullopt}}},

    {.fFormat=TextureFormat::kRGBA8_BC1_sRGB,
     .fChannels={{'r', 10, sRGB}, {'g', 8, sRGB}, {'b', 8, sRGB}, {'a', 8, UNorm}},
     .fCompressionType=SkTextureCompressionType::kBC1_RGBA8_UNORM,
     .fXferSwizzle=std::nullopt,
     .fCompatibleColorTypes={{kSRGBA_8888_SkColorType, Swizzle::RGBA(), std::nullopt}}},

    // For these multiplanar formats, we set the bytes per block assuming the UV planes are the
    // same size as the Y plane, which is an overestimate of the total texture memory.
    {.fFormat=TextureFormat::kYUV8_P2_420,
     .fChannels={{'y', 8, UNorm}, {'u', 8, UNorm}, {'v', 8, UNorm}},
     .fXferSwizzle=std::nullopt,
     .fCompatibleColorTypes={{kRGB_888x_SkColorType, Swizzle::RGBA(), std::nullopt}}},

    {.fFormat=TextureFormat::kYUV8_P3_420,
     .fChannels={{'y', 8, UNorm}, {'u', 8, UNorm}, {'v', 8, UNorm}},
     .fXferSwizzle=std::nullopt,
     .fCompatibleColorTypes={{kRGB_888x_SkColorType, Swizzle::RGBA(), std::nullopt}}},

    {.fFormat=TextureFormat::kYUV10x6_P2_420,
     .fChannels={{'y', 10, UNorm}, {'x', 6, Pad}, {'u', 10, UNorm}, {'x', 6, Pad},
                 {'v', 10, UNorm}, {'x', 6, Pad}},
     .fXferSwizzle=std::nullopt,
     .fCompatibleColorTypes={{kRGBA_10x6_SkColorType, Swizzle::RGBA(), std::nullopt}}},

    {.fFormat=TextureFormat::kExternal,
     // We don't really know this, but most Skia behavior defaults to assuming 8-bit color
     .fChannels={{'r', 8, UNorm}, {'g', 8, UNorm}, {'b', 8, UNorm}, {'a', 8, UNorm}},
     .fXferSwizzle=std::nullopt,
     .fCompatibleColorTypes={{kRGBA_8888_SkColorType, Swizzle::RGBA(), std::nullopt},
                             {kRGB_888x_SkColorType,  Swizzle::RGB1(), std::nullopt}}},

    {.fFormat=TextureFormat::kS8,
     .fChannels={{'s', 8, Signed}},
     .fXferSwizzle=std::nullopt,
     .fCompatibleColorTypes={}},

    {.fFormat=TextureFormat::kD16,
     .fChannels={{'d', 16, UNorm}},
     .fXferSwizzle=std::nullopt,
     .fCompatibleColorTypes={}},

    {.fFormat=TextureFormat::kD32F,
     .fChannels={{'d', 32, Float}},
     .fXferSwizzle=std::nullopt,
     .fCompatibleColorTypes={}},

    {.fFormat=TextureFormat::kD24_S8,
     .fChannels={{'d', 24, UNorm}, {'s', 8, Signed}},
     .fXferSwizzle=std::nullopt,
     .fCompatibleColorTypes={}},

    {.fFormat=TextureFormat::kD32F_S8,
     .fChannels={{'d', 32, Float}, {'s', 8, Signed}},
     .fXferSwizzle=std::nullopt,
     .fCompatibleColorTypes={}},
};

void test_format_transfers(skiatest::Reporter* r,
                           const FormatExpectation& textureFormat,
                           const ColorTypeExpectation& textureCT,
                           SkAlphaType srcAT,
                           SkAlphaType dstAT,
                           bool applyCS) {
    // When transferring to CPU->GPU, we want to apply the textureCT's write swizzle, but if that
    // is undefined because rendering is disabled, switch to RGB1. This is applicable for the
    // RGBx cases and for gray (alongside adjusting the texture channel to produce 'G').
    Swizzle writeSwizzle = textureCT.fWriteSwizzle.value_or(Swizzle::RGB1());
    skia_private::TArray<Channel> expectedTextureChannels = textureFormat.fChannels;
    // Adjust the R8 channel to be 'G' for gray-storing textures so that gen_pixel_data includes
    // any conversion to or from luminance.
    if (textureCT.fColorType == kGray_8_SkColorType) {
        SkASSERT(expectedTextureChannels.size() == 1 && expectedTextureChannels[0].fName == 'r');
        expectedTextureChannels[0].fName = 'G';
    }

    // This colorspace is constructed to have a linear gamma curve and an identity gamut transform
    // so that any operations that it applies are just multiplies by 0 or 1. Anything else adds
    // minor bit variations to processed data, making checking expectations harder.
    sk_sp<SkColorSpace> srcCS = SkColorSpace::MakeRGB(SkNamedTransferFn::kLinear,
                                                      SkNamedGamut::kXYZ);
    sk_sp<SkColorSpace> dstCS = applyCS ? srcCS->makeColorSpin() : srcCS;
    SkColorSpaceXformSteps csSteps{srcCS.get(), srcAT, dstCS.get(), dstAT};

    SkString gpuLabel = SkStringPrintf("GPU format %s as %s",
            TextureFormatName(textureFormat.fFormat),
            ToolUtils::colortype_name(textureCT.fColorType));
    // Transfering from srcCT into a GPU textureFormat interpreted as textureCT
    for (const ColorTypeChannels& src : kColorTypeChannels) {
        std::optional<TextureFormatXferFn> xferFn =
                TextureFormatXferFn::MakeCpuToGpu(src.fColorType,
                                                  csSteps,
                                                  textureFormat.fFormat,
                                                  textureCT.fReadSwizzle);
        REPORTER_ASSERT(r, textureFormat.fXferSwizzle.has_value() == xferFn.has_value());

        if (textureFormat.fXferSwizzle.has_value() && xferFn.has_value()) {
            PixelData cpuPixel = gen_pixel_data(src.fChannels, Swizzle::RGBA(), srcAT);

            // The expected GPU value is formed by applying the source colortype's effective
            // swizzle (i.e. fill in missing channels), and the format's compatible colortype's
            // write swizzle to the channel definition of format.
            Swizzle loadSrc = src.fEffectiveSwizzle;
            if (applyCS) {
                // The colorspace conversion is constructed to be a spin via gamut transform matrix,
                // so the end effect should match a rotation swizzle on R,G,B (ignoring alpha).
                loadSrc = Swizzle::Concat(loadSrc, Swizzle("gbra"));
            }

            PixelData expectedGpuPixel = gen_pixel_data(expectedTextureChannels,
                                                        writeSwizzle,
                                                        dstAT,
                                                        src.fChannels,
                                                        loadSrc,
                                                        srcAT);
            PixelData actualGpuPixel = transfer_data(*xferFn, cpuPixel);

            const int tol = channel_tolerance(src.fChannels, srcAT,
                                              expectedTextureChannels, dstAT);
            if (!compare_pixels(textureFormat.fChannels, expectedGpuPixel, actualGpuPixel, tol)) {
                SkString ctLabel = SkStringPrintf("CPU colortype %s",
                                                  ToolUtils::colortype_name(src.fColorType));
                dump_pixel_comparison(ctLabel,
                                      src.fChannels,
                                      cpuPixel,
                                      gpuLabel,
                                      textureFormat.fChannels,
                                      expectedGpuPixel,
                                      actualGpuPixel);
                REPORTER_ASSERT(r, false,  "Pixel mismatch uploading from %s, alpha %s -> %s%s",
                                ctLabel.c_str(),
                                ToolUtils::alphatype_name(srcAT),
                                ToolUtils::alphatype_name(dstAT),
                                applyCS ? " with colorspace conversion" : "");
                STOP_ON_TRANSFER_FAILURE
            }
        }
    }

    // Transfering from a GPU textureFormat interpreted as textureCT into dstCT
    for (const ColorTypeChannels& dst : kColorTypeChannels) {
        std::optional<TextureFormatXferFn> xferFn =
                TextureFormatXferFn::MakeGpuToCpu(textureFormat.fFormat,
                                                  textureCT.fReadSwizzle,
                                                  csSteps,
                                                  dst.fColorType);
        REPORTER_ASSERT(r, textureFormat.fXferSwizzle.has_value() == xferFn.has_value());

        if (textureFormat.fXferSwizzle.has_value() && xferFn.has_value()) {
            PixelData gpuPixel = gen_pixel_data(expectedTextureChannels,
                                                textureCT.fReadSwizzle,
                                                srcAT);

            // The expected CPU value is formed by applying the TextureFormat's implicit transfer
            // swizzle (i.e. fill in missing channels), its compatible colortype's read swizzle
            // to the channel definition of the dst color type.
            Swizzle loadSrc = Swizzle::Concat(*textureFormat.fXferSwizzle, textureCT.fReadSwizzle);
            if (applyCS) {
                loadSrc = Swizzle::Concat(loadSrc, Swizzle("gbra")); // See above
            }
            PixelData expectedCpuPixel = gen_pixel_data(dst.fChannels,
                                                        Swizzle::RGBA(),
                                                        dstAT,
                                                        expectedTextureChannels,
                                                        loadSrc,
                                                        srcAT);

            PixelData actualCpuPixel = transfer_data(*xferFn, gpuPixel);

            const int tol = channel_tolerance(expectedTextureChannels, srcAT,
                                              dst.fChannels, dstAT);
            if (!compare_pixels(dst.fChannels, expectedCpuPixel, actualCpuPixel, tol)) {
                SkString ctLabel = SkStringPrintf("CPU colortype %s",
                                                  ToolUtils::colortype_name(dst.fColorType));

                dump_pixel_comparison(gpuLabel,
                                      textureFormat.fChannels,
                                      gpuPixel,
                                      ctLabel,
                                      dst.fChannels,
                                      expectedCpuPixel,
                                      actualCpuPixel);
                REPORTER_ASSERT(r, false,  "Pixel mismatch reading back to %s, alpha %s -> %s%s",
                                ctLabel.c_str(),
                                ToolUtils::alphatype_name(srcAT),
                                ToolUtils::alphatype_name(dstAT),
                                applyCS ? " with colorspace conversion" : "");
                STOP_ON_TRANSFER_FAILURE
            }
        }
    }
}

void run_texture_format_test(skiatest::Reporter* r, const Caps* caps, TextureFormat format) {
    bool foundExpectation = false;
    for (auto&& e : kExpectations) {
        if (e.fFormat != format) {
            continue;
        }

        // Should only find it once
        REPORTER_ASSERT(r, !foundExpectation, "Format expectation listed multiple times");
        foundExpectation = true;

        skiatest::ReporterContext scope(r, SkStringPrintf("Format %s", TextureFormatName(format)));

        // Found the expectation for the requested format. Check fixed properties first.
        REPORTER_ASSERT(r, e.fCompressionType == TextureFormatCompressionType(format));
        REPORTER_ASSERT(r, e.bytesPerBlock() == TextureFormatBytesPerBlock(format));
        REPORTER_ASSERT(r, e.channelMask() == TextureFormatChannelMask(format));
        REPORTER_ASSERT(r, e.hasDepthOrStencil() == TextureFormatIsDepthOrStencil(format));
        REPORTER_ASSERT(r, e.hasDepth() == TextureFormatHasDepth(format));
        REPORTER_ASSERT(r, e.hasStencil() == TextureFormatHasStencil(format));
        REPORTER_ASSERT(r, e.isMultiplanar() == TextureFormatIsMultiplanar(format));
        REPORTER_ASSERT(r, e.autoClamps() == TextureFormatAutoClamps(format));
        REPORTER_ASSERT(r, e.isFloatingPoint() == TextureFormatIsFloatingPoint(format));

        // Verify compatible color types
        auto [baseColorType, _] = TextureFormatColorTypeInfo(format);
        if (baseColorType == kUnknown_SkColorType) {
            REPORTER_ASSERT(r, e.fCompatibleColorTypes.empty());
        } else {
            // Should be the first listed compatible color type
            REPORTER_ASSERT(r, !e.fCompatibleColorTypes.empty());
            REPORTER_ASSERT(r, e.fCompatibleColorTypes[0].fColorType == baseColorType);
        }

        for (int c = 0; c <= kLastEnum_SkColorType; ++c) {
            SkColorType ct = static_cast<SkColorType>(c);

            skiatest::ReporterContext ctScope{
                    r, SkStringPrintf("color type %s\n", ToolUtils::colortype_name(ct))};

            bool foundColorExpectation = false;
            for (auto&& ec : e.fCompatibleColorTypes) {
                if (ec.fColorType == ct) {
                    // Expected to be compatible (and should only find it once)
                    REPORTER_ASSERT(r, !foundColorExpectation,
                                    "Color type listed multiple times: %s",
                                    ToolUtils::colortype_name(ec.fColorType));
                    foundColorExpectation = true;

                    // Check swizzles and transfers here, the rest of the color type checks happen
                    // outside the loop based on `foundColorExpectation`.
                    Swizzle actualReadSwizzle = ReadSwizzleForColorType(ct, format);
                    REPORTER_ASSERT(r, ec.fReadSwizzle == actualReadSwizzle,
                                    "actual %s vs. expected %s",
                                    actualReadSwizzle.asString().c_str(),
                                    ec.fReadSwizzle.asString().c_str());

                    auto actualWriteSwizzle = WriteSwizzleForColorType(ct, format);
                    if (ec.fWriteSwizzle.has_value()) {
                        REPORTER_ASSERT(r, actualWriteSwizzle.has_value());
                        REPORTER_ASSERT(r, ec.fWriteSwizzle == actualWriteSwizzle,
                                        "actual %s vs. expected %s",
                                        actualWriteSwizzle ? actualWriteSwizzle->asString().c_str()
                                                           : "null",
                                        ec.fWriteSwizzle->asString().c_str());
                    } else {
                        REPORTER_ASSERT(r, !actualWriteSwizzle.has_value());
                        // This is a proxy for "the format can represent CT, and there are some
                        // formats that can render CT, but this format does not render w/ CT".
                        TextureInfo renderableInfo = caps->getDefaultSampledTextureInfo(
                                ct, Mipmapped::kNo, Protected::kNo, Renderable::kYes);
                        REPORTER_ASSERT(r, format != TextureInfoPriv::ViewFormat(renderableInfo));
                    }

                    // Test all combinations of alpha type x 2 (texture vs cpu) and whether or not
                    // colorspace conversions are handled in the transfer.
                    static constexpr int kAlphaTypeCount = (int) kLastEnum_SkAlphaType + 1;
                    for (bool applyCS : {false, true}) {
                        for (int srcAT = 0; srcAT < kAlphaTypeCount; ++srcAT) {
                            for (int dstAT = 0; dstAT < kAlphaTypeCount; ++dstAT) {
                                test_format_transfers(r, e, ec,
                                                      static_cast<SkAlphaType>(srcAT),
                                                      static_cast<SkAlphaType>(dstAT),
                                                      applyCS);
                            }
                        }
                    }
                }
            }

            // If we found an expectation, it should be detected as compatible (and false otherwise)
            const bool actualCompatible = AreColorTypeAndFormatCompatible(ct, format);
            REPORTER_ASSERT(r, foundColorExpectation == actualCompatible,
                            "actual (%d) vs expected (%d)",
                            actualCompatible, foundColorExpectation);
        }
    }

    // All formats should have expectations
    REPORTER_ASSERT(r, foundExpectation, "Missing expectation for %s", TextureFormatName(format));
}

DEF_GRAPHITE_TEST_FOR_ALL_CONTEXTS(TextureFormatTest, r, ctx, CtsEnforcement::kNextRelease) {
    for (int i = 0; i < kTextureFormatCount; ++i) {
        run_texture_format_test(r, ctx->priv().caps(), static_cast<TextureFormat>(i));
    }
}

} // namespace skgpu::graphite
