blob: e86f1353943145df20b4a0c3ed52becb46793e48 [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.
*/
#ifndef skgpu_graphite_sparse_strips_StripProcessorSimd_DEFINED
#define skgpu_graphite_sparse_strips_StripProcessorSimd_DEFINED
#include "include/private/SkTDArray.h"
#include "src/core/SkVx.h"
#include "src/gpu/graphite/sparse_strips/Polyline.h"
#include "src/gpu/graphite/sparse_strips/SparseStripsTypes.h"
#include "src/gpu/graphite/sparse_strips/Strip.h"
#include "src/gpu/graphite/sparse_strips/Tiler.h"
#include <algorithm>
#include <array>
#include <cmath>
#include <cstdint>
#include <cstring>
#include <limits>
namespace skgpu::graphite {
template <uint16_t kTileWidth, uint16_t kTileHeight, bool kIsWinding>
class StripProcessorSimd {
public:
// Type aliases for convenient handling of the simd registers. In addition to SIMD, we use SWAR
// to pack subsample winding 4 to 1 u32, each subsample being alloted 1 byte of memory, e.g.
// -128 / 127 of winding. So a quality of 8 subsamples per pixel (MSAAx8) requires u32x2
// (SwarPixel) or u8x8 (PixelBytes). Note, currently, this class is hardcoded to MSAAx8.
// Although it would be fairly straight forward to make this class handle a variable subsample
// count, we do not anticipate running above MSAAx8 on the CPU, due to a lack of throughput.
// (Many devices do not 256-wide SIMD, doubling the subsample count doubles memory requirements,
// etc.)
static_assert(Strip::kNumSubSamples == 8);
using SwarPixel = skvx::Vec<2, uint32_t>;
using PixelBytes = skvx::Vec<8, uint8_t>;
StripProcessorSimd(SkTDArray<Strip>* stripBuf,
SkTDArray<uint8_t>* alphaBuf,
bool isInverse,
const Polyline& polyline,
const SkTDArray<uint8_t>& maskLut,
int32_t initialAlphaIdx
#if defined(GPU_TEST_UTILS)
, MsaaExactMaskObserver observer
#endif
)
: fCoarseWinding(0)
, fStripBuf(stripBuf)
, fAlphaBuf(alphaBuf)
, fIsInverse(isInverse)
, fPolyline(polyline)
, fMaskLut(maskLut)
, fLocalAlphaIdx(initialAlphaIdx)
#if defined(GPU_TEST_UTILS)
, fObserver(observer)
#endif
{
this->clearWinding(kInitialWinding);
}
SK_ALWAYS_INLINE void clearWinding(uint32_t val) {
SwarPixel* flatSubsampleWinding = &fSubsampleWinding[0][0];
if constexpr (kTileWidth % 8 == 0) {
skvx::Vec<16, uint32_t> vec(val);
for (int i = 0; i < kTilePixelCount; i += 8) {
vec.store(flatSubsampleWinding + i);
}
} else if constexpr (kTileWidth % 4 == 0) {
skvx::Vec<8, uint32_t> vec(val);
for (int i = 0; i < kTilePixelCount; i += 4) {
vec.store(flatSubsampleWinding + i);
}
} else {
SwarPixel p = {val, val};
for (int i = 0; i < kTilePixelCount; ++i) {
flatSubsampleWinding[i] = p;
}
}
}
SK_ALWAYS_INLINE void clearWithCoarseWinding() {
uint8_t windingByte;
if constexpr (kIsWinding) {
// Cast to i8 to sign extend before casting to u8, then apply SWAR bias to map -127/128
// to 0/255.
windingByte = 0x80 + static_cast<uint8_t>(static_cast<int8_t>(fCoarseWinding));
} else {
windingByte = (fCoarseWinding & 1) ? 1 : 0;
}
this->clearWinding(windingByte * 0x01010101u);
}
SK_ALWAYS_INLINE void clearWindingForNewRow() { this->clearWinding(kInitialWinding); }
SK_ALWAYS_INLINE static bool ShouldFill(int32_t w) {
if constexpr (kIsWinding) {
return w != 0;
} else {
return (w & 1) != 0;
}
}
SK_ALWAYS_INLINE int32_t coarseWinding() const { return fCoarseWinding; }
SK_ALWAYS_INLINE void setCoarseWinding(int32_t val) { fCoarseWinding = val; }
SK_ALWAYS_INLINE int32_t localAlphaIdx() const { return fLocalAlphaIdx; }
// Convert the winding to alpha in row sized chunks. Technically, processChunk could be renamed
// to processRow, but it is intended to be flexible, so that if the tile width were to exceed
// the simd width, the row could be proccessed in serial chunks.
SK_ALWAYS_INLINE void resolveWindingToAlpha() {
uint8_t* tileAlphaBase = reserveAlphaBuffer();
for (int32_t row = 0; row < kTileHeight; ++row) {
if constexpr (kTileWidth % 8 == 0) {
for (int32_t column = 0; column < kTileWidth; column += 8) {
this->processChunk<8>(row, column, tileAlphaBase);
tileAlphaBase += 8;
}
} else if constexpr (kTileWidth % 4 == 0) {
for (int32_t column = 0; column < kTileWidth; column += 4) {
this->processChunk<4>(row, column, tileAlphaBase);
tileAlphaBase += 4;
}
}
}
fLocalAlphaIdx += kTilePixelCount;
}
SK_ALWAYS_INLINE void rasterizeLineToTile(const Tile& tile, std::array<SkPoint, 2> tileBounds) {
Line line = fPolyline.getLine(tile.lineIdx());
bool canonicalXDir = line.p1.fX >= line.p0.fX;
bool canonicalYDir = line.p1.fY >= line.p0.fY;
uint32_t windingBit = tile.coarseWinding() ? 1 : 0;
if constexpr (kIsWinding) {
fCoarseWinding += (canonicalYDir ? 1 : -1) * static_cast<int32_t>(windingBit);
} else {
fCoarseWinding ^= static_cast<int32_t>(windingBit);
}
// TODO (thomsmit): remove once culling lands
// Cull lines that exist entirely to the left of the tile.
float rightEdge = canonicalXDir ? line.p1.fX : line.p0.fX;
if (rightEdge < 0.0f) {
return;
}
float dx = line.p1.fX - line.p0.fX;
float dy = line.p1.fY - line.p0.fY;
float invDx = (std::abs(dx) <= Strip::kStripEpsilon) ? 0.0f : 1.0f / dx;
float invDy = (std::abs(dy) <= Strip::kStripEpsilon) ? 0.0f : 1.0f / dy;
float dxdy = dx * invDy;
std::array<float, 4> derivs = {dx, dy, invDx, invDy};
auto [clippedLine, topIsOnLeftEdge, botIsOnLeftEdge] =
Tile::ClipToTile<kTileWidth, kTileHeight>(line,
tileBounds,
derivs,
tile.intersectionMask(),
canonicalXDir,
canonicalYDir);
SkPoint pTop = clippedLine.p0;
SkPoint pBot = clippedLine.p1;
if (tile.hasLeftIntersection()) {
float yEdge = (pTop.fX < pBot.fX) ? pTop.fY : pBot.fY;
this->fillLeft(yEdge, canonicalXDir);
}
if (std::abs(dy) < Strip::kStripEpsilon && pTop.fY == std::floor(pTop.fY)) {
return;
}
int32_t startY = static_cast<int32_t>(std::floor(pTop.fY));
int32_t endY = static_cast<int32_t>(std::ceil(pBot.fY));
if (startY < endY) {
bool isOnlyRow = (startY == endY - 1);
auto rowInt = FindRowIntersections(pTop, pBot, dxdy, startY, endY);
LineStepParams stepParams = this->computeLineStepParams(pTop, pBot, dx, dy);
// First, and possibly only row
{
float pTopY = pTop.fY;
float pBotY = isOnlyRow ? pBot.fY : static_cast<float>(startY + 1);
bool crossedTop = (pTopY == std::floor(pTopY));
bool defaultInvert = !crossedTop && stepParams.fSortedXDir;
uint8_t startMaskVal = 0xff;
bool startInvert = topIsOnLeftEdge && !crossedTop;
if (!topIsOnLeftEdge) {
startMaskVal = GetTruncationMask</*kIsStart=*/true>(pTopY,
static_cast<float>(startY));
}
uint8_t endMaskVal = 0xff;
if (isOnlyRow && !botIsOnLeftEdge) {
endMaskVal = GetTruncationMask</*kIsStart=*/false>(pBotY,
static_cast<float>(startY));
}
bool leftInvert = stepParams.fSortedXDir ? startInvert : defaultInvert;
bool rightInvert = stepParams.fSortedXDir ? defaultInvert : startInvert;
uint8_t leftMask = stepParams.fSortedXDir ? startMaskVal : endMaskVal;
uint8_t rightMask = stepParams.fSortedXDir ? endMaskVal : startMaskVal;
this->processRowSpan(startY, leftMask, rightMask, leftInvert, defaultInvert,
rightInvert, crossedTop, rowInt, stepParams, canonicalYDir);
}
// Middle rows
for (int32_t row = startY + 1; row < endY - 1; ++row) {
this->processRowSpan(row, /*leftMask=*/0xff, /*rightMask=*/0xff,
/*leftInvert=*/false, /*midInvert=*/false,
/*rightInvert=*/false, /*crossedTop=*/true, rowInt, stepParams,
canonicalYDir);
}
// Bottom row, if it exists
if (!isOnlyRow) {
int32_t lastY = endY - 1;
uint8_t endMaskLast = 0xff;
if (!botIsOnLeftEdge) {
endMaskLast = GetTruncationMask</*kIsStart=*/false>(pBot.fY,
static_cast<float>(lastY));
}
uint8_t leftMask = stepParams.fSortedXDir ? 0xff : endMaskLast;
uint8_t rightMask = stepParams.fSortedXDir ? endMaskLast : 0xff;
this->processRowSpan(lastY, leftMask, rightMask, /*leftInvert=*/false,
/*midInvert=*/false, /*rightInvert=*/false,
/*crossedTop=*/true, rowInt, stepParams, canonicalYDir);
}
}
}
private:
static constexpr int32_t kTilePixelCount = kTileWidth * kTileHeight;
static constexpr uint32_t kInitialWinding = kIsWinding ? 0x80808080u : 0u;
struct LineStepParams {
const uint8_t* fMaskRowLut;
int32_t fStepXFixed;
int32_t fStepYFixed;
int32_t fTBaseFixed;
bool fSortedXDir;
};
template <int N>
SK_ALWAYS_INLINE static void ApplyWinding(SwarPixel* target, uint32_t fillVal) {
auto vSubsampleWinding = skvx::Vec<N, uint32_t>::Load(target);
skvx::Vec<N, uint32_t> vFill(fillVal);
if constexpr (kIsWinding) {
auto windingBytes = sk_bit_cast<skvx::Vec<N * 4, uint8_t>>(vSubsampleWinding);
auto fillBytes = sk_bit_cast<skvx::Vec<N * 4, uint8_t>>(vFill);
vSubsampleWinding = sk_bit_cast<skvx::Vec<N, uint32_t>>(windingBytes + fillBytes);
} else {
vSubsampleWinding ^= vFill;
}
vSubsampleWinding.store(target);
}
// Evaluate the subsamples against the winding rule to determine which are "active" (covered).
// For Even-Odd no processing is required. For the Non-Zero case, we compare subsample winding
// against the empty SWAR mask. In SkVX, the result of !=, true, is ~0, so we mask away any
// extraneous bits.
template <typename VecT> static SK_ALWAYS_INLINE VecT GetActivesWide(VecT v) {
using Vec8 = skvx::Vec<sizeof(VecT), uint8_t>;
if constexpr (kIsWinding) {
const Vec8 emptyBytes(0x80);
Vec8 vecBytes = sk_bit_cast<skvx::Vec<sizeof(VecT), uint8_t>>(v);
Vec8 cmp = (vecBytes != emptyBytes);
return sk_bit_cast<VecT>(cmp) & VecT(0x01010101u);
} else {
SkASSERT(all(sk_bit_cast<Vec8>(v) <= 1));
return v;
}
}
SK_ALWAYS_INLINE uint8_t* reserveAlphaBuffer() {
if (fAlphaBuf->size() + kTilePixelCount > fAlphaBuf->capacity()) {
constexpr size_t kChunkSize = 4 * kTilePixelCount;
fAlphaBuf->reserve(fAlphaBuf->capacity() + kChunkSize);
}
return fAlphaBuf->append(kTilePixelCount);
}
#if defined(GPU_TEST_UTILS)
SK_ALWAYS_INLINE void observeChunk(int32_t row, int32_t column, int32_t chunkSize) {
for (int32_t x = column; x < column + chunkSize; ++x) {
SwarPixel v = fSubsampleWinding[row][x];
uint8_t exactMask = 0;
uint32_t lo = v[0];
uint32_t hi = v[1];
for (int s = 0; s < 4; ++s) {
int8_t sLo = static_cast<int8_t>(lo & 0xFF);
int8_t sHi = static_cast<int8_t>(hi & 0xFF);
if constexpr (kIsWinding) {
sLo = static_cast<int8_t>(static_cast<uint8_t>(sLo) - 0x80);
sHi = static_cast<int8_t>(static_cast<uint8_t>(sHi) - 0x80);
}
if (ShouldFill(sLo)) exactMask |= (1 << s);
if (ShouldFill(sHi)) exactMask |= (1 << (s + 4));
lo >>= 8;
hi >>= 8;
}
if (fIsInverse) {
exactMask = ~exactMask & ((1 << Strip::kNumSubSamples) - 1);
}
fObserver(exactMask);
}
}
#endif
// Resolves the subsample windings of a chunk of pixels to their equivalent 8-bit alpha values:
//
// 1) GetActivesWide(): Evaluates the winding rule, returning a skvx::Vec of the same size
// where each byte lane contains 1 (active) or 0 (inactive).
// 2) activeLo / activeHi: Splits the active masks into two separate 32-bit words, each
// containing 4 subsamples (one per byte lane).
// 3) combinedBytes: Sums the two halves together. Each byte lane now holds a value of 0, 1,
// or 2.
// 4) SWAR Horizontal Sum: Multiplying by 0x01010101u (which is 2^24 + 2^16 + 2^8 + 1)
// accumulates the sum of all 4 byte lanes into the highest byte. Given a 32-bit word
// [ A | B | C | D ], the multiplication expands across the lanes:
// Shifted by 0: [ A | B | C | D ]
// + Shifted by 8: [ B | C | D | 0 ]
// + Shifted by 16: [ C | D | 0 | 0 ]
// + Shifted by 24: [ D | 0 | 0 | 0 ]
// Summed together, the highest byte holds A + B + C + D. This can never overflow into
// neighboring lanes, since the maximum value per byte lane is 2.
// 5) Shifting >> 24: Right-shifting the result isolates the final sum. This discards the lower
// accumulation bytes and moves the total subsample count into the lowest byte.
// 6) Alpha Conversion: The total is multiplied by the maximum alpha value (255), summed by
// subsampleCount / 2 for rounding, and right-shifted by log2(subsampleCount). This keeps
// the entire conversion in integer math and avoids division.
template <int kChunkSize>
SK_ALWAYS_INLINE void processChunk(int32_t row,
int32_t column,
uint8_t* tileAlphaBase) {
auto vSubsampleWinding =
skvx::Vec<kChunkSize * 2, uint32_t>::Load(&fSubsampleWinding[row][column]);
auto actives = GetActivesWide(vSubsampleWinding);
skvx::Vec<kChunkSize, uint32_t> activeLo;
skvx::Vec<kChunkSize, uint32_t> activeHi;
if constexpr (kChunkSize == 8) {
activeLo = skvx::shuffle<0, 2, 4, 6, 8, 10, 12, 14>(actives);
activeHi = skvx::shuffle<1, 3, 5, 7, 9, 11, 13, 15>(actives);
} else {
activeLo = skvx::shuffle<0, 2, 4, 6>(actives);
activeHi = skvx::shuffle<1, 3, 5, 7>(actives);
}
auto combinedBytes = activeLo + activeHi;
skvx::Vec<kChunkSize, uint32_t> activeSamples = (combinedBytes * 0x01010101u) >> 24;
const uint32_t invMask32 = fIsInverse ? 0xffffffff : 0;
skvx::Vec<kChunkSize, uint32_t> alpha32 = ((activeSamples * 255 + 4) >> 3) ^ invMask32;
skvx::cast<uint8_t>(alpha32).store(tileAlphaBase);
#if defined(GPU_TEST_UTILS)
if (fObserver) {
observeChunk(row, column, kChunkSize);
}
#endif
}
SK_ALWAYS_INLINE static std::array<float, kTileHeight + 1> FindRowIntersections(
SkPoint pTop, SkPoint pBot, float dxdy, int32_t startY, int32_t endY) {
std::array<float, kTileHeight + 1> rowInt;
skvx::float4 vPTopX(pTop.fX);
skvx::float4 vPTopY(pTop.fY);
skvx::float4 vDxDy(dxdy);
skvx::float4 vBase(0.0f, 1.0f, 2.0f, 3.0f);
int32_t height;
if constexpr (kTileHeight > 4) {
height = (endY + 3) / 4 * 4;
} else {
height = 4;
}
for (int32_t k = 0; k < height; k += 4) {
skvx::float4 vGridY = skvx::float4(static_cast<float>(k)) + vBase;
skvx::float4 vGridX = vPTopX + (vGridY - vPTopY) * vDxDy;
vGridX.store(rowInt.data() + k);
}
rowInt[startY] = pTop.fX;
rowInt[endY] = pBot.fX;
return rowInt;
}
template<bool kIsStart>
SK_ALWAYS_INLINE static uint8_t GetTruncationMask(float p, float row) {
uint32_t shift = static_cast<uint32_t>(std::round(8.0f * (p - static_cast<float>(row))));
if constexpr (kIsStart) {
return static_cast<uint8_t>(0xff << shift);
} else {
return static_cast<uint8_t>(~(0xff << shift));
}
}
SK_ALWAYS_INLINE LineStepParams computeLineStepParams(SkPoint pTop, SkPoint pBot,
float dx, float dy) const {
float normalX = dy;
float normalY = -dx;
if (normalX < 0.0f) {
normalX = -normalX;
normalY = -normalY;
}
float D = normalX + std::abs(normalY);
float invD = (D < Strip::kStripEpsilon) ? 0.0f : 1.0f / D;
bool hasPositiveSlope = normalY <= 0.0f;
float C = normalX * pTop.fX + normalY * pTop.fY;
float s = std::abs(normalY) * invD;
int lutRowOffset = std::clamp(
static_cast<int>(std::floor(s * (Strip::kLutMaskHeight / 2))),
0,
(Strip::kLutMaskHeight / 2) - 1);
int lutRow = hasPositiveSlope ? (lutRowOffset + Strip::kLutMaskHeight / 2) : lutRowOffset;
// Unlike the scalar version, we simply return the raw pointer to the row in the LUT
const uint8_t* maskRowLut = fMaskLut.data() + (lutRow * Strip::kLutMaskWidth);
float stepX = normalX * invD;
float stepY = normalY * invD;
float tBase = ((hasPositiveSlope ? normalX : D) - C) * invD;
// We use 16.16 fixed-point arithmetic to avoid floating-point math and conversions inside
// the inner loops. A 16.16 fixed-point number uses a 32-bit integer where:
// - The upper 16 bits represent the integer part.
// - The lower 16 bits represent the fractional part.
//
// Encoding: value_fixed = round(x * 65536)
// Decoding (floor): integer_part = value_fixed >> 16
//
// In addition to the 16.16 representation (scaled by 65536), we scale by the LUT's width
// (64.0) so that extracting the LUT column index `u = floor(t * 64)` can be done using a
// simple bit-shift: `u = clamp(tFixed >> 16, 0, 63)`.
//
// Note: We use direct static_cast conversions to int32_t instead of calling SkFloatToFixed
// because the step and base variables are guaranteed to neither overflow or underflow, so
// they don't require saturation checks.
constexpr float kFixedMult = 64.0f * 65536.0f;
int32_t stepXFixed = static_cast<int32_t>(stepX * kFixedMult);
int32_t stepYFixed = static_cast<int32_t>(stepY * kFixedMult);
int32_t tBaseFixed = static_cast<int32_t>(tBase * kFixedMult);
bool sortedXDir = pTop.fX <= pBot.fX;
return {
maskRowLut,
stepXFixed,
stepYFixed,
tBaseFixed,
sortedXDir
};
}
SK_ALWAYS_INLINE void fillLeft(float yEdge, bool canonicalXDir) {
uint8_t fillByte;
if constexpr (kIsWinding) {
fillByte = canonicalXDir ? 0xFF : 1;
} else {
fillByte = 1;
}
uint32_t fill32 = fillByte * 0x01010101u;
int32_t startY = static_cast<int32_t>(std::ceil(yEdge));
for (int32_t row = startY; row < kTileHeight; ++row) {
if constexpr (kTileWidth % 8 == 0) {
for (int32_t column = 0; column < kTileWidth; column += 8) {
ApplyWinding<16>(&fSubsampleWinding[row][column], fill32);
}
} else if constexpr (kTileWidth % 4 == 0) {
for (int32_t column = 0; column < kTileWidth; column += 4) {
ApplyWinding<8>(&fSubsampleWinding[row][column], fill32);
}
}
}
}
template <bool kIsEdgePixel>
SK_ALWAYS_INLINE void processPixel(SwarPixel* pixel,
uint8_t truncationMask,
const PixelBytes& pInvert,
int32_t tFixed,
const uint8_t* maskRowLut,
bool canonicalYDir) {
// Shift right by 16 to extract the integer LUT column index `u = floor(t * 64)`.
int column = std::clamp(tFixed >> 16, 0, Strip::kLutMaskWidthExcl);
uint8_t maskVal = maskRowLut[column];
// Apply the truncation mask if we're one of the candidate pixels.
if constexpr (kIsEdgePixel) {
maskVal &= truncationMask;
}
/*
* Convert 1 byte of packed coverage bits (maskVal) into 8 separate SIMD byte lanes.
* E.g., maskVal = 0b10100011:
*
* 1) Splat maskVal into all 8 lanes:
* [ L0 | L1 | L2 | L3 | L4 | L5 | L6 | L7 ]
* [10100011 | 10100011 | 10100011 | 10100011 | 10100011 | 10100011 | 10100011 | 10100011]
*
* 2) Bitwise AND with vBit (2^0, 2^1, ..., 2^7) to isolate bit k in lane k:
* & [00000001 | 00000010 | 00000100 | 00001000 | 00010000 | 00100000 | 01000000 | 10000000]
* ---------------------------------------------------------------------------------------
* [00000001 | 00000010 | 00000000 | 00000000 | 00000000 | 00100000 | 00000000 | 10000000]
*
* 3) In SkVX, Vector comparison of != 0 yields ~0 for true (SkVx.h:L316-L318,L357-359); this is
* then negated to produce the correct winding across the lanes:
* != 0: [ 0xFF | 0xFF | 0x00 | 0x00 | 0x00 | 0xFF | 0x00 | 0xFF ]
* Unary -:[ 0x01 | 0x01 | 0x00 | 0x00 | 0x00 | 0x01 | 0x00 | 0x01 ]
* (+1 for covered lanes, 0x00 for uncovered)
*
* WARNING: relies on pcmpeqb behavior for 3! If this is not true, this will fail.
*/
const PixelBytes vBit{1, 2, 4, 8, 16, 32, 64, 128};
PixelBytes pRes = -((PixelBytes(maskVal) & vBit) != 0);
PixelBytes pSubsampleWinding = sk_bit_cast<PixelBytes>(*pixel);
if constexpr (kIsWinding) {
pRes -= pInvert;
if (canonicalYDir) {
pSubsampleWinding += pRes;
} else {
pSubsampleWinding -= pRes;
}
} else {
pRes ^= pInvert;
pSubsampleWinding ^= pRes;
}
(*pixel) = sk_bit_cast<SwarPixel>(pSubsampleWinding);
}
// The inversion masks could maybe be moved into templating, but for now simply expose them
// as function arguments and rely on the compiler's DCE to optimize them.
SK_ALWAYS_INLINE void processRowSpan(int32_t row,
uint8_t leftMask, uint8_t rightMask,
bool leftInvert, bool midInvert, bool rightInvert,
bool crossedTop,
const std::array<float, kTileHeight + 1>& rowInt,
const LineStepParams& params,
bool canonicalYDir) {
float pTopX = rowInt[row];
float pBotX = rowInt[row + 1];
float xMin = std::fmin(pTopX, pBotX);
float xMax = std::fmax(pTopX, pBotX);
int32_t xStart = std::clamp(static_cast<int32_t>(std::floor(xMin)), 0, kTileWidth - 1);
int32_t xEnd = std::clamp(static_cast<int32_t>(std::floor(xMax)), 0, kTileWidth - 1);
// Compute the initial translation parameter `tFixed` in 16.16 fixed-point format
// (pre-scaled by 64 * 65536) at the starting pixel (xStart, row) of the span using:
// tFixed = tBaseFixed + stepYFixed * row + stepXFixed * xStart
int32_t tFixed =
params.fTBaseFixed + (params.fStepYFixed * row) + (params.fStepXFixed * xStart);
SwarPixel* rowSubsampleWindings = fSubsampleWinding[row];
PixelBytes pMidInvert(midInvert ? 1 : 0);
if (xStart == xEnd) {
uint8_t combinedMask = leftMask & rightMask;
processPixel</*kIsEdgePixel=*/true>(&rowSubsampleWindings[xStart], combinedMask,
PixelBytes(leftInvert ? 1 : 0), tFixed,
params.fMaskRowLut, canonicalYDir);
tFixed += params.fStepXFixed;
} else {
processPixel</*kIsEdgePixel=*/true>(&rowSubsampleWindings[xStart], leftMask,
PixelBytes(leftInvert ? 1 : 0), tFixed,
params.fMaskRowLut, canonicalYDir);
tFixed += params.fStepXFixed;
for (int32_t column = xStart + 1; column < xEnd; ++column) {
processPixel</*kIsEdgePixel=*/false>(&rowSubsampleWindings[column], 0, pMidInvert,
tFixed, params.fMaskRowLut, canonicalYDir);
tFixed += params.fStepXFixed;
}
processPixel</*kIsEdgePixel=*/true>(&rowSubsampleWindings[xEnd], rightMask,
PixelBytes(rightInvert ? 1 : 0), tFixed,
params.fMaskRowLut, canonicalYDir);
tFixed += params.fStepXFixed;
}
if (crossedTop) {
uint8_t fillByte;
if constexpr (kIsWinding) {
fillByte = canonicalYDir ? 1 : 0xFF;
} else {
fillByte = 1;
}
int32_t column = xEnd + 1;
uint32_t fill32 = fillByte * 0x01010101u;
if constexpr (kTileWidth >= 8) {
while (column + 8 <= kTileWidth) {
ApplyWinding<16>(&fSubsampleWinding[row][column], fill32);
column += 8;
}
} else if constexpr (kTileWidth >= 4) {
while (column + 4 <= kTileWidth) {
ApplyWinding<8>(&fSubsampleWinding[row][column], fill32);
column += 4;
}
}
const PixelBytes pFill(fillByte);
for (; column < kTileWidth; ++column) {
PixelBytes pSubsampleWinding =
sk_bit_cast<PixelBytes>(fSubsampleWinding[row][column]);
if constexpr (kIsWinding) {
pSubsampleWinding += pFill;
} else {
pSubsampleWinding ^= pFill;
}
fSubsampleWinding[row][column] = sk_bit_cast<SwarPixel>(pSubsampleWinding);
}
}
}
SwarPixel fSubsampleWinding[kTileHeight][kTileWidth];
int32_t fCoarseWinding;
SkTDArray<Strip>* fStripBuf;
SkTDArray<uint8_t>* fAlphaBuf;
bool fIsInverse;
const Polyline& fPolyline;
const SkTDArray<uint8_t>& fMaskLut;
int32_t fLocalAlphaIdx;
#if defined(GPU_TEST_UTILS)
MsaaExactMaskObserver fObserver;
#endif
};
} // namespace skgpu::graphite
#endif // skgpu_graphite_sparse_strips_StripProcessorSimd_DEFINED