| /* |
| * 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 |