/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #ifndef MOZILLA_GFX_SWIZZLE_GENERIC_H_ #define MOZILLA_GFX_SWIZZLE_GENERIC_H_ #include "SwizzleGenericDecls.h" namespace mozilla::gfx { // We should generally prefer to specialize LoadRemainder_SIMD and // StoreRemainder_SIMD to avoid the memcpy call which doesn't get optimized out. // The implementations below should provide an acceptable baseline though. template static MOZ_ALWAYS_INLINE xsimd::batch LoadRemainder_SIMD( const uint8_t* aSrc, size_t aLength) { alignas(Arch::alignment()) uint8_t buffer[xsimd::batch::size]; memcpy(buffer, aSrc, aLength * 4); return xsimd::batch::load_aligned(buffer); } template static MOZ_ALWAYS_INLINE void StoreRemainder_SIMD( uint8_t* aDst, size_t aLength, const xsimd::batch& aSrc) { alignas(Arch::alignment()) uint8_t buffer[xsimd::batch::size]; aSrc.store_aligned(buffer); memcpy(aDst, buffer, aLength * 4); } template static MOZ_ALWAYS_INLINE xsimd::batch LoadRemainderRGB_SIMD( const uint8_t* aSrc, size_t aLength) { alignas(Arch::alignment()) uint8_t buffer[xsimd::batch::size]; memcpy(buffer, aSrc, aLength * 3); return xsimd::batch::load_aligned(buffer); } template static MOZ_ALWAYS_INLINE xsimd::batch ExtractAlpha_SIMD( const xsimd::batch& aSrc, const xsimd::batch& aGreenAlpha) { auto a32 = xsimd::bitwise_cast(aSrc) >> 24; return xsimd::bitwise_cast(a32 | (a32 << 16)); } template static MOZ_ALWAYS_INLINE void ProcessChunk_SIMD(const uint8_t*& aSrc, uint8_t*& aDst, int32_t aAlignedRow, int32_t aRemainder) { static constexpr int32_t batchBytes = xsimd::batch::size; const uint8_t* end = aSrc + aAlignedRow; if constexpr (Unroll) { // Process two vectors per iteration. clang unrolls the loop on the fallback // path with SSE2 and does better than our algorithm can without unrolling. const uint8_t* end2 = aSrc + (aAlignedRow & ~(2 * batchBytes - 1)); while (aSrc < end2) { auto px0 = xsimd::batch::load_unaligned(aSrc); auto px1 = xsimd::batch::load_unaligned(aSrc + batchBytes); px0 = Op(px0); px1 = Op(px1); px0.store_unaligned(aDst); px1.store_unaligned(aDst + batchBytes); aSrc += 2 * batchBytes; aDst += 2 * batchBytes; } // Process the last full vector, if any. if (aSrc < end) { auto px = xsimd::batch::load_unaligned(aSrc); px = Op(px); px.store_unaligned(aDst); aSrc += batchBytes; aDst += batchBytes; } } else { for (const uint8_t* end = aSrc + aAlignedRow; aSrc < end;) { auto px = xsimd::batch::load_unaligned(aSrc); px = Op(px); px.store_unaligned(aDst); aSrc += batchBytes; aDst += batchBytes; } } // Handle any remaining pixels that could not fit in one vector. if (aRemainder) { auto px = LoadRemainder_SIMD(aSrc, aRemainder); px = Op(px); StoreRemainder_SIMD(aDst, aRemainder, px); } } template static MOZ_ALWAYS_INLINE void ProcessRow_SIMD(const uint8_t* aSrc, uint8_t* aDst, int32_t aLength) { static constexpr int32_t batchPixels = xsimd::batch::size; int32_t alignedRow = 4 * (aLength & ~(batchPixels - 1)); int32_t remainder = aLength & (batchPixels - 1); ProcessChunk_SIMD(aSrc, aDst, alignedRow, remainder); } template static MOZ_ALWAYS_INLINE void Process_SIMD(const uint8_t* aSrc, int32_t aSrcGap, uint8_t* aDst, int32_t aDstGap, IntSize aSize) { static constexpr int32_t batchPixels = xsimd::batch::size; int32_t alignedRow = 4 * (aSize.width & ~(batchPixels - 1)); int32_t remainder = aSize.width & (batchPixels - 1); // Fold remainder into stride gap. aSrcGap += 4 * remainder; aDstGap += 4 * remainder; for (int32_t height = aSize.height; height > 0; height--) { ProcessChunk_SIMD(aSrc, aDst, alignedRow, remainder); aSrc += aSrcGap; aDst += aDstGap; } } struct SwizzleVectorSwapRbMask { static constexpr uint8_t get(uint8_t i, uint8_t) { uint8_t channel = i % 4; switch (channel) { case 0: return i + 2; case 2: return i - 2; default: return i; } return i; } }; // Swaps 8-bit R and B channels within each pixel. If the target supports byte // shuffle, then this is likely ideal. Otherwise it should be specialized. template static MOZ_ALWAYS_INLINE xsimd::batch SwapRB8_SIMD( const xsimd::batch& aPx) { constexpr auto swapRbMask = xsimd::make_batch_constant(); return xsimd::shuffle(aPx, aPx, swapRbMask); } // Swaps 16-bit R and B channels within each pixel. This doesn't require a byte // shuffle, and instead can be a 16-bit rotate of each 32-bit lane. template static MOZ_ALWAYS_INLINE xsimd::batch SwapRB16_SIMD( const xsimd::batch& aRb) { auto rb32 = xsimd::bitwise_cast(aRb); return xsimd::bitwise_cast((rb32 << 16) | (rb32 >> 16)); } // Forces the A channel to 0xFF. template static MOZ_ALWAYS_INLINE xsimd::batch OpaqueAlpha_SIMD( const xsimd::batch& aPx) { return xsimd::bitwise_cast(xsimd::bitwise_cast(aPx) | xsimd::batch(0xFF000000)); } // Swizzle a vector of pixels, swapping and making opaque as desired. template static MOZ_ALWAYS_INLINE xsimd::batch SwizzleVector_SIMD( const xsimd::batch& aSrc) { auto px = aSrc; if constexpr (aSwapRB) { px = SwapRB8_SIMD(px); } if constexpr (aOpaqueAlpha) { px = OpaqueAlpha_SIMD(px); } return px; } template static MOZ_ALWAYS_INLINE void SwizzleRow_SIMD(const uint8_t* aSrc, uint8_t* aDst, int32_t aLength) { ProcessRow_SIMD, /* Unroll */ true>(aSrc, aDst, aLength); } template static MOZ_ALWAYS_INLINE void Swizzle_SIMD(const uint8_t* aSrc, int32_t aSrcGap, uint8_t* aDst, int32_t aDstGap, IntSize aSize) { Process_SIMD, /* Unroll */ true>(aSrc, aSrcGap, aDst, aDstGap, aSize); } // Convert from CMYK to BGRX/RGBX, swapping and inverting as desired. template static MOZ_ALWAYS_INLINE xsimd::batch SwizzleCmykVector_SIMD( const xsimd::batch& aSrc) { xsimd::batch src; // Invert if necessary, as the math expects inverted CMYK. if constexpr (!aInverted) { src = ~aSrc; } else { src = aSrc; } // Isolate iC and iY with mask. auto px16 = xsimd::bitwise_cast(src); const xsimd::batch lowByte(0x00FF); auto icy = px16 & lowByte; // Isolate iM and iK by shifting down to bottom of word. auto imk = px16 >> 8; auto ik16 = ExtractAlpha_SIMD(src, imk); // Multiply each channel by the iK, add 1, divide by 255, all in place. This // is equivalent to iC * iK / 255. icy = icy * ik16; icy = (icy + (icy >> 8) + xsimd::batch(1)) >> 8; imk = imk * ik16; imk = (imk + (imk >> 8) + xsimd::batch(1)) >> 8; // Swap R and B if necessary. if constexpr (aSwapRB) { icy = SwapRB16_SIMD(icy); } // Combine back to final pixel. return OpaqueAlpha_SIMD(xsimd::bitwise_cast(icy | (imk << 8))); } template void MOZ_ALWAYS_INLINE SwizzleCmykRow_SIMD(const uint8_t* aSrc, uint8_t* aDst, int32_t aLength) { ProcessRow_SIMD>( aSrc, aDst, aLength); } // Premultiply a vector of pixels, swapping and making opaque as desired. template static MOZ_ALWAYS_INLINE xsimd::batch PremultiplyVector_SIMD( const xsimd::batch& aSrc) { // Isolate R and B with mask. auto px16 = xsimd::bitwise_cast(aSrc); const xsimd::batch lowByte(0x00FF); auto rb = px16 & lowByte; // Isolate G and A by shifting down to bottom of word. auto ga = px16 >> 8; auto a16 = ExtractAlpha_SIMD(aSrc, ga); // If format is not opaque, force A to 255 so that A*alpha/255 = alpha. if constexpr (!aOpaqueAlpha) { ga = xsimd::bitwise_cast(xsimd::bitwise_cast(ga) | xsimd::batch(0x00FF0000)); } // Multiply each channel by the alpha, add 255, divide by 255, all in place. rb = xsimd::fma(rb, a16, xsimd::batch(0xFF)); rb = (rb + (rb >> 8)) >> 8; ga = xsimd::fma(ga, a16, xsimd::batch(0xFF)); ga = (ga + (ga >> 8)) >> 8; // Swap R and B if necessary. if constexpr (aSwapRB) { rb = SwapRB16_SIMD(rb); } // Combine back to final pixel. auto px = xsimd::bitwise_cast(rb | (ga << 8)); if constexpr (aOpaqueAlpha) { px = OpaqueAlpha_SIMD(px); } return px; } template static MOZ_ALWAYS_INLINE void PremultiplyRow_SIMD(const uint8_t* aSrc, uint8_t* aDst, int32_t aLength) { ProcessRow_SIMD>( aSrc, aDst, aLength); } template static MOZ_ALWAYS_INLINE void Premultiply_SIMD(const uint8_t* aSrc, int32_t aSrcGap, uint8_t* aDst, int32_t aDstGap, IntSize aSize) { Process_SIMD>( aSrc, aSrcGap, aDst, aDstGap, aSize); } template static MOZ_ALWAYS_INLINE xsimd::batch UnpremultiplyLookup_SIMD( const xsimd::batch& aSrc, const xsimd::batch& aGa) { auto alpha = xsimd::bitwise_cast(aSrc) >> 24; return xsimd::batch::gather(sUnpremultiplyTable, alpha); } template static MOZ_ALWAYS_INLINE xsimd::batch UnpremultiplyReverse_SIMD( const xsimd::batch& aSrc, const xsimd::batch& aRecip, const xsimd::batch& aRb, const xsimd::batch& aGa) { // Split each reciprocal into low and high 16-bit halves, each duplicated into // both 16-bit words of its lane to line up with the R/B and G/A word layout // (Qn Qn per pixel): qLo = Q & 0xFFFF, qHi = Q >> 16. auto qLo = aRecip & xsimd::batch(0x0000FFFF); qLo = qLo | (qLo << 16); auto qHi = aRecip >> 16; qHi = qHi | (qHi << 16); auto qLo16 = xsimd::bitwise_cast(qLo); auto qHi16 = xsimd::bitwise_cast(qHi); // Isolate G now so that we don't accidentally unpremultiply A. auto ga = xsimd::bitwise_cast(xsimd::bitwise_cast(aGa) & xsimd::batch(0x000000FF)); // Exact (channel * reciprocal) >> 16 in 16-bit lanes. Since // channel*Q = channel*qHi*0x10000 + channel*qLo, // (channel*Q) >> 16 = channel*qHi + ((channel*qLo) >> 16) // = mullo(channel, qHi) + mulhi(channel, qLo). // mullo gives the exact low 16 bits of channel*qHi; masking to a byte keeps // the high byte clear for the recombine below and makes any out-of-range // (channel > alpha) input wrap to the low byte exactly as the scalar path. const xsimd::batch lowByte(0x00FF); auto rb = (aRb * qHi16 + xsimd::mul_hi(aRb, qLo16)) & lowByte; ga = (ga * qHi16 + xsimd::mul_hi(ga, qLo16)) & lowByte; // Combine back to final pixel with rb | (ga << 8) | (aSrc & 0xFF000000), // which will add back on the original alpha value unchanged. auto alpha = xsimd::bitwise_cast(xsimd::bitwise_cast(aSrc) & xsimd::batch(0xFF000000)); return xsimd::bitwise_cast(rb | (ga << 8) | alpha); } // Unpremultiply a vector of pixels, swapping and making opaque as desired. template static MOZ_ALWAYS_INLINE xsimd::batch UnpremultiplyVector_SIMD( const xsimd::batch& aSrc) { // Isolate R and B with mask. const xsimd::batch lowByte(0x00FF); auto px16 = xsimd::bitwise_cast(aSrc); auto rb = px16 & lowByte; // Swap R and B if necessary. if constexpr (aSwapRB) { rb = SwapRB16_SIMD(rb); } // Isolate G and A by shifting down to bottom of word. auto ga = px16 >> 8; // Extract the reciprocals from the lookup table. auto recip = UnpremultiplyLookup_SIMD(aSrc, ga); // Perform the unpremultiply. return UnpremultiplyReverse_SIMD(aSrc, recip, rb, ga); } template static MOZ_ALWAYS_INLINE void UnpremultiplyRow_SIMD(const uint8_t* aSrc, uint8_t* aDst, int32_t aLength) { ProcessRow_SIMD>(aSrc, aDst, aLength); } template static MOZ_ALWAYS_INLINE void Unpremultiply_SIMD(const uint8_t* aSrc, int32_t aSrcGap, uint8_t* aDst, int32_t aDstGap, IntSize aSize) { Process_SIMD>( aSrc, aSrcGap, aDst, aDstGap, aSize); } template struct UnpackRowRGB24ExpandMask { static constexpr uint8_t get(uint8_t i, uint8_t) { // Every 4th byte is the alpha which we turn opaque in another step. Here, // we just duplicate the first channel into the alpha. uint8_t channel = i % 4; if (channel == 3) { return 0; } // Swap R and B if necessary as well. uint8_t pixel = i / 4; return 3 * pixel + (aSwapRB ? 2 - channel : channel); } }; template static MOZ_ALWAYS_INLINE xsimd::batch UnpackBatchRGB24_SIMD( const xsimd::batch& aSrc) { constexpr auto expandMask = xsimd::make_batch_constant, Arch>(); // Expand the packed RGB pixels to RGBX and force the alpha to be opaque. return OpaqueAlpha_SIMD(xsimd::swizzle(aSrc, expandMask)); } template static MOZ_ALWAYS_INLINE void UnpackRowRGB24_SIMD(const uint8_t* aSrc, uint8_t* aDst, int32_t aLength) { // Each pass writes batchSize pixels (4, 8, 16), or batchBytes bytes (16, // 32, 64). static constexpr int32_t batchPixels = xsimd::batch::size; static constexpr int32_t batchBytes = xsimd::batch::size; // Each pass reads batchBytes worth of data, which maps to batchPackedPixels // (6, 11, 22) packed pixels (rounded up) per pass. static constexpr int32_t batchPackedPixels = (batchBytes + 2) / 3; // The main loop reads batchBytes at once, overrunning the packed pixels it // consumes, so alignedRow marks the pixels that can be read that way // without running past the source. The trailing pixels (and short rows that // cannot use the main loop at all) are handled with safe partial loads // instead. int32_t alignedRow = 0; if (aLength >= batchPackedPixels) { static constexpr int32_t batchPackedPixelsOverrun = batchPackedPixels - batchPixels; alignedRow = (aLength - batchPackedPixelsOverrun) & ~(batchPixels - 1); } // Because we are expanding in place, process strictly back to front. Handle // the trailing pixels first, in up to batchPixels chunks, using loads that // never read past the 3*aLength source bytes. for (int32_t pos = aLength; pos > alignedRow;) { int32_t n = pos - alignedRow < batchPixels ? pos - alignedRow : batchPixels; int32_t start = pos - n; auto px = LoadRemainderRGB_SIMD(aSrc + start * 3, n); px = UnpackBatchRGB24_SIMD(px); if (n == batchPixels) { px.store_unaligned(aDst + start * 4); } else { StoreRemainder_SIMD(aDst + start * 4, n, px); } pos = start; } // Process the remaining aligned pixels as full vectors, back to front. const uint8_t* src = aSrc + (alignedRow - batchPixels) * 3; uint8_t* dst = aDst + (alignedRow - batchPixels) * 4; while (src >= aSrc) { auto px = xsimd::batch::load_unaligned(src); px = UnpackBatchRGB24_SIMD(px); px.store_unaligned(dst); src -= batchPixels * 3; dst -= batchPixels * 4; } } } // namespace mozilla::gfx #endif /* MOZILLA_GFX_SWIZZLE_GENERIC_H_ */