blob: 1edb5f05f9d29dc1b05b43c359d8e3211e881b71 [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_MakeStrips_DEFINED
#define skgpu_graphite_sparse_strips_MakeStrips_DEFINED
#include "include/core/SkPathTypes.h"
#include "include/private/SkTDArray.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/StripProcessorScalar.h"
#include "src/gpu/graphite/sparse_strips/StripProcessorSimd.h"
#include "src/gpu/graphite/sparse_strips/Tiler.h"
#include <utility>
namespace skgpu::graphite {
/*
* At this point in the sparse strips pipeline, the path has been stroked, flattened into a
* polyline, tiled, and sorted. Now, the tiles are consumed by `MakeStrips::*` to produce "strips,"
* a difference encoded representation of the alpha of a path. Two things are required here:
*
* 1) Coverage Resolution: Multiple line segments often intersect the exact same spatial tile.
* Because the input tiles are generated per-line-segment, the individual winding contributions
* must be combined to produce the final coverage mask for that location:
*
* Line 1 (\) Line 2 (/) Combined Mask (V)
* +----------+ +----------+ +----------+
* | \ | | / | | \ / |
* | \ | + | / | = | \ / |
* |███\ | | /███| |███\ /███|
* |████\ | | /████| |████\/████|
* +----------+ +----------+ +----------+
*
* 2) Strip Generation (Difference Encoding): Because the incoming tiles are now sorted by y, then
* x, the difference in coordinates between consecutive tiles is used to identify contiguous
* interior regions which can be rendered trivially without calculating per-pixel coverage, and
* do not require storing a full coverage mask (as they have a solid fill).
*
* 0 1 2 3 4 5
* +-----------+-----------+-----------+-----------+-----------+-----------+
* | | / |███████████|███████████| \ | |
* | Outside | / |██ Solid ██|██ Solid ██| \ | Outside |
* | (No Fill) | / Edge |██ Fill ██|██ Fill ██| Edge \ | (No Fill) |
* | | Tile |███████████|███████████| Tile | |
* +-----------+-----------+-----------+-----------+-----------+-----------+
* ^ ^
* | <-- Implicit Gap --> |
*
* A strip consists of three primary components:
* 1) Bottom-Left Coordinates (x, y): the location at the bottom left corner, of the left-most
* tile, in a run of alpha tiles.
*
* 2) Alpha Index (alphaIdx): The offset into the buffer that stores the final per-pixel coverage
* values. This is always a multiple of the tile size. The difference in alpha index between two
* successive strips is used to determine the number of alpha tiles prior to a possible fill.
*
* 3) Fill State (shouldFill): A boolean flag packed into the highest bit of the `alphaIdx`. It
* dictates whether the empty space between *this* strip and the *next* strip is inside the
* shape. If true, the renderer solid-fills the implicit gap starting from the right edge of
* this strip's final alpha tile up to the left edge of the next strip's starting tile.
*
* -------------------------------------------------------------------------------------------------
* Example Row (4x4 sized tile, 16 alphas per tile): Single-Tile Strip
* -------------------------------------------------------------------------------------------------
*
* 0 1 2 3 4 5
* +-----------+-----------+-----------+-----------+-----------+-----------+
* | | / |███████████|███████████| \ | |
* | Outside | / |██ Solid ██|██ Solid ██| \ | Outside |
* | (No Fill) | / Alpha |██ Fill ██|██ Fill ██| Alpha \ | (No Fill) |
* | | 1 Tile |███████████|███████████| 1 Tile | |
* +-----------+-----------+-----------+-----------+-----------+-----------+
* ^ ^
* | |
* [Strip A] [Strip B]
* x: 1 x: 4
* alphaIdx: 0 alphaIdx: 16
* shouldFill: 1 (true) shouldFill: 0 (false)
*
* -------------------------------------------------------------------------------------------------
* Example Row (4x4 sized tile, 16 alphas per tile): Multi-Tile Strip (Left Edge Crosses Two Tiles)
* -------------------------------------------------------------------------------------------------
*
* 0 1 2 3 4 5
* +-----------+-----------+-----------+-----------+-----------+-----------+
* | | | / |███████████|███████████| \ |
* | Outside | |/ |██ Solid ██|██ Solid ██| \ |
* | (No Fill) | Alpha / | |██ Fill ██|██ Fill ██| Alpha \ |
* | | / | 2 Tiles |███████████|███████████| 1 Tile |
* +-----------+-----------+-----------+-----------+-----------+-----------+
* ^ ^
* | |
* [Strip C] [Strip D]
* x: 1 x: 5
* alphaIdx: 32 alphaIdx: 64
* shouldFill: 1 (true) shouldFill: 0 (false)
*/
class MakeStrips {
public:
template <uint16_t kTileWidth, uint16_t kTileHeight>
static void MsaaScalar(const Tiles<kTileWidth, kTileHeight>& tileContainer,
SkTDArray<Strip>* stripBuf,
SkTDArray<uint8_t>* alphaBuf,
SkPathFillType fillType,
const Polyline& polyline,
const SkTDArray<uint8_t>& maskLut
#if defined(GPU_TEST_UTILS)
, MsaaExactMaskObserver observer = nullptr
#endif
) {
const auto& tiles = tileContainer.getTiles();
if (tiles.empty()) {
return;
}
Dispatch(fillType, [&](auto isWindingTag, bool isInverse) {
constexpr bool kIsWinding = decltype(isWindingTag)::value;
int32_t localAlphaIdx = alphaBuf->size();
StripProcessorScalar<kTileWidth, kTileHeight, kIsWinding> processor(
stripBuf, alphaBuf, isInverse, polyline, maskLut, localAlphaIdx
#if defined(GPU_TEST_UTILS)
, observer
#endif
);
TraverseCPU<kTileWidth, kTileHeight>(tileContainer, stripBuf, alphaBuf, &processor);
});
}
template <uint16_t kTileWidth, uint16_t kTileHeight>
static void MsaaSimd(const Tiles<kTileWidth, kTileHeight>& tileContainer,
SkTDArray<Strip>* stripBuf,
SkTDArray<uint8_t>* alphaBuf,
SkPathFillType fillType,
const Polyline& polyline,
const SkTDArray<uint8_t>& maskLut
#if defined(GPU_TEST_UTILS)
, MsaaExactMaskObserver observer = nullptr
#endif
) {
const auto& tiles = tileContainer.getTiles();
if (tiles.empty()) {
return;
}
Dispatch(fillType, [&](auto isWindingTag, bool isInverse) {
constexpr bool kIsWinding = decltype(isWindingTag)::value;
int32_t localAlphaIdx = alphaBuf->size();
StripProcessorSimd<kTileWidth, kTileHeight, kIsWinding> processor(
stripBuf, alphaBuf, isInverse, polyline, maskLut, localAlphaIdx
#if defined(GPU_TEST_UTILS)
, observer
#endif
);
TraverseCPU<kTileWidth, kTileHeight>(tileContainer, stripBuf, alphaBuf, &processor);
});
}
private:
template <typename F>
static SK_ALWAYS_INLINE void Dispatch(SkPathFillType fillType, F&& f) {
switch (fillType) {
case SkPathFillType::kWinding:
f(std::bool_constant</*isWinding=*/true>{}, /*isInverse=*/false);
return;
case SkPathFillType::kInverseWinding:
f(std::bool_constant</*isWinding=*/true>{}, /*isInverse=*/true);
return;
case SkPathFillType::kEvenOdd:
f(std::bool_constant</*isWinding=*/false>{}, /*isInverse=*/false);
return;
case SkPathFillType::kInverseEvenOdd:
f(std::bool_constant</*isWinding=*/false>{}, /*isInverse=*/true);
return;
}
SkUNREACHABLE;
}
// While the underlying implementation may be scalar or SIMD, the core traversal across
// the tiles is identical. To reiterate, the goal of MakeStrips is twofold:
// 1) Combine intersecting segments within the same spatial tile to resolve final coverage.
// 2) Generate the difference encoded `Strip` objects mapping out fills and gaps.
//
// To achieve this in a single pass, the traversal treats the strictly sorted tile stream
// as an event-driven state machine governed by three transition events:
//
// 1) Tile Start (`tileStart`):
// Triggered when the current tile's x or y differs from the previous tile.
// Action: All overlapping segments at the previous spatial coordinate have been processed.
// The accumulated coverage is pushed to the dense alpha buffer, and the local mask is reset.
//
// 2) Row Start (`rowStart`):
// Triggered when the current tile's y differs from the previous tile (moves to a new row).
// Action: Because strips define an implicit difference encoding bounded by two successive
// strips, advancing to a new row requires pushing a sentinel strip (`kSentinelCoord`) to
// safely terminate and cap off the final strip of the previous row.
//
// 3) Segment Start (`segStart`):
// Triggered by a `rowStart`, OR when the current tile's x coordinate skips forward by more
// than 1 (a non-contiguous gap in the same row).
// Action: A spatial gap has been found. We commit the previously tracked strip and begin a
// new one. The renderer will use the difference in alpha indices between these two strips
// to evaluate the solid fill space bounded between them.
template <uint16_t kTileWidth, uint16_t kTileHeight, typename Processor>
static SK_ALWAYS_INLINE void TraverseCPU(
const Tiles<kTileWidth, kTileHeight>& tileContainer,
SkTDArray<Strip>* stripBuf,
SkTDArray<uint8_t>* alphaBuf,
Processor* processor) {
const auto& tiles = tileContainer.getTiles();
size_t totalCount = tiles.size();
Tile prevTile = tiles[0];
Strip currentStrip(prevTile.x * kTileWidth, prevTile.y * kTileHeight,
processor->localAlphaIdx(), /*shouldFill*/false);
float prevX = static_cast<float>(prevTile.x * kTileWidth);
float prevY = static_cast<float>(prevTile.y * kTileHeight);
std::array<SkPoint, 2> tileBounds = {
SkPoint::Make(prevX, prevY),
SkPoint::Make(prevX + static_cast<float>(kTileWidth),
prevY + static_cast<float>(kTileHeight))
};
for (size_t i = 0; i < totalCount; ++i) {
Tile tile = tiles[i];
// Determine tile traversal events
bool rowStart = (tile.y != prevTile.y);
bool tileStart = (tile.x != prevTile.x || rowStart);
bool segStart = tileStart && (rowStart || (tile.x != prevTile.x + 1));
if (tileStart) {
// Moving to a new tile implies that all previous tile's coverage has been combined,
// resolve the coverage mask winding to alpha, then clear it.
processor->resolveWindingToAlpha();
if (!rowStart) {
// If we're not a row start, carry the scanline winding by seeding the coverage
// mask with the coarse winding.
processor->clearWithCoarseWinding();
}
}
if (segStart) {
// Moved to a new segment, push back the old strip.
stripBuf->push_back(currentStrip);
if (rowStart) {
// If we're starting a new row, check to see if we need to push a sentinel to
// cap the end of the last row.
if (processor->coarseWinding() != 0) {
stripBuf->push_back(Strip::MakeCap(
prevTile.y * kTileHeight,
processor->localAlphaIdx(),
processor->ShouldFill(processor->coarseWinding())));
}
// The previous row has ended, meaning that the scanline is no longer carried,
// so reset the coarse winding and clear the coverage mask.
processor->setCoarseWinding(0);
processor->clearWindingForNewRow();
}
currentStrip = Strip(tile.x * kTileWidth,
tile.y * kTileHeight,
processor->localAlphaIdx(),
processor->ShouldFill(processor->coarseWinding()));
}
prevTile = tile;
// Lazily recalculate tile bounds only if we have moved to a new tile
if (tileStart) {
float x = static_cast<float>(tile.x * kTileWidth);
float y = static_cast<float>(tile.y * kTileHeight);
tileBounds = {
SkPoint::Make(x, y),
SkPoint::Make(x + static_cast<float>(kTileWidth),
y + static_cast<float>(kTileHeight))
};
}
processor->rasterizeLineToTile(tile, tileBounds);
}
// Process the last tile and emit the final strip
processor->resolveWindingToAlpha();
stripBuf->push_back(currentStrip);
stripBuf->push_back(Strip::MakeCap(prevTile.y * kTileHeight,
processor->localAlphaIdx(),
processor->ShouldFill(processor->coarseWinding())));
// Shrink the alpha buffer to reclaim any unused capacity.
alphaBuf->resize(processor->localAlphaIdx());
}
};
} // namespace skgpu::graphite
#endif // skgpu_graphite_sparse_strips_MakeStrips_DEFINED