blob: 15cf532e8fd255917b917cbb1a316ab66df70d6e [file]
/*
* 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 "src/gpu/graphite/TextureFormatXferFn.h"
#include "include/core/SkColorType.h"
#include "include/private/SkLog.h"
#include "src/core/SkAutoMalloc.h"
#include "src/core/SkColorSpaceXformSteps.h"
#include "src/core/SkFloatBits.h"
#include "src/core/SkHalf.h"
#include "src/core/SkImageInfoPriv.h"
#include "src/core/SkMathPriv.h"
#include "src/core/SkRasterPipeline.h"
#include "src/core/SkRasterPipelineOpContexts.h"
#include "src/core/SkRasterPipelineOpList.h"
#include "src/core/SkSwizzlePriv.h"
#include "src/core/SkVx.h"
#include <functional>
namespace skgpu::graphite {
namespace {
using TF = TextureFormat;
// This is intentionally not a class enum and is distinct from FormatXferOps for two reasons:
// 1. We can't use SkEnumBitmask inside template parameters for the row bit manipulation functions.
// 2. It's convenient to split the FormatXferOps into specific ops based on their conversion
// direction that doesn't need to be exposed in the public API.
enum ExtendedFormatXferOp : uint8_t {
kDropAlpha = 0x1, // FormatXferOp::kDropAlpha for CPU->GPU conversion
kPadAlpha = 0x2, // FormatXferOp::kDropAlpha for GPU->CPU conversion
kSwapRB = 0x4, // FormatXferOp::kSwapRB behaves the same for either conversion direction
kForceOpaque = 0x8, // RGBx handling to avoid raster pipeline is the same in both directions
kIgnoreSrc = 0x10, // The output value does not depend on the src, will be either 0 or 1.
};
using XferRowFn = std::function<void(const char* src, char* dst, int width)>;
template <typename Px, int N, typename PixelFn /* [](Vec<N,Px>, Px opaqueAlpha) -> Vec<N,Px> */>
XferRowFn create_xfer_row_fn(int n, int srcBpp, int dstBpp,
Px opaqueAlpha, Px zeroValue, PixelFn applyPixel) {
using PxVec = skvx::Vec<N, Px>;
if (srcBpp != 0) {
// PxVec should be sufficient to hold N src and dst pixels, and should match at least one
SkASSERT(sizeof(PxVec) >= (size_t)srcBpp*n && sizeof(PxVec) >= (size_t)dstBpp*n);
SkASSERT(sizeof(PxVec) == (size_t)srcBpp*n || sizeof(PxVec) == (size_t)dstBpp*n);
// NOTE: These must all be named explicitly, since using the & to auto-capture does not copy
// them, so then their references become corrupted when executing the lambda later.
return [n, srcBpp, dstBpp, opaqueAlpha, applyPixel](const char* src, char* dst, int width) {
const int srcBppN = n * srcBpp;
const int dstBppN = n * dstBpp;
PxVec pixel{};
while (width >= n) {
memcpy(&pixel, src, srcBppN);
pixel = applyPixel(pixel, opaqueAlpha);
memcpy(dst, &pixel, dstBppN);
width -= n;
src += srcBppN;
dst += dstBppN;
}
if (width > 0) {
// Process tail that is less than a full vector
SkASSERT(width < n);
memcpy(&pixel, src, width * srcBpp);
pixel = applyPixel(pixel, opaqueAlpha);
memcpy(dst, &pixel, width * dstBpp);
}
};
} else {
// Ignore the source in favor of loading zeroValue into pixel each time
SkASSERT(sizeof(PxVec) >= (size_t)dstBpp*n);
return [n, opaqueAlpha, zeroValue, dstBpp, applyPixel](const char*, char* dst, int width) {
const int dstBppN = n * dstBpp;
PxVec pixel{};
while (width >= n) {
pixel = zeroValue;
pixel = applyPixel(pixel, opaqueAlpha);
memcpy(dst, &pixel, dstBppN);
width -= n;
dst += dstBppN;
}
if (width > 0) {
// Process tail that is less than a full vector
SkASSERT(width < n);
pixel = zeroValue;
pixel = applyPixel(pixel, opaqueAlpha);
memcpy(dst, &pixel, width * dstBpp);
}
};
}
}
// N represents the number of pixels being processed; each pixel is packed into a single element of
// the Vec. To implement kSwapRB, this requires that the R and B channels have the same number of
// bits. It arbitrarily requires that R is originally in the lower-significance bits relative to B.
template <typename Px, int N, uint8_t Ops, Px RShift, Px BShift, Px RBBits>
skvx::Vec<N, Px> apply_ops_packed(skvx::Vec<N, Px> pixel, Px opaqueAlpha) {
static_assert(!(Ops & (kPadAlpha | kDropAlpha)), "Packed formats do not drop/pad alpha");
static_assert(RShift < BShift, "Shifts are not in the correct significance order");
static_assert(RShift + RBBits <= BShift, "Low swap channel overflows");
static_assert(BShift + RBBits <= sizeof(Px)*8, "Hi swap channel overflows");
if constexpr (Ops & kSwapRB) {
static constexpr Px kChannelMask = (1 << RBBits) - 1;
static constexpr Px kRMask = kChannelMask << RShift;
static constexpr Px kBMask = kChannelMask << BShift;
pixel = (pixel & ~(kRMask | kBMask)) | // Preserve non-RB bits
((pixel & kRMask) << (BShift - RShift)) | // Move red (lo) to blue (hi) position
((pixel & kBMask) >> (BShift - RShift)); // Move blue (hi) to red (lo) position
}
if constexpr (Ops & kForceOpaque) {
// This assumes the opaqueAlpha value will have 0 bits set in the R,G,B channels.
pixel = pixel | opaqueAlpha;
}
return pixel;
}
// NOTE: This takes no parameters for alpha because none of the formats that use this support
// kDrop/PadAlpha. Since the channels are packed into Px, both source and dst must have the same bpp
// `opaqueAlpha` specifies the bit pattern for the entire pixel representing {0, 0, 0, 1.0}.
// `zeroValue` specifies the bit pattern for the entire pixel representing {0, 0, 0, 0}.
template <typename Px, Px RShift, Px BShift, Px RBBits>
XferRowFn xfer_rows_packed(uint8_t ops, Px opaqueAlpha, Px zeroValue = 0) {
static constexpr int kBpp = sizeof(Px);
static constexpr int N = 16 / sizeof(Px); // Fit to 128-bit/16-byte SIMD
static_assert(N == 1 || N == 2 || N == 4 || N == 8);
#define RETURN_XFER_ROW_FN(ops) \
case ops: \
return create_xfer_row_fn<Px, N>( \
N, (ops) & kIgnoreSrc ? 0 : kBpp, kBpp, opaqueAlpha, zeroValue, \
apply_ops_packed<Px, N, (ops) & ~kIgnoreSrc, RShift, BShift, RBBits>); \
switch (ops) {
// The expected combination of ExtendedFormatXferOps
RETURN_XFER_ROW_FN(kSwapRB)
RETURN_XFER_ROW_FN(kIgnoreSrc)
RETURN_XFER_ROW_FN(kForceOpaque)
RETURN_XFER_ROW_FN(kForceOpaque | kSwapRB)
RETURN_XFER_ROW_FN(kForceOpaque | kIgnoreSrc)
default:
SK_ABORT("Unsupported ExtendedFormatXferOps combination: %u", ops);
}
#undef RETURN_XFER_ROW_FN
}
// N represents the number of pixels being processed; some Ops are only supported for C == 1 or
// C == 4. For other channel counts, those Ops are ignored at compile time (to make the ops switch
// tables easier to write). Hopefully the linker will see that they are equivalent to the template
// w/o the incompatible Ops.
template <typename Cx, int N, int CPow2, uint8_t Ops>
skvx::Vec<CPow2*N, Cx> apply_ops_by_channel(skvx::Vec<CPow2*N, Cx> pixel, Cx opaqueAlpha) {
static_assert(CPow2 == 1 || CPow2 == 2 || CPow2 == 4);
if constexpr (Ops & kPadAlpha && CPow2 == 4) {
// If we are padding alpha, we are moving from a 3-channel source data (loaded in the first
// N*3 values of pixel) to a 4-channel value. 3*N+1 holds undefined data after loading the
// pixel, which we can set to the opaque alpha value. Then we shuffle the slots to spread
// out each pixel's R,G, and B values and insert copies of the opaque alpha value.
static constexpr int kA = 3*N;
pixel[kA] = opaqueAlpha;
if constexpr (N == 4) {
pixel = skvx::shuffle<0,1,2,kA, 3,4,5,kA, 6,7,8,kA, 9,10,11,kA>(pixel);
} else if constexpr (N == 2) {
pixel = skvx::shuffle<0,1,2,kA, 3,4,5,kA>(pixel);
} else {
// No shuffling needed for N=1, since pixel == shuffle<0,1,2,A>(pixel)
static_assert(N == 1);
}
}
// Ops that assume 4 channels can be applied between kPadAlpha and kDropAlpha w/o worrying about
// how to handle the 3-channel formats.
if constexpr (Ops & kSwapRB && CPow2 == 4) {
// For all 3 and 4 channel formats, it is assumed that R and B are in channels 0 and 2.
if constexpr (N == 4) {
pixel = skvx::shuffle<2,1,0,3, 6,5,4,7, 10,9,8,11, 14,13,12,15>(pixel);
} else if constexpr (N == 2) {
pixel = skvx::shuffle<2,1,0,3, 6,5,4,7>(pixel);
} else {
static_assert(N == 1);
pixel = skvx::shuffle<2,1,0,3>(pixel);
}
}
// 2-channel formats are incompatible with forcing opaque since there's never an alpha channel
if constexpr (Ops & kForceOpaque && (CPow2 != 2)) {
if constexpr (CPow2 == 1) {
// For single-channel formats, the opaque alpha gets splatted over the whole vector
pixel = opaqueAlpha;
} else {
// Between kPadAlpha and kDropAlpha, it is assumed that alpha is always channel 3
if constexpr (N == 4) {
pixel[3] = pixel[7] = pixel[11] = pixel[15] = opaqueAlpha;
} else if constexpr (N == 2) {
pixel[3] = pixel[7] = opaqueAlpha;
} else {
static_assert(N == 1);
pixel[3] = opaqueAlpha;
}
}
}
if constexpr (Ops & kDropAlpha && CPow2 == 4) {
// If we are dropping alpha, we need to shuffle the R,G, and B values of the 4-channel
// source data into the first N*3 slots of the returned pixel. The remaining N slots will
// be ignored by the final memcpy.
if constexpr (N == 4) {
pixel = skvx::shuffle<0,1,2, 4,5,6, 8,9,10, 12,13,14, 14,14,14,14>(pixel);
} else if constexpr (N == 2) {
pixel = skvx::shuffle<0,1,2, 4,5,6, 6,6>(pixel);
} else {
// No shuffling needed for N=1, since pixel == shuffle<0,1,2,_>(pixel)
static_assert(N == 1);
}
}
return pixel;
}
// NOTE: This takes no parameters for SwapRB or DropAlpha because for all formats that use this,
// SwapRB involves swapping channel 0 and channel 2, and dropping alpha removes channel 3. This
// can be parameterized via template parameters to be able to push into the skvx::shuffle calls
// if needed in the future.
// `opaqueAlpha` specifies the bit pattern for a channel representing 1.0.
// `zeroValue` specifies the bit pattern for a channel representing 0.0.
template <typename Cx, int C>
XferRowFn xfer_rows_by_channel(uint8_t ops, Cx opaqueAlpha, Cx zeroValue=0) {
static_assert(C == 1 || C == 2 || C == 3 || C == 4);
static constexpr int CPow2 = SkNextPow2(C);
static constexpr int N = 16 / (CPow2 * sizeof(Cx)); // Fit to 128-bit/16-byte SIMD
static_assert(N == 1 || N == 2 || N == 4 || N == 8 || N == 16);
int srcBpp = C * sizeof(Cx);
int dstBpp = C * sizeof(Cx);
if (ops & kDropAlpha) {
// Going from 4-channel src to the 3-channel format
SkASSERT(C == 3);
srcBpp = CPow2 * sizeof(Cx);
} else if (ops & kPadAlpha) {
// Going from the 3-channel format to 4-channel dst
SkASSERT(C == 3);
dstBpp = CPow2 * sizeof(Cx);
}
#define RETURN_XFER_ROW_FN(ops) \
case ops: \
return create_xfer_row_fn<Cx, N*CPow2>( \
N, (ops) & kIgnoreSrc ? 0 : srcBpp, dstBpp, opaqueAlpha, zeroValue, \
apply_ops_by_channel<Cx, N, CPow2, (ops) & ~kIgnoreSrc>);
switch (ops) {
// The expected combination of ExtendedFormatXferOps
RETURN_XFER_ROW_FN(kDropAlpha)
RETURN_XFER_ROW_FN(kDropAlpha | kSwapRB)
RETURN_XFER_ROW_FN(kDropAlpha | kIgnoreSrc)
RETURN_XFER_ROW_FN(kPadAlpha)
RETURN_XFER_ROW_FN(kPadAlpha | kSwapRB)
RETURN_XFER_ROW_FN(kPadAlpha | kIgnoreSrc)
RETURN_XFER_ROW_FN(kSwapRB)
RETURN_XFER_ROW_FN(kIgnoreSrc)
RETURN_XFER_ROW_FN(kForceOpaque)
RETURN_XFER_ROW_FN(kForceOpaque | kSwapRB)
RETURN_XFER_ROW_FN(kForceOpaque | kIgnoreSrc)
default:
SK_ABORT("Unsupported ExtendedFormatXferOps combination: %u", ops);
}
#undef RETURN_XFERFN_CASE
}
XferRowFn get_xfer_row_fn(TextureFormat format, uint8_t ops) {
static constexpr uint32_t kFloatBits1 = 0x3f800000; // SkFloat2Bits isn't constexpr
SkASSERT(kFloatBits1 == SkFloat2Bits(1.f));
// 10 bits for _XR formats that encodes 1.0 and 0.0 given their extended normalization range.
static constexpr uint32_t kXROne = 0x37e;
static constexpr uint32_t kXRZero = 0x180;
SkASSERT(ops); // For now, assume we only call into this if we have work to do.
switch (format) {
// Packed formats operate on a primitive that holds the entire pixel value
case TF::kB5_G6_R5:
case TF::kR5_G6_B5:
return xfer_rows_packed<uint16_t, /*RShift=*/0, /*BShift=*/11, /*RBBits=*/5>(
ops, /*opaqueAlpha=*/0);
case TF::kABGR4:
case TF::kARGB4:
return xfer_rows_packed<uint16_t, /*RShift=*/4, /*BShift=*/12, /*RBBits=*/4>(
ops, /*opaqueAlpha=*/0xF);
case TF::kRGB10_A2:
case TF::kBGR10_A2:
return xfer_rows_packed<uint32_t, /*RShift=*/0, /*BShift=*/20, /*RBBits=*/10>(
ops, /*opaqueAlpha=*/0b11 << 30);
case TF::kBGR10_XR:
// NOTE: opaqueAlpha doesn't matter here since it's just BGRx data, but to match
// SkRP's handling of BGR10A2_XR, treat the last 2 bits as regular unorm.
return xfer_rows_packed<uint32_t, /*RShift=*/0, /*BShift=*/20, /*RBBits=*/10>(
ops, /*opaqueAlpha=*/0b11 << 30,
/*zeroValue=*/(kXRZero << 20) | (kXRZero << 10) | kXRZero);
// The remaining formats can be operated on with each channel as a primitive
case TF::kR8:
case TF::kA8:
return xfer_rows_by_channel<uint8_t, /*C=*/1>(ops, /*opaqueAlpha=*/0xFF);
case TF::kR16:
return xfer_rows_by_channel<uint16_t, /*C=*/1>(ops, /*opaqueAlpha=*/0xFFFF);
case TF::kR16F:
return xfer_rows_by_channel<uint16_t, /*C=*/1>(ops, /*opaqueAlpha=*/SK_Half1);
case TF::kRG8:
return xfer_rows_by_channel<uint8_t, /*C=*/2>(ops, /*opaqueAlpha=*/0xFF);
case TF::kRG16:
return xfer_rows_by_channel<uint16_t, /*C=*/2>(ops, /*opaqueAlpha=*/0xFF);
case TF::kRG16F:
return xfer_rows_by_channel<uint16_t, /*C=*/2>(ops, /*opaqueAlpha=*/SK_Half1);
case TF::kRG32F:
return xfer_rows_by_channel<uint32_t, /*C=*/2>(ops, /*opaqueAlpha=*/kFloatBits1);
case TF::kRGB8_sRGB:
case TF::kRGB8:
case TF::kBGR8:
return xfer_rows_by_channel<uint8_t, /*C=*/3>(ops, /*opaqueAlpha=*/0xFF);
case TF::kRGB16:
return xfer_rows_by_channel<uint16_t, /*C=*/3>(ops, /*opaqueAlpha=*/0xFFFF);
case TF::kRGB16F:
return xfer_rows_by_channel<uint16_t, /*C=*/3>(ops, /*opaqueAlpha=*/SK_Half1);
case TF::kRGB32F:
return xfer_rows_by_channel<uint32_t, /*C=*/3>(ops, /*opaqueAlpha=*/kFloatBits1);
case TF::kRGBA8:
case TF::kRGBA8_sRGB:
case TF::kBGRA8:
case TF::kBGRA8_sRGB:
return xfer_rows_by_channel<uint8_t, /*C=*/4>(ops, /*opaqueAlpha=*/0xFF);
case TF::kRGBA10x6:
// Each channel is the 10 real bits, with the least significant 6 padding bits.
return xfer_rows_by_channel<uint16_t, /*C=*/4>(ops, /*opaqueAlpha=*/0xFFC0);
case TF::kBGRA10x6_XR:
// Like RGBA10x6 except the constants have to fit the extended range.
return xfer_rows_by_channel<uint16_t, /*C=*/4>(
ops, /*opaqueAlpha=*/kXROne << 6, /*zeroValue=*/kXRZero << 6);
case TF::kRGBA16:
return xfer_rows_by_channel<uint16_t, /*C=*/4>(ops, /*opaqueAlpha=*/0xFFFF);
case TF::kRGBA16F:
return xfer_rows_by_channel<uint16_t, /*C=*/4>(ops, /*opaqueAlpha=*/SK_Half1);
case TF::kRGBA32F:
return xfer_rows_by_channel<uint32_t, /*C=*/4>(ops, /*opaqueAlpha=*/kFloatBits1);
default:
// Remaining cases are compressed, multiplanar, or non-color so shouldn't be reached.
// If the first assert trips, we missed a valid transfer format in the cases above.
// If we hit the unreachable, we missed rejecting the transfer sooner.
SkASSERT(TextureFormatColorTypeInfo(format).second & FormatXferOp::kDisabled);
SkUNREACHABLE;
}
}
// --- Functions for collapsing equivalent colortypes and formats into fewer xfer ops
// An RPModifier for RPOps::Make() that handles calculating bt709 luminance. The API is that it
// must have an explicit bool operator, and an apply(SkRasterPipeline*) function.
struct BT709Luminance {
bool fEnabled;
explicit operator bool() const { return fEnabled; }
void apply(SkRasterPipeline* rp) const {
if (fEnabled) {
rp->append(SkRasterPipelineOp::bt709_luminance_or_luma_to_alpha);
} // else no op needed
}
};
bool account_for_luminance(SkColorType srcCT,
bool hasColorSpaceTransform,
SkColorType* dstCT) {
if (*dstCT == kGray_8_SkColorType) {
if (srcCT != kGray_8_SkColorType || hasColorSpaceTransform) {
// Luminance must be calculated by raster pipeline, but we'll have to apply it before
// any srcToDst swizzle, so pull it out to its own RPModifier. The op stores the value
// in the alpha channel, so that needs to be dstCT that SkRasterPipeline sees.
*dstCT = kAlpha_8_SkColorType;
return true;
} // else leave it as gray->gray
} else if (srcCT == kGray_8_SkColorType &&
*dstCT == kR8_unorm_SkColorType &&
!hasColorSpaceTransform) {
// The source gray value can just be copied to the red channel, so adjust this to be
// gray->"gray" for a no-op copy
*dstCT = kGray_8_SkColorType;
return false;
} // else trivially no extra luminance needs to be calculated, leave dstCT as-is
return false;
}
bool rgbx_to_rgba(SkColorType* ct) {
switch (*ct) {
case kRGB_888x_SkColorType: *ct = kRGBA_8888_SkColorType; return true;
case kRGB_101010x_SkColorType: *ct = kRGBA_1010102_SkColorType; return true;
case kRGB_F16F16F16x_SkColorType: *ct = kRGBA_F16_SkColorType; return true;
case kBGR_101010x_SkColorType: *ct = kBGRA_1010102_SkColorType; return true;
default:
// No colortype consolidation possible by switching to a kForceOpaque op instead.
return false;
}
}
bool bgra_to_rgba(SkColorType* ct) {
switch (*ct) {
case kBGRA_8888_SkColorType: *ct = kRGBA_8888_SkColorType; return true;
case kBGRA_1010102_SkColorType: *ct = kRGBA_1010102_SkColorType; return true;
case kBGR_101010x_SkColorType: *ct = kRGB_101010x_SkColorType; return true;
// NOTE: For now there's no RGBA version of the _XR color types, so they can't be swapped
case kBGRA_10101010_XR_SkColorType:
case kBGR_101010x_XR_SkColorType: [[fallthrough]];
default:
// No colortype consolidation possible by switching to a kSwapRB op instead.
return false;
}
}
uint32_t gray_adjusted_channels(SkColorType ct) {
return ct == kGray_8_SkColorType ? kRGB_SkColorChannelFlags : SkColorTypeChannelFlags(ct);
}
uint32_t swizzle_adjusted_channels(uint32_t channels, Swizzle swizzle) {
auto removeChannelIfConstant = [&](SkColorChannelFlag flag, int index) {
if (swizzle[index] == '0' || swizzle[index] == '1') {
channels &= ~flag;
}
};
removeChannelIfConstant(kRed_SkColorChannelFlag, 0);
removeChannelIfConstant(kGreen_SkColorChannelFlag, 1);
removeChannelIfConstant(kBlue_SkColorChannelFlag, 2);
removeChannelIfConstant(kAlpha_SkColorChannelFlag, 3);
return channels;
}
template <bool TextureIsDst>
std::pair</*ops=*/uint8_t, /*computeLuminance=*/bool> optimize_transfer(
SkColorType* cpuCT,
SkColorType* texBaseCT,
Swizzle* texReadSwizzle,
SkColorSpaceXformSteps* csSteps,
SkEnumBitMask<FormatXferOp> xferOps) {
// Aliases for color types that are based on the transfer direction
SkColorType* srcCT = TextureIsDst ? cpuCT : texBaseCT;
SkColorType* dstCT = TextureIsDst ? texBaseCT : cpuCT;
uint8_t finalOps = 0;
// Some combinations of swizzle and load/store ops in raster pipeline are redundant so try to
// make adjustments to reduce or eliminate the use of raster pipeline entirely.
// First, adjust texBaseCT to match colortype semantics that can be inferred from swizzle for
// alpha-only and red-only color types (hopefully creating a no-op transfer). These also test
// the forced-opaque swizzle equivalents.
{
SkColorType adjustedBase = *texBaseCT;
if (*texReadSwizzle == Swizzle("000r") || *texReadSwizzle == Swizzle("0001")) {
// Red -> Alpha so shift the texture's "base" colortype to be the its alpha type
switch(adjustedBase) {
case kR8_unorm_SkColorType: adjustedBase = kAlpha_8_SkColorType; break;
case kR16_unorm_SkColorType: adjustedBase = kA16_unorm_SkColorType; break;
case kR16_float_SkColorType: adjustedBase = kA16_float_SkColorType; break;
default: break; // Go through regular RP + swizzle flow
}
} else if ((*texReadSwizzle == Swizzle("rrra") || *texReadSwizzle == Swizzle("rrr1")) &&
adjustedBase == kR8_unorm_SkColorType) {
// Red -> Gray so shift to kGray, which either ensures RP will generate the gray values
// from a non-gray input, or will be detected as a no-op when transferring to/from
// existing gray.
adjustedBase = kGray_8_SkColorType;
}
if (adjustedBase != *texBaseCT) {
*texBaseCT = adjustedBase;
// Must preserve any forced opacity in the swizzle
if ((SkColorTypeChannelFlags(adjustedBase) & kAlpha_SkColorChannelFlag) &&
(*texReadSwizzle)[3] == '1') {
*texReadSwizzle = Swizzle::RGB1();
} else {
*texReadSwizzle = Swizzle::RGBA();
}
}
}
// Second, remove normalized floating point semantics, since historically we don't enforce them
// during transfers (for better or worse).
{
if (*srcCT == kRGBA_F16Norm_SkColorType) {
*srcCT = kRGBA_F16_SkColorType;
}
if (*dstCT == kRGBA_F16Norm_SkColorType) {
*dstCT = kRGBA_F16_SkColorType;
}
}
// Third, discard colorspace conversions if there's no RGB data that would be adjusted
{
if (SkToBool(*csSteps) &&
(SkColorTypeIsAlphaOnly(*srcCT) || SkColorTypeIsAlphaOnly(*dstCT))) {
*csSteps = {};
}
}
// Fourth, switch swizzle components back to their default if the format doesn't have them
{
uint32_t texChannels = SkColorTypeChannelFlags(*texBaseCT);
const char c[4] = {(*texReadSwizzle)[0], (*texReadSwizzle)[1],
(*texReadSwizzle)[2], (*texReadSwizzle)[3]};
*texReadSwizzle = Swizzle((texChannels & kRed_SkColorChannelFlag) ? c[0] : 'r',
(texChannels & kGreen_SkColorChannelFlag) ? c[1] : 'g',
(texChannels & kBlue_SkColorChannelFlag) ? c[2] : 'b',
(texChannels & kAlpha_SkColorChannelFlag) ? c[3] : 'a');
}
// Fifth, if the channels are disjoint between the src and the dst, then the dst values can
// ignore the source, at which point we reset everything else to the identity and flag what
// value is required by the dst (1 if the dst is alpha, 0 if it's RGB/gray).
{
uint32_t cpuChannels = gray_adjusted_channels(*cpuCT);
uint32_t gpuChannels = swizzle_adjusted_channels(gray_adjusted_channels(*texBaseCT),
*texReadSwizzle);
if (!(gpuChannels & cpuChannels)) {
finalOps |= kIgnoreSrc;
if (SkToBool(xferOps & FormatXferOp::kDropAlpha)) {
// We're skipping the final xfer op handling, but we still have to make sure to drop
// the alpha channel for 3-channel formats
if constexpr (TextureIsDst) {
finalOps |= kDropAlpha;
} else {
// Normally this would be kPadAlpha, but we're ignoring the src texture data,
// so instead it can just use kForceOpaque
finalOps |= kForceOpaque;
}
} else if (!SkColorTypeIsAlwaysOpaque(*dstCT)) {
finalOps |= kForceOpaque;
}
// Disable everything else for the transfer
*texReadSwizzle = Swizzle::RGBA();
*srcCT = *dstCT;
*csSteps = SkColorSpaceXformSteps{};
return {finalOps, /*computeLuminance=*/false};
}
}
// Sixth, handle masking any unknown alpha bits if the dst doesn't already mask them. This must
// happen *after* checking for channel overlap to optimize to the kIgnoreSrc case. If not, the
// RGBx color types getting lifted to RGBA adds a channel overlap when there wasn't one.
{
bool cpuMasksAlpha = rgbx_to_rgba(cpuCT);
bool gpuMasksAlpha = rgbx_to_rgba(texBaseCT) || (*texReadSwizzle)[3] == '1';
bool srcHasJunkAlpha = TextureIsDst ? cpuMasksAlpha : gpuMasksAlpha;
bool dstStoresAlpha =
(SkColorTypeChannelFlags(*dstCT) & kAlpha_SkColorChannelFlag) &&
!(TextureIsDst ? gpuMasksAlpha : cpuMasksAlpha);
if (srcHasJunkAlpha) {
csSteps->fFlags.premul = false;
csSteps->fFlags.unpremul = false;
}
if (dstStoresAlpha && srcHasJunkAlpha) {
finalOps |= kForceOpaque;
}
// Reset any opacity forcing in the swizzle since it's handled by kForceOpaque.
if ((*texReadSwizzle)[3] == '1') {
*texReadSwizzle = Swizzle((*texReadSwizzle)[0],
(*texReadSwizzle)[1],
(*texReadSwizzle)[2],
'a');
}
}
// Seventh, lift gray/luminance calculation out of colortype so that its placement in the
// raster pipeline ops list can be controlled (vs. attached to a store). If luminance has to be
// calculated, some additional optimizations may not be possible.
const bool computeLuminance = account_for_luminance(*srcCT, SkToBool(*csSteps), dstCT);
// Eighth, consolidate red/blue swaps present in colortype, swizzle, and xferOps into just ops
{
// Any swaps from the texture's base color type, swizzle, and xfer ops can always be
// combined since those operations are grouped together, regardless of `TextureIsDst`.
int numRBSwaps = 0;
if ((*texReadSwizzle)[0] == 'b' && (*texReadSwizzle)[2] == 'r') {
// Remove the swap in the swizzle (moving towards a no-op swizzle).
*texReadSwizzle = Swizzle::Concat(*texReadSwizzle, Swizzle::BGRA());
numRBSwaps++;
}
if (bgra_to_rgba(texBaseCT)) {
numRBSwaps++;
}
if (xferOps & FormatXferOp::kSwapRB) {
numRBSwaps++;
}
// If there is not any RGB-dependent calculation between CPU and GPU data, we can also
// consolidate the swap from cpu color type (CS transfer functions and unpremul/premul
// steps are per-channel so swapping is still okay).
if (!computeLuminance && !(csSteps->fFlags.gamut_transform ||
csSteps->fFlags.src_ootf ||
csSteps->fFlags.dst_ootf)) {
// TODO(michaelludwig): Once we push finalOps back into raster pipeline if we need SkRP,
// we can always apply this to cpuCT to remove an implicit swap_rb op.
SkColorType swappedCT = *cpuCT;
if (bgra_to_rgba(&swappedCT) && swappedCT == *texBaseCT) {
numRBSwaps++;
*cpuCT = swappedCT;
}
}
// An even number of swaps is a no-op; an odd number of swaps is the same as one swap. The
// swap can be skipped if the source data is known to have the same values in R and B, or
// if R and B will be discarded.
bool srcHasRBData = *srcCT != kGray_8_SkColorType && !SkColorTypeIsAlphaOnly(*srcCT);
bool dstKeepsRBData = !SkColorTypeIsAlphaOnly(*dstCT) || computeLuminance;
if ((numRBSwaps & 1) && srcHasRBData && dstKeepsRBData) {
finalOps |= kSwapRB;
}
}
if (xferOps & FormatXferOp::kDropAlpha) {
if constexpr (TextureIsDst) {
// On CPU->GPU conversion, FormatXferOp::kDropAlpha actually drops the alpha bits
finalOps |= kDropAlpha;
} else {
finalOps |= kPadAlpha;
}
// Remove kForceOpaque as it's redundant with kDropAlpha/kPadAlpha
finalOps &= ~kForceOpaque;
}
return {finalOps, computeLuminance};
}
} // anonymous namespace
std::optional<TextureFormatXferFn> TextureFormatXferFn::MakeCpuToGpu(
SkColorType srcCT,
const SkColorSpaceXformSteps& csSteps,
TextureFormat dstFormat,
Swizzle dstReadSwizzle) {
auto [baseCT, xferOps] = TextureFormatColorTypeInfo(dstFormat);
if (xferOps & FormatXferOp::kDisabled) {
return std::nullopt;
}
SkColorSpaceXformSteps csStepsOptimized = csSteps;
auto [postOps, luminance] = optimize_transfer</*TextureIsDst=*/true>(
&srcCT, &baseCT, &dstReadSwizzle, &csStepsOptimized, xferOps);
// The CPU -> GPU transform is:
// SkRP{load(srcCT) ->
// csSteps? ->
// luminance? -> NOTE: luminance must be computed *before* the texture's swizzle
// srcToDst(dstReadSwizzle^-1)? ->
// store(baseCT)}? ->
// postOps(baseCT->TF)?
auto rp = RPOps::Make(srcCT, baseCT, &postOps, // ==> rpModifiers
csStepsOptimized, BT709Luminance{luminance}, dstReadSwizzle.invert());
return TextureFormatXferFn(dstFormat, /*preOps=*/0, std::move(rp), postOps);
}
std::optional<TextureFormatXferFn> TextureFormatXferFn::MakeGpuToCpu(
TextureFormat srcFormat,
Swizzle srcReadSwizzle,
const SkColorSpaceXformSteps& csSteps,
SkColorType dstCT) {
auto [baseCT, xferOps] = TextureFormatColorTypeInfo(srcFormat);
if (xferOps & FormatXferOp::kDisabled) {
return std::nullopt;
}
SkColorSpaceXformSteps csStepsOptimized = csSteps;
auto [preOps, luminance] = optimize_transfer</*TextureIsDst=*/false>(
&dstCT, &baseCT, &srcReadSwizzle, &csStepsOptimized, xferOps);
if (preOps & kIgnoreSrc) {
// The source is the texture format, which is what defines how ops are implemented, is
// currently set to be ignored, so pick a new format that matches the `dstCT`. Actual GPU
// support doesn't matter, this will just ensure the CPU data is initialized correctly.
srcFormat = PreferredTextureFormats(dstCT)[0];
}
// The GPU -> CPU transform is:
// preOps(TF->baseCT)? ->
// SkRP{load(baseCT) ->
// srcToDst(srcReadSwizzle)? ->
// csSteps? ->
// luminance? ->
// store(dstCT)}?
auto rp = RPOps::Make(baseCT, dstCT, &preOps, // ==> rpModifiers
srcReadSwizzle, csStepsOptimized, BT709Luminance{luminance});
return TextureFormatXferFn(srcFormat, preOps, std::move(rp), /*postOps=*/0);
}
std::optional<TextureFormatXferFn> TextureFormatXferFn::MakeIdentity(TextureFormat format) {
auto [baseCT, xferOps] = TextureFormatColorTypeInfo(format);
if (xferOps & FormatXferOp::kDisabled) {
if (TextureFormatCompressionType(format) == SkTextureCompressionType::kNone) {
return std::nullopt;
} // else allow compressed formats through for identity conversion uploads
}
return TextureFormatXferFn(format, /*preOps=*/0, /*rp=*/nullptr, /*postOps=*/0);
}
template <typename... RPModifiers>
sk_sp<TextureFormatXferFn::RPOps> TextureFormatXferFn::RPOps::Make(
SkColorType srcColorType,
SkColorType dstColorType,
uint8_t* xferOps,
RPModifiers... rpModifiers) {
if (srcColorType == dstColorType &&
(!SkToBool(rpModifiers) && ...)) {
return nullptr; // Identity conversion
}
// Luminance has to be calculated before the texture's swizzle so it's pulled out manually
// and put in the right place in `rpModifiers`, so we don't want to encounter them where
// the appendStore() also computes luminance.
SkASSERT(dstColorType != kGray_8_SkColorType);
sk_sp<RPOps> ops{new RPOps(/*srcBpp=*/SkColorTypeBytesPerPixel(srcColorType),
/*dstBpp=*/SkColorTypeBytesPerPixel(dstColorType))};
// Match SkConvertPixels swizzle_or_premul and delegate to SkOpts for certain combinations. Not
// all of the Swizzler functions are relevant because the same cases are already covered by sole
// use of ExtendedXferOps (e.g. RGBA_to_BGRA is just kSwapRB) or never occur in format transfers
// (e.g. the CMYK conversions).
auto isOnlyPremul = [] <typename RPModifier> (RPModifier modifier) {
if constexpr (std::is_same_v<RPModifier, SkColorSpaceXformSteps>) {
auto flags = modifier.fFlags;
return flags.premul &&
!(flags.unpremul || flags.linearize || flags.gamut_transform || flags.encode);
} else {
return !SkToBool(modifier);
}
};
if ((srcColorType == kRGBA_8888_SkColorType || srcColorType == kBGRA_8888_SkColorType) &&
(dstColorType == kRGBA_8888_SkColorType || dstColorType == kBGRA_8888_SkColorType)) {
// 32-bit unorm8 colors that can be swizzled with a premul go to SkOpts.
if ((isOnlyPremul(rpModifiers) && ...)) {
SkASSERT(srcColorType == dstColorType && !(*xferOps & kForceOpaque));
if (*xferOps & kSwapRB) {
ops->fSwizzler = SkOpts::RGBA_to_bgrA; // swap RB and premultiply
*xferOps &= ~kSwapRB;
} else {
ops->fSwizzler = SkOpts::RGBA_to_rgbA; // just premultiply
}
return ops;
} // else fall through to use raster pipeline
}
// NOTE: The src and dst memory contexts are not modified here, they just provide stable
// pointers for the appended ops to reference, and will be patched during run().
ops->fRP.appendLoad(srcColorType, &ops->fSrcCtx);
auto apply = [target = ops.get()] <typename RPModifier> (RPModifier modifier,
uint8_t* xferOps) {
const RPModifier* stableModifier = &modifier;
if constexpr (std::is_same_v<RPModifier, Swizzle>) {
// Swizzle does not require any persisted context, but we can also fold kSwapRB into
// the swizzle. This is done here so that the kSwapRB op is grouped with the swizzle
// modifier (given the order of modifiers passed into Make).
if (*xferOps & kSwapRB) {
modifier = Swizzle::Concat(modifier, Swizzle::BGRA());
*xferOps &= ~kSwapRB;
}
if (*xferOps & kForceOpaque) {
modifier = Swizzle::Concat(modifier, Swizzle::RGB1());
*xferOps &= ~kForceOpaque;
}
} else if constexpr (!std::is_same_v<RPModifier, BT709Luminance>) {
// We must create a copy of rpModifiers[i] in the arena, because its apply() function
// may reference parts of itself as the context's passed to the appended ops.
stableModifier = target->fArena.make<RPModifier>(modifier);
} // else BT709Luminance does not require any persisted context
stableModifier->apply(&target->fRP);
};
(apply(rpModifiers, xferOps), ...);
ops->fRP.appendStore(dstColorType, &ops->fDstCtx);
return ops;
}
int TextureFormatXferFn::getRowInvokeCount(int* width,
int* height,
size_t srcRowBytes,
size_t dstRowBytes) const {
bool denseTransfer;
const size_t widthSz = (size_t) (*width);
if (fRP) {
// Mixed transfer methods are always row-by-row to minimize intermediate storage
if (((fPreOps | fPostOps) != 0) ||
// Swizzler has to go row by row if the src and dst strides aren't dense
(fRP->fSwizzler && (widthSz * fRP->fSrcBpp != srcRowBytes ||
widthSz * fRP->fDstBpp != dstRowBytes)) ||
// RasterPipeline operates in pixel units so its built-in row handling can be used if
// the src and dst strides are multiples of the pixel sizes
(!fRP->fSwizzler && srcRowBytes % fRP->fSrcBpp == 0 &&
dstRowBytes % fRP->fDstBpp == 0)) {
// Set the strides to 0 when the control loop will be handling the src/dst pointers
fRP->fSrcCtx.stride = 0;
fRP->fDstCtx.stride = 0;
denseTransfer = false;
} else {
// RasterPipeline or the Swizzler function can be invoked just once
fRP->fSrcCtx.stride = SkTo<int>(srcRowBytes / fRP->fSrcBpp);
fRP->fDstCtx.stride = SkTo<int>(dstRowBytes / fRP->fDstBpp);
denseTransfer = true;
}
} else {
// There's only extended xfer ops, so the src and dst bpp's can be derived from the format.
const int bpp = TextureFormatBytesPerBlock(fFormat);
if (fPreOps & kPadAlpha) {
const size_t srcStride = widthSz * bpp;
const size_t dstStride = widthSz * (bpp + bpp/3);
denseTransfer = srcStride == srcRowBytes && dstStride == dstRowBytes;
} else if (fPostOps & kDropAlpha) {
const size_t srcStride = widthSz * (bpp + bpp/3);
const size_t dstStride = widthSz * bpp;
denseTransfer = srcStride == srcRowBytes && dstStride == dstRowBytes;
} else {
const size_t stride = widthSz * bpp;
denseTransfer = stride == srcRowBytes && stride == dstRowBytes;
}
}
if (denseTransfer) {
if (fRP && !fRP->fRP.empty()) {
// RasterPipeline manages its own dense transfer and needs to see the original width and
// height values, since it will use the set strides on the MemoryCtxs to adjust pointers
return 1; // one RP invocation over width x height pixels
} else {
// When not using the raster pipeline, the dense transfer happens by treating the data
// as a single row that is width*height long. This can only be done if that still fits
// into an int.
SkASSERT(!fRP || fRP->fSwizzler);
if (std::numeric_limits<int>::max() / *height > *width) {
*width *= *height;
*height = 1;
return 1; // one "width*height" invocation of swizzler or xfer_row_fn
} // otherwise fall through to do a row-by-row transfer
}
}
// If we're here, it can't be dense, so the transfer will be row-by-row
const int rowInvokeCount = *height;
*height = 1;
return rowInvokeCount;
}
void TextureFormatXferFn::run(int width, int height,
const void* src, size_t srcRowBytes,
void* dst, size_t dstRowBytes) const {
SkASSERT(width >= 1 && height >= 1);
const int rowInvokeCount = this->getRowInvokeCount(&width, &height, srcRowBytes, dstRowBytes);
SkAutoMalloc tempRowStorage; // empty if not mixing transfer methods
skia_private::STArray<2, XferRowFn> rowFns; // At most 2 actions per row
if (fPreOps) {
// `src` is definitively the texture
if (fRP) {
// We need a temporary buffer equal to srcBpp*width to hold the output of the preOps
// that is used as the source of data for SkRasterPipeline (executed per row).
tempRowStorage.reset(fRP->fSrcBpp * width);
}
rowFns.push_back(get_xfer_row_fn(fFormat, fPreOps));
}
if (fRP) {
if (fRP->fSwizzler) {
SkASSERT(fRP->fRP.empty());
rowFns.push_back([&](const char* src, char* dst, int width) {
const uint32_t* srcU32 = reinterpret_cast<const uint32_t*>(src);
uint32_t* dstU32 = reinterpret_cast<uint32_t*>(dst);
fRP->fSwizzler(dstU32, srcU32, width);
});
} else {
SkASSERT(!fRP->fRP.empty());
rowFns.push_back([&](const char* src, char* dst, int width) {
// NOTE: When height != 1, this invocation actually processes the entire image.
// Otherwise we assume src and dst have been offset by y so we update the
// MemoryCtx's pixel addresses.
fRP->fSrcCtx.pixels = const_cast<char*>(src); // This won't be written to
fRP->fDstCtx.pixels = dst;
fRP->fRP.run(0, 0, width, height);
});
}
}
if (fPostOps) {
// `dst` is definitively the texture
if (fRP) {
// We need a temporary buffer equal to dstBpp*width to hold the output of the
// SkRasterPipeline conversion that is used as the input to postOps (executed per row).
tempRowStorage.reset(fRP->fDstBpp * width);
}
rowFns.push_back(get_xfer_row_fn(fFormat, fPostOps));
}
if (rowFns.empty()) {
// Identity conversion function still needs to move the data
const int bpp = TextureFormatBytesPerBlock(fFormat);
rowFns.push_back([bpp](const char* src, char* dst, int width) {
memcpy(dst, src, bpp * width);
});
}
for (int y = 0; y < rowInvokeCount; ++y) {
// Always start by processing `src`
const char* input = static_cast<const char*>(src) + y * srcRowBytes;
for (int i = 0; i < rowFns.size(); ++i) {
// And either output to the temporary row or the final `dst`
char* target = i == rowFns.size() - 1 ? (static_cast<char*>(dst) + y * dstRowBytes)
: static_cast<char*>(tempRowStorage.get());
rowFns[i](input, target, width);
// If there's more than one rowFn, switch to using the temporary row as input
input = target;
}
}
}
#if defined(GPU_TEST_UTILS)
bool TextureFormatXferFn::isIgnoreSrcForceOpaque() const {
uint8_t ops = fPreOps | fPostOps;
return ops == (kIgnoreSrc | kForceOpaque);
}
bool TextureFormatXferFn::isDropOrPadAlpha() const {
uint8_t ops = fPreOps | fPostOps;
return ops == kPadAlpha || ops == kDropAlpha;
}
#endif // defined(GPU_TEST_UTILS)
} // namespace skgpu::graphite
// TODO(b/390473370): TextureUploadWriter isn't very useful with TextureFormatXferFn's
// capabilities and the writer model doesn't work well with the more explicit placement of dst
// data that has to happen. UploadBufferManager should just provide a span and then we can call
// run() directly and delete TextureUploadWriter entirely.
#include "src/gpu/BufferWriter.h"
namespace skgpu {
void TextureUploadWriter::convert(size_t offset, int width, int height,
const void* src, size_t srcRowBytes,
const graphite::TextureFormatXferFn& dst, size_t dstRowBytes) {
this->validate(offset + dstRowBytes * height);
void* dstPtr = SkTAddOffset<void>(fPtr, offset);
dst.run(width, height, src, srcRowBytes, dstPtr, dstRowBytes);
}
} // namespace skgpu